收件箱与通知
收件箱不是日志,而是按来源聚合的提醒:同一 CategoryKey 再次发生时不新增一条,而是刷新内容、把已读重置为未读、把发生次数加一。理解这一点,才能解释「刚读过的通知又变回未读」——那是设计如此。
聚合键是分类键:同一来源反复发生时只保留一条记录并累计次数,避免收件箱被同一类事件刷屏。
模型与字段
| 字段 / 常量 | 取值 |
InboxNotification | Category · CategoryKey · Level · Title · Content · IsRead · IsArchived · OccurrenceCount · Link · IsDeleted |
级别 Level | info · success · warning · error |
类别 Category | system · scheduledTask · workflow · kanbanTask |
| 批量动作 | read · unread · archive · unarchive · delete |
| 数据库约束 | Category 必填;CategoryKey / Level / Title / Content / Link 有长度限制;索引 CategoryKey / IsArchived / UpdatedAt;查询过滤软删 |
聚合规则
- 写入时若
CategoryKey 非空,且存在同键的未归档记录,则合并到那一条。
- 合并动作:刷新
Category / Level / Title / Content / Link,把 IsRead 置为未读、IsArchived 置为未归档,并把 OccurrenceCount 加一。
- 不存在同键未归档记录时新建一条,
OccurrenceCount = 1。
- 归档会把它移出「当前聚合」的范围,所以归档后同一来源再次发生会生成新的一条。
端点
| 端点 | 用途 |
GET /api/inbox?archived=&category=&page=&pageSize= | 列表,按归档状态与类别过滤,分页 |
GET /api/inbox/categories?archived= | 分类聚合:每条给总数与未读数 |
GET /api/inbox/unread-count | 未读总数(角标用) |
POST /api/inbox | 写入通知(走聚合逻辑) |
POST /api/inbox/{id}/read | 单条已读(或 InboxStateRequest.Value 指定目标状态) |
POST /api/inbox/{id}/archive | 单条归档 |
POST /api/inbox/batch | 批量:{ ids, action } |
DELETE /api/inbox/{id} | 软删单条 |
状态与删除语义
- 单条已读与归档只改一个字段,互不连带。
- 批量
delete 会把 IsDeleted 与 IsArchived 同时置为真,让它既不可见也不参与聚合。
- 软删的单条仍保留在数据库中,只是查询默认过滤掉。
前端行为
- 数据来自 React Query 的三个键:列表、分类统计、未读数;页面靠查询失效后重取刷新,没有专用 SSE 或轮询定时器。
- 点击一条通知:先标记已读,然后按
link 跳转到对应页面。
- 分类展示做了本地映射(定时任务 / 工作流 / 系统);
kanbanTask 已在数据层与常量中预留。
实作要点
CategoryKey 是聚合的唯一依据:写通知时要给它有意义且稳定的值(例如「任务 id + 事件类型」),否则要么每条都新建、要么把不相干的事情合并。
- 合并会重置已读,这是刻意的「再次发生就再提醒一次」;确实不想再被提醒就归档。
- 通知不是审计日志:它会被合并、归档、删除;要追溯历史请用运行记录(看板)、执行历史(定时任务)与检查点。