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

@@ -15,7 +15,6 @@ import threading
import xmltodict
from decimal import Decimal
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
from django.conf import settings
from django.db import models, transaction, IntegrityError
@@ -260,38 +259,66 @@ def _fetch_images(order, oss_domain):
# ================================================================
# 辅助函数:并发获取多个订单的打手图片
# 辅助函数:批量获取多个订单的打手图片(单次 DB 查询)
# ================================================================
def _batch_images(orders, oss_domain):
"""
并发调用 _fetch_images提升批量获取图片的速度
批量获取订单的打手提交图片,使用单次 DB 查询替代循环/线程池 N 次查询
参数:
orders: Dingdan 订单对象列表
oss_domain: OSS 域名
返回:
字典键为订单ID (dingdan_id),值为 (images_list, is_punished, punishment_info)
字典键为订单ID (OrderID),值为 (images_list, is_punished, punishment_info)
"""
result = {} # 存储结果
# 使用线程池并发执行max_workers 可根据服务器性能调整
with ThreadPoolExecutor(max_workers=10) as executor:
# 建立 future 到订单ID的映射
future_to_oid = {
executor.submit(_fetch_images, order, oss_domain): order.OrderID
for order in orders
}
# 等待所有任务完成
for future in as_completed(future_to_oid):
oid = future_to_oid[future] # 对应的订单ID
try:
# 获取任务返回值
imgs, pun, pinfo = future.result()
result[oid] = (imgs, pun, pinfo)
except Exception as e:
# 某个订单获取失败,记录日志并赋予空值,保证整体流程不中断
logger.error(f"获取订单 {oid} 图片失败: {e}")
result[oid] = ([], False, {})
result = {}
# 先为所有订单填充默认空值,保证调用方可拿到键
for order in orders:
result[order.OrderID] = ([], False, {})
# 收集所有 (OrderID, PlayerID) 组合用于批量查询
_order_player_pairs = [
(order.OrderID, order.PlayerID)
for order in orders
if order.PlayerID
]
if not _order_player_pairs:
return result
# 单次批量查询所有相关图片记录(避免循环 N 次或线程池掩盖 N 次查询)
# PlayerDeliveryImage 表的主键是 OrderID + PlayerID使用元组列表过滤
_order_ids = [pair[0] for pair in _order_player_pairs]
_pair_set = set(_order_player_pairs)
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

View File

@@ -771,10 +771,12 @@ class ShopProductModifyView1(APIView):
else:
# 自动从公共类型获取会员ID或转为佣金
try:
shop_type = ShangpinLeixingDianpu.query.get(
shop_type = ShangpinLeixingDianpu.query.select_related('gonggong_leixing').get(
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
if auto_huiyuan_id:
product.huiyuan_id = auto_huiyuan_id
@@ -893,13 +895,12 @@ class ShopProductModifyView1(APIView):
return Response({'code': 400, 'msg': '必填项缺失'})
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:
return Response({'code': 400, 'msg': '无效的店铺商品类型'})
try:
public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id, shenhezhuangtai=1)
except ShangpinLeixing.DoesNotExist:
public_type = shop_type.gonggong_leixing
if not public_type or public_type.shenhezhuangtai != 1:
return Response({'code': 400, 'msg': '关联的公共商品类型无效'})
# 商品图片上传

View File

@@ -125,7 +125,7 @@ class ShopRefundView(APIView):
# 更新打手统计(退款量+1状态置为空闲
if dashou_id:
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.tuikuanliang += 1
dashou_profile.zhuangtai = 1
@@ -368,7 +368,7 @@ class ShopForceCompleteView(APIView):
# ---------- 更新打手余额和统计 ----------
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.chengjiaozongliang += 1
dashou_profile.yue += dashou_fencheng