diff --git a/inventory-backend/app/api/v1/transactions.py b/inventory-backend/app/api/v1/transactions.py index 15735e8..05f463b 100644 --- a/inventory-backend/app/api/v1/transactions.py +++ b/inventory-backend/app/api/v1/transactions.py @@ -283,12 +283,14 @@ def dispatch_borrow(): approval_id: int, // 关联的审批单ID items: [ // 扫码选中的库存物品 { - id: int, // 库存主键(按 source_table 路由到 StockBuy/StockSemi/StockProduct) - source_table: str, // 'stock_buy' | 'stock_semi' | 'stock_product' - sku: str, // 用于按 (source_table, sku) 与审批单做超额交叉校验 + id: int, // 库存主键(按 source_table 路由到 StockBuy/StockSemi/StockProduct) + source_table: str, // 'stock_buy' | 'stock_semi' | 'stock_product' + sku: str, // 可选;不参与审批上限校验 out_quantity: float } ], + // ★ 审批上限校验在 service 层完成:以 (name, spec_model) 为物料维度聚合 + // 锁定 stock 行后从 material_base 表取真实 (name, spec_model) 与审批单比对 borrower_name: str, signature_path: str, remark: str, diff --git a/inventory-backend/app/services/trans_service.py b/inventory-backend/app/services/trans_service.py index fdb533b..6fddfc3 100644 --- a/inventory-backend/app/services/trans_service.py +++ b/inventory-backend/app/services/trans_service.py @@ -34,7 +34,12 @@ class TransService: signature=None, remark=None, expected_return_time=None): """ 执行借库扣减(审批通过后调用) - 流程:锁审批单 → 超额校验 → 锁库存行 → 扣减库存 → 生成 TransBorrow 记录 → 标记审批单完成 + 流程:锁审批单 → 构建审批上限字典 → 锁库存行 → 名称规格校验 → 扣减库存 → 生成 TransBorrow 记录 → 标记审批单完成 + + ★ 关键设计:审批维度是 (name, spec_model) 而非 SKU + 借库申请是按【名称 + 规格型号】发起的(borrow_service 强制要求 name/spec_model/quantity 三字段), + 申请时尚未绑定具体库存行;扫码出库时通过锁定 stock 行回查 material_base 表, + 用 (name, spec_model) 与审批单做物料维度聚合比对,避免 sku 维度坍塌或绕过。 """ from app.models.borrow import BorrowApproval @@ -58,36 +63,23 @@ class TransService: raise ValueError("审批单中未记录借库人姓名,请联系管理员补录") # ============================================== - # ★ 防线2:超额篡改校验 - 交叉比对前端传来的实际扣减量与审批单上限 + # ★ 防线2:构建审批上限字典(按 名称+规格 聚合,strip 防止匹配失败) + # Key = (name, spec_model),Value = 该物料累计允许借出数量 # ============================================== approved_items = approval.get_items() if not approved_items: raise ValueError("审批单中无物料明细,请联系管理员检查") - # 构建审批上限字典:key=(source_table, sku) → approved_total_qty - approved_limit = {} + approval_limits = {} for ai in approved_items: - key = (ai.get('source_table', ''), ai.get('sku', '')) - qty = float(ai.get('quantity', 0)) - approved_limit[key] = approved_limit.get(key, 0) + qty + key = ( + (ai.get('name') or '').strip(), + (ai.get('spec_model') or '').strip() + ) + approval_limits[key] = approval_limits.get(key, 0) + float(ai.get('quantity', 0)) - # 汇总前端传来的实际出库量(按 source_table+sku 聚合) - dispatch_qty = {} - for item in items: - key = (item.get('source_table', ''), item.get('sku', '')) - qty = float(item.get('out_quantity', 0)) - dispatch_qty[key] = dispatch_qty.get(key, 0) + qty - - # 逐条比对:任意一条实际出库量 > 审批上限 → 直接拒绝 - for key, actual_qty in dispatch_qty.items(): - limit_qty = approved_limit.get(key, 0) - if actual_qty > limit_qty: - source_table, sku = key - raise ValueError( - f"实际出库数量超出了审批单允许的上限: " - f"SKU={sku or '(无)'}({source_table}) " - f"审批上限={limit_qty}, 实际出库={actual_qty}" - ) + # 累计本次扫码出库量(key 与 approval_limits 完全一致) + dispatch_acc = {} borrow_no = TransService.generate_borrow_no() model_map = {'stock_buy': StockBuy, 'stock_semi': StockSemi, 'stock_product': StockProduct} @@ -104,11 +96,39 @@ class TransService: # ============================================== # ★ 防线3:并发超卖与负库存 - 锁行后再查可用库存 # ============================================== - stock = ModelClass.query.with_for_update().get(stock_id) + stock = ModelClass.query.options(joinedload(ModelClass.base)).with_for_update().get(stock_id) if not stock: raise ValueError(f"库存不存在 ID:{stock_id}") + # ============================================== + # ★ 防线4:名称+规格 超额校验(动态累加、即时拦截) + # 库存表本身没有 name/spec_model 字段,通过 base 关联到 material_base + # ============================================== + if stock.base: + stock_name = (stock.base.name or '').strip() + stock_spec = (stock.base.spec_model or '').strip() + else: + stock_name = '' + stock_spec = '' + key = (stock_name, stock_spec) + + limit = approval_limits.get(key) + if limit is None: + raise ValueError( + f"扫码物料【{stock_name} / {stock_spec}】不在审批单允许范围内," + f"请检查审批单明细或重新发起申请" + ) + + dispatch_acc[key] = dispatch_acc.get(key, 0) + qty + current_total = dispatch_acc[key] + if current_total > limit: + raise ValueError( + f"实际出库数量超出了审批单允许的上限: " + f"物料={stock_name}({stock_spec}) " + f"审批上限={limit}, 实际扫码={current_total}" + ) + if float(stock.available_quantity) < qty: - raise ValueError(f"SKU {stock.sku} 可用库存不足") + raise ValueError(f"物料【{stock_name} / {stock_spec}】可用库存不足") # 1. 冻结库存 (只减可用) stock.available_quantity = float(stock.available_quantity) - qty