perf(worker): 「我的订单」缓存改为事件驱动失效,TTL 2s/5s → 30s

这是当前最大瓶颈的根治:打手端三件套(列表/计数/概览)占全库约 75% 缓冲读,
单查询成本是 O(打手单量) 的固有成本(重打手 8,586 单逐行探测),
索引层面已无可抠,只能把"计算次数"和"访问次数"解耦。

原先 TTL 只有 2s/5s,是因为文档担心"再长会让分页总数滞后"。
但该数据只在该打手自己的工单流转时才变,而项目已有 realtime 快照广播
精确感知每次流转——把同一事件流接到缓存失效上,长 TTL 与新鲜度可以兼得,
正确性反而优于固定 5s(那同样存在滞后窗口)。

- WorkerListCache 新增 evictByPrefix:按键前缀整体逐出;
- 缓存实例抽到中立模块 worker-my-order-cache.ts:realtime 需要引用失效入口,
  而 my-order-service 已 import realtime,实例放任何一方都会循环依赖;
- 缓存键统一为 ["w",workerId,scope,detail],前缀失效不会误伤其它打手;
- 失效挂在 publishWorkOrderSnapshots:那里恰好已有工单 + 全部拼单人,
  受影响集合取 当前指派 + 历史保留 + 全部拼单人 + 调用方指定,
  21 处工单写入路径(含取消申请/反馈处理)都会经过;
  工单已删除的分支同样失效,避免打手继续看到已删的单。

最坏情况(漏失效)是打手最多 30s 看到旧状态,与原 5s TTL 的滞后窗口同性质。
守卫测试锁定:失效入口存在、键前缀格式、受影响集合三要素齐全。
This commit is contained in:
yml2213
2026-10-03 15:23:11 +08:00
parent fbdef6ecab
commit 69e3d9a396
5 changed files with 157 additions and 17 deletions
@@ -29,6 +29,7 @@ import {
mapWorkerUser,
resolveWorkerPermissions,
} from '../worker-platform/mappers.js'
import { invalidateWorkerMyOrderCaches } from '../worker-platform/worker-my-order-cache.js'
export type RealtimeEventType =
| 'admin_notification.changed'
@@ -249,6 +250,9 @@ async function publishWorkOrderSnapshots(input: {
const workOrderId = Math.max(0, Number(input.workOrderId) || 0)
const workOrder = await getWorkOrderById(workOrderId)
if (!workOrder) {
// 工单已不存在(删除):调用方传入的打手集合仍需失效缓存,
// 否则他们的「我的订单」会继续展示已删除的单。
invalidateWorkerMyOrderCaches(input.workerIds || [])
publishRealtimeEvent({
type: 'work_order.changed',
entityId: workOrderId,
@@ -262,6 +266,18 @@ async function publishWorkOrderSnapshots(input: {
const pendingUnfreezeByOrderId = await sumPendingUnfreezeByOrderIds([workOrderId])
const shares = await listWorkOrderShares(workOrderId)
// 事件驱动缓存失效:这里已经拿到的工单与拼单快照恰好给出完整的受影响打手集合
// (当前指派 + 历史保留 + 全部拼单人 + 调用方显式指定的)。
// 先失效再广播,保证打手端随后的拉取一定看到新数据;这也是「我的订单」缓存
// 能放心用 30s 长 TTL 的前提(见 worker-my-order-cache.ts)。
invalidateWorkerMyOrderCaches([
...(input.workerIds || []),
workOrder.assigned_worker_id,
workOrder.last_assigned_worker_id,
...shares.map((share) => share.worker_id),
])
const joinedQuantity = shares
.filter((share) => share.status !== 'cancelled')
.reduce((sum, share) => sum + Number(share.quantity || 0), 0)