QAs = QA.getQAs(getQAListPort);
+ return QAs;
+ }
+}
diff --git a/qa-service/qa-service-application/src/main/java/com/example/application/service/UpdateQAService.java b/qa-service/qa-service-application/src/main/java/com/example/application/service/UpdateQAService.java
new file mode 100644
index 0000000000000000000000000000000000000000..d908f68854c34ca7f754bb93f0b71e72dabcd597
--- /dev/null
+++ b/qa-service/qa-service-application/src/main/java/com/example/application/service/UpdateQAService.java
@@ -0,0 +1,26 @@
+package com.example.application.service;
+
+import com.example.application.command.UpdateQACommand;
+import com.example.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;
+/**
+ * @author lizishan
+ */
+@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..465bd90c38c7a8b5568e47b5b4d4d546d04ff9e4
--- /dev/null
+++ b/qa-service/qa-service-bootstrap/pom.xml
@@ -0,0 +1,92 @@
+
+
+ 4.0.0
+ com.example
+ qa-service-bootstrap
+ 0.0.1-SNAPSHOT
+ qa-service-bootstrap
+ qa-service-bootstrap
+
+
+ com.example
+ qa-service
+ 0.0.1-SNAPSHOT
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.example
+ qa-adapter-in-web
+ 0.0.1-SNAPSHOT
+
+
+
+ com.example
+ qa-adapter-out-persistence
+ 0.0.1-SNAPSHOT
+
+
+
+ com.example
+ qa-adapter-out-external
+ 0.0.1-SNAPSHOT
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-config
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-discovery
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ 21
+ 21
+ UTF-8
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring-boot.version}
+
+ com.example.qa.service.bootstrap.QaServiceBootstrapApplication
+ false
+
+
+
+ repackage
+
+ repackage
+
+
+
+
+
+
+
+
diff --git a/qa-service/qa-service-bootstrap/src/main/java/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..d2fcc6a66104d1153c3794e555e0d96d1c2ab57b
--- /dev/null
+++ b/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/QaServiceBootstrapApplication.java
@@ -0,0 +1,26 @@
+package com.example.qa.service.bootstrap;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.amqp.rabbit.annotation.EnableRabbit;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * QA服务启动类
+ * 配置Spring Boot应用的启动入口,启用RabbitMQ和MyBatis支持
+ * @author yangwenkai
+ */
+@SpringBootApplication(scanBasePackages = "com.example")
+@MapperScan("com.example.qa.adapter.out.persistence.mapper")
+@EnableRabbit // 启用RabbitMQ支持
+public class QaServiceBootstrapApplication {
+
+ /**
+ * 应用程序主入口
+ * 启动Spring Boot应用
+ * @param args 命令行参数
+ */
+ 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..628f98507db41d712fc733265c3e238f601feb6f
--- /dev/null
+++ b/qa-service/qa-service-bootstrap/src/main/resources/application.properties
@@ -0,0 +1,28 @@
+server.port=28080
+
+spring.application.name=qa-service
+
+# MyBatis Plus配置
+mybatis-plus.configuration.map-underscore-to-camel-case=true
+mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
+mybatis-plus.global-config.db-config.id-type=assign_id
+
+# Nacos认证信息
+spring.cloud.nacos.discovery.username=nacos
+spring.cloud.nacos.discovery.password=nacos
+# Nacos 服务发现与注册配置,其中子属性 server-addr 指定 Nacos 服务器主机和端口
+spring.cloud.nacos.discovery.server-addr=192.168.168.128:8848
+# 注册到 nacos 的指定 namespace,默认为 public
+spring.cloud.nacos.discovery.namespace=public
+
+# Nacos帮助文档: https://nacos.io/zh-cn/docs/concepts.html
+# Nacos认证信息
+spring.cloud.nacos.config.username=nacos
+spring.cloud.nacos.config.password=nacos
+spring.cloud.nacos.config.contextPath=/nacos
+# 设置配置中心服务端地址
+spring.cloud.nacos.config.server-addr=192.168.168.128:8848
+# Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
+# spring.cloud.nacos.config.namespace=
+spring.config.import=nacos:${spring.application.name}.properties?refresh=true
+
diff --git a/qa-service/qa-service-bootstrap/src/test/java/com/example/qa/service/bootstrap/QaServiceBootstrapApplicationTests.java b/qa-service/qa-service-bootstrap/src/test/java/com/example/qa/service/bootstrap/QaServiceBootstrapApplicationTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..153ddf1989c7fe0269bdf4085b4e7e054849d3e5
--- /dev/null
+++ b/qa-service/qa-service-bootstrap/src/test/java/com/example/qa/service/bootstrap/QaServiceBootstrapApplicationTests.java
@@ -0,0 +1,13 @@
+package com.example.qa.service.bootstrap;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class QaServiceBootstrapApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/qa-service/qa-service-common/pom.xml b/qa-service/qa-service-common/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..38c3ef0bb9654e3dcb276165538ccdf3f6d99163
--- /dev/null
+++ b/qa-service/qa-service-common/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 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.projectlombok
+ lombok
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+
+
diff --git a/qa-service/qa-service-common/src/main/java/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..947c215b0c1be042c70620f74b908292b19510d3
--- /dev/null
+++ b/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/IdWorker.java
@@ -0,0 +1,201 @@
+package com.example.qa.service.common;
+
+import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import org.springframework.stereotype.Component;
+
+/**
+ * @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
+ */
+@Component
+public class IdWorker {
+ /**
+ * 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
+ */
+ private final static long TWEPOCH = 1288834974657L;
+
+ /**
+ * 机器标识位数
+ */
+ private final static long WORKER_ID_BITS = 5L;
+
+ /**
+ * 数据中心标识位数
+ */
+ private final static long DATA_CENTER_ID_BITS = 5L;
+
+ /**
+ * 机器ID最大值
+ */
+ private final static long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);
+
+ /**
+ * 数据中心ID最大值
+ */
+ private final static long MAX_DATACENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);
+
+ /**
+ * 毫秒内自增位
+ */
+ private final static long SEQUENCE_BITS = 12L;
+
+ /**
+ * 机器ID偏左移12位
+ */
+ private final static long WORKER_ID_SHIFT = SEQUENCE_BITS;
+
+ /**
+ * 数据中心ID左移17位
+ */
+ private final static long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
+
+ /**
+ * 时间毫秒左移22位
+ */
+ private final static long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
+
+ private final static long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
+
+ /**
+ * 上次生产id时间戳
+ */
+ private static long lastTimestamp = -1L;
+
+ /**
+ * 0,并发控制
+ */
+ private long sequence = 0L;
+
+ private final long workerId;
+
+ /**
+ * 数据标识id部分
+ */
+ private final long datacenterId;
+
+ public IdWorker() {
+ this.datacenterId = getDatacenterId();
+ this.workerId = getMaxWorkerId(datacenterId);
+ }
+
+ /**
+ * @param workerId 工作机器ID
+ * @param datacenterId 序列号
+ */
+ public IdWorker(long workerId, long datacenterId) {
+ if (workerId > MAX_WORKER_ID || workerId < 0) {
+ throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID));
+ }
+ if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
+ throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", MAX_DATACENTER_ID));
+ }
+ this.workerId = workerId;
+ this.datacenterId = datacenterId;
+ }
+
+ /**
+ * 获取下一个ID
+ *
+ * @return
+ */
+ public synchronized long nextId() {
+ long timestamp = timeGen();
+ if (timestamp < lastTimestamp) {
+ throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
+ }
+
+ if (lastTimestamp == timestamp) {
+ // 当前毫秒内,则+1
+ sequence = (sequence + 1) & SEQUENCE_MASK;
+ if (sequence == 0) {
+ // 当前毫秒内计数满了,则等待下一秒
+ timestamp = tilNextMillis(lastTimestamp);
+ }
+ } else {
+ sequence = 0L;
+ }
+ lastTimestamp = timestamp;
+ // ID偏移组合生成最终的ID,并返回ID
+
+ return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT)
+ | (datacenterId << DATACENTER_ID_SHIFT)
+ | (workerId << WORKER_ID_SHIFT) | sequence;
+ }
+
+ private long tilNextMillis(final long lastTimestamp) {
+ long timestamp = this.timeGen();
+ while (timestamp <= lastTimestamp) {
+ timestamp = this.timeGen();
+ }
+ return timestamp;
+ }
+
+ private long timeGen() {
+ return System.currentTimeMillis();
+ }
+
+ /**
+ *
+ * 获取 MAX_WORKER_ID
+ *
+ */
+ protected static long getMaxWorkerId(long datacenterId) {
+ StringBuilder mpid = new StringBuilder();
+ mpid.append(datacenterId);
+ String name = ManagementFactory.getRuntimeMXBean().getName();
+ if (!name.isEmpty()) {
+ /*
+ * GET jvmPid
+ */
+ mpid.append(name.split("@")[0]);
+ }
+ /*
+ * MAC + PID 的 hashcode 获取16个低位
+ */
+ return (mpid.toString().hashCode() & 0xffff) % (IdWorker.MAX_WORKER_ID + 1);
+ }
+
+ /**
+ *
+ * 数据标识id部分
+ *
+ */
+ protected static long getDatacenterId() {
+ long id = 0L;
+ try {
+ InetAddress ip = InetAddress.getLocalHost();
+ NetworkInterface network = NetworkInterface.getByInetAddress(ip);
+ if (network == null) {
+ id = 1L;
+ } else {
+ byte[] mac = network.getHardwareAddress();
+ id = ((0x000000FF & (long) mac[mac.length - 1])
+ | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
+ id = id % (IdWorker.MAX_DATACENTER_ID + 1);
+ }
+ } catch (Exception e) {
+ System.out.println(" getDatacenterId: " + e.getMessage());
+ }
+ return id;
+ }
+
+
+
+
+}
diff --git a/qa-service/qa-service-domain/pom.xml b/qa-service/qa-service-domain/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..58f67afc28696872aff07a8951d4ab7380489c26
--- /dev/null
+++ b/qa-service/qa-service-domain/pom.xml
@@ -0,0 +1,70 @@
+
+
+ 4.0.0
+ com.example
+ qa-service-domain
+ 0.0.1-SNAPSHOT
+ qa-service-domain
+ qa-service-domain
+
+
+ com.example
+ qa-service
+ 0.0.1-SNAPSHOT
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ 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..c40b3bb8b50adb5ed894a91f9236691c5fa3da7b
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/QA.java
@@ -0,0 +1,48 @@
+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.QAId;
+import com.example.qa.service.domain.valueobject.Question;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.util.List;
+
+@Setter
+@Getter
+@ToString
+/**
+ * @author yangwenkai
+ * QA类
+ */
+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..aeb293a24a7b7f71effce1863e805726e786cf58
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/CreateQAPort.java
@@ -0,0 +1,11 @@
+package com.example.qa.service.domain.port;
+
+import com.example.qa.service.domain.QA;
+
+/**
+ * @author yangwenkai
+ * 创建QA的端口
+ */
+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..2ea8e475eb4d2f5e5df59009a0a16960b9b998ff
--- /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 qiuyujie
+ */
+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..fdc3b92c6866260c51aaeeaa7d8a5bc90486037b
--- /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 dengli
+ */
+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..3fc891d5e12b7ecac4e4b6afdba25780db31b214
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQAListPort.java
@@ -0,0 +1,11 @@
+package com.example.qa.service.domain.port;
+
+import com.example.qa.service.domain.QA;
+
+import java.util.List;
+/**
+ * @author wangqingqing
+ */
+public interface GetQAListPort {
+ List getQAs();
+}
diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/KimiAiPort.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/KimiAiPort.java
new file mode 100644
index 0000000000000000000000000000000000000000..8eb8269247e0e423b6196fa2a1e181e0d907645f
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/KimiAiPort.java
@@ -0,0 +1,16 @@
+package com.example.qa.service.domain.port;
+
+/**
+ * KIMI AI调用端口接口
+ * 定义了与外部AI服务交互的契约
+ * @author yangwenkai
+ */
+public interface KimiAiPort {
+
+ /**
+ * 调用KIMI AI获取问题的答案
+ * @param question 用户提出的问题
+ * @return AI生成的答案
+ */
+ String getAnswer(String question);
+}
\ No newline at end of file
diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/MessageQueuePort.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/MessageQueuePort.java
new file mode 100644
index 0000000000000000000000000000000000000000..1b498eaaa8dbcb4203d8f55e7f6e764de9bd61af
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/MessageQueuePort.java
@@ -0,0 +1,22 @@
+package com.example.qa.service.domain.port;
+
+/**
+ * 消息队列端口接口
+ * 定义了与消息队列交互的契约
+ * @author yangwenkai
+ */
+public interface MessageQueuePort {
+
+ /**
+ * 发送问题到消息队列进行异步处理
+ * @param question 用户提出的问题
+ */
+ void sendQuestionToQueue(String question);
+
+ /**
+ * 发送问答对到消息队列进行数据库保存
+ * @param question 问题
+ * @param answer 答案
+ */
+ void sendQAToSaveQueue(String question, String answer);
+}
\ No newline at end of file
diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/QARepositoryPort.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/QARepositoryPort.java
new file mode 100644
index 0000000000000000000000000000000000000000..cefd047fc35c06f54b7e0ed9fb38b233aa0d90a2
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/QARepositoryPort.java
@@ -0,0 +1,17 @@
+package com.example.qa.service.domain.port;
+
+/**
+ * QA仓储端口接口
+ * 定义问答数据的持久化操作
+ * @author yangwenkai
+ */
+public interface QARepositoryPort {
+
+ /**
+ * 保存问答对到数据库
+ * 将问题和答案保存为一条记录
+ * @param question 问题内容
+ * @param answer 答案内容
+ */
+ void saveQA(String question, String answer);
+}
\ No newline at end of file
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..5713b796ac0f3a0ec2fc46c8336bcfcc2357ede6
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/UpdateQAPort.java
@@ -0,0 +1,9 @@
+package com.example.qa.service.domain.port;
+
+import com.example.qa.service.domain.QA;
+/**
+ * @author lizishan
+ */
+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..8a34b3c6566dccc0ca7ffc28636c7f2a0de8e817
--- /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 qiuyujie
+ */
+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..a423600d57ed7a90faf87eace687a414c0b76d37
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QAId.java
@@ -0,0 +1,10 @@
+package com.example.qa.service.domain.valueobject;
+
+/**
+ * @author dengli
+ */
+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/QAMessageDTO.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QAMessageDTO.java
new file mode 100644
index 0000000000000000000000000000000000000000..2934e6a71fa675ef079956f66605ae5b70d0740a
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QAMessageDTO.java
@@ -0,0 +1,38 @@
+package com.example.qa.service.domain.valueobject;
+
+import lombok.Data;
+
+/**
+ * QA消息DTO
+ * 用于在消息队列中传递问答数据的值对象
+ * @author yangwenkai
+ */
+@Data
+public class QAMessageDTO {
+
+ /**
+ * 问题内容
+ */
+ private String question;
+
+ /**
+ * 答案内容
+ */
+ private String answer;
+
+ /**
+ * 默认构造函数
+ */
+ public QAMessageDTO() {
+ }
+
+ /**
+ * 带参数的构造函数
+ * @param question 问题内容
+ * @param answer 答案内容
+ */
+ public QAMessageDTO(String question, String answer) {
+ this.question = question;
+ this.answer = answer;
+ }
+}
\ No newline at end of file
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..773a8f95b7ddc44f57555c60fc3f4ff3b5d71a60
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/Question.java
@@ -0,0 +1,9 @@
+package com.example.qa.service.domain.valueobject;
+/**
+ * @author wangqingqing
+ */
+public record Question(String question) {
+ public String getValue() {
+ return question;
+ }
+}
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..ea11873083652477b60fb3755988fe94724b7f84 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
@@ -19,6 +19,10 @@
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-starter-security
+
org.springframework.boot
@@ -41,6 +45,12 @@
0.0.1-SNAPSHOT
+
+ com.example
+ user-adapter-out-persistence
+ 0.0.1-SNAPSHOT
+
+
com.github.xiaoymin
knife4j-openapi3-jakarta-spring-boot-starter
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/SecurityConfig.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/SecurityConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..985dac5f7d092fdfa460e71e0553ce59cd174b82
--- /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/SecurityConfig.java
@@ -0,0 +1,209 @@
+package com.example.user.adapter.in.web.config;
+
+import com.example.user.adapter.in.web.filter.JwtAuthenticationFilter;
+import com.example.user.adapter.in.web.service.CustomUserDetailsService;
+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.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+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 安全配置类
+ *
+ * 这个类负责配置整个用户服务的认证和授权策略,包括:
+ * 1. JWT Token认证机制
+ * 2. 跨域资源共享(CORS)配置
+ * 3. 请求授权规则
+ * 4. 密码加密策略
+ * 5. 会话管理策略
+ *
+ * 注解说明:
+ * @Configuration - 标记为Spring配置类
+ * @EnableWebSecurity - 启用Spring Security的Web安全支持
+ * @EnableMethodSecurity - 启用方法级别的安全控制
+ * @RequiredArgsConstructor - Lombok注解,自动生成构造函数
+ */
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity(prePostEnabled = true)
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+ /**
+ * JWT认证过滤器 - 用于拦截请求并验证JWT Token
+ */
+ private final JwtAuthenticationFilter jwtAuthenticationFilter;
+
+ /**
+ * 自定义用户详情服务 - 用于加载用户信息进行认证
+ */
+ private final CustomUserDetailsService userDetailsService;
+
+ /**
+ * 认证管理器Bean
+ *
+ * 这个方法创建并配置Spring Security的认证管理器
+ * 认证管理器负责处理用户认证请求,比如验证用户名和密码
+ *
+ * @param config Spring Security的认证配置对象,由Spring自动注入
+ * @return AuthenticationManager 认证管理器实例
+ * @throws Exception 如果配置过程中出现异常
+ */
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+ return config.getAuthenticationManager();
+ }
+
+ /**
+ * 密码编码器Bean
+ *
+ * 这个方法配置BCrypt密码编码器,用于:
+ * 1. 用户注册时加密密码
+ * 2. 用户登录时验证密码
+ *
+ * BCrypt是一种安全的密码哈希算法,会自动处理salt和哈希迭代
+ *
+ * @return PasswordEncoder 密码编码器实例
+ */
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ /**
+ * CORS配置源
+ *
+ * 配置跨域资源共享策略,允许前端应用访问后端API
+ * 支持所有HTTP方法、所有请求头,并允许携带凭证
+ *
+ * @return CorsConfigurationSource CORS配置源
+ */
+ @Bean
+ public CorsConfigurationSource corsConfigurationSource() {
+ CorsConfiguration configuration = new CorsConfiguration();
+
+ // 允许所有域名访问(生产环境应限制为具体域名)
+ configuration.setAllowedOriginPatterns(List.of("*"));
+
+ // 允许所有HTTP方法
+ configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
+
+ // 允许所有请求头
+ configuration.setAllowedHeaders(List.of("*"));
+
+ // 允许携带凭证(如Cookie)
+ configuration.setAllowCredentials(true);
+
+ // 预检请求缓存时间(1小时)
+ configuration.setMaxAge(3600L);
+
+ // 暴露给客户端的响应头
+ configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type"));
+
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", configuration);
+ return source;
+ }
+
+ /**
+ * 安全过滤器链配置
+ *
+ * 这是Spring Security的核心配置方法,定义了整个应用的安全策略:
+ * 1. 禁用CSRF保护(前后端分离项目不需要)
+ * 2. 启用CORS跨域支持
+ * 3. 配置无状态会话管理(使用JWT,不需要Session)
+ * 4. 设置请求授权规则
+ * 5. 添加JWT认证过滤器
+ * 6. 配置异常处理
+ *
+ * @param http HttpSecurity对象,用于配置Web安全
+ * @return SecurityFilterChain 安全过滤器链
+ * @throws Exception 如果配置过程中出现异常
+ */
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http
+ // 禁用CSRF保护(前后端分离项目通常禁用)
+ .csrf(AbstractHttpConfigurer::disable)
+
+ // 启用CORS并使用上面定义的配置
+ .cors(cors -> cors.configurationSource(corsConfigurationSource()))
+
+ // 配置无状态会话管理(使用JWT Token认证)
+ .sessionManagement(session -> session
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ )
+
+ // 配置请求授权规则
+ .authorizeHttpRequests(authz -> authz
+ // 公开接口,无需认证即可访问
+ .requestMatchers("/users/login").permitAll() // 登录接口
+ .requestMatchers("/users/register").permitAll() // 注册接口
+ // .requestMatchers("/users/test").permitAll() // 测试接口需要认证
+
+ // 需要认证的接口
+ .requestMatchers("/users/logout").authenticated() // 登出接口需要认证
+ .requestMatchers("/users/me").authenticated() // 获取用户信息接口需要认证
+
+ // 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()
+ )
+
+ // 禁用默认的登录页面
+ .formLogin(AbstractHttpConfigurer::disable)
+
+ // 禁用默认的登出页面
+ .logout(AbstractHttpConfigurer::disable)
+
+ // 禁用HTTP Basic认证
+ .httpBasic(AbstractHttpConfigurer::disable)
+
+ // 添加JWT认证过滤器(在用户名密码认证过滤器之前)
+ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
+
+ // 配置用户详情服务
+ .userDetailsService(userDetailsService)
+
+ // 配置异常处理
+ .exceptionHandling(exceptions -> exceptions
+ // 认证入口点(未认证用户访问需要认证的资源时调用)
+ .authenticationEntryPoint((request, response, authException) -> {
+ response.setStatus(401);
+ response.setContentType("application/json;charset=UTF-8");
+ response.getWriter().write("{\"code\":401,\"message\":\"未授权访问,请先登录\",\"data\":null}");
+ })
+ // 访问拒绝处理器(已认证用户访问没有权限的资源时调用)
+ .accessDeniedHandler((request, response, accessDeniedException) -> {
+ response.setStatus(403);
+ response.setContentType("application/json;charset=UTF-8");
+ response.getWriter().write("{\"code\":403,\"message\":\"访问被拒绝,权限不足\",\"data\":null}");
+ })
+ );
+
+ return http.build();
+ }
+}
\ 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/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..88cdc35b496d43b4b4c6c9927d05b0a75fada16d 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
@@ -1,19 +1,32 @@
package com.example.user.adapter.in.web.controller;
import com.example.user.adapter.in.web.dto.CreateUserRequestDTO;
+import com.example.user.adapter.in.web.dto.LoginResponse;
+import com.example.user.adapter.in.web.dto.Result;
import com.example.user.adapter.in.web.dto.UpdateUserRequestDTO;
+import com.example.user.adapter.in.web.dto.UserInfo;
import com.example.user.adapter.in.web.dto.UserLoginRequestDTO;
import com.example.user.adapter.in.web.dto.UserResponseDTO;
import com.example.user.service.application.command.CreateUserCommand;
import com.example.user.service.application.command.UpdateUserCommand;
import com.example.user.service.application.command.UserLoginCommand;
import com.example.user.service.application.port.in.*;
+import com.example.user.service.application.util.JwtUtil;
+import com.example.user.service.application.service.TokenBlacklistService;
+import com.example.user.service.domain.port.GetUserByNamePort;
import com.example.user.service.domain.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
@Slf4j
@RequestMapping("/users")
@@ -27,20 +40,214 @@ public class UserController {
private final UpdateUserUseCase updateUserUseCase;
private final GetUserByIdUseCase getUserByIdUseCase;
private final UserLoginUseCase userLoginUseCase;
+ private final JwtUtil jwtUtil;
+ private final TokenBlacklistService tokenBlacklistService;
+ private final GetUserByNamePort getUserByNamePort;
+ /**
+ * 用户登录接口
+ * @author yangwenkai
+ * 这个接口用于用户登录验证,验证成功后返回JWT令牌和用户信息
+ *
+ * 实现流程:
+ * 1. 验证请求参数
+ * 2. 调用登录用侌生成JWT令牌
+ * 3. 获取用户详细信息
+ * 4. 构建登录响应对象
+ * 5. 返回统一格式的响应结果
+ *
+ * @param userLoginRequestDTO 登录请求数据,包含用户名和密码
+ * @return Result 登录成功的响应数据,包含JWT令牌和用户信息
+ */
@PostMapping("login")
- public String login(@RequestBody UserLoginRequestDTO userLoginRequestDTO){
- log.info("UserLoginRequestDTO:{}",userLoginRequestDTO);
- UserLoginCommand command=UserLoginCommand.builder()
- .name(userLoginRequestDTO.getName())
- .password(userLoginRequestDTO.getPassword())
- .build();
- String token = userLoginUseCase.login(command);
- return token;
+ public Result login(@RequestBody UserLoginRequestDTO userLoginRequestDTO){
+ log.info("接收到登录请求:{}", userLoginRequestDTO);
+
+ try {
+ // 验证请求参数
+ if (userLoginRequestDTO.getUsername() == null || userLoginRequestDTO.getUsername().trim().isEmpty()) {
+ return Result.error("用户名不能为空");
+ }
+ if (userLoginRequestDTO.getPassword() == null || userLoginRequestDTO.getPassword().trim().isEmpty()) {
+ return Result.error("密码不能为空");
+ }
+
+ // 创建登录命令对象
+ UserLoginCommand command = new UserLoginCommand(
+ userLoginRequestDTO.getUsername().trim(),
+ userLoginRequestDTO.getPassword().trim()
+ );
+
+ // 调用登录用侌,生成JWT令牌
+ String token = userLoginUseCase.login(command);
+
+ // 获取用户详细信息
+ User user = getUserByNamePort.getUserByName(userLoginRequestDTO.getUsername().trim());
+ if (user == null) {
+ return Result.error("用户信息获取失败");
+ }
+
+ // 构建UserInfo对象
+ UserInfo userInfo = UserInfo.builder()
+ .id(user.getId().getValue())
+ .username(user.getUsername().getValue())
+ .email(user.getEmail().getValue())
+ .phone(user.getPhone() != null ? user.getPhone().getValue() : null)
+ .realName(user.getRealName() != null ? user.getRealName().getValue() : null)
+ .status(user.getStatus().getValue())
+ .role(user.getRole().getValue())
+ .createTime(user.getCreateTime().getValue())
+ .lastLoginTime(user.getLastLoginTime() != null ? user.getLastLoginTime().getValue() : null)
+ .build();
+
+ // 构建LoginResponse对象
+ LoginResponse loginResponse = LoginResponse.of(
+ token, // JWT令牌
+ userInfo, // 用户信息
+ System.currentTimeMillis() + jwtUtil.expiration // 过期时间戳(当前时间 + 过期时长)
+ );
+
+ log.info("用户 {} 登录成功,生成JWT令牌", userLoginRequestDTO.getUsername());
+
+ // 返回统一格式的成功响应
+ return Result.success("登录成功", loginResponse);
+
+ } catch (RuntimeException e) {
+ // 登录失败的情况(用户不存在、密码错误等)
+ log.warn("登录失败:{}", e.getMessage());
+ return Result.error(e.getMessage());
+
+ } catch (Exception e) {
+ // 其他异常情况
+ log.error("登录过程中发生异常:{}", e.getMessage(), e);
+ return Result.serverError("登录系统异常,请联系管理员");
+ }
}
+ /**
+ * 用户登出接口
+ * @author qiuyujie dengli
+ * 实现用户登出功能,将JWT令牌加入黑名单并清除SecurityContext
+ * 参考learn文件夹中的登出实现,适配当前项目架构
+ *
+ * 主要流程:
+ * 1. 从请求头中提取JWT令牌
+ * 2. 验证令牌的有效性
+ * 3. 将令牌加入黑名单
+ * 4. 清除当前的安全上下文
+ * 5. 返回登出结果
+ *
+ * @param request HTTP请求对象,用于获取Authorization头
+ * @return Result 登出结果响应
+ */
+ @PostMapping("/logout")
+ public Result logout(jakarta.servlet.http.HttpServletRequest request) {
+ try {
+ // 1. 从请求头中提取JWT令牌
+ String authorizationHeader = request.getHeader("Authorization");
+ String token = jwtUtil.extractTokenFromHeader(authorizationHeader);
+
+ if (token == null || token.trim().isEmpty()) {
+ log.warn("登出失败: 未找到有效的JWT令牌");
+ return Result.error("未找到有效的登录令牌");
+ }
+
+ // 2. 从令牌中获取用户名用于验证
+ String username = jwtUtil.getUsernameFromToken(token);
+ if (username == null) {
+ log.warn("登出失败: 无法从令牌中获取用户名");
+ return Result.error("登录令牌格式错误");
+ }
+
+ // 3. 验证令牌的有效性(排除已过期的令牌)
+ if (!jwtUtil.validateToken(token, username)) {
+ log.warn("登出失败: JWT令牌无效或已过期");
+ return Result.error("登录令牌无效或已过期");
+ }
+
+ // 4. 将令牌加入黑名单
+ tokenBlacklistService.addToBlacklist(token);
+
+ // 5. 清除当前的安全上下文
+ SecurityContextHolder.clearContext();
+
+ log.info("用户登出成功: token已加入黑名单");
+ return Result.success("登出成功");
+
+ } catch (Exception e) {
+ log.error("登出失败: 系统错误 - {}", e.getMessage(), e);
+ return Result.error("登出失败,请稍后重试");
+ }
+ }
+ /**
+ * 获取当前用户信息接口
+ *
+ * 基于JWT令牌获取当前登录用户的详细信息
+ * 参考learn文件夹中的用户信息获取实现,适配当前项目架构
+ *
+ * 工作流程:
+ * 1. 从SecurityContext获取当前认证信息
+ * 2. 验证用户是否已登录
+ * 3. 根据用户名查询用户详细信息
+ * 4. 构建并返回用户信息响应
+ *
+ * 安全机制:
+ * - JWT令牌验证:确保令牌未被篡改且未过期
+ * - 用户状态检查:确保用户账户仍然有效
+ * - 敏感信息过滤:返回的用户信息不包含密码等敏感数据
+ *
+ * @return Result 当前用户信息响应
+ */
+ @GetMapping("/me")
+ public Result getCurrentUser() {
+ try {
+ // 1. 从SecurityContext获取当前认证信息
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+
+ // 2. 验证用户是否已登录
+ if (authentication == null || !authentication.isAuthenticated() ||
+ authentication instanceof AnonymousAuthenticationToken) {
+ log.warn("获取用户信息失败: 用户未登录");
+ return Result.error("用户未登录,请先登录");
+ }
+
+ // 3. 获取用户名
+ String username = authentication.getName();
+ if (username == null || username.trim().isEmpty()) {
+ log.warn("获取用户信息失败: 无法获取用户名");
+ return Result.error("无法获取用户信息");
+ }
+
+ // 4. 根据用户名查询用户详细信息
+ User user = getUserByNamePort.getUserByName(username.trim());
+ if (user == null) {
+ log.warn("获取用户信息失败: 用户不存在 - {}", username);
+ return Result.error("用户信息不存在");
+ }
+
+ // 5. 构建用户信息响应对象
+ UserInfo userInfo = UserInfo.builder()
+ .id(user.getId().getValue())
+ .username(user.getUsername().getValue())
+ .email(user.getEmail().getValue())
+ .phone(user.getPhone() != null ? user.getPhone().getValue() : null)
+ .realName(user.getRealName() != null ? user.getRealName().getValue() : null)
+ .status(user.getStatus().getValue())
+ .role(user.getRole().getValue())
+ .createTime(user.getCreateTime().getValue())
+ .lastLoginTime(user.getLastLoginTime() != null ? user.getLastLoginTime().getValue() : null)
+ .build();
+
+ log.info("获取用户信息成功: username={}, id={}", username, user.getId().getValue());
+ return Result.success(userInfo);
+
+ } catch (Exception e) {
+ log.error("获取用户信息失败: 系统错误 - {}", e.getMessage(), e);
+ return Result.error("获取用户信息失败,请稍后重试");
+ }
+ }
@GetMapping("")
public List getUsers() {
@@ -49,24 +256,91 @@ public class UserController {
}
/**
- * 创建新用户
- * 功能:接收用户注册信息,验证密码一致性,创建新用户账户
- * @author dongxuanfeng
- * @param createUserRequestDTO
- * @return User - 成功创建的新用户
- * @throws IllegalArgumentException 当密码与确认密码不匹配时抛出此异常
+ * 用户注册接口
+ * @author wangqingqing lizishan
+ * 实现用户注册功能,包括用户名重复检查、密码验证和加密等
+ * 参考learn文件夹中的注册实现,适配当前项目架构
+ *
+ * 主要流程:
+ * 1. 验证请求参数的有效性
+ * 2. 检查用户名是否已经存在
+ * 3. 验证密码格式和一致性
+ * 4. 创建新用户(密码会在领域层自动加密)
+ * 5. 返回注册结果
+ *
+ * @param createUserRequestDTO 注册请求数据,包含用户名、密码、邮箱等信息
+ * @return Result 注册结果响应
+ */
+ @PostMapping("/register")
+ public Result register(@RequestBody CreateUserRequestDTO createUserRequestDTO){
+ try {
+ log.info("用户注册请求: username={}, email={}", createUserRequestDTO.username(), createUserRequestDTO.email());
+
+ // 1. 验证请求参数
+ if (createUserRequestDTO.username() == null || createUserRequestDTO.username().trim().isEmpty()) {
+ return Result.error("用户名不能为空");
+ }
+ if (createUserRequestDTO.email() == null || createUserRequestDTO.email().trim().isEmpty()) {
+ return Result.error("邮箱不能为空");
+ }
+
+ // 2. 检查用户名是否已存在
+ User existingUser = getUserByNamePort.getUserByName(createUserRequestDTO.username().trim());
+ if (existingUser != null) {
+ log.warn("注册失败: 用户名已存在 - {}", createUserRequestDTO.username());
+ return Result.error("用户名已存在,请选择其他用户名");
+ }
+
+ // 3. 验证密码格式和一致性
+ createUserRequestDTO.validatePassword();
+
+ // 4. 创建用户命令对象
+ CreateUserCommand command = CreateUserCommand.builder()
+ .username(createUserRequestDTO.username().trim())
+ .email(createUserRequestDTO.email().trim())
+ .password(createUserRequestDTO.password())
+ .phone(createUserRequestDTO.phone())
+ .realName(createUserRequestDTO.realName())
+ .build();
+
+ // 5. 创建新用户
+ User newUser = createUserUseCase.createUser(command);
+
+ log.info("用户注册成功: username={}, id={}", newUser.getUsername().getValue(), newUser.getId().getValue());
+ return Result.success("注册成功");
+
+ } catch (IllegalArgumentException e) {
+ log.warn("注册失败: 参数验证错误 - {}", e.getMessage());
+ return Result.error(e.getMessage());
+ } catch (Exception e) {
+ log.error("注册失败: 系统错误 - {}", e.getMessage(), e);
+ return Result.error("注册失败,请稍后重试");
+ }
+ }
+
+ /**
+ * 创建用户接口(管理员使用)
+ *
+ * 保留原有的创建用户功能,供管理员或系统内部使用
+ * 与注册接口的区别:不进行用户名重复检查,直接创建用户
+ *
+ * @param createUserRequestDTO 创建用户请求数据
+ * @return User 创建的用户对象
*/
@PostMapping()
public User createUser(@RequestBody CreateUserRequestDTO createUserRequestDTO){
+ // 验证密码是否符合要求
+ createUserRequestDTO.validatePassword();
if (!createUserRequestDTO.isPasswordValid()) {
throw new IllegalArgumentException("密码和确认密码不匹配");
}
CreateUserCommand command=CreateUserCommand.builder()
- .name(createUserRequestDTO.name())
- .age(createUserRequestDTO.age())
+ .username(createUserRequestDTO.username())
.email(createUserRequestDTO.email())
.password(createUserRequestDTO.password())
+ .phone(createUserRequestDTO.phone())
+ .realName(createUserRequestDTO.realName())
.build();
return createUserUseCase.createUser(command);
@@ -84,9 +358,12 @@ public class UserController {
public User updateUser(@RequestBody UpdateUserRequestDTO updateUserRequestDTO){
UpdateUserCommand command=UpdateUserCommand.builder()
.id(updateUserRequestDTO.id())
- .name(updateUserRequestDTO.name())
- .age(updateUserRequestDTO.age())
+ .username(updateUserRequestDTO.username())
.email(updateUserRequestDTO.email())
+ .phone(updateUserRequestDTO.phone())
+ .realName(updateUserRequestDTO.realName())
+ .status(updateUserRequestDTO.status())
+ .role(updateUserRequestDTO.role())
.build();
User user = updateUserUseCase.updateUser(command);
return user;
@@ -98,12 +375,44 @@ public class UserController {
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());
+ user.getId().getValue(),
+ user.getUsername().getValue(),
+ user.getEmail().getValue(),
+ user.getPhone() != null ? user.getPhone().getValue() : null,
+ user.getRealName() != null ? user.getRealName().getValue() : null,
+ user.getStatus().getValue(),
+ user.getRole().getValue());
return userResponseDTO;
}
+ /**
+ * 测试接口 - 验证JWT认证功能
+ * 这个接口用于测试认证系统是否正常工作
+ * 返回当前认证用户的信息
+ * @author qiuyujie dengli wangqingqing lizishan
+ * @return Map 包含认证状态和用户信息
+ */
+ @GetMapping("test")
+ public Map testAuthentication() {
+ Map result = new HashMap<>();
+ result.put("status", "success");
+ result.put("message", "测试接口调用成功");
+ result.put("timestamp", System.currentTimeMillis());
+
+ // 获取当前认证信息
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication != null && authentication.isAuthenticated() &&
+ !(authentication instanceof AnonymousAuthenticationToken)) {
+ result.put("authenticated", true);
+ result.put("username", authentication.getName());
+ result.put("authorities", authentication.getAuthorities().stream()
+ .map(GrantedAuthority::getAuthority)
+ .collect(Collectors.toList()));
+ } else {
+ result.put("authenticated", false);
+ result.put("message", "用户未认证,请先登录获取token");
+ }
+
+ return result;
+ }
}
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..fdffac30c7ce2317a30fb207825edf2d1b6ddc08 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,22 @@
package com.example.user.adapter.in.web.dto;
+import com.example.user.service.domain.valueobject.Password;
+
public record CreateUserRequestDTO(
- String name,
- Integer age,
+ String username,
String email,
String password,
- String rePassword) {
- // TODO: 密码校验
-
+ String rePassword,
+ String phone,
+ String realName) {
+
/**
- * 验证密码与确认密码是否一致
- * @author dongxuanfeng
- * @return boolean -验证结果:true表示密码与重复密码一致,false表示两次密码不一致
+ * 验证密码是否符合要求
+ * 包括密码长度、复杂度以及密码和确认密码的一致性
+ * @author yangwenkai
+ * @throws IllegalArgumentException 如果密码不符合要求
*/
- public boolean isPasswordValid() {
- return password != null && password.equals(rePassword);
+ public void validatePassword() {
+ Password.validatePassword(password, 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/LoginResponse.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..10c061f55abb4aedf82929eb5831e3eac218b9d7
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginResponse.java
@@ -0,0 +1,82 @@
+package com.example.user.adapter.in.web.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+
+/**
+ * 登录响应DTO
+ * 专门用于封装登录成功后的响应数据
+ *
+ * 这个类包含了登录成功后需要返回给前端的所有信息
+ * 包括JWT令牌、令牌类型、用户信息、过期时间等
+ *
+ * 使用Builder模式可以方便地构建复杂的对象
+ * 例如:LoginResponse.builder().accessToken("token").userInfo(userInfo).build()
+ *
+ * @Data Lombok注解,自动生成getter、setter、toString等方法
+ * @Builder Lombok注解,提供Builder模式构建对象
+ * @NoArgsConstructor Lombok注解,生成无参构造函数
+ * @AllArgsConstructor Lombok注解,生成全参构造函数
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class LoginResponse {
+
+ /**
+ * 访问令牌 (Access Token)
+ *
+ * JWT格式的令牌,用于后续API请求的身份认证
+ * 前端需要在请求头中携带这个令牌:Authorization: Bearer
+ * 令牌中包含了用户的基本信息和权限信息
+ */
+ private String accessToken;
+
+ /**
+ * 令牌类型 (Token Type)
+ *
+ * 指定令牌的类型,通常是"Bearer"
+ * Bearer是一种认证方案,表示持有令牌即可访问资源
+ * 这个字段告诉前端如何正确使用这个令牌
+ */
+ private String tokenType;
+
+ /**
+ * 用户信息
+ *
+ * 包含登录用户的基本信息,如用户名、邮箱、角色等
+ * 前端可以使用这些信息来显示用户界面、控制权限等
+ * 使用UserInfo类来封装用户信息,保持数据结构清晰
+ */
+ private UserInfo userInfo;
+
+ /**
+ * 过期时间戳 (Expires In)
+ *
+ * 令牌的有效期,单位是毫秒
+ * 表示从当前时间开始,令牌还有多少毫秒会过期
+ * 前端可以根据这个时间戳来判断是否需要刷新令牌
+ * 例如:7200000 表示2小时(2 * 60 * 60 * 1000)
+ */
+ private Long expiresIn;
+
+ /**
+ * 创建登录响应对象的便捷方法
+ *
+ * @param accessToken JWT访问令牌
+ * @param userInfo 用户信息对象
+ * @param expiresIn 过期时间(毫秒)
+ * @return 构建好的LoginResponse对象
+ */
+ public static LoginResponse of(String accessToken, UserInfo userInfo, Long expiresIn) {
+ return LoginResponse.builder()
+ .accessToken(accessToken)
+ .tokenType("Bearer")
+ .userInfo(userInfo)
+ .expiresIn(expiresIn)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java
new file mode 100644
index 0000000000000000000000000000000000000000..42c60eb18896114c5c0530f1908bb88fac4d4e63
--- /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,138 @@
+package com.example.user.adapter.in.web.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.AllArgsConstructor;
+
+/**
+ * 通用响应结果类
+ * 用于封装所有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 message 成功消息
+ * @param data 响应数据
+ * @return Result 成功响应对象
+ */
+ public static Result success(String message, T data) {
+ Result result = new Result<>();
+ result.setCode(200);
+ result.setMessage(message);
+ result.setData(data);
+ return result;
+ }
+
+ /**
+ * 成功响应静态方法(简化版)
+ *
+ * @param data 响应数据
+ * @return Result 成功响应对象
+ */
+ public static Result success(T data) {
+ return success("操作成功", data);
+ }
+
+ /**
+ * 错误响应静态方法
+ *
+ * @param message 错误消息
+ * @return Result 错误响应对象
+ */
+ public static Result error(String message) {
+ Result result = new Result<>();
+ result.setCode(400);
+ result.setMessage(message);
+ result.setData(null);
+ return result;
+ }
+
+ /**
+ * 未授权响应静态方法
+ *
+ * @param message 未授权消息
+ * @return Result 未授权响应对象
+ */
+ public static Result unauthorized(String message) {
+ Result result = new Result<>();
+ result.setCode(401);
+ result.setMessage(message);
+ result.setData(null);
+ return result;
+ }
+
+ /**
+ * 禁止访问响应静态方法
+ *
+ * @param message 禁止访问消息
+ * @return Result 禁止访问响应对象
+ */
+ public static Result forbidden(String message) {
+ Result result = new Result<>();
+ result.setCode(403);
+ result.setMessage(message);
+ result.setData(null);
+ return result;
+ }
+
+ /**
+ * 服务器错误响应静态方法
+ *
+ * @param message 服务器错误消息
+ * @return Result 服务器错误响应对象
+ */
+ public static Result serverError(String message) {
+ Result result = new Result<>();
+ result.setCode(500);
+ result.setMessage(message);
+ result.setData(null);
+ return result;
+ }
+}
\ 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..92ec9af2ad5fb6d23dac4d9e4dbf6e6cc7529bba 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
@@ -1,7 +1,10 @@
package com.example.user.adapter.in.web.dto;
public record UpdateUserRequestDTO(Long id,
- String name,
- Integer age,
- String email) {
+ String username,
+ String email,
+ String phone,
+ String realName,
+ Integer status,
+ String role) {
}
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserInfo.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserInfo.java
new file mode 100644
index 0000000000000000000000000000000000000000..d3525e39b9ef2c690aab441d45421f873d041697
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserInfo.java
@@ -0,0 +1,102 @@
+package com.example.user.adapter.in.web.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+
+import java.time.LocalDateTime;
+
+/**
+ * 用户信息DTO (Data Transfer Object)
+ * 用于封装用户的基本信息,在不同层之间传输
+ *
+ * DTO是数据传输对象,主要用于在不同层之间传输数据
+ * 这里专门用于传输用户的基本信息,不包含敏感信息如密码
+ *
+ * @Data Lombok注解,自动生成getter、setter、toString等方法
+ * @Builder Lombok注解,提供Builder模式构建对象,使代码更清晰易读
+ * @NoArgsConstructor Lombok注解,生成无参构造函数
+ * @AllArgsConstructor Lombok注解,生成全参构造函数
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UserInfo {
+
+ /**
+ * 用户ID
+ *
+ * 数据库中的主键,唯一标识一个用户
+ * 使用Long类型可以支持更大的数据量
+ */
+ private Long id;
+
+ /**
+ * 用户名
+ *
+ * 用户登录时使用的名称,必须唯一
+ * 通常用于登录认证
+ */
+ private String username;
+
+ /**
+ * 邮箱
+ *
+ * 用户的电子邮箱地址
+ * 可以用于找回密码、接收通知等
+ */
+ private String email;
+
+ /**
+ * 手机号
+ *
+ * 用户的手机号码
+ * 可以用于登录、找回密码、接收短信通知等
+ */
+ private String phone;
+
+ /**
+ * 真实姓名
+ *
+ * 用户的真实姓名
+ * 用于实名认证、显示用户真实身份等
+ */
+ private String realName;
+
+ /**
+ * 用户状态:0-禁用,1-启用
+ *
+ * 用于控制用户账户是否可以正常使用
+ * 0表示账户被禁用,无法登录
+ * 1表示账户正常,可以登录使用
+ */
+ private Integer status;
+
+ /**
+ * 角色:ADMIN-管理员,USER-普通用户
+ *
+ * 用于区分用户的角色和权限
+ * ADMIN表示管理员,拥有更高的权限
+ * USER表示普通用户,拥有基本权限
+ */
+ private String role;
+
+ /**
+ * 创建时间
+ *
+ * 记录用户账户的创建时间
+ * 使用LocalDateTime类型,是Java 8引入的新时间API
+ * 相比Date类型更加易用和直观
+ */
+ private LocalDateTime createTime;
+
+ /**
+ * 最后登录时间
+ *
+ * 记录用户最后一次登录的时间
+ * 用于统计用户活跃度、安全监控等
+ */
+ private LocalDateTime lastLoginTime;
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserLoginRequestDTO.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserLoginRequestDTO.java
index 112434d3ec034fbfe024cf9148d6a805976e0536..c649b3875c74caefdf863c9c3f9b4be4f7fff84e 100644
--- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserLoginRequestDTO.java
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserLoginRequestDTO.java
@@ -8,7 +8,7 @@ import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
public class UserLoginRequestDTO {
- private String name;
+ private String username;
// 这里的password指的是用户输入的密码,而不是数据库中的密码。所以这里应该使用明文,类型是String
private String password;
}
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/UserResponseDTO.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserResponseDTO.java
index 153323ddd9e7c276b93f76c11f1ee300407d600f..0e21ec411ef23a694de3a5a00c608f5ed5e45bae 100644
--- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserResponseDTO.java
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserResponseDTO.java
@@ -9,9 +9,12 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
public class UserResponseDTO {
private long id;
- private String name;
- private int age;
+ private String username;
private String email;
- private Boolean isSuper; // 添加isSuper字段
+ private String phone;
+ private String realName;
+ private Integer status;
+ private String role;
+
}
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..7212fe1ba3b0035dd8a5e89407ab7fdc855a5c8e
--- /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,243 @@
+package com.example.user.adapter.in.web.filter;
+
+import com.example.user.adapter.in.web.service.CustomUserDetailsService;
+import com.example.user.service.application.util.JwtUtil;
+import com.example.user.service.application.service.TokenBlacklistService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * JWT认证过滤器
+ *
+ * 这个过滤器负责拦截所有HTTP请求,检查并验证JWT Token
+ * 如果Token有效,将用户认证信息设置到Spring Security上下文中
+ *
+ * 工作流程:
+ * 1. 从HTTP请求头中提取JWT Token
+ * 2. 验证Token的有效性
+ * 3. 从Token中提取用户名
+ * 4. 加载用户详情信息
+ * 5. 设置认证信息到SecurityContext
+ * 6. 继续过滤器链
+ *
+ * 注解说明:
+ * @Component - 标记为Spring组件,可以被自动注入
+ * @Slf4j - Lombok注解,自动生成日志记录器
+ * @RequiredArgsConstructor - 自动生成构造函数,注入依赖
+ */
+@Component
+@Slf4j
+@RequiredArgsConstructor
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ /**
+ * JWT工具类 - 用于Token的解析和验证
+ */
+ private final JwtUtil jwtUtil;
+
+ /**
+ * 自定义用户详情服务 - 用于加载用户信息
+ */
+ private final CustomUserDetailsService userDetailsService;
+
+ /**
+ * Token黑名单服务 - 管理已失效的JWT Token
+ * 用于解决JWT无状态特性导致的logout后token仍然有效的问题
+ */
+ private final TokenBlacklistService tokenBlacklistService;
+
+ /**
+ * HTTP请求头中JWT Token的字段名
+ * 通常格式为:Authorization: Bearer
+ */
+ private static final String AUTH_HEADER = "Authorization";
+
+ /**
+ * Token前缀,用于标识Bearer Token
+ */
+ private static final String TOKEN_PREFIX = "Bearer ";
+
+ /**
+ * 过滤器的核心方法
+ *
+ * 这个方法在每个HTTP请求到达时被调用,负责:
+ * 1. 提取和验证JWT Token
+ * 2. 设置用户认证信息
+ * 3. 继续处理请求
+ *
+ * @param request HTTP请求对象
+ * @param response HTTP响应对象
+ * @param filterChain 过滤器链,用于继续处理请求
+ * @throws ServletException 如果处理过程中发生Servlet异常
+ * @throws IOException 如果发生IO异常
+ */
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain)
+ throws ServletException, IOException {
+
+ try {
+ // 从请求中提取JWT Token
+ String jwt = extractJwtFromRequest(request);
+
+ // 如果存在Token且格式正确
+ if (StringUtils.hasText(jwt)) {
+ // 检查Token是否在黑名单中(用户已登出)
+ if (tokenBlacklistService.isBlacklisted(jwt)) {
+ log.warn("Token已在黑名单中(用户已登出): {}",
+ jwt.substring(0, Math.min(20, jwt.length())) + "...");
+ // 清除认证上下文,确保不会认证成功
+ SecurityContextHolder.clearContext();
+ // 继续过滤器链,让Spring Security返回401未授权
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ // 从Token中提取用户名
+ String username = jwtUtil.getUsernameFromToken(jwt);
+
+ // 如果成功提取到用户名且当前上下文中没有认证信息
+ if (username != null &&
+ SecurityContextHolder.getContext().getAuthentication() == null) {
+
+ // 加载用户详情信息
+ UserDetails userDetails = userDetailsService.loadUserByUsername(username);
+
+ // 验证Token的有效性
+ if (jwtUtil.validateToken(jwt, username)) {
+ // 创建认证令牌
+ UsernamePasswordAuthenticationToken authentication =
+ new UsernamePasswordAuthenticationToken(
+ userDetails,
+ null, // 密码为null,因为已经通过Token验证
+ userDetails.getAuthorities()
+ );
+
+ // 设置认证详情(如IP地址、Session ID等)
+ authentication.setDetails(
+ new WebAuthenticationDetailsSource().buildDetails(request)
+ );
+
+ // 将认证信息设置到SecurityContext中
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ log.debug("用户 {} 认证成功,权限: {}", username, userDetails.getAuthorities());
+ } else {
+ log.warn("Token验证失败: {}", jwt);
+ }
+ }
+ }
+ } catch (Exception e) {
+ log.error("JWT认证过滤器处理异常: {}", e.getMessage(), e);
+
+ // 清除认证上下文,确保安全
+ SecurityContextHolder.clearContext();
+
+ // 设置错误响应
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ response.setContentType("application/json;charset=UTF-8");
+ response.getWriter().write(
+ "{\"code\":401,\"message\":\"认证失败: " + e.getMessage() + "\",\"data\":null}"
+ );
+ return;
+ }
+
+ // 继续过滤器链的处理
+ filterChain.doFilter(request, response);
+ }
+
+ /**
+ * 从HTTP请求中提取JWT Token
+ *
+ * 这个方法从Authorization请求头中提取Bearer Token
+ * 支持格式:Authorization: Bearer
+ *
+ * @param request HTTP请求对象
+ * @return String 提取到的JWT Token,如果没有找到返回null
+ */
+ private String extractJwtFromRequest(HttpServletRequest request) {
+ // 从Authorization头中获取Token
+ String bearerToken = request.getHeader(AUTH_HEADER);
+
+ // 检查Token是否存在且格式正确
+ if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(TOKEN_PREFIX)) {
+ // 去掉"Bearer "前缀,返回纯Token
+ return bearerToken.substring(TOKEN_PREFIX.length());
+ }
+
+ // 也可以从查询参数中获取Token(可选)
+ String tokenParam = request.getParameter("token");
+ if (StringUtils.hasText(tokenParam)) {
+ return tokenParam;
+ }
+
+ // 从Cookie中获取Token(可选)
+ jakarta.servlet.http.Cookie[] cookies = request.getCookies();
+ if (cookies != null) {
+ for (jakarta.servlet.http.Cookie cookie : cookies) {
+ if ("token".equals(cookie.getName())) {
+ return cookie.getValue();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * 检查请求是否需要跳过JWT认证
+ *
+ * 这个方法可以重写以跳过某些不需要认证的请求
+ * 比如登录接口、公开API等
+ *
+ * @param request HTTP请求对象
+ * @return boolean 如果返回true,则跳过JWT认证
+ */
+ @Override
+ protected boolean shouldNotFilter(HttpServletRequest request) {
+ String path = request.getServletPath();
+
+ // 跳过登录和注册接口(已经在SecurityConfig中配置为公开接口)
+ if (path.startsWith("/users/login") || path.startsWith("/users/register")) {
+ return true;
+ }
+
+ // 测试接口需要认证,不再跳过
+ // if (path.startsWith("/users/test")) {
+ // return true;
+ // }
+
+ // 跳过API文档接口
+ if (path.startsWith("/swagger-ui") ||
+ path.startsWith("/v3/api-docs") ||
+ path.startsWith("/swagger-resources")) {
+ return true;
+ }
+
+ // 跳过监控接口
+ if (path.startsWith("/actuator")) {
+ return true;
+ }
+
+ // 跳过错误页面
+ if (path.startsWith("/error")) {
+ return true;
+ }
+
+ return false;
+ }
+}
\ 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/service/CustomUserDetailsService.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/service/CustomUserDetailsService.java
new file mode 100644
index 0000000000000000000000000000000000000000..eef9933a5c6cbc8965b9fbb643ccb156511c4429
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/service/CustomUserDetailsService.java
@@ -0,0 +1,246 @@
+package com.example.user.adapter.in.web.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.example.user.adapter.out.persistence.convertor.UserConvertor;
+import com.example.user.adapter.out.persistence.entity.UserEntity;
+import com.example.user.adapter.out.persistence.mapper.UserMapper;
+import com.example.user.service.domain.User;
+import com.example.user.service.domain.port.GetUserByNamePort;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 自定义用户详情服务
+ *
+ * 这个服务负责加载用户信息,供Spring Security进行认证和授权
+ * 实现了UserDetailsService接口,用于根据用户名加载用户详情
+ *
+ * 工作流程:
+ * 1. 根据用户名从数据库查询用户信息
+ * 2. 将用户信息转换为Spring Security的UserDetails对象
+ * 3. 设置用户的权限信息
+ * 4. 处理用户状态(启用/禁用)
+ *
+ * 注解说明:
+ * @Service - 标记为Spring服务组件
+ * @Slf4j - Lombok注解,自动生成日志记录器
+ * @RequiredArgsConstructor - 自动生成构造函数,注入依赖
+ */
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class CustomUserDetailsService implements UserDetailsService {
+
+ /**
+ * 用户Mapper - 用于数据库操作
+ */
+ private final UserMapper userMapper;
+
+
+
+ /**
+ * 根据用户名加载用户详情
+ *
+ * 这是Spring Security认证流程的核心方法,负责:
+ * 1. 从数据库查询用户信息
+ * 2. 检查用户是否存在
+ * 3. 检查用户状态是否启用
+ * 4. 构建UserDetails对象
+ *
+ * @param username 用户名
+ * @return UserDetails 用户详情对象,包含用户信息和权限
+ * @throws UsernameNotFoundException 如果用户不存在或状态异常
+ */
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+ log.info("正在加载用户详情: {}", username);
+
+ try {
+ // 从数据库查询用户信息
+ User user = getUserByUsername(username);
+
+ if (user == null) {
+ log.warn("用户不存在: {}", username);
+ throw new UsernameNotFoundException("用户不存在: " + username);
+ }
+
+ // 检查用户状态
+ if (!user.getStatus().isEnabled()) {
+ log.warn("用户已被禁用: {}", username);
+ throw new UsernameNotFoundException("用户已被禁用: " + username);
+ }
+
+ // 构建权限列表
+ Collection extends GrantedAuthority> authorities = getAuthorities(user);
+
+ // 创建Spring Security的UserDetails对象
+ return org.springframework.security.core.userdetails.User.builder()
+ .username(user.getUsername().getValue())
+ .password(user.getPassword().encryptedValue())
+ .authorities(authorities)
+ .accountExpired(false) // 账户未过期
+ .accountLocked(false) // 账户未锁定
+ .credentialsExpired(false) // 凭证未过期
+ .disabled(!user.getStatus().isEnabled()) // 账户是否禁用
+ .build();
+
+ } catch (Exception e) {
+ log.error("加载用户详情失败: {}", e.getMessage(), e);
+ throw new UsernameNotFoundException("加载用户详情失败: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 根据用户名获取用户信息
+ *
+ * 这个方法从数据库查询用户信息,使用MyBatis-Plus的查询功能
+ *
+ * @param username 用户名
+ * @return User 用户领域对象,如果不存在返回null
+ */
+ public User getUserByUsername(String username) {
+ try {
+ // 构建查询条件
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(UserEntity::getUsername, username);
+
+ // 执行查询
+ UserEntity userEntity = userMapper.selectOne(wrapper);
+
+ if (userEntity == null) {
+ log.warn("数据库中未找到用户: {}", username);
+ return null;
+ }
+
+ log.info("找到用户实体: {}", userEntity);
+
+ // 转换为领域对象
+ User user = UserConvertor.toDomain(userEntity);
+ log.info("转换为用户领域对象: {}", user);
+
+ return user;
+
+ } catch (Exception e) {
+ log.error("查询用户信息失败: {}", e.getMessage(), e);
+ return null;
+ }
+ }
+
+ /**
+ * 验证用户密码
+ *
+ * 这个方法验证用户输入的密码是否正确
+ * 使用Spring Security的密码编码器进行验证
+ *
+ * @param username 用户名
+ * @param rawPassword 原始密码(未加密)
+ * @return boolean 如果密码正确返回true,否则返回false
+ */
+ public boolean authenticate(String username, String rawPassword) {
+ try {
+ // 获取用户信息
+ User user = getUserByUsername(username);
+
+ if (user == null) {
+ log.warn("用户不存在,无法验证密码: {}", username);
+ return false;
+ }
+
+ // 验证密码
+ boolean passwordMatches = user.validatePassword(rawPassword);
+
+ if (passwordMatches) {
+ log.info("密码验证成功: {}", username);
+ } else {
+ log.warn("密码验证失败: {}", username);
+ }
+
+ return passwordMatches;
+
+ } catch (Exception e) {
+ log.error("密码验证过程出错: {}", e.getMessage(), e);
+ return false;
+ }
+ }
+
+ /**
+ * 获取用户权限
+ *
+ * 这个方法根据用户角色构建权限列表
+ * 支持的角色:ADMIN(管理员)、USER(普通用户)
+ *
+ * @param user 用户领域对象
+ * @return Collection extends GrantedAuthority> 权限集合
+ */
+ private Collection extends GrantedAuthority> getAuthorities(User user) {
+ List authorities = new ArrayList<>();
+
+ // 根据用户角色添加权限
+ if (user.getRole() != null) {
+ String role = user.getRole().getValue();
+
+ if ("ADMIN".equals(role)) {
+ // 管理员权限
+ authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
+ authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
+ authorities.add(new SimpleGrantedAuthority("user:read"));
+ authorities.add(new SimpleGrantedAuthority("user:write"));
+ authorities.add(new SimpleGrantedAuthority("user:delete"));
+ log.info("用户 {} 是管理员,拥有所有权限", user.getUsername().getValue());
+
+ } else if ("USER".equals(role)) {
+ // 普通用户权限
+ authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
+ authorities.add(new SimpleGrantedAuthority("user:read"));
+ authorities.add(new SimpleGrantedAuthority("user:write:self"));
+ log.info("用户 {} 是普通用户,拥有基本权限", user.getUsername().getValue());
+
+ } else {
+ // 默认权限(如果没有指定角色)
+ authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
+ authorities.add(new SimpleGrantedAuthority("user:read"));
+ log.warn("用户 {} 的角色 {} 未知,分配默认权限", user.getUsername().getValue(), role);
+ }
+ } else {
+ // 如果没有角色信息,分配默认权限
+ authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
+ authorities.add(new SimpleGrantedAuthority("user:read"));
+ log.warn("用户 {} 没有角色信息,分配默认权限", user.getUsername().getValue());
+ }
+
+ return authorities;
+ }
+
+ /**
+ * 检查用户是否存在
+ *
+ * 这个方法检查指定用户名的用户是否存在
+ *
+ * @param username 用户名
+ * @return boolean 如果用户存在返回true,否则返回false
+ */
+ public boolean userExists(String username) {
+ try {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(UserEntity::getUsername, username);
+
+ Long count = userMapper.selectCount(wrapper);
+ return count != null && count > 0;
+
+ } catch (Exception e) {
+ log.error("检查用户是否存在失败: {}", e.getMessage(), e);
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/bridge/GetUserByNameBridge.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/bridge/GetUserByNameBridge.java
index 38dc3ea5a32a0f658d80085de97fb59d19db3827..0d3e6e67b5f50d2c1ff79ace1d85b59c50ad38c7 100644
--- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/bridge/GetUserByNameBridge.java
+++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/bridge/GetUserByNameBridge.java
@@ -16,9 +16,9 @@ public class GetUserByNameBridge implements GetUserByNamePort {
@Resource
private UserMapper userMapper;
@Override
- public User getUserByName(String name) {
+ public User getUserByName(String username) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(UserEntity::getName, name);
+ wrapper.eq(UserEntity::getUsername, username);
UserEntity userEntity = userMapper.selectOne(wrapper);
//password不空
log.info("userEntity: {}", userEntity);
diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/convertor/UserConvertor.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/convertor/UserConvertor.java
index cc56ab567d7dd43b0fd158c989ea8b8069c882fb..548b94a725ee4c16ca54e34597b9d6318f41a0e5 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
@@ -22,20 +22,35 @@ public class UserConvertor {
);
}
- /**
- * 将领域对象转换为持久化实体
- * @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.getUsername()),
+ Password.fromEncrypted(userEntity.getPassword()),
+ new Email(userEntity.getEmail()),
+ userEntity.getPhone() != null ? new Phone(userEntity.getPhone()) : null,
+ userEntity.getRealName() != null ? new RealName(userEntity.getRealName()) : null,
+ new UserStatus(userEntity.getStatus()),
+ new UserRole(userEntity.getRole()),
+ new CreateTime(userEntity.getCreateTime()),
+ new UpdateTime(userEntity.getUpdateTime()),
+ userEntity.getLastLoginTime() != null ? new LastLoginTime(userEntity.getLastLoginTime()) : null
+ );
+ }
+
+ public static UserEntity toEntity(User user) {
+ return new UserEntity(
+ user.getId() != null ? user.getId().getValue() : null,
+ user.getUsername() != null ? user.getUsername().getValue() : null,
+ user.getPassword() != null ? user.getPassword().encryptedValue() : null,
+ user.getEmail() != null ? user.getEmail().getValue() : null,
+ user.getPhone() != null ? user.getPhone().getValue() : null,
+ user.getRealName() != null ? user.getRealName().getValue() : null,
+ user.getStatus() != null ? user.getStatus().getValue() : 1,
+ user.getRole() != null ? user.getRole().getValue() : "USER",
+ user.getCreateTime() != null ? user.getCreateTime().getValue() : null,
+ user.getUpdateTime() != null ? user.getUpdateTime().getValue() : null,
+ user.getLastLoginTime() != null ? user.getLastLoginTime().getValue() : null
+ );
+ }
}
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..65df8392bb01aa9a69e52a2346ddad35962795ae 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
@@ -1,27 +1,42 @@
package com.example.user.adapter.out.persistence.entity;
import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
+import java.time.LocalDateTime;
+
@Data
@AllArgsConstructor
@NoArgsConstructor
-@TableName("user")
+@TableName("sys_user")
public class UserEntity {
- @TableId(type= IdType.ASSIGN_ID)
- private long id;
- private String name;
- private Integer age;
- private String email;
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ @TableField("username")
+ private String username;
+
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;
+
+ @TableField("real_name")
+ private String realName;
+
+ private Integer status;
+ private String role;
+
+ @TableField("create_time")
+ private LocalDateTime createTime;
+
+ @TableField("update_time")
+ private LocalDateTime updateTime;
+
+ @TableField("last_login_time")
+ private LocalDateTime lastLoginTime;
}
diff --git a/user-service/user-service-application/pom.xml b/user-service/user-service-application/pom.xml
index df05045dc789c7f49514ce9c6c7c90a6a664231f..1e0b8b6dfe3940f02ba38fbda28200301dd0ec97 100644
--- a/user-service/user-service-application/pom.xml
+++ b/user-service/user-service-application/pom.xml
@@ -36,6 +36,25 @@
user-service-domain
0.0.1-SNAPSHOT
+
+
+
+ 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-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..3474d03f4e9a26e2c0af7458d19d660b8c0e972c 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,10 @@ import lombok.Builder;
@Builder
public record CreateUserCommand(
Long id,
- String name,
- Integer age,
+ String username,
+ String password,
String email,
- String password
+ String phone,
+ String realName
) {
}
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..719e6771679d6b654316e8a5f00b7175a64244b6 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
@@ -4,7 +4,10 @@ import lombok.Builder;
@Builder
public record UpdateUserCommand(Long id,
- String name,
- Integer age,
- String email) {
+ String username,
+ String email,
+ String phone,
+ String realName,
+ Integer status,
+ String role) {
}
diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/command/UserLoginCommand.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/command/UserLoginCommand.java
index f5132799d94db877dc88c9e60b37a31023929e74..7b9a6609f5b6da2f66f91318261a805c551f8860 100644
--- a/user-service/user-service-application/src/main/java/com/example/user/service/application/command/UserLoginCommand.java
+++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/command/UserLoginCommand.java
@@ -3,5 +3,5 @@ package com.example.user.service.application.command;
import lombok.Builder;
@Builder
-public record UserLoginCommand(String name, String password) {
+public record UserLoginCommand(String username, String password) {
}
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..fb2c65b66de624cfde010ba36f27f50301805c88 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
@@ -6,8 +6,11 @@ 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.Phone;
+import com.example.user.service.domain.valueobject.RealName;
import com.example.user.service.domain.valueobject.UserName;
+import com.example.user.service.domain.valueobject.UserRole;
+import com.example.user.service.domain.valueobject.UserStatus;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -17,17 +20,20 @@ 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 = new User(
+ new UserName(createUserCommand.username()),
+ Password.fromRaw(createUserCommand.password()),
new Email(createUserCommand.email()),
-// new Password( createUserCommand.password())
- Password.fromRaw(createUserCommand.password())
+ createUserCommand.phone() != null ? new Phone(createUserCommand.phone()) : null,
+ createUserCommand.realName() != null ? new RealName(createUserCommand.realName()) : null,
+ new UserStatus(1), // 默认启用状态
+ new UserRole("USER") // 默认用户角色
);
- log.info("user:{}",user);
+ log.info("user:{}", user);
return createUserPort.createUser(user);
}
}
diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/TokenBlacklistService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/TokenBlacklistService.java
new file mode 100644
index 0000000000000000000000000000000000000000..dd29b304dc062e075209b3128e0162482a52c478
--- /dev/null
+++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/TokenBlacklistService.java
@@ -0,0 +1,224 @@
+package com.example.user.service.application.service;
+
+import com.example.user.service.application.util.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 = 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);
+ }
+
+ /**
+ * 从令牌中获取过期时间的私有方法
+ *
+ * 由于当前JwtUtil没有公开的getExpirationDateFromToken方法,
+ * 这里提供一个简化的实现,基于令牌的过期时间配置
+ *
+ * @param token JWT令牌字符串
+ * @return Date 令牌的过期时间
+ */
+ private Date getExpirationDateFromToken(String token) {
+ try {
+ // 简化实现:基于当前时间和配置的过期时间计算
+ // 在实际项目中,应该解析JWT令牌获取真实的过期时间
+ return new Date(System.currentTimeMillis() + jwtUtil.expiration);
+ } catch (Exception e) {
+ log.error("获取令牌过期时间失败: {}", e.getMessage());
+ // 返回一个默认的过期时间(当前时间 + 1天)
+ return new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000);
+ }
+ }
+}
\ No newline at end of file
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..4cf1ec17ca3f59082d08f85b23602d8c950341dd 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
@@ -5,9 +5,12 @@ import com.example.user.service.application.port.in.UpdateUserUseCase;
import com.example.user.service.domain.User;
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.Phone;
+import com.example.user.service.domain.valueobject.RealName;
import com.example.user.service.domain.valueobject.UserId;
import com.example.user.service.domain.valueobject.UserName;
+import com.example.user.service.domain.valueobject.UserRole;
+import com.example.user.service.domain.valueobject.UserStatus;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -20,9 +23,17 @@ public class UpdateUserService implements UpdateUserUseCase {
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()));
+ new UserName(command.username()),
+ null, // 密码不通过更新接口修改
+ new Email(command.email()),
+ command.phone() != null ? new Phone(command.phone()) : null,
+ command.realName() != null ? new RealName(command.realName()) : null,
+ command.status() != null ? new UserStatus(command.status()) : null,
+ command.role() != null ? new UserRole(command.role()) : null,
+ null, // createTime不通过更新接口修改
+ null, // updateTime不通过更新接口修改
+ null // lastLoginTime不通过更新接口修改
+ );
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..b18a9399b30e9960efa227ddaa35856b9e3995ee 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
@@ -2,7 +2,7 @@ package com.example.user.service.application.service;
import com.example.user.service.application.command.UserLoginCommand;
import com.example.user.service.application.port.in.UserLoginUseCase;
-import com.example.user.service.common.JwtUtil;
+import com.example.user.service.application.util.JwtUtil;
import com.example.user.service.domain.User;
import com.example.user.service.domain.port.GetUserByNamePort;
import jakarta.annotation.Resource;
@@ -15,11 +15,14 @@ public class UserLoginService implements UserLoginUseCase {
@Resource
private GetUserByNamePort getUserByNamePort;
+
+ @Resource
+ private JwtUtil jwtUtil;
@Override
public String login(UserLoginCommand userLoginCommand) {
//验证用户
- User user = User.getUserByName(userLoginCommand.name(), getUserByNamePort);
+ User user = User.getUserByName(userLoginCommand.username(), getUserByNamePort);
log.info("user:{}", user);
if(user==null){
throw new RuntimeException("用户不存在");
@@ -28,13 +31,21 @@ 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()
+ //签发token
+ /**
+ * @author yangwenkai
+ */
+ // 使用JwtUtil生成JWT令牌,包含用户信息
+ // 根据用户角色判断是否是超级用户
+ boolean isSuperUser = user.getRole().isAdmin();
+
+ String token = jwtUtil.generateToken(
+ user.getId().getValue(), // 用户ID
+ user.getUsername().getValue(), // 用户名
+ isSuperUser // 是否是超级用户
);
- log.info("生成的JWT令牌: {}", token);
+
+ log.info("用户登录成功,生成JWT令牌: {}", token);
return token;
}
}
diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/util/JwtUtil.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/util/JwtUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..9d0bc15146b9f4dc9063afacf5b050c75f5e3e1f
--- /dev/null
+++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/util/JwtUtil.java
@@ -0,0 +1,376 @@
+package com.example.user.service.application.util;
+
+import io.jsonwebtoken.*;
+import io.jsonwebtoken.security.Keys;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * JWT工具类
+ *
+ * 这个类负责JWT Token的生成、解析和验证,包括:
+ * 1. 生成JWT Token
+ * 2. 从Token中提取用户名
+ * 3. 验证Token的有效性
+ * 4. 刷新Token
+ *
+ * JWT(JSON Web Token)是一种安全的身份验证方式,包含三部分:
+ * 1. Header(头部)- 包含算法和类型信息
+ * 2. Payload(载荷)- 包含用户信息和声明
+ * 3. Signature(签名)- 用于验证Token的真实性
+ *
+ * 注解说明:
+ * @Component - 标记为Spring组件,可以被自动注入
+ * @Slf4j - Lombok注解,自动生成日志记录器
+ */
+@Component
+@Slf4j
+public class JwtUtil {
+
+ /**
+ * JWT密钥 - 用于签名和验证Token
+ * 从配置文件application.yml中读取,默认值为"mySecretKey123456789012345678901234567890"
+ * 密钥长度至少32位,建议使用更长的随机字符串
+ * HS512算法需要至少512位(64字节)的密钥
+ */
+ @Value("${jwt.secret:mySecretKey123456789012345678901234567890123456789012345678901234567890}")
+ private String secret;
+
+ /**
+ * Token过期时间(毫秒)
+ * 从配置文件application.yml中读取,默认值为7天(604800000毫秒)
+ */
+ @Value("${jwt.expiration:604800000}")
+ public Long expiration;
+
+ /**
+ * 生成安全的密钥对象
+ *
+ * 这个方法将字符串密钥转换为安全的SecretKey对象
+ * 使用HMAC-SHA算法进行签名
+ *
+ * @return SecretKey 安全的密钥对象
+ */
+ private SecretKey getSigningKey() {
+ return Keys.hmacShaKeyFor(secret.getBytes());
+ }
+
+ /**
+ * 生成JWT Token
+ *
+ * 这个方法根据用户名生成JWT Token,包含:
+ * 1. 主题(subject)- 用户名
+ * 2. 签发时间(issuedAt)- 当前时间
+ * 3. 过期时间(expiration)- 当前时间 + 过期时长
+ * 4. 签名(signature)- 使用密钥进行签名
+ *
+ * @param username 用户名,将作为Token的主题
+ * @return String 生成的JWT Token字符串
+ */
+ public String generateToken(String username) {
+ Map claims = new HashMap<>();
+ return createToken(claims, username);
+ }
+
+ /**
+ * 创建Token的内部方法
+ *
+ * 这个方法实际构建JWT Token,设置各种声明和签名
+ *
+ * @param claims 额外的声明信息(可以存放用户角色、权限等)
+ * @param subject Token的主题,通常是用户名
+ * @return String 生成的JWT Token
+ */
+ private String createToken(Map claims, String subject) {
+ return Jwts.builder()
+ // 设置额外的声明信息
+ .setClaims(claims)
+ // 设置主题(通常是用户名)
+ .setSubject(subject)
+ // 设置签发时间
+ .setIssuedAt(new Date(System.currentTimeMillis()))
+ // 设置过期时间
+ .setExpiration(new Date(System.currentTimeMillis() + expiration))
+ // 使用密钥进行签名
+ .signWith(getSigningKey(), SignatureAlgorithm.HS512)
+ // 压缩Token(可选)
+ .compact();
+ }
+
+ /**
+ * 从Token中提取用户名
+ *
+ * 这个方法解析JWT Token,提取其中的主题(subject)作为用户名
+ *
+ * @param token JWT Token字符串
+ * @return String 从Token中提取的用户名
+ */
+ public String getUsernameFromToken(String token) {
+ try {
+ // 解析Token并获取主题(用户名)
+ return Jwts.parserBuilder()
+ .setSigningKey(getSigningKey())
+ .build()
+ .parseClaimsJws(token)
+ .getBody()
+ .getSubject();
+ } catch (Exception e) {
+ log.error("从Token中提取用户名失败", e);
+ return null;
+ }
+ }
+
+ /**
+ * 验证JWT Token的有效性
+ *
+ * 这个方法检查Token是否:
+ * 1. 格式正确
+ * 2. 签名有效
+ * 3. 没有过期
+ * 4. 包含的用户名与预期一致
+ *
+ * @param token 需要验证的JWT Token
+ * @param username 预期的用户名
+ * @return boolean 如果Token有效返回true,否则返回false
+ */
+ public boolean validateToken(String token, String username) {
+ try {
+ // 解析Token获取用户名
+ String extractedUsername = getUsernameFromToken(token);
+
+ // 检查用户名是否匹配且Token未过期
+ return (extractedUsername != null &&
+ extractedUsername.equals(username) &&
+ !isTokenExpired(token));
+ } catch (Exception e) {
+ log.warn("Token验证失败: {}", e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 检查Token是否过期
+ *
+ * 这个方法检查Token的过期时间是否在当前时间之前
+ *
+ * @param token JWT Token
+ * @return boolean 如果Token已过期返回true,否则返回false
+ */
+ private boolean isTokenExpired(String token) {
+ try {
+ // 获取Token的过期时间
+ Date expirationDate = getExpirationDateFromToken(token);
+
+ // 检查是否过期
+ return expirationDate.before(new Date());
+ } catch (Exception e) {
+ log.error("检查Token过期状态失败", e);
+ return true; // 如果解析失败,视为过期
+ }
+ }
+
+ /**
+ * 从Token中获取过期时间
+ *
+ * 这个方法解析Token并提取其中的过期时间
+ *
+ * @param token JWT Token
+ * @return Date Token的过期时间
+ */
+ public Date getExpirationDateFromToken(String token) {
+ try {
+ return Jwts.parserBuilder()
+ .setSigningKey(getSigningKey())
+ .build()
+ .parseClaimsJws(token)
+ .getBody()
+ .getExpiration();
+ } catch (ExpiredJwtException e) {
+ // 如果Token已过期,仍然返回过期时间
+ return e.getClaims().getExpiration();
+ } catch (Exception e) {
+ log.error("从Token中获取过期时间失败", e);
+ return new Date(0); // 返回一个很早的日期表示无效
+ }
+ }
+
+ /**
+ * 刷新JWT Token
+ *
+ * 这个方法根据旧的Token生成一个新的Token
+ * 新的Token使用相同的用户信息,但更新了签发时间和过期时间
+ *
+ * @param token 旧的JWT Token
+ * @return String 新的JWT Token,如果刷新失败返回null
+ */
+ public String refreshToken(String token) {
+ try {
+ // 从旧Token中提取用户名
+ String username = getUsernameFromToken(token);
+
+ if (username != null) {
+ // 生成新的Token
+ return generateToken(username);
+ }
+ return null;
+ } catch (Exception e) {
+ log.error("刷新Token失败", e);
+ return null;
+ }
+ }
+
+ /**
+ * 获取Token的剩余有效时间(毫秒)
+ *
+ * 这个方法计算Token还有多少毫秒会过期
+ *
+ * @param token JWT Token
+ * @return long 剩余有效时间(毫秒),如果无效返回-1
+ */
+ public long getRemainingMillis(String token) {
+ try {
+ Date expirationDate = getExpirationDateFromToken(token);
+ return expirationDate.getTime() - System.currentTimeMillis();
+ } catch (Exception e) {
+ log.error("获取Token剩余时间失败", e);
+ return -1;
+ }
+ }
+
+ /**
+ * 生成包含用户ID、用户名和角色信息的JWT Token
+ *
+ * @param userId 用户ID
+ * @param username 用户名
+ * @param isSuperUser 是否是超级用户
+ * @return String 生成的JWT Token
+ */
+ public String generateToken(long userId, String username, boolean isSuperUser) {
+ Map claims = new HashMap<>();
+ claims.put("userId", userId);
+ claims.put("isSuperUser", isSuperUser);
+
+ return createToken(claims, username);
+ }
+
+ /**
+ * 从Token中获取Claims(载荷信息)
+ *
+ * 这个方法解析JWT Token并返回其中包含的所有声明信息
+ * Claims包含了用户信息、过期时间、签发时间等数据
+ *
+ * @param token JWT Token字符串
+ * @return Claims Token中的载荷信息
+ */
+ public Claims getClaimsFromToken(String token) {
+ try {
+ return Jwts.parserBuilder()
+ .setSigningKey(getSigningKey())
+ .build()
+ .parseClaimsJws(token)
+ .getBody();
+ } catch (ExpiredJwtException e) {
+ // 即使Token过期,也返回Claims信息
+ return e.getClaims();
+ } catch (Exception e) {
+ log.error("解析Token获取Claims失败: {}", e.getMessage());
+ throw new RuntimeException("无效的JWT Token", e);
+ }
+ }
+
+ /**
+ * 从HTTP请求头中提取JWT Token
+ *
+ * 这个方法从HTTP Authorization头中提取Bearer Token
+ * 标准的Authorization头格式为:"Bearer "
+ *
+ * 使用场景:
+ * - 在过滤器中提取Token进行身份验证
+ * - 在控制器中获取当前用户的Token
+ * - 在服务层中处理需要Token的业务逻辑
+ *
+ * 安全考虑:
+ * - 只接受Bearer类型的Token,拒绝其他格式
+ * - 对Token格式进行严格验证
+ * - 不在日志中记录完整的Token内容
+ *
+ * @param authorizationHeader HTTP Authorization头的值
+ * @return String 提取的JWT Token,如果格式不正确返回null
+ */
+ public String extractTokenFromHeader(String authorizationHeader) {
+ // 检查Authorization头是否存在且不为空
+ if (authorizationHeader == null || authorizationHeader.trim().isEmpty()) {
+ return null;
+ }
+
+ // 检查是否以"Bearer "开头(注意Bearer后面有一个空格)
+ if (authorizationHeader.startsWith("Bearer ")) {
+ // 提取Bearer后面的Token部分
+ String token = authorizationHeader.substring(7); // "Bearer "长度为7
+
+ // 检查提取的Token是否为空
+ if (token.trim().isEmpty()) {
+ return null;
+ }
+
+ return token;
+ }
+
+ // 如果不是Bearer格式,返回null
+ return null;
+ }
+
+ /**
+ * 从Token中获取用户ID
+ *
+ * 这个方法从JWT Token的Claims中提取用户ID信息
+ *
+ * @param token JWT Token字符串
+ * @return Long 用户ID,如果获取失败返回null
+ */
+ public Long getUserIdFromToken(String token) {
+ try {
+ Claims claims = getClaimsFromToken(token);
+ Object userIdObj = claims.get("userId");
+
+ if (userIdObj instanceof Number) {
+ return ((Number) userIdObj).longValue();
+ }
+
+ return null;
+ } catch (Exception e) {
+ log.error("从Token中获取用户ID失败: {}", e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * 从Token中获取用户角色信息
+ *
+ * 这个方法从JWT Token的Claims中提取用户是否为超级用户的信息
+ *
+ * @param token JWT Token字符串
+ * @return Boolean 是否为超级用户,如果获取失败返回false
+ */
+ public Boolean isSuperUserFromToken(String token) {
+ try {
+ Claims claims = getClaimsFromToken(token);
+ Object isSuperUserObj = claims.get("isSuperUser");
+
+ if (isSuperUserObj instanceof Boolean) {
+ return (Boolean) isSuperUserObj;
+ }
+
+ return false;
+ } catch (Exception e) {
+ log.error("从Token中获取用户角色失败: {}", e.getMessage());
+ return false;
+ }
+ }
+}
\ No newline at end of file
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..255a08804fcf778cb7692da76ab945cf3a18a87e 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,41 +18,65 @@ import java.util.List;
@ToString
public class User {
private UserId id;
- private UserName name;
- private UserAge age;
- private Email email;
+ private UserName username;
private Password password;
- private IsSuper isSuper; // 添加isSuper字段
+ private Email email;
+ private Phone phone;
+ private RealName realName;
+ private UserStatus status;
+ private UserRole role;
+ private CreateTime createTime;
+ private UpdateTime updateTime;
+ private LastLoginTime lastLoginTime;
+
public User() {
}
- public User(UserId id, UserName name, UserAge age, Email email, Password password,IsSuper isSuper) {
+ public User(UserId id, UserName username, Password password, Email email, Phone phone,
+ RealName realName, UserStatus status, UserRole role, CreateTime createTime,
+ UpdateTime updateTime, LastLoginTime lastLoginTime) {
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;
+ this.createTime = createTime;
+ this.updateTime = updateTime;
+ this.lastLoginTime = lastLoginTime;
}
- public User( UserName name, UserAge age, Email email, Password password) {
- this.id= genId() ;
- this.name = name;
- this.age = age;
- this.email = email;
+ public User(UserName username, Password password, Email email, Phone phone,
+ RealName realName, UserStatus status, UserRole role) {
+ this.id = genId();
+ this.username = username;
this.password = password;
- this.isSuper = new IsSuper(false);
+ this.email = email;
+ this.phone = phone;
+ this.realName = realName;
+ this.status = status;
+ this.role = role;
+ this.createTime = new CreateTime(LocalDateTime.now());
+ this.updateTime = new UpdateTime(LocalDateTime.now());
+ this.lastLoginTime = null;
}
- public User(UserId userId, UserName userName, UserAge userAge, Email email) {
- this.id = id;
- this.name = name;
- this.age = age;
+ public User(UserId userId, UserName userName, Email email) {
+ this.id = userId;
+ this.username = userName;
this.email = email;
- this.isSuper = new IsSuper(false);
+ this.password = null;
+ this.phone = null;
+ this.realName = null;
+ this.status = new UserStatus(1); // 默认启用状态
+ this.role = new UserRole("USER"); // 默认用户角色
+ this.createTime = new CreateTime(LocalDateTime.now());
+ this.updateTime = new UpdateTime(LocalDateTime.now());
+ this.lastLoginTime = null;
}
-
public static List getUsers(GetUserListPort getUserListPort){
return getUserListPort.getUsers();
}
@@ -60,12 +85,12 @@ public class User {
* 根据用户名查询用户
* 当需要使用类似GetUserByNamePort这种对象的时候,需要在方法参数注入该对象
* 因为通过构造方法或者字段注入都会失败,因为方法是静态方法,会早于对象创建,导致对象无法注入
- * @param name 用户名
+ * @param username 用户名
* @param getUserByNamePort 查询用户的端口
* @return 用户模型
*/
- public static User getUserByName(String name, GetUserByNamePort getUserByNamePort){
- User user = getUserByNamePort.getUserByName(name);
+ public static User getUserByName(String username, GetUserByNamePort getUserByNamePort){
+ User user = getUserByNamePort.getUserByName(username);
log.info("user:{}", user);
return user;
}
diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/port/GetUserByNamePort.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/port/GetUserByNamePort.java
index 599d5ab921037d14bef8fd463e7a638f416c5158..3f5cbea71a2197e9daa1e21186314a381d6aa7b8 100644
--- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/port/GetUserByNamePort.java
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/port/GetUserByNamePort.java
@@ -3,5 +3,5 @@ package com.example.user.service.domain.port;
import com.example.user.service.domain.User;
public interface GetUserByNamePort {
- User getUserByName(String name );
+ User getUserByName(String username);
}
diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/CreateTime.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/CreateTime.java
new file mode 100644
index 0000000000000000000000000000000000000000..1cfbf8a5267e037c9950eb8aaa451408afe15652
--- /dev/null
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/CreateTime.java
@@ -0,0 +1,15 @@
+package com.example.user.service.domain.valueobject;
+
+import java.time.LocalDateTime;
+
+/**
+ * 创建时间值对象
+ * 表示用户的创建时间
+ *
+ * @param createTime 创建时间
+ */
+public record CreateTime(LocalDateTime createTime) {
+ public LocalDateTime getValue() {
+ return createTime;
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/LastLoginTime.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/LastLoginTime.java
new file mode 100644
index 0000000000000000000000000000000000000000..be47dbb4115a3ce59819f1e5c727be3985ee8771
--- /dev/null
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/LastLoginTime.java
@@ -0,0 +1,15 @@
+package com.example.user.service.domain.valueobject;
+
+import java.time.LocalDateTime;
+
+/**
+ * 最后登录时间值对象
+ * 表示用户的最后登录时间
+ *
+ * @param lastLoginTime 最后登录时间
+ */
+public record LastLoginTime(LocalDateTime lastLoginTime) {
+ public LocalDateTime getValue() {
+ return lastLoginTime;
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Password.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Password.java
index 57d9be7020b4d75f261b1aa30f2da10dc40b657d..5fbaea15e4a7b139f4973f7734a1cff2c28e8da2 100644
--- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Password.java
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Password.java
@@ -4,36 +4,138 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
- *
- * @param encryptedValue 密码 明文密码还是密文密码?->密文
+ * 密码值对象
+ * 负责密码的验证、加密和存储
+ *
+ * @param encryptedValue 加密后的密码值
*/
@Slf4j
public record Password(String encryptedValue) {
private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
+ /**
+ * 从原始密码创建Password对象
+ * 会先验证密码复杂度,然后进行加密
+ * @author qiuyujie
+ * @param rawPassword 原始密码
+ * @return 加密后的Password对象
+ * @throws IllegalArgumentException 如果密码不符合复杂度要求
+ */
public static Password fromRaw(String rawPassword) {
- if (rawPassword == null || rawPassword.length() < 6) {
- throw new RuntimeException("密码长度至少6位");
- }
-
- log.info("密码明文: {},密码密文{}", rawPassword,encoder.encode(rawPassword));
- return new Password(encoder.encode(rawPassword));
+ validatePasswordComplexity(rawPassword);
+ String encryptedPassword = encoder.encode(rawPassword);
+ log.info("密码明文: {},密码密文{}", rawPassword, encryptedPassword);
+ return new Password(encryptedPassword);
}
/**
* 从已加密的密码创建Password对象
* 用于从持久化层恢复数据
+ * @author qiuyujie
+ * @param encryptedPassword 已加密的密码
+ * @return Password对象
*/
public static Password fromEncrypted(String encryptedPassword) {
return new Password(encryptedPassword);
}
+ /**
+ * 验证原始密码是否与加密密码匹配
+ * @author dengli
+ * @param rawPassword 原始密码
+ * @return 是否匹配
+ */
public boolean verify(String rawPassword) {
return encoder.matches(rawPassword, encryptedValue);
}
- // 用于密码重置
+ /**
+ * 用于密码重置
+ * @author dengli
+ * @param encryptedValue 加密后的密码值
+ * @return Password对象
+ */
public static Password of(String encryptedValue) {
return new Password(encryptedValue);
}
+
+ /**
+ * 验证密码长度
+ * @author wangqingqing
+ * @param password 待验证的密码
+ * @throws IllegalArgumentException 如果密码长度不符合要求
+ */
+ public static void validatePasswordLength(String password) {
+ if (password == null) {
+ throw new IllegalArgumentException("密码不能为空");
+ }
+ if (password.length() < 6) {
+ throw new IllegalArgumentException("密码长度不足,至少需要6个字符");
+ }
+ if (password.length() > 20) {
+ throw new IllegalArgumentException("密码长度过长,最多允许20个字符");
+ }
+ }
+
+ /**
+ * 验证密码复杂度
+ * @@author wangqingqing
+ * @param password 待验证的密码
+ * @throws IllegalArgumentException 根据具体缺失的元素提供详细的错误信息
+ */
+ public static void validatePasswordComplexity(String password) {
+ if (password == null) {
+ throw new IllegalArgumentException("密码不能为空");
+ }
+
+ boolean hasLetter = password.matches(".*[A-Za-z].*");
+ boolean hasDigit = password.matches(".*\\d.*");
+
+ if (!hasLetter && !hasDigit) {
+ throw new IllegalArgumentException("密码必须包含至少一个字母和一个数字");
+ }
+ if (!hasLetter) {
+ throw new IllegalArgumentException("密码必须包含至少一个字母");
+ }
+ if (!hasDigit) {
+ throw new IllegalArgumentException("密码必须包含至少一个数字");
+ }
+
+ // 额外的复杂度检查:特殊字符(可选)
+ boolean hasSpecialChar = password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?].*");
+ if (!hasSpecialChar) {
+ // 这里只是记录,不抛出异常,因为特殊字符不是强制要求
+ // 可以根据需要调整为强制要求
+ }
+ }
+
+ /**
+ * 验证密码和确认密码是否一致
+ * @author lizishan
+ * @param password 密码
+ * @param rePassword 确认密码
+ * @throws IllegalArgumentException 根据具体情况提供详细的错误信息
+ */
+ public static void validatePasswordMatch(String password, String rePassword) {
+ if (rePassword == null) {
+ throw new IllegalArgumentException("确认密码不能为空");
+ }
+ if (!password.equals(rePassword)) {
+ throw new IllegalArgumentException("密码和确认密码不匹配,请重新输入");
+ }
+ }
+
+ /**
+ * 完整的密码验证流程
+ * 包括密码长度、复杂度以及密码和确认密码的一致性
+ * @author lizishan
+ * @param password 密码
+ * @param rePassword 确认密码
+ * @throws IllegalArgumentException 如果密码不符合要求
+ */
+ public static void validatePassword(String password, String rePassword) {
+ validatePasswordLength(password);
+ validatePasswordComplexity(password);
+ validatePasswordMatch(password, rePassword);
+ }
}
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..be68d97a3a063755b4f6b55f0829507ec59ea69a
--- /dev/null
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Phone.java
@@ -0,0 +1,26 @@
+package com.example.user.service.domain.valueobject;
+
+/**
+ * 手机号值对象
+ * 负责手机号的格式验证和存储
+ *
+ * @param phone 手机号字符串
+ */
+public record Phone(String phone) {
+ public String getValue() {
+ return phone;
+ }
+
+ /**
+ * 验证手机号格式
+ * 简单的手机号格式验证(中国手机号)
+ */
+ public static void validatePhoneFormat(String phone) {
+ if (phone == null) {
+ throw new IllegalArgumentException("手机号不能为空");
+ }
+ if (!phone.matches("^1[3-9]\\d{9}$")) {
+ throw new IllegalArgumentException("手机号格式不正确");
+ }
+ }
+}
\ 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..23b7f3d8e4d294c4a8972d7a08a0ea3da02031de
--- /dev/null
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/RealName.java
@@ -0,0 +1,26 @@
+package com.example.user.service.domain.valueobject;
+
+/**
+ * 真实姓名的值对象
+ * 负责真实姓名的验证和存储
+ *
+ * @param realName 真实姓名
+ */
+public record RealName(String realName) {
+ public String getValue() {
+ return realName;
+ }
+
+ /**
+ * 验证真实姓名格式
+ * 简单的姓名格式验证(2-10个中文字符)
+ */
+ public static void validateRealNameFormat(String realName) {
+ if (realName == null) {
+ throw new IllegalArgumentException("真实姓名不能为空");
+ }
+ if (!realName.matches("^[\\u4e00-\\u9fa5]{2,10}$")) {
+ throw new IllegalArgumentException("真实姓名格式不正确,应为2-10个中文字符");
+ }
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UpdateTime.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UpdateTime.java
new file mode 100644
index 0000000000000000000000000000000000000000..5037d37949ca1f2081fa0d140715846ba27f7c8d
--- /dev/null
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UpdateTime.java
@@ -0,0 +1,15 @@
+package com.example.user.service.domain.valueobject;
+
+import java.time.LocalDateTime;
+
+/**
+ * 更新时间值对象
+ * 表示用户的最后更新时间
+ *
+ * @param updateTime 更新时间
+ */
+public record UpdateTime(LocalDateTime updateTime) {
+ public LocalDateTime getValue() {
+ return updateTime;
+ }
+}
\ 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..c1948f674963b3838a034e1c08ba807c0cd5c437
--- /dev/null
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserRole.java
@@ -0,0 +1,39 @@
+package com.example.user.service.domain.valueobject;
+
+/**
+ * 用户角色值对象
+ * 表示用户的角色:ADMIN-管理员,USER-普通用户
+ *
+ * @param role 角色字符串
+ */
+public record UserRole(String role) {
+ public String getValue() {
+ return role;
+ }
+
+ /**
+ * 验证角色值是否有效
+ */
+ public static void validateRole(String role) {
+ if (role == null) {
+ throw new IllegalArgumentException("用户角色不能为空");
+ }
+ if (!"ADMIN".equals(role) && !"USER".equals(role)) {
+ throw new IllegalArgumentException("用户角色无效,只能是'ADMIN'或'USER'");
+ }
+ }
+
+ /**
+ * 判断用户是否是管理员
+ */
+ public boolean isAdmin() {
+ return "ADMIN".equals(role);
+ }
+
+ /**
+ * 判断用户是否是普通用户
+ */
+ public boolean isUser() {
+ return "USER".equals(role);
+ }
+}
\ 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..728db919c97525840cf9bb876561e9ade54b04e0
--- /dev/null
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserStatus.java
@@ -0,0 +1,36 @@
+package com.example.user.service.domain.valueobject;
+
+/**
+ * 用户状态值对象
+ * 表示用户的状态:0-禁用,1-启用
+ *
+ * @param status 状态值
+ */
+public record UserStatus(int status) {
+ public int getValue() {
+ return status;
+ }
+
+ /**
+ * 验证状态值是否有效
+ */
+ public static void validateStatus(int status) {
+ if (status != 0 && status != 1) {
+ throw new IllegalArgumentException("用户状态值无效,只能是0(禁用)或1(启用)");
+ }
+ }
+
+ /**
+ * 判断用户是否启用
+ */
+ public boolean isEnabled() {
+ return status == 1;
+ }
+
+ /**
+ * 判断用户是否禁用
+ */
+ public boolean isDisabled() {
+ return status == 0;
+ }
+}
\ No newline at end of file