IM系统中如何优解“聊天消息撤回”

聊天消息撤回重点关注

聊天消息撤回由于属于一个低频率的操作,所以我们重点关注实时性、一致性即可

主要技术及解决方案

一、消息分发与同步

  1. 消息分发与同步相对复杂,由于一条消息可能会被发送给多个接收方,所以撤回功能必须要确保所有接收方都能同步收到撤回操作,这就要求撤回指令能有效分发并被客户端正确处理
  2. 不同的客户端可能会有不同的同步延迟或网络问题,如何确保所有客户端在撤回后尽快同步消息状态是一个挑战

解决方案:

  • 消息撤回通知机制:使用消息队列Kafka来分发撤回通知
  • 可靠传输协议:使用可靠的传输协议WebSocket保持客户端与服务器的长连接,即时推送撤回通知
  • 冗余确认机制:每个客户端在接收到撤回消息后,必须向服务器发送确认消息

二、存储与缓存一致性

  1. 消息撤回通常涉及到消息状态的修改,消息在客户端和服务器之间可能都有缓存。撤回后,需要确保所有缓存、数据库和日志中的消息状态一致更新
  2. 在数据库层面增加了撤回状态字段,则消息队列、缓存层如Redis等也需要考虑如何及时清理和同步更新

解决方案:

  • 软删除和状态字段设计:在数据库中,使用软删除来标记已撤回的消息,保持历史记录的完整性。通过添加字段revoke_status来记录消息是否被撤回
  • 缓存与数据库的同步:撤回消息时,通过脚本更新数据库、缓存。如需要,比如网络抖动等特殊情况导致缓存更新失败则通过事务锁、回滚等操作同步删除、更新缓存(由于撤回消息属于一个低频的操作,所以不进行此优化)
  • 异步处理机制:使用异步任务处理来更新不同系统中的消息状态,通过消息队列通知缓存层和数据库同步撤回状态

三、多端状态管理

  1. 如果用户在多个设备上登录(如手机、平板、PC等),撤回消息时需要同时通知所有设备,并且处理设备之间不同步带来的数据冲突问题
  2. 各个终端的消息展示状态、撤回提示(如"该消息已撤回")都需要一致,避免出现某端显示错误或未撤回的情况

解决方案:

(脱敏)既定规则下,操作一个端时,自动同步其他端

四、历史记录的管理

  1. 用户有可能在聊天历史中搜索或查看过去的消息,撤回功能需要确保历史记录中的消息也能正确显示已撤回的状态
  2. 需要避免撤回后的消息在某些缓存中被错误保留,导致用户通过某种方式仍能查看到已撤回的内容

解决方案:

  • 更新撤回后的历史记录
  • 缓存清理与持久存储同步:消息撤回后,更新客户端的本地缓存与服务器端的持久化存储,通过事件驱动机制(钩子(类似于中间件),进行回调)确保撤回信息同步到所有存储层(es、日志等)。在客户端,定期刷新缓存,以防旧数据残留
  • 防止缓存保留已撤回内容:客户端在收到撤回消息时,清除本地缓存的相关内容,避免在用户通过某些方式(如离线模式、数据抓包等)重新读取已撤回的消息

五、安全性与隐私

  1. 消息撤回的实现要确保消息内容在撤回后不会通过客户端逆向工程、缓存泄露等方式被找回
  2. 消息撤回需考虑如何防止恶意用户通过撤回功能误导或进行不当操作

解决方案:

  • 消息加密与撤回保护:为了防止客户端逆向工程获取撤回的消息内容,消息在传输和存储过程中应使用端到端加密
  • 本地存储清理:撤回操作后,客户端及时清理本地缓存中的消息记录,防止用户通过客户端内存或本地文件找回已撤回的消息,同时通过本地文件系统的加密存储,进一步防止消息泄露
  • 日志记录:为了防止恶意撤回行为,通过日志记录所有撤回操作

核心代码实现

核心代码实现包括【api】聊天记录消息撤回、【ws】撤回消息通知接收、【ws】消息事件更新

聊天记录消息撤回

一、校验消息参数

	if len(req.ServerId) == 0 {
		err = openerror.New(cerror.LostRequiredParameters, `缺少聊天消息server_id`)
		return
	}
	if len(req.LocalId) == 0 {
		err = openerror.New(cerror.LostRequiredParameters, `缺少聊天消息local_id`)
		return
	}
	if len(req.HkExtUserid) == 0 {
		err = openerror.New(cerror.LostRequiredParameters, `缺少聊天消息hk_ext_userid`)
		return
	}

二、检查消息是否存在

	var msgInfo dao.ZqMsg
	session := dao.NewZqMsgDao().GetSlaveDB(req.HkUserid).
		Where("hk_userid = ?", req.HkUserid).
		And("hk_object_id = ?", req.HkExtUserid).
		And("server_id = ?", req.ServerId).
		And("local_id = ?", req.LocalId)
	if req.MsgId > 0 {
		session.And("msg_id = ?", req.MsgId)
	}
	_, err = session.OrderBy("id desc").Get(&msgInfo)
	if err != nil {
		logger.Ex(ctx, tag, fmt.Sprintf("查询聊天消息失败 req = %+v, err = %v", req, err))
		return
	}

	if msgInfo.MsgId == 0 {
		err = openerror.New(cerror.InvalidData, `聊天消息不存在`)
		logger.Ex(ctx, tag, fmt.Sprintf("撤回的聊天消息不存在 req = %+v", req))
		return
	}

三、权限与时效性检查

	if msgInfo.SenderWorkCode == `` {
		err = openerror.New(cerror.InvalidData, `该消息非客服发送消息,无法撤回`)
		logger.Wx(ctx, tag, fmt.Sprintf("撤回的聊天消息非客服发送消息 req = %+v", req))
		return
	}

	//如果消息发送超过2分钟,提示消息超过2分钟无法撤回
	if gtools.UnixMillis()-msgInfo.CreateTime > 120*1000 {
		err = openerror.New(cerror.InvalidData, `超过2分钟无法撤回`)
		logger.Wx(ctx, tag, fmt.Sprintf("撤回的聊天消息超过2分钟 msg_id:%d req = %+v", msgInfo.MsgId, req))
		return
	}

四、调用RPC接口撤回消息

	rpcReq := &proto.RevokeMsgSendRequest{
		Request: proto.Request{
			HkUserId: req.HkUserid,
			CorpId:   req.CorpId,
		},
		MsgId:    msgInfo.MsgId,
		ServerId: req.ServerId,
		LocalId:  req.LocalId,
	}
	rpcResp := &proto.RevokeMsgSendResponse{}
	err = client.RevokeMsgSend(ctx, rpcReq, rpcResp)
	logger.Ix(ctx, tag, fmt.Sprintf("撤回聊天消息 req:%+v resp:%+v", rpcReq, rpcResp))
	if err != nil {
		logger.Ex(ctx, tag, fmt.Sprintf("RPC调用失败 req = %+v, err = %v", req, err))
		return
	}
	if rpcResp.TaskId == `` {
		logger.Ex(ctx, tag, fmt.Sprintf("撤回聊天消息失败 msg_id:%d req = %+v, resp = %+v", msgInfo.MsgId, req, rpcResp))
		return
	}

五、缓存记录

	_, _ = cache.NewStringRepo().SetNx(ctx, rediskey.RdsImMsgKey, fmt.Sprintf(rediskey.ImMsgRevokeStaffWorkCodeKey, req.MsgId), req.MsgId, 120)
    return

撤回消息通知接收

一、注册handle

	// hk用户消息事件
	handleMap["hk_user_msg_event"] = handle.HkUserMsgEvent

二、消息事件接收,根据事件类型调用对应的处理函数 

func HkUserMsgEvent(ctx context.Context, v []byte) (ret bool, err error) {

	tag := "HkUserMsgEvent"
	ret = true

	logger.Ix(ctx, tag+".body", string(v))

	eventMsg := new(metadata.WechatZhuqueEvent)
	err = json.Unmarshal(v, eventMsg)
	if err != nil {
		logger.Ex(ctx, tag, fmt.Sprintf("data json unmarshal error. data:%s", string(v)), "error", err)
		return
	}

	switch eventMsg.Event {
	case metadata.RetryUploadMediaEvent:
		// 处理消息
		err = immsgService.HandleImMsgReload(ctx, eventMsg)
		if err != nil {
			logger.Ex(ctx, tag, fmt.Sprintf("send msg error, err:%+v", err))
			return
		}
	case metadata.RevokeMsgEvent:
		err = hkmsgService.RevokeMsg(ctx, eventMsg)
		if err != nil {
			logger.Ex(ctx, tag, fmt.Sprintf("revoke msg error err:%+v", err))
			return
		}
	default:
		logger.Wx(ctx, tag, "event type error")
		return
	}

	return true, err
}

消息事件更新

全部代码不再一一赘述,展示的代码已进行脱敏处理:

一、解析事件数据

二、判断消息是否为引用的消息,是则暂不进行撤回

三、查询消息(是否存在)

四、设置撤回锁,防止消息重复撤回

	if back, err = cache.NewStringRepo().SetNx(ctx, rediskey.RdsImMsgKey, fmt.Sprintf(rediskey.ImMsgRevokeLockKey, hkUserMsgInfo.MsgId), hkUserMsgInfo.MsgId, 30); err != nil || !back {
		logger.Dx(ctx, tag, fmt.Sprintf("setnx error. hk_userid:%s msg_id:%d server_id:%s back:%v err:%+v", imMsg.HkUserid, hkUserMsgInfo.MsgId, eventBody.ServerId, back, err))
		return
	}

	if hkUserMsgInfo.RevokeStatus == 1 {
		logger.Dx(ctx, tag, fmt.Sprintf("msg is revoke  hk_userid:%s msg_id:%d server_id:%s err:%+v", imMsg.HkUserid, hkUserMsgInfo.MsgId, eventBody.ServerId, err))
		return
	}

五、查询消息撤回数量,更新撤回状态

六、判断消息撤回方、撤回消息、推送前端

	//构造revoke消息
	msgItem := entity.RevokeMsgItem{
		Workcode:   convInfo.StaffWorkcode,
		WxUserid:   convInfo.WxUserid,
		WxCorpId:   convInfo.WxCorpId,
		ConvId:     convInfo.Id,
		ConvType:   convInfo.Type,
		Msgid:      hkUserMsgInfo.MsgId,
		ServerId:   hkUserMsgInfo.ServerId,
		LocalId:    hkUserMsgInfo.LocalId,
		RevokeTime: eventBody.SendTime * 1000,
		UserType:   getRevokeUserType(ctx, hkUserMsgInfo),
	}
	//推送Ws消息到前端
	traceId, err := wsmanage.RevokeMsg(ctx, convInfo.StaffWorkcode, convInfo.WxCorpId, &msgItem)
在这个互联网时代,客服可以说必不可少,每个电商网站都应该有一个强大的智能客服对话系统,以满足用户沟通的需求。智能客服对话系统,不仅需要人工的沟通,同时结合人工智能实现智能对话,减少人工客服的成本,势在必行。基于SpringBoot+Python的多语言前后端智能多人聊天系统课程,将以基础知识为根基,带大家完成一个强大的智能客服系统,该系统将包含以下功能:智能对话机器人、单聊、群聊、消息撤回、上线、下线通知、用户动态信息实时提示等。即时通讯和人工智能,在未来的发展趋势,必然需要大批人才,掌握这两个技术势在必行。项目是一个真实可用的项目,商业价值不言而喻。也可以基于课程的基础上进一步完善和化,所以价值是很高的。本课程包含的技术: 开发工具为:IDEA、WebStorm、PyCharmTensorflowRNNLSTMAnacondaSpringBoot SpringCloudWebsocketSTOMPDjangoVue+Nodejs+jQuery等 课程亮点: 1.与企业接轨、真实工业界产品2.从基础到案例,逐层深入,学完即用3.市场主流的前后端分离架构和人工智能应用结合开发4.多语言结合开发,满足多元化的需求5.涵盖TensorFlow1.x+TensorFlow2.x版本6.智能机器人实战7.即时通讯实战8.多Python环境切换9.微服务SpringBoot10.集成SpringCloud实现统一整合方案 11.全程代码实操,提供全部代码和资料 12.提供答疑和提供企业技术方案咨询 课程目录:第一章、Anaconda以及TensorFlow环境和使用0、智能多人聊天系统课程说明1、智能多人聊天系统之Anaconda讲2、智能多人聊天系统之Anaconda安装和使用3、智能多人聊天系统之Anaconda之conda命令使用4、智能多人聊天系统之TensorFlow讲5、智能多人聊天系统之TensorFlow安装和使用6、TensorFlow常量、变量和占位符实战讲17、TensorFlow常量、变量和占位符实战讲28、TensorFlow原理补充讲9、TensorFlow四则运算实战讲10、TensorFlow矩阵操作以及运算实战讲111、TensorFlow矩阵操作以及运算实战讲212、TensorFlow均匀分布和正态分布数据实战讲13、智能多人聊天系统之Numpy实战讲14、智能多人聊天系统之matplotlib实战讲15、TensorFlow深度学习DNN讲16、TensorFlow常用Python扩展包讲17、TensorFlow常用回归算法以及正则化讲18、TensorFlow损失函数定义和使用实战讲19、TensorFlow化器讲以及综合案例实战讲20、智能多人聊天系统之RNN讲21、智能多人聊天系统之RNN种类讲22、智能多人聊天系统之RNN代码实战23、智能多人聊天系统之LSTM讲24、智能多人聊天系统之attention机制讲25、智能多人聊天系统之Django环境构建及初体验26、智能多人聊天系统之Django开发27、Python章节环境侯建和项目搭建28、Python TensorFlow读取训练数据代码编写29、Python TensorFlow形成语料编码30、Python TensorFlow保存字典文件31、Python TensorFlow构建词向量32、Python TensorFlow构建lstm模型以及attention wrapper33、Python TensorFlow训练代码编写34、Python整体代码讲35、Python运用模型代码讲36、SpringBoot讲以及构建web应用37、Spring Cloud注册中心构建38、智能多人聊天系统之前端Vue项目构建39、SpringBoot+Websocket群聊40、SpringBoot+Websocket昵称群聊41、SpringBoot+Websocket群聊+单聊实战42、SpringBoot+Stomp单聊143、SpringBoot+Stomp单聊244、SpringBoot+Stomp单聊+群聊45、Django Web整合TF代码讲及Postman调试46、智能客服系统单聊群聊等项目功能代码讲147、智能客服系统单聊群聊等项目功能代码讲248、智能客服系统集成机器人对话代码开发讲49、智能机器人TensorFlow2版本升级实战之训练模型代码讲50、智能机器人TensorFlow2版本升级实战之预测代码讲 51、智能机器人TensorFlow2版本升级实战补充讲
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值