diff --git a/jituan/services/huiyuan_bundle.py b/jituan/services/huiyuan_bundle.py index 8f74eac..ed31f63 100644 --- a/jituan/services/huiyuan_bundle.py +++ b/jituan/services/huiyuan_bundle.py @@ -71,12 +71,16 @@ def get_bundle_included_ids(club_id, bundle_huiyuan_id): if not club_id or not bundle_huiyuan_id: return [] - return list( - ClubHuiyuanBundleInclude.query.filter( - club_id=club_id, - bundle_huiyuan_id=bundle_huiyuan_id, - ).values_list('included_huiyuan_id', flat=True) - ) + for bid in expand_huiyuan_id_variants(bundle_huiyuan_id): + ids = list( + ClubHuiyuanBundleInclude.query.filter( + club_id=club_id, + bundle_huiyuan_id=bid, + ).values_list('included_huiyuan_id', flat=True) + ) + if ids: + return ids + return [] def _membership_club_candidates(yonghu_id, club_id=None, record_club_id=None): @@ -123,10 +127,11 @@ def resolve_bundle_included_ids(bundle_huiyuan_id, club_id=None, yonghu_id=None, def _included_huiyuan_id_set(included_ids): included_set = set() for x in included_ids or []: - sx = normalize_huiyuan_id(x) - if sx: - included_set.add(sx) - included_set.add(sx.lstrip('0') or '0') + for variant in expand_huiyuan_id_variants(x): + sx = normalize_huiyuan_id(variant) + if sx: + included_set.add(sx) + included_set.add(sx.lstrip('0') or '0') return included_set @@ -159,10 +164,13 @@ def _record_club_id(record, club_id=None): def is_bundle_huiyuan(huiyuan_id): - try: - return bool(Huiyuan.query.get(huiyuan_id=huiyuan_id).is_bundle) - except Huiyuan.DoesNotExist: + hid = normalize_huiyuan_id(huiyuan_id) + if not hid: return False + for bid in Huiyuan.query.filter(is_bundle=True).values_list('huiyuan_id', flat=True): + if huiyuan_ids_match(bid, hid): + return True + return False def _membership_record_active(record): @@ -188,18 +196,13 @@ def user_has_huiyuan_access(yonghu_id, required_huiyuan_id, club_id=None): def _record_valid(rec): return _membership_record_active(rec) + req_set = {req, req.lstrip('0') or '0'} for rec in Huiyuangoumai.query.filter(yonghu_id=yonghu_id): - if _record_valid(rec) and huiyuan_ids_match(rec.huiyuan_id, req): + if not _record_valid(rec): + continue + if huiyuan_ids_match(rec.huiyuan_id, req): return True - - bundle_ids = set( - Huiyuan.query.filter(is_bundle=True).values_list('huiyuan_id', flat=True) - ) - if bundle_ids: - req_set = {req, req.lstrip('0') or '0'} - for rec in Huiyuangoumai.query.filter(yonghu_id=yonghu_id, huiyuan_id__in=bundle_ids): - if not _record_valid(rec): - continue + if is_bundle_huiyuan(rec.huiyuan_id): included = resolve_bundle_included_ids( rec.huiyuan_id, club_id=club_id, diff --git a/rank/reward_services.py b/rank/reward_services.py index c081237..d9aae73 100644 --- a/rank/reward_services.py +++ b/rank/reward_services.py @@ -7,7 +7,8 @@ from decimal import Decimal from typing import Optional from django.db import IntegrityError, transaction -from django.db.models import F, Sum +from django.db.models import F, Sum, Value +from django.db.models.functions import Coalesce from django.utils import timezone from backend.models import ( @@ -51,6 +52,13 @@ DEFAULT_SORT_FIELD = { 'shangjia': 'jiesuan_jine', } +# 总榜:管事/组长按无效人数排序展示(其它周期不变) +WUXIAO_TOTAL_RANK_SORT_FIELDS = { + 'guanshi': 'wuxiao_yaoqing_dashou_shu', + 'zuzhang': 'wuxiao_yaoqing_guanshi_shu', +} +WUXIAO_TOTAL_RANK_SORT_LABEL = '无效人数' + # shenfen -> sort_field_key -> (model, id_field, db_field, is_int) SORT_FIELD_REGISTRY = { 'dashou': { @@ -171,6 +179,9 @@ def sort_field_options_for_shenfen(shenfen: str) -> list: def resolve_sort_field_for_display(club_id: str, shenfen: str, riqi: str) -> tuple[str, str]: """按后台奖励方案决定展示/排序字段(方案存在即生效,与是否启用奖励无关)。""" + if riqi == '总榜' and shenfen in WUXIAO_TOTAL_RANK_SORT_FIELDS: + sf = WUXIAO_TOTAL_RANK_SORT_FIELDS[shenfen] + return sf, WUXIAO_TOTAL_RANK_SORT_LABEL period_type = RIQI_TO_PERIOD.get(riqi) or OPEN_RIQI_PERIOD_MAP.get(riqi) sort_field = None if period_type: @@ -246,8 +257,88 @@ def _resolve_date_range_for_display(riqi: str): return None +def _is_wuxiao_total_rank(riqi: str, shenfen: str, sort_field: str) -> bool: + return ( + riqi == '总榜' + and shenfen in WUXIAO_TOTAL_RANK_SORT_FIELDS + and sort_field == WUXIAO_TOTAL_RANK_SORT_FIELDS[shenfen] + ) + + +def query_wuxiao_total_rank_rows( + shenfen: str, club_id: str = None, limit: int = MAX_RANK, +) -> list: + """总榜专用:管事/组长按无效人数排行(仅展示,不影响其它周期)。""" + from rank.display_utils import apply_zuzhang_wuxiao_fields + + member_uids = club_member_uids(club_id) if club_id else None + if club_id and not member_uids: + return [] + fetch_limit = min(max(limit * 5, 100), 250) + + if shenfen == 'guanshi': + model = ManagerDailyStats + id_field = 'ManagerID' + base_qs = model.objects.all() + if club_id: + base_qs = base_qs.filter( + club_id=club_id, **{f'{id_field}__in': member_uids}, + ) + rows = ( + base_qs.values(id_field) + .annotate( + invited=Coalesce(Sum('InvitedPlayerCount'), Value(0)), + valid=Coalesce(Sum('RechargedPlayerCount'), Value(0)), + ) + .annotate(metric=F('invited') - F('valid')) + .order_by('-metric')[:fetch_limit] + ) + out = [] + for r in rows: + uid = r[id_field] + metric = int(r.get('metric') or 0) + if metric <= 0: + continue + out.append({'yonghuid': uid, 'metric': metric, 'is_int': True}) + if club_id: + out = filter_rank_rows_by_user_club(out, club_id) + out.sort(key=lambda x: x['metric'], reverse=True) + return out[:limit] + + if shenfen == 'zuzhang': + model = LeaderDailyStats + id_field = 'LeaderID' + base_qs = model.objects.all() + if club_id: + base_qs = base_qs.filter( + club_id=club_id, **{f'{id_field}__in': member_uids}, + ) + candidates = ( + base_qs.values(id_field) + .annotate(invited=Coalesce(Sum('InvitedManagerCount'), Value(0))) + .order_by('-invited')[:fetch_limit] + ) + out = [] + for r in candidates: + uid = r[id_field] + stats = {'yaoqing_guanshi_shu': int(r.get('invited') or 0)} + apply_zuzhang_wuxiao_fields(stats, uid, None, None) + metric = int(stats.get('wuxiao_yaoqing_guanshi_shu') or 0) + if metric <= 0: + continue + out.append({'yonghuid': uid, 'metric': metric, 'is_int': True}) + if club_id: + out = filter_rank_rows_by_user_club(out, club_id) + out.sort(key=lambda x: x['metric'], reverse=True) + return out[:limit] + + return [] + + def query_club_rank_rows(club_id: str, shenfen: str, sort_field: str, riqi: str, limit: int = MAX_RANK): """按 club 归属用户 + 日统计 club_id 双过滤(分奖与展示唯一依据)。""" + if _is_wuxiao_total_rank(riqi, shenfen, sort_field): + return query_wuxiao_total_rank_rows(shenfen, club_id=club_id, limit=limit) reg = SORT_FIELD_REGISTRY.get(shenfen, {}).get(sort_field) if not reg: return [] @@ -320,6 +411,8 @@ def query_group_rank_rows(shenfen: str, sort_field: str, riqi: str, limit: int = def _query_group_rank_rows(shenfen: str, sort_field: str, riqi: str, limit: int = MAX_RANK): + if _is_wuxiao_total_rank(riqi, shenfen, sort_field): + return query_wuxiao_total_rank_rows(shenfen, club_id=None, limit=limit) reg = SORT_FIELD_REGISTRY.get(shenfen, {}).get(sort_field) if not reg: return [] diff --git a/rank/reward_views.py b/rank/reward_views.py index 2731d66..a530521 100644 --- a/rank/reward_views.py +++ b/rank/reward_views.py @@ -167,15 +167,18 @@ class PhbJlPhbView(APIView): if riqi not in RIQI_OPTIONS: return Response({'code': 400, 'msg': '周期参数错误', 'data': {'list': []}}) - from rank.reward_services import SORT_FIELD_REGISTRY + from rank.reward_services import SORT_FIELD_REGISTRY, WUXIAO_TOTAL_RANK_SORT_FIELDS - scheme_sort, sort_label = resolve_sort_field_for_display(club_id, shenfen, riqi) - if not sort_field: - sort_field = scheme_sort - elif sort_field not in (SORT_FIELD_REGISTRY.get(shenfen) or {}): - return Response({'code': 400, 'msg': '排序指标无效', 'data': {'list': []}}) + if riqi == '总榜' and shenfen in WUXIAO_TOTAL_RANK_SORT_FIELDS: + sort_field, sort_label = resolve_sort_field_for_display(club_id, shenfen, riqi) else: - sort_label = SORT_FIELD_LABELS.get(sort_field, sort_field) + scheme_sort, sort_label = resolve_sort_field_for_display(club_id, shenfen, riqi) + if not sort_field: + sort_field = scheme_sort + elif sort_field not in (SORT_FIELD_REGISTRY.get(shenfen) or {}): + return Response({'code': 400, 'msg': '排序指标无效', 'data': {'list': []}}) + else: + sort_label = SORT_FIELD_LABELS.get(sort_field, sort_field) if fanwei == 'club': rows = query_club_rank_rows(club_id, shenfen, sort_field, riqi) diff --git a/users/xzj_paihang_views.py b/users/xzj_paihang_views.py index 0686f51..fe60c95 100644 --- a/users/xzj_paihang_views.py +++ b/users/xzj_paihang_views.py @@ -181,6 +181,48 @@ class XzjPhbHqsjView(APIView): }) try: + if riqi == '总榜' and shenfen in ('guanshi', 'zuzhang'): + from rank.reward_services import ( + WUXIAO_TOTAL_RANK_SORT_FIELDS, + enrich_rank_stats, + query_wuxiao_total_rank_rows, + ) + wuxiao_rows = query_wuxiao_total_rank_rows(shenfen, club_id=None, limit=MAX_RANK) + cfg = XZJ_ROLE_CONFIG[shenfen] + id_field = cfg['id_field'] + wuxiao_key = WUXIAO_TOTAL_RANK_SORT_FIELDS[shenfen] + pseudo_rows = [{id_field: r['yonghuid']} for r in wuxiao_rows] + nick_map, avatar_map = self._load_user_display(pseudo_rows, shenfen) + yonghuids = [r['yonghuid'] for r in wuxiao_rows] + stats_map = enrich_rank_stats(shenfen, yonghuids, riqi, '') + result_list = [] + for idx, row in enumerate(wuxiao_rows): + yonghuid = row['yonghuid'] + metric = int(row.get('metric') or 0) + item = { + 'mingci': idx + 1, + 'yonghuid': yonghuid, + 'nicheng': nick_map.get(yonghuid, '用户'), + 'touxiang': avatar_map.get(yonghuid, '') or '', + wuxiao_key: metric, + 'sort_field': wuxiao_key, + 'metric_value': metric, + 'metric_label': '无效人数', + } + extra = stats_map.get(yonghuid) or {} + for k, v in extra.items(): + item[k] = v + result_list.append(item) + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'list': result_list, + 'sort_field': wuxiao_key, + 'sort_label': '无效人数', + }, + }) + if riqi == '总榜': rows = self._query_rows(shenfen, all_time=True) start_date, end_date = None, None