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

@@ -268,35 +268,98 @@ class DashouHuiyuanList(APIView):
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 仅返回本俱乐部可售会员club_huiyuan_price 已配置且启用)
from jituan.services.member_recharge import club_huiyuan_sellable, can_purchase_trial
club_id = resolve_effective_club_id(request, request.user)
huiyuan_queryset = Huiyuan.query.all().only(
'huiyuan_id', 'jieshao', 'jtjieshao', 'jiage',
).order_by('jiage')
from jituan.constants import CLUB_ID_DEFAULT
from jituan.models import ClubHuiyuanPrice
club_rows = {
r.huiyuan_id: r
for r in ClubHuiyuanPrice.query.filter(club_id=club_id, is_enabled=True)
}
from decimal import Decimal as _Decimal
club_id = resolve_effective_club_id(request, request.user) or CLUB_ID_DEFAULT
huiyuan_queryset = list(Huiyuan.query.all().only(
'huiyuan_id', 'jieshao', 'jtjieshao', 'jiage',
).order_by('jiage'))
yonghuid = request.user.UserUID
_huiyuan_ids = [h.huiyuan_id for h in huiyuan_queryset]
# 批量预取 1本俱乐部所有 ClubHuiyuanPrice启用按 huiyuan_id 索引
_club_price_map = {
cp.huiyuan_id: cp
for cp in ClubHuiyuanPrice.query.filter(
club_id=club_id,
huiyuan_id__in=_huiyuan_ids,
is_enabled=True,
)
} if _huiyuan_ids else {}
# 批量预取 2当前用户已购 Huiyuangoumai按 huiyuan_id 索引
_goumai_map = {
gm.huiyuan_id: gm
for gm in Huiyuangoumai.query.filter(
yonghu_id=yonghuid,
huiyuan_id__in=_huiyuan_ids,
)
} if _huiyuan_ids else {}
# 批量预取 3当前用户在本俱乐部已支付正式会员的 huiyuan_id 集合
_formal_paid_set = set(
Czjilu.query.filter(
yonghuid=yonghuid,
huiyuan_id__in=_huiyuan_ids,
leixing=1,
zhuangtai=3,
club_id=club_id,
is_trial=False,
).values_list('huiyuan_id', flat=True)
) if _huiyuan_ids else set()
def _local_sellable(cp_row, is_trial):
"""单行 ClubHuiyuanPrice 计算 sellable/price/days避免重复查询"""
if not cp_row:
return False, _Decimal('0'), 0
if is_trial:
if not cp_row.trial_enabled:
return False, _Decimal('0'), 0
price = _Decimal(str(cp_row.trial_price or 0))
days = int(cp_row.trial_days or 0)
if price <= 0 or days <= 0:
return False, _Decimal('0'), 0
return True, price, days
price = _Decimal(str(cp_row.jiage or 0))
days = int(cp_row.formal_days or 30)
if price <= 0:
return False, _Decimal('0'), 0
return True, price, days
def _local_can_trial(huiyuan_id):
"""基于已预取的数据判断体验会员是否可购。"""
cp_row = _club_price_map.get(huiyuan_id)
trial_sellable, _, _ = _local_sellable(cp_row, is_trial=True)
if not trial_sellable:
return False
goumai = _goumai_map.get(huiyuan_id)
if goumai and goumai.has_used_trial:
return False
if huiyuan_id in _formal_paid_set:
return False
return True
huiyuan_list = []
yonghuid = request.user.UserUID
for huiyuan in huiyuan_queryset:
sellable, club_price, formal_days = club_huiyuan_sellable(
club_id, huiyuan.huiyuan_id, is_trial=False,
)
cp_row = _club_price_map.get(huiyuan.huiyuan_id)
sellable, club_price, formal_days = _local_sellable(cp_row, is_trial=False)
if not sellable or not club_price:
continue
trial_ok, _ = can_purchase_trial(yonghuid, huiyuan.huiyuan_id, club_id)
trial_sellable, trial_price, trial_days = club_huiyuan_sellable(
club_id, huiyuan.huiyuan_id, is_trial=True,
)
club_row = club_rows.get(huiyuan.huiyuan_id)
card_image = (club_row.card_image or '').strip() if club_row else ''
card_bg = (club_row.card_bg or '').strip() if club_row else ''
trial_ok = _local_can_trial(huiyuan.huiyuan_id)
trial_sellable, trial_price, trial_days = _local_sellable(cp_row, is_trial=True)
# 远程新增card_image / card_bg / is_bundle 字段
card_image = (cp_row.card_image or '').strip() if cp_row else ''
card_bg = (cp_row.card_bg or '').strip() if cp_row else ''
is_bundle = bool(getattr(huiyuan, 'is_bundle', False))
# 总会员is_bundle禁用体验会员
if is_bundle:
trial_sellable = False
trial_price = _Decimal('0')
trial_days = 0
trial_ok = False
huiyuan_list.append({
'id': huiyuan.huiyuan_id,
'mingzi': huiyuan.jieshao,
@@ -304,10 +367,10 @@ class DashouHuiyuanList(APIView):
'formal_days': formal_days,
'jieshao': huiyuan.jtjieshao,
'is_bundle': is_bundle,
'trial_enabled': False if is_bundle else bool(trial_sellable),
'trial_price': '0.00' if is_bundle else (format_yuan(trial_price) if trial_sellable else '0.00'),
'trial_days': 0 if is_bundle else (trial_days if trial_sellable else 0),
'can_buy_trial': False if is_bundle else bool(trial_ok and trial_sellable),
'trial_enabled': bool(trial_sellable),
'trial_price': format_yuan(trial_price) if trial_sellable else '0.00',
'trial_days': trial_days if trial_sellable else 0,
'can_buy_trial': bool(trial_ok and trial_sellable),
'card_image': card_image,
'card_bg': card_bg,
})
@@ -475,20 +538,21 @@ class AdGetAllDataView(APIView):
# 6. 查询商品类型数据
try:
# 使用select_related或prefetch_related优化查询如果需要关联查询
# 这里直接查询所有字段,然后提取需要的字段
shangpinleixing_queryset = ShangpinLeixing.query.all()
# 优化:使用 .values() 直接提取字段,避免模型实例化开销(字典表 <10 条
shangpinleixing_rows = ShangpinLeixing.query.all().values(
'id', 'yaoqiuleixing', 'huiyuan_id', 'yongjin', 'jieshao', 'tupian_url'
)
# 转换为前端需要的格式
shangpinleixing_list = []
for leixing in shangpinleixing_queryset:
for row in shangpinleixing_rows:
leixing_data = {
'id': leixing.id,
'yaoqiuleixing': leixing.yaoqiuleixing if leixing.yaoqiuleixing is not None else None,
'huiyuan_id': leixing.huiyuan_id if leixing.huiyuan_id else None,
'yongjin': str(leixing.yongjin) if leixing.yongjin is not None else None,
'jieshao': leixing.jieshao if leixing.jieshao else '',
'tupian_url': leixing.tupian_url if leixing.tupian_url else ''
'id': row['id'],
'yaoqiuleixing': row['yaoqiuleixing'],
'huiyuan_id': row['huiyuan_id'] or None,
'yongjin': str(row['yongjin']) if row['yongjin'] is not None else None,
'jieshao': row['jieshao'] or '',
'tupian_url': row['tupian_url'] or ''
}
shangpinleixing_list.append(leixing_data)
@@ -500,16 +564,16 @@ class AdGetAllDataView(APIView):
# 7. 查询商品专区数据
try:
# 查询所有商品专区
zhuanqu_queryset = ShangpinZhuanqu.query.all()
# 优化:使用 .values() 直接提取字段(字典表 <50 条)
zhuanqu_rows = ShangpinZhuanqu.query.all().values('id', 'mingzi', 'leixing_id')
# 转换为前端需要的格式
zhuanqu_list = []
for zhuanqu in zhuanqu_queryset:
for row in zhuanqu_rows:
zhuanqu_data = {
'id': zhuanqu.id,
'mingzi': zhuanqu.mingzi if zhuanqu.mingzi else '',
'leixing_id': zhuanqu.leixing_id
'id': row['id'],
'mingzi': row['mingzi'] or '',
'leixing_id': row['leixing_id']
}
zhuanqu_list.append(zhuanqu_data)
@@ -593,17 +657,20 @@ class HuiyuanTypeListView(APIView):
}, status=status.HTTP_401_UNAUTHORIZED)
# 🔥 2. 获取所有会员类型
huiyuan_list = Huiyuan.query.all().order_by('CreateTime')
# 优化:使用 .values() 直接提取字段,避免模型实例化开销
huiyuan_rows = Huiyuan.query.all().order_by('CreateTime').values(
'huiyuan_id', 'jieshao', 'jiage', 'goumai_cishu', 'CreateTime'
)
# 🔥 3. 构建返回数据
huiyuan_data = []
for huiyuan in huiyuan_list:
for row in huiyuan_rows:
huiyuan_data.append({
'id': huiyuan.huiyuan_id, # 会员ID
'jieshao': huiyuan.jieshao, # 会员介绍
'jiage': str(huiyuan.jiage), # 价格
'goumai_cishu': huiyuan.goumai_cishu, # 购买次数
'CreateTime': huiyuan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if huiyuan.CreateTime else None
'id': row['huiyuan_id'], # 会员ID
'jieshao': row['jieshao'], # 会员介绍
'jiage': str(row['jiage']), # 价格
'goumai_cishu': row['goumai_cishu'], # 购买次数
'CreateTime': row['CreateTime'].strftime('%Y-%m-%d %H:%M:%S') if row['CreateTime'] else None
})
return Response({
@@ -757,34 +824,40 @@ class HuiyuanRecordListView(APIView):
has_more = (page * pagesize) < total_count
# 🔥 9. 获取会员类型统计信息(所有会员类型的当前状态总数)
huiyuan_types = Huiyuan.query.all()
huiyuan_stats = []
for huiyuan in huiyuan_types:
# 使用COUNT查询统计
stats_sql = """
SELECT COUNT(*)
FROM huiyuangoumai
WHERE huiyuan_id = %s
"""
stats_params = [huiyuan.huiyuan_id]
if jiluzhuangtai == 1: # 未过期
stats_sql += " AND daoqi_time > %s"
stats_params.append(now)
else: # 已过期
stats_sql += " AND daoqi_time <= %s"
stats_params.append(now)
with connection.cursor() as cursor:
cursor.execute(stats_sql, stats_params)
count = cursor.fetchone()[0]
huiyuan_stats.append({
'id': huiyuan.huiyuan_id,
'jieshao': huiyuan.jieshao,
'count': count
})
# 优化:原实现为 N+1 查询(外层遍历 Huiyuan 全表 + 内层每条记录执行一次 COUNT SQL
# 改为单条 LEFT JOIN + GROUP BY 聚合,将 N+1 次查询降为 1 次
# 注意Huiyuangoumai.huiyuan_id 是 CharField 而非 ForeignKey无法用 ORM annotate
stats_sql = """
SELECT
h.huiyuan_id,
h.jieshao,
COUNT(hg.id) AS count
FROM huiyuan h
LEFT JOIN huiyuangoumai hg
ON hg.huiyuan_id = h.huiyuan_id
"""
stats_params = []
if jiluzhuangtai == 1: # 未过期
# 条件放在 ON 子句而非 WHERE确保无购买记录的会员类型仍返回 count=0
stats_sql += " AND hg.daoqi_time > %s"
stats_params.append(now)
else: # 已过期
stats_sql += " AND hg.daoqi_time <= %s"
stats_params.append(now)
stats_sql += " GROUP BY h.huiyuan_id, h.jieshao"
with connection.cursor() as cursor:
cursor.execute(stats_sql, stats_params)
stats_rows = cursor.fetchall()
huiyuan_stats = [
{
'id': row[0],
'jieshao': row[1],
'count': row[2]
}
for row in stats_rows
]
return Response({
'code': 0,