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

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

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

View File

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

View File

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

View File

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