diff --git a/docs/plan/2026-07-17-todo-center-security-performance-checklist.md b/docs/plan/2026-07-17-todo-center-security-performance-checklist.md index 569863e73..165e1e62c 100644 --- a/docs/plan/2026-07-17-todo-center-security-performance-checklist.md +++ b/docs/plan/2026-07-17-todo-center-security-performance-checklist.md @@ -11,7 +11,7 @@ ## 路由安全 -- 默认允许 `invoiceDetail`、`financeDetail`、`accountUserInfo` 三个业务路由。 +- 默认允许 `invoiceDetail`、`financeDetail`、`accountUserInfo`、`AbnormalRecord` 四个业务路由。 - 新业务路由上线前必须追加到 `todo.route-whitelist`,不允许直接信任请求体中的路由名。 - `route_params` 必须是 JSON 对象,只允许 `params`、`query` 两层结构,最终值只能是字符串、数字、布尔值或空值。 - 用户端详情、已读、完成和取消接口继续使用当前登录用户 ID 作为 SQL 条件。 diff --git a/docs/plan/2026-07-22-todo-center-realtime-evaluation.md b/docs/plan/2026-07-22-todo-center-realtime-evaluation.md new file mode 100644 index 000000000..1ee09590b --- /dev/null +++ b/docs/plan/2026-07-22-todo-center-realtime-evaluation.md @@ -0,0 +1,72 @@ +# 待办中心实时刷新评估 + +## 结论 + +当前继续使用首页 60 秒轮询,不新增 WebSocket 或 SSE。 + +现阶段没有低于 60 秒的待办送达 SLA,PRD 也将 30~60 秒轮询定义为 V1 推荐方案,将实时推送保留为实时性要求提高后的 V2 能力。现有实现能够在进入首页时立即加载、返回首页时主动刷新,并在离开页面后停止轮询,满足当前需求。 + +如果后续达到本文列出的升级触发条件,优先采用 SSE;WebSocket 仅在出现双向实时交互需求时考虑。实时通道只发送“待办状态已变化”事件,任务数据仍通过现有 REST 接口查询。 + +## 当前实现与负载 + +- 首页创建和重新激活时立即调用 `/system/todo/unreadCount` 与 `/system/todo/homeList?limit=3`。 +- 首页激活期间每 60 秒刷新一次,并通过 `todoLoading` 避免同一页面内请求重叠。 +- 页面失活或销毁时清除定时器,待办中心列表打开时自行查询。 +- 每个停留在首页的活跃用户每分钟产生 2 个只读请求,理论请求速率为 `活跃首页用户数 / 30` QPS;1000 个同时停留首页的用户约为 33.3 QPS。 +- 首页查询已有按接收人、状态和排序设计的索引,当前验收目标为 P95 小于 500ms;真实数据基准仍按安全与性能清单执行。 +- 轮询的最坏可见延迟为 60 秒,平均约 30 秒。业务页面完成待办后已有主动刷新,不必等待下一轮轮询。 + +## 方案比较 + +| 维度 | 60 秒轮询 | SSE | WebSocket | +| --- | --- | --- | --- | +| 当前需求匹配 | 满足 PRD 的 30~60 秒要求 | 超出当前 SLA | 超出当前 SLA | +| 通信方向 | 前端拉取 | 服务端单向通知 | 双向通信 | +| 实现成本 | 已完成 | 中 | 高 | +| 现有鉴权兼容 | 直接复用 Bearer 请求头 | 原生 `EventSource` 不能设置 Bearer 请求头,需要流式 fetch/polyfill 或鉴权改造 | 握手鉴权和令牌续期需要单独实现 | +| 多实例支持 | 无额外要求 | 需要跨实例事件分发 | 需要跨实例事件分发和连接管理 | +| 代理与运维 | 普通 HTTP | 需要关闭缓冲、配置超时和心跳 | 需要 Upgrade、超时、心跳和连接容量配置 | +| 降级 | REST 本身即主链路 | 必须回退轮询 | 必须回退轮询 | + +待办只需要服务端向前端发送“数据有变化”的信号,不需要客户端通过长连接提交命令,因此若未来升级,SSE 比 WebSocket 更贴合通信模型。 + +## 当前基础设施缺口 + +- 后端没有 `spring-boot-starter-websocket`、`SseEmitter` 端点或待办连接注册表。 +- `TodoTaskService` 的创建、完成、取消、过期和归档操作没有统一发布提交后事件。 +- 待办由 system、pile、Quartz 等多个模块触发,需要统一事件出口,不能在各业务入口分别维护推送逻辑。 +- 项目虽已配置 Redis 和 RabbitMQ,但没有面向待办用户事件的跨实例发布订阅通道;仅使用进程内连接表会在多实例部署时漏推。 +- 当前后台登录是无状态 Bearer 鉴权。令牌保存在前端 Cookie 中,但服务端只读取 `Authorization` 请求头,原生 `EventSource` 无法直接复用。 +- 仓库没有可验证的 Nginx/网关长连接超时、SSE 缓冲或 WebSocket Upgrade 配置。 + +## 升级触发条件 + +满足任一条件后重新评估实时推送: + +1. 产品明确要求待办创建或状态变化后 10 秒内可见。 +2. 待办需要在首页之外显示全局角标、桌面提醒或声音提醒。 +3. 测试或生产监控表明轮询接口的 QPS、数据库负载或 P95 已超过约定容量预算,且索引和接口合并仍不足以解决。 +4. 系统已经具备可复用的认证长连接、跨实例事件总线和网关长连接运维能力,接入成本明显下降。 + +在触发实时化之前,优先考虑低成本优化:页面不可见时暂停轮询、恢复可见时立即刷新,以及在证据表明确有必要时合并首页数量和最近待办接口。 + +## SSE 目标方案 + +1. `TodoTaskService` 在数据库事务提交后发布只包含 `assigneeUserId`、事件类型和版本号的待办变更事件,不发送标题、内容或业务参数。 +2. 使用 Redis Pub/Sub 或 RabbitMQ 将事件广播到所有应用实例;每个实例只向本机对应用户的连接发送刷新信号。 +3. 增加受认证保护的 `/system/todo/events` SSE 端点。前端使用支持 `Authorization` 请求头的流式 fetch/polyfill,避免把令牌放入 URL。 +4. 服务端发送 20~30 秒心跳并清理断开的连接;前端自动重连,对连续事件做短时间去抖。 +5. 前端收到事件后仍调用现有 `/unreadCount`、`/homeList` 或 `/list` 接口获取权威数据。 +6. SSE 断开、网关不支持或事件总线异常时自动回退到 60 秒轮询;重新连接后立即全量刷新。 + +该方案不要求事件可靠保存。SSE 事件只是刷新提示,断线期间遗漏的事件由重连后的 REST 查询和轮询兜底。 + +## 实时化验收项 + +- 任何用户只能收到自己的刷新事件,不能从事件内容推断其他用户或运营商的任务。 +- 创建、完成、取消、过期和归档均在事务提交后触发事件,回滚事务不推送。 +- 单实例和多实例部署均能送达,断线重连不会造成连接泄漏。 +- 令牌过期后连接关闭,重新登录后建立新连接;日志和 URL 不记录令牌。 +- 长连接不可用时自动回退轮询,待办最终展示结果与数据库一致。 +- 在明确实时 SLA 后,以事件提交到页面完成 REST 刷新为口径记录 P95,并在压测中验证连接数、线程数和网关超时配置。