Browse Source

Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro

# Conflicts:
#	yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java
#	yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/product/ProductStatisticsJob.java
#	yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java
#	yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java
#	yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java
#	yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java
#	yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppService.java
#	yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java
#	yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java
#	yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
#	yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletTransactionServiceImpl.java
#	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java
#	yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java
YunaiV 9 tháng trước cách đây
mục cha
commit
e2197551ec
80 tập tin đã thay đổi với 1454 bổ sung641 xóa
  1. 1 1
      yudao-module-ai/pom.xml
  2. 1 1
      yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java
  3. 3 1
      yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java
  4. 1 1
      yudao-module-ai/yudao-module-ai-biz/pom.xml
  5. 8 4
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
  6. 14 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicPageReqVO.java
  7. 29 5
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java
  8. 30 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java
  9. 36 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapRespVO.java
  10. 8 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java
  11. 12 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java
  12. 9 3
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java
  13. 6 3
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
  14. 15 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java
  15. 42 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java
  16. 18 0
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java
  17. 25 2
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java
  18. 29 1
      yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml
  19. 41 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
  20. 2 1
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java
  21. 24 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java
  22. 4 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java
  23. 59 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java
  24. 456 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java
  25. BIN
      yudao-module-ai/yudao-spring-boot-starter-ai/src/main/resources/webapp/test/Fel.pdf
  26. 70 0
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java
  27. 1 2
      yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java
  28. 0 1
      yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java
  29. 0 1
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java
  30. 0 2
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/brand/ProductBrandDO.java
  31. 0 6
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/sku/ProductSkuDO.java
  32. 3 31
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java
  33. 0 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java
  34. 0 1
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/job/coupon/CouponExpireJob.java
  35. 0 18
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordService.java
  36. 0 56
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java
  37. 1 4
      yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/product/ProductStatisticsJob.java
  38. 0 1
      yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/trade/TradeStatisticsJob.java
  39. 3 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java
  40. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java
  41. 1 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java
  42. 0 4
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/package-info.java
  43. 0 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderConfig.java
  44. 10 4
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java
  45. 0 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java
  46. 3 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  47. 3 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java
  48. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceTest.java
  49. 6 6
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java
  50. 6 7
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java
  51. 7 7
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java
  52. 1 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
  53. 4 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java
  54. 4 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java
  55. 3 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageReqVO.java
  56. 7 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppRespVO.java
  57. 1 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java
  58. 10 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java
  59. 4 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java
  60. 5 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/app/PayAppMapper.java
  61. 8 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java
  62. 13 3
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppService.java
  63. 41 5
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java
  64. 4 4
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java
  65. 2 2
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java
  66. 11 10
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java
  67. 7 7
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java
  68. 7 7
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java
  69. 12 2
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletTransactionServiceImpl.java
  70. 6 3
      yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java
  71. 16 16
      yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java
  72. 1 0
      yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql
  73. 21 0
      yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java
  74. 0 3
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java
  75. 2 4
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java
  76. 88 216
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java
  77. 0 9
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java
  78. 61 22
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java
  79. 117 140
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java
  80. 9 2
      yudao-server/src/main/resources/application.yaml

+ 1 - 1
yudao-module-ai/pom.xml

@@ -18,7 +18,7 @@
 
     <name>${project.artifactId}</name>
     <description>
-        ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维图等功能。
+        ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维图等功能。
         目前已接入各种模型,不限于:
           国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
           国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno

+ 1 - 1
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java

@@ -22,7 +22,7 @@ public enum AiChatRoleEnum implements IntArrayValuable {
             除此之外不需要除了正文内容外的其他回复,如标题、开头、任何解释性语句或道歉。
             """),
 
-    AI_MIND_MAP_ROLE(2, "图助手", """
+    AI_MIND_MAP_ROLE(2, "图助手", """
              你是一位非常优秀的思维导图助手,你会把用户的所有提问都总结成思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子:
              # Geek-AI 助手
              ## 完整的开源系统

+ 3 - 1
yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java

@@ -45,9 +45,11 @@ public interface ErrorCodeConstants {
     // ========== API 音乐 1-040-006-000 ==========
     ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!");
 
-
     // ========== API 写作 1-022-007-000 ==========
     ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_022_007_000, "作文不存在!");
     ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "写作生成异常!");
 
+    // ========== API 思维导图 1-040-008-000 ==========
+    ErrorCode MIND_MAP_NOT_EXISTS = new ErrorCode(1_040_008_000, "思维导图不存在!");
+
 }

+ 1 - 1
yudao-module-ai/yudao-module-ai-biz/pom.xml

@@ -12,7 +12,7 @@
 
     <name>${project.artifactId}</name>
     <description>
-        ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维图等功能。
+        ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维图等功能。
         目前已接入各种模型,不限于:
         国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek
         国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno

+ 8 - 4
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java

@@ -5,10 +5,7 @@ import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageRespVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
@@ -45,6 +42,13 @@ public class AiImageController {
         return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
     }
 
+    @GetMapping("/public-page")
+    @Operation(summary = "获取公开的绘图分页")
+    public CommonResult<PageResult<AiImageRespVO>> getImagePagePublic(AiImagePublicPageReqVO pageReqVO) {
+        PageResult<AiImageDO> pageResult = imageService.getImagePagePublic(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
+    }
+
     @GetMapping("/get-my")
     @Operation(summary = "获取【我的】绘图记录")
     @Parameter(name = "id", required = true, description = "绘画编号", example = "1024")

+ 14 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePublicPageReqVO.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.ai.controller.admin.image.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "管理后台 - AI 绘画公开的分页 Request VO")
+@Data
+public class AiImagePublicPageReqVO extends PageParam {
+
+    @Schema(description = "提示词")
+    private String prompt;
+
+}

+ 29 - 5
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java

@@ -1,20 +1,25 @@
 package cn.iocoder.yudao.module.ai.controller.admin.mindmap;
 
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapRespVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
 import cn.iocoder.yudao.module.ai.service.mindmap.AiMindMapService;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.annotation.Resource;
 import jakarta.annotation.security.PermitAll;
 import jakarta.validation.Valid;
 import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
 import reactor.core.publisher.Flux;
 
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
 
 @Tag(name = "管理后台 - AI 思维导图")
@@ -26,10 +31,29 @@ public class AiMindMapController {
     private AiMindMapService mindMapService;
 
     @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
-    @Operation(summary = "图生成(流式)", description = "流式返回,响应较快")
+    @Operation(summary = "图生成(流式)", description = "流式返回,响应较快")
     @PermitAll  // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题
     public Flux<CommonResult<String>> generateMindMap(@RequestBody @Valid AiMindMapGenerateReqVO generateReqVO) {
         return mindMapService.generateMindMap(generateReqVO, getLoginUserId());
     }
 
+    // ================ 导图管理 ================
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除思维导图")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('ai:mind-map:delete')")
+    public CommonResult<Boolean> deleteMindMap(@RequestParam("id") Long id) {
+        mindMapService.deleteMindMap(id);
+        return success(true);
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得思维导图分页")
+    @PreAuthorize("@ss.hasPermission('ai:mind-map:query')")
+    public CommonResult<PageResult<AiMindMapRespVO>> getMindMapPage(@Valid AiMindMapPageReqVO pageReqVO) {
+        PageResult<AiMindMapDO> pageResult = mindMapService.getMindMapPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, AiMindMapRespVO.class));
+    }
+
 }

+ 30 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapPageReqVO.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - AI 思维导图分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class AiMindMapPageReqVO extends PageParam {
+
+    @Schema(description = "用户编号", example = "4325")
+    private Long userId;
+
+    @Schema(description = "生成内容提示", example = "Java 学习路线")
+    private String prompt;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 36 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapRespVO.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - AI 思维导图 Response VO")
+@Data
+public class AiMindMapRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3373")
+    private Long id;
+
+    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4325")
+    private Long userId;
+
+    @Schema(description = "生成内容提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 学习路线")
+    private String prompt;
+
+    @Schema(description = "生成的思维导图内容")
+    private String generatedContent;
+
+    @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI")
+    private String platform;
+
+    @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-3.5-turbo-0125")
+    private String model;
+
+    @Schema(description = "错误信息")
+    private String errorMessage;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    private LocalDateTime createTime;
+
+}

+ 8 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java

@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePublicPageReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
 import org.apache.ibatis.annotations.Mapper;
 
@@ -41,6 +42,13 @@ public interface AiImageMapper extends BaseMapperX<AiImageDO> {
                 .orderByDesc(AiImageDO::getId));
     }
 
+    default PageResult<AiImageDO> selectPage(AiImagePublicPageReqVO pageReqVO) {
+        return selectPage(pageReqVO, new LambdaQueryWrapperX<AiImageDO>()
+                .eqIfPresent(AiImageDO::getPublicStatus, Boolean.TRUE)
+                .likeIfPresent(AiImageDO::getPrompt, pageReqVO.getPrompt())
+                .orderByDesc(AiImageDO::getId));
+    }
+
     default List<AiImageDO> selectListByStatusAndPlatform(Integer status, String platform) {
         return selectList(AiImageDO::getStatus, status,
                 AiImageDO::getPlatform, platform);

+ 12 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java

@@ -1,6 +1,9 @@
 package cn.iocoder.yudao.module.ai.dal.mysql.mindmap;
 
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
 import org.apache.ibatis.annotations.Mapper;
 
@@ -11,4 +14,13 @@ import org.apache.ibatis.annotations.Mapper;
  */
 @Mapper
 public interface AiMindMapMapper extends BaseMapperX<AiMindMapDO> {
+
+    default PageResult<AiMindMapDO> selectPage(AiMindMapPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<AiMindMapDO>()
+                .eqIfPresent(AiMindMapDO::getUserId, reqVO.getUserId())
+                .eqIfPresent(AiMindMapDO::getPrompt, reqVO.getPrompt())
+                .betweenIfPresent(AiMindMapDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(AiMindMapDO::getId));
+    }
+
 }

+ 9 - 3
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java

@@ -2,9 +2,7 @@ package cn.iocoder.yudao.module.ai.service.image;
 
 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
@@ -28,6 +26,14 @@ public interface AiImageService {
      */
     PageResult<AiImageDO> getImagePageMy(Long userId, AiImagePageReqVO pageReqVO);
 
+    /**
+     * 获取公开的绘图分页
+     *
+     * @param pageReqVO 分页条件
+     * @return 绘图分页
+     */
+    PageResult<AiImageDO> getImagePagePublic(AiImagePublicPageReqVO pageReqVO);
+
     /**
      * 获得绘图记录
      *

+ 6 - 3
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java

@@ -12,9 +12,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
 import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO;
-import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.image.vo.*;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO;
 import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
@@ -70,6 +68,11 @@ public class AiImageServiceImpl implements AiImageService {
         return imageMapper.selectPageMy(userId, pageReqVO);
     }
 
+    @Override
+    public PageResult<AiImageDO> getImagePagePublic(AiImagePublicPageReqVO pageReqVO) {
+        return imageMapper.selectPage(pageReqVO);
+    }
+
     @Override
     public AiImageDO getImage(Long id) {
         return imageMapper.selectById(id);

+ 15 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocService.java

@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.ai.service.knowledge;
+
+/**
+ * AI 知识库 Service 接口
+ *
+ * @author xiaoxin
+ */
+public interface DocService {
+
+    /**
+     * 向量化文档
+     */
+    void embeddingDoc();
+
+}

+ 42 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/DocServiceImpl.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.ai.service.knowledge;
+
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.reader.tika.TikaDocumentReader;
+import org.springframework.ai.transformer.splitter.TokenTextSplitter;
+import org.springframework.ai.vectorstore.RedisVectorStore;
+import org.springframework.beans.factory.annotation.Value;
+
+import java.util.List;
+
+/**
+ * AI 知识库 Service 实现类
+ *
+ * @author xiaoxin
+ */
+//@Service  // TODO 芋艿:临时注释,避免无法启动
+@Slf4j
+public class DocServiceImpl implements DocService {
+
+    @Resource
+    private RedisVectorStore vectorStore;
+    @Resource
+    private TokenTextSplitter tokenTextSplitter;
+
+    // TODO @xin 临时测试用,后续删
+    @Value("classpath:/webapp/test/Fel.pdf")
+    private org.springframework.core.io.Resource data;
+
+    @Override
+    public void embeddingDoc() {
+        // 读取文件
+        TikaDocumentReader loader = new TikaDocumentReader(data);
+        List<Document> documents = loader.get();
+        // 文档分段
+        List<Document> segments = tokenTextSplitter.apply(documents);
+        // 向量化并存储
+        vectorStore.add(segments);
+    }
+
+}

+ 18 - 0
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java

@@ -1,7 +1,10 @@
 package cn.iocoder.yudao.module.ai.service.mindmap;
 
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
+import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
 import reactor.core.publisher.Flux;
 
 /**
@@ -20,4 +23,19 @@ public interface AiMindMapService {
      */
     Flux<CommonResult<String>> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId);
 
+    /**
+     * 删除思维导图
+     *
+     * @param id 编号
+     */
+    void deleteMindMap(Long id);
+
+    /**
+     * 获得思维导图分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 思维导图分页
+     */
+    PageResult<AiMindMapDO> getMindMapPage(AiMindMapPageReqVO pageReqVO);
+
 }

+ 25 - 2
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java

@@ -6,9 +6,11 @@ import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
 import cn.iocoder.yudao.framework.ai.core.util.AiUtils;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO;
+import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapPageReqVO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
 import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
@@ -33,8 +35,10 @@ import reactor.core.publisher.Flux;
 import java.util.ArrayList;
 import java.util.List;
 
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MIND_MAP_NOT_EXISTS;
 
 /**
  * AI 思维导图 Service 实现类
@@ -57,10 +61,10 @@ public class AiMindMapServiceImpl implements AiMindMapService {
 
     @Override
     public Flux<CommonResult<String>> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId) {
-        // 1. 获取图模型。尝试获取思维导图助手角色,如果没有则使用默认模型
+        // 1. 获取图模型。尝试获取思维导图助手角色,如果没有则使用默认模型
         AiChatRoleDO role = CollUtil.getFirst(
                 chatRoleService.getChatRoleListByName(AiChatRoleEnum.AI_MIND_MAP_ROLE.getName()));
-        // 1.1 获取图执行模型
+        // 1.1 获取图执行模型
         AiChatModelDO model = getModel(role);
         // 1.2 获取角色设定消息
         String systemMessage = role != null && StrUtil.isNotBlank(role.getSystemMessage())
@@ -131,4 +135,23 @@ public class AiMindMapServiceImpl implements AiMindMapService {
         return model;
     }
 
+    @Override
+    public void deleteMindMap(Long id) {
+        // 校验存在
+        validateMindMapExists(id);
+        // 删除
+        mindMapMapper.deleteById(id);
+    }
+
+    private void validateMindMapExists(Long id) {
+        if (mindMapMapper.selectById(id) == null) {
+            throw exception(MIND_MAP_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public PageResult<AiMindMapDO> getMindMapPage(AiMindMapPageReqVO pageReqVO) {
+        return mindMapMapper.selectPage(pageReqVO);
+    }
+
 }

+ 29 - 1
yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml

@@ -23,12 +23,16 @@
             <artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
             <version>${spring-ai.version}</version>
         </dependency>
-
         <dependency>
             <groupId>org.springframework.ai</groupId>
             <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
             <version>${spring-ai.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.springframework.ai</groupId>
             <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
@@ -40,6 +44,30 @@
             <version>${spring-ai.version}</version>
         </dependency>
 
+        <!-- 向量化,基于 Redis 存储,Tika 解析内容 -->
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-transformers-spring-boot-starter</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-tika-document-reader</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.ai</groupId>
+            <artifactId>spring-ai-redis-store</artifactId>
+            <version>${spring-ai.version}</version>
+        </dependency>
+
+        <!-- TODO @xin:引入我们项目的 starter -->
+        <dependency>
+            <groupId>org.springframework.data</groupId>
+            <artifactId>spring-data-redis</artifactId>
+            <optional>true</optional>
+        </dependency>
+
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-common</artifactId>

+ 41 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java

@@ -10,11 +10,20 @@ import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions;
 import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties;
+import org.springframework.ai.document.MetadataMode;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.transformer.splitter.TokenTextSplitter;
+import org.springframework.ai.transformers.TransformersEmbeddingModel;
+import org.springframework.ai.vectorstore.RedisVectorStore;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Lazy;
+import redis.clients.jedis.JedisPooled;
 
 /**
  * 芋道 AI 自动配置
@@ -73,4 +82,36 @@ public class YudaoAiAutoConfiguration {
         return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl());
     }
 
+    // ========== rag 相关 ==========
+    @Bean
+    @Lazy // TODO 芋艿:临时注释,避免无法启动
+    public EmbeddingModel transformersEmbeddingClient() {
+        return new TransformersEmbeddingModel(MetadataMode.EMBED);
+    }
+
+    /**
+     * 我们启动有加载很多 Embedding 模型,不晓得取哪个好,先 new 个 TransformersEmbeddingModel 跑
+     */
+    @Bean
+    @Lazy // TODO 芋艿:临时注释,避免无法启动
+    public RedisVectorStore vectorStore(TransformersEmbeddingModel transformersEmbeddingModel, RedisVectorStoreProperties properties,
+                                        RedisProperties redisProperties) {
+        var config = RedisVectorStore.RedisVectorStoreConfig.builder()
+                .withIndexName(properties.getIndex())
+                .withPrefix(properties.getPrefix())
+                .build();
+
+        RedisVectorStore redisVectorStore = new RedisVectorStore(config, transformersEmbeddingModel,
+                new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
+                properties.isInitializeSchema());
+        redisVectorStore.afterPropertiesSet();
+        return redisVectorStore;
+    }
+
+    @Bean
+    @Lazy // TODO 芋艿:临时注释,避免无法启动
+    public TokenTextSplitter tokenTextSplitter() {
+        return new TokenTextSplitter(500, 100, 5, 10000, true);
+    }
+
 }

+ 2 - 1
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java

@@ -22,7 +22,8 @@ public enum AiPlatformEnum {
 
     // ========== 国外平台 ==========
 
-    OPENAI("OpenAI", "OpenAI"),
+    OPENAI("OpenAI", "OpenAI"), // OpenAI 官方
+    AZURE_OPENAI("AzureOpenAI", "AzureOpenAI"), // OpenAI 微软
     OLLAMA("Ollama", "Ollama"),
 
     STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI

+ 24 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java

@@ -21,6 +21,10 @@ import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel;
 import com.alibaba.cloud.ai.tongyi.image.TongYiImagesProperties;
 import com.alibaba.dashscope.aigc.generation.Generation;
 import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
+import com.azure.ai.openai.OpenAIClient;
+import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration;
+import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties;
+import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties;
 import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
 import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
 import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration;
@@ -31,6 +35,7 @@ import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration;
 import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiChatProperties;
 import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiConnectionProperties;
 import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiImageProperties;
+import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
 import org.springframework.ai.chat.model.ChatModel;
 import org.springframework.ai.image.ImageModel;
 import org.springframework.ai.model.function.FunctionCallbackContext;
@@ -82,6 +87,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
                     return buildXingHuoChatModel(apiKey);
                 case OPENAI:
                     return buildOpenAiChatModel(apiKey, url);
+                case AZURE_OPENAI:
+                    return buildAzureOpenAiChatModel(apiKey, url);
                 case OLLAMA:
                     return buildOllamaChatModel(url);
                 default:
@@ -106,6 +113,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
                 return SpringUtil.getBean(XingHuoChatModel.class);
             case OPENAI:
                 return SpringUtil.getBean(OpenAiChatModel.class);
+            case AZURE_OPENAI:
+                return SpringUtil.getBean(AzureOpenAiChatModel.class);
             case OLLAMA:
                 return SpringUtil.getBean(OllamaChatModel.class);
             default:
@@ -268,6 +277,21 @@ public class AiModelFactoryImpl implements AiModelFactory {
         return new OpenAiChatModel(openAiApi);
     }
 
+    /**
+     * 可参考 {@link AzureOpenAiAutoConfiguration}
+     */
+    private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) {
+        AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration();
+        // 创建 OpenAIClient 对象
+        AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties();
+        connectionProperties.setApiKey(apiKey);
+        connectionProperties.setEndpoint(url);
+        OpenAIClient openAIClient = azureOpenAiAutoConfiguration.openAIClient(connectionProperties);
+        // 获取 AzureOpenAiChatProperties 对象
+        AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class);
+        return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, null, null);
+    }
+
     /**
      * 可参考 {@link OpenAiAutoConfiguration}
      */

+ 4 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java

@@ -5,6 +5,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
 import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions;
 import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions;
 import com.alibaba.cloud.ai.tongyi.chat.TongYiChatOptions;
+import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
 import org.springframework.ai.chat.messages.*;
 import org.springframework.ai.chat.prompt.ChatOptions;
 import org.springframework.ai.ollama.api.OllamaOptions;
@@ -35,6 +36,9 @@ public class AiUtils {
                 return XingHuoChatOptions.builder().model(model).temperature(temperatureF).maxTokens(maxTokens).build();
             case OPENAI:
                 return OpenAiChatOptions.builder().withModel(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
+            case AZURE_OPENAI:
+                // TODO 芋艿:貌似没 model 字段???!
+                return AzureOpenAiChatOptions.builder().withDeploymentName(model).withTemperature(temperatureF).withMaxTokens(maxTokens).build();
             case OLLAMA:
                 return OllamaOptions.create().withModel(model).withTemperature(temperatureF).withNumPredict(maxTokens);
             default:

+ 59 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/autoconfigure/vectorstore/redis/RedisVectorStoreAutoConfiguration.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.autoconfigure.vectorstore.redis;
+
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.vectorstore.RedisVectorStore;
+import org.springframework.ai.vectorstore.RedisVectorStore.RedisVectorStoreConfig;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import redis.clients.jedis.JedisPooled;
+
+/**
+ * TODO @xin 先拿 spring-ai 最新代码覆盖,1.0.0-M1 跟 redis 自动配置会冲突
+ *
+ * TODO 这个官方,有说啥时候 fix 哇?
+ *
+ * @author Christian Tzolov
+ * @author Eddú Meléndez
+ */
+@AutoConfiguration(after = RedisAutoConfiguration.class)
+@ConditionalOnClass({JedisPooled.class, JedisConnectionFactory.class, RedisVectorStore.class, EmbeddingModel.class})
+//@ConditionalOnBean(JedisConnectionFactory.class)
+@EnableConfigurationProperties(RedisVectorStoreProperties.class)
+public class RedisVectorStoreAutoConfiguration {
+
+    @Bean
+    @ConditionalOnMissingBean
+    public RedisVectorStore vectorStore(EmbeddingModel embeddingModel, RedisVectorStoreProperties properties,
+                                        JedisConnectionFactory jedisConnectionFactory) {
+
+        var config = RedisVectorStoreConfig.builder()
+                .withIndexName(properties.getIndex())
+                .withPrefix(properties.getPrefix())
+                .build();
+
+        return new RedisVectorStore(config, embeddingModel,
+                new JedisPooled(jedisConnectionFactory.getHostName(), jedisConnectionFactory.getPort()),
+                properties.isInitializeSchema());
+    }
+
+}

+ 456 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/org/springframework/ai/vectorstore/RedisVectorStore.java

@@ -0,0 +1,456 @@
+/*
+ * Copyright 2023 - 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.ai.vectorstore;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.vectorstore.filter.FilterExpressionConverter;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import redis.clients.jedis.JedisPooled;
+import redis.clients.jedis.Pipeline;
+import redis.clients.jedis.json.Path2;
+import redis.clients.jedis.search.*;
+import redis.clients.jedis.search.Schema.FieldType;
+import redis.clients.jedis.search.schemafields.*;
+import redis.clients.jedis.search.schemafields.VectorField.VectorAlgorithm;
+
+import java.text.MessageFormat;
+import java.util.*;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * The RedisVectorStore is for managing and querying vector data in a Redis database. It
+ * offers functionalities like adding, deleting, and performing similarity searches on
+ * documents.
+ *
+ * The store utilizes RedisJSON and RedisSearch to handle JSON documents and to index and
+ * search vector data. It supports various vector algorithms (e.g., FLAT, HSNW) for
+ * efficient similarity searches. Additionally, it allows for custom metadata fields in
+ * the documents to be stored alongside the vector and content data.
+ *
+ * This class requires a RedisVectorStoreConfig configuration object for initialization,
+ * which includes settings like Redis URI, index name, field names, and vector algorithms.
+ * It also requires an EmbeddingModel to convert documents into embeddings before storing
+ * them.
+ *
+ * @author Julien Ruaux
+ * @author Christian Tzolov
+ * @author Eddú Meléndez
+ * @see VectorStore
+ * @see RedisVectorStoreConfig
+ * @see EmbeddingModel
+ */
+public class RedisVectorStore implements VectorStore, InitializingBean {
+
+    public enum Algorithm {
+
+        FLAT, HSNW
+
+    }
+
+    public record MetadataField(String name, FieldType fieldType) {
+
+        public static MetadataField text(String name) {
+            return new MetadataField(name, FieldType.TEXT);
+        }
+
+        public static MetadataField numeric(String name) {
+            return new MetadataField(name, FieldType.NUMERIC);
+        }
+
+        public static MetadataField tag(String name) {
+            return new MetadataField(name, FieldType.TAG);
+        }
+
+    }
+
+    /**
+     * Configuration for the Redis vector store.
+     */
+    public static final class RedisVectorStoreConfig {
+
+        private final String indexName;
+
+        private final String prefix;
+
+        private final String contentFieldName;
+
+        private final String embeddingFieldName;
+
+        private final Algorithm vectorAlgorithm;
+
+        private final List<MetadataField> metadataFields;
+
+        private RedisVectorStoreConfig() {
+            this(builder());
+        }
+
+        private RedisVectorStoreConfig(Builder builder) {
+            this.indexName = builder.indexName;
+            this.prefix = builder.prefix;
+            this.contentFieldName = builder.contentFieldName;
+            this.embeddingFieldName = builder.embeddingFieldName;
+            this.vectorAlgorithm = builder.vectorAlgorithm;
+            this.metadataFields = builder.metadataFields;
+        }
+
+        /**
+         * Start building a new configuration.
+         * @return The entry point for creating a new configuration.
+         */
+        public static Builder builder() {
+
+            return new Builder();
+        }
+
+        /**
+         * {@return the default config}
+         */
+        public static RedisVectorStoreConfig defaultConfig() {
+
+            return builder().build();
+        }
+
+        public static class Builder {
+
+            private String indexName = DEFAULT_INDEX_NAME;
+
+            private String prefix = DEFAULT_PREFIX;
+
+            private String contentFieldName = DEFAULT_CONTENT_FIELD_NAME;
+
+            private String embeddingFieldName = DEFAULT_EMBEDDING_FIELD_NAME;
+
+            private Algorithm vectorAlgorithm = DEFAULT_VECTOR_ALGORITHM;
+
+            private List<MetadataField> metadataFields = new ArrayList<>();
+
+            private Builder() {
+            }
+
+            /**
+             * Configures the Redis index name to use.
+             * @param name the index name to use
+             * @return this builder
+             */
+            public Builder withIndexName(String name) {
+                this.indexName = name;
+                return this;
+            }
+
+            /**
+             * Configures the Redis key prefix to use (default: "embedding:").
+             * @param prefix the prefix to use
+             * @return this builder
+             */
+            public Builder withPrefix(String prefix) {
+                this.prefix = prefix;
+                return this;
+            }
+
+            /**
+             * Configures the Redis content field name to use.
+             * @param name the content field name to use
+             * @return this builder
+             */
+            public Builder withContentFieldName(String name) {
+                this.contentFieldName = name;
+                return this;
+            }
+
+            /**
+             * Configures the Redis embedding field name to use.
+             * @param name the embedding field name to use
+             * @return this builder
+             */
+            public Builder withEmbeddingFieldName(String name) {
+                this.embeddingFieldName = name;
+                return this;
+            }
+
+            /**
+             * Configures the Redis vector algorithmto use.
+             * @param algorithm the vector algorithm to use
+             * @return this builder
+             */
+            public Builder withVectorAlgorithm(Algorithm algorithm) {
+                this.vectorAlgorithm = algorithm;
+                return this;
+            }
+
+            public Builder withMetadataFields(MetadataField... fields) {
+                return withMetadataFields(Arrays.asList(fields));
+            }
+
+            public Builder withMetadataFields(List<MetadataField> fields) {
+                this.metadataFields = fields;
+                return this;
+            }
+
+            /**
+             * {@return the immutable configuration}
+             */
+            public RedisVectorStoreConfig build() {
+
+                return new RedisVectorStoreConfig(this);
+            }
+
+        }
+
+    }
+
+    private final boolean initializeSchema;
+
+    public static final String DEFAULT_INDEX_NAME = "spring-ai-index";
+
+    public static final String DEFAULT_CONTENT_FIELD_NAME = "content";
+
+    public static final String DEFAULT_EMBEDDING_FIELD_NAME = "embedding";
+
+    public static final String DEFAULT_PREFIX = "embedding:";
+
+    public static final Algorithm DEFAULT_VECTOR_ALGORITHM = Algorithm.HSNW;
+
+    private static final String QUERY_FORMAT = "%s=>[KNN %s @%s $%s AS %s]";
+
+    private static final Path2 JSON_SET_PATH = Path2.of("$");
+
+    private static final String JSON_PATH_PREFIX = "$.";
+
+    private static final Logger logger = LoggerFactory.getLogger(RedisVectorStore.class);
+
+    private static final Predicate<Object> RESPONSE_OK = Predicate.isEqual("OK");
+
+    private static final Predicate<Object> RESPONSE_DEL_OK = Predicate.isEqual(1l);
+
+    private static final String VECTOR_TYPE_FLOAT32 = "FLOAT32";
+
+    private static final String EMBEDDING_PARAM_NAME = "BLOB";
+
+    public static final String DISTANCE_FIELD_NAME = "vector_score";
+
+    private static final String DEFAULT_DISTANCE_METRIC = "COSINE";
+
+    private final JedisPooled jedis;
+
+    private final EmbeddingModel embeddingModel;
+
+    private final RedisVectorStoreConfig config;
+
+    private FilterExpressionConverter filterExpressionConverter;
+
+    public RedisVectorStore(RedisVectorStoreConfig config, EmbeddingModel embeddingModel, JedisPooled jedis,
+                            boolean initializeSchema) {
+
+        Assert.notNull(config, "Config must not be null");
+        Assert.notNull(embeddingModel, "Embedding model must not be null");
+        this.initializeSchema = initializeSchema;
+
+        this.jedis = jedis;
+        this.embeddingModel = embeddingModel;
+        this.config = config;
+        this.filterExpressionConverter = new RedisFilterExpressionConverter(this.config.metadataFields);
+    }
+
+    public JedisPooled getJedis() {
+        return this.jedis;
+    }
+
+    @Override
+    public void add(List<Document> documents) {
+        try (Pipeline pipeline = this.jedis.pipelined()) {
+            for (Document document : documents) {
+                var embedding = this.embeddingModel.embed(document);
+                document.setEmbedding(embedding);
+
+                var fields = new HashMap<String, Object>();
+                fields.put(this.config.embeddingFieldName, embedding);
+                fields.put(this.config.contentFieldName, document.getContent());
+                fields.putAll(document.getMetadata());
+                pipeline.jsonSetWithEscape(key(document.getId()), JSON_SET_PATH, fields);
+            }
+            List<Object> responses = pipeline.syncAndReturnAll();
+            Optional<Object> errResponse = responses.stream().filter(Predicate.not(RESPONSE_OK)).findAny();
+            if (errResponse.isPresent()) {
+                String message = MessageFormat.format("Could not add document: {0}", errResponse.get());
+                if (logger.isErrorEnabled()) {
+                    logger.error(message);
+                }
+                throw new RuntimeException(message);
+            }
+        }
+    }
+
+    private String key(String id) {
+        return this.config.prefix + id;
+    }
+
+    @Override
+    public Optional<Boolean> delete(List<String> idList) {
+        try (Pipeline pipeline = this.jedis.pipelined()) {
+            for (String id : idList) {
+                pipeline.jsonDel(key(id));
+            }
+            List<Object> responses = pipeline.syncAndReturnAll();
+            Optional<Object> errResponse = responses.stream().filter(Predicate.not(RESPONSE_DEL_OK)).findAny();
+            if (errResponse.isPresent()) {
+                if (logger.isErrorEnabled()) {
+                    logger.error("Could not delete document: {}", errResponse.get());
+                }
+                return Optional.of(false);
+            }
+            return Optional.of(true);
+        }
+    }
+
+    @Override
+    public List<Document> similaritySearch(SearchRequest request) {
+
+        Assert.isTrue(request.getTopK() > 0, "The number of documents to returned must be greater than zero");
+        Assert.isTrue(request.getSimilarityThreshold() >= 0 && request.getSimilarityThreshold() <= 1,
+                "The similarity score is bounded between 0 and 1; least to most similar respectively.");
+
+        String filter = nativeExpressionFilter(request);
+
+        String queryString = String.format(QUERY_FORMAT, filter, request.getTopK(), this.config.embeddingFieldName,
+                EMBEDDING_PARAM_NAME, DISTANCE_FIELD_NAME);
+
+        List<String> returnFields = new ArrayList<>();
+        this.config.metadataFields.stream().map(MetadataField::name).forEach(returnFields::add);
+        returnFields.add(this.config.embeddingFieldName);
+        returnFields.add(this.config.contentFieldName);
+        returnFields.add(DISTANCE_FIELD_NAME);
+        var embedding = toFloatArray(this.embeddingModel.embed(request.getQuery()));
+        Query query = new Query(queryString).addParam(EMBEDDING_PARAM_NAME, RediSearchUtil.toByteArray(embedding))
+                .returnFields(returnFields.toArray(new String[0]))
+                .setSortBy(DISTANCE_FIELD_NAME, true)
+                .dialect(2);
+
+        SearchResult result = this.jedis.ftSearch(this.config.indexName, query);
+        return result.getDocuments()
+                .stream()
+                .filter(d -> similarityScore(d) >= request.getSimilarityThreshold())
+                .map(this::toDocument)
+                .toList();
+    }
+
+    private Document toDocument(redis.clients.jedis.search.Document doc) {
+        var id = doc.getId().substring(this.config.prefix.length());
+        var content = doc.hasProperty(this.config.contentFieldName) ? doc.getString(this.config.contentFieldName)
+                : null;
+        Map<String, Object> metadata = this.config.metadataFields.stream()
+                .map(MetadataField::name)
+                .filter(doc::hasProperty)
+                .collect(Collectors.toMap(Function.identity(), doc::getString));
+        metadata.put(DISTANCE_FIELD_NAME, 1 - similarityScore(doc));
+        return new Document(id, content, metadata);
+    }
+
+    private float similarityScore(redis.clients.jedis.search.Document doc) {
+        return (2 - Float.parseFloat(doc.getString(DISTANCE_FIELD_NAME))) / 2;
+    }
+
+    private String nativeExpressionFilter(SearchRequest request) {
+        if (request.getFilterExpression() == null) {
+            return "*";
+        }
+        return "(" + this.filterExpressionConverter.convertExpression(request.getFilterExpression()) + ")";
+    }
+
+    @Override
+    public void afterPropertiesSet() {
+
+        if (!this.initializeSchema) {
+            return;
+        }
+
+        // If index already exists don't do anything
+        if (this.jedis.ftList().contains(this.config.indexName)) {
+            return;
+        }
+
+        String response = this.jedis.ftCreate(this.config.indexName,
+                FTCreateParams.createParams().on(IndexDataType.JSON).addPrefix(this.config.prefix), schemaFields());
+        if (!RESPONSE_OK.test(response)) {
+            String message = MessageFormat.format("Could not create index: {0}", response);
+            throw new RuntimeException(message);
+        }
+    }
+
+    private Iterable<SchemaField> schemaFields() {
+        Map<String, Object> vectorAttrs = new HashMap<>();
+        vectorAttrs.put("DIM", this.embeddingModel.dimensions());
+        vectorAttrs.put("DISTANCE_METRIC", DEFAULT_DISTANCE_METRIC);
+        vectorAttrs.put("TYPE", VECTOR_TYPE_FLOAT32);
+        List<SchemaField> fields = new ArrayList<>();
+        fields.add(TextField.of(jsonPath(this.config.contentFieldName)).as(this.config.contentFieldName).weight(1.0));
+        fields.add(VectorField.builder()
+                .fieldName(jsonPath(this.config.embeddingFieldName))
+                .algorithm(vectorAlgorithm())
+                .attributes(vectorAttrs)
+                .as(this.config.embeddingFieldName)
+                .build());
+
+        if (!CollectionUtils.isEmpty(this.config.metadataFields)) {
+            for (MetadataField field : this.config.metadataFields) {
+                fields.add(schemaField(field));
+            }
+        }
+        return fields;
+    }
+
+    private SchemaField schemaField(MetadataField field) {
+        String fieldName = jsonPath(field.name);
+        switch (field.fieldType) {
+            case NUMERIC:
+                return NumericField.of(fieldName).as(field.name);
+            case TAG:
+                return TagField.of(fieldName).as(field.name);
+            case TEXT:
+                return TextField.of(fieldName).as(field.name);
+            default:
+                throw new IllegalArgumentException(
+                        MessageFormat.format("Field {0} has unsupported type {1}", field.name, field.fieldType));
+        }
+    }
+
+    private VectorAlgorithm vectorAlgorithm() {
+        if (config.vectorAlgorithm == Algorithm.HSNW) {
+            return VectorAlgorithm.HNSW;
+        }
+        return VectorAlgorithm.FLAT;
+    }
+
+    private String jsonPath(String field) {
+        return JSON_PATH_PREFIX + field;
+    }
+
+    private static float[] toFloatArray(List<Double> embeddingDouble) {
+        float[] embeddingFloat = new float[embeddingDouble.size()];
+        int i = 0;
+        for (Double d : embeddingDouble) {
+            embeddingFloat[i++] = d.floatValue();
+        }
+        return embeddingFloat;
+    }
+
+}

BIN
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/resources/webapp/test/Fel.pdf


+ 70 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/AzureOpenAIChatModelTests.java

@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.framework.ai.chat;
+
+import com.azure.ai.openai.OpenAIClient;
+import com.azure.ai.openai.OpenAIClientBuilder;
+import com.azure.core.credential.AzureKeyCredential;
+import com.azure.core.util.ClientOptions;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
+import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
+import org.springframework.ai.chat.messages.Message;
+import org.springframework.ai.chat.messages.SystemMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import reactor.core.publisher.Flux;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME;
+
+/**
+ * {@link AzureOpenAiChatModel} 集成测试
+ *
+ * @author 芋道源码
+ */
+public class AzureOpenAIChatModelTests {
+
+    private final OpenAIClient openAiApi = (new OpenAIClientBuilder())
+            .endpoint("https://eastusprejade.openai.azure.com")
+            .credential(new AzureKeyCredential("xxx"))
+            .clientOptions((new ClientOptions()).setApplicationId("spring-ai"))
+            .buildClient();
+    private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi,
+            AzureOpenAiChatOptions.builder().withDeploymentName(DEFAULT_DEPLOYMENT_NAME).build());
+
+    @Test
+    @Disabled
+    public void testCall() {
+        // 准备参数
+        List<Message> messages = new ArrayList<>();
+        messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
+        messages.add(new UserMessage("1 + 1 = ?"));
+
+        // 调用
+        ChatResponse response = chatModel.call(new Prompt(messages));
+        // 打印结果
+        System.out.println(response);
+        System.out.println(response.getResult().getOutput());
+    }
+
+    @Test
+    @Disabled
+    public void testStream() {
+        // 准备参数
+        List<Message> messages = new ArrayList<>();
+        messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
+        messages.add(new UserMessage("1 + 1 = ?"));
+
+        // 调用
+        Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
+        // 打印结果
+        flux.doOnNext(response -> {
+//            System.out.println(response);
+            System.out.println(response.getResult().getOutput());
+        }).then().block();
+    }
+
+}

+ 1 - 2
yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.framework.ai.chat;
 
-import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.springframework.ai.chat.messages.Message;
@@ -17,7 +16,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 /**
- * {@link XingHuoChatModel} 集成测试
+ * {@link OpenAiChatModel} 集成测试
  *
  * @author 芋道源码
  */

+ 0 - 1
yudao-module-mall/yudao-module-product-api/src/main/java/cn/iocoder/yudao/module/product/api/spu/dto/ProductSpuRespDTO.java

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.product.api.spu.dto;
 import cn.iocoder.yudao.module.product.enums.spu.ProductSpuStatusEnum;
 import lombok.Data;
 
-// TODO @LeeYan9: ProductSpuRespDTO
 /**
  * 商品 SPU 信息 Response DTO
  *

+ 0 - 1
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/app/spu/AppProductSpuController.java

@@ -148,5 +148,4 @@ public class AppProductSpuController {
         return price - newPrice;
     }
 
-    // TODO 芋艿:商品的浏览记录;
 }

+ 0 - 2
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/brand/ProductBrandDO.java

@@ -48,6 +48,4 @@ public class ProductBrandDO extends BaseDO {
      */
     private Integer status;
 
-    // TODO 芋艿:firstLetter 首字母
-
 }

+ 0 - 6
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/dataobject/sku/ProductSkuDO.java

@@ -130,11 +130,5 @@ public class ProductSkuDO extends BaseDO {
 
     }
 
-    // TODO 芋艿:integral from y
-    // TODO 芋艿:pinkPrice from y
-    // TODO 芋艿:seckillPrice from y
-    // TODO 芋艿:pinkStock from y
-    // TODO 芋艿:seckillStock from y
-
 }
 

+ 3 - 31
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java

@@ -10,23 +10,20 @@ import cn.iocoder.yudao.module.promotion.controller.app.combination.vo.record.Ap
 import cn.iocoder.yudao.module.promotion.controller.app.combination.vo.record.AppCombinationRecordSummaryRespVO;
 import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivityConvert;
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
-import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
 import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordService;
-import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.Parameters;
 import io.swagger.v3.oas.annotations.tags.Tag;
-import org.springframework.context.annotation.Lazy;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Max;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
-import javax.annotation.Resource;
-import javax.validation.Valid;
-import javax.validation.constraints.Max;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
@@ -43,9 +40,6 @@ public class AppCombinationRecordController {
 
     @Resource
     private CombinationRecordService combinationRecordService;
-    @Resource
-    @Lazy
-    private TradeOrderApi tradeOrderApi;
 
     @GetMapping("/get-summary")
     @Operation(summary = "获得拼团记录的概要信息", description = "用于小程序首页")
@@ -117,26 +111,4 @@ public class AppCombinationRecordController {
         return success(CombinationActivityConvert.INSTANCE.convert(getLoginUserId(), headRecord, memberRecords));
     }
 
-    @GetMapping("/cancel")
-    @Operation(summary = "取消拼团")
-    @Parameter(name = "id", description = "拼团记录编号", required = true, example = "1024")
-    public CommonResult<Boolean> cancelCombinationRecord(@RequestParam("id") Long id) {
-        Long userId = getLoginUserId();
-        // 1、查找这条拼团记录
-        CombinationRecordDO record = combinationRecordService.getCombinationRecordByIdAndUser(userId, id);
-        if (record == null) {
-            return success(Boolean.FALSE);
-        }
-        // 1.1、需要先校验拼团记录未完成;
-        if (!CombinationRecordStatusEnum.isInProgress(record.getStatus())) {
-            return success(Boolean.FALSE);
-        }
-
-        // 2. 取消已支付的订单
-        tradeOrderApi.cancelPaidOrder(userId, record.getOrderId());
-        // 3. 取消拼团记录
-        combinationRecordService.cancelCombinationRecord(userId, record.getId(), record.getHeadId());
-        return success(Boolean.TRUE);
-    }
-
 }

+ 0 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java

@@ -72,7 +72,6 @@ public interface SeckillActivityMapper extends BaseMapperX<SeckillActivityDO> {
     default PageResult<SeckillActivityDO> selectPage(AppSeckillActivityPageReqVO pageReqVO, Integer status) {
         return selectPage(pageReqVO, new LambdaQueryWrapperX<SeckillActivityDO>()
                 .eqIfPresent(SeckillActivityDO::getStatus, status)
-                // TODO 芋艿:对 find in set 的想法;
                 .apply(ObjectUtil.isNotNull(pageReqVO.getConfigId()), "FIND_IN_SET(" + pageReqVO.getConfigId() + ",config_ids) > 0"));
     }
 

+ 0 - 1
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/job/coupon/CouponExpireJob.java

@@ -8,7 +8,6 @@ import org.springframework.stereotype.Component;
 
 import javax.annotation.Resource;
 
-// TODO 芋艿:配置一个 Job
 /**
  * 优惠券过期 Job
  *

+ 0 - 18
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordService.java

@@ -139,24 +139,6 @@ public interface CombinationRecordService {
                                                               @Nullable Integer status,
                                                               @Nullable Long headId);
 
-    /**
-     * 获取拼团记录
-     *
-     * @param userId 用户编号
-     * @param id     拼团记录编号
-     * @return 拼团记录
-     */
-    CombinationRecordDO getCombinationRecordByIdAndUser(Long userId, Long id);
-
-    /**
-     * 取消拼团
-     *
-     * @param userId 用户编号
-     * @param id     拼团记录编号
-     * @param headId 团长编号
-     */
-    void cancelCombinationRecord(Long userId, Long id, Long headId);
-
     /**
      * 处理过期拼团
      *

+ 0 - 56
yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java

@@ -69,7 +69,6 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
     private ProductSpuApi productSpuApi;
     @Resource
     private ProductSkuApi productSkuApi;
-
     @Resource
     @Lazy // 延迟加载,避免循环依赖
     private TradeOrderApi tradeOrderApi;
@@ -289,61 +288,6 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
         return combinationRecordMapper.selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId(activityIds, status, headId);
     }
 
-    @Override
-    public CombinationRecordDO getCombinationRecordByIdAndUser(Long userId, Long id) {
-        return combinationRecordMapper.selectOne(CombinationRecordDO::getUserId, userId, CombinationRecordDO::getId, id);
-    }
-
-    @Override
-    @Transactional(rollbackFor = Exception.class)
-    public void cancelCombinationRecord(Long userId, Long id, Long headId) {
-        // 删除记录
-        combinationRecordMapper.deleteById(id);
-
-        // 需要更新的记录
-        List<CombinationRecordDO> updateRecords = new ArrayList<>();
-        // 如果它是团长,则顺序(下单时间)继承
-        if (Objects.equals(headId, CombinationRecordDO.HEAD_ID_GROUP)) { // 情况一:团长
-            // 团员
-            List<CombinationRecordDO> list = getCombinationRecordListByHeadId(id);
-            if (CollUtil.isEmpty(list)) {
-                return;
-            }
-            // 按照创建时间升序排序
-            list.sort(Comparator.comparing(CombinationRecordDO::getCreateTime)); // 影响原 list
-            CombinationRecordDO newHead = list.get(0); // 新团长继位
-            list.forEach(item -> {
-                CombinationRecordDO recordDO = new CombinationRecordDO();
-                recordDO.setId(item.getId());
-                if (ObjUtil.equal(item.getId(), newHead.getId())) { // 新团长
-                    recordDO.setHeadId(CombinationRecordDO.HEAD_ID_GROUP);
-                } else {
-                    recordDO.setHeadId(newHead.getId());
-                }
-                recordDO.setUserCount(list.size());
-                updateRecords.add(recordDO);
-            });
-        } else { // 情况二:团员
-            // 团长
-            CombinationRecordDO recordHead = combinationRecordMapper.selectById(headId);
-            // 团员
-            List<CombinationRecordDO> records = getCombinationRecordListByHeadId(headId);
-            if (CollUtil.isEmpty(records)) {
-                return;
-            }
-            records.add(recordHead); // 加入团长,团长数据也需要更新
-            records.forEach(item -> {
-                CombinationRecordDO recordDO = new CombinationRecordDO();
-                recordDO.setId(item.getId());
-                recordDO.setUserCount(records.size());
-                updateRecords.add(recordDO);
-            });
-        }
-
-        // 更新拼团记录
-        combinationRecordMapper.updateBatch(updateRecords);
-    }
-
     @Override
     public KeyValue<Integer, Integer> expireCombinationRecord() {
         // 1. 获取所有正在进行中的过期的父拼团

+ 1 - 4
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/product/ProductStatisticsJob.java

@@ -7,12 +7,9 @@ import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
 import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
 import cn.iocoder.yudao.module.statistics.service.product.ProductStatisticsService;
+import jakarta.annotation.Resource;
 import org.springframework.stereotype.Component;
 
-import javax.annotation.Resource;
-
-// TODO 芋艿:缺个 Job 的配置;等和 Product 一起配置
-
 /**
  * 商品统计 Job
  *

+ 0 - 1
yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/trade/TradeStatisticsJob.java

@@ -11,7 +11,6 @@ import org.springframework.stereotype.Component;
 
 import javax.annotation.Resource;
 
-// TODO 芋艿:缺个 Job 的配置;等和 Product 一起配置
 /**
  * 交易统计 Job
  *

+ 3 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java

@@ -21,6 +21,9 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
 @ToString(callSuper = true)
 public class AfterSalePageReqVO extends PageParam {
 
+    @Schema(description = "用户编号", example = "1024")
+    private Long userId;
+
     @Schema(description = "售后流水号", example = "202211190847450020500077")
     private String no;
 

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java

@@ -101,7 +101,7 @@ public interface TradeOrderConvert {
     default PayOrderCreateReqDTO convert(TradeOrderDO order, List<TradeOrderItemDO> orderItems,
                                          TradeOrderProperties orderProperties) {
         PayOrderCreateReqDTO createReqDTO = new PayOrderCreateReqDTO()
-                .setAppId(orderProperties.getAppId()).setUserIp(order.getUserIp());
+                .setAppKey(orderProperties.getPayAppKey()).setUserIp(order.getUserIp());
         // 商户相关字段
         createReqDTO.setMerchantOrderId(String.valueOf(order.getId()));
         String subject = orderItems.get(0).getSpuName();

+ 1 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/aftersale/AfterSaleMapper.java

@@ -16,6 +16,7 @@ public interface AfterSaleMapper extends BaseMapperX<AfterSaleDO> {
 
     default PageResult<AfterSaleDO> selectPage(AfterSalePageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<AfterSaleDO>()
+                .eqIfPresent(AfterSaleDO::getUserId, reqVO.getUserId())
                 .likeIfPresent(AfterSaleDO::getNo, reqVO.getNo())
                 .eqIfPresent(AfterSaleDO::getStatus, reqVO.getStatus())
                 .eqIfPresent(AfterSaleDO::getType, reqVO.getType())

+ 0 - 4
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/dal/mysql/package-info.java

@@ -1,4 +0,0 @@
-/**
- * TODO 占位
- */
-package cn.iocoder.yudao.module.trade.dal.mysql;

+ 0 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderConfig.java

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.trade.framework.order.config;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Configuration;
 
-// TODO @LeeYan9: 可以直接给 TradeOrderProperties 一个 @Component生效哈
 /**
  * @author LeeYan9
  * @since 2022-09-15

+ 10 - 4
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java

@@ -1,10 +1,12 @@
 package cn.iocoder.yudao.module.trade.framework.order.config;
 
+import jakarta.validation.constraints.NotEmpty;
 import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.validation.annotation.Validated;
 
-import javax.validation.constraints.NotNull;
+import jakarta.validation.constraints.NotNull;
+
 import java.time.Duration;
 
 /**
@@ -18,11 +20,15 @@ import java.time.Duration;
 @Validated
 public class TradeOrderProperties {
 
+    private static final String PAY_APP_KEY_DEFAULT = "mall";
+
     /**
-     * 应用编号
+     * 支付应用标识
+     *
+     * 在 pay 模块的 [支付管理 -> 应用信息] 里添加
      */
-    @NotNull(message = "应用编号不能为空")
-    private Long appId;
+    @NotEmpty(message = "Pay 应用标识不能为空")
+    private String payAppKey = PAY_APP_KEY_DEFAULT;
 
     /**
      * 支付超时时间

+ 0 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java

@@ -125,7 +125,6 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
 
     @Override
     public BrokerageUserDO getOrCreateBrokerageUser(Long id) {
-        // TODO @芋艿:这块优化下;统一到注册时处理;
         BrokerageUserDO brokerageUser = brokerageUserMapper.selectById(id);
         // 特殊:人人分销的情况下,如果分销人为空则创建分销人
         if (brokerageUser == null && ObjUtil.equal(BrokerageEnabledConditionEnum.ALL.getCondition(),

+ 3 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java

@@ -855,12 +855,14 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void cancelPaidOrder(Long userId, Long orderId) {
-        // TODO 芋艿:这里实现要优化下;
+        // TODO @puhui999:需要校验状态;已支付的情况下,才可以。
         TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
         if (order == null) {
             throw exception(ORDER_NOT_FOUND);
         }
         cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL);
+
+        // TODO @puhui999:需要退款
     }
 
     /**

+ 3 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/price/calculator/TradePriceCalculatorHelper.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.trade.service.price.calculator;
 
 import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
 import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
@@ -61,7 +62,7 @@ public class TradePriceCalculatorHelper {
             orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId())
                     .setDeliveryTemplateId(spu.getDeliveryTemplateId())
                     .setGivePoint(spu.getGiveIntegral()).setUsePoint(0);
-            if (orderItem.getPicUrl() == null) {
+            if (StrUtil.isBlank(orderItem.getPicUrl())) {
                 orderItem.setPicUrl(spu.getPicUrl());
             }
         });
@@ -240,7 +241,7 @@ public class TradePriceCalculatorHelper {
      *
      * 和 {@link #dividePrice(List, Integer)} 逻辑一致,只是传入的是 TradeOrderItemDO 对象
      *
-     * @param items         订单项
+     * @param items 订单项
      * @param price 订单支付金额
      * @return 分摊金额数组,和传入的 orderItems 一一对应
      */

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/test/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceTest.java

@@ -99,7 +99,7 @@ public class TradeOrderUpdateServiceTest extends BaseDbUnitTest {
 
     @BeforeEach
     public void setUp() {
-        when(tradeOrderProperties.getAppId()).thenReturn(888L);
+        when(tradeOrderProperties.getPayAppKey()).thenReturn("mall");
         when(tradeOrderProperties.getPayExpireTime()).thenReturn(Duration.ofDays(1));
         when(tradeNoRedisDAO.generate(anyString())).thenReturn(IdUtil.randomUUID());
     }

+ 6 - 6
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java

@@ -1,11 +1,11 @@
 package cn.iocoder.yudao.module.pay.api.order.dto;
 
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 import org.hibernate.validator.constraints.Length;
 
-import javax.validation.constraints.DecimalMin;
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
 import java.io.Serializable;
 import java.time.LocalDateTime;
 
@@ -18,10 +18,10 @@ public class PayOrderCreateReqDTO implements Serializable {
     public static final int SUBJECT_MAX_LENGTH = 32;
 
     /**
-     * 应用编号
+     * 应用标识
      */
-    @NotNull(message = "应用编号不能为空")
-    private Long appId;
+    @NotNull(message = "应用标识不能为空")
+    private String appKey;
     /**
      * 用户 IP
      */

+ 6 - 7
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java

@@ -1,12 +1,11 @@
 package cn.iocoder.yudao.module.pay.api.refund.dto;
 
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 import org.hibernate.validator.constraints.Length;
 
-import javax.validation.constraints.Min;
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
-
 /**
  * 退款单创建 Request DTO
  *
@@ -16,10 +15,10 @@ import javax.validation.constraints.NotNull;
 public class PayRefundCreateReqDTO {
 
     /**
-     * 应用编号
+     * 应用标识
      */
-    @NotNull(message = "应用编号不能为空")
-    private Long appId;
+    @NotNull(message = "应用标识不能为空")
+    private String appKey;
     /**
      * 用户 IP
      */

+ 7 - 7
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java

@@ -2,12 +2,12 @@ package cn.iocoder.yudao.module.pay.api.transfer.dto;
 
 import cn.iocoder.yudao.framework.common.validation.InEnum;
 import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferTypeEnum;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
-import javax.validation.constraints.Min;
-import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
 import java.util.Map;
 
 /**
@@ -19,10 +19,10 @@ import java.util.Map;
 public class PayTransferCreateReqDTO {
 
     /**
-     * 应用编号
+     * 应用标识
      */
-    @NotNull(message = "应用编号不能为空")
-    private Long appId;
+    @NotNull(message = "应用标识不能为空")
+    private String appKey;
 
     @NotEmpty(message = "转账渠道不能为空")
     private String channelCode;

+ 1 - 0
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java

@@ -14,6 +14,7 @@ public interface ErrorCodeConstants {
     ErrorCode APP_IS_DISABLE = new ErrorCode(1_007_000_002, "App 已经被禁用");
     ErrorCode APP_EXIST_ORDER_CANT_DELETE =  new ErrorCode(1_007_000_003, "支付应用存在支付订单,无法删除");
     ErrorCode APP_EXIST_REFUND_CANT_DELETE =  new ErrorCode(1_007_000_004, "支付应用存在退款订单,无法删除");
+    ErrorCode APP_KEY_EXISTS = new ErrorCode(1_007_000_005, "支付应用标识已经存在");
 
     // ========== CHANNEL 模块 1-007-001-000 ==========
     ErrorCode CHANNEL_NOT_FOUND = new ErrorCode(1_007_001_000, "支付渠道的配置不存在");

+ 4 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java

@@ -14,6 +14,10 @@ import javax.validation.constraints.*;
 @Data
 public class PayAppBaseVO {
 
+    @Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
+    @NotEmpty(message = "应用标识不能为空")
+    private String appKey;
+
     @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "小豆")
     @NotNull(message = "应用名不能为空")
     private String name;

+ 4 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java

@@ -1,6 +1,9 @@
 package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
+
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
 
 @Schema(description = "管理后台 - 支付应用信息创建 Request VO")
 @Data

+ 3 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppPageReqVO.java

@@ -20,6 +20,9 @@ public class PayAppPageReqVO extends PageParam {
     @Schema(description = "应用名", example = "小豆")
     private String name;
 
+    @Schema(description = "应用标识", example = "yudao")
+    private String appKey;
+
     @Schema(description = "开启状态", example = "0")
     private Integer status;
 

+ 7 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppRespVO.java

@@ -1,6 +1,9 @@
 package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
+
 import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
 
 import java.time.LocalDateTime;
 
@@ -13,6 +16,9 @@ public class PayAppRespVO extends PayAppBaseVO {
     @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
     private Long id;
 
+    @Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
+    private String appKey;
+
     @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
     private LocalDateTime createTime;
 

+ 1 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java

@@ -1,4 +1,5 @@
 package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
+
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
 import javax.validation.constraints.*;

+ 10 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java

@@ -1,6 +1,8 @@
 package cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.transaction;
 
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.validation.InEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -8,7 +10,14 @@ import lombok.Data;
 @Data
 public class PayWalletTransactionPageReqVO extends PageParam  {
 
-    @Schema(description = "钱包编号",  example = "1")
+    @Schema(description = "钱包编号", example = "888")
     private Long walletId;
 
+    @Schema(description = "用户编号", example = "1024")
+    private Long userId;
+
+    @Schema(description = "用户类型", example = "1")
+    @InEnum(UserTypeEnum.class)
+    private Integer userType;
+
 }

+ 4 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java

@@ -31,6 +31,10 @@ public class PayAppDO extends BaseDO {
      */
     @TableId
     private Long id;
+    /**
+     * 应用标识
+     */
+    private String appKey;
     /**
      * 应用名
      */

+ 5 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/app/PayAppMapper.java

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.pay.dal.mysql.app;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
-import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
 import org.apache.ibatis.annotations.Mapper;
@@ -14,9 +13,14 @@ public interface PayAppMapper extends BaseMapperX<PayAppDO> {
     default PageResult<PayAppDO> selectPage(PayAppPageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<PayAppDO>()
                 .likeIfPresent(PayAppDO::getName, reqVO.getName())
+                .likeIfPresent(PayAppDO::getAppKey, reqVO.getAppKey())
                 .eqIfPresent(PayAppDO::getStatus, reqVO.getStatus())
                 .betweenIfPresent(PayAppDO::getCreateTime, reqVO.getCreateTime())
                 .orderByDesc(PayAppDO::getId));
     }
 
+    default PayAppDO selectByAppKey(String appKey) {
+        return selectOne(PayAppDO::getAppKey, appKey);
+    }
+
 }

+ 8 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/config/PayProperties.java

@@ -15,6 +15,8 @@ public class PayProperties {
     private static final String ORDER_NO_PREFIX = "P";
     private static final String REFUND_NO_PREFIX = "R";
 
+    private static final String WALLET_PAY_APP_KEY_DEFAULT = "wallet";
+
     /**
      * 支付回调地址
      *
@@ -49,4 +51,10 @@ public class PayProperties {
     @NotEmpty(message = "退款订单 no 的前缀不能为空")
     private String refundNoPrefix = REFUND_NO_PREFIX;
 
+    /**
+     * 钱包支付应用 AppKey
+     */
+    @NotEmpty(message = "钱包支付应用 AppKey 不能为空")
+    private String walletPayAppKey = WALLET_PAY_APP_KEY_DEFAULT;
+
 }

+ 13 - 3
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppService.java

@@ -7,8 +7,8 @@ import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
+import jakarta.validation.Valid;
 
-import javax.validation.Valid;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -88,13 +88,13 @@ public interface PayAppService {
      * @return 商户 Map
      */
     default Map<Long, PayAppDO> getAppMap(Collection<Long> ids) {
-        List<PayAppDO> list =  getAppList(ids);
+        List<PayAppDO> list = getAppList(ids);
         return CollectionUtils.convertMap(list, PayAppDO::getId);
     }
 
     /**
      * 支付应用的合法性
-     *
+     * <p>
      * 如果不合法,抛出 {@link ServiceException} 业务异常
      *
      * @param id 应用编号
@@ -102,4 +102,14 @@ public interface PayAppService {
      */
     PayAppDO validPayApp(Long id);
 
+    /**
+     * 支付应用的合法性
+     * <p>
+     * 如果不合法,抛出 {@link ServiceException} 业务异常
+     *
+     * @param appKey 应用标识
+     * @return 应用
+     */
+    PayAppDO validPayApp(String appKey);
+
 }

+ 41 - 5
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceImpl.java

@@ -11,11 +11,11 @@ import cn.iocoder.yudao.module.pay.dal.mysql.app.PayAppMapper;
 import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
+import jakarta.annotation.Resource;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
 import java.util.Collection;
 import java.util.List;
 
@@ -43,6 +43,9 @@ public class PayAppServiceImpl implements PayAppService {
 
     @Override
     public Long createApp(PayAppCreateReqVO createReqVO) {
+        // 验证 appKey 是否重复
+        validateAppKeyUnique(null, createReqVO.getAppKey());
+
         // 插入
         PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO);
         appMapper.insert(app);
@@ -54,11 +57,28 @@ public class PayAppServiceImpl implements PayAppService {
     public void updateApp(PayAppUpdateReqVO updateReqVO) {
         // 校验存在
         validateAppExists(updateReqVO.getId());
+        // 验证 appKey 是否重复
+        validateAppKeyUnique(updateReqVO.getId(), updateReqVO.getAppKey());
+
         // 更新
         PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO);
         appMapper.updateById(updateObj);
     }
 
+    void validateAppKeyUnique(Long id, String appKey) {
+        PayAppDO app = appMapper.selectByAppKey(appKey);
+        if (app == null) {
+            return;
+        }
+        // 如果 id 为空,说明不用比较是否为相同 appKey 的应用
+        if (id == null) {
+            throw exception(APP_KEY_EXISTS);
+        }
+        if (!app.getId().equals(id)) {
+            throw exception(APP_KEY_EXISTS);
+        }
+    }
+
     @Override
     public void updateAppStatus(Long id, Integer status) {
         // 校验商户存在
@@ -101,7 +121,7 @@ public class PayAppServiceImpl implements PayAppService {
 
     @Override
     public List<PayAppDO> getAppList() {
-         return appMapper.selectList();
+        return appMapper.selectList();
     }
 
     @Override
@@ -110,14 +130,30 @@ public class PayAppServiceImpl implements PayAppService {
     }
 
     @Override
-    public PayAppDO validPayApp(Long id) {
-        PayAppDO app = appMapper.selectById(id);
+    public PayAppDO validPayApp(Long appId) {
+        PayAppDO app = appMapper.selectById(appId);
+        return validatePayApp(app);
+    }
+
+    @Override
+    public PayAppDO validPayApp(String appKey) {
+        PayAppDO app = appMapper.selectByAppKey(appKey);
+        return validatePayApp(app);
+    }
+
+    /**
+     * 校验支付应用实体的有效性:存在 + 开启
+     *
+     * @param app 待校验的支付应用实体
+     * @return 校验通过的支付应用实体
+     */
+    private PayAppDO validatePayApp(PayAppDO app) {
         // 校验是否存在
         if (app == null) {
             throw exception(ErrorCodeConstants.APP_NOT_FOUND);
         }
         // 校验是否禁用
-        if (CommonStatusEnum.DISABLE.getStatus().equals(app.getStatus())) {
+        if (CommonStatusEnum.isDisable(app.getStatus())) {
             throw exception(ErrorCodeConstants.APP_IS_DISABLE);
         }
         return app;

+ 4 - 4
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java

@@ -43,11 +43,11 @@ import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
 public class PayDemoOrderServiceImpl implements PayDemoOrderService {
 
     /**
-     * 接入的实力应用编号
+     * 接入的支付应用标识
      *
      * 从 [支付管理 -> 应用信息] 里添加
      */
-    private static final Long PAY_APP_ID = 7L;
+    private static final String PAY_APP_KEY = "demo";
 
     /**
      * 商品信息 Map
@@ -88,7 +88,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
 
         // 2.1 创建支付单
         Long payOrderId = payOrderApi.createOrder(new PayOrderCreateReqDTO()
-                .setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
+                .setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用
                 .setMerchantOrderId(demoOrder.getId().toString()) // 业务的订单编号
                 .setSubject(spuName).setBody("").setPrice(price) // 价格信息
                 .setExpireTime(addTime(Duration.ofHours(2L)))); // 支付的过期时间
@@ -190,7 +190,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
         String refundId = order.getId() + "-refund";
         // 2.2 创建退款单
         Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO()
-                .setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
+                .setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用
                 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
                 .setMerchantRefundId(refundId)
                 .setReason("想退钱").setPrice(order.getPrice()));// 价格信息

+ 2 - 2
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java

@@ -111,11 +111,11 @@ public class PayOrderServiceImpl implements PayOrderService {
     @Override
     public Long createOrder(PayOrderCreateReqDTO reqDTO) {
         // 校验 App
-        PayAppDO app = appService.validPayApp(reqDTO.getAppId());
+        PayAppDO app = appService.validPayApp(reqDTO.getAppKey());
 
         // 查询对应的支付交易单是否已经存在。如果是,则直接返回
         PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId(
-                reqDTO.getAppId(), reqDTO.getMerchantOrderId());
+                app.getId(), reqDTO.getMerchantOrderId());
         if (order != null) {
             log.warn("[createOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
                     order.getMerchantOrderId(), toJsonString(order)); // 理论来说,不会出现这个情况

+ 11 - 10
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java

@@ -26,12 +26,12 @@ import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -93,9 +93,9 @@ public class PayRefundServiceImpl implements PayRefundService {
     @Override
     public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
         // 1.1 校验 App
-        PayAppDO app = appService.validPayApp(reqDTO.getAppId());
+        PayAppDO app = appService.validPayApp(reqDTO.getAppKey());
         // 1.2 校验支付订单
-        PayOrderDO order = validatePayOrderCanRefund(reqDTO);
+        PayOrderDO order = validatePayOrderCanRefund(reqDTO, app.getId());
         // 1.3 校验支付渠道是否有效
         PayChannelDO channel = channelService.validPayChannel(order.getChannelId());
         PayClient client = channelService.getPayClient(channel.getId());
@@ -113,7 +113,7 @@ public class PayRefundServiceImpl implements PayRefundService {
         // 2.1 插入退款单
         String no = noRedisDAO.generate(payProperties.getRefundNoPrefix());
         refund = PayRefundConvert.INSTANCE.convert(reqDTO)
-                .setNo(no).setOrderId(order.getId()).setOrderNo(order.getNo())
+                .setNo(no).setAppId(app.getId()).setOrderId(order.getId()).setOrderNo(order.getNo())
                 .setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode())
                 // 商户相关的字段
                 .setNotifyUrl(app.getRefundNotifyUrl())
@@ -153,8 +153,8 @@ public class PayRefundServiceImpl implements PayRefundService {
      * @param reqDTO 退款申请信息
      * @return 支付订单
      */
-    private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO) {
-        PayOrderDO order = orderService.getOrder(reqDTO.getAppId(), reqDTO.getMerchantOrderId());
+    private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO, Long appId) {
+        PayOrderDO order = orderService.getOrder(appId, reqDTO.getMerchantOrderId());
         if (order == null) {
             throw exception(PAY_ORDER_NOT_FOUND);
         }
@@ -164,11 +164,11 @@ public class PayRefundServiceImpl implements PayRefundService {
         }
 
         // 校验金额,退款金额不能大于原定的金额
-        if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){
+        if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()) {
             throw exception(REFUND_PRICE_EXCEED);
         }
         // 是否有退款中的订单
-        if (refundMapper.selectCountByAppIdAndOrderId(reqDTO.getAppId(), order.getId(),
+        if (refundMapper.selectCountByAppIdAndOrderId(appId, order.getId(),
                 PayRefundStatusEnum.WAITING.getStatus()) > 0) {
             throw exception(REFUND_HAS_REFUNDING);
         }
@@ -197,9 +197,10 @@ public class PayRefundServiceImpl implements PayRefundService {
      * 通知并更新订单的退款结果
      *
      * @param channel 支付渠道
-     * @param notify 通知
+     * @param notify  通知
      */
-    @Transactional(rollbackFor = Exception.class)  // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效
+    // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效
+    @Transactional(rollbackFor = Exception.class)
     public void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) {
         // 情况一:退款成功
         if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) {

+ 7 - 7
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java

@@ -24,12 +24,12 @@ import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
+import jakarta.annotation.Resource;
+import jakarta.validation.Validator;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import javax.annotation.Resource;
-import javax.validation.Validator;
 import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -79,16 +79,16 @@ public class PayTransferServiceImpl implements PayTransferService {
     @Override
     public Long createTransfer(PayTransferCreateReqDTO reqDTO) {
         // 1.1 校验 App
-        PayAppDO payApp = appService.validPayApp(reqDTO.getAppId());
+        PayAppDO payApp = appService.validPayApp(reqDTO.getAppKey());
         // 1.2 校验支付渠道是否有效
-        PayChannelDO channel = channelService.validPayChannel(reqDTO.getAppId(), reqDTO.getChannelCode());
+        PayChannelDO channel = channelService.validPayChannel(payApp.getId(), reqDTO.getChannelCode());
         PayClient client = channelService.getPayClient(channel.getId());
         if (client == null) {
             log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
             throw exception(CHANNEL_NOT_FOUND);
         }
         // 1.3 校验转账单已经发起过转账。
-        PayTransferDO transfer = validateTransferCanCreate(reqDTO);
+        PayTransferDO transfer = validateTransferCanCreate(reqDTO, payApp.getId());
 
         if (transfer == null) {
             // 2.不存在创建转账单. 否则允许使用相同的 no 再次发起转账
@@ -116,8 +116,8 @@ public class PayTransferServiceImpl implements PayTransferService {
         return transfer.getId();
     }
 
-    private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto) {
-        PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(dto.getAppId(), dto.getMerchantTransferId());
+    private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto, Long appId) {
+        PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, dto.getMerchantTransferId());
         if (transfer != null) {
             // 已经存在,并且状态不为等待状态。说明已经调用渠道转账并返回结果.
             if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) {

+ 7 - 7
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java

@@ -18,6 +18,7 @@ import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletRechargeMapper;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
+import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
 import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
@@ -51,11 +52,6 @@ import static cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum.*;
 @Slf4j
 public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
 
-    /**
-     * TODO 芋艿:放到 payconfig
-     */
-    private static final Long WALLET_PAY_APP_ID = 8L;
-
     private static final String WALLET_RECHARGE_ORDER_SUBJECT = "钱包余额充值";
 
     @Resource
@@ -68,9 +64,13 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
     private PayRefundService payRefundService;
     @Resource
     private PayWalletRechargePackageService payWalletRechargePackageService;
+
     @Resource
     public SocialClientApi socialClientApi;
 
+    @Resource
+    private PayProperties payProperties;
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public PayWalletRechargeDO createWalletRecharge(Long userId, Integer userType, String userIp,
@@ -92,7 +92,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
 
         // 2.1 创建支付单
         Long payOrderId = payOrderService.createOrder(new PayOrderCreateReqDTO()
-                .setAppId(WALLET_PAY_APP_ID).setUserIp(userIp)
+                .setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp)
                 .setMerchantOrderId(recharge.getId().toString()) // 业务的订单编号
                 .setSubject(WALLET_RECHARGE_ORDER_SUBJECT).setBody("")
                 .setPrice(recharge.getPayPrice())
@@ -174,7 +174,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
         String walletRechargeId = String.valueOf(id);
         String refundId = walletRechargeId + "-refund";
         Long payRefundId = payRefundService.createPayRefund(new PayRefundCreateReqDTO()
-                .setAppId(WALLET_PAY_APP_ID).setUserIp(userIp)
+                .setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp)
                 .setMerchantOrderId(walletRechargeId)
                 .setMerchantRefundId(refundId)
                 .setReason("想退钱").setPrice(walletRecharge.getPayPrice()));

+ 12 - 2
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletTransactionServiceImpl.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.pay.service.wallet;
 
+import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionPageReqVO;
 import cn.iocoder.yudao.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO;
@@ -11,12 +12,11 @@ import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletTransactionMapper;
 import cn.iocoder.yudao.module.pay.dal.redis.no.PayNoRedisDAO;
 import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
 import cn.iocoder.yudao.module.pay.service.wallet.bo.WalletTransactionCreateReqBO;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
-
 import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO.TYPE_EXPENSE;
@@ -53,6 +53,16 @@ public class PayWalletTransactionServiceImpl implements PayWalletTransactionServ
 
     @Override
     public PageResult<PayWalletTransactionDO> getWalletTransactionPage(PayWalletTransactionPageReqVO pageVO) {
+        // 基于 userId + userType 查询钱包
+        if (pageVO.getWalletId() == null
+            && ObjectUtil.isAllNotEmpty(pageVO.getUserId(), pageVO.getUserType())) {
+            PayWalletDO wallet = payWalletService.getOrCreateWallet(pageVO.getUserId(), pageVO.getUserType());
+            if (wallet != null) {
+                pageVO.setWalletId(wallet.getId());
+            }
+        }
+
+        // 查询分页
         return payWalletTransactionMapper.selectPage(pageVO.getWalletId(), null, pageVO, null);
     }
 

+ 6 - 3
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java

@@ -218,11 +218,11 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
     public void testCreateOrder_success() {
         // mock 参数
         PayOrderCreateReqDTO reqDTO = randomPojo(PayOrderCreateReqDTO.class,
-                o -> o.setAppId(1L).setMerchantOrderId("10")
+                o -> o.setAppKey("demo").setMerchantOrderId("10")
                         .setSubject(randomString()).setBody(randomString()));
         // mock 方法
         PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L).setOrderNotifyUrl("http://127.0.0.1"));
-        when(appService.validPayApp(eq(reqDTO.getAppId()))).thenReturn(app);
+        when(appService.validPayApp(eq(reqDTO.getAppKey()))).thenReturn(app);
 
         // 调用
         Long orderId = orderService.createOrder(reqDTO);
@@ -239,10 +239,13 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
     public void testCreateOrder_exists() {
         // mock 参数
         PayOrderCreateReqDTO reqDTO = randomPojo(PayOrderCreateReqDTO.class,
-                o -> o.setAppId(1L).setMerchantOrderId("10"));
+                o -> o.setAppKey("demo").setMerchantOrderId("10"));
         // mock 数据
         PayOrderDO dbOrder = randomPojo(PayOrderDO.class,  o -> o.setAppId(1L).setMerchantOrderId("10"));
         orderMapper.insert(dbOrder);
+        // mock 方法
+        PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L).setOrderNotifyUrl("http://127.0.0.1"));
+        when(appService.validPayApp(eq(reqDTO.getAppKey()))).thenReturn(app);
 
         // 调用
         Long orderId = orderService.createOrder(reqDTO);

+ 16 - 16
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java

@@ -209,10 +209,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
     @Test
     public void testCreateRefund_orderNotFound() {
         PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
-                o -> o.setAppId(1L));
+                o -> o.setAppKey("demo"));
         // mock 方法(app)
         PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
-        when(appService.validPayApp(eq(1L))).thenReturn(app);
+        when(appService.validPayApp(eq("demo"))).thenReturn(app);
 
         // 调用,并断言异常
         assertServiceException(() -> refundService.createPayRefund(reqDTO),
@@ -232,10 +232,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
     private void testCreateRefund_orderWaitingOrClosed(Integer status) {
         // 准备参数
         PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
-                o -> o.setAppId(1L).setMerchantOrderId("100"));
+                o -> o.setAppKey("demo").setMerchantOrderId("100"));
         // mock 方法(app)
         PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
-        when(appService.validPayApp(eq(1L))).thenReturn(app);
+        when(appService.validPayApp(eq("demo"))).thenReturn(app);
         // mock 数据(order)
         PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(status));
         when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order);
@@ -249,10 +249,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
     public void testCreateRefund_refundPriceExceed() {
         // 准备参数
         PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
-                o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(10));
+                o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(10));
         // mock 方法(app)
         PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
-        when(appService.validPayApp(eq(1L))).thenReturn(app);
+        when(appService.validPayApp(eq("demo"))).thenReturn(app);
         // mock 数据(order)
         PayOrderDO order = randomPojo(PayOrderDO.class, o ->
                 o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
@@ -268,10 +268,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
     public void testCreateRefund_orderHasRefunding() {
         // 准备参数
         PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
-                o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(10));
+                o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(10));
         // mock 方法(app)
         PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
-        when(appService.validPayApp(eq(1L))).thenReturn(app);
+        when(appService.validPayApp(eq("demo"))).thenReturn(app);
         // mock 数据(order)
         PayOrderDO order = randomPojo(PayOrderDO.class, o ->
                 o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
@@ -291,10 +291,10 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
     public void testCreateRefund_channelNotFound() {
         // 准备参数
         PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
-                o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9));
+                o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9));
         // mock 方法(app)
         PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
-        when(appService.validPayApp(eq(1L))).thenReturn(app);
+        when(appService.validPayApp(eq("demo"))).thenReturn(app);
         // mock 数据(order)
         PayOrderDO order = randomPojo(PayOrderDO.class, o ->
                 o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
@@ -315,11 +315,11 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
     public void testCreateRefund_refundExists() {
         // 准备参数
         PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
-                o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)
+                o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9)
                         .setMerchantRefundId("200").setReason("测试退款"));
         // mock 方法(app)
         PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
-        when(appService.validPayApp(eq(1L))).thenReturn(app);
+        when(appService.validPayApp(eq("demo"))).thenReturn(app);
         // mock 数据(order)
         PayOrderDO order = randomPojo(PayOrderDO.class, o ->
                 o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
@@ -347,11 +347,11 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
     public void testCreateRefund_invokeException() {
         // 准备参数
         PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
-                o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)
+                o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9)
                         .setMerchantRefundId("200").setReason("测试退款"));
         // mock 方法(app)
         PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
-        when(appService.validPayApp(eq(1L))).thenReturn(app);
+        when(appService.validPayApp(eq("demo"))).thenReturn(app);
         // mock 数据(order)
         PayOrderDO order = randomPojo(PayOrderDO.class, o ->
                 o.setStatus(PayOrderStatusEnum.REFUND.getStatus())
@@ -391,11 +391,11 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
 
             // 准备参数
             PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class,
-                    o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)
+                    o -> o.setAppKey("demo").setMerchantOrderId("100").setPrice(9)
                             .setMerchantRefundId("200").setReason("测试退款"));
             // mock 方法(app)
             PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L));
-            when(appService.validPayApp(eq(1L))).thenReturn(app);
+            when(appService.validPayApp(eq("demo"))).thenReturn(app);
             // mock 数据(order)
             PayOrderDO order = randomPojo(PayOrderDO.class, o ->
                     o.setStatus(PayOrderStatusEnum.REFUND.getStatus())

+ 1 - 0
yudao-module-pay/yudao-module-pay-biz/src/test/resources/sql/create_tables.sql

@@ -1,5 +1,6 @@
 CREATE TABLE IF NOT EXISTS "pay_app" (
     "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "app_key"              varchar(64)   NOT NULL,
     "name"              varchar(64)   NOT NULL,
     "status"            tinyint       NOT NULL,
     "remark"            varchar(255)           DEFAULT NULL,

+ 21 - 0
yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java

@@ -28,6 +28,11 @@ public class AlipayPayClientConfig implements PayClientConfig {
      */
     public static final Integer MODE_CERTIFICATE = 2;
 
+    /**
+     * 接口内容加密方式 - AES 加密
+     */
+    public static final String ENC_TYPE_AES = "AES";
+
     /**
      * 签名算法类型 - RSA
      */
@@ -94,6 +99,22 @@ public class AlipayPayClientConfig implements PayClientConfig {
     @NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class})
     private String rootCertContent;
 
+    /**
+     * 接口内容加密方式
+     *
+     * 1. 如果为空,将使用无加密方式
+     * 2. 如果要加密,目前支付宝只有 AES 一种加密方式
+     *
+     * @see <a href="https://opendocs.alipay.com/common/02mse3">支付宝开放平台</a>
+     * @see AlipayPayClientConfig#ENC_TYPE_AES
+     */
+    private String encryptType;
+
+    /**
+     * 接口内容加密的私钥
+     */
+    private String encryptKey;
+
     public interface ModePublicKey {
     }
 

+ 0 - 3
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java

@@ -102,8 +102,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
         queryParam.put("TemplateCode", apiTemplateId);
         JSONObject response = request("QuerySmsTemplate", queryParam);
 
-        System.out.println("getSmsTemplate response is =====" + response.toString());
-
         // 2.1 请求失败
         String code = response.getStr("Code");
         if (ObjectUtil.notEqual(code, RESPONSE_CODE_SUCCESS)) {
@@ -170,7 +168,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
         // 4. 构建 Authorization 签名
         String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
         String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest);
-
         String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest;
         String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
         headers.put("Authorization", "ACS3-HMAC-SHA256" + " " + "Credential=" + properties.getApiKey()

+ 2 - 4
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
 
-
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
 
@@ -31,13 +30,12 @@ import java.util.*;
 
 import java.time.LocalDateTime;
 
-
 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
 
-
+// todo @scholar:参考阿里云在优化下
 /**
  * 华为短信客户端的实现类
  *
@@ -56,7 +54,6 @@ public class HuaweiSmsClient extends AbstractSmsClient {
 
     @Override
     protected void doInit() {
-
     }
 
     public HuaweiSmsClient(SmsChannelProperties properties) {
@@ -68,6 +65,7 @@ public class HuaweiSmsClient extends AbstractSmsClient {
     @Override
     public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
                                   List<KeyValue<String, Object>> templateParams) throws Throwable {
+        // 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html
         // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构
         // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。
         String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号

+ 88 - 216
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java

@@ -2,38 +2,29 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
 
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
-import cn.hutool.http.HttpRequest;
-import cn.hutool.http.HttpResponse;
 import cn.hutool.json.JSONArray;
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
-import com.fasterxml.jackson.annotation.JsonFormat;
-import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.annotations.VisibleForTesting;
-import lombok.Data;
+import jakarta.xml.bind.DatatypeConverter;
 
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
-import javax.xml.bind.DatatypeConverter;
 import java.nio.charset.StandardCharsets;
 import java.text.SimpleDateFormat;
-import java.time.LocalDateTime;
 import java.util.*;
 
 import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
 
-// TODO @scholar 建议参考 AliyunSmsClient 优化下
 /**
  * 腾讯云短信功能实现
  *
@@ -43,6 +34,9 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
  */
 public class TencentSmsClient extends AbstractSmsClient {
 
+    private static final String VERSION = "2021-01-11";
+    private static final String REGION = "ap-guangzhou";
+
     /**
      * 调用成功 code
      */
@@ -56,7 +50,6 @@ public class TencentSmsClient extends AbstractSmsClient {
      */
     private static final long INTERNATIONAL_CHINA = 0L;
 
-
     public TencentSmsClient(SmsChannelProperties properties) {
         super(properties);
         Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
@@ -65,7 +58,6 @@ public class TencentSmsClient extends AbstractSmsClient {
 
     @Override
     protected void doInit() {
-
     }
 
     /**
@@ -95,31 +87,96 @@ public class TencentSmsClient extends AbstractSmsClient {
     @Override
     public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
                                   String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
-        // 构建请求
+        // 1. 执行请求
+        // 参考链接 https://cloud.tencent.com/document/product/382/55981
         TreeMap<String, Object> body = new TreeMap<>();
-        String[] phones = {mobile};
-        body.put("PhoneNumberSet",phones);
-        body.put("SmsSdkAppId",getSdkAppId());
-        body.put("SignName",properties.getSignature());
+        body.put("PhoneNumberSet", new String[]{mobile});
+        body.put("SmsSdkAppId", getSdkAppId());
+        body.put("SignName", properties.getSignature());
         body.put("TemplateId",apiTemplateId);
-        body.put("TemplateParamSet",ArrayUtils.toArray(templateParams, e -> String.valueOf(e.getValue())));
+        body.put("TemplateParamSet", ArrayUtils.toArray(templateParams, param -> String.valueOf(param.getValue())));
+        JSONObject response = request("SendSms", body);
+
+        // 2. 解析请求
+        JSONObject responseResult = response.getJSONObject("Response");
+        JSONObject error = responseResult.getJSONObject("Error");
+        if (error != null) {
+            return new SmsSendRespDTO().setSuccess(false)
+                    .setApiRequestId(responseResult.getStr("RequestId"))
+                    .setApiCode(error.getStr("Code"))
+                    .setApiMsg(error.getStr("Message"));
+        }
+        JSONObject responseData = responseResult.getJSONArray("SendStatusSet").getJSONObject(0);
+        return new SmsSendRespDTO().setSuccess(Objects.equals(API_CODE_SUCCESS, responseData.getStr("Code")))
+                .setApiRequestId(responseResult.getStr("RequestId"))
+                .setSerialNo(responseData.getStr("SerialNo"))
+                .setApiMsg(responseData.getStr("Message"));
+    }
+
+    @Override
+    public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
+        JSONArray statuses = JSONUtil.parseArray(text);
+        // 字段参考
+        return convertList(statuses, status -> {
+            JSONObject statusObj = (JSONObject) status;
+            return new SmsReceiveRespDTO()
+                    .setSuccess("SUCCESS".equals(statusObj.getStr("report_status"))) // 是否接收成功
+                    .setErrorCode(statusObj.getStr("errmsg")) // 状态报告编码
+                    .setMobile(statusObj.getStr("mobile")) // 手机号
+                    .setReceiveTime(statusObj.getLocalDateTime("user_receive_time", null)) // 状态报告时间
+                    .setSerialNo(statusObj.getStr("sid")); // 发送序列号
+        });
+    }
 
-        JSONObject JsonResponse = sendSmsRequest(body,"SendSms","2021-01-11","ap-guangzhou");
-        SmsResponse smsResponse = getSmsSendResponse(JsonResponse);
+    @Override
+    public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
+        // 1. 构建请求
+        // 参考链接 https://cloud.tencent.com/document/product/382/52067
+        TreeMap<String, Object> body = new TreeMap<>();
+        body.put("International", INTERNATIONAL_CHINA);
+        body.put("TemplateIdSet", new Integer[]{Integer.valueOf(apiTemplateId)});
+        JSONObject response = request("DescribeSmsTemplateList", body);
 
-        return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString());
+        // TODO @scholar:会有请求失败的情况么?类似发送的(那块逻辑我补充了)
+        JSONObject TemplateStatusSet = response.getJSONObject("Response").getJSONArray("DescribeTemplateStatusSet").getJSONObject(0);
+        String content = TemplateStatusSet.get("TemplateContent").toString();
+        int templateStatus = Integer.parseInt(TemplateStatusSet.get("StatusCode").toString());
+        String auditReason = TemplateStatusSet.get("ReviewReply").toString();
 
+        return new SmsTemplateRespDTO().setId(apiTemplateId).setContent(content)
+                .setAuditStatus(convertSmsTemplateAuditStatus(templateStatus)).setAuditReason(auditReason);
     }
 
-    JSONObject sendSmsRequest(TreeMap<String, Object> body,String action,String version,String region) throws Exception {
+    @VisibleForTesting
+    Integer convertSmsTemplateAuditStatus(int templateStatus) {
+        switch (templateStatus) {
+            case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
+            case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
+            case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
+            default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
+        }
+    }
 
+    /**
+     * 请求腾讯云短信
+     *
+     * @see <a href="https://cloud.tencent.com/document/product/382/52072">签名方法 v3</a>
+     *
+     * @param action 请求的 API 名称
+     * @param body 请求参数
+     * @return 请求结果
+     */
+    private JSONObject request(String action, TreeMap<String, Object> body) throws Exception {
         String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
+        // TODO @scholar:这个 format,看看怎么写的可以简化点
         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
         // 注意时区,否则容易出错
         sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
         String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
 
+        // TODO @scholar:这个步骤,看看怎么参考阿里云 client,归类下;1. 2.1 2.2 这种
         // ************* 步骤 1:拼接规范请求串 *************
+        // TODO @scholar:这个 hsot 枚举下;
         String host = "sms.tencentcloudapi.com"; //APP接入地址+接口访问URI
         String httpMethod = "POST"; // 请求方式
         String canonicalUri = "/";
@@ -129,6 +186,7 @@ public class TencentSmsClient extends AbstractSmsClient {
                 + "host:" + host + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";
         String signedHeaders = "content-type;host;x-tc-action";
         String hashedRequestBody = sha256Hex(JSONUtil.toJsonStr(body));
+        // TODO @scholar:换行下,不然单行太长了
         String canonicalRequest = httpMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
 
         // ************* 步骤 2:拼接待签名字符串 *************
@@ -153,205 +211,19 @@ public class TencentSmsClient extends AbstractSmsClient {
         headers.put("Host", host);
         headers.put("X-TC-Action", action);
         headers.put("X-TC-Timestamp", timestamp);
-        headers.put("X-TC-Version", version);
-        headers.put("X-TC-Region", region);
+        headers.put("X-TC-Version", VERSION);
+        headers.put("X-TC-Region", REGION);
 
-        HttpResponse response = HttpRequest.post("https://"+host)
-                .addHeaders(headers)
-                .body(JSONUtil.toJsonStr(body))
-                .execute();
+        String responseBody = HttpUtils.post("https://" + host, headers, JSONUtil.toJsonStr(body));
 
-        return JSONUtil.parseObj(response.body());
+        return JSONUtil.parseObj(responseBody);
     }
 
-    public static byte[] hmac256(byte[] key, String msg) throws Exception {
+    // TODO @scholar:使用 hutool 简化下
+    private static byte[] hmac256(byte[] key, String msg) throws Exception {
         Mac mac = Mac.getInstance("HmacSHA256");
         SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
         mac.init(secretKeySpec);
         return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8));
     }
-
-    private SmsResponse getSmsSendResponse(JSONObject resJson) {
-        SmsResponse smsResponse = new SmsResponse();
-        JSONArray statusJson =resJson.getJSONObject("Response").getJSONArray("SendStatusSet");
-        smsResponse.setSuccess("Ok".equals(statusJson.getJSONObject(0).getStr("Code")));
-        smsResponse.setData(resJson);
-        return smsResponse;
-    }
-
-    @Override
-    public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
-        List<SmsReceiveStatus> callback = JsonUtils.parseArray(text, SmsReceiveStatus.class);
-        return convertList(callback, status -> new SmsReceiveRespDTO()
-                .setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus()))
-                .setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription())
-                .setMobile(status.getMobile()).setReceiveTime(status.getReceiveTime())
-                .setSerialNo(status.getSerialNo()).setLogId(status.getSessionContext().getLogId()));
-    }
-
-    @Override
-    public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
-
-        // 构建请求
-        TreeMap<String, Object> body = new TreeMap<>();
-        body.put("International",0);
-        Integer[] templateIds = {Integer.valueOf(apiTemplateId)};
-        body.put("TemplateIdSet",templateIds);
-
-        JSONObject JsonResponse = sendSmsRequest(body,"DescribeSmsTemplateList","2021-01-11","ap-guangzhou");
-        QuerySmsTemplateResponse smsTemplateResponse = getSmsTemplateResponse(JsonResponse);
-        String templateId = Integer.toString(smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getTemplateId());
-        String content = smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getTemplateContent();
-        Integer templateStatus = smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getStatusCode();
-        String auditReason = smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getReviewReply();
-
-        return new SmsTemplateRespDTO().setId(templateId).setContent(content)
-                .setAuditStatus(convertSmsTemplateAuditStatus(templateStatus)).setAuditReason(auditReason);
-    }
-
-    private QuerySmsTemplateResponse getSmsTemplateResponse(JSONObject resJson) {
-
-        QuerySmsTemplateResponse smsTemplateResponse = new QuerySmsTemplateResponse();
-
-        smsTemplateResponse.setRequestId(resJson.getJSONObject("Response").getStr("RequestId"));
-
-        smsTemplateResponse.setDescribeTemplateStatusSet(new ArrayList<>());
-
-        QuerySmsTemplateResponse.TemplateInfo templateInfo = new QuerySmsTemplateResponse.TemplateInfo();
-
-        Object statusObject = resJson.getJSONObject("Response").getJSONArray("DescribeTemplateStatusSet").get(0);
-
-        JSONObject statusJSON = new JSONObject(statusObject);
-
-        templateInfo.setTemplateContent(statusJSON.get("TemplateContent").toString());
-
-        templateInfo.setStatusCode(Integer.parseInt(statusJSON.get("StatusCode").toString()));
-
-        templateInfo.setReviewReply(statusJSON.get("ReviewReply").toString());
-
-        templateInfo.setTemplateId(Integer.parseInt(statusJSON.get("TemplateId").toString()));
-
-        smsTemplateResponse.getDescribeTemplateStatusSet().add(templateInfo);
-
-        return smsTemplateResponse;
-    }
-
-    @VisibleForTesting
-    Integer convertSmsTemplateAuditStatus(int templateStatus) {
-        switch (templateStatus) {
-            case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
-            case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
-            case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
-            default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
-        }
-    }
-
-    @Data
-    public static class SmsResponse {
-
-        /**
-         * 是否成功
-         */
-        private boolean success;
-
-        /**
-         * 厂商原返回体
-         */
-        private Object data;
-
-    }
-
-
-    /**
-     * <p>类名: QuerySmsTemplateResponse
-     * <p>说明:  sms模板查询返回信息
-     *
-     * @author :scholar
-     * 2024/07/17  0:25
-     **/
-    @Data
-    public static class QuerySmsTemplateResponse {
-        private List<TemplateInfo> DescribeTemplateStatusSet;
-        private String RequestId;
-        @Data
-        static class TemplateInfo {
-            private String TemplateName;
-            private Integer TemplateId;
-            private Integer International;
-            private String ReviewReply;
-            private long CreateTime;
-            private String TemplateContent;
-            private Integer StatusCode;
-        }
-    }
-
-    @Data
-    private static class SmsReceiveStatus {
-
-        /**
-         * 短信接受成功 code
-         */
-        public static final String SUCCESS_CODE = "SUCCESS";
-
-        /**
-         * 用户实际接收到短信的时间
-         */
-        @JsonProperty("user_receive_time")
-        @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
-        private LocalDateTime receiveTime;
-
-        /**
-         * 国家(或地区)码
-         */
-        @JsonProperty("nationcode")
-        private String nationCode;
-
-        /**
-         * 手机号码
-         */
-        private String mobile;
-
-        /**
-         * 实际是否收到短信接收状态,SUCCESS(成功)、FAIL(失败)
-         */
-        @JsonProperty("report_status")
-        private String status;
-
-        /**
-         * 用户接收短信状态码错误信息
-         */
-        @JsonProperty("errmsg")
-        private String errCode;
-
-        /**
-         * 用户接收短信状态描述
-         */
-        @JsonProperty("description")
-        private String description;
-
-        /**
-         * 本次发送标识 ID(与发送接口返回的SerialNo对应)
-         */
-        @JsonProperty("sid")
-        private String serialNo;
-
-        /**
-         * 用户的 session 内容(与发送接口的请求参数 SessionContext 一致)
-         */
-        @JsonProperty("ext")
-        private SessionContext sessionContext;
-
-    }
-
-    @VisibleForTesting
-    @Data
-    static class SessionContext {
-
-        /**
-         * 发送短信记录id
-         */
-        private Long logId;
-
-    }
-
-}
+}

+ 0 - 9
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java

@@ -38,15 +38,6 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
     @InjectMocks
     private final AliyunSmsClient smsClient = new AliyunSmsClient(properties);
 
-    @Test
-    public void testDoInit() {
-        // 准备参数
-        // mock 方法
-
-        // 调用
-        smsClient.doInit();
-    }
-
     @Test
     public void tesSendSms_success() throws Throwable {
         try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {

+ 61 - 22
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.java

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
 
-import cn.hutool.core.collection.ListUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
@@ -18,24 +17,6 @@ import java.util.List;
  */
 public class SmsClientTests {
 
-    @Test
-    @Disabled
-    public void testHuaweiSmsClient_sendSms() throws Throwable {
-        SmsChannelProperties properties = new SmsChannelProperties()
-                .setApiKey("123")
-                .setApiSecret("456");
-        HuaweiSmsClient client = new HuaweiSmsClient(properties);
-        // 准备参数
-        Long sendLogId = System.currentTimeMillis();
-        String mobile = "15601691323";
-        String apiTemplateId = "xx test01";
-        List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<>("code", "1024"));
-        // 调用
-        SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
-        // 打印结果
-        System.out.println(smsSendRespDTO);
-    }
-
     // ========== 阿里云 ==========
 
     @Test
@@ -59,14 +40,14 @@ public class SmsClientTests {
         SmsChannelProperties properties = new SmsChannelProperties()
                 .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")
                 .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")
-                .setSignature("Ballcat");
+                .setSignature("runpu");
         AliyunSmsClient client = new AliyunSmsClient(properties);
         // 准备参数
         Long sendLogId = System.currentTimeMillis();
-        String mobile = "173213154791";
+        String mobile = "15601691323";
         String apiTemplateId = "SMS_207945135";
         // 调用
-        SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024")));
+        SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024")));
         // 打印结果
         System.out.println(sendRespDTO);
     }
@@ -100,4 +81,62 @@ public class SmsClientTests {
         System.out.println(statuses);
     }
 
+    // ========== 腾讯云 ==========
+
+    @Test
+    @Disabled
+    public void testTencentSmsClient_sendSms() throws Throwable {
+        SmsChannelProperties properties = new SmsChannelProperties()
+                .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523")
+                .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")
+                .setSignature("芋道源码");
+        TencentSmsClient client = new TencentSmsClient(properties);
+        // 准备参数
+        Long sendLogId = System.currentTimeMillis();
+        String mobile = "15601691323";
+        String apiTemplateId = "2136358";
+        // 调用
+        SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024")));
+        // 打印结果
+        System.out.println(sendRespDTO);
+    }
+
+    @Test
+    @Disabled
+    public void testTencentSmsClient_getSmsTemplate() throws Throwable {
+        SmsChannelProperties properties = new SmsChannelProperties()
+                .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523")
+                .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")
+                .setSignature("芋道源码");
+        TencentSmsClient client = new TencentSmsClient(properties);
+        // 准备参数
+        String apiTemplateId = "2136358";
+        // 调用
+        SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId);
+        // 打印结果
+        System.out.println(template);
+    }
+
+    // ========== 华为云 ==========
+
+    @Test
+    @Disabled
+    public void testHuaweiSmsClient_sendSms() throws Throwable {
+        SmsChannelProperties properties = new SmsChannelProperties()
+                .setApiKey("123")
+                .setApiSecret("456")
+                .setSignature("runpu");
+        HuaweiSmsClient client = new HuaweiSmsClient(properties);
+        // 准备参数
+        Long sendLogId = System.currentTimeMillis();
+        String mobile = "15601691323";
+        String apiTemplateId = "xx test01";
+        List<KeyValue<String, Object>> templateParams = List.of(new KeyValue<>("code", "1024"));
+        // 调用
+        SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
+        // 打印结果
+        System.out.println(smsSendRespDTO);
+    }
+
 }
+

+ 117 - 140
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java

@@ -1,22 +1,28 @@
 package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
 
-import cn.hutool.core.util.ReflectUtil;
+import cn.iocoder.yudao.framework.common.core.KeyValue;
+import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
-import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient;
 import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
+import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
+import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
 import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
 import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
+import com.google.common.collect.Lists;
+
 import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
-import org.mockito.Mock;
+import org.mockito.MockedStatic;
 
 import java.time.LocalDateTime;
 import java.util.List;
 
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
 import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mockStatic;
 
-// TODO @芋艿:补全单测
 /**
  * {@link TencentSmsClient} 的单元测试
  *
@@ -32,115 +38,85 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
     @InjectMocks
     private TencentSmsClient smsClient = new TencentSmsClient(properties);
 
-    @Mock
-    private SmsClient client;
-
     @Test
-    public void testDoInit() {
-        // 准备参数
-        // mock 方法
-
-        // 调用
-        smsClient.doInit();
-        // 断言
-        assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client"));
+    public void testDoSendSms_success() throws Throwable {
+        try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
+            // 准备参数
+            Long sendLogId = randomLongId();
+            String mobile = randomString();
+            String apiTemplateId = randomString();
+            List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
+                    new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
+            // mock 方法
+            httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
+                    .thenReturn("{\n" +
+                                    "    \"Response\": {\n" +
+                                    "        \"SendStatusSet\": [\n" +
+                                    "            {\n" +
+                                    "                \"SerialNo\": \"5000:1045710669157053657849499619\",\n" +
+                                    "                \"PhoneNumber\": \"+8618511122233\",\n" +
+                                    "                \"Fee\": 1,\n" +
+                                    "                \"SessionContext\": \"test\",\n" +
+                                    "                \"Code\": \"Ok\",\n" +
+                                    "                \"Message\": \"send success\",\n" +
+                                    "                \"IsoCode\": \"CN\"\n" +
+                                    "            },\n" +
+                                    "        ],\n" +
+                                    "        \"RequestId\": \"a0aabda6-cf91-4f3e-a81f-9198114a2279\"\n" +
+                                    "    }\n" +
+                                    "}");
+
+            // 调用
+            SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
+                    apiTemplateId, templateParams);
+            // 断言
+            assertTrue(result.getSuccess());
+            assertEquals("5000:1045710669157053657849499619", result.getSerialNo());
+            assertEquals("a0aabda6-cf91-4f3e-a81f-9198114a2279", result.getApiRequestId());
+            assertEquals("send success", result.getApiMsg());
+        }
     }
 
     @Test
-    public void testRefresh() {
-        // 准备参数
-        SmsChannelProperties p = new SmsChannelProperties()
-                .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错
-                .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错
-                .setSignature("芋道源码");
-        // 调用
-        smsClient.refresh(p);
-        // 断言
-        assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client"));
+    public void testDoSendSms_fail() throws Throwable {
+        try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
+            // 准备参数
+            Long sendLogId = randomLongId();
+            String mobile = randomString();
+            String apiTemplateId = randomString();
+            List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
+                    new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
+
+            // mock 方法
+            httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
+                    .thenReturn("{\n" +
+                                    "    \"Response\": {\n" +
+                                    "        \"SendStatusSet\": [\n" +
+                                    "            {\n" +
+                                    "                \"SerialNo\": \"5000:1045710669157053657849499619\",\n" +
+                                    "                \"PhoneNumber\": \"+8618511122233\",\n" +
+                                    "                \"Fee\": 1,\n" +
+                                    "                \"SessionContext\": \"test\",\n" +
+                                    "                \"Code\": \"ERROR\",\n" +
+                                    "                \"Message\": \"send success\",\n" +
+                                    "                \"IsoCode\": \"CN\"\n" +
+                                    "            },\n" +
+                                    "        ],\n" +
+                                    "        \"RequestId\": \"a0aabda6-cf91-4f3e-a81f-9198114a2279\"\n" +
+                                    "    }\n" +
+                                    "}");
+
+            // 调用
+            SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
+                    apiTemplateId, templateParams);
+            // 断言
+            assertFalse(result.getSuccess());
+            assertEquals("5000:1045710669157053657849499619", result.getSerialNo());
+            assertEquals("a0aabda6-cf91-4f3e-a81f-9198114a2279", result.getApiRequestId());
+            assertEquals("send success", result.getApiMsg());
+        }
     }
 
-//    @Test
-//    public void testDoSendSms_success() throws Throwable {
-//        // 准备参数
-//        Long sendLogId = randomLongId();
-//        String mobile = randomString();
-//        String apiTemplateId = randomString();
-//        List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
-//                new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
-//        String requestId = randomString();
-//        String serialNo = randomString();
-//        // mock 方法
-//        SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> {
-//            o.setRequestId(requestId);
-//            SendStatus[] sendStatuses = new SendStatus[1];
-//            o.setSendStatusSet(sendStatuses);
-//            SendStatus sendStatus = new SendStatus();
-//            sendStatuses[0] = sendStatus;
-//            sendStatus.setCode(TencentSmsClient.API_CODE_SUCCESS);
-//            sendStatus.setMessage("send success");
-//            sendStatus.setSerialNo(serialNo);
-//        });
-//        when(client.SendSms(argThat(request -> {
-//            assertEquals(mobile, request.getPhoneNumberSet()[0]);
-//            assertEquals(properties.getSignature(), request.getSignName());
-//            assertEquals(apiTemplateId, request.getTemplateId());
-//            assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)),
-//                    toJsonString(request.getTemplateParamSet()));
-//            assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId"));
-//            return true;
-//        }))).thenReturn(response);
-//
-//        // 调用
-//        SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
-//        // 断言
-//        assertTrue(result.getSuccess());
-//        assertEquals(response.getRequestId(), result.getApiRequestId());
-//        assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode());
-//        assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg());
-//        assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo());
-//    }
-
-//    @Test
-//    public void testDoSendSms_fail() throws Throwable {
-//        // 准备参数
-//        Long sendLogId = randomLongId();
-//        String mobile = randomString();
-//        String apiTemplateId = randomString();
-//        List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
-//                new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
-//        String requestId = randomString();
-//        String serialNo = randomString();
-//        // mock 方法
-//        SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> {
-//            o.setRequestId(requestId);
-//            SendStatus[] sendStatuses = new SendStatus[1];
-//            o.setSendStatusSet(sendStatuses);
-//            SendStatus sendStatus = new SendStatus();
-//            sendStatuses[0] = sendStatus;
-//            sendStatus.setCode("ERROR");
-//            sendStatus.setMessage("send success");
-//            sendStatus.setSerialNo(serialNo);
-//        });
-//        when(client.SendSms(argThat(request -> {
-//            assertEquals(mobile, request.getPhoneNumberSet()[0]);
-//            assertEquals(properties.getSignature(), request.getSignName());
-//            assertEquals(apiTemplateId, request.getTemplateId());
-//            assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)),
-//                    toJsonString(request.getTemplateParamSet()));
-//            assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId"));
-//            return true;
-//        }))).thenReturn(response);
-//
-//        // 调用
-//        SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
-//        // 断言
-//        assertFalse(result.getSuccess());
-//        assertEquals(response.getRequestId(), result.getApiRequestId());
-//        assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode());
-//        assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg());
-//        assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo());
-//    }
-
     @Test
     public void testParseSmsReceiveStatus() {
         // 准备参数
@@ -156,7 +132,6 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
                 "        \"ext\": {\"logId\":\"67890\"}\n" +
                 "    }\n" +
                 "]";
-        // mock 方法
 
         // 调用
         List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
@@ -164,42 +139,44 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
         assertEquals(1, statuses.size());
         assertTrue(statuses.get(0).getSuccess());
         assertEquals("DELIVRD", statuses.get(0).getErrorCode());
-        assertEquals("用户短信送达成功", statuses.get(0).getErrorMsg());
         assertEquals("13900000001", statuses.get(0).getMobile());
         assertEquals(LocalDateTime.of(2015, 10, 17, 8, 3, 4), statuses.get(0).getReceiveTime());
         assertEquals("12345", statuses.get(0).getSerialNo());
-        assertEquals(67890L, statuses.get(0).getLogId());
     }
 
-//    @Test
-//    public void testGetSmsTemplate() throws Throwable {
-//        // 准备参数
-//        Long apiTemplateId = randomLongId();
-//        String requestId = randomString();
-//
-//        // mock 方法
-//        DescribeSmsTemplateListResponse response = randomPojo(DescribeSmsTemplateListResponse.class, o -> {
-//            DescribeTemplateListStatus[] describeTemplateListStatuses = new DescribeTemplateListStatus[1];
-//            DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus();
-//            templateStatus.setTemplateId(apiTemplateId);
-//            templateStatus.setStatusCode(0L);// 设置模板通过
-//            describeTemplateListStatuses[0] = templateStatus;
-//            o.setDescribeTemplateStatusSet(describeTemplateListStatuses);
-//            o.setRequestId(requestId);
-//        });
-//        when(client.DescribeSmsTemplateList(argThat(request -> {
-//            assertEquals(apiTemplateId, request.getTemplateIdSet()[0]);
-//            return true;
-//        }))).thenReturn(response);
-//
-//        // 调用
-//        SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId.toString());
-//        // 断言
-//        assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getId());
-//        assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getContent());
-//        assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
-//        assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getAuditReason());
-//    }
+    @Test
+    public void testGetSmsTemplate() throws Throwable {
+        try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
+            // 准备参数
+            String apiTemplateId = "1122";
+
+            // mock 方法
+            httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
+                    .thenReturn("{     \"Response\": {\n" +
+                            "        \"DescribeTemplateStatusSet\": [\n" +
+                            "            {\n" +
+                            "                \"TemplateName\": \"验证码\",\n" +
+                            "                \"TemplateId\": 1122,\n" +
+                            "                \"International\": 0,\n" +
+                            "                \"ReviewReply\": \"审批备注\",\n" +
+                            "                \"CreateTime\": 1617379200,\n" +
+                            "                \"TemplateContent\": \"您的验证码是{1}\",\n" +
+                            "                \"StatusCode\": 0\n" +
+                            "            },\n" +
+                            "            \n" +
+                            "        ],\n" +
+                            "        \"RequestId\": \"f36e4f00-605e-49b1-ad0d-bfaba81c7325\"\n" +
+                            "    }}");
+
+            // 调用
+            SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId);
+            // 断言
+            assertEquals("1122", result.getId());
+            assertEquals("您的验证码是{1}", result.getContent());
+            assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
+            assertEquals("审批备注", result.getAuditReason());
+        }
+    }
 
     @Test
     public void testConvertSmsTemplateAuditStatus() {

+ 9 - 2
yudao-server/src/main/resources/application.yaml

@@ -147,14 +147,22 @@ spring:
 
 spring:
   ai:
+    vectorstore: # 向量存储
+      redis:
+        index: default-index
+        prefix: "default:"
     qianfan: # 文心一言
       api-key: x0cuLZ7XsaTCU08vuJWO87Lg
       secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK
     zhipuai: # 智谱 AI
       api-key: 32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs
-    openai:
+    openai: # OpenAI 官方
       api-key: sk-yzKea6d8e8212c3bdd99f9f44ced1cae37c097e5aa3BTS7z
       base-url: https://api.gptsapi.net
+    azure: # OpenAI 微软
+      openai:
+        endpoint: https://eastusprejade.openai.azure.com
+        api-key: xxx
     ollama:
       base-url: http://127.0.0.1:11434
       chat:
@@ -301,7 +309,6 @@ yudao:
     end-code: 9999 # 这里配置 9999 的原因是,测试方便。
   trade:
     order:
-      app-id: 1 # 商户编号
       pay-expire-time: 2h # 支付的过期时间
       receive-expire-time: 14d # 收货的过期时间
       comment-expire-time: 7d # 评论的过期时间