From c42f97908212b2f9e4bb45be08be2664f9808aab Mon Sep 17 00:00:00 2001 From: XingQue Date: Wed, 24 Jun 2026 06:07:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(jituan):=20=E8=AE=A2=E5=8D=95=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E4=BF=AE=E5=A4=8D=E3=80=81=E8=B4=A2=E5=8A=A1=20club?= =?UTF-8?q?=20=E8=BF=87=E6=BB=A4=E3=80=81=E6=95=B0=E6=8D=AE=E8=8C=83?= =?UTF-8?q?=E5=9B=B4=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commands/backfill_order_club_id.py | 41 +++++++++++++ jituan/services/caiwu_stats.py | 49 ++++----------- jituan/services/club_context.py | 28 ++++++++- jituan/urls.py | 2 + jituan/views.py | 61 ++++++++++++++++++- users/views.py | 46 +++++++------- 6 files changed, 161 insertions(+), 66 deletions(-) create mode 100644 jituan/management/commands/backfill_order_club_id.py diff --git a/jituan/management/commands/backfill_order_club_id.py b/jituan/management/commands/backfill_order_club_id.py new file mode 100644 index 0000000..531df0a --- /dev/null +++ b/jituan/management/commands/backfill_order_club_id.py @@ -0,0 +1,41 @@ +"""回填订单 ClubID(空值设为 xq)。""" +from django.core.management.base import BaseCommand +from django.db import transaction + +from jituan.constants import CLUB_ID_DEFAULT +from orders.models import Order + + +class Command(BaseCommand): + help = '回填 Order.ClubID:空或缺省设为 xq' + + def add_arguments(self, parser): + parser.add_argument( + '--dry-run', + action='store_true', + help='仅统计不落库', + ) + + def handle(self, *args, **options): + dry_run = options.get('dry_run', False) + updated = 0 + skipped = 0 + + for order in Order.query.all().only('id', 'OrderID', 'ClubID'): + current = getattr(order, 'ClubID', None) or '' + if current: + skipped += 1 + continue + + if dry_run: + self.stdout.write(f'[dry-run] {order.OrderID} -> ClubID={CLUB_ID_DEFAULT}') + updated += 1 + continue + + with transaction.atomic(): + order.ClubID = CLUB_ID_DEFAULT + order.save(update_fields=['ClubID']) + updated += 1 + + suffix = '(dry-run)' if dry_run else '' + self.stdout.write(self.style.SUCCESS(f'完成{suffix}:更新 {updated},跳过 {skipped}')) diff --git a/jituan/services/caiwu_stats.py b/jituan/services/caiwu_stats.py index 06df6e1..6b11775 100644 --- a/jituan/services/caiwu_stats.py +++ b/jituan/services/caiwu_stats.py @@ -5,8 +5,13 @@ from decimal import Decimal from django.db.models import Sum -from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope -from jituan.constants import DATA_SCOPE_ALL, CLUB_ID_DEFAULT +from jituan.services.club_context import ( + filter_club_char_field, + filter_queryset_by_club, + resolve_club_id_from_request, + resolve_club_scope, +) +from jituan.constants import DATA_SCOPE_ALL from config.models import DailyIncomeStat, DailyPayoutStat, Szjilu from orders.models import Order @@ -50,53 +55,23 @@ def _build_szjilu_payload(request): def _order_qs(request): - qs = Order.query.all() - if resolve_club_scope(request) == DATA_SCOPE_ALL: - return qs - club_id = resolve_club_id_from_request(request) - if hasattr(Order, 'ClubID'): - return qs.filter(ClubID=club_id) - return qs + return filter_queryset_by_club(Order.query.all(), request, club_field='ClubID') def _czjilu_qs(request): - qs = Czjilu.query.all() - if resolve_club_scope(request) == DATA_SCOPE_ALL: - return qs - club_id = resolve_club_id_from_request(request) - if hasattr(Czjilu, 'club_id'): - return qs.filter(club_id=club_id) - return qs + return filter_club_char_field(Czjilu.query.all(), request, field='club_id') def _huiyuangoumai_qs(request): - qs = Huiyuangoumai.query.all() - if resolve_club_scope(request) == DATA_SCOPE_ALL: - return qs - club_id = resolve_club_id_from_request(request) - if hasattr(Huiyuangoumai, 'club_id'): - return qs.filter(club_id=club_id) - return qs + return filter_club_char_field(Huiyuangoumai.query.all(), request, field='club_id') def _daily_income_qs(request): - qs = DailyIncomeStat.query.all() - if resolve_club_scope(request) == DATA_SCOPE_ALL: - return qs - club_id = resolve_club_id_from_request(request) - if hasattr(DailyIncomeStat, 'club_id'): - return qs.filter(club_id=club_id) - return qs + return filter_club_char_field(DailyIncomeStat.query.all(), request, field='club_id') def _daily_payout_qs(request): - qs = DailyPayoutStat.query.all() - if resolve_club_scope(request) == DATA_SCOPE_ALL: - return qs - club_id = resolve_club_id_from_request(request) - if hasattr(DailyPayoutStat, 'club_id'): - return qs.filter(club_id=club_id) - return qs + return filter_club_char_field(DailyPayoutStat.query.all(), request, field='club_id') def build_caiwu_payload(request): diff --git a/jituan/services/club_context.py b/jituan/services/club_context.py index 102f73d..4dabfdc 100644 --- a/jituan/services/club_context.py +++ b/jituan/services/club_context.py @@ -46,18 +46,40 @@ def get_active_club(club_id): return None +def filter_club_char_field(qs, request, field='club_id'): + """按 club_id 字符字段过滤;xq 视图兼容历史空值。""" + scope = resolve_club_scope(request) + if scope == DATA_SCOPE_ALL: + return qs + if not hasattr(qs, 'filter'): + return qs + club_id = resolve_club_id_from_request(request) + if club_id == CLUB_ID_DEFAULT: + from django.db.models import Q + return qs.filter( + Q(**{field: club_id}) | Q(**{f'{field}__isnull': True}) | Q(**{field: ''}) + ) + return qs.filter(**{field: club_id}) + + def filter_queryset_by_club(qs, request, club_field='club_id'): """ 按请求上下文过滤 QuerySet / FluentQuery。 集团 ALL_CLUBS 视图不过滤;子公司 SINGLE_CLUB 按 club_id 过滤。 + 历史订单 ClubID 为空时视为 xq,避免列表被滤空。 """ scope = resolve_club_scope(request) club_id = resolve_club_id_from_request(request) if scope == DATA_SCOPE_ALL: return qs - if hasattr(qs, 'filter'): - return qs.filter(**{club_field: club_id}) - return qs + if not hasattr(qs, 'filter'): + return qs + if club_field == 'ClubID' and club_id == CLUB_ID_DEFAULT: + from django.db.models import Q + return qs.filter( + Q(ClubID=club_id) | Q(ClubID__isnull=True) | Q(ClubID='') + ) + return qs.filter(**{club_field: club_id}) def club_id_for_write(request): diff --git a/jituan/urls.py b/jituan/urls.py index 58fdc22..173d492 100644 --- a/jituan/urls.py +++ b/jituan/urls.py @@ -18,6 +18,7 @@ from backend.view import ( from users.views import KefuPunishmentListView from jituan.views_display import ClubGonggaoView, ClubLunboListView, ClubLunboManageView from jituan.views import ( + ClubAdminAssignmentListView, ClubAuditLogListView, ClubCaiwuView, ClubChongzhiFinanceView, @@ -39,6 +40,7 @@ urlpatterns = [ path('auth/kefu-login', ClubKefuLoginView.as_view(), name='jituan_kefu_login'), path('auth/me-context', ClubMeContextView.as_view(), name='jituan_me_context'), path('auth/dashou-register', ClubDashouRegisterView.as_view(), name='jituan_dashou_register'), + path('houtai/admin-assignments', ClubAdminAssignmentListView.as_view(), name='jituan_admin_assignments'), path('houtai/caiwu', ClubCaiwuView.as_view(), name='jituan_caiwu'), path('houtai/szxx', ClubSzxxView.as_view(), name='jituan_szxx'), path('houtai/audit-log', ClubAuditLogListView.as_view(), name='jituan_audit_log'), diff --git a/jituan/views.py b/jituan/views.py index c48e192..5e22dfc 100644 --- a/jituan/views.py +++ b/jituan/views.py @@ -9,7 +9,8 @@ from rest_framework.views import APIView from rest_framework.throttling import AnonRateThrottle from rest_framework_simplejwt.tokens import RefreshToken -from jituan.models import Club +from jituan.models import Club, AdminAssignment +from jituan.constants import ADMIN_ROLE_LABELS from jituan.services.admin_context import build_admin_club_context from jituan.services.wechat_login import login_or_register_by_club from jituan.services.caiwu_stats import build_caiwu_payload @@ -193,6 +194,64 @@ class ClubMeContextView(APIView): }) +class ClubAdminAssignmentListView(APIView): + """POST /jituan/houtai/admin-assignments — 查看数据范围任职(非 gvsdsdk 功能权限)。""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = (request.data.get('phone') or request.data.get('username') or '').strip() + if not username: + return Response({'code': 401, 'msg': '缺少账号'}, status=401) + kefu_obj, permissions = verify_kefu_permission(request, username) + if kefu_obj is None: + return permissions + + user = request.user + is_super = bool(user.IsSuperuser) or user.UserType == 'admin' + if is_super: + qs = AdminAssignment.query.filter(status=1).order_by('yonghuid', '-is_primary', 'club_id') + else: + qs = AdminAssignment.query.filter(yonghuid=user.UserUID, status=1).order_by('-is_primary') + + club_name_map = { + c.club_id: c.name for c in Club.query.filter(status=1) + } + uids = list({a.yonghuid for a in qs}) + user_map = {} + if uids: + for u in User.query.filter(UserUID__in=uids).only('UserUID', 'Phone', 'UserName'): + user_map[u.UserUID] = u.Phone or u.UserName or u.UserUID + + rows = [] + for a in qs: + club_label = '集团(全部子公司)' + if a.club_id: + club_label = club_name_map.get(a.club_id, a.club_id) + rows.append({ + 'yonghuid': a.yonghuid, + 'phone': user_map.get(a.yonghuid, ''), + 'club_id': a.club_id, + 'club_label': club_label, + 'role_code': a.role_code, + 'role_name': ADMIN_ROLE_LABELS.get(a.role_code, a.role_code), + 'data_scope': a.data_scope, + 'is_primary': a.is_primary, + }) + + return Response({ + 'code': 0, + 'msg': 'ok', + 'data': { + 'list': rows, + 'perm_note': ( + '功能菜单权限(能进哪些页面)在「角色管理」配置;' + '本表 admin_assignment 仅控制集团/子公司数据范围。' + ), + 'current_context': build_admin_club_context(user), + }, + }) + + class ClubCaiwuView(APIView): """ 俱乐部维度财务(新接口)。 diff --git a/users/views.py b/users/views.py index 1cb8a1b..9ef18cc 100644 --- a/users/views.py +++ b/users/views.py @@ -7100,19 +7100,17 @@ class KefuGetOrderListView(APIView): '6': platform_orders.filter(Status=6).count(), } - # ---------- 分页查询 ---------- - total_count = club_orders.filter(q_conditions).count() - - orders_qs = club_orders.filter(q_conditions).select_related( - 'pingtai_kuozhan' - ).only( - 'OrderID', 'Description', 'Status', 'ProductTypeID', 'Platform', - 'Amount', 'ImageURL', 'CreateTime', 'Nickname', 'PlayerID', - 'pingtai_kuozhan__BossID' - ).order_by('-CreateTime')[(page-1)*page_size : page*page_size] + # ---------- 分页查询(避免 only+select_related 导致列表为空) ---------- + list_qs = club_orders.filter(q_conditions).select_related('pingtai_kuozhan').order_by('-CreateTime') + total_count = list_qs.count() + orders_page = list_qs[(page - 1) * page_size: page * page_size].to_list() # ---------- 获取老板昵称 ---------- - laoban_ids = [o.pingtai_kuozhan.BossID for o in orders_qs if hasattr(o, 'pingtai_kuozhan') and o.pingtai_kuozhan.BossID] + laoban_ids = [ + o.pingtai_kuozhan.BossID + for o in orders_page + if getattr(o, 'pingtai_kuozhan', None) and o.pingtai_kuozhan.BossID + ] nickname_map = {} if laoban_ids: users = User.query.filter(UserUID__in=laoban_ids).select_related('BossProfile').only( @@ -7122,7 +7120,7 @@ class KefuGetOrderListView(APIView): nickname_map[user.UserUID] = (user.BossProfile.nickname or '') if getattr(user, 'BossProfile', None) else '' result_list = [] - for order in orders_qs: + for order in orders_page: ext = getattr(order, 'pingtai_kuozhan', None) laoban_id = ext.BossID if ext else '' youxi_nicheng = nickname_map.get(laoban_id, order.Nickname or '') @@ -7183,8 +7181,8 @@ class KefuGetShangjiaOrderListView(APIView): def post(self, request): start_total = time.time() - # 1. 获取前端参数 - username = request.data.get('username', '').strip() + # 1. 获取前端参数(兼容 phone / username) + username = request.data.get('username', '').strip() or request.data.get('phone', '').strip() if not username: return Response({'code': 400, 'msg': '缺少username'}) @@ -7277,18 +7275,16 @@ class KefuGetShangjiaOrderListView(APIView): } # ---------- 分页查询 ---------- - total_count = club_orders.filter(q_conditions).count() - - orders_qs = club_orders.filter(q_conditions).select_related( - 'shangjia_kuozhan' - ).only( - 'OrderID', 'Description', 'Status', 'ProductTypeID', 'Platform', - 'Amount', 'ImageURL', 'CreateTime', 'Nickname', 'PlayerID', - 'shangjia_kuozhan__MerchantID' - ).order_by('-CreateTime')[(page-1)*page_size : page*page_size] + list_qs = club_orders.filter(q_conditions).select_related('shangjia_kuozhan').order_by('-CreateTime') + total_count = list_qs.count() + orders_page = list_qs[(page - 1) * page_size: page * page_size].to_list() # ---------- 获取商家昵称 ---------- - shangjia_ids = [o.shangjia_kuozhan.MerchantID for o in orders_qs if hasattr(o, 'shangjia_kuozhan') and o.shangjia_kuozhan.MerchantID] + shangjia_ids = [ + o.shangjia_kuozhan.MerchantID + for o in orders_page + if getattr(o, 'shangjia_kuozhan', None) and o.shangjia_kuozhan.MerchantID + ] nickname_map = {} if shangjia_ids: users = User.query.filter(UserUID__in=shangjia_ids).select_related('ShopProfile').only( @@ -7298,7 +7294,7 @@ class KefuGetShangjiaOrderListView(APIView): nickname_map[user.UserUID] = user.ShopProfile.nicheng or '' if hasattr(user, 'ShopProfile') else '' result_list = [] - for order in orders_qs: + for order in orders_page: ext = getattr(order, 'shangjia_kuozhan', None) shangjia_id = ext.MerchantID if ext else '' youxi_nicheng = order.Nickname or '' # 商家订单的玩家昵称