feat(workbench): persist topbar notification state
This commit is contained in:
111
document/development/通知中心状态持久化/CONCEPT.md
Normal file
111
document/development/通知中心状态持久化/CONCEPT.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# 通知中心状态持久化概念文档
|
||||
|
||||
## 功能一句话
|
||||
|
||||
为首页小铃铛通知中心补齐服务端状态接口,让同一用户在不同电脑登录时看到一致的已读、清空和隐藏状态,并优化笔记本等小屏幕下的通知弹窗可读性。
|
||||
|
||||
## 背景与问题
|
||||
|
||||
当前小铃铛通知由前端从单据中心、个人工作台摘要等数据源即时生成,但已读与清空状态主要写入浏览器 `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 状态会作为首次迁移兜底,后续服务端会逐步成为主状态源。
|
||||
Reference in New Issue
Block a user