fix(consignment): 问题单「退回大厅」也要解绑寄卖现货

问题单处置的「退回大厅」只把订单置回 open,没有解绑被本工单占用的寄卖现货,也没摘 material 上的
寄售标记。后果是卖家端「我的寄卖」把这张单显示成「履约中 + 关联订单状态已变化,请联系客服核对」
(状态机只认 in_progress / problem / pending_acceptance,open 落到兜底分支),而现货永久停在
consumed 占库不还、既不再参与匹配也没有释放入口。

其余同义路径(后台退回大厅、超时重开、打手退单通过)本来都会解绑,只有问题单这条漏了,属口径不一致。
现在把解绑条件从「仅取消」扩到「取消或退回大厅」;「退回打手重做」保持占用(货还在这一单上履约)。
调用位置仍在状态 UPDATE 之前——该 UPDATE 在 open/cancelled 时会清空 friend_added_at,
清完再解绑会把已确认加好友的货误判成可回池(二次出货)。

验证:真实库跑通两条路径——退回打手重做(现货仍 consumed+绑定的、标记保留、打手端仍「已匹配-待加好友」)
与退回大厅(现货回 in_stock、解绑、标记摘除、打手端变「已通过(在库待匹配)」);后端 520 项单测全绿。
This commit is contained in:
yml2213
2026-10-01 12:22:56 +08:00
parent 40ee36d72a
commit 9573acc59d
2 changed files with 27 additions and 14 deletions
@@ -143,17 +143,25 @@ export async function resolveProblemWorkOrder(input: {
input.action === 'reopen' ||
(input.action === 'return_to_worker' && targetStatus === 'in_progress')
// 取消处置 = 工单终止:被本工单占用的寄卖现货必须解绑(回库;已确认加好友则作废下架),
// 并同步摘掉 material 上的寄售标记。否则会出现两个后果:
// 1) 列表「打手」列继续显示「供货打手 / 内部现货」,看着像打手还挂在这张已取消的单上;
// 2) 现货永久停在 consumed 占库不还,既不参与匹配也没有押金释放入口。
// ⚠️ 必须放在下面的 UPDATE 之前:那条 UPDATE 在取消时会把 friend_added_at 清空,
// 问题单处置把工单带离履约态(取消 / 退回大厅)时,被本工单占用的寄卖现货必须解绑
// (回库;已确认加好友则作废下架),并同步摘掉 material 上的寄售标记。
// 不解绑会出现两个后果:
// 1) 卖家端「我的寄卖」把这张单显示成「履约中 + 关联订单状态已变化,请联系客服核对」
// ——状态机只认 in_progress / problem / pending_acceptance,open 落到兜底分支;
// 2) 现货永久停在 consumed 占库不还,既不再参与匹配,也没有押金释放入口。
// 「退回大厅」也必须解绑:后台退回大厅、超时重开、打手退单通过这三条同义路径都会解绑,
// 只有这里漏了(口径不一致,且订单回到大厅后本单已不承担交付)。
// 退回打手重做(in_progress / pending_acceptance)保持占用:货还在这一单上履约。
// ⚠️ 必须放在下面的 UPDATE 之前:那条 UPDATE 在 open/cancelled 时会把 friend_added_at 清空,
// 而解绑是否需要改成「作废」正是看这个字段——清空后再解绑会把已加好友的货退回在库池,
// 等于二次出货。
if (targetStatus === 'cancelled') {
if (targetStatus === 'cancelled' || targetStatus === 'open') {
await releaseConsignmentInventoryBindingForWorkOrderWithClient(client, input.workOrderId, {
now: input.now,
reason: `问题单处置取消(${effectiveAction})`,
reason:
targetStatus === 'cancelled'
? `问题单处置取消(${effectiveAction})`
: `问题单处置退回大厅(${effectiveAction})`,
})
}
@@ -240,7 +240,7 @@ test('没解绑到现货时也必须摘掉工单上的寄售标记(老帖 /
assert.equal(detach!.values[3], null, '无现货可解绑时应按工单整体摘标记')
})
test('问题单处置取消:先解绑寄卖现货,再改工单状态', async () => {
test('问题单处置离开履约态(取消 / 退回大厅)都要先解绑寄卖现货', async () => {
const readFile = await import('node:fs/promises')
const src = await readFile.readFile(
new URL('../../repositories/worker-platform/work-order-issue-repo.ts', import.meta.url),
@@ -253,13 +253,18 @@ test('问题单处置取消:先解绑寄卖现货,再改工单状态', async
const releaseIndex = body.indexOf(
'releaseConsignmentInventoryBindingForWorkOrderWithClient(client, input.workOrderId',
)
assert.ok(releaseIndex > 0, '取消问题单必须解绑被本工单占用的寄卖现货,否则现货永久占库不还')
assert.ok(
body.includes("if (targetStatus === 'cancelled')"),
'只对「取消」处置解绑:退回打手重做 / 退回大厅时货还在履约,不能解绑',
)
assert.ok(releaseIndex > 0, '处置问题单必须解绑被本工单占用的寄卖现货,否则现货永久占库不还')
// 顺序很重要:那条 UPDATE 会在取消时把 friend_added_at 清空,而"是否改判作废"正是看这个字段。
// 「退回大厅」也必须解绑:后台退回大厅 / 超时重开 / 打手退单通过这三条同义路径都会解绑,
// 只有问题单这条漏了,导致卖家端一直显示「履约中 + 关联订单状态已变化,请联系客服核对」。
assert.ok(
body.includes("if (targetStatus === 'cancelled' || targetStatus === 'open')"),
'取消与退回大厅都要解绑',
)
// 退回打手重做必须保持占用:货还在这一单上履约
assert.ok(body.includes('return_to_worker'), '未找到退回打手重做的分支(该分支不能解绑)')
// 顺序很重要:那条 UPDATE 在 open/cancelled 时会把 friend_added_at 清空,而"是否改判作废"正是看这个字段。
// 清空之后再解绑,会把已加好友的现货退回在库池 → 二次出货。
const updateIndex = body.indexOf('UPDATE work_orders')
assert.ok(updateIndex > 0, '未找到工单状态更新语句')