diff --git a/backend/view.py b/backend/view.py index ae98eb7..e3a1c9a 100644 --- a/backend/view.py +++ b/backend/view.py @@ -2726,21 +2726,10 @@ class GetMemberListView(APIView): if '3300a' not in permissions: return Response({'code': 403, 'msg': '无权限查看会员列表'}) - members = Huiyuan.query.all().order_by('-CreateTime') - member_list = [] - for m in members: - member_list.append({ - 'huiyuan_id': m.huiyuan_id, - 'jieshao': m.jieshao, - 'jtjieshao': m.jtjieshao, - 'jiage': str(m.jiage), - 'guanshifc': str(m.guanshifc), - 'zuzhangfc': str(m.zuzhangfc), - 'goumai_cishu': m.goumai_cishu, - 'CreateTime': m.CreateTime.isoformat() if m.CreateTime else None, - 'UpdateTime': m.UpdateTime.isoformat() if m.UpdateTime else None, - }) - return Response({'code': 0, 'data': {'list': member_list}}) + from jituan.services.club_member_admin import build_member_list_payload + + payload = build_member_list_payload(request) + return Response({'code': 0, 'data': {'list': payload['list']}, 'club_id': payload.get('club_id'), 'scope': payload.get('scope')}) class UpdateMemberView(APIView): @@ -2790,19 +2779,13 @@ class UpdateMemberView(APIView): if '3300a' not in permissions: return Response({'code': 403, 'msg': '无权限修改会员'}) - with transaction.atomic(): - try: - member = Huiyuan.objects.select_for_update().get(huiyuan_id=huiyuan_id) - except Huiyuan.DoesNotExist: - return Response({'code': 404, 'msg': '会员不存在'}) - - member.jieshao = jieshao - member.jiage = jiage - member.guanshifc = guanshifc - member.zuzhangfc = zuzhangfc - member.jtjieshao = jtjieshao - member.save() + from jituan.services.club_member_admin import update_member_for_club + data, err = update_member_for_club( + request, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, + ) + if err: + return Response({'code': 400, 'msg': err}) return Response({'code': 0, 'msg': '修改成功'}) @@ -2851,26 +2834,14 @@ class AddMemberView(APIView): if '3300a' not in permissions: return Response({'code': 403, 'msg': '无权限添加会员'}) - # 生成唯一6位会员ID(数字+字母) - def generate_huiyuan_id(): - while True: - new_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) - if not Huiyuan.query.filter(huiyuan_id=new_id).exists(): - return new_id + from jituan.services.club_member_admin import add_member_for_club - with transaction.atomic(): - new_id = generate_huiyuan_id() - member = Huiyuan.query.create( - huiyuan_id=new_id, - jieshao=jieshao, - jiage=jiage, - guanshifc=guanshifc, - zuzhangfc=zuzhangfc, - jtjieshao=jtjieshao, - goumai_cishu=0 - ) - - return Response({'code': 0, 'msg': '添加成功', 'data': {'huiyuan_id': member.huiyuan_id}}) + data, err = add_member_for_club( + request, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, + ) + if err: + return Response({'code': 400, 'msg': err}) + return Response({'code': 0, 'msg': '添加成功', 'data': data}) @@ -6755,14 +6726,34 @@ class HqbkxxView(APIView): 'bankuai_id': sp.bankuai_id }) - # 7. 查询所有会员 + # 7. 查询会员(附带当前俱乐部售价) + from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL + from jituan.models import ClubHuiyuanPrice + from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope + + scope = resolve_club_scope(request) + club_id = resolve_club_id_from_request(request) + price_map = {} + if scope != DATA_SCOPE_ALL: + for row in ClubHuiyuanPrice.query.filter(club_id=club_id): + price_map[row.huiyuan_id] = row + huiyuan_list = [] for hy in Huiyuan.query.all(): - huiyuan_list.append({ + row = price_map.get(hy.huiyuan_id) + if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT and row is None: + continue + if row is not None and not row.is_enabled: + continue + item = { 'huiyuan_id': hy.huiyuan_id, 'jieshao': hy.jieshao, - 'bankuai_id': hy.bankuai_id - }) + 'bankuai_id': hy.bankuai_id, + 'jiage': str(row.jiage) if row else str(hy.jiage), + 'guanshifc': str(row.guanshifc) if row else str(hy.guanshifc), + 'zuzhangfc': str(row.zuzhangfc) if row else str(hy.zuzhangfc), + } + huiyuan_list.append(item) return Response({ 'code': 0, @@ -6770,7 +6761,9 @@ class HqbkxxView(APIView): 'data': { 'bankuai': bankuai_list, 'shangpin_leixing': shangpin_leixing_list, - 'huiyuan': huiyuan_list + 'huiyuan': huiyuan_list, + 'club_id': club_id, + 'scope': scope, } }) @@ -6935,131 +6928,14 @@ class CaiwuView(APIView): if 'caiwu' not in permissions: return Response({'code': 403, 'msg': '无财务查看权限'}, status=403) - today = date.today() - today_start = datetime.combine(today, datetime.min.time()) - today_end = today_start + timedelta(days=1) - - from jituan.services.club_context import ( - filter_club_char_field, - filter_queryset_by_club, - resolve_club_id_from_request, - resolve_club_scope, - ) - from jituan.services.daily_finance_display import ( - cumulative_income_payout, - daily_stats_for_request, - sum_payout_stat_for_date, - today_income_for_dashboard, - ) - - order_qs = filter_queryset_by_club(Order.query.all(), request, club_field='ClubID') - - # 四个板子:按 Header 俱乐部范围读 daily_income_stat / daily_payout_stat - today_income_amount, today_income_count = today_income_for_dashboard(today, request) - today_payout_amount, today_payout_count = sum_payout_stat_for_date(today, request) - - # 今日订单(已付款及之后的状态) - today_orders = order_qs.filter( - CreateTime__gte=today_start, - CreateTime__lt=today_end, - Status__in=[1,2,3,4,5,6,7,8] - ) - today_order_count = today_orders.count() - today_order_amount = float(today_orders.aggregate(total=Sum('Amount'))['total'] or 0.00) - - # 今日成交(Status=3) - today_completed = Order.query.filter( - CreateTime__gte=today_start, - CreateTime__lt=today_end, - Status=3 - ) - today_completed_count = today_completed.count() - today_completed_amount = float(today_completed.aggregate(total=Sum('Amount'))['total'] or 0.00) - - # 今日退款(Status=5) - today_tuikuan = Order.query.filter( - CreateTime__gte=today_start, - CreateTime__lt=today_end, - Status=5 - ) - today_tuikuan_count = today_tuikuan.count() - today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('Amount'))['total'] or 0.00) - - # 今日充值(会员充值,leixing=1, zhuangtai=3) - cz_qs = filter_club_char_field(Czjilu.query.all(), request, field='club_id') - today_chongzhi = cz_qs.filter( - CreateTime__gte=today_start, - CreateTime__lt=today_end, - leixing=1, - zhuangtai=3 - ) - today_chongzhi_count = today_chongzhi.count() - today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00) - - # 今日新增会员 - hy_qs = filter_club_char_field(Huiyuangoumai.query.all(), request, field='club_id') - new_huiyuan_user_ids = hy_qs.filter( - CreateTime__gte=today_start, - CreateTime__lt=today_end - ).values_list('yonghu_id', flat=True).distinct() - today_new_huiyuan = len(new_huiyuan_user_ids) - - # 新增会员充值总额(这些用户今天的会员充值金额) - today_new_huiyuan_amount = 0.0 - if new_huiyuan_user_ids: - amount_sum = cz_qs.filter( - CreateTime__gte=today_start, - CreateTime__lt=today_end, - yonghuid__in=new_huiyuan_user_ids, - leixing=1, - zhuangtai=3 - ).aggregate(total=Sum('jine')) - today_new_huiyuan_amount = float(amount_sum['total'] or 0.0) - - # 各角色可提现余额总和 - all_guanshi_yue = float(UserGuanshi.query.aggregate(total=Sum('yue'))['total'] or 0.00) - all_dashou_yue = float(UserDashou.query.aggregate(total=Sum('yue'))['total'] or 0.00) - all_zuzhang_yue = float(UserZuzhang.query.aggregate(total=Sum('ketixian_jine'))['total'] or 0.00) - all_shangjia_yue = float(UserShangjia.query.aggregate(total=Sum('yue'))['total'] or 0.00) - - # 累计收支 / 利润(当前俱乐部视图范围) - total_income, total_payout, platform_profit = cumulative_income_payout(request) - - daily_stats = daily_stats_for_request(request, days=30) - - return Response({ - 'code': 0, - 'msg': 'ok', - 'data': { - 'club_id': resolve_club_id_from_request(request), - 'scope': resolve_club_scope(request), - 'today_income': round(today_income_amount, 2), - 'today_income_count': today_income_count, - 'today_payout': round(today_payout_amount, 2), - 'today_payout_count': today_payout_count, - 'today_order_count': today_order_count, - 'today_order_amount': round(today_order_amount, 2), - 'today_completed_count': today_completed_count, - 'today_completed_amount': round(today_completed_amount, 2), - 'today_tuikuan_count': today_tuikuan_count, - 'today_tuikuan_amount': round(today_tuikuan_amount, 2), - 'today_chongzhi_count': today_chongzhi_count, - 'today_chongzhi_amount': round(today_chongzhi_amount, 2), - 'today_new_huiyuan': today_new_huiyuan, - 'today_new_huiyuan_amount': round(today_new_huiyuan_amount, 2), - 'all_guanshi_yue': round(all_guanshi_yue, 2), - 'all_dashou_yue': round(all_dashou_yue, 2), - 'all_zuzhang_yue': round(all_zuzhang_yue, 2), - 'all_shangjia_yue': round(all_shangjia_yue, 2), - 'total_income': round(total_income, 2), - 'total_payout': round(total_payout, 2), - 'platform_profit': platform_profit, - 'daily_stats': daily_stats - } - }) - - + from jituan.services.caiwu_stats import build_caiwu_payload + try: + data = build_caiwu_payload(request) + return Response({'code': 0, 'msg': 'ok', 'data': data}) + except Exception: + logger.exception('CaiwuView 异常') + return Response({'code': 99, 'msg': '统计失败'}, status=500) class CwhybkhqView(APIView): diff --git a/jituan/management/commands/seed_club_xzj.py b/jituan/management/commands/seed_club_xzj.py new file mode 100644 index 0000000..0014ea7 --- /dev/null +++ b/jituan/management/commands/seed_club_xzj.py @@ -0,0 +1,41 @@ +"""一键初始化星之界(xzj)俱乐部及客服数据范围。""" +from django.core.management import call_command +from django.core.management.base import BaseCommand, CommandError + +from jituan.models import Club + + +class Command(BaseCommand): + help = '创建星之界电竞俱乐部(xzj)并分配客服数据范围(需先 seed_club_xq)' + + def add_arguments(self, parser): + parser.add_argument( + '--wx-appid', default='wxdefa454152e78a03', help='星之界小程序 appid', + ) + parser.add_argument( + '--skip-assignments', action='store_true', help='跳过客服 admin_assignment', + ) + + def handle(self, *args, **options): + wx_appid = (options.get('wx_appid') or '').strip() + club_id = 'xzj' + name = '星之界电竞' + + if Club.query.filter(club_id=club_id).exists(): + self.stdout.write(self.style.WARNING(f'俱乐部 {club_id} 已存在,跳过 create_club')) + else: + self.stdout.write(f'创建俱乐部 {club_id} ...') + call_command( + 'create_club', + club_id, + name=name, + wx_appid=wx_appid, + ) + + if not options.get('skip_assignments'): + self.stdout.write('分配客服数据范围 ...') + call_command('seed_admin_assignments', club_id=club_id) + + self.stdout.write(self.style.SUCCESS( + f'星之界 {club_id} 就绪。请在后台「俱乐部配置」检查支付密钥,并在 xzj 视图下单独改轮播/会员价。' + )) diff --git a/jituan/services/caiwu_stats.py b/jituan/services/caiwu_stats.py index 29e87ae..de1b008 100644 --- a/jituan/services/caiwu_stats.py +++ b/jituan/services/caiwu_stats.py @@ -3,6 +3,7 @@ from datetime import date, datetime, timedelta from django.db.models import Sum +from jituan.constants import DATA_SCOPE_ALL from jituan.services.club_context import ( filter_club_char_field, filter_queryset_by_club, diff --git a/jituan/services/club_member_admin.py b/jituan/services/club_member_admin.py new file mode 100644 index 0000000..9edfd9a --- /dev/null +++ b/jituan/services/club_member_admin.py @@ -0,0 +1,144 @@ +"""后台会员管理:全局 Huiyuan 定义 + 各俱乐部 ClubHuiyuanPrice 售价/分成。""" +import random +import string +from decimal import Decimal + +from django.db import transaction + +from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL +from jituan.models import ClubHuiyuanPrice +from jituan.services.club_context import club_id_for_write, resolve_club_id_from_request, resolve_club_scope +from jituan.services.club_user_access import list_response_meta +from products.models import Huiyuan + + +def _format_member(m, price_row=None): + item = { + 'huiyuan_id': m.huiyuan_id, + 'jieshao': m.jieshao, + 'jtjieshao': m.jtjieshao, + 'jiage': str(m.jiage), + 'guanshifc': str(m.guanshifc), + 'zuzhangfc': str(m.zuzhangfc), + 'goumai_cishu': m.goumai_cishu, + 'CreateTime': m.CreateTime.isoformat() if m.CreateTime else None, + 'UpdateTime': m.UpdateTime.isoformat() if m.UpdateTime else None, + 'bankuai_id': m.bankuai_id, + 'club_enabled': True, + } + if price_row is not None: + item['jiage'] = str(price_row.jiage) + item['guanshifc'] = str(price_row.guanshifc) + item['zuzhangfc'] = str(price_row.zuzhangfc) + item['club_enabled'] = bool(price_row.is_enabled) + return item + + +def build_member_list_payload(request): + scope = resolve_club_scope(request) + club_id = resolve_club_id_from_request(request) + members = Huiyuan.query.all().order_by('-CreateTime') + member_list = [] + + if scope == DATA_SCOPE_ALL: + for m in members: + member_list.append(_format_member(m)) + else: + price_map = { + r.huiyuan_id: r + for r in ClubHuiyuanPrice.query.filter(club_id=club_id) + } + for m in members: + row = price_map.get(m.huiyuan_id) + if row is None and club_id != CLUB_ID_DEFAULT: + continue + if row is not None and not row.is_enabled: + continue + member_list.append(_format_member(m, row)) + + return { + 'list': member_list, + 'club_id': club_id, + 'scope': scope, + **list_response_meta(request), + } + + +def update_member_for_club(request, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao): + if resolve_club_scope(request) == DATA_SCOPE_ALL: + return None, '集团汇总视图下不可修改,请切换到具体俱乐部' + + club_id = club_id_for_write(request) + + with transaction.atomic(): + try: + member = Huiyuan.objects.select_for_update().get(huiyuan_id=huiyuan_id) + except Huiyuan.DoesNotExist: + return None, '会员不存在' + + member.jieshao = jieshao + member.jtjieshao = jtjieshao + member.save(update_fields=['jieshao', 'jtjieshao', 'UpdateTime']) + + row, created = ClubHuiyuanPrice.query.get_or_create( + club_id=club_id, + huiyuan_id=huiyuan_id, + defaults={ + 'jiage': jiage, + 'guanshifc': guanshifc, + 'zuzhangfc': zuzhangfc, + 'is_enabled': True, + 'bankuai_id': member.bankuai_id, + }, + ) + if not created: + row.jiage = jiage + row.guanshifc = guanshifc + row.zuzhangfc = zuzhangfc + row.is_enabled = True + row.save() + + if club_id == CLUB_ID_DEFAULT: + member.jiage = jiage + member.guanshifc = guanshifc + member.zuzhangfc = zuzhangfc + member.save(update_fields=['jiage', 'guanshifc', 'zuzhangfc', 'UpdateTime']) + + return {'huiyuan_id': huiyuan_id}, None + + +def add_member_for_club(request, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, bankuai_id=None): + if resolve_club_scope(request) == DATA_SCOPE_ALL: + return None, '集团汇总视图下不可添加,请切换到具体俱乐部' + + club_id = club_id_for_write(request) + + def generate_huiyuan_id(): + while True: + new_id = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) + if not Huiyuan.query.filter(huiyuan_id=new_id).exists(): + return new_id + + with transaction.atomic(): + new_id = generate_huiyuan_id() + member = Huiyuan.query.create( + huiyuan_id=new_id, + jieshao=jieshao, + jiage=jiage, + guanshifc=guanshifc, + zuzhangfc=zuzhangfc, + jtjieshao=jtjieshao, + goumai_cishu=0, + bankuai_id=bankuai_id, + ) + ClubHuiyuanPrice.query.create( + club_id=club_id, + huiyuan_id=new_id, + jiage=jiage, + guanshifc=guanshifc, + zuzhangfc=zuzhangfc, + is_enabled=True, + bankuai_id=bankuai_id, + ) + + return {'huiyuan_id': member.huiyuan_id}, None diff --git a/jituan/services/club_user_access.py b/jituan/services/club_user_access.py index b4e368f..45920bd 100644 --- a/jituan/services/club_user_access.py +++ b/jituan/services/club_user_access.py @@ -1,7 +1,7 @@ -"""后台按俱乐部校验用户是否在当前数据范围内。""" +"""后台按俱乐部校验用户/订单是否在当前数据范围内。""" from rest_framework.response import Response -from jituan.constants import DATA_SCOPE_ALL +from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope @@ -25,3 +25,19 @@ def list_response_meta(request): 'club_id': resolve_club_id_from_request(request), 'scope': resolve_club_scope(request), } + + +def order_belongs_to_request(order, request): + if resolve_club_scope(request) == DATA_SCOPE_ALL: + return True + club_id = resolve_club_id_from_request(request) + order_club = (getattr(order, 'ClubID', None) or '').strip() + if club_id == CLUB_ID_DEFAULT: + return order_club in (club_id, '', None) or not order_club + return order_club == club_id + + +def forbid_if_order_out_of_scope(request, order): + if order_belongs_to_request(order, request): + return None + return Response({'code': 403, 'msg': '该订单不属于当前俱乐部数据范围'}) diff --git a/jituan/urls.py b/jituan/urls.py index 4d4160d..ed333d2 100644 --- a/jituan/urls.py +++ b/jituan/urls.py @@ -1,11 +1,13 @@ from django.urls import path from backend.view import ( + AddMemberView, FaKuanChuLiView, FaKuanChuangJianView, FaKuanLieBiaoView, FaKuanTongJiView, GetGuanliListView, + GetMemberListView, GetOperationLogListView, GetWithdrawSettingsView, GetZuzhangListView, @@ -13,6 +15,7 @@ from backend.view import ( KefuGetShangjiaListView, PopupNoticeListAPIView, PopupNoticeModifyAPIView, + UpdateMemberView, UpdateWithdrawSettingsView, ) from users.views import KefuPunishmentListView @@ -70,5 +73,8 @@ urlpatterns = [ path('houtai/glyclfk', FaKuanChuLiView.as_view(), name='jituan_penalty_action'), path('houtai/htfksc', FaKuanChuangJianView.as_view(), name='jituan_penalty_create'), path('houtai/kefu-cfgl', KefuPunishmentListView.as_view(), name='jituan_kefu_cfgl'), + path('houtai/hthqhylb', GetMemberListView.as_view(), name='jituan_member_list'), + path('houtai/htxghyxx', UpdateMemberView.as_view(), name='jituan_member_update'), + path('houtai/httjhy', AddMemberView.as_view(), name='jituan_member_add'), path('club/list', ClubListView.as_view(), name='jituan_club_list'), ] diff --git a/users/views.py b/users/views.py index 4b51846..3edb4f2 100644 --- a/users/views.py +++ b/users/views.py @@ -7084,17 +7084,17 @@ class KefuGetOrderListView(APIView): ) from orders.models import PlatformOrderExt - # 旧 /yonghu/kfhqddlb 不按俱乐部过滤,与改造前行为一致 - stats_qs = order_scope_qs(None).filter(q_conditions) + # 按俱乐部过滤订单列表 + stats_qs = order_scope_qs(request).filter(q_conditions) stats = stats_qs.aggregate( total_orders=Count('id'), completed_orders=Count('id', filter=Q(Status=3)), refund_orders=Count('id', filter=Q(Status=5)), pending_orders=Count('id', filter=Q(Status__in=[4, 8])) ) - status_counts = count_order_status_buckets(q_conditions, request=None) + status_counts = count_order_status_buckets(q_conditions, request) - list_qs = order_scope_qs(None).filter(q_conditions).order_by('-CreateTime') + list_qs = order_scope_qs(request).filter(q_conditions).order_by('-CreateTime') total_count, orders_page = paginate_fluent_query(list_qs, page, page_size) if total_count > 0 and not orders_page: start = (max(1, page) - 1) * page_size @@ -7256,16 +7256,16 @@ class KefuGetShangjiaOrderListView(APIView): ) from orders.models import MerchantOrderExt - stats_qs = order_scope_qs(None).filter(q_conditions) + stats_qs = order_scope_qs(request).filter(q_conditions) stats = stats_qs.aggregate( total_orders=Count('id'), completed_orders=Count('id', filter=Q(Status=3)), refund_orders=Count('id', filter=Q(Status=5)), pending_orders=Count('id', filter=Q(Status__in=[4, 8])) ) - status_counts = count_order_status_buckets(q_conditions, request=None) + status_counts = count_order_status_buckets(q_conditions, request) - list_qs = order_scope_qs(None).filter(q_conditions).order_by('-CreateTime') + list_qs = order_scope_qs(request).filter(q_conditions).order_by('-CreateTime') total_count, orders_page = paginate_fluent_query(list_qs, page, page_size) if total_count > 0 and not orders_page: start = (max(1, page) - 1) * page_size @@ -9118,6 +9118,11 @@ class KefuGetOrderDetailView(APIView): logger.exception(f"[kefuhqddxq] 订单查询异常 dingdan_id={dingdan_id}") return Response({'code': 500, 'msg': '服务器错误'}, status=500) + from jituan.services.club_user_access import forbid_if_order_out_of_scope + deny = forbid_if_order_out_of_scope(request, dingdan_obj) + if deny: + return deny + # 4. 构建基础数据 try: response_data = {