企业 OA 即时通讯 IM:从审批协同、消息触达到业务上下文闭环的架构设计
企业 OA 即时通讯 IM:从审批协同、消息触达到业务上下文闭环的架构设计
🌐 文档地址:http://ruoyioffice.com | 📦 源码1:https://gitcode.com/zhouzhongyan/ruoyi-office-vben.git | 📦 源码2:https://gitcode.com/zhouzhongyan/ruoyi-office.git | 📦 源码3:https://github.com/yuqing2026/ruoyi-office.git
很多企业上线 OA 后,表单、流程、通知都做了,却还是逃不开一个现实:审批卡住了,最后靠外部群里 @ 人;材料缺了,审批人在外部聊天里追问;事情办完了,真正的沟通过程却沉在群消息里,回到 OA 只能看到一个"通过"或"驳回"。这说明 OA 的核心不是"提交表单",而是让跨部门协作更快闭环。内置 IM 的价值,也不只是聊天,而是给 OA 增加一层实时通讯与业务上下文连接能力。
▲ OA 内置 IM 协同闭环:业务单据、审批待办、系统通知、工作台入口和即时通讯共用统一用户、租户、权限与上下文,形成从"发起"到"沟通"再到"处理"的完整链路
引言:OA 真正要解决的不是表单,而是协作闭环
很多人理解 OA,会先想到这些功能:
- 请假、出差、报销、用印、公文等表单。
- 发起流程、审批节点、抄送节点、流程归档。
- 通知公告、待办提醒、站内信、工作台。
- 权限、组织架构、角色、数据范围。
这些都没错,但它们只是 OA 的"静态骨架"。企业真实运转时,更多问题发生在流程节点之间:
| 场景 | 表单系统能解决什么 | 没有实时协同会发生什么 |
|---|---|---|
| 审批人看不懂申请原因 | 展示表单字段和附件 | 只能驳回或外部私聊追问 |
| 发起人缺材料 | 提交流程、等待审批 | 审批人催材料,材料发在外部群里 |
| 多部门并行审批 | BPM 能分派任务 | 部门之间沟通结论不回流系统 |
| 领导临时变更意见 | 流程能记录审批动作 | 口头/群聊意见难以追溯 |
| 员工只看工作台 | 有待办、通知、公告 | 不知道谁正在处理、该找谁沟通 |
所以,企业 OA 的关键命题不是"能不能把表单提交上去",而是:
一件事从发起、沟通、补充、审批、通知到归档,能不能在同一个系统里被触达、被理解、被追溯。
这就是内置 IM 的位置。它不是 OA 旁边的一个聊天小工具,而是 OA 协同链路里的实时通讯层。
一、业务场景:为什么外部 IM 很难承接 OA 协作?
1.1 外部 IM 看似方便,实际容易形成"协作断点"
企业常见做法是:OA 里发流程,外部聊天群里催审批。
短期看很方便,因为员工已经在用外部 IM;长期看,问题会越来越明显:
| 问题 | 外部 IM 的表现 | 对 OA 的影响 |
|---|---|---|
| 链接断裂 | 群里发一个流程链接,过几天就被消息淹没 | 业务单据与沟通记录脱节 |
| 权限不一致 | 群成员不等于 OA 权限成员 | 有权限的人收不到,没权限的人可能看到 |
| 上下文丢失 | 只看到一句"帮我看下这个单" | 审批人需要重新打开系统理解背景 |
| 追溯困难 | 沟通在外部群、审批在 OA | 审计时只能看到审批动作,看不到协商过程 |
| 租户边界弱 | 外部群不天然理解租户、部门、数据权限 | SaaS 多租户场景更难治理 |
尤其是流程审批类业务,“为什么通过”“为什么驳回”“谁补充了材料”"谁确认了口径"往往和审批结果一样重要。如果沟通全部留在外部 IM,OA 里只剩结果,管理链路就是不完整的。
1.2 内置 IM 的真正价值:让沟通附着在业务上下文上
内置 IM 不是为了替代所有聊天软件,而是解决 OA 内部协作的几个关键问题:
| 能力 | 说明 |
|---|---|
| 统一身份 | 使用系统用户、部门、岗位、角色,不再维护第二套通讯录 |
| 统一租户 | 会话、成员、消息都带 tenant_id,天然隔离不同企业数据 |
| 统一权限 | 是否能发起沟通、是否能看到入口、是否能打开单据,都走同一套权限体系 |
| 统一上下文 | 从审批详情、通知消息、工作台直接进入会话,携带单据 ID 和流程名称 |
| 统一触达 | WebSocket、未读数、顶部徽章、通知公告、待办入口形成触达闭环 |
这也是本文的核心观点:
OA 的核心不是"提交表单",而是让跨部门协作更快闭环;IM 的核心也不是"聊天",而是把实时沟通嵌入业务流。


▲ RuoyiOffice实现单据有IM联动:形成从"发起"到"沟通"再到"处理"的完整链路
二、系统设计:OA 内置 IM 应该放在哪一层?
2.1 不是 OA 模块私有能力,而是 infra 基础设施
RuoYi Office 的 IM 前端入口放在 OA → 即时通讯,但后端实现并没有塞进 oa 模块,而是放在 infra 基础设施模块下:
| 层级 | 位置 | 职责 |
|---|---|---|
| 前端页面 | apps/web-antd/src/views/oa/im/index.vue |
IM 聊天界面、会话列表、消息区、上传入口 |
| 前端 API | apps/web-antd/src/api/infra/im/index.ts |
会话分页、创建单聊/群聊、消息分页、已读、未读数 |
| 后端服务 | yudao-module-infra-server/service/im |
会话、成员、消息、未读数、WebSocket 推送 |
| WebSocket | /infra/ws |
双向消息通道,承载发送、接收、会话更新 |
| 数据表 | infra_im_conversation 等三表 |
会话主数据、成员配置、消息明细 |
为什么这样设计?
因为 IM 不是只服务一个 OA 页面。它未来可以连接审批、通知、工单、项目、CRM 客户跟进、ERP 采购协同等模块。放在 infra 层,才能成为企业协同平台的公共实时通讯能力。
2.2 架构全景:会话、消息、在线状态、未读数
RuoYi Office 的 IM MVP 已经具备企业内部聊天的核心骨架:
| 功能 | 实现方式 |
|---|---|
| 单聊 | create-single 创建,使用 single_key=minUserId:maxUserId 防重复 |
| 群聊 | create-group 创建,成员写入会话成员表 |
| 消息发送 | 前端通过 WebSocket 发送 im.message.send |
| 消息接收 | 后端推送 im.message.receive |
| 会话更新 | 后端推送 im.conversation.update |
| 未读数 | 成员表维护 unread_count,布局顶部轮询读取 |
| 置顶/免打扰 | 成员维度维护 top_flag、mute_flag |
| 在线状态 | 通过 WebSocket Session 管理器判断目标用户是否在线 |
| 图片/文件 | 前端复用统一文件上传接口,消息内容存 URL 或文件 JSON |
这套设计里,有一个非常重要的边界:会话是多人共享的,未读数和置顶免打扰是个人私有的。
所以系统拆成三张表,而不是一张大表:
infra_im_conversation:描述会话本身。infra_im_conversation_member:描述某个用户在某个会话里的个人状态。infra_im_message:描述会话里的消息流水。
三、流程设计:从审批单据到 IM 会话的上下文传递
3.1 审批协同为什么需要"一键沟通"
审批系统最常见的低效,不是流程引擎跑不动,而是审批人理解成本太高。
例如一张出差报销单:
- 发起人提交报销。
- 部门经理发现发票附件不清晰。
- 财务想确认是否属于项目费用。
- 发起人补充解释。
- 财务确认后审批通过。
如果系统只有"同意/驳回",那审批人遇到问题只有两种选择:
- 直接驳回,让发起人重新提交。
- 去外部 IM 私聊,再回到 OA 审批。
这两种都不理想。更好的方式是:审批详情页识别当前流程上下文,一键发起与发起人或待办人的单聊,并把 processInstanceId、processInstanceName 带到 IM 页面。
3.2 URL/query 设计:业务上下文如何进入 IM?
RuoYi Office 前端已经有一个很清晰的上下文跳转模型。审批详情页可以创建单聊,然后跳转到 IM 页面:
export async function startConversationFromProcessInstance(options: {
processInstanceId: number | string;
processInstanceName?: string;
}) {
const currentUserId = Number(useUserStore().userInfo?.id || 0) || undefined;
const detail = await getApprovalDetail({
processInstanceId: String(options.processInstanceId),
});
const targetUserId = resolveConversationTargetUserId(detail, currentUserId);
if (!targetUserId) {
message.warning('暂未识别出可沟通对象');
return;
}
const conversation = await createSingleConversation({ targetUserId });
await router.push({
name: 'OaImIndex',
query: {
conversationId: conversation.id,
processInstanceId: String(options.processInstanceId),
processInstanceName:
options.processInstanceName || detail.processInstance?.name,
},
});
}
这段代码的价值不在于"跳转页面",而在于它定义了 OA 单据和 IM 会话之间的连接协议:
| query | 含义 | 价值 |
|---|---|---|
conversationId |
进入哪个会话 | 打开 IM 后直接定位沟通对象 |
processInstanceId |
对应哪条流程实例 | 后续可反向打开审批详情 |
processInstanceName |
流程实例名称 | 在聊天区展示当前讨论的业务主题 |
这就是业务上下文闭环的第一步:沟通不是从空白聊天框开始,而是从一个明确的业务对象开始。
3.3 通知、待办、工作台与 IM 的触达闭环
OA 内置 IM 不应该孤立在菜单里,而应该和已有触达体系联动:
| 入口 | 触达方式 | 与 IM 的关系 |
|---|---|---|
| 审批待办 | 流程详情页 | 发现问题时一键沟通 |
| 系统通知 | 站内信、通知下拉 | 通知可携带业务详情 URL |
| 工作台 | 待办、公告、统计卡片 | 员工登录后第一时间看到待处理事项 |
| 顶部 IM 图标 | 未读数徽章 | 实时提醒有人发来消息 |
| IM 会话页 | 单聊、群聊、附件 | 承接业务沟通过程 |
这套设计让 OA 不再是"我要主动去看有没有事",而是变成:
业务发生 → 系统通知 → 工作台/顶部徽章触达 → 进入详情 → 发起沟通 → 回到业务处理```
四、功能实现:IM 页面为什么要兼顾聊天体验和企业属性?
4.1 IM 即时通讯页面

▲ IM 即时通讯页面:左侧是会话列表和未读数,右侧是消息区、输入区、图片/文件上传入口,并支持单聊、群聊、置顶、免打扰、在线状态等企业内部协作能力
一个企业 IM 页面,不只是把消息发出去,还要处理很多"协作细节":
| 页面区域 | 功能 | 设计要点 |
|---|---|---|
| 会话列表 | 搜索、排序、未读数 | 置顶优先,其次按最后消息时间排序 |
| 会话标题 | 单聊昵称、群聊名称、在线状态 | 单聊显示对方在线状态,群聊显示成员数 |
| 消息区域 | 文本、图片、文件 | 根据 messageType 渲染不同内容 |
| 输入区域 | 文本输入、发送、上传 | 文件先上传,再发送文件类型消息 |
| 成员配置 | 置顶、免打扰 | 用户级配置,不影响其他成员 |
4.2 前端 WebSocket 消息接收
IM 的实时性来自 WebSocket。前端连接 /infra/ws 后,监听服务端推送的消息类型:
const socketServer = buildWebSocketUrl('/infra/ws', refreshToken);
const { status, data, send, open } = useWebSocket(socketServer, {
autoReconnect: true,
heartbeat: true,
});
watchEffect(() => {
if (!data.value || data.value === 'pong') {
return;
}
const jsonMessage = JSON.parse(data.value);
const content = parseMessageContent(jsonMessage.content);
if (jsonMessage.type === 'im.message.receive') {
handleIncomingMessage(content as InfraImApi.ImMessage);
return;
}
if (jsonMessage.type === 'im.conversation.update') {
upsertConversation(content as InfraImApi.ImConversation);
}
});
这里有两个消息类型非常关键:
| 类型 | 触发时机 | 前端动作 |
|---|---|---|
im.message.receive |
有新消息写入后 | 当前会话追加消息,必要时滚动到底部 |
im.conversation.update |
会话最后消息、未读数、置顶等变化 | 更新左侧会话列表并重新排序 |
为什么要拆成两类?
因为"消息明细"和"会话摘要"是两个不同视图。当前聊天窗口需要新消息,左侧会话列表需要最后消息、未读数和排序。拆开推送,前端状态会更清晰。
4.3 顶部未读数:让 IM 进入工作台触达体系
在布局层,RuoYi Office 会读取 IM 未读数,并显示在顶部图标上:
async function handleImUnreadCount() {
imUnreadCount.value = await getImUnreadCount();
}
onMounted(() => {
handleNotificationGetUnreadCount();
handleImUnreadCount();
setInterval(
() => {
if (userStore.userInfo) {
handleNotificationGetUnreadCount();
handleImUnreadCount();
}
},
1000 * 60 * 2,
);
});
这里体现了一个产品设计细节:IM 未读数不是藏在菜单里,而是和站内通知一起进入顶部触达区域。员工不需要点开"OA → 即时通讯"才知道有人找他。
▲ RuoyiOffice:工作台顶部消息通知提醒
五、后端核心实现:发送一条消息背后发生了什么?
5.1 WebSocket 输入消息模型
前端发送消息时,后端接收的是一个非常克制的消息对象:
@Data
@Accessors(chain = true)
public class ImMessageSendMessage {
private Long conversationId;
private String messageType;
private String content;
private String clientMsgId;
}
这四个字段分别解决四个问题:
| 字段 | 作用 |
|---|---|
conversationId |
消息发到哪个会话 |
messageType |
文本、图片、文件的渲染方式 |
content |
文本内容、图片 URL 或文件 JSON |
clientMsgId |
客户端幂等 ID,防止重发产生重复消息 |
5.2 发送消息:校验、落库、更新会话、增加未读
后端发送消息的主流程集中在 sendMessage():
@Override
@Transactional(rollbackFor = Exception.class)
public void sendMessage(Long loginUserId, Long tenantId, ImMessageSendMessage request) {
if (!ImMessageTypeEnum.isSupported(request.getMessageType())) {
throw exception(IM_MESSAGE_TYPE_INVALID);
}
if (StrUtil.isBlank(request.getContent())) {
throw exception(IM_MESSAGE_CONTENT_EMPTY);
}
ImConversationDO conversation = validateConversationMember(
loginUserId, tenantId, request.getConversationId());
ImMessageDO message = ImMessageDO.builder()
.conversationId(request.getConversationId())
.senderId(loginUserId)
.messageType(request.getMessageType())
.content(StrUtil.trim(request.getContent()))
.clientMsgId(StrUtil.blankToDefault(request.getClientMsgId(), null))
.sendTime(LocalDateTime.now())
.status(MESSAGE_STATUS_NORMAL)
.build();
message.setTenantId(tenantId);
messageMapper.insert(message);
}
这段代码先做两类校验:
- 消息类型必须是系统支持的
text、image、file。 - 当前用户必须是会话成员,否则不能往这个会话发消息。
也就是说,IM 的权限不是"知道会话 ID 就能发",而是会检查 tenant_id + conversation_id + user_id 的成员关系。
5.3 会话摘要更新:为什么消息表之外还要冗余 lastMessage?
消息写入后,系统会同步更新会话主表:
conversation.setLastMessageId(message.getId());
conversation.setLastMessagePreview(buildPreview(message.getMessageType(), message.getContent()));
conversation.setLastMessageType(message.getMessageType());
conversation.setLastSenderId(loginUserId);
conversation.setLastMessageTime(now);
conversationMapper.updateById(conversation);
List<ImConversationMemberDO> members =
conversationMemberMapper.selectListByConversationId(tenantId, conversation.getId());
for (ImConversationMemberDO member : members) {
if (!Objects.equals(member.getUserId(), loginUserId)) {
member.setUnreadCount(ObjectUtil.defaultIfNull(member.getUnreadCount(), 0) + 1);
}
conversationMemberMapper.updateById(member);
}
会话列表是 IM 高频页面,如果每次都从消息表聚合最后一条消息,查询成本会越来越高。因此把最后消息摘要冗余到 infra_im_conversation 是合理的:
| 冗余字段 | 用途 |
|---|---|
last_message_id |
定位最后一条消息 |
last_message_preview |
左侧列表展示摘要 |
last_message_type |
显示 [图片]、[文件] 等预览 |
last_sender_id |
展示最后发言人 |
last_message_time |
会话排序 |
而未读数放在成员表里,是因为同一条消息对发送人、接收人、群成员的状态不同。
5.4 后端会话更新推送:让列表和消息同时刷新
消息写入、会话摘要更新、未读数更新之后,后端会向所有成员推送两类事件:
for (ImConversationMemberDO member : members) {
pushMessage(member.getUserId(), buildMessageResp(member.getUserId(), message, sender));
pushConversationUpdate(member.getUserId(), tenantId, conversation.getId());
}
private void pushMessage(Long userId, ImMessageRespVO message) {
if (webSocketMessageSender == null) {
return;
}
webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId,
ImWebSocketMessageTypeConstants.MESSAGE_RECEIVE, message);
}
private void pushConversationUpdate(Long userId, Long tenantId, Long conversationId) {
ImConversationRespVO conversation = getConversation(userId, tenantId, conversationId);
webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId,
ImWebSocketMessageTypeConstants.CONVERSATION_UPDATE, conversation);
}
这也是 RuoYi Office IM 设计里很值得借鉴的一点:服务端不只推消息,还推更新后的会话视图。
这样前端不需要收到消息后再猜未读数、最后消息、会话标题、在线状态怎么变化,而是直接拿到后端构造好的 ImConversationRespVO。
六、数据结构:三张表撑起企业内部 IM
6.1 会话表 infra_im_conversation
infra_im_conversation 记录会话本体,关注"这是什么会话"以及"最后一条消息是什么"。
| 字段 | 含义 | 设计说明 |
|---|---|---|
id |
会话编号 | 主键 |
tenant_id |
租户编号 | 多租户隔离 |
type |
会话类型 | 1=单聊,2=群聊 |
single_key |
单聊唯一键 | minUserId:maxUserId,防止重复单聊 |
name |
群聊名称 | 单聊不需要,群聊需要 |
avatar |
群头像 | 群聊展示 |
status |
会话状态 | 预留禁用、归档等状态 |
last_message_id |
最后一条消息 ID | 会话列表快速展示 |
last_message_preview |
最后一条消息预览 | 左侧摘要 |
last_message_type |
最后一条消息类型 | 文本、图片、文件 |
last_sender_id |
最后发送人 | 群聊摘要可用 |
last_message_time |
最后消息时间 | 会话排序 |
关键索引:
UNIQUE KEY `uk_infra_im_conversation_single`
(`tenant_id`, `single_key`, `deleted`),
KEY `idx_infra_im_conversation_last_time`
(`tenant_id`, `last_message_time`)
single_key 的唯一约束很重要。否则 A 找 B 聊天创建一个会话,B 找 A 又创建一个会话,单聊历史就被拆散了。
6.2 成员表 infra_im_conversation_member
成员表记录"谁在这个会话里",以及这个用户自己的会话配置。
| 字段 | 含义 | 为什么放在成员表 |
|---|---|---|
conversation_id |
会话编号 | 关联会话 |
user_id |
用户编号 | 关联系统用户 |
top_flag |
是否置顶 | 每个用户的置顶不同 |
mute_flag |
是否免打扰 | 每个用户的免打扰不同 |
last_read_message_id |
最后已读消息 ID | 每个用户阅读进度不同 |
unread_count |
未读数量 | 每个用户未读数不同 |
关键索引:
UNIQUE KEY `uk_infra_im_member_user`
(`tenant_id`, `conversation_id`, `user_id`, `deleted`),
KEY `idx_infra_im_member_unread`
(`tenant_id`, `user_id`, `unread_count`)
这张表是企业 IM 的关键。很多系统会把未读数存在缓存里,但 RuoYi Office 选择把它落在成员表中,优点是简单、可恢复、易查询,也适合企业内部 IM 这种消息规模。
6.3 消息表 infra_im_message
消息表记录真正的聊天流水。
| 字段 | 含义 | 设计说明 |
|---|---|---|
conversation_id |
会话编号 | 消息归属 |
sender_id |
发送人 | 关联系统用户 |
message_type |
消息类型 | text、image、file |
content |
消息内容 | 文本、图片 URL、文件 JSON |
client_msg_id |
客户端消息 ID | 幂等去重 |
send_time |
发送时间 | 展示与审计 |
status |
消息状态 | 预留撤回、删除等扩展 |
关键索引:
UNIQUE KEY `uk_infra_im_message_client`
(`tenant_id`, `conversation_id`, `sender_id`, `client_msg_id`, `deleted`),
KEY `idx_infra_im_message_time`
(`tenant_id`, `conversation_id`, `id`)
其中 client_msg_id 的唯一索引用于处理网络抖动下的重复发送。前端发送失败重试时,只要客户端消息 ID 不变,后端就能避免插入重复消息。
七、RuoyiOffice 创新设计:把 IM 做成 OA 协同层,而不是聊天插件
7.1 创新一:IM 入口在 OA,能力沉到 infra
很多项目会把聊天功能做成一个独立页面,和业务模块毫无关系。RuoYi Office 的设计更像一层协同基础设施:
| 设计 | 价值 |
|---|---|
| OA 菜单下有"即时通讯" | 员工认知上属于办公协同 |
| 后端放在 infra 模块 | 可以被 OA、BPM、CRM、项目等模块复用 |
API 路径为 /infra/im/* |
明确它是基础能力而非单一业务功能 |
这符合企业平台的演进规律:一开始是 OA 内部聊天,后面会自然扩展成全平台沟通能力。
7.2 创新二:审批详情可以直接发起业务沟通
startConversationFromProcessInstance() 的设计,让审批系统和 IM 建立了第一条业务链路:
流程详情 → 识别发起人/审批人 → 创建单聊 → 跳转 IM → 携带流程上下文
这个能力看似很小,但对实际办公效率影响很大:
- 审批人不需要离开系统找人。
- 发起人收到消息时能知道在讨论哪张单。
- 后续可以扩展为聊天面板内直接打开审批详情。
- 业务沟通不再从系统外部开始。
7.3 创新三:会话更新作为服务端事实推送
前端收到新消息后,如果自己计算会话列表状态,很容易出现多个页面状态不一致:
- 当前聊天窗口有新消息。
- 左侧会话列表摘要没更新。
- 顶部未读数没变化。
- 置顶排序没有刷新。
RuoYi Office 后端每次发送消息都会推送 im.conversation.update,把会话视图作为服务端事实下发。这样前端只负责渲染,不需要复制后端规则。
7.4 创新四:统一文件上传支撑图片和附件消息
企业聊天里,文本只是基础,更多协同发生在附件里:
- 审批补充材料。
- 发票、合同、制度文件。
- 公文正文、附件、扫描件。
- 项目交付文档。
RuoYi Office 前端 IM 页面复用 uploadFile,消息类型支持 image 和 file。这样 IM 不需要单独实现一套文件存储,也能和系统统一文件能力保持一致。
7.5 创新五:和顶部工作台形成未读触达闭环
顶部 IM 图标 + 未读数徽章,是一个非常关键的产品设计。
它让 IM 不再依赖用户主动进入菜单,而是进入工作台级触达:
| 触达对象 | 触达入口 |
|---|---|
| 审批待办 | 工作台、流程中心 |
| 系统公告 | 通知下拉、首页组件 |
| 私聊/群聊 | 顶部 IM 图标、未读数 |
这三者组合起来,才是企业 OA 真正需要的"消息触达体系"。
八、扩展设计:内置 IM 还能如何继续演进?
当前 RuoYi Office IM 已经具备基础可用能力。如果继续向企业级深水区演进,可以沿着以下方向扩展。
8.1 业务对象消息卡片
现在 IM 通过 query 携带 processInstanceId 和 processInstanceName。下一步可以把业务对象变成结构化消息卡片:
{
"type": "business_card",
"bizType": "bpm_process",
"bizId": "187654321",
"title": "出差报销审批",
"summary": "张三提交了 3 天上海出差报销,金额 2680 元",
"url": "/bpm/process-instance/detail?id=187654321"
}
这样聊天里就不只是文本链接,而是可读、可点击、可追溯的业务卡片。
8.2 会话与单据关系表
如果要让一张审批单长期绑定一个讨论组,可以增加业务关系表:
| 字段 | 含义 |
|---|---|
biz_type |
业务类型,如 bpm_process、oa_seal、crm_customer |
biz_id |
业务主键 |
conversation_id |
绑定会话 |
tenant_id |
租户编号 |
这样可以支持:
- 每张流程单自动创建协作群。
- 单据详情页展示相关沟通记录。
- 归档时把会话作为审计材料的一部分。
8.3 消息已读明细
当前成员表通过 last_read_message_id 和 unread_count 表达阅读进度,适合 MVP。群聊规模变大后,可以增加消息已读明细:
| 能力 | 价值 |
|---|---|
| 单条消息已读人数 | 群聊中知道谁看过 |
| 重要消息确认 | 制度公告、任务通知需要确认 |
| 审批沟通留痕 | 谁何时读取过关键意见 |
8.4 与 BPM 评论/审批意见联动
审批意见和 IM 消息不是一回事:
- 审批意见是正式结论。
- IM 消息是沟通过程。
但两者可以互相引用。比如审批人可以把某条 IM 消息"引用为审批说明",或者在审批详情里展示"与该流程相关的沟通记录"。
这样既不污染正式审批意见,又能保留必要的协商上下文。
九、技术亮点总结
| 设计点 | 实现方式 | 业务价值 |
|---|---|---|
| 内置 IM | OA 菜单入口 + infra 后端能力 | 沟通留在企业系统内部 |
| 统一身份 | 复用系统用户、部门、租户 | 不维护第二套通讯录 |
| 单聊去重 | single_key=minUserId:maxUserId |
避免重复会话 |
| 群聊支持 | 会话表 + 成员表 | 支持多人协同 |
| 实时推送 | WebSocket /infra/ws |
消息即时触达 |
| 双事件模型 | im.message.receive + im.conversation.update |
消息区和会话列表同步刷新 |
| 未读数 | 成员表 unread_count |
顶部徽章、会话列表可直接展示 |
| 个人配置 | top_flag、mute_flag |
置顶和免打扰不影响其他人 |
| 图片/文件 | 复用统一上传接口 | 附件协同不重复造轮子 |
| 业务上下文 | query 携带流程实例信息 | 从审批自然进入沟通 |
| 多租户隔离 | 三张表都继承租户字段 | SaaS 场景数据边界清晰 |
十、快速体验路径
如果你想体验 RuoYi Office 的内置 IM,可以按下面路径操作:
- 登录后台系统。
- 进入 OA → 即时通讯。
- 点击发起单聊,选择一个系统用户。
- 发送文本消息,观察左侧会话列表最后消息变化。
- 上传图片或文件,查看消息类型渲染。
- 创建群聊,选择多个成员。
- 使用置顶、免打扰,观察会话排序和个人配置。
- 回到顶部导航,查看 IM 未读数徽章。
如果从审批场景体验,可以从流程详情页发起沟通,系统会自动创建单聊并跳转到 IM 页面,携带流程实例上下文。
结语:IM 不是聊天功能,而是 OA 的实时协同层
企业 OA 发展到一定阶段后,真正拉开差距的不是"表单数量",而是协作链路是否完整。
外部 IM 可以解决临时沟通,却很难解决企业系统需要的身份一致、权限一致、租户隔离、上下文回流和过程追溯。内置 IM 的意义,是把"谁在处理、为什么卡住、需要谁补充、沟通结果是什么"这些协作过程,重新拉回 OA 的业务闭环里。
RuoYi Office 当前的 IM 设计,用三张表、两类 WebSocket 事件、一个 OA 入口和一套上下文跳转机制,完成了从"聊天页面"到"协同基础设施"的第一步。
一套代码,通吃多端;一层 IM,打通协作上下文。这才是企业 OA 内置即时通讯真正值得做的原因。
💡 RuoYi Office —— 一个平台,管好整个企业
🌐 在线演示:http://ruoyioffice.com/web/(账号 admin / admin123)
📦 源码1:https://gitcode.com/zhouzhongyan/ruoyi-office-vben.git
📦 源码2:https://gitcode.com/zhouzhongyan/ruoyi-office.git
📦 源码3:https://github.com/yuqing2026/ruoyi-office.git
💬 微信:添加 17156169080,备注「RuoYi Office」
⭐ 如果觉得不错,请给个 Star 支持一下!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)