perf: 消除 outbound/borrow BOM 匹配的 while(true) 全量加载

## 后端
- outbound.py: 新增 POST /api/v1/outbound/bom-match-stock 端点
  接收 child_ids[],服务端按 base_id IN 查询三表有库存记录并返回

## 前端
- outbound.ts: 新增 bomMatchStock(childIds) API 函数
- Selection.vue: loadAllStockForBom (while(true) 全量) → loadStockForBom (单次 API)
- borrow/apply/index.vue: 同上

## 效果
- BOM 匹配从 ~90 次 HTTP 请求降为 1 次
- 浏览器内存从 ~18000 条降为 ~8-50 条
This commit is contained in:
yueli
2026-07-15 17:52:08 +08:00
parent 2556b77530
commit 329820117f
4 changed files with 131 additions and 55 deletions

View File

@ -122,4 +122,16 @@ export function approveRequest(id: number, data: { action: 'approve' | 'reject';
method: 'patch',
data
})
}
/**
* BOM 匹配库存(替代前端 while(true) 全量加载)
* 根据 BOM 子件的 base_id 列表,服务端精确查询有库存的记录
*/
export function bomMatchStock(childIds: number[]) {
return request({
url: '/v1/outbound/bom-match-stock',
method: 'post',
data: { child_ids: childIds }
})
}

View File

@ -443,6 +443,7 @@ import { ElMessage, ElTable, ElMessageBox } from 'element-plus'
import { printSelectionList } from '@/api/inbound/stock'
import { useUserStore } from '@/stores/user'
import { submitBorrowRequest, getBorrowStockList } from '@/api/transaction'
import { bomMatchStock } from '@/api/outbound'
import { getApproversList } from '@/api/auth'
import { getBomList, getBomDetail, getBomWithStock } from '@/api/bom'
@ -602,32 +603,20 @@ const loadStockList = async () => {
}
}
// BOM 匹配用:全量加载库存(不分页),确保所有物料都能匹配到
const loadAllStockForBom = async () => {
// BOM 匹配用:服务端精确查询匹配库存(替代前端 while(true) 全量加载)
const loadStockForBom = async (childIds: number[]) => {
stockLoading.value = true
try {
let allItems: any[] = []
let page = 1
const pageSize = 200
while (true) {
const res: any = await getBorrowStockList({
page,
pageSize,
is_aggregated: true
})
const list = (res.data?.list || []).map((item: any) => ({
...item,
uniqueKey: `${item.type}_${item.id}`,
warehouse_location: item.warehouse_location || item.warehouse_loc || item.full_path || ''
}))
allItems = allItems.concat(list)
if (list.length < pageSize) break
page++
}
stockList.value = allItems
stockTotal.value = allItems.length
const res: any = await bomMatchStock(childIds)
const items = (res.data?.items || []).map((item: any) => ({
...item,
uniqueKey: item.uniqueKey || `${item.type}_${item.id}`,
warehouse_location: item.warehouse_location || item.warehouse_loc || item.full_path || ''
}))
stockList.value = items
stockTotal.value = items.length
} catch (e) {
ElMessage.error('加载库存列表失败')
ElMessage.error('加载 BOM 匹配库存失败')
} finally {
stockLoading.value = false
}
@ -743,10 +732,6 @@ watch(selectedBomNo, async (newBomNo) => {
const confirmBomAdd = async () => {
if (!selectedBomNo.value) return ElMessage.warning('请选择 BOM')
if (stockList.value.length === 0) {
await loadAllStockForBom()
}
if (currentBomDetail.value.length === 0) {
try {
const detailRes: any = await getBomWithStock(selectedBomNo.value)
@ -758,6 +743,10 @@ const confirmBomAdd = async () => {
}
const bomRows = currentBomDetail.value
// ★ 服务端精确查询 BOM 子件匹配库存(替代前端 while(true) 全量加载)
const childIds = bomRows.map((b: any) => b.child_id).filter(Boolean)
await loadStockForBom(childIds)
let addedCount = 0
let skippedCount = 0

View File

@ -442,7 +442,7 @@ import { ElMessage, ElTable, ElMessageBox } from 'element-plus'
import { getStockList, printSelectionList } from '@/api/inbound/stock'
import { getBomList, getBomDetail, getBomWithStock } from '@/api/bom'
import { useUserStore } from '@/stores/user'
import { submitOutboundRequest } from '@/api/outbound'
import { submitOutboundRequest, bomMatchStock } from '@/api/outbound'
import { getApproversList } from '@/api/auth'
const userStore = useUserStore()
@ -605,32 +605,20 @@ const loadStockList = async () => {
}
}
// BOM 匹配用:全量加载库存(不分页),确保所有物料都能匹配到
const loadAllStockForBom = async () => {
// BOM 匹配用:服务端精确查询匹配库存(替代前端 while(true) 全量加载)
const loadStockForBom = async (childIds: number[]) => {
stockLoading.value = true
try {
let allItems: any[] = []
let page = 1
const pageSize = 200
while (true) {
const res: any = await getStockList({
page,
pageSize,
is_aggregated: true
})
const list = (res.data?.list || []).map((item: any) => ({
...item,
uniqueKey: `${item.type}_${item.id}`,
warehouse_location: item.warehouse_location || item.warehouse_loc || item.full_path || ''
}))
allItems = allItems.concat(list)
if (list.length < pageSize) break
page++
}
stockList.value = allItems
stockTotal.value = allItems.length
const res: any = await bomMatchStock(childIds)
const items = (res.data?.items || []).map((item: any) => ({
...item,
uniqueKey: item.uniqueKey || `${item.type}_${item.id}`,
warehouse_location: item.warehouse_location || item.warehouse_loc || item.full_path || ''
}))
stockList.value = items
stockTotal.value = items.length
} catch (e) {
ElMessage.error('加载库存列表失败')
ElMessage.error('加载 BOM 匹配库存失败')
} finally {
stockLoading.value = false
}
@ -776,10 +764,6 @@ watch(selectedBomNo, async (newBomNo) => {
const confirmBomAdd = async () => {
if (!selectedBomNo.value) return ElMessage.warning('请选择 BOM')
if (stockList.value.length === 0) {
await loadAllStockForBom()
}
if (currentBomDetail.value.length === 0) {
try {
const detailRes: any = await getBomWithStock(selectedBomNo.value)
@ -791,6 +775,10 @@ const confirmBomAdd = async () => {
}
const bomRows = currentBomDetail.value
// ★ 服务端精确查询 BOM 子件匹配库存(替代前端 while(true) 全量加载)
const childIds = bomRows.map((b: any) => b.child_id).filter(Boolean)
await loadStockForBom(childIds)
let addedCount = 0
let skippedCount = 0