feat(consignment): 拆分自动/手动匹配口径并优化内部匹配列表

This commit is contained in:
2026-11-14 18:26:01 +08:00
parent 5ff0ddb58e
commit e97932dcdf
16 changed files with 877 additions and 144 deletions
@@ -0,0 +1,126 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import test from 'node:test'
function readSource(relativePath: string): string {
return readFileSync(fileURLToPath(new URL(relativePath, import.meta.url)), 'utf8')
}
test('寄卖配置:每个规格字段同时提供自动/手动两套匹配方式', () => {
const source = readSource('../src/pages/admin/panels/ConsignmentConfigPanel.tsx')
assert.ok(source.includes('自动匹配方式'), '必须保留自动匹配方式下拉')
assert.ok(source.includes('手动匹配方式'), '必须新增手动匹配方式下拉')
assert.ok(source.includes("manualMatchMode: 'exact'"), '新增字段与模板生成的字段默认手动精确相等')
assert.ok(
source.includes("value={spec.manualMatchMode ?? 'exact'}"),
'历史配置没有手动口径时按精确相等展示',
)
const typeSource = readSource('../src/types/consignment.ts')
assert.ok(typeSource.includes('manualMatchMode?'), '前端类型必须包含 manualMatchMode')
})
test('寄卖配置:两套匹配方式与通配值/进物品名/支持筛选必须在同一行', () => {
const source = readSource('../src/pages/admin/panels/ConsignmentConfigPanel.tsx')
const css = readSource('../src/styles/admin.css')
assert.ok(
source.includes('order-service-field-extra is-match-modes'),
'字段行必须使用 5 列栅格类',
)
assert.ok(css.includes('.order-service-field-extra.is-match-modes'))
assert.ok(
css.includes('minmax(90px, 0.72fr)'),
'手动匹配方式必须比自动匹配方式窄,避免把通配值挤没',
)
assert.ok(
css.includes('.order-service-field-extra.is-match-modes,'),
'窄屏媒体查询必须包含该 5 列布局,保证折行',
)
})
test('后端:规格字段归一化补 manualMatchMode,历史配置缺省精确相等', () => {
const source = readSource(
'../../backend/src/services/worker-platform/consignment-spec-service.ts',
)
assert.ok(source.includes('manualMatchMode?: ConsignmentSpecMatchMode'))
assert.ok(source.includes('source.manualMatchMode ?? source.manual_match_mode'))
assert.ok(
source.includes(
"return specs.map((spec) => ({ ...spec, matchMode: spec.manualMatchMode ?? 'exact' }))",
),
'手动口径必须把 matchMode 替换成 manualMatchMode(缺省 exact)',
)
})
test('后端:手动候选、手动复检、待匹配看板都使用手动口径', () => {
const engine = readSource(
'../../backend/src/services/worker-platform/consignment-match-engine-service.ts',
)
const pending = readSource(
'../../backend/src/services/worker-platform/consignment-pending-match-service.ts',
)
assert.equal(
(engine.match(/matchScope: 'manual'/g) || []).length,
2,
'手动候选与手动复检必须传 manual 口径',
)
assert.ok(engine.includes("matchScope: 'auto'"), '自动匹配候选必须显式使用 auto 口径')
assert.ok(pending.includes("matchScope: 'manual'"), '一键查询待匹配订单必须按手动口径')
})
test('内部库存匹配弹窗:默认展示全部候选,按打分降序,并显示寄卖单号', () => {
const source = readSource('../src/components/admin/ConsignmentWorkOrderMatchModal.tsx')
assert.ok(source.includes('默认按匹配打分从高到低展示全部候选'), '默认展示口径必须在界面上说明')
assert.ok(
!source.includes("if (!searchable) return (item.matchType || 'exact') === 'exact'"),
'不能再默认只过滤精确匹配',
)
assert.ok(
source.includes('computeConsignmentMatchScore(right) - computeConsignmentMatchScore(left)'),
'列表必须按匹配打分降序排列',
)
assert.ok(
source.includes('buildCandidateHaystack(item).includes(normalizedKeyword)'),
'关键词必须覆盖全部匹配状态',
)
assert.ok(source.includes("title: '寄卖单号'"), '必须新增寄卖单号列')
assert.ok(
source.includes('item.consignmentNo || item.inventoryNo'),
'寄卖单号优先展示提报单号,无提报号回退库存号',
)
assert.ok(source.includes("item.consignmentNo || ''"), '搜索必须能命中寄卖单号')
assert.ok(!source.includes('显示不匹配项'), '「显示不匹配项」勾选框必须删除')
assert.ok(!source.includes('includeConflicts'), '不再保留 includeConflicts 状态')
assert.ok(source.includes('initialInventoryIds'), '必须支持外部预选库存')
assert.ok(source.includes('if (presetInventoryKey)'), '预选库存必须优先于默认自动勾选')
})
test('手动匹配弹窗:状态不允许时仍能搜索,但禁止提交', () => {
const source = readSource('../src/components/admin/ConsignmentWorkOrderMatchModal.tsx')
assert.ok(source.includes('const matchable = data?.matchable !== false'))
assert.ok(source.includes('当前仍可搜索和核对库存'), '只读状态必须给出明确提示')
assert.ok(source.includes('disabled={!canSubmit}'))
assert.ok(
/matchable &&\s+selectedIds\.length > 0/.test(source),
'canSubmit 必须受 matchable 约束',
)
const searchIndex = source.indexOf('搜索寄卖单号 / 库存号 / 物品 / 规格 / 打手')
const optionsBranchIndex = source.indexOf('visibleOptions.length > 0 ? (')
assert.ok(searchIndex > 0)
assert.ok(
searchIndex < optionsBranchIndex,
'搜索框必须常驻在候选分支之外,空列表 / 只读状态也能使用',
)
const typeSource = readSource('../src/types/consignment.ts')
assert.ok(typeSource.includes('matchable: boolean'), '候选响应类型必须带 matchable')
assert.ok(typeSource.includes('blockedReason: string'))
})