diff --git a/backend/urls.py b/backend/urls.py index 40fb570..36253c4 100644 --- a/backend/urls.py +++ b/backend/urls.py @@ -16,6 +16,7 @@ from .view import (GetRolePermissionView, HqbkxxView,BkxgView,CaiwuView,CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView, CwqtczhqView, KhpzhqView, ChzsgcView,KhgglView, ShgxgsjView, KhjlglView, KhjlczView, ZxkfghdsView) from config.views import MiniappScriptHoutaiListView, MiniappScriptHoutaiModifyView +from rank.reward_houtai_views import RankRewardHoutaiListView, RankRewardHoutaiSaveView, RankRewardHoutaiClaimsView urlpatterns = [ path('gljshq', GetRolePermissionView.as_view(), name='获取就角色信息'), @@ -66,6 +67,9 @@ urlpatterns = [ path('htxgtcxx', PopupNoticeModifyAPIView.as_view(), name='后台更新创建弹窗配置'), path('hthqhs', MiniappScriptHoutaiListView.as_view(), name='后台获取话术配置'), path('htxghs', MiniappScriptHoutaiModifyView.as_view(), name='后台更新话术配置'), + path('phbjhq', RankRewardHoutaiListView.as_view(), name='后台排行榜奖励配置'), + path('phbjbc', RankRewardHoutaiSaveView.as_view(), name='后台保存排行榜奖励'), + path('phbjjl', RankRewardHoutaiClaimsView.as_view(), name='后台排行榜领取明细'), path('hqdplbsj', ShopListView.as_view(), name='获取店铺列表数据'), path('xgdpxx', ShopModifyView.as_view(), name='修改或添加店铺数据'), diff --git a/jituan/management/commands/seed_gvsdsdk_permissions.py b/jituan/management/commands/seed_gvsdsdk_permissions.py index 32354ab..0f7e7f5 100644 --- a/jituan/management/commands/seed_gvsdsdk_permissions.py +++ b/jituan/management/commands/seed_gvsdsdk_permissions.py @@ -14,6 +14,12 @@ EXTRA_PERMISSIONS = [ '配置各俱乐部考试开关、抽题规则与题库(小程序配置-打手接单考试)', '小程序配置', ), + ( + 'phbj666', + '排行榜奖励配置', + '配置各俱乐部排行榜名次奖金、排序指标与领取明细(小程序配置-排行榜奖励)', + '小程序配置', + ), ( 'kfhs666', '客服聊天配置', diff --git a/jituan/services/kefu_menu_definitions.py b/jituan/services/kefu_menu_definitions.py index 52bc32f..e571f32 100644 --- a/jituan/services/kefu_menu_definitions.py +++ b/jituan/services/kefu_menu_definitions.py @@ -89,6 +89,8 @@ KEFU_MENU_ROW_DEFS = [ 'perm_codes': ['8080a']}, {'page_id': 'miniapp.dashou-exam', 'name': '打手接单考试', 'path': '/miniapp/dashou-exam', 'parent_id': 'miniapp', 'sort_order': 117, 'perm_codes': ['dsks666', '8080a']}, + {'page_id': 'miniapp.rank-reward', 'name': '排行榜奖励', 'path': '/miniapp/rank-reward', 'parent_id': 'miniapp', 'sort_order': 118, + 'perm_codes': ['phbj666', '8080a']}, {'page_id': 'shop', 'name': '店铺管理', 'path': '', 'parent_id': '', 'sort_order': 120}, {'page_id': 'shop.list', 'name': '店铺列表', 'path': '/shop/list', 'parent_id': 'shop', 'sort_order': 121, 'perm_codes': ['999a', '999b', '999c', '999d', '999e']}, diff --git a/rank/migrations/0004_rank_reward.py b/rank/migrations/0004_rank_reward.py new file mode 100644 index 0000000..b877820 --- /dev/null +++ b/rank/migrations/0004_rank_reward.py @@ -0,0 +1,102 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('rank', '0003_rename_dashou_biao_dashou__36e1c3_idx_dashou_biao_dashou__502414_idx_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='RankRewardScheme', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('club_id', models.CharField(db_index=True, max_length=16, verbose_name='俱乐部ID')), + ('shenfen', models.CharField(db_index=True, max_length=16, verbose_name='身份')), + ('period_type', models.CharField(choices=[('day', '日榜(昨日)'), ('week', '周榜(上周)'), ('month', '月榜(上月)')], max_length=16, verbose_name='周期类型')), + ('sort_field', models.CharField(max_length=32, verbose_name='排序指标键')), + ('title', models.CharField(blank=True, default='', max_length=128, verbose_name='展示标题')), + ('description', models.TextField(blank=True, default='', verbose_name='规则说明')), + ('enabled', models.BooleanField(default=False, verbose_name='启用')), + ('claim_days', models.PositiveSmallIntegerField(default=30, verbose_name='领取有效天数')), + ('min_metric', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='最低指标门槛')), + ('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '排行榜奖励方案', + 'verbose_name_plural': '排行榜奖励方案', + 'db_table': 'rank_reward_scheme', + 'unique_together': {('club_id', 'shenfen', 'period_type')}, + }, + ), + migrations.CreateModel( + name='RankRewardTier', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('rank_from', models.PositiveSmallIntegerField(verbose_name='名次起')), + ('rank_to', models.PositiveSmallIntegerField(verbose_name='名次止')), + ('reward_amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='奖金(元)')), + ('label', models.CharField(blank=True, default='', max_length=32, verbose_name='档位标签')), + ('scheme', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tiers', to='rank.rankrewardscheme', verbose_name='方案')), + ], + options={ + 'verbose_name': '排行榜奖励档位', + 'verbose_name_plural': '排行榜奖励档位', + 'db_table': 'rank_reward_tier', + 'ordering': ['rank_from'], + }, + ), + migrations.CreateModel( + name='RankRewardSettlement', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('club_id', models.CharField(db_index=True, max_length=16, verbose_name='俱乐部ID')), + ('period_key', models.CharField(db_index=True, max_length=32, verbose_name='周期键')), + ('period_start', models.DateField(verbose_name='统计起始')), + ('period_end', models.DateField(verbose_name='统计截止')), + ('riqi_label', models.CharField(max_length=16, verbose_name='前端周期文案')), + ('sort_field', models.CharField(max_length=32, verbose_name='结算时排序字段快照')), + ('settled_at', models.DateTimeField(auto_now_add=True, verbose_name='结算时间')), + ('claim_count', models.PositiveIntegerField(default=0, verbose_name='生成待领取数')), + ('scheme', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rank.rankrewardscheme', verbose_name='方案')), + ], + options={ + 'verbose_name': '排行榜奖励结算', + 'verbose_name_plural': '排行榜奖励结算', + 'db_table': 'rank_reward_settlement', + 'unique_together': {('club_id', 'scheme', 'period_key')}, + }, + ), + migrations.CreateModel( + name='RankRewardClaim', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('club_id', models.CharField(db_index=True, max_length=16, verbose_name='俱乐部ID')), + ('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')), + ('shenfen', models.CharField(max_length=16, verbose_name='身份')), + ('mingci', models.PositiveSmallIntegerField(verbose_name='名次')), + ('metric_value', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='指标快照')), + ('reward_amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='奖金')), + ('status', models.PositiveSmallIntegerField(choices=[(0, '待领取'), (1, '已领取'), (2, '已过期'), (3, '已取消')], db_index=True, default=0)), + ('expire_at', models.DateTimeField(verbose_name='领取截止')), + ('claimed_at', models.DateTimeField(blank=True, null=True, verbose_name='领取时间')), + ('balance_field', models.CharField(max_length=32, verbose_name='入账字段')), + ('idempotent_key', models.CharField(max_length=96, unique=True, verbose_name='幂等键')), + ('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('settlement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='claims', to='rank.rankrewardsettlement', verbose_name='结算批次')), + ], + options={ + 'verbose_name': '排行榜奖励领取', + 'verbose_name_plural': '排行榜奖励领取', + 'db_table': 'rank_reward_claim', + 'indexes': [ + models.Index(fields=['yonghuid', 'status'], name='rank_reward_yonghu_st_idx'), + models.Index(fields=['club_id', 'status'], name='rank_reward_club_st_idx'), + ], + }, + ), + ] diff --git a/rank/models.py b/rank/models.py index b03736e..7c4382c 100644 --- a/rank/models.py +++ b/rank/models.py @@ -221,4 +221,7 @@ class KaoheguanBankuai(QModel): verbose_name = '考核官板块关联' verbose_name_plural = verbose_name unique_together = ('kaoheguan_id', 'bankuai') - indexes = [models.Index(fields=['kaoheguan_id'])] \ No newline at end of file + indexes = [models.Index(fields=['kaoheguan_id'])] + + +from .reward_models import RankRewardScheme, RankRewardTier, RankRewardSettlement, RankRewardClaim # noqa: E402,F401 \ No newline at end of file diff --git a/rank/reward_houtai_views.py b/rank/reward_houtai_views.py new file mode 100644 index 0000000..f4c5fd2 --- /dev/null +++ b/rank/reward_houtai_views.py @@ -0,0 +1,172 @@ +"""排行榜奖励 — 后台 kefu API。""" +import logging + +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from backend.utils import verify_kefu_permission +from jituan.services.club_context import resolve_club_id_from_request +from jituan.services.display_config import forbid_display_write_in_all_scope +from rank.reward_models import RankRewardClaim, RankRewardScheme, RankRewardTier +from rank.reward_services import SORT_FIELD_REGISTRY, sort_field_options_for_shenfen + +logger = logging.getLogger(__name__) + +REWARD_PERMS = ('phbj666', '8080a') + + +def _has_perm(permissions): + return any(p in permissions for p in REWARD_PERMS) + + +def _serialize_scheme(scheme): + tiers = RankRewardTier.query.filter(scheme_id=scheme.id).order_by('rank_from') + return { + 'id': scheme.id, + 'club_id': scheme.club_id, + 'shenfen': scheme.shenfen, + 'period_type': scheme.period_type, + 'sort_field': scheme.sort_field, + 'sort_options': sort_field_options_for_shenfen(scheme.shenfen), + 'title': scheme.title, + 'description': scheme.description, + 'enabled': scheme.enabled, + 'claim_days': scheme.claim_days, + 'min_metric': float(scheme.min_metric or 0), + 'tiers': [ + { + 'id': t.id, + 'rank_from': t.rank_from, + 'rank_to': t.rank_to, + 'reward_amount': float(t.reward_amount), + 'label': t.label, + } + for t in tiers + ], + } + + +class RankRewardHoutaiListView(APIView): + """POST /houtai/phbjhq""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + if not _has_perm(permissions): + return Response({'code': 403, 'msg': '无排行榜奖励配置权限'}) + + club_id = resolve_club_id_from_request(request) + schemes = RankRewardScheme.query.filter(club_id=club_id).order_by('shenfen', 'period_type') + return Response({ + 'code': 0, + 'data': { + 'club_id': club_id, + 'schemes': [_serialize_scheme(s) for s in schemes], + 'shenfen_options': list(SORT_FIELD_REGISTRY.keys()), + 'period_options': [ + {'key': RankRewardScheme.PERIOD_DAY, 'label': '日榜(昨日)'}, + {'key': RankRewardScheme.PERIOD_WEEK, 'label': '周榜(上周)'}, + {'key': RankRewardScheme.PERIOD_MONTH, 'label': '月榜(上月)'}, + ], + }, + }) + + +class RankRewardHoutaiSaveView(APIView): + """POST /houtai/phbjbc""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + if not _has_perm(permissions): + return Response({'code': 403, 'msg': '无权限'}) + + deny = forbid_display_write_in_all_scope(request) + if deny: + return deny + + club_id = resolve_club_id_from_request(request) + data = request.data + shenfen = (data.get('shenfen') or '').strip() + period_type = (data.get('period_type') or '').strip() + sort_field = (data.get('sort_field') or '').strip() + + if shenfen not in SORT_FIELD_REGISTRY: + return Response({'code': 400, 'msg': '身份无效'}) + if period_type not in (RankRewardScheme.PERIOD_DAY, RankRewardScheme.PERIOD_WEEK, RankRewardScheme.PERIOD_MONTH): + return Response({'code': 400, 'msg': '周期无效'}) + if sort_field not in SORT_FIELD_REGISTRY[shenfen]: + return Response({'code': 400, 'msg': '排序指标无效'}) + + tiers = data.get('tiers') or [] + if not tiers: + return Response({'code': 400, 'msg': '请配置至少一个奖励档位'}) + + scheme, _ = RankRewardScheme.objects.update_or_create( + club_id=club_id, + shenfen=shenfen, + period_type=period_type, + defaults={ + 'sort_field': sort_field, + 'title': (data.get('title') or '').strip(), + 'description': (data.get('description') or '').strip(), + 'enabled': bool(data.get('enabled', False)), + 'claim_days': int(data.get('claim_days') or 30), + 'min_metric': data.get('min_metric') or 0, + }, + ) + + RankRewardTier.query.filter(scheme_id=scheme.id).delete() + for t in tiers: + RankRewardTier.query.create( + scheme_id=scheme.id, + rank_from=int(t['rank_from']), + rank_to=int(t['rank_to']), + reward_amount=t['reward_amount'], + label=(t.get('label') or '').strip(), + ) + + return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_scheme(scheme)}) + + +class RankRewardHoutaiClaimsView(APIView): + """POST /houtai/phbjjl 领取明细""" + permission_classes = [IsAuthenticated] + + def post(self, request): + username = request.data.get('username') + kefu, permissions = verify_kefu_permission(request, username) + if kefu is None: + return Response({'code': 403, 'msg': '身份验证失败'}) + if not _has_perm(permissions): + return Response({'code': 403, 'msg': '无权限'}) + + club_id = resolve_club_id_from_request(request) + status = request.data.get('status') + qs = RankRewardClaim.query.filter(club_id=club_id).order_by('-CreateTime')[:100] + if status is not None: + qs = qs.filter(status=int(status)) + + status_map = {0: '待领取', 1: '已领取', 2: '已过期', 3: '已取消'} + items = [ + { + 'id': c.id, + 'yonghuid': c.yonghuid, + 'shenfen': c.shenfen, + 'mingci': c.mingci, + 'amount': float(c.reward_amount), + 'status': c.status, + 'status_text': status_map.get(c.status, ''), + 'claimed_at': c.claimed_at.isoformat() if c.claimed_at else '', + 'CreateTime': c.CreateTime.isoformat() if c.CreateTime else '', + } + for c in qs + ] + return Response({'code': 0, 'data': {'list': items}}) diff --git a/rank/reward_models.py b/rank/reward_models.py new file mode 100644 index 0000000..e693c3c --- /dev/null +++ b/rank/reward_models.py @@ -0,0 +1,117 @@ +"""排行榜奖励 — 按俱乐部分奖,与集团展示榜分离。""" +from django.db import models +from gvsdsdk.model_base import QModel + + +class RankRewardScheme(QModel): + """俱乐部 + 身份 + 周期 奖励方案""" + + PERIOD_DAY = 'day' + PERIOD_WEEK = 'week' + PERIOD_MONTH = 'month' + PERIOD_CHOICES = [ + (PERIOD_DAY, '日榜(昨日)'), + (PERIOD_WEEK, '周榜(上周)'), + (PERIOD_MONTH, '月榜(上月)'), + ] + + club_id = models.CharField(max_length=16, db_index=True, verbose_name='俱乐部ID') + shenfen = models.CharField(max_length=16, db_index=True, verbose_name='身份') + period_type = models.CharField(max_length=16, choices=PERIOD_CHOICES, verbose_name='周期类型') + sort_field = models.CharField(max_length=32, verbose_name='排序指标键') + title = models.CharField(max_length=128, blank=True, default='', verbose_name='展示标题') + description = models.TextField(blank=True, default='', verbose_name='规则说明') + enabled = models.BooleanField(default=False, verbose_name='启用') + claim_days = models.PositiveSmallIntegerField(default=30, verbose_name='领取有效天数') + min_metric = models.DecimalField( + max_digits=12, decimal_places=2, default=0, verbose_name='最低指标门槛', + ) + CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'rank_reward_scheme' + verbose_name = '排行榜奖励方案' + verbose_name_plural = verbose_name + unique_together = [['club_id', 'shenfen', 'period_type']] + + def __str__(self): + return f'{self.club_id}:{self.shenfen}:{self.period_type}' + + +class RankRewardTier(QModel): + scheme = models.ForeignKey( + RankRewardScheme, on_delete=models.CASCADE, related_name='tiers', verbose_name='方案', + ) + rank_from = models.PositiveSmallIntegerField(verbose_name='名次起') + rank_to = models.PositiveSmallIntegerField(verbose_name='名次止') + reward_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='奖金(元)') + label = models.CharField(max_length=32, blank=True, default='', verbose_name='档位标签') + + class Meta: + db_table = 'rank_reward_tier' + verbose_name = '排行榜奖励档位' + verbose_name_plural = verbose_name + ordering = ['rank_from'] + + def __str__(self): + return f'{self.rank_from}-{self.rank_to} ¥{self.reward_amount}' + + +class RankRewardSettlement(QModel): + """结算批次 — 同一 club+方案+period_key 仅一条""" + + scheme = models.ForeignKey(RankRewardScheme, on_delete=models.CASCADE, verbose_name='方案') + club_id = models.CharField(max_length=16, db_index=True, verbose_name='俱乐部ID') + period_key = models.CharField(max_length=32, db_index=True, verbose_name='周期键') + period_start = models.DateField(verbose_name='统计起始') + period_end = models.DateField(verbose_name='统计截止') + riqi_label = models.CharField(max_length=16, verbose_name='前端周期文案') + sort_field = models.CharField(max_length=32, verbose_name='结算时排序字段快照') + settled_at = models.DateTimeField(auto_now_add=True, verbose_name='结算时间') + claim_count = models.PositiveIntegerField(default=0, verbose_name='生成待领取数') + + class Meta: + db_table = 'rank_reward_settlement' + verbose_name = '排行榜奖励结算' + verbose_name_plural = verbose_name + unique_together = [['club_id', 'scheme', 'period_key']] + + +class RankRewardClaim(QModel): + STATUS_PENDING = 0 + STATUS_CLAIMED = 1 + STATUS_EXPIRED = 2 + STATUS_CANCELLED = 3 + STATUS_CHOICES = [ + (STATUS_PENDING, '待领取'), + (STATUS_CLAIMED, '已领取'), + (STATUS_EXPIRED, '已过期'), + (STATUS_CANCELLED, '已取消'), + ] + + settlement = models.ForeignKey( + RankRewardSettlement, on_delete=models.CASCADE, related_name='claims', verbose_name='结算批次', + ) + club_id = models.CharField(max_length=16, db_index=True, verbose_name='俱乐部ID') + yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='用户ID') + shenfen = models.CharField(max_length=16, verbose_name='身份') + mingci = models.PositiveSmallIntegerField(verbose_name='名次') + metric_value = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='指标快照') + reward_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='奖金') + status = models.PositiveSmallIntegerField(default=STATUS_PENDING, choices=STATUS_CHOICES, db_index=True) + expire_at = models.DateTimeField(verbose_name='领取截止') + claimed_at = models.DateTimeField(null=True, blank=True, verbose_name='领取时间') + balance_field = models.CharField(max_length=32, verbose_name='入账字段') + idempotent_key = models.CharField(max_length=96, unique=True, verbose_name='幂等键') + CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + db_table = 'rank_reward_claim' + verbose_name = '排行榜奖励领取' + verbose_name_plural = verbose_name + indexes = [ + models.Index(fields=['yonghuid', 'status']), + models.Index(fields=['club_id', 'status']), + ] diff --git a/rank/reward_services.py b/rank/reward_services.py new file mode 100644 index 0000000..a48553f --- /dev/null +++ b/rank/reward_services.py @@ -0,0 +1,521 @@ +"""排行榜奖励 — 俱乐部分榜结算、懒结算、领取入账(严格幂等)。""" +from __future__ import annotations + +import logging +from datetime import date, datetime, timedelta +from decimal import Decimal +from typing import Optional + +from django.db import IntegrityError, transaction +from django.db.models import F, Sum +from django.utils import timezone + +from backend.models import ( + LeaderDailyStats, + ManagerDailyStats, + MerchantDailyStats, + PlayerDailyStats, +) +from jituan.services.club_user import get_user_club_id +from rank.reward_models import ( + RankRewardClaim, + RankRewardScheme, + RankRewardSettlement, + RankRewardTier, +) +from users.business_models import User +from users.models import UserDashou, UserGuanshi, UserShangjia, UserZuzhang + +logger = logging.getLogger(__name__) + +MAX_RANK = 50 + +RIQI_TO_PERIOD = { + '昨日': RankRewardScheme.PERIOD_DAY, + '上周': RankRewardScheme.PERIOD_WEEK, + '上月': RankRewardScheme.PERIOD_MONTH, +} +OPEN_RIQI = frozenset({'今日', '本周', '本月', '总榜'}) + +# shenfen -> sort_field_key -> (model, id_field, db_field, is_int) +SORT_FIELD_REGISTRY = { + 'dashou': { + 'chengjiao_zongliang': (PlayerDailyStats, 'PlayerID', 'CompletedOrderTotal', True), + 'chengjiao_zonge': (PlayerDailyStats, 'PlayerID', 'CompletedAmount', False), + 'jiedan_zongliang': (PlayerDailyStats, 'PlayerID', 'AcceptedOrderTotal', True), + 'jiedan_zonge': (PlayerDailyStats, 'PlayerID', 'AcceptedAmount', False), + }, + 'guanshi': { + 'chongzhi_dashou_shu': (ManagerDailyStats, 'ManagerID', 'RechargedPlayerCount', True), + 'shouru_zonge': (ManagerDailyStats, 'ManagerID', 'TotalIncome', False), + 'yaoqing_dashou_shu': (ManagerDailyStats, 'ManagerID', 'InvitedPlayerCount', True), + }, + 'zuzhang': { + 'yaoqing_guanshi_shu': (LeaderDailyStats, 'LeaderID', 'InvitedManagerCount', True), + 'shouru_zonge': (LeaderDailyStats, 'LeaderID', 'TotalIncome', False), + 'fenyong_jine': (LeaderDailyStats, 'LeaderID', 'CommissionAmount', False), + }, + 'shangjia': { + 'jiesuan_dingdan_shu': (MerchantDailyStats, 'MerchantID', 'SettledOrderCount', True), + 'jiesuan_jine': (MerchantDailyStats, 'MerchantID', 'SettledAmount', False), + 'paifa_dingdan_shu': (MerchantDailyStats, 'MerchantID', 'AssignedOrderCount', True), + 'paifa_jine': (MerchantDailyStats, 'MerchantID', 'AssignedAmount', False), + }, +} + +SORT_FIELD_LABELS = { + 'chengjiao_zongliang': '成交量', + 'chengjiao_zonge': '成交总额', + 'jiedan_zongliang': '接单量', + 'jiedan_zonge': '接单总额', + 'chongzhi_dashou_shu': '有效打手数', + 'shouru_zonge': '收益总额', + 'yaoqing_dashou_shu': '邀请打手数', + 'yaoqing_guanshi_shu': '邀请管事数', + 'fenyong_jine': '分佣金额', + 'jiesuan_dingdan_shu': '结算单量', + 'jiesuan_jine': '结算金额', + 'paifa_dingdan_shu': '派单量', + 'paifa_jine': '派单流水', +} + +BALANCE_FIELD_MAP = { + 'dashou': 'yue', + 'guanshi': 'yue', + 'zuzhang': 'ketixian_jine', + 'shangjia': 'yue', +} + + +def sort_field_options_for_shenfen(shenfen: str) -> list: + reg = SORT_FIELD_REGISTRY.get(shenfen) or {} + return [ + {'key': k, 'label': SORT_FIELD_LABELS.get(k, k)} + for k in reg.keys() + ] + + +def resolve_period_dates(riqi: str) -> Optional[tuple[date, date, str]]: + today = date.today() + if riqi == '昨日': + d = today - timedelta(days=1) + return d, d, f'day:{d.isoformat()}' + if riqi == '上周': + this_monday = today - timedelta(days=today.weekday()) + last_sunday = this_monday - timedelta(days=1) + last_monday = last_sunday - timedelta(days=6) + iso = last_monday.isocalendar() + return last_monday, last_sunday, f'week:{iso[0]}-W{iso[1]:02d}' + if riqi == '上月': + first_this = today.replace(day=1) + last_day = first_this - timedelta(days=1) + first_prev = last_day.replace(day=1) + return first_prev, last_day, f'month:{first_prev.strftime("%Y-%m")}' + return None + + +def _resolve_date_range_for_display(riqi: str): + """与 paihang_views 一致,用于集团/俱乐部展示榜。""" + 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 = today.replace(day=1) + last_day = first_this - timedelta(days=1) + return last_day.replace(day=1), last_day + return None + + +def query_club_rank_rows(club_id: str, shenfen: str, sort_field: str, riqi: str, limit: int = MAX_RANK): + """按 club_id 过滤的俱乐部榜(分奖唯一依据)。""" + reg = SORT_FIELD_REGISTRY.get(shenfen, {}).get(sort_field) + if not reg: + return [] + model, id_field, db_field, is_int = reg + + if riqi == '总榜': + rows = ( + model.objects.filter(club_id=club_id) + .values(id_field) + .annotate(metric=Sum(db_field)) + .order_by('-metric')[:limit] + ) + return [ + {'yonghuid': r[id_field], 'metric': r['metric'] or 0, 'is_int': is_int} + for r in rows if r.get('metric') + ] + + date_range = _resolve_date_range_for_display(riqi) + if not date_range: + return [] + start_date, end_date = date_range + + if start_date == end_date: + qs = model.objects.filter(club_id=club_id, Date=start_date).order_by(f'-{db_field}')[:limit] + return [ + {'yonghuid': getattr(r, id_field), 'metric': getattr(r, db_field) or 0, 'is_int': is_int} + for r in qs if getattr(r, db_field) + ] + + rows = ( + model.objects.filter(club_id=club_id, Date__gte=start_date, Date__lte=end_date) + .values(id_field) + .annotate(metric=Sum(db_field)) + .order_by('-metric')[:limit] + ) + return [ + {'yonghuid': r[id_field], 'metric': r['metric'] or 0, 'is_int': is_int} + for r in rows if r.get('metric') + ] + + +def query_group_rank_rows(shenfen: str, sort_field: str, riqi: str, limit: int = MAX_RANK): + """集团总榜(仅展示,不过滤 club)。""" + return _query_group_rank_rows(shenfen, sort_field, riqi, limit) + + +def _query_group_rank_rows(shenfen: str, sort_field: str, riqi: str, limit: int = MAX_RANK): + reg = SORT_FIELD_REGISTRY.get(shenfen, {}).get(sort_field) + if not reg: + return [] + model, id_field, db_field, is_int = reg + + if riqi == '总榜': + rows = ( + model.objects.values(id_field) + .annotate(metric=Sum(db_field)) + .order_by('-metric')[:limit] + ) + return [ + {'yonghuid': r[id_field], 'metric': r['metric'] or 0, 'is_int': is_int} + for r in rows if r.get('metric') + ] + + date_range = _resolve_date_range_for_display(riqi) + if not date_range: + return [] + start_date, end_date = date_range + + if start_date == end_date: + qs = model.objects.filter(Date=start_date).order_by(f'-{db_field}')[:limit] + return [ + {'yonghuid': getattr(r, id_field), 'metric': getattr(r, db_field) or 0, 'is_int': is_int} + for r in qs if getattr(r, db_field) + ] + + rows = ( + model.objects.filter(Date__gte=start_date, Date__lte=end_date) + .values(id_field) + .annotate(metric=Sum(db_field)) + .order_by('-metric')[:limit] + ) + return [ + {'yonghuid': r[id_field], 'metric': r['metric'] or 0, 'is_int': is_int} + for r in rows if r.get('metric') + ] + + +def load_user_club_map(yonghuids: list) -> dict: + if not yonghuids: + return {} + users = User.objects.filter(UserUID__in=yonghuids) + return {u.UserUID: get_user_club_id(u) or getattr(u, 'ClubID', None) or '' for u in users} + + +def user_has_active_shenfen(yonghuid: str, shenfen: str) -> bool: + if shenfen == 'dashou': + return UserDashou.objects.filter(user__UserUID=yonghuid, zhanghaozhuangtai=1).exists() + if shenfen == 'guanshi': + return UserGuanshi.objects.filter(user__UserUID=yonghuid, zhuangtai=1).exists() + if shenfen == 'zuzhang': + return UserZuzhang.objects.filter(user__UserUID=yonghuid, zhuangtai=1).exists() + if shenfen == 'shangjia': + return UserShangjia.objects.filter(user__UserUID=yonghuid, zhuangtai=1).exists() + return False + + +def tier_amount_for_rank(tiers, mingci: int) -> Optional[Decimal]: + for t in tiers: + if t.rank_from <= mingci <= t.rank_to: + return t.reward_amount + return None + + +def maybe_settle_club_rewards(club_id: str, shenfen: str, riqi: str) -> Optional[RankRewardSettlement]: + """懒结算:仅已结束周期;幂等。""" + if riqi in OPEN_RIQI: + return None + period_type = RIQI_TO_PERIOD.get(riqi) + if not period_type: + return None + + period_info = resolve_period_dates(riqi) + if not period_info: + return None + period_start, period_end, period_key = period_info + + scheme = RankRewardScheme.query.filter( + club_id=club_id, shenfen=shenfen, period_type=period_type, enabled=True, + ).first() + if not scheme: + return None + + if scheme.sort_field not in (SORT_FIELD_REGISTRY.get(shenfen) or {}): + logger.warning('invalid sort_field %s for %s', scheme.sort_field, shenfen) + return None + + existing = RankRewardSettlement.query.filter( + club_id=club_id, scheme_id=scheme.id, period_key=period_key, + ).first() + if existing: + return existing + + rows = query_club_rank_rows(club_id, shenfen, scheme.sort_field, riqi) + tiers = list(RankRewardTier.query.filter(scheme_id=scheme.id).order_by('rank_from')) + if not tiers: + return None + + min_metric = scheme.min_metric or Decimal('0') + club_map = load_user_club_map([r['yonghuid'] for r in rows]) + expire_at = timezone.now() + timedelta(days=scheme.claim_days or 30) + balance_field = BALANCE_FIELD_MAP[shenfen] + claims_to_create = [] + + for idx, row in enumerate(rows): + mingci = idx + 1 + metric = Decimal(str(row['metric'] or 0)) + if metric < min_metric: + continue + amount = tier_amount_for_rank(tiers, mingci) + if not amount or amount <= 0: + continue + uid = row['yonghuid'] + if club_map.get(uid) != club_id: + continue + if not user_has_active_shenfen(uid, shenfen): + continue + idem = f'{club_id}|{scheme.id}|{period_key}|{uid}' + claims_to_create.append(RankRewardClaim( + club_id=club_id, + yonghuid=uid, + shenfen=shenfen, + mingci=mingci, + metric_value=metric, + reward_amount=amount, + status=RankRewardClaim.STATUS_PENDING, + expire_at=expire_at, + balance_field=balance_field, + idempotent_key=idem, + )) + + try: + with transaction.atomic(): + settlement = RankRewardSettlement.query.create( + scheme_id=scheme.id, + club_id=club_id, + period_key=period_key, + period_start=period_start, + period_end=period_end, + riqi_label=riqi, + sort_field=scheme.sort_field, + claim_count=0, + ) + created = 0 + for claim in claims_to_create: + claim.settlement_id = settlement.id + try: + claim.save() + created += 1 + except IntegrityError: + pass + settlement.claim_count = created + settlement.save(update_fields=['claim_count']) + return settlement + except IntegrityError: + return RankRewardSettlement.query.filter( + club_id=club_id, scheme_id=scheme.id, period_key=period_key, + ).first() + + +def build_reward_payload(club_id: str, shenfen: str, riqi: str, yonghuid: str = '') -> dict: + """奖励规则 + 我的待领取/已领取状态。""" + period_type = RIQI_TO_PERIOD.get(riqi) + scheme = None + if period_type: + scheme = RankRewardScheme.query.filter( + club_id=club_id, shenfen=shenfen, period_type=period_type, + ).first() + + settlement = None + if scheme and riqi not in OPEN_RIQI: + maybe_settle_club_rewards(club_id, shenfen, riqi) + period_info = resolve_period_dates(riqi) + if period_info: + _, _, period_key = period_info + settlement = RankRewardSettlement.query.filter( + club_id=club_id, scheme_id=scheme.id, period_key=period_key, + ).first() + + tiers = [] + if scheme: + tiers = [ + { + 'rank_from': t.rank_from, + 'rank_to': t.rank_to, + 'amount': float(t.reward_amount), + 'label': t.label or '', + } + for t in RankRewardTier.query.filter(scheme_id=scheme.id).order_by('rank_from') + ] + + rank_rewards = {} + if scheme and settlement: + for c in RankRewardClaim.query.filter(settlement_id=settlement.id): + rank_rewards[c.mingci] = float(c.reward_amount) + + my_claim = None + my_claimed = None + if yonghuid and settlement: + pending = RankRewardClaim.query.filter( + settlement_id=settlement.id, yonghuid=yonghuid, status=RankRewardClaim.STATUS_PENDING, + ).first() + if pending: + if pending.expire_at < timezone.now(): + pending.status = RankRewardClaim.STATUS_EXPIRED + pending.save(update_fields=['status', 'UpdateTime']) + else: + my_claim = { + 'claim_id': pending.id, + 'amount': float(pending.reward_amount), + 'mingci': pending.mingci, + 'status': 'pending', + 'status_text': '待领取', + 'expire_at': pending.expire_at.isoformat(), + 'club_id': club_id, + } + claimed = RankRewardClaim.query.filter( + settlement_id=settlement.id, yonghuid=yonghuid, status=RankRewardClaim.STATUS_CLAIMED, + ).first() + if claimed: + my_claimed = { + 'claim_id': claimed.id, + 'amount': float(claimed.reward_amount), + 'mingci': claimed.mingci, + 'status': 'claimed', + 'status_text': '已领取', + 'claimed_at': claimed.claimed_at.isoformat() if claimed.claimed_at else '', + } + + return { + 'club_id': club_id, + 'shenfen': shenfen, + 'riqi': riqi, + 'scheme_enabled': bool(scheme and scheme.enabled), + 'period_status': 'open' if riqi in OPEN_RIQI else 'closed', + 'period_label': riqi, + 'sort_field': scheme.sort_field if scheme else '', + 'sort_label': SORT_FIELD_LABELS.get(scheme.sort_field if scheme else '', ''), + 'title': scheme.title if scheme else '', + 'description': scheme.description if scheme else '', + 'tiers': tiers, + 'rank_rewards': rank_rewards, + 'my_claim': my_claim, + 'my_claimed': my_claimed, + } + + +def list_my_pending_claims(yonghuid: str, club_id: str = '') -> list: + qs = RankRewardClaim.query.filter( + yonghuid=yonghuid, status=RankRewardClaim.STATUS_PENDING, + ).order_by('-CreateTime') + if club_id: + qs = qs.filter(club_id=club_id) + now = timezone.now() + result = [] + for c in qs[:20]: + if c.expire_at < now: + c.status = RankRewardClaim.STATUS_EXPIRED + c.save(update_fields=['status', 'UpdateTime']) + continue + result.append({ + 'claim_id': c.id, + 'club_id': c.club_id, + 'shenfen': c.shenfen, + 'amount': float(c.reward_amount), + 'mingci': c.mingci, + 'status': 'pending', + 'status_text': '待领取', + 'expire_at': c.expire_at.isoformat(), + }) + return result + + +def claim_reward(claim_id: int, user) -> dict: + uid = user.UserUID + user_club = get_user_club_id(user) or getattr(user, 'ClubID', None) or '' + + with transaction.atomic(): + claim = RankRewardClaim.objects.select_for_update().filter(id=claim_id).first() + if not claim: + return {'ok': False, 'msg': '奖励记录不存在'} + if claim.yonghuid != uid: + return {'ok': False, 'msg': '无权领取该奖励'} + if claim.club_id != user_club: + return {'ok': False, 'msg': '俱乐部不匹配,无法领取'} + if claim.status == RankRewardClaim.STATUS_CLAIMED: + return {'ok': True, 'msg': '已领取', 'amount': float(claim.reward_amount), 'duplicate': True} + if claim.status != RankRewardClaim.STATUS_PENDING: + return {'ok': False, 'msg': '奖励不可领取'} + if claim.expire_at < timezone.now(): + claim.status = RankRewardClaim.STATUS_EXPIRED + claim.save(update_fields=['status', 'UpdateTime']) + return {'ok': False, 'msg': '奖励已过期'} + + if not user_has_active_shenfen(uid, claim.shenfen): + return {'ok': False, 'msg': '身份状态异常,无法领取'} + + amount = claim.reward_amount + _credit_balance(claim.shenfen, uid, amount) + + claim.status = RankRewardClaim.STATUS_CLAIMED + claim.claimed_at = timezone.now() + claim.save(update_fields=['status', 'claimed_at', 'UpdateTime']) + + return { + 'ok': True, + 'msg': '领取成功', + 'amount': float(amount), + 'shenfen': claim.shenfen, + 'balance_field': claim.balance_field, + } + + +def _credit_balance(shenfen: str, yonghuid: str, amount: Decimal): + amount = Decimal(str(amount)) + if shenfen == 'dashou': + p = UserDashou.objects.select_for_update().get(user__UserUID=yonghuid) + UserDashou.objects.filter(pk=p.pk).update(yue=F('yue') + amount) + elif shenfen == 'guanshi': + p = UserGuanshi.objects.select_for_update().get(user__UserUID=yonghuid) + UserGuanshi.objects.filter(pk=p.pk).update(yue=F('yue') + amount) + elif shenfen == 'zuzhang': + p = UserZuzhang.objects.select_for_update().get(user__UserUID=yonghuid) + UserZuzhang.objects.filter(pk=p.pk).update(ketixian_jine=F('ketixian_jine') + amount) + elif shenfen == 'shangjia': + p = UserShangjia.objects.select_for_update().get(user__UserUID=yonghuid) + UserShangjia.objects.filter(pk=p.pk).update(yue=F('yue') + amount) + else: + raise ValueError(f'unsupported shenfen: {shenfen}') diff --git a/rank/reward_views.py b/rank/reward_views.py new file mode 100644 index 0000000..777e6e6 --- /dev/null +++ b/rank/reward_views.py @@ -0,0 +1,235 @@ +"""排行榜奖励 — 小程序 API(不修改 phbhqsj)。""" +import logging + +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from jituan.models import Club +from jituan.services.club_user import get_user_club_id +from rank.reward_services import ( + BALANCE_FIELD_MAP, + OPEN_RIQI, + SORT_FIELD_LABELS, + build_reward_payload, + claim_reward, + list_my_pending_claims, + query_club_rank_rows, + query_group_rank_rows, + sort_field_options_for_shenfen, +) +from users.business_models import User +from users.models import UserDashou, UserShangjia + +logger = logging.getLogger(__name__) + +SHENFEN_OPTIONS = frozenset({'dashou', 'guanshi', 'zuzhang', 'shangjia'}) +RIQI_OPTIONS = frozenset({'今日', '本周', '本月', '总榜', '昨日', '上周', '上月'}) + + +def _resolve_club_id(request, data_club_id=''): + from jituan.services.club_context import resolve_club_id_from_request + cid = (data_club_id or '').strip() + if not cid: + cid = get_user_club_id(request.user) or getattr(request.user, 'ClubID', None) or '' + if not cid: + cid = resolve_club_id_from_request(request) or 'xq' + return cid + + +def _load_display_maps(yonghuids, shenfen): + users = User.objects.filter(UserUID__in=yonghuids) + nick_map = {u.UserUID: u.UserName or u.Phone or '用户' for u in users} + avatar_map = {u.UserUID: u.Avatar or '' for u in users} + if shenfen == 'dashou': + for p in UserDashou.objects.filter(user__UserUID__in=yonghuids).select_related('user'): + if p.nicheng: + nick_map[p.user.UserUID] = p.nicheng + elif shenfen == 'shangjia': + for p in UserShangjia.objects.filter(user__UserUID__in=yonghuids).select_related('user'): + if p.nicheng: + nick_map[p.user.UserUID] = p.nicheng + return nick_map, avatar_map + + +def _format_rank_list(rows, shenfen, sort_field, sort_label): + yonghuids = [r['yonghuid'] for r in rows] + nick_map, avatar_map = _load_display_maps(yonghuids, shenfen) + club_map = {} + try: + from rank.reward_services import load_user_club_map + club_map = load_user_club_map(yonghuids) + except Exception: + pass + club_names = {} + if club_map: + cids = set(club_map.values()) - {''} + for c in Club.query.filter(club_id__in=cids): + club_names[c.club_id] = c.name + + result = [] + for idx, row in enumerate(rows): + uid = row['yonghuid'] + cid = club_map.get(uid, '') + metric = row['metric'] + is_int = row.get('is_int', False) + item = { + 'mingci': idx + 1, + 'yonghuid': uid, + 'nicheng': nick_map.get(uid, '用户'), + 'touxiang': avatar_map.get(uid, '') or '', + 'club_id': cid, + 'club_name': club_names.get(cid, cid), + 'metric_value': int(metric) if is_int else float(metric), + 'metric_label': sort_label, + } + # 兼容 fighter-rank 现有字段 + if is_int: + item['chengjiao_zongliang'] = int(metric) + else: + item['chengjiao_zonge'] = float(metric) + result.append(item) + return result + + +class PhbJlXxView(APIView): + """POST /yonghu/phbjlxx 奖励规则 + 我的待领/已领(按俱乐部分奖)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + shenfen = (request.data.get('shenfen') or 'dashou').strip() + riqi = (request.data.get('riqi') or '上周').strip() + club_id = _resolve_club_id(request, request.data.get('club_id')) + + if shenfen not in SHENFEN_OPTIONS: + return Response({'code': 400, 'msg': '身份参数错误', 'data': None}) + if riqi not in RIQI_OPTIONS: + return Response({'code': 400, 'msg': '周期参数错误', 'data': None}) + + uid = request.user.UserUID + payload = build_reward_payload(club_id, shenfen, riqi, uid) + payload['balance_hint'] = BALANCE_FIELD_MAP.get(shenfen, '') + return Response({'code': 200, 'msg': 'success', 'data': payload}) + + +class PhbJlWdView(APIView): + """POST /yonghu/phbjlwd 我的全部待领取""" + permission_classes = [IsAuthenticated] + + def post(self, request): + club_id = _resolve_club_id(request, request.data.get('club_id')) + items = list_my_pending_claims(request.user.UserUID, club_id) + return Response({'code': 200, 'msg': 'success', 'data': {'list': items, 'total': len(items)}}) + + +class PhbJlLqView(APIView): + """POST /yonghu/phbjllq 领取奖励""" + permission_classes = [IsAuthenticated] + + def post(self, request): + claim_id = request.data.get('claim_id') + if not claim_id: + return Response({'code': 400, 'msg': '缺少 claim_id', 'data': None}) + try: + claim_id = int(claim_id) + except (TypeError, ValueError): + return Response({'code': 400, 'msg': 'claim_id 无效', 'data': None}) + + result = claim_reward(claim_id, request.user) + if not result.get('ok'): + return Response({'code': 400, 'msg': result.get('msg', '领取失败'), 'data': None}) + return Response({ + 'code': 200, + 'msg': result.get('msg', '领取成功'), + 'data': { + 'amount': result.get('amount'), + 'shenfen': result.get('shenfen'), + 'duplicate': result.get('duplicate', False), + }, + }) + + +class PhbJlPhbView(APIView): + """ + POST /yonghu/phbjlphb + 俱乐部榜 / 集团榜(展示用);分奖仍只按 club 方案。 + fanwei: jituan | club + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + shenfen = (request.data.get('shenfen') or 'dashou').strip() + riqi = (request.data.get('riqi') or '今日').strip() + fanwei = (request.data.get('fanwei') or 'jituan').strip() + club_id = _resolve_club_id(request, request.data.get('club_id')) + sort_field = (request.data.get('sort_field') or '').strip() + + if shenfen not in SHENFEN_OPTIONS: + return Response({'code': 400, 'msg': '身份参数错误', 'data': {'list': []}}) + if riqi not in RIQI_OPTIONS: + return Response({'code': 400, 'msg': '周期参数错误', 'data': {'list': []}}) + + from rank.reward_models import RankRewardScheme + from rank.reward_services import RIQI_TO_PERIOD, SORT_FIELD_REGISTRY + + if not sort_field: + period_type = RIQI_TO_PERIOD.get(riqi) + if period_type and fanwei == 'club': + scheme = RankRewardScheme.query.filter( + club_id=club_id, shenfen=shenfen, period_type=period_type, enabled=True, + ).first() + if scheme: + sort_field = scheme.sort_field + if not sort_field: + defaults = { + 'dashou': 'chengjiao_zongliang', + 'guanshi': 'chongzhi_dashou_shu', + 'zuzhang': 'yaoqing_guanshi_shu', + 'shangjia': 'jiesuan_jine', + } + sort_field = defaults.get(shenfen, 'chengjiao_zongliang') + + if sort_field not in (SORT_FIELD_REGISTRY.get(shenfen) or {}): + return Response({'code': 400, 'msg': '排序指标无效', 'data': {'list': []}}) + + sort_label = SORT_FIELD_LABELS.get(sort_field, sort_field) + + if fanwei == 'club': + rows = query_club_rank_rows(club_id, shenfen, sort_field, riqi) + else: + rows = query_group_rank_rows(shenfen, sort_field, riqi) + + result_list = _format_rank_list(rows, shenfen, sort_field, sort_label) + reward = build_reward_payload(club_id, shenfen, riqi, request.user.UserUID) + + # 给每行附加该 club 奖励金额(仅 club 方案启用且已结算) + rank_rewards = reward.get('rank_rewards') or {} + for item in result_list: + item['reward_amount'] = rank_rewards.get(item['mingci'], 0) + + return Response({ + 'code': 200, + 'msg': '获取成功', + 'data': { + 'list': result_list, + 'fanwei': fanwei, + 'club_id': club_id, + 'sort_field': sort_field, + 'sort_label': sort_label, + 'reward': reward, + }, + }) + + +class PhbJlClubListView(APIView): + """POST /yonghu/phbjljlb 有奖励方案的俱乐部列表(展示筛选用)""" + permission_classes = [IsAuthenticated] + + def post(self, request): + from rank.reward_models import RankRewardScheme + cids = RankRewardScheme.query.filter(enabled=True).values_list('club_id', flat=True).distinct() + clubs = [] + for c in Club.query.filter(club_id__in=list(cids)).order_by('sort_order', 'club_id'): + clubs.append({'club_id': c.club_id, 'name': c.name}) + my_club = _resolve_club_id(request) + return Response({'code': 200, 'msg': 'success', 'data': {'clubs': clubs, 'my_club_id': my_club}}) diff --git a/users/urls.py b/users/urls.py index 84db893..efb8f85 100644 --- a/users/urls.py +++ b/users/urls.py @@ -20,6 +20,7 @@ from .views import WechatMiniProgramLoginView, UserInfoUpdateView, DashouXinxiAP from .tixian_shenhe_views import TixianZddkshApplyView from .paihang_views import PhbHqsjView from .xzj_paihang_views import XzjPhbHqsjView +from rank.reward_views import PhbJlXxView, PhbJlWdView, PhbJlLqView, PhbJlPhbView, PhbJlClubListView urlpatterns = [ @@ -164,5 +165,10 @@ urlpatterns = [ path('tixiancallback', csrf_exempt(TixianCallbackV3View.as_view()), name='提现回调'), path('phbhqsj', PhbHqsjView.as_view(), name='排行榜数据'), path('xzjphbhqsj', XzjPhbHqsjView.as_view(), name='星之界排行榜数据'), + path('phbjlxx', PhbJlXxView.as_view(), name='排行榜奖励信息'), + path('phbjlwd', PhbJlWdView.as_view(), name='排行榜待领取列表'), + path('phbjllq', PhbJlLqView.as_view(), name='排行榜奖励领取'), + path('phbjlphb', PhbJlPhbView.as_view(), name='排行榜俱乐部集团榜'), + path('phbjljlb', PhbJlClubListView.as_view(), name='排行榜奖励俱乐部列表'), path('merchant-staff/', include('merchant_ops.urls')), ] \ No newline at end of file