fix(consignment): 手动匹配寄卖后刷新工单列表并展示寄卖价格

This commit is contained in:
2026-11-07 15:57:39 +08:00
parent 8920afa0e5
commit 0e9087c8bb
11 changed files with 712 additions and 75 deletions
@@ -0,0 +1,158 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import test from 'node:test'
import {
computeConsignmentMatchScore,
describeConsignmentMatch,
resolveMatchCoverage,
} from '../src/utils/consignment-match-coverage.ts'
test('匹配打分:与仓库列表同口径(精确 100 / 高配 85 / 未知 50 / 冲突 0)', () => {
assert.equal(computeConsignmentMatchScore({ matchType: 'exact', matchDetails: [] }), 100)
assert.equal(computeConsignmentMatchScore({ matchType: 'partial', matchDetails: [] }), 85)
assert.equal(computeConsignmentMatchScore({ matchType: 'unknown', matchDetails: [] }), 50)
assert.equal(computeConsignmentMatchScore({ matchType: 'conflict', matchDetails: [] }), 0)
assert.equal(
computeConsignmentMatchScore({
matchType: 'partial',
matchDetails: [
{
key: 'grade',
label: '等级',
orderValue: 'A',
inventoryValue: 'S',
status: 'match',
},
{
key: 'series',
label: '系列',
orderValue: 'A',
inventoryValue: 'S',
status: 'partial',
},
],
}),
93,
)
})
test('匹配说明:精确/非精确/冲突都必须有可读文案', () => {
assert.equal(describeConsignmentMatch({ matchType: 'exact' }), '规格完全一致')
assert.equal(
describeConsignmentMatch({ matchType: 'partial', matchReasons: [] }),
'规格高于要求(非精确)',
)
assert.equal(
describeConsignmentMatch({
matchType: 'conflict',
matchReasons: ['系统不一致', '区服不一致'],
}),
'不匹配:系统不一致;区服不一致',
)
})
test('拼单覆盖:1+1 不够 3 份,2+1 才允许提交', () => {
const short = resolveMatchCoverage({
sharingEnabled: true,
totalQuantity: 3,
activeQuantity: 0,
selected: [
{ inventoryId: 1, remainingQuantity: 1 },
{ inventoryId: 2, remainingQuantity: 1 },
],
})
assert.equal(short.requiredQuantity, 3)
assert.equal(short.selectedQuantity, 2)
assert.equal(short.sufficient, false)
assert.equal(short.gap, 1)
const enough = resolveMatchCoverage({
sharingEnabled: true,
totalQuantity: 3,
activeQuantity: 0,
selected: [
{ inventoryId: 1, remainingQuantity: 2 },
{ inventoryId: 2, remainingQuantity: 1 },
],
})
assert.equal(enough.selectedQuantity, 3)
assert.equal(enough.sufficient, true)
assert.equal(enough.gap, 0)
})
test('拼单覆盖:已匹配 1 份时只需再选 2 份', () => {
const result = resolveMatchCoverage({
sharingEnabled: true,
totalQuantity: 3,
activeQuantity: 1,
selected: [{ inventoryId: 1, remainingQuantity: 2 }],
})
assert.equal(result.requiredQuantity, 2)
assert.equal(result.sufficient, true)
})
test('整单覆盖:必须一条库存满足全部数量', () => {
const short = resolveMatchCoverage({
sharingEnabled: false,
totalQuantity: 3,
activeQuantity: 0,
selected: [{ inventoryId: 1, remainingQuantity: 2 }],
})
assert.equal(short.requiredQuantity, 3)
assert.equal(short.selectedQuantity, 2)
assert.equal(short.sufficient, false)
assert.equal(short.gap, 1)
const enough = resolveMatchCoverage({
sharingEnabled: false,
totalQuantity: 3,
activeQuantity: 0,
selected: [{ inventoryId: 1, remainingQuantity: 3 }],
})
assert.equal(enough.sufficient, true)
})
test('工单页:internal_matching 显示「手动匹配寄卖」入口', () => {
const source = readFileSync(
fileURLToPath(new URL('../src/pages/admin/panels/WorkOrdersPanel.tsx', import.meta.url)),
'utf8',
)
assert.ok(source.includes('手动匹配寄卖'), '内部匹配中的按钮文案必须是手动匹配寄卖')
assert.ok(
source.includes("String(row.status || '') === 'internal_matching'"),
'按钮文案必须只在 internal_matching 下切换',
)
})
test('工单页:手动匹配寄卖成功后必须按真实查询键刷新列表与库存', () => {
const source = readFileSync(
fileURLToPath(new URL('../src/pages/admin/panels/WorkOrdersPanel.tsx', import.meta.url)),
'utf8',
)
const modalIndex = source.indexOf('<ConsignmentWorkOrderMatchModal')
assert.ok(modalIndex > 0, '工单页必须挂载手动匹配寄卖弹窗')
const matchedIndex = source.indexOf('onMatched={() => {', modalIndex)
const matchedBlock = source.slice(matchedIndex, source.indexOf('}}', matchedIndex) + 2)
assert.ok(matchedBlock.includes('refreshAll()'), '匹配成功必须调用 refreshAll() 重取列表')
assert.ok(
!matchedBlock.includes("'admin-work-orders'") &&
!matchedBlock.includes("'admin-work-orders-stats'"),
'不得 invalidate 不存在的 admin-work-orders / admin-work-orders-stats 查询键',
)
const refreshIndex = source.indexOf('async function refreshAll()')
const refreshBlock = source.slice(
refreshIndex,
source.indexOf('async function runAction', refreshIndex),
)
for (const key of [
'admin-worker-platform-orders',
'admin-worker-platform-summary',
'admin-consignment-inventories',
'admin-consignment-stats',
]) {
assert.ok(refreshBlock.includes(key), `refreshAll() 必须刷新 ${key}`)
}
})