perf: optimize worker order query hot paths

This commit is contained in:
yml2213
2026-08-30 15:30:46 +08:00
parent 7b9794665c
commit 79d8f84ac9
3 changed files with 71 additions and 44 deletions
+9
View File
@@ -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[] = []
@@ -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 全表扫描';
@@ -74,6 +74,16 @@ type WorkOrderStatisticsRow = {
refund_amount: number | string
}
type TimeoutCountCacheEntry = {
expiresAt: number
counts: Map<number, number>
}
// 后台打手列表会在短时间内重复请求同一页;超时次数只是展示字段,允许短 TTL。
const TIMEOUT_COUNT_CACHE_TTL_MS = 5_000
const TIMEOUT_COUNT_CACHE_MAX_ENTRIES = 256
const timeoutCountCache = new Map<string, TimeoutCountCacheEntry>()
/** 后台按母工单拼单参与情况计算业务状态,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<WorkerOrderOverviewRow>(
`
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<Map<number, number>> {
const counts = new Map<number, number>()
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<number, number>()
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
}