diff --git a/backend/views/exams.py b/backend/views/exams.py
index aa32aab..88ed5e1 100644
--- a/backend/views/exams.py
+++ b/backend/views/exams.py
@@ -580,12 +580,19 @@ class KhjlglView(APIView):
pending=Count('jilu_id', filter=Q(zhuangtai=3)),
).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 = {
- 'total': stats_base.count(),
- 'passed': stats_base.filter(zhuangtai=1).count(),
- 'failed': stats_base.filter(zhuangtai=2).count(),
- 'in_progress': stats_base.filter(zhuangtai=0).count(),
- 'pending': stats_base.filter(zhuangtai=3).count(),
+ 'total': _stats_agg['total'],
+ 'passed': _stats_agg['passed'],
+ 'failed': _stats_agg['failed'],
+ 'in_progress': _stats_agg['in_progress'],
+ 'pending': _stats_agg['pending'],
'total_fee': float(agg['total_fee'] or 0),
'tag_stats': [
{
@@ -702,8 +709,11 @@ class KhjlczView(APIView):
for f in KaoheJiluFeiyong.query.filter(jilu_id=jilu_id).order_by('cishu')
]
reject_list = []
- for r in KaoheJujueJilu.query.filter(jilu_id=jilu_id).order_by('-CreateTime'):
- kg_prof = _batch_user_profile_map([r.shenheguan_id]).get(r.shenheguan_id, {})
+ _reject_records = list(KaoheJujueJilu.query.filter(jilu_id=jilu_id).order_by('-CreateTime'))
+ _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({
'cishu': r.cishu,
'shenheguan_id': r.shenheguan_id,
diff --git a/backend/views/finance.py b/backend/views/finance.py
index f67d21a..7152fe4 100644
--- a/backend/views/finance.py
+++ b/backend/views/finance.py
@@ -1,4 +1,4 @@
-import hmac
+import hmac
import threading
import traceback
import uuid
@@ -12,6 +12,7 @@ import secrets
import random
import string
from calendar import monthrange
+from collections import defaultdict
from decimal import Decimal
from datetime import date, datetime, timedelta
from django.utils import timezone
@@ -155,13 +156,18 @@ class CwhybkhqView(APIView):
if 'huiyuancaiwu' not in permissions:
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 = []
- for bk in Bankuai.query.all():
- members = Huiyuan.query.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id')
+ for bk in all_bankuai:
bankuai_list.append({
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
- 'members': list(members)
+ 'members': members_by_bankuai.get(bk.bankuai_id, [])
})
return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list})
@@ -597,21 +603,30 @@ class CwhqjtddsjView(APIView):
summary_qs = filter_queryset_by_club(
Order.query.filter(**filters), request, club_field='ClubID',
)
- total_order_count = summary_qs.count()
- total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0
- total_success_count = summary_qs.filter(Status=3).count()
- total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0
- total_refund_count = summary_qs.filter(Status=5).count()
- total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0
- total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate(
- s=Sum('pingtai_kuozhan__ShopIncome'))['s'] or 0
-
- platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0
- merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0
- platform_dashou_sum = \
- summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0
- merchant_dashou_sum = \
- summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['s'] or 0
+ agg = summary_qs.aggregate(
+ total_order_count=Count('id'),
+ total_order_amount=Sum('Amount'),
+ total_success_count=Count('id', filter=Q(Status=3)),
+ total_success_amount=Sum('Amount', filter=Q(Status=3)),
+ total_refund_count=Count('id', filter=Q(Status=5)),
+ total_dashou=Sum('PlayerCommission', filter=Q(Status=3)),
+ total_dianpu_fenhong=Sum('pingtai_kuozhan__ShopIncome', filter=Q(Status=3, Platform=1)),
+ platform_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
+ merchant_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
+ platform_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
+ merchant_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
+ )
+ total_order_count = agg['total_order_count'] 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_merchant = merchant_amount - merchant_dashou_sum
diff --git a/backend/views/guanli.py b/backend/views/guanli.py
index e2fbe0b..4cfa3b7 100644
--- a/backend/views/guanli.py
+++ b/backend/views/guanli.py
@@ -1,4 +1,4 @@
-import hmac
+import hmac
import threading
import traceback
import uuid
@@ -357,9 +357,16 @@ class GetGuanliDetailView(APIView):
custom_first = {} # 首次分红定制(cishu=1)
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:
# 获取会员信息
- member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first()
+ member = _huiyuan_map.get(record.huiyuan)
if not member:
continue
member_info = {
@@ -711,10 +718,16 @@ class UpdateGuanliView(APIView):
if custom_first is not None and isinstance(custom_first, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
- # 获取当前所有首次定制记录
- existing_first = DuociFenhong.query.filter(
- club_id=guanshi_club, yonghuid=yonghuid, cishu=1,
+ # 批量预取当前所有首次定制记录(避免循环内 N+1)
+ existing_first_list = list(
+ 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: 金额}
for huiyuan_id, amount in custom_first.items():
try:
@@ -722,24 +735,34 @@ class UpdateGuanliView(APIView):
except:
continue
if amount <= 0:
- DuociFenhong.query.filter(
- club_id=guanshi_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1,
- ).delete()
+ rec = _existing_map.get(huiyuan_id)
+ if rec is not None:
+ _to_delete_ids.append(rec.id)
else:
- DuociFenhong.query.update_or_create(
- club_id=guanshi_club,
- yonghuid=yonghuid,
- huiyuan=huiyuan_id,
- cishu=1,
- defaults={
- 'guanshi_fenhong': amount,
- 'zuzhang_fenhong': Decimal('0'),
- },
- )
+ rec = _existing_map.get(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,
+ yonghuid=yonghuid,
+ huiyuan=huiyuan_id,
+ cishu=1,
+ guanshi_fenhong=amount,
+ zuzhang_fenhong=Decimal('0'),
+ ))
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
- for record in existing_first:
- if record.huiyuan not in custom_first:
- record.delete()
+ for rec in existing_first_list:
+ if rec.huiyuan not in custom_first and rec.id not in _to_delete_ids:
+ _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)
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)
# 收集需要保留的记录
keep_set = set()
+ # 解析前端数据,先汇总 keep_set 与目标金额
+ _extra_targets = [] # [(cishu, huiyuan_id, amount)]
for times_str, members in extra_map.items():
try:
cishu = int(times_str)
@@ -767,24 +792,42 @@ class UpdateGuanliView(APIView):
except:
continue
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=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,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=cishu,
- defaults={
- 'guanshi_fenhong': amount,
- 'zuzhang_fenhong': Decimal('0')
- }
- )
+ guanshi_fenhong=amount,
+ zuzhang_fenhong=Decimal('0'),
+ ))
# 删除不在 keep_set 中的额外次数记录
- existing_extra = DuociFenhong.query.filter(
- club_id=guanshi_club, yonghuid=yonghuid, cishu__gte=2,
- )
- for record in existing_extra:
- if (record.cishu, record.huiyuan) not in keep_set:
- record.delete()
+ for rec in existing_extra_list:
+ if (rec.cishu, rec.huiyuan) not in keep_set:
+ _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()
dividend_parts = []
if perm_enabled is not None:
diff --git a/backend/views/kefu.py b/backend/views/kefu.py
index 0e68b9b..7bfbe29 100644
--- a/backend/views/kefu.py
+++ b/backend/views/kefu.py
@@ -342,6 +342,12 @@ class KefuGetDashouDetailView(APIView):
member_records = Huiyuangoumai.query.filter(
yonghu_id=uid
).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 = []
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
if not is_expired:
- try:
- huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id)
- huiyuan_name = huiyuan.jieshao
- except Huiyuan.DoesNotExist:
- huiyuan_name = '未知会员'
+ huiyuan_name = _huiyuan_map.get(record.huiyuan_id, '未知会员')
member_list.append({
'id': record.id,
'huiyuan_name': huiyuan_name,
@@ -828,8 +830,12 @@ class KefuGetShangjiaListView(APIView):
return Response({'code': 400, 'msg': '商家状态格式错误'})
# 5. 统计总商家数(应用筛选后)
- total = shangjia_qs.count()
- valid_merchant_count = shangjia_qs.filter(zhuangtai=1).count()
+ _agg = shangjia_qs.aggregate(
+ total=Count('id'),
+ valid=Count('id', filter=Q(zhuangtai=1)),
+ )
+ total = _agg['total']
+ valid_merchant_count = _agg['valid']
# 6. 分页
offset = (page - 1) * page_size
diff --git a/backend/views/penalties.py b/backend/views/penalties.py
index 15e7173..66b594f 100644
--- a/backend/views/penalties.py
+++ b/backend/views/penalties.py
@@ -1,4 +1,4 @@
-import hmac
+import hmac
import threading
import traceback
import uuid
@@ -518,13 +518,15 @@ class FaKuanChuangJianView(APIView):
user=request.user,
),
)
- for relative_url in uploaded_urls:
- PenaltyAppealImage.query.create(
+ PenaltyAppealImage.objects.bulk_create([
+ PenaltyAppealImage(
Penalty=penalty,
PenalizedUserID=PenalizedUserID,
ImageURL=relative_url,
- Purpose=1
+ Purpose=1,
)
+ for relative_url in uploaded_urls
+ ])
from users.fadan_fenhong_utils import lock_penalty_bonus
lock_penalty_bonus(penalty)
diff --git a/backend/views/products.py b/backend/views/products.py
index 2e6969a..0681681 100644
--- a/backend/views/products.py
+++ b/backend/views/products.py
@@ -1,4 +1,4 @@
-import hmac
+import hmac
import threading
import traceback
import uuid
@@ -147,14 +147,16 @@ class GetProductBaseDataView(APIView):
# 获取所有商品类型(按 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 = []
for t in type_qs:
- # 🔥 修改:统计该类型下的公共商品数量(店铺为空且店铺商品类型为空)
- product_count = Shangpin.query.filter(
- leixing_id=t.id,
- dianpu__isnull=True, # 所属店铺为空
- dianpu_leixing__isnull=True # 店铺商品类型为空
- ).count()
+ product_count = _type_counts.get(t.id, 0)
type_list.append({
'id': t.id,
'jieshao': t.jieshao or '',
@@ -166,14 +168,15 @@ class GetProductBaseDataView(APIView):
# 获取所有专区(按 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 = []
for z in zone_qs:
- # 🔥 修改:统计该专区下的公共商品数量(店铺为空且店铺商品类型为空)
- product_count = Shangpin.query.filter(
- zhuanqu_id=z.id,
- dianpu__isnull=True, # 所属店铺为空
- dianpu_leixing__isnull=True # 店铺商品类型为空
- ).count()
+ product_count = _zone_counts.get(z.id, 0)
zone_list.append({
'id': z.id,
'mingzi': z.mingzi or '',
@@ -368,10 +371,14 @@ class GetProductListView(APIView):
# 获取所有商品类型(按 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 = []
for t in type_qs:
- # 统计该类型下的商品数量(忽略审核状态?通常统计所有商品,可根据需求调整)
- product_count = Shangpin.query.filter(leixing_id=t.id).count()
+ product_count = _type_counts.get(t.id, 0)
type_list.append({
'id': t.id,
'jieshao': t.jieshao or '',
@@ -383,9 +390,13 @@ class GetProductListView(APIView):
# 获取所有专区(按 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 = []
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({
'id': z.id,
'mingzi': z.mingzi or '',
diff --git a/backend/views/roles.py b/backend/views/roles.py
index 1dccde3..38a75a9 100644
--- a/backend/views/roles.py
+++ b/backend/views/roles.py
@@ -1,4 +1,4 @@
-import hmac
+import hmac
import threading
import traceback
import uuid
@@ -157,19 +157,29 @@ class GetRolePermissionView(APIView):
kefu_user_uuids = list(
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 = []
for role in roles:
user_count = count_role_users_in_scope(role, request, kefu_user_uuids)
- role_perm_uuids = RolePermission.objects.filter(
- 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')
- ]
+ perms_of_role = _perms_by_role.get(role.RoleUUID, [])
roles_data.append({
'role_code': role.RoleName,
'role_name': role.RoleName,
@@ -434,18 +444,29 @@ class GetAdminUserListView(APIView):
paginator = Paginator(users, page_size)
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 = []
- for user in page_obj:
- # 通过 gvsdsdk UserRole 获取用户角色
- 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
- ]
+ for user in _page_users:
+ roles_data = _roles_by_user.get(user.UserUUID, [])
data_list.append({
'phone': user.Phone or '',
'yonghuid': user.UserUID or '',
diff --git a/backend/views/sections.py b/backend/views/sections.py
index 9df043d..d99a9e5 100644
--- a/backend/views/sections.py
+++ b/backend/views/sections.py
@@ -1,4 +1,4 @@
-import hmac
+import hmac
import threading
import traceback
import uuid
@@ -173,19 +173,27 @@ class HqbkxxView(APIView):
price_map[row.huiyuan_id] = row
huiyuan_list = []
- for hy in Huiyuan.query.all():
- row = price_map.get(hy.huiyuan_id)
+ # 在受限 scope 下预过滤,避免 Huiyuan 全表遍历
+ # 注:当 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:
continue
if row is not None and not row.is_enabled:
continue
item = {
- 'huiyuan_id': hy.huiyuan_id,
- 'jieshao': hy.jieshao,
- 'bankuai_id': hy.bankuai_id,
- 'jiage': str(row.jiage) if row else str(hy.jiage),
- 'guanshifc': str(row.guanshifc) if row else str(hy.guanshifc),
- 'zuzhangfc': str(row.zuzhangfc) if row else str(hy.zuzhangfc),
+ 'huiyuan_id': hy['huiyuan_id'],
+ 'jieshao': hy['jieshao'],
+ 'bankuai_id': hy['bankuai_id'],
+ 'jiage': str(row.jiage) if row else str(hy['jiage']),
+ 'guanshifc': str(row.guanshifc) if row else str(hy['guanshifc']),
+ 'zuzhangfc': str(row.zuzhangfc) if row else str(hy['zuzhangfc']),
}
huiyuan_list.append(item)
diff --git a/backend/views/system.py b/backend/views/system.py
index e02baa8..1407dd0 100644
--- a/backend/views/system.py
+++ b/backend/views/system.py
@@ -1,4 +1,4 @@
-import hmac
+import hmac
import threading
import traceback
import uuid
@@ -241,11 +241,16 @@ class GetProductTypeZoneView(APIView):
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 越大越靠前
type_list = []
for t in types:
- # 统计该类型下的专区数量
- zone_count = ShangpinZhuanqu.query.filter(leixing_id=t.id).count()
+ # 从预聚合的 map 中取专区数量
+ zone_count = zone_count_map.get(t.id, 0)
type_list.append({
'id': t.id,
'jieshao': t.jieshao or '',
@@ -531,10 +536,11 @@ class ModifyProductTypeZoneView(APIView):
# 删除该类型下的所有专区
zones = ShangpinZhuanqu.query.filter(leixing_id=type_id)
if delete_products:
- # 删除专区下的所有商品
- for zone in zones:
- Shangpin.query.filter(zhuanqu_id=zone.id).delete()
- zones.delete()
+ # 收集 zone_ids 后批量删除,避免循环内 N+1 delete
+ zone_ids = list(zones.values_list('id', flat=True))
+ if zone_ids:
+ 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()
else:
@@ -928,7 +934,8 @@ class PopupNoticeModifyAPIView(APIView):
if cascade:
# 级联删除:使用事务保证原子性
with transaction.atomic():
- popups = page.popups.all()
+ # 预加载 images 关联,避免循环内 N+1 访问 related manager
+ popups = page.popups.prefetch_related('images')
for popup in popups:
# 删除弹窗下的所有图片文件
for img in popup.images.all():
diff --git a/backend/views/titles.py b/backend/views/titles.py
index f88e569..4a63e10 100644
--- a/backend/views/titles.py
+++ b/backend/views/titles.py
@@ -1,4 +1,4 @@
-import hmac
+import hmac
import threading
import traceback
import uuid
@@ -19,7 +19,7 @@ from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator
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 django.db.models.functions import TruncDate, TruncMonth, TruncYear
from django.contrib.auth.hashers import make_password
@@ -132,13 +132,16 @@ class KhpzhqView(APIView):
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 = []
for ch in chenghao_qs:
# 获取费用:按次数排序,转为 {cishu: feiyong} 字典
+ # prefetch_related 已用 Prefetch 排序,直接遍历不会绕过缓存
feiyong_dict = {
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
diff --git a/backend/views/zuzhang.py b/backend/views/zuzhang.py
index 196d5fc..03b9e22 100644
--- a/backend/views/zuzhang.py
+++ b/backend/views/zuzhang.py
@@ -1,4 +1,4 @@
-import hmac
+import hmac
import threading
import traceback
import uuid
@@ -351,9 +351,16 @@ class GetZuzhangDetailView(APIView):
custom_first = {} # 首次分红定制(cishu=1)
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:
# 获取会员信息
- member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first()
+ member = _huiyuan_map.get(record.huiyuan)
if not member:
continue
member_info = {
@@ -580,9 +587,16 @@ class UpdateZuzhangView(APIView):
if custom_first is not None and isinstance(custom_first, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
- existing_first = DuociFenhong.query.filter(
- club_id=zuzhang_club, yonghuid=yonghuid, cishu=1,
+ # 批量预取当前所有首次定制记录(避免循环内 N+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():
try:
amount = Decimal(str(amount))
@@ -590,30 +604,41 @@ class UpdateZuzhangView(APIView):
continue
if amount <= 0:
# 删除定制(恢复默认)
- DuociFenhong.query.filter(
- club_id=zuzhang_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1,
- ).delete()
+ rec = _existing_map.get(huiyuan_id)
+ if rec is not None:
+ _to_delete_ids.append(rec.id)
else:
- DuociFenhong.query.update_or_create(
- club_id=zuzhang_club,
- yonghuid=yonghuid,
- huiyuan=huiyuan_id,
- cishu=1,
- defaults={
- 'zuzhang_fenhong': amount,
- 'guanshi_fenhong': Decimal('0') # 组长表只关心组长分红
- }
- )
+ rec = _existing_map.get(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,
+ yonghuid=yonghuid,
+ huiyuan=huiyuan_id,
+ cishu=1,
+ zuzhang_fenhong=amount,
+ guanshi_fenhong=Decimal('0'),
+ ))
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
- for record in existing_first:
- if record.huiyuan not in custom_first:
- record.delete()
+ for rec in existing_first_list:
+ if rec.huiyuan not in custom_first and rec.id not in _to_delete_ids:
+ _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)
if extra_map is not None and isinstance(extra_map, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
keep_set = set()
+ _extra_targets = [] # [(cishu, huiyuan_id, amount)]
for times_str, members in extra_map.items():
try:
cishu = int(times_str)
@@ -634,23 +659,42 @@ class UpdateZuzhangView(APIView):
except:
continue
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,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=cishu,
- defaults={
- 'zuzhang_fenhong': amount,
- 'guanshi_fenhong': Decimal('0')
- }
- )
+ zuzhang_fenhong=amount,
+ guanshi_fenhong=Decimal('0'),
+ ))
# 删除不在 keep_set 中的额外次数记录
- existing_extra = DuociFenhong.query.filter(
- club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2,
- )
- for record in existing_extra:
- if (record.cishu, record.huiyuan) not in keep_set:
- record.delete()
+ for rec in existing_extra_list:
+ if (rec.cishu, rec.huiyuan) not in keep_set:
+ _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()
dividend_parts = []
if perm_enabled is not None:
diff --git a/backend/views/zxkf.py b/backend/views/zxkf.py
index 302ee0c..1068eb5 100644
--- a/backend/views/zxkf.py
+++ b/backend/views/zxkf.py
@@ -175,7 +175,7 @@ class ZxkfghdsView(APIView):
shangjia_ext = old_order.shangjia_kuozhan
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}"
except (ObjectDoesNotExist, UserShangjia.DoesNotExist):
logger.error(f"商家信息缺失,订单: {OrderID}")
diff --git a/check_prod_uid.py b/check_prod_uid.py
deleted file mode 100644
index 9dfab53..0000000
--- a/check_prod_uid.py
+++ /dev/null
@@ -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()
diff --git a/config/views/admin_config.py b/config/views/admin_config.py
index 9158179..127dc32 100644
--- a/config/views/admin_config.py
+++ b/config/views/admin_config.py
@@ -1,4 +1,4 @@
-import io
+import io
import os
import re
import time
@@ -394,40 +394,46 @@ class AdminUpdateConfigView(APIView):
# 🔴 修复:更新QQ群配置 - 确保根据peizhiid正确更新对应的记录
qq_groups = request.data.get('qq_groups')
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 = []
for group_data in qq_groups:
peizhiid = group_data.get('peizhiid')
if not peizhiid:
continue
- # 🔴 修复:使用filter()确保查询到正确的记录
- qun_configs = Qunpeizhi.query.filter(GroupType=peizhiid)
-
- 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')
+ 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:
qun_config.GroupContent = neirong or ''
if qunid is not None:
qun_config.GroupID = qunid or ''
if jieshao is not None:
qun_config.Description = jieshao or ''
-
- qun_config.save()
+ _to_update.append(qun_config)
else:
- # 如果不存在,创建新的(使用提供的peizhiid)
- qun_config = Qunpeizhi.query.create(
+ # 不存在则创建新的
+ qun_config = Qunpeizhi(
GroupType=peizhiid,
- GroupContent=group_data.get('neirong', '') or '',
- GroupID=group_data.get('qunid', '') or '',
- Description=group_data.get('jieshao', '') or ''
+ GroupContent=neirong or '',
+ GroupID=qunid or '',
+ Description=jieshao or '',
)
+ _to_create.append(qun_config)
updated_groups.append({
'peizhiid': qun_config.GroupType,
@@ -436,6 +442,13 @@ class AdminUpdateConfigView(APIView):
'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:
response_data['qq_groups'] = updated_groups
diff --git a/config/views/admin_stats.py b/config/views/admin_stats.py
index 93fa84b..f45e880 100644
--- a/config/views/admin_stats.py
+++ b/config/views/admin_stats.py
@@ -1,4 +1,4 @@
-import io
+import io
import os
import re
import time
@@ -270,6 +270,13 @@ class DashouPaihangView(APIView):
# 第一名数量不足,不展示
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 = []
for index, guanshi in enumerate(guanshi_list):
@@ -277,8 +284,7 @@ class DashouPaihangView(APIView):
user_main = guanshi.user
# 管事的昵称需要查询老板扩展表
- boss_profile = UserBoss.query.filter(user=user_main).first()
- guanshi_nicheng = boss_profile.nickname if boss_profile else '未设置昵称'
+ guanshi_nicheng = _boss_nicheng_map.get(guanshi.user_id, '未设置昵称')
# 获取头像(相对URL)
touxiang_url = user_main.Avatar or ''
@@ -337,6 +343,13 @@ class DashouPaihangView(APIView):
# 第一名流水不足,不展示
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 = []
for index, shangjia in enumerate(shangjia_list):
@@ -344,8 +357,7 @@ class DashouPaihangView(APIView):
user_main = shangjia.user
# 商家的昵称需要查询老板扩展表
- boss_profile = UserBoss.query.filter(user=user_main).first()
- shangjia_nicheng = boss_profile.nickname if boss_profile else '未设置昵称'
+ shangjia_nicheng = _boss_nicheng_map.get(shangjia.user_id, '未设置昵称')
# 获取头像(相对URL)
touxiang_url = user_main.Avatar or ''
@@ -604,21 +616,21 @@ class GetXiugaiJiluView(APIView):
records = cursor.fetchall()
# 🔥 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 = []
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 = ''
diff --git a/config/views/merchant_links.py b/config/views/merchant_links.py
index d5f3af4..7071ee8 100644
--- a/config/views/merchant_links.py
+++ b/config/views/merchant_links.py
@@ -1,4 +1,4 @@
-import io
+import io
import os
import re
import time
@@ -508,7 +508,7 @@ class KehuGetDingdanLianjieView(APIView):
# 获取商家昵称
shangjia_nicheng = ''
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_nicheng = shangjia_obj.nicheng
except (User.DoesNotExist, UserShangjia.DoesNotExist):
@@ -816,7 +816,7 @@ class KehuTianxieDingdanView(APIView):
# 验证打手是否存在且有效
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}")
# 通过反向关系获取打手扩展信息
@@ -922,7 +922,7 @@ class KehuTianxieDingdanView(APIView):
# 获取商家信息用于返回
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_mingcheng = shangjia_obj.nicheng
except:
diff --git a/config/views/merchant_templates.py b/config/views/merchant_templates.py
index f3d3a03..fd84a92 100644
--- a/config/views/merchant_templates.py
+++ b/config/views/merchant_templates.py
@@ -1,4 +1,4 @@
-import io
+import io
import os
import re
import time
@@ -142,17 +142,21 @@ class ShangjiaMobanListView(APIView):
return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400)
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 = []
- for moban in current_page.object_list:
+ for moban in _mobans_list:
texiao_json = ''
label_name = ''
- if moban.TitleID:
- try:
- ch = Chenghao.query.get(id=moban.TitleID)
- label_name = ch.mingcheng
- texiao_json = ch.texiao_miaoshu or ''
- except Chenghao.DoesNotExist:
- pass
+ ch = _ch_map.get(moban.TitleID)
+ if ch:
+ label_name = ch.mingcheng
+ texiao_json = ch.texiao_miaoshu or ''
formatted.append({
'mobanId': moban.id,
'shangpinTypeId': moban.ProductTypeID,
diff --git a/create_rbac_tables.sql b/create_rbac_tables.sql
deleted file mode 100644
index aff10cd..0000000
--- a/create_rbac_tables.sql
+++ /dev/null
@@ -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`);
diff --git a/jituan/services/caiwu_stats.py b/jituan/services/caiwu_stats.py
index de1b008..37af4b3 100644
--- a/jituan/services/caiwu_stats.py
+++ b/jituan/services/caiwu_stats.py
@@ -1,7 +1,7 @@
"""俱乐部维度财务统计(供 /jituan/houtai/caiwu,旧 /houtai/caiwu 不变)。"""
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.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_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__lt=today_end,
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_amount = float(today_orders.aggregate(total=Sum('Amount'))['total'] or 0.00)
+ today_order_count = _today_order_agg['today_order_count'] or 0
+ 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(
- CreateTime__gte=today_start, CreateTime__lt=today_end, Status=3,
- )
- 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(
+ # 合并充值统计:2 次独立 count/aggregate → 1 次 aggregate
+ _today_chongzhi_agg = cz_qs.filter(
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_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00)
+ today_chongzhi_count = _today_chongzhi_agg['count'] or 0
+ today_chongzhi_amount = float(_today_chongzhi_agg['total'] or 0.00)
new_huiyuan_user_ids = hy_qs.filter(
CreateTime__gte=today_start, CreateTime__lt=today_end,
diff --git a/jituan/services/club_context.py b/jituan/services/club_context.py
index 7b187bc..bb8d7c5 100644
--- a/jituan/services/club_context.py
+++ b/jituan/services/club_context.py
@@ -141,10 +141,13 @@ def count_order_status_buckets(q_conditions, request=None):
('5', Q(Status=5)),
('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:
- counts[key] = order_scope_qs(request).filter(q_conditions & status_q).count()
- return counts
+ _agg_kwargs[key] = Count('id', filter=status_q)
+ return _base.aggregate(**_agg_kwargs)
def paginate_fluent_query(fluent_qs, page, page_size):
diff --git a/jituan/services/club_member_admin.py b/jituan/services/club_member_admin.py
index 0bb1b51..32c8dbb 100644
--- a/jituan/services/club_member_admin.py
+++ b/jituan/services/club_member_admin.py
@@ -95,7 +95,12 @@ def count_club_member_sales(club_id, huiyuan_id, formal_only=True):
def build_member_list_payload(request):
scope = resolve_club_scope(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 = []
price_map = {}
@@ -105,6 +110,20 @@ def build_member_list_payload(request):
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:
for m in members:
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:
continue
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)
return {
@@ -138,17 +157,18 @@ def build_member_catalog_payload(request):
enabled_ids = set(
ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True).values_list('huiyuan_id', flat=True)
)
- all_members = Huiyuan.query.all().order_by('-CreateTime')
- catalog = []
- for m in all_members:
- if m.huiyuan_id in enabled_ids:
- continue
- catalog.append({
- 'huiyuan_id': m.huiyuan_id,
- 'jieshao': m.jieshao,
- 'jtjieshao': m.jtjieshao,
- 'is_bundle': bool(getattr(m, 'is_bundle', False)),
- })
+ # P1 优化:DB 端排除已上架会员,限制字段,避免全表加载 + Python 过滤
+ catalog_qs = Huiyuan.query.all()
+ if enabled_ids:
+ catalog_qs = catalog_qs.exclude(huiyuan_id__in=list(enabled_ids))
+ catalog = list(
+ catalog_qs.only('huiyuan_id', 'jieshao', 'jtjieshao', 'is_bundle')
+ .order_by('-CreateTime')
+ .values('huiyuan_id', 'jieshao', 'jtjieshao', 'is_bundle')
+ )
+ # values 返回 dict 列表,需补 is_bundle bool 化
+ for row in catalog:
+ row['is_bundle'] = bool(row.get('is_bundle'))
return {'club_id': club_id, 'catalog': catalog}
diff --git a/jituan/services/dashou_exam.py b/jituan/services/dashou_exam.py
index faffd5f..ed32871 100644
--- a/jituan/services/dashou_exam.py
+++ b/jituan/services/dashou_exam.py
@@ -218,14 +218,22 @@ def build_exam_bundle(request, user):
return None, '题库暂无已上架题目,请至少录入一道题并上架(每题至少两个选项且标记正确答案)'
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 = []
for qid in selected_ids:
- q = ClubDashouExamQuestion.query.get(id=qid)
- imgs = [
- r.image_url for r in ClubDashouExamQuestionImage.query.filter(
- club_id=club_id, question_id=qid,
- ).order_by('sort_order', 'id')
- ]
+ q = _questions_map.get(qid)
+ if q is None:
+ continue
+ imgs = _imgs_map.get(qid, [])
questions.append(build_question_bundle_item(q, imgs))
return {
@@ -284,8 +292,14 @@ def build_admin_config_payload(request, user, data=None):
if err:
return {**clubs_info, 'scope_error': err}
cfg = get_or_create_config(club_id)
- pool = ClubDashouExamQuestion.query.filter(club_id=club_id).count()
- enabled_pool = ClubDashouExamQuestion.query.filter(club_id=club_id, is_enabled=True).count()
+ # 合并 2 次独立 count → 1 次 aggregate with 条件 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 {
**clubs_info,
'club_id': club_id,
@@ -319,14 +333,20 @@ def list_admin_questions(request, user, data=None):
if err:
return {**exam_admin_clubs_payload(user), 'list': [], 'scope_error': err}
rows = ClubDashouExamQuestion.query.filter(club_id=club_id).order_by('-sort_order', 'id')
- out = []
- for q in rows:
- imgs = [
+ rows_list = list(rows)
+ _q_ids = [q.id for q in rows_list]
+ # 批量预取所有题目图片,避免循环内 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}
- 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))
return {**exam_admin_clubs_payload(user), 'club_id': club_id, 'list': out}
diff --git a/jituan/services/finance_detail_stats.py b/jituan/services/finance_detail_stats.py
index 470f059..dbfcbe4 100644
--- a/jituan/services/finance_detail_stats.py
+++ b/jituan/services/finance_detail_stats.py
@@ -203,19 +203,31 @@ def build_order_finance_payload(
})
summary_qs = filter_queryset_by_club(Order.query.filter(**filters), request, club_field='ClubID')
- total_order_count = summary_qs.count()
- total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0
- total_success_count = summary_qs.filter(Status=3).count()
- total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0
- total_refund_count = summary_qs.filter(Status=5).count()
- total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0
- total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate(
- s=Sum('pingtai_kuozhan__ShopIncome'),
- )['s'] or 0
- platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0
- merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0
- platform_dashou_sum = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0
- merchant_dashou_sum = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['s'] or 0
+ # 合并 13 次独立 count/aggregate → 1 次 aggregate
+ _summary_agg = summary_qs.aggregate(
+ total_order_count=Count('id'),
+ total_order_amount=Sum('Amount'),
+ total_success_count=Count('id', filter=Q(Status=3)),
+ total_success_amount=Sum('Amount', filter=Q(Status=3)),
+ total_refund_count=Count('id', filter=Q(Status=5)),
+ total_dashou=Sum('PlayerCommission', filter=Q(Status=3)),
+ total_dianpu_fenhong=Sum('pingtai_kuozhan__ShopIncome', filter=Q(Status=3, Platform=1)),
+ platform_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
+ merchant_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
+ platform_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
+ 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_merchant = merchant_amount - merchant_dashou_sum
total_profit = profit_platform + profit_merchant
@@ -392,10 +404,16 @@ def build_huiyuan_bankuai_payload(request):
'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 = []
- for bk in Bankuai.query.all():
+ for bk in _bankuai_qs:
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)
if m['huiyuan_id'] in price_map:
item['club_price'] = price_map[m['huiyuan_id']]
diff --git a/jituan/services/szjilu_accounting.py b/jituan/services/szjilu_accounting.py
index c3d4e1e..faba6a5 100644
--- a/jituan/services/szjilu_accounting.py
+++ b/jituan/services/szjilu_accounting.py
@@ -188,11 +188,11 @@ def zero_all_income_statistics():
except _PLATFORM_LOG_ERRORS:
pass
DailyIncomeStat.objects.all().delete()
- for szjilu in Szjilu.objects.select_for_update().all():
- szjilu.TotalIncome = _ZERO
- szjilu.TotalFlow = _ZERO
- szjilu.DailyFlow = _ZERO
- szjilu.save(update_fields=['TotalIncome', 'TotalFlow', 'DailyFlow', 'UpdateTime'])
+ from django.utils import timezone
+ Szjilu.objects.update(
+ TotalIncome=_ZERO, TotalFlow=_ZERO, DailyFlow=_ZERO,
+ UpdateTime=timezone.now(),
+ )
return True
@@ -689,13 +689,12 @@ def get_szjilu_snapshot(club_id=None):
def reset_all_daily_szjilu():
"""每日清零:所有俱乐部的今日流水/今日支出。"""
- updated = 0
with transaction.atomic():
- for szjilu in Szjilu.objects.select_for_update().all():
- szjilu.DailyExpense = _ZERO
- szjilu.DailyFlow = _ZERO
- szjilu.save(update_fields=['DailyExpense', 'DailyFlow', 'UpdateTime'])
- updated += 1
+ from django.utils import timezone
+ updated = Szjilu.objects.update(
+ DailyExpense=_ZERO, DailyFlow=_ZERO,
+ UpdateTime=timezone.now(),
+ )
return updated
diff --git a/jituan/views_shenhe_manage.py b/jituan/views_shenhe_manage.py
index c5fc6d8..f2da2b9 100644
--- a/jituan/views_shenhe_manage.py
+++ b/jituan/views_shenhe_manage.py
@@ -71,8 +71,13 @@ class ClubKhgglAddView(APIView):
if not user_belongs_to_request_club(user, request):
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:
- if not Bankuai.query.filter(bankuai_id=bid).exists():
+ if bid not in _existing_bids:
return Response({'code': 1, 'msg': f'板块不存在: {bid}'}, status=400)
created_new = False
diff --git a/merchant_ops/services/dispatch.py b/merchant_ops/services/dispatch.py
index 3fbf928..7c5dd0d 100644
--- a/merchant_ops/services/dispatch.py
+++ b/merchant_ops/services/dispatch.py
@@ -109,7 +109,7 @@ def staff_dispatch_order(member, staff_user, data):
zhiding_dashou = None
if zhiding_uid:
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
if zhiding_dashou.zhanghaozhuangtai != 1:
return False, {'code': 400, 'msg': '指定打手状态异常', 'data': None}
diff --git a/merchant_ops/services/link.py b/merchant_ops/services/link.py
index a997c1b..c314e1a 100644
--- a/merchant_ops/services/link.py
+++ b/merchant_ops/services/link.py
@@ -119,6 +119,7 @@ def staff_generate_link(member, staff_user, data):
final_huiyuan_id = shangpin_leixing.huiyuan_id or ''
label_name = ''
+ ch = None
if chenghao_id:
ch = Chenghao.query.filter(id=chenghao_id).first()
if ch:
@@ -155,10 +156,9 @@ def staff_generate_link(member, staff_user, data):
MerchantNickname=shangjia.nicheng or f"商家{merchant_id}",
)
- if chenghao_id:
- ch = Chenghao.query.filter(id=chenghao_id).first()
- if ch:
- DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=ch)
+ # 复用已查询的 ch 对象,避免重复查询
+ if ch:
+ DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=ch)
UserShangjia.query.filter(id=shangjia.id).update(
fabu=F('fabu') + 1,
diff --git a/merchant_ops/views_manage.py b/merchant_ops/views_manage.py
index 8e3c244..7eef8ba 100644
--- a/merchant_ops/views_manage.py
+++ b/merchant_ops/views_manage.py
@@ -128,8 +128,16 @@ class StaffInviteListView(APIView):
qs = MerchantStaffInvite.query.filter(merchant_id=request.user.UserUID).select_related('role')
if status_filter is not None:
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 = []
- for inv in qs.order_by('-CreateTime')[:200]:
+ for inv in invites:
item = {
'id': inv.id,
'code': inv.invite_code,
@@ -140,8 +148,7 @@ class StaffInviteListView(APIView):
'user': None,
}
if inv.used_by:
- from users.business_models import User
- u = User.query.filter(UserUID=inv.used_by).first()
+ u = _user_map.get(inv.used_by)
item['user'] = {'nickname': getattr(u, 'Nickname', '') or inv.used_by, 'uid': inv.used_by}
items.append(item)
return Response({'code': 0, 'msg': '成功', 'data': {'list': items, 'gudingCode': 'XQKF2026'}})
@@ -181,21 +188,51 @@ class StaffMemberListView(APIView):
merchant_id=mid,
status__in=[MEMBER_STATUS_ACTIVE, MEMBER_STATUS_DISABLED],
).select_related('role')
- result = []
- for m in members:
- total_stats = MerchantStaffDailyStats.query.filter(member_id=m.id).aggregate(
+ members_list = list(members)
+ _member_ids = [m.id for m in members_list]
+ _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_amount=Sum('dispatch_amount'),
settle_count=Sum('settle_count'),
settle_amount=Sum('settle_amount'),
refund_count=Sum('refund_count'),
refund_amount=Sum('refund_amount'),
- )
- today_stat = MerchantStaffDailyStats.query.filter(
- member_id=m.id, stat_date=today,
- ).first()
- wallet = get_or_create_wallet(m)
- u = User.query.filter(UserUID=m.staff_user_id).first()
+ ):
+ _total_stats_map[item['member_id']] = item
+ for s in MerchantStaffDailyStats.query.filter(
+ member_id__in=_member_ids, stat_date=today,
+ ):
+ _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({
'id': m.id,
'uid': m.staff_user_id,
@@ -210,13 +247,13 @@ class StaffMemberListView(APIView):
'isDisabled': m.status == MEMBER_STATUS_DISABLED,
'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')),
- 'dispatchCount': total_stats['dispatch_count'] or 0,
- 'dispatchAmount': str(total_stats['dispatch_amount'] or '0.00'),
- 'orderCount': total_stats['dispatch_count'] or 0,
- 'settleCount': total_stats['settle_count'] or 0,
- 'settleAmount': str(total_stats['settle_amount'] or '0.00'),
- 'refundCount': total_stats['refund_count'] or 0,
- 'refundAmount': str(total_stats['refund_amount'] or '0.00'),
+ 'dispatchCount': total_stats.get('dispatch_count') or 0,
+ 'dispatchAmount': str(total_stats.get('dispatch_amount') or '0.00'),
+ 'orderCount': total_stats.get('dispatch_count') or 0,
+ 'settleCount': total_stats.get('settle_count') or 0,
+ 'settleAmount': str(total_stats.get('settle_amount') or '0.00'),
+ 'refundCount': total_stats.get('refund_count') or 0,
+ 'refundAmount': str(total_stats.get('refund_amount') or '0.00'),
'quota_available': str(wallet.quota_available),
'quota_total': str(wallet.quota_total),
'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'
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 = []
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({
'rank': idx,
'member_id': row['member_id'],
diff --git a/orders/views/admin.py b/orders/views/admin.py
index 5663a9d..aff094d 100644
--- a/orders/views/admin.py
+++ b/orders/views/admin.py
@@ -364,7 +364,7 @@ class AdGengHuanDaShou(APIView):
# 8. 验证新打手ID是否存在,并且是打手身份
try:
- new_dashou_user = User.query.get(
+ new_dashou_user = User.query.select_related('DashouProfile').get(
UserUID=new_dashou_id,
#user_type='PlayerID'
)
@@ -459,7 +459,7 @@ class AdGengHuanDaShou(APIView):
if old_dashou_id:
try:
# 查询原打手主表
- old_dashou_user = User.query.get(
+ old_dashou_user = User.query.select_related('DashouProfile').get(
UserUID=old_dashou_id,
#user_type='PlayerID'
)
@@ -567,7 +567,7 @@ class AdQiangZhiJieDan(APIView):
# 6. 查询订单主表
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:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
@@ -603,7 +603,7 @@ class AdQiangZhiJieDan(APIView):
if jiedan_dashou_id:
try:
# 查询打手用户主表
- dashou_user = User.query.get(
+ dashou_user = User.query.select_related('DashouProfile').get(
UserUID=jiedan_dashou_id,
#user_type='PlayerID'
)
@@ -646,7 +646,7 @@ class AdQiangZhiJieDan(APIView):
if shangjia_id:
# 查询商家用户主表
- shangjia_user = User.query.get(
+ shangjia_user = User.query.select_related('ShopProfile').get(
UserUID=shangjia_id,
#user_type='shop'
)
@@ -750,7 +750,7 @@ class AdJuJueTuiKuan(APIView):
# 6. 查询订单主表
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:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
@@ -779,7 +779,7 @@ class AdJuJueTuiKuan(APIView):
if jiedan_dashou_id:
try:
# 查询打手用户主表
- dashou_user = User.query.get(
+ dashou_user = User.query.select_related('DashouProfile').get(
UserUID=jiedan_dashou_id,
#user_type='PlayerID'
)
@@ -822,7 +822,7 @@ class AdJuJueTuiKuan(APIView):
if shangjia_id:
# 查询商家用户主表
- shangjia_user = User.query.get(
+ shangjia_user = User.query.select_related('ShopProfile').get(
UserUID=shangjia_id,
#user_type='shop'
)
@@ -958,7 +958,7 @@ class AdTongYiTuiKuanShangJia(APIView):
# 7. 查询订单主表
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:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
@@ -1010,7 +1010,7 @@ class AdTongYiTuiKuanShangJia(APIView):
if jiedan_dashou_id:
try:
# 查询打手用户主表
- dashou_user = User.query.get(
+ dashou_user = User.query.select_related('DashouProfile').get(
UserUID=jiedan_dashou_id,
#user_type='PlayerID'
)
@@ -1035,10 +1035,10 @@ class AdTongYiTuiKuanShangJia(APIView):
if shangjia_id:
try:
# 查询商家用户主表
- shangjia_user = User.query.get(
- UserUID=shangjia_id,
- #user_type='shop'
- )
+ shangjia_user = User.query.select_related('ShopProfile').get(
+ UserUID=shangjia_id,
+ #user_type='shop'
+ )
# 获取商家扩展表
shangjia_profile = shangjia_user.ShopProfile
@@ -1228,7 +1228,7 @@ class AdTongYiTuiKuanPingTai(APIView):
# 14. 更新打手扩展表
if jiedan_dashou_id:
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.zhuangtai = 1 # 打手状态设为空闲
dashou_profile.tuikuanliang += 1
@@ -1239,7 +1239,7 @@ class AdTongYiTuiKuanPingTai(APIView):
# 15. 更新老板扩展表
if laoban_id:
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.alltui += 1
laoban_profile.zonge -= jine
@@ -1463,7 +1463,7 @@ class AdJuJueJieSuan(APIView):
# 8. 更新打手扩展表
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.zhuangtai = 1 # 打手状态设为正常
dashou_profile.tuikuanliang += 1 # 退款订单总量+1
@@ -1545,7 +1545,7 @@ class AdZhuanYiDaTing(APIView):
# 如果有接单打手,更新其扩展表
if jiedan_dashou_id:
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.zhuangtai = 1
dashou_profile.tuikuanliang += 1
@@ -1835,7 +1835,7 @@ class ZxsjghdsView(APIView):
# 6.2 释放打手
if dashou_id:
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.tuikuanliang = (dashou_profile.tuikuanliang or 0) + 1
dashou_profile.zhuangtai = 1 # 空闲
diff --git a/orders/views/boss_orders.py b/orders/views/boss_orders.py
index 88155ba..dd28d95 100644
--- a/orders/views/boss_orders.py
+++ b/orders/views/boss_orders.py
@@ -422,11 +422,14 @@ class JiedanView(APIView):
# 7. 更新打手扩展表(如果有打手ID和分成)
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:
# 查询打手扩展表
- dashou_kuozhan = UserDashou.query.filter(user=dashou_user).first()
+ try:
+ dashou_kuozhan = dashou_user.DashouProfile
+ except UserDashou.DoesNotExist:
+ dashou_kuozhan = None
if dashou_kuozhan:
# 更新四个金额字段
@@ -585,7 +588,7 @@ class DingdanXiangqingView2(APIView):
jiedan_time = ''
tijiao_time = ''
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:
dashou_touxiang = dashou_user.Avatar or ''
try:
@@ -734,7 +737,7 @@ class JiedanView2(APIView):
# 9. 更新打手收益与状态
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:
try:
dashou_ext = dashou_user.DashouProfile
diff --git a/orders/views/chat.py b/orders/views/chat.py
index 9cf7a3c..4bb1b04 100644
--- a/orders/views/chat.py
+++ b/orders/views/chat.py
@@ -193,13 +193,13 @@ def send_group_message(
def check_user_permission(identity_type, uid, order):
"""验证用户身份、账号状态、以及是否属于该订单"""
try:
- user = User.query.get(UserUID=uid)
+ user = User.query.select_related('DashouProfile').get(UserUID=uid)
except User.DoesNotExist:
return False, '用户不存在'
if identity_type == 'PlayerID':
try:
- dashou = UserDashou.query.get(user=user)
+ dashou = user.DashouProfile
if dashou.zhanghaozhuangtai != 1 :
return False, '打手账号已被封禁或状态异常'
except UserDashou.DoesNotExist:
@@ -516,14 +516,16 @@ class LianTongDingDanZhuangTaiPiLiangView(APIView):
return Response({'code': 400, 'msg': 'order_ids 格式错误'})
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 = {}
- for oid in order_ids[:20]:
- oid = str(oid).strip()
- if not oid:
- continue
- try:
- order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=oid)
- except Order.DoesNotExist:
+ for oid in oids:
+ order = orders_map.get(oid)
+ if not order:
continue
for identity in ('PlayerID', 'shangjia', 'boss'):
valid, _ = check_user_permission(identity, uid, order)
diff --git a/orders/views/merchant.py b/orders/views/merchant.py
index 17df04d..5d96775 100644
--- a/orders/views/merchant.py
+++ b/orders/views/merchant.py
@@ -521,11 +521,13 @@ class ShangjiaShujuTongjiView(APIView):
penalty_stats = None
try:
from orders.models import Penalty, PenaltyRecord
- from django.db.models import Q, Sum
- fakuan_pending = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).count()
- fakuan_pending_amt = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).aggregate(
- amt=Sum('FineAmount')
- )['amt']
+ from django.db.models import Q, Sum, Count
+ _fakuan_agg = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).aggregate(
+ count=Count('id'),
+ amt=Sum('FineAmount'),
+ )
+ fakuan_pending = _fakuan_agg['count']
+ fakuan_pending_amt = _fakuan_agg['amt']
jifen_pending = PenaltyRecord.query.filter(
ApplicantID=yonghuid,
).filter(Q(ApplyStatus=0) | Q(ApplyStatus=3)).count()
@@ -1204,7 +1206,7 @@ class ShangjiaTuikuanShenqingView(APIView):
try:
dashou_main = User.query.filter(
UserUID=dingdan.PlayerID
- ).first()
+ ).select_related('DashouProfile').first()
if dashou_main and hasattr(dashou_main, 'DashouProfile'):
dashou_profile = dashou_main.DashouProfile
dashou_profile.zhuangtai = 1
@@ -1285,7 +1287,7 @@ class ShangjiaChufaShenqingView(APIView):
with transaction.atomic():
# 验证打手身份
try:
- dashou_user = User.query.get(UserUID=dashou_id)
+ dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
dashou = dashou_user.DashouProfile
except User.DoesNotExist:
return Response({
@@ -1367,13 +1369,16 @@ class ShangjiaChufaShenqingView(APIView):
# 🔴【新增】保存处罚图片到数据库
if chufa_tupian_urls and isinstance(chufa_tupian_urls, list):
- for tupian_url in chufa_tupian_urls:
- if tupian_url and tupian_url.strip():
- PenaltyEvidenceImage.query.create(
+ _valid_urls = [u.strip() for u in chufa_tupian_urls if u and u.strip()]
+ if _valid_urls:
+ PenaltyEvidenceImage.objects.bulk_create([
+ PenaltyEvidenceImage(
OrderID=dingdan_id,
UserID=current_user.UserUID,
- ImageURL=tupian_url
+ ImageURL=url,
)
+ for url in _valid_urls
+ ])
# 更新商家订单扩展表的处罚相关字段
shangjia_kuozhan.PenaltyReason = chufa_liyou
diff --git a/orders/views/merchant_penalty.py b/orders/views/merchant_penalty.py
index d71dff4..eb65aea 100644
--- a/orders/views/merchant_penalty.py
+++ b/orders/views/merchant_penalty.py
@@ -188,13 +188,15 @@ class ShangjiaFakuanApplyView(APIView):
),
ShopStaffUserID=staff_kefu_id,
)
- for url in evidence_urls:
- PenaltyAppealImage.query.create(
+ PenaltyAppealImage.objects.bulk_create([
+ PenaltyAppealImage(
Penalty=fadan,
PenalizedUserID=dashou_id,
ImageURL=url,
Purpose=1,
)
+ for url in evidence_urls
+ ])
lock_penalty_bonus(fadan)
logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}提交罚款申请(平台审核),金额{fakuanjine}")
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()
if existing_count + len(add_urls) > 9:
return Response({'code': 400, 'msg': '证据图片最多9张'})
- for url in add_urls:
- url = str(url).strip()
- if not url:
- continue
- PenaltyAppealImage.query.create(
- Penalty=fadan,
- PenalizedUserID=fadan.PenalizedUserID,
- ImageURL=url,
- Purpose=1,
- )
+ _valid_add_urls = [str(u).strip() for u in add_urls if str(u).strip()]
+ if _valid_add_urls:
+ PenaltyAppealImage.objects.bulk_create([
+ PenaltyAppealImage(
+ Penalty=fadan,
+ PenalizedUserID=fadan.PenalizedUserID,
+ ImageURL=url,
+ Purpose=1,
+ )
+ for url in _valid_add_urls
+ ])
logger.info(
f"罚单{fadan.id}证据图变更:新增{len(add_urls)}张,删除{len(remove_ids)}张"
)
diff --git a/orders/views/payment.py b/orders/views/payment.py
index 3e4af80..3469ed3 100644
--- a/orders/views/payment.py
+++ b/orders/views/payment.py
@@ -204,7 +204,9 @@ class WechatPayNotifyView(APIView):
# 锁定订单防止并发
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:
logger.error(f"查询订单异常: {e}")
raise
@@ -263,7 +265,7 @@ class WechatPayNotifyView(APIView):
if hasattr(dingdan, 'pingtai_kuozhan'):
laoban_id = dingdan.pingtai_kuozhan.BossID
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'):
boss = user_main.BossProfile
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
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'):
boss = user_main.BossProfile
boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0)
@@ -709,13 +711,16 @@ class CreateOrderView(APIView):
zhiding_user = User.query.filter(
UserUID=zhiding,
#user_type='PlayerID'
- ).first()
+ ).select_related('DashouProfile').first()
if not zhiding_user:
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:
return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None})
diff --git a/products/views/product_query.py b/products/views/product_query.py
index 6dc17e3..6d72083 100644
--- a/products/views/product_query.py
+++ b/products/views/product_query.py
@@ -268,35 +268,98 @@ class DashouHuiyuanList(APIView):
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 仅返回本俱乐部可售会员(club_huiyuan_price 已配置且启用)
- from jituan.services.member_recharge import club_huiyuan_sellable, can_purchase_trial
-
- 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.constants import CLUB_ID_DEFAULT
from jituan.models import ClubHuiyuanPrice
- club_rows = {
- r.huiyuan_id: r
- for r in ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True)
- }
+ from decimal import Decimal as _Decimal
+
+ 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 = []
- yonghuid = request.user.UserUID
for huiyuan in huiyuan_queryset:
- sellable, club_price, formal_days = club_huiyuan_sellable(
- club_id, huiyuan.huiyuan_id, is_trial=False,
- )
+ cp_row = _club_price_map.get(huiyuan.huiyuan_id)
+ sellable, club_price, formal_days = _local_sellable(cp_row, is_trial=False)
if not sellable or not club_price:
continue
- trial_ok, _ = can_purchase_trial(yonghuid, huiyuan.huiyuan_id, club_id)
- trial_sellable, trial_price, trial_days = club_huiyuan_sellable(
- club_id, huiyuan.huiyuan_id, is_trial=True,
- )
- club_row = club_rows.get(huiyuan.huiyuan_id)
- card_image = (club_row.card_image or '').strip() if club_row else ''
- card_bg = (club_row.card_bg or '').strip() if club_row else ''
+ trial_ok = _local_can_trial(huiyuan.huiyuan_id)
+ trial_sellable, trial_price, trial_days = _local_sellable(cp_row, is_trial=True)
+ # 远程新增:card_image / card_bg / is_bundle 字段
+ card_image = (cp_row.card_image or '').strip() if cp_row else ''
+ card_bg = (cp_row.card_bg or '').strip() if cp_row else ''
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({
'id': huiyuan.huiyuan_id,
'mingzi': huiyuan.jieshao,
@@ -304,10 +367,10 @@ class DashouHuiyuanList(APIView):
'formal_days': formal_days,
'jieshao': huiyuan.jtjieshao,
'is_bundle': is_bundle,
- 'trial_enabled': False if is_bundle else bool(trial_sellable),
- 'trial_price': '0.00' if is_bundle else (format_yuan(trial_price) if trial_sellable else '0.00'),
- 'trial_days': 0 if is_bundle else (trial_days if trial_sellable else 0),
- 'can_buy_trial': False if is_bundle else bool(trial_ok and trial_sellable),
+ 'trial_enabled': bool(trial_sellable),
+ 'trial_price': format_yuan(trial_price) if trial_sellable else '0.00',
+ 'trial_days': trial_days if trial_sellable else 0,
+ 'can_buy_trial': bool(trial_ok and trial_sellable),
'card_image': card_image,
'card_bg': card_bg,
})
@@ -475,20 +538,21 @@ class AdGetAllDataView(APIView):
# 6. 查询商品类型数据
try:
- # 使用select_related或prefetch_related优化查询(如果需要关联查询)
- # 这里直接查询所有字段,然后提取需要的字段
- shangpinleixing_queryset = ShangpinLeixing.query.all()
+ # 优化:使用 .values() 直接提取字段,避免模型实例化开销(字典表 <10 条)
+ shangpinleixing_rows = ShangpinLeixing.query.all().values(
+ 'id', 'yaoqiuleixing', 'huiyuan_id', 'yongjin', 'jieshao', 'tupian_url'
+ )
# 转换为前端需要的格式
shangpinleixing_list = []
- for leixing in shangpinleixing_queryset:
+ for row in shangpinleixing_rows:
leixing_data = {
- 'id': leixing.id,
- 'yaoqiuleixing': leixing.yaoqiuleixing if leixing.yaoqiuleixing is not None else None,
- 'huiyuan_id': leixing.huiyuan_id if leixing.huiyuan_id else None,
- 'yongjin': str(leixing.yongjin) if leixing.yongjin is not None else None,
- 'jieshao': leixing.jieshao if leixing.jieshao else '',
- 'tupian_url': leixing.tupian_url if leixing.tupian_url else ''
+ 'id': row['id'],
+ 'yaoqiuleixing': row['yaoqiuleixing'],
+ 'huiyuan_id': row['huiyuan_id'] or None,
+ 'yongjin': str(row['yongjin']) if row['yongjin'] is not None else None,
+ 'jieshao': row['jieshao'] or '',
+ 'tupian_url': row['tupian_url'] or ''
}
shangpinleixing_list.append(leixing_data)
@@ -500,16 +564,16 @@ class AdGetAllDataView(APIView):
# 7. 查询商品专区数据
try:
- # 查询所有商品专区
- zhuanqu_queryset = ShangpinZhuanqu.query.all()
+ # 优化:使用 .values() 直接提取字段(字典表 <50 条)
+ zhuanqu_rows = ShangpinZhuanqu.query.all().values('id', 'mingzi', 'leixing_id')
# 转换为前端需要的格式
zhuanqu_list = []
- for zhuanqu in zhuanqu_queryset:
+ for row in zhuanqu_rows:
zhuanqu_data = {
- 'id': zhuanqu.id,
- 'mingzi': zhuanqu.mingzi if zhuanqu.mingzi else '',
- 'leixing_id': zhuanqu.leixing_id
+ 'id': row['id'],
+ 'mingzi': row['mingzi'] or '',
+ 'leixing_id': row['leixing_id']
}
zhuanqu_list.append(zhuanqu_data)
@@ -593,17 +657,20 @@ class HuiyuanTypeListView(APIView):
}, status=status.HTTP_401_UNAUTHORIZED)
# 🔥 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. 构建返回数据
huiyuan_data = []
- for huiyuan in huiyuan_list:
+ for row in huiyuan_rows:
huiyuan_data.append({
- 'id': huiyuan.huiyuan_id, # 会员ID
- 'jieshao': huiyuan.jieshao, # 会员介绍
- 'jiage': str(huiyuan.jiage), # 价格
- 'goumai_cishu': huiyuan.goumai_cishu, # 购买次数
- 'CreateTime': huiyuan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.CreateTime else None
+ 'id': row['huiyuan_id'], # 会员ID
+ 'jieshao': row['jieshao'], # 会员介绍
+ 'jiage': str(row['jiage']), # 价格
+ 'goumai_cishu': row['goumai_cishu'], # 购买次数
+ 'CreateTime': row['CreateTime'].strftime('%Y-%m-%d %H:%M:%S') if row['CreateTime'] else None
})
return Response({
@@ -757,34 +824,40 @@ class HuiyuanRecordListView(APIView):
has_more = (page * pagesize) < total_count
# 🔥 9. 获取会员类型统计信息(所有会员类型的当前状态总数)
- huiyuan_types = Huiyuan.query.all()
- huiyuan_stats = []
-
- for huiyuan in huiyuan_types:
- # 使用COUNT查询统计
- stats_sql = """
- SELECT COUNT(*)
- FROM huiyuangoumai
- WHERE huiyuan_id = %s
- """
- stats_params = [huiyuan.huiyuan_id]
-
- if jiluzhuangtai == 1: # 未过期
- stats_sql += " AND daoqi_time > %s"
- stats_params.append(now)
- else: # 已过期
- stats_sql += " AND daoqi_time <= %s"
- stats_params.append(now)
-
- with connection.cursor() as cursor:
- cursor.execute(stats_sql, stats_params)
- count = cursor.fetchone()[0]
-
- huiyuan_stats.append({
- 'id': huiyuan.huiyuan_id,
- 'jieshao': huiyuan.jieshao,
- 'count': count
- })
+ # 优化:原实现为 N+1 查询(外层遍历 Huiyuan 全表 + 内层每条记录执行一次 COUNT SQL)
+ # 改为单条 LEFT JOIN + GROUP BY 聚合,将 N+1 次查询降为 1 次
+ # 注意:Huiyuangoumai.huiyuan_id 是 CharField 而非 ForeignKey,无法用 ORM annotate
+ stats_sql = """
+ SELECT
+ h.huiyuan_id,
+ h.jieshao,
+ COUNT(hg.id) AS count
+ FROM huiyuan h
+ LEFT JOIN huiyuangoumai hg
+ ON hg.huiyuan_id = h.huiyuan_id
+ """
+ stats_params = []
+ if jiluzhuangtai == 1: # 未过期
+ # 条件放在 ON 子句而非 WHERE,确保无购买记录的会员类型仍返回 count=0
+ stats_sql += " AND hg.daoqi_time > %s"
+ stats_params.append(now)
+ else: # 已过期
+ stats_sql += " AND hg.daoqi_time <= %s"
+ stats_params.append(now)
+ stats_sql += " GROUP BY h.huiyuan_id, h.jieshao"
+
+ with connection.cursor() as cursor:
+ cursor.execute(stats_sql, stats_params)
+ stats_rows = cursor.fetchall()
+
+ huiyuan_stats = [
+ {
+ 'id': row[0],
+ 'jieshao': row[1],
+ 'count': row[2]
+ }
+ for row in stats_rows
+ ]
return Response({
'code': 0,
diff --git a/rank/reward_houtai_views.py b/rank/reward_houtai_views.py
index cd2dec4..cb77bf1 100644
--- a/rank/reward_houtai_views.py
+++ b/rank/reward_houtai_views.py
@@ -35,8 +35,9 @@ def _has_perm(permissions):
return any(p in permissions for p in REWARD_PERMS)
-def _serialize_scheme(scheme):
- tiers = RankRewardTier.query.filter(scheme_id=scheme.id).order_by('rank_from')
+def _serialize_scheme(scheme, tiers=None):
+ if tiers is None:
+ tiers = RankRewardTier.query.filter(scheme_id=scheme.id).order_by('rank_from')
return {
'id': scheme.id,
'club_id': scheme.club_id,
@@ -78,11 +79,18 @@ class RankRewardHoutaiListView(APIView):
club_id = resolve_club_id_from_request(request)
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({
'code': 0,
'data': {
'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()),
'period_options': [
{'key': RankRewardScheme.PERIOD_DAY, 'label': '日榜(昨日)'},
@@ -148,14 +156,16 @@ class RankRewardHoutaiSaveView(APIView):
)
RankRewardTier.query.filter(scheme_id=scheme.id).delete()
- for t in tiers:
- RankRewardTier.query.create(
+ RankRewardTier.objects.bulk_create([
+ RankRewardTier(
scheme_id=scheme.id,
rank_from=int(t['rank_from']),
rank_to=int(t['rank_to']),
reward_amount=t['reward_amount'],
label=(t.get('label') or '').strip(),
)
+ for t in tiers
+ ])
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_scheme(scheme)})
diff --git a/rank/reward_services.py b/rank/reward_services.py
index cbf77dc..f026644 100644
--- a/rank/reward_services.py
+++ b/rank/reward_services.py
@@ -574,11 +574,36 @@ def maybe_settle_club_rewards(club_id: str, shenfen: str, riqi: str) -> Optional
return None
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)
balance_field = BALANCE_FIELD_MAP[shenfen]
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):
mingci = idx + 1
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']
if club_map.get(uid) != club_id:
continue
- if not user_has_active_shenfen(uid, shenfen):
+ if uid not in _active_uids:
continue
idem = f'{club_id}|{scheme.id}|{period_key}|{uid}'
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,
claim_count=0,
)
- created = 0
- for claim in claims_to_create:
- claim.settlement_id = settlement.id
- try:
- claim.save()
- created += 1
- except IntegrityError:
- pass
+ # 批量创建 claims,用 ignore_conflicts 处理幂等冲突
+ if claims_to_create:
+ for claim in claims_to_create:
+ claim.settlement_id = settlement.id
+ RankRewardClaim.objects.bulk_create(claims_to_create, ignore_conflicts=True)
+ created = RankRewardClaim.objects.filter(settlement_id=settlement.id).count()
+ else:
+ created = 0
settlement.claim_count = created
settlement.save(update_fields=['claim_count'])
return settlement
diff --git a/rank/views/biaoqian.py b/rank/views/biaoqian.py
index dde3b3b..50f2acd 100644
--- a/rank/views/biaoqian.py
+++ b/rank/views/biaoqian.py
@@ -3,7 +3,7 @@ import json
import logging
from decimal import Decimal
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.response import Response
from rest_framework.permissions import IsAuthenticated
@@ -118,15 +118,26 @@ class KaoheJinpaiTagsView(APIView):
.values_list('chenghao__mingcheng', flat=True)
)
- # 板块列表
+ # 板块列表(prefetch 一次性加载标签和费用,避免三层嵌套 N+1)
plate_list = []
- for bk in Bankuai.objects.all():
- tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou')
+ all_bankuai = Bankuai.objects.prefetch_related(
+ 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 = []
- for tag in tags:
- # 费用列表
- fee_objs = KaoheCishuFeiyong.objects.filter(chenghao=tag).order_by('cishu')
- fee_list = [{'cishu': f.cishu, 'feiyong': float(f.feiyong)} for f in fee_objs]
+ for tag in getattr(bk, 'dashou_tags', []):
+ # 费用列表(已预加载,保持 cishu 升序)
+ fee_list = [{'cishu': f.cishu, 'feiyong': float(f.feiyong)}
+ for f in tag.kaohecishufeiyong_set.all()]
# 若没有记录,补充默认费用
if not fee_list:
@@ -151,12 +162,13 @@ class KaoheJinpaiTagsView(APIView):
# 新增:返回所有在职考核官信息(用于指定考核官)
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
avatar = kg.user.Avatar or ''
- # 获取考核官的打手昵称(从打手扩展表)
+ # 获取考核官的打手昵称(从打手扩展表,已预加载)
dashou_nick = ''
- dashou_prof = UserDashou.objects.filter(user=kg.user).first()
+ dashou_prof = getattr(kg.user, 'DashouProfile', None)
if dashou_prof:
dashou_nick = dashou_prof.nicheng or ''
kaoheguan_list.append({
diff --git a/rank/views/kaohe_jilu.py b/rank/views/kaohe_jilu.py
index aa4294b..69c587b 100644
--- a/rank/views/kaohe_jilu.py
+++ b/rank/views/kaohe_jilu.py
@@ -48,20 +48,42 @@ class KaoheJiluView(APIView):
total = query.count()
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 = []
- for jl in records:
- # 汇总缴纳总额
- total_fee = KaoheJiluFeiyong.objects.filter(jilu=jl).aggregate(
- total=models.Sum('jine')
- )['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 ''
+ for jl in records_list:
+ total_fee = _fee_map.get(jl.jilu_id, 0.00)
+ fail_reason = _fail_map.get(jl.jilu_id, '')
+ kg_avatar = _avatar_map.get(jl.shenheguan_id, '')
data.append({
'jilu_id': jl.jilu_id,
diff --git a/rank/views/shenhe.py b/rank/views/shenhe.py
index 3018704..6b0c636 100644
--- a/rank/views/shenhe.py
+++ b/rank/views/shenhe.py
@@ -3,7 +3,7 @@ import json
import logging
from decimal import Decimal
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.response import Response
from rest_framework.permissions import IsAuthenticated
@@ -33,12 +33,17 @@ class ShenheDatingModuleView(APIView):
.values_list('bankuai_id', flat=True))
bankuai_list = []
- for bk in Bankuai.objects.all():
- count = ShenheJilu.objects.filter(bankuai=bk, zhuangtai=3, shenheguan_id__isnull=True).count()
+ all_bankuai = Bankuai.objects.annotate(
+ pending_count=Count(
+ 'shenhejilu',
+ filter=Q(shenhejilu__zhuangtai=3, shenhejilu__shenheguan_id__isnull=True)
+ )
+ )
+ for bk in all_bankuai:
bankuai_list.append({
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
- 'count': count,
+ 'count': bk.pending_count,
'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')[
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 = []
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 ''
texiao_json = {}
if jl.chenghao and jl.chenghao.texiao_miaoshu:
@@ -202,9 +211,27 @@ class ShenheXiangqingView(APIView):
offset = (page - 1) * 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 = []
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 ''
texiao_json = {}
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
- # 本次待收考核费(基于记录中的当前次数,从费用明细表查询)
- daishou_fei = 0.00
- fee_detail = KaoheJiluFeiyong.objects.filter(
- jilu=jl,
- 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
+ # 本次待收考核费(从预取 dict 查询)
+ daishou_fei = _fee_detail_map.get((jl.jilu_id, jl.cishu), 0.00)
+ # 上次失败原因(从预取 dict 查询)
+ last_fail_reason = _fail_map.get((jl.jilu_id, jl.cishu - 1), '')
records.append({
'jilu_id': jl.jilu_id,
diff --git a/shop/views/shop_order_views.py b/shop/views/shop_order_views.py
index 2de0a08..a151196 100644
--- a/shop/views/shop_order_views.py
+++ b/shop/views/shop_order_views.py
@@ -15,7 +15,6 @@ import threading
import xmltodict
from decimal import Decimal
from collections import defaultdict
-from concurrent.futures import ThreadPoolExecutor, as_completed
from django.conf import settings
from django.db import models, transaction, IntegrityError
@@ -260,38 +259,66 @@ def _fetch_images(order, oss_domain):
# ================================================================
-# 辅助函数:并发获取多个订单的打手图片
+# 辅助函数:批量获取多个订单的打手图片(单次 DB 查询)
# ================================================================
def _batch_images(orders, oss_domain):
"""
- 并发调用 _fetch_images,提升批量获取图片的速度。
+ 批量获取订单的打手提交图片,使用单次 DB 查询替代循环/线程池 N 次查询。
参数:
orders: Dingdan 订单对象列表
oss_domain: OSS 域名
返回:
- 字典,键为订单ID (dingdan_id),值为 (images_list, is_punished, punishment_info)
+ 字典,键为订单ID (OrderID),值为 (images_list, is_punished, punishment_info)
"""
- result = {} # 存储结果
- # 使用线程池并发执行,max_workers 可根据服务器性能调整
- with ThreadPoolExecutor(max_workers=10) as executor:
- # 建立 future 到订单ID的映射
- future_to_oid = {
- executor.submit(_fetch_images, order, oss_domain): order.OrderID
- for order in orders
- }
- # 等待所有任务完成
- for future in as_completed(future_to_oid):
- oid = future_to_oid[future] # 对应的订单ID
- try:
- # 获取任务返回值
- imgs, pun, pinfo = future.result()
- result[oid] = (imgs, pun, pinfo)
- except Exception as e:
- # 某个订单获取失败,记录日志并赋予空值,保证整体流程不中断
- logger.error(f"获取订单 {oid} 图片失败: {e}")
- result[oid] = ([], False, {})
+ result = {}
+ # 先为所有订单填充默认空值,保证调用方可拿到键
+ for order in orders:
+ result[order.OrderID] = ([], False, {})
+
+ # 收集所有 (OrderID, PlayerID) 组合用于批量查询
+ _order_player_pairs = [
+ (order.OrderID, order.PlayerID)
+ for order in orders
+ if order.PlayerID
+ ]
+ if not _order_player_pairs:
+ return result
+
+ # 单次批量查询所有相关图片记录(避免循环 N 次或线程池掩盖 N 次查询)
+ # PlayerDeliveryImage 表的主键是 OrderID + PlayerID,使用元组列表过滤
+ _order_ids = [pair[0] for pair in _order_player_pairs]
+ _pair_set = set(_order_player_pairs)
+
+ 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
diff --git a/shop/views/shop_product_views.py b/shop/views/shop_product_views.py
index 40c497d..96d84ab 100644
--- a/shop/views/shop_product_views.py
+++ b/shop/views/shop_product_views.py
@@ -771,10 +771,12 @@ class ShopProductModifyView1(APIView):
else:
# 自动从公共类型获取会员ID,或转为佣金
try:
- shop_type = ShangpinLeixingDianpu.query.get(
+ shop_type = ShangpinLeixingDianpu.query.select_related('gonggong_leixing').get(
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
if auto_huiyuan_id:
product.huiyuan_id = auto_huiyuan_id
@@ -893,13 +895,12 @@ class ShopProductModifyView1(APIView):
return Response({'code': 400, 'msg': '必填项缺失'})
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:
return Response({'code': 400, 'msg': '无效的店铺商品类型'})
- try:
- public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id, shenhezhuangtai=1)
- except ShangpinLeixing.DoesNotExist:
+ public_type = shop_type.gonggong_leixing
+ if not public_type or public_type.shenhezhuangtai != 1:
return Response({'code': 400, 'msg': '关联的公共商品类型无效'})
# 商品图片上传
diff --git a/shop/views/shop_refund_views.py b/shop/views/shop_refund_views.py
index 0786cba..2ceb5ce 100644
--- a/shop/views/shop_refund_views.py
+++ b/shop/views/shop_refund_views.py
@@ -125,7 +125,7 @@ class ShopRefundView(APIView):
# 更新打手统计(退款量+1,状态置为空闲)
if dashou_id:
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.tuikuanliang += 1
dashou_profile.zhuangtai = 1
@@ -368,7 +368,7 @@ class ShopForceCompleteView(APIView):
# ---------- 更新打手余额和统计 ----------
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.chengjiaozongliang += 1
dashou_profile.yue += dashou_fencheng
diff --git a/users/views/__init__.py b/users/views/__init__.py
index f8be391..356e275 100644
--- a/users/views/__init__.py
+++ b/users/views/__init__.py
@@ -88,29 +88,41 @@ from .chufa import (
ShangjiaChufaJiluHuoquView,
)
-from .kefu import (
+from .kefu_base import (
+ KefuGetOrderTypesView,
+ KefuLoginView,
+ KefuStatsView,
+)
+
+from .kefu_orders import (
KefuCancelDesignationView,
KefuChangeDashouView,
KefuForceCompleteView,
- KefuGetDashouDetailView,
- KefuGetDashouListView,
KefuGetOrderDetailView,
KefuGetOrderListView,
- KefuGetOrderTypesView,
KefuGetShangjiaOrderListView,
- KefuLoginView,
KefuMerchantRefundView,
KefuPlatformRefundView,
+ KefuRecoverOrderView,
+ KefuRejectRefundView,
+ KefuRejectSettlementView,
+ KefuTransferHallView,
+)
+
+from .kefu_dashou import (
+ KefuGetDashouDetailView,
+ KefuGetDashouListView,
+ KefuUpdateDashouView,
+)
+
+from .kefu_punishment import (
KefuPunishView,
KefuPunishmentActionView,
KefuPunishmentDetailView,
KefuPunishmentListView,
- KefuRecoverOrderView,
- KefuRejectRefundView,
- KefuRejectSettlementView,
- KefuStatsView,
- KefuTransferHallView,
- KefuUpdateDashouView,
+)
+
+from .kefu_withdraw import (
KefuWithdrawActionView,
KefuWithdrawDetailView,
KefuWithdrawListView,
diff --git a/users/views/admin.py b/users/views/admin.py
index 288e3fd..3adef87 100644
--- a/users/views/admin.py
+++ b/users/views/admin.py
@@ -532,12 +532,18 @@ class AdGetOrderList(APIView):
# 注意:这里统计所有订单,不考虑筛选条件
try:
# 平台订单统计 (Platform=1)
- platform_total = Order.query.filter(Platform=1).count()
- platform_completed = Order.query.filter(Platform=1, Status=3).count()
+ _agg = Order.query.aggregate(
+ 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)
- merchant_total = Order.query.filter(Platform=2).count()
- merchant_completed = Order.query.filter(Platform=2, Status=3).count()
+ merchant_total = _agg['merchant_total']
+ merchant_completed = _agg['merchant_completed']
platform_stats = {
'total': platform_total,
@@ -1170,9 +1176,14 @@ class CfGuanLi(APIView):
)
# 7. 获取统计信息(总是需要统计)
- zongshu = PenaltyRecord.query.count()
- daichuli = PenaltyRecord.query.filter(ApplyStatus__in=[0, 3]).count()
- yichuli = PenaltyRecord.query.filter(ApplyStatus__in=[1, 2]).count()
+ _agg = PenaltyRecord.query.aggregate(
+ zongshu=Count('id'),
+ 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. 如果只请求统计信息,直接返回
if qingqiu_tongji:
@@ -1687,6 +1698,13 @@ class AdckyhxqView(APIView):
yonghu_id=uid
).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 = []
for record in member_records:
# 调用方法检查是否过期,并更新状态
@@ -1694,9 +1712,7 @@ class AdckyhxqView(APIView):
if not is_expired: # 只返回未过期的
# 查询会员详情
- huiyuan = Huiyuan.query.filter(
- huiyuan_id=record.huiyuan_id
- ).first()
+ huiyuan = _huiyuan_map.get(record.huiyuan_id)
if huiyuan:
member_list.append({
@@ -2282,15 +2298,13 @@ class AddtxshView(APIView):
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 = {
- 'awaiting': Tixianjilu.query.filter(
- zhuangtai=1,
- leixing=leixing
- ).count(),
- 'processed': Tixianjilu.query.filter(
- zhuangtai__in=[2, 3],
- leixing=leixing
- ).count()
+ 'awaiting': _agg['awaiting'],
+ 'processed': _agg['processed'],
}
# 构建响应数据
diff --git a/users/views/chufa.py b/users/views/chufa.py
index 4908bd8..1577c30 100644
--- a/users/views/chufa.py
+++ b/users/views/chufa.py
@@ -526,16 +526,16 @@ class DashouShensuView(APIView):
chufa_record.save()
# 6. 保存申诉图片(如果有)
- saved_tupian_urls = []
- if shensu_tupian_urls:
- for tupian_url in shensu_tupian_urls:
- if tupian_url: # 确保URL不为空
- chufa_tupian = PenaltyEvidenceImage.query.create(
- OrderID=chufa_record.OrderID,
- UserID=yonghuid,
- ImageURL=tupian_url
- )
- saved_tupian_urls.append(tupian_url)
+ saved_tupian_urls = [u for u in shensu_tupian_urls if u] if shensu_tupian_urls else []
+ if saved_tupian_urls:
+ PenaltyEvidenceImage.objects.bulk_create([
+ PenaltyEvidenceImage(
+ OrderID=chufa_record.OrderID,
+ UserID=yonghuid,
+ ImageURL=tupian_url,
+ )
+ for tupian_url in saved_tupian_urls
+ ])
# 7. 构建返回数据
response_data = {
diff --git a/users/views/kefu.py b/users/views/kefu.py
deleted file mode 100644
index b53ae32..0000000
--- a/users/views/kefu.py
+++ /dev/null
@@ -1,3794 +0,0 @@
-"""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. 返回成功数据(管理员无 KefuProfile,nicheng 用 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': []})
-
-
-class KefuGetOrderListView(APIView):
- """
- 客服获取平台订单列表接口(发单平台=1,非跨平台)
- 需要权限:002ab(平台订单管理)
- 请求参数:
- username: 客服账号(用于权限校验)
- page: 页码
- pageSize: 每页条数
- dingdan_id: 订单ID(精确搜索)
- laoban_id: 老板ID(搜索平台扩展表中的老板)
- dashou_id: 打手ID(搜索主表中的接单打手)
- leixing: 商品类型ID
- zhuangtai: 状态(多个用逗号分隔,如 "1,7")
- date: 日期(YYYY-MM-DD),筛选订单创建时间
- nicheng: 游戏昵称(订单主表的 nicheng 字段)
- jieshao: 订单介绍(订单主表的 jieshao 字段)
- clkf: 处理客服(订单主表的 clkf 字段)
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- start_total = time.time()
-
- # 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 '002ab' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
-
- # 4. 获取分页和筛选参数
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 20))
-
- dingdan_id = request.data.get('dingdan_id', '').strip()
- laoban_id = request.data.get('laoban_id', '').strip()
- dashou_id = request.data.get('dashou_id', '').strip()
- leixing = request.data.get('leixing')
- zhuangtai = request.data.get('zhuangtai', '')
- date = request.data.get('date', '').strip()
- # 新增筛选条件
- nicheng = request.data.get('nicheng', '').strip()
- jieshao = request.data.get('jieshao', '').strip()
- clkf = request.data.get('clkf', '').strip()
-
- # ---------- 查询条件 ----------
- # 基础条件:平台发单 + 兼容历史 Platform 为空
- from jituan.services.club_context import platform_order_base_q
- base_q = platform_order_base_q()
-
- if dingdan_id:
- q_conditions = base_q & Q(OrderID=dingdan_id)
- else:
- q_conditions = base_q
- if leixing is not None and str(leixing).strip() != '':
- try:
- leixing_int = int(leixing)
- q_conditions &= Q(ProductTypeID=leixing_int)
- except (ValueError, TypeError):
- pass
- if zhuangtai and zhuangtai != 'all':
- try:
- status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
- if status_list:
- q_conditions &= Q(Status__in=status_list)
- except (ValueError, TypeError):
- pass
- if dashou_id:
- q_conditions &= Q(PlayerID=dashou_id)
- if laoban_id:
- q_conditions &= Q(pingtai_kuozhan__BossID=laoban_id)
- if date:
- q_conditions &= Q(CreateTime__date=date)
- # 新增筛选条件
- if nicheng:
- q_conditions &= Q(Nickname__icontains=nicheng)
- if jieshao:
- q_conditions &= Q(Description__icontains=jieshao)
- if clkf:
- q_conditions &= Q(AssignedCS__icontains=clkf)
-
- from jituan.services.club_context import (
- count_order_status_buckets,
- order_scope_qs,
- paginate_fluent_query,
- )
- from orders.models import PlatformOrderExt
-
- # 按俱乐部过滤订单列表
- stats_qs = order_scope_qs(request).filter(q_conditions)
- stats = stats_qs.aggregate(
- total_orders=Count('id'),
- completed_orders=Count('id', filter=Q(Status=3)),
- refund_orders=Count('id', filter=Q(Status=5)),
- pending_orders=Count('id', filter=Q(Status__in=[4, 8]))
- )
- status_counts = count_order_status_buckets(q_conditions, request)
-
- list_qs = order_scope_qs(request).filter(q_conditions).order_by('-CreateTime')
- total_count, orders_page = paginate_fluent_query(list_qs, page, page_size)
- if total_count > 0 and not orders_page:
- start = (max(1, page) - 1) * page_size
- orders_page = list(list_qs._qs[start:start + page_size])
-
- order_ids = [o.OrderID for o in orders_page if o.OrderID]
- ext_map = {}
- if order_ids:
- for ext in PlatformOrderExt.query.filter(Order__OrderID__in=order_ids).select_related('Order'):
- if ext.Order:
- ext_map[ext.Order.OrderID] = ext
-
- laoban_ids = [
- ext.BossID for ext in ext_map.values() if ext.BossID
- ]
- nickname_map = {}
- if laoban_ids:
- users = User.query.filter(UserUID__in=laoban_ids).select_related('BossProfile').only(
- 'UserUID', 'BossProfile__nickname'
- )
- for user in users:
- nickname_map[user.UserUID] = (user.BossProfile.nickname or '') if getattr(user, 'BossProfile', None) else ''
-
- result_list = []
- for order in orders_page:
- ext = ext_map.get(order.OrderID)
- laoban_id = ext.BossID if ext else ''
- youxi_nicheng = nickname_map.get(laoban_id, order.Nickname or '')
- result_list.append({
- 'dingdan_id': order.OrderID or '',
- 'jieshao': order.Description or '',
- 'zhuangtai': order.Status,
- 'leixing': order.ProductTypeID,
- 'fadanpingtai': order.Platform,
- 'jiage': float(order.Amount) if order.Amount else 0.00,
- 'tupian_url': order.ImageURL or '',
- 'fadanshijian': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '',
- 'youxi_nicheng': youxi_nicheng
- })
-
- from jituan.services.club_user_access import list_response_meta
-
- response_data = {
- 'stats': {
- 'totalOrders': stats['total_orders'] or 0,
- 'completedOrders': stats['completed_orders'] or 0,
- 'refundOrders': stats['refund_orders'] or 0,
- 'pendingOrders': stats['pending_orders'] or 0
- },
- 'statusCounts': status_counts,
- 'list': result_list,
- 'total': total_count,
- 'page': page,
- 'pageSize': page_size,
- **list_response_meta(request),
- }
-
- logger.info(f"平台订单接口总耗时: {time.time()-start_total:.3f}s")
- return Response({'code': 0, 'data': response_data})
-
-
-class KefuGetShangjiaOrderListView(APIView):
- """
- 客服获取商家订单列表接口(发单平台=2,非跨平台)
- 需要权限:002ac(商家订单管理)—— 如不区分可使用 002ab
- 请求参数:
- username: 客服账号(用于权限校验)
- page: 页码
- pageSize: 每页条数
- dingdan_id: 订单ID(精确搜索)
- shangjia_id: 商家ID(搜索商家扩展表中的商家ID)
- dashou_id: 打手ID(搜索主表中的接单打手)
- leixing: 商品类型ID
- zhuangtai: 状态(多个用逗号分隔,如 "1,7")
- date: 日期(YYYY-MM-DD),筛选订单创建时间
- nicheng: 游戏昵称(订单主表的 nicheng 字段)
- jieshao: 订单介绍(订单主表的 jieshao 字段)
- clkf: 处理客服(订单主表的 clkf 字段)
- shangjia_nicheng: 商家昵称(商家扩展表中的昵称)
- """
- permission_classes = [IsAuthenticated]
- parser_classes = [JSONParser]
-
- def post(self, request):
- start_total = time.time()
-
- # 1. 获取前端参数(兼容 phone / username)
- username = request.data.get('username', '').strip() or 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. 检查商家订单管理权限(使用 002ac,如果没有可改用 002ab)
- if '002ac' not in permissions:
- return Response({'code': 403, 'msg': '您无权查看商家订单列表'})
-
- # 4. 获取分页和筛选参数
- page = int(request.data.get('page', 1))
- page_size = int(request.data.get('pageSize', 20))
-
- dingdan_id = request.data.get('dingdan_id', '').strip()
- shangjia_id = request.data.get('shangjia_id', '').strip()
- dashou_id = request.data.get('dashou_id', '').strip()
- leixing = request.data.get('leixing')
- zhuangtai = request.data.get('zhuangtai', '')
- date = request.data.get('date', '').strip()
- # 新增筛选条件
- nicheng = request.data.get('nicheng', '').strip()
- jieshao = request.data.get('jieshao', '').strip()
- clkf = request.data.get('clkf', '').strip()
- shangjia_nicheng = request.data.get('shangjia_nicheng', '').strip()
-
- # ---------- 查询条件 ----------
- from jituan.services.club_context import merchant_order_base_q
- base_q = merchant_order_base_q()
-
- if dingdan_id:
- q_conditions = base_q & Q(OrderID=dingdan_id)
- else:
- q_conditions = base_q
- if leixing is not None and str(leixing).strip() != '':
- try:
- leixing_int = int(leixing)
- q_conditions &= Q(ProductTypeID=leixing_int)
- except (ValueError, TypeError):
- pass
- if zhuangtai and zhuangtai != 'all':
- try:
- status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
- if status_list:
- q_conditions &= Q(Status__in=status_list)
- except (ValueError, TypeError):
- pass
- if dashou_id:
- q_conditions &= Q(PlayerID=dashou_id)
- if shangjia_id:
- q_conditions &= Q(shangjia_kuozhan__MerchantID=shangjia_id)
- if date:
- q_conditions &= Q(CreateTime__date=date)
- if nicheng:
- q_conditions &= Q(Nickname__icontains=nicheng)
- if jieshao:
- q_conditions &= Q(Description__icontains=jieshao)
- if clkf:
- q_conditions &= Q(AssignedCS__icontains=clkf)
- if shangjia_nicheng:
- # 通过商家扩展表关联 UserShangjia 的昵称
- q_conditions &= Q(shangjia_kuozhan__MerchantID__in=User.query.filter(
- ShopProfile__nicheng__icontains=shangjia_nicheng
- ).values_list('UserUID', flat=True))
-
- from jituan.services.club_context import (
- count_order_status_buckets,
- order_scope_qs,
- paginate_fluent_query,
- )
- from orders.models import MerchantOrderExt
-
- stats_qs = order_scope_qs(request).filter(q_conditions)
- stats = stats_qs.aggregate(
- total_orders=Count('id'),
- completed_orders=Count('id', filter=Q(Status=3)),
- refund_orders=Count('id', filter=Q(Status=5)),
- pending_orders=Count('id', filter=Q(Status__in=[4, 8]))
- )
- status_counts = count_order_status_buckets(q_conditions, request)
-
- list_qs = order_scope_qs(request).filter(q_conditions).order_by('-CreateTime')
- total_count, orders_page = paginate_fluent_query(list_qs, page, page_size)
- if total_count > 0 and not orders_page:
- start = (max(1, page) - 1) * page_size
- orders_page = list(list_qs._qs[start:start + page_size])
-
- order_ids = [o.OrderID for o in orders_page if o.OrderID]
- ext_map = {}
- if order_ids:
- for ext in MerchantOrderExt.query.filter(Order__OrderID__in=order_ids).select_related('Order'):
- if ext.Order:
- ext_map[ext.Order.OrderID] = ext
-
- shangjia_ids = [
- ext.MerchantID for ext in ext_map.values() if ext.MerchantID
- ]
- nickname_map = {}
- if shangjia_ids:
- users = User.query.filter(UserUID__in=shangjia_ids).select_related('ShopProfile').only(
- 'UserUID', 'ShopProfile__nicheng'
- )
- for user in users:
- nickname_map[user.UserUID] = user.ShopProfile.nicheng or '' if hasattr(user, 'ShopProfile') else ''
-
- result_list = []
- for order in orders_page:
- ext = ext_map.get(order.OrderID)
- shangjia_id = ext.MerchantID if ext else ''
- youxi_nicheng = order.Nickname or ''
- result_list.append({
- 'dingdan_id': order.OrderID or '',
- 'jieshao': order.Description or '',
- 'zhuangtai': order.Status,
- 'leixing': order.ProductTypeID,
- 'fadanpingtai': order.Platform,
- 'jiage': float(order.Amount) if order.Amount else 0.00,
- 'tupian_url': order.ImageURL or '',
- 'fadanshijian': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '',
- 'youxi_nicheng': youxi_nicheng
- })
-
- from jituan.services.club_user_access import list_response_meta
-
- response_data = {
- 'stats': {
- 'totalOrders': stats['total_orders'] or 0,
- 'completedOrders': stats['completed_orders'] or 0,
- 'refundOrders': stats['refund_orders'] or 0,
- 'pendingOrders': stats['pending_orders'] or 0
- },
- 'statusCounts': status_counts,
- 'list': result_list,
- 'total': total_count,
- 'page': page,
- 'pageSize': page_size,
- **list_response_meta(request),
- }
-
- logger.info(f"商家订单接口总耗时: {time.time()-start_total:.3f}s")
- return Response({'code': 0, 'data': response_data})
-
-
-class KefuChangeDashouView(APIView):
- """
- 客服更换打手接口
- 请求:POST /yonghu/kefughds
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "new_dashou_id": "新打手ID"
- }
- 认证: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()
- new_dashou_id = request.data.get('new_dashou_id', '').strip()
-
- if not phone or not dingdan_id or not new_dashou_id:
- 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:
- order = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 验证订单状态是否允许更换打手(进行中2 或 结算中8)
- if order.Status not in [2, 8]:
- return Response({'code': 400, 'msg': '订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 查询新打手
- try:
- new_dashou_user = User.query.get(UserUID=new_dashou_id)
- except User.DoesNotExist:
- return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 6. 验证打手扩展表
- try:
- new_dashou_profile = new_dashou_user.DashouProfile
- except ObjectDoesNotExist:
- return Response({'code': 404, 'msg': '打手信息不完整'}, status=status.HTTP_404_NOT_FOUND)
-
- # 7. 验证打手状态(打手状态和账号状态必须为1)
- if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1:
- return Response({'code': 400, 'msg': '打手状态不允许接单'}, status=status.HTTP_400_BAD_REQUEST)
- # 8. 验证打手积分是否足够(至少5分)
- if new_dashou_profile.jifen < 5:
- return Response({'code': 400, 'msg': '打手积分不足,无法接单'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 8. 根据订单的抢单要求类型进行额外验证
- yaoqiuleixing = order.GrabRequirement
-
- if yaoqiuleixing == 1: # 会员抢单
- huiyuan_id = order.MembershipID
- if not huiyuan_id:
- return Response({'code': 400, 'msg': '订单会员ID为空'}, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- # 查询打手的会员购买记录
- huiyuan_goumai = Huiyuangoumai.query.get(
- huiyuan_id=huiyuan_id,
- yonghu_id=new_dashou_id # 假设 yonghu_id 字段存储用户ID
- )
- except Huiyuangoumai.DoesNotExist:
- return Response({'code': 400, 'msg': '打手未购买该订单要求的会员'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 检查会员是否过期(调用模型方法)
- is_expired = huiyuan_goumai.jiance_shifou_daoqi()
- if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1:
- return Response({'code': 400, 'msg': '打手的会员已过期'}, status=status.HTTP_400_BAD_REQUEST)
-
- elif yaoqiuleixing == 2: # 押金抢单
- order_yajin = order.CommissionReq # 假设订单的 yongjin 字段存储押金要求
- if order_yajin is None:
- return Response({'code': 400, 'msg': '订单押金为空'}, status=status.HTTP_400_BAD_REQUEST)
-
- if new_dashou_profile.yajin < order_yajin:
- return Response({'code': 400, 'msg': '打手押金不足'}, status=status.HTTP_400_BAD_REQUEST)
- else:
- return Response({'code': 400, 'msg': '订单抢单要求类型错误'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 9. 所有验证通过,开始更换打手(使用事务)
- with transaction.atomic():
- # 9.1 更新新打手扩展表
- new_dashou_profile.zhuangtai = 0 # 设为接单中
- new_dashou_profile.jiedanzongliang += 1
- new_dashou_profile.jinrijiedan += 1
- new_dashou_profile.jinyuejiedan += 1
- new_dashou_profile.save()
-
- # 9.2 处理原打手(如果存在)
- old_dashou_id = order.PlayerID
- if old_dashou_id:
- try:
- old_dashou_user = User.query.get(UserUID=old_dashou_id)
- old_dashou_profile = old_dashou_user.DashouProfile
- old_dashou_profile.zhuangtai = 1 # 恢复空闲
- old_dashou_profile.tuikuanliang += 1 # 原打手退款总量+1(业务逻辑)
- old_dashou_profile.save()
- logger.info(f"原打手 {old_dashou_id} 状态已更新")
- except (User.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"原打手 {old_dashou_id} 不存在或信息不完整,跳过更新")
- except Exception as e:
- logger.error(f"更新原打手时异常: {str(e)}", exc_info=True)
-
- # 9.3 更新订单
- order.PlayerID = new_dashou_id
- order.Status = 2 # 进行中
- order.AssignedCS = current_user.UserUID # 记录处理客服ID
- order.save()
-
- return Response({'code': 0, 'msg': '更换打手成功'})
-
-
-class KefuRecoverOrderView(APIView):
- """
- 客服恢复订单接口(仅状态9可恢复)
- 请求:POST /yonghu/kefuhf
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID"
- }
- 认证: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()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- 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
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- 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. 验证订单状态是否为9(未付款)
- if order.Status != 9:
- return Response({'code': 400, 'msg': '订单状态不允许恢复'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 根据是否有指定ID决定新状态
- if order.AssignedID:
- new_status = 7 # 指定中
- else:
- new_status = 1 # 下单中
-
- # 6. 更新订单
- order.Status = new_status
- order.AssignedCS = current_user.UserUID
- order.save()
-
- return Response({'code': 0, 'msg': '恢复成功', 'data': {'new_status': new_status}})
-
-
-class KefuPlatformRefundView(APIView):
- """
- 客服平台订单退款接口(调用微信支付)
- 请求:POST /yonghu/kefupttk
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "tuikuan_liyou": "退款理由(可选)"
- }
- 认证:JWT
- 返回:code=0 成功,code=其他 失败
- """
- 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()
- tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
-
- if not phone or not dingdan_id:
- 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:
- with transaction.atomic():
- order = Order.query.select_related('pingtai_kuozhan').select_for_update().get(OrderID=dingdan_id)
-
- # 4. 验证平台订单
- if order.Platform != 1:
- return Response({'code': 400, 'msg': '该订单不是平台订单'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 验证状态允许退款(根据模型,状态值:1下单中,2进行中,3已完成,4退款审核,5已退款,6退款失败,7指定中,8结算中)
- allowed_statuses = [1, 2, 4, 7, 8] # 允许退款的已支付或处理中状态
- if order.Status not in allowed_statuses:
- return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 6. 获取微信交易号(如果存在)
- wechat_transaction_id = getattr(order, 'WechatTransactionID', None)
-
- # 7. 调用微信退款接口
- refund_result = self.call_wechat_refund(
- dingdan_id=order.OrderID,
- jine=order.Amount,
- transaction_id=wechat_transaction_id
- )
-
- if refund_result['code'] != 0:
- # 微信退款失败,直接返回错误,数据库不更新
- logger.error(f"微信退款失败,订单 {dingdan_id},原因:{refund_result['msg']}")
- return Response({'code': 400, 'msg': refund_result['msg']}, status=status.HTTP_400_BAD_REQUEST)
-
- # 8. 更新订单状态为5(已退款)
- order.Status = 5
- order.AssignedCS = current_user.Phone # 记录处理客服手机号
- if tuikuan_liyou:
- order.RefundReason = tuikuan_liyou
- order.save()
-
- # 9. 更新打手数据
- jiedan_dashou_id = order.PlayerID
- if jiedan_dashou_id:
- try:
- dashou_user = User.query.get(UserUID=jiedan_dashou_id)
- dashou_profile = dashou_user.DashouProfile
- dashou_profile.tuikuanliang += 1
- dashou_profile.zhuangtai = 1 # 打手状态设为空闲
- dashou_profile.save()
- logger.info(f"平台退款:打手 {jiedan_dashou_id} 退款订单+1")
- except (User.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"平台退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
-
- # ========== 新增:商品和店铺每日统计 ==========
- try:
- # 商品每日统计(只要有商品ID就执行)
- if order.ProductID:
- # 从扩展表获取已计算好的收益值(下单时已存储)
- pingtai_shouyi = Decimal('0.00')
- dianpu_shouyi = Decimal('0.00')
- if hasattr(order, 'pingtai_kuozhan'):
- pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
- dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
-
- update_shangpin_daily_stat(
- shangpin_id=order.ProductID,
- caozuo_leixing=3, # 3 = 退款
- dingdan_jiage=order.Amount,
- pingtai_shouyi=pingtai_shouyi,
- dianpu_zongshouyi=dianpu_shouyi
- )
- logger.info(f"商品统计更新完成,商品ID: {order.ProductID}")
-
- # 店铺每日统计(仅当存在店铺ID时执行)
- dianpu_id = None
- if hasattr(order, 'pingtai_kuozhan'):
- dianpu_id = order.pingtai_kuozhan.ShopID
- if dianpu_id:
- dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
- update_dianpu_daily_stat(
- dianpu_id=dianpu_id,
- caozuo_leixing=3, # 3 = 退款
- dingdan_jiage=order.Amount,
- dianpu_shouyi=dianpu_shouyi
- )
- logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
- except Exception as e:
- logger.error(f"商品/店铺统计更新失败: {e}")
- logger.error(traceback.format_exc())
- # 统计失败不影响主流程
- # =============================================
-
- # 10. 更新退款记录表(假设存在 RefundRecord 模型,字段需根据实际调整)
-
- #更新支出记录
- update_daily_payout(order.Amount, getattr(order, 'ClubID', None))
-
-
- try:
- refund_record, created = RefundRecord.query.get_or_create(
- OrderID=dingdan_id,
- defaults={
- 'PlayerID': jiedan_dashou_id or '',
- 'ApplicantID': '',
- 'ProcessorID': phone,
- 'Reason': tuikuan_liyou or '客服退款',
- 'ApplyStatus': 1, # 成功
- 'Amount': order.Amount,
- 'PlayerCommission': order.PlayerCommission or 0,
- 'Description': order.Description or '',
- 'Remark': '客服平台退款',
- 'Nickname': order.Nickname or '',
- }
- )
- if not created:
- refund_record.ApplyStatus = 1
- refund_record.ProcessorID = phone
- if tuikuan_liyou:
- refund_record.Reason = tuikuan_liyou
- refund_record.save()
- except Exception as e:
- logger.error(f"平台退款更新退款记录异常: {str(e)}", exc_info=True)
-
- # 11. 更新客服统计数据
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.PlayerID,
- amount=order.PlayerCommission,
- action=3 # 3 表示退款(退款)
- )
- logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
- logger.info(f"平台退款成功,订单 {dingdan_id},退款单号 {refund_result.get('refund_id')}")
- return Response({'code': 0, 'msg': '退款成功'})
-
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
- except Exception as e:
- logger.error(f"平台退款接口异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
- def call_wechat_refund(self, dingdan_id, jine, transaction_id=None):
- """
- 调用微信支付退款接口
- :param dingdan_id: 商户订单号(我们的订单ID)
- :param jine: 退款金额
- :param transaction_id: 微信支付订单号(优先使用)
- :return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'}
- """
- # 获取配置
- appid = getattr(settings, 'WEIXIN_APPID', '')
- mch_id = getattr(settings, 'WEIXIN_MCHID', '')
- key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '')
- cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '')
- key_path = getattr(settings, 'WEIXIN_KEY_PATH', '')
-
- if not all([appid, mch_id, key, cert_path, key_path]):
- missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v]
- logger.error(f"微信支付配置缺失: {missing}")
- return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'}
-
- # 生成退款单号
- out_refund_no = self.generate_refund_no(dingdan_id)
- total_fee = yuan_to_fen(jine)
- refund_fee = total_fee
- refund_desc = '客服退款'
-
- # 构造请求参数
- params = {
- 'appid': appid,
- 'mch_id': mch_id,
- 'nonce_str': self.generate_nonce_str(),
- 'out_refund_no': out_refund_no,
- 'total_fee': total_fee,
- 'refund_fee': refund_fee,
- 'refund_desc': refund_desc,
- }
- if transaction_id:
- params['transaction_id'] = transaction_id
- else:
- params['out_trade_no'] = dingdan_id # 使用订单ID作为商户订单号
-
- # 生成签名
- sorted_keys = sorted(params.keys())
- stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys])
- stringSignTemp = f"{stringA}&key={key}"
- sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
- params['sign'] = sign
-
- xml_data = self.dict_to_xml(params)
- url = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
-
- try:
- response = requests.post(
- url,
- data=xml_data,
- cert=(cert_path, key_path),
- timeout=10
- )
- result = xmltodict.parse(response.content)['xml']
- logger.info(f"微信退款响应: {result}")
- except Exception as e:
- logger.error(f"微信退款请求异常: {str(e)}", exc_info=True)
- return {'code': 500, 'msg': '微信退款请求异常'}
-
- if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
- return {'code': 0, 'msg': 'success', 'refund_id': out_refund_no}
- else:
- err_msg = result.get('err_code_des', result.get('return_msg', '未知错误'))
- logger.warning(f"微信退款失败: {err_msg}")
- return {'code': 400, 'msg': err_msg}
-
- def generate_nonce_str(self):
- return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
-
- def generate_refund_no(self, dingdan_id):
- timestamp = str(int(time.time()))
- rand = ''.join(random.choices('0123456789', k=4))
- return f"{dingdan_id}REF{timestamp}{rand}"
-
- def dict_to_xml(self, data):
- xml = ['']
- for k, v in data.items():
- xml.append(f'<{k}>{v}{k}>')
- xml.append('')
- return ''.join(xml)
-
-
-class KefuRejectRefundView(APIView):
- """
- 客服拒绝退款接口
- 请求:POST /yonghu/kefujjtk
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "jujue_liyou": "拒绝理由(可选)"
- }
- 认证: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()
- jujue_liyou = request.data.get('jujue_liyou', '').strip()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证
- 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
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 3. 查询订单并加锁
- try:
- with transaction.atomic():
- order = Order.objects.select_for_update().get(OrderID=dingdan_id)
-
- # 4. 验证订单状态为退款审核中(4)
- if order.Status != 4:
- return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取必要字段
- jiedan_dashou_id = order.PlayerID
- dashou_fencheng = order.PlayerCommission
- fadan_pingtai = order.Platform
-
- # 5. 验证打手分成金额是否存在
- if dashou_fencheng is None:
- return Response({'code': 400, 'msg': '订单缺少打手分成金额'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 6. 更新订单状态为退款失败(6)
- order.Status = 6
- order.AssignedCS = current_user.Phone # 记录处理客服手机号
- if jujue_liyou:
- order.RefundReason = jujue_liyou
- order.save()
-
- # 7. 给打手结算(返还分成)
- if jiedan_dashou_id:
- try:
- dashou_user = User.query.get(UserUID=jiedan_dashou_id)
- dashou_profile = dashou_user.DashouProfile
- # 增加打手相关统计数据
- dashou_profile.chengjiaozongliang += 1
- dashou_profile.yue += dashou_fencheng
- dashou_profile.zonge += dashou_fencheng
- dashou_profile.jinrishouyi += dashou_fencheng
- dashou_profile.jinyueshouyi += dashou_fencheng
- dashou_profile.save()
- logger.info(f"拒绝退款:打手 {jiedan_dashou_id} 获得分成 {dashou_fencheng}")
- except (User.DoesNotExist, AttributeError):
- logger.warning(f"拒绝退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过结算")
-
- # 8. 如果是商家发单,更新商家成交订单数量
- if fadan_pingtai == 2:
- try:
- # 获取商家订单扩展表
- shangjia_kuozhan = order.shangjia_kuozhan
- shangjia_id = shangjia_kuozhan.MerchantID
- if shangjia_id:
- shangjia_user = User.query.get(UserUID=shangjia_id)
- shangjia_profile = shangjia_user.ShopProfile
- shangjia_profile.chengjiao += 1
- shangjia_profile.save()
- logger.info(f"拒绝退款:商家 {shangjia_id} 成交订单+1")
-
- # ✅ 新方法(行为驱动,更安全)
- update_shangjia_daily(
- yonghuid=shangjia_id,
- amount=Decimal(str(order.Amount)),
- action=2, # 2 = 结算
- order_id=dingdan_id,
- )
- except (ObjectDoesNotExist, User.DoesNotExist, AttributeError):
- logger.warning(f"拒绝退款:商家信息不存在,跳过商家更新")
-
- # 9. 更新退款记录表
- try:
- refund_record = RefundRecord.query.get(OrderID=dingdan_id)
- refund_record.ApplyStatus = 2 # 2表示拒绝
- refund_record.ProcessorID = phone
- refund_record.RejectReason = jujue_liyou # 记录驳回理由(假设模型有此字段)
- refund_record.save()
- logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}")
- except RefundRecord.DoesNotExist:
- # 如果退款记录不存在,可以创建一条记录(可选)
- RefundRecord.query.create(
- OrderID=dingdan_id,
- PlayerID=jiedan_dashou_id or '',
- ApplicantID='',
- ProcessorID=phone,
- Reason=jujue_liyou or '客服拒绝退款',
- ApplyStatus=2,
- Amount=order.Amount,
- PlayerCommission=dashou_fencheng or 0,
- Description=order.Description or '',
- Remark='客服拒绝退款',
- Nickname=order.Nickname or '',
- RejectReason=jujue_liyou or '',
- )
- except Exception as e:
- logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True)
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.PlayerID,
- amount=dashou_fencheng,
- action=2 # 2 表示成交(结算)
- )
- logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
- # 10. 更新客服统计数据
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- return Response({'code': 0, 'msg': '拒绝退款成功'})
-
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
- except Exception as e:
- logger.error(f"拒绝退款接口异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-
-
-class KefuMerchantRefundView(APIView):
- """
- 客服商家订单退款接口(发单平台=2)
- 请求:POST /yonghu/kefusjtk
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "tuikuan_liyou": "退款理由(可选)"
- }
- 认证: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()
- tuikuan_liyou = request.data.get('tuikuan_liyou', '').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:
- 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
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
-
-
- # 查询订单,预加载商家扩展表
- try:
- order = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 验证商家订单
- if order.Platform != 2:
- return Response({'code': 400, 'msg': '该订单不是商家订单'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 验证状态允许退款
- allowed_statuses = [1, 7, 2, 8, 4]
- if order.Status not in allowed_statuses:
- return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 获取必要数据
- jiedan_dashou_id = order.PlayerID
- jine = order.Amount
- if jine is None:
- return Response({'code': 400, 'msg': '订单金额缺失'}, status=status.HTTP_400_BAD_REQUEST)
-
- try:
- shangjia_kuozhan = order.shangjia_kuozhan
- shangjia_id = shangjia_kuozhan.MerchantID
- except ObjectDoesNotExist:
- return Response({'code': 400, 'msg': '订单商家信息不存在'}, status=status.HTTP_400_BAD_REQUEST)
-
- with transaction.atomic():
- # 更新订单
- order.Status = 5
- order.AssignedCS = current_user.Phone
- if tuikuan_liyou:
- order.RefundReason = tuikuan_liyou
- order.save()
-
- # 更新打手
- if jiedan_dashou_id:
- try:
- dashou_user = User.query.get(UserUID=jiedan_dashou_id)
- dashou_profile = dashou_user.DashouProfile
- dashou_profile.tuikuanliang += 1
- dashou_profile.zhuangtai = 1
- dashou_profile.save()
- except (User.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"商家退款:打手 {jiedan_dashou_id} 不存在,跳过更新")
-
- # 更新商家(客服额度出资的订单不退商家余额,仅释放子客服额度)
- try:
- from merchant_ops.services.wallet import is_staff_quota_order
- shangjia_user = User.query.get(UserUID=shangjia_id)
- shangjia_profile = shangjia_user.ShopProfile
- shangjia_profile.tuikuan += 1
- if not is_staff_quota_order(dingdan_id):
- shangjia_profile.yue += jine
- shangjia_profile.save()
- except (User.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"商家退款:商家 {shangjia_id} 不存在,跳过更新")
-
- # 更新退款记录表
- try:
- refund_record = RefundRecord.query.get(OrderID=dingdan_id)
- refund_record.ApplyStatus = 1
- refund_record.ProcessorID = phone
- refund_record.save()
- except RefundRecord.DoesNotExist:
- pass
-
- # 更新客服统计
- update_shangjia_daily(
- yonghuid=shangjia_id,
- amount=Decimal(str(order.Amount)),
- action=3, # 3 = 退款
- order_id=dingdan_id,
- )
-
-
- # 调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.PlayerID,
- amount=order.PlayerCommission,
- action=3 # 3 表示退款(退款)
- )
- logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
-
-
- # 管理员无客服扩展表,跳过计数器更新
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- if not is_admin:
- kefu = current_user.KefuProfile
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- return Response({'code': 0, 'msg': '退款成功'})
-
-
-class KefuRejectSettlementView(APIView):
- """
- 客服拒绝结算接口(仅平台订单,状态2/8)
- 请求:POST /yonghu/kefujjjs
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID",
- "tkly": "拒绝结算理由(必填)"
- }
- 认证: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()
- tkly = request.data.get('tkly', '').strip()
-
- if not phone or not dingdan_id or not tkly:
- 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:
- order = Order.query.select_related('pingtai_kuozhan').get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 校验订单条件:平台发单(1)且状态为2或8
- if order.Platform != 1 or order.Status not in [2, 8]:
- return Response({'code': 400, 'msg': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算'}, status=status.HTTP_400_BAD_REQUEST)
-
- jiedan_dashou_id = order.PlayerID
- if not jiedan_dashou_id:
- return Response({'code': 400, 'msg': '订单暂无接单打手'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 使用事务更新
- with transaction.atomic():
- # 5.1 更新订单主表
- order.Status = 5 # 已退款
- order.RefundReason = tkly # 拒绝理由
- order.AssignedCS = phone # 记录客服手机号
- order.save()
-
- # 5.2 更新平台订单扩展表:标记为拒绝结算(假设字段 jjjs_biaoshi)
- # 注意:需要确保 PlatformOrderExt 模型中已添加 jjjs_biaoshi 字段,IntegerField,默认0
- if hasattr(order, 'pingtai_kuozhan') and order.pingtai_kuozhan:
- pingtai_ext = order.pingtai_kuozhan
- pingtai_ext.RejectSettlementFlag = 1 # 1表示拒绝结算
- pingtai_ext.save()
- else:
- # 理论上应该存在,但若不存在则记录日志
- logger.warning(f"订单 {dingdan_id} 缺少平台扩展表,无法标记拒绝结算")
-
- # 5.3 更新打手扩展表
- try:
- dashou_user = User.query.get(UserUID=jiedan_dashou_id)
- dashou_profile = dashou_user.DashouProfile
- dashou_profile.zhuangtai = 1 # 打手状态设为空闲
- dashou_profile.tuikuanliang += 1 # 退款订单总量+1
- dashou_profile.save()
- logger.info(f"拒绝结算:打手 {jiedan_dashou_id} 退款订单+1,状态空闲")
- except (User.DoesNotExist, ObjectDoesNotExist):
- logger.warning(f"拒绝结算:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
- except Exception as e:
- logger.error(f"拒绝结算更新打手异常: {str(e)}", exc_info=True)
-
- # 5.4 更新客服扩展表统计数据
-
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.PlayerID,
- amount=order.PlayerCommission,
- action=3 # 3 表示退款(退款)
- )
- logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
-
-
- kefu.jinrichuli += 1
- kefu.jinyuechuli += 1
- kefu.zongchuli += 1
- kefu.save()
-
- return Response({'code': 0, 'msg': '拒绝结算成功'})
-
-
-class KefuTransferHallView(APIView):
- """
- 客服恢复大厅(转移大厅)接口
- 请求:POST /yonghu/kefuzydt
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID"
- }
- 认证: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()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证(同前,略,但必须完整实现)
- 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)
-
- # 3. 查询订单
- try:
- order = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 校验订单状态(允许状态:2,8,4)
- if order.Status not in [2, 8, 4]:
- return Response({'code': 400, 'msg': '订单状态不允许转移大厅'}, status=status.HTTP_400_BAD_REQUEST)
-
-
- if order.PlayerID:
- UserDashou.query.filter(user__UserUID=order.PlayerID).update(zhuangtai=1)
- else:
- pass
-
- # 5. 根据是否有指定ID决定新状态
- if order.AssignedID:
- new_status = 7 # 指定中
- else:
- new_status = 1 # 下单中
-
- # 6. 使用事务更新
- with transaction.atomic():
- # 清空接单打手ID,更新状态,记录客服
- order.PlayerID = None
- order.Status = new_status
- order.AssignedCS = phone
- order.save()
-
- return Response({'code': 0, 'msg': '转移大厅成功'})
-
-
-class KefuCancelDesignationView(APIView):
- """
- 客服取消指定接口(仅状态7)
- 请求:POST /yonghu/kefuqxzd
- 参数:{
- "phone": "客服手机号",
- "dingdan_id": "订单ID"
- }
- 认证: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()
-
- if not phone or not dingdan_id:
- return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
-
- # 2. 客服身份验证(同前,略,但必须完整实现)
- 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)
-
- # 3. 查询订单
- try:
- order = Order.query.get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
-
- # 4. 校验订单状态为7(指定中)
-
- if order.Status != 7:
- print(999)
- return Response({'code': 400, 'msg': '仅状态为指定中(7)的订单可取消指定'}, status=status.HTTP_400_BAD_REQUEST)
-
- # 5. 使用事务更新
- with transaction.atomic():
- # 状态改为1(下单中),清空指定ID,记录客服
- order.Status = 1
- order.AssignedID = None
- order.AssignedCS = phone
- order.save()
-
- return Response({'code': 0, 'msg': '取消指定成功'})
-
-
-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 = Huiyuangoumai.query.filter(
- yonghu_id=uid
- ).order_by('-CreateTime')
-
- 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:
- # 获取会员详情
- try:
- huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id)
- huiyuan_name = huiyuan.jieshao
- except Huiyuan.DoesNotExist:
- huiyuan_name = '未知会员'
-
- 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.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': '服务器内部错误'})
-
-
-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)
- stats = {
- 'zongshu': base_qs.count(),
- 'daichuli': base_qs.filter(ApplyStatus__in=[0, 3]).count(),
- 'yichuli': base_qs.filter(ApplyStatus__in=[1, 2]).count(),
- }
-
- 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 KefuGetOrderDetailView(APIView):
- 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)
-
- # 2. 获取当前用户
- 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 Exception as e:
- logger.exception(f"[kefuhqddxq] 获取客服扩展表异常 user={getattr(current_user, 'yonghuid', '?')}")
- return Response({'code': 500, 'msg': '服务器错误'}, status=500)
-
- # 3. 查询订单
- try:
- dingdan_obj = Order.query.select_related(
- 'pingtai_kuozhan', 'shangjia_kuozhan'
- ).get(OrderID=dingdan_id)
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'}, status=404)
- except Exception as e:
- logger.exception(f"[kefuhqddxq] 订单查询异常 dingdan_id={dingdan_id}")
- return Response({'code': 500, 'msg': '服务器错误'}, status=500)
-
- from jituan.services.club_user_access import forbid_if_order_out_of_scope
- deny = forbid_if_order_out_of_scope(request, dingdan_obj)
- if deny:
- return deny
-
- # 4. 构建基础数据
- try:
- response_data = {
- 'dingdan_id': dingdan_obj.OrderID or '',
- 'zhuangtai': dingdan_obj.Status or 0,
- 'fadanpingtai': dingdan_obj.Platform or 0,
- 'jiage': float(dingdan_obj.Amount) if dingdan_obj.Amount else 0.00,
- 'dashou_fencheng': float(dingdan_obj.PlayerCommission) if dingdan_obj.PlayerCommission else 0.00,
- 'jiedan_dashou_id': dingdan_obj.PlayerID or '',
- 'dashou_liuyan': dingdan_obj.PlayerRemark or '',
- 'zhiding_id': dingdan_obj.AssignedID or '',
- 'shangpin_id': dingdan_obj.ProductID or '',
- 'leixing': dingdan_obj.ProductTypeID or 0,
- 'tupian_url': dingdan_obj.ImageURL or '',
- 'jieshao': dingdan_obj.Description or '',
- 'beizhu': dingdan_obj.Remark or '',
- 'tkly': dingdan_obj.RefundReason or '',
- 'nicheng': dingdan_obj.Nickname or '',
- 'clkf': dingdan_obj.AssignedCS or '',
- 'chuangjianshijian': dingdan_obj.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.CreateTime else '',
- 'genggaishijian': dingdan_obj.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.UpdateTime else '',
- }
- except Exception as e:
- logger.exception(f"[kefuhqddxq] 构建基础数据异常 dingdan_id={dingdan_id}")
- return Response({'code': 500, 'msg': '服务器错误'}, status=500)
-
- # 5. 扩展表数据
- try:
- fadan_pingtai = dingdan_obj.Platform
- if fadan_pingtai == 1:
- if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan:
- pingtai = dingdan_obj.pingtai_kuozhan
- response_data['pingtai_kuozhan'] = {
- 'laoban_id': pingtai.BossID or '',
- 'laoban_pingjia': pingtai.BossReview or '',
- }
- else:
- response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''}
- elif fadan_pingtai == 2:
- if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan:
- shangjia = dingdan_obj.shangjia_kuozhan
- response_data['shangjia_kuozhan'] = {
- 'shangjia_id': shangjia.MerchantID or '',
- 'sjnicheng': shangjia.MerchantNickname or '',
- 'shangjia_pingjia': shangjia.MerchantReview or '',
- 'cfliyou': shangjia.PenaltyReason or '',
- 'sqzhuangtai': shangjia.PenaltyApplyStatus if shangjia.PenaltyApplyStatus is not None else 0,
- 'bhliyou': shangjia.RejectReason or '',
- }
- else:
- response_data['shangjia_kuozhan'] = {
- 'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '',
- 'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': ''
- }
- except Exception as e:
- logger.exception(f"[kefuhqddxq] 扩展数据异常 dingdan_id={dingdan_id}")
- return Response({'code': 500, 'msg': '服务器错误'}, status=500)
-
- # 6. 打手图片
- try:
- dashou_images = PlayerDeliveryImage.query.filter(OrderID=dingdan_id).values_list('ImageURL', flat=True)
- response_data['dashou_images'] = [img for img in dashou_images if img]
- except Exception as e:
- logger.exception(f"[kefuhqddxq] 打手图片查询异常 dingdan_id={dingdan_id}")
- response_data['dashou_images'] = []
-
- # 7. 退款理由
- try:
- tuikuan_record = RefundRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first()
- response_data['tuikuan_liyou'] = tuikuan_record.Reason if tuikuan_record and tuikuan_record.Reason else ''
- except Exception as e:
- logger.exception(f"[kefuhqddxq] 退款理由查询异常 dingdan_id={dingdan_id}")
- response_data['tuikuan_liyou'] = ''
-
- # 8. 指定打手响应
- ZHIDING_HF_TEXT = {1: '想接', 2: '不想接', 3: '等会接'}
- hf = getattr(dingdan_obj, 'AssignedResponse', None)
- response_data['zhiding_hf'] = hf
- response_data['zhiding_hf_text'] = ZHIDING_HF_TEXT.get(hf, '') if hf else ''
- response_data['zhiding_hf_sj'] = (
- dingdan_obj.AssignedResponseAt.strftime('%Y-%m-%d %H:%M:%S')
- if getattr(dingdan_obj, 'AssignedResponseAt', None) else ''
- )
- response_data['zhiding_dhj_sm'] = getattr(dingdan_obj, 'AssignedLaterNote', '') or ''
-
- # 9. 关联罚款(打手)
- from utils.penalty_status import PENALTY_STATUS_LABELS as PENALTY_STATUS
- try:
- from orders.models import Penalty
- dashou_id = dingdan_obj.PlayerID or dingdan_obj.AssignedID or ''
- fakuan_qs = Penalty.query.filter(RelatedOrderID=dingdan_id)
- if dashou_id:
- fakuan_qs = fakuan_qs.filter(PenalizedUserID=dashou_id)
- fakuan_list = []
- for p in fakuan_qs.order_by('-CreateTime')[:5]:
- fakuan_list.append({
- 'amount': str(p.FineAmount or '0.00'),
- 'status': p.Status,
- 'status_text': PENALTY_STATUS.get(p.Status, ''),
- 'reason': p.Reason or '',
- 'create_time': p.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if p.CreateTime else '',
- })
- response_data['fakuan_list'] = fakuan_list
- latest = fakuan_list[0] if fakuan_list else None
- response_data['fakuan_info'] = latest
- response_data['is_fadaned'] = Penalty.query.filter(
- RelatedOrderID=dingdan_id, ApplicantID=getattr(current_user, 'UserUID', '')
- ).exists()
- except Exception as e:
- logger.exception(f"[kefuhqddxq] 罚款查询异常 dingdan_id={dingdan_id}")
- response_data['fakuan_list'] = []
- response_data['fakuan_info'] = None
- response_data['is_fadaned'] = False
-
- return Response({'code': 0, 'data': response_data})
-
-
-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.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.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 KefuForceCompleteView(APIView):
- """
- 客服强制结单接口(仅本地订单)
- 请求:POST /yonghu/kefuqiangzhijiedan
- 参数:{"phone": "客服手机号", "dingdan_id": "订单ID"}
- 认证:JWT,仅限状态正常的客服
- 逻辑:
- 1. 验证客服身份及状态
- 2. 查询订单并加锁(事务内)
- 3. 校验订单状态是否为8(结算中)
- 4. 校验订单必须有接单打手且打手分成金额有效
- 5. 给打手增加余额、成交统计、今日/本月收益
- 6. 如果是商家发单,更新商家成交订单数量
- 7. 更新订单状态为3(已完成)
- 8. 更新客服处理统计
- 9. 返回成功
- """
- permission_classes = [permissions.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': '参数缺失'})
-
- # 1. 客服身份验证
- current_user = request.user
- if getattr(current_user, 'Phone', '') != phone:
- return Response({'code': 401, 'msg': '认证失败'})
-
- if current_user.UserType not in ('kefu', 'admin'):
- return Response({'code': 401, 'msg': '认证失败'})
-
- is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
- kefu = None
- if not is_admin:
- try:
- kefu = current_user.KefuProfile
- except ObjectDoesNotExist:
- return Response({'code': 401, 'msg': '认证失败'})
-
- if kefu.zhuangtai != 1:
- return Response({'code': 401, 'msg': '客服状态异常'})
-
- # 2. 订单处理(事务内)
- try:
- with transaction.atomic():
- order = Order.objects.select_for_update().get(OrderID=dingdan_id)
-
- # 3. 校验订单状态
- if order.Status != 8:
- return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.Status}'})
-
- # 4. 校验必要字段
- dashou_id = order.PlayerID
- dashou_fencheng = order.PlayerCommission
- if not dashou_id:
- return Response({'code': 400, 'msg': '订单无接单打手'})
- if not dashou_fencheng or dashou_fencheng <= 0:
- return Response({'code': 400, 'msg': '打手分成金额无效'})
-
- # 5. 给打手结算
- try:
- dashou_user = User.query.get(UserUID=dashou_id)
- dashou_profile = dashou_user.DashouProfile
- # 原子更新(避免并发重复加)
- dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1
- dashou_profile.yue = F('yue') + dashou_fencheng
- dashou_profile.zonge = F('zonge') + dashou_fencheng
- dashou_profile.jinrishouyi = F('jinrishouyi') + dashou_fencheng
- dashou_profile.jinyueshouyi = F('jinyueshouyi') + dashou_fencheng
- dashou_profile.save(update_fields=[
- 'chengjiaozongliang', 'yue', 'zonge',
- 'jinrishouyi', 'jinyueshouyi'
- ])
- except (User.DoesNotExist, AttributeError):
- return Response({'code': 400, 'msg': '打手不存在或扩展表缺失'})
-
- # 6. 如果是商家发单,更新商家成交订单数量
- if order.Platform == 2:
- try:
- # 通过 MerchantOrderExt 获取商家ID
- shangjia_ext = order.shangjia_kuozhan # 假设 related_name='shangjia_kuozhan'
- if shangjia_ext and shangjia_ext.shangjia_id:
- shangjia_user = User.query.get(UserUID=shangjia_ext.shangjia_id)
- shangjia_profile = shangjia_user.ShopProfile
- shangjia_profile.chengjiao = F('chengjiao') + 1
- shangjia_profile.save(update_fields=['chengjiao'])
-
- # (行为驱动,更安全)
- update_shangjia_daily(
- yonghuid=shangjia_ext.shangjia_id,
- amount=Decimal(str(order.Amount)),
- action=2, # 2 = 结算
- order_id=dingdan_id,
- )
-
- except ObjectDoesNotExist:
- # 没有商家扩展记录则忽略
- pass
-
- # 7. 更新订单状态
- order.Status = 3
- order.AssignedCS = phone
- order.save(update_fields=['Status', 'AssignedCS'])
-
-
- if order.Platform == 1:
-
- # ========== 新增:商品和店铺每日统计 ==========
- try:
- # 商品每日统计(只要有商品ID就执行)
- if order.ProductID:
- # 从扩展表获取已计算好的收益值(下单时已存储)
- pingtai_shouyi = Decimal('0.00')
- dianpu_shouyi = Decimal('0.00')
- if hasattr(order, 'pingtai_kuozhan'):
- pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
- dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
-
- update_shangpin_daily_stat(
- shangpin_id=order.ProductID,
- caozuo_leixing=2, # 2 = 结单
- dingdan_jiage=order.Amount,
- pingtai_shouyi=pingtai_shouyi,
- dianpu_zongshouyi=dianpu_shouyi
- )
- logger.info(f"商品统计更新完成,商品ID: {order.ProductID}")
-
- # 店铺每日统计(仅当存在店铺ID时执行)
- dianpu_id = None
- if hasattr(order, 'pingtai_kuozhan'):
- dianpu_id = order.pingtai_kuozhan.ShopID
- if dianpu_id:
- dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
- update_dianpu_daily_stat(
- dianpu_id=dianpu_id,
- caozuo_leixing=2, # 2 = 结单
- dingdan_jiage=order.Amount,
- dianpu_shouyi=dianpu_shouyi
- )
- logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
- except Exception as e:
- logger.error(f"商品/店铺统计更新失败: {e}")
- logger.error(traceback.format_exc())
- # 统计失败不影响主流程
- # =============================================
-
- # 🔥 新增:调用打手每日统计(成交)
- try:
- update_dashou_daily_by_action(
- yonghuid=order.PlayerID,
- amount=dashou_fencheng,
- action=2 # 2 表示成交(结算)
- )
- logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
- except Exception as e:
- logger.error(f"打手每日统计更新失败: {e}")
- # 不影响主流程
-
- # 商家订单管事分红
- if order.Platform == 2:
- settle_shangjia_order_guanshi_fenhong(order, dashou_id)
-
- # 8. 更新客服统计
- kefu.jinrichuli = F('jinrichuli') + 1
- kefu.jinyuechuli = F('jinyuechuli') + 1
- kefu.zongchuli = F('zongchuli') + 1
- kefu.save(update_fields=['jinrichuli', 'jinyuechuli', 'zongchuli'])
-
- return Response({'code': 0, 'msg': '强制结单成功'})
-
- except Order.DoesNotExist:
- return Response({'code': 404, 'msg': '订单不存在'})
- except Exception as e:
- logger.error(f"强制结单异常: {str(e)}", exc_info=True)
- return Response({'code': 500, 'msg': '服务器内部错误'})
-
-
-kefu_txsh_list_logger = logging.getLogger('yonghu.kefu_txsh_list')
-if not kefu_txsh_list_logger.handlers:
- _console_handler = logging.StreamHandler(sys.stderr)
- _console_handler.setFormatter(logging.Formatter(
- '[%(asctime)s] %(levelname)s %(name)s: %(message)s'
- ))
- kefu_txsh_list_logger.addHandler(_console_handler)
- kefu_txsh_list_logger.setLevel(logging.DEBUG)
- kefu_txsh_list_logger.propagate = False
-
-_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)
- total_count = base_qs.count()
- awaiting_count = self._list_queryset(q & Q(zhuangtai=1), shenhe_field_ok, request).count()
- processed_count = self._list_queryset(
- q & Q(zhuangtai__in=processed_statuses), shenhe_field_ok, request,
- ).count()
- agg = base_qs.aggregate(total=Sum('jine'))
- total_amount = agg.get('total') or 0
-
- stats = {
- 'total': total_count,
- 'awaiting': awaiting_count,
- 'processed': processed_count,
- 'total_amount': str(round(float(total_amount), 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)}})
-
-
-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.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': '处罚成功'})
diff --git a/users/views/kefu_base.py b/users/views/kefu_base.py
new file mode 100644
index 0000000..c3f1f45
--- /dev/null
+++ b/users/views/kefu_base.py
@@ -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. 返回成功数据(管理员无 KefuProfile,nicheng 用 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': []})
diff --git a/users/views/kefu_dashou.py b/users/views/kefu_dashou.py
new file mode 100644
index 0000000..a7c24cd
--- /dev/null
+++ b/users/views/kefu_dashou.py
@@ -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': '服务器内部错误'})
diff --git a/users/views/kefu_orders.py b/users/views/kefu_orders.py
new file mode 100644
index 0000000..5116ee0
--- /dev/null
+++ b/users/views/kefu_orders.py
@@ -0,0 +1,1831 @@
+"""users.views.kefu_orders - 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 KefuGetOrderListView(APIView):
+ """
+ 客服获取平台订单列表接口(发单平台=1,非跨平台)
+ 需要权限:002ab(平台订单管理)
+ 请求参数:
+ username: 客服账号(用于权限校验)
+ page: 页码
+ pageSize: 每页条数
+ dingdan_id: 订单ID(精确搜索)
+ laoban_id: 老板ID(搜索平台扩展表中的老板)
+ dashou_id: 打手ID(搜索主表中的接单打手)
+ leixing: 商品类型ID
+ zhuangtai: 状态(多个用逗号分隔,如 "1,7")
+ date: 日期(YYYY-MM-DD),筛选订单创建时间
+ nicheng: 游戏昵称(订单主表的 nicheng 字段)
+ jieshao: 订单介绍(订单主表的 jieshao 字段)
+ clkf: 处理客服(订单主表的 clkf 字段)
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ start_total = time.time()
+
+ # 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 '002ab' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
+
+ # 4. 获取分页和筛选参数
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('pageSize', 20))
+
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ laoban_id = request.data.get('laoban_id', '').strip()
+ dashou_id = request.data.get('dashou_id', '').strip()
+ leixing = request.data.get('leixing')
+ zhuangtai = request.data.get('zhuangtai', '')
+ date = request.data.get('date', '').strip()
+ # 新增筛选条件
+ nicheng = request.data.get('nicheng', '').strip()
+ jieshao = request.data.get('jieshao', '').strip()
+ clkf = request.data.get('clkf', '').strip()
+
+ # ---------- 查询条件 ----------
+ # 基础条件:平台发单 + 兼容历史 Platform 为空
+ from jituan.services.club_context import platform_order_base_q
+ base_q = platform_order_base_q()
+
+ if dingdan_id:
+ q_conditions = base_q & Q(OrderID=dingdan_id)
+ else:
+ q_conditions = base_q
+ if leixing is not None and str(leixing).strip() != '':
+ try:
+ leixing_int = int(leixing)
+ q_conditions &= Q(ProductTypeID=leixing_int)
+ except (ValueError, TypeError):
+ pass
+ if zhuangtai and zhuangtai != 'all':
+ try:
+ status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
+ if status_list:
+ q_conditions &= Q(Status__in=status_list)
+ except (ValueError, TypeError):
+ pass
+ if dashou_id:
+ q_conditions &= Q(PlayerID=dashou_id)
+ if laoban_id:
+ q_conditions &= Q(pingtai_kuozhan__BossID=laoban_id)
+ if date:
+ q_conditions &= Q(CreateTime__date=date)
+ # 新增筛选条件
+ if nicheng:
+ q_conditions &= Q(Nickname__icontains=nicheng)
+ if jieshao:
+ q_conditions &= Q(Description__icontains=jieshao)
+ if clkf:
+ q_conditions &= Q(AssignedCS__icontains=clkf)
+
+ from jituan.services.club_context import (
+ count_order_status_buckets,
+ order_scope_qs,
+ paginate_fluent_query,
+ )
+ from orders.models import PlatformOrderExt
+
+ # 按俱乐部过滤订单列表
+ stats_qs = order_scope_qs(request).filter(q_conditions)
+ stats = stats_qs.aggregate(
+ total_orders=Count('id'),
+ completed_orders=Count('id', filter=Q(Status=3)),
+ refund_orders=Count('id', filter=Q(Status=5)),
+ pending_orders=Count('id', filter=Q(Status__in=[4, 8]))
+ )
+ status_counts = count_order_status_buckets(q_conditions, request)
+
+ list_qs = order_scope_qs(request).filter(q_conditions).order_by('-CreateTime')
+ total_count, orders_page = paginate_fluent_query(list_qs, page, page_size)
+ if total_count > 0 and not orders_page:
+ start = (max(1, page) - 1) * page_size
+ orders_page = list(list_qs._qs[start:start + page_size])
+
+ order_ids = [o.OrderID for o in orders_page if o.OrderID]
+ ext_map = {}
+ if order_ids:
+ for ext in PlatformOrderExt.query.filter(Order__OrderID__in=order_ids).select_related('Order'):
+ if ext.Order:
+ ext_map[ext.Order.OrderID] = ext
+
+ laoban_ids = [
+ ext.BossID for ext in ext_map.values() if ext.BossID
+ ]
+ nickname_map = {}
+ if laoban_ids:
+ users = User.query.filter(UserUID__in=laoban_ids).select_related('BossProfile').only(
+ 'UserUID', 'BossProfile__nickname'
+ )
+ for user in users:
+ nickname_map[user.UserUID] = (user.BossProfile.nickname or '') if getattr(user, 'BossProfile', None) else ''
+
+ result_list = []
+ for order in orders_page:
+ ext = ext_map.get(order.OrderID)
+ laoban_id = ext.BossID if ext else ''
+ youxi_nicheng = nickname_map.get(laoban_id, order.Nickname or '')
+ result_list.append({
+ 'dingdan_id': order.OrderID or '',
+ 'jieshao': order.Description or '',
+ 'zhuangtai': order.Status,
+ 'leixing': order.ProductTypeID,
+ 'fadanpingtai': order.Platform,
+ 'jiage': float(order.Amount) if order.Amount else 0.00,
+ 'tupian_url': order.ImageURL or '',
+ 'fadanshijian': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '',
+ 'youxi_nicheng': youxi_nicheng
+ })
+
+ from jituan.services.club_user_access import list_response_meta
+
+ response_data = {
+ 'stats': {
+ 'totalOrders': stats['total_orders'] or 0,
+ 'completedOrders': stats['completed_orders'] or 0,
+ 'refundOrders': stats['refund_orders'] or 0,
+ 'pendingOrders': stats['pending_orders'] or 0
+ },
+ 'statusCounts': status_counts,
+ 'list': result_list,
+ 'total': total_count,
+ 'page': page,
+ 'pageSize': page_size,
+ **list_response_meta(request),
+ }
+
+ logger.info(f"平台订单接口总耗时: {time.time()-start_total:.3f}s")
+ return Response({'code': 0, 'data': response_data})
+
+
+class KefuGetShangjiaOrderListView(APIView):
+ """
+ 客服获取商家订单列表接口(发单平台=2,非跨平台)
+ 需要权限:002ac(商家订单管理)—— 如不区分可使用 002ab
+ 请求参数:
+ username: 客服账号(用于权限校验)
+ page: 页码
+ pageSize: 每页条数
+ dingdan_id: 订单ID(精确搜索)
+ shangjia_id: 商家ID(搜索商家扩展表中的商家ID)
+ dashou_id: 打手ID(搜索主表中的接单打手)
+ leixing: 商品类型ID
+ zhuangtai: 状态(多个用逗号分隔,如 "1,7")
+ date: 日期(YYYY-MM-DD),筛选订单创建时间
+ nicheng: 游戏昵称(订单主表的 nicheng 字段)
+ jieshao: 订单介绍(订单主表的 jieshao 字段)
+ clkf: 处理客服(订单主表的 clkf 字段)
+ shangjia_nicheng: 商家昵称(商家扩展表中的昵称)
+ """
+ permission_classes = [IsAuthenticated]
+ parser_classes = [JSONParser]
+
+ def post(self, request):
+ start_total = time.time()
+
+ # 1. 获取前端参数(兼容 phone / username)
+ username = request.data.get('username', '').strip() or 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. 检查商家订单管理权限(使用 002ac,如果没有可改用 002ab)
+ if '002ac' not in permissions:
+ return Response({'code': 403, 'msg': '您无权查看商家订单列表'})
+
+ # 4. 获取分页和筛选参数
+ page = int(request.data.get('page', 1))
+ page_size = int(request.data.get('pageSize', 20))
+
+ dingdan_id = request.data.get('dingdan_id', '').strip()
+ shangjia_id = request.data.get('shangjia_id', '').strip()
+ dashou_id = request.data.get('dashou_id', '').strip()
+ leixing = request.data.get('leixing')
+ zhuangtai = request.data.get('zhuangtai', '')
+ date = request.data.get('date', '').strip()
+ # 新增筛选条件
+ nicheng = request.data.get('nicheng', '').strip()
+ jieshao = request.data.get('jieshao', '').strip()
+ clkf = request.data.get('clkf', '').strip()
+ shangjia_nicheng = request.data.get('shangjia_nicheng', '').strip()
+
+ # ---------- 查询条件 ----------
+ from jituan.services.club_context import merchant_order_base_q
+ base_q = merchant_order_base_q()
+
+ if dingdan_id:
+ q_conditions = base_q & Q(OrderID=dingdan_id)
+ else:
+ q_conditions = base_q
+ if leixing is not None and str(leixing).strip() != '':
+ try:
+ leixing_int = int(leixing)
+ q_conditions &= Q(ProductTypeID=leixing_int)
+ except (ValueError, TypeError):
+ pass
+ if zhuangtai and zhuangtai != 'all':
+ try:
+ status_list = [int(s.strip()) for s in zhuangtai.split(',') if s.strip()]
+ if status_list:
+ q_conditions &= Q(Status__in=status_list)
+ except (ValueError, TypeError):
+ pass
+ if dashou_id:
+ q_conditions &= Q(PlayerID=dashou_id)
+ if shangjia_id:
+ q_conditions &= Q(shangjia_kuozhan__MerchantID=shangjia_id)
+ if date:
+ q_conditions &= Q(CreateTime__date=date)
+ if nicheng:
+ q_conditions &= Q(Nickname__icontains=nicheng)
+ if jieshao:
+ q_conditions &= Q(Description__icontains=jieshao)
+ if clkf:
+ q_conditions &= Q(AssignedCS__icontains=clkf)
+ if shangjia_nicheng:
+ # 通过商家扩展表关联 UserShangjia 的昵称
+ q_conditions &= Q(shangjia_kuozhan__MerchantID__in=User.query.filter(
+ ShopProfile__nicheng__icontains=shangjia_nicheng
+ ).values_list('UserUID', flat=True))
+
+ from jituan.services.club_context import (
+ count_order_status_buckets,
+ order_scope_qs,
+ paginate_fluent_query,
+ )
+ from orders.models import MerchantOrderExt
+
+ stats_qs = order_scope_qs(request).filter(q_conditions)
+ stats = stats_qs.aggregate(
+ total_orders=Count('id'),
+ completed_orders=Count('id', filter=Q(Status=3)),
+ refund_orders=Count('id', filter=Q(Status=5)),
+ pending_orders=Count('id', filter=Q(Status__in=[4, 8]))
+ )
+ status_counts = count_order_status_buckets(q_conditions, request)
+
+ list_qs = order_scope_qs(request).filter(q_conditions).order_by('-CreateTime')
+ total_count, orders_page = paginate_fluent_query(list_qs, page, page_size)
+ if total_count > 0 and not orders_page:
+ start = (max(1, page) - 1) * page_size
+ orders_page = list(list_qs._qs[start:start + page_size])
+
+ order_ids = [o.OrderID for o in orders_page if o.OrderID]
+ ext_map = {}
+ if order_ids:
+ for ext in MerchantOrderExt.query.filter(Order__OrderID__in=order_ids).select_related('Order'):
+ if ext.Order:
+ ext_map[ext.Order.OrderID] = ext
+
+ shangjia_ids = [
+ ext.MerchantID for ext in ext_map.values() if ext.MerchantID
+ ]
+ nickname_map = {}
+ if shangjia_ids:
+ users = User.query.filter(UserUID__in=shangjia_ids).select_related('ShopProfile').only(
+ 'UserUID', 'ShopProfile__nicheng'
+ )
+ for user in users:
+ nickname_map[user.UserUID] = user.ShopProfile.nicheng or '' if hasattr(user, 'ShopProfile') else ''
+
+ result_list = []
+ for order in orders_page:
+ ext = ext_map.get(order.OrderID)
+ shangjia_id = ext.MerchantID if ext else ''
+ youxi_nicheng = order.Nickname or ''
+ result_list.append({
+ 'dingdan_id': order.OrderID or '',
+ 'jieshao': order.Description or '',
+ 'zhuangtai': order.Status,
+ 'leixing': order.ProductTypeID,
+ 'fadanpingtai': order.Platform,
+ 'jiage': float(order.Amount) if order.Amount else 0.00,
+ 'tupian_url': order.ImageURL or '',
+ 'fadanshijian': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '',
+ 'youxi_nicheng': youxi_nicheng
+ })
+
+ from jituan.services.club_user_access import list_response_meta
+
+ response_data = {
+ 'stats': {
+ 'totalOrders': stats['total_orders'] or 0,
+ 'completedOrders': stats['completed_orders'] or 0,
+ 'refundOrders': stats['refund_orders'] or 0,
+ 'pendingOrders': stats['pending_orders'] or 0
+ },
+ 'statusCounts': status_counts,
+ 'list': result_list,
+ 'total': total_count,
+ 'page': page,
+ 'pageSize': page_size,
+ **list_response_meta(request),
+ }
+
+ logger.info(f"商家订单接口总耗时: {time.time()-start_total:.3f}s")
+ return Response({'code': 0, 'data': response_data})
+
+
+class KefuChangeDashouView(APIView):
+ """
+ 客服更换打手接口
+ 请求:POST /yonghu/kefughds
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "new_dashou_id": "新打手ID"
+ }
+ 认证: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()
+ new_dashou_id = request.data.get('new_dashou_id', '').strip()
+
+ if not phone or not dingdan_id or not new_dashou_id:
+ 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:
+ order = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 验证订单状态是否允许更换打手(进行中2 或 结算中8)
+ if order.Status not in [2, 8]:
+ return Response({'code': 400, 'msg': '订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 查询新打手
+ try:
+ new_dashou_user = User.query.select_related('DashouProfile').get(UserUID=new_dashou_id)
+ except User.DoesNotExist:
+ return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 6. 验证打手扩展表
+ try:
+ new_dashou_profile = new_dashou_user.DashouProfile
+ except ObjectDoesNotExist:
+ return Response({'code': 404, 'msg': '打手信息不完整'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 7. 验证打手状态(打手状态和账号状态必须为1)
+ if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1:
+ return Response({'code': 400, 'msg': '打手状态不允许接单'}, status=status.HTTP_400_BAD_REQUEST)
+ # 8. 验证打手积分是否足够(至少5分)
+ if new_dashou_profile.jifen < 5:
+ return Response({'code': 400, 'msg': '打手积分不足,无法接单'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 8. 根据订单的抢单要求类型进行额外验证
+ yaoqiuleixing = order.GrabRequirement
+
+ if yaoqiuleixing == 1: # 会员抢单
+ huiyuan_id = order.MembershipID
+ if not huiyuan_id:
+ return Response({'code': 400, 'msg': '订单会员ID为空'}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ # 查询打手的会员购买记录
+ huiyuan_goumai = Huiyuangoumai.query.get(
+ huiyuan_id=huiyuan_id,
+ yonghu_id=new_dashou_id # 假设 yonghu_id 字段存储用户ID
+ )
+ except Huiyuangoumai.DoesNotExist:
+ return Response({'code': 400, 'msg': '打手未购买该订单要求的会员'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 检查会员是否过期(调用模型方法)
+ is_expired = huiyuan_goumai.jiance_shifou_daoqi()
+ if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1:
+ return Response({'code': 400, 'msg': '打手的会员已过期'}, status=status.HTTP_400_BAD_REQUEST)
+
+ elif yaoqiuleixing == 2: # 押金抢单
+ order_yajin = order.CommissionReq # 假设订单的 yongjin 字段存储押金要求
+ if order_yajin is None:
+ return Response({'code': 400, 'msg': '订单押金为空'}, status=status.HTTP_400_BAD_REQUEST)
+
+ if new_dashou_profile.yajin < order_yajin:
+ return Response({'code': 400, 'msg': '打手押金不足'}, status=status.HTTP_400_BAD_REQUEST)
+ else:
+ return Response({'code': 400, 'msg': '订单抢单要求类型错误'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 9. 所有验证通过,开始更换打手(使用事务)
+ with transaction.atomic():
+ # 9.1 更新新打手扩展表
+ new_dashou_profile.zhuangtai = 0 # 设为接单中
+ new_dashou_profile.jiedanzongliang += 1
+ new_dashou_profile.jinrijiedan += 1
+ new_dashou_profile.jinyuejiedan += 1
+ new_dashou_profile.save()
+
+ # 9.2 处理原打手(如果存在)
+ old_dashou_id = order.PlayerID
+ if old_dashou_id:
+ try:
+ old_dashou_user = User.query.get(UserUID=old_dashou_id)
+ old_dashou_profile = old_dashou_user.DashouProfile
+ old_dashou_profile.zhuangtai = 1 # 恢复空闲
+ old_dashou_profile.tuikuanliang += 1 # 原打手退款总量+1(业务逻辑)
+ old_dashou_profile.save()
+ logger.info(f"原打手 {old_dashou_id} 状态已更新")
+ except (User.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"原打手 {old_dashou_id} 不存在或信息不完整,跳过更新")
+ except Exception as e:
+ logger.error(f"更新原打手时异常: {str(e)}", exc_info=True)
+
+ # 9.3 更新订单
+ order.PlayerID = new_dashou_id
+ order.Status = 2 # 进行中
+ order.AssignedCS = current_user.UserUID # 记录处理客服ID
+ order.save()
+
+ return Response({'code': 0, 'msg': '更换打手成功'})
+
+
+class KefuRecoverOrderView(APIView):
+ """
+ 客服恢复订单接口(仅状态9可恢复)
+ 请求:POST /yonghu/kefuhf
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID"
+ }
+ 认证: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()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ 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
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ 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. 验证订单状态是否为9(未付款)
+ if order.Status != 9:
+ return Response({'code': 400, 'msg': '订单状态不允许恢复'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 根据是否有指定ID决定新状态
+ if order.AssignedID:
+ new_status = 7 # 指定中
+ else:
+ new_status = 1 # 下单中
+
+ # 6. 更新订单
+ order.Status = new_status
+ order.AssignedCS = current_user.UserUID
+ order.save()
+
+ return Response({'code': 0, 'msg': '恢复成功', 'data': {'new_status': new_status}})
+
+
+class KefuPlatformRefundView(APIView):
+ """
+ 客服平台订单退款接口(调用微信支付)
+ 请求:POST /yonghu/kefupttk
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "tuikuan_liyou": "退款理由(可选)"
+ }
+ 认证:JWT
+ 返回:code=0 成功,code=其他 失败
+ """
+ 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()
+ tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
+
+ if not phone or not dingdan_id:
+ 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:
+ with transaction.atomic():
+ order = Order.query.select_related('pingtai_kuozhan').select_for_update().get(OrderID=dingdan_id)
+
+ # 4. 验证平台订单
+ if order.Platform != 1:
+ return Response({'code': 400, 'msg': '该订单不是平台订单'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 验证状态允许退款(根据模型,状态值:1下单中,2进行中,3已完成,4退款审核,5已退款,6退款失败,7指定中,8结算中)
+ allowed_statuses = [1, 2, 4, 7, 8] # 允许退款的已支付或处理中状态
+ if order.Status not in allowed_statuses:
+ return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 6. 获取微信交易号(如果存在)
+ wechat_transaction_id = getattr(order, 'WechatTransactionID', None)
+
+ # 7. 调用微信退款接口
+ refund_result = self.call_wechat_refund(
+ dingdan_id=order.OrderID,
+ jine=order.Amount,
+ transaction_id=wechat_transaction_id
+ )
+
+ if refund_result['code'] != 0:
+ # 微信退款失败,直接返回错误,数据库不更新
+ logger.error(f"微信退款失败,订单 {dingdan_id},原因:{refund_result['msg']}")
+ return Response({'code': 400, 'msg': refund_result['msg']}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 8. 更新订单状态为5(已退款)
+ order.Status = 5
+ order.AssignedCS = current_user.Phone # 记录处理客服手机号
+ if tuikuan_liyou:
+ order.RefundReason = tuikuan_liyou
+ order.save()
+
+ # 9. 更新打手数据
+ jiedan_dashou_id = order.PlayerID
+ if jiedan_dashou_id:
+ try:
+ dashou_user = User.query.get(UserUID=jiedan_dashou_id)
+ dashou_profile = dashou_user.DashouProfile
+ dashou_profile.tuikuanliang += 1
+ dashou_profile.zhuangtai = 1 # 打手状态设为空闲
+ dashou_profile.save()
+ logger.info(f"平台退款:打手 {jiedan_dashou_id} 退款订单+1")
+ except (User.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"平台退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
+
+ # ========== 新增:商品和店铺每日统计 ==========
+ try:
+ # 商品每日统计(只要有商品ID就执行)
+ if order.ProductID:
+ # 从扩展表获取已计算好的收益值(下单时已存储)
+ pingtai_shouyi = Decimal('0.00')
+ dianpu_shouyi = Decimal('0.00')
+ if hasattr(order, 'pingtai_kuozhan'):
+ pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
+ dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
+
+ update_shangpin_daily_stat(
+ shangpin_id=order.ProductID,
+ caozuo_leixing=3, # 3 = 退款
+ dingdan_jiage=order.Amount,
+ pingtai_shouyi=pingtai_shouyi,
+ dianpu_zongshouyi=dianpu_shouyi
+ )
+ logger.info(f"商品统计更新完成,商品ID: {order.ProductID}")
+
+ # 店铺每日统计(仅当存在店铺ID时执行)
+ dianpu_id = None
+ if hasattr(order, 'pingtai_kuozhan'):
+ dianpu_id = order.pingtai_kuozhan.ShopID
+ if dianpu_id:
+ dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
+ update_dianpu_daily_stat(
+ dianpu_id=dianpu_id,
+ caozuo_leixing=3, # 3 = 退款
+ dingdan_jiage=order.Amount,
+ dianpu_shouyi=dianpu_shouyi
+ )
+ logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
+ except Exception as e:
+ logger.error(f"商品/店铺统计更新失败: {e}")
+ logger.error(traceback.format_exc())
+ # 统计失败不影响主流程
+ # =============================================
+
+ # 10. 更新退款记录表(假设存在 RefundRecord 模型,字段需根据实际调整)
+
+ #更新支出记录
+ update_daily_payout(order.Amount, getattr(order, 'ClubID', None))
+
+
+ try:
+ refund_record, created = RefundRecord.query.get_or_create(
+ OrderID=dingdan_id,
+ defaults={
+ 'PlayerID': jiedan_dashou_id or '',
+ 'ApplicantID': '',
+ 'ProcessorID': phone,
+ 'Reason': tuikuan_liyou or '客服退款',
+ 'ApplyStatus': 1, # 成功
+ 'Amount': order.Amount,
+ 'PlayerCommission': order.PlayerCommission or 0,
+ 'Description': order.Description or '',
+ 'Remark': '客服平台退款',
+ 'Nickname': order.Nickname or '',
+ }
+ )
+ if not created:
+ refund_record.ApplyStatus = 1
+ refund_record.ProcessorID = phone
+ if tuikuan_liyou:
+ refund_record.Reason = tuikuan_liyou
+ refund_record.save()
+ except Exception as e:
+ logger.error(f"平台退款更新退款记录异常: {str(e)}", exc_info=True)
+
+ # 11. 更新客服统计数据
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.PlayerID,
+ amount=order.PlayerCommission,
+ action=3 # 3 表示退款(退款)
+ )
+ logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+ logger.info(f"平台退款成功,订单 {dingdan_id},退款单号 {refund_result.get('refund_id')}")
+ return Response({'code': 0, 'msg': '退款成功'})
+
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+ except Exception as e:
+ logger.error(f"平台退款接口异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+ def call_wechat_refund(self, dingdan_id, jine, transaction_id=None):
+ """
+ 调用微信支付退款接口
+ :param dingdan_id: 商户订单号(我们的订单ID)
+ :param jine: 退款金额
+ :param transaction_id: 微信支付订单号(优先使用)
+ :return: {'code': 0, 'msg': '成功', 'refund_id': 'xxx'} 或 {'code': 非0, 'msg': '错误信息'}
+ """
+ # 获取配置
+ appid = getattr(settings, 'WEIXIN_APPID', '')
+ mch_id = getattr(settings, 'WEIXIN_MCHID', '')
+ key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '')
+ cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '')
+ key_path = getattr(settings, 'WEIXIN_KEY_PATH', '')
+
+ if not all([appid, mch_id, key, cert_path, key_path]):
+ missing = [k for k, v in {'appid': appid, 'mch_id': mch_id, 'key': key, 'cert_path': cert_path, 'key_path': key_path}.items() if not v]
+ logger.error(f"微信支付配置缺失: {missing}")
+ return {'code': 500, 'msg': f'系统支付配置缺失: {missing}'}
+
+ # 生成退款单号
+ out_refund_no = self.generate_refund_no(dingdan_id)
+ total_fee = yuan_to_fen(jine)
+ refund_fee = total_fee
+ refund_desc = '客服退款'
+
+ # 构造请求参数
+ params = {
+ 'appid': appid,
+ 'mch_id': mch_id,
+ 'nonce_str': self.generate_nonce_str(),
+ 'out_refund_no': out_refund_no,
+ 'total_fee': total_fee,
+ 'refund_fee': refund_fee,
+ 'refund_desc': refund_desc,
+ }
+ if transaction_id:
+ params['transaction_id'] = transaction_id
+ else:
+ params['out_trade_no'] = dingdan_id # 使用订单ID作为商户订单号
+
+ # 生成签名
+ sorted_keys = sorted(params.keys())
+ stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys])
+ stringSignTemp = f"{stringA}&key={key}"
+ sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper()
+ params['sign'] = sign
+
+ xml_data = self.dict_to_xml(params)
+ url = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
+
+ try:
+ response = requests.post(
+ url,
+ data=xml_data,
+ cert=(cert_path, key_path),
+ timeout=10
+ )
+ result = xmltodict.parse(response.content)['xml']
+ logger.info(f"微信退款响应: {result}")
+ except Exception as e:
+ logger.error(f"微信退款请求异常: {str(e)}", exc_info=True)
+ return {'code': 500, 'msg': '微信退款请求异常'}
+
+ if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
+ return {'code': 0, 'msg': 'success', 'refund_id': out_refund_no}
+ else:
+ err_msg = result.get('err_code_des', result.get('return_msg', '未知错误'))
+ logger.warning(f"微信退款失败: {err_msg}")
+ return {'code': 400, 'msg': err_msg}
+
+ def generate_nonce_str(self):
+ return ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+
+ def generate_refund_no(self, dingdan_id):
+ timestamp = str(int(time.time()))
+ rand = ''.join(random.choices('0123456789', k=4))
+ return f"{dingdan_id}REF{timestamp}{rand}"
+
+ def dict_to_xml(self, data):
+ xml = ['']
+ for k, v in data.items():
+ xml.append(f'<{k}>{v}{k}>')
+ xml.append('')
+ return ''.join(xml)
+
+
+class KefuRejectRefundView(APIView):
+ """
+ 客服拒绝退款接口
+ 请求:POST /yonghu/kefujjtk
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "jujue_liyou": "拒绝理由(可选)"
+ }
+ 认证: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()
+ jujue_liyou = request.data.get('jujue_liyou', '').strip()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证
+ 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
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 3. 查询订单并加锁
+ try:
+ with transaction.atomic():
+ order = Order.objects.select_for_update().get(OrderID=dingdan_id)
+
+ # 4. 验证订单状态为退款审核中(4)
+ if order.Status != 4:
+ return Response({'code': 400, 'msg': '订单状态不是退款审核中'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取必要字段
+ jiedan_dashou_id = order.PlayerID
+ dashou_fencheng = order.PlayerCommission
+ fadan_pingtai = order.Platform
+
+ # 5. 验证打手分成金额是否存在
+ if dashou_fencheng is None:
+ return Response({'code': 400, 'msg': '订单缺少打手分成金额'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 6. 更新订单状态为退款失败(6)
+ order.Status = 6
+ order.AssignedCS = current_user.Phone # 记录处理客服手机号
+ if jujue_liyou:
+ order.RefundReason = jujue_liyou
+ order.save()
+
+ # 7. 给打手结算(返还分成)
+ if jiedan_dashou_id:
+ try:
+ dashou_user = User.query.get(UserUID=jiedan_dashou_id)
+ dashou_profile = dashou_user.DashouProfile
+ # 增加打手相关统计数据
+ dashou_profile.chengjiaozongliang += 1
+ dashou_profile.yue += dashou_fencheng
+ dashou_profile.zonge += dashou_fencheng
+ dashou_profile.jinrishouyi += dashou_fencheng
+ dashou_profile.jinyueshouyi += dashou_fencheng
+ dashou_profile.save()
+ logger.info(f"拒绝退款:打手 {jiedan_dashou_id} 获得分成 {dashou_fencheng}")
+ except (User.DoesNotExist, AttributeError):
+ logger.warning(f"拒绝退款:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过结算")
+
+ # 8. 如果是商家发单,更新商家成交订单数量
+ if fadan_pingtai == 2:
+ try:
+ # 获取商家订单扩展表
+ shangjia_kuozhan = order.shangjia_kuozhan
+ shangjia_id = shangjia_kuozhan.MerchantID
+ if shangjia_id:
+ shangjia_user = User.query.get(UserUID=shangjia_id)
+ shangjia_profile = shangjia_user.ShopProfile
+ shangjia_profile.chengjiao += 1
+ shangjia_profile.save()
+ logger.info(f"拒绝退款:商家 {shangjia_id} 成交订单+1")
+
+ # ✅ 新方法(行为驱动,更安全)
+ update_shangjia_daily(
+ yonghuid=shangjia_id,
+ amount=Decimal(str(order.Amount)),
+ action=2, # 2 = 结算
+ order_id=dingdan_id,
+ )
+ except (ObjectDoesNotExist, User.DoesNotExist, AttributeError):
+ logger.warning(f"拒绝退款:商家信息不存在,跳过商家更新")
+
+ # 9. 更新退款记录表
+ try:
+ refund_record = RefundRecord.query.get(OrderID=dingdan_id)
+ refund_record.ApplyStatus = 2 # 2表示拒绝
+ refund_record.ProcessorID = phone
+ refund_record.RejectReason = jujue_liyou # 记录驳回理由(假设模型有此字段)
+ refund_record.save()
+ logger.info(f"拒绝退款:更新退款记录表,订单 {dingdan_id}")
+ except RefundRecord.DoesNotExist:
+ # 如果退款记录不存在,可以创建一条记录(可选)
+ RefundRecord.query.create(
+ OrderID=dingdan_id,
+ PlayerID=jiedan_dashou_id or '',
+ ApplicantID='',
+ ProcessorID=phone,
+ Reason=jujue_liyou or '客服拒绝退款',
+ ApplyStatus=2,
+ Amount=order.Amount,
+ PlayerCommission=dashou_fencheng or 0,
+ Description=order.Description or '',
+ Remark='客服拒绝退款',
+ Nickname=order.Nickname or '',
+ RejectReason=jujue_liyou or '',
+ )
+ except Exception as e:
+ logger.error(f"拒绝退款更新退款记录异常: {str(e)}", exc_info=True)
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.PlayerID,
+ amount=dashou_fencheng,
+ action=2 # 2 表示成交(结算)
+ )
+ logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+ # 10. 更新客服统计数据
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ return Response({'code': 0, 'msg': '拒绝退款成功'})
+
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+ except Exception as e:
+ logger.error(f"拒绝退款接口异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+class KefuMerchantRefundView(APIView):
+ """
+ 客服商家订单退款接口(发单平台=2)
+ 请求:POST /yonghu/kefusjtk
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "tuikuan_liyou": "退款理由(可选)"
+ }
+ 认证: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()
+ tuikuan_liyou = request.data.get('tuikuan_liyou', '').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:
+ 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
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
+
+
+ # 查询订单,预加载商家扩展表
+ try:
+ order = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 验证商家订单
+ if order.Platform != 2:
+ return Response({'code': 400, 'msg': '该订单不是商家订单'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 验证状态允许退款
+ allowed_statuses = [1, 7, 2, 8, 4]
+ if order.Status not in allowed_statuses:
+ return Response({'code': 400, 'msg': '订单状态不允许退款'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 获取必要数据
+ jiedan_dashou_id = order.PlayerID
+ jine = order.Amount
+ if jine is None:
+ return Response({'code': 400, 'msg': '订单金额缺失'}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ shangjia_kuozhan = order.shangjia_kuozhan
+ shangjia_id = shangjia_kuozhan.MerchantID
+ except ObjectDoesNotExist:
+ return Response({'code': 400, 'msg': '订单商家信息不存在'}, status=status.HTTP_400_BAD_REQUEST)
+
+ with transaction.atomic():
+ # 更新订单
+ order.Status = 5
+ order.AssignedCS = current_user.Phone
+ if tuikuan_liyou:
+ order.RefundReason = tuikuan_liyou
+ order.save()
+
+ # 更新打手
+ if jiedan_dashou_id:
+ try:
+ dashou_user = User.query.get(UserUID=jiedan_dashou_id)
+ dashou_profile = dashou_user.DashouProfile
+ dashou_profile.tuikuanliang += 1
+ dashou_profile.zhuangtai = 1
+ dashou_profile.save()
+ except (User.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"商家退款:打手 {jiedan_dashou_id} 不存在,跳过更新")
+
+ # 更新商家(客服额度出资的订单不退商家余额,仅释放子客服额度)
+ try:
+ from merchant_ops.services.wallet import is_staff_quota_order
+ shangjia_user = User.query.get(UserUID=shangjia_id)
+ shangjia_profile = shangjia_user.ShopProfile
+ shangjia_profile.tuikuan += 1
+ if not is_staff_quota_order(dingdan_id):
+ shangjia_profile.yue += jine
+ shangjia_profile.save()
+ except (User.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"商家退款:商家 {shangjia_id} 不存在,跳过更新")
+
+ # 更新退款记录表
+ try:
+ refund_record = RefundRecord.query.get(OrderID=dingdan_id)
+ refund_record.ApplyStatus = 1
+ refund_record.ProcessorID = phone
+ refund_record.save()
+ except RefundRecord.DoesNotExist:
+ pass
+
+ # 更新客服统计
+ update_shangjia_daily(
+ yonghuid=shangjia_id,
+ amount=Decimal(str(order.Amount)),
+ action=3, # 3 = 退款
+ order_id=dingdan_id,
+ )
+
+
+ # 调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.PlayerID,
+ amount=order.PlayerCommission,
+ action=3 # 3 表示退款(退款)
+ )
+ logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+
+
+ # 管理员无客服扩展表,跳过计数器更新
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ if not is_admin:
+ kefu = current_user.KefuProfile
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ return Response({'code': 0, 'msg': '退款成功'})
+
+
+class KefuRejectSettlementView(APIView):
+ """
+ 客服拒绝结算接口(仅平台订单,状态2/8)
+ 请求:POST /yonghu/kefujjjs
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID",
+ "tkly": "拒绝结算理由(必填)"
+ }
+ 认证: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()
+ tkly = request.data.get('tkly', '').strip()
+
+ if not phone or not dingdan_id or not tkly:
+ 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:
+ order = Order.query.select_related('pingtai_kuozhan').get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 校验订单条件:平台发单(1)且状态为2或8
+ if order.Platform != 1 or order.Status not in [2, 8]:
+ return Response({'code': 400, 'msg': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算'}, status=status.HTTP_400_BAD_REQUEST)
+
+ jiedan_dashou_id = order.PlayerID
+ if not jiedan_dashou_id:
+ return Response({'code': 400, 'msg': '订单暂无接单打手'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 使用事务更新
+ with transaction.atomic():
+ # 5.1 更新订单主表
+ order.Status = 5 # 已退款
+ order.RefundReason = tkly # 拒绝理由
+ order.AssignedCS = phone # 记录客服手机号
+ order.save()
+
+ # 5.2 更新平台订单扩展表:标记为拒绝结算(假设字段 jjjs_biaoshi)
+ # 注意:需要确保 PlatformOrderExt 模型中已添加 jjjs_biaoshi 字段,IntegerField,默认0
+ if hasattr(order, 'pingtai_kuozhan') and order.pingtai_kuozhan:
+ pingtai_ext = order.pingtai_kuozhan
+ pingtai_ext.RejectSettlementFlag = 1 # 1表示拒绝结算
+ pingtai_ext.save()
+ else:
+ # 理论上应该存在,但若不存在则记录日志
+ logger.warning(f"订单 {dingdan_id} 缺少平台扩展表,无法标记拒绝结算")
+
+ # 5.3 更新打手扩展表
+ try:
+ dashou_user = User.query.get(UserUID=jiedan_dashou_id)
+ dashou_profile = dashou_user.DashouProfile
+ dashou_profile.zhuangtai = 1 # 打手状态设为空闲
+ dashou_profile.tuikuanliang += 1 # 退款订单总量+1
+ dashou_profile.save()
+ logger.info(f"拒绝结算:打手 {jiedan_dashou_id} 退款订单+1,状态空闲")
+ except (User.DoesNotExist, ObjectDoesNotExist):
+ logger.warning(f"拒绝结算:打手 {jiedan_dashou_id} 不存在或扩展表缺失,跳过更新")
+ except Exception as e:
+ logger.error(f"拒绝结算更新打手异常: {str(e)}", exc_info=True)
+
+ # 5.4 更新客服扩展表统计数据
+
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.PlayerID,
+ amount=order.PlayerCommission,
+ action=3 # 3 表示退款(退款)
+ )
+ logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+
+
+ kefu.jinrichuli += 1
+ kefu.jinyuechuli += 1
+ kefu.zongchuli += 1
+ kefu.save()
+
+ return Response({'code': 0, 'msg': '拒绝结算成功'})
+
+
+class KefuTransferHallView(APIView):
+ """
+ 客服恢复大厅(转移大厅)接口
+ 请求:POST /yonghu/kefuzydt
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID"
+ }
+ 认证: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()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证(同前,略,但必须完整实现)
+ 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)
+
+ # 3. 查询订单
+ try:
+ order = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 校验订单状态(允许状态:2,8,4)
+ if order.Status not in [2, 8, 4]:
+ return Response({'code': 400, 'msg': '订单状态不允许转移大厅'}, status=status.HTTP_400_BAD_REQUEST)
+
+
+ if order.PlayerID:
+ UserDashou.query.filter(user__UserUID=order.PlayerID).update(zhuangtai=1)
+ else:
+ pass
+
+ # 5. 根据是否有指定ID决定新状态
+ if order.AssignedID:
+ new_status = 7 # 指定中
+ else:
+ new_status = 1 # 下单中
+
+ # 6. 使用事务更新
+ with transaction.atomic():
+ # 清空接单打手ID,更新状态,记录客服
+ order.PlayerID = None
+ order.Status = new_status
+ order.AssignedCS = phone
+ order.save()
+
+ return Response({'code': 0, 'msg': '转移大厅成功'})
+
+
+class KefuCancelDesignationView(APIView):
+ """
+ 客服取消指定接口(仅状态7)
+ 请求:POST /yonghu/kefuqxzd
+ 参数:{
+ "phone": "客服手机号",
+ "dingdan_id": "订单ID"
+ }
+ 认证: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()
+
+ if not phone or not dingdan_id:
+ return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ # 2. 客服身份验证(同前,略,但必须完整实现)
+ 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)
+
+ # 3. 查询订单
+ try:
+ order = Order.query.get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
+
+ # 4. 校验订单状态为7(指定中)
+
+ if order.Status != 7:
+ print(999)
+ return Response({'code': 400, 'msg': '仅状态为指定中(7)的订单可取消指定'}, status=status.HTTP_400_BAD_REQUEST)
+
+ # 5. 使用事务更新
+ with transaction.atomic():
+ # 状态改为1(下单中),清空指定ID,记录客服
+ order.Status = 1
+ order.AssignedID = None
+ order.AssignedCS = phone
+ order.save()
+
+ return Response({'code': 0, 'msg': '取消指定成功'})
+
+
+class KefuGetOrderDetailView(APIView):
+ 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)
+
+ # 2. 获取当前用户
+ 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 Exception as e:
+ logger.exception(f"[kefuhqddxq] 获取客服扩展表异常 user={getattr(current_user, 'yonghuid', '?')}")
+ return Response({'code': 500, 'msg': '服务器错误'}, status=500)
+
+ # 3. 查询订单
+ try:
+ dingdan_obj = Order.query.select_related(
+ 'pingtai_kuozhan', 'shangjia_kuozhan'
+ ).get(OrderID=dingdan_id)
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'}, status=404)
+ except Exception as e:
+ logger.exception(f"[kefuhqddxq] 订单查询异常 dingdan_id={dingdan_id}")
+ return Response({'code': 500, 'msg': '服务器错误'}, status=500)
+
+ from jituan.services.club_user_access import forbid_if_order_out_of_scope
+ deny = forbid_if_order_out_of_scope(request, dingdan_obj)
+ if deny:
+ return deny
+
+ # 4. 构建基础数据
+ try:
+ response_data = {
+ 'dingdan_id': dingdan_obj.OrderID or '',
+ 'zhuangtai': dingdan_obj.Status or 0,
+ 'fadanpingtai': dingdan_obj.Platform or 0,
+ 'jiage': float(dingdan_obj.Amount) if dingdan_obj.Amount else 0.00,
+ 'dashou_fencheng': float(dingdan_obj.PlayerCommission) if dingdan_obj.PlayerCommission else 0.00,
+ 'jiedan_dashou_id': dingdan_obj.PlayerID or '',
+ 'dashou_liuyan': dingdan_obj.PlayerRemark or '',
+ 'zhiding_id': dingdan_obj.AssignedID or '',
+ 'shangpin_id': dingdan_obj.ProductID or '',
+ 'leixing': dingdan_obj.ProductTypeID or 0,
+ 'tupian_url': dingdan_obj.ImageURL or '',
+ 'jieshao': dingdan_obj.Description or '',
+ 'beizhu': dingdan_obj.Remark or '',
+ 'tkly': dingdan_obj.RefundReason or '',
+ 'nicheng': dingdan_obj.Nickname or '',
+ 'clkf': dingdan_obj.AssignedCS or '',
+ 'chuangjianshijian': dingdan_obj.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.CreateTime else '',
+ 'genggaishijian': dingdan_obj.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan_obj.UpdateTime else '',
+ }
+ except Exception as e:
+ logger.exception(f"[kefuhqddxq] 构建基础数据异常 dingdan_id={dingdan_id}")
+ return Response({'code': 500, 'msg': '服务器错误'}, status=500)
+
+ # 5. 扩展表数据
+ try:
+ fadan_pingtai = dingdan_obj.Platform
+ if fadan_pingtai == 1:
+ if hasattr(dingdan_obj, 'pingtai_kuozhan') and dingdan_obj.pingtai_kuozhan:
+ pingtai = dingdan_obj.pingtai_kuozhan
+ response_data['pingtai_kuozhan'] = {
+ 'laoban_id': pingtai.BossID or '',
+ 'laoban_pingjia': pingtai.BossReview or '',
+ }
+ else:
+ response_data['pingtai_kuozhan'] = {'laoban_id': '', 'laoban_pingjia': ''}
+ elif fadan_pingtai == 2:
+ if hasattr(dingdan_obj, 'shangjia_kuozhan') and dingdan_obj.shangjia_kuozhan:
+ shangjia = dingdan_obj.shangjia_kuozhan
+ response_data['shangjia_kuozhan'] = {
+ 'shangjia_id': shangjia.MerchantID or '',
+ 'sjnicheng': shangjia.MerchantNickname or '',
+ 'shangjia_pingjia': shangjia.MerchantReview or '',
+ 'cfliyou': shangjia.PenaltyReason or '',
+ 'sqzhuangtai': shangjia.PenaltyApplyStatus if shangjia.PenaltyApplyStatus is not None else 0,
+ 'bhliyou': shangjia.RejectReason or '',
+ }
+ else:
+ response_data['shangjia_kuozhan'] = {
+ 'shangjia_id': '', 'sjnicheng': '', 'shangjia_pingjia': '',
+ 'cfliyou': '', 'sqzhuangtai': 0, 'bhliyou': ''
+ }
+ except Exception as e:
+ logger.exception(f"[kefuhqddxq] 扩展数据异常 dingdan_id={dingdan_id}")
+ return Response({'code': 500, 'msg': '服务器错误'}, status=500)
+
+ # 6. 打手图片
+ try:
+ dashou_images = PlayerDeliveryImage.query.filter(OrderID=dingdan_id).values_list('ImageURL', flat=True)
+ response_data['dashou_images'] = [img for img in dashou_images if img]
+ except Exception as e:
+ logger.exception(f"[kefuhqddxq] 打手图片查询异常 dingdan_id={dingdan_id}")
+ response_data['dashou_images'] = []
+
+ # 7. 退款理由
+ try:
+ tuikuan_record = RefundRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first()
+ response_data['tuikuan_liyou'] = tuikuan_record.Reason if tuikuan_record and tuikuan_record.Reason else ''
+ except Exception as e:
+ logger.exception(f"[kefuhqddxq] 退款理由查询异常 dingdan_id={dingdan_id}")
+ response_data['tuikuan_liyou'] = ''
+
+ # 8. 指定打手响应
+ ZHIDING_HF_TEXT = {1: '想接', 2: '不想接', 3: '等会接'}
+ hf = getattr(dingdan_obj, 'AssignedResponse', None)
+ response_data['zhiding_hf'] = hf
+ response_data['zhiding_hf_text'] = ZHIDING_HF_TEXT.get(hf, '') if hf else ''
+ response_data['zhiding_hf_sj'] = (
+ dingdan_obj.AssignedResponseAt.strftime('%Y-%m-%d %H:%M:%S')
+ if getattr(dingdan_obj, 'AssignedResponseAt', None) else ''
+ )
+ response_data['zhiding_dhj_sm'] = getattr(dingdan_obj, 'AssignedLaterNote', '') or ''
+
+ # 9. 关联罚款(打手)
+ from utils.penalty_status import PENALTY_STATUS_LABELS as PENALTY_STATUS
+ try:
+ from orders.models import Penalty
+ dashou_id = dingdan_obj.PlayerID or dingdan_obj.AssignedID or ''
+ fakuan_qs = Penalty.query.filter(RelatedOrderID=dingdan_id)
+ if dashou_id:
+ fakuan_qs = fakuan_qs.filter(PenalizedUserID=dashou_id)
+ fakuan_list = []
+ for p in fakuan_qs.order_by('-CreateTime')[:5]:
+ fakuan_list.append({
+ 'amount': str(p.FineAmount or '0.00'),
+ 'status': p.Status,
+ 'status_text': PENALTY_STATUS.get(p.Status, ''),
+ 'reason': p.Reason or '',
+ 'create_time': p.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if p.CreateTime else '',
+ })
+ response_data['fakuan_list'] = fakuan_list
+ latest = fakuan_list[0] if fakuan_list else None
+ response_data['fakuan_info'] = latest
+ response_data['is_fadaned'] = Penalty.query.filter(
+ RelatedOrderID=dingdan_id, ApplicantID=getattr(current_user, 'UserUID', '')
+ ).exists()
+ except Exception as e:
+ logger.exception(f"[kefuhqddxq] 罚款查询异常 dingdan_id={dingdan_id}")
+ response_data['fakuan_list'] = []
+ response_data['fakuan_info'] = None
+ response_data['is_fadaned'] = False
+
+ return Response({'code': 0, 'data': response_data})
+
+
+class KefuForceCompleteView(APIView):
+ """
+ 客服强制结单接口(仅本地订单)
+ 请求:POST /yonghu/kefuqiangzhijiedan
+ 参数:{"phone": "客服手机号", "dingdan_id": "订单ID"}
+ 认证:JWT,仅限状态正常的客服
+ 逻辑:
+ 1. 验证客服身份及状态
+ 2. 查询订单并加锁(事务内)
+ 3. 校验订单状态是否为8(结算中)
+ 4. 校验订单必须有接单打手且打手分成金额有效
+ 5. 给打手增加余额、成交统计、今日/本月收益
+ 6. 如果是商家发单,更新商家成交订单数量
+ 7. 更新订单状态为3(已完成)
+ 8. 更新客服处理统计
+ 9. 返回成功
+ """
+ permission_classes = [permissions.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': '参数缺失'})
+
+ # 1. 客服身份验证
+ current_user = request.user
+ if getattr(current_user, 'Phone', '') != phone:
+ return Response({'code': 401, 'msg': '认证失败'})
+
+ if current_user.UserType not in ('kefu', 'admin'):
+ return Response({'code': 401, 'msg': '认证失败'})
+
+ is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
+ kefu = None
+ if not is_admin:
+ try:
+ kefu = current_user.KefuProfile
+ except ObjectDoesNotExist:
+ return Response({'code': 401, 'msg': '认证失败'})
+
+ if kefu.zhuangtai != 1:
+ return Response({'code': 401, 'msg': '客服状态异常'})
+
+ # 2. 订单处理(事务内)
+ try:
+ with transaction.atomic():
+ order = Order.objects.select_for_update().get(OrderID=dingdan_id)
+
+ # 3. 校验订单状态
+ if order.Status != 8:
+ return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.Status}'})
+
+ # 4. 校验必要字段
+ dashou_id = order.PlayerID
+ dashou_fencheng = order.PlayerCommission
+ if not dashou_id:
+ return Response({'code': 400, 'msg': '订单无接单打手'})
+ if not dashou_fencheng or dashou_fencheng <= 0:
+ return Response({'code': 400, 'msg': '打手分成金额无效'})
+
+ # 5. 给打手结算
+ try:
+ dashou_user = User.query.get(UserUID=dashou_id)
+ dashou_profile = dashou_user.DashouProfile
+ # 原子更新(避免并发重复加)
+ dashou_profile.chengjiaozongliang = F('chengjiaozongliang') + 1
+ dashou_profile.yue = F('yue') + dashou_fencheng
+ dashou_profile.zonge = F('zonge') + dashou_fencheng
+ dashou_profile.jinrishouyi = F('jinrishouyi') + dashou_fencheng
+ dashou_profile.jinyueshouyi = F('jinyueshouyi') + dashou_fencheng
+ dashou_profile.save(update_fields=[
+ 'chengjiaozongliang', 'yue', 'zonge',
+ 'jinrishouyi', 'jinyueshouyi'
+ ])
+ except (User.DoesNotExist, AttributeError):
+ return Response({'code': 400, 'msg': '打手不存在或扩展表缺失'})
+
+ # 6. 如果是商家发单,更新商家成交订单数量
+ if order.Platform == 2:
+ try:
+ # 通过 MerchantOrderExt 获取商家ID
+ shangjia_ext = order.shangjia_kuozhan # 假设 related_name='shangjia_kuozhan'
+ if shangjia_ext and shangjia_ext.shangjia_id:
+ shangjia_user = User.query.get(UserUID=shangjia_ext.shangjia_id)
+ shangjia_profile = shangjia_user.ShopProfile
+ shangjia_profile.chengjiao = F('chengjiao') + 1
+ shangjia_profile.save(update_fields=['chengjiao'])
+
+ # (行为驱动,更安全)
+ update_shangjia_daily(
+ yonghuid=shangjia_ext.shangjia_id,
+ amount=Decimal(str(order.Amount)),
+ action=2, # 2 = 结算
+ order_id=dingdan_id,
+ )
+
+ except ObjectDoesNotExist:
+ # 没有商家扩展记录则忽略
+ pass
+
+ # 7. 更新订单状态
+ order.Status = 3
+ order.AssignedCS = phone
+ order.save(update_fields=['Status', 'AssignedCS'])
+
+
+ if order.Platform == 1:
+
+ # ========== 新增:商品和店铺每日统计 ==========
+ try:
+ # 商品每日统计(只要有商品ID就执行)
+ if order.ProductID:
+ # 从扩展表获取已计算好的收益值(下单时已存储)
+ pingtai_shouyi = Decimal('0.00')
+ dianpu_shouyi = Decimal('0.00')
+ if hasattr(order, 'pingtai_kuozhan'):
+ pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
+ dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
+
+ update_shangpin_daily_stat(
+ shangpin_id=order.ProductID,
+ caozuo_leixing=2, # 2 = 结单
+ dingdan_jiage=order.Amount,
+ pingtai_shouyi=pingtai_shouyi,
+ dianpu_zongshouyi=dianpu_shouyi
+ )
+ logger.info(f"商品统计更新完成,商品ID: {order.ProductID}")
+
+ # 店铺每日统计(仅当存在店铺ID时执行)
+ dianpu_id = None
+ if hasattr(order, 'pingtai_kuozhan'):
+ dianpu_id = order.pingtai_kuozhan.ShopID
+ if dianpu_id:
+ dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
+ update_dianpu_daily_stat(
+ dianpu_id=dianpu_id,
+ caozuo_leixing=2, # 2 = 结单
+ dingdan_jiage=order.Amount,
+ dianpu_shouyi=dianpu_shouyi
+ )
+ logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")
+ except Exception as e:
+ logger.error(f"商品/店铺统计更新失败: {e}")
+ logger.error(traceback.format_exc())
+ # 统计失败不影响主流程
+ # =============================================
+
+ # 🔥 新增:调用打手每日统计(成交)
+ try:
+ update_dashou_daily_by_action(
+ yonghuid=order.PlayerID,
+ amount=dashou_fencheng,
+ action=2 # 2 表示成交(结算)
+ )
+ logger.info(f"打手 {order.PlayerID} 每日统计更新成功")
+ except Exception as e:
+ logger.error(f"打手每日统计更新失败: {e}")
+ # 不影响主流程
+
+ # 商家订单管事分红
+ if order.Platform == 2:
+ settle_shangjia_order_guanshi_fenhong(order, dashou_id)
+
+ # 8. 更新客服统计
+ kefu.jinrichuli = F('jinrichuli') + 1
+ kefu.jinyuechuli = F('jinyuechuli') + 1
+ kefu.zongchuli = F('zongchuli') + 1
+ kefu.save(update_fields=['jinrichuli', 'jinyuechuli', 'zongchuli'])
+
+ return Response({'code': 0, 'msg': '强制结单成功'})
+
+ except Order.DoesNotExist:
+ return Response({'code': 404, 'msg': '订单不存在'})
+ except Exception as e:
+ logger.error(f"强制结单异常: {str(e)}", exc_info=True)
+ return Response({'code': 500, 'msg': '服务器内部错误'})
diff --git a/users/views/kefu_punishment.py b/users/views/kefu_punishment.py
new file mode 100644
index 0000000..224ada8
--- /dev/null
+++ b/users/views/kefu_punishment.py
@@ -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': '处罚成功'})
diff --git a/users/views/kefu_withdraw.py b/users/views/kefu_withdraw.py
new file mode 100644
index 0000000..6761ccf
--- /dev/null
+++ b/users/views/kefu_withdraw.py
@@ -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)}})
diff --git a/users/views/shangjia.py b/users/views/shangjia.py
index dd9e835..15268d1 100644
--- a/users/views/shangjia.py
+++ b/users/views/shangjia.py
@@ -719,13 +719,15 @@ class FaKuanShenSuView(APIView):
PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=2).delete()
# 插入新上传的申诉图片
- for url in tupian_urls:
- PenaltyAppealImage.query.create(
+ PenaltyAppealImage.objects.bulk_create([
+ PenaltyAppealImage(
Penalty=fadan,
PenalizedUserID=yonghuid,
ImageURL=url,
- Purpose=2
+ Purpose=2,
)
+ for url in tupian_urls
+ ])
return Response({'code': 0, 'msg': '申诉提交成功'})
diff --git a/users/views/tixian.py b/users/views/tixian.py
index 6364d08..757a237 100644
--- a/users/views/tixian.py
+++ b/users/views/tixian.py
@@ -1052,7 +1052,9 @@ class TixianAssetView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
- user = request.user
+ user = User.query.select_related(
+ 'DashouProfile', 'ShopProfile', 'GuanshiProfile', 'ZuzhangProfile', 'ShenheguanProfile'
+ ).get(UserUID=request.user.UserUID)
# 初始化返回数据,默认全部为0
data = {
'dashou_yue': '0.00',