收件箱与通知

收件箱不是日志,而是按来源聚合的提醒:同一 CategoryKey 再次发生时不新增一条,而是刷新内容、把已读重置为未读、把发生次数加一。理解这一点,才能解释「刚读过的通知又变回未读」——那是设计如此。

写入来源 系统 · 定时任务 · 工作流 · 看板任务 按 CategoryKey 聚合 同键未归档则合并更新 合并动作 刷新内容 · 置未读 · 次数 +1 读取 列表 · 分类统计 · 未读数 操作 单条已读 / 归档 · 批量已读 / 未读 / 归档 / 取消归档 / 删除 · 软删单条 点击通知:先标已读,再按 link 跳转(link 为空则只标已读)
聚合键是分类键:同一来源反复发生时只保留一条记录并累计次数,避免收件箱被同一类事件刷屏。

模型与字段

字段 / 常量取值
InboxNotificationCategory · CategoryKey · Level · Title · Content · IsRead · IsArchived · OccurrenceCount · Link · IsDeleted
级别 Levelinfo · success · warning · error
类别 Categorysystem · scheduledTask · workflow · kanbanTask
批量动作read · unread · archive · unarchive · delete
数据库约束Category 必填;CategoryKey / Level / Title / Content / Link 有长度限制;索引 CategoryKey / IsArchived / UpdatedAt;查询过滤软删

聚合规则

  1. 写入时若 CategoryKey 非空,且存在同键的未归档记录,则合并到那一条。
  2. 合并动作:刷新 Category / Level / Title / Content / Link,把 IsRead 置为未读、IsArchived 置为未归档,并把 OccurrenceCount 加一。
  3. 不存在同键未归档记录时新建一条,OccurrenceCount = 1。
  4. 归档会把它移出「当前聚合」的范围,所以归档后同一来源再次发生会生成新的一条。

端点

端点用途
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 + 事件类型」),否则要么每条都新建、要么把不相干的事情合并。
  • 合并会重置已读,这是刻意的「再次发生就再提醒一次」;确实不想再被提醒就归档。
  • 通知不是审计日志:它会被合并、归档、删除;要追溯历史请用运行记录(看板)、执行历史(定时任务)与检查点。