简介:这份资源面向需要在 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> <groupId>cn.rongcloud.sdk</groupId> <artifactId>rongcloud-server-sdk</artifactId> <version>3.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) => { // 初始化时传入 AppKey,connect 时传入后端签发的 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 或音视频相关代码,先不急着测业务页面,而是用这个最小页面跑一遍「登录 → 发消息 → 进房间 → 退房间 → 退出登录」的完整链路。这条链路能过,业务页面基本不会出大问题;这条链路过不了,业务页面里排查成本会高很多。希望帮到你。
本文还有配套的精品资源,点击获取