Files
Django/users/paihang_views.py
XingQue 1e3d65e0e4 fix: 排行榜昵称与管事/组长统计字段对齐
管事/组长不再展示 openid,改读业务昵称;俱乐部榜补充无效/有效邀请与收益字段;管事按有效人数、组长按收益总额排序。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 21:49:36 +08:00

319 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
排行榜接口 — 基于 houtai 日统计表聚合查询
POST /yonghu/phbhqsj
"""
import logging
from datetime import date, timedelta
from decimal import Decimal
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 (
PlayerDailyStats,
ManagerDailyStats,
LeaderDailyStats,
MerchantDailyStats,
)
from users.models import UserDashou, UserShangjia, UserGuanshi, UserZuzhang
from users.business_models import User
from rank.display_utils import load_rank_nicknames
logger = logging.getLogger(__name__)
MAX_RANK = 50
RIQI_OPTIONS = frozenset({'今日', '本周', '本月', '总榜', '昨日', '上周', '上月'})
SHENFEN_OPTIONS = frozenset({'dashou', 'guanshi', 'zuzhang', 'shangjia'})
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',
},
},
'guanshi': {
'model': ManagerDailyStats,
'id_field': 'ManagerID',
'sort_field': 'TotalIncome',
'fields': ('InvitedPlayerCount', 'RechargedPlayerCount', 'TotalIncome'),
'int_fields': frozenset({'InvitedPlayerCount', 'RechargedPlayerCount'}),
'response_field_map': {
'InvitedPlayerCount': 'yaoqing_dashou_shu',
'RechargedPlayerCount': 'chongzhi_dashou_shu',
'TotalIncome': 'shouru_zonge',
},
},
'zuzhang': {
'model': LeaderDailyStats,
'id_field': 'LeaderID',
'sort_field': 'TotalIncome',
'fields': ('InvitedManagerCount', 'CommissionAmount', 'TotalIncome'),
'int_fields': frozenset({'InvitedManagerCount'}),
'response_field_map': {
'InvitedManagerCount': 'yaoqing_guanshi_shu',
'CommissionAmount': 'fenyong_jine',
'TotalIncome': 'shouru_zonge',
},
},
'shangjia': {
'model': MerchantDailyStats,
'id_field': 'MerchantID',
'sort_field': 'SettledAmount',
'fields': ('AssignedOrderCount', 'AssignedAmount', 'SettledOrderCount', 'SettledAmount'),
'int_fields': frozenset({'AssignedOrderCount', 'SettledOrderCount'}),
'response_field_map': {
'AssignedOrderCount': 'paifa_dingdan_shu',
'AssignedAmount': 'paifa_jine',
'SettledOrderCount': 'jiesuan_dingdan_shu',
'SettledAmount': 'jiesuan_jine',
},
},
}
# "今日"实时数据 — 从用户Profile表读取jinri系列字段
TODAY_PROFILE_CONFIG = {
'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': 'jinrichongzhi',
'field_map': {
'InvitedPlayerCount': 'yaogingshuliang',
'RechargedPlayerCount': 'jinrichongzhi',
'TotalIncome': 'chongzhifenrun',
},
},
'zuzhang': {
'profile_model': UserZuzhang,
'profile_sort_field': 'fenyong_zonge',
'field_map': {
'InvitedManagerCount': 'yaoqing_zongshu',
'CommissionAmount': 'jinri_fenyong',
'TotalIncome': 'fenyong_zonge',
},
},
'shangjia': {
'profile_model': UserShangjia,
'profile_sort_field': 'jinriliushui',
'field_map': {
'AssignedOrderCount': 'jinridingdan',
'AssignedAmount': 'jinriliushui',
'SettledOrderCount': None,
'SettledAmount': None,
},
},
}
def _resolve_date_range(riqi: str):
"""根据前端 riqi 参数返回 (start_date, end_date) 闭区间。"""
today = date.today()
if riqi == '今日':
return today, today
if riqi == '昨日':
d = today - timedelta(days=1)
return d, d
if riqi == '本周':
monday = today - timedelta(days=today.weekday())
return monday, today
if riqi == '上周':
this_monday = today - timedelta(days=today.weekday())
last_sunday = this_monday - timedelta(days=1)
last_monday = last_sunday - timedelta(days=6)
return last_monday, last_sunday
if riqi == '本月':
return today.replace(day=1), today
if riqi == '上月':
first_this_month = today.replace(day=1)
last_day_prev = first_this_month - timedelta(days=1)
return last_day_prev.replace(day=1), last_day_prev
return None
def _fmt_value(val, is_int: bool):
if val is None:
return 0 if is_int else 0.0
if is_int:
return int(val)
if isinstance(val, Decimal):
return float(val.quantize(Decimal('0.01')))
return float(val)
class PhbHqsjView(APIView):
"""
排行榜数据接口
POST /yonghu/phbhqsj
请求参数:
shenfen: dashou | guanshi | zuzhang | shangjia
riqi: 今日 | 本周 | 本月 | 总榜 | 昨日 | 上周 | 上月
响应 data.list[]最多50条已按各身份排序字段降序:
mingci, yonghuid, nicheng, touxiang + 各身份统计字段
"""
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': []},
})
try:
if riqi == '总榜':
rows = self._query_rank_rows(shenfen, all_time=True)
else:
date_range = _resolve_date_range(riqi)
if date_range is None:
return Response({
'code': 400,
'msg': '参数错误:无效的日期范围',
'data': {'list': []},
})
rows = self._query_rank_rows(shenfen, date_range[0], date_range[1])
nick_map, avatar_map = self._load_user_display(rows, shenfen)
cfg = ROLE_CONFIG[shenfen]
int_fields = cfg['int_fields']
id_field = cfg['id_field']
result_list = []
response_field_map = cfg.get('response_field_map', {})
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)
result_list.append(item)
return Response({
'code': 200,
'msg': '获取成功',
'data': {'list': result_list},
})
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_rank_rows(self, shenfen, start_date=None, end_date=None, all_time=False):
cfg = 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}
if all_time:
rows = (
model.objects
.values(id_field)
.annotate(**annotate_kwargs)
.order_by(f'-{sort_field}')[:MAX_RANK]
)
return list(rows)
if start_date == end_date:
# "今日"/"昨日" 单日查询 — 先查日统计表
qs = model.objects.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
# 日统计表无数据 — 对于"今日"回退到用户Profile实时字段
today = date.today()
if start_date == today:
return self._query_today_from_profiles(shenfen)
return rows
rows = (
model.objects
.filter(Date__gte=start_date, Date__lte=end_date)
.values(id_field)
.annotate(**annotate_kwargs)
.order_by(f'-{sort_field}')[:MAX_RANK]
)
return list(rows)
def _query_today_from_profiles(self, shenfen):
"""从用户Profile表读取今日实时数据jinri系列字段"""
cfg = ROLE_CONFIG[shenfen]
today_cfg = TODAY_PROFILE_CONFIG[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)
if profile_field:
row[f] = getattr(p, profile_field)
else:
row[f] = 0
rows.append(row)
return rows
def _load_user_display(self, rows, shenfen):
id_field = 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 = load_rank_nicknames(shenfen, yonghuids)
return nick_map, avatar_map