CIM和websockt-实现实时消息通信:双人聊天和消息列表展示

欢迎大佬的来访,给大佬奉茶

在这里插入图片描述

一、文章背景

有一个业务需求是:实现一个聊天室,我和对方可以聊天;以及有一个消息列表展示我和对方(多个人)的聊天信息和及时接收到对方发来的消息并展示在列表上。
项目框架概述:后端使用SpringCloud Alibaba+mybatis-plus;前端是uniapp框架的微信小程序。

文章目录

    • 欢迎大佬的来访,给大佬奉茶
  • 一、文章背景
  • 二、实现思路
    • 可以使用什么实现?
    • 使用CIM+websockt实现的优点是什么?
      • CIM是什么?
    • 业务的实现思路
  • 三、数据库中涉及的表
  • 四、业务UML图
    • 双人聊天类图+NS图
    • 消息列表展示类图+NS图
  • 五、业务代码
    • 后端代码
      • bootstrap配置文件(配置模块信息、中间件配置信息等)
      • nacos配置
      • controller层
      • service接口
      • service实现层
      • mapper接口
      • mapper.xml
    • 前端代码
      • 双人聊天
      • 聊天列表
      • 配置文件(JS后缀)
      • 需要注意的点
      • 待优化点:持续更新中
  • 五、配置CIM
    • CIM的数据结构
  • 六、消息业务还可以使用什么技术
  • 七、总结

二、实现思路

可以使用什么实现?

1、最低效的方法:单纯使用数据库去存储发送的消息,在对方端一直去请求数据库的数据:频繁网络请求和IO请求;不可取!
2、使用websockt建立二者的连接,通过websockt服务器去进行消息的实时发送和接收,下面会详细说明。
3、使用Comet(长轮询):通过HTTP长连接(如Ajax),服务器可以实时向客户端推送消息,客户端再将消息显示出来。

使用CIM+websockt实现的优点是什么?

CIM是什么?

CIM是一套完善的消息推送框架,可应用于信令推送,即时聊天,移动设备指令推送等领域。开发者可沉浸于业务开发,不用关心消息通道链接,消息编解码协议等繁杂处理。CIM仅提供了消息推送核心功能,和各个客户端的集成示例,并无任何业务功能,需要使用者自行在此基础上做自己的业务
CIM项目的分享:CIM项目分享

业务的实现思路

双人聊天:需要两个人能实时对话并且展示我和对方的头像及消息分布在屏幕两侧;已经有历史消息的需要在一进入页面时就将历史消息进行展示;
消息列表展示:需要及时接收到其他人给我发的消息并且展示的是最新的一条消息。

三、数据库中涉及的表

在这里插入图片描述

四、业务UML图

双人聊天类图+NS图

持久化消息数据到mysql数据库中
在这里插入图片描述
在这里插入图片描述

消息列表展示类图+NS图

在这里插入图片描述
在这里插入图片描述

五、业务代码

后端代码

bootstrap配置文件(配置模块信息、中间件配置信息等)

格式一定要正确

server:port: 6644servlet:context-path: /message
spring:application:name: prosper-messageprofiles:active: localcloud:nacos:config:server-addr: 你的IP地址:8848 #nacos地址namespace: 你的命名空间名称file-extension: yamlextension-configs:- data-id: 你的common模块配置名称(我将数据库等公共性配置抽到了common模块中)refresh: true

nacos配置

#cim接口地址
cimUrl: http://cim.tfjy.tech:9000/api/message/sendAll
cimContactMerchantUrl: http://cim.tfjy.tech:9000/api/message/send

controller层

package com.tfjybj.controller;import com.alibaba.nacos.common.model.core.IResultCode;
import com.tfjybj.entity.MessageEntity;
import com.tfjybj.exception.FrontResult;
import com.tfjybj.exception.codeEnum.ResultCodeEnum;
import com.tfjybj.exception.codeEnum.ResultMsgEnum;
import com.tfjybj.pojo.MessageListPojo;
import com.tfjybj.pojo.SendMessagePojo;
import com.tfjybj.service.ContactMerchantService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.util.List;
import java.util.Objects;@Api(tags = "联系商家")
@RestController
@RequestMapping("/Business")
public class ContactMerchantController {@Autowiredprivate ContactMerchantService contactMerchantService;@ApiOperation(value = "商家发消息")@PostMapping("/contactMerchant")public FrontResult contactMerchant(@RequestBody SendMessagePojo sendMessagePojo){boolean result= contactMerchantService.sendMessage(sendMessagePojo);if (result=true){return FrontResult.build(ResultCodeEnum.SUCCESS.getCode(), ResultMsgEnum.FIND_SUCCESS.getMsg(), result);}return FrontResult.build(ResultCodeEnum.FAIL.getCode(), ResultMsgEnum.FIND_FAIL.getMsg(), null);}/*** @Description:  通过发送者UserId和接受者receiverId按照时间倒序查询聊天室消息* @param: Long userId,Long receiverId* @return: List<MessageEntity>**/@ApiOperation(value = "查询聊天室中的消息")@GetMapping("/getMessageContent")public FrontResult getMessageContent( Long userId, Long receiverId){List<MessageEntity> messageEntities= contactMerchantService.getMessagesByUserIdAndReceiverId(userId,receiverId);if (Objects.isNull(messageEntities)){return FrontResult.build(ResultCodeEnum.FAIL.getCode(), ResultMsgEnum.FIND_FAIL.getMsg(), null);}else {return FrontResult.build(ResultCodeEnum.SUCCESS.getCode(), ResultMsgEnum.FIND_SUCCESS.getMsg(), messageEntities);}}
/** @Description:根据sellerId查询与该买家进行过聊天的所有人的最后一条消息
*/@ApiOperation(value = "根据sellerId查询与该买家进行过聊天的所有人的最后一条消息")@GetMapping("/getMessageListByUserId")public FrontResult getMessageListByUserId( Long userId){List<MessageListPojo> messageEntities= contactMerchantService.getMessageListByUserId(userId);if (Objects.isNull(messageEntities)){return FrontResult.build(ResultCodeEnum.FAIL.getCode(), ResultMsgEnum.FIND_SUCCESS.getMsg(), "暂无数据");}return FrontResult.build(ResultCodeEnum.SUCCESS.getCode(), ResultMsgEnum.FIND_SUCCESS.getMsg(), messageEntities);}}

service接口

package com.tfjybj.service;import com.tfjybj.entity.MessageEntity;
import com.tfjybj.pojo.MessageListPojo;
import com.tfjybj.pojo.SendMessagePojo;
import io.swagger.models.auth.In;import java.util.List;public interface ContactMerchantService {boolean sendMessage(SendMessagePojo sendMessagePojo);List<MessageEntity> getMessagesByUserIdAndReceiverId(Long userId, Long receiverId);List<MessageListPojo> getMessageListByUserId(Long userId);
}

service实现层

package com.tfjybj.service.impl;import com.alibaba.fastjson.JSONObject;
import com.tfjybj.entity.MessageEntity;
import com.tfjybj.mapper.MessageMapper;
import com.tfjybj.pojo.MessageListPojo;
import com.tfjybj.pojo.SendMessagePojo;
import com.tfjybj.pojo.UserShopInfoPojo;
import com.tfjybj.service.ContactMerchantService;
import com.tfjybj.service.UserShopRoleService;
import lombok.extern.log4j.Log4j2;
import net.sf.jsqlparser.expression.LongValue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.tfjybj.utils.*;import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.*;import static com.tfjybj.utils.CommonAttribute.ZERO_INT;@Log4j2
@Service
public class ContactMerchantImpl implements ContactMerchantService {@Autowiredprivate RestTemplate restTemplate;@Resourceprivate MessageMapper messageMapper;@Resourceprivate UserShopRoleService userShopRoleService;@Value("${cimContactMerchantUrl}")private String cimContactMerchantUrl;/*** @description: 联系商家,发消息**/@Overridepublic boolean sendMessage(SendMessagePojo sendMessagePojo) {try{if(Objects.isNull(sendMessagePojo)) {log.error("异常,原因是:在聊天室发消息功能中的sendMessage()中参数有null值");return false;}else {String action=sendMessagePojo.getAction();Long receiver=sendMessagePojo.getReceiver();Long sender=sendMessagePojo.getSender();String content=sendMessagePojo.getContent();MessageEntity messageEntity = new MessageEntity();messageEntity.setMessageContent(content);messageEntity.setUserId(sender);messageEntity.setMessageType(action);messageEntity.setReceiverId(receiver);messageEntity.setMessageRecordId((new SnowFlakeGenerateIdWorker(ZERO_INT,ZERO_INT).nextId()));// 添加聊天室信息记录String url = cimContactMerchantUrl + "?action=" + action + "&content=" + content + "&receiver=" + receiver + "&sender=" + sender;String response = restTemplate.postForObject(url, null, String.class);Integer result = messageMapper.insertContactMerchant(messageEntity);if (result>0){return true;}return false;}}catch (Exception e){log.error("异常,原因是:", e);return false;}}/*** @Description:  通过发送者UserId和接受者receiverId按照时间倒序查询聊天室消息* @param: Long userId,Long receiverId* @return: List<MessageEntity>**/@Overridepublic List<MessageEntity> getMessagesByUserIdAndReceiverId(Long userId, Long receiverId) {try{if (null!=userId && null!=receiverId){return messageMapper.selectMessageContent(userId, receiverId);}}catch (Exception e){log.error("异常,原因是:", e);return null;}return null;}//@Description:要根据前端传递的sellerId查询与该卖家进行过聊天的所有人的最后一条消息@Overridepublic List<MessageListPojo> getMessageListByUserId(Long userId){//查询最新一条消息内容,包括receiverId、userId、content、createTImeList<MessageListPojo> messageEntities = messageMapper.selectLatestMessages(userId);Set<Long> setUserId = new HashSet<>(); //声明一个set,放置receiverId和userId,用set集合进行去重for (MessageListPojo messageEntity:messageEntities) { // 遍历查询出来的内容,将每条信息的receiverId和userId放到set集合中setUserId.add(messageEntity.getSenderId());setUserId.add(messageEntity.getReceiverId());}List<Long> userIdList = new ArrayList<>(setUserId);//用所有的userId查询对应的店铺名称、店铺头像和个人姓名List<UserShopInfoPojo> userShopInfoPojos = userShopRoleService.queryMessageContent(userIdList);messageEntities.forEach(messageEntity -> {userShopInfoPojos.stream().filter(userShopInfoPojo -> userShopInfoPojo.getUserId().equals(messageEntity.getSenderId())).findFirst().ifPresent(userShopInfoPojo -> {messageEntity.setSenderShopName(userShopInfoPojo.getShopName());messageEntity.setSenderPicture(userShopInfoPojo.getShopPicture());messageEntity.setSenderName(userShopInfoPojo.getUserName());});userShopInfoPojos.stream().filter(userShopInfoPojo -> userShopInfoPojo.getUserId().equals(messageEntity.getReceiverId())).findFirst().ifPresent(userShopInfoPojo -> {messageEntity.setReceiverShopName(userShopInfoPojo.getShopName());messageEntity.setReceiverPicture(userShopInfoPojo.getShopPicture());messageEntity.setReceiverName(userShopInfoPojo.getUserName());});});return messageEntities;}
}

mapper接口

package com.tfjybj.mapper;import com.baomidou.mybatisplus.mapper.BaseMapper;
import com.tfjybj.entity.MessageEntity;
import com.tfjybj.pojo.MessageListPojo;import java.util.List;public interface MessageMapper extends BaseMapper<MessageEntity> {List<MessageEntity> queryMapMessageByDate(String messageType);Integer insertContactMerchant(MessageEntity messageEntity);List<MessageEntity> selectMessageContent(Long userId,Long receiverId);List<MessageListPojo> selectLatestMessages(Long sellerId);
}

mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tfjybj.mapper.MessageMapper"><resultMap type="com.tfjybj.entity.MessageEntity" id="ProsperMessageRecordMap"><result property="messageRecordId" column="message_record_id" jdbcType="INTEGER"/><result property="userId" column="user_id" jdbcType="INTEGER"/><result property="messageContent" column="message_content" jdbcType="VARCHAR"/><result property="messageType" column="message_type" jdbcType="VARCHAR"/><result property="createTime" column="create_time" jdbcType="TIMESTAMP"/><result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/><result property="isDelete" column="is_delete" jdbcType="VARCHAR"/><result property="receiverId" column="receiver_id" jdbcType="INTEGER"/></resultMap><!--通过主键修改数据--><update id="update">update prosper_message_record<set><if test="userId != null">user_id = #{userId},</if><if test="messageContent != null and messageContent != ''">message_content = #{messageContent},</if><if test="messageType != null and messageType != ''">message_type = #{messageType},</if><if test="createTime != null">create_time = #{createTime},</if><if test="updateTime != null">update_time = #{updateTime},</if><if test="isDelete != null and isDelete != ''">is_delete = #{isDelete},</if></set>where message_record_id = #{messageRecordId}</update><!--通过主键删除--><delete id="deleteById">delete from prosper_message_record where message_record_id = #{messageRecordId}</delete><insert id="insertContactMerchant">insert into prosper_message_record(message_record_id,user_id, message_content, message_type,receiver_id)values (#{messageRecordId},#{userId}, #{messageContent}, #{messageType},#{receiverId})</insert><!--通过发送者UserId和接受者receiverId按照时间正序查询聊天室消息--><select id="selectMessageContent" resultMap="ProsperMessageRecordMap">SELECT  m.user_id, m.receiver_id,  m.create_time, m.message_contentFROM prosper_message_record mWHERE (m.user_id = #{userId} AND m.receiver_id = #{receiverId}) OR (m.receiver_id = #{userId} AND m.user_id = #{receiverId})AND m.is_delete = 0AND m.message_type = 2ORDER BY m.create_time</select><!--  要根据前端传递的sellerId查询与该卖家进行过聊天的所有人的最后一条消息  --><select id="selectLatestMessages" resultType="com.tfjybj.pojo.MessageListPojo">SELECT m.user_id as senderId, m.receiver_id as receiverId, m.message_content as content, m.create_time as createTimeFROM prosper_message_record mWHERE m.create_time IN (SELECT MAX(create_time)FROM prosper_message_recordWHERE user_id = #{sellerId} OR receiver_id = #{sellerId}GROUP BY CASEWHEN user_id = #{sellerId} THEN receiver_idWHEN receiver_id = #{sellerId} THEN user_idEND)AND message_type = 2 AND is_delete = 0ORDER BY m.create_time DESC</select>
</mapper>

代码具体的编写还是要根据自己的业务来实现,如有对这个需求和展示代码有疑惑的,欢迎各位大佬前来指导交流。

前端代码

代码使用uniapp框架编写的,并且是微信小程序的项目。
下面的代码是vue文件

双人聊天

<template><view class="content"><scroll-view :style="{ height: `${windowHeight - inputHeight}rpx` }" :scroll-top="scrollTop"class="scroll-container" id="scrollview" scroll-y><view id="msglistview" class="chat-body"><!-- 聊天 --><view v-for="(item, idx) in chatList" :key="idx" :value="fileList":class="item.isself ? 'chatself' : 'chatother'"><!-- 如果个人头像不为空且是自己发送的消息,则显示自己个人中心的头像 --><image v-if="personalAvatar != '' && shopid != '' && item.isself" :src="item.sellerPicture"style="width: 80rpx;height: 80rpx;margin-left: 20rpx;"></image><!-- 否则,如果是自己发送的消息,显示默认头像 --><image v-else-if="item.isself" src="/static/user2.png"style="width: 80rpx;height: 80rpx;margin-left: 20rpx;"></image><!-- 否则,如果对方头像不为空且不是自己发送的消息,则显示对方个人中心的头像 --><image v-else-if="otherAvatar != '' && shopid != '' && !item.isself" :src="otherAvatar"style="width: 80rpx;height: 80rpx;margin-right: 20rpx;"></image><!-- 否则,如果不是自己发送的信息,则对方显示默认头像 --><image v-else src="/static/user1.png" style="width: 80rpx;height: 80rpx;margin-right: 20rpx;"></image><!-- 根据消息发送者是自己还是对方,应用不同的样式 --><view class="showStyle" :class="item.isself ? 'chatbgvS' : 'chatbgvO'">{{ item.msg }}</view></view></view></scroll-view><!-- input --><!-- <view class="chatinput">发送的图片按钮<image src="@/static/image.png" style="width:50rpx;height:50rpx;margin: 0rpx 20rpx;"></image><uni-easyinput class="inputtext" autoHeight v-model="contentValue" placeholder="请输入内容"></uni-easyinput>发送表情包的图片按钮 <image src="@/static/smile.png" style="width:50rpx;height:50rpx;margin:0rpx 20rpx;"></image></view> --><view class="chat-bottom" :style="{ height: `${inputHeight}rpx` }"><view class="send-msg" :style="{ bottom: `${keyboardHeight}rpx` }"><view class="uni-textarea"><textarea v-model="contentValue" maxlength="255" confirm-type="send" @confirm="sendMsg()":show-confirm-bar="false" :adjust-position="false" @linechange="sendHeight" @focus="focus"@blur="blur" auto-height></textarea></view><button @click="sendMsg()" class="send-btn">发送</button></view></view></view>
</template><script>
import { querySelInfoBySelIdBySelAliId, selectSellerShopInfo } from '@/api/seller/index.js';
import { sendMessage, historicalChatRecords } from "../../../api/message/index.js";
import { generateUUID, getTimeStamp } from "@/api/message/webSocket.js";
import {webSocketUrl
} from '@/request/config.js'
import { ZEROZEROZEROZERO_STRING } from '../../../utils/constant.js';
export default {data() {return {//键盘高度keyboardHeight: 0,//底部消息发送高度bottomHeight: 0,//滚动距离scrollTop: 0,contentValue: "",//聊天内容chatList: [],//商家买家发信息的数据对象data: {action: "2",//聊天室的标识content: "",receiver: "",sender: ""},customerId: "",//买家idoperatorId: "",//卖家idpersonalAvatar: "",//个人头像otherAvatar: "",//对方头像fileList: [], //商家头像};},computed: {windowHeight() {return this.rpxTopx(uni.getSystemInfoSync().windowHeight)},// 键盘弹起来的高度+发送框高度inputHeight() {return this.bottomHeight + this.keyboardHeight}},updated() {//页面更新时调用聊天消息定位到最底部this.scrollToBottom();},//关闭当前页面时断开连接onHide() {uni.closeSocket({success: () => {console.log('WebSocket连接关闭成功!');}})},//当开打页面的时候进行websocket连接onShow() {const sellerId = uni.getStorageSync("sellerId");var socketTask = uni.connectSocket({url: webSocketUrl, //仅为示例,并非真实接口地址。success: () => { }});//相当于进行cim的登录socketTask.onOpen(function (res) {//从本地获取sellerIdconst content = {"key": "client_bind","timestamp": getTimeStamp(),"data": {"uid": sellerId,"appVersion": "1.0.0","channel": "web","packageName": "com.farsunset.cim","deviceId": generateUUID(),"deviceName": "Chrome"}}let data = {};data.type = 3;data.content = JSON.stringify(content);socketTask.send({data: JSON.stringify(data),success: () => {console.log('发送消息成功!');},complete: () => {console.log('发送消息完成!');}});});//接收消息socketTask.onMessage(async (message) => {const object = JSON.parse(message.data);if (object.type == 1) {console.log("给服务端发送PONG");//给服务端发送ponglet pongData = {};pongData.type = 1;pongData.content = "PONG";socketTask.send({data: JSON.stringify(pongData),success: () => {console.log('PONG消息成功!');},});return;}//获取对方的消息内容if (JSON.parse(object.content).content != undefined) {//如果自己给自己发消息,消息页面左边部分不显示内容if (this.operatorId != this.customerId) {const newMsgReceiver = {isself: false,msg: JSON.parse(object.content).content}this.chatList.push(newMsgReceiver);} else {// 更新头像渲染await this.getOtherAvatar();}}});socketTask.onError((res) => {console.log('WebSocket连接打开失败,请检查!');});},onLoad(options) {this.customerId = options.customId;this.operatorId = options.operatorId;this.queryHistoricalChatRecords();//查询历史聊天记录uni.offKeyboardHeightChange()//用UniApp的uni.onKeyboardHeightChange方法来监听键盘高度的变化,并在键盘高度变化时执行相应的逻辑。uni.onKeyboardHeightChange(res => {this.keyboardHeight = this.rpxTopx(res.height - 30)if (this.keyboardHeight < 0) this.keyboardHeight = 0;})},mounted() {this.getShopIdByUserId();//获取自己的个人中心头像this.getPersonalAvatar();//获取对方的个人中心头像this.getOtherAvatar();},methods: {focus() {this.scrollToBottom()},blur() {this.scrollToBottom()},// 监视聊天发送栏高度sendHeight() {setTimeout(() => {let query = uni.createSelectorQuery();query.select('.send-msg').boundingClientRect()query.exec(res => {this.bottomHeight = this.rpxTopx(res[0].height)})}, 10)},// px转换成rpxrpxTopx(px) {let deviceWidth = wx.getSystemInfoSync().windowWidthlet rpx = (750 / deviceWidth) * Number(px)return Math.floor(rpx)},// 滚动至聊天底部scrollToBottom(e) {setTimeout(() => {let query = uni.createSelectorQuery().in(this);query.select('#scrollview').boundingClientRect();query.select('#msglistview').boundingClientRect();query.exec((res) => {if (res[1].height > res[0].height) {this.scrollTop = this.rpxTopx(res[1].height - res[0].height)}})}, 15)},//发送消息async sendMsg() {if (uni.getStorageSync("sellerId") == this.operatorId) {this.data.receiver = this.customerIdthis.data.sender = this.operatorId} else {this.data.receiver = this.operatorIdthis.data.sender = this.customerId}if (this.data.receiver == this.data.sender) {}this.data.content = this.contentValue.trim(); //去除首尾空格const regex = /^[\s\n]*$/; // 匹配不包含空格和回车的文本if (regex.test(this.data.content)) {uni.showToast({title: '请输入有效文本',icon: 'none'});} else {// 进行提交操作await sendMessage(this.data);const newMsgSend = {isself: true,msg: this.contentValue}this.chatList.push(newMsgSend)this.contentValue = ""// 更新头像渲染await this.getPersonalAvatar();}},//查询历史聊天记录async queryHistoricalChatRecords() {const { code, data } = await historicalChatRecords(this.customerId, this.operatorId)for (let i = 0; i < data.length; i++) {if (data[i].userId == uni.getStorageSync("sellerId")) {const myChat = {isself: true,msg: data[i].messageContent,}this.chatList.push(myChat)} else {const otherChat = {isself: false,msg: data[i].messageContent,}this.chatList.push(otherChat)}}},//获取用户的shopIdasync getShopIdByUserId() {this.userId = JSON.parse(uni.getStorageSync('sellerId'))const { code, data } = await selectSellerShopInfo(this.userId)if (ZEROZEROZEROZERO_STRING == code && this.userId != null) {this.shopid = data[0];uni.setStorageSync('shopId', this.shopid)this.getPersonalAvatar();}else {this.chatList.forEach(item => {item.sellerPicture = item.isself ? "/static/user2.png" : "/static/user1.png";});}},//获取对方的个人中心头像async getOtherAvatar() {const { code, data } = await querySelInfoBySelIdBySelAliId(this.operatorId);this.otherAvatar = data.sellerPicture;},//获取个人中心的头像async getPersonalAvatar() {this.sellerId = uni.getStorageSync('sellerId')const { code, data } = await querySelInfoBySelIdBySelAliId(this.customerId);this.personalAvatar = data.sellerPictureif (ZEROZEROZEROZERO_STRING == code) {//将data对象中的sellerPicture属性值添加到this.fileList数组中this.fileList.push({ url: data.sellerPicture });// 遍历this.chatList数组中的每个item对象this.chatList.forEach(item => {// 如果item对象的isself属性为true,并且this.sellerId等于this.customerIdif (item.isself && this.sellerId == this.customerId) {// 将this.personalAvatar赋给item对象的sellerPicture属性,显示自己个人中心的头像item.sellerPicture = this.personalAvatar;} else {// 将this.otherAvatar赋给item对象的sellerPicture属性,显示对方个人中心的头像item.sellerPicture = this.otherAvatar;}});}}}
}
</script><style lang="scss" scoped>
$sendBtnbgc: #4F7DF5;
$chatContentbgc: #C2DCFF;.showStyle{flex-wrap: wrap;display:flex
}
.scroll-container {::-webkit-scrollbar {display: none;width: 0 !important;height: 0 !important;-webkit-appearance: none;background: transparent;color: transparent;}
}.uni-textarea {padding-bottom: 70rpx;textarea {width: 537rpx;min-height: 75rpx;max-height: 500rpx;background: #FFFFFF;border-radius: 8rpx;font-size: 32rpx;font-family: PingFang SC;color: #333333;line-height: 43rpx;padding: 5rpx 8rpx;}
}.send-btn {display: flex;align-items: center;justify-content: center;margin-bottom: 70rpx;margin-left: 25rpx;width: 128rpx;height: 75rpx;background: $sendBtnbgc;border-radius: 8rpx;font-size: 28rpx;font-family: PingFang SC;font-weight: 500;color: #FFFFFF;line-height: 28rpx;
}.chat-bottom {width: 100%;height: 177rpx;background: #F4F5F7;transition: all 0.1s ease;
}.send-msg {display: flex;align-items: flex-end;padding: 16rpx 30rpx;width: 100%;min-height: 177rpx;position: fixed;bottom: 0;background: #EDEDED;transition: all 0.1s ease;
}.content {height: 100%;position: fixed;width: 100%;height: 100%;// background-color: #0F0F27;overflow: scroll;word-break: break-all;.chat-body {display: flex;flex-direction: column;padding-top: 23rpx;.self {justify-content: flex-end;}.item {display: flex;padding: 23rpx 30rpx;}}.chatself {display: flex;flex-direction: row-reverse;// align-items: center;// height: 120rpx;width: 90%;margin-left: 5%;// background-color: #007AFF;margin-top: 20rpx;margin-bottom: 10rpx;}.chatother {display: flex;// align-items: center;// height: 120rpx;width: 90%;margin-left: 5%;// background-color: #fc02ff;margin-top: 20rpx;margin-bottom: 10rpx;}.chatbgvS {color: #000000;padding: 20rpx 40rpx;max-width: calc(90% - 140rpx);background-color: $chatContentbgc;font-size: 27rpx;border-radius: 5px;}.chatbgvO {color: #000000;padding: 20rpx 40rpx;max-width: calc(90% - 140rpx);background-color: #FFFFFF;font-size: 27rpx;border-radius: 5px;}.send {color: golenrod;font-size: 12px;margin-right: 5px;}.chatinput {position: fixed;bottom: 0rpx;height: 70px;width: 100%;background-color: #ffffff;display: flex;// justify-content: space-between;align-items: center;.inputtext {width: calc(100% - 80rpx - 50rpx - 38rpx);color: #FFFFFF;font-size: 28rpx;}}
}
</style>

聊天列表

<template><view><uni-list><uni-list :border="true"><uni-list-chat  class="style" v-for="item in messageList" :key="item.createTime" :title="item.senderId === currentUser? item.receiverShopName: item.senderShopName" :avatar="item.senderId === currentUser? item.receiverPicture: item.senderPicture " :note="truncateText(item.content.replace(/\n/g, '\u00a0'))" :time="item.createTime" :badge-position="item.countNoread > 0 ? 'left' : 'none'" link@click="gotoChat(item.senderId === currentUser ? item.receiverId : item.senderId)"/></uni-list></uni-list><footer><view class="none"> <text>没有更多数据了</text></view></footer></view>
</template>
<script>
import {generateUUID,getTimeStamp
} from "@/api/message/webSocket.js";
import {webSocketUrl
} from '@/request/config.js'
import {queryMessageList
} from '@/api/message/index.js'
import {ZEROZEROZEROZERO_STRING,ZERO_INT,ONEONEONEONE_STRING
} from '../../../utils/constant';
export default {data() {return {//消息列表messageList: [],currentUser: ""};},//关闭当前页面时断开连接onHide() {uni.closeSocket({success: () => {console.log('WebSocket连接关闭成功!');}})},//当开打页面的时候进行websocket连接onShow() {const sellerId = uni.getStorageSync("sellerId");var socketTask = uni.connectSocket({url: webSocketUrl, //仅为示例,并非真实接口地址。success: () => { }});//相当于进行cim的登录socketTask.onOpen(function (res) {//从本地获取sellerIdconst content = {"key": "client_bind","timestamp": getTimeStamp(),"data": {"uid": sellerId,"appVersion": "1.0.0","channel": "web","packageName": "com.farsunset.cim","deviceId": generateUUID(),"deviceName": "Chrome"}}let data = {};data.type = 3;data.content = JSON.stringify(content);socketTask.send({data: JSON.stringify(data),success: () => {console.log('发送消息成功!');},complete: () => {console.log('发送消息完成!');}});});//接收消息socketTask.onMessage((message) => {const object = JSON.parse(message.data);if (object.type == 1) {console.log("给服务端发送PONG");//给服务端发送ponglet pongData = {};pongData.type = 1;pongData.content = "PONG";socketTask.send({data: JSON.stringify(pongData),success: () => {console.log('PONG消息成功!');},});return;}console.log("这个是object.content", object, JSON.parse(object.content))//获取对方的消息内容,如果不为空则替换最新的显示消息if (JSON.parse(object.content).content != undefined) {//获取用户idconst userId = JSON.parse(object.content).sender;//获取消息内容const lastMessage = JSON.parse(object.content).content;//根据消息中的id遍历消息集合中的id更新消息this.messageList.forEach(item => {if ((item.senderId === this.currentUser ? item.receiverId : item.senderId)  == userId) {item.content = lastMessage;}})}});socketTask.onError((res) => {console.log('WebSocket连接打开失败,请检查!');});},onLoad() {this.currentUser = uni.getStorageSync("sellerId");},onShow() {this.queryMessageLists();},methods: {truncateText(text) {const maxLength = 20; // 设置最大字符长度if (text.length > maxLength) {return text.substring(0, maxLength) + '...'; // 超过最大长度时截断并添加省略号} else {return text;}},gochat() {uni.navigateTo({url: "../chat/chat",});},formatTime(timestamp) {const date = new Date(timestamp * 1000);const year = date.getFullYear();const month = String(date.getMonth() + 1).padStart(2, "0");const day = String(date.getDate()).padStart(2, "0");const hours = String(date.getHours()).padStart(2, "0");const minutes = String(date.getMinutes()).padStart(2, "0");return `${year}-${month}-${day} ${hours}:${minutes}`;},gotoChat(operatorId) {uni.navigateTo({url: '/pages/views/message/Chat?customId=' + this.currentUser + "&operatorId=" + operatorId,})},//查询聊天列表async queryMessageLists() {const {code,data} = await queryMessageList(this.currentUser);if (ZEROZEROZEROZERO_STRING == code) {this.messageList = data;}}},
};
</script>
<style lang="less" scoped>
.style{white-space: nowrap;  overflow: hidden;  text-overflow: ellipsis;
}
.chat-custom-right {flex: 1;/* #ifndef APP-NVUE */display: flex;/* #endif */flex-direction: column;justify-content: space-between;align-items: flex-end;
}.chat-custom-text {font-size: 12px;color: #999;
}page {background-color: #f1f1f1;
}footer {height: 140rpx;width: 100%;.none,.yszy {width: 100%;height: 70rpx;line-height: 70rpx;text-align: center;}.none {font-size: 26rpx;font-weight: 900;text {font-weight: 500;color: #777;padding: 10rpx;}}.yszy {font-size: 26rpx;color: #777;}
}
</style>

配置文件(JS后缀)

以下是使用到的一些公共性配置文件
WebSockt.js

//生成UUID
export function generateUUID() {let d = new Date().getTime();let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {let r = (d + Math.random() * 16) % 16 | 0;d = Math.floor(d / 16);return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);});return uuid.replace(/-/g, '');
}//获取时间戳
export function getTimeStamp() {return new Date().getTime();
}//字符串转Uint8Array
function toUint8Arr(str) {const buffer = [];for (let i of str) {const _code = i.charCodeAt(0);if (_code < 0x80) {buffer.push(_code);} else if (_code < 0x800) {buffer.push(0xc0 + (_code >> 6));buffer.push(0x80 + (_code & 0x3f));} else if (_code < 0x10000) {buffer.push(0xe0 + (_code >> 12));buffer.push(0x80 + (_code >> 6 & 0x3f));buffer.push(0x80 + (_code & 0x3f));}}return Uint8Array.from(buffer);
}

cim服务器路径

const webSocketUrl = '这里写你服务器的路径';export {webSocketUrl};

封装的request请求文件:复用(其中还增加了微信小程序的日志功能)

import Log from '../utils/Log.js'
import moment from 'moment'
import {ONEONEONEONE_STRING} from "@/utils/constant.js";const request = (config) => {// 拼接完整的接口路径,这里是在package.json里做的环境区分config.url = process.env.VUE_APP_BASE_URL+config.url;//判断是都携带参数if(!config.data){config.data = {};}config.header= {'Authorization': uni.getStorageSync("authToken"),// 'content-type': 'application/x-www-form-urlencoded'}let promise = new Promise(function(resolve, reject) {uni.request(config).then(responses => {// 异常if (responses[0]) {reject({message : "网络超时"});} else {let response = responses[1].data; // 如果返回的结果是data.data的,嫌麻烦可以用这个,return res,这样只返回一个dataresolve(response);}if(ONEONEONEONE_STRING == responses[1].data.code){//在微信提供的we分析上打印实时日志  https://wedata.weixin.qq.com/mp2/realtime-log/mini?source=25Log.error(config.url,"接口访问失败,请排查此问题")}}).catch(error => {reject(error);})})return promise;
};export default request;

用到的调用后端的api接口


import request from '@/request/request.js'; // 引入封装好的requestexport function delay(ms){return new Promise(resolve => setTimeout(resolve, ms));
}//休眠函数
export function sleep(delay) {var start = (new Date()).getTime();while((new Date()).getTime() - start < delay) {continue;}
}/*** 查询聊天列表* @param {Object} userId 用户id*/export function queryMessageList(userId) {return request({method: "get", // 请求方式url: '/message/Business/getMessageListByUserId?userId=' + userId})
}
//联系商家发消息
export function sendMessage(data) {return request({url: "/message/Business/contactMerchant",method: "POST",data})
}
//查询历史聊天记录
export function historicalChatRecords(receiverId,userId) {return request({url:"/message/Business/getMessageContent?receiverId="+receiverId+"&userId="+userId,method: "GET"})
}

constant.js(封装的常量类):复用

/** @Descripttion: 统一管理常量* @version: 1.0/*** 数字*/export const ZERO_INT=0;export const ONE_INT=1;export const TWO_INT=2;export const THREE_INT=3;export const FOUR_INT=4;export const FIVE_INT=5;/*** 字符串*/
export const ZERO_STRING="0";
export const ONE_STRING="1";
export const TWO_STRING="2";
export const THREE_STRING="3";
export const FOUR_STRING="4";
export const FIVE_STRING="5";
export const NULL_STRING="null";
export const ZEROZEROZEROZERO_STRING="0000";  //后端请求返回码——执行成功
export const ONEONEONEONE_STRING="1111";  //后端请求返回码——执行失败

需要注意的点

cim服务器的路径需要时ws开头,和http类似;如果是微信小程序上必须是wss(安全协议)开头,和https类似(微信小程序要求!)

待优化点:持续更新中

我们可以看到,这两个功能中的前端代码里均有去进行websockt连接和cim登录等相同的代码,所以这里要抽出一个公共性的js文件进行复用

五、配置CIM

在gitee上将文件拉下来
https://gitee.com/farsunset/cim
部署在服务器上,就是一个启动jar包的命令;
如果有需要可以找博主要一份jar包开机自启的配置。

CIM的数据结构

字段类型说明
idlong唯一ID
senderString消息发送者ID
receiverString消息接收者ID
actionString消息动作、类型
titleString消息标题
contentString消息正文
formatString消息格式,例如聊天场景可用于文字、图片
extraString业务扩展数据字段
timestamplong消息13位时间戳

六、消息业务还可以使用什么技术

除了常见的数据库存储外,消息业务还可以使用一些消息队列(Message Queue,MQ)技术实现。MQ技术可以解耦消息发送者和接收者之间的关系,提高系统的可伸缩性和可扩展性,保证消息的可靠性和时效性,更好地支持分布式系统的消息传递。常见的MQ技术有RabbitMQ、Kafka、RocketMQ等。

七、总结

本文介绍了通过CIM和WebSocket技术实现实时消息通信的方法,实现了双人聊天和消息列表展示的功能。在介绍实现方法之前,先介绍了CIM和WebSocket的概念和优势。接下来,详细介绍了如何使用CIM和WebSocket实现双人聊天和消息列表展示的功能。其中,双人聊天主要包括前端页面的设计和后端代码的实现,通过WebSocket实现实时消息的推送和接收。消息列表展示主要是展示聊天记录和消息通知,通过数据库存储聊天记录和实时推送消息通知。最后,针对文章介绍的功能和实现方法,给出了一些优化和改进的建议,以及其他常见的消息技术的介绍。总体来说,本文介绍了一种简单易懂、实用可行的实时消息通信方案,对于需要实现实时消息传递的应用场景具有一定参考价值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/119816.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CSS中如何实现文字渐变色效果(Text Gradient Color)?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 文字渐变色效果&#xff08;Text Gradient Color&#xff09;⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入门之旅&#xff01;这…

直接插入排序与希尔排序

目录 一&#xff0c;排序的概念 二&#xff0c;插入排序 2.1直接插入排序 2.2 希尔排序 一&#xff0c;排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些或某些关键字的大小&#xff0c;递增或递减的排列 稳定性&#xff…

164到网络安全面试大全(附答案)

最近有不少小伙伴跑来咨询&#xff1a; 想找网络安全工作&#xff0c;应该要怎么进行技术面试准备&#xff1f;工作不到 2 年&#xff0c;想跳槽看下机会&#xff0c;有没有相关的面试题呢&#xff1f; 为了更好地帮助大家高薪就业&#xff0c;今天就给大家分享两份网络安全工…

Mysql批量插入大量数据的方法

使用存储过程进行插入&#xff0c; 在navicate中示例如下&#xff1a; 输入需要的参数点击完成 在begin end中输入代码&#xff0c;示例代码如下 CREATE DEFINERskip-grants userskip-grants host PROCEDURE batch_insert() BEGINdeclare i int default 0; set i0;while i<1…

银行测试能不能长期做下去呢?

银行测试是一个相对稳定的领域&#xff0c;因为银行作为金融机构必须遵守法律法规&#xff0c;要求其业务的安全性、可靠性和稳定性等方面都需要不断地测试和验证。因此从长远来看&#xff0c;银行测试有着相对较好的就业前景。 当然&#xff0c;随着技术的发展和变化&#xf…

数学建模:拟合算法

&#x1f506; 文章首发于我的个人博客&#xff1a;欢迎大佬们来逛逛 数学建模&#xff1a;拟合算法 文章目录 数学建模&#xff1a;拟合算法拟合算法多项式拟合非线性拟合cftool工具箱的使用 拟合算法 根据1到12点间的温度数据求出温度与时间之间的近似函数关系 F ( t ) F(…

华为云云服务器评测|前端开发同学的初体验部署贪吃蛇!

文章目录 前言初配置初始化宝塔面板安装Nginx、上传项目修改nginx配置效果展示 前言 作为一名前端同学&#xff0c;我的技能和日常工作主要集中在用户界面的设计和交互上&#xff0c;与服务器产品相关的经验相对较少。正好看到了咱们华为云开展的评测活动&#xff0c;决定借着…

论文解读 | OmniObject3D:用于逼真感知、重建和生成的大词汇量3D对象数据集

原创 | 文 BFT机器人 这篇论文的主要目标是介绍和探索OmniObject3D数据集&#xff0c;该数据集包含大量真实扫描的3D物体&#xff0c;涵盖了190个类别&#xff0c;提供了多种丰富的注释&#xff0c;包括纹理3D网格、采样点云、多视图图像等。作者将OmniObject3D应用于多个3D视…

医疗小程序:让服务更高效,用户体验更优化

随着移动互联网的快速发展&#xff0c;小程序已经成为了一个热门的开发方向。医疗健康类小程序也不例外&#xff0c;拥有广泛的市场需求和前景。本文将为你提供一份完整的医疗健康类小程序开发攻略&#xff0c;帮助你快速开发上线一个专业成熟的小程序商城。 一、选择合适的小程…

LTE ATTACH流程、PDN流程、PGW地址分配介绍

1、S-GW\P-GW选择 MME根据S-GW和P-GW的拓扑信息进行S-GW/P-GW的选择&#xff0c;在S-GW的候选序列和P-GW的候选序列中比较&#xff0c;寻找是否有合一的S-GW/P-GW&#xff0c;并且根据S-GW的优先级和权重信息进行排序&#xff0c;得到S-GW/P-GW的候选组。 2、SGW>PGW连接 PD…

c++11 标准模板(STL)(std::basic_stringstream)(四)

定义于头文件 <sstream> template< class CharT, class Traits std::char_traits<CharT> > class basic_stringstream;(C11 前)template< class CharT, class Traits std::char_traits<CharT>, class Allocator std::alloc…

el-table动态生成多级表头的表格(js + ts)

展示形式&#xff1a; 详细代码&#xff1a; &#xff08;js&#xff09; <template><div><el-table :data"tableData" style"width: 100%"><el-table-column label"题目信息" align"center"><el-table-…

无涯教程-Android - ToggleButton函数

ToggleButton将已选中/未选中状态显示为按钮。它基本上是一个带有指示灯的开/关按钮。 Toggle Button ToggleButton属性 以下是与ToggleButton控件相关的重要属性。您可以查看Android官方文档以获取属性的完整列表以及可以在运行时更改这些属性的相关方法。 Sr.No.Attribute…

Linux centos7 bash编程(break和continue)

在学习shell知识时&#xff0c;简单编程要从格式入手。 首先学习好单行注释和多行注释。 先学习简单整数的打印输出&#xff0c;主要学习echo命令&#xff0c;学习选项-e -n的使用。 下面的练习是常用的两个分支跳转程序&#xff1a;break和continue。 #!/bin/bash # 这是单…

说说Kappa架构

分析&回答 对于实时数仓而言&#xff0c;Lmabda架构有很明显的不足&#xff0c;首先同时维护两套系统&#xff0c;资源占用率高&#xff0c;其次这两套系统的数据处理逻辑相同&#xff0c;代码重复开发。 能否有一种架构&#xff0c;只需要维护一套系统&#xff0c;就可以…

文心一言 VS 讯飞星火 VS chatgpt (86)-- 算法导论8.2 3题

三、用go语言&#xff0c;假设我们在 COUNTING-SORT的第 10行循环的开始部分&#xff0c;将代码改写为: 10 for j 1 to A.length 试证明该算法仍然是正确的。它还稳定吗? 文心一言&#xff1a; 首先&#xff0c;让我们先明确 COUNTING-SORT 算法的基本思想。COUNTING-SOR…

Nginx安装与部署

文章目录 一,说明二,下载三,Windows下安装1,安装2,启动3,验证 四,Linux下安装1,安装2,启动3,验证 五,Nginx配置 一,说明 Nginx是一款高性能Web和反向代理服务器,提供内存少,高并发,负载均衡和反向代理服务,支持windos和linux系统 二,下载 打开浏览器,输入地址: https://ngin…

Web Components详解-Shadow DOM基础

目录 引言 概念 基本用法 attachShadow函数 mode&#xff08;模式&#xff09; delegatesFocus&#xff08;委托聚焦&#xff09; Custom ElementsShadow DOM 基本用法 样式及属性隔离 写在最后 相关代码 参考文章 引言 上篇文章的自定义标签中&#xff0c;我们使…

idea使用maven时的java.lang.IllegalArgumentException: Malformed \uxxxx encoding问题解决

idea使用maven时的java.lang.IllegalArgumentException: Malformed \uxxxx encoding问题解决 欢迎使用Markdown编辑器1、使用maven clean install -X会提示报错日志2、在Poperties.java文件的这一行打上断点3、maven debug进行调试4、运行到断点位置后&#xff0c;查看报错char…

超详细!80个Python入门实例,代码清晰拿来即用,学习提升必备

对于大部分Python学习者来说&#xff0c;核心知识基本已经掌握了&#xff0c;但"纸上得来终觉浅,绝知此事要躬行"&#xff0c;要想完全掌握Python&#xff0c;还得靠实践应用。 今天给大家分享80个Python入门实例&#xff0c;都是基础实例&#xff0c;经典实用&…