reactor:将 pay starter 融合到 pay-module 的 framework/pay 里

This commit is contained in:
YunaiV
2025-05-16 23:27:00 +08:00
parent ee7b3fb275
commit d4ed2c49f1
79 changed files with 475 additions and 682 deletions

View File

@@ -30,11 +30,6 @@
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
@@ -75,6 +70,24 @@
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-excel</artifactId>
</dependency>
<!-- 三方云服务相关 -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.35.79.ALL</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.pay.api.transfer;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateRespDTO;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferRespDTO;

View File

@@ -4,10 +4,10 @@ import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO;
@@ -156,7 +156,7 @@ public class PayNotifyController {
}
// 拼接返回
Map<Long, PayAppDO> apps = appService.getAppMap(convertList(pageResult.getList(), PayNotifyTaskDO::getAppId));
// 转换对象
return success(BeanUtils.toBean(pageResult, PayNotifyTaskRespVO.class, order -> {
PayAppDO app = apps.get(order.getAppId());

View File

@@ -6,15 +6,15 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*;
import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.WalletPayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.wallet.WalletPayClient;
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;

View File

@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.pay.controller.app.order;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderRespVO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO;
import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO;
@@ -10,8 +9,9 @@ import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitResp
import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.WalletPayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.wallet.WalletPayClient;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.wallet.PayWalletService;
import com.google.common.collect.Maps;

View File

@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.pay.convert.order;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*;
@@ -67,7 +67,7 @@ public interface PayOrderConvert {
PayOrderUnifiedReqDTO convert2(PayOrderSubmitReqVO reqVO, String userIp);
@Mapping(source = "order.status", target = "status")
PayOrderSubmitRespVO convert(PayOrderDO order, cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO respDTO);
PayOrderSubmitRespVO convert(PayOrderDO order, cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO respDTO);
AppPayOrderSubmitRespVO convert3(PayOrderSubmitRespVO bean);

View File

@@ -1,16 +1,26 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.channel;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.NonePayClientConfig;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayAppPayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.*;
import java.lang.reflect.Field;
/**
* 支付渠道 DO
* 一个应用下,会有多种支付渠道,例如说微信支付、支付宝支付等等
@@ -63,7 +73,46 @@ public class PayChannelDO extends TenantBaseDO {
/**
* 支付渠道配置
*/
@TableField(typeHandler = JacksonTypeHandler.class)
@TableField(typeHandler = PayClientConfigTypeHandler.class)
private PayClientConfig config;
public static class PayClientConfigTypeHandler extends AbstractJsonTypeHandler<Object> {
public PayClientConfigTypeHandler(Class<?> type) {
super(type);
}
public PayClientConfigTypeHandler(Class<?> type, Field field) {
super(type, field);
}
@Override
public Object parse(String json) {
PayClientConfig config = JsonUtils.parseObjectQuietly(json, new TypeReference<>() {});
if (config != null) {
return config;
}
// 兼容老版本的包路径
String className = JsonUtils.parseObject(json, "@class", String.class);
className = StrUtil.subAfter(className, ".", true);
switch (className) {
case "AlipayPayClientConfig":
return JsonUtils.parseObject2(json, AlipayPayClientConfig.class);
case "WxPayClientConfig":
return JsonUtils.parseObject2(json, WxPayClientConfig.class);
case "NonePayClientConfig":
return JsonUtils.parseObject2(json, NonePayClientConfig.class);
default:
throw new IllegalArgumentException("未知的 PayClientConfig 类型:" + json);
}
}
@Override
public String toJson(Object obj) {
return JsonUtils.toJsonString(obj);
}
}
}

View File

@@ -1,9 +1,9 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.order;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableName;

View File

@@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.order;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;

View File

@@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.refund;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;

View File

@@ -1,10 +1,10 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.transfer;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -89,7 +89,7 @@ public class PayTransferDO extends BaseDO {
/**
* 转账状态
*
* 枚举 {@link PayTransferStatusRespEnum}
* 枚举 {@link PayTransferStatusEnum}
*/
private Integer status;

View File

@@ -1,9 +1,18 @@
package cn.iocoder.yudao.module.pay.framework.pay.config;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.PayClientFactoryImpl;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PayProperties.class)
public class PayConfiguration {
@Bean
public PayClientFactory payClientFactory() {
return new PayClientFactoryImpl();
}
}

View File

@@ -0,0 +1,118 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import java.util.Map;
/**
* 支付客户端,用于对接各支付渠道的 SDK实现发起支付、退款等功能
*
* @author 芋道源码
*/
public interface PayClient<Config> {
/**
* 获得渠道编号
*
* @return 渠道编号
*/
Long getId();
/**
* 获得渠道配置
*
* @return 渠道配置
*/
Config getConfig();
// ============ 支付相关 ==========
/**
* 调用支付渠道,统一下单
*
* @param reqDTO 下单信息
* @return 支付订单信息
*/
PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
/**
* 解析 order 回调数据
*
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @param headers HTTP 回调接口的 request headers
* @return 支付订单信息
*/
PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body, Map<String, String> headers);
/**
* 获得支付订单信息
*
* @param outTradeNo 外部订单号
* @return 支付订单信息
*/
PayOrderRespDTO getOrder(String outTradeNo);
// ============ 退款相关 ==========
/**
* 调用支付渠道,进行退款
*
* @param reqDTO 统一退款请求信息
* @return 退款信息
*/
PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
/**
* 解析 refund 回调数据
*
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @param headers HTTP 回调接口的 request headers
* @return 支付订单信息
*/
PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body, Map<String, String> headers);
/**
* 获得退款订单信息
*
* @param outTradeNo 外部订单号
* @param outRefundNo 外部退款号
* @return 退款订单信息
*/
PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo);
// ============ 转账相关 ==========
/**
* 调用渠道,进行转账
*
* @param reqDTO 统一转账请求信息
* @return 转账信息
*/
PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO);
/**
* 获得转账订单信息
*
* @param outTradeNo 外部订单号
* @return 转账信息
*/
PayTransferRespDTO getTransfer(String outTradeNo);
/**
* 解析 transfer 回调数据
*
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @param headers HTTP 回调接口的 request headers
* @return 转账信息
*/
PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body, Map<String, String> headers);
}

View File

@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import jakarta.validation.Validator;
/**
* 支付客户端的配置,本质是支付渠道的配置
* 每个不同的渠道,需要不同的配置,通过子类来定义
*
* @author 芋道源码
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
// @JsonTypeInfo 注解的作用Jackson 多态
// 1. 序列化到时数据库时,增加 @class 属性。
// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
@JsonIgnoreProperties(ignoreUnknown = true) // 目的:忽略未知的属性,避免反序列化失败
public interface PayClientConfig {
/**
* 参数校验
*
* @param validator 校验对象
*/
void validate(Validator validator);
}

View File

@@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client;
/**
* 支付客户端的工厂接口
*
* @author 芋道源码
*/
public interface PayClientFactory {
/**
* 获得支付客户端
*
* @param channelId 渠道编号
* @return 支付客户端
*/
PayClient getPayClient(Long channelId);
/**
* 创建支付客户端
*
* @param channelId 渠道编号
* @param channelCode 渠道编码
* @param config 支付配置
* @return 支付客户端
*/
<Config extends PayClientConfig> PayClient createOrUpdatePayClient(Long channelId, String channelCode,
Config config);
}

View File

@@ -0,0 +1,141 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.exception.PayClientException;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 渠道支付订单 Response DTO
*
* @author 芋道源码
*/
@Data
public class PayOrderRespDTO {
/**
* 支付状态
*
* 枚举:{@link PayOrderStatusEnum}
*/
private Integer status;
/**
* 外部订单号
*
* 对应 PayOrderExtensionDO 的 no 字段
*/
private String outTradeNo;
/**
* 支付渠道编号
*/
private String channelOrderNo;
/**
* 支付渠道用户编号
*/
private String channelUserId;
/**
* 支付成功时间
*/
private LocalDateTime successTime;
/**
* 原始的同步/异步通知结果
*/
private Object rawData;
// ========== 主动发起支付时,会返回的字段 ==========
/**
* 展示模式
*
* 枚举 {@link PayOrderDisplayModeEnum} 类
*/
private String displayMode;
/**
* 展示内容
*/
private String displayContent;
/**
* 调用渠道的错误码
*
* 注意:这里返回的是业务异常,而是不系统异常。
* 如果是系统异常,则会抛出 {@link PayClientException}
*/
private String channelErrorCode;
/**
* 调用渠道报错时,错误信息
*/
private String channelErrorMsg;
public PayOrderRespDTO() {
}
/**
* 创建【WAITING】状态的订单返回
*/
public static PayOrderRespDTO waitingOf(String displayMode, String displayContent,
String outTradeNo, Object rawData) {
PayOrderRespDTO respDTO = new PayOrderRespDTO();
respDTO.status = PayOrderStatusEnum.WAITING.getStatus();
respDTO.displayMode = displayMode;
respDTO.displayContent = displayContent;
// 相对通用的字段
respDTO.outTradeNo = outTradeNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建【SUCCESS】状态的订单返回
*/
public static PayOrderRespDTO successOf(String channelOrderNo, String channelUserId, LocalDateTime successTime,
String outTradeNo, Object rawData) {
PayOrderRespDTO respDTO = new PayOrderRespDTO();
respDTO.status = PayOrderStatusEnum.SUCCESS.getStatus();
respDTO.channelOrderNo = channelOrderNo;
respDTO.channelUserId = channelUserId;
respDTO.successTime = successTime;
// 相对通用的字段
respDTO.outTradeNo = outTradeNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建指定状态的订单返回,适合支付渠道回调时
*/
public static PayOrderRespDTO of(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime,
String outTradeNo, Object rawData) {
PayOrderRespDTO respDTO = new PayOrderRespDTO();
respDTO.status = status;
respDTO.channelOrderNo = channelOrderNo;
respDTO.channelUserId = channelUserId;
respDTO.successTime = successTime;
// 相对通用的字段
respDTO.outTradeNo = outTradeNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建【CLOSED】状态的订单返回适合调用支付渠道失败时
*/
public static PayOrderRespDTO closedOf(String channelErrorCode, String channelErrorMsg,
String outTradeNo, Object rawData) {
PayOrderRespDTO respDTO = new PayOrderRespDTO();
respDTO.status = PayOrderStatusEnum.CLOSED.getStatus();
respDTO.channelErrorCode = channelErrorCode;
respDTO.channelErrorMsg = channelErrorMsg;
// 相对通用的字段
respDTO.outTradeNo = outTradeNo;
respDTO.rawData = rawData;
return respDTO;
}
}

View File

@@ -0,0 +1,92 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 统一下单 Request DTO
*
* @author 芋道源码
*/
@Data
public class PayOrderUnifiedReqDTO {
/**
* 用户 IP
*/
@NotEmpty(message = "用户 IP 不能为空")
private String userIp;
// ========== 商户相关字段 ==========
/**
* 外部订单号
*
* 对应 PayOrderExtensionDO 的 no 字段
*/
@NotEmpty(message = "外部订单编号不能为空")
private String outTradeNo;
/**
* 商品标题
*/
@NotEmpty(message = "商品标题不能为空")
@Length(max = 32, message = "商品标题不能超过 32")
private String subject;
/**
* 商品描述信息
*/
@Length(max = 128, message = "商品描述信息长度不能超过128")
private String body;
/**
* 支付结果的 notify 回调地址
*/
@NotEmpty(message = "支付结果的回调地址不能为空")
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
private String notifyUrl;
/**
* 支付结果的 return 回调地址
*/
@URL(message = "支付结果的 return 回调地址必须是 URL 格式")
private String returnUrl;
// ========== 订单相关字段 ==========
/**
* 支付金额,单位:分
*/
@NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer price;
/**
* 支付过期时间
*/
@NotNull(message = "支付过期时间不能为空")
private LocalDateTime expireTime;
// ========== 拓展参数 ==========
/**
* 支付渠道的额外参数
*
* 例如说,微信公众号需要传递 openid 参数
*/
private Map<String, String> channelExtras;
/**
* 展示模式
*
* 如果不传递,则每个支付渠道使用默认的方式
*
* 枚举 {@link PayOrderDisplayModeEnum}
*/
private String displayMode;
}

View File

@@ -0,0 +1,115 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.exception.PayClientException;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 渠道退款订单 Response DTO
*
* @author jason
*/
@Data
public class PayRefundRespDTO {
/**
* 退款状态
*
* 枚举 {@link PayRefundStatusEnum}
*/
private Integer status;
/**
* 外部退款号
*
* 对应 PayRefundDO 的 no 字段
*/
private String outRefundNo;
/**
* 渠道退款单号
*
* 对应 PayRefundDO.channelRefundNo 字段
*/
private String channelRefundNo;
/**
* 退款成功时间
*/
private LocalDateTime successTime;
/**
* 原始的异步通知结果
*/
private Object rawData;
/**
* 调用渠道的错误码
*
* 注意:这里返回的是业务异常,而是不系统异常。
* 如果是系统异常,则会抛出 {@link PayClientException}
*/
private String channelErrorCode;
/**
* 调用渠道报错时,错误信息
*/
private String channelErrorMsg;
private PayRefundRespDTO() {
}
/**
* 创建【WAITING】状态的退款返回
*/
public static PayRefundRespDTO waitingOf(String channelRefundNo,
String outRefundNo, Object rawData) {
PayRefundRespDTO respDTO = new PayRefundRespDTO();
respDTO.status = PayRefundStatusEnum.WAITING.getStatus();
respDTO.channelRefundNo = channelRefundNo;
// 相对通用的字段
respDTO.outRefundNo = outRefundNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建【SUCCESS】状态的退款返回
*/
public static PayRefundRespDTO successOf(String channelRefundNo, LocalDateTime successTime,
String outRefundNo, Object rawData) {
PayRefundRespDTO respDTO = new PayRefundRespDTO();
respDTO.status = PayRefundStatusEnum.SUCCESS.getStatus();
respDTO.channelRefundNo = channelRefundNo;
respDTO.successTime = successTime;
// 相对通用的字段
respDTO.outRefundNo = outRefundNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建【FAILURE】状态的退款返回
*/
public static PayRefundRespDTO failureOf(String outRefundNo, Object rawData) {
return failureOf(null, null,
outRefundNo, rawData);
}
/**
* 创建【FAILURE】状态的退款返回
*/
public static PayRefundRespDTO failureOf(String channelErrorCode, String channelErrorMsg,
String outRefundNo, Object rawData) {
PayRefundRespDTO respDTO = new PayRefundRespDTO();
respDTO.status = PayRefundStatusEnum.FAILURE.getStatus();
respDTO.channelErrorCode = channelErrorCode;
respDTO.channelErrorMsg = channelErrorMsg;
// 相对通用的字段
respDTO.outRefundNo = outRefundNo;
respDTO.rawData = rawData;
return respDTO;
}
}

View File

@@ -0,0 +1,70 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.hibernate.validator.constraints.URL;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
/**
* 统一 退款 Request DTO
*
* @author jason
*/
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PayRefundUnifiedReqDTO {
/**
* 外部订单号
*
* 对应 PayOrderExtensionDO 的 no 字段
*/
@NotEmpty(message = "外部订单编号不能为空")
private String outTradeNo;
/**
* 外部退款号
*
* 对应 PayRefundDO 的 no 字段
*/
@NotEmpty(message = "退款请求单号不能为空")
private String outRefundNo;
/**
* 退款原因
*/
@NotEmpty(message = "退款原因不能为空")
private String reason;
/**
* 支付金额,单位:分
*
* 目前微信支付在退款的时候,必须传递该字段
*/
@NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer payPrice;
/**
* 退款金额,单位:分
*/
@NotNull(message = "退款金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer refundPrice;
/**
* 退款结果的 notify 回调地址
*/
@NotEmpty(message = "支付结果的回调地址不能为空")
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
private String notifyUrl;
}

View File

@@ -0,0 +1,116 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer;
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 统一转账 Response DTO
*
* @author jason
*/
@Data
public class PayTransferRespDTO {
/**
* 转账状态
*
* 关联 {@link PayTransferStatusEnum}
*/
private Integer status;
/**
* 外部转账单号
*/
private String outTransferNo;
/**
* 支付渠道编号
*/
private String channelTransferNo;
/**
* 支付成功时间
*/
private LocalDateTime successTime;
/**
* 原始的返回结果
*/
private Object rawData;
/**
* 调用渠道的错误码
*/
private String channelErrorCode;
/**
* 调用渠道报错时,错误信息
*/
private String channelErrorMsg;
/**
* 渠道 package 信息
*
* 特殊:目前只有微信转账有这个东西!!!
* @see <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012716430">JSAPI 调起用户确认收款</a>
*/
private String channelPackageInfo;
/**
* 创建【WAITING】状态的转账返回
*/
public static PayTransferRespDTO waitingOf(String channelTransferNo,
String outTransferNo, Object rawData) {
PayTransferRespDTO respDTO = new PayTransferRespDTO();
respDTO.status = PayTransferStatusEnum.WAITING.getStatus();
respDTO.channelTransferNo = channelTransferNo;
respDTO.outTransferNo = outTransferNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建【IN_PROGRESS】状态的转账返回
*/
public static PayTransferRespDTO processingOf(String channelTransferNo,
String outTransferNo, Object rawData) {
PayTransferRespDTO respDTO = new PayTransferRespDTO();
respDTO.status = PayTransferStatusEnum.PROCESSING.getStatus();
respDTO.channelTransferNo = channelTransferNo;
respDTO.outTransferNo = outTransferNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建【CLOSED】状态的转账返回
*/
public static PayTransferRespDTO closedOf(String channelErrorCode, String channelErrorMsg,
String outTransferNo, Object rawData) {
PayTransferRespDTO respDTO = new PayTransferRespDTO();
respDTO.status = PayTransferStatusEnum.CLOSED.getStatus();
respDTO.channelErrorCode = channelErrorCode;
respDTO.channelErrorMsg = channelErrorMsg;
// 相对通用的字段
respDTO.outTransferNo = outTransferNo;
respDTO.rawData = rawData;
return respDTO;
}
/**
* 创建【SUCCESS】状态的转账返回
*/
public static PayTransferRespDTO successOf(String channelTransferNo, LocalDateTime successTime,
String outTransferNo, Object rawData) {
PayTransferRespDTO respDTO = new PayTransferRespDTO();
respDTO.status = PayTransferStatusEnum.SUCCESS.getStatus();
respDTO.channelTransferNo = channelTransferNo;
respDTO.successTime = successTime;
// 相对通用的字段
respDTO.outTransferNo = outTransferNo;
respDTO.rawData = rawData;
return respDTO;
}
}

View File

@@ -0,0 +1,73 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.URL;
import java.util.Map;
/**
* 统一转账 Request DTO
*
* @author jason
*/
@Data
public class PayTransferUnifiedReqDTO {
/**
* 用户 IP
*/
@NotEmpty(message = "用户 IP 不能为空")
private String userIp;
/**
* 外部转账单编号
*/
@NotEmpty(message = "外部转账单编号不能为空")
private String outTransferNo;
/**
* 转账金额,单位:分
*/
@NotNull(message = "转账金额不能为空")
@Min(value = 1, message = "转账金额必须大于零")
private Integer price;
/**
* 转账标题
*/
@NotEmpty(message = "转账标题不能为空")
@Length(max = 128, message = "转账标题不能超过 128")
private String subject;
/**
* 收款人账号
*
* 微信场景下openid
* 支付宝场景下:支付宝账号
*/
@NotEmpty(message = "收款人账号不能为空")
private String userAccount;
/**
* 收款人姓名
*/
private String userName;
/**
* 支付渠道的额外参数
*
* 微信支付sceneId 和 scene_report_infos 字段,必须传递;参考 <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012711988#%EF%BC%883%EF%BC%89%E6%8C%89%E8%BD%AC%E8%B4%A6%E5%9C%BA%E6%99%AF%E6%8A%A5%E5%A4%87%E8%83%8C%E6%99%AF%E4%BF%A1%E6%81%AF">按转账场景报备背景信息</>
*/
private Map<String, String> channelExtras;
/**
* 转账结果的 notify 回调地址
*/
@NotEmpty(message = "转账结果的回调地址不能为空")
@URL(message = "转账结果的 notify 回调地址必须是 URL 格式")
private String notifyUrl;
}

View File

@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.exception;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 支付系统异常 Exception
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PayClientException extends RuntimeException {
public PayClientException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,251 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.exception.PayClientException;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/**
* 支付客户端的抽象类,提供模板方法,减少子类的冗余代码
*
* @author 芋道源码
*/
@Slf4j
public abstract class AbstractPayClient<Config extends PayClientConfig> implements PayClient<Config> {
/**
* 渠道编号
*/
private final Long channelId;
/**
* 渠道编码
*/
@SuppressWarnings("FieldCanBeLocal")
private final String channelCode;
/**
* 支付配置
*/
protected Config config;
public AbstractPayClient(Long channelId, String channelCode, Config config) {
this.channelId = channelId;
this.channelCode = channelCode;
this.config = config;
}
/**
* 初始化
*/
public final void init() {
doInit();
log.debug("[init][客户端({}) 初始化完成]", getId());
}
/**
* 自定义初始化
*/
protected abstract void doInit();
public final void refresh(Config config) {
// 判断是否更新
if (config.equals(this.config)) {
return;
}
log.info("[refresh][客户端({})发生变化,重新初始化]", getId());
this.config = config;
// 初始化
this.init();
}
@Override
public Long getId() {
return channelId;
}
@Override
public Config getConfig() {
return config;
}
// ============ 支付相关 ==========
@Override
public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
ValidationUtils.validate(reqDTO);
// 执行统一下单
PayOrderRespDTO resp;
try {
resp = doUnifiedOrder(reqDTO);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
// 系统异常,则包装成 PayException 异常抛出
log.error("[unifiedOrder][客户端({}) request({}) 发起支付异常]",
getId(), toJsonString(reqDTO), ex);
throw buildPayException(ex);
}
return resp;
}
protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
throws Throwable;
@Override
public final PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) {
try {
return doParseOrderNotify(params, body, headers);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[parseOrderNotify][客户端({}) params({}) body({}) headers({}) 解析失败]",
getId(), params, body, headers, ex);
throw buildPayException(ex);
}
}
protected abstract PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers)
throws Throwable;
@Override
public final PayOrderRespDTO getOrder(String outTradeNo) {
try {
return doGetOrder(outTradeNo);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[getOrder][客户端({}) outTradeNo({}) 查询支付单异常]",
getId(), outTradeNo, ex);
throw buildPayException(ex);
}
}
protected abstract PayOrderRespDTO doGetOrder(String outTradeNo)
throws Throwable;
// ============ 退款相关 ==========
@Override
public final PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
ValidationUtils.validate(reqDTO);
// 执行统一退款
PayRefundRespDTO resp;
try {
resp = doUnifiedRefund(reqDTO);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
// 系统异常,则包装成 PayException 异常抛出
log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]",
getId(), toJsonString(reqDTO), ex);
throw buildPayException(ex);
}
return resp;
}
protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
@Override
public final PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) {
try {
return doParseRefundNotify(params, body, headers);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[parseRefundNotify][客户端({}) params({}) body({}) headers({}) 解析失败]",
getId(), params, body, headers, ex);
throw buildPayException(ex);
}
}
protected abstract PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers)
throws Throwable;
@Override
public final PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo) {
try {
return doGetRefund(outTradeNo, outRefundNo);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[getRefund][客户端({}) outTradeNo({}) outRefundNo({}) 查询退款单异常]",
getId(), outTradeNo, outRefundNo, ex);
throw buildPayException(ex);
}
}
protected abstract PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo)
throws Throwable;
@Override
public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
PayTransferRespDTO resp;
try {
resp = doUnifiedTransfer(reqDTO);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
// 系统异常,则包装成 PayException 异常抛出
log.error("[unifiedTransfer][客户端({}) request({}) 发起转账异常]",
getId(), toJsonString(reqDTO), ex);
throw buildPayException(ex);
}
return resp;
}
@Override
public final PayTransferRespDTO parseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
try {
return doParseTransferNotify(params, body, headers);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[doParseTransferNotify][客户端({}) params({}) body({}) headers({}) 解析失败]",
getId(), params, body, headers, ex);
throw buildPayException(ex);
}
}
protected abstract PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers)
throws Throwable;
@Override
public final PayTransferRespDTO getTransfer(String outTradeNo) {
try {
return doGetTransfer(outTradeNo);
} catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
throw ex;
} catch (Throwable ex) {
log.error("[getTransfer][客户端({}) outTradeNo({}) 查询转账单异常]",
getId(), outTradeNo, ex);
throw buildPayException(ex);
}
}
protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO)
throws Throwable;
protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo)
throws Throwable;
// ========== 各种工具方法 ==========
private PayClientException buildPayException(Throwable ex) {
if (ex instanceof PayClientException) {
return (PayClientException) ex;
}
throw new PayClientException(ex);
}
}

View File

@@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClientConfig;
import lombok.Data;
import jakarta.validation.Validator;
/**
* 无需任何配置 PayClientConfig 实现类
*
* @author jason
*/
@Data
public class NonePayClientConfig implements PayClientConfig {
/**
* 配置名称
* <p>
* 如果不加任何属性JsonUtils.parseObject2 解析会报错,所以暂时加个名称
*/
private String name;
public NonePayClientConfig(){
this.name = "none-config";
}
@Override
public void validate(Validator validator) {
// 无任何配置不需要校验
}
}

View File

@@ -0,0 +1,97 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.TypeUtil;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.*;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.wallet.WalletPayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.*;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.mock.MockPayClient;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static cn.iocoder.yudao.module.pay.enums.PayChannelEnum.*;
/**
* 支付客户端的工厂实现类
*
* @author 芋道源码
*/
@Slf4j
public class PayClientFactoryImpl implements PayClientFactory {
/**
* 支付客户端 Map
*
* key渠道编号
*/
private final ConcurrentMap<Long, AbstractPayClient<?>> clients = new ConcurrentHashMap<>();
/**
* 支付客户端 Class Map
*/
private final Map<PayChannelEnum, Class<? extends PayClient<?>>> clientClass = new ConcurrentHashMap<>();
public PayClientFactoryImpl() {
// 微信支付客户端
clientClass.put(WX_PUB, WxPubPayClient.class);
clientClass.put(WX_LITE, WxLitePayClient.class);
clientClass.put(WX_APP, WxAppPayClient.class);
clientClass.put(WX_BAR, WxBarPayClient.class);
clientClass.put(WX_NATIVE, WxNativePayClient.class);
clientClass.put(WX_WAP, WxWapPayClient.class);
// 支付包支付客户端
clientClass.put(ALIPAY_WAP, AlipayWapPayClient.class);
clientClass.put(ALIPAY_QR, AlipayQrPayClient.class);
clientClass.put(ALIPAY_APP, AlipayAppPayClient.class);
clientClass.put(ALIPAY_PC, AlipayPcPayClient.class);
clientClass.put(ALIPAY_BAR, AlipayBarPayClient.class);
// 钱包支付客户端
clientClass.put(WALLET, WalletPayClient.class);
// Mock 支付客户端
clientClass.put(MOCK, MockPayClient.class);
}
@Override
public PayClient getPayClient(Long channelId) {
AbstractPayClient<?> client = clients.get(channelId);
if (client == null) {
log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId);
}
return client;
}
@Override
@SuppressWarnings("unchecked")
public <Config extends PayClientConfig> PayClient createOrUpdatePayClient(Long channelId, String channelCode,
Config config) {
AbstractPayClient<Config> client = (AbstractPayClient<Config>) clients.get(channelId);
if (client == null) {
client = this.createPayClient(channelId, channelCode, config);
client.init();
clients.put(client.getId(), client);
} else {
client.refresh(config);
}
return client;
}
@SuppressWarnings("unchecked")
private <Config extends PayClientConfig> AbstractPayClient<Config> createPayClient(Long channelId, String channelCode,
Config config) {
PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode);
Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelCode));
Class<?> payClientClass = clientClass.get(channelEnum);
Assert.notNull(payClientClass, String.format("支付渠道(%s) Class 为空", channelCode));
return (AbstractPayClient<Config>) ReflectUtil.newInstance(payClientClass, channelId, config);
}
}

View File

@@ -0,0 +1,379 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.AbstractPayClient;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConfig;
import com.alipay.api.AlipayResponse;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.*;
import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.internal.util.AntCertificationUtil;
import com.alipay.api.internal.util.codec.Base64;
import com.alipay.api.request.*;
import com.alipay.api.response.*;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.security.cert.X509Certificate;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_PUBLIC_KEY;
/**
* 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款)
*
* @author jason
*/
@Slf4j
public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPayClientConfig> {
@Getter // 仅用于单测场景
protected DefaultAlipayClient client;
public AbstractAlipayPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) {
super(channelId, channelCode, config);
}
@Override
@SneakyThrows
protected void doInit() {
AlipayConfig alipayConfig = new AlipayConfig();
BeanUtil.copyProperties(config, alipayConfig, false);
this.client = new DefaultAlipayClient(alipayConfig);
}
// ============ 支付相关 ==========
/**
* 构造支付关闭的 {@link PayOrderRespDTO} 对象
*
* @return 支付关闭的 {@link PayOrderRespDTO} 对象
*/
protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) {
Assert.isFalse(response.isSuccess());
return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
reqDTO.getOutTradeNo(), response);
}
@Override
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) throws Throwable {
// 1. 校验回调数据
verifyNotifyData(params);
// 2. 解析订单的状态
// 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
Integer status = parseStatus(bodyObj.get("trade_status"));
// 特殊逻辑: 支付宝没有退款成功的状态,所以,如果有退款金额,我们认为是退款成功
if (MapUtil.getDouble(bodyObj, "refund_fee", 0D) > 0) {
status = PayOrderStatusEnum.REFUND.getStatus();
}
Assert.notNull(status, (Supplier<Throwable>) () -> {
throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body));
});
return PayOrderRespDTO.of(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")),
bodyObj.get("out_trade_no"), body);
}
@Override
protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
// 1.1 构建 AlipayTradeRefundModel 请求
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo(outTradeNo);
// 1.2 构建 AlipayTradeQueryRequest 请求
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizModel(model);
AlipayTradeQueryResponse response;
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
// 证书模式
response = client.certificateExecute(request);
} else {
response = client.execute(request);
}
if (!response.isSuccess()) { // 不成功,例如说订单不存在
return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
outTradeNo, response);
}
// 2.2 解析订单的状态
Integer status = parseStatus(response.getTradeStatus());
Assert.notNull(status, () -> {
throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
});
return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
outTradeNo, response);
}
private static Integer parseStatus(String tradeStatus) {
return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusEnum.WAITING.getStatus()
: ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusEnum.SUCCESS.getStatus()
: Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusEnum.CLOSED.getStatus() : null;
}
// ============ 退款相关 ==========
/**
* 支付宝统一的退款接口 alipay.trade.refund
*
* @param reqDTO 退款请求 request DTO
* @return 退款请求 Response
*/
@Override
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradeRefundModel 请求
AlipayTradeRefundModel model = new AlipayTradeRefundModel();
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setOutRequestNo(reqDTO.getOutRefundNo());
model.setRefundAmount(formatAmount(reqDTO.getRefundPrice()));
model.setRefundReason(reqDTO.getReason());
// 1.2 构建 AlipayTradePayRequest 请求
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
request.setBizModel(model);
// 2.1 执行请求
AlipayTradeRefundResponse response;
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
response = client.certificateExecute(request);
} else {
response = client.execute(request);
}
if (!response.isSuccess()) {
// 当出现 ACQ.SYSTEM_ERROR, 退款可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询
if (ObjectUtils.equalsAny(response.getSubCode(), "ACQ.SYSTEM_ERROR", "SYSTEM_ERROR")) {
return PayRefundRespDTO.waitingOf(null, reqDTO.getOutRefundNo(), response);
}
return PayRefundRespDTO.failureOf(response.getSubCode(), response.getSubMsg(), reqDTO.getOutRefundNo(), response);
}
// 2.2 创建返回结果
// 支付宝只要退款调用返回 success就认为退款成功不需要回调。具体可见 parseNotify 方法的说明。
// 另外,支付宝没有退款单号,所以不用设置
return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()),
reqDTO.getOutRefundNo(), response);
}
@Override
public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) {
// 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
// ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
// ② 全部退款Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
// 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。
// 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。
throw new UnsupportedOperationException("支付宝无退款回调");
}
@Override
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws AlipayApiException {
// 1.1 构建 AlipayTradeFastpayRefundQueryModel 请求
AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel();
model.setOutTradeNo(outTradeNo);
model.setOutRequestNo(outRefundNo);
model.setQueryOptions(Collections.singletonList("gmt_refund_pay"));
// 1.2 构建 AlipayTradeFastpayRefundQueryRequest 请求
AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();
request.setBizModel(model);
// 2.1 执行请求
AlipayTradeFastpayRefundQueryResponse response;
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
response = client.certificateExecute(request);
} else {
response = client.execute(request);
}
if (!response.isSuccess()) {
// 明确不存在的情况,应该就是失败,可进行关闭
if (ObjectUtils.equalsAny(response.getSubCode(), "TRADE_NOT_EXIST", "ACQ.TRADE_NOT_EXIST")) {
return PayRefundRespDTO.failureOf(outRefundNo, response);
}
// 可能存在“ACQ.SYSTEM_ERROR”系统错误等情况所以返回 WAIT 继续等待
return PayRefundRespDTO.waitingOf(null, outRefundNo, response);
}
// 2.2 创建返回结果
if (Objects.equals(response.getRefundStatus(), "REFUND_SUCCESS")) {
return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()),
outRefundNo, response);
}
return PayRefundRespDTO.waitingOf(null, outRefundNo, response);
}
@Override
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException {
// 补充说明https://opendocs.alipay.com/open/03dcrm?pathHash=4ba3b20b
// 沙箱环境:可通过 公钥模式 或 公钥证书模式 加签进行调试
// 生产环境:必须使用 公钥证书模式 加签请求强校验请求
// 1.1 构建 AlipayFundTransUniTransferModel
AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
// ① 通用的参数
model.setTransAmount(formatAmount(reqDTO.getPrice())); // 转账金额
model.setOrderTitle(reqDTO.getSubject()); // 转账业务的标题,用于在支付宝用户的账单里显示。
model.setOutBizNo(reqDTO.getOutTransferNo());
model.setProductCode("TRANS_ACCOUNT_NO_PWD"); // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD
model.setBizScene("DIRECT_TRANSFER"); // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER
if (reqDTO.getChannelExtras() != null) {
model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
}
// ② 个性化的参数
Participant payeeInfo = new Participant();
payeeInfo.setIdentityType("ALIPAY_LOGON_ID"); // 暂时只考虑转账到支付宝,银行没有权限 https://opendocs.alipay.com/open/02byvc?scene=66dd06f5a923403393b85de68d3c0055
payeeInfo.setIdentity(reqDTO.getUserAccount()); // 支付宝登录号
payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名
model.setPayeeInfo(payeeInfo);
// 1.2 构建 AlipayFundTransUniTransferRequest
AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
request.setBizModel(model);
// 2.1 执行请求
AlipayFundTransUniTransferResponse response;
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
response = client.certificateExecute(request);
} else {
response = client.execute(request);
}
if (!response.isSuccess()) {
// 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询,或相同 outBizNo 重新发起转账
// 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询
if (ObjectUtils.equalsAny(response.getSubCode(),"PAYMENT_INFO_INCONSISTENCY", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
}
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
reqDTO.getOutTransferNo(), response);
}
// 2.2 处理结果
if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
reqDTO.getOutTransferNo(), response);
}
if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
return PayTransferRespDTO.processingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response);
}
return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
response.getOutBizNo(), response);
}
@Override
protected PayTransferRespDTO doGetTransfer(String outTradeNo) throws Throwable {
// 1.1 构建 AlipayFundTransCommonQueryModel
AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel();
model.setProductCode("TRANS_ACCOUNT_NO_PWD");
model.setBizScene("DIRECT_TRANSFER"); //业务场景
model.setOutBizNo(outTradeNo);
// 1.2 构建 AlipayFundTransCommonQueryRequest
AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest();
request.setBizModel(model);
// 2.1 执行请求
AlipayFundTransCommonQueryResponse response;
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
response = client.certificateExecute(request);
} else {
response = client.execute(request);
}
if (!response.isSuccess()) {
// 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
// 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
if (ObjectUtils.equalsAny(response.getSubCode(), "ORDER_NOT_EXIST", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
return PayTransferRespDTO.waitingOf(null, outTradeNo, response);
}
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
outTradeNo, response);
}
// 2.2 处理返回结果
if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
outTradeNo, response);
}
if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
return PayTransferRespDTO.processingOf(response.getOrderId(), outTradeNo, response);
}
return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()),
response.getOutBizNo(), response);
}
// TODO @芋艿:由于支付宝一直没触发回调,这个方法暂时没办法测试
@Override
protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers)
throws Throwable {
// 1. 校验回调数据
verifyNotifyData(params);
// 2. 解析转账状态
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
String status = bodyObj.get("status");
String outBizNo = bodyObj.get("out_biz_no");
String orderId = bodyObj.get("order_id");
String payDate = bodyObj.get("pay_date");
// 3. 根据状态返回对应的结果
if (Objects.equals(status, "SUCCESS")) {
return PayTransferRespDTO.successOf(orderId, parseTime(payDate), outBizNo, bodyObj);
}
if (Objects.equals(status, "DEALING")) {
return PayTransferRespDTO.processingOf(orderId, outBizNo, bodyObj);
}
if (ObjectUtils.equalsAny(status, "REFUND", "FAIL")) {
return PayTransferRespDTO.closedOf(bodyObj.get("sub_code"), bodyObj.get("sub_msg"),
outBizNo, bodyObj);
}
return PayTransferRespDTO.waitingOf(orderId, outBizNo, bodyObj);
}
/**
* 校验回调数据
*
* @param params 回调参数
* @throws Throwable 验签失败时抛出异常
*/
protected void verifyNotifyData(Map<String, String> params) throws Throwable {
boolean verify;
if (Objects.equals(config.getMode(), MODE_PUBLIC_KEY)) {
verify = AlipaySignature.rsaCheckV1(params, config.getAlipayPublicKey(),
StandardCharsets.UTF_8.name(), config.getSignType());
} else if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
// 由于 rsaCertCheckV1 的第二个参数是 path所以不能这么调用通过阅读源码发现可以采用如下方式
X509Certificate cert = AntCertificationUtil.getCertFromContent(config.getAlipayPublicCertContent());
String publicKey = Base64.encodeBase64String(cert.getEncoded());
verify = AlipaySignature.rsaCheckV1(params, publicKey,
StandardCharsets.UTF_8.name(), config.getSignType());
} else {
throw new IllegalArgumentException("未知的公钥类型:" + config.getMode());
}
Assert.isTrue(verify, "验签结果不通过");
}
// ========== 各种工具方法 ==========
protected String formatAmount(Integer amount) {
return String.valueOf(amount / 100.0);
}
protected String formatTime(LocalDateTime time) {
return LocalDateTimeUtil.format(time, NORM_DATETIME_FORMATTER);
}
protected LocalDateTime parseTime(String str) {
return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER);
}
}

View File

@@ -0,0 +1,60 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradeAppPayModel;
import com.alipay.api.request.AlipayTradeAppPayRequest;
import com.alipay.api.response.AlipayTradeAppPayResponse;
import lombok.extern.slf4j.Slf4j;
/**
* 支付宝【App 支付】的 PayClient 实现类
*
* 文档:<a href="https://opendocs.alipay.com/open/02e7gq">App 支付</a>
*
* // TODO 芋艿:未详细测试,因为手头没 App
*
* @author 芋道源码
*/
@Slf4j
public class AlipayAppPayClient extends AbstractAlipayPayClient {
public AlipayAppPayClient(Long channelId, AlipayPayClientConfig config) {
super(channelId, PayChannelEnum.ALIPAY_APP.getCode(), config);
}
@Override
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradeAppPayModel 请求
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody() + "test");
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
model.setProductCode("QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品
// ② 个性化的参数【无】
// ③ 支付宝扫码支付只有一种展示
String displayMode = PayOrderDisplayModeEnum.APP.getMode();
// 1.2 构建 AlipayTradePrecreateRequest 请求
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setBizModel(model);
request.setNotifyUrl(reqDTO.getNotifyUrl());
request.setReturnUrl(reqDTO.getReturnUrl());
// 2.1 执行请求
AlipayTradeAppPayResponse response = client.sdkExecute(request);
// 2.2 处理结果
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
reqDTO.getOutTradeNo(), response);
}
}

View File

@@ -0,0 +1,86 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradePayModel;
import com.alipay.api.request.AlipayTradePayRequest;
import com.alipay.api.response.AlipayTradePayResponse;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
/**
* 支付宝【条码支付】的 PayClient 实现类
*
* 文档:<a href="https://opendocs.alipay.com/open/194/105072">当面付</a>
*
* @author 芋道源码
*/
@Slf4j
public class AlipayBarPayClient extends AbstractAlipayPayClient {
public AlipayBarPayClient(Long channelId, AlipayPayClientConfig config) {
super(channelId, PayChannelEnum.ALIPAY_BAR.getCode(), config);
}
@Override
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code");
if (StrUtil.isEmpty(authCode)) {
throw exception0(BAD_REQUEST.getCode(), "条形码不能为空");
}
// 1.1 构建 AlipayTradePayModel 请求
AlipayTradePayModel model = new AlipayTradePayModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setScene("bar_code"); // 当面付条码支付场景
// ② 个性化的参数
model.setAuthCode(authCode);
// ③ 支付宝条码支付只有一种展示
String displayMode = PayOrderDisplayModeEnum.BAR_CODE.getMode();
// 1.2 构建 AlipayTradePayRequest 请求
AlipayTradePayRequest request = new AlipayTradePayRequest();
request.setBizModel(model);
request.setNotifyUrl(reqDTO.getNotifyUrl());
request.setReturnUrl(reqDTO.getReturnUrl());
// 2.1 执行请求
AlipayTradePayResponse response;
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
// 证书模式
response = client.certificateExecute(request);
} else {
response = client.execute(request);
}
// 2.2 处理结果
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
if ("10000".equals(response.getCode())) { // 免密支付
LocalDateTime successTime = LocalDateTimeUtil.of(response.getGmtPayment());
return PayOrderRespDTO.successOf(response.getTradeNo(), response.getBuyerUserId(), successTime,
response.getOutTradeNo(), response)
.setDisplayMode(displayMode).setDisplayContent("");
}
// 大额支付,需要用户输入密码,所以返回 waiting。此时前端一般会进行轮询
return PayOrderRespDTO.waitingOf(displayMode, "",
reqDTO.getOutTradeNo(), response);
}
}

View File

@@ -0,0 +1,128 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClientConfig;
import lombok.Data;
import jakarta.validation.Validator;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
/**
* 支付宝的 PayClientConfig 实现类
* 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性
*
* @author 芋道源码
*/
@Data
public class AlipayPayClientConfig implements PayClientConfig {
/**
* 公钥类型 - 公钥模式
*/
public static final Integer MODE_PUBLIC_KEY = 1;
/**
* 公钥类型 - 证书模式
*/
public static final Integer MODE_CERTIFICATE = 2;
/**
* 接口内容加密方式 - AES 加密
*/
public static final String ENC_TYPE_AES = "AES";
/**
* 签名算法类型 - RSA
*/
public static final String SIGN_TYPE_DEFAULT = "RSA2";
/**
* 网关地址
*
* 1. <a href="https://openapi.alipay.com/gateway.do">生产环境</a>
* 2. <a href="https://openapi-sandbox.dl.alipaydev.com/gateway.do">沙箱环境</a>
*/
@NotBlank(message = "网关地址不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
private String serverUrl;
/**
* 开放平台上创建的应用的 ID
*/
@NotBlank(message = "开放平台上创建的应用的 ID不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
private String appId;
/**
* 签名算法类型推荐RSA2
* <p>
* {@link #SIGN_TYPE_DEFAULT}
*/
@NotBlank(message = "签名算法类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
private String signType;
/**
* 公钥类型
* 1. {@link #MODE_PUBLIC_KEY} 情况privateKey + alipayPublicKey
* 2. {@link #MODE_CERTIFICATE} 情况appCertContent + alipayPublicCertContent + rootCertContent
*/
@NotNull(message = "公钥类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class})
private Integer mode;
// ========== 公钥模式 ==========
/**
* 商户私钥
*/
@NotBlank(message = "商户私钥不能为空", groups = {ModePublicKey.class})
private String privateKey;
/**
* 支付宝公钥字符串
*/
@NotBlank(message = "支付宝公钥字符串不能为空", groups = {ModePublicKey.class})
private String alipayPublicKey;
// ========== 证书模式 ==========
/**
* 指定商户公钥应用证书内容字符串
*/
@NotBlank(message = "指定商户公钥应用证书内容不能为空", groups = {ModeCertificate.class})
private String appCertContent;
/**
* 指定支付宝公钥证书内容字符串
*/
@NotBlank(message = "指定支付宝公钥证书内容不能为空", groups = {ModeCertificate.class})
private String alipayPublicCertContent;
/**
* 指定根证书内容字符串
*/
@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 {
}
public interface ModeCertificate {
}
@Override
public void validate(Validator validator) {
ValidationUtils.validate(validator, this,
MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class);
}
}

View File

@@ -0,0 +1,70 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.Method;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradePagePayModel;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import lombok.extern.slf4j.Slf4j;
import java.util.Objects;
/**
* 支付宝【PC 网站】的 PayClient 实现类
*
* 文档:<a href="https://opendocs.alipay.com/open/270/105898">电脑网站支付</a>
*
* @author XGD
*/
@Slf4j
public class AlipayPcPayClient extends AbstractAlipayPayClient {
public AlipayPcPayClient(Long channelId, AlipayPayClientConfig config) {
super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config);
}
@Override
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradePagePayModel 请求
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
// ② 个性化的参数
// 如果想弄更多个性化的参数,可参考 https://www.pingxx.com/api/支付渠道 extra 参数说明.html 的 alipay_pc_direct 部分进行拓展
model.setQrPayMode("2"); // 跳转模式 - 订单码效果参见https://help.pingxx.com/article/1137360/
// ③ 支付宝 PC 支付有两种展示模式FORM、URL
String displayMode = ObjectUtil.defaultIfNull(reqDTO.getDisplayMode(),
PayOrderDisplayModeEnum.URL.getMode());
// 1.2 构建 AlipayTradePagePayRequest 请求
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setBizModel(model);
request.setNotifyUrl(reqDTO.getNotifyUrl());
request.setReturnUrl(reqDTO.getReturnUrl());
// 2.1 执行请求
AlipayTradePagePayResponse response;
if (Objects.equals(displayMode, PayOrderDisplayModeEnum.FORM.getMode())) {
response = client.pageExecute(request, Method.POST.name()); // 需要特殊使用 POST 请求
} else {
response = client.pageExecute(request, Method.GET.name());
}
// 2.2 处理结果
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
reqDTO.getOutTradeNo(), response);
}
}

View File

@@ -0,0 +1,66 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradePrecreateModel;
import com.alipay.api.request.AlipayTradePrecreateRequest;
import com.alipay.api.response.AlipayTradePrecreateResponse;
import lombok.extern.slf4j.Slf4j;
import java.util.Objects;
import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
/**
* 支付宝【扫码支付】的 PayClient 实现类
*
* 文档:<a href="https://opendocs.alipay.com/apis/02890k">扫码支付</a>
*
* @author 芋道源码
*/
@Slf4j
public class AlipayQrPayClient extends AbstractAlipayPayClient {
public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) {
super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config);
}
@Override
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradePrecreateModel 请求
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT
// ② 个性化的参数【无】
// ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开
String displayMode = PayOrderDisplayModeEnum.QR_CODE.getMode();
// 1.2 构建 AlipayTradePrecreateRequest 请求
AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
request.setBizModel(model);
request.setNotifyUrl(reqDTO.getNotifyUrl());
request.setReturnUrl(reqDTO.getReturnUrl());
// 2.1 执行请求
AlipayTradePrecreateResponse response;
if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
// 证书模式
response = client.certificateExecute(request);
} else {
response = client.execute(request);
}
// 2.2 处理结果
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
return PayOrderRespDTO.waitingOf(displayMode, response.getQrCode(),
reqDTO.getOutTradeNo(), response);
}
}

View File

@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay;
import cn.hutool.http.Method;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradeWapPayModel;
import com.alipay.api.request.AlipayTradeWapPayRequest;
import com.alipay.api.response.AlipayTradeWapPayResponse;
import lombok.extern.slf4j.Slf4j;
/**
* 支付宝【Wap 网站】的 PayClient 实现类
*
* 文档:<a href="https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay">手机网站支付接口</a>
*
* @author 芋道源码
*/
@Slf4j
public class AlipayWapPayClient extends AbstractAlipayPayClient {
public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) {
super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config);
}
@Override
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradeWapPayModel 请求
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
// ① 通用的参数
model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
// ② 个性化的参数【无】
// ③ 支付宝 Wap 支付只有一种展示URL
String displayMode = PayOrderDisplayModeEnum.URL.getMode();
// 1.2 构建 AlipayTradeWapPayRequest 请求
AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
request.setBizModel(model);
request.setNotifyUrl(reqDTO.getNotifyUrl());
request.setReturnUrl(reqDTO.getReturnUrl());
model.setQuitUrl(reqDTO.getReturnUrl());
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
// 2.1 执行请求
AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name());
// 2.2 处理结果
if (!response.isSuccess()) {
return buildClosedPayOrderRespDTO(reqDTO, response);
}
return PayOrderRespDTO.waitingOf(displayMode, response.getBody(),
reqDTO.getOutTradeNo(), response);
}
}

View File

@@ -0,0 +1,84 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.mock;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.NonePayClientConfig;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 模拟支付的 PayClient 实现类
*
* 模拟支付返回结果都是成功,方便大家日常流畅
*
* @author jason
*/
public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
private static final String MOCK_RESP_SUCCESS_DATA = "MOCK_SUCCESS";
public MockPayClient(Long channelId, NonePayClientConfig config) {
super(channelId, PayChannelEnum.MOCK.getCode(), config);
}
@Override
protected void doInit() {
}
@Override
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
return PayOrderRespDTO.successOf("MOCK-P-" + reqDTO.getOutTradeNo(), "", LocalDateTime.now(),
reqDTO.getOutTradeNo(), MOCK_RESP_SUCCESS_DATA);
}
@Override
protected PayOrderRespDTO doGetOrder(String outTradeNo) {
return PayOrderRespDTO.successOf("MOCK-P-" + outTradeNo, "", LocalDateTime.now(),
outTradeNo, MOCK_RESP_SUCCESS_DATA);
}
@Override
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
return PayRefundRespDTO.successOf("MOCK-R-" + reqDTO.getOutRefundNo(), LocalDateTime.now(),
reqDTO.getOutRefundNo(), MOCK_RESP_SUCCESS_DATA);
}
@Override
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) {
return PayRefundRespDTO.successOf("MOCK-R-" + outRefundNo, LocalDateTime.now(),
outRefundNo, MOCK_RESP_SUCCESS_DATA);
}
@Override
protected PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) {
throw new UnsupportedOperationException("未实现");
}
@Override
protected PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) {
throw new UnsupportedOperationException("模拟支付无退款回调");
}
@Override
protected PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) {
throw new UnsupportedOperationException("模拟支付无支付回调");
}
@Override
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
throw new UnsupportedOperationException("待实现");
}
@Override
protected PayTransferRespDTO doGetTransfer(String outTradeNo) {
throw new UnsupportedOperationException("待实现");
}
}

View File

@@ -1,20 +1,20 @@
package cn.iocoder.yudao.module.pay.framework.pay.core;
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.wallet;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import cn.iocoder.yudao.module.pay.enums.transfer.PayTransferStatusEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.NonePayClientConfig;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.transfer.PayTransferDO;
@@ -51,7 +51,7 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
private PayRefundService refundService;
private PayTransferService transferService;
public WalletPayClient(Long channelId, NonePayClientConfig config) {
public WalletPayClient(Long channelId, NonePayClientConfig config) {
super(channelId, PayChannelEnum.WALLET.getCode(), config);
}
@@ -163,12 +163,12 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
outRefundNo, "");
}
// 退款失败
if (PayRefundStatusRespEnum.isFailure(payRefund.getStatus())) {
if (PayRefundStatusEnum.isFailure(payRefund.getStatus())) {
return PayRefundRespDTO.failureOf(payRefund.getChannelErrorCode(), payRefund.getChannelErrorMsg(),
outRefundNo, "");
}
// 退款成功
if (PayRefundStatusRespEnum.isSuccess(payRefund.getStatus())) {
if (PayRefundStatusEnum.isSuccess(payRefund.getStatus())) {
PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransaction(
String.valueOf(payRefund.getId()), PayWalletBizTypeEnum.PAYMENT_REFUND);
Assert.notNull(walletTransaction, "支付退款单 {} 钱包流水不能为空", outRefundNo);
@@ -221,12 +221,12 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
PAY_ORDER_EXTENSION_NOT_FOUND.getMsg(), outTradeNo, "");
}
// 关闭状态
if (PayTransferStatusRespEnum.isClosed(transfer.getStatus())) {
if (PayTransferStatusEnum.isClosed(transfer.getStatus())) {
return PayTransferRespDTO.closedOf(transfer.getChannelErrorCode(),
transfer.getChannelErrorMsg(), outTradeNo, "");
}
// 成功状态
if (PayTransferStatusRespEnum.isSuccess(transfer.getStatus())) {
if (PayTransferStatusEnum.isSuccess(transfer.getStatus())) {
PayWalletTransactionDO walletTransaction = walletTransactionService.getWalletTransaction(
String.valueOf(transfer.getId()), PayWalletBizTypeEnum.TRANSFER);
Assert.notNull(walletTransaction, "转账单 {} 钱包流水不能为空", outTradeNo);
@@ -234,12 +234,12 @@ public class WalletPayClient extends AbstractPayClient<NonePayClientConfig> {
outTradeNo, walletTransaction);
}
// 处理中状态
if (PayTransferStatusRespEnum.isProcessing(transfer.getStatus())) {
if (PayTransferStatusEnum.isProcessing(transfer.getStatus())) {
return PayTransferRespDTO.processingOf(transfer.getChannelTransferNo(),
outTradeNo, transfer);
}
// 等待状态
if (transfer.getStatus().equals(PayTransferStatusRespEnum.WAITING.getStatus())) {
if (PayTransferStatusEnum.isWaiting(transfer.getStatus())) {
return PayTransferRespDTO.waitingOf(transfer.getChannelTransferNo(),
outTradeNo, transfer);
}

View File

@@ -0,0 +1,597 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.date.TemporalAccessorUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.AbstractPayClient;
import com.github.binarywang.wxpay.bean.notify.*;
import com.github.binarywang.wxpay.bean.request.*;
import com.github.binarywang.wxpay.bean.result.*;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsGetResult;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsNotifyResult;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsRequest;
import com.github.binarywang.wxpay.bean.transfer.TransferBillsResult;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Map;
import java.util.Objects;
import static cn.hutool.core.date.DatePattern.*;
import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2;
import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V3;
/**
* 微信支付抽象类,实现微信统一的接口、以及部分实现(退款)
*
* @author 芋道源码
*/
@Slf4j
public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientConfig> {
protected WxPayService client;
public AbstractWxPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
super(channelId, channelCode, config);
}
/**
* 初始化 client 客户端
*
* @param tradeType 交易类型
*/
protected void doInit(String tradeType) {
// 创建 config 配置
WxPayConfig payConfig = new WxPayConfig();
BeanUtil.copyProperties(config, payConfig, "keyContent", "privateKeyContent", "publicKeyContent");
payConfig.setTradeType(tradeType);
// weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
if (Objects.equals(config.getApiVersion(), API_VERSION_V2)) {
payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
} else if (Objects.equals(config.getApiVersion(), API_VERSION_V3)) {
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
payConfig.setPublicKeyPath(FileUtils.createTempFile(config.getPublicKeyContent()).getPath());
// 特殊:强制使用微信公用模式,避免灰度期间的问题!!!
payConfig.setStrictlyNeedWechatPaySerial(true);
}
// 创建 client 客户端
client = new WxPayServiceImpl();
client.setConfig(payConfig);
}
// ============ 支付相关 ==========
@Override
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
try {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doUnifiedOrderV2(reqDTO);
case API_VERSION_V3:
// TODO @芋艿:【可能是 wxjava 的 bug】参考 https://github.com/binarywang/WxJava/issues/1557
client.getConfig().setApiV3HttpClient(null);
return doUnifiedOrderV3(reqDTO);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
log.error("[doUnifiedOrder][支付({}) 发起微信支付异常", reqDTO, e);
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayOrderRespDTO.closedOf(errorCode, errorMessage,
reqDTO.getOutTradeNo(), e.getXmlString());
}
}
/**
* 【V2】调用支付渠道统一下单
*
* @param reqDTO 下单信息
* @return 各支付渠道的返回结果
*/
protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
throws Exception;
/**
* 【V3】调用支付渠道统一下单
*
* @param reqDTO 下单信息
* @return 各支付渠道的返回结果
*/
protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
throws WxPayException;
/**
* 【V2】创建微信下单请求
*
* @param reqDTO 下信息
* @return 下单请求
*/
protected WxPayUnifiedOrderRequest buildPayUnifiedOrderRequestV2(PayOrderUnifiedReqDTO reqDTO) {
return WxPayUnifiedOrderRequest.newBuilder()
.outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject())
.detail(reqDTO.getBody())
.totalFee(reqDTO.getPrice()) // 单位分
.timeExpire(formatDateV2(reqDTO.getExpireTime()))
.spbillCreateIp(reqDTO.getUserIp())
.notifyUrl(reqDTO.getNotifyUrl())
.build();
}
/**
* 【V3】创建微信下单请求
*
* @param reqDTO 下信息
* @return 下单请求
*/
protected WxPayUnifiedOrderV3Request buildPayUnifiedOrderRequestV3(PayOrderUnifiedReqDTO reqDTO) {
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(reqDTO.getOutTradeNo());
request.setDescription(reqDTO.getSubject());
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分
request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
request.setNotifyUrl(reqDTO.getNotifyUrl());
return request;
}
@Override
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doParseOrderNotifyV2(body);
case API_VERSION_V3:
return doParseOrderNotifyV3(body, headers);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
}
private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException {
// 1. 解析回调
WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
// 2. 构建结果
// V2 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂
Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ?
PayOrderStatusEnum.SUCCESS.getStatus() : PayOrderStatusEnum.CLOSED.getStatus();
return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
response.getOutTradeNo(), body);
}
private PayOrderRespDTO doParseOrderNotifyV3(String body, Map<String, String> headers) throws WxPayException {
// 1. 解析回调
SignatureHeader signatureHeader = getRequestHeader(headers);
WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, signatureHeader);
WxPayNotifyV3Result.DecryptNotifyResult result = response.getResult();
// 2. 构建结果
Integer status = parseStatus(result.getTradeState());
String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null;
return PayOrderRespDTO.of(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()),
result.getOutTradeNo(), body);
}
@Override
protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable {
try {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doGetOrderV2(outTradeNo);
case API_VERSION_V3:
return doGetOrderV3(outTradeNo);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
if (ObjectUtils.equalsAny(e.getErrCode(), "ORDERNOTEXIST", "ORDER_NOT_EXIST")) {
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayOrderRespDTO.closedOf(errorCode, errorMessage,
outTradeNo, e.getXmlString());
}
throw e;
}
}
private PayOrderRespDTO doGetOrderV2(String outTradeNo) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayOrderQueryRequest request = WxPayOrderQueryRequest.newBuilder()
.outTradeNo(outTradeNo).build();
// 执行请求
WxPayOrderQueryResult response = client.queryOrder(request);
// 转换结果
Integer status = parseStatus(response.getTradeState());
return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
outTradeNo, response);
}
private PayOrderRespDTO doGetOrderV3(String outTradeNo) throws WxPayException {
fixV3HttpClientConnectionPoolShutDown();
// 构建 WxPayUnifiedOrderRequest 对象
WxPayOrderQueryV3Request request = new WxPayOrderQueryV3Request()
.setOutTradeNo(outTradeNo);
// 执行请求
WxPayOrderQueryV3Result response = client.queryOrderV3(request);
// 转换结果
Integer status = parseStatus(response.getTradeState());
String openid = response.getPayer() != null ? response.getPayer().getOpenid() : null;
return PayOrderRespDTO.of(status, response.getTransactionId(), openid, parseDateV3(response.getSuccessTime()),
outTradeNo, response);
}
private static Integer parseStatus(String tradeState) {
switch (tradeState) {
case "NOTPAY":
case "USERPAYING": // 支付中,等待用户输入密码(条码支付独有)
return PayOrderStatusEnum.WAITING.getStatus();
case "SUCCESS":
return PayOrderStatusEnum.SUCCESS.getStatus();
case "REFUND":
return PayOrderStatusEnum.REFUND.getStatus();
case "CLOSED":
case "REVOKED": // 已撤销(刷卡支付独有)
case "PAYERROR": // 支付失败(其它原因,如银行返回失败)
return PayOrderStatusEnum.CLOSED.getStatus();
default:
throw new IllegalArgumentException(StrUtil.format("未知的支付状态({})", tradeState));
}
}
// ============ 退款相关 ==========
@Override
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
try {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doUnifiedRefundV2(reqDTO);
case API_VERSION_V3:
return doUnifiedRefundV3(reqDTO);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayRefundRespDTO.failureOf(errorCode, errorMessage,
reqDTO.getOutRefundNo(), e.getXmlString());
}
}
private PayRefundRespDTO doUnifiedRefundV2(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundRequest request = new WxPayRefundRequest()
.setOutTradeNo(reqDTO.getOutTradeNo())
.setOutRefundNo(reqDTO.getOutRefundNo())
.setRefundFee(reqDTO.getRefundPrice())
.setRefundDesc(reqDTO.getReason())
.setTotalFee(reqDTO.getPayPrice())
.setNotifyUrl(reqDTO.getNotifyUrl());
// 2.1 执行请求
WxPayRefundResult response = client.refundV2(request);
// 2.2 创建返回结果
if (Objects.equals("SUCCESS", response.getResultCode())) { // V2 情况下,不直接返回退款成功,而是等待异步通知
return PayRefundRespDTO.waitingOf(response.getRefundId(),
reqDTO.getOutRefundNo(), response);
}
return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
}
private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
fixV3HttpClientConnectionPoolShutDown();
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundV3Request request = new WxPayRefundV3Request()
.setOutTradeNo(reqDTO.getOutTradeNo())
.setOutRefundNo(reqDTO.getOutRefundNo())
.setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice())
.setTotal(reqDTO.getPayPrice()).setCurrency("CNY"))
.setReason(reqDTO.getReason())
.setNotifyUrl(reqDTO.getNotifyUrl());
// 2.1 执行请求
WxPayRefundV3Result response = client.refundV3(request);
// 2.2 创建返回结果
if (Objects.equals("SUCCESS", response.getStatus())) {
return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
reqDTO.getOutRefundNo(), response);
}
if (Objects.equals("PROCESSING", response.getStatus())) {
return PayRefundRespDTO.waitingOf(response.getRefundId(),
reqDTO.getOutRefundNo(), response);
}
return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
}
@Override
public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doParseRefundNotifyV2(body);
case API_VERSION_V3:
return parseRefundNotifyV3(body, headers);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
}
private PayRefundRespDTO doParseRefundNotifyV2(String body) throws WxPayException {
// 1. 解析回调
WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
WxPayRefundNotifyResult.ReqInfo result = response.getReqInfo();
// 2. 构建结果
if (Objects.equals("SUCCESS", result.getRefundStatus())) {
return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV2B(result.getSuccessTime()),
result.getOutRefundNo(), response);
}
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
}
private PayRefundRespDTO parseRefundNotifyV3(String body, Map<String, String> headers) throws WxPayException {
// 1. 解析回调
SignatureHeader signatureHeader = getRequestHeader(headers);
WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, signatureHeader);
WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult();
// 2. 构建结果
if (Objects.equals("SUCCESS", result.getRefundStatus())) {
return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV3(result.getSuccessTime()),
result.getOutRefundNo(), response);
}
return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
}
@Override
protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException {
try {
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doGetRefundV2(outTradeNo, outRefundNo);
case API_VERSION_V3:
return doGetRefundV3(outTradeNo, outRefundNo);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
if (ObjectUtils.equalsAny(e.getErrCode(), "REFUNDNOTEXIST", "RESOURCE_NOT_EXISTS")) {
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayRefundRespDTO.failureOf(errorCode, errorMessage,
outRefundNo, e.getXmlString());
}
throw e;
}
}
private PayRefundRespDTO doGetRefundV2(String outTradeNo, String outRefundNo) throws WxPayException {
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundQueryRequest request = WxPayRefundQueryRequest.newBuilder()
.outTradeNo(outTradeNo)
.outRefundNo(outRefundNo)
.build();
// 2.1 执行请求
WxPayRefundQueryResult response = client.refundQuery(request);
// 2.2 创建返回结果
if (!Objects.equals("SUCCESS", response.getResultCode())) {
return PayRefundRespDTO.waitingOf(null,
outRefundNo, response);
}
WxPayRefundQueryResult.RefundRecord refund = CollUtil.findOne(response.getRefundRecords(),
record -> record.getOutRefundNo().equals(outRefundNo));
if (refund == null) {
return PayRefundRespDTO.failureOf(outRefundNo, response);
}
switch (refund.getRefundStatus()) {
case "SUCCESS":
return PayRefundRespDTO.successOf(refund.getRefundId(), parseDateV2B(refund.getRefundSuccessTime()),
outRefundNo, response);
case "PROCESSING":
return PayRefundRespDTO.waitingOf(refund.getRefundId(),
outRefundNo, response);
case "CHANGE": // 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者财付通转账的方式进行退款
case "FAIL":
return PayRefundRespDTO.failureOf(outRefundNo, response);
default:
throw new IllegalArgumentException(String.format("未知的退款状态(%s)", refund.getRefundStatus()));
}
}
private PayRefundRespDTO doGetRefundV3(String outTradeNo, String outRefundNo) throws WxPayException {
fixV3HttpClientConnectionPoolShutDown();
// 1. 构建 WxPayRefundRequest 请求
WxPayRefundQueryV3Request request = new WxPayRefundQueryV3Request();
request.setOutRefundNo(outRefundNo);
// 2.1 执行请求
WxPayRefundQueryV3Result response = client.refundQueryV3(request);
// 2.2 创建返回结果
switch (response.getStatus()) {
case "SUCCESS":
return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
outRefundNo, response);
case "PROCESSING":
return PayRefundRespDTO.waitingOf(response.getRefundId(),
outRefundNo, response);
case "ABNORMAL": // 退款异常
case "CLOSED":
return PayRefundRespDTO.failureOf(outRefundNo, response);
default:
throw new IllegalArgumentException(String.format("未知的退款状态(%s)", response.getStatus()));
}
}
@Override
protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws WxPayException {
fixV3HttpClientConnectionPoolShutDown();
// 1. 构建 TransferBillsRequest 请求
TransferBillsRequest request = TransferBillsRequest.newBuilder()
.appid(this.config.getAppId())
.outBillNo(reqDTO.getOutTransferNo())
.transferAmount(reqDTO.getPrice())
.transferRemark(reqDTO.getSubject())
.transferSceneId(reqDTO.getChannelExtras().get("sceneId"))
.openid(reqDTO.getUserAccount())
.userName(reqDTO.getUserName())
.transferSceneReportInfos(JsonUtils.parseArray(reqDTO.getChannelExtras().get("sceneReportInfos"),
TransferBillsRequest.TransferSceneReportInfo.class))
.notifyUrl(reqDTO.getNotifyUrl())
.build();
// 特殊:微信转账,必须 0.3 元起,才允许传入姓名
if (reqDTO.getPrice() < 30) {
request.setUserName(null);
}
// 2.1 执行请求
try {
TransferBillsResult response = client.getTransferService().transferBills(request);
// 2.2 创建返回结果
String state = response.getState();
if (ObjectUtils.equalsAny(state, "ACCEPTED", "PROCESSING", "WAIT_USER_CONFIRM", "TRANSFERING")) {
return PayTransferRespDTO.processingOf(response.getTransferBillNo(), response.getOutBillNo(), response)
.setChannelPackageInfo(response.getPackageInfo()); // 一般情况下,只有 WAIT_USER_CONFIRM 会有!
}
if (Objects.equals("SUCCESS", state)) {
return PayTransferRespDTO.successOf(response.getTransferBillNo(), parseDateV3(response.getCreateTime()),
response.getOutBillNo(), response);
}
return PayTransferRespDTO.closedOf(state, response.getFailReason(),
response.getOutBillNo(), response);
} catch (WxPayException e) {
log.error("[doUnifiedTransfer][转账({}) 发起微信支付异常", reqDTO, e);
String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayTransferRespDTO.closedOf(errorCode, errorMessage,
reqDTO.getOutTransferNo(), e.getXmlString());
}
}
@Override
protected PayTransferRespDTO doGetTransfer(String outTradeNo) throws WxPayException {
fixV3HttpClientConnectionPoolShutDown();
// 1. 执行请求
TransferBillsGetResult response = client.getTransferService().getBillsByOutBillNo(outTradeNo);
// 2. 创建返回结果
String state = response.getState();
if (ObjectUtils.equalsAny(state, "ACCEPTED", "PROCESSING", "WAIT_USER_CONFIRM", "TRANSFERING")) {
return PayTransferRespDTO.processingOf(response.getTransferBillNo(), response.getOutBillNo(), response);
}
if (Objects.equals("SUCCESS", state)) {
return PayTransferRespDTO.successOf(response.getTransferBillNo(), parseDateV3(response.getUpdateTime()),
response.getOutBillNo(), response);
}
return PayTransferRespDTO.closedOf(state, response.getFailReason(),
response.getOutBillNo(), response);
}
@Override
public PayTransferRespDTO doParseTransferNotify(Map<String, String> params, String body, Map<String, String> headers) throws WxPayException {
switch (config.getApiVersion()) {
case API_VERSION_V3:
return parseTransferNotifyV3(body, headers);
case API_VERSION_V2:
throw new UnsupportedOperationException("V2 版本暂不支持,建议使用 V3 版本");
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
}
private PayTransferRespDTO parseTransferNotifyV3(String body, Map<String, String> headers) throws WxPayException {
// 1. 解析回调
SignatureHeader signatureHeader = getRequestHeader(headers);
TransferBillsNotifyResult response = client.getTransferService().parseTransferBillsNotifyResult(body, signatureHeader);
TransferBillsNotifyResult.DecryptNotifyResult result = response.getResult();
// 2. 创建返回结果
String state = result.getState();
if (ObjectUtils.equalsAny(state, "ACCEPTED", "PROCESSING", "WAIT_USER_CONFIRM", "TRANSFERING")) {
return PayTransferRespDTO.processingOf(result.getTransferBillNo(), result.getOutBillNo(), response);
}
if (Objects.equals("SUCCESS", state)) {
return PayTransferRespDTO.successOf(result.getTransferBillNo(), parseDateV3(result.getUpdateTime()),
result.getOutBillNo(), response);
}
return PayTransferRespDTO.closedOf(state, result.getFailReason(),
result.getOutBillNo(), response);
}
// ========== 各种工具方法 ==========
/**
* 组装请求头重的签名信息
*
* @see <a href="https://github.com/binarywang/weixin-java-pay-demo/blob/master/src/main/java/com/github/binarywang/demo/wx/pay/controller/WxPayV3Controller.java#L202-L221">官方示例</a>
*/
private SignatureHeader getRequestHeader(Map<String, String> headers) {
return SignatureHeader.builder()
.signature(headers.get("wechatpay-signature"))
.nonce(headers.get("wechatpay-nonce"))
.serial(headers.get("wechatpay-serial"))
.timeStamp(headers.get("wechatpay-timestamp"))
.build();
}
// TODO @芋艿:可能是 wxjava 的 bughttps://github.com/binarywang/WxJava/issues/1557
private void fixV3HttpClientConnectionPoolShutDown() {
client.getConfig().setApiV3HttpClient(null);
}
static String formatDateV2(LocalDateTime time) {
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN);
}
static LocalDateTime parseDateV2(String time) {
return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN);
}
static LocalDateTime parseDateV2B(String time) {
return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN);
}
static String formatDateV3(LocalDateTime time) {
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN);
}
static LocalDateTime parseDateV3(String time) {
return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN);
}
static String getErrorCode(WxPayException e) {
if (StrUtil.isNotEmpty(e.getErrCode())) {
return e.getErrCode();
}
if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
return "CUSTOM_ERROR";
}
return e.getReturnCode();
}
static String getErrorMessage(WxPayException e) {
if (StrUtil.isNotEmpty(e.getErrCode())) {
return e.getErrCodeDes();
}
if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
return e.getCustomErrorMsg();
}
return e.getReturnMsg();
}
}

View File

@@ -0,0 +1,63 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import com.github.binarywang.wxpay.bean.order.WxPayAppOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/**
* 微信支付【App 支付】的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_5_3.shtml">App 支付</a>
*
* // TODO 芋艿:未详细测试,因为手头没 App
*
* @author 芋道源码
*/
@Slf4j
public class WxAppPayClient extends AbstractWxPayClient {
public WxAppPayClient(Long channelId, WxPayClientConfig config) {
super(channelId, PayChannelEnum.WX_APP.getCode(), config);
}
@Override
protected void doInit() {
super.doInit(WxPayConstants.TradeType.APP);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO);
// 执行请求
WxPayAppOrderResult response = client.createOrder(request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
reqDTO.getOutTradeNo(), response);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderV3Request 对象
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
// 执行请求
WxPayUnifiedOrderV3Result.AppResult response = client.createOrderV3(TradeTypeEnum.APP, request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
reqDTO.getOutTradeNo(), response);
}
}

View File

@@ -0,0 +1,107 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/**
* 微信支付【付款码支付】的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_10&index=1">付款码支付</a>
*
* @author 芋道源码
*/
@Slf4j
public class WxBarPayClient extends AbstractWxPayClient {
/**
* 微信付款码的过期时间
*/
private static final Duration AUTH_CODE_EXPIRE = Duration.ofMinutes(3);
public WxBarPayClient(Long channelId, WxPayClientConfig config) {
super(channelId, PayChannelEnum.WX_BAR.getCode(), config);
}
@Override
protected void doInit() {
super.doInit(WxPayConstants.TradeType.MICROPAY);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 由于付款码需要不断轮询,所以需要在较短的时间完成支付
LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE);
if (expireTime.isAfter(reqDTO.getExpireTime())) {
expireTime = reqDTO.getExpireTime();
}
// 构建 WxPayMicropayRequest 对象
WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
.outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject())
.detail(reqDTO.getBody())
.totalFee(reqDTO.getPrice()) // 单位分
.timeExpire(formatDateV2(expireTime))
.spbillCreateIp(reqDTO.getUserIp())
.authCode(getAuthCode(reqDTO))
.build();
// 执行请求,重试直到失败(过期),或者成功
WxPayException lastWxPayException = null;
for (int i = 1; i < Byte.MAX_VALUE; i++) {
try {
WxPayMicropayResult response = client.micropay(request);
// 支付成功例如说1用户输入了密码2用户免密支付
return PayOrderRespDTO.successOf(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
response.getOutTradeNo(), response)
.setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode());
} catch (WxPayException ex) {
lastWxPayException = ex;
// 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理
// 1. SYSTEMERROR接口返回错误请立即调用被扫订单结果查询API查询当前订单状态并根据订单的状态决定下一步的操作。
// 2. USERPAYING用户支付中需要输入密码等待 5 秒,然后调用被扫订单结果查询 API查询当前订单的不同状态决定下一步的操作。
// 3. BANKERROR银行系统异常请立即调用被扫订单结果查询 API查询当前订单的不同状态决定下一步的操作。
if (!StrUtil.equalsAny(ex.getErrCode(), "SYSTEMERROR", "USERPAYING", "BANKERROR")) {
throw ex;
}
// 等待 5 秒,继续下一轮重新发起支付
log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i,
toJsonString(request), ex.getMessage());
ThreadUtil.sleep(5, TimeUnit.SECONDS);
}
}
throw lastWxPayException;
}
@Override
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return doUnifiedOrderV2(reqDTO);
}
// ========== 各种工具方法 ==========
static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) {
String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "authCode");
if (StrUtil.isEmpty(authCode)) {
throw invalidParamException("支付请求的 authCode 不能为空!");
}
return authCode;
}
}

View File

@@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import lombok.extern.slf4j.Slf4j;
/**
* 微信支付【小程序】的 PayClient 实现类
*
* 由于公众号和小程序的微信支付逻辑一致,所以直接进行继承
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml">JSAPI 下单</>
*
* @author zwy
*/
@Slf4j
public class WxLitePayClient extends WxPubPayClient {
public WxLitePayClient(Long channelId, WxPayClientConfig config) {
super(channelId, PayChannelEnum.WX_LITE.getCode(), config);
}
}

View File

@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import lombok.extern.slf4j.Slf4j;
/**
* 微信支付【Native 二维码】的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml">Native 下单</a>
*
* @author zwy
*/
@Slf4j
public class WxNativePayClient extends AbstractWxPayClient {
public WxNativePayClient(Long channelId, WxPayClientConfig config) {
super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config);
}
@Override
protected void doInit() {
super.doInit(WxPayConstants.TradeType.NATIVE);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO)
.setProductId(reqDTO.getOutTradeNo()); // V2 必须传递 productId无需在微信配置。该参数在 V3 简化,无需传递!
// 执行请求
WxPayNativeOrderResult response = client.createOrder(request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(),
reqDTO.getOutTradeNo(), response);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderV3Request 对象
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
// 执行请求
String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response,
reqDTO.getOutTradeNo(), response);
}
}

View File

@@ -0,0 +1,107 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClientConfig;
import jakarta.validation.Validator;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 微信支付的 PayClientConfig 实现类
* 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性
*
* @author 芋道源码
*/
@Data
public class WxPayClientConfig implements PayClientConfig {
/**
* API 版本 - V2
*
* <a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1">V2 协议说明</a>
*/
public static final String API_VERSION_V2 = "v2";
/**
* API 版本 - V3
*
* <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml">V3 协议说明</a>
*/
public static final String API_VERSION_V3 = "v3";
/**
* 公众号或者小程序的 appid
*
* 只有公众号或小程序需要该字段
*/
@NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
private String appId;
/**
* 商户号
*/
@NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class})
private String mchId;
/**
* API 版本
*/
@NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class})
private String apiVersion;
// ========== V2 版本的参数 ==========
/**
* 商户密钥
*/
@NotBlank(message = "商户密钥不能为空", groups = V2.class)
private String mchKey;
/**
* apiclient_cert.p12 证书文件的对应字符串【base64 格式】
*
* 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储
*/
@NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class)
private String keyContent;
// ========== V3 版本的参数 ==========
/**
* apiclient_key.pem 证书文件的对应字符串
*/
@NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
private String privateKeyContent;
/**
* apiV3 密钥值
*/
@NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
private String apiV3Key;
/**
* 证书序列号merchantSerialNumber
*/
@NotBlank(message = "证书序列号不能为空", groups = V3.class)
private String certSerialNo;
/**
* pub_key.pem 证书文件的对应字符串
*/
@NotBlank(message = "pub_key.pem 不能为空", groups = V3.class)
private String publicKeyContent;
@NotBlank(message = "publicKeyId 不能为空", groups = V3.class)
private String publicKeyId;
/**
* 分组校验 v2版本
*/
public interface V2 {
}
/**
* 分组校验 v3版本
*/
public interface V3 {
}
@Override
public void validate(Validator validator) {
ValidationUtils.validate(validator, this,
API_VERSION_V2.equals(this.getApiVersion()) ? V2.class : V3.class);
}
}

View File

@@ -0,0 +1,81 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/**
* 微信支付(公众号)的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml">JSAPI 下单</>
*
* @author 芋道源码
*/
@Slf4j
public class WxPubPayClient extends AbstractWxPayClient {
@SuppressWarnings("unused") // 反射会调用到,所以不能删除
public WxPubPayClient(Long channelId, WxPayClientConfig config) {
super(channelId, PayChannelEnum.WX_PUB.getCode(), config);
}
protected WxPubPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
super(channelId, channelCode, config);
}
@Override
protected void doInit() {
super.doInit(WxPayConstants.TradeType.JSAPI);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO)
.setOpenid(getOpenid(reqDTO));
// 执行请求
WxPayMpOrderResult response = client.createOrder(request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
reqDTO.getOutTradeNo(), response);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO)
.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
// 执行请求
WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
reqDTO.getOutTradeNo(), response);
}
// ========== 各种工具方法 ==========
static String getOpenid(PayOrderUnifiedReqDTO reqDTO) {
String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid");
if (StrUtil.isEmpty(openid)) {
throw invalidParamException("支付请求的 openid 不能为空!");
}
return openid;
}
}

View File

@@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import com.github.binarywang.wxpay.bean.order.WxPayMwebOrderResult;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import lombok.extern.slf4j.Slf4j;
/**
* 微信支付H5 网页)的 PayClient 实现类
*
* 文档:<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_1.shtml">H5下单API</>
*
* @author YYQ
*/
@Slf4j
public class WxWapPayClient extends AbstractWxPayClient {
public WxWapPayClient(Long channelId, WxPayClientConfig config) {
super(channelId, PayChannelEnum.WX_WAP.getCode(), config);
}
protected WxWapPayClient(Long channelId, String channelCode, WxPayClientConfig config) {
super(channelId, channelCode, config);
}
@Override
protected void doInit() {
super.doInit(WxPayConstants.TradeType.MWEB);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO);
// 执行请求
WxPayMwebOrderResult response = client.createOrder(request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.URL.getMode(), response.getMwebUrl(),
reqDTO.getOutTradeNo(), response);
}
@Override
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO);
// 执行请求
String response = client.createOrderV3(TradeTypeEnum.H5, request);
// 转换结果
return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.URL.getMode(), response,
reqDTO.getOutTradeNo(), response);
}
}

View File

@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付 UI 展示模式
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum PayOrderDisplayModeEnum {
URL("url"), // Redirect 跳转链接的方式
IFRAME("iframe"), // IFrame 内嵌链接的方式【目前暂时用不到】
FORM("form"), // HTML 表单提交
QR_CODE("qr_code"), // 二维码的文字内容
QR_CODE_URL("qr_code_url"), // 二维码的图片链接
BAR_CODE("bar_code"), // 条形码
APP("app"), // 应用Android、iOS、微信小程序、微信公众号等需要做自定义处理的
;
/**
* 展示模式
*/
private final String mode;
}

View File

@@ -0,0 +1,6 @@
/**
* 支付客户端,接入国内多个支付渠道:
* 1. 支付宝,基于官方 SDK 接入
* 2. 微信支付,基于 weixin-java-pay 接入
*/
package cn.iocoder.yudao.module.pay.framework.pay;

View File

@@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.pay.service.channel;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;

View File

@@ -4,17 +4,18 @@ import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClientConfig;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO;
import cn.iocoder.yudao.module.pay.convert.channel.PayChannelConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.mysql.channel.PayChannelMapper;
import cn.iocoder.yudao.module.pay.framework.pay.core.WalletPayClient;
import jakarta.annotation.PostConstruct;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.NonePayClientConfig;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig;
import jakarta.annotation.Resource;
import jakarta.validation.Validator;
import lombok.extern.slf4j.Slf4j;
@@ -46,14 +47,6 @@ public class PayChannelServiceImpl implements PayChannelService {
@Resource
private Validator validator;
/**
* 初始化,为了注册钱包
*/
@PostConstruct
public void init() {
payClientFactory.registerPayClientClass(PayChannelEnum.WALLET, WalletPayClient.class);
}
@Override
public Long createChannel(PayChannelCreateReqVO reqVO) {
// 断言是否有重复的
@@ -89,7 +82,9 @@ public class PayChannelServiceImpl implements PayChannelService {
*/
private PayClientConfig parseConfig(String code, String configStr) {
// 解析配置
Class<? extends PayClientConfig> payClass = PayChannelEnum.getByCode(code).getConfigClass();
Class<? extends PayClientConfig> payClass = PayChannelEnum.isAlipay(configStr) ? AlipayPayClientConfig.class
: PayChannelEnum.isWeixin(configStr) ? WxPayClientConfig.class
: NonePayClientConfig.class;
if (ObjectUtil.isNull(payClass)) {
throw exception(CHANNEL_NOT_FOUND);
}

View File

@@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.pay.service.order;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO;

View File

@@ -7,10 +7,9 @@ import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.number.MoneyUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
@@ -218,13 +217,13 @@ public class PayOrderServiceImpl implements PayOrderService {
throw exception(PAY_ORDER_EXTENSION_IS_PAID);
}
// 情况二:调用三方接口,查询支付单状态,是不是已支付
PayClient payClient = channelService.getPayClient(orderExtension.getChannelId());
PayClient<?> payClient = channelService.getPayClient(orderExtension.getChannelId());
if (payClient == null) {
log.error("[validateOrderCanSubmit][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
return;
}
PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo());
if (respDTO != null && PayOrderStatusRespEnum.isSuccess(respDTO.getStatus())) {
if (respDTO != null && PayOrderStatusEnum.isSuccess(respDTO.getStatus())) {
log.warn("[validateOrderCanSubmit][order({}) 的 PayOrderRespDTO({}) 已支付,可能是回调延迟]",
id, toJsonString(respDTO));
throw exception(PAY_ORDER_EXTENSION_IS_PAID);
@@ -273,12 +272,12 @@ public class PayOrderServiceImpl implements PayOrderService {
// 注意,如果是方法内调用该方法,需要通过 getSelf().notifyPayOrder(channel, notify) 调用,否则事务不生效
public void notifyOrder(PayChannelDO channel, PayOrderRespDTO notify) {
// 情况一:支付成功的回调
if (PayOrderStatusRespEnum.isSuccess(notify.getStatus())) {
if (PayOrderStatusEnum.isSuccess(notify.getStatus())) {
notifyOrderSuccess(channel, notify);
return;
}
// 情况二:支付失败的回调
if (PayOrderStatusRespEnum.isClosed(notify.getStatus())) {
if (PayOrderStatusEnum.isClosed(notify.getStatus())) {
notifyOrderClosed(channel, notify);
}
// 情况三WAITING无需处理
@@ -373,6 +372,7 @@ public class PayOrderServiceImpl implements PayOrderService {
updateOrderExtensionClosed(channel, notify);
}
@SuppressWarnings("unused")
private void updateOrderExtensionClosed(PayChannelDO channel, PayOrderRespDTO notify) {
// 1. 查询 PayOrderExtensionDO
PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo());
@@ -488,7 +488,7 @@ public class PayOrderServiceImpl implements PayOrderService {
private boolean syncOrder(PayOrderExtensionDO orderExtension) {
try {
// 1.1 查询支付订单信息
PayClient payClient = channelService.getPayClient(orderExtension.getChannelId());
PayClient<?> payClient = channelService.getPayClient(orderExtension.getChannelId());
if (payClient == null) {
log.error("[syncOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
return false;
@@ -499,14 +499,14 @@ public class PayOrderServiceImpl implements PayOrderService {
// 当用户支付成功之后,该订单状态在渠道的回调中无法从已关闭改为已支付,造成重大影响。
// 考虑此定时任务是异常场景的兜底操作,因此这里不做变更,优先以回调为准。
// 让订单自动随着支付渠道那边一起等到过期,确保渠道先过期关闭支付入口,而后通过订单过期定时任务关闭自己的订单。
if (PayOrderStatusRespEnum.isClosed(respDTO.getStatus())) {
if (PayOrderStatusEnum.isClosed(respDTO.getStatus())) {
return false;
}
// 1.2 回调支付结果
notifyOrder(orderExtension.getChannelId(), respDTO);
// 2. 如果是已支付,则返回 true
return PayOrderStatusRespEnum.isSuccess(respDTO.getStatus());
return PayOrderStatusEnum.isSuccess(respDTO.getStatus());
} catch (Throwable e) {
log.error("[syncOrder][orderExtension({}) 同步支付状态异常]", orderExtension.getId(), e);
return false;
@@ -551,20 +551,20 @@ public class PayOrderServiceImpl implements PayOrderService {
return false;
}
// 情况二:调用三方接口,查询支付单状态,是不是已支付/已退款
PayClient payClient = channelService.getPayClient(orderExtension.getChannelId());
PayClient<?> payClient = channelService.getPayClient(orderExtension.getChannelId());
if (payClient == null) {
log.error("[expireOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId());
return false;
}
PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo());
if (PayOrderStatusRespEnum.isRefund(respDTO.getStatus())) {
if (PayOrderStatusEnum.isRefund(respDTO.getStatus())) {
// 补充说明:按道理,应该是 WAITING => SUCCESS => REFUND 状态,如果直接 WAITING => REFUND 状态,说明中间丢了过程
// 此时,需要人工介入,手工补齐数据,保持 WAITING => SUCCESS => REFUND 的过程
log.error("[expireOrder][extension({}) 的 PayOrderRespDTO({}) 已退款,可能是回调延迟]",
orderExtension.getId(), toJsonString(respDTO));
return false;
}
if (PayOrderStatusRespEnum.isSuccess(respDTO.getStatus())) {
if (PayOrderStatusEnum.isSuccess(respDTO.getStatus())) {
notifyOrder(orderExtension.getChannelId(), respDTO);
return false;
}

View File

@@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.pay.service.refund;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;

View File

@@ -3,10 +3,9 @@ package cn.iocoder.yudao.module.pay.service.refund;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
@@ -203,12 +202,12 @@ public class PayRefundServiceImpl implements PayRefundService {
@Transactional(rollbackFor = Exception.class)
public void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) {
// 情况一:退款成功
if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) {
if (PayRefundStatusEnum.isSuccess(notify.getStatus())) {
notifyRefundSuccess(channel, notify);
return;
}
// 情况二:退款失败
if (PayRefundStatusRespEnum.isFailure(notify.getStatus())) {
if (PayRefundStatusEnum.isFailure(notify.getStatus())) {
notifyRefundFailure(channel, notify);
}
}
@@ -302,7 +301,7 @@ public class PayRefundServiceImpl implements PayRefundService {
private boolean syncRefund(PayRefundDO refund) {
try {
// 1.1 查询退款订单信息
PayClient payClient = channelService.getPayClient(refund.getChannelId());
PayClient<?> payClient = channelService.getPayClient(refund.getChannelId());
if (payClient == null) {
log.error("[syncRefund][渠道编号({}) 找不到对应的支付客户端]", refund.getChannelId());
return false;

View File

@@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.pay.service.transfer;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateRespDTO;
import cn.iocoder.yudao.module.pay.controller.admin.transfer.vo.PayTransferPageReqVO;

View File

@@ -6,10 +6,9 @@ import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferStatusRespEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.transfer.dto.PayTransferCreateRespDTO;
@@ -142,15 +141,15 @@ public class PayTransferServiceImpl implements PayTransferService {
// 注意,如果是方法内调用该方法,需要通过 getSelf().notifyTransfer(channel, notify) 调用,否则事务不生效
public void notifyTransfer(PayChannelDO channel, PayTransferRespDTO notify) {
// 转账成功的回调
if (PayTransferStatusRespEnum.isSuccess(notify.getStatus())) {
if (PayTransferStatusEnum.isSuccess(notify.getStatus())) {
notifyTransferSuccess(channel, notify);
}
// 转账关闭的回调
if (PayTransferStatusRespEnum.isClosed(notify.getStatus())) {
if (PayTransferStatusEnum.isClosed(notify.getStatus())) {
notifyTransferClosed(channel, notify);
}
// 转账处理中的回调
if (PayTransferStatusRespEnum.isProcessing(notify.getStatus())) {
if (PayTransferStatusEnum.isProcessing(notify.getStatus())) {
notifyTransferProgressing(channel, notify);
}
// WAITING 状态无需处理
@@ -162,7 +161,7 @@ public class PayTransferServiceImpl implements PayTransferService {
if (transfer == null) {
throw exception(PAY_TRANSFER_NOT_FOUND);
}
if (PayTransferStatusEnum.isProgressing(transfer.getStatus())) { // 如果已经是转账中,直接返回,不用重复更新
if (PayTransferStatusEnum.isProcessing(transfer.getStatus())) { // 如果已经是转账中,直接返回,不用重复更新
log.info("[notifyTransferProgressing][transfer({}) 已经是转账中状态,无需更新]", transfer.getId());
return;
}

View File

@@ -6,7 +6,6 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
@@ -224,7 +223,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService {
.setRefundTotalPrice(walletRecharge.getTotalPrice()).setRefundPayPrice(walletRecharge.getPayPrice())
.setRefundBonusPrice(walletRecharge.getBonusPrice());
// 情况二:退款失败
} else if (PayRefundStatusRespEnum.isFailure(payRefund.getStatus())) {
} else if (PayRefundStatusEnum.isFailure(payRefund.getStatus())) {
// 2.2 解冻余额
payWalletService.unfreezePrice(walletRecharge.getWalletId(), walletRecharge.getTotalPrice());

View File

@@ -0,0 +1,134 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.RandomUtil;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.PayClientFactoryImpl;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayQrPayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayWapPayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.WxPubPayClient;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
/**
* {@link PayClientFactoryImpl} 的集成测试
*
* @author 芋道源码
*/
@Disabled
public class PayClientFactoryImplIntegrationTest {
private static final String SERVER_URL_SANDBOX = "https://openapi.alipaydev.com/gateway.do";
private final PayClientFactoryImpl payClientFactory = new PayClientFactoryImpl();
/**
* {@link WxPubPayClient} 的 V2 版本
*/
@Test
public void testCreatePayClient_WX_PUB_V2() {
// 创建配置
WxPayClientConfig config = new WxPayClientConfig();
config.setAppId("wx041349c6f39b268b");
config.setMchId("1545083881");
config.setApiVersion(WxPayClientConfig.API_VERSION_V2);
config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p");
// 创建客户端
Long channelId = RandomUtil.randomLong();
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config);
PayClient<?> client = payClientFactory.getPayClient(channelId);
// 发起支付
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
// CommonResult<?> result = client.unifiedOrder(reqDTO);
// System.out.println(result);
}
/**
* {@link WxPubPayClient} 的 V3 版本
*/
@Test
public void testCreatePayClient_WX_PUB_V3() throws FileNotFoundException {
// 创建配置
WxPayClientConfig config = new WxPayClientConfig();
config.setAppId("wx041349c6f39b268b");
config.setMchId("1545083881");
config.setApiVersion(WxPayClientConfig.API_VERSION_V3);
config.setPrivateKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_key.pem")));
// config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem")));
config.setApiV3Key("joerVi8y5DJ3o4ttA0o1uH47Xz1u2Ase");
// 创建客户端
Long channelId = RandomUtil.randomLong();
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config);
PayClient<?> client = payClientFactory.getPayClient(channelId);
// 发起支付
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
// CommonResult<?> result = client.unifiedOrder(reqDTO);
// System.out.println(result);
}
/**
* {@link AlipayQrPayClient}
*/
@Test
@SuppressWarnings("unchecked")
public void testCreatePayClient_ALIPAY_QR() {
// 创建配置
AlipayPayClientConfig config = new AlipayPayClientConfig();
config.setAppId("2021000118634035");
config.setServerUrl(SERVER_URL_SANDBOX);
config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8=");
config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB");
// 创建客户端
Long channelId = RandomUtil.randomLong();
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config);
PayClient<?> client = payClientFactory.getPayClient(channelId);
// 发起支付
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
reqDTO.setNotifyUrl("http://yunai.natapp1.cc/admin-api/pay/notify/callback/18"); // TODO @tina: 这里改成你的 natapp 回调地址
// CommonResult<AlipayTradePrecreateResponse> result = (CommonResult<AlipayTradePrecreateResponse>) client.unifiedOrder(reqDTO);
// System.out.println(JsonUtils.toJsonString(result));
// System.out.println(result.getData().getQrCode());
}
/**
* {@link AlipayWapPayClient}
*/
@Test
public void testCreatePayClient_ALIPAY_WAP() {
// 创建配置
AlipayPayClientConfig config = new AlipayPayClientConfig();
config.setAppId("2021000118634035");
config.setServerUrl(SERVER_URL_SANDBOX);
config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8=");
config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB");
// 创建客户端
Long channelId = RandomUtil.randomLong();
payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config);
PayClient<?> client = payClientFactory.getPayClient(channelId);
// 发起支付
PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO();
// CommonResult<?> result = client.unifiedOrder(reqDTO);
// System.out.println(JsonUtils.toJsonString(result));
}
private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() {
PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO();
reqDTO.setPrice(123);
reqDTO.setSubject("IPhone 13");
reqDTO.setBody("biubiubiu");
reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
reqDTO.setUserIp("127.0.0.1");
reqDTO.setNotifyUrl("http://127.0.0.1:8080");
return reqDTO;
}
}

View File

@@ -0,0 +1,221 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.RandomUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.exception.PayClientException;
import com.alipay.api.AlipayApiException;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.DefaultSigner;
import com.alipay.api.domain.AlipayTradeRefundModel;
import com.alipay.api.request.AlipayTradeRefundRequest;
import com.alipay.api.response.AlipayTradeRefundResponse;
import lombok.Setter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import jakarta.validation.ConstraintViolationException;
import java.util.Date;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_PUBLIC_KEY;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.when;
/**
* 支付宝 Client 的测试基类
*
* @author jason
*/
public abstract class AbstractAlipayClientTest extends BaseMockitoUnitTest {
protected AlipayPayClientConfig config = randomPojo(AlipayPayClientConfig.class, o -> {
o.setServerUrl(randomURL());
o.setPrivateKey(randomString());
o.setMode(MODE_PUBLIC_KEY);
o.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
o.setAppCertContent("");
o.setAlipayPublicCertContent("");
o.setRootCertContent("");
});
@Mock
protected DefaultAlipayClient defaultAlipayClient;
@Setter
private AbstractAlipayPayClient client;
/**
* 子类需要实现该方法. 设置 client 的具体实现
*/
@BeforeEach
public abstract void setUp();
@Test
@DisplayName("支付宝 Client 初始化")
public void testDoInit() {
// 调用
client.doInit();
// 断言
DefaultAlipayClient realClient = client.getClient();
assertNotSame(defaultAlipayClient, realClient);
assertInstanceOf(DefaultSigner.class, realClient.getSigner());
assertEquals(config.getPrivateKey(), ((DefaultSigner) realClient.getSigner()).getPrivateKey());
}
@Test
@DisplayName("支付宝 Client 统一退款:成功")
public void testUnifiedRefund_success() throws AlipayApiException {
// mock 方法
String notifyUrl = randomURL();
Date refundTime = randomDate();
String outRefundNo = randomString();
String outTradeNo = randomString();
Integer refundAmount = randomInteger();
AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> {
o.setSubCode("");
o.setGmtRefundPay(refundTime);
});
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> {
assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel());
AlipayTradeRefundModel bizModel = (AlipayTradeRefundModel) request.getBizModel();
assertEquals(outRefundNo, bizModel.getOutRequestNo());
assertEquals(outTradeNo, bizModel.getOutTradeNo());
assertEquals(String.valueOf(refundAmount / 100.0), bizModel.getRefundAmount());
return true;
}))).thenReturn(response);
// 准备请求参数
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> {
o.setOutRefundNo(outRefundNo);
o.setOutTradeNo(outTradeNo);
o.setNotifyUrl(notifyUrl);
o.setRefundPrice(refundAmount);
});
// 调用
PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO);
// 断言
assertEquals(PayRefundStatusEnum.SUCCESS.getStatus(), resp.getStatus());
assertEquals(outRefundNo, resp.getOutRefundNo());
assertNull(resp.getChannelRefundNo());
assertEquals(LocalDateTimeUtil.of(refundTime), resp.getSuccessTime());
assertSame(response, resp.getRawData());
assertNull(resp.getChannelErrorCode());
assertNull(resp.getChannelErrorMsg());
}
@Test
@DisplayName("支付宝 Client 统一退款:渠道返回失败")
public void test_unified_refund_channel_failed() throws AlipayApiException {
// mock 方法
String notifyUrl = randomURL();
String subCode = randomString();
String subMsg = randomString();
AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> {
o.setSubCode(subCode);
o.setSubMsg(subMsg);
});
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> {
assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel());
return true;
}))).thenReturn(response);
// 准备请求参数
String outRefundNo = randomString();
String outTradeNo = randomString();
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> {
o.setOutRefundNo(outRefundNo);
o.setOutTradeNo(outTradeNo);
o.setNotifyUrl(notifyUrl);
});
// 调用
PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO);
// 断言
assertEquals(PayRefundStatusEnum.FAILURE.getStatus(), resp.getStatus());
assertEquals(outRefundNo, resp.getOutRefundNo());
assertNull(resp.getChannelRefundNo());
assertNull(resp.getSuccessTime());
assertSame(response, resp.getRawData());
assertEquals(subCode, resp.getChannelErrorCode());
assertEquals(subMsg, resp.getChannelErrorMsg());
}
@Test
@DisplayName("支付宝 Client 统一退款:参数校验不通过")
public void testUnifiedRefund_paramInvalidate() {
// 准备请求参数
String notifyUrl = randomURL();
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> {
o.setOutTradeNo("");
o.setNotifyUrl(notifyUrl);
});
// 调用,并断言
assertThrows(ConstraintViolationException.class, () -> client.unifiedRefund(refundReqDTO));
}
@Test
@DisplayName("支付宝 Client 统一退款:抛出业务异常")
public void testUnifiedRefund_throwServiceException() throws AlipayApiException {
// mock 方法
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> true)))
.thenThrow(ServiceExceptionUtil.exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR));
// 准备请求参数
String notifyUrl = randomURL();
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> o.setNotifyUrl(notifyUrl));
// 调用,并断言
assertThrows(ServiceException.class, () -> client.unifiedRefund(refundReqDTO));
}
@Test
@DisplayName("支付宝 Client 统一退款:抛出系统异常")
public void testUnifiedRefund_throwPayException() throws AlipayApiException {
// mock 方法
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> true)))
.thenThrow(new RuntimeException("系统异常"));
// 准备请求参数
String notifyUrl = randomURL();
PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> o.setNotifyUrl(notifyUrl));
// 调用,并断言
assertThrows(PayClientException.class, () -> client.unifiedRefund(refundReqDTO));
}
@Test
@DisplayName("支付宝 Client 统一下单:参数校验不通过")
public void testUnifiedOrder_paramInvalidate() {
// 准备请求参数
String outTradeNo = randomString();
String notifyUrl = randomURL();
PayOrderUnifiedReqDTO reqDTO = randomPojo(PayOrderUnifiedReqDTO.class, o -> {
o.setOutTradeNo(outTradeNo);
o.setNotifyUrl(notifyUrl);
});
// 调用,并断言
assertThrows(ConstraintViolationException.class, () -> client.unifiedOrder(reqDTO));
}
protected PayOrderUnifiedReqDTO buildOrderUnifiedReqDTO(String notifyUrl, String outTradeNo, Integer price) {
return randomPojo(PayOrderUnifiedReqDTO.class, o -> {
o.setOutTradeNo(outTradeNo);
o.setNotifyUrl(notifyUrl);
o.setPrice(price);
o.setSubject(RandomUtil.randomString(32));
o.setBody(RandomUtil.randomString(32));
});
}
}

View File

@@ -0,0 +1,170 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradePayModel;
import com.alipay.api.request.AlipayTradePayRequest;
import com.alipay.api.response.AlipayTradePayResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.InjectMocks;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import static cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum.CLOSED;
import static cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum.WAITING;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.when;
/**
* {@link AlipayBarPayClient} 单元测试
*
* @author jason
*/
public class AlipayBarPayClientTest extends AbstractAlipayClientTest {
@InjectMocks
private AlipayBarPayClient client = new AlipayBarPayClient(randomLongId(), config);
@Override
@BeforeEach
public void setUp() {
setClient(client);
}
@Test
@DisplayName("支付宝条码支付:非免密码支付下单成功")
public void testUnifiedOrder_success() throws AlipayApiException {
// mock 方法
String outTradeNo = randomString();
String notifyUrl = randomURL();
Integer price = randomInteger();
String authCode = randomString();
AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> o.setSubCode(""));
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePayRequest>) request -> {
assertInstanceOf(AlipayTradePayModel.class, request.getBizModel());
assertEquals(notifyUrl, request.getNotifyUrl());
AlipayTradePayModel model = (AlipayTradePayModel) request.getBizModel();
assertEquals(outTradeNo, model.getOutTradeNo());
assertEquals(String.valueOf(price / 100.0), model.getTotalAmount());
assertEquals(authCode, model.getAuthCode());
return true;
}))).thenReturn(response);
// 准备请求参数
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price);
Map<String, String> extraParam = new HashMap<>();
extraParam.put("auth_code", authCode);
reqDTO.setChannelExtras(extraParam);
// 调用方法
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
// 断言
assertEquals(WAITING.getStatus(), resp.getStatus());
assertEquals(outTradeNo, resp.getOutTradeNo());
assertNull(resp.getChannelOrderNo());
assertNull(resp.getChannelUserId());
assertNull(resp.getSuccessTime());
assertEquals(PayOrderDisplayModeEnum.BAR_CODE.getMode(), resp.getDisplayMode());
assertEquals("", resp.getDisplayContent());
assertSame(response, resp.getRawData());
assertNull(resp.getChannelErrorCode());
assertNull(resp.getChannelErrorMsg());
}
@Test
@DisplayName("支付宝条码支付:免密码支付下单成功")
public void testUnifiedOrder_code10000Success() throws AlipayApiException {
// mock 方法
String outTradeNo = randomString();
String channelNo = randomString();
String channelUserId = randomString();
Date payTime = randomDate();
AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> {
o.setSubCode("");
o.setCode("10000");
o.setOutTradeNo(outTradeNo);
o.setTradeNo(channelNo);
o.setBuyerUserId(channelUserId);
o.setGmtPayment(payTime);
});
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePayRequest>) request -> true)))
.thenReturn(response);
// 准备请求参数
String authCode = randomString();
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger());
Map<String, String> extraParam = new HashMap<>();
extraParam.put("auth_code", authCode);
reqDTO.setChannelExtras(extraParam);
// 下单请求
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
// 断言
assertEquals(PayOrderStatusEnum.SUCCESS.getStatus(), resp.getStatus());
assertEquals(outTradeNo, resp.getOutTradeNo());
assertEquals(channelNo, resp.getChannelOrderNo());
assertEquals(channelUserId, resp.getChannelUserId());
assertEquals(LocalDateTimeUtil.of(payTime), resp.getSuccessTime());
assertEquals(PayOrderDisplayModeEnum.BAR_CODE.getMode(), resp.getDisplayMode());
assertEquals("", resp.getDisplayContent());
assertSame(response, resp.getRawData());
assertNull(resp.getChannelErrorCode());
assertNull(resp.getChannelErrorMsg());
}
@Test
@DisplayName("支付宝条码支付:没有传条码")
public void testUnifiedOrder_emptyAuthCode() {
// 准备参数
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), randomString(), randomInteger());
// 调用,并断言
assertThrows(ServiceException.class, () -> client.unifiedOrder(reqDTO));
}
@Test
@DisplayName("支付宝条码支付:渠道返回失败")
public void test_unified_order_channel_failed() throws AlipayApiException {
// mock 方法
String subCode = randomString();
String subMsg = randomString();
AlipayTradePayResponse response = randomPojo(AlipayTradePayResponse.class, o -> {
o.setSubCode(subCode);
o.setSubMsg(subMsg);
});
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePayRequest>) request -> true)))
.thenReturn(response);
// 准备请求参数
String authCode = randomString();
String outTradeNo = randomString();
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger());
Map<String, String> extraParam = new HashMap<>();
extraParam.put("auth_code", authCode);
reqDTO.setChannelExtras(extraParam);
// 调用方法
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
// 断言
assertEquals(CLOSED.getStatus(), resp.getStatus());
assertEquals(outTradeNo, resp.getOutTradeNo());
assertNull(resp.getChannelOrderNo());
assertNull(resp.getChannelUserId());
assertNull(resp.getSuccessTime());
assertNull(resp.getDisplayMode());
assertNull(resp.getDisplayContent());
assertSame(response, resp.getRawData());
assertEquals(subCode, resp.getChannelErrorCode());
assertEquals(subMsg, resp.getChannelErrorMsg());
}
}

View File

@@ -0,0 +1,131 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay;
import cn.hutool.http.Method;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.InjectMocks;
import static cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum.CLOSED;
import static cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum.WAITING;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
/**
* {@link AlipayPcPayClient} 单元测试
*
* @author jason
*/
public class AlipayPcPayClientTest extends AbstractAlipayClientTest {
@InjectMocks
private AlipayPcPayClient client = new AlipayPcPayClient(randomLongId(), config);
@Override
@BeforeEach
public void setUp() {
setClient(client);
}
@Test
@DisplayName("支付宝 PC 网站支付URL Display Mode 下单成功")
public void testUnifiedOrder_urlSuccess() throws AlipayApiException {
// mock 方法
String notifyUrl = randomURL();
AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> o.setSubCode(""));
when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher<AlipayTradePagePayRequest>) request -> true),
eq(Method.GET.name()))).thenReturn(response);
// 准备请求参数
String outTradeNo = randomString();
Integer price = randomInteger();
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price);
reqDTO.setDisplayMode(null);
// 调用
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
// 断言
assertEquals(WAITING.getStatus(), resp.getStatus());
assertEquals(outTradeNo, resp.getOutTradeNo());
assertNull(resp.getChannelOrderNo());
assertNull(resp.getChannelUserId());
assertNull(resp.getSuccessTime());
assertEquals(PayOrderDisplayModeEnum.URL.getMode(), resp.getDisplayMode());
assertEquals(response.getBody(), resp.getDisplayContent());
assertSame(response, resp.getRawData());
assertNull(resp.getChannelErrorCode());
assertNull(resp.getChannelErrorMsg());
}
@Test
@DisplayName("支付宝 PC 网站支付Form Display Mode 下单成功")
public void testUnifiedOrder_formSuccess() throws AlipayApiException {
// mock 方法
String notifyUrl = randomURL();
AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> o.setSubCode(""));
when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher<AlipayTradePagePayRequest>) request -> true),
eq(Method.POST.name()))).thenReturn(response);
// 准备请求参数
String outTradeNo = randomString();
Integer price = randomInteger();
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price);
reqDTO.setDisplayMode(PayOrderDisplayModeEnum.FORM.getMode());
// 调用
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
// 断言
assertEquals(WAITING.getStatus(), resp.getStatus());
assertEquals(outTradeNo, resp.getOutTradeNo());
assertNull(resp.getChannelOrderNo());
assertNull(resp.getChannelUserId());
assertNull(resp.getSuccessTime());
assertEquals(PayOrderDisplayModeEnum.FORM.getMode(), resp.getDisplayMode());
assertEquals(response.getBody(), resp.getDisplayContent());
assertSame(response, resp.getRawData());
assertNull(resp.getChannelErrorCode());
assertNull(resp.getChannelErrorMsg());
}
@Test
@DisplayName("支付宝 PC 网站支付:渠道返回失败")
public void testUnifiedOrder_channelFailed() throws AlipayApiException {
// mock 方法
String subCode = randomString();
String subMsg = randomString();
AlipayTradePagePayResponse response = randomPojo(AlipayTradePagePayResponse.class, o -> {
o.setSubCode(subCode);
o.setSubMsg(subMsg);
});
when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher<AlipayTradePagePayRequest>) request -> true),
eq(Method.GET.name()))).thenReturn(response);
// 准备请求参数
String outTradeNo = randomString();
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger());
reqDTO.setDisplayMode(PayOrderDisplayModeEnum.URL.getMode());
// 调用
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
// 断言
assertEquals(CLOSED.getStatus(), resp.getStatus());
assertEquals(outTradeNo, resp.getOutTradeNo());
assertNull(resp.getChannelOrderNo());
assertNull(resp.getChannelUserId());
assertNull(resp.getSuccessTime());
assertNull(resp.getDisplayMode());
assertNull(resp.getDisplayContent());
assertSame(response, resp.getRawData());
assertEquals(subCode, resp.getChannelErrorCode());
assertEquals(subMsg, resp.getChannelErrorMsg());
}
}

View File

@@ -0,0 +1,147 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.exception.PayClientException;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.request.AlipayTradePrecreateRequest;
import com.alipay.api.response.AlipayTradePrecreateResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.InjectMocks;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum.CLOSED;
import static cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum.WAITING;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.when;
/**
* {@link AlipayQrPayClient} 单元测试
*
* @author jason
*/
public class AlipayQrPayClientTest extends AbstractAlipayClientTest {
@InjectMocks
private AlipayQrPayClient client = new AlipayQrPayClient(randomLongId(), config);
@BeforeEach
public void setUp() {
setClient(client);
}
@Test
@DisplayName("支付宝扫描支付:下单成功")
public void testUnifiedOrder_success() throws AlipayApiException {
// mock 方法
String notifyUrl = randomURL();
String qrCode = randomString();
Integer price = randomInteger();
AlipayTradePrecreateResponse response = randomPojo(AlipayTradePrecreateResponse.class, o -> {
o.setQrCode(qrCode);
o.setSubCode("");
});
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePrecreateRequest>) request -> {
assertEquals(notifyUrl, request.getNotifyUrl());
return true;
}))).thenReturn(response);
// 准备请求参数
String outTradeNo = randomString();
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price);
// 调用
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
// 断言
assertEquals(WAITING.getStatus(), resp.getStatus());
assertEquals(outTradeNo, resp.getOutTradeNo());
assertNull(resp.getChannelOrderNo());
assertNull(resp.getChannelUserId());
assertNull(resp.getSuccessTime());
assertEquals(PayOrderDisplayModeEnum.QR_CODE.getMode(), resp.getDisplayMode());
assertEquals(response.getQrCode(), resp.getDisplayContent());
assertSame(response, resp.getRawData());
assertNull(resp.getChannelErrorCode());
assertNull(resp.getChannelErrorMsg());
}
@Test
@DisplayName("支付宝扫描支付:渠道返回失败")
public void testUnifiedOrder_channelFailed() throws AlipayApiException {
// mock 方法
String notifyUrl = randomURL();
String subCode = randomString();
String subMsg = randomString();
Integer price = randomInteger();
AlipayTradePrecreateResponse response = randomPojo(AlipayTradePrecreateResponse.class, o -> {
o.setSubCode(subCode);
o.setSubMsg(subMsg);
});
// mock
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePrecreateRequest>) request -> {
assertEquals(notifyUrl, request.getNotifyUrl());
return true;
}))).thenReturn(response);
// 准备请求参数
String outTradeNo = randomString();
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price);
// 调用
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
// 断言
assertEquals(CLOSED.getStatus(), resp.getStatus());
assertEquals(outTradeNo, resp.getOutTradeNo());
assertNull(resp.getChannelOrderNo());
assertNull(resp.getChannelUserId());
assertNull(resp.getSuccessTime());
assertNull(resp.getDisplayMode());
assertNull(resp.getDisplayContent());
assertSame(response, resp.getRawData());
assertEquals(subCode, resp.getChannelErrorCode());
assertEquals(subMsg, resp.getChannelErrorMsg());
}
@Test
@DisplayName("支付宝扫描支付, 抛出系统异常")
public void testUnifiedOrder_throwPayException() throws AlipayApiException {
// mock 方法
String outTradeNo = randomString();
String notifyUrl = randomURL();
Integer price = randomInteger();
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePrecreateRequest>) request -> {
assertEquals(notifyUrl, request.getNotifyUrl());
return true;
}))).thenThrow(new RuntimeException("系统异常"));
// 准备请求参数
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo,price);
// 调用,并断言
assertThrows(PayClientException.class, () -> client.unifiedOrder(reqDTO));
}
@Test
@DisplayName("支付宝 Client 统一下单:抛出业务异常")
public void testUnifiedOrder_throwServiceException() throws AlipayApiException {
// mock 方法
String outTradeNo = randomString();
String notifyUrl = randomURL();
Integer price = randomInteger();
when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradePrecreateRequest>) request -> {
assertEquals(notifyUrl, request.getNotifyUrl());
return true;
}))).thenThrow(ServiceExceptionUtil.exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR));
// 准备请求参数
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price);
// 调用,并断言
assertThrows(ServiceException.class, () -> client.unifiedOrder(reqDTO));
}
}

View File

@@ -0,0 +1,111 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay;
import cn.hutool.http.Method;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradeWapPayModel;
import com.alipay.api.request.AlipayTradeWapPayRequest;
import com.alipay.api.response.AlipayTradeWapPayResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.InjectMocks;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum.CLOSED;
import static cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum.WAITING;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
/**
* {@link AlipayWapPayClient} 单元测试
*
* @author jason
*/
public class AlipayWapPayClientTest extends AbstractAlipayClientTest {
/**
* 支付宝 H5 支付 Client
*/
@InjectMocks
private AlipayWapPayClient client = new AlipayWapPayClient(randomLongId(), config);
@BeforeEach
public void setUp() {
setClient(client);
}
@Test
@DisplayName("支付宝 H5 支付:下单成功")
public void testUnifiedOrder_success() throws AlipayApiException {
// mock 方法
String h5Body = randomString();
Integer price = randomInteger();
AlipayTradeWapPayResponse response = randomPojo(AlipayTradeWapPayResponse.class, o -> {
o.setSubCode("");
o.setBody(h5Body);
});
String notifyUrl = randomURL();
when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher<AlipayTradeWapPayRequest>) request -> {
assertInstanceOf(AlipayTradeWapPayModel.class, request.getBizModel());
AlipayTradeWapPayModel bizModel = (AlipayTradeWapPayModel) request.getBizModel();
assertEquals(String.valueOf(price / 100.0), bizModel.getTotalAmount());
assertEquals(notifyUrl, request.getNotifyUrl());
return true;
}), eq(Method.GET.name()))).thenReturn(response);
// 准备请求参数
String outTradeNo = randomString();
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(notifyUrl, outTradeNo, price);
// 调用
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
// 断言
assertEquals(WAITING.getStatus(), resp.getStatus());
assertEquals(outTradeNo, resp.getOutTradeNo());
assertNull(resp.getChannelOrderNo());
assertNull(resp.getChannelUserId());
assertNull(resp.getSuccessTime());
assertEquals(PayOrderDisplayModeEnum.URL.getMode(), resp.getDisplayMode());
assertEquals(response.getBody(), resp.getDisplayContent());
assertSame(response, resp.getRawData());
assertNull(resp.getChannelErrorCode());
assertNull(resp.getChannelErrorMsg());
}
@Test
@DisplayName("支付宝 H5 支付:渠道返回失败")
public void test_unified_order_channel_failed() throws AlipayApiException {
// mock 方法
String subCode = randomString();
String subMsg = randomString();
AlipayTradeWapPayResponse response = randomPojo(AlipayTradeWapPayResponse.class, o -> {
o.setSubCode(subCode);
o.setSubMsg(subMsg);
});
when(defaultAlipayClient.pageExecute(argThat((ArgumentMatcher<AlipayTradeWapPayRequest>) request -> true),
eq(Method.GET.name()))).thenReturn(response);
String outTradeNo = randomString();
PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), outTradeNo, randomInteger());
// 调用
PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
// 断言
assertEquals(CLOSED.getStatus(), resp.getStatus());
assertEquals(outTradeNo, resp.getOutTradeNo());
assertNull(resp.getChannelOrderNo());
assertNull(resp.getChannelUserId());
assertNull(resp.getSuccessTime());
assertNull(resp.getDisplayMode());
assertNull(resp.getDisplayContent());
assertSame(response, resp.getRawData());
assertEquals(subCode, resp.getChannelErrorCode());
assertEquals(subMsg, resp.getChannelErrorMsg());
}
}

View File

@@ -0,0 +1,123 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult;
import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV2;
/**
* {@link WxBarPayClient} 的集成测试,用于快速调试微信条码支付
*
* @author 芋道源码
*/
@Disabled
public class WxBarPayClientIntegrationTest {
@Test
public void testPayV2() throws WxPayException {
// 创建 config 配置
WxPayConfig config = buildWxPayConfigV2();
// 创建 WxPayService 客户端
WxPayService client = new WxPayServiceImpl();
client.setConfig(config);
// 执行发起支付
WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
.outTradeNo(String.valueOf(System.currentTimeMillis()))
.body("测试支付-body")
.detail("测试支付-detail")
.totalFee(1) // 单位分
.timeExpire(formatDateV2(LocalDateTimeUtils.addTime(Duration.ofMinutes(2))))
.spbillCreateIp("127.0.0.1")
.authCode("134298744426278497")
.build();
System.out.println("========= request ==========");
System.out.println(JsonUtils.toJsonPrettyString(request));
WxPayMicropayResult response = client.micropay(request);
System.out.println("========= response ==========");
System.out.println(JsonUtils.toJsonPrettyString(response));
}
@Test
public void testParseRefundNotifyV2() throws WxPayException {
// 创建 config 配置
WxPayConfig config = buildWxPayConfigV2();
// 创建 WxPayService 客户端
WxPayService client = new WxPayServiceImpl();
client.setConfig(config);
// 执行解析
String xml = "<xml><return_code>SUCCESS</return_code><appid><![CDATA[wx62056c0d5e8db250]]></appid><mch_id><![CDATA[1545083881]]></mch_id><nonce_str><![CDATA[ed8f02c21d15635cede114a42d0525a0]]></nonce_str><req_info><![CDATA[bGp+wB9DAHjoOO9Nw1iSmmIFdN2zZDhsoRWZBYdf/8bcpjowr4T8i2qjLsbMtvKQeVC5kBZOL/Agal3be6UPwnoantil+L+ojZgvLch7dXFKs/AcoxIYcVYyGka+wmnRJfUmuFRBgzt++8HOFsmJz6e2brYv1EAz+93fP2AsJtRuw1FEzodcg8eXm52hbE0KhLNqC2OyNVkn8AbOOrwIxSYobg2jVbuJ4JllYbEGIQ/6kWzNbVmMKhGJGYBy/NbUGKoQsoe4QeTQqcqQqVp08muxaOfJGThaN3B9EEMFSrog/3yT7ykVV6WQ5+Ygt89LplOf5ucWa4Ird7VJhHWtzI92ZePj4Omy1XkT1TRlwtDegA0S5MeQpM4WZ1taMrhxgmNkTUJ0JXFncx5e2KLQvbvD/HOcccx48Xv1c16JBz6G3501k8E++LWXgZ2TeNXwGsk6FyRZb0ApLyQHIx5ZtPo/UET9z3AmJCPXkrUsZ4WK46fDtbzxVPU2r8nTOcGCPbO0LUsGT6wpsuQVC4CisXDJwoZmL6kKwHfKs6mmUL2YZYzNfgoB/KgpJYSpC96kcpQyFvw+xuwqK2SXGZbAl9lADT+a83z04feQHSSIG3PCrX4QEWzpCZZ4+ySEz1Y34aoU20X9GtX+1LSwUjmQgwHrMBSvFm3/B7+IFM8OUqDB+Uvkr9Uvy7P2/KDvfy3Ih7GFcGd0C5NXpSvVTTfu1IlK/T3/t6MR/8iq78pp/2ZTYvO6eNDRJWaXYU+x6sl2dTs9n+2Z4W4AfYTvEyuxlx+aI19SqCJh7WmaFcAxidFl/9iqDjWiplb9+C6ijZv2hJtVjSCuoptIWpGDYItH7RAqlKHrx6flJD+M/5BceMHBv2w4OWCD9vPRLo8gl9o06ip0iflzO1dixhOAgLFjsQmQHNGFtR3EvCID+iS4FUlilwK+hcKNxrr0wp9Btkl9W1R9aTo289CUiIxx45skfCYzHwb+7Hqj3uTiXnep6zhCKZBAnPsDOvISXfBgXKufcFsTNtts09jX8H5/uMc9wyJ179H1cp+At1mIK2duwfo4Q9asfEoffl6Zn1olGdtEruxHGeVU0NwJ8V7RflC/Cx5RXtJ3sPJ/sHmVnBlVyR0=]]></req_info></xml>";
WxPayRefundNotifyResult response = client.parseRefundNotifyResult(xml);
System.out.println(response.getReqInfo());
}
@Test
public void testRefundV2() throws WxPayException {
// 创建 config 配置
WxPayConfig config = buildWxPayConfigV2();
// 创建 WxPayService 客户端
WxPayService client = new WxPayServiceImpl();
client.setConfig(config);
// 执行发起退款
WxPayRefundRequest request = new WxPayRefundRequest()
.setOutTradeNo("1689545667276")
.setOutRefundNo(String.valueOf(System.currentTimeMillis()))
.setRefundFee(1)
.setRefundDesc("就是想退了")
.setTotalFee(1);
System.out.println("========= request ==========");
System.out.println(JsonUtils.toJsonPrettyString(request));
WxPayRefundResult response = client.refund(request);
System.out.println("========= response ==========");
System.out.println(JsonUtils.toJsonPrettyString(response));
}
@Test
public void testRefundV3() throws WxPayException {
// 创建 config 配置
WxPayConfig config = buildWxPayConfigV2();
// 创建 WxPayService 客户端
WxPayService client = new WxPayServiceImpl();
client.setConfig(config);
// 执行发起退款
WxPayRefundV3Request request = new WxPayRefundV3Request()
.setOutTradeNo("1689506325635")
.setOutRefundNo(String.valueOf(System.currentTimeMillis()))
.setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY"))
.setReason("就是想退了");
System.out.println("========= request ==========");
System.out.println(JsonUtils.toJsonPrettyString(request));
WxPayRefundV3Result response = client.refundV3(request);
System.out.println("========= response ==========");
System.out.println(JsonUtils.toJsonPrettyString(response));
}
private WxPayConfig buildWxPayConfigV2() {
WxPayConfig config = new WxPayConfig();
config.setAppId("wx62056c0d5e8db250");
config.setMchId("1545083881");
config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim");
// config.setSignType(WxPayConstants.SignType.MD5);
config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12");
return config;
}
}

View File

@@ -0,0 +1,84 @@
package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV3;
/**
* {@link WxNativePayClient} 的集成测试,用于快速调试微信扫码支付
*
* @author 芋道源码
*/
@Disabled
public class WxNativePayClientIntegrationTest {
@Test
public void testPayV3() throws WxPayException {
// 创建 config 配置
WxPayConfig config = buildWxPayConfigV3();
// 创建 WxPayService 客户端
WxPayService client = new WxPayServiceImpl();
client.setConfig(config);
// 执行发起支付
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request()
.setOutTradeNo(String.valueOf(System.currentTimeMillis()))
.setDescription("测试支付-body")
.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(1)) // 单位分
.setTimeExpire(formatDateV3(LocalDateTimeUtils.addTime(Duration.ofMinutes(2))))
.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp("127.0.0.1"))
.setNotifyUrl("http://127.0.0.1:48080");
System.out.println("========= request ==========");
System.out.println(JsonUtils.toJsonPrettyString(request));
String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
System.out.println("========= response ==========");
System.out.println(JsonUtils.toJsonPrettyString(response));
}
@Test
public void testRefundV3() throws WxPayException {
// 创建 config 配置
WxPayConfig config = buildWxPayConfigV3();
// 创建 WxPayService 客户端
WxPayService client = new WxPayServiceImpl();
client.setConfig(config);
// 执行发起退款
WxPayRefundV3Request request = new WxPayRefundV3Request()
.setOutTradeNo("1689545729695")
.setOutRefundNo(String.valueOf(System.currentTimeMillis()))
.setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY"))
.setReason("就是想退了");
System.out.println("========= request ==========");
System.out.println(JsonUtils.toJsonPrettyString(request));
WxPayRefundV3Result response = client.refundV3(request);
System.out.println("========= response ==========");
System.out.println(JsonUtils.toJsonPrettyString(response));
}
private WxPayConfig buildWxPayConfigV3() {
WxPayConfig config = new WxPayConfig();
config.setAppId("wx62056c0d5e8db250");
config.setMchId("1545083881");
config.setApiV3Key("459arNsYHl1mgkiO6H9ZH5KkhFXSxaA4");
// config.setCertSerialNo(serialNo);
config.setPrivateCertPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem");
config.setPrivateKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_key.pem");
return config;
}
}

View File

@@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.pay.service.channel;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
import cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.weixin.WxPayClientConfig;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO;

View File

@@ -2,11 +2,10 @@ package cn.iocoder.yudao.module.pay.service.order;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
@@ -515,7 +514,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus()));
o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()));
// 调用,并断言异常
assertServiceException(() -> orderService.notifyOrder(channel, notify),
@@ -532,7 +531,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())
.setOutTradeNo("P110"));
// 调用,并断言异常
@@ -550,7 +549,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())
.setOutTradeNo("P110"));
// 调用,并断言异常
@@ -583,7 +582,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())
.setOutTradeNo("P110"));
// 调用,并断言异常
@@ -611,7 +610,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())
.setOutTradeNo("P110"));
// 调用,并断言异常
@@ -641,7 +640,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)
.setFeeRate(10D));
PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())
.setOutTradeNo("P110"));
// 调用,并断言异常
@@ -669,7 +668,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus()));
o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()));
// 调用,并断言异常
assertServiceException(() -> orderService.notifyOrder(channel, notify),
@@ -686,7 +685,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus())
o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())
.setOutTradeNo("P110"));
// 调用,并断言
@@ -705,7 +704,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus())
o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())
.setOutTradeNo("P110"));
// 调用,并断言
@@ -724,7 +723,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus())
o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())
.setOutTradeNo("P110"));
// 调用,并断言异常
@@ -742,7 +741,7 @@ public class PayOrderServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L));
PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class,
o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus())
o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())
.setOutTradeNo("P110"));
// 调用

View File

@@ -2,11 +2,10 @@ package cn.iocoder.yudao.module.pay.service.refund;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
@@ -464,7 +463,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
o -> o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
// 调用,并断言异常
assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO),
@@ -476,7 +475,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
o -> o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
// mock 数据refund + 已支付)
PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100")
.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()));
@@ -493,7 +492,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
o -> o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
// mock 数据refund + 已支付)
PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100")
.setStatus(PayRefundStatusEnum.FAILURE.getStatus()));
@@ -509,7 +508,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
o -> o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
// mock 数据refund + 已支付)
PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100")
.setStatus(PayRefundStatusEnum.WAITING.getStatus())
@@ -530,7 +529,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
o -> o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()).setOutRefundNo("R100"));
// mock 数据refund + 已支付)
PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100")
.setStatus(PayRefundStatusEnum.WAITING.getStatus())
@@ -557,7 +556,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100"));
o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus()).setOutRefundNo("R100"));
// 调用,并断言异常
assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO),
@@ -569,7 +568,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100"));
o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus()).setOutRefundNo("R100"));
// mock 数据refund + 退款失败)
PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100")
.setStatus(PayRefundStatusEnum.FAILURE.getStatus()));
@@ -586,7 +585,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100"));
o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus()).setOutRefundNo("R100"));
// mock 数据refund + 已支付)
PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100")
.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()));
@@ -602,7 +601,7 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
// 准备参数
PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L));
PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class,
o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100"));
o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus()).setOutRefundNo("R100"));
// mock 数据refund + 已支付)
PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100")
.setStatus(PayRefundStatusEnum.WAITING.getStatus())
@@ -639,17 +638,17 @@ public class PayRefundServiceTest extends BaseDbAndRedisUnitTest {
@Test
public void testSyncRefund_waiting() {
assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusRespEnum.WAITING.getStatus()), 0);
assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusEnum.WAITING.getStatus()), 0);
}
@Test
public void testSyncRefund_success() {
assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusRespEnum.SUCCESS.getStatus()), 1);
assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusEnum.SUCCESS.getStatus()), 1);
}
@Test
public void testSyncRefund_failure() {
assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusRespEnum.FAILURE.getStatus()), 1);
assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusEnum.FAILURE.getStatus()), 1);
}
private int testSyncRefund_waitingOrSuccessOrFailure(Integer status) {