瀏覽代碼

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

# Conflicts:
#	yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/ProductBrowseHistoryController.java
#	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/AppSocialUserController.java
#	yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java
#	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java
#	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.java
#	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
YunaiV 8 月之前
父節點
當前提交
f3a8d6f06c
共有 37 個文件被更改,包括 588 次插入80 次删除
  1. 2 1
      yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/Area.java
  2. 4 0
      yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/RandomUtils.java
  3. 1 2
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java
  4. 3 2
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java
  5. 8 5
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java
  6. 1 2
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java
  7. 2 3
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java
  8. 1 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java
  9. 1 1
      yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java
  10. 20 3
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/ProductBrowseHistoryController.java
  11. 13 13
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java
  12. 12 0
      yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java
  13. 14 0
      yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/MessageTemplateConstants.java
  14. 27 2
      yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationRecordServiceImpl.java
  15. 9 4
      yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/MessageTemplateConstants.java
  16. 2 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java
  17. 2 2
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java
  18. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/message/TradeMessageServiceImpl.java
  19. 26 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  20. 3 0
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationOrderHandler.java
  21. 12 10
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/AppSocialUserController.java
  22. 27 0
      yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/vo/AppSocialWxSubscribeTemplateRespVO.java
  23. 14 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/MessageTemplateConstants.java
  24. 39 3
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java
  25. 23 7
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java
  26. 61 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeMessageSendReqDTO.java
  27. 42 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeTemplateRespDTO.java
  28. 4 2
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
  29. 51 4
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java
  30. 20 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.http
  31. 13 3
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.java
  32. 9 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java
  33. 23 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java
  34. 69 2
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java
  35. 14 4
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
  36. 8 0
      yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java
  37. 7 2
      yudao-server/src/main/resources/application-local.yaml

+ 2 - 1
yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/Area.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.framework.ip.core;
 
 import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
+import com.fasterxml.jackson.annotation.JsonBackReference;
 import com.fasterxml.jackson.annotation.JsonManagedReference;
 import lombok.AllArgsConstructor;
 import lombok.Data;
@@ -54,7 +55,7 @@ public class Area {
     /**
      * 子节点
      */
-    @JsonManagedReference
+    @JsonBackReference
     private List<Area> children;
 
 }

+ 4 - 0
yudao-framework/yudao-spring-boot-starter-test/src/main/java/cn/iocoder/yudao/framework/test/core/util/RandomUtils.java

@@ -106,6 +106,10 @@ public class RandomUtils {
         return randomString() + "@qq.com";
     }
 
+    public static String randomMobile() {
+        return "13800138" + RandomUtil.randomNumbers(3);
+    }
+
     public static String randomURL() {
         return "https://www.iocoder.cn/" + randomString();
     }

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

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.ai.controller.admin.image;
 import cn.hutool.core.util.ObjUtil;
 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.PageParam;
 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;
@@ -41,7 +40,7 @@ public class AiImageController {
 
     @GetMapping("/my-page")
     @Operation(summary = "获取【我的】绘图分页")
-    public CommonResult<PageResult<AiImageRespVO>> getImagePageMy(@Validated PageParam pageReqVO) {
+    public CommonResult<PageResult<AiImageRespVO>> getImagePageMy(@Validated AiImagePageReqVO pageReqVO) {
         PageResult<AiImageDO> pageResult = imageService.getImagePageMy(getLoginUserId(), pageReqVO);
         return success(BeanUtils.toBean(pageResult, AiImageRespVO.class));
     }

+ 3 - 2
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java

@@ -3,8 +3,6 @@ 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;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
 import org.springframework.format.annotation.DateTimeFormat;
 
 import java.time.LocalDateTime;
@@ -21,6 +19,9 @@ public class AiImagePageReqVO extends PageParam {
     @Schema(description = "平台", example = "OpenAI")
     private String platform;
 
+    @Schema(description = "提示词", example = "1")
+    private String prompt;
+
     @Schema(description = "绘画状态", example = "1")
     private Integer status;
 

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

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.ai.dal.mysql.image;
 
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
 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;
@@ -19,7 +18,7 @@ import java.util.List;
 public interface AiImageMapper extends BaseMapperX<AiImageDO> {
 
     default AiImageDO selectByTaskId(String taskId) {
-        return this.selectOne(AiImageDO::getTaskId, taskId);
+        return selectOne(AiImageDO::getTaskId, taskId);
     }
 
     default PageResult<AiImageDO> selectPage(AiImagePageReqVO reqVO) {
@@ -32,9 +31,13 @@ public interface AiImageMapper extends BaseMapperX<AiImageDO> {
                 .orderByDesc(AiImageDO::getId));
     }
 
-    default PageResult<AiImageDO> selectPage(Long userId, PageParam pageReqVO) {
-        return selectPage(pageReqVO, new LambdaQueryWrapperX<AiImageDO>()
-                .eq(AiImageDO::getUserId, userId)
+    default PageResult<AiImageDO> selectPageMy(Long userId, AiImagePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<AiImageDO>()
+                .likeIfPresent(AiImageDO::getPrompt, reqVO.getPrompt())
+                // 情况一:公开
+                .eq(Boolean.TRUE.equals(reqVO.getPublicStatus()), AiImageDO::getPublicStatus, reqVO.getPublicStatus())
+                // 情况二:私有
+                .eq(Boolean.FALSE.equals(reqVO.getPublicStatus()), AiImageDO::getUserId, userId)
                 .orderByDesc(AiImageDO::getId));
     }
 

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

@@ -1,7 +1,6 @@
 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.PageParam;
 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;
@@ -27,7 +26,7 @@ public interface AiImageService {
      * @param pageReqVO 分页条件
      * @return 绘图分页
      */
-    PageResult<AiImageDO> getImagePageMy(Long userId, PageParam pageReqVO);
+    PageResult<AiImageDO> getImagePageMy(Long userId, AiImagePageReqVO pageReqVO);
 
     /**
      * 获得绘图记录

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

@@ -10,7 +10,6 @@ import cn.hutool.extra.spring.SpringUtil;
 import cn.hutool.http.HttpUtil;
 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.PageParam;
 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;
@@ -67,8 +66,8 @@ public class AiImageServiceImpl implements AiImageService {
     private AiApiKeyService apiKeyService;
 
     @Override
-    public PageResult<AiImageDO> getImagePageMy(Long userId, PageParam pageReqVO) {
-        return imageMapper.selectPage(userId, pageReqVO);
+    public PageResult<AiImageDO> getImagePageMy(Long userId, AiImagePageReqVO pageReqVO) {
+        return imageMapper.selectPageMy(userId, pageReqVO);
     }
 
     @Override

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

@@ -124,7 +124,7 @@ public class AiMindMapServiceImpl implements AiMindMapService {
         if (role != null && role.getModelId() != null) {
             model = chatModalService.getChatModel(role.getModelId());
         }
-        if (model != null) {
+        if (model == null) {
             model = chatModalService.getRequiredDefaultChatModel();
         }
         Assert.notNull(model, "[AI] 获取不到模型");

+ 1 - 1
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java

@@ -114,7 +114,7 @@ public class AiWriteServiceImpl implements AiWriteService {
         if (Objects.nonNull(writeRole) && Objects.nonNull(writeRole.getModelId())) {
             model = chatModalService.getChatModel(writeRole.getModelId());
         }
-        if (Objects.isNull(model)) {
+        if (model == null) {
             model = chatModalService.getRequiredDefaultChatModel();
         }
         Assert.notNull(model, "[AI] 获取不到模型");

+ 20 - 3
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/ProductBrowseHistoryController.java

@@ -1,24 +1,30 @@
 package cn.iocoder.yudao.module.product.controller.admin.history;
 
+import cn.hutool.core.collection.CollUtil;
 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.product.controller.admin.history.vo.ProductBrowseHistoryPageReqVO;
 import cn.iocoder.yudao.module.product.controller.admin.history.vo.ProductBrowseHistoryRespVO;
 import cn.iocoder.yudao.module.product.dal.dataobject.history.ProductBrowseHistoryDO;
+import cn.iocoder.yudao.module.product.dal.dataobject.spu.ProductSpuDO;
 import cn.iocoder.yudao.module.product.service.history.ProductBrowseHistoryService;
+import cn.iocoder.yudao.module.product.service.spu.ProductSpuService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
 import org.springframework.security.access.prepost.PreAuthorize;
 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.RestController;
 
-import javax.annotation.Resource;
-import javax.validation.Valid;
+import java.util.Map;
+import java.util.Optional;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 
 @Tag(name = "管理后台 - 商品浏览记录")
 @RestController
@@ -28,13 +34,24 @@ public class ProductBrowseHistoryController {
 
     @Resource
     private ProductBrowseHistoryService browseHistoryService;
+    @Resource
+    private ProductSpuService productSpuService;
 
     @GetMapping("/page")
     @Operation(summary = "获得商品浏览记录分页")
     @PreAuthorize("@ss.hasPermission('product:browse-history:query')")
     public CommonResult<PageResult<ProductBrowseHistoryRespVO>> getBrowseHistoryPage(@Valid ProductBrowseHistoryPageReqVO pageReqVO) {
         PageResult<ProductBrowseHistoryDO> pageResult = browseHistoryService.getBrowseHistoryPage(pageReqVO);
-        return success(BeanUtils.toBean(pageResult, ProductBrowseHistoryRespVO.class));
+        if (CollUtil.isEmpty(pageResult.getList())) {
+            return success(PageResult.empty());
+        }
+
+        // 得到商品 spu 信息
+        Map<Long, ProductSpuDO> spuMap = productSpuService.getSpuMap(
+                convertSet(pageResult.getList(), ProductBrowseHistoryDO::getSpuId));
+        return success(BeanUtils.toBean(pageResult, ProductBrowseHistoryRespVO.class,
+                vo -> Optional.ofNullable(spuMap.get(vo.getSpuId()))
+                        .ifPresent(spu -> vo.setSpuName(spu.getName()).setPicUrl(spu.getPicUrl()).setPrice(spu.getPrice()))));
     }
 
 }

+ 13 - 13
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java

@@ -7,28 +7,28 @@ import lombok.Data;
 
 import java.time.LocalDateTime;
 
+import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
+
 @Schema(description = "管理后台 - 商品浏览记录 Response VO")
 @Data
 @ExcelIgnoreUnannotated
 public class ProductBrowseHistoryRespVO {
 
-    @Schema(description = "记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26055")
-    @ExcelProperty("记录编号")
+    @Schema(description = "编号", requiredMode = REQUIRED, example = "1")
     private Long id;
 
-    @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4314")
-    @ExcelProperty("用户编号")
-    private Long userId;
+    @Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502")
+    private Long spuId;
+
+    // ========== 商品相关字段 ==========
 
-    @Schema(description = "用户是否删除", example = "false")
-    private Boolean userDeleted;
+    @Schema(description = "商品 SPU 名称", example = "赵六")
+    private String spuName;
 
-    @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "42")
-    @ExcelProperty("商品 SPU 编号")
-    private Long spuId;
+    @Schema(description = "商品封面图", example = "https://domain/pic.png")
+    private String picUrl;
 
-    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
-    @ExcelProperty("创建时间")
-    private LocalDateTime createTime;
+    @Schema(description = "商品单价", example = "100")
+    private Integer price;
 
 }

+ 12 - 0
yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/service/spu/ProductSpuService.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.module.product.service.spu;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuPageReqVO;
 import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuSaveReqVO;
 import cn.iocoder.yudao.module.product.controller.admin.spu.vo.ProductSpuUpdateStatusReqVO;
@@ -58,6 +59,17 @@ public interface ProductSpuService {
      */
     List<ProductSpuDO> getSpuList(Collection<Long> ids);
 
+    /**
+     * 获得商品 SPU Map
+     *
+     * @param ids 编号数组
+     * @return 商品 SPU Map
+     */
+    default Map<Long, ProductSpuDO> getSpuMap(Collection<Long> ids) {
+        List<ProductSpuDO> list = getSpuList(ids);
+        return CollectionUtils.convertMap(list, ProductSpuDO::getId);
+    }
+
     /**
      * 获得指定状态的商品 SPU 列表
      *

+ 14 - 0
yudao-module-mall/yudao-module-promotion-api/src/main/java/cn/iocoder/yudao/module/promotion/enums/MessageTemplateConstants.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.promotion.enums;
+
+/**
+ * 通知模板枚举类
+ *
+ * @author HUIHUI
+ */
+public interface MessageTemplateConstants {
+
+    //======================= 小程序订阅消息模版 =======================
+
+    String COMBINATION_SUCCESS = "拼团结果通知";
+
+}

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

@@ -5,6 +5,7 @@ import cn.hutool.core.util.ObjUtil;
 import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.module.member.api.user.MemberUserApi;
@@ -23,9 +24,12 @@ import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationP
 import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
 import cn.iocoder.yudao.module.promotion.dal.mysql.combination.CombinationRecordMapper;
 import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
+import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
 import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Lazy;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
@@ -40,6 +44,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.afterNow;
 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.beforeNow;
 import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.promotion.enums.MessageTemplateConstants.COMBINATION_SUCCESS;
 
 // TODO 芋艿:等拼团记录做完,完整 review 下
 
@@ -66,8 +71,10 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
     private ProductSkuApi productSkuApi;
 
     @Resource
-    @Lazy
+    @Lazy // 延迟加载,避免循环依赖
     private TradeOrderApi tradeOrderApi;
+    @Resource
+    public SocialClientApi socialClientApi;
 
     // TODO @芋艿:在详细预览下;
     @Override
@@ -205,7 +212,25 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
             }
             updateRecords.add(updateRecord);
         });
-        combinationRecordMapper.updateBatch(updateRecords);
+        Boolean updateSuccess = combinationRecordMapper.updateBatch(updateRecords);
+
+        // 3. 拼团成功发送订阅消息
+        if (updateSuccess && isFull) {
+            records.forEach(item -> {
+                getSelf().sendCombinationResultMessage(item);
+            });
+        }
+    }
+
+    @Async
+    public void sendCombinationResultMessage(CombinationRecordDO record) {
+        // 构建并发送模版消息
+        socialClientApi.sendWxaSubscribeMessage(new SocialWxaSubscribeMessageSendReqDTO()
+                .setUserId(record.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue())
+                .setTemplateTitle(COMBINATION_SUCCESS)
+                .setPage("pages/order/detail?id=" + record.getOrderId()) // 订单详情页
+                .addMessage("thing1", "商品拼团活动") // 活动标题
+                .addMessage("thing2", "恭喜您拼团成功!我们将尽快为您发货。")); // 温馨提示
     }
 
     @Override

+ 9 - 4
yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/MessageTemplateConstants.java

@@ -1,6 +1,5 @@
 package cn.iocoder.yudao.module.trade.enums;
 
-// TODO @芋艿:枚举
 /**
  * 通知模板枚举类
  *
@@ -8,9 +7,15 @@ package cn.iocoder.yudao.module.trade.enums;
  */
 public interface MessageTemplateConstants {
 
-    String ORDER_DELIVERY = "order_delivery"; // 短信模版编号
+    // ======================= 短信消息模版 =======================
 
-    String BROKERAGE_WITHDRAW_AUDIT_APPROVE = "brokerage_withdraw_audit_approve"; // 佣金提现(审核通过)
-    String BROKERAGE_WITHDRAW_AUDIT_REJECT = "brokerage_withdraw_audit_reject"; // 佣金提现(审核不通过)
+    String SMS_ORDER_DELIVERY = "order_delivery"; // 短信模版编号
+
+    String SMS_BROKERAGE_WITHDRAW_AUDIT_APPROVE = "brokerage_withdraw_audit_approve"; // 佣金提现(审核通过)
+    String SMS_BROKERAGE_WITHDRAW_AUDIT_REJECT = "brokerage_withdraw_audit_reject"; // 佣金提现(审核不通过)
+
+    // ======================= 小程序订阅消息模版 =======================
+
+    String WXA_ORDER_DELIVERY = "订单发货通知";
 
 }

+ 2 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java

@@ -33,6 +33,7 @@ import cn.iocoder.yudao.module.trade.service.delivery.DeliveryExpressService;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.transaction.support.TransactionSynchronization;
@@ -56,6 +57,7 @@ import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*;
 public class AfterSaleServiceImpl implements AfterSaleService {
 
     @Resource
+    @Lazy // 延迟加载,避免循环依赖
     private TradeOrderUpdateService tradeOrderUpdateService;
     @Resource
     private TradeOrderQueryService tradeOrderQueryService;

+ 2 - 2
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java

@@ -77,14 +77,14 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService {
 
         String templateCode;
         if (BrokerageWithdrawStatusEnum.AUDIT_SUCCESS.equals(status)) {
-            templateCode = MessageTemplateConstants.BROKERAGE_WITHDRAW_AUDIT_APPROVE;
+            templateCode = MessageTemplateConstants.SMS_BROKERAGE_WITHDRAW_AUDIT_APPROVE;
             // 3.1 通过时佣金转余额
             if (BrokerageWithdrawTypeEnum.WALLET.getType().equals(withdraw.getType())) {
                 // todo 疯狂:
             }
             // TODO 疯狂:调用转账接口
         } else if (BrokerageWithdrawStatusEnum.AUDIT_FAIL.equals(status)) {
-            templateCode = MessageTemplateConstants.BROKERAGE_WITHDRAW_AUDIT_REJECT;
+            templateCode = MessageTemplateConstants.SMS_BROKERAGE_WITHDRAW_AUDIT_REJECT;
             // 3.2 驳回时需要退还用户佣金
             brokerageRecordService.addBrokerage(withdraw.getUserId(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT,
                     String.valueOf(withdraw.getId()), withdraw.getPrice(), BrokerageRecordBizTypeEnum.WITHDRAW_REJECT.getTitle());

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/message/TradeMessageServiceImpl.java

@@ -37,7 +37,7 @@ public class TradeMessageServiceImpl implements TradeMessageService {
         notifyMessageSendApi.sendSingleMessageToMember(
                 new NotifySendSingleToUserReqDTO()
                         .setUserId(reqBO.getUserId())
-                        .setTemplateCode(MessageTemplateConstants.ORDER_DELIVERY)
+                        .setTemplateCode(MessageTemplateConstants.SMS_ORDER_DELIVERY)
                         .setTemplateParams(msgMap));
     }
 

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

@@ -1,11 +1,13 @@
 package cn.iocoder.yudao.module.trade.service.order;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
@@ -19,6 +21,8 @@ import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
 import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
 import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO;
+import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderRemarkReqVO;
 import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderUpdateAddressReqVO;
@@ -50,6 +54,7 @@ import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateReqBO;
 import cn.iocoder.yudao.module.trade.service.price.bo.TradePriceCalculateRespBO;
 import cn.iocoder.yudao.module.trade.service.price.calculator.TradePriceCalculatorHelper;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -67,6 +72,7 @@ import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.min
 import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP;
 import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getTerminal;
 import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.trade.enums.MessageTemplateConstants.WXA_ORDER_DELIVERY;
 
 /**
  * 交易订单【写】Service 实现类
@@ -103,6 +109,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     private MemberAddressApi addressApi;
     @Resource
     private ProductCommentApi productCommentApi;
+    @Resource
+    public SocialClientApi socialClientApi;
 
     @Resource
     private TradeOrderProperties tradeOrderProperties;
@@ -364,9 +372,26 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
                 MapUtil.<String, Object>builder().put("expressName", express != null ? express.getName() : "无")
                         .put("logisticsNo", express != null ? deliveryReqVO.getLogisticsNo() : "无").build());
 
-        // 4. 发送站内信
+        // 4.1 发送站内信
         tradeMessageService.sendMessageWhenDeliveryOrder(new TradeOrderMessageWhenDeliveryOrderReqBO()
                 .setOrderId(order.getId()).setUserId(order.getUserId()).setMessage(null));
+        // 4.2 发送订阅消息
+        getSelf().sendDeliveryOrderMessage(order, deliveryReqVO);
+    }
+
+    @Async
+    public void sendDeliveryOrderMessage(TradeOrderDO order, TradeOrderDeliveryReqVO deliveryReqVO) {
+        // 构建并发送模版消息
+        Long orderId = order.getId();
+        socialClientApi.sendWxaSubscribeMessage(new SocialWxaSubscribeMessageSendReqDTO()
+                .setUserId(order.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue())
+                .setTemplateTitle(WXA_ORDER_DELIVERY)
+                .setPage("pages/order/detail?id=" + orderId) // 订单详情页
+                .addMessage("character_string3", String.valueOf(orderId)) // 订单编号
+                .addMessage("phrase6", TradeOrderStatusEnum.DELIVERED.getName()) // 订单状态
+                .addMessage("date4", LocalDateTimeUtil.formatNormal(LocalDateTime.now()))// 发货时间
+                .addMessage("character_string5", StrUtil.blankToDefault(deliveryReqVO.getLogisticsNo(), "-")) // 快递单号
+                .addMessage("thing9", order.getReceiverDetailAddress())); // 收货地址
     }
 
     /**

+ 3 - 0
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/order/handler/TradeCombinationOrderHandler.java

@@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.trade.enums.order.TradeOrderStatusEnum;
 import cn.iocoder.yudao.module.trade.enums.order.TradeOrderTypeEnum;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderQueryService;
 import cn.iocoder.yudao.module.trade.service.order.TradeOrderUpdateService;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.Resource;
@@ -28,11 +29,13 @@ import static cn.iocoder.yudao.module.trade.enums.ErrorCodeConstants.ORDER_DELIV
 public class TradeCombinationOrderHandler implements TradeOrderHandler {
 
     @Resource
+    @Lazy // 延迟加载,避免循环依赖
     private TradeOrderUpdateService orderUpdateService;
     @Resource
     private TradeOrderQueryService orderQueryService;
 
     @Resource
+    @Lazy // 延迟加载,避免循环依赖
     private CombinationRecordApi combinationRecordApi;
 
     @Override

+ 12 - 10
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/AppSocialUserController.java

@@ -5,24 +5,19 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
-import cn.iocoder.yudao.module.member.controller.app.social.vo.AppSocialUserBindReqVO;
-import cn.iocoder.yudao.module.member.controller.app.social.vo.AppSocialUserRespVO;
-import cn.iocoder.yudao.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO;
-import cn.iocoder.yudao.module.member.controller.app.social.vo.AppSocialWxQrcodeReqVO;
+import cn.iocoder.yudao.module.member.controller.app.social.vo.*;
 import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
 import cn.iocoder.yudao.module.system.api.social.SocialUserApi;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialUserBindReqDTO;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialUserRespDTO;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialUserUnbindReqDTO;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.*;
 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.validation.Valid;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import javax.annotation.Resource;
-import javax.validation.Valid;
+import java.util.List;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@@ -73,4 +68,11 @@ public class AppSocialUserController {
         return success(Base64.encode(wxQrcode));
     }
 
+    @GetMapping("/get-subscribe-template-list")
+    @Operation(summary = "获得微信小程订阅模板列表")
+    public CommonResult<List<AppSocialWxSubscribeTemplateRespVO>> getSubscribeTemplateList() {
+        List<SocialWxaSubscribeTemplateRespDTO> template = socialClientApi.getWxaSubscribeTemplateList(UserTypeEnum.MEMBER.getValue());
+        return success(BeanUtils.toBean(template, AppSocialWxSubscribeTemplateRespVO.class));
+    }
+
 }

+ 27 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/social/vo/AppSocialWxSubscribeTemplateRespVO.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.member.controller.app.social.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "用户 APP - 获得小程序订阅模版 Response VO")
+@Data
+public class AppSocialWxSubscribeTemplateRespVO {
+
+    @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED,
+            example = "9Aw5ZV1j9xdWTFEkqCpZ7mIBbSC34khK55OtzUPl0rU")
+    private String id;
+
+    @Schema(description = "模版标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "订单支付通知")
+    private String title;
+
+    @Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED,
+            example = "{ {result.DATA} }\\n\\n领奖金额:{ {withdrawMoney.DATA} }\\n领奖时间:    { {withdrawTime.DATA} }")
+    private String content;
+
+    @Schema(description = "模板内容示例", requiredMode = Schema.RequiredMode.REQUIRED, example = "下单时间:2016年8月8日")
+    private String example;
+
+    @Schema(description = "模版类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
+    private Integer type; // 2 为一次性订阅,3 为长期订阅
+
+}

+ 14 - 0
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/MessageTemplateConstants.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.module.pay.enums;
+
+/**
+ * 通知模板枚举类
+ *
+ * @author HUIHUI
+ */
+public interface MessageTemplateConstants {
+
+    // ======================= 小程序订阅消息 =======================
+
+    String WXA_WALLET_RECHARGER_PAID = "充值成功通知";
+
+}

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

@@ -1,6 +1,8 @@
 package cn.iocoder.yudao.module.pay.service.wallet;
 
+import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.lang.Assert;
+import cn.hutool.extra.spring.SpringUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
@@ -13,16 +15,19 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletRechargeDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletRechargePackageDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletRechargeMapper;
-import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
 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.service.order.PayOrderService;
 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
+import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
+import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import javax.annotation.Resource;
 import java.time.Duration;
 import java.time.LocalDateTime;
 import java.util.Objects;
@@ -31,8 +36,10 @@ import static cn.hutool.core.util.ObjectUtil.notEqual;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.addTime;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+import static cn.iocoder.yudao.framework.common.util.number.MoneyUtils.fenToYuanStr;
 import static cn.iocoder.yudao.module.pay.convert.wallet.PayWalletRechargeConvert.INSTANCE;
 import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.module.pay.enums.MessageTemplateConstants.WXA_WALLET_RECHARGER_PAID;
 import static cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum.*;
 
 /**
@@ -61,6 +68,8 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
     private PayRefundService payRefundService;
     @Resource
     private PayWalletRechargePackageService payWalletRechargePackageService;
+    @Resource
+    public SocialClientApi socialClientApi;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -96,7 +105,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
 
     @Override
     public PageResult<PayWalletRechargeDO> getWalletRechargePackagePage(Long userId, Integer userType,
-                                                                               PageParam pageReqVO, Boolean payStatus) {
+                                                                        PageParam pageReqVO, Boolean payStatus) {
         PayWalletDO wallet = payWalletService.getOrCreateWallet(userId, userType);
         return walletRechargeMapper.selectPage(pageReqVO, wallet.getId(), payStatus);
     }
@@ -126,6 +135,24 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
         // TODO 需要钱包中加个可提现余额
         payWalletService.addWalletBalance(walletRecharge.getWalletId(), String.valueOf(id),
                 PayWalletBizTypeEnum.RECHARGE, walletRecharge.getTotalPrice());
+
+        // 4. 发送订阅消息
+        getSelf().sendWalletRechargerPaidMessage(payOrderId, walletRecharge);
+    }
+
+    @Async
+    public void sendWalletRechargerPaidMessage(Long payOrderId, PayWalletRechargeDO walletRecharge) {
+        // 1. 获得会员钱包信息
+        PayWalletDO wallet = payWalletService.getWallet(walletRecharge.getWalletId());
+        // 2. 构建并发送模版消息
+        socialClientApi.sendWxaSubscribeMessage(new SocialWxaSubscribeMessageSendReqDTO()
+                .setUserId(wallet.getUserId()).setUserType(wallet.getUserType())
+                .setTemplateTitle(WXA_WALLET_RECHARGER_PAID)
+                .setPage("pages/user/wallet/money") // 钱包详情界面
+                .addMessage("character_string1", String.valueOf(payOrderId)) // 支付单编号
+                .addMessage("amount2", fenToYuanStr(walletRecharge.getTotalPrice())) // 充值金额
+                .addMessage("time3", LocalDateTimeUtil.formatNormal(walletRecharge.getCreateTime())) // 充值时间
+                .addMessage("phrase4", "充值成功")); // 充值状态
     }
 
     @Override
@@ -282,4 +309,13 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
         return payOrder;
     }
 
+    /**
+     * 获得自身的代理对象,解决 AOP 生效问题
+     *
+     * @return 自己
+     */
+    private PayWalletRechargeServiceImpl getSelf() {
+        return SpringUtil.getBean(getClass());
+    }
+
 }

+ 23 - 7
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApi.java

@@ -1,12 +1,12 @@
 package cn.iocoder.yudao.module.system.api.social;
 
-import cn.iocoder.yudao.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialWxPhoneNumberInfoRespDTO;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.*;
 import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
 
 import javax.validation.Valid;
 
+import java.util.List;
+
 /**
  * 社交应用的 API 接口
  *
@@ -17,8 +17,8 @@ public interface SocialClientApi {
     /**
      * 获得社交平台的授权 URL
      *
-     * @param socialType 社交平台的类型 {@link SocialTypeEnum}
-     * @param userType 用户类型
+     * @param socialType  社交平台的类型 {@link SocialTypeEnum}
+     * @param userType    用户类型
      * @param redirectUri 重定向 URL
      * @return 社交平台的授权 URL
      */
@@ -28,15 +28,17 @@ public interface SocialClientApi {
      * 创建微信公众号 JS SDK 初始化所需的签名
      *
      * @param userType 用户类型
-     * @param url 访问的 URL 地址
+     * @param url      访问的 URL 地址
      * @return 签名
      */
     SocialWxJsapiSignatureRespDTO createWxMpJsapiSignature(Integer userType, String url);
 
+    //======================= 微信小程序独有 =======================
+
     /**
      * 获得微信小程序的手机信息
      *
-     * @param userType 用户类型
+     * @param userType  用户类型
      * @param phoneCode 手机授权码
      * @return 手机信息
      */
@@ -50,4 +52,18 @@ public interface SocialClientApi {
      */
     byte[] getWxaQrcode(@Valid SocialWxQrcodeReqDTO reqVO);
 
+    /**
+     * 获得微信小程订阅模板
+     *
+     * @return 小程序订阅消息模版
+     */
+    List<SocialWxaSubscribeTemplateRespDTO> getWxaSubscribeTemplateList(Integer userType);
+
+    /**
+     * 发送微信小程序订阅消息
+     *
+     * @param reqDTO 请求
+     */
+    void sendWxaSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO);
+
 }

+ 61 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeMessageSendReqDTO.java

@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.system.api.social.dto;
+
+import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 微信小程序订阅消息发送 Request DTO
+ *
+ * @author HUIHUI
+ */
+@Data
+public class SocialWxaSubscribeMessageSendReqDTO {
+
+    /**
+     * 用户编号
+     *
+     * 关联 MemberUserDO 的 id 编号
+     * 关联 AdminUserDO 的 id 编号
+     */
+    @NotNull(message = "用户编号不能为空")
+    private Long userId;
+    /**
+     * 用户类型
+     *
+     * 关联 {@link UserTypeEnum}
+     */
+    @NotNull(message = "用户类型不能为空")
+    private Integer userType;
+
+    /**
+     * 消息模版标题
+     */
+    @NotEmpty(message = "消息模版标题不能为空")
+    private String templateTitle;
+
+    /**
+     * 点击模板卡片后的跳转页面,仅限本小程序内的页面
+     *
+     * 支持带参数,(示例 index?foo=bar )。该字段不填则模板无跳转。
+     */
+    private String page;
+
+    /**
+     * 模板内容的参数
+     */
+    private Map<String, String> messages;
+
+    public SocialWxaSubscribeMessageSendReqDTO addMessage(String key, String value) {
+        if (messages == null) {
+            messages = new HashMap<>();
+        }
+        messages.put(key, value);
+        return this;
+    }
+
+}

+ 42 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialWxaSubscribeTemplateRespDTO.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.system.api.social.dto;
+
+import lombok.Data;
+
+
+/**
+ * 小程序订阅消息模版 Response DTO
+ *
+ * @author HUIHUI
+ */
+@Data
+public class SocialWxaSubscribeTemplateRespDTO {
+
+    /**
+     * 模版编号
+     */
+    private String id;
+
+    /**
+     * 模版标题
+     */
+    private String title;
+
+    /**
+     * 模版内容
+     */
+    private String content;
+
+    /**
+     * 模板内容示例
+     */
+    private String example;
+
+    /**
+     * 模版类型
+     *
+     * 2:为一次性订阅
+     * 3:为长期订阅
+     */
+    private Integer type;
+
+}

+ 4 - 2
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java

@@ -117,8 +117,10 @@ public interface ErrorCodeConstants {
 
     ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_PHONE_CODE_ERROR = new ErrorCode(1_002_018_200, "获得手机号失败");
     ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_QRCODE_ERROR = new ErrorCode(1_002_018_201, "获得小程序码失败");
-    ErrorCode SOCIAL_CLIENT_NOT_EXISTS = new ErrorCode(1_002_018_202, "社交客户端不存在");
-    ErrorCode SOCIAL_CLIENT_UNIQUE = new ErrorCode(1_002_018_203, "社交客户端已存在配置");
+    ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_TEMPLATE_ERROR = new ErrorCode(1_002_018_202, "获得小程序订阅消息模版失败");
+    ErrorCode SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_MESSAGE_ERROR = new ErrorCode(1_002_018_203, "发送小程序订阅消息失败");
+    ErrorCode SOCIAL_CLIENT_NOT_EXISTS = new ErrorCode(1_002_018_210, "社交客户端不存在");
+    ErrorCode SOCIAL_CLIENT_UNIQUE = new ErrorCode(1_002_018_211, "社交客户端已存在配置");
 
 
     // ========== OAuth2 客户端 1-002-020-000 =========

+ 51 - 4
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/social/SocialClientApiImpl.java

@@ -1,16 +1,24 @@
 package cn.iocoder.yudao.module.system.api.social;
 
 import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialWxJsapiSignatureRespDTO;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialWxPhoneNumberInfoRespDTO;
-import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.*;
+import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
 import cn.iocoder.yudao.module.system.service.social.SocialClientService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.common.bean.WxJsapiSignature;
+import me.chanjar.weixin.common.bean.subscribemsg.TemplateInfo;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
-import javax.annotation.Resource;
+import java.util.List;
+
+import static cn.hutool.core.collection.CollUtil.findOne;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 
 /**
  * 社交应用的 API 实现类
@@ -19,10 +27,13 @@ import javax.annotation.Resource;
  */
 @Service
 @Validated
+@Slf4j
 public class SocialClientApiImpl implements SocialClientApi {
 
     @Resource
     private SocialClientService socialClientService;
+    @Resource
+    public SocialUserApi socialUserApi;
 
     @Override
     public String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri) {
@@ -35,6 +46,8 @@ public class SocialClientApiImpl implements SocialClientApi {
         return BeanUtils.toBean(signature, SocialWxJsapiSignatureRespDTO.class);
     }
 
+    //======================= 微信小程序独有 =======================
+
     @Override
     public SocialWxPhoneNumberInfoRespDTO getWxMaPhoneNumberInfo(Integer userType, String phoneCode) {
         WxMaPhoneNumberInfo info = socialClientService.getWxMaPhoneNumberInfo(userType, phoneCode);
@@ -46,4 +59,38 @@ public class SocialClientApiImpl implements SocialClientApi {
         return socialClientService.getWxaQrcode(reqVO);
     }
 
+    @Override
+    public List<SocialWxaSubscribeTemplateRespDTO> getWxaSubscribeTemplateList(Integer userType) {
+        List<TemplateInfo> list = socialClientService.getSubscribeTemplateList(userType);
+        return convertList(list, item -> BeanUtils.toBean(item, SocialWxaSubscribeTemplateRespDTO.class).setId(item.getPriTmplId()));
+    }
+
+    @Override
+    public void sendWxaSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO) {
+        // 1.1 获得订阅模版列表
+        List<SocialWxaSubscribeTemplateRespDTO> templateList = getWxaSubscribeTemplateList(reqDTO.getUserType());
+        if (CollUtil.isEmpty(templateList)) {
+            log.warn("[sendSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:没有找到订阅模板]", reqDTO);
+            return;
+        }
+        // 1.2 获得需要使用的模版
+        SocialWxaSubscribeTemplateRespDTO template = findOne(templateList, item ->
+                ObjUtil.equal(item.getTitle(), reqDTO.getTemplateTitle()));
+        if (template == null) {
+            log.warn("[sendSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:没有找到订阅模板]", reqDTO);
+            return;
+        }
+
+        // 2. 获得社交用户
+        SocialUserRespDTO socialUser = socialUserApi.getSocialUserByUserId(reqDTO.getUserType(), reqDTO.getUserId(),
+                SocialTypeEnum.WECHAT_MINI_APP.getType());
+        if (StrUtil.isBlankIfStr(socialUser.getOpenid())) {
+            log.warn("[sendSubscribeMessage][reqDTO({}) 发送订阅消息失败,原因:会员 openid 缺失]", reqDTO);
+            return;
+        }
+
+        // 3. 发送订阅消息
+        socialClientService.sendSubscribeMessage(reqDTO, template.getId(), socialUser.getOpenid());
+    }
+
 }

+ 20 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.http

@@ -0,0 +1,20 @@
+### 请求 /system/social-client/send-subscribe-message 接口 => 发送测试订阅消息
+POST {{baseUrl}}/system/social-client/send-subscribe-message
+Authorization: Bearer {{token}}
+Content-Type: application/json
+#Authorization: Bearer test100
+tenant-id: {{adminTenentId}}
+
+{
+  "userId": 247,
+  "userType": 1,
+  "socialType": 34,
+  "templateTitle": "充值成功通知",
+  "page": "",
+  "messages": {
+     "character_string1":"5616122165165",
+     "amount2":"1000.00",
+     "time3":"2024-01-01 10:10:10",
+    "phrase4": "充值成功"
+  }
+}

+ 13 - 3
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialClientController.java

@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.socail;
 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.system.api.social.SocialClientApi;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
 import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientRespVO;
 import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO;
@@ -11,13 +13,12 @@ import cn.iocoder.yudao.module.system.service.social.SocialClientService;
 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.validation.Valid;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
-import javax.annotation.Resource;
-import javax.validation.Valid;
-
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 
 @Tag(name = "管理后台 - 社交客户端")
@@ -28,6 +29,8 @@ public class SocialClientController {
 
     @Resource
     private SocialClientService socialClientService;
+    @Resource
+    private SocialClientApi socialClientApi;
 
     @PostMapping("/create")
     @Operation(summary = "创建社交客户端")
@@ -70,4 +73,11 @@ public class SocialClientController {
         return success(BeanUtils.toBean(pageResult, SocialClientRespVO.class));
     }
 
+    @PostMapping("/send-subscribe-message")
+    @Operation(summary = "发送订阅消息") // 用于测试
+    @PreAuthorize("@ss.hasPermission('system:social-client:query')")
+    public void sendSubscribeMessage(@RequestBody SocialWxaSubscribeMessageSendReqDTO reqDTO) {
+        socialClientApi.sendWxaSubscribeMessage(reqDTO);
+    }
+
 }

+ 9 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/redis/RedisKeyConstants.java

@@ -98,4 +98,13 @@ public interface RedisKeyConstants {
      * VALUE 数据格式:String 模版信息
      */
     String SMS_TEMPLATE = "sms_template";
+
+    /**
+     * 小程序订阅模版的缓存
+     *
+     * KEY 格式:wxa_subscribe_template:{userType}
+     * VALUE 数据格式 String, 模版信息
+     */
+    String WXA_SUBSCRIBE_TEMPLATE = "wxa_subscribe_template";
+
 }

+ 23 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientService.java

@@ -3,12 +3,16 @@ package cn.iocoder.yudao.module.system.service.social;
 import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
 import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO;
 import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
 import com.xingyuv.jushauth.model.AuthUser;
 import me.chanjar.weixin.common.bean.WxJsapiSignature;
+import me.chanjar.weixin.common.bean.subscribemsg.TemplateInfo;
+
+import java.util.List;
 
 import javax.validation.Valid;
 
@@ -70,6 +74,25 @@ public interface SocialClientService {
      */
     byte[] getWxaQrcode(SocialWxQrcodeReqDTO reqVO);
 
+    /**
+     * 获得微信小程订阅模板
+     *
+     * 缓存的目的:考虑到微信小程序订阅消息选择好模版后几乎不会变动,缓存增加查询效率
+     *
+     * @param userType 用户类型
+     * @return 微信小程订阅模板
+     */
+    List<TemplateInfo> getSubscribeTemplateList(Integer userType);
+
+    /**
+     * 发送微信小程序订阅消息
+     *
+     * @param reqDTO     请求
+     * @param templateId 模版编号
+     * @param openId     会员 openId
+     */
+    void sendSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO, String templateId, String openId);
+
     // =================== 客户端管理 ===================
 
     /**

+ 69 - 2
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialClientServiceImpl.java

@@ -1,10 +1,14 @@
 package cn.iocoder.yudao.module.system.service.social;
 
 import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.api.WxMaSubscribeService;
 import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
 import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
+import cn.binarywang.wx.miniapp.bean.WxMaSubscribeMessage;
 import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl;
+import cn.binarywang.wx.miniapp.constant.WxMaConstants;
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.ReflectUtil;
@@ -15,10 +19,12 @@ import cn.iocoder.yudao.framework.common.util.cache.CacheUtils;
 import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.system.api.social.dto.SocialWxQrcodeReqDTO;
+import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
 import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientPageReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.socail.vo.client.SocialClientSaveReqVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.social.SocialClientDO;
 import cn.iocoder.yudao.module.system.dal.mysql.social.SocialClientMapper;
+import cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants;
 import cn.iocoder.yudao.module.system.enums.social.SocialTypeEnum;
 import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties;
 import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
@@ -35,20 +41,25 @@ import com.xingyuv.justauth.AuthRequestFactory;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 import me.chanjar.weixin.common.bean.WxJsapiSignature;
+import me.chanjar.weixin.common.bean.subscribemsg.TemplateInfo;
 import me.chanjar.weixin.common.error.WxErrorException;
 import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
 import me.chanjar.weixin.mp.api.WxMpService;
 import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
 import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
 import java.time.Duration;
+import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
 
@@ -62,7 +73,7 @@ import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
 public class SocialClientServiceImpl implements SocialClientService {
 
     /**
-     * 小程序版本
+     * 小程序码要打开的小程序版本
      *
      * 1. release:正式版
      * 2. trial:体验版
@@ -70,6 +81,15 @@ public class SocialClientServiceImpl implements SocialClientService {
      */
     @Value("${yudao.wxa-code.env-version:release}")
     public String envVersion;
+    /**
+     * 订阅消息跳转小程序类型
+     *
+     * 1. developer:开发版
+     * 2. trial:体验版
+     * 3. formal:正式版
+     */
+    @Value("${yudao.wxa-subscribe-message.miniprogram-state:formal}")
+    public String miniprogramState;
 
     @Resource
     private AuthRequestFactory authRequestFactory;
@@ -254,11 +274,58 @@ public class SocialClientServiceImpl implements SocialClientService {
                     null,
                     ObjUtil.defaultIfNull(reqVO.getHyaline(), SocialWxQrcodeReqDTO.HYALINE));
         } catch (WxErrorException e) {
-            log.error("[getWxQrcode][reqVO({})) 获得小程序码失败]", reqVO, e);
+            log.error("[getWxQrcode][reqVO({}) 获得小程序码失败]", reqVO, e);
             throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_QRCODE_ERROR);
         }
     }
 
+    @Override
+    @Cacheable(cacheNames = RedisKeyConstants.WXA_SUBSCRIBE_TEMPLATE, key = "#userType", condition = "#result != null")
+    public List<TemplateInfo> getSubscribeTemplateList(Integer userType) {
+        WxMaService service = getWxMaService(userType);
+        try {
+            WxMaSubscribeService subscribeService = service.getSubscribeService();
+            return subscribeService.getTemplateList();
+        } catch (WxErrorException e) {
+            log.error("[getSubscribeTemplate][userType({}) 获得小程序订阅消息模版]", userType, e);
+            throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_TEMPLATE_ERROR);
+        }
+    }
+
+    @Override
+    public void sendSubscribeMessage(SocialWxaSubscribeMessageSendReqDTO reqDTO, String templateId, String openId) {
+        WxMaService service = getWxMaService(reqDTO.getUserType());
+        try {
+            WxMaSubscribeService subscribeService = service.getSubscribeService();
+            subscribeService.sendSubscribeMsg(buildMessageSendReqDTO(reqDTO, templateId, openId));
+        } catch (WxErrorException e) {
+            log.error("[sendSubscribeMessage][reqVO({}) templateId({}) openId({}) 发送小程序订阅消息失败]", reqDTO, templateId, openId, e);
+            throw exception(SOCIAL_CLIENT_WEIXIN_MINI_APP_SUBSCRIBE_MESSAGE_ERROR);
+        }
+    }
+
+    /**
+     * 构建发送消息请求参数
+     *
+     * @param reqDTO     请求
+     * @param templateId 模版编号
+     * @param openId     会员 openId
+     * @return 微信小程序订阅消息请求参数
+     */
+    private WxMaSubscribeMessage buildMessageSendReqDTO(SocialWxaSubscribeMessageSendReqDTO reqDTO,
+                                                        String templateId, String openId) {
+        // 设置订阅消息基本参数
+        WxMaSubscribeMessage subscribeMessage = new WxMaSubscribeMessage().setLang(WxMaConstants.MiniProgramLang.ZH_CN)
+                .setMiniprogramState(miniprogramState).setTemplateId(templateId).setToUser(openId).setPage(reqDTO.getPage());
+        // 设置具体消息参数
+        Map<String, String> messages = reqDTO.getMessages();
+        if (CollUtil.isNotEmpty(messages)) {
+            reqDTO.getMessages().keySet().forEach(key -> findAndThen(messages, key, value ->
+                    subscribeMessage.addData(new WxMaSubscribeMessage.MsgData(key, value))));
+        }
+        return subscribeMessage;
+    }
+
     /**
      * 获得 clientId + clientSecret 对应的 WxMpService 对象
      *

+ 14 - 4
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java

@@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
 import cn.iocoder.yudao.framework.datapermission.core.util.DataPermissionUtils;
 import cn.iocoder.yudao.module.infra.api.config.ConfigApi;
 import cn.iocoder.yudao.module.infra.api.file.FileApi;
@@ -31,13 +32,14 @@ import com.google.common.annotations.VisibleForTesting;
 import com.mzt.logapi.context.LogRecordContext;
 import com.mzt.logapi.service.impl.DiffParseFunction;
 import com.mzt.logapi.starter.annotation.LogRecord;
+import jakarta.annotation.Resource;
+import jakarta.validation.ConstraintViolationException;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import javax.annotation.Resource;
 import java.io.InputStream;
 import java.time.LocalDateTime;
 import java.util.*;
@@ -443,7 +445,14 @@ public class AdminUserServiceImpl implements AdminUserService {
         UserImportRespVO respVO = UserImportRespVO.builder().createUsernames(new ArrayList<>())
                 .updateUsernames(new ArrayList<>()).failureUsernames(new LinkedHashMap<>()).build();
         importUsers.forEach(importUser -> {
-            // 校验,判断是否有不符合的原因
+            // 2.1.1 校验字段是否符合要求
+            try {
+                ValidationUtils.validate(BeanUtils.toBean(importUser, UserSaveReqVO.class).setPassword(initPassword));
+            } catch (ConstraintViolationException ex){
+                respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage());
+                return;
+            }
+            // 2.1.2 校验,判断是否有不符合的原因
             try {
                 validateUserForCreateOrUpdate(null, null, importUser.getMobile(), importUser.getEmail(),
                         importUser.getDeptId(), null);
@@ -451,7 +460,8 @@ public class AdminUserServiceImpl implements AdminUserService {
                 respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage());
                 return;
             }
-            // 判断如果不存在,在进行插入
+
+            // 2.2.1 判断如果不存在,在进行插入
             AdminUserDO existUser = userMapper.selectByUsername(importUser.getUsername());
             if (existUser == null) {
                 userMapper.insert(BeanUtils.toBean(importUser, AdminUserDO.class)
@@ -459,7 +469,7 @@ public class AdminUserServiceImpl implements AdminUserService {
                 respVO.getCreateUsernames().add(importUser.getUsername());
                 return;
             }
-            // 如果存在,判断是否允许更新
+            // 2.2.2 如果存在,判断是否允许更新
             if (!isUpdateSupport) {
                 respVO.getFailureUsernames().put(importUser.getUsername(), USER_USERNAME_EXISTS.getMsg());
                 return;

+ 8 - 0
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java

@@ -430,6 +430,8 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
     public void testImportUserList_01() {
         // 准备参数
         UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> {
+            o.setEmail(randomEmail());
+            o.setMobile(randomMobile());
         });
         // mock 方法,模拟失败
         doThrow(new ServiceException(DEPT_NOT_FOUND)).when(deptService).validateDeptList(any());
@@ -452,6 +454,8 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
         UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> {
             o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围
             o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围
+            o.setEmail(randomEmail());
+            o.setMobile(randomMobile());
         });
         // mock deptService 的方法
         DeptDO dept = randomPojo(DeptDO.class, o -> {
@@ -486,6 +490,8 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
             o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围
             o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围
             o.setUsername(dbUser.getUsername());
+            o.setEmail(randomEmail());
+            o.setMobile(randomMobile());
         });
         // mock deptService 的方法
         DeptDO dept = randomPojo(DeptDO.class, o -> {
@@ -516,6 +522,8 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
             o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围
             o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围
             o.setUsername(dbUser.getUsername());
+            o.setEmail(randomEmail());
+            o.setMobile(randomMobile());
         });
         // mock deptService 的方法
         DeptDO dept = randomPojo(DeptDO.class, o -> {

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

@@ -174,6 +174,7 @@ logging:
     cn.iocoder.yudao.module.statistics.dal.mysql: debug
     cn.iocoder.yudao.module.crm.dal.mysql: debug
     cn.iocoder.yudao.module.erp.dal.mysql: debug
+    cn.iocoder.yudao.module.ai.dal.mysql: debug
     org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示
 
 debug: false
@@ -195,10 +196,12 @@ wx:
   miniapp: # 小程序配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md 文档
     #    appid: wx62056c0d5e8db250 # 测试号(牛希尧提供的)
     #    secret: 333ae72f41552af1e998fe1f54e1584a
-    appid: wx63c280fe3248a3e7 # wenhualian的接口测试号
-    secret: 6f270509224a7ae1296bbf1c8cb97aed
+#    appid: wx63c280fe3248a3e7 # wenhualian的接口测试号
+#    secret: 6f270509224a7ae1296bbf1c8cb97aed
 #    appid: wxc4598c446f8a9cb3 # 测试号(Kongdy 提供的)
 #    secret: 4a1a04e07f6a4a0751b39c3064a92c8b
+    appid: wx66186af0759f47c9 # 测试号(puhui 提供的)
+    secret: 3218bcbd112cbc614c7264ceb20144ac
     config-storage:
       type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取
       key-prefix: wa # Redis Key 的前缀
@@ -220,6 +223,8 @@ yudao:
   demo: false # 关闭演示模式
   wxa-code:
     env-version: develop # 小程序版本: 正式版为 "release";体验版为 "trial";开发版为 "develop"
+  wxa-subscribe-message:
+    miniprogram-state: developer # 跳转小程序类型:开发版为 “developer”;体验版为 “trial”为;正式版为 “formal”
   tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
 
 justauth: