uniapp 对接融云 IM:单聊、群聊与音视频通话实战

📅 发布时间:2026/10/14 19:57:24
uniapp 对接融云 IM:单聊、群聊与音视频通话实战
简介这份资源面向需要在 uniapp 中集成即时通讯与音视频能力的开发者重点解决融云 IM 聊天、群聊以及单多人音视频通话的落地问题。内容覆盖消息监听、消息撤回回执、分页获取聊天记录、会话未读数统计、免打扰时间设置、输入状态消息等常见接口并附带后端 token 生成与 maven 环境说明适合有一定前端与 Java 基础、希望快速跑通 IM 与音视频链路的团队参考。压缩包共 651 个文件以 379 个 png 图片、177 个 h 头文件、27 个 js 脚本为主另含 json 配置、aar 与 a 静态库、plist、strings 等原生资源整体约 79.12MB涵盖 Android、iOS 及小程序多端依赖。资源内提供可直接运行的 demo并配有实现文档便于对照接口调试与二次开发。目前已有 2639 人学习下载适合作为融云 IM 与音视频通话功能接入的实践参考。1. 从零跑通 uniapp 对接融云 IM单聊、群聊、音视频通话到底难在哪很多人第一次在 uniapp 里接 IM都会卡在同一个地方前端 SDK 明明初始化成功了消息也能发出去但一到「后端签发 token」和「多人音视频房间」这两步就翻车。原因不复杂——融云的 IM 能力和音视频能力是两套独立的 SDK前者靠 AppKey Token 走长连接后者靠房间号 用户 ID 走媒体通道而 uniapp 又横跨 H5、小程序、App 三端每端的原生能力差异会让同一份代码表现完全不同。这篇笔记就围绕「uniapp 对接融云 IM 聊天、群聊、单多人音视频通话」这条主线把后端 token 签发Maven 环境、前端 SDK 初始化、会话列表、群组管理、音视频房间这几块拆开讲清楚。适合已经会写 uniapp 页面、但对 IM 长连接和实时音视频没有系统落地经验的开发者也适合想评估这套方案值不值得投入的技术负责人。2. 后端先立住Maven 工程里签发融云 Token 的最小闭环前端能不能连上融云第一步不在前端而在后端有没有把 Token 正确签出来。融云的 Token 是服务端用 AppKey 和 AppSecret 按规则算出来的前端拿不到 AppSecret所以这一步必须放在后端。下面用一个 Maven 工程把这件事做完整。2.1 为什么 Token 必须后端签发前端不能自己算融云的鉴权模型是「AppKey 公开、AppSecret 保密」。AppKey 会写进前端配置用来标识应用AppSecret 只能存在服务端用来计算 Token 的签名。如果为了省事把 AppSecret 塞进前端等于把整个应用的鉴权能力公开出去任何人拿到它都能伪造任意用户的 Token 登录你的 IM。所以标准做法是前端登录自己的业务系统后带着业务用户 ID 请求后端接口后端用 AppSecret 换算出融云 Token 再返回。Token 本身有三个关键属性userId融云侧用户唯一标识、name昵称、portraitUri头像。userId 一旦确定就不要改它是消息路由和会话归属的主键。常见做法是直接用业务系统的用户 ID避免再做一层映射。2.2 Maven 依赖与 Token 签发代码融云官方提供了 Java SDK在 Maven 里引入即可不需要自己手写 SHA1 签名逻辑。!-- pom.xml 片段融云服务端 SDK -- dependency groupIdcn.rongcloud.sdk/groupId artifactIdrongcloud-server-sdk/artifactId version3.2.0/version /dependency// TokenService.java // 用 AppKey AppSecret 为用户签发融云 Token import io.rong.RongCloud; import io.rong.models.response.TokenResult; import io.rong.models.user.UserModel; public class TokenService { // 这两个值从融云控制台获取AppSecret 绝不能下发到前端 private static final String APP_KEY 你的AppKey; private static final String APP_SECRET 你的AppSecret; public String getToken(String userId, String name, String portraitUri) throws Exception { RongCloud rongCloud RongCloud.getInstance(APP_KEY, APP_SECRET); UserModel user new UserModel(); user.setId(userId); user.setName(name); user.setPortrait(portraitUri); // 每次调用都会返回一个有效 Token同一用户重复调用会得到不同字符串但都可用 TokenResult result rongCloud.user.register(user); if (result.getCode() ! 200) { throw new RuntimeException(签发失败: result.getErrorMessage()); } return result.getToken(); } }逻辑说明RongCloud.getInstance用 AppKey 和 AppSecret 建立服务端客户端user.register在融云侧注册或更新用户信息同时返回 Token。参数上userId建议用业务主键name和portraitUri用于会话列表和消息推送展示可以为空但强烈建议填。注意register是幂等的同一 userId 重复调用不会报错只会刷新用户资料。2.3 把 Token 接口暴露给 uniapp 的 Controller 写法后端签好 Token 后需要一个 HTTP 接口给 uniapp 调用。这里用最朴素的 Spring Boot Controller 演示重点是返回结构要固定方便前端统一处理。// AuthController.java import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/im) public class AuthController { private final TokenService tokenService new TokenService(); PostMapping(/token) public Result getToken(RequestBody TokenReq req) { try { // req.userId 来自业务登录态不要直接信任前端传来的任意 userId String token tokenService.getToken(req.getUserId(), req.getName(), req.getAvatar()); return Result.ok(token); } catch (Exception e) { return Result.fail(e.getMessage()); } } }参数说明userId必须从服务端登录态里取不能直接拿前端传的值否则任何人都能拿到别人的 Token。name和avatar可以前端传但要做长度和内容校验。返回结构里code、msg、data三字段固定前端只判断code即可。提示Token 有效期由融云控制台配置默认较长。如果业务要求强制下线需要在服务端维护「用户当前 Token 版本」旧版本 Token 主动失效。3. uniapp 前端接入SDK 初始化、会话列表与群聊管理后端 Token 通了之后前端才有资格谈「连上」。这一章把 uniapp 侧的初始化、单聊、群聊三件事串起来。3.1 uniapp 里引入融云 IM SDK 的两种方式融云 IM SDK 在 uniapp 里通常有两种接法一是用官方提供的原生插件App 端二是用 npm 包配合条件编译H5 和小程序端。实际项目里最常见的是「App 用原生插件、H5 用 npm 包」通过#ifdef区分。// im.js —— 统一封装初始化逻辑 // #ifdef APP-PLUS const RongIM uni.requireNativePlugin(RongIMLib); // #endif // #ifdef H5 import RongIM from rongcloud-sdk; // #endif let imClient null; export function initIM(appKey, token) { return new Promise((resolve, reject) { // 初始化时传入 AppKeyconnect 时传入后端签发的 token imClient RongIM.init({ appKey }); imClient.connect({ token: token, success: (userId) { console.log(IM 连接成功, userId, userId); resolve(userId); }, fail: (code, msg) reject(new Error(code : msg)) }); }); }逻辑说明init只做一次connect在每次登录后调用。参数appKey来自融云控制台token来自上一章的后端接口。注意connect是异步的页面里所有依赖连接状态的操作发消息、拉会话都要放在success回调之后否则会拿到空会话列表。3.2 单聊会话列表与消息发送的最小实现连接成功后第一件事是拉会话列表第二件事是发消息。融云的会话列表由 SDK 本地维护调用getConversationList即可。// 拉取会话列表 export function loadConversations() { return new Promise((resolve) { imClient.getConversationList({ count: 50, // 一次最多拉 50 条分页靠 timestamp success: (list) resolve(list), fail: () resolve([]) }); }); } // 发送单聊文本消息 export function sendTextMessage(targetId, text) { // ConversationType.PRIVATE 1群聊为 3 const conversationType 1; imClient.sendTextMessage({ conversationType: conversationType, targetId: targetId, // 单聊时是对方 userId群聊时是群 ID text: text, success: (message) console.log(发送成功, message.messageId), fail: (code, msg) console.error(发送失败, code, msg) }); }参数说明conversationType是融云的核心枚举单聊 1、群聊 3、系统会话 6写错会导致消息发到错误通道。targetId在单聊里是对方 userId在群聊里是群 ID两者不能混。count控制单次拉取数量实际项目里建议配合timestamp做滚动分页避免一次拉太多卡住渲染。3.3 群聊创建群、拉人、收群消息群聊比单聊多一层「群组管理」。融云的群组操作走createGroup、joinGroup、quitGroup这几个方法群消息的收发和单聊共用同一套消息接口只是conversationType换成 3。// 创建群组并拉人 export function createGroup(groupId, groupName, userIds) { imClient.createGroup({ groupId: groupId, // 业务侧生成的唯一群 ID groupName: groupName, memberIds: userIds, // 初始成员 userId 数组 success: (info) console.log(建群成功, info), fail: (code, msg) console.error(建群失败, code, msg) }); } // 监听群消息 export function onGroupMessage(callback) { imClient.setOnReceiveMessageListener({ received: (message) { // message.conversationType 3 表示群消息 if (message.conversationType 3) { callback(message); } } }); }逻辑说明groupId建议由业务后端生成并落库不要用融云自动生成的 ID否则业务侧无法关联。memberIds是初始成员后续拉人用joinGroup。群消息监听是全局的建议在 App 启动时注册一次页面里只做 UI 更新避免重复注册导致消息回调多次触发。注意群成员上限和群数量上限由融云套餐决定创建前先确认套餐额度否则会在fail回调里拿到配额错误。4. 音视频通话单人 1v1 与多人房间的落地差异IM 消息通了之后音视频是另一套体系。融云音视频 SDK 和 IM SDK 是分开的房间模型也不一样1v1 通话是「两人房间」多人通话是「N 人房间」底层都是同一个房间概念区别在于加入人数和 UI 布局。4.1 音视频 SDK 初始化与房间加入音视频 SDK 需要单独初始化并且要在 IM 连接成功之后再做因为房间鉴权依赖 IM 的登录态。// rtc.js —— 音视频初始化与加入房间 // #ifdef APP-PLUS const RongRTCLib uni.requireNativePlugin(RongRTCLib); // #endif let rtcClient null; export function initRTC() { // 音视频 SDK 初始化必须在 IM connect 成功之后调用 rtcClient RongRTCLib.init({}); return rtcClient; } // 加入房间roomId 相同即进入同一房间 export function joinRoom(roomId, userId) { return new Promise((resolve, reject) { rtcClient.joinRoom({ roomId: roomId, // 业务侧生成的房间号1v1 和多人共用 userId: userId, success: () { // 加入成功后发布本地音视频流 rtcClient.publish({ audio: true, video: true, success: () resolve(), fail: (c, m) reject(new Error(c : m)) }); }, fail: (code, msg) reject(new Error(code : msg)) }); }); }参数说明roomId是房间唯一标识1v1 通话时可以用「双方 userId 排序拼接」生成多人通话时用业务房间号。publish的audio、video控制是否发布对应流纯语音房间把video设为 false 可以省带宽。注意joinRoom和publish是两步只 join 不 publish 的话对方看不到你的画面。4.2 1v1 与多人通话的 UI 布局差异1v1 通话的 UI 通常是「对方大画面 自己小窗」多人通话是「宫格布局」。这个差异不在 SDK而在渲染层。融云音视频 SDK 会把远端用户的流以视图形式暴露出来uniapp 里需要用原生视图承载。// 渲染远端用户画面App 端用原生 view 承载 export function renderRemoteView(userId, containerId) { rtcClient.setRemoteVideoView({ userId: userId, // 远端用户 ID containerId: containerId, // 页面里预留的容器 id success: () console.log(远端视图渲染成功), fail: (c, m) console.error(渲染失败, c, m) }); }逻辑说明setRemoteVideoView把远端流绑定到指定容器。1v1 时只调用一次多人时对每个远端 userId 调用一次容器用宫格排列。参数containerId要和页面里的id严格对应写错会黑屏但不报错这是最常见的排查点。4.3 通话邀请与挂断的信令设计音视频 SDK 只负责媒体通道通话邀请、接听、挂断这些信令要靠 IM 消息来传。常见做法是自定义消息类型收到后弹接听界面。// 发送通话邀请自定义消息 export function sendCallInvite(targetId, roomId, isVideo) { imClient.sendMessage({ conversationType: 1, // 1v1 邀请 targetId: targetId, messageType: CALL_INVITE, // 自定义消息类型标识 content: JSON.stringify({ roomId: roomId, video: isVideo }), success: () console.log(邀请已发送), fail: (c, m) console.error(邀请失败, c, m) }); }参数说明messageType用自定义字符串接收端在onReceiveMessageListener里判断这个类型再弹窗。content里带roomId和是否视频接收端据此加入同一房间。挂断同理发一条CALL_HANGUP消息双方收到后离开房间并释放视图。提示信令消息建议加超时逻辑比如 30 秒未接听自动发挂断避免房间一直挂着占用配额。5. 避坑与排查那些让 IM 和音视频集体翻车的细节这一章是我踩过的坑里最值得单独拎出来的几条每条按「现象 → 原因 → 解决」写方便对照排查。5.1 现象IM 连接成功但收不到消息原因setOnReceiveMessageListener注册时机太晚或者被注册了多次。融云的监听是覆盖式的后注册的会覆盖先注册的如果页面里每次onShow都注册一次最后一次注册的监听会生效但前面的页面逻辑就收不到回调了。解决把监听注册放在 App 启动或 IM 初始化之后只注册一次页面里通过全局事件总线或状态管理接收消息不要在页面生命周期里反复注册。5.2 现象Token 签发接口返回 200 但前端 connect 失败原因Token 和 AppKey 不匹配。常见于测试环境和生产环境混用后端用了测试 AppKey 签发前端用了生产 AppKey 初始化或者反过来。解决把 AppKey 和 AppSecret 成对管理环境变量里绑定在一起签发接口返回时顺带返回当前 AppKey前端初始化前先比对一次。5.3 现象多人音视频房间只有自己能听到声音原因只joinRoom没有publish或者publish时audio设成了 false。另一个常见原因是远端视图没有调用setRemoteVideoView导致对方流没被渲染误以为没声音。解决加入房间后必须显式publish并对每个远端用户调用setRemoteVideoView。排查时先看本地是否有声音再看远端视图容器 id 是否写对。5.4 现象群聊消息发送成功但群成员收不到原因群成员没有真正加入群。createGroup时传的memberIds只是初始成员如果成员是后来通过业务逻辑加进来的必须调用joinGroup否则融云侧不认为他在群里消息不会推送。解决建群后统一走一次joinGroup同步成员或者建群时就把所有成员放进memberIds。群成员变更后要同步更新融云侧群组不要只改业务库。5.5 现象H5 端音视频无法采集原因H5 端受浏览器安全策略限制非 HTTPS 或非 localhost 环境下无法调用摄像头和麦克风。小程序端则需要在后台配置音视频权限。解决H5 调试用 localhost 或配 HTTPS小程序端在管理后台开通音视频权限并配置合法域名。App 端相对宽松但也要在 manifest 里声明相机和麦克风权限。6. 进阶技巧把 IM 和音视频的初始化收敛成一个可复用模块做到这里功能基本能跑了但代码往往散落在各个页面里登录一次初始化一次退出再登录又初始化一次状态管理很乱。我后来习惯把 IM 和音视频的初始化收敛成一个单例模块用状态机管理「未初始化 → 已连接 → 已断开」三种状态页面只订阅状态变化。具体做法是模块内部维护imClient和rtcClient两个实例暴露login、logout、onMessage、joinRoom几个方法。login里先调后端拿 Token再connect成功后初始化音视频 SDK。logout里先离开所有房间再断开 IM 连接最后清空实例。这样页面里只需要在onLoad时调一次login在onUnload时不用管模块自己处理引用计数。验证方法也很直接写一个最小页面只放一个按钮触发login一个按钮触发sendTextMessage一个按钮触发joinRoom。如果三个按钮都能按预期工作说明模块封装没问题。然后再把这个页面复制到 H5、小程序、App 三端各跑一遍重点看音视频权限和视图渲染的差异。一个我自己的习惯是每次改完 IM 或音视频相关代码先不急着测业务页面而是用这个最小页面跑一遍「登录 → 发消息 → 进房间 → 退房间 → 退出登录」的完整链路。这条链路能过业务页面基本不会出大问题这条链路过不了业务页面里排查成本会高很多。希望帮到你。本文还有配套的精品资源点击获取