Skip to content

系统消息中心 - 整体规划

状态:规划定稿待评审 | 日期:2026-07-04(2026-07-05 修订:去掉 WS 实时通道,改为轮询;短信/邮件复用现有 ISenderService 体系) 范围:全账户类型(platform/channel/merchant/user)、多渠道(站内/短信/邮件/微信公众号/小程序订阅/APP 推送/Webhook)、模板化、准实时(轮询)送达。


一、目标与定位

为整个 SaaS 平台提供统一的消息能力,业务方只做一件事:"我要给谁、按哪个模板、带什么参数发一条消息",渠道选择、内容渲染、偏好过滤、派发重试、回执全部由消息中心承担。

明确的边界:

  • 消息中心不理解业务。跳转路由、业务 ID 都由业务方通过参数传入。
  • 站内信收件箱是渠道之一(INBOX),不是特殊存在;消息真值永远在 DB,前端未读数用轮询刷新(不做 WS/SSE 常驻通道),需要离线强触达的场景走 APP 推送/小程序订阅消息渠道。
  • 公告(一对多)与通知(一对一)是两个模型,推拉分离,不用一张表打天下。

二、模块落位

集成到 hiapi-system-basic-core,作为三个新子模块(message-entity / message-service / message-api),网关前缀 /cloud-api/**

放置决策:

  1. 消息中心是系统基础能力,同属 hiapi-system-basic 的范畴;
  2. 首期只做站内信(INBOX),不涉及外部渠道派发的复杂性,无需独立服务;
  3. 后续多渠道派发(阶段2+)若成为性能瓶颈,可再考虑独立部署,但代码位置保持统一。

架构(首期同进程,量大后 dispatcher 可拆为独立部署):

hiapi-system-basic-core
├── hiapi-system-basic-message-entity    消息模型 (MsgNotification, MsgCategory)
├── hiapi-system-basic-message-service   业务逻辑 (saveNotification/read/readAll/unreadCount, Redis 未读数)
└── hiapi-system-basic-message-api       API 层 (四账户类型 Controller, 事件监听)
                                          路由: /api/cloud-api/{accountType}/message/notification/*

请求流向:前端 → /api/cloud-api/{accountType}/message/notification/query → Vite 代理去掉 /api → Gateway 处理 /cloud-api 路由 → hiapi-cloud-public-app 服务 → Controller 分发

  • Webhook 渠道派发复用 hiapi-task-worker(已有成熟的 webhook 派发与重试),消息中心只投递任务过去。
  • SMS/EMAIL 渠道复用现有 ISenderService 体系(实现在 hiapi-cloud-public,经 FeignSender 调用,详见 §7.1),消息中心不重复对接阿里云/SMTP。

三、核心概念

3.1 消息类型

类型模型存储典型场景
通知 Notification一对一,推模式每接收者一行支付成功、设备离线、审核结果、配额告警
公告 Announcement一对多,拉模式一条记录 + 已读表版本更新、维护通知、运营活动

公告不做接收者展开(避免写放大 + 天然覆盖后加入的租户);通知逐行存储(个体已读状态 + 跳转上下文)。

3.2 渠道 Channel

INBOX        站内信(收件箱,未读数轮询刷新)
SMS          短信(复用现有 ISenderService/阿里云)
EMAIL        邮件(复用现有 ISenderService/SMTP)
WECHAT_MP    微信公众号模板消息(需 openid + 关注)
WECHAT_MINI  微信小程序订阅消息(需前端一次性授权收集)
APP_PUSH     APP 推送(UniPush 2.0,uniapp 生态最顺)
WEBHOOK      外部回调(复用 task-worker)

3.3 模板:业务模板 + 渠道模板 两层

一个业务动作对应一个业务模板(templateCode,如 PAY_SUCCESS),下挂多个渠道模板:

  • 站内信/邮件:标题 + 内容(变量占位 ${amount}),邮件为 HTML;
  • 短信:存服务商侧模板 ID + 变量映射(短信内容在运营商报备,本地只存映射);
  • 公众号/小程序:存微信侧 template_id + 字段映射。

业务方只传 templateCode + params,不知道渠道存在。

3.4 渠道账号配置:平台默认 + 商户覆盖 两级

参照 finance 支付渠道配置的先例:mid=0 为平台级默认配置,商户可配置自己的账号覆盖(典型:商户有自己的微信公众号,给自己的 user 发模板消息必须走商户自己的公众号;短信也可能用商户自己的签名)。解析顺序:商户配置 → 平台默认 → 该渠道不可用。

3.5 订阅偏好与合规

  • category 消息分类:TRADE / DEVICE / AUDIT / SECURITY / SYSTEM / MARKETING;
  • 接收人可按 分类 × 渠道 开关(偏好表,默认策略兜底);
  • 强制规则:SECURITY/TRADE 的站内信不可关;MARKETING 必须可退订(邮件带退订链接、短信回 T 退订),夜间(22:00–8:00)禁发营销类短信。

四、数据库设计(DDL 概要)

所有表带 mid 租户字段,查询强制按 token 过滤。

sql
-- ① 站内信收件箱(推模式)
msg_notification
  id, mid, receiver_type, receiver_id,
  category, title, content, extra,          -- extra JSON: {route, bizType, bizId}
  read, read_time, created
  KEY (receiver_type, receiver_id, read, created)

-- ② 公告(拉模式)
msg_announcement
  id, mid,                                   -- mid=0 平台发布
  scope,                                     -- ALL_MERCHANT / ALL_CHANNEL / MERCHANT_USERS
  title, content, level,                     -- level: NORMAL / IMPORTANT(登录弹窗)
  status,                                    -- DRAFT / PUBLISHED / REVOKED,发布后内容只读
  publish_time, expire_time, created

-- ③ 公告已读(点开才插行)
msg_announcement_read
  id, announcement_id, receiver_type, receiver_id, created
  UNIQUE (announcement_id, receiver_type, receiver_id)

-- ④ 业务模板
msg_template
  id, code UNIQUE, name, category, remark,
  channels,                                  -- 默认启用渠道集合 JSON
  status, created

-- ⑤ 渠道模板
msg_template_channel
  id, template_id, channel,
  title, content,                            -- INBOX/EMAIL 用
  provider_template_id, field_mapping,       -- SMS/WECHAT_* 用
  enabled
  UNIQUE (template_id, channel)

-- ⑥ 发送主记录(一次发送请求一行,幂等锚点)
msg_send_record
  id, mid, template_code, biz_id,            -- UNIQUE(template_code, biz_id) 幂等
  receiver_type, receiver_id, params,
  channels_planned, status, created          -- status: PROCESSING / DONE / PARTIAL_FAILED

-- ⑦ 渠道派发任务(outbox,每渠道一行)
msg_channel_task
  id, send_id, channel, provider,
  target,                                    -- 手机号/邮箱/openid/push-cid(脱敏存储)
  rendered_content,
  status,                                    -- PENDING / SENDING / SUCCESS / FAILED / DEAD
  fail_reason, retry_count, next_retry_time,
  provider_msg_id,                           -- 回执关联(短信状态报告等)
  created, updated
  KEY (status, next_retry_time)

-- ⑧ 渠道账号配置(两级)
msg_channel_config
  id, mid,                                   -- mid=0 平台默认
  channel, provider, config,                 -- config JSON 加密存储(appid/secret/签名)
  enabled
  UNIQUE (mid, channel)

-- ⑨ 订阅偏好
msg_preference
  id, receiver_type, receiver_id, category, channel, enabled
  UNIQUE (receiver_type, receiver_id, category, channel)

-- ⑩ 推送令牌(APP 推送 cid / 小程序订阅授权余量)
msg_push_token
  id, receiver_type, receiver_id, platform,  -- platform: APP_ANDROID / APP_IOS / MP_WEIXIN
  token, updated

接收人的手机号/邮箱/openid 不冗余存储,发送时经 Feign 向 user 服务实时解析(user 服务提供批量解析接口),保证换绑即时生效。

五、发送链路

业务服务                    message-core                       message-dispatcher
   │  ①NotifyEvent(MQ)          │                                    │
   ├──或 Feign SendApi ────────▶│ ②幂等检查(template_code+biz_id)     │
   │                            │ ③加载业务模板+渠道模板               │
   │                            │ ④渠道决策 = 模板默认渠道             │
   │                            │     ∩ 接收人偏好 ∩ 渠道配置可用      │
   │                            │ ⑤解析接收人地址(Feign→user)         │
   │                            │ ⑥渲染内容                          │
   │                            │ ⑦INBOX: 落 notification + 未读数   │
   │                            │ ⑧其它渠道: 落 channel_task(outbox) │
   │                            │     事务提交后投 MQ ───────────────▶│ ⑨调 Provider
   │                            │                                    │ ⑩成功→SUCCESS
   │                            │                                    │   失败→退避重试
   │                            │◀── 回执回调(短信状态报告/微信事件) ──│   15s/1m/5m/30m/2h
   │                            │    更新 channel_task 终态           │   超限→DEAD+告警

要点:

  1. 两个发送入口并存:
    • 通用:NotifyEvent(新增 MsgRoutingKey.SYSTEM_NOTIFY = "system.notify"),异步、解耦、不阻塞业务事务,默认推荐;
    • 转换订阅:消息中心直接订阅既有业务事件(如 FINANCE_PAY_SUCCESS_ALLUSER_REGISTER)自动生成通知,业务方零改动;
    • Feign MessageSendApi 留给需要同步拿 sendId 的场景(如验证码短信——验证码走同一渠道层但跳过收件箱和偏好,单独接口)。
  2. 幂等:(template_code, biz_id) 唯一约束,MQ 重复投递天然免疫。
  3. Outbox:channel_task 与 send_record 同事务落库,TransactionSynchronization.afterCommit() 后投 MQ;15s 定时扫描 PENDING + next_retry_time 到期 兜底(与 finance NotifyDispatcher 同款思路)。
  4. 渠道熔断:某 provider 连续 N 次失败则熔断 5 分钟,任务留在队列等恢复,避免雪崩式重试。
  5. 频控:单接收人 × 渠道 × 日限额(Redis 计数),超限任务标记 SKIPPED。

六、消息触达方式(轮询 + 推送渠道,不做 WS)

不建 WebSocket/SSE 常驻通道(评审决策 2026-07-05):现有场景对秒级到达无硬需求,轮询 /unread-count 即可满足;省掉会话注册、扇出、心跳、网关升级一整套复杂度。未来若出现真实秒级场景(如客服 IM),再单独立项。

各端接入方式

触达方案说明
admin-ts(Web)铃铛角标 60s 轮询 /unread-count,切回页签/路由切换时立即刷一次接口只读 Redis,成本可忽略
uniapp APPUniPush 2.0(离线/在线厂商通道)+ 进入消息页拉取离线可达,点通知落地到 extra.route
uniapp 微信小程序小程序订阅消息(离线触达)+ onShow 拉未读数无常驻通道,本来也只能这样
uniapp H5页面 onShow / 定时轮询

未读数

Redis 增量计数 unread:{type}:{id}:写通知 INCR,已读 DECR/重算,GET /unread-count 直读 Redis(公告未读 = 范围内发布数 − 已读数,short TTL 缓存)。轮询打在 Redis 上,不碰 DB。

七、各渠道接入要点与前置依赖

渠道Provider前置依赖备注
INBOX本地阶段 0
SMS复用现有 SmsSenderService(阿里云),经 FeignSender平台短信账号 + 签名/模板报备(已有)见 §7.1
EMAIL复用现有 MailSenderService(SMTP),经 FeignSender发信邮箱(已有配置体系)见 §7.1
WECHAT_MP公众号模板消息 API商户认证服务号 + 用户 openid 绑定(user 服务微信登录体系)强依赖两级渠道配置
WECHAT_MINI小程序订阅消息前端在关键动作处 requestSubscribeMessage 收集授权,余量记入 msg_push_token一次性授权,发一条扣一次
APP_PUSHUniPush 2.0(个推)uniapp 开通 UniPush,APP 启动上报 cid离线可达
WEBHOOKtask-worker已有只投任务

7.1 现有 ISenderService 体系的复用与完善

现状:接口/模型在 hiapi-core-sender(fast-frame),实现在 hiapi-cloud-public 的 cn.hiapi.core.sender(SenderFactory + SmsSenderService 阿里云 + MailSenderService SMTP),入口 /public/sender + /feign/sender(FeignSender),发送记录 hiapi_core_sender_record(时间分片),验证码全生命周期(发送/verify/一次性使用/过期/区号校验)已完整,AccountController/UserController 在用。

分工:验证码链路保持现状不迁移(它自带 token/verify 语义,消息中心不掺和);消息中心的 SMS/EMAIL 通知类派发由 dispatcher 经 FeignSender 调用(MsgType.Content),渠道模板映射由消息中心 msg_template_channel 承担,ISenderService 只做"投递执行器"。

需要修的问题(接消息中心前完成):

  1. QUEUE 模式是死代码:SenderFactory QUEUE 模式落 status=0 后直接返回成功,但没有任何消费者,消息会永远躺在表里。消息中心的 outbox+dispatcher 就是它想要的异步化——删掉 QUEUE 模式,保留 SYNC。
  2. verify 并发可重放:校验通过后 update status=10 的 WHERE 只有 id+created,不含 status=2,并发两次 verify 同一验证码都能通过。WHERE 加 eq("status",2) 并校验影响行数。
  3. MailSenderService 异常契约不一致:发送失败抛 BasicException(Sms 是返回 toError),导致 SenderFactory finally 落库时 status 停留在 0(待发送)且调用方拿到 500。统一为返回 ResponseEntity。
  4. 无频控:/public/sender 无需登录,没有 单手机号/IP 的频率限制(如 1 条/分钟、5 条/日),存在短信轰炸 + 费用攻击风险。加 Redis 频控(消息中心阶段 2 的频控组件可先在这里落地)。
  5. 短信模板兜底危险:resolveTemplateCode 找不到模板时回退 register_template,会把任意业务短信按注册模板发出去。改为直接报错。

接消息中心时的增强(阶段 2 顺手做):

  1. 配置支持 mid 两级解析:AbsSenderService.getConfig 目前硬编码 mid=0(纯平台级),改为 先查 mid 再回退 0,即可支持商户自有短信签名/发信邮箱(对应 §3.4 两级渠道配置)。
  2. SenderRequest 支持显式服务商模板码:目前 templateId 依赖 config_sms JSON 里的映射,消息中心已有 msg_template_channel 存映射,允许直接传 provider template code,避免两处维护。
  3. 回执字段:阿里云返回的 BizId 透传回去存入 msg_channel_task.provider_msg_id,为短信状态报告对账留口。
  4. 小问题:verify 查询窗口固定 60 分钟(expire 更长时会查不到)、错误文案带调试后缀("不匹配t/cc/c")、serviceMap 旧客户端不清理(有界,可不管)。

八、API 设计

全部走标准 controller 体系(BasicQueryController / BasicCurdController),前缀即权限边界。

接收侧(merchant 为例,platform/channel/user 同构)

GET  /cloud-message/merchant/message/notification/query        分页(TablePage 直接对接)
POST /cloud-message/merchant/message/notification/read         批量已读 {ids:[...]}
POST /cloud-message/merchant/message/notification/read-all
GET  /cloud-message/merchant/message/notification/unread-count {notification, announcement}
GET  /cloud-message/merchant/message/announcement/query        按 scope 拉可见公告 + 已读标记(阶段1)
POST /cloud-message/merchant/message/announcement/read
GET  /cloud-message/merchant/message/preference/get            我的订阅偏好(阶段2)
POST /cloud-message/merchant/message/preference/update
POST /cloud-message/user/message/push-token/report             APP cid / 小程序订阅授权上报(阶段3)

路径遵循仓库惯例 /{accountType}/{service}/{resource}(如 /user/shop/order),service 段为 message

管理侧

平台(platform):
  /cloud-message/platform/template/*                   业务模板 + 渠道模板 CRUD
  /cloud-message/platform/announcement/*               平台公告 CRUD + publish/revoke
  /cloud-message/platform/channel-config/*             平台级渠道账号
  /cloud-message/platform/send-record/query            全局发送记录 + 单条重发
  /cloud-message/platform/stats/*                      发送量/成功率/渠道分布

商户(merchant):
  /cloud-message/merchant/announcement-manage/*        商户→自己 user 的公告
  /cloud-message/merchant/channel-config/*             商户自有渠道(自己的公众号/短信签名)
  /cloud-message/merchant/send-record/query            本租户发送记录

内部发送

java
// MQ(推荐): MsgRoutingKey.SYSTEM_NOTIFY
NotifyEvent { mid, receiverType, receiverId, templateCode, params(Map), bizId, channelsOverride? }

// Feign: MessageSendApi
send(SendRequest): sendId                // 同步场景
sendCaptcha(phone/email, code, scene)    // 验证码专线:跳过收件箱/偏好,强频控

九、前端设计

admin-ts(三类账户共用组件,菜单按账户类型挂)

  • 顶栏铃铛 + el-badge:60s 轮询 /unread-count(路由切换时立即刷一次);下拉最近 5 条 + 「查看全部」;
  • 消息中心页:tab(通知/公告),通知用 TablePage(category/已读状态筛选),extra.route 点击直达业务页;
  • level=IMPORTANT 公告登录后弹窗(本地记已弹);
  • 平台管理页:模板管理(业务模板 + 渠道模板 tab)、渠道配置、公告管理(富文本)、发送记录(含失败原因/手动重发)、统计看板;
  • 商户管理页:公告管理、自有渠道配置、发送记录。

uniapp

  • 消息中心页:wd-tabs 按 category 分类,列表 + 角标(tabbar badge);
  • extra.route → uni-mini-router 跳转;
  • 关键动作处(如下单)调 requestSubscribeMessage 收集小程序订阅授权;
  • APP 端启动上报 UniPush cid;HTTP 一律 HiapiUtils

十、治理与运维

  • 归档:msg_notification / msg_channel_task 按 created 保留 6 个月,定时任务移入归档表;
  • 统计:按日汇总 渠道 × 模板 × 租户 的发送量/成功率/失败 TopN,平台看板展示;
  • 计费预留:channel_task 含 mid + channel,后续按量对接 finance 资产扣费(短信成本转嫁商户)即有账可算;
  • 告警:DEAD 任务、渠道熔断、回执大量失败 → 通过消息中心自己给平台管理员发通知(吃自己的狗粮);
  • 安全:渠道 config JSON 加密落库;target(手机号/邮箱)脱敏展示。

十一、分阶段实施

阶段交付物依赖/前置
0 地基 + 站内信 ✅已完成(2026-07-05)hiapi-cloud-message 服务骨架、①表、NotifyEvent(system.notify)、四类账户 notification query/read/read-all/unread-count、admin 铃铛(60s 轮询)+ 三端共用消息中心页、public-web user 端消息中心页、订阅 FINANCE_PAY_SUCCESS_ALL/USER_REGISTER 做首批自动通知。待部署:建库 hiapi_cloud_message(表 ddl-auto 自建)、Nacos 网关加 /cloud-message/**lb://hiapi-cloud-message 路由、菜单表插三端「消息中心」菜单(pages.ts 已有种子,铃铛不依赖菜单)、平台注册 app-id P-10005
1 公告 + 模板②③④⑤表、平台/商户公告发布端(富文本 + 弹窗)、模板管理页、发送链路改为模板渲染阶段 0
2 多渠道派发框架 + 短信/邮件⑥⑦⑧⑨表、outbox + 重试 + 熔断 + 频控、SMS/EMAIL 经 FeignSender 复用现有 ISenderService、§7.1 问题 1–5 修复 + 增强 6–8、两级渠道配置页、偏好页 + 退订(验证码链路保持现状不迁移)短信账号/签名/发信邮箱均已有
3 微信/APP 推送UniPush 2.0、公众号模板消息、小程序订阅消息 + 授权收集服务号认证、UniPush 开通、openid 绑定链路确认
4 治理闭环统计看板、归档任务、DEAD 告警、计费对接 finance(可选)阶段 2/3 数据积累

每阶段独立可上线、可回滚;阶段 0/1 不碰任何第三方,零外部依赖先把骨架和体验立起来。

十二、待确认决策点

  1. 短信服务商 已定:阿里云(现有 SmsSenderService 就是阿里云,直接复用)。
  2. 公众号现状:平台/商户目前是否已有认证服务号、user 微信登录是否已沉淀 openid?决定阶段 3 的 WECHAT_MP 是否要先补绑定链路。
  3. 验证码短信盘点 已盘点:SenderFactory + /public/sender + FeignSender,调用点为 AccountController(登录/注册)与 UserController(换绑手机/邮箱)。决策:验证码链路保持现状不迁移,只按 §7.1 修复问题 1–5。