no message

This commit is contained in:
cb 2025-09-24 11:35:49 +08:00
parent 4d49e11023
commit ea9d926a27
4 changed files with 404 additions and 15 deletions

View File

@ -3,15 +3,21 @@ package com.ruoyi.web.websocket;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.websocket.OnClose; import javax.websocket.OnClose;
import javax.websocket.OnError; import javax.websocket.OnError;
import javax.websocket.OnMessage; import javax.websocket.OnMessage;
import javax.websocket.OnOpen; import javax.websocket.OnOpen;
import javax.websocket.PongMessage;
import javax.websocket.Session; import javax.websocket.Session;
import javax.websocket.server.PathParam; import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint; import javax.websocket.server.ServerEndpoint;
import java.nio.ByteBuffer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -41,12 +47,24 @@ public class CustomerServiceWebSocket {
/** concurrent包的线程安全Set用来存放每个客户端对应的WebSocket对象 */ /** concurrent包的线程安全Set用来存放每个客户端对应的WebSocket对象 */
private static final ConcurrentHashMap<String, CustomerServiceWebSocket> webSocketMap = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<String, CustomerServiceWebSocket> webSocketMap = new ConcurrentHashMap<>();
/** 心跳定时器 */
private static final ScheduledExecutorService heartbeatExecutor = Executors.newScheduledThreadPool(10);
/** 心跳间隔时间(秒) */
private static final int HEARTBEAT_INTERVAL = 30;
/** 连接超时时间(毫秒) */
private static final long SESSION_TIMEOUT = 300000; // 5分钟
/** 与某个客户端的连接会话,需要通过它来给客户端发送数据 */ /** 与某个客户端的连接会话,需要通过它来给客户端发送数据 */
private Session session; private Session session;
/** 接收userId */ /** 接收userId */
private String userId = ""; private String userId = "";
/** 心跳任务 */
private java.util.concurrent.ScheduledFuture<?> heartbeatTask;
/** /**
* 注入客服服务 * 注入客服服务
*/ */
@ -62,7 +80,16 @@ public class CustomerServiceWebSocket {
public void onOpen(Session session, @PathParam("userId") String userId) { public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session; this.session = session;
this.userId = userId; this.userId = userId;
// 设置session超时时间
session.setMaxIdleTimeout(SESSION_TIMEOUT);
if (webSocketMap.containsKey(userId)) { if (webSocketMap.containsKey(userId)) {
// 如果已存在连接先清理旧的心跳任务
CustomerServiceWebSocket oldWebSocket = webSocketMap.get(userId);
if (oldWebSocket != null && oldWebSocket.heartbeatTask != null) {
oldWebSocket.heartbeatTask.cancel(true);
}
webSocketMap.remove(userId); webSocketMap.remove(userId);
webSocketMap.put(userId, this); webSocketMap.put(userId, this);
} else { } else {
@ -70,7 +97,10 @@ public class CustomerServiceWebSocket {
addOnlineCount(); addOnlineCount();
} }
log.info("用户连接:" + userId + ",当前在线人数为:" + getOnlineCount()); // 启动心跳机制
startHeartbeat();
log.info("用户连接:" + userId + ",当前在线人数为:" + getOnlineCount() + ",连接超时设置为:" + SESSION_TIMEOUT + "ms");
try { try {
sendMessage("连接成功"); sendMessage("连接成功");
@ -84,6 +114,9 @@ public class CustomerServiceWebSocket {
*/ */
@OnClose @OnClose
public void onClose() { public void onClose() {
// 停止心跳任务
stopHeartbeat();
if (webSocketMap.containsKey(userId)) { if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId); webSocketMap.remove(userId);
subOnlineCount(); subOnlineCount();
@ -105,6 +138,14 @@ public class CustomerServiceWebSocket {
log.info("用户退出:" + userId + ",当前在线人数为:" + getOnlineCount()); log.info("用户退出:" + userId + ",当前在线人数为:" + getOnlineCount());
} }
/**
* 处理Pong消息
*/
@OnMessage
public void onPong(PongMessage pongMessage, Session session) {
log.debug("收到用户{}的pong消息连接正常", this.userId);
}
/** /**
* 收到客户端消息后调用的方法 * 收到客户端消息后调用的方法
* *
@ -280,4 +321,57 @@ public class CustomerServiceWebSocket {
log.error("发送消息给用户{}失败", userId, e); log.error("发送消息给用户{}失败", userId, e);
} }
} }
/**
* 启动心跳机制
*/
private void startHeartbeat() {
if (heartbeatTask != null) {
heartbeatTask.cancel(true);
}
heartbeatTask = heartbeatExecutor.scheduleWithFixedDelay(() -> {
try {
if (session != null && session.isOpen()) {
// 发送ping消息
ByteBuffer pingData = ByteBuffer.wrap("ping".getBytes());
session.getBasicRemote().sendPing(pingData);
log.debug("向用户{}发送ping消息", userId);
} else {
log.warn("用户{}的session已关闭停止心跳", userId);
stopHeartbeat();
}
} catch (Exception e) {
log.error("发送心跳消息失败,用户: {}, 错误: {}", userId, e.getMessage());
stopHeartbeat();
}
}, HEARTBEAT_INTERVAL, HEARTBEAT_INTERVAL, TimeUnit.SECONDS);
log.info("用户{}启动心跳机制,间隔{}秒", userId, HEARTBEAT_INTERVAL);
}
/**
* 停止心跳机制
*/
private void stopHeartbeat() {
if (heartbeatTask != null && !heartbeatTask.isCancelled()) {
heartbeatTask.cancel(true);
log.info("用户{}停止心跳机制", userId);
}
}
/**
* 发送ping消息保持连接活跃
*/
public void sendPing() {
try {
if (session != null && session.isOpen()) {
ByteBuffer pingData = ByteBuffer.wrap("heartbeat".getBytes());
session.getBasicRemote().sendPing(pingData);
log.debug("向用户{}发送ping消息", userId);
}
} catch (Exception e) {
log.error("发送ping消息失败用户: {}", userId, e);
}
}
} }

View File

@ -199,6 +199,64 @@
padding: 2px 8px; padding: 2px 8px;
font-size: 11px; font-size: 11px;
} }
/* 会话分隔符样式 */
.session-separator {
margin: 20px 0;
text-align: center;
position: relative;
}
.session-separator::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: #e7eaec;
}
.session-separator-text {
background: #f8f8f9;
padding: 5px 15px;
color: #666;
font-size: 12px;
border: 1px solid #e7eaec;
border-radius: 15px;
display: inline-block;
}
.session-separator.current-session .session-separator-text {
background: #e6f7ff;
border-color: #1890ff;
color: #1890ff;
font-weight: bold;
}
/* 历史消息样式 */
.history-message {
opacity: 0.8;
}
.history-message .message-content {
border-style: dashed;
}
/* 加载状态样式 */
.loading-message {
text-align: center;
color: #666;
font-style: italic;
margin: 20px 0;
}
.no-history-message {
text-align: center;
color: #999;
font-style: italic;
margin: 40px 0;
}
</style> </style>
</head> </head>
<body class="gray-bg"> <body class="gray-bg">
@ -307,6 +365,11 @@
} }
} }
// 心跳相关变量
var heartbeatTimer = null;
var heartbeatInterval = 30000; // 30秒心跳间隔
var manualClose = false; // 手动关闭标志
// 创建WebSocket连接 // 创建WebSocket连接
function createWebSocketConnection(userId) { function createWebSocketConnection(userId) {
// 根据当前页面协议动态选择WebSocket协议 // 根据当前页面协议动态选择WebSocket协议
@ -318,9 +381,12 @@
try { try {
// 如果已有连接,先关闭 // 如果已有连接,先关闭
if (wsConnection && wsConnection.readyState !== WebSocket.CLOSED) { if (wsConnection && wsConnection.readyState !== WebSocket.CLOSED) {
manualClose = true;
stopHeartbeat();
wsConnection.close(); wsConnection.close();
} }
manualClose = false;
wsConnection = new WebSocket(wsUrl); wsConnection = new WebSocket(wsUrl);
console.log("[DEBUG] WebSocket对象创建成功"); console.log("[DEBUG] WebSocket对象创建成功");
} catch (e) { } catch (e) {
@ -368,10 +434,19 @@
wsConnection.onopen = function() { wsConnection.onopen = function() {
console.log("[SUCCESS] WebSocket连接已建立"); console.log("[SUCCESS] WebSocket连接已建立");
console.log("[DEBUG] WebSocket readyState:", wsConnection.readyState); console.log("[DEBUG] WebSocket readyState:", wsConnection.readyState);
// 启动心跳机制
startHeartbeat();
}; };
wsConnection.onmessage = function(event) { wsConnection.onmessage = function(event) {
console.log("[DEBUG] 收到WebSocket原始消息:", event.data); console.log("[DEBUG] 收到WebSocket原始消息:", event.data);
// 处理pong消息
if (event.data === 'pong') {
console.log("[DEBUG] 收到服务端pong响应");
return;
}
try { try {
var message = JSON.parse(event.data); var message = JSON.parse(event.data);
console.log("[DEBUG] 解析后的消息对象:", message); console.log("[DEBUG] 解析后的消息对象:", message);
@ -387,9 +462,21 @@
reason: event.reason, reason: event.reason,
wasClean: event.wasClean wasClean: event.wasClean
}); });
// 尝试重连
// 停止心跳
stopHeartbeat();
// 如果不是手动关闭,则尝试重连
if (!manualClose) {
console.log("[DEBUG] 5秒后尝试重连WebSocket..."); console.log("[DEBUG] 5秒后尝试重连WebSocket...");
setTimeout(initWebSocket, 5000); setTimeout(function() {
getCurrentUserId(function(serviceId) {
if (serviceId) {
initWebSocket(serviceId);
}
});
}, 5000);
}
}; };
wsConnection.onerror = function(error) { wsConnection.onerror = function(error) {
@ -398,6 +485,34 @@
}; };
} }
// 启动心跳机制
function startHeartbeat() {
console.log("[DEBUG] 启动心跳机制,间隔:", heartbeatInterval + "ms");
stopHeartbeat(); // 先停止之前的心跳
heartbeatTimer = setInterval(function() {
sendPing();
}, heartbeatInterval);
}
// 停止心跳机制
function stopHeartbeat() {
if (heartbeatTimer) {
console.log("[DEBUG] 停止心跳机制");
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
}
// 发送ping消息
function sendPing() {
if (wsConnection && wsConnection.readyState === WebSocket.OPEN) {
console.log("[DEBUG] 发送ping心跳消息");
wsConnection.send('ping');
} else {
console.log("[WARN] WebSocket连接不可用无法发送ping消息");
}
}
// 处理WebSocket消息 // 处理WebSocket消息
function handleWebSocketMessage(message) { function handleWebSocketMessage(message) {
console.log('[DEBUG] 开始处理WebSocket消息:', message); console.log('[DEBUG] 开始处理WebSocket消息:', message);
@ -491,7 +606,14 @@
html += ' <div style="display: flex; align-items: center;">'; html += ' <div style="display: flex; align-items: center;">';
html += ' <div class="session-avatar">' + avatar + '</div>'; html += ' <div class="session-avatar">' + avatar + '</div>';
html += ' <div class="session-info">'; html += ' <div class="session-info">';
html += ' <div class="session-name">' + (session.customerName || '访客' + session.sessionId) + '</div>'; // 构建显示名称:用户名:手机号
var displayName = '';
if (session.userId) {
displayName = '访客:' + session.userId;
} else {
displayName = '访客:未知号码';
}
html += ' <div class="session-name">' + displayName + '</div>';
html += ' <div class="session-last-msg">' + (session.lastMessage || '暂无消息') + '</div>'; html += ' <div class="session-last-msg">' + (session.lastMessage || '暂无消息') + '</div>';
html += ' <div class="session-time">' + formatTime(session.lastMessageTime) + '</div>'; html += ' <div class="session-time">' + formatTime(session.lastMessageTime) + '</div>';
html += ' </div>'; html += ' </div>';
@ -520,21 +642,154 @@
// 加载聊天历史 // 加载聊天历史
function loadChatHistory(sessionId) { function loadChatHistory(sessionId) {
$.get('/customer/service/messages/' + sessionId, function(data) { console.log('[DEBUG] 开始加载聊天历史 - sessionId:', sessionId);
var html = '';
if (data.code === 0 && data.data) { // 显示加载状态
data.data.forEach(function(message) { $('#chatMessages').html('<div class="loading-message">正在加载聊天历史...</div>');
html += createMessageHtml(message.content, message.senderType === 'CUSTOMER', message.createTime);
// 首先获取会话信息以获取用户ID
$.get('/customer/service/sessions')
.done(function(sessionsData) {
if (sessionsData.code === 0 && sessionsData.data) {
// 查找当前会话
var currentSession = sessionsData.data.find(function(session) {
return session.sessionId === sessionId;
});
if (currentSession && currentSession.userId) {
console.log('[DEBUG] 找到会话用户ID:', currentSession.userId);
// 通过用户ID获取所有历史记录
loadChatHistoryByUserId(currentSession.userId, sessionId);
} else {
console.log('[DEBUG] 未找到用户ID使用原有方式加载会话历史');
// 如果没有用户ID回退到原有方式
loadChatHistoryBySessionId(sessionId);
}
} else {
console.log('[DEBUG] 获取会话列表失败,使用原有方式加载会话历史');
loadChatHistoryBySessionId(sessionId);
}
})
.fail(function(xhr, status, error) {
console.error('[ERROR] 获取会话列表失败:', {
status: xhr.status,
statusText: xhr.statusText,
error: error
});
// 回退到原有方式
loadChatHistoryBySessionId(sessionId);
}); });
} }
// 通过用户ID加载聊天历史
function loadChatHistoryByUserId(userId, currentSessionId) {
console.log('[DEBUG] 通过用户ID加载聊天历史 - userId:', userId);
$.get('/system/chatHistory/app/user/' + userId)
.done(function(data) {
console.log('[DEBUG] 用户聊天历史API响应:', data);
var html = '';
if (data.code === 0 && data.data && data.data.length > 0) {
console.log('[DEBUG] 用户聊天历史消息数量:', data.data.length);
// 按时间排序
var sortedMessages = data.data.sort(function(a, b) {
return new Date(a.createTime) - new Date(b.createTime);
});
// 按会话分组显示
var sessionGroups = {};
sortedMessages.forEach(function(message) {
if (!sessionGroups[message.sessionId]) {
sessionGroups[message.sessionId] = [];
}
sessionGroups[message.sessionId].push(message);
});
// 渲染消息,当前会话高亮显示
Object.keys(sessionGroups).forEach(function(sessionId) {
var isCurrentSession = sessionId === currentSessionId;
var separatorClass = isCurrentSession ? 'session-separator current-session' : 'session-separator';
var separatorText = isCurrentSession ? '当前会话' : '历史会话 - ' + sessionId;
html += '<div class="' + separatorClass + '">';
html += ' <span class="session-separator-text">' + separatorText + '</span>';
html += '</div>';
sessionGroups[sessionId].forEach(function(message) {
console.log('[DEBUG] 消息类型:', message.messageType, '消息内容:', message.content);
// messageType为"user"的消息显示在左边(received=true),其他消息显示在右边(received=false)
// received样式左对齐白色背景sent样式右对齐蓝色背景
var isReceived = message.messageType === 'user';
html += createMessageHtml(message.content, isReceived, message.createTime, isCurrentSession);
});
});
} else {
console.log('[DEBUG] 用户聊天历史数据为空');
html = '<div class="no-history-message">暂无聊天记录</div>';
}
$('#chatMessages').html(html); $('#chatMessages').html(html);
scrollToBottom(); scrollToBottom();
console.log('[DEBUG] 用户聊天历史加载完成');
})
.fail(function(xhr, status, error) {
console.error('[ERROR] 用户聊天历史API调用失败:', {
status: xhr.status,
statusText: xhr.statusText,
error: error,
response: xhr.responseText
});
// 回退到原有方式
loadChatHistoryBySessionId(currentSessionId);
});
}
// 通过会话ID加载聊天历史原有方式
function loadChatHistoryBySessionId(sessionId) {
console.log('[DEBUG] 通过会话ID加载聊天历史 - sessionId:', sessionId);
$.get('/customer/service/messages/' + sessionId)
.done(function(data) {
console.log('[DEBUG] 会话聊天历史API响应:', data);
var html = '';
if (data.code === 0 && data.data) {
console.log('[DEBUG] 会话聊天历史消息数量:', data.data.length);
data.data.forEach(function(message) {
console.log('[DEBUG] 消息类型:', message.messageType, '消息内容:', message.content);
// messageType为"user"的消息显示在左边(received=true),其他消息显示在右边(received=false)
// received样式左对齐白色背景sent样式右对齐蓝色背景
var isReceived = message.messageType === 'user';
html += createMessageHtml(message.content, isReceived, message.createTime, true);
});
} else {
console.log('[DEBUG] 会话聊天历史数据为空或返回错误:', data);
html = '<div class="no-history-message">暂无聊天记录</div>';
}
$('#chatMessages').html(html);
scrollToBottom();
console.log('[DEBUG] 会话聊天历史加载完成');
})
.fail(function(xhr, status, error) {
console.error('[ERROR] 会话聊天历史API调用失败:', {
status: xhr.status,
statusText: xhr.statusText,
error: error,
response: xhr.responseText
});
$('#chatMessages').html('<div class="no-history-message" style="color: #f5222d;">加载聊天历史失败,请稍后重试</div>');
}); });
} }
// 创建消息HTML // 创建消息HTML
function createMessageHtml(content, isReceived, timestamp) { function createMessageHtml(content, isReceived, timestamp, isCurrentSession) {
var messageClass = isReceived ? 'received' : 'sent'; var messageClass = isReceived ? 'received' : 'sent';
if (isCurrentSession === false) {
messageClass += ' history-message';
}
var html = '<div class="message-item ' + messageClass + '">'; var html = '<div class="message-item ' + messageClass + '">';
html += ' <div class="message-content">'; html += ' <div class="message-content">';
html += ' <div>' + content + '</div>'; html += ' <div>' + content + '</div>';

View File

@ -304,4 +304,12 @@ public interface ICustomerServiceService
* @return 已接受的转人工记录如果没有则返回null * @return 已接受的转人工记录如果没有则返回null
*/ */
public ManualServiceSessions getAcceptedManualSessionByUserId(Long userId); public ManualServiceSessions getAcceptedManualSessionByUserId(Long userId);
/**
* 清理客服的已接受转人工记录
*
* @param serviceId 客服ID
* @return 清理数量
*/
public int cleanupAcceptedManualRequestsByServiceId(Long serviceId);
} }

View File

@ -621,4 +621,36 @@ public class CustomerServiceServiceImpl implements ICustomerServiceService
e.printStackTrace(); e.printStackTrace();
} }
} }
/**
* 清理客服的已接受转人工记录
*
* @param serviceId 客服ID
* @return 清理数量
*/
@Override
public int cleanupAcceptedManualRequestsByServiceId(Long serviceId)
{
ManualServiceSessions condition = new ManualServiceSessions();
condition.setServiceId(serviceId);
condition.setStatus("1"); // 已接受状态
List<ManualServiceSessions> acceptedRequests = manualServiceSessionsMapper.selectManualServiceSessionsList(condition);
if (acceptedRequests != null && !acceptedRequests.isEmpty()) {
System.out.println("[DEBUG] 清理客服 " + serviceId + " 的已接受转人工记录,数量: " + acceptedRequests.size());
// 将状态更新为已关闭
for (ManualServiceSessions request : acceptedRequests) {
request.setStatus("3"); // 3-已关闭WebSocket断开
request.setEndTime(DateUtils.getNowDate());
request.setUpdateTime(DateUtils.getNowDate());
manualServiceSessionsMapper.updateManualServiceSessions(request);
}
return acceptedRequests.size();
}
return 0;
}
} }