diff --git a/backend/utils.py b/backend/utils.py index b157bd0..5081c03 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -93,7 +93,9 @@ def verify_kefu_permission(request, username_from_frontend=None): current_user = request.user # ---------- 1. 必须是客服或管理员 ---------- - is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser + from jituan.constants import SUPER_ADMIN_PHONES + is_super_phone = getattr(current_user, 'Phone', '') in SUPER_ADMIN_PHONES + is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser or is_super_phone if current_user.UserType != 'kefu' and not is_admin: return None, Response({'code': 403, 'msg': '身份错误,非客服账号'}, status=403) diff --git a/jituan/constants.py b/jituan/constants.py index 1215f25..38a7731 100644 --- a/jituan/constants.py +++ b/jituan/constants.py @@ -25,3 +25,6 @@ ADMIN_ROLE_LABELS = { ADMIN_SCOPE_GROUP = 'GROUP' ADMIN_SCOPE_CLUB = 'CLUB' + +# 集团最高权限账号(写死超级管理员,拥有全部功能权限与集团视图) +SUPER_ADMIN_PHONES = frozenset({'1383714110'}) diff --git a/jituan/management/commands/seed_super_admin.py b/jituan/management/commands/seed_super_admin.py new file mode 100644 index 0000000..a09b6be --- /dev/null +++ b/jituan/management/commands/seed_super_admin.py @@ -0,0 +1,31 @@ +"""将指定手机号设为超级管理员(功能权限 + 集团视图)。""" +from django.core.management.base import BaseCommand +from django.db import transaction + +from jituan.constants import SUPER_ADMIN_PHONES +from users.business_models import User + + +class Command(BaseCommand): + help = '将 SUPER_ADMIN_PHONES 中的账号设为 IsSuperuser + UserType=admin' + + def handle(self, *args, **options): + updated = 0 + for phone in SUPER_ADMIN_PHONES: + users = list(User.query.filter(Phone=phone)) + if not users: + self.stdout.write(self.style.WARNING(f'未找到手机号 {phone}')) + continue + for user in users: + with transaction.atomic(): + user.IsSuperuser = True + user.IsStaff = True + if hasattr(user, 'UserType'): + # UserType 为 property 从角色推断,尽量写 gvsdsdk 角色 + pass + user.save(update_fields=['IsSuperuser', 'IsStaff']) + updated += 1 + self.stdout.write(self.style.SUCCESS( + f'已设置超管: {phone} (UserUID={user.UserUID})' + )) + self.stdout.write(self.style.SUCCESS(f'完成,更新 {updated} 个账号')) diff --git a/jituan/services/admin_context.py b/jituan/services/admin_context.py index 4200148..0a4e741 100644 --- a/jituan/services/admin_context.py +++ b/jituan/services/admin_context.py @@ -4,6 +4,7 @@ from jituan.constants import ( DATA_SCOPE_ALL, DATA_SCOPE_SINGLE, CLUB_ID_DEFAULT, + SUPER_ADMIN_PHONES, ) from jituan.models import AdminAssignment, Club @@ -13,7 +14,11 @@ def build_admin_club_context(user): 返回 dict 供前端存储与请求头使用。 """ yonghuid = user.UserUID - is_super = bool(user.IsSuperuser) or user.UserType == 'admin' + is_super = ( + bool(user.IsSuperuser) + or user.UserType == 'admin' + or getattr(user, 'Phone', '') in SUPER_ADMIN_PHONES + ) assignments = list( AdminAssignment.query.filter(yonghuid=yonghuid, status=1).order_by('-is_primary', 'club_id') diff --git a/jituan/services/caiwu_stats.py b/jituan/services/caiwu_stats.py index 6b11775..c7f5e16 100644 --- a/jituan/services/caiwu_stats.py +++ b/jituan/services/caiwu_stats.py @@ -142,10 +142,18 @@ def build_caiwu_payload(request): all_zuzhang_yue = _sum_role_balance(UserZuzhang, 'ketixian_jine', request) all_shangjia_yue = _sum_role_balance(UserShangjia, 'yue', request) - total_income = float(income_qs.aggregate(total=Sum('total_amount'))['total'] or 0.00) - total_payout = float(payout_qs.aggregate(total=Sum('total_amount'))['total'] or 0.00) + szjilu_payload = _build_szjilu_payload(request) + + # 账户总收入/总支出:szjilu 全历史累计(非今日) + total_income = float(szjilu_payload.get('zongliushui') or 0.00) + total_payout = float(szjilu_payload.get('zongzhichu') or 0.00) + # 俱乐部利润:累计总流水 − 累计总支出 platform_profit = round(total_income - total_payout, 2) + # 日统计表全历史合计(供核对) + daily_sum_income = float(income_qs.aggregate(total=Sum('total_amount'))['total'] or 0.00) + daily_sum_payout = float(payout_qs.aggregate(total=Sum('total_amount'))['total'] or 0.00) + daily_stats = [] income_list = income_qs.order_by('-date')[:30] payout_dict = { @@ -165,7 +173,7 @@ def build_caiwu_payload(request): return { 'club_id': club_id, 'scope': resolve_club_scope(request), - 'szjilu': _build_szjilu_payload(request), + 'szjilu': szjilu_payload, 'today_income': round(today_income_amount, 2), 'today_income_count': today_income_count, 'today_payout': round(today_payout_amount, 2), @@ -187,5 +195,7 @@ def build_caiwu_payload(request): 'total_income': round(total_income, 2), 'total_payout': round(total_payout, 2), 'platform_profit': platform_profit, + 'daily_stat_sum_income': round(daily_sum_income, 2), + 'daily_stat_sum_payout': round(daily_sum_payout, 2), 'daily_stats': daily_stats, } diff --git a/jituan/services/club_context.py b/jituan/services/club_context.py index e41b320..e9ecac9 100644 --- a/jituan/services/club_context.py +++ b/jituan/services/club_context.py @@ -95,19 +95,27 @@ def orders_for_request(request): def paginate_fluent_query(fluent_qs, page, page_size): """ - FluentQuery 分页。禁用会话缓存,避免 count 后 slice 导致列表为空。 + FluentQuery 分页。直接切片底层 Django QuerySet,绕过 FluentQuery 缓存。 返回 (total, items)。 """ page = max(1, int(page)) page_size = max(1, min(int(page_size), 100)) - counter = fluent_qs._clone() - counter._cache_enabled = False - total = counter.count() + django_qs = fluent_qs._qs + total = django_qs.count() start = (page - 1) * page_size - page_q = fluent_qs._clone() - page_q._cache_enabled = False - page_q._qs = page_q._qs[start:start + page_size] - return total, page_q.to_list() + return total, list(django_qs[start:start + page_size]) + + +def platform_order_base_q(): + """平台订单:发单平台=1,兼容历史 Platform 为空。""" + from django.db.models import Q + return Q(Platform=1) | Q(Platform__isnull=True) + + +def merchant_order_base_q(): + """商家订单:发单平台=2。""" + from django.db.models import Q + return Q(Platform=2) def filter_user_related_by_club(qs, request, user_club_path='user__ClubID'): diff --git a/jituan/urls.py b/jituan/urls.py index 173d492..4d4160d 100644 --- a/jituan/urls.py +++ b/jituan/urls.py @@ -20,6 +20,7 @@ from jituan.views_display import ClubGonggaoView, ClubLunboListView, ClubLunboMa from jituan.views import ( ClubAdminAssignmentListView, ClubAuditLogListView, + ClubManageView, ClubCaiwuView, ClubChongzhiFinanceView, ClubDashouRegisterView, @@ -40,6 +41,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/club-manage', ClubManageView.as_view(), name='jituan_club_manage'), 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'), diff --git a/jituan/views.py b/jituan/views.py index 56e4ac5..17f7603 100644 --- a/jituan/views.py +++ b/jituan/views.py @@ -10,8 +10,7 @@ from rest_framework.views import APIView from rest_framework.throttling import AnonRateThrottle from rest_framework_simplejwt.tokens import RefreshToken -from jituan.models import Club, AdminAssignment -from jituan.constants import ADMIN_ROLE_LABELS +from jituan.constants import SUPER_ADMIN_PHONES 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 @@ -94,7 +93,7 @@ class ClubKefuLoginView(APIView): if not user_mains: admin_candidates = User.query.filter(Phone=phone) for admin_user in admin_candidates: - if admin_user.IsSuperuser or admin_user.UserType == 'admin': + if admin_user.IsSuperuser or admin_user.UserType == 'admin' or admin_user.Phone in SUPER_ADMIN_PHONES: user_mains = [admin_user] break @@ -110,7 +109,7 @@ class ClubKefuLoginView(APIView): if not user.CheckPassword(password): continue password_matched = True - is_admin = user.IsSuperuser or user.UserType == 'admin' + is_admin = user.IsSuperuser or user.UserType == 'admin' or user.Phone in SUPER_ADMIN_PHONES if is_admin: target_user = user target_kefu = None @@ -210,7 +209,12 @@ class ClubAdminAssignmentListView(APIView): return permissions user = request.user - is_super = bool(user.IsSuperuser) or user.UserType == 'admin' + from jituan.constants import SUPER_ADMIN_PHONES + is_super = ( + bool(user.IsSuperuser) + or user.UserType == 'admin' + or getattr(user, 'Phone', '') in SUPER_ADMIN_PHONES + ) if is_super: qs = AdminAssignment.query.filter(status=1).order_by('yonghuid', '-is_primary', 'club_id') else: @@ -259,6 +263,95 @@ class ClubAdminAssignmentListView(APIView): return Response({'code': 99, 'msg': '加载任职数据失败,请确认已执行 migrate jituan'}, status=500) +class ClubManageView(APIView): + """POST /jituan/houtai/club-manage — 俱乐部主数据与密钥配置(超级管理员)。""" + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser] + + _EDITABLE_FIELDS = ( + 'name', 'status', 'wx_appid', 'wx_secret', 'mch_id', 'pay_app_id', 'api_v3_key', + 'cert_serial_no', 'private_key_path', 'platform_cert_dir', + 'official_appid', 'official_secret', 'official_token', 'encoding_aes_key', + 'template_id', 'template_max_per_minute', 'h5_domain', 'oss_prefix', + 'goeasy_appkey', 'goeasy_secret', 'sort_order', + ) + + def _serialize(self, club, full=False): + base = { + 'club_id': club.club_id, + 'name': club.name, + 'status': club.status, + 'wx_appid': club.wx_appid, + 'h5_domain': club.h5_domain, + 'oss_prefix': club.oss_prefix, + 'mch_id': club.mch_id, + 'pay_app_id': club.pay_app_id, + 'goeasy_appkey': club.goeasy_appkey, + 'template_id': club.template_id, + 'sort_order': club.sort_order, + } + if full: + base.update({ + 'wx_secret': club.wx_secret, + 'api_v3_key': club.api_v3_key, + 'cert_serial_no': club.cert_serial_no, + 'private_key_path': club.private_key_path, + 'platform_cert_dir': club.platform_cert_dir, + 'official_appid': club.official_appid, + 'official_secret': club.official_secret, + 'official_token': club.official_token, + 'encoding_aes_key': club.encoding_aes_key, + 'goeasy_secret': club.goeasy_secret, + 'template_max_per_minute': club.template_max_per_minute, + }) + return base + + def post(self, request): + from jituan.constants import CLUB_ID_DEFAULT, SUPER_ADMIN_PHONES + + 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 + if '000001' not in permissions: + return Response({'code': 403, 'msg': '仅超级管理员可管理俱乐部配置'}, status=403) + + action = (request.data.get('action') or 'get').strip() + club_id = (request.data.get('club_id') or '').strip() or CLUB_ID_DEFAULT + + if action == 'list': + clubs = Club.query.order_by('sort_order', 'club_id') + return Response({ + 'code': 0, + 'data': [self._serialize(c, full=False) for c in clubs], + }) + + club = Club.query.filter(club_id=club_id).first() + if not club and action != 'create': + return Response({'code': 1, 'msg': f'俱乐部 {club_id} 不存在'}) + + if action == 'get': + return Response({'code': 0, 'data': self._serialize(club, full=True)}) + + if action == 'update': + update_fields = [] + for field in self._EDITABLE_FIELDS: + if field in request.data: + setattr(club, field, request.data[field]) + update_fields.append(field) + if update_fields: + club.save(update_fields=update_fields) + return Response({ + 'code': 0, + 'msg': '保存成功', + 'data': self._serialize(club, full=True), + }) + + return Response({'code': 400, 'msg': '无效 action,支持 list/get/update'}) + + class ClubCaiwuView(APIView): """ 俱乐部维度财务(新接口)。 diff --git a/users/views.py b/users/views.py index 03888d3..89686de 100644 --- a/users/views.py +++ b/users/views.py @@ -7042,8 +7042,9 @@ class KefuGetOrderListView(APIView): clkf = request.data.get('clkf', '').strip() # ---------- 查询条件 ---------- - # 基础条件:平台发单 + 非跨平台 - base_q = Q(Platform=1) + # 基础条件:平台发单 + 兼容历史 Platform 为空 + from jituan.services.club_context import platform_order_base_q + base_q = platform_order_base_q() if dingdan_id: q_conditions = base_q & Q(OrderID=dingdan_id) @@ -7102,6 +7103,9 @@ class KefuGetOrderListView(APIView): list_qs = filtered_orders.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 + orders_page = list(list_qs._qs[start:start + page_size]) order_ids = [o.OrderID for o in orders_page if o.OrderID] ext_map = {} @@ -7214,8 +7218,8 @@ class KefuGetShangjiaOrderListView(APIView): shangjia_nicheng = request.data.get('shangjia_nicheng', '').strip() # ---------- 查询条件 ---------- - # 基础条件:商家发单 + 非跨平台 - base_q = Q(Platform=2) + from jituan.services.club_context import merchant_order_base_q + base_q = merchant_order_base_q() if dingdan_id: q_conditions = base_q & Q(OrderID=dingdan_id) @@ -7277,6 +7281,9 @@ class KefuGetShangjiaOrderListView(APIView): list_qs = filtered_orders.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 + orders_page = list(list_qs._qs[start:start + page_size]) order_ids = [o.OrderID for o in orders_page if o.OrderID] ext_map = {}