diff --git a/qa-service/docker-compose.yml b/qa-service/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..b92c130edf1cd7cac69d42ab559368b344fc1b40 --- /dev/null +++ b/qa-service/docker-compose.yml @@ -0,0 +1,250 @@ +networks: + app_net: + driver: bridge +services: + # MySQL 主库 + mysql: + image: mysql:8.4 + container_name: mysql + environment: + - MYSQL_ROOT_PASSWORD=root + - TZ=Asia/Shanghai + - MYSQL_CHARSET=utf8mb4 + - MYSQL_COLLATION=utf8mb4_general_ci + - MYSQL_ROOT_HOST=% + - MYSQL_SSL_MODE=REQUIRED + ports: + - "3306:3306" + privileged: true + volumes: + - ./mysql/conf.d:/etc/mysql/conf.d + - ./mysql/data:/var/lib/mysql + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] + interval: 10s + timeout: 5s + retries: 5 + networks: + - app_net + restart: always + redis: + container_name: redis + image: redis:latest + ports: + - "6379:6379" + privileged: true + healthcheck: + test: [ "CMD", "redis-cli", "ping" ] + interval: 5s + timeout: 3s + retries: 5 + networks: + - app_net + restart: always + + rabbitmq: + container_name: rabbitmq + image: rabbitmq:4.1.2-management + ports: + - "5672:5672" + - "15672:15672" + privileged: true + healthcheck: + test: [ "CMD", "rabbitmqctl", "status" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + restart: always + minio: + image: quay.io/minio/minio:latest + container_name: minio + restart: always + ports: + - "9000:9000" # S3 API 端口 + - "9001:9001" # Web 控制台端口 + environment: + MINIO_ROOT_USER: admin + MINIO_ROOT_PASSWORD: admin123456 + volumes: + - ./minio-data:/data + command: server /data --console-address ":9001" + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:9000/minio/health/live" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + # RocketMQ NameServer + rocketmq-namesrv: + image: apache/rocketmq:latest + container_name: rocketmq-namesrv + command: sh mqnamesrv + ports: + - "9876:9876" + environment: + JAVA_OPT_EXT: "-server -Xms128m -Xmx128m -Xmn128m" + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:9876" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + restart: always + + # RocketMQ Broker + rocketmq-broker: + image: apache/rocketmq:latest + container_name: rocketmq-broker + command: sh mqbroker -c /home/rocketmq/rocketmq-5.3.3/conf/broker.conf + ports: + - "10909:10909" + - "10911:10911" + - "10912:10912" + environment: + JAVA_OPT_EXT: "-server -Xms128m -Xmx128m -Xmn128m" + NAMESRV_ADDR: "rocketmq-namesrv:9876" + volumes: + - ./rocketmq/conf/broker.conf:/home/rocketmq/rocketmq-5.3.3/conf/broker.conf + - ./rocketmq/logs:/home/rocketmq/logs + - ./rocketmq/store:/home/rocketmq/store + depends_on: + - rocketmq-namesrv + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:10911" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + restart: always + + # Elasticsearch + elasticsearch: + image: elasticsearch:7.17.28 + container_name: elasticsearch + environment: + - discovery.type=single-node + - ES_JAVA_OPTS=-Xms512m -Xmx512m + - xpack.security.enabled=false + ports: + - "9200:9200" + - "9300:9300" + volumes: + - ./es/data:/usr/share/elasticsearch/data + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:9200" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + restart: always + + # Nginx + nginx: + image: nginx:latest + container_name: nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/conf.d:/etc/nginx/conf.d + - ./nginx/html:/usr/share/nginx/html + - ./nginx/logs:/var/log/nginx + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + restart: always + nacos: + container_name: nacos + image: nacos/nacos-server:v3.0.2 + ports: + - "8848:8848" + - "8080:8080" + - "9848:9848" + - "9849:9849" + environment: + - TZ=Asia/Shanghai + - MODE=standalone + - PREFER_HOST_MODE=hostname + - SPRING_DATASOURCE_PLATFORM=mysql + - MYSQL_SERVICE_HOST=mysql + - MYSQL_SERVICE_DB_NAME=nacos_config + - MYSQL_SERVICE_PORT=3306 + - MYSQL_SERVICE_USER=root + - MYSQL_SERVICE_PASSWORD=root + - MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + - NACOS_AUTH_IDENTITY_KEY=2222 + - NACOS_AUTH_IDENTITY_VALUE=2xxx + - NACOS_AUTH_TOKEN=VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg= + volumes: + - ./nacos/logs:/home/nacos/logs + depends_on: + mysql: + condition: service_healthy + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:8848/nacos" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + privileged: true + restart: always + # Seata + seata-server: + image: seataio/seata-server:1.6.0 + container_name: seata-server + ports: + - "7091:7091" + - "8091:8091" + networks: + - app_net + restart: always + + # Sentinel + sentinel: + image: bladex/sentinel-dashboard:latest + container_name: sentinel + ports: + - "8858:8858" + environment: + - JAVA_OPTS=-Dserver.port=8858 -Dcsp.sentinel.dashboard.server=localhost:8858 -Dproject.name=sentinel-dashboard + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:8858" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + restart: always + leaf-server: + image: registry.cn-hangzhou.aliyuncs.com/itheima/meituan-leaf:1.0.1 + container_name: leaf-server + ports: + - "8090:8080" + environment: + - SPRING_PROFILES_ACTIVE=prod + volumes: + - ./leaf/application.properties:/leaf-server/config/application.properties + networks: + - app_net + + zookeeper: + image: bitnami/zookeeper:latest + container_name: zookeeper + ports: + - "2181:2181" + environment: + - ALLOW_ANONYMOUS_LOGIN=yes + networks: + - app_net + restart: always \ No newline at end of file diff --git a/qa-service/nacos_config.sql b/qa-service/nacos_config.sql new file mode 100644 index 0000000000000000000000000000000000000000..433a90b94a37d64ee738e9cc23d4fd82a1663551 --- /dev/null +++ b/qa-service/nacos_config.sql @@ -0,0 +1,179 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/******************************************/ +/* 表名称 = config_info */ +/******************************************/ +CREATE TABLE `config_info` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) DEFAULT NULL COMMENT 'group_id', + `content` longtext NOT NULL COMMENT 'content', + `md5` varchar(32) DEFAULT NULL COMMENT 'md5', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + `src_user` text COMMENT 'source user', + `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', + `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', + `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', + `c_desc` varchar(256) DEFAULT NULL COMMENT 'configuration description', + `c_use` varchar(64) DEFAULT NULL COMMENT 'configuration usage', + `effect` varchar(64) DEFAULT NULL COMMENT '配置生效的描述', + `type` varchar(64) DEFAULT NULL COMMENT '配置的类型', + `c_schema` text COMMENT '配置的模式', + `encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info'; + +/******************************************/ +/* 表名称 = config_info since 2.5.0 */ +/******************************************/ +CREATE TABLE `config_info_gray` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) NOT NULL COMMENT 'group_id', + `content` longtext NOT NULL COMMENT 'content', + `md5` varchar(32) DEFAULT NULL COMMENT 'md5', + `src_user` text COMMENT 'src_user', + `src_ip` varchar(100) DEFAULT NULL COMMENT 'src_ip', + `gmt_create` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_create', + `gmt_modified` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_modified', + `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', + `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', + `gray_name` varchar(128) NOT NULL COMMENT 'gray_name', + `gray_rule` text NOT NULL COMMENT 'gray_rule', + `encrypted_data_key` varchar(256) NOT NULL DEFAULT '' COMMENT 'encrypted_data_key', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_configinfogray_datagrouptenantgray` (`data_id`,`group_id`,`tenant_id`,`gray_name`), + KEY `idx_dataid_gmt_modified` (`data_id`,`gmt_modified`), + KEY `idx_gmt_modified` (`gmt_modified`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='config_info_gray'; + +/******************************************/ +/* 表名称 = config_tags_relation */ +/******************************************/ +CREATE TABLE `config_tags_relation` ( + `id` bigint(20) NOT NULL COMMENT 'id', + `tag_name` varchar(128) NOT NULL COMMENT 'tag_name', + `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) NOT NULL COMMENT 'group_id', + `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', + `nid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增长标识', + PRIMARY KEY (`nid`), + UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`), + KEY `idx_tenant_id` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation'; + +/******************************************/ +/* 表名称 = group_capacity */ +/******************************************/ +CREATE TABLE `group_capacity` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群', + `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', + `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', + `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', + `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值', + `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', + `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_group_id` (`group_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表'; + +/******************************************/ +/* 表名称 = his_config_info */ +/******************************************/ +CREATE TABLE `his_config_info` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'id', + `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增标识', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) NOT NULL COMMENT 'group_id', + `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', + `content` longtext NOT NULL COMMENT 'content', + `md5` varchar(32) DEFAULT NULL COMMENT 'md5', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + `src_user` text COMMENT 'source user', + `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', + `op_type` char(10) DEFAULT NULL COMMENT 'operation type', + `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', + `encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥', + `publish_type` varchar(50) DEFAULT 'formal' COMMENT 'publish type gray or formal', + `gray_name` varchar(50) DEFAULT NULL COMMENT 'gray name', + `ext_info` longtext DEFAULT NULL COMMENT 'ext info', + PRIMARY KEY (`nid`), + KEY `idx_gmt_create` (`gmt_create`), + KEY `idx_gmt_modified` (`gmt_modified`), + KEY `idx_did` (`data_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造'; + + +/******************************************/ +/* 表名称 = tenant_capacity */ +/******************************************/ +CREATE TABLE `tenant_capacity` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID', + `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', + `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', + `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', + `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数', + `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', + `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tenant_id` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表'; + + +CREATE TABLE `tenant_info` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `kp` varchar(128) NOT NULL COMMENT 'kp', + `tenant_id` varchar(128) default '' COMMENT 'tenant_id', + `tenant_name` varchar(128) default '' COMMENT 'tenant_name', + `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc', + `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source', + `gmt_create` bigint(20) NOT NULL COMMENT '创建时间', + `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`), + KEY `idx_tenant_id` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info'; + +CREATE TABLE `users` ( + `username` varchar(50) NOT NULL PRIMARY KEY COMMENT 'username', + `password` varchar(500) NOT NULL COMMENT 'password', + `enabled` boolean NOT NULL COMMENT 'enabled' +); + +CREATE TABLE `roles` ( + `username` varchar(50) NOT NULL COMMENT 'username', + `role` varchar(50) NOT NULL COMMENT 'role', + UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE +); + +CREATE TABLE `permissions` ( + `role` varchar(50) NOT NULL COMMENT 'role', + `resource` varchar(128) NOT NULL COMMENT 'resource', + `action` varchar(8) NOT NULL COMMENT 'action', + UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE +); + diff --git a/qa-service/pom.xml b/qa-service/pom.xml index 476ca31ee84446204ef71aedf1d07bdd6d774ad2..09d8cea6a2725bfb34e3340fe909d1c67150e906 100644 --- a/qa-service/pom.xml +++ b/qa-service/pom.xml @@ -11,22 +11,37 @@ 21 UTF-8 UTF-8 - 3.0.2 + 3.2.4 + 2023.0.1.0 + 2023.0.1 + 3.5.12 + 4.5.0 + 2.3.0 + + pom + + + qa-service-bootstrap + qa-service-adapter + qa-service-application + qa-service-domain + qa-service-common + + + - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-starter-test - test - + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + org.springframework.boot spring-boot-dependencies @@ -34,6 +49,26 @@ pom import + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + ${knife4j.version} + @@ -54,7 +89,7 @@ spring-boot-maven-plugin ${spring-boot.version} - com.example.qa.service.QaServiceApplication + com.example.qa-service.qa-serviceApplication true diff --git a/qa-service/qa-service-adapter/pom.xml b/qa-service/qa-service-adapter/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0074d0c8f60c8bf3868ca60caccd07f5c42eb7fd --- /dev/null +++ b/qa-service/qa-service-adapter/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + com.example + qa-service-adapter + 0.0.1-SNAPSHOT + qa-service-adapter + qa-service-adapter + + pom + + + com.example + qa-service + 0.0.1-SNAPSHOT + + + + qa-adapter-in + qa-adapter-out + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/qa-service-adapter/qa-adapter-in/pom.xml b/qa-service/qa-service-adapter/qa-adapter-in/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..473ef75673f90161aee8129d1094187b03635b29 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + com.example + qa-adapter-in + 0.0.1-SNAPSHOT + qa-adapter-in + qa-adapter-in + + pom + + + com.example + qa-service-adapter + 0.0.1-SNAPSHOT + + + + qa-adapter-in-web + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/pom.xml b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..2d991ccb34f54281c36dffbb183925418139f746 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + com.example + qa-adapter-in-web + 0.0.1-SNAPSHOT + qa-adapter-in-web + qa-adapter-in-web + + + com.example + qa-adapter-in + 0.0.1-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + provided + + + org.projectlombok + lombok + + + + org.springframework.boot + spring-boot-starter-amqp + + + + org.springframework.amqp + spring-rabbit-test + test + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.58 + + + + com.example + qa-service-application + 0.0.1-SNAPSHOT + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + ${knife4j.version} + + + com.example + qa-adapter-out-persistence + 0.0.1-SNAPSHOT + compile + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/config/MoonShotConfig.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/config/MoonShotConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..e47402200fe27bdfd87f0812d592d9ac30d86cef --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/config/MoonShotConfig.java @@ -0,0 +1,14 @@ +package org.example.xmut.qa.adapter.in.web.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "moonshot") +public class MoonShotConfig { + private String url; + private String key; + private String model; +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/config/RestConfig.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/config/RestConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..425e041d43513f332caabb310dba3b507030acc2 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/config/RestConfig.java @@ -0,0 +1,13 @@ +package org.example.xmut.qa.adapter.in.web.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestConfig { + @Bean + public RestTemplate getRestTemplate(){ + return new RestTemplate(); + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/kimiController.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/kimiController.java new file mode 100644 index 0000000000000000000000000000000000000000..44a6ed72cd86327c947cf849b9618a79093f1323 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/kimiController.java @@ -0,0 +1,104 @@ +package org.example.xmut.qa.adapter.in.web.controller; + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.example.xmut.qa.adapter.in.web.config.MoonShotConfig; +import org.example.xmut.qa.adapter.in.web.dto.ChatCompletionRequestDTO; +import org.example.xmut.qa.adapter.in.web.dto.ChatCompletionResponseDTO; +import org.example.xmut.qa.adapter.in.web.dto.Choice; +import org.example.xmut.qa.adapter.in.web.dto.Message; +import org.example.xmut.qa.service.common.IdWorker; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.valueobject.Answer; +import org.example.xmut.qa.service.domain.valueobject.QaId; +import org.example.xmut.qa.service.domain.valueobject.Question; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import java.util.Arrays; +import java.util.List; + +@Slf4j +@Tag(name="示例控制器") +@RestController +@RequestMapping("kimi") +public class kimiController { + @Resource + private RestTemplate restTemplate; + + @Resource + private MoonShotConfig moonShotConfig; + + @Resource + private RabbitTemplate rabbitTemplate; + + // 创建IdWorker实例用于生成唯一ID + private static final IdWorker idWorker = new IdWorker(); + + @Operation(summary = "kimi示例") + @GetMapping("demo") + public List demo(@RequestParam("question") String question) { + log.info("demo"); + + //创建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + moonShotConfig.getKey()); + //创建请求体 + ChatCompletionRequestDTO request = new ChatCompletionRequestDTO(); + request.setModel(moonShotConfig.getModel()); + request.setTemperature(0.6); + //创建消息列表 + Message systemMessage = new Message(); + systemMessage.setRole("system"); + systemMessage.setContent("你是 Kimi,由 Moonshot AI 提供的人工智能助手,你更擅长中文和英文的对话。你会为用户提供安全,有帮助,准确的回答。同时,你会拒绝一切涉及恐怖主义,种族歧视,黄色暴力等问题的回答。Moonshot AI 为专有名词,不可翻译成其他语言。"); + + Message userMessage=new Message(); + userMessage.setRole("user"); + userMessage.setContent(question); + + request.setMessages(Arrays.asList( + systemMessage,userMessage + )); + + //创建Http实体 + HttpEntity response= restTemplate.exchange( + moonShotConfig.getUrl(), + HttpMethod.POST, + new HttpEntity<>(request, headers), + ChatCompletionResponseDTO.class + ); + + log.info("response:{}",response); + + List choices = response.getBody().getChoices(); + + for (Choice choice : choices) { + //把消息交给mq处理 + Qa qa = new Qa(); + // 使用IdWorker生成唯一ID + qa.setId(new QaId(idWorker.nextId())); + // 用户的原始问题 + qa.setQuestion(new Question(question)); + // Kimi的回答内容 + qa.setAnswer(new Answer(choice.getMessage().getContent())); + rabbitTemplate.convertAndSend("qaTest",qa); + } + + return choices; + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java new file mode 100644 index 0000000000000000000000000000000000000000..3e65761f5c06b4a5d52b749379f94a9c5734e5c1 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java @@ -0,0 +1,94 @@ +package org.example.xmut.qa.adapter.in.web.controller; + + +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.example.xmut.qa.adapter.in.web.dto.*; +import org.example.xmut.qa.service.application.command.CreateQaCommand; +import org.example.xmut.qa.service.application.command.UpdateQaCommand; +import org.example.xmut.qa.service.application.port.in.*; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.valueobject.Answer; +import org.example.xmut.qa.service.domain.valueobject.QaId; +import org.example.xmut.qa.service.domain.valueobject.Question; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Slf4j +@RequestMapping("/qas") +@RestController +@RequiredArgsConstructor +public class qaController { + + private final GetQaListUseCase getQaListUseCase; + private final CreateQaUseCase createQaUseCase; + private final DeleteQaUseCase deleteQaUseCase; + private final UpdateQaUseCase updateQaUseCase; + private final GetQaByIdUseCase getQaByIdUseCase; + + @Resource + private RabbitTemplate rabbitTemplate; + + @GetMapping("") + public List getQas() { + log.info("getQas"); + return getQaListUseCase.getQas(); + } + + @PostMapping() + public Qa createQa(@RequestBody CreateQaRequestDTO createqaRequestDTO){ + + CreateQaCommand command=CreateQaCommand.builder() + .question(createqaRequestDTO.question()) + .answer(createqaRequestDTO.answer()) + .build(); + + return createQaUseCase.createQa(command); + } + + + @DeleteMapping("{id}") + public String deleteQa(@PathVariable("id") Long id){ + deleteQaUseCase.deleteQa(id); + return "success"; + } + + @PutMapping("") + public Qa updateQa(@RequestBody UpdateQaRequestDTO updateQaRequestDTO){ + UpdateQaCommand command=UpdateQaCommand.builder() + .id(updateQaRequestDTO.id()) + .question(updateQaRequestDTO.question()) + .answer(updateQaRequestDTO.answer()) + .build(); + Qa qa = updateQaUseCase.updateQa(command); + return qa; + } + + + @GetMapping("{id}") + public QaResponseDTO getqaById(@PathVariable("id") Long id){ + Qa qa = getQaByIdUseCase.getQaById(id); + QaResponseDTO qaResponseDTO = new QaResponseDTO( + qa.getId().id(), + qa.getQuestion().question(), + qa.getAnswer().answer()); + return qaResponseDTO; + } + + +// //新增mq添加到数据库的方法 +// @GetMapping("sendObject") +// public String sendObject() { +// //把消息交给mq处理 +// Qa qa = new Qa(); +// qa.setId(new QaId(1L)); +// qa.setQuestion(new Question("question")); +// qa.setAnswer(new Answer("answer")); +// rabbitTemplate.convertAndSend("qaTest",qa); +// return "success"; +// } + +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/ChatCompletionRequestDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/ChatCompletionRequestDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..a2185a9d31a7ff5bd2bc82f5ea066b939e41f5c0 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/ChatCompletionRequestDTO.java @@ -0,0 +1,12 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class ChatCompletionRequestDTO { + private String model; + private List messages; + private double temperature; +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/ChatCompletionResponseDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/ChatCompletionResponseDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..50c6bdef08b6b1bcdfceaf3d2cd7905835cb7ba0 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/ChatCompletionResponseDTO.java @@ -0,0 +1,14 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class ChatCompletionResponseDTO { + private String id; + private String object; + private long created; + private String model; + private List choices; +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Choice.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Choice.java new file mode 100644 index 0000000000000000000000000000000000000000..586ca17d553d44fbb202e49a74dd10a6058b0044 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Choice.java @@ -0,0 +1,10 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +import lombok.Data; + +@Data +public class Choice { + private int index; + private Message message; + private String finish_reason; +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/CreateQaRequestDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/CreateQaRequestDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..dc81be00179aa6819f88340fdd400be7bdfa13c1 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/CreateQaRequestDTO.java @@ -0,0 +1,6 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +public record CreateQaRequestDTO( + String question, + String answer) { +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Message.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Message.java new file mode 100644 index 0000000000000000000000000000000000000000..ffd0d8c9cb856c606ab9eb700577370c384d176d --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Message.java @@ -0,0 +1,9 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +import lombok.Data; + +@Data +public class Message { + private String role; + private String content; +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/QaInfo.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/QaInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..68cf3231019ba0bc7376ca5fbaa9d4852fc1c7bc --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/QaInfo.java @@ -0,0 +1,50 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 用户信息DTO (Data Transfer Object) + * 用于封装用户的基本信息,在不同层之间传输 + * + * DTO是数据传输对象,主要用于在不同层之间传输数据 + * 这里专门用于传输用户的基本信息,不包含敏感信息如密码 + * + * @Data Lombok注解,自动生成getter、setter、toString等方法 + * @Builder Lombok注解,提供Builder模式构建对象,使代码更清晰易读 + * @NoArgsConstructor Lombok注解,生成无参构造函数 + * @AllArgsConstructor Lombok注解,生成全参构造函数 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QaInfo { + + /** + * 用户ID + * + * 数据库中的主键,唯一标识一个用户 + * 使用Long类型可以支持更大的数据量 + */ + private Long id; + + /** + * 问题 + * + * 用户提出的问题 + */ + private String question; + + /** + * 答案 + * + * 系统回复的答案 + */ + private String answer; + +} diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserLoginRequestDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/QaResponseDTO.java similarity index 33% rename from user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserLoginRequestDTO.java rename to qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/QaResponseDTO.java index 112434d3ec034fbfe024cf9148d6a805976e0536..a7839ad089ad39219f7e3d82941e2f17f0a2f72e 100644 --- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserLoginRequestDTO.java +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/QaResponseDTO.java @@ -1,4 +1,4 @@ -package com.example.user.adapter.in.web.dto; +package org.example.xmut.qa.adapter.in.web.dto; import lombok.AllArgsConstructor; import lombok.Data; @@ -7,8 +7,8 @@ import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor -public class UserLoginRequestDTO { - private String name; - // 这里的password指的是用户输入的密码,而不是数据库中的密码。所以这里应该使用明文,类型是String - private String password; +public class QaResponseDTO { + private Long id; + private String question; + private String answer; } diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Result.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Result.java new file mode 100644 index 0000000000000000000000000000000000000000..24c5b9799e34d08a02677eb92c727a39b33fda52 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Result.java @@ -0,0 +1,149 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 通用响应结果类 + * 用于封装所有API接口的响应数据,统一返回格式 + * + * 这样做有以下好处: + * 1. 前端可以统一处理响应格式 + * 2. 便于统一错误处理 + * 3. 提高代码的可维护性 + * + * 使用泛型可以让这个类适用于任何类型的数据 + * + * @Data Lombok注解,自动生成getter、setter、toString等方法 + * @NoArgsConstructor Lombok注解,生成无参构造函数 + * @AllArgsConstructor Lombok注解,生成全参构造函数 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Result { + + /** + * 响应码 + * + * 用于表示请求处理的结果状态 + * 常见的响应码: + * 200 - 成功 + * 400 - 请求参数错误 + * 401 - 未授权 + * 403 - 禁止访问 + * 500 - 服务器内部错误 + */ + private Integer code; + + /** + * 响应消息 + * + * 用于描述请求处理的结果信息 + * 成功时可以是"操作成功" + * 失败时可以是具体的错误信息,如"用户名或密码错误" + */ + private String message; + + /** + * 响应数据 + * + * 用于携带具体的业务数据 + * 可以是任何类型,如用户信息、列表数据等 + * 使用泛型T使得这个字段可以适应不同类型的数据 + */ + private T data; + + /** + * 成功响应静态方法 + * + * 用于创建成功的响应结果 + * + * @param data 响应数据 + * @return Result 成功的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result success(T data) { + return new Result<>(200, "操作成功", data); + } + + /** + * 成功响应静态方法(无数据) + * + * 用于创建不携带数据的成功响应结果 + * + * @return Result 成功的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result success() { + return new Result<>(200, "操作成功", null); + } + + /** + * 成功响应静态方法(自定义消息) + * + * 用于创建携带自定义成功消息的响应结果 + * + * @param message 自定义的成功消息 + * @param data 响应数据 + * @return Result 成功的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result success(String message, T data) { + return new Result<>(200, message, data); + } + + /** + * 失败响应静态方法 + * + * 用于创建失败的响应结果,默认使用500状态码 + * + * @param message 错误消息 + * @return Result 失败的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result error(String message) { + return new Result<>(500, message, null); + } + + /** + * 失败响应静态方法(自定义状态码) + * + * 用于创建携带自定义状态码的失败响应结果 + * + * @param code 自定义状态码 + * @param message 错误消息 + * @return Result 失败的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result error(Integer code, String message) { + return new Result<>(code, message, null); + } + + /** + * 未授权响应静态方法 + * + * 用于创建401未授权的响应结果 + * + * @param message 错误消息 + * @return Result 未授权的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result unauthorized(String message) { + return new Result<>(401, message, null); + } + + /** + * 禁止访问响应静态方法 + * + * 用于创建403禁止访问的响应结果 + * + * @param message 错误消息 + * @return Result 禁止访问的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result forbidden(String message) { + return new Result<>(403, message, null); + } +} \ No newline at end of file diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/UpdateQaRequestDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/UpdateQaRequestDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..25875effda3e66191da622cc56bd0d255789803a --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/UpdateQaRequestDTO.java @@ -0,0 +1,6 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +public record UpdateQaRequestDTO(long id, + String question, + String answer) { +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/exception/GlobalExceptionHandler.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..b0f990d59b47e0d11bd32a5976c27d50d091cb85 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/exception/GlobalExceptionHandler.java @@ -0,0 +1,259 @@ +package org.example.xmut.qa.adapter.in.web.exception; + +import org.example.xmut.qa.adapter.in.web.dto.Result; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.stream.Collectors; + +/** + * 全局异常处理器 + * 统一处理应用中的各种异常,避免异常直接暴露给用户 + * + * @Slf4j Lombok注解,自动生成日志对象log,用于记录异常日志 + * @RestControllerAdvice 组合注解,相当于@ControllerAdvice + @ResponseBody + * 用于定义全局异常处理器,可以捕获控制器层抛出的异常 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理认证异常 + * + * 当用户认证失败时(如Token无效、过期等)会抛出AuthenticationException + * + * @ExceptionHandler 注解指定该方法处理哪种异常 + * @ResponseStatus 注解指定返回的HTTP状态码 + * + * @param e 认证异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(AuthenticationException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public Result handleAuthenticationException(AuthenticationException e) { + log.error("认证异常: {}", e.getMessage()); + return Result.unauthorized("认证失败: " + e.getMessage()); + } + + /** + * 处理凭据错误异常 + * + * 当用户名或密码错误时会抛出BadCredentialsException + * 这是AuthenticationException的一个子类 + * + * @param e 凭据错误异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(BadCredentialsException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public Result handleBadCredentialsException(BadCredentialsException e) { + log.error("凭据错误: {}", e.getMessage()); + return Result.unauthorized("用户名或密码错误"); + } + + /** + * 处理访问拒绝异常 + * + * 当已认证用户尝试访问没有权限的资源时会抛出AccessDeniedException + * + * @param e 访问拒绝异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleAccessDeniedException(AccessDeniedException e) { + log.error("访问拒绝: {}", e.getMessage()); + return Result.forbidden("访问被拒绝,权限不足"); + } + + /** + * 处理参数校验异常 - @Valid注解 + * + * 当使用@Valid注解验证请求参数失败时会抛出MethodArgumentNotValidException + * 例如LoginRequest中的用户名或密码不符合验证规则 + * + * @param e 参数校验异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + // 收集所有字段的错误信息并拼接成字符串 + String errorMessage = e.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.error("参数校验失败: {}", errorMessage); + return Result.error("参数校验失败: " + errorMessage); + } + + /** + * 处理参数绑定异常 + * + * 当请求参数绑定到对象时发生错误会抛出BindException + * + * @param e 参数绑定异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBindException(BindException e) { + // 收集所有字段的错误信息并拼接成字符串 + String errorMessage = e.getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.error("参数绑定失败: {}", errorMessage); + return Result.error("参数绑定失败: " + errorMessage); + } + + /** + * 处理约束违反异常 + * + * 当使用Bean Validation API验证失败时会抛出ConstraintViolationException + * + * @param e 约束违反异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleConstraintViolationException(ConstraintViolationException e) { + // 收集所有约束违反的错误信息并拼接成字符串 + String errorMessage = e.getConstraintViolations().stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.joining(", ")); + log.error("约束违反: {}", errorMessage); + return Result.error("参数校验失败: " + errorMessage); + } + + /** + * 处理非法参数异常 + * + * 当传递给方法的参数不合法时会抛出IllegalArgumentException + * + * @param e 非法参数异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数: {}", e.getMessage()); + return Result.error("参数错误: " + e.getMessage()); + } + + /** + * 处理空指针异常 + * + * 当尝试访问空对象的属性或方法时会抛出NullPointerException + * + * @param e 空指针异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(NullPointerException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleNullPointerException(NullPointerException e) { + log.error("空指针异常", e); + return Result.error("系统内部错误,请联系管理员"); + } + + /** + * 处理运行时异常 + * + * 当发生未预期的运行时错误时会抛出RuntimeException + * 这是很多异常的父类 + * + * @param e 运行时异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(RuntimeException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleRuntimeException(RuntimeException e) { + log.error("运行时异常: {}", e.getMessage(), e); + return Result.error("系统异常: " + e.getMessage()); + } + + /** + * 处理其他所有异常 + * + * 作为兜底的异常处理方法,处理所有未被上面方法处理的异常 + * + * @param e 异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleException(Exception e) { + log.error("未知异常: {}", e.getMessage(), e); + return Result.error("系统内部错误,请联系管理员"); + } + + /** + * 自定义业务异常类 + * + * 用于处理应用程序中特定的业务异常 + * 继承自RuntimeException,是一个受检异常 + */ + public static class BusinessException extends RuntimeException { + /** + * 异常码 + * + * 用于标识异常的类型,便于前端进行不同的处理 + */ + private final int code; + + /** + * 构造函数 - 只有消息 + * + * @param message 异常消息 + */ + public BusinessException(String message) { + super(message); + this.code = 500; + } + + /** + * 构造函数 - 有码和消息 + * + * @param code 异常码 + * @param message 异常消息 + */ + public BusinessException(int code, String message) { + super(message); + this.code = code; + } + + /** + * 获取异常码 + * + * @return int 异常码 + */ + public int getCode() { + return code; + } + } + + /** + * 处理自定义业务异常 + * + * 处理应用程序中抛出的BusinessException + * + * @param e 业务异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBusinessException(BusinessException e) { + log.error("业务异常: {}", e.getMessage()); + return Result.error(e.getCode(), e.getMessage()); + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/listener/QaListener.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/listener/QaListener.java new file mode 100644 index 0000000000000000000000000000000000000000..ebba911840e7aa085f4167857a6c043c8029d872 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/listener/QaListener.java @@ -0,0 +1,23 @@ +package org.example.xmut.qa.adapter.in.web.listener; + +import jakarta.annotation.Resource; + +import org.example.xmut.qa.adapter.out.persistence.convertor.QaConvertor; +import org.example.xmut.qa.adapter.out.persistence.entity.QaEntity; +import org.example.xmut.qa.adapter.out.persistence.mapper.QaMapper; +import org.example.xmut.qa.service.domain.Qa; +import org.springframework.amqp.rabbit.annotation.RabbitHandler; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +@RabbitListener(queues = "qaTest") +public class QaListener { + @Resource + private QaMapper qaMapper; + @RabbitHandler + public void handler(Qa qa){ + QaEntity qaEntity = QaConvertor.toEntity(qa); + qaMapper.insert(qaEntity); + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/pom.xml b/qa-service/qa-service-adapter/qa-adapter-out/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..dfc017ba19ebbd1648d399197503113f06c9bf31 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + com.example + qa-adapter-out + 0.0.1-SNAPSHOT + qa-adapter-out + qa-adapter-out + + pom + + + com.example + qa-service-adapter + 0.0.1-SNAPSHOT + + + + qa-adapter-out-persistence + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/pom.xml b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0e3616277424c59b56bf76d1e2e9529a1ac62093 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + com.example + qa-adapter-out-persistence + 0.0.1-SNAPSHOT + qa-adapter-out-persistence + qa-adapter-out-persistence + + com.example + qa-adapter-out + 0.0.1-SNAPSHOT + + + + + org.projectlombok + lombok + provided + + + + com.example + qa-service-domain + 0.0.1-SNAPSHOT + + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + + com.mysql + mysql-connector-j + runtime + + + com.example + qa-service-application + 0.0.1-SNAPSHOT + compile + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/CreateQaBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/CreateQaBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..c8304c87d7b05226a4de41057d4eacb1cae724c2 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/CreateQaBridge.java @@ -0,0 +1,26 @@ +package org.example.xmut.qa.adapter.out.persistence.bridge; + +import org.example.xmut.qa.adapter.out.persistence.convertor.QaConvertor; +import org.example.xmut.qa.adapter.out.persistence.entity.QaEntity; +import org.example.xmut.qa.adapter.out.persistence.mapper.QaMapper; +import org.example.xmut.qa.service.domain.Qa; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.example.xmut.qa.service.domain.port.CreateQaPort; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class CreateQaBridge implements CreateQaPort { + @Resource + private QaMapper qaMapper; + + @Override + public Qa createQa(Qa qa) { + QaEntity qaEntity = QaConvertor.toEntity(qa); + int result = qaMapper.insert(qaEntity); + //result 指受影响行数 + log.info("result:{}",result); + return qa; + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/DeleteQaBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/DeleteQaBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..1fc5ef78545122534297c0a420bd9903a4361732 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/DeleteQaBridge.java @@ -0,0 +1,19 @@ +package org.example.xmut.qa.adapter.out.persistence.bridge; + +import org.example.xmut.qa.adapter.out.persistence.mapper.QaMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.example.xmut.qa.service.domain.port.DeleteQaPort; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class DeleteQaBridge implements DeleteQaPort { + @Resource + private QaMapper qaMapper; + @Override + public void deleteQa(Long id) { + int result = qaMapper.deleteById(id); + log.info("result:{}",result); + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/GetQaByIdBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/GetQaByIdBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..be1151422770ae8dd195179d47a39e35dfe05d64 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/GetQaByIdBridge.java @@ -0,0 +1,22 @@ +package org.example.xmut.qa.adapter.out.persistence.bridge; + +import org.example.xmut.qa.adapter.out.persistence.convertor.QaConvertor; +import org.example.xmut.qa.adapter.out.persistence.entity.QaEntity; +import org.example.xmut.qa.adapter.out.persistence.mapper.QaMapper; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.port.GetQaByIdPort; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class GetQaByIdBridge implements GetQaByIdPort { + @Resource + private QaMapper qaMapper; + @Override + public Qa getQaById(Long id) { + QaEntity qaEntity = qaMapper.selectById(id); + return QaConvertor.toDomain(qaEntity); + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/GetQaListBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/GetQaListBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..66cf65f94b4b957259a3d6b39129303a80e8025c --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/GetQaListBridge.java @@ -0,0 +1,32 @@ +package org.example.xmut.qa.adapter.out.persistence.bridge; + +import org.example.xmut.qa.adapter.out.persistence.convertor.QaConvertor; +import org.example.xmut.qa.adapter.out.persistence.entity.QaEntity; +import org.example.xmut.qa.adapter.out.persistence.mapper.QaMapper; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.port.GetQaListPort; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class GetQaListBridge implements GetQaListPort { + + @Resource + private QaMapper qaMapper; + + @Override + public List getQas() { + List entities = qaMapper.selectList(null); + + ArrayList list = new ArrayList<>(); + + entities.forEach(qaEntity -> { + Qa qa = QaConvertor.toDomain(qaEntity); + list.add(qa); + }); + return list; + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/UpdateQaBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/UpdateQaBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..f848b6b2a06826b95be6686db8417b96f5bb6e38 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/UpdateQaBridge.java @@ -0,0 +1,24 @@ +package org.example.xmut.qa.adapter.out.persistence.bridge; + +import org.example.xmut.qa.adapter.out.persistence.convertor.QaConvertor; +import org.example.xmut.qa.adapter.out.persistence.mapper.QaMapper; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.port.UpdateQaPort; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class UpdateQaBridge implements UpdateQaPort { + @Resource + private QaMapper qaMapper; + + + @Override + public Qa updateQa(Qa qa) { + int result = qaMapper.updateById(QaConvertor.toEntity(qa)); + log.info("result:{}",result); + return qa; + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/convertor/QaConvertor.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/convertor/QaConvertor.java new file mode 100644 index 0000000000000000000000000000000000000000..6edfa606b082176b9c10a1a82d391040d7c950d7 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/convertor/QaConvertor.java @@ -0,0 +1,25 @@ +package org.example.xmut.qa.adapter.out.persistence.convertor; + +import org.example.xmut.qa.adapter.out.persistence.entity.QaEntity; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.valueobject.*; + +public class QaConvertor { + + public static Qa toDomain(QaEntity qaEntity) { + return new Qa( + new QaId(qaEntity.getId()), + new Question(qaEntity.getQuestion()), + new Answer(qaEntity.getAnswer()) + ); + } + + public static QaEntity toEntity(Qa qa) { + return new QaEntity( + qa.getId().id(), + qa.getQuestion().question(), + qa.getAnswer().answer() + ); + } +} + diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/entity/QaEntity.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/entity/QaEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..2953d0e42c21e6cfd2fdb73919685d06fc290827 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/entity/QaEntity.java @@ -0,0 +1,29 @@ +package org.example.xmut.qa.adapter.out.persistence.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName("sys_user") +public class QaEntity { + @TableId(type = IdType.AUTO) + private Long id; + + private String question; + + private String answer; + + public QaEntity(long id, String question, String answer) { + this.id = id; + this.question = question; + this.answer = answer; + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/mapper/QaMapper.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/mapper/QaMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..7f40a22a231a339410c9f7c5711421b794c42869 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/mapper/QaMapper.java @@ -0,0 +1,15 @@ +package org.example.xmut.qa.adapter.out.persistence.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import org.example.xmut.qa.adapter.out.persistence.entity.QaEntity; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + + +public interface QaMapper extends BaseMapper{ + + @Select("SELECT * FROM sys_qa WHERE id = #{id}") + QaEntity selectById(@Param("id") Long id); +} diff --git a/qa-service/qa-service-application/pom.xml b/qa-service/qa-service-application/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8d9eb8b60832982af28d84f7a38d2ae55832bfa6 --- /dev/null +++ b/qa-service/qa-service-application/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + com.example + qa-service-application + 0.0.1-SNAPSHOT + qa-service-application + qa-service-application + + + com.example + qa-service + 0.0.1-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + provided + + + + com.example + qa-service-domain + 0.0.1-SNAPSHOT + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/command/CreateQaCommand.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/command/CreateQaCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..95881488e9e59df2f8d33533d8fc5ca1f2f451ef --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/command/CreateQaCommand.java @@ -0,0 +1,11 @@ +package org.example.xmut.qa.service.application.command; + +import lombok.Builder; + +@Builder +public record CreateQaCommand( + Long id, + String question, + String answer +) { +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/command/UpdateQaCommand.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/command/UpdateQaCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..fa5f5ba2b6114b03c5ab8f968f5c489102de02ff --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/command/UpdateQaCommand.java @@ -0,0 +1,9 @@ +package org.example.xmut.qa.service.application.command; + +import lombok.Builder; + +@Builder +public record UpdateQaCommand(Long id, + String question, + String answer) { +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/CreateQaUseCase.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/CreateQaUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..c884e62a1bdbc0053982c34877efb879c1f18dc0 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/CreateQaUseCase.java @@ -0,0 +1,8 @@ +package org.example.xmut.qa.service.application.port.in; + +import org.example.xmut.qa.service.application.command.CreateQaCommand; +import org.example.xmut.qa.service.domain.Qa; + +public interface CreateQaUseCase { + Qa createQa(CreateQaCommand createQaCommand); +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/DeleteQaUseCase.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/DeleteQaUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..cd49ae62366940c6634b869fda149e0e7ffa005a --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/DeleteQaUseCase.java @@ -0,0 +1,5 @@ +package org.example.xmut.qa.service.application.port.in; + +public interface DeleteQaUseCase { + void deleteQa(Long id); +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/GetQaByIdUseCase.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/GetQaByIdUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..51df2816defcb6bc43e9e8355df91aacf355c38c --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/GetQaByIdUseCase.java @@ -0,0 +1,7 @@ +package org.example.xmut.qa.service.application.port.in; + +import org.example.xmut.qa.service.domain.Qa; + +public interface GetQaByIdUseCase { + Qa getQaById(Long id); +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/GetQaListUseCase.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/GetQaListUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..78bf93c44bcb87d5e1b305dcbe6cb6b168dec2a6 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/GetQaListUseCase.java @@ -0,0 +1,10 @@ +package org.example.xmut.qa.service.application.port.in; + +import org.example.xmut.qa.service.domain.Qa; + +import java.util.List; + +public interface GetQaListUseCase { + + List getQas(); +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/UpdateQaUseCase.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/UpdateQaUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..16a7d4ef550894837a1feba34e7942e476a79087 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/UpdateQaUseCase.java @@ -0,0 +1,10 @@ +package org.example.xmut.qa.service.application.port.in; + + +import org.example.xmut.qa.service.application.command.UpdateQaCommand; +import org.example.xmut.qa.service.domain.Qa; + +public interface UpdateQaUseCase { + + Qa updateQa(UpdateQaCommand command); +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/CreateQaService.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/CreateQaService.java new file mode 100644 index 0000000000000000000000000000000000000000..d7a60ccd9a7ec2c4e16ca4e367dcd81f46743ce4 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/CreateQaService.java @@ -0,0 +1,29 @@ +package org.example.xmut.qa.service.application.service; + +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.example.xmut.qa.service.application.command.CreateQaCommand; +import org.example.xmut.qa.service.application.port.in.CreateQaUseCase; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.port.CreateQaPort; +import org.example.xmut.qa.service.domain.valueobject.Answer; +import org.example.xmut.qa.service.domain.valueobject.QaId; +import org.example.xmut.qa.service.domain.valueobject.Question; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class CreateQaService implements CreateQaUseCase { + @Resource + private CreateQaPort createQaPort; + @Override + public Qa createQa(CreateQaCommand createQaCommand) { + //command -> domain + Qa qa=new Qa( + new Question(createQaCommand.question()), new Question(createQaCommand.question()), + new Answer(createQaCommand.answer()) + ); + log.info("qa:{}",qa); + return createQaPort.createQa(qa); + } +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/DeleteQaService.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/DeleteQaService.java new file mode 100644 index 0000000000000000000000000000000000000000..6db5748813c3ae82582ee7f44c74f1ae71ca7d6f --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/DeleteQaService.java @@ -0,0 +1,17 @@ +package org.example.xmut.qa.service.application.service; + + +import jakarta.annotation.Resource; +import org.example.xmut.qa.service.application.port.in.DeleteQaUseCase; +import org.example.xmut.qa.service.domain.port.DeleteQaPort; +import org.springframework.stereotype.Service; + +@Service +public class DeleteQaService implements DeleteQaUseCase { + @Resource + private DeleteQaPort deleteQaPort; + @Override + public void deleteQa(Long id) { + deleteQaPort.deleteQa(id); + } +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/GetQaByIdService.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/GetQaByIdService.java new file mode 100644 index 0000000000000000000000000000000000000000..72cb81cd81ac56561323a553be0cbcdcdafa9822 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/GetQaByIdService.java @@ -0,0 +1,18 @@ +package org.example.xmut.qa.service.application.service; + +import jakarta.annotation.Resource; +import org.example.xmut.qa.service.application.port.in.GetQaByIdUseCase; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.port.GetQaByIdPort; +import org.springframework.stereotype.Service; + +@Service +public class GetQaByIdService implements GetQaByIdUseCase { + + @Resource + private GetQaByIdPort getQaByIdPort; + @Override + public Qa getQaById(Long id) { + return getQaByIdPort.getQaById(id); + } +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/GetQaListService.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/GetQaListService.java new file mode 100644 index 0000000000000000000000000000000000000000..4b49c493864e65777af32fe8e65d0dde158a21bf --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/GetQaListService.java @@ -0,0 +1,22 @@ +package org.example.xmut.qa.service.application.service; + + +import jakarta.annotation.Resource; +import org.example.xmut.qa.service.application.port.in.GetQaListUseCase; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.port.GetQaListPort; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class GetQaListService implements GetQaListUseCase { + + @Resource + GetQaListPort getQaListPort; + @Override + public List getQas() { + List qa = Qa.getQas(getQaListPort); + return qa; + } +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/UpdateQaService.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/UpdateQaService.java new file mode 100644 index 0000000000000000000000000000000000000000..1e5448e65bc83a64ff6c75024b6b8c90fb03e170 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/UpdateQaService.java @@ -0,0 +1,26 @@ +package org.example.xmut.qa.service.application.service; + +import jakarta.annotation.Resource; +import org.example.xmut.qa.service.application.command.UpdateQaCommand; +import org.example.xmut.qa.service.application.port.in.UpdateQaUseCase; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.port.UpdateQaPort; +import org.example.xmut.qa.service.domain.valueobject.Answer; +import org.example.xmut.qa.service.domain.valueobject.QaId; +import org.example.xmut.qa.service.domain.valueobject.Question; +import org.springframework.stereotype.Service; + +@Service +public class UpdateQaService implements UpdateQaUseCase { + @Resource + private UpdateQaPort updateQaPort; + + @Override + public Qa updateQa(UpdateQaCommand command) { + Qa qa = new Qa( + new QaId(command.id()), + new Question(command.question()), + new Answer(command.answer())); + return updateQaPort.updateQa(qa); + } +} diff --git a/qa-service/qa-service-bootstrap/pom.xml b/qa-service/qa-service-bootstrap/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..0ddd8f7654a6cb9626282fa50e7fc26b9359dfa1 --- /dev/null +++ b/qa-service/qa-service-bootstrap/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + com.example + qa-service-bootstrap + 0.0.1-SNAPSHOT + qa-service-bootstrap + qa-service-bootstrap + + + com.example + qa-service + 0.0.1-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.example + qa-adapter-in-web + 0.0.1-SNAPSHOT + + + + com.example + qa-adapter-out-persistence + 0.0.1-SNAPSHOT + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + com.example.qa.service.bootstrap.qaServiceBootstrapApplication + false + + + + repackage + + repackage + + + + + + + + diff --git a/qa-service/qa-service-bootstrap/src/main/java/org/example/xmut/qa/service/bootstrap/QaServiceBootstrapApplication.java b/qa-service/qa-service-bootstrap/src/main/java/org/example/xmut/qa/service/bootstrap/QaServiceBootstrapApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..4b1d931b402072c9d4abea74f9885f8a9ff6c64c --- /dev/null +++ b/qa-service/qa-service-bootstrap/src/main/java/org/example/xmut/qa/service/bootstrap/QaServiceBootstrapApplication.java @@ -0,0 +1,13 @@ +package org.example.xmut.qa.service.bootstrap; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class QaServiceBootstrapApplication { + + public static void main(String[] args) { + SpringApplication.run(QaServiceBootstrapApplication.class, args); + } + +} diff --git a/qa-service/qa-service-bootstrap/src/main/resources/application.properties b/qa-service/qa-service-bootstrap/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..9ca871a766b1c282419bece2b4bc0db88689ed5b --- /dev/null +++ b/qa-service/qa-service-bootstrap/src/main/resources/application.properties @@ -0,0 +1,25 @@ +server.port=28080 + +spring.application.name=qa-service + + + +# Nacos认证信息 +spring.cloud.nacos.discovery.username=nacos +spring.cloud.nacos.discovery.password=nacos +# Nacos 服务发现与注册配置,其中子属性 server-addr 指定 Nacos 服务器主机和端口 +spring.cloud.nacos.discovery.server-addr=192.168.168.128:8848 +# 注册到 nacos 的指定 namespace,默认为 public +spring.cloud.nacos.discovery.namespace=public + +# Nacos帮助文档: https://nacos.io/zh-cn/docs/concepts.html +# Nacos认证信息 +spring.cloud.nacos.config.username=nacos +spring.cloud.nacos.config.password=nacos +spring.cloud.nacos.config.contextPath=/nacos +# 设置配置中心服务端地址 +spring.cloud.nacos.config.server-addr=192.168.168.128:8848 +# Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可 +# spring.cloud.nacos.config.namespace= +spring.config.import=nacos:${spring.application.name}.properties?refresh=true + diff --git a/qa-service/qa-service-common/pom.xml b/qa-service/qa-service-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..5a51d3cc0e4d9070669b531a02d5663a6bfca2a7 --- /dev/null +++ b/qa-service/qa-service-common/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + com.example + qa-service-common + 0.0.1-SNAPSHOT + qa-service-common + qa-service-common + + 21 + UTF-8 + UTF-8 + 3.2.4 + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + org.springframework.boot + spring-boot-starter + + + io.jsonwebtoken + jjwt + 0.9.1 + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + com.sun.xml.bind + jaxb-core + 2.3.0.1 + + + com.sun.xml.bind + jaxb-impl + 2.3.3 + + + javax.activation + activation + 1.1.1 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + provided + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/qa-service-common/src/main/java/org/example/xmut/qa/service/common/IdWorker.java b/qa-service/qa-service-common/src/main/java/org/example/xmut/qa/service/common/IdWorker.java new file mode 100644 index 0000000000000000000000000000000000000000..6035356b79a57b0f55e39f4a39e5ae1577b2d0c7 --- /dev/null +++ b/qa-service/qa-service-common/src/main/java/org/example/xmut/qa/service/common/IdWorker.java @@ -0,0 +1,199 @@ +package org.example.xmut.qa.service.common; + +import java.lang.management.ManagementFactory; +import java.net.InetAddress; +import java.net.NetworkInterface; + +/** + * @author wuyunbin + *

名称:IdWorker.java

+ *

描述:分布式自增长ID

+ *
+ *     Twitter的 Snowflake JAVA实现方案
+ * 
+ * 核心代码为其IdWorker这个类实现,其原理结构如下,我分别用一个0表示一位,用—分割开部分的作用: + * 1||0---0000000000 0000000000 0000000000 0000000000 0 --- 00000 ---00000 ---000000000000 + * 在上面的字符串中,第一位为未使用(实际上也可作为long的符号位),接下来的41位为毫秒级时间, + * 然后5位datacenter标识位,5位机器ID(并不算标识符,实际是为线程标识), + * 然后12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个Long型。 + * 这样的好处是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分), + * 并且效率较高,经测试,snowflake每秒能够产生26万ID左右,完全满足需要。 + *

+ * 64位ID (42(毫秒)+5(机器ID)+5(业务编码)+12(重复累加)) + * @author Polim + */ +public class IdWorker { + /** + * 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动) + */ + private final static long TWEPOCH = 1288834974657L; + + /** + * 机器标识位数 + */ + private final static long WORKER_ID_BITS = 5L; + + /** + * 数据中心标识位数 + */ + private final static long DATA_CENTER_ID_BITS = 5L; + + /** + * 机器ID最大值 + */ + private final static long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS); + + /** + * 数据中心ID最大值 + */ + private final static long MAX_DATACENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS); + + /** + * 毫秒内自增位 + */ + private final static long SEQUENCE_BITS = 12L; + + /** + * 机器ID偏左移12位 + */ + private final static long WORKER_ID_SHIFT = SEQUENCE_BITS; + + /** + * 数据中心ID左移17位 + */ + private final static long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; + + /** + * 时间毫秒左移22位 + */ + private final static long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS; + + private final static long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS); + + /** + * 上次生产id时间戳 + */ + private static long lastTimestamp = -1L; + + /** + * 0,并发控制 + */ + private long sequence = 0L; + + private final long workerId; + + /** + * 数据标识id部分 + */ + private final long datacenterId; + + public IdWorker() { + this.datacenterId = getDatacenterId(); + this.workerId = getMaxWorkerId(datacenterId); + } + + /** + * @param workerId 工作机器ID + * @param datacenterId 序列号 + */ + public IdWorker(long workerId, long datacenterId) { + if (workerId > MAX_WORKER_ID || workerId < 0) { + throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID)); + } + if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) { + throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", MAX_DATACENTER_ID)); + } + this.workerId = workerId; + this.datacenterId = datacenterId; + } + + /** + * 获取下一个ID + * + * @return + */ + public synchronized long nextId() { + long timestamp = timeGen(); + if (timestamp < lastTimestamp) { + throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); + } + + if (lastTimestamp == timestamp) { + // 当前毫秒内,则+1 + sequence = (sequence + 1) & SEQUENCE_MASK; + if (sequence == 0) { + // 当前毫秒内计数满了,则等待下一秒 + timestamp = tilNextMillis(lastTimestamp); + } + } else { + sequence = 0L; + } + lastTimestamp = timestamp; + // ID偏移组合生成最终的ID,并返回ID + + return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT) + | (datacenterId << DATACENTER_ID_SHIFT) + | (workerId << WORKER_ID_SHIFT) | sequence; + } + + private long tilNextMillis(final long lastTimestamp) { + long timestamp = this.timeGen(); + while (timestamp <= lastTimestamp) { + timestamp = this.timeGen(); + } + return timestamp; + } + + private long timeGen() { + return System.currentTimeMillis(); + } + + /** + *

+ * 获取 MAX_WORKER_ID + *

+ */ + protected static long getMaxWorkerId(long datacenterId) { + StringBuilder mpid = new StringBuilder(); + mpid.append(datacenterId); + String name = ManagementFactory.getRuntimeMXBean().getName(); + if (!name.isEmpty()) { + /* + * GET jvmPid + */ + mpid.append(name.split("@")[0]); + } + /* + * MAC + PID 的 hashcode 获取16个低位 + */ + return (mpid.toString().hashCode() & 0xffff) % (IdWorker.MAX_WORKER_ID + 1); + } + + /** + *

+ * 数据标识id部分 + *

+ */ + protected static long getDatacenterId() { + long id = 0L; + try { + InetAddress ip = InetAddress.getLocalHost(); + NetworkInterface network = NetworkInterface.getByInetAddress(ip); + if (network == null) { + id = 1L; + } else { + byte[] mac = network.getHardwareAddress(); + id = ((0x000000FF & (long) mac[mac.length - 1]) + | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6; + id = id % (IdWorker.MAX_DATACENTER_ID + 1); + } + } catch (Exception e) { + System.out.println(" getDatacenterId: " + e.getMessage()); + } + return id; + } + + + + +} diff --git a/qa-service/qa-service-domain/pom.xml b/qa-service/qa-service-domain/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..19c5de593ebf5018ec8d278b36866db2876463a1 --- /dev/null +++ b/qa-service/qa-service-domain/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + com.example + qa-service-domain + 0.0.1-SNAPSHOT + qa-service-domain + qa-service-domain + + + com.example + qa-service + 0.0.1-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.security + spring-security-test + test + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + provided + + + + com.example + qa-service-common + 0.0.1-SNAPSHOT + + + org.springframework.security + spring-security-crypto + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + + diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/Qa.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/Qa.java new file mode 100644 index 0000000000000000000000000000000000000000..5d31b579eb853f06274d9b8ecfc7be5f22f210ca --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/Qa.java @@ -0,0 +1,41 @@ +package org.example.xmut.qa.service.domain; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.example.xmut.qa.service.domain.port.GetQaListPort; +import org.example.xmut.qa.service.domain.valueobject.Answer; +import org.example.xmut.qa.service.domain.valueobject.QaId; +import org.example.xmut.qa.service.domain.valueobject.Question; + +import java.util.List; + +@Slf4j +@Setter +@Getter +@ToString +public class Qa { + private QaId id; + private Question question; + private Answer answer; + + public Qa(QaId qaId, Question question, Answer answer) { + this.id = qaId; + this.question=question; + this.answer=answer; + } + + public Qa(Question question, Question question1, Answer answer) { + this.question=question; + this.answer=answer; + } + + public Qa() { + + } + + public static List getQas(GetQaListPort getQaListPort) { + return getQaListPort.getQas(); + } +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/CreateQaPort.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/CreateQaPort.java new file mode 100644 index 0000000000000000000000000000000000000000..8ddc319c5df1c9dc0cd6d62a0cef0595d1222f38 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/CreateQaPort.java @@ -0,0 +1,7 @@ +package org.example.xmut.qa.service.domain.port; + +import org.example.xmut.qa.service.domain.Qa; + +public interface CreateQaPort { + Qa createQa(Qa qa); +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/DeleteQaPort.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/DeleteQaPort.java new file mode 100644 index 0000000000000000000000000000000000000000..4b457c9f6da9832c7b1a248013bcadb372f9869c --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/DeleteQaPort.java @@ -0,0 +1,5 @@ +package org.example.xmut.qa.service.domain.port; + +public interface DeleteQaPort { + void deleteQa(Long id); +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/GetQaByIdPort.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/GetQaByIdPort.java new file mode 100644 index 0000000000000000000000000000000000000000..e09144e2ee7c23159ea7359ab02664f062f9e06c --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/GetQaByIdPort.java @@ -0,0 +1,8 @@ +package org.example.xmut.qa.service.domain.port; + + +import org.example.xmut.qa.service.domain.Qa; + +public interface GetQaByIdPort { + Qa getQaById(Long id); +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/GetQaListPort.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/GetQaListPort.java new file mode 100644 index 0000000000000000000000000000000000000000..e3417e49b2af554a9e5c3458378008a8079e2ab5 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/GetQaListPort.java @@ -0,0 +1,11 @@ +package org.example.xmut.qa.service.domain.port; + + + +import org.example.xmut.qa.service.domain.Qa; + +import java.util.List; + +public interface GetQaListPort { + List getQas(); +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/UpdateQaPort.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/UpdateQaPort.java new file mode 100644 index 0000000000000000000000000000000000000000..dffe38b60477297ab42b45886b111e3b4565726c --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/UpdateQaPort.java @@ -0,0 +1,8 @@ +package org.example.xmut.qa.service.domain.port; + + +import org.example.xmut.qa.service.domain.Qa; + +public interface UpdateQaPort { + Qa updateQa(Qa qa); +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/Answer.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/Answer.java new file mode 100644 index 0000000000000000000000000000000000000000..3e54ff75cfcb6c2dba31f7e7c4b67b051800edf5 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/Answer.java @@ -0,0 +1,7 @@ +package org.example.xmut.qa.service.domain.valueobject; + +public record Answer(String answer) { + public String getValue() { + return answer; + } +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/QaId.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/QaId.java new file mode 100644 index 0000000000000000000000000000000000000000..7ecddca8d407b7f291312b131f82c9ddfdf3e282 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/QaId.java @@ -0,0 +1,7 @@ +package org.example.xmut.qa.service.domain.valueobject; + +public record QaId(long id) { + public long getValue() { + return id; + } +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/Question.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/Question.java new file mode 100644 index 0000000000000000000000000000000000000000..9dd7f5db13715cdeb63b679bdfdeee122d3d9dc5 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/Question.java @@ -0,0 +1,7 @@ +package org.example.xmut.qa.service.domain.valueobject; + +public record Question(String question) { + public String getValue() { + return question; + } +} diff --git a/user-service/pom.xml b/user-service/pom.xml index c69afc83c25b3353caf5990a6177cba309dcb683..931dcb0217161a7c3e4a2490a21b931486f42c88 100644 --- a/user-service/pom.xml +++ b/user-service/pom.xml @@ -69,6 +69,8 @@ knife4j-openapi3-jakarta-spring-boot-starter ${knife4j.version} + + diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController.java deleted file mode 100644 index 96820c00d7cf18e1f698ddc65b3e279601b5d576..0000000000000000000000000000000000000000 --- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.example.user.adapter.in.web.controller; - -import com.example.user.adapter.in.web.dto.CreateUserRequestDTO; -import com.example.user.adapter.in.web.dto.UpdateUserRequestDTO; -import com.example.user.adapter.in.web.dto.UserLoginRequestDTO; -import com.example.user.adapter.in.web.dto.UserResponseDTO; -import com.example.user.service.application.command.CreateUserCommand; -import com.example.user.service.application.command.UpdateUserCommand; -import com.example.user.service.application.command.UserLoginCommand; -import com.example.user.service.application.port.in.*; -import com.example.user.service.domain.User; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@Slf4j -@RequestMapping("/users") -@RestController -@RequiredArgsConstructor -public class UserController { - - private final GetUserListUseCase getUserListUseCase; - private final CreateUserUseCase createUserUseCase; - private final DeleteUserUseCase deleteUserUseCase; - private final UpdateUserUseCase updateUserUseCase; - private final GetUserByIdUseCase getUserByIdUseCase; - private final UserLoginUseCase userLoginUseCase; - - - @PostMapping("login") - public String login(@RequestBody UserLoginRequestDTO userLoginRequestDTO){ - log.info("UserLoginRequestDTO:{}",userLoginRequestDTO); - UserLoginCommand command=UserLoginCommand.builder() - .name(userLoginRequestDTO.getName()) - .password(userLoginRequestDTO.getPassword()) - .build(); - String token = userLoginUseCase.login(command); - return token; - } - - - - @GetMapping("") - public List getUsers() { - log.info("getUsers"); - return getUserListUseCase.getUsers(); - } - - /** - * 创建新用户 - * 功能:接收用户注册信息,验证密码一致性,创建新用户账户 - * @author dongxuanfeng - * @param createUserRequestDTO - * @return User - 成功创建的新用户 - * @throws IllegalArgumentException 当密码与确认密码不匹配时抛出此异常 - */ - @PostMapping() - public User createUser(@RequestBody CreateUserRequestDTO createUserRequestDTO){ - - if (!createUserRequestDTO.isPasswordValid()) { - throw new IllegalArgumentException("密码和确认密码不匹配"); - } - CreateUserCommand command=CreateUserCommand.builder() - .name(createUserRequestDTO.name()) - .age(createUserRequestDTO.age()) - .email(createUserRequestDTO.email()) - .password(createUserRequestDTO.password()) - .build(); - - return createUserUseCase.createUser(command); - } - - - @DeleteMapping("{id}") - public String deleteUser(@PathVariable("id") Long id){ - deleteUserUseCase.deleteUser(id); - return "success"; - } - - - @PutMapping("") - public User updateUser(@RequestBody UpdateUserRequestDTO updateUserRequestDTO){ - UpdateUserCommand command=UpdateUserCommand.builder() - .id(updateUserRequestDTO.id()) - .name(updateUserRequestDTO.name()) - .age(updateUserRequestDTO.age()) - .email(updateUserRequestDTO.email()) - .build(); - User user = updateUserUseCase.updateUser(command); - return user; - } - - - - @GetMapping("{id}") - public UserResponseDTO getUserById(@PathVariable("id") Long id){ - User user = getUserByIdUseCase.getUserById(id); - UserResponseDTO userResponseDTO = new UserResponseDTO( - user.getId().id(), - user.getName().username(), - user.getAge().age(), - user.getEmail().email(), - user.getIsSuper().value()); - return userResponseDTO; - } - -} diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginRequest.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..eb2ce38e1e0012007841f71ffeb4a6385e65e14b --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginRequest.java @@ -0,0 +1,47 @@ +package com.example.user.adapter.in.web.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +/** + * 登录请求DTO (Data Transfer Object) + * 用于封装用户登录时提交的数据 + * + * DTO是数据传输对象,主要用于在不同层之间传输数据 + * 这里专门用于接收用户登录请求的数据 + * + * @Data Lombok注解,自动生成getter、setter、toString等方法 + * @NoArgsConstructor Lombok注解,生成无参构造函数 + * @AllArgsConstructor Lombok注解,生成全参构造函数 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LoginRequest { + + /** + * 用户名字段 + * + * @NotBlank 验证注解,确保用户名不为空且去除空格后不为空 + * @Size 验证注解,限制用户名长度在3-20个字符之间 + * message 属性定义了验证失败时的错误提示信息 + */ + @NotBlank(message = "用户名不能为空") + @Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间") + private String username; + + /** + * 密码字段 + * + * @NotBlank 验证注解,确保密码不为空且去除空格后不为空 + * @Size 验证注解,限制密码长度在6-20个字符之间 + * message 属性定义了验证失败时的错误提示信息 + */ + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间") + private String password; +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginResponse.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..8017dc9ce70779bd3036340b037b52c3e0e3080c --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginResponse.java @@ -0,0 +1,59 @@ +package com.example.user.adapter.in.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 登录响应DTO (Data Transfer Object) + * 用于封装用户登录成功后的响应数据 + * + * DTO是数据传输对象,主要用于在不同层之间传输数据 + * 这里专门用于返回用户登录成功后的数据 + * + * @Data Lombok注解,自动生成getter、setter、toString等方法 + * @Builder Lombok注解,提供Builder模式构建对象,使代码更清晰易读 + * @NoArgsConstructor Lombok注解,生成无参构造函数 + * @AllArgsConstructor Lombok注解,生成全参构造函数 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LoginResponse { + + /** + * JWT访问令牌 + * + * 这是用户登录成功后生成的JWT Token + * 客户端在后续请求中需要在请求头中携带这个Token进行身份认证 + * 格式通常是: Authorization: Bearer + */ + private String accessToken; + + /** + * 令牌类型 + * + * 默认值为"Bearer",表示这是一个Bearer Token + * Bearer Token是一种HTTP认证方案,用于OAuth 2.0等认证协议 + */ + private String tokenType = "Bearer"; + + /** + * 用户信息 + * + * 包含登录用户的基本信息,如用户名、邮箱、角色等 + * 这样客户端登录后可以直接获取用户信息,无需再次请求 + */ + private UserInfo userInfo; + + /** + * 令牌过期时间(毫秒时间戳) + * + * 表示这个Token将在什么时候过期 + * 客户端可以根据这个时间判断是否需要重新登录或刷新Token + * 这里设置为当前时间加上24小时(86400000毫秒) + */ + private Long expiresIn; +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/RegisterRequest.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/RegisterRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..cfbccc25e331ac97c970cdda1652091001a75ead --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/RegisterRequest.java @@ -0,0 +1,99 @@ +package com.example.user.adapter.in.web.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 用户注册请求DTO (Data Transfer Object) + * 用于封装用户注册时提交的数据 + * + * DTO是数据传输对象,主要用于在不同层之间传输数据 + * 这里专门用于接收用户注册请求的数据 + * + * @Data Lombok注解,自动生成getter、setter、toString等方法 + * @Schema Swagger注解,用于API文档生成,描述这个类的作用 + */ +@Data +@Schema(description = "用户注册请求") +public class RegisterRequest { + + /** + * 用户名字段 + * + * @NotBlank 验证注解,确保用户名不为空且去除空格后不为空 + * @Size 验证注解,限制用户名长度在3-20个字符之间 + * @Pattern 验证注解,使用正则表达式限制用户名只能包含字母、数字和下划线 + * regexp 属性定义了正则表达式规则:^[a-zA-Z0-9_]+$ + * ^ 表示字符串开始 + * [a-zA-Z0-9_] 表示字符可以是字母、数字或下划线 + * + 表示前面的字符可以出现一次或多次 + * $ 表示字符串结束 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @NotBlank(message = "用户名不能为空") + @Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间") + @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线") + @Schema(description = "用户名", example = "testuser") + private String username; + + /** + * 密码字段 + * + * @NotBlank 验证注解,确保密码不为空且去除空格后不为空 + * @Size 验证注解,限制密码长度在6-20个字符之间 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间") + @Schema(description = "密码", example = "123456") + private String password; + + /** + * 确认密码字段 + * + * @NotBlank 验证注解,确保确认密码不为空且去除空格后不为空 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @NotBlank(message = "确认密码不能为空") + @Schema(description = "确认密码", example = "123456") + private String confirmPassword; + + /** + * 邮箱字段 + * + * @Email 验证注解,确保邮箱格式正确 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @Email(message = "邮箱格式不正确") + @Schema(description = "邮箱", example = "test@example.com") + private String email; + + /** + * 手机号字段 + * + * @Pattern 验证注解,使用正则表达式验证手机号格式 + * regexp 属性定义了手机号的正则表达式规则:^1[3-9]\d{9}$ + * ^ 表示字符串开始 + * 1 表示手机号必须以1开头 + * [3-9] 表示第二位数字必须是3-9之间的数字 + * \d{9} 表示后面必须跟9个数字 + * $ 表示字符串结束 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + @Schema(description = "手机号", example = "13800138000") + private String phone; + + /** + * 真实姓名字段 + * + * 这个字段没有强制验证注解,是可选填写的 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @Schema(description = "真实姓名", example = "张三") + private String realName; +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java new file mode 100644 index 0000000000000000000000000000000000000000..245d8f6c5461a9de6be5ca7d52fcb79a1a4e5b6a --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java @@ -0,0 +1,149 @@ +package com.example.user.adapter.in.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 通用响应结果类 + * 用于封装所有API接口的响应数据,统一返回格式 + * + * 这样做有以下好处: + * 1. 前端可以统一处理响应格式 + * 2. 便于统一错误处理 + * 3. 提高代码的可维护性 + * + * 使用泛型可以让这个类适用于任何类型的数据 + * + * @Data Lombok注解,自动生成getter、setter、toString等方法 + * @NoArgsConstructor Lombok注解,生成无参构造函数 + * @AllArgsConstructor Lombok注解,生成全参构造函数 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Result { + + /** + * 响应码 + * + * 用于表示请求处理的结果状态 + * 常见的响应码: + * 200 - 成功 + * 400 - 请求参数错误 + * 401 - 未授权 + * 403 - 禁止访问 + * 500 - 服务器内部错误 + */ + private Integer code; + + /** + * 响应消息 + * + * 用于描述请求处理的结果信息 + * 成功时可以是"操作成功" + * 失败时可以是具体的错误信息,如"用户名或密码错误" + */ + private String message; + + /** + * 响应数据 + * + * 用于携带具体的业务数据 + * 可以是任何类型,如用户信息、列表数据等 + * 使用泛型T使得这个字段可以适应不同类型的数据 + */ + private T data; + + /** + * 成功响应静态方法 + * + * 用于创建成功的响应结果 + * + * @param data 响应数据 + * @return Result 成功的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result success(T data) { + return new Result<>(200, "操作成功", data); + } + + /** + * 成功响应静态方法(无数据) + * + * 用于创建不携带数据的成功响应结果 + * + * @return Result 成功的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result success() { + return new Result<>(200, "操作成功", null); + } + + /** + * 成功响应静态方法(自定义消息) + * + * 用于创建携带自定义成功消息的响应结果 + * + * @param message 自定义的成功消息 + * @param data 响应数据 + * @return Result 成功的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result success(String message, T data) { + return new Result<>(200, message, data); + } + + /** + * 失败响应静态方法 + * + * 用于创建失败的响应结果,默认使用500状态码 + * + * @param message 错误消息 + * @return Result 失败的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result error(String message) { + return new Result<>(500, message, null); + } + + /** + * 失败响应静态方法(自定义状态码) + * + * 用于创建携带自定义状态码的失败响应结果 + * + * @param code 自定义状态码 + * @param message 错误消息 + * @return Result 失败的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result error(Integer code, String message) { + return new Result<>(code, message, null); + } + + /** + * 未授权响应静态方法 + * + * 用于创建401未授权的响应结果 + * + * @param message 错误消息 + * @return Result 未授权的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result unauthorized(String message) { + return new Result<>(401, message, null); + } + + /** + * 禁止访问响应静态方法 + * + * 用于创建403禁止访问的响应结果 + * + * @param message 错误消息 + * @return Result 禁止访问的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result forbidden(String message) { + return new Result<>(403, message, null); + } +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserInfo.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..85f781f9d56640f36ea4339b9200a3ca27bd458e --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserInfo.java @@ -0,0 +1,102 @@ +package com.example.user.adapter.in.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 用户信息DTO (Data Transfer Object) + * 用于封装用户的基本信息,在不同层之间传输 + * + * DTO是数据传输对象,主要用于在不同层之间传输数据 + * 这里专门用于传输用户的基本信息,不包含敏感信息如密码 + * + * @Data Lombok注解,自动生成getter、setter、toString等方法 + * @Builder Lombok注解,提供Builder模式构建对象,使代码更清晰易读 + * @NoArgsConstructor Lombok注解,生成无参构造函数 + * @AllArgsConstructor Lombok注解,生成全参构造函数 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserInfo { + + /** + * 用户ID + * + * 数据库中的主键,唯一标识一个用户 + * 使用Long类型可以支持更大的数据量 + */ + private Long id; + + /** + * 用户名 + * + * 用户登录时使用的名称,必须唯一 + * 通常用于登录认证 + */ + private String username; + + /** + * 邮箱 + * + * 用户的电子邮箱地址 + * 可以用于找回密码、接收通知等 + */ + private String email; + + /** + * 手机号 + * + * 用户的手机号码 + * 可以用于登录、找回密码、接收短信通知等 + */ + private String phone; + + /** + * 真实姓名 + * + * 用户的真实姓名 + * 用于实名认证、显示用户真实身份等 + */ + private String realName; + + /** + * 用户状态:0-禁用,1-启用 + * + * 用于控制用户账户是否可以正常使用 + * 0表示账户被禁用,无法登录 + * 1表示账户正常,可以登录使用 + */ + private Integer status; + + /** + * 角色:ADMIN-管理员,USER-普通用户 + * + * 用于区分用户的角色和权限 + * ADMIN表示管理员,拥有更高的权限 + * USER表示普通用户,拥有基本权限 + */ + private String role; + + /** + * 创建时间 + * + * 记录用户账户的创建时间 + * 使用LocalDateTime类型,是Java 8引入的新时间API + * 相比Date类型更加易用和直观 + */ + private LocalDateTime createTime; + + /** + * 最后登录时间 + * + * 记录用户最后一次登录的时间 + * 用于统计用户活跃度、安全监控等 + */ + private LocalDateTime lastLoginTime; +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/exception/GlobalExceptionHandler.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..14605dca959969c6af1d7442a584e194efda5282 --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/exception/GlobalExceptionHandler.java @@ -0,0 +1,259 @@ +package com.example.user.adapter.in.web.exception; + +import com.example.user.adapter.in.web.dto.Result; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.stream.Collectors; + +/** + * 全局异常处理器 + * 统一处理应用中的各种异常,避免异常直接暴露给用户 + * + * @Slf4j Lombok注解,自动生成日志对象log,用于记录异常日志 + * @RestControllerAdvice 组合注解,相当于@ControllerAdvice + @ResponseBody + * 用于定义全局异常处理器,可以捕获控制器层抛出的异常 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理认证异常 + * + * 当用户认证失败时(如Token无效、过期等)会抛出AuthenticationException + * + * @ExceptionHandler 注解指定该方法处理哪种异常 + * @ResponseStatus 注解指定返回的HTTP状态码 + * + * @param e 认证异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(AuthenticationException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public Result handleAuthenticationException(AuthenticationException e) { + log.error("认证异常: {}", e.getMessage()); + return Result.unauthorized("认证失败: " + e.getMessage()); + } + + /** + * 处理凭据错误异常 + * + * 当用户名或密码错误时会抛出BadCredentialsException + * 这是AuthenticationException的一个子类 + * + * @param e 凭据错误异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(BadCredentialsException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public Result handleBadCredentialsException(BadCredentialsException e) { + log.error("凭据错误: {}", e.getMessage()); + return Result.unauthorized("用户名或密码错误"); + } + + /** + * 处理访问拒绝异常 + * + * 当已认证用户尝试访问没有权限的资源时会抛出AccessDeniedException + * + * @param e 访问拒绝异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleAccessDeniedException(AccessDeniedException e) { + log.error("访问拒绝: {}", e.getMessage()); + return Result.forbidden("访问被拒绝,权限不足"); + } + + /** + * 处理参数校验异常 - @Valid注解 + * + * 当使用@Valid注解验证请求参数失败时会抛出MethodArgumentNotValidException + * 例如LoginRequest中的用户名或密码不符合验证规则 + * + * @param e 参数校验异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + // 收集所有字段的错误信息并拼接成字符串 + String errorMessage = e.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.error("参数校验失败: {}", errorMessage); + return Result.error("参数校验失败: " + errorMessage); + } + + /** + * 处理参数绑定异常 + * + * 当请求参数绑定到对象时发生错误会抛出BindException + * + * @param e 参数绑定异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBindException(BindException e) { + // 收集所有字段的错误信息并拼接成字符串 + String errorMessage = e.getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.error("参数绑定失败: {}", errorMessage); + return Result.error("参数绑定失败: " + errorMessage); + } + + /** + * 处理约束违反异常 + * + * 当使用Bean Validation API验证失败时会抛出ConstraintViolationException + * + * @param e 约束违反异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleConstraintViolationException(ConstraintViolationException e) { + // 收集所有约束违反的错误信息并拼接成字符串 + String errorMessage = e.getConstraintViolations().stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.joining(", ")); + log.error("约束违反: {}", errorMessage); + return Result.error("参数校验失败: " + errorMessage); + } + + /** + * 处理非法参数异常 + * + * 当传递给方法的参数不合法时会抛出IllegalArgumentException + * + * @param e 非法参数异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数: {}", e.getMessage()); + return Result.error("参数错误: " + e.getMessage()); + } + + /** + * 处理空指针异常 + * + * 当尝试访问空对象的属性或方法时会抛出NullPointerException + * + * @param e 空指针异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(NullPointerException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleNullPointerException(NullPointerException e) { + log.error("空指针异常", e); + return Result.error("系统内部错误,请联系管理员"); + } + + /** + * 处理运行时异常 + * + * 当发生未预期的运行时错误时会抛出RuntimeException + * 这是很多异常的父类 + * + * @param e 运行时异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(RuntimeException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleRuntimeException(RuntimeException e) { + log.error("运行时异常: {}", e.getMessage(), e); + return Result.error("系统异常: " + e.getMessage()); + } + + /** + * 处理其他所有异常 + * + * 作为兜底的异常处理方法,处理所有未被上面方法处理的异常 + * + * @param e 异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleException(Exception e) { + log.error("未知异常: {}", e.getMessage(), e); + return Result.error("系统内部错误,请联系管理员"); + } + + /** + * 自定义业务异常类 + * + * 用于处理应用程序中特定的业务异常 + * 继承自RuntimeException,是一个受检异常 + */ + public static class BusinessException extends RuntimeException { + /** + * 异常码 + * + * 用于标识异常的类型,便于前端进行不同的处理 + */ + private final int code; + + /** + * 构造函数 - 只有消息 + * + * @param message 异常消息 + */ + public BusinessException(String message) { + super(message); + this.code = 500; + } + + /** + * 构造函数 - 有码和消息 + * + * @param code 异常码 + * @param message 异常消息 + */ + public BusinessException(int code, String message) { + super(message); + this.code = code; + } + + /** + * 获取异常码 + * + * @return int 异常码 + */ + public int getCode() { + return code; + } + } + + /** + * 处理自定义业务异常 + * + * 处理应用程序中抛出的BusinessException + * + * @param e 业务异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBusinessException(BusinessException e) { + log.error("业务异常: {}", e.getMessage()); + return Result.error(e.getCode(), e.getMessage()); + } +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/pom.xml b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/pom.xml index 812d59d239543ac421905880a779976e3b0c980f..10d3de7e9d6ff43d987cc2d15d61ea45e0c14d49 100644 --- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/pom.xml +++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/pom.xml @@ -36,6 +36,12 @@ mysql-connector-j runtime + + com.example + user-service-application + 0.0.1-SNAPSHOT + compile + diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/bridge/GetUserByNameBridge.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/bridge/GetUserByNameBridge.java index 38dc3ea5a32a0f658d80085de97fb59d19db3827..79e2a0764e53e7f31e5acdda464c7de366551eb2 100644 --- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/bridge/GetUserByNameBridge.java +++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/bridge/GetUserByNameBridge.java @@ -18,7 +18,7 @@ public class GetUserByNameBridge implements GetUserByNamePort { @Override public User getUserByName(String name) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(UserEntity::getName, name); + wrapper.eq(UserEntity::getUsername, name); UserEntity userEntity = userMapper.selectOne(wrapper); //password不空 log.info("userEntity: {}", userEntity); diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/convertor/UserConvertor.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/convertor/UserConvertor.java index cc56ab567d7dd43b0fd158c989ea8b8069c882fb..3c6e442ccb7c4039fb57db90d8840beaffe2b653 100644 --- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/convertor/UserConvertor.java +++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/convertor/UserConvertor.java @@ -14,11 +14,9 @@ public class UserConvertor { public static User toDomain(UserEntity userEntity) { return new User( new UserId(userEntity.getId()), - new UserName(userEntity.getName()), - new UserAge(userEntity.getAge()), + new UserName(userEntity.getUsername()), new Email(userEntity.getEmail()), - Password.fromEncrypted(userEntity.getPassword()), - new IsSuper(userEntity.getIsSuper() == 1) + Password.fromEncrypted(userEntity.getPassword()) ); } @@ -32,10 +30,7 @@ public class UserConvertor { return new UserEntity( user.getId().id(), user.getName().username(), - user.getAge().age(), - user.getEmail().email(), - user.getPassword().encryptedValue(), - user.getIsSuper().value() ? 1 : 0 + user.getEmail().email() ); } } diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/entity/UserEntity.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/entity/UserEntity.java index 1455f81d5b6a2f987fc9344007f8013df1f4c4fa..e3c08a9a11610087cb75aaefdaf4a1055c766f86 100644 --- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/entity/UserEntity.java +++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/entity/UserEntity.java @@ -7,21 +7,123 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + @Data @AllArgsConstructor @NoArgsConstructor -@TableName("user") +@TableName("sys_user") public class UserEntity { - @TableId(type= IdType.ASSIGN_ID) - private long id; - private String name; - private Integer age; - private String email; + /** + * 用户ID + * + * 数据库表的主键字段 + * + * @TableId MyBatis Plus注解,标识这是主键字段 + * type = IdType.AUTO 表示使用数据库自增主键 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户名 + * + * 用户登录时使用的名称,必须唯一 + * 通常用于登录认证 + */ + private String username; + + /** + * 密码 + * + * 用户的登录密码,存储的是经过加密的哈希值 + * 出于安全考虑,数据库中不会存储明文密码 + * 通常使用BCrypt等算法进行加密 + */ private String password; - private Integer isSuper; + + /** + * 邮箱 + * + * 用户的电子邮箱地址 + * 可以用于找回密码、接收通知等 + */ + private String email; + + /** + * 手机号 + * + * 用户的手机号码 + * 可以用于登录、找回密码、接收短信通知等 + */ + private String phone; + + /** + * 真实姓名 + * + * 用户的真实姓名 + * 用于实名认证、显示用户真实身份等 + */ + private String realName; + + /** + * 用户状态:0-禁用,1-启用 + * + * 用于控制用户账户是否可以正常使用 + * 0表示账户被禁用,无法登录 + * 1表示账户正常,可以登录使用 + */ + private Integer status; + + /** + * 角色:ADMIN-管理员,USER-普通用户 + * + * 用于区分用户的角色和权限 + * ADMIN表示管理员,拥有更高的权限 + * USER表示普通用户,拥有基本权限 + */ + private String role; + + /** + * 创建时间 + * + * 记录用户账户的创建时间 + * 使用LocalDateTime类型,是Java 8引入的新时间API + * 相比Date类型更加易用和直观 + */ + private LocalDateTime createTime; + + /** + * 更新时间 + * + * 记录用户信息最后一次更新的时间 + * 每次修改用户信息时都应该更新这个字段 + */ + private LocalDateTime updateTime; + + /** + * 最后登录时间 + * + * 记录用户最后一次登录的时间 + * 用于统计用户活跃度、安全监控等 + */ + private LocalDateTime lastLoginTime; public UserEntity(long value, String value1, int value2, String value3) { } - public UserEntity(long id, String name, Integer age, String email, String password) { - this(id, name, age, email, password, 0); // 默认isSuper为0 + public UserEntity(Long id, String username, String password, String email, String phone, String realName, Integer status, String role) { + this.id = id; + this.username=username; + this.password=password; + this.email=email; + this.phone=phone; + this.realName=realName; + this.status=1; + this.role="USER"; + } + + public UserEntity(long id, String username, String email) { + this.id = id; + this.username = username; + this.email = email; } -} +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/mapper/UserMapper.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/mapper/UserMapper.java index 92125d3cc1d3386d1f0ddd3c2fae1953dd77a5f4..484fc603c73d11efcfa3fbf576c5a1988633995d 100644 --- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/mapper/UserMapper.java +++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/mapper/UserMapper.java @@ -1,7 +1,53 @@ package com.example.user.adapter.out.persistence.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; + import com.example.user.adapter.out.persistence.entity.UserEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +/** + * 用户数据访问层接口 + * + * Mapper是数据访问层的接口,负责与数据库进行交互 + * 这里使用MyBatis Plus框架,继承BaseMapper可以获得常用的CRUD操作 + * + * @Mapper MyBatis注解,标识这是一个Mapper接口 + * MyBatis会为这个接口生成实现类,用于执行SQL语句 + */ +@Mapper public interface UserMapper extends BaseMapper { -} + + /** + * 根据用户名查询用户 + * + * 这是一个自定义的查询方法,用于根据用户名查找用户信息 + * + * @Select MyBatis注解,直接在方法上编写SQL语句 + * SQL语句:SELECT * FROM sys_user WHERE username = #{username} + * #{username} 是参数占位符,会被方法参数替换 + * + * @param username 用户名 + * @return User 用户信息,如果不存在则返回null + */ + @Select("SELECT * FROM sys_user WHERE username = #{username}") + UserEntity selectByUsername(@Param("username") String username); + + /** + * 根据用户名更新密码 + * + * 这是一个自定义的更新方法,用于更新指定用户的密码 + * + * @Update MyBatis注解,直接在方法上编写SQL更新语句 + * SQL语句:UPDATE sys_user SET password = #{password} WHERE username = #{username} + * #{password} 和 #{username} 是参数占位符 + * + * @param username 用户名 + * @param password 新密码(应该是加密后的) + * @return int 更新记录数,成功更新返回1,未找到记录返回0 + */ + @Update("UPDATE sys_user SET password = #{password} WHERE username = #{username}") + int updatePasswordByUsername(@Param("username") String username, @Param("password") String password); +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/service/CustomUserDetailsService.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/service/CustomUserDetailsService.java new file mode 100644 index 0000000000000000000000000000000000000000..2b8fe4069006c8a15b4d3eb1fcf94484748f8dae --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/service/CustomUserDetailsService.java @@ -0,0 +1,367 @@ +package com.example.user.adapter.out.persistence.service; + + + +import com.example.user.adapter.out.persistence.entity.UserEntity; +import com.example.user.adapter.out.persistence.mapper.UserMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +/** + * 自定义用户详情服务 - Spring Security认证的核心组件 + * 实现Spring Security的UserDetailsService接口 + * + * 【什么是UserDetailsService?】 + * UserDetailsService是Spring Security提供的核心接口,专门用于加载用户认证信息 + * 可以理解为"用户信息提供者",当有人要登录时,Spring Security会问它:"这个用户存在吗?密码对吗?有什么权限?" + * + * 【认证流程中的作用】 + * 1. 用户提交用户名和密码 + * 2. Spring Security调用loadUserByUsername方法 + * 3. 该方法从数据库查询用户信息 + * 4. 返回UserDetails对象(包含用户名、密码、权限等) + * 5. Spring Security自动验证密码是否匹配 + * 6. 验证成功则生成认证令牌 + * + * 【为什么要自定义实现?】 + * - Spring Security默认不知道我们的用户数据存在哪里(数据库、文件、内存等) + * - 通过实现UserDetailsService接口,告诉Spring Security如何获取用户信息 + * - 可以自定义用户权限、状态检查等业务逻辑 + * + * 【核心方法说明】 + * - loadUserByUsername:Spring Security认证时自动调用 + * - authenticate:自定义的登录验证方法 + * - saveUser:用户注册时保存用户信息 + * + * 【注解说明】 + * @Slf4j:Lombok注解,自动生成日志对象log,用于记录操作日志 + * @Service:Spring注解,将该类注册为Spring容器管理的服务Bean + * @RequiredArgsConstructor:Lombok注解,为所有final字段生成构造函数,实现依赖注入 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + /** + * 密码编码器 - 密码安全的守护者 + * + * 【作用说明】 + * 用于密码的加密和验证,确保用户密码安全存储 + * 不直接存储明文密码,而是存储加密后的哈希值 + * + * 【工作原理】 + * - 注册时:将明文密码加密成哈希值存储到数据库 + * - 登录时:将用户输入的明文密码加密,与数据库中的哈希值比较 + * - 即使数据库泄露,攻击者也无法直接获得用户的真实密码 + * + * 【依赖注入】 + * 通过@RequiredArgsConstructor注解自动注入 + * Spring容器会自动提供BCryptPasswordEncoder实例 + */ + private final PasswordEncoder passwordEncoder; + + /** + * 用户数据访问层 - 数据库操作的桥梁 + * + * 【作用说明】 + * MyBatis的Mapper接口,专门用于用户表的数据库操作 + * 提供了用户的增删改查功能 + * + * 【设计模式】 + * 采用DAO(Data Access Object)模式 + * 将数据访问逻辑与业务逻辑分离,提高代码的可维护性 + * + * 【主要功能】 + * - selectByUsername:根据用户名查询用户信息 + * - insert:插入新用户记录 + * - updatePasswordByUsername:更新用户密码 + * + * 【依赖注入】 + * 通过@RequiredArgsConstructor注解自动注入 + * MyBatis会自动生成该接口的实现类 + */ + private final UserMapper userMapper; + + /** + * 根据用户名加载用户详情 - Spring Security认证的核心入口 + * + * 【方法重要性】 + * 这是UserDetailsService接口的核心方法,Spring Security认证流程的关键环节 + * 当用户尝试登录时,Spring Security会自动调用这个方法获取用户信息 + * + * 【执行时机】 + * 1. 用户提交登录表单(用户名+密码) + * 2. Spring Security的认证管理器开始工作 + * 3. 认证管理器调用此方法,传入用户名 + * 4. 方法返回UserDetails对象 + * 5. Spring Security自动比较密码是否匹配 + * + * 【返回值说明】 + * UserDetails是Spring Security定义的用户信息接口,包含: + * - 用户名(username) + * - 密码(password,已加密) + * - 权限列表(authorities) + * - 账户状态(是否过期、锁定、禁用等) + * + * 【异常处理策略】 + * - 用户不存在:抛出UsernameNotFoundException + * - 用户被禁用:也抛出UsernameNotFoundException(安全考虑,不暴露具体原因) + * - 这样可以防止攻击者通过错误信息判断用户是否存在 + * + * @param username 用户名(来自登录表单) + * @return UserDetails Spring Security的用户详情接口实现 + * @throws UsernameNotFoundException 当用户不存在或被禁用时抛出此异常 + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 【步骤1:记录调试日志】 + // 使用debug级别记录方法调用,便于开发时追踪认证流程 + log.debug("Loading user by username: {}", username); + + // 【步骤2:从数据库查询用户信息】 + // 调用MyBatis Mapper查询用户,这里会执行SQL: SELECT * FROM users WHERE username = ? + UserEntity user = userMapper.selectByUsername(username); + if (user == null) { + // 【安全策略】用户不存在时的处理 + // 记录警告日志,但不在异常信息中暴露过多细节,防止用户名枚举攻击 + log.warn("User not found: {}", username); + throw new UsernameNotFoundException("用户不存在: " + username); + } + + // 【步骤3:检查用户状态】 + // 业务规则:status=0表示用户被禁用,status=1表示正常 + if (user.getStatus() == 0) { + // 【安全策略】禁用用户的处理 + // 同样抛出UsernameNotFoundException,不暴露用户存在但被禁用的信息 + log.warn("User is disabled: {}", username); + throw new UsernameNotFoundException("用户已被禁用: " + username); + } + + // 【步骤4:构建Spring Security的UserDetails对象】 + // 【重要概念区分】 + // - org.springframework.security.core.userdetails.User:Spring Security的用户类,用于认证和授权 + // - com.example.ss.demo.entity.User:我们自定义的用户实体类,用于数据库存储 + return org.springframework.security.core.userdetails.User.builder() + // 【字段映射】将数据库用户信息映射到Spring Security用户对象 + .username(user.getUsername()) // 设置用户名 + .password(user.getPassword()) // 设置密码(已是BCrypt加密的哈希值) + .authorities(Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole()))) // 设置用户角色权限(如ROLE_ADMIN、ROLE_USER) + .accountExpired(false) // 账户是否过期(这里设置为永不过期) + .accountLocked(false) // 账户是否被锁定(这里设置为永不锁定) + .credentialsExpired(false) // 凭据是否过期(这里设置为永不过期) + .disabled(user.getStatus() == 0) // 账户是否被禁用(根据数据库status字段决定) + .build(); + } + + /** + * 根据用户名获取用户信息 + * + * 这是一个自定义方法,用于业务逻辑中获取用户信息 + * 与loadUserByUsername不同,这个方法不会抛出异常 + * + * @param username 用户名 + * @return User 用户信息,如果不存在则返回null + */ + public UserEntity getUserByUsername(String username) { + return userMapper.selectByUsername(username); + } + + /** + * 验证用户登录 - 自定义的完整登录验证方法 + * + * 【方法作用】 + * 这是一个自定义的登录验证方法,完成用户名和密码的完整验证流程 + * 与loadUserByUsername不同,这个方法会直接验证密码是否正确 + * + * 【与loadUserByUsername的区别】 + * - loadUserByUsername:只负责根据用户名获取用户信息,Spring Security自动验证密码 + * - authenticate:完整的登录验证,包括用户名查询和密码验证 + * + * 【使用场景】 + * - 自定义登录接口(如REST API登录) + * - 需要在业务逻辑中手动验证用户身份 + * - 登录前的预检查(如检查登录次数限制等) + * + * 【验证流程】 + * 1. 根据用户名查询用户信息 + * 2. 检查用户是否存在 + * 3. 使用PasswordEncoder验证密码 + * 4. 返回验证结果 + * + * 【安全考虑】 + * - 密码验证使用BCrypt算法,安全性高 + * - 验证失败时记录日志,便于安全审计 + * - 不在返回值中暴露失败原因,防止信息泄露 + * + * @param username 用户名 + * @param password 密码(明文,方法内部会进行加密比较) + * @return User 验证成功返回用户信息,验证失败返回null + */ + public UserEntity authenticate(String username, String password) { + // 【步骤1:查询用户信息】 + // 从数据库中根据用户名查询用户记录 + UserEntity user = userMapper.selectByUsername(username); + if (user == null) { + // 【安全日志】记录登录失败的尝试,用于安全审计 + // 不返回具体错误信息给调用者,防止用户名枚举攻击 + log.warn("用户不存在: {}", username); + return null; + } + + // 【调试信息】输出当前密码的加密结果(仅用于开发调试) + // 注意:生产环境中应该删除此行,避免日志中出现敏感信息 + log.info("password:{}",passwordEncoder.encode( password)); + + // 【步骤2:密码验证 - 核心安全检查】 + // 【重要原理】BCrypt密码验证过程: + // 1. passwordEncoder.matches(明文密码, 数据库中的哈希值) + // 2. BCrypt会提取数据库哈希值中的盐值(salt) + // 3. 使用相同的盐值对明文密码进行哈希 + // 4. 比较两个哈希值是否相同 + // 5. 这样即使相同的密码,每次存储的哈希值也不同(因为盐值随机) + if (!passwordEncoder.matches(password, user.getPassword())) { + // 【安全日志】记录密码错误的登录尝试 + // 这类日志对于检测暴力破解攻击很重要 + log.warn("用户登录失败: 密码错误 - {}", username); + return null; + } + + // 【步骤3:登录成功处理】 + // 记录成功登录的日志,用于用户行为分析和安全审计 + log.info("用户登录成功: {}", username); + return user; + } + + /** + * 测试密码匹配(调试用) + * + * 用于调试时验证密码哈希是否正确 + * + * @param rawPassword 原始密码(明文) + * @param encodedPassword 编码后的密码(哈希值) + * @return boolean 是否匹配 + */ + public boolean testPasswordMatch(String rawPassword, String encodedPassword) { + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 修正admin用户的密码哈希 + * + * 用于修复数据库中admin用户的密码哈希值 + * + * @return boolean 修正是否成功 + */ + public boolean fixAdminPassword() { + try { + // 生成新的正确密码哈希 + String correctPasswordHash = generatePasswordHash("password"); + + // 更新admin用户的密码哈希 + int result = userMapper.updatePasswordByUsername("admin", correctPasswordHash); + + // 同时更新user用户的密码哈希 + userMapper.updatePasswordByUsername("user", correctPasswordHash); + + return result > 0; + } catch (Exception e) { + log.error("修正密码哈希失败: {}", e.getMessage()); + return false; + } + } + + /** + * 生成密码哈希 + * + * 使用密码编码器对原始密码进行加密 + * + * @param password 原始密码(明文) + * @return String 生成的密码哈希 + */ + public String generatePasswordHash(String password) { + return passwordEncoder.encode(password); + } + + /** + * 保存用户信息 - 用户注册的核心数据持久化方法 + * + * 【方法作用】 + * 将新用户信息保存到数据库中,这是用户注册流程的最后一步 + * 完成用户数据的持久化存储 + * + * 【调用时机】 + * 1. 用户提交注册表单 + * 2. 控制器验证用户名不重复 + * 3. 控制器加密用户密码 + * 4. 调用此方法保存用户信息 + * + * 【数据库操作】 + * 使用MyBatis Plus的insert方法进行数据插入 + * - 自动生成主键ID + * - 自动设置创建时间(如果配置了自动填充) + * - 返回影响的行数 + * + * 【事务处理】 + * 建议在调用此方法的上层添加@Transactional注解 + * 确保注册过程的数据一致性 + * + * 【异常处理策略】 + * - 捕获所有异常,避免敏感信息泄露 + * - 记录详细的错误日志,便于问题排查 + * - 返回统一的结果码,便于上层处理 + * + * 【返回值说明】 + * - 1:保存成功 + * - 0:保存失败(可能是数据库约束冲突、网络问题等) + * + * @param user 要保存的用户对象(包含用户名、加密密码、角色等信息) + * @return int 保存结果,成功返回1,失败返回0 + */ + public int saveUser(UserEntity user) { + try { + // 【数据库插入操作】 + // 使用MyBatis Plus的insert方法保存用户到数据库 + // 【自动功能】 + // 1. 自动生成主键ID(通常使用数据库的AUTO_INCREMENT) + // 2. 自动设置创建时间和更新时间(如果配置了自动填充) + // 3. 返回影响的行数(成功插入返回1) + int result = userMapper.insert(user); + + // 【结果检查和日志记录】 + if (result > 0) { + // 【成功日志】记录用户注册成功的信息 + // 包含用户名和自动生成的用户ID,便于后续追踪 + log.info("用户保存成功: username={}, userId={}", user.getUsername(), user.getId()); + } else { + // 【失败日志】记录保存失败的情况 + // 这种情况比较少见,可能是数据库连接问题或其他系统问题 + log.warn("用户保存失败: username={}", user.getUsername()); + } + + return result; + } catch (Exception e) { + // 【异常处理】捕获所有可能的异常 + // 【常见异常类型】 + // 1. DuplicateKeyException:用户名重复(违反唯一约束) + // 2. DataAccessException:数据库连接或SQL执行异常 + // 3. ConstraintViolationException:数据约束违反 + // + // 【安全考虑】 + // - 记录详细的异常信息到日志中,便于开发人员排查问题 + // - 但不向调用者暴露具体的异常信息,只返回统一的失败标识 + // - 这样既便于调试,又不会泄露系统内部信息 + log.error("保存用户异常: username={}, error={}", user.getUsername(), e.getMessage(), e); + return 0; + } + } + +} \ No newline at end of file diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/TokenBlacklistService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/TokenBlacklistService.java new file mode 100644 index 0000000000000000000000000000000000000000..6944907992ff4bf1472444035406e3a7453c9e50 --- /dev/null +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/TokenBlacklistService.java @@ -0,0 +1,204 @@ +package com.example.user.service.application.service; + + +import com.example.user.service.common.JwtUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Token黑名单服务类 - JWT令牌失效管理 + * + * 这个服务类解决了JWT无状态认证中的一个重要问题:如何在用户登出后立即使令牌失效 + * + * JWT的无状态特性说明: + * - JWT令牌是自包含的,包含了所有必要的用户信息 + * - 服务端不需要存储会话状态,每次请求都通过验证令牌签名来确认身份 + * - 这种设计的优点是可扩展性好,但缺点是无法在服务端主动"销毁"令牌 + * - 令牌在过期时间之前始终有效,即使用户已经登出 + * + * 黑名单机制原理: + * 1. 维护一个已失效令牌的黑名单列表 + * 2. 用户登出时,将令牌添加到黑名单 + * 3. 每次验证令牌时,先检查是否在黑名单中 + * 4. 如果在黑名单中,则拒绝访问,即使令牌本身是有效的 + * + * 存储方案选择: + * - 内存存储(ConcurrentHashMap):适合单机部署,性能最好 + * - Redis存储:适合分布式部署,多个服务实例共享黑名单 + * - 数据库存储:适合对数据持久性要求高的场景 + * + * 本实现使用内存存储,具有以下特点: + * - 高性能:内存访问速度快,不涉及网络IO + * - 线程安全:使用ConcurrentHashMap保证并发安全 + * - 自动清理:定时清理过期的黑名单记录,避免内存泄漏 + * - 简单可靠:无外部依赖,部署简单 + * + * 注意事项: + * - 重启服务会丢失黑名单数据,已登出的用户令牌可能重新生效 + * - 多实例部署时,各实例的黑名单不共享 + * - 如需解决以上问题,建议改用Redis等外部存储 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TokenBlacklistService { + + /** + * JWT工具类,用于解析令牌获取过期时间 + */ + private final JwtUtil jwtUtil; + + /** + * 黑名单存储容器 + * + * 使用ConcurrentHashMap确保线程安全的并发访问 + * Key: JWT令牌字符串 + * Value: 令牌的过期时间,用于定时清理 + * + * 为什么存储过期时间: + * - 避免永久存储已过期的令牌,节省内存空间 + * - 支持定时清理机制,自动移除不再需要的黑名单记录 + * - 提供调试信息,便于排查问题 + */ + private final ConcurrentHashMap blacklistedTokens = new ConcurrentHashMap<>(); + + /** + * 将令牌添加到黑名单 + * + * 当用户登出时调用此方法,将JWT令牌加入黑名单 + * 加入黑名单后,该令牌将无法再用于身份验证 + * + * 实现细节: + * 1. 解析令牌获取过期时间 + * 2. 将令牌和过期时间存入黑名单Map + * 3. 记录操作日志,便于审计和调试 + * + * 异常处理: + * - 如果令牌格式无效,会记录警告日志但不抛出异常 + * - 确保即使个别令牌处理失败,也不影响整体功能 + * + * @param token JWT令牌字符串 + */ + public void addToBlacklist(String token) { + try { + // 解析令牌获取过期时间 + // 这样可以在令牌自然过期后自动清理黑名单记录 + Date expirationTime = jwtUtil.getExpirationDateFromToken(token); + + // 将令牌添加到黑名单 + blacklistedTokens.put(token, expirationTime); + + // 记录操作日志(只记录令牌的前几位,避免泄露完整令牌) + log.info("令牌已添加到黑名单,过期时间: {}, 令牌前缀: {}...", + expirationTime, + token.substring(0, Math.min(token.length(), 10))); + + } catch (Exception e) { + // 如果解析令牌失败,记录警告日志 + log.warn("添加令牌到黑名单时发生错误: {}, 令牌前缀: {}...", + e.getMessage(), + token.substring(0, Math.min(token.length(), 10))); + } + } + + /** + * 检查令牌是否在黑名单中 + * + * 在JWT认证过滤器中调用此方法,检查令牌是否已被列入黑名单 + * 如果令牌在黑名单中,则应拒绝该请求的访问 + * + * 性能考虑: + * - ConcurrentHashMap的containsKey操作时间复杂度为O(1) + * - 即使黑名单中有大量令牌,查询性能也很好 + * - 无需额外的网络请求或磁盘IO + * + * @param token JWT令牌字符串 + * @return boolean true表示令牌在黑名单中(应拒绝访问),false表示不在黑名单中 + */ + public boolean isBlacklisted(String token) { + boolean isBlacklisted = blacklistedTokens.containsKey(token); + + if (isBlacklisted) { + log.debug("检测到黑名单令牌访问尝试,令牌前缀: {}...", + token.substring(0, Math.min(token.length(), 10))); + } + + return isBlacklisted; + } + + /** + * 定时清理过期的黑名单令牌 + * + * 使用Spring的@Scheduled注解实现定时任务 + * 每小时执行一次清理操作,移除已经自然过期的令牌 + * + * 清理的必要性: + * - 避免内存泄漏:长期运行的服务会积累大量过期令牌 + * - 提高性能:减少黑名单大小,提高查询效率 + * - 节省资源:释放不再需要的内存空间 + * + * 清理策略: + * - 只清理已经过期的令牌(当前时间 > 令牌过期时间) + * - 使用迭代器安全地删除元素,避免并发修改异常 + * - 记录清理统计信息,便于监控和调试 + * + * 定时配置说明: + * - fixedRate = 3600000:每3600000毫秒(1小时)执行一次 + * - 可以根据实际需求调整清理频率 + * - 频率过高会增加CPU开销,频率过低会占用更多内存 + */ + @Scheduled(fixedRate = 3600000) // 每小时执行一次 + public void cleanupExpiredTokens() { + Date now = new Date(); + int initialSize = blacklistedTokens.size(); + + // 使用removeIf方法安全地移除过期令牌 + // 这个方法是线程安全的,不会与其他操作产生冲突 + blacklistedTokens.entrySet().removeIf(entry -> { + Date expirationTime = entry.getValue(); + return expirationTime != null && now.after(expirationTime); + }); + + int finalSize = blacklistedTokens.size(); + int cleanedCount = initialSize - finalSize; + + if (cleanedCount > 0) { + log.info("黑名单清理完成,清理了 {} 个过期令牌,当前黑名单大小: {}", cleanedCount, finalSize); + } else { + log.debug("黑名单清理完成,无过期令牌需要清理,当前黑名单大小: {}", finalSize); + } + } + + /** + * 获取当前黑名单大小 + * + * 提供监控和调试功能,可以了解当前黑名单的使用情况 + * + * @return int 黑名单中令牌的数量 + */ + public int getBlacklistSize() { + return blacklistedTokens.size(); + } + + /** + * 清空所有黑名单令牌 + * + * 提供管理功能,在特殊情况下可以清空整个黑名单 + * 注意:此操作会使所有已登出用户的令牌重新生效 + * + * 使用场景: + * - 系统维护时需要重置黑名单状态 + * - 测试环境中需要快速清理数据 + * - 紧急情况下需要恢复所有用户访问 + */ + public void clearBlacklist() { + int size = blacklistedTokens.size(); + blacklistedTokens.clear(); + log.warn("黑名单已被清空,共清理了 {} 个令牌", size); + } +} \ No newline at end of file diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java index b10d43e0179a76381f25cb3e62b9319077c76549..03883a8676d79e976d1933333af43f150e3f98d4 100644 --- a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java @@ -29,10 +29,9 @@ public class UserLoginService implements UserLoginUseCase { throw new RuntimeException("密码错误"); } // 签发token - String token = JwtUtil.generateToken( - user.getId().id(), - user.getName().username(), - user.getIsSuper().value() + JwtUtil jwtUtil = new JwtUtil(); + String token = jwtUtil.generateToken( + user.getName().username() ); log.info("生成的JWT令牌: {}", token); return token; diff --git a/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/config/BasicSecurityConfig.java b/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/config/BasicSecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..8d99aea45e9afa28078405047e319bef90e63dbe --- /dev/null +++ b/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/config/BasicSecurityConfig.java @@ -0,0 +1,181 @@ +package com.example.user.service.bootstrap.security.config; + + + +import com.example.user.adapter.out.persistence.service.CustomUserDetailsService; +import com.example.user.service.bootstrap.security.filter.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; + +/** + * Spring Security 配置类 + * 支持JWT认证和跨域配置 + * + * 这个类负责配置整个应用程序的安全策略,包括: + * 1. 用户认证和授权规则 + * 2. JWT Token验证 + * 3. 跨域资源共享(CORS)配置 + * 4. 会话管理策略 + */ +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) +@RequiredArgsConstructor +public class BasicSecurityConfig { + + /** + * JWT认证过滤器 + * 用于拦截请求并验证JWT Token的有效性 + */ + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + /** + * 自定义用户详情服务 + * 用于加载用户信息进行认证 + */ + private final CustomUserDetailsService userDetailsService; + + + + /** + * 认证管理器 + * 负责处理用户认证请求 + * + * @param config 认证配置对象,由Spring自动注入 + * @return AuthenticationManager 认证管理器实例 + * @throws Exception 如果配置过程中出现异常 + */ + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + /** + * CORS配置源 + * 配置跨域资源共享策略,允许前端应用访问后端API + * + * @return CorsConfigurationSource CORS配置源 + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + // 允许的源(Origin),这里设置为允许所有域名访问 + // 在生产环境中应该设置为具体的域名以提高安全性 + configuration.setAllowedOriginPatterns(List.of("*")); + + // 允许的HTTP方法,包括常用的GET、POST、PUT、DELETE等 + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); + + // 允许的请求头,这里设置为允许所有请求头 + configuration.setAllowedHeaders(List.of("*")); + + // 允许携带凭证(如Cookie),设置为true表示允许跨域请求携带身份信息 + configuration.setAllowCredentials(true); + + // 预检请求的缓存时间,单位秒,这里设置为1小时 + configuration.setMaxAge(3600L); + + // 暴露给客户端的响应头,允许客户端访问这些响应头 + configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type")); + + // 创建基于URL的CORS配置源 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + // 对所有路径应用相同的CORS配置 + source.registerCorsConfiguration("/**", configuration); + return source; + } + + /** + * 安全过滤器链配置 + * 定义HTTP请求的安全处理规则 + * + * @param http HttpSecurity对象,用于配置Web安全 + * @return SecurityFilterChain 安全过滤器链 + * @throws Exception 如果配置过程中出现异常 + */ + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // 禁用CSRF(跨站请求伪造)保护 + // 前后端分离项目通常禁用,因为使用Token认证而不是Session + .csrf(AbstractHttpConfigurer::disable) + + // 启用CORS(跨域资源共享)并使用上面定义的配置 + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + + // 配置会话管理为无状态 + // 因为使用JWT Token认证,不需要服务器保存会话状态 + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + + // 配置请求授权规则 + .authorizeHttpRequests(authz -> authz + // 公开接口,无需认证即可访问 + .requestMatchers("/api/auth/login", "/api/auth/register").permitAll() + .requestMatchers("/api/auth/debug/**").permitAll() + // Swagger和knife4j文档接口放行,方便查看API文档 + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll() + .requestMatchers("/doc.html", "/webjars/**", "/favicon.ico").permitAll() + .requestMatchers("/actuator/**").permitAll() + .requestMatchers("/error").permitAll() + // 其他所有请求都需要认证 + .anyRequest().authenticated() + ) + + // 禁用默认的登录页面 + // 因为使用自定义的登录接口,不需要Spring Security提供的默认登录页面 + .formLogin(AbstractHttpConfigurer::disable) + + // 禁用默认的登出页面 + // 因为使用自定义的登出接口,不需要Spring Security提供的默认登出功能 + .logout(AbstractHttpConfigurer::disable) + + // 禁用HTTP Basic认证 + // 因为使用JWT Token认证,不需要HTTP Basic认证方式 + .httpBasic(AbstractHttpConfigurer::disable) + + // 添加JWT认证过滤器 + // 在用户名密码认证过滤器之前添加JWT认证过滤器 + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + + // 配置用户详情服务 + // 用于加载用户信息进行认证 + .userDetailsService(userDetailsService) + + // 配置异常处理 + .exceptionHandling(exceptions -> exceptions + // 认证入口点,当未认证用户访问需要认证的资源时调用 + .authenticationEntryPoint((request, response, authException) -> { + response.setStatus(401); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"message\":\"未授权访问,请先登录\",\"data\":null}"); + }) + // 访问拒绝处理器,当已认证用户访问没有权限的资源时调用 + .accessDeniedHandler((request, response, accessDeniedException) -> { + response.setStatus(403); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":403,\"message\":\"访问被拒绝,权限不足\",\"data\":null}"); + }) + ); + + return http.build(); + } +} diff --git a/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/controller/UserController.java b/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..7d8f9ed3694df9161185572af015f0ab06cb9744 --- /dev/null +++ b/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/controller/UserController.java @@ -0,0 +1,513 @@ +package com.example.user.service.bootstrap.security.controller; + +import com.example.user.adapter.in.web.dto.*; +import com.example.user.adapter.out.persistence.entity.UserEntity; +import com.example.user.adapter.out.persistence.service.CustomUserDetailsService; +import com.example.user.service.application.service.TokenBlacklistService; +import com.example.user.service.common.JwtUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; + +/** + * 用户控制器 - Spring Security JWT认证系统的核心控制器 + * + * 这个控制器是整个认证系统的入口点,负责处理所有与用户认证和授权相关的HTTP请求 + * 主要功能包括: + * 1. 用户登录认证 - 验证用户名密码,生成JWT令牌 + * 2. 用户注册 - 创建新用户账户,密码加密存储 + * 3. 用户信息获取 - 基于JWT令牌获取当前登录用户信息 + * 4. 用户登出 - 使JWT令牌失效(可选实现) + * 5. 系统测试接口 - 用于开发阶段的功能验证 + * + * 设计理念: + * - RESTful API设计:使用标准的HTTP方法和状态码 + * - 统一响应格式:所有接口都返回Result格式的响应 + * - 安全性优先:密码加密、JWT令牌、异常信息脱敏 + * - 日志记录:详细记录用户操作和系统异常 + * + * 技术栈说明: + * @RestController - Spring MVC注解,将类标记为REST控制器,自动将返回值序列化为JSON + * @RequestMapping("/api/auth") - 设置控制器的基础路径,所有方法的路径都会以/api/auth开头 + * @RequiredArgsConstructor - Lombok注解,为所有final字段自动生成构造函数,实现依赖注入 + * @Slf4j - Lombok注解,自动生成名为log的日志对象,用于记录系统日志 + * @Tag - Swagger注解,用于API文档生成,将相关接口分组显示 + * + * 依赖注入说明: + * - JwtUtil:JWT令牌的生成和验证工具 + * - CustomUserDetailsService:用户详情服务,处理用户认证和数据操作 + * - UserMapper:用户数据访问层,直接操作数据库 + * + * 安全考虑: + * - 所有用户输入都经过验证(@Valid注解) + * - 密码使用BCrypt加密存储 + * - 异常信息不暴露敏感数据 + * - JWT令牌有过期时间限制 + */ +@Slf4j +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +@Tag(name = "用户认证和管理", description = "用户登录、注册、信息管理等接口") +public class UserController { + + /** + * 自定义用户详情服务 + * 用于处理用户认证、查询用户信息等业务逻辑 + */ + private final CustomUserDetailsService userDetailsService; + + /** + * JWT工具类 + * 用于生成和验证JWT Token + */ + private final JwtUtil jwtUtil; + + /** + * Token黑名单服务 + * 用于管理已失效的JWT Token,实现真正的登出功能 + */ + private final TokenBlacklistService tokenBlacklistService; + + /** + * 用户登录接口 - JWT认证系统的核心入口 + * + * 这是整个认证系统最重要的接口,负责验证用户身份并生成访问令牌 + * + * 登录流程详解: + * 1. 接收前端传来的用户名和密码(JSON格式) + * 2. 使用@Valid注解自动验证请求参数的格式(如用户名长度、密码复杂度等) + * 3. 调用CustomUserDetailsService.authenticate()方法验证用户名密码 + * - 从数据库查询用户信息 + * - 使用BCrypt算法验证密码哈希 + * - 检查用户状态(是否被禁用) + * 4. 验证成功后,使用JwtUtil生成JWT访问令牌 + * - 令牌中包含用户ID、用户名、角色等信息 + * - 设置令牌过期时间(通常24小时) + * 5. 构造登录响应对象,包含令牌和基本用户信息 + * 6. 记录登录日志,便于安全审计 + * + * 安全特性: + * - 密码传输:前端应使用HTTPS确保密码传输安全 + * - 密码存储:数据库中存储的是BCrypt加密后的哈希值,不是明文 + * - 令牌安全:JWT令牌使用HMAC SHA256算法签名,防止篡改 + * - 失败处理:登录失败不暴露具体原因(用户不存在 vs 密码错误) + * + * 错误处理: + * - 参数验证失败:返回400 Bad Request + * - 用户名或密码错误:返回统一的"用户名或密码错误"消息 + * - 系统异常:返回通用错误消息,具体错误记录在日志中 + * + * @param loginRequest 登录请求对象,包含用户名和密码 + * - username: 用户名,必填,长度3-20字符 + * - password: 密码,必填,长度6-20字符 + * @return Result 登录结果响应 + * 成功时包含: + * - accessToken: JWT访问令牌 + * - tokenType: 令牌类型(固定为"Bearer") + * - expiresIn: 令牌过期时间(秒) + * - userInfo: 用户基本信息(ID、用户名、角色等) + * @PostMapping("/login") POST请求映射,处理/api/auth/login路径的POST请求 + * @Operation Swagger注解,描述该接口的作用和详细信息 + */ + @PostMapping("/login") + @Operation(summary = "用户登录", description = "用户名密码登录,返回JWT token") + public Result login(@Valid @RequestBody LoginRequest loginRequest) { + try { + log.info("用户登录请求: {}", loginRequest.getUsername()); + + // 调用用户详情服务验证用户凭据 + // authenticate方法会检查用户名是否存在,密码是否正确 + UserEntity user = userDetailsService.authenticate(loginRequest.getUsername(), loginRequest.getPassword()); + if (user == null) { + // 如果用户认证失败,返回错误信息 + return Result.error("登录失败:用户名或密码错误"); + } + + // 用户认证成功,生成JWT Token + // JWT Token是一种无状态的认证方式,包含用户信息且经过签名验证 + String token = jwtUtil.generateToken(user.getUsername()); + + // 构建用户信息对象 - 为了避免暴露敏感信息,这里只返回部分信息 + // 使用Builder模式构建对象,代码更清晰易读 + UserInfo userInfo = UserInfo.builder() + .id(user.getId()) // 用户ID + .username(user.getUsername()) // 用户名 + .email(user.getEmail()) // 邮箱 + .phone(user.getPhone()) // 手机号 + .realName(user.getRealName()) // 真实姓名 + .status(user.getStatus()) // 用户状态 + .role(user.getRole()) // 用户角色 + .createTime(user.getCreateTime()) // 创建时间 + .lastLoginTime(LocalDateTime.now()) // 最后登录时间 + .build(); + + // 构建登录响应对象 + LoginResponse loginResponse = LoginResponse.builder() + .accessToken(token) // JWT访问令牌 + .tokenType("Bearer") // 令牌类型 + .userInfo(userInfo) // 用户信息 + .expiresIn(System.currentTimeMillis() + 86400000L) // 24小时后过期 + .build(); + + log.info("用户登录成功: {}", user.getUsername()); + // 返回登录成功的响应结果 + return Result.success("登录成功", loginResponse); + + } catch (Exception e) { + // 捕获异常并记录错误日志 + log.error("用户登录失败: {}", e.getMessage()); + // 返回登录失败的响应结果 + return Result.error("登录失败: " + e.getMessage()); + } + } + + /** + * 用户注册 + * + * 用户注册接口,处理新用户的注册请求 + * 主要流程: + * 1. 验证请求参数的有效性(通过@Valid注解自动验证) + * 2. 检查用户名是否已经存在 + * 3. 验证两次输入的密码是否一致 + * 4. 对密码进行加密处理 + * 5. 创建新用户并保存到数据库 + * + * @param registerRequest 注册请求对象,包含用户名、密码、邮箱等信息 + * @return Result 注册结果响应 + */ + @PostMapping("/register") + @Operation(summary = "用户注册", description = "用户注册接口") + public Result register(@Valid @RequestBody RegisterRequest registerRequest) { + try { + log.info("用户注册请求: username={}, email={}", registerRequest.getUsername(), registerRequest.getEmail()); + + // 1. 检查用户名是否已存在 + // 通过CustomUserDetailsService查询数据库,避免重复用户名 + UserEntity existingUser = userDetailsService.getUserByUsername(registerRequest.getUsername()); + if (existingUser != null) { + log.warn("注册失败: 用户名已存在 - {}", registerRequest.getUsername()); + return Result.error("用户名已存在,请选择其他用户名"); + } + + // 2. 验证两次密码是否一致 + // 前端应该已经验证过,但后端也需要再次验证确保数据安全 + if (!registerRequest.getPassword().equals(registerRequest.getConfirmPassword())) { + log.warn("注册失败: 两次密码不一致 - {}", registerRequest.getUsername()); + return Result.error("两次输入的密码不一致"); + } + + // 3. 创建新用户对象 + UserEntity newUser = new UserEntity(); + newUser.setUsername(registerRequest.getUsername()); + + // 4. 对密码进行加密 + // 使用Spring Security提供的密码编码器进行加密 + // 这里使用BCrypt算法,每次加密结果都不同,但验证时能正确匹配 + String encodedPassword = userDetailsService.generatePasswordHash(registerRequest.getPassword()); + newUser.setPassword(encodedPassword); + + // 5. 设置其他用户信息 + newUser.setEmail(registerRequest.getEmail()); + newUser.setPhone(registerRequest.getPhone()); + newUser.setRealName(registerRequest.getRealName()); + newUser.setRole("USER"); // 默认角色为普通用户 + newUser.setStatus(1); // 默认状态为启用 + + // 6. 保存用户到数据库 + // 使用MyBatis Plus的insert方法保存用户 + int result = userDetailsService.saveUser(newUser); + + if (result > 0) { + log.info("用户注册成功: username={}, userId={}", registerRequest.getUsername(), newUser.getId()); + return Result.success("注册成功,请使用用户名和密码登录"); + } else { + log.error("用户注册失败: 数据库插入失败 - {}", registerRequest.getUsername()); + return Result.error("注册失败,请稍后重试"); + } + + } catch (Exception e) { + // 捕获所有异常,避免敏感信息泄露 + log.error("用户注册异常: username={}, error={}", registerRequest.getUsername(), e.getMessage(), e); + return Result.error("系统异常,请稍后重试"); + } + } + + /** + * 获取当前用户信息接口 - 基于JWT令牌的用户信息查询 + * + * 这个接口展示了JWT认证系统中如何获取当前登录用户的信息 + * 它是一个受保护的接口,只有携带有效JWT令牌的请求才能访问 + * + * 工作流程详解: + * 1. 客户端发送请求时,必须在HTTP头中携带JWT令牌 + * 格式:Authorization: Bearer + * 2. Spring Security的JwtAuthenticationFilter会拦截请求 + * - 提取并验证JWT令牌的有效性 + * - 解析令牌中的用户信息(用户名、角色等) + * - 创建Authentication对象并设置到SecurityContext中 + * 3. 控制器方法通过Authentication参数获取当前用户信息 + * - Authentication.getName()获取用户名 + * - Authentication.getAuthorities()获取用户权限 + * 4. 根据用户名从数据库查询完整的用户信息 + * 5. 返回用户详细信息(密码等敏感信息已过滤) + * + * 安全机制: + * - JWT令牌验证:确保令牌未被篡改且未过期 + * - 用户状态检查:确保用户账户仍然有效(未被禁用) + * - 敏感信息过滤:返回的用户信息不包含密码等敏感数据 + * - 权限控制:可以根据用户角色返回不同级别的信息 + * + * 使用场景: + * - 用户个人中心页面加载用户信息 + * - 前端验证用户登录状态 + * - 获取用户权限信息用于前端菜单控制 + * - 用户信息更新前的数据回显 + * + * 错误处理: + * - 令牌无效或过期:返回401 Unauthorized + * - 用户不存在:返回404 Not Found + * - 用户被禁用:返回403 Forbidden + * - 系统异常:返回500 Internal Server Error + * + * @return Result 用户信息响应 + * 成功时包含用户的详细信息: + * - id: 用户ID + * - username: 用户名 + * - email: 邮箱 + * - realName: 真实姓名 + * - role: 用户角色 + * - status: 账户状态 + * - createTime: 创建时间 + * 注意:密码字段不会返回,确保安全性 + * @GetMapping("/info") GET请求映射,处理/api/auth/info路径的GET请求 + */ + @GetMapping("/info") + @Operation(summary = "获取用户信息", description = "获取当前登录用户的详细信息") + public Result getUserInfo() { + try { + // 从Spring Security上下文中获取当前认证信息 + // SecurityContextHolder是Spring Security提供的安全上下文持有者 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + // 检查用户是否已认证 + if (authentication == null || !authentication.isAuthenticated()) { + // 如果用户未认证,返回未授权错误 + return Result.unauthorized("用户未登录"); + } + + // 从认证信息中获取用户名 + String username = authentication.getName(); + // 根据用户名查询用户详细信息 + UserEntity user = userDetailsService.getUserByUsername(username); + + if (user == null) { + // 如果用户不存在,返回错误信息 + return Result.error("用户不存在"); + } + + // 构建用户信息对象 + UserInfo userInfo = UserInfo.builder() + .id(user.getId()) // 用户ID + .username(user.getUsername()) // 用户名 + .email(user.getEmail()) // 邮箱 + .phone(user.getPhone()) // 手机号 + .realName(user.getRealName()) // 真实姓名 + .status(user.getStatus()) // 用户状态 + .role(user.getRole()) // 用户角色 + .createTime(user.getCreateTime()) // 创建时间 + .lastLoginTime(user.getLastLoginTime()) // 最后登录时间 + .build(); + + // 返回用户信息 + return Result.success(userInfo); + + } catch (Exception e) { + // 捕获异常并记录错误日志 + log.error("获取用户信息失败: {}", e.getMessage()); + // 返回获取用户信息失败的响应结果 + return Result.error("获取用户信息失败: " + e.getMessage()); + } + } + + /** + * 用户登出接口 - JWT无状态认证的登出处理 + * + * 这个接口展示了JWT认证系统中登出功能的实现方式 + * 由于JWT是无状态的令牌,服务端不存储会话信息,因此登出的处理方式与传统session不同 + * + * JWT登出的特殊性: + * 1. 无状态特性:JWT令牌是自包含的,服务端不存储令牌状态 + * 2. 令牌有效期:令牌在过期前始终有效,无法在服务端直接"销毁" + * 3. 客户端责任:真正的登出主要依赖客户端删除存储的令牌 + * + * 登出流程: + * 1. 客户端发送登出请求(携带JWT令牌用于身份验证) + * 2. 服务端验证令牌有效性(确保是合法的登出请求) + * 3. 记录用户登出日志(用于安全审计和用户行为分析) + * 4. 清理服务端相关状态(如缓存的用户信息、临时数据等) + * 5. 返回登出成功响应 + * 6. 客户端收到响应后删除本地存储的JWT令牌 + * + * 可选的增强安全措施: + * 1. 令牌黑名单:将登出的令牌加入黑名单,拒绝后续使用 + * - 优点:可以立即使令牌失效 + * - 缺点:需要额外的存储空间和查询开销 + * 2. 短期令牌 + 刷新令牌:使用较短的访问令牌过期时间 + * - 减少令牌被滥用的时间窗口 + * - 通过刷新令牌机制保持用户体验 + * 3. 设备绑定:令牌与特定设备绑定,增加安全性 + * + * 客户端配合工作: + * 1. 删除本地存储的JWT令牌(localStorage、sessionStorage等) + * 2. 清除相关的用户状态和缓存数据 + * 3. 重定向到登录页面或首页 + * 4. 停止所有需要认证的后台请求 + * + * 安全考虑: + * - 即使用户已登出,令牌在过期前仍可能被恶意使用 + * - 建议使用HTTPS防止令牌在传输过程中被截获 + * - 定期更换JWT签名密钥增强安全性 + * - 监控异常的令牌使用模式 + * + * @return Result 登出结果响应 + * - 成功:返回"登出成功"消息 + * - 失败:返回相应的错误信息 + * @PostMapping("/logout") POST请求映射,处理/api/auth/logout路径的POST请求 + */ + @PostMapping("/logout") + @Operation(summary = "用户登出", description = "用户登出,清除认证信息并将token加入黑名单") + public Result logout(HttpServletRequest request) { + try { + // 从请求中提取JWT token + String token = getJwtFromRequest(request); + + if (token != null) { + // 将token加入黑名单,使其立即失效 + tokenBlacklistService.addToBlacklist(token); + log.info("Token已加入黑名单: {}", token.substring(0, Math.min(token.length(), 20)) + "..."); + } + + // 清除Spring Security上下文中的认证信息 + // 这样用户下次访问需要认证的接口时就需要重新登录 + SecurityContextHolder.clearContext(); + log.info("用户登出成功"); + // 返回登出成功的响应结果 + return Result.success("登出成功,token已失效"); + } catch (Exception e) { + // 捕获异常并记录错误日志 + log.error("用户登出失败: {}", e.getMessage()); + // 返回登出失败的响应结果 + return Result.error("登出失败: " + e.getMessage()); + } + } + + /** + * 测试接口 - JWT认证系统的功能验证接口 + * + * 这是一个专门用于开发和测试阶段的接口,用来验证JWT认证系统是否正常工作 + * 它可以帮助开发者快速检查认证流程的各个环节 + * + * 接口用途: + * 1. 验证JWT令牌解析:确认服务端能正确解析客户端发送的JWT令牌 + * 2. 验证用户身份提取:确认能从令牌中正确提取用户信息 + * 3. 验证权限控制:确认只有有效令牌才能访问受保护的接口 + * 4. 验证过滤器链:确认JwtAuthenticationFilter正常工作 + * 5. 调试认证问题:当认证出现问题时,可以通过此接口定位问题 + * + * 测试场景: + * 1. 正常情况测试: + * - 使用有效的JWT令牌访问 + * - 应该返回成功响应和用户信息 + * 2. 异常情况测试: + * - 不携带令牌:应该返回401 Unauthorized + * - 携带无效令牌:应该返回401 Unauthorized + * - 携带过期令牌:应该返回401 Unauthorized + * - 携带被篡改的令牌:应该返回401 Unauthorized + * + * 使用方法: + * 1. 首先通过/api/auth/login接口获取JWT令牌 + * 2. 在请求头中添加:Authorization: Bearer + * 3. 发送GET请求到/api/auth/test + * 4. 检查返回结果是否包含正确的用户信息 + * + * 返回信息说明: + * - 用户名:从JWT令牌中解析出的用户标识 + * - 权限列表:用户拥有的角色和权限信息 + * - 认证状态:确认用户已通过认证 + * - 当前时间:服务器处理请求的时间戳 + * + * 生产环境注意事项: + * - 此接口仅用于开发和测试,生产环境中应该移除或限制访问 + * - 可以通过配置文件控制是否启用此接口 + * - 建议添加访问频率限制,防止被恶意调用 + * - 不要在返回信息中包含敏感数据 + * + * 调试技巧: + * - 查看日志中的JWT解析过程 + * - 使用浏览器开发者工具检查请求头 + * - 使用在线JWT解码工具验证令牌内容 + * - 检查系统时间,确保令牌未过期 + * + * @return Result 测试结果响应 + * 成功时返回包含用户信息的测试消息: + * - 用户名 + * - 权限列表 + * - 当前时间 + * - 认证状态确认 + * @GetMapping("/test") GET请求映射,处理/api/auth/test路径的GET请求 + */ + @GetMapping("/test") + @Operation(summary = "测试接口", description = "需要JWT认证的测试接口") + public Result test() { + // 从Spring Security上下文中获取当前认证信息 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + // 获取认证用户的用户名 + String username = authentication.getName(); + // 返回测试成功的响应结果,包含用户名 + return Result.success("Hello, " + username + "! JWT认证成功!"); + } + + /** + * 从HTTP请求中提取JWT Token + * + * 这个私有方法用于从HTTP请求的Authorization头中提取JWT令牌 + * 支持标准的Bearer Token格式:Authorization: Bearer + * + * 提取流程: + * 1. 从请求头中获取Authorization字段的值 + * 2. 检查该值是否以"Bearer "开头(注意Bearer后面有一个空格) + * 3. 如果格式正确,提取Bearer后面的令牌部分 + * 4. 返回纯净的JWT令牌字符串 + * + * 安全考虑: + * - 只接受标准的Bearer Token格式,拒绝其他格式 + * - 对提取的令牌进行基本的格式验证 + * - 不在日志中记录完整的令牌内容,避免泄露 + * + * @param request HTTP请求对象 + * @return String JWT令牌字符串,如果未找到或格式不正确则返回null + */ + private String getJwtFromRequest(HttpServletRequest request) { + // 从请求头中获取Authorization字段 + String bearerToken = request.getHeader("Authorization"); + + // 检查Authorization头是否存在且以"Bearer "开头 + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + // 提取Bearer后面的令牌部分(去掉"Bearer "前缀) + return bearerToken.substring(7); + } + + // 如果没有找到有效的Bearer Token,返回null + return null; + } + +} \ No newline at end of file diff --git a/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/filter/JwtAuthenticationFilter.java b/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..8152dc3127a69330646f334abd9b3bff84b67706 --- /dev/null +++ b/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,351 @@ +package com.example.user.service.bootstrap.security.filter; + +import com.example.user.service.application.service.TokenBlacklistService; +import com.example.user.service.common.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +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.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * JWT认证过滤器 - Spring Security认证体系的核心组件 + * + * 【什么是过滤器?】 + * 过滤器(Filter)是Java Web开发中的重要组件,它像一个"检查站",在请求到达Controller之前 + * 对每个HTTP请求进行预处理和验证。想象成机场安检,每个乘客(请求)都必须通过安检(过滤器) + * 才能登机(到达Controller)。 + * + * 【JWT认证过滤器的作用】 + * 1. 拦截所有HTTP请求 + * 2. 检查请求头中是否包含有效的JWT Token + * 3. 验证Token的合法性(是否过期、签名是否正确) + * 4. 如果Token有效,将用户信息设置到Spring Security上下文中 + * 5. 如果Token无效或不存在,让请求继续,由其他安全机制处理 + * + * 【为什么继承OncePerRequestFilter?】 + * OncePerRequestFilter确保每个请求只被过滤一次,避免重复验证造成性能问题。 + * 在复杂的Web应用中,一个请求可能会经过多个过滤器,这个基类保证了我们的 + * JWT验证逻辑只执行一次。 + * + * 【Spring Security认证流程】 + * 1. 用户发送请求 → 2. JWT过滤器验证Token → 3. 设置认证上下文 → 4. 后续代码可以获取用户信息 + * + * 【注解说明】 + * @Slf4j: Lombok注解,自动生成日志对象log,用于记录调试和错误信息 + * @Component: Spring注解,将该类注册为Spring容器管理的Bean,可以被自动注入 + */ +@Slf4j +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + /** + * JWT工具类 - Token操作的核心工具 + * + * 【作用说明】 + * 负责JWT Token的生成、解析、验证等所有操作 + * + * 【@Autowired注解】 + * Spring的依赖注入注解,Spring容器会自动找到JwtUtil类型的Bean并注入到这里 + * 这样我们就不需要手动创建JwtUtil对象,Spring帮我们管理对象的生命周期 + */ + @Autowired + private JwtUtil jwtUtil; + + /** + * Token黑名单服务 - 管理已失效的JWT Token + * + * 【作用说明】 + * 用于解决JWT无状态特性导致的logout后token仍然有效的问题 + * 通过维护一个黑名单,记录已经失效的token + * + * 【JWT无状态认证的挑战】 + * JWT设计为无状态,服务器不保存token信息,这带来了扩展性优势 + * 但也带来了问题:无法主动使token失效(如用户logout) + * + * 【黑名单解决方案】 + * 1. 用户logout时,将token加入黑名单 + * 2. 每次验证token时,先检查是否在黑名单中 + * 3. 黑名单中的token被视为无效,拒绝访问 + * + * 【性能考虑】 + * 黑名单检查会增加一定的性能开销,但这是安全性的必要代价 + * 在生产环境中,建议使用Redis等高性能缓存来存储黑名单 + */ + @Autowired + private TokenBlacklistService tokenBlacklistService; + + /** + * Spring应用上下文 - Spring容器的入口 + * + * 【作用说明】 + * ApplicationContext是Spring容器的核心接口,通过它可以获取容器中的任何Bean + * + * 【为什么需要它?】 + * 在某些情况下,我们需要动态获取Bean,而不是通过@Autowired静态注入 + * 特别是在解决循环依赖问题时,这种方式非常有用 + */ + @Autowired + private ApplicationContext applicationContext; + + /** + * 用户详情服务 - 用户信息加载器 + * + * 【设计说明】 + * 注意这里没有使用@Autowired注解,而是通过延迟加载的方式获取 + * 这是为了避免循环依赖问题: + * JwtAuthenticationFilter需要UserDetailsService, + * 而UserDetailsService的实现类可能也需要其他被JwtAuthenticationFilter保护的组件 + * + * 【延迟加载的好处】 + * 只有在真正需要时才获取Bean,避免启动时的循环依赖问题 + */ + private UserDetailsService userDetailsService; + + /** + * 获取用户详情服务实例 - 延迟加载模式的实现 + * + * 【延迟加载模式(Lazy Loading)】 + * 这是一种设计模式,只有在真正需要对象时才创建或获取它 + * 类似于"用时再买"的概念,避免提前准备造成的资源浪费 + * + * 【解决循环依赖的核心方法】 + * 循环依赖问题:A需要B,B需要A,如果同时创建会造成死锁 + * 解决方案:A先创建,需要B时再去获取B,这样打破了循环 + * + * 【实现原理】 + * 1. 第一次调用时,userDetailsService为null,通过ApplicationContext获取Bean + * 2. 后续调用直接返回已获取的实例,提高性能 + * 3. 这种方式叫做"单例模式 + 延迟初始化" + * + * 【为什么不直接用@Autowired?】 + * 如果直接用@Autowired,Spring在启动时就要解决所有依赖关系 + * 可能会遇到循环依赖导致启动失败 + * + * @return UserDetailsService 用户详情服务实例 + */ + private UserDetailsService getUserDetailsService() { + if (userDetailsService == null) { + // 通过Spring容器动态获取Bean,避免循环依赖 + userDetailsService = applicationContext.getBean(UserDetailsService.class); + } + return userDetailsService; + } + + /** + * 过滤器核心方法 - JWT认证的完整流程实现 + * + * 【方法执行时机】 + * 每个HTTP请求到达Controller之前都会执行这个方法 + * 这是Spring Security认证链中的关键环节 + * + * 【完整的JWT认证流程】 + * 1. 提取Token:从HTTP请求头的Authorization字段中提取JWT Token + * 2. 解析Token:使用JWT工具类解析Token,获取用户名 + * 3. 验证Token:检查Token是否过期、签名是否正确 + * 4. 加载用户:从数据库加载用户详细信息 + * 5. 创建认证:创建Spring Security认证对象 + * 6. 设置上下文:将认证信息存储到SecurityContext中 + * 7. 继续处理:调用过滤器链,让请求继续向下传递 + * + * 【Spring Security上下文的作用】 + * SecurityContext就像一个"身份证明",一旦设置成功,后续的所有代码 + * 都可以通过SecurityContextHolder.getContext()获取当前用户信息 + * + * 【异常处理策略】 + * 如果JWT验证失败,不会阻止请求继续执行,而是清除认证上下文 + * 让Spring Security的其他机制(如返回401未授权)来处理 + * + * @param request HTTP请求对象,包含客户端发送的所有信息 + * @param response HTTP响应对象,用于向客户端发送响应 + * @param filterChain 过滤器链,用于调用下一个过滤器或最终的Controller + * @throws ServletException Servlet相关异常 + * @throws IOException 输入输出异常 + */ + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + try { + // 【步骤1:提取JWT Token】 + // 从HTTP请求头的Authorization字段中提取Token + // 标准格式:Authorization: Bearer + String jwt = getJwtFromRequest(request); + + // 【步骤2:检查Token是否存在】 + // StringUtils.hasText()检查字符串是否不为null且不为空字符串 + // 如果没有Token,说明可能是匿名访问或登录请求 + if (StringUtils.hasText(jwt)) { + // 【步骤2.5:检查Token是否在黑名单中】 + // 这是新增的安全检查,解决JWT无状态认证的logout问题 + // 如果token在黑名单中,说明用户已经logout,应该拒绝访问 + if (tokenBlacklistService.isBlacklisted(jwt)) { + log.warn("Token is blacklisted (user has logged out): {}", + jwt.substring(0, Math.min(20, jwt.length())) + "..."); + // 清除认证上下文,确保不会认证成功 + SecurityContextHolder.clearContext(); + // 继续过滤器链,让Spring Security返回401未授权 + filterChain.doFilter(request, response); + return; + } + + // 【步骤3:解析Token获取用户名】 + // 这一步会解析JWT的payload部分,提取用户名信息 + // 如果Token格式错误或签名无效,会抛出异常 + String username = jwtUtil.getUsernameFromToken(jwt); + + // 【步骤4:检查是否需要认证】 + // username != null: 确保成功从Token中提取到用户名 + // SecurityContextHolder.getContext().getAuthentication() == null: 确保当前请求还没有认证信息 + // 这样避免重复认证,提高性能 + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + // 【步骤5:加载用户详细信息】 + // 通过用户名从数据库加载完整的用户信息(包括权限、角色等) + // 这里使用了延迟加载的UserDetailsService,避免循环依赖 + UserDetails userDetails = getUserDetailsService().loadUserByUsername(username); + + // 【步骤6:验证Token的完整性】 + // 不仅要检查Token格式,还要验证: + // 1. Token是否过期 + // 2. 签名是否正确 + // 3. Token中的用户名是否与数据库中的一致 + if (jwtUtil.validateToken(jwt, userDetails.getUsername())) { + // 【步骤7:创建Spring Security认证对象】 + // UsernamePasswordAuthenticationToken是Spring Security的标准认证对象 + // 三个参数的含义: + // 1. principal: 主体,通常是用户详情对象 + // 2. credentials: 凭据,JWT模式下不需要密码,设为null + // 3. authorities: 权限列表,从用户详情中获取 + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + userDetails, // 用户详情(包含用户名、权限等) + null, // 凭据(JWT认证不需要密码) + userDetails.getAuthorities() // 用户权限列表 + ); + + // 【步骤8:设置认证详情】 + // 添加额外的认证信息,如IP地址、Session ID等 + // 这些信息在安全审计和日志记录中很有用 + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + // 【步骤9:设置到Spring Security上下文】 + // 这是最关键的一步!将认证信息存储到SecurityContext中 + // 后续的Controller、Service等都可以通过SecurityContextHolder获取当前用户信息 + // 这就是Spring Security"记住"用户身份的机制 + SecurityContextHolder.getContext().setAuthentication(authentication); + + // 记录成功的认证日志,便于调试和监控 + log.debug("JWT authentication successful for user: {}", username); + } else { + // Token验证失败,记录警告日志 + // 可能的原因:Token过期、签名错误、用户名不匹配等 + log.warn("JWT token validation failed for user: {}", username); + } + } + } + } catch (Exception e) { + // 【异常处理:安全优先原则】 + // 任何异常都不应该影响系统的安全性 + // 记录错误日志,便于排查问题 + log.error("Cannot set user authentication: {}", e.getMessage()); + // 清除认证上下文,确保不会使用错误或不完整的认证信息 + // 这是安全编程的重要原则:出错时选择更安全的状态 + SecurityContextHolder.clearContext(); + } + + // 继续过滤器链,让请求继续向下处理 + filterChain.doFilter(request, response); + } + + /** + * 从请求中提取JWT Token - HTTP标准认证头解析 + * + * 【HTTP Authorization头标准】 + * HTTP协议规定,认证信息应该放在Authorization请求头中 + * JWT认证的标准格式:Authorization: Bearer + * + * 【Bearer认证方案】 + * Bearer是OAuth 2.0规范中定义的认证方案 + * 意思是"持有者",表示谁持有这个Token,谁就有相应的权限 + * 格式:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + * + * 【为什么要单独提取?】 + * 1. 代码复用:多个地方可能需要提取Token + * 2. 职责分离:Token提取和Token验证是不同的职责 + * 3. 易于测试:可以单独测试Token提取逻辑 + * + * @param request HTTP请求对象,包含所有请求头信息 + * @return JWT Token字符串,如果不存在或格式错误则返回null + */ + private String getJwtFromRequest(HttpServletRequest request) { + // 从请求头中获取Authorization字段的值 + // 例如:"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + String bearerToken = request.getHeader("Authorization"); + // 使用JwtUtil工具类提取实际的Token部分(去掉"Bearer "前缀) + return jwtUtil.extractTokenFromHeader(bearerToken); + } + + /** + * 判断是否需要过滤 - 实现白名单机制 + * + * 【白名单机制的重要性】 + * 不是所有的接口都需要JWT认证,有些接口必须是公开的: + * 1. 登录接口:用户还没有Token,怎么能要求提供Token? + * 2. 注册接口:新用户注册时也没有Token + * 3. 文档接口:开发时需要查看API文档 + * + * 【OncePerRequestFilter的设计】 + * OncePerRequestFilter提供了shouldNotFilter方法,让我们可以灵活控制 + * 哪些请求需要过滤,哪些不需要。返回true表示跳过过滤。 + * + * 【安全考虑】 + * 白名单要谨慎设置,只有真正需要公开访问的接口才能加入白名单 + * 过多的白名单会降低系统安全性 + * + * 【路径匹配策略】 + * 使用startsWith进行前缀匹配,这样可以匹配一类接口 + * 例如:/user/login 可以匹配 /user/login、/user/login?username=xxx 等 + * + * @param request HTTP请求对象 + * @return boolean 是否不需要过滤,true表示跳过JWT验证 + * @throws ServletException Servlet异常 + */ + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + // 获取请求的URI路径 + String path = request.getRequestURI(); + + // 【业务接口白名单】 + // /user/login: 用户登录接口,用户通过用户名密码获取Token + // /user/register: 用户注册接口,新用户创建账号 + boolean isBusinessWhitelist = path.startsWith("/user/login") || + path.startsWith("/user/register"); + + // 【文档接口白名单】 + // 这些是Swagger API文档相关的接口,开发阶段需要公开访问 + // /doc.html: Knife4j文档首页 + // /swagger-ui: Swagger UI界面 + // /v3/api-docs: OpenAPI 3.0规范的JSON文档 + // /webjars: 前端资源文件(CSS、JS等) + boolean isDocWhitelist = path.startsWith("/doc.html") || + path.startsWith("/swagger-ui") || + path.startsWith("/v3/api-docs") || + path.startsWith("/webjars"); + + // 返回true表示不需要JWT验证,false表示需要验证 + return isBusinessWhitelist || isDocWhitelist; + } +} \ No newline at end of file diff --git a/user-service/user-service-common/pom.xml b/user-service/user-service-common/pom.xml index 20aa5fc4a8e02f4fce861aab3cca87288a00c8c4..d1d701d2bf4c6f793399caf5947d884b04d15ac8 100644 --- a/user-service/user-service-common/pom.xml +++ b/user-service/user-service-common/pom.xml @@ -14,6 +14,24 @@ 3.2.4 + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + org.springframework.boot spring-boot-starter diff --git a/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java b/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java index f65593845868a5b959ccd7990d64a4675e1cf2bb..0137dd90e932173f00214e825eb0196872b60aac 100644 --- a/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java +++ b/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java @@ -1,78 +1,124 @@ package com.example.user.service.common; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import javax.crypto.SecretKey; import java.util.Date; -import java.util.HashMap; -import java.util.Map; - +/** + * JWT工具类 + * 用于生成、验证和解析JWT token + */ @Slf4j +@Component public class JwtUtil { + // JWT密钥,实际项目中应该从配置文件读取 + @Value("${jwt.secret:mySecretKeyForJwtTokenGenerationAndValidation123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789}") + private String secret; - private static final String SECRET_KEY = "123456"; - - - private static final long EXPIRATION_TIME = 5 * 60 * 1000; // 5分钟 + // token过期时间(毫秒) + @Value("${jwt.expiration:86400000}") // 默认24小时 + private Long expiration; - - - public static String generateToken(Long userId, String username, Boolean isSuper) { - Map claims = new HashMap<>(); - claims.put("id", userId); - claims.put("name", username); - claims.put("is_super", isSuper); + /** + * 生成JWT token + * @param username 用户名 + * @return JWT token + */ + public String generateToken(String username) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + expiration); return Jwts.builder() - .setClaims(claims) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) - .signWith(SignatureAlgorithm.HS256, SECRET_KEY) + .setSubject(username) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(getSigningKey(), SignatureAlgorithm.HS512) .compact(); } - - public static Claims parseToken(String token) { - return Jwts.parser() - .setSigningKey(SECRET_KEY) - .parseClaimsJws(token) - .getBody(); + /** + * 从token中获取用户名 + * @param token JWT token + * @return 用户名 + */ + public String getUsernameFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.getSubject(); } - - public static Long getUserIdFromToken(String token) { - Claims claims = parseToken(token); - return claims.get("id", Long.class); + /** + * 验证token是否有效 + * @param token JWT token + * @param username 用户名 + * @return 是否有效 + */ + public boolean validateToken(String token, String username) { + try { + String tokenUsername = getUsernameFromToken(token); + return (username.equals(tokenUsername) && !isTokenExpired(token)); + } catch (Exception e) { + log.error("Token validation failed: {}", e.getMessage()); + return false; + } } - - public static String getUsernameFromToken(String token) { - Claims claims = parseToken(token); - return claims.get("name", String.class); + /** + * 检查token是否过期 + * @param token JWT token + * @return 是否过期 + */ + public boolean isTokenExpired(String token) { + Date expiration = getExpirationDateFromToken(token); + return expiration.before(new Date()); } - public static Boolean getIsSuperFromToken(String token) { - Claims claims = parseToken(token); - return claims.get("is_super", Boolean.class); + /** + * 从token中获取过期时间 + * @param token JWT token + * @return 过期时间 + */ + public Date getExpirationDateFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.getExpiration(); } + /** + * 从token中解析Claims + * @param token JWT token + * @return Claims + */ + private Claims getClaimsFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } - public static boolean validateToken(String token) { - try { - parseToken(token); - return true; - } catch (Exception e) { - log.error("JWT令牌验证失败: {}", e.getMessage()); - return false; - } + /** + * 获取签名密钥 + * @return 签名密钥 + */ + private SecretKey getSigningKey() { + byte[] keyBytes = secret.getBytes(); + return Keys.hmacShaKeyFor(keyBytes); } - public static boolean isTokenExpired(String token) { - Claims claims = parseToken(token); - return claims.getExpiration().before(new Date()); + /** + * 从请求头中提取token + * @param authHeader Authorization头 + * @return JWT token + */ + public String extractTokenFromHeader(String authHeader) { + if (authHeader != null && authHeader.startsWith("Bearer ")) { + return authHeader.substring(7); + } + return null; } } \ No newline at end of file diff --git a/user-service/user-service-domain/pom.xml b/user-service/user-service-domain/pom.xml index 502cbe0cdd4a7f789e429f56d87b2162bb6f9b42..47ee4c1e30cd9ea027e1aa89f68750fee7d1ef72 100644 --- a/user-service/user-service-domain/pom.xml +++ b/user-service/user-service-domain/pom.xml @@ -15,6 +15,17 @@ + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.security + spring-security-test + test + + org.springframework.boot spring-boot-starter diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java index 8d9521b5c44f57f74deb13c1872241318475abe4..7bf3f1aa9988297734b45090f39e68f9db6fc1f7 100644 --- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java @@ -21,17 +21,22 @@ public class User { private UserAge age; private Email email; private Password password; - private IsSuper isSuper; // 添加isSuper字段 + private Phone phone; + private RealName realName; + private Status status; + private Role role; + private CreateTime createTime; + private UpdateTime updateTime; + private LastLoginTime lastLoginTime; public User() { } - public User(UserId id, UserName name, UserAge age, Email email, Password password,IsSuper isSuper) { + public User(UserId id, UserName name, UserAge age, Email email, Password password) { this.id = id; this.name = name; this.age = age; this.email = email; this.password = password; - this.isSuper = isSuper; } public User( UserName name, UserAge age, Email email, Password password) { @@ -40,15 +45,22 @@ public class User { this.age = age; this.email = email; this.password = password; - this.isSuper = new IsSuper(false); + } public User(UserId userId, UserName userName, UserAge userAge, Email email) { - this.id = id; - this.name = name; - this.age = age; + this.id = userId; + this.name = userName; + this.age = userAge; this.email = email; - this.isSuper = new IsSuper(false); + + } + + public User(UserId userId, UserName userName, Email email, Password password) { + this.id = userId; + this.name = userName; + this.email = email; + this.password = password; } diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/config/PasswordConfig.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/config/PasswordConfig.java index ebb036dc573ecf94a8af1b3a7b23bdf713906c9d..9bb30fbd4b28b5bec16fd1463128c5941e425cb2 100644 --- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/config/PasswordConfig.java +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/config/PasswordConfig.java @@ -1,14 +1,32 @@ -//package com.example.user.service.domain.config; -// -//import org.springframework.context.annotation.Bean; -//import org.springframework.context.annotation.Configuration; -//import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -//import org.springframework.security.crypto.password.PasswordEncoder; -// -//@Configuration -//public class PasswordConfig { -// @Bean -// public PasswordEncoder passwordEncoder() { -// return new BCryptPasswordEncoder(); -// } -//} +package com.example.user.service.domain.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * 密码编码器配置类 + * 独立配置以避免循环依赖 + * + * 这个类专门用于配置密码编码器Bean,独立出来可以避免在复杂依赖关系中产生循环依赖问题 + */ +@Configuration +public class PasswordConfig { + + /** + * 密码编码器Bean + * 使用BCrypt算法对密码进行加密和验证 + * + * BCrypt是一种强大的密码哈希函数,具有以下特点: + * 1. 自动加盐(Salt) - 每次加密都会生成随机盐值,防止彩虹表攻击 + * 2. 可配置的计算复杂度 - 可以调整计算轮数来平衡安全性和性能 + * 3. 单向性 - 无法从哈希值反推出原始密码 + * + * @return PasswordEncoder 密码编码器实例 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/CreateTime.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/CreateTime.java new file mode 100644 index 0000000000000000000000000000000000000000..d11074cdddf0919f3f9796d7705ed1abbb102f35 --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/CreateTime.java @@ -0,0 +1,9 @@ +package com.example.user.service.domain.valueobject; + +import java.time.LocalDateTime; + +public record CreateTime(LocalDateTime createTime) { + public LocalDateTime getValue() { + return createTime; + } +} diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/IsSuper.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/IsSuper.java deleted file mode 100644 index f05ce455c4d7b8f6c90f818413590ec88c4c37f8..0000000000000000000000000000000000000000 --- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/IsSuper.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.user.service.domain.valueobject; - - -public record IsSuper(boolean value) { - public IsSuper { - - if (value && !isValidSuperUser()) { - throw new IllegalArgumentException("Invalid super user configuration"); - } - } - private boolean isValidSuperUser() { - - return true; - } - - public static IsSuper fromBoolean(boolean value) { - return new IsSuper(value); - } - -} \ No newline at end of file diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/LastLoginTime.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/LastLoginTime.java new file mode 100644 index 0000000000000000000000000000000000000000..ba06c343f6df04ad22c1e0e7de6074d8eb698ce2 --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/LastLoginTime.java @@ -0,0 +1,7 @@ +package com.example.user.service.domain.valueobject; + +import java.time.LocalDateTime; + +public record LastLoginTime(LocalDateTime value) { + public LocalDateTime getValue() {return value;} +} diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Phone.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Phone.java new file mode 100644 index 0000000000000000000000000000000000000000..578177d8cb33a6d6c78077fe9dcc73a1389188a1 --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Phone.java @@ -0,0 +1,5 @@ +package com.example.user.service.domain.valueobject; + +public record Phone(String phone) { + public String getValue() { return phone; } +} diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/RealName.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/RealName.java new file mode 100644 index 0000000000000000000000000000000000000000..73fe39e4e485179bedead7b3d899f301ab7741ef --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/RealName.java @@ -0,0 +1,5 @@ +package com.example.user.service.domain.valueobject; + +public record RealName(String realName) { + public String getValue() { return realName; } +} diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Role.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Role.java new file mode 100644 index 0000000000000000000000000000000000000000..4cfb617a24b0e387cba049924a38f79cc229145c --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Role.java @@ -0,0 +1,5 @@ +package com.example.user.service.domain.valueobject; + +public record Role(String role) { + public String getValue() {return role;} +} diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Status.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Status.java new file mode 100644 index 0000000000000000000000000000000000000000..2dc838a64f84781fc82d2c21c178f6e3396320c9 --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Status.java @@ -0,0 +1,7 @@ +package com.example.user.service.domain.valueobject; + +public record Status(Integer status) { + public Integer getValue() { + return status; + } +} diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UpdateTime.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UpdateTime.java new file mode 100644 index 0000000000000000000000000000000000000000..8574d02810368af5d660431c08953c48966f3ba0 --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UpdateTime.java @@ -0,0 +1,7 @@ +package com.example.user.service.domain.valueobject; + +import java.time.LocalDateTime; + +public record UpdateTime(LocalDateTime value) { + public LocalDateTime getValue() {return value;} +}