三处都出自审计清单,改动互不重叠。
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 不得加缓存)。
20 lines
894 B
TypeScript
20 lines
894 B
TypeScript
import type { NextFunction, Request, Response } from 'express'
|
|
|
|
/**
|
|
* 给「几乎不变」的配置类接口加 HTTP 缓存头。
|
|
*
|
|
* 背景:后台刷新页面时前端会重新拉一遍这些静态配置(接单分类、快手行业店铺、
|
|
* 自定义店铺、dev-mock 状态)。它们此前没有任何 Cache-Control,于是每次刷新都真的
|
|
* 走一次网络 —— 生产抓包显示一次刷新里有 4 个这类请求,且都和业务查询挤在同一个
|
|
* 连接池窗口内。
|
|
*
|
|
* 必须带 `private`:这些是带鉴权的后台接口,禁止任何共享/中间缓存留存响应。
|
|
*/
|
|
export function setHttpCache(maxAgeSeconds: number) {
|
|
const headerValue = `private, max-age=${Math.max(0, Math.floor(maxAgeSeconds))}`
|
|
return (_req: Request, res: Response, next: NextFunction): void => {
|
|
res.setHeader('Cache-Control', headerValue)
|
|
next()
|
|
}
|
|
}
|