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

news2025/1/4 17:25:41

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

在这里插入图片描述

一、文章背景

有一个业务需求是:实现一个聊天室,我和对方可以聊天;以及有一个消息列表展示我和对方(多个人)的聊天信息和及时接收到对方发来的消息并展示在列表上。
项目框架概述:后端使用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: 6644
  servlet:
    context-path: /message
spring:
  application:
    name: prosper-message
  profiles:
    active: local

  cloud:
    nacos:
      config:
        server-addr: 你的IP地址:8848 #nacos地址
        namespace: 你的命名空间名称
        file-extension: yaml
        extension-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 {
    @Autowired
    private 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 {

    @Autowired
    private RestTemplate restTemplate;

    @Resource
    private MessageMapper messageMapper;
    @Resource
    private UserShopRoleService userShopRoleService;

    @Value("${cimContactMerchantUrl}")
    private String cimContactMerchantUrl;
    /**
     * @description: 联系商家,发消息
     **/
    @Override
    public 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>
     **/
    @Override
    public 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查询与该卖家进行过聊天的所有人的最后一条消息
    @Override
    public List<MessageListPojo> getMessageListByUserId(Long userId){
        //查询最新一条消息内容,包括receiverId、userId、content、createTIme
        List<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_content
        FROM prosper_message_record m
        WHERE (m.user_id = #{userId} AND m.receiver_id = #{receiverId}) OR (m.receiver_id = #{userId} AND m.user_id = #{receiverId})
        AND m.is_delete = 0
        AND m.message_type = 2
        ORDER 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 createTime
        FROM prosper_message_record m
        WHERE m.create_time IN (
            SELECT MAX(create_time)
            FROM prosper_message_record
            WHERE user_id = #{sellerId} OR receiver_id = #{sellerId}
            GROUP BY CASE
                         WHEN user_id = #{sellerId} THEN receiver_id
                         WHEN receiver_id = #{sellerId} THEN user_id
                         END
        )
        AND message_type = 2 AND is_delete = 0
        ORDER 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: "",//买家id
			operatorId: "",//卖家id
			personalAvatar: "",//个人头像
			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) {
			//从本地获取sellerId
			const 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");
				//给服务端发送pong
				let 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转换成rpx
		rpxTopx(px) {
			let deviceWidth = wx.getSystemInfoSync().windowWidth
			let 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.customerId
				this.data.sender = this.operatorId
			} else {
				this.data.receiver = this.operatorId
				this.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)
				}
			}
		},

		//获取用户的shopId
		async 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.sellerPicture

			if (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.customerId
					if (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) {
			//从本地获取sellerId
			const 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");
				//给服务端发送pong
				let 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) {
				//获取用户id
				const 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,这样只返回一个data
				resolve(response);
			}
			if(ONEONEONEONE_STRING == responses[1].data.code){
				//在微信提供的we分析上打印实时日志  https://wedata.weixin.qq.com/mp2/realtime-log/mini?source=25
				Log.error(config.url,"接口访问失败,请排查此问题")
			}
		}).catch(error => {
			reject(error);
		})
	})
	return promise;
};

export default request;

用到的调用后端的api接口


import request from '@/request/request.js'; // 引入封装好的request


export 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.coloradmin.cn/o/970313.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SNP 分享:SAP S/4HANA Cloud 私有云版本及其独特优势

近几年来&#xff0c;SAP一直强调其愿景是帮助客户达成智慧型企业(Intelligent Enterprise)&#xff0c;为此其相关产品也在不断进行快速迭代&#xff0c;其核心就是S4HANA。同时SAP一直强调其要成为一家云计算公司&#xff0c;近些年也一直在推行云优先战略(Cloud First)。因此…

指针(通过指针间接访问内存)

#include <iostream> #include <algorithm> using namespace std; int main() { int a 2;//定义指针 &#xff1a; 数据类型 *指针变量名;int *p &a;cout << &a << " " << p << endl;//使用指针 &#xff1a; 可以通过…

怎么把表情包做成动态?分享一个简单的方法

表情包在我们的日常交流中已经成为了一种非常流行的表达方式&#xff0c;而将表情包做成动态则可以让它更加生动有趣。本文将介绍如何将表情包制作成动态图&#xff0c;以及一些简单的方法和制作注意事项。 制作动态表情包的方法有很多种&#xff0c;以下是其中两种简单易行的方…

SpringCloud面试题大全(Netflix+Alibaba)

SpringCloud面试题大全 ​ Spring cloud 是一个基于 Spring Boot 实现的服务治理工具包&#xff0c;用于微服务架构中管理和协调服务的。Spring Cloud 是一系列框架的有序集合。它利用 Spring Boot 的开发便利性巧妙地简化了分布式系统基础设施的开发&#xff0c;如服务发现注…

华为云云服务器评测 | 3分钟搞懂如何在华为云服务器安装Nginx并配置静态访问页面

文章目录 一、什么是Nginx&#xff1f;二、申请华为云服务器三、使用XShell连接华为云服务器并安装Nginx四、FileZilla连接服务器五、Linux下安装Nginx❇️配置80端口并关闭Linux防火墙✳️测试 六、配置静态html至华为云服务器并访问⚠️在华为服务器新建路径⏰使用Filezilla上…

java+ssm+mysql电费管理系统

项目介绍&#xff1a; 使用javassmmysql开发的用户电费管理系统&#xff0c;系统包含超级管理员&#xff0c;系统管理员、用户角色&#xff0c;功能如下&#xff1a; 超级管理员&#xff1a;管理员管理、用户管理、用电管理&#xff08;用电记录、缴费提醒&#xff09;、电费…

C++多态案例-设计计算器类

1.前置知识点 多态是面向对象的三大特性之一 多态分为两类 静态多态&#xff1a;函数重载和运算符重载都属于静态多态&#xff0c;复用函数名动态多态&#xff1a;派生类和虚函数实现运行时多态 静态多态和动态多态的区别 静态多态的函数地址早绑定-----编译阶段确定函数地…

Navicat Premium 16.2.7 for Mac

Navicat Premium 16是一款功能强大的跨平台数据库管理工具&#xff0c;支持多种数据库类型&#xff0c;如MySQL、MariaDB、Oracle、SQLite、PostgreSQL等等。它提供了丰富的数据库管理功能和工具&#xff0c;可以帮助开发人员和数据库管理员快速地创建、管理和维护数据库。 Nav…

采用第11代Intel®Core处理器的多网口嵌入式边缘计算平台

Intel Core™ 11th i7/i5/i3/Celeron 处理器 及 8GB DDR4 3200Mb/s 内存4 x GbE, 3 x USB 3.2 Gen2, 1 x USB2.0, 1 x HDMI 1.4, 1 x DP 1.4a, 4 x RS232/422/485可选的第二堆栈支持多达2 x iDoor扩展&#xff0c;用于扩展无线连接、工业现场总线或更多I/O紧凑型无风扇设计零电…

1.15 自实现GetProcAddress

在正常情况下&#xff0c;要想使用GetProcAddress函数&#xff0c;需要首先调用LoadLibraryA函数获取到kernel32.dll动态链接库的内存地址&#xff0c;接着在调用GetProcAddress函数时传入模块基址以及模块中函数名即可动态获取到特定函数的内存地址&#xff0c;但在有时这个函…

做答题小程序前期需要准备哪些工作

做一个答题小程序前期需要做哪些准备工作呢&#xff1f; 第一、要有明确的答题活动规则需求&#xff0c;比如是想用个人答题形式、还是pk答题形式&#xff0c;每个模式具体的出题规则和得分规则&#xff0c;这些要计划清楚&#xff0c;让开发答题小程序的公司能够充分理解你的需…

vue3哪个数组方法在vue2上做了升级处理

在 Vue 3 中&#xff0c;v-for 指令的数组更新行为进行了升级处理。在 Vue 2 中&#xff0c;当使用 v-for 渲染数组时&#xff0c;如果对数组进行了以下操作&#xff0c;Vue 无法检测到变化&#xff1a; 直接通过索引修改数组元素&#xff0c;例如 arr[0] newValue修改数组的…

MySQL 存储引擎,你了解几个?

引言 MySQL是一种流行的关系型数据库管理系统&#xff08;RDBMS&#xff09;&#xff0c;它支持多种不同的数据库引擎。数据库引擎是用于存储、管理和检索数据的核心组件&#xff0c;它们直接影响着数据库的性能、可靠性和功能&#xff0c;接下来本文介绍下一些常见的MySQL数据…

华为云云服务器评测 | 从零开始:云耀云服务器L实例的全面使用解析指南

文章目录 一、前言二、云耀云服务器L实例要点介绍2.1 什么是云耀云服务器L实例2.1.1 浅析云耀云服务器L实例 2.2 云耀云服务器L实例的产品定位2.3 云耀云服务器L实例优势2.4 云耀云服务器L实例支持的镜像与应用场景2.5 云耀云服务器L实例与弹性云服务器&#xff08;ECS&#xf…

MySQL基础篇:掌握数据表操作的基础知识

表(table)是一种结构化的文件&#xff0c;可以用来存储特定类型的数据&#xff0c;如&#xff1a;学生信息&#xff0c;课程信息&#xff0c;都可以放到表中。另外表都有特定的名称&#xff0c;而且不能重复。表中具有几个概念&#xff1a;列、行、主键。 列叫做字段(Column),行…

C#,《小白学程序》第十一课:双向链表(Linked-List)其二,链表的插入与删除的方法(函数)与代码

1 文本格式 /// <summary> /// 改进的车站信息类 class /// 增加了 链表 需要的两个属性 Last Next /// </summary> public class StationAdvanced { /// <summary> /// 编号 /// </summary> public int Id { get; set; } 0; ///…

无涯教程-JavaScript - CUBEVALUE函数

描述 CUBEVALUE函数从多维数据集返回一个聚合值。 语法 CUBEVALUE (connection, [member_expression1], [member_expression2], …)争论 Argument描述Required/OptionalconnectionThe name of the connection to the cube. - A text stringRequiredmember_expression 多维表…

ssm+vue网络教学平台源码和论文

ssmvue网络教学平台源码和论文117 开发工具&#xff1a;idea 数据库mysql5.7 数据库链接工具&#xff1a;navcat,小海豚等 技术&#xff1a;ssm 摘 要 社会的进步&#xff0c;教育行业发展迅速&#xff0c;人们对教育越来越重视&#xff0c;在当今网络普及的情况下&#x…

易记笔记-Ubuntu 下【netstat】指令全解

文章目录 - 显示所有网络连接- 只显示监听的网络连接- 只显示TCP连接- 只显示UDP连接- 显示PID&#xff08;进程ID&#xff09;和程序名称- 显示数字格式的IP地址和端口号- 显示路由表- 仅显示IPv4或IPv6连接- 显示多重广播功能群组组员名单- netstat在线帮助- netstat的替代工…

Coles 五个月内推出SAP S/4HANA 财务核心

Coles是澳大利亚领先的零售企业&#xff0c;在全国拥有2,500多家零售店。100多年来&#xff0c;这家超市一直致力于为每周在Coles购物的2100万顾客提供优质、有价值的服务。 从Wesfarmers西农集团分拆之前&#xff0c;Coles抓住机会在其正在进行的数字化转型战略中向前迈进了一…