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 00000000..cc32f792 Binary files /dev/null and b/backend-services/api-gateway/target/classes/com/ai/qa/gateway/ApiGatewayApplication.class differ 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 00000000..a281c18f Binary files /dev/null and b/backend-services/api-gateway/target/classes/com/ai/qa/gateway/api/controller/TestConfigController.class differ 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 00000000..81e3214a Binary files /dev/null and b/backend-services/api-gateway/target/classes/com/ai/qa/gateway/api/web/filter/AuthenticationFilter.class differ 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 00000000..06db9de2 Binary files /dev/null and b/backend-services/api-gateway/target/classes/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig$1.class differ 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 00000000..3529003a Binary files /dev/null and b/backend-services/api-gateway/target/classes/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig$RateLimiterConfig.class differ 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 00000000..496fa14f Binary files /dev/null and b/backend-services/api-gateway/target/classes/com/ai/qa/gateway/infrastructure/config/InMemoryRateLimiterConfig.class differ 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 00000000..d9fc044b Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/QAServiceApplication.class differ 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 00000000..36455498 Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/api/controller/QAController.class differ 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 00000000..b36b2dc0 Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/ApiResponse.class differ diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/AskQaRequest.class b/backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/AskQaRequest.class new file mode 100644 index 00000000..dc1ebee0 Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/AskQaRequest.class differ 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 00000000..df2156f2 Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/QAHistoryDTO.class differ 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 00000000..851d485a Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/api/dto/SaveHistoryRequest.class differ 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 00000000..6b9dffc4 Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/api/exception/ErrCode.class differ 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 00000000..98676e0b Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/api/exception/GlobalExceptionHandler.class differ 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 00000000..432e54ee Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/application/dto/QAHistoryQuery.class differ diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/application/dto/SaveHistoryCommand.class b/backend-services/qa-service/target/classes/com/ai/qa/service/application/dto/SaveHistoryCommand.class new file mode 100644 index 00000000..c2d1e7d3 Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/application/dto/SaveHistoryCommand.class differ diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/application/service/QAHistoryService.class b/backend-services/qa-service/target/classes/com/ai/qa/service/application/service/QAHistoryService.class new file mode 100644 index 00000000..39a94ece Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/application/service/QAHistoryService.class differ 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 00000000..29019a4e Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/domain/exception/QADomainException.class differ diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/domain/model/QAHistory.class b/backend-services/qa-service/target/classes/com/ai/qa/service/domain/model/QAHistory.class new file mode 100644 index 00000000..9189a024 Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/domain/model/QAHistory.class differ 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 00000000..824bc703 Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/domain/repo/QAHistoryRepo.class differ 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 00000000..e5df613b Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/domain/service/QAService.class differ 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 00000000..e9c8e616 Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/DeepSeekClient.class differ 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 00000000..39d92754 Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/DeepSeekRestTemplateConfig.class differ diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/FeignConfig.class b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/FeignConfig.class new file mode 100644 index 00000000..8ab7778c Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/FeignConfig.class differ diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/UserClient.class b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/UserClient.class new file mode 100644 index 00000000..f55add0f Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/UserClient.class differ 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 00000000..689fff07 Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/feign/UserClientFallback.class differ 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 00000000..74a9431d Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/entities/QAHistoryPO.class differ 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 00000000..5d6d6ba1 Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapper.class differ 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 00000000..c33c2b3b Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/mapper/QAHistoryMapperImpl.class differ 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 00000000..5dedb88e Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/repositories/JpaQAHistoryRepository.class differ diff --git a/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/repositories/QAHistoryRepoImpl.class b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/repositories/QAHistoryRepoImpl.class new file mode 100644 index 00000000..d1a18fbe Binary files /dev/null and b/backend-services/qa-service/target/classes/com/ai/qa/service/infrastructure/persistence/repositories/QAHistoryRepoImpl.class differ 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 00000000..8711ebf4 Binary files /dev/null and b/backend-services/qa-service/target/test-classes/com/ai/qa/service/domain/model/QAHistoryTest.class differ 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 00000000..e16361a2 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/UserServiceApplication.class differ 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 00000000..153a8d8e Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/api/controller/UserController.class differ 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 00000000..1902576a Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/Response.class differ diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/request/LoginRequest.class b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/request/LoginRequest.class new file mode 100644 index 00000000..1d3951c9 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/request/LoginRequest.class differ 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 00000000..57c551b4 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/request/RegisterRequest.class differ diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/request/UpdatePasswordRequest.class b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/request/UpdatePasswordRequest.class new file mode 100644 index 00000000..f61a0790 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/request/UpdatePasswordRequest.class differ 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 00000000..bd601c9f Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/BaseResponse.class differ diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/LoginResponse.class b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/LoginResponse.class new file mode 100644 index 00000000..e3f2232e Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/LoginResponse.class differ diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/RegisterResponse.class b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/RegisterResponse.class new file mode 100644 index 00000000..86b91710 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/RegisterResponse.class differ 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 00000000..7e7cd5c9 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/UpdatePasswordResponse.class differ 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 00000000..8b03a374 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/api/dto/response/UserResponse.class differ diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/api/exception/BusinessException.class b/backend-services/user-service/target/classes/com/ai/qa/user/api/exception/BusinessException.class new file mode 100644 index 00000000..f34d816d Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/api/exception/BusinessException.class differ diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/api/exception/ErrCode.class b/backend-services/user-service/target/classes/com/ai/qa/user/api/exception/ErrCode.class new file mode 100644 index 00000000..7eab4416 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/api/exception/ErrCode.class differ 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 00000000..d97c5f7a Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/api/exception/GlobalExceptionHandler.class differ 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 00000000..46e80886 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/application/service/UserService.class differ 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 00000000..651c5e17 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/application/service/impl/UserDetailsServiceImpl.class differ 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 00000000..0933e1df Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/application/service/impl/UserServiceImpl.class differ diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/common/CommonUtil.class b/backend-services/user-service/target/classes/com/ai/qa/user/common/CommonUtil.class new file mode 100644 index 00000000..5b8da122 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/common/CommonUtil.class differ 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 00000000..c37637a5 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/common/DateUtil.class differ 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 00000000..252c1b4e Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/common/JwtUtil.class differ diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/common/KeyGenerator.class b/backend-services/user-service/target/classes/com/ai/qa/user/common/KeyGenerator.class new file mode 100644 index 00000000..cd8e54b6 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/common/KeyGenerator.class differ diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/domain/entity/User.class b/backend-services/user-service/target/classes/com/ai/qa/user/domain/entity/User.class new file mode 100644 index 00000000..93c32018 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/domain/entity/User.class differ 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 00000000..c5410b7f Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/domain/repository/UserRepository.class differ 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 00000000..879479fd Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/config/JwtAuthenticationFilter.class differ diff --git a/backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/config/SecurityConfig.class b/backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/config/SecurityConfig.class new file mode 100644 index 00000000..ceae68a2 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/config/SecurityConfig.class differ 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 00000000..590c39a3 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/config/SwaggerConfig.class differ 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 00000000..81c93d22 Binary files /dev/null and b/backend-services/user-service/target/classes/com/ai/qa/user/infrastructure/persistent/UserMapper.class differ 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 (