站内信与消息通知
RuoYi Office 的消息能力分为两层:
| 层 | 说明 | 典型入口 |
|---|---|---|
| 渠道底座 | 各通道的模板、账号、发送记录 | 站内信模板、NotifySendService |
| 统一消息中心(编排层) | 按「消息定义(场景)」一次调用,自动多渠道派发、定时调度、统一监控 | MessageSendApi |
新业务开发推荐走 MessageSendApi:业务代码只需传「场景编号 + 接收人 + 参数 + 业务单据 ID」,渠道、模板、跳转、定时由消息中心配置决定。站内信、短信、邮件仍分别在各自管理页维护模板,消息中心通过 template_code 引用,不重复维护两套文案。
管理端菜单
路径:系统管理 > 消息中心
消息中心
├─ 消息定义 ← 注册场景、绑定各渠道模板编码
├─ 跳转路由 ← 业务类型 → PC/App 详情页
├─ 消息实例 ← 每次发送请求(含定时待发送)
├─ 发送日志 ← 跨渠道统一监控
├─ 站内信管理 ← 模板 / 消息记录 / 我的站内信
├─ 短信管理
├─ 邮件管理
└─ 通知公告本地实现位置
| 层 | 位置 |
|---|---|
| 站内信模板页面 | ruoyi-office-vben/apps/web-antd/src/views/system/notify/template |
| 站内信消息页面 | views/system/notify/message |
| 我的站内信 | views/system/notify/my |
| 消息定义 / 路由 / 实例 / 日志 | views/system/msg/{scene,route,instance,log} |
| 渠道发送服务 | NotifySendService、SmsSendService、MailSendService |
| 业务对接 API | yudao-module-system-api/.../api/msg/MessageSendApi |
能力链路
推荐:业务模块 → 消息中心 → 各渠道
历史:业务模块直连站内信
管理端:站内信模板与消息
站内信模板
模板页面支持新增、编辑、删除、批量删除、导出和测试发送。
| 操作 | 接口 | 权限标识 |
|---|---|---|
| 创建模板 | POST /system/notify-template/create | system:notify-template:create |
| 更新模板 | PUT /system/notify-template/update | system:notify-template:update |
| 删除模板 | DELETE /system/notify-template/delete | system:notify-template:delete |
| 模板分页 | GET /system/notify-template/page | 页面查询使用 |
| 测试发送 | POST /system/notify-template/send-notify | system:notify-template:send-notify |
占位符统一为 {paramName}。若模板需要点击跳转业务详情,正文中可引用 {detailUrl}(由消息中心在派发时自动注入,见下文)。
站内信消息 / 我的站内信
| 操作 | 接口 | 说明 |
|---|---|---|
| 全量消息分页 | GET /system/notify-message/page | 管理员排查 |
| 我的分页 | GET /system/notify-message/my-page | 当前用户列表 |
| 标记已读 | PUT /system/notify-message/update-read | 单条或多条 |
| 全部已读 | PUT /system/notify-message/update-all-read | 清空未读 |
PC 端顶部铃铛轮询未读;用户可在「偏好设置 > Antd 拓展」开启「新消息闪烁提醒」。
业务系统对接指南
本节说明 OA、合同、项目等业务模块如何接入统一消息中心。
两种调用方式对比
| 维度 | 历史方式(仅站内信) | 推荐方式(消息中心) |
|---|---|---|
| 入口 | NotifyMessageSendApi | MessageSendApi |
| 业务传入 | templateCode + 用户 ID + 参数 | sceneCode + 接收人 + 参数 + bizType/bizId |
| 多渠道 | 需分别调短信/邮件 API | 场景配置一次,自动按渠道派发 |
| 定时发送 | 业务自建 Job / 延时队列 | planSendTime,由 MsgScheduledSendJob 统一扫描 |
| 跳转 | 业务自行拼 detailUrl | 配置「跳转路由」后自动注入 {detailUrl} |
| 监控 | 各渠道日志分散 | 「消息实例 / 发送日志」统一查看 |
存量仅发站内信且逻辑稳定的代码可暂保留
NotifyMessageSendApi;新功能默认用MessageSendApi。
业务侧只需关心什么
一次性在管理端(或初始化 SQL)完成配置后,业务代码每次触发只需 4 项:
| 传入项 | 说明 | 示例 |
|---|---|---|
sceneCode | 消息定义编号 | OA_MEETING_REMIND |
receiverUserIds | 接收人用户 ID 列表 | [1, 2, 3] |
params | 模板占位符 Map | { meetingTitle, startTime, ... } |
bizType + bizId | 业务类型 + 单据 ID(跳转、取消、幂等) | oa_meeting_room_booking + "123" |
渠道列表、各渠道模板、PC/App 跳转路由均由消息定义 + 跳转路由配置,业务代码不必硬编码模板正文或 URL。
接入前准备(四步)
以「会议开始提醒」为例:
| 步骤 | 做什么 | 在哪里配 | 示例 |
|---|---|---|---|
| ① 渠道模板 | 文案与占位符 | 站内信管理 > 模板(短信/邮件同理) | code=OA_MEETING_REMIND,含 {startTime}、{meetingTitle} |
| ② 消息定义 | 场景编号、默认渠道、接收人模式 | 消息中心 > 消息定义 | code=OA_MEETING_REMIND,渠道=站内信,接收人=仅上游指定 |
| ③ 场景×渠道 | 绑定原生模板编码 | 消息定义 > 渠道模板配置 | 站内信 → template_code=OA_MEETING_REMIND |
| ④ 跳转路由 | 业务类型对应详情页 | 消息中心 > 跳转路由 | biz_type=oa_meeting_room_booking,pc_route=/oa/meetingroom/booking-info?id={bizId} |
Maven 依赖与 RPC
1. server 模块引入 system-api(多数模块已有):
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-system-api</artifactId>
<version>${revision}</version>
</dependency>2. 微服务模式:在业务模块 RpcConfiguration 注册 Feign 客户端:
@EnableFeignClients(clients = {
// ... 其他 Api
MessageSendApi.class
})单体 boot 模式无需额外配置,注入方式相同。
3. Service 注入:
@Resource
private MessageSendApi messageSendApi;API 说明
接口类:cn.iocoder.yudao.module.system.api.msg.MessageSendApi
| 方法 | 说明 | 返回值 |
|---|---|---|
send(reqDTO) | 发送(立即或定时) | 消息实例 ID |
cancelByBiz(bizType, bizId) | 取消该单据所有待发送实例(幂等) | true |
MessageSendReqDTO 主要字段:
| 字段 | 必填 | 说明 |
|---|---|---|
sceneCode | 是 | 消息场景编号 |
bizType | 建议 | 业务类型;用于跳转与取消 |
bizId | 建议 | 业务单据 ID(字符串) |
receiverUserIds | 视场景 | 管理员用户 ID 列表 |
receivers | 视场景 | 完整接收人(含会员、外部手机/邮箱) |
params | 建议 | 模板占位符;消息中心会自动注入 detailUrl |
channels | 否 | 覆盖场景默认渠道 |
planSendTime | 否 | 计划发送时间;空 = 立即发送 |
典型接入模式
立即发送(审批通过、状态变更)
MessageSendReqDTO req = new MessageSendReqDTO();
req.setSceneCode("OA_SEAL_APPROVED");
req.setBizType("oa_seal_apply");
req.setBizId(String.valueOf(apply.getId()));
req.setReceiverUserIds(List.of(apply.getApproverId()));
req.setParams(Map.of(
"billCode", apply.getBillCode(),
"applicantName", apply.getApplicantName()
));
messageSendApi.send(req).getCheckedData();在 Service 末尾、BPM 监听 onProcessApproved 等时机调用。
定时发送(会议提醒、到期预警)
req.setPlanSendTime(meetingStartTime.minusMinutes(10));
messageSendApi.send(req).getCheckedData();到点后由 XXL-Job MsgScheduledSendJob(需已启用)扫描派发。业务 Job 只负责扫业务数据并调用 send,不必再写独立「到点发消息」逻辑。
取消与重建(改期、撤回、驳回)
messageSendApi.cancelByBiz("oa_meeting_room_booking", String.valueOf(bookingId));
messageSendApi.send(newReq).getCheckedData(); // 需要时重新注册改期场景建议先 cancel 再 send,避免重复定时实例。
定时 Job 批量发送
@XxlJob("contractExpireNotifyJob")
@TenantJob
public String execute() {
for (ContractLedgerDO contract : expiringList) {
MessageSendReqDTO req = new MessageSendReqDTO();
req.setSceneCode("CONTRACT_EXPIRE_REMIND");
req.setBizType("contract_ledger");
req.setBizId(String.valueOf(contract.getId()));
req.setReceiverUserIds(List.of(contract.getOwnerUserId()));
req.setParams(buildParams(contract));
messageSendApi.send(req).getCheckedData();
}
return "ok";
}参考实现:OA 会议提醒
文件:yudao-module-oa-server/.../MeetingRoomBookingServiceImpl.java
private static final String MEETING_REMIND_SCENE_CODE = "OA_MEETING_REMIND";
private static final String MEETING_REMIND_BIZ_TYPE = "oa_meeting_room_booking";
private void registerMeetingReminder(MeetingRoomBookingDO booking) {
try {
// ... 计算 planSendTime、接收人、params
messageSendApi.cancelByBiz(MEETING_REMIND_BIZ_TYPE, String.valueOf(booking.getId()));
MessageSendReqDTO req = new MessageSendReqDTO();
req.setSceneCode(MEETING_REMIND_SCENE_CODE);
req.setBizType(MEETING_REMIND_BIZ_TYPE);
req.setBizId(String.valueOf(booking.getId()));
req.setReceiverUserIds(receiverUserIds);
req.setParams(params);
req.setPlanSendTime(planSendTime);
messageSendApi.send(req).getCheckedData();
} catch (Exception e) {
log.error("[registerMeetingReminder] 失败 id={}", booking.getId(), e);
}
}
private void cancelMeetingReminder(Long id) {
messageSendApi.cancelByBiz(MEETING_REMIND_BIZ_TYPE, String.valueOf(id));
}审批通过注册提醒;撤回/驳回/取消时调用 cancelMeetingReminder。
占位符与跳转
| 项 | 约定 |
|---|---|
| 占位符 | {paramName},与站内信/短信/邮件模板一致 |
业务 params | key 须与模板声明的参数名一致 |
{detailUrl} | 无需业务传入;配置跳转路由后由消息中心按渠道注入 |
| 站内信点击 | 前端读取 templateParams.detailUrl 做站内路由跳转 |
站内信模板示例:
您有一场会议将于 {startTime} 开始。
会议主题:{meetingTitle}
点击查看:{detailUrl}异常处理与排查
| 实践 | 说明 |
|---|---|
| 不阻断主流程 | send / cancelByBiz 建议 try-catch,失败只记日志 |
| 统一监控 | 管理端「消息实例」「发送日志」 |
| 渠道明细 | 「站内信消息 / 短信日志 / 邮件记录」可对照 ref_biz_id |
| 定时未发 | 查实例是否「待发送」、plan_send_time 是否已到、Job 是否启用、业务侧是否因「会议已开始」等条件跳过注册 |
接入自检清单
- [ ]
yudao-module-system-api依赖已引入 - [ ]
RpcConfiguration已注册MessageSendApi(cloud 模式) - [ ] 站内信(及需要的短信/邮件)模板已建,
template_code与消息定义一致 - [ ] 消息定义已启用,渠道与接收人模式正确
- [ ] 跳转路由
biz_type与代码中bizType一致 - [ ] 立即发送:发送日志有记录,用户收到消息
- [ ] 定时发送:到点或手动触发 Job 后实例变为成功
- [ ] 取消:
cancelByBiz后待发送实例变为已取消 - [ ] 发送失败不影响主业务事务
历史方式:直连站内信(存量代码)
仅需站内信、且尚未迁移的场景,可继续调用 NotifyMessageSendApi:
NotifySendSingleToUserReqDTO req = new NotifySendSingleToUserReqDTO();
req.setUserId(userId);
req.setTemplateCode("contract-expire-notify");
req.setTemplateParams(Map.of("contractName", name));
notifyMessageSendApi.sendSingleMessageToAdmin(req);迁移到消息中心时:保留原模板 code → 新建消息定义并绑定该 template_code → 业务改调 MessageSendApi → 在发送日志验收。
二开建议
- 模板 code / 场景 code 作为稳定契约:上线后不要随意修改。
- 参数 key 语义清晰:如
meetingTitle、billCode,便于日志回溯。 - 跳转走跳转路由:不要在前端或业务里硬编码详情 URL。
- 站内信不替代短信/邮件:强触达场景在消息定义里配置多渠道。
- 新业务默认
MessageSendApi:多渠道、定时、统一监控一次到位。
排查清单
| 现象 | 排查方向 |
|---|---|
| 用户没收到站内信 | 模板 code、场景×渠道 template_code、用户 ID、发送日志状态 |
| 内容参数没替换 | 模板变量名与 params key 是否一致 |
| 点击无法进详情 | 跳转路由是否配置;模板是否含 {detailUrl};bizType/bizId 是否正确 |
| 定时消息未发送 | 消息实例状态、plan_send_time、XXL-Job MsgScheduledSendJob、业务注册条件 |
| 改期后收到 duplicate | 改期前是否调用 cancelByBiz |
| 消息已发送但仍未读 | 用户是否打开「我的站内信」或调用全部已读 |
