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 单号、拼单门禁四入口 + 未过度拦截反向用例。
This commit is contained in:
yml2213
2026-10-01 12:17:00 +08:00
parent 610c65bb34
commit 40ee36d72a
11 changed files with 869 additions and 95 deletions
@@ -824,16 +824,23 @@ export async function releaseConsignmentInventoryBindingForWorkOrderWithClient(
[input.now, workOrderId, input.reason || ''],
)
const released = res.rows[0] || null
if (!released) return null
// 现货已解绑回库,工单上的寄售标记必须同步摘掉:否则工单列表会继续显示
// 「寄售履约: 某现货」,撤销按钮也会再次出现(点下去必然 409)。
// 工单上的寄售标记必须同步摘掉:否则工单列表会继续显示「供货打手 / 内部现货」,
// 撤销按钮也会再次出现(点下去必然 409)。
// 关键:**没有解绑到现货时也要摘**。released 为 null 有两类真实场景:
// 1) 老帖工单的 material.consignment 指向的库存行早已不存在或已被人工回库;
// 2) 现货此前已被其它路径处理过(重复处置 / 补偿链路)。
// 这类单子若直接 return,标记会永久留在工单上——"问题单取消后打手列仍显示供货打手"
// 就是这么来的。此时按工单整体摘(不做 inventoryId 过滤):已经不存在被本工单占用的
// 现货,也就不存在误伤新绑定的可能。
await detachConsignmentMaterialFromWorkOrderWithClient(client, {
workOrderId,
inventoryId: released.id,
reason: archiveOnly
? `已确认加好友,现货作废下架: ${input.reason || ''}`
: `工单解绑回库: ${input.reason || ''}`,
inventoryId: released ? released.id : null,
reason: released
? archiveOnly
? `已确认加好友,现货作废下架: ${input.reason || ''}`
: `工单解绑回库: ${input.reason || ''}`
: `无在库现货可解绑,仅摘除寄售标记: ${input.reason || ''}`,
now: input.now,
})
return released
@@ -143,6 +143,20 @@ 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 清空,
// 而解绑是否需要改成「作废」正是看这个字段——清空后再解绑会把已加好友的货退回在库池,
// 等于二次出货。
if (targetStatus === 'cancelled') {
await releaseConsignmentInventoryBindingForWorkOrderWithClient(client, input.workOrderId, {
now: input.now,
reason: `问题单处置取消(${effectiveAction})`,
})
}
await client.query(
`
UPDATE work_orders