perf(admin): 打手等级筛选用 UNION 形式、概览加短缓存、静态配置接口加缓存头
三处都出自审计清单,改动互不重叠。
1) 打手等级筛选(生产实测 1119ms)
原写法是对 worker_users 的两个相关 EXISTS(接单人 / 拼单参与人),规划器无法用它
驱动索引,只能顺序扫完整张 work_orders。改成先把等级解析成打手 id
(worker_users(level_id) 有索引,等级只有 6 个、解析出几百个 id),再走索引。
注意:**不能**写成 `assigned_worker_id = ANY(...) OR id IN (...)` —— 实测 1852ms,
比原写法还慢(OR 右支要逐行回表取 id)。用 UNION 形式:
wo.id IN (SELECT id FROM work_orders WHERE assigned_worker_id = ANY(...) UNION
SELECT work_order_id FROM work_order_shares WHERE worker_id = ANY(...) AND status != 'cancelled')
实测 61ms(count),语义与原来完全一致(含"空数组=过滤掉全部")。
三个入口 listWorkOrders / countWorkOrders / getWorkOrderStatistics 都先解析;
未解析时保留原相关 EXISTS 兜底。
2) 平台概览短缓存(5 秒)
一次并发 5 条 count(4 条打 work_orders,一条是运营态 CASE),而池只有 8 条连接,
后台刷新时与其他请求互相争抢(抓包显示整批 9 个请求同时发出、它自身 167ms)。
看板计数滞后几秒无感,用 WorkerListCache 缓存并合并并发。
3) 三个静态配置接口加 HTTP 缓存头(private, max-age=300)
接单分类、快手行业店铺、自定义店铺此前没有任何 Cache-Control,每次刷新都真发一次
请求(抓包里 4 个这类请求)。带 private 是因为这些是带鉴权的后台接口。
**dev-mock/status 故意不加**:它是可切换状态,缓存会让"切了开关界面没变"。
验证:后端 547 项单测全绿(新增 4 项守卫,分别锁住 UNION 形式、三个入口都解析、
概览缓存、以及 dev-mock 不得加缓存)。
This commit is contained in:
@@ -2,9 +2,25 @@ import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||
import { countWorkOrders, listWorkerUsers } from '../../repositories/worker-platform/index.js'
|
||||
import { refreshStoredFileUrl } from '../file-storage/file-storage-service.js'
|
||||
import { publishWorkerHallRealtimeRefresh } from '../realtime/realtime-event-service.js'
|
||||
import { WorkerListCache } from './worker-list-cache.js'
|
||||
import { getWorkerHallConfig, saveWorkerHallConfig } from './worker-hall-config-service.js'
|
||||
|
||||
/**
|
||||
* 平台概览的短缓存。
|
||||
*
|
||||
* 这个接口一次并发 5 条 count(其中 4 条打 work_orders,一条还是运营态 CASE),
|
||||
* 而数据库连接池只有 8 条 —— 后台刷新页面时会和其他请求挤在一起(生产抓包显示
|
||||
* 整批 9 个请求同时发出,这个接口自身耗时 167ms 且独占 5 条连接)。
|
||||
* 它是看板计数,滞后几秒无感,因此按 5 秒缓存并合并并发请求。
|
||||
*/
|
||||
const ADMIN_SUMMARY_CACHE_TTL_MS = 5_000
|
||||
const adminSummaryCache = new WorkerListCache(ADMIN_SUMMARY_CACHE_TTL_MS, 4)
|
||||
|
||||
export async function getAdminWorkerPlatformSummary() {
|
||||
return adminSummaryCache.getOrSet('summary', computeAdminWorkerPlatformSummary)
|
||||
}
|
||||
|
||||
async function computeAdminWorkerPlatformSummary() {
|
||||
const hallConfig = getWorkerHallConfig()
|
||||
const [
|
||||
pendingWorkers,
|
||||
|
||||
@@ -2808,3 +2808,44 @@ test('打手端「我的订单」必须走"先取 id 再取详情"的延迟分
|
||||
)
|
||||
assert.ok(src.includes('rowsById.get(id)'), 'ANY() 不保证顺序,必须按 id 顺序还原分页结果')
|
||||
})
|
||||
|
||||
test('打手等级筛选必须先把等级解析成打手 id,并用 UNION 形式(不能用 OR + IN 子查询)', 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('resolveWorkerLevelFilterIds'),
|
||||
'缺少等级→打手 id 的解析:相关 EXISTS 无法走索引,会顺序扫整张 work_orders',
|
||||
)
|
||||
assert.ok(
|
||||
/SELECT id FROM worker_users WHERE level_id = ANY/.test(src),
|
||||
'解析必须只查 worker_users(level_id)(该列有 idx_worker_users_level)',
|
||||
)
|
||||
const branch = src.slice(
|
||||
src.indexOf('if (workerIds) {'),
|
||||
src.indexOf('} else if (workerLevelIds.length > 0) {'),
|
||||
)
|
||||
assert.ok(branch.includes('UNION'), '必须用 UNION 形式(生产实测 61ms)')
|
||||
assert.ok(
|
||||
!/OR\s+wo\.id IN/.test(branch),
|
||||
'不能写成 `assigned_worker_id = ANY(...) OR id IN (...)`:OR 右支逐行回表,实测 1852ms 反而更慢',
|
||||
)
|
||||
// 三个入口都要先解析再建 where
|
||||
const resolved = src.split('resolveWorkerLevelFilterIds(').length - 1
|
||||
assert.ok(resolved >= 4, `解析调用点应为 1 处定义 + 3 个入口,实际 ${resolved}`)
|
||||
})
|
||||
|
||||
test('平台概览必须有短缓存(一次并发 5 条 count,池只有 8 条连接)', async () => {
|
||||
const { readFile } = await import('node:fs/promises')
|
||||
const src = await readFile(
|
||||
new URL('./admin-worker-platform-summary-service.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
assert.ok(src.includes('new WorkerListCache('), '缺少概览缓存:一次刷新会独占 5 条数据库连接')
|
||||
assert.ok(
|
||||
src.includes("adminSummaryCache.getOrSet('summary'"),
|
||||
'概览必须走 getOrSet 合并并发请求',
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user