必须以服务端为已读状态唯一权威,通过messageid幂等去重、servertimestamp排序、心跳+离线兜底实现多端一致:客户端收消息比对lastreadmessageid再上报,服务端更新last_read_message_id并广播read_ack,各端监听后刷新unreadcount。

App端多终端同步必须解决“会话状态漂移”问题
App端多个设备(比如同一账号在iPhone和安卓手机同时在线)收到同一条消息却各自标记已读、各自更新未读数,这是最常见的同步失效。根本原因在于:uni-im默认把每个socket连接视为独立会话,而没把“用户+会话ID”作为唯一状态锚点。
实操建议:
- 服务端下发消息时,必须携带
conversationId和messageId,且messageId需全局唯一(推荐Snowflake生成) - 客户端收到消息后,不要直接调用
conversation.read(),而是先比对本地lastReadMessageId是否小于该messageId,再决定是否触发已读上报 - 每次打开会话页时,必须先拉取服务端该会话的
lastReadMessageId,再用它初始化本地状态,不能依赖缓存值
uni-im的conversation.read()不是原子操作,必须配服务端确认
调用conversation.read(conversationId)只会在本地清空角标,但服务端未同步更新时,其他设备下次拉未读数仍会返回旧值——这就是“幽灵红点”的根源。
正确做法是让服务端承担已读状态的权威性:
- 前端调用
conversation.read()后,立即发起一次POST /api/conversation/read请求,带上conversationId和最新messageId - 服务端收到后,更新数据库中该会话的
last_read_message_id字段,并广播read_ack事件给该用户所有在线socket连接 - 各端监听到
read_ack后,主动刷新本地unreadCount,避免轮询
断网重连后消息乱序?关键在客户端消息去重与排序
App切后台再切回前台时,常出现新消息插在旧消息中间、甚至重复渲染——这不是WebSocket断连问题,而是本地消息队列没做幂等处理。
必须在插入聊天列表前完成三步校验:
- 用
messageId查本地数据库,若已存在则跳过插入 - 若不存在,按
createdAt时间戳升序插入;但注意:不同设备系统时间可能偏差,所以最终排序必须以服务端返回的serverTimestamp为准 - 收消息时统一走
uniIm.on('message', handler),不要混用socket.on('message'),否则绕过uni-im内置的消息去重逻辑
Android/iOS后台保活差异导致同步延迟,得靠心跳+离线兜底
iOS App进入后台几分钟后websocket会被系统强制关闭,Android虽能维持更久但也不稳定。单纯依赖连接存活,必然丢消息。
生产环境必须叠加两层保障:
- 客户端每30秒发一次
ping心跳包(用uni.sendSocketMessage),服务端超60秒未收到即标记该连接失效 - 服务端对每个用户维护一个
last_active_time,当新消息到达时,若该用户所有连接都失效,则写入离线队列,并在下次任意设备上线时推送 - App冷启动时,在
onLaunch里立刻调用uniIm.syncMessages()拉取离线消息,而不是等WebSocket连上才开始同步











