perf: optimize worker order query hot paths
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user