diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/controller/AIChatController.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/controller/AIChatController.java new file mode 100644 index 0000000000000000000000000000000000000000..5f3dade84f372d1d23560249bfbdce23ddaeaf0a --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/controller/AIChatController.java @@ -0,0 +1,98 @@ +package com.example.qa.adapter.in.web.controller; + +import com.example.qa.service.application.command.CreateQaCommand; +import com.example.qa.service.application.config.RabbitConfig; +import com.example.qa.service.application.port.in.AIChatUseCase; +import com.example.qa.service.application.port.in.CreateQaUseCase; +import com.example.qa.service.common.dto.ChatCompletionRequestDTO; +import com.example.qa.service.common.dto.ChatCompletionResponseDTO; +import com.example.qa.service.domain.Qa; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.web.bind.annotation.*; + +@Slf4j +@RequestMapping("/api") +@RestController +@RequiredArgsConstructor +public class AIChatController { + + private final AIChatUseCase aiChatUseCase; + private final CreateQaUseCase createQaUseCase; + private final RabbitTemplate rabbitTemplate; + private final ObjectMapper objectMapper; + + /** + * AI问答接口 + */ + @PostMapping("/chat") + public ChatCompletionResponseDTO chat(@RequestBody ChatCompletionRequestDTO request) { + log.info("AI问答请求: {}", request); + + // 从请求中提取用户的问题 + String question = null; + if (request.getMessages() != null && !request.getMessages().isEmpty()) { + // 假设最后一条消息是用户的问题 + question = request.getMessages().get(request.getMessages().size() - 1).getContent(); + } + + // 获取AI的回答 + ChatCompletionResponseDTO response = aiChatUseCase.chatWithAI(request); + + // 从响应中提取AI的回答 + String answer = null; + if (response.getChoices() != null && !response.getChoices().isEmpty()) { + answer = response.getChoices().get(0).getMessage().getContent(); + } + + // 保存问答记录到数据库 + if (question != null && answer != null) { + CreateQaCommand command = CreateQaCommand.builder() + .QaQuestion(question) + .QaAnswer(answer) + .build(); + createQaUseCase.createQa(command); + log.info("问答记录已保存到数据库"); + } + + return response; + } + + /** + * 发送字符串消息到RabbitMQ队列 + */ + @GetMapping("/send") + public String sendMessage(@RequestParam String message) { + log.info("发送消息到test队列: {}", message); + rabbitTemplate.convertAndSend(RabbitConfig.TEST_QUEUE, message); + return "消息发送成功"; + } + + /** + * 发送Qa对象到RabbitMQ队列 + */ + @PostMapping("/sendQa") + public String sendQa(@RequestBody Qa qa) { + log.info("发送qa对象到test2队列: {}", qa); + rabbitTemplate.convertAndSend(RabbitConfig.TEST_QUEUE_2, qa); + return "Qa对象发送成功"; + } + + /** + * 发送JSON字符串到RabbitMQ队列 + */ + @PostMapping("/sendJson") + public String sendJson(@RequestBody Object json) { + try { + String jsonString = objectMapper.writeValueAsString(json); + log.info("发送JSON到test3队列: {}", jsonString); + rabbitTemplate.convertAndSend(RabbitConfig.TEST_QUEUE_3, jsonString); + return "JSON发送成功"; + } catch (Exception e) { + log.error("JSON序列化失败: {}", e.getMessage()); + return "JSON发送失败"; + } + } +} \ No newline at end of file diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/controller/QaController.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/controller/QaController.java new file mode 100644 index 0000000000000000000000000000000000000000..17877ee1dc3bd93c3aefbf9bb0c9d0ebfc207401 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/controller/QaController.java @@ -0,0 +1,117 @@ +package com.example.qa.adapter.in.web.controller; + +import com.example.qa.adapter.in.web.dto.CreateQaRequestDTO; +import com.example.qa.adapter.in.web.dto.UpdateQaRequestDTO; +import com.example.qa.adapter.in.web.dto.QaResponseDTO; +import com.example.qa.service.application.command.CreateQaCommand; +import com.example.qa.service.application.command.UpdateQaCommand; +import com.example.qa.service.application.port.in.*; +import com.example.qa.service.domain.Qa; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Slf4j +@RequestMapping("/qas") +@RestController +@RequiredArgsConstructor +public class QaController { + + private final GetQaListUseCase getQaListUseCase; + private final CreateQaUseCase createQaUseCase; + private final DeleteQaUseCase deleteQaUseCase; + private final UpdateQaUseCase updateQaUseCase; + private final GetQaByIdUseCase getQaByIdUseCase; + + /** + * 获取所有问答列表 + */ + @GetMapping("") + public List getQas() { + log.info("getQas"); + return getQaListUseCase.getQas(); + } + + /** + * 创建新问答 + */ + @PostMapping() + public Qa createQa(@RequestBody CreateQaRequestDTO createQaRequestDTO){ + CreateQaCommand command = CreateQaCommand.builder() + .QaQuestion(createQaRequestDTO.QaQuestion()) + .QaAnswer(createQaRequestDTO.QaAnswer()) + .build(); + + return createQaUseCase.createQa(command); + } + + /** + * 删除问答 + */ + @DeleteMapping("{id}") + public String deleteQa(@PathVariable("id") Long id){ + deleteQaUseCase.deleteQa(id); + return "success"; + } + + /** + * 更新问答信息 + */ + @PutMapping("") + public Qa updateQa(@RequestBody UpdateQaRequestDTO updateQaRequestDTO){ + UpdateQaCommand command = UpdateQaCommand.builder() + .QaId(updateQaRequestDTO.QaId()) + .QaQuestion(updateQaRequestDTO.QaQuestion()) + .QaIAnswer(updateQaRequestDTO.QaIAnswer()) + .build(); + return updateQaUseCase.updateQa(command); + } + + /** + * 根据ID获取问答详情 + */ + @GetMapping("{id}") + public QaResponseDTO getQaById(@PathVariable("id") Long id){ + Qa qa = getQaByIdUseCase.getQaById(id); + // 映射QA表字段到响应DTO + return new QaResponseDTO( + qa.getQaId().id(), + qa.getQaQuestion().question(), + qa.getQaIAnswer().answer() + ); + } +} + + +/** + * @author ZhengYuan + * @param getQaListUsecase + * @return + */ + +/** + * 更新信息 + * @author qinshijie && liuxin + * @param updateQaRequestDTO 更新用户请求DTO + * @return 更新后的信息 + */ + +/** + * @author ChenYiZhao + * @param deleteQa + * @return + */ + +/** + * @author XieQiWei + * @param createQa 创建问答 + * @return + */ + +/** + * @author zhangxuanwei + * @param getQaById 根据id查找问答 + * * @return + * */ \ No newline at end of file diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/CreateQaRequestDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/CreateQaRequestDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..f14d3c1cb8382d84f42b182a34cd848e5c934625 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/CreateQaRequestDTO.java @@ -0,0 +1,10 @@ +package com.example.qa.adapter.in.web.dto; + + +public record CreateQaRequestDTO( + Long id, + String QaQuestion, + String QaAnswer +) { + +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/QaResponseDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/QaResponseDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..0a731725e687deeac851a92e6a1eb1da4ff58836 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/QaResponseDTO.java @@ -0,0 +1,19 @@ +package com.example.qa.adapter.in.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author ChenYiZhao + * @param QaResponseDTO + * @return + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class QaResponseDTO { + private long QaId; + private String QaQuestion; + private String QaIAnswer; +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/UpdateQaRequestDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/UpdateQaRequestDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..6b0fd4bd01719eeb4e6531d90920dcdd8a1599e5 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/com/example/qa/adapter/in/web/dto/UpdateQaRequestDTO.java @@ -0,0 +1,12 @@ +package com.example.qa.adapter.in.web.dto; + +public record UpdateQaRequestDTO( Long QaId, + String QaQuestion, + String QaIAnswer) { +} +/** + * @author ZhengYuan + * @param UpdateQaRequestDTO + * @return + */ + diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/CreateQaBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/CreateQaBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..7f1da2654661841337bfddfe69d697266c3351d3 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/CreateQaBridge.java @@ -0,0 +1,31 @@ +package com.example.qa.adapter.out.persistence.bridge; + +import com.example.qa.adapter.out.persistence.convertor.QaConvertor; +import com.example.qa.adapter.out.persistence.entity.QaEntity; +import com.example.qa.adapter.out.persistence.mapper.QaMapper; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.port.CreateQaPort; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + + +@Slf4j +@Component +public class CreateQaBridge implements CreateQaPort { + @Resource + private QaMapper qaMapper; + + @Override + public Qa createQa(Qa qa) { + // 将领域对象转换为持久化实体 + QaEntity qaEntity = QaConvertor.toEntity(qa); + // 执行入库操作 + int result = qaMapper.insert(qaEntity); + // 记录受影响行数日志 + log.info("result:{}", result); + return qa; + } +} + + diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/DeleteQaBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/DeleteQaBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..be2c96751b32541045f7c5ed882f672747abbcf0 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/DeleteQaBridge.java @@ -0,0 +1,25 @@ +package com.example.qa.adapter.out.persistence.bridge; + +import com.example.qa.adapter.out.persistence.mapper.QaMapper; +import com.example.qa.service.domain.port.DeleteQaPort; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * @author ChenYiZhao + * @param DeleteQaBridge + * @return + */ + +@Slf4j +@Component +public class DeleteQaBridge implements DeleteQaPort { + @Resource + private QaMapper qaMapper; + @Override + public void deleteQa(Long id) { + int result = qaMapper.deleteById(id); + log.info("result:{}",result); + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/GetQaByIdBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/GetQaByIdBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..27f0576914c2ab799703b9dc94f193d0540572dc --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/GetQaByIdBridge.java @@ -0,0 +1,23 @@ +package com.example.qa.adapter.out.persistence.bridge; + +import com.example.qa.adapter.out.persistence.convertor.QaConvertor; +import com.example.qa.adapter.out.persistence.entity.QaEntity; +import com.example.qa.adapter.out.persistence.mapper.QaMapper; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.port.GetQaByIdPort; +import com.example.qa.service.domain.valueobject.QaId; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class GetQaByIdBridge implements GetQaByIdPort { + @Resource + private QaMapper qaMapper; + @Override + public Qa getQaById(QaId id) { + QaEntity qaEntity = qaMapper.selectById(id.id()); + return QaConvertor.toDomain(qaEntity); + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/GetQaListBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/GetQaListBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..8272144bd36351fad58b5f07048cc7b2dcfb739f --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/GetQaListBridge.java @@ -0,0 +1,36 @@ +package com.example.qa.adapter.out.persistence.bridge; + +import com.example.qa.adapter.out.persistence.convertor.QaConvertor; +import com.example.qa.adapter.out.persistence.entity.QaEntity; +import com.example.qa.adapter.out.persistence.mapper.QaMapper; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.port.GetQaListPort; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * QA列表查询桥接类,实现从持久化层到领域层的数据转换 + * @author ZhengYuan + */ +@Slf4j +@Component +public class GetQaListBridge implements GetQaListPort { + + @Resource + private QaMapper qaMapper; + + @Override + public List getQas() { + log.info("查询QA列表"); + List entities = qaMapper.selectList(null); + + // 使用Stream API简化集合转换 + return entities.stream() + .map(QaConvertor::toDomain) + .collect(Collectors.toList()); + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/UpdateQaBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/UpdateQaBridge.java new file mode 100644 index 0000000000000000000000000000000000000000..f59f988eb494791170d93e4e81031542ee61b274 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/bridge/UpdateQaBridge.java @@ -0,0 +1,30 @@ +package com.example.qa.adapter.out.persistence.bridge; + +import com.example.qa.adapter.out.persistence.convertor.QaConvertor; +import com.example.qa.adapter.out.persistence.mapper.QaMapper; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.port.UpdateQaPort; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class UpdateQaBridge implements UpdateQaPort { + @Resource + private QaMapper qaMapper; + + @Override + public Qa updateQa(Qa qa) { + int result = qaMapper.updateById(QaConvertor.toEntity(qa)); + log.info("result:{}",result); + return qa; + } +} + +/** + * 更新信息 + * @author qinshijie && liuxin + * @param UpdateQaBridge + * @return + */ \ No newline at end of file diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/convertor/QaConvertor.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/convertor/QaConvertor.java new file mode 100644 index 0000000000000000000000000000000000000000..d32005b2d79a73a58a0ab920fe524a911f2938c6 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/convertor/QaConvertor.java @@ -0,0 +1,37 @@ +package com.example.qa.adapter.out.persistence.convertor; + +import com.example.qa.adapter.out.persistence.entity.QaEntity; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.valueobject.QaId; +import com.example.qa.service.domain.valueobject.QaQuestion; +import com.example.qa.service.domain.valueobject.QaAnswer; + +public class QaConvertor { + + public static Qa toDomain(QaEntity qaEntity) { + return new Qa( + new QaId(qaEntity.getQaId()), + new QaQuestion(qaEntity.getQaQuestion()), + new QaAnswer(qaEntity.getQaIAnswer()) + ); + } + + public static QaEntity toEntity(Qa qa) { + return new QaEntity( + qa.getQaId().id(), + qa.getQaQuestion().question(), + qa.getQaIAnswer().answer() + ); + } +} +/** + * @author ZhengYuan + * @param QaConvertor + * @return + */ + +/** + * @author ChenYiZhao + * @param QaConvertor + * @return + */ \ No newline at end of file diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/entity/QaEntity.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/entity/QaEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..d7d9d24bd6fa7f160b3822b0cdfb43a5ec3a990a --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/entity/QaEntity.java @@ -0,0 +1,26 @@ +package com.example.qa.adapter.out.persistence.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName("qa") +public class QaEntity { + @TableId(value = "id", type = IdType.ASSIGN_ID) + private long QaId; + + @TableField("question") + private String QaQuestion; + + @TableField("answer") + private String QaIAnswer; +} + diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/mapper/QaMapper.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/mapper/QaMapper.java new file mode 100644 index 0000000000000000000000000000000000000000..56b66c7ec39b87141ee81c483985ca52a9e59517 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/com/example/qa/adapter/out/persistence/mapper/QaMapper.java @@ -0,0 +1,12 @@ +package com.example.qa.adapter.out.persistence.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.qa.adapter.out.persistence.entity.QaEntity; + +public interface QaMapper extends BaseMapper { +} +/** + * @author ZhengYuan + * @param QaMapper + * @return + */ diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/command/CreateQaCommand.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/command/CreateQaCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..59e4d960fef651825afdfaaab8fc25375ab03b30 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/command/CreateQaCommand.java @@ -0,0 +1,12 @@ +package com.example.qa.service.application.command; + +import lombok.Builder; + + +@Builder +public record CreateQaCommand( + Long QaId, + String QaQuestion, + String QaAnswer +) { +} diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/command/UpdateQaCommand.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/command/UpdateQaCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..16ed7a905ce5aa7ca6c0f167c3450b569874fd21 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/command/UpdateQaCommand.java @@ -0,0 +1,14 @@ +package com.example.qa.service.application.command; + +import lombok.Builder; + +@Builder +public record UpdateQaCommand( Long QaId, + String QaQuestion, + String QaIAnswer) { +} +/** + * @author qinshijie && liuxin + * @param UpdateQaCommand + * @return + */ \ No newline at end of file diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/config/MoonShotConfig.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/config/MoonShotConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..091db40502ae4ffd523397015b436ed1c267a2a3 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/config/MoonShotConfig.java @@ -0,0 +1,14 @@ +package com.example.qa.service.application.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "moonshot") +@Data +public class MoonShotConfig { + private String url; + private String key; + private String model; +} \ No newline at end of file diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/config/RabbitConfig.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/config/RabbitConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..5e627dcff1afb4ac8373b0875b67b5e707d9bf36 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/config/RabbitConfig.java @@ -0,0 +1,61 @@ +package com.example.qa.service.application.config; + +import com.example.qa.service.domain.Qa; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.DefaultJackson2JavaTypeMapper; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class RabbitConfig { + // 队列名称常量定义 + public static final String TEST_QUEUE = "test"; + public static final String TEST_QUEUE_2 = "test2"; + public static final String TEST_QUEUE_3 = "test3"; + + //让springboot启动时自动创建队列 + + @Bean + public Queue queue() { + return new Queue(TEST_QUEUE); + } + + @Bean + public Queue queue2() { + return new Queue(TEST_QUEUE_2); + } + + @Bean + public Queue queue3() { + return new Queue(TEST_QUEUE_3); + } + + @Bean + public MessageConverter messageConverter() { + Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); + // 配置类型映射器以支持 Qa 类的反序列化 + DefaultJackson2JavaTypeMapper typeMapper = new DefaultJackson2JavaTypeMapper(); + Map> idClassMapping = new HashMap<>(); + + idClassMapping.put("com.example.qa.service.domain.Qa", Qa.class); + typeMapper.setIdClassMapping(idClassMapping); + typeMapper.setTrustedPackages("com.example.qa.service.domain"); + converter.setJavaTypeMapper(typeMapper); + + return converter; + } + + @Bean + public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { + RabbitTemplate template = new RabbitTemplate(connectionFactory); + template.setMessageConverter(messageConverter()); + return template; + } +} \ No newline at end of file diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/AIChatUseCase.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/AIChatUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..98f8ac1764bf13467caae14f93962e975af4c0f6 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/AIChatUseCase.java @@ -0,0 +1,8 @@ +package com.example.qa.service.application.port.in; + +import com.example.qa.service.common.dto.ChatCompletionRequestDTO; +import com.example.qa.service.common.dto.ChatCompletionResponseDTO; + +public interface AIChatUseCase { + ChatCompletionResponseDTO chatWithAI(ChatCompletionRequestDTO request); +} \ No newline at end of file diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/CreateQaUseCase.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/CreateQaUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..4592adf26c37a606340cc0558529e6f33d59c85c --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/CreateQaUseCase.java @@ -0,0 +1,13 @@ +package com.example.qa.service.application.port.in; + +import com.example.qa.service.application.command.CreateQaCommand; +import com.example.qa.service.domain.Qa; + +public interface CreateQaUseCase { + Qa createQa(CreateQaCommand qaCommand); +} +/** + * @author XieQiWei + * @param CreateQaUseCase 创建问答用例 + * @return + */ \ No newline at end of file diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/DeleteQaUseCase.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/DeleteQaUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..c7b86b923b20e0b00020a5cb66291ac58be0eab5 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/DeleteQaUseCase.java @@ -0,0 +1,11 @@ +package com.example.qa.service.application.port.in; + +/** + * @author ChenYiZhao + * @param DeleteQaUseCase + * @return + */ + +public interface DeleteQaUseCase { + void deleteQa(Long id); +} diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/GetQaByIdUseCase.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/GetQaByIdUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..95cfdad2f3519e9cd4688aaf5364c0febcc29982 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/GetQaByIdUseCase.java @@ -0,0 +1,12 @@ +package com.example.qa.service.application.port.in; + +import com.example.qa.service.domain.Qa; + +public interface GetQaByIdUseCase { + Qa getQaById(Long id); +} +/** + * @auther zhangxuanwei + * @param GetQaByIdUseCase + * @return + * */ \ No newline at end of file diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/GetQaListUseCase.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/GetQaListUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..400a74a0c051a5f1be8b3fc5fe4494a540b7798a --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/GetQaListUseCase.java @@ -0,0 +1,16 @@ +package com.example.qa.service.application.port.in; + +import com.example.qa.service.domain.Qa; + +import java.util.List; + +public interface GetQaListUseCase { + List getQas(); +} +/** + * @author ZhangYuan + * @param GetQaListUseCase + * @return + * */ + + diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/UpdateQaUseCase.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/UpdateQaUseCase.java new file mode 100644 index 0000000000000000000000000000000000000000..345c5be7863edd3255d7955a7a3efbf5732fb758 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/port/in/UpdateQaUseCase.java @@ -0,0 +1,15 @@ +package com.example.qa.service.application.port.in; + +import com.example.qa.service.application.command.UpdateQaCommand; +import com.example.qa.service.domain.Qa; + +public interface UpdateQaUseCase { + Qa updateQa(UpdateQaCommand command); +} + + /** + * 更新信息 + * @author qinshijie && liuxin + * @param UpdateQaUseCase + * @return + */ \ No newline at end of file diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/AIChatService.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/AIChatService.java new file mode 100644 index 0000000000000000000000000000000000000000..01276a2df87116e7807e7dd507c54f1caaa47e9e --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/AIChatService.java @@ -0,0 +1,58 @@ +package com.example.qa.service.application.service; + +import com.example.qa.service.application.port.in.AIChatUseCase; +import com.example.qa.service.application.config.MoonShotConfig; +import com.example.qa.service.common.dto.ChatCompletionRequestDTO; +import com.example.qa.service.common.dto.ChatCompletionResponseDTO; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@Service +public class AIChatService implements AIChatUseCase { + + @Resource + private RestTemplate restTemplate; + + @Resource + private MoonShotConfig moonShotConfig; + + @Override + public ChatCompletionResponseDTO chatWithAI(ChatCompletionRequestDTO request) { + try { + // 设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(org.springframework.http.MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + moonShotConfig.getKey()); + + // 设置请求体 + request.setModel(moonShotConfig.getModel()); + HttpEntity requestEntity = new HttpEntity<>(request, headers); + + // 发送请求 + log.info("发送AI请求: {}", request); + ResponseEntity responseEntity = restTemplate.postForEntity( + moonShotConfig.getUrl(), + requestEntity, + ChatCompletionResponseDTO.class + ); + + if (responseEntity.getStatusCode().is2xxSuccessful()) { + ChatCompletionResponseDTO response = responseEntity.getBody(); + log.info("AI响应: {}", response); + return response; + } else { + log.error("AI请求失败,状态码: {}", responseEntity.getStatusCode()); + throw new RuntimeException("AI请求失败"); + } + } catch (Exception e) { + log.error("AI请求异常: {}", e.getMessage(), e); + throw new RuntimeException("AI请求异常", e); + } + } +} \ No newline at end of file diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/CreateQaService.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/CreateQaService.java new file mode 100644 index 0000000000000000000000000000000000000000..0fe1f6190fb5652b4b6493af3644cfd92c009d86 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/CreateQaService.java @@ -0,0 +1,40 @@ +/** + * QA创建服务 + * 实现了创建问答对象的业务逻辑 + * + * @author XieQiWei + */ +package com.example.qa.service.application.service; + +import com.example.qa.service.application.command.CreateQaCommand; +import com.example.qa.service.application.port.in.CreateQaUseCase; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.port.CreateQaPort; +import com.example.qa.service.domain.valueobject.QaAnswer; +import com.example.qa.service.domain.valueobject.QaQuestion; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class CreateQaService implements CreateQaUseCase { + @Resource + private CreateQaPort createQaPort; + + /** + * 创建新的问答对象 + * @param createQaCommand 创建问答命令对象,包含问题和答案内容 + * @return 创建成功的问答领域对象 + */ + @Override + public Qa createQa(CreateQaCommand createQaCommand) { + // 将命令对象转换为领域对象,使用QA相关字段 + Qa qa = new Qa( + new QaQuestion(createQaCommand.QaQuestion()), // 问题内容 + new QaAnswer(createQaCommand.QaAnswer()) // 答案内容 + ); + log.info("创建QA对象: {}", qa); + return createQaPort.createQa(qa); + } +} diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/DeleteQaService.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/DeleteQaService.java new file mode 100644 index 0000000000000000000000000000000000000000..2ea6628e2b48d74594f63dca4a4795b0e83e3ba3 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/DeleteQaService.java @@ -0,0 +1,27 @@ +/** + * QA删除服务 + * 实现了删除问答对象的业务逻辑 + * + * @author XieQiWei + */ +package com.example.qa.service.application.service; + +import com.example.qa.service.application.port.in.DeleteQaUseCase; +import com.example.qa.service.domain.port.DeleteQaPort; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +@Service +public class DeleteQaService implements DeleteQaUseCase { + @Resource + private DeleteQaPort deleteQaPort; + + /** + * 根据ID删除问答对象 + * @param id 问答ID + */ + @Override + public void deleteQa(Long id) { + deleteQaPort.deleteQa(id); + } +} diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/GetQaByIdService.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/GetQaByIdService.java new file mode 100644 index 0000000000000000000000000000000000000000..b5db3e8b9c7046d4cdd52b78ba0dd7d19fa3f69c --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/GetQaByIdService.java @@ -0,0 +1,35 @@ +/** + * QA详情查询服务 + * 实现了根据ID获取问答对象的业务逻辑 + * + * @author XieQiWei + */ +package com.example.qa.service.application.service; + +import com.example.qa.service.application.port.in.GetQaByIdUseCase; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.port.GetQaByIdPort; +import com.example.qa.service.domain.valueobject.QaId; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +@Service +public class GetQaByIdService implements GetQaByIdUseCase { + @Resource + private GetQaByIdPort getQaByIdPort; + + /** + * 根据ID获取问答对象详情 + * @param id 问答ID + * @return 问答领域对象 + */ + @Override + public Qa getQaById(Long id) { + return getQaByIdPort.getQaById(new QaId(id)); + } +} +/** + * @author ZhangXuanWei + * @param GetQaByIdService + * @return + * */ diff --git a/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/GetQaListService.java b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/GetQaListService.java new file mode 100644 index 0000000000000000000000000000000000000000..ac6e032a598ad0a10aee984466fba11712f1e22f --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/GetQaListService.java @@ -0,0 +1,32 @@ +/** + * QA列表查询服务 + * 实现了获取所有问答列表的业务逻辑 + * + * @author ZhengYuan + */ +package com.example.qa.service.application.service; + +import com.example.qa.service.application.port.in.GetQaListUseCase; +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.port.GetQaListPort; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class GetQaListService implements GetQaListUseCase { + + @Resource + GetQaListPort getQaListPort; + + /** + * 获取所有问答列表 + * @return 问答对象列表 + */ + @Override + public List getQas() { + List Qas = Qa.getQas(getQaListPort); + return Qas; + } +} \ No newline at end of file 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..bf88ee5c016df733e823623babff6f9d64107f86 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/com/example/qa/service/application/service/UpdateQaService.java @@ -0,0 +1,38 @@ +/** + * QA更新服务 + * 实现了更新问答对象的业务逻辑 + * + * @author XieQiWei + */ +package com.example.qa.service.application.service; + +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.QaId; +import com.example.qa.service.domain.valueobject.QaQuestion; +import com.example.qa.service.domain.valueobject.QaAnswer; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; + +@Service +public class UpdateQaService implements UpdateQaUseCase { + @Resource + private UpdateQaPort updateQaPort; + + /** + * 更新问答对象 + * @param command 更新问答命令对象,包含问答ID、新的问题和答案内容 + * @return 更新后的问答领域对象 + */ + @Override + public Qa updateQa(UpdateQaCommand command) { + Qa qa = new Qa( + new QaId(command.QaId()), // 保持ID用于定位更新对象 + new QaQuestion(command.QaQuestion()), // 替换用户姓名为问题内容 + new QaAnswer(command.QaIAnswer()) // 替换用户年龄、邮箱为答案内容 + ); + return updateQaPort.updateQa(qa); + } +} \ No newline at end of file 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..4fc7e0d3fa839da7129e86819ac2611a035a3f0d --- /dev/null +++ b/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/QaServiceBootstrapApplication.java @@ -0,0 +1,16 @@ +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") +@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/java/com/example/qa/service/bootstrap/config/RestConfig.java b/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/config/RestConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..d581b3bbc2576dc5f4a2404d6b78d88d5fec3a62 --- /dev/null +++ b/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/config/RestConfig.java @@ -0,0 +1,18 @@ +package com.example.qa.service.bootstrap.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; + +@Configuration +public class RestConfig { + + @Bean + public RestTemplate restTemplate() { + RestTemplate restTemplate = new RestTemplate(); + return restTemplate; + } +} \ No newline at end of file diff --git a/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/listener/Demo2Listener.java b/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/listener/Demo2Listener.java new file mode 100644 index 0000000000000000000000000000000000000000..695ce3479e384eed1f10a726ce2af4fd10e776d9 --- /dev/null +++ b/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/listener/Demo2Listener.java @@ -0,0 +1,18 @@ +package com.example.qa.service.bootstrap.listener; + +import com.example.qa.service.application.config.RabbitConfig; +import com.example.qa.service.domain.Qa; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class Demo2Listener { + + @RabbitListener(queues = RabbitConfig.TEST_QUEUE_2) + public void receiveQa(Qa qa) { + log.info("接收到test2队列的Qa对象: {}", qa); + // 这里可以添加Qa对象处理逻辑 + } +} \ No newline at end of file diff --git a/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/listener/Demo3Listener.java b/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/listener/Demo3Listener.java new file mode 100644 index 0000000000000000000000000000000000000000..4018546ac1259d8293cf1709a7343a326c528862 --- /dev/null +++ b/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/listener/Demo3Listener.java @@ -0,0 +1,33 @@ +package com.example.qa.service.bootstrap.listener; + +import com.example.qa.service.application.config.RabbitConfig; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +import java.util.Map; + +@Slf4j +@Component +public class Demo3Listener { + + private final ObjectMapper objectMapper; + + public Demo3Listener(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @RabbitListener(queues = RabbitConfig.TEST_QUEUE_3) + public void receiveJson(String jsonString) { + log.info("接收到test3队列的JSON字符串: {}", jsonString); + try { + // 将JSON字符串转换为Map对象,方便处理 + Map jsonMap = objectMapper.readValue(jsonString, Map.class); + log.info("解析后的JSON对象: {}", jsonMap); + // 这里可以添加JSON处理逻辑 + } catch (Exception e) { + log.error("JSON解析失败: {}", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/listener/DemoListener.java b/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/listener/DemoListener.java new file mode 100644 index 0000000000000000000000000000000000000000..a8835ec83469cf061ce4c1c7a5eac3cf013922a7 --- /dev/null +++ b/qa-service/qa-service-bootstrap/src/main/java/com/example/qa/service/bootstrap/listener/DemoListener.java @@ -0,0 +1,17 @@ +package com.example.qa.service.bootstrap.listener; + +import com.example.qa.service.application.config.RabbitConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class DemoListener { + + @RabbitListener(queues = RabbitConfig.TEST_QUEUE) + public void receiveMessage(String message) { + log.info("接收到test队列的消息: {}", message); + // 这里可以添加消息处理逻辑 + } +} \ No newline at end of file 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-common/src/main/java/com/example/qa/service/common/dto/ChatCompletionRequestDTO.java b/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/dto/ChatCompletionRequestDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..3911e07f7890813328089686e9534bab6f5f81c6 --- /dev/null +++ b/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/dto/ChatCompletionRequestDTO.java @@ -0,0 +1,12 @@ +package com.example.qa.service.common.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class ChatCompletionRequestDTO { + private String model; + private List messages; + private double temperature; +} \ No newline at end of file diff --git a/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/dto/ChatCompletionResponseDTO.java b/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/dto/ChatCompletionResponseDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..9966fc60bbbdbf59c42b3d73edc3deda3f6d845e --- /dev/null +++ b/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/dto/ChatCompletionResponseDTO.java @@ -0,0 +1,14 @@ +package com.example.qa.service.common.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class ChatCompletionResponseDTO { + private String id; + private String object; + private long created; + private String model; + private List choices; +} \ No newline at end of file diff --git a/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/dto/Choice.java b/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/dto/Choice.java new file mode 100644 index 0000000000000000000000000000000000000000..19b0a10dc7a13c9906934c0d14c966af9e93d3e6 --- /dev/null +++ b/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/dto/Choice.java @@ -0,0 +1,10 @@ +package com.example.qa.service.common.dto; + +import lombok.Data; + +@Data +public class Choice { + private int index; + private Message message; + private String finish_reason; +} \ No newline at end of file diff --git a/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/dto/Message.java b/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/dto/Message.java new file mode 100644 index 0000000000000000000000000000000000000000..14b567fa66f11b3c94457725a63d77a0e5f36f15 --- /dev/null +++ b/qa-service/qa-service-common/src/main/java/com/example/qa/service/common/dto/Message.java @@ -0,0 +1,9 @@ +package com.example.qa.service.common.dto; + +import lombok.Data; + +@Data +public class Message { + private String role; + private String content; +} \ No newline at end of file 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..a3caa71058a4e2733fd45a15f4ca800a79bd71d5 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/Qa.java @@ -0,0 +1,44 @@ +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.QaId; +import com.example.qa.service.domain.valueobject.QaQuestion; +import com.example.qa.service.domain.valueobject.QaAnswer; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; + +@Setter +@Getter +@ToString +public class Qa { + private QaId QaId; + private QaQuestion QaQuestion; + private QaAnswer QaIAnswer; + + public Qa() { + } + + public Qa(QaId QaId, QaQuestion QaQuestion, QaAnswer QaIAnswer) { + this.QaId = QaId; + this.QaQuestion = QaQuestion; + this.QaIAnswer = QaIAnswer; + } + + public Qa(QaQuestion question, QaAnswer answer) { + this.QaId = genId(); + this.QaQuestion = question; + this.QaIAnswer = answer; + } + + public static List getQas(GetQaListPort getQaListPort){ + return getQaListPort.getQas(); + } + + public QaId genId(){ + return new QaId(new IdWorker().nextId()); + } +} \ No newline at end of file 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..f03b131b8fbbfd81005a1111bf63b2c74dbe1b9f --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/CreateQaPort.java @@ -0,0 +1,12 @@ +package com.example.qa.service.domain.port; + +import com.example.qa.service.domain.Qa; + +public interface CreateQaPort { + Qa createQa(Qa qa); +} +/** + * @author XieQiWei + * @param CreateQaPort + * @return + * */ \ No newline at end of file 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..748baa9088183d87f1379351f99a8914d41cd37c --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/DeleteQaPort.java @@ -0,0 +1,11 @@ +package com.example.qa.service.domain.port; + +/** + * @author ChenYiZhao + * @param DeleteQaPort + * @return + */ + +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..662a93d79cbc5c7072e66a2e82282527e93dcb2e --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQaByIdPort.java @@ -0,0 +1,13 @@ +package com.example.qa.service.domain.port; + +import com.example.qa.service.domain.Qa; +import com.example.qa.service.domain.valueobject.QaId; + +public interface GetQaByIdPort { + Qa getQaById(QaId id); +} +/** + * @author zhangxuanwei + * @param GetqaByIdPort + * @return + */ \ No newline at end of file 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..84cb655c2deb115ccce3dc578073d5fa1941df8e --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/GetQaListPort.java @@ -0,0 +1,15 @@ +package com.example.qa.service.domain.port; + +import com.example.qa.service.domain.Qa; + +import java.util.List; + +public interface GetQaListPort { + List getQas(); +} + + /** + * @author ZhengYuan + * @param GetQaListPort + * @return + */ \ No newline at end of file diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/UpdateQaPort.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/UpdateQaPort.java new file mode 100644 index 0000000000000000000000000000000000000000..b4aabf440457c06b11f41acb5a9e8635b9b3c7bd --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/port/UpdateQaPort.java @@ -0,0 +1,13 @@ +package com.example.qa.service.domain.port; + +import com.example.qa.service.domain.Qa; + +public interface UpdateQaPort { + Qa updateQa(Qa qa); +} + /** + * 更新信息 + * @author qinshijie && liuxin + * @param UpdateQaPort + * @return + */ \ No newline at end of file diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QaAnswer.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QaAnswer.java new file mode 100644 index 0000000000000000000000000000000000000000..a6b4b86a09cd3550b2c8553a547da9030c8d58e0 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QaAnswer.java @@ -0,0 +1,7 @@ +package com.example.qa.service.domain.valueobject; + +public record QaAnswer(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..ea201ec9c04293b15114f7e82a6a58d6521f1274 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QaId.java @@ -0,0 +1,9 @@ +package com.example.qa.service.domain.valueobject; + +public record QaId(long id) { + + public long getValue(){ + return id; + } + +} diff --git a/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QaQuestion.java b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QaQuestion.java new file mode 100644 index 0000000000000000000000000000000000000000..fbae4ac346e8b2a4d5f72d785d84039a27a7315c --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/com/example/qa/service/domain/valueobject/QaQuestion.java @@ -0,0 +1,7 @@ +package com.example.qa.service.domain.valueobject; + +public record QaQuestion(String question) { + public String getValue() { + return question; + } +} 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..44b3cecc2e624d1c944f1c2b7932d0b40da3a344 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 @@ -7,18 +7,34 @@ import com.example.user.adapter.in.web.dto.UserResponseDTO; import com.example.user.service.application.command.CreateUserCommand; import com.example.user.service.application.command.UpdateUserCommand; import com.example.user.service.application.command.UserLoginCommand; +import com.example.user.service.application.dto.LoginResponse; +import com.example.user.service.application.dto.Result; +import com.example.user.service.application.dto.UserInfo; import com.example.user.service.application.port.in.*; +import com.example.user.service.application.service.TokenBlacklistService; +import com.example.user.service.application.util.JwtUtil; import com.example.user.service.domain.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import java.util.List; +/** + * @author ChenYiZhao + */ @Slf4j @RequestMapping("/users") @RestController @RequiredArgsConstructor +@Tag(name = "用户认证和管理", description = "用户登录、注册、信息管理等接口") public class UserController { private final GetUserListUseCase getUserListUseCase; @@ -27,83 +43,302 @@ public class UserController { private final UpdateUserUseCase updateUserUseCase; private final GetUserByIdUseCase getUserByIdUseCase; private final UserLoginUseCase userLoginUseCase; + private final JwtUtil jwtUtil; + private final TokenBlacklistService tokenBlacklistService; + /** + * 用户登录接口 + * + * 验证用户凭据并生成JWT令牌 + * + * @param userLoginRequestDTO 登录请求对象,包含用户名和密码 + * @return Result 登录结果响应,包含令牌和用户信息 + */ + @PostMapping("/login") + @Operation(summary = "用户登录", description = "用户名密码登录,返回JWT token") + public Result login(@Valid @RequestBody UserLoginRequestDTO userLoginRequestDTO) { + try { + log.info("用户登录请求: {}", userLoginRequestDTO.getName()); + + UserLoginCommand command = UserLoginCommand.builder() + .name(userLoginRequestDTO.getName()) + .password(userLoginRequestDTO.getPassword()) + .build(); + + String token = userLoginUseCase.login(command); + if (token == null || token.isEmpty()) { + return Result.error("登录失败:用户名或密码错误"); + } + + // 获取用户信息(假设userLoginUseCase有获取用户信息的方法或需要单独查询) + User user = getUserByUsername(userLoginRequestDTO.getName()); + if (user == null) { + return Result.error("登录失败:用户信息不存在"); + } + + UserInfo userInfo = UserInfo.builder() + .id(user.getId().id()) + .name(user.getName().username()) + .email(user.getEmail().email()) + .age(user.getAge().age()) + .build(); + + LoginResponse loginResponse = LoginResponse.builder() + .accessToken(token) + .tokenType("Bearer") + .userInfo(userInfo) + .expiresIn(System.currentTimeMillis() + 86400000L) // 24小时过期 + .build(); - @PostMapping("login") - public String login(@RequestBody UserLoginRequestDTO userLoginRequestDTO){ - log.info("UserLoginRequestDTO:{}",userLoginRequestDTO); - UserLoginCommand command=UserLoginCommand.builder() - .name(userLoginRequestDTO.getName()) - .password(userLoginRequestDTO.getPassword()) - .build(); - String token = userLoginUseCase.login(command); - return token; + log.info("用户登录成功: {}", user.getName().username()); + return Result.success("登录成功", loginResponse); + + } catch (Exception e) { + log.error("用户登录失败: {}", e.getMessage()); + return Result.error("登录失败: " + e.getMessage()); + } } + /** + * 用户注册接口 + * + * 创建新用户账户,包含输入验证和密码处理 + * + * @param createUserRequestDTO 注册请求对象 + * @return Result 注册结果响应 + */ + @PostMapping + @Operation(summary = "用户注册", description = "创建新用户账户") + public Result register(@Valid @RequestBody CreateUserRequestDTO createUserRequestDTO) { + try { + log.info("用户注册请求: username={}, email={}", + createUserRequestDTO.name(), createUserRequestDTO.email()); + + // 检查用户名是否已存在 + User existingUser = getUserByUsername(createUserRequestDTO.name()); + if (existingUser != null) { + log.warn("注册失败: 用户名已存在 - {}", createUserRequestDTO.name()); + return Result.error("用户名已存在,请选择其他用户名"); + } + + // 验证两次密码是否一致 + if (!createUserRequestDTO.password().equals(createUserRequestDTO.rePassword())) { + log.warn("注册失败: 两次密码不一致 - {}", createUserRequestDTO.name()); + return Result.error("两次输入的密码不一致"); + } + CreateUserCommand command = CreateUserCommand.builder() + .name(createUserRequestDTO.name()) + .age(createUserRequestDTO.age()) + .email(createUserRequestDTO.email()) + .build(); - @GetMapping("") - public List getUsers() { - log.info("getUsers"); - return getUserListUseCase.getUsers(); + User newUser = createUserUseCase.createUser(command); + + if (newUser != null) { + log.info("用户注册成功: username={}, userId={}", + createUserRequestDTO.name(), newUser.getId().id()); + return Result.success("注册成功,请使用用户名和密码登录"); + } else { + log.error("用户注册失败: 数据库操作失败 - {}", createUserRequestDTO.name()); + return Result.error("注册失败,请稍后重试"); + } + + } catch (Exception e) { + log.error("用户注册异常: username={}, error={}", + createUserRequestDTO.name(), e.getMessage(), e); + return Result.error("系统异常,请稍后重试"); + } } /** - * 创建新用户 - * 功能:接收用户注册信息,验证密码一致性,创建新用户账户 - * @author dongxuanfeng - * @param createUserRequestDTO - * @return User - 成功创建的新用户 - * @throws IllegalArgumentException 当密码与确认密码不匹配时抛出此异常 + * 获取当前登录用户信息 + * + * 基于JWT令牌获取当前认证用户的详细信息 + * + * @return Result 用户信息响应 */ - @PostMapping() - public User createUser(@RequestBody CreateUserRequestDTO createUserRequestDTO){ + @GetMapping("/info") + @Operation(summary = "获取当前用户信息", description = "获取当前登录用户的详细信息") + public Result getCurrentUserInfo() { + try { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + return Result.unauthorized("用户未登录"); + } + + String username = authentication.getName(); + User user = getUserByUsername(username); + + if (user == null) { + return Result.error("用户不存在"); + } + + UserInfo userInfo = UserInfo.builder() + .id(user.getId().id()) + .name(user.getName().username()) + .email(user.getEmail().email()) + .age(user.getAge().age()) + .build(); - if (!createUserRequestDTO.isPasswordValid()) { - throw new IllegalArgumentException("密码和确认密码不匹配"); + return Result.success(userInfo); + + } catch (Exception e) { + log.error("获取用户信息失败: {}", e.getMessage()); + return Result.error("获取用户信息失败: " + e.getMessage()); } - CreateUserCommand command=CreateUserCommand.builder() - .name(createUserRequestDTO.name()) - .age(createUserRequestDTO.age()) - .email(createUserRequestDTO.email()) - .password(createUserRequestDTO.password()) - .build(); - - return createUserUseCase.createUser(command); } + /** + * 用户登出接口 + * + * 将JWT令牌加入黑名单,使其失效 + * + * @param request HTTP请求对象 + * @return Result 登出结果响应 + */ + @PostMapping("/logout") + @Operation(summary = "用户登出", description = "用户登出,使当前JWT token失效") + public Result logout(HttpServletRequest request) { + try { + String token = getJwtFromRequest(request); + + if (token != null) { + tokenBlacklistService.addToBlacklist(token); + log.info("Token已加入黑名单: {}", token.substring(0, Math.min(token.length(), 20)) + "..."); + } + + SecurityContextHolder.clearContext(); + log.info("用户登出成功"); + return Result.success("登出成功,token已失效"); + } catch (Exception e) { + log.error("用户登出失败: {}", e.getMessage()); + return Result.error("登出失败: " + e.getMessage()); + } + } - @DeleteMapping("{id}") - public String deleteUser(@PathVariable("id") Long id){ - deleteUserUseCase.deleteUser(id); - return "success"; + /** + * 测试接口 + * + * 验证JWT认证是否正常工作 + * + * @return Result 测试结果响应 + */ + @GetMapping("/test") + @Operation(summary = "测试接口", description = "需要JWT认证的测试接口") + public Result test() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String username = authentication.getName(); + return Result.success("Hello, " + username + "! JWT认证成功!"); } + /** + * 获取用户列表 + * + * @return Result> 用户列表响应 + */ + @GetMapping + @Operation(summary = "获取用户列表", description = "查询所有用户信息") + public Result> getUsers() { + try { + log.info("获取用户列表"); + List users = getUserListUseCase.getUsers(); + return Result.success(users); + } catch (Exception e) { + log.error("获取用户列表失败: {}", e.getMessage()); + return Result.error("获取用户列表失败: " + e.getMessage()); + } + } - @PutMapping("") - public User updateUser(@RequestBody UpdateUserRequestDTO updateUserRequestDTO){ - UpdateUserCommand command=UpdateUserCommand.builder() - .id(updateUserRequestDTO.id()) - .name(updateUserRequestDTO.name()) - .age(updateUserRequestDTO.age()) - .email(updateUserRequestDTO.email()) - .build(); - User user = updateUserUseCase.updateUser(command); - return user; + /** + * 删除用户 + * + * @param id 用户ID + * @return Result 删除结果响应 + */ + @DeleteMapping("/{id}") + @Operation(summary = "删除用户", description = "根据ID删除用户") + public Result deleteUser(@PathVariable("id") Long id) { + try { + deleteUserUseCase.deleteUser(id); + log.info("删除用户成功: id={}", id); + return Result.success("删除成功"); + } catch (Exception e) { + log.error("删除用户失败: id={}, error={}", id, e.getMessage()); + return Result.error("删除用户失败: " + e.getMessage()); + } } + /** + * 更新用户信息 + * + * @param updateUserRequestDTO 更新用户请求对象 + * @return Result 更新后的用户信息 + */ + @PutMapping + @Operation(summary = "更新用户", description = "更新用户信息") + public Result updateUser(@Valid @RequestBody UpdateUserRequestDTO updateUserRequestDTO) { + try { + UpdateUserCommand command = UpdateUserCommand.builder() + .id(updateUserRequestDTO.id()) + .name(updateUserRequestDTO.name()) + .age(updateUserRequestDTO.age()) + .email(updateUserRequestDTO.email()) + .build(); + User user = updateUserUseCase.updateUser(command); + log.info("更新用户成功: id={}", updateUserRequestDTO.id()); + return Result.success(user); + } catch (Exception e) { + log.error("更新用户失败: id={}, error={}", updateUserRequestDTO.id(), e.getMessage()); + return Result.error("更新用户失败: " + e.getMessage()); + } + } + + /** + * 根据ID获取用户信息 + * + * @param id 用户ID + * @return Result 用户信息响应 + */ + @GetMapping("/{id}") + @Operation(summary = "获取用户详情", description = "根据ID查询用户详细信息") + public Result getUserById(@PathVariable("id") Long id) { + try { + User user = getUserByIdUseCase.getUserById(id); + UserResponseDTO userResponseDTO = new UserResponseDTO( + user.getId().id(), + user.getName().username(), + user.getAge().age(), + user.getEmail().email()); + return Result.success(userResponseDTO); + } catch (Exception e) { + log.error("获取用户详情失败: id={}, error={}", id, e.getMessage()); + return Result.error("获取用户详情失败: " + e.getMessage()); + } + } - @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; + /** + * 从请求中提取JWT令牌 + * + * @param request HTTP请求对象 + * @return JWT令牌字符串,如不存在则返回null + */ + private String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; } + /** + * 根据用户名获取用户信息 + * 注意:实际实现中需要根据项目情况从相应的UseCase或Service获取 + */ + private User getUserByUsername(String username) { + // 这里需要实现根据用户名查询用户的逻辑 + // 可能需要调用新的UseCase或现有服务 + return null; + } } 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..2e1d4621bc44bd95d7af230b856b85295002f73b 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,24 @@ package com.example.user.adapter.in.web.dto; +/** + * @author QingShiJie && liuxin && zhangxuanwei + * password、rePassword 不能为空 + * 两次输入必须相同 + */ + public record CreateUserRequestDTO( String name, Integer age, String email, String password, String rePassword) { - // TODO: 密码校验 - - /** - * 验证密码与确认密码是否一致 - * @author dongxuanfeng - * @return boolean -验证结果:true表示密码与重复密码一致,false表示两次密码不一致 - */ - public boolean isPasswordValid() { - return password != null && password.equals(rePassword); + // 密码一致性检查 + public CreateUserRequestDTO { + if (password == null || rePassword == null) { + throw new RuntimeException("密码不能为空"); + } + if (!password.equals(rePassword)) { + throw new RuntimeException("两次输入的密码不一致"); + } } - } diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/mapper/UserMapper.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/mapper/UserMapper.java index 92125d3cc1d3386d1f0ddd3c2fae1953dd77a5f4..3bcb413cd6d5b519745e792d7f72992702026fa4 100644 --- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/mapper/UserMapper.java +++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/mapper/UserMapper.java @@ -1,7 +1,23 @@ package com.example.user.adapter.out.persistence.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.user.adapter.out.persistence.entity.UserEntity; +import com.example.user.service.domain.User; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; -public interface UserMapper extends BaseMapper { +/** + * @author XieQiWei + * 添加根据用户名查询用户以及根据用户名更新密码两个方法 + */ +@Mapper +public interface UserMapper extends BaseMapper { + + @Select("SELECT * FROM user WHERE username = #{username}") + User selectByUsername(@Param("name") String username); + + + @Update("UPDATE user SET password = #{password} WHERE username = #{username}") + int updatePasswordByUsername(@Param("name") String username, @Param("password") String password); } 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..e47cc12522a57eabff38b82a33f1850a8ee372ac 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 @@ -8,6 +8,8 @@ public record CreateUserCommand( String name, Integer age, String email, - String password + String password, + + String rePassword ) { } diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/config/BasicSecurityConfig.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/config/BasicSecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..077920bf5b97a9272d80ae82e5f46d82359fa420 --- /dev/null +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/config/BasicSecurityConfig.java @@ -0,0 +1,174 @@ +package com.example.user.service.application.config; + +import com.example.user.service.application.filter.JwtAuthenticationFilter; +import com.example.user.service.application.service.CustomUserDetailsService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.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; + +/** + * @author XieQiWei + * Spring Security 配置类 + * 支持JWT认证和跨域配置 + */ +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) +@RequiredArgsConstructor +public class BasicSecurityConfig { + + /** + * JWT认证过滤器 + * 用于拦截请求并验证JWT Token的有效性 + */ + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + /** + * 自定义用户详情服务 + * 用于加载用户信息进行认证 + */ + private final CustomUserDetailsService userDetailsService; + + + + /** + * 认证管理器 + * 负责处理用户认证请求 + * + * @param config 认证配置对象,由Spring自动注入 + * @return AuthenticationManager 认证管理器实例 + * @throws Exception 如果配置过程中出现异常 + */ + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + /** + * CORS配置源 + * 配置跨域资源共享策略,允许前端应用访问后端API + * + * @return CorsConfigurationSource CORS配置源 + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + // 允许的源(Origin),这里设置为允许所有域名访问 + // 在生产环境中应该设置为具体的域名以提高安全性 + configuration.setAllowedOriginPatterns(List.of("*")); + + // 允许的HTTP方法,包括常用的GET、POST、PUT、DELETE等 + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); + + // 允许的请求头,这里设置为允许所有请求头 + configuration.setAllowedHeaders(List.of("*")); + + // 允许携带凭证(如Cookie),设置为true表示允许跨域请求携带身份信息 + configuration.setAllowCredentials(true); + + // 预检请求的缓存时间,单位秒,这里设置为1小时 + configuration.setMaxAge(3600L); + + // 暴露给客户端的响应头,允许客户端访问这些响应头 + configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type")); + + // 创建基于URL的CORS配置源 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + // 对所有路径应用相同的CORS配置 + source.registerCorsConfiguration("/**", configuration); + return source; + } + + /** + * 安全过滤器链配置 + * 定义HTTP请求的安全处理规则 + * + * @param http HttpSecurity对象,用于配置Web安全 + * @return SecurityFilterChain 安全过滤器链 + * @throws Exception 如果配置过程中出现异常 + */ + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // 禁用CSRF(跨站请求伪造)保护 + // 前后端分离项目通常禁用,因为使用Token认证而不是Session + .csrf(AbstractHttpConfigurer::disable) + + // 启用CORS(跨域资源共享)并使用上面定义的配置 + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + + // 配置会话管理为无状态 + // 因为使用JWT Token认证,不需要服务器保存会话状态 + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + + // 配置请求授权规则 + .authorizeHttpRequests(authz -> authz + // 公开接口,无需认证即可访问 + .requestMatchers("/api/auth/login", "/api/auth/register").permitAll() + .requestMatchers("/api/auth/debug/**").permitAll() + // Swagger和knife4j文档接口放行,方便查看API文档 + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll() + .requestMatchers("/doc.html", "/webjars/**", "/favicon.ico").permitAll() + .requestMatchers("/actuator/**").permitAll() + .requestMatchers("/error").permitAll() + // 其他所有请求都需要认证 + .anyRequest().authenticated() + ) + + // 禁用默认的登录页面 + // 因为使用自定义的登录接口,不需要Spring Security提供的默认登录页面 + .formLogin(AbstractHttpConfigurer::disable) + + // 禁用默认的登出页面 + // 因为使用自定义的登出接口,不需要Spring Security提供的默认登出功能 + .logout(AbstractHttpConfigurer::disable) + + // 禁用HTTP Basic认证 + // 因为使用JWT Token认证,不需要HTTP Basic认证方式 + .httpBasic(AbstractHttpConfigurer::disable) + + // 添加JWT认证过滤器 + // 在用户名密码认证过滤器之前添加JWT认证过滤器 + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + + // 配置用户详情服务 + // 用于加载用户信息进行认证 + .userDetailsService(userDetailsService) + + // 配置异常处理 + .exceptionHandling(exceptions -> exceptions + // 认证入口点,当未认证用户访问需要认证的资源时调用 + .authenticationEntryPoint((request, response, authException) -> { + response.setStatus(401); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"message\":\"未授权访问,请先登录\",\"data\":null}"); + }) + // 访问拒绝处理器,当已认证用户访问没有权限的资源时调用 + .accessDeniedHandler((request, response, accessDeniedException) -> { + response.setStatus(403); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":403,\"message\":\"访问被拒绝,权限不足\",\"data\":null}"); + }) + ); + + return http.build(); + } +} diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/config/PasswordConfig.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/config/PasswordConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..d89447f9c0f11ba416c2a8ce89290860e59fd94e --- /dev/null +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/config/PasswordConfig.java @@ -0,0 +1,19 @@ +package com.example.user.service.application.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * @author XieQiWei + * PasswordConfig密码编码器Bean + */ +@Configuration +public class PasswordConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/dto/LoginRequest.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/dto/LoginRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..efcfc2dcf18e7055335d6efc4a8a99da7f416853 --- /dev/null +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/dto/LoginRequest.java @@ -0,0 +1,39 @@ +package com.example.user.service.application.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author QinShiJie + */ + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LoginRequest { + + /** + * 用户名字段 + * + * @NotBlank 验证注解,确保用户名不为空且去除空格后不为空 + * @Size 验证注解,限制用户名长度在3-20个字符之间 + * message 属性定义了验证失败时的错误提示信息 + */ + @NotBlank(message = "用户名不能为空") + @Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间") + private String username; + + /** + * 密码字段 + * + * @NotBlank 验证注解,确保密码不为空且去除空格后不为空 + * @Size 验证注解,限制密码长度在6-20个字符之间 + * message 属性定义了验证失败时的错误提示信息 + */ + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间") + private String password; +} \ No newline at end of file diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/dto/LoginResponse.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/dto/LoginResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..16452418d6a7dad6d570b0b550b9b4c0cc9cb2b9 --- /dev/null +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/dto/LoginResponse.java @@ -0,0 +1,51 @@ +package com.example.user.service.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author QinShiJie + */ + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LoginResponse { + + /** + * JWT访问令牌 + * + * 这是用户登录成功后生成的JWT Token + * 客户端在后续请求中需要在请求头中携带这个Token进行身份认证 + * 格式通常是: Authorization: Bearer + */ + private String accessToken; + + /** + * 令牌类型 + * + * 默认值为"Bearer",表示这是一个Bearer Token + * Bearer Token是一种HTTP认证方案,用于OAuth 2.0等认证协议 + */ + private String tokenType = "Bearer"; + + /** + * 用户信息 + * + * 包含登录用户的基本信息,如用户名、邮箱、角色等 + * 这样客户端登录后可以直接获取用户信息,无需再次请求 + */ + private UserInfo userInfo; + + /** + * 令牌过期时间(毫秒时间戳) + * + * 表示这个Token将在什么时候过期 + * 客户端可以根据这个时间判断是否需要重新登录或刷新Token + * 这里设置为当前时间加上24小时(86400000毫秒) + */ + private Long expiresIn; +} \ No newline at end of file diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/dto/RegisterRequest.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/dto/RegisterRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..30f9ed4b68736c6e12096ce1fac197b55b268afc --- /dev/null +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/dto/RegisterRequest.java @@ -0,0 +1,69 @@ +package com.example.user.service.application.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * @author LiuXin + */ +@Data +@Schema(description = "用户注册请求") +public class RegisterRequest { + + /** + * 用户名字段 + * + * @NotBlank 验证注解,确保用户名不为空且去除空格后不为空 + * @Size 验证注解,限制用户名长度在3-20个字符之间 + * @Pattern 验证注解,使用正则表达式限制用户名只能包含字母、数字和下划线 + * regexp 属性定义了正则表达式规则:^[a-zA-Z0-9_]+$ + * ^ 表示字符串开始 + * [a-zA-Z0-9_] 表示字符可以是字母、数字或下划线 + * + 表示前面的字符可以出现一次或多次 + * $ 表示字符串结束 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @NotBlank(message = "用户名不能为空") + @Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间") + @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线") + @Schema(description = "用户名", example = "testuser") + private String username; + + /** + * 密码字段 + * + * @NotBlank 验证注解,确保密码不为空且去除空格后不为空 + * @Size 验证注解,限制密码长度在6-20个字符之间 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间") + @Schema(description = "密码", example = "123456") + private String password; + + /** + * 确认密码字段 + * + * @NotBlank 验证注解,确保确认密码不为空且去除空格后不为空 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @NotBlank(message = "确认密码不能为空") + @Schema(description = "确认密码", example = "123456") + private String confirmPassword; + + /** + * 邮箱字段 + * + * @Email 验证注解,确保邮箱格式正确 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @Email(message = "邮箱格式不正确") + @Schema(description = "邮箱", example = "test@example.com") + private String email; + + +} \ No newline at end of file diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/dto/Result.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/dto/Result.java new file mode 100644 index 0000000000000000000000000000000000000000..4e4a3f4535b814906e20101034874140f176f8bc --- /dev/null +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/dto/Result.java @@ -0,0 +1,138 @@ +package com.example.user.service.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author LiuXin + * 通用响应结果类 + */ +@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-application/src/main/java/com/example/user/service/application/dto/UserInfo.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/dto/UserInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..a8deb86bc01d5c6f9eafe0ef69abef09cc71a0d0 --- /dev/null +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/dto/UserInfo.java @@ -0,0 +1,24 @@ +package com.example.user.service.application.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author XieQiWei + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserInfo { + + private Long id; + + private String name; + + private String email; + + private int age; +} \ No newline at end of file diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/exception/GlobalExceptionHandler.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..b4a579d9450680f88e4375cb66d8f5864d2e2903 --- /dev/null +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/exception/GlobalExceptionHandler.java @@ -0,0 +1,210 @@ +package com.example.user.service.application.exception; + +import com.example.user.service.application.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; + +/** + * @author ChenYiZhao + * 全局异常处理器 + * 统一处理应用中的各种异常,避免异常直接暴露给用户 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理认证异常 + * @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()); + } + + /** + * 处理凭据错误异常 + * @param e 凭据错误异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(BadCredentialsException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public Result handleBadCredentialsException(BadCredentialsException e) { + log.error("凭据错误: {}", e.getMessage()); + return Result.unauthorized("用户名或密码错误"); + } + + /** + * 处理访问拒绝异常 + * @param e 访问拒绝异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleAccessDeniedException(AccessDeniedException e) { + log.error("访问拒绝: {}", e.getMessage()); + return Result.forbidden("访问被拒绝,权限不足"); + } + + /** + * 处理参数校验异常 + * @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); + } + + /** + * 处理参数绑定异常 + * @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); + } + + /** + * 处理约束违反异常 + * @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); + } + + /** + * 处理非法参数异常 + * @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()); + } + + /** + * 处理空指针异常 + * @param e 空指针异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(NullPointerException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleNullPointerException(NullPointerException e) { + log.error("空指针异常", e); + return Result.error("系统内部错误,请联系管理员"); + } + + /** + * 处理运行时异常 + * @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("系统内部错误,请联系管理员"); + } + + /** + * 自定义业务异常类 + */ + 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; + } + } + + /** + * 处理自定义业务异常 + */ + @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-application/src/main/java/com/example/user/service/application/filter/JwtAuthenticationFilter.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..98d659c586b545c45b15673616fc77abca0cedb0 --- /dev/null +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/filter/JwtAuthenticationFilter.java @@ -0,0 +1,178 @@ +package com.example.user.service.application.filter; + +import com.example.user.service.application.util.JwtUtil; +import com.example.user.service.application.service.TokenBlacklistService; +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; + + +/** + * @author zhangxuanwei + */ +@Slf4j +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private TokenBlacklistService tokenBlacklistService; + + @Autowired + private ApplicationContext applicationContext; + private UserDetailsService userDetailsService; + + private UserDetailsService getUserDetailsService() { + if (userDetailsService == null) { + // 通过Spring容器动态获取Bean,避免循环依赖 + userDetailsService = applicationContext.getBean(UserDetailsService.class); + } + return userDetailsService; + } + + @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); + } + + private String getJwtFromRequest(HttpServletRequest request) { + // 从请求头中获取Authorization字段的值 + // 例如:"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + String bearerToken = request.getHeader("Authorization"); + // 使用JwtUtil工具类提取实际的Token部分(去掉"Bearer "前缀) + return jwtUtil.extractTokenFromHeader(bearerToken); + } + + @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-application/src/main/java/com/example/user/service/application/service/CustomUserDetailsService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/CustomUserDetailsService.java new file mode 100644 index 0000000000000000000000000000000000000000..102b2ba2d28d9d525026aa3883e06f0dc6d95038 --- /dev/null +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/CustomUserDetailsService.java @@ -0,0 +1,114 @@ +package com.example.user.service.application.service; + +import com.example.user.adapter.out.persistence.mapper.UserMapper; +import com.example.user.service.domain.User; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +/** + * @author ZHENGYUAN + * 实现Spring Security的UserDetailsService接口 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + private final PasswordEncoder passwordEncoder; + + private final UserMapper userMapper; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 【步骤1:记录调试日志】 + // 使用debug级别记录方法调用,便于开发时追踪认证流程 + log.debug("Loading user by username: {}", username); + // 【步骤2:从数据库查询用户信息】 + // 调用MyBatis Mapper查询用户,这里会执行SQL: SELECT * FROM users WHERE username = ? + User user = userMapper.selectByUsername(username); + if (user == null) { + // 【安全策略】用户不存在时的处理 + // 记录警告日志,但不在异常信息中暴露过多细节,防止用户名枚举攻击 + log.warn("User not found: {}", username); + throw new UsernameNotFoundException("用户不存在: " + username); + } + + // 根据User类中的isSuper字段确定角色 + String role = user.getIsSuper() ? "ADMIN" : "USER"; + // 【步骤3:构建Spring Security的UserDetails对象】 + return org.springframework.security.core.userdetails.User.builder() + .username(user.getName().getValue()) // 使用UserName值对象的getValue方法 + .password(user.getPassword().getValue()) // 使用Password值对象的getValue方法 + .authorities(Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role))) + .accountExpired(false) + .accountLocked(false) + .credentialsExpired(false) + .disabled(false) // 根据User类,没有状态字段,默认不禁用 + .build(); + } + //根据用户名获取用户信息 + public User getUserByUsername(String username) { + return userMapper.selectByUsername(username); + } + //验证用户登录 + public User authenticate(String username, String password) { + User user = userMapper.selectByUsername(username); + if (user == null) { + log.warn("用户不存在: {}", username); + return null; + } + + log.info("password:{}", passwordEncoder.encode(password)); + + // 使用User类的validatePassword方法验证密码 + if (!user.validatePassword(password)) { + log.warn("用户登录失败: 密码错误 - {}", username); + return null; + } + + log.info("用户登录成功: {}", username); + return user; + } + //测试密码匹配 + public boolean testPasswordMatch(String rawPassword, String encodedPassword) { + return passwordEncoder.matches(rawPassword, encodedPassword); + } + //修正admin用户的密码哈希 + public boolean fixAdminPassword() { + try { + String correctPasswordHash = generatePasswordHash("password"); + int result = userMapper.updatePasswordByUsername("admin", correctPasswordHash); + userMapper.updatePasswordByUsername("user", correctPasswordHash); + return result > 0; + } catch (Exception e) { + log.error("修正密码哈希失败: {}", e.getMessage()); + return false; + } + } + //生成密码哈希 + public String generatePasswordHash(String password) { + return passwordEncoder.encode(password); + } + //保存用户信息 + public int saveUser(User user) { + try { + int result = userMapper.insert(user); + if (result > 0) { + log.info("用户保存成功: username={}, userId={}", user.getName().getValue(), user.getId().getValue()); + } else { + log.warn("用户保存失败: username={}", user.getName().getValue()); + } + return result; + } catch (Exception e) { + log.error("保存用户异常: username={}, error={}", user.getName().getValue(), e.getMessage(), e); + return 0; + } + } +} \ No newline at end of file diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/TokenBlacklistService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/TokenBlacklistService.java new file mode 100644 index 0000000000000000000000000000000000000000..557380cb2f315a31df78b076f37e0622c72923bf --- /dev/null +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/TokenBlacklistService.java @@ -0,0 +1,88 @@ +package com.example.user.service.application.service; + +import com.example.user.service.application.util.JwtUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author zhengyuan && XieQiWei + * Token黑名单服务类,用于管理已失效的JWT token + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TokenBlacklistService { + + private final JwtUtil jwtUtil; + + private final ConcurrentHashMap blacklistedTokens = new ConcurrentHashMap<>(); + 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))); + } + } + + public boolean isBlacklisted(String token) { + boolean isBlacklisted = blacklistedTokens.containsKey(token); + + if (isBlacklisted) { + log.debug("检测到黑名单令牌访问尝试,令牌前缀: {}...", + token.substring(0, Math.min(token.length(), 10))); + } + + return isBlacklisted; + } + + @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); + } + } + + public int getBlacklistSize() { + return blacklistedTokens.size(); + } + + public void clearBlacklist() { + int size = blacklistedTokens.size(); + blacklistedTokens.clear(); + log.warn("黑名单已被清空,共清理了 {} 个令牌", size); + } +} \ No newline at end of file diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java index b10d43e0179a76381f25cb3e62b9319077c76549..96bc03e821c61d6875e3197bfa1006cd31d1ee49 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 @@ -1,14 +1,19 @@ package com.example.user.service.application.service; +import com.example.user.service.application.util.JwtUtil; import com.example.user.service.application.command.UserLoginCommand; import com.example.user.service.application.port.in.UserLoginUseCase; -import com.example.user.service.common.JwtUtil; import com.example.user.service.domain.User; import com.example.user.service.domain.port.GetUserByNamePort; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +/** + * @author ChenYiZhao && XieQiWei && ZHENGYUAN + * 登录成功签发token + * @return token令牌 + */ @Slf4j @Service public class UserLoginService implements UserLoginUseCase { @@ -16,25 +21,27 @@ public class UserLoginService implements UserLoginUseCase { @Resource private GetUserByNamePort getUserByNamePort; + @Resource + private JwtUtil jwtUtil; // 注入JwtUtil + @Override public String login(UserLoginCommand userLoginCommand) { - //验证用户 + // 验证用户 User user = User.getUserByName(userLoginCommand.name(), getUserByNamePort); log.info("user:{}", user); - if(user==null){ + if (user == null) { throw new RuntimeException("用户不存在"); } - //验证密码 - if(!user.validatePassword(userLoginCommand.password())){ + // 验证密码 + + if (!user.validatePassword(userLoginCommand.password())) { throw new RuntimeException("密码错误"); } // 签发token - String token = JwtUtil.generateToken( - user.getId().id(), - user.getName().username(), - user.getIsSuper().value() + return jwtUtil.generateToken( + user.getName(), + user.getId(), + user.getIsSuper() ); - log.info("生成的JWT令牌: {}", token); - return token; } -} +} \ No newline at end of file diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/util/JwtUtil.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/util/JwtUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..ec0d0903f6616cc7896603dafd44f2039f9f85a6 --- /dev/null +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/util/JwtUtil.java @@ -0,0 +1,138 @@ +package com.example.user.service.application.util; + +import com.example.user.service.domain.valueobject.UserId; +import com.example.user.service.domain.valueobject.UserName; +import io.jsonwebtoken.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * @author XieQiWei && ChenYiZhao &&ZHENGYUAN + * 一个用来发token的工具类JwtUtil + * @return token + */ +@Component +public class JwtUtil { + + // 固定密钥为123456 + private static final String FIXED_SECRET = "123456"; + + @Value("${jwt.expiration:300000}") // 默认5分钟(300秒) + private long expiration; + + /** + * 生成签名密钥 + * 使用固定密钥"123456"并通过填充方式确保密钥长度足够 + */ + private SecretKey getSigningKey() { + // 将固定密钥转换为字节数组 + byte[] keyBytes = FIXED_SECRET.getBytes(StandardCharsets.UTF_8); + + // 创建一个32字节的数组(256位) + byte[] paddedKey = new byte[32]; + + // 将原始密钥复制到新数组,如果长度不够则填充零 + System.arraycopy(keyBytes, 0, paddedKey, 0, Math.min(keyBytes.length, paddedKey.length)); + + // 使用填充后的密钥创建SecretKey + return new SecretKeySpec(paddedKey, "HmacSHA256"); + } + + /** + * 生成JWT令牌 + */ + public String generateToken(UserName name, UserId id, Boolean isSuper) { + Map claims = new HashMap<>(); + claims.put("name", name.getValue()); // 使用getValue()获取实际值 + claims.put("id", id.getValue()); // 使用getValue()获取实际值 + claims.put("is_super", isSuper); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + /** + * 从token中获取用户名 + */ + public String getUsernameFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.get("name", String.class); + } + + /** + * 从token中获取用户ID + */ + public Long getUserIdFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.get("id", Long.class); + } + + /** + * 从token中获取用户权限 + */ + public Boolean getIsSuperFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.get("is_super", Boolean.class); + } + + /** + * 验证token是否有效 + */ + public boolean validateToken(String token, String username) { + try { + String tokenUsername = getUsernameFromToken(token); + return (username.equals(tokenUsername) && !isTokenExpired(token)); + } catch (Exception e) { + System.err.println("Token validation failed: " + e.getMessage()); + return false; + } + } + + /** + * 检查token是否过期 + */ + public boolean isTokenExpired(String token) { + Date expiration = getExpirationDateFromToken(token); + return expiration.before(new Date()); + } + + /** + * 从token中获取过期时间 + */ + public Date getExpirationDateFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.getExpiration(); + } + + /** + * 从token中解析Claims + */ + private Claims getClaimsFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + /** + * 从请求头中提取token + */ + public String extractTokenFromHeader(String authHeader) { + if (authHeader != null && authHeader.startsWith("Bearer ")) { + return authHeader.substring(7); + } + return null; + } +} \ No newline at end of file diff --git a/user-service/user-service-domain/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..8e08ce7099d6f6047c1eb62670b752d0a04ff89d 100644 --- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java @@ -21,17 +21,17 @@ public class User { private UserAge age; private Email email; private Password password; - private IsSuper isSuper; // 添加isSuper字段 + private Boolean isSuper; + public User() { } - public User(UserId id, UserName name, UserAge age, Email email, Password password,IsSuper isSuper) { + public User(UserId id, UserName name, UserAge age, Email email, Password password) { this.id = id; this.name = name; this.age = age; this.email = email; this.password = password; - this.isSuper = isSuper; } public User( UserName name, UserAge age, Email email, Password password) { @@ -40,7 +40,6 @@ public class User { this.age = age; this.email = email; this.password = password; - this.isSuper = new IsSuper(false); } public User(UserId userId, UserName userName, UserAge userAge, Email email) { @@ -48,7 +47,6 @@ public class User { this.name = name; this.age = age; this.email = email; - this.isSuper = new IsSuper(false); } diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Password.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Password.java index 57d9be7020b4d75f261b1aa30f2da10dc40b657d..9670a6f0d5006b8b767c84dfdc0cbf17d8695199 100644 --- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Password.java +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Password.java @@ -36,4 +36,13 @@ public record Password(String encryptedValue) { public static Password of(String encryptedValue) { return new Password(encryptedValue); } + + /** + * @author XieQiWei + * 添加一个方法得到密码的值 + * @return 密码的值 + */ + public String getValue() { + return encryptedValue; + } }