Bootstrap

SpringBoot+VUE2完成WebSocket聊天(数据入库)

下载依赖

        <!-- websocket -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <!-- MybatisPlus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.0</version>
        </dependency>

数据库sql

CREATE TABLE `interrogation`  (
  `ID` int(0) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `INITIATOR_ID` int(0) NULL DEFAULT NULL COMMENT '发起人的ID()',
  `DOCTOR` int(0) NULL DEFAULT NULL COMMENT '接受人ID',
  `STATUS` int(0) NULL DEFAULT NULL COMMENT '状态(1.开始聊天和2.结束)',
  PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '聊天列表' ROW_FORMAT = Dynamic;


CREATE TABLE `interrogation_chat`  (
  `ID` int(0) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `INTERROGATION_ID` int(0) NULL DEFAULT NULL COMMENT '聊天表的id',
  `CONTENT` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '内容',
  `INITIATOR_ID` int(0) NULL DEFAULT NULL COMMENT '发送人',
  `CREATE_TIME` datetime(0) NULL DEFAULT NULL COMMENT '发送时间',
  PRIMARY KEY (`ID`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 130 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '聊天记录表' ROW_FORMAT = Dynamic;

实体类 

后续业务采用mybatils-plus写的,如果不会使用手写sql也是可以的

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;

import java.util.Date;

/**
 * @description:  聊天记录表
 * @author: 
 * @date: 2024/10/27 9:37
 **/
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class InterrogationChat {
    /**
     * ID
     **/
    @TableId(value = "ID", type = IdType.AUTO)
    private Integer id;

    /**
     * 聊天表的id
     **/
    @TableField("INTERROGATION_ID")
    private Integer interrogationId;

    /**
     * 内容
     **/
    @TableField("CONTENT")
    private String content;

    /**
     * 发送人
     **/
    @TableField("INITIATOR_ID")
    private Integer initiatorId;

    /**
     * 发送时间
     **/
    @TableField("CREATE_TIME")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
}

websocker配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @author 
 * @date 2024/10/23
 * @apiNote
 */

@Configuration
public class WebSocketConfig {

    /**
     * 注入一个ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();

    }

}


import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import javax.websocket.server.ServerEndpointConfig;

/**
 * @author 
 * @date 2024/10/30
 * @apiNote
 */

public class MyEndpointConfigure extends ServerEndpointConfig.Configurator implements ApplicationContextAware {
 private static volatile BeanFactory context;

 @Override
 public <T> T getEndpointInstance(Class<T> clazz) throws InstantiationException
 {
  return context.getBean(clazz);
 }

 @Override
 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
 {
  System.out.println("auto load"+this.hashCode());
  MyEndpointConfigure.context = applicationContext;
 }
}



import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author 
 * @date 2024/10/30
 * @apiNote
 */

@Configuration
public class MyConfigure {

 @Bean
 public MyEndpointConfigure newConfigure()
 {
  return new MyEndpointConfigure();
 }

}



import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.*;

import java.io.Serializable;

/**
 * @description: 聊天列表
 * @author: 
 * @date: 2024/10/27 9:30
 **/
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class Interrogation implements Serializable {
    /**
     * ID
     **/
    @TableId(value = "ID", type = IdType.AUTO)
    private Integer id;

    /**
     * 发起人ID
     **/
    @TableField("INITIATOR_ID")
    private Integer initiatorId;

    /**
     * 收到人ID
     **/
    @TableField("DOCTOR")
    private Integer doctor;

    /**
     * 状态(1.开始聊天和2.结束)
     **/
    @TableField("STATUS")
    private Integer status;


}




websocket业务代码

import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.ruoyi.dai.config.MyEndpointConfigure;
import com.ruoyi.system.RemoteSendService;
import com.ruoyi.system.domain.InterrogationChat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author 
 * @date 2024/10/23
 * @apiNote
 */

@ServerEndpoint(value = "/imserver/{username}", configurator = MyEndpointConfigure.class)
@Component
public class WebSocketServer {

 @Autowired
 private RemoteSendService remoteSendService;

 private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);

 /**
  * 记录当前在线连接数
  */
 public static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();

 /**
  * 连接建立成功调用的方法
  */
 @OnOpen
 public void onOpen(Session session, @PathParam("username") String username) {
  sessionMap.put(username, session);
  log.info("有新用户加入,username={}, 当前在线人数为:{}", username, sessionMap.size());
  JSONObject result = new JSONObject();
  JSONArray array = new JSONArray();
  result.set("users", array);
  for (Object key : sessionMap.keySet()) {
   JSONObject jsonObject = new JSONObject();
   jsonObject.set("username", key);
   array.add(jsonObject);
  }

  sendAllMessage(JSONUtil.toJsonStr(result));
 }

 /**
  * 连接关闭调用的方法
  */
 @OnClose
 public void onClose(Session session, @PathParam("username") String username) {
  sessionMap.remove(username);
  log.info("有一连接关闭,移除username={}的用户session, 当前在线人数为:{}", username, sessionMap.size());
 }

 /**
  * 收到客户端消息后调用的方法
  * 后台收到客户端发送过来的消息
  * onMessage 是一个消息的中转站
  * 接受 浏览器端 socket.send 发送过来的 json数据
  *
  * @param message 客户端发送过来的消息
  */
 @OnMessage
 public void onMessage(String message, Session session, @PathParam("username") String username) {
  log.info("服务端收到用户username={}的消息:{}", username, message);
  JSONObject obj = JSONUtil.parseObj(message);
  String toUsername = obj.getStr("to");
  String text = obj.getStr("content"); // 注意这里修改成了 "content"
  //非初始化消息转发和缓存
  if (!text.equals("初始化连接")){

   Session toSession = sessionMap.get(toUsername);
   if (toSession != null) {
    JSONObject jsonObject = new JSONObject();

    jsonObject.set("id",obj.getLong("id"));
    jsonObject.set("interrogationId",obj.getInt("interrogationId"));
    jsonObject.set("content",text); // 注意这里修改成了 "content"
    jsonObject.set("createTime",obj.getDate("createTime")); // 注意这里修改成了 "createTime"
    jsonObject.set("initiatorId",obj.getInt("initiatorId")); // 注意这里修改成了 "initiatorId"
    this.sendMessage(jsonObject.toString(), toSession);

    // 数据库记录聊天记录
    InterrogationChat interrogationChat = new InterrogationChat();

    interrogationChat.setInterrogationId(obj.getInt("interrogationId"));
    interrogationChat.setContent(text); // 注意这里修改成了 "content"
    interrogationChat.setCreateTime(obj.getDate("createTime")); // 注意这里修改成了 "createTime"
    interrogationChat.setInitiatorId(obj.getInt("initiatorId")); // 注意这里修改成了 "initiatorId"

    remoteSendService.sendMessage(interrogationChat);

    log.info("发送给用户username={},消息:{}", toUsername, jsonObject.toString());
   } else {
    log.info("发送失败,未找到用户username={}的session", toUsername);
   }
  }

 }

 @OnError
 public void onError(Session session, Throwable error) {
  log.error("发生错误");
  error.printStackTrace();
 }

 /**
  * 服务端发送消息给客户端
  */
 private void sendMessage(String message, Session toSession) {
  try {
   log.info("服务端给客户端[{}]发送消息{}", toSession.getId(), message);
   toSession.getBasicRemote().sendText(message);
  } catch (Exception e) {
   log.error("服务端发送消息给客户端失败", e);
  }
 }

 /**
  * 服务端发送消息给所有客户端
  */
 private void sendAllMessage(String message) {
  try {
   for (Session session : sessionMap.values()) {
    log.info("服务端给客户端[{}]发送消息{}", session.getId(), message);
    session.getBasicRemote().sendText(message);
   }
  } catch (Exception e) {
   log.error("服务端发送消息给客户端失败", e);
  }
 }
}

聊天业务代码

controller

@RestController
@RequestMapping("/send")
public class SendController {



    @Resource
    private SendService sendService;

    /**
     * 聊天消息入库
     **/
    @PostMapping("/sendMessage")
    public R<String> sendMessage(@RequestBody InterrogationChat message) {
        return R.ok(
                sendService.sendMessage(message)
        );
    }

    /**
     * 发起一个聊天
     **/
    @PostMapping("/launchInterrogation")
    public R<String> launchInterrogation(@RequestBody Interrogation interrogation ){
        return R.ok(
                sendService.launchInterrogation(interrogation)
        );
    }
}

service

public interface SendService {
    String sendMessage(InterrogationChat message);

    String launchInterrogation(Interrogation interrogation);
}

impl

@Service
public class SendServiceImpl implements SendService {

    @Resource
    private InterrogationChatMapper interrogationChatMapper;

    @Resource
    private InterrogationMapper interrogationMapper;




    @Override
    public String sendMessage(InterrogationChat message) {

        try {
            interrogationChatMapper.insert(message);
            return "发送成功";
        }catch (Exception e){
            e.printStackTrace();
            return "发送失败";
        }
    }

    @Override
    public String launchInterrogation(Interrogation interrogation) {
        // 有没有
        Interrogation ones = interrogationMapper.selectOne(
                new QueryWrapper<Interrogation>()
                        .eq("INITIATOR_ID",interrogation.getInitiatorId())
                        .eq("DOCTOR",interrogation.getDoctor())
                        .eq("STATUS",1));
        // 为空创建
        if (StringUtils.isNull(ones)){
            interrogation.setStatus(1);
            int  insert = interrogationMapper.insert(interrogation);
            return  interrogation.getId().toString();
        }
        // 有就直接返回
        return ones.getId().toString();
    }
}

mapper

@Mapper
public interface InterrogationMapper extends BaseMapper<Interrogation> {
}



@Mapper
public interface InterrogationChatMapper extends BaseMapper<InterrogationChat> {
}

前端VUE代码

index.vue  第一个页面

<template>
  <div>
    <h2>欢迎来到聊天室</h2>
    <div v-for="message in messages" :key="message.id" class="message-container">
      <div v-if="message.initiatorId !== id" class="message-received">
        <span class="message-text">{{ message.content }}</span>
      </div>
      <div v-else class="message-sent">
        <span class="message-text">{{ message.content }}</span>
        <span class="time">{{ message.createTime }}</span>
      </div>
    </div>
    <input type="text" v-model="newMessage" placeholder="输入消息" />
    <button @click="sendMessage">发送</button>
  </div>
</template>

<script>
import { launchInterrogation, historyInterrogation } from '@/api/interrogation/interrogationType';

export default {
  components: {},
  props: {},
  data() {
    return {
      interrogation: {
        id: null,
        initiatorId: 101,
        doctor: 1
      },
      messages: [],
      newMessage: '',
      ws: null,
      id: 1,
      isConnected: false,
      interrogationId: null,
    };
  },
  computed: {},
  watch: {},
  methods: {
    launchInterrogation() {
      launchInterrogation(this.interrogation).then(res => {
        this.interrogationId = res.data;

        historyInterrogation(this.interrogationId).then(res => {
          console.log("查询到的历史记录:", res.data.interrogationChats);
          if (res.data != null) {
            this.messages = res.data.interrogationChats || [];
          }
        });
      });
    },
    connectToWebSocket() {
      const wsUrl = `ws://ip:port/imserver/${this.id}`;
      this.ws = new WebSocket(wsUrl);

      this.ws.onopen = () => {
        console.log('WebSocket 连接已打开');
        this.isConnected = true;
        // 在 WebSocket 连接完全建立后再发送初始化消息
        this.sendMessageIfConnected();
      };

      this.ws.onclose = () => {
        console.log('WebSocket 连接已关闭');
        this.isConnected = false;
      };

      this.ws.onerror = (error) => {
        console.error('WebSocket 错误', error);
        this.isConnected = false;
      };

      this.ws.onmessage = (event) => {
        const message = JSON.parse(event.data);
        if (message.content !== undefined) {
          console.log("获取到的回调数据:", message);
          this.messages.push(message);
        } else {
          console.warn('接收到的数据无效或格式错误:', event.data);
        }
      };
    },
    sendMessage() {
      if (this.newMessage.trim() === '') return;
      const now = new Date();
      const formattedTime = now.toISOString().slice(0, 19).replace('T', ' ');
      const message = {
        interrogationId: this.interrogationId,
        id: Date.now(),
        content: this.newMessage,
        createTime: formattedTime,
        initiatorId: this.id,
        to: '101',
      };
      if (this.isConnected) {
        this.messages.push(message);
        this.ws.send(JSON.stringify(message));
        this.newMessage = '';
      } else {
        console.warn('WebSocket 连接尚未建立,消息未发送');
      }
    },
    sendMessageIfConnected() {
      if (this.isConnected) {
        const initialMessage = {
          interrogationId: this.interrogationId,
          id: Date.now(),
          content: '初始化连接',
          initiatorId: this.id,
          createTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
          to: '101',
        };
        this.ws.send(JSON.stringify(initialMessage));
      }
    }
  },
  created() {
    
    this.connectToWebSocket();
    this.launchInterrogation();
  },
  mounted() {},
  beforeCreate() {},
  beforeMount() {},
  beforeUpdate() {},
  updated() {},
  beforeDestroy() {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.close();
    }
  },
  destroyed() {},
  activated() {}
}
</script>

<style scoped>
.message-container {
  display: flex;
  align-items: flex-start;
  margin-bottom: 10px;
}

.message-received {
  margin-right: auto;
  background-color: #ddd;
  border-radius: 10px;
  padding: 10px;
  max-width: 70%;
  word-wrap: break-word;
}

.message-sent {
  margin-left: auto;
  background-color: #00bfff;
  border-radius: 10px;
  padding: 10px;
  max-width: 70%;
  word-wrap: break-word;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

.message-text {
  display: block;
  color: #333;
  font-size: 1em;
}

.time {
  font-size: 0.8em;
  color: #999;
  margin-top: 5px;
}

input {
  width: 200px;
}
button {
  margin-left: 10px;
}
</style>

inde1.vue  第二个页面

<template>
  <div>
    <h2>欢迎来到聊天室</h2>
    <div v-for="message in messages" :key="message.id" class="message-container">
      <div v-if="message.initiatorId !== id" class="message-received">
        <span class="message-text">{{ message.content }}</span>
      </div>
      <div v-else class="message-sent">
        <span class="message-text">{{ message.content }}</span>
        <span class="time">{{ message.createTime }}</span>
      </div>
    </div>
    <input type="text" v-model="newMessage" placeholder="输入消息" />
    <button @click="sendMessage">发送</button>
  </div>
</template>

<script>
import { launchInterrogation, historyInterrogation } from '@/api/interrogation/interrogationType';

export default {
  components: {},
  props: {},
  data() {
    return {
      interrogation: {
        id: null,
        initiatorId: 101,
        doctor: 1
      },
      messages: [],
      newMessage: '',
      ws: null,
      id: 101,
      isConnected: false,
      interrogationId: null,
    };
  },
  computed: {},
  watch: {},
  methods: {
    launchInterrogation() {
      launchInterrogation(this.interrogation).then(res => {
        this.interrogationId = res.data;

        historyInterrogation(this.interrogationId).then(res => {
          console.log("查询到的历史记录:", res.data.interrogationChats);
          if (res.data != null) {
            this.messages = res.data.interrogationChats || [];
          }
        });
      });
    },
    connectToWebSocket() {
      const wsUrl = `ws://ip:port/imserver/${this.id}`;
      this.ws = new WebSocket(wsUrl);

      this.ws.onopen = () => {
        console.log('WebSocket 连接已打开');
        this.isConnected = true;
        this.sendMessageIfConnected();
      };

      this.ws.onclose = () => {
        console.log('WebSocket 连接已关闭');
        this.isConnected = false;
      };

      this.ws.onerror = (error) => {
        console.error('WebSocket 错误', error);
        this.isConnected = false;
      };

      this.ws.onmessage = (event) => {

        const message = JSON.parse(event.data);
        if (message.content !== undefined) {
          console.log("获取到的回调数据:", message);
          this.messages.push(message);
        } else {
          console.warn('接收到的数据无效或格式错误:', event.data);
        }
      };
    },
    sendMessage() {
      if (this.newMessage.trim() === '') return;
      const now = new Date();
      const formattedTime = now.toISOString().slice(0, 19).replace('T', ' ');
      const message = {
        //问诊记录id
        interrogationId: this.interrogationId,

        id: Date.now(),
        content: this.newMessage,
        createTime: formattedTime,
        initiatorId: this.id,
        to: '1',
      };
      if (this.isConnected) {
        this.messages.push(message);
        this.ws.send(JSON.stringify(message));
        this.newMessage = '';
      } else {
        console.warn('WebSocket 连接尚未建立,消息未发送');
      }
    },
    sendMessageIfConnected() {
      if (this.isConnected) {
        const initialMessage = {
          //问诊记录id
          interrogationId: this.interrogationId,

          id: Date.now(),
          content: '初始化连接',
          initiatorId: this.id,
          createTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
          to: '1',
        };
        this.ws.send(JSON.stringify(initialMessage));
      }
    }
  },
  created() {
    this.connectToWebSocket();
    this.launchInterrogation();
  },
  mounted() {},
  beforeCreate() {},
  beforeMount() {},
  beforeUpdate() {},
  updated() {},
  beforeDestroy() {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.close();
    }
  },
  destroyed() {},
  activated() {}
}
</script>

<style scoped>
.message-container {
  display: flex;
  align-items: flex-start;
  margin-bottom: 10px;
}

.message-received {
  margin-right: auto;
  background-color: #ddd;
  border-radius: 10px;
  padding: 10px;
  max-width: 70%;
  word-wrap: break-word;
}

.message-sent {
  margin-left: auto;
  background-color: #00bfff;
  border-radius: 10px;
  padding: 10px;
  max-width: 70%;
  word-wrap: break-word;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

.message-text {
  display: block;
  color: #333;
  font-size: 1em;
}

.time {
  font-size: 0.8em;
  color: #999;
  margin-top: 5px;
}

input {
  width: 200px;
}
button {
  margin-left: 10px;
}
</style>

js方法代码

import request from '@/utils/request'

export function launchInterrogation(data) {
  return request({
    url: '/send/launchInterrogation',
    method: 'post',
    data
  })
}

export function historyInterrogation(param){
  return request({
    url: '/send/historyInterrogation?id='+param,
    method: 'get',
  })
}

效果

 

刷新聊天记录不会消失

注意:因为服务器时间比系统时间慢八个小时,注意同步一下,不然可能发送完消息刷新页面,从数据库读取到的消息发送时间会慢八个小时

;