perf: P1 性能优化批量修复 + kefu 视图拆分
P1 性能优化(避免循环内 N+1 / N 次 DB 查询): - shop_order_views._batch_images: 移除 ThreadPoolExecutor 掩盖的 N 次查询,改单次批量 + Python 侧分组 - product_query.DashouHuiyuanList: 5N 查询 -> 3 次批量预取 + 本地闭包判断 - roles.GetRolePermissionView / 用户列表: 循环内 RolePermission/Permission/UserRole/Role -> 批量 __in 预取 - guanli / zuzhang 杜次分红 4.2/4.3: 循环内 update_or_create + 单条 delete -> bulk_create + bulk_update + 批量 delete - admin_config QQ 群配置: 循环内 exists/first/save/create -> 批量预取 + bulk_create + bulk_update - jituan 服务层多处合并统计查询、批量预取 map - rank 多处循环 N+1 改批量预取 - backend 多处循环内 count/create 改批量 - config/orders/merchant_ops 多处循环 N+1 改批量预取 其他改动: - users/views/kefu.py 拆分为 kefu_base/kefu_dashou/kefu_orders/kefu_punishment/kefu_withdraw 5 个文件 - 删除遗留脚本 check_prod_uid.py / create_rbac_tables.sql
This commit is contained in:
1 parent
76ea5f8255
commit
f1c8633345
54 files changed
+5332
-4494
No files matched your search
+17
-7
@@ -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,
|
||||
|
||||
+34
-19
@@ -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
|
||||
|
||||
+77
-34
@@ -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:
|
||||
|
||||
+13
-7
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+27
-16
@@ -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 '',
|
||||
|
||||
+43
-22
@@ -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 '',
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+15
-8
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+76
-32
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
|
||||
+30
-18
@@ -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 = ''
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`);
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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']]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'],
|
||||
|
||||
+19
-19
@@ -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 # 空闲
|
||||
|
||||
@@ -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
|
||||
|
||||
+11
-9
@@ -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)
|
||||
|
||||
+16
-11
@@ -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
|
||||
|
||||
@@ -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)}张"
|
||||
)
|
||||
|
||||
+10
-5
@@ -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})
|
||||
|
||||
+151
-78
@@ -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,
|
||||
|
||||
@@ -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)})
|
||||
|
||||
|
||||
+35
-10
@@ -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
|
||||
|
||||
+23
-11
@@ -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({
|
||||
|
||||
+35
-13
@@ -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,
|
||||
|
||||
+37
-25
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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': '关联的公共商品类型无效'})
|
||||
|
||||
# 商品图片上传
|
||||
|
||||
@@ -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
|
||||
|
||||
+23
-11
@@ -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,
|
||||
|
||||
+32
-18
@@ -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'],
|
||||
}
|
||||
|
||||
# 构建响应数据
|
||||
|
||||
+10
-10
@@ -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 = {
|
||||
|
||||
-3794
File diff suppressed because it is too large.
Load diff
@@ -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': []})
|
||||
@@ -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': '服务器内部错误'})
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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': '处罚成功'})
|
||||
@@ -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)}})
|
||||
@@ -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': '申诉提交成功'})
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user