diff --git a/apps/backend/scripts/check-sql-guard.ts b/apps/backend/scripts/check-sql-guard.ts index 5817b7e9..859fcb76 100644 --- a/apps/backend/scripts/check-sql-guard.ts +++ b/apps/backend/scripts/check-sql-guard.ts @@ -45,6 +45,15 @@ const checks: Array<{ name: string; file: string; patterns: string[] }> = [ 'DROP INDEX IF EXISTS idx_work_product_match_logs_created_at', ], }, + { + name: '打手超时事件统计索引', + file: path.join(BACKEND_ROOT, 'src/db/migrations/068_timeout_event_worker_index.sql'), + patterns: [ + 'idx_work_order_events_timeout_worker_id', + "payload_json->>'workerId'", + "WHERE event_type LIKE 'timeout_%'", + ], + }, ] const failures: string[] = [] diff --git a/apps/backend/src/db/migrations/068_timeout_event_worker_index.sql b/apps/backend/src/db/migrations/068_timeout_event_worker_index.sql new file mode 100644 index 00000000..fc7653d2 --- /dev/null +++ b/apps/backend/src/db/migrations/068_timeout_event_worker_index.sql @@ -0,0 +1,9 @@ +-- 068_timeout_event_worker_index.sql —— 后台打手超时次数统计。 +-- countTimeoutEventsByWorkerIds 按 payload_json->>'workerId' 聚合; +-- 部分表达式索引避免每次后台列表请求扫描全部 work_order_events。 +CREATE INDEX IF NOT EXISTS idx_work_order_events_timeout_worker_id + ON work_order_events ((payload_json->>'workerId')) + WHERE event_type LIKE 'timeout_%'; + +COMMENT ON INDEX idx_work_order_events_timeout_worker_id IS + '支持按打手统计 timeout_* 工单事件,避免 JSON 全表扫描'; 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 aa566527..3c920410 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 @@ -74,6 +74,16 @@ type WorkOrderStatisticsRow = { refund_amount: number | string } +type TimeoutCountCacheEntry = { + expiresAt: number + counts: Map +} + +// 后台打手列表会在短时间内重复请求同一页;超时次数只是展示字段,允许短 TTL。 +const TIMEOUT_COUNT_CACHE_TTL_MS = 5_000 +const TIMEOUT_COUNT_CACHE_MAX_ENTRIES = 256 +const timeoutCountCache = new Map() + /** 后台按母工单拼单参与情况计算业务状态,open 只代表大厅生命周期。 */ function adminOperationalStatusExpression(alias = 'wo', shareSummaryAlias = '') { if (shareSummaryAlias) { @@ -367,7 +377,30 @@ export async function getWorkerOrderOverview(workerId: number | string): Promise const normalizedWorkerId = Number(workerId) const result = await query( ` - WITH worker_base AS ( + WITH visible_orders AS ( + SELECT wo.id AS work_order_id + FROM work_orders wo + WHERE wo.assigned_worker_id = $1 + UNION + SELECT wo.id AS work_order_id + FROM work_orders wo + WHERE wo.last_assigned_worker_id = $1 + AND ( + wo.status <> 'open' + OR EXISTS ( + SELECT 1 + FROM work_order_events timeout_event + WHERE timeout_event.work_order_id = wo.id + AND timeout_event.event_type = 'timeout_reopen' + ) + ) + UNION + SELECT wos.work_order_id + FROM work_order_shares wos + WHERE wos.worker_id = $1 + AND wos.status != 'cancelled' + ), + worker_base AS ( SELECT wo.id, wo.status AS wo_status, @@ -380,51 +413,11 @@ export async function getWorkerOrderOverview(workerId: number | string): Promise END AS worker_settlement_amount, wo.acceptance_json FROM work_orders wo + INNER JOIN visible_orders vo ON vo.work_order_id = wo.id LEFT JOIN work_order_shares wos ON wos.work_order_id = wo.id AND wos.worker_id = $1 AND wos.status != 'cancelled' - WHERE wo.assigned_worker_id = $1 - UNION - SELECT - wo.id, - wo.status, - wos.status, - wos.id IS NOT NULL, - CASE - WHEN wos.id IS NOT NULL THEN wos.share_reward - WHEN wo.assigned_worker_id = $1 THEN wo.reward_amount - ELSE 0 - END, - wo.acceptance_json - FROM work_orders wo - LEFT JOIN work_order_shares wos - ON wos.work_order_id = wo.id - AND wos.worker_id = $1 - AND wos.status != 'cancelled' - WHERE wo.last_assigned_worker_id = $1 - AND ( - wo.status <> 'open' - OR EXISTS ( - SELECT 1 - FROM work_order_events timeout_event - WHERE timeout_event.work_order_id = wo.id - AND timeout_event.event_type = 'timeout_reopen' - ) - ) - UNION - SELECT - wo.id, - wo.status, - wos.status, - TRUE, - wos.share_reward, - wo.acceptance_json - FROM work_orders wo - INNER JOIN work_order_shares wos - ON wos.work_order_id = wo.id - AND wos.worker_id = $1 - AND wos.status != 'cancelled' ), worker_orders AS ( SELECT @@ -588,9 +581,17 @@ export async function countWorkerActiveOrdersWithClient( export async function countTimeoutEventsByWorkerIds( workerIds: number[], ): Promise> { - const counts = new Map() const uniqueIds = [...new Set(workerIds.map((id) => Number(id)).filter((id) => id > 0))] - if (uniqueIds.length === 0) return counts + if (uniqueIds.length === 0) return new Map() + uniqueIds.sort((left, right) => left - right) + const cacheKey = uniqueIds.join(',') + const now = Date.now() + const cached = timeoutCountCache.get(cacheKey) + if (cached && cached.expiresAt > now) { + return new Map(cached.counts) + } + + const counts = new Map() const result = await query<{ worker_id: string; total: number }>( ` SELECT payload_json->>'workerId' AS worker_id, COUNT(*)::int AS total @@ -609,6 +610,14 @@ export async function countTimeoutEventsByWorkerIds( counts.set(workerId, Number(row.total || 0)) } } + if (timeoutCountCache.size >= TIMEOUT_COUNT_CACHE_MAX_ENTRIES) { + const oldestKey = timeoutCountCache.keys().next().value + if (oldestKey) timeoutCountCache.delete(oldestKey) + } + timeoutCountCache.set(cacheKey, { + expiresAt: now + TIMEOUT_COUNT_CACHE_TTL_MS, + counts: new Map(counts), + }) return counts }