Springboot+WebSocket×场景×渠道:企业统一消息中心编排实战

Springboot+WebSocket×场景×渠道:企业统一消息中心编排实战

一句话 :业务只喊 sceneCode;渲染、多渠道投递、落库、红点推送、改期取消------全交给消息中心编排,别在每个 Service 里 new 短信客户端。

开源仓库:GitCode · RuoyiOfficeAtomGit · RuoyiOffice


先认清:你不是缺「发短信的 SDK」,你缺「编排」

企业里消息失败的典型症状:

症状 根因
审批到了,角标不跳 只写了库,没 WebSocket;或前端没订 notify 类型
同一事件短信轰炸 各模块各自 sendSms,无场景日限、无去重
改一句文案要发版 模板硬编码在 Java
用户说「没收到」无法举证 无发送日志 / 无服务商回执
会议改期仍收到旧提醒 cancelByBiz,定时消息成孤儿
换短信云厂商改半天 渠道与业务耦合

老一代做法是「站内信一套 + 短信一套 + 邮件一套」。RuoyiOffice 在此之上收敛出 统一消息中心(Msg)

场景(Scene)→ 实例(Instance)→ 发送日志(Log = 接收人 × 渠道)→ 渠道适配器

业务侧只依赖一个发送 API。


产品界面长什么样?

站内信模板与我的消息------用户最终看见的「收件箱」:

运营配置的「消息定义 / 场景」则在系统管理下的消息中心:场景码、默认渠道、各渠道绑定的原生模板编码、接收人规则。业务代码只认 sceneCode,不认某家云的 TemplateId。


领域模型:四张表理清职责

text 复制代码
MsgScene(消息定义)
  code = BPM_TASK_APPROVE
  channels = [站内信, 短信, ...]
  ├─ MsgSceneChannel:channel → templateCode / provider
  └─ MsgSceneReceiver:角色/发起人/指定人/SpEL...

一次调用 MessageSendApi.send(sceneCode, params, bizId...)
        │
        ▼
MsgInstance(消息实例)
  标题快照、参数、接收人快照、计划发送时间、状态 WAIT/SENT/CANCEL
        │
        ▼
MsgSendLog(发送记录)  receiver × channel 笛卡尔(可裁剪)
  WAIT → 适配器发送 → SUCCESS / FAIL + retryCount
对象 一句话
Scene 「这类事要通知谁、走哪些渠、用哪套模板」的配置
Instance 「这件事在这一刻要发」的批次头
SendLog 「发给张三的短信」「发给李四的站内信」原子投递单
ChannelAdapter 真正对接站内信 / 短信 / 邮件 / App / 微信

这样拆的好处:短信失败不影响站内信已成功;重试只打失败的 Log;取消按 bizType+bizId 扫未发送实例。


发送编排:八步流水线

核心 Service 的顺序(与实现一致):

  1. 校验场景存在且启用
  2. 算渠道 = 请求覆盖 or 场景默认
  3. 合并参数 (补 bizId / bizType
  4. 按渠道渲染:读场景渠道配置 → 原生模板解析 → 得到 title/content/link
  5. 解析接收人:场景接收人规则 + 请求指定人
  6. 落库实例(状态 WAIT)
  7. 生成 SendLog 批次(接收人 × 渠道,跳过不适用组合)
  8. 若计划时间已到dispatchInstance;否则等定时 Job
java 复制代码
// 业务侧示意:审批任务创建后
messageSendApi.send(MessageSendReqDTO.builder()
    .sceneCode("BPM_TASK_ASSIGN")
    .bizType("bpm_task")
    .bizId(String.valueOf(taskId))
    .params(Map.of(
        "processName", processName,
        "taskName", taskName,
        "assignee", nickname
    ))
    .build());

改期 / 撤回

java 复制代码
messageSendApi.cancelByBiz("meeting", meetingId); // 幂等取消未发送
// 再 send 一条新的 planSendTime

定时发送由 Job 扫描 planSendTime <= now 的 WAIT 实例再派发------会议提醒、绩效截止催办都靠它。

硬编码 vs 场景编排(对照)

维度 业务里直接调短信 SDK 统一消息中心
改文案 改代码发版 改模板 / 场景配置
加渠道 每个调用点改一遍 场景勾选 + 新 Adapter
举证 往往无日志 Instance + SendLog
改期 容易漏取消 cancelByBiz 幂等
红点 经常忘记推 WS 站内信适配器内置尽力推
测试 Mock 多家云 SDK Mock Dispatcher 即可

审批催办时序(概念)

text 复制代码
审批人变更 / 任务创建
    → BPM 监听或业务 Service
    → MessageSendApi.send(BPM_TASK_ASSIGN, bizId=taskId, params=...)
    → 实例 WAIT → 立即 dispatch
         ├─ 站内信 Log → 落库 + WS notify → 角标 +1
         ├─ 短信 Log   → 云厂商(若场景勾选)
         └─ 邮件 Log   → SMTP(若场景勾选)
用户点击铃铛 / 推送 payload.detailUrl
    → 打开待办详情

若任务转办:先 cancelByBiz("bpm_task", oldTaskId)(若仍有未发定时催办),再对新人 send------避免旧审批人继续收催。


渠道适配器:站内信为什么「双写」?

站内信适配器的关键设计:

text 复制代码
NotifyChannelAdapter.send(log)
  ① notifySendService.sendSingleNotify(...)  // 落库,可查历史
  ② webSocketSenderApi.sendObject(..., "notify", payload)  // 尽力推红点
     失败只 warn,不影响 ① 成功
通道 成功标准 实时性
站内信 DB 插入成功 刷新可见
WebSocket notify 用户在线且前端订阅 秒级角标
短信 / 邮件 服务商接受 秒~分钟

前端约定:订阅消息类型 notify,拿到 { id, title, content, detailUrl } 后刷新未读数。这与 IM 聊天 WebSocket 消息类型分离,避免审批红点与聊天气泡抢同一 handler。

整体通知能力还可以对照经典「模板 + 三通道」全景(历史能力仍在,Msg 是其上的编排层):


接收人与渠道裁剪

不是每个接收人都适合所有渠道:

  • 系统用户 → 站内信 / App / 微信订阅
  • 裸手机号 / 邮箱(外部联系人)→ 短信 / 邮件
  • 接收人可声明自己允许的 channels,与场景默认求交

这能避免「给外部专家发站内信却没有 userId」的无效 Log,也减少短信误触达。


和业务模块怎么解耦?

text 复制代码
OA / BPM / HRM / CRM
        │  Feign / 本地 API
        ▼
MessageSendApi
        │
        ▼
MessageSendService(编排)
        │
        ├─ NativeTemplateResolver  → 复用站内信/短信/邮件既有模板表
        ├─ ReceiverResolver
        └─ MessageChannelDispatcher → 各 ChannelAdapter

业务 禁止 再直接依赖某云短信 SDK。要加「钉钉工作通知」:新 Adapter + 字典渠道值 + 场景勾选即可,旧 sceneCode 调用方零改动。


可观测与补偿

能力 做法
发送日志页 按场景、渠道、状态、接收人筛选
失败重试 retryCount + 可重试错误码
用户投诉 用 instanceId / bizId 拉齐全渠道结果
红点不准 查 WS 是否推送失败;前端是否只信本地缓存
定时漏发 查 Job 是否调度;实例是否仍 WAIT

运维口诀:先看 Instance 状态,再看 SendLog,最后看渠道回执。


与旧「通知模块」关系(避免选型混乱)

层级 职责
站内信模板 / 我的消息 用户收件箱与模板 CRUD(仍在)
短信 / 邮件模板与渠道账号 各通道原生能力(仍在)
Msg 场景中心 跨通道编排、实例、定时、按业务取消

新业务优先打 MessageSendApi;不要再复制一套「审批里直接调短信」。


落地检查清单

  • 每个业务通知定义唯一 sceneCode
  • 场景渠道都绑定了存在的原生 templateCode
  • 接收人规则在测试租户验证过(别只测 admin)
  • 前端订阅 WebSocket 类型 notify 并刷新未读
  • 改期/撤销调用 cancelByBiz
  • 短信渠道有频控与日上限(与登录短信场景隔离)
  • 发送日志对运营只读开放,含脱敏

FAQ

Q1:一次 send 会不会短信+站内信重复打扰?

A:这是产品决策。场景可默认「仅站内信」,紧急场景再勾短信;或按用户偏好表过滤渠道。

Q2:WebSocket 推失败怎么办?

A:站内信已落库;用户打开铃铛仍能看到。可补偿:下次心跳拉未读数。

Q3:和 MQ 的关系?

A:编排可同步派发;短信/邮件适配器内部仍可异步。先保证 Instance/Log 落库,再投递,避免「发了没记录」。

Q4:多租户下模板怎么隔离?

A:场景与模板随租户;sceneCode 可同名不同租户。跨租户 Job 要注意租户上下文。

Q5:和 IM 即时通讯重复吗?

A:IM 是会话聊天;消息中心是事件通知。可以在站内信点「去处理」跳转审批,但不在聊天窗刷业务模板。

Q6:场景默认渠道被请求覆盖,会不会被滥用?

A:channels 覆盖适合「这次只要站内信」的调用方;生产可加权限或白名单,禁止业务随意加短信渠道绕过运营策略。

Q7:SendLog 很多,表会不会爆?

A:按月归档 / 冷热分离;保留「失败 + 近 N 天成功」即可满足客服举证。Instance 头表更小,适合长期检索。

Q8:模板占位符和业务 params 对不上?

A:渲染阶段应失败并跳过该渠道(打 warn),避免把 {assignee} 原文发给用户;上线前用场景「试发」校验。


和 RuoyiOffice 的对应关系

能力 位置(概念)
发送 API MessageSendApi / send / cancelByBiz
编排实现 MessageSendService
站内信 + WS NotifyChannelAdapter(类型 notify
场景 / 实例 / 日志 CRUD system 模块 msg 包 + 前端 system/msg/*
定时派发 MsgScheduledSendJob
用户收件箱 站内信消息页

完整后端:GitCode / AtomGit

前端:GitCode · vben

相关阅读:接口传输加解密 @ApiEncrypt、IM WebSocket 聊天设计、登录短信频控。


总结

  1. sceneCode 是业务与通道之间的防腐层
  2. Instance + SendLog 让「一次业务事件、多渠道、可取消、可重试」可运营。
  3. 站内信落库 + WebSocket 尽力推 解决「红点不亮」与「可追溯」的矛盾。
  4. 新渠道只加 Adapter,不改遍地 sendSms

把通知从「散落的 SDK 调用」升级成「可配置的编排引擎」,审批催办、会议提醒、薪资到账才能同一套治理。


GitCode · AtomGit 点星,夏日活动攒积分。

在线体验:RuoyiOffice

商业版源码授权:联系页 · 企业微信 RuoyiOffice


关键词:统一消息中心、sceneCode、WebSocket 红点、站内信、多渠道发送、cancelByBiz、消息实例、Spring Boot 3

相关推荐
摇滚侠1 小时前
《SpringBoot 3:入门与应用实战》第 14 章 打包与部署 Spring Boot 应用打包 阅读笔记 41
spring boot·笔记·后端
大梦想家a1 小时前
新能源汽车共享充电桩收费管理系统的设计与实现(SpringBoot+MyBatis-Plus+Vue,前后台分离完整源码)
spring boot·汽车·mybatis
sugar__salt1 小时前
从跨域到 WebSocket:一篇讲透浏览器的通信边界
网络·websocket·网络协议
摇滚侠3 小时前
《SpringBoot 3:入门与应用实战》第 14 章 打包与部署 制作 Docker 镜像 阅读笔记 43
spring boot·笔记·docker
ocean21034 小时前
2025-2026年计算机网络大厂面试高频问题示例
websocket·计算机网络·秋招·tcp·后端面试·大厂面经·面试真题
xiaoqiMikko4 小时前
七条 advisory 各给了一个修复版,而一次修完的那个数字一条都没写
java·spring boot
凯哥Java5 小时前
System.setProperty 的正确姿势:Spring Boot 启动类里的“缺省值“魔法
java·spring boot·后端
Darling噜啦啦5 小时前
WebSocket 双工通信实战:从协议握手到跨域原理,彻底搞懂实时通信的"另一条路"
websocket
Dreams_l7 小时前
RabbitMQ在SpringBoot中的应用
spring boot·rabbitmq·java-rabbitmq
计算机学姐7 小时前
基于SpringBoot的高校爱心慈善管理系统
java·vue.js·spring boot·后端·spring·tomcat·mybatis