通信模块初步实习

This commit is contained in:
aiShuiJiaoDeXioShou 2024-09-11 17:09:26 +08:00
parent 7638324bb9
commit ccabf6c09d
19 changed files with 584 additions and 4 deletions

1
acdr-ui/env/.env vendored
View File

@ -10,6 +10,7 @@ VITE_APP_PUBLIC_BASE=/acdr/
# VITE_SERVER_BASEURL = 'http://47.99.70.12:28184/api'
# VITE_UPLOAD_BASEURL = 'http://47.99.70.12:28184'
VITE_SERVER_BASEURL = 'http://localhost:28184/api'
VITE_WS_BASEURL = 'ws://localhost:28184/api'
VITE_UPLOAD_BASEURL = 'http://localhost:28184'
VITE_OSS_BASEURL = 'http://116.204.119.171:9000/linghe'

View File

@ -0,0 +1,40 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationBarTitleText: '聊天',
},
}
</route>
<template>
<view>
<text>客服聊天系统</text>
<input v-model="message" placeholder="输入消息" />
<button @click="sendMessageToServer">发送</button>
</view>
</template>
<script setup>
import useWebSocket from '@/service/webSocket'
import { ref } from 'vue'
const { connectWebSocket, sendMessage, closeWebSocket } = useWebSocket()
const message = ref('')
const sendMessageToServer = () => {
if (message.value.trim() !== '') {
sendMessage(message.value)
message.value = ''
}
}
onMounted(() => {
connectWebSocket() // WebSocket
})
onUnmounted(() => {
closeWebSocket() // WebSocket
})
</script>

View File

@ -0,0 +1,64 @@
import { useUserStore } from '@/store/user'
export default function useWebSocket() {
const userStore = useUserStore()
let ws = null
const connectWebSocket = () => {
if (!userStore.isLogined) {
console.error("用户未登录,无法建立 WebSocket 连接")
return
}
const token = userStore.userInfo.token
const wsUrl = `${import.meta.env.VITE_WS_BASEURL}/chat/${token}` // 根据你的服务端口和路径
if (ws) {
console.log("WebSocket 已连接")
return
}
ws = new WebSocket(wsUrl)
ws.onopen = () => {
console.log("WebSocket 连接成功")
}
ws.onmessage = (event) => {
const message = event.data
console.log("收到消息:", message)
// 这里可以根据需要处理收到的消息,比如显示在聊天窗口
}
ws.onerror = (error) => {
console.error("WebSocket 错误:", error)
}
ws.onclose = () => {
console.log("WebSocket 连接关闭")
ws = null
}
}
const sendMessage = (message) => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
console.error("WebSocket 尚未连接,无法发送消息")
return
}
ws.send(message)
}
const closeWebSocket = () => {
if (ws) {
ws.close()
ws = null
}
}
return {
connectWebSocket,
sendMessage,
closeWebSocket,
}
}

View File

@ -74,7 +74,7 @@ public class TableToEntityConstructor {
}
public static void main(String[] args) {
AutoTable("personal", "", "", "",
"acdr_service_name");
AutoTable("chat", "", "", "",
"acdr_chat_message");
}
}

View File

@ -0,0 +1,14 @@
package com.yskj.acdr.config;
import com.yskj.acdr.utils.SpringContextHolder;
import jakarta.websocket.server.ServerEndpointConfig;
public class MyEndpointConfigurator extends ServerEndpointConfig.Configurator {
@Override
public <T> T getEndpointInstance(Class<T> clazz) throws InstantiationException {
return SpringContextHolder.getBean(clazz); // Spring 上下文中获取 WebSocket 类的实例
}
}

View File

@ -44,6 +44,7 @@ public class WebConfig implements WebMvcConfigurer {
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
.addPathPatterns("/**")
.excludePathPatterns("/chat/**")
.excludePathPatterns("/others/**")
.excludePathPatterns("/public/**")
.excludePathPatterns("/profile/**")

View File

@ -0,0 +1,15 @@
package com.yskj.acdr.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
// WebSocket 端点自动注册
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

View File

@ -0,0 +1,21 @@
package com.yskj.acdr.master.chat.chatenum;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
@Getter
public enum ChatState {
READ(1, "已读"),
NO_READ(0, "未读");
@EnumValue
private final int value;
@JsonValue
private final String info;
ChatState(int value, String info) {
this.value = value;
this.info = info;
}
}

View File

@ -0,0 +1,72 @@
package com.yskj.acdr.master.chat.controller;
import com.yskj.acdr.master.chat.entity.ChatMessage;
import com.yskj.acdr.master.chat.service.ChatMessageService;
import com.yskj.acdr.common.response.GlobalResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.groups.Default;
import org.springframework.validation.annotation.Validated;
import java.util.List;
/**
* <p>
* 前端控制器
* </p>
*
* @author 林河
* @since 2024-09-11
*/
@Api(tags = "通信模块控制器")
@RestController
@RequestMapping("/chatMessage")
public class ChatMessageController {
@Resource
private ChatMessageService service;
@ApiOperation(value = "分页列表", response = ChatMessage.class)
@PostMapping(value = "/page")
public GlobalResponse<ChatMessage> list(GlobalResponse<ChatMessage> page) {
return service.lambdaQuery().page(page);
}
@ApiOperation(value = "详情", response = ChatMessage.class)
@GetMapping(value = "/info/{id}")
public GlobalResponse<ChatMessage> info(@Validated({GetMapping.class}) @PathVariable Long id) {
ChatMessage chatMessage = service.getById(id);
return GlobalResponse.success(chatMessage);
}
@ApiOperation(value = "新增")
@PostMapping(value = "/add")
public GlobalResponse<ChatMessage> add(@Validated({PostMapping.class, Default.class}) @RequestBody ChatMessage param) {
service.save(param);
return GlobalResponse.success("新增成功!");
}
@ApiOperation(value = "修改")
@PostMapping(value = "/modify")
public GlobalResponse<ChatMessage> modify(@Validated({PutMapping.class, Default.class}) @RequestBody ChatMessage param) {
service.updateById(param);
return GlobalResponse.success("修改成功!");
}
@ApiOperation(value = "删除(单个条目)")
@GetMapping(value = "/remove/{id}")
public GlobalResponse<ChatMessage> remove(@Validated({DeleteMapping.class}) @PathVariable Long id) {
service.removeById(id);
return GlobalResponse.success("删除(单个条目)");
}
@ApiOperation(value = "删除(多个条目)")
@PostMapping(value = "/removes")
public GlobalResponse<ChatMessage> removes(@RequestBody List<Long> ids) {
service.removeBatchByIds(ids);
return GlobalResponse.success("删除(多个条目)");
}
}

View File

@ -0,0 +1,164 @@
package com.yskj.acdr.master.chat.controller;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaFoxUtil;
import com.yskj.acdr.common.cache.GlobalRedisCache;
import com.yskj.acdr.config.MyEndpointConfigurator;
import com.yskj.acdr.master.chat.chatenum.ChatState;
import com.yskj.acdr.master.chat.entity.ChatMessage;
import com.yskj.acdr.master.chat.entity.WSRes;
import com.yskj.acdr.master.chat.service.ChatMessageService;
import com.yskj.acdr.master.user.entity.Users;
import com.yskj.acdr.master.user.service.UsersService;
import jakarta.annotation.Resource;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 客服系统或者是宠托师聊天功能实现
*/
@ServerEndpoint(value = "/chat/{token}", configurator = MyEndpointConfigurator.class)
@Component
@Slf4j
public class ChatServerEndpoint {
private Session session;
@Resource
private UsersService usersService;
@Resource
private GlobalRedisCache<String> redisCache;
@Resource
private ChatMessageService cms;
@Resource
private PlatformTransactionManager transactionManager; // 注入 PlatformTransactionManager
private static final Map<Long, Session> sessionMap = new ConcurrentHashMap<>();
private Long userId;
/**
* 收到客户端消息后调用的方法
*/
@OnMessage
public void onMessage(String message, Session session) throws IOException {
try {
// 处理消息并对服务进行消息转发
Map<String, List<String>> parameter = session.getRequestParameterMap();
List<String> userIds = parameter.get("toUserId");
if (!userIds.isEmpty()) {
// 获取转发消息的toUserId
String toUserId = userIds.getFirst();
Session toSession = sessionMap.get(Long.parseLong(toUserId));
// 创建 TransactionDefinition 定义事务的属性
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 开启事务
TransactionStatus status = transactionManager.getTransaction(def);
try {
ChatMessage chatMessage = new ChatMessage()
.setUserId(userId)
.setBeUserId(Long.parseLong(toUserId))
.setState(ChatState.NO_READ)
.setContent(message);
// 保存消息到数据库
boolean save = cms.save(chatMessage);
if (save) {
// 提交事务
transactionManager.commit(status);
// 如果对方在线则发送消息
if (toSession != null) {
sendText(toSession, WSRes.success(chatMessage));
}
} else {
// 如果保存失败回滚事务
transactionManager.rollback(status);
sendText(session, WSRes.error("发送消息失败,请检查网络连接!"));
}
} catch (Exception e) {
// 在发生任何异常时回滚事务
transactionManager.rollback(status);
log.error(e.getMessage());
sendText(session, WSRes.error("发送消息失败,请检查网络连接!"));
}
}
} catch (Exception e) {
log.error(e.getMessage());
sendText(session, WSRes.error("发送消息失败,请检查网络连接!"));
}
}
/**
* 连接建立成功时调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) throws IOException {
this.session = session;
// 使用 Sa-Token 进行登录检查
try {
Object loginIdByToken = StpUtil.getLoginIdByToken(token);
if (loginIdByToken == null) {
sendText(session, WSRes.error("未登录或登录已过期,连接将关闭"));
session.close();
return;
}
userId = SaFoxUtil.getValueByType(loginIdByToken, long.class);
// 如果登录有效
Users users = usersService.getById(userId);
sendText(session, WSRes.success("尊敬的%s您好连接成功欢迎进入客服系统".formatted(users.getNickname())));
sessionMap.put(userId, session);
} catch (Exception e) {
sendText(session, WSRes.error("未登录或登录已过期,连接将关闭"));
session.close();
}
}
// 发送信息方法
private void sendText(Session session, String text) {
try {
session.getBasicRemote().sendText(text);
} catch (IOException e) {
log.error(e.getMessage());
}
}
/**
* 连接关闭时调用的方法
*/
@OnClose
public void onClose() {
if (userId != null) {
sessionMap.remove(userId);
}
}
/**
* 发生错误时调用的方法
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
}

View File

@ -0,0 +1,56 @@
package com.yskj.acdr.master.chat.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.yskj.acdr.master.chat.chatenum.ChatState;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
*
* </p>
*
* @author 林河
* @since 2024-09-11
*/
@Getter
@Setter
@ToString
@Accessors(chain = true)
@TableName("acdr_chat_message")
@ApiModel(value = "ChatMessage对象", description = "")
public class ChatMessage implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("消息主键")
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@ApiModelProperty("这个用于储存html格式的数据")
@TableField("content")
private String content;
@ApiModelProperty("发送信息用户的id")
@TableField(value = "user_id", fill = FieldFill.INSERT)
private Long userId;
@ApiModelProperty("通信用户的id")
@TableField("be_user_id")
private Long beUserId;
@ApiModelProperty("创建时间")
@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;
@ApiModelProperty("消息状态,0未读1已读")
@TableField(value = "state")
private ChatState state;
}

View File

@ -0,0 +1,50 @@
package com.yskj.acdr.master.chat.entity;
import com.alibaba.fastjson2.JSON;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 返回给前端的消息类
*/
@Accessors(chain = true)
@Data
public class WSRes {
// 默认为成功返回的消息
private int code = 1;
// 错误信息
private String message = "";
// 返回消息体
private String content;
public static String success(String message, String data) {
return JSON.toJSONString(new WSRes()
.setMessage(message)
.setContent(data));
}
public static String success(String message) {
return success(message, "");
}
public static <T> String success(T data) {
return success("", JSON.toJSONString(data));
}
public static <T> String success(String message, T data) {
return success(message, JSON.toJSONString(data));
}
public static String error(String errorMsg) {
return JSON.toJSONString(new WSRes()
.setMessage(errorMsg)
.setCode(0));
}
public static String error() {
return error("通信失败!");
}
}

View File

@ -0,0 +1,18 @@
package com.yskj.acdr.master.chat.mapper;
import com.yskj.acdr.master.chat.entity.ChatMessage;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* <p>
* Mapper 接口
* </p>
*
* @author 林河
* @since 2024-09-11
*/
@Mapper
public interface ChatMessageMapper extends BaseMapper<ChatMessage> {
}

View File

@ -0,0 +1,5 @@
<?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.yskj.acdr.master.chat.mapper.ChatMessageMapper">
</mapper>

View File

@ -0,0 +1,16 @@
package com.yskj.acdr.master.chat.service;
import com.yskj.acdr.master.chat.entity.ChatMessage;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* <p>
* 服务类
* </p>
*
* @author 林河
* @since 2024-09-11
*/
public interface ChatMessageService extends IService<ChatMessage> {
}

View File

@ -0,0 +1,20 @@
package com.yskj.acdr.master.chat.service.impl;
import com.yskj.acdr.master.chat.entity.ChatMessage;
import com.yskj.acdr.master.chat.mapper.ChatMessageMapper;
import com.yskj.acdr.master.chat.service.ChatMessageService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* <p>
* 服务实现类
* </p>
*
* @author 林河
* @since 2024-09-11
*/
@Service
public class ChatMessageServiceImpl extends ServiceImpl<ChatMessageMapper, ChatMessage> implements ChatMessageService {
}

View File

@ -0,0 +1,21 @@
package com.yskj.acdr.utils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringContextHolder implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext context) {
applicationContext = context;
}
public static <T> T getBean(Class<T> requiredType) {
return applicationContext.getBean(requiredType);
}
}

View File

@ -14,6 +14,7 @@ server:
encoding: UTF-8
max-days: 7
directory: ${path.logs}tomcat\
max-http-request-header-size: 8192
logging:
charset:
@ -29,7 +30,7 @@ spring:
date-format: yyyy-MM-dd
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/acdr?useSSL=FALSE&serverTimezone=Asia/Shanghai&useOldAliasMetadataBehavior=true&rewriteBatchedStatements=true
url: jdbc:mysql://127.0.0.1:3306/cwet?useSSL=FALSE&serverTimezone=Asia/Shanghai&useOldAliasMetadataBehavior=true&rewriteBatchedStatements=true
username: root
password: root
# HikariCP连接池

View File

@ -14,6 +14,7 @@ server:
encoding: UTF-8
max-days: 7
directory: ${path.logs}tomcat\
max-http-request-header-size: 8192
logging:
charset:
@ -27,7 +28,7 @@ logging:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/acdr?useSSL=FALSE&serverTimezone=Asia/Shanghai&useOldAliasMetadataBehavior=true&rewriteBatchedStatements=true
url: jdbc:mysql://127.0.0.1:3306/cwet?useSSL=FALSE&serverTimezone=Asia/Shanghai&useOldAliasMetadataBehavior=true&rewriteBatchedStatements=true
username: root
password: root
# HikariCP连接池