perf: 消除出库列表和还库操作的 N+1 查询,改用批量 IN + joinedload
This commit is contained in:
@ -1,6 +1,7 @@
|
|||||||
import uuid # .material -> .base refactor checked
|
import uuid # .material -> .base refactor checked
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
from sqlalchemy import or_, func, desc, and_
|
from sqlalchemy import or_, func, desc, and_
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.outbound import TransOutbound, OutboundApproval
|
from app.models.outbound import TransOutbound, OutboundApproval
|
||||||
|
|
||||||
@ -475,6 +476,37 @@ class OutboundService:
|
|||||||
'stock_product': StockProduct
|
'stock_product': StockProduct
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# ★ 优化步骤 1:第一遍循环,单纯收集所有的 stock_id
|
||||||
|
# ==========================================
|
||||||
|
stock_ids_by_table = {'stock_buy': set(), 'stock_semi': set(), 'stock_product': set()}
|
||||||
|
|
||||||
|
for d in details:
|
||||||
|
if d.source_table in stock_ids_by_table and d.stock_id:
|
||||||
|
stock_ids_by_table[d.source_table].add(d.stock_id)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# ★ 优化步骤 2:发起批量查询,并强制 JOIN 基础物料表
|
||||||
|
# ==========================================
|
||||||
|
# 格式: { ('stock_buy', 101): stock_obj, ... }
|
||||||
|
preloaded_stocks = {}
|
||||||
|
|
||||||
|
for table_name, ids in stock_ids_by_table.items():
|
||||||
|
if not ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ModelClass = model_map[table_name]
|
||||||
|
# 魔法在这里:in_() 一次性查出所有库存,joinedload 顺便把 base 表的数据一起拉回来
|
||||||
|
items = ModelClass.query.options(
|
||||||
|
joinedload(ModelClass.base)
|
||||||
|
).filter(ModelClass.id.in_(ids)).all()
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
preloaded_stocks[(table_name, item.id)] = item
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# ★ 优化步骤 3:第二遍循环,纯内存拼装(极速)
|
||||||
|
# ==========================================
|
||||||
for d in details:
|
for d in details:
|
||||||
ono = d.outbound_no
|
ono = d.outbound_no
|
||||||
if ono not in grouped_map:
|
if ono not in grouped_map:
|
||||||
@ -490,34 +522,20 @@ class OutboundService:
|
|||||||
'items': []
|
'items': []
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 查询物品详细信息 (名称, 规格, 类型, 类别, 批号/SN) ---
|
# --- 直接从内存字典中获取,O(1) 复杂度,绝对不触发 SQL ---
|
||||||
item_name = "未知物品"
|
item_name, item_spec, item_cat, item_type, batch_sn = "未知物品", "", "", "", "-"
|
||||||
item_spec = ""
|
|
||||||
item_cat = ""
|
|
||||||
item_type = ""
|
|
||||||
batch_sn = "-"
|
|
||||||
|
|
||||||
ModelClass = model_map.get(d.source_table)
|
stock_item = preloaded_stocks.get((d.source_table, d.stock_id))
|
||||||
if ModelClass and d.stock_id:
|
|
||||||
try:
|
if stock_item:
|
||||||
stock_item = ModelClass.query.get(d.stock_id)
|
batch_sn = getattr(stock_item, 'batch_number', None) or getattr(stock_item, 'serial_number', None) or '-'
|
||||||
if stock_item:
|
|
||||||
# 获取批号/序列号用于追溯
|
# 因为前面用了 joinedload,这里调用 .base 瞬间返回,不会去查数据库
|
||||||
batch_sn = getattr(stock_item, 'batch_number', None) or getattr(stock_item, 'serial_number', None) or '-'
|
if stock_item.base:
|
||||||
if stock_item.base:
|
item_name = stock_item.base.name
|
||||||
item_name = stock_item.base.name
|
item_spec = stock_item.base.spec_model
|
||||||
item_spec = stock_item.base.spec_model
|
item_cat = stock_item.base.category
|
||||||
item_cat = stock_item.base.category
|
item_type = stock_item.base.material_type
|
||||||
item_type = stock_item.base.material_type
|
|
||||||
elif stock_item and hasattr(stock_item, 'base_id') and stock_item.base_id:
|
|
||||||
base_info = MaterialBase.query.get(stock_item.base_id)
|
|
||||||
if base_info:
|
|
||||||
item_name = base_info.name
|
|
||||||
item_spec = base_info.spec_model
|
|
||||||
item_cat = base_info.category
|
|
||||||
item_type = base_info.material_type
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error fetching detail for stock_id {d.stock_id}: {e}")
|
|
||||||
|
|
||||||
# 计算金额
|
# 计算金额
|
||||||
price = float(d.unit_price) if d.unit_price else 0
|
price = float(d.unit_price) if d.unit_price else 0
|
||||||
|
|||||||
@ -114,12 +114,12 @@ class TransService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def process_return(data, operator_name):
|
def process_return(data, operator_name):
|
||||||
"""
|
"""
|
||||||
还库逻辑(支持部分归还):
|
还库逻辑(支持部分归还)- 已优化,消除 N+1 和长事务死锁风险
|
||||||
1. 校验本次归还数量不能大于待还数量
|
四步走策略:
|
||||||
2. 恢复可用库存(按本次归还数量)
|
1. 收集所有 borrow_id
|
||||||
3. 更新库位 (如果有变动)
|
2. 批量锁定借用记录
|
||||||
4. 记录库管签字
|
3. 收集库存ID并批量锁定库存
|
||||||
5. 更新归还数量和状态(部分归还/全部归还)
|
4. 内存中完成业务逻辑
|
||||||
"""
|
"""
|
||||||
items = data.get('items', [])
|
items = data.get('items', [])
|
||||||
signature = data.get('signature_path') # 库管签字
|
signature = data.get('signature_path') # 库管签字
|
||||||
@ -130,15 +130,60 @@ class TransService:
|
|||||||
model_map = {'stock_buy': StockBuy, 'stock_semi': StockSemi, 'stock_product': StockProduct}
|
model_map = {'stock_buy': StockBuy, 'stock_semi': StockSemi, 'stock_product': StockProduct}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# ==========================================
|
||||||
|
# ★ 优化步骤 1:收集所有 borrow_id
|
||||||
|
# ==========================================
|
||||||
|
borrow_ids = []
|
||||||
|
item_map = {} # 存储原始 item 数据,key=borrow_id
|
||||||
for item in items:
|
for item in items:
|
||||||
borrow_id = item.get('id')
|
borrow_id = item.get('id')
|
||||||
# 前端传入的本次归还数量
|
if borrow_id:
|
||||||
return_qty = float(item.get('return_qty', 0))
|
borrow_ids.append(borrow_id)
|
||||||
# 前端如果没有填 return_location,应该在提交前处理好,或者这里做 fallback
|
item_map[borrow_id] = {
|
||||||
# 这里假设前端传来的 return_location 就是最终要保存的库位
|
'return_qty': float(item.get('return_qty', 0)),
|
||||||
final_location = item.get('return_location')
|
'final_location': item.get('return_location')
|
||||||
|
}
|
||||||
|
|
||||||
record = TransBorrow.query.with_for_update().get(borrow_id)
|
if not borrow_ids:
|
||||||
|
raise ValueError("没有有效的归还记录")
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# ★ 优化步骤 2:批量锁定借用记录
|
||||||
|
# ==========================================
|
||||||
|
borrow_records = TransBorrow.query.with_for_update().filter(
|
||||||
|
TransBorrow.id.in_(borrow_ids)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
borrow_map = {r.id: r for r in borrow_records}
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# ★ 优化步骤 3:收集库存ID并批量锁定库存
|
||||||
|
# ==========================================
|
||||||
|
stock_ids_by_table = {'stock_buy': set(), 'stock_semi': set(), 'stock_product': set()}
|
||||||
|
|
||||||
|
for borrow_id, record in borrow_map.items():
|
||||||
|
if record.source_table in stock_ids_by_table and record.stock_id:
|
||||||
|
stock_ids_by_table[record.source_table].add(record.stock_id)
|
||||||
|
|
||||||
|
stock_map = {} # 格式: { ('stock_buy', 101): stock_obj }
|
||||||
|
for table_name, ids in stock_ids_by_table.items():
|
||||||
|
if not ids:
|
||||||
|
continue
|
||||||
|
ModelClass = model_map[table_name]
|
||||||
|
stocks = ModelClass.query.with_for_update().filter(
|
||||||
|
ModelClass.id.in_(ids)
|
||||||
|
).all()
|
||||||
|
for stock in stocks:
|
||||||
|
stock_map[(table_name, stock.id)] = stock
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# ★ 优化步骤 4:内存中完成业务逻辑
|
||||||
|
# ==========================================
|
||||||
|
for borrow_id, item_data in item_map.items():
|
||||||
|
return_qty = item_data['return_qty']
|
||||||
|
final_location = item_data['final_location']
|
||||||
|
|
||||||
|
record = borrow_map.get(borrow_id)
|
||||||
if not record:
|
if not record:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -153,22 +198,19 @@ class TransService:
|
|||||||
if return_qty > pending_qty:
|
if return_qty > pending_qty:
|
||||||
raise ValueError(f"本次归还数量({return_qty})不能大于待还数量({pending_qty})")
|
raise ValueError(f"本次归还数量({return_qty})不能大于待还数量({pending_qty})")
|
||||||
|
|
||||||
ModelClass = model_map.get(record.source_table)
|
# 更新库存
|
||||||
if ModelClass:
|
stock = stock_map.get((record.source_table, record.stock_id))
|
||||||
stock = ModelClass.query.with_for_update().get(record.stock_id)
|
if stock:
|
||||||
if stock:
|
# 恢复可用库存
|
||||||
# 1. 恢复可用库存(按本次归还数量)
|
stock.available_quantity = float(stock.available_quantity) + return_qty
|
||||||
stock.available_quantity = float(stock.available_quantity) + return_qty
|
# 更新库位
|
||||||
|
if final_location:
|
||||||
|
stock.warehouse_location = final_location
|
||||||
|
|
||||||
# 2. 更新库位 (如果提供了有效值)
|
# 更新归还数量和状态
|
||||||
if final_location:
|
|
||||||
stock.warehouse_location = final_location
|
|
||||||
|
|
||||||
# 3. 更新归还数量
|
|
||||||
new_returned_qty = returned_qty + return_qty
|
new_returned_qty = returned_qty + return_qty
|
||||||
record.returned_quantity = new_returned_qty
|
record.returned_quantity = new_returned_qty
|
||||||
|
|
||||||
# 4. 更新状态
|
|
||||||
if new_returned_qty >= total_qty:
|
if new_returned_qty >= total_qty:
|
||||||
record.is_returned = True
|
record.is_returned = True
|
||||||
record.status = 'returned'
|
record.status = 'returned'
|
||||||
|
|||||||
Reference in New Issue
Block a user