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 单号、拼单门禁四入口 + 未过度拦截反向用例。
110 lines
4.4 KiB
TypeScript
110 lines
4.4 KiB
TypeScript
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} 本就不该给入口`,
|
||
)
|
||
}
|
||
})
|