diff --git a/apps/backend/scripts/check-sql-guard.ts b/apps/backend/scripts/check-sql-guard.ts index d64bad14..cba7b262 100644 --- a/apps/backend/scripts/check-sql-guard.ts +++ b/apps/backend/scripts/check-sql-guard.ts @@ -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[] = [] diff --git a/apps/backend/src/db/migrations/074_admin_order_and_task_lookup_indexes.sql b/apps/backend/src/db/migrations/074_admin_order_and_task_lookup_indexes.sql new file mode 100644 index 00000000..3226f39f --- /dev/null +++ b/apps/backend/src/db/migrations/074_admin_order_and_task_lookup_indexes.sql @@ -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 + '支持外部订单按平台订单号前缀模式查找'; diff --git a/apps/backend/src/repositories/order-repo.ts b/apps/backend/src/repositories/order-repo.ts index 3584358e..944d14e2 100644 --- a/apps/backend/src/repositories/order-repo.ts +++ b/apps/backend/src/repositories/order-repo.ts @@ -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) { diff --git a/apps/backend/src/repositories/task-list-filter.test.ts b/apps/backend/src/repositories/task-list-filter.test.ts index 8ab98122..ce4424e4 100644 --- a/apps/backend/src/repositories/task-list-filter.test.ts +++ b/apps/backend/src/repositories/task-list-filter.test.ts @@ -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']) +}) diff --git a/apps/backend/src/repositories/task-repo.ts b/apps/backend/src/repositories/task-repo.ts index 9105597d..ad4a2db6 100644 --- a/apps/backend/src/repositories/task-repo.ts +++ b/apps/backend/src/repositories/task-repo.ts @@ -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( `${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) { diff --git a/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts index 58960a70..8a39c9c7 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts @@ -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)