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:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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': '关联的公共商品类型无效'})
|
||||
|
||||
# 商品图片上传
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user