站内信
这篇解决什么
业务要给管理员或会员塞一条站内通知:改文案、测发送、在顶栏铃铛里看见未读。读完能新增模板、用 NotifyMessageSendApi 发信,并分清「只落库」和「落库后再 WebSocket 推一把」。
默认端口 48080,管理端前缀 /admin-api。PC 开发页 http://127.0.0.1:5800。站内信在 yudao-module-system,不经过短信网关或 SMTP。
示意图:业务先写模板、再插消息表;统一消息中心这条路径才会再推 type=notify。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 模板接口 | @RequestMapping("/system/notify-template") | ruoyi-office/yudao-module-system/.../controller/admin/notify/NotifyTemplateController.java |
| 消息接口 | @RequestMapping("/system/notify-message") | 同目录 NotifyMessageController.java |
| 跨模块发送 | Feign:/rpc-api/system/notify/send | yudao-module-system-api/.../api/notify/NotifyMessageSendApi.java |
| 发送实现 | 校验模板、格式化、落库 | .../service/notify/NotifySendServiceImpl.java |
| 模板页 | 菜单「模板管理」,路由 /system/messages/notify/notify-template | ruoyi-office-vben/apps/web-antd/src/views/system/notify/template/ |
| 记录页 | 菜单「消息记录」,路由 /system/messages/notify/notify-message | 同目录 message/ |
| 我的站内信 | 隐藏路由 /system/notify-message,顶栏「查看所有」 | 同目录 my/,路由名 MyNotifyMessage |
官方菜单:父级 2739 消息中心,2144 站内信管理,2145 模板管理,2151 消息记录。我的站内信不进菜单树,写在 web-antd/src/router/routes/modules/system.ts。
三个页面
管理后台能看见三块:模板、全站发送记录、当前登录人的收件箱。后两块读的是同一张 system_notify_message,过滤条件不同。

本系统截图:系统管理 → 消息中心 → 站内信管理 → 模板管理。列是编码、发送人、内容、类型、状态。

本系统截图:同级菜单「消息记录」。按用户编号、用户类型、模板编码查全站已发。

本系统截图:顶栏铃铛 → 查看所有,路由 /system/notify-message。只能看自己的信,可单条或全部标已读。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 模板 CRUD / 测试发送 | 权限 system:notify-template:create 等,测试要 send-notify | NotifyTemplateController |
| 记录分页 | GET /system/notify-message/page | NotifyMessageController#getNotifyMessagePage |
| 我的分页 | GET /my-page,强制当前 userId + ADMIN | 同上 #getMyMyNotifyMessagePage |
| 未读列表 / 数量 | GET /get-unread-list、/get-unread-count | 顶栏铃铛调用 |
| 标已读 | PUT /update-read、/update-all-read | 只能改自己的未读行 |
get-unread-count 标了 @ApiAccessLog(enable = false)。顶栏两分钟轮询一次,记访问日志没有意义。
为什么要模板
产品改发送人昵称或正文时,改 system_notify_template 一行即可。业务代码只认 code,不必重新发版。
表与字段
模板一张表、消息一张表。消息把发送当时的编码、类型、发送人、渲染后正文和参数都冗余下来,以后改模板不影响历史信。
示意图:模板按 code 被发送接口引用;消息行是一次投递的快照。
NotifyTemplateDO 上 @TableName("system_notify_template"),并加了 @TenantIgnore。NotifyMessageDO 上 @TableName("system_notify_message"),走租户插件,表里有 tenant_id。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
code | 业务调用时的唯一键,租户间也共用 | .../dal/dataobject/notify/NotifyTemplateDO.java |
nickname | 收件箱里显示的发件人,不是系统用户 | 同上 |
content | 占位符写成 {name},保存时抽出 params | 同上 |
params | JacksonTypeHandler 存 List<String> | 同上 |
type | 字典 system_notify_template_type:1 通知公告,2 系统消息 | NotifyTemplateTypeEnum |
status | CommonStatusEnum。停用后发送只打日志、返回 null | NotifyTemplateDO |
userType | 1 会员,2 管理员 | NotifyMessageDO + UserTypeEnum |
templateContent | 已经 StrUtil.format 过的正文 | NotifyMessageDO |
templateParams | 当时传入的 Map | 同上 |
readStatus / readTime | 标已读时一起写 | NotifyMessageMapper#updateListRead |
模板没有租户列
@TenantIgnore 表示所有租户读同一套 code。两个租户不要抢同一个编码;消息行仍按当前租户隔离。
缓存名 notify_template,按 code 做 @Cacheable。改、删模板会 @CacheEvict(allEntries = true),因为主键不是缓存键。
模板怎么配
打开模板管理,点新增或编辑。code 给 API 用,nickname 给收件人看,content 里用花括号包参数名。

本系统截图:培训证书模板。编码 hrm-training-cert-issued,正文含 {certName}、{certNo}。
保存时 parseTemplateContentParams 用正则 \{(.*?)\} 抽出参数名,写入 params。发送时 formatNotifyTemplateContent 走 Hutool StrUtil.format,{certName} 换成传入 Map 里的值。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 抽出参数 | ReUtil.findAllGroup1 | NotifyTemplateServiceImpl#parseTemplateContentParams |
| 渲染正文 | StrUtil.format(content, params) | 同上 #formatNotifyTemplateContent |
| 编码重复 | NOTIFY_TEMPLATE_CODE_DUPLICATE | ErrorCodeConstants |
| 字典 | system_notify_template_type | 静态数据 id 167 |
前端表单在 template/data.ts。类型下拉读 DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE。状态默认启用。
测试发送
模板行上的「测试」打开发送弹窗。选用户类型、接收人,按 params 动态补输入框,再 POST /system/notify-template/send-notify。

本系统截图:用户类型默认会员;选管理员后接收人变成用户下拉。参数名来自模板 params。
发完到「消息记录」核对;接收人打开顶栏铃铛,或进「我的站内信」。

本系统截图:工作台右上角铃铛。上半是聊天会话,下半「通知」是站内信,标题用 templateNickname,正文用 templateContent。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 测试 VO | userId、userType、templateCode、templateParams | .../vo/template/NotifyTemplateSendReqVO.java |
| 测试页 | 按 params 拼 param_{name} | web-antd/.../template/modules/send-form.vue |
| 顶栏拉取 | getUnreadNotifyMessageList + 两分钟轮询未读数 | web-antd/src/layouts/basic.vue |
| 跳转详情 | templateCode 以 bpm_ 开头,或参数里有 detailUrl | web-antd/src/views/oa/im/helper.ts |
模板不存在时报的是公告错误码
validateNotifyTemplate 缓存未命中时抛 NOTICE_NOT_FOUND(「当前通知公告不存在」),不是 NOTIFY_TEMPLATE_NOT_EXISTS。对日志和前端提示时按这句文案排,不要去找公告表。
模板停用:sendSingleNotify 打 info 日志后返回 null,不抛异常。调用方不要把 null 当成已写入的消息 id。
缺参数:params 里某个键在 Map 中为 null,抛 NOTIFY_SEND_TEMPLATE_PARAM_MISS。
sendBatchNotify 默认抛 UnsupportedOperationException。要群发就自己循环 sendSingleNotifyToAdmin。
业务怎么发
其它模块不要直接插 system_notify_message。注入 NotifyMessageSendApi,按用户类型选方法。
@Resource
private NotifyMessageSendApi notifyMessageSendApi;
public void send(Long userId, String templateCode, Map<String, Object> params) {
NotifySendSingleToUserReqDTO req = new NotifySendSingleToUserReqDTO();
req.setUserId(userId);
req.setTemplateCode(templateCode);
req.setTemplateParams(params);
notifyMessageSendApi.sendSingleMessageToAdmin(req);
}培训模块就是这样发的:TrainingNotifyHelper。合同到期、项目超期、工单 SLA 也走同一接口。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 发给管理员 | POST .../send-single-admin | NotifyMessageSendApi#sendSingleMessageToAdmin |
| 发给会员 | POST .../send-single-member | #sendSingleMessageToMember |
| 入参 | userId、templateCode 必填 | .../api/notify/dto/NotifySendSingleToUserReqDTO.java |
| 落地 | 转 NotifySendService | .../api/notify/NotifyMessageSendApiImpl.java |
| 微服务声明 | @EnableFeignClients 列出该 API | yudao-module-oa-server、yudao-module-project-server 的 RpcConfiguration |
单体启动时实现类是 @RestController,本地就能调。微服务要把 system-server 当 Feign 名,路径前缀 /rpc-api/system。
BPM 审批信的 templateCode 建议以 bpm_ 开头,并在 templateParams 里带 processInstanceId 或 detailUrl。顶栏和「我的站内信」会用 parseNotifyNavigationMeta 拼跳转。
实时推送
NotifyMessageSendApi 只落库。顶栏未读数靠轮询,打开铃铛再拉最近 10 条。要立刻弹到在线用户,走统一消息中心的站内信渠道。
示意图:渠道适配器先保证消息 id 写入,再尽力推 WebSocket;推失败只打 warn。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 渠道 | MsgChannelEnum.NOTIFY = 1 | .../service/msg/channel/NotifyChannelAdapter.java |
| 帧类型 | 常量 WS_MESSAGE_TYPE = "notify" | 同上 |
| 载荷 | id、title、content、detailUrl | 同上 |
| 发送器 | 跨模块 WebSocketSenderApi | yudao-module-infra-api |
| 原生模板 | 消息中心复用 system_notify_template | .../service/msg/core/NativeTemplateResolver.java |
顶栏那条 /infra/ws 目前只解析 im.message.receive / im.conversation.update,用来刷聊天角标。type=notify 的帧要自己在页面里听;站内信角标仍以 get-unread-count 为准。
直接调发送 API 不会推 WebSocket
NotifySendServiceImpl 没有 WebSocketSenderApi。培训、合同 Job 发出去的信,接收人可能要等下一次轮询或手动打开铃铛才看见。需要秒达就走消息中心渠道,或自己在业务里补一次 sendObject。
WebSocket 路径、集群 sender-type、握手鉴权见 WebSocket。
配置与操作
管理员在 消息中心 → 站内信 → 模板管理 写编码和内容,保存后业务按编码推送。员工看顶栏铃铛或「我的站内信」页(该页在源码里,dump 未必有菜单)。
改模板只影响之后发送的行。已发出的 system_notify_message 不会回写。开启见 系统功能设计。截图见上文模板和消息列表。
