fix(web): 扫码页与备选库位带单据ID,按有效可用量限流

配合后端预占回加:扫码/借库/审批页面调用 /outbound/scan 与 /outbound/alternatives
时带上本单 id 与 biz_type,拿到的 available_quantity 即为「实时可用量 + 本单
预占」,本单锁定的行不会再因实时可用量为 0 而被拦住或从列表消失。

后端已在响应边界完成归一化,故前端模板与校验逻辑(:max、库存列展示、超量
警告、草稿存取)一律不动,只多传参数 —— 避免把同一语义散落到多个读写点。

- api/outbound.ts:getStockByBarcode / getStockAlternatives 加 requestId 与
  bizType 参数,导出 ScanBizType,ScanResult 补 reserved_quantity 等字段
- api/transaction.ts:同名的历史副本同步签名(当前无调用方,加注释指向唯一来源)
- views/outbound/create.vue、views/transaction/borrow.vue:各 3 处调用点带上
  单据 id;refreshStockFromDraft 改为显式透传 id,不依赖 computed 已解析
- views/borrow/approval/index.vue:审批页也带上本单 id,否则待审批单
  (预占已生效)锁定的行会因实时可用量为 0 而从备选列表消失

借库侧 request_id 是 BorrowApproval.id,靠 biz_type='borrow' 与出库单区分 ——
两张表 ID 空间独立,不可省。
This commit is contained in:
yueli
2026-09-11 10:34:23 +08:00
parent a8a3c82331
commit b313fefdfa
5 changed files with 107 additions and 32 deletions

View File

@ -424,10 +424,13 @@ const loadAlternatives = async (row: any) => {
altItems.value = []
altTotal.value = 0
try {
const res: any = await getStockAlternatives(row.base_id, {
stock_id: row.stock_id,
source_table: row.source_table,
})
// ★ 带上本单 id:本单锁定的批次实时可用量为 0,不回加的话会从列表里消失
const res: any = await getStockAlternatives(
row.base_id,
{ stock_id: row.stock_id, source_table: row.source_table },
selectedApprovalId.value,
'borrow'
)
altItems.value = res?.data?.items || []
altTotal.value = res?.data?.total_available || 0
} catch (e) {
@ -598,7 +601,7 @@ const restoreDraft = async (requestId: number) => {
// ★ 刷新库存:草稿里的 available_quantity 是扫描那一刻的快照,
// 期间别人出库/借出会消耗可用量,需重新拉取实时值。
await refreshStockFromDraft()
await refreshStockFromDraft(requestId)
} catch (e) {
console.warn('恢复草稿失败', e)
}
@ -607,15 +610,24 @@ const restoreDraft = async (requestId: number) => {
/**
* 用实时库存刷新购物车行的「库存」列(与扫码出库页同款)。
* 复用 /alternatives 端点;失败不阻断,沿用草稿快照。
*
* ★ 传入本单 id 后,返回的 available_quantity 是**有效可用量**
* (实时值 + 本单预占)。借库申请提交时已冻结可用数,不回加的话
* 被本单占满的行会显示 0,下面「已借数量 > 最新可用量」也会误报。
*/
const refreshStockFromDraft = async () => {
const refreshStockFromDraft = async (requestId?: number | null) => {
const plan = selectedApproval.value?.items || []
const baseIds = [...new Set(plan.map((p: any) => p.base_id).filter(Boolean))]
if (!baseIds.length) return
// ★ 显式透传单据 id(而非依赖 selectedApproval 已解析)
const rid = requestId ?? selectedApproval.value?.id ?? selectedApprovalId.value
try {
const results = await Promise.all(
(baseIds as number[]).map(bid => getStockAlternatives(bid).catch(() => null))
(baseIds as number[]).map(bid =>
getStockAlternatives(bid, undefined, rid, 'borrow').catch(() => null)
)
)
const latestByKey = new Map<string, number>()
for (const r of results) {
@ -793,7 +805,10 @@ const handleManualInput = async () => {
}
// 查库
const res = await getStockByBarcode(code)
// ★ 带上本单 id:借库申请提交时已冻结可用数(available_quantity 被扣减),
// 不回加的话,被本单占满的行会显示 0,工人根本扫不进去。
// biz_type='borrow' 不可省 —— 出库单与借库单是两张独立表、ID 空间独立。
const res = await getStockByBarcode(code, selectedApprovalId.value, 'borrow')
if (res.data) {
const item = res.data
const availQty = parseFloat(item.available_quantity || 0)