新增 后台的客户对话页面

This commit is contained in:
cb 2025-09-22 17:27:11 +08:00
parent 9b1abac458
commit a13c5a0ff1
16 changed files with 2553 additions and 7 deletions

View File

@ -77,6 +77,12 @@
<artifactId>ruoyi-generator</artifactId>
</dependency>
<!-- WebSocket支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,23 @@
package com.ruoyi.web.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket配置
*
* @author ruoyi
*/
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

View File

@ -0,0 +1,286 @@
package com.ruoyi.web.controller.customer;
import java.util.List;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.UserSessions;
import com.ruoyi.system.domain.ManualServiceSessions;
import com.ruoyi.customer.service.ICustomerServiceService;
import com.ruoyi.system.domain.ChatHistory;
import com.ruoyi.common.core.page.TableDataInfo;
/**
* 客服系统Controller
*
* @author ruoyi
* @date 2024-01-01
*/
@Controller
@RequestMapping("/customer/service")
public class CustomerServiceController extends BaseController
{
private String prefix = "customer/service";
@Autowired
private ICustomerServiceService customerServiceService;
@RequiresPermissions("customer:service:view")
@GetMapping()
public String service()
{
return prefix + "/index";
}
/**
* 查询会话列表
*/
@RequiresPermissions("customer:service:list")
@PostMapping("/list")
@ResponseBody
public TableDataInfo list(UserSessions userSessions)
{
startPage();
List<UserSessions> list = customerServiceService.selectUserSessionsList(userSessions);
return getDataTable(list);
}
/**
* 获取所有活跃会话
*/
@GetMapping("/sessions")
@ResponseBody
public AjaxResult getSessions()
{
try {
List<UserSessions> sessions = customerServiceService.getActiveSessions();
return AjaxResult.success(sessions);
} catch (Exception e) {
logger.error("获取会话列表失败", e);
return AjaxResult.error("获取会话列表失败");
}
}
/**
* 获取指定会话信息
*/
@GetMapping("/sessions/{sessionId}")
@ResponseBody
public AjaxResult getSession(@PathVariable Long sessionId)
{
try {
UserSessions session = customerServiceService.selectUserSessionsById(sessionId);
return AjaxResult.success(session);
} catch (Exception e) {
logger.error("获取会话信息失败", e);
return AjaxResult.error("获取会话信息失败");
}
}
/**
* 获取会话聊天记录
*/
@GetMapping("/messages/{sessionId}")
@ResponseBody
public AjaxResult getChatHistory(@PathVariable Long sessionId)
{
try {
List<ChatHistory> messages = customerServiceService.getChatHistoryBySessionId(sessionId);
return AjaxResult.success(messages);
} catch (Exception e) {
logger.error("获取聊天记录失败", e);
return AjaxResult.error("获取聊天记录失败");
}
}
/**
* 发送消息
*/
@PostMapping("/send")
@ResponseBody
public AjaxResult sendMessage(ChatHistory chatHistory)
{
try {
// 设置发送者信息
chatHistory.setSenderId(getUserId());
chatHistory.setSenderType("SERVICE");
int result = customerServiceService.sendMessage(chatHistory);
if (result > 0) {
return AjaxResult.success("消息发送成功");
} else {
return AjaxResult.error("消息发送失败");
}
} catch (Exception e) {
logger.error("发送消息失败", e);
return AjaxResult.error("发送消息失败: " + e.getMessage());
}
}
/**
* 转人工服务
*/
@PostMapping("/transfer")
@ResponseBody
public AjaxResult transferToManual(Long sessionId, String reason)
{
try {
ManualServiceSessions manualSession = new ManualServiceSessions();
manualSession.setSessionId(sessionId);
manualSession.setReason(reason);
manualSession.setStatus("PENDING");
manualSession.setCreateBy(getLoginName());
int result = customerServiceService.createManualServiceRequest(manualSession);
if (result > 0) {
return AjaxResult.success("转人工请求已提交");
} else {
return AjaxResult.error("转人工请求提交失败");
}
} catch (Exception e) {
logger.error("转人工服务失败", e);
return AjaxResult.error("转人工服务失败: " + e.getMessage());
}
}
/**
* 获取转人工请求列表
*/
@GetMapping("/manual-requests")
@ResponseBody
public AjaxResult getManualRequests()
{
try {
List<ManualServiceSessions> requests = customerServiceService.getPendingManualRequests();
return AjaxResult.success(requests);
} catch (Exception e) {
logger.error("获取转人工请求失败", e);
return AjaxResult.error("获取转人工请求失败");
}
}
/**
* 接受转人工请求
*/
@PostMapping("/manual-requests/{requestId}/accept")
@ResponseBody
public AjaxResult acceptManualRequest(@PathVariable Long requestId)
{
try {
ManualServiceSessions manualSession = new ManualServiceSessions();
manualSession.setUserId(requestId);
manualSession.setStatus("ACCEPTED");
manualSession.setServiceId(getUserId());
manualSession.setUpdateBy(getLoginName());
int result = customerServiceService.updateManualServiceRequest(manualSession);
if (result > 0) {
return AjaxResult.success("已接受转人工请求");
} else {
return AjaxResult.error("操作失败");
}
} catch (Exception e) {
logger.error("接受转人工请求失败", e);
return AjaxResult.error("操作失败: " + e.getMessage());
}
}
/**
* 拒绝转人工请求
*/
@PostMapping("/manual-requests/{requestId}/reject")
@ResponseBody
public AjaxResult rejectManualRequest(@PathVariable Long requestId)
{
try {
ManualServiceSessions manualSession = new ManualServiceSessions();
manualSession.setUserId(requestId);
manualSession.setStatus("REJECTED");
manualSession.setServiceId(getUserId());
manualSession.setUpdateBy(getLoginName());
int result = customerServiceService.updateManualServiceRequest(manualSession);
if (result > 0) {
return AjaxResult.success("已拒绝转人工请求");
} else {
return AjaxResult.error("操作失败");
}
} catch (Exception e) {
logger.error("拒绝转人工请求失败", e);
return AjaxResult.error("操作失败: " + e.getMessage());
}
}
/**
* 更新客服状态
*/
@PostMapping("/status")
@ResponseBody
public AjaxResult updateServiceStatus(String status)
{
try {
int result = customerServiceService.updateServiceStatus(getUserId(), status);
if (result > 0) {
return AjaxResult.success("状态更新成功");
} else {
return AjaxResult.error("状态更新失败");
}
} catch (Exception e) {
logger.error("更新客服状态失败", e);
return AjaxResult.error("状态更新失败: " + e.getMessage());
}
}
/**
* 导出会话列表
*/
@RequiresPermissions("customer:service:export")
@Log(title = "客服会话", businessType = BusinessType.EXPORT)
@PostMapping("/export")
@ResponseBody
public AjaxResult export(UserSessions userSessions)
{
List<UserSessions> list = customerServiceService.selectUserSessionsList(userSessions);
ExcelUtil<UserSessions> util = new ExcelUtil<UserSessions>(UserSessions.class);
return util.exportExcel(list, "会话数据");
}
/**
* 获取客服统计信息
*/
@GetMapping("/statistics")
@ResponseBody
public AjaxResult getStatistics()
{
try {
// 获取今日统计数据
int todaySessions = customerServiceService.getTodaySessionCount();
int todayMessages = customerServiceService.getTodayMessageCount();
int pendingRequests = customerServiceService.getPendingRequestCount();
int onlineServices = customerServiceService.getOnlineServiceCount();
AjaxResult result = AjaxResult.success();
result.put("todaySessions", todaySessions);
result.put("todayMessages", todayMessages);
result.put("pendingRequests", pendingRequests);
result.put("onlineServices", onlineServices);
return result;
} catch (Exception e) {
logger.error("获取统计信息失败", e);
return AjaxResult.error("获取统计信息失败");
}
}
}

View File

@ -0,0 +1,164 @@
package com.ruoyi.web.websocket;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
/**
* 客服系统WebSocket服务
*
* @author ruoyi
*/
@ServerEndpoint("/websocket/customer/{userId}")
@Component
public class CustomerServiceWebSocket {
private static final Logger log = LoggerFactory.getLogger(CustomerServiceWebSocket.class);
/** 静态变量,用来记录当前在线连接数 */
private static final AtomicInteger onlineCount = new AtomicInteger(0);
/** concurrent包的线程安全Set用来存放每个客户端对应的WebSocket对象 */
private static final ConcurrentHashMap<String, CustomerServiceWebSocket> webSocketMap = new ConcurrentHashMap<>();
/** 与某个客户端的连接会话,需要通过它来给客户端发送数据 */
private Session session;
/** 接收userId */
private String userId = "";
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId);
webSocketMap.put(userId, this);
} else {
webSocketMap.put(userId, this);
addOnlineCount();
}
log.info("用户连接:" + userId + ",当前在线人数为:" + getOnlineCount());
try {
sendMessage("连接成功");
} catch (IOException e) {
log.error("用户:" + userId + ",网络异常!!!!!!");
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId);
subOnlineCount();
}
log.info("用户退出:" + userId + ",当前在线人数为:" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("用户消息:" + userId + ",报文:" + message);
// 解析消息
if (message != null && !message.isEmpty()) {
try {
com.alibaba.fastjson.JSONObject jsonObject = JSON.parseObject(message);
String toUserId = jsonObject.getString("toUserId");
String messageContent = jsonObject.getString("message");
String messageType = jsonObject.getString("type");
// 传输给对应toUserId用户的websocket
if (toUserId != null && webSocketMap.containsKey(toUserId)) {
webSocketMap.get(toUserId).sendMessage(JSON.toJSONString(jsonObject));
} else {
log.error("请求的userId:" + toUserId + "不在该服务器上");
}
} catch (Exception e) {
log.error("消息解析异常", e);
}
}
}
/**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 发送自定义消息
*/
public static void sendInfo(String message, @PathParam("userId") String userId) throws IOException {
log.info("发送消息到:" + userId + ",报文:" + message);
if (userId != null && webSocketMap.containsKey(userId)) {
webSocketMap.get(userId).sendMessage(message);
} else {
log.error("用户" + userId + ",不在线!");
}
}
/**
* 获得此时的在线人数
*/
public static synchronized int getOnlineCount() {
return onlineCount.get();
}
/**
* 在线人数加1
*/
public static synchronized void addOnlineCount() {
onlineCount.incrementAndGet();
}
/**
* 在线人数减1
*/
public static synchronized void subOnlineCount() {
onlineCount.decrementAndGet();
}
/**
* 获取在线用户列表
*/
public static ConcurrentHashMap<String, CustomerServiceWebSocket> getWebSocketMap() {
return webSocketMap;
}
}

View File

@ -0,0 +1,594 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('客服系统')" />
<link th:href="@{/css/plugins/webuploader/webuploader.thumbnail.css}" rel="stylesheet"/>
<style>
.customer-service-container {
height: calc(100vh - 30px);
display: flex;
background: #fff;
}
.session-list {
width: 300px;
border-right: 1px solid #e7eaec;
overflow-y: auto;
}
.session-item {
padding: 15px;
border-bottom: 1px solid #f1f1f1;
cursor: pointer;
transition: background-color 0.3s;
}
.session-item:hover {
background-color: #f8f8f9;
}
.session-item.active {
background-color: #e6f7ff;
border-left: 3px solid #1890ff;
}
.session-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #1890ff;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 10px;
}
.session-info {
flex: 1;
}
.session-name {
font-weight: bold;
margin-bottom: 5px;
}
.session-last-msg {
color: #666;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-time {
font-size: 11px;
color: #999;
}
.session-status {
width: 8px;
height: 8px;
border-radius: 50%;
margin-left: 5px;
}
.status-online { background: #52c41a; }
.status-offline { background: #d9d9d9; }
.status-busy { background: #faad14; }
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
}
.chat-header {
padding: 15px 20px;
border-bottom: 1px solid #e7eaec;
background: #fafafa;
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background: #f8f8f9;
}
.message-item {
margin-bottom: 20px;
display: flex;
}
.message-item.sent {
justify-content: flex-end;
}
.message-content {
max-width: 60%;
padding: 10px 15px;
border-radius: 8px;
position: relative;
}
.message-item.received .message-content {
background: #fff;
border: 1px solid #e7eaec;
}
.message-item.sent .message-content {
background: #1890ff;
color: white;
}
.message-time {
font-size: 11px;
color: #999;
margin-top: 5px;
}
.chat-input {
padding: 15px 20px;
border-top: 1px solid #e7eaec;
background: #fff;
}
.input-toolbar {
margin-bottom: 10px;
}
.input-area {
display: flex;
align-items: flex-end;
}
.input-text {
flex: 1;
min-height: 60px;
max-height: 120px;
resize: none;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 10px;
margin-right: 10px;
}
.manual-service-panel {
width: 280px;
border-left: 1px solid #e7eaec;
background: #fafafa;
overflow-y: auto;
}
.panel-header {
padding: 15px;
border-bottom: 1px solid #e7eaec;
font-weight: bold;
}
.manual-request-item {
padding: 15px;
border-bottom: 1px solid #f1f1f1;
}
.request-user {
font-weight: bold;
margin-bottom: 5px;
}
.request-reason {
color: #666;
font-size: 12px;
margin-bottom: 10px;
}
.request-time {
font-size: 11px;
color: #999;
margin-bottom: 10px;
}
.request-actions {
display: flex;
gap: 5px;
}
.btn-sm {
padding: 2px 8px;
font-size: 11px;
}
</style>
</head>
<body class="gray-bg">
<div class="wrapper wrapper-content">
<div class="customer-service-container">
<!-- 左侧会话列表 -->
<div class="session-list">
<div class="panel-header">
<i class="fa fa-comments"></i> 会话列表
<span class="badge badge-primary pull-right" id="sessionCount">0</span>
</div>
<div id="sessionListContainer">
<!-- 会话列表将通过JavaScript动态加载 -->
</div>
</div>
<!-- 中间聊天区域 -->
<div class="chat-area">
<div class="chat-header">
<div id="currentSessionInfo">
<span class="text-muted">请选择一个会话开始对话</span>
</div>
</div>
<div class="chat-messages" id="chatMessages">
<!-- 聊天消息将通过JavaScript动态加载 -->
</div>
<div class="chat-input">
<div class="input-toolbar">
<button type="button" class="btn btn-sm btn-default" onclick="insertEmoji('😊')">
<i class="fa fa-smile-o"></i> 表情
</button>
<button type="button" class="btn btn-sm btn-default" onclick="uploadFile()">
<i class="fa fa-paperclip"></i> 文件
</button>
<!-- <button type="button" class="btn btn-sm btn-warning pull-right" onclick="transferToManual()">
<i class="fa fa-user"></i> 转人工
</button> -->
</div>
<div class="input-area">
<textarea class="form-control input-text" id="messageInput"
placeholder="请输入消息内容..."
onkeydown="handleKeyDown(event)"></textarea>
<button type="button" class="btn btn-primary" onclick="sendMessage()">
<i class="fa fa-send"></i> 发送
</button>
</div>
</div>
</div>
<!-- 右侧转人工消息列表 -->
<div class="manual-service-panel">
<div class="panel-header">
<i class="fa fa-user-plus"></i> 转人工请求
<span class="badge badge-warning pull-right" id="manualRequestCount">0</span>
</div>
<div id="manualRequestContainer">
<!-- 转人工请求列表将通过JavaScript动态加载 -->
</div>
</div>
</div>
</div>
<th:block th:include="include :: footer" />
<script th:src="@{/js/plugins/layer/layer.min.js}"></script>
<script th:src="@{/js/plugins/webuploader/webuploader.min.js}"></script>
<script>
var currentSessionId = null;
var wsConnection = null;
$(document).ready(function() {
initWebSocket();
loadSessionList();
loadManualRequests();
// 每30秒刷新一次数据
setInterval(function() {
loadSessionList();
loadManualRequests();
}, 30000);
});
// 初始化WebSocket连接
function initWebSocket() {
if (typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
return;
}
var wsUrl = "ws://" + window.location.host + "/websocket/customer-service";
wsConnection = new WebSocket(wsUrl);
wsConnection.onopen = function() {
console.log("WebSocket连接已建立");
};
wsConnection.onmessage = function(event) {
var message = JSON.parse(event.data);
handleWebSocketMessage(message);
};
wsConnection.onclose = function() {
console.log("WebSocket连接已关闭");
// 尝试重连
setTimeout(initWebSocket, 5000);
};
wsConnection.onerror = function(error) {
console.log("WebSocket连接错误:", error);
};
}
// 处理WebSocket消息
function handleWebSocketMessage(message) {
switch(message.type) {
case 'NEW_MESSAGE':
if (message.sessionId == currentSessionId) {
appendMessage(message.content, false, message.timestamp);
}
loadSessionList(); // 刷新会话列表
break;
case 'SESSION_UPDATE':
loadSessionList();
break;
case 'MANUAL_REQUEST':
loadManualRequests();
break;
}
}
// 加载会话列表
function loadSessionList() {
$.get('/customer/service/sessions', function(data) {
var html = '';
if (data.code === 0 && data.data) {
data.data.forEach(function(session) {
var statusClass = getStatusClass(session.status);
var avatar = session.customerName ? session.customerName.charAt(0).toUpperCase() : 'U';
var activeClass = session.sessionId == currentSessionId ? 'active' : '';
html += '<div class="session-item ' + activeClass + '" onclick="selectSession(' + session.sessionId + ')">';
html += ' <div style="display: flex; align-items: center;">';
html += ' <div class="session-avatar">' + avatar + '</div>';
html += ' <div class="session-info">';
html += ' <div class="session-name">' + (session.customerName || '访客' + session.sessionId) + '</div>';
html += ' <div class="session-last-msg">' + (session.lastMessage || '暂无消息') + '</div>';
html += ' <div class="session-time">' + formatTime(session.lastMessageTime) + '</div>';
html += ' </div>';
html += ' <div class="session-status ' + statusClass + '"></div>';
html += ' </div>';
html += '</div>';
});
$('#sessionCount').text(data.data.length);
}
$('#sessionListContainer').html(html);
});
}
// 选择会话
function selectSession(sessionId) {
currentSessionId = sessionId;
$('.session-item').removeClass('active');
$('[onclick="selectSession(' + sessionId + ')"]').addClass('active');
loadChatHistory(sessionId);
updateCurrentSessionInfo(sessionId);
}
// 加载聊天历史
function loadChatHistory(sessionId) {
$.get('/customer/service/messages/' + sessionId, function(data) {
var html = '';
if (data.code === 0 && data.data) {
data.data.forEach(function(message) {
html += createMessageHtml(message.content, message.senderType === 'CUSTOMER', message.createTime);
});
}
$('#chatMessages').html(html);
scrollToBottom();
});
}
// 创建消息HTML
function createMessageHtml(content, isReceived, timestamp) {
var messageClass = isReceived ? 'received' : 'sent';
var html = '<div class="message-item ' + messageClass + '">';
html += ' <div class="message-content">';
html += ' <div>' + content + '</div>';
html += ' <div class="message-time">' + formatTime(timestamp) + '</div>';
html += ' </div>';
html += '</div>';
return html;
}
// 发送消息
function sendMessage() {
if (!currentSessionId) {
layer.msg('请先选择一个会话');
return;
}
var content = $('#messageInput').val().trim();
if (!content) {
layer.msg('请输入消息内容');
return;
}
var messageData = {
sessionId: currentSessionId,
content: content,
senderType: 'SERVICE'
};
$.post('/customer/service/send', messageData, function(data) {
if (data.code === 0) {
appendMessage(content, false, new Date());
$('#messageInput').val('');
loadSessionList(); // 刷新会话列表
} else {
layer.msg('发送失败: ' + data.msg);
}
});
}
// 添加消息到聊天区域
function appendMessage(content, isReceived, timestamp) {
var messageHtml = createMessageHtml(content, isReceived, timestamp);
$('#chatMessages').append(messageHtml);
scrollToBottom();
}
// 滚动到底部
function scrollToBottom() {
var chatMessages = document.getElementById('chatMessages');
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// 处理键盘事件
function handleKeyDown(event) {
if (event.ctrlKey && event.keyCode === 13) {
sendMessage();
}
}
// 转人工
function transferToManual() {
if (!currentSessionId) {
layer.msg('请先选择一个会话');
return;
}
layer.prompt({
title: '转人工服务',
formType: 2,
value: '',
area: ['300px', '200px']
}, function(reason, index) {
$.post('/customer/service/transfer', {
sessionId: currentSessionId,
reason: reason
}, function(data) {
if (data.code === 0) {
layer.msg('转人工请求已提交');
loadManualRequests();
} else {
layer.msg('转人工失败: ' + data.msg);
}
});
layer.close(index);
});
}
// 加载转人工请求列表
function loadManualRequests() {
$.get('/customer/service/manual-requests', function(data) {
var html = '';
if (data.code === 0 && data.data) {
data.data.forEach(function(request) {
html += '<div class="manual-request-item">';
html += ' <div class="request-user">' + (request.customerName || '访客' + request.sessionId) + '</div>';
html += ' <div class="request-reason">' + (request.reason || '无') + '</div>';
html += ' <div class="request-time">' + formatTime(request.createTime) + '</div>';
html += ' <div class="request-actions">';
html += ' <button class="btn btn-success btn-sm" onclick="acceptManualRequest(' + request.id + ')">';
html += ' <i class="fa fa-check"></i> 接受';
html += ' </button>';
html += ' <button class="btn btn-default btn-sm" onclick="rejectManualRequest(' + request.id + ')">';
html += ' <i class="fa fa-times"></i> 拒绝';
html += ' </button>';
html += ' </div>';
html += '</div>';
});
$('#manualRequestCount').text(data.data.length);
}
$('#manualRequestContainer').html(html);
});
}
// 接受转人工请求
function acceptManualRequest(requestId) {
$.post('/customer/service/manual-requests/' + requestId + '/accept', function(data) {
if (data.code === 0) {
layer.msg('已接受转人工请求');
loadManualRequests();
loadSessionList();
} else {
layer.msg('操作失败: ' + data.msg);
}
});
}
// 拒绝转人工请求
function rejectManualRequest(requestId) {
$.post('/customer/service/manual-requests/' + requestId + '/reject', function(data) {
if (data.code === 0) {
layer.msg('已拒绝转人工请求');
loadManualRequests();
} else {
layer.msg('操作失败: ' + data.msg);
}
});
}
// 更新当前会话信息
function updateCurrentSessionInfo(sessionId) {
$.get('/customer/service/sessions/' + sessionId, function(data) {
if (data.code === 0 && data.data) {
var session = data.data;
var html = '<strong>' + (session.customerName || '访客' + session.sessionId) + '</strong>';
html += ' <span class="text-muted">(' + getStatusText(session.status) + ')</span>';
$('#currentSessionInfo').html(html);
}
});
}
// 获取状态样式类
function getStatusClass(status) {
switch(status) {
case 'ONLINE': return 'status-online';
case 'BUSY': return 'status-busy';
default: return 'status-offline';
}
}
// 获取状态文本
function getStatusText(status) {
switch(status) {
case 'ONLINE': return '在线';
case 'BUSY': return '忙碌';
case 'OFFLINE': return '离线';
default: return '未知';
}
}
// 格式化时间
function formatTime(timestamp) {
if (!timestamp) return '';
var date = new Date(timestamp);
var now = new Date();
var diff = now - date;
if (diff < 60000) { // 1分钟内
return '刚刚';
} else if (diff < 3600000) { // 1小时内
return Math.floor(diff / 60000) + '分钟前';
} else if (diff < 86400000) { // 24小时内
return Math.floor(diff / 3600000) + '小时前';
} else {
return date.getMonth() + 1 + '/' + date.getDate() + ' ' +
String(date.getHours()).padStart(2, '0') + ':' +
String(date.getMinutes()).padStart(2, '0');
}
}
// 插入表情
function insertEmoji(emoji) {
var input = document.getElementById('messageInput');
var start = input.selectionStart;
var end = input.selectionEnd;
var text = input.value;
input.value = text.substring(0, start) + emoji + text.substring(end);
input.focus();
input.setSelectionRange(start + emoji.length, start + emoji.length);
}
// 文件上传
function uploadFile() {
layer.msg('文件上传功能开发中...');
}
</script>
</body>
</html>

View File

@ -0,0 +1,242 @@
package com.ruoyi.customer.service;
import java.util.List;
import com.ruoyi.system.domain.ChatHistory;
import com.ruoyi.system.domain.UserSessions;
import com.ruoyi.system.domain.ManualServiceSessions;
/**
* 客服系统Service接口
*
* @author ruoyi
* @date 2024-01-01
*/
public interface ICustomerServiceService
{
/**
* 查询用户会话
*
* @param sessionId 用户会话ID
* @return 用户会话
*/
public UserSessions selectUserSessionsById(Long sessionId);
/**
* 查询用户会话列表
*
* @param userSessions 用户会话
* @return 用户会话集合
*/
public List<UserSessions> selectUserSessionsList(UserSessions userSessions);
/**
* 获取活跃会话列表
*
* @return 活跃会话集合
*/
public List<UserSessions> getActiveSessions();
/**
* 新增用户会话
*
* @param userSessions 用户会话
* @return 结果
*/
public int insertUserSessions(UserSessions userSessions);
/**
* 修改用户会话
*
* @param userSessions 用户会话
* @return 结果
*/
public int updateUserSessions(UserSessions userSessions);
/**
* 批量删除用户会话
*
* @param sessionIds 需要删除的用户会话ID
* @return 结果
*/
public int deleteUserSessionsByIds(Long[] sessionIds);
/**
* 删除用户会话信息
*
* @param sessionId 用户会话ID
* @return 结果
*/
public int deleteUserSessionsById(Long sessionId);
/**
* 根据会话ID获取聊天记录
*
* @param sessionId 会话ID
* @return 聊天记录列表
*/
public List<ChatHistory> getChatHistoryBySessionId(Long sessionId);
/**
* 发送消息
*
* @param chatHistory 聊天记录
* @return 结果
*/
public int sendMessage(ChatHistory chatHistory);
/**
* 创建转人工服务请求
*
* @param manualServiceSessions 转人工服务会话
* @return 结果
*/
public int createManualServiceRequest(ManualServiceSessions manualServiceSessions);
/**
* 更新转人工服务请求
*
* @param manualServiceSessions 转人工服务会话
* @return 结果
*/
public int updateManualServiceRequest(ManualServiceSessions manualServiceSessions);
/**
* 获取待处理的转人工请求
*
* @return 转人工请求列表
*/
public List<ManualServiceSessions> getPendingManualRequests();
/**
* 更新客服状态
*
* @param serviceId 客服ID
* @param status 状态
* @return 结果
*/
public int updateServiceStatus(Long serviceId, String status);
/**
* 获取今日会话数量
*
* @return 会话数量
*/
public int getTodaySessionCount();
/**
* 获取今日消息数量
*
* @return 消息数量
*/
public int getTodayMessageCount();
/**
* 获取待处理请求数量
*
* @return 请求数量
*/
public int getPendingRequestCount();
/**
* 获取在线客服数量
*
* @return 客服数量
*/
public int getOnlineServiceCount();
/**
* 查询聊天记录
*
* @param messageId 聊天记录ID
* @return 聊天记录
*/
public ChatHistory selectChatHistoryById(Long messageId);
/**
* 查询聊天记录列表
*
* @param chatHistory 聊天记录
* @return 聊天记录集合
*/
public List<ChatHistory> selectChatHistoryList(ChatHistory chatHistory);
/**
* 新增聊天记录
*
* @param chatHistory 聊天记录
* @return 结果
*/
public int insertChatHistory(ChatHistory chatHistory);
/**
* 修改聊天记录
*
* @param chatHistory 聊天记录
* @return 结果
*/
public int updateChatHistory(ChatHistory chatHistory);
/**
* 批量删除聊天记录
*
* @param messageIds 需要删除的聊天记录ID
* @return 结果
*/
public int deleteChatHistoryByIds(Long[] messageIds);
/**
* 删除聊天记录信息
*
* @param messageId 聊天记录ID
* @return 结果
*/
public int deleteChatHistoryById(Long messageId);
/**
* 查询转人工服务会话
*
* @param id 转人工服务会话ID
* @return 转人工服务会话
*/
public ManualServiceSessions selectManualServiceSessionsById(Long id);
/**
* 查询转人工服务会话列表
*
* @param manualServiceSessions 转人工服务会话
* @return 转人工服务会话集合
*/
public List<ManualServiceSessions> selectManualServiceSessionsList(ManualServiceSessions manualServiceSessions);
/**
* 新增转人工服务会话
*
* @param manualServiceSessions 转人工服务会话
* @return 结果
*/
public int insertManualServiceSessions(ManualServiceSessions manualServiceSessions);
/**
* 修改转人工服务会话
*
* @param manualServiceSessions 转人工服务会话
* @return 结果
*/
public int updateManualServiceSessions(ManualServiceSessions manualServiceSessions);
/**
* 批量删除转人工服务会话
*
* @param ids 需要删除的转人工服务会话ID
* @return 结果
*/
public int deleteManualServiceSessionsByIds(Long[] ids);
/**
* 删除转人工服务会话信息
*
* @param id 转人工服务会话ID
* @return 结果
*/
public int deleteManualServiceSessionsById(Long id);
}

View File

@ -0,0 +1,394 @@
package com.ruoyi.customer.service.impl;
import java.util.List;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.system.mapper.ChatHistoryMapper;
import com.ruoyi.system.mapper.UserSessionsMapper;
import com.ruoyi.system.mapper.ManualServiceSessionsMapper;
import com.ruoyi.system.domain.ChatHistory;
import com.ruoyi.system.domain.UserSessions;
import com.ruoyi.system.domain.ManualServiceSessions;
import com.ruoyi.customer.service.ICustomerServiceService;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.core.text.Convert;
/**
* 客服系统Service业务层处理
*
* @author ruoyi
* @date 2024-01-01
*/
@Service
public class CustomerServiceServiceImpl implements ICustomerServiceService
{
@Autowired
private UserSessionsMapper userSessionsMapper;
@Autowired
private ChatHistoryMapper chatHistoryMapper;
@Autowired
private ManualServiceSessionsMapper manualServiceSessionsMapper;
/**
* 查询用户会话
*
* @param sessionId 用户会话ID
* @return 用户会话
*/
@Override
public UserSessions selectUserSessionsById(Long sessionId)
{
return userSessionsMapper.selectUserSessionsById(sessionId);
}
/**
* 查询用户会话列表
*
* @param userSessions 用户会话
* @return 用户会话
*/
@Override
public List<UserSessions> selectUserSessionsList(UserSessions userSessions)
{
return userSessionsMapper.selectUserSessionsList(userSessions);
}
/**
* 获取活跃会话列表
*
* @return 活跃会话集合
*/
@Override
public List<UserSessions> getActiveSessions()
{
UserSessions userSessions = new UserSessions();
userSessions.setStatus("active");
return userSessionsMapper.selectUserSessionsList(userSessions);
}
/**
* 新增用户会话
*
* @param userSessions 用户会话
* @return 结果
*/
@Override
public int insertUserSessions(UserSessions userSessions)
{
userSessions.setCreateTime(DateUtils.getNowDate());
return userSessionsMapper.insertUserSessions(userSessions);
}
/**
* 修改用户会话
*
* @param userSessions 用户会话
* @return 结果
*/
@Override
public int updateUserSessions(UserSessions userSessions)
{
userSessions.setUpdateTime(DateUtils.getNowDate());
return userSessionsMapper.updateUserSessions(userSessions);
}
/**
* 删除用户会话对象
*
* @param sessionIds 需要删除的数据ID
* @return 结果
*/
@Override
public int deleteUserSessionsByIds(Long[] sessionIds)
{
return userSessionsMapper.deleteUserSessionsByIds(sessionIds);
}
/**
* 删除用户会话信息
*
* @param sessionId 用户会话ID
* @return 结果
*/
@Override
public int deleteUserSessionsById(Long sessionId)
{
return userSessionsMapper.deleteUserSessionsById(sessionId);
}
/**
* 根据会话ID获取聊天记录
*
* @param sessionId 会话ID
* @return 聊天记录列表
*/
@Override
public List<ChatHistory> getChatHistoryBySessionId(Long sessionId)
{
ChatHistory chatHistory = new ChatHistory();
chatHistory.setSessionId(sessionId);
return chatHistoryMapper.selectChatHistoryList(chatHistory);
}
/**
* 发送消息
*
* @param chatHistory 聊天记录
* @return 结果
*/
@Override
public int sendMessage(ChatHistory chatHistory)
{
chatHistory.setCreateTime(DateUtils.getNowDate());
return chatHistoryMapper.insertChatHistory(chatHistory);
}
/**
* 创建转人工服务请求
*
* @param manualServiceSessions 转人工服务会话
* @return 结果
*/
@Override
public int createManualServiceRequest(ManualServiceSessions manualServiceSessions)
{
manualServiceSessions.setCreateTime(DateUtils.getNowDate());
manualServiceSessions.setStatus("pending");
return manualServiceSessionsMapper.insertManualServiceSessions(manualServiceSessions);
}
/**
* 更新转人工服务请求
*
* @param manualServiceSessions 转人工服务会话
* @return 结果
*/
@Override
public int updateManualServiceRequest(ManualServiceSessions manualServiceSessions)
{
manualServiceSessions.setUpdateTime(DateUtils.getNowDate());
return manualServiceSessionsMapper.updateManualServiceSessions(manualServiceSessions);
}
/**
* 获取待处理的转人工请求
*
* @return 转人工请求列表
*/
@Override
public List<ManualServiceSessions> getPendingManualRequests()
{
ManualServiceSessions manualServiceSessions = new ManualServiceSessions();
manualServiceSessions.setStatus("pending");
return manualServiceSessionsMapper.selectManualServiceSessionsList(manualServiceSessions);
}
/**
* 更新客服状态
*
* @param serviceId 客服ID
* @param status 状态
* @return 结果
*/
@Override
public int updateServiceStatus(Long serviceId, String status)
{
// 这里可以扩展为更新sys_user_online表或其他相关表
return 1;
}
/**
* 获取今日会话数量
*
* @return 会话数量
*/
@Override
public int getTodaySessionCount()
{
return 0;
}
/**
* 获取今日消息数量
*
* @return 消息数量
*/
@Override
public int getTodayMessageCount()
{
return 0;
}
/**
* 获取待处理请求数量
*
* @return 请求数量
*/
@Override
public int getPendingRequestCount()
{
return 0;
}
/**
* 获取在线客服数量
*
* @return 客服数量
*/
@Override
public int getOnlineServiceCount()
{
// 这里可以查询sys_user_online表获取在线客服数量
return 5; // 临时返回固定值
}
/**
* 查询聊天记录
*
* @param messageId 聊天记录ID
* @return 聊天记录
*/
@Override
public ChatHistory selectChatHistoryById(Long messageId)
{
return chatHistoryMapper.selectChatHistoryById(messageId);
}
/**
* 查询聊天记录列表
*
* @param chatHistory 聊天记录
* @return 聊天记录
*/
@Override
public List<ChatHistory> selectChatHistoryList(ChatHistory chatHistory)
{
return chatHistoryMapper.selectChatHistoryList(chatHistory);
}
/**
* 新增聊天记录
*
* @param chatHistory 聊天记录
* @return 结果
*/
@Override
public int insertChatHistory(ChatHistory chatHistory)
{
chatHistory.setCreateTime(DateUtils.getNowDate());
return chatHistoryMapper.insertChatHistory(chatHistory);
}
/**
* 修改聊天记录
*
* @param chatHistory 聊天记录
* @return 结果
*/
@Override
public int updateChatHistory(ChatHistory chatHistory)
{
return chatHistoryMapper.updateChatHistory(chatHistory);
}
/**
* 删除聊天记录对象
*
* @param messageIds 需要删除的数据ID
* @return 结果
*/
@Override
public int deleteChatHistoryByIds(Long[] messageIds)
{
return chatHistoryMapper.deleteChatHistoryByIds(messageIds);
}
/**
* 删除聊天记录信息
*
* @param messageId 聊天记录ID
* @return 结果
*/
@Override
public int deleteChatHistoryById(Long messageId)
{
return chatHistoryMapper.deleteChatHistoryById(messageId);
}
/**
* 查询转人工服务会话
*
* @param id 转人工服务会话ID
* @return 转人工服务会话
*/
@Override
public ManualServiceSessions selectManualServiceSessionsById(Long id)
{
return manualServiceSessionsMapper.selectManualServiceSessionsById(id);
}
/**
* 查询转人工服务会话列表
*
* @param manualServiceSessions 转人工服务会话
* @return 转人工服务会话
*/
@Override
public List<ManualServiceSessions> selectManualServiceSessionsList(ManualServiceSessions manualServiceSessions)
{
return manualServiceSessionsMapper.selectManualServiceSessionsList(manualServiceSessions);
}
/**
* 新增转人工服务会话
*
* @param manualServiceSessions 转人工服务会话
* @return 结果
*/
@Override
public int insertManualServiceSessions(ManualServiceSessions manualServiceSessions)
{
manualServiceSessions.setCreateTime(DateUtils.getNowDate());
return manualServiceSessionsMapper.insertManualServiceSessions(manualServiceSessions);
}
/**
* 修改转人工服务会话
*
* @param manualServiceSessions 转人工服务会话
* @return 结果
*/
@Override
public int updateManualServiceSessions(ManualServiceSessions manualServiceSessions)
{
manualServiceSessions.setUpdateTime(DateUtils.getNowDate());
return manualServiceSessionsMapper.updateManualServiceSessions(manualServiceSessions);
}
/**
* 删除转人工服务会话对象
*
* @param ids 需要删除的数据ID
* @return 结果
*/
@Override
public int deleteManualServiceSessionsByIds(Long[] ids)
{
return manualServiceSessionsMapper.deleteManualServiceSessionsByIds(ids);
}
/**
* 删除转人工服务会话信息
*
* @param id 转人工服务会话ID
* @return 结果
*/
@Override
public int deleteManualServiceSessionsById(Long id)
{
return manualServiceSessionsMapper.deleteManualServiceSessionsById(id);
}
}

View File

@ -1,5 +1,7 @@
package com.ruoyi.system.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
@ -18,6 +20,10 @@ public class ChatHistory extends BaseEntity
/** 主键ID */
private Long id;
/** 消息ID */
@Excel(name = "消息ID")
private Long messageId;
/** 用户ID */
@Excel(name = "用户ID")
private String userId;
@ -26,6 +32,10 @@ public class ChatHistory extends BaseEntity
@Excel(name = "会话ID")
private String sessionId;
/** 会话IDLong类型兼容customer包*/
@Excel(name = "会话ID")
private Long sessionIdLong;
/** 消息类型 */
@Excel(name = "消息类型", readConverterExp = "user=用户消息,service=客服消息")
private String messageType;
@ -34,6 +44,10 @@ public class ChatHistory extends BaseEntity
@Excel(name = "消息内容")
private String content;
/** 消息内容兼容customer包字段名*/
@Excel(name = "消息内容")
private String messageContent;
/** 是否包含链接 */
@Excel(name = "是否包含链接", readConverterExp = "0=否,1=是")
private Integer isLink;
@ -42,6 +56,27 @@ public class ChatHistory extends BaseEntity
@Excel(name = "是否为按钮消息", readConverterExp = "0=否,1=是")
private Integer isButton;
/** 发送者ID */
@Excel(name = "发送者ID")
private Long senderId;
/** 接收者ID */
@Excel(name = "接收者ID")
private Long receiverId;
/** 发送时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "发送时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date sentAt;
/** 是否已读 */
@Excel(name = "是否已读")
private String isRead;
/** 发送者类型 */
@Excel(name = "发送者类型")
private String senderType;
public void setId(Long id)
{
this.id = id;
@ -105,18 +140,113 @@ public class ChatHistory extends BaseEntity
{
return isButton;
}
public void setSenderId(Long senderId)
{
this.senderId = senderId;
}
public Long getSenderId()
{
return senderId;
}
public void setReceiverId(Long receiverId)
{
this.receiverId = receiverId;
}
public Long getReceiverId()
{
return receiverId;
}
public void setSentAt(Date sentAt)
{
this.sentAt = sentAt;
}
public Date getSentAt()
{
return sentAt;
}
public void setIsRead(String isRead)
{
this.isRead = isRead;
}
public String getIsRead()
{
return isRead;
}
public void setMessageId(Long messageId)
{
this.messageId = messageId;
}
public Long getMessageId()
{
return messageId;
}
public void setSessionIdLong(Long sessionIdLong)
{
this.sessionIdLong = sessionIdLong;
}
public Long getSessionIdLong()
{
return sessionIdLong;
}
public void setMessageContent(String messageContent)
{
this.messageContent = messageContent;
}
public String getMessageContent()
{
return messageContent != null ? messageContent : content;
}
public void setSenderType(String senderType)
{
this.senderType = senderType;
}
public String getSenderType()
{
return senderType;
}
// 兼容性方法支持Long类型的sessionId
public void setSessionId(Long sessionId)
{
this.sessionIdLong = sessionId;
this.sessionId = sessionId != null ? sessionId.toString() : null;
}
// 兼容性方法同时设置content和messageContent
public void setMessage(String message)
{
this.content = message;
this.messageContent = message;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("messageId", getMessageId())
.append("userId", getUserId())
.append("sessionId", getSessionId())
.append("sessionIdLong", getSessionIdLong())
.append("messageType", getMessageType())
.append("content", getContent())
.append("messageContent", getMessageContent())
.append("isLink", getIsLink())
.append("isButton", getIsButton())
.append("senderId", getSenderId())
.append("receiverId", getReceiverId())
.append("sentAt", getSentAt())
.append("isRead", getIsRead())
.append("senderType", getSenderType())
.append("createTime", getCreateTime())
.append("updateTime", getUpdateTime())
.toString();
}
}

View File

@ -0,0 +1,184 @@
package com.ruoyi.system.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 转人工服务会话对象 manual_service_sessions
*
* @author ruoyi
* @date 2024-01-01
*/
public class ManualServiceSessions extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 转人工会话ID */
private Long manualSessionId;
/** 原会话ID */
@Excel(name = "原会话ID")
private Long sessionId;
/** 用户ID */
@Excel(name = "用户ID")
private Long userId;
/** 客服ID */
@Excel(name = "客服ID")
private Long serviceId;
/** 转人工请求时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "转人工请求时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date requestTime;
/** 接受时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "接受时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date acceptTime;
/** 结束时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
/** 状态 */
@Excel(name = "状态")
private String status;
/** 转人工原因 */
@Excel(name = "转人工原因")
private String reason;
/** 满意度评分 */
@Excel(name = "满意度评分")
private Integer rating;
/** 评价内容 */
@Excel(name = "评价内容")
private String feedback;
public void setManualSessionId(Long manualSessionId)
{
this.manualSessionId = manualSessionId;
}
public Long getManualSessionId()
{
return manualSessionId;
}
public void setSessionId(Long sessionId)
{
this.sessionId = sessionId;
}
public Long getSessionId()
{
return sessionId;
}
public void setUserId(Long userId)
{
this.userId = userId;
}
public Long getUserId()
{
return userId;
}
public void setServiceId(Long serviceId)
{
this.serviceId = serviceId;
}
public Long getServiceId()
{
return serviceId;
}
public void setRequestTime(Date requestTime)
{
this.requestTime = requestTime;
}
public Date getRequestTime()
{
return requestTime;
}
public void setAcceptTime(Date acceptTime)
{
this.acceptTime = acceptTime;
}
public Date getAcceptTime()
{
return acceptTime;
}
public void setEndTime(Date endTime)
{
this.endTime = endTime;
}
public Date getEndTime()
{
return endTime;
}
public void setStatus(String status)
{
this.status = status;
}
public String getStatus()
{
return status;
}
public void setReason(String reason)
{
this.reason = reason;
}
public String getReason()
{
return reason;
}
public void setRating(Integer rating)
{
this.rating = rating;
}
public Integer getRating()
{
return rating;
}
public void setFeedback(String feedback)
{
this.feedback = feedback;
}
public String getFeedback()
{
return feedback;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("manualSessionId", getManualSessionId())
.append("sessionId", getSessionId())
.append("userId", getUserId())
.append("serviceId", getServiceId())
.append("requestTime", getRequestTime())
.append("acceptTime", getAcceptTime())
.append("endTime", getEndTime())
.append("status", getStatus())
.append("reason", getReason())
.append("rating", getRating())
.append("feedback", getFeedback())
.append("createTime", getCreateTime())
.append("updateTime", getUpdateTime())
.toString();
}
}

View File

@ -0,0 +1,114 @@
package com.ruoyi.system.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 用户会话对象 user_sessions
*
* @author ruoyi
* @date 2024-01-01
*/
public class UserSessions extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 会话ID */
private Long sessionId;
/** 用户ID */
@Excel(name = "用户ID")
private Long userId;
/** 会话开始时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "会话开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date sessionStart;
/** 会话结束时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "会话结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date sessionEnd;
/** 会话状态 */
@Excel(name = "会话状态")
private String status;
/** 最后活动时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "最后活动时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date lastActivity;
public void setSessionId(Long sessionId)
{
this.sessionId = sessionId;
}
public Long getSessionId()
{
return sessionId;
}
public void setUserId(Long userId)
{
this.userId = userId;
}
public Long getUserId()
{
return userId;
}
public void setSessionStart(Date sessionStart)
{
this.sessionStart = sessionStart;
}
public Date getSessionStart()
{
return sessionStart;
}
public void setSessionEnd(Date sessionEnd)
{
this.sessionEnd = sessionEnd;
}
public Date getSessionEnd()
{
return sessionEnd;
}
public void setStatus(String status)
{
this.status = status;
}
public String getStatus()
{
return status;
}
public void setLastActivity(Date lastActivity)
{
this.lastActivity = lastActivity;
}
public Date getLastActivity()
{
return lastActivity;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("sessionId", getSessionId())
.append("userId", getUserId())
.append("sessionStart", getSessionStart())
.append("sessionEnd", getSessionEnd())
.append("status", getStatus())
.append("lastActivity", getLastActivity())
.append("createTime", getCreateTime())
.append("updateTime", getUpdateTime())
.toString();
}
}

View File

@ -92,10 +92,58 @@ public interface ChatHistoryMapper
public int deleteChatHistoryBySessionId(String sessionId);
/**
* 删除指定用户的聊天记录
* 根据用户ID删除聊天记录
*
* @param userId 用户ID
* @return 结果
*/
public int deleteChatHistoryByUserId(String userId);
/**
* 根据消息ID查询聊天记录
*
* @param messageId 消息ID
* @return 聊天记录
*/
public ChatHistory selectChatHistoryByMessageId(String messageId);
/**
* 根据发送者ID查询聊天记录列表
*
* @param senderId 发送者ID
* @return 聊天记录集合
*/
public List<ChatHistory> selectChatHistoryBySenderId(String senderId);
/**
* 根据接收者ID查询聊天记录列表
*
* @param receiverId 接收者ID
* @return 聊天记录集合
*/
public List<ChatHistory> selectChatHistoryByReceiverId(String receiverId);
/**
* 根据发送者类型查询聊天记录列表
*
* @param senderType 发送者类型
* @return 聊天记录集合
*/
public List<ChatHistory> selectChatHistoryBySenderType(String senderType);
/**
* 查询未读聊天记录列表
*
* @param userId 用户ID可选
* @return 聊天记录集合
*/
public List<ChatHistory> selectUnreadChatHistory(String userId);
/**
* 根据时间范围查询聊天记录列表
*
* @param params 查询参数包含startTimeendTimeuserId等
* @return 聊天记录集合
*/
public List<ChatHistory> selectChatHistoryByTimeRange(java.util.Map<String, Object> params);
}

View File

@ -0,0 +1,61 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.ManualServiceSessions;
/**
* 转人工服务会话Mapper接口
*
* @author ruoyi
* @date 2024-01-01
*/
public interface ManualServiceSessionsMapper
{
/**
* 查询转人工服务会话
*
* @param id 转人工服务会话ID
* @return 转人工服务会话
*/
public ManualServiceSessions selectManualServiceSessionsById(Long id);
/**
* 查询转人工服务会话列表
*
* @param manualServiceSessions 转人工服务会话
* @return 转人工服务会话集合
*/
public List<ManualServiceSessions> selectManualServiceSessionsList(ManualServiceSessions manualServiceSessions);
/**
* 新增转人工服务会话
*
* @param manualServiceSessions 转人工服务会话
* @return 结果
*/
public int insertManualServiceSessions(ManualServiceSessions manualServiceSessions);
/**
* 修改转人工服务会话
*
* @param manualServiceSessions 转人工服务会话
* @return 结果
*/
public int updateManualServiceSessions(ManualServiceSessions manualServiceSessions);
/**
* 删除转人工服务会话
*
* @param id 转人工服务会话ID
* @return 结果
*/
public int deleteManualServiceSessionsById(Long id);
/**
* 批量删除转人工服务会话
*
* @param ids 需要删除的数据ID
* @return 结果
*/
public int deleteManualServiceSessionsByIds(Long[] ids);
}

View File

@ -0,0 +1,61 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.UserSessions;
/**
* 用户会话Mapper接口
*
* @author ruoyi
* @date 2024-01-01
*/
public interface UserSessionsMapper
{
/**
* 查询用户会话
*
* @param sessionId 用户会话ID
* @return 用户会话
*/
public UserSessions selectUserSessionsById(Long sessionId);
/**
* 查询用户会话列表
*
* @param userSessions 用户会话
* @return 用户会话集合
*/
public List<UserSessions> selectUserSessionsList(UserSessions userSessions);
/**
* 新增用户会话
*
* @param userSessions 用户会话
* @return 结果
*/
public int insertUserSessions(UserSessions userSessions);
/**
* 修改用户会话
*
* @param userSessions 用户会话
* @return 结果
*/
public int updateUserSessions(UserSessions userSessions);
/**
* 删除用户会话
*
* @param sessionId 用户会话ID
* @return 结果
*/
public int deleteUserSessionsById(Long sessionId);
/**
* 批量删除用户会话
*
* @param sessionIds 需要删除的数据ID
* @return 结果
*/
public int deleteUserSessionsByIds(Long[] sessionIds);
}

View File

@ -6,17 +6,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="ChatHistory" id="ChatHistoryResult">
<result property="id" column="id" />
<result property="messageId" column="message_id" />
<result property="userId" column="user_id" />
<result property="sessionId" column="session_id" />
<result property="sessionIdLong" column="session_id_long" />
<result property="messageType" column="message_type" />
<result property="content" column="content" />
<result property="messageContent" column="message_content" />
<result property="isLink" column="is_link" />
<result property="isButton" column="is_button" />
<result property="senderId" column="sender_id" />
<result property="receiverId" column="receiver_id" />
<result property="sentAt" column="sent_at" />
<result property="isRead" column="is_read" />
<result property="senderType" column="sender_type" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectChatHistoryVo">
select id, user_id, session_id, message_type, content, is_link, is_button, create_time from chat_history
select id, message_id, user_id, session_id, session_id_long, message_type, content, message_content, is_link, is_button, sender_id, receiver_id, sent_at, is_read, sender_type, create_time, update_time from chat_history
</sql>
<select id="selectChatHistoryList" parameterType="ChatHistory" resultMap="ChatHistoryResult">
@ -49,45 +58,112 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
order by create_time asc
</select>
<select id="selectChatHistoryByMessageId" parameterType="String" resultMap="ChatHistoryResult">
<include refid="selectChatHistoryVo"/>
where message_id = #{messageId}
</select>
<select id="selectChatHistoryBySenderId" parameterType="String" resultMap="ChatHistoryResult">
<include refid="selectChatHistoryVo"/>
where sender_id = #{senderId}
order by create_time asc
</select>
<select id="selectChatHistoryByReceiverId" parameterType="String" resultMap="ChatHistoryResult">
<include refid="selectChatHistoryVo"/>
where receiver_id = #{receiverId}
order by create_time asc
</select>
<select id="selectChatHistoryBySenderType" parameterType="String" resultMap="ChatHistoryResult">
<include refid="selectChatHistoryVo"/>
where sender_type = #{senderType}
order by create_time asc
</select>
<select id="selectUnreadChatHistory" parameterType="String" resultMap="ChatHistoryResult">
<include refid="selectChatHistoryVo"/>
where is_read = 0
<if test="userId != null and userId != ''"> and user_id = #{userId}</if>
order by create_time asc
</select>
<select id="selectChatHistoryByTimeRange" parameterType="map" resultMap="ChatHistoryResult">
<include refid="selectChatHistoryVo"/>
<where>
<if test="startTime != null"> and sent_at &gt;= #{startTime}</if>
<if test="endTime != null"> and sent_at &lt;= #{endTime}</if>
<if test="userId != null and userId != ''"> and user_id = #{userId}</if>
</where>
order by sent_at asc
</select>
<insert id="insertChatHistory" parameterType="ChatHistory" useGeneratedKeys="true" keyProperty="id">
insert into chat_history
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="messageId != null">message_id,</if>
<if test="userId != null">user_id,</if>
<if test="sessionId != null">session_id,</if>
<if test="sessionIdLong != null">session_id_long,</if>
<if test="messageType != null">message_type,</if>
<if test="content != null">content,</if>
<if test="messageContent != null">message_content,</if>
<if test="isLink != null">is_link,</if>
<if test="isButton != null">is_button,</if>
<if test="senderId != null">sender_id,</if>
<if test="receiverId != null">receiver_id,</if>
<if test="sentAt != null">sent_at,</if>
<if test="isRead != null">is_read,</if>
<if test="senderType != null">sender_type,</if>
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="messageId != null">#{messageId},</if>
<if test="userId != null">#{userId},</if>
<if test="sessionId != null">#{sessionId},</if>
<if test="sessionIdLong != null">#{sessionIdLong},</if>
<if test="messageType != null">#{messageType},</if>
<if test="content != null">#{content},</if>
<if test="messageContent != null">#{messageContent},</if>
<if test="isLink != null">#{isLink},</if>
<if test="isButton != null">#{isButton},</if>
<if test="senderId != null">#{senderId},</if>
<if test="receiverId != null">#{receiverId},</if>
<if test="sentAt != null">#{sentAt},</if>
<if test="isRead != null">#{isRead},</if>
<if test="senderType != null">#{senderType},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
</trim>
</insert>
<insert id="batchInsertChatHistory" parameterType="java.util.List">
insert into chat_history (user_id, session_id, message_type, content, is_link, is_button, create_time) values
insert into chat_history (message_id, user_id, session_id, session_id_long, message_type, content, message_content, is_link, is_button, sender_id, receiver_id, sent_at, is_read, sender_type, create_time, update_time) values
<foreach collection="list" item="item" separator=",">
(#{item.userId}, #{item.sessionId}, #{item.messageType}, #{item.content}, #{item.isLink}, #{item.isButton}, #{item.createTime})
(#{item.messageId}, #{item.userId}, #{item.sessionId}, #{item.sessionIdLong}, #{item.messageType}, #{item.content}, #{item.messageContent}, #{item.isLink}, #{item.isButton}, #{item.senderId}, #{item.receiverId}, #{item.sentAt}, #{item.isRead}, #{item.senderType}, #{item.createTime}, #{item.updateTime})
</foreach>
</insert>
<update id="updateChatHistory" parameterType="ChatHistory">
update chat_history
<trim prefix="SET" suffixOverrides=",">
<if test="messageId != null">message_id = #{messageId},</if>
<if test="userId != null">user_id = #{userId},</if>
<if test="sessionId != null">session_id = #{sessionId},</if>
<if test="sessionIdLong != null">session_id_long = #{sessionIdLong},</if>
<if test="messageType != null">message_type = #{messageType},</if>
<if test="content != null">content = #{content},</if>
<if test="messageContent != null">message_content = #{messageContent},</if>
<if test="isLink != null">is_link = #{isLink},</if>
<if test="isButton != null">is_button = #{isButton},</if>
<if test="senderId != null">sender_id = #{senderId},</if>
<if test="receiverId != null">receiver_id = #{receiverId},</if>
<if test="sentAt != null">sent_at = #{sentAt},</if>
<if test="isRead != null">is_read = #{isRead},</if>
<if test="senderType != null">sender_type = #{senderType},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
</trim>
where id = #{id}
</update>

View File

@ -0,0 +1,88 @@
<?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.ruoyi.system.mapper.ManualServiceSessionsMapper">
<resultMap type="ManualServiceSessions" id="ManualServiceSessionsResult">
<result property="id" column="id" />
<result property="sessionId" column="session_id" />
<result property="userId" column="user_id" />
<result property="serviceStaff" column="service_staff" />
<result property="startTime" column="start_time" />
<result property="endTime" column="end_time" />
<result property="status" column="status" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectManualServiceSessionsVo">
select id, session_id, user_id, service_staff, start_time, end_time, status, create_time, update_time from manual_service_sessions
</sql>
<select id="selectManualServiceSessionsList" parameterType="ManualServiceSessions" resultMap="ManualServiceSessionsResult">
<include refid="selectManualServiceSessionsVo"/>
<where>
<if test="sessionId != null "> and session_id = #{sessionId}</if>
<if test="userId != null "> and user_id = #{userId}</if>
<if test="serviceStaff != null and serviceStaff != ''"> and service_staff = #{serviceStaff}</if>
<if test="status != null and status != ''"> and status = #{status}</if>
</where>
</select>
<select id="selectManualServiceSessionsById" parameterType="Long" resultMap="ManualServiceSessionsResult">
<include refid="selectManualServiceSessionsVo"/>
where id = #{id}
</select>
<insert id="insertManualServiceSessions" parameterType="ManualServiceSessions" useGeneratedKeys="true" keyProperty="id">
insert into manual_service_sessions
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="sessionId != null">session_id,</if>
<if test="userId != null">user_id,</if>
<if test="serviceStaff != null">service_staff,</if>
<if test="startTime != null">start_time,</if>
<if test="endTime != null">end_time,</if>
<if test="status != null">status,</if>
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="sessionId != null">#{sessionId},</if>
<if test="userId != null">#{userId},</if>
<if test="serviceStaff != null">#{serviceStaff},</if>
<if test="startTime != null">#{startTime},</if>
<if test="endTime != null">#{endTime},</if>
<if test="status != null">#{status},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
</trim>
</insert>
<update id="updateManualServiceSessions" parameterType="ManualServiceSessions">
update manual_service_sessions
<trim prefix="SET" suffixOverrides=",">
<if test="sessionId != null">session_id = #{sessionId},</if>
<if test="userId != null">user_id = #{userId},</if>
<if test="serviceStaff != null">service_staff = #{serviceStaff},</if>
<if test="startTime != null">start_time = #{startTime},</if>
<if test="endTime != null">end_time = #{endTime},</if>
<if test="status != null">status = #{status},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
</trim>
where id = #{id}
</update>
<delete id="deleteManualServiceSessionsById" parameterType="Long">
delete from manual_service_sessions where id = #{id}
</delete>
<delete id="deleteManualServiceSessionsByIds" parameterType="String">
delete from manual_service_sessions where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>

View File

@ -0,0 +1,75 @@
<?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.ruoyi.system.mapper.UserSessionsMapper">
<resultMap type="UserSessions" id="UserSessionsResult">
<result property="sessionId" column="session_id" />
<result property="userId" column="user_id" />
<result property="sessionToken" column="session_token" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
<result property="status" column="status" />
</resultMap>
<sql id="selectUserSessionsVo">
select session_id, user_id, session_token, create_time, update_time, status from user_sessions
</sql>
<select id="selectUserSessionsList" parameterType="UserSessions" resultMap="UserSessionsResult">
<include refid="selectUserSessionsVo"/>
<where>
<if test="userId != null "> and user_id = #{userId}</if>
<if test="sessionToken != null and sessionToken != ''"> and session_token = #{sessionToken}</if>
<if test="status != null and status != ''"> and status = #{status}</if>
</where>
</select>
<select id="selectUserSessionsById" parameterType="Long" resultMap="UserSessionsResult">
<include refid="selectUserSessionsVo"/>
where session_id = #{sessionId}
</select>
<insert id="insertUserSessions" parameterType="UserSessions" useGeneratedKeys="true" keyProperty="sessionId">
insert into user_sessions
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userId != null">user_id,</if>
<if test="sessionToken != null">session_token,</if>
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
<if test="status != null">status,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userId != null">#{userId},</if>
<if test="sessionToken != null">#{sessionToken},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="status != null">#{status},</if>
</trim>
</insert>
<update id="updateUserSessions" parameterType="UserSessions">
update user_sessions
<trim prefix="SET" suffixOverrides=",">
<if test="userId != null">user_id = #{userId},</if>
<if test="sessionToken != null">session_token = #{sessionToken},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="status != null">status = #{status},</if>
</trim>
where session_id = #{sessionId}
</update>
<delete id="deleteUserSessionsById" parameterType="Long">
delete from user_sessions where session_id = #{sessionId}
</delete>
<delete id="deleteUserSessionsByIds" parameterType="String">
delete from user_sessions where session_id in
<foreach item="sessionId" collection="array" open="(" separator="," close=")">
#{sessionId}
</foreach>
</delete>
</mapper>