新增通知规则实体、扫描任务、API与管理界面,重构站内通知支持用户/电工双类型,移除硬编码的证照到期提醒,新增工单派单、电工审核结果等通知触发场景,调整菜单与数据库结构适配新通知体系。
14 KiB
14 KiB
可配置消息通知中心 · 实施计划
Context(为什么做)
现状(代码核对结论):
- 后端通知是硬编码单点:
notice.service.ts只支持「证照到期」一种,标题模板写死,接收人写死为 ADMIN 角色;由@midwayjs/cron的cert_expire.task.ts每日 08:00 触发。 - Web 管理端消息中心已完整(铃铛 + 未读角标 + 抽屉 + 60s 轮询 + 已读),见 layouts/index.vue。
- 管理端 Flutter App:顶栏铃铛 workbench_tab.dart 传了
trailingIcon但没传onTrailingTap,点击无反应;无 notice API、无消息中心页。 - 电工端 Flutter App:三处铃铛只弹
showAppToast('暂无新通知');后端/api/worker-app/notices是实时计算的证书临期清单,不落库、无已读/未读数。
目标:建立「可配置 + 两端统一」的站内消息中心——规则模板自动触发 + 手动群发,接收对象覆盖管理端用户与电工,配置在 Web 后台,管理端/电工端 Flutter 各自拥有消息中心页。
已确认决策(来自需求澄清,不得擅自更改)
- 形态:站内消息中心(不做离线/厂商推送)。
- 能力:规则模板自动触发 与 手动群发 两者都要;手动群发仅立即发送(不做草稿/定时)。
- 接收对象:管理端用户 + 电工端用户;完全靠配置决定,代码不内置默认接收人。
- 数据模型:改造现有
sys_notice增加接收人类型,两端统一。 - 事件捕获:混合式 = 扫描式规则(时间/状态类)+ 业务代码埋点(一次性状态变更)。
- 配置入口:Web 后台。
- 首批事件域:电工域 / 客户域 / 工单域(项目域、验收、项目计划下期扩)。
- 两个 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 与任务注册表 job_registry.ts:通过扩展
JobRuntime注入"执行通知规则"能力(写法照搬cleanOperationLogs用app.createAnonymousContext()取 service)。 - RBAC 路径映射 rbac.middleware.ts、菜单/权限迁移写法 20261002_job_management.sql。
- Web 列表页骨架 dispatch/price/index.vue。
- Flutter 两端
go_router+XPageShell/XTopBar/XCard+ Riverpod。
一、数据库(迁移 database/migrations/20261004_notice_config.sql)
1) 改造 sys_notice
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.tstask/notice_event_registry.ts:事件 provider 注册表(eventKey → { name, variables, scan(advanceDays) }),扫描式事件在此实现数据扫描,返回{ bizType, bizId, vars }[]。task/notice_scan.task.ts:BuiltinJob(jobKey='notice_scan',默认 cron0 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-managementspec Assumption 6 预留的后续动作)。
埋点式事件(在业务 service 内调用 NoticeService.deliver(),必须 try/catch 包裹,通知失败不得阻断业务)
| event_key | 埋点位置 |
|---|---|
worker_audit_result |
worker.service.ts 审核通过/驳回 → 通知电工本人 |
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后,页面展示统一的消息中心(含证照到期、审核结果、派单通知)。
六、验证(端到端)
- 后端:
server下yarn dev无编译错误;/api/notice-rulesCRUD、/api/notice-batches群发返回{code:200};在「定时任务」页对notice_scan点「立即执行」。 - 幂等:连续执行
notice_scan两次,sys_notice不新增重复(唯一索引uk_sys_notice_deliver生效)。 - 接收人解析:配置规则接收对象为「角色 ADMIN + 指定电工」,确认管理端用户与电工各自收到且
receiver_type正确。 - 埋点:审核通过一个电工 → 该电工消息中心立即可见;且审核接口在通知写入失败时仍成功(断点/模拟异常验证)。
- Web:登录 ADMIN → 通知管理页配置规则 + 群发;顶栏铃铛角标与已读正常。
- 管理端 App:登录 → workbench 铃铛红点 → 消息中心 → 标记已读 → 红点消失。
- 电工端 App:登录电工账号 → 铃铛红点 → 消息中心 → 已读;审核通过/驳回后收到消息。
- 数据库:核对
sys_notice新旧索引替换、缓存数据保留。
七、分期与边界
- 本期:电工域 / 客户域 / 工单域(扫描式 + 埋点式)+ 手动群发 + Web 配置页 + 两端 App 消息中心。
- 下期:项目域 / 验收 / 项目计划节点(仅需注册 provider 或在 service 埋点,框架不变)。
- 不做:离线/厂商推送(极光、个推、FCM)、消息撤销、已读回执、群发定时/草稿、发送失败重试、消息模板富文本。
八、假设与风险(如需调整请指出)
- 管理端接口固定
receiver_type='user',电工端固定'worker';两者 id 空间不同,靠receiver_type区分。 - 埋点会改动多个业务 service;一律
try/catch且不参与业务事务提交(通知失败只记日志)。 sys_notice唯一索引需先DROP旧索引再建新索引,历史 cert 通知数据保留。- 扫描式事件存在调度间隔延迟(默认每日 08:00,可在「定时任务」页按需调整)。
cert_expire.task.ts迁入notice_scan后删除;@midwayjs/cron依赖是否保留取决于是否还有其它注解任务(demo_heartbeat/log_clean已是注册表任务)。- Flutter 两端新增页面的视觉细节(空态、时间格式)沿用各自现有组件与
format_util,不引入新设计。