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',
|
'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[] = []
|
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
|
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 只代表大厅生命周期。 */
|
/** 后台按母工单拼单参与情况计算业务状态,open 只代表大厅生命周期。 */
|
||||||
function adminOperationalStatusExpression(alias = 'wo', shareSummaryAlias = '') {
|
function adminOperationalStatusExpression(alias = 'wo', shareSummaryAlias = '') {
|
||||||
if (shareSummaryAlias) {
|
if (shareSummaryAlias) {
|
||||||
@@ -367,7 +377,30 @@ export async function getWorkerOrderOverview(workerId: number | string): Promise
|
|||||||
const normalizedWorkerId = Number(workerId)
|
const normalizedWorkerId = Number(workerId)
|
||||||
const result = await query<WorkerOrderOverviewRow>(
|
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
|
SELECT
|
||||||
wo.id,
|
wo.id,
|
||||||
wo.status AS wo_status,
|
wo.status AS wo_status,
|
||||||
@@ -380,51 +413,11 @@ export async function getWorkerOrderOverview(workerId: number | string): Promise
|
|||||||
END AS worker_settlement_amount,
|
END AS worker_settlement_amount,
|
||||||
wo.acceptance_json
|
wo.acceptance_json
|
||||||
FROM work_orders wo
|
FROM work_orders wo
|
||||||
|
INNER JOIN visible_orders vo ON vo.work_order_id = wo.id
|
||||||
LEFT JOIN work_order_shares wos
|
LEFT JOIN work_order_shares wos
|
||||||
ON wos.work_order_id = wo.id
|
ON wos.work_order_id = wo.id
|
||||||
AND wos.worker_id = $1
|
AND wos.worker_id = $1
|
||||||
AND wos.status != 'cancelled'
|
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 (
|
worker_orders AS (
|
||||||
SELECT
|
SELECT
|
||||||
@@ -588,9 +581,17 @@ export async function countWorkerActiveOrdersWithClient(
|
|||||||
export async function countTimeoutEventsByWorkerIds(
|
export async function countTimeoutEventsByWorkerIds(
|
||||||
workerIds: number[],
|
workerIds: number[],
|
||||||
): Promise<Map<number, number>> {
|
): Promise<Map<number, number>> {
|
||||||
const counts = new Map<number, number>()
|
|
||||||
const uniqueIds = [...new Set(workerIds.map((id) => Number(id)).filter((id) => id > 0))]
|
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 }>(
|
const result = await query<{ worker_id: string; total: number }>(
|
||||||
`
|
`
|
||||||
SELECT payload_json->>'workerId' AS worker_id, COUNT(*)::int AS total
|
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))
|
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
|
return counts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user