Files
order_site/apps/frontend/tests/consignment-manual-match.test.ts
yml2213 40ee36d72a fix(consignment): 修复寄卖取消解绑、SUB 单号写空、拼单门禁与后台工单列表实时乱跳
1. 问题单处置「取消」后打手信息不消失:resolveProblemWorkOrder 的取消分支从未解绑寄卖现货,现货永久停在 consumed 占库不还,工单 material 上的寄售标记残留又让列表继续显示「供货打手」。取消时补上解绑;调用必须排在状态 UPDATE 之前——那条 UPDATE 会清空 friend_added_at,清完再解绑会把已确认加好友的货误判成可回池(二次出货);
2. 解绑函数在没有现货可解绑时直接 return,跳过了摘标记,于是「库存行早已不存在」的老帖永远摘不掉寄售标记(列表一直显示供货打手)。现在无论是否解绑到现货都摘标记,痕迹保留在 material.consignmentHistory 的 detachReason 里;
3. 内部现货履约写入的 sourceSubmissionNo 恒为空:取的是占库 `UPDATE ... RETURNING *` 的行,只带回本表列,没有 join 出来的 submission_no,导致工单列表把所有寄卖单都显示成「老单无提报单号」,SUB 单号这个唯一锚点丢失。改取带 join 的预读库存(内部现货资料、指派工单资料、指派事件留痕三处)。存量老单按业务口径不回填;
4. 后台接单工单列表实时乱跳:SSE 快照补丁按数组下标读 queryKey,面板 key 加了字段后下标全错(page 被读成 targetWorkOrderId → 任何页都被当成第 1 页),且完全没判「内部寄售匹配」等筛选,于是任意一条不在当前页的老单只要发生变更就被插到列表顶部。改为结构化 queryKey + 三态判定(match 才允许插顶、mismatch 才允许移除、其余只原地更新),插入还要求 page===1 且工单 id 大于当前首行;状态比对改用 rawStatus(快照的 status 是运营态,拼单母单会被显示成 in_progress),分类筛选因快照不带 parentCategoryId 归入不可证伪;
5. 新增「寄卖只做整单」硬门禁:已有人参与的拼单工单不参与现货匹配与库存提醒,履约入口一律返回 work_order_sharing_not_consignable,前端「手动匹配寄卖」入口按同一口径收敛;开了拼单但无人参与的整单不受影响,避免过度拦截。

验证:后端 520 项单测(517 pass / 3 skip)、前端 78 项全绿;并对真实开发库跑通问题单取消三类场景(老帖无库存行 / 回库 / 已加好友作废)、退回打手与退回大厅的反向用例(不得解绑)、履约写入真实 SUB 单号、拼单门禁四入口 + 未过度拦截反向用例。
2026-10-01 12:17:00 +08:00

110 lines
4.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict'
import test from 'node:test'
import {
canManuallyMatchConsignmentOrder,
canSubmitManualMatch,
describeManualMatchType,
filterManualMatchOptions,
resolveManualMatchRequirement,
} from '../src/utils/consignment-manual-match.ts'
test('精确匹配不需要任何确认,直接可提交', () => {
assert.deepEqual(resolveManualMatchRequirement('exact'), {
needsPartialConfirm: false,
needsMismatchConfirm: false,
needsNote: false,
})
assert.equal(canSubmitManualMatch({ matchType: 'exact' }), true)
})
test('非精确匹配必须勾选确认,但不用写原因', () => {
assert.equal(resolveManualMatchRequirement('partial').needsNote, false)
assert.equal(canSubmitManualMatch({ matchType: 'partial' }), false)
assert.equal(canSubmitManualMatch({ matchType: 'partial', confirmedPartial: true }), true)
})
test('规格冲突必须勾选确认**并且**填原因', () => {
assert.equal(resolveManualMatchRequirement('conflict').needsNote, true)
assert.equal(canSubmitManualMatch({ matchType: 'conflict', confirmedMismatch: true }), false)
assert.equal(
canSubmitManualMatch({ matchType: 'conflict', confirmedMismatch: true, matchNote: ' ' }),
false,
)
assert.equal(
canSubmitManualMatch({
matchType: 'conflict',
confirmedMismatch: true,
matchNote: '订单规格录错,已与买家确认',
}),
true,
)
})
test('匹配状态文案与色调:绿/橙/红三档,冲突要一眼看出来', () => {
assert.equal(describeManualMatchType('exact').tone, 'ok')
assert.equal(describeManualMatchType('partial').tone, 'warning')
assert.equal(describeManualMatchType('conflict').tone, 'danger')
assert.equal(describeManualMatchType('conflict').label, '规格冲突')
})
test('关键词过滤:现货能按寄卖单号/物品名搜到,大小写与空格无关', () => {
const list = [
{ submissionNo: 'SUB26091711256110', itemName: '苹果微信暗潮2钻' },
{ submissionNo: 'SUB26091519501248', itemName: '安卓QQ苍龙4钻' },
]
const pick = (item: (typeof list)[number]) => [item.submissionNo, item.itemName]
assert.equal(filterManualMatchOptions(list, '', pick).length, 2)
assert.equal(filterManualMatchOptions(list, ' sub260917 ', pick).length, 1)
assert.equal(filterManualMatchOptions(list, '苍龙', pick).length, 1)
assert.equal(filterManualMatchOptions(list, '不存在', pick).length, 0)
})
test('待指派/待补资料/大厅待抢的订单均能手动指定现货(与后端前置校验同口径)', async () => {
const { isManuallyMatchableOrderStatus } =
await import('../src/utils/consignment-manual-match.ts')
assert.equal(isManuallyMatchableOrderStatus('unassigned'), true)
assert.equal(isManuallyMatchableOrderStatus('pending_material'), true)
assert.equal(isManuallyMatchableOrderStatus('open'), true)
for (const status of ['in_progress', 'pending_acceptance', 'accepted', 'cancelled', 'disputed']) {
assert.equal(isManuallyMatchableOrderStatus(status), false, `${status} 不该给手动匹配入口`)
}
})
test('寄卖只做整单:已有打手参与的拼单工单不给「手动匹配寄卖」入口', () => {
// 状态允许但已有人拼单 → 不给入口(后端也会返回 work_order_sharing_not_consignable)
assert.equal(
canManuallyMatchConsignmentOrder({ status: 'open', sharingProgress: { joinedQuantity: 2 } }),
false,
'有份额被认领的拼单工单不能走寄卖现货履约',
)
assert.equal(
canManuallyMatchConsignmentOrder({ status: 'open', sharingWorkers: [{ workerId: 7 }] }),
false,
'拼单参与者列表非空同样说明已有人参与',
)
// 开了拼单但还没人参与(整单照样能履约)→ 仍然给入口
assert.equal(
canManuallyMatchConsignmentOrder({
status: 'open',
sharingProgress: { joinedQuantity: 0 },
sharingWorkers: [],
}),
true,
)
// 普通整单
assert.equal(canManuallyMatchConsignmentOrder({ status: 'unassigned' }), true)
assert.equal(canManuallyMatchConsignmentOrder({ status: 'pending_material' }), true)
// 状态本身不允许的仍然不给
for (const status of ['in_progress', 'pending_acceptance', 'accepted', 'cancelled']) {
assert.equal(
canManuallyMatchConsignmentOrder({ status, sharingProgress: { joinedQuantity: 0 } }),
false,
`${status} 本就不该给入口`,
)
}
})