import assert from 'node:assert/strict' import test from 'node:test' import { evaluateAdminWorkOrderRealtimeMatch, isExplicitOrderNoSearch, normalizeSearchText, parseAdminWorkOrderListFilters, patchAdminWorkOrderListItems, shouldPrependAdminWorkOrder, type AdminWorkOrderListFilters, } from '../src/utils/admin-realtime-work-orders.ts' /** 复刻 WorkOrdersPanel 的结构化 queryKey。 */ function buildQueryKey(overrides: Partial = {}) { const filters: AdminWorkOrderListFilters = { statuses: '', keyword: '', shopNames: '', 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, shopNames: filters.shopNames, 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 = {}) { 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[], total = items.length) { // 统计里的状态分布用运营态口径(与后端 statistics 一致),这里直接从行里推出来 const statusCounts: Record = {} for (const item of items) { const status = String((item as { status?: unknown })?.status || '') if (status) statusCounts[status] = (statusCounts[status] || 0) + 1 } return { ok: true, data: { items, pagination: { total, page: 1, pageSize: 20 }, statistics: { totalCount: total, statusCounts, userPaymentAmount: 43000, rewardAmount: 41000, }, }, } } 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 } function statisticsOf(envelope: unknown): { totalCount: number statusCounts: Record } { return ( envelope as { data: { statistics: { totalCount: number; statusCounts: Record } } } ).data.statistics } test('按工单号搜到的行,状态变化后不能被实时补丁删掉(线上"统计还有 1 单、列表暂无数据"的成因)', () => { const filters = parseAdminWorkOrderListFilters(buildQueryKey({ keyword: 'WO26093017345168' }))! // 旧逻辑只拿商品名/平台单号比关键词,工单号搜出来的行会被判成不匹配 → 直接删掉 assert.equal( evaluateAdminWorkOrderRealtimeMatch(buildSnapshot({ status: 'in_progress' }), filters), 'unknown', '关键词筛选无法证真,只能原地更新,绝不能删行', ) const row = buildSnapshot({ workOrderId: 79325, status: 'pending_material' }) const envelope = buildEnvelope([row]) const patched = patchAdminWorkOrderListItems(envelope, row, { remove: false, insert: false }) assert.notEqual(patched, envelope, '同一行要能原地刷新') assert.equal(itemsOfWorkOrders(patched).length, 1, '行必须还在') assert.equal(totalOf(patched), 1) assert.equal(statisticsOf(patched).totalCount, 1, '统计不能和列表打架') }) test('实时补丁要同时维护筛选统计,避免"统计说有、列表没有"', () => { // ① 原地替换且状态变化:总数不变,状态分布跟着走 const openRow = buildSnapshot({ workOrderId: 100, status: 'open', rawStatus: 'open' }) const envelope = buildEnvelope([openRow]) assert.deepEqual(statisticsOf(envelope).statusCounts, { open: 1 }) const grabbed = buildSnapshot({ workOrderId: 100, status: 'in_progress', rawStatus: 'in_progress', }) const patched = patchAdminWorkOrderListItems(envelope, grabbed, { remove: false, insert: false }) assert.equal(statisticsOf(patched).totalCount, 1) assert.deepEqual(statisticsOf(patched).statusCounts, { open: 0, in_progress: 1 }) // ② 命中状态筛选的新单插入:总数与状态分布都要 +1 const inserted = patchAdminWorkOrderListItems( buildEnvelope([buildSnapshot({ workOrderId: 200, status: 'open' })]), buildSnapshot({ workOrderId: 201, status: 'in_progress' }), { remove: false, insert: true }, ) assert.equal(statisticsOf(inserted).totalCount, 2) assert.deepEqual(statisticsOf(inserted).statusCounts, { open: 1, in_progress: 1 }) // ③ 已不命中的行被移除:总数与它原来的状态计数都要 -1 const removed = patchAdminWorkOrderListItems( buildEnvelope([ buildSnapshot({ workOrderId: 300, status: 'open', rawStatus: 'open' }), buildSnapshot({ workOrderId: 301, status: 'in_progress', rawStatus: 'in_progress' }), ]), buildSnapshot({ workOrderId: 300, status: 'accepted', rawStatus: 'accepted' }), { remove: true, insert: false }, ) assert.equal(itemsOfWorkOrders(removed).length, 1) assert.equal(statisticsOf(removed).totalCount, 1) assert.deepEqual(statisticsOf(removed).statusCounts, { open: 0, in_progress: 1 }) }) 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, 124), 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, 124), 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 })], 124, ), 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', ) }) /** * 显式单号检索的护栏。 * * 生产现象:按工单号搜出来的那一行,在订单状态变化后会从结果里消失,必须整页刷新才回来。 * 成因是实时补丁的判定链里,「状态/接单人员/日期范围」这些可证伪条件排在关键词护栏之前, * 一旦它们判定 mismatch 就允许把行删掉。而面板搜索时只清空了状态,没清接单人员/日期范围, * 所以护栏还会被这些条件绕过。 */ test('显式单号检索:状态流转后不得把正在看的单删掉', () => { const cases: Array> = [ { keyword: 'WO26100217194691', statuses: 'in_progress' }, { keyword: 'WO26100217194691', workerId: 7 }, { keyword: 'WO26100217194691', createdFrom: '2026-10-05' }, { keyword: 'OD26100217', statuses: 'open' }, { keyword: '2627502003065030', statuses: 'open' }, ] for (const override of cases) { const filters = parseAdminWorkOrderListFilters(buildQueryKey(override))! assert.equal( evaluateAdminWorkOrderRealtimeMatch(buildSnapshot({ status: 'accepted' }), filters), 'unknown', `${JSON.stringify(override)} 应判为 unknown(只原地更新),否则状态一变就搜不到`, ) } }) test('模糊关键词不享受单号护栏:状态可证伪时仍按原设计移除', () => { const filters = parseAdminWorkOrderListFilters( buildQueryKey({ keyword: '暗潮', statuses: 'open' }), )! assert.equal( evaluateAdminWorkOrderRealtimeMatch(buildSnapshot({ status: 'accepted' }), filters), 'mismatch', ) }) /** * 服务端 operation='remove' 曾经直接短路成 mismatch,绕过了上面的单号护栏, * 等于「显式单号检索不删行」这条规则在最需要它的路径上失效。现在交给判定函数统一处理。 */ test('服务端 remove 事件不得绕过显式单号护栏;模糊关键词仍可被移除', () => { const explicit = parseAdminWorkOrderListFilters( buildQueryKey({ keyword: 'WO26101213031939', statuses: 'open' }), )! assert.equal( evaluateAdminWorkOrderRealtimeMatch(buildSnapshot({ status: 'accepted' }), explicit, { operation: 'remove', }), 'unknown', '按单号搜出来的行,即便服务端发 remove 也不能删(工单并未删除)', ) const fuzzy = parseAdminWorkOrderListFilters(buildQueryKey({ keyword: '暗潮' }))! assert.equal( evaluateAdminWorkOrderRealtimeMatch(buildSnapshot(), fuzzy, { operation: 'remove' }), 'mismatch', '普通关键词仍要保留「服务端说没了就移除」的语义', ) }) test('isExplicitOrderNoSearch 只认单号形态,普通关键词不误判', () => { for (const orderNo of [ // 现役前缀 WK 'WK26101213031939', 'wk26101213031939', // 历史前缀 WO 'WO26100217194691', 'wo12345678', // 把前缀的 O 看成 0(线上真实案例:用平台号搜得到、用工单号搜不到) 'W026101213031939', 'OD26100217', '2627502003065030', ]) { assert.equal(isExplicitOrderNoSearch(orderNo), true, `${orderNo} 应识别为单号`) } for (const keyword of ['暗潮', '无限潮汐', 'WO1', 'WK', 'W0', '', 'abcd']) { assert.equal(isExplicitOrderNoSearch(keyword), false, `${keyword} 不应识别为单号`) } }) test('isExplicitOrderNoSearch 容错:全角字符与零宽字符不影响识别', () => { assert.equal(isExplicitOrderNoSearch(' WK26101213031939 '), true) assert.equal(isExplicitOrderNoSearch('WK26101213031939\u200b'), true) assert.equal(isExplicitOrderNoSearch('\ufeffWO26101213031939'), true) // 归一化口径与后端 normalizeLookupKeyword 一致 assert.equal(normalizeSearchText(' WO26101213031939\u200d '), 'WO26101213031939') }) test('关联单号(寄卖/仓库/售后/履约)也算显式单号检索', () => { // 客服界面「只认寄卖单号」,粘进来的就是 SUB;漏判会让搜到的单被状态页签挡掉 for (const orderNo of [ 'SUB26100301127318', 'INV26100310477194', 'ASde06bcb25aca', 'DTb946808bcd5b', // 大小写与空格容忍 ' sub26100301127318 ', ]) { assert.equal(isExplicitOrderNoSearch(orderNo), true, `${orderNo} 应识别为单号`) } // 前缀相近但不是单号的普通词,不能被误判(误判会清掉客服的筛选条件) for (const keyword of ['SUBWAY', 'ASO优化', 'DT', 'INV', 'SUB123', 'ASzzz']) { assert.equal(isExplicitOrderNoSearch(keyword), false, `${keyword} 不应识别为单号`) } })