perf: P1 性能优化批量修复 + kefu 视图拆分

P1 性能优化(避免循环内 N+1 / N 次 DB 查询):
- shop_order_views._batch_images: 移除 ThreadPoolExecutor 掩盖的 N 次查询,改单次批量 + Python 侧分组
- product_query.DashouHuiyuanList: 5N 查询 -> 3 次批量预取 + 本地闭包判断
- roles.GetRolePermissionView / 用户列表: 循环内 RolePermission/Permission/UserRole/Role -> 批量 __in 预取
- guanli / zuzhang 杜次分红 4.2/4.3: 循环内 update_or_create + 单条 delete -> bulk_create + bulk_update + 批量 delete
- admin_config QQ 群配置: 循环内 exists/first/save/create -> 批量预取 + bulk_create + bulk_update
- jituan 服务层多处合并统计查询、批量预取 map
- rank 多处循环 N+1 改批量预取
- backend 多处循环内 count/create 改批量
- config/orders/merchant_ops 多处循环 N+1 改批量预取

其他改动:
- users/views/kefu.py 拆分为 kefu_base/kefu_dashou/kefu_orders/kefu_punishment/kefu_withdraw 5 个文件
- 删除遗留脚本 check_prod_uid.py / create_rbac_tables.sql
This commit is contained in:
2026-07-05 23:17:38 +08:00
parent 76ea5f8255
commit f1c8633345
54 changed files with 5332 additions and 4494 deletions

View File

@@ -580,12 +580,19 @@ class KhjlglView(APIView):
pending=Count('jilu_id', filter=Q(zhuangtai=3)), pending=Count('jilu_id', filter=Q(zhuangtai=3)),
).order_by('-total') ).order_by('-total')
_stats_agg = stats_base.aggregate(
total=Count('id'),
passed=Count('id', filter=Q(zhuangtai=1)),
failed=Count('id', filter=Q(zhuangtai=2)),
in_progress=Count('id', filter=Q(zhuangtai=0)),
pending=Count('id', filter=Q(zhuangtai=3)),
)
stats = { stats = {
'total': stats_base.count(), 'total': _stats_agg['total'],
'passed': stats_base.filter(zhuangtai=1).count(), 'passed': _stats_agg['passed'],
'failed': stats_base.filter(zhuangtai=2).count(), 'failed': _stats_agg['failed'],
'in_progress': stats_base.filter(zhuangtai=0).count(), 'in_progress': _stats_agg['in_progress'],
'pending': stats_base.filter(zhuangtai=3).count(), 'pending': _stats_agg['pending'],
'total_fee': float(agg['total_fee'] or 0), 'total_fee': float(agg['total_fee'] or 0),
'tag_stats': [ 'tag_stats': [
{ {
@@ -702,8 +709,11 @@ class KhjlczView(APIView):
for f in KaoheJiluFeiyong.query.filter(jilu_id=jilu_id).order_by('cishu') for f in KaoheJiluFeiyong.query.filter(jilu_id=jilu_id).order_by('cishu')
] ]
reject_list = [] reject_list = []
for r in KaoheJujueJilu.query.filter(jilu_id=jilu_id).order_by('-CreateTime'): _reject_records = list(KaoheJujueJilu.query.filter(jilu_id=jilu_id).order_by('-CreateTime'))
kg_prof = _batch_user_profile_map([r.shenheguan_id]).get(r.shenheguan_id, {}) _kg_ids = [r.shenheguan_id for r in _reject_records if r.shenheguan_id]
_kg_prof_map = _batch_user_profile_map(_kg_ids) if _kg_ids else {}
for r in _reject_records:
kg_prof = _kg_prof_map.get(r.shenheguan_id, {})
reject_list.append({ reject_list.append({
'cishu': r.cishu, 'cishu': r.cishu,
'shenheguan_id': r.shenheguan_id, 'shenheguan_id': r.shenheguan_id,

View File

@@ -1,4 +1,4 @@
import hmac import hmac
import threading import threading
import traceback import traceback
import uuid import uuid
@@ -12,6 +12,7 @@ import secrets
import random import random
import string import string
from calendar import monthrange from calendar import monthrange
from collections import defaultdict
from decimal import Decimal from decimal import Decimal
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from django.utils import timezone from django.utils import timezone
@@ -155,13 +156,18 @@ class CwhybkhqView(APIView):
if 'huiyuancaiwu' not in permissions: if 'huiyuancaiwu' not in permissions:
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403) return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
all_bankuai = list(Bankuai.query.all())
bankuai_ids = [bk.bankuai_id for bk in all_bankuai]
all_members = Huiyuan.query.filter(bankuai_id__in=bankuai_ids).values('huiyuan_id', 'jieshao', 'bankuai_id')
members_by_bankuai = defaultdict(list)
for m in all_members:
members_by_bankuai[m['bankuai_id']].append(dict(m))
bankuai_list = [] bankuai_list = []
for bk in Bankuai.query.all(): for bk in all_bankuai:
members = Huiyuan.query.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id')
bankuai_list.append({ bankuai_list.append({
'bankuai_id': bk.bankuai_id, 'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng, 'mingcheng': bk.mingcheng,
'members': list(members) 'members': members_by_bankuai.get(bk.bankuai_id, [])
}) })
return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list}) return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list})
@@ -597,21 +603,30 @@ class CwhqjtddsjView(APIView):
summary_qs = filter_queryset_by_club( summary_qs = filter_queryset_by_club(
Order.query.filter(**filters), request, club_field='ClubID', Order.query.filter(**filters), request, club_field='ClubID',
) )
total_order_count = summary_qs.count() agg = summary_qs.aggregate(
total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0 total_order_count=Count('id'),
total_success_count = summary_qs.filter(Status=3).count() total_order_amount=Sum('Amount'),
total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0 total_success_count=Count('id', filter=Q(Status=3)),
total_refund_count = summary_qs.filter(Status=5).count() total_success_amount=Sum('Amount', filter=Q(Status=3)),
total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0 total_refund_count=Count('id', filter=Q(Status=5)),
total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate( total_dashou=Sum('PlayerCommission', filter=Q(Status=3)),
s=Sum('pingtai_kuozhan__ShopIncome'))['s'] or 0 total_dianpu_fenhong=Sum('pingtai_kuozhan__ShopIncome', filter=Q(Status=3, Platform=1)),
platform_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0 merchant_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0 platform_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
platform_dashou_sum = \ merchant_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0 )
merchant_dashou_sum = \ total_order_count = agg['total_order_count'] or 0
summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['s'] or 0 total_order_amount = agg['total_order_amount'] or 0
total_success_count = agg['total_success_count'] or 0
total_success_amount = agg['total_success_amount'] or 0
total_refund_count = agg['total_refund_count'] or 0
total_dashou = agg['total_dashou'] or 0
total_dianpu_fenhong = agg['total_dianpu_fenhong'] or 0
platform_amount = agg['platform_amount'] or 0
merchant_amount = agg['merchant_amount'] or 0
platform_dashou_sum = agg['platform_dashou_sum'] or 0
merchant_dashou_sum = agg['merchant_dashou_sum'] or 0
profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
profit_merchant = merchant_amount - merchant_dashou_sum profit_merchant = merchant_amount - merchant_dashou_sum

View File

@@ -1,4 +1,4 @@
import hmac import hmac
import threading import threading
import traceback import traceback
import uuid import uuid
@@ -357,9 +357,16 @@ class GetGuanliDetailView(APIView):
custom_first = {} # 首次分红定制cishu=1 custom_first = {} # 首次分红定制cishu=1
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } } extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } }
# 批量预取会员信息
_huiyuan_ids = [r.huiyuan for r in duoci_records]
_huiyuan_map = {
h.huiyuan_id: h
for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao', 'jiage')
}
for record in duoci_records: for record in duoci_records:
# 获取会员信息 # 获取会员信息
member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first() member = _huiyuan_map.get(record.huiyuan)
if not member: if not member:
continue continue
member_info = { member_info = {
@@ -711,10 +718,16 @@ class UpdateGuanliView(APIView):
if custom_first is not None and isinstance(custom_first, dict): if custom_first is not None and isinstance(custom_first, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id from jituan.services.club_penalty import resolve_gsfenhong_club_id
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid) guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
# 取当前所有首次定制记录 # 批量预取当前所有首次定制记录(避免循环内 N+1
existing_first = DuociFenhong.query.filter( existing_first_list = list(
club_id=guanshi_club, yonghuid=yonghuid, cishu=1, DuociFenhong.query.filter(
club_id=guanshi_club, yonghuid=yonghuid, cishu=1,
)
) )
_existing_map = {r.huiyuan: r for r in existing_first_list}
_to_create = []
_to_update = []
_to_delete_ids = []
# 前端传来的定制字典 {会员ID: 金额} # 前端传来的定制字典 {会员ID: 金额}
for huiyuan_id, amount in custom_first.items(): for huiyuan_id, amount in custom_first.items():
try: try:
@@ -722,24 +735,34 @@ class UpdateGuanliView(APIView):
except: except:
continue continue
if amount <= 0: if amount <= 0:
DuociFenhong.query.filter( rec = _existing_map.get(huiyuan_id)
club_id=guanshi_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1, if rec is not None:
).delete() _to_delete_ids.append(rec.id)
else: else:
DuociFenhong.query.update_or_create( rec = _existing_map.get(huiyuan_id)
club_id=guanshi_club, if rec is not None:
yonghuid=yonghuid, rec.guanshi_fenhong = amount
huiyuan=huiyuan_id, rec.zuzhang_fenhong = Decimal('0')
cishu=1, _to_update.append(rec)
defaults={ else:
'guanshi_fenhong': amount, _to_create.append(DuociFenhong(
'zuzhang_fenhong': Decimal('0'), club_id=guanshi_club,
}, yonghuid=yonghuid,
) huiyuan=huiyuan_id,
cishu=1,
guanshi_fenhong=amount,
zuzhang_fenhong=Decimal('0'),
))
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制) # 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
for record in existing_first: for rec in existing_first_list:
if record.huiyuan not in custom_first: if rec.huiyuan not in custom_first and rec.id not in _to_delete_ids:
record.delete() _to_delete_ids.append(rec.id)
if _to_create:
DuociFenhong.objects.bulk_create(_to_create)
if _to_update:
DuociFenhong.objects.bulk_update(_to_update, ['guanshi_fenhong', 'zuzhang_fenhong'])
if _to_delete_ids:
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
# 4.3 额外次数分红cishu >= 2 # 4.3 额外次数分红cishu >= 2
if extra_map is not None and isinstance(extra_map, dict): if extra_map is not None and isinstance(extra_map, dict):
@@ -747,6 +770,8 @@ class UpdateGuanliView(APIView):
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid) guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
# 收集需要保留的记录 # 收集需要保留的记录
keep_set = set() keep_set = set()
# 解析前端数据,先汇总 keep_set 与目标金额
_extra_targets = [] # [(cishu, huiyuan_id, amount)]
for times_str, members in extra_map.items(): for times_str, members in extra_map.items():
try: try:
cishu = int(times_str) cishu = int(times_str)
@@ -767,24 +792,42 @@ class UpdateGuanliView(APIView):
except: except:
continue continue
keep_set.add((cishu, huiyuan_id)) keep_set.add((cishu, huiyuan_id))
# 更新或创建 _extra_targets.append((cishu, huiyuan_id, amount))
DuociFenhong.query.update_or_create( # 批量预取已有额外次数记录
existing_extra_list = list(
DuociFenhong.query.filter(
club_id=guanshi_club, yonghuid=yonghuid, cishu__gte=2,
)
)
_extra_map = {(r.cishu, r.huiyuan): r for r in existing_extra_list}
_to_create = []
_to_update = []
_to_delete_ids = []
for cishu, huiyuan_id, amount in _extra_targets:
rec = _extra_map.get((cishu, huiyuan_id))
if rec is not None:
rec.guanshi_fenhong = amount
rec.zuzhang_fenhong = Decimal('0')
_to_update.append(rec)
else:
_to_create.append(DuociFenhong(
club_id=guanshi_club, club_id=guanshi_club,
yonghuid=yonghuid, yonghuid=yonghuid,
huiyuan=huiyuan_id, huiyuan=huiyuan_id,
cishu=cishu, cishu=cishu,
defaults={ guanshi_fenhong=amount,
'guanshi_fenhong': amount, zuzhang_fenhong=Decimal('0'),
'zuzhang_fenhong': Decimal('0') ))
}
)
# 删除不在 keep_set 中的额外次数记录 # 删除不在 keep_set 中的额外次数记录
existing_extra = DuociFenhong.query.filter( for rec in existing_extra_list:
club_id=guanshi_club, yonghuid=yonghuid, cishu__gte=2, if (rec.cishu, rec.huiyuan) not in keep_set:
) _to_delete_ids.append(rec.id)
for record in existing_extra: if _to_create:
if (record.cishu, record.huiyuan) not in keep_set: DuociFenhong.objects.bulk_create(_to_create)
record.delete() if _to_update:
DuociFenhong.objects.bulk_update(_to_update, ['guanshi_fenhong', 'zuzhang_fenhong'])
if _to_delete_ids:
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
dividend_parts = [] dividend_parts = []
if perm_enabled is not None: if perm_enabled is not None:

View File

@@ -342,6 +342,12 @@ class KefuGetDashouDetailView(APIView):
member_records = Huiyuangoumai.query.filter( member_records = Huiyuangoumai.query.filter(
yonghu_id=uid yonghu_id=uid
).order_by('-CreateTime') ).order_by('-CreateTime')
# 批量预取会员信息
_huiyuan_ids = [r.huiyuan_id for r in member_records]
_huiyuan_map = {
h.huiyuan_id: h.jieshao
for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao')
}
member_list = [] member_list = []
for record in member_records: for record in member_records:
# 检查是否过期 # 检查是否过期
@@ -351,11 +357,7 @@ class KefuGetDashouDetailView(APIView):
is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
if not is_expired: if not is_expired:
try: huiyuan_name = _huiyuan_map.get(record.huiyuan_id, '未知会员')
huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id)
huiyuan_name = huiyuan.jieshao
except Huiyuan.DoesNotExist:
huiyuan_name = '未知会员'
member_list.append({ member_list.append({
'id': record.id, 'id': record.id,
'huiyuan_name': huiyuan_name, 'huiyuan_name': huiyuan_name,
@@ -828,8 +830,12 @@ class KefuGetShangjiaListView(APIView):
return Response({'code': 400, 'msg': '商家状态格式错误'}) return Response({'code': 400, 'msg': '商家状态格式错误'})
# 5. 统计总商家数(应用筛选后) # 5. 统计总商家数(应用筛选后)
total = shangjia_qs.count() _agg = shangjia_qs.aggregate(
valid_merchant_count = shangjia_qs.filter(zhuangtai=1).count() total=Count('id'),
valid=Count('id', filter=Q(zhuangtai=1)),
)
total = _agg['total']
valid_merchant_count = _agg['valid']
# 6. 分页 # 6. 分页
offset = (page - 1) * page_size offset = (page - 1) * page_size

View File

@@ -1,4 +1,4 @@
import hmac import hmac
import threading import threading
import traceback import traceback
import uuid import uuid
@@ -518,13 +518,15 @@ class FaKuanChuangJianView(APIView):
user=request.user, user=request.user,
), ),
) )
for relative_url in uploaded_urls: PenaltyAppealImage.objects.bulk_create([
PenaltyAppealImage.query.create( PenaltyAppealImage(
Penalty=penalty, Penalty=penalty,
PenalizedUserID=PenalizedUserID, PenalizedUserID=PenalizedUserID,
ImageURL=relative_url, ImageURL=relative_url,
Purpose=1 Purpose=1,
) )
for relative_url in uploaded_urls
])
from users.fadan_fenhong_utils import lock_penalty_bonus from users.fadan_fenhong_utils import lock_penalty_bonus
lock_penalty_bonus(penalty) lock_penalty_bonus(penalty)

View File

@@ -1,4 +1,4 @@
import hmac import hmac
import threading import threading
import traceback import traceback
import uuid import uuid
@@ -147,14 +147,16 @@ class GetProductBaseDataView(APIView):
# 获取所有商品类型(按 paixu 升序) # 获取所有商品类型(按 paixu 升序)
type_qs = ShangpinLeixing.query.all().order_by('paixu') type_qs = ShangpinLeixing.query.all().order_by('paixu')
# 批量聚合商品数量,避免循环内 N+1 count
_type_counts = {
item['leixing_id']: item['cnt']
for item in Shangpin.query.filter(
dianpu__isnull=True, dianpu_leixing__isnull=True, leixing_id__isnull=False
).values('leixing_id').annotate(cnt=Count('id'))
}
type_list = [] type_list = []
for t in type_qs: for t in type_qs:
# 🔥 修改:统计该类型下的公共商品数量(店铺为空且店铺商品类型为空) product_count = _type_counts.get(t.id, 0)
product_count = Shangpin.query.filter(
leixing_id=t.id,
dianpu__isnull=True, # 所属店铺为空
dianpu_leixing__isnull=True # 店铺商品类型为空
).count()
type_list.append({ type_list.append({
'id': t.id, 'id': t.id,
'jieshao': t.jieshao or '', 'jieshao': t.jieshao or '',
@@ -166,14 +168,15 @@ class GetProductBaseDataView(APIView):
# 获取所有专区(按 paixu 升序) # 获取所有专区(按 paixu 升序)
zone_qs = ShangpinZhuanqu.query.all().order_by('paixu') zone_qs = ShangpinZhuanqu.query.all().order_by('paixu')
_zone_counts = {
item['zhuanqu_id']: item['cnt']
for item in Shangpin.query.filter(
dianpu__isnull=True, dianpu_leixing__isnull=True, zhuanqu_id__isnull=False
).values('zhuanqu_id').annotate(cnt=Count('id'))
}
zone_list = [] zone_list = []
for z in zone_qs: for z in zone_qs:
# 🔥 修改:统计该专区下的公共商品数量(店铺为空且店铺商品类型为空) product_count = _zone_counts.get(z.id, 0)
product_count = Shangpin.query.filter(
zhuanqu_id=z.id,
dianpu__isnull=True, # 所属店铺为空
dianpu_leixing__isnull=True # 店铺商品类型为空
).count()
zone_list.append({ zone_list.append({
'id': z.id, 'id': z.id,
'mingzi': z.mingzi or '', 'mingzi': z.mingzi or '',
@@ -368,10 +371,14 @@ class GetProductListView(APIView):
# 获取所有商品类型(按 paixu 升序) # 获取所有商品类型(按 paixu 升序)
type_qs = ShangpinLeixing.query.all().order_by('paixu') type_qs = ShangpinLeixing.query.all().order_by('paixu')
# 批量聚合商品数量,避免循环内 N+1 count
_type_counts = {
item['leixing_id']: item['cnt']
for item in Shangpin.query.filter(leixing_id__isnull=False).values('leixing_id').annotate(cnt=Count('id'))
}
type_list = [] type_list = []
for t in type_qs: for t in type_qs:
# 统计该类型下的商品数量(忽略审核状态?通常统计所有商品,可根据需求调整) product_count = _type_counts.get(t.id, 0)
product_count = Shangpin.query.filter(leixing_id=t.id).count()
type_list.append({ type_list.append({
'id': t.id, 'id': t.id,
'jieshao': t.jieshao or '', 'jieshao': t.jieshao or '',
@@ -383,9 +390,13 @@ class GetProductListView(APIView):
# 获取所有专区(按 paixu 升序) # 获取所有专区(按 paixu 升序)
zone_qs = ShangpinZhuanqu.query.all().order_by('paixu') zone_qs = ShangpinZhuanqu.query.all().order_by('paixu')
_zone_counts = {
item['zhuanqu_id']: item['cnt']
for item in Shangpin.query.filter(zhuanqu_id__isnull=False).values('zhuanqu_id').annotate(cnt=Count('id'))
}
zone_list = [] zone_list = []
for z in zone_qs: for z in zone_qs:
product_count = Shangpin.query.filter(zhuanqu_id=z.id).count() product_count = _zone_counts.get(z.id, 0)
zone_list.append({ zone_list.append({
'id': z.id, 'id': z.id,
'mingzi': z.mingzi or '', 'mingzi': z.mingzi or '',

View File

@@ -1,4 +1,4 @@
import hmac import hmac
import threading import threading
import traceback import traceback
import uuid import uuid
@@ -157,19 +157,29 @@ class GetRolePermissionView(APIView):
kefu_user_uuids = list( kefu_user_uuids = list(
User.objects.filter(KefuProfile__isnull=False).values_list('UserUUID', flat=True) User.objects.filter(KefuProfile__isnull=False).values_list('UserUUID', flat=True)
) )
roles = filter_roles_for_request(request) roles = list(filter_roles_for_request(request))
# 批量预取 RolePermission + Permission避免循环内 N+1
_role_uuids = [r.RoleUUID for r in roles]
_rp_pairs = list(
RolePermission.objects.filter(RoleUUID__in=_role_uuids)
.values_list('RoleUUID', 'PermUUID')
) if _role_uuids else []
_perm_uuids = list({p for _, p in _rp_pairs})
_perm_rows_by_uuid = {
p['PermUUID']: p
for p in Permission.objects.filter(PermUUID__in=_perm_uuids)
.values('PermUUID', 'PermCode', 'PermName', 'PermDesc')
} if _perm_uuids else {}
_perms_by_role = {}
for role_uuid, perm_uuid in _rp_pairs:
perm_row = _perm_rows_by_uuid.get(perm_uuid)
if perm_row is not None:
_perms_by_role.setdefault(role_uuid, []).append(_serialize_permission_row(perm_row))
roles_data = [] roles_data = []
for role in roles: for role in roles:
user_count = count_role_users_in_scope(role, request, kefu_user_uuids) user_count = count_role_users_in_scope(role, request, kefu_user_uuids)
role_perm_uuids = RolePermission.objects.filter( perms_of_role = _perms_by_role.get(role.RoleUUID, [])
RoleUUID=role.RoleUUID
).values_list('PermUUID', flat=True)
perms_of_role = [
_serialize_permission_row(p)
for p in Permission.objects.filter(
PermUUID__in=list(role_perm_uuids)
).values('PermCode', 'PermName', 'PermDesc')
]
roles_data.append({ roles_data.append({
'role_code': role.RoleName, 'role_code': role.RoleName,
'role_name': role.RoleName, 'role_name': role.RoleName,
@@ -434,18 +444,29 @@ class GetAdminUserListView(APIView):
paginator = Paginator(users, page_size) paginator = Paginator(users, page_size)
page_obj = paginator.get_page(page) page_obj = paginator.get_page(page)
# 批量预取本页所有用户的 UserRole + Role避免循环内 N+1
_page_users = list(page_obj)
_page_user_uuids = [u.UserUUID for u in _page_users]
_ur_pairs = list(
UserRole.objects.filter(UserUUID__in=_page_user_uuids)
.values_list('UserUUID', 'RoleUUID')
) if _page_user_uuids else []
_role_uuids_in_page = list({r for _, r in _ur_pairs})
_role_name_by_uuid = {
r.RoleUUID: r.RoleName
for r in Role.objects.filter(RoleUUID__in=_role_uuids_in_page)
} if _role_uuids_in_page else {}
_roles_by_user = {}
for user_uuid, role_uuid in _ur_pairs:
role_name = _role_name_by_uuid.get(role_uuid)
if role_name is not None:
_roles_by_user.setdefault(user_uuid, []).append(
{'role_code': role_name, 'role_name': role_name}
)
data_list = [] data_list = []
for user in page_obj: for user in _page_users:
# 通过 gvsdsdk UserRole 获取用户角色 roles_data = _roles_by_user.get(user.UserUUID, [])
user_role_uuids = list(
UserRole.objects.filter(UserUUID=user.UserUUID)
.values_list('RoleUUID', flat=True)
)
user_roles = Role.objects.filter(RoleUUID__in=user_role_uuids)
roles_data = [
{'role_code': r.RoleName, 'role_name': r.RoleName}
for r in user_roles
]
data_list.append({ data_list.append({
'phone': user.Phone or '', 'phone': user.Phone or '',
'yonghuid': user.UserUID or '', 'yonghuid': user.UserUID or '',

View File

@@ -1,4 +1,4 @@
import hmac import hmac
import threading import threading
import traceback import traceback
import uuid import uuid
@@ -173,19 +173,27 @@ class HqbkxxView(APIView):
price_map[row.huiyuan_id] = row price_map[row.huiyuan_id] = row
huiyuan_list = [] huiyuan_list = []
for hy in Huiyuan.query.all(): # 在受限 scope 下预过滤,避免 Huiyuan 全表遍历
row = price_map.get(hy.huiyuan_id) # 注:当 scope != DATA_SCOPE_ALL 且 club_id != CLUB_ID_DEFAULT 时,
# 只有 price_map 中存在记录的会员才可能被保留(无记录的会被跳过)
huiyuan_qs = Huiyuan.query.all()
if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT:
huiyuan_qs = huiyuan_qs.filter(huiyuan_id__in=list(price_map.keys()))
# 仅查询所需字段减少内存占用dict 比 model 实例更轻)
huiyuan_qs = huiyuan_qs.values('huiyuan_id', 'jieshao', 'bankuai_id', 'jiage', 'guanshifc', 'zuzhangfc')
for hy in huiyuan_qs:
row = price_map.get(hy['huiyuan_id'])
if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT and row is None: if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT and row is None:
continue continue
if row is not None and not row.is_enabled: if row is not None and not row.is_enabled:
continue continue
item = { item = {
'huiyuan_id': hy.huiyuan_id, 'huiyuan_id': hy['huiyuan_id'],
'jieshao': hy.jieshao, 'jieshao': hy['jieshao'],
'bankuai_id': hy.bankuai_id, 'bankuai_id': hy['bankuai_id'],
'jiage': str(row.jiage) if row else str(hy.jiage), 'jiage': str(row.jiage) if row else str(hy['jiage']),
'guanshifc': str(row.guanshifc) if row else str(hy.guanshifc), 'guanshifc': str(row.guanshifc) if row else str(hy['guanshifc']),
'zuzhangfc': str(row.zuzhangfc) if row else str(hy.zuzhangfc), 'zuzhangfc': str(row.zuzhangfc) if row else str(hy['zuzhangfc']),
} }
huiyuan_list.append(item) huiyuan_list.append(item)

View File

@@ -1,4 +1,4 @@
import hmac import hmac
import threading import threading
import traceback import traceback
import uuid import uuid
@@ -241,11 +241,16 @@ class GetProductTypeZoneView(APIView):
return Response({'code': 400, 'msg': scope_err}) return Response({'code': 400, 'msg': scope_err})
# 查询所有商品类型(含专区数量) # 查询所有商品类型(含专区数量)
# 一次性分组聚合所有类型的专区数量,避免循环内 N+1 count
# 注ShangpinZhuanqu.leixing_id 是 PositiveIntegerField非 FK无法用反向 annotate
zone_count_rows = ShangpinZhuanqu.query.values('leixing_id').annotate(cnt=Count('id'))
zone_count_map = {row['leixing_id']: row['cnt'] for row in zone_count_rows}
types = ShangpinLeixing.query.all().order_by('-paixu') # paixu 越大越靠前 types = ShangpinLeixing.query.all().order_by('-paixu') # paixu 越大越靠前
type_list = [] type_list = []
for t in types: for t in types:
# 统计该类型下的专区数量 # 从预聚合的 map 中取专区数量
zone_count = ShangpinZhuanqu.query.filter(leixing_id=t.id).count() zone_count = zone_count_map.get(t.id, 0)
type_list.append({ type_list.append({
'id': t.id, 'id': t.id,
'jieshao': t.jieshao or '', 'jieshao': t.jieshao or '',
@@ -531,10 +536,11 @@ class ModifyProductTypeZoneView(APIView):
# 删除该类型下的所有专区 # 删除该类型下的所有专区
zones = ShangpinZhuanqu.query.filter(leixing_id=type_id) zones = ShangpinZhuanqu.query.filter(leixing_id=type_id)
if delete_products: if delete_products:
# 删除专区下的所有商品 # 收集 zone_ids 后批量删除,避免循环内 N+1 delete
for zone in zones: zone_ids = list(zones.values_list('id', flat=True))
Shangpin.query.filter(zhuanqu_id=zone.id).delete() if zone_ids:
zones.delete() Shangpin.query.filter(zhuanqu_id__in=zone_ids).delete()
ShangpinZhuanqu.query.filter(id__in=zone_ids).delete()
# 删除直接关联该类型的商品 # 删除直接关联该类型的商品
Shangpin.query.filter(leixing_id=type_id).delete() Shangpin.query.filter(leixing_id=type_id).delete()
else: else:
@@ -928,7 +934,8 @@ class PopupNoticeModifyAPIView(APIView):
if cascade: if cascade:
# 级联删除:使用事务保证原子性 # 级联删除:使用事务保证原子性
with transaction.atomic(): with transaction.atomic():
popups = page.popups.all() # 预加载 images 关联,避免循环内 N+1 访问 related manager
popups = page.popups.prefetch_related('images')
for popup in popups: for popup in popups:
# 删除弹窗下的所有图片文件 # 删除弹窗下的所有图片文件
for img in popup.images.all(): for img in popup.images.all():

View File

@@ -1,4 +1,4 @@
import hmac import hmac
import threading import threading
import traceback import traceback
import uuid import uuid
@@ -19,7 +19,7 @@ from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db import models, transaction, IntegrityError from django.db import models, transaction, IntegrityError
from django.db.models import Q, Count, Sum, F, Exists, OuterRef from django.db.models import Q, Count, Sum, F, Exists, OuterRef, Prefetch
from gvsdsdk.fluent import db, func, FQ from gvsdsdk.fluent import db, func, FQ
from django.db.models.functions import TruncDate, TruncMonth, TruncYear from django.db.models.functions import TruncDate, TruncMonth, TruncYear
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
@@ -132,13 +132,16 @@ class KhpzhqView(APIView):
bankuai_list = list(Bankuai.query.values('bankuai_id', 'mingcheng')) bankuai_list = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
chenghao_qs = Chenghao.query.prefetch_related('kaohecishufeiyong_set').all() chenghao_qs = Chenghao.query.prefetch_related(
Prefetch('kaohecishufeiyong_set', queryset=KaoheCishuFeiyong.objects.order_by('cishu'))
).all()
chenghao_list = [] chenghao_list = []
for ch in chenghao_qs: for ch in chenghao_qs:
# 获取费用:按次数排序,转为 {cishu: feiyong} 字典 # 获取费用:按次数排序,转为 {cishu: feiyong} 字典
# prefetch_related 已用 Prefetch 排序,直接遍历不会绕过缓存
feiyong_dict = { feiyong_dict = {
item.cishu: float(item.feiyong) item.cishu: float(item.feiyong)
for item in ch.kaohecishufeiyong_set.order_by('cishu') for item in ch.kaohecishufeiyong_set.all()
} }
max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0 max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0

View File

@@ -1,4 +1,4 @@
import hmac import hmac
import threading import threading
import traceback import traceback
import uuid import uuid
@@ -351,9 +351,16 @@ class GetZuzhangDetailView(APIView):
custom_first = {} # 首次分红定制cishu=1 custom_first = {} # 首次分红定制cishu=1
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } } extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } }
# 批量预取会员信息
_huiyuan_ids = [r.huiyuan for r in duoci_records]
_huiyuan_map = {
h.huiyuan_id: h
for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao', 'jiage')
}
for record in duoci_records: for record in duoci_records:
# 获取会员信息 # 获取会员信息
member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first() member = _huiyuan_map.get(record.huiyuan)
if not member: if not member:
continue continue
member_info = { member_info = {
@@ -580,9 +587,16 @@ class UpdateZuzhangView(APIView):
if custom_first is not None and isinstance(custom_first, dict): if custom_first is not None and isinstance(custom_first, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id from jituan.services.club_penalty import resolve_gsfenhong_club_id
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid) zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
existing_first = DuociFenhong.query.filter( # 批量预取当前所有首次定制记录(避免循环内 N+1
club_id=zuzhang_club, yonghuid=yonghuid, cishu=1, existing_first_list = list(
DuociFenhong.query.filter(
club_id=zuzhang_club, yonghuid=yonghuid, cishu=1,
)
) )
_existing_map = {r.huiyuan: r for r in existing_first_list}
_to_create = []
_to_update = []
_to_delete_ids = []
for huiyuan_id, amount in custom_first.items(): for huiyuan_id, amount in custom_first.items():
try: try:
amount = Decimal(str(amount)) amount = Decimal(str(amount))
@@ -590,30 +604,41 @@ class UpdateZuzhangView(APIView):
continue continue
if amount <= 0: if amount <= 0:
# 删除定制(恢复默认) # 删除定制(恢复默认)
DuociFenhong.query.filter( rec = _existing_map.get(huiyuan_id)
club_id=zuzhang_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1, if rec is not None:
).delete() _to_delete_ids.append(rec.id)
else: else:
DuociFenhong.query.update_or_create( rec = _existing_map.get(huiyuan_id)
club_id=zuzhang_club, if rec is not None:
yonghuid=yonghuid, rec.zuzhang_fenhong = amount
huiyuan=huiyuan_id, rec.guanshi_fenhong = Decimal('0') # 组长表只关心组长分红
cishu=1, _to_update.append(rec)
defaults={ else:
'zuzhang_fenhong': amount, _to_create.append(DuociFenhong(
'guanshi_fenhong': Decimal('0') # 组长表只关心组长分红 club_id=zuzhang_club,
} yonghuid=yonghuid,
) huiyuan=huiyuan_id,
cishu=1,
zuzhang_fenhong=amount,
guanshi_fenhong=Decimal('0'),
))
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制) # 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
for record in existing_first: for rec in existing_first_list:
if record.huiyuan not in custom_first: if rec.huiyuan not in custom_first and rec.id not in _to_delete_ids:
record.delete() _to_delete_ids.append(rec.id)
if _to_create:
DuociFenhong.objects.bulk_create(_to_create)
if _to_update:
DuociFenhong.objects.bulk_update(_to_update, ['zuzhang_fenhong', 'guanshi_fenhong'])
if _to_delete_ids:
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
# 4.3 额外次数分红cishu >= 2 # 4.3 额外次数分红cishu >= 2
if extra_map is not None and isinstance(extra_map, dict): if extra_map is not None and isinstance(extra_map, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id from jituan.services.club_penalty import resolve_gsfenhong_club_id
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid) zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
keep_set = set() keep_set = set()
_extra_targets = [] # [(cishu, huiyuan_id, amount)]
for times_str, members in extra_map.items(): for times_str, members in extra_map.items():
try: try:
cishu = int(times_str) cishu = int(times_str)
@@ -634,23 +659,42 @@ class UpdateZuzhangView(APIView):
except: except:
continue continue
keep_set.add((cishu, huiyuan_id)) keep_set.add((cishu, huiyuan_id))
DuociFenhong.query.update_or_create( _extra_targets.append((cishu, huiyuan_id, amount))
# 批量预取已有额外次数记录
existing_extra_list = list(
DuociFenhong.query.filter(
club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2,
)
)
_extra_map = {(r.cishu, r.huiyuan): r for r in existing_extra_list}
_to_create = []
_to_update = []
_to_delete_ids = []
for cishu, huiyuan_id, amount in _extra_targets:
rec = _extra_map.get((cishu, huiyuan_id))
if rec is not None:
rec.zuzhang_fenhong = amount
rec.guanshi_fenhong = Decimal('0')
_to_update.append(rec)
else:
_to_create.append(DuociFenhong(
club_id=zuzhang_club, club_id=zuzhang_club,
yonghuid=yonghuid, yonghuid=yonghuid,
huiyuan=huiyuan_id, huiyuan=huiyuan_id,
cishu=cishu, cishu=cishu,
defaults={ zuzhang_fenhong=amount,
'zuzhang_fenhong': amount, guanshi_fenhong=Decimal('0'),
'guanshi_fenhong': Decimal('0') ))
}
)
# 删除不在 keep_set 中的额外次数记录 # 删除不在 keep_set 中的额外次数记录
existing_extra = DuociFenhong.query.filter( for rec in existing_extra_list:
club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2, if (rec.cishu, rec.huiyuan) not in keep_set:
) _to_delete_ids.append(rec.id)
for record in existing_extra: if _to_create:
if (record.cishu, record.huiyuan) not in keep_set: DuociFenhong.objects.bulk_create(_to_create)
record.delete() if _to_update:
DuociFenhong.objects.bulk_update(_to_update, ['zuzhang_fenhong', 'guanshi_fenhong'])
if _to_delete_ids:
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
dividend_parts = [] dividend_parts = []
if perm_enabled is not None: if perm_enabled is not None:

View File

@@ -175,7 +175,7 @@ class ZxkfghdsView(APIView):
shangjia_ext = old_order.shangjia_kuozhan shangjia_ext = old_order.shangjia_kuozhan
shangjia_id = shangjia_ext.MerchantID shangjia_id = shangjia_ext.MerchantID
# 获取商家昵称用于新订单扩展表 # 获取商家昵称用于新订单扩展表
shangjia_user = User.query.get(UserUID=shangjia_id) shangjia_user = User.query.select_related('ShopProfile').get(UserUID=shangjia_id)
shangjia_nicheng = shangjia_user.ShopProfile.nicheng or f"商家{shangjia_id}" shangjia_nicheng = shangjia_user.ShopProfile.nicheng or f"商家{shangjia_id}"
except (ObjectDoesNotExist, UserShangjia.DoesNotExist): except (ObjectDoesNotExist, UserShangjia.DoesNotExist):
logger.error(f"商家信息缺失,订单: {OrderID}") logger.error(f"商家信息缺失,订单: {OrderID}")

View File

@@ -1,27 +0,0 @@
"""检查生产库中 UserUID 为空的用户"""
import pymysql
conn = pymysql.connect(
host='gvsds.com', port=50030,
user='root', password='sajksh.sdfGH3YUge.wjkd+',
database='xaio_cheng_xu', charset='utf8mb4'
)
cur = conn.cursor()
# UserUID 为 NULL 的用户数
cur.execute("SELECT COUNT(*) FROM user WHERE UserUID IS NULL OR UserUID = ''")
print(f'UserUID为空的用户数: {cur.fetchone()[0]}')
# 总用户数
cur.execute("SELECT COUNT(*) FROM user")
print(f'总用户数: {cur.fetchone()[0]}')
# 抽样 UserUID 为 NULL 的用户
cur.execute("SELECT UserUUID, UserName, OpenID, Phone FROM user WHERE UserUID IS NULL OR UserUID = '' LIMIT 5")
rows = cur.fetchall()
if rows:
print('\nUserUID为空的用户示例:')
for r in rows:
print(f' UserName={r[1]}, OpenID={r[2]}, Phone={r[3]}')
conn.close()

View File

@@ -1,4 +1,4 @@
import io import io
import os import os
import re import re
import time import time
@@ -394,40 +394,46 @@ class AdminUpdateConfigView(APIView):
# 🔴 修复更新QQ群配置 - 确保根据peizhiid正确更新对应的记录 # 🔴 修复更新QQ群配置 - 确保根据peizhiid正确更新对应的记录
qq_groups = request.data.get('qq_groups') qq_groups = request.data.get('qq_groups')
if qq_groups and isinstance(qq_groups, list): if qq_groups and isinstance(qq_groups, list):
# 收集所有 peizhiid批量预取已有记录避免循环内 N+1
_peizhiids = [g.get('peizhiid') for g in qq_groups if g.get('peizhiid')]
_existing_map = {}
if _peizhiids:
for q in Qunpeizhi.query.filter(GroupType__in=_peizhiids):
# 如果存在多个相同 peizhiid 的记录,取第一个(应该只有一个)
if q.GroupType not in _existing_map:
_existing_map[q.GroupType] = q
_to_create = []
_to_update = []
updated_groups = [] updated_groups = []
for group_data in qq_groups: for group_data in qq_groups:
peizhiid = group_data.get('peizhiid') peizhiid = group_data.get('peizhiid')
if not peizhiid: if not peizhiid:
continue continue
# 🔴 修复使用filter()确保查询到正确的记录 neirong = group_data.get('neirong')
qun_configs = Qunpeizhi.query.filter(GroupType=peizhiid) qunid = group_data.get('qunid')
jieshao = group_data.get('jieshao')
if qun_configs.exists():
# 如果存在多个相同peizhiid的记录取第一个应该只有一个
qun_config = qun_configs.first()
# 更新字段
neirong = group_data.get('neirong')
qunid = group_data.get('qunid')
jieshao = group_data.get('jieshao')
qun_config = _existing_map.get(peizhiid)
if qun_config is not None:
# 更新已有记录
if neirong is not None: if neirong is not None:
qun_config.GroupContent = neirong or '' qun_config.GroupContent = neirong or ''
if qunid is not None: if qunid is not None:
qun_config.GroupID = qunid or '' qun_config.GroupID = qunid or ''
if jieshao is not None: if jieshao is not None:
qun_config.Description = jieshao or '' qun_config.Description = jieshao or ''
_to_update.append(qun_config)
qun_config.save()
else: else:
# 如果不存在创建新的使用提供的peizhiid # 不存在创建新的
qun_config = Qunpeizhi.query.create( qun_config = Qunpeizhi(
GroupType=peizhiid, GroupType=peizhiid,
GroupContent=group_data.get('neirong', '') or '', GroupContent=neirong or '',
GroupID=group_data.get('qunid', '') or '', GroupID=qunid or '',
Description=group_data.get('jieshao', '') or '' Description=jieshao or '',
) )
_to_create.append(qun_config)
updated_groups.append({ updated_groups.append({
'peizhiid': qun_config.GroupType, 'peizhiid': qun_config.GroupType,
@@ -436,6 +442,13 @@ class AdminUpdateConfigView(APIView):
'jieshao': qun_config.Description 'jieshao': qun_config.Description
}) })
if _to_create:
Qunpeizhi.objects.bulk_create(_to_create)
if _to_update:
Qunpeizhi.objects.bulk_update(
_to_update, ['GroupContent', 'GroupID', 'Description']
)
if updated_groups: if updated_groups:
response_data['qq_groups'] = updated_groups response_data['qq_groups'] = updated_groups

View File

@@ -1,4 +1,4 @@
import io import io
import os import os
import re import re
import time import time
@@ -270,6 +270,13 @@ class DashouPaihangView(APIView):
# 第一名数量不足,不展示 # 第一名数量不足,不展示
return [], False return [], False
# 批量预取老板扩展表昵称
_boss_user_ids = [g.user_id for g in guanshi_list]
_boss_nicheng_map = {
b.user_id: b.nickname
for b in UserBoss.query.filter(user_id__in=_boss_user_ids).only('user_id', 'nickname')
}
# 构建返回数据 # 构建返回数据
paihang_list = [] paihang_list = []
for index, guanshi in enumerate(guanshi_list): for index, guanshi in enumerate(guanshi_list):
@@ -277,8 +284,7 @@ class DashouPaihangView(APIView):
user_main = guanshi.user user_main = guanshi.user
# 管事的昵称需要查询老板扩展表 # 管事的昵称需要查询老板扩展表
boss_profile = UserBoss.query.filter(user=user_main).first() guanshi_nicheng = _boss_nicheng_map.get(guanshi.user_id, '未设置昵称')
guanshi_nicheng = boss_profile.nickname if boss_profile else '未设置昵称'
# 获取头像相对URL # 获取头像相对URL
touxiang_url = user_main.Avatar or '' touxiang_url = user_main.Avatar or ''
@@ -337,6 +343,13 @@ class DashouPaihangView(APIView):
# 第一名流水不足,不展示 # 第一名流水不足,不展示
return [], False return [], False
# 批量预取老板扩展表昵称
_boss_user_ids = [s.user_id for s in shangjia_list]
_boss_nicheng_map = {
b.user_id: b.nickname
for b in UserBoss.query.filter(user_id__in=_boss_user_ids).only('user_id', 'nickname')
}
# 构建返回数据 # 构建返回数据
paihang_list = [] paihang_list = []
for index, shangjia in enumerate(shangjia_list): for index, shangjia in enumerate(shangjia_list):
@@ -344,8 +357,7 @@ class DashouPaihangView(APIView):
user_main = shangjia.user user_main = shangjia.user
# 商家的昵称需要查询老板扩展表 # 商家的昵称需要查询老板扩展表
boss_profile = UserBoss.query.filter(user=user_main).first() shangjia_nicheng = _boss_nicheng_map.get(shangjia.user_id, '未设置昵称')
shangjia_nicheng = boss_profile.nickname if boss_profile else '未设置昵称'
# 获取头像相对URL # 获取头像相对URL
touxiang_url = user_main.Avatar or '' touxiang_url = user_main.Avatar or ''
@@ -604,21 +616,21 @@ class GetXiugaiJiluView(APIView):
records = cursor.fetchall() records = cursor.fetchall()
# 🔥 6. 处理查询结果(严格按照字段类型处理) # 🔥 6. 处理查询结果(严格按照字段类型处理)
# 预查询修改者账号信息(循环外执行一次,避免循环内重复查询同一 SQL
xiugaizhe_zhanghao = ''
with connection.cursor() as cursor2:
cursor2.execute(
"SELECT phone, yonghuid FROM user_main WHERE phone = %s OR yonghuid = %s LIMIT 1",
[xiugaizhe_zhanghaoid, xiugaizhe_zhanghaoid]
)
xiugaizhe_info = cursor2.fetchone()
if xiugaizhe_info:
xiugaizhe_zhanghao = str(xiugaizhe_info[0]) if xiugaizhe_info[0] else str(xiugaizhe_info[1]) if \
xiugaizhe_info[1] else xiugaizhe_zhanghaoid
jilu_list = [] jilu_list = []
for record in records: for record in records:
# 获取修改者账号信息(都是字符串) # 修改者账号信息已预查询,直接使用
xiugaizhe_zhanghao = ''
with connection.cursor() as cursor2:
# 🔥 phone和yonghuid都是CharField用字符串查询
cursor2.execute(
"SELECT phone, yonghuid FROM user_main WHERE phone = %s OR yonghuid = %s LIMIT 1",
[xiugaizhe_zhanghaoid, xiugaizhe_zhanghaoid]
)
xiugaizhe_info = cursor2.fetchone()
if xiugaizhe_info:
# 🔥 都是字符串
xiugaizhe_zhanghao = str(xiugaizhe_info[0]) if xiugaizhe_info[0] else str(xiugaizhe_info[1]) if \
xiugaizhe_info[1] else xiugaizhe_zhanghaoid
# 获取会员名称(如果有) # 获取会员名称(如果有)
huiyuan_mingzi = '' huiyuan_mingzi = ''

View File

@@ -1,4 +1,4 @@
import io import io
import os import os
import re import re
import time import time
@@ -508,7 +508,7 @@ class KehuGetDingdanLianjieView(APIView):
# 获取商家昵称 # 获取商家昵称
shangjia_nicheng = '' shangjia_nicheng = ''
try: try:
user_main = User.query.get(UserUID=lianjie_obj.UserID) user_main = User.query.select_related('ShopProfile').get(UserUID=lianjie_obj.UserID)
shangjia_obj = user_main.ShopProfile shangjia_obj = user_main.ShopProfile
shangjia_nicheng = shangjia_obj.nicheng shangjia_nicheng = shangjia_obj.nicheng
except (User.DoesNotExist, UserShangjia.DoesNotExist): except (User.DoesNotExist, UserShangjia.DoesNotExist):
@@ -816,7 +816,7 @@ class KehuTianxieDingdanView(APIView):
# 验证打手是否存在且有效 # 验证打手是否存在且有效
try: try:
# 先查询用户主表 # 先查询用户主表
dashou_user = User.query.get(UserUID=zhiding_dashou) dashou_user = User.query.select_related('DashouProfile').get(UserUID=zhiding_dashou)
logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}") logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}")
# 通过反向关系获取打手扩展信息 # 通过反向关系获取打手扩展信息
@@ -922,7 +922,7 @@ class KehuTianxieDingdanView(APIView):
# 获取商家信息用于返回 # 获取商家信息用于返回
try: try:
user_main = User.query.get(UserUID=lianjie_obj.UserID) user_main = User.query.select_related('ShopProfile').get(UserUID=lianjie_obj.UserID)
shangjia_obj = user_main.ShopProfile shangjia_obj = user_main.ShopProfile
shangjia_mingcheng = shangjia_obj.nicheng shangjia_mingcheng = shangjia_obj.nicheng
except: except:

View File

@@ -1,4 +1,4 @@
import io import io
import os import os
import re import re
import time import time
@@ -142,17 +142,21 @@ class ShangjiaMobanListView(APIView):
return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400) return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400)
current_page = paginator.page(page) current_page = paginator.page(page)
# 批量预取所有称号,避免循环内 N+1
_mobans_list = list(current_page.object_list)
_title_ids = [m.TitleID for m in _mobans_list if m.TitleID]
_ch_map = {
ch.id: ch
for ch in Chenghao.query.filter(id__in=_title_ids).only('id', 'mingcheng', 'texiao_miaoshu')
} if _title_ids else {}
formatted = [] formatted = []
for moban in current_page.object_list: for moban in _mobans_list:
texiao_json = '' texiao_json = ''
label_name = '' label_name = ''
if moban.TitleID: ch = _ch_map.get(moban.TitleID)
try: if ch:
ch = Chenghao.query.get(id=moban.TitleID) label_name = ch.mingcheng
label_name = ch.mingcheng texiao_json = ch.texiao_miaoshu or ''
texiao_json = ch.texiao_miaoshu or ''
except Chenghao.DoesNotExist:
pass
formatted.append({ formatted.append({
'mobanId': moban.id, 'mobanId': moban.id,
'shangpinTypeId': moban.ProductTypeID, 'shangpinTypeId': moban.ProductTypeID,

View File

@@ -1,82 +0,0 @@
-- ============================================================
-- RBAC 表创建脚本
-- 仅创建生产数据库中缺失的表,已存在的表会被跳过
-- 执行方式: mysql -u root -p xaio_cheng_xu < create_rbac_tables.sql
-- 注意: 索引和外键单独处理,避免重复创建报错
-- ============================================================
-- ============================================================
-- Part 1: backend RBAC 表小写表名verify_kefu_permission 使用)
-- ============================================================
CREATE TABLE IF NOT EXISTS `role` (
`role_id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`role_code` varchar(50) NOT NULL UNIQUE,
`role_name` varchar(50) NOT NULL,
`description` varchar(200) NOT NULL,
`create_time` datetime(6) NOT NULL,
`update_time` datetime(6) NOT NULL
);
CREATE TABLE IF NOT EXISTS `permission` (
`perm_id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`perm_code` varchar(100) NOT NULL UNIQUE,
`perm_name` varchar(100) NOT NULL,
`description` varchar(200) NOT NULL,
`create_time` datetime(6) NOT NULL,
`update_time` datetime(6) NOT NULL
);
CREATE TABLE IF NOT EXISTS `user_role` (
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`account_id` varchar(11) NOT NULL,
`create_time` datetime(6) NOT NULL,
`update_time` datetime(6) NOT NULL,
`role_id` integer NOT NULL,
UNIQUE KEY `user_role_account_id_role_id_uniq` (`account_id`, `role_id`)
);
CREATE TABLE IF NOT EXISTS `role_permission` (
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
`create_time` datetime(6) NOT NULL,
`permission_id` integer NOT NULL,
`role_id` integer NOT NULL,
UNIQUE KEY `role_permission_role_id_permission_id_uniq` (`role_id`, `permission_id`)
);
-- backend 表索引(忽略已存在的错误)
CREATE INDEX `user_role_account_id_idx` ON `user_role` (`account_id`);
-- backend 外键约束
ALTER TABLE `user_role` ADD CONSTRAINT `user_role_role_fk` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`);
ALTER TABLE `role_permission` ADD CONSTRAINT `role_permission_permission_fk` FOREIGN KEY (`permission_id`) REFERENCES `permission` (`perm_id`);
ALTER TABLE `role_permission` ADD CONSTRAINT `role_permission_role_fk` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`);
-- ============================================================
-- Part 2: gvsdsdk RBAC 表大写表名UUID 主键)
-- ============================================================
CREATE TABLE IF NOT EXISTS `AccountActivation` (`AccountActivationCode` BINARY(6) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `EphemeralToken` (`ELTUUID` BINARY(16) NOT NULL PRIMARY KEY, `ELTUserUUID` BINARY(16) NOT NULL, `ELTCreateTime` datetime(6) NOT NULL, `ELTEndTime` datetime(6) NOT NULL, `ELTType` smallint NOT NULL, `ELTAllowIP` json NOT NULL, `ELTStatus` smallint NOT NULL);
CREATE TABLE IF NOT EXISTS `Password` (`UserUUID` BINARY(16) NOT NULL PRIMARY KEY, `Password` varchar(1024) NOT NULL);
CREATE TABLE IF NOT EXISTS `Permission` (`PermUUID` BINARY(16) NOT NULL PRIMARY KEY, `TenantUUID` BINARY(16) NOT NULL, `PermName` varchar(128) NOT NULL, `PermCode` varchar(128) NOT NULL, `PermCategory` varchar(64) NOT NULL, `PermStatus` smallint NOT NULL, `PermDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `Position` (`PositionUUID` BINARY(16) NOT NULL PRIMARY KEY, `TenantUUID` BINARY(16) NOT NULL, `PositionName` varchar(128) NOT NULL, `PositionCode` varchar(64) NOT NULL, `PositionLevel` smallint NOT NULL, `PositionStatus` smallint NOT NULL, `PositionDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL, `UpdateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `PositionTemplate` (`PositionTemplateUUID` BINARY(16) NOT NULL PRIMARY KEY, `TenantUUID` BINARY(16) NOT NULL, `TemplateName` varchar(128) NOT NULL, `TemplateCode` varchar(64) NOT NULL, `TemplateStatus` smallint NOT NULL, `TemplateDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL, `UpdateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `PositionTemplatePermission` (`PTPermUUID` BINARY(16) NOT NULL PRIMARY KEY, `PositionTemplateUUID` BINARY(16) NOT NULL, `PermUUID` BINARY(16) NOT NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `Role` (`RoleUUID` BINARY(16) NOT NULL PRIMARY KEY, `TenantUUID` BINARY(16) NOT NULL, `RoleName` varchar(128) NOT NULL, `RoleType` smallint NOT NULL, `RoleStatus` smallint NOT NULL, `AssignScope` smallint NOT NULL, `AssignScopeUUID` BINARY(16) NULL, `RoleDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `RolePermission` (`RolePermUUID` BINARY(16) NOT NULL PRIMARY KEY, `RoleUUID` BINARY(16) NOT NULL, `PermUUID` BINARY(16) NOT NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `RoleShop` (`RoleShopUUID` BINARY(16) NOT NULL PRIMARY KEY, `RoleUUID` BINARY(16) NOT NULL, `ShopUUID` BINARY(16) NOT NULL, `IsMainShop` smallint NOT NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `Shop` (`ShopUUID` BINARY(16) NOT NULL PRIMARY KEY, `CompanyUUID` BINARY(16) NOT NULL, `ShopName` varchar(128) NOT NULL, `ShopCode` varchar(64) NOT NULL, `ShopType` smallint NOT NULL, `ShopStatus` smallint NOT NULL, `ShopAddress` varchar(512) NOT NULL, `ShopContact` varchar(128) NOT NULL, `ShopPhone` varchar(32) NOT NULL, `ShopDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL, `UpdateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `User2FA` (`User2FAUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `SecretKey` varchar(64) NOT NULL, `IsEnabled` smallint NOT NULL, `Verified` smallint NOT NULL, `LastUsedAt` datetime(6) NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `UserABAC` (`UserABACUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `HierarchicalPermissionIdentifiers` varchar(256) NOT NULL);
CREATE TABLE IF NOT EXISTS `UserAvatar` (`UserUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserAvatar` longblob NOT NULL, `AvatarUploadTime` datetime(6) NOT NULL, `AvatarFileType` varchar(16) NOT NULL);
CREATE TABLE IF NOT EXISTS `UserDept` (`UserDeptUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `DeptUUID` BINARY(16) NOT NULL, `PositionUUID` BINARY(16) NULL, `IsMainDept` smallint NOT NULL, `CreateTime` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `UserLoginLog` (`LogUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `LoginIP` varchar(39) NOT NULL, `LoginPlatform` varchar(32) NOT NULL, `LoginStatus` smallint NOT NULL, `LoginTime` datetime(6) NOT NULL, `UserAgent` varchar(512) NOT NULL);
CREATE TABLE IF NOT EXISTS `UserPending2FA` (`PendingUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL UNIQUE, `SecretKey` varchar(64) NOT NULL, `ExpireAt` datetime(6) NOT NULL);
CREATE TABLE IF NOT EXISTS `UserRole` (`UserRoleUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `RoleUUID` BINARY(16) NOT NULL, `CompanyUUID` BINARY(16) NOT NULL, `CreateTime` datetime(6) NOT NULL);
-- gvsdsdk 索引
CREATE INDEX `EphemELTUserUUID_idx` ON `EphemeralToken` (`ELTUserUUID`);
CREATE INDEX `EphemELTStatus_idx` ON `EphemeralToken` (`ELTStatus`);
CREATE INDEX `User2FA_UserUUID_idx` ON `User2FA` (`UserUUID`);
CREATE INDEX `UserLoginLog_UserUUID_idx` ON `UserLoginLog` (`UserUUID`);

View File

@@ -1,7 +1,7 @@
"""俱乐部维度财务统计(供 /jituan/houtai/caiwu旧 /houtai/caiwu 不变)。""" """俱乐部维度财务统计(供 /jituan/houtai/caiwu旧 /houtai/caiwu 不变)。"""
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from django.db.models import Sum from django.db.models import Count, Q, Sum
from jituan.constants import DATA_SCOPE_ALL from jituan.constants import DATA_SCOPE_ALL
from jituan.services.club_context import ( from jituan.services.club_context import (
@@ -61,31 +61,35 @@ def build_caiwu_payload(request):
today_income_amount, today_income_count = today_income_for_dashboard(today, request) today_income_amount, today_income_count = today_income_for_dashboard(today, request)
today_payout_amount, today_payout_count = sum_payout_stat_for_date(today, request) today_payout_amount, today_payout_count = sum_payout_stat_for_date(today, request)
today_orders = order_qs.filter( # 合并订单统计6 次独立 count/aggregate → 1 次 aggregate
_today_order_agg = order_qs.filter(
CreateTime__gte=today_start, CreateTime__gte=today_start,
CreateTime__lt=today_end, CreateTime__lt=today_end,
Status__in=[1, 2, 3, 4, 5, 6, 7, 8], Status__in=[1, 2, 3, 4, 5, 6, 7, 8],
).aggregate(
today_order_count=Count('id'),
today_order_amount=Sum('Amount'),
today_completed_count=Count('id', filter=Q(Status=3)),
today_completed_amount=Sum('Amount', filter=Q(Status=3)),
today_tuikuan_count=Count('id', filter=Q(Status=5)),
today_tuikuan_amount=Sum('Amount', filter=Q(Status=5)),
) )
today_order_count = today_orders.count() today_order_count = _today_order_agg['today_order_count'] or 0
today_order_amount = float(today_orders.aggregate(total=Sum('Amount'))['total'] or 0.00) today_order_amount = float(_today_order_agg['today_order_amount'] or 0.00)
today_completed_count = _today_order_agg['today_completed_count'] or 0
today_completed_amount = float(_today_order_agg['today_completed_amount'] or 0.00)
today_tuikuan_count = _today_order_agg['today_tuikuan_count'] or 0
today_tuikuan_amount = float(_today_order_agg['today_tuikuan_amount'] or 0.00)
today_completed = order_qs.filter( # 合并充值统计2 次独立 count/aggregate → 1 次 aggregate
CreateTime__gte=today_start, CreateTime__lt=today_end, Status=3, _today_chongzhi_agg = cz_qs.filter(
)
today_completed_count = today_completed.count()
today_completed_amount = float(today_completed.aggregate(total=Sum('Amount'))['total'] or 0.00)
today_tuikuan = order_qs.filter(
CreateTime__gte=today_start, CreateTime__lt=today_end, Status=5,
)
today_tuikuan_count = today_tuikuan.count()
today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('Amount'))['total'] or 0.00)
today_chongzhi = cz_qs.filter(
CreateTime__gte=today_start, CreateTime__lt=today_end, leixing=1, zhuangtai=3, CreateTime__gte=today_start, CreateTime__lt=today_end, leixing=1, zhuangtai=3,
).aggregate(
count=Count('id'),
total=Sum('jine'),
) )
today_chongzhi_count = today_chongzhi.count() today_chongzhi_count = _today_chongzhi_agg['count'] or 0
today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00) today_chongzhi_amount = float(_today_chongzhi_agg['total'] or 0.00)
new_huiyuan_user_ids = hy_qs.filter( new_huiyuan_user_ids = hy_qs.filter(
CreateTime__gte=today_start, CreateTime__lt=today_end, CreateTime__gte=today_start, CreateTime__lt=today_end,

View File

@@ -141,10 +141,13 @@ def count_order_status_buckets(q_conditions, request=None):
('5', Q(Status=5)), ('5', Q(Status=5)),
('6', Q(Status=6)), ('6', Q(Status=6)),
) )
counts = {'all': order_scope_qs(request).filter(q_conditions).count()} # 合并 8 次独立 count → 1 次 aggregate with 条件 Count
from django.db.models import Count
_base = order_scope_qs(request).filter(q_conditions)
_agg_kwargs = {'all': Count('id')}
for key, status_q in buckets: for key, status_q in buckets:
counts[key] = order_scope_qs(request).filter(q_conditions & status_q).count() _agg_kwargs[key] = Count('id', filter=status_q)
return counts return _base.aggregate(**_agg_kwargs)
def paginate_fluent_query(fluent_qs, page, page_size): def paginate_fluent_query(fluent_qs, page, page_size):

View File

@@ -95,7 +95,12 @@ def count_club_member_sales(club_id, huiyuan_id, formal_only=True):
def build_member_list_payload(request): def build_member_list_payload(request):
scope = resolve_club_scope(request) scope = resolve_club_scope(request)
club_id = resolve_club_id_from_request(request) club_id = resolve_club_id_from_request(request)
members = Huiyuan.query.all().order_by('-CreateTime') # P1 优化:限制查询字段,避免加载不必要的大字段
members = list(Huiyuan.query.all().only(
'huiyuan_id', 'jieshao', 'jtjieshao', 'jiage',
'guanshifc', 'zuzhangfc', 'goumai_cishu', 'bankuai_id',
'CreateTime', 'is_bundle',
).order_by('-CreateTime'))
member_list = [] member_list = []
price_map = {} price_map = {}
@@ -105,6 +110,20 @@ def build_member_list_payload(request):
for r in ClubHuiyuanPrice.query.filter(club_id=club_id) for r in ClubHuiyuanPrice.query.filter(club_id=club_id)
} }
# P1 优化:批量预取本俱乐部各会员的已支付单数,避免循环内 N+1
_sales_map = {}
if scope != DATA_SCOPE_ALL and members:
from products.models import Czjilu
from django.db.models import Count
_sales_rows = Czjilu.query.filter(
club_id=club_id,
huiyuan_id__in=[m.huiyuan_id for m in members],
leixing=1,
zhuangtai=3,
is_trial=False,
).values('huiyuan_id').annotate(cnt=Count('id'))
_sales_map = {r['huiyuan_id']: r['cnt'] for r in _sales_rows}
if scope == DATA_SCOPE_ALL: if scope == DATA_SCOPE_ALL:
for m in members: for m in members:
member_list.append(_format_member(m)) member_list.append(_format_member(m))
@@ -116,7 +135,7 @@ def build_member_list_payload(request):
if row is not None and not row.is_enabled: if row is not None and not row.is_enabled:
continue continue
item = _format_member(m, row, club_id=club_id) item = _format_member(m, row, club_id=club_id)
item['goumai_cishu'] = count_club_member_sales(club_id, m.huiyuan_id) item['goumai_cishu'] = _sales_map.get(m.huiyuan_id, 0)
member_list.append(item) member_list.append(item)
return { return {
@@ -138,17 +157,18 @@ def build_member_catalog_payload(request):
enabled_ids = set( enabled_ids = set(
ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True).values_list('huiyuan_id', flat=True) ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True).values_list('huiyuan_id', flat=True)
) )
all_members = Huiyuan.query.all().order_by('-CreateTime') # P1 优化DB 端排除已上架会员,限制字段,避免全表加载 + Python 过滤
catalog = [] catalog_qs = Huiyuan.query.all()
for m in all_members: if enabled_ids:
if m.huiyuan_id in enabled_ids: catalog_qs = catalog_qs.exclude(huiyuan_id__in=list(enabled_ids))
continue catalog = list(
catalog.append({ catalog_qs.only('huiyuan_id', 'jieshao', 'jtjieshao', 'is_bundle')
'huiyuan_id': m.huiyuan_id, .order_by('-CreateTime')
'jieshao': m.jieshao, .values('huiyuan_id', 'jieshao', 'jtjieshao', 'is_bundle')
'jtjieshao': m.jtjieshao, )
'is_bundle': bool(getattr(m, 'is_bundle', False)), # values 返回 dict 列表,需补 is_bundle bool 化
}) for row in catalog:
row['is_bundle'] = bool(row.get('is_bundle'))
return {'club_id': club_id, 'catalog': catalog} return {'club_id': club_id, 'catalog': catalog}

View File

@@ -218,14 +218,22 @@ def build_exam_bundle(request, user):
return None, '题库暂无已上架题目,请至少录入一道题并上架(每题至少两个选项且标记正确答案)' return None, '题库暂无已上架题目,请至少录入一道题并上架(每题至少两个选项且标记正确答案)'
selected_ids = random.sample(all_ids, draw) selected_ids = random.sample(all_ids, draw)
# 批量预取题目和图片,避免循环内 N+1
_questions_map = {
q.id: q for q in ClubDashouExamQuestion.query.filter(id__in=selected_ids)
}
_imgs_map = {}
for img in ClubDashouExamQuestionImage.query.filter(
club_id=club_id, question_id__in=selected_ids,
).order_by('question_id', 'sort_order', 'id'):
_imgs_map.setdefault(img.question_id, []).append(img.image_url)
questions = [] questions = []
for qid in selected_ids: for qid in selected_ids:
q = ClubDashouExamQuestion.query.get(id=qid) q = _questions_map.get(qid)
imgs = [ if q is None:
r.image_url for r in ClubDashouExamQuestionImage.query.filter( continue
club_id=club_id, question_id=qid, imgs = _imgs_map.get(qid, [])
).order_by('sort_order', 'id')
]
questions.append(build_question_bundle_item(q, imgs)) questions.append(build_question_bundle_item(q, imgs))
return { return {
@@ -284,8 +292,14 @@ def build_admin_config_payload(request, user, data=None):
if err: if err:
return {**clubs_info, 'scope_error': err} return {**clubs_info, 'scope_error': err}
cfg = get_or_create_config(club_id) cfg = get_or_create_config(club_id)
pool = ClubDashouExamQuestion.query.filter(club_id=club_id).count() # 合并 2 次独立 count → 1 次 aggregate with 条件 Count
enabled_pool = ClubDashouExamQuestion.query.filter(club_id=club_id, is_enabled=True).count() from django.db.models import Count, Q
_pool_agg = ClubDashouExamQuestion.query.filter(club_id=club_id).aggregate(
pool=Count('id'),
enabled_pool=Count('id', filter=Q(is_enabled=True)),
)
pool = _pool_agg['pool'] or 0
enabled_pool = _pool_agg['enabled_pool'] or 0
return { return {
**clubs_info, **clubs_info,
'club_id': club_id, 'club_id': club_id,
@@ -319,14 +333,20 @@ def list_admin_questions(request, user, data=None):
if err: if err:
return {**exam_admin_clubs_payload(user), 'list': [], 'scope_error': err} return {**exam_admin_clubs_payload(user), 'list': [], 'scope_error': err}
rows = ClubDashouExamQuestion.query.filter(club_id=club_id).order_by('-sort_order', 'id') rows = ClubDashouExamQuestion.query.filter(club_id=club_id).order_by('-sort_order', 'id')
out = [] rows_list = list(rows)
for q in rows: _q_ids = [q.id for q in rows_list]
imgs = [ # 批量预取所有题目图片,避免循环内 N+1
_imgs_map = {}
for img in ClubDashouExamQuestionImage.query.filter(
club_id=club_id, question_id__in=_q_ids,
).order_by('question_id', 'sort_order', 'id'):
_imgs_map.setdefault(img.question_id, []).append(
{'id': img.id, 'image_url': img.image_url, 'sort_order': img.sort_order} {'id': img.id, 'image_url': img.image_url, 'sort_order': img.sort_order}
for img in ClubDashouExamQuestionImage.query.filter( )
club_id=club_id, question_id=q.id,
).order_by('sort_order', 'id') out = []
] for q in rows_list:
imgs = _imgs_map.get(q.id, [])
out.append(question_to_admin_dict(q, imgs)) out.append(question_to_admin_dict(q, imgs))
return {**exam_admin_clubs_payload(user), 'club_id': club_id, 'list': out} return {**exam_admin_clubs_payload(user), 'club_id': club_id, 'list': out}

View File

@@ -203,19 +203,31 @@ def build_order_finance_payload(
}) })
summary_qs = filter_queryset_by_club(Order.query.filter(**filters), request, club_field='ClubID') summary_qs = filter_queryset_by_club(Order.query.filter(**filters), request, club_field='ClubID')
total_order_count = summary_qs.count() # 合并 13 次独立 count/aggregate → 1 次 aggregate
total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0 _summary_agg = summary_qs.aggregate(
total_success_count = summary_qs.filter(Status=3).count() total_order_count=Count('id'),
total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0 total_order_amount=Sum('Amount'),
total_refund_count = summary_qs.filter(Status=5).count() total_success_count=Count('id', filter=Q(Status=3)),
total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0 total_success_amount=Sum('Amount', filter=Q(Status=3)),
total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate( total_refund_count=Count('id', filter=Q(Status=5)),
s=Sum('pingtai_kuozhan__ShopIncome'), total_dashou=Sum('PlayerCommission', filter=Q(Status=3)),
)['s'] or 0 total_dianpu_fenhong=Sum('pingtai_kuozhan__ShopIncome', filter=Q(Status=3, Platform=1)),
platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0 platform_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0 merchant_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
platform_dashou_sum = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0 platform_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
merchant_dashou_sum = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['s'] or 0 merchant_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
)
total_order_count = _summary_agg['total_order_count'] or 0
total_order_amount = _summary_agg['total_order_amount'] or 0
total_success_count = _summary_agg['total_success_count'] or 0
total_success_amount = _summary_agg['total_success_amount'] or 0
total_refund_count = _summary_agg['total_refund_count'] or 0
total_dashou = _summary_agg['total_dashou'] or 0
total_dianpu_fenhong = _summary_agg['total_dianpu_fenhong'] or 0
platform_amount = _summary_agg['platform_amount'] or 0
merchant_amount = _summary_agg['merchant_amount'] or 0
platform_dashou_sum = _summary_agg['platform_dashou_sum'] or 0
merchant_dashou_sum = _summary_agg['merchant_dashou_sum'] or 0
profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
profit_merchant = merchant_amount - merchant_dashou_sum profit_merchant = merchant_amount - merchant_dashou_sum
total_profit = profit_platform + profit_merchant total_profit = profit_platform + profit_merchant
@@ -392,10 +404,16 @@ def build_huiyuan_bankuai_payload(request):
'zuzhangfc': float(row.zuzhangfc or 0), 'zuzhangfc': float(row.zuzhangfc or 0),
} }
# 批量预取所有 bankuai 和 huiyuan避免循环内 N+1
_bankuai_qs = Bankuai.query.all().only('bankuai_id', 'mingcheng')
_members_by_bankuai = {}
for m in Huiyuan.query.values('huiyuan_id', 'jieshao', 'bankuai_id'):
_members_by_bankuai.setdefault(m['bankuai_id'], []).append(m)
bankuai_list = [] bankuai_list = []
for bk in Bankuai.query.all(): for bk in _bankuai_qs:
members = [] members = []
for m in Huiyuan.query.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id'): for m in _members_by_bankuai.get(bk.bankuai_id, []):
item = dict(m) item = dict(m)
if m['huiyuan_id'] in price_map: if m['huiyuan_id'] in price_map:
item['club_price'] = price_map[m['huiyuan_id']] item['club_price'] = price_map[m['huiyuan_id']]

View File

@@ -188,11 +188,11 @@ def zero_all_income_statistics():
except _PLATFORM_LOG_ERRORS: except _PLATFORM_LOG_ERRORS:
pass pass
DailyIncomeStat.objects.all().delete() DailyIncomeStat.objects.all().delete()
for szjilu in Szjilu.objects.select_for_update().all(): from django.utils import timezone
szjilu.TotalIncome = _ZERO Szjilu.objects.update(
szjilu.TotalFlow = _ZERO TotalIncome=_ZERO, TotalFlow=_ZERO, DailyFlow=_ZERO,
szjilu.DailyFlow = _ZERO UpdateTime=timezone.now(),
szjilu.save(update_fields=['TotalIncome', 'TotalFlow', 'DailyFlow', 'UpdateTime']) )
return True return True
@@ -689,13 +689,12 @@ def get_szjilu_snapshot(club_id=None):
def reset_all_daily_szjilu(): def reset_all_daily_szjilu():
"""每日清零:所有俱乐部的今日流水/今日支出。""" """每日清零:所有俱乐部的今日流水/今日支出。"""
updated = 0
with transaction.atomic(): with transaction.atomic():
for szjilu in Szjilu.objects.select_for_update().all(): from django.utils import timezone
szjilu.DailyExpense = _ZERO updated = Szjilu.objects.update(
szjilu.DailyFlow = _ZERO DailyExpense=_ZERO, DailyFlow=_ZERO,
szjilu.save(update_fields=['DailyExpense', 'DailyFlow', 'UpdateTime']) UpdateTime=timezone.now(),
updated += 1 )
return updated return updated

View File

@@ -71,8 +71,13 @@ class ClubKhgglAddView(APIView):
if not user_belongs_to_request_club(user, request): if not user_belongs_to_request_club(user, request):
return Response({'code': 403, 'msg': '该用户不属于当前俱乐部,无法添加为考核官'}, status=403) return Response({'code': 403, 'msg': '该用户不属于当前俱乐部,无法添加为考核官'}, status=403)
# 一次性查询所有板块是否存在,避免循环内 N+1 exists
_existing_bids = set(
Bankuai.query.filter(bankuai_id__in=bankuai_ids)
.values_list('bankuai_id', flat=True)
)
for bid in bankuai_ids: for bid in bankuai_ids:
if not Bankuai.query.filter(bankuai_id=bid).exists(): if bid not in _existing_bids:
return Response({'code': 1, 'msg': f'板块不存在: {bid}'}, status=400) return Response({'code': 1, 'msg': f'板块不存在: {bid}'}, status=400)
created_new = False created_new = False

View File

@@ -109,7 +109,7 @@ def staff_dispatch_order(member, staff_user, data):
zhiding_dashou = None zhiding_dashou = None
if zhiding_uid: if zhiding_uid:
try: try:
zhiding_user = User.query.get(UserUID=zhiding_uid) zhiding_user = User.query.select_related('DashouProfile').get(UserUID=zhiding_uid)
zhiding_dashou = zhiding_user.DashouProfile zhiding_dashou = zhiding_user.DashouProfile
if zhiding_dashou.zhanghaozhuangtai != 1: if zhiding_dashou.zhanghaozhuangtai != 1:
return False, {'code': 400, 'msg': '指定打手状态异常', 'data': None} return False, {'code': 400, 'msg': '指定打手状态异常', 'data': None}

View File

@@ -119,6 +119,7 @@ def staff_generate_link(member, staff_user, data):
final_huiyuan_id = shangpin_leixing.huiyuan_id or '' final_huiyuan_id = shangpin_leixing.huiyuan_id or ''
label_name = '' label_name = ''
ch = None
if chenghao_id: if chenghao_id:
ch = Chenghao.query.filter(id=chenghao_id).first() ch = Chenghao.query.filter(id=chenghao_id).first()
if ch: if ch:
@@ -155,10 +156,9 @@ def staff_generate_link(member, staff_user, data):
MerchantNickname=shangjia.nicheng or f"商家{merchant_id}", MerchantNickname=shangjia.nicheng or f"商家{merchant_id}",
) )
if chenghao_id: # 复用已查询的 ch 对象,避免重复查询
ch = Chenghao.query.filter(id=chenghao_id).first() if ch:
if ch: DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=ch)
DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=ch)
UserShangjia.query.filter(id=shangjia.id).update( UserShangjia.query.filter(id=shangjia.id).update(
fabu=F('fabu') + 1, fabu=F('fabu') + 1,

View File

@@ -128,8 +128,16 @@ class StaffInviteListView(APIView):
qs = MerchantStaffInvite.query.filter(merchant_id=request.user.UserUID).select_related('role') qs = MerchantStaffInvite.query.filter(merchant_id=request.user.UserUID).select_related('role')
if status_filter is not None: if status_filter is not None:
qs = qs.filter(status=int(status_filter)) qs = qs.filter(status=int(status_filter))
invites = list(qs.order_by('-CreateTime')[:200])
# 批量预取已使用邀请码对应的用户,避免循环内 N+1
from users.business_models import User
_used_uids = [inv.used_by for inv in invites if inv.used_by]
_user_map = {
u.UserUID: u
for u in User.query.filter(UserUID__in=_used_uids).only('UserUID', 'Nickname')
} if _used_uids else {}
items = [] items = []
for inv in qs.order_by('-CreateTime')[:200]: for inv in invites:
item = { item = {
'id': inv.id, 'id': inv.id,
'code': inv.invite_code, 'code': inv.invite_code,
@@ -140,8 +148,7 @@ class StaffInviteListView(APIView):
'user': None, 'user': None,
} }
if inv.used_by: if inv.used_by:
from users.business_models import User u = _user_map.get(inv.used_by)
u = User.query.filter(UserUID=inv.used_by).first()
item['user'] = {'nickname': getattr(u, 'Nickname', '') or inv.used_by, 'uid': inv.used_by} item['user'] = {'nickname': getattr(u, 'Nickname', '') or inv.used_by, 'uid': inv.used_by}
items.append(item) items.append(item)
return Response({'code': 0, 'msg': '成功', 'data': {'list': items, 'gudingCode': 'XQKF2026'}}) return Response({'code': 0, 'msg': '成功', 'data': {'list': items, 'gudingCode': 'XQKF2026'}})
@@ -181,21 +188,51 @@ class StaffMemberListView(APIView):
merchant_id=mid, merchant_id=mid,
status__in=[MEMBER_STATUS_ACTIVE, MEMBER_STATUS_DISABLED], status__in=[MEMBER_STATUS_ACTIVE, MEMBER_STATUS_DISABLED],
).select_related('role') ).select_related('role')
result = [] members_list = list(members)
for m in members: _member_ids = [m.id for m in members_list]
total_stats = MerchantStaffDailyStats.query.filter(member_id=m.id).aggregate( _staff_uids = [m.staff_user_id for m in members_list if m.staff_user_id]
# 批量预取聚合统计,避免循环内 N+1
_total_stats_map = {}
_today_stat_map = {}
if _member_ids:
from django.db.models import Count
for item in MerchantStaffDailyStats.query.filter(
member_id__in=_member_ids
).values('member_id').annotate(
dispatch_count=Sum('dispatch_count'), dispatch_count=Sum('dispatch_count'),
dispatch_amount=Sum('dispatch_amount'), dispatch_amount=Sum('dispatch_amount'),
settle_count=Sum('settle_count'), settle_count=Sum('settle_count'),
settle_amount=Sum('settle_amount'), settle_amount=Sum('settle_amount'),
refund_count=Sum('refund_count'), refund_count=Sum('refund_count'),
refund_amount=Sum('refund_amount'), refund_amount=Sum('refund_amount'),
) ):
today_stat = MerchantStaffDailyStats.query.filter( _total_stats_map[item['member_id']] = item
member_id=m.id, stat_date=today, for s in MerchantStaffDailyStats.query.filter(
).first() member_id__in=_member_ids, stat_date=today,
wallet = get_or_create_wallet(m) ):
u = User.query.filter(UserUID=m.staff_user_id).first() _today_stat_map[s.member_id] = s
# 批量预取用户头像
_user_map = {
u.UserUID: u
for u in User.query.filter(UserUID__in=_staff_uids).only('UserUID', 'Avatar')
} if _staff_uids else {}
# 批量预取钱包
_wallet_map = {
w.member_id: w
for w in MerchantStaffWallet.objects.filter(member_id__in=_member_ids)
} if _member_ids else {}
result = []
for m in members_list:
total_stats = _total_stats_map.get(m.id, {})
today_stat = _today_stat_map.get(m.id)
wallet = _wallet_map.get(m.id)
if wallet is None:
wallet = get_or_create_wallet(m)
u = _user_map.get(m.staff_user_id)
result.append({ result.append({
'id': m.id, 'id': m.id,
'uid': m.staff_user_id, 'uid': m.staff_user_id,
@@ -210,13 +247,13 @@ class StaffMemberListView(APIView):
'isDisabled': m.status == MEMBER_STATUS_DISABLED, 'isDisabled': m.status == MEMBER_STATUS_DISABLED,
'todayDispatchCount': today_stat.dispatch_count if today_stat else int(m.today_dispatch_count or 0), 'todayDispatchCount': today_stat.dispatch_count if today_stat else int(m.today_dispatch_count or 0),
'todayDispatchAmount': str(today_stat.dispatch_amount if today_stat else (m.today_dispatch_amount or '0.00')), 'todayDispatchAmount': str(today_stat.dispatch_amount if today_stat else (m.today_dispatch_amount or '0.00')),
'dispatchCount': total_stats['dispatch_count'] or 0, 'dispatchCount': total_stats.get('dispatch_count') or 0,
'dispatchAmount': str(total_stats['dispatch_amount'] or '0.00'), 'dispatchAmount': str(total_stats.get('dispatch_amount') or '0.00'),
'orderCount': total_stats['dispatch_count'] or 0, 'orderCount': total_stats.get('dispatch_count') or 0,
'settleCount': total_stats['settle_count'] or 0, 'settleCount': total_stats.get('settle_count') or 0,
'settleAmount': str(total_stats['settle_amount'] or '0.00'), 'settleAmount': str(total_stats.get('settle_amount') or '0.00'),
'refundCount': total_stats['refund_count'] or 0, 'refundCount': total_stats.get('refund_count') or 0,
'refundAmount': str(total_stats['refund_amount'] or '0.00'), 'refundAmount': str(total_stats.get('refund_amount') or '0.00'),
'quota_available': str(wallet.quota_available), 'quota_available': str(wallet.quota_available),
'quota_total': str(wallet.quota_total), 'quota_total': str(wallet.quota_total),
'quota_used': str(wallet.quota_used), 'quota_used': str(wallet.quota_used),
@@ -618,9 +655,15 @@ class StaffRankView(APIView):
sort_field = sort_by if sort_by in ('dispatch_count', 'dispatch_amount', 'settle_count', 'settle_amount') else 'dispatch_count' sort_field = sort_by if sort_by in ('dispatch_count', 'dispatch_amount', 'settle_count', 'settle_amount') else 'dispatch_count'
agg = sorted(agg, key=lambda x: x[sort_field] or 0, reverse=True)[:50] agg = sorted(agg, key=lambda x: x[sort_field] or 0, reverse=True)[:50]
# 批量预取 member 信息,避免循环内 N+1
_member_ids = [row['member_id'] for row in agg]
_members_map = {
m.id: m
for m in MerchantStaffMember.query.filter(id__in=_member_ids).only('id', 'display_name')
} if _member_ids else {}
result = [] result = []
for idx, row in enumerate(agg, 1): for idx, row in enumerate(agg, 1):
m = MerchantStaffMember.query.filter(id=row['member_id']).first() m = _members_map.get(row['member_id'])
result.append({ result.append({
'rank': idx, 'rank': idx,
'member_id': row['member_id'], 'member_id': row['member_id'],

View File

@@ -364,7 +364,7 @@ class AdGengHuanDaShou(APIView):
# 8. 验证新打手ID是否存在并且是打手身份 # 8. 验证新打手ID是否存在并且是打手身份
try: try:
new_dashou_user = User.query.get( new_dashou_user = User.query.select_related('DashouProfile').get(
UserUID=new_dashou_id, UserUID=new_dashou_id,
#user_type='PlayerID' #user_type='PlayerID'
) )
@@ -459,7 +459,7 @@ class AdGengHuanDaShou(APIView):
if old_dashou_id: if old_dashou_id:
try: try:
# 查询原打手主表 # 查询原打手主表
old_dashou_user = User.query.get( old_dashou_user = User.query.select_related('DashouProfile').get(
UserUID=old_dashou_id, UserUID=old_dashou_id,
#user_type='PlayerID' #user_type='PlayerID'
) )
@@ -567,7 +567,7 @@ class AdQiangZhiJieDan(APIView):
# 6. 查询订单主表 # 6. 查询订单主表
try: try:
dingdan_obj = Order.query.get(OrderID=dingdan_id) dingdan_obj = Order.query.select_related('shangjia_kuozhan', 'pingtai_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist: except Order.DoesNotExist:
return Response( return Response(
{'code': 404, 'message': '订单不存在', 'data': None}, {'code': 404, 'message': '订单不存在', 'data': None},
@@ -603,7 +603,7 @@ class AdQiangZhiJieDan(APIView):
if jiedan_dashou_id: if jiedan_dashou_id:
try: try:
# 查询打手用户主表 # 查询打手用户主表
dashou_user = User.query.get( dashou_user = User.query.select_related('DashouProfile').get(
UserUID=jiedan_dashou_id, UserUID=jiedan_dashou_id,
#user_type='PlayerID' #user_type='PlayerID'
) )
@@ -646,7 +646,7 @@ class AdQiangZhiJieDan(APIView):
if shangjia_id: if shangjia_id:
# 查询商家用户主表 # 查询商家用户主表
shangjia_user = User.query.get( shangjia_user = User.query.select_related('ShopProfile').get(
UserUID=shangjia_id, UserUID=shangjia_id,
#user_type='shop' #user_type='shop'
) )
@@ -750,7 +750,7 @@ class AdJuJueTuiKuan(APIView):
# 6. 查询订单主表 # 6. 查询订单主表
try: try:
dingdan_obj = Order.query.get(OrderID=dingdan_id) dingdan_obj = Order.query.select_related('shangjia_kuozhan', 'pingtai_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist: except Order.DoesNotExist:
return Response( return Response(
{'code': 404, 'message': '订单不存在', 'data': None}, {'code': 404, 'message': '订单不存在', 'data': None},
@@ -779,7 +779,7 @@ class AdJuJueTuiKuan(APIView):
if jiedan_dashou_id: if jiedan_dashou_id:
try: try:
# 查询打手用户主表 # 查询打手用户主表
dashou_user = User.query.get( dashou_user = User.query.select_related('DashouProfile').get(
UserUID=jiedan_dashou_id, UserUID=jiedan_dashou_id,
#user_type='PlayerID' #user_type='PlayerID'
) )
@@ -822,7 +822,7 @@ class AdJuJueTuiKuan(APIView):
if shangjia_id: if shangjia_id:
# 查询商家用户主表 # 查询商家用户主表
shangjia_user = User.query.get( shangjia_user = User.query.select_related('ShopProfile').get(
UserUID=shangjia_id, UserUID=shangjia_id,
#user_type='shop' #user_type='shop'
) )
@@ -958,7 +958,7 @@ class AdTongYiTuiKuanShangJia(APIView):
# 7. 查询订单主表 # 7. 查询订单主表
try: try:
dingdan_obj = Order.query.get(OrderID=dingdan_id) dingdan_obj = Order.query.select_related('shangjia_kuozhan', 'pingtai_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist: except Order.DoesNotExist:
return Response( return Response(
{'code': 404, 'message': '订单不存在', 'data': None}, {'code': 404, 'message': '订单不存在', 'data': None},
@@ -1010,7 +1010,7 @@ class AdTongYiTuiKuanShangJia(APIView):
if jiedan_dashou_id: if jiedan_dashou_id:
try: try:
# 查询打手用户主表 # 查询打手用户主表
dashou_user = User.query.get( dashou_user = User.query.select_related('DashouProfile').get(
UserUID=jiedan_dashou_id, UserUID=jiedan_dashou_id,
#user_type='PlayerID' #user_type='PlayerID'
) )
@@ -1035,10 +1035,10 @@ class AdTongYiTuiKuanShangJia(APIView):
if shangjia_id: if shangjia_id:
try: try:
# 查询商家用户主表 # 查询商家用户主表
shangjia_user = User.query.get( shangjia_user = User.query.select_related('ShopProfile').get(
UserUID=shangjia_id, UserUID=shangjia_id,
#user_type='shop' #user_type='shop'
) )
# 获取商家扩展表 # 获取商家扩展表
shangjia_profile = shangjia_user.ShopProfile shangjia_profile = shangjia_user.ShopProfile
@@ -1228,7 +1228,7 @@ class AdTongYiTuiKuanPingTai(APIView):
# 14. 更新打手扩展表 # 14. 更新打手扩展表
if jiedan_dashou_id: if jiedan_dashou_id:
try: try:
dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou_user = User.query.select_related('DashouProfile').get(UserUID=jiedan_dashou_id)
dashou_profile = dashou_user.DashouProfile dashou_profile = dashou_user.DashouProfile
dashou_profile.zhuangtai = 1 # 打手状态设为空闲 dashou_profile.zhuangtai = 1 # 打手状态设为空闲
dashou_profile.tuikuanliang += 1 dashou_profile.tuikuanliang += 1
@@ -1239,7 +1239,7 @@ class AdTongYiTuiKuanPingTai(APIView):
# 15. 更新老板扩展表 # 15. 更新老板扩展表
if laoban_id: if laoban_id:
try: try:
laoban_user = User.query.get(UserUID=laoban_id) laoban_user = User.query.select_related('BossProfile').get(UserUID=laoban_id)
laoban_profile = laoban_user.BossProfile laoban_profile = laoban_user.BossProfile
laoban_profile.alltui += 1 laoban_profile.alltui += 1
laoban_profile.zonge -= jine laoban_profile.zonge -= jine
@@ -1463,7 +1463,7 @@ class AdJuJueJieSuan(APIView):
# 8. 更新打手扩展表 # 8. 更新打手扩展表
try: try:
dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou_user = User.query.select_related('DashouProfile').get(UserUID=jiedan_dashou_id)
dashou_profile = dashou_user.DashouProfile dashou_profile = dashou_user.DashouProfile
dashou_profile.zhuangtai = 1 # 打手状态设为正常 dashou_profile.zhuangtai = 1 # 打手状态设为正常
dashou_profile.tuikuanliang += 1 # 退款订单总量+1 dashou_profile.tuikuanliang += 1 # 退款订单总量+1
@@ -1545,7 +1545,7 @@ class AdZhuanYiDaTing(APIView):
# 如果有接单打手,更新其扩展表 # 如果有接单打手,更新其扩展表
if jiedan_dashou_id: if jiedan_dashou_id:
try: try:
dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou_user = User.query.select_related('DashouProfile').get(UserUID=jiedan_dashou_id)
dashou_profile = dashou_user.DashouProfile dashou_profile = dashou_user.DashouProfile
dashou_profile.zhuangtai = 1 dashou_profile.zhuangtai = 1
dashou_profile.tuikuanliang += 1 dashou_profile.tuikuanliang += 1
@@ -1835,7 +1835,7 @@ class ZxsjghdsView(APIView):
# 6.2 释放打手 # 6.2 释放打手
if dashou_id: if dashou_id:
try: try:
dashou_user = User.query.get(UserUID=dashou_id) dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
dashou_profile = dashou_user.DashouProfile dashou_profile = dashou_user.DashouProfile
dashou_profile.tuikuanliang = (dashou_profile.tuikuanliang or 0) + 1 dashou_profile.tuikuanliang = (dashou_profile.tuikuanliang or 0) + 1
dashou_profile.zhuangtai = 1 # 空闲 dashou_profile.zhuangtai = 1 # 空闲

View File

@@ -422,11 +422,14 @@ class JiedanView(APIView):
# 7. 更新打手扩展表如果有打手ID和分成 # 7. 更新打手扩展表如果有打手ID和分成
if jiedan_dashou_id and dashou_fencheng > 0: if jiedan_dashou_id and dashou_fencheng > 0:
# 查询打手用户 # 查询打手用户
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first() dashou_user = User.query.filter(UserUID=jiedan_dashou_id).select_related('DashouProfile').first()
if dashou_user: if dashou_user:
# 查询打手扩展表 # 查询打手扩展表
dashou_kuozhan = UserDashou.query.filter(user=dashou_user).first() try:
dashou_kuozhan = dashou_user.DashouProfile
except UserDashou.DoesNotExist:
dashou_kuozhan = None
if dashou_kuozhan: if dashou_kuozhan:
# 更新四个金额字段 # 更新四个金额字段
@@ -585,7 +588,7 @@ class DingdanXiangqingView2(APIView):
jiedan_time = '' jiedan_time = ''
tijiao_time = '' tijiao_time = ''
if jiedan_dashou_id: if jiedan_dashou_id:
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first() dashou_user = User.query.filter(UserUID=jiedan_dashou_id).select_related('DashouProfile').first()
if dashou_user: if dashou_user:
dashou_touxiang = dashou_user.Avatar or '' dashou_touxiang = dashou_user.Avatar or ''
try: try:
@@ -734,7 +737,7 @@ class JiedanView2(APIView):
# 9. 更新打手收益与状态 # 9. 更新打手收益与状态
if jiedan_dashou_id and dashou_fencheng > 0: if jiedan_dashou_id and dashou_fencheng > 0:
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first() dashou_user = User.query.filter(UserUID=jiedan_dashou_id).select_related('DashouProfile').first()
if dashou_user: if dashou_user:
try: try:
dashou_ext = dashou_user.DashouProfile dashou_ext = dashou_user.DashouProfile

View File

@@ -193,13 +193,13 @@ def send_group_message(
def check_user_permission(identity_type, uid, order): def check_user_permission(identity_type, uid, order):
"""验证用户身份、账号状态、以及是否属于该订单""" """验证用户身份、账号状态、以及是否属于该订单"""
try: try:
user = User.query.get(UserUID=uid) user = User.query.select_related('DashouProfile').get(UserUID=uid)
except User.DoesNotExist: except User.DoesNotExist:
return False, '用户不存在' return False, '用户不存在'
if identity_type == 'PlayerID': if identity_type == 'PlayerID':
try: try:
dashou = UserDashou.query.get(user=user) dashou = user.DashouProfile
if dashou.zhanghaozhuangtai != 1 : if dashou.zhanghaozhuangtai != 1 :
return False, '打手账号已被封禁或状态异常' return False, '打手账号已被封禁或状态异常'
except UserDashou.DoesNotExist: except UserDashou.DoesNotExist:
@@ -516,14 +516,16 @@ class LianTongDingDanZhuangTaiPiLiangView(APIView):
return Response({'code': 400, 'msg': 'order_ids 格式错误'}) return Response({'code': 400, 'msg': 'order_ids 格式错误'})
uid = request.user.UserUID uid = request.user.UserUID
# 批量查询订单,避免循环内 N+1
oids = [str(oid).strip() for oid in order_ids[:20] if str(oid).strip()]
orders_map = {
o.OrderID: o
for o in Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').filter(OrderID__in=oids)
}
result = {} result = {}
for oid in order_ids[:20]: for oid in oids:
oid = str(oid).strip() order = orders_map.get(oid)
if not oid: if not order:
continue
try:
order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=oid)
except Order.DoesNotExist:
continue continue
for identity in ('PlayerID', 'shangjia', 'boss'): for identity in ('PlayerID', 'shangjia', 'boss'):
valid, _ = check_user_permission(identity, uid, order) valid, _ = check_user_permission(identity, uid, order)

View File

@@ -521,11 +521,13 @@ class ShangjiaShujuTongjiView(APIView):
penalty_stats = None penalty_stats = None
try: try:
from orders.models import Penalty, PenaltyRecord from orders.models import Penalty, PenaltyRecord
from django.db.models import Q, Sum from django.db.models import Q, Sum, Count
fakuan_pending = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).count() _fakuan_agg = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).aggregate(
fakuan_pending_amt = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).aggregate( count=Count('id'),
amt=Sum('FineAmount') amt=Sum('FineAmount'),
)['amt'] )
fakuan_pending = _fakuan_agg['count']
fakuan_pending_amt = _fakuan_agg['amt']
jifen_pending = PenaltyRecord.query.filter( jifen_pending = PenaltyRecord.query.filter(
ApplicantID=yonghuid, ApplicantID=yonghuid,
).filter(Q(ApplyStatus=0) | Q(ApplyStatus=3)).count() ).filter(Q(ApplyStatus=0) | Q(ApplyStatus=3)).count()
@@ -1204,7 +1206,7 @@ class ShangjiaTuikuanShenqingView(APIView):
try: try:
dashou_main = User.query.filter( dashou_main = User.query.filter(
UserUID=dingdan.PlayerID UserUID=dingdan.PlayerID
).first() ).select_related('DashouProfile').first()
if dashou_main and hasattr(dashou_main, 'DashouProfile'): if dashou_main and hasattr(dashou_main, 'DashouProfile'):
dashou_profile = dashou_main.DashouProfile dashou_profile = dashou_main.DashouProfile
dashou_profile.zhuangtai = 1 dashou_profile.zhuangtai = 1
@@ -1285,7 +1287,7 @@ class ShangjiaChufaShenqingView(APIView):
with transaction.atomic(): with transaction.atomic():
# 验证打手身份 # 验证打手身份
try: try:
dashou_user = User.query.get(UserUID=dashou_id) dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
dashou = dashou_user.DashouProfile dashou = dashou_user.DashouProfile
except User.DoesNotExist: except User.DoesNotExist:
return Response({ return Response({
@@ -1367,13 +1369,16 @@ class ShangjiaChufaShenqingView(APIView):
# 🔴【新增】保存处罚图片到数据库 # 🔴【新增】保存处罚图片到数据库
if chufa_tupian_urls and isinstance(chufa_tupian_urls, list): if chufa_tupian_urls and isinstance(chufa_tupian_urls, list):
for tupian_url in chufa_tupian_urls: _valid_urls = [u.strip() for u in chufa_tupian_urls if u and u.strip()]
if tupian_url and tupian_url.strip(): if _valid_urls:
PenaltyEvidenceImage.query.create( PenaltyEvidenceImage.objects.bulk_create([
PenaltyEvidenceImage(
OrderID=dingdan_id, OrderID=dingdan_id,
UserID=current_user.UserUID, UserID=current_user.UserUID,
ImageURL=tupian_url ImageURL=url,
) )
for url in _valid_urls
])
# 更新商家订单扩展表的处罚相关字段 # 更新商家订单扩展表的处罚相关字段
shangjia_kuozhan.PenaltyReason = chufa_liyou shangjia_kuozhan.PenaltyReason = chufa_liyou

View File

@@ -188,13 +188,15 @@ class ShangjiaFakuanApplyView(APIView):
), ),
ShopStaffUserID=staff_kefu_id, ShopStaffUserID=staff_kefu_id,
) )
for url in evidence_urls: PenaltyAppealImage.objects.bulk_create([
PenaltyAppealImage.query.create( PenaltyAppealImage(
Penalty=fadan, Penalty=fadan,
PenalizedUserID=dashou_id, PenalizedUserID=dashou_id,
ImageURL=url, ImageURL=url,
Purpose=1, Purpose=1,
) )
for url in evidence_urls
])
lock_penalty_bonus(fadan) lock_penalty_bonus(fadan)
logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}提交罚款申请(平台审核),金额{fakuanjine}") logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}提交罚款申请(平台审核),金额{fakuanjine}")
return Response({'code': 0, 'msg': '罚款申请已提交,等待平台审核', 'data': {'fadan_id': fadan.id}}) return Response({'code': 0, 'msg': '罚款申请已提交,等待平台审核', 'data': {'fadan_id': fadan.id}})
@@ -362,16 +364,17 @@ class ShangjiaFakuaiXiugaiView(APIView):
existing_count = PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=1).count() existing_count = PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=1).count()
if existing_count + len(add_urls) > 9: if existing_count + len(add_urls) > 9:
return Response({'code': 400, 'msg': '证据图片最多9张'}) return Response({'code': 400, 'msg': '证据图片最多9张'})
for url in add_urls: _valid_add_urls = [str(u).strip() for u in add_urls if str(u).strip()]
url = str(url).strip() if _valid_add_urls:
if not url: PenaltyAppealImage.objects.bulk_create([
continue PenaltyAppealImage(
PenaltyAppealImage.query.create( Penalty=fadan,
Penalty=fadan, PenalizedUserID=fadan.PenalizedUserID,
PenalizedUserID=fadan.PenalizedUserID, ImageURL=url,
ImageURL=url, Purpose=1,
Purpose=1, )
) for url in _valid_add_urls
])
logger.info( logger.info(
f"罚单{fadan.id}证据图变更:新增{len(add_urls)}张,删除{len(remove_ids)}" f"罚单{fadan.id}证据图变更:新增{len(add_urls)}张,删除{len(remove_ids)}"
) )

View File

@@ -204,7 +204,9 @@ class WechatPayNotifyView(APIView):
# 锁定订单防止并发 # 锁定订单防止并发
try: try:
dingdan = Order.query.select_for_update().filter(OrderID=out_trade_no).first() dingdan = Order.query.select_for_update().select_related(
'pingtai_kuozhan', 'shangjia_kuozhan'
).filter(OrderID=out_trade_no).first()
except Exception as e: except Exception as e:
logger.error(f"查询订单异常: {e}") logger.error(f"查询订单异常: {e}")
raise raise
@@ -263,7 +265,7 @@ class WechatPayNotifyView(APIView):
if hasattr(dingdan, 'pingtai_kuozhan'): if hasattr(dingdan, 'pingtai_kuozhan'):
laoban_id = dingdan.pingtai_kuozhan.BossID laoban_id = dingdan.pingtai_kuozhan.BossID
if laoban_id: if laoban_id:
user_main = User.query.filter(UserUID=laoban_id).first() user_main = User.query.filter(UserUID=laoban_id).select_related('BossProfile').first()
if user_main and hasattr(user_main, 'BossProfile'): if user_main and hasattr(user_main, 'BossProfile'):
boss = user_main.BossProfile boss = user_main.BossProfile
boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0) boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0)
@@ -537,7 +539,7 @@ class PaymentVerifyView(APIView):
# 更新老板扩展表 # 更新老板扩展表
laoban_id = dingdan.pingtai_kuozhan.BossID if hasattr(dingdan, 'pingtai_kuozhan') else None laoban_id = dingdan.pingtai_kuozhan.BossID if hasattr(dingdan, 'pingtai_kuozhan') else None
if laoban_id: if laoban_id:
user_main = User.query.filter(UserUID=laoban_id).first() user_main = User.query.filter(UserUID=laoban_id).select_related('BossProfile').first()
if user_main and hasattr(user_main, 'BossProfile'): if user_main and hasattr(user_main, 'BossProfile'):
boss = user_main.BossProfile boss = user_main.BossProfile
boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0) boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0)
@@ -709,13 +711,16 @@ class CreateOrderView(APIView):
zhiding_user = User.query.filter( zhiding_user = User.query.filter(
UserUID=zhiding, UserUID=zhiding,
#user_type='PlayerID' #user_type='PlayerID'
).first() ).select_related('DashouProfile').first()
if not zhiding_user: if not zhiding_user:
return Response({'code': 7, 'msg': '指定打手不存在', 'data': None}) return Response({'code': 7, 'msg': '指定打手不存在', 'data': None})
# 查询打手扩展表 # 查询打手扩展表
dashou_profile = UserDashou.query.filter(user=zhiding_user).first() try:
dashou_profile = zhiding_user.DashouProfile
except UserDashou.DoesNotExist:
dashou_profile = None
if not dashou_profile: if not dashou_profile:
return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None}) return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None})

View File

@@ -268,35 +268,98 @@ class DashouHuiyuanList(APIView):
}, status=status.HTTP_400_BAD_REQUEST) }, status=status.HTTP_400_BAD_REQUEST)
# 3. 仅返回本俱乐部可售会员club_huiyuan_price 已配置且启用) # 3. 仅返回本俱乐部可售会员club_huiyuan_price 已配置且启用)
from jituan.services.member_recharge import club_huiyuan_sellable, can_purchase_trial from jituan.constants import CLUB_ID_DEFAULT
club_id = resolve_effective_club_id(request, request.user)
huiyuan_queryset = Huiyuan.query.all().only(
'huiyuan_id', 'jieshao', 'jtjieshao', 'jiage',
).order_by('jiage')
from jituan.models import ClubHuiyuanPrice from jituan.models import ClubHuiyuanPrice
club_rows = { from decimal import Decimal as _Decimal
r.huiyuan_id: r
for r in ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True) club_id = resolve_effective_club_id(request, request.user) or CLUB_ID_DEFAULT
} huiyuan_queryset = list(Huiyuan.query.all().only(
'huiyuan_id', 'jieshao', 'jtjieshao', 'jiage',
).order_by('jiage'))
yonghuid = request.user.UserUID
_huiyuan_ids = [h.huiyuan_id for h in huiyuan_queryset]
# 批量预取 1本俱乐部所有 ClubHuiyuanPrice启用按 huiyuan_id 索引
_club_price_map = {
cp.huiyuan_id: cp
for cp in ClubHuiyuanPrice.query.filter(
club_id=club_id,
huiyuan_id__in=_huiyuan_ids,
is_enabled=True,
)
} if _huiyuan_ids else {}
# 批量预取 2当前用户已购 Huiyuangoumai按 huiyuan_id 索引
_goumai_map = {
gm.huiyuan_id: gm
for gm in Huiyuangoumai.query.filter(
yonghu_id=yonghuid,
huiyuan_id__in=_huiyuan_ids,
)
} if _huiyuan_ids else {}
# 批量预取 3当前用户在本俱乐部已支付正式会员的 huiyuan_id 集合
_formal_paid_set = set(
Czjilu.query.filter(
yonghuid=yonghuid,
huiyuan_id__in=_huiyuan_ids,
leixing=1,
zhuangtai=3,
club_id=club_id,
is_trial=False,
).values_list('huiyuan_id', flat=True)
) if _huiyuan_ids else set()
def _local_sellable(cp_row, is_trial):
"""单行 ClubHuiyuanPrice 计算 sellable/price/days避免重复查询"""
if not cp_row:
return False, _Decimal('0'), 0
if is_trial:
if not cp_row.trial_enabled:
return False, _Decimal('0'), 0
price = _Decimal(str(cp_row.trial_price or 0))
days = int(cp_row.trial_days or 0)
if price <= 0 or days <= 0:
return False, _Decimal('0'), 0
return True, price, days
price = _Decimal(str(cp_row.jiage or 0))
days = int(cp_row.formal_days or 30)
if price <= 0:
return False, _Decimal('0'), 0
return True, price, days
def _local_can_trial(huiyuan_id):
"""基于已预取的数据判断体验会员是否可购。"""
cp_row = _club_price_map.get(huiyuan_id)
trial_sellable, _, _ = _local_sellable(cp_row, is_trial=True)
if not trial_sellable:
return False
goumai = _goumai_map.get(huiyuan_id)
if goumai and goumai.has_used_trial:
return False
if huiyuan_id in _formal_paid_set:
return False
return True
huiyuan_list = [] huiyuan_list = []
yonghuid = request.user.UserUID
for huiyuan in huiyuan_queryset: for huiyuan in huiyuan_queryset:
sellable, club_price, formal_days = club_huiyuan_sellable( cp_row = _club_price_map.get(huiyuan.huiyuan_id)
club_id, huiyuan.huiyuan_id, is_trial=False, sellable, club_price, formal_days = _local_sellable(cp_row, is_trial=False)
)
if not sellable or not club_price: if not sellable or not club_price:
continue continue
trial_ok, _ = can_purchase_trial(yonghuid, huiyuan.huiyuan_id, club_id) trial_ok = _local_can_trial(huiyuan.huiyuan_id)
trial_sellable, trial_price, trial_days = club_huiyuan_sellable( trial_sellable, trial_price, trial_days = _local_sellable(cp_row, is_trial=True)
club_id, huiyuan.huiyuan_id, is_trial=True, # 远程新增card_image / card_bg / is_bundle 字段
) card_image = (cp_row.card_image or '').strip() if cp_row else ''
club_row = club_rows.get(huiyuan.huiyuan_id) card_bg = (cp_row.card_bg or '').strip() if cp_row else ''
card_image = (club_row.card_image or '').strip() if club_row else ''
card_bg = (club_row.card_bg or '').strip() if club_row else ''
is_bundle = bool(getattr(huiyuan, 'is_bundle', False)) is_bundle = bool(getattr(huiyuan, 'is_bundle', False))
# 总会员is_bundle禁用体验会员
if is_bundle:
trial_sellable = False
trial_price = _Decimal('0')
trial_days = 0
trial_ok = False
huiyuan_list.append({ huiyuan_list.append({
'id': huiyuan.huiyuan_id, 'id': huiyuan.huiyuan_id,
'mingzi': huiyuan.jieshao, 'mingzi': huiyuan.jieshao,
@@ -304,10 +367,10 @@ class DashouHuiyuanList(APIView):
'formal_days': formal_days, 'formal_days': formal_days,
'jieshao': huiyuan.jtjieshao, 'jieshao': huiyuan.jtjieshao,
'is_bundle': is_bundle, 'is_bundle': is_bundle,
'trial_enabled': False if is_bundle else bool(trial_sellable), 'trial_enabled': bool(trial_sellable),
'trial_price': '0.00' if is_bundle else (format_yuan(trial_price) if trial_sellable else '0.00'), 'trial_price': format_yuan(trial_price) if trial_sellable else '0.00',
'trial_days': 0 if is_bundle else (trial_days if trial_sellable else 0), 'trial_days': trial_days if trial_sellable else 0,
'can_buy_trial': False if is_bundle else bool(trial_ok and trial_sellable), 'can_buy_trial': bool(trial_ok and trial_sellable),
'card_image': card_image, 'card_image': card_image,
'card_bg': card_bg, 'card_bg': card_bg,
}) })
@@ -475,20 +538,21 @@ class AdGetAllDataView(APIView):
# 6. 查询商品类型数据 # 6. 查询商品类型数据
try: try:
# 使用select_related或prefetch_related优化查询如果需要关联查询 # 优化:使用 .values() 直接提取字段,避免模型实例化开销(字典表 <10 条
# 这里直接查询所有字段,然后提取需要的字段 shangpinleixing_rows = ShangpinLeixing.query.all().values(
shangpinleixing_queryset = ShangpinLeixing.query.all() 'id', 'yaoqiuleixing', 'huiyuan_id', 'yongjin', 'jieshao', 'tupian_url'
)
# 转换为前端需要的格式 # 转换为前端需要的格式
shangpinleixing_list = [] shangpinleixing_list = []
for leixing in shangpinleixing_queryset: for row in shangpinleixing_rows:
leixing_data = { leixing_data = {
'id': leixing.id, 'id': row['id'],
'yaoqiuleixing': leixing.yaoqiuleixing if leixing.yaoqiuleixing is not None else None, 'yaoqiuleixing': row['yaoqiuleixing'],
'huiyuan_id': leixing.huiyuan_id if leixing.huiyuan_id else None, 'huiyuan_id': row['huiyuan_id'] or None,
'yongjin': str(leixing.yongjin) if leixing.yongjin is not None else None, 'yongjin': str(row['yongjin']) if row['yongjin'] is not None else None,
'jieshao': leixing.jieshao if leixing.jieshao else '', 'jieshao': row['jieshao'] or '',
'tupian_url': leixing.tupian_url if leixing.tupian_url else '' 'tupian_url': row['tupian_url'] or ''
} }
shangpinleixing_list.append(leixing_data) shangpinleixing_list.append(leixing_data)
@@ -500,16 +564,16 @@ class AdGetAllDataView(APIView):
# 7. 查询商品专区数据 # 7. 查询商品专区数据
try: try:
# 查询所有商品专区 # 优化:使用 .values() 直接提取字段(字典表 <50 条)
zhuanqu_queryset = ShangpinZhuanqu.query.all() zhuanqu_rows = ShangpinZhuanqu.query.all().values('id', 'mingzi', 'leixing_id')
# 转换为前端需要的格式 # 转换为前端需要的格式
zhuanqu_list = [] zhuanqu_list = []
for zhuanqu in zhuanqu_queryset: for row in zhuanqu_rows:
zhuanqu_data = { zhuanqu_data = {
'id': zhuanqu.id, 'id': row['id'],
'mingzi': zhuanqu.mingzi if zhuanqu.mingzi else '', 'mingzi': row['mingzi'] or '',
'leixing_id': zhuanqu.leixing_id 'leixing_id': row['leixing_id']
} }
zhuanqu_list.append(zhuanqu_data) zhuanqu_list.append(zhuanqu_data)
@@ -593,17 +657,20 @@ class HuiyuanTypeListView(APIView):
}, status=status.HTTP_401_UNAUTHORIZED) }, status=status.HTTP_401_UNAUTHORIZED)
# 🔥 2. 获取所有会员类型 # 🔥 2. 获取所有会员类型
huiyuan_list = Huiyuan.query.all().order_by('CreateTime') # 优化:使用 .values() 直接提取字段,避免模型实例化开销
huiyuan_rows = Huiyuan.query.all().order_by('CreateTime').values(
'huiyuan_id', 'jieshao', 'jiage', 'goumai_cishu', 'CreateTime'
)
# 🔥 3. 构建返回数据 # 🔥 3. 构建返回数据
huiyuan_data = [] huiyuan_data = []
for huiyuan in huiyuan_list: for row in huiyuan_rows:
huiyuan_data.append({ huiyuan_data.append({
'id': huiyuan.huiyuan_id, # 会员ID 'id': row['huiyuan_id'], # 会员ID
'jieshao': huiyuan.jieshao, # 会员介绍 'jieshao': row['jieshao'], # 会员介绍
'jiage': str(huiyuan.jiage), # 价格 'jiage': str(row['jiage']), # 价格
'goumai_cishu': huiyuan.goumai_cishu, # 购买次数 'goumai_cishu': row['goumai_cishu'], # 购买次数
'CreateTime': huiyuan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.CreateTime else None 'CreateTime': row['CreateTime'].strftime('%Y-%m-%d %H:%M:%S') if row['CreateTime'] else None
}) })
return Response({ return Response({
@@ -757,34 +824,40 @@ class HuiyuanRecordListView(APIView):
has_more = (page * pagesize) < total_count has_more = (page * pagesize) < total_count
# 🔥 9. 获取会员类型统计信息(所有会员类型的当前状态总数) # 🔥 9. 获取会员类型统计信息(所有会员类型的当前状态总数)
huiyuan_types = Huiyuan.query.all() # 优化:原实现为 N+1 查询(外层遍历 Huiyuan 全表 + 内层每条记录执行一次 COUNT SQL
huiyuan_stats = [] # 改为单条 LEFT JOIN + GROUP BY 聚合,将 N+1 次查询降为 1 次
# 注意Huiyuangoumai.huiyuan_id 是 CharField 而非 ForeignKey无法用 ORM annotate
for huiyuan in huiyuan_types: stats_sql = """
# 使用COUNT查询统计 SELECT
stats_sql = """ h.huiyuan_id,
SELECT COUNT(*) h.jieshao,
FROM huiyuangoumai COUNT(hg.id) AS count
WHERE huiyuan_id = %s FROM huiyuan h
""" LEFT JOIN huiyuangoumai hg
stats_params = [huiyuan.huiyuan_id] ON hg.huiyuan_id = h.huiyuan_id
"""
if jiluzhuangtai == 1: # 未过期 stats_params = []
stats_sql += " AND daoqi_time > %s" if jiluzhuangtai == 1: # 未过期
stats_params.append(now) # 条件放在 ON 子句而非 WHERE确保无购买记录的会员类型仍返回 count=0
else: # 已过期 stats_sql += " AND hg.daoqi_time > %s"
stats_sql += " AND daoqi_time <= %s" stats_params.append(now)
stats_params.append(now) else: # 已过期
stats_sql += " AND hg.daoqi_time <= %s"
with connection.cursor() as cursor: stats_params.append(now)
cursor.execute(stats_sql, stats_params) stats_sql += " GROUP BY h.huiyuan_id, h.jieshao"
count = cursor.fetchone()[0]
with connection.cursor() as cursor:
huiyuan_stats.append({ cursor.execute(stats_sql, stats_params)
'id': huiyuan.huiyuan_id, stats_rows = cursor.fetchall()
'jieshao': huiyuan.jieshao,
'count': count huiyuan_stats = [
}) {
'id': row[0],
'jieshao': row[1],
'count': row[2]
}
for row in stats_rows
]
return Response({ return Response({
'code': 0, 'code': 0,

View File

@@ -35,8 +35,9 @@ def _has_perm(permissions):
return any(p in permissions for p in REWARD_PERMS) return any(p in permissions for p in REWARD_PERMS)
def _serialize_scheme(scheme): def _serialize_scheme(scheme, tiers=None):
tiers = RankRewardTier.query.filter(scheme_id=scheme.id).order_by('rank_from') if tiers is None:
tiers = RankRewardTier.query.filter(scheme_id=scheme.id).order_by('rank_from')
return { return {
'id': scheme.id, 'id': scheme.id,
'club_id': scheme.club_id, 'club_id': scheme.club_id,
@@ -78,11 +79,18 @@ class RankRewardHoutaiListView(APIView):
club_id = resolve_club_id_from_request(request) club_id = resolve_club_id_from_request(request)
schemes = RankRewardScheme.query.filter(club_id=club_id).order_by('shenfen', 'period_type') schemes = RankRewardScheme.query.filter(club_id=club_id).order_by('shenfen', 'period_type')
# 批量预取所有 tiers避免列表推导内 N+1
schemes_list = list(schemes)
_scheme_ids = [s.id for s in schemes_list]
_tiers_map = {}
if _scheme_ids:
for t in RankRewardTier.query.filter(scheme_id__in=_scheme_ids).order_by('scheme_id', 'rank_from'):
_tiers_map.setdefault(t.scheme_id, []).append(t)
return Response({ return Response({
'code': 0, 'code': 0,
'data': { 'data': {
'club_id': club_id, 'club_id': club_id,
'schemes': [_serialize_scheme(s) for s in schemes], 'schemes': [_serialize_scheme(s, _tiers_map.get(s.id, [])) for s in schemes_list],
'shenfen_options': list(SORT_FIELD_REGISTRY.keys()), 'shenfen_options': list(SORT_FIELD_REGISTRY.keys()),
'period_options': [ 'period_options': [
{'key': RankRewardScheme.PERIOD_DAY, 'label': '日榜(昨日)'}, {'key': RankRewardScheme.PERIOD_DAY, 'label': '日榜(昨日)'},
@@ -148,14 +156,16 @@ class RankRewardHoutaiSaveView(APIView):
) )
RankRewardTier.query.filter(scheme_id=scheme.id).delete() RankRewardTier.query.filter(scheme_id=scheme.id).delete()
for t in tiers: RankRewardTier.objects.bulk_create([
RankRewardTier.query.create( RankRewardTier(
scheme_id=scheme.id, scheme_id=scheme.id,
rank_from=int(t['rank_from']), rank_from=int(t['rank_from']),
rank_to=int(t['rank_to']), rank_to=int(t['rank_to']),
reward_amount=t['reward_amount'], reward_amount=t['reward_amount'],
label=(t.get('label') or '').strip(), label=(t.get('label') or '').strip(),
) )
for t in tiers
])
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_scheme(scheme)}) return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_scheme(scheme)})

View File

@@ -574,11 +574,36 @@ def maybe_settle_club_rewards(club_id: str, shenfen: str, riqi: str) -> Optional
return None return None
min_metric = scheme.min_metric or Decimal('0') min_metric = scheme.min_metric or Decimal('0')
club_map = load_user_club_map([r['yonghuid'] for r in rows]) _all_uids = [r['yonghuid'] for r in rows]
club_map = load_user_club_map(_all_uids)
expire_at = timezone.now() + timedelta(days=scheme.claim_days or 30) expire_at = timezone.now() + timedelta(days=scheme.claim_days or 30)
balance_field = BALANCE_FIELD_MAP[shenfen] balance_field = BALANCE_FIELD_MAP[shenfen]
claims_to_create = [] claims_to_create = []
# 批量预取活跃用户集合,避免循环内 exists 查询
if shenfen == 'dashou':
_active_uids = set(
UserDashou.objects.filter(user__UserUID__in=_all_uids, zhanghaozhuangtai=1)
.values_list('user__UserUID', flat=True)
)
elif shenfen == 'guanshi':
_active_uids = set(
UserGuanshi.objects.filter(user__UserUID__in=_all_uids, zhuangtai=1)
.values_list('user__UserUID', flat=True)
)
elif shenfen == 'zuzhang':
_active_uids = set(
UserZuzhang.objects.filter(user__UserUID__in=_all_uids, zhuangtai=1)
.values_list('user__UserUID', flat=True)
)
elif shenfen == 'shangjia':
_active_uids = set(
UserShangjia.objects.filter(user__UserUID__in=_all_uids, zhuangtai=1)
.values_list('user__UserUID', flat=True)
)
else:
_active_uids = set()
for idx, row in enumerate(rows): for idx, row in enumerate(rows):
mingci = idx + 1 mingci = idx + 1
metric = Decimal(str(row['metric'] or 0)) metric = Decimal(str(row['metric'] or 0))
@@ -590,7 +615,7 @@ def maybe_settle_club_rewards(club_id: str, shenfen: str, riqi: str) -> Optional
uid = row['yonghuid'] uid = row['yonghuid']
if club_map.get(uid) != club_id: if club_map.get(uid) != club_id:
continue continue
if not user_has_active_shenfen(uid, shenfen): if uid not in _active_uids:
continue continue
idem = f'{club_id}|{scheme.id}|{period_key}|{uid}' idem = f'{club_id}|{scheme.id}|{period_key}|{uid}'
claims_to_create.append(RankRewardClaim( claims_to_create.append(RankRewardClaim(
@@ -618,14 +643,14 @@ def maybe_settle_club_rewards(club_id: str, shenfen: str, riqi: str) -> Optional
sort_field=scheme.sort_field, sort_field=scheme.sort_field,
claim_count=0, claim_count=0,
) )
created = 0 # 批量创建 claims用 ignore_conflicts 处理幂等冲突
for claim in claims_to_create: if claims_to_create:
claim.settlement_id = settlement.id for claim in claims_to_create:
try: claim.settlement_id = settlement.id
claim.save() RankRewardClaim.objects.bulk_create(claims_to_create, ignore_conflicts=True)
created += 1 created = RankRewardClaim.objects.filter(settlement_id=settlement.id).count()
except IntegrityError: else:
pass created = 0
settlement.claim_count = created settlement.claim_count = created
settlement.save(update_fields=['claim_count']) settlement.save(update_fields=['claim_count'])
return settlement return settlement

View File

@@ -3,7 +3,7 @@ import json
import logging import logging
from decimal import Decimal from decimal import Decimal
from django.db import models, transaction from django.db import models, transaction
from django.db.models import F from django.db.models import F, Prefetch
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
@@ -118,15 +118,26 @@ class KaoheJinpaiTagsView(APIView):
.values_list('chenghao__mingcheng', flat=True) .values_list('chenghao__mingcheng', flat=True)
) )
# 板块列表 # 板块列表prefetch 一次性加载标签和费用,避免三层嵌套 N+1
plate_list = [] plate_list = []
for bk in Bankuai.objects.all(): all_bankuai = Bankuai.objects.prefetch_related(
tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou') Prefetch(
'chenghao_set',
queryset=Chenghao.objects.filter(leixing='dashou').prefetch_related(
Prefetch(
'kaohecishufeiyong_set',
queryset=KaoheCishuFeiyong.objects.order_by('cishu'),
)
),
to_attr='dashou_tags'
)
).all()
for bk in all_bankuai:
tag_list = [] tag_list = []
for tag in tags: for tag in getattr(bk, 'dashou_tags', []):
# 费用列表 # 费用列表(已预加载,保持 cishu 升序)
fee_objs = KaoheCishuFeiyong.objects.filter(chenghao=tag).order_by('cishu') fee_list = [{'cishu': f.cishu, 'feiyong': float(f.feiyong)}
fee_list = [{'cishu': f.cishu, 'feiyong': float(f.feiyong)} for f in fee_objs] for f in tag.kaohecishufeiyong_set.all()]
# 若没有记录,补充默认费用 # 若没有记录,补充默认费用
if not fee_list: if not fee_list:
@@ -151,12 +162,13 @@ class KaoheJinpaiTagsView(APIView):
# 新增:返回所有在职考核官信息(用于指定考核官) # 新增:返回所有在职考核官信息(用于指定考核官)
kaoheguan_list = [] kaoheguan_list = []
for kg in UserShenheguan.objects.filter(zhuangtai=1).select_related('user'): # select_related 预加载 OneToOne 反向关系 DashouProfile避免循环内 N+1
for kg in UserShenheguan.objects.filter(zhuangtai=1).select_related('user', 'user__DashouProfile'):
uid = kg.user.UserUID uid = kg.user.UserUID
avatar = kg.user.Avatar or '' avatar = kg.user.Avatar or ''
# 获取考核官的打手昵称(从打手扩展表) # 获取考核官的打手昵称(从打手扩展表,已预加载
dashou_nick = '' dashou_nick = ''
dashou_prof = UserDashou.objects.filter(user=kg.user).first() dashou_prof = getattr(kg.user, 'DashouProfile', None)
if dashou_prof: if dashou_prof:
dashou_nick = dashou_prof.nicheng or '' dashou_nick = dashou_prof.nicheng or ''
kaoheguan_list.append({ kaoheguan_list.append({

View File

@@ -48,20 +48,42 @@ class KaoheJiluView(APIView):
total = query.count() total = query.count()
records = query.order_by('-CreateTime')[(page-1)*page_size : page*page_size] records = query.order_by('-CreateTime')[(page-1)*page_size : page*page_size]
# 批量预取关联数据,避免循环内 N+1
records = records.select_related('chenghao', 'bankuai')
records_list = list(records)
jilu_ids = [jl.jilu_id for jl in records_list]
# 批量汇总缴纳总额(按 jilu_id 分组聚合)
_fee_map = {}
if jilu_ids:
_fee_agg = KaoheJiluFeiyong.objects.filter(
jilu_id__in=jilu_ids
).values('jilu_id').annotate(total=models.Sum('jine'))
for item in _fee_agg:
_fee_map[item['jilu_id']] = float(item['total'] or 0.00)
# 批量获取上次拒绝原因(每个 jilu_id 取最近一条)
_fail_map = {}
if jilu_ids:
_reject_qs = KaoheJujueJilu.objects.filter(
jilu_id__in=jilu_ids
).order_by('jilu_id', '-CreateTime')
for rej in _reject_qs:
if rej.jilu_id not in _fail_map: # 只取首条(最近)
_fail_map[rej.jilu_id] = rej.yuanyin or ''
# 批量获取考核官头像
_avatar_map = {}
_kg_ids = {jl.shenheguan_id for jl in records_list if jl.shenheguan_id}
if _kg_ids:
for u in User.objects.filter(UserUID__in=_kg_ids):
_avatar_map[u.UserUID] = u.Avatar or ''
data = [] data = []
for jl in records: for jl in records_list:
# 汇总缴纳总额 total_fee = _fee_map.get(jl.jilu_id, 0.00)
total_fee = KaoheJiluFeiyong.objects.filter(jilu=jl).aggregate( fail_reason = _fail_map.get(jl.jilu_id, '')
total=models.Sum('jine') kg_avatar = _avatar_map.get(jl.shenheguan_id, '')
)['total'] or 0.00
# 上次拒绝原因(最近一条)
last_fail = KaoheJujueJilu.objects.filter(jilu=jl).order_by('-CreateTime').first()
fail_reason = last_fail.yuanyin if last_fail else ''
# 考核官头像
kg_user = User.objects.filter(UserUID=jl.shenheguan_id).first()
kg_avatar = kg_user.Avatar if kg_user else ''
data.append({ data.append({
'jilu_id': jl.jilu_id, 'jilu_id': jl.jilu_id,

View File

@@ -3,7 +3,7 @@ import json
import logging import logging
from decimal import Decimal from decimal import Decimal
from django.db import models, transaction from django.db import models, transaction
from django.db.models import F from django.db.models import F, Count, Q
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
@@ -33,12 +33,17 @@ class ShenheDatingModuleView(APIView):
.values_list('bankuai_id', flat=True)) .values_list('bankuai_id', flat=True))
bankuai_list = [] bankuai_list = []
for bk in Bankuai.objects.all(): all_bankuai = Bankuai.objects.annotate(
count = ShenheJilu.objects.filter(bankuai=bk, zhuangtai=3, shenheguan_id__isnull=True).count() pending_count=Count(
'shenhejilu',
filter=Q(shenhejilu__zhuangtai=3, shenhejilu__shenheguan_id__isnull=True)
)
)
for bk in all_bankuai:
bankuai_list.append({ bankuai_list.append({
'bankuai_id': bk.bankuai_id, 'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng, 'mingcheng': bk.mingcheng,
'count': count, 'count': bk.pending_count,
'is_related': bk.bankuai_id in related_ids 'is_related': bk.bankuai_id in related_ids
}) })
@@ -67,9 +72,13 @@ class ShenheDatingDataView(APIView):
jilu_list = query.select_related('bankuai', 'chenghao').order_by('-CreateTime')[ jilu_list = query.select_related('bankuai', 'chenghao').order_by('-CreateTime')[
offset:offset + page_size] offset:offset + page_size]
# 批量加载申请人用户,避免循环内 N+1
shenqingren_ids = [jl.shenqingren_id for jl in jilu_list if jl.shenqingren_id]
user_map = {u.UserUID: u for u in User.objects.filter(UserUID__in=shenqingren_ids)}
records = [] records = []
for jl in jilu_list: for jl in jilu_list:
user = User.objects.filter(UserUID=jl.shenqingren_id).first() user = user_map.get(jl.shenqingren_id)
avatar = user.Avatar if user else '' avatar = user.Avatar if user else ''
texiao_json = {} texiao_json = {}
if jl.chenghao and jl.chenghao.texiao_miaoshu: if jl.chenghao and jl.chenghao.texiao_miaoshu:
@@ -202,9 +211,27 @@ class ShenheXiangqingView(APIView):
offset = (page - 1) * page_size offset = (page - 1) * page_size
jilu_list = base_query[offset:offset + page_size] jilu_list = base_query[offset:offset + page_size]
# 批量加载申请人用户,避免循环内 N+1
shenqingren_ids = [jl.shenqingren_id for jl in jilu_list if jl.shenqingren_id]
user_map = {u.UserUID: u for u in User.objects.filter(UserUID__in=shenqingren_ids)}
# 批量预取费用明细和拒绝记录,避免循环内 N+1
_jilu_ids = [jl.jilu_id for jl in jilu_list]
_fee_detail_map = {} # (jilu_id, cishu) -> jine
_fail_map = {} # (jilu_id, cishu) -> yuanyin
if _jilu_ids:
for _fee in KaoheJiluFeiyong.objects.filter(jilu_id__in=_jilu_ids):
_fee_detail_map[(_fee.jilu_id, _fee.cishu)] = float(_fee.jine)
for _rej in KaoheJujueJilu.objects.filter(
jilu_id__in=_jilu_ids
).order_by('jilu_id', 'cishu', '-CreateTime'):
_key = (_rej.jilu_id, _rej.cishu)
if _key not in _fail_map: # 只取首条(最近)
_fail_map[_key] = _rej.yuanyin or ''
records = [] records = []
for jl in jilu_list: for jl in jilu_list:
user = User.objects.filter(UserUID=jl.shenqingren_id).first() user = user_map.get(jl.shenqingren_id)
avatar = user.Avatar if user else '' avatar = user.Avatar if user else ''
texiao_json = {} texiao_json = {}
if jl.chenghao and jl.chenghao.texiao_miaoshu: if jl.chenghao and jl.chenghao.texiao_miaoshu:
@@ -216,25 +243,10 @@ class ShenheXiangqingView(APIView):
# 是否开启金牌 # 是否开启金牌
is_jinpai = jl.chenghao.kaioi_jinpai if jl.chenghao else False is_jinpai = jl.chenghao.kaioi_jinpai if jl.chenghao else False
# 本次待收考核费(基于记录中的当前次数,从费用明细表查询) # 本次待收考核费(从预取 dict 查询)
daishou_fei = 0.00 daishou_fei = _fee_detail_map.get((jl.jilu_id, jl.cishu), 0.00)
fee_detail = KaoheJiluFeiyong.objects.filter( # 上次失败原因(从预取 dict 查询)
jilu=jl, last_fail_reason = _fail_map.get((jl.jilu_id, jl.cishu - 1), '')
cishu=jl.cishu
).first()
if fee_detail:
daishou_fei = float(fee_detail.jine)
else:
# 兜底:若明细表无记录,尝试用通用费用表计算
daishou_fei = 0.00
# 上次失败原因
last_fail_reason = ''
last_fail = KaoheJujueJilu.objects.filter(
jilu=jl,
cishu=jl.cishu - 1
).order_by('-CreateTime').first()
if last_fail:
last_fail_reason = last_fail.yuanyin
records.append({ records.append({
'jilu_id': jl.jilu_id, 'jilu_id': jl.jilu_id,

View File

@@ -15,7 +15,6 @@ import threading
import xmltodict import xmltodict
from decimal import Decimal from decimal import Decimal
from collections import defaultdict from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
from django.conf import settings from django.conf import settings
from django.db import models, transaction, IntegrityError from django.db import models, transaction, IntegrityError
@@ -260,38 +259,66 @@ def _fetch_images(order, oss_domain):
# ================================================================ # ================================================================
# 辅助函数:并发获取多个订单的打手图片 # 辅助函数:批量获取多个订单的打手图片(单次 DB 查询)
# ================================================================ # ================================================================
def _batch_images(orders, oss_domain): def _batch_images(orders, oss_domain):
""" """
并发调用 _fetch_images提升批量获取图片的速度 批量获取订单的打手提交图片,使用单次 DB 查询替代循环/线程池 N 次查询
参数: 参数:
orders: Dingdan 订单对象列表 orders: Dingdan 订单对象列表
oss_domain: OSS 域名 oss_domain: OSS 域名
返回: 返回:
字典键为订单ID (dingdan_id),值为 (images_list, is_punished, punishment_info) 字典键为订单ID (OrderID),值为 (images_list, is_punished, punishment_info)
""" """
result = {} # 存储结果 result = {}
# 使用线程池并发执行max_workers 可根据服务器性能调整 # 先为所有订单填充默认空值,保证调用方可拿到键
with ThreadPoolExecutor(max_workers=10) as executor: for order in orders:
# 建立 future 到订单ID的映射 result[order.OrderID] = ([], False, {})
future_to_oid = {
executor.submit(_fetch_images, order, oss_domain): order.OrderID # 收集所有 (OrderID, PlayerID) 组合用于批量查询
for order in orders _order_player_pairs = [
} (order.OrderID, order.PlayerID)
# 等待所有任务完成 for order in orders
for future in as_completed(future_to_oid): if order.PlayerID
oid = future_to_oid[future] # 对应的订单ID ]
try: if not _order_player_pairs:
# 获取任务返回值 return result
imgs, pun, pinfo = future.result()
result[oid] = (imgs, pun, pinfo) # 单次批量查询所有相关图片记录(避免循环 N 次或线程池掩盖 N 次查询)
except Exception as e: # PlayerDeliveryImage 表的主键是 OrderID + PlayerID使用元组列表过滤
# 某个订单获取失败,记录日志并赋予空值,保证整体流程不中断 _order_ids = [pair[0] for pair in _order_player_pairs]
logger.error(f"获取订单 {oid} 图片失败: {e}") _pair_set = set(_order_player_pairs)
result[oid] = ([], False, {})
try:
rows = list(
PlayerDeliveryImage.query.filter(
OrderID__in=_order_ids
).values_list('OrderID', 'PlayerID', 'ImageURL')
)
except Exception as e:
logger.error(f"_batch_images 批量查询图片失败: {e}")
return result
# 按 (OrderID, PlayerID) 分组
_img_map = {}
for oid, pid, img_path in rows:
if not img_path:
continue
# 仅保留属于本批 orders 且 PlayerID 匹配的记录(防止跨打手串号)
if (oid, pid) not in _pair_set:
continue
if img_path.startswith('http'):
url = img_path
else:
url = oss_domain + img_path.lstrip('/')
_img_map.setdefault(oid, []).append(url)
# 把分组结果写回 result
for oid, imgs in _img_map.items():
if oid in result:
result[oid] = (imgs, False, {})
return result return result

View File

@@ -771,10 +771,12 @@ class ShopProductModifyView1(APIView):
else: else:
# 自动从公共类型获取会员ID或转为佣金 # 自动从公共类型获取会员ID或转为佣金
try: try:
shop_type = ShangpinLeixingDianpu.query.get( shop_type = ShangpinLeixingDianpu.query.select_related('gonggong_leixing').get(
id=product.dianpu_leixing_id, dianpu_id=dianpu.id id=product.dianpu_leixing_id, dianpu_id=dianpu.id
) )
public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id) public_type = shop_type.gonggong_leixing
if not public_type:
raise ShangpinLeixing.DoesNotExist
auto_huiyuan_id = public_type.huiyuan_id auto_huiyuan_id = public_type.huiyuan_id
if auto_huiyuan_id: if auto_huiyuan_id:
product.huiyuan_id = auto_huiyuan_id product.huiyuan_id = auto_huiyuan_id
@@ -893,13 +895,12 @@ class ShopProductModifyView1(APIView):
return Response({'code': 400, 'msg': '必填项缺失'}) return Response({'code': 400, 'msg': '必填项缺失'})
try: try:
shop_type = ShangpinLeixingDianpu.query.get(id=dianpu_leixing_id, dianpu_id=dianpu.id) shop_type = ShangpinLeixingDianpu.query.select_related('gonggong_leixing').get(id=dianpu_leixing_id, dianpu_id=dianpu.id)
except ShangpinLeixingDianpu.DoesNotExist: except ShangpinLeixingDianpu.DoesNotExist:
return Response({'code': 400, 'msg': '无效的店铺商品类型'}) return Response({'code': 400, 'msg': '无效的店铺商品类型'})
try: public_type = shop_type.gonggong_leixing
public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id, shenhezhuangtai=1) if not public_type or public_type.shenhezhuangtai != 1:
except ShangpinLeixing.DoesNotExist:
return Response({'code': 400, 'msg': '关联的公共商品类型无效'}) return Response({'code': 400, 'msg': '关联的公共商品类型无效'})
# 商品图片上传 # 商品图片上传

View File

@@ -125,7 +125,7 @@ class ShopRefundView(APIView):
# 更新打手统计(退款量+1状态置为空闲 # 更新打手统计(退款量+1状态置为空闲
if dashou_id: if dashou_id:
try: try:
dashou_user = User.query.get(UserUID=dashou_id) dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
dashou_profile = dashou_user.DashouProfile dashou_profile = dashou_user.DashouProfile
dashou_profile.tuikuanliang += 1 dashou_profile.tuikuanliang += 1
dashou_profile.zhuangtai = 1 dashou_profile.zhuangtai = 1
@@ -368,7 +368,7 @@ class ShopForceCompleteView(APIView):
# ---------- 更新打手余额和统计 ---------- # ---------- 更新打手余额和统计 ----------
try: try:
dashou_user = User.query.get(UserUID=dashou_id) dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
dashou_profile = dashou_user.DashouProfile dashou_profile = dashou_user.DashouProfile
dashou_profile.chengjiaozongliang += 1 dashou_profile.chengjiaozongliang += 1
dashou_profile.yue += dashou_fencheng dashou_profile.yue += dashou_fencheng

View File

@@ -88,29 +88,41 @@ from .chufa import (
ShangjiaChufaJiluHuoquView, ShangjiaChufaJiluHuoquView,
) )
from .kefu import ( from .kefu_base import (
KefuGetOrderTypesView,
KefuLoginView,
KefuStatsView,
)
from .kefu_orders import (
KefuCancelDesignationView, KefuCancelDesignationView,
KefuChangeDashouView, KefuChangeDashouView,
KefuForceCompleteView, KefuForceCompleteView,
KefuGetDashouDetailView,
KefuGetDashouListView,
KefuGetOrderDetailView, KefuGetOrderDetailView,
KefuGetOrderListView, KefuGetOrderListView,
KefuGetOrderTypesView,
KefuGetShangjiaOrderListView, KefuGetShangjiaOrderListView,
KefuLoginView,
KefuMerchantRefundView, KefuMerchantRefundView,
KefuPlatformRefundView, KefuPlatformRefundView,
KefuRecoverOrderView,
KefuRejectRefundView,
KefuRejectSettlementView,
KefuTransferHallView,
)
from .kefu_dashou import (
KefuGetDashouDetailView,
KefuGetDashouListView,
KefuUpdateDashouView,
)
from .kefu_punishment import (
KefuPunishView, KefuPunishView,
KefuPunishmentActionView, KefuPunishmentActionView,
KefuPunishmentDetailView, KefuPunishmentDetailView,
KefuPunishmentListView, KefuPunishmentListView,
KefuRecoverOrderView, )
KefuRejectRefundView,
KefuRejectSettlementView, from .kefu_withdraw import (
KefuStatsView,
KefuTransferHallView,
KefuUpdateDashouView,
KefuWithdrawActionView, KefuWithdrawActionView,
KefuWithdrawDetailView, KefuWithdrawDetailView,
KefuWithdrawListView, KefuWithdrawListView,

View File

@@ -532,12 +532,18 @@ class AdGetOrderList(APIView):
# 注意:这里统计所有订单,不考虑筛选条件 # 注意:这里统计所有订单,不考虑筛选条件
try: try:
# 平台订单统计 (Platform=1) # 平台订单统计 (Platform=1)
platform_total = Order.query.filter(Platform=1).count() _agg = Order.query.aggregate(
platform_completed = Order.query.filter(Platform=1, Status=3).count() platform_total=Count('id', filter=Q(Platform=1)),
platform_completed=Count('id', filter=Q(Platform=1, Status=3)),
merchant_total=Count('id', filter=Q(Platform=2)),
merchant_completed=Count('id', filter=Q(Platform=2, Status=3)),
)
platform_total = _agg['platform_total']
platform_completed = _agg['platform_completed']
# 商家订单统计 (Platform=2) # 商家订单统计 (Platform=2)
merchant_total = Order.query.filter(Platform=2).count() merchant_total = _agg['merchant_total']
merchant_completed = Order.query.filter(Platform=2, Status=3).count() merchant_completed = _agg['merchant_completed']
platform_stats = { platform_stats = {
'total': platform_total, 'total': platform_total,
@@ -1170,9 +1176,14 @@ class CfGuanLi(APIView):
) )
# 7. 获取统计信息(总是需要统计) # 7. 获取统计信息(总是需要统计)
zongshu = PenaltyRecord.query.count() _agg = PenaltyRecord.query.aggregate(
daichuli = PenaltyRecord.query.filter(ApplyStatus__in=[0, 3]).count() zongshu=Count('id'),
yichuli = PenaltyRecord.query.filter(ApplyStatus__in=[1, 2]).count() daichuli=Count('id', filter=Q(ApplyStatus__in=[0, 3])),
yichuli=Count('id', filter=Q(ApplyStatus__in=[1, 2])),
)
zongshu = _agg['zongshu']
daichuli = _agg['daichuli']
yichuli = _agg['yichuli']
# 8. 如果只请求统计信息,直接返回 # 8. 如果只请求统计信息,直接返回
if qingqiu_tongji: if qingqiu_tongji:
@@ -1687,6 +1698,13 @@ class AdckyhxqView(APIView):
yonghu_id=uid yonghu_id=uid
).order_by('-CreateTime') ).order_by('-CreateTime')
# 批量预取会员详情
_huiyuan_ids = [r.huiyuan_id for r in member_records]
_huiyuan_map = {
h.huiyuan_id: h
for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao')
}
member_list = [] member_list = []
for record in member_records: for record in member_records:
# 调用方法检查是否过期,并更新状态 # 调用方法检查是否过期,并更新状态
@@ -1694,9 +1712,7 @@ class AdckyhxqView(APIView):
if not is_expired: # 只返回未过期的 if not is_expired: # 只返回未过期的
# 查询会员详情 # 查询会员详情
huiyuan = Huiyuan.query.filter( huiyuan = _huiyuan_map.get(record.huiyuan_id)
huiyuan_id=record.huiyuan_id
).first()
if huiyuan: if huiyuan:
member_list.append({ member_list.append({
@@ -2282,15 +2298,13 @@ class AddtxshView(APIView):
has_more = (offset + current_count) < total_count has_more = (offset + current_count) < total_count
# 获取状态统计(统计与搜索无关,依然按筛选条件统计) # 获取状态统计(统计与搜索无关,依然按筛选条件统计)
_agg = Tixianjilu.query.filter(leixing=leixing).aggregate(
awaiting=Count('id', filter=Q(zhuangtai=1)),
processed=Count('id', filter=Q(zhuangtai__in=[2, 3])),
)
status_counts = { status_counts = {
'awaiting': Tixianjilu.query.filter( 'awaiting': _agg['awaiting'],
zhuangtai=1, 'processed': _agg['processed'],
leixing=leixing
).count(),
'processed': Tixianjilu.query.filter(
zhuangtai__in=[2, 3],
leixing=leixing
).count()
} }
# 构建响应数据 # 构建响应数据

View File

@@ -526,16 +526,16 @@ class DashouShensuView(APIView):
chufa_record.save() chufa_record.save()
# 6. 保存申诉图片(如果有) # 6. 保存申诉图片(如果有)
saved_tupian_urls = [] saved_tupian_urls = [u for u in shensu_tupian_urls if u] if shensu_tupian_urls else []
if shensu_tupian_urls: if saved_tupian_urls:
for tupian_url in shensu_tupian_urls: PenaltyEvidenceImage.objects.bulk_create([
if tupian_url: # 确保URL不为空 PenaltyEvidenceImage(
chufa_tupian = PenaltyEvidenceImage.query.create( OrderID=chufa_record.OrderID,
OrderID=chufa_record.OrderID, UserID=yonghuid,
UserID=yonghuid, ImageURL=tupian_url,
ImageURL=tupian_url )
) for tupian_url in saved_tupian_urls
saved_tupian_urls.append(tupian_url) ])
# 7. 构建返回数据 # 7. 构建返回数据
response_data = { response_data = {

File diff suppressed because it is too large Load Diff

383
users/views/kefu_base.py Normal file
View File

@@ -0,0 +1,383 @@
"""users.views.kefu_base - auto-generated by split script."""
"""users.views.kefu - auto-generated by split script."""
import os
import sys
import json
import re
import time
import uuid
import random
import string
import requests
import hashlib
import traceback
import decimal
import jwt
import ipaddress
import xmltodict
from datetime import datetime, timedelta
from decimal import Decimal
import defusedxml.ElementTree as ET
from django.conf import settings
from django.db import models, transaction, IntegrityError, DatabaseError, connection
from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch
from django.core.cache import cache
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.http import HttpResponse
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.hashers import make_password
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.authentication import JWTAuthentication
from gvsdsdk.fluent import db, func, FQ
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from utils.money import yuan_to_fen
from utils.fadan_utils import check_fadan_qiangdan_eligible
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from users.fadan_fenhong_utils import process_fadan_fenhong
from orders.utils import (
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from backend.utils import (
update_dashou_daily_by_action, update_guanshi_daily_by_action,
update_shangjia_daily, update_zuzhang_daily_by_action,
verify_kefu_permission
)
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
from ..models import (
UserBoss, UserDashou, UserShangjia, UserGuanshi,
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
RankingRecord
)
from users.business_models import User
from ..tixian_shenhe_services import (
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
mark_transfer_success, release_collect_quota_for_record,
close_audit_wechat_failed, query_wechat_transfer_bill,
check_shijidaozhang_within_limit,
check_dashou_trial_blocks_commission_withdraw,
WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING,
)
from ..tixian_shenhe_views import process_audit_collect
from orders.models import (
Order, PlatformOrderExt, MerchantOrderExt, CommissionRate,
PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage,
Penalty, PenaltyAppealImage
)
from products.models import (
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
)
from config.models import Qunpeizhi
from backend.models import MerchantDailyStats
from rank.models import (
KaohePayTemp, Chenghao, ShenheJilu,
KaoheCishuFeiyong, YonghuChenghao
)
from users.models import UserShenheguan
from products.utils import update_shangpin_daily_stat
from shop.utils import update_dianpu_daily_stat
from rank.utils import create_shenhe_jilu_from_temp
import logging
logger = logging.getLogger(__name__)
class KefuLoginView(APIView):
"""
客服/管理员登录接口(支持手机号重复)
请求POST /yonghu/kefujinru
请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"}
返回:
code=0 成功data包含token,nicheng,yonghuid
code=1 失败msg区分用户不存在/密码错误/二级密码错误)
code=4 账号被封禁
管理员IsSuperuser 或 UserType='admin')无需 erjimima 和 KefuProfile。
"""
throttle_classes = [AnonRateThrottle] # 限制匿名请求频率
permission_classes = [AllowAny] # 任何人都可访问
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
password = request.data.get('password', '')
erjimima = request.data.get('erjimima', '')
if not phone or not password:
return Response({
'code': 1,
'msg': '请填写手机号和密码',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 获取客户端真实IP
client_ip = self.get_client_ip(request)
# 3. 查询该手机号下所有账号(客服 + 管理员)
# 先查客服(有 KefuProfile再查管理员IsSuperuser 或 UserType='admin'
user_mains = list(
User.query.filter(
Phone=phone,
KefuProfile__isnull=False,
).select_related('KefuProfile')
)
# 如果没有客服账号,尝试查找管理员账号
if not user_mains:
admin_candidates = User.query.filter(Phone=phone)
for admin_user in admin_candidates:
if admin_user.IsSuperuser or admin_user.UserType == 'admin':
user_mains = [admin_user]
break
if not user_mains:
logger.warning(f"登录失败,用户不存在: {phone}, IP: {client_ip}")
return Response({
'code': 1,
'msg': '用户不存在',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 遍历所有匹配的账号,验证密码和二级密码
target_user = None
target_kefu = None
password_matched = False
for user in user_mains:
# 验证主密码bcrypt 哈希验证)
if not user.CheckPassword(password):
continue
password_matched = True
# 管理员:跳过二级密码和客服扩展表检查
is_admin = user.IsSuperuser or user.UserType == 'admin'
if is_admin:
target_user = user
target_kefu = None
break
# 客服:验证二级密码
kefu = user.KefuProfile
stored_erji = kefu.erjimima or ''
if stored_erji.startswith(('$2b$', '$2a$')) and len(stored_erji) == 60:
import bcrypt as _bcrypt
if not _bcrypt.checkpw(erjimima.encode('utf-8'), stored_erji.encode('utf-8')):
continue
elif stored_erji != erjimima:
continue
# 如果账号被禁用,记录但继续查找
if kefu.zhuangtai != 1:
if target_user is None:
target_user = user
target_kefu = kefu
continue
# 找到完全匹配且状态正常的账号,立即使用
target_user = user
target_kefu = kefu
break
else:
# 循环结束未找到正常账号
if target_user and target_kefu:
# 只有被禁用的账号匹配,返回封禁提示
logger.warning(f"登录失败,客服账号已禁用: {phone}, IP: {client_ip}")
return Response({
'code': 4,
'msg': '账号已被封禁',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
elif password_matched:
# 密码正确但二级密码错误
logger.warning(f"登录失败,二级密码错误: {phone}, IP: {client_ip}")
return Response({
'code': 1,
'msg': '二级密码错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
else:
# 密码错误
logger.warning(f"登录失败,密码错误: {phone}, IP: {client_ip}")
return Response({
'code': 1,
'msg': '密码错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 5. 更新IP和最后登录时间
target_user.IP = client_ip
target_user.UserLastLoginDate = timezone.now()
target_user.save(update_fields=['IP', 'UserLastLoginDate'])
# 6. 生成JWT token
refresh = RefreshToken.for_user(target_user)
token = str(refresh.access_token)
# 7. 返回成功数据(管理员无 KefuProfilenicheng 用 UserName 或 Phone 兜底)
nicheng = target_kefu.nicheng if target_kefu else (target_user.UserName or target_user.Phone)
return Response({
'code': 0,
'msg': '登录成功',
'data': {
'token': token,
'nicheng': nicheng,
'yonghuid': target_user.UserUID,
}
})
def get_client_ip(self, request):
"""
获取客户端真实IP
"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
if ip:
return ip
return request.META.get('REMOTE_ADDR', '0.0.0.0')
class KefuStatsView(APIView):
"""
客服统计数据接口
请求POST /yonghu/kfjrhqzl
请求体:{"phone": "13800138000"}
认证:必须携带有效的 JWT token放在 Authorization 头)
返回:
code=0 成功data 包含 jinrichuli, jinyuechuli, zongchuli
验证失败统一返回 401不透露具体原因
"""
permission_classes = [IsAuthenticated] # 必须登录
def post(self, request):
# 1. 获取请求中的 phone
phone = request.data.get('phone', '').strip()
if not phone:
# 缺少参数,但按约定返回 401可以改为 400但为了统一用401
logger.warning(f"统计接口缺少 phone 参数,用户: {request.user.UserUID}")
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 获取当前登录用户(从 JWT 中解析)
current_user = request.user
# 3. 验证前端传来的 phone 是否与当前用户的 phone 一致
if current_user.Phone != phone:
# 不一致,可能 token 被冒用或参数错误
logger.warning(f"统计接口 phone 不匹配: 请求phone={phone}, 用户phone={current_user.Phone}")
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 4. 验证是否为后台客服账号
from jituan.services.admin_context import is_kefu_backend_account, is_system_super_admin
if not is_kefu_backend_account(current_user):
logger.warning(f"统计接口用户类型非客服: {current_user.UserUID}")
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 5. 尝试获取客服扩展表数据(管理员跳过)
is_admin = is_system_super_admin(current_user)
if is_admin:
return Response({
'code': 0,
'data': {
'jinrichuli': 0,
'jinyuechuli': 0,
'zongchuli': 0,
}
}, status=status.HTTP_200_OK)
try:
kefu_profile = current_user.KefuProfile # OneToOneField 反向关系
except UserKefu.DoesNotExist:
logger.warning(f"统计接口客服扩展表不存在: {current_user.UserUID}")
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 6. 返回统计数据(字段名与前端约定一致)
return Response({
'code': 0,
'data': {
'jinrichuli': kefu_profile.jinrichuli,
'jinyuechuli': kefu_profile.jinyuechuli,
'zongchuli': kefu_profile.zongchuli,
}
}, status=status.HTTP_200_OK)
class KefuGetOrderTypesView(APIView):
"""
客服获取订单类型列表接口(平台订单用)
请求POST /yonghu/kfhqptddlx
参数:{"phone": "13800138000"}
认证JWT
返回code=0 + 类型列表
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
if not phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
current_user = request.user
# 2. 验证手机号一致性
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: 请求phone={phone}, 用户phone={current_user.Phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 验证用户类型及客服状态
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 管理员跳过客服扩展表检查
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
if not is_admin:
try:
kefu_profile = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu_profile.zhuangtai != 1:
logger.warning(f"客服账号已被禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 4. 查询订单类型(商品类型)
try:
# 只查询需要的字段,减少数据传输
types_qs = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url')
type_list = [
{
'id': t.id,
'biaoti': t.jieshao or '', # 假设商品类型的标题字段为 jieshao
'tupian_url': t.tupian_url or ''
}
for t in types_qs
]
return Response({'code': 0, 'data': type_list})
except Exception as e:
logger.error(f"查询商品类型表失败: {str(e)}")
# 表不存在或查询失败,返回空列表
return Response({'code': 0, 'data': []})

636
users/views/kefu_dashou.py Normal file
View File

@@ -0,0 +1,636 @@
"""users.views.kefu_dashou - auto-generated by split script."""
"""users.views.kefu - auto-generated by split script."""
import os
import sys
import json
import re
import time
import uuid
import random
import string
import requests
import hashlib
import traceback
import decimal
import jwt
import ipaddress
import xmltodict
from datetime import datetime, timedelta
from decimal import Decimal
import defusedxml.ElementTree as ET
from django.conf import settings
from django.db import models, transaction, IntegrityError, DatabaseError, connection
from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch
from django.core.cache import cache
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.http import HttpResponse
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.hashers import make_password
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.authentication import JWTAuthentication
from gvsdsdk.fluent import db, func, FQ
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from utils.money import yuan_to_fen
from utils.fadan_utils import check_fadan_qiangdan_eligible
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from users.fadan_fenhong_utils import process_fadan_fenhong
from orders.utils import (
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from backend.utils import (
update_dashou_daily_by_action, update_guanshi_daily_by_action,
update_shangjia_daily, update_zuzhang_daily_by_action,
verify_kefu_permission
)
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
from ..models import (
UserBoss, UserDashou, UserShangjia, UserGuanshi,
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
RankingRecord
)
from users.business_models import User
from ..tixian_shenhe_services import (
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
mark_transfer_success, release_collect_quota_for_record,
close_audit_wechat_failed, query_wechat_transfer_bill,
check_shijidaozhang_within_limit,
check_dashou_trial_blocks_commission_withdraw,
WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING,
)
from ..tixian_shenhe_views import process_audit_collect
from orders.models import (
Order, PlatformOrderExt, MerchantOrderExt, CommissionRate,
PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage,
Penalty, PenaltyAppealImage
)
from products.models import (
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
)
from config.models import Qunpeizhi
from backend.models import MerchantDailyStats
from rank.models import (
KaohePayTemp, Chenghao, ShenheJilu,
KaoheCishuFeiyong, YonghuChenghao
)
from users.models import UserShenheguan
from products.utils import update_shangpin_daily_stat
from shop.utils import update_dianpu_daily_stat
from rank.utils import create_shenhe_jilu_from_temp
import logging
logger = logging.getLogger(__name__)
class KefuGetDashouListView(APIView):
"""
客服获取打手列表接口(仅打手类型)
请求POST /yonghu/kefuhqdslb
参数:{
"phone": "客服手机号",
"keyword": "搜索关键词可选搜索用户ID或昵称",
"page": "页码从1开始",
"page_size": "每页数量"
}
认证JWT
返回:{
"code": 0,
"data": {
"list": [打手对象],
"has_more": true/false
}
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('zhanghao', '').strip()
keyword = request.data.get('keyword', '').strip()
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 20))
except ValueError:
return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
if not phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 构建查询条件(只查询打手)
# 通过 UserDashou 扩展表关联 User
queryset = UserDashou.query.select_related('user').all()
if keyword:
# 搜索用户ID或打手昵称
queryset = queryset.filter(
Q(user__UserUID__icontains=keyword) |
Q(nicheng__icontains=keyword)
)
# 4. 计算总数并分页
total = queryset.count()
offset = (page - 1) * page_size
dashou_list = queryset.order_by('-CreateTime')[offset:offset + page_size]
# 5. 构建返回数据(只取需要的字段)
result = []
for dashou in dashou_list:
user = dashou.user
result.append({
'yonghuid': user.UserUID,
'avatar': user.Avatar or '',
'zaixianzhuangtai': dashou.zaixianzhuangtai,
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
'nicheng': dashou.nicheng or '',
'zhuangtai': dashou.zhuangtai,
})
# 6. 判断是否有更多数据
has_more = (offset + len(dashou_list)) < total
return Response({
'code': 0,
'data': {
'list': result,
'has_more': has_more
}
})
class KefuGetDashouDetailView(APIView):
"""
客服获取打手详情接口
请求POST /yonghu/kefuhqdsxq
参数:{
"phone": "客服手机号",
"uid": "打手ID (yonghuid)"
}
认证JWT
返回code=0 + 打手信息 + 会员列表
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
uid = request.data.get('uid', '').strip()
if not phone or not uid:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询打手主表和扩展表
try:
# 使用 select_related 预加载 dashou_profile减少数据库查询
user_main = User.query.select_related('DashouProfile').get(
UserUID=uid
)
except User.DoesNotExist:
return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
# 验证用户类型是否为打手(可选,但建议验证)
# 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册)
try:
dashou = user_main.DashouProfile
except ObjectDoesNotExist:
# 用户存在但未注册打手身份,返回基本信息
user_info = {
'yonghuid': user_main.UserUID,
'avatar': user_main.Avatar or '',
'zaixianzhuangtai': 0,
'zhanghaozhuangtai': 0,
'zhuangtai': 0,
'nicheng': '',
'chenghao': '',
'dianhua': '',
'wechat': '',
'phone': user_main.Phone or '',
'CreateTime': user_main.UserCreateTime,
'create_time': user_main.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user_main, 'UserCreateTime', None) else '',
'ip': user_main.IP or '',
'jiedanzongliang': 0,
'chengjiaozongliang': 0,
'tuikuanliang': 0,
'jinrijiedan': 0,
'yue': 0.00,
'zonge': 0.00,
'jifen': 0,
'yajin': 0.00,
'jinrishouyi': 0.00,
'jinyueshouyi': 0.00,
'yaoqingren': '',
'jieshao': '',
}
member_list = []
return Response({
'code': 0,
'data': {
'user_info': user_info,
'member_list': member_list
}
})
# 4. 构建打手信息
user_info = {
'yonghuid': user_main.UserUID,
'avatar': user_main.Avatar or '',
'zaixianzhuangtai': dashou.zaixianzhuangtai,
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
'zhuangtai': dashou.zhuangtai,
'nicheng': dashou.nicheng or '',
'chenghao': dashou.chenghao or '',
'dianhua': dashou.dianhua or '',
'wechat': dashou.wechat or '',
'phone': user_main.Phone or '',
'CreateTime': user_main.UserCreateTime,
'create_time': user_main.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user_main, 'UserCreateTime', None) else '',
'ip': user_main.IP or '',
'jiedanzongliang': dashou.jiedanzongliang,
'chengjiaozongliang': dashou.chengjiaozongliang,
'tuikuanliang': dashou.tuikuanliang,
'jinrijiedan': dashou.jinrijiedan,
'yue': float(dashou.yue) if dashou.yue else 0.00,
'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
'jifen': dashou.jifen,
'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
'yaoqingren': dashou.yaoqingren or '',
'jieshao': dashou.jieshao or '',
}
# 5. 查询打手会员信息(仅未过期)
member_records = list(Huiyuangoumai.query.filter(
yonghu_id=uid
).order_by('-CreateTime'))
huiyuan_ids = {r.huiyuan_id for r in member_records if r.huiyuan_id}
huiyuan_map = {
h.huiyuan_id: h.jieshao
for h in Huiyuan.query.filter(huiyuan_id__in=huiyuan_ids).only('huiyuan_id', 'jieshao')
}
member_list = []
for record in member_records:
# 调用模型方法检查是否过期
if hasattr(record, 'jiance_shifou_daoqi'):
is_expired = record.jiance_shifou_daoqi()
else:
# 如果方法不存在,简单判断
is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
if not is_expired:
# 获取会员详情
huiyuan_name = huiyuan_map.get(record.huiyuan_id, '未知会员')
member_list.append({
'id': record.id,
'huiyuan_name': huiyuan_name,
'daoqi_time': record.daoqi_time,
'is_active': record.huiyuan_zhuangtai == 1,
})
return Response({
'code': 0,
'data': {
'user_info': user_info,
'member_list': member_list
}
})
class KefuUpdateDashouView(APIView):
permission_classes = [IsAuthenticated]
"""
客服修改打手信息接口
支持操作:
- jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb)
- jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb)
- jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd)
- gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee)
- gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee)
- gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee)
- jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg)
"""
def post(self, request):
# 1. 获取前端参数
username = request.data.get('username', '').strip()
dashou_id = request.data.get('dashou_id', '').strip()
caozuo = request.data.get('caozuo', '').strip()
value = request.data.get('value') # 金额、积分、天数等
huiyuan_id = request.data.get('huiyuan_id', '').strip() # 会员ID加/减会员时使用)
if not username or not dashou_id or not caozuo:
return Response({'code': 400, 'msg': '参数不完整'})
# 2. 权限校验(统一方法)
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions # 直接返回错误响应
# 3. 查询打手是否存在
try:
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
dashou_profile = dashou_user.DashouProfile
except User.DoesNotExist:
return Response({'code': 404, 'msg': '打手不存在'})
except UserDashou.DoesNotExist:
return Response({'code': 404, 'msg': '打手扩展信息缺失'})
# 4. 根据操作类型执行(事务内)
with transaction.atomic():
try:
# 记录修改前的值(用于日志)
before = {
'yue': dashou_profile.yue,
'yajin': dashou_profile.yajin,
'jifen': dashou_profile.jifen,
'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
'zhuangtai': dashou_profile.zhuangtai,
}
# ---------- 余额操作 ----------
if caozuo == 'jian_yue':
if '001aa' not in permissions:
return Response({'code': 403, 'msg': '无权限减打手余额'})
amount = Decimal(str(value))
if dashou_profile.yue < amount:
return Response({'code': 400, 'msg': '余额不足'})
dashou_profile.yue -= amount
dashou_profile.save()
after_yue = dashou_profile.yue
# 记录日志
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2, # 2=打手
xiugaitijiao=after_yue,
xiugaitijiaoq=before['yue'],
)
return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after_yue)}})
elif caozuo == 'jia_yue':
if '001bb' not in permissions:
return Response({'code': 403, 'msg': '无权限加打手余额'})
from jituan.services.group_finance_gate import deny_group_finance_exec
deny = deny_group_finance_exec(request.user, request)
if deny:
return deny
amount = Decimal(str(value))
dashou_profile.yue += amount
dashou_profile.save()
after_yue = dashou_profile.yue
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
xiugaitijiao=after_yue,
xiugaitijiaoq=before['yue'],
)
return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after_yue)}})
# ---------- 押金操作 ----------
elif caozuo == 'jian_yajin':
if '001aa' not in permissions:
return Response({'code': 403, 'msg': '无权限减打手押金'})
amount = Decimal(str(value))
if dashou_profile.yajin < amount:
return Response({'code': 400, 'msg': '押金不足'})
dashou_profile.yajin -= amount
dashou_profile.save()
after_yajin = dashou_profile.yajin
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
yajin=after_yajin,
yyajin=before['yajin'],
)
return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after_yajin)}})
elif caozuo == 'jia_yajin':
if '001bb' not in permissions:
return Response({'code': 403, 'msg': '无权限加打手押金'})
from jituan.services.group_finance_gate import deny_group_finance_exec
deny = deny_group_finance_exec(request.user, request)
if deny:
return deny
amount = Decimal(str(value))
dashou_profile.yajin += amount
dashou_profile.save()
after_yajin = dashou_profile.yajin
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
yajin=after_yajin,
yyajin=before['yajin'],
)
return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after_yajin)}})
# ---------- 积分操作 ----------
elif caozuo == 'jian_jifen':
if '001cc' not in permissions:
return Response({'code': 403, 'msg': '无权限减打手积分'})
jifen = int(value)
if dashou_profile.jifen < jifen:
return Response({'code': 400, 'msg': '积分不足'})
dashou_profile.jifen -= jifen
dashou_profile.save()
after_jifen = dashou_profile.jifen
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
jifen=after_jifen,
yjifen=before['jifen'],
)
return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after_jifen}})
elif caozuo == 'jia_jifen':
if '001dd' not in permissions:
return Response({'code': 403, 'msg': '无权限加打手积分'})
jifen = int(value)
dashou_profile.jifen += jifen
dashou_profile.save()
after_jifen = dashou_profile.jifen
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
jifen=after_jifen,
yjifen=before['jifen'],
)
return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after_jifen}})
# ---------- 状态修改 ----------
elif caozuo == 'gai_zhanghaozhuangtai':
if '001ee' not in permissions:
return Response({'code': 403, 'msg': '无权限修改打手账号状态'})
new_status = int(value)
if new_status not in [0, 1]:
return Response({'code': 400, 'msg': '状态值无效0=封禁,1=正常)'})
dashou_profile.zhanghaozhuangtai = new_status
dashou_profile.save()
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
qitashuoming=f'修改账号状态为{new_status}',
)
return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}})
elif caozuo == 'gai_zaixianzhuangtai':
if '001ee' not in permissions:
return Response({'code': 403, 'msg': '无权限修改打手在线状态'})
new_status = int(value)
if new_status not in [0, 1]:
return Response({'code': 400, 'msg': '状态值无效0=离线,1=在线)'})
dashou_profile.zaixianzhuangtai = new_status
dashou_profile.save()
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
qitashuoming=f'修改在线状态为{new_status}',
)
return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}})
elif caozuo == 'gai_zhuangtai':
if '001ee' not in permissions:
return Response({'code': 403, 'msg': '无权限修改打手接单状态'})
new_status = int(value)
if new_status not in [0, 1]:
return Response({'code': 400, 'msg': '状态值无效0=忙碌,1=空闲)'})
dashou_profile.zhuangtai = new_status
dashou_profile.save()
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
qitashuoming=f'修改接单状态为{new_status}',
)
return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}})
# ---------- 会员操作 ----------
elif caozuo == 'jia_huiyuan':
if '001ff' not in permissions:
return Response({'code': 403, 'msg': '无权限给打手加会员'})
if not huiyuan_id:
return Response({'code': 400, 'msg': '缺少会员ID'})
try:
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
except Huiyuan.DoesNotExist:
return Response({'code': 404, 'msg': '会员类型不存在'})
from jituan.services.club_user import get_user_club_id
from jituan.services.member_recharge import extend_member_entitlement_admin, get_club_huiyuan_row
player_club = get_user_club_id(dashou_user)
row = get_club_huiyuan_row(player_club, huiyuan_id, require_enabled=False)
days = int(value) if value else int(row.formal_days or 30) if row else 30
record, err = extend_member_entitlement_admin(
dashou_id, huiyuan_id, days, player_club,
)
if err:
return Response({'code': 400, 'msg': err})
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
huiyuants=days,
huiyuan_id=huiyuan_id,
qitashuoming=f'加正式会员 {huiyuan_id},增加{days}天(无支付单、无分红)',
)
return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
elif caozuo == 'jian_huiyuan':
if '001gg' not in permissions:
return Response({'code': 403, 'msg': '无权限给打手减会员'})
if not huiyuan_id:
return Response({'code': 400, 'msg': '缺少会员ID'})
try:
record = Huiyuangoumai.query.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id)
except Huiyuangoumai.DoesNotExist:
return Response({'code': 404, 'msg': '该打手未购买此会员'})
# 删除会员记录(或设置为过期,根据业务要求)
record.delete()
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
huiyuan_id=huiyuan_id,
qitashuoming=f'移除会员 {huiyuan_id}',
)
return Response({'code': 0, 'msg': '减会员成功'})
else:
return Response({'code': 400, 'msg': '未知操作类型'})
except Exception as e:
logger.error(f"修改打手信息异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'})

1831
users/views/kefu_orders.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,564 @@
"""users.views.kefu_punishment - auto-generated by split script."""
"""users.views.kefu - auto-generated by split script."""
import os
import sys
import json
import re
import time
import uuid
import random
import string
import requests
import hashlib
import traceback
import decimal
import jwt
import ipaddress
import xmltodict
from datetime import datetime, timedelta
from decimal import Decimal
import defusedxml.ElementTree as ET
from django.conf import settings
from django.db import models, transaction, IntegrityError, DatabaseError, connection
from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch
from django.core.cache import cache
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.http import HttpResponse
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.hashers import make_password
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.authentication import JWTAuthentication
from gvsdsdk.fluent import db, func, FQ
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from utils.money import yuan_to_fen
from utils.fadan_utils import check_fadan_qiangdan_eligible
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from users.fadan_fenhong_utils import process_fadan_fenhong
from orders.utils import (
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from backend.utils import (
update_dashou_daily_by_action, update_guanshi_daily_by_action,
update_shangjia_daily, update_zuzhang_daily_by_action,
verify_kefu_permission
)
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
from ..models import (
UserBoss, UserDashou, UserShangjia, UserGuanshi,
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
RankingRecord
)
from users.business_models import User
from ..tixian_shenhe_services import (
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
mark_transfer_success, release_collect_quota_for_record,
close_audit_wechat_failed, query_wechat_transfer_bill,
check_shijidaozhang_within_limit,
check_dashou_trial_blocks_commission_withdraw,
WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING,
)
from ..tixian_shenhe_views import process_audit_collect
from orders.models import (
Order, PlatformOrderExt, MerchantOrderExt, CommissionRate,
PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage,
Penalty, PenaltyAppealImage
)
from products.models import (
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
)
from config.models import Qunpeizhi
from backend.models import MerchantDailyStats
from rank.models import (
KaohePayTemp, Chenghao, ShenheJilu,
KaoheCishuFeiyong, YonghuChenghao
)
from users.models import UserShenheguan
from products.utils import update_shangpin_daily_stat
from shop.utils import update_dianpu_daily_stat
from rank.utils import create_shenhe_jilu_from_temp
import logging
logger = logging.getLogger(__name__)
class KefuPunishmentListView(APIView):
"""
客服获取处罚记录列表接口
请求POST /yonghu/kefu_cfgl
参数:{
"phone": "客服手机号",
"status": [0,3] 或 [1,2], # 状态数组
"dingdan_id": "订单ID可选",
"dashouid": "打手ID可选",
"page": 1,
"page_size": 20
}
认证JWT
返回:{
"code": 0,
"data": {
"list": [处罚记录],
"total": 总数,
"stats": {
"zongshu": 总记录数,
"daichuli": 待处理数,
"yichuli": 已处理数
}
}
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
@staticmethod
def _parse_status_list(raw):
"""兼容 status=[0,3]、\"0,3\"、单数字;空/all 表示不过滤。"""
if raw is None:
return None
if isinstance(raw, str):
s = raw.strip().lower()
if not s or s in ('all', 'null', 'none'):
return None
if s.startswith('['):
import json
try:
raw = json.loads(s)
except (TypeError, ValueError):
return None
else:
raw = [x.strip() for x in s.split(',') if x.strip()]
if isinstance(raw, (int, float)):
return [int(raw)]
if isinstance(raw, list):
out = []
for x in raw:
try:
out.append(int(x))
except (TypeError, ValueError):
continue
return out or None
return None
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
status_list = self._parse_status_list(request.data.get('status'))
dingdan_id = request.data.get('dingdan_id', '').strip()
dashouid = request.data.get('dashouid', '').strip()
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 20))
except ValueError:
return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
if not phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 客服身份验证
current_user = request.user
# 1. 获取前端参数
username = request.data.get('phone', '').strip()
if not username:
return Response({'code': 400, 'msg': '缺少username'})
# 2. 公共权限校验
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 3. 检查平台订单管理权限002ab
if '005aa' not in permissions:
return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
# 3. 获取统计信息(不分页)
from jituan.services.club_penalty import filter_penalty_record_qs
from jituan.services.club_user_access import list_response_meta
base_qs = filter_penalty_record_qs(PenaltyRecord.query.all(), request)
agg = base_qs.aggregate(
zongshu=Count('id'),
daichuli=Count('id', filter=Q(ApplyStatus__in=[0, 3])),
yichuli=Count('id', filter=Q(ApplyStatus__in=[1, 2])),
)
stats = {
'zongshu': agg['zongshu'],
'daichuli': agg['daichuli'],
'yichuli': agg['yichuli'],
}
queryset = base_qs
if status_list and isinstance(status_list, list):
queryset = queryset.filter(ApplyStatus__in=status_list)
if dingdan_id:
queryset = queryset.filter(OrderID=dingdan_id)
if dashouid:
queryset = queryset.filter(PlayerID=dashouid)
# 5. 分页
page_obj = queryset.order_by('-CreateTime').paginate(page=page, per_page=page_size)
total = page_obj.total
records = page_obj.items
# 6. 构建返回列表
list_data = []
for item in records:
ct = item.CreateTime
ut = item.UpdateTime
list_data.append({
'dingdan_id': item.OrderID,
'dashouid': item.PlayerID,
'qingqiuid': item.ApplicantID,
'cfliyou': item.PenaltyReason,
'sqzhuangtai': item.ApplyStatus,
'jifen': item.DeductedPoints,
'CreateTime': ct,
'UpdateTime': ut,
'create_time': ct.isoformat() if ct else '',
'update_time': ut.isoformat() if ut else '',
})
return Response({
'code': 0,
'data': {
'list': list_data,
'total': total,
'stats': stats,
**list_response_meta(request),
}
})
class KefuPunishmentDetailView(APIView):
"""
客服获取处罚详情接口
请求POST /yonghu/kefu_cf_detail
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID"
}
认证JWT
返回code=0 + 处罚记录详情(含图片)
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
if not phone or not dingdan_id:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 查询处罚记录(按创建时间倒序取最新一条)
try:
record = PenaltyRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first()
if not record:
return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"查询处罚记录失败: {e}")
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 查询商家证据图片
zhengju_images = []
shensu_images = []
try:
# 商家证据图片:上传者为请求人 (ApplicantID)
zhengju_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.ApplicantID)
zhengju_images = [item.ImageURL for item in zhengju_qs if item.ImageURL]
# 打手申诉图片:上传者为打手 (PlayerID)
shensu_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.PlayerID)
shensu_images = [item.ImageURL for item in shensu_qs if item.ImageURL]
except Exception as e:
logger.error(f"查询图片失败: {e}")
# 构建返回数据
data = {
'dingdan_id': record.OrderID,
'dashouid': record.PlayerID,
'qingqiuid': record.ApplicantID,
'chuliid': record.ProcessorID,
'cfliyou': record.PenaltyReason,
'sqzhuangtai': record.ApplyStatus,
'bhliyou': record.RejectReason,
'jifen': record.DeductedPoints,
'ssliyou': record.AppealReason or '',
'zhengju_tupian': zhengju_images,
'shensu_tupian': shensu_images,
'CreateTime': record.CreateTime,
'UpdateTime': record.UpdateTime,
}
return Response({'code': 0, 'data': data})
class KefuPunishmentActionView(APIView):
"""
客服同意/驳回处罚接口
请求POST /yonghu/kefu_cf_action
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID",
"action": 1(同意) / 2(驳回),
"reject_reason": "驳回理由(驳回时必填)"
}
认证JWT
返回code=0 成功
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
action = request.data.get('action')
reject_reason = request.data.get('reject_reason', '').strip()
if not phone or not dingdan_id or action not in [1, 2]:
return Response({'code': 401, 'msg': '参数错误'}, status=status.HTTP_400_BAD_REQUEST)
if action == 2 and not reject_reason:
return Response({'code': 400, 'msg': '驳回理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
# 客服身份验证(完整验证,此处略)
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
if kefu.zhuangtai != 1:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
except ObjectDoesNotExist:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 查询待处理的处罚记录状态0或3
try:
record = PenaltyRecord.query.filter(
OrderID=dingdan_id,
ApplyStatus__in=[0, 3]
).order_by('-CreateTime').first()
if not record:
exists = PenaltyRecord.query.filter(OrderID=dingdan_id).exists()
if exists:
return Response({'code': 400, 'msg': '该处罚记录已处理'}, status=status.HTTP_400_BAD_REQUEST)
else:
return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"查询处罚记录失败: {e}")
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
with transaction.atomic():
# 处理重复记录:同一订单的其他待处理/申诉中记录标记为驳回
duplicate_records = PenaltyRecord.query.filter(
OrderID=dingdan_id,
ApplyStatus__in=[0, 3]
).exclude(id=record.id)
for dup in duplicate_records:
dup.ApplyStatus = 2
dup.ProcessorID = phone
dup.RejectReason = "系统自动驳回:存在重复处罚记录"
dup.save()
logger.info(f"自动驳回重复处罚记录 ID={dup.id}")
if action == 1: # 同意
# 可选验证打手存在
if record.PlayerID:
try:
dashou = UserDashou.query.get(user__UserUID=record.PlayerID)
except UserDashou.DoesNotExist:
return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST)
record.ApplyStatus = 1
record.ProcessorID = phone
record.save()
# 更新商家订单扩展表
try:
dingdan = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id)
if hasattr(dingdan, 'shangjia_kuozhan'):
dingdan.shangjia_kuozhan.PenaltyApplyStatus = 1
dingdan.shangjia_kuozhan.save()
except Exception as e:
logger.warning(f"更新商家扩展表失败: {e}")
elif action == 2: # 驳回
# 返还积分
if record.DeductedPoints > 0 and record.PlayerID:
try:
dashou = UserDashou.query.get(user__UserUID=record.PlayerID)
if dashou.jifen <= 1000: # 安全限制
dashou.jifen += record.DeductedPoints
dashou.save()
logger.info(f"驳回处罚,打手{record.PlayerID}返还积分{record.DeductedPoints}")
else:
logger.warning(f"打手积分异常({dashou.jifen}),不再返还")
except UserDashou.DoesNotExist:
logger.warning(f"打手{record.PlayerID}不存在,无法返还积分")
record.ApplyStatus = 2
record.ProcessorID = phone
record.RejectReason = reject_reason
record.save()
try:
dingdan = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id)
if hasattr(dingdan, 'shangjia_kuozhan'):
dingdan.shangjia_kuozhan.PenaltyApplyStatus = 2
dingdan.shangjia_kuozhan.RejectReason = reject_reason
dingdan.shangjia_kuozhan.save()
except Exception as e:
logger.warning(f"更新商家扩展表失败: {e}")
return Response({'code': 0, 'msg': '处理成功'})
class KefuPunishView(APIView):
"""
客服处罚打手接口
请求POST /yonghu/kefuchufa
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID",
"reason": "处罚原因(可选)"
}
认证JWT
返回code=0 成功
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
reason = request.data.get('reason', '').strip()
if not phone or not dingdan_id:
return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
# 2. 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单
try:
order = Order.query.get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 4. 获取打手ID
dashou_id = order.PlayerID
if not dashou_id:
return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 查询打手
try:
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
dashou = dashou_user.DashouProfile
except User.DoesNotExist:
return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND)
except ObjectDoesNotExist:
return Response({'code': 400, 'msg': '该用户不是打手'}, status=status.HTTP_400_BAD_REQUEST)
# 6. 使用事务
from jituan.services.club_penalty import resolve_penalty_record_club_id
with transaction.atomic():
# 创建处罚记录积分扣除5分
jifen = 5
chufa = PenaltyRecord.query.create(
PlayerID=dashou_id,
ApplicantID=phone, # 客服手机号作为请求人
PenaltyReason=reason or '',
ApplyStatus=0, # 待处理
DeductedPoints=jifen,
OrderID=dingdan_id,
ClubID=resolve_penalty_record_club_id(
order_id=dingdan_id,
player_id=dashou_id,
request=request,
user=request.user,
),
)
# 扣除打手积分
if dashou.jifen >= jifen:
dashou.jifen -= jifen
else:
dashou.jifen = 0
dashou.save()
# 可选:更新订单的 clkf 字段记录处理客服
order.AssignedCS = phone
order.save()
return Response({'code': 0, 'msg': '处罚成功'})

View File

@@ -0,0 +1,786 @@
"""users.views.kefu_withdraw - auto-generated by split script."""
"""users.views.kefu - auto-generated by split script."""
import os
import sys
import json
import re
import time
import uuid
import random
import string
import requests
import hashlib
import traceback
import decimal
import jwt
import ipaddress
import xmltodict
from datetime import datetime, timedelta
from decimal import Decimal
import defusedxml.ElementTree as ET
from django.conf import settings
from django.db import models, transaction, IntegrityError, DatabaseError, connection
from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch
from django.core.cache import cache
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.http import HttpResponse
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.hashers import make_password
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.authentication import JWTAuthentication
from gvsdsdk.fluent import db, func, FQ
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from utils.money import yuan_to_fen
from utils.fadan_utils import check_fadan_qiangdan_eligible
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from users.fadan_fenhong_utils import process_fadan_fenhong
from orders.utils import (
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from backend.utils import (
update_dashou_daily_by_action, update_guanshi_daily_by_action,
update_shangjia_daily, update_zuzhang_daily_by_action,
verify_kefu_permission
)
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
from ..models import (
UserBoss, UserDashou, UserShangjia, UserGuanshi,
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
RankingRecord
)
from users.business_models import User
from ..tixian_shenhe_services import (
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
mark_transfer_success, release_collect_quota_for_record,
close_audit_wechat_failed, query_wechat_transfer_bill,
check_shijidaozhang_within_limit,
check_dashou_trial_blocks_commission_withdraw,
WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING,
)
from ..tixian_shenhe_views import process_audit_collect
from orders.models import (
Order, PlatformOrderExt, MerchantOrderExt, CommissionRate,
PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage,
Penalty, PenaltyAppealImage
)
from products.models import (
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
)
from config.models import Qunpeizhi
from backend.models import MerchantDailyStats
from rank.models import (
KaohePayTemp, Chenghao, ShenheJilu,
KaoheCishuFeiyong, YonghuChenghao
)
from users.models import UserShenheguan
from products.utils import update_shangpin_daily_stat
from shop.utils import update_dianpu_daily_stat
from rank.utils import create_shenhe_jilu_from_temp
import logging
logger = logging.getLogger(__name__)
kefu_txsh_list_logger = logging.getLogger('yonghu.kefu_txsh_list')
_TIXIAN_LIST_BASE_FIELDS = (
'id', 'yonghuid', 'avatar', 'phone', 'nicheng', 'leixing',
'zhifu', 'skzhanghao', 'jine', 'zhuangtai', 'fangshi',
'shenheid', 'bhliyou', 'CreateTime', 'UpdateTime',
)
_TIXIAN_LIST_AUDIT_FIELDS = ('shenhe_jilu_id', 'shenhe_danhao')
class KefuWithdrawListView(APIView):
"""
客服获取提现审核列表接口
请求POST /yonghu/kefu_txsh_list
参数:{
"phone": "客服手机号",
"page": 1,
"page_size": 20,
"status": 1(待审核)/2(已处理),
"type": 1(打手佣金)/2(管事分红)/3(组长分红)/4(审核官分佣)/5(打手押金)/6(商家余额), 可选
"payment": 1(微信)/2(支付宝) 可选,
"search_uid": "用户ID搜索" 可选,
"date": "YYYY-MM-DD" 可选,
"min_amount": 0, # 最小提现金额(元)可选
"max_amount": 999999, # 最大提现金额(元)可选
"dakuan_mode": 1 # 打款方式1手动打款 2自动打款可选
}
认证JWT
返回:{
"code": 0,
"data": {
"list": [提现记录],
"total": 总数,
"stats": {
"total": 总记录数,
"awaiting": 待审核数,
"processed": 已处理数,
"total_amount": "总金额(元)"
}
}
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def _safe_request_params(self, request):
try:
return dict(request.data)
except Exception:
return {'raw': str(request.data)}
def _shenhe_jilu_field_available(self):
"""探测 shenhe_jilu_id 字段是否已在数据库中就绪"""
try:
Tixianjilu.query.values_list('shenhe_jilu_id', flat=True).first()
return True
except DatabaseError as e:
kefu_txsh_list_logger.error(
'kefu_txsh_list: shenhe_jilu_id 字段不可用,将按旧表结构查询: %s', e,
exc_info=True,
)
return False
def _list_queryset(self, q, shenhe_field_ok, request):
"""字段未迁移时 only 旧字段,避免 SELECT 不存在的列导致 500"""
fields = _TIXIAN_LIST_BASE_FIELDS + (
_TIXIAN_LIST_AUDIT_FIELDS if shenhe_field_ok else ()
)
from jituan.services.tixian_club import filter_tixianjilu_by_request
qs = Tixianjilu.query.filter(q).only(*fields)
return filter_tixianjilu_by_request(qs, request)
def _resolve_row_dakuan_mode(self, item, audit_mode_map, shenhe_field_ok):
"""无关联审核或审核表未标记自动 → 默认手动(1)"""
if not shenhe_field_ok:
return 1
sid = getattr(item, 'shenhe_jilu_id', None)
if not sid:
return 1
if audit_mode_map.get(sid) == 2:
return 2
return 1
def post(self, request):
from jituan.services.group_finance_gate import require_withdraw_audit_access
dakuan_mode_filter = request.data.get('dakuan_mode')
req_params = self._safe_request_params(request)
kefu_txsh_list_logger.info('kefu_txsh_list 请求开始 params=%s', req_params)
try:
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 20))
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
if page < 1:
page = 1
if page_size < 1 or page_size > 100:
page_size = min(max(page_size, 1), 100)
status_filter = request.data.get('status')
type_filter = request.data.get('type')
payment_filter = request.data.get('payment')
search_uid = (request.data.get('search_uid') or '').strip()
date_filter = (request.data.get('date') or '').strip()
min_amount = request.data.get('min_amount')
max_amount = request.data.get('max_amount')
phone = (request.data.get('phone') or '').strip()
if not phone:
return Response({'code': 401, 'msg': '缺少客服手机号'}, status=status.HTTP_401_UNAUTHORIZED)
kefu, err_resp = require_withdraw_audit_access(request, phone)
if err_resp:
return err_resp
q = Q()
try:
status_val = int(status_filter) if status_filter is not None and str(status_filter).strip() != '' else None
except (TypeError, ValueError):
status_val = None
try:
dakuan_mode_val = int(dakuan_mode_filter) if dakuan_mode_filter is not None and str(dakuan_mode_filter).strip() != '' else None
except (TypeError, ValueError):
dakuan_mode_val = None
# 未传打款方式参数 → 默认手动打款(1)
effective_dakuan = dakuan_mode_val if dakuan_mode_val in (1, 2) else 1
if status_val == 1:
q &= Q(zhuangtai=1)
elif status_val == 2:
if effective_dakuan == 2:
q &= Q(zhuangtai__in=[2, 3, 4, 5, 6])
else:
q &= Q(zhuangtai__in=[2, 3])
if type_filter is not None and str(type_filter).strip() != '':
try:
type_val = int(type_filter)
if type_val in [1, 2, 3, 4, 5, 6]:
q &= Q(leixing=type_val)
except (TypeError, ValueError):
pass
if payment_filter is not None and str(payment_filter).strip() != '':
try:
payment_val = int(payment_filter)
if payment_val in [1, 2]:
q &= Q(fangshi=payment_val)
except (TypeError, ValueError):
pass
if search_uid:
q &= Q(yonghuid=search_uid)
if date_filter:
q &= Q(UpdateTime__date=date_filter)
if min_amount is not None and str(min_amount).strip() != '':
try:
q &= Q(jine__gte=float(min_amount))
except (TypeError, ValueError):
pass
if max_amount is not None and str(max_amount).strip() != '':
try:
q &= Q(jine__lte=float(max_amount))
except (TypeError, ValueError):
pass
shenhe_field_ok = self._shenhe_jilu_field_available()
if shenhe_field_ok:
if effective_dakuan == 2:
q &= Q(shenhe_jilu_id__isnull=False)
else:
q &= Q(shenhe_jilu_id__isnull=True)
elif effective_dakuan == 2:
kefu_txsh_list_logger.warning('kefu_txsh_list: 自动打款筛选不可用,返回空列表')
return Response({
'code': 0,
'data': {
'list': [],
'total': 0,
'stats': {'total': 0, 'awaiting': 0, 'processed': 0, 'total_amount': '0.00'},
},
})
processed_statuses = [2, 3, 4, 5, 6] if effective_dakuan == 2 else [2, 3]
base_qs = self._list_queryset(q, shenhe_field_ok, request)
agg = base_qs.aggregate(
total=Count('id'),
awaiting=Count('id', filter=Q(zhuangtai=1)),
processed=Count('id', filter=Q(zhuangtai__in=processed_statuses)),
total_amount=Sum('jine'),
)
stats = {
'total': agg['total'],
'awaiting': agg['awaiting'],
'processed': agg['processed'],
'total_amount': str(round(float(agg.get('total_amount') or 0), 2)),
}
queryset = base_qs.order_by('CreateTime')
total = queryset.count()
records = list(queryset[(page - 1) * page_size: page * page_size])
audit_ids = [
getattr(item, 'shenhe_jilu_id', None)
for item in records if getattr(item, 'shenhe_jilu_id', None)
]
audit_meta_map = {}
if audit_ids:
try:
audit_meta_map = load_audit_meta_map(audit_ids)
except Exception as e:
kefu_txsh_list_logger.error(
'kefu_txsh_list: 查询审核表信息失败: %s', e,
exc_info=True,
)
audit_mode_map = {
aid: meta.get('dakuan_mode', 2) for aid, meta in audit_meta_map.items()
}
list_data = []
for item in records:
shenhe_jilu_id = getattr(item, 'shenhe_jilu_id', None) if shenhe_field_ok else None
shenhe_danhao = (getattr(item, 'shenhe_danhao', None) or '') if shenhe_field_ok else ''
if shenhe_jilu_id and not shenhe_danhao:
shenhe_danhao = audit_meta_map.get(shenhe_jilu_id, {}).get('shenhe_danhao', '')
list_data.append({
'id': item.id,
'shenhe_jilu_id': shenhe_jilu_id,
'shenhe_danhao': shenhe_danhao,
'yonghuid': item.yonghuid,
'avatar': item.avatar or '',
'phone': item.phone or '',
'nicheng': item.nicheng or '',
'leixing': item.leixing,
'zhifu': item.zhifu or '',
'skzhanghao': item.skzhanghao or '',
'jine': str(item.jine) if item.jine is not None else '0.00',
'zhuangtai': item.zhuangtai,
'fangshi': item.fangshi,
'dakuan_mode': self._resolve_row_dakuan_mode(item, audit_mode_map, shenhe_field_ok),
'shenheid': item.shenheid or '',
'bhliyou': item.bhliyou or '',
'CreateTime': item.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if item.CreateTime else '',
'UpdateTime': item.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if item.UpdateTime else '',
})
from jituan.services.club_user_access import list_response_meta
return Response({
'code': 0,
'data': {
'list': list_data,
'total': total,
'stats': stats,
**list_response_meta(request),
},
})
except DatabaseError as e:
kefu_txsh_list_logger.error(
'kefu_txsh_list 数据库错误 dakuan_mode=%s params=%s err=%s',
dakuan_mode_filter, req_params, e,
exc_info=True,
)
return Response({
'code': 500,
'msg': '数据库查询失败,请确认已执行 python manage.py migrate yonghu',
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
kefu_txsh_list_logger.error(
'kefu_txsh_list 接口异常 dakuan_mode=%s params=%s err=%s',
dakuan_mode_filter, req_params, e,
exc_info=True,
)
return Response({
'code': 500,
'msg': '获取提现列表失败',
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class KefuWithdrawDetailView(APIView):
"""
客服获取提现详情接口
请求POST /yonghu/kefu_txsh_detail
参数:{
"phone": "客服手机号",
"tixian_id": "提现记录ID"
}
认证JWT
返回code=0 + 提现记录详情 + 用户扩展信息
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = request.data.get('phone', '').strip()
tixian_id = request.data.get('tixian_id')
if not phone or not tixian_id:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
from jituan.services.group_finance_gate import require_withdraw_audit_access
_, err_resp = require_withdraw_audit_access(request, phone)
if err_resp:
return err_resp
# 查询提现记录
try:
record = Tixianjilu.query.get(id=tixian_id)
except Tixianjilu.DoesNotExist:
return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, record)
if deny:
return deny
# 查询用户主表
try:
user = User.query.get(UserUID=record.yonghuid)
except User.DoesNotExist:
user = None
# 构建基础返回数据(字段必须与前端一致)
data = {
'id': record.id,
'yonghuid': record.yonghuid,
'avatar': record.avatar or '',
'phone': record.phone or '',
'nicheng': record.nicheng or '',
'leixing': record.leixing, # 1打手 2管事
'zhifu': record.zhifu or '',
'skzhanghao': record.skzhanghao or '',
'jine': str(record.jine) if record.jine else '0.00',
'zhuangtai': record.zhuangtai,
'fangshi': record.fangshi,
'shenheid': record.shenheid or '',
'bhliyou': record.bhliyou or '',
'CreateTime': record.CreateTime,
'UpdateTime': record.UpdateTime,
}
# 根据提现类型添加扩展信息
if user:
if record.leixing == 1: # 打手
try:
dashou = UserDashou.query.get(user=user)
# 计算成交率和退款率
total = dashou.jiedanzongliang or 0
completed = dashou.chengjiaozongliang or 0
refund = dashou.tuikuanliang or 0
completion_rate = round((completed / total * 100) if total > 0 else 0)
refund_rate = round((refund / completed * 100) if completed > 0 else 0)
data['dashouInfo'] = {
'zhuangtai': dashou.zhuangtai, # 工作状态
'zaixianzhuangtai': dashou.zaixianzhuangtai, # 在线状态
'zhanghaozhuangtai': dashou.zhanghaozhuangtai, # 账号状态
'yue': float(dashou.yue) if dashou.yue else 0.00, # 可提现余额
'zonge': float(dashou.zonge) if dashou.zonge else 0.00, # 接单总额
'jifen': dashou.jifen, # 积分
'jiedanzongliang': dashou.jiedanzongliang, # 接单总量
'chengjiaozongliang': dashou.chengjiaozongliang, # 成交总量
'tuikuanliang': dashou.tuikuanliang, # 退款总量
'completionRate': completion_rate,
'refundRate': refund_rate,
}
except ObjectDoesNotExist:
data['dashouInfo'] = None
elif record.leixing == 2: # 管事
try:
guanshi = UserGuanshi.query.get(user=user)
data['guanshiInfo'] = {
'zhuangtai': guanshi.zhuangtai, # 账号状态
'yue': float(guanshi.yue) if guanshi.yue else 0.00,
'yaogingshuliang': guanshi.yaogingshuliang, # 邀请打手总数
'jinrichongzhi': guanshi.jinrichongzhi, # 今日充值
'jinyuechongzhi': guanshi.jinyuechongzhi, # 今月充值
'chongzhifenrun': float(guanshi.chongzhifenrun) if guanshi.chongzhifenrun else 0.00,
}
except ObjectDoesNotExist:
data['guanshiInfo'] = None
return Response({'code': 0, 'data': data})
class KefuWithdrawActionView(APIView):
"""
客服处理提现接口(同意/拒绝)—— 仅此接口处理后台审核打款
POST /yonghu/kefu_txsh_action
单条:{ phone, action, tixian_id, yonghuid, leixing, reason? }
批量:{ phone, action, batch_list: [{tixian_id, yonghuid, leixing}, ...], reason? }
自动打款(dakuan_mode=2)同意→6待收款拒绝→5+驳回原因+退还可到账金额(shijidaozhang),手续费不退,不更新平台收支/每日统计
手动打款(dakuan_mode=1):原逻辑不变
状态要求Tixianjilu.zhuangtai=1 且 TixianShenheJilu.zhuangtai=1审核中其他状态一律拒绝
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def _verify_kefu(self, request, phone):
if not phone:
return None, Response({'code': 401, 'msg': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST)
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 管理员跳过客服扩展表检查
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
return current_user, None
def _parse_items(self, request):
"""解析单条或 batch_list返回 [{tixian_id, yonghuid, leixing}]"""
batch_list = request.data.get('batch_list')
if batch_list is not None:
if not isinstance(batch_list, list) or len(batch_list) == 0:
return None, Response({'code': 400, 'msg': 'batch_list 不能为空'}, status=status.HTTP_400_BAD_REQUEST)
items = []
for idx, row in enumerate(batch_list):
if not isinstance(row, dict):
return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 格式错误'}, status=status.HTTP_400_BAD_REQUEST)
tid = row.get('tixian_id')
yid = (row.get('yonghuid') or '').strip()
lx = row.get('leixing')
if not tid or not yid or lx is None:
return None, Response(
{'code': 400, 'msg': f'batch_list[{idx}] 缺少 tixian_id / yonghuid / leixing'},
status=status.HTTP_400_BAD_REQUEST,
)
try:
items.append({
'tixian_id': int(tid),
'yonghuid': yid,
'leixing': int(lx),
})
except (TypeError, ValueError):
return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
return items, None
tixian_id = request.data.get('tixian_id')
if not tixian_id:
return None, Response({'code': 400, 'msg': '提现记录ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
yonghuid = (request.data.get('yonghuid') or '').strip()
leixing = request.data.get('leixing')
try:
item = {'tixian_id': int(tixian_id), 'yonghuid': yonghuid, 'leixing': int(leixing) if leixing is not None else None}
except (TypeError, ValueError):
return None, Response({'code': 400, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
return [item], None
def _process_auto_item(self, request, item, action, reason, kefu_user):
"""自动打款单条处理(须在 transaction.atomic 内调用)"""
tixian_id = item['tixian_id']
req_yonghuid = item['yonghuid']
req_leixing = item['leixing']
if req_leixing not in [1, 2, 3, 4, 5, 6]:
raise ValueError(f'提现记录{tixian_id}:提现类型无效')
try:
tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
except Tixianjilu.DoesNotExist:
raise ValueError(f'提现记录{tixian_id}不存在')
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, tixian)
if deny:
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
if tixian.yonghuid != req_yonghuid:
raise ValueError(f'提现记录{tixian_id}用户ID不匹配')
if tixian.leixing != req_leixing:
raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
if tixian.zhuangtai != 1:
raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
if not tixian.shenhe_jilu_id:
raise ValueError(f'提现记录{tixian_id}:非自动打款审核记录')
try:
audit = TixianShenheJilu.objects.select_for_update().get(id=tixian.shenhe_jilu_id)
except TixianShenheJilu.DoesNotExist:
raise ValueError(f'提现记录{tixian_id}:审核记录不存在')
if audit.yonghuid != req_yonghuid:
raise ValueError(f'提现记录{tixian_id}审核记录用户ID不匹配')
if audit.leixing != req_leixing:
raise ValueError(f'提现记录{tixian_id}:审核记录提现类型不匹配')
if audit.zhuangtai != 1:
raise ValueError(f'提现记录{tixian_id}:审核记录非审核中状态')
try:
user = User.objects.select_for_update().get(UserUID=req_yonghuid)
except User.DoesNotExist:
raise ValueError(f'用户{req_yonghuid}不存在')
if action == 1:
limit_ok, limit_msg = check_shijidaozhang_within_limit(audit.shijidaozhang)
if not limit_ok:
sync_audit_and_jilu_status(
audit, 5,
bhliyou=limit_msg,
bo_hui_ren_id=kefu_user.UserUID,
)
refund_balance(user, audit.leixing, audit.shijidaozhang)
tixian.shenheid = kefu_user.UserUID
tixian.bhliyou = limit_msg
tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
raise ValueError(f'提现记录{tixian_id}{limit_msg},已自动驳回并退款')
sync_audit_and_jilu_status(audit, 6, shenhe_ren_id=kefu_user.UserUID)
tixian.shenheid = kefu_user.UserUID
tixian.save(update_fields=['shenheid', 'UpdateTime'])
else:
sync_audit_and_jilu_status(
audit, 5,
bhliyou=reason,
bo_hui_ren_id=kefu_user.UserUID,
)
# 退还可到账金额,申请时扣的 shenqing_jine 中的手续费(shouxufei)不退
refund_balance(user, audit.leixing, audit.shijidaozhang)
tixian.shenheid = kefu_user.UserUID
tixian.bhliyou = reason
tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
def _process_manual_item(self, request, item, action, reason, phone):
"""手动打款单条处理(原逻辑,须在 transaction.atomic 内调用)"""
tixian_id = item['tixian_id']
try:
tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
except Tixianjilu.DoesNotExist:
raise ValueError(f'提现记录{tixian_id}不存在')
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, tixian)
if deny:
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
req_yonghuid = item.get('yonghuid')
req_leixing = item.get('leixing')
if req_yonghuid and tixian.yonghuid != req_yonghuid:
raise ValueError(f'提现记录{tixian_id}用户ID不匹配')
if req_leixing is not None and tixian.leixing != req_leixing:
raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
if tixian.zhuangtai != 1:
raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
if getattr(tixian, 'shenhe_jilu_id', None):
raise ValueError(f'提现记录{tixian_id}:自动打款请走自动流程')
try:
user = User.query.get(UserUID=tixian.yonghuid)
except User.DoesNotExist:
raise ValueError(f'用户{tixian.yonghuid}不存在')
from jituan.services.club_user import get_user_club_id
if action == 1:
tixian.zhuangtai = 2
try:
from jituan.services.szjilu_accounting import apply_szjilu_expense
update_daily_payout(tixian.jine, get_user_club_id(user))
apply_szjilu_expense(tixian.jine, get_user_club_id(user))
except Exception as e:
logger.error(f'更新收支记录失败: {e}')
else:
try:
refund_balance(user, tixian.leixing, tixian.jine)
except ValueError as e:
raise ValueError(str(e))
tixian.zhuangtai = 3
tixian.bhliyou = reason
tixian.shenheid = phone
tixian.UpdateTime = timezone.now()
tixian.save()
def post(self, request):
logger.info(f'接收到提现处理请求,数据: {request.data}')
phone = request.data.get('phone', '').strip()
action_raw = request.data.get('action')
reason = request.data.get('reason', '').strip()
try:
action = int(action_raw)
except (TypeError, ValueError):
return Response({'code': 400, 'msg': 'action 参数必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
if action not in [1, 2]:
return Response({'code': 400, 'msg': 'action 必须为1(同意)或2(拒绝)'}, status=status.HTTP_400_BAD_REQUEST)
if action == 2 and not reason:
return Response({'code': 400, 'msg': '拒绝提现时理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
kefu_user, err_resp = self._verify_kefu(request, phone)
if err_resp:
return err_resp
from jituan.services.group_finance_gate import require_withdraw_audit_access
_, audit_err = require_withdraw_audit_access(request, phone)
if audit_err:
return audit_err
items, err_resp = self._parse_items(request)
if err_resp:
return err_resp
is_batch = isinstance(request.data.get('batch_list'), list) and len(request.data.get('batch_list')) > 0
try:
with transaction.atomic():
for item in items:
tixian_id = item['tixian_id']
try:
tixian_probe = Tixianjilu.objects.select_for_update().get(id=tixian_id)
except Tixianjilu.DoesNotExist:
raise ValueError(f'提现记录{tixian_id}不存在')
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, tixian_probe)
if deny:
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
# 有关联审核记录(shenhe_jilu_id)即为 zddksh 自动打款,不依赖 dakuan_mode 字段
is_auto = bool(getattr(tixian_probe, 'shenhe_jilu_id', None))
if is_auto:
self._process_auto_item(request, item, action, reason, kefu_user)
else:
if is_batch:
raise ValueError(f'提现记录{tixian_id}:批量操作仅支持自动打款记录')
self._process_manual_item(request, item, action, reason, phone)
except ValueError as e:
return Response({'code': 400, 'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f'处理提现事务失败: {e}', exc_info=True)
return Response({'code': 500, 'msg': f'处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
msg = '批量处理成功' if is_batch and len(items) > 1 else '处理成功'
return Response({'code': 0, 'msg': msg, 'data': {'count': len(items)}})

View File

@@ -719,13 +719,15 @@ class FaKuanShenSuView(APIView):
PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=2).delete() PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=2).delete()
# 插入新上传的申诉图片 # 插入新上传的申诉图片
for url in tupian_urls: PenaltyAppealImage.objects.bulk_create([
PenaltyAppealImage.query.create( PenaltyAppealImage(
Penalty=fadan, Penalty=fadan,
PenalizedUserID=yonghuid, PenalizedUserID=yonghuid,
ImageURL=url, ImageURL=url,
Purpose=2 Purpose=2,
) )
for url in tupian_urls
])
return Response({'code': 0, 'msg': '申诉提交成功'}) return Response({'code': 0, 'msg': '申诉提交成功'})

View File

@@ -1052,7 +1052,9 @@ class TixianAssetView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def post(self, request): def post(self, request):
user = request.user user = User.query.select_related(
'DashouProfile', 'ShopProfile', 'GuanshiProfile', 'ZuzhangProfile', 'ShenheguanProfile'
).get(UserUID=request.user.UserUID)
# 初始化返回数据默认全部为0 # 初始化返回数据默认全部为0
data = { data = {
'dashou_yue': '0.00', 'dashou_yue': '0.00',