diff --git a/apps/backend/src/repositories/worker-platform-repo.test.ts b/apps/backend/src/repositories/worker-platform-repo.test.ts index c8aa0633..487a6926 100644 --- a/apps/backend/src/repositories/worker-platform-repo.test.ts +++ b/apps/backend/src/repositories/worker-platform-repo.test.ts @@ -173,6 +173,34 @@ test('普通关键词仍然走 trgm 表达式模糊搜索', () => { assert.deepEqual(query.params, ['%指挥官密钥%']) }) +test('「仅问题单」检索走短路分支,从 pending 表反查而非枚举可见集', () => { + const query = buildWorkOrderWhere({ workerSharingId: 7, statuses: ['problem'] }) + + // 短路形状:sargable 的直查 + 从两张极小的 pending 表反查 + assert.match(query.whereClause, /wo\.status = 'problem'/) + assert.match(query.whereClause, /FROM worker_order_cancel_requests/) + assert.match(query.whereClause, /FROM worker_order_feedbacks/) + assert.match(query.whereClause, /status = 'pending'/) + // 不得再生成完整派生表达式(那会强制对该打手全部可见单逐行求值) + assert.doesNotMatch(query.whereClause, /THEN 'problem'\s+ELSE CASE/) + // 第一个 7 来自 worker_visible 连接,第二个 7 是短路分支的打手 id + assert.deepEqual(query.params, [7, 7]) +}) + +test('问题单与其它状态混合时不得短路,必须走完整派生表达式', () => { + const query = buildWorkOrderWhere({ workerSharingId: 7, statuses: ['problem', 'in_progress'] }) + + assert.match(query.whereClause, /= ANY\(\$\d+::text\[\]\)/, '混合状态必须用完整表达式') + // 完整表达式的 EXISTS 里也会出现这两张表,用短路分支独有的「SELECT work_order_id + UNION」 + // 形状来判别,确保混合状态没有误入短路 + assert.doesNotMatch( + query.whereClause, + /SELECT work_order_id\s+FROM worker_order_cancel_requests/, + '混合状态误入短路分支会导致结果集不完整', + ) + assert.deepEqual(query.params, [7, ['problem', 'in_progress']]) +}) + test('单号检索分支不得回退成裸列模糊匹配(源码级防退化守卫)', async () => { const { readFile } = await import('node:fs/promises') const src = await readFile( 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 27aaef3b..8a2461c7 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 @@ -192,12 +192,19 @@ function buildRelatedWorkOrderLookupFilter( } /** - * 打手订单个人状态:拼单按个人份额状态计算。 - * 传入 workerId 时,代练中但存在待审核取消申请或未处理反馈的订单归入问题单, - * 客服驳回/解决后自动回到代练中(计算型归类,无状态迁移)。 + * 是否为「仅问题单」筛选。前端问题单页签固定传单个 problem; + * 一旦混入其它状态就必须走完整派生表达式,不能短路。 */ -export function workerOrderStatusExpression(workerId = 0, workerShareAlias = 'worker_share') { - const baseExpression = `CASE +function isProblemOnlyStatusFilter(statuses: string[]): boolean { + return statuses.length === 1 && statuses[0] === 'problem' +} + +/** + * 打手订单基础状态:拼单按个人份额状态计算(不含"问题单"派生)。 + * 拆出来是因为「仅问题单」检索可以用它做短路,不必派生完整表达式。 + */ +export function workerOrderBaseStatusExpression(workerShareAlias = 'worker_share') { + return `CASE WHEN wo.status IN ('accepted', 'cancelled', 'problem') THEN wo.status ELSE COALESCE(CASE ${workerShareAlias}.status WHEN 'joined' THEN 'in_progress' @@ -207,6 +214,15 @@ export function workerOrderStatusExpression(workerId = 0, workerShareAlias = 'wo ELSE wo.status END, wo.status) END` +} + +/** + * 打手订单个人状态:拼单按个人份额状态计算。 + * 传入 workerId 时,代练中但存在待审核取消申请或未处理反馈的订单归入问题单, + * 客服驳回/解决后自动回到代练中(计算型归类,无状态迁移)。 + */ +export function workerOrderStatusExpression(workerId = 0, workerShareAlias = 'worker_share') { + const baseExpression = workerOrderBaseStatusExpression(workerShareAlias) const normalizedWorkerId = Math.max(0, Math.floor(Number(workerId) || 0)) if (normalizedWorkerId <= 0) return baseExpression const pendingHandlingExists = `( @@ -963,16 +979,45 @@ export function buildWorkOrderWhere({ filters.push(`(${vipEvidencePendingWhere})`) } } else if (normalizedStatuses.length > 0) { - params.push(normalizedStatuses) - const statusesPlaceholder = `$${params.length}` - if (workerSharingId) { - filters.push( - `${workerOrderStatusExpression(workerSharingId)} = ANY(${statusesPlaceholder}::text[])`, - ) + if (workerSharingId && isProblemOnlyStatusFilter(normalizedStatuses)) { + // 「仅问题单」短路:问题单 = wo.status 直接是 problem,或 进行中 且挂着 + // 待审核取消申请/未处理反馈。两条 pending 表极小(生产实测常为 0 行), + // 从它们出发反查,避免为了一个通常为空的结果枚举该打手全部可见单再逐行求值 + // 完整 CASE(重打手 8,586 单 × 逐行探测 ≈ 233MB 缓冲读)。 + // 注意左臂必须写成 sargable 的 wo.status = 'problem':完整表达式里 + // 基础状态等于 'problem' 当且仅当 wo.status = 'problem'(派生分支没有 problem 映射)。 + params.push(workerSharingId) + const workerIdPlaceholder = `$${params.length}` + const baseExpression = workerOrderBaseStatusExpression('worker_share') + filters.push(`( + wo.status = 'problem' + OR ( + ${baseExpression} = 'in_progress' + AND wo.id IN ( + SELECT work_order_id + FROM worker_order_cancel_requests + WHERE worker_id = ${workerIdPlaceholder} + AND status = 'pending' + UNION + SELECT work_order_id + FROM worker_order_feedbacks + WHERE worker_id = ${workerIdPlaceholder} + AND status = 'pending' + ) + ) + )`) } else { - filters.push( - `${useOperationalStatus ? adminOperationalStatusExpression('wo', 'operational_share_summary') : 'wo.status'} = ANY(${statusesPlaceholder}::text[])`, - ) + params.push(normalizedStatuses) + const statusesPlaceholder = `$${params.length}` + if (workerSharingId) { + filters.push( + `${workerOrderStatusExpression(workerSharingId)} = ANY(${statusesPlaceholder}::text[])`, + ) + } else { + filters.push( + `${useOperationalStatus ? adminOperationalStatusExpression('wo', 'operational_share_summary') : 'wo.status'} = ANY(${statusesPlaceholder}::text[])`, + ) + } } } if (workOrderId) {