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 3cf59912..2e5be9d3 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 @@ -295,6 +295,37 @@ export async function listWorkOrders({ : 'wo.id DESC' const effectiveOrderBy = pinnedFirst ? `wo.pinned_at DESC NULLS LAST, ${orderBy}` : orderBy params.push(pageSize, offset) + + // 打手端「我的订单」的排序是表达式(依赖打手参数与拼单表),**没有索引能支撑**, + // 规划器只能把该打手全部可见单连同宽列(含 1.2KB 的 material_json)物化后排序。 + // 生产实测:名下 6600 单的打手首访 1112ms、每次读约 60MB(列表缓存 2 秒一失效, + // 所以这个代价每分钟会重复付好几次)。这里改成"先只取当前页的 id,再按 id 取详情": + // 排序阶段只带窄列,宽列只回表 20 行。语义(行集、顺序、总数)完全不变。 + if (sort === 'worker_claimed_at_desc' && workerSharingId && !pinnedFirst) { + const idsResult = await query<{ id: number }>( + `SELECT wo.id + FROM work_orders wo + ${joins} + ${whereClause} + ORDER BY ${effectiveOrderBy} + LIMIT $${params.length - 1} OFFSET $${params.length}`, + params, + ) + const orderedIds = idsResult.rows.map((row) => Number(row.id)) + const [itemsResult, total] = await Promise.all([ + orderedIds.length === 0 + ? Promise.resolve({ rows: [] as WorkOrderRow[] }) + : query(`${WORK_ORDER_SELECT} WHERE wo.id = ANY($1::bigint[])`, [orderedIds]), + includeTotal ? countWorkOrdersWithWhere(joins, whereClause, countParams) : Promise.resolve(0), + ]) + // ANY() 不保证顺序,按 id 顺序还原分页结果 + const rowsById = new Map(itemsResult.rows.map((row) => [Number(row.id), row])) + const items = orderedIds + .map((id) => rowsById.get(id)) + .filter((row): row is WorkOrderRow => Boolean(row)) + return { items, total } + } + // 宽详情只查当前页;不将 COUNT 窗口放进详情查询,避免 LIMIT 前处理全量 JSON。 const [itemsResult, total] = await Promise.all([ query( diff --git a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts index 4793a55a..ae70f176 100644 --- a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts +++ b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts @@ -2787,3 +2787,24 @@ test('统计的「状态+日期范围」组合必须有 created_at 键的覆盖 '迁移在事务内执行,不能使用 CONCURRENTLY', ) }) + +test('打手端「我的订单」必须走"先取 id 再取详情"的延迟分页', async () => { + const { readFile } = await import('node:fs/promises') + const src = await readFile( + new URL('../../repositories/worker-platform/work-order-query-repo.ts', import.meta.url), + 'utf8', + ) + assert.ok( + src.includes("sort === 'worker_claimed_at_desc' && workerSharingId && !pinnedFirst"), + '缺少打手排序的延迟分页分支:该排序是表达式,无索引可支撑,会物化全部可见单的宽列', + ) + assert.ok( + src.includes('SELECT wo.id'), + '第一阶段必须只取 id(窄投影),否则排序仍会读 material_json', + ) + assert.ok( + src.includes('WHERE wo.id = ANY($1::bigint[])'), + '第二阶段必须按 id 批量取详情,而不是重新跑一遍带排序的宽查询', + ) + assert.ok(src.includes('rowsById.get(id)'), 'ANY() 不保证顺序,必须按 id 顺序还原分页结果') +})