feat(notice): 站内通知(子模块 + openspec change)
Some checks are pending
CI / backend (push) Waiting to run
CI / frontend (push) Waiting to run

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
liuyun 2026-10-11 22:25:30 +08:00
parent 06ce64e6f4
commit ff7d9bffa3
10 changed files with 180 additions and 2 deletions

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-11

View File

@ -0,0 +1,45 @@
## 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
无。

View File

@ -0,0 +1,27 @@
## Why
缺少站内通知/公告:管理员发布的通知用户无处查看,也没有未读提示。需要一个最小闭环的站内通知能力——管理员发公告,用户顶栏铃铛看未读、读后标记。投递用轮询(简单、fork 友好),范围仅站内(不接邮件/短信)。
## What Changes
- 新增 `notice`(通知/公告)与 `notice_read`(已读追踪)实体。
- 后端 `notices` 模块:管理侧 CRUD(`notice:*` 权限)+ 用户侧 `mine`/`unread-count`/`read`/`read-all`(仅登录态)。
- 前端:通知管理页 + 顶栏铃铛(未读角标 + 列表 + 标记已读),未读数轮询刷新。
## Capabilities
### New Capabilities
- `backend/notice`: 站内通知域——公告管理 + 用户已读/未读。
- `frontend/AD/notice`: 通知管理页 + 顶栏通知铃铛 + 轮询。
### Modified Capabilities
- `shared/data-model`: 新增 `notice`、`notice_read` 实体。
- `shared/api-contract`: 新增通知接口目录(管理 CRUD + 用户 mine/unread-count/read/read-all)。
## Impact
- **后端**:新增 `notices` 模块;Prisma(`sys_notice`、`sys_notice_read`);`tenant-template`(notice 菜单 + 接口资源)。
- **前端**:`api/notice.ts`、通知管理页、`ty-header` 铃铛 + 未读 store + 轮询定时器。
- **数据**:Prisma migration(两张新表)。

View File

@ -0,0 +1,27 @@
## Purpose
定义站内通知/公告域的行为:管理员发布公告,用户查看并追踪已读/未读。
## ADDED Requirements
### Requirement: 公告管理
系统 SHALL 支持管理员对通知做分页查询、新增、编辑、删除(软删回收唯一键)。
#### Scenario: 发布公告
- **WHEN** 管理员新增一条通知
- **THEN** 通知写入,status=enabled 时对本租户所有用户可见
### Requirement: 用户通知与已读
系统 SHALL 支持当前用户分页查看可见通知(含已读状态)、查询未读数、标记单条/全部已读。
#### Scenario: 查看通知列表
- **WHEN** 用户查看通知
- **THEN** 返回其可见通知(status=enabled),未读的 readAt 为空
#### Scenario: 未读数
- **WHEN** 用户请求未读数
- **THEN** 返回 status=enabled 且当前用户未读的通知数
#### Scenario: 标记已读
- **WHEN** 用户标记某条或全部通知已读
- **THEN** 对应 notice_read 记录其 read_at

View File

@ -0,0 +1,23 @@
## Purpose
定义站内通知的前端行为:顶栏铃铛展示未读数与通知列表、标记已读,管理页做通知 CRUD。
## ADDED Requirements
### Requirement: 顶栏通知铃铛
前端 SHALL 在顶栏展示通知铃铛与未读角标,点击展开通知列表,支持标记单条/全部已读;未读数轮询刷新。
#### Scenario: 展示未读数
- **WHEN** 用户登录后
- **THEN** 铃铛展示其未读通知数,并定时刷新
#### Scenario: 标记已读
- **WHEN** 用户点开某条通知或点「全部已读」
- **THEN** 该通知标记已读,未读数更新
### Requirement: 通知管理页
前端 SHALL 提供通知管理页,支持管理员新增/编辑/删除通知。
#### Scenario: 管理通知
- **WHEN** 管理员进入通知管理页
- **THEN** 可分页查看并增删改通知

View File

@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: 通知接口
系统 SHALL 提供:`GET /notices`(分页,管理)、`POST /notices`、`PATCH /notices/:id`、`DELETE /notices/:id`(管理,需 `notice:*` 权限);`GET /notices/mine`(当前用户可见通知 + 已读状态)、`GET /notices/unread-count`(未读数)、`POST /notices/:id/read`(标记已读)、`POST /notices/read-all`(全部已读),后四者仅需登录态。
#### Scenario: 拉取未读数
- **WHEN** 用户请求 GET /notices/unread-count
- **THEN** 返回其未读通知数
#### Scenario: 标记已读
- **WHEN** 用户请求 POST /notices/:id/read
- **THEN** 该通知对当前用户标记为已读

View File

@ -0,0 +1,12 @@
## ADDED Requirements
### Requirement: 通知实体
系统 SHALL 包含 `notice` 实体(公告/通知:id、tenant_code、title、content、status、created_by、created_at、deleted_at)与 `notice_read` 关联表(notice_id + user_id + read_at,唯一 tenant_code+notice_id+user_id),用于站内通知与已读追踪。
#### Scenario: 发布通知
- **WHEN** 管理员发布一条通知
- **THEN** 写入 notice,status=enabled 时对本租户所有用户可见
#### Scenario: 记录已读
- **WHEN** 用户阅读某通知
- **THEN** 写入 notice_read 记录其 read_at

View File

@ -0,0 +1,30 @@
## 1. 数据模型与迁移
- [x] 1.1 Prisma:`Notice`(sys_notice,软删)+ `NoticeRead`(sys_notice_read,唯一 tenant_code+notice_id+user_id)
- [x] 1.2 生成并部署 migration
## 2. 后端 notices 模块
- [x] 2.1 `Permissions` 加 `notice: { list, create, update, delete }`
- [x] 2.2 新增 `notices` 模块(dto/service/controller/module),service `extends BaseCrudService`
- [x] 2.3 管理侧 CRUD:`GET/POST/PATCH/DELETE /notices`,挂权限 + `@AuditLog`
- [x] 2.4 用户侧:`GET /notices/mine`(enabled + 已读状态)、`GET /notices/unread-count`、`POST /notices/:id/read`、`POST /notices/read-all`(仅登录态)
- [x] 2.5 `app.module.ts` 注册 `NoticesModule`
## 3. 资源注册
- [x] 3.1 `tenant-template` 加 notice 菜单(parent=system)+ 管理侧接口资源
- [x] 3.2 存量租户跑 `pnpm sync:resources`
## 4. 前端
- [x] 4.1 `api/notice.ts`:类型 + 管理 CRUD + mine/unread-count/read/read-all
- [x] 4.2 notice store:未读数 + 列表 + 30s 轮询定时器
- [x] 4.3 `ty-header` 顶栏铃铛:`el-badge` 未读角标 + `el-popover` 列表 + 标记单条/全部已读
- [x] 4.4 通知管理页 `pages/system/notice/index.vue`(ty-crud,`defineOptions({ name: 'notice' })`)
## 5. 验证
- [x] 5.1 后端 build + 单测 + e2e 通过
- [x] 5.2 前端 type-check 通过
- [x] 5.3 冒烟:发布通知 → 未读数=1 → 标记已读 → 未读数=0

@ -1 +1 @@
Subproject commit ff0e1dcd27295a9cd2eb99a828c9f367aee583e9 Subproject commit 64bbb0a987a1f66373b6470b7c2001a582c3394f

@ -1 +1 @@
Subproject commit 9869974d4c53a7e1062c80b35d90c7a29d8c632a Subproject commit bad863917502c08afedc4f504bf04bc80dda59e8