Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/yudao-cloud
# Conflicts: # yudao-module-mall/yudao-module-promotion-biz/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/combination/AppCombinationRecordController.java # yudao-module-mall/yudao-module-statistics-biz/src/main/java/cn/iocoder/yudao/module/statistics/job/product/ProductStatisticsJob.java # yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/framework/order/config/TradeOrderProperties.java # yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java # yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java # yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java # yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java # yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java # yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/property/SmsChannelProperties.java # yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/SmsClientTests.javapull/132/head
commit
68f6270f6a
|
@ -1961,7 +1961,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
|
|||
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2793, '写作管理', '', 2, 13, 2760, 'write', 'fa:bookmark-o', 'ai/write/manager/index.vue', 'AiWriteManager', 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '1', '2024-07-10 21:31:59', b'0');
|
||||
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2794, 'AI 写作查询', 'ai:write:query', 3, 1, 2793, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '', '2024-07-10 13:24:34', b'0');
|
||||
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2795, 'AI 写作删除', 'ai:write:delete', 3, 4, 2793, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '', '2024-07-10 13:24:34', b'0');
|
||||
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2796, 'AI 音乐', '', 2, 4, 2758, 'music', 'fa:music', 'ai/music/index/index.vue', 'AiWrite', 0, b'1', b'1', b'1', '1', '2024-07-17 09:21:12', '1', '2024-07-17 09:36:12', b'0');
|
||||
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2796, 'AI 音乐', '', 2, 4, 2758, 'music', 'fa:music', 'ai/music/index/index.vue', 'AiMusic', 0, b'1', b'1', b'1', '1', '2024-07-17 09:21:12', '1', '2024-07-17 09:36:12', b'0');
|
||||
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2797, '客服中心', '', 2, 100, 2362, 'kefu', 'fa-solid:user-alt', 'mall/promotion/kefu/index', 'KeFu', 0, b'1', b'1', b'1', '1', '2024-07-17 23:49:05', '1', '2024-07-17 23:49:16', b'0');
|
||||
COMMIT;
|
||||
|
||||
|
|
|
@ -188,7 +188,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
|||
* 可参考 {@link TongYiAutoConfiguration#tongYiChatClient(Generation, TongYiChatProperties, TongYiConnectionProperties)}
|
||||
*/
|
||||
private static TongYiChatModel buildTongYiChatModel(String key) {
|
||||
Generation generation = SpringUtil.getBean(Generation.class);
|
||||
com.alibaba.dashscope.aigc.generation.Generation generation = SpringUtil.getBean(Generation.class);
|
||||
TongYiChatProperties chatOptions = SpringUtil.getBean(TongYiChatProperties.class);
|
||||
// TODO @芋艿:貌似 apiKey 是全局唯一的???得测试下
|
||||
// TODO @芋艿:貌似阿里云不是增量返回的
|
||||
|
|
|
@ -148,5 +148,4 @@ public class AppProductSpuController {
|
|||
return price - newPrice;
|
||||
}
|
||||
|
||||
// TODO 芋艿:商品的浏览记录;
|
||||
}
|
||||
|
|
|
@ -48,6 +48,4 @@ public class ProductBrandDO extends BaseDO {
|
|||
*/
|
||||
private Integer status;
|
||||
|
||||
// TODO 芋艿:firstLetter 首字母
|
||||
|
||||
}
|
||||
|
|
|
@ -130,11 +130,5 @@ public class ProductSkuDO extends BaseDO {
|
|||
|
||||
}
|
||||
|
||||
// TODO 芋艿:integral from y
|
||||
// TODO 芋艿:pinkPrice from y
|
||||
// TODO 芋艿:seckillPrice from y
|
||||
// TODO 芋艿:pinkStock from y
|
||||
// TODO 芋艿:seckillStock from y
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -10,23 +10,20 @@ import cn.iocoder.yudao.module.promotion.controller.app.combination.vo.record.Ap
|
|||
import cn.iocoder.yudao.module.promotion.controller.app.combination.vo.record.AppCombinationRecordSummaryRespVO;
|
||||
import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivityConvert;
|
||||
import cn.iocoder.yudao.module.promotion.dal.dataobject.combination.CombinationRecordDO;
|
||||
import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
|
||||
import cn.iocoder.yudao.module.promotion.service.combination.CombinationRecordService;
|
||||
import cn.iocoder.yudao.module.trade.api.order.TradeOrderApi;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.Max;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
@ -43,9 +40,6 @@ public class AppCombinationRecordController {
|
|||
|
||||
@Resource
|
||||
private CombinationRecordService combinationRecordService;
|
||||
@Resource
|
||||
@Lazy
|
||||
private TradeOrderApi tradeOrderApi;
|
||||
|
||||
@GetMapping("/get-summary")
|
||||
@Operation(summary = "获得拼团记录的概要信息", description = "用于小程序首页")
|
||||
|
@ -117,26 +111,4 @@ public class AppCombinationRecordController {
|
|||
return success(CombinationActivityConvert.INSTANCE.convert(getLoginUserId(), headRecord, memberRecords));
|
||||
}
|
||||
|
||||
@GetMapping("/cancel")
|
||||
@Operation(summary = "取消拼团")
|
||||
@Parameter(name = "id", description = "拼团记录编号", required = true, example = "1024")
|
||||
public CommonResult<Boolean> cancelCombinationRecord(@RequestParam("id") Long id) {
|
||||
Long userId = getLoginUserId();
|
||||
// 1、查找这条拼团记录
|
||||
CombinationRecordDO record = combinationRecordService.getCombinationRecordByIdAndUser(userId, id);
|
||||
if (record == null) {
|
||||
return success(Boolean.FALSE);
|
||||
}
|
||||
// 1.1、需要先校验拼团记录未完成;
|
||||
if (!CombinationRecordStatusEnum.isInProgress(record.getStatus())) {
|
||||
return success(Boolean.FALSE);
|
||||
}
|
||||
|
||||
// 2. 取消已支付的订单
|
||||
tradeOrderApi.cancelPaidOrder(userId, record.getOrderId());
|
||||
// 3. 取消拼团记录
|
||||
combinationRecordService.cancelCombinationRecord(userId, record.getId(), record.getHeadId());
|
||||
return success(Boolean.TRUE);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
/**
|
||||
* 处理过期拼团
|
||||
*
|
||||
|
|
|
@ -37,7 +37,10 @@ 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 cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
|
||||
|
@ -69,7 +72,6 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
|
|||
private ProductSpuApi productSpuApi;
|
||||
@Resource
|
||||
private ProductSkuApi productSkuApi;
|
||||
|
||||
@Resource
|
||||
@Lazy // 延迟加载,避免循环依赖
|
||||
private TradeOrderApi tradeOrderApi;
|
||||
|
@ -289,61 +291,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. 获取所有正在进行中的过期的父拼团
|
||||
|
|
|
@ -7,12 +7,9 @@ import cn.hutool.core.util.StrUtil;
|
|||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
|
||||
import cn.iocoder.yudao.module.statistics.service.product.ProductStatisticsService;
|
||||
import com.xxl.job.core.handler.annotation.XxlJob;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
// TODO 芋艿:缺个 Job 的配置;等和 Product 一起配置
|
||||
|
||||
/**
|
||||
* 商品统计 Job
|
||||
*
|
||||
|
|
|
@ -11,7 +11,6 @@ import org.springframework.stereotype.Component;
|
|||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
// TODO 芋艿:缺个 Job 的配置;等和 Product 一起配置
|
||||
/**
|
||||
* 交易统计 Job
|
||||
*
|
||||
|
|
|
@ -21,6 +21,9 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_
|
|||
@ToString(callSuper = true)
|
||||
public class AfterSalePageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "用户编号", example = "1024")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "售后流水号", example = "202211190847450020500077")
|
||||
private String no;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
/**
|
||||
* TODO 占位
|
||||
*/
|
||||
package cn.iocoder.yudao.module.trade.dal.mysql;
|
|
@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.trade.framework.order.config;
|
|||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
// TODO @LeeYan9: 可以直接给 TradeOrderProperties 一个 @Component生效哈
|
||||
/**
|
||||
* @author LeeYan9
|
||||
* @since 2022-09-15
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package cn.iocoder.yudao.module.trade.framework.order.config;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
|
@ -18,11 +20,15 @@ import java.time.Duration;
|
|||
@Validated
|
||||
public class TradeOrderProperties {
|
||||
|
||||
private static final String PAY_APP_KEY_DEFAULT = "mall";
|
||||
|
||||
/**
|
||||
* 应用编号
|
||||
* 支付应用标识
|
||||
*
|
||||
* 在 pay 模块的 [支付管理 -> 应用信息] 里添加
|
||||
*/
|
||||
@NotNull(message = "应用编号不能为空")
|
||||
private Long appId;
|
||||
@NotEmpty(message = "Pay 应用标识不能为空")
|
||||
private String payAppKey = PAY_APP_KEY_DEFAULT;
|
||||
|
||||
/**
|
||||
* 支付超时时间
|
||||
|
|
|
@ -125,7 +125,6 @@ public class BrokerageUserServiceImpl implements BrokerageUserService {
|
|||
|
||||
@Override
|
||||
public BrokerageUserDO getOrCreateBrokerageUser(Long id) {
|
||||
// TODO @芋艿:这块优化下;统一到注册时处理;
|
||||
BrokerageUserDO brokerageUser = brokerageUserMapper.selectById(id);
|
||||
// 特殊:人人分销的情况下,如果分销人为空则创建分销人
|
||||
if (brokerageUser == null && ObjUtil.equal(BrokerageEnabledConditionEnum.ALL.getCondition(),
|
||||
|
|
|
@ -855,12 +855,14 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
|
|||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void cancelPaidOrder(Long userId, Long orderId) {
|
||||
// TODO 芋艿:这里实现要优化下;
|
||||
// TODO @puhui999:需要校验状态;已支付的情况下,才可以。
|
||||
TradeOrderDO order = tradeOrderMapper.selectOrderByIdAndUserId(orderId, userId);
|
||||
if (order == null) {
|
||||
throw exception(ORDER_NOT_FOUND);
|
||||
}
|
||||
cancelOrder0(order, TradeOrderCancelTypeEnum.MEMBER_CANCEL);
|
||||
|
||||
// TODO @puhui999:需要退款
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package cn.iocoder.yudao.module.trade.service.price.calculator;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
||||
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
|
||||
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
|
||||
|
@ -61,7 +62,7 @@ public class TradePriceCalculatorHelper {
|
|||
orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId())
|
||||
.setDeliveryTemplateId(spu.getDeliveryTemplateId())
|
||||
.setGivePoint(spu.getGiveIntegral()).setUsePoint(0);
|
||||
if (orderItem.getPicUrl() == null) {
|
||||
if (StrUtil.isBlank(orderItem.getPicUrl())) {
|
||||
orderItem.setPicUrl(spu.getPicUrl());
|
||||
}
|
||||
});
|
||||
|
@ -240,7 +241,7 @@ public class TradePriceCalculatorHelper {
|
|||
*
|
||||
* 和 {@link #dividePrice(List, Integer)} 逻辑一致,只是传入的是 TradeOrderItemDO 对象
|
||||
*
|
||||
* @param items 订单项
|
||||
* @param items 订单项
|
||||
* @param price 订单支付金额
|
||||
* @return 分摊金额数组,和传入的 orderItems 一一对应
|
||||
*/
|
||||
|
|
|
@ -125,7 +125,6 @@ yudao:
|
|||
ignore-tables:
|
||||
trade:
|
||||
order:
|
||||
app-id: 1 # 商户编号
|
||||
pay-expire-time: 2h # 支付的过期时间
|
||||
receive-expire-time: 14d # 收货的过期时间
|
||||
comment-expire-time: 7d # 评论的过期时间
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -16,10 +16,10 @@ import javax.validation.constraints.NotNull;
|
|||
public class PayRefundCreateReqDTO {
|
||||
|
||||
/**
|
||||
* 应用编号
|
||||
* 应用标识
|
||||
*/
|
||||
@NotNull(message = "应用编号不能为空")
|
||||
private Long appId;
|
||||
@NotNull(message = "应用标识不能为空")
|
||||
private String appKey;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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, "支付渠道的配置不存在");
|
||||
|
|
|
@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
|
|||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 支付应用信息 Base VO,提供给添加、修改、详细的子 VO 使用
|
||||
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
|
||||
|
@ -15,6 +15,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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.transaction;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||
import cn.iocoder.yudao.framework.common.validation.InEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
|
@ -8,7 +10,14 @@ import lombok.Data;
|
|||
@Data
|
||||
public class PayWalletTransactionPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "钱包编号", example = "1")
|
||||
@Schema(description = "钱包编号", example = "888")
|
||||
private Long walletId;
|
||||
|
||||
@Schema(description = "用户编号", example = "1024")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户类型", example = "1")
|
||||
@InEnum(UserTypeEnum.class)
|
||||
private Integer userType;
|
||||
|
||||
}
|
||||
|
|
|
@ -31,6 +31,10 @@ public class PayAppDO extends BaseDO {
|
|||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
/**
|
||||
* 应用标识
|
||||
*/
|
||||
private String appKey;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -88,13 +88,13 @@ public interface PayAppService {
|
|||
* @return 商户 Map
|
||||
*/
|
||||
default Map<Long, PayAppDO> getAppMap(Collection<Long> ids) {
|
||||
List<PayAppDO> list = getAppList(ids);
|
||||
List<PayAppDO> list = getAppList(ids);
|
||||
return CollectionUtils.convertMap(list, PayAppDO::getId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付应用的合法性
|
||||
*
|
||||
* <p>
|
||||
* 如果不合法,抛出 {@link ServiceException} 业务异常
|
||||
*
|
||||
* @param id 应用编号
|
||||
|
@ -102,4 +102,14 @@ public interface PayAppService {
|
|||
*/
|
||||
PayAppDO validPayApp(Long id);
|
||||
|
||||
/**
|
||||
* 支付应用的合法性
|
||||
* <p>
|
||||
* 如果不合法,抛出 {@link ServiceException} 业务异常
|
||||
*
|
||||
* @param appKey 应用标识
|
||||
* @return 应用
|
||||
*/
|
||||
PayAppDO validPayApp(String appKey);
|
||||
|
||||
}
|
||||
|
|
|
@ -43,6 +43,9 @@ public class PayAppServiceImpl implements PayAppService {
|
|||
|
||||
@Override
|
||||
public Long createApp(PayAppCreateReqVO createReqVO) {
|
||||
// 验证 appKey 是否重复
|
||||
validateAppKeyUnique(null, createReqVO.getAppKey());
|
||||
|
||||
// 插入
|
||||
PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO);
|
||||
appMapper.insert(app);
|
||||
|
@ -54,11 +57,28 @@ public class PayAppServiceImpl implements PayAppService {
|
|||
public void updateApp(PayAppUpdateReqVO updateReqVO) {
|
||||
// 校验存在
|
||||
validateAppExists(updateReqVO.getId());
|
||||
// 验证 appKey 是否重复
|
||||
validateAppKeyUnique(updateReqVO.getId(), updateReqVO.getAppKey());
|
||||
|
||||
// 更新
|
||||
PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO);
|
||||
appMapper.updateById(updateObj);
|
||||
}
|
||||
|
||||
void validateAppKeyUnique(Long id, String appKey) {
|
||||
PayAppDO app = appMapper.selectByAppKey(appKey);
|
||||
if (app == null) {
|
||||
return;
|
||||
}
|
||||
// 如果 id 为空,说明不用比较是否为相同 appKey 的应用
|
||||
if (id == null) {
|
||||
throw exception(APP_KEY_EXISTS);
|
||||
}
|
||||
if (!app.getId().equals(id)) {
|
||||
throw exception(APP_KEY_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAppStatus(Long id, Integer status) {
|
||||
// 校验商户存在
|
||||
|
@ -101,7 +121,7 @@ public class PayAppServiceImpl implements PayAppService {
|
|||
|
||||
@Override
|
||||
public List<PayAppDO> getAppList() {
|
||||
return appMapper.selectList();
|
||||
return appMapper.selectList();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -110,14 +130,30 @@ public class PayAppServiceImpl implements PayAppService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public PayAppDO validPayApp(Long id) {
|
||||
PayAppDO app = appMapper.selectById(id);
|
||||
public PayAppDO validPayApp(Long appId) {
|
||||
PayAppDO app = appMapper.selectById(appId);
|
||||
return validatePayApp(app);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayAppDO validPayApp(String appKey) {
|
||||
PayAppDO app = appMapper.selectByAppKey(appKey);
|
||||
return validatePayApp(app);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验支付应用实体的有效性:存在 + 开启
|
||||
*
|
||||
* @param app 待校验的支付应用实体
|
||||
* @return 校验通过的支付应用实体
|
||||
*/
|
||||
private PayAppDO validatePayApp(PayAppDO app) {
|
||||
// 校验是否存在
|
||||
if (app == null) {
|
||||
throw exception(ErrorCodeConstants.APP_NOT_FOUND);
|
||||
}
|
||||
// 校验是否禁用
|
||||
if (CommonStatusEnum.DISABLE.getStatus().equals(app.getStatus())) {
|
||||
if (CommonStatusEnum.isDisable(app.getStatus())) {
|
||||
throw exception(ErrorCodeConstants.APP_IS_DISABLE);
|
||||
}
|
||||
return app;
|
||||
|
|
|
@ -18,7 +18,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
|
@ -43,11 +43,11 @@ import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
|
|||
public class PayDemoOrderServiceImpl implements PayDemoOrderService {
|
||||
|
||||
/**
|
||||
* 接入的实力应用编号
|
||||
* 接入的支付应用标识
|
||||
*
|
||||
* 从 [支付管理 -> 应用信息] 里添加
|
||||
*/
|
||||
private static final Long PAY_APP_ID = 7L;
|
||||
private static final String PAY_APP_KEY = "demo";
|
||||
|
||||
/**
|
||||
* 商品信息 Map
|
||||
|
@ -88,7 +88,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
|
|||
|
||||
// 2.1 创建支付单
|
||||
Long payOrderId = payOrderApi.createOrder(new PayOrderCreateReqDTO()
|
||||
.setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
|
||||
.setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用
|
||||
.setMerchantOrderId(demoOrder.getId().toString()) // 业务的订单编号
|
||||
.setSubject(spuName).setBody("").setPrice(price) // 价格信息
|
||||
.setExpireTime(addTime(Duration.ofHours(2L)))).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();// 价格信息
|
||||
|
|
|
@ -37,7 +37,7 @@ import org.springframework.stereotype.Service;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
@ -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)); // 理论来说,不会出现这个情况
|
||||
|
|
|
@ -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())) {
|
||||
|
|
|
@ -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())) {
|
||||
|
|
|
@ -18,6 +18,7 @@ import cn.iocoder.yudao.module.pay.dal.mysql.wallet.PayWalletRechargeMapper;
|
|||
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
|
||||
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
|
||||
import cn.iocoder.yudao.module.pay.enums.wallet.PayWalletBizTypeEnum;
|
||||
import cn.iocoder.yudao.module.pay.framework.pay.config.PayProperties;
|
||||
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
|
||||
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
|
||||
import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
|
||||
|
@ -51,11 +52,6 @@ import static cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum.*;
|
|||
@Slf4j
|
||||
public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
|
||||
|
||||
/**
|
||||
* TODO 芋艿:放到 payconfig
|
||||
*/
|
||||
private static final Long WALLET_PAY_APP_ID = 8L;
|
||||
|
||||
private static final String WALLET_RECHARGE_ORDER_SUBJECT = "钱包余额充值";
|
||||
|
||||
@Resource
|
||||
|
@ -68,9 +64,13 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
|
|||
private PayRefundService payRefundService;
|
||||
@Resource
|
||||
private PayWalletRechargePackageService payWalletRechargePackageService;
|
||||
|
||||
@Resource
|
||||
public SocialClientApi socialClientApi;
|
||||
|
||||
@Resource
|
||||
private PayProperties payProperties;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PayWalletRechargeDO createWalletRecharge(Long userId, Integer userType, String userIp,
|
||||
|
@ -92,7 +92,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
|
|||
|
||||
// 2.1 创建支付单
|
||||
Long payOrderId = payOrderService.createOrder(new PayOrderCreateReqDTO()
|
||||
.setAppId(WALLET_PAY_APP_ID).setUserIp(userIp)
|
||||
.setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp)
|
||||
.setMerchantOrderId(recharge.getId().toString()) // 业务的订单编号
|
||||
.setSubject(WALLET_RECHARGE_ORDER_SUBJECT).setBody("")
|
||||
.setPrice(recharge.getPayPrice())
|
||||
|
@ -174,7 +174,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
|
|||
String walletRechargeId = String.valueOf(id);
|
||||
String refundId = walletRechargeId + "-refund";
|
||||
Long payRefundId = payRefundService.createPayRefund(new PayRefundCreateReqDTO()
|
||||
.setAppId(WALLET_PAY_APP_ID).setUserIp(userIp)
|
||||
.setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp)
|
||||
.setMerchantOrderId(walletRechargeId)
|
||||
.setMerchantRefundId(refundId)
|
||||
.setReason("想退钱").setPrice(walletRecharge.getPayPrice()));
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package cn.iocoder.yudao.module.pay.service.wallet;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionPageReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,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,
|
||||
|
|
|
@ -4,9 +4,9 @@ import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
|||
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 支付宝的 PayClientConfig 实现类
|
||||
|
@ -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 {
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,8 @@ public interface SmsClient {
|
|||
/**
|
||||
* 查询指定的短信模板
|
||||
*
|
||||
* 如果查询失败,则返回 null 空
|
||||
*
|
||||
* @param apiTemplateId 短信 API 的模板编号
|
||||
* @return 短信模板
|
||||
*/
|
||||
|
|
|
@ -102,8 +102,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
|
|||
queryParam.put("TemplateCode", apiTemplateId);
|
||||
JSONObject response = request("QuerySmsTemplate", queryParam);
|
||||
|
||||
System.out.println("getSmsTemplate response is =====" + response.toString());
|
||||
|
||||
// 2.1 请求失败
|
||||
String code = response.getStr("Code");
|
||||
if (ObjectUtil.notEqual(code, RESPONSE_CODE_SUCCESS)) {
|
||||
|
@ -170,7 +168,6 @@ public class AliyunSmsClient extends AbstractSmsClient {
|
|||
// 4. 构建 Authorization 签名
|
||||
String canonicalRequest = "POST" + "\n" + "/" + "\n" + queryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
|
||||
String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest);
|
||||
|
||||
String stringToSign = "ACS3-HMAC-SHA256" + "\n" + hashedCanonicalRequest;
|
||||
String signature = SecureUtil.hmacSha256(properties.getApiSecret()).digestHex(stringToSign); // 计算签名
|
||||
headers.put("Authorization", "ACS3-HMAC-SHA256" + " " + "Credential=" + properties.getApiKey()
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
|
||||
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
|
@ -32,7 +31,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
|
|||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
|
||||
|
||||
|
||||
// todo @scholar:参考阿里云在优化下
|
||||
/**
|
||||
* 华为短信客户端的实现类
|
||||
*
|
||||
|
@ -51,7 +50,6 @@ public class HuaweiSmsClient extends AbstractSmsClient {
|
|||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
|
||||
}
|
||||
|
||||
public HuaweiSmsClient(SmsChannelProperties properties) {
|
||||
|
@ -63,6 +61,7 @@ public class HuaweiSmsClient extends AbstractSmsClient {
|
|||
@Override
|
||||
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
|
||||
List<KeyValue<String, Object>> templateParams) throws Throwable {
|
||||
// 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html
|
||||
// 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构
|
||||
// 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。
|
||||
String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号
|
||||
|
|
|
@ -2,38 +2,29 @@ package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
|
|||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import lombok.Data;
|
||||
import jakarta.xml.bind.DatatypeConverter;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.xml.bind.DatatypeConverter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
import static cn.hutool.crypto.digest.DigestUtil.sha256Hex;
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
|
||||
|
||||
// TODO @scholar 建议参考 AliyunSmsClient 优化下
|
||||
/**
|
||||
* 腾讯云短信功能实现
|
||||
*
|
||||
|
@ -43,6 +34,9 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
|
|||
*/
|
||||
public class TencentSmsClient extends AbstractSmsClient {
|
||||
|
||||
private static final String VERSION = "2021-01-11";
|
||||
private static final String REGION = "ap-guangzhou";
|
||||
|
||||
/**
|
||||
* 调用成功 code
|
||||
*/
|
||||
|
@ -56,7 +50,6 @@ public class TencentSmsClient extends AbstractSmsClient {
|
|||
*/
|
||||
private static final long INTERNATIONAL_CHINA = 0L;
|
||||
|
||||
|
||||
public TencentSmsClient(SmsChannelProperties properties) {
|
||||
super(properties);
|
||||
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
|
||||
|
@ -65,7 +58,6 @@ public class TencentSmsClient extends AbstractSmsClient {
|
|||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -95,31 +87,96 @@ public class TencentSmsClient extends AbstractSmsClient {
|
|||
@Override
|
||||
public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
|
||||
String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
|
||||
// 构建请求
|
||||
// 1. 执行请求
|
||||
// 参考链接 https://cloud.tencent.com/document/product/382/55981
|
||||
TreeMap<String, Object> body = new TreeMap<>();
|
||||
String[] phones = {mobile};
|
||||
body.put("PhoneNumberSet",phones);
|
||||
body.put("SmsSdkAppId",getSdkAppId());
|
||||
body.put("SignName",properties.getSignature());
|
||||
body.put("PhoneNumberSet", new String[]{mobile});
|
||||
body.put("SmsSdkAppId", getSdkAppId());
|
||||
body.put("SignName", properties.getSignature());
|
||||
body.put("TemplateId",apiTemplateId);
|
||||
body.put("TemplateParamSet",ArrayUtils.toArray(templateParams, e -> String.valueOf(e.getValue())));
|
||||
|
||||
JSONObject JsonResponse = sendSmsRequest(body,"SendSms","2021-01-11","ap-guangzhou");
|
||||
SmsResponse smsResponse = getSmsSendResponse(JsonResponse);
|
||||
|
||||
return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString());
|
||||
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 sendSmsRequest(TreeMap<String, Object> body,String action,String version,String region) throws Exception {
|
||||
@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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Integer convertSmsTemplateAuditStatus(int templateStatus) {
|
||||
switch (templateStatus) {
|
||||
case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
|
||||
case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
|
||||
case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
|
||||
default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求腾讯云短信
|
||||
*
|
||||
* @see <a href="https://cloud.tencent.com/document/product/382/52072">签名方法 v3</a>
|
||||
*
|
||||
* @param action 请求的 API 名称
|
||||
* @param body 请求参数
|
||||
* @return 请求结果
|
||||
*/
|
||||
private JSONObject request(String action, TreeMap<String, Object> body) throws Exception {
|
||||
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
|
||||
// TODO @scholar:这个 format,看看怎么写的可以简化点
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
||||
// 注意时区,否则容易出错
|
||||
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
|
||||
|
||||
// TODO @scholar:这个步骤,看看怎么参考阿里云 client,归类下;1. 2.1 2.2 这种
|
||||
// ************* 步骤 1:拼接规范请求串 *************
|
||||
// TODO @scholar:这个 hsot 枚举下;
|
||||
String host = "sms.tencentcloudapi.com"; //APP接入地址+接口访问URI
|
||||
String httpMethod = "POST"; // 请求方式
|
||||
String canonicalUri = "/";
|
||||
|
@ -129,6 +186,7 @@ public class TencentSmsClient extends AbstractSmsClient {
|
|||
+ "host:" + host + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";
|
||||
String signedHeaders = "content-type;host;x-tc-action";
|
||||
String hashedRequestBody = sha256Hex(JSONUtil.toJsonStr(body));
|
||||
// TODO @scholar:换行下,不然单行太长了
|
||||
String canonicalRequest = httpMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
|
||||
|
||||
// ************* 步骤 2:拼接待签名字符串 *************
|
||||
|
@ -153,205 +211,19 @@ public class TencentSmsClient extends AbstractSmsClient {
|
|||
headers.put("Host", host);
|
||||
headers.put("X-TC-Action", action);
|
||||
headers.put("X-TC-Timestamp", timestamp);
|
||||
headers.put("X-TC-Version", version);
|
||||
headers.put("X-TC-Region", region);
|
||||
headers.put("X-TC-Version", VERSION);
|
||||
headers.put("X-TC-Region", REGION);
|
||||
|
||||
HttpResponse response = HttpRequest.post("https://"+host)
|
||||
.addHeaders(headers)
|
||||
.body(JSONUtil.toJsonStr(body))
|
||||
.execute();
|
||||
String responseBody = HttpUtils.post("https://" + host, headers, JSONUtil.toJsonStr(body));
|
||||
|
||||
return JSONUtil.parseObj(response.body());
|
||||
return JSONUtil.parseObj(responseBody);
|
||||
}
|
||||
|
||||
public static byte[] hmac256(byte[] key, String msg) throws Exception {
|
||||
// TODO @scholar:使用 hutool 简化下
|
||||
private static byte[] hmac256(byte[] key, String msg) throws Exception {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
|
||||
mac.init(secretKeySpec);
|
||||
return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private SmsResponse getSmsSendResponse(JSONObject resJson) {
|
||||
SmsResponse smsResponse = new SmsResponse();
|
||||
JSONArray statusJson =resJson.getJSONObject("Response").getJSONArray("SendStatusSet");
|
||||
smsResponse.setSuccess("Ok".equals(statusJson.getJSONObject(0).getStr("Code")));
|
||||
smsResponse.setData(resJson);
|
||||
return smsResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
|
||||
List<SmsReceiveStatus> callback = JsonUtils.parseArray(text, SmsReceiveStatus.class);
|
||||
return convertList(callback, status -> new SmsReceiveRespDTO()
|
||||
.setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus()))
|
||||
.setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription())
|
||||
.setMobile(status.getMobile()).setReceiveTime(status.getReceiveTime())
|
||||
.setSerialNo(status.getSerialNo()).setLogId(status.getSessionContext().getLogId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
|
||||
|
||||
// 构建请求
|
||||
TreeMap<String, Object> body = new TreeMap<>();
|
||||
body.put("International",0);
|
||||
Integer[] templateIds = {Integer.valueOf(apiTemplateId)};
|
||||
body.put("TemplateIdSet",templateIds);
|
||||
|
||||
JSONObject JsonResponse = sendSmsRequest(body,"DescribeSmsTemplateList","2021-01-11","ap-guangzhou");
|
||||
QuerySmsTemplateResponse smsTemplateResponse = getSmsTemplateResponse(JsonResponse);
|
||||
String templateId = Integer.toString(smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getTemplateId());
|
||||
String content = smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getTemplateContent();
|
||||
Integer templateStatus = smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getStatusCode();
|
||||
String auditReason = smsTemplateResponse.getDescribeTemplateStatusSet().get(0).getReviewReply();
|
||||
|
||||
return new SmsTemplateRespDTO().setId(templateId).setContent(content)
|
||||
.setAuditStatus(convertSmsTemplateAuditStatus(templateStatus)).setAuditReason(auditReason);
|
||||
}
|
||||
|
||||
private QuerySmsTemplateResponse getSmsTemplateResponse(JSONObject resJson) {
|
||||
|
||||
QuerySmsTemplateResponse smsTemplateResponse = new QuerySmsTemplateResponse();
|
||||
|
||||
smsTemplateResponse.setRequestId(resJson.getJSONObject("Response").getStr("RequestId"));
|
||||
|
||||
smsTemplateResponse.setDescribeTemplateStatusSet(new ArrayList<>());
|
||||
|
||||
QuerySmsTemplateResponse.TemplateInfo templateInfo = new QuerySmsTemplateResponse.TemplateInfo();
|
||||
|
||||
Object statusObject = resJson.getJSONObject("Response").getJSONArray("DescribeTemplateStatusSet").get(0);
|
||||
|
||||
JSONObject statusJSON = new JSONObject(statusObject);
|
||||
|
||||
templateInfo.setTemplateContent(statusJSON.get("TemplateContent").toString());
|
||||
|
||||
templateInfo.setStatusCode(Integer.parseInt(statusJSON.get("StatusCode").toString()));
|
||||
|
||||
templateInfo.setReviewReply(statusJSON.get("ReviewReply").toString());
|
||||
|
||||
templateInfo.setTemplateId(Integer.parseInt(statusJSON.get("TemplateId").toString()));
|
||||
|
||||
smsTemplateResponse.getDescribeTemplateStatusSet().add(templateInfo);
|
||||
|
||||
return smsTemplateResponse;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Integer convertSmsTemplateAuditStatus(int templateStatus) {
|
||||
switch (templateStatus) {
|
||||
case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
|
||||
case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
|
||||
case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
|
||||
default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class SmsResponse {
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
private boolean success;
|
||||
|
||||
/**
|
||||
* 厂商原返回体
|
||||
*/
|
||||
private Object data;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <p>类名: QuerySmsTemplateResponse
|
||||
* <p>说明: sms模板查询返回信息
|
||||
*
|
||||
* @author :scholar
|
||||
* 2024/07/17 0:25
|
||||
**/
|
||||
@Data
|
||||
public static class QuerySmsTemplateResponse {
|
||||
private List<TemplateInfo> DescribeTemplateStatusSet;
|
||||
private String RequestId;
|
||||
@Data
|
||||
static class TemplateInfo {
|
||||
private String TemplateName;
|
||||
private Integer TemplateId;
|
||||
private Integer International;
|
||||
private String ReviewReply;
|
||||
private long CreateTime;
|
||||
private String TemplateContent;
|
||||
private Integer StatusCode;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class SmsReceiveStatus {
|
||||
|
||||
/**
|
||||
* 短信接受成功 code
|
||||
*/
|
||||
public static final String SUCCESS_CODE = "SUCCESS";
|
||||
|
||||
/**
|
||||
* 用户实际接收到短信的时间
|
||||
*/
|
||||
@JsonProperty("user_receive_time")
|
||||
@JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT)
|
||||
private LocalDateTime receiveTime;
|
||||
|
||||
/**
|
||||
* 国家(或地区)码
|
||||
*/
|
||||
@JsonProperty("nationcode")
|
||||
private String nationCode;
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
*/
|
||||
private String mobile;
|
||||
|
||||
/**
|
||||
* 实际是否收到短信接收状态,SUCCESS(成功)、FAIL(失败)
|
||||
*/
|
||||
@JsonProperty("report_status")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 用户接收短信状态码错误信息
|
||||
*/
|
||||
@JsonProperty("errmsg")
|
||||
private String errCode;
|
||||
|
||||
/**
|
||||
* 用户接收短信状态描述
|
||||
*/
|
||||
@JsonProperty("description")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 本次发送标识 ID(与发送接口返回的SerialNo对应)
|
||||
*/
|
||||
@JsonProperty("sid")
|
||||
private String serialNo;
|
||||
|
||||
/**
|
||||
* 用户的 session 内容(与发送接口的请求参数 SessionContext 一致)
|
||||
*/
|
||||
@JsonProperty("ext")
|
||||
private SessionContext sessionContext;
|
||||
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@Data
|
||||
static class SessionContext {
|
||||
|
||||
/**
|
||||
* 发送短信记录id
|
||||
*/
|
||||
private Long logId;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,12 +1,11 @@
|
|||
package cn.iocoder.yudao.module.system.framework.sms.core.property;
|
||||
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 短信渠道配置类
|
||||
*
|
||||
|
|
|
@ -24,7 +24,7 @@ import static org.mockito.ArgumentMatchers.anyString;
|
|||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
/**
|
||||
* {@link AliyunSmsClient} 的单元测试
|
||||
* {@link cn.iocoder.yudao.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)) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
|
||||
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
|
||||
|
@ -18,24 +17,6 @@ import java.util.List;
|
|||
*/
|
||||
public class SmsClientTests {
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testHuaweiSmsClient_sendSms() throws Throwable {
|
||||
SmsChannelProperties properties = new SmsChannelProperties()
|
||||
.setApiKey("123")
|
||||
.setApiSecret("456");
|
||||
HuaweiSmsClient client = new HuaweiSmsClient(properties);
|
||||
// 准备参数
|
||||
Long sendLogId = System.currentTimeMillis();
|
||||
String mobile = "15601691323";
|
||||
String apiTemplateId = "xx test01";
|
||||
List<KeyValue<String, Object>> templateParams = ListUtil.of(new KeyValue<>("code", "1024"));
|
||||
// 调用
|
||||
SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
|
||||
// 打印结果
|
||||
System.out.println(smsSendRespDTO);
|
||||
}
|
||||
|
||||
// ========== 阿里云 ==========
|
||||
|
||||
@Test
|
||||
|
@ -59,14 +40,14 @@ public class SmsClientTests {
|
|||
SmsChannelProperties properties = new SmsChannelProperties()
|
||||
.setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")
|
||||
.setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")
|
||||
.setSignature("Ballcat");
|
||||
.setSignature("runpu");
|
||||
AliyunSmsClient client = new AliyunSmsClient(properties);
|
||||
// 准备参数
|
||||
Long sendLogId = System.currentTimeMillis();
|
||||
String mobile = "173213154791";
|
||||
String mobile = "15601691323";
|
||||
String apiTemplateId = "SMS_207945135";
|
||||
// 调用
|
||||
SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, ListUtil.of(new KeyValue<>("code", "1024")));
|
||||
SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024")));
|
||||
// 打印结果
|
||||
System.out.println(sendRespDTO);
|
||||
}
|
||||
|
@ -100,4 +81,62 @@ public class SmsClientTests {
|
|||
System.out.println(statuses);
|
||||
}
|
||||
|
||||
// ========== 腾讯云 ==========
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testTencentSmsClient_sendSms() throws Throwable {
|
||||
SmsChannelProperties properties = new SmsChannelProperties()
|
||||
.setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523")
|
||||
.setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")
|
||||
.setSignature("芋道源码");
|
||||
TencentSmsClient client = new TencentSmsClient(properties);
|
||||
// 准备参数
|
||||
Long sendLogId = System.currentTimeMillis();
|
||||
String mobile = "15601691323";
|
||||
String apiTemplateId = "2136358";
|
||||
// 调用
|
||||
SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024")));
|
||||
// 打印结果
|
||||
System.out.println(sendRespDTO);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testTencentSmsClient_getSmsTemplate() throws Throwable {
|
||||
SmsChannelProperties properties = new SmsChannelProperties()
|
||||
.setApiKey("LTAI5tAicJAxaSFiZuGGeXHR 1428926523")
|
||||
.setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")
|
||||
.setSignature("芋道源码");
|
||||
TencentSmsClient client = new TencentSmsClient(properties);
|
||||
// 准备参数
|
||||
String apiTemplateId = "2136358";
|
||||
// 调用
|
||||
SmsTemplateRespDTO template = client.getSmsTemplate(apiTemplateId);
|
||||
// 打印结果
|
||||
System.out.println(template);
|
||||
}
|
||||
|
||||
// ========== 华为云 ==========
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testHuaweiSmsClient_sendSms() throws Throwable {
|
||||
SmsChannelProperties properties = new SmsChannelProperties()
|
||||
.setApiKey("123")
|
||||
.setApiSecret("456")
|
||||
.setSignature("runpu");
|
||||
HuaweiSmsClient client = new HuaweiSmsClient(properties);
|
||||
// 准备参数
|
||||
Long sendLogId = System.currentTimeMillis();
|
||||
String mobile = "15601691323";
|
||||
String apiTemplateId = "xx test01";
|
||||
List<KeyValue<String, Object>> templateParams = List.of(new KeyValue<>("code", "1024"));
|
||||
// 调用
|
||||
SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
|
||||
// 打印结果
|
||||
System.out.println(smsSendRespDTO);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,28 @@
|
|||
package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
|
||||
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.SmsClient;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
|
||||
import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
|
||||
import static cn.iocoder.yudao.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 @芋艿:补全单测
|
||||
/**
|
||||
* {@link TencentSmsClient} 的单元测试
|
||||
*
|
||||
|
@ -32,115 +38,85 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
|
|||
@InjectMocks
|
||||
private TencentSmsClient smsClient = new TencentSmsClient(properties);
|
||||
|
||||
@Mock
|
||||
private SmsClient client;
|
||||
|
||||
@Test
|
||||
public void testDoInit() {
|
||||
// 准备参数
|
||||
// mock 方法
|
||||
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" +
|
||||
"}");
|
||||
|
||||
// 调用
|
||||
smsClient.doInit();
|
||||
// 断言
|
||||
assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client"));
|
||||
// 调用
|
||||
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
|
||||
apiTemplateId, templateParams);
|
||||
// 断言
|
||||
assertTrue(result.getSuccess());
|
||||
assertEquals("5000:1045710669157053657849499619", result.getSerialNo());
|
||||
assertEquals("a0aabda6-cf91-4f3e-a81f-9198114a2279", result.getApiRequestId());
|
||||
assertEquals("send success", result.getApiMsg());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRefresh() {
|
||||
// 准备参数
|
||||
SmsChannelProperties p = new SmsChannelProperties()
|
||||
.setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错
|
||||
.setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错
|
||||
.setSignature("芋道源码");
|
||||
// 调用
|
||||
smsClient.refresh(p);
|
||||
// 断言
|
||||
assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client"));
|
||||
public void testDoSendSms_fail() throws Throwable {
|
||||
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
|
||||
// 准备参数
|
||||
Long sendLogId = randomLongId();
|
||||
String mobile = randomString();
|
||||
String apiTemplateId = randomString();
|
||||
List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
|
||||
new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
|
||||
|
||||
// mock 方法
|
||||
httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
|
||||
.thenReturn("{\n" +
|
||||
" \"Response\": {\n" +
|
||||
" \"SendStatusSet\": [\n" +
|
||||
" {\n" +
|
||||
" \"SerialNo\": \"5000:1045710669157053657849499619\",\n" +
|
||||
" \"PhoneNumber\": \"+8618511122233\",\n" +
|
||||
" \"Fee\": 1,\n" +
|
||||
" \"SessionContext\": \"test\",\n" +
|
||||
" \"Code\": \"ERROR\",\n" +
|
||||
" \"Message\": \"send success\",\n" +
|
||||
" \"IsoCode\": \"CN\"\n" +
|
||||
" },\n" +
|
||||
" ],\n" +
|
||||
" \"RequestId\": \"a0aabda6-cf91-4f3e-a81f-9198114a2279\"\n" +
|
||||
" }\n" +
|
||||
"}");
|
||||
|
||||
// 调用
|
||||
SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
|
||||
apiTemplateId, templateParams);
|
||||
// 断言
|
||||
assertFalse(result.getSuccess());
|
||||
assertEquals("5000:1045710669157053657849499619", result.getSerialNo());
|
||||
assertEquals("a0aabda6-cf91-4f3e-a81f-9198114a2279", result.getApiRequestId());
|
||||
assertEquals("send success", result.getApiMsg());
|
||||
}
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void testDoSendSms_success() throws Throwable {
|
||||
// // 准备参数
|
||||
// Long sendLogId = randomLongId();
|
||||
// String mobile = randomString();
|
||||
// String apiTemplateId = randomString();
|
||||
// List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
|
||||
// new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
|
||||
// String requestId = randomString();
|
||||
// String serialNo = randomString();
|
||||
// // mock 方法
|
||||
// SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> {
|
||||
// o.setRequestId(requestId);
|
||||
// SendStatus[] sendStatuses = new SendStatus[1];
|
||||
// o.setSendStatusSet(sendStatuses);
|
||||
// SendStatus sendStatus = new SendStatus();
|
||||
// sendStatuses[0] = sendStatus;
|
||||
// sendStatus.setCode(TencentSmsClient.API_CODE_SUCCESS);
|
||||
// sendStatus.setMessage("send success");
|
||||
// sendStatus.setSerialNo(serialNo);
|
||||
// });
|
||||
// when(client.SendSms(argThat(request -> {
|
||||
// assertEquals(mobile, request.getPhoneNumberSet()[0]);
|
||||
// assertEquals(properties.getSignature(), request.getSignName());
|
||||
// assertEquals(apiTemplateId, request.getTemplateId());
|
||||
// assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)),
|
||||
// toJsonString(request.getTemplateParamSet()));
|
||||
// assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId"));
|
||||
// return true;
|
||||
// }))).thenReturn(response);
|
||||
//
|
||||
// // 调用
|
||||
// SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
|
||||
// // 断言
|
||||
// assertTrue(result.getSuccess());
|
||||
// assertEquals(response.getRequestId(), result.getApiRequestId());
|
||||
// assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode());
|
||||
// assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg());
|
||||
// assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo());
|
||||
// }
|
||||
|
||||
// @Test
|
||||
// public void testDoSendSms_fail() throws Throwable {
|
||||
// // 准备参数
|
||||
// Long sendLogId = randomLongId();
|
||||
// String mobile = randomString();
|
||||
// String apiTemplateId = randomString();
|
||||
// List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
|
||||
// new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
|
||||
// String requestId = randomString();
|
||||
// String serialNo = randomString();
|
||||
// // mock 方法
|
||||
// SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> {
|
||||
// o.setRequestId(requestId);
|
||||
// SendStatus[] sendStatuses = new SendStatus[1];
|
||||
// o.setSendStatusSet(sendStatuses);
|
||||
// SendStatus sendStatus = new SendStatus();
|
||||
// sendStatuses[0] = sendStatus;
|
||||
// sendStatus.setCode("ERROR");
|
||||
// sendStatus.setMessage("send success");
|
||||
// sendStatus.setSerialNo(serialNo);
|
||||
// });
|
||||
// when(client.SendSms(argThat(request -> {
|
||||
// assertEquals(mobile, request.getPhoneNumberSet()[0]);
|
||||
// assertEquals(properties.getSignature(), request.getSignName());
|
||||
// assertEquals(apiTemplateId, request.getTemplateId());
|
||||
// assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)),
|
||||
// toJsonString(request.getTemplateParamSet()));
|
||||
// assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId"));
|
||||
// return true;
|
||||
// }))).thenReturn(response);
|
||||
//
|
||||
// // 调用
|
||||
// SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
|
||||
// // 断言
|
||||
// assertFalse(result.getSuccess());
|
||||
// assertEquals(response.getRequestId(), result.getApiRequestId());
|
||||
// assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode());
|
||||
// assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg());
|
||||
// assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo());
|
||||
// }
|
||||
|
||||
@Test
|
||||
public void testParseSmsReceiveStatus() {
|
||||
// 准备参数
|
||||
|
@ -156,7 +132,6 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
|
|||
" \"ext\": {\"logId\":\"67890\"}\n" +
|
||||
" }\n" +
|
||||
"]";
|
||||
// mock 方法
|
||||
|
||||
// 调用
|
||||
List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
|
||||
|
@ -164,42 +139,44 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
|
|||
assertEquals(1, statuses.size());
|
||||
assertTrue(statuses.get(0).getSuccess());
|
||||
assertEquals("DELIVRD", statuses.get(0).getErrorCode());
|
||||
assertEquals("用户短信送达成功", statuses.get(0).getErrorMsg());
|
||||
assertEquals("13900000001", statuses.get(0).getMobile());
|
||||
assertEquals(LocalDateTime.of(2015, 10, 17, 8, 3, 4), statuses.get(0).getReceiveTime());
|
||||
assertEquals("12345", statuses.get(0).getSerialNo());
|
||||
assertEquals(67890L, statuses.get(0).getLogId());
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void testGetSmsTemplate() throws Throwable {
|
||||
// // 准备参数
|
||||
// Long apiTemplateId = randomLongId();
|
||||
// String requestId = randomString();
|
||||
//
|
||||
// // mock 方法
|
||||
// DescribeSmsTemplateListResponse response = randomPojo(DescribeSmsTemplateListResponse.class, o -> {
|
||||
// DescribeTemplateListStatus[] describeTemplateListStatuses = new DescribeTemplateListStatus[1];
|
||||
// DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus();
|
||||
// templateStatus.setTemplateId(apiTemplateId);
|
||||
// templateStatus.setStatusCode(0L);// 设置模板通过
|
||||
// describeTemplateListStatuses[0] = templateStatus;
|
||||
// o.setDescribeTemplateStatusSet(describeTemplateListStatuses);
|
||||
// o.setRequestId(requestId);
|
||||
// });
|
||||
// when(client.DescribeSmsTemplateList(argThat(request -> {
|
||||
// assertEquals(apiTemplateId, request.getTemplateIdSet()[0]);
|
||||
// return true;
|
||||
// }))).thenReturn(response);
|
||||
//
|
||||
// // 调用
|
||||
// SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId.toString());
|
||||
// // 断言
|
||||
// assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getId());
|
||||
// assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getContent());
|
||||
// assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
|
||||
// assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getAuditReason());
|
||||
// }
|
||||
@Test
|
||||
public void testGetSmsTemplate() throws Throwable {
|
||||
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
|
||||
// 准备参数
|
||||
String apiTemplateId = "1122";
|
||||
|
||||
// mock 方法
|
||||
httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
|
||||
.thenReturn("{ \"Response\": {\n" +
|
||||
" \"DescribeTemplateStatusSet\": [\n" +
|
||||
" {\n" +
|
||||
" \"TemplateName\": \"验证码\",\n" +
|
||||
" \"TemplateId\": 1122,\n" +
|
||||
" \"International\": 0,\n" +
|
||||
" \"ReviewReply\": \"审批备注\",\n" +
|
||||
" \"CreateTime\": 1617379200,\n" +
|
||||
" \"TemplateContent\": \"您的验证码是{1}\",\n" +
|
||||
" \"StatusCode\": 0\n" +
|
||||
" },\n" +
|
||||
" \n" +
|
||||
" ],\n" +
|
||||
" \"RequestId\": \"f36e4f00-605e-49b1-ad0d-bfaba81c7325\"\n" +
|
||||
" }}");
|
||||
|
||||
// 调用
|
||||
SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId);
|
||||
// 断言
|
||||
assertEquals("1122", result.getId());
|
||||
assertEquals("您的验证码是{1}", result.getContent());
|
||||
assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
|
||||
assertEquals("审批备注", result.getAuditReason());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConvertSmsTemplateAuditStatus() {
|
||||
|
|
Loading…
Reference in New Issue