## Context 已有实体/资源/组织等业务域均为「`BaseCrudService` + 软删 + 租户隔离 + 资源表授权」的同构结构。通知无长连接需求,用轮询即可。动机见 proposal.md - Why。 ## Goals / Non-Goals **Goals:** - 管理员发布公告,本租户用户经顶栏铃铛查看未读、标记已读;未读数轮询刷新。 **Non-Goals:** - 不做定向通知(按用户/角色投递),本期仅广播式公告。 - 不接邮件/短信,不做 WebSocket 实时推送。 ## Decisions ### D1 — 实体:`Notice` + `NoticeRead` - `Notice`(`sys_notice`):id、tenant_code、title(128)、content(text)、status(enabled/disabled)、created_by、created_at/updated_at/deleted_at;软删,无业务唯一键(无需改名回收)。 - `NoticeRead`(`sys_notice_read`):tenant_code、notice_id、user_id、read_at,唯一 `(tenant_code, notice_id, user_id)`;**不软删**(追踪实体)。 ### D2 — 广播式 + enabled 才可见 通知对**本租户所有用户**可见,`status=enabled` 才计入未读/列表;`disabled` 仅管理员列表可见。未读数 = `enabled 通知数 − 当前用户已读数`(两条 count 相减)。 ### D3 — 用户侧接口仅登录态,管理侧挂权限 - 用户侧 `GET /notices/mine`、`GET /notices/unread-count`、`POST /notices/:id/read`、`POST /notices/read-all` 不加 `@RequirePermissions`(仅需登录)。 - 管理侧 `GET/POST/PATCH/DELETE /notices` 挂 `notice:list/create/update/delete` + 写操作 `@AuditLog('notice', ...)`。 ### D4 — 轮询未读数 前端 notice store 启动 30s 定时器轮询 `unread-count`(登录后启动、登出/401 停止),路由切换与标记已读后即时刷新;铃铛用 `el-badge` 显示未读数、`el-popover` 展示最近通知列表。 ## Risks / Trade-offs - **[轮询有延迟]** → 30s 内新通知才被看到;站内公告场景可接受,标记已读后即时刷新兜底。 - **[未读数计算随通知量增长]** → 用 `count` 聚合(有 `(tenant_code, notice_id, user_id)` 索引),不拉全量。 ## Migration Plan 1. Prisma migration:新建 `sys_notice`、`sys_notice_read`(无回填)。 2. 代码上线 + `pnpm sync:resources` 补齐 notice 菜单/接口。 3. 回滚:删除两张表 + 回滚代码即可。 ## Open Questions 无。