Files
X-Financial/document/development/通知中心状态持久化/CONCEPT.md
2026-06-03 21:43:35 +08:00

112 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 通知中心状态持久化概念文档
## 功能一句话
为首页小铃铛通知中心补齐服务端状态接口,让同一用户在不同电脑登录时看到一致的已读、清空和隐藏状态,并优化笔记本等小屏幕下的通知弹窗可读性。
## 背景与问题
当前小铃铛通知由前端从单据中心、个人工作台摘要等数据源即时生成,但已读与清空状态主要写入浏览器 `localStorage`。这会导致同一账号在 A 电脑清空通知后,换到 B 电脑仍然看到通知。
同时,通知条数较多或屏幕高度较小时,列表内容容易挤压头部操作区,通知标题与描述也容易在窄宽度下互相挤压。
## 目标与非目标
目标:
- 提供当前用户维度的通知状态接口。
- 支持批量同步通知状态,至少覆盖已读与隐藏。
- 前端优先使用服务端状态,接口不可用时保留本地降级能力。
- 优化小屏幕通知弹窗,列表多时使用内部滚动,标题、描述与操作按钮不互相挤压。
非目标:
- 不做独立消息投递系统。
- 不新增推送、WebSocket 或邮件通知能力。
- 不改变通知来源生成逻辑,当前仍由单据中心和工作台摘要生成。
## 用户与场景
- 普通员工:在个人工作台查看待办、单据新消息,跨电脑登录后已读状态一致。
- 审批人:处理待审批单据后,通知中心不因换电脑重新显示已清空内容。
- 管理员:仍可看到系统内已有通知入口,但 admin 是否展示工作台由现有逻辑决定。
## 功能能力
- `GET /notification-states`:读取当前登录用户的通知状态集合。
- `POST /notification-states`:批量保存当前登录用户的通知状态。
- 状态字段:
- `notification_id`:前端生成的稳定通知 ID。
- `read_at`:已读时间。
- `hidden_at`:隐藏或清空时间。
- `context_json`:保留通知来源、类型等低风险上下文,便于排查。
- 前端能力:
- 打开工作台或弹窗时读取服务端状态。
- 点击通知写入已读。
- 清空通知写入隐藏。
- 接口失败时仍写入本地缓存,避免用户操作失效。
## 方案设计
后端:
- 新增 `NotificationState` SQLAlchemy 模型。
- 新增 `NotificationStateService`,负责按 `CurrentUserContext.username` 读写状态。
- 新增 `notification_states` endpoint并挂到 API v1 router。
- 服务初始化时使用项目现有 `Base.metadata.create_all(..., tables=[...])` 模式确保表存在。
前端:
- 新增 `web/src/services/notificationStates.js` 封装接口。
- `TopBar.vue``localStorage` 状态作为初始兜底,服务端状态返回后合并覆盖。
- `markNotificationRead``clearAllNotifications` 做乐观更新,再异步同步服务端。
- 对单据通知仍调用现有 `markDocumentInboxRowRead`,同时写入通知状态接口,保证跨设备一致。
小屏幕布局:
- 弹窗宽度使用 `clamp``100vw` 约束。
- 弹窗最大高度使用 `min(..., calc(100vh - ...))`
- 列表作为唯一滚动区域,头部和 tab 固定在弹窗网格内。
- 通知描述允许两行截断,避免窄屏时横向挤压。
## 算法与公式
当前功能不涉及显式数学公式。状态合并规则为:
$$
visible = notification\_id \notin hiddenIds
$$
$$
unread = sourceUnread \land notification\_id \notin readIds \land notification\_id \notin hiddenIds
$$
服务端状态优先,前端本地状态仅作为接口失败或首次加载前的兜底。
## 测试方案
- 后端单元测试:
- 当前用户只能读取自己的通知状态。
- 批量 upsert 后可读取 `read_at``hidden_at`
- 清空通知写入 hidden 状态。
- 前端静态测试:
- `TopBar` 引用通知状态服务。
- 已读、清空操作会同步服务端。
- 小屏 CSS 使用弹窗 max-height、内部滚动和移动端约束。
- 构建验证:
- 运行前端构建确认 Vue 与服务导入无误。
- 在容器内运行后端定向 pytest。
## 指标与验收
- 同一用户跨电脑登录后,已读和清空状态由服务端保持一致。
- 接口失败时用户仍可本地清空,不阻断主要流程。
- 通知弹窗在笔记本高度下不会挤压头部按钮,列表内部滚动。
- 通知标题、描述、时间在窄屏下不横向溢出。
## 风险与开放问题
- 当前通知本身仍由前端即时生成,服务端只保存状态,不保存完整通知正文。
- 通知 ID 需要保持稳定,否则服务端状态无法命中;本次沿用现有 `document:``workbench:` 前缀。
- 历史 localStorage 状态会作为首次迁移兜底,后续服务端会逐步成为主状态源。