## 线上定位到的真正根因
客服搜 `WO26101213031939` 搜不到,但用同单的平台号 `2628501763808879` 搜得到。
到生产库核对后确认:
- 库里真实值 `work_order_no = WO26101213031939`(hex 574f… = 字母 O),状态 pending_acceptance;
- 用字母 O 的 SQL 分支 → 1 行;用数字 0(`W026101213031939`)→ 0 行;
- 全库 127,906 单**全部**以 `WO` 开头、`W0` 开头 0 单、字母 `o` 只出现在第 2 位。
即:前缀里的字母 O 被输成数字 0(`WO` / `W0` 在界面字体里几乎无法区分),
落进通配符分支后必然为空 —— 与前面几轮的「筛选把单藏起来」是两个独立成因,
这也解释了为什么改筛选逻辑一直没解决。
## 修法
1. 生成端换前缀:generateWorkOrderNo 产出 `WK…`(K 与 0 不会看混),从源头消掉歧义;
2. 检索端同时兼容三种写法(历史 WO 号无需迁移):
- isWorkOrderNoLookupKeyword 放宽为 `^W[oOkK0]…`;
- 新增 buildWorkOrderNoKeywordVariants:W0→[W0, WO, WK]、WO→[WO, W0]、WK→[WK],
SQL 分支对每个候选下发「等值 + 前缀」条件(仍是索引条件,不会退化成全表扫);
- 前端 isExplicitOrderNoSearch 同步(否则面板不清理筛选、实时补丁护栏也不生效);
3. 顺手加一层归一化:NFKC(全角→半角)+ 去零宽字符,前后端同一口径
(normalizeLookupKeyword / normalizeSearchText),覆盖「粘贴带不可见字符」这类同族问题;
4. 寄卖匹配弹窗的占位提示更新为 WK... / WO...。
## 验证
- 后端 npm run check 全绿:692 tests(689 pass / 3 skip),新增
work-order-keyword-lookup.test.ts 直接断言 SQL 参数与「不得出现前置通配符」;
- 前端 npm run check 全绿:139 tests;
- 既有用例 worker-platform-repo.test.ts 的期望值同步为新形态(两条候选条件)。
484 lines
18 KiB
TypeScript
484 lines
18 KiB
TypeScript
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<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[], total = items.length) {
|
||
// 统计里的状态分布用运营态口径(与后端 statistics 一致),这里直接从行里推出来
|
||
const statusCounts: Record<string, number> = {}
|
||
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<string, number>
|
||
} {
|
||
return (
|
||
envelope as {
|
||
data: { statistics: { totalCount: number; statusCounts: Record<string, number> } }
|
||
}
|
||
).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<Partial<AdminWorkOrderListFilters>> = [
|
||
{ 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} 不应识别为单号`)
|
||
}
|
||
})
|