【功能优化】支付:支付应用,增加 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 (2794, 'AI 写作查询', 'ai:write:query', 3, 1, 2793, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '', '2024-07-10 13:24:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2795, 'AI 写作删除', 'ai:write:delete', 3, 4, 2793, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2024-07-10 13:24:34', '', '2024-07-10 13:24:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2796, 'AI 音乐', '', 2, 4, 2758, 'music', 'fa:music', 'ai/music/index/index.vue', 'AiWrite', 0, b'1', b'1', b'1', '1', '2024-07-17 09:21:12', '1', '2024-07-17 09:36:12', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2796, 'AI 音乐', '', 2, 4, 2758, 'music', 'fa:music', 'ai/music/index/index.vue', 'AiMusic', 0, b'1', b'1', b'1', '1', '2024-07-17 09:21:12', '1', '2024-07-17 09:36:12', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2797, '客服中心', '', 2, 100, 2362, 'kefu', 'fa-solid:user-alt', 'mall/promotion/kefu/index', 'KeFu', 0, b'1', b'1', b'1', '1', '2024-07-17 23:49:05', '1', '2024-07-17 23:49:16', b'0');
COMMIT;

View File

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

View File

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

View File

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

View File

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

View File

@ -139,24 +139,6 @@ public interface CombinationRecordService {
@Nullable Integer status,
@Nullable Long headId);
/**
*
*
* @param userId
* @param id
* @return
*/
CombinationRecordDO getCombinationRecordByIdAndUser(Long userId, Long id);
/**
*
*
* @param userId
* @param id
* @param headId
*/
void cancelCombinationRecord(Long userId, Long id, Long headId);
/**
*
*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.context.annotation.Configuration;
// TODO @LeeYan9: 可以直接给 TradeOrderProperties 一个 @Component生效哈
/**
* @author LeeYan9
* @since 2022-09-15

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.trade.service.price.calculator;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.product.api.sku.dto.ProductSkuRespDTO;
import cn.iocoder.yudao.module.product.api.spu.dto.ProductSpuRespDTO;
@ -61,7 +62,7 @@ public class TradePriceCalculatorHelper {
orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId())
.setDeliveryTemplateId(spu.getDeliveryTemplateId())
.setGivePoint(spu.getGiveIntegral()).setUsePoint(0);
if (orderItem.getPicUrl() == null) {
if (StrUtil.isBlank(orderItem.getPicUrl())) {
orderItem.setPicUrl(spu.getPicUrl());
}
});
@ -240,7 +241,7 @@ public class TradePriceCalculatorHelper {
*
* {@link #dividePrice(List, Integer)} TradeOrderItemDO
*
* @param items
* @param items
* @param price
* @return orderItems
*/

View File

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

View File

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

View File

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

View File

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

View File

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

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.validation.InEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
@ -14,6 +15,10 @@ import org.hibernate.validator.constraints.URL;
@Data
public class PayAppBaseVO {
@Schema(description = "应用标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
@NotEmpty(message = "应用标识不能为空")
private String appKey;
@Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "小豆")
@NotNull(message = "应用名不能为空")
private String name;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -88,13 +88,13 @@ public interface PayAppService {
* @return Map
*/
default Map<Long, PayAppDO> getAppMap(Collection<Long> ids) {
List<PayAppDO> list = getAppList(ids);
List<PayAppDO> list = getAppList(ids);
return CollectionUtils.convertMap(list, PayAppDO::getId);
}
/**
*
*
* <p>
* {@link ServiceException}
*
* @param id
@ -102,4 +102,14 @@ public interface PayAppService {
*/
PayAppDO validPayApp(Long id);
/**
*
* <p>
* {@link ServiceException}
*
* @param appKey
* @return
*/
PayAppDO validPayApp(String appKey);
}

View File

@ -43,6 +43,9 @@ public class PayAppServiceImpl implements PayAppService {
@Override
public Long createApp(PayAppCreateReqVO createReqVO) {
// 验证 appKey 是否重复
validateAppKeyUnique(null, createReqVO.getAppKey());
// 插入
PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO);
appMapper.insert(app);
@ -54,11 +57,28 @@ public class PayAppServiceImpl implements PayAppService {
public void updateApp(PayAppUpdateReqVO updateReqVO) {
// 校验存在
validateAppExists(updateReqVO.getId());
// 验证 appKey 是否重复
validateAppKeyUnique(updateReqVO.getId(), updateReqVO.getAppKey());
// 更新
PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO);
appMapper.updateById(updateObj);
}
void validateAppKeyUnique(Long id, String appKey) {
PayAppDO app = appMapper.selectByAppKey(appKey);
if (app == null) {
return;
}
// 如果 id 为空,说明不用比较是否为相同 appKey 的应用
if (id == null) {
throw exception(APP_KEY_EXISTS);
}
if (!app.getId().equals(id)) {
throw exception(APP_KEY_EXISTS);
}
}
@Override
public void updateAppStatus(Long id, Integer status) {
// 校验商户存在
@ -101,7 +121,7 @@ public class PayAppServiceImpl implements PayAppService {
@Override
public List<PayAppDO> getAppList() {
return appMapper.selectList();
return appMapper.selectList();
}
@Override
@ -110,14 +130,30 @@ public class PayAppServiceImpl implements PayAppService {
}
@Override
public PayAppDO validPayApp(Long id) {
PayAppDO app = appMapper.selectById(id);
public PayAppDO validPayApp(Long appId) {
PayAppDO app = appMapper.selectById(appId);
return validatePayApp(app);
}
@Override
public PayAppDO validPayApp(String appKey) {
PayAppDO app = appMapper.selectByAppKey(appKey);
return validatePayApp(app);
}
/**
* +
*
* @param app
* @return
*/
private PayAppDO validatePayApp(PayAppDO app) {
// 校验是否存在
if (app == null) {
throw exception(ErrorCodeConstants.APP_NOT_FOUND);
}
// 校验是否禁用
if (CommonStatusEnum.DISABLE.getStatus().equals(app.getStatus())) {
if (CommonStatusEnum.isDisable(app.getStatus())) {
throw exception(ErrorCodeConstants.APP_IS_DISABLE);
}
return app;

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

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

View File

@ -93,9 +93,9 @@ public class PayRefundServiceImpl implements PayRefundService {
@Override
public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
// 1.1 校验 App
PayAppDO app = appService.validPayApp(reqDTO.getAppId());
PayAppDO app = appService.validPayApp(reqDTO.getAppKey());
// 1.2 校验支付订单
PayOrderDO order = validatePayOrderCanRefund(reqDTO);
PayOrderDO order = validatePayOrderCanRefund(reqDTO, app.getId());
// 1.3 校验支付渠道是否有效
PayChannelDO channel = channelService.validPayChannel(order.getChannelId());
PayClient client = channelService.getPayClient(channel.getId());
@ -113,7 +113,7 @@ public class PayRefundServiceImpl implements PayRefundService {
// 2.1 插入退款单
String no = noRedisDAO.generate(payProperties.getRefundNoPrefix());
refund = PayRefundConvert.INSTANCE.convert(reqDTO)
.setNo(no).setOrderId(order.getId()).setOrderNo(order.getNo())
.setNo(no).setAppId(app.getId()).setOrderId(order.getId()).setOrderNo(order.getNo())
.setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode())
// 商户相关的字段
.setNotifyUrl(app.getRefundNotifyUrl())
@ -153,8 +153,8 @@ public class PayRefundServiceImpl implements PayRefundService {
* @param reqDTO 退
* @return
*/
private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO) {
PayOrderDO order = orderService.getOrder(reqDTO.getAppId(), reqDTO.getMerchantOrderId());
private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO, Long appId) {
PayOrderDO order = orderService.getOrder(appId, reqDTO.getMerchantOrderId());
if (order == null) {
throw exception(PAY_ORDER_NOT_FOUND);
}
@ -164,11 +164,11 @@ public class PayRefundServiceImpl implements PayRefundService {
}
// 校验金额,退款金额不能大于原定的金额
if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){
if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()) {
throw exception(REFUND_PRICE_EXCEED);
}
// 是否有退款中的订单
if (refundMapper.selectCountByAppIdAndOrderId(reqDTO.getAppId(), order.getId(),
if (refundMapper.selectCountByAppIdAndOrderId(appId, order.getId(),
PayRefundStatusEnum.WAITING.getStatus()) > 0) {
throw exception(REFUND_HAS_REFUNDING);
}
@ -197,9 +197,10 @@ public class PayRefundServiceImpl implements PayRefundService {
* 退
*
* @param channel
* @param notify
* @param notify
*/
@Transactional(rollbackFor = Exception.class) // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效
// 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效
@Transactional(rollbackFor = Exception.class)
public void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) {
// 情况一:退款成功
if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) {

View File

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

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

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.pay.service.wallet;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.controller.admin.wallet.vo.transaction.PayWalletTransactionPageReqVO;
import cn.iocoder.yudao.module.pay.controller.app.wallet.vo.transaction.AppPayWalletTransactionPageReqVO;
@ -52,6 +53,16 @@ public class PayWalletTransactionServiceImpl implements PayWalletTransactionServ
@Override
public PageResult<PayWalletTransactionDO> getWalletTransactionPage(PayWalletTransactionPageReqVO pageVO) {
// 基于 userId + userType 查询钱包
if (pageVO.getWalletId() == null
&& ObjectUtil.isAllNotEmpty(pageVO.getUserId(), pageVO.getUserType())) {
PayWalletDO wallet = payWalletService.getOrCreateWallet(pageVO.getUserId(), pageVO.getUserType());
if (wallet != null) {
pageVO.setWalletId(wallet.getId());
}
}
// 查询分页
return payWalletTransactionMapper.selectPage(pageVO.getWalletId(), null, pageVO, null);
}

View File

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

View File

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

View File

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

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.pay.core.client.PayClientConfig;
import lombok.Data;
import jakarta.validation.Validator;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* PayClientConfig
@ -25,6 +26,11 @@ public class AlipayPayClientConfig implements PayClientConfig {
*/
public static final Integer MODE_CERTIFICATE = 2;
/**
* - AES
*/
public static final String ENC_TYPE_AES = "AES";
/**
* - RSA
*/
@ -91,6 +97,22 @@ public class AlipayPayClientConfig implements PayClientConfig {
@NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class})
private String rootCertContent;
/**
*
*
* 1. 使
* 2. AES
*
* @see <a href="https://opendocs.alipay.com/common/02mse3"></a>
* @see AlipayPayClientConfig#ENC_TYPE_AES
*/
private String encryptType;
/**
*
*/
private String encryptKey;
public interface ModePublicKey {
}

View File

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

View File

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

View File

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

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

View File

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

View File

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