Quellcode durchsuchen

【功能优化】支付:支付应用,增加 appKey 标识,用于不同接入方的标识

rayson vor 10 Monaten
Ursprung
Commit
4509c13559
50 geänderte Dateien mit 635 neuen und 671 gelöschten Zeilen
  1. 0 1
      citu-module-mall/citu-module-product-biz/src/main/java/com/citu/module/product/controller/app/spu/AppProductSpuController.java
  2. 0 2
      citu-module-mall/citu-module-product-biz/src/main/java/com/citu/module/product/dal/dataobject/brand/ProductBrandDO.java
  3. 7 16
      citu-module-mall/citu-module-product-biz/src/main/java/com/citu/module/product/dal/dataobject/sku/ProductSkuDO.java
  4. 14 0
      citu-module-mall/citu-module-promotion-api/src/main/java/com/citu/module/promotion/enums/MessageTemplateConstants.java
  5. 0 25
      citu-module-mall/citu-module-promotion-biz/src/main/java/com/citu/module/promotion/controller/app/combination/AppCombinationRecordController.java
  6. 0 1
      citu-module-mall/citu-module-promotion-biz/src/main/java/com/citu/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java
  7. 0 18
      citu-module-mall/citu-module-promotion-biz/src/main/java/com/citu/module/promotion/service/combination/CombinationRecordService.java
  8. 31 60
      citu-module-mall/citu-module-promotion-biz/src/main/java/com/citu/module/promotion/service/combination/CombinationRecordServiceImpl.java
  9. 0 2
      citu-module-mall/citu-module-statistics-biz/src/main/java/com/citu/module/statistics/job/product/ProductStatisticsJob.java
  10. 0 2
      citu-module-mall/citu-module-statistics-biz/src/main/java/com/citu/module/statistics/job/trade/TradeStatisticsJob.java
  11. 3 0
      citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java
  12. 1 1
      citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/convert/order/TradeOrderConvert.java
  13. 1 0
      citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/dal/mysql/aftersale/AfterSaleMapper.java
  14. 0 1
      citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/framework/order/config/TradeOrderConfig.java
  15. 9 3
      citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/framework/order/config/TradeOrderProperties.java
  16. 11 10
      citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/service/brokerage/BrokerageUserService.java
  17. 24 11
      citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/service/brokerage/BrokerageUserServiceImpl.java
  18. 26 2
      citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/service/order/TradeOrderUpdateServiceImpl.java
  19. 3 2
      citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/service/price/calculator/TradePriceCalculatorHelper.java
  20. 0 1
      citu-module-mall/citu-module-trade-biz/src/main/resources/application.yaml
  21. 3 3
      citu-module-pay/citu-module-pay-api/src/main/java/com/citu/module/pay/api/order/dto/PayOrderCreateReqDTO.java
  22. 3 3
      citu-module-pay/citu-module-pay-api/src/main/java/com/citu/module/pay/api/refund/dto/PayRefundCreateReqDTO.java
  23. 3 3
      citu-module-pay/citu-module-pay-api/src/main/java/com/citu/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java
  24. 1 0
      citu-module-pay/citu-module-pay-api/src/main/java/com/citu/module/pay/enums/ErrorCodeConstants.java
  25. 5 0
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/controller/admin/app/vo/PayAppBaseVO.java
  26. 3 0
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/controller/admin/app/vo/PayAppPageReqVO.java
  27. 3 0
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/controller/admin/app/vo/PayAppRespVO.java
  28. 12 1
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java
  29. 4 0
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/dal/dataobject/app/PayAppDO.java
  30. 5 0
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/dal/mysql/app/PayAppMapper.java
  31. 8 0
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/framework/pay/config/PayProperties.java
  32. 10 0
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/app/PayAppService.java
  33. 41 5
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/app/PayAppServiceImpl.java
  34. 4 4
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/demo/PayDemoOrderServiceImpl.java
  35. 2 2
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/order/PayOrderServiceImpl.java
  36. 10 9
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/refund/PayRefundServiceImpl.java
  37. 5 5
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/transfer/PayTransferServiceImpl.java
  38. 7 8
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/wallet/PayWalletRechargeServiceImpl.java
  39. 11 0
      citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/wallet/PayWalletTransactionServiceImpl.java
  40. 6 3
      citu-module-pay/citu-module-pay-biz/src/test/java/com/citu/module/pay/service/order/PayOrderServiceTest.java
  41. 16 16
      citu-module-pay/citu-module-pay-biz/src/test/java/com/citu/module/pay/service/refund/PayRefundServiceTest.java
  42. 1 0
      citu-module-pay/citu-module-pay-biz/src/test/resources/sql/create_tables.sql
  43. 21 0
      citu-module-pay/citu-spring-boot-starter-biz-pay/src/main/java/com/citu/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java
  44. 2 0
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/framework/sms/core/client/SmsClient.java
  45. 21 28
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/framework/sms/core/client/impl/AliyunSmsClient.java
  46. 35 35
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java
  47. 86 219
      citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/framework/sms/core/client/impl/TencentSmsClient.java
  48. 1 10
      citu-module-system/citu-module-system-biz/src/test/java/com/citu/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java
  49. 60 21
      citu-module-system/citu-module-system-biz/src/test/java/com/citu/module/system/framework/sms/core/client/impl/SmsClientTests.java
  50. 116 138
      citu-module-system/citu-module-system-biz/src/test/java/com/citu/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java

+ 0 - 1
citu-module-mall/citu-module-product-biz/src/main/java/com/citu/module/product/controller/app/spu/AppProductSpuController.java

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

+ 0 - 2
citu-module-mall/citu-module-product-biz/src/main/java/com/citu/module/product/dal/dataobject/brand/ProductBrandDO.java

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

+ 7 - 16
citu-module-mall/citu-module-product-biz/src/main/java/com/citu/module/product/dal/dataobject/sku/ProductSkuDO.java

@@ -1,16 +1,14 @@
 package com.citu.module.product.dal.dataobject.sku;
 
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
 import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
-import com.citu.framework.common.util.json.JsonUtils;
 import com.citu.framework.mybatis.core.dataobject.BaseDO;
 import com.citu.module.product.dal.dataobject.property.ProductPropertyDO;
 import com.citu.module.product.dal.dataobject.property.ProductPropertyValueDO;
 import com.citu.module.product.dal.dataobject.spu.ProductSpuDO;
-import com.baomidou.mybatisplus.annotation.KeySequence;
-import com.baomidou.mybatisplus.annotation.TableField;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
 import lombok.*;
 
 import java.util.List;
@@ -37,7 +35,7 @@ public class ProductSkuDO extends BaseDO {
     private Long id;
     /**
      * SPU 编号
-     *
+     * <p>
      * 关联 {@link ProductSpuDO#getId()}
      */
     private Long spuId;
@@ -112,7 +110,7 @@ public class ProductSkuDO extends BaseDO {
         /**
          * 属性名字
          * 冗余 {@link ProductPropertyDO#getName()}
-         *
+         * <p>
          * 注意:每次属性名字发生变化时,需要更新该冗余
          */
         private String propertyName;
@@ -125,18 +123,11 @@ public class ProductSkuDO extends BaseDO {
         /**
          * 属性值名字
          * 冗余 {@link ProductPropertyValueDO#getName()}
-         *
+         * <p>
          * 注意:每次属性值名字发生变化时,需要更新该冗余
          */
         private String valueName;
 
     }
-
-    // TODO 芋艿:integral from y
-    // TODO 芋艿:pinkPrice from y
-    // TODO 芋艿:seckillPrice from y
-    // TODO 芋艿:pinkStock from y
-    // TODO 芋艿:seckillStock from y
-
 }
 

+ 14 - 0
citu-module-mall/citu-module-promotion-api/src/main/java/com/citu/module/promotion/enums/MessageTemplateConstants.java

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

+ 0 - 25
citu-module-mall/citu-module-promotion-biz/src/main/java/com/citu/module/promotion/controller/app/combination/AppCombinationRecordController.java

@@ -43,9 +43,6 @@ public class AppCombinationRecordController {
 
     @Resource
     private CombinationRecordService combinationRecordService;
-    @Resource
-    @Lazy
-    private TradeOrderApi tradeOrderApi;
 
     @GetMapping("/get-summary")
     @Operation(summary = "获得拼团记录的概要信息", description = "用于小程序首页")
@@ -117,26 +114,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
citu-module-mall/citu-module-promotion-biz/src/main/java/com/citu/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 - 18
citu-module-mall/citu-module-promotion-biz/src/main/java/com/citu/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);
-
     /**
      * 处理过期拼团
      *

+ 31 - 60
citu-module-mall/citu-module-promotion-biz/src/main/java/com/citu/module/promotion/service/combination/CombinationRecordServiceImpl.java

@@ -5,6 +5,7 @@ import cn.hutool.core.util.ObjUtil;
 import cn.hutool.extra.spring.SpringUtil;
 import com.citu.framework.common.core.KeyValue;
 import com.citu.framework.common.enums.CommonStatusEnum;
+import com.citu.framework.common.enums.UserTypeEnum;
 import com.citu.framework.common.pojo.PageResult;
 import com.citu.framework.common.util.json.JsonUtils;
 import com.citu.module.member.api.user.MemberUserApi;
@@ -23,9 +24,12 @@ import com.citu.module.promotion.dal.dataobject.combination.CombinationProductDO
 import com.citu.module.promotion.dal.dataobject.combination.CombinationRecordDO;
 import com.citu.module.promotion.dal.mysql.combination.CombinationRecordMapper;
 import com.citu.module.promotion.enums.combination.CombinationRecordStatusEnum;
+import com.citu.module.system.api.social.SocialClientApi;
+import com.citu.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
 import com.citu.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;
@@ -33,13 +37,17 @@ import org.springframework.validation.annotation.Validated;
 import javax.annotation.Nullable;
 import javax.annotation.Resource;
 import java.time.LocalDateTime;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
 
 import static com.citu.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static com.citu.framework.common.util.collection.CollectionUtils.*;
 import static com.citu.framework.common.util.date.LocalDateTimeUtils.afterNow;
 import static com.citu.framework.common.util.date.LocalDateTimeUtils.beforeNow;
 import static com.citu.module.promotion.enums.ErrorCodeConstants.*;
+import static com.citu.module.promotion.enums.MessageTemplateConstants.COMBINATION_SUCCESS;
 
 // TODO 芋艿:等拼团记录做完,完整 review 下
 
@@ -53,20 +61,20 @@ import static com.citu.module.promotion.enums.ErrorCodeConstants.*;
 @Validated
 public class CombinationRecordServiceImpl implements CombinationRecordService {
 
+    @Resource
+    public SocialClientApi socialClientApi;
     @Resource
     private CombinationActivityService combinationActivityService;
     @Resource
     private CombinationRecordMapper combinationRecordMapper;
-
     @Resource
     private MemberUserApi memberUserApi;
     @Resource
     private ProductSpuApi productSpuApi;
     @Resource
     private ProductSkuApi productSkuApi;
-
     @Resource
-    @Lazy
+    @Lazy // 延迟加载,避免循环依赖
     private TradeOrderApi tradeOrderApi;
 
     // TODO @芋艿:在详细预览下;
@@ -205,7 +213,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
@@ -264,61 +290,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. 获取所有正在进行中的过期的父拼团

+ 0 - 2
citu-module-mall/citu-module-statistics-biz/src/main/java/com/citu/module/statistics/job/product/ProductStatisticsJob.java

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

+ 0 - 2
citu-module-mall/citu-module-statistics-biz/src/main/java/com/citu/module/statistics/job/trade/TradeStatisticsJob.java

@@ -10,8 +10,6 @@ import com.xxl.job.core.handler.annotation.XxlJob;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.Resource;
-
-// TODO 芋艿:缺个 Job 的配置;等和 Product 一起配置
 /**
  * 交易统计 Job
  *

+ 3 - 0
citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/controller/admin/aftersale/vo/AfterSalePageReqVO.java

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

+ 1 - 1
citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/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
citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/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 - 1
citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/framework/order/config/TradeOrderConfig.java

@@ -3,7 +3,6 @@ package com.citu.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

+ 9 - 3
citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/framework/order/config/TradeOrderProperties.java

@@ -4,9 +4,11 @@ import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.validation.annotation.Validated;
 
+import javax.validation.constraints.NotEmpty;
 import javax.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";
+
     /**
-     * 应用编号
+     * 支付应用标识
+     * <p>
+     * 在 pay 模块的 [支付管理 -> 应用信息] 里添加
      */
-    @NotNull(message = "应用编号不能为空")
-    private Long appId;
+    @NotEmpty(message = "Pay 应用标识不能为空")
+    private String payAppKey = PAY_APP_KEY_DEFAULT;
 
     /**
      * 支付超时时间

+ 11 - 10
citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/service/brokerage/BrokerageUserService.java

@@ -27,14 +27,6 @@ public interface BrokerageUserService {
      */
     BrokerageUserDO getBrokerageUser(Long id);
 
-    /**
-     * 获得分销用户列表
-     *
-     * @param ids 编号
-     * @return 分销用户列表
-     */
-    List<BrokerageUserDO> getBrokerageUserList(Collection<Long> ids);
-
     /**
      * 获得分销用户分页
      *
@@ -67,6 +59,14 @@ public interface BrokerageUserService {
      */
     BrokerageUserDO getBindBrokerageUser(Long id);
 
+    /**
+     * 获得或创建分销用户
+     *
+     * @param id 用户编号
+     * @return 分销用户
+     */
+    BrokerageUserDO getOrCreateBrokerageUser(Long id);
+
     /**
      * 更新用户佣金
      *
@@ -104,8 +104,8 @@ public interface BrokerageUserService {
     /**
      * 【会员】绑定推广员
      *
-     * @param userId       用户编号
-     * @param bindUserId   推广员编号
+     * @param userId     用户编号
+     * @param bindUserId 推广员编号
      * @return 是否绑定
      */
     boolean bindBrokerageUser(@NotNull Long userId, @NotNull Long bindUserId);
@@ -134,4 +134,5 @@ public interface BrokerageUserService {
      * @return 下级分销统计分页
      */
     PageResult<AppBrokerageUserChildSummaryRespVO> getBrokerageUserChildSummaryPage(AppBrokerageUserChildSummaryPageReqVO pageReqVO, Long userId);
+
 }

+ 24 - 11
citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/service/brokerage/BrokerageUserServiceImpl.java

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.BooleanUtil;
+import cn.hutool.core.util.ObjUtil;
 import cn.hutool.core.util.StrUtil;
 import com.citu.framework.common.pojo.PageResult;
 import com.citu.framework.common.util.date.LocalDateTimeUtils;
@@ -59,11 +60,6 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
         return brokerageUserMapper.selectById(id);
     }
 
-    @Override
-    public List<BrokerageUserDO> getBrokerageUserList(Collection<Long> ids) {
-        return brokerageUserMapper.selectBatchIds(ids);
-    }
-
     @Override
     public PageResult<BrokerageUserDO> getBrokerageUserPage(BrokerageUserPageReqVO pageReqVO) {
         List<Long> childIds = getChildUserIdsByLevel(pageReqVO.getBindUserId(), pageReqVO.getLevel());
@@ -127,6 +123,19 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
                 .orElse(null);
     }
 
+    @Override
+    public BrokerageUserDO getOrCreateBrokerageUser(Long id) {
+        BrokerageUserDO brokerageUser = brokerageUserMapper.selectById(id);
+        // 特殊:人人分销的情况下,如果分销人为空则创建分销人
+        if (brokerageUser == null && ObjUtil.equal(BrokerageEnabledConditionEnum.ALL.getCondition(),
+                tradeConfigService.getTradeConfig().getBrokerageEnabledCondition())) {
+            brokerageUser = new BrokerageUserDO().setId(id).setBrokerageEnabled(true).setBrokeragePrice(0)
+                    .setBrokerageTime(LocalDateTime.now()).setFrozenPrice(0);
+            brokerageUserMapper.insert(brokerageUser);
+        }
+        return brokerageUser;
+    }
+
     @Override
     public boolean updateUserPrice(Long id, Integer price) {
         if (price > 0) {
@@ -184,7 +193,6 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
             if (BrokerageEnabledConditionEnum.ALL.getCondition().equals(enabledCondition)) { // 人人分销:用户默认就有分销资格
                 brokerageUser.setBrokerageEnabled(true).setBrokerageTime(LocalDateTime.now());
             }
-            brokerageUser.setBindUserId(bindUserId).setBindUserTime(LocalDateTime.now());
             brokerageUserMapper.insert(fillBindUserData(bindUserId, brokerageUser));
         } else {
             brokerageUserMapper.updateById(fillBindUserData(bindUserId, new BrokerageUserDO().setId(userId)));
@@ -290,22 +298,27 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
      */
     private boolean isNewRegisterUser(Long userId) {
         MemberUserRespDTO user = memberUserApi.getUser(userId).getCheckedData();
-        return user != null && LocalDateTimeUtils.beforeNow(user.getCreateTime().plusSeconds(30));
+        return user != null && LocalDateTimeUtils.afterNow(user.getCreateTime().plusSeconds(30));
     }
 
     private void validateCanBindUser(BrokerageUserDO user, Long bindUserId) {
-        // 校验要绑定的用户有无推广资格
-        BrokerageUserDO bindUser = brokerageUserMapper.selectById(bindUserId);
+        // 1.1 校验推广人是否存在
+        MemberUserRespDTO bindUserInfo = memberUserApi.getUser(bindUserId).getCheckedData();
+        if (bindUserInfo == null) {
+            throw exception(BROKERAGE_USER_NOT_EXISTS);
+        }
+        // 1.2 校验要绑定的用户有无推广资格
+        BrokerageUserDO bindUser = getOrCreateBrokerageUser(bindUserId);
         if (bindUser == null || BooleanUtil.isFalse(bindUser.getBrokerageEnabled())) {
             throw exception(BROKERAGE_BIND_USER_NOT_ENABLED);
         }
 
-        // 校验绑定自己
+        // 2. 校验绑定自己
         if (Objects.equals(user.getId(), bindUserId)) {
             throw exception(BROKERAGE_BIND_SELF);
         }
 
-        // 下级不能绑定自己的上级
+        // 3. 下级不能绑定自己的上级
         for (int i = 0; i <= Short.MAX_VALUE; i++) {
             if (Objects.equals(bindUser.getBindUserId(), user.getId())) {
                 throw exception(BROKERAGE_BIND_LOOP);

+ 26 - 2
citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/service/order/TradeOrderUpdateServiceImpl.java

@@ -1,11 +1,13 @@
 package com.citu.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 com.citu.framework.common.core.KeyValue;
 import com.citu.framework.common.enums.UserTypeEnum;
@@ -50,6 +52,7 @@ import com.citu.module.trade.service.price.bo.TradePriceCalculateReqBO;
 import com.citu.module.trade.service.price.bo.TradePriceCalculateRespBO;
 import com.citu.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;
 
@@ -103,6 +106,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
     private MemberAddressApi addressApi;
     @Resource
     private ProductCommentApi productCommentApi;
+    @Resource
+    public SocialClientApi socialClientApi;
 
     @Resource
     private TradeOrderProperties tradeOrderProperties;
@@ -364,9 +369,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())); // 收货地址
     }
 
     /**
@@ -830,12 +852,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
citu-module-mall/citu-module-trade-biz/src/main/java/com/citu/module/trade/service/price/calculator/TradePriceCalculatorHelper.java

@@ -1,6 +1,7 @@
 package com.citu.module.trade.service.price.calculator;
 
 import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
 import com.citu.framework.common.util.collection.CollectionUtils;
 import com.citu.module.product.api.sku.dto.ProductSkuRespDTO;
 import com.citu.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 一一对应
      */

+ 0 - 1
citu-module-mall/citu-module-trade-biz/src/main/resources/application.yaml

@@ -111,7 +111,6 @@ citu:
       - com.citu.module.menduner.system.enums.ErrorCodeConstants
   trade:
     order:
-      app-id: 1 # 商户编号
       pay-expire-time: 2h # 支付的过期时间
       receive-expire-time: 14d # 收货的过期时间
       comment-expire-time: 7d # 评论的过期时间

+ 3 - 3
citu-module-pay/citu-module-pay-api/src/main/java/com/citu/module/pay/api/order/dto/PayOrderCreateReqDTO.java

@@ -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
      */

+ 3 - 3
citu-module-pay/citu-module-pay-api/src/main/java/com/citu/module/pay/api/refund/dto/PayRefundCreateReqDTO.java

@@ -16,10 +16,10 @@ import javax.validation.constraints.NotNull;
 public class PayRefundCreateReqDTO {
 
     /**
-     * 应用编号
+     * 应用标识
      */
-    @NotNull(message = "应用编号不能为空")
-    private Long appId;
+    @NotNull(message = "应用标识不能为空")
+    private String appKey;
     /**
      * 用户 IP
      */

+ 3 - 3
citu-module-pay/citu-module-pay-api/src/main/java/com/citu/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java

@@ -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
citu-module-pay/citu-module-pay-api/src/main/java/com/citu/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, "支付渠道的配置不存在");

+ 5 - 0
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/controller/admin/app/vo/PayAppBaseVO.java

@@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import org.hibernate.validator.constraints.URL;
 
+import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.NotNull;
 
 /**
@@ -15,6 +16,10 @@ import javax.validation.constraints.NotNull;
 @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;

+ 3 - 0
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/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;
 

+ 3 - 0
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/controller/admin/app/vo/PayAppRespVO.java

@@ -16,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;
 

+ 12 - 1
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/controller/admin/wallet/vo/transaction/PayWalletTransactionPageReqVO.java

@@ -1,6 +1,8 @@
 package com.citu.module.pay.controller.admin.wallet.vo.transaction;
 
+import com.citu.framework.common.enums.UserTypeEnum;
 import com.citu.framework.common.pojo.PageParam;
+import com.citu.framework.common.validation.InEnum;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -8,7 +10,16 @@ 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
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/dal/dataobject/app/PayAppDO.java

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

+ 5 - 0
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/dal/mysql/app/PayAppMapper.java

@@ -13,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
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/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;
+
 }

+ 10 - 0
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/app/PayAppService.java

@@ -102,4 +102,14 @@ public interface PayAppService {
      */
     PayAppDO validPayApp(Long id);
 
+    /**
+     * 支付应用的合法性
+     * <p>
+     * 如果不合法,抛出 {@link ServiceException} 业务异常
+     *
+     * @param appKey 应用标识
+     * @return 应用
+     */
+    PayAppDO validPayApp(String appKey);
+
 }

+ 41 - 5
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/app/PayAppServiceImpl.java

@@ -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,17 +130,33 @@ 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
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/demo/PayDemoOrderServiceImpl.java

@@ -43,11 +43,11 @@ import static com.citu.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)))).getCheckedData(); // 支付的过期时间
@@ -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())).getCheckedData();// 价格信息

+ 2 - 2
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/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)); // 理论来说,不会出现这个情况

+ 10 - 9
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/refund/PayRefundServiceImpl.java

@@ -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())) {

+ 5 - 5
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/transfer/PayTransferServiceImpl.java

@@ -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 - 8
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/wallet/PayWalletRechargeServiceImpl.java

@@ -20,6 +20,7 @@ import com.citu.module.pay.dal.mysql.wallet.PayWalletRechargeMapper;
 import com.citu.module.pay.enums.order.PayOrderStatusEnum;
 import com.citu.module.pay.enums.refund.PayRefundStatusEnum;
 import com.citu.module.pay.enums.wallet.PayWalletBizTypeEnum;
+import com.citu.module.pay.framework.pay.config.PayProperties;
 import com.citu.module.pay.service.order.PayOrderService;
 import com.citu.module.pay.service.refund.PayRefundService;
 import com.citu.module.system.api.social.SocialClientApi;
@@ -53,11 +54,6 @@ import static com.citu.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
     public SocialClientApi socialClientApi;
@@ -71,6 +67,9 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
     private PayRefundService payRefundService;
     @Resource
     private PayWalletRechargePackageService payWalletRechargePackageService;
+    @Resource
+    private PayProperties payProperties;
+
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -93,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())
@@ -113,7 +112,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
 
     @Override
     public PageResult<PayWalletRechargeDO> page(PayWalletRechargeCombinationPageReqVO reqVO) {
-        if (null != reqVO.getUserId() ) {
+        if (null != reqVO.getUserId()) {
             PayWalletDO wallet = payWalletService.getOrCreateWallet(reqVO.getUserId(), UserTypeEnum.MEMBER.getValue());
             reqVO.setWalletId(wallet.getId());
         }
@@ -184,7 +183,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()));

+ 11 - 0
citu-module-pay/citu-module-pay-biz/src/main/java/com/citu/module/pay/service/wallet/PayWalletTransactionServiceImpl.java

@@ -1,5 +1,6 @@
 package com.citu.module.pay.service.wallet;
 
+import cn.hutool.core.util.ObjectUtil;
 import com.citu.framework.common.pojo.PageResult;
 import com.citu.module.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionPageReqVO;
 import com.citu.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO;
@@ -52,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
citu-module-pay/citu-module-pay-biz/src/test/java/com/citu/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
citu-module-pay/citu-module-pay-biz/src/test/java/com/citu/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
citu-module-pay/citu-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
citu-module-pay/citu-spring-boot-starter-biz-pay/src/main/java/com/citu/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java

@@ -26,6 +26,11 @@ public class AlipayPayClientConfig implements PayClientConfig {
      */
     public static final Integer MODE_CERTIFICATE = 2;
 
+    /**
+     * 接口内容加密方式 - AES 加密
+     */
+    public static final String ENC_TYPE_AES = "AES";
+
     /**
      * 签名算法类型 - RSA
      */
@@ -92,6 +97,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 {
     }
 

+ 2 - 0
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/framework/sms/core/client/SmsClient.java

@@ -46,6 +46,8 @@ public interface SmsClient {
     /**
      * 查询指定的短信模板
      *
+     * 如果查询失败,则返回 null 空
+     *
      * @param apiTemplateId 短信 API 的模板编号
      * @return 短信模板
      */

+ 21 - 28
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/framework/sms/core/client/impl/AliyunSmsClient.java

@@ -50,21 +50,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
         Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
     }
 
-    /**
-     * 对指定的字符串进行 URL 编码,并对特定的字符进行替换,以符合URL编码规范
-     *
-     * @param str 需要进行 URL 编码的字符串
-     * @return 编码后的字符串
-     */
-    @SneakyThrows
-    private static String percentCode(String str) {
-        Assert.notNull(str, "str 不能为空");
-        return URLEncoder.encode(str, StandardCharsets.UTF_8.name())
-                .replace("+", "%20") // 加号 "+" 被替换为 "%20"
-                .replace("*", "%2A") // 星号 "*" 被替换为 "%2A"
-                .replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~"
-    }
-
     @Override
     protected void doInit() {
     }
@@ -117,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)) {
@@ -136,24 +119,20 @@ public class AliyunSmsClient extends AbstractSmsClient {
     @VisibleForTesting
     Integer convertSmsTemplateAuditStatus(Integer templateStatus) {
         switch (templateStatus) {
-            case 0:
-                return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
-            case 1:
-                return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
-            case 2:
-                return SmsTemplateAuditStatusEnum.FAIL.getStatus();
-            default:
-                throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
+            case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
+            case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
+            case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
+            default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
         }
     }
 
     /**
      * 请求阿里云短信
      *
-     * @param apiName     请求的 API 名称
+     * @see <a href="https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature">V3 版本请求体&签名机制</>
+     * @param apiName 请求的 API 名称
      * @param queryParams 请求参数
      * @return 请求结果
-     * @see <a href="https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature">V3 版本请求体&签名机制</>
      */
     private JSONObject request(String apiName, TreeMap<String, Object> queryParams) {
         // 1. 请求参数
@@ -189,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()
@@ -200,4 +178,19 @@ public class AliyunSmsClient extends AbstractSmsClient {
         return JSONUtil.parseObj(responseBody);
     }
 
+    /**
+     * 对指定的字符串进行 URL 编码,并对特定的字符进行替换,以符合URL编码规范
+     *
+     * @param str 需要进行 URL 编码的字符串
+     * @return 编码后的字符串
+     */
+    @SneakyThrows
+    private static String percentCode(String str) {
+        Assert.notNull(str, "str 不能为空");
+        return URLEncoder.encode(str, StandardCharsets.UTF_8.name())
+                .replace("+", "%20") // 加号 "+" 被替换为 "%20"
+                .replace("*", "%2A") // 星号 "*" 被替换为 "%2A"
+                .replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~"
+    }
+
 }

+ 35 - 35
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/framework/sms/core/client/impl/HuaweiSmsClient.java

@@ -31,6 +31,7 @@ import static com.citu.framework.common.util.collection.CollectionUtils.convertL
 import static com.citu.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 import static com.citu.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
 
+// todo @scholar:参考阿里云在优化下
 /**
  * 华为短信客户端的实现类
  *
@@ -47,44 +48,20 @@ public class HuaweiSmsClient extends AbstractSmsClient {
     public static final String HOST = "smsapi.cn-north-4.myhuaweicloud.com:443";
     public static final String SIGNEDHEADERS = "content-type;host;x-sdk-date";
 
+    @Override
+    protected void doInit() {
+    }
+
     public HuaweiSmsClient(SmsChannelProperties properties) {
         super(properties);
         Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
         Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
     }
 
-    static String buildRequestBody(String sender, String receiver, String templateId, List<String> templateParas,
-                                   String statusCallBack, String signature) throws UnsupportedEncodingException {
-        if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()
-                || templateId.isEmpty()) {
-            System.out.println("buildRequestBody(): sender, receiver or templateId is null.");
-            return null;
-        }
-
-        StringBuilder body = new StringBuilder();
-        appendToBody(body, "from=", sender);
-        appendToBody(body, "&to=", receiver);
-        appendToBody(body, "&templateId=", templateId);
-        appendToBody(body, "&templateParas=", JsonUtils.toJsonString(templateParas));
-        appendToBody(body, "&statusCallback=", statusCallBack);
-        appendToBody(body, "&signature=", signature);
-        return body.toString();
-    }
-
-    private static void appendToBody(StringBuilder body, String key, String val) throws UnsupportedEncodingException {
-        if (null != val && !val.isEmpty()) {
-            body.append(key).append(URLEncoder.encode(val, "UTF-8"));
-        }
-    }
-
-    @Override
-    protected void doInit() {
-
-    }
-
     @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); //中国大陆短信签名通道号或全球短信通道号
@@ -95,13 +72,13 @@ public class HuaweiSmsClient extends AbstractSmsClient {
 
         List<String> templateParas = CollectionUtils.convertList(templateParams, kv -> String.valueOf(kv.getValue()));
 
-        JSONObject JsonResponse = sendSmsRequest(sender, mobile, templateId, templateParas, statusCallBack);
+        JSONObject JsonResponse = sendSmsRequest(sender,mobile,templateId,templateParas,statusCallBack);
         SmsResponse smsResponse = getSmsSendResponse(JsonResponse);
 
         return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString());
     }
 
-    JSONObject sendSmsRequest(String sender, String mobile, String templateId, List<String> templateParas, String statusCallBack) throws UnsupportedEncodingException {
+    JSONObject sendSmsRequest(String sender,String mobile,String templateId,List<String> templateParas,String statusCallBack) throws UnsupportedEncodingException {
 
         SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.ENGLISH);
         sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
@@ -112,7 +89,7 @@ public class HuaweiSmsClient extends AbstractSmsClient {
         String canonicalUri = "/sms/batchSendSms/v1/";
         String canonicalQueryString = "";//查询参数为空
         String canonicalHeaders = "content-type:application/x-www-form-urlencoded\n"
-                + "host:" + HOST + "\n"
+                + "host:"+ HOST +"\n"
                 + "x-sdk-date:" + sdkDate + "\n";
         //请求Body,不携带签名名称时,signature请填null
         String body = buildRequestBody(sender, mobile, templateId, templateParas, statusCallBack, null);
@@ -138,7 +115,7 @@ public class HuaweiSmsClient extends AbstractSmsClient {
         HttpResponse response = HttpRequest.post(URL)
                 .header("Content-Type", "application/x-www-form-urlencoded")
                 .header("X-Sdk-Date", sdkDate)
-                .header("host", HOST)
+                .header("host",HOST)
                 .header("Authorization", authorization)
                 .body(body)
                 .execute();
@@ -153,10 +130,33 @@ public class HuaweiSmsClient extends AbstractSmsClient {
         return smsResponse;
     }
 
+    static String buildRequestBody(String sender, String receiver, String templateId, List<String> templateParas,
+                                   String statusCallBack, String signature) throws UnsupportedEncodingException {
+        if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty()
+                || templateId.isEmpty()) {
+            System.out.println("buildRequestBody(): sender, receiver or templateId is null.");
+            return null;
+        }
+
+        StringBuilder body = new StringBuilder();
+        appendToBody(body, "from=", sender);
+        appendToBody(body, "&to=", receiver);
+        appendToBody(body, "&templateId=", templateId);
+        appendToBody(body, "&templateParas=", JsonUtils.toJsonString(templateParas));
+        appendToBody(body, "&statusCallback=", statusCallBack);
+        appendToBody(body, "&signature=", signature);
+        return body.toString();
+    }
+
+    private static void appendToBody(StringBuilder body, String key, String val) throws UnsupportedEncodingException {
+        if (null != val && !val.isEmpty()) {
+            body.append(key).append(URLEncoder.encode(val, "UTF-8"));
+        }
+    }
     @Override
     public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
         List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
-        return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(Objects.equals(status.getStatus(), "DELIVRD"))
+        return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(Objects.equals(status.getStatus(),"DELIVRD"))
                 .setErrorCode(status.getStatus()).setErrorMsg(status.getStatus())
                 .setMobile(status.getPhoneNumber()).setReceiveTime(status.getUpdateTime())
                 .setSerialNo(status.getSmsMsgId()));
@@ -188,7 +188,7 @@ public class HuaweiSmsClient extends AbstractSmsClient {
 
     /**
      * 短信接收状态
-     * <p>
+     *
      * 参见 <a href="https://support.huaweicloud.com/api-msgsms/sms_05_0003.html">文档</a>
      *
      * @author scholar

+ 86 - 219
citu-module-system/citu-module-system-biz/src/main/java/com/citu/module/system/framework/sms/core/client/impl/TencentSmsClient.java

@@ -2,38 +2,28 @@ package com.citu.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 com.citu.framework.common.core.KeyValue;
 import com.citu.framework.common.util.collection.ArrayUtils;
-import com.citu.framework.common.util.json.JsonUtils;
+import com.citu.framework.common.util.http.HttpUtils;
 import com.citu.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
 import com.citu.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
 import com.citu.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
 import com.citu.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
 import com.citu.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 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 com.citu.framework.common.util.collection.CollectionUtils.convertList;
-import static com.citu.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
-import static com.citu.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
-
-// TODO @scholar 建议参考 AliyunSmsClient 优化下
 
 /**
  * 腾讯云短信功能实现
@@ -48,7 +38,8 @@ public class TencentSmsClient extends AbstractSmsClient {
      * 调用成功 code
      */
     public static final String API_CODE_SUCCESS = "Ok";
-
+    private static final String VERSION = "2021-01-11";
+    private static final String REGION = "ap-guangzhou";
     /**
      * 是否国际/港澳台短信:
      * <p>
@@ -57,7 +48,6 @@ public class TencentSmsClient extends AbstractSmsClient {
      */
     private static final long INTERNATIONAL_CHINA = 0L;
 
-
     public TencentSmsClient(SmsChannelProperties properties) {
         super(properties);
         Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
@@ -80,7 +70,8 @@ public class TencentSmsClient extends AbstractSmsClient {
         Assert.isTrue(keys.length == 2, "腾讯云短信 apiKey 配置格式错误,请配置 为[secretId sdkAppId]");
     }
 
-    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);
@@ -89,7 +80,6 @@ public class TencentSmsClient extends AbstractSmsClient {
 
     @Override
     protected void doInit() {
-
     }
 
     private String getSdkAppId() {
@@ -103,31 +93,99 @@ 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("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"));
+    }
 
-        JSONObject JsonResponse = sendSmsRequest(body, "SendSms", "2021-01-11", "ap-guangzhou");
-        SmsResponse smsResponse = getSmsSendResponse(JsonResponse);
+    @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")); // 发送序列号
+        });
+    }
+
+    @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));
+        }
+    }
 
+    /**
+     * 请求腾讯云短信
+     *
+     * @param action 请求的 API 名称
+     * @param body   请求参数
+     * @return 请求结果
+     * @see <a href="https://cloud.tencent.com/document/product/382/52072">签名方法 v3</a>
+     */
+    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 = "/";
@@ -137,6 +195,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:拼接待签名字符串 *************
@@ -161,203 +220,11 @@ 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);
-
-        HttpResponse response = HttpRequest.post("https://" + host)
-                .addHeaders(headers)
-                .body(JSONUtil.toJsonStr(body))
-                .execute();
-
-        return JSONUtil.parseObj(response.body());
-    }
-
-    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()));
-    }
+        headers.put("X-TC-Version", VERSION);
+        headers.put("X-TC-Region", REGION);
 
-    @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);
+        String responseBody = HttpUtils.post("https://" + host, headers, JSONUtil.toJsonStr(body));
 
-        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);
+        return JSONUtil.parseObj(responseBody);
     }
-
-    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;
-
-    }
-
 }

+ 1 - 10
citu-module-system/citu-module-system-biz/src/test/java/com/citu/module/system/framework/sms/core/client/impl/AliyunSmsClientTest.java

@@ -24,7 +24,7 @@ import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mockStatic;
 
 /**
- * {@link AliyunSmsClient} 的单元测试
+ * {@link com.citu.module.system.framework.sms.core.client.impl.AliyunSmsClient} 的单元测试
  *
  * @author 芋道源码
  */
@@ -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)) {

+ 60 - 21
citu-module-system/citu-module-system-biz/src/test/java/com/citu/module/system/framework/sms/core/client/impl/SmsClientTests.java

@@ -10,7 +10,6 @@ import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
 import java.util.List;
-
 /**
  * 各种 {@link SmsClientTests  集成测试
  *
@@ -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,11 +40,11 @@ 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")));
@@ -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, ListUtil.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 = ListUtil.of(new KeyValue<>("code", "1024"));
+        // 调用
+        SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
+        // 打印结果
+        System.out.println(smsSendRespDTO);
+    }
+
 }
+

+ 116 - 138
citu-module-system/citu-module-system-biz/src/test/java/com/citu/module/system/framework/sms/core/client/impl/TencentSmsClientTest.java

@@ -1,20 +1,27 @@
 package com.citu.module.system.framework.sms.core.client.impl;
 
-import cn.hutool.core.util.ReflectUtil;
+import com.citu.framework.common.core.KeyValue;
+import com.citu.framework.common.util.http.HttpUtils;
 import com.citu.framework.test.core.ut.BaseMockitoUnitTest;
-import com.citu.module.system.framework.sms.core.client.SmsClient;
 import com.citu.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
+import com.citu.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
+import com.citu.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
 import com.citu.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
 import com.citu.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 com.citu.framework.test.core.util.RandomUtils.randomLongId;
 import static com.citu.framework.test.core.util.RandomUtils.randomString;
 import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mockStatic;
 
 // TODO @芋艿:补全单测
 
@@ -33,115 +40,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() {
         // 准备参数
@@ -157,7 +134,6 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
                 "        \"ext\": {\"logId\":\"67890\"}\n" +
                 "    }\n" +
                 "]";
-        // mock 方法
 
         // 调用
         List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
@@ -165,42 +141,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() {