qas = Qa.getQas(getQaListPort);
+ return qas;
+ }
+}
diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/UpdateQaService.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/UpdateQaService.java
new file mode 100644
index 0000000000000000000000000000000000000000..3a40bb63ec8ef3b41875c5b3bd25f040e758bcbe
--- /dev/null
+++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/UpdateQaService.java
@@ -0,0 +1,28 @@
+package com.example.qa.service.application.service;
+/**
+ * @author zhujunjie
+ * @param
+ * @return
+ */
+import com.example.qa.service.application.command.UpdateQaCommand;
+import com.example.qa.service.application.port.in.UpdateQaUseCase;
+import com.example.qa.service.domain.Qa;
+import com.example.qa.service.domain.port.UpdateQaPort;
+import com.example.qa.service.domain.valueobject.*;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UpdateQaService implements UpdateQaUseCase {
+ @Resource
+ private UpdateQaPort updateQaPort;
+
+ @Override
+ public Qa updateQa(UpdateQaCommand command) {
+ Qa qa = new Qa(
+ new QaId(command.id()),
+ new Question(command.question()),
+ new Answer(command.answer()));
+ return updateQaPort.updateQa(qa);
+ }
+}
diff --git a/qa-service/qa-service-bootstrap/pom.xml b/qa-service/qa-service-bootstrap/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..09a0db1cf41ed077195430bfa1a9dfd8e105b8fb
--- /dev/null
+++ b/qa-service/qa-service-bootstrap/pom.xml
@@ -0,0 +1,95 @@
+
+
+ 4.0.0
+ com.example
+ qa-service-bootstrap
+ 0.0.1-SNAPSHOT
+ qa-service-bootstrap
+ qa-service-bootstrap
+
+
+ com.example
+ qa-service
+ 0.0.1-SNAPSHOT
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+
+
+ com.example
+ qa-adapter-in-web
+ 0.0.1-SNAPSHOT
+
+
+
+ com.example
+ qa-adapter-out-persistence
+ 0.0.1-SNAPSHOT
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-config
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-discovery
+
+
+ org.mybatis
+ mybatis-spring
+ 3.0.5
+ compile
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ 21
+ 21
+ UTF-8
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring-boot.version}
+
+ com.example.qa.service.bootstrap.qaServiceBootstrapApplication
+ false
+
+
+
+ repackage
+
+ repackage
+
+
+
+
+
+
+
+
diff --git a/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/QaServiceBootstrapApplication.java b/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/QaServiceBootstrapApplication.java
new file mode 100644
index 0000000000000000000000000000000000000000..c7c67596cc4d4a6cbe370bf35104b1310f2d1e87
--- /dev/null
+++ b/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/QaServiceBootstrapApplication.java
@@ -0,0 +1,15 @@
+package com.example.qa.service.bootstrap;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication(scanBasePackages = "com.example.qa")
+@MapperScan("com.example.qa.adapter.out.persistence.mapper")
+public class QaServiceBootstrapApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(QaServiceBootstrapApplication.class, args);
+ }
+
+}
diff --git a/qa-service/qa-service-bootstrap/src/main/resources/application.properties b/qa-service/qa-service-bootstrap/src/main/resources/application.properties
new file mode 100644
index 0000000000000000000000000000000000000000..1e3baf8a8f9504f0f19560239a4c3d06f4e7dfe3
--- /dev/null
+++ b/qa-service/qa-service-bootstrap/src/main/resources/application.properties
@@ -0,0 +1,27 @@
+server.port=28081
+spring.application.name=qa-service
+# Nacos认证信息
+spring.cloud.nacos.discovery.username=nacos
+spring.cloud.nacos.discovery.password=nacos
+# Nacos 服务发现与注册配置,其中子属性 server-addr 指定 Nacos 服务器主机和端口
+spring.cloud.nacos.discovery.server-addr=192.168.168.128:8848
+# 注册到 nacos 的指定 namespace,默认为 public
+spring.cloud.nacos.discovery.namespace=public
+# Nacos帮助文档: https://nacos.io/zh-cn/docs/concepts.html
+# Nacos认证信息
+spring.cloud.nacos.config.username=nacos
+spring.cloud.nacos.config.password=nacos
+spring.cloud.nacos.config.contextPath=/nacos
+# 设置配置中心服务端地址
+spring.cloud.nacos.config.server-addr=192.168.168.128:8848
+# Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可
+# spring.cloud.nacos.config.namespace=
+spring.config.import=nacos:${spring.application.name}.properties?refresh=true
+spring.rabbitmq.addresses=192.168.168.128
+spring.rabbitmq.username=guest
+spring.rabbitmq.password=guest
+spring.amqp.deserialization.trust.all=true
+
+moonshot.url=https://api.moonshot.cn/v1/chat/completions
+moonshot.key=sk-BMEWhXEYHR0sLrixvuIzv25dRSMHD1E5X21UugQk0Tstc6qs
+moonshot.model=kimi-k2-0905-preview
\ No newline at end of file
diff --git a/qa-service/qa-service-common/.gitignore b/qa-service/qa-service-common/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..549e00a2a96fa9d7c5dbc9859664a78d980158c2
--- /dev/null
+++ b/qa-service/qa-service-common/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/qa-service/qa-service-common/pom.xml b/qa-service/qa-service-common/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d214d8ebe263591dd4c29bf25469c3229558e8b9
--- /dev/null
+++ b/qa-service/qa-service-common/pom.xml
@@ -0,0 +1,72 @@
+
+
+ 4.0.0
+ com.example
+ qa-service-common
+ 0.0.1-SNAPSHOT
+ qa-service-common
+ qa-service-common
+
+ 21
+ UTF-8
+ UTF-8
+ 3.0.2
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ 21
+ 21
+ UTF-8
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring-boot.version}
+
+ com.example.qr.service.common.QrServiceCommonApplication
+ true
+
+
+
+ repackage
+
+ repackage
+
+
+
+
+
+
+
+
diff --git a/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/IdWorker.java b/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/IdWorker.java
new file mode 100644
index 0000000000000000000000000000000000000000..ac96e23534a47e45c5d6fb06dbe2a4b3838df792
--- /dev/null
+++ b/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/IdWorker.java
@@ -0,0 +1,199 @@
+package com.example.qa.service.common;
+
+import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+
+/**
+ * @author wuyunbin
+ * 名称:IdWorker.java
+ * 描述:分布式自增长ID
+ *
+ * Twitter的 Snowflake JAVA实现方案
+ *
+ * 核心代码为其IdWorker这个类实现,其原理结构如下,我分别用一个0表示一位,用—分割开部分的作用:
+ * 1||0---0000000000 0000000000 0000000000 0000000000 0 --- 00000 ---00000 ---000000000000
+ * 在上面的字符串中,第一位为未使用(实际上也可作为long的符号位),接下来的41位为毫秒级时间,
+ * 然后5位datacenter标识位,5位机器ID(并不算标识符,实际是为线程标识),
+ * 然后12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个Long型。
+ * 这样的好处是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分),
+ * 并且效率较高,经测试,snowflake每秒能够产生26万ID左右,完全满足需要。
+ *
+ * 64位ID (42(毫秒)+5(机器ID)+5(业务编码)+12(重复累加))
+ * @author Polim
+ */
+public class IdWorker {
+ /**
+ * 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
+ */
+ private final static long TWEPOCH = 1288834974657L;
+
+ /**
+ * 机器标识位数
+ */
+ private final static long WORKER_ID_BITS = 5L;
+
+ /**
+ * 数据中心标识位数
+ */
+ private final static long DATA_CENTER_ID_BITS = 5L;
+
+ /**
+ * 机器ID最大值
+ */
+ private final static long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);
+
+ /**
+ * 数据中心ID最大值
+ */
+ private final static long MAX_DATACENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);
+
+ /**
+ * 毫秒内自增位
+ */
+ private final static long SEQUENCE_BITS = 12L;
+
+ /**
+ * 机器ID偏左移12位
+ */
+ private final static long WORKER_ID_SHIFT = SEQUENCE_BITS;
+
+ /**
+ * 数据中心ID左移17位
+ */
+ private final static long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
+
+ /**
+ * 时间毫秒左移22位
+ */
+ private final static long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
+
+ private final static long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
+
+ /**
+ * 上次生产id时间戳
+ */
+ private static long lastTimestamp = -1L;
+
+ /**
+ * 0,并发控制
+ */
+ private long sequence = 0L;
+
+ private final long workerId;
+
+ /**
+ * 数据标识id部分
+ */
+ private final long datacenterId;
+
+ public IdWorker() {
+ this.datacenterId = getDatacenterId();
+ this.workerId = getMaxWorkerId(datacenterId);
+ }
+
+ /**
+ * @param workerId 工作机器ID
+ * @param datacenterId 序列号
+ */
+ public IdWorker(long workerId, long datacenterId) {
+ if (workerId > MAX_WORKER_ID || workerId < 0) {
+ throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID));
+ }
+ if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
+ throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", MAX_DATACENTER_ID));
+ }
+ this.workerId = workerId;
+ this.datacenterId = datacenterId;
+ }
+
+ /**
+ * 获取下一个ID
+ *
+ * @return
+ */
+ public synchronized long nextId() {
+ long timestamp = timeGen();
+ if (timestamp < lastTimestamp) {
+ throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
+ }
+
+ if (lastTimestamp == timestamp) {
+ // 当前毫秒内,则+1
+ sequence = (sequence + 1) & SEQUENCE_MASK;
+ if (sequence == 0) {
+ // 当前毫秒内计数满了,则等待下一秒
+ timestamp = tilNextMillis(lastTimestamp);
+ }
+ } else {
+ sequence = 0L;
+ }
+ lastTimestamp = timestamp;
+ // ID偏移组合生成最终的ID,并返回ID
+
+ return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT)
+ | (datacenterId << DATACENTER_ID_SHIFT)
+ | (workerId << WORKER_ID_SHIFT) | sequence;
+ }
+
+ private long tilNextMillis(final long lastTimestamp) {
+ long timestamp = this.timeGen();
+ while (timestamp <= lastTimestamp) {
+ timestamp = this.timeGen();
+ }
+ return timestamp;
+ }
+
+ private long timeGen() {
+ return System.currentTimeMillis();
+ }
+
+ /**
+ *
+ * 获取 MAX_WORKER_ID
+ *
+ */
+ protected static long getMaxWorkerId(long datacenterId) {
+ StringBuilder mpid = new StringBuilder();
+ mpid.append(datacenterId);
+ String name = ManagementFactory.getRuntimeMXBean().getName();
+ if (!name.isEmpty()) {
+ /*
+ * GET jvmPid
+ */
+ mpid.append(name.split("@")[0]);
+ }
+ /*
+ * MAC + PID 的 hashcode 获取16个低位
+ */
+ return (mpid.toString().hashCode() & 0xffff) % (IdWorker.MAX_WORKER_ID + 1);
+ }
+
+ /**
+ *
+ * 数据标识id部分
+ *
+ */
+ protected static long getDatacenterId() {
+ long id = 0L;
+ try {
+ InetAddress ip = InetAddress.getLocalHost();
+ NetworkInterface network = NetworkInterface.getByInetAddress(ip);
+ if (network == null) {
+ id = 1L;
+ } else {
+ byte[] mac = network.getHardwareAddress();
+ id = ((0x000000FF & (long) mac[mac.length - 1])
+ | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
+ id = id % (IdWorker.MAX_DATACENTER_ID + 1);
+ }
+ } catch (Exception e) {
+ System.out.println(" getDatacenterId: " + e.getMessage());
+ }
+ return id;
+ }
+
+
+
+
+}
diff --git a/qa-service/qa-service-domain/.gitignore b/qa-service/qa-service-domain/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..549e00a2a96fa9d7c5dbc9859664a78d980158c2
--- /dev/null
+++ b/qa-service/qa-service-domain/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/qa-service/qa-service-domain/pom.xml b/qa-service/qa-service-domain/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bcd25fed0a7d7bfc349711c9e772a865297c7a1c
--- /dev/null
+++ b/qa-service/qa-service-domain/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+ qa-service-domain
+ 0.0.1-SNAPSHOT
+ qa-service-domain
+ qa-service-domain
+
+
+ com.example
+ qa-service
+ 0.0.1-SNAPSHOT
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+ com.example
+ qa-service-common
+ 0.0.1-SNAPSHOT
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+
+
+
diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/Qa.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/Qa.java
new file mode 100644
index 0000000000000000000000000000000000000000..bddc8f86a14494372d4b95d88a3453d5f74407c9
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/Qa.java
@@ -0,0 +1,49 @@
+package com.example.qa.service.domain;
+
+import com.example.qa.service.common.IdWorker;
+import com.example.qa.service.domain.port.GetQaListPort;
+import com.example.qa.service.domain.valueobject.Answer;
+import com.example.qa.service.domain.valueobject.Question;
+import com.example.qa.service.domain.valueobject.QaId;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.util.List;
+
+@Setter
+@Getter
+@ToString
+
+/*
+ author: zhangyucheng
+ */
+
+public class Qa {
+ private QaId id;
+ private Question question;
+ private Answer answer;
+
+ public Qa() {
+ }
+
+ public Qa(QaId id, Question question, Answer answer) {
+ this.id = id;
+ this.question = question;
+ this.answer = answer;
+ }
+
+ public Qa( Question question, Answer answer) {
+ this.id= genId() ;
+ this.question = question;
+ this.answer = answer;
+ }
+
+ public static List getQas(GetQaListPort getQaListPort){
+ return getQaListPort.getQas();
+ }
+
+ public QaId genId(){
+ return new QaId(new IdWorker().nextId());
+ }
+}
diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/CreateQaPort.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/CreateQaPort.java
new file mode 100644
index 0000000000000000000000000000000000000000..5812a352040fff637e11de4d146473d5d0fc42d7
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/CreateQaPort.java
@@ -0,0 +1,10 @@
+package com.example.qa.service.domain.port;
+
+import com.example.qa.service.domain.Qa;
+
+/*
+ author: zhangyucheng
+ */
+public interface CreateQaPort {
+ Qa createQa(Qa qa);
+}
diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/DeleteQaPort.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/DeleteQaPort.java
new file mode 100644
index 0000000000000000000000000000000000000000..16549fb821772e3f5d360042504bbdf86571208a
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/DeleteQaPort.java
@@ -0,0 +1,8 @@
+package com.example.qa.service.domain.port;
+
+/*
+ author: zhangyucheng
+ */
+public interface DeleteQaPort {
+ void deleteQa(Long id);
+}
diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQaByIdPort.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQaByIdPort.java
new file mode 100644
index 0000000000000000000000000000000000000000..b985a2b7b21977022313ee79b0978f70b5fefc21
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQaByIdPort.java
@@ -0,0 +1,10 @@
+package com.example.qa.service.domain.port;
+
+import com.example.qa.service.domain.Qa;
+
+/*
+ author: zhangyucheng
+ */
+public interface GetQaByIdPort {
+ Qa getQaById(Long id);
+}
diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQaListPort.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQaListPort.java
new file mode 100644
index 0000000000000000000000000000000000000000..b93c6f667a45e1b5716ae57d290195a7842744da
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQaListPort.java
@@ -0,0 +1,12 @@
+package com.example.qa.service.domain.port;
+
+import com.example.qa.service.domain.Qa;
+
+import java.util.List;
+
+/*
+ author: zhangyucheng
+ */
+public interface GetQaListPort {
+ List getQas();
+}
diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/UpdateQaPort.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/UpdateQaPort.java
new file mode 100644
index 0000000000000000000000000000000000000000..bbd8c4402bc3a6240740d97bd0e38961f098d544
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/UpdateQaPort.java
@@ -0,0 +1,10 @@
+package com.example.qa.service.domain.port;
+
+import com.example.qa.service.domain.Qa;
+
+/*
+ author: zhangyucheng
+ */
+public interface UpdateQaPort {
+ Qa updateQa(Qa qa);
+}
diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/Answer.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/Answer.java
new file mode 100644
index 0000000000000000000000000000000000000000..2cf248805c58d7e6ecf153671f345ae1259e6964
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/Answer.java
@@ -0,0 +1,10 @@
+package com.example.qa.service.domain.valueobject;
+
+/*
+ author: zhangyucheng
+ */
+public record Answer(String answer) {
+ public String getValue() {
+ return answer;
+ }
+}
diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QaId.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QaId.java
new file mode 100644
index 0000000000000000000000000000000000000000..75f02deaee5999fd454850404c58a11baddfad09
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QaId.java
@@ -0,0 +1,12 @@
+package com.example.qa.service.domain.valueobject;
+
+/*
+ author: zhangyucheng
+ */
+public record QaId(long id) {
+
+ public long getValue(){
+ return id;
+ }
+
+}
diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/Question.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/Question.java
new file mode 100644
index 0000000000000000000000000000000000000000..5c1b0a47cfcaf12ca40148d2ea8678c15d6d4da6
--- /dev/null
+++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/Question.java
@@ -0,0 +1,10 @@
+package com.example.qa.service.domain.valueobject;
+
+/*
+ author: zhangyucheng
+ */
+public record Question(String question) {
+ public String getValue() {
+ return question;
+ }
+}
diff --git a/user-service/pom.xml b/user-service/pom.xml
index c69afc83c25b3353caf5990a6177cba309dcb683..a000efeef31d69c94b24f509aadff11fe7dea434 100644
--- a/user-service/pom.xml
+++ b/user-service/pom.xml
@@ -56,7 +56,11 @@
pom
import
-
+
+ org.springframework.security
+ spring-security-test
+ test
+
com.baomidou
mybatis-plus-spring-boot3-starter
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/pom.xml b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/pom.xml
index b673beeab279b6a48e1682bc66bcab53fa002d75..1cc92b48260c8e5e678b133424ed49bab46cb386 100644
--- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/pom.xml
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/pom.xml
@@ -25,6 +25,10 @@
spring-boot-starter-test
test
+
+ org.springframework.boot
+ spring-boot-starter-security
+
org.projectlombok
lombok
@@ -46,6 +50,12 @@
knife4j-openapi3-jakarta-spring-boot-starter
${knife4j.version}
+
+ com.example
+ user-adapter-out-persistence
+ 0.0.1-SNAPSHOT
+ compile
+
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/BasicSecurityConfig.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/BasicSecurityConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..74d73854df25a39f29cc3d5bb47bea54d72cd8e4
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/config/BasicSecurityConfig.java
@@ -0,0 +1,175 @@
+package com.example.user.adapter.in.web.config;
+
+
+import com.example.user.adapter.in.web.filter.JwtAuthenticationFilter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Spring Security 配置类
+ * 支持JWT认证和跨域配置
+ *
+ * 这个类负责配置整个应用程序的安全策略,包括:
+ * 1. 用户认证和授权规则
+ * 2. JWT Token验证
+ * 3. 跨域资源共享(CORS)配置
+ * 4. 会话管理策略
+ */
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity(prePostEnabled = true)
+@RequiredArgsConstructor
+public class BasicSecurityConfig {
+
+ /**
+ * JWT认证过滤器
+ * 用于拦截请求并验证JWT Token的有效性
+ */
+ private final JwtAuthenticationFilter jwtAuthenticationFilter;
+
+ /**
+ * 自定义用户详情服务
+ * 用于加载用户信息进行认证
+ */
+
+
+
+ /**
+ * 认证管理器
+ * 负责处理用户认证请求
+ *
+ * @param config 认证配置对象,由Spring自动注入
+ * @return AuthenticationManager 认证管理器实例
+ * @throws Exception 如果配置过程中出现异常
+ */
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
+ return config.getAuthenticationManager();
+ }
+
+ /**
+ * CORS配置源
+ * 配置跨域资源共享策略,允许前端应用访问后端API
+ *
+ * @return CorsConfigurationSource CORS配置源
+ */
+ @Bean
+ public CorsConfigurationSource corsConfigurationSource() {
+ CorsConfiguration configuration = new CorsConfiguration();
+
+ // 允许的源(Origin),这里设置为允许所有域名访问
+ // 在生产环境中应该设置为具体的域名以提高安全性
+ configuration.setAllowedOriginPatterns(List.of("*"));
+
+ // 允许的HTTP方法,包括常用的GET、POST、PUT、DELETE等
+ configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
+
+ // 允许的请求头,这里设置为允许所有请求头
+ configuration.setAllowedHeaders(List.of("*"));
+
+ // 允许携带凭证(如Cookie),设置为true表示允许跨域请求携带身份信息
+ configuration.setAllowCredentials(true);
+
+ // 预检请求的缓存时间,单位秒,这里设置为1小时
+ configuration.setMaxAge(3600L);
+
+ // 暴露给客户端的响应头,允许客户端访问这些响应头
+ configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type"));
+
+ // 创建基于URL的CORS配置源
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ // 对所有路径应用相同的CORS配置
+ source.registerCorsConfiguration("/**", configuration);
+ return source;
+ }
+
+ /**
+ * 安全过滤器链配置
+ * 定义HTTP请求的安全处理规则
+ *
+ * @param http HttpSecurity对象,用于配置Web安全
+ * @return SecurityFilterChain 安全过滤器链
+ * @throws Exception 如果配置过程中出现异常
+ */
+ @Bean
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http
+ // 禁用CSRF(跨站请求伪造)保护
+ // 前后端分离项目通常禁用,因为使用Token认证而不是Session
+ .csrf(AbstractHttpConfigurer::disable)
+
+ // 启用CORS(跨域资源共享)并使用上面定义的配置
+ .cors(cors -> cors.configurationSource(corsConfigurationSource()))
+
+ // 配置会话管理为无状态
+ // 因为使用JWT Token认证,不需要服务器保存会话状态
+ .sessionManagement(session -> session
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ )
+
+ // 配置请求授权规则
+ .authorizeHttpRequests(authz -> authz
+ // 公开接口,无需认证即可访问
+ .requestMatchers("/user/login", "/user/register").permitAll()
+ .requestMatchers("/user/debug/**").permitAll()
+ // Swagger和knife4j文档接口放行,方便查看API文档
+ .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll()
+ .requestMatchers("/doc.html", "/webjars/**", "/favicon.ico").permitAll()
+ .requestMatchers("/actuator/**").permitAll()
+ .requestMatchers("/error").permitAll()
+ // 其他所有请求都需要认证
+ .anyRequest().authenticated()
+ )
+
+ // 禁用默认的登录页面
+ // 因为使用自定义的登录接口,不需要Spring Security提供的默认登录页面
+ .formLogin(AbstractHttpConfigurer::disable)
+
+ // 禁用默认的登出页面
+ // 因为使用自定义的登出接口,不需要Spring Security提供的默认登出功能
+ .logout(AbstractHttpConfigurer::disable)
+
+ // 禁用HTTP Basic认证
+ // 因为使用JWT Token认证,不需要HTTP Basic认证方式
+ .httpBasic(AbstractHttpConfigurer::disable)
+
+ // 添加JWT认证过滤器
+ // 在用户名密码认证过滤器之前添加JWT认证过滤器
+ .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
+
+
+ // 配置异常处理
+ .exceptionHandling(exceptions -> exceptions
+ // 认证入口点,当未认证用户访问需要认证的资源时调用
+ .authenticationEntryPoint((request, response, authException) -> {
+ response.setStatus(401);
+ response.setContentType("application/json;charset=UTF-8");
+ response.getWriter().write("{\"code\":401,\"message\":\"未授权访问,请先登录\",\"data\":null}");
+ })
+ // 访问拒绝处理器,当已认证用户访问没有权限的资源时调用
+ .accessDeniedHandler((request, response, accessDeniedException) -> {
+ response.setStatus(403);
+ response.setContentType("application/json;charset=UTF-8");
+ response.getWriter().write("{\"code\":403,\"message\":\"访问被拒绝,权限不足\",\"data\":null}");
+ })
+ );
+
+ return http.build();
+ }
+}
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController.java
index 96820c00d7cf18e1f698ddc65b3e279601b5d576..12f566812b37c58ed2c70491884825183afcbf5b 100644
--- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController.java
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController.java
@@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
-@RequestMapping("/users")
+@RequestMapping("/user")
@RestController
@RequiredArgsConstructor
public class UserController {
@@ -48,25 +48,16 @@ public class UserController {
return getUserListUseCase.getUsers();
}
- /**
- * 创建新用户
- * 功能:接收用户注册信息,验证密码一致性,创建新用户账户
- * @author dongxuanfeng
- * @param createUserRequestDTO
- * @return User - 成功创建的新用户
- * @throws IllegalArgumentException 当密码与确认密码不匹配时抛出此异常
- */
- @PostMapping()
+ @PostMapping("register")
public User createUser(@RequestBody CreateUserRequestDTO createUserRequestDTO){
- if (!createUserRequestDTO.isPasswordValid()) {
- throw new IllegalArgumentException("密码和确认密码不匹配");
- }
CreateUserCommand command=CreateUserCommand.builder()
- .name(createUserRequestDTO.name())
- .age(createUserRequestDTO.age())
+ .username(createUserRequestDTO.name())
.email(createUserRequestDTO.email())
+ .phone(createUserRequestDTO.phone())
+ .realname(createUserRequestDTO.realname())
.password(createUserRequestDTO.password())
+ .repassword(createUserRequestDTO.repassword())
.build();
return createUserUseCase.createUser(command);
@@ -79,31 +70,22 @@ public class UserController {
return "success";
}
-
+ /**
+ * @author liuxin
+ * @param updateUserRequestDTO
+ * @return
+ */
@PutMapping("")
public User updateUser(@RequestBody UpdateUserRequestDTO updateUserRequestDTO){
UpdateUserCommand command=UpdateUserCommand.builder()
.id(updateUserRequestDTO.id())
.name(updateUserRequestDTO.name())
- .age(updateUserRequestDTO.age())
.email(updateUserRequestDTO.email())
+ .phone(updateUserRequestDTO.phone())
+ .real_name(updateUserRequestDTO.real_name())
.build();
User user = updateUserUseCase.updateUser(command);
return user;
}
-
-
- @GetMapping("{id}")
- public UserResponseDTO getUserById(@PathVariable("id") Long id){
- User user = getUserByIdUseCase.getUserById(id);
- UserResponseDTO userResponseDTO = new UserResponseDTO(
- user.getId().id(),
- user.getName().username(),
- user.getAge().age(),
- user.getEmail().email(),
- user.getIsSuper().value());
- return userResponseDTO;
- }
-
}
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/CreateUserRequestDTO.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/CreateUserRequestDTO.java
index 574d0e0d349d44bf914066a763d8f59390c32203..019367347dbe5e3bb0c5334ec2461a1da74b6cba 100644
--- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/CreateUserRequestDTO.java
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/CreateUserRequestDTO.java
@@ -1,20 +1,19 @@
package com.example.user.adapter.in.web.dto;
public record CreateUserRequestDTO(
- String name,
- Integer age,
- String email,
- String password,
- String rePassword) {
- // TODO: 密码校验
-
+ String name,
+ String email,
+ String phone,
+ String realname,
+ String password,
+ String repassword
+) {
/**
* 验证密码与确认密码是否一致
* @author dongxuanfeng
* @return boolean -验证结果:true表示密码与重复密码一致,false表示两次密码不一致
*/
public boolean isPasswordValid() {
- return password != null && password.equals(rePassword);
+ return password != null && password.equals(repassword);
}
-
}
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java
new file mode 100644
index 0000000000000000000000000000000000000000..245d8f6c5461a9de6be5ca7d52fcb79a1a4e5b6a
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java
@@ -0,0 +1,149 @@
+package com.example.user.adapter.in.web.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 通用响应结果类
+ * 用于封装所有API接口的响应数据,统一返回格式
+ *
+ * 这样做有以下好处:
+ * 1. 前端可以统一处理响应格式
+ * 2. 便于统一错误处理
+ * 3. 提高代码的可维护性
+ *
+ * 使用泛型可以让这个类适用于任何类型的数据
+ *
+ * @Data Lombok注解,自动生成getter、setter、toString等方法
+ * @NoArgsConstructor Lombok注解,生成无参构造函数
+ * @AllArgsConstructor Lombok注解,生成全参构造函数
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Result {
+
+ /**
+ * 响应码
+ *
+ * 用于表示请求处理的结果状态
+ * 常见的响应码:
+ * 200 - 成功
+ * 400 - 请求参数错误
+ * 401 - 未授权
+ * 403 - 禁止访问
+ * 500 - 服务器内部错误
+ */
+ private Integer code;
+
+ /**
+ * 响应消息
+ *
+ * 用于描述请求处理的结果信息
+ * 成功时可以是"操作成功"
+ * 失败时可以是具体的错误信息,如"用户名或密码错误"
+ */
+ private String message;
+
+ /**
+ * 响应数据
+ *
+ * 用于携带具体的业务数据
+ * 可以是任何类型,如用户信息、列表数据等
+ * 使用泛型T使得这个字段可以适应不同类型的数据
+ */
+ private T data;
+
+ /**
+ * 成功响应静态方法
+ *
+ * 用于创建成功的响应结果
+ *
+ * @param data 响应数据
+ * @return Result 成功的响应结果
+ * @param 泛型参数,表示数据的类型
+ */
+ public static Result success(T data) {
+ return new Result<>(200, "操作成功", data);
+ }
+
+ /**
+ * 成功响应静态方法(无数据)
+ *
+ * 用于创建不携带数据的成功响应结果
+ *
+ * @return Result 成功的响应结果
+ * @param 泛型参数,表示数据的类型
+ */
+ public static Result success() {
+ return new Result<>(200, "操作成功", null);
+ }
+
+ /**
+ * 成功响应静态方法(自定义消息)
+ *
+ * 用于创建携带自定义成功消息的响应结果
+ *
+ * @param message 自定义的成功消息
+ * @param data 响应数据
+ * @return Result 成功的响应结果
+ * @param 泛型参数,表示数据的类型
+ */
+ public static Result success(String message, T data) {
+ return new Result<>(200, message, data);
+ }
+
+ /**
+ * 失败响应静态方法
+ *
+ * 用于创建失败的响应结果,默认使用500状态码
+ *
+ * @param message 错误消息
+ * @return Result 失败的响应结果
+ * @param 泛型参数,表示数据的类型
+ */
+ public static Result error(String message) {
+ return new Result<>(500, message, null);
+ }
+
+ /**
+ * 失败响应静态方法(自定义状态码)
+ *
+ * 用于创建携带自定义状态码的失败响应结果
+ *
+ * @param code 自定义状态码
+ * @param message 错误消息
+ * @return Result 失败的响应结果
+ * @param 泛型参数,表示数据的类型
+ */
+ public static Result error(Integer code, String message) {
+ return new Result<>(code, message, null);
+ }
+
+ /**
+ * 未授权响应静态方法
+ *
+ * 用于创建401未授权的响应结果
+ *
+ * @param message 错误消息
+ * @return Result 未授权的响应结果
+ * @param 泛型参数,表示数据的类型
+ */
+ public static Result unauthorized(String message) {
+ return new Result<>(401, message, null);
+ }
+
+ /**
+ * 禁止访问响应静态方法
+ *
+ * 用于创建403禁止访问的响应结果
+ *
+ * @param message 错误消息
+ * @return Result 禁止访问的响应结果
+ * @param 泛型参数,表示数据的类型
+ */
+ public static Result forbidden(String message) {
+ return new Result<>(403, message, null);
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UpdateUserRequestDTO.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UpdateUserRequestDTO.java
index d42ae558a29f0f8fd392aaeae649437aebd8e65e..6baa149d682764cc239a81203e105c9f6dd83ec6 100644
--- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UpdateUserRequestDTO.java
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UpdateUserRequestDTO.java
@@ -2,6 +2,7 @@ package com.example.user.adapter.in.web.dto;
public record UpdateUserRequestDTO(Long id,
String name,
- Integer age,
- String email) {
+ String email,
+ String phone,
+ String real_name) {
}
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/exception/GlobalExceptionHandler.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..14605dca959969c6af1d7442a584e194efda5282
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/exception/GlobalExceptionHandler.java
@@ -0,0 +1,259 @@
+package com.example.user.adapter.in.web.exception;
+
+import com.example.user.adapter.in.web.dto.Result;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.util.stream.Collectors;
+
+/**
+ * 全局异常处理器
+ * 统一处理应用中的各种异常,避免异常直接暴露给用户
+ *
+ * @Slf4j Lombok注解,自动生成日志对象log,用于记录异常日志
+ * @RestControllerAdvice 组合注解,相当于@ControllerAdvice + @ResponseBody
+ * 用于定义全局异常处理器,可以捕获控制器层抛出的异常
+ */
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ /**
+ * 处理认证异常
+ *
+ * 当用户认证失败时(如Token无效、过期等)会抛出AuthenticationException
+ *
+ * @ExceptionHandler 注解指定该方法处理哪种异常
+ * @ResponseStatus 注解指定返回的HTTP状态码
+ *
+ * @param e 认证异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(AuthenticationException.class)
+ @ResponseStatus(HttpStatus.UNAUTHORIZED)
+ public Result handleAuthenticationException(AuthenticationException e) {
+ log.error("认证异常: {}", e.getMessage());
+ return Result.unauthorized("认证失败: " + e.getMessage());
+ }
+
+ /**
+ * 处理凭据错误异常
+ *
+ * 当用户名或密码错误时会抛出BadCredentialsException
+ * 这是AuthenticationException的一个子类
+ *
+ * @param e 凭据错误异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(BadCredentialsException.class)
+ @ResponseStatus(HttpStatus.UNAUTHORIZED)
+ public Result handleBadCredentialsException(BadCredentialsException e) {
+ log.error("凭据错误: {}", e.getMessage());
+ return Result.unauthorized("用户名或密码错误");
+ }
+
+ /**
+ * 处理访问拒绝异常
+ *
+ * 当已认证用户尝试访问没有权限的资源时会抛出AccessDeniedException
+ *
+ * @param e 访问拒绝异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(AccessDeniedException.class)
+ @ResponseStatus(HttpStatus.FORBIDDEN)
+ public Result handleAccessDeniedException(AccessDeniedException e) {
+ log.error("访问拒绝: {}", e.getMessage());
+ return Result.forbidden("访问被拒绝,权限不足");
+ }
+
+ /**
+ * 处理参数校验异常 - @Valid注解
+ *
+ * 当使用@Valid注解验证请求参数失败时会抛出MethodArgumentNotValidException
+ * 例如LoginRequest中的用户名或密码不符合验证规则
+ *
+ * @param e 参数校验异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+ // 收集所有字段的错误信息并拼接成字符串
+ String errorMessage = e.getBindingResult().getFieldErrors().stream()
+ .map(FieldError::getDefaultMessage)
+ .collect(Collectors.joining(", "));
+ log.error("参数校验失败: {}", errorMessage);
+ return Result.error("参数校验失败: " + errorMessage);
+ }
+
+ /**
+ * 处理参数绑定异常
+ *
+ * 当请求参数绑定到对象时发生错误会抛出BindException
+ *
+ * @param e 参数绑定异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(BindException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public Result handleBindException(BindException e) {
+ // 收集所有字段的错误信息并拼接成字符串
+ String errorMessage = e.getFieldErrors().stream()
+ .map(FieldError::getDefaultMessage)
+ .collect(Collectors.joining(", "));
+ log.error("参数绑定失败: {}", errorMessage);
+ return Result.error("参数绑定失败: " + errorMessage);
+ }
+
+ /**
+ * 处理约束违反异常
+ *
+ * 当使用Bean Validation API验证失败时会抛出ConstraintViolationException
+ *
+ * @param e 约束违反异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(ConstraintViolationException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public Result handleConstraintViolationException(ConstraintViolationException e) {
+ // 收集所有约束违反的错误信息并拼接成字符串
+ String errorMessage = e.getConstraintViolations().stream()
+ .map(ConstraintViolation::getMessage)
+ .collect(Collectors.joining(", "));
+ log.error("约束违反: {}", errorMessage);
+ return Result.error("参数校验失败: " + errorMessage);
+ }
+
+ /**
+ * 处理非法参数异常
+ *
+ * 当传递给方法的参数不合法时会抛出IllegalArgumentException
+ *
+ * @param e 非法参数异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(IllegalArgumentException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public Result handleIllegalArgumentException(IllegalArgumentException e) {
+ log.error("非法参数: {}", e.getMessage());
+ return Result.error("参数错误: " + e.getMessage());
+ }
+
+ /**
+ * 处理空指针异常
+ *
+ * 当尝试访问空对象的属性或方法时会抛出NullPointerException
+ *
+ * @param e 空指针异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(NullPointerException.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public Result handleNullPointerException(NullPointerException e) {
+ log.error("空指针异常", e);
+ return Result.error("系统内部错误,请联系管理员");
+ }
+
+ /**
+ * 处理运行时异常
+ *
+ * 当发生未预期的运行时错误时会抛出RuntimeException
+ * 这是很多异常的父类
+ *
+ * @param e 运行时异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(RuntimeException.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public Result handleRuntimeException(RuntimeException e) {
+ log.error("运行时异常: {}", e.getMessage(), e);
+ return Result.error("系统异常: " + e.getMessage());
+ }
+
+ /**
+ * 处理其他所有异常
+ *
+ * 作为兜底的异常处理方法,处理所有未被上面方法处理的异常
+ *
+ * @param e 异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(Exception.class)
+ @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+ public Result handleException(Exception e) {
+ log.error("未知异常: {}", e.getMessage(), e);
+ return Result.error("系统内部错误,请联系管理员");
+ }
+
+ /**
+ * 自定义业务异常类
+ *
+ * 用于处理应用程序中特定的业务异常
+ * 继承自RuntimeException,是一个受检异常
+ */
+ public static class BusinessException extends RuntimeException {
+ /**
+ * 异常码
+ *
+ * 用于标识异常的类型,便于前端进行不同的处理
+ */
+ private final int code;
+
+ /**
+ * 构造函数 - 只有消息
+ *
+ * @param message 异常消息
+ */
+ public BusinessException(String message) {
+ super(message);
+ this.code = 500;
+ }
+
+ /**
+ * 构造函数 - 有码和消息
+ *
+ * @param code 异常码
+ * @param message 异常消息
+ */
+ public BusinessException(int code, String message) {
+ super(message);
+ this.code = code;
+ }
+
+ /**
+ * 获取异常码
+ *
+ * @return int 异常码
+ */
+ public int getCode() {
+ return code;
+ }
+ }
+
+ /**
+ * 处理自定义业务异常
+ *
+ * 处理应用程序中抛出的BusinessException
+ *
+ * @param e 业务异常对象
+ * @return Result 统一响应结果
+ */
+ @ExceptionHandler(BusinessException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public Result handleBusinessException(BusinessException e) {
+ log.error("业务异常: {}", e.getMessage());
+ return Result.error(e.getCode(), e.getMessage());
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/JwtAuthenticationFilter.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/JwtAuthenticationFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..055d347b6121ca0be1468ad467676d7862034743
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/JwtAuthenticationFilter.java
@@ -0,0 +1,351 @@
+package com.example.user.adapter.in.web.filter;
+
+
+import com.example.user.service.common.JwtUtil;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+/**
+ * JWT认证过滤器 - Spring Security认证体系的核心组件
+ *
+ * 【什么是过滤器?】
+ * 过滤器(Filter)是Java Web开发中的重要组件,它像一个"检查站",在请求到达Controller之前
+ * 对每个HTTP请求进行预处理和验证。想象成机场安检,每个乘客(请求)都必须通过安检(过滤器)
+ * 才能登机(到达Controller)。
+ *
+ * 【JWT认证过滤器的作用】
+ * 1. 拦截所有HTTP请求
+ * 2. 检查请求头中是否包含有效的JWT Token
+ * 3. 验证Token的合法性(是否过期、签名是否正确)
+ * 4. 如果Token有效,将用户信息设置到Spring Security上下文中
+ * 5. 如果Token无效或不存在,让请求继续,由其他安全机制处理
+ *
+ * 【为什么继承OncePerRequestFilter?】
+ * OncePerRequestFilter确保每个请求只被过滤一次,避免重复验证造成性能问题。
+ * 在复杂的Web应用中,一个请求可能会经过多个过滤器,这个基类保证了我们的
+ * JWT验证逻辑只执行一次。
+ *
+ * 【Spring Security认证流程】
+ * 1. 用户发送请求 → 2. JWT过滤器验证Token → 3. 设置认证上下文 → 4. 后续代码可以获取用户信息
+ *
+ * 【注解说明】
+ * @Slf4j: Lombok注解,自动生成日志对象log,用于记录调试和错误信息
+ * @Component: Spring注解,将该类注册为Spring容器管理的Bean,可以被自动注入
+ */
+@Slf4j
+@Component
+public class JwtAuthenticationFilter extends OncePerRequestFilter {
+
+ /**
+ * JWT工具类 - Token操作的核心工具
+ *
+ * 【作用说明】
+ * 负责JWT Token的生成、解析、验证等所有操作
+ *
+ * 【@Autowired注解】
+ * Spring的依赖注入注解,Spring容器会自动找到JwtUtil类型的Bean并注入到这里
+ * 这样我们就不需要手动创建JwtUtil对象,Spring帮我们管理对象的生命周期
+ */
+ @Autowired
+ private JwtUtil jwtUtil;
+
+ /**
+ * Token黑名单服务 - 管理已失效的JWT Token
+ *
+ * 【作用说明】
+ * 用于解决JWT无状态特性导致的logout后token仍然有效的问题
+ * 通过维护一个黑名单,记录已经失效的token
+ *
+ * 【JWT无状态认证的挑战】
+ * JWT设计为无状态,服务器不保存token信息,这带来了扩展性优势
+ * 但也带来了问题:无法主动使token失效(如用户logout)
+ *
+ * 【黑名单解决方案】
+ * 1. 用户logout时,将token加入黑名单
+ * 2. 每次验证token时,先检查是否在黑名单中
+ * 3. 黑名单中的token被视为无效,拒绝访问
+ *
+ * 【性能考虑】
+ * 黑名单检查会增加一定的性能开销,但这是安全性的必要代价
+ * 在生产环境中,建议使用Redis等高性能缓存来存储黑名单
+ */
+ @Autowired
+ private TokenBlacklistService tokenBlacklistService;
+
+ /**
+ * Spring应用上下文 - Spring容器的入口
+ *
+ * 【作用说明】
+ * ApplicationContext是Spring容器的核心接口,通过它可以获取容器中的任何Bean
+ *
+ * 【为什么需要它?】
+ * 在某些情况下,我们需要动态获取Bean,而不是通过@Autowired静态注入
+ * 特别是在解决循环依赖问题时,这种方式非常有用
+ */
+ @Autowired
+ private ApplicationContext applicationContext;
+
+ /**
+ * 用户详情服务 - 用户信息加载器
+ *
+ * 【设计说明】
+ * 注意这里没有使用@Autowired注解,而是通过延迟加载的方式获取
+ * 这是为了避免循环依赖问题:
+ * JwtAuthenticationFilter需要UserDetailsService,
+ * 而UserDetailsService的实现类可能也需要其他被JwtAuthenticationFilter保护的组件
+ *
+ * 【延迟加载的好处】
+ * 只有在真正需要时才获取Bean,避免启动时的循环依赖问题
+ */
+ private UserDetailsService userDetailsService;
+
+ /**
+ * 获取用户详情服务实例 - 延迟加载模式的实现
+ *
+ * 【延迟加载模式(Lazy Loading)】
+ * 这是一种设计模式,只有在真正需要对象时才创建或获取它
+ * 类似于"用时再买"的概念,避免提前准备造成的资源浪费
+ *
+ * 【解决循环依赖的核心方法】
+ * 循环依赖问题:A需要B,B需要A,如果同时创建会造成死锁
+ * 解决方案:A先创建,需要B时再去获取B,这样打破了循环
+ *
+ * 【实现原理】
+ * 1. 第一次调用时,userDetailsService为null,通过ApplicationContext获取Bean
+ * 2. 后续调用直接返回已获取的实例,提高性能
+ * 3. 这种方式叫做"单例模式 + 延迟初始化"
+ *
+ * 【为什么不直接用@Autowired?】
+ * 如果直接用@Autowired,Spring在启动时就要解决所有依赖关系
+ * 可能会遇到循环依赖导致启动失败
+ *
+ * @return UserDetailsService 用户详情服务实例
+ */
+ private UserDetailsService getUserDetailsService() {
+ if (userDetailsService == null) {
+ // 通过Spring容器动态获取Bean,避免循环依赖
+ userDetailsService = applicationContext.getBean(UserDetailsService.class);
+ }
+ return userDetailsService;
+ }
+
+ /**
+ * 过滤器核心方法 - JWT认证的完整流程实现
+ *
+ * 【方法执行时机】
+ * 每个HTTP请求到达Controller之前都会执行这个方法
+ * 这是Spring Security认证链中的关键环节
+ *
+ * 【完整的JWT认证流程】
+ * 1. 提取Token:从HTTP请求头的Authorization字段中提取JWT Token
+ * 2. 解析Token:使用JWT工具类解析Token,获取用户名
+ * 3. 验证Token:检查Token是否过期、签名是否正确
+ * 4. 加载用户:从数据库加载用户详细信息
+ * 5. 创建认证:创建Spring Security认证对象
+ * 6. 设置上下文:将认证信息存储到SecurityContext中
+ * 7. 继续处理:调用过滤器链,让请求继续向下传递
+ *
+ * 【Spring Security上下文的作用】
+ * SecurityContext就像一个"身份证明",一旦设置成功,后续的所有代码
+ * 都可以通过SecurityContextHolder.getContext()获取当前用户信息
+ *
+ * 【异常处理策略】
+ * 如果JWT验证失败,不会阻止请求继续执行,而是清除认证上下文
+ * 让Spring Security的其他机制(如返回401未授权)来处理
+ *
+ * @param request HTTP请求对象,包含客户端发送的所有信息
+ * @param response HTTP响应对象,用于向客户端发送响应
+ * @param filterChain 过滤器链,用于调用下一个过滤器或最终的Controller
+ * @throws ServletException Servlet相关异常
+ * @throws IOException 输入输出异常
+ */
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+
+ try {
+ // 【步骤1:提取JWT Token】
+ // 从HTTP请求头的Authorization字段中提取Token
+ // 标准格式:Authorization: Bearer
+ String jwt = getJwtFromRequest(request);
+
+ // 【步骤2:检查Token是否存在】
+ // StringUtils.hasText()检查字符串是否不为null且不为空字符串
+ // 如果没有Token,说明可能是匿名访问或登录请求
+ if (StringUtils.hasText(jwt)) {
+ // 【步骤2.5:检查Token是否在黑名单中】
+ // 这是新增的安全检查,解决JWT无状态认证的logout问题
+ // 如果token在黑名单中,说明用户已经logout,应该拒绝访问
+ if (tokenBlacklistService.isBlacklisted(jwt)) {
+ log.warn("Token is blacklisted (user has logged out): {}",
+ jwt.substring(0, Math.min(20, jwt.length())) + "...");
+ // 清除认证上下文,确保不会认证成功
+ SecurityContextHolder.clearContext();
+ // 继续过滤器链,让Spring Security返回401未授权
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ // 【步骤3:解析Token获取用户名】
+ // 这一步会解析JWT的payload部分,提取用户名信息
+ // 如果Token格式错误或签名无效,会抛出异常
+ String username = jwtUtil.getUsernameFromToken(jwt);
+
+ // 【步骤4:检查是否需要认证】
+ // username != null: 确保成功从Token中提取到用户名
+ // SecurityContextHolder.getContext().getAuthentication() == null: 确保当前请求还没有认证信息
+ // 这样避免重复认证,提高性能
+ if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+ // 【步骤5:加载用户详细信息】
+ // 通过用户名从数据库加载完整的用户信息(包括权限、角色等)
+ // 这里使用了延迟加载的UserDetailsService,避免循环依赖
+ UserDetails userDetails = getUserDetailsService().loadUserByUsername(username);
+
+ // 【步骤6:验证Token的完整性】
+ // 不仅要检查Token格式,还要验证:
+ // 1. Token是否过期
+ // 2. 签名是否正确
+ // 3. Token中的用户名是否与数据库中的一致
+ if (jwtUtil.validateToken(jwt, userDetails.getUsername())) {
+ // 【步骤7:创建Spring Security认证对象】
+ // UsernamePasswordAuthenticationToken是Spring Security的标准认证对象
+ // 三个参数的含义:
+ // 1. principal: 主体,通常是用户详情对象
+ // 2. credentials: 凭据,JWT模式下不需要密码,设为null
+ // 3. authorities: 权限列表,从用户详情中获取
+ UsernamePasswordAuthenticationToken authentication =
+ new UsernamePasswordAuthenticationToken(
+ userDetails, // 用户详情(包含用户名、权限等)
+ null, // 凭据(JWT认证不需要密码)
+ userDetails.getAuthorities() // 用户权限列表
+ );
+
+ // 【步骤8:设置认证详情】
+ // 添加额外的认证信息,如IP地址、Session ID等
+ // 这些信息在安全审计和日志记录中很有用
+ authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+
+ // 【步骤9:设置到Spring Security上下文】
+ // 这是最关键的一步!将认证信息存储到SecurityContext中
+ // 后续的Controller、Service等都可以通过SecurityContextHolder获取当前用户信息
+ // 这就是Spring Security"记住"用户身份的机制
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+
+ // 记录成功的认证日志,便于调试和监控
+ log.debug("JWT authentication successful for user: {}", username);
+ } else {
+ // Token验证失败,记录警告日志
+ // 可能的原因:Token过期、签名错误、用户名不匹配等
+ log.warn("JWT token validation failed for user: {}", username);
+ }
+ }
+ }
+ } catch (Exception e) {
+ // 【异常处理:安全优先原则】
+ // 任何异常都不应该影响系统的安全性
+ // 记录错误日志,便于排查问题
+ log.error("Cannot set user authentication: {}", e.getMessage());
+ // 清除认证上下文,确保不会使用错误或不完整的认证信息
+ // 这是安全编程的重要原则:出错时选择更安全的状态
+ SecurityContextHolder.clearContext();
+ }
+
+ // 继续过滤器链,让请求继续向下处理
+ filterChain.doFilter(request, response);
+ }
+
+ /**
+ * 从请求中提取JWT Token - HTTP标准认证头解析
+ *
+ * 【HTTP Authorization头标准】
+ * HTTP协议规定,认证信息应该放在Authorization请求头中
+ * JWT认证的标准格式:Authorization: Bearer
+ *
+ * 【Bearer认证方案】
+ * Bearer是OAuth 2.0规范中定义的认证方案
+ * 意思是"持有者",表示谁持有这个Token,谁就有相应的权限
+ * 格式:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
+ *
+ * 【为什么要单独提取?】
+ * 1. 代码复用:多个地方可能需要提取Token
+ * 2. 职责分离:Token提取和Token验证是不同的职责
+ * 3. 易于测试:可以单独测试Token提取逻辑
+ *
+ * @param request HTTP请求对象,包含所有请求头信息
+ * @return JWT Token字符串,如果不存在或格式错误则返回null
+ */
+ private String getJwtFromRequest(HttpServletRequest request) {
+ // 从请求头中获取Authorization字段的值
+ // 例如:"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+ String bearerToken = request.getHeader("Authorization");
+ // 使用JwtUtil工具类提取实际的Token部分(去掉"Bearer "前缀)
+ return jwtUtil.extractTokenFromHeader(bearerToken);
+ }
+
+ /**
+ * 判断是否需要过滤 - 实现白名单机制
+ *
+ * 【白名单机制的重要性】
+ * 不是所有的接口都需要JWT认证,有些接口必须是公开的:
+ * 1. 登录接口:用户还没有Token,怎么能要求提供Token?
+ * 2. 注册接口:新用户注册时也没有Token
+ * 3. 文档接口:开发时需要查看API文档
+ *
+ * 【OncePerRequestFilter的设计】
+ * OncePerRequestFilter提供了shouldNotFilter方法,让我们可以灵活控制
+ * 哪些请求需要过滤,哪些不需要。返回true表示跳过过滤。
+ *
+ * 【安全考虑】
+ * 白名单要谨慎设置,只有真正需要公开访问的接口才能加入白名单
+ * 过多的白名单会降低系统安全性
+ *
+ * 【路径匹配策略】
+ * 使用startsWith进行前缀匹配,这样可以匹配一类接口
+ * 例如:/user/login 可以匹配 /user/login、/user/login?username=xxx 等
+ *
+ * @param request HTTP请求对象
+ * @return boolean 是否不需要过滤,true表示跳过JWT验证
+ * @throws ServletException Servlet异常
+ */
+ @Override
+ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
+ // 获取请求的URI路径
+ String path = request.getRequestURI();
+
+ // 【业务接口白名单】
+ // /user/login: 用户登录接口,用户通过用户名密码获取Token
+ // /user/register: 用户注册接口,新用户创建账号
+ boolean isBusinessWhitelist = path.startsWith("/user/login") ||
+ path.startsWith("/user/register");
+
+ // 【文档接口白名单】
+ // 这些是Swagger API文档相关的接口,开发阶段需要公开访问
+ // /doc.html: Knife4j文档首页
+ // /swagger-ui: Swagger UI界面
+ // /v3/api-docs: OpenAPI 3.0规范的JSON文档
+ // /webjars: 前端资源文件(CSS、JS等)
+ boolean isDocWhitelist = path.startsWith("/doc.html") ||
+ path.startsWith("/swagger-ui") ||
+ path.startsWith("/v3/api-docs") ||
+ path.startsWith("/webjars");
+
+ // 返回true表示不需要JWT验证,false表示需要验证
+ return isBusinessWhitelist || isDocWhitelist;
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/TokenBlacklistService.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/TokenBlacklistService.java
new file mode 100644
index 0000000000000000000000000000000000000000..9e2152218107cc8a5a58ab3a8a483a0b9da6d1ce
--- /dev/null
+++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/filter/TokenBlacklistService.java
@@ -0,0 +1,203 @@
+package com.example.user.adapter.in.web.filter;
+
+import com.example.user.service.common.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Token黑名单服务类 - JWT令牌失效管理
+ *
+ * 这个服务类解决了JWT无状态认证中的一个重要问题:如何在用户登出后立即使令牌失效
+ *
+ * JWT的无状态特性说明:
+ * - JWT令牌是自包含的,包含了所有必要的用户信息
+ * - 服务端不需要存储会话状态,每次请求都通过验证令牌签名来确认身份
+ * - 这种设计的优点是可扩展性好,但缺点是无法在服务端主动"销毁"令牌
+ * - 令牌在过期时间之前始终有效,即使用户已经登出
+ *
+ * 黑名单机制原理:
+ * 1. 维护一个已失效令牌的黑名单列表
+ * 2. 用户登出时,将令牌添加到黑名单
+ * 3. 每次验证令牌时,先检查是否在黑名单中
+ * 4. 如果在黑名单中,则拒绝访问,即使令牌本身是有效的
+ *
+ * 存储方案选择:
+ * - 内存存储(ConcurrentHashMap):适合单机部署,性能最好
+ * - Redis存储:适合分布式部署,多个服务实例共享黑名单
+ * - 数据库存储:适合对数据持久性要求高的场景
+ *
+ * 本实现使用内存存储,具有以下特点:
+ * - 高性能:内存访问速度快,不涉及网络IO
+ * - 线程安全:使用ConcurrentHashMap保证并发安全
+ * - 自动清理:定时清理过期的黑名单记录,避免内存泄漏
+ * - 简单可靠:无外部依赖,部署简单
+ *
+ * 注意事项:
+ * - 重启服务会丢失黑名单数据,已登出的用户令牌可能重新生效
+ * - 多实例部署时,各实例的黑名单不共享
+ * - 如需解决以上问题,建议改用Redis等外部存储
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class TokenBlacklistService {
+
+ /**
+ * JWT工具类,用于解析令牌获取过期时间
+ */
+ private final JwtUtil jwtUtil;
+
+ /**
+ * 黑名单存储容器
+ *
+ * 使用ConcurrentHashMap确保线程安全的并发访问
+ * Key: JWT令牌字符串
+ * Value: 令牌的过期时间,用于定时清理
+ *
+ * 为什么存储过期时间:
+ * - 避免永久存储已过期的令牌,节省内存空间
+ * - 支持定时清理机制,自动移除不再需要的黑名单记录
+ * - 提供调试信息,便于排查问题
+ */
+ private final ConcurrentHashMap blacklistedTokens = new ConcurrentHashMap<>();
+
+ /**
+ * 将令牌添加到黑名单
+ *
+ * 当用户登出时调用此方法,将JWT令牌加入黑名单
+ * 加入黑名单后,该令牌将无法再用于身份验证
+ *
+ * 实现细节:
+ * 1. 解析令牌获取过期时间
+ * 2. 将令牌和过期时间存入黑名单Map
+ * 3. 记录操作日志,便于审计和调试
+ *
+ * 异常处理:
+ * - 如果令牌格式无效,会记录警告日志但不抛出异常
+ * - 确保即使个别令牌处理失败,也不影响整体功能
+ *
+ * @param token JWT令牌字符串
+ */
+ public void addToBlacklist(String token) {
+ try {
+ // 解析令牌获取过期时间
+ // 这样可以在令牌自然过期后自动清理黑名单记录
+ Date expirationTime = jwtUtil.getExpirationDateFromToken(token);
+
+ // 将令牌添加到黑名单
+ blacklistedTokens.put(token, expirationTime);
+
+ // 记录操作日志(只记录令牌的前几位,避免泄露完整令牌)
+ log.info("令牌已添加到黑名单,过期时间: {}, 令牌前缀: {}...",
+ expirationTime,
+ token.substring(0, Math.min(token.length(), 10)));
+
+ } catch (Exception e) {
+ // 如果解析令牌失败,记录警告日志
+ log.warn("添加令牌到黑名单时发生错误: {}, 令牌前缀: {}...",
+ e.getMessage(),
+ token.substring(0, Math.min(token.length(), 10)));
+ }
+ }
+
+ /**
+ * 检查令牌是否在黑名单中
+ *
+ * 在JWT认证过滤器中调用此方法,检查令牌是否已被列入黑名单
+ * 如果令牌在黑名单中,则应拒绝该请求的访问
+ *
+ * 性能考虑:
+ * - ConcurrentHashMap的containsKey操作时间复杂度为O(1)
+ * - 即使黑名单中有大量令牌,查询性能也很好
+ * - 无需额外的网络请求或磁盘IO
+ *
+ * @param token JWT令牌字符串
+ * @return boolean true表示令牌在黑名单中(应拒绝访问),false表示不在黑名单中
+ */
+ public boolean isBlacklisted(String token) {
+ boolean isBlacklisted = blacklistedTokens.containsKey(token);
+
+ if (isBlacklisted) {
+ log.debug("检测到黑名单令牌访问尝试,令牌前缀: {}...",
+ token.substring(0, Math.min(token.length(), 10)));
+ }
+
+ return isBlacklisted;
+ }
+
+ /**
+ * 定时清理过期的黑名单令牌
+ *
+ * 使用Spring的@Scheduled注解实现定时任务
+ * 每小时执行一次清理操作,移除已经自然过期的令牌
+ *
+ * 清理的必要性:
+ * - 避免内存泄漏:长期运行的服务会积累大量过期令牌
+ * - 提高性能:减少黑名单大小,提高查询效率
+ * - 节省资源:释放不再需要的内存空间
+ *
+ * 清理策略:
+ * - 只清理已经过期的令牌(当前时间 > 令牌过期时间)
+ * - 使用迭代器安全地删除元素,避免并发修改异常
+ * - 记录清理统计信息,便于监控和调试
+ *
+ * 定时配置说明:
+ * - fixedRate = 3600000:每3600000毫秒(1小时)执行一次
+ * - 可以根据实际需求调整清理频率
+ * - 频率过高会增加CPU开销,频率过低会占用更多内存
+ */
+ @Scheduled(fixedRate = 3600000) // 每小时执行一次
+ public void cleanupExpiredTokens() {
+ Date now = new Date();
+ int initialSize = blacklistedTokens.size();
+
+ // 使用removeIf方法安全地移除过期令牌
+ // 这个方法是线程安全的,不会与其他操作产生冲突
+ blacklistedTokens.entrySet().removeIf(entry -> {
+ Date expirationTime = entry.getValue();
+ return expirationTime != null && now.after(expirationTime);
+ });
+
+ int finalSize = blacklistedTokens.size();
+ int cleanedCount = initialSize - finalSize;
+
+ if (cleanedCount > 0) {
+ log.info("黑名单清理完成,清理了 {} 个过期令牌,当前黑名单大小: {}", cleanedCount, finalSize);
+ } else {
+ log.debug("黑名单清理完成,无过期令牌需要清理,当前黑名单大小: {}", finalSize);
+ }
+ }
+
+ /**
+ * 获取当前黑名单大小
+ *
+ * 提供监控和调试功能,可以了解当前黑名单的使用情况
+ *
+ * @return int 黑名单中令牌的数量
+ */
+ public int getBlacklistSize() {
+ return blacklistedTokens.size();
+ }
+
+ /**
+ * 清空所有黑名单令牌
+ *
+ * 提供管理功能,在特殊情况下可以清空整个黑名单
+ * 注意:此操作会使所有已登出用户的令牌重新生效
+ *
+ * 使用场景:
+ * - 系统维护时需要重置黑名单状态
+ * - 测试环境中需要快速清理数据
+ * - 紧急情况下需要恢复所有用户访问
+ */
+ public void clearBlacklist() {
+ int size = blacklistedTokens.size();
+ blacklistedTokens.clear();
+ log.warn("黑名单已被清空,共清理了 {} 个令牌", size);
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/pom.xml b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/pom.xml
index 812d59d239543ac421905880a779976e3b0c980f..eb6838cf650d3b2224bf95151caeb97c9775dc90 100644
--- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/pom.xml
+++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/pom.xml
@@ -55,3 +55,4 @@
+
diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/convertor/UserConvertor.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/convertor/UserConvertor.java
index cc56ab567d7dd43b0fd158c989ea8b8069c882fb..808eb015389c8c94615607a3812ee2fd5fd2cc6f 100644
--- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/convertor/UserConvertor.java
+++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/convertor/UserConvertor.java
@@ -5,37 +5,33 @@ import com.example.user.service.domain.User;
import com.example.user.service.domain.valueobject.*;
public class UserConvertor {
- /**
- * 将持久化实体转换为领域对象
- * @author dongxuanfeng
- * @param userEntity 数据库用户实体,包含用户的所有持久化数据
- * @return User 领域对象,包含用户的所有业务属性和行为
- */
- public static User toDomain(UserEntity userEntity) {
- return new User(
- new UserId(userEntity.getId()),
- new UserName(userEntity.getName()),
- new UserAge(userEntity.getAge()),
- new Email(userEntity.getEmail()),
- Password.fromEncrypted(userEntity.getPassword()),
- new IsSuper(userEntity.getIsSuper() == 1)
- );
- }
- /**
- * 将领域对象转换为持久化实体
- * @author dongxuanfeng
- * @param user 用户领域对象,包含用户的所有业务属性和行为
- * @return UserEntity 数据库用户实体,包含用户的所有持久化数据
- */
- public static UserEntity toEntity(User user) {
- return new UserEntity(
- user.getId().id(),
- user.getName().username(),
- user.getAge().age(),
- user.getEmail().email(),
- user.getPassword().encryptedValue(),
- user.getIsSuper().value() ? 1 : 0
- );
- }
+ public static User toDomain(UserEntity userEntity) {
+ return new User(
+ new UserId(userEntity.getId()),
+ new UserName(userEntity.getName()),
+ new Password(userEntity.getPassword()),
+ new Email(userEntity.getEmail()),
+ new Phone(userEntity.getPhone()),
+ new RealName(userEntity.getRealName()),
+ new UserStatus(userEntity.getStatus()),
+ new UserRole(userEntity.getRole())
+ );
+ }
+
+ public static UserEntity toEntity(User user) {
+ return new UserEntity(
+ user.getId().getValue(),
+ user.getUsername().getValue(),
+ user.getPassword().encryptedValue(),
+ user.getEmail().getValue(),
+ user.getPhone().getValue(),
+ user.getRealName().getValue(),
+ user.getStatus().getValue(),
+ user.getRole().getValue(),
+ user.getCreateTime(),
+ user.getUpdateTime(),
+ user.getLastLoginTime()
+ );
+ }
}
diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/entity/UserEntity.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/entity/UserEntity.java
index 1455f81d5b6a2f987fc9344007f8013df1f4c4fa..d04f201ff2603a06db4d5f9a086a844a066e91af 100644
--- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/entity/UserEntity.java
+++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/entity/UserEntity.java
@@ -7,6 +7,10 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
@Data
@AllArgsConstructor
@NoArgsConstructor
@@ -15,13 +19,27 @@ public class UserEntity {
@TableId(type= IdType.ASSIGN_ID)
private long id;
private String name;
- private Integer age;
- private String email;
private String password;
- private Integer isSuper;
- public UserEntity(long value, String value1, int value2, String value3) {
- }
- public UserEntity(long id, String name, Integer age, String email, String password) {
- this(id, name, age, email, password, 0); // 默认isSuper为0
+ private String email;
+ private String phone;
+ private String realName;
+ private int status;
+ private String role;
+ private LocalDateTime createTime;
+ private LocalDateTime updateTime;
+ private LocalDateTime lastLoginTime;
+
+// public UserEntity(long value, String value1, int value2, String value3) {
+// }
+ public UserEntity(long id, String name, String password, String email,
+ String phone, String realName, int status, String role) {
+ this.id = id;
+ this.name = name;
+ this.password = password;
+ this.email = email;
+ this.phone = phone;
+ this.realName = realName;
+ this.status = status;
+ this.role = role;
}
}
diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/command/CreateUserCommand.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/command/CreateUserCommand.java
index d7f351917f3750105968088e03b460e2b9ba8bcb..8aca7f970b1eeb7afda13ae63cc291f6ef747b16 100644
--- a/user-service/user-service-application/src/main/java/com/example/user/service/application/command/CreateUserCommand.java
+++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/command/CreateUserCommand.java
@@ -5,9 +5,11 @@ import lombok.Builder;
@Builder
public record CreateUserCommand(
Long id,
- String name,
- Integer age,
+ String username,
String email,
- String password
+ String phone,
+ String realname,
+ String password,
+ String repassword
) {
}
diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/command/UpdateUserCommand.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/command/UpdateUserCommand.java
index 0ef0ed9076efe403013ff1e6c169c353387b6b30..8b1aa405b79e3b2fb54a85c2b55d1ba29e2e1524 100644
--- a/user-service/user-service-application/src/main/java/com/example/user/service/application/command/UpdateUserCommand.java
+++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/command/UpdateUserCommand.java
@@ -5,6 +5,7 @@ import lombok.Builder;
@Builder
public record UpdateUserCommand(Long id,
String name,
- Integer age,
- String email) {
+ String email,
+ String phone,
+ String real_name) {
}
diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/CreateUserService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/CreateUserService.java
index 931828a4d32e7cae7a78cc0e55f92ade3a7ff2e5..913d1c894123d81151e144ecf548e746eac916ce 100644
--- a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/CreateUserService.java
+++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/CreateUserService.java
@@ -4,10 +4,7 @@ import com.example.user.service.application.command.CreateUserCommand;
import com.example.user.service.application.port.in.CreateUserUseCase;
import com.example.user.service.domain.User;
import com.example.user.service.domain.port.CreateUserPort;
-import com.example.user.service.domain.valueobject.Email;
-import com.example.user.service.domain.valueobject.Password;
-import com.example.user.service.domain.valueobject.UserAge;
-import com.example.user.service.domain.valueobject.UserName;
+import com.example.user.service.domain.valueobject.*;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -17,17 +14,19 @@ import org.springframework.stereotype.Service;
public class CreateUserService implements CreateUserUseCase {
@Resource
private CreateUserPort createUserPort;
+
@Override
public User createUser(CreateUserCommand createUserCommand) {
- //command -> domain
- User user=new User(
- new UserName(createUserCommand.name()),
- new UserAge(createUserCommand.age()),
+ // 创建用户领域对象
+ User user = User.createUser(
+ new UserName(createUserCommand.username()),
+ Password.fromRaw(createUserCommand.password()),
new Email(createUserCommand.email()),
-// new Password( createUserCommand.password())
- Password.fromRaw(createUserCommand.password())
+ new Phone(createUserCommand.phone()),
+ new RealName(createUserCommand.realname()),
+ UserRole.user() // 默认用户角色
);
- log.info("user:{}",user);
+ log.info("Creating user: {}", user);
return createUserPort.createUser(user);
}
}
diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UpdateUserService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UpdateUserService.java
index 97da325352c39b77ddfdc0f9982a3f654a201c91..3ece0ce73907990e559b7f253d883e4b1901c823 100644
--- a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UpdateUserService.java
+++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UpdateUserService.java
@@ -3,11 +3,10 @@ package com.example.user.service.application.service;
import com.example.user.service.application.command.UpdateUserCommand;
import com.example.user.service.application.port.in.UpdateUserUseCase;
import com.example.user.service.domain.User;
+import com.example.user.service.domain.port.GetUserByIdPort;
import com.example.user.service.domain.port.UpdateUserPort;
-import com.example.user.service.domain.valueobject.Email;
-import com.example.user.service.domain.valueobject.UserAge;
-import com.example.user.service.domain.valueobject.UserId;
-import com.example.user.service.domain.valueobject.UserName;
+import com.example.user.service.domain.valueobject.*;
+
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -15,14 +14,19 @@ import org.springframework.stereotype.Service;
public class UpdateUserService implements UpdateUserUseCase {
@Resource
private UpdateUserPort updateUserPort;
+ @Resource
+ private GetUserByIdPort getUserByIdPort;
@Override
public User updateUser(UpdateUserCommand command) {
- User user = new User(
- new UserId(command.id()),
- new UserName(command.name()),
- new UserAge(command.age()),
- new Email(command.email()));
+ User user = getUserByIdPort.getUserById(command.id());
+
+ user.updateProfile(
+ new Email(command.email()),
+ new Phone(command.phone()),
+ new RealName(command.real_name())
+ );
+
return updateUserPort.updateUser(user);
}
}
diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java
index b10d43e0179a76381f25cb3e62b9319077c76549..f0c7ec444490059a4f91af0f69ec9b672788e25c 100644
--- a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java
+++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java
@@ -7,6 +7,7 @@ import com.example.user.service.domain.User;
import com.example.user.service.domain.port.GetUserByNamePort;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Slf4j
@@ -15,6 +16,8 @@ public class UserLoginService implements UserLoginUseCase {
@Resource
private GetUserByNamePort getUserByNamePort;
+ @Autowired
+ private JwtUtil jwtUtil;
@Override
public String login(UserLoginCommand userLoginCommand) {
@@ -28,13 +31,6 @@ public class UserLoginService implements UserLoginUseCase {
if(!user.validatePassword(userLoginCommand.password())){
throw new RuntimeException("密码错误");
}
- // 签发token
- String token = JwtUtil.generateToken(
- user.getId().id(),
- user.getName().username(),
- user.getIsSuper().value()
- );
- log.info("生成的JWT令牌: {}", token);
- return token;
+ return jwtUtil.generateToken(userLoginCommand.name());
}
}
diff --git a/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/UserServiceBootstrapApplication.java b/user-service/user-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/UserServiceBootstrapApplication.java
similarity index 100%
rename from user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/UserServiceBootstrapApplication.java
rename to user-service/user-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/UserServiceBootstrapApplication.java
diff --git a/user-service/user-service-bootstrap/src/main/resources/application.properties b/user-service/user-service-bootstrap/src/main/resources/application.properties
index 3ffae5ba70d1950bb4d5530c8b2e561a15ee9e5e..6ed57fa4c993d15ea90c456e5f44ec9289f323d7 100644
--- a/user-service/user-service-bootstrap/src/main/resources/application.properties
+++ b/user-service/user-service-bootstrap/src/main/resources/application.properties
@@ -3,7 +3,6 @@ server.port=28080
spring.application.name=user-service
-
# Nacos认证信息
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
@@ -23,3 +22,6 @@ spring.cloud.nacos.config.server-addr=192.168.168.128:8848
# spring.cloud.nacos.config.namespace=
spring.config.import=nacos:${spring.application.name}.properties?refresh=true
+springdoc.api-docs.enabled=true
+springdoc.swagger-ui.enabled=true
+springdoc.swagger-ui.path=/swagger-ui.html
\ No newline at end of file
diff --git a/user-service/user-service-common/pom.xml b/user-service/user-service-common/pom.xml
index 20aa5fc4a8e02f4fce861aab3cca87288a00c8c4..4d8b80a0971315f39ee8d30cb7e2ca6d3546608e 100644
--- a/user-service/user-service-common/pom.xml
+++ b/user-service/user-service-common/pom.xml
@@ -18,32 +18,6 @@
org.springframework.boot
spring-boot-starter
-
- io.jsonwebtoken
- jjwt
- 0.9.1
-
-
-
- javax.xml.bind
- jaxb-api
- 2.3.1
-
-
- com.sun.xml.bind
- jaxb-core
- 2.3.0.1
-
-
- com.sun.xml.bind
- jaxb-impl
- 2.3.3
-
-
- javax.activation
- activation
- 1.1.1
-
org.springframework.boot
spring-boot-starter-test
@@ -54,6 +28,23 @@
lombok
provided
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.11.5
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.11.5
+ runtime
+
diff --git a/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java b/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java
index f65593845868a5b959ccd7990d64a4675e1cf2bb..0229663a12c265c0c36c77dab8eef8cf09b9b792 100644
--- a/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java
+++ b/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java
@@ -1,78 +1,345 @@
package com.example.user.service.common;
-import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.*;
+import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import javax.crypto.SecretKey;
import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-
+/**
+ * JWT工具类 - JSON Web Token 核心处理工具
+ *
+ * 【什么是JWT?】
+ * JWT (JSON Web Token) 是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。
+ * 它是一种无状态的认证方式,服务器不需要保存用户的登录状态。
+ *
+ * 【JWT的结构】
+ * JWT由三部分组成,用点(.)分隔:
+ * 1. Header(头部):包含token类型和签名算法,如 {"typ":"JWT","alg":"HS512"}
+ * 2. Payload(载荷):包含声明信息,如用户名、过期时间等,如 {"sub":"user123","exp":1234567890}
+ * 3. Signature(签名):用于验证token的完整性,防止篡改
+ * 格式:Header.Payload.Signature
+ * 例如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
+ *
+ * 【为什么使用JWT?】
+ * 1. 无状态:服务器不需要存储session,适合分布式系统
+ * 2. 跨域:可以在不同域名间传递认证信息
+ * 3. 移动友好:适合移动应用的认证方式
+ * 4. 性能好:避免了频繁的数据库查询
+ *
+ * 【安全考虑】
+ * 1. 密钥安全:签名密钥必须保密,泄露会导致token可被伪造
+ * 2. HTTPS传输:token应通过HTTPS传输,防止被截获
+ * 3. 过期时间:设置合理的过期时间,平衡安全性和用户体验
+ * 4. 敏感信息:不要在payload中存储敏感信息,因为它只是Base64编码,不是加密
+ *
+ * 【注解说明】
+ * @Slf4j:Lombok注解,自动生成日志对象log,用于记录调试和错误信息
+ * @Component:Spring注解,将该类注册为Spring容器管理的Bean,可以被其他类注入使用
+ */
@Slf4j
+@Component
public class JwtUtil {
+ /**
+ * JWT签名密钥 - 用于保证token安全性的核心要素
+ *
+ * 【作用说明】
+ * 这个密钥用于对JWT进行签名和验证,确保token的完整性和真实性。
+ * 任何人如果没有这个密钥,就无法伪造有效的JWT token。
+ *
+ * 【配置方式】
+ * @Value注解从application.properties或application.yml中读取jwt.secret配置
+ * 如果配置文件中没有设置,则使用冒号后面的默认值
+ *
+ * 【安全要求】
+ * 1. 密钥长度:至少256位(32字节)才能满足HS256算法要求
+ * 2. 密钥复杂度:应包含大小写字母、数字,避免使用简单密码
+ * 3. 密钥保密:绝对不能泄露给客户端或第三方
+ * 4. 生产环境:必须使用环境变量或加密配置文件存储
+ *
+ * 【为什么这样设计?】
+ * 使用@Value注解可以灵活配置密钥,不同环境(开发、测试、生产)可以使用不同的密钥
+ */
+ @Value("${jwt.secret:mySecretKeyForJwtTokenGenerationAndValidation123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789}")
+ private String secret;
- private static final String SECRET_KEY = "123456";
-
-
- private static final long EXPIRATION_TIME = 5 * 60 * 1000; // 5分钟
-
+ /**
+ * JWT token过期时间(毫秒) - 控制token有效期的关键参数
+ *
+ * 【时间设置说明】
+ * 默认值86400000毫秒 = 24小时 = 1天
+ * 计算方式:24小时 × 60分钟 × 60秒 × 1000毫秒 = 86400000毫秒
+ *
+ * 【为什么需要过期时间?】
+ * 1. 安全考虑:限制token的有效期,即使被盗用也会自动失效
+ * 2. 减少风险:缩短攻击者可利用stolen token的时间窗口
+ * 3. 强制重新认证:定期要求用户重新登录,提高安全性
+ *
+ * 【时间设置策略】
+ * - 高安全应用:1-2小时
+ * - 一般应用:24小时
+ * - 低风险应用:7天
+ * - 移动应用:可以设置更长时间,配合refresh token使用
+ *
+ * 【配置灵活性】
+ * 通过@Value注解,可以在不同环境中设置不同的过期时间
+ */
+ @Value("${jwt.expiration:86400000}")
+ private Long expiration;
+ /**
+ * 生成JWT token - 用户登录成功后创建身份凭证
+ *
+ * 【方法作用】
+ * 当用户登录成功后,调用此方法生成一个JWT token作为用户的身份凭证。
+ * 用户后续的请求都需要携带这个token来证明自己的身份。
+ *
+ * 【生成流程】
+ * 1. 获取当前时间作为token的签发时间
+ * 2. 计算token的过期时间(当前时间 + 配置的有效期)
+ * 3. 使用JJWT库的Builder模式构建token
+ * 4. 设置token的各个声明(Claims)
+ * 5. 使用密钥和算法对token进行签名
+ * 6. 生成最终的token字符串
+ *
+ * 【参数说明】
+ * @param username 用户名,将作为token的主体(subject),用于标识token属于哪个用户
+ *
+ * 【返回值说明】
+ * @return String 生成的JWT token,格式为:Header.Payload.Signature
+ *
+ * 【使用场景】
+ * - 用户登录成功后
+ * - 需要为用户创建新的身份凭证时
+ * - token刷新时(如果实现了refresh token机制)
+ */
+ public String generateToken(String username) {
+ // 获取当前时间 - 作为token的签发时间(iat - issued at)
+ Date now = new Date();
+ // 计算过期时间 - 当前时间加上配置的有效期
+ // 这样设计可以灵活控制token的生命周期
+ Date expiryDate = new Date(now.getTime() + expiration);
- public static String generateToken(Long userId, String username, Boolean isSuper) {
- Map claims = new HashMap<>();
- claims.put("id", userId);
- claims.put("name", username);
- claims.put("is_super", isSuper);
-
+ // 使用JJWT库的Builder模式构建JWT Token
+ // Builder模式的优点:代码清晰、易于理解、支持链式调用
return Jwts.builder()
- .setClaims(claims)
- .setIssuedAt(new Date())
- .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
- .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
- .compact();
+ .setSubject(username) // 设置主体(sub):标识token的所有者
+ .setIssuedAt(now) // 设置签发时间(iat):token的创建时间
+ .setExpiration(expiryDate) // 设置过期时间(exp):token的失效时间
+ .signWith(getSigningKey(), SignatureAlgorithm.HS512) // 使用HS512算法和密钥进行签名
+ .compact(); // 生成紧凑的URL安全字符串格式
}
-
- public static Claims parseToken(String token) {
- return Jwts.parser()
- .setSigningKey(SECRET_KEY)
- .parseClaimsJws(token)
- .getBody();
+ /**
+ * 从token中获取用户名 - 解析JWT获取用户身份信息
+ *
+ * 【方法作用】
+ * 当收到客户端发送的JWT token时,需要从中提取用户名来识别用户身份。
+ * 这是JWT认证流程中的关键步骤。
+ *
+ * 【解析过程】
+ * 1. 调用getClaimsFromToken()方法解析token
+ * 2. 从Claims中获取subject字段(用户名)
+ * 3. 返回用户名供后续业务逻辑使用
+ *
+ * 【为什么这样设计?】
+ * JWT的payload部分包含了用户信息,通过解析可以直接获取,
+ * 无需查询数据库,提高了性能。
+ *
+ * @param token JWT token字符串
+ * @return String 用户名,如果token格式错误或解析失败则可能抛出异常
+ *
+ * 【注意事项】
+ * - 此方法不验证token是否过期,只负责提取用户名
+ * - 如果token被篡改,解析时会抛出签名验证异常
+ */
+ public String getUsernameFromToken(String token) {
+ // 解析token获取载荷(Claims) - Claims包含了token中的所有声明信息
+ Claims claims = getClaimsFromToken(token);
+ // 从载荷中获取主体(subject),即用户名
+ // subject是JWT标准中用于标识token所有者的字段
+ return claims.getSubject();
}
-
- public static Long getUserIdFromToken(String token) {
- Claims claims = parseToken(token);
- return claims.get("id", Long.class);
+ /**
+ * 验证token是否有效 - JWT认证的核心验证逻辑
+ *
+ * 【验证目的】
+ * 确保接收到的JWT token是合法、有效且未被篡改的。
+ * 这是保护API安全的重要防线。
+ *
+ * 【验证步骤】
+ * 1. 从token中提取用户名
+ * 2. 比较token中的用户名与期望的用户名是否一致
+ * 3. 检查token是否已过期
+ * 4. 隐式验证:getClaimsFromToken()会验证签名
+ *
+ * 【为什么需要双重验证?】
+ * - 用户名匹配:确保token属于正确的用户
+ * - 过期检查:确保token仍在有效期内
+ * - 签名验证:确保token未被篡改(在解析过程中自动完成)
+ *
+ * @param token JWT token字符串
+ * @param username 期望的用户名,通常来自请求上下文
+ * @return boolean true表示token有效,false表示无效
+ *
+ * 【异常处理】
+ * 使用try-catch捕获所有可能的异常(签名错误、格式错误等),
+ * 确保方法不会因异常而中断,统一返回false表示验证失败。
+ */
+ public boolean validateToken(String token, String username) {
+ try {
+ // 从token中获取用户名 - 这一步会验证token的签名
+ String tokenUsername = getUsernameFromToken(token);
+ // 验证用户名是否匹配且token未过期
+ // 两个条件都必须满足才认为token有效
+ return (username.equals(tokenUsername) && !isTokenExpired(token));
+ } catch (Exception e) {
+ // 捕获解析token时的异常(如签名错误、格式错误等)
+ // 记录错误日志便于调试,但不暴露具体错误信息给客户端
+ log.error("Token validation failed: {}", e.getMessage());
+ return false; // 任何异常都视为验证失败
+ }
}
-
- public static String getUsernameFromToken(String token) {
- Claims claims = parseToken(token);
- return claims.get("name", String.class);
+ /**
+ * 检查token是否过期
+ *
+ * @param token JWT token
+ * @return boolean token是否过期
+ */
+ public boolean isTokenExpired(String token) {
+ // 获取token的过期时间
+ Date expiration = getExpirationDateFromToken(token);
+ // 比较过期时间是否在当前时间之前
+ return expiration.before(new Date());
}
- public static Boolean getIsSuperFromToken(String token) {
- Claims claims = parseToken(token);
- return claims.get("is_super", Boolean.class);
+ /**
+ * 从token中获取过期时间
+ *
+ * @param token JWT token
+ * @return Date 过期时间
+ */
+ public Date getExpirationDateFromToken(String token) {
+ // 解析token获取载荷(Claims)
+ Claims claims = getClaimsFromToken(token);
+ // 从载荷中获取过期时间
+ return claims.getExpiration();
}
+ /**
+ * 从token中解析Claims - JWT解析的核心方法
+ *
+ * 【Claims概念】
+ * Claims是JWT的载荷(Payload)部分,包含了关于用户和token的声明信息。
+ * 标准声明包括:sub(主体)、exp(过期时间)、iat(签发时间)等。
+ *
+ * 【解析流程】
+ * 1. 创建JWT解析器(Parser)
+ * 2. 设置签名密钥用于验证token完整性
+ * 3. 解析token字符串,验证签名
+ * 4. 提取并返回Claims对象
+ *
+ * 【安全机制】
+ * 解析过程中会自动验证token的签名,如果token被篡改,
+ * 签名验证会失败并抛出异常,确保了token的安全性。
+ *
+ * 【为什么设计为private?】
+ * 这是一个内部工具方法,只供本类的其他方法使用,
+ * 不需要暴露给外部调用者,符合封装原则。
+ *
+ * @param token JWT token字符串
+ * @return Claims 载荷对象,包含token中的所有声明信息
+ * @throws JwtException 如果token格式错误、签名无效或已过期
+ */
+ private Claims getClaimsFromToken(String token) {
+ return Jwts.parserBuilder()
+ .setSigningKey(getSigningKey()) // 设置签名密钥用于验证token完整性
+ .build() // 构建解析器
+ .parseClaimsJws(token) // 解析JWT token并验证签名
+ .getBody(); // 获取载荷部分(Claims)
+ }
- public static boolean validateToken(String token) {
- try {
- parseToken(token);
- return true;
- } catch (Exception e) {
- log.error("JWT令牌验证失败: {}", e.getMessage());
- return false;
- }
+ /**
+ * 获取签名密钥 - 将配置的字符串密钥转换为加密算法所需的密钥对象
+ *
+ * 【转换目的】
+ * JJWT库需要SecretKey对象来进行HMAC签名算法,
+ * 而我们配置的是字符串,需要转换为合适的密钥格式。
+ *
+ * 【HMAC算法说明】
+ * HMAC (Hash-based Message Authentication Code) 是一种基于哈希的消息认证码算法。
+ * 它结合了哈希函数和密钥,既能验证数据完整性,又能验证数据来源。
+ *
+ * 【转换过程】
+ * 1. 将字符串密钥转换为字节数组
+ * 2. 使用Keys.hmacShaKeyFor()方法生成适合HMAC-SHA算法的密钥
+ * 3. 返回SecretKey对象供签名和验证使用
+ *
+ * 【安全考虑】
+ * - 密钥长度必须足够(至少256位)
+ * - 密钥应该是随机生成的,不能是简单的字符串
+ * - 密钥必须保密,不能泄露给客户端
+ *
+ * @return SecretKey 适用于HMAC-SHA算法的签名密钥
+ */
+ private SecretKey getSigningKey() {
+ // 将字符串密钥转换为字节数组
+ // 使用UTF-8编码确保字符串到字节的转换一致性
+ byte[] keyBytes = secret.getBytes();
+ // 使用JJWT提供的工具方法生成HMAC-SHA密钥
+ // 该方法会自动选择合适的HMAC算法(HS256、HS384、HS512)
+ return Keys.hmacShaKeyFor(keyBytes);
}
- public static boolean isTokenExpired(String token) {
- Claims claims = parseToken(token);
- return claims.getExpiration().before(new Date());
+ /**
+ * 从请求头中提取token - 解析HTTP Authorization头获取JWT token
+ *
+ * 【HTTP Authorization头格式】
+ * 根据RFC 6750 (OAuth 2.0 Bearer Token Usage)标准,
+ * JWT token应该放在HTTP请求头的Authorization字段中,
+ * 格式为: "Authorization: Bearer "
+ *
+ * 【为什么使用Bearer?】
+ * Bearer是OAuth 2.0标准中定义的token类型,表示"持有者token",
+ * 意思是任何持有该token的人都可以使用它,无需额外的身份验证。
+ *
+ * 【提取逻辑】
+ * 1. 检查Authorization头是否存在
+ * 2. 验证是否以"Bearer "开头(注意Bearer后有一个空格)
+ * 3. 提取"Bearer "后面的token部分
+ * 4. 如果格式不正确,返回null
+ *
+ * 【使用场景】
+ * 主要在JWT认证过滤器中使用,用于从HTTP请求中提取token,
+ * 然后进行后续的验证和用户身份识别。
+ *
+ * 【安全考虑】
+ * - 只接受标准的Bearer格式,拒绝其他格式
+ * - 返回null而不是抛出异常,便于调用者处理
+ * - 不对token内容进行验证,只负责提取
+ *
+ * @param authHeader HTTP请求中的Authorization头的值
+ * @return String JWT token字符串,如果格式不正确则返回null
+ *
+ * 【示例】
+ * 输入: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+ * 输出: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+ */
+ public String extractTokenFromHeader(String authHeader) {
+ // 检查Authorization头是否存在且以"Bearer "开头
+ // 注意:"Bearer "后面有一个空格,这是标准格式要求
+ if (authHeader != null && authHeader.startsWith("Bearer ")) {
+ // 提取Bearer后面的token部分
+ // substring(7)是因为"Bearer "有7个字符(包括空格)
+ return authHeader.substring(7);
+ }
+ // 如果格式不正确,返回null,让调用者知道没有有效的token
+ return null;
}
}
\ No newline at end of file
diff --git a/user-service/user-service-common/src/main/java/com/example/user/service/common/config/JwtConfig.java b/user-service/user-service-common/src/main/java/com/example/user/service/common/config/JwtConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..5abf081c54efdcdabf40648e8d50572cc94d5489
--- /dev/null
+++ b/user-service/user-service-common/src/main/java/com/example/user/service/common/config/JwtConfig.java
@@ -0,0 +1,29 @@
+// JwtConfig.java - 手动实现getter/setter
+package com.example.user.service.common.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ConfigurationProperties(prefix = "jwt")
+public class JwtConfig {
+ private String secret;
+ private long expiration;
+
+ // 手动实现getter和setter
+ public String getSecret() {
+ return secret;
+ }
+
+ public void setSecret(String secret) {
+ this.secret = secret;
+ }
+
+ public long getExpiration() {
+ return expiration;
+ }
+
+ public void setExpiration(long expiration) {
+ this.expiration = expiration;
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-domain/pom.xml b/user-service/user-service-domain/pom.xml
index 502cbe0cdd4a7f789e429f56d87b2162bb6f9b42..79ea885fe6ca9c480438c70717b4488f4dda6983 100644
--- a/user-service/user-service-domain/pom.xml
+++ b/user-service/user-service-domain/pom.xml
@@ -40,6 +40,12 @@
org.springframework.security
spring-security-crypto
+
+ org.jetbrains
+ annotations
+ 13.0
+ compile
+
diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java
index 8d9521b5c44f57f74deb13c1872241318475abe4..959897e7551b8acb7943e5129a08aa13eff9afd5 100644
--- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java
@@ -9,6 +9,7 @@ import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
+import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@@ -17,69 +18,133 @@ import java.util.List;
@ToString
public class User {
private UserId id;
- private UserName name;
- private UserAge age;
+ private UserName username;
private Email email;
private Password password;
- private IsSuper isSuper; // 添加isSuper字段
+ private Phone phone;
+ private RealName realName;
+ private UserStatus status;
+ private UserRole role;
+ private LocalDateTime createTime;
+ private LocalDateTime updateTime;
+ private LocalDateTime lastLoginTime;
+
+ // 私有默认构造方法,用于框架反射
public User() {
}
- public User(UserId id, UserName name, UserAge age, Email email, Password password,IsSuper isSuper) {
+ // 静态工厂方法 - 用于从持久化实体重建领域对象
+ public User(UserId id, UserName username, Password password, Email email,
+ Phone phone, RealName realName, UserStatus status, UserRole role) {
this.id = id;
- this.name = name;
- this.age = age;
- this.email = email;
+ this.username = username;
this.password = password;
- this.isSuper = isSuper;
+ this.email = email;
+ this.phone = phone;
+ this.realName = realName;
+ this.status = status;
+ this.role = role;
}
- public User( UserName name, UserAge age, Email email, Password password) {
- this.id= genId() ;
- this.name = name;
- this.age = age;
- this.email = email;
- this.password = password;
- this.isSuper = new IsSuper(false);
+ // 静态工厂方法 - 用于创建新用户(注册时使用)
+ public static User createUser(UserName userName, Password password, Email email,
+ Phone phone, RealName realName, UserRole role) {
+ User user = new User();
+ user.id = user.genId();
+ user.username = userName;
+ user.password = password;
+ user.email = email;
+ user.phone = phone;
+ user.realName = realName;
+ user.status = UserStatus.ENABLED; // 默认激活状态
+ user.role = role != null ? role : UserRole.USER; // 默认用户角色
+ user.createTime = LocalDateTime.now().withNano(0);
+ user.updateTime = LocalDateTime.now().withNano(0);
+ user.lastLoginTime = LocalDateTime.now().withNano(0);
+ return user;
}
- public User(UserId userId, UserName userName, UserAge userAge, Email email) {
- this.id = id;
- this.name = name;
- this.age = age;
+ // 静态工厂方法 - 用于从数据库重建用户(查询时使用)
+ public static User reconstruct(UserId id, UserName username, Email email, Password password,
+ Phone phone, RealName realName, UserStatus status, UserRole role,
+ LocalDateTime createTime, LocalDateTime updateTime, LocalDateTime lastLoginTime) {
+ User user = new User();
+ user.id = id;
+ user.username = username;
+ user.email = email;
+ user.password = password;
+ user.phone = phone;
+ user.realName = realName;
+ user.status = status;
+ user.role = role;
+ user.createTime = createTime;
+ user.updateTime = updateTime;
+ user.lastLoginTime = lastLoginTime;
+ return user;
+ }
+
+ // 业务方法 - 更新用户信息
+ public void updateProfile(Email email, Phone phone, RealName realName) {
this.email = email;
- this.isSuper = new IsSuper(false);
+ this.phone = phone;
+ this.realName = realName;
+ this.updateTime = LocalDateTime.now(); // 自动更新修改时间
}
+ // 业务方法 - 更新密码
+ public void changePassword(Password newPassword) {
+ this.password = newPassword;
+ this.updateTime = LocalDateTime.now(); // 自动更新修改时间
+ }
+
+ // 业务方法 - 用户登录
+ public void login() {
+ this.lastLoginTime = LocalDateTime.now();
+ this.updateTime = LocalDateTime.now(); // 更新最后修改时间
+ }
+
+ // 业务方法 - 激活/禁用用户
+ public void activate() {
+ this.status = UserStatus.ENABLED;
+ this.updateTime = LocalDateTime.now();
+ }
- public static List getUsers(GetUserListPort getUserListPort){
+ public void deactivate() {
+ this.status = UserStatus.ENABLED;
+ this.updateTime = LocalDateTime.now();
+ }
+
+ public static List getUsers(GetUserListPort getUserListPort) {
return getUserListPort.getUsers();
}
/**
* 根据用户名查询用户
- * 当需要使用类似GetUserByNamePort这种对象的时候,需要在方法参数注入该对象
- * 因为通过构造方法或者字段注入都会失败,因为方法是静态方法,会早于对象创建,导致对象无法注入
- * @param name 用户名
- * @param getUserByNamePort 查询用户的端口
- * @return 用户模型
*/
- public static User getUserByName(String name, GetUserByNamePort getUserByNamePort){
+ public static User getUserByName(String name, GetUserByNamePort getUserByNamePort) {
User user = getUserByNamePort.getUserByName(name);
log.info("user:{}", user);
return user;
}
- public UserId genId(){
+ private UserId genId() {
return new UserId(new IdWorker().nextId());
}
/**
* 验证密码
- * @param password 密码 明文还是密文?
- * @return 验证结果
*/
- public boolean validatePassword(String password){
- return this.password.verify( password);
+ public boolean validatePassword(String password) {
+ return this.password.verify(password);
+ }
+
+ // 验证业务规则的方法
+ public boolean isValid() {
+ return username != null &&
+ password != null &&
+ email != null &&
+ status != null &&
+ role != null &&
+ createTime != null;
}
-}
+}
\ No newline at end of file
diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Email.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Email.java
index f83b24311157286872b13584c9780f3279a4f762..8ca701a34e1b35ac37c6097abd9b3b3ab5d98bdd 100644
--- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Email.java
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Email.java
@@ -1,7 +1,24 @@
package com.example.user.service.domain.valueobject;
-public record Email(String email) {
- public String getValue() {
- return email;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public record Email(String value) {
+ public static Email of(String email) {
+ if (email == null || email.isBlank()) {
+ throw new RuntimeException("邮箱不能为空");
+ }
+
+ if (!email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
+ throw new RuntimeException("邮箱格式不正确");
+ }
+
+ return new Email(email);
+ }
+
+ public String getValue() {return value;}
+
+ public boolean isValid() {
+ return value != null && value.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
}
diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/IsSuper.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/IsSuper.java
deleted file mode 100644
index f05ce455c4d7b8f6c90f818413590ec88c4c37f8..0000000000000000000000000000000000000000
--- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/IsSuper.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.example.user.service.domain.valueobject;
-
-
-public record IsSuper(boolean value) {
- public IsSuper {
-
- if (value && !isValidSuperUser()) {
- throw new IllegalArgumentException("Invalid super user configuration");
- }
- }
- private boolean isValidSuperUser() {
-
- return true;
- }
-
- public static IsSuper fromBoolean(boolean value) {
- return new IsSuper(value);
- }
-
-}
\ No newline at end of file
diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Phone.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Phone.java
new file mode 100644
index 0000000000000000000000000000000000000000..4634d88e7d506339eca370646202b8cb3ad98780
--- /dev/null
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Phone.java
@@ -0,0 +1,28 @@
+package com.example.user.service.domain.valueobject;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @Author: zhangyucheng
+ * @Date: 2025/9/16 14:15
+ */
+@Slf4j
+public record Phone(String value) {
+ public static Phone of(String phone) {
+ if (phone == null || phone.isBlank()) {
+ throw new RuntimeException("手机号不能为空");
+ }
+
+ if (!phone.matches("^1[3-9]\\d{9}$")) {
+ throw new RuntimeException("手机号格式不正确");
+ }
+
+ return new Phone(phone);
+ }
+
+ public String getValue() {return value;}
+
+ public boolean isValid() {
+ return value != null && value.matches("^1[3-9]\\d{9}$");
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/RealName.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/RealName.java
new file mode 100644
index 0000000000000000000000000000000000000000..0b5884a3b063cd0905488b83c3af4fc9b6c1d846
--- /dev/null
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/RealName.java
@@ -0,0 +1,28 @@
+package com.example.user.service.domain.valueobject;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @Author: zhangyucheng
+ * @Date: 2025/9/16 14:15
+ */
+@Slf4j
+public record RealName(String value) {
+ public static RealName of(String name) {
+ if (name == null || name.isBlank()) {
+ throw new RuntimeException("真实姓名不能为空");
+ }
+
+ if (name.length() < 2 || name.length() > 20) {
+ throw new RuntimeException("真实姓名长度必须在2-20个字符之间");
+ }
+
+ return new RealName(name.trim());
+ }
+
+ public String getValue() {return value;}
+
+ public boolean isValid() {
+ return value != null && value.length() >= 2 && value.length() <= 20;
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserAge.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserAge.java
deleted file mode 100644
index 38ef9370aa8f675e832d9cbebea3ef1ac069fd3f..0000000000000000000000000000000000000000
--- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserAge.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.example.user.service.domain.valueobject;
-
-public record UserAge(int age) {
- public int getValue() {
- return age;
- }
-}
diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserName.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserName.java
index 0c11a15528b836bbc7852e1f763f9233b3f30283..184129827d9f3094c37a0c62ba4f7478d751e3ca 100644
--- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserName.java
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserName.java
@@ -1,7 +1,30 @@
package com.example.user.service.domain.valueobject;
-public record UserName(String username) {
- public String getValue() {
- return username;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public record UserName(String value) {
+ public static UserName of(String username) {
+ if (username == null || username.isBlank()) {
+ throw new RuntimeException("用户名不能为空");
+ }
+
+ if (username.length() < 3 || username.length() > 20) {
+ throw new RuntimeException("用户名长度必须在3-20个字符之间");
+ }
+
+ if (!username.matches("^[a-zA-Z0-9_]+$")) {
+ throw new RuntimeException("用户名只能包含字母、数字和下划线");
+ }
+
+ return new UserName(username);
+ }
+ public String getValue() {return value;}
+
+ public boolean isValid() {
+ return value != null &&
+ value.length() >= 3 &&
+ value.length() <= 20 &&
+ value.matches("^[a-zA-Z0-9_]+$");
}
-}
+}
\ No newline at end of file
diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserRole.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserRole.java
new file mode 100644
index 0000000000000000000000000000000000000000..620b6c7cca9d8eda0cb29a6ac02af578053e5b0e
--- /dev/null
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserRole.java
@@ -0,0 +1,72 @@
+package com.example.user.service.domain.valueobject;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 用户角色值对象
+ */
+@Slf4j
+public record UserRole(String value) {
+ public static final UserRole ADMIN = new UserRole("ADMIN");
+ public static final UserRole USER = new UserRole("USER");
+
+ public UserRole {
+ if (value == null || value.isBlank()) {
+ throw new IllegalArgumentException("用户角色不能为空");
+ }
+ // 修改验证逻辑,直接使用字符串比较,不要引用静态字段
+ if (!"ADMIN".equals(value) && !"USER".equals(value)) {
+ throw new IllegalArgumentException("用户角色必须是ADMIN或USER");
+ }
+ }
+
+ private static boolean isValidRole(String role) {
+ // 同样修改这里
+ return "ADMIN".equals(role) || "USER".equals(role);
+ }
+ // 静态工厂方法
+ public static UserRole of(String role) {
+ return new UserRole(role);
+ }
+
+ public static UserRole admin() {
+ return ADMIN;
+ }
+
+ public static UserRole user() {
+ return USER;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public boolean isAdmin() {
+ return ADMIN.value.equals(value);
+ }
+
+ public boolean isUser() {
+ return USER.value.equals(value);
+ }
+
+ public boolean isValid(String role) {
+ return ADMIN.value.equals(role) || USER.value.equals(role);
+ }
+ @Override
+ public String toString() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null || getClass() != obj.getClass()) return false;
+ UserRole userRole = (UserRole) obj;
+ return value.equals(userRole.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+}
\ No newline at end of file
diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserStatus.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserStatus.java
new file mode 100644
index 0000000000000000000000000000000000000000..a893b6acf3b2577089b0e8d2731730e1850e2f27
--- /dev/null
+++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UserStatus.java
@@ -0,0 +1,83 @@
+package com.example.user.service.domain.valueobject;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 用户状态值对象
+ */
+
+@Slf4j
+public record UserStatus(Integer value) {
+ public static final UserStatus DISABLED = new UserStatus(0);
+ public static final UserStatus ENABLED = new UserStatus(1);
+ public static final UserStatus PENDING = new UserStatus(2); // 可选:添加待激活状态
+
+ public UserStatus {
+ if (value == null) {
+ throw new IllegalArgumentException("用户状态不能为空");
+ }
+ // 直接使用数字字面量进行比较,不要引用静态字段
+ if (value != 0 && value != 1 && value != 2) {
+ throw new IllegalArgumentException("用户状态必须是0(禁用)、1(启用)或2(待激活)");
+ }
+ }
+
+ // 静态工厂方法
+ public static UserStatus of(Integer status) {
+ return new UserStatus(status);
+ }
+
+ public static UserStatus disabled() {
+ return DISABLED;
+ }
+
+ public static UserStatus enabled() {
+ return ENABLED;
+ }
+
+ public static UserStatus pending() {
+ return PENDING;
+ }
+
+ public boolean isEnabled() {
+ return Integer.valueOf(1).equals(value);
+ }
+
+ public boolean isDisabled() {
+ return Integer.valueOf(0).equals(value);
+ }
+
+ public boolean isPending() {
+ return Integer.valueOf(2).equals(value);
+ }
+
+ private static boolean isValidStatus(Integer status) {
+ // 同样修改这里,使用字面量
+ return Integer.valueOf(0).equals(status) ||
+ Integer.valueOf(1).equals(status) ||
+ Integer.valueOf(2).equals(status);
+ }
+
+ // 转换方法:从数据库值创建
+ public Integer getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(value);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null || getClass() != obj.getClass()) return false;
+ UserStatus that = (UserStatus) obj;
+ return value.equals(that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+}
\ No newline at end of file