Files
andyesfly 6cf6a3d5b9 feat: 新增可配置通知中心,支持扫描式/埋点式通知规则与手动群发
新增通知规则实体、扫描任务、API与管理界面,重构站内通知支持用户/电工双类型,移除硬编码的证照到期提醒,新增工单派单、电工审核结果等通知触发场景,调整菜单与数据库结构适配新通知体系。
2026-10-06 11:53:02 +08:00

14 KiB
Raw Permalink Blame History

可配置消息通知中心 · 实施计划

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 各自拥有消息中心页。

已确认决策(来自需求澄清,不得擅自更改)

  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 消息中心
(已存在,改造)  (新建页面)        (新建页面)

复用现有资产(不重复造):


一、数据库(迁移 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.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 审核通过/驳回 → 通知电工本人
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,不引入新设计。