From 17a538891d380b660f305e3d1a515e87abf0373c Mon Sep 17 00:00:00 2001 From: Hai Li Cui Date: Tue, 21 Oct 2025 13:56:02 +0800 Subject: [PATCH] submit homework day3 --- README.md | 3 + application.log | 709 ++++++++++++++++++ backend-services/api-gateway/Dockerfile | 24 + backend-services/api-gateway/pom.xml | 9 +- .../api/controller/TestConfigController.java | 10 +- .../api/web/filter/AuthenticationFilter.java | 2 - .../config/InMemoryRateLimiterConfig.java | 10 +- .../src/main/resources/application.yml | 59 +- .../target/classes/application.yml | 52 ++ .../ai/qa/gateway/ApiGatewayApplication.class | Bin 0 -> 889 bytes .../api/controller/TestConfigController.class | Bin 0 -> 899 bytes .../api/web/filter/AuthenticationFilter.class | Bin 0 -> 4751 bytes .../config/InMemoryRateLimiterConfig$1.class | Bin 0 -> 5544 bytes ...yRateLimiterConfig$RateLimiterConfig.class | Bin 0 -> 1180 bytes .../config/InMemoryRateLimiterConfig.class | Bin 0 -> 2930 bytes backend-services/pom.xml | 15 +- backend-services/qa-service/Dockerfile | 24 + backend-services/qa-service/pom.xml | 63 +- .../service/api/controller/QAController.java | 172 ++++- .../ai/qa/service/api/dto/ApiResponse.java | 71 ++ .../ai/qa/service/api/dto/AskQaRequest.java | 14 + .../ai/qa/service/api/dto/QAHistoryDTO.java | 60 +- .../service/api/dto/SaveHistoryRequest.java | 36 + .../ai/qa/service/api/exception/ErrCode.java | 107 ++- .../api/exception/GlobalExceptionHandler.java | 53 ++ .../application/dto/QAHistoryQuery.java | 34 +- .../application/dto/SaveHistoryCommand.java | 34 +- .../application/service/QAHistoryService.java | 273 ++++++- .../domain/exception/QADomainException.java | 36 + .../ai/qa/service/domain/model/QAHistory.java | 124 ++- .../qa/service/domain/repo/QAHistoryRepo.java | 69 +- .../qa/service/domain/service/QAService.java | 182 ++++- .../infrastructure/feign/DeepSeekClient.java | 164 ++++ .../feign/DeepSeekRestTemplateConfig.java | 63 ++ .../infrastructure/feign/FeignConfig.java | 24 + .../infrastructure/feign/UserClient.java | 51 +- .../feign/UserClientFallback.java | 50 ++ .../persistence/entities/QAHistoryPO.java | 85 ++- .../persistence/mapper/QAHistoryMapper.java | 53 ++ .../repositories/JpaQAHistoryRepository.java | 107 ++- .../repositories/QAHistoryRepoImpl.java | 133 +++- .../src/main/resources/application.yml | 87 ++- .../service/domain/model/QAHistoryTest.java | 9 + .../qa-service/target/classes/application.yml | 88 +++ .../ai/qa/service/QAServiceApplication.class | Bin 0 -> 886 bytes .../service/api/controller/QAController.class | Bin 0 -> 7775 bytes .../ai/qa/service/api/dto/ApiResponse.class | Bin 0 -> 4491 bytes .../ai/qa/service/api/dto/AskQaRequest.class | Bin 0 -> 3003 bytes .../ai/qa/service/api/dto/QAHistoryDTO.class | Bin 0 -> 6517 bytes .../service/api/dto/SaveHistoryRequest.class | Bin 0 -> 3825 bytes .../ai/qa/service/api/exception/ErrCode.class | Bin 0 -> 2690 bytes .../exception/GlobalExceptionHandler.class | Bin 0 -> 3323 bytes .../application/dto/QAHistoryQuery.class | Bin 0 -> 4132 bytes .../application/dto/SaveHistoryCommand.class | Bin 0 -> 3841 bytes .../service/QAHistoryService.class | Bin 0 -> 8422 bytes .../domain/exception/QADomainException.class | Bin 0 -> 765 bytes .../qa/service/domain/model/QAHistory.class | Bin 0 -> 4250 bytes .../service/domain/repo/QAHistoryRepo.class | Bin 0 -> 1244 bytes .../qa/service/domain/service/QAService.class | Bin 0 -> 5507 bytes .../infrastructure/feign/DeepSeekClient.class | Bin 0 -> 7534 bytes .../feign/DeepSeekRestTemplateConfig.class | Bin 0 -> 2646 bytes .../infrastructure/feign/FeignConfig.class | Bin 0 -> 718 bytes .../infrastructure/feign/UserClient.class | Bin 0 -> 1023 bytes .../feign/UserClientFallback.class | Bin 0 -> 2407 bytes .../persistence/entities/QAHistoryPO.class | Bin 0 -> 6343 bytes .../persistence/mapper/QAHistoryMapper.class | Bin 0 -> 1334 bytes .../mapper/QAHistoryMapperImpl.class | Bin 0 -> 2156 bytes .../repositories/JpaQAHistoryRepository.class | Bin 0 -> 2856 bytes .../repositories/QAHistoryRepoImpl.class | Bin 0 -> 6377 bytes .../mapper/QAHistoryMapperImpl.java | 55 ++ .../service/domain/model/QAHistoryTest.class | Bin 0 -> 326 bytes backend-services/user-service/Dockerfile | 24 + backend-services/user-service/pom.xml | 44 +- .../ai/qa/user/UserServiceApplication.java | 4 - .../user/api/controller/UserController.java | 239 ++++-- .../java/com/ai/qa/user/api/dto/Response.java | 4 + .../qa/user/api/dto/request/LoginRequest.java | 31 + .../user/api/dto/request/RegisterRequest.java | 61 ++ .../dto/request/UpdatePasswordRequest.java | 50 ++ .../user/api/dto/response/BaseResponse.java | 113 +++ .../user/api/dto/response/LoginResponse.java | 68 ++ .../api/dto/response/RegisterResponse.java | 61 ++ .../dto/response/UpdatePasswordResponse.java | 39 + .../user/api/dto/response/UserResponse.java | 51 ++ .../user/api/exception/BusinessException.java | 155 +++- .../com/ai/qa/user/api/exception/ErrCode.java | 48 ++ .../api/exception/GlobalExceptionHandler.java | 161 +++- .../user/application/service/UserService.java | 82 ++ .../service/impl/UserDetailsServiceImpl.java | 46 ++ .../service/impl/UserServiceImpl.java | 314 ++++++++ .../java/com/ai/qa/user/common/JwtUtil.java | 203 +++++ .../com/ai/qa/user/common/KeyGenerator.java | 40 + .../com/ai/qa/user/domain/entity/User.java | 83 ++ .../domain/repository/UserRepository.java | 93 +++ .../config/JwtAuthenticationFilter.java | 102 +++ .../infrastructure/config/SecurityConfig.java | 76 +- .../infrastructure/config/SwaggerConfig.java | 42 +- .../infrastructure/persistent/UserMapper.java | 4 + .../src/main/resources/application.yml | 72 +- .../target/classes/application.yml | 81 ++ .../ai/qa/user/UserServiceApplication.class | Bin 0 -> 750 bytes .../user/api/controller/UserController.class | Bin 0 -> 7047 bytes .../com/ai/qa/user/api/dto/Response.class | Bin 0 -> 295 bytes .../user/api/dto/request/LoginRequest.class | Bin 0 -> 2408 bytes .../api/dto/request/RegisterRequest.class | Bin 0 -> 3858 bytes .../dto/request/UpdatePasswordRequest.class | Bin 0 -> 3490 bytes .../user/api/dto/response/BaseResponse.class | Bin 0 -> 4327 bytes .../user/api/dto/response/LoginResponse.class | Bin 0 -> 4547 bytes .../api/dto/response/RegisterResponse.class | Bin 0 -> 4119 bytes .../dto/response/UpdatePasswordResponse.class | Bin 0 -> 3204 bytes .../user/api/dto/response/UserResponse.class | Bin 0 -> 3648 bytes .../api/exception/BusinessException.class | Bin 0 -> 3348 bytes .../ai/qa/user/api/exception/ErrCode.class | Bin 0 -> 1533 bytes .../exception/GlobalExceptionHandler.class | Bin 0 -> 7462 bytes .../application/service/UserService.class | Bin 0 -> 923 bytes .../service/impl/UserDetailsServiceImpl.class | Bin 0 -> 3202 bytes .../service/impl/UserServiceImpl.class | Bin 0 -> 7737 bytes .../com/ai/qa/user/common/CommonUtil.class | Bin 0 -> 299 bytes .../com/ai/qa/user/common/DateUtil.class | Bin 0 -> 293 bytes .../com/ai/qa/user/common/JwtUtil.class | Bin 0 -> 4681 bytes .../com/ai/qa/user/common/KeyGenerator.class | Bin 0 -> 1436 bytes .../com/ai/qa/user/domain/entity/User.class | Bin 0 -> 5224 bytes .../domain/repository/UserRepository.class | Bin 0 -> 1897 bytes .../config/JwtAuthenticationFilter.class | Bin 0 -> 4602 bytes .../config/SecurityConfig.class | Bin 0 -> 4959 bytes .../infrastructure/config/SwaggerConfig.class | Bin 0 -> 2677 bytes .../persistent/UserMapper.class | Bin 0 -> 337 bytes .../user-service/target/classes/sql/init.sql | 36 + docker-compose.yml | 91 +++ frontend-nextjs/frontend/.dockerignore | 52 ++ frontend-nextjs/frontend/Dockerfile | 54 ++ .../frontend/app/api/hello/route.ts | 12 +- frontend-nextjs/frontend/app/hello/page.tsx | 2 - frontend-nextjs/frontend/app/page.tsx | 294 ++++++-- .../frontend/components/auth/login-form.tsx | 65 +- .../components/auth/register-form.tsx | 101 ++- .../frontend/components/chat-header.tsx | 10 +- .../frontend/components/chat-window.tsx | 353 +++++++-- frontend-nextjs/frontend/components/icons.tsx | 7 + .../frontend/components/markdown-content.tsx | 233 +++--- .../frontend/components/sidebar.tsx | 292 +++++--- .../frontend/components/ui/alert-dialog.tsx | 57 +- .../frontend/components/ui/calendar.tsx | 39 +- .../frontend/components/ui/chart.tsx | 264 ++++--- .../frontend/components/ui/input.tsx | 38 +- .../frontend/components/ui/radio-group.tsx | 26 +- .../frontend/components/ui/sidebar.tsx | 287 ++++--- .../frontend/components/ui/textarea.tsx | 36 +- .../frontend/components/ui/use-toast.ts | 176 ++--- .../frontend/contexts/auth-context.tsx | 114 +-- frontend-nextjs/frontend/hooks/use-mobile.ts | 26 + frontend-nextjs/frontend/lib/auth-api.ts | 156 +++- frontend-nextjs/frontend/lib/chat-api.ts | 102 +++ frontend-nextjs/frontend/lib/utils.ts | 18 +- frontend-nextjs/frontend/middleware.ts | 19 +- frontend-nextjs/frontend/package.json | 16 +- frontend-nextjs/frontend/pnpm-lock.yaml | 485 +++++++++++- frontend-nextjs/frontend/tsconfig.json | 8 +- frontend-nextjs/frontend/types/auth.ts | 57 +- frontend-nextjs/frontend/types/chat.ts | 38 +- frontend-nextjs/frontend/types/qa.ts | 5 + 161 files changed, 7878 insertions(+), 1296 deletions(-) create mode 100644 README.md create mode 100644 application.log create mode 100644 backend-services/api-gateway/Dockerfile create mode 100644 backend-services/api-gateway/target/classes/application.yml create mode 100644 backend-services/api-gateway/target/classes/com/ai/qa/gateway/ApiGatewayApplication.class create mode 100644 backend-services/api-gateway/target/classes/com/ai/qa/gateway/api/controller/TestConfigController.class create mode 100644 backend-services/api-gateway/target/classes/com/ai/qa/gateway/api/web/filter/AuthenticationFilter.class create mode 100644 backend-services/api-gateway/target/classes/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig$1.class create mode 100644 backend-services/api-gateway/target/classes/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig$RateLimiterConfig.class create mode 100644 backend-services/api-gateway/target/classes/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig.class create mode 100644 backend-services/qa-service/Dockerfile create mode 100644 backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/ApiResponse.java create mode 100644 backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/AskQaRequest.java create mode 100644 backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/SaveHistoryRequest.java create mode 100644 backend-services/qa-service/src/main/java/com/ai/qa/service/api/exception/GlobalExceptionHandler.java create mode 100644 backend-services/qa-service/src/main/java/com/ai/qa/service/domain/exception/QADomainException.java create mode 100644 backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/DeepSeekClient.java create mode 100644 backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/DeepSeekRestTemplateConfig.java create mode 100644 backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/FeignConfig.java create mode 100644 backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/UserClientFallback.java create mode 100644 backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapper.java create mode 100644 backend-services/qa-service/src/test/java/com/ai/qa/service/domain/model/QAHistoryTest.java create mode 100644 backend-services/qa-service/target/classes/application.yml create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/QAServiceApplication.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/api/controller/QAController.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/ApiResponse.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/AskQaRequest.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/QAHistoryDTO.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/SaveHistoryRequest.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/api/exception/ErrCode.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/api/exception/GlobalExceptionHandler.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/application/dto/QAHistoryQuery.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/application/dto/SaveHistoryCommand.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/application/service/QAHistoryService.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/domain/exception/QADomainException.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/domain/model/QAHistory.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/domain/repo/QAHistoryRepo.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/domain/service/QAService.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/DeepSeekClient.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/DeepSeekRestTemplateConfig.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/FeignConfig.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/UserClient.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/UserClientFallback.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/entities/QAHistoryPO.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapper.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapperImpl.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/repositories/JpaQAHistoryRepository.class create mode 100644 backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/repositories/QAHistoryRepoImpl.class create mode 100644 backend-services/qa-service/target/generated-sources/annotations/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapperImpl.java create mode 100644 backend-services/qa-service/target/test-classes/com/ai/qa/service/domain/model/QAHistoryTest.class create mode 100644 backend-services/user-service/Dockerfile create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/Response.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/request/LoginRequest.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/request/RegisterRequest.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/request/UpdatePasswordRequest.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/BaseResponse.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/LoginResponse.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/RegisterResponse.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/UpdatePasswordResponse.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/UserResponse.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/api/exception/ErrCode.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/application/service/UserService.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/application/service/impl/UserDetailsServiceImpl.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/application/service/impl/UserServiceImpl.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/common/JwtUtil.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/common/KeyGenerator.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/domain/entity/User.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/domain/repository/UserRepository.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/config/JwtAuthenticationFilter.java create mode 100644 backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/persistent/UserMapper.java create mode 100644 backend-services/user-service/target/classes/application.yml create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/UserServiceApplication.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/api/controller/UserController.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/api/dto/Response.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/api/dto/request/LoginRequest.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/api/dto/request/RegisterRequest.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/api/dto/request/UpdatePasswordRequest.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/BaseResponse.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/LoginResponse.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/RegisterResponse.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/UpdatePasswordResponse.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/UserResponse.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/api/exception/BusinessException.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/api/exception/ErrCode.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/api/exception/GlobalExceptionHandler.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/application/service/UserService.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/application/service/impl/UserDetailsServiceImpl.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/application/service/impl/UserServiceImpl.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/common/CommonUtil.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/common/DateUtil.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/common/JwtUtil.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/common/KeyGenerator.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/domain/entity/User.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/domain/repository/UserRepository.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/config/JwtAuthenticationFilter.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/config/SecurityConfig.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/config/SwaggerConfig.class create mode 100644 backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/persistent/UserMapper.class create mode 100644 backend-services/user-service/target/classes/sql/init.sql create mode 100644 docker-compose.yml create mode 100644 frontend-nextjs/frontend/.dockerignore create mode 100644 frontend-nextjs/frontend/Dockerfile create mode 100644 frontend-nextjs/frontend/components/icons.tsx create mode 100644 frontend-nextjs/frontend/hooks/use-mobile.ts create mode 100644 frontend-nextjs/frontend/lib/chat-api.ts create mode 100644 frontend-nextjs/frontend/types/qa.ts diff --git a/README.md b/README.md new file mode 100644 index 00000000..6d5d49f9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ + +## 作业提交说明 +本项目为 AI 问答系统作业,请 review 代码实现。 diff --git a/application.log b/application.log new file mode 100644 index 00000000..320f0968 --- /dev/null +++ b/application.log @@ -0,0 +1,709 @@ +2025-10-21 12:14:14.349 INFO 35936 --- [main] com.ai.qa.user.UserServiceApplication : Starting UserServiceApplication using Java 21.0.8 on IBM-PF4E74KP with PID 35936 (C:\qasystem\ai-qa-system-chl\backend-services\user-service\target\classes started by HaiLiCui in C:\qasystem\ai-qa-system-chl) +2025-10-21 12:14:14.352 INFO 35936 --- [main] com.ai.qa.user.UserServiceApplication : No active profile set, falling back to 1 default profile: "default" +2025-10-21 12:14:16.283 INFO 35936 --- [main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. +2025-10-21 12:14:16.349 INFO 35936 --- [main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 52 ms. Found 1 JPA repository interfaces. +2025-10-21 12:14:17.195 INFO 35936 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8081 (http) +2025-10-21 12:14:17.206 INFO 35936 --- [main] o.apache.catalina.core.StandardService : Starting service [Tomcat] +2025-10-21 12:14:17.206 INFO 35936 --- [main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.82] +2025-10-21 12:14:17.340 INFO 35936 --- [main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext +2025-10-21 12:14:17.341 INFO 35936 --- [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2932 ms +2025-10-21 12:14:17.759 INFO 35936 --- [main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] +2025-10-21 12:14:17.872 INFO 35936 --- [main] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.6.15.Final +2025-10-21 12:14:18.075 INFO 35936 --- [main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final} +2025-10-21 12:14:18.207 INFO 35936 --- [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... +2025-10-21 12:14:22.126 ERROR 35936 --- [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Exception during pool initialization. + +com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure + +The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. + at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:175) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:825) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.(ConnectionImpl.java:446) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:239) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:188) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:138) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:364) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:206) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:476) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:561) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.HikariPool.(HikariPool.java:115) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:112) ~[HikariCP-4.0.3.jar:na] + at org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl.getConnection(DatasourceConnectionProviderImpl.java:122) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess.obtainConnection(JdbcEnvironmentInitiator.java:181) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:68) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:35) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:101) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:272) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:246) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:223) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.id.factory.internal.DefaultIdentifierGeneratorFactory.injectServices(DefaultIdentifierGeneratorFactory.java:175) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.service.internal.AbstractServiceRegistryImpl.injectDependencies(AbstractServiceRegistryImpl.java:295) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:252) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:223) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.(InFlightMetadataCollectorImpl.java:173) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:127) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1460) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1494) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:58) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:365) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:341) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:113) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.ConstructorResolver.resolveConstructorArguments(ConstructorResolver.java:688) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:505) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1352) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1195) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveInnerBean(BeanDefinitionValueResolver.java:374) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:134) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1707) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1452) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:710) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:693) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:408) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:710) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:693) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:408) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:213) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAsRegistrationBean(ServletContextInitializerBeans.java:176) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAsRegistrationBean(ServletContextInitializerBeans.java:171) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAdaptableBeans(ServletContextInitializerBeans.java:156) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.(ServletContextInitializerBeans.java:87) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.getServletContextInitializerBeans(ServletWebServerApplicationContext.java:262) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.selfInitialize(ServletWebServerApplicationContext.java:236) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatStarter.onStartup(TomcatStarter.java:53) ~[spring-boot-2.7.17.jar:2.7.17] + at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4904) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1332) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1322) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.FutureTask.run(Unknown Source) ~[na:na] + at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.AbstractExecutorService.submit(Unknown Source) ~[na:na] + at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:866) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:794) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1332) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1322) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.FutureTask.run(Unknown Source) ~[na:na] + at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.AbstractExecutorService.submit(Unknown Source) ~[na:na] + at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:866) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:248) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardService.startInternal(StandardService.java:433) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:921) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.startup.Tomcat.start(Tomcat.java:489) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:123) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.(TomcatWebServer.java:104) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:481) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:211) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:184) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:162) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:577) ~[spring-context-5.3.30.jar:5.3.30] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:732) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:409) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1300) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1289) ~[spring-boot-2.7.17.jar:2.7.17] + at com.ai.qa.user.UserServiceApplication.main(UserServiceApplication.java:9) ~[classes/:na] +Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure + +The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. + at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(Unknown Source) ~[na:na] + at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Unknown Source) ~[na:na] + at java.base/java.lang.reflect.Constructor.newInstance(Unknown Source) ~[na:na] + at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:62) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:150) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:166) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.protocol.a.NativeSocketConnection.connect(NativeSocketConnection.java:89) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.NativeSession.connect(NativeSession.java:121) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:945) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:815) ~[mysql-connector-j-8.0.33.jar:8.0.33] + ... 130 common frames omitted +Caused by: java.net.UnknownHostException: No such host is known (database) + at java.base/java.net.Inet6AddressImpl.lookupAllHostAddr(Native Method) ~[na:na] + at java.base/java.net.Inet6AddressImpl.lookupAllHostAddr(Unknown Source) ~[na:na] + at java.base/java.net.InetAddress$PlatformResolver.lookupByName(Unknown Source) ~[na:na] + at java.base/java.net.InetAddress.getAddressesFromNameService(Unknown Source) ~[na:na] + at java.base/java.net.InetAddress$NameServiceAddresses.get(Unknown Source) ~[na:na] + at java.base/java.net.InetAddress.getAllByName0(Unknown Source) ~[na:na] + at java.base/java.net.InetAddress.getAllByName(Unknown Source) ~[na:na] + at com.mysql.cj.protocol.StandardSocketFactory.connect(StandardSocketFactory.java:130) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.protocol.a.NativeSocketConnection.connect(NativeSocketConnection.java:63) ~[mysql-connector-j-8.0.33.jar:8.0.33] + ... 133 common frames omitted + +2025-10-21 12:14:22.131 WARN 35936 --- [main] o.h.e.j.e.i.JdbcEnvironmentInitiator : HHH000342: Could not obtain connection to query metadata + +com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure + +The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. + at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:175) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:825) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.(ConnectionImpl.java:446) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:239) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:188) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:138) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:364) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:206) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:476) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:561) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.HikariPool.(HikariPool.java:115) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:112) ~[HikariCP-4.0.3.jar:na] + at org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl.getConnection(DatasourceConnectionProviderImpl.java:122) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess.obtainConnection(JdbcEnvironmentInitiator.java:181) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:68) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.initiateService(JdbcEnvironmentInitiator.java:35) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.boot.registry.internal.StandardServiceRegistryImpl.initiateService(StandardServiceRegistryImpl.java:101) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.service.internal.AbstractServiceRegistryImpl.createService(AbstractServiceRegistryImpl.java:272) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:246) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:223) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.id.factory.internal.DefaultIdentifierGeneratorFactory.injectServices(DefaultIdentifierGeneratorFactory.java:175) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.service.internal.AbstractServiceRegistryImpl.injectDependencies(AbstractServiceRegistryImpl.java:295) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:252) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:223) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.(InFlightMetadataCollectorImpl.java:173) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:127) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1460) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1494) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:58) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:365) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:341) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:113) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.ConstructorResolver.resolveConstructorArguments(ConstructorResolver.java:688) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:505) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1352) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1195) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveInnerBean(BeanDefinitionValueResolver.java:374) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:134) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1707) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1452) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:710) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:693) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:408) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:710) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:693) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:408) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:213) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAsRegistrationBean(ServletContextInitializerBeans.java:176) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAsRegistrationBean(ServletContextInitializerBeans.java:171) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAdaptableBeans(ServletContextInitializerBeans.java:156) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.(ServletContextInitializerBeans.java:87) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.getServletContextInitializerBeans(ServletWebServerApplicationContext.java:262) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.selfInitialize(ServletWebServerApplicationContext.java:236) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatStarter.onStartup(TomcatStarter.java:53) ~[spring-boot-2.7.17.jar:2.7.17] + at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4904) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1332) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1322) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.FutureTask.run(Unknown Source) ~[na:na] + at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.AbstractExecutorService.submit(Unknown Source) ~[na:na] + at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:866) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:794) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1332) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1322) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.FutureTask.run(Unknown Source) ~[na:na] + at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.AbstractExecutorService.submit(Unknown Source) ~[na:na] + at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:866) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:248) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardService.startInternal(StandardService.java:433) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:921) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.startup.Tomcat.start(Tomcat.java:489) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:123) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.(TomcatWebServer.java:104) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:481) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:211) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:184) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:162) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:577) ~[spring-context-5.3.30.jar:5.3.30] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:732) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:409) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1300) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1289) ~[spring-boot-2.7.17.jar:2.7.17] + at com.ai.qa.user.UserServiceApplication.main(UserServiceApplication.java:9) ~[classes/:na] +Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure + +The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. + at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(Unknown Source) ~[na:na] + at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Unknown Source) ~[na:na] + at java.base/java.lang.reflect.Constructor.newInstance(Unknown Source) ~[na:na] + at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:62) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:150) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:166) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.protocol.a.NativeSocketConnection.connect(NativeSocketConnection.java:89) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.NativeSession.connect(NativeSession.java:121) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:945) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:815) ~[mysql-connector-j-8.0.33.jar:8.0.33] + ... 130 common frames omitted +Caused by: java.net.UnknownHostException: No such host is known (database) + at java.base/java.net.Inet6AddressImpl.lookupAllHostAddr(Native Method) ~[na:na] + at java.base/java.net.Inet6AddressImpl.lookupAllHostAddr(Unknown Source) ~[na:na] + at java.base/java.net.InetAddress$PlatformResolver.lookupByName(Unknown Source) ~[na:na] + at java.base/java.net.InetAddress.getAddressesFromNameService(Unknown Source) ~[na:na] + at java.base/java.net.InetAddress$NameServiceAddresses.get(Unknown Source) ~[na:na] + at java.base/java.net.InetAddress.getAllByName0(Unknown Source) ~[na:na] + at java.base/java.net.InetAddress.getAllByName(Unknown Source) ~[na:na] + at com.mysql.cj.protocol.StandardSocketFactory.connect(StandardSocketFactory.java:130) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.protocol.a.NativeSocketConnection.connect(NativeSocketConnection.java:63) ~[mysql-connector-j-8.0.33.jar:8.0.33] + ... 133 common frames omitted + +2025-10-21 12:14:22.144 INFO 35936 --- [main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL8Dialect +2025-10-21 12:14:22.614 INFO 35936 --- [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... +2025-10-21 12:14:23.631 ERROR 35936 --- [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Exception during pool initialization. + +com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure + +The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. + at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:175) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:825) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.(ConnectionImpl.java:446) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:239) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:188) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:138) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:364) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:206) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:476) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:561) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.HikariPool.(HikariPool.java:115) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:112) ~[HikariCP-4.0.3.jar:na] + at org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl.getConnection(DatasourceConnectionProviderImpl.java:122) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess.obtainConnection(JdbcEnvironmentInitiator.java:181) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl.getIsolatedConnection(DdlTransactionIsolatorNonJtaImpl.java:44) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.internal.exec.ImprovedExtractionContextImpl.getJdbcConnection(ImprovedExtractionContextImpl.java:63) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.internal.exec.ImprovedExtractionContextImpl.getJdbcDatabaseMetaData(ImprovedExtractionContextImpl.java:70) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.extract.internal.InformationExtractorJdbcDatabaseMetaDataImpl.processTableResultSet(InformationExtractorJdbcDatabaseMetaDataImpl.java:64) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.extract.internal.AbstractInformationExtractorImpl.getTables(AbstractInformationExtractorImpl.java:565) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.extract.internal.DatabaseInformationImpl.getTablesInformation(DatabaseInformationImpl.java:122) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.internal.GroupedSchemaMigratorImpl.performTablesMigration(GroupedSchemaMigratorImpl.java:68) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.performMigration(AbstractSchemaMigrator.java:220) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.doMigration(AbstractSchemaMigrator.java:123) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.performDatabaseAction(SchemaManagementToolCoordinator.java:196) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.process(SchemaManagementToolCoordinator.java:85) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:335) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:471) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1498) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:58) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:365) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:341) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:113) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.ConstructorResolver.resolveConstructorArguments(ConstructorResolver.java:688) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:505) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1352) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1195) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveInnerBean(BeanDefinitionValueResolver.java:374) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:134) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1707) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1452) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:710) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:693) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:408) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:710) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:693) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:408) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:213) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAsRegistrationBean(ServletContextInitializerBeans.java:176) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAsRegistrationBean(ServletContextInitializerBeans.java:171) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAdaptableBeans(ServletContextInitializerBeans.java:156) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.(ServletContextInitializerBeans.java:87) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.getServletContextInitializerBeans(ServletWebServerApplicationContext.java:262) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.selfInitialize(ServletWebServerApplicationContext.java:236) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatStarter.onStartup(TomcatStarter.java:53) ~[spring-boot-2.7.17.jar:2.7.17] + at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4904) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1332) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1322) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.FutureTask.run(Unknown Source) ~[na:na] + at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.AbstractExecutorService.submit(Unknown Source) ~[na:na] + at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:866) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:794) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1332) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1322) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.FutureTask.run(Unknown Source) ~[na:na] + at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.AbstractExecutorService.submit(Unknown Source) ~[na:na] + at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:866) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:248) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardService.startInternal(StandardService.java:433) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:921) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.startup.Tomcat.start(Tomcat.java:489) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:123) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.(TomcatWebServer.java:104) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:481) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:211) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:184) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:162) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:577) ~[spring-context-5.3.30.jar:5.3.30] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:732) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:409) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1300) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1289) ~[spring-boot-2.7.17.jar:2.7.17] + at com.ai.qa.user.UserServiceApplication.main(UserServiceApplication.java:9) ~[classes/:na] +Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure + +The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. + at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(Unknown Source) ~[na:na] + at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Unknown Source) ~[na:na] + at java.base/java.lang.reflect.Constructor.newInstance(Unknown Source) ~[na:na] + at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:62) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:150) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:166) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.protocol.a.NativeSocketConnection.connect(NativeSocketConnection.java:89) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.NativeSession.connect(NativeSession.java:121) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:945) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:815) ~[mysql-connector-j-8.0.33.jar:8.0.33] + ... 130 common frames omitted +Caused by: java.net.UnknownHostException: database + at java.base/java.net.InetAddress$CachedLookup.get(Unknown Source) ~[na:na] + at java.base/java.net.InetAddress.getAllByName0(Unknown Source) ~[na:na] + at java.base/java.net.InetAddress.getAllByName(Unknown Source) ~[na:na] + at com.mysql.cj.protocol.StandardSocketFactory.connect(StandardSocketFactory.java:130) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.protocol.a.NativeSocketConnection.connect(NativeSocketConnection.java:63) ~[mysql-connector-j-8.0.33.jar:8.0.33] + ... 133 common frames omitted + +2025-10-21 12:14:23.635 WARN 35936 --- [main] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: 08S01 +2025-10-21 12:14:23.635 ERROR 35936 --- [main] o.h.engine.jdbc.spi.SqlExceptionHelper : Communications link failure + +The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. +2025-10-21 12:14:23.639 ERROR 35936 --- [main] j.LocalContainerEntityManagerFactoryBean : Failed to initialize JPA EntityManagerFactory: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to open JDBC Connection for DDL execution +2025-10-21 12:14:23.641 ERROR 35936 --- [main] o.s.b.web.embedded.tomcat.TomcatStarter : Error starting Tomcat context. Exception: org.springframework.beans.factory.UnsatisfiedDependencyException. Message: Error creating bean with name 'jwtAuthenticationFilter': Unsatisfied dependency expressed through field 'userDetailsService'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userDetailsServiceImpl': Unsatisfied dependency expressed through field 'userRepository'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRepository' defined in com.ai.qa.user.domain.repository.UserRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Cannot create inner bean '(inner bean)#f2d890c' of type [org.springframework.orm.jpa.SharedEntityManagerCreator] while setting bean property 'entityManager'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#f2d890c': Cannot resolve reference to bean 'entityManagerFactory' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to open JDBC Connection for DDL execution +2025-10-21 12:14:23.667 INFO 35936 --- [main] o.apache.catalina.core.StandardService : Stopping service [Tomcat] +2025-10-21 12:14:23.671 WARN 35936 --- [main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat +2025-10-21 12:14:23.682 INFO 35936 --- [main] ConditionEvaluationReportLoggingListener : + +Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. +2025-10-21 12:14:23.700 ERROR 35936 --- [main] o.s.boot.SpringApplication : Application run failed + +org.springframework.context.ApplicationContextException: Unable to start web server; nested exception is org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:165) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:577) ~[spring-context-5.3.30.jar:5.3.30] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:732) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:409) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1300) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1289) ~[spring-boot-2.7.17.jar:2.7.17] + at com.ai.qa.user.UserServiceApplication.main(UserServiceApplication.java:9) ~[classes/:na] +Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat + at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:142) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.(TomcatWebServer.java:104) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:481) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:211) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:184) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:162) ~[spring-boot-2.7.17.jar:2.7.17] + ... 8 common frames omitted +Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'jwtAuthenticationFilter': Unsatisfied dependency expressed through field 'userDetailsService'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userDetailsServiceImpl': Unsatisfied dependency expressed through field 'userRepository'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRepository' defined in com.ai.qa.user.domain.repository.UserRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Cannot create inner bean '(inner bean)#f2d890c' of type [org.springframework.orm.jpa.SharedEntityManagerCreator] while setting bean property 'entityManager'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#f2d890c': Cannot resolve reference to bean 'entityManagerFactory' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to open JDBC Connection for DDL execution + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:713) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:693) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:408) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:213) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAsRegistrationBean(ServletContextInitializerBeans.java:176) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAsRegistrationBean(ServletContextInitializerBeans.java:171) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAdaptableBeans(ServletContextInitializerBeans.java:156) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.ServletContextInitializerBeans.(ServletContextInitializerBeans.java:87) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.getServletContextInitializerBeans(ServletWebServerApplicationContext.java:262) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.selfInitialize(ServletWebServerApplicationContext.java:236) ~[spring-boot-2.7.17.jar:2.7.17] + at org.springframework.boot.web.embedded.tomcat.TomcatStarter.onStartup(TomcatStarter.java:53) ~[spring-boot-2.7.17.jar:2.7.17] + at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4904) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1332) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1322) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.FutureTask.run(Unknown Source) ~[na:na] + at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.AbstractExecutorService.submit(Unknown Source) ~[na:na] + at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:866) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:794) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1332) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1322) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.FutureTask.run(Unknown Source) ~[na:na] + at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at java.base/java.util.concurrent.AbstractExecutorService.submit(Unknown Source) ~[na:na] + at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:866) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:248) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardService.startInternal(StandardService.java:433) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:921) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:171) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.apache.catalina.startup.Tomcat.start(Tomcat.java:489) ~[tomcat-embed-core-9.0.82.jar:9.0.82] + at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:123) ~[spring-boot-2.7.17.jar:2.7.17] + ... 13 common frames omitted +Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userDetailsServiceImpl': Unsatisfied dependency expressed through field 'userRepository'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRepository' defined in com.ai.qa.user.domain.repository.UserRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Cannot create inner bean '(inner bean)#f2d890c' of type [org.springframework.orm.jpa.SharedEntityManagerCreator] while setting bean property 'entityManager'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#f2d890c': Cannot resolve reference to bean 'entityManagerFactory' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to open JDBC Connection for DDL execution + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:713) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:693) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:119) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:408) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:710) ~[spring-beans-5.3.30.jar:5.3.30] + ... 55 common frames omitted +Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRepository' defined in com.ai.qa.user.domain.repository.UserRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Cannot create inner bean '(inner bean)#f2d890c' of type [org.springframework.orm.jpa.SharedEntityManagerCreator] while setting bean property 'entityManager'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#f2d890c': Cannot resolve reference to bean 'entityManagerFactory' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to open JDBC Connection for DDL execution + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveInnerBean(BeanDefinitionValueResolver.java:389) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:134) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1707) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1452) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:619) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1391) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:710) ~[spring-beans-5.3.30.jar:5.3.30] + ... 69 common frames omitted +Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#f2d890c': Cannot resolve reference to bean 'entityManagerFactory' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to open JDBC Connection for DDL execution + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:342) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:113) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.ConstructorResolver.resolveConstructorArguments(ConstructorResolver.java:688) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:505) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1352) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1195) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveInnerBean(BeanDefinitionValueResolver.java:374) ~[spring-beans-5.3.30.jar:5.3.30] + ... 82 common frames omitted +Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to open JDBC Connection for DDL execution + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1804) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:330) ~[spring-beans-5.3.30.jar:5.3.30] + ... 90 common frames omitted +Caused by: javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.exception.JDBCConnectionException: Unable to open JDBC Connection for DDL execution + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:421) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:396) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:341) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863) ~[spring-beans-5.3.30.jar:5.3.30] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800) ~[spring-beans-5.3.30.jar:5.3.30] + ... 97 common frames omitted +Caused by: org.hibernate.exception.JDBCConnectionException: Unable to open JDBC Connection for DDL execution + at org.hibernate.exception.internal.SQLStateConversionDelegate.convert(SQLStateConversionDelegate.java:112) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:37) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:113) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:99) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl.getIsolatedConnection(DdlTransactionIsolatorNonJtaImpl.java:71) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.internal.exec.ImprovedExtractionContextImpl.getJdbcConnection(ImprovedExtractionContextImpl.java:63) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.internal.exec.ImprovedExtractionContextImpl.getJdbcDatabaseMetaData(ImprovedExtractionContextImpl.java:70) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.extract.internal.InformationExtractorJdbcDatabaseMetaDataImpl.processTableResultSet(InformationExtractorJdbcDatabaseMetaDataImpl.java:64) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.extract.internal.AbstractInformationExtractorImpl.getTables(AbstractInformationExtractorImpl.java:565) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.extract.internal.DatabaseInformationImpl.getTablesInformation(DatabaseInformationImpl.java:122) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.internal.GroupedSchemaMigratorImpl.performTablesMigration(GroupedSchemaMigratorImpl.java:68) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.performMigration(AbstractSchemaMigrator.java:220) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.internal.AbstractSchemaMigrator.doMigration(AbstractSchemaMigrator.java:123) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.performDatabaseAction(SchemaManagementToolCoordinator.java:196) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.process(SchemaManagementToolCoordinator.java:85) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.internal.SessionFactoryImpl.(SessionFactoryImpl.java:335) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:471) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1498) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:58) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:365) ~[spring-orm-5.3.30.jar:5.3.30] + at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:409) ~[spring-orm-5.3.30.jar:5.3.30] + ... 101 common frames omitted +Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure + +The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. + at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:175) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:825) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.(ConnectionImpl.java:446) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:239) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:188) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:138) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:364) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:206) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:476) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.HikariPool.checkFailFast(HikariPool.java:561) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.pool.HikariPool.(HikariPool.java:115) ~[HikariCP-4.0.3.jar:na] + at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:112) ~[HikariCP-4.0.3.jar:na] + at org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl.getConnection(DatasourceConnectionProviderImpl.java:122) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess.obtainConnection(JdbcEnvironmentInitiator.java:181) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + at org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl.getIsolatedConnection(DdlTransactionIsolatorNonJtaImpl.java:44) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final] + ... 117 common frames omitted +Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure + +The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. + at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(Unknown Source) ~[na:na] + at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Unknown Source) ~[na:na] + at java.base/java.lang.reflect.Constructor.newInstance(Unknown Source) ~[na:na] + at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:62) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:150) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:166) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.protocol.a.NativeSocketConnection.connect(NativeSocketConnection.java:89) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.NativeSession.connect(NativeSession.java:121) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:945) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:815) ~[mysql-connector-j-8.0.33.jar:8.0.33] + ... 130 common frames omitted +Caused by: java.net.UnknownHostException: database + at java.base/java.net.InetAddress$CachedLookup.get(Unknown Source) ~[na:na] + at java.base/java.net.InetAddress.getAllByName0(Unknown Source) ~[na:na] + at java.base/java.net.InetAddress.getAllByName(Unknown Source) ~[na:na] + at com.mysql.cj.protocol.StandardSocketFactory.connect(StandardSocketFactory.java:130) ~[mysql-connector-j-8.0.33.jar:8.0.33] + at com.mysql.cj.protocol.a.NativeSocketConnection.connect(NativeSocketConnection.java:63) ~[mysql-connector-j-8.0.33.jar:8.0.33] + ... 133 common frames omitted + diff --git a/backend-services/api-gateway/Dockerfile b/backend-services/api-gateway/Dockerfile new file mode 100644 index 00000000..057f2165 --- /dev/null +++ b/backend-services/api-gateway/Dockerfile @@ -0,0 +1,24 @@ +# 使用官方OpenJDK运行时作为基础镜像 +FROM openjdk:17-jdk-slim + +# 安装curl用于健康检查 +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +# 设置工作目录 +WORKDIR /app + +# 复制Maven构建产物 +COPY target/*.jar app.jar + +# 暴露端口 +EXPOSE 8080 + +# 设置JVM参数 +ENV JAVA_OPTS="-Xmx512m -Xms256m" + +# 健康检查 +# HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ +# CMD curl -f http://localhost:8082/health || exit 1 + +# 启动应用 +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] \ No newline at end of file diff --git a/backend-services/api-gateway/pom.xml b/backend-services/api-gateway/pom.xml index 5491a0fe..e9ece7ea 100644 --- a/backend-services/api-gateway/pom.xml +++ b/backend-services/api-gateway/pom.xml @@ -22,7 +22,7 @@ spring-cloud-starter-gateway - + org.springframework.cloud spring-cloud-starter-openfeign @@ -60,11 +60,10 @@ 0.11.5 runtime - + - + --> diff --git a/backend-services/api-gateway/src/main/java/com/ai/qa/gateway/api/controller/TestConfigController.java b/backend-services/api-gateway/src/main/java/com/ai/qa/gateway/api/controller/TestConfigController.java index bec1482c..8b5669f0 100644 --- a/backend-services/api-gateway/src/main/java/com/ai/qa/gateway/api/controller/TestConfigController.java +++ b/backend-services/api-gateway/src/main/java/com/ai/qa/gateway/api/controller/TestConfigController.java @@ -1,8 +1,6 @@ package com.ai.qa.gateway.api.controller; -import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -10,13 +8,13 @@ @RequestMapping("/api/test") public class TestConfigController { - - @Value("${jwt.secret}") - private String jwtSecret; + // @Value("${jwt.secret}") + // private String jwtSecret; @GetMapping("/config") public String login() { System.out.println("测试config"); - return "测试JWT:"+jwtSecret; + // return "测试JWT:" + jwtSecret; + return "/api/test/config"; } } diff --git a/backend-services/api-gateway/src/main/java/com/ai/qa/gateway/api/web/filter/AuthenticationFilter.java b/backend-services/api-gateway/src/main/java/com/ai/qa/gateway/api/web/filter/AuthenticationFilter.java index 2e4ff2dd..52282f13 100644 --- a/backend-services/api-gateway/src/main/java/com/ai/qa/gateway/api/web/filter/AuthenticationFilter.java +++ b/backend-services/api-gateway/src/main/java/com/ai/qa/gateway/api/web/filter/AuthenticationFilter.java @@ -3,13 +3,11 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import org.springframework.beans.factory.annotation.Value; -import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; diff --git a/backend-services/api-gateway/src/main/java/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig.java b/backend-services/api-gateway/src/main/java/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig.java index fbedb088..16c0d12b 100644 --- a/backend-services/api-gateway/src/main/java/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig.java +++ b/backend-services/api-gateway/src/main/java/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig.java @@ -31,8 +31,11 @@ public KeyResolver ipKeyResolver() { public org.springframework.cloud.gateway.filter.ratelimit.RateLimiter inMemoryRateLimiter() { // 定义默认的限流速率 - final double defaultReplenishRate = 5.0; // 每秒生成的令牌数 - final int defaultBurstCapacity = 100; // 令牌桶总容量 +// final double defaultReplenishRate = 1000.0; // 每秒生成的令牌数 +// final int defaultBurstCapacity = 10000; // 令牌桶总容量 + + final double defaultReplenishRate = 10000.0; + final int defaultBurstCapacity = 100000; return new org.springframework.cloud.gateway.filter.ratelimit.RateLimiter() { @@ -41,6 +44,8 @@ public org.springframework.cloud.gateway.filter.ratelimit.RateLimiter isAllowed(String routeId, String id) { + + // routeId 是当前请求匹配的路由ID // id 是 KeyResolver 解析出的 key (IP地址) // 获取当前路由的配置,如果不存在则使用默认配置 @@ -57,6 +62,7 @@ public Mono isAllowed(String routeId, String id) { // 尝试获取一个令牌 boolean allowed = limiter.tryAcquire(); + log.info("Limiter: {} Allow: {}", id, allowed); if (allowed) { log.info("Request ALLOWED. Route: {}, Key: {}", routeId, id); diff --git a/backend-services/api-gateway/src/main/resources/application.yml b/backend-services/api-gateway/src/main/resources/application.yml index 7dbc4e3e..62accdff 100644 --- a/backend-services/api-gateway/src/main/resources/application.yml +++ b/backend-services/api-gateway/src/main/resources/application.yml @@ -1,45 +1,52 @@ server: - port: 8080 # 所有后端请求的入口 + port: ${SERVER_PORT:8080} + logging: + file: + path: ./logs + name: application.log level: - # 将 Gateway 的核心日志级别设置为 DEBUG org.springframework.cloud.gateway: DEBUG - # (可选) 将 Reactor Netty 的日志也设为 DEBUG,可以看到更底层的网络交互 reactor.netty.http.client: DEBUG - com.alibaba.nacos.client: DEBUG + spring: application: name: api-gateway - config: - import: - # 引入共享配置 - - "nacos:shared-config.yml" cloud: - nacos: - server-addr: 54.219.180.170:8848 - config: - # 明确告诉 Nacos Config 默认的文件扩展名是 yml - file-extension: yml - group: DEFAULT_GROUP gateway: + globalcors: + cors-configurations: + "[/**]": + allowed-origins: "${ALLOWED_ORIGINS:http://localhost:3000,http://127.0.0.1:3000,http://16.170.233.101:3000}" + allowed-methods: "*" + allowed-headers: "*" + allow-credentials: true + max-age: 3600 discovery: locator: - enabled: false # 开启基于服务发现的路由功能 - lower-case-service-id: true # 将服务名转为小写路径,e.g., user-service -> /user-service/** + enabled: false routes: - id: user_service_route - uri: lb://user-service # lb:// 表示从Nacos负载均衡地选择一个user-service实例 -# uri: http://192.168.31.186:8081 + uri: ${USER_SERVICE_URL} # http://user-service:8081 predicates: - - Path=/api/user/** # 匹配所有/api/user/开头的请求 + - Path=/api/user/** filters: - - StripPrefix=2 + - StripPrefix=0 - id: qa_service_route - uri: lb://qa-service + uri: ${QA_SERVICE_URL} # http://qa-service:8082 predicates: - Path=/api/qa/** - default-filters: - # 配置默认的限流过滤器 - - name: RequestRateLimiter - args: - key-resolver: '#{@ipKeyResolver}' \ No newline at end of file + filters: + - StripPrefix=0 + - name: RequestRateLimiter + args: + rate-limiter: "#{@inMemoryRateLimiter}" + key-resolver: "#{@ipKeyResolver}" + replenish-rate: 10000 + burst-capacity: 100000 + requested-tokens: 1 + +springdoc: + swagger-ui: + path: /swagger-ui.html + enabled: true diff --git a/backend-services/api-gateway/target/classes/application.yml b/backend-services/api-gateway/target/classes/application.yml new file mode 100644 index 00000000..62accdff --- /dev/null +++ b/backend-services/api-gateway/target/classes/application.yml @@ -0,0 +1,52 @@ +server: + port: ${SERVER_PORT:8080} + +logging: + file: + path: ./logs + name: application.log + level: + org.springframework.cloud.gateway: DEBUG + reactor.netty.http.client: DEBUG + +spring: + application: + name: api-gateway + cloud: + gateway: + globalcors: + cors-configurations: + "[/**]": + allowed-origins: "${ALLOWED_ORIGINS:http://localhost:3000,http://127.0.0.1:3000,http://16.170.233.101:3000}" + allowed-methods: "*" + allowed-headers: "*" + allow-credentials: true + max-age: 3600 + discovery: + locator: + enabled: false + routes: + - id: user_service_route + uri: ${USER_SERVICE_URL} # http://user-service:8081 + predicates: + - Path=/api/user/** + filters: + - StripPrefix=0 + - id: qa_service_route + uri: ${QA_SERVICE_URL} # http://qa-service:8082 + predicates: + - Path=/api/qa/** + filters: + - StripPrefix=0 + - name: RequestRateLimiter + args: + rate-limiter: "#{@inMemoryRateLimiter}" + key-resolver: "#{@ipKeyResolver}" + replenish-rate: 10000 + burst-capacity: 100000 + requested-tokens: 1 + +springdoc: + swagger-ui: + path: /swagger-ui.html + enabled: true diff --git a/backend-services/api-gateway/target/classes/com/ai/qa/gateway/ApiGatewayApplication.class b/backend-services/api-gateway/target/classes/com/ai/qa/gateway/ApiGatewayApplication.class new file mode 100644 index 0000000000000000000000000000000000000000..cc32f792998f7ce5768fd14f4d483c89f274fc10 GIT binary patch literal 889 zcma)4%We}f6g^IpPCA4>QYf^%#EK+~dWVn#mD)hrgog+U3s#kL~%rRD2l z7D@YFM5$zG4^1r6v9QYP3uE1#shpuZ{7*W4Dm4>IGwgLf4wtk>&MKYsyWvJoEOc6*GBh>?a->vhW&Si zc4{WaDp$U`*2a}DNhn8^q|2Fz!Wm*;vX+YpozSpW*c0uON`N>u#e~PHlG^c!%42gO z?G=Baef!V<>VwixAW!}QVlt_xQYCtked_P?%)F0>phu!l4AkgG|BTLnG)|I{Z1ldN z`L%S1bb2>%jTX?F2avo@+6FeUMIb);aR~-A+pPB;+aH$sA$dUY0Yxt{n%KiFvfrlN ZpnpiBhW+w9z#WpC#PKf4d$jKZe*mrq^uz!F literal 0 HcmV?d00001 diff --git a/backend-services/api-gateway/target/classes/com/ai/qa/gateway/api/controller/TestConfigController.class b/backend-services/api-gateway/target/classes/com/ai/qa/gateway/api/controller/TestConfigController.class new file mode 100644 index 0000000000000000000000000000000000000000..a281c18f3925164b90faf8351bb04e9024c10722 GIT binary patch literal 899 zcmbVK$!^p@5PdzFiDxlMa5BKKhjoSneE|UqAfZTbF^e+MTsXCzj_F``PwaL^`3BB> z2a5zq9QXh}1o0J!vd5Yb!2vk9s>@yV>ecJ|`s4E#0QWF(P-D0mnuH6*cLk4xl@qbg z#aQvsXq%cimMPzqnO!$}TSa86pAPB_OFLpu@L1@GKMi(dXc^}3Dy{5&hPnP=#86*1 zTe6Kgv|KbW&#>q#EuZ8`Ak$3|#FD}DO(^1#NL3M6KkN2|${23>|3z+%p%I%%X@-^l zz&~I)w5ie&UH3L}ZB-&iDpS<;f!4+ftBj_mH++*uJR29O+o?$8#H26zLxHy3xNn~yxaeM?!l;ui^QjVU`wxB zQDtZkO`eAGp(h7L)M)pkUv5Z!vYowWQooT6dk3{dk&UixdgmZHUK*B svErTlh|}JMx0G5#0ox`IDs9611-er~*Kn!Kyj+5NlzWBZtFs3EpVD;c?f?J) literal 0 HcmV?d00001 diff --git a/backend-services/api-gateway/target/classes/com/ai/qa/gateway/api/web/filter/AuthenticationFilter.class b/backend-services/api-gateway/target/classes/com/ai/qa/gateway/api/web/filter/AuthenticationFilter.class new file mode 100644 index 0000000000000000000000000000000000000000..81e3214a7955baebb799a46e6c697ee70adebd7f GIT binary patch literal 4751 zcmd5<>t7tz8Gg>PFbg|GiY65!F)^qRLdK+tNm&vbXf8xT5CYMp7iX6P3@kI7nOO*0 zYj0|=F}>M(X>0Ent&P@V)>eDh*0#2PMf>abe&~nV_Bk^HEN0kHKK1v5Ju_#{dC&Vk z@AF>H>;Hb~6#)D37Yzx4JvnFEFfHS(X^fk`oHgeRvtSvsGHZ-kd0)E5!J(^=`BG^vZy*vXms5!1EQdo-E!CoC$r zw{K0Jpnf$Wo&x7vV+~Zk^rSmOUk(MsDVaTVE;m8P$xQ5ycz1>L2>sPFVOn;kt7u|=S6T>69ZY*Bh-=YiI?SZpVJzp&tr06l9)Mt(WUPtpEN3R}sSAQp>sp5e-I z%VRjEa1)ueG@p0IEjx`{(59hPU|kRrMc>LBeO#|&8`=dD&X~Y$tz|Zc0h_serY)RW zlt5TIU})GO&>08I>det`D|V8p1Za4$m#Kr`1e)5?H#n22!#l7?!)^h+d|tVVm1NFg<|>u79oM=y$bEG=(q!Ygp#Nqwp>N#o6Dh$P>(!V?uZon>8x&Py3*ZThXD*~xN}*d2>0q3!b#fa z`KIf8r!3{!W!_pig5GMzh>laZOCTMDmsfCXkNM>GN_a%+E;hbG0hwBy#(OlJ5!hN0 zlOdLmqNn4%cpu3*+<$QR^33R7G3y1jR(Y&9 zh68o5QPAKpRxQVv^c_<)A{*iT$ZqK^CV!3a}UDs~We$T`LKGu^9D zM;-Jb9UsO=*dt_>B?8Tr>>v|9hKDpf$Tnp~)pR_Jk28@60QKZeYuYM$NXsuK!s>wlvjj)h)`|AG>*#4GZOT#qPgi`3ZuB+iPbhEMKawk`+B<*ix6s z@HE+RggppV4iY|tDiz+gQ^;7QBuPe)~zZePz{Yw301&2rg;lbbJY1??_nL6u(L-4 zf8WMW21miMdfSU=e2xzZT*Kd*V6qYG`K)$w0XE?690@X-DC9amC$LcoN$f$2r$Hp! zOSoR3zwyS#mP@#~Guf2vC}F$6Ja(0^FTo6(-p(~$30&ldgS>hin#YmO>Za;fQQuVE zczhlw9>*pw85VdRcbD+~1WtvoB@ma3P1OzBJgm-|OPJpF0=#(?OPEWbga?RPO$i@W z&z}(Z1s>_tc*tjY$QP3@W8m(pCar`o2@I9+coiC-T9|D7M&m#U->Tx4f$&=i-{Uwp_2Mz!5^i2r&d8jA~c1+;%_uM8DNXuMjYYW z2wITk-C_!DY-+Z#+_&QaY{x?^faf`Pfw!7Ru@g^Y7hdB9=8t@T1G{;`9{im*mw#e! z;HNSAn?8vAq@FHOU;2(uFX5;3mWDT(3V%lLHQ*n38Lv>oeU$fe?yTndj}d}8`u+kt z;a}ocT>l33{xx@`c+y7v2EXOptF%UOB9!qOW&DmaJCS@7?HW2X-10AM)G%<}f7pj; zl#}v687ZqGV-qw& Xt@;xc1{(SF1V#M0rk44(S&jb%O!u3) literal 0 HcmV?d00001 diff --git a/backend-services/api-gateway/target/classes/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig$1.class b/backend-services/api-gateway/target/classes/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig$1.class new file mode 100644 index 0000000000000000000000000000000000000000..06db9de221f25a08af81401393c91fb3f5d46bc3 GIT binary patch literal 5544 zcmcIoiGLJV9sj;$v$I*IumnO2);7`*Hx~<7Y00KYE=bBIsR>O0tvcDvZYG(XS!QR6 zDOT&z!>d}YT0E=us%NPsp`aD5*T;{4Q$K#+%xrdYln;Csl23Nu`@Q$O&zJwcd-ZJq zkKjKF>I4R|b|Ix1sdHK?ueti7wv;lgoTHUor<`@mj-JZeR?f($Myyf2U^`0_yp=Hu zhO0XR-bw301rdS7ymnqonVOYPjnB;MSyw?+;Hd57Q>CI~Sa}jH=!>?qkjk2Nd3Kdu z&M-+U<&dr^>83&)1Y#accS-`ujK`wv8m1H~TXr1Xa#NHdSPpBYxlyf{7HFI_@|GsW z7g)GSv|j(7r@^EvB~E9gaq_mEH>DX01=~uk=N=L^eUzLP-&_>;#Txn6=9G|JzMDMh zqVAkgYCS-=d#csMg33mF4a;zk3LH<~272vN0+9iGR&RvB4i$G{YaI1xRCM!Zw5=0*sc2gzogt^-g94Ma*O(3A96k2V^DJXv zfQV;biK7i&GETY$8j^z}?NieGDHR8BP(aP=?zl6k=d`lv3OpP(kzcFQq1+E3?HfT& zWcMKz58}h*q7{qgl7M>~BENNmcIkk-S8Z}-E9)ApJ;#GZS}OODiidH8u}tF@%dS3> z>zgUD9X zS936^7{n0Cns(kxT-jmF+=KI}jGfQxPCAAY0?k2>>S?*~REw8mauvr2PAYg*V0$&Q z`fc0PHA_VXqb%U(HM6Xb=LB{o&xS1%TuX1kI6kW2F($N^Cn44HO)Vt8EOfeHOwSueRQeVNSo^w8k`mI<4FmTGjhoYa@(Lcz4aiQAN} z3eHsQ;sr|3PUAR@<~XMCahYIGurw#nhFY6!xH8>72~EMLSZ!}mpH$2sOZ&{1ON`Tl zVf(CR-3`#`S%%ZWid_bW#zuw)Taq$+T6}lUwr^%7>9M?uInOtXnq$Q<&j>l)y_$F3 z6M8{oFSGJ7m;&N}RGJZC%kojr^orq}z@9Khf7vOy-2+-t%Np*IEI=g{F3JoGmK*Q1 za_FVNNF0k;Qt;_DqmmyMGW4FLM9GnGzy%=_!)Mm9p3sY?Zn5lp3P{$URq;7I?XzAz zmD+i%2PNw-NG4yDOa?b$!nk*2bd9hPHH9i)AcH`i!IP*z+z32=8(QM3@hgKQD~=LO zdjcts!_CiL#MLUDRGj5*PS<9=9=jtHtknr|*V=}^RID`pBO9-8HW^m)I53eM{_dJ| zwd?{jA{ZbV843RM#l%VmX7_T-A5))P58I4W&GmAV_9BJdx+>g&sg*d4?5IfN6(bMG zYx5hSy?4a>Ql>^rU#u8xnvCy+?lNDR1$M5{D=0s44v9Zf@iH#TmxorEnbqX9{PLY? z@*DHa*ZAh6UiRFds`wdRmC4D{DKq|%%>q~bD`1-9@N zQ2`13FWgN7OrQ9$X3d&#1mf8o<%#Io3*WP#LK zBDZg7S_=Lyu&*l98XK*(yn=r)ac+E-Y3#d+^{4WT;&P00;ILhC(LzP};z0vHo8+^W z>qb6H>JaCikK5I~nn=hOG|v?NZ9{^0wsSQ_%sPIzbaY-uQ^yK+c3i{VXI8NLGWJxL z_g>;t;Tm!`IQ%6WJYEM(^Q~NU&1Ydvm;2~P!7$xmPKL&7u?~zFi zA;*=&2gU_Fif3>VFLB^{6{DW5hJED&ThR>o67L9U5IoCO9jRW#b9kO+>m=mM_zJlm z;#Bule2w0|fW!DYUO*JzAda-W5BVk`FZz&okyFs7pocYtkj6T)07=pK_!hoR7QR1T z^1>iRsAS(D{qZ^%zOSPma607QhCpV4@8G+uM84+{=?;k0H?^%N@kEG3{VEAIR-(u- zJVa8Ge@uqL3ckM{KO4f2`S^)GHs&7$`1=CZ4IRt);d*4@Av4Moaaj`rlyS1GC(9i~ zp{rjR?c%~&#?Pm_ssy%rSB@}|(v-8;BeaW%M8mH z<37T$_&p>=>82IZdkcTY+xROXdwD9gmOpL0cb%Aj!#nsVb~7;>@GtKF&9gf6{U6F~ BYTf_< literal 0 HcmV?d00001 diff --git a/backend-services/api-gateway/target/classes/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig$RateLimiterConfig.class b/backend-services/api-gateway/target/classes/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig$RateLimiterConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..3529003a96850f490bb1ae59eb28757999e27741 GIT binary patch literal 1180 zcmcIiT~8B16g@+?E!09w5&S?91f}rcKKQ7TD8$&LpaCWEak>ockaib#rx;8$Ci-(s zj0wcV2Y-M+%6RS;K?I(R*<|MK{W$mBbLYp;Z{Gnt#9SVI3hP1C@wN4Lwcpgvyw>}^ z4Yy*QxVRg*ZfyJ@3b$<2uZ1h76UF;$%&OatbtYa)BeQ2bd3XxtZM~=cwho*Av&Oaw zoWfvicG@PiNlQu;)G~dIZk)I!y`ux`_UWrBA99o6kgWKN`=3L6xF@8jOk=UY5E~R_Iv^VryHhh;Nj4@v@dqayQN~)As zmM@b*g&UYS=JZ?21B`xR@J~+f&*e0Uo5!4FQ^INdlzKf*AK2{U1)M~yV@{$|22*MC TY24y1i&mJyZHj*<_0RkQKo8-C literal 0 HcmV?d00001 diff --git a/backend-services/api-gateway/target/classes/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig.class b/backend-services/api-gateway/target/classes/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..496fa14f1959fa6bb705953b32a19916197f80a1 GIT binary patch literal 2930 zcmbtW?Q#@F6g{1f9kL9tgdhlth(R|f3?QPiF_5gpL^gb6F^J#2ySH6f~anyKsj~aYwDIW>&>aQAFR$(tD@W5sGyvzSQro}EXft= zxYBDmH)>0&ZUp*Wza=nS@q?BVy3GqqPQ`DvR8SJwHC}hMr_BVlien1`17*LV^4N_% z7Hok-8~04hx}j|wd$CVosHIF7OWIus~ZcI-3yV2|!xQ>*hT^xYK|2uu}I z-km^uEk>ZN?)$;AQ+NHSu^yYIcIkL0U=Z9m2u=orpfAryp3!Z!phL~0OnRPgq|v@d z+NZm2*$D4@Mm;c&Tvv6bl=Mo&%#mX@Uc(UqE9kNmIG{IY!3$H*WD=w!l_cc(-{bs3Qz3D_XmzJqI=X-u>H!k3! zh4<+Bq$+I4-|diXypInA@+=qg>RzNmR;7t&q}HM_oi&OH9lfGbt}|Rp_mj;fj9XwH zT-u7w^P$+RzASx(gjd@)YO>Mb72_Q}Ms`U#^ORx5Ri^6Kmz7D^OL<(ul!ZxwBhS{d zQ3m4`pL!kl@J8Go;xuL~e8TuXg_y3}n8l|8dxABuKmZJ+1Jj*4iUJx zr3p60jb*c2BcdRww@i@2lB@2t1don7!5=`~yxj#hksIqH-$JuwYVM)K6g+p8UwRe%1)uF389 zrrDf5X<52%m9K}=z8vlpa6Zr0{GE++I8VX16bRU}p2yck+{{&Ifl;dNykKss=6n%s zl37^flVVeJw(_xsC1%g|ssg!gKp4pDEI)if@zoLHb)LDO9f9{aev|OOLF|IXkA0A{ zF^=(XXjnW#{#O)!;iM07PtGMv2XG*f#1MR%q}v>h@y5YZzv1AL{>=c48X=|oFPApm=2lt7xHoPP!ij? zoH9Rn>M`CGc#IDP9^&#tTz!OVJwshDW)E@@w=hqDK~(v-54YEy@)_p~{9@OAO)}p= fqJ}ypM>s2TjDHQTHc6ocjb*ykr7TCBA{ze!p)XG6 literal 0 HcmV?d00001 diff --git a/backend-services/pom.xml b/backend-services/pom.xml index 9094f9fe..a56198e3 100644 --- a/backend-services/pom.xml +++ b/backend-services/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.5 + 2.7.17 @@ -25,21 +25,13 @@ 17 17 UTF-8 - 2022.0.5 - 3.1.11 - 2022.0.0.0 + 2021.0.8 + 2021.0.5.0 - - org.springframework.boot - spring-boot-dependencies - ${spring-boot.version} - pom - import - org.springframework.cloud spring-cloud-dependencies @@ -54,6 +46,7 @@ pom import + \ No newline at end of file diff --git a/backend-services/qa-service/Dockerfile b/backend-services/qa-service/Dockerfile new file mode 100644 index 00000000..50d374a7 --- /dev/null +++ b/backend-services/qa-service/Dockerfile @@ -0,0 +1,24 @@ +# 使用官方OpenJDK运行时作为基础镜像 +FROM openjdk:17-jdk-slim + +# 安装curl用于健康检查 +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +# 设置工作目录 +WORKDIR /app + +# 复制Maven构建产物 +COPY target/*.jar app.jar + +# 暴露端口 +EXPOSE 8082 + +# 设置JVM参数 +ENV JAVA_OPTS="-Xmx512m -Xms256m" + +# 健康检查 +# HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ +# CMD curl -f http://localhost:8081/health || exit 1 + +# 启动应用 +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] \ No newline at end of file diff --git a/backend-services/qa-service/pom.xml b/backend-services/qa-service/pom.xml index e124272d..f1d0b1db 100644 --- a/backend-services/qa-service/pom.xml +++ b/backend-services/qa-service/pom.xml @@ -18,35 +18,88 @@ false - + org.springframework.boot spring-boot-starter-web - + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud spring-cloud-starter-openfeign - + + + org.springframework.boot spring-boot-starter-data-jpa + + + mysql + mysql-connector-java + 8.0.33 + org.projectlombok lombok true + + jakarta.validation + jakarta.validation-api + + + + org.mapstruct + mapstruct + 1.5.3.Final + + + org.mapstruct + mapstruct-processor + 1.5.3.Final + provided + + + + + org.springdoc + springdoc-openapi-ui + 1.7.0 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + io.github.cdimascio + java-dotenv + 5.2.2 + + + + org.springframework.boot + spring-boot-starter-actuator + diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/api/controller/QAController.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/api/controller/QAController.java index 4da8cab5..69faf055 100644 --- a/backend-services/qa-service/src/main/java/com/ai/qa/service/api/controller/QAController.java +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/api/controller/QAController.java @@ -1,33 +1,183 @@ package com.ai.qa.service.api.controller; +import com.ai.qa.service.api.dto.AskQaRequest; import com.ai.qa.service.api.dto.QAHistoryDTO; +import com.ai.qa.service.api.dto.SaveHistoryRequest; +import com.ai.qa.service.application.dto.QAHistoryQuery; // 确保这个导入正确 import com.ai.qa.service.application.dto.SaveHistoryCommand; +import com.ai.qa.service.application.service.QAHistoryService; import com.ai.qa.service.domain.service.QAService; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.List; +/** + * QA问答控制器 + * 提供问答相关的REST API接口 + */ @RestController @RequestMapping("/api/qa") @RequiredArgsConstructor public class QAController { private final QAService qaService; + private final QAHistoryService qaHistoryService; - @GetMapping("/test") - public String testFeign() { - System.out.println("测试feign"); - return qaService.processQuestion(1L); + /** + * 处理用户问题 + * + * @param userId 用户ID + * @param question 用户问题 + * @param sessionId 会话ID(可选) + * @return AI生成的回答 + */ + @PostMapping("/ask") + public ResponseEntity askQuestion( + @RequestBody AskQaRequest request) { + try { + String answer = qaService.processQuestion(request.getUserId(), request.getQuestion(), + request.getSessionId()); + return ResponseEntity.ok(answer); + } catch (Exception e) { + return ResponseEntity.internalServerError() + .body("处理问题时发生错误: " + e.getMessage()); + } } - + /** + * 保存问答历史记录 + * + * @param request 保存请求 + * @return 保存后的历史记录 + */ @PostMapping("/save") - public ReponseEntity saveHistory(@RequestBody SaveHistoryRequest request){ -// request.getUserId - SaveHistoryCommand command new = SaveHistoryCommand() - QAHistoryDTO dto= qaHistorySerive.saveHistory(command); + public ResponseEntity saveHistory(@RequestBody SaveHistoryRequest request) { + SaveHistoryCommand command = new SaveHistoryCommand(); + command.setUserId(request.getUserId()); + command.setQuestion(request.getQuestion()); + command.setAnswer(request.getAnswer()); + command.setSessionId(request.getSessionId()); + command.setRagContext(request.getRagContext()); + + QAHistoryDTO dto = qaHistoryService.saveHistory(command); + return ResponseEntity.ok(dto); + } + + /** + * 根据ID获取问答记录 + * + * @param id 记录ID + * @return 问答记录 + */ + @GetMapping("/history/{id}") + public ResponseEntity getHistoryById(@PathVariable String id) { + try { + QAHistoryDTO dto = qaHistoryService.getHistoryById(id); + return ResponseEntity.ok(dto); + } catch (Exception e) { + return ResponseEntity.notFound().build(); + } + } + + /** + * 查询用户问答历史 + * + * @param userId 用户ID + * @param sessionId 会话ID(可选) + * @param page 页码(可选,默认1) + * @param size 每页大小(可选,默认10) + * @return 问答历史列表 + */ + @GetMapping("/history/user/{userId}") + public ResponseEntity> getUserHistory( + @PathVariable String userId, + @RequestParam(required = false) String sessionId, + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "10") Integer size) { + + QAHistoryQuery query = new QAHistoryQuery(); + query.setUserId(userId); + query.setSessionId(sessionId); + query.setPage(page); + query.setSize(size); + query.setDesc(true); + + List historyList = qaHistoryService.queryUserHistory(query); + return ResponseEntity.ok(historyList); + } - return new ReponseEntity(dto) ; + /** + * 查询会话问答历史 + * + * @param sessionId 会话ID + * @return 会话问答历史 + */ + @GetMapping("/history/session/{sessionId}") + public ResponseEntity> getSessionHistory(@PathVariable String sessionId) { + List historyList = qaHistoryService.querySessionHistory(sessionId); + return ResponseEntity.ok(historyList); + } + + /** + * 删除问答记录 + * + * @param id 记录ID + * @return 操作结果 + */ + @DeleteMapping("/history/{id}") + public ResponseEntity deleteHistory(@PathVariable String id) { + try { + qaHistoryService.deleteHistory(id); + return ResponseEntity.ok().build(); + } catch (Exception e) { + return ResponseEntity.notFound().build(); + } + } + + /** + * 清空会话历史 + * + * @param sessionId 会话ID + * @return 操作结果 + */ + @DeleteMapping("/history/session/{sessionId}") + public ResponseEntity clearSessionHistory(@PathVariable String sessionId) { + try { + qaHistoryService.clearSessionHistory(sessionId); + return ResponseEntity.ok().build(); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } + + /** + * 获取用户问答统计 + * + * @param userId 用户ID + * @return 问答记录数量 + */ + @GetMapping("/stats/user/{userId}") + public ResponseEntity getUserHistoryCount(@PathVariable String userId) { + try { + long count = qaHistoryService.getUserHistoryCount(userId); + return ResponseEntity.ok(count); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } + + /** + * Feign客户端测试接口 + * + * @return 测试结果 + */ + @GetMapping("/test") + public String testFeign() { + System.out.println("测试feign"); + // return qaService.processQuestion(1L, "测试问题", "test-session"); + return "OK"; } -} +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/ApiResponse.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/ApiResponse.java new file mode 100644 index 00000000..63700d35 --- /dev/null +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/ApiResponse.java @@ -0,0 +1,71 @@ +package com.ai.qa.service.api.dto; + +import com.ai.qa.service.api.exception.ErrCode; +import lombok.Data; + +/** + * 统一API响应格式 + * 用于规范所有接口的返回数据格式,包含状态码、消息和数据 + * @param 响应数据的类型 + */ +@Data +public class ApiResponse { + + /** + * 响应状态码 + */ + private String code; + + /** + * 响应消息 + */ + private String message; + + /** + * 响应数据 + */ + private T data; + + /** + * 构造函数 + * @param code 状态码 + * @param message 消息 + * @param data 数据 + */ + public ApiResponse(String code, String message, T data) { + this.code = code; + this.message = message; + this.data = data; + } + + /** + * 创建成功响应 + * @param data 响应数据 + * @param 数据类型 + * @return 成功的ApiResponse实例 + */ + public static ApiResponse success(T data) { + return new ApiResponse<>(ErrCode.SUCCESS, ErrCode.SUCCESS_MSG, data); + } + + /** + * 创建错误响应 + * @param code 错误码 + * @param message 错误消息 + * @param 数据类型 + * @return 错误的ApiResponse实例 + */ + public static ApiResponse error(String code, String message) { + return new ApiResponse<>(code, message, null); + } + + /** + * 创建错误响应(根据错误码自动获取错误消息) + * @param code 错误码 + * @param 数据类型 + * @return 错误的ApiResponse实例 + */ + public static ApiResponse error(String code) { + return new ApiResponse<>(code, ErrCode.getErrMsg(code), null); + } +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/AskQaRequest.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/AskQaRequest.java new file mode 100644 index 00000000..225578c9 --- /dev/null +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/AskQaRequest.java @@ -0,0 +1,14 @@ +package com.ai.qa.service.api.dto; + +import lombok.Data; + +/** + * QA问答请求DTO + * 用于接收前端发送的问答请求参数 + */ +@Data +public class AskQaRequest { + private Long userId; + private String question; + private String sessionId; +} diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/QAHistoryDTO.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/QAHistoryDTO.java index 4aa9f036..33457a3f 100644 --- a/backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/QAHistoryDTO.java +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/QAHistoryDTO.java @@ -1,4 +1,62 @@ package com.ai.qa.service.api.dto; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * QA历史记录响应DTO + * 用于返回给前端的问答历史数据 + */ +@Data public class QAHistoryDTO { -} + + /** + * 记录ID + */ + private Long id; + + /** + * 用户ID + */ + private String userId; + + /** + * 用户问题 + */ + private String question; + + /** + * AI回答 + */ + private String answer; + + /** + * 问答时间戳 + */ + private LocalDateTime timestamp; + + /** + * 会话ID + */ + private String sessionId; + + /** + * 记录创建时间 + */ + private LocalDateTime createTime; + + /** + * 记录最后更新时间 + */ + private LocalDateTime updateTime; + + /** + * 简化的回答(用于列表显示) + */ + private String shortAnswer; + + /** + * 问答持续时间 + */ + private String duration; +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/SaveHistoryRequest.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/SaveHistoryRequest.java new file mode 100644 index 00000000..c0c05052 --- /dev/null +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/api/dto/SaveHistoryRequest.java @@ -0,0 +1,36 @@ +package com.ai.qa.service.api.dto; + +import lombok.Data; + +/** + * 保存历史记录请求DTO + * 用于接收前端保存问答历史的请求参数 + */ +@Data +public class SaveHistoryRequest { + + /** + * 用户ID + */ + private String userId; + + /** + * 用户问题 + */ + private String question; + + /** + * AI回答 + */ + private String answer; + + /** + * 会话ID + */ + private String sessionId; + + /** + * RAG上下文(可选) + */ + private String ragContext; +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/api/exception/ErrCode.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/api/exception/ErrCode.java index 8f6ce930..db7adf2b 100644 --- a/backend-services/qa-service/src/main/java/com/ai/qa/service/api/exception/ErrCode.java +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/api/exception/ErrCode.java @@ -1,18 +1,111 @@ package com.ai.qa.service.api.exception; +/** + * 错误码枚举 + * 定义系统错误码和错误信息 + */ public final class ErrCode { - /*** - * + /** + * 成功 + */ + public static final String SUCCESS = "0000"; + public static final String SUCCESS_MSG = "成功"; + + /** + * 通用业务错误 + */ + public static final String BAD_REQUEST = "4000"; + public static final String BAD_REQUEST_MSG = "请求参数错误"; + + public static final String UNAUTHORIZED = "4001"; + public static final String UNAUTHORIZED_MSG = "未授权访问"; + + public static final String FORBIDDEN = "4003"; + public static final String FORBIDDEN_MSG = "禁止访问"; + + public static final String NOT_FOUND = "4004"; + public static final String NOT_FOUND_MSG = "资源不存在"; + + public static final String INTERNAL_ERROR = "5000"; + public static final String INTERNAL_ERROR_MSG = "系统内部错误"; + + /** + * QA服务特定错误 */ -// SUCCESS="" + public static final String QA_HISTORY_NOT_FOUND = "4100"; + public static final String QA_HISTORY_NOT_FOUND_MSG = "问答记录不存在"; + + public static final String QA_USER_ID_REQUIRED = "4101"; + public static final String QA_USER_ID_REQUIRED_MSG = "用户ID不能为空"; + + public static final String QA_QUESTION_REQUIRED = "4102"; + public static final String QA_QUESTION_REQUIRED_MSG = "问题不能为空"; + + public static final String QA_ANSWER_REQUIRED = "4103"; + public static final String QA_ANSWER_REQUIRED_MSG = "回答不能为空"; - /*** - * 通用业务 + public static final String QA_SESSION_ID_REQUIRED = "4104"; + public static final String QA_SESSION_ID_REQUIRED_MSG = "会话ID不能为空"; + + public static final String QA_PROCESS_FAILED = "4105"; + public static final String QA_PROCESS_FAILED_MSG = "问题处理失败"; + + /** + * 用户服务错误 */ + public static final String USER_SERVICE_UNAVAILABLE = "4200"; + public static final String USER_SERVICE_UNAVAILABLE_MSG = "用户服务不可用"; + public static final String USER_NOT_FOUND = "4201"; + public static final String USER_NOT_FOUND_MSG = "用户不存在"; + + /** + * 数据库错误 + */ + public static final String DB_OPERATION_FAILED = "4300"; + public static final String DB_OPERATION_FAILED_MSG = "数据库操作失败"; - /*** + /** + * 根据错误码获取错误信息 * + * @param errCode 错误码 + * @return 错误信息 */ -} + public static String getErrMsg(String errCode) { + switch (errCode) { + case SUCCESS: + return SUCCESS_MSG; + case BAD_REQUEST: + return BAD_REQUEST_MSG; + case UNAUTHORIZED: + return UNAUTHORIZED_MSG; + case FORBIDDEN: + return FORBIDDEN_MSG; + case NOT_FOUND: + return NOT_FOUND_MSG; + case INTERNAL_ERROR: + return INTERNAL_ERROR_MSG; + case QA_HISTORY_NOT_FOUND: + return QA_HISTORY_NOT_FOUND_MSG; + case QA_USER_ID_REQUIRED: + return QA_USER_ID_REQUIRED_MSG; + case QA_QUESTION_REQUIRED: + return QA_QUESTION_REQUIRED_MSG; + case QA_ANSWER_REQUIRED: + return QA_ANSWER_REQUIRED_MSG; + case QA_SESSION_ID_REQUIRED: + return QA_SESSION_ID_REQUIRED_MSG; + case QA_PROCESS_FAILED: + return QA_PROCESS_FAILED_MSG; + case USER_SERVICE_UNAVAILABLE: + return USER_SERVICE_UNAVAILABLE_MSG; + case USER_NOT_FOUND: + return USER_NOT_FOUND_MSG; + case DB_OPERATION_FAILED: + return DB_OPERATION_FAILED_MSG; + default: + return "未知错误"; + } + } +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/api/exception/GlobalExceptionHandler.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/api/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..57973d14 --- /dev/null +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/api/exception/GlobalExceptionHandler.java @@ -0,0 +1,53 @@ +package com.ai.qa.service.api.exception; + +import com.ai.qa.service.domain.exception.QADomainException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.HashMap; +import java.util.Map; + +/** + * 全局异常处理器 + * 统一处理控制器层抛出的异常 + */ +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理领域异常 + */ + @ExceptionHandler(QADomainException.class) + public ResponseEntity> handleQADomainException(QADomainException e) { + Map response = new HashMap<>(); + response.put("code", ErrCode.BAD_REQUEST); + response.put("message", e.getMessage()); + response.put("data", null); + return ResponseEntity.badRequest().body(response); + } + + /** + * 处理运行时异常 + */ + @ExceptionHandler(RuntimeException.class) + public ResponseEntity> handleRuntimeException(RuntimeException e) { + Map response = new HashMap<>(); + response.put("code", ErrCode.INTERNAL_ERROR); + response.put("message", "系统内部错误: " + e.getMessage()); + response.put("data", null); + return ResponseEntity.internalServerError().body(response); + } + + /** + * 处理其他异常 + */ + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException(Exception e) { + Map response = new HashMap<>(); + response.put("code", ErrCode.INTERNAL_ERROR); + response.put("message", "系统异常: " + e.getMessage()); + response.put("data", null); + return ResponseEntity.internalServerError().body(response); + } +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/application/dto/QAHistoryQuery.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/application/dto/QAHistoryQuery.java index a9243c74..c62d2cbf 100644 --- a/backend-services/qa-service/src/main/java/com/ai/qa/service/application/dto/QAHistoryQuery.java +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/application/dto/QAHistoryQuery.java @@ -1,4 +1,36 @@ package com.ai.qa.service.application.dto; +import lombok.Data; + +/** + * QA历史记录查询DTO + * 用于接收前端查询参数 + */ +@Data public class QAHistoryQuery { -} + + /** + * 用户ID + */ + private String userId; + + /** + * 会话ID + */ + private String sessionId; + + /** + * 页码 + */ + private Integer page = 1; + + /** + * 每页大小 + */ + private Integer size = 10; + + /** + * 是否按时间倒序排列 + */ + private Boolean desc = true; +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/application/dto/SaveHistoryCommand.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/application/dto/SaveHistoryCommand.java index dfc7893a..19902642 100644 --- a/backend-services/qa-service/src/main/java/com/ai/qa/service/application/dto/SaveHistoryCommand.java +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/application/dto/SaveHistoryCommand.java @@ -1,4 +1,36 @@ package com.ai.qa.service.application.dto; +import lombok.Data; + +/** + * 保存历史记录命令DTO + * 用于接收前端保存问答历史的请求参数 + */ +@Data public class SaveHistoryCommand { -} + + /** + * 用户ID + */ + private String userId; + + /** + * 用户问题 + */ + private String question; + + /** + * AI回答 + */ + private String answer; + + /** + * 会话ID + */ + private String sessionId; + + /** + * RAG上下文(可选) + */ + private String ragContext; +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/application/service/QAHistoryService.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/application/service/QAHistoryService.java index 4b1fa187..4efd20d7 100644 --- a/backend-services/qa-service/src/main/java/com/ai/qa/service/application/service/QAHistoryService.java +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/application/service/QAHistoryService.java @@ -1,23 +1,276 @@ package com.ai.qa.service.application.service; -import com.ai.qa.service.application.dto.SaveHistoryCommand; +import com.ai.qa.service.api.dto.QAHistoryDTO; // 使用API层的DTO,确保与控制器层数据类型一致 import com.ai.qa.service.application.dto.QAHistoryQuery; +import com.ai.qa.service.application.dto.SaveHistoryCommand; +import com.ai.qa.service.domain.exception.QADomainException; import com.ai.qa.service.domain.model.QAHistory; +import com.ai.qa.service.domain.repo.QAHistoryRepo; +import com.ai.qa.service.domain.service.QAService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; +/** + * QA历史记录应用服务 + * 负责协调领域服务和前端请求,处理应用层的业务逻辑 + * 提供问答历史的增删改查和统计功能 + * + * 应用服务职责: + * 1. 协调多个领域服务完成复杂业务逻辑 + * 2. 处理事务边界 + * 3. 数据转换(领域对象 -> DTO) + * 4. 参数验证和业务规则执行 + */ +@Service +@Transactional(readOnly = true) // 默认只读事务,写操作需要单独标注@Transactional +@RequiredArgsConstructor // Lombok注解,自动生成包含final字段的构造函数 public class QAHistoryService { - public QAHistoryDto saveHistory(SaveHistoryCommand command){ - //command.getUserid!=null - QAHistory history = QAHistory.createNew("","",""); - repo.save(history); - return toDto(history); + // 依赖注入:QA历史记录仓库,负责数据持久化操作 + private final QAHistoryRepo qaHistoryRepo; + + // 依赖注入:QA领域服务,处理核心业务逻辑 + private final QAService qaService; + + /** + * 保存问答历史记录 + * 验证参数后创建领域对象并持久化 + * + * @param command 保存命令DTO,包含用户ID、问题、回答等信息 + * @return 保存后的历史记录DTO + * @throws QADomainException 当参数验证失败时抛出领域异常 + */ + @Transactional // 写操作需要开启事务 + public QAHistoryDTO saveHistory(SaveHistoryCommand command) { + // 参数验证:确保必要字段不为空 + validateSaveCommand(command); + + // 使用工厂方法创建领域对象:保持领域模型的完整性 + QAHistory history = QAHistory.createNew( + command.getUserId(), + command.getQuestion(), + command.getAnswer(), + command.getSessionId()); + + // 调用仓库保存数据:基础设施层负责具体持久化实现 + QAHistory savedHistory = qaHistoryRepo.save(history); + + // 转换为DTO返回给调用方:隔离领域模型和API层 + return toDto(savedHistory); } - public List queryUserHistory(QAHistoryQuery query){ -// query.getUserId + /** + * 查询用户问答历史 + * 根据查询条件获取用户的问答记录列表 + * + * @param query 查询参数DTO,包含用户ID、分页等信息 + * @return 用户问答历史DTO列表 + * @throws QADomainException 当用户ID为空时抛出领域异常 + */ + public List queryUserHistory(QAHistoryQuery query) { + // 验证用户ID:确保查询条件有效 + validateUserId(query.getUserId()); - List historyList= qaHistory.findHistoryByUserId(query.getUserId); + // 调用仓库获取数据:基础设施层执行数据库查询 + List historyList = qaHistoryRepo.findHistoryByUserId(query.getUserId()); + // 转换为DTO列表:应用层负责数据展示格式转换 return toDtoList(historyList); } -} + + /** + * 分页查询用户问答历史 + * 支持分页和排序的用户问答记录查询 + * + * @param query 查询参数DTO + * @return 分页的用户问答历史DTO + * @throws QADomainException 当用户ID为空时抛出领域异常 + */ + public Page queryUserHistoryPage(QAHistoryQuery query) { + // 验证用户ID + validateUserId(query.getUserId()); + + // 创建分页请求:封装分页和排序参数 + PageRequest pageRequest = createPageRequest(query); + + // 获取分页数据:目前是内存分页,后续可优化为数据库分页 + List historyList = qaHistoryRepo.findHistoryByUserId(query.getUserId()); + long totalCount = qaHistoryRepo.countByUserId(query.getUserId()); + + // 转换为DTO列表并封装分页结果 + List dtoList = toDtoList(historyList); + return new PageImpl<>(dtoList, pageRequest, totalCount); + } + + /** + * 查询会话问答历史 + * 获取特定会话中的所有问答记录 + * + * @param sessionId 会话ID + * @return 会话问答历史DTO列表 + * @throws QADomainException 当会话ID为空时抛出领域异常 + */ + public List querySessionHistory(String sessionId) { + // 验证会话ID:确保查询条件有效 + if (sessionId == null || sessionId.trim().isEmpty()) { + throw new QADomainException("会话ID不能为空"); + } + + // 调用仓库获取会话历史数据 + List historyList = qaHistoryRepo.findHistoryBySession(sessionId); + + // 转换为DTO列表返回 + return toDtoList(historyList); + } + + /** + * 根据ID获取问答记录 + * 通过记录ID精确查询单条问答历史 + * + * @param id 记录ID + * @return 问答记录DTO + * @throws RuntimeException 如果记录不存在时抛出运行时异常 + */ + public QAHistoryDTO getHistoryById(String id) { + // 调用领域服务获取记录:领域服务处理业务逻辑和异常 + QAHistory history = qaService.getHistoryById(id); + + // 转换为DTO返回 + return toDto(history); + } + + /** + * 删除问答记录 + * 根据记录ID删除指定的问答历史 + * + * @param id 要删除的记录ID + */ + @Transactional // 写操作需要事务 + public void deleteHistory(String id) { + // 委托给领域服务执行删除操作 + qaService.deleteHistory(id); + } + + /** + * 获取用户问答统计 + * 统计指定用户的问答记录数量 + * + * @param userId 用户ID + * @return 问答记录数量 + * @throws QADomainException 当用户ID为空时抛出领域异常 + */ + public long getUserHistoryCount(String userId) { + // 验证用户ID + validateUserId(userId); + + // 调用领域服务获取统计数量 + return qaService.getUserHistoryCount(userId); + } + + /** + * 清空会话历史 + * 删除指定会话中的所有问答记录 + * + * @param sessionId 会话ID + * @throws QADomainException 当会话ID为空时抛出领域异常 + */ + @Transactional // 写操作需要事务 + public void clearSessionHistory(String sessionId) { + // 验证会话ID + if (sessionId == null || sessionId.trim().isEmpty()) { + throw new QADomainException("会话ID不能为空"); + } + + // 委托给领域服务执行清空操作 + qaService.clearSessionHistory(sessionId); + } + + /** + * 验证保存命令参数 + * 确保保存操作的必要参数不为空 + * + * @param command 保存命令DTO + * @throws QADomainException 当参数验证失败时抛出领域异常 + */ + private void validateSaveCommand(SaveHistoryCommand command) { + if (command.getUserId() == null || command.getUserId().trim().isEmpty()) { + throw new QADomainException("用户ID不能为空"); + } + if (command.getQuestion() == null || command.getQuestion().trim().isEmpty()) { + throw new QADomainException("问题不能为空"); + } + if (command.getAnswer() == null || command.getAnswer().trim().isEmpty()) { + throw new QADomainException("回答不能为空"); + } + } + + /** + * 验证用户ID + * 确保用户ID参数有效 + * + * @param userId 用户ID + * @throws QADomainException 当用户ID为空时抛出领域异常 + */ + private void validateUserId(String userId) { + if (userId == null || userId.trim().isEmpty()) { + throw new QADomainException("用户ID不能为空"); + } + } + + /** + * 创建分页请求 + * 根据查询参数构建Spring Data的分页请求对象 + * + * @param query 查询参数DTO + * @return 分页请求对象 + */ + private PageRequest createPageRequest(QAHistoryQuery query) { + return PageRequest.of( + Math.max(query.getPage() - 1, 0), // 确保页码不小于0(前端页码从1开始,后端从0开始) + Math.max(query.getSize(), 1), // 确保每页大小不小于1 + query.getDesc() ? Sort.by(Sort.Direction.DESC, "timestamp") : Sort.by(Sort.Direction.ASC, "timestamp")); + } + + /** + * 将领域对象转换为DTO + * 领域模型 -> API数据传输对象 + * + * @param history QA历史记录领域对象 + * @return QA历史记录DTO对象 + */ + private QAHistoryDTO toDto(QAHistory history) { + QAHistoryDTO dto = new QAHistoryDTO(); + dto.setId(history.getId()); + dto.setUserId(history.getUserId()); + dto.setQuestion(history.getQuestion()); + dto.setAnswer(history.getAnswer()); + dto.setTimestamp(history.getTimestamp()); + dto.setSessionId(history.getSessionId()); + dto.setCreateTime(history.getCreateTime()); + dto.setUpdateTime(history.getUpdateTime()); + dto.setShortAnswer(history.getShortAnswer(100)); // 截取前100字符作为简略回答 + dto.setDuration(history.getDuration()); // 计算问答持续时间 + return dto; + } + + /** + * 将领域对象列表转换为DTO列表 + * 批量转换领域对象为API层可用的数据传输对象 + * + * @param historyList QA历史记录领域对象列表 + * @return QA历史记录DTO对象列表 + */ + private List toDtoList(List historyList) { + return historyList.stream() + .map(this::toDto) // 使用Stream API进行批量转换 + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/exception/QADomainException.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/exception/QADomainException.java new file mode 100644 index 00000000..3f086ca5 --- /dev/null +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/exception/QADomainException.java @@ -0,0 +1,36 @@ +package com.ai.qa.service.domain.exception; + +/** + * QA领域异常 + * 用于表示问答业务逻辑中的错误情况 + */ +public class QADomainException extends RuntimeException { + + /** + * 构造带有详细消息的领域异常 + * + * @param message 异常详细信息 + */ + public QADomainException(String message) { + super(message); + } + + /** + * 构造带有详细消息和原因的领域异常 + * + * @param message 异常详细信息 + * @param cause 异常原因 + */ + public QADomainException(String message, Throwable cause) { + super(message, cause); + } + + /** + * 获取异常类型 + * + * @return 异常类型标识 + */ + public String getExceptionType() { + return "QA_DOMAIN_EXCEPTION"; + } +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/model/QAHistory.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/model/QAHistory.java index 54c53080..af9c21a1 100644 --- a/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/model/QAHistory.java +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/model/QAHistory.java @@ -5,50 +5,126 @@ import java.time.LocalDateTime; - - +/** + * QA历史记录领域模型 + * 包含用户问答的核心业务逻辑和状态管理 + */ +@Getter +@Setter public class QAHistory { - private String id; + /** + * 唯一标识符 + */ + private Long id; + + /** + * 用户ID + */ private String userId; + + /** + * 用户提出的问题 + */ private String question; + + /** + * AI生成的回答 + */ private String answer; + + /** + * 问答发生的时间戳 + */ private LocalDateTime timestamp; + + /** + * 会话ID,用于关联同一会话中的多条记录 + */ private String sessionId; - private Object rag; + /** + * 记录创建时间 + */ + private LocalDateTime createTime; + + /** + * 记录最后更新时间 + */ + private LocalDateTime updateTime; - public String getId(){ - return this.id; + /** + * 私有构造函数,确保通过工厂方法创建对象 + * 保持领域模型的完整性和一致性 + */ + public QAHistory() { + this.timestamp = LocalDateTime.now(); + this.createTime = LocalDateTime.now(); + this.updateTime = LocalDateTime.now(); } /** + * 创建新的QA历史记录(工厂方法) * - * @param question - * @return + * @param userId 用户ID + * @param question 用户问题 + * @param answer AI回答 + * @param sessionId 会话ID + * @param rag RAG上下文 + * @return 新创建的QAHistory实例 */ - public String getAnswer(String question) { - String response = rag.getContext(); - return answer+response; + public static QAHistory createNew(String userId, String question, String answer, + String sessionId) { + QAHistory history = new QAHistory(); + history.userId = userId; + history.question = question; + history.answer = answer; + history.sessionId = sessionId; + return history; } - private QAHistory(String id){ - + /** + * 更新回答内容 + * + * @param newAnswer 新的回答内容 + */ + public void updateAnswer(String newAnswer) { + this.answer = newAnswer; + this.updateTime = LocalDateTime.now(); } - public String getUserId(){ - + /** + * 验证QA历史记录的有效性 + * + * @return true如果记录有效,否则false + */ + public boolean isValid() { + return userId != null && !userId.trim().isEmpty() && + question != null && !question.trim().isEmpty() && + answer != null && !answer.trim().isEmpty(); } - public String getRAGAnswer(){ - - getAnswer(); - serivice.sss(); - return ""; + /** + * 获取简化的回答(用于显示) + * + * @param maxLength 最大显示长度 + * @return 简化后的回答内容 + */ + public String getShortAnswer(int maxLength) { + if (answer == null) + return ""; + return answer.length() > maxLength ? answer.substring(0, maxLength) + "..." : answer; } - public static QAHistory createNew(String userId, String question, String answer,...){ - - return new QAHistory(); + /** + * 获取问答持续时间(如果适用) + * + * @return 问答处理时长描述 + */ + public String getDuration() { + if (createTime == null || updateTime == null) + return "unknown"; + long seconds = java.time.Duration.between(createTime, updateTime).getSeconds(); + return seconds + "s"; } -} +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/repo/QAHistoryRepo.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/repo/QAHistoryRepo.java index c23cbf4a..9e387a3a 100644 --- a/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/repo/QAHistoryRepo.java +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/repo/QAHistoryRepo.java @@ -1,14 +1,77 @@ package com.ai.qa.service.domain.repo; import com.ai.qa.service.domain.model.QAHistory; - import java.util.List; import java.util.Optional; +/** + * QA历史记录仓库接口 + * 定义领域层与基础设施层的契约,提供数据访问抽象 + */ public interface QAHistoryRepo { - void save(QAHistory history); + /** + * 保存QA历史记录 + * + * @param history 要保存的QA历史记录 + * @return 保存后的QA历史记录(包含生成的ID) + */ + QAHistory save(QAHistory history); + + /** + * 根据ID查找QA历史记录 + * + * @param id 记录ID + * @return 包含QA历史记录的Optional对象 + */ Optional findHistoryById(String id); + + /** + * 根据会话ID查找QA历史记录 + * + * @param sessionId 会话ID + * @return 该会话的所有QA历史记录列表 + */ List findHistoryBySession(String sessionId); -} + /** + * 根据用户ID查找QA历史记录 + * + * @param userId 用户ID + * @return 该用户的所有QA历史记录列表 + */ + List findHistoryByUserId(String userId); + + /** + * 根据用户ID和会话ID查找QA历史记录 + * + * @param userId 用户ID + * @param sessionId 会话ID + * @return 符合条件的QA历史记录列表 + */ + List findHistoryByUserIdAndSessionId(String userId, String sessionId); + + /** + * 删除QA历史记录 + * + * @param id 要删除的记录ID + */ + void deleteById(String id); + + /** + * 获取用户最近的QA历史记录 + * + * @param userId 用户ID + * @param limit 返回记录数量限制 + * @return 最近的QA历史记录列表 + */ + List findRecentHistoryByUserId(String userId, int limit); + + /** + * 统计用户的问答数量 + * + * @param userId 用户ID + * @return 该用户的问答记录总数 + */ + long countByUserId(String userId); +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/service/QAService.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/service/QAService.java index 8ed29162..03eb478d 100644 --- a/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/service/QAService.java +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/domain/service/QAService.java @@ -1,37 +1,181 @@ package com.ai.qa.service.domain.service; -import com.ai.qa.service.infrastructure.feign.UserClient; +import com.ai.qa.service.domain.model.QAHistory; +import com.ai.qa.service.domain.repo.QAHistoryRepo; +import com.ai.qa.service.infrastructure.feign.DeepSeekClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.List; + +/** + * QA领域服务 + * 处理问答相关的核心业务逻辑,协调领域对象和外部依赖 + */ @Service +@Transactional public class QAService { @Autowired - private UserClient userClient; + private QAHistoryRepo qaHistoryRepo; + + @Autowired // 添加Autowired注解 + private DeepSeekClient deepSeekClient; + + // @Autowired // 添加Autowired注解 + // private GeminiClient geminiClient; + + /** + * 处理用户问题并返回答案 + * 完整的问答处理流程:获取用户信息 → 生成RAG上下文 → 生成答案 → 保存历史 + * + * @param userId 用户名 + * @param question 用户问题 + * @param sessionId 会话ID + * @return AI生成的答案 + */ + public String processQuestion(Long userId, String question, String sessionId) { + // 1. 获取用户信息 + // String userInfo = getUserInfo(userId); + + // 2. 生成RAG上下文 + // QARAG rag = generateRAGContext(question, userId); + + // 3. 使用Gemini生成答案 + String answer = generateAnswer(question); + + // 4. 保存问答历史 + saveQAHistory(userId.toString(), question, answer, sessionId); - public String processQuestion(Long userId) { - // 1. 调用 user-service 获取用户信息 - System.out.println("Fetching user info for userId: " + userId); - String user; + return answer; + } + + /** + * 使用Gemini生成答案 + * 结合RAG上下文和用户问题生成更准确的回答 + * + * @param question 用户问题 + * @param rag RAG上下文 + * @return Gemini生成的答案 + */ + private String generateAnswer(String question) { try { + // 构建包含RAG上下文的增强问题 + // String enhancedQuestion = buildEnhancedQuestion(question, rag); + + // 调用deepSeek客户端获取答案 + String answer = deepSeekClient.askQuestion(question); + + // 可选:对答案进行后处理 + return postProcessAnswer(answer); - // 就像调用一个本地方法一样! - user = userClient.getUserById(userId); } catch (Exception e) { - // Feign 在遇到 4xx/5xx 错误时会抛出异常,需要处理 - System.err.println("Failed to fetch user info for userId: " + userId + ". Error: " + e.getMessage()); - // 可以根据业务返回一个默认的、友好的错误信息 - return "Sorry, I cannot get your user information right now."; + // 如果Gemini调用失败,使用降级方案 + return generateFallbackAnswer(question); } + } - if (user == null) { - return "Sorry, user with ID " + userId + " not found."; - } + /** + * 对Gemini的回答进行后处理 + * + * @param rawAnswer 原始回答 + * @return 处理后的回答 + */ + private String postProcessAnswer(String rawAnswer) { + // 这里可以添加一些后处理逻辑,比如: + // - 移除不必要的标记 + // - 格式化回答 + // - 检查回答质量等 + + return rawAnswer.trim(); + } + + /** + * 降级方案:当Gemini调用失败时使用 + * + * @param question 用户问题 + * @return 降级回答 + */ + private String generateFallbackAnswer(String question) { + return String.format( + "根据您的查询 '%s',我找到以下相关信息:\n\n%s\n\n" + + "(注意:当前为简化模式回答,完整AI功能暂时不可用)", + question); + } + + /** + * 保存问答历史 + * + * @param userId 用户ID + * @param question 用户问题 + * @param answer AI回答 + * @param sessionId 会话ID + */ + private void saveQAHistory(String userId, String question, String answer, + String sessionId) { + QAHistory history = QAHistory.createNew(userId, question, answer, sessionId); + qaHistoryRepo.save(history); + } + + /** + * 获取用户的问答历史 + * + * @param userId 用户ID + * @return 用户的问答历史列表 + */ + public List getUserHistory(String userId) { + return qaHistoryRepo.findHistoryByUserId(userId); + } - System.out.println("Question from user: " + user); + /** + * 获取会话的问答历史 + * + * @param sessionId 会话ID + * @return 会话的问答历史列表 + */ + public List getSessionHistory(String sessionId) { + return qaHistoryRepo.findHistoryBySession(sessionId); + } + + /** + * 根据ID获取问答记录 + * + * @param id 记录ID + * @return QA历史记录 + * @throws RuntimeException 如果记录不存在 + */ + public QAHistory getHistoryById(String id) { + return qaHistoryRepo.findHistoryById(id) + .orElseThrow(() -> new RuntimeException("问答记录不存在: " + id)); + } + + /** + * 删除问答记录 + * + * @param id 要删除的记录ID + */ + public void deleteHistory(String id) { + qaHistoryRepo.deleteById(id); + } + + /** + * 获取用户问答统计 + * + * @param userId 用户ID + * @return 用户的问答记录数量 + */ + public long getUserHistoryCount(String userId) { + return qaHistoryRepo.countByUserId(userId); + } - // 返回最终结果 - return user; + /** + * 批量删除会话历史 + * + * @param sessionId 会话ID + */ + public void clearSessionHistory(String sessionId) { + List sessionHistory = getSessionHistory(sessionId); + sessionHistory.forEach(history -> deleteHistory(history.getId().toString())); } -} +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/DeepSeekClient.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/DeepSeekClient.java new file mode 100644 index 00000000..f39ac789 --- /dev/null +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/DeepSeekClient.java @@ -0,0 +1,164 @@ +package com.ai.qa.service.infrastructure.feign; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * DeepSeek AI客户端 + * + * 负责与DeepSeek API进行通信,发送问题并获取AI回答 + * 封装了HTTP请求的细节,提供简单易用的接口 + * + */ +@Slf4j +@Component +public class DeepSeekClient { + + /** + * DeepSeek API的基础URL + */ + @Value("${deepseek.api.base-url:https://api.deepseek.com/v1}") + private String baseUrl; + + /** + * DeepSeek API Key + */ + @Value("${deepseek.api.key:YOUR_DEEPSEEK_API_KEY_HERE}") + private String apiKey; + + /** + * 使用的模型名称 + */ + @Value("${deepseek.api.model:deepseek-chat}") + private String model; + + private final RestTemplate restTemplate; + + public DeepSeekClient(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + log.info("DeepSeekClient 初始化完成"); + } + + /** + * 向DeepSeek AI发送问题并获取回答 + */ + public String askQuestion(String question) { + log.info("开始调用 DeepSeek API,问题长度: {}", question.length()); + + if ("YOUR_DEEPSEEK_API_KEY_HERE".equals(apiKey)) { + log.warn("DeepSeek API Key 未配置,返回模拟回答"); + return generateMockResponse(question); + } + + try { + String url = baseUrl + "/chat/completions"; + + Map requestBody = buildRequestBody(question); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(apiKey); // DeepSeek使用Bearer Token认证 + + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + + long start = System.currentTimeMillis(); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class); + long end = System.currentTimeMillis(); + + log.info("DeepSeek API 调用成功,耗时: {} ms", end - start); + return parseResponse(response.getBody()); + + } catch (Exception e) { + log.error("调用 DeepSeek API 失败: {}", e.getMessage(), e); + log.info("使用模拟回答作为备用方案"); + return generateMockResponse(question); + } + } + + /** + * 构建DeepSeek API请求体 + */ + private Map buildRequestBody(String question) { + Map requestBody = new HashMap<>(); + + // DeepSeek的请求格式与OpenAI兼容 + Map message = new HashMap<>(); + message.put("role", "user"); + message.put("content", question); + + requestBody.put("model", model); + requestBody.put("messages", List.of(message)); + requestBody.put("temperature", 0.7); + requestBody.put("max_tokens", 1000); + requestBody.put("stream", false); + + return requestBody; + } + + /** + * 解析DeepSeek API响应 + */ + private String parseResponse(Map responseBody) { + try { + Object choicesObj = responseBody.get("choices"); + if (!(choicesObj instanceof List) || ((List) choicesObj).isEmpty()) { + log.warn("DeepSeek API响应中choices格式不正确或为空"); + return "抱歉,我现在无法回答这个问题。"; + } + + Map firstChoice = (Map) ((List) choicesObj).get(0); + Object messageObj = firstChoice.get("message"); + if (!(messageObj instanceof Map)) { + log.warn("DeepSeek API响应中message格式不正确"); + return "抱歉,我现在无法回答这个问题。"; + } + + Map message = (Map) messageObj; + Object contentObj = message.get("content"); + + return contentObj != null ? contentObj.toString().trim() : "抱歉,我现在无法回答这个问题。"; + + } catch (Exception e) { + log.error("解析DeepSeek API响应失败: {}", e.getMessage(), e); + return "抱歉,处理回答时出现了问题。"; + } + } + + /** + * 生成模拟回答(当API调用失败或未配置时使用) + */ + private String generateMockResponse(String question) { + // 可以复用你原来的模拟回答逻辑 + String lowerQuestion = question.toLowerCase(); + + if (lowerQuestion.contains("你好") || lowerQuestion.contains("hello") || lowerQuestion.contains("介绍")) { + return "你好!我是DeepSeek AI助手,很高兴为您服务!\n\n" + + "我可以帮助您:\n" + + "• 回答各种问题\n" + + "• 提供信息查询\n" + + "• 协助解决问题\n" + + "• 进行日常对话\n\n" + + "请随时告诉我您需要什么帮助!"; + } else if (lowerQuestion.contains("天气")) { + return "关于天气查询:\n\n" + + "抱歉,我目前无法获取实时天气信息。建议您:\n" + + "• 查看手机天气应用\n" + + "• 访问天气预报网站\n" + + "• 询问语音助手\n\n" + + "如果您有其他问题,我很乐意帮助您!"; + } else { + return String.format("感谢您的提问:\"%s\"\n\n" + + "我正在努力理解您的问题。作为DeepSeek AI助手,我会尽力为您提供有用的信息和建议。\n\n" + + "如果您能提供更多详细信息,我将能够给出更准确的回答。\n\n" + + "请问您还有什么其他问题需要我帮助解决吗?", + question); + } + } +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/DeepSeekRestTemplateConfig.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/DeepSeekRestTemplateConfig.java new file mode 100644 index 00000000..80259f23 --- /dev/null +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/DeepSeekRestTemplateConfig.java @@ -0,0 +1,63 @@ +package com.ai.qa.service.infrastructure.feign; + +import java.net.InetSocketAddress; +import java.net.Proxy; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Configuration +public class DeepSeekRestTemplateConfig { + + @Bean("deepseekRestTemplate") + public RestTemplate deepseekRestTemplate() { + // 创建不配置代理的 RestTemplate + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + // factory.setProxy(createProxy()); + factory.setConnectTimeout(30_000); // 30秒连接超时 + factory.setReadTimeout(60_000); // 60秒读取超时 + + log.info("DeepSeek 专用 RestTemplate 已创建(无代理,连接超时: 30s,读取超时: 60s)"); + return new RestTemplate(factory); + } + + private Proxy createProxy() { + // 创建代理对象 + String proxyHost = "9.36.235.13"; + int proxyPort = 8080; + + Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); + + return proxy; + } + + /** + * 配置系统代理属性(备用方案) + */ + private void configureSystemProxy() { + // 启用系统代理 + System.setProperty("java.net.useSystemProxies", "true"); + + // 使用您的Mac代理配置 + String proxyHost = "9.36.235.13"; + String proxyPort = "8080"; + + // 设置HTTP代理 + System.setProperty("http.proxyHost", proxyHost); + System.setProperty("http.proxyPort", proxyPort); + + // 设置HTTPS代理 + System.setProperty("https.proxyHost", proxyHost); + System.setProperty("https.proxyPort", proxyPort); + + // 设置不使用代理的主机(本地服务) + System.setProperty("http.nonProxyHosts", "localhost|127.0.0.1|*.local|54.219.180.170"); + + log.info("系统代理属性配置完成: {}:{}", proxyHost, proxyPort); + } +} diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/FeignConfig.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/FeignConfig.java new file mode 100644 index 00000000..f07fd8e0 --- /dev/null +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/FeignConfig.java @@ -0,0 +1,24 @@ +package com.ai.qa.service.infrastructure.feign; + +import feign.Logger; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Feign客户端配置类 + * 用于配置Feign客户端的全局行为 + */ +@Configuration +public class FeignConfig { + + /** + * 配置Feign客户端的日志级别 + * FULL级别会记录请求和响应的头信息、正文和元数据 + * + * @return Logger.Level.FULL 最详细的日志级别 + */ + @Bean + Logger.Level feignLoggerLevel() { + return Logger.Level.FULL; + } +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/UserClient.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/UserClient.java index 8faae898..6966c940 100644 --- a/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/UserClient.java +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/UserClient.java @@ -5,27 +5,50 @@ import org.springframework.web.bind.annotation.PathVariable; /** - * 调用 user-service 的 Feign 客户端 + * 用户服务Feign客户端接口 + * 用于调用user-service微服务的REST API + * + * @FeignClient 注解说明: + * - name: 指定要调用的服务在注册中心的服务名称 + * - fallback: 指定服务降级处理类,当服务不可用时执行降级逻辑 */ -// name/value 属性值必须与目标服务在 Nacos 上注册的服务名完全一致! -@FeignClient(name = "user-service") +@FeignClient(name = "user-service", fallback = UserClientFallback.class) public interface UserClient { /** - * 根据用户ID获取用户信息 + * 根据用户ID获取用户完整信息 * * @param userId 用户ID - * @return 用户信息String - * - * 注意: - * 1. @GetMapping 里的路径必须与 user-service 中 Controller 方法的完整路径匹配。 - * 2. 方法签名 (方法名、参数) 可以自定义,但 @PathVariable, @RequestParam 等注解必须和远程接口保持一致。 + * @return 用户信息的JSON字符串 */ - @GetMapping("/api/user/{userId}") // <-- 这个路径要和 user-service 的接口完全匹配 + @GetMapping("/api/user/{userId}") String getUserById(@PathVariable("userId") Long userId); - // 你可以在这里定义 user-service 暴露的其他任何接口 - // 例如: - // @PostMapping("/api/user/internal/check-status") - // StatusDTO checkUserStatus(@RequestBody CheckRequest request); + /** + * 根据用户名获取用户完整信息 + * + * @param username 用户名 + * @return 用户信息的JSON字符串 + */ + @GetMapping("/api/user/username/{username}") + String getUserByUsername(@PathVariable("username") String username); + + /** + * 获取用户状态信息 + * + * @param userId 用户ID + * @return 用户状态信息的JSON字符串 + */ + @GetMapping("/api/user/{userId}/status") + String getUserStatus(@PathVariable("userId") Long userId); + + /** + * 获取用户基本信息 + * + * @param userId 用户ID + * @return 用户基本信息的JSON字符串 + */ + @GetMapping("/api/user/{userId}/basic-info") + String getUserBasicInfo(@PathVariable("userId") Long userId); + } \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/UserClientFallback.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/UserClientFallback.java new file mode 100644 index 00000000..488fe1ee --- /dev/null +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/feign/UserClientFallback.java @@ -0,0 +1,50 @@ +package com.ai.qa.service.infrastructure.feign; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * UserClient的降级处理类 + * 当user-service服务不可用时,自动执行此类中的方法返回降级结果 + * 避免服务雪崩,提高系统容错性 + */ +@Slf4j +@Component +public class UserClientFallback implements UserClient { + + /** + * 获取用户信息的降级处理 + */ + @Override + public String getUserById(Long userId) { + log.warn("UserService unavailable, fallback triggered for userId: {}", userId); + return "{\"error\": \"用户服务暂时不可用\", \"userId\": " + userId + "}"; + } + + /** + * 获取用户信息的降级处理 + */ + @Override + public String getUserByUsername(String username) { + log.warn("UserService unavailable, fallback triggered for username: {}", username); + return "{\"error\": \"用户服务暂时不可用\", \"userId\": " + username + "}"; + } + + /** + * 获取用户状态的降级处理 + */ + @Override + public String getUserStatus(Long userId) { + log.warn("UserService unavailable, fallback triggered for user status: {}", userId); + return "{\"status\": \"unknown\", \"userId\": " + userId + "}"; + } + + /** + * 获取用户基本信息的降级处理 + */ + @Override + public String getUserBasicInfo(Long userId) { + log.warn("UserService unavailable, fallback triggered for user basic info: {}", userId); + return "{\"name\": \"未知用户\", \"userId\": " + userId + "}"; + } +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/entities/QAHistoryPO.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/entities/QAHistoryPO.java index 910cba9a..0c9384dc 100644 --- a/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/entities/QAHistoryPO.java +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/entities/QAHistoryPO.java @@ -1,22 +1,91 @@ package com.ai.qa.service.infrastructure.persistence.entities; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; +import java.time.LocalDateTime; // 使用 javax.persistence 而不是 jakarta -import java.time.LocalDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.PrePersist; +import javax.persistence.PreUpdate; +import javax.persistence.Table; +import lombok.Data; + +/** + * QA历史记录持久化对象(Persistent Object) + * 对应数据库中的qa_history表 + * 使用JPA注解进行ORM映射 + */ +@Data @Entity -@Table(name= "qa_history") +@Table(name = "qa_history") public class QAHistoryPO { - private String id; + /** + * 主键ID,自增长 + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 用户ID + */ + @Column(name = "user_id") private String userId; + + /** + * 用户问题,最大长度2000字符 + */ + @Column(name = "question", length = 2000) private String question; + + /** + * AI回答,最大长度4000字符 + */ + @Column(name = "answer", length = 4000) private String answer; + + /** + * 问答时间戳 + */ + @Column(name = "timestamp") private LocalDateTime timestamp; + + /** + * 会话ID,用于关联同一会话中的多条问答记录 + */ + @Column(name = "session_id") private String sessionId; - private LocalDateTime create_time; - private LocalDateTime update_time; + /** + * 记录创建时间 + */ + @Column(name = "create_time") + private LocalDateTime createTime; + + /** + * 记录最后更新时间 + */ + @Column(name = "update_time") + private LocalDateTime updateTime; + + /** + * 持久化前的回调方法,自动设置创建时间和更新时间 + */ + @PrePersist + protected void onCreate() { + createTime = LocalDateTime.now(); + updateTime = LocalDateTime.now(); + } -} + /** + * 更新前的回调方法,自动更新更新时间 + */ + @PreUpdate + protected void onUpdate() { + updateTime = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapper.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapper.java new file mode 100644 index 00000000..7c96dd54 --- /dev/null +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapper.java @@ -0,0 +1,53 @@ +package com.ai.qa.service.infrastructure.persistence.mapper; + +import com.ai.qa.service.domain.model.QAHistory; +import com.ai.qa.service.infrastructure.persistence.entities.QAHistoryPO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +/** + * QA历史记录映射器 + * 用于领域对象QAHistory和持久化对象QAHistoryPO之间的转换 + * 使用MapStruct实现自动映射,避免手动编写转换代码 + */ +@Mapper(componentModel = "spring") +public interface QAHistoryMapper { + + /** + * MapStruct实例,用于非Spring环境下的手动调用 + */ + QAHistoryMapper INSTANCE = Mappers.getMapper(QAHistoryMapper.class); + + /** + * 将领域对象转换为持久化对象 + * + * @param domain 领域对象 + * @return 持久化对象 + */ + @Mapping(source = "id", target = "id") + @Mapping(source = "userId", target = "userId") + @Mapping(source = "question", target = "question") + @Mapping(source = "answer", target = "answer") + @Mapping(source = "timestamp", target = "timestamp") + @Mapping(source = "sessionId", target = "sessionId") + @Mapping(source = "createTime", target = "createTime") + @Mapping(source = "updateTime", target = "updateTime") + QAHistoryPO toPO(QAHistory domain); + + /** + * 将持久化对象转换为领域对象 + * + * @param po 持久化对象 + * @return 领域对象 + */ + @Mapping(source = "id", target = "id") + @Mapping(source = "userId", target = "userId") + @Mapping(source = "question", target = "question") + @Mapping(source = "answer", target = "answer") + @Mapping(source = "timestamp", target = "timestamp") + @Mapping(source = "sessionId", target = "sessionId") + @Mapping(source = "createTime", target = "createTime") + @Mapping(source = "updateTime", target = "updateTime") + QAHistory toDomain(QAHistoryPO po); +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/repositories/JpaQAHistoryRepository.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/repositories/JpaQAHistoryRepository.java index 0eaffcca..b1810ca8 100644 --- a/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/repositories/JpaQAHistoryRepository.java +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/repositories/JpaQAHistoryRepository.java @@ -1,12 +1,113 @@ package com.ai.qa.service.infrastructure.persistence.repositories; import com.ai.qa.service.infrastructure.persistence.entities.QAHistoryPO; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + +/** + * QA历史记录JPA数据访问接口 + * 继承JpaRepository获得基础的CRUD操作能力 + * 使用@Query注解定义自定义的JPQL查询语句 + * + * 注意:此接口位于基础设施层,负责与数据库的直接交互 + */ @Repository -public interface JpaQAHistoryRepository extends JpaRepository { +public interface JpaQAHistoryRepository extends JpaRepository { + + /** + * 根据主键ID查找QA历史记录 + * 此方法由JPA自动实现,无需编写SQL + * + * @param id 主键ID + * @return 包含QA历史记录的Optional对象 + */ + Optional findById(Long id); + + /** + * 根据用户ID查找所有QA历史记录,按时间降序排列 + * 使用JPQL自定义查询语句 + * + * @param userId 用户ID + * @return 该用户的所有QA历史记录列表,按时间倒序排列 + */ + @Query("SELECT q FROM QAHistoryPO q WHERE q.userId = :userId ORDER BY q.timestamp DESC") + List findByUserId(@Param("userId") String userId); + + /** + * 根据用户ID分页查找QA历史记录 + * 支持分页和排序,提高大数据量查询性能 + * + * @param userId 用户ID + * @param pageable 分页参数,包含页码、每页大小和排序信息 + * @return 分页后的QA历史记录列表 + */ + @Query("SELECT q FROM QAHistoryPO q WHERE q.userId = :userId") + List findByUserId(@Param("userId") String userId, Pageable pageable); + + /** + * 根据会话ID查找所有QA历史记录,按时间降序排列 + * 用于获取同一会话中的完整对话历史 + * + * @param sessionId 会话ID + * @return 该会话的所有QA历史记录列表,按时间倒序排列 + */ + @Query("SELECT q FROM QAHistoryPO q WHERE q.sessionId = :sessionId ORDER BY q.timestamp DESC") + List findBySessionId(@Param("sessionId") String sessionId); + + /** + * 根据用户ID和会话ID联合查找QA历史记录,按时间降序排列 + * 用于精确获取特定用户在特定会话中的问答记录 + * + * @param userId 用户ID + * @param sessionId 会话ID + * @return 符合条件的QA历史记录列表,按时间倒序排列 + */ + @Query("SELECT q FROM QAHistoryPO q WHERE q.userId = :userId AND q.sessionId = :sessionId ORDER BY q.timestamp DESC") + List findByUserIdAndSessionId(@Param("userId") String userId, @Param("sessionId") String sessionId); + + /** + * 统计指定用户的问答记录数量 + * 用于用户行为分析和统计报表 + * + * @param userId 用户ID + * @return 该用户的问答记录总数 + */ + @Query("SELECT COUNT(q) FROM QAHistoryPO q WHERE q.userId = :userId") + long countByUserId(@Param("userId") String userId); + + /** + * 根据会话ID统计问答记录数量 + * 用于会话长度监控和清理策略 + * + * @param sessionId 会话ID + * @return 该会话的问答记录总数 + */ + @Query("SELECT COUNT(q) FROM QAHistoryPO q WHERE q.sessionId = :sessionId") + long countBySessionId(@Param("sessionId") String sessionId); - QAHistoryPO findHistoryById(String userId); + /** + * 删除指定用户的全部问答记录 + * 用于用户数据清理或账号注销功能 + * + * @param userId 用户ID + * @return 删除的记录数量 + */ + @Query("DELETE FROM QAHistoryPO q WHERE q.userId = :userId") + long deleteByUserId(@Param("userId") String userId); -} + /** + * 删除指定会话的全部问答记录 + * 用于会话数据清理和维护 + * + * @param sessionId 会话ID + * @return 删除的记录数量 + */ + @Query("DELETE FROM QAHistoryPO q WHERE q.sessionId = :sessionId") + long deleteBySessionId(@Param("sessionId") String sessionId); +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/repositories/QAHistoryRepoImpl.java b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/repositories/QAHistoryRepoImpl.java index 96a4cc78..ac8492e8 100644 --- a/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/repositories/QAHistoryRepoImpl.java +++ b/backend-services/qa-service/src/main/java/com/ai/qa/service/infrastructure/persistence/repositories/QAHistoryRepoImpl.java @@ -2,34 +2,147 @@ import com.ai.qa.service.domain.model.QAHistory; import com.ai.qa.service.domain.repo.QAHistoryRepo; +import com.ai.qa.service.infrastructure.persistence.entities.QAHistoryPO; +import com.ai.qa.service.infrastructure.persistence.mapper.QAHistoryMapper; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; +/** + * QA历史记录仓库实现类 + * 实现领域层的QAHistoryRepo接口,提供具体的持久化操作 + * 使用JpaQAHistoryRepository进行数据库操作,使用Mapper进行对象转换 + */ +@Component @RequiredArgsConstructor public class QAHistoryRepoImpl implements QAHistoryRepo { private final JpaQAHistoryRepository jpaQAHistoryRepository; + private final QAHistoryMapper mapper; - private Mapper mapper; - + /** + * 保存QA历史记录 + * + * @param history 领域对象 + * @return 保存后的领域对象(包含生成的ID) + */ @Override - public void save(QAHistory history) { + public QAHistory save(QAHistory history) { QAHistoryPO qaHistoryPO = mapper.toPO(history); - jpaQAHistoryRepository.save(jpaQAHistoryRepository); + QAHistoryPO savedPO = jpaQAHistoryRepository.save(qaHistoryPO); + return mapper.toDomain(savedPO); } + /** + * 根据ID查找QA历史记录 + * + * @param id 记录ID(字符串格式) + * @return 包含QA历史记录的Optional对象 + */ @Override public Optional findHistoryById(String id) { - //ddd - QAHistoryPO qaHistoryPO = jpaQAHistoryRepository.findHistoryById(id); - return mapper.toDomain(qaHistoryPO); + try { + Long historyId = Long.parseLong(id); + Optional qaHistoryPO = jpaQAHistoryRepository.findById(historyId); + return qaHistoryPO.map(mapper::toDomain); + } catch (NumberFormatException e) { + // 记录ID格式错误,返回空Optional + return Optional.empty(); + } } + /** + * 根据会话ID查找QA历史记录 + * + * @param sessionId 会话ID + * @return 该会话的所有QA历史记录列表 + */ @Override public List findHistoryBySession(String sessionId) { - //ddd - return null; + List historyPOs = jpaQAHistoryRepository.findBySessionId(sessionId); + return historyPOs.stream() + .map(mapper::toDomain) + .collect(Collectors.toList()); + } + + /** + * 根据用户ID查找QA历史记录 + * + * @param userId 用户ID + * @return 该用户的所有QA历史记录列表 + */ + @Override + public List findHistoryByUserId(String userId) { + List historyPOs = jpaQAHistoryRepository.findByUserId(userId); + return historyPOs.stream() + .map(mapper::toDomain) + .collect(Collectors.toList()); + } + + /** + * 根据用户ID和会话ID查找QA历史记录 + * + * @param userId 用户ID + * @param sessionId 会话ID + * @return 符合条件的QA历史记录列表 + */ + @Override + public List findHistoryByUserIdAndSessionId(String userId, String sessionId) { + List historyPOs = jpaQAHistoryRepository.findByUserIdAndSessionId(userId, sessionId); + return historyPOs.stream() + .map(mapper::toDomain) + .collect(Collectors.toList()); + } + + /** + * 删除QA历史记录 + * + * @param id 要删除的记录ID + */ + @Override + public void deleteById(String id) { + try { + Long historyId = Long.parseLong(id); + jpaQAHistoryRepository.deleteById(historyId); + } catch (NumberFormatException e) { + // 记录日志:ID格式错误,忽略删除操作 + // log.warn("Invalid ID format for deletion: {}", id); + } + } + + /** + * 获取用户最近的QA历史记录 + * + * @param userId 用户ID + * @param limit 返回记录数量限制 + * @return 最近的QA历史记录列表 + */ + @Override + public List findRecentHistoryByUserId(String userId, int limit) { + // 创建分页请求,按时间戳降序排列 + PageRequest pageRequest = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "timestamp")); + + // 调用JPA仓库的分页查询方法 + List historyPOs = jpaQAHistoryRepository.findByUserId(userId, pageRequest); + + return historyPOs.stream() + .map(mapper::toDomain) + .collect(Collectors.toList()); + } + + /** + * 统计用户的问答数量 + * + * @param userId 用户ID + * @return 该用户的问答记录总数 + */ + @Override + public long countByUserId(String userId) { + return jpaQAHistoryRepository.countByUserId(userId); } -} +} \ No newline at end of file diff --git a/backend-services/qa-service/src/main/resources/application.yml b/backend-services/qa-service/src/main/resources/application.yml index 8686fb2f..e6c3bd0a 100644 --- a/backend-services/qa-service/src/main/resources/application.yml +++ b/backend-services/qa-service/src/main/resources/application.yml @@ -1,13 +1,88 @@ server: port: 8082 + spring: + config: + # import: "optional:file:D:/dev/ai-qa-system/.env[.properties]" + import: "optional:file:.env[.properties]" # 改为可选,当前目录 application: name: qa-service - cloud: - nacos: - server-addr: 54.219.180.170:8848 + # cloud: + # nacos: + # discovery: + # server-addr: 3.101.113.38:8848 + # #ip: 127.0.0.1 + # #port: 8082 + datasource: + url: jdbc:mysql://database:3306/ai_qa_system?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true + username: root + password: ai_qa_system + # url: jdbc:mysql://database:3306/ai_qa_system?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC + # username: root + # password: root1234 + hikari: + maximum-pool-size: 5 + minimum-idle: 2 + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect + format_sql: true + show-sql: true + autoconfigure: + exclude: + - org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration + +springdoc: + api-docs: + path: /v3/api-docs + enabled: true + swagger-ui: + path: /swagger-ui.html + enabled: true + operations-sorter: method + tags-sorter: alpha + +deepseek: + api: + base-url: https://api.deepseek.com/v1 + key: ${DEEPSEEK_API_KEY:sk-31581bd22e844e1d8387f8570a7f3fdb} + model: deepseek-chat + logging: + file: + name: application.log + path: logs level: - # 将你的FeignClient接口所在的包路径设置为DEBUG - # 假设 UserClient 在 com.ai.qa.qaservice.feign 包下 - com.ai.qa.qaservice.feign: DEBUG \ No newline at end of file + com.ai.qa.service: DEBUG + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + charset: + console: UTF-8 + file: UTF-8 + +management: + endpoints: + web: + exposure: + include: health,info + base-path: /actuator + endpoint: + health: + enabled: true + show-details: WHEN_AUTHORIZED + info: + enabled: true + metrics: + enabled: false + metrics: + enable: + process: false + system: false diff --git a/backend-services/qa-service/src/test/java/com/ai/qa/service/domain/model/QAHistoryTest.java b/backend-services/qa-service/src/test/java/com/ai/qa/service/domain/model/QAHistoryTest.java new file mode 100644 index 00000000..af3b9a95 --- /dev/null +++ b/backend-services/qa-service/src/test/java/com/ai/qa/service/domain/model/QAHistoryTest.java @@ -0,0 +1,9 @@ +package com.ai.qa.service.domain.model; + +/** + * QAHistory 单元测试类 + * 测试QAHistory领域模型的核心业务逻辑 + */ +public class QAHistoryTest { + +} \ No newline at end of file diff --git a/backend-services/qa-service/target/classes/application.yml b/backend-services/qa-service/target/classes/application.yml new file mode 100644 index 00000000..e6c3bd0a --- /dev/null +++ b/backend-services/qa-service/target/classes/application.yml @@ -0,0 +1,88 @@ +server: + port: 8082 + +spring: + config: + # import: "optional:file:D:/dev/ai-qa-system/.env[.properties]" + import: "optional:file:.env[.properties]" # 改为可选,当前目录 + application: + name: qa-service + # cloud: + # nacos: + # discovery: + # server-addr: 3.101.113.38:8848 + # #ip: 127.0.0.1 + # #port: 8082 + datasource: + url: jdbc:mysql://database:3306/ai_qa_system?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true + username: root + password: ai_qa_system + # url: jdbc:mysql://database:3306/ai_qa_system?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC + # username: root + # password: root1234 + hikari: + maximum-pool-size: 5 + minimum-idle: 2 + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect + format_sql: true + show-sql: true + autoconfigure: + exclude: + - org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration + +springdoc: + api-docs: + path: /v3/api-docs + enabled: true + swagger-ui: + path: /swagger-ui.html + enabled: true + operations-sorter: method + tags-sorter: alpha + +deepseek: + api: + base-url: https://api.deepseek.com/v1 + key: ${DEEPSEEK_API_KEY:sk-31581bd22e844e1d8387f8570a7f3fdb} + model: deepseek-chat + +logging: + file: + name: application.log + path: logs + level: + com.ai.qa.service: DEBUG + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + charset: + console: UTF-8 + file: UTF-8 + +management: + endpoints: + web: + exposure: + include: health,info + base-path: /actuator + endpoint: + health: + enabled: true + show-details: WHEN_AUTHORIZED + info: + enabled: true + metrics: + enabled: false + metrics: + enable: + process: false + system: false diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/QAServiceApplication.class b/backend-services/qa-service/target/classes/com/ai/qa/service/QAServiceApplication.class new file mode 100644 index 0000000000000000000000000000000000000000..d9fc044b4367c3212f44fe2bbb7b5cbea8996c17 GIT binary patch literal 886 zcma)4%We}f6g^IpPCA6Pq);fYiVaG$z&nH#sMH3!8lFL6!OAmnrY&%O4q-#>l=c!I+K4Tk%P$+=Mcqu_F3oTDfC}jo#6ip*b`Y8Daxl9kj5?uoEdQUzB+) z?OPFNlA#-!L}X)OmDiWXraM;!LofP2HUp}Z3#A$Md+(zatC6!xr-P$$h%49&&}HZw zoAP4j?Ncjq`N`N1JT}JhQDt1~v4g9)MxEG_faBg8?l2QYF{rB))xjr5JNeo1q0v*7 zmex1AR)@^q?VAubrMCBKkZIb_r8_s1SHjbF(o$eJGNn!Aneu}?sC%aeei-C?Q);Jj zIaY=8#kDrBdPf2|uH{^fM3l}D`HHk$4(OC-wML#;pVk5-nJFhc$&}QNPgIeZxwK#S zGwr*7`d1%Tegb*)4-hkxdMZ__m)U3jzQ`;FxCeS8`oTbhZuHCO3`pZ78Oc`v8`@th zcSxsq1D9w4okal2%cN~#8#@HzlOI-KK)cKO-?95{l^>D^6d6$L3ZspETqpYt+AaEq WBpNuV&YQSJa+^5bCV7YUUEmL@(eow% literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/api/controller/QAController.class b/backend-services/qa-service/target/classes/com/ai/qa/service/api/controller/QAController.class new file mode 100644 index 0000000000000000000000000000000000000000..36455498f52da55b4714aeed897978b0586ebcaa GIT binary patch literal 7775 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 RG^@JGYTiXPtMQf9{{_3TfhPa} literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/ApiResponse.class b/backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/ApiResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..b36b2dc0a6264a6d30e381a2370170219250e6c5 GIT binary patch literal 4491 zcmbVP-%}gc75*;T)h=Se3Jy4`-85;j4FV!J{gnVFaR_z@$aV}S&5yQO#1__wU94RZ zyT2bZo$*W`J9)`l{o;pqrgk&wOs9QGXZnA%e?{B$J9}5sVyQ%l$CA!H_v|^}Ip4YG zF8}!NCw~EO5x=r9pm4Sp?pIvDa@(y$UgOZOc@_7-uWZL*<ujyZ4L{hKSFrZIC~|kWZEU--d(=uhpw-~IzZ1A| zvq8Ju#s)R{1wZiPMTHk9yWJkENu5q@DwO+rmhr^cSYj3hZ$`r~Uy zyxcu#d7A0Z#R8LBq@QBdi+98A>u$r{_hPR>5LVQz)kuoMe6~ETvi0YmuCFX?%w>p3 zpI@Ama`xv$gS0Tg%JeRbcc$HqA_b%FnEOtMS<^}%oc`Rk7g|zU9})_ApeB17oW9WvVt?OT2@d_p=i#{&1Yzt7{cP6J zwR*U}74B88c!Ae&V{e&Vm9hI)1s( zevg>94h+rnNz;qq-EO*dqU~whP2fOguQHO55>0P76ktS>r)7^xlyz=%O)UmH{YTw=SmcpRs z#&7yD!3R<71~IvNy+>1Y`|8&8Z0f>#*lg6i%f8$V#mr^;oLrU)lf6%C#(TSci?4)X z9K{XyKo9k(fCx{8FB9DfRyJRy{rb%h>i)!YX zv{6KqM!Be_6r7cSvtX(2Mv)g;_!(bhx(d-NH4DE`c=~u#cB`%fp5+nDv2$`5kQV_; z1M(V>{PHD$yM;s^!U$zvLbTMH5uD)82edc%`(*h~D3+%m;$(T|A;!wH4{_=b)aOv* z_c(onGuFTaeV(PSb2y3f_zJaU{X9}?Ej z?`xEYb4ehJqRr1Bo?(J5z+X=Quh2rA5C0kJtxE}kmu6b{?%Q1f}`cq#v!~*|{$aT41V@ASkK56() zn`=^aWi+Yzc3bC@x~ohZ$)41x%|wyqTKxQgWBmryUDP4vv49pN;7fR!2T&jBOI?|6 zW;4wk=;JfhgQIg;TY;s$H>k^T z+|_V{l7TCHS6ZiJqDl@nDCK#HJ&T)^EHvnClaieodiTIrrGsXE`l4%#G@57pXgm>N({py?{8zm#%v4vv2 zRh=`FEwYr8LL7Inasup9~b0e(pQlE;rI4e&s$;yvb+2Aqxs{9D= z9e;q_aG^7b4i}Ez;EcaX#pmp9l1)=-XOirD-mmk%&-=0Y>)&U;0hmP9KuX|v)mtw~ zr}R*ke7nBsRPB=7a7rtIS32jfU6%{?!-nk#1~h?z2Xa%EYSO)5y0-klt_A{`2CZgR zD4T5yXFd0RMZgR#9nTdQOh^}lx)aK?zU})IEA#AqJGdFU8XO%-ukk)R=Q}rc3lSc3h&pZycIhO#%4iBMxcMzaqX*(^<}$$ zL(+w^^QyA8Bv!^__DuY{2z$9EhkZ!=* zT~0cSExUbnbCTl~n=$p%j_U+xLVT7KKG!W=#WjJRMXyn>+S862n7zrRa6*kdvp(;6 zfgjZ6M%0{s50>zWfm;Ik_EK=%O>fOEMfXMNOv`HE)gP%mKNa{knN6%6IlLs@m74A6 zXFYGNu~F$xD8cD4yzywm-bVdSLVueY#4e^}t+wa{cBMaqnh za5_@uW?gk9-@EVTn z(Vg8N+^u_6?w0#(=$>1;J9=@7iD#MQH-+)*;LY=Frai-|J{uOZPn--Htk!?atL4JBzrQ2JRH1D9^Xo+ z;vG^NE@B$*lF~7UkMJI;3~C&e_emM3;}AX|W#Sa?EWk3%zp=-F_y@xV()^et4@6`( zNj6)fL~u3rhU^BH?-ddDm99`#N&R2r>8+%6rlOLH8}DRTI=-K1x}~qh3G&pC-yEo% z)+WOO9UiExvOMNM;W3QjQ&jFLexa->K~9S$rYc$_Dwg$7#CnBBW=;=9hqo`mndrhe z+B6pU|M3i?)iIB=q*B~2R|vBz!7-NN94U?b5u7Kbhai82%nLF!f-D4q<>Li^+vk*G z3_))yGa;eNPr?y=*XH^WuaY#YVMVexQAur{sW$#F=qe( literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/QAHistoryDTO.class b/backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/QAHistoryDTO.class new file mode 100644 index 0000000000000000000000000000000000000000..df2156f230519129569899912733fcd614683114 GIT binary patch literal 6517 zcmd5=S#TUz6}?Zhbk9tWX0$qujkAq|7CQrmK+>{99C=fayvee$g>l-Wwl&shTAt}q zVgdw6*f+C>0AY_IxC*Eu22vCS9|Vf;e8CU=@y9pCV$SXNdKqi%nIEdC(&*iH@3}|s z-FNT1Z}i9iyz*-z+D+Fq>JxNd#a%7i)$%2~+;kf2)rwQL*Q(_u&n+L_bFkX<+{Wd- z#}8|i6jZ!uuiNFCUB6I1eD0!C@dWi%m!O}G46|-&`H#-j*Q?Fyxtg=5UUxm;v&mbj zxvS^gmGS|n?tsr(!g7++?vj%sL3bFGqO_pGY_;wjYOS7g8pmx=upe*Xgx#p}Tq%>@ zauu9ov%STiH;$DrsHhINtzN~IS8mG{d4{uf}Da*engSp?X@N0mh{2f8h#bc?^lJKBiU#dCH(yZ&QwAQ9>8_qVz zy1027r2K9%jy^C`$7^A_W;a2n(GLV|+YseWantA}L0fMRMHKwaM635l^lpDdtM*4UW`9H*_DA$we?*VPXo49n1boAo zr7;`9IEqghvcQ;{_!({b34}g?C4?99I>?3u&}Q0#nc%+Dh3L-Sh|spW5Z%)o5xQO% zqHVnqp=ov@8t;t=J+}+d_TGrlvbzwKdm}=}??SY*HzGV3x)43k8xh_cU5Fm+jmV-Y z)XmMAZGpUxQ+ZEGU+{+?dx`IZRjGd}$z-RZaWO3#;1NT2oT_t2x=f-~MLJ>O{|ea@$!?!Nl%z0&hl z8Pex{`n}y(U+$HjZ{U!=->2W#oqlJp^n9_0^aY=OfA`=#&?`Oni;%wP(;w(g|6s55 z+=fE>0iPaUWHx3_Vi*;<30>yOX8n{Zv4l{Y34hM*%Rtk`#R@zK4CjBZ6UZuR5GSe~8yrls&Z|SDSAYFkp zQ;&fbtPDU4R@TfgD62pjGaCb)v~mEQv#{V~v@ zRRCzwDw+ib6&0vp7Gt0@)&M|ftU+^tL4yi3U=GGWXRRTC&RWCf5QBylXviFnfoy98 zAln)>M;J7!KqKa807_oNjhNW*H29l5tKj|=SExcuIOq}a6ghY<^ifIF=sa`@nh@vd z0;D7z5Hqw4DMfQ)nyQe}R1+n-2uY)c*h(vqbbO1rK{ZGjJc|E8tB|sEo&H31NICiy zy+$r1gZ@k}(;B2a{gr-9mmu}yNppo7kP0GApP(kBqSyvr57K~`#Ohj*2E}8vpVlD_ zi6is~JppN0xOj;@328*Mu(QjMM(I7+|C4mLrvHojH4^`(QH>J#(IMU;#mxUq4Ej+8 zg_Goe5Ns?QdI(|Xc9-Ic;nTWRw@U@5h4?$zTUeHkUzYgB3D=vNX_Ct_kFp#rPVGwW z_KR;zacUW*rxz!$lD6YIz3*|>AGMNfFq6unb3sbeyp{4zs72pNH&cOW!Akq4bZA0_ zO0Sca=9^HQ%1aAOr!3t!>7fZFE4>!2jBi4DWP9!=P#y?S%pM+#^ z()OLE;TupSk+eb}Q8C=W?`Yn~iV;c$fB=(hS1DLJ28)n4YZX{)2S@l02IZn}aEZJ+ z+mvIblVjw2OS5v%-oia%xE(@~M?sw%<%E7^Eg#lF)1 ooW9!mfztk!HbGzW@5`^#H!$);<-7QNk6y%#H*%GJNIxR;zdeRxSO5S3 literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/SaveHistoryRequest.class b/backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/SaveHistoryRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..851d485a53091f5c2ca32d5df9597069a6feef12 GIT binary patch literal 3825 zcmbVOTXWk~5dLIKmTW7D8=9t0TWBGLTx$p+q;dkJ=`E1-lC&wbT&g%CQR7Idr3CuO z48t(+0#7jf0Qyp9V1O{wVVJ^0nc)xg2f)t&%6BCBlG@aU8Oz$U`|awS-EUX=?eE(^ z0holNA|_#|;I8LPJ9pj8c~)u5E?7Bp!_F=HZf?QcvQFEc@0M=NTh})&&sQN!$XqkG z%v{lQR&r;Tu2}_NLSmD?Gs_Y>XRF2qzhpZrc?nwJYr77CrsKV1l_VrR%k!ubPN|Z) zGVMCP^)5ptS1kWx6m553f1`N&<~JSRUbkkPE!(q~iq@3lxW4H#oF}$Wbk~>MwcIJo zu}Y?IEprRiR^C@oglIS111*(3@X1_OpUlFM0A`U9 zc04Ssf4={6id!4fF^mD0yI?ve1X?I_I?~Wnh~pU@{TL9sXLalmItkC|$cEDMI`)Us zQ5}y#mms-2=@dOng5vtCf^gvr#wgtv2@!CUkRS|lZ=@@yJm+gUf4MjUb z_gYN=sby{^#MG+V7JRd?HfL@`bkbH$Z&grHLcFhkMx^Bx9j7tFQ=OZinLEK_Q%;mD zZ?ni|P%Gz2_Exv9_LldV>aE#Py0`Yxn(u}U*vV#J)Nu)~O6XoU*DSVL!SpZLK6m1H zzUlZZtJ8o(^{o1TnublBn6Mq&e<4W3qDaK+I$pzNPJ;z^vsAE7+M+4D>u1mrF=0sV zW3KCae#zVjS?;A^qM%|)LT_~j+0K@`X63?6ga0$htVo!vPeETu`blZ% zV3`#{4p||j3M<5-%8E&;pN&4_a4t?4O^+xQ*CZUS%U7ces3>wUJse8vhUvl~c6Xf2 zQxFpamr_hjz~IiA7u-4Gf;$Hq?;Z5us35M5s{~i=ffmS7mMAL&-y!ogg%|{>1q7Uv zkMLg)0oaW$t|T#Engr@@EfB|3lR!PK1>$sT66io{fjAtS1UlGSpvUn9OY~r}4F->i z><$cmkIXle>a)EkL`2yZl@KLn;}NZJlp({#;pXAHTMaLmQwgsI;h$_CzNgjjf{>N) zS`hxJ=HU;t8eZ_c5n8N!th|k<%ZL#}A!NizWEFu)+(qY2 zv}KiSq6Rc-s057~T2>VxEdr@otp+q^BncWbQrV;cr6N!=o2mhg8)<^Z4LzF{AUy(6 zDud)(JX*YNGQ1xixx&?ZFpy`yy6_7o_#Y!{zvb6(p1uS43NKKSaSET{MM?_h@DX03 zlt2+{n4+Yj1PjL~Y4{W;F-<9n&xkrsDTQqgzY~QLkUde zP2i+um9lBfNCc)t#Y8rRUgL%um`J3^OASo>8<%}P+-7vfI6lNIODSg42RK71#zMV^ zvpjd4t+%8Wiz&r0!E8vxt(!!tr*W|J8L&D7v7O z0cDK5Qx(AyHLxo3L2U$9cY>?<28PH*t8=M(HMY@_X}ZwxC3BpK5%1g{QFu{gUZcFm zc#Yr2Mc`p?o}Q=}lK9T6=T^;Y{s&xXJp6XP=HqxH;P;z&i+7D%vv|GDYJ%03V6$P@ Gk^Kj{qM=&= literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/api/exception/ErrCode.class b/backend-services/qa-service/target/classes/com/ai/qa/service/api/exception/ErrCode.class new file mode 100644 index 0000000000000000000000000000000000000000..6b9dffc4e77bc80c4c617b1b017d5495034fb391 GIT binary patch literal 2690 zcma)--%}G;6vxj+h44d>D+pSDwYDg%HG2`dCcf+1L2b=oR* z7^f&R>eO1SQ%4PLM_5~{4p!~6)0e(!nhH|vFgOg zgT&p_@e8LDm(|HjpG}O6Fv_TRSn561PM598T4|yjL!t-?h!ciJ9;3{};Ma-seTi#l zCWfC*emcx3waV!#w^deJ9VU9oh~A21W=4(BHCSo<_D6|3H)i4-PLHq3S?|EAFB@^& z5H~Yw#J!3oC%*4Zj1G)F?2q5M5+595w8ZA{SX~Z_-Dh>VoGue>KzxawIG;Ubh!i6C z@$VjuKe`@2`|;$Nq3PVS>ny%%o7>}bHTq`OHPP$ZZi=*n&ztWBLld(($bbCKrHNrR z{`j)6y=921cUxUPn^7B^OFLN{il&N_&7Ei75PgGD_V}ftMBf8jB?KqVJRW;EI{wut zh7qGz+2(ZU1vfoiww@N?#{Jf|I4gClp^5UYoc|R=w;E}V{cv?+fxtDk5wL|25Xo3R*d z;4JVmfwRHO1PB zOTcdmydC_uz@^|F0`COx7T5x=5V#UtCGZ}wP2jy?yTCPIr@*yfm%wgty}(}Zet{do z?+APVd`Ms)YRa_l22VdKY=&j9iwYl5>s`I%hw;=d0O1O>;hAB)K97&=XP2T#JDhdl**74`$z+pr(PeoJ}s4%jEKJ7J%} z?t=XVb~o(ruol=qV9R0uq`cPWSR)zsZ&*%w?N4FXP~OkK!4^u~be|$uX*fo|N_>g# z--^*s63^EC!5IB0@s+xNF-G?!zE1bg#b`+4n{>Z7MuQSB*8LMP8j$#Q-H*hmSK>Q$ zKM}0THSZV$RlyD?ptG2E%5`oUlyY>iRbHnPK>tjbd_hR zJXhtbRlZ*31u8eIyhP=tDz~V7kIHLQ?pArD%6%$7rs5nHkXEBA%EHsm%E-ZXQ7tPc z7qgOwRg;(Pqy5Z5O{|U%Fb^GK4b;q<&@2y9D{H31s8TySOdTvh!51}VK{}4g6TOGO gd|LY#^`%f}`s`oQ>>s_4cNZ}P;{53Zo=J4_A8}HTNdN!< literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/api/exception/GlobalExceptionHandler.class b/backend-services/qa-service/target/classes/com/ai/qa/service/api/exception/GlobalExceptionHandler.class new file mode 100644 index 0000000000000000000000000000000000000000..98676e0b8d7865411660ba395744c728343a4799 GIT binary patch literal 3323 zcmcImTXz#x6#h*&vuwI}QJt55iO7=ZL1zNnm{)n`m4O`s`(BqJlb#nOVFGXS*wEnMU>A1c zT?4xW)+}qF^b~fZmx)`cGLMh&9JKOSL_JX#s+C#2Pz27!(Y^fnjatoD|Ba;8 zH<2B&B4|L8z*$E0D9bL1^Fb|K5ZD$*BJ4&l^QSqPE~b#h6gzlX&Z`O6$xD6B(j~5Z zEgem-?u|pPl|YlK2x{SS3Ri;k-e5V5h$HP84mstqDYzb!A_`9|E~n$7>-0AY$en*F zV)c>O6pO^N?Mci9j=x4HEIYpbISqk}EdLp|>gCm0D;UY!o6r1%!Eu4nrmdmbVBlEP z44d{`n^%UhLU6la;dPI@uJ*MjD^bDtNqmMc416xoyKJH5EV%Q^jKUkG!C9HtuD2NE z2kqcoZQzasMsvvT&ARSrB!BK2R==p zAh7A#FHfF5`Qhnz-#-85$LDwN{dxcXiJc;Wo6Nl0r{BJQ`tTu%0;vm*qr3@Q`o8iF zSOR+*R5XKUU|!(ue`e*qEb@gwSFB%S7lYr*k^|bf;suRQgFBON!FT)q2T1(PMGKOA z2A_d$bn%&r82DbpoxobGqmXW{Td*EM9j_&I1OFRuK;*G06nvNJ0)<0Q&~yC(woDDA zx2E5Egm+qTZ6Lkpz^~Z1w0TcNtA{WL9^q&Ue&V^-(2gFO3T#FKTc}|x=lM45$M(>! zoss;SUDSjOb#oM804I6tZNVtd2G&QE_enWTO1mIq0Avi~bs!T>fSd_|+^Yc@i36Ej zvONHD?lC?9mMF_;E;UE9oA&e)%^v>tG64HxXh!O2Mrh=j(Dp+OXaY(DnyZ+uqnT-f zW_E>WK75Ri-T;kBG=oHQh-eNI&5>84nO!-WHe6?fTJUj*BS^*@+<(IVT2gb|=fkH$ Uk+d{vCES9|1R16=<&Mt30ml)iYybcN literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/application/dto/QAHistoryQuery.class b/backend-services/qa-service/target/classes/com/ai/qa/service/application/dto/QAHistoryQuery.class new file mode 100644 index 0000000000000000000000000000000000000000..432e54eed5e0f76cd4f0179d748df51b2a760298 GIT binary patch literal 4132 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 O#|~l-9#ivY*ZddK$P!xs literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/domain/exception/QADomainException.class b/backend-services/qa-service/target/classes/com/ai/qa/service/domain/exception/QADomainException.class new file mode 100644 index 0000000000000000000000000000000000000000..29019a4e7cdc6c5305279904c979594aa7cb548c GIT binary patch literal 765 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 literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/domain/repo/QAHistoryRepo.class b/backend-services/qa-service/target/classes/com/ai/qa/service/domain/repo/QAHistoryRepo.class new file mode 100644 index 0000000000000000000000000000000000000000..824bc7031c04e6f433cfbb8ec847e31d6521dbe1 GIT binary patch literal 1244 zcmbW0+fLg+5QhJCb0BFsLtA=)B7qcCse8u_Pzf$7K&d#S<$hyt!yzRi?clQ7euvv%0@Yyv17mEKDJd$BwxsrP(5K8k<-Wq;! zc&s98!oek->ZmX*+=#y5zR*2>*1eIgWvE1=FBz^{?SCcschY1=Q4{*_8NijY<&AUiUOh=5`1iIQ#Vn^K&Y2%D0s5oHX?do54E{iHESZfkM= zu~8r&YA^$-nx)}OY!`S= z9uHLB%|)5GDdDTW3KXr_tZQOzj|M}g!ahzJnjI5|uKcEan&_p8?fsH$K#hUn1?@jD zQ(>s0M&}tiRcXzT>~{lAICx1{4qgqV*;Jat{770DN{gwqR3t5@(h62buGgun;eaG$ h&Np~FM#$={;oZpb{;}gjn(!mm$BrZs!$ykx=`Y3qUM>It literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/domain/service/QAService.class b/backend-services/qa-service/target/classes/com/ai/qa/service/domain/service/QAService.class new file mode 100644 index 0000000000000000000000000000000000000000..e5df613b52383bcae24322233d62eeb5cd17020e GIT binary patch literal 5507 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 UPXAxv&zvDD|6lMo`~!9W1NvpFi2wiq literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/DeepSeekClient.class b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/DeepSeekClient.class new file mode 100644 index 0000000000000000000000000000000000000000..e9c8e616b8e17f988c124d93d417daefed2fb6e8 GIT binary patch literal 7534 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

3gpwjE>$q6>UElIY#k4 zdS`F6^G$~%+IeH>qE)TUeMKA6Q_cw)m_nE3tJ-e7rTlMpAN4k#K zMXtb)^*z_ZeTU31=IT;iN1P9m=BGOJ{VgWVqla$jhj~&zEmt$dn2M9^dVgQEqa(py ziW_V@K)fPt+#|>(8E4!n80JMgZqX|9^U%57k>(471FsKWIbn8oo?~aU^XkxX^?rR%cewwkiHaGD^hdb2HM09~0_wDOh7k8fcAtrMo#Vv8le>vEXVJnL zvx74XUoprT!+P^Odh;m~U&f0u(iBd!%soyr4iK8~&69f90H<0sFkBgF+fOUGnhA~m z&4VU16qaB{^wj~=^N~~6_46mAofilD_nI>-le+eDjDG&iVE+k*p&a(wUM3_8Hko9| zlfFMDQIFm_&V?<1C*Wi9K(f-{qe&>e_1K-8XJp*Wa*Ekv4oOkt4>Tyuu*r6bNjLn% zdlf5nb2s3X;W>O!;;8h80&aDMN8T@G$1pVSE*Hr8%sfeVA3U%HRKLc(ldHtvz+|KP zG!~qRtUVg*meOdRse%qsAY3-FfTeX<9glAd%bd&9if39L>f<+O&xuC_jwdDtYqTkF zrQ)mhszIKf{q>>7#FLL@W5Nbu#6)>!PBQ#cfU`uDC|+-+M^lR?WQcsb_yW_YaLGGH z7q74yy{dIN>n!u}Qr0+vsX?3gBKN&_8J)}wn^?kQ$FCkr0+=@AA+S*LY9t-^$(usH z+4v@(c0N<&%?aD%*dE8HlYirbxQ89Q?zy{TB0IQOy1OF_S@K5E5N*f3n8G1b`8JPF z`7bj|^x$*XF#TP=q#&2SQU`F#L&g{IZ3bqtPfpvwXNv62yMbAG^S_Nb?_qk@++O6R zU|2#H(l{oczXh~o0Tpl=(x#Ym@dzGezg$DACh&Q-)5%;u1ipZU=3?SQsv-fnE!S8y zzZ;8t@I)_4QjxVH?>bg?W7P>{%g!~uSeJr44yo*7&&ED{8K{u&&#&pn=1n)C6sF~* zy@#r9)a9g^lfC`$Zz{-1yMdPq(|e#T&fxbs`S0QDIq6wVsRKyu#;!uU?8%gi?3j=dX)KR{DA-sq~3{U`FfaVozT?P-v&7YtBLooH48&Y~S{#3Yv{tuc&U zM4v6j1H8O@7>{wp6MTD;zr|RAWgJt2XHm+Fs;5wgmDqtY?80gs;XPkFd(UAlF<*y% zJZ+#`L;n-3IJ&oS$za4{-aK7qD}|D)@iyKe?^fV>T)}Tr8#gaEucCt-tfLN{=pyJl zFpHAkBNKPwL0sdvOs@7IuH)O3(#=(JDZ`X@gDq1=4`rA$dpWa@qcV|F@N)!hG~06V zC4Nnj_k5hyZ?tb3y!sh6k({AV2I#F@71nc2LDmOZztxN1NyW1TS>Ky)w(_$+q^o`q zd-=VB9{fS<y9`mQ3;B;!ewMqXq}%m)Qf*eK}G&;d~^a6@;|^wy|^QA0uv?G{@tK|)_-)#j?`ZK zJOv-1iP-&*F-=ncHkwYFga0=+I46yR3e!pU@0r6gax%VydvY?m@kzI!N$Hd*nVl_C zhpC+{(z~c9Fo!pJZOkK;#&aXSZahtTFJ+bj?%hrZ?M(G)=nHG>166zPsnI{9OPDEBcS~M7zBh}kXn?sTsRDni%}@X@mm_D zO*ep-bk62RJ(F~Pg-K>4bt#w9Zcv&bLyV_I#!R--4eGj(@iFeVA=UW_@@;5h>k~X= zHi~Un@=NNLVgsc#$)6a(*^~t5y*Phm!jyp}(ON#gTijETpDiYJi|mt_%GOk)HJ+^; zwobs7Ev9wx?Sa_agI(F;p)}^9O{w!Md&JCP!p<s-%y`&H2{KH1(D@iMX;kX?L`tTKx$I04^Nb|Ssvz=6%K7|e WXZun9eO@dyrVFu1EEbO;=l=i|_p|{3 literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/DeepSeekRestTemplateConfig.class b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/DeepSeekRestTemplateConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..39d92754885cf1fe8bdddb2054819c78e183da18 GIT binary patch literal 2646 zcmbtWSyL2O6#kmU8HS0-L{VJAxFu{(4~wjVA_gHrB|tF5nAi-Lp<$*Qx_eMct;&lj zQ)LpXl8O(hGD)RU$&-m%1?rgOJ-5t%#Z@~oRzIQkn(?@>S{3Z- zPZ^G$6g|d_ZtEin(P0>-qdB^1*z!oiOpch7>PcY;i+)CV#D64~&<)*br{VI7Aq9Dz z=BNnZ6%_ffS;6-8Zo0IXV_LI8Y(cStg1B%z_X-Y{_pYMtOlY>i29S##L6jgsl+L)$kUD$+H_hZ=>nM|8 zuSNEbC(nsO>0uUC`MG0CJaZ-&KeDsX80f4fc--r73*xpOv4eE993IM zlzLof25sffXo-~YVXuOAHwWuZjyulOGK>M8g(Es$Ox=|Ci|G{WDU+%I!q_1n2ZA_= z*A*1o!eNaWET=(MiHbx~W& zen~HU@%6H-LCQWYL^}{?JY~#(VayWj(LT$(G|RqVExOHR2uF2g zt$wsot2t8F5UH&@9I2`E;V3h z$t#s1MhGjCva??5!uI1mIdH5L$Kw0=z=v~d=CWrfi1YZ6`C!A)=9I7;X02%hKAsOC zlUaAjXJYc>0$nymo1%V`MZ+{) z0;ExfnNU!fFgbt6xxZ3V+Zc)RQ*)&<;!3X^u8-8#G)HQhC~PD$gEN2WetIeWt=HxY zcW*C#{L^38K3n?j&cdA=i`Q?qgfCxhxqMYN^^|PiX$8$M=fu0^TC-TnNMwTr2F#Qd z6J5HzD$17UQ1<-suFizzvRPUKC=-V63{I9l!%CV0!_$zH);UR)fI zeD0LD7gry5a`kh~=d%C-?h3i9;41$LLdpXK?;~`Vn;dNAQyu`X*KMvun&X9*Ii_-z zZ}ThXv3>oKBKJtpQ$Q(E$ius^n}+xB|IOUJ#&6jYx15PeQI-8KZ8eh>(yAS4cLrPfkaaVa1mQY0h`BuaC4wv)x}#%{gdz|SgN zkT~!I`lBjlH&KZM2RQ85Z#;VQ=FMJR|M&^u1s=z!3G63sEVYprTIS>@CLw9;$m=}# zA_;{jIUa>nw90dB_d{WW8PmYzhWo#^)&(7mv)q1HIiJe>+#8#+9b@|J{FzLg4fG{QeN!sm zkhX_QSi{{I>paq*om%MN9{bThR!U&~?^>EiuJ8%WIlbCYBF476~o zid6xl+l+uaSZ0zo-^!{LK1-s#8Bz2NtCx&x_|4Oqfgc5S5oh@zn;}>&Fu*$X{HtMblIe-Co65*UT6sv0bPm;Uo2~FRu!;0yZ(##55m|LVJ&6!Tt52DQqlzD&zfc1}SAKo`=89aaw6zoS7m)lS^HO zsTU$u;@GunX+|?>*yAUJkR^>M%OZ+T1at@g9n#(u+^q``aE${GQ03g#pw6cTpKfq{ zji2kEK{!~44X!oAa5EGgZ1My^jSXcaONG`-;MOHzdj@Qu18-fyc>5CY55_xJ0Pp@4 a2o<=;dspE8xylxc>G0`+gNN`ahV9>}z72f< literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/UserClientFallback.class b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/UserClientFallback.class new file mode 100644 index 0000000000000000000000000000000000000000..689fff078a87c405d50d2f4482a15012d7be7a55 GIT binary patch literal 2407 zcmbVNTTdHD6#fPSHpUd>!tus6(tFicspx#QZkf?KnNQ4(fZ8u#fmZJ9#43*55uyif_F zm7!;iZ*arnvTQ8n*F?b$qn+Wx3xhI*ELCQRXO&$x9IJG3&B&^9S=cFtGxuuJHLD^c zH%!OOTjGY4%H^)9q@ztNRn4o4aZ^ZPb5|6}Br;d9Oli8;2zYFKg`qXAiXw_`oC>3d zq5n9|0x!7A-ijfPml-_m<9GsW4&_QOsj zF&xJ0)DXuL8^d{wP-L6jmJFjoZ+!BAJ6;c^##f>k!$cV4M>@>Yk6{v14B-uK)x=VX zVI+vTj}QR$Y2wuN(=iyBq4HOGMWmH1aQA`fuF{v|a_Lg)kAV4>O%le??nO@y2B_-! zrL9HbuBzf)Zu6>eg-y7p4vM0)ENMT2w;AS6l5=gXvmU3Oo2FjZp6ET@XAPxg=<=y^ zbY*Hz1al16PePlZ91(!W+@hI#_JiV%S;)weir`&_^ohXp8Y4loM|lJxO%Tngnq3eJ zroKe|8u#c7n%3bm4EO_LrO9}h@b2om?sJ-JRk5k;is4XIgmSmmg^^a(btP%A(begh zSITu)-$0dGzedpJ+chl}PID$`nbFJ&fQkLvlOmHG;OS$%<5)vO(`2 z&CTtkZc44S_xQ)>Prmnfj;S)lGLq(YeXt5AjQeESkm_d13*!O9(0{AaHT5!$b97fi z^t914sK+$P4w?wY~*+N&V}HvmLzj8+w1CMGH>TTN{8>`|CTP6-{p6 z%pj>x+a{i3;J8Uw-9)RB0?y!7k_=}tM8KMJk^E~R+4n1Yl`BKvKa#t~Roa5X)$QrSE{HX&~2{vs@3d+-PJmh-K#gvb18J oB5FYz?~&B?exKw5J%eaPmez}b_fq})L*(c@N@vSh!ABVQ7Xkflod5s; literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/entities/QAHistoryPO.class b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/entities/QAHistoryPO.class new file mode 100644 index 0000000000000000000000000000000000000000..74a9431d134920b21d53a8fd7818f55fd8e58788 GIT binary patch literal 6343 zcmcgw`*Ryt75>(iw6e6eWv5P}CMjv^6hD%R253P}LS5%&uoEYC(k3k=Yin)GmUkU# zl{CDeyeZJ~YAH|(lm<$mw2)9b3{!rf%XTN*y-Tdy~ul@?aKK#@~n}N|eZzW^rGw1D0$thmQ&pDaAJ72U*ez82~my1qj z)hU+pCEsxwa$G;}=bcjK_~;S(yyB&)Vw0$Rc$}W^0 zg5sPs#|xL%#q*bDF0BUm@dF2s&Wz8THV`Xw_2ZmyZ)n2PelZ_R`PK%%KIRq5D=vRE z;@T^Y0aF;x%9`Wn%T7raMB0MmF8GV>fcbllY`1jLDRR{PtbqK!@U_Xdf6n zW#G03i<>GsQ|htH0>`|ZlR!W2G_ljbrr?RzPu0R3F+ft+yJ%p`z~D`n)ByuS7Pers zSi3B2!)62B6M5G;T3(rTiZeEKA&){Tamp^{WvrbMe=$#?j7|L4x)s5A?#ZA~2DUY> zf(jEFU2yzBAC-j#TGx`^q9r}06fr(uGN07yyJZc# z)}Mg)TIfUY$)L7L(jiV)1h7AMi@z$y!Qc6ua{-T$gvWwgjJ_Qu0x4OE%aFSk}q$NpO zS(M69OW`~jsV>04R!!h@Za3@`;SqFoRh&DAYu3VXcr7vKeAzB=j%((n+Skb7nFPyn z#==8*kV2WW-GhP_=esSu8E;9T9cL{(fCt5ATX;LNV$E6Xt%2oOxKCLN7RE3j(Y%F2 z%C~G`T=`Zkyi<0L;jL{ydM4DK=Px=%wiwZVf6!tAPhchkr!*gtr=Pyw^%CIZQmSXER)veM)9t8NM~#6-$L~S8iCki1R!8UB zD?^|U#uDf4v)nRrPa*OBp68c%Ls(T^s+7R<_`ZqD2KsB4l)QVvTXr&Px~k5H>^WhO z2k-*}Pld~@(Tebh?dA$jsei)rmdmRn>sESAVsIU)hd&f@H4ulU7_$q7>AdfZbjR?$ zIIb922vu+(pWA2b+O8BYRz>k*E5(adRlE$UikCr6@iM9@exEVOelhOy3N}`-OUyCx zqJf=tm26ZS6EE@Ju|AYaQH8|q?3=tZA&xdVF*!0h>ftEPZ#lgZ4wA6w;2EjINq%?p zlPU#B9+Enc3Ic5s=*5O{+`=>-mi5s-iEd<+C#H^uNH;OS6SF>WD+9dR)CaaQz?)8e z;5Pn31aH800%T>f!rS@1gP&3Q41RYHy@ETgVo-ezzeHCXcJpsnP?{+w9;bZ*chP65 z6lCxwjv}~w4fP)Sg!&#$9k1g|Fan$==a~>+T=N-n`VsL~Vq~iKXub?>nQC*-v1pZuM`jbEk=BTK;x-}L-x?7Q>?TB`tr79OZ$fmSH6mUYnh+gsjfl66 zCPag+5%IFpglM=mqIY3}>b+4%vRu($Y-so@?yV_3EF{^MqCTZ?u~cK2q&LdbAF|7j z;#f2KkyhzdW6|`T0sT~S`u(latM;VndjtC8&FM#5rC0q;)At4RCotWtI0ss#R}E6r zZwlyVn$P}ltMsbPYWj3Qf3o@P2V13A&05oM3FuEXrw_H@^@XWgzNX(6(DR+4zV>a8 zQPUd$S4F4L&(}=cT8Ga4MI2{aWJbZK)|70V(e~V-~@;Z7h!`hvW zT*G@Rh;!K}A?LEObW||0*U@_!o#|*g76Q#?O@d~#@w6$RxCWW&cnFlsb`X@yCej@O zN@!3=IuQcRXOjfYXRUNnK$Zs43V{~0odhjryV9Kk>e8UjbXN$plaRLJ%aRLJ(Gdj!<0K|Sf-3Mg`gouEB9A;XX7HC5qC-!Klrg~wLWXWWfdo|bLs zH+JAWeeD=BwxUQYg2T9u60ImE@ela4VkqD*DAO{D`6DjSisK1f#YI{jcoHw-5n2gc z!{fL_D~Vs?Gk7m83xCBj-bbqwe}{wj)8d;q-wQuLtJ{d-K75c?%Gk+kz=vq{7{k0$ ze3(`*KEid)Vv8C7C%Q}+|H4KS?fe&~xmlSV{{xdN4LYey!A9gAxL&Y);pKnw2~3nK z^{zkgRiCaL_=K{`Qg6+^8f*u?urc+9W1xKpeMyr3Ej6+?vM&(**3`%%B_B@>UxvB+ z8a{ST{O7U}38W)B5|@pjLbKUu5aJ#PlH%!TC6vp?f>5j);?7W6^I0kf5N9| zwNX}oz^7TrcHDtqP*x(+{dfkSr4{AH?NK~ND@Lg<;&Zf2e1RqZJgqohz#fi7q`%?4 z^Kn`U5tZ>WQrG1oN>LdGg~YGx{97BhiK7qE3ZR0CtaWKvECj0|Z}e)gxfWc*HwfsI zcwI^@t8q*xb~H+?<=!-`(-SvIuQ9BL&?L#5lhk;cfJjA6JkjuW_%NTd+`9M3ws{3# zlwHElEI&DZ=9O!a=8`fC{CX8n0qb4d_Ehifd~2`Yv5Zz;0Dg{dHa>mTn@Ob~MDVSk qYdlMV@)d`a-{$vsNW+)2@8UTQWF0T?^F#cIBl#b+A9Ic`BmE!cEU23R literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapper.class b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapper.class new file mode 100644 index 0000000000000000000000000000000000000000..5d6d6ba1c78ece7a9b4c52c2bf218c82d6736897 GIT binary patch literal 1334 zcmd6mUr$pp7{=d@u??7jh=3yhMTa8i15Ax%Vk8-C%EFCT<*dw-bJ|1OGvl?N$arJC zFkbtij8D(ZLCr8R@xmqT>F<5J_UY5NU%!3&Ohk`pr%VM#jZnt{m%%v?tT25Uia@GU z!>u!E=u#tsL>Mcr6AG!AC&1vv?sG)6d3%s?nTm|2&v>5)5m&vSb#f*`$EdP@c+}ZF zZ0s@GYyN|M>R9q&7)d4FV@4yjtz$;ThVF`SnxHFXsxn&Arsq`-?2Y+Plfdx zA;y(`BMi*;TCC%7g0U6WBJJmnhemKGI>-Xjq?_K)Bbs+vbMH!l4YYgVRql z^Dc4r54Ahgu3?s6ayvM6q9OGt^Wd5EH$VH^OW5(X#At4agTlbTNnCKE6{H;ayZ4SJ z2KJ+@P|$dmjBfZU!)SymR7Qhqply8puR6m%()br*A87hLxIi=LOIdc7u4NJb8fWP` za&zE!$jzfIyrRkztX`m-mKNz2x*V50F6FrFaXH5ok1IK@dR)zM&Er~*>mJv0-0-+T jn-DU}=r(z6cj)easI8WHx0UtxGUq1X+I_TbOAo#Ses)q> literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapperImpl.class b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapperImpl.class new file mode 100644 index 0000000000000000000000000000000000000000..c33c2b3bf1a3b1a84421ce9633603a2ed5373eb3 GIT binary patch literal 2156 zcmb_dZBr9h6n<`aQ&`l3rXW;lYcT=L3cl1f6dQs_Y7j6$Yu}gT5*FCqxVzEuU)e9~ zOgrP3e&|epl;d;q($KPWh8aKPo^$U#=ef^wbI;ko-~Rpvz*jizLqg$U*>gsvwS*UMqu*Je%ofgOa}<(6aBI5jtND_rsXm#d9=!Ko3fTj9dK_1rQK zEw^f}?d=+1i;-H$E|80f|y8Hvgw%6 z|G9SUhIVM{po6f!mhHo^Lhe*D;&A#ggn@n}G17+*6;j6m<24u<#bt$_styYkUUn*7 z?EL#h&#h(!dECH9xWcfY88)2yZ&Z3)4xcdaF+Nc^OSmmQM1j2cFUV5p+bz&*20p_i zg9+LWzH}7072AYP8AuEC0HHVPItXpg6>8H4W&~w`D4q2rG6u{xm7E*A(7r^^8klRJ z*1MIW>i`HuI2cX5$$yS)rDQ#;x2l@CCkP8jn7bHr~<@lhu?m zZ?JADMUE@!AfS>$McCap@U^fLz#|(;EE>oQ!9ix^ecd7y77e9O@xJDI|n&|7L$DC!3RfW+fb#@jTK&wv;SOmu;E99)4KF zvLSOk%b7{5XnUq!qyINL2^buE8;j>KNZ)7%;2h3##*YX-V3Z{PN!vxcKK&PZ)T=)* z^z=0@uFgzHxD;Vb?9~XLioG6TO6-jYH^ttH@VVG*gaxs=2uorgM)+pt6*0RSJmwnX z5V|o;PDyeY;W&ybxQt1RA%k(;Wd~c}$}+CvF{KLUi6*ZWXEMYp_Y~)gSmP{#jm8=K z^f7Id)OMZPrpRrE+HO!uhT2SyH>quw+UBV37Wv;fLEA1BHMLb4bzIvnwI#6E7;Ws+ J%Kv)+yaPQ7Bys=% literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/repositories/JpaQAHistoryRepository.class b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/repositories/JpaQAHistoryRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..5dedb88e675095ba1e12b82c49b85ebc94e5d0a4 GIT binary patch literal 2856 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 literal 0 HcmV?d00001 diff --git a/backend-services/qa-service/target/generated-sources/annotations/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapperImpl.java b/backend-services/qa-service/target/generated-sources/annotations/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapperImpl.java new file mode 100644 index 00000000..ba535243 --- /dev/null +++ b/backend-services/qa-service/target/generated-sources/annotations/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapperImpl.java @@ -0,0 +1,55 @@ +package com.ai.qa.service.infrastructure.persistence.mapper; + +import com.ai.qa.service.domain.model.QAHistory; +import com.ai.qa.service.infrastructure.persistence.entities.QAHistoryPO; +import javax.annotation.processing.Generated; +import org.springframework.stereotype.Component; + +@Generated( + value = "org.mapstruct.ap.MappingProcessor", + date = "2025-10-21T11:47:14+0800", + comments = "version: 1.5.3.Final, compiler: Eclipse JDT (IDE) 3.44.0.v20251001-1143, environment: Java 21.0.8 (Eclipse Adoptium)" +) +@Component +public class QAHistoryMapperImpl implements QAHistoryMapper { + + @Override + public QAHistoryPO toPO(QAHistory domain) { + if ( domain == null ) { + return null; + } + + QAHistoryPO qAHistoryPO = new QAHistoryPO(); + + qAHistoryPO.setId( domain.getId() ); + qAHistoryPO.setUserId( domain.getUserId() ); + qAHistoryPO.setQuestion( domain.getQuestion() ); + qAHistoryPO.setAnswer( domain.getAnswer() ); + qAHistoryPO.setTimestamp( domain.getTimestamp() ); + qAHistoryPO.setSessionId( domain.getSessionId() ); + qAHistoryPO.setCreateTime( domain.getCreateTime() ); + qAHistoryPO.setUpdateTime( domain.getUpdateTime() ); + + return qAHistoryPO; + } + + @Override + public QAHistory toDomain(QAHistoryPO po) { + if ( po == null ) { + return null; + } + + QAHistory qAHistory = new QAHistory(); + + qAHistory.setId( po.getId() ); + qAHistory.setUserId( po.getUserId() ); + qAHistory.setQuestion( po.getQuestion() ); + qAHistory.setAnswer( po.getAnswer() ); + qAHistory.setTimestamp( po.getTimestamp() ); + qAHistory.setSessionId( po.getSessionId() ); + qAHistory.setCreateTime( po.getCreateTime() ); + qAHistory.setUpdateTime( po.getUpdateTime() ); + + return qAHistory; + } +} diff --git a/backend-services/qa-service/target/test-classes/com/ai/qa/service/domain/model/QAHistoryTest.class b/backend-services/qa-service/target/test-classes/com/ai/qa/service/domain/model/QAHistoryTest.class new file mode 100644 index 0000000000000000000000000000000000000000..8711ebf445e79a68a265d25fd25b16dea093488d GIT binary patch literal 326 zcmb7<&q~8U5XQerlcvUMi*L}IigX{qKOjQ!5G=*g`)0eeQ<5xhwgn%{li z@Z!zBGHE#WM8 zMwct?x%I!rIBdBgO!NORT@$irxA(@*xV!v^c-q2Z0zs3fA(2`n(Nvu3Jdj+zWAG-0 Y2$^Wxh725w4!b;&cp{E)DjH$*1BpFMaR2}S literal 0 HcmV?d00001 diff --git a/backend-services/user-service/Dockerfile b/backend-services/user-service/Dockerfile new file mode 100644 index 00000000..2e7b54b5 --- /dev/null +++ b/backend-services/user-service/Dockerfile @@ -0,0 +1,24 @@ +# 使用官方OpenJDK运行时作为基础镜像 +FROM openjdk:17-jdk-slim + +# 安装curl用于健康检查 +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +# 设置工作目录 +WORKDIR /app + +# 复制Maven构建产物 +COPY target/*.jar app.jar + +# 暴露端口 +EXPOSE 8081 + +# 设置JVM参数 +ENV JAVA_OPTS="-Xmx512m -Xms256m" + +# 健康检查 +# HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \ +# CMD curl -f http://localhost:8081/health || exit 1 + +# 启动应用 +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] \ No newline at end of file diff --git a/backend-services/user-service/pom.xml b/backend-services/user-service/pom.xml index 5ce40e77..0e8e5d66 100644 --- a/backend-services/user-service/pom.xml +++ b/backend-services/user-service/pom.xml @@ -25,14 +25,10 @@ org.springframework.boot spring-boot-starter-data-jpa - + mysql mysql-connector-java @@ -43,6 +39,42 @@ lombok true + + + org.springframework.boot + spring-boot-starter-security + + + + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + + + + + org.springdoc + springdoc-openapi-ui + 1.7.0 + + + + org.springframework.boot + spring-boot-starter-actuator + diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/UserServiceApplication.java b/backend-services/user-service/src/main/java/com/ai/qa/user/UserServiceApplication.java index 675cae84..ac1965ef 100644 --- a/backend-services/user-service/src/main/java/com/ai/qa/user/UserServiceApplication.java +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/UserServiceApplication.java @@ -2,12 +2,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.client.discovery.EnableDiscoveryClient; -import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication -@EnableDiscoveryClient // (在新版中可选,但建议保留) -@EnableFeignClients public class UserServiceApplication { public static void main(String[] args) { SpringApplication.run(UserServiceApplication.class, args); diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/api/controller/UserController.java b/backend-services/user-service/src/main/java/com/ai/qa/user/api/controller/UserController.java index 16275a23..dc8579d5 100644 --- a/backend-services/user-service/src/main/java/com/ai/qa/user/api/controller/UserController.java +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/api/controller/UserController.java @@ -1,69 +1,210 @@ package com.ai.qa.user.api.controller; -import com.ai.qa.user.api.dto.ApiResponse; -import com.ai.qa.user.api.dto.AuthRequest; -import com.ai.qa.user.api.dto.AuthResponse; -import com.ai.qa.user.application.dto.UpdateNicknameRequest; -import com.ai.qa.user.application.service.UserApplicationService; -import com.ai.qa.user.domain.model.User; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -/*** - * 为什么user-service必须也要自己做安全限制? - * 1。零信任网络 (Zero Trust Network):在微服务架构中,你必须假设内部网络是不安全的。不能因为一个请求来自API Gateway就完全信任它。万一有其他内部服务被攻破,它可能会伪造请求直接调用user-service,绕过Gateway。如果user-service没有自己的安全防线,它就会被完全暴露。 - * 2。职责分离 (Separation of Concerns):Gateway的核心职责是路由、限流、熔断和边缘认证。而user-service的核心职责是处理用户相关的业务逻辑,业务逻辑与谁能执行它是密不可分的。授权逻辑是业务逻辑的一部分,必须放在离业务最近的地方。 - * 3。细粒度授权 (Fine-Grained Authorization):Gateway通常只做粗粒度的授权,比如“USER角色的用户可以访问/api/users/**这个路径”。但它无法知道更精细的业务规则,例如: - * GET /api/users/{userId}: 用户123是否有权查看用户456的资料? - * PUT /api/users/{userId}: 只有用户自己或者管理员才能修改用户信息。 - * 这些判断必须由user-service结合自身的业务逻辑和数据来完成。 +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.ai.qa.user.api.dto.request.LoginRequest; +import com.ai.qa.user.api.dto.request.RegisterRequest; +import com.ai.qa.user.api.dto.request.UpdatePasswordRequest; +import com.ai.qa.user.api.dto.response.LoginResponse; +import com.ai.qa.user.api.dto.response.RegisterResponse; +import com.ai.qa.user.api.dto.response.UpdatePasswordResponse; +import com.ai.qa.user.api.dto.response.UserResponse; +import com.ai.qa.user.application.service.UserService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * 用户控制器 + * 处理用户相关的HTTP请求,包括登录、注册等接口 + * 作为RESTful API的入口点,负责请求转发和响应处理 + * */ +@Tag(name = "用户管理", description = "用户登录、注册等身份认证相关接口") @RestController @RequestMapping("/api/user") +@RequiredArgsConstructor +@Slf4j public class UserController { - private final UserApplicationService userApplicationService; + private final UserService userService; + + /** + * 用户登录接口 + * 接收登录请求,调用服务层进行身份验证,返回登录结果 + * + * @param loginRequest 登录请求参数,包含用户名和密码 + * @return ResponseEntity 包含登录结果的响应实体 + * @apiNote POST /api/users/login + * @example + * { + * "username": "testuser", + * "password": "password123" + * } + */ + @Operation(summary = "用户登录", description = "通过用户名和密码进行身份验证") + @io.swagger.v3.oas.annotations.responses.ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "登录成功"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "用户名或密码错误"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "用户不存在") + }) + @PostMapping("/login") + public ResponseEntity login( + @Parameter(description = "登录请求参数", required = true) + @RequestBody LoginRequest loginRequest + ) { + log.info("Start login(), username:{}", loginRequest.getUsername()); - @Autowired - public UserController(UserApplicationService userApplicationService) { - this.userApplicationService = userApplicationService; + // 调用用户服务处理登录逻辑 + LoginResponse response = userService.login(loginRequest); + + log.info("End login(), username:{}", loginRequest.getUsername()); + + // 根据登录成功与否返回相应的HTTP状态码 + // 成功返回200 OK,失败时业务异常已在服务层抛出,不会执行到此处 + return ResponseEntity.ok(response); } /** - * 更新用户昵称的API端点 + * 用户注册接口 + * 接收注册请求,调用服务层创建新用户,返回注册结果 * - * @param userId 从URL路径中获取的用户ID - * @param request 包含新昵称的请求体 - * @return 返回更新后的用户信息和HTTP状态码200 (OK) + * @param registerRequest 注册请求参数,包含用户名、昵称、密码等信息 + * @return ResponseEntity 包含注册结果的响应实体 + * @apiNote POST /api/users/register + * @example + * { + * "username": "newuser", + * "nickname": "新用户", + * "password": "password123", + * "confirmPassword": "password123" + * } */ - @PostMapping("/{userId}/nickname") - public ApiResponse updateNickname( - @PathVariable Long userId, - @RequestBody UpdateNicknameRequest request) { - //校验。。。 - - // 控制器只负责调用应用层,不处理业务逻辑 - User updatedUser = userApplicationService.updateNickname(userId, request.getNickname()); - // 为了安全,最佳实践是返回一个DTO而不是直接返回领域实体,这里为了简化直接返回 - return ApiResponse.success(updatedUser); + @Operation(summary = "用户注册", description = "创建新的用户账户") + @io.swagger.v3.oas.annotations.responses.ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "201", description = "注册成功"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "用户名已存在或密码不匹配"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "用户已存在") + }) + @PostMapping("/register") + public ResponseEntity register( + @Parameter(description = "注册请求参数", required = true) + @RequestBody RegisterRequest registerRequest + ) { + log.info("Start register(), username:{}", registerRequest.getUsername()); + + // 调用用户服务处理注册逻辑 + RegisterResponse response = userService.register(registerRequest); + + log.info("Start register(), username:{}", registerRequest.getUsername()); + + // 注册成功返回201 Created状态码 + // 失败时业务异常已在服务层抛出,不会执行到此处 + return ResponseEntity.status(201).body(response); } - @PostMapping("/login") - public AuthResponse login(@RequestBody AuthRequest request) { - System.out.println("测试login"); - return new AuthResponse("token"); + + /** + * 根据用户ID查询用户信息 + * 获取指定用户ID的完整用户信息 + * + * @param id 用户ID + * @return ResponseEntity 包含用户信息的响应实体 + * @apiNote GET /api/user/{id} + * @example GET /api/user/1 + */ + @Operation(summary = "根据ID查询用户", description = "通过用户ID获取用户详细信息") + @io.swagger.v3.oas.annotations.responses.ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "用户不存在") + }) + @GetMapping("/{id}") + public ResponseEntity getUserById( + @Parameter(description = "用户ID", required = true, example = "1") + @PathVariable Long id + ) { + log.info("Start getUserById(), id:{}", id); + + // 调用用户服务查询用户信息 + UserResponse response = userService.getUserById(id); + + log.info("End getUserById(), id:{}, username:{}", id, response.getUsername()); + + return ResponseEntity.ok(response); } - @PostMapping("/register") - public AuthResponse register(@RequestBody AuthRequest request) { - System.out.println("测试register"); - return new AuthResponse("register"); + /** + * 根据用户名查询用户信息 + * 获取指定用户名的完整用户信息 + * + * @param username 用户名 + * @return ResponseEntity 包含用户信息的响应实体 + * @apiNote GET /api/user/username/{username} + * @example GET /api/user/username/testuser + */ + @Operation(summary = "根据用户名查询用户", description = "通过用户名获取用户详细信息") + @io.swagger.v3.oas.annotations.responses.ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "用户不存在") + }) + @GetMapping("/username/{username}") + public ResponseEntity getUserByUsername( + @Parameter(description = "用户名", required = true, example = "testuser") + @PathVariable String username + ) { + log.info("Start getUserByUsername(), username:{}", username); + + // 调用用户服务查询用户信息 + UserResponse response = userService.findByUsername(username); + + log.info("End getUserByUsername(), username:{}, userId:{}", username, response.getUserId()); + + return ResponseEntity.ok(response); } - @GetMapping("/{userId}") - public String getUserById(@PathVariable("userId") Long userId) { - System.out.println("测试userid"); - return "userid:"+userId; + /** + * 修改用户密码接口 + * 接收密码修改请求,调用服务层进行密码更新,返回操作结果 + * + * @param updatePasswordRequest 修改密码请求参数 + * @return ResponseEntity 包含修改结果的响应实体 + * @apiNote POST /api/users/updatePassword + * @example + * { + * "userId": 12345, + * "oldPassword": "oldPassword123", + * "newPassword": "newPassword456", + * "confirmNewPassword": "newPassword456" + * } + */ + @Operation(summary = "修改密码", description = "验证旧密码后更新用户密码") + @io.swagger.v3.oas.annotations.responses.ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "密码修改成功"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "旧密码错误或新密码不匹配"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "未授权访问"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "用户不存在") + }) + @PostMapping("/updatePassword") + public ResponseEntity updatePassword( + @Parameter(description = "修改密码请求参数", required = true) + @RequestBody UpdatePasswordRequest updatePasswordRequest + ) { + log.info("Start updatePassword(), username:{}", updatePasswordRequest.getUsername()); + + // 调用用户服务处理密码修改逻辑 + UpdatePasswordResponse response = userService.updatePassword(updatePasswordRequest); + + log.info("End updatePassword(), username:{}", updatePasswordRequest.getUsername()); + + // 密码修改成功返回200 OK状态码 + return ResponseEntity.ok(response); } -} +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/Response.java b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/Response.java new file mode 100644 index 00000000..f2244779 --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/Response.java @@ -0,0 +1,4 @@ +package com.ai.qa.user.api.dto; + +public class Response { +} diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/request/LoginRequest.java b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/request/LoginRequest.java new file mode 100644 index 00000000..d2e31d2c --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/request/LoginRequest.java @@ -0,0 +1,31 @@ +package com.ai.qa.user.api.dto.request; + +import lombok.Data; + +/** + * 用户登录请求数据传输对象 + * 用于接收前端传递的用户登录信息 + * + */ +@Data +public class LoginRequest { + + /** + * 用户名 + * 用户的唯一标识,用于登录认证 + * 必填字段,不能为空 + * + * @apiNote 示例值: "admin" 或 "testuser" + */ + private String username; + + /** + * 密码 + * 用户的登录密码,需要进行加密传输 + * 必填字段,不能为空 + * + * @apiNote 示例值: "password123" + * @security 建议在前端进行初步的密码强度验证 + */ + private String password; +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/request/RegisterRequest.java b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/request/RegisterRequest.java new file mode 100644 index 00000000..34830025 --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/request/RegisterRequest.java @@ -0,0 +1,61 @@ +package com.ai.qa.user.api.dto.request; + +import lombok.Data; + +/** + * 用户注册请求数据传输对象 + * 用于接收前端传递的用户注册信息 + * 包含用户基本信息及密码确认字段 + * + */ +@Data +public class RegisterRequest { + + /** + * 用户名 + * 用户的唯一登录标识,需要保证唯一性 + * 必填字段,长度建议在3-20个字符之间 + * + * @apiNote 示例值: "john_doe" + */ + private String username; + + /** + * 用户昵称 + * 用户的显示名称,可以重复 + * 可选字段,长度建议在2-30个字符之间 + * + * @apiNote 示例值: "John Doe" + */ + private String nickname; + + /** + * 用户邮箱 + * 用户的电子邮箱地址,用于接收通知和重置密码 + * 要求符合邮箱格式规范,通常需要唯一性约束 + * 可用于替代用户名进行登录 + * + * @apiNote 示例值: "user@example.com", "admin@company.com" + */ + private String email; + + /** + * 密码 + * 用户的登录密码,需要进行加密存储 + * 必填字段,建议长度至少6个字符 + * + * @apiNote 示例值: "Password123" + * @security 建议包含字母、数字和特殊字符的组合 + */ + private String password; + + /** + * 确认密码 + * 用于验证用户输入的密码一致性 + * 必填字段,必须与password字段值相同 + * + * @apiNote 示例值: "Password123" + * @validation 必须与password字段完全匹配 + */ + private String confirmPassword; +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/request/UpdatePasswordRequest.java b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/request/UpdatePasswordRequest.java new file mode 100644 index 00000000..01fa0ea1 --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/request/UpdatePasswordRequest.java @@ -0,0 +1,50 @@ +package com.ai.qa.user.api.dto.request; + +import lombok.Data; + +/** + * 修改密码请求数据传输对象 + * 用于接收前端传递的密码修改信息 + * + */ +@Data +public class UpdatePasswordRequest { + + /** + * 用户名 + * 当前登录用户的用户名,用于身份验证 + * 必填字段,用于验证用户身份 + * + * @apiNote 示例值: "john_doe" + */ + private String username; + + /** + * 旧密码 + * 用户当前的登录密码,用于身份验证 + * 必填字段,不能为空 + * + * @apiNote 示例值: "oldPassword123" + */ + private String oldPassword; + + /** + * 新密码 + * 用户想要设置的新密码 + * 必填字段,建议长度至少6个字符 + * + * @apiNote 示例值: "newPassword456" + * @security 建议包含字母、数字和特殊字符的组合 + */ + private String newPassword; + + /** + * 确认新密码 + * 用于验证用户输入的新密码一致性 + * 必填字段,必须与newPassword字段值相同 + * + * @apiNote 示例值: "newPassword456" + * @validation 必须与newPassword字段完全匹配 + */ + private String confirmNewPassword; +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/BaseResponse.java b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/BaseResponse.java new file mode 100644 index 00000000..436d51c1 --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/BaseResponse.java @@ -0,0 +1,113 @@ +package com.ai.qa.user.api.dto.response; + +import com.ai.qa.user.api.exception.ErrCode; + +import lombok.Data; + +/** + * 基础响应数据传输对象 + * 所有API响应的基类,提供统一的响应格式和结构 + * 包含操作结果、消息和错误代码等通用字段 + * + */ +@Data +public class BaseResponse { + + /** + * 操作是否成功 + * 标识本次请求的处理结果状态 + * true: 操作成功,false: 操作失败 + * + * @apiNote 示例值: true + */ + private Boolean success; + + /** + * 响应消息 + * 对操作结果的文字描述,可用于前端显示 + * 成功时显示成功信息,失败时显示错误原因 + * + * @apiNote 示例值: "操作成功" 或 "用户不存在" + */ + private String message; + + /** + * 错误代码 + * 数字化的操作结果标识,便于程序处理 + * 成功时通常为200,失败时为具体的错误代码 + * + * @apiNote 示例值: 200 或 1001 + * @see ErrCode + */ + private Integer errorCode; + + /** + * 创建成功响应对象的静态工厂方法 + * 使用预定义的成功消息和错误代码 + * + * @return BaseResponse 包含成功信息的响应对象 + * @apiNote 通常用于操作成功的场景 + */ + public static BaseResponse success() { + BaseResponse response = new BaseResponse(); + response.setSuccess(true); + response.setMessage(ErrCode.MSG_SUCCESS); + response.setErrorCode(ErrCode.SUCCESS); + return response; + } + + /** + * 创建错误响应对象的静态工厂方法 + * 支持自定义错误消息和错误代码 + * + * @param message 错误消息描述 + * @param errorCode 错误代码 + * @return BaseResponse 包含错误信息的响应对象 + * @apiNote 通常用于操作失败的场景 + */ + public static BaseResponse error(String message, Integer errorCode) { + BaseResponse response = new BaseResponse(); + response.setSuccess(false); + response.setMessage(message); + response.setErrorCode(errorCode); + return response; + } + + /** + * 创建错误响应对象的便捷方法(使用预定义错误常量) + * + * @param errCode 错误代码 + * @param errMessage 错误消息 + * @return BaseResponse 包含错误信息的响应对象 + */ + public static BaseResponse error(Integer errCode, String errMessage) { + return error(errMessage, errCode); + } + + /** + * 快速创建业务异常对应的错误响应 + * + * @param errCode 错误代码 + * @return BaseResponse 包含错误信息的响应对象 + */ + public static BaseResponse error(Integer errCode) { + String message = ""; + switch (errCode) { + case ErrCode.USER_NOT_FOUND: + message = ErrCode.MSG_USER_NOT_FOUND; + break; + case ErrCode.USER_ALREADY_EXISTS: + message = ErrCode.MSG_USER_ALREADY_EXISTS; + break; + case ErrCode.PASSWORD_INCORRECT: + message = ErrCode.MSG_PASSWORD_INCORRECT; + break; + case ErrCode.PASSWORD_MISMATCH: + message = ErrCode.MSG_PASSWORD_MISMATCH; + break; + default: + message = ErrCode.MSG_BAD_REQUEST; + } + return error(message, errCode); + } +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/LoginResponse.java b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/LoginResponse.java new file mode 100644 index 00000000..a2347070 --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/LoginResponse.java @@ -0,0 +1,68 @@ +package com.ai.qa.user.api.dto.response; + +import java.time.LocalDateTime; + +import lombok.Data; + +/** + * 用户登录响应数据传输对象 + * 继承BaseResponse包含基本响应信息,扩展登录相关的返回数据 + * 用于返回用户登录成功后的令牌和用户信息 + * + */ +@Data +public class LoginResponse extends BaseResponse { + + /** + * 访问令牌 + * JWT token,用于后续接口的身份认证和授权 + * 客户端需要在请求头中携带此token: Authorization: Bearer {token} + * + * @apiNote 示例值: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * @security 令牌有过期时间,需要定期刷新 + */ + private String token; + + /** + * 用户ID + * 用户的唯一标识,系统内部使用 + * + * @apiNote 示例值: 12345 + */ + private Long userId; + + /** + * 用户名 + * 用户的登录账号 + * + * @apiNote 示例值: "john_doe" + */ + private String username; + + /** + * 用户昵称 + * 用户的显示名称,用于界面展示 + * + * @apiNote 示例值: "John Doe" + */ + private String nickname; + + /** + * 用户邮箱 + * 用户的电子邮箱地址,用于接收通知和重置密码 + * 要求符合邮箱格式规范,通常需要唯一性约束 + * 可用于替代用户名进行登录 + * + * @apiNote 示例值: "user@example.com", "admin@company.com" + */ + private String email; + + /** + * 登录时间 + * 用户本次登录的成功时间 + * 使用ISO-8601格式的时间戳 + * + * @apiNote 示例值: "2024-01-15T10:30:00" + */ + private LocalDateTime loginTime; +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/RegisterResponse.java b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/RegisterResponse.java new file mode 100644 index 00000000..dcefd6b4 --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/RegisterResponse.java @@ -0,0 +1,61 @@ +package com.ai.qa.user.api.dto.response; + +import java.time.LocalDateTime; + +import lombok.Data; + +/** + * 用户注册响应数据传输对象 + * 继承BaseResponse包含基本响应信息,扩展注册相关的返回数据 + * 用于返回用户注册成功后的用户信息和注册时间 + * + */ +@Data +public class RegisterResponse extends BaseResponse { + + /** + * 用户ID + * 系统自动生成的用户唯一标识 + * 注册成功后分配的唯一ID,用于后续业务操作 + * + * @apiNote 示例值: 10001 + */ + private Long userId; + + /** + * 用户名 + * 用户注册时设置的登录账号 + * 具有唯一性,不可重复 + * + * @apiNote 示例值: "john_doe" + */ + private String username; + + /** + * 用户昵称 + * 用户注册时设置的显示名称 + * 用于界面展示,可以重复 + * + * @apiNote 示例值: "John Doe" 或 "小明" + */ + private String nickname; + + /** + * 用户邮箱 + * 用户的电子邮箱地址,用于接收通知和重置密码 + * 要求符合邮箱格式规范,通常需要唯一性约束 + * 可用于替代用户名进行登录 + * + * @apiNote 示例值: "user@example.com", "admin@company.com" + */ + private String email; + + /** + * 注册时间 + * 用户账号创建的成功时间 + * 使用ISO-8601格式的时间戳,记录账号创建的确切时间 + * + * @apiNote 示例值: "2024-01-15T10:30:00" + */ + private LocalDateTime registerTime; +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/UpdatePasswordResponse.java b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/UpdatePasswordResponse.java new file mode 100644 index 00000000..e44f1992 --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/UpdatePasswordResponse.java @@ -0,0 +1,39 @@ +package com.ai.qa.user.api.dto.response; + +import java.time.LocalDateTime; + +import lombok.Data; + +/** + * 修改密码响应数据传输对象 + * 用于返回密码修改操作的结果信息 + * + */ +@Data +public class UpdatePasswordResponse extends BaseResponse { + + /** + * 用户ID + * 密码被修改的用户唯一标识 + * + * @apiNote 示例值: 12345 + */ + private Long userId; + + /** + * 用户名 + * 用户的登录账号 + * + * @apiNote 示例值: "john_doe" + */ + private String username; + + /** + * 密码修改时间 + * 密码成功修改的时间戳 + * 使用ISO-8601格式的时间戳 + * + * @apiNote 示例值: "2024-01-15T14:30:00" + */ + private LocalDateTime updateTime; +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/UserResponse.java b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/UserResponse.java new file mode 100644 index 00000000..42d695d0 --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/api/dto/response/UserResponse.java @@ -0,0 +1,51 @@ +package com.ai.qa.user.api.dto.response; + +import java.time.LocalDateTime; + +import lombok.Data; + +/** + * 用户注册响应数据传输对象 + * 继承BaseResponse包含基本响应信息,扩展注册相关的返回数据 + * 用于返回用户注册成功后的用户信息和注册时间 + * + */ +@Data +public class UserResponse extends BaseResponse { + + /** + * 用户ID + * 系统自动生成的用户唯一标识 + * 注册成功后分配的唯一ID,用于后续业务操作 + * + * @apiNote 示例值: 10001 + */ + private Long userId; + + /** + * 用户名 + * 用户注册时设置的登录账号 + * 具有唯一性,不可重复 + * + * @apiNote 示例值: "john_doe" + */ + private String username; + + /** + * 用户昵称 + * 用户注册时设置的显示名称 + * 用于界面展示,可以重复 + * + * @apiNote 示例值: "John Doe" 或 "小明" + */ + private String nickname; + + /** + * 注册时间 + * 用户账号创建的成功时间 + * 使用ISO-8601格式的时间戳,记录账号创建的确切时间 + * + * @apiNote 示例值: "2024-01-15T10:30:00" + */ + private LocalDateTime registerTime; +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/api/exception/BusinessException.java b/backend-services/user-service/src/main/java/com/ai/qa/user/api/exception/BusinessException.java index 12a6f8ca..6b1e8e6e 100644 --- a/backend-services/user-service/src/main/java/com/ai/qa/user/api/exception/BusinessException.java +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/api/exception/BusinessException.java @@ -1,22 +1,159 @@ package com.ai.qa.user.api.exception; +import lombok.Getter; + +/** + * 业务异常类 + * 用于处理系统中的业务逻辑异常,包含错误代码和错误信息 + * + */ +@Getter public class BusinessException extends RuntimeException { - private final ErrorCode errorCode; + /** + * 错误代码 + * 用于标识具体的错误类型,便于前后端统一处理 + */ + private final Integer errorCode; + + /** + * 错误消息 + * 对错误的详细描述,可用于前端显示给用户 + */ + private final String errorMessage; - public BusinessException(ErrorCode errorCode) { - // 使用ErrorCode的默认消息 - super(errorCode.getMessage()); + /** + * 构造业务异常 + * + * @param errorCode 错误代码,参考ErrCode中定义的常量 + * @param errorMessage 错误消息描述 + */ + public BusinessException(Integer errorCode, String errorMessage) { + super(errorMessage); this.errorCode = errorCode; + this.errorMessage = errorMessage; } - public BusinessException(ErrorCode errorCode, String message) { - // 使用自定义消息 - super(message); + /** + * 构造业务异常(带原始异常) + * + * @param errorCode 错误代码,参考ErrCode中定义的常量 + * @param errorMessage 错误消息描述 + * @param cause 原始异常,用于异常链追踪 + */ + public BusinessException(Integer errorCode, String errorMessage, Throwable cause) { + super(errorMessage, cause); this.errorCode = errorCode; + this.errorMessage = errorMessage; + } + + // ========== 常用业务异常快捷方法 ========== // + + /** + * 用户不存在异常 + * + * @return BusinessException 用户不存在业务异常 + */ + public static BusinessException userNotFound() { + return new BusinessException(ErrCode.USER_NOT_FOUND, ErrCode.MSG_USER_NOT_FOUND); + } + + /** + * 用户已存在异常 + * 通常在用户注册时,用户名重复时抛出 + * + * @return BusinessException 用户已存在业务异常 + */ + public static BusinessException userAlreadyExists() { + return new BusinessException(ErrCode.USER_ALREADY_EXISTS, ErrCode.MSG_USER_ALREADY_EXISTS); + } + + /** + * 密码不正确异常 + * 用户登录时密码验证失败时抛出 + * + * @return BusinessException 密码不正确业务异常 + */ + public static BusinessException passwordIncorrect() { + return new BusinessException(ErrCode.PASSWORD_INCORRECT, ErrCode.MSG_PASSWORD_INCORRECT); + } + + /** + * 密码不匹配异常 + * 用户注册或修改密码时,两次输入的密码不一致时抛出 + * + * @return BusinessException 密码不匹配业务异常 + */ + public static BusinessException passwordMismatch() { + return new BusinessException(ErrCode.PASSWORD_MISMATCH, ErrCode.MSG_PASSWORD_MISMATCH); + } + + /** + * 无效token异常 + * JWT token验证失败时抛出 + * + * @return BusinessException 无效token业务异常 + */ + public static BusinessException invalidToken() { + return new BusinessException(ErrCode.INVALID_TOKEN, ErrCode.MSG_INVALID_TOKEN); + } + + /** + * 请求参数错误异常 + * 通用的参数验证失败异常 + * + * @param message 具体的错误消息 + * @return BusinessException 请求参数错误业务异常 + */ + public static BusinessException badRequest(String message) { + return new BusinessException(ErrCode.BAD_REQUEST, message); + } + + /** + * 自定义业务异常 + * 用于创建特定的业务异常 + * + * @param errorCode 错误代码 + * @param errorMessage 错误消息 + * @return BusinessException 自定义业务异常 + */ + public static BusinessException of(Integer errorCode, String errorMessage) { + return new BusinessException(errorCode, errorMessage); + } + + /** + * 自定义业务异常(带原始异常) + * + * @param errorCode 错误代码 + * @param errorMessage 错误消息 + * @param cause 原始异常 + * @return BusinessException 自定义业务异常 + */ + public static BusinessException of(Integer errorCode, String errorMessage, Throwable cause) { + return new BusinessException(errorCode, errorMessage, cause); + } + + /** + * 重写toString方法,便于日志输出 + * + * @return 异常信息的字符串表示 + */ + @Override + public String toString() { + return "BusinessException{" + + "errorCode=" + errorCode + + ", errorMessage='" + errorMessage + '\'' + + ", message='" + getMessage() + '\'' + + '}'; } - public ErrorCode getErrorCode() { - return errorCode; + /** + * 获取详细的异常信息 + * 包含错误代码和错误消息 + * + * @return 详细的异常信息字符串 + */ + public String getDetailMessage() { + return "ErrorCode: " + errorCode + ", Message: " + errorMessage; } } \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/api/exception/ErrCode.java b/backend-services/user-service/src/main/java/com/ai/qa/user/api/exception/ErrCode.java new file mode 100644 index 00000000..68342b5b --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/api/exception/ErrCode.java @@ -0,0 +1,48 @@ +package com.ai.qa.user.api.exception; + +/** + * 异常定数定义 + * + */ +public final class ErrCode { + + // 成功状态码 + public static final int SUCCESS = 200; + public static final int CREATED = 201; + public static final int NO_CONTENT = 204; + + // 客户端错误状态码 + public static final int BAD_REQUEST = 400; + public static final int UNAUTHORIZED = 401; + public static final int FORBIDDEN = 403; + public static final int NOT_FOUND = 404; + public static final int CONFLICT = 409; + + // 服务端错误状态码 + public static final int INTERNAL_SERVER_ERROR = 500; + public static final int SERVICE_UNAVAILABLE = 503; + + // 业务错误码(可在基础HTTP状态码上扩展) + public static final int USER_NOT_FOUND = 1001; + public static final int USER_ALREADY_EXISTS = 1002; + public static final int PASSWORD_INCORRECT = 1003; + public static final int PASSWORD_MISMATCH = 1004; + public static final int INVALID_TOKEN = 1005; + public static final int PASSWORD_UPDATE_FAILED = 1006; + + // 错误消息 + public static final String MSG_SUCCESS = "操作成功"; + public static final String MSG_CREATED = "创建成功"; + public static final String MSG_USER_NOT_FOUND = "用户不存在"; + public static final String MSG_USER_ALREADY_EXISTS = "用户已存在"; + public static final String MSG_PASSWORD_INCORRECT = "密码错误"; + public static final String MSG_PASSWORD_MISMATCH = "密码不匹配"; + public static final String MSG_INVALID_TOKEN = "无效的token"; + public static final String MSG_BAD_REQUEST = "请求参数错误"; + public static final String MSG_INTERNAL_ERROR = "服务器内部错误"; + public static final String MSG_PASSWORD_UPDATE_FAILED = "密码修改失败"; + + private ErrCode() { + // 防止实例化 + } +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/api/exception/GlobalExceptionHandler.java b/backend-services/user-service/src/main/java/com/ai/qa/user/api/exception/GlobalExceptionHandler.java index 4290ac4c..d9479268 100644 --- a/backend-services/user-service/src/main/java/com/ai/qa/user/api/exception/GlobalExceptionHandler.java +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/api/exception/GlobalExceptionHandler.java @@ -1,49 +1,162 @@ package com.ai.qa.user.api.exception; +import java.util.HashMap; +import java.util.Map; -import com.ai.qa.user.api.dto.ApiResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import jakarta.persistence.EntityNotFoundException; +import lombok.extern.slf4j.Slf4j; +/** + * 全局异常处理器 + * 统一处理控制器层抛出的各种异常,返回规范的错误响应格式 + * + */ +@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { - private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); - /** - * 处理自定义的业务异常 + * 处理业务异常(BusinessException) + * 捕获所有业务逻辑相关的异常,如用户不存在、密码错误等 + * + * @param ex 业务异常对象 + * @param request HTTP请求对象,用于获取请求信息 + * @return ResponseEntity 包含错误信息的响应实体 */ @ExceptionHandler(BusinessException.class) - public ResponseEntity> handleBusinessException(BusinessException ex) { - log.warn("业务异常: {}", ex.getMessage()); - ErrorCode errorCode = ex.getErrorCode(); - return new ResponseEntity<>(ApiResponse.failure(errorCode), errorCode.getHttpStatus()); + public ResponseEntity> handleBusinessException(BusinessException ex, + HttpServletRequest request) { + // 记录警告日志,包含错误代码、消息和请求路径 + log.warn("业务异常: 错误代码={}, 错误消息={}, 请求路径={}", + ex.getErrorCode(), ex.getErrorMessage(), request.getRequestURI()); + + // 构建错误响应 + Map errorResponse = buildErrorResponse(ex.getErrorCode(), ex.getErrorMessage()); + + // 根据错误代码确定HTTP状态码 + HttpStatus status = determineHttpStatus(ex.getErrorCode()); + + return ResponseEntity.status(status).body(errorResponse); } /** - * 处理JPA等持久化层抛出的“实体未找到”异常 - * 这是将基础设施层的异常转换为统一业务响应的好例子 + * 处理参数校验异常(MethodArgumentNotValidException) + * 捕获Spring Validation框架的参数校验失败异常 + * + * @param ex 参数校验异常对象 + * @param request HTTP请求对象 + * @return ResponseEntity 包含详细校验错误信息的响应实体 */ - @ExceptionHandler(EntityNotFoundException.class) - public ResponseEntity> handleEntityNotFoundException(EntityNotFoundException ex) { - log.warn("实体未找到: {}", ex.getMessage()); - ErrorCode errorCode = ErrorCode.USER_NOT_FOUND; // 映射到一个具体的业务错误码 - return new ResponseEntity<>(ApiResponse.failure(errorCode, ex.getMessage()), errorCode.getHttpStatus()); + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationException(MethodArgumentNotValidException ex, + HttpServletRequest request) { + // 记录警告日志 + log.warn("参数校验异常: {}, 请求路径: {}", ex.getMessage(), request.getRequestURI()); + + // 收集所有字段级别的错误信息 + Map fieldErrors = new HashMap<>(); + ex.getBindingResult().getAllErrors().forEach(error -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + fieldErrors.put(fieldName, errorMessage); + }); + + // 构建基础错误响应 + Map errorResponse = buildErrorResponse(ErrCode.BAD_REQUEST, ErrCode.MSG_BAD_REQUEST); + // 添加详细的字段错误信息 + errorResponse.put("details", fieldErrors); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); } /** - * 处理所有未被捕获的异常(兜底) + * 处理其他所有未捕获的异常(Exception) + * 作为兜底处理,捕获所有未被特定处理的异常 + * + * @param ex 异常对象 + * @param request HTTP请求对象 + * @return ResponseEntity 包含内部错误信息的响应实体 */ @ExceptionHandler(Exception.class) - public ResponseEntity> handleAllUncaughtException(Exception ex) { - // 对于未知的严重异常,需要记录详细的错误日志 - log.error("发生未知异常", ex); - ErrorCode errorCode = ErrorCode.INTERNAL_SERVER_ERROR; - return new ResponseEntity<>(ApiResponse.failure(errorCode), errorCode.getHttpStatus()); + public ResponseEntity> handleGlobalException(Exception ex, HttpServletRequest request) { + // 记录错误日志,包含堆栈信息 + log.error("系统异常: 异常消息={}, 请求路径={}", ex.getMessage(), request.getRequestURI(), ex); + + // 构建通用的内部错误响应 + Map errorResponse = buildErrorResponse( + ErrCode.INTERNAL_SERVER_ERROR, + ErrCode.MSG_INTERNAL_ERROR); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + } + + @ExceptionHandler(UsernameNotFoundException.class) + public ResponseEntity> handleUsernameNotFound(UsernameNotFoundException ex) { + Map error = buildErrorResponse(ErrCode.USER_NOT_FOUND, ex.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error); + } + + // @ExceptionHandler(InvalidJwtException.class) + // public ResponseEntity> + // handleInvalidJwt(InvalidJwtException ex) { + // Map error = buildErrorResponse(ErrCode.INVALID_TOKEN, + // ErrCode.MSG_INVALID_TOKEN); + // return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error); + // } + + /** + * 构建统一的错误响应格式 + * + * @param errorCode 错误代码 + * @param errorMessage 错误消息 + * @return Map 错误响应数据 + */ + private Map buildErrorResponse(Integer errorCode, String errorMessage) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("errorCode", errorCode); + response.put("errorMessage", errorMessage); + response.put("timestamp", System.currentTimeMillis()); + return response; + } + + /** + * 根据业务错误代码确定对应的HTTP状态码 + * 将业务错误代码映射到合适的HTTP状态码 + * + * @param errorCode 业务错误代码 + * @return HttpStatus 对应的HTTP状态码 + */ + private HttpStatus determineHttpStatus(Integer errorCode) { + if (errorCode == null) { + return HttpStatus.INTERNAL_SERVER_ERROR; + } + + // 根据错误代码返回相应的HTTP状态码 + return switch (errorCode) { + case ErrCode.UNAUTHORIZED -> HttpStatus.UNAUTHORIZED; + case ErrCode.FORBIDDEN -> HttpStatus.FORBIDDEN; + case ErrCode.NOT_FOUND -> HttpStatus.NOT_FOUND; + case ErrCode.CONFLICT -> HttpStatus.CONFLICT; + case ErrCode.USER_NOT_FOUND, + ErrCode.USER_ALREADY_EXISTS, + ErrCode.PASSWORD_INCORRECT, + ErrCode.PASSWORD_MISMATCH, + ErrCode.INVALID_TOKEN -> + HttpStatus.BAD_REQUEST; + case ErrCode.INTERNAL_SERVER_ERROR, + ErrCode.SERVICE_UNAVAILABLE -> + HttpStatus.INTERNAL_SERVER_ERROR; + default -> HttpStatus.BAD_REQUEST; // 默认返回400错误 + }; } } \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/application/service/UserService.java b/backend-services/user-service/src/main/java/com/ai/qa/user/application/service/UserService.java new file mode 100644 index 00000000..8741b3ea --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/application/service/UserService.java @@ -0,0 +1,82 @@ +package com.ai.qa.user.application.service; + +import com.ai.qa.user.api.dto.request.LoginRequest; +import com.ai.qa.user.api.dto.request.RegisterRequest; +import com.ai.qa.user.api.dto.request.UpdatePasswordRequest; +import com.ai.qa.user.api.dto.response.LoginResponse; +import com.ai.qa.user.api.dto.response.RegisterResponse; +import com.ai.qa.user.api.dto.response.UpdatePasswordResponse; +import com.ai.qa.user.api.dto.response.UserResponse; + +/** + * 用户服务接口 + * 定义用户相关的业务逻辑方法,包括身份认证、用户管理等功能 + * 作为业务逻辑层接口,负责处理核心业务规则和数据操作 + * + */ +public interface UserService { + + /** + * 用户登录认证 + * 验证用户名和密码,生成访问令牌并返回用户信息 + * + * @param loginRequest 登录请求参数,包含用户名和密码 + * @return LoginResponse 登录响应结果,包含令牌和用户信息 + * @throws com.ai.qa.user.api.exception.BusinessException 当用户不存在或密码错误时抛出业务异常 + * @see LoginRequest + * @see LoginResponse + */ + LoginResponse login(LoginRequest loginRequest); + + /** + * 用户注册 + * 创建新用户账户,验证用户名唯一性和密码一致性 + * + * @param registerRequest 注册请求参数,包含用户名、昵称、密码等信息 + * @return RegisterResponse 注册响应结果,包含新创建的用户信息 + * @throws com.ai.qa.user.api.exception.BusinessException 当用户名已存在或密码不匹配时抛出业务异常 + * @see RegisterRequest + * @see RegisterResponse + */ + RegisterResponse register(RegisterRequest registerRequest); + + /** + * 根据用户ID查询用户信息 + * 用于获取指定用户ID的完整用户信息 + * + * @param id 用户ID + * @return UserResponse 用户响应对象,包含用户信息和操作结果 + * @throws com.ai.qa.user.api.exception.BusinessException 当用户不存在时抛出业务异常 + * @see UserResponse + */ + UserResponse getUserById(Long id); + + /** + * 根据用户名查询用户信息 + * 用于获取指定用户名的完整用户信息 + * + * @param username 用户名 + * @return UserResponse 用户响应对象,包含用户信息和操作结果 + * @throws com.ai.qa.user.api.exception.BusinessException 当用户不存在时抛出业务异常 + * @see UserResponse + */ + UserResponse findByUsername(String username); + + /** + * 检查用户名是否存在 + * 用于验证用户名是否已被注册,通常在注册前调用 + * + * @param username 需要检查的用户名 + * @return Boolean true表示用户名已存在,false表示用户名可用 + */ + Boolean existsByUsername(String username); + + /** + * 修改用户密码 + * 验证旧密码后更新为新密码,需要用户已登录并通过JWT认证 + * + * @param updatePasswordRequest 修改密码请求参数,包含用户ID、旧密码和新密码 + * @return UpdatePasswordResponse 修改密码响应结果 + */ + UpdatePasswordResponse updatePassword(UpdatePasswordRequest updatePasswordRequest); +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/application/service/impl/UserDetailsServiceImpl.java b/backend-services/user-service/src/main/java/com/ai/qa/user/application/service/impl/UserDetailsServiceImpl.java new file mode 100644 index 00000000..06f6e58e --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/application/service/impl/UserDetailsServiceImpl.java @@ -0,0 +1,46 @@ +package com.ai.qa.user.application.service.impl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import com.ai.qa.user.domain.entity.User; +import com.ai.qa.user.domain.repository.UserRepository; + +/** + * 用户详情服务实现类 + * 实现Spring Security的UserDetailsService接口,用于用户认证 + * 负责从数据库加载用户信息并转换为Spring Security可识别的UserDetails对象 + * + */ +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + @Autowired + private UserRepository userRepository; + + /** + * 根据用户名加载用户详情 + * Spring Security认证过程中的核心方法,用于用户身份验证 + * + * @param username 用户名 + * @return UserDetails Spring Security的用户详情对象 + * @throws UsernameNotFoundException 当用户不存在时抛出此异常 + * @apiNote 此方法在用户登录时被Spring Security自动调用 + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 从数据库查询用户 + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); + + // 转换为Spring Security的UserDetails(可自定义权限) + return org.springframework.security.core.userdetails.User.builder() + .username(user.getUsername()) + .password(user.getPassword()) + .roles("USER") // 简单起见,默认赋予USER角色 + .build(); + } +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/application/service/impl/UserServiceImpl.java b/backend-services/user-service/src/main/java/com/ai/qa/user/application/service/impl/UserServiceImpl.java new file mode 100644 index 00000000..60d98749 --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/application/service/impl/UserServiceImpl.java @@ -0,0 +1,314 @@ +package com.ai.qa.user.application.service.impl; + +import java.time.LocalDateTime; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.ai.qa.user.api.dto.request.LoginRequest; +import com.ai.qa.user.api.dto.request.RegisterRequest; +import com.ai.qa.user.api.dto.request.UpdatePasswordRequest; +import com.ai.qa.user.api.dto.response.LoginResponse; +import com.ai.qa.user.api.dto.response.RegisterResponse; +import com.ai.qa.user.api.dto.response.UpdatePasswordResponse; +import com.ai.qa.user.api.dto.response.UserResponse; +import com.ai.qa.user.api.exception.BusinessException; +import com.ai.qa.user.api.exception.ErrCode; +import com.ai.qa.user.application.service.UserService; +import com.ai.qa.user.common.JwtUtil; +import com.ai.qa.user.domain.entity.User; +import com.ai.qa.user.domain.repository.UserRepository; + +/** + * 用户服务实现类 + * 处理用户相关的业务逻辑,包括登录、注册等功能 + * + */ +@Service +public class UserServiceImpl implements UserService { + private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class); + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private JwtUtil jwtUtil; + + /** + * 用户登录处理 + * 验证用户名和密码,生成访问令牌 + * + * @param loginRequest 登录请求参数,包含用户名和密码 + * @return LoginResponse 登录响应结果,包含令牌和用户信息 + * @throws BusinessException 当用户不存在或密码错误时抛出业务异常 + */ + @Override + public LoginResponse login(LoginRequest loginRequest) { + + log.info("Start login(), username:{}", loginRequest.getUsername()); + + // 根据用户名查询用户信息 + Optional userOptional = userRepository.findByUsername(loginRequest.getUsername()); + + // 验证用户是否存在 + if (!userOptional.isPresent()) { + throw BusinessException.userNotFound(); + } + + User user = userOptional.get(); + + // 验证密码是否正确 + if (!passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) { + throw BusinessException.passwordIncorrect(); + } + + // 更新用户最后登录时间 + user.setUpdateDate(LocalDateTime.now()); + userRepository.save(user); + + // 生成访问令牌(模拟实现,实际应使用JWT) + String token = generateToken(user); + + // 构建登录成功响应 + LoginResponse response = new LoginResponse(); + response.setSuccess(true); + response.setMessage(ErrCode.MSG_SUCCESS); + response.setErrorCode(ErrCode.SUCCESS); + response.setToken(token); + response.setUserId(user.getId()); + response.setUsername(user.getUsername()); + response.setNickname(user.getNickname()); + response.setEmail(user.getEmail()); + response.setLoginTime(LocalDateTime.now()); + + log.info("End login(), username:{}", loginRequest.getUsername()); + + return response; + } + + /** + * 用户注册处理 + * 创建新用户账户,验证用户名唯一性和密码一致性 + * + * @param registerRequest 注册请求参数,包含用户名、昵称、密码等信息 + * @return RegisterResponse 注册响应结果,包含新创建的用户信息 + * @throws BusinessException 当用户名已存在或密码不匹配时抛出业务异常 + */ + @Override + @Transactional + public RegisterResponse register(RegisterRequest registerRequest) { + + log.info("Start register(), username:{}", registerRequest.getUsername()); + + // 检查用户名是否已被注册 + if (userRepository.existsByUsername(registerRequest.getUsername())) { + throw BusinessException.userAlreadyExists(); + } + + // 验证两次输入的密码是否一致 + if (!registerRequest.getPassword().equals(registerRequest.getConfirmPassword())) { + throw BusinessException.passwordMismatch(); + } + + // 创建新用户对象并设置属性 + User user = new User(); + user.setUsername(registerRequest.getUsername()); + user.setNickname(registerRequest.getNickname()); + user.setEmail(registerRequest.getEmail()); + // 对密码进行加密存储 + user.setPassword(passwordEncoder.encode(registerRequest.getPassword())); + user.setCreateDate(LocalDateTime.now()); + user.setUpdateDate(LocalDateTime.now()); + + // 保存用户到数据库 + User savedUser = userRepository.save(user); + + // 构建注册成功响应 + RegisterResponse response = new RegisterResponse(); + response.setSuccess(true); + response.setMessage(ErrCode.MSG_CREATED); + response.setErrorCode(ErrCode.CREATED); + response.setUserId(savedUser.getId()); + response.setUsername(savedUser.getUsername()); + response.setNickname(savedUser.getNickname()); + response.setEmail(user.getEmail()); + response.setRegisterTime(LocalDateTime.now()); + + log.info("End register(), username:{}", registerRequest.getUsername()); + + return response; + } + + /** + * 修改用户密码处理 + * 验证旧密码正确性,更新为新密码 + * + * @param updatePasswordRequest 修改密码请求参数 + * @return UpdatePasswordResponse 修改密码响应结果 + * @throws BusinessException 当用户不存在、旧密码错误或新密码不匹配时抛出业务异常 + */ + @Override + @Transactional + public UpdatePasswordResponse updatePassword(UpdatePasswordRequest updatePasswordRequest) { + + log.info("Start updatePassword(), username:{}", updatePasswordRequest.getUsername()); + + // 根据用户名查询用户信息 + Optional userOptional = userRepository.findByUsername(updatePasswordRequest.getUsername()); + + // 验证用户是否存在 + if (!userOptional.isPresent()) { + throw BusinessException.userNotFound(); + } + + User user = userOptional.get(); + + // 验证旧密码是否正确 + if (!passwordEncoder.matches(updatePasswordRequest.getOldPassword(), user.getPassword())) { + throw BusinessException.passwordIncorrect(); + } + + // 验证两次输入的新密码是否一致 + if (!updatePasswordRequest.getNewPassword().equals(updatePasswordRequest.getConfirmNewPassword())) { + throw BusinessException.passwordMismatch(); + } + + // 更新密码(加密存储) + user.setPassword(passwordEncoder.encode(updatePasswordRequest.getNewPassword())); + user.setUpdateDate(LocalDateTime.now()); + + // 保存更新后的用户信息 + User updatedUser = userRepository.save(user); + + // 构建密码修改成功响应 + UpdatePasswordResponse response = new UpdatePasswordResponse(); + response.setSuccess(true); + response.setMessage("密码修改成功"); + response.setErrorCode(ErrCode.SUCCESS); + response.setUserId(updatedUser.getId()); + response.setUsername(updatedUser.getUsername()); + response.setUpdateTime(LocalDateTime.now()); + + log.info("End updatePassword(), username:{}", updatePasswordRequest.getUsername()); + + return response; + } + + /** + * 根据用户ID查询用户信息 + * 用于获取指定用户ID的完整用户信息 + * + * @param id 用户ID + * @return UserResponse 用户响应对象,包含用户信息和操作结果 + * @throws BusinessException 当用户不存在时抛出业务异常 + */ + @Override + public UserResponse getUserById(Long id) { + log.info("Querying user by ID: {}", id); + + Optional userOptional = userRepository.getUserById(id); + + if (!userOptional.isPresent()) { + log.warn("User not found, id:{}", id); + throw BusinessException.userNotFound(); + } + + User user = userOptional.get(); + return convertToUserResponse(user); + } + + /** + * 根据用户名查询用户信息 + * 用于获取指定用户名的完整用户信息 + * + * @param username 用户名 + * @return UserResponse 用户响应对象,包含用户信息和操作结果 + * @throws BusinessException 当用户不存在时抛出业务异常 + */ + @Override + public UserResponse findByUsername(String username) { + log.info("Querying user by username: {}", username); + + Optional userOptional = userRepository.findByUsername(username); + + if (!userOptional.isPresent()) { + log.warn("User not found, username:{}", username); + throw BusinessException.userNotFound(); + } + + User user = userOptional.get(); + return convertToUserResponse(user); + } + + /** + * 检查用户名是否已存在 + * + * @param username 需要检查的用户名 + * @return Boolean true表示用户名已存在,false表示不存在 + */ + @Override + public Boolean existsByUsername(String username) { + return userRepository.existsByUsername(username); + } + + /** + * 将User实体转换为UserResponse DTO + * + * @param user User实体对象 + * @return UserResponse 响应DTO对象 + */ + private UserResponse convertToUserResponse(User user) { + UserResponse response = new UserResponse(); + response.setSuccess(true); + response.setMessage(ErrCode.MSG_SUCCESS); + response.setErrorCode(ErrCode.SUCCESS); + response.setUserId(user.getId()); + response.setUsername(user.getUsername()); + response.setNickname(user.getNickname()); + response.setRegisterTime(user.getCreateDate()); + return response; + } + + /** + * 生成用户访问令牌(模拟实现) + * 实际项目中应使用JWT等标准token生成机制 + * + * @param user 用户实体对象 + * @return String 生成的访问令牌 + */ + private String generateToken(User user) { + // 调用JwtUtil生成真实token + return jwtUtil.generateToken(user.getUsername()); + } + + // /** + // * 验证用户密码是否正确 + // * + // * @param rawPassword 原始密码(用户输入) + // * @param encodedPassword 加密后的密码(数据库存储) + // * @return Boolean true表示密码正确,false表示密码错误 + // */ + // private Boolean validatePassword(String rawPassword, String encodedPassword) + // { + // return passwordEncoder.matches(rawPassword, encodedPassword); + // } + + // /** + // * 加密用户密码 + // * + // * @param rawPassword 原始密码 + // * @return String 加密后的密码 + // */ + // private String encodePassword(String rawPassword) { + // return passwordEncoder.encode(rawPassword); + // } + +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/common/JwtUtil.java b/backend-services/user-service/src/main/java/com/ai/qa/user/common/JwtUtil.java new file mode 100644 index 00000000..fe1f4d14 --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/common/JwtUtil.java @@ -0,0 +1,203 @@ +package com.ai.qa.user.common; + +import java.util.Base64; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.SecretKey; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; + +/** + * JWT工具类 + * 负责JWT令牌的生成、解析和验证 + * 使用JJWT库处理JSON Web Tokens,支持HS512签名算法 + * 提供完整的JWT生命周期管理功能 + * + */ +@Component +public class JwtUtil { + + /** + * JWT签名密钥 + * 从配置文件中注入,需要Base64编码的字符串 + * 建议使用至少256位的密钥长度以确保安全性 + * + * @apiNote 在application.yml中配置: jwt.secret=your-base64-encoded-secret + * @security 生产环境应使用安全的密钥管理方式 + */ + @Value("${jwt.secret}") + private String secret; + + /** + * JWT过期时间(毫秒) + * 从配置文件中注入,表示令牌的有效期 + * 根据安全要求调整过期时间,平衡安全性和用户体验 + * + * @apiNote 示例值: + * - 3600000: 1小时 (60*60*1000) + * - 86400000: 24小时 (24*60*60*1000) + * - 604800000: 7天 (7*24*60*60*1000) + */ + @Value("${jwt.expiration}") + private long expiration; + + /** + * JWT声明中用户名的键名常量 + * 用于统一管理JWT payload中的字段名称 + * 避免硬编码,提高代码可维护性 + */ + private static final String CLAIM_KEY_USERNAME = "username"; + + /** + * 获取符合HMAC-SHA512要求的签名密钥 + * 将Base64编码的密钥字符串转换为JJWT所需的SecretKey对象 + * 使用Keys.hmacShaKeyFor()方法确保密钥长度符合HS512算法要求 + * + * @return SecretKey 用于JWT签名验证的密钥对象 + * @throws IllegalArgumentException 当密钥Base64解码失败或长度不符合要求时抛出 + * @security 确保密钥长度至少256位(32字节) + */ + private SecretKey getSigningKey() { + byte[] keyBytes = Base64.getDecoder().decode(secret); + return Keys.hmacShaKeyFor(keyBytes); + } + + /** + * 生成JWT令牌 + * 根据用户名创建包含基本声明的JWT令牌,使用HS512算法进行签名 + * 包含标准声明(iat, exp)和自定义声明(username) + * + * @param username 用户名,将作为自定义声明添加到令牌payload中 + * @return String 生成的JWT令牌字符串,格式: header.payload.signature + * @throws IllegalArgumentException 当用户名为空或密钥无效时抛出 + * @apiNote 生成的令牌示例: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9... + */ + public String generateToken(String username) { + // 参数验证 + if (username == null || username.trim().isEmpty()) { + throw new IllegalArgumentException("Username cannot be null or empty"); + } + + Date now = new Date(); + Date expirationDate = new Date(now.getTime() + expiration); + + // 创建自定义声明(Claims) + Map claims = new HashMap<>(); + claims.put(CLAIM_KEY_USERNAME, username); + + // 构建JWT令牌(使用JJWT 0.12.x版本的流式API) + return Jwts.builder() + .claims(claims) // 设置自定义声明 + .issuedAt(now) // 设置签发时间(iat) + .expiration(expirationDate) // 设置过期时间(exp) + .signWith(getSigningKey(), Jwts.SIG.HS512) // 使用HS512算法签名 + .compact(); // 生成最终的令牌字符串 + } + + /** + * 从JWT令牌中提取用户名 + * 解析并验证令牌签名,然后从payload中获取用户名声明 + * + * @param token JWT令牌字符串 + * @return String 用户名 + * @throws ExpiredJwtException 当令牌已过期时抛出 + * @throws UnsupportedJwtException 当令牌格式不支持时抛出 + * @throws MalformedJwtException 当令牌格式错误时抛出 + * @throws SignatureException 当签名验证失败时抛出 + * @throws IllegalArgumentException 当令牌为空或格式错误时抛出 + * @security 此方法会验证令牌签名,确保令牌未被篡改 + */ + public String getUsernameFromToken(String token) { + // 参数基础验证 + if (token == null || token.trim().isEmpty()) { + throw new IllegalArgumentException("Token cannot be null or empty"); + } + + // 创建JWT解析器并设置验证密钥 + JwtParser parser = Jwts.parser() + .verifyWith(getSigningKey()) // 设置验证密钥 + .build(); + + // 解析并验证令牌签名 + Jws jws = parser.parseSignedClaims(token); + + // 从payload中获取username声明 + return jws.getPayload().get(CLAIM_KEY_USERNAME, String.class); + } + + /** + * 验证JWT令牌的有效性 + * 综合检查令牌的签名、过期时间、格式等是否有效 + * + * @param token JWT令牌字符串 + * @return boolean true表示令牌有效且未过期,false表示无效或已过期 + * @apiNote 此方法捕获所有异常并返回false,适合用于快速验证 + * @apiNote 如果需要详细的错误信息,请使用getUsernameFromToken方法 + */ + public boolean validateToken(String token) { + try { + // 基础空值检查 + if (token == null || token.trim().isEmpty()) { + return false; + } + + JwtParser parser = Jwts.parser() + .verifyWith(getSigningKey()) + .build(); + + // 解析并验证令牌,失败会抛出相应异常 + parser.parseSignedClaims(token); + return true; + + } catch (ExpiredJwtException e) { + // 令牌已过期 + return false; + } catch (UnsupportedJwtException e) { + // 不支持的JWT格式 + return false; + } catch (MalformedJwtException e) { + // 令牌格式错误 + return false; + } catch (SignatureException e) { + // 签名验证失败(可能被篡改) + return false; + } catch (IllegalArgumentException e) { + // 参数错误(如空令牌) + return false; + } catch (JwtException e) { + // 其他JWT相关异常 + return false; + } + } + + /** + * 获取令牌剩余有效时间(毫秒) + * 计算当前时间到令牌过期时间的间隔 + * + * @param token JWT令牌字符串 + * @return long 剩余有效时间(毫秒),负数表示已过期 + * @throws JwtException 当令牌无效时抛出 + */ + public long getRemainingExpiration(String token) { + JwtParser parser = Jwts.parser() + .verifyWith(getSigningKey()) + .build(); + Jws jws = parser.parseSignedClaims(token); + Date expiration = jws.getPayload().getExpiration(); + return expiration.getTime() - System.currentTimeMillis(); + } +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/common/KeyGenerator.java b/backend-services/user-service/src/main/java/com/ai/qa/user/common/KeyGenerator.java new file mode 100644 index 00000000..bd7fc62f --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/common/KeyGenerator.java @@ -0,0 +1,40 @@ +package com.ai.qa.user.common; + +import java.util.Base64; + +import io.jsonwebtoken.Jwts; + +/** + * JWT密钥生成工具类 + * 用于生成安全的HS512算法密钥,适用于JWT令牌签名 + * 生成的密钥为Base64编码格式,可直接配置到application.yml中 + * + */ +public class KeyGenerator { + /** + * 主方法 - 生成HS512 JWT签名密钥 + * 生成512位(64字节)的HMAC-SHA512密钥,并输出Base64编码结果 + * + * @param args 命令行参数(未使用) + * + * @apiNote 运行方法: java KeyGenerator + * @apiNote 输出示例: MzVkMjBjZDctYjUzNi00ZmUyLWEwODAtNGQ5OTZhYzE2M2JhYjY0ZWM0NDctNTIzNS00ZDkyLWExMjMtZjA1M2Q4ZGUxNzRk + * + * @security 注意: + * 1. 生成的密钥应妥善保管,不要泄露 + * 2. 生产环境建议使用密钥管理服务(如KMS、HashiCorp Vault等) + * 3. 定期轮换密钥以增强安全性 + * 4. 不同环境(开发、测试、生产)应使用不同的密钥 + */ + public static void main(String[] args) { + // 使用JJWT 0.12.x版本推荐的方式生成HS512密钥 + byte[] keyBytes = Jwts.SIG.HS512.key().build().getEncoded(); + // 将二进制密钥转换为Base64编码字符串,便于配置文件使用 + // Base64编码使二进制数据可以文本形式安全存储 + String base64Key = Base64.getEncoder().encodeToString(keyBytes); + System.out.println("生成的512位密钥(请复制到application.yml):"); + System.out.println("=================================================="); + System.out.println(base64Key); + System.out.println("=================================================="); + } +} diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/domain/entity/User.java b/backend-services/user-service/src/main/java/com/ai/qa/user/domain/entity/User.java new file mode 100644 index 00000000..f2ef40a1 --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/domain/entity/User.java @@ -0,0 +1,83 @@ +package com.ai.qa.user.domain.entity; + +import lombok.Data; + +import javax.persistence.*; +import java.time.LocalDateTime; + +/** + * 用户实体类 + * 映射数据库中的用户表,存储系统用户的基本信息 + * 使用JPA注解进行对象关系映射(ORM) + * + */ +@Entity +@Table(name = "user") // 映射数据库表名 +@Data // Lombok注解,自动生成getter、setter、toString等方法 +public class User { + /** + * 用户唯一标识ID + * 主键,自增长策略,由数据库自动生成 + * + * @apiNote 示例值: 1, 2, 3... + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + /** + * 用户名 + * 用户的登录账号,要求唯一不可重复 + * 长度建议限制,用于身份认证 + * + * @apiNote 示例值: "admin", "john_doe", "testuser" + * @constraint 唯一约束,不能为空 + */ + private String username; + + /** + * 用户昵称 + * 用户的显示名称,可以重复 + * 用于界面展示,增强用户体验 + * + * @apiNote 示例值: "John Doe" + */ + private String nickname; + + /** + * 用户邮箱 + * 用户的电子邮箱地址,用于接收通知和重置密码 + * 要求符合邮箱格式规范,通常需要唯一性约束 + * 可用于替代用户名进行登录 + * + * @apiNote 示例值: "user@example.com", "admin@company.com" + */ + private String email; + + /** + * 用户密码 + * 存储加密后的密码哈希值,不应存储明文密码 + * 使用强加密算法(如BCrypt)进行加密 + * + * @apiNote 示例值: "$2a$10$abcdefghijklmnopqrstuvwxyz123456" + * @security 密码长度建议至少6位,包含字母、数字、特殊字符 + */ + private String password; + + /** + * 创建时间 + * 记录用户账号的创建时间 + * 由系统自动设置,不应手动修改 + * + * @apiNote 格式: ISO-8601, 示例值: "2024-01-15T10:30:00" + */ + private LocalDateTime createDate; + /** + * 更新时间 + * 记录用户信息的最后修改时间 + * 每次用户信息更新时自动更新为当前时间 + * + * @apiNote 格式: ISO-8601, 示例值: "2024-01-15T10:30:00" + */ + private LocalDateTime updateDate; +} diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/domain/repository/UserRepository.java b/backend-services/user-service/src/main/java/com/ai/qa/user/domain/repository/UserRepository.java new file mode 100644 index 00000000..5353f50d --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/domain/repository/UserRepository.java @@ -0,0 +1,93 @@ +package com.ai.qa.user.domain.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import com.ai.qa.user.domain.entity.User; + +/** + * 用户数据访问接口 + * 继承JpaRepository提供基本的CRUD操作,定义用户相关的数据查询方法 + * 作为数据访问层(DAO),负责与数据库进行交互 + * + */ +@Repository +public interface UserRepository extends JpaRepository { + + /** + * 根据用户ID查询用户信息 + * 使用自定义查询语句,可以更灵活地控制查询 + * + * @param id 用户ID + * @return Optional 用户信息的Optional包装 + */ + @Query("SELECT u FROM User u WHERE u.id = :id") + Optional getUserById(@Param("id") Long id); + + /** + * 根据用户名查询用户信息 + * 使用Spring Data JPA的查询方法命名约定自动生成查询 + * + * @param username 用户名 + * @return Optional 用户信息的Optional包装,避免空指针异常 + * @see User + * @see Optional + */ + Optional findByUsername(String username); + + /** + * 检查用户名是否存在 + * 验证指定用户名是否已被注册使用 + * + * @param username 需要检查的用户名 + * @return Boolean true表示用户名已存在,false表示用户名可用 + */ + Boolean existsByUsername(String username); + + /** + * 根据用户名和密码查询用户 + * 用于登录验证,同时匹配用户名和加密后的密码 + * + * @param username 用户名 + * @param password 加密后的密码 + * @return Optional 用户信息的Optional包装 + */ + @Query("SELECT u FROM User u WHERE u.username = :username AND u.password = :password") + Optional findByUsernameAndPassword( + @Param("username") String username, + @Param("password") String password + ); + + /** + * 根据昵称模糊查询用户 + * 使用LIKE操作符进行模糊匹配 + * + * @param nickname 昵称关键词 + * @return Optional 用户信息的Optional包装 + */ + @Query("SELECT u FROM User u WHERE u.nickname LIKE %:nickname%") + Optional findByNicknameContaining(@Param("nickname") String nickname); + + /** + * 统计指定昵称的用户数量 + * 用于验证昵称的唯一性或统计使用情况 + * + * @param nickname 昵称 + * @return Long 用户数量 + */ + Long countByNickname(String nickname); + + /** + * 根据用户ID和用户名查询用户 + * 用于验证用户身份或权限检查 + * + * @param id 用户ID + * @param username 用户名 + * @return Optional 用户信息的Optional包装 + */ + Optional findByIdAndUsername(Long id, String username); +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/config/JwtAuthenticationFilter.java b/backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/config/JwtAuthenticationFilter.java new file mode 100644 index 00000000..38f42180 --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/config/JwtAuthenticationFilter.java @@ -0,0 +1,102 @@ +package com.ai.qa.user.infrastructure.config; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import com.ai.qa.user.common.JwtUtil; + +/** + * JWT认证过滤器 + * 继承OncePerRequestFilter确保每次请求只过滤一次 + * 用于处理JWT令牌的认证流程 + * + */ +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + // JWT工具类,用于生成和解析JWT令牌 + @Autowired + private JwtUtil jwtUtil; + + // 用户详情服务,用于根据用户名加载用户信息 + @Autowired + private UserDetailsService userDetailsService; + + /** + * 核心过滤方法,处理每个HTTP请求 + * + * @param request HTTP请求对象 + * @param response HTTP响应对象 + * @param filterChain 过滤器链,用于继续处理请求 + * @throws ServletException 可能抛出的Servlet异常 + * @throws IOException 可能抛出的IO异常 + */ + @SuppressWarnings("null") + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + // String path = request.getRequestURI(); + // + // System.out.println("JwtFilter: path=" + path + " method=" + + // request.getMethod()); + + // 允许匿名访问的接口直接放行 + // if ("/api/user/login".equals(path) || + // "/api/user/register".equals(path) || + // request.getMethod().equalsIgnoreCase("OPTIONS")) { + // filterChain.doFilter(request, response); + // return; + // } + + String path = request.getServletPath(); + System.out.println("JwtFilter: servletPath=" + path + " method=" + request.getMethod()); + + // 放行 login 和 register,无论前面加不加前缀 + if (path.endsWith("/login") || + path.endsWith("/register") || + request.getMethod().equalsIgnoreCase("OPTIONS")) { + filterChain.doFilter(request, response); + return; + } + + String authHeader = request.getHeader("Authorization"); + String token = null; + String username = null; + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + token = authHeader.substring(7); + try { + username = jwtUtil.getUsernameFromToken(token); + } catch (Exception e) { + logger.error("Invalid JWT token: {}", e); + } + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + if (jwtUtil.validateToken(token)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/config/SecurityConfig.java b/backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/config/SecurityConfig.java index 804edaa0..69b3ba4f 100644 --- a/backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/config/SecurityConfig.java +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/config/SecurityConfig.java @@ -1,4 +1,78 @@ package com.ai.qa.user.infrastructure.config; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +/** + * Spring Security 安全配置类 + * 用于配置应用程序的安全策略,包括认证、授权、密码加密等 + * + */ +@Configuration +@EnableWebSecurity public class SecurityConfig { -} + + @Autowired + private JwtAuthenticationFilter jwtAuthFilter; + + /** + * 配置密码编码器Bean + * 使用BCrypt强哈希算法进行密码加密和验证 + * + * @return BCryptPasswordEncoder实例 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * 配置认证管理器Bean + * 用于处理用户认证请求 + * + * @param config 认证配置对象,由Spring自动注入 + * @return AuthenticationManager实例 + * @throws Exception 可能抛出的异常 + */ + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + /** + * 配置安全过滤器链 + * 定义HTTP请求的安全策略和过滤规则 + * + * @param http HttpSecurity对象,用于构建安全配置 + * @return SecurityFilterChain实例 + * @throws Exception 可能抛出的异常 + */ + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() + .antMatchers(HttpMethod.POST, "/api/user/login", "/api/user/register").permitAll() + .antMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**").permitAll() + .antMatchers("/actuator/health", "/actuator/info").permitAll() + .antMatchers("/actuator/**").hasRole("ADMIN") + .anyRequest().authenticated() + .and() + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) + .build(); + } +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/config/SwaggerConfig.java b/backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/config/SwaggerConfig.java index a0f1f21a..333b0043 100644 --- a/backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/config/SwaggerConfig.java +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/config/SwaggerConfig.java @@ -1,4 +1,44 @@ package com.ai.qa.user.infrastructure.config; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Swagger/OpenAPI 配置类 + * 用于配置API文档的生成和展示 + */ +@Configuration public class SwaggerConfig { -} + + /** + * 自定义OpenAPI配置 + * 配置API文档的基本信息、服务器地址和安全认证方案 + * + * @return OpenAPI对象,包含所有API文档配置 + */ + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("用户管理API") + .description("用户登录、注册、管理等接口") + .version("1.0.0")) + .servers(java.util.Arrays.asList( + new Server().url("http://localhost:8081") + .description("development environment"))) + .addSecurityItem(new SecurityRequirement().addList("bearerAuth")) + .components(new Components() + .addSecuritySchemes("bearerAuth", new SecurityScheme() + .name("bearerAuth") + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT") + .description("JWT authentication token"))); + } +} \ No newline at end of file diff --git a/backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/persistent/UserMapper.java b/backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/persistent/UserMapper.java new file mode 100644 index 00000000..2e3f3665 --- /dev/null +++ b/backend-services/user-service/src/main/java/com/ai/qa/user/infrastructure/persistent/UserMapper.java @@ -0,0 +1,4 @@ +package com.ai.qa.user.infrastructure.persistent; + +public class UserMapper { +} diff --git a/backend-services/user-service/src/main/resources/application.yml b/backend-services/user-service/src/main/resources/application.yml index 4fa926ce..087a3c8b 100644 --- a/backend-services/user-service/src/main/resources/application.yml +++ b/backend-services/user-service/src/main/resources/application.yml @@ -3,17 +3,79 @@ server: spring: application: name: user-service - cloud: - nacos: - server-addr: 54.219.180.170:8848 + # cloud: + # nacos: + # discovery: + # server-addr: 3.101.113.38:8848 + # #ip: 127.0.0.1 + # #port: 8081 datasource: - url: jdbc:mysql://54.219.180.170:3306/ai_qa_system?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai + url: jdbc:mysql://database:3306/ai_qa_system?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: ai_qa_system + # url: jdbc:mysql://9.78.190.130:3306/ai_qa_system?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC + # username: remote_user_group2 + # password: Group2Password123! + hikari: + maximum-pool-size: 5 + minimum-idle: 2 + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 jpa: hibernate: ddl-auto: update properties: hibernate: dialect: org.hibernate.dialect.MySQL8Dialect - show-sql: true \ No newline at end of file + format_sql: true + show-sql: true + autoconfigure: + exclude: + - org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration + +springdoc: + api-docs: + path: /v3/api-docs + enabled: true + swagger-ui: + path: /swagger-ui.html + enabled: true + operations-sorter: method + tags-sorter: alpha + +logging: + file: + name: application.log + path: logs + level: + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + charset: + console: UTF-8 + file: UTF-8 + +jwt: + secret: HhyD5jnwwGbyecOrbkAY6OEWNM/zDgF2qwFlF4MFAHznR4AaLweF8Xu71MF4TaYUfX7dRY9JIzOgF/yTYqx5Qg== + expiration: 86400000 + +management: + endpoints: + web: + exposure: + include: health,info + base-path: /actuator + endpoint: + health: + enabled: true + show-details: WHEN_AUTHORIZED + info: + enabled: true + metrics: + enabled: false + metrics: + enable: + process: false + system: false diff --git a/backend-services/user-service/target/classes/application.yml b/backend-services/user-service/target/classes/application.yml new file mode 100644 index 00000000..087a3c8b --- /dev/null +++ b/backend-services/user-service/target/classes/application.yml @@ -0,0 +1,81 @@ +server: + port: 8081 +spring: + application: + name: user-service + # cloud: + # nacos: + # discovery: + # server-addr: 3.101.113.38:8848 + # #ip: 127.0.0.1 + # #port: 8081 + datasource: + url: jdbc:mysql://database:3306/ai_qa_system?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true + username: root + password: ai_qa_system + # url: jdbc:mysql://9.78.190.130:3306/ai_qa_system?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC + # username: remote_user_group2 + # password: Group2Password123! + hikari: + maximum-pool-size: 5 + minimum-idle: 2 + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect + format_sql: true + show-sql: true + autoconfigure: + exclude: + - org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration + - org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration + +springdoc: + api-docs: + path: /v3/api-docs + enabled: true + swagger-ui: + path: /swagger-ui.html + enabled: true + operations-sorter: method + tags-sorter: alpha + +logging: + file: + name: application.log + path: logs + level: + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE + charset: + console: UTF-8 + file: UTF-8 + +jwt: + secret: HhyD5jnwwGbyecOrbkAY6OEWNM/zDgF2qwFlF4MFAHznR4AaLweF8Xu71MF4TaYUfX7dRY9JIzOgF/yTYqx5Qg== + expiration: 86400000 + +management: + endpoints: + web: + exposure: + include: health,info + base-path: /actuator + endpoint: + health: + enabled: true + show-details: WHEN_AUTHORIZED + info: + enabled: true + metrics: + enabled: false + metrics: + enable: + process: false + system: false diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/UserServiceApplication.class b/backend-services/user-service/target/classes/com/ai/qa/user/UserServiceApplication.class new file mode 100644 index 0000000000000000000000000000000000000000..e16361a29b2a0eb73e653f1c41408e5e7cbc84ea GIT binary patch literal 750 zcma)4yHeaR6g^sY@nV)o7D9Ly6qzhrpf1fO%p^OL$_Y$1Y>HH}5k_E38(Aj#Eh;h; zd;lMn$(6$lE?iLfUf+k#J@-ftfA8)AyvJUE7Q^eri0j$!_t(#jn%%nc5v4DGQwlOg7?&_f6F3@ee+@=KY= z(tZvXNkldkR{8ib*>)GIV0aTfi_C}yRLj}m<(wQBT7*axXZF9>MSA6+xqfFe$wk?#y| z(Y>k4Az5DrH7t_t{cj+@M9~74u|gpJ@Y@Ou==Rz04_3cFoe!zvdjo_uj4n3tit?NE Ub?70GYhkOh+t?xR$L<3E07-bmaR2}S literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/api/controller/UserController.class b/backend-services/user-service/target/classes/com/ai/qa/user/api/controller/UserController.class new file mode 100644 index 0000000000000000000000000000000000000000..153a8d8e324c71a45db1808b9347a3e70b2e006f GIT binary patch literal 7047 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 G1^)*ayU^GG literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/Response.class b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/Response.class new file mode 100644 index 0000000000000000000000000000000000000000..1902576a7ceeab0d7686bf5cc9608f13c450d23b GIT binary patch literal 295 zcmaivO-{p541~vN(m-fw#RX7zzycqDpA8bL2&5Ku@1z#ID$OhT;a;qeSa1Lih3Z$q zn$3*ImaWg;11=fJGJf|8*gSmuu*YCXJ0M5hfd&Zm(^|8 z^+Fjso*U>8Wp97m+p`yyt9%)%9fmOGydBj7fes5MQUaYTj;q!x``gNYAn8KC=M`mn zOZtx9$4VnCIrKTT@;_)`IGx6NoRp5*XCbYlwgrvp^^Kj+ZhVUruBx!)?W{|moQ29K z0R3Xkp+Jg!%QDqxJVbOC#=mG~#J4P5!T`A~N_SpcYvquI3|d-|#5)$=#DLa~Sa<_1 z0>V36o!Faz>4hcb3$$x0M(U;PsOlROkwLwdu|ZwYz8T8m-l{x^$<35xP|}4FNREv! zYh2eXOyMenu5T=_&J)!#v-MR_DYK7S8iKWq)cs~cFY2WpHtw{Ao5%}X*q3{1&U1@0 z{M-o{#tlO0h9qse3GGb^ab86zGmh(ox2kxzG~Qbl3YZaS-SjGcQ7t&Sle!y+;;L?V z*7T0&g+b`cgQ&PcE9Nkt#$8sRJ`#?5=(42Yk!Q%tf#Qe(!`≀sA3m$vRvMDLRIKY;Z6pu76H_6gt1z? z%!fj)rE5rLiV(>E))ZQ^W!;v%!;WiUdC#`>Mo2 z$=x8p1Mz0-Sb9i zg8ok;OA|#YH}M@z<2d&e;#-)>PO=XP} zT&`IR=S`Xn=Tliz+oYbM=LqdtGn=Xz8OMy3JXf-%2*W8qC5+--CeVjXTwxg#7{OhP zQJ2I7Zeg5~fkk%E1SJ!z%q>SLg-QA!LQ6XH7dp}qf1^K*B)=Jo7bB6^Y0Ud5i81s| z+=Q5$46n;-YDP5b|Ej8airOSrjXL&zrAj+5OuqVJ)BHi5NvHX)!i+InC3mo^P$IRN zuG|sQQzx*$()!`Np%1b~e3H?G^JuU+k=G-yF{C6u!h3k1kWAdb2b46DYq(CBNer{! zZcs9+q}_w@oC<@a2|=Q`Jm<5%PiRFS%$h;X#yG2@8RLD}bmpcRU(acWPFN@RgaGlf y;p>Pq5&9x;!ejk9_!+a!hvV#Zki^H;T7H7tTr-5T$lns~ai{-qKV>i7NA_RJbH{}M literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/request/RegisterRequest.class b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/request/RegisterRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..57c551b41180317f9b8ff39aae7a2913d1ce2af9 GIT binary patch literal 3858 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 literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/BaseResponse.class b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/BaseResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..bd601c9f08366f79b12b7729a2f2b5224922f010 GIT binary patch literal 4327 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 A+W-In literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/UpdatePasswordResponse.class b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/UpdatePasswordResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..7e7cd5c9ac998e2d50b143639c74767812d8038e GIT binary patch literal 3204 zcma)8U3U{z6x}zO$t3BNhC-pS6+sHNO-ryMVnQpld<9yl{XzkiX)-oL)0vP=iu?%P zUH$-h;X&8Z>f*trFSwS!NyU9Sir#RIO~ub+R?HUUlrcnO&}y4A-1D>h&j1t+WuRH7EjOd)ZDKb+c(9 z5R(TKN&>0bN5-a+tr+%NcGj`i@&a+WXB!))KyPSw(XCnDezM_(v1Ca^+DEut(7tF? zt}sHmMMsG>(_IeE=^Y*k`3g=NSZLVNvS4pob!)X^Ubbz=HC&5q<>3`)W7S#DUN>#C zMz_3fk`8s43=krkF~&UDL8 zY2K)@My^>St**exkF76k7KA1ZyCo_V(qMq=5Rrt(%*PF*Lhn2L-C2EP7Tx^Fy(EJk z)-i(9Y?-2AUzKR7%;@NXp2Rq^8qP4@?GE#eb)3R!iF{wj5ge7$2Rh!ufPipzHeGO< zfaS|?sM>Sft> z0@2}-f+TiX#}qzcRPzgkxvLDjz;yANHY%=w)`}6E1%aa*#=1G<*hRy=Yq?}$*ImPQ zS=5_hXMttAuXauN8V6?Ne$lcmcgjo9N)l15=vc&(KyuM()QaXcOJ2-@&UfpK><{vv zb{x0v){Ls(-}NN!;|mQB1ctUd$g($`bu;Uq>=V0Y6kVtGR5Jcj;M*{-0PQ=xVc4aL zSs$8pob^UE-%c&0*gL%%6pisEcA0 zMO>hy;5x41V@fLL@+IUb#ZX}>^OQ8y*m@T!#rf|Xkb$nn|Hc6g;vXE*FwVa?#eQE* zgvDlSoMUno`8{nR52gNWPLUgFCD@6}qWN1BIn~=@y*#6MTJ^RDJC|U} zKQM?kiUmw#hS92+#}!Huw%sjUC4(qVVuGocC79x%xK2s+lKcd**HkD>vJgxxU#|(@ zUPq*&4{8(8#3(ye0W21RZ6kMU31*l#I!>%t<8 literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/UserResponse.class b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/UserResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..8b03a37411c92705d4b05e179c12578c6e1ca882 GIT binary patch literal 3648 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 E0UW4Ri2wiq literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/api/exception/GlobalExceptionHandler.class b/backend-services/user-service/target/classes/com/ai/qa/user/api/exception/GlobalExceptionHandler.class new file mode 100644 index 0000000000000000000000000000000000000000..d97c5f7a96c4c99d731bd871b4028a9fb7f3e004 GIT binary patch literal 7462 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 UoC)&1SMhKBhgOAA$HG?hU;hKU!vFvP literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/application/service/UserService.class b/backend-services/user-service/target/classes/com/ai/qa/user/application/service/UserService.class new file mode 100644 index 0000000000000000000000000000000000000000..46e80886f71de838b3a6d714939ace0fae42f97e GIT binary patch literal 923 zcmbVL+fKqj5S>N22nh9pmoE@s*bg8krVl12H4++MP1bg;OUiE9-J<-O4}O3jW!$#V z0%{<>G&5)Ca^}qR^XvTs0IuM?3MB+RPmD2PctUWjsKg|SLgo?81jojoGLPa1W4M`J zg0mgg+pXjH#wlA*e|jFlB56{zf*#dF=Ltij167d(knc)d;9gRI_Kd zK7#A6Qz+&L9&9mpl0Bq(ixM++TF;F8_AGhgW?TgJa$#3X_Mg(VIIm~XH=~?dZgKHz zi((vGh>#L)Hq(kDpJ+NDO1%nc=tdjWe^|-beh2G$xlEl^4*5Rnu85^aZ&+wlvl0Q_ z?4EncBozb*cFa!!P%-CShKliP#w#PJLUQqYP%ofH3N>NBfLbZk2CIM$Qs@wl)}dqQ P6wrz3g5cDAOK|oLKC~2Y literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/application/service/impl/UserDetailsServiceImpl.class b/backend-services/user-service/target/classes/com/ai/qa/user/application/service/impl/UserDetailsServiceImpl.class new file mode 100644 index 0000000000000000000000000000000000000000..651c5e17409f5cf7fc2fea7e7f61d40198b9ab44 GIT binary patch literal 3202 zcmcImZFdtz6n-X6yCq#AM6ebVTSzf&u?zS@B~bf93rL{Yv_(YJNp{i>-Ry*YX{j%N z!{g7;b38%u=<&11-{kSmZfH}ohnI>Uc4udH=HBPt``mlyuYVr>0pKFOGmv7Ktg5=j zrFEZMp)Wj(HyVzt@<1xrqWOlb3QN`-jAP~H~9 znbH$*ZB^&ewY=>;k(`|)MTUXJ&<$i=tVmy095L;>Dv0d!874|eAS;5qzE$H@t<>V% z2dwEZP@B>dHi_-IAYB(iJztdPR6kob)o}N~+48 z74AtrZ!M;SHAz6Go;2(Q<3NCNj^ehC(#)2AyX10)g+gg3IORYGqd1ai^m9dq)MVx! zR7FGc!#D6c!{sCoK3$lGc0&d8Ds=5_Y5Tx%#Dsy~KD>cr2Hs>Kmh2+uv!ZR{EesNE zYSOjay--N>Lgbh*kdCzw*~6V;jxyu8i4(|ETvtUvjC;n>TIg1zWGRO_bA(sS;3NtL z-lm9jYA`W^QHDO{%{jhUTJzNAj*bXRr@)c65XSjcG;oSxDA_xL*iH$iIgr6PP8+Zo z&c1k*Oq{`42BQ*6#}*#LOkpp~$w!ooWsq;i@SchH@d5dO{JTTcLnOR|sToY*ynzp? zOuab%cEu)&xWLdGZYRc4VqE{5mdLzG6CXV`?5Q|p z7xPq1mdkUC`p^{Au1CnnYfM{q$vtF!^J%XyVW3cHR{Ak zc4_}JoOrU>weq@+xg;TbT?A{&zR7j-2!u!G_d2{@v3dS9!`+t@zrWt0Am%+@7@yXOs&3z~hbBmwpBuRRa}<<2x{=yP?*FRf>E|8+ohq znXo}<`pEl_ml*zTH=uPg7Jh}hHjy-6QffVH6g%VJwzBKs5`k{t{`@~jQk zzd|#MMn+@z=tJ!Pi9S-u(pzr^Ud2Ir>#ei_IhupM58*Hgd5yl)H0zJVd{6(oXk}vb zXAF$}ilZrvJt8P*V)##tUH=8|oND6TCeG1tv5Bel@0h*am76O!ak+_WKhoJ)^T`No zo}dnsjy%aP;0WCwhA>K9o!Tf);T)}LWP`EJ7BbRK?QS8$G=!V@gd{G~u5L!l$YwO7 z>8G}OJn{mt|2OpC9({ym;301BfQZc>j+CTme;S|DJB7R3p8SI5U(%DK)f!1#!#e8l VNq&Hh*7_#4@C^nC);PY!z`u;($^8HT literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/application/service/impl/UserServiceImpl.class b/backend-services/user-service/target/classes/com/ai/qa/user/application/service/impl/UserServiceImpl.class new file mode 100644 index 0000000000000000000000000000000000000000..0933e1df0534569081af1a8c685c5b1690f85d96 GIT binary patch literal 7737 zcmbtZ33yc175;BBVTQ>ALf9Mx2`Z34!hj+W5?R8c5CTXb3bpDmc?l0DGht=|M5RJ& z6;!HLszuyt#ig~bbl6l-t3_)UTUu+?R;^&^Vy#-WmfHU3Ei*5Z3E)@#GVi_n?mhRM z^Pm5mb005$zUL_bXRGtP$WxdQjaBsjWsvx@hXL$qSQjYHI^`vv3R?}*tuEK8)GdYBN~jS=a`yimQYAwVOun5wCD{+ z!dMp3r$wW&q-kM7VWPEbYut!7H^oCO`kGjLWpJ4uiY9_hp|ETk45b$Yr?n+xYmB(w zNU^x@QsD4eXp3m!zn1HmdWJ?{a%@E--!kuEiYia7*U0bN;?ZNf{k=>Twm3uCz29JMs%U|K$EgU zC)Dc9PPE3N3Ei1)JyrQITw!2+G89jim^+r1m6u37Mj0lPE?ehEDMorxrZB2Y!JXC6 zFbd@gg>=7=Kx8TOEiH2)P@j}&tSZE4jPW8!o@GO+VJyy|FO6tZOkso@K6|lCoJGy5 zvIYTgoQCf37h*gndNDyEn5Fvh=%k?%lgP^^BidNgp0S`bhYq$)Z4Bt({8mXRp-7b< z)tKyM{z0)Ba~=5uFV*lRab#~Ncg<$Xi^rMq{rEC2_TnPu8xu~+ zv*Zy|!HiH+U&JR3mmoy1MPqB2Kb%1CJdZxEMOZ^4NQfQ@(xbJJB1DYBAY0`e%gP!g znwvE&he1jtLaRA{xZ6aB050q7AwPjwsUdkC`$Z@g9ViM^v=kviNzwUMHrIMcVZFO$z->m%6vMr_qH` ztkuwtOBMWdVSQUTOg_kbm!i(>G6vV>3a97LHN%CO@wn6#AHG5!9lmMvJ5Szrc;gL+ zuDMh8TCZUPzH04N$BjbG^nE|~UQQS__;Dq!^5W|q%$pre>MZnP-qoVyY`0PNOVg-L z8m_^$)PP1&qnN2vmRZjHGVT?{W)0sE)qGrGA}Zl^gN7UNO;!+6cs4UZu@jDF4>LE$ ztSf7`X!sUxW*(9#BwuA}AGcNJOxdRvBW1x>4Y$f>h4uo;>hkh74Yxbgo@aztTH{gN zso^eXerCKDcWd~r%=b~hne1yKB7D1sdmT&CgZFEAK!_p^F{_+V&4=$RoHR4qn6qAS zvRTILddMKnl?A&jpIk0!)Jf#bfL2I#NZ_o_9+{>&_Ii)_WN@U%mQUSJI{ z3p2}9wzOjtifZ&XlTf-JX>WCUlFA%vS7(dQq3mP|?ZIv!ys|FTYWFuqb$W7ntZ_j| zI>)3QC)22OvytGvLSv^4lF`~~3>MmRPrOCRtgyQjg-JQP&Sab|EIU4F7l0nUxM3Bn?Va@S=vFm~}~FZdxR+hZ@^wnp^p?9}DE?XBvJklqx2rrpKa9 zM!Y3cWM9Uwy!d5@gNzk@8V=&u3f)*3LlL6dA*VU@#4L@kX!xzDP~?=xIwN6LTH!yF zdM&n!8n?+EbLM{BEqB|cw!f+2Exawem!+XTw(8g_t_U>_Nz!9vMcbnvA zi~P{qBVy>uiSmlwvxyNZL`EY^th!QdN1_<#!3P zm3OjtGDrO?Uv<-zSJ={%Y?&WnVzW-T@~XS0{LZU+`Wp8uO;bJORlf|!Tr<3?m%{ii zHL_iwt&$ylswabwKWlxe4@rFJ;j7-gYr{MH9y+{bw;h6fxn5>$lAkT|vz0$8ps9fl z1uWTS*r`Em&?W4S=}DT>@qIc3$9Ek=$8asLc6q9`PAv`-Op>&x^A0SwwpsxXx2zI^@_z=MJEMT=iV z^W?(_#|nAhjaSVa;~Blv7|qNY^HV}^6mzr>SC#Q9|8*}`yU_P>41AOic^JfB5e%G) z!R83~#E)Y3vLLVWwqWE=49Q+H)Lf!j6R7{GoXDzx(;H;x^637R(N+|+CJW%vmdqdIH8Uc=2xV!a5w7nd5+=xCHpFN;ldP_ z$@+@jXv)L+BROToB+p(%m+VCAq=GClY!p1;Bz>~st zfdVeMK82gin_E)&wt2HHg*(if@1$@~pupVdK2iI7X|)3dkI)v6>9VEh&-;nQp^DFw zY4a3}VDA^?x1H&jg>x_m^Vkb5;qN8y_!53(cnptIGXoc62Y$#1 zN>Mw(#hSm9^LKHiC-RPBgcsFEF~m#lc5|#iQKZ1O2YV^Z3A86TCJ^4vu>#&75??$? zU+i;3$cJb0xY_et1)vx9@%>Bu`)LZlFd_TRzP+d=GOyN^%hD8nC)d|fcwMeT(lmiHp5BLScWI)FKLh_`W*iSszixoMma;WPZ3U`%0D2)cQc7{XB#=go|j9rzD%7A!r;8`j%F@G}l5 ze2)L-K=3|}2?-ChBUDrTrETzj<5{@{YBxL&u(bI(D8Ehjw_wro$2r0M+&vWDe zUdD?qG9FBWbWjaer_kSnNm+}GV{ng6#to=fLkO3LlILKDDshp~n(vUY7xIsyG)+ba zOC?!YvAa2y;7j6ug7|rOtv0PcZBz!l-D+4mfA%romX6x3M&>gl79i z;1wo^S1|yuG1I?}k$3~6@g|SPZ<+RvwDwEeTdl^bGidJs(!jEJJR{0VK2D;s-Z{4U zppIfRZLM>Oga8ue)xMonbS z{J)HvXd1~g!rsmYR5$($*2C2yw6bz4eJ<5kt9)XegXI>Xsm zfCnTWk0wQ`7yoUp2qmgFN>wq&s6Lpe`tsvOKU3WjYxlIeH753K2Qi5eK8xDfg~Sri w57)NhQ+XAKXd`#1b1a=^XH+oT4rTQamPfSo7Mi_7xV6otmq#40|i zq$D)obgs(}LSsDH5!{8@s{jqOJMhpXgptnGvdUsrY-F4&LO(KzOn0)-mhVHiJn9Q! z6g~5p6N0s=ibQ>CyDfYu&a54VU>E)wxXC(j`b?r`z?)vt`sRUyfYV-sAG~mO>a)k) LWg@(CItc#(A>%?h literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/common/DateUtil.class b/backend-services/user-service/target/classes/com/ai/qa/user/common/DateUtil.class new file mode 100644 index 0000000000000000000000000000000000000000..c37637a5cd9c04452641fc0ff4c31f6195773464 GIT binary patch literal 293 zcmaKn&rZTX5XQeLEtSf@@Bl_{=)rve(2Imq0|%7*v}|xm+l^)GdvPN1-~;$jhFPLl zF5k>=CNuNR&+peafF}%N1caO1mWqw~VAbq(tprP3sTn){=1dVIBn)@_$*SOLtCrbL z=Z?^xn##CmLTh}#CWJ3`qZ72y=^{d#Fi1_M7fqSz`jxXn6Go}cxma^;JpUC#_ipxt z+w`)F2_ae8rq1=;_+7n!dyn23?jSzoCrBiJUo;j{X9l8&Bd!io2#|>SHe}#hw0q7Y Mi9I1gUo^nr1ba3=asU7T literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/common/JwtUtil.class b/backend-services/user-service/target/classes/com/ai/qa/user/common/JwtUtil.class new file mode 100644 index 0000000000000000000000000000000000000000..252c1b4eae9b09e5e2a9035bf9dad89069f49da9 GIT binary patch literal 4681 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 literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/domain/repository/UserRepository.class b/backend-services/user-service/target/classes/com/ai/qa/user/domain/repository/UserRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..c5410b7f549d958e2207ba934615cd6f15759960 GIT binary patch literal 1897 zcmbVNTT|0O6h2$PaubTkO+_P8E;_@G4>}GNXDF5tDWI*FC&yWur7onqCE1{T@rU`~ z5Aa7ho=uu2VB5*?(CNw9obUV2Wl#S6{q-9Fw%~CVG6ZN%H7FDGoKfH7F11yI2}xbv zRG!evZPAbDu5=!=Fi0SG$}Sjnn5@&I<5OPKS-3&qnR4sYYq~<#Ph8gE-<5ktZKfGL zZL%cd+a`+v35?abHYCN?o=sq7tsJA5m8=)mLx-=0LywwTD9M}xfsv}HOQwC76WC7` z^s?MNSuVBEVWNdsh`dno(bM3cgeQ(2zAdHF%)KYWzl8mRlWh7d6ZY0nE0fl4? zdhUShZ_4hVVoDPyLfXZa5hRg{z*@4JRc#vDs|g1wUrk&3VZWmy$A1V*`W6#5h#1d?FK}pP9|AfC@J2-6#%6;b~s*jnLj=g>O8b_$v&d?l1 zHi6;ToDYj~C~9Y>(K||NyzAIq0?+%B%65cR-rFx(`OWA#kMNOAV6vw0-V|FAK7r*f z#cJQ?pkKlDCNLf9+Ox4u@f5yD1_y7ZZqk=)hNf-NXjS=cjqeHvZ_Bii9PhIYv%LuX zOo$D4WTE$!?YAG(Xi9Jeg)&R*Nmn`#7YSFo)#NmmT)^c`U;uwZ_+`ir63D_Z48jOL zWndJ>@P7Tza6?3 z;7*6@?p3aP0oPn6=9-Po#QH=a bmwd#BX$G7Fu!2?wR@=$01&lWkV;$fhS0^JR literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/config/JwtAuthenticationFilter.class b/backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/config/JwtAuthenticationFilter.class new file mode 100644 index 0000000000000000000000000000000000000000..879479fd5147345563961c91d0b8a097cc6a0fe5 GIT binary patch literal 4602 zcmbVQ`+FQ!8GcW0v)N4AG_=CfQc^9Dw7{UHLN`sJNgJ{?P0}V!ZMjZ%Pd3xc&TMDS zCJmRWsGuTVQ7GVD1n(9xC9UF}K9B!^KkIMuFX*GbXJ!+2bBCwdxt#gVcX_|->EHf) z`7Hol_^W{$f&E#(VA+m!*0xHa3@pbR5A09{rK~Ci(#rbYxRbYzOsj(>H6cCaWNqd6 zUa#XS85pP&*y9IzD=Y?%m*>`koc4oBYg&$3;}MND=w;=Q435jQB^j!CqkwT{TAfgi zD{y1~A{F>r@I5V46Lbosj+ZF7AV;0h8FS@9&+}EJMJRB0|8nBTr0s>)xSdsgFk{&Z z8!TGkPdkCkk!`Cs_>fe#VQCPd&j6iU5}rWNz`Jai6)o=8~Ytk4wMRGG8nOG5q0VJv$i{G2abL(FV?9EhbA6a z4A6V>5!GqQ`7!Jnk52P!f?jCvKVwhX=Par}!gjn)4MFnoxvVVe$O#QxC$PO*dRgcKk%8+4uF*7><6D`*g%3^GhV2G!5V)zD z!wUMFxDjmv8}d?>WenMBLSS=yM`W4h+FsroRyuk+8}UKxH1HuJ7Kx6(&4fx8Ox%Q< z=}*6;1h&Lmso4Ca{+=Yq@^j z@shX;`wZM&jX{Kui8L5Z3F+lRt^W$_PAAYs1gt>j9YPl*a1T`&92&_C4h(Dd2TgQq zMlGxewi{;h9+Rra4rLP#q1V7+W_-1CE27E7edwbS^C2y8@8VEiJ%Sg$mIGIte?*{J z*Ec_K9*qi%_GQ0`qZo*N*(dFs3flrycD)Af~J0r6yt|W0Lj7$Xnv@VFO zAxXIK3={@1Z?;VR0LlOp zdEfhHVxoj8flbt^y44EYxi00Zcd?iWoHNmgWD_36;|3mMs;sTu>b=dx6FARq<@$C` zM^*RCd~68}R{FABQP%_N3KBbYpnuZDr*z&lM=@tB8HKNbPgC5gw%AqEO4Oxw1`OF@ z$oV8!ja9_737^H&2A*1%4_DCH#OLt&xZ7w6EGNp5jEd{#yG@A$jn(73E(4vtkTyPv zXY^3;MIC|_g=A?iRIM!L(=lq`O9BI{VxniVMYx=lW7Sa1k3)Vb$Z}+P78~{Dt0umN zuM3!CCCAN`X#~PpA014HMtQ^SYW!wV+H@Yb%4*F{(53f{7RL5>t!h z)z%8^ZC^*)S8rl1GVrbWh8xWY3*!l^eh|;O0=Go&TpJuVvp~kpNMLP5%WX@bX*`;* zqZvzJ>mq3tEtC0ESB$2f)`|!#XDCtBF)`oZ>Z6iQm2`ybj1dt0l_#ERW68HklP$QQ za~)W&kU8YlsP%|aQQ+9>oHXUTXOZ<}H~BXYD?2-R)GkKTwSkLl=hb7z(iw8zEiLgd zH)QJr4rdzXbaFV#5#mgIwykhLx?Kxkn_J!-GLpWUDN2@v0>@~YL_J%&eP3}Ju#2(K zFo`#D*}$B@&UeRB$D3mFEl;j&W1J~_4w(AZ%N_K8+z@-G<64*h|;xJ9OF^u%7Ll5U& zqKz1HbnX(YI2q5AL66J2EqQYIV|nh6$Ac>6s;x5c=kg(AWm3*QF$CDbURldQy@vld zV)4!3o4zNaRW{KjYYcvvTx}+W#k=6We%B>zxEO6|;Z-vLC&^8`YElxQ71!`yht1eR zkssi51BG6Tlx9@3g%nAyy|a4`TLsc}uVY85jxV((zI6^gwYa#uH8Y2g)gWCj(hVYQr0UYD7;4F?^Ku4;+#hArN zx?$HF_;{*e7N^t3t7uIp`2KKfHkFuztVPOr8zXOGat>z&t|G&a($y2F>(Az#Ea0y>XH>Mh|;Mr4`@a5gJ_{uDvBb!v?9A2owf{w(+!weqB%fv&Y zHcl+S6G8$#*os~ryzV2wef%Gb3=ZQ6SB_!`101LaIXxeP#Bp*MA&(O{&y{Cz8ZU8n zcnuHW4Lr!?c?f^v{VE>D-|-0kfgJwDUD_)}I+19Xy`rDKjqgOBdI#Ud_vjhD_I><- zx+U;W{1C6ucSGzaKjKq8b$A}H@~MGxpQFTA7&zzY$sgk;3D7k+M<)Q(ZNl=7B+Eu?`~FD-kZG;s|{ahfzvTLNW~Ram%__cP+b2lWTI(ai7tP(y%>!!7zQtUDl124La>|7A;qnD0cn*mTmd_ z1lA4jJSxzibxJaWb=aU`5E+3@dCQiE8nvQy=M3r<7|J`QQ9Ww7mKwJv`~8YVL+)Ig zm#ZP96`SgY=h5g=&NgWP@!CJUGtnupg}!dO%XQz;+Z*+n)ect*yINAqUSgQZEt#^ei0GwomB7}B=__9{xCYmyaIHYOJAD6` za~iJ44FcQCGRUz6pmV+Gvqt~dhei-;*@3#mCCFhOsuVKiiGtbTQMwK z;KlQ%)1oZu#fDX7j(Pfo@7D{heCov)wXwZ6X2WO(J8^3YBLWlW8T@maqTx2|Vkt5` zcR}EAuSkXwis!SE9L}(Dg0Yt>HuvZ{evb?ZT9=tHq z<6#*OV->)Br-r+5x4;%pdLA<}%+ea!FYx4rsfs&O=#(0RY!LT?74klwV{3t|LSm6E zft6WT2E}T|saoc;hWqh=z+ho+Y%Z726$-4mYs##1izGG=Vl0LI0*|f5xL9%RBuWjw5!rsDuwO0b)t}PS0c+sO?hmq>W@-D^)5al#Hgok<18+J)x{QL>iPX9P3Cp8?z zBjlv2r;ckqD`(}&hV*>atXI!pMQ1>AOLaG-(Gj<*&M2iKpi@N7o1#UNJlb7#UUBuT zEL-f|mPZFMh3OOy3EY@Ok02xyBy3o8HO$~JlX!Y&ZgTn%>A543o*lcG8(bP|0}HCFcuK?5ijaeK>DDZNtXfs^FKRGVSo>$D z3v+3(i`}Ffbt`Q1t4`Un(+IVb23vQ%B8{qFNrPp6V>HXliIc`D z0WF$SHY^DYj2)PoJfzTgR>O06o^-Al-mIg#_s@r8;aZ8{#Ryfz+{83hDe_+p|HJ>; zbsF|^tBwd<$};jdhAHP>)bNrD^@dn4Cre7sE2`A(9fZJZ8V0aFg*OD|x;m*UPfQMv zSN#a@fHpg@c8jmL7PXpT6QtUow=}$scgP2$R0?Ou<$~i9Y%c_RNvEV!Rq5;D=h2uy z*(&cXy_&D&;-B2dDU06I@V>%vAXt*ZhXQx>^uAr*W8EDSMil^ogNa43N7+iYkI^kS z4OVIDAw*l-YQuuQTSZ#nu0(x=q4Vn2J_dp_f%lcU#Em@+4dHEHs&cmK-j2y)+cJvP zCT&}~L98S%LSG4(UGWShb~~-3Y&drvC$(4AH-T%SZAnSQYZ1>I{zpTgPw}4b6i4cv z=DVM7Uh4V0gd?4A!S}$(&(KcuM;|uwsTKp(Fb@|zv*YHc~GzIoAfOMIg> zGql*maufdv_Po%<%d}(UC%iiJdJ}Ip@$OlC5Mi<}K;uC=yPwe=W01xPZWdSJ0B%PP zhgl74co;8Z0v}=$U$JNV4v*k_z@m*zy+`UQ4zB literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/config/SwaggerConfig.class b/backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/config/SwaggerConfig.class new file mode 100644 index 0000000000000000000000000000000000000000..590c39a3caa65be4222cb0f0ec224960720176de GIT binary patch literal 2677 zcmbtWTT>iG6#kmq!e(NUNibkYvI&qZF=2p^1R;qbD6+{WAiEL~qYg{6JFq*m^z=Yb zgZh%vDocIP2OllVs=R1Rf&cz*(Hr%|h>@<;+<4ca1SIkI$Pa5W? zs&Sp0_CN>+86y8>G7KFJAw0`)vaBkF!ZBjhgwcqGdNdP-8(%(L|9Exdhi^8%ctHLs z$q@}LILc5fEvZQDi^jKNlaLDaKm`sqtqj!TIK%0Zg|7>1kG}fz!;jW~dARoAQyK&| zH@^R5{j=}ZKL1u(KB3{Hvb<-8yD4Gi6<_seBPiXSU7cM@<&=igN+q<&g-c@PcZY^E zc%BkOnPwP?mJiStxC$OTj$#Ps2!B~RDZ3R2BT?+bIIB4AVra}rneR7@tjfiVBeJnQ6h+?Ek;FIvK})mRzpQ)Kpr(T~d^ zP(}s?=1D7S3<_Z`yBgIj6<3|<#x0l1tvx!mEzm&`QlN{mFD_GR1B&o#8U}Hlf>6Rl z#Z~fUOK(T~zQk~$0>3FwSaO*N4hG|V!LtMp92`mJ^a`9D$*0Nj;Ik~)J zQ{>$+#?(QOWauiZ{S=r)Dnqn}kwAlL?r3-&lT=VP9Zn3x|C7OB_kjjX;cf_T?keWN z+P36!o@;muZxg7IWO9t*Qu!h;<6dzS3Sk1KhH0c|D@ofj%{+W=LHTNw;gwq&p_ z$XNv?ts$eRu5*hasK*=~4>#^4l`N|vr({~7KJ18`DeFN;ToY+TOiFo*xRom2D-r3; zb2<(aSWtyOsq%nIT*9Mkaa*be_~{lRub(>Bcg<-f3bt~xST{==?qQhc4h|FAIN^9A#fPopOW=tYMyK+W;o?p&-r!y0i6XOfc4{%(rq@c=bX`u=)u?pD z2~eo$yp9%{8ff%?5PV|WJpXp{XVtpn7n zjs7aFSt&rDT*aXR8mT+1H~Dx@KjtH%xB6)J5v_TIvy;^YYF5#y8$P=A3qCF-e#Hwu zUMygU{=cjb`xx;NFJO$O}Oh+$G60UD+8#DVaMU=&D|# m|L>CgKJB%|#!88eYLY37?``t&KF#mb7bd2rD8W~*HU9(Th#6}D literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/persistent/UserMapper.class b/backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/persistent/UserMapper.class new file mode 100644 index 0000000000000000000000000000000000000000..81c93d2227f2a8bf9657edbfeb0d58124293cde1 GIT binary patch literal 337 zcmbV{Jx;?w5QX13b_^!qh#SxW1zR90ej21y1_~hkcv)a0XC3y3dr=`#Z~zX47)MC- zG;ij;(P+N;`u_L?aE@t-jBu>`rDCgISfyaRvhLAyh(76&hUs-kRp43gdFgg;(XV%$xK^W~H%nA8r|6nSNusK12F`;g(GuLU^8h^)aX9&AZ zueqDEw}bx;^Z0B7;iUNo+9{#B?UUDLW`_gyANA1yj6DdR{12%#V2PIE)Y=2Z;X5|o ZB*;*S4r9o|mgr>N7gD#y5q3l~)ISrXQbqs( literal 0 HcmV?d00001 diff --git a/backend-services/user-service/target/classes/sql/init.sql b/backend-services/user-service/target/classes/sql/init.sql new file mode 100644 index 00000000..0d6d22a2 --- /dev/null +++ b/backend-services/user-service/target/classes/sql/init.sql @@ -0,0 +1,36 @@ +-- 创建一个名为 'ai_qa_system' 的数据库,如果它不存在的话 +CREATE DATABASE IF NOT EXISTS `ai_qa_system` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- 切换到该数据库 +USE `ai_qa_system`; + +-- ---------------------------- +-- 用户表 (user) +-- ---------------------------- +DROP TABLE IF EXISTS `user`; +CREATE TABLE `user` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `username` VARCHAR(255) NOT NULL COMMENT '用户名', + `password` VARCHAR(255) NOT NULL COMMENT '加密后的密码', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_username` (`username`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; + +-- ---------------------------- +-- 问答历史表 (qa_history) (可选,用于功能扩展) +-- ---------------------------- +DROP TABLE IF EXISTS `qa_history`; +CREATE TABLE `qa_history` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `question` TEXT NOT NULL COMMENT '用户提出的问题', + `answer` LONGTEXT COMMENT 'AI返回的回答', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='问答历史表'; + +-- 插入一些测试数据 (可选) +INSERT INTO `user` (`username`, `password`) VALUES ('testuser', '$2a$10$abcdefghijklmnopqrstuv'); -- 密码是加密的,请通过注册接口创建用户 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..9f8dc42c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,91 @@ +services: + # MySQL 数据库 + database: + image: mysql:8.0 + container_name: ai-qa-mysql + restart: always + environment: + # MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + # MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_ROOT_PASSWORD: ai_qa_system + MYSQL_DATABASE: ai_qa_system + # ✅ 正确:删除了 MYSQL_USER 和 MYSQL_PASSWORD + ports: + - "3306:3306" + volumes: + - db_data:/var/lib/mysql + networks: + - ai-qa-network + + # User Service - 使用 root 用户连接 + user-service: + image: chl/ai-qa-user-service:latest + container_name: ai-qa-user-service + restart: always + ports: + - "8081:8081" + depends_on: + - database + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://database:3306/ai_qa_system?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true + SPRING_DATASOURCE_USERNAME: root # 修改:直接写 root,不要用变量 + SPRING_DATASOURCE_PASSWORD: ai_qa_system # 使用 root 密码 + networks: + - ai-qa-network + + # QA Service - 使用 root 用户连接 + qa-service: + image: chl/ai-qa-qa-service:latest + container_name: ai-qa-qa-service + restart: always + ports: + - "8082:8082" + depends_on: + - database + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://database:3306/ai_qa_system?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true + SPRING_DATASOURCE_USERNAME: root # 修改:直接写 root,不要用变量 + SPRING_DATASOURCE_PASSWORD: ai_qa_system # 使用 root 密码 + DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} + networks: + - ai-qa-network + + # API Gateway + api-gateway: + image: chl/ai-qa-api-gateway:latest + container_name: ai-qa-api-gateway + restart: always + ports: + - "8080:8080" + depends_on: + - user-service + - qa-service + environment: + NEXT_PUBLIC_API_BASE_URL: http://16.170.233.101:8080 + USER_SERVICE_URL: http://user-service:8081 + QA_SERVICE_URL: http://qa-service:8082 + networks: + - ai-qa-network + + # Frontend + frontend: + image: chl/ai-qa-frontend:latest + container_name: ai-qa-frontend + restart: always + ports: + - "3000:3000" + depends_on: + - api-gateway + environment: + NEXT_PUBLIC_API_BASE_URL: http://16.170.233.101:8080 + networks: + - ai-qa-network + +# 添加:声明 volumes 部分 +volumes: + db_data: + +# 添加:声明 networks 部分 +networks: + ai-qa-network: + driver: bridge diff --git a/frontend-nextjs/frontend/.dockerignore b/frontend-nextjs/frontend/.dockerignore new file mode 100644 index 00000000..2397d55e --- /dev/null +++ b/frontend-nextjs/frontend/.dockerignore @@ -0,0 +1,52 @@ +# 依赖目录 +node_modules/ +.npm +.pnpm-store + +# 构建输出 +.next/ +dist/ +out/ + +# 环境变量文件 +.env* +!.env.example + +# 日志文件 +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# 运行时数据 +pids +*.pid +*.seed +*.pid.lock + +# 覆盖文件 +*.tgz +*.tar.gz + +# IDE 文件 +.vscode/ +.idea/ +*.swp +*.swo + +# OS 文件 +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# 测试文件 +coverage/ +.jest/ + +# Docker 相关 +Dockerfile* +docker-compose* +.dockerignore \ No newline at end of file diff --git a/frontend-nextjs/frontend/Dockerfile b/frontend-nextjs/frontend/Dockerfile new file mode 100644 index 00000000..6100e405 --- /dev/null +++ b/frontend-nextjs/frontend/Dockerfile @@ -0,0 +1,54 @@ +FROM node:18-alpine + +WORKDIR /app + +# 安装 pnpm +RUN npm install -g pnpm + +# 复制包管理文件 +COPY package.json pnpm-lock.yaml* ./ + +# 安装依赖 +RUN pnpm install --frozen-lockfile + +# 复制源代码 +COPY . . + +# 详细调试信息 +RUN echo "=== 当前目录结构 ===" +RUN ls -la + +RUN echo "=== lib 目录内容 ===" +RUN ls -la lib/ + +RUN echo "=== 检查 utils.ts 是否存在 ===" +RUN find . -name "utils.ts" -type f + +RUN echo "=== 检查 tsconfig.json 路径配置 ===" +RUN cat tsconfig.json | grep -A 5 -B 5 "paths" + +RUN echo "=== 检查导入语句 ===" +RUN grep -n "from.*@/lib/utils" components/ui/toast.tsx + +RUN echo "=== 尝试直接导入测试 ===" +RUN node -e "console.log('测试导入:', require.resolve('./lib/utils.ts'))" || echo "直接导入失败" + +# 构建应用 +RUN pnpm build + +EXPOSE 3000 +CMD ["pnpm", "start"] + +# FROM node:18-alpine +# WORKDIR /app +# COPY . . + +# # 必须使用 ARG 和 ENV 组合 +# ARG NEXT_PUBLIC_API_BASE_URL +# ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL} + +# RUN npm install +# RUN npm run build + +# EXPOSE 3000 +# CMD ["npm", "start"] \ No newline at end of file diff --git a/frontend-nextjs/frontend/app/api/hello/route.ts b/frontend-nextjs/frontend/app/api/hello/route.ts index 482825bb..5a44a880 100644 --- a/frontend-nextjs/frontend/app/api/hello/route.ts +++ b/frontend-nextjs/frontend/app/api/hello/route.ts @@ -1,11 +1,5 @@ -// 文件路径: app/api/hello/route.ts -import { NextResponse } from 'next/server'; +import { NextResponse } from "next/server"; -export async function GET(request: Request) { - // 在这里可以进行数据库查询等后端操作 - const data = { message: 'Hello from the Server-side API!' }; - - // 返回 JSON 响应 - return NextResponse.json(data); +export async function GET() { + return NextResponse.json({ message: "Hello World!" }); } - diff --git a/frontend-nextjs/frontend/app/hello/page.tsx b/frontend-nextjs/frontend/app/hello/page.tsx index 5b145053..7a1f1026 100644 --- a/frontend-nextjs/frontend/app/hello/page.tsx +++ b/frontend-nextjs/frontend/app/hello/page.tsx @@ -1,5 +1,3 @@ -import Image from "next/image"; - export default function Home() { return (
diff --git a/frontend-nextjs/frontend/app/page.tsx b/frontend-nextjs/frontend/app/page.tsx index 213c0d05..1488c61b 100644 --- a/frontend-nextjs/frontend/app/page.tsx +++ b/frontend-nextjs/frontend/app/page.tsx @@ -1,63 +1,247 @@ -"use client" +"use client"; -import { useState, useCallback } from "react" -import { Sidebar } from "@/components/sidebar" -import { ChatWindow } from "@/components/chat-window" -import { ProtectedRoute } from "@/components/auth/protected-route" -import type { Conversation } from "@/types/chat" +import { useState, useCallback, useEffect } from "react"; +import { Sidebar } from "@/components/sidebar"; +import { ChatWindow } from "@/components/chat-window"; +import { ProtectedRoute } from "@/components/auth/protected-route"; +import type { Conversation, HistoryConversation } from "@/types/chat"; +import { useAuth } from "@/contexts/auth-context"; +import { chatAPI } from "@/lib/chat-api"; +import { generateUniqueId } from "@/lib/utils"; +// 主页组件,包含聊天界面和侧边栏 function HomePage() { - const [conversations, setConversations] = useState([]) - const [activeConversationId, setActiveConversationId] = useState() + const [conversations, setConversations] = useState([]); + const [activeConversationId, setActiveConversationId] = useState< + string | undefined + >(); + const [isLoadingHistory, setIsLoadingHistory] = useState(false); + const { user, token } = useAuth(); + // 添加调试日志 + useEffect(() => { + console.log("当前活动对话ID:", activeConversationId); + console.log( + "当前活动对话:", + conversations.find((conv) => conv.id === activeConversationId) + ); + console.log("所有对话数量:", conversations.length); + }, [activeConversationId, conversations]); + + /** + * 将历史对话转换为当前对话格式 + */ + const convertHistoryToConversation = useCallback( + (history: HistoryConversation): Conversation => { + const conversationId = history.sessionId || history.id; + + return { + id: conversationId, + title: history.question + ? history.question.substring(0, 30) + + (history.question.length > 30 ? "..." : "") + : "历史对话", + messages: [ + { + id: `${history.id}_user`, + role: "user" as const, + content: history.question || "", + timestamp: new Date(history.createTime || Date.now()), + }, + { + id: `${history.id}_assistant`, + role: "assistant" as const, + content: history.answer || "", + timestamp: new Date(history.createTime || Date.now()), + }, + ], + createdAt: new Date(history.createTime || Date.now()), + updatedAt: new Date(history.createTime || Date.now()), + isHistory: true, // 标记为历史对话 + }; + }, + [] + ); + + /** + * 加载用户历史对话 + */ + const loadUserHistory = useCallback(async () => { + if (!user?.id || !token) return; + + try { + setIsLoadingHistory(true); + const historyConversations = await chatAPI.getUserConversations( + user.id, + token + ); + + console.log("Loaded history conversations:", historyConversations); + + if (!historyConversations || historyConversations.length === 0) { + console.log("No history conversations found"); + return; + } + + // 转换历史对话数据 + const convertedConversations: Conversation[] = historyConversations.map( + convertHistoryToConversation + ); + + setConversations((prev) => { + // 创建映射来去重和合并对话 + const conversationMap = new Map(); + + // 先添加历史对话(服务器数据优先) + convertedConversations.forEach((conv) => { + conversationMap.set(conv.id, conv); + }); + + // 再添加现有对话,但不覆盖历史对话 + prev.forEach((conv) => { + if (!conversationMap.has(conv.id)) { + conversationMap.set(conv.id, conv); + } + }); + + // 转换为数组并按更新时间排序 + const mergedConversations = Array.from(conversationMap.values()).sort( + (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime() + ); + + console.log("Merged conversations:", mergedConversations); + return mergedConversations; + }); + + // 移除自动设置第一个历史对话为活动对话的逻辑 + // 这样登录后会保持空会话状态 + console.log("历史对话已加载,但未设置活动对话"); + } catch (error) { + console.error("Failed to load conversation history:", error); + } finally { + setIsLoadingHistory(false); + } + }, [user?.id, token, convertHistoryToConversation]); + + /** + * 处理加载历史对话(侧边栏回调) + */ + const handleLoadConversations = useCallback( + (loadedConversations: HistoryConversation[]) => { + // 如果通过侧边栏加载了对话,转换为当前格式并合并 + if (loadedConversations && loadedConversations.length > 0) { + const convertedConversations: Conversation[] = loadedConversations.map( + convertHistoryToConversation + ); + + setConversations((prev) => { + const conversationMap = new Map(); + + // 先添加现有对话 + prev.forEach((conv) => conversationMap.set(conv.id, conv)); + + // 添加新加载的对话,不覆盖现有对话 + convertedConversations.forEach((conv) => { + if (!conversationMap.has(conv.id)) { + conversationMap.set(conv.id, conv); + } + }); + + return Array.from(conversationMap.values()).sort( + (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime() + ); + }); + } + }, + [convertHistoryToConversation] + ); + + /** + * 根据第一条消息内容生成对话标题 + */ const generateConversationTitle = (firstMessage: string): string => { - const title = firstMessage.length > 30 ? firstMessage.substring(0, 30) + "..." : firstMessage - return title || "新对话" - } + const title = + firstMessage.length > 30 + ? firstMessage.substring(0, 30) + "..." + : firstMessage; + return title || "新对话"; + }; + /** + * 处理新建聊天 + */ const handleNewChat = useCallback(() => { const newConversation: Conversation = { - id: crypto.randomUUID(), + id: generateUniqueId("conv"), title: "新对话", messages: [], createdAt: new Date(), updatedAt: new Date(), - } + }; + + setConversations((prev) => { + // 确保新对话的ID不重复 + const finalId = prev.some((conv) => conv.id === newConversation.id) + ? generateUniqueId("conv") + : newConversation.id; - setConversations((prev) => [newConversation, ...prev]) - setActiveConversationId(newConversation.id) - }, []) + return [{ ...newConversation, id: finalId }, ...prev]; + }); + setActiveConversationId(newConversation.id); + }, []); + /** + * 处理选择对话 + */ const handleSelectConversation = useCallback((conversationId: string) => { - setActiveConversationId(conversationId) - }, []) + console.log("选择对话:", conversationId); + setActiveConversationId(conversationId); + }, []); + /** + * 处理删除对话 + */ const handleDeleteConversation = useCallback( (conversationId: string) => { - setConversations((prev) => prev.filter((conv) => conv.id !== conversationId)) + setConversations((prev) => + prev.filter((conv) => conv.id !== conversationId) + ); if (activeConversationId === conversationId) { - setActiveConversationId(undefined) + setActiveConversationId(undefined); } }, - [activeConversationId], - ) + [activeConversationId] + ); - const handleRenameConversation = useCallback((conversationId: string, newTitle: string) => { - setConversations((prev) => - prev.map((conv) => (conv.id === conversationId ? { ...conv, title: newTitle, updatedAt: new Date() } : conv)), - ) - }, []) + /** + * 处理重命名对话 + */ + const handleRenameConversation = useCallback( + (conversationId: string, newTitle: string) => { + setConversations((prev) => + prev.map((conv) => + conv.id === conversationId + ? { ...conv, title: newTitle, updatedAt: new Date() } + : conv + ) + ); + }, + [] + ); + /** + * 处理新增消息 + */ const handleMessageAdded = useCallback( (message: { role: "user" | "assistant"; content: string }) => { - if (!activeConversationId) return + if (!activeConversationId) return; const newMessage = { - id: crypto.randomUUID(), + id: generateUniqueId("msg"), role: message.role, content: message.content, timestamp: new Date(), - } + }; setConversations((prev) => prev.map((conv) => @@ -67,18 +251,21 @@ function HomePage() { messages: [...conv.messages, newMessage], updatedAt: new Date(), } - : conv, - ), - ) + : conv + ) + ); }, - [activeConversationId], - ) + [activeConversationId] + ); + /** + * 处理第一条消息 + */ const handleFirstMessage = useCallback( (content: string) => { - if (!activeConversationId) return + if (!activeConversationId) return; - const title = generateConversationTitle(content) + const title = generateConversationTitle(content); setConversations((prev) => prev.map((conv) => conv.id === activeConversationId @@ -87,17 +274,26 @@ function HomePage() { title, updatedAt: new Date(), } - : conv, - ), - ) + : conv + ) + ); }, - [activeConversationId], - ) + [activeConversationId] + ); + + // 在组件挂载时加载历史对话 + useEffect(() => { + loadUserHistory(); + }, [loadUserHistory]); - const activeConversation = conversations.find((conv) => conv.id === activeConversationId) + // 根据活动对话ID查找当前活动对话 + const activeConversation = conversations.find( + (conv) => conv.id === activeConversationId + ); return ( -
+
+ {/* 侧边栏组件 */} -
+ {/* 主聊天区域 */} +
- ) + ); } +// 受保护的主页组件(需要认证才能访问) export default function ProtectedHomePage() { return ( - ) + ); } diff --git a/frontend-nextjs/frontend/components/auth/login-form.tsx b/frontend-nextjs/frontend/components/auth/login-form.tsx index a7327e50..6582fa2d 100644 --- a/frontend-nextjs/frontend/components/auth/login-form.tsx +++ b/frontend-nextjs/frontend/components/auth/login-form.tsx @@ -1,36 +1,44 @@ -"use client" +"use client"; -import type React from "react" +import type React from "react"; -import { useState } from "react" -import { useRouter } from "next/navigation" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { useAuth } from "@/contexts/auth-context" -import { Eye, EyeOff, MessageSquare } from "lucide-react" +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { useAuth } from "@/contexts/auth-context"; +import { Eye, EyeOff, MessageSquare } from "lucide-react"; interface LoginFormProps { - onSwitchToRegister: () => void + onSwitchToRegister: () => void; } export function LoginForm({ onSwitchToRegister }: LoginFormProps) { - const [username, setUsername] = useState("") - const [password, setPassword] = useState("") - const [showPassword, setShowPassword] = useState(false) - const { login, isLoading } = useAuth() - const router = useRouter() + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const { login, isLoading } = useAuth(); + const router = useRouter(); const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() + e.preventDefault(); + console.log("Form submitted:", { username, password }); try { - await login({ username, password }) - router.push("/") + await login({ username, password }); + console.log("Login successful, redirecting..."); + router.push("/"); } catch (error) { - // Error is handled by the auth context + console.error("Login failed:", error); } - } + }; return ( @@ -78,7 +86,11 @@ export function LoginForm({ onSwitchToRegister }: LoginFormProps) { onClick={() => setShowPassword(!showPassword)} disabled={isLoading} > - {showPassword ? : } + {showPassword ? ( + + ) : ( + + )}
@@ -91,12 +103,17 @@ export function LoginForm({ onSwitchToRegister }: LoginFormProps) {

还没有账户?{" "} -

- ) + ); } diff --git a/frontend-nextjs/frontend/components/auth/register-form.tsx b/frontend-nextjs/frontend/components/auth/register-form.tsx index 2ddc04af..83c71e34 100644 --- a/frontend-nextjs/frontend/components/auth/register-form.tsx +++ b/frontend-nextjs/frontend/components/auth/register-form.tsx @@ -1,44 +1,51 @@ -"use client" +"use client"; -import type React from "react" +import type React from "react"; -import { useState } from "react" -import { useRouter } from "next/navigation" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { useAuth } from "@/contexts/auth-context" -import { Eye, EyeOff, MessageSquare } from "lucide-react" +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { useAuth } from "@/contexts/auth-context"; +import { Eye, EyeOff, MessageSquare } from "lucide-react"; interface RegisterFormProps { - onSwitchToLogin: () => void + onSwitchToLogin: () => void; } export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) { - const [username, setUsername] = useState("") - const [email, setEmail] = useState("") - const [password, setPassword] = useState("") - const [confirmPassword, setConfirmPassword] = useState("") - const [showPassword, setShowPassword] = useState(false) - const [showConfirmPassword, setShowConfirmPassword] = useState(false) - const { register, isLoading } = useAuth() - const router = useRouter() + const [username, setUsername] = useState(""); + const [nickname, setNickname] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const { register, isLoading } = useAuth(); + const router = useRouter(); const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() + e.preventDefault(); if (password !== confirmPassword) { - return + return; } try { - await register({ username, email, password }) - router.push("/") + await register({ username, nickname, email, password, confirmPassword }); + router.push("/"); } catch (error) { - // Error is handled by the auth context + console.error("Register failed:", error); } - } + }; return ( @@ -66,6 +73,19 @@ export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) { />
+
+ + setNickname(e.target.value)} + required + disabled={isLoading} + /> +
+
setShowPassword(!showPassword)} disabled={isLoading} > - {showPassword ? : } + {showPassword ? ( + + ) : ( + + )}
@@ -124,13 +148,23 @@ export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) { onClick={() => setShowConfirmPassword(!showConfirmPassword)} disabled={isLoading} > - {showConfirmPassword ? : } + {showConfirmPassword ? ( + + ) : ( + + )} - {password !== confirmPassword && confirmPassword &&

密码不匹配

} + {password !== confirmPassword && confirmPassword && ( +

密码不匹配

+ )} - @@ -138,12 +172,17 @@ export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) {

已有账户?{" "} -

- ) + ); } diff --git a/frontend-nextjs/frontend/components/chat-header.tsx b/frontend-nextjs/frontend/components/chat-header.tsx index 80e0ccad..9b2cba16 100644 --- a/frontend-nextjs/frontend/components/chat-header.tsx +++ b/frontend-nextjs/frontend/components/chat-header.tsx @@ -1,7 +1,7 @@ -import { MessageSquare } from "lucide-react" +import { MessageSquare } from "lucide-react"; interface ChatHeaderProps { - title?: string + title?: string; } export function ChatHeader({ title }: ChatHeaderProps) { @@ -9,8 +9,10 @@ export function ChatHeader({ title }: ChatHeaderProps) {
-

{title || "AI 聊天助手"}

+

+ {title || "AI 聊天助手"} +

- ) + ); } diff --git a/frontend-nextjs/frontend/components/chat-window.tsx b/frontend-nextjs/frontend/components/chat-window.tsx index 93e875d7..579ff4c7 100644 --- a/frontend-nextjs/frontend/components/chat-window.tsx +++ b/frontend-nextjs/frontend/components/chat-window.tsx @@ -1,25 +1,40 @@ -"use client" +import { Button } from "@/components/ui/button"; +import { useAuth } from "@/contexts/auth-context"; +import { QARequest } from "@/types/qa"; +import { Bot, Sparkles } from "lucide-react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { v4 as uuidv4 } from "uuid"; +import { ChatHeader } from "./chat-header"; +import { ChatInput } from "./chat-input"; +import { MessageBubble } from "./message-bubble"; + +const API_BASE_URL = + process.env.NEXT_PUBLIC_API_BASE_URL || "http://16.170.233.101:8080"; + +// const API_BASE_URL = +// process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8080"; -import { useChat } from "@ai-sdk/react" -import { DefaultChatTransport } from "ai" -import { MessageBubble } from "./message-bubble" -import { ChatInput } from "./chat-input" -import { ChatHeader } from "./chat-header" -import { useEffect, useRef, useCallback } from "react" -import { Bot, Sparkles } from "lucide-react" -import { useAuth } from "@/contexts/auth-context" interface ChatWindowProps { - conversationId?: string - conversationTitle?: string + conversationId?: string; + conversationTitle?: string; initialMessages?: Array<{ - id: string - role: "user" | "assistant" - content: string - timestamp: Date - }> - onMessageAdded?: (message: { role: "user" | "assistant"; content: string }) => void - onFirstMessage?: (content: string) => void + id: string; + role: "user" | "assistant"; + content: string; + timestamp: Date; + }>; + onMessageAdded?: (message: { + role: "user" | "assistant"; + content: string; + }) => void; + onFirstMessage?: (content: string) => void; +} + +interface CustomMessage { + id: string; + role: "user" | "assistant"; + content: string; } export function ChatWindow({ @@ -29,81 +44,241 @@ export function ChatWindow({ onMessageAdded, onFirstMessage, }: ChatWindowProps) { - const messagesEndRef = useRef(null) - const { token } = useAuth() - - const { messages, sendMessage, status } = useChat({ - transport: new DefaultChatTransport({ - api: "/api/chat", - headers: token ? { Authorization: `Bearer ${token}` } : undefined, - }), - initialMessages: initialMessages.map((msg) => ({ + const messagesEndRef = useRef(null); + const { token, user } = useAuth(); + const [isLoading, setIsLoading] = useState(false); + + // 生成会话ID的函数 + const generateSessionId = useCallback((): string => { + if (typeof uuidv4 === "function") { + return uuidv4(); + } + return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + }, []); + + // 会话ID状态管理 + const [currentSessionId, setCurrentSessionId] = useState( + conversationId || "" + ); + + // 初始化或重置会话ID + useEffect(() => { + if (!conversationId && !currentSessionId) { + const newSessionId = generateSessionId(); + setCurrentSessionId(newSessionId); + console.log("生成新的会话ID:", newSessionId); + } else if (conversationId && conversationId !== currentSessionId) { + setCurrentSessionId(conversationId); + console.log("更新会话ID:", conversationId); + } + }, [conversationId, currentSessionId, generateSessionId]); + + // 消息状态管理 - 关键修复:正确响应 initialMessages 变化 + const [messages, setMessages] = useState([]); + + // 当 initialMessages 或 conversationId 变化时,更新消息状态 + useEffect(() => { + console.log("ChatWindow: initialMessages 发生变化", { + conversationId, + initialMessagesCount: initialMessages.length, + initialMessages, + }); + + const convertedMessages = initialMessages.map((msg) => ({ id: msg.id, - role: msg.role, + role: msg.role as "user" | "assistant", content: msg.content, - })), - onFinish: (message) => { - if (onMessageAdded) { - onMessageAdded({ role: "assistant", content: message.content }) - } - }, - }) + })); + + setMessages(convertedMessages); + console.log("ChatWindow: 更新消息状态", convertedMessages); + }, [initialMessages, conversationId]); - // Auto-scroll to bottom when new messages arrive useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) - }, [messages]) + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); const handleSendMessage = useCallback( - (content: string) => { + async (content: string) => { + if (!user) { + console.error("用户未登录"); + const errorMessage: CustomMessage = { + id: (Date.now() + 1).toString(), + role: "assistant", + content: "请先登录后再使用聊天功能。", + }; + setMessages([...messages, errorMessage]); + return; + } + + // 确保会话ID存在 + const effectiveSessionId = currentSessionId || generateSessionId(); + if (!currentSessionId) { + setCurrentSessionId(effectiveSessionId); + console.log("发送消息前生成会话ID:", effectiveSessionId); + } + if (messages.length === 0 && onFirstMessage) { - onFirstMessage(content) + onFirstMessage(content); } + const userMessage: CustomMessage = { + id: Date.now().toString(), + role: "user", + content, + }; + + const newMessages = [...messages, userMessage]; + setMessages(newMessages); + if (onMessageAdded) { - onMessageAdded({ role: "user", content }) + onMessageAdded({ role: "user", content }); } - sendMessage({ content }) + setIsLoading(true); + + try { + const requestData: QARequest = { + userId: parseInt(user.id), + question: content, + sessionId: effectiveSessionId, + }; + + console.log("发送QA请求,会话ID:", effectiveSessionId); + console.log("QaRequestData:", requestData); + console.log("API_BASE_URL@chat-window:", API_BASE_URL); + + const response = await fetch(`${API_BASE_URL}/api/qa/ask`, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(token && { Authorization: `Bearer ${token}` }), + }, + body: JSON.stringify(requestData), + }); + + console.log("响应状态:", response.status, response.statusText); + + if (!response.ok) { + const errorText = await response.text(); + console.error("请求失败详情:", { + status: response.status, + statusText: response.statusText, + errorResponse: errorText, + }); + throw new Error( + `HTTP ${response.status}: ${response.statusText} - ${errorText}` + ); + } + + const answer = await response.text(); + console.log("成功响应:", answer); + + const assistantMessage: CustomMessage = { + id: (Date.now() + 1).toString(), + role: "assistant", + content: answer, + }; + + const updatedMessages = [...newMessages, assistantMessage]; + setMessages(updatedMessages); + + if (onMessageAdded) { + onMessageAdded({ role: "assistant", content: answer }); + } + } catch (error) { + console.error("发送消息失败:", error); + + const errorMessage: CustomMessage = { + id: (Date.now() + 1).toString(), + role: "assistant", + content: `抱歉,发送消息时出现错误:${ + error instanceof Error ? error.message : "未知错误" + }`, + }; + setMessages([...newMessages, errorMessage]); + } finally { + setIsLoading(false); + } }, - [messages.length, onFirstMessage, onMessageAdded, sendMessage], - ) + [ + messages, + currentSessionId, + conversationId, + token, + user, + onFirstMessage, + onMessageAdded, + generateSessionId, + ] + ); - if (!conversationId && messages.length === 0) { + if (!user) { return ( -
+
- - {/* Welcome Screen */} -
+
-

AI 聊天助手

-

- 我是您的智能助手,可以帮助您解答问题、提供建议或进行有趣的对话。支持 Markdown 格式和代码高亮。 +

请先登录

+

+ 登录后即可使用AI聊天助手功能

+ +
+
+
+ ); + } - {/* Example prompts */} -
-

试试这些问题:

-
- {[ - "解释一下人工智能的基本概念", - "帮我写一个简单的 Python 函数", - "推荐一些学习编程的资源", - "用 Markdown 格式介绍一下 React", - ].map((prompt, index) => ( - - ))} + if (!conversationId && messages.length === 0) { + return ( +
+ + + {/* 欢迎界面 */} +
+
+
+
+ +
+

+ AI 聊天助手 +

+

+ 我是您的智能助手,通过DeepSeek API为您服务。 + 可以帮助您解答问题、提供建议或进行有趣的对话。 +

+ +
+

+ 试试这些问题: +

+
+ {[ + "帮我写一个简单的 Python 函数", + "推荐一些学习编程的资源", + "用 Markdown 格式介绍一下 React", + ].map((prompt, index) => ( + + ))} +
@@ -111,25 +286,36 @@ export function ChatWindow({
- ) + ); } return ( -
+
+ {/* 固定头部 */} - {/* Messages Area */} -
- {messages.map((message) => ( - - ))} + {/* 可滚动的消息区域 */} +
+ {messages.length === 0 ? ( +
+

暂无消息

+
+ ) : ( + messages.map((message) => ( + + )) + )} - {/* Loading indicator */} - {status === "in_progress" && ( + {isLoading && (
@@ -147,7 +333,8 @@ export function ChatWindow({
- + {/* 固定输入框 */} +
- ) + ); } diff --git a/frontend-nextjs/frontend/components/icons.tsx b/frontend-nextjs/frontend/components/icons.tsx new file mode 100644 index 00000000..bb1c031e --- /dev/null +++ b/frontend-nextjs/frontend/components/icons.tsx @@ -0,0 +1,7 @@ +import { PanelLeftClose, PanelLeftOpen } from "lucide-react"; + +export const Icons = { + sidebarClose: PanelLeftClose, + sidebarOpen: PanelLeftOpen, + // Add more icons as needed +}; diff --git a/frontend-nextjs/frontend/components/markdown-content.tsx b/frontend-nextjs/frontend/components/markdown-content.tsx index 6a688da6..617f198b 100644 --- a/frontend-nextjs/frontend/components/markdown-content.tsx +++ b/frontend-nextjs/frontend/components/markdown-content.tsx @@ -1,117 +1,156 @@ -"use client" - -import ReactMarkdown from "react-markdown" -import { Copy, Check } from "lucide-react" -import { useState } from "react" -import { Button } from "@/components/ui/button" +import ReactMarkdown from "react-markdown"; +import { Copy, Check } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; interface MarkdownContentProps { - content: string + content: string; } export function MarkdownContent({ content }: MarkdownContentProps) { - const [copiedCode, setCopiedCode] = useState(null) + const [copiedCode, setCopiedCode] = useState(null); const copyToClipboard = async (text: string) => { try { - await navigator.clipboard.writeText(text) - setCopiedCode(text) - setTimeout(() => setCopiedCode(null), 2000) + await navigator.clipboard.writeText(text); + setCopiedCode(text); + setTimeout(() => setCopiedCode(null), 2000); } catch (err) { - console.error("Failed to copy text: ", err) + console.error("Failed to copy text: ", err); } - } + }; return (
+ +
+                    
+ {match[1]} +
+ + {codeString} + +
+
+ ); + } - if (match) { return ( -
- -
-                  
{match[1]}
- - {codeString} - -
+ + {children} + + ); + }, + pre({ children }) { + return <>{children}; + }, + h1({ children }) { + return ( +

+ {children} +

+ ); + }, + h2({ children }) { + return ( +

{children}

+ ); + }, + h3({ children }) { + return ( +

{children}

+ ); + }, + p({ children }) { + return

{children}

; + }, + ul({ children }) { + return ( +
    + {children} +
+ ); + }, + ol({ children }) { + return ( +
    + {children} +
+ ); + }, + li({ children }) { + return
  • {children}
  • ; + }, + blockquote({ children }) { + return ( +
    + {children} +
    + ); + }, + a({ href, children }) { + return ( + + {children} + + ); + }, + table({ children }) { + return ( +
    + + {children} +
    - ) - } - - return ( - - {children} - - ) - }, - pre({ children }) { - return <>{children} - }, - h1({ children }) { - return

    {children}

    - }, - h2({ children }) { - return

    {children}

    - }, - h3({ children }) { - return

    {children}

    - }, - p({ children }) { - return

    {children}

    - }, - ul({ children }) { - return
      {children}
    - }, - ol({ children }) { - return
      {children}
    - }, - li({ children }) { - return
  • {children}
  • - }, - blockquote({ children }) { - return ( -
    - {children} -
    - ) - }, - a({ href, children }) { - return ( - - {children} - - ) - }, - table({ children }) { - return ( -
    - {children}
    -
    - ) - }, - th({ children }) { - return {children} - }, - td({ children }) { - return {children} - }, - }} - > - {content} - + ); + }, + th({ children }) { + return ( + + {children} + + ); + }, + td({ children }) { + return ( + {children} + ); + }, + }} + > + {content} +
    - ) + ); } diff --git a/frontend-nextjs/frontend/components/sidebar.tsx b/frontend-nextjs/frontend/components/sidebar.tsx index 151578b0..ccf76494 100644 --- a/frontend-nextjs/frontend/components/sidebar.tsx +++ b/frontend-nextjs/frontend/components/sidebar.tsx @@ -1,29 +1,40 @@ -"use client" - -import { useState } from "react" -import { Button } from "@/components/ui/button" -import { ScrollArea } from "@/components/ui/scroll-area" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" -import { Plus, MessageSquare, MoreHorizontal, Trash2, Edit3, User, LogOut } from "lucide-react" -import { cn } from "@/lib/utils" -import type { Conversation } from "@/types/chat" +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + Plus, + MessageSquare, + MoreHorizontal, + Trash2, + Edit3, + User, + LogOut, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import type { Conversation, HistoryConversation } from "@/types/chat"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, -} from "@/components/ui/dropdown-menu" -import { useAuth } from "@/contexts/auth-context" -import { UserProfileModal } from "@/components/user-profile-modal" +} from "@/components/ui/dropdown-menu"; +import { useAuth } from "@/contexts/auth-context"; +import { UserProfileModal } from "@/components/user-profile-modal"; +import { chatAPI } from "@/lib/chat-api"; interface SidebarProps { - conversations: Conversation[] - activeConversationId?: string - onNewChat: () => void - onSelectConversation: (conversationId: string) => void - onDeleteConversation: (conversationId: string) => void - onRenameConversation: (conversationId: string, newTitle: string) => void + conversations: Conversation[]; + activeConversationId?: string; + onNewChat: () => void; + onSelectConversation: (conversationId: string) => void; + onDeleteConversation: (conversationId: string) => void; + onRenameConversation: (conversationId: string, newTitle: string) => void; + onLoadConversations: (conversations: HistoryConversation[]) => void; + isLoadingHistory?: boolean; } export function Sidebar({ @@ -33,45 +44,74 @@ export function Sidebar({ onSelectConversation, onDeleteConversation, onRenameConversation, + onLoadConversations, + isLoadingHistory = false, }: SidebarProps) { - const [editingId, setEditingId] = useState(null) - const [editingTitle, setEditingTitle] = useState("") - const [showProfileModal, setShowProfileModal] = useState(false) - const { user, logout } = useAuth() + const [editingId, setEditingId] = useState(null); + const [editingTitle, setEditingTitle] = useState(""); + const [showProfileModal, setShowProfileModal] = useState(false); + const { user, token, logout } = useAuth(); const handleStartEdit = (conversation: Conversation) => { - setEditingId(conversation.id) - setEditingTitle(conversation.title) - } + setEditingId(conversation.id); + setEditingTitle(conversation.title); + }; const handleSaveEdit = (conversationId: string) => { if (editingTitle.trim()) { - onRenameConversation(conversationId, editingTitle.trim()) + onRenameConversation(conversationId, editingTitle.trim()); } - setEditingId(null) - setEditingTitle("") - } + setEditingId(null); + setEditingTitle(""); + }; const handleCancelEdit = () => { - setEditingId(null) - setEditingTitle("") + setEditingId(null); + setEditingTitle(""); + }; + + const handleDeleteConversation = async (conversationId: string) => { + if (!token) return; + + try { + await chatAPI.deleteConversation(conversationId, token); + onDeleteConversation(conversationId); + } catch (error) { + console.error("Failed to delete conversation:", error); + } + }; + + // 调试:检查重复的对话 ID + console.log("Sidebar conversations:", conversations); + const duplicateIds = conversations + .map((c) => c.id) + .filter((id, index, arr) => arr.indexOf(id) !== index); + if (duplicateIds.length > 0) { + console.warn("Duplicate conversation IDs found:", duplicateIds); } return ( <>
    - {/* Header */} -
    + {/* Header - 固定高度 */} +
    - + {user?.username?.charAt(0).toUpperCase() || "U"}
    -

    {user?.username || "用户"}

    -

    {user?.email || ""}

    +

    + {user?.username || "用户"} +

    +

    + {user?.email || ""} +

    @@ -89,7 +129,10 @@ export function Sidebar({ 个人资料 - + 退出登录 @@ -101,101 +144,128 @@ export function Sidebar({ onClick={onNewChat} className="w-full bg-sidebar-accent hover:bg-sidebar-accent/80 text-sidebar-accent-foreground" size="lg" + disabled={isLoadingHistory} > 新建对话
    - {/* Conversations List */} - + {/* Conversations List - 可滚动区域 */} +
    - {conversations.length === 0 ? ( + {isLoadingHistory ? ( +
    +

    加载对话历史中...

    +
    + ) : conversations.length === 0 ? (

    暂无对话历史

    ) : (
    - {conversations.map((conversation) => ( -
    onSelectConversation(conversation.id)} - > - - - {editingId === conversation.id ? ( - setEditingTitle(e.target.value)} - onBlur={() => handleSaveEdit(conversation.id)} - onKeyDown={(e) => { - if (e.key === "Enter") { - handleSaveEdit(conversation.id) - } else if (e.key === "Escape") { - handleCancelEdit() - } - }} - className="flex-1 bg-transparent border-none outline-none text-sm" - autoFocus - onClick={(e) => e.stopPropagation()} - /> - ) : ( - - {conversation.title} - - )} - - {editingId !== conversation.id && ( - - - - - - handleStartEdit(conversation)}> - - 重命名 - - onDeleteConversation(conversation.id)} - className="text-destructive focus:text-destructive" - > - - 删除 - - - - )} -
    - ))} + {conversations.map((conversation) => { + // 检查重复 ID + const isDuplicate = + conversations.filter((c) => c.id === conversation.id) + .length > 1; + + return ( +
    onSelectConversation(conversation.id)} + > + + + {editingId === conversation.id ? ( + setEditingTitle(e.target.value)} + onBlur={() => handleSaveEdit(conversation.id)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleSaveEdit(conversation.id); + } else if (e.key === "Escape") { + handleCancelEdit(); + } + }} + className="flex-1 bg-transparent border-none outline-none text-sm" + autoFocus + onClick={(e) => e.stopPropagation()} + /> + ) : ( + + {conversation.title} + {isDuplicate && " (重复)"} + + )} + + {editingId !== conversation.id && ( + + + + + + handleStartEdit(conversation)} + > + + 重命名 + + + handleDeleteConversation(conversation.id) + } + className="text-destructive focus:text-destructive" + > + + 删除 + + + + )} +
    + ); + })}
    )}
    - {/* Footer */} -
    -
    AI 聊天助手 v1.0
    + {/* Footer - 固定高度 */} + {/* 修改这里:移除 border-t,添加 border-b 来与右侧输入框对齐 */} +
    +
    + AI 聊天助手 v1.0 +
    {/* User Profile Modal */} - + - ) + ); } diff --git a/frontend-nextjs/frontend/components/ui/alert-dialog.tsx b/frontend-nextjs/frontend/components/ui/alert-dialog.tsx index d20a9882..f40d35e0 100644 --- a/frontend-nextjs/frontend/components/ui/alert-dialog.tsx +++ b/frontend-nextjs/frontend/components/ui/alert-dialog.tsx @@ -1,31 +1,28 @@ -"use client" +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" +const AlertDialog = AlertDialogPrimitive.Root; -const AlertDialog = AlertDialogPrimitive.Root - -const AlertDialogTrigger = AlertDialogPrimitive.Trigger +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; const AlertDialogPortal = ({ - children, ...props }: AlertDialogPrimitive.AlertDialogPortalProps) => (
    - {children} + {props.children}
    -) -AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName +); +AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName; const AlertDialogOverlay = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( +>(({ className, ...props }, ref) => ( -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName +)); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; const AlertDialogContent = React.forwardRef< React.ElementRef, @@ -52,8 +49,8 @@ const AlertDialogContent = React.forwardRef< {...props} /> -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName +)); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; const AlertDialogHeader = ({ className, @@ -66,8 +63,8 @@ const AlertDialogHeader = ({ )} {...props} /> -) -AlertDialogHeader.displayName = "AlertDialogHeader" +); +AlertDialogHeader.displayName = "AlertDialogHeader"; const AlertDialogFooter = ({ className, @@ -80,8 +77,8 @@ const AlertDialogFooter = ({ )} {...props} /> -) -AlertDialogFooter.displayName = "AlertDialogFooter" +); +AlertDialogFooter.displayName = "AlertDialogFooter"; const AlertDialogTitle = React.forwardRef< React.ElementRef, @@ -92,8 +89,8 @@ const AlertDialogTitle = React.forwardRef< className={cn("text-lg font-semibold", className)} {...props} /> -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; const AlertDialogDescription = React.forwardRef< React.ElementRef, @@ -104,9 +101,9 @@ const AlertDialogDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) +)); AlertDialogDescription.displayName = - AlertDialogPrimitive.Description.displayName + AlertDialogPrimitive.Description.displayName; const AlertDialogAction = React.forwardRef< React.ElementRef, @@ -117,8 +114,8 @@ const AlertDialogAction = React.forwardRef< className={cn(buttonVariants(), className)} {...props} /> -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; const AlertDialogCancel = React.forwardRef< React.ElementRef, @@ -133,8 +130,8 @@ const AlertDialogCancel = React.forwardRef< )} {...props} /> -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; export { AlertDialog, @@ -146,4 +143,4 @@ export { AlertDialogDescription, AlertDialogAction, AlertDialogCancel, -} +}; diff --git a/frontend-nextjs/frontend/components/ui/calendar.tsx b/frontend-nextjs/frontend/components/ui/calendar.tsx index 6af8f10d..c5708889 100644 --- a/frontend-nextjs/frontend/components/ui/calendar.tsx +++ b/frontend-nextjs/frontend/components/ui/calendar.tsx @@ -1,13 +1,11 @@ -"use client" +import * as React from "react"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { DayPicker, type ChevronProps } from "react-day-picker"; -import * as React from "react" -import { ChevronLeft, ChevronRight } from "lucide-react" -import { DayPicker } from "react-day-picker" +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" - -export type CalendarProps = React.ComponentProps +export type CalendarProps = React.ComponentProps; function Calendar({ className, @@ -36,15 +34,17 @@ function Calendar({ head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]", row: "flex w-full mt-2", - cell: "text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", + cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", day: cn( buttonVariants({ variant: "ghost" }), "h-9 w-9 p-0 font-normal aria-selected:opacity-100" ), + day_range_end: "day-range-end", day_selected: "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", day_today: "bg-accent text-accent-foreground", - day_outside: "text-muted-foreground opacity-50", + day_outside: + "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30", day_disabled: "text-muted-foreground opacity-50", day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground", @@ -52,13 +52,22 @@ function Calendar({ ...classNames, }} components={{ - IconLeft: ({ ...props }) => , - IconRight: ({ ...props }) => , + Chevron: (props: ChevronProps) => { + const { orientation, ...restProps } = props; + if (orientation === "left") { + return ; + } + if (orientation === "right") { + return ; + } + // 默认返回一个空元素而不是null + return ; + }, }} {...props} /> - ) + ); } -Calendar.displayName = "Calendar" +Calendar.displayName = "Calendar"; -export { Calendar } +export { Calendar }; diff --git a/frontend-nextjs/frontend/components/ui/chart.tsx b/frontend-nextjs/frontend/components/ui/chart.tsx index 8ac6ee65..94074e13 100644 --- a/frontend-nextjs/frontend/components/ui/chart.tsx +++ b/frontend-nextjs/frontend/components/ui/chart.tsx @@ -1,55 +1,48 @@ -"use client" +import * as React from "react"; +import * as RechartsPrimitive from "recharts"; +import { TooltipProps } from "recharts"; -import * as React from "react" -import * as RechartsPrimitive from "recharts" -import { - NameType, - Payload, - ValueType, -} from "recharts/types/component/DefaultTooltipContent" - -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; // Format: { THEME_NAME: CSS_SELECTOR } -const THEMES = { light: "", dark: ".dark" } as const +const THEMES = { light: "", dark: ".dark" } as const; export type ChartConfig = { [k in string]: { - label?: React.ReactNode - icon?: React.ComponentType + label?: React.ReactNode; + icon?: React.ComponentType; } & ( | { color?: string; theme?: never } | { color?: never; theme: Record } - ) -} + ); +}; type ChartContextProps = { - config: ChartConfig -} + config: ChartConfig; +}; -const ChartContext = React.createContext(null) +const ChartContext = React.createContext(null); function useChart() { - const context = React.useContext(ChartContext) - + const context = React.useContext(ChartContext); if (!context) { - throw new Error("useChart must be used within a ") + throw new Error("useChart must be used within a "); } - - return context + return context; } +/* ---------- ChartContainer ---------- */ const ChartContainer = React.forwardRef< HTMLDivElement, React.ComponentProps<"div"> & { - config: ChartConfig + config: ChartConfig; children: React.ComponentProps< typeof RechartsPrimitive.ResponsiveContainer - >["children"] + >["children"]; } >(({ id, className, children, config, ...props }, ref) => { - const uniqueId = React.useId() - const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + const uniqueId = React.useId(); + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`; return ( @@ -68,18 +61,16 @@ const ChartContainer = React.forwardRef<
    - ) -}) -ChartContainer.displayName = "Chart" + ); +}); +ChartContainer.displayName = "Chart"; +/* ---------- ChartStyle ---------- */ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { const colorConfig = Object.entries(config).filter( - ([_, config]) => config.theme || config.color - ) - - if (!colorConfig.length) { - return null - } + ([, cfg]) => cfg.theme || cfg.color + ); + if (!colorConfig.length) return null; return (