Files
order_site/apps/frontend/tests/admin-realtime-work-orders.test.ts
T
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

290 lines
10 KiB
TypeScript
Raw 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 {
evaluateAdminWorkOrderRealtimeMatch,
parseAdminWorkOrderListFilters,
patchAdminWorkOrderListItems,
shouldPrependAdminWorkOrder,
type AdminWorkOrderListFilters,
} from '../src/utils/admin-realtime-work-orders.ts'
/** 复刻 WorkOrdersPanel 的结构化 queryKey。 */
function buildQueryKey(overrides: Partial<AdminWorkOrderListFilters> = {}) {
const filters: AdminWorkOrderListFilters = {
statuses: '',
keyword: '',
workerId: 0,
categoryId: 0,
workerLevelIds: '',
targetWorkOrderId: 0,
acceptanceMode: '',
giftPhase: '',
page: 1,
createdFrom: '',
createdTo: '',
...overrides,
}
return [
'admin-worker-platform-orders',
{
statuses: filters.statuses,
keyword: filters.keyword,
workerId: filters.workerId,
categoryId: filters.categoryId,
workerLevelIds: filters.workerLevelIds,
targetWorkOrderId: filters.targetWorkOrderId,
acceptanceMode: filters.acceptanceMode,
giftPhase: filters.giftPhase,
page: filters.page,
pageSize: 20,
createdFrom: filters.createdFrom,
createdTo: filters.createdTo,
},
] as const
}
function buildSnapshot(overrides: Record<string, unknown> = {}) {
return {
workOrderId: 100,
status: 'in_progress',
rawStatus: 'in_progress',
productName: '安卓暗潮3钻 苏尔南',
platformOrderId: '2627291924608680',
createdAt: '2026-09-29T10:51:05.000Z',
material: { consignment: { inventoryId: 23, inventoryNo: 'INV26091917439822' } },
...overrides,
}
}
function buildEnvelope(items: unknown[]) {
return { ok: true, data: { items, pagination: { total: 124, page: 1, pageSize: 20 } } }
}
function itemsOfWorkOrders(envelope: unknown): Array<{ workOrderId: number }> {
return (envelope as { data: { items: Array<{ workOrderId: number }> } }).data.items
}
function totalOf(envelope: unknown): number {
return (envelope as { data: { pagination: { total: number } } }).data.pagination.total
}
test('状态筛选必须按原始状态判定:拼单母单的运营态不会把行误删', () => {
const filters = parseAdminWorkOrderListFilters(buildQueryKey({ statuses: 'open' }))!
// 拼单母单:原始 wo.status = open(列表筛选用它),但快照的 status 被显示成 in_progress
const sharingMasterOrder = buildSnapshot({ status: 'in_progress', rawStatus: 'open' })
assert.equal(
evaluateAdminWorkOrderRealtimeMatch(sharingMasterOrder, filters),
'match',
'拿运营态比会误判成 mismatch 并把行从列表里删掉',
)
// 反过来:原始状态确实不符仍然要能证伪
assert.equal(
evaluateAdminWorkOrderRealtimeMatch(
buildSnapshot({ status: 'open', rawStatus: 'accepted' }),
filters,
),
'mismatch',
)
})
test('状态列表里的虚拟值「VIP 待补图」不可证真:不插不移', () => {
const filters = parseAdminWorkOrderListFilters(
buildQueryKey({ statuses: 'vip_evidence_pending' }),
)!
assert.equal(evaluateAdminWorkOrderRealtimeMatch(buildSnapshot(), filters), 'unknown')
// 与真实状态混选时,服务端是「真实状态命中 OR VIP 待补图命中」:
// 任何一行都可能通过 VIP 分支被救回来,所以整条状态筛选都不可证伪,一律 unknown(宁可不删)。
const mixed = parseAdminWorkOrderListFilters(
buildQueryKey({ statuses: 'open,vip_evidence_pending' }),
)!
for (const rawStatus of ['open', 'accepted', 'cancelled']) {
assert.equal(
evaluateAdminWorkOrderRealtimeMatch(buildSnapshot({ rawStatus }), mixed),
'unknown',
`${rawStatus} 仍可能通过 VIP 分支命中,不能判为 mismatch`,
)
}
})
test('实时补丁:只认结构化 queryKey,旧的下标数组一律不猜(返回 null)', () => {
// 老结构:['admin-worker-platform-orders', statuses, keyword, workerId, categoryId, ...]
const legacyKey = ['admin-worker-platform-orders', '', '', 0, 0, '', 0, '', '', 1, 20, '', '']
assert.equal(parseAdminWorkOrderListFilters(legacyKey), null)
const parsed = parseAdminWorkOrderListFilters(
buildQueryKey({ page: 3, statuses: 'open,problem' }),
)
assert.ok(parsed)
assert.equal(parsed!.page, 3, 'page 必须按名字读出来(原来被读成 targetWorkOrderId → 恒为 1)')
assert.deepEqual(parsed!.statuses, ['open', 'problem'])
})
test('「内部寄售匹配」筛选下:普通单不会因为实时事件被插进列表', () => {
const key = buildQueryKey({ acceptanceMode: 'internal_consignment', page: 1 })
const filters = parseAdminWorkOrderListFilters(key)!
assert.ok(filters)
// 验收模式筛选前端无法证真 → unknown:既不能插入,也不能把已有行移除
const ordinary = buildSnapshot({ workOrderId: 999, material: { collect: {} } })
assert.equal(evaluateAdminWorkOrderRealtimeMatch(ordinary, filters), 'unknown')
const envelope = buildEnvelope([buildSnapshot({ workOrderId: 100 })])
const patched = patchAdminWorkOrderListItems(envelope, ordinary, { remove: false, insert: false })
assert.equal(patched, envelope, '无法证真时不应改动列表')
assert.equal(itemsOfWorkOrders(patched).length, 1)
})
test('第 2 页及以后绝不插入(原来任何页都被当成第 1 页,是乱跳的直接原因)', () => {
const filters = parseAdminWorkOrderListFilters(buildQueryKey({ page: 2 }))!
assert.equal(filters.page, 2)
assert.equal(evaluateAdminWorkOrderRealtimeMatch(buildSnapshot(), filters), 'match')
// 即使判定为 match,page !== 1 也不允许插到顶部
const page2Items = [buildSnapshot({ workOrderId: 80 }), buildSnapshot({ workOrderId: 79 })]
const fresh = buildSnapshot({ workOrderId: 5000 })
const envelope = buildEnvelope(page2Items)
const patched = patchAdminWorkOrderListItems(envelope, fresh, {
remove: false,
insert: filters.page === 1 && shouldPrependAdminWorkOrder(page2Items, fresh),
})
assert.equal(patched, envelope, '第 2 页不应被插入新行')
})
test('历史老单(id 小于首行)不会被插到列表顶部', () => {
const items = [buildSnapshot({ workOrderId: 100 }), buildSnapshot({ workOrderId: 99 })]
const legacy = buildSnapshot({ workOrderId: 42 })
assert.equal(shouldPrependAdminWorkOrder(items, legacy), false)
const envelope = buildEnvelope(items)
const patched = patchAdminWorkOrderListItems(envelope, legacy, { remove: false, insert: false })
assert.equal(patched, envelope, '排序不该变化:老单不进当前页')
})
test('真正的新单(id 大于首行)可以插到顶部,并同步分页总数', () => {
const items = [buildSnapshot({ workOrderId: 100 })]
const fresh = buildSnapshot({ workOrderId: 101 })
assert.equal(shouldPrependAdminWorkOrder(items, fresh), true)
const patched = patchAdminWorkOrderListItems(buildEnvelope(items), fresh, {
remove: false,
insert: true,
})
assert.deepEqual(
itemsOfWorkOrders(patched).map((item) => item.workOrderId),
[101, 100],
)
assert.equal(totalOf(patched), 125)
})
test('已存在的行只做原地替换,不改变位置', () => {
const items = [
buildSnapshot({ workOrderId: 100 }),
buildSnapshot({ workOrderId: 99, status: 'open' }),
buildSnapshot({ workOrderId: 98 }),
]
const updated = buildSnapshot({ workOrderId: 99, status: 'in_progress' })
const patched = patchAdminWorkOrderListItems(buildEnvelope(items), updated, {
remove: false,
insert: true,
})
assert.deepEqual(
itemsOfWorkOrders(patched).map((item) => item.workOrderId),
[100, 99, 98],
'原地替换不能把它提到第一位',
)
assert.equal(totalOf(patched), 124, '行数没变,总数不能变')
})
test('状态筛选:已不命中的工单会被移除并扣减总数', () => {
const filters = parseAdminWorkOrderListFilters(buildQueryKey({ statuses: 'open' }))!
const accepted = buildSnapshot({ workOrderId: 100, status: 'accepted' })
assert.equal(evaluateAdminWorkOrderRealtimeMatch(accepted, filters), 'mismatch')
const patched = patchAdminWorkOrderListItems(
buildEnvelope([
buildSnapshot({ workOrderId: 100, status: 'open' }),
buildSnapshot({ workOrderId: 99 }),
]),
accepted,
{ remove: true, insert: false },
)
assert.deepEqual(
itemsOfWorkOrders(patched).map((item) => item.workOrderId),
[99],
)
assert.equal(totalOf(patched), 123)
})
test('打手 / 分类 / 工单号 / 创建日期筛选的证伪能力', () => {
const filters = parseAdminWorkOrderListFilters(
buildQueryKey({
workerId: 7,
categoryId: 12,
targetWorkOrderId: 0,
createdFrom: '2026-09-01',
createdTo: '2026-09-30',
}),
)!
// 指派给别的打手 → mismatch
assert.equal(
evaluateAdminWorkOrderRealtimeMatch(buildSnapshot({ worker: { workerId: 8 } }), filters),
'mismatch',
)
// 拼单参与者是目标打手 → 命中(后端 workerId 筛选也认拼单参与者)
assert.equal(
evaluateAdminWorkOrderRealtimeMatch(
buildSnapshot({ sharingWorkers: [{ workerId: 7 }], categoryId: 12 }),
filters,
),
'match',
)
// 分类不等:可能命中所选分类的**子分类**,而快照不带 parentCategoryId → 不能证伪,只能 unknown
assert.equal(
evaluateAdminWorkOrderRealtimeMatch(
buildSnapshot({ worker: { workerId: 7 }, categoryId: 99 }),
filters,
),
'unknown',
)
// 创建时间在范围外 → mismatch
assert.equal(
evaluateAdminWorkOrderRealtimeMatch(
buildSnapshot({
worker: { workerId: 7 },
categoryId: 12,
createdAt: '2026-08-01T00:00:00.000Z',
}),
filters,
),
'mismatch',
)
})
test('关键词 / 赠送阶段 / 打手等级筛选:无法证真,只原地更新不插不移', () => {
for (const override of [
{ keyword: '暗潮' },
{ giftPhase: 'gift_ready' },
{ workerLevelIds: '1,2' },
] as const) {
const filters = parseAdminWorkOrderListFilters(buildQueryKey(override))!
assert.equal(
evaluateAdminWorkOrderRealtimeMatch(buildSnapshot(), filters),
'unknown',
`${JSON.stringify(override)} 应判为 unknown`,
)
}
// 但「状态已经不匹配」这种可以证伪的条件优先级更高,仍要能移除
const filters = parseAdminWorkOrderListFilters(
buildQueryKey({ keyword: '暗潮', statuses: 'open' }),
)!
assert.equal(
evaluateAdminWorkOrderRealtimeMatch(buildSnapshot({ status: 'accepted' }), filters),
'mismatch',
)
})