From ff7d9bffa3379b582db4b06a69d7c69fc88125c9 Mon Sep 17 00:00:00 2001 From: liuyun Date: Sun, 11 Oct 2026 22:25:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(notice):=20=E7=AB=99=E5=86=85=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=EF=BC=88=E5=AD=90=E6=A8=A1=E5=9D=97=20+=20openspec=20?= =?UTF-8?q?change=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../in-app-notification/.openspec.yaml | 2 + .../changes/in-app-notification/design.md | 45 +++++++++++++++++++ .../changes/in-app-notification/proposal.md | 27 +++++++++++ .../specs/backend/notice/spec.md | 27 +++++++++++ .../specs/frontend/AD/notice/spec.md | 23 ++++++++++ .../specs/shared/api-contract/spec.md | 12 +++++ .../specs/shared/data-model/spec.md | 12 +++++ openspec/changes/in-app-notification/tasks.md | 30 +++++++++++++ sources/sysmgr-admin | 2 +- sources/sysmgr-nest | 2 +- 10 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 openspec/changes/in-app-notification/.openspec.yaml create mode 100644 openspec/changes/in-app-notification/design.md create mode 100644 openspec/changes/in-app-notification/proposal.md create mode 100644 openspec/changes/in-app-notification/specs/backend/notice/spec.md create mode 100644 openspec/changes/in-app-notification/specs/frontend/AD/notice/spec.md create mode 100644 openspec/changes/in-app-notification/specs/shared/api-contract/spec.md create mode 100644 openspec/changes/in-app-notification/specs/shared/data-model/spec.md create mode 100644 openspec/changes/in-app-notification/tasks.md diff --git a/openspec/changes/in-app-notification/.openspec.yaml b/openspec/changes/in-app-notification/.openspec.yaml new file mode 100644 index 0000000..a369219 --- /dev/null +++ b/openspec/changes/in-app-notification/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-11 diff --git a/openspec/changes/in-app-notification/design.md b/openspec/changes/in-app-notification/design.md new file mode 100644 index 0000000..96e7084 --- /dev/null +++ b/openspec/changes/in-app-notification/design.md @@ -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 + +无。 diff --git a/openspec/changes/in-app-notification/proposal.md b/openspec/changes/in-app-notification/proposal.md new file mode 100644 index 0000000..842d2d1 --- /dev/null +++ b/openspec/changes/in-app-notification/proposal.md @@ -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(两张新表)。 diff --git a/openspec/changes/in-app-notification/specs/backend/notice/spec.md b/openspec/changes/in-app-notification/specs/backend/notice/spec.md new file mode 100644 index 0000000..e110be1 --- /dev/null +++ b/openspec/changes/in-app-notification/specs/backend/notice/spec.md @@ -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 diff --git a/openspec/changes/in-app-notification/specs/frontend/AD/notice/spec.md b/openspec/changes/in-app-notification/specs/frontend/AD/notice/spec.md new file mode 100644 index 0000000..d2cbe35 --- /dev/null +++ b/openspec/changes/in-app-notification/specs/frontend/AD/notice/spec.md @@ -0,0 +1,23 @@ +## Purpose + +定义站内通知的前端行为:顶栏铃铛展示未读数与通知列表、标记已读,管理页做通知 CRUD。 + +## ADDED Requirements + +### Requirement: 顶栏通知铃铛 +前端 SHALL 在顶栏展示通知铃铛与未读角标,点击展开通知列表,支持标记单条/全部已读;未读数轮询刷新。 + +#### Scenario: 展示未读数 +- **WHEN** 用户登录后 +- **THEN** 铃铛展示其未读通知数,并定时刷新 + +#### Scenario: 标记已读 +- **WHEN** 用户点开某条通知或点「全部已读」 +- **THEN** 该通知标记已读,未读数更新 + +### Requirement: 通知管理页 +前端 SHALL 提供通知管理页,支持管理员新增/编辑/删除通知。 + +#### Scenario: 管理通知 +- **WHEN** 管理员进入通知管理页 +- **THEN** 可分页查看并增删改通知 diff --git a/openspec/changes/in-app-notification/specs/shared/api-contract/spec.md b/openspec/changes/in-app-notification/specs/shared/api-contract/spec.md new file mode 100644 index 0000000..a1c0f5b --- /dev/null +++ b/openspec/changes/in-app-notification/specs/shared/api-contract/spec.md @@ -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** 该通知对当前用户标记为已读 diff --git a/openspec/changes/in-app-notification/specs/shared/data-model/spec.md b/openspec/changes/in-app-notification/specs/shared/data-model/spec.md new file mode 100644 index 0000000..03a2066 --- /dev/null +++ b/openspec/changes/in-app-notification/specs/shared/data-model/spec.md @@ -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 diff --git a/openspec/changes/in-app-notification/tasks.md b/openspec/changes/in-app-notification/tasks.md new file mode 100644 index 0000000..5ab8aa7 --- /dev/null +++ b/openspec/changes/in-app-notification/tasks.md @@ -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 diff --git a/sources/sysmgr-admin b/sources/sysmgr-admin index ff0e1dc..64bbb0a 160000 --- a/sources/sysmgr-admin +++ b/sources/sysmgr-admin @@ -1 +1 @@ -Subproject commit ff0e1dcd27295a9cd2eb99a828c9f367aee583e9 +Subproject commit 64bbb0a987a1f66373b6470b7c2001a582c3394f diff --git a/sources/sysmgr-nest b/sources/sysmgr-nest index 9869974..bad8639 160000 --- a/sources/sysmgr-nest +++ b/sources/sysmgr-nest @@ -1 +1 @@ -Subproject commit 9869974d4c53a7e1062c80b35d90c7a29d8c632a +Subproject commit bad863917502c08afedc4f504bf04bc80dda59e8