// 转发消息给接收方(对方,客服)
s.forwardMessage(msg)
// 回显消息给发送者(就是当前这个发消息的用户自己)
if err := conn.WriteJSON(msg); err != nil {
utils.Logger.Error("im ws echo message failed", zap.Error(err), zap.String("uid", uid))
}
采用消息回显方案,前端使用本地预渲染提升体验:点击发送立刻展示消息气泡并显示发送加载圈;收到这条回显包就代表消息已经在服务端入库成功,前端可以使用后端返回的真实数据更新本地临时消息,清除加载转圈,标记发送成功;如果长时间收不到回显包,则判定发送失败,展示失败重试标识。 两条推送相互独立:forwardMessage 负责推送给对话的另一方;conn.WriteJSON 是给发送方自身的回执回包,不要混淆。
export interface WsRawResponse {
msgId: string;
msgSeq: number;
convId: string;
senderId: string;
receiverId: string;
content: string;
sendTime: number;
msgType: number; // 1=文本消息
}
// 发送普通消息,适配后端WsMessage协议
private sendNormalMessage() {
if (!this.message.trim() || !this.currentConvId) {
return;
}
// 组装后端WebSocket协议结构体
const sendRaw: WsRawResponse = {
msgId: "",
msgSeq: 0,
convId: this.currentConvId!,
senderId: this.clientId.toString(),
receiverId: "44", // 临时硬编码客服ID,后续从会话列表接口拿到targetId替换
content: this.message.trim(),
sendTime: Date.now(),
msgType: 1, //1=文本消息
};
wsClient.sendMessage(sendRaw);
// 本地临时渲染(可选,也可以等ws回包再渲染,避免双消息)
const selfMsg = new MessageModel(
"",
this.clientId,
this.message.trim(),
Date.now(),
true,
0
);
//this.msgList = [...this.msgList,selfMsg]
Logger.debug(TAG, `===== msgList 发送后 =====`, JSON.stringify(this.msgList));
//this.msgList.push(selfMsg);
this.message = '';
this.scroller.scrollEdge(Edge.Bottom);
}
前端发送消息时,只能填充临时数据:msgId为空、msgSeq置 0。消息入库之后,后端会生成真实的msgId、会话维度自增msgSeq以及服务端时间戳sendTime,并通过回显包把这一套权威数据推送回前端。
// 服务端强制生成、覆盖关键字段,防止前端篡改
msg.MsgId = fmt.Sprintf("msg_%d_%s", time.Now().UnixMilli(), uuid.NewString()[:8])
msg.SenderId = uid
msg.SendTime = time.Now().UnixMilli()
if msg.MsgType == 0 {
msg.MsgType = 1
}
// 入库
s.saveMessageToDB(&msg)
该设计的业务价值:前端可以将本地预渲染的临时消息,替换成后端下发的真实msgId。后续消息撤回、已读回执、消息重发、离线消息同步等能力,都必须依赖后端生成的msgId;时间采用服务端时间,规避手机本地时间可随意修改带来的数据错乱;msgSeq用于消息断点续拉、多端消息同步以及弱网环境下的消息顺序校正。如果后端不做消息回显,前端本地预渲染出来的消息会一直没有真实msgId,撤回、标记已读等功能就无法实现。