feat(jituan): 集团多俱乐部改造 — club 隔离、财务/提现/分红/展示配置

This commit is contained in:
XingQue
2026-06-24 05:02:18 +08:00
parent ce9b09f096
commit dcc4936428
82 changed files with 5748 additions and 924 deletions

View File

@@ -38,7 +38,12 @@ from backend.utils import (
update_dashou_daily_by_action,
update_shangjia_daily
)
from orders.utils import update_daily_payout
from jituan.services.club_context import filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
from jituan.services.club_penalty import (
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
)
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
from shop.utils import update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.notice_tasks import dingdan_guangbo
@@ -743,7 +748,9 @@ class KefuGetDashouListView(APIView):
return Response({'code': 403, 'msg': '您没有权限查看打手列表'})
# 4. 构建打手查询集(仅打手类型)
dashou_qs = UserDashou.query.select_related('user').all()
dashou_qs = filter_user_related_by_club(
UserDashou.query.select_related('user').all(), request,
)
# 关键词筛选用户ID或昵称
if keyword:
@@ -758,12 +765,12 @@ class KefuGetDashouListView(APIView):
# 6. 统计有效打手数(有会员且未过期)
dashou_user_ids = dashou_qs.values_list('user__UserUID', flat=True)
now = timezone.now()
valid_user_ids = Huiyuangoumai.query.filter(
hy_qs = filter_queryset_by_club(Huiyuangoumai.query.all(), request)
valid_dashou_count = hy_qs.filter(
yonghu_id__in=dashou_user_ids,
huiyuan_zhuangtai=1,
daoqi_time__gt=now
).values_list('yonghu_id', flat=True).distinct()
valid_dashou_count = valid_user_ids.count()
daoqi_time__gt=now,
).values('yonghu_id').distinct().count()
# 7. 分页
offset = (page - 1) * page_size
@@ -784,8 +791,11 @@ class KefuGetDashouListView(APIView):
has_more = (offset + len(dashou_list)) < total_dashou
meta = list_response_meta(request)
return Response({
'code': 0,
'club_id': meta['club_id'],
'scope': meta['scope'],
'data': {
'list': result,
'has_more': has_more,
@@ -836,6 +846,10 @@ class KefuGetDashouDetailView(APIView):
except User.DoesNotExist:
return Response({'code': 404, 'msg': '打手不存在'})
denied = forbid_if_user_out_of_scope(request, user_main)
if denied:
return denied
try:
dashou = user_main.DashouProfile
except ObjectDoesNotExist:
@@ -1346,7 +1360,9 @@ class KefuGetShangjiaListView(APIView):
return Response({'code': 403, 'msg': '您没有权限查看商家列表'})
# 4. 构建商家查询集(商家扩展表 + 关联用户主表)
shangjia_qs = UserShangjia.query.select_related('user').all()
shangjia_qs = filter_user_related_by_club(
UserShangjia.query.select_related('user').all(), request,
)
# 关键词筛选用户ID或昵称
if keyword:
@@ -1393,6 +1409,7 @@ class KefuGetShangjiaListView(APIView):
# 5. 统计总商家数(应用筛选后)
total = shangjia_qs.count()
valid_merchant_count = shangjia_qs.filter(zhuangtai=1).count()
# 6. 分页
offset = (page - 1) * page_size
@@ -1412,11 +1429,15 @@ class KefuGetShangjiaListView(APIView):
'zhuangtai': shangjia.zhuangtai, # 1=正常,2=禁用
})
meta = list_response_meta(request)
return Response({
'code': 0,
'club_id': meta['club_id'],
'scope': meta['scope'],
'data': {
'list': result,
'total': total,
'valid_merchant_count': valid_merchant_count,
}
})
@@ -1466,6 +1487,10 @@ class KefuGetShangjiaDetailView(APIView):
except UserShangjia.DoesNotExist:
return Response({'code': 404, 'msg': '商家不存在'})
denied = forbid_if_user_out_of_scope(request, shangjia.user)
if denied:
return denied
user = shangjia.user
data = {
@@ -2847,9 +2872,12 @@ class GetGuanliListView(APIView):
status = request.data.get('status')
# 构建查询:从 User 开始,关联 UserGuanshi 和 UserBoss左连接获取昵称
qs = User.query.filter(
GuanshiProfile__isnull=False # 确保是管事
).select_related('GuanshiProfile').select_related('BossProfile')
qs = filter_user_qs_by_club(
User.query.filter(
GuanshiProfile__isnull=False # 确保是管事
).select_related('GuanshiProfile').select_related('BossProfile'),
request,
)
# 关键词搜索用户ID 或 昵称来自 boss_profile.nickname
if keyword:
@@ -2929,7 +2957,11 @@ class GetGuanliListView(APIView):
'chongzhifenrun': str(guanshi.chongzhifenrun),
})
return Response({'code': 0, 'data': {'list': result, 'total': total}})
return Response({
'code': 0,
**list_response_meta(request),
'data': {'list': result, 'total': total},
})
@@ -2984,6 +3016,10 @@ class GetGuanliDetailView(APIView):
except UserGuanshi.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是管事'})
denied = forbid_if_user_out_of_scope(request, user)
if denied:
return denied
# 基础信息
is_zuzhang = UserZuzhang.query.filter(user=user).exists()
base_data = {
@@ -3377,8 +3413,12 @@ class UpdateGuanliView(APIView):
# 4.2 首次定制分红cishu=1
if custom_first is not None and isinstance(custom_first, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
# 获取当前所有首次定制记录
existing_first = DuociFenhong.query.filter(yonghuid=yonghuid, cishu=1)
existing_first = DuociFenhong.query.filter(
club_id=guanshi_club, yonghuid=yonghuid, cishu=1,
)
# 前端传来的定制字典 {会员ID: 金额}
for huiyuan_id, amount in custom_first.items():
try:
@@ -3386,17 +3426,19 @@ class UpdateGuanliView(APIView):
except:
continue
if amount <= 0:
# 金额为0或负数表示删除定制
DuociFenhong.query.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete()
DuociFenhong.query.filter(
club_id=guanshi_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1,
).delete()
else:
DuociFenhong.query.update_or_create(
club_id=guanshi_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=1,
defaults={
'guanshi_fenhong': amount,
'zuzhang_fenhong': Decimal('0')
}
'zuzhang_fenhong': Decimal('0'),
},
)
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
for record in existing_first:
@@ -3405,6 +3447,8 @@ class UpdateGuanliView(APIView):
# 4.3 额外次数分红cishu >= 2
if extra_map is not None and isinstance(extra_map, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
# 收集需要保留的记录
keep_set = set()
for times_str, members in extra_map.items():
@@ -3429,6 +3473,7 @@ class UpdateGuanliView(APIView):
keep_set.add((cishu, huiyuan_id))
# 更新或创建
DuociFenhong.query.update_or_create(
club_id=guanshi_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=cishu,
@@ -3438,7 +3483,9 @@ class UpdateGuanliView(APIView):
}
)
# 删除不在 keep_set 中的额外次数记录
existing_extra = DuociFenhong.query.filter(yonghuid=yonghuid, cishu__gte=2)
existing_extra = DuociFenhong.query.filter(
club_id=guanshi_club, yonghuid=yonghuid, cishu__gte=2,
)
for record in existing_extra:
if (record.cishu, record.huiyuan) not in keep_set:
record.delete()
@@ -3493,53 +3540,8 @@ class GetWithdrawSettingsView(APIView):
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
return Response({'code': 403, 'msg': '无权限查看提现设置'})
# 1. 手续费利率CommissionRate5打手/6管事/8组长/9审核官/10商家/11打手押金
rate_map = {}
for leixing, code in [(1, '5'), (2, '6'), (3, '8'), (4, '9'), (5, '11'), (6, '10')]:
obj = CommissionRate.query.filter(Platform=code).first()
rate_map[leixing] = float(obj.Rate) if obj and obj.Rate is not None else 0.0
# 1b. 订单/押金分红费率CommissionRate1平台订单/3商家订单/12押金管事/13商家订单管事分红
order_rate_map = {}
for leixing, code in [(1, '1'), (3, '3'), (12, '12'), (13, '13')]:
obj = CommissionRate.query.filter(Platform=code).first()
order_rate_map[leixing] = float(obj.Rate) if obj and obj.Rate is not None else 0.0
# 2. 个人每日限额 (leixing=1,2,3)
quota_map = {}
for leixing in [1, 2, 3]:
obj = TixianQuotaDefault.query.filter(UserType=leixing).first()
quota_map[leixing] = float(obj.default_quota) if obj else 20.0
# 3. 每日总限额提现类型1~6 → 配置表4~9
total_limit_map = {}
for withdraw_leixing, quota_leixing in [(1, 4), (2, 5), (3, 6), (4, 7), (5, 8), (6, 9)]:
obj = TixianQuotaDefault.query.filter(UserType=quota_leixing).first()
total_limit_map[withdraw_leixing] = float(obj.default_quota) if obj else 0.0
# 4. 当日已提现总额(按提现类型 leixing 1~6
today = date.today()
today_totals = {i: 0.0 for i in range(1, 7)}
records = WithdrawalDailyStats.query.filter(Date=today)
for rec in records:
if rec.WithdrawalType in today_totals:
today_totals[rec.WithdrawalType] = float(rec.total_amount)
# 🆕 获取提现模式自动1 / 手动2默认2
config = WithdrawConfig.query.filter(id=1).first()
withdraw_mode = config.mode if config else 2
return Response({
'code': 0,
'data': {
'withdraw_mode': withdraw_mode,
'rates': rate_map,
'order_rates': order_rate_map,
'quotas': quota_map,
'total_limits': total_limit_map,
'today_totals': today_totals,
}
})
from jituan.services.withdraw_config import build_withdraw_settings_payload
return Response({'code': 0, 'data': build_withdraw_settings_payload(request)})
class UpdateWithdrawSettingsView(APIView):
@@ -3566,124 +3568,8 @@ class UpdateWithdrawSettingsView(APIView):
# 权限映射:要修改的角色需要对应的权限
# 前端传来 rates, quotas, total_limits 三个对象,每个包含 1,2,3
rates = request.data.get('rates', {})
order_rates = request.data.get('order_rates', request.data.get('commission_rates', {}))
quotas = request.data.get('quotas', {})
total_limits = request.data.get('total_limits', {})
# 角色类型映射
role_perms = {1: '5500a', 2: '5500b', 3: '5500c'}
# 利率代码映射1-3 有水缸限额4审核官/5打手押金/6商家余额仅费率
rate_code_map = {1: '5', 2: '6', 3: '8', 4: '9', 5: '11', 6: '10'}
# 订单/押金分红1平台订单/3商家订单/12押金管事/13商家订单管事分红
order_rate_code_map = {1: '1', 3: '3', 12: '12', 13: '13'}
# 总限额:提现类型 → TixianQuotaDefault.leixing4~9
total_code_map = {1: 4, 2: 5, 3: 6, 4: 7, 5: 8, 6: 9}
def _rate_key_present(data, role):
return (str(role) in data and data[str(role)] is not None) or \
(role in data and data[role] is not None)
with transaction.atomic():
# 遍历三个角色(限额相关)
for role in [1, 2, 3]:
perm_needed = role_perms[role]
if perm_needed not in permissions:
if _rate_key_present(rates, role) or \
(str(role) in quotas and quotas[str(role)] is not None) or \
(role in quotas and quotas.get(role) is not None) or \
(str(role) in total_limits and total_limits[str(role)] is not None) or \
(role in total_limits and total_limits.get(role) is not None):
return Response({'code': 403, 'msg': f'无权限修改{["","打手","管事","组长"][role]}相关设置(需要{perm_needed}'})
# 其他提现手续费(审核官/打手押金/商家余额):需 5500a/b/c 任一
extra_role_names = {4: '考核官', 5: '打手押金', 6: '商家余额'}
for role in [4, 5, 6]:
if _rate_key_present(rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')):
return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}提现手续费率'})
if (str(role) in total_limits and total_limits[str(role)] is not None) or \
(role in total_limits and total_limits.get(role) is not None):
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
return Response({'code': 403, 'msg': f'无权限修改{extra_role_names[role]}总限额'})
# 订单/押金分红费率:需 5500a/b/c 任一
for role in [1, 3, 12, 13]:
if _rate_key_present(order_rates, role) and not any(p in permissions for p in ('5500a', '5500b', '5500c')):
order_names = {
1: '平台订单打手分红', 3: '商家订单打手分红',
12: '押金管事分红', 13: '管事订单分红',
}
return Response({'code': 403, 'msg': f'无权限修改{order_names[role]}费率'})
# 1. 修改手续费利率
for role in [1, 2, 3, 4, 5, 6]:
val = rates.get(str(role), rates.get(role))
if val is not None:
rate = Decimal(str(val))
code = rate_code_map[role]
obj, created = CommissionRate.objects.select_for_update().get_or_create(
Platform=code,
defaults={'Rate': rate}
)
if not created:
obj.Rate = rate
obj.save()
# 1b. 修改订单/押金分红费率
for role in [1, 3, 12, 13]:
val = order_rates.get(str(role), order_rates.get(role))
if val is not None:
rate = Decimal(str(val))
code = order_rate_code_map[role]
obj, created = CommissionRate.objects.select_for_update().get_or_create(
Platform=code,
defaults={'Rate': rate}
)
if not created:
obj.Rate = rate
obj.save()
# 2. 修改个人每日限额
for role in [1, 2, 3]:
if str(role) in quotas:
val = quotas[str(role)]
if val is not None:
quota = Decimal(str(val))
obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create(
UserType=role,
defaults={'default_quota': quota}
)
if not created:
obj.default_quota = quota
obj.save()
# 3. 修改每日总限额1打手/2管事/3组长/4考核官/5押金/6商家
for role in [1, 2, 3, 4, 5, 6]:
val = total_limits.get(str(role), total_limits.get(role))
if val is not None:
limit = Decimal(str(val))
code = total_code_map[role]
obj, created = TixianQuotaDefault.objects.select_for_update().get_or_create(
UserType=code,
defaults={'default_quota': limit}
)
if not created:
obj.default_quota = limit
obj.save()
# 🆕 修改提现模式有5500a/b/c任一即可
withdraw_mode = request.data.get('withdraw_mode')
if withdraw_mode is not None:
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
return Response({'code': 403, 'msg': '无权限修改提现模式'})
# 更新或创建 id=1 的配置
WithdrawConfig.query.update_or_create(
id=1,
defaults={'mode': int(withdraw_mode)}
)
# 注意WithdrawalDailyStats 表由提现流程自动更新,此处不直接修改
return Response({'code': 0, 'msg': '设置修改成功'})
from jituan.services.withdraw_config import apply_withdraw_settings_update
return apply_withdraw_settings_update(request, permissions)
@@ -3738,7 +3624,10 @@ class GetZuzhangListView(APIView):
commission_value = request.data.get('commission_value')
# 5. 构建基础 QuerySet预加载关联表
queryset = UserZuzhang.query.select_related('user', 'user__BossProfile').all()
queryset = filter_user_related_by_club(
UserZuzhang.query.select_related('user', 'user__BossProfile').all(),
request,
)
# 6. 应用筛选条件(使用 ORM 安全过滤)
if keyword:
@@ -3808,6 +3697,7 @@ class GetZuzhangListView(APIView):
return Response({
'code': 0,
'msg': 'success',
**list_response_meta(request),
'data': {
'list': data_list,
'total': total,
@@ -3869,6 +3759,10 @@ class GetZuzhangDetailView(APIView):
except UserZuzhang.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是组长'})
denied = forbid_if_user_out_of_scope(request, user)
if denied:
return denied
# 基础信息
base_data = {
'yonghuid': user.UserUID,
@@ -4134,7 +4028,11 @@ class UpdateZuzhangView(APIView):
# 4.2 首次定制分红cishu=1
if custom_first is not None and isinstance(custom_first, dict):
existing_first = DuociFenhong.query.filter(yonghuid=yonghuid, cishu=1)
from jituan.services.club_penalty import resolve_gsfenhong_club_id
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
existing_first = DuociFenhong.query.filter(
club_id=zuzhang_club, yonghuid=yonghuid, cishu=1,
)
for huiyuan_id, amount in custom_first.items():
try:
amount = Decimal(str(amount))
@@ -4142,9 +4040,12 @@ class UpdateZuzhangView(APIView):
continue
if amount <= 0:
# 删除定制(恢复默认)
DuociFenhong.query.filter(yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1).delete()
DuociFenhong.query.filter(
club_id=zuzhang_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1,
).delete()
else:
DuociFenhong.query.update_or_create(
club_id=zuzhang_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=1,
@@ -4160,6 +4061,8 @@ class UpdateZuzhangView(APIView):
# 4.3 额外次数分红cishu >= 2
if extra_map is not None and isinstance(extra_map, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
keep_set = set()
for times_str, members in extra_map.items():
try:
@@ -4182,6 +4085,7 @@ class UpdateZuzhangView(APIView):
continue
keep_set.add((cishu, huiyuan_id))
DuociFenhong.query.update_or_create(
club_id=zuzhang_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=cishu,
@@ -4191,7 +4095,9 @@ class UpdateZuzhangView(APIView):
}
)
# 删除不在 keep_set 中的额外次数记录
existing_extra = DuociFenhong.query.filter(yonghuid=yonghuid, cishu__gte=2)
existing_extra = DuociFenhong.query.filter(
club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2,
)
for record in existing_extra:
if (record.cishu, record.huiyuan) not in keep_set:
record.delete()
@@ -4367,6 +4273,8 @@ class GetOperationLogListView(APIView):
page_size = 20
qs = Xiugaijilu.query.all().order_by('-CreateTime')
from jituan.services.admin_audit import filter_xiugaijilu_by_request
qs = filter_xiugaijilu_by_request(qs, request)
if yonghuid:
qs = qs.filter(yonghuid=yonghuid)
if xiugaiid:
@@ -4412,6 +4320,7 @@ class GetOperationLogListView(APIView):
'create_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
})
from jituan.services.club_user_access import list_response_meta
return Response({
'code': 0,
'data': {
@@ -4419,6 +4328,7 @@ class GetOperationLogListView(APIView):
'total': total,
'page': page,
'page_size': page_size,
**list_response_meta(request),
},
})
@@ -4995,11 +4905,19 @@ class PopupNoticeListAPIView(APIView):
return Response({'code': 403, 'msg': '您没有权限访问弹窗管理功能'})
# 4. 查询所有页面并预加载关联的弹窗和图片避免N+1查询
pages = PopupPage.query.all().prefetch_related('popups__images').order_by('id')
from jituan.services.display_config import filter_popup_page_by_request, list_response_meta
pages = filter_popup_page_by_request(
PopupPage.query.all().prefetch_related('popups__images'),
request,
).order_by('id')
# 5. 序列化输出
serializer = PopupPageSerializer(pages, many=True)
return Response({'code': 0, 'data': {'pages': serializer.data}})
return Response({
'code': 0,
'data': {'pages': serializer.data, **list_response_meta(request)},
})
class PopupNoticeModifyAPIView(APIView):
@@ -5020,6 +4938,11 @@ class PopupNoticeModifyAPIView(APIView):
if REQUIRED_PERMISSION not in permissions:
return Response({'code': 403, 'msg': '您没有权限操作弹窗管理'})
from jituan.services.display_config import forbid_display_write_in_all_scope
deny = forbid_display_write_in_all_scope(request)
if deny:
return deny
# ---------- 2. 获取 action 并分发 ----------
action = request.data.get('action')
if not action:
@@ -5055,15 +4978,24 @@ class PopupNoticeModifyAPIView(APIView):
def add_page(self, request):
"""添加新页面"""
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.display_config import forbid_display_write_in_all_scope
deny = forbid_display_write_in_all_scope(request)
if deny:
return deny
data = request.data
# 必填字段校验
if not data.get('page_key') or not data.get('name'):
return Response({'code': 400, 'msg': '缺少 page_key 或 name'})
# 唯一性校验
if PopupPage.query.filter(page_key=data['page_key']).exists():
club_id = resolve_club_id_from_request(request)
# 唯一性校验(同俱乐部内)
if PopupPage.query.filter(club_id=club_id, page_key=data['page_key']).exists():
return Response({'code': 400, 'msg': '页面标识已存在'})
page = PopupPage.query.create(
club_id=club_id,
page_key=data['page_key'],
name=data['name'],
description=data.get('description', ''),
@@ -5082,6 +5014,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupPage.DoesNotExist:
return Response({'code': 404, 'msg': '页面不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope
deny = forbid_popup_page_out_of_scope(request, page)
if deny:
return deny
# 更新允许修改的字段
page.page_key = request.data.get('page_key', page.page_key)
page.name = request.data.get('name', page.name)
@@ -5102,6 +5039,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupPage.DoesNotExist:
return Response({'code': 404, 'msg': '页面不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope, forbid_display_write_in_all_scope
deny = forbid_display_write_in_all_scope(request) or forbid_popup_page_out_of_scope(request, page)
if deny:
return deny
if cascade:
# 级联删除:使用事务保证原子性
with transaction.atomic():
@@ -5135,6 +5077,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupPage.DoesNotExist:
return Response({'code': 404, 'msg': '页面不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope
deny = forbid_popup_page_out_of_scope(request, page)
if deny:
return deny
# 同一页面下 popup_id 必须唯一
if PopupConfig.query.filter(page=page, popup_id=popup_id).exists():
return Response({'code': 400, 'msg': '该页面下已存在相同的弹窗ID'})
@@ -5166,6 +5113,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupConfig.DoesNotExist:
return Response({'code': 404, 'msg': '弹窗不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope
deny = forbid_popup_page_out_of_scope(request, popup.page)
if deny:
return deny
# 更新弹窗字段
popup.popup_id = request.data.get('popup_id', popup.popup_id)
popup.title = request.data.get('title', popup.title)
@@ -5192,6 +5144,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupConfig.DoesNotExist:
return Response({'code': 404, 'msg': '弹窗不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope
deny = forbid_popup_page_out_of_scope(request, popup.page)
if deny:
return deny
with transaction.atomic():
for img in popup.images.all():
if img.image_url:
@@ -5237,6 +5194,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupConfig.DoesNotExist:
return Response({'code': 404, 'msg': '弹窗不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope
deny = forbid_popup_page_out_of_scope(request, popup.page)
if deny:
return deny
# 获取上传的文件
file_obj = request.FILES.get('file')
if not file_obj:
@@ -5267,6 +5229,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupImage.DoesNotExist:
return Response({'code': 404, 'msg': '图片不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope
deny = forbid_popup_page_out_of_scope(request, image.popup_config.page)
if deny:
return deny
# 更新文字和排序
image.text = request.data.get('text', image.text)
image.sort_order = request.data.get('sort_order', image.sort_order)
@@ -5297,6 +5264,11 @@ class PopupNoticeModifyAPIView(APIView):
except PopupImage.DoesNotExist:
return Response({'code': 404, 'msg': '图片不存在'})
from jituan.services.display_config import forbid_popup_page_out_of_scope
deny = forbid_popup_page_out_of_scope(request, image.popup_config.page)
if deny:
return deny
if image.image_url:
delete_from_oss(image.image_url)
image.delete()
@@ -6191,7 +6163,7 @@ class FaKuanTongJiView(APIView):
if not has_fadan_view_permission(permissions, username):
return Response({'code': 403, 'msg': '无权限查看罚款统计'})
stats = Penalty.query.aggregate(
stats = filter_penalty_qs(Penalty.query.all(), request).aggregate(
total=Count('id'),
daijiaona=Count('id', filter=Q(Status=1)),
shensuzhong=Count('id', filter=Q(Status=3)),
@@ -6199,7 +6171,8 @@ class FaKuanTongJiView(APIView):
yibohui=Count('id', filter=Q(Status=4)),
)
data = {k: v or 0 for k, v in stats.items()}
return Response({'code': 0, 'msg': '成功', 'data': data})
from jituan.services.club_user_access import list_response_meta
return Response({'code': 0, 'msg': '成功', 'data': {**data, **list_response_meta(request)}})
except Exception as e:
logger.error(f"罚款统计异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})
@@ -6230,7 +6203,7 @@ class FaKuanLieBiaoView(APIView):
if page_size > 100:
page_size = 100
qs = Penalty.query.all()
qs = filter_penalty_qs(Penalty.query.all(), request)
# 状态筛选
zhuangtai = request.data.get('zhuangtai')
@@ -6309,6 +6282,7 @@ class FaKuanLieBiaoView(APIView):
'UpdateTime': r.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if r.UpdateTime else '',
})
from jituan.services.club_user_access import list_response_meta
return Response({
'code': 0, 'msg': '成功',
'data': {
@@ -6316,6 +6290,7 @@ class FaKuanLieBiaoView(APIView):
'total': paginator.count,
'page': page,
'page_size': page_size,
**list_response_meta(request),
}
})
except Exception as e:
@@ -6351,6 +6326,10 @@ class FaKuanChuLiView(APIView):
except Penalty.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在'})
deny = forbid_penalty_out_of_scope(request, penalty)
if deny:
return deny
if penalty.Status != 3:
return Response({'code': 400, 'msg': '当前状态不可处理'})
@@ -6479,6 +6458,9 @@ class FaKuanChuangJianView(APIView):
FineAmount=FineAmount,
AffectsGrabbing=AffectsGrabbing,
Status=1, # 待缴纳
ClubID=resolve_penalty_club_id(
penalized_user_id=PenalizedUserID, request=request,
),
)
for relative_url in uploaded_urls:
PenaltyAppealImage.query.create(
@@ -6580,6 +6562,7 @@ class FineApplyView(APIView):
Status=1, # 待缴纳
AffectsGrabbing=AffectsGrabbing,
ApplicantIdentity=1, # 申请人身份1 客服
ClubID=resolve_penalty_club_id(order=order, request=request),
)
for relative_url in uploaded_urls:
PenaltyAppealImage.query.create(
@@ -6658,6 +6641,7 @@ class PunishDashouView(APIView):
ApplyStatus=0,
DeductedPoints=jifen,
OrderID=OrderID,
ClubID=resolve_penalty_record_club_id(order_id=OrderID, player_id=dashou_id),
)
# 扣除打手积分
dashou.jifen = F('jifen') - jifen
@@ -7158,7 +7142,9 @@ class HybkjtsjView(APIView):
}
# 充值分组查询
chongzhi_qs = Czjilu.query.filter(**chongzhi_filter) \
chongzhi_qs = filter_queryset_by_club(
Czjilu.query.filter(**chongzhi_filter), request,
) \
.annotate(period=trunc_func) \
.values('period') \
.annotate(
@@ -7174,7 +7160,9 @@ class HybkjtsjView(APIView):
fenhong_agg['guanshi_amount'] = Sum('fenhong')
if include_zuzhang:
fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong')
fenhong_qs = Gsfenhong.query.filter(**fenhong_filter) \
fenhong_qs = filter_gsfenhong_qs(
Gsfenhong.query.filter(**fenhong_filter), request,
) \
.annotate(period=trunc_func) \
.values('period') \
.annotate(**fenhong_agg) \
@@ -7347,13 +7335,16 @@ class SzxxView(APIView):
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
# ---------- 收入分组查询 ----------
income_qs = DailyIncomeStat.query.filter(**income_filter) \
.values(group_field) \
income_qs = filter_queryset_by_club(
DailyIncomeStat.query.filter(**income_filter), request,
).values(group_field) \
.annotate(total_income=Sum('total_amount'), total_count=Sum('total_count')) \
.order_by(group_field)
# ---------- 支出分组查询 ----------
payout_qs = DailyPayoutStat.query.filter(**payout_filter) \
payout_qs = filter_queryset_by_club(
DailyPayoutStat.query.filter(**payout_filter), request,
).values(group_field) \
.values(group_field) \
.annotate(total_payout=Sum('total_amount'), total_count=Sum('total_count')) \
.order_by(group_field)
@@ -7395,10 +7386,14 @@ class SzxxView(APIView):
time_series.sort(key=lambda x: x['date'])
# ---------- 汇总数据 ----------
income_summary = DailyIncomeStat.query.filter(**summary_filter).aggregate(
income_summary = filter_queryset_by_club(
DailyIncomeStat.query.filter(**summary_filter), request,
).aggregate(
total_income=Sum('total_amount'), total_count=Sum('total_count')
)
payout_summary = DailyPayoutStat.query.filter(**summary_filter).aggregate(
payout_summary = filter_queryset_by_club(
DailyPayoutStat.query.filter(**summary_filter), request,
).aggregate(
total_payout=Sum('total_amount'), total_count=Sum('total_count')
)
total_income = float(income_summary['total_income'] or 0)
@@ -7542,7 +7537,10 @@ class CwhqjtddsjView(APIView):
filters['Platform'] = order_source
# ---------- 时间序列分组查询 ----------
qs = Order.query.filter(**filters) \
order_base = filter_queryset_by_club(
Order.query.filter(**filters), request, club_field='ClubID',
)
qs = order_base \
.annotate(period=trunc_func) \
.values('period') \
.annotate(
@@ -7595,7 +7593,9 @@ class CwhqjtddsjView(APIView):
})
# ---------- 汇总数据 ----------
summary_qs = Order.query.filter(**filters)
summary_qs = filter_queryset_by_club(
Order.query.filter(**filters), request, club_field='ClubID',
)
total_order_count = summary_qs.count()
total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0
total_success_count = summary_qs.filter(Status=3).count()
@@ -7728,11 +7728,14 @@ class CwqtczhqView(APIView):
# 1. 处理充值记录Czjilu—— 完全不变
cz_types = [t for t in types if t in [2, 3, 4, 5]]
if cz_types:
cz_qs = Czjilu.query.filter(
CreateTime__gte=start_dt,
CreateTime__lt=end_dt,
leixing__in=cz_types,
zhuangtai=3 # 只统计支付成功的
cz_qs = filter_queryset_by_club(
Czjilu.query.filter(
CreateTime__gte=start_dt,
CreateTime__lt=end_dt,
leixing__in=cz_types,
zhuangtai=3,
),
request,
).annotate(
period=trunc_func
).values('period', 'leixing').annotate(
@@ -7762,10 +7765,13 @@ class CwqtczhqView(APIView):
# 2. 处理罚款Penalty—— 修改:直接从 Penalty 表获取分红总额和分红笔数
if 6 in types:
# 使用一次聚合查询,同时获取罚款笔数、罚款金额、分红总额、分红笔数
penalty_qs = Penalty.query.filter(
CreateTime__gte=start_dt,
CreateTime__lt=end_dt,
Status=2 # 已缴纳
penalty_qs = filter_penalty_qs(
Penalty.query.filter(
CreateTime__gte=start_dt,
CreateTime__lt=end_dt,
Status=2,
),
request,
).annotate(
period=trunc_func
).values('period').annotate(
@@ -7816,7 +7822,9 @@ class CwqtczhqView(APIView):
filter_kwargs['CreateTime__month'] = summary_q.month
else:
filter_kwargs['CreateTime__year'] = summary_q.year
agg = Czjilu.query.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine'))
agg = filter_queryset_by_club(
Czjilu.query.filter(**filter_kwargs), request,
).aggregate(count=Count('id'), amount=Sum('jine'))
count = agg['count'] or 0
amount = float(agg['amount'] or 0)
profit = amount if lx == 3 else 0.0
@@ -7845,7 +7853,9 @@ class CwqtczhqView(APIView):
penalty_filter['CreateTime__year'] = summary_q.year
# 一次聚合:罚款笔数、金额、分红总额、分红笔数
agg = Penalty.query.filter(**penalty_filter).aggregate(
agg = filter_penalty_qs(
Penalty.query.filter(**penalty_filter), request,
).aggregate(
count=Count('id'),
amount=Sum('FineAmount'),
fenhong_amount=Sum('ApplicantBonusAmount'),
@@ -8073,7 +8083,9 @@ class CwqtczhqView(APIView):
filter_kwargs['CreateTime__month'] = summary_q.month
else:
filter_kwargs['CreateTime__year'] = summary_q.year
agg = Czjilu.query.filter(**filter_kwargs).aggregate(count=Count('id'), amount=Sum('jine'))
agg = filter_queryset_by_club(
Czjilu.query.filter(**filter_kwargs), request,
).aggregate(count=Count('id'), amount=Sum('jine'))
count = agg['count'] or 0
amount = float(agg['amount'] or 0)
profit = amount if lx == 3 else 0.0
@@ -8422,7 +8434,10 @@ class KhgglView(APIView):
filter_bankuai_name = request.data.get('bankuai_name', '').strip()
# 基础查询:所有审核官,预加载用户及打手扩展表
queryset = UserShenheguan.query.select_related('user__DashouProfile')
queryset = filter_user_related_by_club(
UserShenheguan.query.select_related('user__DashouProfile'),
request,
)
if filter_yonghuid:
queryset = queryset.filter(user__UserUID=filter_yonghuid)
@@ -8446,14 +8461,19 @@ class KhgglView(APIView):
end = start + page_size
shenheguan_list = queryset.order_by('-CreateTime')[start:end]
from jituan.services.shenhe_club import yonghu_chenghao_qs_for_request
# ---------- 预计算每个板块的打手数量 ----------
bankuai_dashou_count_map = {}
all_banquai = Bankuai.query.all()
for bk in all_banquai:
tag_ids = Chenghao.query.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True)
count = YonghuChenghao.query.filter(
chenghao_id__in=tag_ids,
yonghu__DashouProfile__isnull=False
count = yonghu_chenghao_qs_for_request(
YonghuChenghao.query.filter(
chenghao_id__in=tag_ids,
yonghu__DashouProfile__isnull=False,
),
request,
).values('yonghu').distinct().count()
bankuai_dashou_count_map[bk.bankuai_id] = count
@@ -8463,7 +8483,10 @@ class KhgglView(APIView):
tags = Chenghao.query.filter(bankuai=bk, leixing='dashou')
tag_list = []
for tag in tags:
user_count = YonghuChenghao.query.filter(chenghao=tag).values('yonghu').distinct().count()
user_count = yonghu_chenghao_qs_for_request(
YonghuChenghao.query.filter(chenghao=tag),
request,
).values('yonghu').distinct().count()
tag_list.append({
'id': tag.id,
'name': tag.mingcheng,
@@ -8772,6 +8795,7 @@ class ZxkfghdsView(APIView):
Remark=old_order.Remark,
User1ID=old_order.User1ID, # 商家标识
ImageURL=old_order.ImageURL,
ClubID=getattr(old_order, 'ClubID', None) or 'xq',
# 注意:不继承 AssignedID、PlayerID
)
@@ -9022,13 +9046,24 @@ class KhjlglView(APIView):
if page_size > 100:
page_size = 100
from jituan.services.shenhe_club import filter_shenhe_jilu_by_request
filter_data = request.data
qs = _build_shenhe_jilu_filters(filter_data)
qs = filter_shenhe_jilu_by_request(
_build_shenhe_jilu_filters(filter_data),
request,
)
# FluentQuery.filter() 会原地修改同一对象;统计与列表必须各自独立查询
stats_base = _build_shenhe_jilu_filters(filter_data)
stats_base = filter_shenhe_jilu_by_request(
_build_shenhe_jilu_filters(filter_data),
request,
)
agg = stats_base.aggregate(total_fee=Sum('jiaofei_jine'))
tag_rows = _build_shenhe_jilu_filters(filter_data).values(
tag_rows = filter_shenhe_jilu_by_request(
_build_shenhe_jilu_filters(filter_data),
request,
).values(
'chenghao_id', 'chenghao__mingcheng'
).annotate(
total=Count('jilu_id'),
@@ -9040,10 +9075,10 @@ class KhjlglView(APIView):
stats = {
'total': stats_base.count(),
'passed': _build_shenhe_jilu_filters(filter_data).filter(zhuangtai=1).count(),
'failed': _build_shenhe_jilu_filters(filter_data).filter(zhuangtai=2).count(),
'in_progress': _build_shenhe_jilu_filters(filter_data).filter(zhuangtai=0).count(),
'pending': _build_shenhe_jilu_filters(filter_data).filter(zhuangtai=3).count(),
'passed': stats_base.filter(zhuangtai=1).count(),
'failed': stats_base.filter(zhuangtai=2).count(),
'in_progress': stats_base.filter(zhuangtai=0).count(),
'pending': stats_base.filter(zhuangtai=3).count(),
'total_fee': float(agg['total_fee'] or 0),
'tag_stats': [
{