优化后台任务与订单按订单号搜索性能

This commit is contained in:
yml2213
2026-09-04 21:58:44 +08:00
parent 84a8f138d4
commit 4c6c53d818
6 changed files with 167 additions and 45 deletions
+9
View File
@@ -75,6 +75,15 @@ const checks: Array<{
"material_json->'collect'->'fields'->>'gameId'",
],
},
{
name: '任务与订单平台订单号检索索引',
file: path.join(BACKEND_ROOT, 'src/db/migrations/074_admin_order_and_task_lookup_indexes.sql'),
patterns: [
'idx_fulfillment_tasks_platform_order_id',
'idx_fulfillment_tasks_task_no_pattern',
'idx_orders_platform_order_id',
],
},
]
const failures: string[] = []
@@ -0,0 +1,31 @@
-- 优化后台任务列表与订单列表按平台订单号、任务号搜索性能。
-- 修复原表仅有复合索引 (provider, platform, platform_order_id),导致单独按订单号查询时无法命中 B-Tree 索引的问题。
-- 1. 履约任务表:平台订单号独立索引与前缀模式索引
CREATE INDEX IF NOT EXISTS idx_fulfillment_tasks_platform_order_id
ON fulfillment_tasks(platform_order_id);
CREATE INDEX IF NOT EXISTS idx_fulfillment_tasks_platform_order_id_pattern
ON fulfillment_tasks(platform_order_id text_pattern_ops);
-- 2. 履约任务表:任务号独立索引与前缀模式索引
CREATE INDEX IF NOT EXISTS idx_fulfillment_tasks_task_no_pattern
ON fulfillment_tasks(task_no text_pattern_ops);
-- 3. 外部订单主表:平台订单号独立索引与前缀模式索引
CREATE INDEX IF NOT EXISTS idx_orders_platform_order_id
ON orders(platform_order_id);
CREATE INDEX IF NOT EXISTS idx_orders_platform_order_id_pattern
ON orders(platform_order_id text_pattern_ops);
COMMENT ON INDEX idx_fulfillment_tasks_platform_order_id IS
'支持履约任务按平台订单号快速精确查找';
COMMENT ON INDEX idx_fulfillment_tasks_platform_order_id_pattern IS
'支持履约任务按平台订单号前缀模式查找';
COMMENT ON INDEX idx_fulfillment_tasks_task_no_pattern IS
'支持履约任务按任务号前缀模式查找';
COMMENT ON INDEX idx_orders_platform_order_id IS
'支持外部订单按平台订单号快速精确查找';
COMMENT ON INDEX idx_orders_platform_order_id_pattern IS
'支持外部订单按平台订单号前缀模式查找';
+28 -17
View File
@@ -228,24 +228,35 @@ export async function listOrdersWithExecutor(
const params: unknown[] = []
if (keyword) {
params.push(`%${keyword}%`)
const keywordParam = `$${params.length}`
filters.push(`(
(
o.platform_order_id || ' ' || o.shop_id || ' ' || o.shop_name || ' ' ||
o.buyer_id || ' ' || o.buyer_name || ' ' || o.receiver_contact
) ILIKE ${keywordParam}
OR EXISTS (
SELECT 1
FROM order_items oi
WHERE oi.order_id = o.id
AND (
oi.sku_code || ' ' || oi.sku_name || ' ' ||
COALESCE(oi.item_snapshot_json->'kuaishouSendCode'->>'itemTitle', '') || ' ' ||
COALESCE(oi.item_snapshot_json->'kuaishouSendCode'->>'skuNick', '')
) ILIKE ${keywordParam}
const trimmedKeyword = String(keyword).trim()
const isPureOrderNo = /^\d{6,}$/.test(trimmedKeyword)
if (isPureOrderNo) {
params.push(trimmedKeyword)
const paramIndex = `$${params.length}`
filters.push(
`(o.platform_order_id = ${paramIndex} OR o.platform_order_id LIKE ${paramIndex} || '%')`,
)
)`)
} else {
params.push(`%${keyword}%`)
const keywordParam = `$${params.length}`
filters.push(`(
(
o.platform_order_id || ' ' || o.shop_id || ' ' || o.shop_name || ' ' ||
o.buyer_id || ' ' || o.buyer_name || ' ' || o.receiver_contact
) ILIKE ${keywordParam}
OR EXISTS (
SELECT 1
FROM order_items oi
WHERE oi.order_id = o.id
AND (
oi.sku_code || ' ' || oi.sku_name || ' ' ||
COALESCE(oi.item_snapshot_json->'kuaishouSendCode'->>'itemTitle', '') || ' ' ||
COALESCE(oi.item_snapshot_json->'kuaishouSendCode'->>'skuNick', '')
) ILIKE ${keywordParam}
)
)`)
}
}
if (provider) {
@@ -46,3 +46,29 @@ test('任务列表保留旧版分字段筛选参数', () => {
assert.match(result.whereClause, /kcts\.role_id.*ctx\.role_id.*ILIKE \$4/s)
assert.deepEqual(result.params, ['%26245%', '%DT-1%', 'SKU-1', '%10001%'])
})
test('任务列表关键词为纯数字时智能路由到平台订单号索引条件', () => {
const result = buildTaskListFilters({
keyword: '20247230034021572',
})
assert.match(
result.whereClause,
/ft\.platform_order_id = \$1 OR ft\.platform_order_id LIKE \$1 \|\| '%'/s,
)
assert.doesNotMatch(result.whereClause, /oi\.sku_code/i)
assert.doesNotMatch(result.whereClause, /item_snapshot_json/i)
assert.doesNotMatch(result.whereClause, /kcts/i)
assert.deepEqual(result.params, ['20247230034021572'])
})
test('任务列表关键词为 DT 任务号时智能路由到任务号索引条件', () => {
const result = buildTaskListFilters({
keyword: 'DT881aa66294cc',
})
assert.match(result.whereClause, /ft\.task_no = \$1 OR ft\.task_no LIKE \$1 \|\| '%'/s)
assert.doesNotMatch(result.whereClause, /oi\.sku_code/i)
assert.doesNotMatch(result.whereClause, /item_snapshot_json/i)
assert.deepEqual(result.params, ['DT881aa66294cc'])
})
+62 -26
View File
@@ -436,26 +436,46 @@ export async function listTasks({
const offset = (page - 1) * pageSize
const { whereClause, params } = buildTaskListFilters(filters)
const totalResult = await query<{ [column: string]: unknown; total: number }>(
`
SELECT COUNT(*)::int AS total
FROM fulfillment_tasks ft
LEFT JOIN order_items oi ON oi.id = ft.order_item_id
LEFT JOIN task_runtime_contexts ctx ON ctx.task_id = ft.id
LEFT JOIN kuaishou_cloud_task_states kcts ON kcts.task_id = ft.id
${whereClause}
`,
params,
)
params.push(pageSize)
params.push(offset)
const itemsParams = [...params, pageSize, offset]
const itemsResult = await query<TaskRow>(
`${buildTaskSelect('oi.sku_code, oi.sku_name, oi.quantity')}
LEFT JOIN order_items oi ON oi.id = ft.order_item_id
${whereClause}
ORDER BY ft.id DESC
LIMIT $${params.length - 1} OFFSET $${params.length}`,
LIMIT $${itemsParams.length - 1} OFFSET $${itemsParams.length}`,
itemsParams,
)
// 首页且返回记录不满一页时直接得出总数,彻底免除第二次全量 COUNT(*) 扫描
if (page === 1 && itemsResult.rows.length < pageSize) {
return {
items: itemsResult.rows,
total: itemsResult.rows.length,
}
}
// 仅在过滤条件真正引用子表字段时才进行关联,单表过滤时避免无谓的 3 表 LEFT JOIN 开销
const needsJoinOi = whereClause.includes('oi.')
const needsJoinCtx = whereClause.includes('ctx.')
const needsJoinKcts = whereClause.includes('kcts.')
const joinClauses: string[] = []
if (needsJoinOi) {
joinClauses.push('LEFT JOIN order_items oi ON oi.id = ft.order_item_id')
}
if (needsJoinCtx) {
joinClauses.push('LEFT JOIN task_runtime_contexts ctx ON ctx.task_id = ft.id')
}
if (needsJoinKcts) {
joinClauses.push('LEFT JOIN kuaishou_cloud_task_states kcts ON kcts.task_id = ft.id')
}
const totalResult = await query<{ [column: string]: unknown; total: number }>(
`
SELECT COUNT(*)::int AS total
FROM fulfillment_tasks ft
${joinClauses.join('\n')}
${whereClause}
`,
params,
)
@@ -480,17 +500,33 @@ export function buildTaskListFilters({
const params: unknown[] = []
if (keyword) {
params.push(`%${keyword}%`)
const keywordParam = `$${params.length}`
filters.push(`(
(ft.task_no || ' ' || ft.platform_order_id) ILIKE ${keywordParam}
OR oi.sku_code ILIKE ${keywordParam}
OR oi.sku_name ILIKE ${keywordParam}
OR COALESCE(oi.item_snapshot_json->'kuaishouSendCode'->>'itemTitle', '') ILIKE ${keywordParam}
OR COALESCE(oi.item_snapshot_json->'kuaishouSendCode'->>'skuNick', '') ILIKE ${keywordParam}
OR COALESCE(NULLIF(kcts.role_id, ''), ctx.role_id, '') ILIKE ${keywordParam}
OR COALESCE(NULLIF(kcts.role_name, ''), NULLIF(ctx.role_name, ''), ctx.nickname, '') ILIKE ${keywordParam}
)`)
const trimmedKeyword = String(keyword).trim()
const isPureOrderNo = /^\d{6,}$/.test(trimmedKeyword)
const isTaskNo = /^DT[a-zA-Z0-9_-]+$/i.test(trimmedKeyword)
if (isPureOrderNo) {
params.push(trimmedKeyword)
const paramIndex = `$${params.length}`
filters.push(
`(ft.platform_order_id = ${paramIndex} OR ft.platform_order_id LIKE ${paramIndex} || '%')`,
)
} else if (isTaskNo) {
params.push(trimmedKeyword)
const paramIndex = `$${params.length}`
filters.push(`(ft.task_no = ${paramIndex} OR ft.task_no LIKE ${paramIndex} || '%')`)
} else {
params.push(`%${keyword}%`)
const keywordParam = `$${params.length}`
filters.push(`(
(ft.task_no || ' ' || ft.platform_order_id) ILIKE ${keywordParam}
OR oi.sku_code ILIKE ${keywordParam}
OR oi.sku_name ILIKE ${keywordParam}
OR COALESCE(oi.item_snapshot_json->'kuaishouSendCode'->>'itemTitle', '') ILIKE ${keywordParam}
OR COALESCE(oi.item_snapshot_json->'kuaishouSendCode'->>'skuNick', '') ILIKE ${keywordParam}
OR COALESCE(NULLIF(kcts.role_id, ''), ctx.role_id, '') ILIKE ${keywordParam}
OR COALESCE(NULLIF(kcts.role_name, ''), NULLIF(ctx.role_name, ''), ctx.nickname, '') ILIKE ${keywordParam}
)`)
}
}
if (executorKey) {
@@ -816,8 +816,17 @@ export function buildWorkOrderWhere({
filters.push(`wo.id = $${params.length}`)
}
if (normalizedKeyword) {
params.push(`%${normalizedKeyword}%`)
filters.push(`${workOrderSearchExpression()} LIKE LOWER($${params.length})`)
const isPureOrderNo = /^\d{6,}$/.test(normalizedKeyword)
if (isPureOrderNo) {
params.push(normalizedKeyword)
const paramIndex = `$${params.length}`
filters.push(
`(wo.platform_order_id = ${paramIndex} OR wo.platform_order_id LIKE ${paramIndex} || '%')`,
)
} else {
params.push(`%${normalizedKeyword}%`)
filters.push(`${workOrderSearchExpression()} LIKE LOWER($${params.length})`)
}
}
if (workerId) {
params.push(workerId)