feat: 完成Nacos配置优化和WebSocket集成
主要更新: 1. 统一所有微服务端口配置(19000-19008) 2. 为所有服务创建本地/测试/生产三套环境配置 3. 配置Nacos认证密码(本地:Peanut2817*#, 测试/生产:EmotionMuseum2025) 4. 优化网关路由配置,支持负载均衡和WebSocket 5. 新增emotion-websocket模块,支持实时聊天 6. 前端集成WebSocket,替代HTTP轮询 7. 添加配置验证和管理工具脚本 技术特性: - 完整的环境隔离和服务发现 - WebSocket实时通信支持 - 负载均衡路由配置 - 跨域和安全配置 - 自动重连和心跳检测
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
FROM openjdk:17-jdk-slim
|
||||
|
||||
LABEL maintainer="emotion-museum"
|
||||
|
||||
# 设置时区
|
||||
ENV TZ=Asia/Shanghai
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
# 创建应用目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制jar文件
|
||||
COPY target/emotion-websocket-1.0.0.jar app.jar
|
||||
|
||||
# 创建日志目录
|
||||
RUN mkdir -p /app/logs
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 19007
|
||||
|
||||
# 启动应用
|
||||
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]
|
||||
@@ -0,0 +1,268 @@
|
||||
# Emotion WebSocket 聊天服务
|
||||
|
||||
## 概述
|
||||
|
||||
emotion-websocket 是情绪博物馆项目的WebSocket聊天微服务,提供实时聊天功能,支持用户与AI的实时对话。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ WebSocket实时通信
|
||||
- ✅ 用户与AI实时对话
|
||||
- ✅ 会话管理
|
||||
- ✅ 消息状态跟踪
|
||||
- ✅ 心跳检测
|
||||
- ✅ 在线用户管理
|
||||
- ✅ 消息广播
|
||||
- ✅ 异步AI响应处理
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Spring Boot 3.0.2
|
||||
- Spring WebSocket
|
||||
- STOMP协议
|
||||
- SockJS
|
||||
- Spring Cloud Alibaba
|
||||
- Nacos服务发现
|
||||
- OpenFeign服务调用
|
||||
- MyBatis Plus
|
||||
- MySQL
|
||||
- Redis
|
||||
|
||||
## 端口配置
|
||||
|
||||
- 服务端口: 19007
|
||||
- WebSocket端点: `/ws/chat`
|
||||
|
||||
## API接口
|
||||
|
||||
### WebSocket端点
|
||||
|
||||
```
|
||||
ws://localhost:19007/ws/chat
|
||||
```
|
||||
|
||||
### STOMP消息映射
|
||||
|
||||
- `/app/chat.send` - 发送聊天消息
|
||||
- `/app/chat.connect` - 用户连接
|
||||
- `/app/chat.disconnect` - 用户断开连接
|
||||
- `/app/chat.heartbeat` - 心跳检测
|
||||
|
||||
### 订阅端点
|
||||
|
||||
- `/user/queue/messages` - 用户私有消息
|
||||
- `/topic/conversation/{conversationId}` - 会话消息
|
||||
- `/topic/broadcast` - 广播消息
|
||||
|
||||
### REST API
|
||||
|
||||
#### 发送测试消息
|
||||
```http
|
||||
POST /websocket/send?userId={userId}&message={message}
|
||||
```
|
||||
|
||||
#### 广播测试消息
|
||||
```http
|
||||
POST /websocket/broadcast?message={message}
|
||||
```
|
||||
|
||||
#### 获取在线用户
|
||||
```http
|
||||
GET /websocket/online-users
|
||||
```
|
||||
|
||||
## 消息格式
|
||||
|
||||
### 聊天请求 (ChatRequest)
|
||||
```json
|
||||
{
|
||||
"conversationId": "会话ID",
|
||||
"content": "消息内容",
|
||||
"senderId": "发送者ID",
|
||||
"senderType": "USER|GUEST|AI|SYSTEM",
|
||||
"messageType": "TEXT|TYPING|SYSTEM|ERROR|HEARTBEAT|CONNECTION|AI_THINKING"
|
||||
}
|
||||
```
|
||||
|
||||
### WebSocket消息 (WebSocketMessage)
|
||||
```json
|
||||
{
|
||||
"messageId": "消息ID",
|
||||
"conversationId": "会话ID",
|
||||
"type": "TEXT|TYPING|SYSTEM|ERROR|HEARTBEAT|CONNECTION|AI_THINKING",
|
||||
"content": "消息内容",
|
||||
"senderId": "发送者ID",
|
||||
"senderType": "USER|GUEST|AI|SYSTEM",
|
||||
"status": "SENDING|SENT|DELIVERED|READ|FAILED",
|
||||
"createTime": "2025-07-17 15:30:00",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
## 启动方式
|
||||
|
||||
### 本地开发启动
|
||||
|
||||
1. 确保MySQL和Redis服务已启动
|
||||
2. 确保Nacos服务已启动
|
||||
3. 启动emotion-ai服务(WebSocket服务依赖AI服务)
|
||||
|
||||
```bash
|
||||
# 进入项目根目录
|
||||
cd backend
|
||||
|
||||
# 启动单个服务
|
||||
cd emotion-websocket
|
||||
mvn spring-boot:run -Dspring-boot.run.profiles=local
|
||||
|
||||
# 或使用统一启动脚本
|
||||
./start-services.sh
|
||||
```
|
||||
|
||||
### Docker启动
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
cd emotion-websocket
|
||||
docker build -t emotion-websocket:1.0.0 .
|
||||
|
||||
# 运行容器
|
||||
docker run -d \
|
||||
--name emotion-websocket \
|
||||
-p 19007:19007 \
|
||||
-e SPRING_PROFILES_ACTIVE=prod \
|
||||
emotion-websocket:1.0.0
|
||||
```
|
||||
|
||||
## 测试方法
|
||||
|
||||
### 1. 使用内置测试页面
|
||||
|
||||
访问: http://localhost:19007/websocket-test.html
|
||||
|
||||
### 2. 使用JavaScript客户端
|
||||
|
||||
```javascript
|
||||
// 连接WebSocket
|
||||
const socket = new SockJS('http://localhost:19007/ws/chat');
|
||||
const stompClient = Stomp.over(socket);
|
||||
|
||||
stompClient.connect({}, function (frame) {
|
||||
console.log('Connected: ' + frame);
|
||||
|
||||
// 订阅消息
|
||||
stompClient.subscribe('/user/queue/messages', function (message) {
|
||||
const messageData = JSON.parse(message.body);
|
||||
console.log('Received:', messageData);
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
const chatRequest = {
|
||||
content: "Hello AI!",
|
||||
senderId: "test-user",
|
||||
senderType: "USER",
|
||||
messageType: "TEXT",
|
||||
conversationId: "test-conversation"
|
||||
};
|
||||
|
||||
stompClient.send("/app/chat.send", {}, JSON.stringify(chatRequest));
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 使用REST API测试
|
||||
|
||||
```bash
|
||||
# 发送测试消息
|
||||
curl -X POST "http://localhost:19007/websocket/send?userId=test-user&message=Hello"
|
||||
|
||||
# 广播消息
|
||||
curl -X POST "http://localhost:19007/websocket/broadcast?message=System Message"
|
||||
|
||||
# 查看在线用户
|
||||
curl -X GET "http://localhost:19007/websocket/online-users"
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### application.yml
|
||||
```yaml
|
||||
server:
|
||||
port: 19007
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-websocket
|
||||
```
|
||||
|
||||
### application-local.yml
|
||||
```yaml
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
```
|
||||
|
||||
## 日志配置
|
||||
|
||||
日志文件位置: `logs/emotion-websocket.log`
|
||||
|
||||
查看日志:
|
||||
```bash
|
||||
tail -f logs/emotion-websocket.log
|
||||
```
|
||||
|
||||
## 监控端点
|
||||
|
||||
- 健康检查: http://localhost:19007/actuator/health
|
||||
- 指标监控: http://localhost:19007/actuator/metrics
|
||||
- Prometheus: http://localhost:19007/actuator/prometheus
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. WebSocket服务依赖emotion-ai服务,请确保AI服务已启动
|
||||
2. 需要配置正确的Nacos服务发现地址
|
||||
3. 确保数据库连接配置正确
|
||||
4. 生产环境需要配置适当的跨域策略
|
||||
5. 建议配置负载均衡和会话粘性
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **连接失败**
|
||||
- 检查服务是否启动: `curl http://localhost:19007/actuator/health`
|
||||
- 检查端口是否被占用: `lsof -i :19007`
|
||||
|
||||
2. **AI回复失败**
|
||||
- 检查emotion-ai服务是否正常
|
||||
- 查看日志中的Feign调用错误
|
||||
|
||||
3. **消息发送失败**
|
||||
- 检查WebSocket连接状态
|
||||
- 查看浏览器控制台错误信息
|
||||
|
||||
### 日志级别调整
|
||||
|
||||
```yaml
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum.websocket: DEBUG
|
||||
org.springframework.web.socket: DEBUG
|
||||
```
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 添加新的消息类型
|
||||
|
||||
1. 在`WebSocketMessage.MessageType`枚举中添加新类型
|
||||
2. 在`ChatWebSocketController`中添加对应的处理方法
|
||||
3. 在`ChatWebSocketServiceImpl`中实现具体逻辑
|
||||
|
||||
### 扩展功能
|
||||
|
||||
- 添加文件传输支持
|
||||
- 实现消息持久化
|
||||
- 添加消息加密
|
||||
- 实现群聊功能
|
||||
- 添加消息撤回功能
|
||||
@@ -0,0 +1,135 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.emotionmuseum</groupId>
|
||||
<artifactId>backend</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>emotion-websocket</artifactId>
|
||||
<name>emotion-websocket</name>
|
||||
<description>WebSocket聊天服务</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- 内部模块依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.emotionmuseum</groupId>
|
||||
<artifactId>emotion-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Cloud Discovery -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-bootstrap</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot DevTools for automatic restart -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- WebSocket -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenFeign -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Druid -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 监控 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 监控指标 -->
|
||||
<dependency>
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 测试数据库 -->
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<configuration>
|
||||
<mainClass>com.emotionmuseum.websocket.WebsocketApplication</mainClass>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.emotionmuseum.websocket;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
/**
|
||||
* WebSocket聊天服务启动类
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableDiscoveryClient
|
||||
@EnableFeignClients(basePackages = "com.emotionmuseum")
|
||||
public class WebsocketApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(WebsocketApplication.class, args);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.emotionmuseum.websocket.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* 异步配置类
|
||||
*/
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig {
|
||||
|
||||
/**
|
||||
* 配置异步任务执行器
|
||||
* @return 任务执行器
|
||||
*/
|
||||
@Bean(name = "taskExecutor")
|
||||
public Executor taskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(10);
|
||||
executor.setMaxPoolSize(50);
|
||||
executor.setQueueCapacity(200);
|
||||
executor.setThreadNamePrefix("websocket-async-");
|
||||
executor.setKeepAliveSeconds(60);
|
||||
executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.emotionmuseum.websocket.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
/**
|
||||
* WebSocket配置类
|
||||
* 用于配置WebSocket消息代理和端点
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
/**
|
||||
* 配置消息代理
|
||||
* @param config 消息代理注册器
|
||||
*/
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
// 启用简单消息代理,用于向客户端发送消息
|
||||
config.enableSimpleBroker("/topic", "/queue");
|
||||
// 设置应用程序目的地前缀,客户端发送消息时使用
|
||||
config.setApplicationDestinationPrefixes("/app");
|
||||
// 设置用户目的地前缀,用于点对点消息
|
||||
config.setUserDestinationPrefix("/user");
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册STOMP端点
|
||||
* @param registry STOMP端点注册器
|
||||
*/
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
// 注册WebSocket端点,允许跨域访问
|
||||
registry.addEndpoint("/ws/chat")
|
||||
.setAllowedOriginPatterns("*")
|
||||
.withSockJS();
|
||||
|
||||
// 注册原生WebSocket端点(不使用SockJS)
|
||||
registry.addEndpoint("/ws/chat")
|
||||
.setAllowedOriginPatterns("*");
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package com.emotionmuseum.websocket.controller;
|
||||
|
||||
import com.emotionmuseum.websocket.dto.ChatRequest;
|
||||
import com.emotionmuseum.websocket.dto.WebSocketMessage;
|
||||
import com.emotionmuseum.websocket.service.ChatWebSocketService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.Payload;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* WebSocket聊天控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class ChatWebSocketController {
|
||||
|
||||
private final ChatWebSocketService chatWebSocketService;
|
||||
|
||||
/**
|
||||
* 处理聊天消息
|
||||
* @param chatRequest 聊天请求
|
||||
* @param headerAccessor 消息头访问器
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
@MessageMapping("/chat.send")
|
||||
public void sendMessage(@Payload ChatRequest chatRequest,
|
||||
SimpMessageHeaderAccessor headerAccessor,
|
||||
Principal principal) {
|
||||
try {
|
||||
log.info("收到WebSocket聊天消息: {}", chatRequest);
|
||||
|
||||
// 获取会话ID
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
|
||||
// 处理聊天消息
|
||||
chatWebSocketService.handleChatMessage(chatRequest, sessionId, principal);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理WebSocket聊天消息失败", e);
|
||||
|
||||
// 发送错误消息
|
||||
WebSocketMessage errorMessage = WebSocketMessage.builder()
|
||||
.type(WebSocketMessage.MessageType.ERROR)
|
||||
.content("消息发送失败: " + e.getMessage())
|
||||
.senderType(WebSocketMessage.SenderType.SYSTEM)
|
||||
.status(WebSocketMessage.MessageStatus.FAILED)
|
||||
.build();
|
||||
|
||||
chatWebSocketService.sendMessageToUser(chatRequest.getSenderId(), errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户连接
|
||||
* @param headerAccessor 消息头访问器
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
@MessageMapping("/chat.connect")
|
||||
public void connectUser(SimpMessageHeaderAccessor headerAccessor, Principal principal) {
|
||||
try {
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
log.info("用户连接WebSocket: sessionId={}, principal={}", sessionId, principal);
|
||||
|
||||
chatWebSocketService.handleUserConnect(sessionId, principal);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户WebSocket连接失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户断开连接
|
||||
* @param headerAccessor 消息头访问器
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
@MessageMapping("/chat.disconnect")
|
||||
public void disconnectUser(SimpMessageHeaderAccessor headerAccessor, Principal principal) {
|
||||
try {
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
log.info("用户断开WebSocket连接: sessionId={}, principal={}", sessionId, principal);
|
||||
|
||||
chatWebSocketService.handleUserDisconnect(sessionId, principal);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户WebSocket断开连接失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理心跳消息
|
||||
* @param headerAccessor 消息头访问器
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
@MessageMapping("/chat.heartbeat")
|
||||
public void heartbeat(SimpMessageHeaderAccessor headerAccessor, Principal principal) {
|
||||
try {
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
|
||||
// 发送心跳响应
|
||||
WebSocketMessage heartbeatMessage = WebSocketMessage.builder()
|
||||
.type(WebSocketMessage.MessageType.HEARTBEAT)
|
||||
.content("pong")
|
||||
.senderType(WebSocketMessage.SenderType.SYSTEM)
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.build();
|
||||
|
||||
String userId = principal != null ? principal.getName() : sessionId;
|
||||
chatWebSocketService.sendMessageToUser(userId, heartbeatMessage);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理WebSocket心跳失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package com.emotionmuseum.websocket.controller;
|
||||
|
||||
import com.emotionmuseum.common.result.Result;
|
||||
import com.emotionmuseum.websocket.dto.WebSocketMessage;
|
||||
import com.emotionmuseum.websocket.manager.WebSocketSessionManager;
|
||||
import com.emotionmuseum.websocket.service.ChatWebSocketService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* WebSocket测试控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/websocket")
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocketTestController {
|
||||
|
||||
private final ChatWebSocketService chatWebSocketService;
|
||||
private final WebSocketSessionManager sessionManager;
|
||||
|
||||
/**
|
||||
* 发送测试消息
|
||||
* @param userId 用户ID
|
||||
* @param message 消息内容
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/send")
|
||||
public Result<Map<String, Object>> sendTestMessage(@RequestParam String userId, @RequestParam String message) {
|
||||
try {
|
||||
log.info("发送测试消息: userId={}, message={}", userId, message);
|
||||
|
||||
// 创建测试消息
|
||||
WebSocketMessage testMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.type(WebSocketMessage.MessageType.TEXT)
|
||||
.content(message)
|
||||
.senderId("system")
|
||||
.senderType(WebSocketMessage.SenderType.SYSTEM)
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 发送消息
|
||||
chatWebSocketService.sendMessageToUser(userId, testMessage);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", true);
|
||||
result.put("messageId", testMessage.getMessageId());
|
||||
result.put("timestamp", testMessage.getCreateTime());
|
||||
|
||||
return Result.success(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送测试消息失败", e);
|
||||
return Result.error("发送测试消息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播测试消息
|
||||
* @param message 消息内容
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/broadcast")
|
||||
public Result<Map<String, Object>> broadcastTestMessage(@RequestParam String message) {
|
||||
try {
|
||||
log.info("广播测试消息: message={}", message);
|
||||
|
||||
// 创建广播消息
|
||||
WebSocketMessage broadcastMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.type(WebSocketMessage.MessageType.SYSTEM)
|
||||
.content(message)
|
||||
.senderId("system")
|
||||
.senderType(WebSocketMessage.SenderType.SYSTEM)
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 广播消息
|
||||
chatWebSocketService.broadcastMessage(broadcastMessage);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", true);
|
||||
result.put("messageId", broadcastMessage.getMessageId());
|
||||
result.put("timestamp", broadcastMessage.getCreateTime());
|
||||
|
||||
return Result.success(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("广播测试消息失败", e);
|
||||
return Result.error("广播测试消息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在线用户信息
|
||||
* @return 在线用户信息
|
||||
*/
|
||||
@GetMapping("/online-users")
|
||||
public Result<Map<String, Object>> getOnlineUsers() {
|
||||
try {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("count", sessionManager.getOnlineUserCount());
|
||||
result.put("users", sessionManager.getOnlineUserIds());
|
||||
|
||||
return Result.success(result);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取在线用户信息失败", e);
|
||||
return Result.error("获取在线用户信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.emotionmuseum.websocket.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 聊天请求DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ChatRequest {
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
@NotBlank(message = "消息内容不能为空")
|
||||
@Size(max = 2000, message = "消息内容不能超过2000字符")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 发送者ID(用户ID或guest标识)
|
||||
*/
|
||||
private String senderId;
|
||||
|
||||
/**
|
||||
* 发送者类型
|
||||
*/
|
||||
private WebSocketMessage.SenderType senderType;
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
private WebSocketMessage.MessageType messageType;
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package com.emotionmuseum.websocket.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* WebSocket消息DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class WebSocketMessage {
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
private MessageType type;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 发送者ID(用户ID或guest标识)
|
||||
*/
|
||||
private String senderId;
|
||||
|
||||
/**
|
||||
* 发送者类型
|
||||
*/
|
||||
private SenderType senderType;
|
||||
|
||||
/**
|
||||
* 消息状态
|
||||
*/
|
||||
private MessageStatus status;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 额外数据
|
||||
*/
|
||||
private Object data;
|
||||
|
||||
/**
|
||||
* 消息类型枚举
|
||||
*/
|
||||
public enum MessageType {
|
||||
TEXT, // 文本消息
|
||||
TYPING, // 正在输入
|
||||
SYSTEM, // 系统消息
|
||||
ERROR, // 错误消息
|
||||
HEARTBEAT, // 心跳消息
|
||||
CONNECTION, // 连接状态
|
||||
AI_THINKING // AI思考中
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送者类型枚举
|
||||
*/
|
||||
public enum SenderType {
|
||||
USER, // 用户
|
||||
GUEST, // 游客
|
||||
AI, // AI
|
||||
SYSTEM // 系统
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息状态枚举
|
||||
*/
|
||||
public enum MessageStatus {
|
||||
SENDING, // 发送中
|
||||
SENT, // 已发送
|
||||
DELIVERED, // 已送达
|
||||
READ, // 已读
|
||||
FAILED // 发送失败
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.emotionmuseum.websocket.feign;
|
||||
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AI服务Feign客户端
|
||||
*/
|
||||
@FeignClient(name = "emotion-ai")
|
||||
public interface AiServiceClient {
|
||||
|
||||
/**
|
||||
* 调用AI聊天接口
|
||||
* @param requestBody 请求体
|
||||
* @return AI响应
|
||||
*/
|
||||
@PostMapping("/api/ai/chat/send")
|
||||
Map<String, Object> chat(@RequestBody Map<String, Object> requestBody);
|
||||
|
||||
/**
|
||||
* 调用游客聊天接口
|
||||
* @param requestBody 请求体
|
||||
* @return AI响应
|
||||
*/
|
||||
@PostMapping("/api/ai/guest/chat")
|
||||
Map<String, Object> guestChat(@RequestBody Map<String, Object> requestBody);
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package com.emotionmuseum.websocket.listener;
|
||||
|
||||
import com.emotionmuseum.websocket.manager.WebSocketSessionManager;
|
||||
import com.emotionmuseum.websocket.service.ChatWebSocketService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.messaging.SessionConnectedEvent;
|
||||
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
|
||||
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
|
||||
import org.springframework.web.socket.messaging.SessionUnsubscribeEvent;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* WebSocket事件监听器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocketEventListener {
|
||||
|
||||
private final WebSocketSessionManager sessionManager;
|
||||
private final ChatWebSocketService chatWebSocketService;
|
||||
|
||||
/**
|
||||
* 监听WebSocket连接事件
|
||||
* @param event 连接事件
|
||||
*/
|
||||
@EventListener
|
||||
public void handleWebSocketConnectListener(SessionConnectedEvent event) {
|
||||
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
Principal principal = headerAccessor.getUser();
|
||||
|
||||
log.info("收到WebSocket连接事件: sessionId={}, principal={}", sessionId, principal);
|
||||
|
||||
// 处理用户连接
|
||||
chatWebSocketService.handleUserConnect(sessionId, principal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听WebSocket断开连接事件
|
||||
* @param event 断开连接事件
|
||||
*/
|
||||
@EventListener
|
||||
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
|
||||
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
Principal principal = headerAccessor.getUser();
|
||||
|
||||
log.info("收到WebSocket断开连接事件: sessionId={}, principal={}", sessionId, principal);
|
||||
|
||||
// 处理用户断开连接
|
||||
chatWebSocketService.handleUserDisconnect(sessionId, principal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听WebSocket订阅事件
|
||||
* @param event 订阅事件
|
||||
*/
|
||||
@EventListener
|
||||
public void handleWebSocketSubscribeListener(SessionSubscribeEvent event) {
|
||||
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
String destination = headerAccessor.getDestination();
|
||||
|
||||
log.info("收到WebSocket订阅事件: sessionId={}, destination={}", sessionId, destination);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听WebSocket取消订阅事件
|
||||
* @param event 取消订阅事件
|
||||
*/
|
||||
@EventListener
|
||||
public void handleWebSocketUnsubscribeListener(SessionUnsubscribeEvent event) {
|
||||
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
String destination = headerAccessor.getDestination();
|
||||
|
||||
log.info("收到WebSocket取消订阅事件: sessionId={}, destination={}", sessionId, destination);
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package com.emotionmuseum.websocket.manager;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* WebSocket会话管理器
|
||||
* 用于管理WebSocket连接会话
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class WebSocketSessionManager {
|
||||
|
||||
/**
|
||||
* 存储用户ID与会话ID的映射关系
|
||||
* key: userId, value: sessionId
|
||||
*/
|
||||
private final ConcurrentMap<String, String> userSessionMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 存储会话ID与用户ID的映射关系
|
||||
* key: sessionId, value: userId
|
||||
*/
|
||||
private final ConcurrentMap<String, String> sessionUserMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 存储会话ID与会话信息的映射关系
|
||||
* key: sessionId, value: SessionInfo
|
||||
*/
|
||||
private final ConcurrentMap<String, SessionInfo> sessionInfoMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 添加会话
|
||||
* @param userId 用户ID
|
||||
* @param sessionId 会话ID
|
||||
* @param conversationId 对话ID
|
||||
*/
|
||||
public void addSession(String userId, String sessionId, String conversationId) {
|
||||
// 如果用户已有会话,先移除旧会话
|
||||
String oldSessionId = userSessionMap.get(userId);
|
||||
if (oldSessionId != null) {
|
||||
removeSession(oldSessionId);
|
||||
}
|
||||
|
||||
userSessionMap.put(userId, sessionId);
|
||||
sessionUserMap.put(sessionId, userId);
|
||||
sessionInfoMap.put(sessionId, new SessionInfo(userId, sessionId, conversationId, System.currentTimeMillis()));
|
||||
|
||||
log.info("WebSocket会话已添加: userId={}, sessionId={}, conversationId={}", userId, sessionId, conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除会话
|
||||
* @param sessionId 会话ID
|
||||
*/
|
||||
public void removeSession(String sessionId) {
|
||||
String userId = sessionUserMap.remove(sessionId);
|
||||
if (userId != null) {
|
||||
userSessionMap.remove(userId);
|
||||
sessionInfoMap.remove(sessionId);
|
||||
log.info("WebSocket会话已移除: userId={}, sessionId={}", userId, sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID获取会话ID
|
||||
* @param userId 用户ID
|
||||
* @return 会话ID
|
||||
*/
|
||||
public String getSessionIdByUserId(String userId) {
|
||||
return userSessionMap.get(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID获取用户ID
|
||||
* @param sessionId 会话ID
|
||||
* @return 用户ID
|
||||
*/
|
||||
public String getUserIdBySessionId(String sessionId) {
|
||||
return sessionUserMap.get(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID获取会话信息
|
||||
* @param sessionId 会话ID
|
||||
* @return 会话信息
|
||||
*/
|
||||
public SessionInfo getSessionInfo(String sessionId) {
|
||||
return sessionInfoMap.get(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否在线
|
||||
* @param userId 用户ID
|
||||
* @return 是否在线
|
||||
*/
|
||||
public boolean isUserOnline(String userId) {
|
||||
return userSessionMap.containsKey(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取在线用户数量
|
||||
* @return 在线用户数量
|
||||
*/
|
||||
public int getOnlineUserCount() {
|
||||
return userSessionMap.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有在线用户ID
|
||||
* @return 在线用户ID集合
|
||||
*/
|
||||
public Set<String> getOnlineUserIds() {
|
||||
return userSessionMap.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话信息内部类
|
||||
*/
|
||||
public static class SessionInfo {
|
||||
private final String userId;
|
||||
private final String sessionId;
|
||||
private final String conversationId;
|
||||
private final long connectTime;
|
||||
|
||||
public SessionInfo(String userId, String sessionId, String conversationId, long connectTime) {
|
||||
this.userId = userId;
|
||||
this.sessionId = sessionId;
|
||||
this.conversationId = conversationId;
|
||||
this.connectTime = connectTime;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getUserId() { return userId; }
|
||||
public String getSessionId() { return sessionId; }
|
||||
public String getConversationId() { return conversationId; }
|
||||
public long getConnectTime() { return connectTime; }
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.emotionmuseum.websocket.service;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* AI聊天服务接口
|
||||
*/
|
||||
public interface AiChatService {
|
||||
|
||||
/**
|
||||
* 异步获取AI聊天响应
|
||||
* @param message 用户消息
|
||||
* @param conversationId 会话ID
|
||||
* @param userId 用户ID
|
||||
* @return AI回复的CompletableFuture
|
||||
*/
|
||||
CompletableFuture<String> getChatResponseAsync(String message, String conversationId, String userId);
|
||||
|
||||
/**
|
||||
* 同步获取AI聊天响应
|
||||
* @param message 用户消息
|
||||
* @param conversationId 会话ID
|
||||
* @param userId 用户ID
|
||||
* @return AI回复
|
||||
*/
|
||||
String getChatResponse(String message, String conversationId, String userId);
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.emotionmuseum.websocket.service;
|
||||
|
||||
import com.emotionmuseum.websocket.dto.ChatRequest;
|
||||
import com.emotionmuseum.websocket.dto.WebSocketMessage;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* WebSocket聊天服务接口
|
||||
*/
|
||||
public interface ChatWebSocketService {
|
||||
|
||||
/**
|
||||
* 处理聊天消息
|
||||
* @param chatRequest 聊天请求
|
||||
* @param sessionId 会话ID
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
void handleChatMessage(ChatRequest chatRequest, String sessionId, Principal principal);
|
||||
|
||||
/**
|
||||
* 处理用户连接
|
||||
* @param sessionId 会话ID
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
void handleUserConnect(String sessionId, Principal principal);
|
||||
|
||||
/**
|
||||
* 处理用户断开连接
|
||||
* @param sessionId 会话ID
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
void handleUserDisconnect(String sessionId, Principal principal);
|
||||
|
||||
/**
|
||||
* 向用户发送消息
|
||||
* @param userId 用户ID
|
||||
* @param message 消息
|
||||
*/
|
||||
void sendMessageToUser(String userId, WebSocketMessage message);
|
||||
|
||||
/**
|
||||
* 向会话发送消息
|
||||
* @param conversationId 会话ID
|
||||
* @param message 消息
|
||||
*/
|
||||
void sendMessageToConversation(String conversationId, WebSocketMessage message);
|
||||
|
||||
/**
|
||||
* 向所有用户广播消息
|
||||
* @param message 消息
|
||||
*/
|
||||
void broadcastMessage(WebSocketMessage message);
|
||||
|
||||
/**
|
||||
* 发送AI回复消息
|
||||
* @param userId 用户ID
|
||||
* @param conversationId 会话ID
|
||||
* @param aiReply AI回复内容
|
||||
*/
|
||||
void sendAiReplyMessage(String userId, String conversationId, String aiReply);
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
package com.emotionmuseum.websocket.service.impl;
|
||||
|
||||
import com.emotionmuseum.websocket.feign.AiServiceClient;
|
||||
import com.emotionmuseum.websocket.service.AiChatService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* AI聊天服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AiChatServiceImpl implements AiChatService {
|
||||
|
||||
private final AiServiceClient aiServiceClient;
|
||||
|
||||
@Async
|
||||
@Override
|
||||
public CompletableFuture<String> getChatResponseAsync(String message, String conversationId, String userId) {
|
||||
try {
|
||||
String response = getChatResponse(message, conversationId, userId);
|
||||
return CompletableFuture.completedFuture(response);
|
||||
} catch (Exception e) {
|
||||
log.error("异步获取AI聊天响应失败", e);
|
||||
CompletableFuture<String> future = new CompletableFuture<>();
|
||||
future.completeExceptionally(e);
|
||||
return future;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getChatResponse(String message, String conversationId, String userId) {
|
||||
try {
|
||||
log.info("调用AI服务获取聊天响应: message={}, conversationId={}, userId={}", message, conversationId, userId);
|
||||
|
||||
Map<String, Object> response;
|
||||
|
||||
// 判断是否为游客用户
|
||||
if (userId != null && userId.startsWith("guest_")) {
|
||||
// 调用游客聊天接口
|
||||
Map<String, Object> guestRequestBody = new HashMap<>();
|
||||
guestRequestBody.put("message", message);
|
||||
guestRequestBody.put("conversationId", conversationId);
|
||||
guestRequestBody.put("title", "WebSocket聊天");
|
||||
|
||||
response = aiServiceClient.guestChat(guestRequestBody);
|
||||
|
||||
// 处理游客聊天响应
|
||||
if (response != null && response.containsKey("data")) {
|
||||
Object data = response.get("data");
|
||||
if (data instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> dataMap = (Map<String, Object>) data;
|
||||
if (dataMap.containsKey("aiReply")) {
|
||||
return dataMap.get("aiReply").toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 调用注册用户聊天接口
|
||||
Map<String, Object> userRequestBody = new HashMap<>();
|
||||
userRequestBody.put("message", message);
|
||||
userRequestBody.put("conversationId", conversationId);
|
||||
userRequestBody.put("userId", userId);
|
||||
userRequestBody.put("type", "text");
|
||||
userRequestBody.put("needEmotionAnalysis", false);
|
||||
|
||||
response = aiServiceClient.chat(userRequestBody);
|
||||
|
||||
// 处理用户聊天响应
|
||||
if (response != null && response.containsKey("data")) {
|
||||
Object data = response.get("data");
|
||||
if (data instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> dataMap = (Map<String, Object>) data;
|
||||
if (dataMap.containsKey("content")) {
|
||||
return dataMap.get("content").toString();
|
||||
}
|
||||
// 兼容旧格式
|
||||
if (dataMap.containsKey("aiReply")) {
|
||||
return dataMap.get("aiReply").toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("AI服务返回的响应格式不正确: {}", response);
|
||||
return "抱歉,AI服务暂时无法提供回复。";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("调用AI服务获取聊天响应失败", e);
|
||||
return "抱歉,AI服务暂时不可用,请稍后再试。";
|
||||
}
|
||||
}
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
package com.emotionmuseum.websocket.service.impl;
|
||||
|
||||
import com.emotionmuseum.websocket.dto.ChatRequest;
|
||||
import com.emotionmuseum.websocket.dto.WebSocketMessage;
|
||||
import com.emotionmuseum.websocket.manager.WebSocketSessionManager;
|
||||
import com.emotionmuseum.websocket.service.ChatWebSocketService;
|
||||
import com.emotionmuseum.websocket.service.AiChatService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* WebSocket聊天服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ChatWebSocketServiceImpl implements ChatWebSocketService {
|
||||
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
private final WebSocketSessionManager sessionManager;
|
||||
private final AiChatService aiChatService;
|
||||
|
||||
@Override
|
||||
public void handleChatMessage(ChatRequest chatRequest, String sessionId, Principal principal) {
|
||||
try {
|
||||
// 获取用户ID
|
||||
String userId = getUserId(chatRequest, principal);
|
||||
|
||||
// 获取会话信息
|
||||
WebSocketSessionManager.SessionInfo sessionInfo = sessionManager.getSessionInfo(sessionId);
|
||||
String conversationId = chatRequest.getConversationId();
|
||||
|
||||
// 如果没有提供会话ID,尝试从会话管理器获取
|
||||
if (conversationId == null && sessionInfo != null) {
|
||||
conversationId = sessionInfo.getConversationId();
|
||||
}
|
||||
|
||||
// 如果仍然没有会话ID,为游客用户创建一个
|
||||
if (conversationId == null && userId.startsWith("guest_")) {
|
||||
conversationId = "ws_conversation_" + userId + "_" + System.currentTimeMillis();
|
||||
// 更新会话管理器中的会话信息
|
||||
sessionManager.addSession(userId, sessionId, conversationId);
|
||||
}
|
||||
|
||||
// 创建消息
|
||||
WebSocketMessage message = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.conversationId(conversationId)
|
||||
.type(chatRequest.getMessageType() != null ? chatRequest.getMessageType() : WebSocketMessage.MessageType.TEXT)
|
||||
.content(chatRequest.getContent())
|
||||
.senderId(userId)
|
||||
.senderType(chatRequest.getSenderType() != null ? chatRequest.getSenderType() : WebSocketMessage.SenderType.USER)
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 保存用户消息到数据库
|
||||
saveUserMessage(message);
|
||||
|
||||
// 向用户确认消息已收到
|
||||
sendMessageToUser(userId, message);
|
||||
|
||||
// 如果是文本消息,调用AI服务获取回复
|
||||
if (message.getType() == WebSocketMessage.MessageType.TEXT) {
|
||||
handleAiResponse(userId, message);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理聊天消息失败: {}", e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUserConnect(String sessionId, Principal principal) {
|
||||
try {
|
||||
String userId = principal != null ? principal.getName() : "guest_" + sessionId;
|
||||
|
||||
// 为游客用户生成会话ID
|
||||
String conversationId = null;
|
||||
if (userId.startsWith("guest_")) {
|
||||
conversationId = "ws_conversation_" + userId + "_" + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// 添加会话到管理器
|
||||
sessionManager.addSession(userId, sessionId, conversationId);
|
||||
|
||||
// 发送连接成功消息
|
||||
WebSocketMessage connectMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.conversationId(conversationId)
|
||||
.type(WebSocketMessage.MessageType.CONNECTION)
|
||||
.content("WebSocket连接成功,欢迎使用情绪博物馆AI聊天服务!")
|
||||
.senderId("system")
|
||||
.senderType(WebSocketMessage.SenderType.SYSTEM)
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
sendMessageToUser(userId, connectMessage);
|
||||
|
||||
log.info("用户WebSocket连接成功: userId={}, sessionId={}, conversationId={}", userId, sessionId, conversationId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户连接失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUserDisconnect(String sessionId, Principal principal) {
|
||||
try {
|
||||
// 从会话管理器中移除会话
|
||||
sessionManager.removeSession(sessionId);
|
||||
|
||||
log.info("用户WebSocket断开连接: sessionId={}", sessionId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户断开连接失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessageToUser(String userId, WebSocketMessage message) {
|
||||
try {
|
||||
messagingTemplate.convertAndSendToUser(userId, "/queue/messages", message);
|
||||
log.debug("向用户发送消息: userId={}, messageType={}", userId, message.getType());
|
||||
} catch (Exception e) {
|
||||
log.error("向用户发送消息失败: userId={}, error={}", userId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessageToConversation(String conversationId, WebSocketMessage message) {
|
||||
try {
|
||||
messagingTemplate.convertAndSend("/topic/conversation/" + conversationId, message);
|
||||
log.debug("向会话发送消息: conversationId={}, messageType={}", conversationId, message.getType());
|
||||
} catch (Exception e) {
|
||||
log.error("向会话发送消息失败: conversationId={}, error={}", conversationId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void broadcastMessage(WebSocketMessage message) {
|
||||
try {
|
||||
messagingTemplate.convertAndSend("/topic/broadcast", message);
|
||||
log.debug("广播消息: messageType={}", message.getType());
|
||||
} catch (Exception e) {
|
||||
log.error("广播消息失败: error={}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendAiReplyMessage(String userId, String conversationId, String aiReply) {
|
||||
try {
|
||||
// 分割AI回复(如果包含\n或\n\n)
|
||||
String[] replyParts = aiReply.split("\\n\\n|\\n");
|
||||
|
||||
for (String part : replyParts) {
|
||||
if (part.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
WebSocketMessage aiMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.conversationId(conversationId)
|
||||
.type(WebSocketMessage.MessageType.TEXT)
|
||||
.content(part.trim())
|
||||
.senderId("ai")
|
||||
.senderType(WebSocketMessage.SenderType.AI)
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
// 保存AI消息到数据库
|
||||
saveAiMessage(aiMessage);
|
||||
|
||||
// 发送给用户
|
||||
sendMessageToUser(userId, aiMessage);
|
||||
|
||||
// 短暂延迟,模拟自然对话
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("发送AI回复消息失败: userId={}, error={}", userId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理AI响应
|
||||
*/
|
||||
private void handleAiResponse(String userId, WebSocketMessage userMessage) {
|
||||
try {
|
||||
// 发送AI思考中状态
|
||||
WebSocketMessage thinkingMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.conversationId(userMessage.getConversationId())
|
||||
.type(WebSocketMessage.MessageType.AI_THINKING)
|
||||
.content("AI正在思考中...")
|
||||
.senderId("ai")
|
||||
.senderType(WebSocketMessage.SenderType.AI)
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
sendMessageToUser(userId, thinkingMessage);
|
||||
|
||||
// 异步调用AI服务
|
||||
aiChatService.getChatResponseAsync(userMessage.getContent(), userMessage.getConversationId(), userId)
|
||||
.thenAccept(aiReply -> {
|
||||
if (aiReply != null && !aiReply.trim().isEmpty()) {
|
||||
sendAiReplyMessage(userId, userMessage.getConversationId(), aiReply);
|
||||
}
|
||||
})
|
||||
.exceptionally(throwable -> {
|
||||
log.error("AI服务调用失败", throwable);
|
||||
|
||||
WebSocketMessage errorMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.conversationId(userMessage.getConversationId())
|
||||
.type(WebSocketMessage.MessageType.ERROR)
|
||||
.content("AI服务暂时不可用,请稍后再试")
|
||||
.senderId("ai")
|
||||
.senderType(WebSocketMessage.SenderType.AI)
|
||||
.status(WebSocketMessage.MessageStatus.FAILED)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
|
||||
sendMessageToUser(userId, errorMessage);
|
||||
return null;
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理AI响应失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户ID
|
||||
*/
|
||||
private String getUserId(ChatRequest chatRequest, Principal principal) {
|
||||
if (chatRequest.getSenderId() != null) {
|
||||
return chatRequest.getSenderId();
|
||||
}
|
||||
if (principal != null) {
|
||||
return principal.getName();
|
||||
}
|
||||
// 为游客用户生成一个基于时间戳的ID,保持会话期间的一致性
|
||||
return "guest_" + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户消息到数据库
|
||||
*/
|
||||
private void saveUserMessage(WebSocketMessage message) {
|
||||
// TODO: 实现保存用户消息到数据库的逻辑
|
||||
log.debug("保存用户消息: {}", message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存AI消息到数据库
|
||||
*/
|
||||
private void saveAiMessage(WebSocketMessage message) {
|
||||
// TODO: 实现保存AI消息到数据库的逻辑
|
||||
log.debug("保存AI消息: {}", message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# 本地开发环境配置
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
username: nacos
|
||||
password: Peanut2817*#
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
zone: local
|
||||
register-enabled: true
|
||||
ephemeral: true
|
||||
cluster-name: DEFAULT
|
||||
service: ${spring.application.name}
|
||||
weight: 1
|
||||
heart-beat-interval: 5000
|
||||
heart-beat-timeout: 15000
|
||||
ip-delete-timeout: 30000
|
||||
config:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
file-extension: yml
|
||||
enabled: false
|
||||
username: nacos
|
||||
password: Peanut2817*#
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 123456
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password:
|
||||
database: 0
|
||||
|
||||
# WebSocket配置
|
||||
websocket:
|
||||
allowed-origins: "*"
|
||||
sockjs:
|
||||
enabled: true
|
||||
heartbeat-time: 25000
|
||||
disconnect-delay: 5000
|
||||
stomp:
|
||||
relay:
|
||||
enabled: false
|
||||
broker:
|
||||
enabled: true
|
||||
destinations: ["/topic", "/queue"]
|
||||
application-destination-prefixes: ["/app"]
|
||||
user-destination-prefix: "/user"
|
||||
|
||||
# Coze平台配置
|
||||
coze:
|
||||
base-url: https://api.coze.cn
|
||||
api-key: your-coze-api-key
|
||||
bot-id: 7523042446285439016
|
||||
workflow-id: 7523047462895796287
|
||||
user-id: emotion-museum-user
|
||||
token: pat_GCR4qKzqpf90wMCvKsldMrB18KG3QsLDci65bZthssKsbLxu8X70BKYumleDcabO
|
||||
timeout: 60
|
||||
max-retries: 3
|
||||
stream: false
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: debug
|
||||
com.baomidou.mybatisplus: debug
|
||||
com.alibaba.nacos: info
|
||||
org.springframework.web.socket: debug
|
||||
org.springframework.messaging: debug
|
||||
file:
|
||||
name: logs/emotion-websocket-local.log
|
||||
@@ -0,0 +1,85 @@
|
||||
# 本地开发环境配置
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
username: nacos
|
||||
password: nacos
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
zone: local
|
||||
register-enabled: true
|
||||
ephemeral: true
|
||||
cluster-name: DEFAULT
|
||||
service: ${spring.application.name}
|
||||
weight: 1
|
||||
heart-beat-interval: 5000
|
||||
heart-beat-timeout: 15000
|
||||
ip-delete-timeout: 30000
|
||||
config:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
file-extension: yml
|
||||
enabled: false
|
||||
username: nacos
|
||||
password: nacos
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: 123456
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password:
|
||||
database: 0
|
||||
|
||||
# WebSocket配置
|
||||
websocket:
|
||||
allowed-origins: "*"
|
||||
sockjs:
|
||||
enabled: true
|
||||
heartbeat-time: 25000
|
||||
disconnect-delay: 5000
|
||||
stomp:
|
||||
relay:
|
||||
enabled: false
|
||||
broker:
|
||||
enabled: true
|
||||
destinations: ["/topic", "/queue"]
|
||||
application-destination-prefixes: ["/app"]
|
||||
user-destination-prefix: "/user"
|
||||
|
||||
# Coze平台配置
|
||||
coze:
|
||||
base-url: https://api.coze.cn
|
||||
api-key: your-coze-api-key
|
||||
bot-id: 7523042446285439016
|
||||
workflow-id: 7523047462895796287
|
||||
user-id: emotion-museum-user
|
||||
token: pat_GCR4qKzqpf90wMCvKsldMrB18KG3QsLDci65bZthssKsbLxu8X70BKYumleDcabO
|
||||
timeout: 60
|
||||
max-retries: 3
|
||||
stream: false
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: debug
|
||||
com.baomidou.mybatisplus: debug
|
||||
com.alibaba.nacos: info
|
||||
org.springframework.web.socket: debug
|
||||
org.springframework.messaging: debug
|
||||
file:
|
||||
name: logs/emotion-websocket-local.log
|
||||
@@ -0,0 +1,55 @@
|
||||
# 生产环境配置
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 47.111.10.27:8848
|
||||
namespace: prod
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
username: nacos
|
||||
password: EmotionMuseum2025
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
zone: prod
|
||||
register-enabled: true
|
||||
ephemeral: true
|
||||
cluster-name: DEFAULT
|
||||
service: ${spring.application.name}
|
||||
weight: 1
|
||||
heart-beat-interval: 5000
|
||||
heart-beat-timeout: 15000
|
||||
ip-delete-timeout: 30000
|
||||
config:
|
||||
server-addr: 47.111.10.27:8848
|
||||
namespace: prod
|
||||
group: DEFAULT_GROUP
|
||||
file-extension: yml
|
||||
enabled: false
|
||||
username: nacos
|
||||
password: EmotionMuseum2025
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://47.111.10.27:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: EmotionMuseum2025*#
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: 47.111.10.27
|
||||
port: 6379
|
||||
password: EmotionMuseum2025*#
|
||||
database: 0
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: warn
|
||||
com.baomidou.mybatisplus: warn
|
||||
com.alibaba.nacos: error
|
||||
file:
|
||||
name: logs/emotion-websocket-prod.log
|
||||
@@ -0,0 +1,55 @@
|
||||
# 测试环境配置
|
||||
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: 47.111.10.27:8848
|
||||
namespace: test
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
username: nacos
|
||||
password: EmotionMuseum2025
|
||||
metadata:
|
||||
version: 1.0.0
|
||||
zone: test
|
||||
register-enabled: true
|
||||
ephemeral: true
|
||||
cluster-name: DEFAULT
|
||||
service: ${spring.application.name}
|
||||
weight: 1
|
||||
heart-beat-interval: 5000
|
||||
heart-beat-timeout: 15000
|
||||
ip-delete-timeout: 30000
|
||||
config:
|
||||
server-addr: 47.111.10.27:8848
|
||||
namespace: test
|
||||
group: DEFAULT_GROUP
|
||||
file-extension: yml
|
||||
enabled: false
|
||||
username: nacos
|
||||
password: EmotionMuseum2025
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://47.111.10.27:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: EmotionMuseum2025*#
|
||||
|
||||
# Redis配置
|
||||
data:
|
||||
redis:
|
||||
host: 47.111.10.27
|
||||
port: 6379
|
||||
password: EmotionMuseum2025*#
|
||||
database: 0
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum: info
|
||||
com.baomidou.mybatisplus: info
|
||||
com.alibaba.nacos: warn
|
||||
file:
|
||||
name: logs/emotion-websocket-test.log
|
||||
@@ -0,0 +1,86 @@
|
||||
server:
|
||||
port: 19007
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-websocket
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:local}
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: EmotionMuseum2025*#
|
||||
druid:
|
||||
initial-size: 5
|
||||
min-idle: 5
|
||||
max-active: 20
|
||||
max-wait: 60000
|
||||
time-between-eviction-runs-millis: 60000
|
||||
min-evictable-idle-time-millis: 300000
|
||||
validation-query: SELECT 1 FROM DUAL
|
||||
test-while-idle: true
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
pool-prepared-statements: true
|
||||
max-pool-prepared-statement-per-connection-size: 20
|
||||
filters: stat,wall,slf4j
|
||||
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
|
||||
|
||||
# Redis配置
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password:
|
||||
database: 0
|
||||
timeout: 5000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-idle: 10
|
||||
min-idle: 0
|
||||
max-wait: -1ms
|
||||
|
||||
# MyBatis Plus配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
cache-enabled: false
|
||||
call-setters-on-nulls: true
|
||||
jdbc-type-for-null: 'null'
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: ASSIGN_ID
|
||||
logic-delete-field: is_deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
mapper-locations: classpath*:mapper/**/*Mapper.xml
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum.websocket: DEBUG
|
||||
org.springframework.web.socket: DEBUG
|
||||
org.springframework.messaging: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
|
||||
file:
|
||||
name: logs/emotion-websocket.log
|
||||
|
||||
# 监控配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
metrics:
|
||||
export:
|
||||
prometheus:
|
||||
enabled: true
|
||||
@@ -0,0 +1,17 @@
|
||||
spring:
|
||||
application:
|
||||
name: emotion-websocket
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:local}
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
|
||||
namespace: ${NACOS_NAMESPACE:}
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
config:
|
||||
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
|
||||
namespace: ${NACOS_NAMESPACE:}
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
file-extension: yml
|
||||
enabled: false
|
||||
@@ -0,0 +1,270 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebSocket聊天测试</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.status.connected {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.status.disconnected {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.chat-container {
|
||||
height: 400px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
background-color: #fafafa;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.message {
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
max-width: 70%;
|
||||
}
|
||||
.message.user {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
.message.ai {
|
||||
background-color: #e9ecef;
|
||||
color: #333;
|
||||
}
|
||||
.message.system {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
.message.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.input-container input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.input-container button {
|
||||
padding: 10px 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.input-container button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.input-container button:disabled {
|
||||
background-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.controls {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.controls button {
|
||||
margin-right: 10px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.controls button:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>WebSocket聊天测试</h1>
|
||||
|
||||
<div id="status" class="status disconnected">未连接</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="connect()">连接</button>
|
||||
<button onclick="disconnect()">断开连接</button>
|
||||
<button onclick="clearMessages()">清空消息</button>
|
||||
<input type="text" id="userId" placeholder="用户ID (默认: test-user)" value="test-user">
|
||||
</div>
|
||||
|
||||
<div id="messages" class="chat-container"></div>
|
||||
|
||||
<div class="input-container">
|
||||
<input type="text" id="messageInput" placeholder="输入消息..." onkeypress="handleKeyPress(event)">
|
||||
<button onclick="sendMessage()" id="sendButton" disabled>发送</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
||||
<script>
|
||||
let stompClient = null;
|
||||
let connected = false;
|
||||
|
||||
function connect() {
|
||||
const userId = document.getElementById('userId').value || 'test-user';
|
||||
const socket = new SockJS('http://localhost:19007/ws/chat');
|
||||
stompClient = Stomp.over(socket);
|
||||
|
||||
stompClient.connect({}, function (frame) {
|
||||
console.log('Connected: ' + frame);
|
||||
connected = true;
|
||||
updateStatus('已连接', true);
|
||||
document.getElementById('sendButton').disabled = false;
|
||||
|
||||
// 订阅用户消息
|
||||
stompClient.subscribe('/user/queue/messages', function (message) {
|
||||
const messageData = JSON.parse(message.body);
|
||||
displayMessage(messageData);
|
||||
});
|
||||
|
||||
// 订阅广播消息
|
||||
stompClient.subscribe('/topic/broadcast', function (message) {
|
||||
const messageData = JSON.parse(message.body);
|
||||
displayMessage(messageData);
|
||||
});
|
||||
|
||||
// 发送连接消息
|
||||
stompClient.send("/app/chat.connect", {}, JSON.stringify({}));
|
||||
|
||||
}, function (error) {
|
||||
console.log('Connection error: ' + error);
|
||||
updateStatus('连接失败: ' + error, false);
|
||||
});
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (stompClient !== null) {
|
||||
stompClient.send("/app/chat.disconnect", {}, JSON.stringify({}));
|
||||
stompClient.disconnect();
|
||||
}
|
||||
connected = false;
|
||||
updateStatus('已断开连接', false);
|
||||
document.getElementById('sendButton').disabled = true;
|
||||
console.log("Disconnected");
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const message = messageInput.value.trim();
|
||||
const userId = document.getElementById('userId').value || 'test-user';
|
||||
|
||||
if (message && connected) {
|
||||
const chatRequest = {
|
||||
content: message,
|
||||
senderId: userId,
|
||||
senderType: 'USER',
|
||||
messageType: 'TEXT',
|
||||
conversationId: 'test-conversation-' + userId
|
||||
};
|
||||
|
||||
stompClient.send("/app/chat.send", {}, JSON.stringify(chatRequest));
|
||||
messageInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function displayMessage(messageData) {
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message';
|
||||
|
||||
// 根据发送者类型设置样式
|
||||
switch (messageData.senderType) {
|
||||
case 'USER':
|
||||
messageDiv.className += ' user';
|
||||
break;
|
||||
case 'AI':
|
||||
messageDiv.className += ' ai';
|
||||
break;
|
||||
case 'SYSTEM':
|
||||
messageDiv.className += ' system';
|
||||
break;
|
||||
default:
|
||||
messageDiv.className += ' system';
|
||||
}
|
||||
|
||||
// 根据消息类型设置样式
|
||||
if (messageData.type === 'ERROR') {
|
||||
messageDiv.className = 'message error';
|
||||
}
|
||||
|
||||
// 设置消息内容
|
||||
let content = messageData.content;
|
||||
if (messageData.createTime) {
|
||||
content += ' <small>(' + messageData.createTime + ')</small>';
|
||||
}
|
||||
|
||||
messageDiv.innerHTML = content;
|
||||
messagesDiv.appendChild(messageDiv);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
|
||||
function updateStatus(message, isConnected) {
|
||||
const statusDiv = document.getElementById('status');
|
||||
statusDiv.textContent = message;
|
||||
statusDiv.className = 'status ' + (isConnected ? 'connected' : 'disconnected');
|
||||
}
|
||||
|
||||
function clearMessages() {
|
||||
document.getElementById('messages').innerHTML = '';
|
||||
}
|
||||
|
||||
function handleKeyPress(event) {
|
||||
if (event.key === 'Enter') {
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后自动连接
|
||||
window.onload = function() {
|
||||
// 可以在这里自动连接
|
||||
// connect();
|
||||
};
|
||||
|
||||
// 页面关闭时断开连接
|
||||
window.onbeforeunload = function() {
|
||||
if (connected) {
|
||||
disconnect();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.emotionmuseum.websocket;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
/**
|
||||
* WebSocket应用测试类
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
class WebSocketTestApplication {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
// 测试Spring上下文是否能正常加载
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
show-sql: true
|
||||
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
enabled: false
|
||||
config:
|
||||
enabled: false
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum.websocket: DEBUG
|
||||
org.springframework.web.socket: DEBUG
|
||||
@@ -0,0 +1,33 @@
|
||||
spring:
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
enabled: true
|
||||
config:
|
||||
server-addr: localhost:8848
|
||||
namespace:
|
||||
group: DEFAULT_GROUP
|
||||
file-extension: yml
|
||||
enabled: false
|
||||
|
||||
# Feign配置
|
||||
feign:
|
||||
client:
|
||||
config:
|
||||
default:
|
||||
connect-timeout: 5000
|
||||
read-timeout: 10000
|
||||
logger-level: basic
|
||||
httpclient:
|
||||
enabled: true
|
||||
max-connections: 200
|
||||
max-connections-per-route: 50
|
||||
|
||||
# WebSocket配置
|
||||
websocket:
|
||||
allowed-origins: "*"
|
||||
heartbeat-interval: 30000
|
||||
max-session-idle-timeout: 300000
|
||||
@@ -0,0 +1,86 @@
|
||||
server:
|
||||
port: 19007
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: emotion-websocket
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:local}
|
||||
|
||||
# 数据源配置
|
||||
datasource:
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/emotion_museum?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: EmotionMuseum2025*#
|
||||
druid:
|
||||
initial-size: 5
|
||||
min-idle: 5
|
||||
max-active: 20
|
||||
max-wait: 60000
|
||||
time-between-eviction-runs-millis: 60000
|
||||
min-evictable-idle-time-millis: 300000
|
||||
validation-query: SELECT 1 FROM DUAL
|
||||
test-while-idle: true
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
pool-prepared-statements: true
|
||||
max-pool-prepared-statement-per-connection-size: 20
|
||||
filters: stat,wall,slf4j
|
||||
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
|
||||
|
||||
# Redis配置
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password:
|
||||
database: 0
|
||||
timeout: 5000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 20
|
||||
max-idle: 10
|
||||
min-idle: 0
|
||||
max-wait: -1ms
|
||||
|
||||
# MyBatis Plus配置
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
cache-enabled: false
|
||||
call-setters-on-nulls: true
|
||||
jdbc-type-for-null: 'null'
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: ASSIGN_ID
|
||||
logic-delete-field: is_deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
mapper-locations: classpath*:mapper/**/*Mapper.xml
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
com.emotionmuseum.websocket: DEBUG
|
||||
org.springframework.web.socket: DEBUG
|
||||
org.springframework.messaging: DEBUG
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
|
||||
file:
|
||||
name: logs/emotion-websocket.log
|
||||
|
||||
# 监控配置
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
metrics:
|
||||
export:
|
||||
prometheus:
|
||||
enabled: true
|
||||
@@ -0,0 +1,17 @@
|
||||
spring:
|
||||
application:
|
||||
name: emotion-websocket
|
||||
profiles:
|
||||
active: ${SPRING_PROFILES_ACTIVE:local}
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
|
||||
namespace: ${NACOS_NAMESPACE:}
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
config:
|
||||
server-addr: ${NACOS_SERVER_ADDR:localhost:8848}
|
||||
namespace: ${NACOS_NAMESPACE:}
|
||||
group: ${NACOS_GROUP:DEFAULT_GROUP}
|
||||
file-extension: yml
|
||||
enabled: false
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,270 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebSocket聊天测试</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.status.connected {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.status.disconnected {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.chat-container {
|
||||
height: 400px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
background-color: #fafafa;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.message {
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
max-width: 70%;
|
||||
}
|
||||
.message.user {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
}
|
||||
.message.ai {
|
||||
background-color: #e9ecef;
|
||||
color: #333;
|
||||
}
|
||||
.message.system {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
.message.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
.input-container input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.input-container button {
|
||||
padding: 10px 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.input-container button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.input-container button:disabled {
|
||||
background-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.controls {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.controls button {
|
||||
margin-right: 10px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
.controls button:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>WebSocket聊天测试</h1>
|
||||
|
||||
<div id="status" class="status disconnected">未连接</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="connect()">连接</button>
|
||||
<button onclick="disconnect()">断开连接</button>
|
||||
<button onclick="clearMessages()">清空消息</button>
|
||||
<input type="text" id="userId" placeholder="用户ID (默认: test-user)" value="test-user">
|
||||
</div>
|
||||
|
||||
<div id="messages" class="chat-container"></div>
|
||||
|
||||
<div class="input-container">
|
||||
<input type="text" id="messageInput" placeholder="输入消息..." onkeypress="handleKeyPress(event)">
|
||||
<button onclick="sendMessage()" id="sendButton" disabled>发送</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
|
||||
<script>
|
||||
let stompClient = null;
|
||||
let connected = false;
|
||||
|
||||
function connect() {
|
||||
const userId = document.getElementById('userId').value || 'test-user';
|
||||
const socket = new SockJS('http://localhost:19007/ws/chat');
|
||||
stompClient = Stomp.over(socket);
|
||||
|
||||
stompClient.connect({}, function (frame) {
|
||||
console.log('Connected: ' + frame);
|
||||
connected = true;
|
||||
updateStatus('已连接', true);
|
||||
document.getElementById('sendButton').disabled = false;
|
||||
|
||||
// 订阅用户消息
|
||||
stompClient.subscribe('/user/queue/messages', function (message) {
|
||||
const messageData = JSON.parse(message.body);
|
||||
displayMessage(messageData);
|
||||
});
|
||||
|
||||
// 订阅广播消息
|
||||
stompClient.subscribe('/topic/broadcast', function (message) {
|
||||
const messageData = JSON.parse(message.body);
|
||||
displayMessage(messageData);
|
||||
});
|
||||
|
||||
// 发送连接消息
|
||||
stompClient.send("/app/chat.connect", {}, JSON.stringify({}));
|
||||
|
||||
}, function (error) {
|
||||
console.log('Connection error: ' + error);
|
||||
updateStatus('连接失败: ' + error, false);
|
||||
});
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (stompClient !== null) {
|
||||
stompClient.send("/app/chat.disconnect", {}, JSON.stringify({}));
|
||||
stompClient.disconnect();
|
||||
}
|
||||
connected = false;
|
||||
updateStatus('已断开连接', false);
|
||||
document.getElementById('sendButton').disabled = true;
|
||||
console.log("Disconnected");
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const message = messageInput.value.trim();
|
||||
const userId = document.getElementById('userId').value || 'test-user';
|
||||
|
||||
if (message && connected) {
|
||||
const chatRequest = {
|
||||
content: message,
|
||||
senderId: userId,
|
||||
senderType: 'USER',
|
||||
messageType: 'TEXT',
|
||||
conversationId: 'test-conversation-' + userId
|
||||
};
|
||||
|
||||
stompClient.send("/app/chat.send", {}, JSON.stringify(chatRequest));
|
||||
messageInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function displayMessage(messageData) {
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message';
|
||||
|
||||
// 根据发送者类型设置样式
|
||||
switch (messageData.senderType) {
|
||||
case 'USER':
|
||||
messageDiv.className += ' user';
|
||||
break;
|
||||
case 'AI':
|
||||
messageDiv.className += ' ai';
|
||||
break;
|
||||
case 'SYSTEM':
|
||||
messageDiv.className += ' system';
|
||||
break;
|
||||
default:
|
||||
messageDiv.className += ' system';
|
||||
}
|
||||
|
||||
// 根据消息类型设置样式
|
||||
if (messageData.type === 'ERROR') {
|
||||
messageDiv.className = 'message error';
|
||||
}
|
||||
|
||||
// 设置消息内容
|
||||
let content = messageData.content;
|
||||
if (messageData.createTime) {
|
||||
content += ' <small>(' + messageData.createTime + ')</small>';
|
||||
}
|
||||
|
||||
messageDiv.innerHTML = content;
|
||||
messagesDiv.appendChild(messageDiv);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
}
|
||||
|
||||
function updateStatus(message, isConnected) {
|
||||
const statusDiv = document.getElementById('status');
|
||||
statusDiv.textContent = message;
|
||||
statusDiv.className = 'status ' + (isConnected ? 'connected' : 'disconnected');
|
||||
}
|
||||
|
||||
function clearMessages() {
|
||||
document.getElementById('messages').innerHTML = '';
|
||||
}
|
||||
|
||||
function handleKeyPress(event) {
|
||||
if (event.key === 'Enter') {
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后自动连接
|
||||
window.onload = function() {
|
||||
// 可以在这里自动连接
|
||||
// connect();
|
||||
};
|
||||
|
||||
// 页面关闭时断开连接
|
||||
window.onbeforeunload = function() {
|
||||
if (connected) {
|
||||
disconnect();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
com/emotionmuseum/websocket/service/ChatWebSocketService.class
|
||||
com/emotionmuseum/websocket/feign/AiServiceClient.class
|
||||
com/emotionmuseum/websocket/controller/WebSocketTestController.class
|
||||
com/emotionmuseum/websocket/config/WebSocketConfig.class
|
||||
com/emotionmuseum/websocket/dto/WebSocketMessage$SenderType.class
|
||||
com/emotionmuseum/websocket/manager/WebSocketSessionManager$SessionInfo.class
|
||||
com/emotionmuseum/websocket/dto/ChatRequest.class
|
||||
com/emotionmuseum/websocket/service/impl/AiChatServiceImpl.class
|
||||
com/emotionmuseum/websocket/dto/WebSocketMessage$MessageType.class
|
||||
com/emotionmuseum/websocket/service/impl/ChatWebSocketServiceImpl.class
|
||||
com/emotionmuseum/websocket/service/AiChatService.class
|
||||
com/emotionmuseum/websocket/dto/ChatRequest$ChatRequestBuilder.class
|
||||
com/emotionmuseum/websocket/manager/WebSocketSessionManager.class
|
||||
com/emotionmuseum/websocket/dto/WebSocketMessage$MessageStatus.class
|
||||
com/emotionmuseum/websocket/dto/WebSocketMessage.class
|
||||
com/emotionmuseum/websocket/dto/WebSocketMessage$WebSocketMessageBuilder.class
|
||||
com/emotionmuseum/websocket/listener/WebSocketEventListener.class
|
||||
com/emotionmuseum/websocket/config/AsyncConfig.class
|
||||
com/emotionmuseum/websocket/WebsocketApplication.class
|
||||
com/emotionmuseum/websocket/controller/ChatWebSocketController.class
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-websocket/src/main/java/com/emotionmuseum/websocket/feign/AiServiceClient.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-websocket/src/main/java/com/emotionmuseum/websocket/service/AiChatService.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-websocket/src/main/java/com/emotionmuseum/websocket/service/ChatWebSocketService.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-websocket/src/main/java/com/emotionmuseum/websocket/controller/WebSocketTestController.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-websocket/src/main/java/com/emotionmuseum/websocket/WebsocketApplication.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-websocket/src/main/java/com/emotionmuseum/websocket/dto/WebSocketMessage.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-websocket/src/main/java/com/emotionmuseum/websocket/config/AsyncConfig.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-websocket/src/main/java/com/emotionmuseum/websocket/service/impl/ChatWebSocketServiceImpl.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-websocket/src/main/java/com/emotionmuseum/websocket/manager/WebSocketSessionManager.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-websocket/src/main/java/com/emotionmuseum/websocket/controller/ChatWebSocketController.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-websocket/src/main/java/com/emotionmuseum/websocket/listener/WebSocketEventListener.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-websocket/src/main/java/com/emotionmuseum/websocket/dto/ChatRequest.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-websocket/src/main/java/com/emotionmuseum/websocket/config/WebSocketConfig.java
|
||||
/Users/huazhongmin/peanut/AppleDevelop/EmotionMuseum/backend/emotion-websocket/src/main/java/com/emotionmuseum/websocket/service/impl/AiChatServiceImpl.java
|
||||
Reference in New Issue
Block a user