diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b5d4abcede2c102d29e872aea2c572d13b76708d..cbe6e5fe7b11239182ee9da6adfed8bcef9bec5f 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -7,15 +7,29 @@ - + - - - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml index 26953e9d99ef69007b7f49f84c971f235fefc89d..6418d5e0f18ee682ea8ce05461aa70d009b20db0 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -1,8 +1,27 @@ - + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/misc.xml b/.idea/misc.xml index 29f55d2bb5db165c7b03de7300a9065b367405c9..102fee36c47a6ba56261b8e9fe08272bdfa2e77c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,13 +4,19 @@ - + \ No newline at end of file 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..7ba8bd5db047647fb746dec45f803f167ed4d017 100644 --- a/qa-service/pom.xml +++ b/qa-service/pom.xml @@ -11,22 +11,34 @@ 21 UTF-8 UTF-8 - 3.0.2 + 3.2.4 + 2023.0.1.0 + 2023.0.1 + + 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 +46,19 @@ pom import + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + + + + com.baomidou + mybatis-plus-spring-boot3-starter + 3.5.14 + @@ -54,7 +79,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..7cf0a5e16d698c049b5ab82fde2de3ca7564c94c --- /dev/null +++ b/qa-service/qa-service-adapter/pom.xml @@ -0,0 +1,65 @@ + + + 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.springframework.boot + spring-boot-starter-amqp + + + com.fasterxml.jackson.core + jackson-databind + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + 4.5.0 + + + com.alibaba.fastjson2 + fastjson2 + 2.0.58 + + + org.springframework.amqp + spring-rabbit-test + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/.gitignore b/qa-service/qa-service-adapter/qa-adapter-in/.gitignore similarity index 100% rename from qa-service/.gitignore rename to qa-service/qa-service-adapter/qa-adapter-in/.gitignore 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/.gitignore b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..549e00a2a96fa9d7c5dbc9859664a78d980158c2 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ 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..d333edd0ca6360906d80f03073942e35cf98f327 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/pom.xml @@ -0,0 +1,68 @@ + + + 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 + + + + com.example + qa-service-application + 0.0.1-SNAPSHOT + + + com.example + qa-service-application + 0.0.1-SNAPSHOT + compile + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + --enable-preview + + + + + + diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/config/MoonShotConfig.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/config/MoonShotConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..4a8493815c813546936c73b5cf5a48b8a9ce2dd9 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/config/MoonShotConfig.java @@ -0,0 +1,19 @@ +package com.example.qa.adapter.in.web.config; + + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @Author: zhangyucheng + * @Date: 2025/9/17 17:29 + */ +@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/com/example/qa/adapter/in/web/config/RabbitConfig.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/config/RabbitConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..a7356c8405a6e5322b6adc3bdc34f7d8269ae6eb --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/config/RabbitConfig.java @@ -0,0 +1,56 @@ +package com.example.qa.adapter.in.web.config; + +import com.example.qa.service.domain.Qa; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.DefaultJackson2JavaTypeMapper; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class RabbitConfig { + //让springboot启动时自动创建队列 + + @Bean + public Queue queue() { + return new Queue("test"); + } + + @Bean + public Queue queue2() { + return new Queue("test2"); + } + + @Bean + public Queue queue3() { + return new Queue("test3"); + } + + @Bean + public MessageConverter messageConverter() { + Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); + // 配置类型映射器以支持 Student 类的反序列化 + DefaultJackson2JavaTypeMapper typeMapper = new DefaultJackson2JavaTypeMapper(); + Map> idClassMapping = new HashMap<>(); + // 添加 Student 类的映射 如果有更多的类需要转换,则继续添加 + idClassMapping.put("com.example.qa-service-domain.Qa", Qa.class); + typeMapper.setIdClassMapping(idClassMapping); + typeMapper.setTrustedPackages("com.example.qa-service-domain"); + converter.setJavaTypeMapper(typeMapper); + + return converter; + } + + @Bean + public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { + RabbitTemplate template = new RabbitTemplate(connectionFactory); + template.setMessageConverter(messageConverter()); + return template; + } +} \ No newline at end of file diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/config/RestConfig.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/config/RestConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..a3e0f0f8682ec4d03684c92c6198815948315585 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/config/RestConfig.java @@ -0,0 +1,18 @@ +package com.example.qa.adapter.in.web.config; + + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +/** + * @Author: zhangyucheng + * @Date: 2025/9/17 17:29 + */ +@Configuration +public class RestConfig { + @Bean + public RestTemplate getRestTemplate(){ + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/controller/QaController.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/controller/QaController.java new file mode 100644 index 0000000000000000000000000000000000000000..2f3e865b2edc1b1c5ea96ba4ca8d3a6495131963 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/controller/QaController.java @@ -0,0 +1,136 @@ +package com.example.qa.adapter.in.web.controller; + +import com.alibaba.fastjson2.JSON; +import com.example.qa.adapter.in.web.dto.*; +import com.example.qa.service.application.command.*; +import com.example.qa.service.application.port.in.*; +import com.example.qa.service.domain.Qa; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.web.bind.annotation.*; +import java.util.List; + +@Slf4j +@RequestMapping("/qa") +@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; + + @GetMapping("") + public List getQas() { + log.info("getQas"); + return getQaListUseCase.getQas(); + } + + /** + * @author liyujie + * @param + * @return + */ + + + @PostMapping("") + public Qa createQa(@RequestBody CreateQaRequestDTO createQaRequestDTO){ + + CreateQaCommand command=CreateQaCommand.builder() + .question(createQaRequestDTO.question()) + .answer(createQaRequestDTO.answer()) + .build(); + + return createQaUseCase.createQa(command); + } + + + /** + * @author liaoqi + * @param + * @return + */ + @DeleteMapping("{id}") + public String deleteQa(@PathVariable("id") Long id){ + deleteQaUseCase.deleteQa(id); + return "success"; + } + /** + * @author zhujunjie + * @param + * @return + */ + @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; + } + /** + * @author ZhangQianyu + 2210705220 + * @param id + * @return + */ + @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; + } + + + @Resource + private RabbitTemplate rabbitTemplate; + /** + * @param : + * @return String + * @author zhujunjie + * @date 2025/9/17 18:27 + */ + @GetMapping("send") + public String send() { + //把消息交给mq处理 + rabbitTemplate.convertAndSend("test","hello world"); + return "success"; + } + /** + * @param createQaRequestDTO: + * @return String + * @author zhujunjie + * @date 2025/9/17 19:24 + */ + @PostMapping("sendObject") + public String sendObject(@RequestBody CreateQaRequestDTO createQaRequestDTO) { + //把消息交给mq处理 + Qa qa=new Qa(); + CreateQaCommand command=CreateQaCommand.builder() + .question(createQaRequestDTO.question()) + .answer(createQaRequestDTO.answer()) + .build(); + qa=createQaUseCase.createQa(command); + rabbitTemplate.convertAndSend("test2",qa); + return "success"; + } + /** + * @param : + * @return String + * @author zhujunjie + * @date 2025/9/17 20:12 + */ + @GetMapping("sendJson") + public String sendJson(@RequestParam("question") String question) { + rabbitTemplate.convertAndSend("test3", JSON.toJSONString(question)); + return "success"; + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/ChatCompletionRequestDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/ChatCompletionRequestDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..7ef84a0212635e066db4b8384af6964471be897a --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/ChatCompletionRequestDTO.java @@ -0,0 +1,12 @@ +package com.example.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/com/example/qa/adapter/in/web/dto/ChatCompletionResponseDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/ChatCompletionResponseDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..ef17370f977025d9851a39b31937c08b8c941a8a --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/ChatCompletionResponseDTO.java @@ -0,0 +1,14 @@ +package com.example.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/com/example/qa/adapter/in/web/dto/Choice.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/Choice.java new file mode 100644 index 0000000000000000000000000000000000000000..6b135a47035238ac867eac9a0bb6b683ab111477 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/Choice.java @@ -0,0 +1,10 @@ +package com.example.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/com/example/qa/adapter/in/web/dto/CreateQaRequestDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/CreateQaRequestDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..fe642cc8459b392594c39a07a0fcaa9c1f1a6273 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/CreateQaRequestDTO.java @@ -0,0 +1,6 @@ +package com.example.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/com/example/qa/adapter/in/web/dto/Message.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/Message.java new file mode 100644 index 0000000000000000000000000000000000000000..e000aacc6b923259b4b2b4272eb6cf78bd060f7c --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/Message.java @@ -0,0 +1,9 @@ +package com.example.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/com/example/qa/adapter/in/web/dto/QaResponseDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/QaResponseDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..ff5501f165024912bd49f9418e0a6f11250fd7e8 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/QaResponseDTO.java @@ -0,0 +1,18 @@ +package com.example.qa.adapter.in.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +/** + * @author zhujunjie + * @param + * @return + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +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/com/example/qa/adapter/in/web/dto/UpdateQaRequestDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/UpdateQaRequestDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..4cdfc5d0da0fd60d4a5f38051265a4917dd7001a --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/UpdateQaRequestDTO.java @@ -0,0 +1,10 @@ +package com.example.qa.adapter.in.web.dto; +/** + * @author zhujunjie + * @param + * @return + */ +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/com/example/qa/adapter/out/persistence/listener/Demo2Listener.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/out/persistence/listener/Demo2Listener.java new file mode 100644 index 0000000000000000000000000000000000000000..a767b0f8e5b83757c4ce48da6a13893e5f8d509c --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/out/persistence/listener/Demo2Listener.java @@ -0,0 +1,16 @@ +package com.example.qa.adapter.out.persistence.listener; + + +import com.example.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 = "test2") +public class Demo2Listener { + @RabbitHandler + public void handler(Qa qa){ + System.out.println("收到的消息是:"+qa); + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/out/persistence/listener/Demo3Listener.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/out/persistence/listener/Demo3Listener.java new file mode 100644 index 0000000000000000000000000000000000000000..87944488b97674f4610e17ac893ed6c625b68a9d --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/out/persistence/listener/Demo3Listener.java @@ -0,0 +1,102 @@ +package com.example.qa.adapter.out.persistence.listener; + + +import com.alibaba.fastjson2.JSON; +import com.example.qa.adapter.in.web.config.MoonShotConfig; +import com.example.qa.adapter.in.web.dto.ChatCompletionRequestDTO; +import com.example.qa.adapter.in.web.dto.ChatCompletionResponseDTO; +import com.example.qa.adapter.in.web.dto.Choice; +import com.example.qa.adapter.in.web.dto.Message; +import com.example.qa.service.application.command.CreateQaCommand; +import com.example.qa.service.application.port.in.CreateQaUseCase; +import com.example.qa.service.domain.Qa; +import jakarta.annotation.Resource; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitHandler; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.Arrays; +@Slf4j +@Component +@RequiredArgsConstructor +@RabbitListener(queues = "test3") +public class Demo3Listener { + @Resource + private MoonShotConfig moonShotConfig; + @Resource + private RestTemplate restTemplate; + private final CreateQaUseCase createQaUseCase; + + @RabbitHandler + public void handler(String question) { + /** + * @author zhangyucheng + * @param + * @return + */ + try { + log.info("收到问题: {}", question); + 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 提供的人工智能助手..."); + + Message userMessage = new Message(); + userMessage.setRole("user"); + userMessage.setContent(question); + + request.setMessages(Arrays.asList(systemMessage, userMessage)); + + HttpEntity requestEntity = new HttpEntity<>(request, headers); + ChatCompletionResponseDTO response = restTemplate.exchange( + moonShotConfig.getUrl(), + HttpMethod.POST, + requestEntity, + ChatCompletionResponseDTO.class + ).getBody(); + + log.info("API响应: {}", response); + + String answer = extractAnswerFromResponse(response); + + CreateQaCommand command = new CreateQaCommand( + null, // id由数据库自动生成 + question, + answer + ); + createQaUseCase.createQa(command); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * @param + * @return + * @author zhangyucheng + */ + private String extractAnswerFromResponse(ChatCompletionResponseDTO response) { + if (response != null && response.getChoices() != null && !response.getChoices().isEmpty()) { + Choice firstChoice = response.getChoices().get(0); + if (firstChoice.getMessage() != null) { + return firstChoice.getMessage().getContent(); + } + } + return "抱歉,无法获取答案"; + } +} + diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/out/persistence/listener/DemoListener.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/out/persistence/listener/DemoListener.java new file mode 100644 index 0000000000000000000000000000000000000000..12f17981271919f14f6b9dfc1979c1e157b7b38b --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/out/persistence/listener/DemoListener.java @@ -0,0 +1,15 @@ +package com.example.qa.adapter.out.persistence.listener; + +import org.springframework.amqp.rabbit.annotation.RabbitHandler; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +@RabbitListener(queues = "test") +public class DemoListener { + + @RabbitHandler + public void handler(String msg){ + System.out.println("收到的消息是:"+msg); + } +} 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/.gitignore b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..549e00a2a96fa9d7c5dbc9859664a78d980158c2 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ 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..9d786080b448936b020cf883b21bd357f164ae29 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/pom.xml @@ -0,0 +1,57 @@ + + + 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 + + + + + + + + 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/com/example/qa/adapter/out/persistence/bridge/CreateQaBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/CreateQaBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..0df2a1a25ef1adbb56a942d3b2c7a8f29a58bc8c --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/CreateQaBridge.java @@ -0,0 +1,30 @@ +package com.example.qa.adapter.out.persistence.bridge; + +import com.example.qa.adapter.out.persistence.convertor.QaConvertor; +import com.example.qa.adapter.out.persistence.entity.QaEntity; +import com.example.qa.adapter.out.persistence.mapper.QaMapper; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.port.CreateQaPort; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/* + author: liyujie + */ + +@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/com/example/qa/adapter/out/persistence/bridge/DeleteQaBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/DeleteQaBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..a58a72537a5ed5106afb43159f6e3609059809d0 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/DeleteQaBridge.java @@ -0,0 +1,24 @@ +package com.example.qa.adapter.out.persistence.bridge; + +import com.example.qa.adapter.out.persistence.mapper.QaMapper; +import com.example.qa.service.domain.port.DeleteQaPort; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * @author liaoqi + * @param + * @return + */ +@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/com/example/qa/adapter/out/persistence/bridge/GetQaByIdBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/GetQaByIdBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..5b4fa5f926cee135dca97dbdbb1cf84eada0ae0c --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/GetQaByIdBridge.java @@ -0,0 +1,26 @@ +package com.example.qa.adapter.out.persistence.bridge; + +/* + author: ZhangQianyu + 2210705220 + */ + +import com.example.qa.adapter.out.persistence.convertor.QaConvertor; +import com.example.qa.adapter.out.persistence.entity.QaEntity; +import com.example.qa.adapter.out.persistence.mapper.QaMapper; +import com.example.qa.service.domain.Qa; +import com.example.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/com/example/qa/adapter/out/persistence/bridge/GetQaListBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/GetQaListBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..d5b2378f9983afb524f3481e824d6b39ce682440 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/GetQaListBridge.java @@ -0,0 +1,36 @@ +package com.example.qa.adapter.out.persistence.bridge; + +/* + author: ZhangQianyu + 2210705220 + */ + +import com.example.qa.adapter.out.persistence.convertor.QaConvertor; +import com.example.qa.adapter.out.persistence.entity.QaEntity; +import com.example.qa.adapter.out.persistence.mapper.QaMapper; +import com.example.qa.service.domain.Qa; +import com.example.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/com/example/qa/adapter/out/persistence/bridge/UpdateQaBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/UpdateQaBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..b4d3c9440867a77f94bda1a0317efb1c625a196e --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/UpdateQaBridge.java @@ -0,0 +1,27 @@ +package com.example.qa.adapter.out.persistence.bridge; +import com.example.qa.adapter.out.persistence.convertor.QaConvertor; +import com.example.qa.adapter.out.persistence.mapper.QaMapper; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.port.UpdateQaPort; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * @author zhujunjie + * @param + * @return + */ +@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/com/example/qa/adapter/out/persistence/convertor/QaConvertor.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/convertor/QaConvertor.java new file mode 100644 index 0000000000000000000000000000000000000000..7a9bbd5f277ca5c8a68077d18899f9652ba66b4d --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/convertor/QaConvertor.java @@ -0,0 +1,29 @@ +package com.example.qa.adapter.out.persistence.convertor; + +import com.example.qa.adapter.out.persistence.entity.QaEntity; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.valueobject.Answer; +import com.example.qa.service.domain.valueobject.Question; +import com.example.qa.service.domain.valueobject.QaId; + +/* + author: zhangyucheng + */ +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().getValue(), + qa.getQuestion().getValue(), + qa.getAnswer().getValue() + ); + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/entity/QaEntity.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/entity/QaEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..fa70580ee7f7329135dd2769f98a50e660116001 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/entity/QaEntity.java @@ -0,0 +1,22 @@ +package com.example.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; + +/* + author: zhangyucheng + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName("qa") +public class QaEntity { + @TableId(type= IdType.ASSIGN_ID) + private long id; + private String question; + private String answer; +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/mapper/QaMapper.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/mapper/QaMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..937e69aef998a114b1da57bdadc6592f618e025e --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/mapper/QaMapper.java @@ -0,0 +1,10 @@ +package com.example.qa.adapter.out.persistence.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.qa.adapter.out.persistence.entity.QaEntity; + +/* + author: zhangyucheng + */ +public interface QaMapper extends BaseMapper { +} diff --git a/qa-service/qa-service-application/pom.xml b/qa-service/qa-service-application/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..8078a941519ae8780ad017eb068c9ecc7457898f --- /dev/null +++ b/qa-service/qa-service-application/pom.xml @@ -0,0 +1,61 @@ + + + 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.projectlombok + lombok + + + + + + + + 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/com/example/qa/service/application/command/CreateQaCommand.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/command/CreateQaCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..839adf24814dbc3994a762d5b03b424799f60f3c --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/command/CreateQaCommand.java @@ -0,0 +1,13 @@ +package com.example.qa.service.application.command; +/* + author: liyujie + */ +import lombok.Builder; + +@Builder +public record CreateQaCommand( + Long id, + String question, + String answer +) { +} diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/command/UpdateQaCommand.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/command/UpdateQaCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..15c4c85466925ce5b3ad12ab244e578d73bcc6ea --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/command/UpdateQaCommand.java @@ -0,0 +1,13 @@ +package com.example.qa.service.application.command; + +import lombok.Builder; +/** + * @author zhujunjie + * @param + * @return + */ +@Builder +public record UpdateQaCommand(Long id, + String question, + String answer) { +} diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/CreateQaUseCase.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/CreateQaUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..22fb92f744659be3a9c903076e7227e9f139da3d --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/CreateQaUseCase.java @@ -0,0 +1,12 @@ +package com.example.qa.service.application.port.in; + +import com.example.qa.service.application.command.CreateQaCommand; +import com.example.qa.service.domain.Qa; + +/* + author: liyujie + */ + +public interface CreateQaUseCase { + Qa createQa(CreateQaCommand command); +} diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/DeleteQaUseCase.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/DeleteQaUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..ccb167507b4556d121458d54bc98ee879df75f9d --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/DeleteQaUseCase.java @@ -0,0 +1,10 @@ +package com.example.qa.service.application.port.in; + +/** + * @author liaoqi + * @param + * @return + */ +public interface DeleteQaUseCase { + void deleteQa(Long id); +} diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/GetQaByIdUseCase.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/GetQaByIdUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..ad20ceace4db42cc4a9a987c12594b778d8014be --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/GetQaByIdUseCase.java @@ -0,0 +1,11 @@ +package com.example.qa.service.application.port.in; + +/* + author: ZhangQianyu + 2210705220 + */ + +import com.example.qa.service.domain.Qa; + +public interface GetQaByIdUseCase { + Qa getQaById(Long id); +} diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/GetQaListUseCase.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/GetQaListUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..c8ddb7c181b466b2d8ea923223064e58ec61b7a1 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/GetQaListUseCase.java @@ -0,0 +1,13 @@ +package com.example.qa.service.application.port.in; + +/* + author: ZhangQianyu + 2210705220 + */ + +import com.example.qa.service.domain.Qa; + +import java.util.List; + +public interface GetQaListUseCase { + List getQas(); +} diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/UpdateQaUseCase.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/UpdateQaUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..8ff994fb0763b2c9ba24ed436cb8b2ca08edf35b --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/UpdateQaUseCase.java @@ -0,0 +1,12 @@ +package com.example.qa.service.application.port.in; +/** + * @author zhujunjie + * @param + * @return + */ +import com.example.qa.service.application.command.UpdateQaCommand; +import com.example.qa.service.domain.Qa; + +public interface UpdateQaUseCase { + Qa updateQa(UpdateQaCommand command); +} diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/CreateQaService.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/CreateQaService.java new file mode 100644 index 0000000000000000000000000000000000000000..9a3c7ac1789677d595f84df083f2eef5c6598b6b --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/CreateQaService.java @@ -0,0 +1,32 @@ +package com.example.qa.service.application.service; + +import com.example.qa.service.application.command.CreateQaCommand; +import com.example.qa.service.application.port.in.CreateQaUseCase; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.port.CreateQaPort; +import com.example.qa.service.domain.valueobject.Answer; +import com.example.qa.service.domain.valueobject.Question; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/* + author: liyujie + */ + +@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 Answer(createQaCommand.answer()) + ); + log.info("qa:{}",qa); + return createqaPort.createQa(qa); + } +} diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/DeleteQaService.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/DeleteQaService.java new file mode 100644 index 0000000000000000000000000000000000000000..0d2937b5a3c767e20ae378e44dc5b09f089eab68 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/DeleteQaService.java @@ -0,0 +1,21 @@ +package com.example.qa.service.application.service; + +import com.example.qa.service.application.port.in.DeleteQaUseCase; +import com.example.qa.service.domain.port.DeleteQaPort; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +/** + * @author liaoqi + * @param + * @return + */ +@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/com/example/qa/service/application/service/GetQaByIdService.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/GetQaByIdService.java new file mode 100644 index 0000000000000000000000000000000000000000..5e4ea05f3e3716e30a4de2904db1f5fc9b00dac7 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/GetQaByIdService.java @@ -0,0 +1,22 @@ +package com.example.qa.service.application.service; + +/* + author: ZhangQianyu + 2210705220 + */ + +import com.example.qa.service.application.port.in.GetQaByIdUseCase; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.port.GetQaByIdPort; +import jakarta.annotation.Resource; +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/com/example/qa/service/application/service/GetQaListService.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/GetQaListService.java new file mode 100644 index 0000000000000000000000000000000000000000..5acc87b65bd505bf7372414c5a039f4fd38509f7 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/GetQaListService.java @@ -0,0 +1,25 @@ +package com.example.qa.service.application.service; + +/* + author: ZhangQianyu + 2210705220 + */ + +import com.example.qa.service.application.port.in.GetQaListUseCase; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.port.GetQaListPort; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class GetQaListService implements GetQaListUseCase { + + @Resource + GetQaListPort getQaListPort; + @Override + public List getQas() { + List qas = Qa.getQas(getQaListPort); + return qas; + } +} diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/UpdateQaService.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/UpdateQaService.java new file mode 100644 index 0000000000000000000000000000000000000000..3a40bb63ec8ef3b41875c5b3bd25f040e758bcbe --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/UpdateQaService.java @@ -0,0 +1,28 @@ +package com.example.qa.service.application.service; +/** + * @author zhujunjie + * @param + * @return + */ +import com.example.qa.service.application.command.UpdateQaCommand; +import com.example.qa.service.application.port.in.UpdateQaUseCase; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.port.UpdateQaPort; +import com.example.qa.service.domain.valueobject.*; +import jakarta.annotation.Resource; +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..09a0db1cf41ed077195430bfa1a9dfd8e105b8fb --- /dev/null +++ b/qa-service/qa-service-bootstrap/pom.xml @@ -0,0 +1,95 @@ + + + 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 + + + org.springframework.boot + spring-boot-starter-amqp + + + 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.mybatis + mybatis-spring + 3.0.5 + compile + + + + + + + + + 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/com/example/qa/service/bootstrap/QaServiceBootstrapApplication.java b/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/QaServiceBootstrapApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..c7c67596cc4d4a6cbe370bf35104b1310f2d1e87 --- /dev/null +++ b/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/QaServiceBootstrapApplication.java @@ -0,0 +1,15 @@ +package com.example.qa.service.bootstrap; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = "com.example.qa") +@MapperScan("com.example.qa.adapter.out.persistence.mapper") +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..1e3baf8a8f9504f0f19560239a4c3d06f4e7dfe3 --- /dev/null +++ b/qa-service/qa-service-bootstrap/src/main/resources/application.properties @@ -0,0 +1,27 @@ +server.port=28081 +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 +spring.rabbitmq.addresses=192.168.168.128 +spring.rabbitmq.username=guest +spring.rabbitmq.password=guest +spring.amqp.deserialization.trust.all=true + +moonshot.url=https://api.moonshot.cn/v1/chat/completions +moonshot.key=sk-BMEWhXEYHR0sLrixvuIzv25dRSMHD1E5X21UugQk0Tstc6qs +moonshot.model=kimi-k2-0905-preview \ No newline at end of file diff --git a/qa-service/qa-service-common/.gitignore b/qa-service/qa-service-common/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..549e00a2a96fa9d7c5dbc9859664a78d980158c2 --- /dev/null +++ b/qa-service/qa-service-common/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/qa-service/qa-service-common/pom.xml b/qa-service/qa-service-common/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..d214d8ebe263591dd4c29bf25469c3229558e8b9 --- /dev/null +++ b/qa-service/qa-service-common/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + com.example + qa-service-common + 0.0.1-SNAPSHOT + qa-service-common + qa-service-common + + 21 + UTF-8 + UTF-8 + 3.0.2 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + 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 + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + com.example.qr.service.common.QrServiceCommonApplication + true + + + + repackage + + repackage + + + + + + + + diff --git a/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/IdWorker.java b/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/IdWorker.java new file mode 100644 index 0000000000000000000000000000000000000000..ac96e23534a47e45c5d6fb06dbe2a4b3838df792 --- /dev/null +++ b/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/IdWorker.java @@ -0,0 +1,199 @@ +package com.example.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/.gitignore b/qa-service/qa-service-domain/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..549e00a2a96fa9d7c5dbc9859664a78d980158c2 --- /dev/null +++ b/qa-service/qa-service-domain/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/qa-service/qa-service-domain/pom.xml b/qa-service/qa-service-domain/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..bcd25fed0a7d7bfc349711c9e772a865297c7a1c --- /dev/null +++ b/qa-service/qa-service-domain/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + 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 + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + provided + + + + com.example + qa-service-common + 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-domain/src/main/java/com/example/qa/service/domain/Qa.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/Qa.java new file mode 100644 index 0000000000000000000000000000000000000000..bddc8f86a14494372d4b95d88a3453d5f74407c9 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/Qa.java @@ -0,0 +1,49 @@ +package com.example.qa.service.domain; + +import com.example.qa.service.common.IdWorker; +import com.example.qa.service.domain.port.GetQaListPort; +import com.example.qa.service.domain.valueobject.Answer; +import com.example.qa.service.domain.valueobject.Question; +import com.example.qa.service.domain.valueobject.QaId; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Setter +@Getter +@ToString + +/* + author: zhangyucheng + */ + +public class Qa { + private QaId id; + private Question question; + private Answer answer; + + public Qa() { + } + + public Qa(QaId id, Question question, Answer answer) { + this.id = id; + this.question = question; + this.answer = answer; + } + + public Qa( Question question, Answer answer) { + this.id= genId() ; + this.question = question; + this.answer = answer; + } + + public static List getQas(GetQaListPort getQaListPort){ + return getQaListPort.getQas(); + } + + public QaId genId(){ + return new QaId(new IdWorker().nextId()); + } +} diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/CreateQaPort.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/CreateQaPort.java new file mode 100644 index 0000000000000000000000000000000000000000..5812a352040fff637e11de4d146473d5d0fc42d7 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/CreateQaPort.java @@ -0,0 +1,10 @@ +package com.example.qa.service.domain.port; + +import com.example.qa.service.domain.Qa; + +/* + author: zhangyucheng + */ +public interface CreateQaPort { + Qa createQa(Qa qa); +} diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/DeleteQaPort.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/DeleteQaPort.java new file mode 100644 index 0000000000000000000000000000000000000000..16549fb821772e3f5d360042504bbdf86571208a --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/DeleteQaPort.java @@ -0,0 +1,8 @@ +package com.example.qa.service.domain.port; + +/* + author: zhangyucheng + */ +public interface DeleteQaPort { + void deleteQa(Long id); +} diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQaByIdPort.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQaByIdPort.java new file mode 100644 index 0000000000000000000000000000000000000000..b985a2b7b21977022313ee79b0978f70b5fefc21 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQaByIdPort.java @@ -0,0 +1,10 @@ +package com.example.qa.service.domain.port; + +import com.example.qa.service.domain.Qa; + +/* + author: zhangyucheng + */ +public interface GetQaByIdPort { + Qa getQaById(Long id); +} diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQaListPort.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQaListPort.java new file mode 100644 index 0000000000000000000000000000000000000000..b93c6f667a45e1b5716ae57d290195a7842744da --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQaListPort.java @@ -0,0 +1,12 @@ +package com.example.qa.service.domain.port; + +import com.example.qa.service.domain.Qa; + +import java.util.List; + +/* + author: zhangyucheng + */ +public interface GetQaListPort { + List getQas(); +} diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/UpdateQaPort.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/UpdateQaPort.java new file mode 100644 index 0000000000000000000000000000000000000000..bbd8c4402bc3a6240740d97bd0e38961f098d544 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/UpdateQaPort.java @@ -0,0 +1,10 @@ +package com.example.qa.service.domain.port; + +import com.example.qa.service.domain.Qa; + +/* + author: zhangyucheng + */ +public interface UpdateQaPort { + Qa updateQa(Qa qa); +} diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/Answer.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/Answer.java new file mode 100644 index 0000000000000000000000000000000000000000..2cf248805c58d7e6ecf153671f345ae1259e6964 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/Answer.java @@ -0,0 +1,10 @@ +package com.example.qa.service.domain.valueobject; + +/* + author: zhangyucheng + */ +public record Answer(String answer) { + public String getValue() { + return answer; + } +} diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QaId.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QaId.java new file mode 100644 index 0000000000000000000000000000000000000000..75f02deaee5999fd454850404c58a11baddfad09 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QaId.java @@ -0,0 +1,12 @@ +package com.example.qa.service.domain.valueobject; + +/* + author: zhangyucheng + */ +public record QaId(long id) { + + public long getValue(){ + return id; + } + +} diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/Question.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/Question.java new file mode 100644 index 0000000000000000000000000000000000000000..5c1b0a47cfcaf12ca40148d2ea8678c15d6d4da6 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/Question.java @@ -0,0 +1,10 @@ +package com.example.qa.service.domain.valueobject; + +/* + author: zhangyucheng + */ +public record Question(String question) { + public String getValue() { + return question; + } +} diff --git a/user-service/pom.xml b/user-service/pom.xml index c69afc83c25b3353caf5990a6177cba309dcb683..a000efeef31d69c94b24f509aadff11fe7dea434 100644 --- a/user-service/pom.xml +++ b/user-service/pom.xml @@ -56,7 +56,11 @@ pom import - + + org.springframework.security + spring-security-test + test + com.baomidou mybatis-plus-spring-boot3-starter diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/pom.xml b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/pom.xml index b673beeab279b6a48e1682bc66bcab53fa002d75..1cc92b48260c8e5e678b133424ed49bab46cb386 100644 --- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/pom.xml +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/pom.xml @@ -25,6 +25,10 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-security + org.projectlombok lombok @@ -46,6 +50,12 @@ knife4j-openapi3-jakarta-spring-boot-starter ${knife4j.version} + + com.example + user-adapter-out-persistence + 0.0.1-SNAPSHOT + compile + diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/BasicSecurityConfig.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/BasicSecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..74d73854df25a39f29cc3d5bb47bea54d72cd8e4 --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/BasicSecurityConfig.java @@ -0,0 +1,175 @@ +package com.example.user.adapter.in.web.config; + + +import com.example.user.adapter.in.web.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; + + /** + * 自定义用户详情服务 + * 用于加载用户信息进行认证 + */ + + + + /** + * 认证管理器 + * 负责处理用户认证请求 + * + * @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("/user/login", "/user/register").permitAll() + .requestMatchers("/user/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) + + + // 配置异常处理 + .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-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 index 96820c00d7cf18e1f698ddc65b3e279601b5d576..12f566812b37c58ed2c70491884825183afcbf5b 100644 --- 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 @@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.*; import java.util.List; @Slf4j -@RequestMapping("/users") +@RequestMapping("/user") @RestController @RequiredArgsConstructor public class UserController { @@ -48,25 +48,16 @@ public class UserController { return getUserListUseCase.getUsers(); } - /** - * 创建新用户 - * 功能:接收用户注册信息,验证密码一致性,创建新用户账户 - * @author dongxuanfeng - * @param createUserRequestDTO - * @return User - 成功创建的新用户 - * @throws IllegalArgumentException 当密码与确认密码不匹配时抛出此异常 - */ - @PostMapping() + @PostMapping("register") public User createUser(@RequestBody CreateUserRequestDTO createUserRequestDTO){ - if (!createUserRequestDTO.isPasswordValid()) { - throw new IllegalArgumentException("密码和确认密码不匹配"); - } CreateUserCommand command=CreateUserCommand.builder() - .name(createUserRequestDTO.name()) - .age(createUserRequestDTO.age()) + .username(createUserRequestDTO.name()) .email(createUserRequestDTO.email()) + .phone(createUserRequestDTO.phone()) + .realname(createUserRequestDTO.realname()) .password(createUserRequestDTO.password()) + .repassword(createUserRequestDTO.repassword()) .build(); return createUserUseCase.createUser(command); @@ -79,31 +70,22 @@ public class UserController { return "success"; } - + /** + * @author liuxin + * @param updateUserRequestDTO + * @return + */ @PutMapping("") public User updateUser(@RequestBody UpdateUserRequestDTO updateUserRequestDTO){ UpdateUserCommand command=UpdateUserCommand.builder() .id(updateUserRequestDTO.id()) .name(updateUserRequestDTO.name()) - .age(updateUserRequestDTO.age()) .email(updateUserRequestDTO.email()) + .phone(updateUserRequestDTO.phone()) + .real_name(updateUserRequestDTO.real_name()) .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/CreateUserRequestDTO.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/CreateUserRequestDTO.java index 574d0e0d349d44bf914066a763d8f59390c32203..019367347dbe5e3bb0c5334ec2461a1da74b6cba 100644 --- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/CreateUserRequestDTO.java +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/CreateUserRequestDTO.java @@ -1,20 +1,19 @@ package com.example.user.adapter.in.web.dto; public record CreateUserRequestDTO( - String name, - Integer age, - String email, - String password, - String rePassword) { - // TODO: 密码校验 - + String name, + String email, + String phone, + String realname, + String password, + String repassword +) { /** * 验证密码与确认密码是否一致 * @author dongxuanfeng * @return boolean -验证结果:true表示密码与重复密码一致,false表示两次密码不一致 */ public boolean isPasswordValid() { - return password != null && password.equals(rePassword); + return password != null && password.equals(repassword); } - } 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/UpdateUserRequestDTO.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UpdateUserRequestDTO.java index d42ae558a29f0f8fd392aaeae649437aebd8e65e..6baa149d682764cc239a81203e105c9f6dd83ec6 100644 --- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UpdateUserRequestDTO.java +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UpdateUserRequestDTO.java @@ -2,6 +2,7 @@ package com.example.user.adapter.in.web.dto; public record UpdateUserRequestDTO(Long id, String name, - Integer age, - String email) { + String email, + String phone, + String real_name) { } 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-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/JwtAuthenticationFilter.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..055d347b6121ca0be1468ad467676d7862034743 --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/JwtAuthenticationFilter.java @@ -0,0 +1,351 @@ +package com.example.user.adapter.in.web.filter; + + +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-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/TokenBlacklistService.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/TokenBlacklistService.java new file mode 100644 index 0000000000000000000000000000000000000000..9e2152218107cc8a5a58ab3a8a483a0b9da6d1ce --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/TokenBlacklistService.java @@ -0,0 +1,203 @@ +package com.example.user.adapter.in.web.filter; + +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-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..eb6838cf650d3b2224bf95151caeb97c9775dc90 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 @@ -55,3 +55,4 @@ + 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..808eb015389c8c94615607a3812ee2fd5fd2cc6f 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 @@ -5,37 +5,33 @@ import com.example.user.service.domain.User; import com.example.user.service.domain.valueobject.*; public class UserConvertor { - /** - * 将持久化实体转换为领域对象 - * @author dongxuanfeng - * @param userEntity 数据库用户实体,包含用户的所有持久化数据 - * @return User 领域对象,包含用户的所有业务属性和行为 - */ - public static User toDomain(UserEntity userEntity) { - return new User( - new UserId(userEntity.getId()), - new UserName(userEntity.getName()), - new UserAge(userEntity.getAge()), - new Email(userEntity.getEmail()), - Password.fromEncrypted(userEntity.getPassword()), - new IsSuper(userEntity.getIsSuper() == 1) - ); - } - /** - * 将领域对象转换为持久化实体 - * @author dongxuanfeng - * @param user 用户领域对象,包含用户的所有业务属性和行为 - * @return UserEntity 数据库用户实体,包含用户的所有持久化数据 - */ - public static UserEntity toEntity(User user) { - return new UserEntity( - user.getId().id(), - user.getName().username(), - user.getAge().age(), - user.getEmail().email(), - user.getPassword().encryptedValue(), - user.getIsSuper().value() ? 1 : 0 - ); - } + public static User toDomain(UserEntity userEntity) { + return new User( + new UserId(userEntity.getId()), + new UserName(userEntity.getName()), + new Password(userEntity.getPassword()), + new Email(userEntity.getEmail()), + new Phone(userEntity.getPhone()), + new RealName(userEntity.getRealName()), + new UserStatus(userEntity.getStatus()), + new UserRole(userEntity.getRole()) + ); + } + + public static UserEntity toEntity(User user) { + return new UserEntity( + user.getId().getValue(), + user.getUsername().getValue(), + user.getPassword().encryptedValue(), + user.getEmail().getValue(), + user.getPhone().getValue(), + user.getRealName().getValue(), + user.getStatus().getValue(), + user.getRole().getValue(), + user.getCreateTime(), + user.getUpdateTime(), + user.getLastLoginTime() + ); + } } 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..d04f201ff2603a06db4d5f9a086a844a066e91af 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,6 +7,10 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + @Data @AllArgsConstructor @NoArgsConstructor @@ -15,13 +19,27 @@ public class UserEntity { @TableId(type= IdType.ASSIGN_ID) private long id; private String name; - private Integer age; - private String email; private String password; - private Integer isSuper; - 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 + private String email; + private String phone; + private String realName; + private int status; + private String role; + 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, String password, String email, + String phone, String realName, int status, String role) { + this.id = id; + this.name = name; + this.password = password; + this.email = email; + this.phone = phone; + this.realName = realName; + this.status = status; + this.role = role; } } diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/command/CreateUserCommand.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/command/CreateUserCommand.java index d7f351917f3750105968088e03b460e2b9ba8bcb..8aca7f970b1eeb7afda13ae63cc291f6ef747b16 100644 --- a/user-service/user-service-application/src/main/java/com/example/user/service/application/command/CreateUserCommand.java +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/command/CreateUserCommand.java @@ -5,9 +5,11 @@ import lombok.Builder; @Builder public record CreateUserCommand( Long id, - String name, - Integer age, + String username, String email, - String password + String phone, + String realname, + String password, + String repassword ) { } diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/command/UpdateUserCommand.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/command/UpdateUserCommand.java index 0ef0ed9076efe403013ff1e6c169c353387b6b30..8b1aa405b79e3b2fb54a85c2b55d1ba29e2e1524 100644 --- a/user-service/user-service-application/src/main/java/com/example/user/service/application/command/UpdateUserCommand.java +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/command/UpdateUserCommand.java @@ -5,6 +5,7 @@ import lombok.Builder; @Builder public record UpdateUserCommand(Long id, String name, - Integer age, - String email) { + String email, + String phone, + String real_name) { } diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/CreateUserService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/CreateUserService.java index 931828a4d32e7cae7a78cc0e55f92ade3a7ff2e5..913d1c894123d81151e144ecf548e746eac916ce 100644 --- a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/CreateUserService.java +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/CreateUserService.java @@ -4,10 +4,7 @@ import com.example.user.service.application.command.CreateUserCommand; import com.example.user.service.application.port.in.CreateUserUseCase; import com.example.user.service.domain.User; import com.example.user.service.domain.port.CreateUserPort; -import com.example.user.service.domain.valueobject.Email; -import com.example.user.service.domain.valueobject.Password; -import com.example.user.service.domain.valueobject.UserAge; -import com.example.user.service.domain.valueobject.UserName; +import com.example.user.service.domain.valueobject.*; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -17,17 +14,19 @@ import org.springframework.stereotype.Service; public class CreateUserService implements CreateUserUseCase { @Resource private CreateUserPort createUserPort; + @Override public User createUser(CreateUserCommand createUserCommand) { - //command -> domain - User user=new User( - new UserName(createUserCommand.name()), - new UserAge(createUserCommand.age()), + // 创建用户领域对象 + User user = User.createUser( + new UserName(createUserCommand.username()), + Password.fromRaw(createUserCommand.password()), new Email(createUserCommand.email()), -// new Password( createUserCommand.password()) - Password.fromRaw(createUserCommand.password()) + new Phone(createUserCommand.phone()), + new RealName(createUserCommand.realname()), + UserRole.user() // 默认用户角色 ); - log.info("user:{}",user); + log.info("Creating user: {}", user); return createUserPort.createUser(user); } } diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UpdateUserService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UpdateUserService.java index 97da325352c39b77ddfdc0f9982a3f654a201c91..3ece0ce73907990e559b7f253d883e4b1901c823 100644 --- a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UpdateUserService.java +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UpdateUserService.java @@ -3,11 +3,10 @@ package com.example.user.service.application.service; import com.example.user.service.application.command.UpdateUserCommand; import com.example.user.service.application.port.in.UpdateUserUseCase; import com.example.user.service.domain.User; +import com.example.user.service.domain.port.GetUserByIdPort; import com.example.user.service.domain.port.UpdateUserPort; -import com.example.user.service.domain.valueobject.Email; -import com.example.user.service.domain.valueobject.UserAge; -import com.example.user.service.domain.valueobject.UserId; -import com.example.user.service.domain.valueobject.UserName; +import com.example.user.service.domain.valueobject.*; + import jakarta.annotation.Resource; import org.springframework.stereotype.Service; @@ -15,14 +14,19 @@ import org.springframework.stereotype.Service; public class UpdateUserService implements UpdateUserUseCase { @Resource private UpdateUserPort updateUserPort; + @Resource + private GetUserByIdPort getUserByIdPort; @Override public User updateUser(UpdateUserCommand command) { - User user = new User( - new UserId(command.id()), - new UserName(command.name()), - new UserAge(command.age()), - new Email(command.email())); + User user = getUserByIdPort.getUserById(command.id()); + + user.updateProfile( + new Email(command.email()), + new Phone(command.phone()), + new RealName(command.real_name()) + ); + return updateUserPort.updateUser(user); } } 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..f0c7ec444490059a4f91af0f69ec9b672788e25c 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 @@ -7,6 +7,7 @@ import com.example.user.service.domain.User; import com.example.user.service.domain.port.GetUserByNamePort; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Slf4j @@ -15,6 +16,8 @@ public class UserLoginService implements UserLoginUseCase { @Resource private GetUserByNamePort getUserByNamePort; + @Autowired + private JwtUtil jwtUtil; @Override public String login(UserLoginCommand userLoginCommand) { @@ -28,13 +31,6 @@ public class UserLoginService implements UserLoginUseCase { if(!user.validatePassword(userLoginCommand.password())){ throw new RuntimeException("密码错误"); } - // 签发token - String token = JwtUtil.generateToken( - user.getId().id(), - user.getName().username(), - user.getIsSuper().value() - ); - log.info("生成的JWT令牌: {}", token); - return token; + return jwtUtil.generateToken(userLoginCommand.name()); } } diff --git a/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/UserServiceBootstrapApplication.java b/user-service/user-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/UserServiceBootstrapApplication.java similarity index 100% rename from user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/UserServiceBootstrapApplication.java rename to user-service/user-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/UserServiceBootstrapApplication.java diff --git a/user-service/user-service-bootstrap/src/main/resources/application.properties b/user-service/user-service-bootstrap/src/main/resources/application.properties index 3ffae5ba70d1950bb4d5530c8b2e561a15ee9e5e..6ed57fa4c993d15ea90c456e5f44ec9289f323d7 100644 --- a/user-service/user-service-bootstrap/src/main/resources/application.properties +++ b/user-service/user-service-bootstrap/src/main/resources/application.properties @@ -3,7 +3,6 @@ server.port=28080 spring.application.name=user-service - # Nacos认证信息 spring.cloud.nacos.discovery.username=nacos spring.cloud.nacos.discovery.password=nacos @@ -23,3 +22,6 @@ spring.cloud.nacos.config.server-addr=192.168.168.128:8848 # spring.cloud.nacos.config.namespace= spring.config.import=nacos:${spring.application.name}.properties?refresh=true +springdoc.api-docs.enabled=true +springdoc.swagger-ui.enabled=true +springdoc.swagger-ui.path=/swagger-ui.html \ 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..4d8b80a0971315f39ee8d30cb7e2ca6d3546608e 100644 --- a/user-service/user-service-common/pom.xml +++ b/user-service/user-service-common/pom.xml @@ -18,32 +18,6 @@ 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 @@ -54,6 +28,23 @@ lombok provided + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + 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..0229663a12c265c0c36c77dab8eef8cf09b9b792 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,345 @@ 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工具类 - JSON Web Token 核心处理工具 + * + * 【什么是JWT?】 + * JWT (JSON Web Token) 是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。 + * 它是一种无状态的认证方式,服务器不需要保存用户的登录状态。 + * + * 【JWT的结构】 + * JWT由三部分组成,用点(.)分隔: + * 1. Header(头部):包含token类型和签名算法,如 {"typ":"JWT","alg":"HS512"} + * 2. Payload(载荷):包含声明信息,如用户名、过期时间等,如 {"sub":"user123","exp":1234567890} + * 3. Signature(签名):用于验证token的完整性,防止篡改 + * 格式:Header.Payload.Signature + * 例如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + * + * 【为什么使用JWT?】 + * 1. 无状态:服务器不需要存储session,适合分布式系统 + * 2. 跨域:可以在不同域名间传递认证信息 + * 3. 移动友好:适合移动应用的认证方式 + * 4. 性能好:避免了频繁的数据库查询 + * + * 【安全考虑】 + * 1. 密钥安全:签名密钥必须保密,泄露会导致token可被伪造 + * 2. HTTPS传输:token应通过HTTPS传输,防止被截获 + * 3. 过期时间:设置合理的过期时间,平衡安全性和用户体验 + * 4. 敏感信息:不要在payload中存储敏感信息,因为它只是Base64编码,不是加密 + * + * 【注解说明】 + * @Slf4j:Lombok注解,自动生成日志对象log,用于记录调试和错误信息 + * @Component:Spring注解,将该类注册为Spring容器管理的Bean,可以被其他类注入使用 + */ @Slf4j +@Component public class JwtUtil { + /** + * JWT签名密钥 - 用于保证token安全性的核心要素 + * + * 【作用说明】 + * 这个密钥用于对JWT进行签名和验证,确保token的完整性和真实性。 + * 任何人如果没有这个密钥,就无法伪造有效的JWT token。 + * + * 【配置方式】 + * @Value注解从application.properties或application.yml中读取jwt.secret配置 + * 如果配置文件中没有设置,则使用冒号后面的默认值 + * + * 【安全要求】 + * 1. 密钥长度:至少256位(32字节)才能满足HS256算法要求 + * 2. 密钥复杂度:应包含大小写字母、数字,避免使用简单密码 + * 3. 密钥保密:绝对不能泄露给客户端或第三方 + * 4. 生产环境:必须使用环境变量或加密配置文件存储 + * + * 【为什么这样设计?】 + * 使用@Value注解可以灵活配置密钥,不同环境(开发、测试、生产)可以使用不同的密钥 + */ + @Value("${jwt.secret:mySecretKeyForJwtTokenGenerationAndValidation123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789}") + private String secret; - private static final String SECRET_KEY = "123456"; - - - private static final long EXPIRATION_TIME = 5 * 60 * 1000; // 5分钟 - + /** + * JWT token过期时间(毫秒) - 控制token有效期的关键参数 + * + * 【时间设置说明】 + * 默认值86400000毫秒 = 24小时 = 1天 + * 计算方式:24小时 × 60分钟 × 60秒 × 1000毫秒 = 86400000毫秒 + * + * 【为什么需要过期时间?】 + * 1. 安全考虑:限制token的有效期,即使被盗用也会自动失效 + * 2. 减少风险:缩短攻击者可利用stolen token的时间窗口 + * 3. 强制重新认证:定期要求用户重新登录,提高安全性 + * + * 【时间设置策略】 + * - 高安全应用:1-2小时 + * - 一般应用:24小时 + * - 低风险应用:7天 + * - 移动应用:可以设置更长时间,配合refresh token使用 + * + * 【配置灵活性】 + * 通过@Value注解,可以在不同环境中设置不同的过期时间 + */ + @Value("${jwt.expiration:86400000}") + private Long expiration; + /** + * 生成JWT token - 用户登录成功后创建身份凭证 + * + * 【方法作用】 + * 当用户登录成功后,调用此方法生成一个JWT token作为用户的身份凭证。 + * 用户后续的请求都需要携带这个token来证明自己的身份。 + * + * 【生成流程】 + * 1. 获取当前时间作为token的签发时间 + * 2. 计算token的过期时间(当前时间 + 配置的有效期) + * 3. 使用JJWT库的Builder模式构建token + * 4. 设置token的各个声明(Claims) + * 5. 使用密钥和算法对token进行签名 + * 6. 生成最终的token字符串 + * + * 【参数说明】 + * @param username 用户名,将作为token的主体(subject),用于标识token属于哪个用户 + * + * 【返回值说明】 + * @return String 生成的JWT token,格式为:Header.Payload.Signature + * + * 【使用场景】 + * - 用户登录成功后 + * - 需要为用户创建新的身份凭证时 + * - token刷新时(如果实现了refresh token机制) + */ + public String generateToken(String username) { + // 获取当前时间 - 作为token的签发时间(iat - issued at) + Date now = new Date(); + // 计算过期时间 - 当前时间加上配置的有效期 + // 这样设计可以灵活控制token的生命周期 + Date expiryDate = new Date(now.getTime() + 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); - + // 使用JJWT库的Builder模式构建JWT Token + // Builder模式的优点:代码清晰、易于理解、支持链式调用 return Jwts.builder() - .setClaims(claims) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) - .signWith(SignatureAlgorithm.HS256, SECRET_KEY) - .compact(); + .setSubject(username) // 设置主体(sub):标识token的所有者 + .setIssuedAt(now) // 设置签发时间(iat):token的创建时间 + .setExpiration(expiryDate) // 设置过期时间(exp):token的失效时间 + .signWith(getSigningKey(), SignatureAlgorithm.HS512) // 使用HS512算法和密钥进行签名 + .compact(); // 生成紧凑的URL安全字符串格式 } - - public static Claims parseToken(String token) { - return Jwts.parser() - .setSigningKey(SECRET_KEY) - .parseClaimsJws(token) - .getBody(); + /** + * 从token中获取用户名 - 解析JWT获取用户身份信息 + * + * 【方法作用】 + * 当收到客户端发送的JWT token时,需要从中提取用户名来识别用户身份。 + * 这是JWT认证流程中的关键步骤。 + * + * 【解析过程】 + * 1. 调用getClaimsFromToken()方法解析token + * 2. 从Claims中获取subject字段(用户名) + * 3. 返回用户名供后续业务逻辑使用 + * + * 【为什么这样设计?】 + * JWT的payload部分包含了用户信息,通过解析可以直接获取, + * 无需查询数据库,提高了性能。 + * + * @param token JWT token字符串 + * @return String 用户名,如果token格式错误或解析失败则可能抛出异常 + * + * 【注意事项】 + * - 此方法不验证token是否过期,只负责提取用户名 + * - 如果token被篡改,解析时会抛出签名验证异常 + */ + public String getUsernameFromToken(String token) { + // 解析token获取载荷(Claims) - Claims包含了token中的所有声明信息 + Claims claims = getClaimsFromToken(token); + // 从载荷中获取主体(subject),即用户名 + // subject是JWT标准中用于标识token所有者的字段 + return claims.getSubject(); } - - public static Long getUserIdFromToken(String token) { - Claims claims = parseToken(token); - return claims.get("id", Long.class); + /** + * 验证token是否有效 - JWT认证的核心验证逻辑 + * + * 【验证目的】 + * 确保接收到的JWT token是合法、有效且未被篡改的。 + * 这是保护API安全的重要防线。 + * + * 【验证步骤】 + * 1. 从token中提取用户名 + * 2. 比较token中的用户名与期望的用户名是否一致 + * 3. 检查token是否已过期 + * 4. 隐式验证:getClaimsFromToken()会验证签名 + * + * 【为什么需要双重验证?】 + * - 用户名匹配:确保token属于正确的用户 + * - 过期检查:确保token仍在有效期内 + * - 签名验证:确保token未被篡改(在解析过程中自动完成) + * + * @param token JWT token字符串 + * @param username 期望的用户名,通常来自请求上下文 + * @return boolean true表示token有效,false表示无效 + * + * 【异常处理】 + * 使用try-catch捕获所有可能的异常(签名错误、格式错误等), + * 确保方法不会因异常而中断,统一返回false表示验证失败。 + */ + public boolean validateToken(String token, String username) { + try { + // 从token中获取用户名 - 这一步会验证token的签名 + String tokenUsername = getUsernameFromToken(token); + // 验证用户名是否匹配且token未过期 + // 两个条件都必须满足才认为token有效 + return (username.equals(tokenUsername) && !isTokenExpired(token)); + } catch (Exception e) { + // 捕获解析token时的异常(如签名错误、格式错误等) + // 记录错误日志便于调试,但不暴露具体错误信息给客户端 + 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 boolean token是否过期 + */ + public boolean isTokenExpired(String token) { + // 获取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 Date 过期时间 + */ + public Date getExpirationDateFromToken(String token) { + // 解析token获取载荷(Claims) + Claims claims = getClaimsFromToken(token); + // 从载荷中获取过期时间 + return claims.getExpiration(); } + /** + * 从token中解析Claims - JWT解析的核心方法 + * + * 【Claims概念】 + * Claims是JWT的载荷(Payload)部分,包含了关于用户和token的声明信息。 + * 标准声明包括:sub(主体)、exp(过期时间)、iat(签发时间)等。 + * + * 【解析流程】 + * 1. 创建JWT解析器(Parser) + * 2. 设置签名密钥用于验证token完整性 + * 3. 解析token字符串,验证签名 + * 4. 提取并返回Claims对象 + * + * 【安全机制】 + * 解析过程中会自动验证token的签名,如果token被篡改, + * 签名验证会失败并抛出异常,确保了token的安全性。 + * + * 【为什么设计为private?】 + * 这是一个内部工具方法,只供本类的其他方法使用, + * 不需要暴露给外部调用者,符合封装原则。 + * + * @param token JWT token字符串 + * @return Claims 载荷对象,包含token中的所有声明信息 + * @throws JwtException 如果token格式错误、签名无效或已过期 + */ + private Claims getClaimsFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) // 设置签名密钥用于验证token完整性 + .build() // 构建解析器 + .parseClaimsJws(token) // 解析JWT token并验证签名 + .getBody(); // 获取载荷部分(Claims) + } - public static boolean validateToken(String token) { - try { - parseToken(token); - return true; - } catch (Exception e) { - log.error("JWT令牌验证失败: {}", e.getMessage()); - return false; - } + /** + * 获取签名密钥 - 将配置的字符串密钥转换为加密算法所需的密钥对象 + * + * 【转换目的】 + * JJWT库需要SecretKey对象来进行HMAC签名算法, + * 而我们配置的是字符串,需要转换为合适的密钥格式。 + * + * 【HMAC算法说明】 + * HMAC (Hash-based Message Authentication Code) 是一种基于哈希的消息认证码算法。 + * 它结合了哈希函数和密钥,既能验证数据完整性,又能验证数据来源。 + * + * 【转换过程】 + * 1. 将字符串密钥转换为字节数组 + * 2. 使用Keys.hmacShaKeyFor()方法生成适合HMAC-SHA算法的密钥 + * 3. 返回SecretKey对象供签名和验证使用 + * + * 【安全考虑】 + * - 密钥长度必须足够(至少256位) + * - 密钥应该是随机生成的,不能是简单的字符串 + * - 密钥必须保密,不能泄露给客户端 + * + * @return SecretKey 适用于HMAC-SHA算法的签名密钥 + */ + private SecretKey getSigningKey() { + // 将字符串密钥转换为字节数组 + // 使用UTF-8编码确保字符串到字节的转换一致性 + byte[] keyBytes = secret.getBytes(); + // 使用JJWT提供的工具方法生成HMAC-SHA密钥 + // 该方法会自动选择合适的HMAC算法(HS256、HS384、HS512) + return Keys.hmacShaKeyFor(keyBytes); } - public static boolean isTokenExpired(String token) { - Claims claims = parseToken(token); - return claims.getExpiration().before(new Date()); + /** + * 从请求头中提取token - 解析HTTP Authorization头获取JWT token + * + * 【HTTP Authorization头格式】 + * 根据RFC 6750 (OAuth 2.0 Bearer Token Usage)标准, + * JWT token应该放在HTTP请求头的Authorization字段中, + * 格式为: "Authorization: Bearer " + * + * 【为什么使用Bearer?】 + * Bearer是OAuth 2.0标准中定义的token类型,表示"持有者token", + * 意思是任何持有该token的人都可以使用它,无需额外的身份验证。 + * + * 【提取逻辑】 + * 1. 检查Authorization头是否存在 + * 2. 验证是否以"Bearer "开头(注意Bearer后有一个空格) + * 3. 提取"Bearer "后面的token部分 + * 4. 如果格式不正确,返回null + * + * 【使用场景】 + * 主要在JWT认证过滤器中使用,用于从HTTP请求中提取token, + * 然后进行后续的验证和用户身份识别。 + * + * 【安全考虑】 + * - 只接受标准的Bearer格式,拒绝其他格式 + * - 返回null而不是抛出异常,便于调用者处理 + * - 不对token内容进行验证,只负责提取 + * + * @param authHeader HTTP请求中的Authorization头的值 + * @return String JWT token字符串,如果格式不正确则返回null + * + * 【示例】 + * 输入: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * 输出: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + */ + public String extractTokenFromHeader(String authHeader) { + // 检查Authorization头是否存在且以"Bearer "开头 + // 注意:"Bearer "后面有一个空格,这是标准格式要求 + if (authHeader != null && authHeader.startsWith("Bearer ")) { + // 提取Bearer后面的token部分 + // substring(7)是因为"Bearer "有7个字符(包括空格) + return authHeader.substring(7); + } + // 如果格式不正确,返回null,让调用者知道没有有效的token + return null; } } \ No newline at end of file diff --git a/user-service/user-service-common/src/main/java/com/example/user/service/common/config/JwtConfig.java b/user-service/user-service-common/src/main/java/com/example/user/service/common/config/JwtConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..5abf081c54efdcdabf40648e8d50572cc94d5489 --- /dev/null +++ b/user-service/user-service-common/src/main/java/com/example/user/service/common/config/JwtConfig.java @@ -0,0 +1,29 @@ +// JwtConfig.java - 手动实现getter/setter +package com.example.user.service.common.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "jwt") +public class JwtConfig { + private String secret; + private long expiration; + + // 手动实现getter和setter + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + public long getExpiration() { + return expiration; + } + + public void setExpiration(long expiration) { + this.expiration = expiration; + } +} \ 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..79ea885fe6ca9c480438c70717b4488f4dda6983 100644 --- a/user-service/user-service-domain/pom.xml +++ b/user-service/user-service-domain/pom.xml @@ -40,6 +40,12 @@ org.springframework.security spring-security-crypto + + org.jetbrains + annotations + 13.0 + compile + 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..959897e7551b8acb7943e5129a08aa13eff9afd5 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 @@ -9,6 +9,7 @@ import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; +import java.time.LocalDateTime; import java.util.List; @Slf4j @@ -17,69 +18,133 @@ import java.util.List; @ToString public class User { private UserId id; - private UserName name; - private UserAge age; + private UserName username; private Email email; private Password password; - private IsSuper isSuper; // 添加isSuper字段 + private Phone phone; + private RealName realName; + private UserStatus status; + private UserRole role; + private LocalDateTime createTime; + private LocalDateTime updateTime; + private LocalDateTime lastLoginTime; + + // 私有默认构造方法,用于框架反射 public User() { } - public User(UserId id, UserName name, UserAge age, Email email, Password password,IsSuper isSuper) { + // 静态工厂方法 - 用于从持久化实体重建领域对象 + public User(UserId id, UserName username, Password password, Email email, + Phone phone, RealName realName, UserStatus status, UserRole role) { this.id = id; - this.name = name; - this.age = age; - this.email = email; + this.username = username; this.password = password; - this.isSuper = isSuper; + this.email = email; + this.phone = phone; + this.realName = realName; + this.status = status; + this.role = role; } - public User( UserName name, UserAge age, Email email, Password password) { - this.id= genId() ; - this.name = name; - this.age = age; - this.email = email; - this.password = password; - this.isSuper = new IsSuper(false); + // 静态工厂方法 - 用于创建新用户(注册时使用) + public static User createUser(UserName userName, Password password, Email email, + Phone phone, RealName realName, UserRole role) { + User user = new User(); + user.id = user.genId(); + user.username = userName; + user.password = password; + user.email = email; + user.phone = phone; + user.realName = realName; + user.status = UserStatus.ENABLED; // 默认激活状态 + user.role = role != null ? role : UserRole.USER; // 默认用户角色 + user.createTime = LocalDateTime.now().withNano(0); + user.updateTime = LocalDateTime.now().withNano(0); + user.lastLoginTime = LocalDateTime.now().withNano(0); + return user; } - public User(UserId userId, UserName userName, UserAge userAge, Email email) { - this.id = id; - this.name = name; - this.age = age; + // 静态工厂方法 - 用于从数据库重建用户(查询时使用) + public static User reconstruct(UserId id, UserName username, Email email, Password password, + Phone phone, RealName realName, UserStatus status, UserRole role, + LocalDateTime createTime, LocalDateTime updateTime, LocalDateTime lastLoginTime) { + User user = new User(); + user.id = id; + user.username = username; + user.email = email; + user.password = password; + user.phone = phone; + user.realName = realName; + user.status = status; + user.role = role; + user.createTime = createTime; + user.updateTime = updateTime; + user.lastLoginTime = lastLoginTime; + return user; + } + + // 业务方法 - 更新用户信息 + public void updateProfile(Email email, Phone phone, RealName realName) { this.email = email; - this.isSuper = new IsSuper(false); + this.phone = phone; + this.realName = realName; + this.updateTime = LocalDateTime.now(); // 自动更新修改时间 } + // 业务方法 - 更新密码 + public void changePassword(Password newPassword) { + this.password = newPassword; + this.updateTime = LocalDateTime.now(); // 自动更新修改时间 + } + + // 业务方法 - 用户登录 + public void login() { + this.lastLoginTime = LocalDateTime.now(); + this.updateTime = LocalDateTime.now(); // 更新最后修改时间 + } + + // 业务方法 - 激活/禁用用户 + public void activate() { + this.status = UserStatus.ENABLED; + this.updateTime = LocalDateTime.now(); + } - public static List getUsers(GetUserListPort getUserListPort){ + public void deactivate() { + this.status = UserStatus.ENABLED; + this.updateTime = LocalDateTime.now(); + } + + public static List getUsers(GetUserListPort getUserListPort) { return getUserListPort.getUsers(); } /** * 根据用户名查询用户 - * 当需要使用类似GetUserByNamePort这种对象的时候,需要在方法参数注入该对象 - * 因为通过构造方法或者字段注入都会失败,因为方法是静态方法,会早于对象创建,导致对象无法注入 - * @param name 用户名 - * @param getUserByNamePort 查询用户的端口 - * @return 用户模型 */ - public static User getUserByName(String name, GetUserByNamePort getUserByNamePort){ + public static User getUserByName(String name, GetUserByNamePort getUserByNamePort) { User user = getUserByNamePort.getUserByName(name); log.info("user:{}", user); return user; } - public UserId genId(){ + private UserId genId() { return new UserId(new IdWorker().nextId()); } /** * 验证密码 - * @param password 密码 明文还是密文? - * @return 验证结果 */ - public boolean validatePassword(String password){ - return this.password.verify( password); + public boolean validatePassword(String password) { + return this.password.verify(password); + } + + // 验证业务规则的方法 + public boolean isValid() { + return username != null && + password != null && + email != null && + status != null && + role != null && + createTime != null; } -} +} \ No newline at end of file diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Email.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Email.java index f83b24311157286872b13584c9780f3279a4f762..8ca701a34e1b35ac37c6097abd9b3b3ab5d98bdd 100644 --- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Email.java +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Email.java @@ -1,7 +1,24 @@ package com.example.user.service.domain.valueobject; -public record Email(String email) { - public String getValue() { - return email; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public record Email(String value) { + public static Email of(String email) { + if (email == null || email.isBlank()) { + throw new RuntimeException("邮箱不能为空"); + } + + if (!email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) { + throw new RuntimeException("邮箱格式不正确"); + } + + return new Email(email); + } + + public String getValue() {return value;} + + public boolean isValid() { + return value != null && value.matches("^[A-Za-z0-9+_.-]+@(.+)$"); } } 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/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..4634d88e7d506339eca370646202b8cb3ad98780 --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Phone.java @@ -0,0 +1,28 @@ +package com.example.user.service.domain.valueobject; + +import lombok.extern.slf4j.Slf4j; + +/** + * @Author: zhangyucheng + * @Date: 2025/9/16 14:15 + */ +@Slf4j +public record Phone(String value) { + public static Phone of(String phone) { + if (phone == null || phone.isBlank()) { + throw new RuntimeException("手机号不能为空"); + } + + if (!phone.matches("^1[3-9]\\d{9}$")) { + throw new RuntimeException("手机号格式不正确"); + } + + return new Phone(phone); + } + + public String getValue() {return value;} + + public boolean isValid() { + return value != null && value.matches("^1[3-9]\\d{9}$"); + } +} \ No newline at end of file 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..0b5884a3b063cd0905488b83c3af4fc9b6c1d846 --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/RealName.java @@ -0,0 +1,28 @@ +package com.example.user.service.domain.valueobject; + +import lombok.extern.slf4j.Slf4j; + +/** + * @Author: zhangyucheng + * @Date: 2025/9/16 14:15 + */ +@Slf4j +public record RealName(String value) { + public static RealName of(String name) { + if (name == null || name.isBlank()) { + throw new RuntimeException("真实姓名不能为空"); + } + + if (name.length() < 2 || name.length() > 20) { + throw new RuntimeException("真实姓名长度必须在2-20个字符之间"); + } + + return new RealName(name.trim()); + } + + public String getValue() {return value;} + + public boolean isValid() { + return value != null && value.length() >= 2 && value.length() <= 20; + } +} \ No newline at end of file diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserAge.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserAge.java deleted file mode 100644 index 38ef9370aa8f675e832d9cbebea3ef1ac069fd3f..0000000000000000000000000000000000000000 --- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserAge.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.user.service.domain.valueobject; - -public record UserAge(int age) { - public int getValue() { - return age; - } -} diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserName.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserName.java index 0c11a15528b836bbc7852e1f763f9233b3f30283..184129827d9f3094c37a0c62ba4f7478d751e3ca 100644 --- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserName.java +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserName.java @@ -1,7 +1,30 @@ package com.example.user.service.domain.valueobject; -public record UserName(String username) { - public String getValue() { - return username; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public record UserName(String value) { + public static UserName of(String username) { + if (username == null || username.isBlank()) { + throw new RuntimeException("用户名不能为空"); + } + + if (username.length() < 3 || username.length() > 20) { + throw new RuntimeException("用户名长度必须在3-20个字符之间"); + } + + if (!username.matches("^[a-zA-Z0-9_]+$")) { + throw new RuntimeException("用户名只能包含字母、数字和下划线"); + } + + return new UserName(username); + } + public String getValue() {return value;} + + public boolean isValid() { + return value != null && + value.length() >= 3 && + value.length() <= 20 && + value.matches("^[a-zA-Z0-9_]+$"); } -} +} \ No newline at end of file diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserRole.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserRole.java new file mode 100644 index 0000000000000000000000000000000000000000..620b6c7cca9d8eda0cb29a6ac02af578053e5b0e --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserRole.java @@ -0,0 +1,72 @@ +package com.example.user.service.domain.valueobject; + +import lombok.extern.slf4j.Slf4j; + +/** + * 用户角色值对象 + */ +@Slf4j +public record UserRole(String value) { + public static final UserRole ADMIN = new UserRole("ADMIN"); + public static final UserRole USER = new UserRole("USER"); + + public UserRole { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("用户角色不能为空"); + } + // 修改验证逻辑,直接使用字符串比较,不要引用静态字段 + if (!"ADMIN".equals(value) && !"USER".equals(value)) { + throw new IllegalArgumentException("用户角色必须是ADMIN或USER"); + } + } + + private static boolean isValidRole(String role) { + // 同样修改这里 + return "ADMIN".equals(role) || "USER".equals(role); + } + // 静态工厂方法 + public static UserRole of(String role) { + return new UserRole(role); + } + + public static UserRole admin() { + return ADMIN; + } + + public static UserRole user() { + return USER; + } + + public String getValue() { + return value; + } + + public boolean isAdmin() { + return ADMIN.value.equals(value); + } + + public boolean isUser() { + return USER.value.equals(value); + } + + public boolean isValid(String role) { + return ADMIN.value.equals(role) || USER.value.equals(role); + } + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + UserRole userRole = (UserRole) obj; + return value.equals(userRole.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} \ No newline at end of file diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserStatus.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..a893b6acf3b2577089b0e8d2731730e1850e2f27 --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserStatus.java @@ -0,0 +1,83 @@ +package com.example.user.service.domain.valueobject; + +import lombok.extern.slf4j.Slf4j; + +/** + * 用户状态值对象 + */ + +@Slf4j +public record UserStatus(Integer value) { + public static final UserStatus DISABLED = new UserStatus(0); + public static final UserStatus ENABLED = new UserStatus(1); + public static final UserStatus PENDING = new UserStatus(2); // 可选:添加待激活状态 + + public UserStatus { + if (value == null) { + throw new IllegalArgumentException("用户状态不能为空"); + } + // 直接使用数字字面量进行比较,不要引用静态字段 + if (value != 0 && value != 1 && value != 2) { + throw new IllegalArgumentException("用户状态必须是0(禁用)、1(启用)或2(待激活)"); + } + } + + // 静态工厂方法 + public static UserStatus of(Integer status) { + return new UserStatus(status); + } + + public static UserStatus disabled() { + return DISABLED; + } + + public static UserStatus enabled() { + return ENABLED; + } + + public static UserStatus pending() { + return PENDING; + } + + public boolean isEnabled() { + return Integer.valueOf(1).equals(value); + } + + public boolean isDisabled() { + return Integer.valueOf(0).equals(value); + } + + public boolean isPending() { + return Integer.valueOf(2).equals(value); + } + + private static boolean isValidStatus(Integer status) { + // 同样修改这里,使用字面量 + return Integer.valueOf(0).equals(status) || + Integer.valueOf(1).equals(status) || + Integer.valueOf(2).equals(status); + } + + // 转换方法:从数据库值创建 + public Integer getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + UserStatus that = (UserStatus) obj; + return value.equals(that.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} \ No newline at end of file