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:
@@ -580,12 +580,19 @@ class KhjlglView(APIView):
|
|||||||
pending=Count('jilu_id', filter=Q(zhuangtai=3)),
|
pending=Count('jilu_id', filter=Q(zhuangtai=3)),
|
||||||
).order_by('-total')
|
).order_by('-total')
|
||||||
|
|
||||||
|
_stats_agg = stats_base.aggregate(
|
||||||
|
total=Count('id'),
|
||||||
|
passed=Count('id', filter=Q(zhuangtai=1)),
|
||||||
|
failed=Count('id', filter=Q(zhuangtai=2)),
|
||||||
|
in_progress=Count('id', filter=Q(zhuangtai=0)),
|
||||||
|
pending=Count('id', filter=Q(zhuangtai=3)),
|
||||||
|
)
|
||||||
stats = {
|
stats = {
|
||||||
'total': stats_base.count(),
|
'total': _stats_agg['total'],
|
||||||
'passed': stats_base.filter(zhuangtai=1).count(),
|
'passed': _stats_agg['passed'],
|
||||||
'failed': stats_base.filter(zhuangtai=2).count(),
|
'failed': _stats_agg['failed'],
|
||||||
'in_progress': stats_base.filter(zhuangtai=0).count(),
|
'in_progress': _stats_agg['in_progress'],
|
||||||
'pending': stats_base.filter(zhuangtai=3).count(),
|
'pending': _stats_agg['pending'],
|
||||||
'total_fee': float(agg['total_fee'] or 0),
|
'total_fee': float(agg['total_fee'] or 0),
|
||||||
'tag_stats': [
|
'tag_stats': [
|
||||||
{
|
{
|
||||||
@@ -702,8 +709,11 @@ class KhjlczView(APIView):
|
|||||||
for f in KaoheJiluFeiyong.query.filter(jilu_id=jilu_id).order_by('cishu')
|
for f in KaoheJiluFeiyong.query.filter(jilu_id=jilu_id).order_by('cishu')
|
||||||
]
|
]
|
||||||
reject_list = []
|
reject_list = []
|
||||||
for r in KaoheJujueJilu.query.filter(jilu_id=jilu_id).order_by('-CreateTime'):
|
_reject_records = list(KaoheJujueJilu.query.filter(jilu_id=jilu_id).order_by('-CreateTime'))
|
||||||
kg_prof = _batch_user_profile_map([r.shenheguan_id]).get(r.shenheguan_id, {})
|
_kg_ids = [r.shenheguan_id for r in _reject_records if r.shenheguan_id]
|
||||||
|
_kg_prof_map = _batch_user_profile_map(_kg_ids) if _kg_ids else {}
|
||||||
|
for r in _reject_records:
|
||||||
|
kg_prof = _kg_prof_map.get(r.shenheguan_id, {})
|
||||||
reject_list.append({
|
reject_list.append({
|
||||||
'cishu': r.cishu,
|
'cishu': r.cishu,
|
||||||
'shenheguan_id': r.shenheguan_id,
|
'shenheguan_id': r.shenheguan_id,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import hmac
|
import hmac
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
@@ -12,6 +12,7 @@ import secrets
|
|||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
from calendar import monthrange
|
from calendar import monthrange
|
||||||
|
from collections import defaultdict
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -155,13 +156,18 @@ class CwhybkhqView(APIView):
|
|||||||
if 'huiyuancaiwu' not in permissions:
|
if 'huiyuancaiwu' not in permissions:
|
||||||
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
|
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
|
||||||
|
|
||||||
|
all_bankuai = list(Bankuai.query.all())
|
||||||
|
bankuai_ids = [bk.bankuai_id for bk in all_bankuai]
|
||||||
|
all_members = Huiyuan.query.filter(bankuai_id__in=bankuai_ids).values('huiyuan_id', 'jieshao', 'bankuai_id')
|
||||||
|
members_by_bankuai = defaultdict(list)
|
||||||
|
for m in all_members:
|
||||||
|
members_by_bankuai[m['bankuai_id']].append(dict(m))
|
||||||
bankuai_list = []
|
bankuai_list = []
|
||||||
for bk in Bankuai.query.all():
|
for bk in all_bankuai:
|
||||||
members = Huiyuan.query.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id')
|
|
||||||
bankuai_list.append({
|
bankuai_list.append({
|
||||||
'bankuai_id': bk.bankuai_id,
|
'bankuai_id': bk.bankuai_id,
|
||||||
'mingcheng': bk.mingcheng,
|
'mingcheng': bk.mingcheng,
|
||||||
'members': list(members)
|
'members': members_by_bankuai.get(bk.bankuai_id, [])
|
||||||
})
|
})
|
||||||
|
|
||||||
return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list})
|
return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list})
|
||||||
@@ -597,21 +603,30 @@ class CwhqjtddsjView(APIView):
|
|||||||
summary_qs = filter_queryset_by_club(
|
summary_qs = filter_queryset_by_club(
|
||||||
Order.query.filter(**filters), request, club_field='ClubID',
|
Order.query.filter(**filters), request, club_field='ClubID',
|
||||||
)
|
)
|
||||||
total_order_count = summary_qs.count()
|
agg = summary_qs.aggregate(
|
||||||
total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0
|
total_order_count=Count('id'),
|
||||||
total_success_count = summary_qs.filter(Status=3).count()
|
total_order_amount=Sum('Amount'),
|
||||||
total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0
|
total_success_count=Count('id', filter=Q(Status=3)),
|
||||||
total_refund_count = summary_qs.filter(Status=5).count()
|
total_success_amount=Sum('Amount', filter=Q(Status=3)),
|
||||||
total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
total_refund_count=Count('id', filter=Q(Status=5)),
|
||||||
total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate(
|
total_dashou=Sum('PlayerCommission', filter=Q(Status=3)),
|
||||||
s=Sum('pingtai_kuozhan__ShopIncome'))['s'] or 0
|
total_dianpu_fenhong=Sum('pingtai_kuozhan__ShopIncome', filter=Q(Status=3, Platform=1)),
|
||||||
|
platform_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
|
||||||
platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0
|
merchant_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
|
||||||
merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0
|
platform_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
|
||||||
platform_dashou_sum = \
|
merchant_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
|
||||||
summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
)
|
||||||
merchant_dashou_sum = \
|
total_order_count = agg['total_order_count'] or 0
|
||||||
summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
total_order_amount = agg['total_order_amount'] or 0
|
||||||
|
total_success_count = agg['total_success_count'] or 0
|
||||||
|
total_success_amount = agg['total_success_amount'] or 0
|
||||||
|
total_refund_count = agg['total_refund_count'] or 0
|
||||||
|
total_dashou = agg['total_dashou'] or 0
|
||||||
|
total_dianpu_fenhong = agg['total_dianpu_fenhong'] or 0
|
||||||
|
platform_amount = agg['platform_amount'] or 0
|
||||||
|
merchant_amount = agg['merchant_amount'] or 0
|
||||||
|
platform_dashou_sum = agg['platform_dashou_sum'] or 0
|
||||||
|
merchant_dashou_sum = agg['merchant_dashou_sum'] or 0
|
||||||
|
|
||||||
profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
|
profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
|
||||||
profit_merchant = merchant_amount - merchant_dashou_sum
|
profit_merchant = merchant_amount - merchant_dashou_sum
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import hmac
|
import hmac
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
@@ -357,9 +357,16 @@ class GetGuanliDetailView(APIView):
|
|||||||
custom_first = {} # 首次分红定制(cishu=1)
|
custom_first = {} # 首次分红定制(cishu=1)
|
||||||
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } }
|
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } }
|
||||||
|
|
||||||
|
# 批量预取会员信息
|
||||||
|
_huiyuan_ids = [r.huiyuan for r in duoci_records]
|
||||||
|
_huiyuan_map = {
|
||||||
|
h.huiyuan_id: h
|
||||||
|
for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao', 'jiage')
|
||||||
|
}
|
||||||
|
|
||||||
for record in duoci_records:
|
for record in duoci_records:
|
||||||
# 获取会员信息
|
# 获取会员信息
|
||||||
member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first()
|
member = _huiyuan_map.get(record.huiyuan)
|
||||||
if not member:
|
if not member:
|
||||||
continue
|
continue
|
||||||
member_info = {
|
member_info = {
|
||||||
@@ -711,10 +718,16 @@ class UpdateGuanliView(APIView):
|
|||||||
if custom_first is not None and isinstance(custom_first, dict):
|
if custom_first is not None and isinstance(custom_first, dict):
|
||||||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||||
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||||
# 获取当前所有首次定制记录
|
# 批量预取当前所有首次定制记录(避免循环内 N+1)
|
||||||
existing_first = DuociFenhong.query.filter(
|
existing_first_list = list(
|
||||||
club_id=guanshi_club, yonghuid=yonghuid, cishu=1,
|
DuociFenhong.query.filter(
|
||||||
|
club_id=guanshi_club, yonghuid=yonghuid, cishu=1,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
_existing_map = {r.huiyuan: r for r in existing_first_list}
|
||||||
|
_to_create = []
|
||||||
|
_to_update = []
|
||||||
|
_to_delete_ids = []
|
||||||
# 前端传来的定制字典 {会员ID: 金额}
|
# 前端传来的定制字典 {会员ID: 金额}
|
||||||
for huiyuan_id, amount in custom_first.items():
|
for huiyuan_id, amount in custom_first.items():
|
||||||
try:
|
try:
|
||||||
@@ -722,24 +735,34 @@ class UpdateGuanliView(APIView):
|
|||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
if amount <= 0:
|
if amount <= 0:
|
||||||
DuociFenhong.query.filter(
|
rec = _existing_map.get(huiyuan_id)
|
||||||
club_id=guanshi_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1,
|
if rec is not None:
|
||||||
).delete()
|
_to_delete_ids.append(rec.id)
|
||||||
else:
|
else:
|
||||||
DuociFenhong.query.update_or_create(
|
rec = _existing_map.get(huiyuan_id)
|
||||||
club_id=guanshi_club,
|
if rec is not None:
|
||||||
yonghuid=yonghuid,
|
rec.guanshi_fenhong = amount
|
||||||
huiyuan=huiyuan_id,
|
rec.zuzhang_fenhong = Decimal('0')
|
||||||
cishu=1,
|
_to_update.append(rec)
|
||||||
defaults={
|
else:
|
||||||
'guanshi_fenhong': amount,
|
_to_create.append(DuociFenhong(
|
||||||
'zuzhang_fenhong': Decimal('0'),
|
club_id=guanshi_club,
|
||||||
},
|
yonghuid=yonghuid,
|
||||||
)
|
huiyuan=huiyuan_id,
|
||||||
|
cishu=1,
|
||||||
|
guanshi_fenhong=amount,
|
||||||
|
zuzhang_fenhong=Decimal('0'),
|
||||||
|
))
|
||||||
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
|
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
|
||||||
for record in existing_first:
|
for rec in existing_first_list:
|
||||||
if record.huiyuan not in custom_first:
|
if rec.huiyuan not in custom_first and rec.id not in _to_delete_ids:
|
||||||
record.delete()
|
_to_delete_ids.append(rec.id)
|
||||||
|
if _to_create:
|
||||||
|
DuociFenhong.objects.bulk_create(_to_create)
|
||||||
|
if _to_update:
|
||||||
|
DuociFenhong.objects.bulk_update(_to_update, ['guanshi_fenhong', 'zuzhang_fenhong'])
|
||||||
|
if _to_delete_ids:
|
||||||
|
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
|
||||||
|
|
||||||
# 4.3 额外次数分红(cishu >= 2)
|
# 4.3 额外次数分红(cishu >= 2)
|
||||||
if extra_map is not None and isinstance(extra_map, dict):
|
if extra_map is not None and isinstance(extra_map, dict):
|
||||||
@@ -747,6 +770,8 @@ class UpdateGuanliView(APIView):
|
|||||||
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||||
# 收集需要保留的记录
|
# 收集需要保留的记录
|
||||||
keep_set = set()
|
keep_set = set()
|
||||||
|
# 解析前端数据,先汇总 keep_set 与目标金额
|
||||||
|
_extra_targets = [] # [(cishu, huiyuan_id, amount)]
|
||||||
for times_str, members in extra_map.items():
|
for times_str, members in extra_map.items():
|
||||||
try:
|
try:
|
||||||
cishu = int(times_str)
|
cishu = int(times_str)
|
||||||
@@ -767,24 +792,42 @@ class UpdateGuanliView(APIView):
|
|||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
keep_set.add((cishu, huiyuan_id))
|
keep_set.add((cishu, huiyuan_id))
|
||||||
# 更新或创建
|
_extra_targets.append((cishu, huiyuan_id, amount))
|
||||||
DuociFenhong.query.update_or_create(
|
# 批量预取已有额外次数记录
|
||||||
|
existing_extra_list = list(
|
||||||
|
DuociFenhong.query.filter(
|
||||||
|
club_id=guanshi_club, yonghuid=yonghuid, cishu__gte=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_extra_map = {(r.cishu, r.huiyuan): r for r in existing_extra_list}
|
||||||
|
_to_create = []
|
||||||
|
_to_update = []
|
||||||
|
_to_delete_ids = []
|
||||||
|
for cishu, huiyuan_id, amount in _extra_targets:
|
||||||
|
rec = _extra_map.get((cishu, huiyuan_id))
|
||||||
|
if rec is not None:
|
||||||
|
rec.guanshi_fenhong = amount
|
||||||
|
rec.zuzhang_fenhong = Decimal('0')
|
||||||
|
_to_update.append(rec)
|
||||||
|
else:
|
||||||
|
_to_create.append(DuociFenhong(
|
||||||
club_id=guanshi_club,
|
club_id=guanshi_club,
|
||||||
yonghuid=yonghuid,
|
yonghuid=yonghuid,
|
||||||
huiyuan=huiyuan_id,
|
huiyuan=huiyuan_id,
|
||||||
cishu=cishu,
|
cishu=cishu,
|
||||||
defaults={
|
guanshi_fenhong=amount,
|
||||||
'guanshi_fenhong': amount,
|
zuzhang_fenhong=Decimal('0'),
|
||||||
'zuzhang_fenhong': Decimal('0')
|
))
|
||||||
}
|
|
||||||
)
|
|
||||||
# 删除不在 keep_set 中的额外次数记录
|
# 删除不在 keep_set 中的额外次数记录
|
||||||
existing_extra = DuociFenhong.query.filter(
|
for rec in existing_extra_list:
|
||||||
club_id=guanshi_club, yonghuid=yonghuid, cishu__gte=2,
|
if (rec.cishu, rec.huiyuan) not in keep_set:
|
||||||
)
|
_to_delete_ids.append(rec.id)
|
||||||
for record in existing_extra:
|
if _to_create:
|
||||||
if (record.cishu, record.huiyuan) not in keep_set:
|
DuociFenhong.objects.bulk_create(_to_create)
|
||||||
record.delete()
|
if _to_update:
|
||||||
|
DuociFenhong.objects.bulk_update(_to_update, ['guanshi_fenhong', 'zuzhang_fenhong'])
|
||||||
|
if _to_delete_ids:
|
||||||
|
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
|
||||||
|
|
||||||
dividend_parts = []
|
dividend_parts = []
|
||||||
if perm_enabled is not None:
|
if perm_enabled is not None:
|
||||||
|
|||||||
@@ -342,6 +342,12 @@ class KefuGetDashouDetailView(APIView):
|
|||||||
member_records = Huiyuangoumai.query.filter(
|
member_records = Huiyuangoumai.query.filter(
|
||||||
yonghu_id=uid
|
yonghu_id=uid
|
||||||
).order_by('-CreateTime')
|
).order_by('-CreateTime')
|
||||||
|
# 批量预取会员信息
|
||||||
|
_huiyuan_ids = [r.huiyuan_id for r in member_records]
|
||||||
|
_huiyuan_map = {
|
||||||
|
h.huiyuan_id: h.jieshao
|
||||||
|
for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao')
|
||||||
|
}
|
||||||
member_list = []
|
member_list = []
|
||||||
for record in member_records:
|
for record in member_records:
|
||||||
# 检查是否过期
|
# 检查是否过期
|
||||||
@@ -351,11 +357,7 @@ class KefuGetDashouDetailView(APIView):
|
|||||||
is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
|
is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
|
||||||
|
|
||||||
if not is_expired:
|
if not is_expired:
|
||||||
try:
|
huiyuan_name = _huiyuan_map.get(record.huiyuan_id, '未知会员')
|
||||||
huiyuan = Huiyuan.query.get(huiyuan_id=record.huiyuan_id)
|
|
||||||
huiyuan_name = huiyuan.jieshao
|
|
||||||
except Huiyuan.DoesNotExist:
|
|
||||||
huiyuan_name = '未知会员'
|
|
||||||
member_list.append({
|
member_list.append({
|
||||||
'id': record.id,
|
'id': record.id,
|
||||||
'huiyuan_name': huiyuan_name,
|
'huiyuan_name': huiyuan_name,
|
||||||
@@ -828,8 +830,12 @@ class KefuGetShangjiaListView(APIView):
|
|||||||
return Response({'code': 400, 'msg': '商家状态格式错误'})
|
return Response({'code': 400, 'msg': '商家状态格式错误'})
|
||||||
|
|
||||||
# 5. 统计总商家数(应用筛选后)
|
# 5. 统计总商家数(应用筛选后)
|
||||||
total = shangjia_qs.count()
|
_agg = shangjia_qs.aggregate(
|
||||||
valid_merchant_count = shangjia_qs.filter(zhuangtai=1).count()
|
total=Count('id'),
|
||||||
|
valid=Count('id', filter=Q(zhuangtai=1)),
|
||||||
|
)
|
||||||
|
total = _agg['total']
|
||||||
|
valid_merchant_count = _agg['valid']
|
||||||
|
|
||||||
# 6. 分页
|
# 6. 分页
|
||||||
offset = (page - 1) * page_size
|
offset = (page - 1) * page_size
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import hmac
|
import hmac
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
@@ -518,13 +518,15 @@ class FaKuanChuangJianView(APIView):
|
|||||||
user=request.user,
|
user=request.user,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for relative_url in uploaded_urls:
|
PenaltyAppealImage.objects.bulk_create([
|
||||||
PenaltyAppealImage.query.create(
|
PenaltyAppealImage(
|
||||||
Penalty=penalty,
|
Penalty=penalty,
|
||||||
PenalizedUserID=PenalizedUserID,
|
PenalizedUserID=PenalizedUserID,
|
||||||
ImageURL=relative_url,
|
ImageURL=relative_url,
|
||||||
Purpose=1
|
Purpose=1,
|
||||||
)
|
)
|
||||||
|
for relative_url in uploaded_urls
|
||||||
|
])
|
||||||
from users.fadan_fenhong_utils import lock_penalty_bonus
|
from users.fadan_fenhong_utils import lock_penalty_bonus
|
||||||
lock_penalty_bonus(penalty)
|
lock_penalty_bonus(penalty)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import hmac
|
import hmac
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
@@ -147,14 +147,16 @@ class GetProductBaseDataView(APIView):
|
|||||||
|
|
||||||
# 获取所有商品类型(按 paixu 升序)
|
# 获取所有商品类型(按 paixu 升序)
|
||||||
type_qs = ShangpinLeixing.query.all().order_by('paixu')
|
type_qs = ShangpinLeixing.query.all().order_by('paixu')
|
||||||
|
# 批量聚合商品数量,避免循环内 N+1 count
|
||||||
|
_type_counts = {
|
||||||
|
item['leixing_id']: item['cnt']
|
||||||
|
for item in Shangpin.query.filter(
|
||||||
|
dianpu__isnull=True, dianpu_leixing__isnull=True, leixing_id__isnull=False
|
||||||
|
).values('leixing_id').annotate(cnt=Count('id'))
|
||||||
|
}
|
||||||
type_list = []
|
type_list = []
|
||||||
for t in type_qs:
|
for t in type_qs:
|
||||||
# 🔥 修改:统计该类型下的公共商品数量(店铺为空且店铺商品类型为空)
|
product_count = _type_counts.get(t.id, 0)
|
||||||
product_count = Shangpin.query.filter(
|
|
||||||
leixing_id=t.id,
|
|
||||||
dianpu__isnull=True, # 所属店铺为空
|
|
||||||
dianpu_leixing__isnull=True # 店铺商品类型为空
|
|
||||||
).count()
|
|
||||||
type_list.append({
|
type_list.append({
|
||||||
'id': t.id,
|
'id': t.id,
|
||||||
'jieshao': t.jieshao or '',
|
'jieshao': t.jieshao or '',
|
||||||
@@ -166,14 +168,15 @@ class GetProductBaseDataView(APIView):
|
|||||||
|
|
||||||
# 获取所有专区(按 paixu 升序)
|
# 获取所有专区(按 paixu 升序)
|
||||||
zone_qs = ShangpinZhuanqu.query.all().order_by('paixu')
|
zone_qs = ShangpinZhuanqu.query.all().order_by('paixu')
|
||||||
|
_zone_counts = {
|
||||||
|
item['zhuanqu_id']: item['cnt']
|
||||||
|
for item in Shangpin.query.filter(
|
||||||
|
dianpu__isnull=True, dianpu_leixing__isnull=True, zhuanqu_id__isnull=False
|
||||||
|
).values('zhuanqu_id').annotate(cnt=Count('id'))
|
||||||
|
}
|
||||||
zone_list = []
|
zone_list = []
|
||||||
for z in zone_qs:
|
for z in zone_qs:
|
||||||
# 🔥 修改:统计该专区下的公共商品数量(店铺为空且店铺商品类型为空)
|
product_count = _zone_counts.get(z.id, 0)
|
||||||
product_count = Shangpin.query.filter(
|
|
||||||
zhuanqu_id=z.id,
|
|
||||||
dianpu__isnull=True, # 所属店铺为空
|
|
||||||
dianpu_leixing__isnull=True # 店铺商品类型为空
|
|
||||||
).count()
|
|
||||||
zone_list.append({
|
zone_list.append({
|
||||||
'id': z.id,
|
'id': z.id,
|
||||||
'mingzi': z.mingzi or '',
|
'mingzi': z.mingzi or '',
|
||||||
@@ -368,10 +371,14 @@ class GetProductListView(APIView):
|
|||||||
|
|
||||||
# 获取所有商品类型(按 paixu 升序)
|
# 获取所有商品类型(按 paixu 升序)
|
||||||
type_qs = ShangpinLeixing.query.all().order_by('paixu')
|
type_qs = ShangpinLeixing.query.all().order_by('paixu')
|
||||||
|
# 批量聚合商品数量,避免循环内 N+1 count
|
||||||
|
_type_counts = {
|
||||||
|
item['leixing_id']: item['cnt']
|
||||||
|
for item in Shangpin.query.filter(leixing_id__isnull=False).values('leixing_id').annotate(cnt=Count('id'))
|
||||||
|
}
|
||||||
type_list = []
|
type_list = []
|
||||||
for t in type_qs:
|
for t in type_qs:
|
||||||
# 统计该类型下的商品数量(忽略审核状态?通常统计所有商品,可根据需求调整)
|
product_count = _type_counts.get(t.id, 0)
|
||||||
product_count = Shangpin.query.filter(leixing_id=t.id).count()
|
|
||||||
type_list.append({
|
type_list.append({
|
||||||
'id': t.id,
|
'id': t.id,
|
||||||
'jieshao': t.jieshao or '',
|
'jieshao': t.jieshao or '',
|
||||||
@@ -383,9 +390,13 @@ class GetProductListView(APIView):
|
|||||||
|
|
||||||
# 获取所有专区(按 paixu 升序)
|
# 获取所有专区(按 paixu 升序)
|
||||||
zone_qs = ShangpinZhuanqu.query.all().order_by('paixu')
|
zone_qs = ShangpinZhuanqu.query.all().order_by('paixu')
|
||||||
|
_zone_counts = {
|
||||||
|
item['zhuanqu_id']: item['cnt']
|
||||||
|
for item in Shangpin.query.filter(zhuanqu_id__isnull=False).values('zhuanqu_id').annotate(cnt=Count('id'))
|
||||||
|
}
|
||||||
zone_list = []
|
zone_list = []
|
||||||
for z in zone_qs:
|
for z in zone_qs:
|
||||||
product_count = Shangpin.query.filter(zhuanqu_id=z.id).count()
|
product_count = _zone_counts.get(z.id, 0)
|
||||||
zone_list.append({
|
zone_list.append({
|
||||||
'id': z.id,
|
'id': z.id,
|
||||||
'mingzi': z.mingzi or '',
|
'mingzi': z.mingzi or '',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import hmac
|
import hmac
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
@@ -157,19 +157,29 @@ class GetRolePermissionView(APIView):
|
|||||||
kefu_user_uuids = list(
|
kefu_user_uuids = list(
|
||||||
User.objects.filter(KefuProfile__isnull=False).values_list('UserUUID', flat=True)
|
User.objects.filter(KefuProfile__isnull=False).values_list('UserUUID', flat=True)
|
||||||
)
|
)
|
||||||
roles = filter_roles_for_request(request)
|
roles = list(filter_roles_for_request(request))
|
||||||
|
# 批量预取 RolePermission + Permission(避免循环内 N+1)
|
||||||
|
_role_uuids = [r.RoleUUID for r in roles]
|
||||||
|
_rp_pairs = list(
|
||||||
|
RolePermission.objects.filter(RoleUUID__in=_role_uuids)
|
||||||
|
.values_list('RoleUUID', 'PermUUID')
|
||||||
|
) if _role_uuids else []
|
||||||
|
_perm_uuids = list({p for _, p in _rp_pairs})
|
||||||
|
_perm_rows_by_uuid = {
|
||||||
|
p['PermUUID']: p
|
||||||
|
for p in Permission.objects.filter(PermUUID__in=_perm_uuids)
|
||||||
|
.values('PermUUID', 'PermCode', 'PermName', 'PermDesc')
|
||||||
|
} if _perm_uuids else {}
|
||||||
|
_perms_by_role = {}
|
||||||
|
for role_uuid, perm_uuid in _rp_pairs:
|
||||||
|
perm_row = _perm_rows_by_uuid.get(perm_uuid)
|
||||||
|
if perm_row is not None:
|
||||||
|
_perms_by_role.setdefault(role_uuid, []).append(_serialize_permission_row(perm_row))
|
||||||
|
|
||||||
roles_data = []
|
roles_data = []
|
||||||
for role in roles:
|
for role in roles:
|
||||||
user_count = count_role_users_in_scope(role, request, kefu_user_uuids)
|
user_count = count_role_users_in_scope(role, request, kefu_user_uuids)
|
||||||
role_perm_uuids = RolePermission.objects.filter(
|
perms_of_role = _perms_by_role.get(role.RoleUUID, [])
|
||||||
RoleUUID=role.RoleUUID
|
|
||||||
).values_list('PermUUID', flat=True)
|
|
||||||
perms_of_role = [
|
|
||||||
_serialize_permission_row(p)
|
|
||||||
for p in Permission.objects.filter(
|
|
||||||
PermUUID__in=list(role_perm_uuids)
|
|
||||||
).values('PermCode', 'PermName', 'PermDesc')
|
|
||||||
]
|
|
||||||
roles_data.append({
|
roles_data.append({
|
||||||
'role_code': role.RoleName,
|
'role_code': role.RoleName,
|
||||||
'role_name': role.RoleName,
|
'role_name': role.RoleName,
|
||||||
@@ -434,18 +444,29 @@ class GetAdminUserListView(APIView):
|
|||||||
paginator = Paginator(users, page_size)
|
paginator = Paginator(users, page_size)
|
||||||
page_obj = paginator.get_page(page)
|
page_obj = paginator.get_page(page)
|
||||||
|
|
||||||
|
# 批量预取本页所有用户的 UserRole + Role(避免循环内 N+1)
|
||||||
|
_page_users = list(page_obj)
|
||||||
|
_page_user_uuids = [u.UserUUID for u in _page_users]
|
||||||
|
_ur_pairs = list(
|
||||||
|
UserRole.objects.filter(UserUUID__in=_page_user_uuids)
|
||||||
|
.values_list('UserUUID', 'RoleUUID')
|
||||||
|
) if _page_user_uuids else []
|
||||||
|
_role_uuids_in_page = list({r for _, r in _ur_pairs})
|
||||||
|
_role_name_by_uuid = {
|
||||||
|
r.RoleUUID: r.RoleName
|
||||||
|
for r in Role.objects.filter(RoleUUID__in=_role_uuids_in_page)
|
||||||
|
} if _role_uuids_in_page else {}
|
||||||
|
_roles_by_user = {}
|
||||||
|
for user_uuid, role_uuid in _ur_pairs:
|
||||||
|
role_name = _role_name_by_uuid.get(role_uuid)
|
||||||
|
if role_name is not None:
|
||||||
|
_roles_by_user.setdefault(user_uuid, []).append(
|
||||||
|
{'role_code': role_name, 'role_name': role_name}
|
||||||
|
)
|
||||||
|
|
||||||
data_list = []
|
data_list = []
|
||||||
for user in page_obj:
|
for user in _page_users:
|
||||||
# 通过 gvsdsdk UserRole 获取用户角色
|
roles_data = _roles_by_user.get(user.UserUUID, [])
|
||||||
user_role_uuids = list(
|
|
||||||
UserRole.objects.filter(UserUUID=user.UserUUID)
|
|
||||||
.values_list('RoleUUID', flat=True)
|
|
||||||
)
|
|
||||||
user_roles = Role.objects.filter(RoleUUID__in=user_role_uuids)
|
|
||||||
roles_data = [
|
|
||||||
{'role_code': r.RoleName, 'role_name': r.RoleName}
|
|
||||||
for r in user_roles
|
|
||||||
]
|
|
||||||
data_list.append({
|
data_list.append({
|
||||||
'phone': user.Phone or '',
|
'phone': user.Phone or '',
|
||||||
'yonghuid': user.UserUID or '',
|
'yonghuid': user.UserUID or '',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import hmac
|
import hmac
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
@@ -173,19 +173,27 @@ class HqbkxxView(APIView):
|
|||||||
price_map[row.huiyuan_id] = row
|
price_map[row.huiyuan_id] = row
|
||||||
|
|
||||||
huiyuan_list = []
|
huiyuan_list = []
|
||||||
for hy in Huiyuan.query.all():
|
# 在受限 scope 下预过滤,避免 Huiyuan 全表遍历
|
||||||
row = price_map.get(hy.huiyuan_id)
|
# 注:当 scope != DATA_SCOPE_ALL 且 club_id != CLUB_ID_DEFAULT 时,
|
||||||
|
# 只有 price_map 中存在记录的会员才可能被保留(无记录的会被跳过)
|
||||||
|
huiyuan_qs = Huiyuan.query.all()
|
||||||
|
if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT:
|
||||||
|
huiyuan_qs = huiyuan_qs.filter(huiyuan_id__in=list(price_map.keys()))
|
||||||
|
# 仅查询所需字段,减少内存占用(dict 比 model 实例更轻)
|
||||||
|
huiyuan_qs = huiyuan_qs.values('huiyuan_id', 'jieshao', 'bankuai_id', 'jiage', 'guanshifc', 'zuzhangfc')
|
||||||
|
for hy in huiyuan_qs:
|
||||||
|
row = price_map.get(hy['huiyuan_id'])
|
||||||
if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT and row is None:
|
if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT and row is None:
|
||||||
continue
|
continue
|
||||||
if row is not None and not row.is_enabled:
|
if row is not None and not row.is_enabled:
|
||||||
continue
|
continue
|
||||||
item = {
|
item = {
|
||||||
'huiyuan_id': hy.huiyuan_id,
|
'huiyuan_id': hy['huiyuan_id'],
|
||||||
'jieshao': hy.jieshao,
|
'jieshao': hy['jieshao'],
|
||||||
'bankuai_id': hy.bankuai_id,
|
'bankuai_id': hy['bankuai_id'],
|
||||||
'jiage': str(row.jiage) if row else str(hy.jiage),
|
'jiage': str(row.jiage) if row else str(hy['jiage']),
|
||||||
'guanshifc': str(row.guanshifc) if row else str(hy.guanshifc),
|
'guanshifc': str(row.guanshifc) if row else str(hy['guanshifc']),
|
||||||
'zuzhangfc': str(row.zuzhangfc) if row else str(hy.zuzhangfc),
|
'zuzhangfc': str(row.zuzhangfc) if row else str(hy['zuzhangfc']),
|
||||||
}
|
}
|
||||||
huiyuan_list.append(item)
|
huiyuan_list.append(item)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import hmac
|
import hmac
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
@@ -241,11 +241,16 @@ class GetProductTypeZoneView(APIView):
|
|||||||
return Response({'code': 400, 'msg': scope_err})
|
return Response({'code': 400, 'msg': scope_err})
|
||||||
|
|
||||||
# 查询所有商品类型(含专区数量)
|
# 查询所有商品类型(含专区数量)
|
||||||
|
# 一次性分组聚合所有类型的专区数量,避免循环内 N+1 count
|
||||||
|
# 注:ShangpinZhuanqu.leixing_id 是 PositiveIntegerField(非 FK),无法用反向 annotate
|
||||||
|
zone_count_rows = ShangpinZhuanqu.query.values('leixing_id').annotate(cnt=Count('id'))
|
||||||
|
zone_count_map = {row['leixing_id']: row['cnt'] for row in zone_count_rows}
|
||||||
|
|
||||||
types = ShangpinLeixing.query.all().order_by('-paixu') # paixu 越大越靠前
|
types = ShangpinLeixing.query.all().order_by('-paixu') # paixu 越大越靠前
|
||||||
type_list = []
|
type_list = []
|
||||||
for t in types:
|
for t in types:
|
||||||
# 统计该类型下的专区数量
|
# 从预聚合的 map 中取专区数量
|
||||||
zone_count = ShangpinZhuanqu.query.filter(leixing_id=t.id).count()
|
zone_count = zone_count_map.get(t.id, 0)
|
||||||
type_list.append({
|
type_list.append({
|
||||||
'id': t.id,
|
'id': t.id,
|
||||||
'jieshao': t.jieshao or '',
|
'jieshao': t.jieshao or '',
|
||||||
@@ -531,10 +536,11 @@ class ModifyProductTypeZoneView(APIView):
|
|||||||
# 删除该类型下的所有专区
|
# 删除该类型下的所有专区
|
||||||
zones = ShangpinZhuanqu.query.filter(leixing_id=type_id)
|
zones = ShangpinZhuanqu.query.filter(leixing_id=type_id)
|
||||||
if delete_products:
|
if delete_products:
|
||||||
# 删除专区下的所有商品
|
# 收集 zone_ids 后批量删除,避免循环内 N+1 delete
|
||||||
for zone in zones:
|
zone_ids = list(zones.values_list('id', flat=True))
|
||||||
Shangpin.query.filter(zhuanqu_id=zone.id).delete()
|
if zone_ids:
|
||||||
zones.delete()
|
Shangpin.query.filter(zhuanqu_id__in=zone_ids).delete()
|
||||||
|
ShangpinZhuanqu.query.filter(id__in=zone_ids).delete()
|
||||||
# 删除直接关联该类型的商品
|
# 删除直接关联该类型的商品
|
||||||
Shangpin.query.filter(leixing_id=type_id).delete()
|
Shangpin.query.filter(leixing_id=type_id).delete()
|
||||||
else:
|
else:
|
||||||
@@ -928,7 +934,8 @@ class PopupNoticeModifyAPIView(APIView):
|
|||||||
if cascade:
|
if cascade:
|
||||||
# 级联删除:使用事务保证原子性
|
# 级联删除:使用事务保证原子性
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
popups = page.popups.all()
|
# 预加载 images 关联,避免循环内 N+1 访问 related manager
|
||||||
|
popups = page.popups.prefetch_related('images')
|
||||||
for popup in popups:
|
for popup in popups:
|
||||||
# 删除弹窗下的所有图片文件
|
# 删除弹窗下的所有图片文件
|
||||||
for img in popup.images.all():
|
for img in popup.images.all():
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import hmac
|
import hmac
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
@@ -19,7 +19,7 @@ from django.conf import settings
|
|||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db import models, transaction, IntegrityError
|
from django.db import models, transaction, IntegrityError
|
||||||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
from django.db.models import Q, Count, Sum, F, Exists, OuterRef, Prefetch
|
||||||
from gvsdsdk.fluent import db, func, FQ
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
@@ -132,13 +132,16 @@ class KhpzhqView(APIView):
|
|||||||
|
|
||||||
bankuai_list = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
|
bankuai_list = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
|
||||||
|
|
||||||
chenghao_qs = Chenghao.query.prefetch_related('kaohecishufeiyong_set').all()
|
chenghao_qs = Chenghao.query.prefetch_related(
|
||||||
|
Prefetch('kaohecishufeiyong_set', queryset=KaoheCishuFeiyong.objects.order_by('cishu'))
|
||||||
|
).all()
|
||||||
chenghao_list = []
|
chenghao_list = []
|
||||||
for ch in chenghao_qs:
|
for ch in chenghao_qs:
|
||||||
# 获取费用:按次数排序,转为 {cishu: feiyong} 字典
|
# 获取费用:按次数排序,转为 {cishu: feiyong} 字典
|
||||||
|
# prefetch_related 已用 Prefetch 排序,直接遍历不会绕过缓存
|
||||||
feiyong_dict = {
|
feiyong_dict = {
|
||||||
item.cishu: float(item.feiyong)
|
item.cishu: float(item.feiyong)
|
||||||
for item in ch.kaohecishufeiyong_set.order_by('cishu')
|
for item in ch.kaohecishufeiyong_set.all()
|
||||||
}
|
}
|
||||||
max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0
|
max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import hmac
|
import hmac
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
@@ -351,9 +351,16 @@ class GetZuzhangDetailView(APIView):
|
|||||||
custom_first = {} # 首次分红定制(cishu=1)
|
custom_first = {} # 首次分红定制(cishu=1)
|
||||||
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } }
|
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } }
|
||||||
|
|
||||||
|
# 批量预取会员信息
|
||||||
|
_huiyuan_ids = [r.huiyuan for r in duoci_records]
|
||||||
|
_huiyuan_map = {
|
||||||
|
h.huiyuan_id: h
|
||||||
|
for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao', 'jiage')
|
||||||
|
}
|
||||||
|
|
||||||
for record in duoci_records:
|
for record in duoci_records:
|
||||||
# 获取会员信息
|
# 获取会员信息
|
||||||
member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first()
|
member = _huiyuan_map.get(record.huiyuan)
|
||||||
if not member:
|
if not member:
|
||||||
continue
|
continue
|
||||||
member_info = {
|
member_info = {
|
||||||
@@ -580,9 +587,16 @@ class UpdateZuzhangView(APIView):
|
|||||||
if custom_first is not None and isinstance(custom_first, dict):
|
if custom_first is not None and isinstance(custom_first, dict):
|
||||||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||||
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||||
existing_first = DuociFenhong.query.filter(
|
# 批量预取当前所有首次定制记录(避免循环内 N+1)
|
||||||
club_id=zuzhang_club, yonghuid=yonghuid, cishu=1,
|
existing_first_list = list(
|
||||||
|
DuociFenhong.query.filter(
|
||||||
|
club_id=zuzhang_club, yonghuid=yonghuid, cishu=1,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
_existing_map = {r.huiyuan: r for r in existing_first_list}
|
||||||
|
_to_create = []
|
||||||
|
_to_update = []
|
||||||
|
_to_delete_ids = []
|
||||||
for huiyuan_id, amount in custom_first.items():
|
for huiyuan_id, amount in custom_first.items():
|
||||||
try:
|
try:
|
||||||
amount = Decimal(str(amount))
|
amount = Decimal(str(amount))
|
||||||
@@ -590,30 +604,41 @@ class UpdateZuzhangView(APIView):
|
|||||||
continue
|
continue
|
||||||
if amount <= 0:
|
if amount <= 0:
|
||||||
# 删除定制(恢复默认)
|
# 删除定制(恢复默认)
|
||||||
DuociFenhong.query.filter(
|
rec = _existing_map.get(huiyuan_id)
|
||||||
club_id=zuzhang_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1,
|
if rec is not None:
|
||||||
).delete()
|
_to_delete_ids.append(rec.id)
|
||||||
else:
|
else:
|
||||||
DuociFenhong.query.update_or_create(
|
rec = _existing_map.get(huiyuan_id)
|
||||||
club_id=zuzhang_club,
|
if rec is not None:
|
||||||
yonghuid=yonghuid,
|
rec.zuzhang_fenhong = amount
|
||||||
huiyuan=huiyuan_id,
|
rec.guanshi_fenhong = Decimal('0') # 组长表只关心组长分红
|
||||||
cishu=1,
|
_to_update.append(rec)
|
||||||
defaults={
|
else:
|
||||||
'zuzhang_fenhong': amount,
|
_to_create.append(DuociFenhong(
|
||||||
'guanshi_fenhong': Decimal('0') # 组长表只关心组长分红
|
club_id=zuzhang_club,
|
||||||
}
|
yonghuid=yonghuid,
|
||||||
)
|
huiyuan=huiyuan_id,
|
||||||
|
cishu=1,
|
||||||
|
zuzhang_fenhong=amount,
|
||||||
|
guanshi_fenhong=Decimal('0'),
|
||||||
|
))
|
||||||
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
|
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
|
||||||
for record in existing_first:
|
for rec in existing_first_list:
|
||||||
if record.huiyuan not in custom_first:
|
if rec.huiyuan not in custom_first and rec.id not in _to_delete_ids:
|
||||||
record.delete()
|
_to_delete_ids.append(rec.id)
|
||||||
|
if _to_create:
|
||||||
|
DuociFenhong.objects.bulk_create(_to_create)
|
||||||
|
if _to_update:
|
||||||
|
DuociFenhong.objects.bulk_update(_to_update, ['zuzhang_fenhong', 'guanshi_fenhong'])
|
||||||
|
if _to_delete_ids:
|
||||||
|
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
|
||||||
|
|
||||||
# 4.3 额外次数分红(cishu >= 2)
|
# 4.3 额外次数分红(cishu >= 2)
|
||||||
if extra_map is not None and isinstance(extra_map, dict):
|
if extra_map is not None and isinstance(extra_map, dict):
|
||||||
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||||
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||||
keep_set = set()
|
keep_set = set()
|
||||||
|
_extra_targets = [] # [(cishu, huiyuan_id, amount)]
|
||||||
for times_str, members in extra_map.items():
|
for times_str, members in extra_map.items():
|
||||||
try:
|
try:
|
||||||
cishu = int(times_str)
|
cishu = int(times_str)
|
||||||
@@ -634,23 +659,42 @@ class UpdateZuzhangView(APIView):
|
|||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
keep_set.add((cishu, huiyuan_id))
|
keep_set.add((cishu, huiyuan_id))
|
||||||
DuociFenhong.query.update_or_create(
|
_extra_targets.append((cishu, huiyuan_id, amount))
|
||||||
|
# 批量预取已有额外次数记录
|
||||||
|
existing_extra_list = list(
|
||||||
|
DuociFenhong.query.filter(
|
||||||
|
club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_extra_map = {(r.cishu, r.huiyuan): r for r in existing_extra_list}
|
||||||
|
_to_create = []
|
||||||
|
_to_update = []
|
||||||
|
_to_delete_ids = []
|
||||||
|
for cishu, huiyuan_id, amount in _extra_targets:
|
||||||
|
rec = _extra_map.get((cishu, huiyuan_id))
|
||||||
|
if rec is not None:
|
||||||
|
rec.zuzhang_fenhong = amount
|
||||||
|
rec.guanshi_fenhong = Decimal('0')
|
||||||
|
_to_update.append(rec)
|
||||||
|
else:
|
||||||
|
_to_create.append(DuociFenhong(
|
||||||
club_id=zuzhang_club,
|
club_id=zuzhang_club,
|
||||||
yonghuid=yonghuid,
|
yonghuid=yonghuid,
|
||||||
huiyuan=huiyuan_id,
|
huiyuan=huiyuan_id,
|
||||||
cishu=cishu,
|
cishu=cishu,
|
||||||
defaults={
|
zuzhang_fenhong=amount,
|
||||||
'zuzhang_fenhong': amount,
|
guanshi_fenhong=Decimal('0'),
|
||||||
'guanshi_fenhong': Decimal('0')
|
))
|
||||||
}
|
|
||||||
)
|
|
||||||
# 删除不在 keep_set 中的额外次数记录
|
# 删除不在 keep_set 中的额外次数记录
|
||||||
existing_extra = DuociFenhong.query.filter(
|
for rec in existing_extra_list:
|
||||||
club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2,
|
if (rec.cishu, rec.huiyuan) not in keep_set:
|
||||||
)
|
_to_delete_ids.append(rec.id)
|
||||||
for record in existing_extra:
|
if _to_create:
|
||||||
if (record.cishu, record.huiyuan) not in keep_set:
|
DuociFenhong.objects.bulk_create(_to_create)
|
||||||
record.delete()
|
if _to_update:
|
||||||
|
DuociFenhong.objects.bulk_update(_to_update, ['zuzhang_fenhong', 'guanshi_fenhong'])
|
||||||
|
if _to_delete_ids:
|
||||||
|
DuociFenhong.objects.filter(id__in=_to_delete_ids).delete()
|
||||||
|
|
||||||
dividend_parts = []
|
dividend_parts = []
|
||||||
if perm_enabled is not None:
|
if perm_enabled is not None:
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ class ZxkfghdsView(APIView):
|
|||||||
shangjia_ext = old_order.shangjia_kuozhan
|
shangjia_ext = old_order.shangjia_kuozhan
|
||||||
shangjia_id = shangjia_ext.MerchantID
|
shangjia_id = shangjia_ext.MerchantID
|
||||||
# 获取商家昵称用于新订单扩展表
|
# 获取商家昵称用于新订单扩展表
|
||||||
shangjia_user = User.query.get(UserUID=shangjia_id)
|
shangjia_user = User.query.select_related('ShopProfile').get(UserUID=shangjia_id)
|
||||||
shangjia_nicheng = shangjia_user.ShopProfile.nicheng or f"商家{shangjia_id}"
|
shangjia_nicheng = shangjia_user.ShopProfile.nicheng or f"商家{shangjia_id}"
|
||||||
except (ObjectDoesNotExist, UserShangjia.DoesNotExist):
|
except (ObjectDoesNotExist, UserShangjia.DoesNotExist):
|
||||||
logger.error(f"商家信息缺失,订单: {OrderID}")
|
logger.error(f"商家信息缺失,订单: {OrderID}")
|
||||||
|
|||||||
@@ -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 os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@@ -394,40 +394,46 @@ class AdminUpdateConfigView(APIView):
|
|||||||
# 🔴 修复:更新QQ群配置 - 确保根据peizhiid正确更新对应的记录
|
# 🔴 修复:更新QQ群配置 - 确保根据peizhiid正确更新对应的记录
|
||||||
qq_groups = request.data.get('qq_groups')
|
qq_groups = request.data.get('qq_groups')
|
||||||
if qq_groups and isinstance(qq_groups, list):
|
if qq_groups and isinstance(qq_groups, list):
|
||||||
|
# 收集所有 peizhiid,批量预取已有记录(避免循环内 N+1)
|
||||||
|
_peizhiids = [g.get('peizhiid') for g in qq_groups if g.get('peizhiid')]
|
||||||
|
_existing_map = {}
|
||||||
|
if _peizhiids:
|
||||||
|
for q in Qunpeizhi.query.filter(GroupType__in=_peizhiids):
|
||||||
|
# 如果存在多个相同 peizhiid 的记录,取第一个(应该只有一个)
|
||||||
|
if q.GroupType not in _existing_map:
|
||||||
|
_existing_map[q.GroupType] = q
|
||||||
|
|
||||||
|
_to_create = []
|
||||||
|
_to_update = []
|
||||||
updated_groups = []
|
updated_groups = []
|
||||||
for group_data in qq_groups:
|
for group_data in qq_groups:
|
||||||
peizhiid = group_data.get('peizhiid')
|
peizhiid = group_data.get('peizhiid')
|
||||||
if not peizhiid:
|
if not peizhiid:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 🔴 修复:使用filter()确保查询到正确的记录
|
neirong = group_data.get('neirong')
|
||||||
qun_configs = Qunpeizhi.query.filter(GroupType=peizhiid)
|
qunid = group_data.get('qunid')
|
||||||
|
jieshao = group_data.get('jieshao')
|
||||||
if qun_configs.exists():
|
|
||||||
# 如果存在多个相同peizhiid的记录,取第一个(应该只有一个)
|
|
||||||
qun_config = qun_configs.first()
|
|
||||||
|
|
||||||
# 更新字段
|
|
||||||
neirong = group_data.get('neirong')
|
|
||||||
qunid = group_data.get('qunid')
|
|
||||||
jieshao = group_data.get('jieshao')
|
|
||||||
|
|
||||||
|
qun_config = _existing_map.get(peizhiid)
|
||||||
|
if qun_config is not None:
|
||||||
|
# 更新已有记录
|
||||||
if neirong is not None:
|
if neirong is not None:
|
||||||
qun_config.GroupContent = neirong or ''
|
qun_config.GroupContent = neirong or ''
|
||||||
if qunid is not None:
|
if qunid is not None:
|
||||||
qun_config.GroupID = qunid or ''
|
qun_config.GroupID = qunid or ''
|
||||||
if jieshao is not None:
|
if jieshao is not None:
|
||||||
qun_config.Description = jieshao or ''
|
qun_config.Description = jieshao or ''
|
||||||
|
_to_update.append(qun_config)
|
||||||
qun_config.save()
|
|
||||||
else:
|
else:
|
||||||
# 如果不存在,创建新的(使用提供的peizhiid)
|
# 不存在则创建新的
|
||||||
qun_config = Qunpeizhi.query.create(
|
qun_config = Qunpeizhi(
|
||||||
GroupType=peizhiid,
|
GroupType=peizhiid,
|
||||||
GroupContent=group_data.get('neirong', '') or '',
|
GroupContent=neirong or '',
|
||||||
GroupID=group_data.get('qunid', '') or '',
|
GroupID=qunid or '',
|
||||||
Description=group_data.get('jieshao', '') or ''
|
Description=jieshao or '',
|
||||||
)
|
)
|
||||||
|
_to_create.append(qun_config)
|
||||||
|
|
||||||
updated_groups.append({
|
updated_groups.append({
|
||||||
'peizhiid': qun_config.GroupType,
|
'peizhiid': qun_config.GroupType,
|
||||||
@@ -436,6 +442,13 @@ class AdminUpdateConfigView(APIView):
|
|||||||
'jieshao': qun_config.Description
|
'jieshao': qun_config.Description
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if _to_create:
|
||||||
|
Qunpeizhi.objects.bulk_create(_to_create)
|
||||||
|
if _to_update:
|
||||||
|
Qunpeizhi.objects.bulk_update(
|
||||||
|
_to_update, ['GroupContent', 'GroupID', 'Description']
|
||||||
|
)
|
||||||
|
|
||||||
if updated_groups:
|
if updated_groups:
|
||||||
response_data['qq_groups'] = updated_groups
|
response_data['qq_groups'] = updated_groups
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@@ -270,6 +270,13 @@ class DashouPaihangView(APIView):
|
|||||||
# 第一名数量不足,不展示
|
# 第一名数量不足,不展示
|
||||||
return [], False
|
return [], False
|
||||||
|
|
||||||
|
# 批量预取老板扩展表昵称
|
||||||
|
_boss_user_ids = [g.user_id for g in guanshi_list]
|
||||||
|
_boss_nicheng_map = {
|
||||||
|
b.user_id: b.nickname
|
||||||
|
for b in UserBoss.query.filter(user_id__in=_boss_user_ids).only('user_id', 'nickname')
|
||||||
|
}
|
||||||
|
|
||||||
# 构建返回数据
|
# 构建返回数据
|
||||||
paihang_list = []
|
paihang_list = []
|
||||||
for index, guanshi in enumerate(guanshi_list):
|
for index, guanshi in enumerate(guanshi_list):
|
||||||
@@ -277,8 +284,7 @@ class DashouPaihangView(APIView):
|
|||||||
user_main = guanshi.user
|
user_main = guanshi.user
|
||||||
|
|
||||||
# 管事的昵称需要查询老板扩展表
|
# 管事的昵称需要查询老板扩展表
|
||||||
boss_profile = UserBoss.query.filter(user=user_main).first()
|
guanshi_nicheng = _boss_nicheng_map.get(guanshi.user_id, '未设置昵称')
|
||||||
guanshi_nicheng = boss_profile.nickname if boss_profile else '未设置昵称'
|
|
||||||
|
|
||||||
# 获取头像(相对URL)
|
# 获取头像(相对URL)
|
||||||
touxiang_url = user_main.Avatar or ''
|
touxiang_url = user_main.Avatar or ''
|
||||||
@@ -337,6 +343,13 @@ class DashouPaihangView(APIView):
|
|||||||
# 第一名流水不足,不展示
|
# 第一名流水不足,不展示
|
||||||
return [], False
|
return [], False
|
||||||
|
|
||||||
|
# 批量预取老板扩展表昵称
|
||||||
|
_boss_user_ids = [s.user_id for s in shangjia_list]
|
||||||
|
_boss_nicheng_map = {
|
||||||
|
b.user_id: b.nickname
|
||||||
|
for b in UserBoss.query.filter(user_id__in=_boss_user_ids).only('user_id', 'nickname')
|
||||||
|
}
|
||||||
|
|
||||||
# 构建返回数据
|
# 构建返回数据
|
||||||
paihang_list = []
|
paihang_list = []
|
||||||
for index, shangjia in enumerate(shangjia_list):
|
for index, shangjia in enumerate(shangjia_list):
|
||||||
@@ -344,8 +357,7 @@ class DashouPaihangView(APIView):
|
|||||||
user_main = shangjia.user
|
user_main = shangjia.user
|
||||||
|
|
||||||
# 商家的昵称需要查询老板扩展表
|
# 商家的昵称需要查询老板扩展表
|
||||||
boss_profile = UserBoss.query.filter(user=user_main).first()
|
shangjia_nicheng = _boss_nicheng_map.get(shangjia.user_id, '未设置昵称')
|
||||||
shangjia_nicheng = boss_profile.nickname if boss_profile else '未设置昵称'
|
|
||||||
|
|
||||||
# 获取头像(相对URL)
|
# 获取头像(相对URL)
|
||||||
touxiang_url = user_main.Avatar or ''
|
touxiang_url = user_main.Avatar or ''
|
||||||
@@ -604,21 +616,21 @@ class GetXiugaiJiluView(APIView):
|
|||||||
records = cursor.fetchall()
|
records = cursor.fetchall()
|
||||||
|
|
||||||
# 🔥 6. 处理查询结果(严格按照字段类型处理)
|
# 🔥 6. 处理查询结果(严格按照字段类型处理)
|
||||||
|
# 预查询修改者账号信息(循环外执行一次,避免循环内重复查询同一 SQL)
|
||||||
|
xiugaizhe_zhanghao = ''
|
||||||
|
with connection.cursor() as cursor2:
|
||||||
|
cursor2.execute(
|
||||||
|
"SELECT phone, yonghuid FROM user_main WHERE phone = %s OR yonghuid = %s LIMIT 1",
|
||||||
|
[xiugaizhe_zhanghaoid, xiugaizhe_zhanghaoid]
|
||||||
|
)
|
||||||
|
xiugaizhe_info = cursor2.fetchone()
|
||||||
|
if xiugaizhe_info:
|
||||||
|
xiugaizhe_zhanghao = str(xiugaizhe_info[0]) if xiugaizhe_info[0] else str(xiugaizhe_info[1]) if \
|
||||||
|
xiugaizhe_info[1] else xiugaizhe_zhanghaoid
|
||||||
|
|
||||||
jilu_list = []
|
jilu_list = []
|
||||||
for record in records:
|
for record in records:
|
||||||
# 获取修改者账号信息(都是字符串)
|
# 修改者账号信息已预查询,直接使用
|
||||||
xiugaizhe_zhanghao = ''
|
|
||||||
with connection.cursor() as cursor2:
|
|
||||||
# 🔥 phone和yonghuid都是CharField,用字符串查询
|
|
||||||
cursor2.execute(
|
|
||||||
"SELECT phone, yonghuid FROM user_main WHERE phone = %s OR yonghuid = %s LIMIT 1",
|
|
||||||
[xiugaizhe_zhanghaoid, xiugaizhe_zhanghaoid]
|
|
||||||
)
|
|
||||||
xiugaizhe_info = cursor2.fetchone()
|
|
||||||
if xiugaizhe_info:
|
|
||||||
# 🔥 都是字符串
|
|
||||||
xiugaizhe_zhanghao = str(xiugaizhe_info[0]) if xiugaizhe_info[0] else str(xiugaizhe_info[1]) if \
|
|
||||||
xiugaizhe_info[1] else xiugaizhe_zhanghaoid
|
|
||||||
|
|
||||||
# 获取会员名称(如果有)
|
# 获取会员名称(如果有)
|
||||||
huiyuan_mingzi = ''
|
huiyuan_mingzi = ''
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@@ -508,7 +508,7 @@ class KehuGetDingdanLianjieView(APIView):
|
|||||||
# 获取商家昵称
|
# 获取商家昵称
|
||||||
shangjia_nicheng = ''
|
shangjia_nicheng = ''
|
||||||
try:
|
try:
|
||||||
user_main = User.query.get(UserUID=lianjie_obj.UserID)
|
user_main = User.query.select_related('ShopProfile').get(UserUID=lianjie_obj.UserID)
|
||||||
shangjia_obj = user_main.ShopProfile
|
shangjia_obj = user_main.ShopProfile
|
||||||
shangjia_nicheng = shangjia_obj.nicheng
|
shangjia_nicheng = shangjia_obj.nicheng
|
||||||
except (User.DoesNotExist, UserShangjia.DoesNotExist):
|
except (User.DoesNotExist, UserShangjia.DoesNotExist):
|
||||||
@@ -816,7 +816,7 @@ class KehuTianxieDingdanView(APIView):
|
|||||||
# 验证打手是否存在且有效
|
# 验证打手是否存在且有效
|
||||||
try:
|
try:
|
||||||
# 先查询用户主表
|
# 先查询用户主表
|
||||||
dashou_user = User.query.get(UserUID=zhiding_dashou)
|
dashou_user = User.query.select_related('DashouProfile').get(UserUID=zhiding_dashou)
|
||||||
logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}")
|
logger.info(f"找到打手用户 - 用户ID: {zhiding_dashou}")
|
||||||
|
|
||||||
# 通过反向关系获取打手扩展信息
|
# 通过反向关系获取打手扩展信息
|
||||||
@@ -922,7 +922,7 @@ class KehuTianxieDingdanView(APIView):
|
|||||||
|
|
||||||
# 获取商家信息用于返回
|
# 获取商家信息用于返回
|
||||||
try:
|
try:
|
||||||
user_main = User.query.get(UserUID=lianjie_obj.UserID)
|
user_main = User.query.select_related('ShopProfile').get(UserUID=lianjie_obj.UserID)
|
||||||
shangjia_obj = user_main.ShopProfile
|
shangjia_obj = user_main.ShopProfile
|
||||||
shangjia_mingcheng = shangjia_obj.nicheng
|
shangjia_mingcheng = shangjia_obj.nicheng
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
@@ -142,17 +142,21 @@ class ShangjiaMobanListView(APIView):
|
|||||||
return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400)
|
return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400)
|
||||||
|
|
||||||
current_page = paginator.page(page)
|
current_page = paginator.page(page)
|
||||||
|
# 批量预取所有称号,避免循环内 N+1
|
||||||
|
_mobans_list = list(current_page.object_list)
|
||||||
|
_title_ids = [m.TitleID for m in _mobans_list if m.TitleID]
|
||||||
|
_ch_map = {
|
||||||
|
ch.id: ch
|
||||||
|
for ch in Chenghao.query.filter(id__in=_title_ids).only('id', 'mingcheng', 'texiao_miaoshu')
|
||||||
|
} if _title_ids else {}
|
||||||
formatted = []
|
formatted = []
|
||||||
for moban in current_page.object_list:
|
for moban in _mobans_list:
|
||||||
texiao_json = ''
|
texiao_json = ''
|
||||||
label_name = ''
|
label_name = ''
|
||||||
if moban.TitleID:
|
ch = _ch_map.get(moban.TitleID)
|
||||||
try:
|
if ch:
|
||||||
ch = Chenghao.query.get(id=moban.TitleID)
|
label_name = ch.mingcheng
|
||||||
label_name = ch.mingcheng
|
texiao_json = ch.texiao_miaoshu or ''
|
||||||
texiao_json = ch.texiao_miaoshu or ''
|
|
||||||
except Chenghao.DoesNotExist:
|
|
||||||
pass
|
|
||||||
formatted.append({
|
formatted.append({
|
||||||
'mobanId': moban.id,
|
'mobanId': moban.id,
|
||||||
'shangpinTypeId': moban.ProductTypeID,
|
'shangpinTypeId': moban.ProductTypeID,
|
||||||
|
|||||||
@@ -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 不变)。"""
|
"""俱乐部维度财务统计(供 /jituan/houtai/caiwu,旧 /houtai/caiwu 不变)。"""
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
from django.db.models import Sum
|
from django.db.models import Count, Q, Sum
|
||||||
|
|
||||||
from jituan.constants import DATA_SCOPE_ALL
|
from jituan.constants import DATA_SCOPE_ALL
|
||||||
from jituan.services.club_context import (
|
from jituan.services.club_context import (
|
||||||
@@ -61,31 +61,35 @@ def build_caiwu_payload(request):
|
|||||||
today_income_amount, today_income_count = today_income_for_dashboard(today, request)
|
today_income_amount, today_income_count = today_income_for_dashboard(today, request)
|
||||||
today_payout_amount, today_payout_count = sum_payout_stat_for_date(today, request)
|
today_payout_amount, today_payout_count = sum_payout_stat_for_date(today, request)
|
||||||
|
|
||||||
today_orders = order_qs.filter(
|
# 合并订单统计:6 次独立 count/aggregate → 1 次 aggregate
|
||||||
|
_today_order_agg = order_qs.filter(
|
||||||
CreateTime__gte=today_start,
|
CreateTime__gte=today_start,
|
||||||
CreateTime__lt=today_end,
|
CreateTime__lt=today_end,
|
||||||
Status__in=[1, 2, 3, 4, 5, 6, 7, 8],
|
Status__in=[1, 2, 3, 4, 5, 6, 7, 8],
|
||||||
|
).aggregate(
|
||||||
|
today_order_count=Count('id'),
|
||||||
|
today_order_amount=Sum('Amount'),
|
||||||
|
today_completed_count=Count('id', filter=Q(Status=3)),
|
||||||
|
today_completed_amount=Sum('Amount', filter=Q(Status=3)),
|
||||||
|
today_tuikuan_count=Count('id', filter=Q(Status=5)),
|
||||||
|
today_tuikuan_amount=Sum('Amount', filter=Q(Status=5)),
|
||||||
)
|
)
|
||||||
today_order_count = today_orders.count()
|
today_order_count = _today_order_agg['today_order_count'] or 0
|
||||||
today_order_amount = float(today_orders.aggregate(total=Sum('Amount'))['total'] or 0.00)
|
today_order_amount = float(_today_order_agg['today_order_amount'] or 0.00)
|
||||||
|
today_completed_count = _today_order_agg['today_completed_count'] or 0
|
||||||
|
today_completed_amount = float(_today_order_agg['today_completed_amount'] or 0.00)
|
||||||
|
today_tuikuan_count = _today_order_agg['today_tuikuan_count'] or 0
|
||||||
|
today_tuikuan_amount = float(_today_order_agg['today_tuikuan_amount'] or 0.00)
|
||||||
|
|
||||||
today_completed = order_qs.filter(
|
# 合并充值统计:2 次独立 count/aggregate → 1 次 aggregate
|
||||||
CreateTime__gte=today_start, CreateTime__lt=today_end, Status=3,
|
_today_chongzhi_agg = cz_qs.filter(
|
||||||
)
|
|
||||||
today_completed_count = today_completed.count()
|
|
||||||
today_completed_amount = float(today_completed.aggregate(total=Sum('Amount'))['total'] or 0.00)
|
|
||||||
|
|
||||||
today_tuikuan = order_qs.filter(
|
|
||||||
CreateTime__gte=today_start, CreateTime__lt=today_end, Status=5,
|
|
||||||
)
|
|
||||||
today_tuikuan_count = today_tuikuan.count()
|
|
||||||
today_tuikuan_amount = float(today_tuikuan.aggregate(total=Sum('Amount'))['total'] or 0.00)
|
|
||||||
|
|
||||||
today_chongzhi = cz_qs.filter(
|
|
||||||
CreateTime__gte=today_start, CreateTime__lt=today_end, leixing=1, zhuangtai=3,
|
CreateTime__gte=today_start, CreateTime__lt=today_end, leixing=1, zhuangtai=3,
|
||||||
|
).aggregate(
|
||||||
|
count=Count('id'),
|
||||||
|
total=Sum('jine'),
|
||||||
)
|
)
|
||||||
today_chongzhi_count = today_chongzhi.count()
|
today_chongzhi_count = _today_chongzhi_agg['count'] or 0
|
||||||
today_chongzhi_amount = float(today_chongzhi.aggregate(total=Sum('jine'))['total'] or 0.00)
|
today_chongzhi_amount = float(_today_chongzhi_agg['total'] or 0.00)
|
||||||
|
|
||||||
new_huiyuan_user_ids = hy_qs.filter(
|
new_huiyuan_user_ids = hy_qs.filter(
|
||||||
CreateTime__gte=today_start, CreateTime__lt=today_end,
|
CreateTime__gte=today_start, CreateTime__lt=today_end,
|
||||||
|
|||||||
@@ -141,10 +141,13 @@ def count_order_status_buckets(q_conditions, request=None):
|
|||||||
('5', Q(Status=5)),
|
('5', Q(Status=5)),
|
||||||
('6', Q(Status=6)),
|
('6', Q(Status=6)),
|
||||||
)
|
)
|
||||||
counts = {'all': order_scope_qs(request).filter(q_conditions).count()}
|
# 合并 8 次独立 count → 1 次 aggregate with 条件 Count
|
||||||
|
from django.db.models import Count
|
||||||
|
_base = order_scope_qs(request).filter(q_conditions)
|
||||||
|
_agg_kwargs = {'all': Count('id')}
|
||||||
for key, status_q in buckets:
|
for key, status_q in buckets:
|
||||||
counts[key] = order_scope_qs(request).filter(q_conditions & status_q).count()
|
_agg_kwargs[key] = Count('id', filter=status_q)
|
||||||
return counts
|
return _base.aggregate(**_agg_kwargs)
|
||||||
|
|
||||||
|
|
||||||
def paginate_fluent_query(fluent_qs, page, page_size):
|
def paginate_fluent_query(fluent_qs, page, page_size):
|
||||||
|
|||||||
@@ -95,7 +95,12 @@ def count_club_member_sales(club_id, huiyuan_id, formal_only=True):
|
|||||||
def build_member_list_payload(request):
|
def build_member_list_payload(request):
|
||||||
scope = resolve_club_scope(request)
|
scope = resolve_club_scope(request)
|
||||||
club_id = resolve_club_id_from_request(request)
|
club_id = resolve_club_id_from_request(request)
|
||||||
members = Huiyuan.query.all().order_by('-CreateTime')
|
# P1 优化:限制查询字段,避免加载不必要的大字段
|
||||||
|
members = list(Huiyuan.query.all().only(
|
||||||
|
'huiyuan_id', 'jieshao', 'jtjieshao', 'jiage',
|
||||||
|
'guanshifc', 'zuzhangfc', 'goumai_cishu', 'bankuai_id',
|
||||||
|
'CreateTime', 'is_bundle',
|
||||||
|
).order_by('-CreateTime'))
|
||||||
member_list = []
|
member_list = []
|
||||||
|
|
||||||
price_map = {}
|
price_map = {}
|
||||||
@@ -105,6 +110,20 @@ def build_member_list_payload(request):
|
|||||||
for r in ClubHuiyuanPrice.query.filter(club_id=club_id)
|
for r in ClubHuiyuanPrice.query.filter(club_id=club_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# P1 优化:批量预取本俱乐部各会员的已支付单数,避免循环内 N+1
|
||||||
|
_sales_map = {}
|
||||||
|
if scope != DATA_SCOPE_ALL and members:
|
||||||
|
from products.models import Czjilu
|
||||||
|
from django.db.models import Count
|
||||||
|
_sales_rows = Czjilu.query.filter(
|
||||||
|
club_id=club_id,
|
||||||
|
huiyuan_id__in=[m.huiyuan_id for m in members],
|
||||||
|
leixing=1,
|
||||||
|
zhuangtai=3,
|
||||||
|
is_trial=False,
|
||||||
|
).values('huiyuan_id').annotate(cnt=Count('id'))
|
||||||
|
_sales_map = {r['huiyuan_id']: r['cnt'] for r in _sales_rows}
|
||||||
|
|
||||||
if scope == DATA_SCOPE_ALL:
|
if scope == DATA_SCOPE_ALL:
|
||||||
for m in members:
|
for m in members:
|
||||||
member_list.append(_format_member(m))
|
member_list.append(_format_member(m))
|
||||||
@@ -116,7 +135,7 @@ def build_member_list_payload(request):
|
|||||||
if row is not None and not row.is_enabled:
|
if row is not None and not row.is_enabled:
|
||||||
continue
|
continue
|
||||||
item = _format_member(m, row, club_id=club_id)
|
item = _format_member(m, row, club_id=club_id)
|
||||||
item['goumai_cishu'] = count_club_member_sales(club_id, m.huiyuan_id)
|
item['goumai_cishu'] = _sales_map.get(m.huiyuan_id, 0)
|
||||||
member_list.append(item)
|
member_list.append(item)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -138,17 +157,18 @@ def build_member_catalog_payload(request):
|
|||||||
enabled_ids = set(
|
enabled_ids = set(
|
||||||
ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True).values_list('huiyuan_id', flat=True)
|
ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True).values_list('huiyuan_id', flat=True)
|
||||||
)
|
)
|
||||||
all_members = Huiyuan.query.all().order_by('-CreateTime')
|
# P1 优化:DB 端排除已上架会员,限制字段,避免全表加载 + Python 过滤
|
||||||
catalog = []
|
catalog_qs = Huiyuan.query.all()
|
||||||
for m in all_members:
|
if enabled_ids:
|
||||||
if m.huiyuan_id in enabled_ids:
|
catalog_qs = catalog_qs.exclude(huiyuan_id__in=list(enabled_ids))
|
||||||
continue
|
catalog = list(
|
||||||
catalog.append({
|
catalog_qs.only('huiyuan_id', 'jieshao', 'jtjieshao', 'is_bundle')
|
||||||
'huiyuan_id': m.huiyuan_id,
|
.order_by('-CreateTime')
|
||||||
'jieshao': m.jieshao,
|
.values('huiyuan_id', 'jieshao', 'jtjieshao', 'is_bundle')
|
||||||
'jtjieshao': m.jtjieshao,
|
)
|
||||||
'is_bundle': bool(getattr(m, 'is_bundle', False)),
|
# values 返回 dict 列表,需补 is_bundle bool 化
|
||||||
})
|
for row in catalog:
|
||||||
|
row['is_bundle'] = bool(row.get('is_bundle'))
|
||||||
return {'club_id': club_id, 'catalog': catalog}
|
return {'club_id': club_id, 'catalog': catalog}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -218,14 +218,22 @@ def build_exam_bundle(request, user):
|
|||||||
return None, '题库暂无已上架题目,请至少录入一道题并上架(每题至少两个选项且标记正确答案)'
|
return None, '题库暂无已上架题目,请至少录入一道题并上架(每题至少两个选项且标记正确答案)'
|
||||||
|
|
||||||
selected_ids = random.sample(all_ids, draw)
|
selected_ids = random.sample(all_ids, draw)
|
||||||
|
# 批量预取题目和图片,避免循环内 N+1
|
||||||
|
_questions_map = {
|
||||||
|
q.id: q for q in ClubDashouExamQuestion.query.filter(id__in=selected_ids)
|
||||||
|
}
|
||||||
|
_imgs_map = {}
|
||||||
|
for img in ClubDashouExamQuestionImage.query.filter(
|
||||||
|
club_id=club_id, question_id__in=selected_ids,
|
||||||
|
).order_by('question_id', 'sort_order', 'id'):
|
||||||
|
_imgs_map.setdefault(img.question_id, []).append(img.image_url)
|
||||||
|
|
||||||
questions = []
|
questions = []
|
||||||
for qid in selected_ids:
|
for qid in selected_ids:
|
||||||
q = ClubDashouExamQuestion.query.get(id=qid)
|
q = _questions_map.get(qid)
|
||||||
imgs = [
|
if q is None:
|
||||||
r.image_url for r in ClubDashouExamQuestionImage.query.filter(
|
continue
|
||||||
club_id=club_id, question_id=qid,
|
imgs = _imgs_map.get(qid, [])
|
||||||
).order_by('sort_order', 'id')
|
|
||||||
]
|
|
||||||
questions.append(build_question_bundle_item(q, imgs))
|
questions.append(build_question_bundle_item(q, imgs))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -284,8 +292,14 @@ def build_admin_config_payload(request, user, data=None):
|
|||||||
if err:
|
if err:
|
||||||
return {**clubs_info, 'scope_error': err}
|
return {**clubs_info, 'scope_error': err}
|
||||||
cfg = get_or_create_config(club_id)
|
cfg = get_or_create_config(club_id)
|
||||||
pool = ClubDashouExamQuestion.query.filter(club_id=club_id).count()
|
# 合并 2 次独立 count → 1 次 aggregate with 条件 Count
|
||||||
enabled_pool = ClubDashouExamQuestion.query.filter(club_id=club_id, is_enabled=True).count()
|
from django.db.models import Count, Q
|
||||||
|
_pool_agg = ClubDashouExamQuestion.query.filter(club_id=club_id).aggregate(
|
||||||
|
pool=Count('id'),
|
||||||
|
enabled_pool=Count('id', filter=Q(is_enabled=True)),
|
||||||
|
)
|
||||||
|
pool = _pool_agg['pool'] or 0
|
||||||
|
enabled_pool = _pool_agg['enabled_pool'] or 0
|
||||||
return {
|
return {
|
||||||
**clubs_info,
|
**clubs_info,
|
||||||
'club_id': club_id,
|
'club_id': club_id,
|
||||||
@@ -319,14 +333,20 @@ def list_admin_questions(request, user, data=None):
|
|||||||
if err:
|
if err:
|
||||||
return {**exam_admin_clubs_payload(user), 'list': [], 'scope_error': err}
|
return {**exam_admin_clubs_payload(user), 'list': [], 'scope_error': err}
|
||||||
rows = ClubDashouExamQuestion.query.filter(club_id=club_id).order_by('-sort_order', 'id')
|
rows = ClubDashouExamQuestion.query.filter(club_id=club_id).order_by('-sort_order', 'id')
|
||||||
out = []
|
rows_list = list(rows)
|
||||||
for q in rows:
|
_q_ids = [q.id for q in rows_list]
|
||||||
imgs = [
|
# 批量预取所有题目图片,避免循环内 N+1
|
||||||
|
_imgs_map = {}
|
||||||
|
for img in ClubDashouExamQuestionImage.query.filter(
|
||||||
|
club_id=club_id, question_id__in=_q_ids,
|
||||||
|
).order_by('question_id', 'sort_order', 'id'):
|
||||||
|
_imgs_map.setdefault(img.question_id, []).append(
|
||||||
{'id': img.id, 'image_url': img.image_url, 'sort_order': img.sort_order}
|
{'id': img.id, 'image_url': img.image_url, 'sort_order': img.sort_order}
|
||||||
for img in ClubDashouExamQuestionImage.query.filter(
|
)
|
||||||
club_id=club_id, question_id=q.id,
|
|
||||||
).order_by('sort_order', 'id')
|
out = []
|
||||||
]
|
for q in rows_list:
|
||||||
|
imgs = _imgs_map.get(q.id, [])
|
||||||
out.append(question_to_admin_dict(q, imgs))
|
out.append(question_to_admin_dict(q, imgs))
|
||||||
return {**exam_admin_clubs_payload(user), 'club_id': club_id, 'list': out}
|
return {**exam_admin_clubs_payload(user), 'club_id': club_id, 'list': out}
|
||||||
|
|
||||||
|
|||||||
@@ -203,19 +203,31 @@ def build_order_finance_payload(
|
|||||||
})
|
})
|
||||||
|
|
||||||
summary_qs = filter_queryset_by_club(Order.query.filter(**filters), request, club_field='ClubID')
|
summary_qs = filter_queryset_by_club(Order.query.filter(**filters), request, club_field='ClubID')
|
||||||
total_order_count = summary_qs.count()
|
# 合并 13 次独立 count/aggregate → 1 次 aggregate
|
||||||
total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0
|
_summary_agg = summary_qs.aggregate(
|
||||||
total_success_count = summary_qs.filter(Status=3).count()
|
total_order_count=Count('id'),
|
||||||
total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0
|
total_order_amount=Sum('Amount'),
|
||||||
total_refund_count = summary_qs.filter(Status=5).count()
|
total_success_count=Count('id', filter=Q(Status=3)),
|
||||||
total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
total_success_amount=Sum('Amount', filter=Q(Status=3)),
|
||||||
total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate(
|
total_refund_count=Count('id', filter=Q(Status=5)),
|
||||||
s=Sum('pingtai_kuozhan__ShopIncome'),
|
total_dashou=Sum('PlayerCommission', filter=Q(Status=3)),
|
||||||
)['s'] or 0
|
total_dianpu_fenhong=Sum('pingtai_kuozhan__ShopIncome', filter=Q(Status=3, Platform=1)),
|
||||||
platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0
|
platform_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
|
||||||
merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0
|
merchant_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
|
||||||
platform_dashou_sum = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
platform_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
|
||||||
merchant_dashou_sum = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
merchant_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
|
||||||
|
)
|
||||||
|
total_order_count = _summary_agg['total_order_count'] or 0
|
||||||
|
total_order_amount = _summary_agg['total_order_amount'] or 0
|
||||||
|
total_success_count = _summary_agg['total_success_count'] or 0
|
||||||
|
total_success_amount = _summary_agg['total_success_amount'] or 0
|
||||||
|
total_refund_count = _summary_agg['total_refund_count'] or 0
|
||||||
|
total_dashou = _summary_agg['total_dashou'] or 0
|
||||||
|
total_dianpu_fenhong = _summary_agg['total_dianpu_fenhong'] or 0
|
||||||
|
platform_amount = _summary_agg['platform_amount'] or 0
|
||||||
|
merchant_amount = _summary_agg['merchant_amount'] or 0
|
||||||
|
platform_dashou_sum = _summary_agg['platform_dashou_sum'] or 0
|
||||||
|
merchant_dashou_sum = _summary_agg['merchant_dashou_sum'] or 0
|
||||||
profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
|
profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
|
||||||
profit_merchant = merchant_amount - merchant_dashou_sum
|
profit_merchant = merchant_amount - merchant_dashou_sum
|
||||||
total_profit = profit_platform + profit_merchant
|
total_profit = profit_platform + profit_merchant
|
||||||
@@ -392,10 +404,16 @@ def build_huiyuan_bankuai_payload(request):
|
|||||||
'zuzhangfc': float(row.zuzhangfc or 0),
|
'zuzhangfc': float(row.zuzhangfc or 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 批量预取所有 bankuai 和 huiyuan,避免循环内 N+1
|
||||||
|
_bankuai_qs = Bankuai.query.all().only('bankuai_id', 'mingcheng')
|
||||||
|
_members_by_bankuai = {}
|
||||||
|
for m in Huiyuan.query.values('huiyuan_id', 'jieshao', 'bankuai_id'):
|
||||||
|
_members_by_bankuai.setdefault(m['bankuai_id'], []).append(m)
|
||||||
|
|
||||||
bankuai_list = []
|
bankuai_list = []
|
||||||
for bk in Bankuai.query.all():
|
for bk in _bankuai_qs:
|
||||||
members = []
|
members = []
|
||||||
for m in Huiyuan.query.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id'):
|
for m in _members_by_bankuai.get(bk.bankuai_id, []):
|
||||||
item = dict(m)
|
item = dict(m)
|
||||||
if m['huiyuan_id'] in price_map:
|
if m['huiyuan_id'] in price_map:
|
||||||
item['club_price'] = price_map[m['huiyuan_id']]
|
item['club_price'] = price_map[m['huiyuan_id']]
|
||||||
|
|||||||
@@ -188,11 +188,11 @@ def zero_all_income_statistics():
|
|||||||
except _PLATFORM_LOG_ERRORS:
|
except _PLATFORM_LOG_ERRORS:
|
||||||
pass
|
pass
|
||||||
DailyIncomeStat.objects.all().delete()
|
DailyIncomeStat.objects.all().delete()
|
||||||
for szjilu in Szjilu.objects.select_for_update().all():
|
from django.utils import timezone
|
||||||
szjilu.TotalIncome = _ZERO
|
Szjilu.objects.update(
|
||||||
szjilu.TotalFlow = _ZERO
|
TotalIncome=_ZERO, TotalFlow=_ZERO, DailyFlow=_ZERO,
|
||||||
szjilu.DailyFlow = _ZERO
|
UpdateTime=timezone.now(),
|
||||||
szjilu.save(update_fields=['TotalIncome', 'TotalFlow', 'DailyFlow', 'UpdateTime'])
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -689,13 +689,12 @@ def get_szjilu_snapshot(club_id=None):
|
|||||||
|
|
||||||
def reset_all_daily_szjilu():
|
def reset_all_daily_szjilu():
|
||||||
"""每日清零:所有俱乐部的今日流水/今日支出。"""
|
"""每日清零:所有俱乐部的今日流水/今日支出。"""
|
||||||
updated = 0
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for szjilu in Szjilu.objects.select_for_update().all():
|
from django.utils import timezone
|
||||||
szjilu.DailyExpense = _ZERO
|
updated = Szjilu.objects.update(
|
||||||
szjilu.DailyFlow = _ZERO
|
DailyExpense=_ZERO, DailyFlow=_ZERO,
|
||||||
szjilu.save(update_fields=['DailyExpense', 'DailyFlow', 'UpdateTime'])
|
UpdateTime=timezone.now(),
|
||||||
updated += 1
|
)
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,13 @@ class ClubKhgglAddView(APIView):
|
|||||||
if not user_belongs_to_request_club(user, request):
|
if not user_belongs_to_request_club(user, request):
|
||||||
return Response({'code': 403, 'msg': '该用户不属于当前俱乐部,无法添加为考核官'}, status=403)
|
return Response({'code': 403, 'msg': '该用户不属于当前俱乐部,无法添加为考核官'}, status=403)
|
||||||
|
|
||||||
|
# 一次性查询所有板块是否存在,避免循环内 N+1 exists
|
||||||
|
_existing_bids = set(
|
||||||
|
Bankuai.query.filter(bankuai_id__in=bankuai_ids)
|
||||||
|
.values_list('bankuai_id', flat=True)
|
||||||
|
)
|
||||||
for bid in bankuai_ids:
|
for bid in bankuai_ids:
|
||||||
if not Bankuai.query.filter(bankuai_id=bid).exists():
|
if bid not in _existing_bids:
|
||||||
return Response({'code': 1, 'msg': f'板块不存在: {bid}'}, status=400)
|
return Response({'code': 1, 'msg': f'板块不存在: {bid}'}, status=400)
|
||||||
|
|
||||||
created_new = False
|
created_new = False
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ def staff_dispatch_order(member, staff_user, data):
|
|||||||
zhiding_dashou = None
|
zhiding_dashou = None
|
||||||
if zhiding_uid:
|
if zhiding_uid:
|
||||||
try:
|
try:
|
||||||
zhiding_user = User.query.get(UserUID=zhiding_uid)
|
zhiding_user = User.query.select_related('DashouProfile').get(UserUID=zhiding_uid)
|
||||||
zhiding_dashou = zhiding_user.DashouProfile
|
zhiding_dashou = zhiding_user.DashouProfile
|
||||||
if zhiding_dashou.zhanghaozhuangtai != 1:
|
if zhiding_dashou.zhanghaozhuangtai != 1:
|
||||||
return False, {'code': 400, 'msg': '指定打手状态异常', 'data': None}
|
return False, {'code': 400, 'msg': '指定打手状态异常', 'data': None}
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ def staff_generate_link(member, staff_user, data):
|
|||||||
final_huiyuan_id = shangpin_leixing.huiyuan_id or ''
|
final_huiyuan_id = shangpin_leixing.huiyuan_id or ''
|
||||||
|
|
||||||
label_name = ''
|
label_name = ''
|
||||||
|
ch = None
|
||||||
if chenghao_id:
|
if chenghao_id:
|
||||||
ch = Chenghao.query.filter(id=chenghao_id).first()
|
ch = Chenghao.query.filter(id=chenghao_id).first()
|
||||||
if ch:
|
if ch:
|
||||||
@@ -155,10 +156,9 @@ def staff_generate_link(member, staff_user, data):
|
|||||||
MerchantNickname=shangjia.nicheng or f"商家{merchant_id}",
|
MerchantNickname=shangjia.nicheng or f"商家{merchant_id}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if chenghao_id:
|
# 复用已查询的 ch 对象,避免重复查询
|
||||||
ch = Chenghao.query.filter(id=chenghao_id).first()
|
if ch:
|
||||||
if ch:
|
DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=ch)
|
||||||
DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=ch)
|
|
||||||
|
|
||||||
UserShangjia.query.filter(id=shangjia.id).update(
|
UserShangjia.query.filter(id=shangjia.id).update(
|
||||||
fabu=F('fabu') + 1,
|
fabu=F('fabu') + 1,
|
||||||
|
|||||||
@@ -128,8 +128,16 @@ class StaffInviteListView(APIView):
|
|||||||
qs = MerchantStaffInvite.query.filter(merchant_id=request.user.UserUID).select_related('role')
|
qs = MerchantStaffInvite.query.filter(merchant_id=request.user.UserUID).select_related('role')
|
||||||
if status_filter is not None:
|
if status_filter is not None:
|
||||||
qs = qs.filter(status=int(status_filter))
|
qs = qs.filter(status=int(status_filter))
|
||||||
|
invites = list(qs.order_by('-CreateTime')[:200])
|
||||||
|
# 批量预取已使用邀请码对应的用户,避免循环内 N+1
|
||||||
|
from users.business_models import User
|
||||||
|
_used_uids = [inv.used_by for inv in invites if inv.used_by]
|
||||||
|
_user_map = {
|
||||||
|
u.UserUID: u
|
||||||
|
for u in User.query.filter(UserUID__in=_used_uids).only('UserUID', 'Nickname')
|
||||||
|
} if _used_uids else {}
|
||||||
items = []
|
items = []
|
||||||
for inv in qs.order_by('-CreateTime')[:200]:
|
for inv in invites:
|
||||||
item = {
|
item = {
|
||||||
'id': inv.id,
|
'id': inv.id,
|
||||||
'code': inv.invite_code,
|
'code': inv.invite_code,
|
||||||
@@ -140,8 +148,7 @@ class StaffInviteListView(APIView):
|
|||||||
'user': None,
|
'user': None,
|
||||||
}
|
}
|
||||||
if inv.used_by:
|
if inv.used_by:
|
||||||
from users.business_models import User
|
u = _user_map.get(inv.used_by)
|
||||||
u = User.query.filter(UserUID=inv.used_by).first()
|
|
||||||
item['user'] = {'nickname': getattr(u, 'Nickname', '') or inv.used_by, 'uid': inv.used_by}
|
item['user'] = {'nickname': getattr(u, 'Nickname', '') or inv.used_by, 'uid': inv.used_by}
|
||||||
items.append(item)
|
items.append(item)
|
||||||
return Response({'code': 0, 'msg': '成功', 'data': {'list': items, 'gudingCode': 'XQKF2026'}})
|
return Response({'code': 0, 'msg': '成功', 'data': {'list': items, 'gudingCode': 'XQKF2026'}})
|
||||||
@@ -181,21 +188,51 @@ class StaffMemberListView(APIView):
|
|||||||
merchant_id=mid,
|
merchant_id=mid,
|
||||||
status__in=[MEMBER_STATUS_ACTIVE, MEMBER_STATUS_DISABLED],
|
status__in=[MEMBER_STATUS_ACTIVE, MEMBER_STATUS_DISABLED],
|
||||||
).select_related('role')
|
).select_related('role')
|
||||||
result = []
|
members_list = list(members)
|
||||||
for m in members:
|
_member_ids = [m.id for m in members_list]
|
||||||
total_stats = MerchantStaffDailyStats.query.filter(member_id=m.id).aggregate(
|
_staff_uids = [m.staff_user_id for m in members_list if m.staff_user_id]
|
||||||
|
|
||||||
|
# 批量预取聚合统计,避免循环内 N+1
|
||||||
|
_total_stats_map = {}
|
||||||
|
_today_stat_map = {}
|
||||||
|
if _member_ids:
|
||||||
|
from django.db.models import Count
|
||||||
|
for item in MerchantStaffDailyStats.query.filter(
|
||||||
|
member_id__in=_member_ids
|
||||||
|
).values('member_id').annotate(
|
||||||
dispatch_count=Sum('dispatch_count'),
|
dispatch_count=Sum('dispatch_count'),
|
||||||
dispatch_amount=Sum('dispatch_amount'),
|
dispatch_amount=Sum('dispatch_amount'),
|
||||||
settle_count=Sum('settle_count'),
|
settle_count=Sum('settle_count'),
|
||||||
settle_amount=Sum('settle_amount'),
|
settle_amount=Sum('settle_amount'),
|
||||||
refund_count=Sum('refund_count'),
|
refund_count=Sum('refund_count'),
|
||||||
refund_amount=Sum('refund_amount'),
|
refund_amount=Sum('refund_amount'),
|
||||||
)
|
):
|
||||||
today_stat = MerchantStaffDailyStats.query.filter(
|
_total_stats_map[item['member_id']] = item
|
||||||
member_id=m.id, stat_date=today,
|
for s in MerchantStaffDailyStats.query.filter(
|
||||||
).first()
|
member_id__in=_member_ids, stat_date=today,
|
||||||
wallet = get_or_create_wallet(m)
|
):
|
||||||
u = User.query.filter(UserUID=m.staff_user_id).first()
|
_today_stat_map[s.member_id] = s
|
||||||
|
|
||||||
|
# 批量预取用户头像
|
||||||
|
_user_map = {
|
||||||
|
u.UserUID: u
|
||||||
|
for u in User.query.filter(UserUID__in=_staff_uids).only('UserUID', 'Avatar')
|
||||||
|
} if _staff_uids else {}
|
||||||
|
|
||||||
|
# 批量预取钱包
|
||||||
|
_wallet_map = {
|
||||||
|
w.member_id: w
|
||||||
|
for w in MerchantStaffWallet.objects.filter(member_id__in=_member_ids)
|
||||||
|
} if _member_ids else {}
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for m in members_list:
|
||||||
|
total_stats = _total_stats_map.get(m.id, {})
|
||||||
|
today_stat = _today_stat_map.get(m.id)
|
||||||
|
wallet = _wallet_map.get(m.id)
|
||||||
|
if wallet is None:
|
||||||
|
wallet = get_or_create_wallet(m)
|
||||||
|
u = _user_map.get(m.staff_user_id)
|
||||||
result.append({
|
result.append({
|
||||||
'id': m.id,
|
'id': m.id,
|
||||||
'uid': m.staff_user_id,
|
'uid': m.staff_user_id,
|
||||||
@@ -210,13 +247,13 @@ class StaffMemberListView(APIView):
|
|||||||
'isDisabled': m.status == MEMBER_STATUS_DISABLED,
|
'isDisabled': m.status == MEMBER_STATUS_DISABLED,
|
||||||
'todayDispatchCount': today_stat.dispatch_count if today_stat else int(m.today_dispatch_count or 0),
|
'todayDispatchCount': today_stat.dispatch_count if today_stat else int(m.today_dispatch_count or 0),
|
||||||
'todayDispatchAmount': str(today_stat.dispatch_amount if today_stat else (m.today_dispatch_amount or '0.00')),
|
'todayDispatchAmount': str(today_stat.dispatch_amount if today_stat else (m.today_dispatch_amount or '0.00')),
|
||||||
'dispatchCount': total_stats['dispatch_count'] or 0,
|
'dispatchCount': total_stats.get('dispatch_count') or 0,
|
||||||
'dispatchAmount': str(total_stats['dispatch_amount'] or '0.00'),
|
'dispatchAmount': str(total_stats.get('dispatch_amount') or '0.00'),
|
||||||
'orderCount': total_stats['dispatch_count'] or 0,
|
'orderCount': total_stats.get('dispatch_count') or 0,
|
||||||
'settleCount': total_stats['settle_count'] or 0,
|
'settleCount': total_stats.get('settle_count') or 0,
|
||||||
'settleAmount': str(total_stats['settle_amount'] or '0.00'),
|
'settleAmount': str(total_stats.get('settle_amount') or '0.00'),
|
||||||
'refundCount': total_stats['refund_count'] or 0,
|
'refundCount': total_stats.get('refund_count') or 0,
|
||||||
'refundAmount': str(total_stats['refund_amount'] or '0.00'),
|
'refundAmount': str(total_stats.get('refund_amount') or '0.00'),
|
||||||
'quota_available': str(wallet.quota_available),
|
'quota_available': str(wallet.quota_available),
|
||||||
'quota_total': str(wallet.quota_total),
|
'quota_total': str(wallet.quota_total),
|
||||||
'quota_used': str(wallet.quota_used),
|
'quota_used': str(wallet.quota_used),
|
||||||
@@ -618,9 +655,15 @@ class StaffRankView(APIView):
|
|||||||
sort_field = sort_by if sort_by in ('dispatch_count', 'dispatch_amount', 'settle_count', 'settle_amount') else 'dispatch_count'
|
sort_field = sort_by if sort_by in ('dispatch_count', 'dispatch_amount', 'settle_count', 'settle_amount') else 'dispatch_count'
|
||||||
agg = sorted(agg, key=lambda x: x[sort_field] or 0, reverse=True)[:50]
|
agg = sorted(agg, key=lambda x: x[sort_field] or 0, reverse=True)[:50]
|
||||||
|
|
||||||
|
# 批量预取 member 信息,避免循环内 N+1
|
||||||
|
_member_ids = [row['member_id'] for row in agg]
|
||||||
|
_members_map = {
|
||||||
|
m.id: m
|
||||||
|
for m in MerchantStaffMember.query.filter(id__in=_member_ids).only('id', 'display_name')
|
||||||
|
} if _member_ids else {}
|
||||||
result = []
|
result = []
|
||||||
for idx, row in enumerate(agg, 1):
|
for idx, row in enumerate(agg, 1):
|
||||||
m = MerchantStaffMember.query.filter(id=row['member_id']).first()
|
m = _members_map.get(row['member_id'])
|
||||||
result.append({
|
result.append({
|
||||||
'rank': idx,
|
'rank': idx,
|
||||||
'member_id': row['member_id'],
|
'member_id': row['member_id'],
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ class AdGengHuanDaShou(APIView):
|
|||||||
|
|
||||||
# 8. 验证新打手ID是否存在,并且是打手身份
|
# 8. 验证新打手ID是否存在,并且是打手身份
|
||||||
try:
|
try:
|
||||||
new_dashou_user = User.query.get(
|
new_dashou_user = User.query.select_related('DashouProfile').get(
|
||||||
UserUID=new_dashou_id,
|
UserUID=new_dashou_id,
|
||||||
#user_type='PlayerID'
|
#user_type='PlayerID'
|
||||||
)
|
)
|
||||||
@@ -459,7 +459,7 @@ class AdGengHuanDaShou(APIView):
|
|||||||
if old_dashou_id:
|
if old_dashou_id:
|
||||||
try:
|
try:
|
||||||
# 查询原打手主表
|
# 查询原打手主表
|
||||||
old_dashou_user = User.query.get(
|
old_dashou_user = User.query.select_related('DashouProfile').get(
|
||||||
UserUID=old_dashou_id,
|
UserUID=old_dashou_id,
|
||||||
#user_type='PlayerID'
|
#user_type='PlayerID'
|
||||||
)
|
)
|
||||||
@@ -567,7 +567,7 @@ class AdQiangZhiJieDan(APIView):
|
|||||||
|
|
||||||
# 6. 查询订单主表
|
# 6. 查询订单主表
|
||||||
try:
|
try:
|
||||||
dingdan_obj = Order.query.get(OrderID=dingdan_id)
|
dingdan_obj = Order.query.select_related('shangjia_kuozhan', 'pingtai_kuozhan').get(OrderID=dingdan_id)
|
||||||
except Order.DoesNotExist:
|
except Order.DoesNotExist:
|
||||||
return Response(
|
return Response(
|
||||||
{'code': 404, 'message': '订单不存在', 'data': None},
|
{'code': 404, 'message': '订单不存在', 'data': None},
|
||||||
@@ -603,7 +603,7 @@ class AdQiangZhiJieDan(APIView):
|
|||||||
if jiedan_dashou_id:
|
if jiedan_dashou_id:
|
||||||
try:
|
try:
|
||||||
# 查询打手用户主表
|
# 查询打手用户主表
|
||||||
dashou_user = User.query.get(
|
dashou_user = User.query.select_related('DashouProfile').get(
|
||||||
UserUID=jiedan_dashou_id,
|
UserUID=jiedan_dashou_id,
|
||||||
#user_type='PlayerID'
|
#user_type='PlayerID'
|
||||||
)
|
)
|
||||||
@@ -646,7 +646,7 @@ class AdQiangZhiJieDan(APIView):
|
|||||||
|
|
||||||
if shangjia_id:
|
if shangjia_id:
|
||||||
# 查询商家用户主表
|
# 查询商家用户主表
|
||||||
shangjia_user = User.query.get(
|
shangjia_user = User.query.select_related('ShopProfile').get(
|
||||||
UserUID=shangjia_id,
|
UserUID=shangjia_id,
|
||||||
#user_type='shop'
|
#user_type='shop'
|
||||||
)
|
)
|
||||||
@@ -750,7 +750,7 @@ class AdJuJueTuiKuan(APIView):
|
|||||||
|
|
||||||
# 6. 查询订单主表
|
# 6. 查询订单主表
|
||||||
try:
|
try:
|
||||||
dingdan_obj = Order.query.get(OrderID=dingdan_id)
|
dingdan_obj = Order.query.select_related('shangjia_kuozhan', 'pingtai_kuozhan').get(OrderID=dingdan_id)
|
||||||
except Order.DoesNotExist:
|
except Order.DoesNotExist:
|
||||||
return Response(
|
return Response(
|
||||||
{'code': 404, 'message': '订单不存在', 'data': None},
|
{'code': 404, 'message': '订单不存在', 'data': None},
|
||||||
@@ -779,7 +779,7 @@ class AdJuJueTuiKuan(APIView):
|
|||||||
if jiedan_dashou_id:
|
if jiedan_dashou_id:
|
||||||
try:
|
try:
|
||||||
# 查询打手用户主表
|
# 查询打手用户主表
|
||||||
dashou_user = User.query.get(
|
dashou_user = User.query.select_related('DashouProfile').get(
|
||||||
UserUID=jiedan_dashou_id,
|
UserUID=jiedan_dashou_id,
|
||||||
#user_type='PlayerID'
|
#user_type='PlayerID'
|
||||||
)
|
)
|
||||||
@@ -822,7 +822,7 @@ class AdJuJueTuiKuan(APIView):
|
|||||||
|
|
||||||
if shangjia_id:
|
if shangjia_id:
|
||||||
# 查询商家用户主表
|
# 查询商家用户主表
|
||||||
shangjia_user = User.query.get(
|
shangjia_user = User.query.select_related('ShopProfile').get(
|
||||||
UserUID=shangjia_id,
|
UserUID=shangjia_id,
|
||||||
#user_type='shop'
|
#user_type='shop'
|
||||||
)
|
)
|
||||||
@@ -958,7 +958,7 @@ class AdTongYiTuiKuanShangJia(APIView):
|
|||||||
|
|
||||||
# 7. 查询订单主表
|
# 7. 查询订单主表
|
||||||
try:
|
try:
|
||||||
dingdan_obj = Order.query.get(OrderID=dingdan_id)
|
dingdan_obj = Order.query.select_related('shangjia_kuozhan', 'pingtai_kuozhan').get(OrderID=dingdan_id)
|
||||||
except Order.DoesNotExist:
|
except Order.DoesNotExist:
|
||||||
return Response(
|
return Response(
|
||||||
{'code': 404, 'message': '订单不存在', 'data': None},
|
{'code': 404, 'message': '订单不存在', 'data': None},
|
||||||
@@ -1010,7 +1010,7 @@ class AdTongYiTuiKuanShangJia(APIView):
|
|||||||
if jiedan_dashou_id:
|
if jiedan_dashou_id:
|
||||||
try:
|
try:
|
||||||
# 查询打手用户主表
|
# 查询打手用户主表
|
||||||
dashou_user = User.query.get(
|
dashou_user = User.query.select_related('DashouProfile').get(
|
||||||
UserUID=jiedan_dashou_id,
|
UserUID=jiedan_dashou_id,
|
||||||
#user_type='PlayerID'
|
#user_type='PlayerID'
|
||||||
)
|
)
|
||||||
@@ -1035,10 +1035,10 @@ class AdTongYiTuiKuanShangJia(APIView):
|
|||||||
if shangjia_id:
|
if shangjia_id:
|
||||||
try:
|
try:
|
||||||
# 查询商家用户主表
|
# 查询商家用户主表
|
||||||
shangjia_user = User.query.get(
|
shangjia_user = User.query.select_related('ShopProfile').get(
|
||||||
UserUID=shangjia_id,
|
UserUID=shangjia_id,
|
||||||
#user_type='shop'
|
#user_type='shop'
|
||||||
)
|
)
|
||||||
|
|
||||||
# 获取商家扩展表
|
# 获取商家扩展表
|
||||||
shangjia_profile = shangjia_user.ShopProfile
|
shangjia_profile = shangjia_user.ShopProfile
|
||||||
@@ -1228,7 +1228,7 @@ class AdTongYiTuiKuanPingTai(APIView):
|
|||||||
# 14. 更新打手扩展表
|
# 14. 更新打手扩展表
|
||||||
if jiedan_dashou_id:
|
if jiedan_dashou_id:
|
||||||
try:
|
try:
|
||||||
dashou_user = User.query.get(UserUID=jiedan_dashou_id)
|
dashou_user = User.query.select_related('DashouProfile').get(UserUID=jiedan_dashou_id)
|
||||||
dashou_profile = dashou_user.DashouProfile
|
dashou_profile = dashou_user.DashouProfile
|
||||||
dashou_profile.zhuangtai = 1 # 打手状态设为空闲
|
dashou_profile.zhuangtai = 1 # 打手状态设为空闲
|
||||||
dashou_profile.tuikuanliang += 1
|
dashou_profile.tuikuanliang += 1
|
||||||
@@ -1239,7 +1239,7 @@ class AdTongYiTuiKuanPingTai(APIView):
|
|||||||
# 15. 更新老板扩展表
|
# 15. 更新老板扩展表
|
||||||
if laoban_id:
|
if laoban_id:
|
||||||
try:
|
try:
|
||||||
laoban_user = User.query.get(UserUID=laoban_id)
|
laoban_user = User.query.select_related('BossProfile').get(UserUID=laoban_id)
|
||||||
laoban_profile = laoban_user.BossProfile
|
laoban_profile = laoban_user.BossProfile
|
||||||
laoban_profile.alltui += 1
|
laoban_profile.alltui += 1
|
||||||
laoban_profile.zonge -= jine
|
laoban_profile.zonge -= jine
|
||||||
@@ -1463,7 +1463,7 @@ class AdJuJueJieSuan(APIView):
|
|||||||
|
|
||||||
# 8. 更新打手扩展表
|
# 8. 更新打手扩展表
|
||||||
try:
|
try:
|
||||||
dashou_user = User.query.get(UserUID=jiedan_dashou_id)
|
dashou_user = User.query.select_related('DashouProfile').get(UserUID=jiedan_dashou_id)
|
||||||
dashou_profile = dashou_user.DashouProfile
|
dashou_profile = dashou_user.DashouProfile
|
||||||
dashou_profile.zhuangtai = 1 # 打手状态设为正常
|
dashou_profile.zhuangtai = 1 # 打手状态设为正常
|
||||||
dashou_profile.tuikuanliang += 1 # 退款订单总量+1
|
dashou_profile.tuikuanliang += 1 # 退款订单总量+1
|
||||||
@@ -1545,7 +1545,7 @@ class AdZhuanYiDaTing(APIView):
|
|||||||
# 如果有接单打手,更新其扩展表
|
# 如果有接单打手,更新其扩展表
|
||||||
if jiedan_dashou_id:
|
if jiedan_dashou_id:
|
||||||
try:
|
try:
|
||||||
dashou_user = User.query.get(UserUID=jiedan_dashou_id)
|
dashou_user = User.query.select_related('DashouProfile').get(UserUID=jiedan_dashou_id)
|
||||||
dashou_profile = dashou_user.DashouProfile
|
dashou_profile = dashou_user.DashouProfile
|
||||||
dashou_profile.zhuangtai = 1
|
dashou_profile.zhuangtai = 1
|
||||||
dashou_profile.tuikuanliang += 1
|
dashou_profile.tuikuanliang += 1
|
||||||
@@ -1835,7 +1835,7 @@ class ZxsjghdsView(APIView):
|
|||||||
# 6.2 释放打手
|
# 6.2 释放打手
|
||||||
if dashou_id:
|
if dashou_id:
|
||||||
try:
|
try:
|
||||||
dashou_user = User.query.get(UserUID=dashou_id)
|
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
|
||||||
dashou_profile = dashou_user.DashouProfile
|
dashou_profile = dashou_user.DashouProfile
|
||||||
dashou_profile.tuikuanliang = (dashou_profile.tuikuanliang or 0) + 1
|
dashou_profile.tuikuanliang = (dashou_profile.tuikuanliang or 0) + 1
|
||||||
dashou_profile.zhuangtai = 1 # 空闲
|
dashou_profile.zhuangtai = 1 # 空闲
|
||||||
|
|||||||
@@ -422,11 +422,14 @@ class JiedanView(APIView):
|
|||||||
# 7. 更新打手扩展表(如果有打手ID和分成)
|
# 7. 更新打手扩展表(如果有打手ID和分成)
|
||||||
if jiedan_dashou_id and dashou_fencheng > 0:
|
if jiedan_dashou_id and dashou_fencheng > 0:
|
||||||
# 查询打手用户
|
# 查询打手用户
|
||||||
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first()
|
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).select_related('DashouProfile').first()
|
||||||
|
|
||||||
if dashou_user:
|
if dashou_user:
|
||||||
# 查询打手扩展表
|
# 查询打手扩展表
|
||||||
dashou_kuozhan = UserDashou.query.filter(user=dashou_user).first()
|
try:
|
||||||
|
dashou_kuozhan = dashou_user.DashouProfile
|
||||||
|
except UserDashou.DoesNotExist:
|
||||||
|
dashou_kuozhan = None
|
||||||
|
|
||||||
if dashou_kuozhan:
|
if dashou_kuozhan:
|
||||||
# 更新四个金额字段
|
# 更新四个金额字段
|
||||||
@@ -585,7 +588,7 @@ class DingdanXiangqingView2(APIView):
|
|||||||
jiedan_time = ''
|
jiedan_time = ''
|
||||||
tijiao_time = ''
|
tijiao_time = ''
|
||||||
if jiedan_dashou_id:
|
if jiedan_dashou_id:
|
||||||
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first()
|
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).select_related('DashouProfile').first()
|
||||||
if dashou_user:
|
if dashou_user:
|
||||||
dashou_touxiang = dashou_user.Avatar or ''
|
dashou_touxiang = dashou_user.Avatar or ''
|
||||||
try:
|
try:
|
||||||
@@ -734,7 +737,7 @@ class JiedanView2(APIView):
|
|||||||
|
|
||||||
# 9. 更新打手收益与状态
|
# 9. 更新打手收益与状态
|
||||||
if jiedan_dashou_id and dashou_fencheng > 0:
|
if jiedan_dashou_id and dashou_fencheng > 0:
|
||||||
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first()
|
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).select_related('DashouProfile').first()
|
||||||
if dashou_user:
|
if dashou_user:
|
||||||
try:
|
try:
|
||||||
dashou_ext = dashou_user.DashouProfile
|
dashou_ext = dashou_user.DashouProfile
|
||||||
|
|||||||
@@ -193,13 +193,13 @@ def send_group_message(
|
|||||||
def check_user_permission(identity_type, uid, order):
|
def check_user_permission(identity_type, uid, order):
|
||||||
"""验证用户身份、账号状态、以及是否属于该订单"""
|
"""验证用户身份、账号状态、以及是否属于该订单"""
|
||||||
try:
|
try:
|
||||||
user = User.query.get(UserUID=uid)
|
user = User.query.select_related('DashouProfile').get(UserUID=uid)
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return False, '用户不存在'
|
return False, '用户不存在'
|
||||||
|
|
||||||
if identity_type == 'PlayerID':
|
if identity_type == 'PlayerID':
|
||||||
try:
|
try:
|
||||||
dashou = UserDashou.query.get(user=user)
|
dashou = user.DashouProfile
|
||||||
if dashou.zhanghaozhuangtai != 1 :
|
if dashou.zhanghaozhuangtai != 1 :
|
||||||
return False, '打手账号已被封禁或状态异常'
|
return False, '打手账号已被封禁或状态异常'
|
||||||
except UserDashou.DoesNotExist:
|
except UserDashou.DoesNotExist:
|
||||||
@@ -516,14 +516,16 @@ class LianTongDingDanZhuangTaiPiLiangView(APIView):
|
|||||||
return Response({'code': 400, 'msg': 'order_ids 格式错误'})
|
return Response({'code': 400, 'msg': 'order_ids 格式错误'})
|
||||||
|
|
||||||
uid = request.user.UserUID
|
uid = request.user.UserUID
|
||||||
|
# 批量查询订单,避免循环内 N+1
|
||||||
|
oids = [str(oid).strip() for oid in order_ids[:20] if str(oid).strip()]
|
||||||
|
orders_map = {
|
||||||
|
o.OrderID: o
|
||||||
|
for o in Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').filter(OrderID__in=oids)
|
||||||
|
}
|
||||||
result = {}
|
result = {}
|
||||||
for oid in order_ids[:20]:
|
for oid in oids:
|
||||||
oid = str(oid).strip()
|
order = orders_map.get(oid)
|
||||||
if not oid:
|
if not order:
|
||||||
continue
|
|
||||||
try:
|
|
||||||
order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=oid)
|
|
||||||
except Order.DoesNotExist:
|
|
||||||
continue
|
continue
|
||||||
for identity in ('PlayerID', 'shangjia', 'boss'):
|
for identity in ('PlayerID', 'shangjia', 'boss'):
|
||||||
valid, _ = check_user_permission(identity, uid, order)
|
valid, _ = check_user_permission(identity, uid, order)
|
||||||
|
|||||||
@@ -521,11 +521,13 @@ class ShangjiaShujuTongjiView(APIView):
|
|||||||
penalty_stats = None
|
penalty_stats = None
|
||||||
try:
|
try:
|
||||||
from orders.models import Penalty, PenaltyRecord
|
from orders.models import Penalty, PenaltyRecord
|
||||||
from django.db.models import Q, Sum
|
from django.db.models import Q, Sum, Count
|
||||||
fakuan_pending = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).count()
|
_fakuan_agg = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).aggregate(
|
||||||
fakuan_pending_amt = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).aggregate(
|
count=Count('id'),
|
||||||
amt=Sum('FineAmount')
|
amt=Sum('FineAmount'),
|
||||||
)['amt']
|
)
|
||||||
|
fakuan_pending = _fakuan_agg['count']
|
||||||
|
fakuan_pending_amt = _fakuan_agg['amt']
|
||||||
jifen_pending = PenaltyRecord.query.filter(
|
jifen_pending = PenaltyRecord.query.filter(
|
||||||
ApplicantID=yonghuid,
|
ApplicantID=yonghuid,
|
||||||
).filter(Q(ApplyStatus=0) | Q(ApplyStatus=3)).count()
|
).filter(Q(ApplyStatus=0) | Q(ApplyStatus=3)).count()
|
||||||
@@ -1204,7 +1206,7 @@ class ShangjiaTuikuanShenqingView(APIView):
|
|||||||
try:
|
try:
|
||||||
dashou_main = User.query.filter(
|
dashou_main = User.query.filter(
|
||||||
UserUID=dingdan.PlayerID
|
UserUID=dingdan.PlayerID
|
||||||
).first()
|
).select_related('DashouProfile').first()
|
||||||
if dashou_main and hasattr(dashou_main, 'DashouProfile'):
|
if dashou_main and hasattr(dashou_main, 'DashouProfile'):
|
||||||
dashou_profile = dashou_main.DashouProfile
|
dashou_profile = dashou_main.DashouProfile
|
||||||
dashou_profile.zhuangtai = 1
|
dashou_profile.zhuangtai = 1
|
||||||
@@ -1285,7 +1287,7 @@ class ShangjiaChufaShenqingView(APIView):
|
|||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# 验证打手身份
|
# 验证打手身份
|
||||||
try:
|
try:
|
||||||
dashou_user = User.query.get(UserUID=dashou_id)
|
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
|
||||||
dashou = dashou_user.DashouProfile
|
dashou = dashou_user.DashouProfile
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
return Response({
|
return Response({
|
||||||
@@ -1367,13 +1369,16 @@ class ShangjiaChufaShenqingView(APIView):
|
|||||||
|
|
||||||
# 🔴【新增】保存处罚图片到数据库
|
# 🔴【新增】保存处罚图片到数据库
|
||||||
if chufa_tupian_urls and isinstance(chufa_tupian_urls, list):
|
if chufa_tupian_urls and isinstance(chufa_tupian_urls, list):
|
||||||
for tupian_url in chufa_tupian_urls:
|
_valid_urls = [u.strip() for u in chufa_tupian_urls if u and u.strip()]
|
||||||
if tupian_url and tupian_url.strip():
|
if _valid_urls:
|
||||||
PenaltyEvidenceImage.query.create(
|
PenaltyEvidenceImage.objects.bulk_create([
|
||||||
|
PenaltyEvidenceImage(
|
||||||
OrderID=dingdan_id,
|
OrderID=dingdan_id,
|
||||||
UserID=current_user.UserUID,
|
UserID=current_user.UserUID,
|
||||||
ImageURL=tupian_url
|
ImageURL=url,
|
||||||
)
|
)
|
||||||
|
for url in _valid_urls
|
||||||
|
])
|
||||||
|
|
||||||
# 更新商家订单扩展表的处罚相关字段
|
# 更新商家订单扩展表的处罚相关字段
|
||||||
shangjia_kuozhan.PenaltyReason = chufa_liyou
|
shangjia_kuozhan.PenaltyReason = chufa_liyou
|
||||||
|
|||||||
@@ -188,13 +188,15 @@ class ShangjiaFakuanApplyView(APIView):
|
|||||||
),
|
),
|
||||||
ShopStaffUserID=staff_kefu_id,
|
ShopStaffUserID=staff_kefu_id,
|
||||||
)
|
)
|
||||||
for url in evidence_urls:
|
PenaltyAppealImage.objects.bulk_create([
|
||||||
PenaltyAppealImage.query.create(
|
PenaltyAppealImage(
|
||||||
Penalty=fadan,
|
Penalty=fadan,
|
||||||
PenalizedUserID=dashou_id,
|
PenalizedUserID=dashou_id,
|
||||||
ImageURL=url,
|
ImageURL=url,
|
||||||
Purpose=1,
|
Purpose=1,
|
||||||
)
|
)
|
||||||
|
for url in evidence_urls
|
||||||
|
])
|
||||||
lock_penalty_bonus(fadan)
|
lock_penalty_bonus(fadan)
|
||||||
logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}提交罚款申请(平台审核),金额{fakuanjine}")
|
logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}提交罚款申请(平台审核),金额{fakuanjine}")
|
||||||
return Response({'code': 0, 'msg': '罚款申请已提交,等待平台审核', 'data': {'fadan_id': fadan.id}})
|
return Response({'code': 0, 'msg': '罚款申请已提交,等待平台审核', 'data': {'fadan_id': fadan.id}})
|
||||||
@@ -362,16 +364,17 @@ class ShangjiaFakuaiXiugaiView(APIView):
|
|||||||
existing_count = PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=1).count()
|
existing_count = PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=1).count()
|
||||||
if existing_count + len(add_urls) > 9:
|
if existing_count + len(add_urls) > 9:
|
||||||
return Response({'code': 400, 'msg': '证据图片最多9张'})
|
return Response({'code': 400, 'msg': '证据图片最多9张'})
|
||||||
for url in add_urls:
|
_valid_add_urls = [str(u).strip() for u in add_urls if str(u).strip()]
|
||||||
url = str(url).strip()
|
if _valid_add_urls:
|
||||||
if not url:
|
PenaltyAppealImage.objects.bulk_create([
|
||||||
continue
|
PenaltyAppealImage(
|
||||||
PenaltyAppealImage.query.create(
|
Penalty=fadan,
|
||||||
Penalty=fadan,
|
PenalizedUserID=fadan.PenalizedUserID,
|
||||||
PenalizedUserID=fadan.PenalizedUserID,
|
ImageURL=url,
|
||||||
ImageURL=url,
|
Purpose=1,
|
||||||
Purpose=1,
|
)
|
||||||
)
|
for url in _valid_add_urls
|
||||||
|
])
|
||||||
logger.info(
|
logger.info(
|
||||||
f"罚单{fadan.id}证据图变更:新增{len(add_urls)}张,删除{len(remove_ids)}张"
|
f"罚单{fadan.id}证据图变更:新增{len(add_urls)}张,删除{len(remove_ids)}张"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -204,7 +204,9 @@ class WechatPayNotifyView(APIView):
|
|||||||
|
|
||||||
# 锁定订单防止并发
|
# 锁定订单防止并发
|
||||||
try:
|
try:
|
||||||
dingdan = Order.query.select_for_update().filter(OrderID=out_trade_no).first()
|
dingdan = Order.query.select_for_update().select_related(
|
||||||
|
'pingtai_kuozhan', 'shangjia_kuozhan'
|
||||||
|
).filter(OrderID=out_trade_no).first()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"查询订单异常: {e}")
|
logger.error(f"查询订单异常: {e}")
|
||||||
raise
|
raise
|
||||||
@@ -263,7 +265,7 @@ class WechatPayNotifyView(APIView):
|
|||||||
if hasattr(dingdan, 'pingtai_kuozhan'):
|
if hasattr(dingdan, 'pingtai_kuozhan'):
|
||||||
laoban_id = dingdan.pingtai_kuozhan.BossID
|
laoban_id = dingdan.pingtai_kuozhan.BossID
|
||||||
if laoban_id:
|
if laoban_id:
|
||||||
user_main = User.query.filter(UserUID=laoban_id).first()
|
user_main = User.query.filter(UserUID=laoban_id).select_related('BossProfile').first()
|
||||||
if user_main and hasattr(user_main, 'BossProfile'):
|
if user_main and hasattr(user_main, 'BossProfile'):
|
||||||
boss = user_main.BossProfile
|
boss = user_main.BossProfile
|
||||||
boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0)
|
boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0)
|
||||||
@@ -537,7 +539,7 @@ class PaymentVerifyView(APIView):
|
|||||||
# 更新老板扩展表
|
# 更新老板扩展表
|
||||||
laoban_id = dingdan.pingtai_kuozhan.BossID if hasattr(dingdan, 'pingtai_kuozhan') else None
|
laoban_id = dingdan.pingtai_kuozhan.BossID if hasattr(dingdan, 'pingtai_kuozhan') else None
|
||||||
if laoban_id:
|
if laoban_id:
|
||||||
user_main = User.query.filter(UserUID=laoban_id).first()
|
user_main = User.query.filter(UserUID=laoban_id).select_related('BossProfile').first()
|
||||||
if user_main and hasattr(user_main, 'BossProfile'):
|
if user_main and hasattr(user_main, 'BossProfile'):
|
||||||
boss = user_main.BossProfile
|
boss = user_main.BossProfile
|
||||||
boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0)
|
boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0)
|
||||||
@@ -709,13 +711,16 @@ class CreateOrderView(APIView):
|
|||||||
zhiding_user = User.query.filter(
|
zhiding_user = User.query.filter(
|
||||||
UserUID=zhiding,
|
UserUID=zhiding,
|
||||||
#user_type='PlayerID'
|
#user_type='PlayerID'
|
||||||
).first()
|
).select_related('DashouProfile').first()
|
||||||
|
|
||||||
if not zhiding_user:
|
if not zhiding_user:
|
||||||
return Response({'code': 7, 'msg': '指定打手不存在', 'data': None})
|
return Response({'code': 7, 'msg': '指定打手不存在', 'data': None})
|
||||||
|
|
||||||
# 查询打手扩展表
|
# 查询打手扩展表
|
||||||
dashou_profile = UserDashou.query.filter(user=zhiding_user).first()
|
try:
|
||||||
|
dashou_profile = zhiding_user.DashouProfile
|
||||||
|
except UserDashou.DoesNotExist:
|
||||||
|
dashou_profile = None
|
||||||
|
|
||||||
if not dashou_profile:
|
if not dashou_profile:
|
||||||
return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None})
|
return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None})
|
||||||
|
|||||||
@@ -268,35 +268,98 @@ class DashouHuiyuanList(APIView):
|
|||||||
}, status=status.HTTP_400_BAD_REQUEST)
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# 3. 仅返回本俱乐部可售会员(club_huiyuan_price 已配置且启用)
|
# 3. 仅返回本俱乐部可售会员(club_huiyuan_price 已配置且启用)
|
||||||
from jituan.services.member_recharge import club_huiyuan_sellable, can_purchase_trial
|
from jituan.constants import CLUB_ID_DEFAULT
|
||||||
|
|
||||||
club_id = resolve_effective_club_id(request, request.user)
|
|
||||||
huiyuan_queryset = Huiyuan.query.all().only(
|
|
||||||
'huiyuan_id', 'jieshao', 'jtjieshao', 'jiage',
|
|
||||||
).order_by('jiage')
|
|
||||||
|
|
||||||
from jituan.models import ClubHuiyuanPrice
|
from jituan.models import ClubHuiyuanPrice
|
||||||
club_rows = {
|
from decimal import Decimal as _Decimal
|
||||||
r.huiyuan_id: r
|
|
||||||
for r in ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True)
|
club_id = resolve_effective_club_id(request, request.user) or CLUB_ID_DEFAULT
|
||||||
}
|
huiyuan_queryset = list(Huiyuan.query.all().only(
|
||||||
|
'huiyuan_id', 'jieshao', 'jtjieshao', 'jiage',
|
||||||
|
).order_by('jiage'))
|
||||||
|
|
||||||
|
yonghuid = request.user.UserUID
|
||||||
|
_huiyuan_ids = [h.huiyuan_id for h in huiyuan_queryset]
|
||||||
|
|
||||||
|
# 批量预取 1:本俱乐部所有 ClubHuiyuanPrice(启用),按 huiyuan_id 索引
|
||||||
|
_club_price_map = {
|
||||||
|
cp.huiyuan_id: cp
|
||||||
|
for cp in ClubHuiyuanPrice.query.filter(
|
||||||
|
club_id=club_id,
|
||||||
|
huiyuan_id__in=_huiyuan_ids,
|
||||||
|
is_enabled=True,
|
||||||
|
)
|
||||||
|
} if _huiyuan_ids else {}
|
||||||
|
|
||||||
|
# 批量预取 2:当前用户已购 Huiyuangoumai,按 huiyuan_id 索引
|
||||||
|
_goumai_map = {
|
||||||
|
gm.huiyuan_id: gm
|
||||||
|
for gm in Huiyuangoumai.query.filter(
|
||||||
|
yonghu_id=yonghuid,
|
||||||
|
huiyuan_id__in=_huiyuan_ids,
|
||||||
|
)
|
||||||
|
} if _huiyuan_ids else {}
|
||||||
|
|
||||||
|
# 批量预取 3:当前用户在本俱乐部已支付正式会员的 huiyuan_id 集合
|
||||||
|
_formal_paid_set = set(
|
||||||
|
Czjilu.query.filter(
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
huiyuan_id__in=_huiyuan_ids,
|
||||||
|
leixing=1,
|
||||||
|
zhuangtai=3,
|
||||||
|
club_id=club_id,
|
||||||
|
is_trial=False,
|
||||||
|
).values_list('huiyuan_id', flat=True)
|
||||||
|
) if _huiyuan_ids else set()
|
||||||
|
|
||||||
|
def _local_sellable(cp_row, is_trial):
|
||||||
|
"""单行 ClubHuiyuanPrice 计算 sellable/price/days(避免重复查询)。"""
|
||||||
|
if not cp_row:
|
||||||
|
return False, _Decimal('0'), 0
|
||||||
|
if is_trial:
|
||||||
|
if not cp_row.trial_enabled:
|
||||||
|
return False, _Decimal('0'), 0
|
||||||
|
price = _Decimal(str(cp_row.trial_price or 0))
|
||||||
|
days = int(cp_row.trial_days or 0)
|
||||||
|
if price <= 0 or days <= 0:
|
||||||
|
return False, _Decimal('0'), 0
|
||||||
|
return True, price, days
|
||||||
|
price = _Decimal(str(cp_row.jiage or 0))
|
||||||
|
days = int(cp_row.formal_days or 30)
|
||||||
|
if price <= 0:
|
||||||
|
return False, _Decimal('0'), 0
|
||||||
|
return True, price, days
|
||||||
|
|
||||||
|
def _local_can_trial(huiyuan_id):
|
||||||
|
"""基于已预取的数据判断体验会员是否可购。"""
|
||||||
|
cp_row = _club_price_map.get(huiyuan_id)
|
||||||
|
trial_sellable, _, _ = _local_sellable(cp_row, is_trial=True)
|
||||||
|
if not trial_sellable:
|
||||||
|
return False
|
||||||
|
goumai = _goumai_map.get(huiyuan_id)
|
||||||
|
if goumai and goumai.has_used_trial:
|
||||||
|
return False
|
||||||
|
if huiyuan_id in _formal_paid_set:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
huiyuan_list = []
|
huiyuan_list = []
|
||||||
yonghuid = request.user.UserUID
|
|
||||||
for huiyuan in huiyuan_queryset:
|
for huiyuan in huiyuan_queryset:
|
||||||
sellable, club_price, formal_days = club_huiyuan_sellable(
|
cp_row = _club_price_map.get(huiyuan.huiyuan_id)
|
||||||
club_id, huiyuan.huiyuan_id, is_trial=False,
|
sellable, club_price, formal_days = _local_sellable(cp_row, is_trial=False)
|
||||||
)
|
|
||||||
if not sellable or not club_price:
|
if not sellable or not club_price:
|
||||||
continue
|
continue
|
||||||
trial_ok, _ = can_purchase_trial(yonghuid, huiyuan.huiyuan_id, club_id)
|
trial_ok = _local_can_trial(huiyuan.huiyuan_id)
|
||||||
trial_sellable, trial_price, trial_days = club_huiyuan_sellable(
|
trial_sellable, trial_price, trial_days = _local_sellable(cp_row, is_trial=True)
|
||||||
club_id, huiyuan.huiyuan_id, is_trial=True,
|
# 远程新增:card_image / card_bg / is_bundle 字段
|
||||||
)
|
card_image = (cp_row.card_image or '').strip() if cp_row else ''
|
||||||
club_row = club_rows.get(huiyuan.huiyuan_id)
|
card_bg = (cp_row.card_bg or '').strip() if cp_row else ''
|
||||||
card_image = (club_row.card_image or '').strip() if club_row else ''
|
|
||||||
card_bg = (club_row.card_bg or '').strip() if club_row else ''
|
|
||||||
is_bundle = bool(getattr(huiyuan, 'is_bundle', False))
|
is_bundle = bool(getattr(huiyuan, 'is_bundle', False))
|
||||||
|
# 总会员(is_bundle)禁用体验会员
|
||||||
|
if is_bundle:
|
||||||
|
trial_sellable = False
|
||||||
|
trial_price = _Decimal('0')
|
||||||
|
trial_days = 0
|
||||||
|
trial_ok = False
|
||||||
huiyuan_list.append({
|
huiyuan_list.append({
|
||||||
'id': huiyuan.huiyuan_id,
|
'id': huiyuan.huiyuan_id,
|
||||||
'mingzi': huiyuan.jieshao,
|
'mingzi': huiyuan.jieshao,
|
||||||
@@ -304,10 +367,10 @@ class DashouHuiyuanList(APIView):
|
|||||||
'formal_days': formal_days,
|
'formal_days': formal_days,
|
||||||
'jieshao': huiyuan.jtjieshao,
|
'jieshao': huiyuan.jtjieshao,
|
||||||
'is_bundle': is_bundle,
|
'is_bundle': is_bundle,
|
||||||
'trial_enabled': False if is_bundle else bool(trial_sellable),
|
'trial_enabled': bool(trial_sellable),
|
||||||
'trial_price': '0.00' if is_bundle else (format_yuan(trial_price) if trial_sellable else '0.00'),
|
'trial_price': format_yuan(trial_price) if trial_sellable else '0.00',
|
||||||
'trial_days': 0 if is_bundle else (trial_days if trial_sellable else 0),
|
'trial_days': trial_days if trial_sellable else 0,
|
||||||
'can_buy_trial': False if is_bundle else bool(trial_ok and trial_sellable),
|
'can_buy_trial': bool(trial_ok and trial_sellable),
|
||||||
'card_image': card_image,
|
'card_image': card_image,
|
||||||
'card_bg': card_bg,
|
'card_bg': card_bg,
|
||||||
})
|
})
|
||||||
@@ -475,20 +538,21 @@ class AdGetAllDataView(APIView):
|
|||||||
|
|
||||||
# 6. 查询商品类型数据
|
# 6. 查询商品类型数据
|
||||||
try:
|
try:
|
||||||
# 使用select_related或prefetch_related优化查询(如果需要关联查询)
|
# 优化:使用 .values() 直接提取字段,避免模型实例化开销(字典表 <10 条)
|
||||||
# 这里直接查询所有字段,然后提取需要的字段
|
shangpinleixing_rows = ShangpinLeixing.query.all().values(
|
||||||
shangpinleixing_queryset = ShangpinLeixing.query.all()
|
'id', 'yaoqiuleixing', 'huiyuan_id', 'yongjin', 'jieshao', 'tupian_url'
|
||||||
|
)
|
||||||
|
|
||||||
# 转换为前端需要的格式
|
# 转换为前端需要的格式
|
||||||
shangpinleixing_list = []
|
shangpinleixing_list = []
|
||||||
for leixing in shangpinleixing_queryset:
|
for row in shangpinleixing_rows:
|
||||||
leixing_data = {
|
leixing_data = {
|
||||||
'id': leixing.id,
|
'id': row['id'],
|
||||||
'yaoqiuleixing': leixing.yaoqiuleixing if leixing.yaoqiuleixing is not None else None,
|
'yaoqiuleixing': row['yaoqiuleixing'],
|
||||||
'huiyuan_id': leixing.huiyuan_id if leixing.huiyuan_id else None,
|
'huiyuan_id': row['huiyuan_id'] or None,
|
||||||
'yongjin': str(leixing.yongjin) if leixing.yongjin is not None else None,
|
'yongjin': str(row['yongjin']) if row['yongjin'] is not None else None,
|
||||||
'jieshao': leixing.jieshao if leixing.jieshao else '',
|
'jieshao': row['jieshao'] or '',
|
||||||
'tupian_url': leixing.tupian_url if leixing.tupian_url else ''
|
'tupian_url': row['tupian_url'] or ''
|
||||||
}
|
}
|
||||||
shangpinleixing_list.append(leixing_data)
|
shangpinleixing_list.append(leixing_data)
|
||||||
|
|
||||||
@@ -500,16 +564,16 @@ class AdGetAllDataView(APIView):
|
|||||||
|
|
||||||
# 7. 查询商品专区数据
|
# 7. 查询商品专区数据
|
||||||
try:
|
try:
|
||||||
# 查询所有商品专区
|
# 优化:使用 .values() 直接提取字段(字典表 <50 条)
|
||||||
zhuanqu_queryset = ShangpinZhuanqu.query.all()
|
zhuanqu_rows = ShangpinZhuanqu.query.all().values('id', 'mingzi', 'leixing_id')
|
||||||
|
|
||||||
# 转换为前端需要的格式
|
# 转换为前端需要的格式
|
||||||
zhuanqu_list = []
|
zhuanqu_list = []
|
||||||
for zhuanqu in zhuanqu_queryset:
|
for row in zhuanqu_rows:
|
||||||
zhuanqu_data = {
|
zhuanqu_data = {
|
||||||
'id': zhuanqu.id,
|
'id': row['id'],
|
||||||
'mingzi': zhuanqu.mingzi if zhuanqu.mingzi else '',
|
'mingzi': row['mingzi'] or '',
|
||||||
'leixing_id': zhuanqu.leixing_id
|
'leixing_id': row['leixing_id']
|
||||||
}
|
}
|
||||||
zhuanqu_list.append(zhuanqu_data)
|
zhuanqu_list.append(zhuanqu_data)
|
||||||
|
|
||||||
@@ -593,17 +657,20 @@ class HuiyuanTypeListView(APIView):
|
|||||||
}, status=status.HTTP_401_UNAUTHORIZED)
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
# 🔥 2. 获取所有会员类型
|
# 🔥 2. 获取所有会员类型
|
||||||
huiyuan_list = Huiyuan.query.all().order_by('CreateTime')
|
# 优化:使用 .values() 直接提取字段,避免模型实例化开销
|
||||||
|
huiyuan_rows = Huiyuan.query.all().order_by('CreateTime').values(
|
||||||
|
'huiyuan_id', 'jieshao', 'jiage', 'goumai_cishu', 'CreateTime'
|
||||||
|
)
|
||||||
|
|
||||||
# 🔥 3. 构建返回数据
|
# 🔥 3. 构建返回数据
|
||||||
huiyuan_data = []
|
huiyuan_data = []
|
||||||
for huiyuan in huiyuan_list:
|
for row in huiyuan_rows:
|
||||||
huiyuan_data.append({
|
huiyuan_data.append({
|
||||||
'id': huiyuan.huiyuan_id, # 会员ID
|
'id': row['huiyuan_id'], # 会员ID
|
||||||
'jieshao': huiyuan.jieshao, # 会员介绍
|
'jieshao': row['jieshao'], # 会员介绍
|
||||||
'jiage': str(huiyuan.jiage), # 价格
|
'jiage': str(row['jiage']), # 价格
|
||||||
'goumai_cishu': huiyuan.goumai_cishu, # 购买次数
|
'goumai_cishu': row['goumai_cishu'], # 购买次数
|
||||||
'CreateTime': huiyuan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.CreateTime else None
|
'CreateTime': row['CreateTime'].strftime('%Y-%m-%d %H:%M:%S') if row['CreateTime'] else None
|
||||||
})
|
})
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
@@ -757,34 +824,40 @@ class HuiyuanRecordListView(APIView):
|
|||||||
has_more = (page * pagesize) < total_count
|
has_more = (page * pagesize) < total_count
|
||||||
|
|
||||||
# 🔥 9. 获取会员类型统计信息(所有会员类型的当前状态总数)
|
# 🔥 9. 获取会员类型统计信息(所有会员类型的当前状态总数)
|
||||||
huiyuan_types = Huiyuan.query.all()
|
# 优化:原实现为 N+1 查询(外层遍历 Huiyuan 全表 + 内层每条记录执行一次 COUNT SQL)
|
||||||
huiyuan_stats = []
|
# 改为单条 LEFT JOIN + GROUP BY 聚合,将 N+1 次查询降为 1 次
|
||||||
|
# 注意:Huiyuangoumai.huiyuan_id 是 CharField 而非 ForeignKey,无法用 ORM annotate
|
||||||
for huiyuan in huiyuan_types:
|
stats_sql = """
|
||||||
# 使用COUNT查询统计
|
SELECT
|
||||||
stats_sql = """
|
h.huiyuan_id,
|
||||||
SELECT COUNT(*)
|
h.jieshao,
|
||||||
FROM huiyuangoumai
|
COUNT(hg.id) AS count
|
||||||
WHERE huiyuan_id = %s
|
FROM huiyuan h
|
||||||
"""
|
LEFT JOIN huiyuangoumai hg
|
||||||
stats_params = [huiyuan.huiyuan_id]
|
ON hg.huiyuan_id = h.huiyuan_id
|
||||||
|
"""
|
||||||
if jiluzhuangtai == 1: # 未过期
|
stats_params = []
|
||||||
stats_sql += " AND daoqi_time > %s"
|
if jiluzhuangtai == 1: # 未过期
|
||||||
stats_params.append(now)
|
# 条件放在 ON 子句而非 WHERE,确保无购买记录的会员类型仍返回 count=0
|
||||||
else: # 已过期
|
stats_sql += " AND hg.daoqi_time > %s"
|
||||||
stats_sql += " AND daoqi_time <= %s"
|
stats_params.append(now)
|
||||||
stats_params.append(now)
|
else: # 已过期
|
||||||
|
stats_sql += " AND hg.daoqi_time <= %s"
|
||||||
with connection.cursor() as cursor:
|
stats_params.append(now)
|
||||||
cursor.execute(stats_sql, stats_params)
|
stats_sql += " GROUP BY h.huiyuan_id, h.jieshao"
|
||||||
count = cursor.fetchone()[0]
|
|
||||||
|
with connection.cursor() as cursor:
|
||||||
huiyuan_stats.append({
|
cursor.execute(stats_sql, stats_params)
|
||||||
'id': huiyuan.huiyuan_id,
|
stats_rows = cursor.fetchall()
|
||||||
'jieshao': huiyuan.jieshao,
|
|
||||||
'count': count
|
huiyuan_stats = [
|
||||||
})
|
{
|
||||||
|
'id': row[0],
|
||||||
|
'jieshao': row[1],
|
||||||
|
'count': row[2]
|
||||||
|
}
|
||||||
|
for row in stats_rows
|
||||||
|
]
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'code': 0,
|
'code': 0,
|
||||||
|
|||||||
@@ -35,8 +35,9 @@ def _has_perm(permissions):
|
|||||||
return any(p in permissions for p in REWARD_PERMS)
|
return any(p in permissions for p in REWARD_PERMS)
|
||||||
|
|
||||||
|
|
||||||
def _serialize_scheme(scheme):
|
def _serialize_scheme(scheme, tiers=None):
|
||||||
tiers = RankRewardTier.query.filter(scheme_id=scheme.id).order_by('rank_from')
|
if tiers is None:
|
||||||
|
tiers = RankRewardTier.query.filter(scheme_id=scheme.id).order_by('rank_from')
|
||||||
return {
|
return {
|
||||||
'id': scheme.id,
|
'id': scheme.id,
|
||||||
'club_id': scheme.club_id,
|
'club_id': scheme.club_id,
|
||||||
@@ -78,11 +79,18 @@ class RankRewardHoutaiListView(APIView):
|
|||||||
|
|
||||||
club_id = resolve_club_id_from_request(request)
|
club_id = resolve_club_id_from_request(request)
|
||||||
schemes = RankRewardScheme.query.filter(club_id=club_id).order_by('shenfen', 'period_type')
|
schemes = RankRewardScheme.query.filter(club_id=club_id).order_by('shenfen', 'period_type')
|
||||||
|
# 批量预取所有 tiers,避免列表推导内 N+1
|
||||||
|
schemes_list = list(schemes)
|
||||||
|
_scheme_ids = [s.id for s in schemes_list]
|
||||||
|
_tiers_map = {}
|
||||||
|
if _scheme_ids:
|
||||||
|
for t in RankRewardTier.query.filter(scheme_id__in=_scheme_ids).order_by('scheme_id', 'rank_from'):
|
||||||
|
_tiers_map.setdefault(t.scheme_id, []).append(t)
|
||||||
return Response({
|
return Response({
|
||||||
'code': 0,
|
'code': 0,
|
||||||
'data': {
|
'data': {
|
||||||
'club_id': club_id,
|
'club_id': club_id,
|
||||||
'schemes': [_serialize_scheme(s) for s in schemes],
|
'schemes': [_serialize_scheme(s, _tiers_map.get(s.id, [])) for s in schemes_list],
|
||||||
'shenfen_options': list(SORT_FIELD_REGISTRY.keys()),
|
'shenfen_options': list(SORT_FIELD_REGISTRY.keys()),
|
||||||
'period_options': [
|
'period_options': [
|
||||||
{'key': RankRewardScheme.PERIOD_DAY, 'label': '日榜(昨日)'},
|
{'key': RankRewardScheme.PERIOD_DAY, 'label': '日榜(昨日)'},
|
||||||
@@ -148,14 +156,16 @@ class RankRewardHoutaiSaveView(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
RankRewardTier.query.filter(scheme_id=scheme.id).delete()
|
RankRewardTier.query.filter(scheme_id=scheme.id).delete()
|
||||||
for t in tiers:
|
RankRewardTier.objects.bulk_create([
|
||||||
RankRewardTier.query.create(
|
RankRewardTier(
|
||||||
scheme_id=scheme.id,
|
scheme_id=scheme.id,
|
||||||
rank_from=int(t['rank_from']),
|
rank_from=int(t['rank_from']),
|
||||||
rank_to=int(t['rank_to']),
|
rank_to=int(t['rank_to']),
|
||||||
reward_amount=t['reward_amount'],
|
reward_amount=t['reward_amount'],
|
||||||
label=(t.get('label') or '').strip(),
|
label=(t.get('label') or '').strip(),
|
||||||
)
|
)
|
||||||
|
for t in tiers
|
||||||
|
])
|
||||||
|
|
||||||
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_scheme(scheme)})
|
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_scheme(scheme)})
|
||||||
|
|
||||||
|
|||||||
@@ -574,11 +574,36 @@ def maybe_settle_club_rewards(club_id: str, shenfen: str, riqi: str) -> Optional
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
min_metric = scheme.min_metric or Decimal('0')
|
min_metric = scheme.min_metric or Decimal('0')
|
||||||
club_map = load_user_club_map([r['yonghuid'] for r in rows])
|
_all_uids = [r['yonghuid'] for r in rows]
|
||||||
|
club_map = load_user_club_map(_all_uids)
|
||||||
expire_at = timezone.now() + timedelta(days=scheme.claim_days or 30)
|
expire_at = timezone.now() + timedelta(days=scheme.claim_days or 30)
|
||||||
balance_field = BALANCE_FIELD_MAP[shenfen]
|
balance_field = BALANCE_FIELD_MAP[shenfen]
|
||||||
claims_to_create = []
|
claims_to_create = []
|
||||||
|
|
||||||
|
# 批量预取活跃用户集合,避免循环内 exists 查询
|
||||||
|
if shenfen == 'dashou':
|
||||||
|
_active_uids = set(
|
||||||
|
UserDashou.objects.filter(user__UserUID__in=_all_uids, zhanghaozhuangtai=1)
|
||||||
|
.values_list('user__UserUID', flat=True)
|
||||||
|
)
|
||||||
|
elif shenfen == 'guanshi':
|
||||||
|
_active_uids = set(
|
||||||
|
UserGuanshi.objects.filter(user__UserUID__in=_all_uids, zhuangtai=1)
|
||||||
|
.values_list('user__UserUID', flat=True)
|
||||||
|
)
|
||||||
|
elif shenfen == 'zuzhang':
|
||||||
|
_active_uids = set(
|
||||||
|
UserZuzhang.objects.filter(user__UserUID__in=_all_uids, zhuangtai=1)
|
||||||
|
.values_list('user__UserUID', flat=True)
|
||||||
|
)
|
||||||
|
elif shenfen == 'shangjia':
|
||||||
|
_active_uids = set(
|
||||||
|
UserShangjia.objects.filter(user__UserUID__in=_all_uids, zhuangtai=1)
|
||||||
|
.values_list('user__UserUID', flat=True)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_active_uids = set()
|
||||||
|
|
||||||
for idx, row in enumerate(rows):
|
for idx, row in enumerate(rows):
|
||||||
mingci = idx + 1
|
mingci = idx + 1
|
||||||
metric = Decimal(str(row['metric'] or 0))
|
metric = Decimal(str(row['metric'] or 0))
|
||||||
@@ -590,7 +615,7 @@ def maybe_settle_club_rewards(club_id: str, shenfen: str, riqi: str) -> Optional
|
|||||||
uid = row['yonghuid']
|
uid = row['yonghuid']
|
||||||
if club_map.get(uid) != club_id:
|
if club_map.get(uid) != club_id:
|
||||||
continue
|
continue
|
||||||
if not user_has_active_shenfen(uid, shenfen):
|
if uid not in _active_uids:
|
||||||
continue
|
continue
|
||||||
idem = f'{club_id}|{scheme.id}|{period_key}|{uid}'
|
idem = f'{club_id}|{scheme.id}|{period_key}|{uid}'
|
||||||
claims_to_create.append(RankRewardClaim(
|
claims_to_create.append(RankRewardClaim(
|
||||||
@@ -618,14 +643,14 @@ def maybe_settle_club_rewards(club_id: str, shenfen: str, riqi: str) -> Optional
|
|||||||
sort_field=scheme.sort_field,
|
sort_field=scheme.sort_field,
|
||||||
claim_count=0,
|
claim_count=0,
|
||||||
)
|
)
|
||||||
created = 0
|
# 批量创建 claims,用 ignore_conflicts 处理幂等冲突
|
||||||
for claim in claims_to_create:
|
if claims_to_create:
|
||||||
claim.settlement_id = settlement.id
|
for claim in claims_to_create:
|
||||||
try:
|
claim.settlement_id = settlement.id
|
||||||
claim.save()
|
RankRewardClaim.objects.bulk_create(claims_to_create, ignore_conflicts=True)
|
||||||
created += 1
|
created = RankRewardClaim.objects.filter(settlement_id=settlement.id).count()
|
||||||
except IntegrityError:
|
else:
|
||||||
pass
|
created = 0
|
||||||
settlement.claim_count = created
|
settlement.claim_count = created
|
||||||
settlement.save(update_fields=['claim_count'])
|
settlement.save(update_fields=['claim_count'])
|
||||||
return settlement
|
return settlement
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import F
|
from django.db.models import F, Prefetch
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
@@ -118,15 +118,26 @@ class KaoheJinpaiTagsView(APIView):
|
|||||||
.values_list('chenghao__mingcheng', flat=True)
|
.values_list('chenghao__mingcheng', flat=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 板块列表
|
# 板块列表(prefetch 一次性加载标签和费用,避免三层嵌套 N+1)
|
||||||
plate_list = []
|
plate_list = []
|
||||||
for bk in Bankuai.objects.all():
|
all_bankuai = Bankuai.objects.prefetch_related(
|
||||||
tags = Chenghao.objects.filter(bankuai=bk, leixing='dashou')
|
Prefetch(
|
||||||
|
'chenghao_set',
|
||||||
|
queryset=Chenghao.objects.filter(leixing='dashou').prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
'kaohecishufeiyong_set',
|
||||||
|
queryset=KaoheCishuFeiyong.objects.order_by('cishu'),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
to_attr='dashou_tags'
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
for bk in all_bankuai:
|
||||||
tag_list = []
|
tag_list = []
|
||||||
for tag in tags:
|
for tag in getattr(bk, 'dashou_tags', []):
|
||||||
# 费用列表
|
# 费用列表(已预加载,保持 cishu 升序)
|
||||||
fee_objs = KaoheCishuFeiyong.objects.filter(chenghao=tag).order_by('cishu')
|
fee_list = [{'cishu': f.cishu, 'feiyong': float(f.feiyong)}
|
||||||
fee_list = [{'cishu': f.cishu, 'feiyong': float(f.feiyong)} for f in fee_objs]
|
for f in tag.kaohecishufeiyong_set.all()]
|
||||||
|
|
||||||
# 若没有记录,补充默认费用
|
# 若没有记录,补充默认费用
|
||||||
if not fee_list:
|
if not fee_list:
|
||||||
@@ -151,12 +162,13 @@ class KaoheJinpaiTagsView(APIView):
|
|||||||
|
|
||||||
# 新增:返回所有在职考核官信息(用于指定考核官)
|
# 新增:返回所有在职考核官信息(用于指定考核官)
|
||||||
kaoheguan_list = []
|
kaoheguan_list = []
|
||||||
for kg in UserShenheguan.objects.filter(zhuangtai=1).select_related('user'):
|
# select_related 预加载 OneToOne 反向关系 DashouProfile,避免循环内 N+1
|
||||||
|
for kg in UserShenheguan.objects.filter(zhuangtai=1).select_related('user', 'user__DashouProfile'):
|
||||||
uid = kg.user.UserUID
|
uid = kg.user.UserUID
|
||||||
avatar = kg.user.Avatar or ''
|
avatar = kg.user.Avatar or ''
|
||||||
# 获取考核官的打手昵称(从打手扩展表)
|
# 获取考核官的打手昵称(从打手扩展表,已预加载)
|
||||||
dashou_nick = ''
|
dashou_nick = ''
|
||||||
dashou_prof = UserDashou.objects.filter(user=kg.user).first()
|
dashou_prof = getattr(kg.user, 'DashouProfile', None)
|
||||||
if dashou_prof:
|
if dashou_prof:
|
||||||
dashou_nick = dashou_prof.nicheng or ''
|
dashou_nick = dashou_prof.nicheng or ''
|
||||||
kaoheguan_list.append({
|
kaoheguan_list.append({
|
||||||
|
|||||||
@@ -48,20 +48,42 @@ class KaoheJiluView(APIView):
|
|||||||
total = query.count()
|
total = query.count()
|
||||||
records = query.order_by('-CreateTime')[(page-1)*page_size : page*page_size]
|
records = query.order_by('-CreateTime')[(page-1)*page_size : page*page_size]
|
||||||
|
|
||||||
|
# 批量预取关联数据,避免循环内 N+1
|
||||||
|
records = records.select_related('chenghao', 'bankuai')
|
||||||
|
records_list = list(records)
|
||||||
|
jilu_ids = [jl.jilu_id for jl in records_list]
|
||||||
|
|
||||||
|
# 批量汇总缴纳总额(按 jilu_id 分组聚合)
|
||||||
|
_fee_map = {}
|
||||||
|
if jilu_ids:
|
||||||
|
_fee_agg = KaoheJiluFeiyong.objects.filter(
|
||||||
|
jilu_id__in=jilu_ids
|
||||||
|
).values('jilu_id').annotate(total=models.Sum('jine'))
|
||||||
|
for item in _fee_agg:
|
||||||
|
_fee_map[item['jilu_id']] = float(item['total'] or 0.00)
|
||||||
|
|
||||||
|
# 批量获取上次拒绝原因(每个 jilu_id 取最近一条)
|
||||||
|
_fail_map = {}
|
||||||
|
if jilu_ids:
|
||||||
|
_reject_qs = KaoheJujueJilu.objects.filter(
|
||||||
|
jilu_id__in=jilu_ids
|
||||||
|
).order_by('jilu_id', '-CreateTime')
|
||||||
|
for rej in _reject_qs:
|
||||||
|
if rej.jilu_id not in _fail_map: # 只取首条(最近)
|
||||||
|
_fail_map[rej.jilu_id] = rej.yuanyin or ''
|
||||||
|
|
||||||
|
# 批量获取考核官头像
|
||||||
|
_avatar_map = {}
|
||||||
|
_kg_ids = {jl.shenheguan_id for jl in records_list if jl.shenheguan_id}
|
||||||
|
if _kg_ids:
|
||||||
|
for u in User.objects.filter(UserUID__in=_kg_ids):
|
||||||
|
_avatar_map[u.UserUID] = u.Avatar or ''
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
for jl in records:
|
for jl in records_list:
|
||||||
# 汇总缴纳总额
|
total_fee = _fee_map.get(jl.jilu_id, 0.00)
|
||||||
total_fee = KaoheJiluFeiyong.objects.filter(jilu=jl).aggregate(
|
fail_reason = _fail_map.get(jl.jilu_id, '')
|
||||||
total=models.Sum('jine')
|
kg_avatar = _avatar_map.get(jl.shenheguan_id, '')
|
||||||
)['total'] or 0.00
|
|
||||||
|
|
||||||
# 上次拒绝原因(最近一条)
|
|
||||||
last_fail = KaoheJujueJilu.objects.filter(jilu=jl).order_by('-CreateTime').first()
|
|
||||||
fail_reason = last_fail.yuanyin if last_fail else ''
|
|
||||||
|
|
||||||
# 考核官头像
|
|
||||||
kg_user = User.objects.filter(UserUID=jl.shenheguan_id).first()
|
|
||||||
kg_avatar = kg_user.Avatar if kg_user else ''
|
|
||||||
|
|
||||||
data.append({
|
data.append({
|
||||||
'jilu_id': jl.jilu_id,
|
'jilu_id': jl.jilu_id,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import F
|
from django.db.models import F, Count, Q
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
@@ -33,12 +33,17 @@ class ShenheDatingModuleView(APIView):
|
|||||||
.values_list('bankuai_id', flat=True))
|
.values_list('bankuai_id', flat=True))
|
||||||
|
|
||||||
bankuai_list = []
|
bankuai_list = []
|
||||||
for bk in Bankuai.objects.all():
|
all_bankuai = Bankuai.objects.annotate(
|
||||||
count = ShenheJilu.objects.filter(bankuai=bk, zhuangtai=3, shenheguan_id__isnull=True).count()
|
pending_count=Count(
|
||||||
|
'shenhejilu',
|
||||||
|
filter=Q(shenhejilu__zhuangtai=3, shenhejilu__shenheguan_id__isnull=True)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for bk in all_bankuai:
|
||||||
bankuai_list.append({
|
bankuai_list.append({
|
||||||
'bankuai_id': bk.bankuai_id,
|
'bankuai_id': bk.bankuai_id,
|
||||||
'mingcheng': bk.mingcheng,
|
'mingcheng': bk.mingcheng,
|
||||||
'count': count,
|
'count': bk.pending_count,
|
||||||
'is_related': bk.bankuai_id in related_ids
|
'is_related': bk.bankuai_id in related_ids
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -67,9 +72,13 @@ class ShenheDatingDataView(APIView):
|
|||||||
jilu_list = query.select_related('bankuai', 'chenghao').order_by('-CreateTime')[
|
jilu_list = query.select_related('bankuai', 'chenghao').order_by('-CreateTime')[
|
||||||
offset:offset + page_size]
|
offset:offset + page_size]
|
||||||
|
|
||||||
|
# 批量加载申请人用户,避免循环内 N+1
|
||||||
|
shenqingren_ids = [jl.shenqingren_id for jl in jilu_list if jl.shenqingren_id]
|
||||||
|
user_map = {u.UserUID: u for u in User.objects.filter(UserUID__in=shenqingren_ids)}
|
||||||
|
|
||||||
records = []
|
records = []
|
||||||
for jl in jilu_list:
|
for jl in jilu_list:
|
||||||
user = User.objects.filter(UserUID=jl.shenqingren_id).first()
|
user = user_map.get(jl.shenqingren_id)
|
||||||
avatar = user.Avatar if user else ''
|
avatar = user.Avatar if user else ''
|
||||||
texiao_json = {}
|
texiao_json = {}
|
||||||
if jl.chenghao and jl.chenghao.texiao_miaoshu:
|
if jl.chenghao and jl.chenghao.texiao_miaoshu:
|
||||||
@@ -202,9 +211,27 @@ class ShenheXiangqingView(APIView):
|
|||||||
offset = (page - 1) * page_size
|
offset = (page - 1) * page_size
|
||||||
jilu_list = base_query[offset:offset + page_size]
|
jilu_list = base_query[offset:offset + page_size]
|
||||||
|
|
||||||
|
# 批量加载申请人用户,避免循环内 N+1
|
||||||
|
shenqingren_ids = [jl.shenqingren_id for jl in jilu_list if jl.shenqingren_id]
|
||||||
|
user_map = {u.UserUID: u for u in User.objects.filter(UserUID__in=shenqingren_ids)}
|
||||||
|
|
||||||
|
# 批量预取费用明细和拒绝记录,避免循环内 N+1
|
||||||
|
_jilu_ids = [jl.jilu_id for jl in jilu_list]
|
||||||
|
_fee_detail_map = {} # (jilu_id, cishu) -> jine
|
||||||
|
_fail_map = {} # (jilu_id, cishu) -> yuanyin
|
||||||
|
if _jilu_ids:
|
||||||
|
for _fee in KaoheJiluFeiyong.objects.filter(jilu_id__in=_jilu_ids):
|
||||||
|
_fee_detail_map[(_fee.jilu_id, _fee.cishu)] = float(_fee.jine)
|
||||||
|
for _rej in KaoheJujueJilu.objects.filter(
|
||||||
|
jilu_id__in=_jilu_ids
|
||||||
|
).order_by('jilu_id', 'cishu', '-CreateTime'):
|
||||||
|
_key = (_rej.jilu_id, _rej.cishu)
|
||||||
|
if _key not in _fail_map: # 只取首条(最近)
|
||||||
|
_fail_map[_key] = _rej.yuanyin or ''
|
||||||
|
|
||||||
records = []
|
records = []
|
||||||
for jl in jilu_list:
|
for jl in jilu_list:
|
||||||
user = User.objects.filter(UserUID=jl.shenqingren_id).first()
|
user = user_map.get(jl.shenqingren_id)
|
||||||
avatar = user.Avatar if user else ''
|
avatar = user.Avatar if user else ''
|
||||||
texiao_json = {}
|
texiao_json = {}
|
||||||
if jl.chenghao and jl.chenghao.texiao_miaoshu:
|
if jl.chenghao and jl.chenghao.texiao_miaoshu:
|
||||||
@@ -216,25 +243,10 @@ class ShenheXiangqingView(APIView):
|
|||||||
# 是否开启金牌
|
# 是否开启金牌
|
||||||
is_jinpai = jl.chenghao.kaioi_jinpai if jl.chenghao else False
|
is_jinpai = jl.chenghao.kaioi_jinpai if jl.chenghao else False
|
||||||
|
|
||||||
# 本次待收考核费(基于记录中的当前次数,从费用明细表查询)
|
# 本次待收考核费(从预取 dict 查询)
|
||||||
daishou_fei = 0.00
|
daishou_fei = _fee_detail_map.get((jl.jilu_id, jl.cishu), 0.00)
|
||||||
fee_detail = KaoheJiluFeiyong.objects.filter(
|
# 上次失败原因(从预取 dict 查询)
|
||||||
jilu=jl,
|
last_fail_reason = _fail_map.get((jl.jilu_id, jl.cishu - 1), '')
|
||||||
cishu=jl.cishu
|
|
||||||
).first()
|
|
||||||
if fee_detail:
|
|
||||||
daishou_fei = float(fee_detail.jine)
|
|
||||||
else:
|
|
||||||
# 兜底:若明细表无记录,尝试用通用费用表计算
|
|
||||||
daishou_fei = 0.00
|
|
||||||
# 上次失败原因
|
|
||||||
last_fail_reason = ''
|
|
||||||
last_fail = KaoheJujueJilu.objects.filter(
|
|
||||||
jilu=jl,
|
|
||||||
cishu=jl.cishu - 1
|
|
||||||
).order_by('-CreateTime').first()
|
|
||||||
if last_fail:
|
|
||||||
last_fail_reason = last_fail.yuanyin
|
|
||||||
|
|
||||||
records.append({
|
records.append({
|
||||||
'jilu_id': jl.jilu_id,
|
'jilu_id': jl.jilu_id,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import threading
|
|||||||
import xmltodict
|
import xmltodict
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models, transaction, IntegrityError
|
from django.db import models, transaction, IntegrityError
|
||||||
@@ -260,38 +259,66 @@ def _fetch_images(order, oss_domain):
|
|||||||
|
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# 辅助函数:并发获取多个订单的打手图片
|
# 辅助函数:批量获取多个订单的打手图片(单次 DB 查询)
|
||||||
# ================================================================
|
# ================================================================
|
||||||
def _batch_images(orders, oss_domain):
|
def _batch_images(orders, oss_domain):
|
||||||
"""
|
"""
|
||||||
并发调用 _fetch_images,提升批量获取图片的速度。
|
批量获取订单的打手提交图片,使用单次 DB 查询替代循环/线程池 N 次查询。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
orders: Dingdan 订单对象列表
|
orders: Dingdan 订单对象列表
|
||||||
oss_domain: OSS 域名
|
oss_domain: OSS 域名
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
字典,键为订单ID (dingdan_id),值为 (images_list, is_punished, punishment_info)
|
字典,键为订单ID (OrderID),值为 (images_list, is_punished, punishment_info)
|
||||||
"""
|
"""
|
||||||
result = {} # 存储结果
|
result = {}
|
||||||
# 使用线程池并发执行,max_workers 可根据服务器性能调整
|
# 先为所有订单填充默认空值,保证调用方可拿到键
|
||||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
for order in orders:
|
||||||
# 建立 future 到订单ID的映射
|
result[order.OrderID] = ([], False, {})
|
||||||
future_to_oid = {
|
|
||||||
executor.submit(_fetch_images, order, oss_domain): order.OrderID
|
# 收集所有 (OrderID, PlayerID) 组合用于批量查询
|
||||||
for order in orders
|
_order_player_pairs = [
|
||||||
}
|
(order.OrderID, order.PlayerID)
|
||||||
# 等待所有任务完成
|
for order in orders
|
||||||
for future in as_completed(future_to_oid):
|
if order.PlayerID
|
||||||
oid = future_to_oid[future] # 对应的订单ID
|
]
|
||||||
try:
|
if not _order_player_pairs:
|
||||||
# 获取任务返回值
|
return result
|
||||||
imgs, pun, pinfo = future.result()
|
|
||||||
result[oid] = (imgs, pun, pinfo)
|
# 单次批量查询所有相关图片记录(避免循环 N 次或线程池掩盖 N 次查询)
|
||||||
except Exception as e:
|
# PlayerDeliveryImage 表的主键是 OrderID + PlayerID,使用元组列表过滤
|
||||||
# 某个订单获取失败,记录日志并赋予空值,保证整体流程不中断
|
_order_ids = [pair[0] for pair in _order_player_pairs]
|
||||||
logger.error(f"获取订单 {oid} 图片失败: {e}")
|
_pair_set = set(_order_player_pairs)
|
||||||
result[oid] = ([], False, {})
|
|
||||||
|
try:
|
||||||
|
rows = list(
|
||||||
|
PlayerDeliveryImage.query.filter(
|
||||||
|
OrderID__in=_order_ids
|
||||||
|
).values_list('OrderID', 'PlayerID', 'ImageURL')
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"_batch_images 批量查询图片失败: {e}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
# 按 (OrderID, PlayerID) 分组
|
||||||
|
_img_map = {}
|
||||||
|
for oid, pid, img_path in rows:
|
||||||
|
if not img_path:
|
||||||
|
continue
|
||||||
|
# 仅保留属于本批 orders 且 PlayerID 匹配的记录(防止跨打手串号)
|
||||||
|
if (oid, pid) not in _pair_set:
|
||||||
|
continue
|
||||||
|
if img_path.startswith('http'):
|
||||||
|
url = img_path
|
||||||
|
else:
|
||||||
|
url = oss_domain + img_path.lstrip('/')
|
||||||
|
_img_map.setdefault(oid, []).append(url)
|
||||||
|
|
||||||
|
# 把分组结果写回 result
|
||||||
|
for oid, imgs in _img_map.items():
|
||||||
|
if oid in result:
|
||||||
|
result[oid] = (imgs, False, {})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -771,10 +771,12 @@ class ShopProductModifyView1(APIView):
|
|||||||
else:
|
else:
|
||||||
# 自动从公共类型获取会员ID,或转为佣金
|
# 自动从公共类型获取会员ID,或转为佣金
|
||||||
try:
|
try:
|
||||||
shop_type = ShangpinLeixingDianpu.query.get(
|
shop_type = ShangpinLeixingDianpu.query.select_related('gonggong_leixing').get(
|
||||||
id=product.dianpu_leixing_id, dianpu_id=dianpu.id
|
id=product.dianpu_leixing_id, dianpu_id=dianpu.id
|
||||||
)
|
)
|
||||||
public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id)
|
public_type = shop_type.gonggong_leixing
|
||||||
|
if not public_type:
|
||||||
|
raise ShangpinLeixing.DoesNotExist
|
||||||
auto_huiyuan_id = public_type.huiyuan_id
|
auto_huiyuan_id = public_type.huiyuan_id
|
||||||
if auto_huiyuan_id:
|
if auto_huiyuan_id:
|
||||||
product.huiyuan_id = auto_huiyuan_id
|
product.huiyuan_id = auto_huiyuan_id
|
||||||
@@ -893,13 +895,12 @@ class ShopProductModifyView1(APIView):
|
|||||||
return Response({'code': 400, 'msg': '必填项缺失'})
|
return Response({'code': 400, 'msg': '必填项缺失'})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
shop_type = ShangpinLeixingDianpu.query.get(id=dianpu_leixing_id, dianpu_id=dianpu.id)
|
shop_type = ShangpinLeixingDianpu.query.select_related('gonggong_leixing').get(id=dianpu_leixing_id, dianpu_id=dianpu.id)
|
||||||
except ShangpinLeixingDianpu.DoesNotExist:
|
except ShangpinLeixingDianpu.DoesNotExist:
|
||||||
return Response({'code': 400, 'msg': '无效的店铺商品类型'})
|
return Response({'code': 400, 'msg': '无效的店铺商品类型'})
|
||||||
|
|
||||||
try:
|
public_type = shop_type.gonggong_leixing
|
||||||
public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id, shenhezhuangtai=1)
|
if not public_type or public_type.shenhezhuangtai != 1:
|
||||||
except ShangpinLeixing.DoesNotExist:
|
|
||||||
return Response({'code': 400, 'msg': '关联的公共商品类型无效'})
|
return Response({'code': 400, 'msg': '关联的公共商品类型无效'})
|
||||||
|
|
||||||
# 商品图片上传
|
# 商品图片上传
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class ShopRefundView(APIView):
|
|||||||
# 更新打手统计(退款量+1,状态置为空闲)
|
# 更新打手统计(退款量+1,状态置为空闲)
|
||||||
if dashou_id:
|
if dashou_id:
|
||||||
try:
|
try:
|
||||||
dashou_user = User.query.get(UserUID=dashou_id)
|
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
|
||||||
dashou_profile = dashou_user.DashouProfile
|
dashou_profile = dashou_user.DashouProfile
|
||||||
dashou_profile.tuikuanliang += 1
|
dashou_profile.tuikuanliang += 1
|
||||||
dashou_profile.zhuangtai = 1
|
dashou_profile.zhuangtai = 1
|
||||||
@@ -368,7 +368,7 @@ class ShopForceCompleteView(APIView):
|
|||||||
|
|
||||||
# ---------- 更新打手余额和统计 ----------
|
# ---------- 更新打手余额和统计 ----------
|
||||||
try:
|
try:
|
||||||
dashou_user = User.query.get(UserUID=dashou_id)
|
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
|
||||||
dashou_profile = dashou_user.DashouProfile
|
dashou_profile = dashou_user.DashouProfile
|
||||||
dashou_profile.chengjiaozongliang += 1
|
dashou_profile.chengjiaozongliang += 1
|
||||||
dashou_profile.yue += dashou_fencheng
|
dashou_profile.yue += dashou_fencheng
|
||||||
|
|||||||
@@ -88,29 +88,41 @@ from .chufa import (
|
|||||||
ShangjiaChufaJiluHuoquView,
|
ShangjiaChufaJiluHuoquView,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .kefu import (
|
from .kefu_base import (
|
||||||
|
KefuGetOrderTypesView,
|
||||||
|
KefuLoginView,
|
||||||
|
KefuStatsView,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .kefu_orders import (
|
||||||
KefuCancelDesignationView,
|
KefuCancelDesignationView,
|
||||||
KefuChangeDashouView,
|
KefuChangeDashouView,
|
||||||
KefuForceCompleteView,
|
KefuForceCompleteView,
|
||||||
KefuGetDashouDetailView,
|
|
||||||
KefuGetDashouListView,
|
|
||||||
KefuGetOrderDetailView,
|
KefuGetOrderDetailView,
|
||||||
KefuGetOrderListView,
|
KefuGetOrderListView,
|
||||||
KefuGetOrderTypesView,
|
|
||||||
KefuGetShangjiaOrderListView,
|
KefuGetShangjiaOrderListView,
|
||||||
KefuLoginView,
|
|
||||||
KefuMerchantRefundView,
|
KefuMerchantRefundView,
|
||||||
KefuPlatformRefundView,
|
KefuPlatformRefundView,
|
||||||
|
KefuRecoverOrderView,
|
||||||
|
KefuRejectRefundView,
|
||||||
|
KefuRejectSettlementView,
|
||||||
|
KefuTransferHallView,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .kefu_dashou import (
|
||||||
|
KefuGetDashouDetailView,
|
||||||
|
KefuGetDashouListView,
|
||||||
|
KefuUpdateDashouView,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .kefu_punishment import (
|
||||||
KefuPunishView,
|
KefuPunishView,
|
||||||
KefuPunishmentActionView,
|
KefuPunishmentActionView,
|
||||||
KefuPunishmentDetailView,
|
KefuPunishmentDetailView,
|
||||||
KefuPunishmentListView,
|
KefuPunishmentListView,
|
||||||
KefuRecoverOrderView,
|
)
|
||||||
KefuRejectRefundView,
|
|
||||||
KefuRejectSettlementView,
|
from .kefu_withdraw import (
|
||||||
KefuStatsView,
|
|
||||||
KefuTransferHallView,
|
|
||||||
KefuUpdateDashouView,
|
|
||||||
KefuWithdrawActionView,
|
KefuWithdrawActionView,
|
||||||
KefuWithdrawDetailView,
|
KefuWithdrawDetailView,
|
||||||
KefuWithdrawListView,
|
KefuWithdrawListView,
|
||||||
|
|||||||
@@ -532,12 +532,18 @@ class AdGetOrderList(APIView):
|
|||||||
# 注意:这里统计所有订单,不考虑筛选条件
|
# 注意:这里统计所有订单,不考虑筛选条件
|
||||||
try:
|
try:
|
||||||
# 平台订单统计 (Platform=1)
|
# 平台订单统计 (Platform=1)
|
||||||
platform_total = Order.query.filter(Platform=1).count()
|
_agg = Order.query.aggregate(
|
||||||
platform_completed = Order.query.filter(Platform=1, Status=3).count()
|
platform_total=Count('id', filter=Q(Platform=1)),
|
||||||
|
platform_completed=Count('id', filter=Q(Platform=1, Status=3)),
|
||||||
|
merchant_total=Count('id', filter=Q(Platform=2)),
|
||||||
|
merchant_completed=Count('id', filter=Q(Platform=2, Status=3)),
|
||||||
|
)
|
||||||
|
platform_total = _agg['platform_total']
|
||||||
|
platform_completed = _agg['platform_completed']
|
||||||
|
|
||||||
# 商家订单统计 (Platform=2)
|
# 商家订单统计 (Platform=2)
|
||||||
merchant_total = Order.query.filter(Platform=2).count()
|
merchant_total = _agg['merchant_total']
|
||||||
merchant_completed = Order.query.filter(Platform=2, Status=3).count()
|
merchant_completed = _agg['merchant_completed']
|
||||||
|
|
||||||
platform_stats = {
|
platform_stats = {
|
||||||
'total': platform_total,
|
'total': platform_total,
|
||||||
@@ -1170,9 +1176,14 @@ class CfGuanLi(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 7. 获取统计信息(总是需要统计)
|
# 7. 获取统计信息(总是需要统计)
|
||||||
zongshu = PenaltyRecord.query.count()
|
_agg = PenaltyRecord.query.aggregate(
|
||||||
daichuli = PenaltyRecord.query.filter(ApplyStatus__in=[0, 3]).count()
|
zongshu=Count('id'),
|
||||||
yichuli = PenaltyRecord.query.filter(ApplyStatus__in=[1, 2]).count()
|
daichuli=Count('id', filter=Q(ApplyStatus__in=[0, 3])),
|
||||||
|
yichuli=Count('id', filter=Q(ApplyStatus__in=[1, 2])),
|
||||||
|
)
|
||||||
|
zongshu = _agg['zongshu']
|
||||||
|
daichuli = _agg['daichuli']
|
||||||
|
yichuli = _agg['yichuli']
|
||||||
|
|
||||||
# 8. 如果只请求统计信息,直接返回
|
# 8. 如果只请求统计信息,直接返回
|
||||||
if qingqiu_tongji:
|
if qingqiu_tongji:
|
||||||
@@ -1687,6 +1698,13 @@ class AdckyhxqView(APIView):
|
|||||||
yonghu_id=uid
|
yonghu_id=uid
|
||||||
).order_by('-CreateTime')
|
).order_by('-CreateTime')
|
||||||
|
|
||||||
|
# 批量预取会员详情
|
||||||
|
_huiyuan_ids = [r.huiyuan_id for r in member_records]
|
||||||
|
_huiyuan_map = {
|
||||||
|
h.huiyuan_id: h
|
||||||
|
for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao')
|
||||||
|
}
|
||||||
|
|
||||||
member_list = []
|
member_list = []
|
||||||
for record in member_records:
|
for record in member_records:
|
||||||
# 调用方法检查是否过期,并更新状态
|
# 调用方法检查是否过期,并更新状态
|
||||||
@@ -1694,9 +1712,7 @@ class AdckyhxqView(APIView):
|
|||||||
|
|
||||||
if not is_expired: # 只返回未过期的
|
if not is_expired: # 只返回未过期的
|
||||||
# 查询会员详情
|
# 查询会员详情
|
||||||
huiyuan = Huiyuan.query.filter(
|
huiyuan = _huiyuan_map.get(record.huiyuan_id)
|
||||||
huiyuan_id=record.huiyuan_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if huiyuan:
|
if huiyuan:
|
||||||
member_list.append({
|
member_list.append({
|
||||||
@@ -2282,15 +2298,13 @@ class AddtxshView(APIView):
|
|||||||
has_more = (offset + current_count) < total_count
|
has_more = (offset + current_count) < total_count
|
||||||
|
|
||||||
# 获取状态统计(统计与搜索无关,依然按筛选条件统计)
|
# 获取状态统计(统计与搜索无关,依然按筛选条件统计)
|
||||||
|
_agg = Tixianjilu.query.filter(leixing=leixing).aggregate(
|
||||||
|
awaiting=Count('id', filter=Q(zhuangtai=1)),
|
||||||
|
processed=Count('id', filter=Q(zhuangtai__in=[2, 3])),
|
||||||
|
)
|
||||||
status_counts = {
|
status_counts = {
|
||||||
'awaiting': Tixianjilu.query.filter(
|
'awaiting': _agg['awaiting'],
|
||||||
zhuangtai=1,
|
'processed': _agg['processed'],
|
||||||
leixing=leixing
|
|
||||||
).count(),
|
|
||||||
'processed': Tixianjilu.query.filter(
|
|
||||||
zhuangtai__in=[2, 3],
|
|
||||||
leixing=leixing
|
|
||||||
).count()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 构建响应数据
|
# 构建响应数据
|
||||||
|
|||||||
@@ -526,16 +526,16 @@ class DashouShensuView(APIView):
|
|||||||
chufa_record.save()
|
chufa_record.save()
|
||||||
|
|
||||||
# 6. 保存申诉图片(如果有)
|
# 6. 保存申诉图片(如果有)
|
||||||
saved_tupian_urls = []
|
saved_tupian_urls = [u for u in shensu_tupian_urls if u] if shensu_tupian_urls else []
|
||||||
if shensu_tupian_urls:
|
if saved_tupian_urls:
|
||||||
for tupian_url in shensu_tupian_urls:
|
PenaltyEvidenceImage.objects.bulk_create([
|
||||||
if tupian_url: # 确保URL不为空
|
PenaltyEvidenceImage(
|
||||||
chufa_tupian = PenaltyEvidenceImage.query.create(
|
OrderID=chufa_record.OrderID,
|
||||||
OrderID=chufa_record.OrderID,
|
UserID=yonghuid,
|
||||||
UserID=yonghuid,
|
ImageURL=tupian_url,
|
||||||
ImageURL=tupian_url
|
)
|
||||||
)
|
for tupian_url in saved_tupian_urls
|
||||||
saved_tupian_urls.append(tupian_url)
|
])
|
||||||
|
|
||||||
# 7. 构建返回数据
|
# 7. 构建返回数据
|
||||||
response_data = {
|
response_data = {
|
||||||
|
|||||||
3794
users/views/kefu.py
3794
users/views/kefu.py
File diff suppressed because it is too large
Load Diff
383
users/views/kefu_base.py
Normal file
383
users/views/kefu_base.py
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
"""users.views.kefu_base - auto-generated by split script."""
|
||||||
|
"""users.views.kefu - auto-generated by split script."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import requests
|
||||||
|
import hashlib
|
||||||
|
import traceback
|
||||||
|
import decimal
|
||||||
|
import jwt
|
||||||
|
import ipaddress
|
||||||
|
import xmltodict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
import defusedxml.ElementTree as ET
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models, transaction, IntegrityError, DatabaseError, connection
|
||||||
|
from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.views import View
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import permissions, status
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
|
from rest_framework.throttling import AnonRateThrottle
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from utils.money import yuan_to_fen
|
||||||
|
from utils.fadan_utils import check_fadan_qiangdan_eligible
|
||||||
|
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||||
|
from users.fadan_fenhong_utils import process_fadan_fenhong
|
||||||
|
|
||||||
|
from orders.utils import (
|
||||||
|
update_daily_payout,
|
||||||
|
settle_shangjia_order_guanshi_fenhong
|
||||||
|
)
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action, update_guanshi_daily_by_action,
|
||||||
|
update_shangjia_daily, update_zuzhang_daily_by_action,
|
||||||
|
verify_kefu_permission
|
||||||
|
)
|
||||||
|
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
|
||||||
|
|
||||||
|
from ..models import (
|
||||||
|
UserBoss, UserDashou, UserShangjia, UserGuanshi,
|
||||||
|
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
|
||||||
|
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
|
||||||
|
RankingRecord
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
from ..tixian_shenhe_services import (
|
||||||
|
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
|
||||||
|
mark_transfer_success, release_collect_quota_for_record,
|
||||||
|
close_audit_wechat_failed, query_wechat_transfer_bill,
|
||||||
|
check_shijidaozhang_within_limit,
|
||||||
|
check_dashou_trial_blocks_commission_withdraw,
|
||||||
|
WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING,
|
||||||
|
)
|
||||||
|
from ..tixian_shenhe_views import process_audit_collect
|
||||||
|
|
||||||
|
from orders.models import (
|
||||||
|
Order, PlatformOrderExt, MerchantOrderExt, CommissionRate,
|
||||||
|
PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage,
|
||||||
|
Penalty, PenaltyAppealImage
|
||||||
|
)
|
||||||
|
from products.models import (
|
||||||
|
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
|
||||||
|
)
|
||||||
|
from config.models import Qunpeizhi
|
||||||
|
from backend.models import MerchantDailyStats
|
||||||
|
from rank.models import (
|
||||||
|
KaohePayTemp, Chenghao, ShenheJilu,
|
||||||
|
KaoheCishuFeiyong, YonghuChenghao
|
||||||
|
)
|
||||||
|
from users.models import UserShenheguan
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from rank.utils import create_shenhe_jilu_from_temp
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class KefuLoginView(APIView):
|
||||||
|
"""
|
||||||
|
客服/管理员登录接口(支持手机号重复)
|
||||||
|
请求:POST /yonghu/kefujinru
|
||||||
|
请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"}
|
||||||
|
返回:
|
||||||
|
code=0 成功,data包含token,nicheng,yonghuid
|
||||||
|
code=1 失败(msg区分:用户不存在/密码错误/二级密码错误)
|
||||||
|
code=4 账号被封禁
|
||||||
|
|
||||||
|
管理员(IsSuperuser 或 UserType='admin')无需 erjimima 和 KefuProfile。
|
||||||
|
"""
|
||||||
|
throttle_classes = [AnonRateThrottle] # 限制匿名请求频率
|
||||||
|
permission_classes = [AllowAny] # 任何人都可访问
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# 1. 获取参数
|
||||||
|
phone = request.data.get('phone', '').strip()
|
||||||
|
password = request.data.get('password', '')
|
||||||
|
erjimima = request.data.get('erjimima', '')
|
||||||
|
|
||||||
|
if not phone or not password:
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '请填写手机号和密码',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 2. 获取客户端真实IP
|
||||||
|
client_ip = self.get_client_ip(request)
|
||||||
|
|
||||||
|
# 3. 查询该手机号下所有账号(客服 + 管理员)
|
||||||
|
# 先查客服(有 KefuProfile),再查管理员(IsSuperuser 或 UserType='admin')
|
||||||
|
user_mains = list(
|
||||||
|
User.query.filter(
|
||||||
|
Phone=phone,
|
||||||
|
KefuProfile__isnull=False,
|
||||||
|
).select_related('KefuProfile')
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果没有客服账号,尝试查找管理员账号
|
||||||
|
if not user_mains:
|
||||||
|
admin_candidates = User.query.filter(Phone=phone)
|
||||||
|
for admin_user in admin_candidates:
|
||||||
|
if admin_user.IsSuperuser or admin_user.UserType == 'admin':
|
||||||
|
user_mains = [admin_user]
|
||||||
|
break
|
||||||
|
|
||||||
|
if not user_mains:
|
||||||
|
logger.warning(f"登录失败,用户不存在: {phone}, IP: {client_ip}")
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '用户不存在',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 4. 遍历所有匹配的账号,验证密码和二级密码
|
||||||
|
target_user = None
|
||||||
|
target_kefu = None
|
||||||
|
password_matched = False
|
||||||
|
|
||||||
|
for user in user_mains:
|
||||||
|
# 验证主密码(bcrypt 哈希验证)
|
||||||
|
if not user.CheckPassword(password):
|
||||||
|
continue
|
||||||
|
password_matched = True
|
||||||
|
|
||||||
|
# 管理员:跳过二级密码和客服扩展表检查
|
||||||
|
is_admin = user.IsSuperuser or user.UserType == 'admin'
|
||||||
|
if is_admin:
|
||||||
|
target_user = user
|
||||||
|
target_kefu = None
|
||||||
|
break
|
||||||
|
|
||||||
|
# 客服:验证二级密码
|
||||||
|
kefu = user.KefuProfile
|
||||||
|
stored_erji = kefu.erjimima or ''
|
||||||
|
if stored_erji.startswith(('$2b$', '$2a$')) and len(stored_erji) == 60:
|
||||||
|
import bcrypt as _bcrypt
|
||||||
|
if not _bcrypt.checkpw(erjimima.encode('utf-8'), stored_erji.encode('utf-8')):
|
||||||
|
continue
|
||||||
|
elif stored_erji != erjimima:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 如果账号被禁用,记录但继续查找
|
||||||
|
if kefu.zhuangtai != 1:
|
||||||
|
if target_user is None:
|
||||||
|
target_user = user
|
||||||
|
target_kefu = kefu
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 找到完全匹配且状态正常的账号,立即使用
|
||||||
|
target_user = user
|
||||||
|
target_kefu = kefu
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# 循环结束未找到正常账号
|
||||||
|
if target_user and target_kefu:
|
||||||
|
# 只有被禁用的账号匹配,返回封禁提示
|
||||||
|
logger.warning(f"登录失败,客服账号已禁用: {phone}, IP: {client_ip}")
|
||||||
|
return Response({
|
||||||
|
'code': 4,
|
||||||
|
'msg': '账号已被封禁',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
elif password_matched:
|
||||||
|
# 密码正确但二级密码错误
|
||||||
|
logger.warning(f"登录失败,二级密码错误: {phone}, IP: {client_ip}")
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '二级密码错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
else:
|
||||||
|
# 密码错误
|
||||||
|
logger.warning(f"登录失败,密码错误: {phone}, IP: {client_ip}")
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '密码错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 5. 更新IP和最后登录时间
|
||||||
|
target_user.IP = client_ip
|
||||||
|
target_user.UserLastLoginDate = timezone.now()
|
||||||
|
target_user.save(update_fields=['IP', 'UserLastLoginDate'])
|
||||||
|
|
||||||
|
# 6. 生成JWT token
|
||||||
|
refresh = RefreshToken.for_user(target_user)
|
||||||
|
token = str(refresh.access_token)
|
||||||
|
|
||||||
|
# 7. 返回成功数据(管理员无 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': []})
|
||||||
636
users/views/kefu_dashou.py
Normal file
636
users/views/kefu_dashou.py
Normal file
@@ -0,0 +1,636 @@
|
|||||||
|
"""users.views.kefu_dashou - auto-generated by split script."""
|
||||||
|
"""users.views.kefu - auto-generated by split script."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import requests
|
||||||
|
import hashlib
|
||||||
|
import traceback
|
||||||
|
import decimal
|
||||||
|
import jwt
|
||||||
|
import ipaddress
|
||||||
|
import xmltodict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
import defusedxml.ElementTree as ET
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models, transaction, IntegrityError, DatabaseError, connection
|
||||||
|
from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.views import View
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import permissions, status
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
|
from rest_framework.throttling import AnonRateThrottle
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from utils.money import yuan_to_fen
|
||||||
|
from utils.fadan_utils import check_fadan_qiangdan_eligible
|
||||||
|
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||||
|
from users.fadan_fenhong_utils import process_fadan_fenhong
|
||||||
|
|
||||||
|
from orders.utils import (
|
||||||
|
update_daily_payout,
|
||||||
|
settle_shangjia_order_guanshi_fenhong
|
||||||
|
)
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action, update_guanshi_daily_by_action,
|
||||||
|
update_shangjia_daily, update_zuzhang_daily_by_action,
|
||||||
|
verify_kefu_permission
|
||||||
|
)
|
||||||
|
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
|
||||||
|
|
||||||
|
from ..models import (
|
||||||
|
UserBoss, UserDashou, UserShangjia, UserGuanshi,
|
||||||
|
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
|
||||||
|
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
|
||||||
|
RankingRecord
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
from ..tixian_shenhe_services import (
|
||||||
|
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
|
||||||
|
mark_transfer_success, release_collect_quota_for_record,
|
||||||
|
close_audit_wechat_failed, query_wechat_transfer_bill,
|
||||||
|
check_shijidaozhang_within_limit,
|
||||||
|
check_dashou_trial_blocks_commission_withdraw,
|
||||||
|
WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING,
|
||||||
|
)
|
||||||
|
from ..tixian_shenhe_views import process_audit_collect
|
||||||
|
|
||||||
|
from orders.models import (
|
||||||
|
Order, PlatformOrderExt, MerchantOrderExt, CommissionRate,
|
||||||
|
PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage,
|
||||||
|
Penalty, PenaltyAppealImage
|
||||||
|
)
|
||||||
|
from products.models import (
|
||||||
|
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
|
||||||
|
)
|
||||||
|
from config.models import Qunpeizhi
|
||||||
|
from backend.models import MerchantDailyStats
|
||||||
|
from rank.models import (
|
||||||
|
KaohePayTemp, Chenghao, ShenheJilu,
|
||||||
|
KaoheCishuFeiyong, YonghuChenghao
|
||||||
|
)
|
||||||
|
from users.models import UserShenheguan
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from rank.utils import create_shenhe_jilu_from_temp
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class KefuGetDashouListView(APIView):
|
||||||
|
"""
|
||||||
|
客服获取打手列表接口(仅打手类型)
|
||||||
|
请求:POST /yonghu/kefuhqdslb
|
||||||
|
参数:{
|
||||||
|
"phone": "客服手机号",
|
||||||
|
"keyword": "搜索关键词(可选,搜索用户ID或昵称)",
|
||||||
|
"page": "页码(从1开始)",
|
||||||
|
"page_size": "每页数量"
|
||||||
|
}
|
||||||
|
认证:JWT
|
||||||
|
返回:{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"list": [打手对象],
|
||||||
|
"has_more": true/false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# 1. 获取参数
|
||||||
|
phone = request.data.get('zhanghao', '').strip()
|
||||||
|
keyword = request.data.get('keyword', '').strip()
|
||||||
|
try:
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('page_size', 20))
|
||||||
|
except ValueError:
|
||||||
|
return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if not phone:
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 2. 客服身份验证
|
||||||
|
current_user = request.user
|
||||||
|
if getattr(current_user, 'Phone', '') != phone:
|
||||||
|
logger.warning(f"手机号不匹配: {phone}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
if current_user.UserType not in ('kefu', 'admin'):
|
||||||
|
logger.warning(f"用户类型非客服: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||||||
|
kefu = None
|
||||||
|
if not is_admin:
|
||||||
|
try:
|
||||||
|
kefu = current_user.KefuProfile
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
if kefu.zhuangtai != 1:
|
||||||
|
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 3. 构建查询条件(只查询打手)
|
||||||
|
# 通过 UserDashou 扩展表关联 User
|
||||||
|
queryset = UserDashou.query.select_related('user').all()
|
||||||
|
|
||||||
|
if keyword:
|
||||||
|
# 搜索用户ID或打手昵称
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(user__UserUID__icontains=keyword) |
|
||||||
|
Q(nicheng__icontains=keyword)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. 计算总数并分页
|
||||||
|
total = queryset.count()
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
dashou_list = queryset.order_by('-CreateTime')[offset:offset + page_size]
|
||||||
|
|
||||||
|
# 5. 构建返回数据(只取需要的字段)
|
||||||
|
result = []
|
||||||
|
for dashou in dashou_list:
|
||||||
|
user = dashou.user
|
||||||
|
result.append({
|
||||||
|
'yonghuid': user.UserUID,
|
||||||
|
'avatar': user.Avatar or '',
|
||||||
|
'zaixianzhuangtai': dashou.zaixianzhuangtai,
|
||||||
|
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
|
||||||
|
'nicheng': dashou.nicheng or '',
|
||||||
|
'zhuangtai': dashou.zhuangtai,
|
||||||
|
})
|
||||||
|
|
||||||
|
# 6. 判断是否有更多数据
|
||||||
|
has_more = (offset + len(dashou_list)) < total
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'list': result,
|
||||||
|
'has_more': has_more
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class KefuGetDashouDetailView(APIView):
|
||||||
|
"""
|
||||||
|
客服获取打手详情接口
|
||||||
|
请求:POST /yonghu/kefuhqdsxq
|
||||||
|
参数:{
|
||||||
|
"phone": "客服手机号",
|
||||||
|
"uid": "打手ID (yonghuid)"
|
||||||
|
}
|
||||||
|
认证:JWT
|
||||||
|
返回:code=0 + 打手信息 + 会员列表
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# 1. 获取参数
|
||||||
|
phone = request.data.get('phone', '').strip()
|
||||||
|
uid = request.data.get('uid', '').strip()
|
||||||
|
|
||||||
|
if not phone or not uid:
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 2. 客服身份验证
|
||||||
|
current_user = request.user
|
||||||
|
if getattr(current_user, 'Phone', '') != phone:
|
||||||
|
logger.warning(f"手机号不匹配: {phone}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
if current_user.UserType not in ('kefu', 'admin'):
|
||||||
|
logger.warning(f"用户类型非客服: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||||||
|
kefu = None
|
||||||
|
if not is_admin:
|
||||||
|
try:
|
||||||
|
kefu = current_user.KefuProfile
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
if kefu.zhuangtai != 1:
|
||||||
|
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 3. 查询打手主表和扩展表
|
||||||
|
try:
|
||||||
|
# 使用 select_related 预加载 dashou_profile,减少数据库查询
|
||||||
|
user_main = User.query.select_related('DashouProfile').get(
|
||||||
|
UserUID=uid
|
||||||
|
)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
# 验证用户类型是否为打手(可选,但建议验证)
|
||||||
|
# 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册)
|
||||||
|
try:
|
||||||
|
dashou = user_main.DashouProfile
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
# 用户存在但未注册打手身份,返回基本信息
|
||||||
|
user_info = {
|
||||||
|
'yonghuid': user_main.UserUID,
|
||||||
|
'avatar': user_main.Avatar or '',
|
||||||
|
'zaixianzhuangtai': 0,
|
||||||
|
'zhanghaozhuangtai': 0,
|
||||||
|
'zhuangtai': 0,
|
||||||
|
'nicheng': '',
|
||||||
|
'chenghao': '',
|
||||||
|
'dianhua': '',
|
||||||
|
'wechat': '',
|
||||||
|
'phone': user_main.Phone or '',
|
||||||
|
'CreateTime': user_main.UserCreateTime,
|
||||||
|
'create_time': user_main.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user_main, 'UserCreateTime', None) else '',
|
||||||
|
'ip': user_main.IP or '',
|
||||||
|
'jiedanzongliang': 0,
|
||||||
|
'chengjiaozongliang': 0,
|
||||||
|
'tuikuanliang': 0,
|
||||||
|
'jinrijiedan': 0,
|
||||||
|
'yue': 0.00,
|
||||||
|
'zonge': 0.00,
|
||||||
|
'jifen': 0,
|
||||||
|
'yajin': 0.00,
|
||||||
|
'jinrishouyi': 0.00,
|
||||||
|
'jinyueshouyi': 0.00,
|
||||||
|
'yaoqingren': '',
|
||||||
|
'jieshao': '',
|
||||||
|
}
|
||||||
|
member_list = []
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'user_info': user_info,
|
||||||
|
'member_list': member_list
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 4. 构建打手信息
|
||||||
|
user_info = {
|
||||||
|
'yonghuid': user_main.UserUID,
|
||||||
|
'avatar': user_main.Avatar or '',
|
||||||
|
'zaixianzhuangtai': dashou.zaixianzhuangtai,
|
||||||
|
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
|
||||||
|
'zhuangtai': dashou.zhuangtai,
|
||||||
|
'nicheng': dashou.nicheng or '',
|
||||||
|
'chenghao': dashou.chenghao or '',
|
||||||
|
'dianhua': dashou.dianhua or '',
|
||||||
|
'wechat': dashou.wechat or '',
|
||||||
|
'phone': user_main.Phone or '',
|
||||||
|
'CreateTime': user_main.UserCreateTime,
|
||||||
|
'create_time': user_main.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user_main, 'UserCreateTime', None) else '',
|
||||||
|
'ip': user_main.IP or '',
|
||||||
|
'jiedanzongliang': dashou.jiedanzongliang,
|
||||||
|
'chengjiaozongliang': dashou.chengjiaozongliang,
|
||||||
|
'tuikuanliang': dashou.tuikuanliang,
|
||||||
|
'jinrijiedan': dashou.jinrijiedan,
|
||||||
|
'yue': float(dashou.yue) if dashou.yue else 0.00,
|
||||||
|
'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
|
||||||
|
'jifen': dashou.jifen,
|
||||||
|
'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
|
||||||
|
'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
|
||||||
|
'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
|
||||||
|
'yaoqingren': dashou.yaoqingren or '',
|
||||||
|
'jieshao': dashou.jieshao or '',
|
||||||
|
}
|
||||||
|
|
||||||
|
# 5. 查询打手会员信息(仅未过期)
|
||||||
|
member_records = list(Huiyuangoumai.query.filter(
|
||||||
|
yonghu_id=uid
|
||||||
|
).order_by('-CreateTime'))
|
||||||
|
huiyuan_ids = {r.huiyuan_id for r in member_records if r.huiyuan_id}
|
||||||
|
huiyuan_map = {
|
||||||
|
h.huiyuan_id: h.jieshao
|
||||||
|
for h in Huiyuan.query.filter(huiyuan_id__in=huiyuan_ids).only('huiyuan_id', 'jieshao')
|
||||||
|
}
|
||||||
|
|
||||||
|
member_list = []
|
||||||
|
for record in member_records:
|
||||||
|
# 调用模型方法检查是否过期
|
||||||
|
if hasattr(record, 'jiance_shifou_daoqi'):
|
||||||
|
is_expired = record.jiance_shifou_daoqi()
|
||||||
|
else:
|
||||||
|
# 如果方法不存在,简单判断
|
||||||
|
is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
|
||||||
|
|
||||||
|
if not is_expired:
|
||||||
|
# 获取会员详情
|
||||||
|
huiyuan_name = huiyuan_map.get(record.huiyuan_id, '未知会员')
|
||||||
|
|
||||||
|
member_list.append({
|
||||||
|
'id': record.id,
|
||||||
|
'huiyuan_name': huiyuan_name,
|
||||||
|
'daoqi_time': record.daoqi_time,
|
||||||
|
'is_active': record.huiyuan_zhuangtai == 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'user_info': user_info,
|
||||||
|
'member_list': member_list
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class KefuUpdateDashouView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
"""
|
||||||
|
客服修改打手信息接口
|
||||||
|
支持操作:
|
||||||
|
- jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb)
|
||||||
|
- jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb)
|
||||||
|
- jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd)
|
||||||
|
- gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee)
|
||||||
|
- gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee)
|
||||||
|
- gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee)
|
||||||
|
- jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg)
|
||||||
|
"""
|
||||||
|
def post(self, request):
|
||||||
|
# 1. 获取前端参数
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
dashou_id = request.data.get('dashou_id', '').strip()
|
||||||
|
caozuo = request.data.get('caozuo', '').strip()
|
||||||
|
value = request.data.get('value') # 金额、积分、天数等
|
||||||
|
huiyuan_id = request.data.get('huiyuan_id', '').strip() # 会员ID(加/减会员时使用)
|
||||||
|
|
||||||
|
if not username or not dashou_id or not caozuo:
|
||||||
|
return Response({'code': 400, 'msg': '参数不完整'})
|
||||||
|
|
||||||
|
# 2. 权限校验(统一方法)
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions # 直接返回错误响应
|
||||||
|
|
||||||
|
# 3. 查询打手是否存在
|
||||||
|
try:
|
||||||
|
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
|
||||||
|
dashou_profile = dashou_user.DashouProfile
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '打手不存在'})
|
||||||
|
except UserDashou.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '打手扩展信息缺失'})
|
||||||
|
|
||||||
|
# 4. 根据操作类型执行(事务内)
|
||||||
|
with transaction.atomic():
|
||||||
|
try:
|
||||||
|
# 记录修改前的值(用于日志)
|
||||||
|
before = {
|
||||||
|
'yue': dashou_profile.yue,
|
||||||
|
'yajin': dashou_profile.yajin,
|
||||||
|
'jifen': dashou_profile.jifen,
|
||||||
|
'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
|
||||||
|
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
|
||||||
|
'zhuangtai': dashou_profile.zhuangtai,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 余额操作 ----------
|
||||||
|
if caozuo == 'jian_yue':
|
||||||
|
if '001aa' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限减打手余额'})
|
||||||
|
amount = Decimal(str(value))
|
||||||
|
if dashou_profile.yue < amount:
|
||||||
|
return Response({'code': 400, 'msg': '余额不足'})
|
||||||
|
dashou_profile.yue -= amount
|
||||||
|
dashou_profile.save()
|
||||||
|
after_yue = dashou_profile.yue
|
||||||
|
# 记录日志
|
||||||
|
Xiugaijilu.query.create(
|
||||||
|
yonghuid=dashou_id,
|
||||||
|
xiugaiid=kefu.user.Phone,
|
||||||
|
leixing=2, # 2=打手
|
||||||
|
xiugaitijiao=after_yue,
|
||||||
|
xiugaitijiaoq=before['yue'],
|
||||||
|
)
|
||||||
|
return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after_yue)}})
|
||||||
|
|
||||||
|
elif caozuo == 'jia_yue':
|
||||||
|
if '001bb' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限加打手余额'})
|
||||||
|
from jituan.services.group_finance_gate import deny_group_finance_exec
|
||||||
|
deny = deny_group_finance_exec(request.user, request)
|
||||||
|
if deny:
|
||||||
|
return deny
|
||||||
|
amount = Decimal(str(value))
|
||||||
|
dashou_profile.yue += amount
|
||||||
|
dashou_profile.save()
|
||||||
|
after_yue = dashou_profile.yue
|
||||||
|
Xiugaijilu.query.create(
|
||||||
|
yonghuid=dashou_id,
|
||||||
|
xiugaiid=kefu.user.Phone,
|
||||||
|
leixing=2,
|
||||||
|
xiugaitijiao=after_yue,
|
||||||
|
xiugaitijiaoq=before['yue'],
|
||||||
|
)
|
||||||
|
return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after_yue)}})
|
||||||
|
|
||||||
|
# ---------- 押金操作 ----------
|
||||||
|
elif caozuo == 'jian_yajin':
|
||||||
|
if '001aa' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限减打手押金'})
|
||||||
|
amount = Decimal(str(value))
|
||||||
|
if dashou_profile.yajin < amount:
|
||||||
|
return Response({'code': 400, 'msg': '押金不足'})
|
||||||
|
dashou_profile.yajin -= amount
|
||||||
|
dashou_profile.save()
|
||||||
|
after_yajin = dashou_profile.yajin
|
||||||
|
Xiugaijilu.query.create(
|
||||||
|
yonghuid=dashou_id,
|
||||||
|
xiugaiid=kefu.user.Phone,
|
||||||
|
leixing=2,
|
||||||
|
yajin=after_yajin,
|
||||||
|
yyajin=before['yajin'],
|
||||||
|
)
|
||||||
|
return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after_yajin)}})
|
||||||
|
|
||||||
|
elif caozuo == 'jia_yajin':
|
||||||
|
if '001bb' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限加打手押金'})
|
||||||
|
from jituan.services.group_finance_gate import deny_group_finance_exec
|
||||||
|
deny = deny_group_finance_exec(request.user, request)
|
||||||
|
if deny:
|
||||||
|
return deny
|
||||||
|
amount = Decimal(str(value))
|
||||||
|
dashou_profile.yajin += amount
|
||||||
|
dashou_profile.save()
|
||||||
|
after_yajin = dashou_profile.yajin
|
||||||
|
Xiugaijilu.query.create(
|
||||||
|
yonghuid=dashou_id,
|
||||||
|
xiugaiid=kefu.user.Phone,
|
||||||
|
leixing=2,
|
||||||
|
yajin=after_yajin,
|
||||||
|
yyajin=before['yajin'],
|
||||||
|
)
|
||||||
|
return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after_yajin)}})
|
||||||
|
|
||||||
|
# ---------- 积分操作 ----------
|
||||||
|
elif caozuo == 'jian_jifen':
|
||||||
|
if '001cc' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限减打手积分'})
|
||||||
|
jifen = int(value)
|
||||||
|
if dashou_profile.jifen < jifen:
|
||||||
|
return Response({'code': 400, 'msg': '积分不足'})
|
||||||
|
dashou_profile.jifen -= jifen
|
||||||
|
dashou_profile.save()
|
||||||
|
after_jifen = dashou_profile.jifen
|
||||||
|
Xiugaijilu.query.create(
|
||||||
|
yonghuid=dashou_id,
|
||||||
|
xiugaiid=kefu.user.Phone,
|
||||||
|
leixing=2,
|
||||||
|
jifen=after_jifen,
|
||||||
|
yjifen=before['jifen'],
|
||||||
|
)
|
||||||
|
return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after_jifen}})
|
||||||
|
|
||||||
|
elif caozuo == 'jia_jifen':
|
||||||
|
if '001dd' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限加打手积分'})
|
||||||
|
jifen = int(value)
|
||||||
|
dashou_profile.jifen += jifen
|
||||||
|
dashou_profile.save()
|
||||||
|
after_jifen = dashou_profile.jifen
|
||||||
|
Xiugaijilu.query.create(
|
||||||
|
yonghuid=dashou_id,
|
||||||
|
xiugaiid=kefu.user.Phone,
|
||||||
|
leixing=2,
|
||||||
|
jifen=after_jifen,
|
||||||
|
yjifen=before['jifen'],
|
||||||
|
)
|
||||||
|
return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after_jifen}})
|
||||||
|
|
||||||
|
# ---------- 状态修改 ----------
|
||||||
|
elif caozuo == 'gai_zhanghaozhuangtai':
|
||||||
|
if '001ee' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改打手账号状态'})
|
||||||
|
new_status = int(value)
|
||||||
|
if new_status not in [0, 1]:
|
||||||
|
return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'})
|
||||||
|
dashou_profile.zhanghaozhuangtai = new_status
|
||||||
|
dashou_profile.save()
|
||||||
|
Xiugaijilu.query.create(
|
||||||
|
yonghuid=dashou_id,
|
||||||
|
xiugaiid=kefu.user.Phone,
|
||||||
|
leixing=2,
|
||||||
|
qitashuoming=f'修改账号状态为{new_status}',
|
||||||
|
)
|
||||||
|
return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}})
|
||||||
|
|
||||||
|
elif caozuo == 'gai_zaixianzhuangtai':
|
||||||
|
if '001ee' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改打手在线状态'})
|
||||||
|
new_status = int(value)
|
||||||
|
if new_status not in [0, 1]:
|
||||||
|
return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'})
|
||||||
|
dashou_profile.zaixianzhuangtai = new_status
|
||||||
|
dashou_profile.save()
|
||||||
|
Xiugaijilu.query.create(
|
||||||
|
yonghuid=dashou_id,
|
||||||
|
xiugaiid=kefu.user.Phone,
|
||||||
|
leixing=2,
|
||||||
|
qitashuoming=f'修改在线状态为{new_status}',
|
||||||
|
)
|
||||||
|
return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}})
|
||||||
|
|
||||||
|
elif caozuo == 'gai_zhuangtai':
|
||||||
|
if '001ee' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改打手接单状态'})
|
||||||
|
new_status = int(value)
|
||||||
|
if new_status not in [0, 1]:
|
||||||
|
return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'})
|
||||||
|
dashou_profile.zhuangtai = new_status
|
||||||
|
dashou_profile.save()
|
||||||
|
Xiugaijilu.query.create(
|
||||||
|
yonghuid=dashou_id,
|
||||||
|
xiugaiid=kefu.user.Phone,
|
||||||
|
leixing=2,
|
||||||
|
qitashuoming=f'修改接单状态为{new_status}',
|
||||||
|
)
|
||||||
|
return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}})
|
||||||
|
|
||||||
|
# ---------- 会员操作 ----------
|
||||||
|
elif caozuo == 'jia_huiyuan':
|
||||||
|
if '001ff' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限给打手加会员'})
|
||||||
|
if not huiyuan_id:
|
||||||
|
return Response({'code': 400, 'msg': '缺少会员ID'})
|
||||||
|
try:
|
||||||
|
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
|
||||||
|
except Huiyuan.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '会员类型不存在'})
|
||||||
|
from jituan.services.club_user import get_user_club_id
|
||||||
|
from jituan.services.member_recharge import extend_member_entitlement_admin, get_club_huiyuan_row
|
||||||
|
player_club = get_user_club_id(dashou_user)
|
||||||
|
row = get_club_huiyuan_row(player_club, huiyuan_id, require_enabled=False)
|
||||||
|
days = int(value) if value else int(row.formal_days or 30) if row else 30
|
||||||
|
record, err = extend_member_entitlement_admin(
|
||||||
|
dashou_id, huiyuan_id, days, player_club,
|
||||||
|
)
|
||||||
|
if err:
|
||||||
|
return Response({'code': 400, 'msg': err})
|
||||||
|
Xiugaijilu.query.create(
|
||||||
|
yonghuid=dashou_id,
|
||||||
|
xiugaiid=kefu.user.Phone,
|
||||||
|
leixing=2,
|
||||||
|
huiyuants=days,
|
||||||
|
huiyuan_id=huiyuan_id,
|
||||||
|
qitashuoming=f'加正式会员 {huiyuan_id},增加{days}天(无支付单、无分红)',
|
||||||
|
)
|
||||||
|
return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
|
||||||
|
|
||||||
|
elif caozuo == 'jian_huiyuan':
|
||||||
|
if '001gg' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限给打手减会员'})
|
||||||
|
if not huiyuan_id:
|
||||||
|
return Response({'code': 400, 'msg': '缺少会员ID'})
|
||||||
|
try:
|
||||||
|
record = Huiyuangoumai.query.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id)
|
||||||
|
except Huiyuangoumai.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '该打手未购买此会员'})
|
||||||
|
# 删除会员记录(或设置为过期,根据业务要求)
|
||||||
|
record.delete()
|
||||||
|
Xiugaijilu.query.create(
|
||||||
|
yonghuid=dashou_id,
|
||||||
|
xiugaiid=kefu.user.Phone,
|
||||||
|
leixing=2,
|
||||||
|
huiyuan_id=huiyuan_id,
|
||||||
|
qitashuoming=f'移除会员 {huiyuan_id}',
|
||||||
|
)
|
||||||
|
return Response({'code': 0, 'msg': '减会员成功'})
|
||||||
|
|
||||||
|
else:
|
||||||
|
return Response({'code': 400, 'msg': '未知操作类型'})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"修改打手信息异常: {str(e)}", exc_info=True)
|
||||||
|
return Response({'code': 500, 'msg': '服务器内部错误'})
|
||||||
1831
users/views/kefu_orders.py
Normal file
1831
users/views/kefu_orders.py
Normal file
File diff suppressed because it is too large
Load Diff
564
users/views/kefu_punishment.py
Normal file
564
users/views/kefu_punishment.py
Normal file
@@ -0,0 +1,564 @@
|
|||||||
|
"""users.views.kefu_punishment - auto-generated by split script."""
|
||||||
|
"""users.views.kefu - auto-generated by split script."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import requests
|
||||||
|
import hashlib
|
||||||
|
import traceback
|
||||||
|
import decimal
|
||||||
|
import jwt
|
||||||
|
import ipaddress
|
||||||
|
import xmltodict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
import defusedxml.ElementTree as ET
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models, transaction, IntegrityError, DatabaseError, connection
|
||||||
|
from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.views import View
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import permissions, status
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
|
from rest_framework.throttling import AnonRateThrottle
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from utils.money import yuan_to_fen
|
||||||
|
from utils.fadan_utils import check_fadan_qiangdan_eligible
|
||||||
|
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||||
|
from users.fadan_fenhong_utils import process_fadan_fenhong
|
||||||
|
|
||||||
|
from orders.utils import (
|
||||||
|
update_daily_payout,
|
||||||
|
settle_shangjia_order_guanshi_fenhong
|
||||||
|
)
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action, update_guanshi_daily_by_action,
|
||||||
|
update_shangjia_daily, update_zuzhang_daily_by_action,
|
||||||
|
verify_kefu_permission
|
||||||
|
)
|
||||||
|
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
|
||||||
|
|
||||||
|
from ..models import (
|
||||||
|
UserBoss, UserDashou, UserShangjia, UserGuanshi,
|
||||||
|
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
|
||||||
|
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
|
||||||
|
RankingRecord
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
from ..tixian_shenhe_services import (
|
||||||
|
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
|
||||||
|
mark_transfer_success, release_collect_quota_for_record,
|
||||||
|
close_audit_wechat_failed, query_wechat_transfer_bill,
|
||||||
|
check_shijidaozhang_within_limit,
|
||||||
|
check_dashou_trial_blocks_commission_withdraw,
|
||||||
|
WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING,
|
||||||
|
)
|
||||||
|
from ..tixian_shenhe_views import process_audit_collect
|
||||||
|
|
||||||
|
from orders.models import (
|
||||||
|
Order, PlatformOrderExt, MerchantOrderExt, CommissionRate,
|
||||||
|
PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage,
|
||||||
|
Penalty, PenaltyAppealImage
|
||||||
|
)
|
||||||
|
from products.models import (
|
||||||
|
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
|
||||||
|
)
|
||||||
|
from config.models import Qunpeizhi
|
||||||
|
from backend.models import MerchantDailyStats
|
||||||
|
from rank.models import (
|
||||||
|
KaohePayTemp, Chenghao, ShenheJilu,
|
||||||
|
KaoheCishuFeiyong, YonghuChenghao
|
||||||
|
)
|
||||||
|
from users.models import UserShenheguan
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from rank.utils import create_shenhe_jilu_from_temp
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class KefuPunishmentListView(APIView):
|
||||||
|
"""
|
||||||
|
客服获取处罚记录列表接口
|
||||||
|
请求:POST /yonghu/kefu_cfgl
|
||||||
|
参数:{
|
||||||
|
"phone": "客服手机号",
|
||||||
|
"status": [0,3] 或 [1,2], # 状态数组
|
||||||
|
"dingdan_id": "订单ID(可选)",
|
||||||
|
"dashouid": "打手ID(可选)",
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20
|
||||||
|
}
|
||||||
|
认证:JWT
|
||||||
|
返回:{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"list": [处罚记录],
|
||||||
|
"total": 总数,
|
||||||
|
"stats": {
|
||||||
|
"zongshu": 总记录数,
|
||||||
|
"daichuli": 待处理数,
|
||||||
|
"yichuli": 已处理数
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_status_list(raw):
|
||||||
|
"""兼容 status=[0,3]、\"0,3\"、单数字;空/all 表示不过滤。"""
|
||||||
|
if raw is None:
|
||||||
|
return None
|
||||||
|
if isinstance(raw, str):
|
||||||
|
s = raw.strip().lower()
|
||||||
|
if not s or s in ('all', 'null', 'none'):
|
||||||
|
return None
|
||||||
|
if s.startswith('['):
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
raw = json.loads(s)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raw = [x.strip() for x in s.split(',') if x.strip()]
|
||||||
|
if isinstance(raw, (int, float)):
|
||||||
|
return [int(raw)]
|
||||||
|
if isinstance(raw, list):
|
||||||
|
out = []
|
||||||
|
for x in raw:
|
||||||
|
try:
|
||||||
|
out.append(int(x))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return out or None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# 1. 获取参数
|
||||||
|
phone = request.data.get('phone', '').strip()
|
||||||
|
status_list = self._parse_status_list(request.data.get('status'))
|
||||||
|
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||||||
|
dashouid = request.data.get('dashouid', '').strip()
|
||||||
|
try:
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('page_size', 20))
|
||||||
|
except ValueError:
|
||||||
|
return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if not phone:
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 2. 客服身份验证
|
||||||
|
current_user = request.user
|
||||||
|
# 1. 获取前端参数
|
||||||
|
username = request.data.get('phone', '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 400, 'msg': '缺少username'})
|
||||||
|
|
||||||
|
# 2. 公共权限校验
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
# 3. 检查平台订单管理权限(002ab)
|
||||||
|
if '005aa' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
|
||||||
|
|
||||||
|
# 3. 获取统计信息(不分页)
|
||||||
|
from jituan.services.club_penalty import filter_penalty_record_qs
|
||||||
|
from jituan.services.club_user_access import list_response_meta
|
||||||
|
|
||||||
|
base_qs = filter_penalty_record_qs(PenaltyRecord.query.all(), request)
|
||||||
|
agg = base_qs.aggregate(
|
||||||
|
zongshu=Count('id'),
|
||||||
|
daichuli=Count('id', filter=Q(ApplyStatus__in=[0, 3])),
|
||||||
|
yichuli=Count('id', filter=Q(ApplyStatus__in=[1, 2])),
|
||||||
|
)
|
||||||
|
stats = {
|
||||||
|
'zongshu': agg['zongshu'],
|
||||||
|
'daichuli': agg['daichuli'],
|
||||||
|
'yichuli': agg['yichuli'],
|
||||||
|
}
|
||||||
|
|
||||||
|
queryset = base_qs
|
||||||
|
|
||||||
|
if status_list and isinstance(status_list, list):
|
||||||
|
queryset = queryset.filter(ApplyStatus__in=status_list)
|
||||||
|
|
||||||
|
if dingdan_id:
|
||||||
|
queryset = queryset.filter(OrderID=dingdan_id)
|
||||||
|
|
||||||
|
if dashouid:
|
||||||
|
queryset = queryset.filter(PlayerID=dashouid)
|
||||||
|
|
||||||
|
# 5. 分页
|
||||||
|
page_obj = queryset.order_by('-CreateTime').paginate(page=page, per_page=page_size)
|
||||||
|
total = page_obj.total
|
||||||
|
records = page_obj.items
|
||||||
|
|
||||||
|
# 6. 构建返回列表
|
||||||
|
list_data = []
|
||||||
|
for item in records:
|
||||||
|
ct = item.CreateTime
|
||||||
|
ut = item.UpdateTime
|
||||||
|
list_data.append({
|
||||||
|
'dingdan_id': item.OrderID,
|
||||||
|
'dashouid': item.PlayerID,
|
||||||
|
'qingqiuid': item.ApplicantID,
|
||||||
|
'cfliyou': item.PenaltyReason,
|
||||||
|
'sqzhuangtai': item.ApplyStatus,
|
||||||
|
'jifen': item.DeductedPoints,
|
||||||
|
'CreateTime': ct,
|
||||||
|
'UpdateTime': ut,
|
||||||
|
'create_time': ct.isoformat() if ct else '',
|
||||||
|
'update_time': ut.isoformat() if ut else '',
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'list': list_data,
|
||||||
|
'total': total,
|
||||||
|
'stats': stats,
|
||||||
|
**list_response_meta(request),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class KefuPunishmentDetailView(APIView):
|
||||||
|
"""
|
||||||
|
客服获取处罚详情接口
|
||||||
|
请求:POST /yonghu/kefu_cf_detail
|
||||||
|
参数:{
|
||||||
|
"phone": "客服手机号",
|
||||||
|
"dingdan_id": "订单ID"
|
||||||
|
}
|
||||||
|
认证:JWT
|
||||||
|
返回:code=0 + 处罚记录详情(含图片)
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
phone = request.data.get('phone', '').strip()
|
||||||
|
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||||||
|
|
||||||
|
if not phone or not dingdan_id:
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 客服身份验证
|
||||||
|
current_user = request.user
|
||||||
|
if getattr(current_user, 'Phone', '') != phone:
|
||||||
|
logger.warning(f"手机号不匹配: {phone}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
if current_user.UserType not in ('kefu', 'admin'):
|
||||||
|
logger.warning(f"用户类型非客服: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||||||
|
kefu = None
|
||||||
|
if not is_admin:
|
||||||
|
try:
|
||||||
|
kefu = current_user.KefuProfile
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
if kefu.zhuangtai != 1:
|
||||||
|
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 查询处罚记录(按创建时间倒序取最新一条)
|
||||||
|
try:
|
||||||
|
record = PenaltyRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first()
|
||||||
|
if not record:
|
||||||
|
return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"查询处罚记录失败: {e}")
|
||||||
|
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
# 查询商家证据图片
|
||||||
|
zhengju_images = []
|
||||||
|
shensu_images = []
|
||||||
|
try:
|
||||||
|
# 商家证据图片:上传者为请求人 (ApplicantID)
|
||||||
|
zhengju_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.ApplicantID)
|
||||||
|
zhengju_images = [item.ImageURL for item in zhengju_qs if item.ImageURL]
|
||||||
|
|
||||||
|
# 打手申诉图片:上传者为打手 (PlayerID)
|
||||||
|
shensu_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.PlayerID)
|
||||||
|
shensu_images = [item.ImageURL for item in shensu_qs if item.ImageURL]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"查询图片失败: {e}")
|
||||||
|
|
||||||
|
# 构建返回数据
|
||||||
|
data = {
|
||||||
|
'dingdan_id': record.OrderID,
|
||||||
|
'dashouid': record.PlayerID,
|
||||||
|
'qingqiuid': record.ApplicantID,
|
||||||
|
'chuliid': record.ProcessorID,
|
||||||
|
'cfliyou': record.PenaltyReason,
|
||||||
|
'sqzhuangtai': record.ApplyStatus,
|
||||||
|
'bhliyou': record.RejectReason,
|
||||||
|
'jifen': record.DeductedPoints,
|
||||||
|
'ssliyou': record.AppealReason or '',
|
||||||
|
'zhengju_tupian': zhengju_images,
|
||||||
|
'shensu_tupian': shensu_images,
|
||||||
|
'CreateTime': record.CreateTime,
|
||||||
|
'UpdateTime': record.UpdateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response({'code': 0, 'data': data})
|
||||||
|
|
||||||
|
|
||||||
|
class KefuPunishmentActionView(APIView):
|
||||||
|
"""
|
||||||
|
客服同意/驳回处罚接口
|
||||||
|
请求:POST /yonghu/kefu_cf_action
|
||||||
|
参数:{
|
||||||
|
"phone": "客服手机号",
|
||||||
|
"dingdan_id": "订单ID",
|
||||||
|
"action": 1(同意) / 2(驳回),
|
||||||
|
"reject_reason": "驳回理由(驳回时必填)"
|
||||||
|
}
|
||||||
|
认证:JWT
|
||||||
|
返回:code=0 成功
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
phone = request.data.get('phone', '').strip()
|
||||||
|
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||||||
|
action = request.data.get('action')
|
||||||
|
reject_reason = request.data.get('reject_reason', '').strip()
|
||||||
|
|
||||||
|
if not phone or not dingdan_id or action not in [1, 2]:
|
||||||
|
return Response({'code': 401, 'msg': '参数错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if action == 2 and not reject_reason:
|
||||||
|
return Response({'code': 400, 'msg': '驳回理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 客服身份验证(完整验证,此处略)
|
||||||
|
current_user = request.user
|
||||||
|
if getattr(current_user, 'Phone', '') != phone:
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
if current_user.UserType not in ('kefu', 'admin'):
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||||||
|
kefu = None
|
||||||
|
if not is_admin:
|
||||||
|
try:
|
||||||
|
kefu = current_user.KefuProfile
|
||||||
|
if kefu.zhuangtai != 1:
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 查询待处理的处罚记录(状态0或3)
|
||||||
|
try:
|
||||||
|
record = PenaltyRecord.query.filter(
|
||||||
|
OrderID=dingdan_id,
|
||||||
|
ApplyStatus__in=[0, 3]
|
||||||
|
).order_by('-CreateTime').first()
|
||||||
|
if not record:
|
||||||
|
exists = PenaltyRecord.query.filter(OrderID=dingdan_id).exists()
|
||||||
|
if exists:
|
||||||
|
return Response({'code': 400, 'msg': '该处罚记录已处理'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
else:
|
||||||
|
return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"查询处罚记录失败: {e}")
|
||||||
|
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
# 处理重复记录:同一订单的其他待处理/申诉中记录标记为驳回
|
||||||
|
duplicate_records = PenaltyRecord.query.filter(
|
||||||
|
OrderID=dingdan_id,
|
||||||
|
ApplyStatus__in=[0, 3]
|
||||||
|
).exclude(id=record.id)
|
||||||
|
for dup in duplicate_records:
|
||||||
|
dup.ApplyStatus = 2
|
||||||
|
dup.ProcessorID = phone
|
||||||
|
dup.RejectReason = "系统自动驳回:存在重复处罚记录"
|
||||||
|
dup.save()
|
||||||
|
logger.info(f"自动驳回重复处罚记录 ID={dup.id}")
|
||||||
|
|
||||||
|
if action == 1: # 同意
|
||||||
|
# 可选验证打手存在
|
||||||
|
if record.PlayerID:
|
||||||
|
try:
|
||||||
|
dashou = UserDashou.query.get(user__UserUID=record.PlayerID)
|
||||||
|
except UserDashou.DoesNotExist:
|
||||||
|
return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
record.ApplyStatus = 1
|
||||||
|
record.ProcessorID = phone
|
||||||
|
record.save()
|
||||||
|
|
||||||
|
# 更新商家订单扩展表
|
||||||
|
try:
|
||||||
|
dingdan = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id)
|
||||||
|
if hasattr(dingdan, 'shangjia_kuozhan'):
|
||||||
|
dingdan.shangjia_kuozhan.PenaltyApplyStatus = 1
|
||||||
|
dingdan.shangjia_kuozhan.save()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"更新商家扩展表失败: {e}")
|
||||||
|
|
||||||
|
elif action == 2: # 驳回
|
||||||
|
# 返还积分
|
||||||
|
if record.DeductedPoints > 0 and record.PlayerID:
|
||||||
|
try:
|
||||||
|
dashou = UserDashou.query.get(user__UserUID=record.PlayerID)
|
||||||
|
if dashou.jifen <= 1000: # 安全限制
|
||||||
|
dashou.jifen += record.DeductedPoints
|
||||||
|
dashou.save()
|
||||||
|
logger.info(f"驳回处罚,打手{record.PlayerID}返还积分{record.DeductedPoints}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"打手积分异常({dashou.jifen}),不再返还")
|
||||||
|
except UserDashou.DoesNotExist:
|
||||||
|
logger.warning(f"打手{record.PlayerID}不存在,无法返还积分")
|
||||||
|
|
||||||
|
record.ApplyStatus = 2
|
||||||
|
record.ProcessorID = phone
|
||||||
|
record.RejectReason = reject_reason
|
||||||
|
record.save()
|
||||||
|
|
||||||
|
try:
|
||||||
|
dingdan = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id)
|
||||||
|
if hasattr(dingdan, 'shangjia_kuozhan'):
|
||||||
|
dingdan.shangjia_kuozhan.PenaltyApplyStatus = 2
|
||||||
|
dingdan.shangjia_kuozhan.RejectReason = reject_reason
|
||||||
|
dingdan.shangjia_kuozhan.save()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"更新商家扩展表失败: {e}")
|
||||||
|
|
||||||
|
return Response({'code': 0, 'msg': '处理成功'})
|
||||||
|
|
||||||
|
|
||||||
|
class KefuPunishView(APIView):
|
||||||
|
"""
|
||||||
|
客服处罚打手接口
|
||||||
|
请求:POST /yonghu/kefuchufa
|
||||||
|
参数:{
|
||||||
|
"phone": "客服手机号",
|
||||||
|
"dingdan_id": "订单ID",
|
||||||
|
"reason": "处罚原因(可选)"
|
||||||
|
}
|
||||||
|
认证:JWT
|
||||||
|
返回:code=0 成功
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# 1. 获取参数
|
||||||
|
phone = request.data.get('phone', '').strip()
|
||||||
|
dingdan_id = request.data.get('dingdan_id', '').strip()
|
||||||
|
reason = request.data.get('reason', '').strip()
|
||||||
|
|
||||||
|
if not phone or not dingdan_id:
|
||||||
|
return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 2. 客服身份验证
|
||||||
|
current_user = request.user
|
||||||
|
if getattr(current_user, 'Phone', '') != phone:
|
||||||
|
logger.warning(f"手机号不匹配: {phone}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
if current_user.UserType not in ('kefu', 'admin'):
|
||||||
|
logger.warning(f"用户类型非客服: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||||||
|
kefu = None
|
||||||
|
if not is_admin:
|
||||||
|
try:
|
||||||
|
kefu = current_user.KefuProfile
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
if kefu.zhuangtai != 1:
|
||||||
|
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 3. 查询订单
|
||||||
|
try:
|
||||||
|
order = Order.query.get(OrderID=dingdan_id)
|
||||||
|
except Order.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
# 4. 获取打手ID
|
||||||
|
dashou_id = order.PlayerID
|
||||||
|
if not dashou_id:
|
||||||
|
return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 5. 查询打手
|
||||||
|
try:
|
||||||
|
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
|
||||||
|
dashou = dashou_user.DashouProfile
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return Response({'code': 400, 'msg': '该用户不是打手'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 6. 使用事务
|
||||||
|
from jituan.services.club_penalty import resolve_penalty_record_club_id
|
||||||
|
with transaction.atomic():
|
||||||
|
# 创建处罚记录(积分扣除5分)
|
||||||
|
jifen = 5
|
||||||
|
chufa = PenaltyRecord.query.create(
|
||||||
|
PlayerID=dashou_id,
|
||||||
|
ApplicantID=phone, # 客服手机号作为请求人
|
||||||
|
PenaltyReason=reason or '',
|
||||||
|
ApplyStatus=0, # 待处理
|
||||||
|
DeductedPoints=jifen,
|
||||||
|
OrderID=dingdan_id,
|
||||||
|
ClubID=resolve_penalty_record_club_id(
|
||||||
|
order_id=dingdan_id,
|
||||||
|
player_id=dashou_id,
|
||||||
|
request=request,
|
||||||
|
user=request.user,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 扣除打手积分
|
||||||
|
if dashou.jifen >= jifen:
|
||||||
|
dashou.jifen -= jifen
|
||||||
|
else:
|
||||||
|
dashou.jifen = 0
|
||||||
|
dashou.save()
|
||||||
|
|
||||||
|
# 可选:更新订单的 clkf 字段记录处理客服
|
||||||
|
order.AssignedCS = phone
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
return Response({'code': 0, 'msg': '处罚成功'})
|
||||||
786
users/views/kefu_withdraw.py
Normal file
786
users/views/kefu_withdraw.py
Normal file
@@ -0,0 +1,786 @@
|
|||||||
|
"""users.views.kefu_withdraw - auto-generated by split script."""
|
||||||
|
"""users.views.kefu - auto-generated by split script."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import uuid
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import requests
|
||||||
|
import hashlib
|
||||||
|
import traceback
|
||||||
|
import decimal
|
||||||
|
import jwt
|
||||||
|
import ipaddress
|
||||||
|
import xmltodict
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from decimal import Decimal
|
||||||
|
import defusedxml.ElementTree as ET
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models, transaction, IntegrityError, DatabaseError, connection
|
||||||
|
from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.views import View
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import permissions, status
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
|
from rest_framework.throttling import AnonRateThrottle
|
||||||
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from utils.money import yuan_to_fen
|
||||||
|
from utils.fadan_utils import check_fadan_qiangdan_eligible
|
||||||
|
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||||
|
from users.fadan_fenhong_utils import process_fadan_fenhong
|
||||||
|
|
||||||
|
from orders.utils import (
|
||||||
|
update_daily_payout,
|
||||||
|
settle_shangjia_order_guanshi_fenhong
|
||||||
|
)
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action, update_guanshi_daily_by_action,
|
||||||
|
update_shangjia_daily, update_zuzhang_daily_by_action,
|
||||||
|
verify_kefu_permission
|
||||||
|
)
|
||||||
|
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
|
||||||
|
|
||||||
|
from ..models import (
|
||||||
|
UserBoss, UserDashou, UserShangjia, UserGuanshi,
|
||||||
|
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
|
||||||
|
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
|
||||||
|
RankingRecord
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
from ..tixian_shenhe_services import (
|
||||||
|
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
|
||||||
|
mark_transfer_success, release_collect_quota_for_record,
|
||||||
|
close_audit_wechat_failed, query_wechat_transfer_bill,
|
||||||
|
check_shijidaozhang_within_limit,
|
||||||
|
check_dashou_trial_blocks_commission_withdraw,
|
||||||
|
WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING,
|
||||||
|
)
|
||||||
|
from ..tixian_shenhe_views import process_audit_collect
|
||||||
|
|
||||||
|
from orders.models import (
|
||||||
|
Order, PlatformOrderExt, MerchantOrderExt, CommissionRate,
|
||||||
|
PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage,
|
||||||
|
Penalty, PenaltyAppealImage
|
||||||
|
)
|
||||||
|
from products.models import (
|
||||||
|
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
|
||||||
|
)
|
||||||
|
from config.models import Qunpeizhi
|
||||||
|
from backend.models import MerchantDailyStats
|
||||||
|
from rank.models import (
|
||||||
|
KaohePayTemp, Chenghao, ShenheJilu,
|
||||||
|
KaoheCishuFeiyong, YonghuChenghao
|
||||||
|
)
|
||||||
|
from users.models import UserShenheguan
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from rank.utils import create_shenhe_jilu_from_temp
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
kefu_txsh_list_logger = logging.getLogger('yonghu.kefu_txsh_list')
|
||||||
|
|
||||||
|
|
||||||
|
_TIXIAN_LIST_BASE_FIELDS = (
|
||||||
|
'id', 'yonghuid', 'avatar', 'phone', 'nicheng', 'leixing',
|
||||||
|
'zhifu', 'skzhanghao', 'jine', 'zhuangtai', 'fangshi',
|
||||||
|
'shenheid', 'bhliyou', 'CreateTime', 'UpdateTime',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_TIXIAN_LIST_AUDIT_FIELDS = ('shenhe_jilu_id', 'shenhe_danhao')
|
||||||
|
|
||||||
|
|
||||||
|
class KefuWithdrawListView(APIView):
|
||||||
|
"""
|
||||||
|
客服获取提现审核列表接口
|
||||||
|
请求:POST /yonghu/kefu_txsh_list
|
||||||
|
参数:{
|
||||||
|
"phone": "客服手机号",
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20,
|
||||||
|
"status": 1(待审核)/2(已处理),
|
||||||
|
"type": 1(打手佣金)/2(管事分红)/3(组长分红)/4(审核官分佣)/5(打手押金)/6(商家余额), 可选
|
||||||
|
"payment": 1(微信)/2(支付宝) 可选,
|
||||||
|
"search_uid": "用户ID搜索" 可选,
|
||||||
|
"date": "YYYY-MM-DD" 可选,
|
||||||
|
"min_amount": 0, # 最小提现金额(元)可选
|
||||||
|
"max_amount": 999999, # 最大提现金额(元)可选
|
||||||
|
"dakuan_mode": 1 # 打款方式:1手动打款 2自动打款,可选
|
||||||
|
}
|
||||||
|
认证:JWT
|
||||||
|
返回:{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"list": [提现记录],
|
||||||
|
"total": 总数,
|
||||||
|
"stats": {
|
||||||
|
"total": 总记录数,
|
||||||
|
"awaiting": 待审核数,
|
||||||
|
"processed": 已处理数,
|
||||||
|
"total_amount": "总金额(元)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def _safe_request_params(self, request):
|
||||||
|
try:
|
||||||
|
return dict(request.data)
|
||||||
|
except Exception:
|
||||||
|
return {'raw': str(request.data)}
|
||||||
|
|
||||||
|
def _shenhe_jilu_field_available(self):
|
||||||
|
"""探测 shenhe_jilu_id 字段是否已在数据库中就绪"""
|
||||||
|
try:
|
||||||
|
Tixianjilu.query.values_list('shenhe_jilu_id', flat=True).first()
|
||||||
|
return True
|
||||||
|
except DatabaseError as e:
|
||||||
|
kefu_txsh_list_logger.error(
|
||||||
|
'kefu_txsh_list: shenhe_jilu_id 字段不可用,将按旧表结构查询: %s', e,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _list_queryset(self, q, shenhe_field_ok, request):
|
||||||
|
"""字段未迁移时 only 旧字段,避免 SELECT 不存在的列导致 500"""
|
||||||
|
fields = _TIXIAN_LIST_BASE_FIELDS + (
|
||||||
|
_TIXIAN_LIST_AUDIT_FIELDS if shenhe_field_ok else ()
|
||||||
|
)
|
||||||
|
from jituan.services.tixian_club import filter_tixianjilu_by_request
|
||||||
|
qs = Tixianjilu.query.filter(q).only(*fields)
|
||||||
|
return filter_tixianjilu_by_request(qs, request)
|
||||||
|
|
||||||
|
def _resolve_row_dakuan_mode(self, item, audit_mode_map, shenhe_field_ok):
|
||||||
|
"""无关联审核或审核表未标记自动 → 默认手动(1)"""
|
||||||
|
if not shenhe_field_ok:
|
||||||
|
return 1
|
||||||
|
sid = getattr(item, 'shenhe_jilu_id', None)
|
||||||
|
if not sid:
|
||||||
|
return 1
|
||||||
|
if audit_mode_map.get(sid) == 2:
|
||||||
|
return 2
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
from jituan.services.group_finance_gate import require_withdraw_audit_access
|
||||||
|
|
||||||
|
dakuan_mode_filter = request.data.get('dakuan_mode')
|
||||||
|
req_params = self._safe_request_params(request)
|
||||||
|
kefu_txsh_list_logger.info('kefu_txsh_list 请求开始 params=%s', req_params)
|
||||||
|
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('page_size', 20))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if page < 1:
|
||||||
|
page = 1
|
||||||
|
if page_size < 1 or page_size > 100:
|
||||||
|
page_size = min(max(page_size, 1), 100)
|
||||||
|
|
||||||
|
status_filter = request.data.get('status')
|
||||||
|
type_filter = request.data.get('type')
|
||||||
|
payment_filter = request.data.get('payment')
|
||||||
|
search_uid = (request.data.get('search_uid') or '').strip()
|
||||||
|
date_filter = (request.data.get('date') or '').strip()
|
||||||
|
min_amount = request.data.get('min_amount')
|
||||||
|
max_amount = request.data.get('max_amount')
|
||||||
|
|
||||||
|
phone = (request.data.get('phone') or '').strip()
|
||||||
|
if not phone:
|
||||||
|
return Response({'code': 401, 'msg': '缺少客服手机号'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
kefu, err_resp = require_withdraw_audit_access(request, phone)
|
||||||
|
if err_resp:
|
||||||
|
return err_resp
|
||||||
|
|
||||||
|
q = Q()
|
||||||
|
|
||||||
|
try:
|
||||||
|
status_val = int(status_filter) if status_filter is not None and str(status_filter).strip() != '' else None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
status_val = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
dakuan_mode_val = int(dakuan_mode_filter) if dakuan_mode_filter is not None and str(dakuan_mode_filter).strip() != '' else None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
dakuan_mode_val = None
|
||||||
|
|
||||||
|
# 未传打款方式参数 → 默认手动打款(1)
|
||||||
|
effective_dakuan = dakuan_mode_val if dakuan_mode_val in (1, 2) else 1
|
||||||
|
|
||||||
|
if status_val == 1:
|
||||||
|
q &= Q(zhuangtai=1)
|
||||||
|
elif status_val == 2:
|
||||||
|
if effective_dakuan == 2:
|
||||||
|
q &= Q(zhuangtai__in=[2, 3, 4, 5, 6])
|
||||||
|
else:
|
||||||
|
q &= Q(zhuangtai__in=[2, 3])
|
||||||
|
|
||||||
|
if type_filter is not None and str(type_filter).strip() != '':
|
||||||
|
try:
|
||||||
|
type_val = int(type_filter)
|
||||||
|
if type_val in [1, 2, 3, 4, 5, 6]:
|
||||||
|
q &= Q(leixing=type_val)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if payment_filter is not None and str(payment_filter).strip() != '':
|
||||||
|
try:
|
||||||
|
payment_val = int(payment_filter)
|
||||||
|
if payment_val in [1, 2]:
|
||||||
|
q &= Q(fangshi=payment_val)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if search_uid:
|
||||||
|
q &= Q(yonghuid=search_uid)
|
||||||
|
|
||||||
|
if date_filter:
|
||||||
|
q &= Q(UpdateTime__date=date_filter)
|
||||||
|
|
||||||
|
if min_amount is not None and str(min_amount).strip() != '':
|
||||||
|
try:
|
||||||
|
q &= Q(jine__gte=float(min_amount))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if max_amount is not None and str(max_amount).strip() != '':
|
||||||
|
try:
|
||||||
|
q &= Q(jine__lte=float(max_amount))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
shenhe_field_ok = self._shenhe_jilu_field_available()
|
||||||
|
if shenhe_field_ok:
|
||||||
|
if effective_dakuan == 2:
|
||||||
|
q &= Q(shenhe_jilu_id__isnull=False)
|
||||||
|
else:
|
||||||
|
q &= Q(shenhe_jilu_id__isnull=True)
|
||||||
|
elif effective_dakuan == 2:
|
||||||
|
kefu_txsh_list_logger.warning('kefu_txsh_list: 自动打款筛选不可用,返回空列表')
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'list': [],
|
||||||
|
'total': 0,
|
||||||
|
'stats': {'total': 0, 'awaiting': 0, 'processed': 0, 'total_amount': '0.00'},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
processed_statuses = [2, 3, 4, 5, 6] if effective_dakuan == 2 else [2, 3]
|
||||||
|
|
||||||
|
base_qs = self._list_queryset(q, shenhe_field_ok, request)
|
||||||
|
agg = base_qs.aggregate(
|
||||||
|
total=Count('id'),
|
||||||
|
awaiting=Count('id', filter=Q(zhuangtai=1)),
|
||||||
|
processed=Count('id', filter=Q(zhuangtai__in=processed_statuses)),
|
||||||
|
total_amount=Sum('jine'),
|
||||||
|
)
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
'total': agg['total'],
|
||||||
|
'awaiting': agg['awaiting'],
|
||||||
|
'processed': agg['processed'],
|
||||||
|
'total_amount': str(round(float(agg.get('total_amount') or 0), 2)),
|
||||||
|
}
|
||||||
|
|
||||||
|
queryset = base_qs.order_by('CreateTime')
|
||||||
|
total = queryset.count()
|
||||||
|
records = list(queryset[(page - 1) * page_size: page * page_size])
|
||||||
|
|
||||||
|
audit_ids = [
|
||||||
|
getattr(item, 'shenhe_jilu_id', None)
|
||||||
|
for item in records if getattr(item, 'shenhe_jilu_id', None)
|
||||||
|
]
|
||||||
|
audit_meta_map = {}
|
||||||
|
if audit_ids:
|
||||||
|
try:
|
||||||
|
audit_meta_map = load_audit_meta_map(audit_ids)
|
||||||
|
except Exception as e:
|
||||||
|
kefu_txsh_list_logger.error(
|
||||||
|
'kefu_txsh_list: 查询审核表信息失败: %s', e,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
audit_mode_map = {
|
||||||
|
aid: meta.get('dakuan_mode', 2) for aid, meta in audit_meta_map.items()
|
||||||
|
}
|
||||||
|
list_data = []
|
||||||
|
for item in records:
|
||||||
|
shenhe_jilu_id = getattr(item, 'shenhe_jilu_id', None) if shenhe_field_ok else None
|
||||||
|
shenhe_danhao = (getattr(item, 'shenhe_danhao', None) or '') if shenhe_field_ok else ''
|
||||||
|
if shenhe_jilu_id and not shenhe_danhao:
|
||||||
|
shenhe_danhao = audit_meta_map.get(shenhe_jilu_id, {}).get('shenhe_danhao', '')
|
||||||
|
list_data.append({
|
||||||
|
'id': item.id,
|
||||||
|
'shenhe_jilu_id': shenhe_jilu_id,
|
||||||
|
'shenhe_danhao': shenhe_danhao,
|
||||||
|
'yonghuid': item.yonghuid,
|
||||||
|
'avatar': item.avatar or '',
|
||||||
|
'phone': item.phone or '',
|
||||||
|
'nicheng': item.nicheng or '',
|
||||||
|
'leixing': item.leixing,
|
||||||
|
'zhifu': item.zhifu or '',
|
||||||
|
'skzhanghao': item.skzhanghao or '',
|
||||||
|
'jine': str(item.jine) if item.jine is not None else '0.00',
|
||||||
|
'zhuangtai': item.zhuangtai,
|
||||||
|
'fangshi': item.fangshi,
|
||||||
|
'dakuan_mode': self._resolve_row_dakuan_mode(item, audit_mode_map, shenhe_field_ok),
|
||||||
|
'shenheid': item.shenheid or '',
|
||||||
|
'bhliyou': item.bhliyou or '',
|
||||||
|
'CreateTime': item.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if item.CreateTime else '',
|
||||||
|
'UpdateTime': item.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if item.UpdateTime else '',
|
||||||
|
})
|
||||||
|
|
||||||
|
from jituan.services.club_user_access import list_response_meta
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'list': list_data,
|
||||||
|
'total': total,
|
||||||
|
'stats': stats,
|
||||||
|
**list_response_meta(request),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
except DatabaseError as e:
|
||||||
|
kefu_txsh_list_logger.error(
|
||||||
|
'kefu_txsh_list 数据库错误 dakuan_mode=%s params=%s err=%s',
|
||||||
|
dakuan_mode_filter, req_params, e,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'msg': '数据库查询失败,请确认已执行 python manage.py migrate yonghu',
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
except Exception as e:
|
||||||
|
kefu_txsh_list_logger.error(
|
||||||
|
'kefu_txsh_list 接口异常 dakuan_mode=%s params=%s err=%s',
|
||||||
|
dakuan_mode_filter, req_params, e,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'msg': '获取提现列表失败',
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
class KefuWithdrawDetailView(APIView):
|
||||||
|
"""
|
||||||
|
客服获取提现详情接口
|
||||||
|
请求:POST /yonghu/kefu_txsh_detail
|
||||||
|
参数:{
|
||||||
|
"phone": "客服手机号",
|
||||||
|
"tixian_id": "提现记录ID"
|
||||||
|
}
|
||||||
|
认证:JWT
|
||||||
|
返回:code=0 + 提现记录详情 + 用户扩展信息
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
phone = request.data.get('phone', '').strip()
|
||||||
|
tixian_id = request.data.get('tixian_id')
|
||||||
|
|
||||||
|
if not phone or not tixian_id:
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 客服身份验证
|
||||||
|
current_user = request.user
|
||||||
|
if getattr(current_user, 'Phone', '') != phone:
|
||||||
|
logger.warning(f"手机号不匹配: {phone}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
if current_user.UserType not in ('kefu', 'admin'):
|
||||||
|
logger.warning(f"用户类型非客服: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||||||
|
kefu = None
|
||||||
|
if not is_admin:
|
||||||
|
try:
|
||||||
|
kefu = current_user.KefuProfile
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
if kefu.zhuangtai != 1:
|
||||||
|
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
|
||||||
|
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
from jituan.services.group_finance_gate import require_withdraw_audit_access
|
||||||
|
_, err_resp = require_withdraw_audit_access(request, phone)
|
||||||
|
if err_resp:
|
||||||
|
return err_resp
|
||||||
|
|
||||||
|
# 查询提现记录
|
||||||
|
try:
|
||||||
|
record = Tixianjilu.query.get(id=tixian_id)
|
||||||
|
except Tixianjilu.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
|
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
|
||||||
|
deny = forbid_if_tixian_out_of_scope(request, record)
|
||||||
|
if deny:
|
||||||
|
return deny
|
||||||
|
|
||||||
|
# 查询用户主表
|
||||||
|
try:
|
||||||
|
user = User.query.get(UserUID=record.yonghuid)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
user = None
|
||||||
|
|
||||||
|
# 构建基础返回数据(字段必须与前端一致)
|
||||||
|
data = {
|
||||||
|
'id': record.id,
|
||||||
|
'yonghuid': record.yonghuid,
|
||||||
|
'avatar': record.avatar or '',
|
||||||
|
'phone': record.phone or '',
|
||||||
|
'nicheng': record.nicheng or '',
|
||||||
|
'leixing': record.leixing, # 1打手 2管事
|
||||||
|
'zhifu': record.zhifu or '',
|
||||||
|
'skzhanghao': record.skzhanghao or '',
|
||||||
|
'jine': str(record.jine) if record.jine else '0.00',
|
||||||
|
'zhuangtai': record.zhuangtai,
|
||||||
|
'fangshi': record.fangshi,
|
||||||
|
'shenheid': record.shenheid or '',
|
||||||
|
'bhliyou': record.bhliyou or '',
|
||||||
|
'CreateTime': record.CreateTime,
|
||||||
|
'UpdateTime': record.UpdateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 根据提现类型添加扩展信息
|
||||||
|
if user:
|
||||||
|
if record.leixing == 1: # 打手
|
||||||
|
try:
|
||||||
|
dashou = UserDashou.query.get(user=user)
|
||||||
|
# 计算成交率和退款率
|
||||||
|
total = dashou.jiedanzongliang or 0
|
||||||
|
completed = dashou.chengjiaozongliang or 0
|
||||||
|
refund = dashou.tuikuanliang or 0
|
||||||
|
completion_rate = round((completed / total * 100) if total > 0 else 0)
|
||||||
|
refund_rate = round((refund / completed * 100) if completed > 0 else 0)
|
||||||
|
|
||||||
|
data['dashouInfo'] = {
|
||||||
|
'zhuangtai': dashou.zhuangtai, # 工作状态
|
||||||
|
'zaixianzhuangtai': dashou.zaixianzhuangtai, # 在线状态
|
||||||
|
'zhanghaozhuangtai': dashou.zhanghaozhuangtai, # 账号状态
|
||||||
|
'yue': float(dashou.yue) if dashou.yue else 0.00, # 可提现余额
|
||||||
|
'zonge': float(dashou.zonge) if dashou.zonge else 0.00, # 接单总额
|
||||||
|
'jifen': dashou.jifen, # 积分
|
||||||
|
'jiedanzongliang': dashou.jiedanzongliang, # 接单总量
|
||||||
|
'chengjiaozongliang': dashou.chengjiaozongliang, # 成交总量
|
||||||
|
'tuikuanliang': dashou.tuikuanliang, # 退款总量
|
||||||
|
'completionRate': completion_rate,
|
||||||
|
'refundRate': refund_rate,
|
||||||
|
}
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
data['dashouInfo'] = None
|
||||||
|
elif record.leixing == 2: # 管事
|
||||||
|
try:
|
||||||
|
guanshi = UserGuanshi.query.get(user=user)
|
||||||
|
data['guanshiInfo'] = {
|
||||||
|
'zhuangtai': guanshi.zhuangtai, # 账号状态
|
||||||
|
'yue': float(guanshi.yue) if guanshi.yue else 0.00,
|
||||||
|
'yaogingshuliang': guanshi.yaogingshuliang, # 邀请打手总数
|
||||||
|
'jinrichongzhi': guanshi.jinrichongzhi, # 今日充值
|
||||||
|
'jinyuechongzhi': guanshi.jinyuechongzhi, # 今月充值
|
||||||
|
'chongzhifenrun': float(guanshi.chongzhifenrun) if guanshi.chongzhifenrun else 0.00,
|
||||||
|
}
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
data['guanshiInfo'] = None
|
||||||
|
|
||||||
|
return Response({'code': 0, 'data': data})
|
||||||
|
|
||||||
|
|
||||||
|
class KefuWithdrawActionView(APIView):
|
||||||
|
"""
|
||||||
|
客服处理提现接口(同意/拒绝)—— 仅此接口处理后台审核打款
|
||||||
|
POST /yonghu/kefu_txsh_action
|
||||||
|
|
||||||
|
单条:{ phone, action, tixian_id, yonghuid, leixing, reason? }
|
||||||
|
批量:{ phone, action, batch_list: [{tixian_id, yonghuid, leixing}, ...], reason? }
|
||||||
|
|
||||||
|
自动打款(dakuan_mode=2):同意→6待收款;拒绝→5+驳回原因+退还可到账金额(shijidaozhang),手续费不退,不更新平台收支/每日统计
|
||||||
|
手动打款(dakuan_mode=1):原逻辑不变
|
||||||
|
状态要求:Tixianjilu.zhuangtai=1 且 TixianShenheJilu.zhuangtai=1(审核中),其他状态一律拒绝
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def _verify_kefu(self, request, phone):
|
||||||
|
if not phone:
|
||||||
|
return None, Response({'code': 401, 'msg': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
current_user = request.user
|
||||||
|
if getattr(current_user, 'Phone', '') != phone:
|
||||||
|
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
if current_user.UserType not in ('kefu', 'admin'):
|
||||||
|
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
# 管理员跳过客服扩展表检查
|
||||||
|
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
|
||||||
|
if not is_admin:
|
||||||
|
try:
|
||||||
|
kefu = current_user.KefuProfile
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
if kefu.zhuangtai != 1:
|
||||||
|
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
return current_user, None
|
||||||
|
|
||||||
|
def _parse_items(self, request):
|
||||||
|
"""解析单条或 batch_list,返回 [{tixian_id, yonghuid, leixing}]"""
|
||||||
|
batch_list = request.data.get('batch_list')
|
||||||
|
if batch_list is not None:
|
||||||
|
if not isinstance(batch_list, list) or len(batch_list) == 0:
|
||||||
|
return None, Response({'code': 400, 'msg': 'batch_list 不能为空'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
items = []
|
||||||
|
for idx, row in enumerate(batch_list):
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 格式错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
tid = row.get('tixian_id')
|
||||||
|
yid = (row.get('yonghuid') or '').strip()
|
||||||
|
lx = row.get('leixing')
|
||||||
|
if not tid or not yid or lx is None:
|
||||||
|
return None, Response(
|
||||||
|
{'code': 400, 'msg': f'batch_list[{idx}] 缺少 tixian_id / yonghuid / leixing'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
items.append({
|
||||||
|
'tixian_id': int(tid),
|
||||||
|
'yonghuid': yid,
|
||||||
|
'leixing': int(lx),
|
||||||
|
})
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
return items, None
|
||||||
|
|
||||||
|
tixian_id = request.data.get('tixian_id')
|
||||||
|
if not tixian_id:
|
||||||
|
return None, Response({'code': 400, 'msg': '提现记录ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
yonghuid = (request.data.get('yonghuid') or '').strip()
|
||||||
|
leixing = request.data.get('leixing')
|
||||||
|
try:
|
||||||
|
item = {'tixian_id': int(tixian_id), 'yonghuid': yonghuid, 'leixing': int(leixing) if leixing is not None else None}
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None, Response({'code': 400, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
return [item], None
|
||||||
|
|
||||||
|
def _process_auto_item(self, request, item, action, reason, kefu_user):
|
||||||
|
"""自动打款单条处理(须在 transaction.atomic 内调用)"""
|
||||||
|
tixian_id = item['tixian_id']
|
||||||
|
req_yonghuid = item['yonghuid']
|
||||||
|
req_leixing = item['leixing']
|
||||||
|
|
||||||
|
if req_leixing not in [1, 2, 3, 4, 5, 6]:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:提现类型无效')
|
||||||
|
|
||||||
|
try:
|
||||||
|
tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
|
||||||
|
except Tixianjilu.DoesNotExist:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}不存在')
|
||||||
|
|
||||||
|
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
|
||||||
|
deny = forbid_if_tixian_out_of_scope(request, tixian)
|
||||||
|
if deny:
|
||||||
|
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
|
||||||
|
|
||||||
|
if tixian.yonghuid != req_yonghuid:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:用户ID不匹配')
|
||||||
|
if tixian.leixing != req_leixing:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
|
||||||
|
if tixian.zhuangtai != 1:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
|
||||||
|
if not tixian.shenhe_jilu_id:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:非自动打款审核记录')
|
||||||
|
|
||||||
|
try:
|
||||||
|
audit = TixianShenheJilu.objects.select_for_update().get(id=tixian.shenhe_jilu_id)
|
||||||
|
except TixianShenheJilu.DoesNotExist:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:审核记录不存在')
|
||||||
|
|
||||||
|
if audit.yonghuid != req_yonghuid:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:审核记录用户ID不匹配')
|
||||||
|
if audit.leixing != req_leixing:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:审核记录提现类型不匹配')
|
||||||
|
if audit.zhuangtai != 1:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:审核记录非审核中状态')
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.objects.select_for_update().get(UserUID=req_yonghuid)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
raise ValueError(f'用户{req_yonghuid}不存在')
|
||||||
|
|
||||||
|
if action == 1:
|
||||||
|
limit_ok, limit_msg = check_shijidaozhang_within_limit(audit.shijidaozhang)
|
||||||
|
if not limit_ok:
|
||||||
|
sync_audit_and_jilu_status(
|
||||||
|
audit, 5,
|
||||||
|
bhliyou=limit_msg,
|
||||||
|
bo_hui_ren_id=kefu_user.UserUID,
|
||||||
|
)
|
||||||
|
refund_balance(user, audit.leixing, audit.shijidaozhang)
|
||||||
|
tixian.shenheid = kefu_user.UserUID
|
||||||
|
tixian.bhliyou = limit_msg
|
||||||
|
tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:{limit_msg},已自动驳回并退款')
|
||||||
|
sync_audit_and_jilu_status(audit, 6, shenhe_ren_id=kefu_user.UserUID)
|
||||||
|
tixian.shenheid = kefu_user.UserUID
|
||||||
|
tixian.save(update_fields=['shenheid', 'UpdateTime'])
|
||||||
|
else:
|
||||||
|
sync_audit_and_jilu_status(
|
||||||
|
audit, 5,
|
||||||
|
bhliyou=reason,
|
||||||
|
bo_hui_ren_id=kefu_user.UserUID,
|
||||||
|
)
|
||||||
|
# 退还可到账金额,申请时扣的 shenqing_jine 中的手续费(shouxufei)不退
|
||||||
|
refund_balance(user, audit.leixing, audit.shijidaozhang)
|
||||||
|
tixian.shenheid = kefu_user.UserUID
|
||||||
|
tixian.bhliyou = reason
|
||||||
|
tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
|
||||||
|
|
||||||
|
def _process_manual_item(self, request, item, action, reason, phone):
|
||||||
|
"""手动打款单条处理(原逻辑,须在 transaction.atomic 内调用)"""
|
||||||
|
tixian_id = item['tixian_id']
|
||||||
|
try:
|
||||||
|
tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
|
||||||
|
except Tixianjilu.DoesNotExist:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}不存在')
|
||||||
|
|
||||||
|
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
|
||||||
|
deny = forbid_if_tixian_out_of_scope(request, tixian)
|
||||||
|
if deny:
|
||||||
|
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
|
||||||
|
|
||||||
|
req_yonghuid = item.get('yonghuid')
|
||||||
|
req_leixing = item.get('leixing')
|
||||||
|
if req_yonghuid and tixian.yonghuid != req_yonghuid:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:用户ID不匹配')
|
||||||
|
if req_leixing is not None and tixian.leixing != req_leixing:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
|
||||||
|
if tixian.zhuangtai != 1:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
|
||||||
|
|
||||||
|
if getattr(tixian, 'shenhe_jilu_id', None):
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:自动打款请走自动流程')
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.query.get(UserUID=tixian.yonghuid)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
raise ValueError(f'用户{tixian.yonghuid}不存在')
|
||||||
|
|
||||||
|
from jituan.services.club_user import get_user_club_id
|
||||||
|
|
||||||
|
if action == 1:
|
||||||
|
tixian.zhuangtai = 2
|
||||||
|
try:
|
||||||
|
from jituan.services.szjilu_accounting import apply_szjilu_expense
|
||||||
|
update_daily_payout(tixian.jine, get_user_club_id(user))
|
||||||
|
apply_szjilu_expense(tixian.jine, get_user_club_id(user))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'更新收支记录失败: {e}')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
refund_balance(user, tixian.leixing, tixian.jine)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(str(e))
|
||||||
|
tixian.zhuangtai = 3
|
||||||
|
tixian.bhliyou = reason
|
||||||
|
|
||||||
|
tixian.shenheid = phone
|
||||||
|
tixian.UpdateTime = timezone.now()
|
||||||
|
tixian.save()
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
logger.info(f'接收到提现处理请求,数据: {request.data}')
|
||||||
|
|
||||||
|
phone = request.data.get('phone', '').strip()
|
||||||
|
action_raw = request.data.get('action')
|
||||||
|
reason = request.data.get('reason', '').strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
action = int(action_raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return Response({'code': 400, 'msg': 'action 参数必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
if action not in [1, 2]:
|
||||||
|
return Response({'code': 400, 'msg': 'action 必须为1(同意)或2(拒绝)'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
if action == 2 and not reason:
|
||||||
|
return Response({'code': 400, 'msg': '拒绝提现时理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
kefu_user, err_resp = self._verify_kefu(request, phone)
|
||||||
|
if err_resp:
|
||||||
|
return err_resp
|
||||||
|
|
||||||
|
from jituan.services.group_finance_gate import require_withdraw_audit_access
|
||||||
|
_, audit_err = require_withdraw_audit_access(request, phone)
|
||||||
|
if audit_err:
|
||||||
|
return audit_err
|
||||||
|
|
||||||
|
items, err_resp = self._parse_items(request)
|
||||||
|
if err_resp:
|
||||||
|
return err_resp
|
||||||
|
|
||||||
|
is_batch = isinstance(request.data.get('batch_list'), list) and len(request.data.get('batch_list')) > 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
for item in items:
|
||||||
|
tixian_id = item['tixian_id']
|
||||||
|
try:
|
||||||
|
tixian_probe = Tixianjilu.objects.select_for_update().get(id=tixian_id)
|
||||||
|
except Tixianjilu.DoesNotExist:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}不存在')
|
||||||
|
|
||||||
|
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
|
||||||
|
deny = forbid_if_tixian_out_of_scope(request, tixian_probe)
|
||||||
|
if deny:
|
||||||
|
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
|
||||||
|
|
||||||
|
# 有关联审核记录(shenhe_jilu_id)即为 zddksh 自动打款,不依赖 dakuan_mode 字段
|
||||||
|
is_auto = bool(getattr(tixian_probe, 'shenhe_jilu_id', None))
|
||||||
|
|
||||||
|
if is_auto:
|
||||||
|
self._process_auto_item(request, item, action, reason, kefu_user)
|
||||||
|
else:
|
||||||
|
if is_batch:
|
||||||
|
raise ValueError(f'提现记录{tixian_id}:批量操作仅支持自动打款记录')
|
||||||
|
self._process_manual_item(request, item, action, reason, phone)
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
return Response({'code': 400, 'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'处理提现事务失败: {e}', exc_info=True)
|
||||||
|
return Response({'code': 500, 'msg': f'处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
msg = '批量处理成功' if is_batch and len(items) > 1 else '处理成功'
|
||||||
|
return Response({'code': 0, 'msg': msg, 'data': {'count': len(items)}})
|
||||||
@@ -719,13 +719,15 @@ class FaKuanShenSuView(APIView):
|
|||||||
PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=2).delete()
|
PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=2).delete()
|
||||||
|
|
||||||
# 插入新上传的申诉图片
|
# 插入新上传的申诉图片
|
||||||
for url in tupian_urls:
|
PenaltyAppealImage.objects.bulk_create([
|
||||||
PenaltyAppealImage.query.create(
|
PenaltyAppealImage(
|
||||||
Penalty=fadan,
|
Penalty=fadan,
|
||||||
PenalizedUserID=yonghuid,
|
PenalizedUserID=yonghuid,
|
||||||
ImageURL=url,
|
ImageURL=url,
|
||||||
Purpose=2
|
Purpose=2,
|
||||||
)
|
)
|
||||||
|
for url in tupian_urls
|
||||||
|
])
|
||||||
|
|
||||||
return Response({'code': 0, 'msg': '申诉提交成功'})
|
return Response({'code': 0, 'msg': '申诉提交成功'})
|
||||||
|
|
||||||
|
|||||||
@@ -1052,7 +1052,9 @@ class TixianAssetView(APIView):
|
|||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
user = request.user
|
user = User.query.select_related(
|
||||||
|
'DashouProfile', 'ShopProfile', 'GuanshiProfile', 'ZuzhangProfile', 'ShenheguanProfile'
|
||||||
|
).get(UserUID=request.user.UserUID)
|
||||||
# 初始化返回数据,默认全部为0
|
# 初始化返回数据,默认全部为0
|
||||||
data = {
|
data = {
|
||||||
'dashou_yue': '0.00',
|
'dashou_yue': '0.00',
|
||||||
|
|||||||
Reference in New Issue
Block a user