feat: 新增可配置通知中心,支持扫描式/埋点式通知规则与手动群发
新增通知规则实体、扫描任务、API与管理界面,重构站内通知支持用户/电工双类型,移除硬编码的证照到期提醒,新增工单派单、电工审核结果等通知触发场景,调整菜单与数据库结构适配新通知体系。
This commit is contained in:
1 parent
91aa3c06f0
commit
6cf6a3d5b9
36 files changed
+3309
-246
No files matched your search
@@ -0,0 +1,171 @@
|
||||
# 可配置消息通知中心 · 实施计划
|
||||
|
||||
## Context(为什么做)
|
||||
|
||||
现状(代码核对结论):
|
||||
- 后端通知是**硬编码单点**:`notice.service.ts` 只支持「证照到期」一种,标题模板写死,**接收人写死为 ADMIN 角色**;由 `@midwayjs/cron` 的 `cert_expire.task.ts` 每日 08:00 触发。
|
||||
- **Web 管理端**消息中心已完整(铃铛 + 未读角标 + 抽屉 + 60s 轮询 + 已读),见 [layouts/index.vue](file:///d:/MyProject/smart_grid_iot/smart_grid_iot/client/src/layouts/index.vue#L334-L380)。
|
||||
- **管理端 Flutter App**:顶栏铃铛 [workbench_tab.dart](file:///d:/MyProject/smart_grid_iot/smart_grid_iot_manage_app_flutter/lib/features/workbench/presentation/workbench_tab.dart#L139) 传了 `trailingIcon` 但没传 `onTrailingTap`,点击无反应;无 notice API、无消息中心页。
|
||||
- **电工端 Flutter App**:三处铃铛只弹 `showAppToast('暂无新通知')`;后端 `/api/worker-app/notices` 是**实时计算的证书临期清单**,不落库、无已读/未读数。
|
||||
|
||||
目标:建立「可配置 + 两端统一」的站内消息中心——**规则模板自动触发 + 手动群发**,接收对象覆盖**管理端用户与电工**,配置在 **Web 后台**,管理端/电工端 Flutter 各自拥有消息中心页。
|
||||
|
||||
### 已确认决策(来自需求澄清,不得擅自更改)
|
||||
1. 形态:**站内消息中心**(不做离线/厂商推送)。
|
||||
2. 能力:**规则模板自动触发** 与 **手动群发** 两者都要;手动群发**仅立即发送**(不做草稿/定时)。
|
||||
3. 接收对象:**管理端用户 + 电工端用户**;**完全靠配置**决定,代码不内置默认接收人。
|
||||
4. 数据模型:**改造现有 `sys_notice` 增加接收人类型,两端统一**。
|
||||
5. 事件捕获:**混合式** = 扫描式规则(时间/状态类)+ 业务代码埋点(一次性状态变更)。
|
||||
6. 配置入口:**Web 后台**。
|
||||
7. 首批事件域:**电工域 / 客户域 / 工单域**(项目域、验收、项目计划下期扩)。
|
||||
8. 两个 App 消息中心:**独立二级页**(go_router,复用 Cupertino 右滑转场),未读红点 + 60s 轮询(对齐 Web)。
|
||||
|
||||
---
|
||||
|
||||
## 架构总览
|
||||
|
||||
```
|
||||
业务事件 ──┬─ 扫描式:内置任务 notice_scan ─→ NoticeRuleService.executeScan()
|
||||
│ (复用自研调度器,可在「定时任务」页改 cron/启停/立即执行)
|
||||
└─ 埋点式:业务 service 直接调 NoticeService.deliver()
|
||||
↓
|
||||
NoticeService.deliver() ← 接收人解析(sys_user / worker)+ 幂等写入
|
||||
↓
|
||||
sys_notice(receiver_type=user|worker)
|
||||
↓
|
||||
┌──────────────┬───────────────────┬──────────────────┐
|
||||
Web 消息中心 管理端 App 消息中心 电工端 App 消息中心
|
||||
(已存在,改造) (新建页面) (新建页面)
|
||||
```
|
||||
|
||||
**复用现有资产**(不重复造):
|
||||
- 自研调度器 [job_scheduler.service.ts](file:///d:/MyProject/smart_grid_iot/smart_grid_iot/server/src/service/job_scheduler.service.ts#L416-L428) 与任务注册表 [job_registry.ts](file:///d:/MyProject/smart_grid_iot/smart_grid_iot/server/src/task/job_registry.ts#L22-L55):通过扩展 `JobRuntime` 注入"执行通知规则"能力(写法照搬 `cleanOperationLogs` 用 `app.createAnonymousContext()` 取 service)。
|
||||
- RBAC 路径映射 [rbac.middleware.ts](file:///d:/MyProject/smart_grid_iot/smart_grid_iot/server/src/middleware/rbac.middleware.ts#L10-L36)、菜单/权限迁移写法 [20261002_job_management.sql](file:///d:/MyProject/smart_grid_iot/smart_grid_iot/database/migrations/20261002_job_management.sql#L87-L108)。
|
||||
- Web 列表页骨架 [dispatch/price/index.vue](file:///d:/MyProject/smart_grid_iot/smart_grid_iot/client/src/views/dispatch/price/index.vue)。
|
||||
- Flutter 两端 `go_router` + `XPageShell`/`XTopBar`/`XCard` + Riverpod。
|
||||
|
||||
---
|
||||
|
||||
## 一、数据库(迁移 `database/migrations/20261004_notice_config.sql`)
|
||||
|
||||
### 1) 改造 `sys_notice`
|
||||
```sql
|
||||
ALTER TABLE sys_notice ADD COLUMN IF NOT EXISTS receiver_type VARCHAR(16) NOT NULL DEFAULT 'user'; -- user|worker
|
||||
ALTER TABLE sys_notice ADD COLUMN IF NOT EXISTS rule_id BIGINT; -- → sys_notice_rule.id(扫描式来源)
|
||||
ALTER TABLE sys_notice ADD COLUMN IF NOT EXISTS batch_id BIGINT; -- → sys_notice_batch.id(手动群发来源)
|
||||
DROP INDEX IF EXISTS uk_sys_notice_biz;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_sys_notice_deliver
|
||||
ON sys_notice (notice_type, biz_type, biz_id, receiver_type, receiver_id); -- 幂等键
|
||||
CREATE INDEX IF NOT EXISTS idx_sys_notice_receiver_type ON sys_notice (receiver_type, receiver_id, is_read);
|
||||
```
|
||||
|
||||
### 2) 新增 `sys_notice_rule`(规则模板 + 接收对象)
|
||||
| 列 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| id | BIGSERIAL PK | |
|
||||
| rule_key | VARCHAR(64) UNIQUE | 规则键,如 `cert_expire` / `worker_audit_result` / `contract_expire` / `visit_plan` / `dispatch_assign` / `order_timeout` |
|
||||
| rule_name | VARCHAR(64) | 规则名称 |
|
||||
| event_key | VARCHAR(64) | 事件注册表 key |
|
||||
| trigger_mode | SMALLINT | 1 扫描式 2 埋点式 |
|
||||
| enabled | SMALLINT DEFAULT 1 | 启停 |
|
||||
| advance_days | INT NULL | 提前天数(扫描式用) |
|
||||
| title_tpl | VARCHAR(128) | 标题模板,支持 `{变量}` |
|
||||
| content_tpl | VARCHAR(512) | 正文模板,支持 `{变量}` |
|
||||
| receiver_config | JSONB | `{"users":{"roles":["ADMIN"],"ids":[]},"workers":{"mode":"ids\|all\|status","ids":[],"auditStatus":1,"status":1}}` |
|
||||
| remark / created_at / updated_at | | |
|
||||
|
||||
### 3) 新增 `sys_notice_batch`(手动群发批次/发送记录)
|
||||
`id`、`title`、`content`、`receiver_config JSONB`、`receiver_count INT`、`sent_by BIGINT(→sys_user.id)`、`sent_at TIMESTAMPTZ`。
|
||||
|
||||
### 4) 菜单与权限
|
||||
新增「通知管理」菜单(`menu_type=2`,路径 `/system/notice`,权限 `notice:rule`)+ 按钮权限 `notice:batch`(群发);绑定 ADMIN 角色(沿用 `ON CONFLICT` 幂等写法);同步 `database/db_seed.sql` 菜单段与序列。
|
||||
|
||||
---
|
||||
|
||||
## 二、后端(`server/src`)
|
||||
|
||||
### 新增文件
|
||||
- `entity/sys_notice_rule.entity.ts`、`entity/sys_notice_batch.entity.ts`
|
||||
- `task/notice_event_registry.ts`:**事件 provider 注册表**(`eventKey → { name, variables, scan(advanceDays) }`),扫描式事件在此实现数据扫描,返回 `{ bizType, bizId, vars }[]`。
|
||||
- `task/notice_scan.task.ts`:`BuiltinJob`(`jobKey='notice_scan'`,默认 cron `0 0 8 * * *`),handler 调用 `ctx.runtime.runNoticeRules()`。
|
||||
- `service/notice_rule.service.ts`:规则 CRUD;`executeScan()` 遍历 `enabled && trigger_mode=1` 的规则 → provider 扫描 → 解析接收人 → 模板渲染 → 调 `NoticeService.deliver()`;提供 `runForEvent(eventKey, biz...)` 供埋点式复用。
|
||||
- `service/notice_batch.service.ts`:手动群发(解析接收人 → 逐人写入 `sys_notice`,`notice_type='manual'`、`biz_type='manual_batch'`、`biz_id=batchId` → 写批次记录)。
|
||||
- `controller/notice_rule.controller.ts`:`/api/notice-rules`(list/detail/create/update/delete/enable/事件字典/立即执行)。
|
||||
- `controller/notice_batch.controller.ts`:`/api/notice-batches`(list/create 群发)。
|
||||
- `dto/notice_rule.dto.ts`、`dto/notice_batch.dto.ts`(class-validator)。
|
||||
|
||||
### 修改文件
|
||||
- `entity/sys_notice.entity.ts`:加 `receiverType/ruleId/batchId`,索引改为 `uk_sys_notice_deliver`。
|
||||
- `task/job_registry.ts`:`BUILTIN_JOBS` 注册 `notice_scan`;`JobRuntime` 增加 `runNoticeRules(): Promise<string>`。
|
||||
- `service/job_scheduler.service.ts`:`buildRuntime()` 增加 `runNoticeRules`(匿名上下文取 `NoticeRuleService`,照搬 L417-428 的 `LogService` 写法)。
|
||||
- `service/notice.service.ts`:抽出**统一投递** `deliver({...})`(幂等 ON CONFLICT)、**接收人解析**(roles→sys_user,workers→worker);`listMy/unreadCount/markRead/markAllRead` 增加 `receiverType` 维度(管理端固定 `user`;同时新增 worker 侧方法)。
|
||||
- `controller/notice.controller.ts`:管理端固定 `receiver_type='user'`。
|
||||
- `controller/worker_app.controller.ts` + `service/worker_app.service.ts`:`listNotices` **改为读 `sys_notice`**(`receiver_type='worker' AND receiver_id=workerId`),替换现有实时计算;新增 `/notices/unread-count`、`/notices/:id/read`、`/notices/read-all`。
|
||||
- `middleware/rbac.middleware.ts`:新增 `/api/notice-rules → notice:rule`、`/api/notice-batches → notice:batch`(`/api/notice*` 注意前缀匹配顺序,`-rules/-batches` 需在 `/api/notices` 之前不冲突——二者路径不同,无冲突)。
|
||||
|
||||
### 扫描式事件 provider(首批)
|
||||
| event_key | 数据源 | 条件 |
|
||||
|---|---|---|
|
||||
| `cert_expire` | `worker_cert.expireDate` | `expire_date <= today + advance_days`(含已过期) |
|
||||
| `contract_expire` | `contract.endDate` | 同上 |
|
||||
| `visit_plan` | `customer_visit` | `status=0` 且计划日期已到/超期 |
|
||||
| `order_timeout` | `repair_dispatch`/`survey_order`/`install_order`/`maintenance_order` | 派单后 `status` 滞留超 N 天未接单 |
|
||||
|
||||
> 现有 `cert_expire.task.ts`(`@midwayjs/cron`)**迁移进 `notice_scan`** 后删除,消除两套调度并存(这也是 `add-scheduled-job-management` spec Assumption 6 预留的后续动作)。
|
||||
|
||||
### 埋点式事件(在业务 service 内调用 `NoticeService.deliver()`,**必须 try/catch 包裹,通知失败不得阻断业务**)
|
||||
| event_key | 埋点位置 |
|
||||
|---|---|
|
||||
| `worker_audit_result` | [worker.service.ts](file:///d:/MyProject/smart_grid_iot/smart_grid_iot/server/src/service/worker.service.ts#L348-L418) 审核通过/驳回 → 通知电工本人 |
|
||||
| `dispatch_assign` | `repair_dispatch.service.ts`、`survey_order.service.ts`、`install_order.service.ts`、`maintenance.service.ts` 派单/指派 → 通知电工 |
|
||||
| `order_status` | 上述工单状态流转(按需) |
|
||||
|
||||
---
|
||||
|
||||
## 三、Web 管理端(`client/src`)
|
||||
- 新增 `api/noticeRule.ts`、`api/noticeBatch.ts`;`api/notice.ts` 按需加类型筛选。
|
||||
- 新增 `views/system/notice/index.vue`:Tabs「通知规则」(列表 + 编辑弹窗:事件、启停、提前天数、标题/正文模板、接收对象选择器)「手动群发」(标题/正文 + 接收对象 + 立即发送)「发送记录」。
|
||||
- `router/index.ts`:注册 `/system/notice`,`meta.permission='notice:rule'`(侧边栏由后端 `sys_menu` 动态生成,故必须配合迁移菜单)。
|
||||
- `layouts/index.vue`:`handleNoticeClick` 按 `noticeType/bizType` 映射跳转业务详情(管理端可跳电工登记页;无映射则仅标记已读)。
|
||||
|
||||
## 四、Flutter 管理端 App(`smart_grid_iot_manage_app_flutter`)
|
||||
- 新增 `features/notice/data/notice_api.dart`、`features/notice/presentation/notice_center_page.dart`(列表 + 未读高亮 + 全部已读 + 下拉刷新 + 分页)。
|
||||
- `core/providers.dart`:新增 `noticeUnreadCountProvider`(60s 轮询,登录态下生效)。
|
||||
- `app/router.dart`:注册 `/notice`(复用现有 Cupertino 右滑转场)。
|
||||
- `shared/widgets/x_top_bar.dart`:给 `trailing` 增加未读红点角标能力。
|
||||
- `workbench_tab.dart`:`onTrailingTap` → `context.push('/notice')`;其余 tab 顶栏统一挂红点。
|
||||
|
||||
## 五、Flutter 电工端 App(`smart_grid_iot_app_flutter`)
|
||||
- 同上新建 `features/notice/**` 与 `/notice` 路由、`noticeUnreadCountProvider`。
|
||||
- `order_tab.dart`/`earnings_tab.dart`/`profile_tab.dart` 三处铃铛:`onTrailingTap` 改为 `context.push('/notice')` + 红点。
|
||||
- 后端 `listNotices` 改为读 `sys_notice` 后,页面展示统一的消息中心(含证照到期、审核结果、派单通知)。
|
||||
|
||||
---
|
||||
|
||||
## 六、验证(端到端)
|
||||
|
||||
1. **后端**:`server` 下 `yarn dev` 无编译错误;`/api/notice-rules` CRUD、`/api/notice-batches` 群发返回 `{code:200}`;在「定时任务」页对 `notice_scan` 点「立即执行」。
|
||||
2. **幂等**:连续执行 `notice_scan` 两次,`sys_notice` 不新增重复(唯一索引 `uk_sys_notice_deliver` 生效)。
|
||||
3. **接收人解析**:配置规则接收对象为「角色 ADMIN + 指定电工」,确认管理端用户与电工各自收到且 `receiver_type` 正确。
|
||||
4. **埋点**:审核通过一个电工 → 该电工消息中心立即可见;且审核接口在通知写入失败时仍成功(断点/模拟异常验证)。
|
||||
5. **Web**:登录 ADMIN → 通知管理页配置规则 + 群发;顶栏铃铛角标与已读正常。
|
||||
6. **管理端 App**:登录 → workbench 铃铛红点 → 消息中心 → 标记已读 → 红点消失。
|
||||
7. **电工端 App**:登录电工账号 → 铃铛红点 → 消息中心 → 已读;审核通过/驳回后收到消息。
|
||||
8. **数据库**:核对 `sys_notice` 新旧索引替换、缓存数据保留。
|
||||
|
||||
---
|
||||
|
||||
## 七、分期与边界
|
||||
|
||||
- **本期**:电工域 / 客户域 / 工单域(扫描式 + 埋点式)+ 手动群发 + Web 配置页 + 两端 App 消息中心。
|
||||
- **下期**:项目域 / 验收 / 项目计划节点(仅需注册 provider 或在 service 埋点,框架不变)。
|
||||
- **不做**:离线/厂商推送(极光、个推、FCM)、消息撤销、已读回执、群发定时/草稿、发送失败重试、消息模板富文本。
|
||||
|
||||
## 八、假设与风险(如需调整请指出)
|
||||
|
||||
1. 管理端接口固定 `receiver_type='user'`,电工端固定 `'worker'`;两者 id 空间不同,靠 `receiver_type` 区分。
|
||||
2. 埋点会改动多个业务 service;一律 `try/catch` 且不参与业务事务提交(通知失败只记日志)。
|
||||
3. `sys_notice` 唯一索引需先 `DROP` 旧索引再建新索引,历史 cert 通知数据保留。
|
||||
4. 扫描式事件存在调度间隔延迟(默认每日 08:00,可在「定时任务」页按需调整)。
|
||||
5. `cert_expire.task.ts` 迁入 `notice_scan` 后删除;`@midwayjs/cron` 依赖是否保留取决于是否还有其它注解任务(`demo_heartbeat`/`log_clean` 已是注册表任务)。
|
||||
6. Flutter 两端新增页面的视觉细节(空态、时间格式)沿用各自现有组件与 `format_util`,不引入新设计。
|
||||
Reference in new issue
Block a user