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

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

View File

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

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,