From 5ebcb56b65d688b9d043d107525d0986227bdff7 Mon Sep 17 00:00:00 2001 From: XingQue Date: Fri, 26 Jun 2026 03:00:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=98=9F=E4=B9=8B=E7=95=8C=E6=8E=92?= =?UTF-8?q?=E8=A1=8C=E6=A6=9C=E6=96=B0=E6=8E=A5=E5=8F=A3=20xzjphbhqsj?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 管事/组长按邀请人数排序;商家按派单流水;旧 phbhqsj 不变 --- users/urls.py | 2 + users/xzj_paihang_views.py | 344 +++++++++++++++++++++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 users/xzj_paihang_views.py diff --git a/users/urls.py b/users/urls.py index e4ede45..435939e 100644 --- a/users/urls.py +++ b/users/urls.py @@ -19,6 +19,7 @@ from .views import WechatMiniProgramLoginView, UserInfoUpdateView, DashouXinxiAP FaKuanShenSuView, FaKuanPayView,FaKuanHuitiaoView, FaKuanPayPollView, FaKuanPayFailView,KaohePayView,KaohePayCallbackView, KaohePayPollView,KaohePayFailView, TixianAssetView, GuanshiContactView,TixianQueRenAutoView, TixianShenqingV3View, TixianCallbackV3View from .tixian_shenhe_views import TixianZddkshApplyView from .paihang_views import PhbHqsjView +from .xzj_paihang_views import XzjPhbHqsjView urlpatterns = [ @@ -157,5 +158,6 @@ urlpatterns = [ path('tixianqr', TixianQueRenAutoView.as_view(), name='自动提现确认'), path('tixiancallback', csrf_exempt(TixianCallbackV3View.as_view()), name='提现回调'), path('phbhqsj', PhbHqsjView.as_view(), name='排行榜数据'), + path('xzjphbhqsj', XzjPhbHqsjView.as_view(), name='星之界排行榜数据'), path('merchant-staff/', include('merchant_ops.urls')), ] \ No newline at end of file diff --git a/users/xzj_paihang_views.py b/users/xzj_paihang_views.py new file mode 100644 index 0000000..58c94d5 --- /dev/null +++ b/users/xzj_paihang_views.py @@ -0,0 +1,344 @@ +""" +星之界(xzj) 排行榜 — 与 /yonghu/phbhqsj 独立,旧接口零改动。 + +规则摘要: +- 管事/组长:按邀请人数排序;展示无效邀请、有效人数、收入金额 +- 接单员:按成交金额排序 +- 商家:按派单流水排序(非成交额) +""" +import logging +from datetime import date + +from django.db.models import Sum +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from backend.models import ( + LeaderDailyStats, + ManagerDailyStats, + MerchantDailyStats, + PlayerDailyStats, +) +from users.business_models import User +from users.models import UserDashou, UserGuanshi, UserShangjia, UserBoss, UserZuzhang +from users.paihang_views import ( + MAX_RANK, + RIQI_OPTIONS, + SHENFEN_OPTIONS, + _fmt_value, + _resolve_date_range, +) + +logger = logging.getLogger(__name__) + +XZJ_CLUB_ID = 'xzj' + +XZJ_ROLE_CONFIG = { + 'dashou': { + 'model': PlayerDailyStats, + 'id_field': 'PlayerID', + 'sort_field': 'CompletedAmount', + 'fields': ( + 'AcceptedOrderTotal', 'CompletedOrderTotal', + 'AcceptedAmount', 'CompletedAmount', + ), + 'int_fields': frozenset({'AcceptedOrderTotal', 'CompletedOrderTotal'}), + 'response_field_map': { + 'AcceptedOrderTotal': 'jiedan_zongliang', + 'CompletedOrderTotal': 'chengjiao_zongliang', + 'AcceptedAmount': 'jiedan_zonge', + 'CompletedAmount': 'chengjiao_zonge', + }, + 'sort_response_key': 'chengjiao_zonge', + }, + 'guanshi': { + 'model': ManagerDailyStats, + 'id_field': 'ManagerID', + 'sort_field': 'InvitedPlayerCount', + 'fields': ('InvitedPlayerCount', 'RechargedPlayerCount', 'TotalIncome'), + 'int_fields': frozenset({'InvitedPlayerCount', 'RechargedPlayerCount'}), + 'response_field_map': { + 'InvitedPlayerCount': 'yaoqing_dashou_shu', + 'RechargedPlayerCount': 'chongzhi_dashou_shu', + 'TotalIncome': 'shouru_zonge', + }, + 'sort_response_key': 'yaoqing_dashou_shu', + 'wuxiao_field': 'wuxiao_yaoqing_dashou_shu', + 'youxiao_field': 'chongzhi_dashou_shu', + }, + 'zuzhang': { + 'model': LeaderDailyStats, + 'id_field': 'LeaderID', + 'sort_field': 'InvitedManagerCount', + 'fields': ('InvitedManagerCount', 'CommissionAmount', 'TotalIncome'), + 'int_fields': frozenset({'InvitedManagerCount'}), + 'response_field_map': { + 'InvitedManagerCount': 'yaoqing_guanshi_shu', + 'CommissionAmount': 'fenyong_jine', + 'TotalIncome': 'shouru_zonge', + }, + 'sort_response_key': 'yaoqing_guanshi_shu', + 'wuxiao_field': 'wuxiao_yaoqing_guanshi_shu', + 'youxiao_field': 'youxiao_guanshi_shu', + }, + 'shangjia': { + 'model': MerchantDailyStats, + 'id_field': 'MerchantID', + 'sort_field': 'AssignedAmount', + 'fields': ('AssignedOrderCount', 'AssignedAmount'), + 'int_fields': frozenset({'AssignedOrderCount'}), + 'response_field_map': { + 'AssignedOrderCount': 'paifa_dingdan_shu', + 'AssignedAmount': 'paifa_jine', + }, + 'sort_response_key': 'paifa_jine', + }, +} + +XZJ_TODAY_PROFILE = { + 'dashou': { + 'profile_model': UserDashou, + 'profile_sort_field': 'jinrishouyi', + 'field_map': { + 'AcceptedOrderTotal': 'jinrijiedan', + 'CompletedOrderTotal': 'jinrijiedan', + 'AcceptedAmount': 'jinrishouyi', + 'CompletedAmount': 'jinrishouyi', + }, + }, + 'guanshi': { + 'profile_model': UserGuanshi, + 'profile_sort_field': 'yaogingshuliang', + 'field_map': { + 'InvitedPlayerCount': 'yaogingshuliang', + 'RechargedPlayerCount': 'jinrichongzhi', + 'TotalIncome': 'chongzhifenrun', + }, + }, + 'zuzhang': { + 'profile_model': UserZuzhang, + 'profile_sort_field': 'yaoqing_zongshu', + 'field_map': { + 'InvitedManagerCount': 'yaoqing_zongshu', + 'CommissionAmount': 'jinri_fenyong', + 'TotalIncome': 'jinri_fenyong', + }, + }, + 'shangjia': { + 'profile_model': UserShangjia, + 'profile_sort_field': 'jinriliushui', + 'field_map': { + 'AssignedOrderCount': 'jinridingdan', + 'AssignedAmount': 'jinriliushui', + }, + }, +} + +# UserZuzhang 用于今日组长 profile 回退 + + +def _apply_guanshi_wuxiao(item): + invited = int(item.get('yaoqing_dashou_shu') or 0) + valid = int(item.get('chongzhi_dashou_shu') or 0) + item['wuxiao_yaoqing_dashou_shu'] = max(0, invited - valid) + return item + + +def _count_youxiao_guanshi_for_zuzhang(zuzhang_uid, start_date=None, end_date=None): + """组长旗下有充值打手记录的管事人数。""" + guanshi_uids = list( + UserGuanshi.objects.filter(yaoqingren=zuzhang_uid) + .values_list('user__UserUID', flat=True) + ) + if not guanshi_uids: + return 0 + qs = ManagerDailyStats.objects.filter( + club_id=XZJ_CLUB_ID, + ManagerID__in=guanshi_uids, + ) + if start_date and end_date: + qs = qs.filter(Date__gte=start_date, Date__lte=end_date) + qs = ( + qs.values('ManagerID') + .annotate(total_rc=Sum('RechargedPlayerCount')) + .filter(total_rc__gt=0) + ) + return qs.count() + + +def _apply_zuzhang_wuxiao(item, start_date=None, end_date=None): + zuzhang_uid = item.get('yonghuid') + invited = int(item.get('yaoqing_guanshi_shu') or 0) + youxiao = _count_youxiao_guanshi_for_zuzhang(zuzhang_uid, start_date, end_date) + item['youxiao_guanshi_shu'] = youxiao + item['wuxiao_yaoqing_guanshi_shu'] = max(0, invited - youxiao) + return item + + +class XzjPhbHqsjView(APIView): + """ + 星之界排行榜 + POST /yonghu/xzjphbhqsj + 参数同 phbhqsj:shenfen, riqi + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + shenfen = (request.data.get('shenfen') or '').strip() + riqi = (request.data.get('riqi') or '').strip() + + if shenfen not in SHENFEN_OPTIONS: + return Response({ + 'code': 400, + 'msg': '参数错误:shenfen 必须为 dashou/guanshi/zuzhang/shangjia', + 'data': {'list': []}, + }) + if riqi not in RIQI_OPTIONS: + return Response({ + 'code': 400, + 'msg': '参数错误:riqi 必须为 今日/本周/本月/总榜/昨日/上周/上月', + 'data': {'list': []}, + }) + + date_range = None + if riqi != '总榜': + date_range = _resolve_date_range(riqi) + if date_range is None: + return Response({ + 'code': 400, + 'msg': '参数错误:无效的日期范围', + 'data': {'list': []}, + }) + + try: + if riqi == '总榜': + rows = self._query_rows(shenfen, all_time=True) + start_date, end_date = None, None + else: + start_date, end_date = date_range + rows = self._query_rows(shenfen, start_date, end_date) + + nick_map, avatar_map = self._load_user_display(rows, shenfen) + cfg = XZJ_ROLE_CONFIG[shenfen] + int_fields = cfg['int_fields'] + id_field = cfg['id_field'] + response_field_map = cfg.get('response_field_map', {}) + + result_list = [] + for idx, row in enumerate(rows): + yonghuid = row[id_field] + item = { + 'mingci': idx + 1, + 'yonghuid': yonghuid, + 'nicheng': nick_map.get(yonghuid, '用户'), + 'touxiang': avatar_map.get(yonghuid, '') or '', + } + for field in cfg['fields']: + response_key = response_field_map.get(field, field) + item[response_key] = _fmt_value(row.get(field), field in int_fields) + + if shenfen == 'guanshi': + _apply_guanshi_wuxiao(item) + elif shenfen == 'zuzhang': + _apply_zuzhang_wuxiao(item, start_date, end_date) + + result_list.append(item) + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'list': result_list, + 'club_id': XZJ_CLUB_ID, + 'sort_field': cfg.get('sort_response_key'), + }, + }) + except Exception as e: + logger.error('星之界排行榜失败 shenfen=%s riqi=%s: %s', shenfen, riqi, e, exc_info=True) + return Response({ + 'code': 500, + 'msg': '服务器内部错误', + 'data': {'list': []}, + }) + + def _query_rows(self, shenfen, start_date=None, end_date=None, all_time=False): + cfg = XZJ_ROLE_CONFIG[shenfen] + model = cfg['model'] + sort_field = cfg['sort_field'] + fields = cfg['fields'] + id_field = cfg['id_field'] + annotate_kwargs = {f: Sum(f) for f in fields} + base_qs = model.objects.filter(club_id=XZJ_CLUB_ID) + + if all_time: + return list( + base_qs.values(id_field) + .annotate(**annotate_kwargs) + .order_by(f'-{sort_field}')[:MAX_RANK] + ) + + if start_date == end_date: + qs = base_qs.filter(Date=start_date).order_by(f'-{sort_field}')[:MAX_RANK] + rows = [ + {id_field: getattr(r, id_field), **{f: getattr(r, f) for f in fields}} + for r in qs + ] + if rows: + return rows + if start_date == date.today(): + return self._query_today_profiles(shenfen) + return rows + + return list( + base_qs.filter(Date__gte=start_date, Date__lte=end_date) + .values(id_field) + .annotate(**annotate_kwargs) + .order_by(f'-{sort_field}')[:MAX_RANK] + ) + + def _query_today_profiles(self, shenfen): + cfg = XZJ_ROLE_CONFIG[shenfen] + today_cfg = XZJ_TODAY_PROFILE[shenfen] + profile_model = today_cfg['profile_model'] + profile_sort = today_cfg['profile_sort_field'] + field_map = today_cfg['field_map'] + id_field = cfg['id_field'] + fields = cfg['fields'] + + qs = profile_model.objects.filter( + **{f'{profile_sort}__gt': 0} + ).select_related('user').order_by(f'-{profile_sort}')[:MAX_RANK] + + rows = [] + for p in qs: + uid = p.user.UserUID + row = {id_field: uid} + for f in fields: + profile_field = field_map.get(f) + row[f] = getattr(p, profile_field, 0) if profile_field else 0 + rows.append(row) + return rows + + def _load_user_display(self, rows, shenfen): + id_field = XZJ_ROLE_CONFIG[shenfen]['id_field'] + yonghuids = [r[id_field] for r in rows if r.get(id_field)] + if not yonghuids: + return {}, {} + + users = User.objects.filter(UserUID__in=yonghuids) + avatar_map = {u.UserUID: u.Avatar or '' for u in users} + + nick_map = {} + if shenfen == 'dashou': + for p in UserDashou.objects.filter(user__UserUID__in=yonghuids).select_related('user'): + nick_map[p.user.UserUID] = p.nicheng or '用户' + elif shenfen == 'shangjia': + for p in UserShangjia.objects.filter(user__UserUID__in=yonghuids).select_related('user'): + nick_map[p.user.UserUID] = p.nicheng or '用户' + else: + for b in UserBoss.objects.filter(user__UserUID__in=yonghuids).select_related('user'): + nick_map[b.user.UserUID] = b.nickname or '用户' + + return nick_map, avatar_map