SpringBoot+Vue3 企业消息通知中心设计:站内信+短信+邮件+WebSocket 多通道统一推送
🌐 演示地址:http://ruoyioffice.com | 📦 源码1·GitHub:ruoyi-office | 📦 源码2·GitCode:ruoyi-office | 📦 源码3·Gitee:ruoyi-office | 💬 微信:17156169080(备注「RuoYi Office」)
“审批到你了”“验证码 6 位”“您有一条新公告”——企业系统里,消息无处不在。但很多项目的消息发送是这样的:登录发验证码的代码里写死阿里云 SDK,审批通知的代码里又拼了一段站内信 SQL,改个短信文案要改 Java 代码、重新发版,想换个短信服务商更是牵一发动全身。消息散落、硬编码、不可配、发了没记录、用户收不到也查不到,是绝大多数后台系统的通病。RuoYi Office 用"模板 + 渠道 + 日志"三件套统一抽象站内信、短信、邮件三大通道,业务只管喊一句 sendSingleNotify(userId, "templateCode", params),剩下的渲染、发送、落库、实时推送全由通知中心兜底,把"硬编码发消息"收敛成一个可配置、可复用、可追溯的通知引擎。

▲ 消息通知中心架构全景:业务侧统一调用发送 API → 模板渲染({key} 占位符)→ 三通道分发(站内信落库+WebSocket / 短信走 MQ 异步 / 邮件 SMTP)→ 发送日志全程留痕
引言:企业消息推送到底难在哪?
“不就是发条消息吗?”——直到你维护一个有几十处发消息的系统,才会明白它有多乱:
硬编码满天飞:验证码短信的内容、审批通知的文案,全写死在各个 Service 里。运营想把"您好"改成"亲",得提需求、改代码、走发版——一句文案改动要折腾一周。
渠道绑死:发短信的代码直接 new 了阿里云的 client。哪天阿里云涨价想换腾讯云,得把全项目搜一遍 AliyunSmsClient 挨个改,风险极高。
发了查不到:用户投诉"我没收到验证码",但系统里没有任何发送记录——发没发、发给谁、成功还是失败、服务商返回了啥,一概不知,根本没法排查。
通道各自为政:站内信一套代码、短信一套代码、邮件又一套代码,参数格式、模板机制、调用方式全不一样。新人接手要学三套,加个"钉钉通知"还得再造一套。
实时性差:站内信写进数据库了,但用户的浏览器不知道,必须刷新页面才看到红点。审批催办这类强时效消息,体验很差。
本文以 RuoYi Office 的 yudao-module-system 通知模块为例,完整拆解其"模板 + 渠道 + 日志"统一抽象、templateCode 触发、{key} 占位符渲染、MQ 异步发送、WebSocket 实时推送的设计方案。所有结论均来自真实源码。
一、业务设计:模板 + 渠道 + 日志三件套
1.1 核心理念:把"发消息"抽象成统一三段式
消息通知中心是什么?它是一个把"业务事件"翻译成"用户可感知消息"的统一引擎。无论站内信、短信还是邮件,都遵循同一套抽象:
业务事件 ──▶ ① 选模板(templateCode) ──▶ ② 填参数(templateParams)
│
▼
③ 渲染内容({key} 占位符替换)
│
┌───────────────┬───────────┴───────────┐
▼ ▼ ▼
站内信(落库 短信(走 MQ 邮件(SMTP
+ WebSocket) 异步发送) 发送)
│ │ │
└───────────────┴───────────────────────┘
▼
④ 发送日志全程留痕
| 模板 | NotifyTemplateDO | SmsTemplateDO | MailTemplateDO |
| 渠道/账号 | (内部,无外部渠道) | SmsChannelDO(多服务商) | MailAccountDO(SMTP 账号) |
| 消息/日志 | NotifyMessageDO | SmsLogDO | MailLogDO |
这套抽象的价值:业务方永远只跟"模板编码 + 参数"打交道,至于内容长什么样、走哪个服务商、怎么异步、怎么记日志,全部对业务透明。
1.2 模板机制:内容与代码彻底分离
三大通道的模板都用同一套占位符机制——内容里用 {key} 标记变量,发送时传一个 Map<String, Object> 把变量填进去。站内信模板 NotifyTemplateDO 的核心字段:
| code | 模板编码(业务调用的唯一标识,如 bpm_task_assigned) |
| name | 模板名称 |
| type | 模板类型(字典 system_notify_template_type) |
| content | 模板内容,含 {key} 占位符 |
| params | 参数数组(JSON),用于发送前校验必填参数 |
| status | 启用 / 禁用 |
举例:模板内容 你收到了一条待办任务【{taskName}】,请及时处理,发送时传 {"taskName": "请假审批"},渲染出 你收到了一条待办任务【请假审批】,请及时处理。改文案 = 改数据库里的模板,不用动代码、不用发版。
1.3 短信渠道:一套代码切换多家服务商
短信渠道 SmsChannelDO 把"服务商"抽象成可配置的渠道,code 字段对应 SmsChannelEnum 枚举,支持主流服务商:
| ALIYUN | 阿里云 |
| TENCENT | 腾讯云 |
| HUAWEI | 华为云 |
| QINIU | 七牛云 |
| DEBUG_DING_TALK | 调试(钉钉,本地联调用) |
每个渠道配 signature(签名)、apiKey、apiSecret、callbackUrl。换服务商只需在后台新建一个渠道、把模板关联过去,零代码改动——这就是"渠道抽象"的威力。
二、系统设计:模块职责与设计决策
2.1 模块组成
通知中心位于 系统管理 → 消息中心 / 通知公告 目录下:
| 站内信 | 模板管理 / 消息记录 | 维护站内信模板、查看发送记录、手动发送 | 管理员 |
| 我的站内信 | 用户侧收件箱 | 查看/标记已读、跳转业务详情 | 全体用户 |
| 短信 | 渠道 / 模板 / 日志 | 配置服务商、维护短信模板、查发送日志 | 管理员 |
| 邮件 | 账号 / 模板 / 日志 | 配置 SMTP 账号、维护邮件模板、查日志 | 管理员 |
| 通知公告 | 公告管理 | 发布公司公告、规章制度,标记重要 | 管理员 |
2.2 核心设计决策
| 统一抽象 | 模板 + 渠道 + 日志三件套 | 三通道同构,新增通道成本低 |
| 内容配置化 | {key} 占位符 + 模板落库 | 改文案不改代码、不发版 |
| 渠道可插拔 | SmsChannelEnum + SmsClient | 换服务商零代码改动 |
| 发送方式 | 站内信同步落库,短信走 MQ 异步 | 短信有外部 IO,异步不阻塞主流程 |
| 全程留痕 | 每次发送写 Log(成功/失败/返回) | 可排查、可统计、可审计 |
| 实时触达 | 站内信落库 + WebSocket 推送 | 既能查历史,又能实时弹角标 |
| 模板缓存 | 模板按 code 走缓存读取 | 高频发送不打数据库 |
三、PC 端功能实现
3.1 站内信模板与发送
站内信模板管理页维护模板编码、名称、类型、内容(含 {key} 占位符)和参数列表。管理员可在模板页直接"发送测试",选择接收人、填入参数即可。 
▲ 站内信模板管理:逐行展示模板编码(如 bpm_task_assigned)、名称、类型、内容预览、状态;右侧操作含"发送站内信",可选接收人并填充占位符参数
站内信设计要点:
- 模板缓存读取:getNotifyTemplateByCodeFromCache(code) 从缓存取模板,避免高频发送频繁查库。
- 发送前参数校验:validateTemplateParams 遍历模板声明的 params,缺参数直接抛 NOTIFY_SEND_TEMPLATE_PARAM_MISS,杜绝"发出空白消息"。
- 内容快照落库:站内信落库时把渲染后的内容存进 NotifyMessageDO.templateContent,即使日后模板改了,历史消息内容不变。
3.2 短信渠道、模板与日志
短信模块三页联动:先在「短信渠道」配置服务商(阿里云/腾讯云…)和 API 密钥,再在「短信模板」维护内容并关联渠道与服务商模板 ID(apiTemplateId),最后所有发送记录都进「短信日志」可查。 
▲ 短信日志:逐行记录手机号、模板编码、短信渠道、渲染后内容、发送状态(成功/失败)、服务商回执状态与时间,用户投诉"没收到验证码"时一查便知
短信设计要点:
- 渠道与模板分离:模板关联 channelId,换渠道改关联即可;模板 params 自动根据内容里的 {key} 生成。
- 有序参数:部分服务商(如腾讯云)用数组下标而非 key 传参,buildTemplateParams 把 Map 转成有序 KeyValue 数组兼容。
- 禁用也记日志:模板或渠道被禁用时,不真正发送,但仍写一条日志(isSend=false),保留痕迹。
3.3 通知公告
通知公告面向全员发布——公司动态、规章制度、行业资讯等,支持标记"重要通知",配合阅读记录 NoticeReadDO 统计谁已读。
公告设计要点:
- 类型字典化:type 关联字典 system_notice_type(通知公告/公司动态/规章制度等),前端统一渲染标签。
- 重要标记:isImportant 为重要公告,前端可置顶或强提醒。
- 富文本内容:content 支持富文本编辑器,图文混排。
四、实时推送:WebSocket 让红点"秒亮"
4.1 统一发送器接口
站内信落库只解决了"可查",要做到"实时弹角标"还得靠 WebSocket。框架抽象了统一发送器 WebSocketMessageSender,支持按用户、按用户类型、按 Session 三种粒度推送:
public interface WebSocketMessageSender {
/** 发送消息给指定用户 */
void send(Integer userType, Long userId, String messageType, String messageContent);
/** 发送消息给指定用户类型(广播) */
void send(Integer userType, String messageType, String messageContent);
/** 发送消息给指定 Session */
void send(String sessionId, String messageType, String messageContent);
/** 便捷方法:对象自动转 JSON 后发送 */
default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
}
}
4.2 落库 + 推送双写
发站内信时,先落库(保证可查、离线也能看),再通过 sendObject 给在线用户推一条 WebSocket 消息(让前端红点立刻 +1)。落库是"可靠性",WebSocket 是"实时性",双写兼得。 前端建立 WebSocket 长连接后,收到消息即更新未读角标,无需刷新页面。
五、后端核心实现
5.1 站内信统一发送(核心)
NotifySendServiceImpl.sendSingleNotify 是站内信的统一入口——校验模板、校验参数、渲染内容、落库一气呵成。业务方只需提供 userId + templateCode + params:
@Override
public Long sendSingleNotify(Long userId, Integer userType, String templateCode, Map<String, Object> templateParams) {
// 1. 校验模板(缓存读取),模板禁用则直接返回不发送
NotifyTemplateDO template = validateNotifyTemplate(templateCode);
if (Objects.equals(template.getStatus(), CommonStatusEnum.DISABLE.getStatus())) {
log.info("[sendSingleNotify][模版({})已关闭,无法发送]", templateCode);
return null;
}
// 2. 校验必填参数
validateTemplateParams(template, templateParams);
// 3. 渲染内容({key} 占位符替换)
String content = notifyTemplateService.formatNotifyTemplateContent(template.getContent(), templateParams);
// 4. 落库(同时可触发 WebSocket 推送)
return notifyMessageService.createNotifyMessage(userId, userType, template, content, templateParams);
}
占位符渲染本身极简——直接复用 Hutool 的 StrUtil.format,把 {key} 替换成 Map 里的值:
@Override
public String formatNotifyTemplateContent(String content, Map<String, Object> params) {
return StrUtil.format(content, params);
}
5.2 短信发送:同步建日志 + 异步发 MQ
短信涉及外部网络 IO,不能阻塞业务主流程。sendSingleSms 的设计很有代表性:同步部分只做校验和建日志,真正的发送通过 MQ 异步执行:
@Override
public Long sendSingleSms(String mobile, Long userId, Integer userType,
String templateCode, Map<String, Object> templateParams) {
// 1. 校验模板、渠道、手机号
SmsTemplateDO template = validateSmsTemplate(templateCode);
SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId());
mobile = validateMobile(mobile);
// 2. 构建有序参数(兼容腾讯云等用数组下标的服务商)
List<KeyValue<String, Object>> newTemplateParams = buildTemplateParams(template, templateParams);
// 3. 模板/渠道都启用才真发;否则只记日志
Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())
&& CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());
String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams);
Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams);
// 4. 发 MQ 消息,异步真正发送短信
if (isSend) {
smsProducer.sendSmsSendMessage(sendLogId, mobile, template.getChannelId(),
template.getApiTemplateId(), newTemplateParams);
}
return sendLogId; // 返回日志 ID,便于追踪
}
设计巧思:先建日志拿到 sendLogId,再把它随 MQ 消息一起发出去;消费端真正发完后,用这个 ID 回填发送结果与服务商回执,形成"建日志 → 异步发 → 回填结果"的完整闭环。
5.3 站内信落库:内容快照
createNotifyMessage 落库时,把模板信息和渲染后的内容一并冗余进消息记录,这样历史消息不受模板后续修改影响:
@Override
public Long createNotifyMessage(Long userId, Integer userType,
NotifyTemplateDO template, String templateContent, Map<String, Object> templateParams) {
NotifyMessageDO message = new NotifyMessageDO().setUserId(userId).setUserType(userType)
.setTemplateId(template.getId()).setTemplateCode(template.getCode())
.setTemplateType(template.getType()).setTemplateNickname(template.getNickname())
.setTemplateContent(templateContent) // 渲染后内容快照
.setTemplateParams(templateParams)
.setReadStatus(false); // 默认未读
notifyMessageMapper.insert(message);
return message.getId();
}
六、RuoYi Office 通知中心创新设计
6.1 三通道同构,新增通道成本极低
站内信、短信、邮件都遵循"模板 + 渠道 + 日志 + 统一发送 Service"的同一套骨架。要新增"钉钉/企业微信通知",照着这套骨架加一组类即可,业务调用方式完全一致,无需重新设计。
6.2 业务零感知的统一 API
业务方发消息只写一行 sendSingleNotifyToAdmin(userId, "templateCode", params),完全不关心模板内容、服务商、异步、日志。业务与消息基础设施彻底解耦,这是大型系统可维护性的关键。
6.3 异步 + 日志,外部 IO 不拖垮主流程
短信、邮件这类有外部网络调用的通道,发送走 MQ 异步,主流程拿到日志 ID 立即返回。即使服务商抖动、超时,也不会阻塞用户的登录、审批等核心操作,且每次发送都有日志可查。
6.4 落库 + WebSocket 双写
站内信既写库(可靠、可查、离线可见),又推 WebSocket(实时弹角标)。鱼和熊掌兼得,既不丢消息,又能秒级触达在线用户。
七、数据结构
7.1 站内信 system_notify_template / system_notify_message
| 模板 | code / name / type / content / params / status | 模板编码、内容、占位符参数、状态 |
| 消息 | user_id / user_type | 接收用户与类型(管理端/会员端) |
| 消息 | template_code / template_content | 冗余模板编码与渲染后内容快照 |
| 消息 | template_params(JSON) | 渲染参数 |
| 消息 | read_status / read_time | 是否已读 / 阅读时间 |
7.2 短信 system_sms_channel / system_sms_template / system_sms_log
| 渠道 | code / signature / api_key / api_secret | 服务商编码、签名、密钥 |
| 模板 | code / content / params(JSON) | 模板编码、内容、参数 |
| 模板 | channel_id / api_template_id | 关联渠道、服务商侧模板 ID |
| 日志 | mobile / content / send_status / receive_status | 手机号、内容、发送/回执状态 |
7.3 邮件 system_mail_account / system_mail_template / system_mail_log 与通知公告 system_notice
| 邮件账号 | mail / username / password / host / port | SMTP 发件账号 |
| 邮件模板 | code / title / content / params | 模板编码、标题、内容 |
| 通知公告 | title / type / content / status / is_important | 标题、类型、内容、重要标记 |
设计要点:站内信、短信模板都把渲染后内容或有序参数冗余落库,配合发送日志,实现"发了什么、发给谁、成没成功"全程可追溯。
八、技术亮点总结
| 统一三件套抽象 | 模板 + 渠道 + 日志 | 三通道同构,新增通道成本低 |
| 内容配置化 | {key} 占位符 + StrUtil.format | 改文案不改代码、不发版 |
| 渠道可插拔 | SmsChannelEnum + SmsClient | 换服务商零代码改动 |
| 统一发送 API | sendSingleNotify/Sms/Mail | 业务零感知,一行调用 |
| 参数校验前置 | 按模板 params 校验必填 | 杜绝发出空白/错误消息 |
| 异步发送 | 短信走 RocketMQ + @Async | 外部 IO 不阻塞主流程 |
| 全程日志 | 每次发送写 Log(含禁用场景) | 可排查、可统计、可审计 |
| 内容快照 | 落库存渲染后内容 | 模板变更不影响历史消息 |
| 实时推送 | 落库 + WebSocket 双写 | 红点秒亮,又不丢消息 |
| 模板缓存 | 按 code 缓存读取 | 高频发送不打数据库 |
九、快速体验
在线演示:http://ruoyioffice.com/web/(账号 admin / admin123)
操作路径:系统管理 → 站内信管理(模板/消息)、短信管理(渠道/模板/日志)、邮件管理、通知公告
推荐体验流程:
源码仓库:
| GitHub | github.com/yuqing2026/ruoyi-office |
| GitCode | gitcode.com/zhouzhongyan/ruoyi-office |
| Gitee | gitee.com/yqzy1688/ruoyi-office |
结语
消息通知中心的核心设计思想是:把"发消息"这件到处都在做的小事,抽象成"模板 + 渠道 + 日志"的统一三件套,让业务方只跟模板编码和参数打交道,把渲染、分发、异步、留痕、实时推送全部下沉到基础设施。 这套"统一抽象 + 配置化 + 可插拔 + 异步留痕"的模式,是所有"横切关注点"(日志、文件、支付、消息)的通用解法——一旦抽象到位,新增一个通道、换一个服务商、改一句文案,都不再是伤筋动骨的大事。
你们项目的消息发送还在硬编码吗?换过短信服务商踩过哪些坑?欢迎在评论区聊聊。
常见问题(FAQ)
RuoYi Office 的消息通知中心是开源免费的吗?
是。基于 RuoYi-Vue-Pro / Yudao 架构,后端 Spring Boot 3.5 + 前端 Vue3,开源可商用。通知模块位于 yudao-module-system,本地约 10 分钟即可启动。
支持哪些短信服务商?
内置阿里云、腾讯云、华为云、七牛云四家,以及本地联调用的"调试(钉钉)"渠道。换服务商只需在后台新建渠道并关联模板,无需改代码。
改短信/站内信文案需要改代码、重新发版吗?
不需要。所有文案都是数据库里的模板,内容用 {key} 占位符,发送时传参渲染。改文案 = 改后台模板,即时生效。
站内信怎么做到实时弹未读角标?
采用"落库 + WebSocket 双写":消息落库保证可查、离线可见,同时通过 WebSocketMessageSender 给在线用户推送,前端长连接收到后立即更新红点,无需刷新。
发送失败了怎么排查?
每次发送都会写日志(SmsLogDO / MailLogDO / 站内信记录),包含接收人、渲染内容、发送状态、服务商回执。即使模板被禁用未真发,也会记一条 isSend=false 的日志,全程可追溯。
💡 想要体验 RuoYi Office 的强大功能?
🌐 在线演示:http://ruoyioffice.com/web/(账号 admin / admin123)
📦 源码仓库:GitHub | GitCode | Gitee
💬 技术咨询:添加微信 17156169080,备注「RuoYi Office」
⭐ 如果觉得不错,请给个 Star 支持一下!




