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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user