广告设计制作公司网站,免费建站哪家性价比高,无锡做网站公司哪家好电话,注册海外公司欢迎大佬的来访#xff0c;给大佬奉茶 一、文章背景
有一个业务需求是#xff1a;实现一个聊天室#xff0c;我和对方可以聊天#xff1b;以及有一个消息列表展示我和对方#xff08;多个人#xff09;的聊天信息和及时接收到对方发来的消息并展示在列表上。 项目框架概…欢迎大佬的来访给大佬奉茶 一、文章背景
有一个业务需求是实现一个聊天室我和对方可以聊天以及有一个消息列表展示我和对方多个人的聊天信息和及时接收到对方发来的消息并展示在列表上。 项目框架概述后端使用SpringCloud Alibabamybatis-plus前端是uniapp框架的微信小程序。 文章目录 欢迎大佬的来访给大佬奉茶 一、文章背景二、实现思路可以使用什么实现使用CIMwebsockt实现的优点是什么CIM是什么 业务的实现思路 三、数据库中涉及的表四、业务UML图双人聊天类图NS图消息列表展示类图NS图 五、业务代码后端代码bootstrap配置文件配置模块信息、中间件配置信息等nacos配置controller层service接口service实现层mapper接口mapper.xml 前端代码双人聊天聊天列表配置文件JS后缀需要注意的点待优化点持续更新中 五、配置CIMCIM的数据结构 六、消息业务还可以使用什么技术七、总结 二、实现思路
可以使用什么实现
1、最低效的方法单纯使用数据库去存储发送的消息在对方端一直去请求数据库的数据频繁网络请求和IO请求不可取 2、使用websockt建立二者的连接通过websockt服务器去进行消息的实时发送和接收下面会详细说明。 3、使用Comet长轮询通过HTTP长连接如Ajax服务器可以实时向客户端推送消息客户端再将消息显示出来。
使用CIMwebsockt实现的优点是什么
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: truenacos配置
#cim接口地址
cimUrl: http://cim.tfjy.tech:9000/api/message/sendAll
cimContactMerchantUrl: http://cim.tfjy.tech:9000/api/message/sendcontroller层
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 (resulttrue){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: ListMessageEntity**/ApiOperation(value 查询聊天室中的消息)GetMapping(/getMessageContent)public FrontResult getMessageContent( Long userId, Long receiverId){ListMessageEntity 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){ListMessageListPojo 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);ListMessageEntity getMessagesByUserIdAndReceiverId(Long userId, Long receiverId);ListMessageListPojo 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 actionsendMessagePojo.getAction();Long receiversendMessagePojo.getReceiver();Long sendersendMessagePojo.getSender();String contentsendMessagePojo.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 (result0){return true;}return false;}}catch (Exception e){log.error(异常原因是, e);return false;}}/*** Description: 通过发送者UserId和接受者receiverId按照时间倒序查询聊天室消息* param: Long userId,Long receiverId* return: ListMessageEntity**/Overridepublic ListMessageEntity 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 ListMessageListPojo getMessageListByUserId(Long userId){//查询最新一条消息内容包括receiverId、userId、content、createTImeListMessageListPojo messageEntities messageMapper.selectLatestMessages(userId);SetLong setUserId new HashSet(); //声明一个set放置receiverId和userId用set集合进行去重for (MessageListPojo messageEntity:messageEntities) { // 遍历查询出来的内容将每条信息的receiverId和userId放到set集合中setUserId.add(messageEntity.getSenderId());setUserId.add(messageEntity.getReceiverId());}ListLong userIdList new ArrayList(setUserId);//用所有的userId查询对应的店铺名称、店铺头像和个人姓名ListUserShopInfoPojo 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 BaseMapperMessageEntity {ListMessageEntity queryMapMessageByDate(String messageType);Integer insertContactMerchant(MessageEntity messageEntity);ListMessageEntity selectMessageContent(Long userId,Long receiverId);ListMessageListPojo selectLatestMessages(Long sellerId);
}
mapper.xml
?xml version1.0 encodingUTF-8?
!DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd
mapper namespacecom.tfjybj.mapper.MessageMapperresultMap typecom.tfjybj.entity.MessageEntity idProsperMessageRecordMapresult propertymessageRecordId columnmessage_record_id jdbcTypeINTEGER/result propertyuserId columnuser_id jdbcTypeINTEGER/result propertymessageContent columnmessage_content jdbcTypeVARCHAR/result propertymessageType columnmessage_type jdbcTypeVARCHAR/result propertycreateTime columncreate_time jdbcTypeTIMESTAMP/result propertyupdateTime columnupdate_time jdbcTypeTIMESTAMP/result propertyisDelete columnis_delete jdbcTypeVARCHAR/result propertyreceiverId columnreceiver_id jdbcTypeINTEGER//resultMap!--通过主键修改数据--update idupdateupdate prosper_message_recordsetif testuserId ! nulluser_id #{userId},/ifif testmessageContent ! null and messageContent ! message_content #{messageContent},/ifif testmessageType ! null and messageType ! message_type #{messageType},/ifif testcreateTime ! nullcreate_time #{createTime},/ifif testupdateTime ! nullupdate_time #{updateTime},/ifif testisDelete ! null and isDelete ! is_delete #{isDelete},/if/setwhere message_record_id #{messageRecordId}/update!--通过主键删除--delete iddeleteByIddelete from prosper_message_record where message_record_id #{messageRecordId}/deleteinsert idinsertContactMerchantinsert 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 idselectMessageContent resultMapProsperMessageRecordMapSELECT 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 idselectLatestMessages resultTypecom.tfjybj.pojo.MessageListPojoSELECT 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文件哈
双人聊天
templateview classcontentscroll-view :style{ height: ${windowHeight - inputHeight}rpx } :scroll-topscrollTopclassscroll-container idscrollview scroll-yview idmsglistview classchat-body!-- 聊天 --view v-for(item, idx) in chatList :keyidx :valuefileList:classitem.isself ? chatself : chatother!-- 如果个人头像不为空且是自己发送的消息则显示自己个人中心的头像 --image v-ifpersonalAvatar ! shopid ! item.isself :srcitem.sellerPicturestylewidth: 80rpx;height: 80rpx;margin-left: 20rpx;/image!-- 否则如果是自己发送的消息显示默认头像 --image v-else-ifitem.isself src/static/user2.pngstylewidth: 80rpx;height: 80rpx;margin-left: 20rpx;/image!-- 否则如果对方头像不为空且不是自己发送的消息则显示对方个人中心的头像 --image v-else-ifotherAvatar ! shopid ! !item.isself :srcotherAvatarstylewidth: 80rpx;height: 80rpx;margin-right: 20rpx;/image!-- 否则如果不是自己发送的信息则对方显示默认头像 --image v-else src/static/user1.png stylewidth: 80rpx;height: 80rpx;margin-right: 20rpx;/image!-- 根据消息发送者是自己还是对方应用不同的样式 --view classshowStyle :classitem.isself ? chatbgvS : chatbgvO{{ item.msg }}/view/view/view/scroll-view!-- input --!-- view classchatinput发送的图片按钮image src/static/image.png stylewidth:50rpx;height:50rpx;margin: 0rpx 20rpx;/imageuni-easyinput classinputtext autoHeight v-modelcontentValue placeholder请输入内容/uni-easyinput发送表情包的图片按钮 image src/static/smile.png stylewidth:50rpx;height:50rpx;margin:0rpx 20rpx;/image/view --view classchat-bottom :style{ height: ${inputHeight}rpx }view classsend-msg :style{ bottom: ${keyboardHeight}rpx }view classuni-textareatextarea v-modelcontentValue maxlength255 confirm-typesend confirmsendMsg():show-confirm-barfalse :adjust-positionfalse linechangesendHeight focusfocusblurblur auto-height/textarea/viewbutton clicksendMsg() classsend-btn发送/button/view/view/view
/templatescript
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;}});}}}
}
/scriptstyle langscss 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
聊天列表
templateviewuni-listuni-list :bordertrueuni-list-chat classstyle v-foritem in messageList :keyitem.createTime :titleitem.senderId currentUser? item.receiverShopName: item.senderShopName :avataritem.senderId currentUser? item.receiverPicture: item.senderPicture :notetruncateText(item.content.replace(/\n/g, \u00a0)) :timeitem.createTime :badge-positionitem.countNoread 0 ? left : none linkclickgotoChat(item.senderId currentUser ? item.receiverId : item.senderId)//uni-list/uni-listfooterview classnone 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 langless 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_URLconfig.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?source25Log.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?receiverIdreceiverIduserIduserId,method: GET})
}constant.js(封装的常量类)复用
/** Descripttion: 统一管理常量* version: 1.0/*** 数字*/export const ZERO_INT0;export const ONE_INT1;export const TWO_INT2;export const THREE_INT3;export const FOUR_INT4;export const FIVE_INT5;/*** 字符串*/
export const ZERO_STRING0;
export const ONE_STRING1;
export const TWO_STRING2;
export const THREE_STRING3;
export const FOUR_STRING4;
export const FIVE_STRING5;
export const NULL_STRINGnull;
export const ZEROZEROZEROZERO_STRING0000; //后端请求返回码——执行成功
export const ONEONEONEONE_STRING1111; //后端请求返回码——执行失败
需要注意的点
cim服务器的路径需要时ws开头和http类似如果是微信小程序上必须是wss安全协议开头和https类似微信小程序要求
待优化点持续更新中
我们可以看到这两个功能中的前端代码里均有去进行websockt连接和cim登录等相同的代码所以这里要抽出一个公共性的js文件进行复用
五、配置CIM
在gitee上将文件拉下来 https://gitee.com/farsunset/cim 部署在服务器上就是一个启动jar包的命令 如果有需要可以找博主要一份jar包开机自启的配置。
CIM的数据结构
字段类型说明idlong唯一IDsenderString消息发送者IDreceiverString消息接收者IDactionString消息动作、类型titleString消息标题contentString消息正文formatString消息格式例如聊天场景可用于文字、图片extraString业务扩展数据字段timestamplong消息13位时间戳
六、消息业务还可以使用什么技术
除了常见的数据库存储外消息业务还可以使用一些消息队列Message QueueMQ技术实现。MQ技术可以解耦消息发送者和接收者之间的关系提高系统的可伸缩性和可扩展性保证消息的可靠性和时效性更好地支持分布式系统的消息传递。常见的MQ技术有RabbitMQ、Kafka、RocketMQ等。
七、总结
本文介绍了通过CIM和WebSocket技术实现实时消息通信的方法实现了双人聊天和消息列表展示的功能。在介绍实现方法之前先介绍了CIM和WebSocket的概念和优势。接下来详细介绍了如何使用CIM和WebSocket实现双人聊天和消息列表展示的功能。其中双人聊天主要包括前端页面的设计和后端代码的实现通过WebSocket实现实时消息的推送和接收。消息列表展示主要是展示聊天记录和消息通知通过数据库存储聊天记录和实时推送消息通知。最后针对文章介绍的功能和实现方法给出了一些优化和改进的建议以及其他常见的消息技术的介绍。总体来说本文介绍了一种简单易懂、实用可行的实时消息通信方案对于需要实现实时消息传递的应用场景具有一定参考价值。