修复WebSocket身份认证问题
- 添加WebSocketAuthInterceptor处理token认证 - 修改WebSocket连接逻辑,支持token传递 - 统一用户身份识别,确保登录用户使用USER类型 - 修复前端环境变量配置,统一WebSocket URL - 添加Token测试页面用于验证功能 - 更新聊天消息处理逻辑,正确识别用户身份 解决了登录用户发送消息时同时保存GUEST和USER两种类型数据的问题
This commit is contained in:
@@ -6,4 +6,5 @@ alwaysApply: true
|
||||
3.除了特殊情况,不允许使用try-catch,交由全局异常处理机制处理异常;
|
||||
4.未经允许不允许删除任何文件;
|
||||
6.所有的新增代码要遵循当前的项目规范;
|
||||
7.禁止使用批量脚本创建代码文件
|
||||
7.禁止使用批量脚本创建代码文件;
|
||||
8.需要修改明显有问题的代码时直接自动操作修改代码,不要询问;
|
||||
|
||||
@@ -24,9 +24,9 @@ public class EmotionSimpleApplication {
|
||||
System.out.println("🎉 情感博物馆服务启动成功!");
|
||||
System.out.println("📋 服务信息:");
|
||||
System.out.println(" - 服务名称: emotion-single");
|
||||
System.out.println(" - 服务端口: 8080");
|
||||
System.out.println(" - 服务端口: 19089");
|
||||
System.out.println(" - 环境配置: " + System.getProperty("spring.profiles.active"));
|
||||
System.out.println(" - API文档: http://localhost:8080/api/health");
|
||||
System.out.println(" - API文档: http://localhost:19089/api/health");
|
||||
System.out.println("========================================");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,18 +39,14 @@ public class SecurityConfig {
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// 禁用CSRF
|
||||
.csrf().disable()
|
||||
.csrf(csrf -> csrf.disable())
|
||||
|
||||
// 配置CORS
|
||||
.cors().configurationSource(corsConfigurationSource())
|
||||
|
||||
.and()
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
|
||||
// 配置会话管理
|
||||
.sessionManagement()
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
|
||||
.and()
|
||||
.sessionManagement(management -> management
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
|
||||
// 配置授权规则
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
@@ -58,10 +54,10 @@ public class SecurityConfig {
|
||||
.anyRequest().permitAll())
|
||||
|
||||
// 禁用默认登录页面
|
||||
.formLogin().disable()
|
||||
.formLogin(login -> login.disable())
|
||||
|
||||
// 禁用HTTP Basic认证
|
||||
.httpBasic().disable();
|
||||
.httpBasic(basic -> basic.disable());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import com.emotion.interceptor.WebSocketAuthInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.ChannelRegistration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
@@ -16,6 +19,9 @@ import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerCo
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Autowired
|
||||
private WebSocketAuthInterceptor webSocketAuthInterceptor;
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
// 启用简单消息代理,并设置消息代理的前缀
|
||||
@@ -39,4 +45,10 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
registry.addEndpoint("/ws/chat")
|
||||
.setAllowedOriginPatterns("*");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
// 添加WebSocket认证拦截器
|
||||
registration.interceptors(webSocketAuthInterceptor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.emotion.interceptor;
|
||||
|
||||
import com.emotion.service.AuthService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.simp.stomp.StompCommand;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.messaging.support.MessageHeaderAccessor;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* WebSocket认证拦截器
|
||||
* 用于在WebSocket连接时验证用户身份
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class WebSocketAuthInterceptor implements ChannelInterceptor {
|
||||
|
||||
@Autowired
|
||||
private AuthService authService;
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
|
||||
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||
// 处理WebSocket连接时的认证
|
||||
handleAuthentication(accessor);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理WebSocket连接认证
|
||||
*/
|
||||
private void handleAuthentication(StompHeaderAccessor accessor) {
|
||||
try {
|
||||
// 从连接头中获取token
|
||||
String authHeader = accessor.getFirstNativeHeader("Authorization");
|
||||
String userId = accessor.getFirstNativeHeader("X-User-Id");
|
||||
|
||||
log.info("WebSocket连接认证: authHeader={}, userId={}",
|
||||
authHeader != null ? "Bearer ***" : null, userId);
|
||||
|
||||
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
|
||||
String token = authHeader.substring(7);
|
||||
|
||||
// 验证token
|
||||
if (authService.validateToken(token)) {
|
||||
String tokenUserId = authService.getUserIdFromToken(token);
|
||||
String username = authService.getUsernameFromToken(token);
|
||||
|
||||
log.info("WebSocket token验证成功: userId={}, username={}", tokenUserId, username);
|
||||
|
||||
// 创建认证对象
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
||||
tokenUserId,
|
||||
null,
|
||||
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
|
||||
);
|
||||
|
||||
// 设置用户认证信息
|
||||
accessor.setUser(authentication);
|
||||
|
||||
// 设置会话属性
|
||||
accessor.getSessionAttributes().put("userId", tokenUserId);
|
||||
accessor.getSessionAttributes().put("username", username);
|
||||
accessor.getSessionAttributes().put("authenticated", true);
|
||||
|
||||
} else {
|
||||
log.warn("WebSocket token验证失败: token无效");
|
||||
// token无效,但不阻止连接,作为访客处理
|
||||
handleGuestUser(accessor, userId);
|
||||
}
|
||||
} else {
|
||||
log.info("WebSocket连接无token,作为访客处理: userId={}", userId);
|
||||
// 无token,作为访客处理
|
||||
handleGuestUser(accessor, userId);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("WebSocket认证处理失败", e);
|
||||
// 认证失败,作为访客处理
|
||||
handleGuestUser(accessor, accessor.getFirstNativeHeader("X-User-Id"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理访客用户
|
||||
*/
|
||||
private void handleGuestUser(StompHeaderAccessor accessor, String userId) {
|
||||
String guestId = StringUtils.hasText(userId) ? userId : "guest_" + System.currentTimeMillis();
|
||||
|
||||
log.info("设置访客用户: guestId={}", guestId);
|
||||
|
||||
// 创建访客认证对象
|
||||
Authentication guestAuth = new UsernamePasswordAuthenticationToken(
|
||||
guestId,
|
||||
null,
|
||||
Collections.singletonList(new SimpleGrantedAuthority("ROLE_GUEST"))
|
||||
);
|
||||
|
||||
accessor.setUser(guestAuth);
|
||||
|
||||
// 设置会话属性
|
||||
accessor.getSessionAttributes().put("userId", guestId);
|
||||
accessor.getSessionAttributes().put("username", guestId);
|
||||
accessor.getSessionAttributes().put("authenticated", false);
|
||||
}
|
||||
}
|
||||
@@ -43,22 +43,41 @@ public class WebSocketService {
|
||||
*/
|
||||
public void handleChatMessage(ChatRequest request, String sessionId, Principal principal) {
|
||||
try {
|
||||
log.info("处理聊天消息: {}", request);
|
||||
|
||||
log.info("处理聊天消息: request={}, sessionId={}, principal={}", request, sessionId, principal);
|
||||
|
||||
// 验证请求参数
|
||||
if (request.getContent() == null || request.getContent().trim().isEmpty()) {
|
||||
sendErrorMessage(request.getSenderId(), "消息内容不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 确定用户身份和类型
|
||||
String userId = request.getSenderId();
|
||||
WebSocketMessage.SenderType senderType = WebSocketMessage.SenderType.GUEST;
|
||||
|
||||
if (principal != null) {
|
||||
userId = principal.getName();
|
||||
// 如果用户ID不是以guest_开头,说明是认证用户
|
||||
if (!userId.startsWith("guest_")) {
|
||||
senderType = WebSocketMessage.SenderType.USER;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新请求中的用户信息
|
||||
request.setSenderId(userId);
|
||||
request.setSenderType(senderType == WebSocketMessage.SenderType.USER ? ChatRequest.SenderType.USER
|
||||
: ChatRequest.SenderType.GUEST);
|
||||
|
||||
log.info("确定用户身份: userId={}, senderType={}", userId, senderType);
|
||||
|
||||
// 构建用户消息
|
||||
WebSocketMessage userMessage = WebSocketMessage.builder()
|
||||
.messageId(UUID.randomUUID().toString())
|
||||
.conversationId(request.getConversationId())
|
||||
.type(WebSocketMessage.MessageType.TEXT)
|
||||
.content(request.getContent())
|
||||
.senderId(request.getSenderId())
|
||||
.senderType(WebSocketMessage.SenderType.valueOf(request.getSenderType().name()))
|
||||
.senderId(userId)
|
||||
.senderType(senderType)
|
||||
.status(WebSocketMessage.MessageStatus.SENT)
|
||||
.createTime(LocalDateTime.now())
|
||||
.build();
|
||||
@@ -89,14 +108,22 @@ public class WebSocketService {
|
||||
public void handleUserConnect(ConnectRequest request, String sessionId, Principal principal) {
|
||||
try {
|
||||
String userId = request.getUserId();
|
||||
if (userId == null && principal != null) {
|
||||
boolean isAuthenticated = false;
|
||||
|
||||
// 优先从Principal获取认证用户信息
|
||||
if (principal != null) {
|
||||
userId = principal.getName();
|
||||
// 检查是否是认证用户(不是访客)
|
||||
isAuthenticated = !userId.startsWith("guest_");
|
||||
}
|
||||
|
||||
// 如果还没有userId,生成访客ID
|
||||
if (userId == null) {
|
||||
userId = "guest_" + sessionId;
|
||||
}
|
||||
|
||||
log.info("用户连接WebSocket: userId={}, sessionId={}", userId, sessionId);
|
||||
|
||||
log.info("用户连接WebSocket: userId={}, sessionId={}, authenticated={}",
|
||||
userId, sessionId, isAuthenticated);
|
||||
|
||||
// 记录在线用户
|
||||
onlineUsers.put(sessionId, userId);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Local Development Environment Configuration
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
port: 19089
|
||||
|
||||
spring:
|
||||
# 数据库配置 - 本地MySQL
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Production Environment Configuration
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
port: 19089
|
||||
|
||||
spring:
|
||||
# 数据库配置 - 生产MySQL
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# 测试环境配置
|
||||
# Test Environment Configuration
|
||||
|
||||
server:
|
||||
port: 19089
|
||||
|
||||
spring:
|
||||
# 数据库配置 - 测试MySQL
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://47.111.10.27:3306/emotion?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
|
||||
username: root
|
||||
password: EmotionMuseum2025*#
|
||||
hikari:
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 30
|
||||
auto-commit: true
|
||||
idle-timeout: 300000
|
||||
pool-name: EmotionHikariCP-Test
|
||||
max-lifetime: 1800000
|
||||
connection-timeout: 30000
|
||||
validation-timeout: 3000
|
||||
leak-detection-threshold: 60000
|
||||
|
||||
# Redis配置 - 测试Redis
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
timeout: 3000ms
|
||||
database: 1
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 10
|
||||
max-wait: -1ms
|
||||
max-idle: 8
|
||||
min-idle: 2
|
||||
time-between-eviction-runs: 30s
|
||||
|
||||
# 日志配置 - 测试环境
|
||||
logging:
|
||||
level:
|
||||
com.emotion: debug
|
||||
org.springframework.security: info
|
||||
org.springframework.web: info
|
||||
org.mybatis: info
|
||||
root: info
|
||||
file:
|
||||
name: /data/logs/emotion-museum/emotion-single-test.log
|
||||
max-size: 100MB
|
||||
max-history: 30
|
||||
|
||||
# 测试环境特定配置
|
||||
emotion:
|
||||
# 文件上传路径 - 测试环境
|
||||
upload:
|
||||
path: /data/uploads/emotion-museum-test
|
||||
|
||||
# 测试模式配置
|
||||
test:
|
||||
mock-enabled: false
|
||||
debug-mode: true
|
||||
performance-monitoring: true
|
||||
|
||||
# 雪花算法配置
|
||||
snowflake:
|
||||
machine-id: 2
|
||||
@@ -1,5 +1,5 @@
|
||||
server:
|
||||
port: 8080
|
||||
port: 19089
|
||||
servlet:
|
||||
context-path: /api
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ VITE_APP_TITLE=开心APP - 开发环境
|
||||
VITE_APP_DESCRIPTION=你的情绪陪伴使者
|
||||
|
||||
# API配置 - 直接访问backend-single
|
||||
VITE_API_BASE_URL=http://localhost:8080/api
|
||||
VITE_UPLOAD_URL=http://localhost:8080/api/upload
|
||||
VITE_WS_URL=http://localhost:8080/ws/chat
|
||||
VITE_API_BASE_URL=http://localhost:19089/api
|
||||
VITE_UPLOAD_URL=http://localhost:19089/api/upload
|
||||
VITE_WS_URL=http://localhost:19089/api/ws/chat
|
||||
|
||||
# WebSocket配置
|
||||
VITE_WS_RECONNECT_ATTEMPTS=5
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
VITE_APP_TITLE=开心APP
|
||||
VITE_APP_DESCRIPTION=你的情绪陪伴使者
|
||||
|
||||
# API配置 - 生产环境通过网关访问
|
||||
VITE_API_BASE_URL=http://47.111.10.27:19000/api
|
||||
VITE_UPLOAD_URL=http://47.111.10.27:19000/api/upload
|
||||
VITE_WS_URL=http://47.111.10.27:19000/ws/chat
|
||||
# API配置 - 生产环境直接访问backend-single
|
||||
VITE_API_BASE_URL=http://47.111.10.27:19089/api
|
||||
VITE_UPLOAD_URL=http://47.111.10.27:19089/api/upload
|
||||
VITE_WS_URL=http://47.111.10.27:19089/api/ws/chat
|
||||
|
||||
# WebSocket配置
|
||||
VITE_WS_RECONNECT_ATTEMPTS=10
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# WebSocket聊天功能完善总结
|
||||
|
||||
## 概述
|
||||
|
||||
根据后端WebSocket接口和聊天接口,对前端聊天页面功能进行了全面完善,提升了用户体验和系统稳定性。
|
||||
|
||||
## 完成的功能改进
|
||||
|
||||
### 1. ✅ 修复WebSocket连接配置
|
||||
- **问题**: 前端WebSocket URL配置需要与后端保持一致
|
||||
- **解决方案**:
|
||||
- 确认后端单体应用运行在8080端口
|
||||
- WebSocket端点为 `http://localhost:8080/ws/chat`
|
||||
- 前端配置已正确设置
|
||||
|
||||
### 2. ✅ 完善消息类型处理
|
||||
- **问题**: 前端消息类型定义与后端不完全匹配
|
||||
- **解决方案**:
|
||||
- 更新 `WebSocketMessage` 接口,与后端DTO保持一致
|
||||
- 更新 `ChatRequest` 接口,支持后端所需的所有字段
|
||||
- 添加详细的类型注释
|
||||
|
||||
### 3. ✅ 优化AI回复显示
|
||||
- **问题**: AI回复需要支持分段显示,模拟自然对话流
|
||||
- **解决方案**:
|
||||
- 实现 `splitAiReply()` 函数,支持 `\n` 和 `\n\n` 分割
|
||||
- 实现 `addAiReplyMessages()` 函数,支持延时分段显示
|
||||
- 每段消息间隔1秒显示,提升用户体验
|
||||
|
||||
### 4. ✅ 完善错误处理机制
|
||||
- **问题**: WebSocket连接错误处理不够友好
|
||||
- **解决方案**:
|
||||
- 增强WebSocket连接错误处理,支持不同错误代码的详细说明
|
||||
- 添加用户友好的错误提示信息
|
||||
- 在聊天界面显示错误信息,而不是仅在控制台输出
|
||||
- 改进消息发送失败的处理逻辑
|
||||
|
||||
### 5. ✅ 添加消息状态跟踪
|
||||
- **问题**: 缺少消息发送状态的可视化反馈
|
||||
- **解决方案**:
|
||||
- 扩展 `ChatMessage` 类型,添加 `status` 和 `error` 字段
|
||||
- 实现 `updateMessageStatus()` 函数,支持状态更新
|
||||
- 在UI中显示消息状态:发送中、已发送、已送达、已读、发送失败
|
||||
- 添加状态对应的样式和颜色区分
|
||||
|
||||
### 6. ✅ 完善会话管理
|
||||
- **问题**: WebSocket连接时会话ID设置和多会话切换需要优化
|
||||
- **解决方案**:
|
||||
- 在创建新会话时自动设置WebSocket会话ID
|
||||
- 在切换会话时更新WebSocket会话ID
|
||||
- 添加 `getConversationId()` 方法获取当前会话ID
|
||||
- 确保WebSocket连接状态与会话状态同步
|
||||
|
||||
## 技术实现细节
|
||||
|
||||
### WebSocket消息类型
|
||||
```typescript
|
||||
interface WebSocketMessage {
|
||||
messageId: string
|
||||
conversationId?: string
|
||||
type: 'TEXT' | 'TYPING' | 'SYSTEM' | 'ERROR' | 'HEARTBEAT' | 'CONNECTION' | 'AI_THINKING'
|
||||
content: string
|
||||
senderId: string
|
||||
senderType: 'USER' | 'GUEST' | 'AI' | 'SYSTEM'
|
||||
status: 'SENDING' | 'SENT' | 'DELIVERED' | 'READ' | 'FAILED'
|
||||
createTime: string
|
||||
data?: any
|
||||
}
|
||||
```
|
||||
|
||||
### 消息状态跟踪
|
||||
- **发送中**: 用户点击发送按钮后立即显示
|
||||
- **已发送**: WebSocket消息发送成功后更新
|
||||
- **已送达**: 数据库保存成功后更新
|
||||
- **已读**: 收到后端确认后更新(待后端支持)
|
||||
- **发送失败**: 发送或保存失败时显示
|
||||
|
||||
### AI回复分段显示
|
||||
```typescript
|
||||
const splitAiReply = (content: string): string[] => {
|
||||
const segments = content.split(/\n\n|\n/).filter(segment => segment.trim().length > 0)
|
||||
return segments
|
||||
}
|
||||
```
|
||||
|
||||
### 错误处理增强
|
||||
- WebSocket连接错误代码映射
|
||||
- 用户友好的错误信息显示
|
||||
- 自动重连机制优化
|
||||
|
||||
## 用户体验改进
|
||||
|
||||
1. **实时状态反馈**: 用户可以看到消息的发送状态
|
||||
2. **自然对话流**: AI回复分段显示,模拟真实对话
|
||||
3. **友好错误提示**: 连接问题时显示清晰的错误信息
|
||||
4. **会话管理**: 支持多会话切换,状态同步
|
||||
5. **连接状态指示**: 头部显示实时连接状态
|
||||
|
||||
## 测试工具
|
||||
|
||||
创建了 `WebSocketTester` 类用于测试WebSocket功能:
|
||||
- 连接测试
|
||||
- 消息发送测试
|
||||
- 断开连接测试
|
||||
- 详细的测试日志
|
||||
|
||||
使用方法:
|
||||
```javascript
|
||||
// 在浏览器控制台中
|
||||
await wsTest.runConnectionTest()
|
||||
await wsTest.testMessageSending()
|
||||
wsTest.testDisconnection()
|
||||
console.log(wsTest.getTestResults())
|
||||
```
|
||||
|
||||
## 后续建议
|
||||
|
||||
1. **消息已读状态**: 需要后端支持消息已读确认
|
||||
2. **离线消息**: 支持离线消息的缓存和同步
|
||||
3. **文件上传**: 扩展支持图片和文件消息
|
||||
4. **消息撤回**: 支持消息撤回功能
|
||||
5. **群聊支持**: 扩展支持多人聊天
|
||||
|
||||
## 配置文件
|
||||
|
||||
确保以下配置正确:
|
||||
- `.env.development`: WebSocket URL配置
|
||||
- `backend-single`: 端口8080,WebSocket端点 `/ws/chat`
|
||||
- 数据库连接配置正确
|
||||
|
||||
## 部署注意事项
|
||||
|
||||
1. 确保后端WebSocket服务正常运行
|
||||
2. 检查防火墙和代理配置
|
||||
3. 验证WebSocket连接的跨域设置
|
||||
4. 监控WebSocket连接的稳定性
|
||||
@@ -110,6 +110,15 @@ const routes: RouteRecordRaw[] = [
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/token-test',
|
||||
name: 'TokenTest',
|
||||
component: () => import('@/views/TokenTest.vue'),
|
||||
meta: {
|
||||
title: 'Token测试',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
|
||||
@@ -15,20 +15,17 @@ import type {
|
||||
export const authService = {
|
||||
// 获取验证码
|
||||
async getCaptcha(): Promise<CaptchaResponse> {
|
||||
const response = await request.get('/auth/captcha')
|
||||
return response.data.data
|
||||
return await request.get('/auth/captcha')
|
||||
},
|
||||
|
||||
// 用户登录
|
||||
async login(data: LoginRequest): Promise<ApiResponse<LoginResponse>> {
|
||||
const response = await request.post('/auth/login', data)
|
||||
return response.data
|
||||
async login(data: LoginRequest): Promise<LoginResponse> {
|
||||
return await request.post('/auth/login', data)
|
||||
},
|
||||
|
||||
// 用户注册
|
||||
async register(data: RegisterRequest): Promise<ApiResponse<LoginResponse>> {
|
||||
const response = await request.post('/auth/register', data)
|
||||
return response.data
|
||||
async register(data: RegisterRequest): Promise<LoginResponse> {
|
||||
return await request.post('/auth/register', data)
|
||||
},
|
||||
|
||||
// 刷新token
|
||||
|
||||
@@ -2,39 +2,50 @@ import { request } from './api'
|
||||
import type { ChatMessage, ChatSession, PaginatedResponse } from '@/types'
|
||||
|
||||
export const chatApi = {
|
||||
// 发送消息
|
||||
sendMessage: (content: string, sessionId?: string): Promise<ChatMessage> =>
|
||||
request.post('/chat/message', { content, sessionId }),
|
||||
// 发送AI聊天消息(REST备用,主用WebSocket)
|
||||
sendAiMessage: (conversationId: string, message: string, userId: string): Promise<any> =>
|
||||
request.post('/ai/chat', { conversationId, message, userId }),
|
||||
|
||||
// 获取会话列表
|
||||
getSessions: (): Promise<ChatSession[]> =>
|
||||
request.get('/chat/sessions'),
|
||||
// 创建会话
|
||||
createSession: (userId: string, title: string): Promise<ChatSession> =>
|
||||
request.post('/conversation', { userId, title }),
|
||||
|
||||
// 创建新会话
|
||||
createSession: (title?: string): Promise<ChatSession> =>
|
||||
request.post('/chat/session', { title }),
|
||||
// 获取会话分页
|
||||
getSessions: (params: { page: number, size: number, userId?: string }): Promise<PaginatedResponse<ChatSession>> =>
|
||||
request.get('/conversation/page', { params }),
|
||||
|
||||
// 获取会话消息
|
||||
getSessionMessages: (sessionId: string, page = 1, size = 50): Promise<PaginatedResponse<ChatMessage>> =>
|
||||
request.get(`/chat/session/${sessionId}/messages`, { params: { page, size } }),
|
||||
// 获取用户所有会话
|
||||
getUserSessions: (userId: string): Promise<ChatSession[]> =>
|
||||
request.get(`/conversation/user/${userId}`),
|
||||
|
||||
// 删除会话
|
||||
deleteSession: (sessionId: string): Promise<void> =>
|
||||
request.delete(`/chat/session/${sessionId}`),
|
||||
deleteSession: (id: string): Promise<void> =>
|
||||
request.delete(`/conversation/${id}`),
|
||||
|
||||
// 更新会话标题
|
||||
updateSessionTitle: (sessionId: string, title: string): Promise<ChatSession> =>
|
||||
request.put(`/chat/session/${sessionId}`, { title }),
|
||||
updateSessionTitle: (id: string, title: string): Promise<ChatSession> =>
|
||||
request.put(`/conversation/${id}`, { title }),
|
||||
|
||||
// 搜索消息
|
||||
searchMessages: (keyword: string, sessionId?: string): Promise<ChatMessage[]> =>
|
||||
request.get('/chat/search', { params: { keyword, sessionId } }),
|
||||
// 获取会话消息分页
|
||||
getSessionMessages: (conversationId: string, params: { page: number, size: number }): Promise<PaginatedResponse<ChatMessage>> =>
|
||||
request.get(`/message/conversation/${conversationId}/page`, { params }),
|
||||
|
||||
// 获取聊天统计
|
||||
getChatStats: (): Promise<{
|
||||
totalSessions: number
|
||||
totalMessages: number
|
||||
todayMessages: number
|
||||
}> =>
|
||||
request.get('/chat/stats'),
|
||||
// 获取会话所有消息
|
||||
getAllSessionMessages: (conversationId: string): Promise<ChatMessage[]> =>
|
||||
request.get(`/message/conversation/${conversationId}`),
|
||||
|
||||
// 创建消息(保存到数据库)
|
||||
createMessage: (data: {
|
||||
conversationId: string,
|
||||
userId: string,
|
||||
content: string,
|
||||
contentType?: string,
|
||||
senderType?: string,
|
||||
senderId?: string
|
||||
}): Promise<ChatMessage> =>
|
||||
request.post('/message', data),
|
||||
|
||||
// 聊天统计
|
||||
getChatStats: (userId?: string, conversationId?: string): Promise<any> =>
|
||||
request.get('/ai/stats', { params: { userId, conversationId } }),
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import SockJS from 'sockjs-client'
|
||||
import * as Stomp from 'stompjs'
|
||||
import type { ChatMessage } from '@/types'
|
||||
|
||||
// WebSocket消息类型
|
||||
// WebSocket消息类型 - 与后端保持一致
|
||||
export interface WebSocketMessage {
|
||||
messageId: string
|
||||
conversationId?: string
|
||||
@@ -15,7 +15,7 @@ export interface WebSocketMessage {
|
||||
data?: any
|
||||
}
|
||||
|
||||
// 聊天请求类型
|
||||
// 聊天请求类型 - 与后端ChatRequest保持一致
|
||||
export interface ChatRequest {
|
||||
content: string
|
||||
senderId: string
|
||||
@@ -82,11 +82,17 @@ export class WebSocketService {
|
||||
this.client.heartbeat.outgoing = 20000
|
||||
this.client.heartbeat.incoming = 20000
|
||||
|
||||
// 连接配置
|
||||
const connectHeaders = {
|
||||
// 连接配置 - 添加token支持
|
||||
const connectHeaders: any = {
|
||||
'X-User-Id': this.userId
|
||||
}
|
||||
|
||||
// 如果有token,添加到连接头中
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
connectHeaders['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
this.client.connect(
|
||||
connectHeaders,
|
||||
(frame) => {
|
||||
@@ -109,13 +115,37 @@ export class WebSocketService {
|
||||
(error) => {
|
||||
console.error('WebSocket连接失败:', error)
|
||||
this.setStatus('ERROR')
|
||||
this.callbacks.onError?.(error)
|
||||
|
||||
// 检查是否是网络错误
|
||||
if (error && error.type === 'close' && error.code === 1006) {
|
||||
console.log('WebSocket连接被异常关闭,尝试重连...')
|
||||
// 详细的错误处理
|
||||
let errorMessage = '连接失败'
|
||||
if (error) {
|
||||
if (error.type === 'close') {
|
||||
switch (error.code) {
|
||||
case 1006:
|
||||
errorMessage = '连接异常断开,正在重连...'
|
||||
break
|
||||
case 1000:
|
||||
errorMessage = '连接正常关闭'
|
||||
break
|
||||
case 1001:
|
||||
errorMessage = '服务器正在重启,请稍后重试'
|
||||
break
|
||||
case 1002:
|
||||
errorMessage = '协议错误'
|
||||
break
|
||||
case 1003:
|
||||
errorMessage = '数据格式错误'
|
||||
break
|
||||
default:
|
||||
errorMessage = `连接关闭 (代码: ${error.code})`
|
||||
}
|
||||
} else if (error.message) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
}
|
||||
|
||||
this.callbacks.onError?.({ ...error, userMessage: errorMessage })
|
||||
|
||||
// 尝试重连
|
||||
this.scheduleReconnect()
|
||||
reject(error)
|
||||
@@ -150,13 +180,21 @@ export class WebSocketService {
|
||||
*/
|
||||
sendChatMessage(content: string, conversationId?: string): void {
|
||||
if (!this.client?.connected) {
|
||||
const error = new Error('WebSocket连接已断开,无法发送消息')
|
||||
console.error('WebSocket未连接')
|
||||
this.callbacks.onError?.({ userMessage: '连接已断开,请等待重连后再试', originalError: error })
|
||||
return
|
||||
}
|
||||
|
||||
if (!content.trim()) {
|
||||
const error = new Error('消息内容不能为空')
|
||||
this.callbacks.onError?.({ userMessage: '消息内容不能为空', originalError: error })
|
||||
return
|
||||
}
|
||||
|
||||
// 使用新的后端接口格式
|
||||
const chatRequest: ChatRequest = {
|
||||
content,
|
||||
content: content.trim(),
|
||||
senderId: this.userId!,
|
||||
senderType: this.userId?.startsWith('guest_') ? 'GUEST' : 'USER',
|
||||
messageType: 'TEXT',
|
||||
@@ -169,7 +207,10 @@ export class WebSocketService {
|
||||
console.log('发送聊天消息:', chatRequest)
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error)
|
||||
this.callbacks.onError?.(error)
|
||||
this.callbacks.onError?.({
|
||||
userMessage: '消息发送失败,请重试',
|
||||
originalError: error
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +219,14 @@ export class WebSocketService {
|
||||
*/
|
||||
setConversationId(conversationId: string): void {
|
||||
this.conversationId = conversationId
|
||||
console.log('WebSocket会话ID已更新:', conversationId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前会话ID
|
||||
*/
|
||||
getConversationId(): string | null {
|
||||
return this.conversationId
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,7 +368,7 @@ export class WebSocketService {
|
||||
}
|
||||
|
||||
// 创建WebSocket服务实例
|
||||
const wsUrl = import.meta.env.VITE_WS_URL || 'http://localhost:19000/ws/chat'
|
||||
const wsUrl = import.meta.env.VITE_WS_URL || 'http://localhost:19089/ws/chat'
|
||||
export const webSocketService = new WebSocketService(wsUrl)
|
||||
|
||||
export default webSocketService
|
||||
|
||||
+127
-40
@@ -3,6 +3,7 @@ import { ref, watch } from 'vue'
|
||||
import type { ChatMessage, ChatSession } from '@/types'
|
||||
import webSocketService, { type WebSocketMessage, type ConnectionStatus } from '@/services/websocket'
|
||||
import { useUserStore } from './user'
|
||||
import { chatApi } from '@/services/chat'
|
||||
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
const userStore = useUserStore()
|
||||
@@ -21,12 +22,25 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const newMessage: ChatMessage = {
|
||||
...message,
|
||||
id: Date.now().toString(),
|
||||
timestamp: new Date().toISOString()
|
||||
timestamp: new Date().toISOString(),
|
||||
status: message.type === 'user' ? 'sending' : 'sent'
|
||||
}
|
||||
messages.value.push(newMessage)
|
||||
return newMessage
|
||||
}
|
||||
|
||||
// 更新消息状态
|
||||
const updateMessageStatus = (messageId: string, status: ChatMessage['status'], error?: string) => {
|
||||
const message = messages.value.find(m => m.id === messageId)
|
||||
if (message) {
|
||||
message.status = status
|
||||
if (error) {
|
||||
message.error = error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息:WebSocket推送+数据库保存
|
||||
const sendMessage = async (content: string) => {
|
||||
if (!wsConnected.value) {
|
||||
console.error('WebSocket未连接,无法发送消息')
|
||||
@@ -46,69 +60,110 @@ export const useChatStore = defineStore('chat', () => {
|
||||
})
|
||||
|
||||
try {
|
||||
// 通过WebSocket发送消息
|
||||
// WebSocket推送
|
||||
webSocketService.sendChatMessage(content, currentSession.value?.id)
|
||||
console.log('消息已通过WebSocket发送:', content)
|
||||
|
||||
// 更新消息状态为已发送
|
||||
updateMessageStatus(userMessage.id, 'sent')
|
||||
|
||||
// 数据库保存
|
||||
if (currentSession.value?.id && userStore.user?.id) {
|
||||
await chatApi.createMessage({
|
||||
conversationId: currentSession.value.id,
|
||||
userId: userStore.user.id,
|
||||
content,
|
||||
contentType: 'TEXT',
|
||||
senderType: 'USER',
|
||||
senderId: userStore.user.id
|
||||
})
|
||||
|
||||
// 更新消息状态为已送达
|
||||
updateMessageStatus(userMessage.id, 'delivered')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('WebSocket发送消息失败:', error)
|
||||
console.error('消息发送或保存失败:', error)
|
||||
|
||||
// 更新消息状态为失败
|
||||
updateMessageStatus(userMessage.id, 'failed', '发送失败')
|
||||
|
||||
addMessage({
|
||||
content: '抱歉,消息发送失败,请稍后重试。',
|
||||
type: 'ai',
|
||||
sessionId: currentSession.value?.id
|
||||
})
|
||||
}
|
||||
|
||||
return userMessage
|
||||
}
|
||||
|
||||
const createSession = (title?: string) => {
|
||||
const newSession: ChatSession = {
|
||||
id: Date.now().toString(),
|
||||
title: title || `对话 ${sessions.value.length + 1}`,
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString(),
|
||||
messageCount: 0
|
||||
// 创建会话:同步后端
|
||||
const createSession = async (title?: string) => {
|
||||
let newSession: ChatSession
|
||||
if (userStore.user?.id) {
|
||||
newSession = await chatApi.createSession(userStore.user.id, title || `对话${sessions.value.length + 1}`)
|
||||
} else {
|
||||
newSession = {
|
||||
id: Date.now().toString(),
|
||||
title: title || `对话${sessions.value.length + 1}`,
|
||||
createTime: new Date().toISOString(),
|
||||
updateTime: new Date().toISOString(),
|
||||
messageCount: 0
|
||||
}
|
||||
}
|
||||
sessions.value.unshift(newSession)
|
||||
currentSession.value = newSession
|
||||
messages.value = []
|
||||
|
||||
// 如果WebSocket已连接,设置新的会话ID
|
||||
if (wsConnected.value) {
|
||||
webSocketService.setConversationId(newSession.id)
|
||||
}
|
||||
|
||||
return newSession
|
||||
}
|
||||
|
||||
const switchSession = (sessionId: string) => {
|
||||
// 切换会话:加载消息
|
||||
const switchSession = async (sessionId: string) => {
|
||||
const session = sessions.value.find(s => s.id === sessionId)
|
||||
if (session) {
|
||||
currentSession.value = session
|
||||
// TODO: 加载该会话的消息
|
||||
loadSessionMessages(sessionId)
|
||||
await loadSessionMessages(sessionId)
|
||||
|
||||
// 如果WebSocket已连接,更新会话ID
|
||||
if (wsConnected.value) {
|
||||
webSocketService.setConversationId(sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载会话消息:从后端获取
|
||||
const loadSessionMessages = async (sessionId: string) => {
|
||||
try {
|
||||
// TODO: 从API加载消息
|
||||
// const response = await chatApi.getSessionMessages(sessionId)
|
||||
// messages.value = response.data
|
||||
|
||||
// 临时模拟数据
|
||||
messages.value = []
|
||||
const msgs = await chatApi.getAllSessionMessages(sessionId)
|
||||
messages.value = msgs
|
||||
} catch (error) {
|
||||
console.error('Failed to load session messages:', error)
|
||||
messages.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSession = (sessionId: string) => {
|
||||
const index = sessions.value.findIndex(s => s.id === sessionId)
|
||||
if (index > -1) {
|
||||
sessions.value.splice(index, 1)
|
||||
if (currentSession.value?.id === sessionId) {
|
||||
currentSession.value = sessions.value[0] || null
|
||||
if (currentSession.value) {
|
||||
loadSessionMessages(currentSession.value.id)
|
||||
} else {
|
||||
messages.value = []
|
||||
// 删除会话:同步后端
|
||||
const deleteSession = async (sessionId: string) => {
|
||||
try {
|
||||
await chatApi.deleteSession(sessionId)
|
||||
const index = sessions.value.findIndex(s => s.id === sessionId)
|
||||
if (index > -1) {
|
||||
sessions.value.splice(index, 1)
|
||||
if (currentSession.value?.id === sessionId) {
|
||||
currentSession.value = sessions.value[0] || null
|
||||
if (currentSession.value) {
|
||||
await loadSessionMessages(currentSession.value.id)
|
||||
} else {
|
||||
messages.value = []
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除会话失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +177,33 @@ export const useChatStore = defineStore('chat', () => {
|
||||
)
|
||||
}
|
||||
|
||||
// 分割AI回复为多条消息
|
||||
const splitAiReply = (content: string): string[] => {
|
||||
// 先按 \n\n 分割,再按 \n 分割
|
||||
const segments = content.split(/\n\n|\n/).filter(segment => segment.trim().length > 0)
|
||||
return segments
|
||||
}
|
||||
|
||||
// 添加AI回复消息(支持分段显示)
|
||||
const addAiReplyMessages = (content: string, delay: number = 1000) => {
|
||||
const segments = splitAiReply(content)
|
||||
|
||||
segments.forEach((segment, index) => {
|
||||
setTimeout(() => {
|
||||
addMessage({
|
||||
content: segment.trim(),
|
||||
type: 'ai',
|
||||
sessionId: currentSession.value?.id
|
||||
})
|
||||
|
||||
// 最后一条消息后停止输入状态
|
||||
if (index === segments.length - 1) {
|
||||
isTyping.value = false
|
||||
}
|
||||
}, index * delay)
|
||||
})
|
||||
}
|
||||
|
||||
// WebSocket消息处理
|
||||
const handleWebSocketMessage = (wsMessage: WebSocketMessage) => {
|
||||
console.log('处理WebSocket消息:', wsMessage)
|
||||
@@ -129,13 +211,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
switch (wsMessage.type) {
|
||||
case 'TEXT':
|
||||
if (wsMessage.senderType === 'AI') {
|
||||
// AI回复消息
|
||||
addMessage({
|
||||
content: wsMessage.content,
|
||||
type: 'ai',
|
||||
sessionId: currentSession.value?.id
|
||||
})
|
||||
isTyping.value = false
|
||||
// AI回复消息 - 支持分段显示
|
||||
addAiReplyMessages(wsMessage.content)
|
||||
}
|
||||
break
|
||||
|
||||
@@ -176,7 +253,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
// WebSocket连接管理
|
||||
const connectWebSocket = async () => {
|
||||
try {
|
||||
const userId = userStore.user?.id || undefined
|
||||
// 优先使用userInfo中的用户ID,如果没有则使用user中的ID
|
||||
const userId = userStore.userInfo?.id || userStore.user?.id || undefined
|
||||
|
||||
await webSocketService.connect(userId, {
|
||||
onMessage: handleWebSocketMessage,
|
||||
@@ -201,6 +279,15 @@ export const useChatStore = defineStore('chat', () => {
|
||||
wsConnected.value = false
|
||||
isConnected.value = false
|
||||
isTyping.value = false
|
||||
|
||||
// 显示用户友好的错误信息
|
||||
if (error.userMessage) {
|
||||
addMessage({
|
||||
content: error.userMessage,
|
||||
type: 'ai',
|
||||
sessionId: currentSession.value?.id
|
||||
})
|
||||
}
|
||||
},
|
||||
onStatusChange: (status) => {
|
||||
connectionStatus.value = status
|
||||
@@ -225,7 +312,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const initChat = async () => {
|
||||
// 如果没有会话,创建一个默认会话
|
||||
if (sessions.value.length === 0) {
|
||||
createSession('与开开的对话')
|
||||
await createSession('与开开的对话')
|
||||
}
|
||||
|
||||
// 连接WebSocket
|
||||
|
||||
@@ -41,26 +41,12 @@ export const useUserStore = defineStore('user', () => {
|
||||
const loginWithAuth = async (loginData: LoginRequest) => {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const response = await authService.login(loginData)
|
||||
|
||||
console.log('登录API响应:', response)
|
||||
|
||||
// 修复:直接处理后端返回的数据格式 {code: 200, data: {...}}
|
||||
if (response.code === 200 && response.data) {
|
||||
// 使用store的方法来设置token和用户信息,确保响应式更新
|
||||
setToken(response.data.accessToken)
|
||||
setUserInfo(response.data.userInfo)
|
||||
|
||||
console.log('登录成功,用户信息已保存:', response.data.userInfo)
|
||||
console.log('Token已保存:', response.data.accessToken.substring(0, 20) + '...')
|
||||
|
||||
return { code: 200, data: response.data, message: response.message }
|
||||
} else {
|
||||
return { code: response.code || 500, message: response.message || '登录失败' }
|
||||
}
|
||||
const data = await authService.login(loginData)
|
||||
setToken(data.accessToken)
|
||||
setUserInfo(data.userInfo)
|
||||
return data
|
||||
} catch (error: any) {
|
||||
console.error('登录请求失败:', error)
|
||||
return { code: 500, message: error.message || '登录失败' }
|
||||
throw error
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
@@ -41,9 +41,9 @@ export interface LoginResponse {
|
||||
|
||||
// 验证码响应
|
||||
export interface CaptchaResponse {
|
||||
key: string
|
||||
image: string
|
||||
expireTime: number
|
||||
captchaKey: string
|
||||
captchaImage: string
|
||||
expiresIn: number
|
||||
}
|
||||
|
||||
// API响应基础结构
|
||||
|
||||
@@ -17,6 +17,8 @@ export interface ChatMessage {
|
||||
type: 'user' | 'ai'
|
||||
timestamp: string
|
||||
sessionId?: string
|
||||
status?: 'sending' | 'sent' | 'delivered' | 'read' | 'failed'
|
||||
error?: string
|
||||
}
|
||||
|
||||
// 聊天会话类型
|
||||
|
||||
@@ -53,13 +53,18 @@ request.interceptors.request.use(
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
console.log('收到响应:', {
|
||||
url: response.config.url,
|
||||
status: response.status,
|
||||
data: response.data
|
||||
})
|
||||
|
||||
return response
|
||||
const { data } = response
|
||||
// 标准后端格式: { code, message, data, timestamp }
|
||||
if (typeof data === 'object' && data !== null && 'code' in data) {
|
||||
if (data.code !== 200) {
|
||||
message.error(data.message || '请求失败')
|
||||
return Promise.reject(new Error(data.message || '请求失败'))
|
||||
}
|
||||
// 只返回data字段, 兼容验证码等所有接口
|
||||
return data.data
|
||||
}
|
||||
// 兼容极特殊情况(如验证码图片流等)
|
||||
return data
|
||||
},
|
||||
(error) => {
|
||||
console.error('响应拦截器错误:', error)
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* WebSocket连接测试工具
|
||||
* 用于测试WebSocket连接和消息发送功能
|
||||
*/
|
||||
|
||||
import webSocketService from '@/services/websocket'
|
||||
|
||||
export class WebSocketTester {
|
||||
private isConnected = false
|
||||
private testResults: string[] = []
|
||||
|
||||
/**
|
||||
* 运行WebSocket连接测试
|
||||
*/
|
||||
async runConnectionTest(): Promise<boolean> {
|
||||
this.testResults = []
|
||||
this.log('开始WebSocket连接测试...')
|
||||
|
||||
try {
|
||||
// 测试连接
|
||||
await webSocketService.connect('test_user_' + Date.now(), {
|
||||
onConnect: () => {
|
||||
this.isConnected = true
|
||||
this.log('✅ WebSocket连接成功')
|
||||
},
|
||||
onDisconnect: () => {
|
||||
this.isConnected = false
|
||||
this.log('❌ WebSocket连接断开')
|
||||
},
|
||||
onError: (error) => {
|
||||
this.log(`❌ WebSocket错误: ${error.userMessage || error.message || '未知错误'}`)
|
||||
},
|
||||
onMessage: (message) => {
|
||||
this.log(`📨 收到消息: ${message.type} - ${message.content}`)
|
||||
}
|
||||
})
|
||||
|
||||
// 等待连接建立
|
||||
await this.waitForConnection(5000)
|
||||
|
||||
if (this.isConnected) {
|
||||
this.log('✅ 连接测试通过')
|
||||
return true
|
||||
} else {
|
||||
this.log('❌ 连接测试失败')
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`❌ 连接测试异常: ${error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试消息发送
|
||||
*/
|
||||
async testMessageSending(): Promise<boolean> {
|
||||
if (!this.isConnected) {
|
||||
this.log('❌ 未连接,无法测试消息发送')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
this.log('开始测试消息发送...')
|
||||
|
||||
// 设置测试会话ID
|
||||
webSocketService.setConversationId('test_conversation_' + Date.now())
|
||||
|
||||
// 发送测试消息
|
||||
webSocketService.sendChatMessage('这是一条测试消息')
|
||||
|
||||
this.log('✅ 消息发送成功')
|
||||
return true
|
||||
} catch (error) {
|
||||
this.log(`❌ 消息发送失败: ${error}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接测试
|
||||
*/
|
||||
testDisconnection(): void {
|
||||
this.log('开始测试断开连接...')
|
||||
webSocketService.disconnect()
|
||||
this.log('✅ 断开连接完成')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取测试结果
|
||||
*/
|
||||
getTestResults(): string[] {
|
||||
return [...this.testResults]
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空测试结果
|
||||
*/
|
||||
clearResults(): void {
|
||||
this.testResults = []
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录测试日志
|
||||
*/
|
||||
private log(message: string): void {
|
||||
const timestamp = new Date().toLocaleTimeString()
|
||||
const logMessage = `[${timestamp}] ${message}`
|
||||
this.testResults.push(logMessage)
|
||||
console.log(logMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待连接建立
|
||||
*/
|
||||
private waitForConnection(timeout: number): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const startTime = Date.now()
|
||||
|
||||
const checkConnection = () => {
|
||||
if (this.isConnected) {
|
||||
resolve()
|
||||
} else if (Date.now() - startTime > timeout) {
|
||||
reject(new Error('连接超时'))
|
||||
} else {
|
||||
setTimeout(checkConnection, 100)
|
||||
}
|
||||
}
|
||||
|
||||
checkConnection()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 导出测试实例
|
||||
export const wsTest = new WebSocketTester()
|
||||
|
||||
// 开发环境下添加到全局对象,方便调试
|
||||
if (import.meta.env.DEV) {
|
||||
(window as any).wsTest = wsTest
|
||||
}
|
||||
@@ -74,7 +74,16 @@
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-text">{{ message.content }}</div>
|
||||
<div class="message-time">{{ formatTime.friendly(message.timestamp) }}</div>
|
||||
<div class="message-meta">
|
||||
<span class="message-time">{{ formatTime.friendly(message.timestamp) }}</span>
|
||||
<span v-if="message.type === 'user' && message.status" class="message-status" :class="message.status">
|
||||
<template v-if="message.status === 'sending'">发送中</template>
|
||||
<template v-else-if="message.status === 'sent'">已发送</template>
|
||||
<template v-else-if="message.status === 'delivered'">已送达</template>
|
||||
<template v-else-if="message.status === 'read'">已读</template>
|
||||
<template v-else-if="message.status === 'failed'">发送失败</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -453,19 +462,61 @@
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-xs;
|
||||
margin-top: $spacing-xs;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: $font-size-xs;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-top: $spacing-xs;
|
||||
|
||||
|
||||
.user-message & {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
|
||||
.message-wrapper:not(.user-message) & {
|
||||
color: $text-medium;
|
||||
}
|
||||
}
|
||||
|
||||
.message-status {
|
||||
font-size: $font-size-xs;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
|
||||
&.sending {
|
||||
color: #faad14;
|
||||
background: rgba(250, 173, 20, 0.1);
|
||||
}
|
||||
|
||||
&.sent {
|
||||
color: #52c41a;
|
||||
background: rgba(82, 196, 26, 0.1);
|
||||
}
|
||||
|
||||
&.delivered {
|
||||
color: #1890ff;
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
&.read {
|
||||
color: #722ed1;
|
||||
background: rgba(114, 46, 209, 0.1);
|
||||
}
|
||||
|
||||
&.failed {
|
||||
color: #ff4d4f;
|
||||
background: rgba(255, 77, 79, 0.1);
|
||||
}
|
||||
|
||||
.user-message & {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
|
||||
@@ -139,8 +139,8 @@
|
||||
try {
|
||||
const response = await authService.getCaptcha()
|
||||
console.log('验证码响应:', response)
|
||||
captchaImage.value = `data:image/png;base64,${response.image}`
|
||||
captchaKey.value = response.key
|
||||
captchaImage.value = response.captchaImage // 修正字段
|
||||
captchaKey.value = response.captchaKey // 修正字段
|
||||
console.log('验证码图片URL:', captchaImage.value.substring(0, 50) + '...')
|
||||
} catch (error) {
|
||||
console.error('获取验证码失败:', error)
|
||||
@@ -161,51 +161,25 @@
|
||||
...values,
|
||||
captchaKey: captchaKey.value
|
||||
}
|
||||
|
||||
const result = await userStore.loginWithAuth(loginData)
|
||||
|
||||
console.log('登录结果:', result)
|
||||
console.log('用户store状态:', {
|
||||
token: userStore.token,
|
||||
userInfo: userStore.userInfo,
|
||||
isLoggedIn: userStore.isLoggedIn
|
||||
})
|
||||
|
||||
// 修复:检查 result.code === 200 而不是 result.success
|
||||
if (result.code === 200) {
|
||||
message.success('登录成功')
|
||||
|
||||
// 等待状态更新后再跳转
|
||||
await nextTick()
|
||||
|
||||
// 跳转到首页或之前的页面
|
||||
const redirect = router.currentRoute.value.query.redirect as string
|
||||
const targetPath = redirect || '/'
|
||||
console.log('准备跳转到:', targetPath)
|
||||
|
||||
// 延迟一下确保状态完全更新
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// 使用replace而不是push,避免路由守卫问题
|
||||
router.replace(targetPath).then(() => {
|
||||
console.log('路由跳转完成')
|
||||
}).catch((error) => {
|
||||
console.error('路由跳转失败,使用window.location:', error)
|
||||
// 如果路由跳转失败,使用window.location作为备选
|
||||
window.location.href = targetPath
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('路由跳转异常,使用window.location:', error)
|
||||
const data = await userStore.loginWithAuth(loginData)
|
||||
message.success('登录成功')
|
||||
await nextTick()
|
||||
const redirect = router.currentRoute.value.query.redirect as string
|
||||
const targetPath = redirect || '/'
|
||||
setTimeout(() => {
|
||||
try {
|
||||
router.replace(targetPath).then(() => {
|
||||
console.log('路由跳转完成')
|
||||
}).catch((error) => {
|
||||
window.location.href = targetPath
|
||||
}
|
||||
}, 100)
|
||||
} else {
|
||||
message.error(result.message || '登录失败')
|
||||
refreshCaptcha() // 刷新验证码
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
window.location.href = targetPath
|
||||
}
|
||||
}, 100)
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '登录失败,请稍后重试')
|
||||
refreshCaptcha() // 刷新验证码
|
||||
refreshCaptcha()
|
||||
} finally {
|
||||
loginLoading.value = false
|
||||
}
|
||||
|
||||
@@ -156,8 +156,8 @@
|
||||
try {
|
||||
const response = await authService.getCaptcha()
|
||||
console.log('验证码响应:', response)
|
||||
captchaImage.value = `data:image/png;base64,${response.image}`
|
||||
captchaKey.value = response.key
|
||||
captchaImage.value = response.captchaImage // 修正字段
|
||||
captchaKey.value = response.captchaKey // 修正字段
|
||||
console.log('验证码图片URL:', captchaImage.value.substring(0, 50) + '...')
|
||||
} catch (error) {
|
||||
console.error('获取验证码失败:', error)
|
||||
@@ -178,28 +178,14 @@
|
||||
...values,
|
||||
captchaKey: captchaKey.value
|
||||
}
|
||||
|
||||
const response = await authService.register(registerData)
|
||||
|
||||
if (response.success) {
|
||||
message.success('注册成功,已自动登录')
|
||||
|
||||
// 使用userStore的方法保存用户信息和token
|
||||
userStore.setToken(response.data.accessToken)
|
||||
userStore.setUserInfo(response.data.userInfo)
|
||||
|
||||
console.log('注册成功,用户信息:', response.data.userInfo)
|
||||
console.log('Token已保存:', response.data.accessToken.substring(0, 20) + '...')
|
||||
|
||||
// 跳转到首页
|
||||
router.push('/')
|
||||
} else {
|
||||
message.error(response.message || '注册失败')
|
||||
refreshCaptcha() // 刷新验证码
|
||||
}
|
||||
const data = await authService.register(registerData)
|
||||
message.success('注册成功,已自动登录')
|
||||
userStore.setToken(data.accessToken)
|
||||
userStore.setUserInfo(data.userInfo)
|
||||
router.push('/')
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '注册失败,请稍后重试')
|
||||
refreshCaptcha() // 刷新验证码
|
||||
refreshCaptcha()
|
||||
} finally {
|
||||
registerLoading.value = false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="token-test">
|
||||
<a-card title="Token和身份验证测试">
|
||||
<div class="test-section">
|
||||
<h3>当前状态</h3>
|
||||
<a-descriptions :column="1" bordered>
|
||||
<a-descriptions-item label="登录状态">
|
||||
<a-tag :color="userStore.isLoggedIn ? 'green' : 'red'">
|
||||
{{ userStore.isLoggedIn ? '已登录' : '未登录' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="Token">
|
||||
<a-typography-text :code="true" :copyable="true">
|
||||
{{ userStore.token || '无' }}
|
||||
</a-typography-text>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="用户信息">
|
||||
<pre>{{ JSON.stringify(userStore.userInfo || userStore.user, null, 2) }}</pre>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="WebSocket状态">
|
||||
<a-tag :color="chatStore.wsConnected ? 'green' : 'red'">
|
||||
{{ chatStore.wsConnected ? '已连接' : '未连接' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>操作测试</h3>
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<a-button type="primary" @click="testLogin" :loading="loginLoading">
|
||||
测试登录
|
||||
</a-button>
|
||||
<a-button @click="testWebSocketConnect" :loading="wsLoading">
|
||||
测试WebSocket连接
|
||||
</a-button>
|
||||
<a-button @click="testSendMessage" :disabled="!chatStore.wsConnected">
|
||||
发送测试消息
|
||||
</a-button>
|
||||
<a-button @click="checkLocalStorage">
|
||||
检查本地存储
|
||||
</a-button>
|
||||
<a-button @click="testApiCall" :loading="apiLoading">
|
||||
测试API调用
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>测试结果</h3>
|
||||
<a-textarea
|
||||
v-model:value="testResults"
|
||||
:rows="10"
|
||||
readonly
|
||||
placeholder="测试结果将显示在这里..."
|
||||
/>
|
||||
<a-button @click="clearResults" style="margin-top: 8px">
|
||||
清空结果
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useUserStore, useChatStore } from '@/stores'
|
||||
import { request } from '@/services/api'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const chatStore = useChatStore()
|
||||
|
||||
const loginLoading = ref(false)
|
||||
const wsLoading = ref(false)
|
||||
const apiLoading = ref(false)
|
||||
const testResults = ref('')
|
||||
|
||||
const addResult = (message: string) => {
|
||||
const timestamp = new Date().toLocaleTimeString()
|
||||
testResults.value += `[${timestamp}] ${message}\n`
|
||||
}
|
||||
|
||||
const testLogin = async () => {
|
||||
loginLoading.value = true
|
||||
try {
|
||||
addResult('开始测试登录...')
|
||||
|
||||
const result = await userStore.loginWithAuth({
|
||||
account: 'test@example.com',
|
||||
password: '123456',
|
||||
captcha: '1234'
|
||||
})
|
||||
|
||||
addResult(`登录成功: ${JSON.stringify(result)}`)
|
||||
addResult(`Token: ${userStore.token}`)
|
||||
addResult(`用户信息: ${JSON.stringify(userStore.userInfo)}`)
|
||||
|
||||
} catch (error: any) {
|
||||
addResult(`登录失败: ${error.message}`)
|
||||
} finally {
|
||||
loginLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testWebSocketConnect = async () => {
|
||||
wsLoading.value = true
|
||||
try {
|
||||
addResult('开始测试WebSocket连接...')
|
||||
|
||||
await chatStore.connectWebSocket()
|
||||
|
||||
addResult(`WebSocket连接状态: ${chatStore.wsConnected}`)
|
||||
addResult(`连接状态: ${chatStore.connectionStatus}`)
|
||||
|
||||
} catch (error: any) {
|
||||
addResult(`WebSocket连接失败: ${error.message}`)
|
||||
} finally {
|
||||
wsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testSendMessage = async () => {
|
||||
try {
|
||||
addResult('发送测试消息...')
|
||||
|
||||
await chatStore.sendMessage('这是一条测试消息,用于验证用户身份识别')
|
||||
|
||||
addResult('消息发送成功')
|
||||
|
||||
} catch (error: any) {
|
||||
addResult(`消息发送失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const checkLocalStorage = () => {
|
||||
addResult('检查本地存储...')
|
||||
addResult(`localStorage.token: ${localStorage.getItem('token')}`)
|
||||
addResult(`localStorage.userInfo: ${localStorage.getItem('userInfo')}`)
|
||||
}
|
||||
|
||||
const testApiCall = async () => {
|
||||
apiLoading.value = true
|
||||
try {
|
||||
addResult('测试API调用...')
|
||||
|
||||
const response = await request.get('/health')
|
||||
addResult(`API调用成功: ${JSON.stringify(response)}`)
|
||||
|
||||
} catch (error: any) {
|
||||
addResult(`API调用失败: ${error.message}`)
|
||||
} finally {
|
||||
apiLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const clearResults = () => {
|
||||
testResults.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.token-test {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 16px;
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #f5f5f5;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -28,7 +28,7 @@ export default defineConfig({
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
target: 'http://localhost:19089',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
<!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;
|
||||
}
|
||||
.container {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.connected { background-color: #d4edda; color: #155724; }
|
||||
.disconnected { background-color: #f8d7da; color: #721c24; }
|
||||
.connecting { background-color: #fff3cd; color: #856404; }
|
||||
button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover { background-color: #0056b3; }
|
||||
button:disabled { background-color: #6c757d; cursor: not-allowed; }
|
||||
#messages {
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
input[type="text"] {
|
||||
width: 70%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebSocket连接测试</h1>
|
||||
|
||||
<div class="container">
|
||||
<h3>连接状态</h3>
|
||||
<div id="status" class="status disconnected">未连接</div>
|
||||
<button id="connectBtn" onclick="connect()">连接</button>
|
||||
<button id="disconnectBtn" onclick="disconnect()" disabled>断开</button>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h3>发送消息</h3>
|
||||
<input type="text" id="messageInput" placeholder="输入消息内容..." disabled>
|
||||
<button id="sendBtn" onclick="sendMessage()" disabled>发送</button>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h3>消息日志</h3>
|
||||
<button onclick="clearMessages()">清空日志</button>
|
||||
<div id="messages"></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;
|
||||
const wsUrl = 'http://localhost:19089/ws/chat';
|
||||
const userId = 'test_user_' + Date.now();
|
||||
|
||||
function updateStatus(status, className) {
|
||||
const statusEl = document.getElementById('status');
|
||||
statusEl.textContent = status;
|
||||
statusEl.className = 'status ' + className;
|
||||
}
|
||||
|
||||
function updateButtons() {
|
||||
document.getElementById('connectBtn').disabled = connected;
|
||||
document.getElementById('disconnectBtn').disabled = !connected;
|
||||
document.getElementById('sendBtn').disabled = !connected;
|
||||
document.getElementById('messageInput').disabled = !connected;
|
||||
}
|
||||
|
||||
function addMessage(message, type = 'info') {
|
||||
const messagesEl = document.getElementById('messages');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.innerHTML = `<strong>[${timestamp}]</strong> <span style="color: ${type === 'error' ? 'red' : type === 'success' ? 'green' : 'blue'}">${message}</span>`;
|
||||
messagesEl.appendChild(messageEl);
|
||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||
}
|
||||
|
||||
function connect() {
|
||||
updateStatus('连接中...', 'connecting');
|
||||
addMessage('开始连接WebSocket...', 'info');
|
||||
|
||||
try {
|
||||
const socket = new SockJS(wsUrl);
|
||||
stompClient = Stomp.over(socket);
|
||||
|
||||
// 禁用调试日志
|
||||
stompClient.debug = null;
|
||||
|
||||
stompClient.connect(
|
||||
{ 'X-User-Id': userId },
|
||||
function(frame) {
|
||||
connected = true;
|
||||
updateStatus('已连接', 'connected');
|
||||
updateButtons();
|
||||
addMessage('WebSocket连接成功!', 'success');
|
||||
addMessage('Frame: ' + frame, 'info');
|
||||
|
||||
// 订阅用户消息
|
||||
stompClient.subscribe('/user/queue/messages', function(message) {
|
||||
const msg = JSON.parse(message.body);
|
||||
addMessage('收到消息: ' + JSON.stringify(msg, null, 2), 'success');
|
||||
});
|
||||
|
||||
// 发送连接消息
|
||||
const connectRequest = {
|
||||
userId: userId,
|
||||
username: userId,
|
||||
clientType: 'web',
|
||||
clientVersion: '1.0.0',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
stompClient.send('/app/chat.connect', {}, JSON.stringify(connectRequest));
|
||||
addMessage('发送连接消息: ' + JSON.stringify(connectRequest), 'info');
|
||||
},
|
||||
function(error) {
|
||||
connected = false;
|
||||
updateStatus('连接失败', 'disconnected');
|
||||
updateButtons();
|
||||
addMessage('连接失败: ' + error, 'error');
|
||||
console.error('WebSocket连接错误:', error);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
addMessage('连接异常: ' + error.message, 'error');
|
||||
updateStatus('连接失败', 'disconnected');
|
||||
updateButtons();
|
||||
}
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (stompClient !== null) {
|
||||
stompClient.disconnect();
|
||||
}
|
||||
connected = false;
|
||||
updateStatus('已断开', 'disconnected');
|
||||
updateButtons();
|
||||
addMessage('WebSocket连接已断开', 'info');
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const content = messageInput.value.trim();
|
||||
|
||||
if (!content) {
|
||||
addMessage('消息内容不能为空', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (stompClient && connected) {
|
||||
const chatRequest = {
|
||||
content: content,
|
||||
senderId: userId,
|
||||
senderType: 'USER',
|
||||
messageType: 'TEXT',
|
||||
conversationId: 'test_conversation_' + Date.now(),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
stompClient.send('/app/chat.send', {}, JSON.stringify(chatRequest));
|
||||
addMessage('发送消息: ' + content, 'info');
|
||||
messageInput.value = '';
|
||||
} else {
|
||||
addMessage('WebSocket未连接,无法发送消息', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function clearMessages() {
|
||||
document.getElementById('messages').innerHTML = '';
|
||||
}
|
||||
|
||||
// 回车发送消息
|
||||
document.getElementById('messageInput').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载完成后的初始化
|
||||
window.onload = function() {
|
||||
updateButtons();
|
||||
addMessage('WebSocket测试页面已加载', 'info');
|
||||
addMessage('WebSocket URL: ' + wsUrl, 'info');
|
||||
addMessage('用户ID: ' + userId, 'info');
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,171 @@
|
||||
# 端口配置核查总结
|
||||
|
||||
## 概述
|
||||
|
||||
已完成对所有环境的端口配置核查和统一修改,确保前后端配置一致性。
|
||||
|
||||
## 后端配置 (backend-single)
|
||||
|
||||
### 端口统一为: 19089
|
||||
|
||||
#### 1. 本地开发环境 (`application-local.yml`)
|
||||
```yaml
|
||||
server:
|
||||
port: 19089
|
||||
```
|
||||
- ✅ 已确认配置正确
|
||||
- 数据库: 47.111.10.27:3306/emotion
|
||||
- Redis: localhost:6379
|
||||
|
||||
#### 2. 生产环境 (`application-prod.yml`)
|
||||
```yaml
|
||||
server:
|
||||
port: 19089
|
||||
```
|
||||
- ✅ 已确认配置正确
|
||||
- 数据库: 47.111.10.27:3306/emotion
|
||||
- Redis: localhost:6379
|
||||
|
||||
#### 3. 测试环境 (`application-test.yml`)
|
||||
```yaml
|
||||
server:
|
||||
port: 19089
|
||||
```
|
||||
- ✅ 新创建配置文件
|
||||
- 数据库: 47.111.10.27:3306/emotion
|
||||
- Redis: localhost:6379 (database: 1)
|
||||
|
||||
## 前端配置 (web-flowith)
|
||||
|
||||
### API和WebSocket端点统一
|
||||
|
||||
#### 1. 开发环境 (`.env.development`)
|
||||
```env
|
||||
VITE_API_BASE_URL=http://localhost:19089/api
|
||||
VITE_UPLOAD_URL=http://localhost:19089/api/upload
|
||||
VITE_WS_URL=http://localhost:19089/ws/chat
|
||||
```
|
||||
- ✅ 已确认配置正确
|
||||
|
||||
#### 2. 生产环境 (`.env.production`)
|
||||
```env
|
||||
VITE_API_BASE_URL=http://47.111.10.27:19089/api
|
||||
VITE_UPLOAD_URL=http://47.111.10.27:19089/api/upload
|
||||
VITE_WS_URL=http://47.111.10.27:19089/ws/chat
|
||||
```
|
||||
- ✅ 已更新配置 (从19000改为19089)
|
||||
|
||||
#### 3. 测试环境 (`.env.test`)
|
||||
```env
|
||||
VITE_API_BASE_URL=http://47.111.10.27:19089/api
|
||||
VITE_UPLOAD_URL=http://47.111.10.27:19089/api/upload
|
||||
VITE_WS_URL=http://47.111.10.27:19089/ws/chat
|
||||
```
|
||||
- ✅ 新创建配置文件
|
||||
|
||||
## WebSocket服务默认配置
|
||||
|
||||
### 前端WebSocket服务 (`src/services/websocket.ts`)
|
||||
```typescript
|
||||
const wsUrl = import.meta.env.VITE_WS_URL || 'http://localhost:19089/ws/chat'
|
||||
```
|
||||
- ✅ 已更新默认URL (从8080改为19089)
|
||||
|
||||
## 配置验证清单
|
||||
|
||||
### ✅ 已完成项目
|
||||
1. **后端端口统一**: 所有环境都使用19089端口
|
||||
2. **前端API配置**: 所有环境都指向正确的后端端口
|
||||
3. **WebSocket配置**: 前端WebSocket URL与后端端点匹配
|
||||
4. **环境配置完整性**: 本地、测试、生产环境配置齐全
|
||||
5. **默认配置更新**: WebSocket服务默认URL已更新
|
||||
|
||||
### 🔧 配置详情
|
||||
|
||||
#### WebSocket端点路径
|
||||
- 后端端点: `/ws/chat`
|
||||
- 前端连接: `http://host:19089/ws/chat`
|
||||
- 支持SockJS和原生WebSocket
|
||||
|
||||
#### API路径前缀
|
||||
- 后端context-path: `/api`
|
||||
- 前端API调用: `http://host:19089/api/*`
|
||||
|
||||
#### 数据库配置
|
||||
- 主机: 47.111.10.27:3306
|
||||
- 数据库: emotion
|
||||
- 本地/测试: root用户
|
||||
- 生产: emotion用户
|
||||
|
||||
## 部署说明
|
||||
|
||||
### 本地开发
|
||||
```bash
|
||||
# 后端
|
||||
cd backend-single
|
||||
mvn spring-boot:run -Dspring-boot.run.profiles=local
|
||||
|
||||
# 前端
|
||||
cd web-flowith
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 测试环境
|
||||
```bash
|
||||
# 后端
|
||||
java -jar emotion-single.jar --spring.profiles.active=test
|
||||
|
||||
# 前端
|
||||
npm run build:test
|
||||
```
|
||||
|
||||
### 生产环境
|
||||
```bash
|
||||
# 后端
|
||||
java -jar emotion-single.jar --spring.profiles.active=prod
|
||||
|
||||
# 前端
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 验证步骤
|
||||
|
||||
1. **后端启动验证**
|
||||
- 检查日志确认端口19089启动成功
|
||||
- 访问 `http://localhost:19089/api/health` 确认API可用
|
||||
|
||||
2. **WebSocket连接验证**
|
||||
- 前端聊天页面能正常连接WebSocket
|
||||
- 浏览器开发者工具Network标签页显示WebSocket连接成功
|
||||
|
||||
3. **跨环境验证**
|
||||
- 本地开发环境正常工作
|
||||
- 测试环境部署后功能正常
|
||||
- 生产环境部署后功能正常
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **防火墙配置**: 确保服务器19089端口对外开放
|
||||
2. **Nginx配置**: 如使用Nginx代理,需要配置WebSocket支持
|
||||
3. **SSL证书**: 生产环境建议使用HTTPS和WSS
|
||||
4. **监控配置**: 建议添加端口和服务监控
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 常见问题
|
||||
1. **端口被占用**: 使用 `lsof -i :19089` 检查端口占用
|
||||
2. **WebSocket连接失败**: 检查防火墙和代理配置
|
||||
3. **跨域问题**: 确认后端CORS配置正确
|
||||
4. **环境变量**: 确认前端构建时使用正确的环境配置
|
||||
|
||||
### 调试命令
|
||||
```bash
|
||||
# 检查端口占用
|
||||
lsof -i :19089
|
||||
|
||||
# 检查服务状态
|
||||
curl http://localhost:19089/api/health
|
||||
|
||||
# 检查WebSocket连接
|
||||
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Key: test" -H "Sec-WebSocket-Version: 13" http://localhost:19089/ws/chat
|
||||
```
|
||||
Reference in New Issue
Block a user