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

【更多】同步 boot 最新代码到 cloud
pull/132/head
YunaiV 2024-08-18 17:18:03 +08:00
parent 720b426f5e
commit 4ca68ff56a
52 changed files with 484 additions and 591 deletions

View File

@ -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 (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 (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 (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'); 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; COMMIT;

View File

@ -188,7 +188,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
* {@link TongYiAutoConfiguration#tongYiChatClient(Generation, TongYiChatProperties, TongYiConnectionProperties)} * {@link TongYiAutoConfiguration#tongYiChatClient(Generation, TongYiChatProperties, TongYiConnectionProperties)}
*/ */
private static TongYiChatModel buildTongYiChatModel(String key) { 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); TongYiChatProperties chatOptions = SpringUtil.getBean(TongYiChatProperties.class);
// TODO @芋艿:貌似 apiKey 是全局唯一的???得测试下 // TODO @芋艿:貌似 apiKey 是全局唯一的???得测试下
// TODO @芋艿:貌似阿里云不是增量返回的 // TODO @芋艿:貌似阿里云不是增量返回的

View File

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

View File

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

View File

@ -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
} }

View File

@ -10,9 +10,7 @@ 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.controller.app.combination.vo.record.AppCombinationRecordSummaryRespVO;
import cn.iocoder.yudao.module.promotion.convert.combination.CombinationActivityConvert; 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.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.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.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.Parameters;
@ -20,7 +18,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Max;
import org.springframework.context.annotation.Lazy;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -43,9 +40,6 @@ public class AppCombinationRecordController {
@Resource @Resource
private CombinationRecordService combinationRecordService; private CombinationRecordService combinationRecordService;
@Resource
@Lazy
private TradeOrderApi tradeOrderApi;
@GetMapping("/get-summary") @GetMapping("/get-summary")
@Operation(summary = "获得拼团记录的概要信息", description = "用于小程序首页") @Operation(summary = "获得拼团记录的概要信息", description = "用于小程序首页")
@ -117,26 +111,4 @@ public class AppCombinationRecordController {
return success(CombinationActivityConvert.INSTANCE.convert(getLoginUserId(), headRecord, memberRecords)); 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);
}
} }

View File

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

View File

@ -139,24 +139,6 @@ public interface CombinationRecordService {
@Nullable Integer status, @Nullable Integer status,
@Nullable Long headId); @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);
/** /**
* *
* *

View File

@ -37,7 +37,10 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime; 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.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
@ -69,7 +72,6 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
private ProductSpuApi productSpuApi; private ProductSpuApi productSpuApi;
@Resource @Resource
private ProductSkuApi productSkuApi; private ProductSkuApi productSkuApi;
@Resource @Resource
@Lazy // 延迟加载,避免循环依赖 @Lazy // 延迟加载,避免循环依赖
private TradeOrderApi tradeOrderApi; private TradeOrderApi tradeOrderApi;
@ -289,61 +291,6 @@ public class CombinationRecordServiceImpl implements CombinationRecordService {
return combinationRecordMapper.selectCombinationRecordCountMapByActivityIdAndStatusAndHeadId(activityIds, status, headId); 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 @Override
public KeyValue<Integer, Integer> expireCombinationRecord() { public KeyValue<Integer, Integer> expireCombinationRecord() {
// 1. 获取所有正在进行中的过期的父拼团 // 1. 获取所有正在进行中的过期的父拼团

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.trade.service.price.calculator; package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.lang.Assert; 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.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO; import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO; import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
@ -61,7 +62,7 @@ public class TradePriceCalculatorHelper {
orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId()) orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId())
.setDeliveryTemplateId(spu.getDeliveryTemplateId()) .setDeliveryTemplateId(spu.getDeliveryTemplateId())
.setGivePoint(spu.getGiveIntegral()).setUsePoint(0); .setGivePoint(spu.getGiveIntegral()).setUsePoint(0);
if (orderItem.getPicUrl() == null) { if (StrUtil.isBlank(orderItem.getPicUrl())) {
orderItem.setPicUrl(spu.getPicUrl()); orderItem.setPicUrl(spu.getPicUrl());
} }
}); });

View File

@ -125,7 +125,6 @@ yudao:
ignore-tables: ignore-tables:
trade: trade:
order: order:
app-id: 1 # 商户编号
pay-expire-time: 2h # 支付的过期时间 pay-expire-time: 2h # 支付的过期时间
receive-expire-time: 14d # 收货的过期时间 receive-expire-time: 14d # 收货的过期时间
comment-expire-time: 7d # 评论的过期时间 comment-expire-time: 7d # 评论的过期时间

View File

@ -18,10 +18,10 @@ public class PayOrderCreateReqDTO implements Serializable {
public static final int SUBJECT_MAX_LENGTH = 32; public static final int SUBJECT_MAX_LENGTH = 32;
/** /**
* *
*/ */
@NotNull(message = "应用编号不能为空") @NotNull(message = "应用标识不能为空")
private Long appId; private String appKey;
/** /**
* IP * IP
*/ */

View File

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

View File

@ -19,10 +19,10 @@ import java.util.Map;
public class PayTransferCreateReqDTO { public class PayTransferCreateReqDTO {
/** /**
* *
*/ */
@NotNull(message = "应用编号不能为空") @NotNull(message = "应用标识不能为空")
private Long appId; private String appKey;
@NotEmpty(message = "转账渠道不能为空") @NotEmpty(message = "转账渠道不能为空")
private String channelCode; private String channelCode;

View File

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

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.pay.controller.admin.app.vo;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.framework.common.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import org.hibernate.validator.constraints.URL; import org.hibernate.validator.constraints.URL;
@ -14,6 +15,10 @@ import org.hibernate.validator.constraints.URL;
@Data @Data
public class PayAppBaseVO { public class PayAppBaseVO {
@Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
@NotEmpty(message = "应用标识不能为空")
private String appKey;
@Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "小豆") @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "小豆")
@NotNull(message = "应用名不能为空") @NotNull(message = "应用名不能为空")
private String name; private String name;

View File

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

View File

@ -16,6 +16,9 @@ public class PayAppRespVO extends PayAppBaseVO {
@Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id; private Long id;
@Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
private String appKey;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime; private LocalDateTime createTime;

View File

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

View File

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

View File

@ -13,9 +13,14 @@ public interface PayAppMapper extends BaseMapperX<PayAppDO> {
default PageResult<PayAppDO> selectPage(PayAppPageReqVO reqVO) { default PageResult<PayAppDO> selectPage(PayAppPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<PayAppDO>() return selectPage(reqVO, new LambdaQueryWrapperX<PayAppDO>()
.likeIfPresent(PayAppDO::getName, reqVO.getName()) .likeIfPresent(PayAppDO::getName, reqVO.getName())
.likeIfPresent(PayAppDO::getAppKey, reqVO.getAppKey())
.eqIfPresent(PayAppDO::getStatus, reqVO.getStatus()) .eqIfPresent(PayAppDO::getStatus, reqVO.getStatus())
.betweenIfPresent(PayAppDO::getCreateTime, reqVO.getCreateTime()) .betweenIfPresent(PayAppDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(PayAppDO::getId)); .orderByDesc(PayAppDO::getId));
} }
default PayAppDO selectByAppKey(String appKey) {
return selectOne(PayAppDO::getAppKey, appKey);
}
} }

View File

@ -15,6 +15,8 @@ public class PayProperties {
private static final String ORDER_NO_PREFIX = "P"; private static final String ORDER_NO_PREFIX = "P";
private static final String REFUND_NO_PREFIX = "R"; 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 的前缀不能为空") @NotEmpty(message = "退款订单 no 的前缀不能为空")
private String refundNoPrefix = REFUND_NO_PREFIX; private String refundNoPrefix = REFUND_NO_PREFIX;
/**
* AppKey
*/
@NotEmpty(message = "钱包支付应用 AppKey 不能为空")
private String walletPayAppKey = WALLET_PAY_APP_KEY_DEFAULT;
} }

View File

@ -94,7 +94,7 @@ public interface PayAppService {
/** /**
* *
* * <p>
* {@link ServiceException} * {@link ServiceException}
* *
* @param id * @param id
@ -102,4 +102,14 @@ public interface PayAppService {
*/ */
PayAppDO validPayApp(Long id); PayAppDO validPayApp(Long id);
/**
*
* <p>
* {@link ServiceException}
*
* @param appKey
* @return
*/
PayAppDO validPayApp(String appKey);
} }

View File

@ -43,6 +43,9 @@ public class PayAppServiceImpl implements PayAppService {
@Override @Override
public Long createApp(PayAppCreateReqVO createReqVO) { public Long createApp(PayAppCreateReqVO createReqVO) {
// 验证 appKey 是否重复
validateAppKeyUnique(null, createReqVO.getAppKey());
// 插入 // 插入
PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO); PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO);
appMapper.insert(app); appMapper.insert(app);
@ -54,11 +57,28 @@ public class PayAppServiceImpl implements PayAppService {
public void updateApp(PayAppUpdateReqVO updateReqVO) { public void updateApp(PayAppUpdateReqVO updateReqVO) {
// 校验存在 // 校验存在
validateAppExists(updateReqVO.getId()); validateAppExists(updateReqVO.getId());
// 验证 appKey 是否重复
validateAppKeyUnique(updateReqVO.getId(), updateReqVO.getAppKey());
// 更新 // 更新
PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO); PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO);
appMapper.updateById(updateObj); 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 @Override
public void updateAppStatus(Long id, Integer status) { public void updateAppStatus(Long id, Integer status) {
// 校验商户存在 // 校验商户存在
@ -110,14 +130,30 @@ public class PayAppServiceImpl implements PayAppService {
} }
@Override @Override
public PayAppDO validPayApp(Long id) { public PayAppDO validPayApp(Long appId) {
PayAppDO app = appMapper.selectById(id); 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) { if (app == null) {
throw exception(ErrorCodeConstants.APP_NOT_FOUND); throw exception(ErrorCodeConstants.APP_NOT_FOUND);
} }
// 校验是否禁用 // 校验是否禁用
if (CommonStatusEnum.DISABLE.getStatus().equals(app.getStatus())) { if (CommonStatusEnum.isDisable(app.getStatus())) {
throw exception(ErrorCodeConstants.APP_IS_DISABLE); throw exception(ErrorCodeConstants.APP_IS_DISABLE);
} }
return app; return app;

View File

@ -14,11 +14,11 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.demo.PayDemoOrderDO;
import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoOrderMapper; import cn.iocoder.yudao.module.pay.dal.mysql.demo.PayDemoOrderMapper;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; 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.refund.PayRefundStatusEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.HashMap; import java.util.HashMap;
@ -43,11 +43,11 @@ import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
public class PayDemoOrderServiceImpl implements PayDemoOrderService { public class PayDemoOrderServiceImpl implements PayDemoOrderService {
/** /**
* *
* *
* [ -> ] * [ -> ]
*/ */
private static final Long PAY_APP_ID = 7L; private static final String PAY_APP_KEY = "demo";
/** /**
* Map * Map
@ -88,7 +88,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
// 2.1 创建支付单 // 2.1 创建支付单
Long payOrderId = payOrderApi.createOrder(new PayOrderCreateReqDTO() Long payOrderId = payOrderApi.createOrder(new PayOrderCreateReqDTO()
.setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用 .setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用
.setMerchantOrderId(demoOrder.getId().toString()) // 业务的订单编号 .setMerchantOrderId(demoOrder.getId().toString()) // 业务的订单编号
.setSubject(spuName).setBody("").setPrice(price) // 价格信息 .setSubject(spuName).setBody("").setPrice(price) // 价格信息
.setExpireTime(addTime(Duration.ofHours(2L)))).getCheckedData(); // 支付的过期时间 .setExpireTime(addTime(Duration.ofHours(2L)))).getCheckedData(); // 支付的过期时间
@ -190,7 +190,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
String refundId = order.getId() + "-refund"; String refundId = order.getId() + "-refund";
// 2.2 创建退款单 // 2.2 创建退款单
Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO() Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO()
.setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用 .setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用
.setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
.setMerchantRefundId(refundId) .setMerchantRefundId(refundId)
.setReason("想退钱").setPrice(order.getPrice())).getCheckedData();// 价格信息 .setReason("想退钱").setPrice(order.getPrice())).getCheckedData();// 价格信息

View File

@ -32,12 +32,12 @@ import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -111,11 +111,11 @@ public class PayOrderServiceImpl implements PayOrderService {
@Override @Override
public Long createOrder(PayOrderCreateReqDTO reqDTO) { public Long createOrder(PayOrderCreateReqDTO reqDTO) {
// 校验 App // 校验 App
PayAppDO app = appService.validPayApp(reqDTO.getAppId()); PayAppDO app = appService.validPayApp(reqDTO.getAppKey());
// 查询对应的支付交易单是否已经存在。如果是,则直接返回 // 查询对应的支付交易单是否已经存在。如果是,则直接返回
PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId( PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId(
reqDTO.getAppId(), reqDTO.getMerchantOrderId()); app.getId(), reqDTO.getMerchantOrderId());
if (order != null) { if (order != null) {
log.warn("[createOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(), log.warn("[createOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
order.getMerchantOrderId(), toJsonString(order)); // 理论来说,不会出现这个情况 order.getMerchantOrderId(), toJsonString(order)); // 理论来说,不会出现这个情况

View File

@ -93,9 +93,9 @@ public class PayRefundServiceImpl implements PayRefundService {
@Override @Override
public Long createPayRefund(PayRefundCreateReqDTO reqDTO) { public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
// 1.1 校验 App // 1.1 校验 App
PayAppDO app = appService.validPayApp(reqDTO.getAppId()); PayAppDO app = appService.validPayApp(reqDTO.getAppKey());
// 1.2 校验支付订单 // 1.2 校验支付订单
PayOrderDO order = validatePayOrderCanRefund(reqDTO); PayOrderDO order = validatePayOrderCanRefund(reqDTO, app.getId());
// 1.3 校验支付渠道是否有效 // 1.3 校验支付渠道是否有效
PayChannelDO channel = channelService.validPayChannel(order.getChannelId()); PayChannelDO channel = channelService.validPayChannel(order.getChannelId());
PayClient client = channelService.getPayClient(channel.getId()); PayClient client = channelService.getPayClient(channel.getId());
@ -113,7 +113,7 @@ public class PayRefundServiceImpl implements PayRefundService {
// 2.1 插入退款单 // 2.1 插入退款单
String no = noRedisDAO.generate(payProperties.getRefundNoPrefix()); String no = noRedisDAO.generate(payProperties.getRefundNoPrefix());
refund = PayRefundConvert.INSTANCE.convert(reqDTO) 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()) .setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode())
// 商户相关的字段 // 商户相关的字段
.setNotifyUrl(app.getRefundNotifyUrl()) .setNotifyUrl(app.getRefundNotifyUrl())
@ -153,8 +153,8 @@ public class PayRefundServiceImpl implements PayRefundService {
* @param reqDTO 退 * @param reqDTO 退
* @return * @return
*/ */
private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO) { private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO, Long appId) {
PayOrderDO order = orderService.getOrder(reqDTO.getAppId(), reqDTO.getMerchantOrderId()); PayOrderDO order = orderService.getOrder(appId, reqDTO.getMerchantOrderId());
if (order == null) { if (order == null) {
throw exception(PAY_ORDER_NOT_FOUND); 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); throw exception(REFUND_PRICE_EXCEED);
} }
// 是否有退款中的订单 // 是否有退款中的订单
if (refundMapper.selectCountByAppIdAndOrderId(reqDTO.getAppId(), order.getId(), if (refundMapper.selectCountByAppIdAndOrderId(appId, order.getId(),
PayRefundStatusEnum.WAITING.getStatus()) > 0) { PayRefundStatusEnum.WAITING.getStatus()) > 0) {
throw exception(REFUND_HAS_REFUNDING); throw exception(REFUND_HAS_REFUNDING);
} }
@ -199,7 +199,8 @@ public class PayRefundServiceImpl implements PayRefundService {
* @param channel * @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) { public void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) {
// 情况一:退款成功 // 情况一:退款成功
if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) { if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) {

View File

@ -79,16 +79,16 @@ public class PayTransferServiceImpl implements PayTransferService {
@Override @Override
public Long createTransfer(PayTransferCreateReqDTO reqDTO) { public Long createTransfer(PayTransferCreateReqDTO reqDTO) {
// 1.1 校验 App // 1.1 校验 App
PayAppDO payApp = appService.validPayApp(reqDTO.getAppId()); PayAppDO payApp = appService.validPayApp(reqDTO.getAppKey());
// 1.2 校验支付渠道是否有效 // 1.2 校验支付渠道是否有效
PayChannelDO channel = channelService.validPayChannel(reqDTO.getAppId(), reqDTO.getChannelCode()); PayChannelDO channel = channelService.validPayChannel(payApp.getId(), reqDTO.getChannelCode());
PayClient client = channelService.getPayClient(channel.getId()); PayClient client = channelService.getPayClient(channel.getId());
if (client == null) { if (client == null) {
log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); log.error("[createTransfer][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(CHANNEL_NOT_FOUND); throw exception(CHANNEL_NOT_FOUND);
} }
// 1.3 校验转账单已经发起过转账。 // 1.3 校验转账单已经发起过转账。
PayTransferDO transfer = validateTransferCanCreate(reqDTO); PayTransferDO transfer = validateTransferCanCreate(reqDTO, payApp.getId());
if (transfer == null) { if (transfer == null) {
// 2.不存在创建转账单. 否则允许使用相同的 no 再次发起转账 // 2.不存在创建转账单. 否则允许使用相同的 no 再次发起转账
@ -116,8 +116,8 @@ public class PayTransferServiceImpl implements PayTransferService {
return transfer.getId(); return transfer.getId();
} }
private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto) { private PayTransferDO validateTransferCanCreate(PayTransferCreateReqDTO dto, Long appId) {
PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(dto.getAppId(), dto.getMerchantTransferId()); PayTransferDO transfer = transferMapper.selectByAppIdAndMerchantTransferId(appId, dto.getMerchantTransferId());
if (transfer != null) { if (transfer != null) {
// 已经存在,并且状态不为等待状态。说明已经调用渠道转账并返回结果. // 已经存在,并且状态不为等待状态。说明已经调用渠道转账并返回结果.
if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) { if (!PayTransferStatusEnum.isWaiting(transfer.getStatus())) {

View File

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

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.pay.service.wallet; 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.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionPageReqVO; import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionPageReqVO;
import cn.iocoder.yudao.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO; import cn.iocoder.yudao.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO;
@ -52,6 +53,16 @@ public class PayWalletTransactionServiceImpl implements PayWalletTransactionServ
@Override @Override
public PageResult<PayWalletTransactionDO> getWalletTransactionPage(PayWalletTransactionPageReqVO pageVO) { 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); return payWalletTransactionMapper.selectPage(pageVO.getWalletId(), null, pageVO, null);
} }

View File

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

View File

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

View File

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

View File

@ -2,10 +2,11 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import lombok.Data;
import jakarta.validation.Validator; import jakarta.validation.Validator;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data;
/** /**
* PayClientConfig * PayClientConfig
@ -25,6 +26,11 @@ public class AlipayPayClientConfig implements PayClientConfig {
*/ */
public static final Integer MODE_CERTIFICATE = 2; public static final Integer MODE_CERTIFICATE = 2;
/**
* - AES
*/
public static final String ENC_TYPE_AES = "AES";
/** /**
* - RSA * - RSA
*/ */
@ -91,6 +97,22 @@ public class AlipayPayClientConfig implements PayClientConfig {
@NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class}) @NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class})
private String rootCertContent; 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 { public interface ModePublicKey {
} }

View File

@ -46,6 +46,8 @@ public interface SmsClient {
/** /**
* *
* *
* null
*
* @param apiTemplateId API * @param apiTemplateId API
* @return * @return
*/ */

View File

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

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; package cn.iocoder.yudao.module.system.framework.sms.core.client.impl;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil; 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.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; 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 @Override
protected void doInit() { protected void doInit() {
} }
public HuaweiSmsClient(SmsChannelProperties properties) { public HuaweiSmsClient(SmsChannelProperties properties) {
@ -63,6 +61,7 @@ public class HuaweiSmsClient extends AbstractSmsClient {
@Override @Override
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
List<KeyValue<String, Object>> templateParams) throws Throwable { List<KeyValue<String, Object>> templateParams) throws Throwable {
// 参考链接 https://support.huaweicloud.com/api-msgsms/sms_05_0001.html
// 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构 // 相比较阿里短信,华为短信发送的时候需要额外的参数“通道号”,考虑到不破坏原有的的结构
// 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。 // 所以将 通道号 拼接到 apiTemplateId 字段中,格式为 "apiTemplateId 通道号"。空格为分隔符。
String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号 String sender = StrUtil.subAfter(apiTemplateId, " ", true); //中国大陆短信签名通道号或全球短信通道号

View File

@ -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.lang.Assert;
import cn.hutool.core.util.StrUtil; 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.JSONArray;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; 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.SmsReceiveRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.client.dto.SmsSendRespDTO; 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.client.dto.SmsTemplateRespDTO;
import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum; import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; 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 com.google.common.annotations.VisibleForTesting;
import jakarta.xml.bind.DatatypeConverter; import jakarta.xml.bind.DatatypeConverter;
import lombok.Data;
import javax.crypto.Mac; import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import static cn.hutool.crypto.digest.DigestUtil.sha256Hex; 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.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 { public class TencentSmsClient extends AbstractSmsClient {
private static final String VERSION = "2021-01-11";
private static final String REGION = "ap-guangzhou";
/** /**
* code * code
*/ */
@ -56,7 +50,6 @@ public class TencentSmsClient extends AbstractSmsClient {
*/ */
private static final long INTERNATIONAL_CHINA = 0L; private static final long INTERNATIONAL_CHINA = 0L;
public TencentSmsClient(SmsChannelProperties properties) { public TencentSmsClient(SmsChannelProperties properties) {
super(properties); super(properties);
Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
@ -65,7 +58,6 @@ public class TencentSmsClient extends AbstractSmsClient {
@Override @Override
protected void doInit() { protected void doInit() {
} }
/** /**
@ -95,31 +87,96 @@ public class TencentSmsClient extends AbstractSmsClient {
@Override @Override
public SmsSendRespDTO sendSms(Long sendLogId, String mobile, public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable { String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
// 构建请求 // 1. 执行请求
// 参考链接 https://cloud.tencent.com/document/product/382/55981
TreeMap<String, Object> body = new TreeMap<>(); TreeMap<String, Object> body = new TreeMap<>();
String[] phones = {mobile}; body.put("PhoneNumberSet", new String[]{mobile});
body.put("PhoneNumberSet",phones); body.put("SmsSdkAppId", getSdkAppId());
body.put("SmsSdkAppId",getSdkAppId()); body.put("SignName", properties.getSignature());
body.put("SignName",properties.getSignature());
body.put("TemplateId",apiTemplateId); 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);
JSONObject JsonResponse = sendSmsRequest(body,"SendSms","2021-01-11","ap-guangzhou");
SmsResponse smsResponse = getSmsSendResponse(JsonResponse);
return new SmsSendRespDTO().setSuccess(smsResponse.success).setApiMsg(smsResponse.data.toString());
// 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); String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
// TODO @scholar这个 format看看怎么写的可以简化点
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// 注意时区,否则容易出错 // 注意时区,否则容易出错
sdf.setTimeZone(TimeZone.getTimeZone("UTC")); sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
String date = sdf.format(new Date(Long.valueOf(timestamp + "000"))); String date = sdf.format(new Date(Long.valueOf(timestamp + "000")));
// TODO @scholar这个步骤看看怎么参考阿里云 client归类下1. 2.1 2.2 这种
// ************* 步骤 1拼接规范请求串 ************* // ************* 步骤 1拼接规范请求串 *************
// TODO @scholar这个 hsot 枚举下;
String host = "sms.tencentcloudapi.com"; //APP接入地址+接口访问URI String host = "sms.tencentcloudapi.com"; //APP接入地址+接口访问URI
String httpMethod = "POST"; // 请求方式 String httpMethod = "POST"; // 请求方式
String canonicalUri = "/"; String canonicalUri = "/";
@ -129,6 +186,7 @@ public class TencentSmsClient extends AbstractSmsClient {
+ "host:" + host + "\n" + "x-tc-action:" + action.toLowerCase() + "\n"; + "host:" + host + "\n" + "x-tc-action:" + action.toLowerCase() + "\n";
String signedHeaders = "content-type;host;x-tc-action"; String signedHeaders = "content-type;host;x-tc-action";
String hashedRequestBody = sha256Hex(JSONUtil.toJsonStr(body)); String hashedRequestBody = sha256Hex(JSONUtil.toJsonStr(body));
// TODO @scholar换行下不然单行太长了
String canonicalRequest = httpMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody; String canonicalRequest = httpMethod + "\n" + canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestBody;
// ************* 步骤 2拼接待签名字符串 ************* // ************* 步骤 2拼接待签名字符串 *************
@ -153,205 +211,19 @@ public class TencentSmsClient extends AbstractSmsClient {
headers.put("Host", host); headers.put("Host", host);
headers.put("X-TC-Action", action); headers.put("X-TC-Action", action);
headers.put("X-TC-Timestamp", timestamp); headers.put("X-TC-Timestamp", timestamp);
headers.put("X-TC-Version", version); headers.put("X-TC-Version", VERSION);
headers.put("X-TC-Region", region); headers.put("X-TC-Region", REGION);
HttpResponse response = HttpRequest.post("https://"+host) String responseBody = HttpUtils.post("https://" + host, headers, JSONUtil.toJsonStr(body));
.addHeaders(headers)
.body(JSONUtil.toJsonStr(body))
.execute();
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"); Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm()); SecretKeySpec secretKeySpec = new SecretKeySpec(key, mac.getAlgorithm());
mac.init(secretKeySpec); mac.init(secretKeySpec);
return mac.doFinal(msg.getBytes(StandardCharsets.UTF_8)); 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;
/**
* SUCCESSFAIL
*/
@JsonProperty("report_status")
private String status;
/**
*
*/
@JsonProperty("errmsg")
private String errCode;
/**
*
*/
@JsonProperty("description")
private String description;
/**
* IDSerialNo
*/
@JsonProperty("sid")
private String serialNo;
/**
* session SessionContext
*/
@JsonProperty("ext")
private SessionContext sessionContext;
}
@VisibleForTesting
@Data
static class SessionContext {
/**
* id
*/
private Long logId;
}
} }

View File

@ -1,11 +1,10 @@
package cn.iocoder.yudao.module.system.framework.sms.core.property; package cn.iocoder.yudao.module.system.framework.sms.core.property;
import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum; import cn.iocoder.yudao.module.system.framework.sms.core.enums.SmsChannelEnum;
import lombok.Data;
import org.springframework.validation.annotation.Validated;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.validation.annotation.Validated;
/** /**
* *

View File

@ -24,7 +24,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.mockStatic;
/** /**
* {@link AliyunSmsClient} * {@link cn.iocoder.yudao.module.system.framework.sms.core.client.impl.AliyunSmsClient}
* *
* @author * @author
*/ */
@ -38,15 +38,6 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
@InjectMocks @InjectMocks
private final AliyunSmsClient smsClient = new AliyunSmsClient(properties); private final AliyunSmsClient smsClient = new AliyunSmsClient(properties);
@Test
public void testDoInit() {
// 准备参数
// mock 方法
// 调用
smsClient.doInit();
}
@Test @Test
public void tesSendSms_success() throws Throwable { public void tesSendSms_success() throws Throwable {
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) { try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {

View File

@ -17,24 +17,6 @@ import java.util.List;
*/ */
public class SmsClientTests { 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 = List.of(new KeyValue<>("code", "1024"));
// 调用
SmsSendRespDTO smsSendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
// 打印结果
System.out.println(smsSendRespDTO);
}
// ========== 阿里云 ========== // ========== 阿里云 ==========
@Test @Test
@ -58,11 +40,11 @@ public class SmsClientTests {
SmsChannelProperties properties = new SmsChannelProperties() SmsChannelProperties properties = new SmsChannelProperties()
.setApiKey("LTAI5tAicJAxaSFiZuGGeXHR") .setApiKey("LTAI5tAicJAxaSFiZuGGeXHR")
.setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz") .setApiSecret("Fdr9vadxnDvS6GJU0W1tijQ0VmLhYz")
.setSignature("Ballcat"); .setSignature("runpu");
AliyunSmsClient client = new AliyunSmsClient(properties); AliyunSmsClient client = new AliyunSmsClient(properties);
// 准备参数 // 准备参数
Long sendLogId = System.currentTimeMillis(); Long sendLogId = System.currentTimeMillis();
String mobile = "173213154791"; String mobile = "15601691323";
String apiTemplateId = "SMS_207945135"; String apiTemplateId = "SMS_207945135";
// 调用 // 调用
SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024"))); SmsSendRespDTO sendRespDTO = client.sendSms(sendLogId, mobile, apiTemplateId, List.of(new KeyValue<>("code", "1024")));
@ -99,4 +81,62 @@ public class SmsClientTests {
System.out.println(statuses); 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);
}
} }

View File

@ -1,22 +1,28 @@
package cn.iocoder.yudao.module.system.framework.sms.core.client.impl; 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.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.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.enums.SmsTemplateAuditStatusEnum;
import cn.iocoder.yudao.module.system.framework.sms.core.property.SmsChannelProperties; 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.junit.jupiter.api.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.MockedStatic;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; 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 cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.*; 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} * {@link TencentSmsClient}
* *
@ -32,114 +38,84 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
@InjectMocks @InjectMocks
private TencentSmsClient smsClient = new TencentSmsClient(properties); private TencentSmsClient smsClient = new TencentSmsClient(properties);
@Mock
private SmsClient client;
@Test @Test
public void testDoInit() { 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 方法 // 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(); SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
apiTemplateId, templateParams);
// 断言 // 断言
assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); assertTrue(result.getSuccess());
assertEquals("5000:1045710669157053657849499619", result.getSerialNo());
assertEquals("a0aabda6-cf91-4f3e-a81f-9198114a2279", result.getApiRequestId());
assertEquals("send success", result.getApiMsg());
}
} }
@Test @Test
public void testRefresh() { public void testDoSendSms_fail() throws Throwable {
try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
// 准备参数 // 准备参数
SmsChannelProperties p = new SmsChannelProperties() Long sendLogId = randomLongId();
.setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey避免构建报错 String mobile = randomString();
.setApiSecret(randomString()) // 随机一个 apiSecret避免构建报错 String apiTemplateId = randomString();
.setSignature("芋道源码"); 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" +
"}");
// 调用 // 调用
smsClient.refresh(p); SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
apiTemplateId, templateParams);
// 断言 // 断言
assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); 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 @Test
public void testParseSmsReceiveStatus() { public void testParseSmsReceiveStatus() {
@ -156,7 +132,6 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
" \"ext\": {\"logId\":\"67890\"}\n" + " \"ext\": {\"logId\":\"67890\"}\n" +
" }\n" + " }\n" +
"]"; "]";
// mock 方法
// 调用 // 调用
List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text); List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
@ -164,42 +139,44 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
assertEquals(1, statuses.size()); assertEquals(1, statuses.size());
assertTrue(statuses.get(0).getSuccess()); assertTrue(statuses.get(0).getSuccess());
assertEquals("DELIVRD", statuses.get(0).getErrorCode()); assertEquals("DELIVRD", statuses.get(0).getErrorCode());
assertEquals("用户短信送达成功", statuses.get(0).getErrorMsg());
assertEquals("13900000001", statuses.get(0).getMobile()); assertEquals("13900000001", statuses.get(0).getMobile());
assertEquals(LocalDateTime.of(2015, 10, 17, 8, 3, 4), statuses.get(0).getReceiveTime()); assertEquals(LocalDateTime.of(2015, 10, 17, 8, 3, 4), statuses.get(0).getReceiveTime());
assertEquals("12345", statuses.get(0).getSerialNo()); assertEquals("12345", statuses.get(0).getSerialNo());
assertEquals(67890L, statuses.get(0).getLogId());
} }
// @Test @Test
// public void testGetSmsTemplate() throws Throwable { public void testGetSmsTemplate() throws Throwable {
// // 准备参数 try (MockedStatic<HttpUtils> httpUtilsMockedStatic = mockStatic(HttpUtils.class)) {
// Long apiTemplateId = randomLongId(); // 准备参数
// String requestId = randomString(); String apiTemplateId = "1122";
//
// // mock 方法 // mock 方法
// DescribeSmsTemplateListResponse response = randomPojo(DescribeSmsTemplateListResponse.class, o -> { httpUtilsMockedStatic.when(() -> HttpUtils.post(anyString(), anyMap(), anyString()))
// DescribeTemplateListStatus[] describeTemplateListStatuses = new DescribeTemplateListStatus[1]; .thenReturn("{ \"Response\": {\n" +
// DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus(); " \"DescribeTemplateStatusSet\": [\n" +
// templateStatus.setTemplateId(apiTemplateId); " {\n" +
// templateStatus.setStatusCode(0L);// 设置模板通过 " \"TemplateName\": \"验证码\",\n" +
// describeTemplateListStatuses[0] = templateStatus; " \"TemplateId\": 1122,\n" +
// o.setDescribeTemplateStatusSet(describeTemplateListStatuses); " \"International\": 0,\n" +
// o.setRequestId(requestId); " \"ReviewReply\": \"审批备注\",\n" +
// }); " \"CreateTime\": 1617379200,\n" +
// when(client.DescribeSmsTemplateList(argThat(request -> { " \"TemplateContent\": \"您的验证码是{1}\",\n" +
// assertEquals(apiTemplateId, request.getTemplateIdSet()[0]); " \"StatusCode\": 0\n" +
// return true; " },\n" +
// }))).thenReturn(response); " \n" +
// " ],\n" +
// // 调用 " \"RequestId\": \"f36e4f00-605e-49b1-ad0d-bfaba81c7325\"\n" +
// SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId.toString()); " }}");
// // 断言
// assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getId()); // 调用
// assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getContent()); SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId);
// assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus()); // 断言
// assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getAuditReason()); assertEquals("1122", result.getId());
// } assertEquals("您的验证码是{1}", result.getContent());
assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
assertEquals("审批备注", result.getAuditReason());
}
}
@Test @Test
public void testConvertSmsTemplateAuditStatus() { public void testConvertSmsTemplateAuditStatus() {