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 三种粒度推送:
publicinterfaceWebSocketMessageSender{/** 发送消息给指定用户 */voidsend(IntegeruserType,LonguserId,StringmessageType,StringmessageContent);/** 发送消息给指定用户类型(广播) */voidsend(IntegeruserType,StringmessageType,StringmessageContent);/** 发送消息给指定 Session */voidsend(StringsessionId,StringmessageType,StringmessageContent);/** 便捷方法:对象自动转 JSON 后发送 */defaultvoidsendObject(IntegeruserType,LonguserId,StringmessageType,ObjectmessageContent){send(userType,userId,messageType,JsonUtils.toJsonString(messageContent));}}4.2 落库 + 推送双写
发站内信时,先落库(保证可查、离线也能看),再通过sendObject给在线用户推一条 WebSocket 消息(让前端红点立刻 +1)。落库是"可靠性",WebSocket 是"实时性",双写兼得。前端建立 WebSocket 长连接后,收到消息即更新未读角标,无需刷新页面。
五、后端核心实现
5.1 站内信统一发送(核心)
NotifySendServiceImpl.sendSingleNotify是站内信的统一入口——校验模板、校验参数、渲染内容、落库一气呵成。业务方只需提供userId + templateCode + params:
@OverridepublicLongsendSingleNotify(LonguserId,IntegeruserType,StringtemplateCode,Map<String,Object>templateParams){// 1. 校验模板(缓存读取),模板禁用则直接返回不发送NotifyTemplateDOtemplate=validateNotifyTemplate(templateCode);if(Objects.equals(template.getStatus(),CommonStatusEnum.DISABLE.getStatus())){log.info("[sendSingleNotify][模版({})已关闭,无法发送]",templateCode);returnnull;}// 2. 校验必填参数validateTemplateParams(template,templateParams);// 3. 渲染内容({key} 占位符替换)Stringcontent=notifyTemplateService.formatNotifyTemplateContent(template.getContent(),templateParams);// 4. 落库(同时可触发 WebSocket 推送)returnnotifyMessageService.createNotifyMessage(userId,userType,template,content,templateParams);}占位符渲染本身极简——直接复用 Hutool 的StrUtil.format,把{key}替换成 Map 里的值:
@OverridepublicStringformatNotifyTemplateContent(Stringcontent,Map<String,Object>params){returnStrUtil.format(content,params);}5.2 短信发送:同步建日志 + 异步发 MQ
短信涉及外部网络 IO,不能阻塞业务主流程。sendSingleSms的设计很有代表性:同步部分只做校验和建日志,真正的发送通过 MQ 异步执行:
@OverridepublicLongsendSingleSms(Stringmobile,LonguserId,IntegeruserType,StringtemplateCode,Map<String,Object>templateParams){// 1. 校验模板、渠道、手机号SmsTemplateDOtemplate=validateSmsTemplate(templateCode);SmsChannelDOsmsChannel=validateSmsChannel(template.getChannelId());mobile=validateMobile(mobile);// 2. 构建有序参数(兼容腾讯云等用数组下标的服务商)List<KeyValue<String,Object>>newTemplateParams=buildTemplateParams(template,templateParams);// 3. 模板/渠道都启用才真发;否则只记日志BooleanisSend=CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus())&&CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus());Stringcontent=smsTemplateService.formatSmsTemplateContent(template.getContent(),templateParams);LongsendLogId=smsLogService.createSmsLog(mobile,userId,userType,isSend,template,content,templateParams);// 4. 发 MQ 消息,异步真正发送短信if(isSend){smsProducer.sendSmsSendMessage(sendLogId,mobile,template.getChannelId(),template.getApiTemplateId(),newTemplateParams);}returnsendLogId;// 返回日志 ID,便于追踪}设计巧思:先建日志拿到sendLogId,再把它随 MQ 消息一起发出去;消费端真正发完后,用这个 ID 回填发送结果与服务商回执,形成"建日志 → 异步发 → 回填结果"的完整闭环。
5.3 站内信落库:内容快照
createNotifyMessage落库时,把模板信息和渲染后的内容一并冗余进消息记录,这样历史消息不受模板后续修改影响:
@OverridepublicLongcreateNotifyMessage(LonguserId,IntegeruserType,NotifyTemplateDOtemplate,StringtemplateContent,Map<String,Object>templateParams){NotifyMessageDOmessage=newNotifyMessageDO().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);returnmessage.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)
操作路径:系统管理 → 站内信管理(模板/消息)、短信管理(渠道/模板/日志)、邮件管理、通知公告
推荐体验流程:
- 进入「站内信模板」,新建一个模板,内容里写上
{name}占位符; - 点击"发送站内信",选择接收人、填入参数,发送;
- 切到右上角通知图标 / 「我的站内信」,查看刚收到的消息与未读角标;
- 进入「短信渠道」配置一个服务商(本地可用 DEBUG 钉钉渠道联调);
- 在「短信模板」维护模板并关联渠道,发送后到「短信日志」查看发送记录;
- 发布一条「通知公告」并标记为重要,观察全员公告效果。
源码仓库:
| 仓库 | 地址 |
|---|---|
| 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 支持一下!