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

相关推荐
云烟成雨TD1 小时前
Micrometer 系列【67】统一观测:基于 Spring Boot 的生产级演示案例 | 跨线程场景
spring boot·云原生·链路追踪
IT机器猫1 小时前
RabbitMQ基础二
java·spring boot·分布式·spring cloud·log4j·rabbitmq·springamqp
Flynt3 小时前
我试了Spring Boot 4的原生镜像,聊聊那些数字和坑
java·spring boot
凤山老林3 小时前
复杂检索引擎落地:Spring Boot + Elasticsearch 数据同步与高阶查询实战
spring boot·后端·elasticsearch
砍材农夫13 小时前
spring-ai|Spring‑AI 2.0.0 新特性 + 入门教程
spring boot·spring·spring cloud
小强库计算机毕业设计15 小时前
SpringBoot+Vue3 学生宿舍管理系统实战
java·spring boot·后端·vue·学生宿舍管理系统
Lucis__1 天前
如何实现长连接通信机制?Websocket的最佳实践
网络·websocket·网络协议
不是光头 强1 天前
SpringBoot事务优化与状态机实战
java·spring boot·后端
AI领路人.2 天前
[特殊字符] 用 OpenClaw 实现一个 C++ 复刻版 QuantClaw
websocket·单元测试·discord·rest api·内存占用·c++17