perf: 系统级性能优化与并发安全修复

## 并发安全修复 (4处)
- scrap.py: 报废执行添加 SELECT FOR UPDATE 悲观锁,消除 TOCTOU 竞态
- stock.py (adjust_stock): 盘点调整添加 for_update=True 行锁
- outbound_service.py: 低库存预警 SMTP 调用移到 commit 之后,避免长事务
- trans_service.py: execute_dispatch 按 (source_table, id) 排序 items,消除死锁风险

## N+1 查询优化 (2处)
- inventory_task.py: _prefetch_inventory_map 单条 UNION ALL+GROUP BY 替代循环内逐条查询(N*4次→2次)
- stock.py (export_stocktake): get_borrowed_qty 批量 GROUP BY 替代逐条 TransBorrow 查询(~18000次→1次)

## BOM 列表性能重构
- bom_service.py: get_bom_list 单条 GROUP BY+string_agg+分页,消除 N+1 循环查询
- bom_service.py: 新增 get_bom_summary (轻量 GROUP BY category+COUNT)
- bom.py: 新增 /api/v1/bom/summary 路由,/list 支持 category 过滤

## Odoo 基础信息懒加载
- base_service.py: 新增 get_odoo_summary (GROUP BY category+COUNT)
- base.py: 新增 /api/v1/inbound/base/odoo-summary 路由
- buyOdoo.vue: 懒加载分组架构 (fetchOdooSummary + loadGroupItems)
- material_base.ts: 新增 getOdooSummary API

## 前端 Bug 修复
- BomManage.vue: 懒加载分组 (fetchBomSummary + loadGroupItems + collapse)
- BomManage.vue: 适配新 API 格式 (res.data.items 替代 res.data)
- buyOdoo.vue: 移除 "点击展开加载" 文字
- Selection.vue + borrow/apply/index.vue: openBomSelect 适配新 API 格式
This commit is contained in:
yueli
2026-07-15 17:37:57 +08:00
parent 4934cd4d8f
commit 2556b77530
15 changed files with 617 additions and 233 deletions

View File

@ -62,17 +62,22 @@ def filter_item_by_permissions(item_dict, user_permissions):
@jwt_required()
@permission_required('bom_manage')
def get_bom_list():
"""获取所有 BOM 配方列表,支持 keyword 搜索和 active_only 过滤"""
"""获取 BOM 列表,支持 keywordactive_only、category 过滤和分页"""
try:
keyword = request.args.get('keyword', '').strip()
# 将字符串 'true' 转为布尔值
active_only = request.args.get('active_only', 'false').lower() == 'true'
category = request.args.get('category', '').strip() or None
page = request.args.get('page', 1, type=int)
limit = request.args.get('pageSize', 15, type=int)
data = BomService.get_bom_list(keyword=keyword, active_only=active_only)
# 字段级脱敏
data = BomService.get_bom_list(
keyword=keyword, active_only=active_only,
category=category, page=page, limit=limit
)
# 字段级脱敏data 现在是 {items, total, pages, current_page} 字典)
user_permissions = get_current_user_permissions()
if isinstance(data, list):
data = [filter_item_by_permissions(item, user_permissions) for item in data]
if data.get('items'):
data['items'] = [filter_item_by_permissions(item, user_permissions) for item in data['items']]
return jsonify({
'code': 200,
'msg': 'success',
@ -83,6 +88,23 @@ def get_bom_list():
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
# ==============================================================================
# BOM 分组摘要接口 (GET /api/v1/bom/summary)
# 极轻量查询:仅 GROUP BY category + COUNT(DISTINCT bom_no+version)
# ==============================================================================
@bom_bp.route('/summary', methods=['GET'])
@jwt_required()
@permission_required('bom_manage')
def get_bom_summary():
try:
keyword = request.args.get('keyword', '').strip() or None
data = BomService.get_bom_summary(keyword=keyword)
return jsonify({'code': 200, 'msg': 'success', 'data': data})
except Exception as e:
current_app.logger.error(f'获取BOM摘要失败: {str(e)}')
return jsonify({'code': 500, 'msg': '内部服务器错误'}), 500
@bom_bp.route('/detail/<path:bom_no>', methods=['GET'])
@jwt_required()
@permission_required('bom_manage')