perf(worker): 单号检索收敛为可走索引的等值/前缀,并支持关联单号反查

问题:后台「按单号检索」在平台单号/工单号分支里 OR 了一支
`work_order_no ILIKE '%'||$n||'%'`。前置通配符非 sargable,任何 B-Tree 都用不上,
整条查询退化成 work_orders 的 Seq Scan。

生产实测(10.3 万行 / 572MB,pg_stat_statements 46 小时窗口):
  - 列表查询 + 统计查询用同一份 filters 并行执行,一次检索打两遍全表;
    两者合计占全库 66% 的磁盘读(1.1TB/46h),单次读 26,442 个块(约 211MB)、398ms、返回 1 行。
  - 收敛后同库同参数:等值 0.19ms,前缀 30ms(约 2000 倍)。
  - 真实统计查询端到端 760ms → 61.6ms。

改动:
1. 平台单号/工单号分支收敛为「等值 + 前缀」,与 order-repo / task-repo 既有写法一致;
   工单号含随机小写段(10.2 万行里 7.4 万行非全大写),保留 LOWER() 比较。
2. 迁移 090 补 text_pattern_ops 前缀索引:当前库 collation 为 en_US.utf8,
   前缀 LIKE 在默认 B-Tree 上会退化成全索引扫(实测 438ms)。
3. 新增关联单号反查分支:SUB/INV/AS/DT 不在 work_orders 上,
   经关联表反查工单 id,四条链路均命中唯一索引。
4. 前端 isExplicitOrderNoSearch 同步补齐(否则搜到的单会被状态页签挡掉、
   实时补丁还会把行删掉)。

验证:生产 EXPLAIN 逐条比对 + 14 项仓储单测 + 6 项分类器单测。
This commit is contained in:
yml2213
2026-10-03 14:27:32 +08:00
parent f0d745ea50
commit 90de6b9419
8 changed files with 289 additions and 6 deletions
@@ -1,7 +1,11 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { isPlatformOrderIdLookupKeyword, isWorkOrderNoLookupKeyword } from './platform-order-id.js'
import {
isPlatformOrderIdLookupKeyword,
isWorkOrderNoLookupKeyword,
resolveRelatedWorkOrderLookupKind,
} from './platform-order-id.js'
test('识别纯数字平台订单号', () => {
assert.equal(isPlatformOrderIdLookupKeyword('20247230034021572'), true)
@@ -29,3 +33,29 @@ test('拒绝普通关键词', () => {
assert.equal(isPlatformOrderIdLookupKeyword(''), false)
assert.equal(isWorkOrderNoLookupKeyword('皮肤店'), false)
})
test('识别需要反查工单的关联单号', () => {
// 生产真实值
assert.equal(resolveRelatedWorkOrderLookupKind('SUB26100301127318'), 'consignment_submission')
assert.equal(resolveRelatedWorkOrderLookupKind('INV26092619393769'), 'consignment_inventory')
assert.equal(resolveRelatedWorkOrderLookupKind('ASde06bcb25aca'), 'after_sales_case')
assert.equal(resolveRelatedWorkOrderLookupKind('DTb946808bcd5b'), 'fulfillment_task')
// 大小写不敏感,前后空格容忍
assert.equal(resolveRelatedWorkOrderLookupKind(' sub26100301127318 '), 'consignment_submission')
assert.equal(resolveRelatedWorkOrderLookupKind('asDE06BCB25ACA'), 'after_sales_case')
})
test('关联单号识别不得误伤工单号、平台单号与普通关键词', () => {
// WO 走自己的分支,不能被关联识别截胡
assert.equal(resolveRelatedWorkOrderLookupKind('WO26100314105769'), null)
// 平台单号(纯数字 / 带连字符)同样走自己的分支
assert.equal(resolveRelatedWorkOrderLookupKind('2627602160531614'), null)
assert.equal(resolveRelatedWorkOrderLookupKind('261003-072613926530472'), null)
// 前缀相近但不是单号的普通词
assert.equal(resolveRelatedWorkOrderLookupKind('SUBWAY'), null)
assert.equal(resolveRelatedWorkOrderLookupKind('ASO优化'), null)
assert.equal(resolveRelatedWorkOrderLookupKind('DT'), null)
assert.equal(resolveRelatedWorkOrderLookupKind(''), null)
assert.equal(resolveRelatedWorkOrderLookupKind(null), null)
})
@@ -14,3 +14,51 @@ export function isWorkOrderNoLookupKeyword(value: unknown): boolean {
const trimmed = String(value || '').trim()
return /^WO[0-9a-zA-Z]{4,}$/i.test(trimmed)
}
/**
* 关联单号类型:号码本身不在 work_orders 上,必须经关联表反查工单 id。
* - consignment_submission: 寄售线索号 SUB... → 产出的库存 → 被工单消耗后的工单
* - consignment_inventory: 寄售库存号 INV... → 被工单消耗后的工单
* - after_sales_case: 售后案号 AS... → 直接带 work_order_id
* - fulfillment_task: 履约任务号 DT... → work_orders.task_id
*
* 说明:这里按 schema 上的关联链路判定,不看当前数据填充率。
* 例如 SUB/INV 目前只有约一半能关联到工单(库存未被消耗前本就没有工单),
* DT 的 work_orders.task_id 目前全为 NULL(历史数据),
* 但链路是通的,后续数据填上后即可直接命中。
*/
export type RelatedWorkOrderLookupKind =
| 'consignment_submission'
| 'consignment_inventory'
| 'after_sales_case'
| 'fulfillment_task'
const RELATED_WORK_ORDER_PATTERNS: Array<{
kind: RelatedWorkOrderLookupKind
pattern: RegExp
}> = [
// SUB + 年月日时分 + 4 位随机,形如 SUB26100301127318
{ kind: 'consignment_submission', pattern: /^SUB\d{8,}$/i },
// INV + 年月日时分 + 4 位随机,形如 INV26100310477194
{ kind: 'consignment_inventory', pattern: /^INV\d{8,}$/i },
// AS + 12 位随机 hex,形如 ASde06bcb25aca
{ kind: 'after_sales_case', pattern: /^AS[0-9a-f]{8,}$/i },
// DT + 12 位随机 hex,形如 DTb946808bcd5b
{ kind: 'fulfillment_task', pattern: /^DT[0-9a-f]{8,}$/i },
]
/**
* 判断关键词是否是「需要反查工单」的关联单号。
*/
export function resolveRelatedWorkOrderLookupKind(
value: unknown,
): RelatedWorkOrderLookupKind | null {
const trimmed = String(value || '').trim()
if (!trimmed) return null
for (const item of RELATED_WORK_ORDER_PATTERNS) {
if (item.pattern.test(trimmed)) {
return item.kind
}
}
return null
}