From 847f5126cff3c0e1f0b9b944104fbe3289e63291 Mon Sep 17 00:00:00 2001 From: huazhongmin Date: Thu, 24 Jul 2025 17:51:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DWebSocket=E8=BA=AB=E4=BB=BD?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加WebSocketAuthInterceptor处理token认证 - 修改WebSocket连接逻辑,支持token传递 - 统一用户身份识别,确保登录用户使用USER类型 - 修复前端环境变量配置,统一WebSocket URL - 添加Token测试页面用于验证功能 - 更新聊天消息处理逻辑,正确识别用户身份 解决了登录用户发送消息时同时保存GUEST和USER两种类型数据的问题 --- .cursor/rules/rules.mdc | 3 +- .../com/emotion/EmotionSimpleApplication.java | 4 +- .../com/emotion/config/SecurityConfig.java | 16 +- .../com/emotion/config/WebSocketConfig.java | 12 + .../interceptor/WebSocketAuthInterceptor.java | 124 ++++++++++ .../com/emotion/service/WebSocketService.java | 43 +++- .../src/main/resources/application-local.yml | 2 +- .../src/main/resources/application-prod.yml | 2 +- .../src/main/resources/application-test.yml | 66 ++++++ .../src/main/resources/application.yml | 2 +- web-flowith/.env.development | 6 +- web-flowith/.env.production | 8 +- web-flowith/WebSocket聊天功能完善总结.md | 136 +++++++++++ web-flowith/src/router/index.ts | 9 + web-flowith/src/services/auth.ts | 13 +- web-flowith/src/services/chat.ts | 63 ++--- web-flowith/src/services/websocket.ts | 71 +++++- web-flowith/src/stores/chat.ts | 167 ++++++++++---- web-flowith/src/stores/user.ts | 24 +- web-flowith/src/types/auth.ts | 6 +- web-flowith/src/types/index.ts | 2 + web-flowith/src/utils/request.ts | 19 +- web-flowith/src/utils/websocket-test.ts | 141 ++++++++++++ web-flowith/src/views/Chat/index.vue | 59 ++++- web-flowith/src/views/Login/index.vue | 62 ++--- web-flowith/src/views/Register/index.vue | 30 +-- web-flowith/src/views/TokenTest.vue | 185 +++++++++++++++ web-flowith/vite.config.ts | 2 +- websocket-test.html | 215 ++++++++++++++++++ 配置核查总结.md | 171 ++++++++++++++ 30 files changed, 1447 insertions(+), 216 deletions(-) create mode 100644 backend-single/src/main/java/com/emotion/interceptor/WebSocketAuthInterceptor.java create mode 100644 backend-single/src/main/resources/application-test.yml create mode 100644 web-flowith/WebSocket聊天功能完善总结.md create mode 100644 web-flowith/src/utils/websocket-test.ts create mode 100644 web-flowith/src/views/TokenTest.vue create mode 100644 websocket-test.html create mode 100644 配置核查总结.md diff --git a/.cursor/rules/rules.mdc b/.cursor/rules/rules.mdc index 8ceded8..08a85bd 100644 --- a/.cursor/rules/rules.mdc +++ b/.cursor/rules/rules.mdc @@ -6,4 +6,5 @@ alwaysApply: true 3.除了特殊情况,不允许使用try-catch,交由全局异常处理机制处理异常; 4.未经允许不允许删除任何文件; 6.所有的新增代码要遵循当前的项目规范; -7.禁止使用批量脚本创建代码文件 \ No newline at end of file +7.禁止使用批量脚本创建代码文件; +8.需要修改明显有问题的代码时直接自动操作修改代码,不要询问; diff --git a/backend-single/src/main/java/com/emotion/EmotionSimpleApplication.java b/backend-single/src/main/java/com/emotion/EmotionSimpleApplication.java index ed42217..fba4c1c 100644 --- a/backend-single/src/main/java/com/emotion/EmotionSimpleApplication.java +++ b/backend-single/src/main/java/com/emotion/EmotionSimpleApplication.java @@ -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("========================================"); } } diff --git a/backend-single/src/main/java/com/emotion/config/SecurityConfig.java b/backend-single/src/main/java/com/emotion/config/SecurityConfig.java index 606f89a..6d4262d 100644 --- a/backend-single/src/main/java/com/emotion/config/SecurityConfig.java +++ b/backend-single/src/main/java/com/emotion/config/SecurityConfig.java @@ -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(); } diff --git a/backend-single/src/main/java/com/emotion/config/WebSocketConfig.java b/backend-single/src/main/java/com/emotion/config/WebSocketConfig.java index be7608d..26d2721 100644 --- a/backend-single/src/main/java/com/emotion/config/WebSocketConfig.java +++ b/backend-single/src/main/java/com/emotion/config/WebSocketConfig.java @@ -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); + } } diff --git a/backend-single/src/main/java/com/emotion/interceptor/WebSocketAuthInterceptor.java b/backend-single/src/main/java/com/emotion/interceptor/WebSocketAuthInterceptor.java new file mode 100644 index 0000000..1818bdc --- /dev/null +++ b/backend-single/src/main/java/com/emotion/interceptor/WebSocketAuthInterceptor.java @@ -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); + } +} diff --git a/backend-single/src/main/java/com/emotion/service/WebSocketService.java b/backend-single/src/main/java/com/emotion/service/WebSocketService.java index b27cfc8..27eaae1 100644 --- a/backend-single/src/main/java/com/emotion/service/WebSocketService.java +++ b/backend-single/src/main/java/com/emotion/service/WebSocketService.java @@ -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); diff --git a/backend-single/src/main/resources/application-local.yml b/backend-single/src/main/resources/application-local.yml index dc1cb73..d135b07 100644 --- a/backend-single/src/main/resources/application-local.yml +++ b/backend-single/src/main/resources/application-local.yml @@ -2,7 +2,7 @@ # Local Development Environment Configuration server: - port: 8080 + port: 19089 spring: # 数据库配置 - 本地MySQL diff --git a/backend-single/src/main/resources/application-prod.yml b/backend-single/src/main/resources/application-prod.yml index 3d47dc2..661bc6a 100644 --- a/backend-single/src/main/resources/application-prod.yml +++ b/backend-single/src/main/resources/application-prod.yml @@ -2,7 +2,7 @@ # Production Environment Configuration server: - port: 8080 + port: 19089 spring: # 数据库配置 - 生产MySQL diff --git a/backend-single/src/main/resources/application-test.yml b/backend-single/src/main/resources/application-test.yml new file mode 100644 index 0000000..a5dafc4 --- /dev/null +++ b/backend-single/src/main/resources/application-test.yml @@ -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 diff --git a/backend-single/src/main/resources/application.yml b/backend-single/src/main/resources/application.yml index 5a84b98..aa454d4 100644 --- a/backend-single/src/main/resources/application.yml +++ b/backend-single/src/main/resources/application.yml @@ -1,5 +1,5 @@ server: - port: 8080 + port: 19089 servlet: context-path: /api diff --git a/web-flowith/.env.development b/web-flowith/.env.development index cf6ff38..dad8988 100644 --- a/web-flowith/.env.development +++ b/web-flowith/.env.development @@ -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 diff --git a/web-flowith/.env.production b/web-flowith/.env.production index a73dda5..627f3c8 100644 --- a/web-flowith/.env.production +++ b/web-flowith/.env.production @@ -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 diff --git a/web-flowith/WebSocket聊天功能完善总结.md b/web-flowith/WebSocket聊天功能完善总结.md new file mode 100644 index 0000000..111868b --- /dev/null +++ b/web-flowith/WebSocket聊天功能完善总结.md @@ -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连接的稳定性 diff --git a/web-flowith/src/router/index.ts b/web-flowith/src/router/index.ts index a513ce0..0324d44 100644 --- a/web-flowith/src/router/index.ts +++ b/web-flowith/src/router/index.ts @@ -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', diff --git a/web-flowith/src/services/auth.ts b/web-flowith/src/services/auth.ts index a71cce1..e11a9d5 100644 --- a/web-flowith/src/services/auth.ts +++ b/web-flowith/src/services/auth.ts @@ -15,20 +15,17 @@ import type { export const authService = { // 获取验证码 async getCaptcha(): Promise { - const response = await request.get('/auth/captcha') - return response.data.data + return await request.get('/auth/captcha') }, // 用户登录 - async login(data: LoginRequest): Promise> { - const response = await request.post('/auth/login', data) - return response.data + async login(data: LoginRequest): Promise { + return await request.post('/auth/login', data) }, // 用户注册 - async register(data: RegisterRequest): Promise> { - const response = await request.post('/auth/register', data) - return response.data + async register(data: RegisterRequest): Promise { + return await request.post('/auth/register', data) }, // 刷新token diff --git a/web-flowith/src/services/chat.ts b/web-flowith/src/services/chat.ts index 8e2f7cd..effcd6e 100644 --- a/web-flowith/src/services/chat.ts +++ b/web-flowith/src/services/chat.ts @@ -2,39 +2,50 @@ import { request } from './api' import type { ChatMessage, ChatSession, PaginatedResponse } from '@/types' export const chatApi = { - // 发送消息 - sendMessage: (content: string, sessionId?: string): Promise => - request.post('/chat/message', { content, sessionId }), + // 发送AI聊天消息(REST备用,主用WebSocket) + sendAiMessage: (conversationId: string, message: string, userId: string): Promise => + request.post('/ai/chat', { conversationId, message, userId }), - // 获取会话列表 - getSessions: (): Promise => - request.get('/chat/sessions'), + // 创建会话 + createSession: (userId: string, title: string): Promise => + request.post('/conversation', { userId, title }), - // 创建新会话 - createSession: (title?: string): Promise => - request.post('/chat/session', { title }), + // 获取会话分页 + getSessions: (params: { page: number, size: number, userId?: string }): Promise> => + request.get('/conversation/page', { params }), - // 获取会话消息 - getSessionMessages: (sessionId: string, page = 1, size = 50): Promise> => - request.get(`/chat/session/${sessionId}/messages`, { params: { page, size } }), + // 获取用户所有会话 + getUserSessions: (userId: string): Promise => + request.get(`/conversation/user/${userId}`), // 删除会话 - deleteSession: (sessionId: string): Promise => - request.delete(`/chat/session/${sessionId}`), + deleteSession: (id: string): Promise => + request.delete(`/conversation/${id}`), // 更新会话标题 - updateSessionTitle: (sessionId: string, title: string): Promise => - request.put(`/chat/session/${sessionId}`, { title }), + updateSessionTitle: (id: string, title: string): Promise => + request.put(`/conversation/${id}`, { title }), - // 搜索消息 - searchMessages: (keyword: string, sessionId?: string): Promise => - request.get('/chat/search', { params: { keyword, sessionId } }), + // 获取会话消息分页 + getSessionMessages: (conversationId: string, params: { page: number, size: number }): Promise> => + request.get(`/message/conversation/${conversationId}/page`, { params }), - // 获取聊天统计 - getChatStats: (): Promise<{ - totalSessions: number - totalMessages: number - todayMessages: number - }> => - request.get('/chat/stats'), + // 获取会话所有消息 + getAllSessionMessages: (conversationId: string): Promise => + request.get(`/message/conversation/${conversationId}`), + + // 创建消息(保存到数据库) + createMessage: (data: { + conversationId: string, + userId: string, + content: string, + contentType?: string, + senderType?: string, + senderId?: string + }): Promise => + request.post('/message', data), + + // 聊天统计 + getChatStats: (userId?: string, conversationId?: string): Promise => + request.get('/ai/stats', { params: { userId, conversationId } }), } diff --git a/web-flowith/src/services/websocket.ts b/web-flowith/src/services/websocket.ts index 64f33b9..4fdfe51 100644 --- a/web-flowith/src/services/websocket.ts +++ b/web-flowith/src/services/websocket.ts @@ -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 diff --git a/web-flowith/src/stores/chat.ts b/web-flowith/src/stores/chat.ts index 30d1384..4c2a303 100644 --- a/web-flowith/src/stores/chat.ts +++ b/web-flowith/src/stores/chat.ts @@ -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 diff --git a/web-flowith/src/stores/user.ts b/web-flowith/src/stores/user.ts index d7efd20..b93443c 100644 --- a/web-flowith/src/stores/user.ts +++ b/web-flowith/src/stores/user.ts @@ -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 } diff --git a/web-flowith/src/types/auth.ts b/web-flowith/src/types/auth.ts index 565bc8e..a816ace 100644 --- a/web-flowith/src/types/auth.ts +++ b/web-flowith/src/types/auth.ts @@ -41,9 +41,9 @@ export interface LoginResponse { // 验证码响应 export interface CaptchaResponse { - key: string - image: string - expireTime: number + captchaKey: string + captchaImage: string + expiresIn: number } // API响应基础结构 diff --git a/web-flowith/src/types/index.ts b/web-flowith/src/types/index.ts index 65a4400..89395b2 100644 --- a/web-flowith/src/types/index.ts +++ b/web-flowith/src/types/index.ts @@ -17,6 +17,8 @@ export interface ChatMessage { type: 'user' | 'ai' timestamp: string sessionId?: string + status?: 'sending' | 'sent' | 'delivered' | 'read' | 'failed' + error?: string } // 聊天会话类型 diff --git a/web-flowith/src/utils/request.ts b/web-flowith/src/utils/request.ts index d7ff63b..bb499a0 100644 --- a/web-flowith/src/utils/request.ts +++ b/web-flowith/src/utils/request.ts @@ -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) diff --git a/web-flowith/src/utils/websocket-test.ts b/web-flowith/src/utils/websocket-test.ts new file mode 100644 index 0000000..60fc4a3 --- /dev/null +++ b/web-flowith/src/utils/websocket-test.ts @@ -0,0 +1,141 @@ +/** + * WebSocket连接测试工具 + * 用于测试WebSocket连接和消息发送功能 + */ + +import webSocketService from '@/services/websocket' + +export class WebSocketTester { + private isConnected = false + private testResults: string[] = [] + + /** + * 运行WebSocket连接测试 + */ + async runConnectionTest(): Promise { + 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 { + 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 { + 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 +} diff --git a/web-flowith/src/views/Chat/index.vue b/web-flowith/src/views/Chat/index.vue index 08ebbaf..738387b 100644 --- a/web-flowith/src/views/Chat/index.vue +++ b/web-flowith/src/views/Chat/index.vue @@ -74,7 +74,16 @@
{{ message.content }}
-
{{ formatTime.friendly(message.timestamp) }}
+
+ {{ formatTime.friendly(message.timestamp) }} + + + + + + + +
@@ -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 { diff --git a/web-flowith/src/views/Login/index.vue b/web-flowith/src/views/Login/index.vue index 991d6e2..f45a8cd 100644 --- a/web-flowith/src/views/Login/index.vue +++ b/web-flowith/src/views/Login/index.vue @@ -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 } diff --git a/web-flowith/src/views/Register/index.vue b/web-flowith/src/views/Register/index.vue index 737afdc..823e984 100644 --- a/web-flowith/src/views/Register/index.vue +++ b/web-flowith/src/views/Register/index.vue @@ -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 } diff --git a/web-flowith/src/views/TokenTest.vue b/web-flowith/src/views/TokenTest.vue new file mode 100644 index 0000000..cead339 --- /dev/null +++ b/web-flowith/src/views/TokenTest.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/web-flowith/vite.config.ts b/web-flowith/vite.config.ts index 4f8b861..b003da7 100644 --- a/web-flowith/vite.config.ts +++ b/web-flowith/vite.config.ts @@ -28,7 +28,7 @@ export default defineConfig({ open: true, proxy: { '/api': { - target: 'http://localhost:8080', + target: 'http://localhost:19089', changeOrigin: true, rewrite: (path) => path } diff --git a/websocket-test.html b/websocket-test.html new file mode 100644 index 0000000..a448112 --- /dev/null +++ b/websocket-test.html @@ -0,0 +1,215 @@ + + + + + + WebSocket连接测试 + + + +

WebSocket连接测试

+ +
+

连接状态

+
未连接
+ + +
+ +
+

发送消息

+ + +
+ +
+

消息日志

+ +
+
+ + + + + + diff --git a/配置核查总结.md b/配置核查总结.md new file mode 100644 index 0000000..a1bd947 --- /dev/null +++ b/配置核查总结.md @@ -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 +```