feat: 排行榜奖励按俱乐部分奖与手动领取
- 新增奖励方案/档位/结算/领取四表与懒结算服务 - 小程序 phbjlxx/phbjllq/phbjlphb 新接口,phbhqsj 零改动 - 后台 phbjhq/phbjbc/phbjjl 与 phbj666 权限菜单 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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='修改或添加店铺数据'),
|
||||
|
||||
@@ -14,6 +14,12 @@ EXTRA_PERMISSIONS = [
|
||||
'配置各俱乐部考试开关、抽题规则与题库(小程序配置-打手接单考试)',
|
||||
'小程序配置',
|
||||
),
|
||||
(
|
||||
'phbj666',
|
||||
'排行榜奖励配置',
|
||||
'配置各俱乐部排行榜名次奖金、排序指标与领取明细(小程序配置-排行榜奖励)',
|
||||
'小程序配置',
|
||||
),
|
||||
(
|
||||
'kfhs666',
|
||||
'客服聊天配置',
|
||||
|
||||
@@ -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']},
|
||||
|
||||
102
rank/migrations/0004_rank_reward.py
Normal file
102
rank/migrations/0004_rank_reward.py
Normal file
@@ -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'),
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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'])]
|
||||
indexes = [models.Index(fields=['kaoheguan_id'])]
|
||||
|
||||
|
||||
from .reward_models import RankRewardScheme, RankRewardTier, RankRewardSettlement, RankRewardClaim # noqa: E402,F401
|
||||
172
rank/reward_houtai_views.py
Normal file
172
rank/reward_houtai_views.py
Normal file
@@ -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}})
|
||||
117
rank/reward_models.py
Normal file
117
rank/reward_models.py
Normal file
@@ -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']),
|
||||
]
|
||||
521
rank/reward_services.py
Normal file
521
rank/reward_services.py
Normal file
@@ -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}')
|
||||
235
rank/reward_views.py
Normal file
235
rank/reward_views.py
Normal file
@@ -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}})
|
||||
@@ -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')),
|
||||
]
|
||||
Reference in New Issue
Block a user