下载依赖
<!-- 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',
})
}
效果
刷新聊天记录不会消失
注意:因为服务器时间比系统时间慢八个小时,注意同步一下,不然可能发送完消息刷新页面,从数据库读取到的消息发送时间会慢八个小时