Merge remote-tracking branch 'yudao/feature/mall_product' into feature/mall_product

This commit is contained in:
puhui999 2023-07-18 09:06:02 +08:00
commit d80bf5a368
78 changed files with 1638 additions and 1114 deletions

View File

@ -1,10 +1,12 @@
package cn.iocoder.yudao.framework.common.util.validation; package cn.iocoder.yudao.framework.common.util.validation;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException; import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator; import javax.validation.Validator;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -37,6 +39,12 @@ public class ValidationUtils {
&& PATTERN_XML_NCNAME.matcher(str).matches(); && PATTERN_XML_NCNAME.matcher(str).matches();
} }
public static void validate(Object object, Class<?>... groups) {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Assert.notNull(validator);
validate(validator, object, groups);
}
public static void validate(Validator validator, Object object, Class<?>... groups) { public static void validate(Validator validator, Object object, Class<?>... groups) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups); Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (CollUtil.isNotEmpty(constraintViolations)) { if (CollUtil.isNotEmpty(constraintViolations)) {

View File

@ -13,14 +13,25 @@ import javax.validation.constraints.NotEmpty;
public class PayProperties { public class PayProperties {
/** /**
* 回调地址 * 支付回调地址
* *
* 实际上对应的 PayNotifyController notifyCallback 方法的 URL * 实际上对应的 PayNotifyController notifyOrder 方法的 URL
* *
* 注意支付渠道统一回调到 payNotifyUrl 地址由支付模块统一处理然后自己的支付模块在回调 PayAppDO.payNotifyUrl 地址 * 回调顺序支付渠道支付宝支付微信支付 => yudao-module-pay orderNotifyUrl 地址 => 业务的 PayAppDO.orderNotifyUrl 地址
*/ */
@NotEmpty(message = "回调地址不能为空") @NotEmpty(message = "支付回调地址不能为空")
@URL(message = "回调地址的格式必须是 URL") @URL(message = "支付回调地址的格式必须是 URL")
private String callbackUrl; private String orderNotifyUrl;
/**
* 退款回调地址
*
* 实际上对应的 PayNotifyController notifyRefund 方法的 URL
*
* 回调顺序支付渠道支付宝支付微信支付 => yudao-module-pay refundNotifyUrl 地址 => 业务的 PayAppDO.notifyRefundUrl 地址
*/
@NotEmpty(message = "支付回调地址不能为空")
@URL(message = "支付回调地址的格式必须是 URL")
private String refundNotifyUrl;
} }

View File

@ -1,12 +1,11 @@
package cn.iocoder.yudao.framework.pay.core.client; package cn.iocoder.yudao.framework.pay.core.client;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO; 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.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import java.util.Map;
/** /**
* 支付客户端用于对接各支付渠道的 SDK实现发起支付退款等功能 * 支付客户端用于对接各支付渠道的 SDK实现发起支付退款等功能
@ -22,31 +21,42 @@ public interface PayClient {
*/ */
Long getId(); Long getId();
// ============ 支付相关 ==========
/** /**
* 调用支付渠道统一下单 * 调用支付渠道统一下单
* *
* @param reqDTO 下单信息 * @param reqDTO 下单信息
* @return 各支付渠道的返回结果 * @return 各支付渠道的返回结果
*/ */
PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO); PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
/**
* 解析 order 回调数据
*
* @param params HTTP 回调接口 content type application/x-www-form-urlencoded 的所有参数
* @param body HTTP 回调接口的 request body
* @return 支付订单信息
*/
PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body);
// ============ 退款相关 ==========
/** /**
* 调用支付渠道进行退款 * 调用支付渠道进行退款
*
* @param reqDTO 统一退款请求信息 * @param reqDTO 统一退款请求信息
* @return 各支付渠道的统一返回结果 * @return 退款信息
*/ */
PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO); PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
/** /**
* 解析回调数据 * 解析 refund 回调数据
* *
* @param rawNotify 通知内容 * @param params HTTP 回调接口 content type application/x-www-form-urlencoded 的所有参数
* @return 回调对象 * @param body HTTP 回调接口的 request body
* 1. {@link PayRefundNotifyRespDTO} 退款通知 * @return 支付订单信息
* 2. {@link PayOrderNotifyRespDTO} 支付通知
*/ */
default Object parseNotify(PayNotifyReqDTO rawNotify) { PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
throw new UnsupportedOperationException("未实现 parseNotify 方法!");
}
} }

View File

@ -1,29 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.notify;
import lombok.Builder;
import lombok.Data;
import lombok.ToString;
import java.util.Map;
/**
* 支付订单退款订单回调渠道的统一通知请求数据
*/
@Data
@ToString
@Builder
public class PayNotifyReqDTO {
/**
* HTTP 回调接口的 request body
*/
private String body;
/**
* HTTP 回调接口 content type application/x-www-form-urlencoded 的所有参数
*/
private Map<String,String> params;
}

View File

@ -1,38 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.notify;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 支付通知 Response DTO
*
* @author 芋道源码
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayOrderNotifyRespDTO {
/**
* 支付订单号支付模块的
*/
private String orderExtensionNo;
/**
* 支付渠道编号
*/
private String channelOrderNo;
/**
* 支付渠道用户编号
*/
private String channelUserId;
/**
* 支付成功时间
*/
private LocalDateTime successTime;
}

View File

@ -1,58 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.notify;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayNotifyRefundStatusEnum;
import lombok.Builder;
import lombok.Data;
import lombok.ToString;
import java.time.LocalDateTime;
/**
* 从渠道返回数据中解析得到的支付退款通知的Notify DTO
*
* @author jason
*/
@Data
@ToString
@Builder
public class PayRefundNotifyRespDTO {
/**
* 支付渠道编号
*/
private String channelOrderNo;
/**
* 交易订单号根据规则生成
* 调用支付渠道时使用该字段作为对接的订单号
* 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 使用该字段作为 out_trade_no
* 2. 调用支付宝 https://opendocs.alipay.com/apis 使用该字段作为 out_trade_no
* 这里对应 pay_extension 里面的 no
* 例如说P202110132239124200055
*/
private String tradeNo;
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
* 退款请求号
* 标识一次退款请求需要保证在交易号下唯一如需部分退款则此参数必传
* 针对同一次退款请求如果调用接口失败或异常了重试时需要保证退款请求号不能变更
* 防止该笔交易重复退款支付宝会保证同样的退款请求号多次请求只会退一次
* 退款单请求号根据规则生成
*
* 例如说RR202109181134287570000
*/
private String reqNo;
/**
* 退款是否成功
*/
private PayNotifyRefundStatusEnum status;
/**
* 退款成功时间
*/
private LocalDateTime refundSuccessTime;
}

View File

@ -0,0 +1,137 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.order;
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 渠道支付订单 Response DTO
*
* @author 芋道源码
*/
@Data
public class PayOrderRespDTO {
/**
* 支付状态
*
* 枚举{@link PayOrderStatusRespEnum}
*/
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 PayException}
*/
private String channelErrorCode;
/**
* 调用渠道报错时错误信息
*/
private String channelErrorMsg;
public PayOrderRespDTO() {
}
/**
* 创建WAITING状态的订单返回
*/
public PayOrderRespDTO(String displayMode, String displayContent,
String outTradeNo, Object rawData) {
this.status = PayOrderStatusRespEnum.WAITING.getStatus();
this.displayMode = displayMode;
this.displayContent = displayContent;
// 相对通用的字段
this.outTradeNo = outTradeNo;
this.rawData = rawData;
}
/**
* 创建SUCCESS状态的订单返回
*/
public PayOrderRespDTO(String channelOrderNo, String channelUserId, LocalDateTime successTime,
String outTradeNo, Object rawData) {
this.status = PayOrderStatusRespEnum.SUCCESS.getStatus();
this.channelOrderNo = channelOrderNo;
this.channelUserId = channelUserId;
this.successTime = successTime;
// 相对通用的字段
this.outTradeNo = outTradeNo;
this.rawData = rawData;
}
/**
* 创建SUCCESSCLOSED状态的订单返回适合支付渠道回调时
*/
public PayOrderRespDTO(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime,
String outTradeNo, Object rawData) {
this.status = status;
this.channelOrderNo = channelOrderNo;
this.channelUserId = channelUserId;
this.successTime = successTime;
// 相对通用的字段
this.outTradeNo = outTradeNo;
this.rawData = rawData;
}
/**
* 创建CLOSED状态的订单返回适合调用支付渠道失败时
*
* 参数和 {@link #PayOrderRespDTO(String, String, String, Object)} 冲突所以独立个方法出来
*/
public static PayOrderRespDTO build(String channelErrorCode, String channelErrorMsg,
String outTradeNo, Object rawData) {
PayOrderRespDTO respDTO = new PayOrderRespDTO();
respDTO.status = PayOrderStatusRespEnum.CLOSED.getStatus();
respDTO.channelErrorCode = channelErrorCode;
respDTO.channelErrorMsg = channelErrorMsg;
// 相对通用的字段
respDTO.outTradeNo = outTradeNo;
respDTO.rawData = rawData;
return respDTO;
}
}

View File

@ -28,10 +28,12 @@ public class PayOrderUnifiedReqDTO {
// ========== 商户相关字段 ========== // ========== 商户相关字段 ==========
/** /**
* 商户订单编号 * 外部订单号
*
* 对应 PayOrderExtensionDO no 字段
*/ */
@NotEmpty(message = "商户订单编号不能为空") @NotEmpty(message = "外部订单编号不能为空")
private String merchantOrderId; private String outTradeNo;
/** /**
* 商品标题 * 商品标题
*/ */
@ -41,7 +43,6 @@ public class PayOrderUnifiedReqDTO {
/** /**
* 商品描述信息 * 商品描述信息
*/ */
@NotEmpty(message = "商品描述信息不能为空")
@Length(max = 128, message = "商品描述信息长度不能超过128") @Length(max = 128, message = "商品描述信息长度不能超过128")
private String body; private String body;
/** /**
@ -63,7 +64,7 @@ public class PayOrderUnifiedReqDTO {
*/ */
@NotNull(message = "支付金额不能为空") @NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer amount; private Integer price;
/** /**
* 支付过期时间 * 支付过期时间

View File

@ -1,38 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.order;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
import lombok.Data;
/**
* 统一下单 Response DTO
*
* @author 芋道源码
*/
@Data
public class PayOrderUnifiedRespDTO {
/**
* 展示模式
*
* 枚举 {@link PayOrderDisplayModeEnum}
*/
private String displayMode;
/**
* 展示内容
*/
private String displayContent;
/**
* 同步的通知信息
*
* 目前只有 bar 条码支付才会出现它是支付发起时直接返回是否支付成功的而其它支付还是异步通知
*/
private PayOrderNotifyRespDTO notify;
public PayOrderUnifiedRespDTO(String displayMode, String displayContent) {
this.displayMode = displayMode;
this.displayContent = displayContent;
}
}

View File

@ -0,0 +1,47 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 渠道退款订单 Response DTO
*
* @author jason
*/
@Data
public class PayRefundRespDTO {
/**
* 退款状态
*
* 枚举 {@link PayRefundStatusRespEnum}
*/
private Integer status;
/**
* 外部退款号
*
* 对应 PayRefundDO no 字段
*/
private String outRefundNo;
/**
* 渠道退款单号
*
* 对应 PayRefundDO.channelRefundNo 字段
*/
private String channelRefundNo;
/**
* 退款成功时间
*/
private LocalDateTime successTime;
/**
* 原始的异步通知结果
*/
private Object rawData;
}

View File

@ -24,33 +24,20 @@ import javax.validation.constraints.NotNull;
public class PayRefundUnifiedReqDTO { public class PayRefundUnifiedReqDTO {
/** /**
* 用户 IP * 外部订单号
*
* 对应 PayOrderExtensionDO no 字段
*/ */
private String userIp; @NotEmpty(message = "外部订单编号不能为空")
private String outTradeNo;
// TODO @jason这个是否为非必传字段呀只需要传递 payTradeNo 字段即可尽可能精简
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 transaction_id
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 trade_no
* 渠道订单号
*/
private String channelOrderNo;
/** /**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_trade_no * 外部退款号
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no *
* 支付交易号 {PayOrderExtensionDO no字段} 渠道订单号 不能同时为空 * 对应 PayRefundDO no 字段
*/ */
private String payTradeNo; @NotEmpty(message = "退款请求单号不能为空")
private String outRefundNo;
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
* 退款请求单号 同一退款请求单号多次请求只退一笔
* 使用 商户的退款单号{PayRefundDO 字段 merchantRefundNo}
*/
@NotEmpty(message = "退款请求单号")
private String merchantRefundId;
/** /**
* 退款原因 * 退款原因
@ -58,16 +45,25 @@ public class PayRefundUnifiedReqDTO {
@NotEmpty(message = "退款原因不能为空") @NotEmpty(message = "退款原因不能为空")
private String reason; private String reason;
/**
* 支付金额单位
*
* 目前微信支付在退款的时候必须传递该字段
*/
@NotNull(message = "支付金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer payPrice;
/** /**
* 退款金额单位 * 退款金额单位
*/ */
@NotNull(message = "退款金额不能为空") @NotNull(message = "退款金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Integer amount; private Integer refundPrice;
/** /**
* 退款结果 notify 回调地址 支付宝退款不需要回调地址 微信需要 * 退款结果 notify 回调地址
*/ */
@NotEmpty(message = "支付结果的回调地址不能为空")
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式") @URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
private String notifyUrl; private String notifyUrl;

View File

@ -1,24 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* 统一退款 Response DTO
*
* @author jason
*/
@Accessors(chain = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PayRefundUnifiedRespDTO {
/**
* 渠道退款单编号
*/
private String channelRefundId;
}

View File

@ -1,16 +1,17 @@
package cn.iocoder.yudao.framework.pay.core.client.impl; package cn.iocoder.yudao.framework.pay.core.client.impl;
import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.framework.pay.core.client.PayClient; 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.PayClientConfig;
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.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO; 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.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException; import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import javax.validation.Validation; import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
@ -29,6 +30,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
/** /**
* 渠道编码 * 渠道编码
*/ */
@SuppressWarnings("FieldCanBeLocal")
private final String channelCode; private final String channelCode;
/** /**
* 支付配置 * 支付配置
@ -46,7 +48,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
*/ */
public final void init() { public final void init() {
doInit(); doInit();
log.info("[init][配置({}) 初始化完成]", config); log.info("[init][客户端({}) 初始化完成]", getId());
} }
/** /**
@ -59,7 +61,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
if (config.equals(this.config)) { if (config.equals(this.config)) {
return; return;
} }
log.info("[refresh][配置({})发生变化,重新初始化]", config); log.info("[refresh][客户端({})发生变化,重新初始化]", getId());
this.config = config; this.config = config;
// 初始化 // 初始化
this.init(); this.init();
@ -70,32 +72,47 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
return channelId; return channelId;
} }
// ============ 支付相关 ==========
@Override @Override
public final PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO); ValidationUtils.validate(reqDTO);
// 执行统一下单 // 执行统一下单
PayOrderUnifiedRespDTO resp; PayOrderRespDTO resp;
try { try {
resp = doUnifiedOrder(reqDTO); resp = doUnifiedOrder(reqDTO);
} catch (ServiceException ex) {
// 业务异常都是实现类已经翻译所以直接抛出即可
throw ex;
} catch (Throwable ex) { } catch (Throwable ex) {
// 系统异常则包装成 PayException 异常抛出 // 系统异常则包装成 PayException 异常抛出
log.error("[unifiedRefund][request({}) 发起支付异常]", toJsonString(reqDTO), ex); log.error("[unifiedRefund][客户端({}) request({}) 发起支付异常]",
throw buildException(ex); getId(), toJsonString(reqDTO), ex);
throw buildPayException(ex);
} }
return resp; return resp;
} }
protected abstract PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
throws Throwable; throws Throwable;
@Override @Override
public PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) { public PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO); try {
return doParseOrderNotify(params, body);
} catch (Throwable ex) {
log.error("[parseOrderNotify][params({}) body({}) 解析失败]", params, body, ex);
throw buildPayException(ex);
}
}
protected abstract PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body)
throws Throwable;
// ============ 退款相关 ==========
@Override
public PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
ValidationUtils.validate(reqDTO);
// 执行统一退款 // 执行统一退款
PayRefundUnifiedRespDTO resp; PayRefundRespDTO resp;
try { try {
resp = doUnifiedRefund(reqDTO); resp = doUnifiedRefund(reqDTO);
} catch (ServiceException ex) { } catch (ServiceException ex) {
@ -103,17 +120,18 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
throw ex; throw ex;
} catch (Throwable ex) { } catch (Throwable ex) {
// 系统异常则包装成 PayException 异常抛出 // 系统异常则包装成 PayException 异常抛出
log.error("[unifiedRefund][request({}) 发起退款异常]", toJsonString(reqDTO), ex); log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]",
throw buildException(ex); getId(), toJsonString(reqDTO), ex);
throw buildPayException(ex);
} }
return resp; return resp;
} }
protected abstract PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable; protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
// ========== 各种工具方法 ========== // ========== 各种工具方法 ==========
private PayException buildException(Throwable ex) { private PayException buildPayException(Throwable ex) {
if (ex instanceof PayException) { if (ex instanceof PayException) {
return (PayException) ex; return (PayException) ex;
} }

View File

@ -2,16 +2,19 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayNotifyRefundStatusEnum; import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import com.alipay.api.*; 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.AlipayTradeRefundModel; import com.alipay.api.domain.AlipayTradeRefundModel;
import com.alipay.api.internal.util.AlipaySignature; import com.alipay.api.internal.util.AlipaySignature;
import com.alipay.api.request.AlipayTradeRefundRequest; import com.alipay.api.request.AlipayTradeRefundRequest;
@ -22,11 +25,11 @@ import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Map; 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.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants.ORDER_UNIFIED_ERROR;
/** /**
* 支付宝抽象类实现支付宝统一的接口以及部分实现退款 * 支付宝抽象类实现支付宝统一的接口以及部分实现退款
@ -50,72 +53,88 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
this.client = new DefaultAlipayClient(alipayConfig); this.client = new DefaultAlipayClient(alipayConfig);
} }
// ============ 支付相关 ==========
/**
* 构造支付关闭的 {@link PayOrderRespDTO} 对象
*
* @return 支付关闭的 {@link PayOrderRespDTO} 对象
*/
protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) {
Assert.isFalse(response.isSuccess());
return PayOrderRespDTO.build(response.getSubCode(), response.getSubMsg(),
reqDTO.getOutTradeNo(), response);
}
@Override
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws Throwable {
// 1. 校验回调数据
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
StandardCharsets.UTF_8.name(), config.getSignType());
// 2. 解析订单的状态
String tradeStatus = bodyObj.get("trade_status");
Integer status = Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus()
: Objects.equals("TRADE_SUCCESS", tradeStatus) ? PayOrderStatusRespEnum.SUCCESS.getStatus()
: Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null;
Assert.notNull(status, (Supplier<Throwable>) () -> {
throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body));
});
return new PayOrderRespDTO(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")),
bodyObj.get("out_trade_no"), body);
}
// ============ 退款相关 ==========
/** /**
* 支付宝统一的退款接口 alipay.trade.refund * 支付宝统一的退款接口 alipay.trade.refund
*
* @param reqDTO 退款请求 request DTO * @param reqDTO 退款请求 request DTO
* @return 退款请求 Response * @return 退款请求 Response
*/ */
@Override @Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
AlipayTradeRefundModel model=new AlipayTradeRefundModel(); // 1.1 构建 AlipayTradeRefundModel 请求
model.setTradeNo(reqDTO.getChannelOrderNo()); AlipayTradeRefundModel model = new AlipayTradeRefundModel();
model.setOutTradeNo(reqDTO.getPayTradeNo()); model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setOutRequestNo(reqDTO.getOutRefundNo());
model.setOutRequestNo(reqDTO.getMerchantRefundId()); model.setRefundAmount(formatAmount(reqDTO.getRefundPrice()));
model.setRefundAmount(formatAmount(reqDTO.getAmount()));
model.setRefundReason(reqDTO.getReason()); model.setRefundReason(reqDTO.getReason());
// 1.2 构建 AlipayTradePayRequest 请求
AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest(); AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
refundRequest.setBizModel(model); request.setBizModel(model);
refundRequest.setNotifyUrl(reqDTO.getNotifyUrl());
refundRequest.setReturnUrl(reqDTO.getNotifyUrl());
try { try {
AlipayTradeRefundResponse response = client.execute(refundRequest); // 2.1 执行请求
log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response)); AlipayTradeRefundResponse response = client.execute(request);
// 2.2 创建返回结果
PayRefundRespDTO refund = new PayRefundRespDTO()
.setOutRefundNo(reqDTO.getOutRefundNo())
.setRawData(response);
// 支付宝只要退款调用返回 success就认为退款成功不需要回调具体可见 parseNotify 方法的说明
// 另外支付宝没有退款单号所以不用设置
if (response.isSuccess()) { if (response.isSuccess()) {
//退款导致触发的异步通知是发送到支付接口中设置的notify_url refund.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
//支付宝不返回退款单号设置为空 .setSuccessTime(LocalDateTimeUtil.of(response.getGmtRefundPay()));
PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO(); Assert.notNull(refund.getSuccessTime(), "退款成功时间不能为空");
respDTO.setChannelRefundId(""); } else {
// return PayCommonResult.build(response.getCode(), response.getMsg(), respDTO, codeMapping); TODO refund.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus());
return null;
} }
// 失败需要抛出异常 return refund;
// return PayCommonResult.build(response.getCode(), response.getMsg(), null, codeMapping); TODO
return null;
} catch (AlipayApiException e) { } catch (AlipayApiException e) {
// TODO 记录异常日志 log.error("[doUnifiedRefund][request({}) 发起退款异常]", toJsonString(reqDTO), e);
log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);
// return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping); TODO
return null; return null;
} }
} }
@Override @Override
@SneakyThrows public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
public Object parseNotify(PayNotifyReqDTO rawNotify) { // 补充说明支付宝退款时没有回调这点和微信支付是不同的并且退款分成部分退款和全部退款
// 1. 校验回调数据 // 部分退款是会有回调但是它回调的是订单状态的同步回调不是退款订单的回调
String body = rawNotify.getBody(); // 全部退款Wap 支付有订单状态的同步回调但是 PC/扫码又没有
Map<String, String> params = rawNotify.getParams(); // 所以这里在解析时即使是退款导致的订单状态同步我们也忽略不做为退款同步而是订单的回调
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); // 实际上支付宝退款只要发起成功就可以认为退款成功不需要等待回调
AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(), throw new UnsupportedOperationException("支付宝无退款回调");
StandardCharsets.UTF_8.name(), "RSA2");
// 2.1 退款的情况
if (bodyObj.containsKey("refund_fee")) {
return PayRefundNotifyRespDTO.builder().channelOrderNo(bodyObj.get("trade_no"))
.tradeNo(bodyObj.get("out_trade_no")).reqNo(bodyObj.get("out_biz_no"))
.status(PayNotifyRefundStatusEnum.SUCCESS)
.refundSuccessTime(parseTime(params.get("gmt_refund")))
.build();
}
// 2.2 支付的情况
return PayOrderNotifyRespDTO.builder()
.orderExtensionNo(bodyObj.get("out_trade_no"))
.channelOrderNo(bodyObj.get("trade_no"))
.channelUserId(bodyObj.get("seller_id"))
.successTime(parseTime(params.get("notify_time")))
.build();
} }
// ========== 各种工具方法 ========== // ========== 各种工具方法 ==========
@ -132,21 +151,4 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER); return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER);
} }
/**
* 校验支付宝统一下单的响应
*
* 如果校验不通过则抛出 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} 异常
*
* @param request 请求
* @param response 响应
*/
protected void validateUnifiedOrderResponse(Object request, AlipayResponse response) {
if (response.isSuccess()) {
return;
}
log.error("[validateUnifiedOrderResponse][发起支付失败request({})response({})]",
JsonUtils.toJsonString(request), JsonUtils.toJsonString(response));
throw exception0(ORDER_UNIFIED_ERROR.getCode(), response.getSubMsg());
}
} }

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
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.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 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.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayApiException;
@ -27,14 +27,14 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
} }
@Override @Override
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradeAppPayModel 请求 // 1.1 构建 AlipayTradeAppPayModel 请求
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel(); AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
// 通用的参数 // 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId()); model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject()); model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody()); model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount())); model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setProductCode(" QUICK_MSECURITY_PAY"); // 销售产品码无线快捷支付产品 model.setProductCode(" QUICK_MSECURITY_PAY"); // 销售产品码无线快捷支付产品
// 个性化的参数 // 个性化的参数
// 支付宝扫码支付只有一种展示 // 支付宝扫码支付只有一种展示
@ -49,8 +49,11 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
// 2.1 执行请求 // 2.1 执行请求
AlipayTradeAppPayResponse response = client.execute(request); AlipayTradeAppPayResponse response = client.execute(request);
// 2.2 处理结果 // 2.2 处理结果
validateUnifiedOrderResponse(request, response); if (!response.isSuccess()) {
return new PayOrderUnifiedRespDTO(displayMode, ""); return buildClosedPayOrderRespDTO(reqDTO, response);
}
return new PayOrderRespDTO(displayMode, "",
reqDTO.getOutTradeNo(), response);
} }
} }

View File

@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
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.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 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.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayApiException;
@ -30,7 +30,7 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
} }
@Override @Override
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code"); String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code");
if (StrUtil.isEmpty(authCode)) { if (StrUtil.isEmpty(authCode)) {
throw exception0(BAD_REQUEST.getCode(), "条形码不能为空"); throw exception0(BAD_REQUEST.getCode(), "条形码不能为空");
@ -39,10 +39,10 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
// 1.1 构建 AlipayTradePayModel 请求 // 1.1 构建 AlipayTradePayModel 请求
AlipayTradePayModel model = new AlipayTradePayModel(); AlipayTradePayModel model = new AlipayTradePayModel();
// 通用的参数 // 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId()); model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject()); model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody()); model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount())); model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setScene("bar_code"); // 当面付条码支付场景 model.setScene("bar_code"); // 当面付条码支付场景
// 个性化的参数 // 个性化的参数
model.setAuthCode(authCode); model.setAuthCode(authCode);
@ -55,11 +55,15 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
request.setNotifyUrl(reqDTO.getNotifyUrl()); request.setNotifyUrl(reqDTO.getNotifyUrl());
request.setReturnUrl(reqDTO.getReturnUrl()); request.setReturnUrl(reqDTO.getReturnUrl());
// TODO 芋艿各种边界的处理
// 2.1 执行请求 // 2.1 执行请求
AlipayTradePayResponse response = client.execute(request); AlipayTradePayResponse response = client.execute(request);
// 2.2 处理结果 // 2.2 处理结果
validateUnifiedOrderResponse(request, response); if (!response.isSuccess()) {
return new PayOrderUnifiedRespDTO(displayMode, ""); return buildClosedPayOrderRespDTO(reqDTO, response);
}
return new PayOrderRespDTO(displayMode, "",
reqDTO.getOutTradeNo(), response);
} }
} }

View File

@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.Method; import cn.hutool.http.Method;
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.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 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.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayApiException;
@ -29,14 +29,14 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
} }
@Override @Override
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradePagePayModel 请求 // 1.1 构建 AlipayTradePagePayModel 请求
AlipayTradePagePayModel model = new AlipayTradePagePayModel(); AlipayTradePagePayModel model = new AlipayTradePagePayModel();
// 通用的参数 // 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId()); model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject()); model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody()); model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount())); model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setTimeExpire(formatTime(reqDTO.getExpireTime())); model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
// 个性化的参数 // 个性化的参数
@ -60,8 +60,11 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
response = client.pageExecute(request, Method.GET.name()); response = client.pageExecute(request, Method.GET.name());
} }
// 2.2 处理结果 // 2.2 处理结果
validateUnifiedOrderResponse(request, response); if (!response.isSuccess()) {
return new PayOrderUnifiedRespDTO(displayMode, response.getBody()); return buildClosedPayOrderRespDTO(reqDTO, response);
}
return new PayOrderRespDTO(displayMode, response.getBody(),
reqDTO.getOutTradeNo(), response);
} }
} }

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
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.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 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.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayApiException;
@ -25,14 +25,14 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
} }
@Override @Override
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradePrecreateModel 请求 // 1.1 构建 AlipayTradePrecreateModel 请求
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
// 通用的参数 // 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId()); model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject()); model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody()); model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount())); model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT
// 个性化的参数 // 个性化的参数
// 支付宝扫码支付只有一种展示考虑到前端可能希望二维码扫描后手机打开 // 支付宝扫码支付只有一种展示考虑到前端可能希望二维码扫描后手机打开
@ -47,8 +47,11 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
// 2.1 执行请求 // 2.1 执行请求
AlipayTradePrecreateResponse response = client.execute(request); AlipayTradePrecreateResponse response = client.execute(request);
// 2.2 处理结果 // 2.2 处理结果
validateUnifiedOrderResponse(request, response); if (!response.isSuccess()) {
return new PayOrderUnifiedRespDTO(displayMode, response.getQrCode()); return buildClosedPayOrderRespDTO(reqDTO, response);
}
return new PayOrderRespDTO(displayMode, response.getQrCode(),
reqDTO.getOutTradeNo(), response);
} }
} }

View File

@ -1,8 +1,8 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.http.Method; import cn.hutool.http.Method;
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.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 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.PayOrderDisplayModeEnum;
import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayApiException;
@ -26,14 +26,14 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
} }
@Override @Override
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
// 1.1 构建 AlipayTradeWapPayModel 请求 // 1.1 构建 AlipayTradeWapPayModel 请求
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel(); AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
// 通用的参数 // 通用的参数
model.setOutTradeNo(reqDTO.getMerchantOrderId()); model.setOutTradeNo(reqDTO.getOutTradeNo());
model.setSubject(reqDTO.getSubject()); model.setSubject(reqDTO.getSubject());
model.setBody(reqDTO.getBody()); model.setBody(reqDTO.getBody());
model.setTotalAmount(formatAmount(reqDTO.getAmount())); model.setTotalAmount(formatAmount(reqDTO.getPrice()));
model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
// 个性化的参数 // 个性化的参数
// 支付宝 Wap 支付只有一种展示URL // 支付宝 Wap 支付只有一种展示URL
@ -48,10 +48,12 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
// 2.1 执行请求 // 2.1 执行请求
AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name()); AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name());
// 2.2 处理结果 // 2.2 处理结果
validateUnifiedOrderResponse(request, response); if (!response.isSuccess()) {
return new PayOrderUnifiedRespDTO(displayMode, response.getBody()); return buildClosedPayOrderRespDTO(reqDTO, response);
}
return new PayOrderRespDTO(displayMode, response.getBody(),
reqDTO.getOutTradeNo(), response);
} }
} }

View File

@ -1,19 +1,26 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.date.TemporalAccessorUtil; import cn.hutool.core.date.TemporalAccessorUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.io.FileUtils; import cn.iocoder.yudao.framework.common.util.io.FileUtils;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO; 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.impl.AbstractPayClient; import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants; import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result; import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
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.config.WxPayConfig;
import com.github.binarywang.wxpay.exception.WxPayException; import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService; import com.github.binarywang.wxpay.service.WxPayService;
@ -22,12 +29,11 @@ import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import static cn.hutool.core.date.DatePattern.PURE_DATETIME_PATTERN; import static cn.hutool.core.date.DatePattern.*;
import static cn.hutool.core.date.DatePattern.UTC_WITH_XXX_OFFSET_PATTERN; import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.*;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/** /**
* 微信支付抽象类实现微信统一的接口以及部分实现退款 * 微信支付抽象类实现微信统一的接口以及部分实现退款
@ -53,12 +59,14 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
WxPayConfig payConfig = new WxPayConfig(); WxPayConfig payConfig = new WxPayConfig();
BeanUtil.copyProperties(config, payConfig, "keyContent"); BeanUtil.copyProperties(config, payConfig, "keyContent");
payConfig.setTradeType(tradeType); payConfig.setTradeType(tradeType);
// weixin-pay-java 无法设置内容只允许读取文件所以这里要创建临时文件来解决
if (Base64.isBase64(config.getKeyContent())) {
payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
}
if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) { if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
// weixin-pay-java 存在 BUG无法直接设置内容所以创建临时文件来解决
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath()); payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
} }
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) { if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
// weixin-pay-java 存在 BUG无法直接设置内容所以创建临时文件来解决
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath()); payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
} }
@ -67,11 +75,13 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
client.setConfig(payConfig); client.setConfig(payConfig);
} }
// ============ 支付相关 ==========
@Override @Override
protected PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception { protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
try { try {
switch (config.getApiVersion()) { switch (config.getApiVersion()) {
case WxPayClientConfig.API_VERSION_V2: case API_VERSION_V2:
return doUnifiedOrderV2(reqDTO); return doUnifiedOrderV2(reqDTO);
case WxPayClientConfig.API_VERSION_V3: case WxPayClientConfig.API_VERSION_V3:
return doUnifiedOrderV3(reqDTO); return doUnifiedOrderV3(reqDTO);
@ -79,7 +89,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
} }
} catch (WxPayException e) { } catch (WxPayException e) {
throw buildUnifiedOrderException(reqDTO, e); String errorCode = getErrorCode(e);
String errorMessage = getErrorMessage(e);
return PayOrderRespDTO.build(errorCode, errorMessage,
reqDTO.getOutTradeNo(), e.getXmlString());
} }
} }
@ -89,8 +102,8 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
* @param reqDTO 下单信息 * @param reqDTO 下单信息
* @return 各支付渠道的返回结果 * @return 各支付渠道的返回结果
*/ */
protected abstract PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
throws WxPayException; throws Exception;
/** /**
* V3调用支付渠道统一下单 * V3调用支付渠道统一下单
@ -98,87 +111,177 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
* @param reqDTO 下单信息 * @param reqDTO 下单信息
* @return 各支付渠道的返回结果 * @return 各支付渠道的返回结果
*/ */
protected abstract PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
throws WxPayException; throws WxPayException;
@Override
public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws WxPayException {
// 微信支付 v2 回调结果处理
switch (config.getApiVersion()) {
case API_VERSION_V2:
return doParseOrderNotifyV2(body);
case WxPayClientConfig.API_VERSION_V3:
return doParseOrderNotifyV3(body);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
}
private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException {
// 1. 解析回调
WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
// 2. 构建结果
Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ?
PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus();
return new PayOrderRespDTO(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
response.getOutTradeNo(), body);
}
private PayOrderRespDTO doParseOrderNotifyV3(String body) throws WxPayException {
// 1. 解析回调
WxPayOrderNotifyV3Result response = client.parseOrderNotifyV3Result(body, null);
WxPayOrderNotifyV3Result.DecryptNotifyResult result = response.getResult();
// 2. 构建结果
Integer status = Objects.equals(result.getTradeState(), "SUCCESS") ?
PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus();
String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null;
return new PayOrderRespDTO(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()),
result.getOutTradeNo(), body);
}
// ============ 退款相关 ==========
@Override @Override
public Object parseNotify(PayNotifyReqDTO rawNotify) { protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
log.info("[parseNotify][微信支付回调 data 数据: {}]", rawNotify.getBody());
try { try {
// 微信支付 v2 回调结果处理
switch (config.getApiVersion()) { switch (config.getApiVersion()) {
case WxPayClientConfig.API_VERSION_V2: case API_VERSION_V2:
return parseOrderNotifyV2(rawNotify); return doUnifiedRefundV2(reqDTO);
case WxPayClientConfig.API_VERSION_V3: case WxPayClientConfig.API_VERSION_V3:
return parseOrderNotifyV3(rawNotify); return doUnifiedRefundV3(reqDTO);
default: default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
} }
} catch (WxPayException e) { } catch (WxPayException e) {
log.error("[parseNotify][rawNotify({}) 解析失败]", toJsonString(rawNotify), e); // todo 芋艿异常的处理
// throw buildPayException(e); // throw buildUnifiedOrderException(null, e);
return null;
}
}
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 创建返回结果
PayRefundRespDTO refund = new PayRefundRespDTO()
.setOutRefundNo(reqDTO.getOutRefundNo())
.setRawData(response);
if (Objects.equals("SUCCESS", response.getResultCode())) {
refund.setStatus(PayRefundStatusRespEnum.WAITING.getStatus())
.setChannelRefundNo(response.getRefundId());
} else {
refund.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
}
// TODO 芋艿异常的处理
return refund;
}
private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
// 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 创建返回结果
PayRefundRespDTO refund = new PayRefundRespDTO()
.setOutRefundNo(reqDTO.getOutRefundNo())
.setRawData(response);
if (Objects.equals("SUCCESS", response.getStatus())) {
refund.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())
.setChannelRefundNo(response.getRefundId())
.setSuccessTime(parseDateV3(response.getSuccessTime()));
} else if (Objects.equals("PROCESSING", response.getStatus())) {
refund.setStatus(PayRefundStatusRespEnum.WAITING.getStatus())
.setChannelRefundNo(response.getRefundId());
} else {
refund.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
}
// TODO 芋艿异常的处理
return refund;
}
@Override
public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
try {
// 微信支付 v2 回调结果处理
switch (config.getApiVersion()) {
case API_VERSION_V2:
return parseRefundNotifyV2(body);
case WxPayClientConfig.API_VERSION_V3:
return parseRefundNotifyV3(body);
default:
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
}
} catch (WxPayException e) {
log.error("[parseNotify][params({}) body({}) 解析失败]", params, body, e);
throw new RuntimeException(e); throw new RuntimeException(e);
// TODO 芋艿缺一个异常翻译 // TODO 芋艿缺一个异常翻译
} }
} }
private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyReqDTO data) throws WxPayException { @SuppressWarnings("DuplicatedCode")
WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody()); private PayRefundRespDTO parseRefundNotifyV2(String body) throws WxPayException {
Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS"); // 1. 解析回调
// 转换结果 WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
return PayOrderNotifyRespDTO WxPayRefundNotifyResult.ReqInfo responseResult = response.getReqInfo();
.builder() // 2. 构建结果
.orderExtensionNo(notifyResult.getOutTradeNo()) PayRefundRespDTO notify = new PayRefundRespDTO()
.channelOrderNo(notifyResult.getTransactionId()) .setChannelRefundNo(responseResult.getRefundId())
.channelUserId(notifyResult.getOpenid()) .setOutRefundNo(responseResult.getOutRefundNo())
.successTime(parseDateV2(notifyResult.getTimeEnd())) .setRawData(response);
.build(); if (Objects.equals("SUCCESS", responseResult.getRefundStatus())) {
notify.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())
.setSuccessTime(parseDateV2B(responseResult.getSuccessTime()));
} else {
notify.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
}
return notify;
} }
private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyReqDTO data) throws WxPayException { @SuppressWarnings("DuplicatedCode")
WxPayOrderNotifyV3Result notifyResult = client.parseOrderNotifyV3Result(data.getBody(), null); private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException {
WxPayOrderNotifyV3Result.DecryptNotifyResult result = notifyResult.getResult(); // 1. 解析回调
// 转换结果 WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null);
Assert.isTrue(Objects.equals(notifyResult.getResult().getTradeState(), "SUCCESS"), WxPayRefundNotifyV3Result.DecryptNotifyResult responseResult = response.getResult();
"支付结果非 SUCCESS"); // 2. 构建结果
return PayOrderNotifyRespDTO.builder() PayRefundRespDTO notify = new PayRefundRespDTO()
.orderExtensionNo(result.getOutTradeNo()) .setChannelRefundNo(responseResult.getRefundId())
.channelOrderNo(result.getTradeState()) .setOutRefundNo(responseResult.getOutRefundNo())
.channelUserId(result.getPayer() != null ? result.getPayer().getOpenid() : null) .setRawData(response);
.successTime(parseDateV3(result.getSuccessTime())) if (Objects.equals("SUCCESS", responseResult.getRefundStatus())) {
.build(); notify.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())
.setSuccessTime(parseDateV3(responseResult.getSuccessTime()));
} else {
notify.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
}
return notify;
} }
// ========== 各种工具方法 ========== // ========== 各种工具方法 ==========
/**
* 构建统一下单的异常
*
* 目的将参数不正确等异常转换成 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} 业务异常
*
* @param reqDTO 请求
* @param e 微信的支付异常
* @return 转换后的异常
*
*/
static Exception buildUnifiedOrderException(PayOrderUnifiedReqDTO reqDTO, WxPayException e) {
// 情况一业务结果为 FAIL
if (Objects.equals(e.getResultCode(), "FAIL")) {
log.error("[buildUnifiedOrderException][request({}) 发起支付失败]", toJsonString(reqDTO), e);
if (Objects.equals(e.getErrCode(), "PARAM_ERROR")) {
throw invalidParamException(e.getErrCodeDes());
}
throw exception(PayFrameworkErrorCodeConstants.ORDER_UNIFIED_ERROR, e.getErrCodeDes());
}
// 情况二状态码结果为 FAIL
if (Objects.equals(e.getReturnCode(), "FAIL")) {
throw exception(PayFrameworkErrorCodeConstants.ORDER_UNIFIED_ERROR, e.getReturnMsg());
}
// 情况三系统异常这里暂时不打交给上层的 AbstractPayClient 统一打
return e;
}
static String formatDateV2(LocalDateTime time) { static String formatDateV2(LocalDateTime time) {
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN); return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN);
} }
@ -187,6 +290,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN); return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN);
} }
static LocalDateTime parseDateV2B(String time) {
return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN);
}
static String formatDateV3(LocalDateTime time) { static String formatDateV3(LocalDateTime time) {
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN); return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN);
} }
@ -195,4 +302,24 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN); 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

@ -1,9 +1,7 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
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.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import com.github.binarywang.wxpay.constant.WxPayConstants; import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException; import com.github.binarywang.wxpay.exception.WxPayException;
@ -21,17 +19,12 @@ public class WxAppPayClient extends AbstractWxPayClient {
} }
@Override @Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable { protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return null; return null;
} }
@Override @Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return null;
}
@Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return null; return null;
} }

View File

@ -4,12 +4,8 @@ import cn.hutool.core.map.MapUtil;
import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 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.PayOrderDisplayModeEnum;
import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest; import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
@ -23,6 +19,7 @@ import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/** /**
* 微信支付付款码支付 PayClient 实现类 * 微信支付付款码支付 PayClient 实现类
@ -49,7 +46,7 @@ public class WxBarPayClient extends AbstractWxPayClient {
} }
@Override @Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 由于付款码需要不断轮询所以需要在较短的时间完成支付 // 由于付款码需要不断轮询所以需要在较短的时间完成支付
LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE); LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE);
if (expireTime.isAfter(reqDTO.getExpireTime())) { if (expireTime.isAfter(reqDTO.getExpireTime())) {
@ -57,29 +54,25 @@ public class WxBarPayClient extends AbstractWxPayClient {
} }
// 构建 WxPayMicropayRequest 对象 // 构建 WxPayMicropayRequest 对象
WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder() WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
.outTradeNo(reqDTO.getMerchantOrderId()) .outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject()) .body(reqDTO.getSubject())
.detail(reqDTO.getBody()) .detail(reqDTO.getBody())
.totalFee(reqDTO.getAmount()) // 单位分 .totalFee(reqDTO.getPrice()) // 单位分
.timeExpire(formatDateV2(expireTime)) .timeExpire(formatDateV2(expireTime))
.spbillCreateIp(reqDTO.getUserIp()) .spbillCreateIp(reqDTO.getUserIp())
.authCode(getAuthCode(reqDTO)) .authCode(getAuthCode(reqDTO))
.build(); .build();
// 执行请求重试直到失败过期或者成功 // 执行请求重试直到失败过期或者成功
WxPayException lastWxPayException = null;
for (int i = 1; i < Byte.MAX_VALUE; i++) { for (int i = 1; i < Byte.MAX_VALUE; i++) {
try { try {
WxPayMicropayResult response = client.micropay(request); WxPayMicropayResult response = client.micropay(request);
// 支付成功例如说用户输入了密码 // 支付成功例如说1用户输入了密码2
PayOrderNotifyRespDTO notify = PayOrderNotifyRespDTO.builder() return new PayOrderRespDTO(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
.orderExtensionNo(response.getOutTradeNo()) response.getOutTradeNo(), response)
.channelOrderNo(response.getTransactionId()) .setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode());
.channelUserId(response.getOpenid())
.successTime(parseDateV2(response.getTimeEnd()))
.build();
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.BAR_CODE.getMode(),
JsonUtils.toJsonString(response))
.setNotify(notify);
} catch (WxPayException ex) { } catch (WxPayException ex) {
lastWxPayException = ex;
// 如果不满足这 3 种任一的则直接抛出 WxPayException 异常不仅需处理 // 如果不满足这 3 种任一的则直接抛出 WxPayException 异常不仅需处理
// 1. SYSTEMERROR接口返回错误请立即调用被扫订单结果查询API查询当前订单状态并根据订单的状态决定下一步的操作 // 1. SYSTEMERROR接口返回错误请立即调用被扫订单结果查询API查询当前订单状态并根据订单的状态决定下一步的操作
// 2. USERPAYING用户支付中需要输入密码等待 5 然后调用被扫订单结果查询 API查询当前订单的不同状态决定下一步的操作 // 2. USERPAYING用户支付中需要输入密码等待 5 然后调用被扫订单结果查询 API查询当前订单的不同状态决定下一步的操作
@ -89,23 +82,18 @@ public class WxBarPayClient extends AbstractWxPayClient {
} }
// 等待 5 继续下一轮重新发起支付 // 等待 5 继续下一轮重新发起支付
log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i, log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i,
JsonUtils.toJsonString(request), ex.getMessage()); toJsonString(request), ex.getMessage());
ThreadUtil.sleep(5, TimeUnit.SECONDS); ThreadUtil.sleep(5, TimeUnit.SECONDS);
} }
} }
throw new IllegalStateException("微信 Bar 支付,重试多次失败"); throw lastWxPayException;
} }
@Override @Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return doUnifiedOrderV2(reqDTO); return doUnifiedOrderV2(reqDTO);
} }
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
return null;
}
// ========== 各种工具方法 ========== // ========== 各种工具方法 ==========
static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) { static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) {

View File

@ -1,9 +1,7 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
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.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
import com.github.binarywang.wxpay.constant.WxPayConstants; import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException; import com.github.binarywang.wxpay.exception.WxPayException;
@ -21,17 +19,12 @@ public class WxH5PayClient extends AbstractWxPayClient {
} }
@Override @Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable { protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return null; return null;
} }
@Override @Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return null;
}
@Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
return null; return null;
} }

View File

@ -1,9 +1,7 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
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.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 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.PayOrderDisplayModeEnum;
import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult; import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
@ -34,14 +32,14 @@ public class WxNativePayClient extends AbstractWxPayClient {
} }
@Override @Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象 // 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder() WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
.outTradeNo(reqDTO.getMerchantOrderId()) .outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject()) .body(reqDTO.getSubject())
.detail(reqDTO.getBody()) .detail(reqDTO.getBody())
.totalFee(reqDTO.getAmount()) // 单位分 .totalFee(reqDTO.getPrice()) // 单位分
.productId(reqDTO.getMerchantOrderId()) .productId(reqDTO.getOutTradeNo())
.timeExpire(formatDateV2(reqDTO.getExpireTime())) .timeExpire(formatDateV2(reqDTO.getExpireTime()))
.spbillCreateIp(reqDTO.getUserIp()) .spbillCreateIp(reqDTO.getUserIp())
.notifyUrl(reqDTO.getNotifyUrl()) .notifyUrl(reqDTO.getNotifyUrl())
@ -50,31 +48,26 @@ public class WxNativePayClient extends AbstractWxPayClient {
WxPayNativeOrderResult response = client.createOrder(request); WxPayNativeOrderResult response = client.createOrder(request);
// 转换结果 // 转换结果
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.QR_CODE_URL.getMode(), return new PayOrderRespDTO(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(),
response.getCodeUrl()); reqDTO.getOutTradeNo(), response);
} }
@Override @Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象 // 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request()
request.setOutTradeNo(reqDTO.getMerchantOrderId()); .setOutTradeNo(reqDTO.getOutTradeNo())
request.setDescription(reqDTO.getBody()); .setDescription(reqDTO.getSubject())
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分 .setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())) // 单位分
request.setTimeExpire(formatDateV3(reqDTO.getExpireTime())); .setTimeExpire(formatDateV3(reqDTO.getExpireTime()))
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp())); .setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()))
request.setNotifyUrl(reqDTO.getNotifyUrl()); .setNotifyUrl(reqDTO.getNotifyUrl());
// 执行请求 // 执行请求
WxPayNativeOrderResult response = client.createOrderV3(TradeTypeEnum.NATIVE, request); String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
// 转换结果 // 转换结果
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.QR_CODE_URL.getMode(), return new PayOrderRespDTO(PayOrderDisplayModeEnum.QR_CODE.getMode(), response,
response.getCodeUrl()); reqDTO.getOutTradeNo(), response);
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
return null;
} }
} }

View File

@ -37,16 +37,17 @@ public class WxPayClientConfig implements PayClientConfig {
* *
* 只有公众号或小程序需要该字段 * 只有公众号或小程序需要该字段
*/ */
@NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
private String appId; private String appId;
/** /**
* 商户号 * 商户号
*/ */
@NotBlank(message = "商户号 不能为空", groups = {V2.class, V3.class}) @NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class})
private String mchId; private String mchId;
/** /**
* API 版本 * API 版本
*/ */
@NotBlank(message = "API 版本 不能为空", groups = {V2.class, V3.class}) @NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class})
private String apiVersion; private String apiVersion;
// ========== V2 版本的参数 ========== // ========== V2 版本的参数 ==========
@ -54,36 +55,31 @@ public class WxPayClientConfig implements PayClientConfig {
/** /**
* 商户密钥 * 商户密钥
*/ */
@NotBlank(message = "商户密钥 不能为空", groups = V2.class) @NotBlank(message = "商户密钥不能为空", groups = V2.class)
private String mchKey; private String mchKey;
/** /**
* apiclient_cert.p12 证书文件的绝对路径或者以 classpath: 开头的类路径. * apiclient_cert.p12 证书文件的对应字符串base64 格式
* 对应的字符串
* *
* 注意可通过 {@link #main(String[])} 读取 * 为什么采用 base64 格式因为 p12 读取后是二进制需要转换成 base64 格式才好传输和存储
*/ */
/// private String keyContent; @NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class)
private String keyContent;
// ========== V3 版本的参数 ========== // ========== V3 版本的参数 ==========
/** /**
* apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径. * apiclient_key.pem 证书文件的对应字符串
* 对应的字符串
* 注意可通过 {@link #main(String[])} 读取
*/ */
@NotBlank(message = "apiclient_key 不能为空", groups = V3.class) @NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
private String privateKeyContent; private String privateKeyContent;
/** /**
* apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径. * apiclient_cert.pem 证书文件的对应的字符串
* 对应的字符串
* <p>
* 注意可通过 {@link #main(String[])} 读取
*/ */
@NotBlank(message = "apiclient_cert 不能为空", groups = V3.class) @NotBlank(message = "apiclient_cert 不能为空", groups = V3.class)
private String privateCertContent; private String privateCertContent;
/** /**
* apiV3 密钥值 * apiV3 密钥值
*/ */
@NotBlank(message = "apiV3 密钥值 不能为空", groups = V3.class) @NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
private String apiV3Key; private String apiV3Key;
/** /**

View File

@ -2,11 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 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.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; 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.PayOrderDisplayModeEnum;
import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult; import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
@ -19,6 +16,7 @@ import com.github.binarywang.wxpay.exception.WxPayException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/** /**
* 微信支付公众号 PayClient 实现类 * 微信支付公众号 PayClient 实现类
@ -44,13 +42,13 @@ public class WxPubPayClient extends AbstractWxPayClient {
} }
@Override @Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象 // 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder() WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
.outTradeNo(reqDTO.getMerchantOrderId()) .outTradeNo(reqDTO.getOutTradeNo())
.body(reqDTO.getSubject()) .body(reqDTO.getSubject())
.detail(reqDTO.getBody()) .detail(reqDTO.getBody())
.totalFee(reqDTO.getAmount()) // 单位分 .totalFee(reqDTO.getPrice()) // 单位分
.timeExpire(formatDateV2(reqDTO.getExpireTime())) .timeExpire(formatDateV2(reqDTO.getExpireTime()))
.spbillCreateIp(reqDTO.getUserIp()) .spbillCreateIp(reqDTO.getUserIp())
.openid(getOpenid(reqDTO)) .openid(getOpenid(reqDTO))
@ -60,17 +58,17 @@ public class WxPubPayClient extends AbstractWxPayClient {
WxPayMpOrderResult response = client.createOrder(request); WxPayMpOrderResult response = client.createOrder(request);
// 转换结果 // 转换结果
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.CUSTOM.getMode(), return new PayOrderRespDTO(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
JsonUtils.toJsonString(response)); reqDTO.getOutTradeNo(), response);
} }
@Override @Override
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
// 构建 WxPayUnifiedOrderRequest 对象 // 构建 WxPayUnifiedOrderRequest 对象
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
request.setOutTradeNo(reqDTO.getMerchantOrderId()); request.setOutTradeNo(reqDTO.getOutTradeNo());
request.setDescription(reqDTO.getSubject()); request.setDescription(reqDTO.getSubject());
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分 request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分
request.setTimeExpire(formatDateV3(reqDTO.getExpireTime())); request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO))); request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp())); request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
@ -79,14 +77,8 @@ public class WxPubPayClient extends AbstractWxPayClient {
WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request); WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request);
// 转换结果 // 转换结果
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.CUSTOM.getMode(), return new PayOrderRespDTO(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
JsonUtils.toJsonString(response)); reqDTO.getOutTradeNo(), response);
}
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
// TODO 需要实现
throw new UnsupportedOperationException();
} }
// ========== 各种工具方法 ========== // ========== 各种工具方法 ==========

View File

@ -1,16 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.enums;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
/**
* 支付框架的错误码枚举
*
* 支付框架使用 2-002-000-000
*
* @author 芋道源码
*/
public interface PayFrameworkErrorCodeConstants {
ErrorCode ORDER_UNIFIED_ERROR = new ErrorCode(2002000000, "发起支付失败,原因:{}");
}

View File

@ -18,8 +18,7 @@ public enum PayOrderDisplayModeEnum {
QR_CODE("qr_code"), // 二维码的文字内容 QR_CODE("qr_code"), // 二维码的文字内容
QR_CODE_URL("qr_code_url"), // 二维码的图片链接 QR_CODE_URL("qr_code_url"), // 二维码的图片链接
BAR_CODE("bar_code"), // 条形码 BAR_CODE("bar_code"), // 条形码
APP("app"), // 应用目前暂时用不到 APP("app"), // 应用AndroidiOS微信小程序微信公众号等需要做自定义处理的
CUSTOM("custom"), // 自定义每种支付方式做个性化处理例如说微信公众号支付时调用 JSAPI 接口
; ;
/** /**

View File

@ -3,10 +3,12 @@ package cn.iocoder.yudao.framework.pay.core.enums.order;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import java.util.Objects;
/** /**
* 渠道的支付状态枚举 * 渠道的支付状态枚举
* *
* @author 遇到源码 * @author 芋道源码
*/ */
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
@ -20,4 +22,24 @@ public enum PayOrderStatusRespEnum {
private final Integer status; private final Integer status;
private final String name; private final String name;
/**
* 判断是否支付成功
*
* @param status 状态
* @return 是否支付成功
*/
public static boolean isSuccess(Integer status) {
return Objects.equals(status, SUCCESS.getStatus());
}
/**
* 判断是否支付关闭
*
* @param status 状态
* @return 是否支付关闭
*/
public static boolean isClosed(Integer status) {
return Objects.equals(status, CLOSED.getStatus());
}
} }

View File

@ -1,23 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.enums.refund;
// TODO 芋艿看看能不能去掉
/**
* 退款通知, 统一的渠道退款状态
*
* @author jason
*/
public enum PayNotifyRefundStatusEnum {
/**
* 支付宝 全额退款 trade_status=TRADE_CLOSED 部分退款 trade_status=TRADE_SUCCESS
* 退款成功
*/
SUCCESS,
/**
* 支付宝退款通知没有这个状态
* 退款异常
*/
ABNORMAL;
}

View File

@ -1,23 +0,0 @@
package cn.iocoder.yudao.framework.pay.core.enums.refund;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 渠道的退款状态枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum PayRefundRespEnum {
SUCCESS(1, "退款成功"),
FAILURE(2, "退款失败"),
PROCESSING(3,"退款处理中"),
CLOSED(4, "退款关闭");
private final Integer status;
private final String name;
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.framework.pay.core.enums.refund;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
/**
* 渠道的退款状态枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum PayRefundStatusRespEnum {
WAITING(0, "未退款"),
SUCCESS(10, "退款成功"),
FAILURE(20, "退款失败");
private final Integer status;
private final String name;
public static boolean isSuccess(Integer status) {
return Objects.equals(status, SUCCESS.getStatus());
}
}

View File

@ -121,10 +121,10 @@ public class PayClientFactoryImplIntegrationTest {
private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() { private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() {
PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO(); PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO();
reqDTO.setAmount(123); reqDTO.setPrice(123);
reqDTO.setSubject("IPhone 13"); reqDTO.setSubject("IPhone 13");
reqDTO.setBody("biubiubiu"); reqDTO.setBody("biubiubiu");
reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis())); reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
reqDTO.setUserIp("127.0.0.1"); reqDTO.setUserIp("127.0.0.1");
reqDTO.setNotifyUrl("http://127.0.0.1:8080"); reqDTO.setNotifyUrl("http://127.0.0.1:8080");
return reqDTO; return reqDTO;

View File

@ -1,8 +1,6 @@
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay; package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.ReflectUtil;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayApiException;
import com.alipay.api.DefaultAlipayClient; import com.alipay.api.DefaultAlipayClient;
@ -74,8 +72,8 @@ public class AlipayQrPayClientTest extends BaseMockitoUnitTest {
// 这里设置可以直接随机整个对象 // 这里设置可以直接随机整个对象
Long shopOrderId = System.currentTimeMillis(); Long shopOrderId = System.currentTimeMillis();
PayOrderUnifiedReqDTO reqDTO=new PayOrderUnifiedReqDTO(); PayOrderUnifiedReqDTO reqDTO=new PayOrderUnifiedReqDTO();
reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis())); reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
reqDTO.setAmount(1); reqDTO.setPrice(1);
reqDTO.setBody("内容:" + shopOrderId); reqDTO.setBody("内容:" + shopOrderId);
reqDTO.setSubject("标题:"+shopOrderId); reqDTO.setSubject("标题:"+shopOrderId);
String notify="http://niubi.natapp1.cc/api/pay/order/notify"; String notify="http://niubi.natapp1.cc/api/pay/order/notify";

View File

@ -0,0 +1,123 @@
package cn.iocoder.yudao.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.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,83 @@
package cn.iocoder.yudao.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.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

@ -351,7 +351,7 @@ public class TradeAfterSaleServiceImpl implements TradeAfterSaleService, AfterSa
public void afterCommit() { public void afterCommit() {
// 创建退款单 // 创建退款单
PayRefundCreateReqDTO createReqDTO = TradeAfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties); PayRefundCreateReqDTO createReqDTO = TradeAfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties);
Long payRefundId = payRefundApi.createPayRefund(createReqDTO); Long payRefundId = payRefundApi.createRefund(createReqDTO);
// 更新售后单的退款单号 // 更新售后单的退款单号
tradeAfterSaleMapper.updateById(new TradeAfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId)); tradeAfterSaleMapper.updateById(new TradeAfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId));
} }

View File

@ -18,7 +18,7 @@ public interface PayRefundApi {
* @param reqDTO 创建请求 * @param reqDTO 创建请求
* @return 退款单编号 * @return 退款单编号
*/ */
Long createPayRefund(@Valid PayRefundCreateReqDTO reqDTO); Long createRefund(@Valid PayRefundCreateReqDTO reqDTO);
/** /**
* 获得退款单 * 获得退款单
@ -26,6 +26,6 @@ public interface PayRefundApi {
* @param id 退款单编号 * @param id 退款单编号
* @return 退款单 * @return 退款单
*/ */
PayRefundRespDTO getPayRefund(Long id); PayRefundRespDTO getRefund(Long id);
} }

View File

@ -27,26 +27,32 @@ public class PayRefundCreateReqDTO {
private String userIp; private String userIp;
// ========== 商户相关字段 ========== // ========== 商户相关字段 ==========
/**
* 商户订单编号
*/
@NotEmpty(message = "商户订单编号不能为空")
private String merchantOrderId;
/**
* 商户退款编号
*/
@NotEmpty(message = "商户退款编号不能为空")
private String merchantRefundId;
/** /**
* 退款描述 * 退款描述
*/ */
@NotEmpty(message = "退款描述不能为空") @NotEmpty(message = "退款描述不能为空")
@Length(max = 128, message = "退款描述长度不能超过128") @Length(max = 128, message = "退款描述长度不能超过 128")
private String reason; private String reason;
// ========== 订单相关字段 ========== // ========== 订单相关字段 ==========
/**
* 支付单号
*/
@NotNull(message = "支付单号不能为空")
private Long payOrderId;
/** /**
* 退款金额单位 * 退款金额单位
*/ */
@NotNull(message = "退款金额不能为空") @NotNull(message = "退款金额不能为空")
@Min(value = 1, message = "退款金额必须大于零") @Min(value = 1, message = "退款金额必须大于零")
private Integer price; private Integer price;
} }

View File

@ -27,17 +27,19 @@ public interface ErrorCodeConstants {
ErrorCode PAY_ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付"); ErrorCode PAY_ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付");
ErrorCode PAY_ORDER_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007002002, "支付订单不处于已支付"); ErrorCode PAY_ORDER_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007002002, "支付订单不处于已支付");
ErrorCode PAY_ORDER_IS_EXPIRED = new ErrorCode(1007002003, "支付订单已经过期"); ErrorCode PAY_ORDER_IS_EXPIRED = new ErrorCode(1007002003, "支付订单已经过期");
ErrorCode PAY_ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1007002004, "发起支付报错,错误码:{},错误提示:{}");
// ========== ORDER 模块(拓展单) 1007003000 ========== // ========== ORDER 模块(拓展单) 1007003000 ==========
ErrorCode PAY_ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在"); ErrorCode PAY_ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在");
ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付"); ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付");
// ========== 支付模块(退款) 1007006000 ========== // ========== 支付模块(退款) 1007006000 ==========
ErrorCode PAY_PRICE_PRICE_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额"); ErrorCode PAY_REFUND_PRICE_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额");
ErrorCode PAY_REFUND_ALL_REFUNDED = new ErrorCode(1007006001, "订单已经全额退款"); ErrorCode PAY_REFUND_ALL_REFUNDED = new ErrorCode(1007006001, "订单已经全额退款");
ErrorCode PAY_REFUND_CHN_ORDER_NO_IS_NULL = new ErrorCode(1007006002, "该订单的渠道订单为空"); ErrorCode PAY_REFUND_HAS_REFUNDING = new ErrorCode(1007006002, "已经有退款在处理中");
ErrorCode PAY_REFUND_SUCCEED = new ErrorCode(1007006003, "已经退款成功"); ErrorCode PAY_REFUND_EXISTS = new ErrorCode(1007006003, "已经存在退款单");
ErrorCode PAY_REFUND_NOT_FOUND = new ErrorCode(1007006004, "支付退款单不存在"); ErrorCode PAY_REFUND_NOT_FOUND = new ErrorCode(1007006004, "支付退款单不存在");
ErrorCode PAY_REFUND_STATUS_IS_NOT_WAITING = new ErrorCode(1007006005, "支付退款单不处于待退款");
// ========== 示例订单 1007900000 ========== // ========== 示例订单 1007900000 ==========
ErrorCode PAY_DEMO_ORDER_NOT_FOUND = new ErrorCode(1007900000, "示例订单不存在"); ErrorCode PAY_DEMO_ORDER_NOT_FOUND = new ErrorCode(1007900000, "示例订单不存在");

View File

@ -38,4 +38,14 @@ public enum PayOrderStatusEnum implements IntArrayValuable {
return Objects.equals(status, SUCCESS.getStatus()); return Objects.equals(status, SUCCESS.getStatus());
} }
/**
* 判断是否支付关闭
*
* @param status 状态
* @return 是否支付关闭
*/
public static boolean isClosed(Integer status) {
return Objects.equals(status, CLOSED.getStatus());
}
} }

View File

@ -5,14 +5,18 @@ import lombok.Getter;
import java.util.Objects; import java.util.Objects;
/**
* 渠道的退款状态枚举
*
* @author 芋道源码
*/
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum PayRefundStatusEnum { public enum PayRefundStatusEnum {
CREATE(0, "退款订单生成"), WAITING(0, "未退款"),
SUCCESS(1, "退款成功"), SUCCESS(10, "退款成功"),
FAILURE(2, "退款失败"), FAILURE(20, "退款失败");
CLOSE(99, "退款关闭");
private final Integer status; private final Integer status;
private final String name; private final String name;

View File

@ -22,7 +22,7 @@ public class PayOrderApiImpl implements PayOrderApi {
@Override @Override
public Long createOrder(PayOrderCreateReqDTO reqDTO) { public Long createOrder(PayOrderCreateReqDTO reqDTO) {
return payOrderService.createPayOrder(reqDTO); return payOrderService.createOrder(reqDTO);
} }
@Override @Override

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.pay.api.refund;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO; import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@ -21,14 +22,13 @@ public class PayRefundApiImpl implements PayRefundApi {
private PayRefundService payRefundService; private PayRefundService payRefundService;
@Override @Override
public Long createPayRefund(PayRefundCreateReqDTO reqDTO) { public Long createRefund(PayRefundCreateReqDTO reqDTO) {
return payRefundService.createPayRefund(reqDTO); return payRefundService.createPayRefund(reqDTO);
} }
@Override @Override
public PayRefundRespDTO getPayRefund(Long id) { public PayRefundRespDTO getRefund(Long id) {
// TODO 芋艿暂未实现 return PayRefundConvert.INSTANCE.convert02(payRefundService.getRefund(id));
return null;
} }
} }

View File

@ -29,7 +29,7 @@ public class PayAppBaseVO {
@Schema(description = "支付结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/pay-callback") @Schema(description = "支付结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/pay-callback")
@NotNull(message = "支付结果的回调地址不能为空") @NotNull(message = "支付结果的回调地址不能为空")
@URL(message = "支付结果的回调地址必须为 URL 格式") @URL(message = "支付结果的回调地址必须为 URL 格式")
private String payNotifyUrl; private String orderNotifyUrl;
@Schema(description = "退款结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/refund-callback") @Schema(description = "退款结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/refund-callback")
@NotNull(message = "退款结果的回调地址不能为空") @NotNull(message = "退款结果的回调地址不能为空")

View File

@ -3,9 +3,8 @@ package cn.iocoder.yudao.module.pay.controller.admin.notify;
import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
import cn.iocoder.yudao.framework.pay.core.client.PayClient; 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.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
import cn.iocoder.yudao.module.pay.service.order.PayOrderService; import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@ -19,7 +18,6 @@ import javax.annotation.security.PermitAll;
import java.util.Map; import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND; import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND;
@Tag(name = "管理后台 - 支付通知") @Tag(name = "管理后台 - 支付通知")
@ -37,22 +35,14 @@ public class PayNotifyController {
@Resource @Resource
private PayClientFactory payClientFactory; private PayClientFactory payClientFactory;
/** @PostMapping(value = "/order/{channelId}")
* 统一的渠道支付回调支付宝的退款回调 @Operation(summary = "支付渠道的统一【支付】回调")
*
* @param channelId 渠道编号
* @param params form 参数
* @param body request body
* @return 成功返回 "success"
*/
@PostMapping(value = "/callback/{channelId}")
@Operation(summary = "支付渠道的统一回调接口 - 包括支付回调,退款回调")
@PermitAll @PermitAll
@OperateLog(enable = false) // 回调地址无需记录操作日志 @OperateLog(enable = false) // 回调地址无需记录操作日志
public String notifyCallback(@PathVariable("channelId") Long channelId, public String notifyOrder(@PathVariable("channelId") Long channelId,
@RequestParam(required = false) Map<String, String> params, @RequestParam(required = false) Map<String, String> params,
@RequestBody(required = false) String body) { @RequestBody(required = false) String body) {
log.info("[notifyCallback][channelId({}) 回调数据({}/{})]", channelId, params, body); log.info("[notifyOrder][channelId({}) 回调数据({}/{})]", channelId, params, body);
// 1. 校验支付渠道是否存在 // 1. 校验支付渠道是否存在
PayClient payClient = payClientFactory.getPayClient(channelId); PayClient payClient = payClientFactory.getPayClient(channelId);
if (payClient == null) { if (payClient == null) {
@ -61,21 +51,30 @@ public class PayNotifyController {
} }
// 2. 解析通知数据 // 2. 解析通知数据
PayNotifyReqDTO rawNotify = PayNotifyReqDTO.builder().params(params).body(body).build(); PayOrderRespDTO notify = payClient.parseOrderNotify(params, body);
Object notify = payClient.parseNotify(rawNotify); orderService.notifyOrder(channelId, notify);
return "success";
}
// 3. 处理通知 @PostMapping(value = "/refund/{channelId}")
// 3.1退款通知 @Operation(summary = "支付渠道的统一【退款】回调")
if (notify instanceof PayRefundNotifyRespDTO) { @PermitAll
refundService.notifyPayRefund(channelId, (PayRefundNotifyRespDTO) notify, rawNotify); @OperateLog(enable = false) // 回调地址无需记录操作日志
return "success"; public String notifyRefund(@PathVariable("channelId") Long channelId,
@RequestParam(required = false) Map<String, String> params,
@RequestBody(required = false) String body) {
log.info("[notifyRefund][channelId({}) 回调数据({}/{})]", channelId, params, body);
// 1. 校验支付渠道是否存在
PayClient payClient = payClientFactory.getPayClient(channelId);
if (payClient == null) {
log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId);
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
} }
// 3.2支付通知
if (notify instanceof PayOrderNotifyRespDTO) { // 2. 解析通知数据
orderService.notifyPayOrder(channelId, (PayOrderNotifyRespDTO) notify, rawNotify); PayRefundRespDTO notify = payClient.parseRefundNotify(params, body);
return "success"; refundService.notifyRefund(channelId, notify);
} return "success";
throw new UnsupportedOperationException("未知通知:" + toJsonString(notify));
} }
} }

View File

@ -86,7 +86,7 @@ public class PayOrderController {
@PostMapping("/submit") @PostMapping("/submit")
@Operation(summary = "提交支付订单") @Operation(summary = "提交支付订单")
public CommonResult<PayOrderSubmitRespVO> submitPayOrder(@RequestBody PayOrderSubmitReqVO reqVO) { public CommonResult<PayOrderSubmitRespVO> submitPayOrder(@RequestBody PayOrderSubmitReqVO reqVO) {
PayOrderSubmitRespVO respVO = payOrderService.submitPayOrder(reqVO, getClientIP()); PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP());
return success(respVO); return success(respVO);
} }

View File

@ -76,9 +76,6 @@ public class PayRefundExcelVO {
@ExcelProperty("退款成功时间") @ExcelProperty("退款成功时间")
private LocalDateTime successTime; private LocalDateTime successTime;
@ExcelProperty("退款通知时间")
private LocalDateTime notifyTime;
@ExcelProperty("退款失效时间") @ExcelProperty("退款失效时间")
private LocalDateTime expireTime; private LocalDateTime expireTime;

View File

@ -40,7 +40,7 @@ public class AppPayOrderController {
@PostMapping("/submit") @PostMapping("/submit")
@Operation(summary = "提交支付订单") @Operation(summary = "提交支付订单")
public CommonResult<AppPayOrderSubmitRespVO> submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) { public CommonResult<AppPayOrderSubmitRespVO> submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) {
PayOrderSubmitRespVO respVO = payOrderService.submitPayOrder(reqVO, getClientIP()); PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP());
return success(PayOrderConvert.INSTANCE.convert3(respVO)); return success(PayOrderConvert.INSTANCE.convert3(respVO));
} }

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.pay.convert.order;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; 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.api.order.dto.PayOrderRespDTO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*; import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*;
@ -93,7 +92,8 @@ public interface PayOrderConvert {
PayOrderUnifiedReqDTO convert2(PayOrderSubmitReqVO reqVO, String userIp); PayOrderUnifiedReqDTO convert2(PayOrderSubmitReqVO reqVO, String userIp);
PayOrderSubmitRespVO convert(PayOrderDO order, PayOrderUnifiedRespDTO unifiedRespDTO); @Mapping(source = "order.status", target = "status")
PayOrderSubmitRespVO convert(PayOrderDO order, cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO respDTO);
AppPayOrderSubmitRespVO convert3(PayOrderSubmitRespVO bean); AppPayOrderSubmitRespVO convert3(PayOrderSubmitRespVO bean);

View File

@ -1,12 +1,11 @@
package cn.iocoder.yudao.module.pay.convert.refund; package cn.iocoder.yudao.module.pay.convert.refund;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.*; import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.*;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
import java.math.BigDecimal; import java.math.BigDecimal;
@ -44,8 +43,6 @@ public interface PayRefundConvert {
PageResult<PayRefundRespVO> convertPage(PageResult<PayRefundDO> page); PageResult<PayRefundRespVO> convertPage(PageResult<PayRefundDO> page);
List<PayRefundExcelVO> convertList02(List<PayRefundDO> list);
/** /**
* 退款订单DO 导出excel VO * 退款订单DO 导出excel VO
* *
@ -60,20 +57,18 @@ public interface PayRefundConvert {
PayRefundExcelVO payRefundExcelVO = new PayRefundExcelVO(); PayRefundExcelVO payRefundExcelVO = new PayRefundExcelVO();
payRefundExcelVO.setId(bean.getId()); payRefundExcelVO.setId(bean.getId());
payRefundExcelVO.setTradeNo(bean.getTradeNo()); payRefundExcelVO.setTradeNo(bean.getNo());
payRefundExcelVO.setMerchantOrderId(bean.getMerchantOrderId()); payRefundExcelVO.setMerchantOrderId(bean.getMerchantOrderId());
payRefundExcelVO.setMerchantRefundNo(bean.getMerchantRefundNo()); // TODO 芋艿晚点在改
// payRefundExcelVO.setMerchantRefundNo(bean.getMerchantRefundNo());
payRefundExcelVO.setNotifyUrl(bean.getNotifyUrl()); payRefundExcelVO.setNotifyUrl(bean.getNotifyUrl());
payRefundExcelVO.setNotifyStatus(bean.getNotifyStatus()); payRefundExcelVO.setNotifyStatus(bean.getNotifyStatus());
payRefundExcelVO.setStatus(bean.getStatus()); payRefundExcelVO.setStatus(bean.getStatus());
payRefundExcelVO.setType(bean.getType());
payRefundExcelVO.setReason(bean.getReason()); payRefundExcelVO.setReason(bean.getReason());
payRefundExcelVO.setUserIp(bean.getUserIp()); payRefundExcelVO.setUserIp(bean.getUserIp());
payRefundExcelVO.setChannelOrderNo(bean.getChannelOrderNo()); payRefundExcelVO.setChannelOrderNo(bean.getChannelOrderNo());
payRefundExcelVO.setChannelRefundNo(bean.getChannelRefundNo()); payRefundExcelVO.setChannelRefundNo(bean.getChannelRefundNo());
payRefundExcelVO.setExpireTime(bean.getExpireTime());
payRefundExcelVO.setSuccessTime(bean.getSuccessTime()); payRefundExcelVO.setSuccessTime(bean.getSuccessTime());
payRefundExcelVO.setNotifyTime(bean.getNotifyTime());
payRefundExcelVO.setCreateTime(bean.getCreateTime()); payRefundExcelVO.setCreateTime(bean.getCreateTime());
BigDecimal multiple = new BigDecimal(100); BigDecimal multiple = new BigDecimal(100);
@ -85,12 +80,8 @@ public interface PayRefundConvert {
return payRefundExcelVO; return payRefundExcelVO;
} }
//TODO 太多需要处理了 暂时不用 PayRefundDO convert(PayRefundCreateReqDTO bean);
@Mappings(value = {
@Mapping(source = "price", target = "payPrice"), PayRefundRespDTO convert02(PayRefundDO bean);
@Mapping(source = "id", target = "orderId"),
@Mapping(target = "status",ignore = true)
})
PayRefundDO convert(PayOrderDO orderDO);
} }

View File

@ -48,7 +48,7 @@ public class PayAppDO extends BaseDO {
/** /**
* 支付结果的回调地址 * 支付结果的回调地址
*/ */
private String payNotifyUrl; private String orderNotifyUrl;
/** /**
* 退款结果的回调地址 * 退款结果的回调地址
*/ */

View File

@ -6,7 +6,7 @@ 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.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*; import lombok.*;
@ -55,7 +55,8 @@ public class PayOrderDO extends BaseDO {
/** /**
* 商户订单编号 * 商户订单编号
* 例如说内部系统 A 的订单号需要保证每个 PayMerchantDO 唯一 *
* 例如说内部系统 A 的订单号需要保证每个 PayAppDO 唯一
*/ */
private String merchantOrderId; private String merchantOrderId;
/** /**
@ -126,7 +127,7 @@ public class PayOrderDO extends BaseDO {
/** /**
* 退款状态 * 退款状态
* *
* 枚举 {@link PayRefundTypeEnum} * 枚举 {@link PayOrderRefundStatusEnum}
*/ */
private Integer refundStatus; private Integer refundStatus;
/** /**
@ -136,7 +137,7 @@ public class PayOrderDO extends BaseDO {
/** /**
* 退款总金额单位 * 退款总金额单位
*/ */
private Long refundPrice; private Integer refundPrice;
// ========== 渠道相关字段 ========== // ========== 渠道相关字段 ==========
/** /**

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.order; 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.dal.dataobject.channel.PayChannelDO; import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
@ -32,10 +33,11 @@ public class PayOrderExtensionDO extends BaseDO {
*/ */
private Long id; private Long id;
/** /**
* 支付订单号根据规则生成 * 外部订单号根据规则生成
* 调用支付渠道时使用该字段作为对接的订单号 *
* 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 使用该字段作为 out_trade_no * 调用支付渠道时使用该字段作为对接的订单号
* 2. 调用支付宝 https://opendocs.alipay.com/apis 使用该字段作为 out_trade_no * 1. 微信支付对应 <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml">JSAPI 支付</a> out_trade_no 字段
* 2. 支付宝支付对应 <a href="https://opendocs.alipay.com/open/270/105898">电脑网站支付</a> out_trade_no 字段
* *
* 例如说P202110132239124200055 * 例如说P202110132239124200055
*/ */
@ -64,20 +66,29 @@ public class PayOrderExtensionDO extends BaseDO {
* 支付状态 * 支付状态
* *
* 枚举 {@link PayOrderStatusEnum} * 枚举 {@link PayOrderStatusEnum}
* 注意只包含上述枚举的 WAITING SUCCESS
*/ */
private Integer status; private Integer status;
/** /**
* 支付渠道的额外参数 * 支付渠道的额外参数
* *
* 参见 https://www.pingxx.com/api/支付渠道%20extra%20参数说明.html * 参见 <a href="https://www.pingxx.com/api/支付渠道%20extra%20参数说明.html">参数说明</>
*/ */
@TableField(typeHandler = JacksonTypeHandler.class) @TableField(typeHandler = JacksonTypeHandler.class)
private Map<String, String> channelExtras; private Map<String, String> channelExtras;
/** /**
* 支付渠道异步通知的内容 * 调用渠道的错误码
*/
private String channelErrorCode;
/**
* 调用渠道报错时错误信息
*/
private String channelErrorMsg;
/**
* 支付渠道的同步/异步通知的内容
* *
* 在支持成功后会记录回调的数据 * 对应 {@link PayOrderRespDTO#getRawData()}
*/ */
private String channelNotifyData; private String channelNotifyData;

View File

@ -1,12 +1,13 @@
package cn.iocoder.yudao.module.pay.dal.dataobject.refund; package cn.iocoder.yudao.module.pay.dal.dataobject.refund;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 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.framework.pay.core.enums.channel.PayChannelEnum;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; 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.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
@ -37,6 +38,14 @@ public class PayRefundDO extends BaseDO {
*/ */
@TableId @TableId
private Long id; private Long id;
/**
* 外部退款号根据规则生成
*
* 调用支付渠道时使用该字段作为对接的退款号
* 1. 微信退款对应 <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_4">申请退款</a> out_refund_no 字段
* 2. 支付宝退款对应 <a href="https://opendocs.alipay.com/open/02e7go"统一收单交易退款接口></a> out_request_no 字段
*/
private String no;
/** /**
* 应用编号 * 应用编号
@ -63,47 +72,27 @@ public class PayRefundDO extends BaseDO {
*/ */
private Long orderId; private Long orderId;
/**
* 交易订单号根据规则生成
* 调用支付渠道时使用该字段作为对接的订单号
* 1. 调用微信支付 https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 使用该字段作为 out_trade_no
* 2. 调用支付宝 https://opendocs.alipay.com/apis 使用该字段作为 out_trade_no
* 这里对应 pay_extension 里面的 no
* 例如说P202110132239124200055
*/
private String tradeNo;
// ========== 商户相关字段 ========== // ========== 商户相关字段 ==========
/** /**
* 商户订单编号 * 商户订单编号
*
* 例如说内部系统 A 的订单号需要保证每个 PayAppDO 唯一
*/ */
private String merchantOrderId; private String merchantOrderId;
/** /**
* 商户退款订单号, 由商户系统产生 由他们保证唯一不能为空通知商户时会传该字段 * 商户退款订单号
* 例如说内部系统 A 的退款订单号需要保证每个 PayMerchantDO 唯一 *
* 个商户退款订单对应一条退款请求记录可多次提交 渠道保持幂等 * 例如说内部系统 A 的订单号需要保证每个 PayAppDO 唯一
* 使用商户退款单作为退款请求号
* https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml 中的 out_refund_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
* 退款请求号
* 标识一次退款请求需要保证在交易号下唯一如需部分退款则此参数必传
* 针对同一次退款请求如果调用接口失败或异常了重试时需要保证退款请求号不能变更
* 防止该笔交易重复退款支付宝会保证同样的退款请求号多次请求只会退一次
* 退款单请求号根据规则生成
* 例如说R202109181134287570000
*/ */
// TODO @jasonmerchantRefundNo =merchantRefundOId private String merchantRefundId;
private String merchantRefundNo;
/** /**
* 异步通知地址 * 异步通知地址
*/ */
private String notifyUrl; private String notifyUrl;
/** /**
* 通知商户退款结果的回调状态 * 通知商户退款结果的回调状态
* TODO 0 未发送 1 已发送 *
* 枚举 {@link PayNotifyStatusEnum}
*/ */
private Integer notifyStatus; private Integer notifyStatus;
@ -115,12 +104,6 @@ public class PayRefundDO extends BaseDO {
*/ */
private Integer status; private Integer status;
/**
* 退款类型(部分退款全部退款)
*
* 枚举 {@link PayRefundTypeEnum}
*/
private Integer type;
/** /**
* 支付金额单位 * 支付金额单位
*/ */
@ -142,45 +125,37 @@ public class PayRefundDO extends BaseDO {
// ========== 渠道相关字段 ========== // ========== 渠道相关字段 ==========
/** /**
* 渠道订单号pay_order 中的channel_order_no 对应 * 渠道订单号
*
* 冗余 {@link PayOrderDO#getChannelOrderNo()}
*/ */
private String channelOrderNo; private String channelOrderNo;
/** /**
* 微信中的 refund_id * 渠道退款单号
* https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml *
* 支付宝没有 * 1. 微信退款对应 <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_4">申请退款</a> refund_id 字段
* 渠道退款单号渠道返回 * 2. 支付宝退款没有字段
*/ */
private String channelRefundNo; private String channelRefundNo;
/**
* 退款成功时间
*/
private LocalDateTime successTime;
/** /**
* 调用渠道的错误码 * 调用渠道的错误码
*/ */
private String channelErrorCode; private String channelErrorCode;
/** /**
* 调用渠道报错时错误信息 * 调用渠道报错时错误信息
*/ */
private String channelErrorMsg; private String channelErrorMsg;
/** /**
* 支付渠道的额外参数 * 支付渠道的同步/异步通知的内容
* 参见 https://www.pingxx.com/api/Refunds%20退款概述.html *
* 对应 {@link PayRefundRespDTO#getRawData()}
*/ */
private String channelExtras; private String channelNotifyData;
/**
* TODO
* 退款失效时间
*/
private LocalDateTime expireTime;
/**
* 退款成功时间
*/
private LocalDateTime successTime;
/**
* 退款通知时间
*/
private LocalDateTime notifyTime;
} }

View File

@ -1,11 +1,13 @@
package cn.iocoder.yudao.module.pay.dal.mysql.refund; package cn.iocoder.yudao.module.pay.dal.mysql.refund;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.util.List; import java.util.List;
@ -13,6 +15,34 @@ import java.util.List;
@Mapper @Mapper
public interface PayRefundMapper extends BaseMapperX<PayRefundDO> { public interface PayRefundMapper extends BaseMapperX<PayRefundDO> {
default Long selectCountByAppId(Long appId) {
return selectCount(PayRefundDO::getAppId, appId);
}
default PayRefundDO selectByAppIdAndMerchantRefundId(Long appId, String merchantRefundId) {
return selectOne(new LambdaQueryWrapperX<PayRefundDO>()
.eq(PayRefundDO::getAppId, appId)
.eq(PayRefundDO::getMerchantRefundId, merchantRefundId));
}
default Long selectCountByAppIdAndOrderId(Long appId, Long orderId, Integer status) {
return selectCount(new LambdaQueryWrapperX<PayRefundDO>()
.eq(PayRefundDO::getAppId, appId)
.eq(PayRefundDO::getOrderId, orderId)
.eq(PayRefundDO::getStatus, status));
}
default PayRefundDO selectByAppIdAndNo(Long appId, String no) {
return selectOne(new LambdaQueryWrapperX<PayRefundDO>()
.eq(PayRefundDO::getAppId, appId)
.eq(PayRefundDO::getNo, no));
}
default int updateByIdAndStatus(Long id, Integer status, PayRefundDO update) {
return update(update, new LambdaQueryWrapper<PayRefundDO>()
.eq(PayRefundDO::getId, id).eq(PayRefundDO::getStatus, status));
}
default PageResult<PayRefundDO> selectPage(PayRefundPageReqVO reqVO) { default PageResult<PayRefundDO> selectPage(PayRefundPageReqVO reqVO) {
return selectPage(reqVO, new QueryWrapperX<PayRefundDO>() return selectPage(reqVO, new QueryWrapperX<PayRefundDO>()
.eqIfPresent("app_id", reqVO.getAppId()) .eqIfPresent("app_id", reqVO.getAppId())
@ -37,16 +67,4 @@ public interface PayRefundMapper extends BaseMapperX<PayRefundDO> {
.orderByDesc("id")); .orderByDesc("id"));
} }
default Long selectCountByApp(Long appId) {
return selectCount(PayRefundDO::getAppId, appId);
}
default PayRefundDO selectByReqNo(String reqNo) {
return selectOne("req_no", reqNo);
}
default PayRefundDO selectByTradeNoAndMerchantRefundNo(String tradeNo, String merchantRefundNo){
return selectOne("trade_no", tradeNo, "merchant_refund_no", merchantRefundNo);
}
} }

View File

@ -1,4 +1,4 @@
package cn.iocoder.yudao.module.pay.enums.refund; package cn.iocoder.yudao.module.pay.enums.order;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable; import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -11,10 +11,10 @@ import lombok.Getter;
*/ */
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
public enum PayRefundTypeEnum implements IntArrayValuable { public enum PayOrderRefundStatusEnum implements IntArrayValuable {
NO(0, "未退款"), NO(0, "未退款"),
SOME(10, "部分退款"), PART(10, "部分退款"),
ALL(20, "全部退款") ALL(20, "全部退款")
; ;

View File

@ -20,11 +20,11 @@ import javax.annotation.Resource;
public class PayNotifyJob implements JobHandler { public class PayNotifyJob implements JobHandler {
@Resource @Resource
private PayNotifyService payNotifyCoreService; private PayNotifyService payNotifyService;
@Override @Override
public String execute(String param) throws Exception { public String execute(String param) throws Exception {
int notifyCount = payNotifyCoreService.executeNotify(); int notifyCount = payNotifyService.executeNotify();
return String.format("执行支付通知 %s 个", notifyCount); return String.format("执行支付通知 %s 个", notifyCount);
} }

View File

@ -83,17 +83,20 @@ public class PayChannelServiceImpl implements PayChannelService {
*/ */
@Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS) @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
public void refreshLocalCache() { public void refreshLocalCache() {
// 情况一如果缓存里没有数据则直接刷新缓存 // 注意忽略自动多租户因为要全局初始化缓存
if (CollUtil.isEmpty(channelCache)) { TenantUtils.executeIgnore(() -> {
initLocalCache(); // 情况一如果缓存里没有数据则直接刷新缓存
return; if (CollUtil.isEmpty(channelCache)) {
} initLocalCache();
return;
}
// 情况二如果缓存里数据则通过 updateTime 判断是否有数据变更有变更则刷新缓存 // 情况二如果缓存里数据则通过 updateTime 判断是否有数据变更有变更则刷新缓存
LocalDateTime maxTime = CollectionUtils.getMaxValue(channelCache, PayChannelDO::getUpdateTime); LocalDateTime maxTime = CollectionUtils.getMaxValue(channelCache, PayChannelDO::getUpdateTime);
if (channelMapper.selectCountByUpdateTimeGt(maxTime) > 0) { if (channelMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
initLocalCache(); initLocalCache();
} }
});
} }
@Override @Override

View File

@ -184,12 +184,17 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
// 1. 校验订单是否可以退款 // 1. 校验订单是否可以退款
PayDemoOrderDO order = validateDemoOrderCanRefund(id); PayDemoOrderDO order = validateDemoOrderCanRefund(id);
// 2.1 创建退款单 // 2.1 生成退款单号
Long payRefundId = payRefundApi.createPayRefund(new PayRefundCreateReqDTO() // 一般来说用户发起退款的时候都会单独插入一个售后维权表然后使用该表的 id 作为 refundId
// 这里我们是个简单的 demo所以没有售后维权表直接使用订单 id + "-refund" 来演示
String refundId = order.getId() + "-refund";
// 2.2 创建退款单
Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO()
.setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用 .setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
.setPayOrderId(order.getPayOrderId()) // 支付单号 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
.setMerchantRefundId(refundId)
.setReason("想退钱").setPrice(order.getPrice()));// 价格信息 .setReason("想退钱").setPrice(order.getPrice()));// 价格信息
// 2.2 更新退款单到 demo 订单 // 2.3 更新退款单到 demo 订单
payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id) payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id)
.setPayRefundId(payRefundId).setRefundPrice(order.getPrice())); .setPayRefundId(payRefundId).setRefundPrice(order.getPrice()));
} }
@ -234,7 +239,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
} }
// 2.1 校验退款订单 // 2.1 校验退款订单
PayRefundRespDTO payRefund = payRefundApi.getPayRefund(payRefundId); PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId);
if (payRefund == null) { if (payRefund == null) {
throw exception(PAY_DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND); throw exception(PAY_DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND);
} }

View File

@ -7,6 +7,7 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
// TODO 芋艿合并到 PayOrder
/** /**
* 支付订单 Service 接口 * 支付订单 Service 接口
* *

View File

@ -2,8 +2,7 @@ package cn.iocoder.yudao.module.pay.service.order;
import cn.iocoder.yudao.framework.common.pojo.PageResult; 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.CollectionUtils;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; 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.PayOrderExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO; import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO;
@ -32,6 +31,15 @@ public interface PayOrderService {
*/ */
PayOrderDO getOrder(Long id); PayOrderDO getOrder(Long id);
/**
* 获得支付订单
*
* @param appId 应用编号
* @param merchantOrderId 商户订单编号
* @return 支付订单
*/
PayOrderDO getOrder(Long appId, String merchantOrderId);
/** /**
* 获得指定应用的订单数量 * 获得指定应用的订单数量
* *
@ -71,11 +79,11 @@ public interface PayOrderService {
/** /**
* 根据订单 ID 集合获取订单商品名称Map集合 * 根据订单 ID 集合获取订单商品名称Map集合
* *
* @param idList 订单 ID 集合 * @param ids 订单 ID 集合
* @return 订单商品 map 集合 * @return 订单商品 map 集合
*/ */
default Map<Long, PayOrderDO> getOrderSubjectMap(Collection<Long> idList) { default Map<Long, PayOrderDO> getOrderSubjectMap(Collection<Long> ids) {
List<PayOrderDO> list = getOrderSubjectList(idList); List<PayOrderDO> list = getOrderSubjectList(ids);
return CollectionUtils.convertMap(list, PayOrderDO::getId); return CollectionUtils.convertMap(list, PayOrderDO::getId);
} }
@ -85,7 +93,7 @@ public interface PayOrderService {
* @param reqDTO 创建请求 * @param reqDTO 创建请求
* @return 支付单编号 * @return 支付单编号
*/ */
Long createPayOrder(@Valid PayOrderCreateReqDTO reqDTO); Long createOrder(@Valid PayOrderCreateReqDTO reqDTO);
/** /**
* 提交支付 * 提交支付
@ -95,16 +103,23 @@ public interface PayOrderService {
* @param userIp 提交 IP * @param userIp 提交 IP
* @return 提交结果 * @return 提交结果
*/ */
PayOrderSubmitRespVO submitPayOrder(@Valid PayOrderSubmitReqVO reqVO, PayOrderSubmitRespVO submitOrder(@Valid PayOrderSubmitReqVO reqVO,
@NotEmpty(message = "提交 IP 不能为空") String userIp); @NotEmpty(message = "提交 IP 不能为空") String userIp);
/** /**
* 通知支付单成功 * 通知支付单成功
* *
* @param channelId 渠道编号 * @param channelId 渠道编号
* @param notify 通知 * @param notify 通知
* @param rawNotify 通知数据
*/ */
void notifyPayOrder(Long channelId, PayOrderNotifyRespDTO notify, PayNotifyReqDTO rawNotify); void notifyOrder(Long channelId, PayOrderRespDTO notify);
/**
* 更新支付订单的退款金额
*
* @param id 编号
* @param incrRefundPrice 增加的退款金额
*/
void updateOrderRefundPrice(Long id, Integer incrRefundPrice);
} }

View File

@ -4,15 +4,15 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Pair; import cn.hutool.core.lang.Pair;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.pay.config.PayProperties; import cn.iocoder.yudao.framework.pay.config.PayProperties;
import cn.iocoder.yudao.framework.pay.core.client.PayClient; 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.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO; import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; 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.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.PayOrderExportReqVO;
@ -26,11 +26,10 @@ 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.order.PayOrderExtensionDO;
import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderExtensionMapper; import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderExtensionMapper;
import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper; import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
import cn.iocoder.yudao.module.pay.service.app.PayAppService; import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@ -48,6 +47,7 @@ import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
/** /**
* 支付订单 Service 实现类 * 支付订单 Service 实现类
@ -82,6 +82,11 @@ public class PayOrderServiceImpl implements PayOrderService {
return orderMapper.selectById(id); return orderMapper.selectById(id);
} }
@Override
public PayOrderDO getOrder(Long appId, String merchantOrderId) {
return orderMapper.selectByAppIdAndMerchantOrderId(appId, merchantOrderId);
}
@Override @Override
public Long getOrderCountByAppId(Long appId) { public Long getOrderCountByAppId(Long appId) {
return orderMapper.selectCountByAppId(appId); return orderMapper.selectCountByAppId(appId);
@ -104,7 +109,7 @@ public class PayOrderServiceImpl implements PayOrderService {
} }
@Override @Override
public Long createPayOrder(PayOrderCreateReqDTO reqDTO) { public Long createOrder(PayOrderCreateReqDTO reqDTO) {
// 校验 App // 校验 App
PayAppDO app = appService.validPayApp(reqDTO.getAppId()); PayAppDO app = appService.validPayApp(reqDTO.getAppId());
@ -112,7 +117,7 @@ public class PayOrderServiceImpl implements PayOrderService {
PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId( PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId(
reqDTO.getAppId(), reqDTO.getMerchantOrderId()); reqDTO.getAppId(), reqDTO.getMerchantOrderId());
if (order != null) { if (order != null) {
log.warn("[createPayOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(), log.warn("[createOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
order.getMerchantOrderId(), toJsonString(order)); // 理论来说不会出现这个情况 order.getMerchantOrderId(), toJsonString(order)); // 理论来说不会出现这个情况
return order.getId(); return order.getId();
} }
@ -120,20 +125,19 @@ public class PayOrderServiceImpl implements PayOrderService {
// 创建支付交易单 // 创建支付交易单
order = PayOrderConvert.INSTANCE.convert(reqDTO).setAppId(app.getId()) order = PayOrderConvert.INSTANCE.convert(reqDTO).setAppId(app.getId())
// 商户相关字段 // 商户相关字段
.setNotifyUrl(app.getPayNotifyUrl()).setNotifyStatus(PayOrderNotifyStatusEnum.NO.getStatus()) .setNotifyUrl(app.getOrderNotifyUrl()).setNotifyStatus(PayOrderNotifyStatusEnum.NO.getStatus())
// 订单相关字段 // 订单相关字段
.setStatus(PayOrderStatusEnum.WAITING.getStatus()) .setStatus(PayOrderStatusEnum.WAITING.getStatus())
// 退款相关字段 // 退款相关字段
.setRefundStatus(PayRefundTypeEnum.NO.getStatus()).setRefundTimes(0).setRefundPrice(0L); .setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus()).setRefundTimes(0).setRefundPrice(0);
orderMapper.insert(order); orderMapper.insert(order);
return order.getId(); return order.getId();
} }
@Override @Override // 注意这里不能添加事务注解避免调用支付渠道失败时 PayOrderExtensionDO 回滚了
@Transactional(rollbackFor = Exception.class) public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) {
public PayOrderSubmitRespVO submitPayOrder(PayOrderSubmitReqVO reqVO, String userIp) {
// 1. 获得 PayOrderDO 并校验其是否存在 // 1. 获得 PayOrderDO 并校验其是否存在
PayOrderDO order = validatePayOrderCanSubmit(reqVO.getId()); PayOrderDO order = validateOrderCanSubmit(reqVO.getId());
// 1.2 校验支付渠道是否有效 // 1.2 校验支付渠道是否有效
PayChannelDO channel = validatePayChannelCanSubmit(order.getAppId(), reqVO.getChannelCode()); PayChannelDO channel = validatePayChannelCanSubmit(order.getAppId(), reqVO.getChannelCode());
PayClient client = payClientFactory.getPayClient(channel.getId()); PayClient client = payClientFactory.getPayClient(channel.getId());
@ -148,35 +152,38 @@ public class PayOrderServiceImpl implements PayOrderService {
// 3. 调用三方接口 // 3. 调用三方接口
PayOrderUnifiedReqDTO unifiedOrderReqDTO = PayOrderConvert.INSTANCE.convert2(reqVO, userIp) PayOrderUnifiedReqDTO unifiedOrderReqDTO = PayOrderConvert.INSTANCE.convert2(reqVO, userIp)
// 商户相关的字段 // 商户相关的字段
.setMerchantOrderId(orderExtension.getNo()) // 注意此处使用的是 PayOrderExtensionDO.no 属性 .setOutTradeNo(orderExtension.getNo()) // 注意此处使用的是 PayOrderExtensionDO.no 属性
.setSubject(order.getSubject()).setBody(order.getBody()) .setSubject(order.getSubject()).setBody(order.getBody())
.setNotifyUrl(genChannelPayNotifyUrl(channel)) .setNotifyUrl(genChannelOrderNotifyUrl(channel))
.setReturnUrl(reqVO.getReturnUrl()) .setReturnUrl(reqVO.getReturnUrl())
// 订单相关字段 // 订单相关字段
.setAmount(order.getPrice()).setExpireTime(order.getExpireTime()); .setPrice(order.getPrice()).setExpireTime(order.getExpireTime());
PayOrderUnifiedRespDTO unifiedOrderRespDTO = client.unifiedOrder(unifiedOrderReqDTO); PayOrderRespDTO unifiedOrderResp = client.unifiedOrder(unifiedOrderReqDTO);
// 4. 如果调用直接支付成功则直接更新支付单状态为成功例如说付款码支付免密支付时就直接验证支付成功 // 4. 如果调用直接支付成功则直接更新支付单状态为成功例如说付款码支付免密支付时就直接验证支付成功
if (unifiedOrderRespDTO.getNotify() != null) { if (unifiedOrderResp != null) {
notifyPayOrderSuccess(channel, unifiedOrderRespDTO.getNotify(), null); notifyPayOrder(channel, unifiedOrderResp);
// 如有渠道错误码则抛出业务异常提示用户
if (StrUtil.isNotEmpty(unifiedOrderResp.getChannelErrorCode())) {
throw exception(PAY_ORDER_SUBMIT_CHANNEL_ERROR, unifiedOrderResp.getChannelErrorCode(),
unifiedOrderResp.getChannelErrorMsg());
}
// 此处需要读取最新的状态 // 此处需要读取最新的状态
order = orderMapper.selectById(order.getId()); order = orderMapper.selectById(order.getId());
} }
return PayOrderConvert.INSTANCE.convert(order, unifiedOrderResp);
// 返回成功
return PayOrderConvert.INSTANCE.convert(order, unifiedOrderRespDTO);
} }
private PayOrderDO validatePayOrderCanSubmit(Long id) { private PayOrderDO validateOrderCanSubmit(Long id) {
PayOrderDO order = orderMapper.selectById(id); PayOrderDO order = orderMapper.selectById(id);
if (order == null) { // 是否存在 if (order == null) { // 是否存在
throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND); throw exception(PAY_ORDER_NOT_FOUND);
} }
if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态必须是待支付 if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态必须是待支付
throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING); throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
} }
if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期 if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期
throw exception(ErrorCodeConstants.PAY_ORDER_IS_EXPIRED); throw exception(PAY_ORDER_IS_EXPIRED);
} }
return order; return order;
} }
@ -184,14 +191,12 @@ public class PayOrderServiceImpl implements PayOrderService {
private PayChannelDO validatePayChannelCanSubmit(Long appId, String channelCode) { private PayChannelDO validatePayChannelCanSubmit(Long appId, String channelCode) {
// 校验 App // 校验 App
appService.validPayApp(appId); appService.validPayApp(appId);
// 校验支付渠道是否有效 // 校验支付渠道是否有效
PayChannelDO channel = channelService.validPayChannel(appId, channelCode); PayChannelDO channel = channelService.validPayChannel(appId, channelCode);
// 校验支付客户端是否正确初始化
PayClient client = payClientFactory.getPayClient(channel.getId()); PayClient client = payClientFactory.getPayClient(channel.getId());
if (client == null) { if (client == null) {
log.error("[validatePayChannelCanSubmit][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); log.error("[validatePayChannelCanSubmit][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND); throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
} }
return channel; return channel;
} }
@ -202,8 +207,8 @@ public class PayOrderServiceImpl implements PayOrderService {
* @param channel 支付渠道 * @param channel 支付渠道
* @return 支付渠道的回调地址 配置地址 + "/" + channel id * @return 支付渠道的回调地址 配置地址 + "/" + channel id
*/ */
private String genChannelPayNotifyUrl(PayChannelDO channel) { private String genChannelOrderNotifyUrl(PayChannelDO channel) {
return payProperties.getCallbackUrl() + "/" + channel.getId(); return payProperties.getOrderNotifyUrl() + "/" + channel.getId();
} }
private String generateOrderExtensionNo() { private String generateOrderExtensionNo() {
@ -226,18 +231,57 @@ public class PayOrderServiceImpl implements PayOrderService {
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void notifyPayOrder(Long channelId, PayOrderNotifyRespDTO notify, PayNotifyReqDTO rawNotify) { public void notifyOrder(Long channelId, PayOrderRespDTO notify) {
// 校验支付渠道是否有效 // 校验支付渠道是否有效
PayChannelDO channel = channelService.validPayChannel(channelId); PayChannelDO channel = channelService.validPayChannel(channelId);
// 更新支付订单为已支付 // 更新支付订单为已支付
TenantUtils.execute(channel.getTenantId(), () -> notifyPayOrderSuccess(channel, notify, rawNotify)); TenantUtils.execute(channel.getTenantId(), () -> notifyPayOrder(channel, notify));
} }
private void notifyPayOrderSuccess(PayChannelDO channel, PayOrderNotifyRespDTO notify, PayNotifyReqDTO rawNotify) { @Override
public void updateOrderRefundPrice(Long id, Integer incrRefundPrice) {
PayOrderDO order = orderMapper.selectById(id);
if (order == null) {
throw exception(PAY_ORDER_NOT_FOUND);
}
if (!PayOrderStatusEnum.isSuccess(order.getStatus())) {
throw exception(PAY_REFUND_PRICE_EXCEED);
}
if (order.getRefundPrice() + incrRefundPrice > order.getPrice()) {
throw exception(PAY_REFUND_PRICE_EXCEED);
}
// 更新订单
PayOrderDO updateObj = new PayOrderDO()
.setRefundPrice(order.getRefundPrice() + incrRefundPrice)
.setRefundTimes(order.getRefundTimes() + 1);
if (Objects.equals(updateObj.getRefundPrice(), order.getPrice())) {
updateObj.setStatus(PayOrderStatusEnum.CLOSED.getStatus())
.setRefundStatus(PayOrderRefundStatusEnum.ALL.getStatus());
} else {
updateObj.setStatus(PayOrderStatusEnum.CLOSED.getStatus())
.setRefundStatus(PayOrderRefundStatusEnum.PART.getStatus());
}
orderMapper.updateByIdAndStatus(id, PayOrderStatusEnum.SUCCESS.getStatus(), updateObj);
}
private void notifyPayOrder(PayChannelDO channel, PayOrderRespDTO notify) {
// 情况一支付成功的回调
if (PayOrderStatusRespEnum.isSuccess(notify.getStatus())) {
notifyOrderSuccess(channel, notify);
return;
}
// 情况二支付失败的回调
if (PayOrderStatusRespEnum.isClosed(notify.getStatus())) {
notifyOrderClosed(channel, notify);
}
}
private void notifyOrderSuccess(PayChannelDO channel, PayOrderRespDTO notify) {
// 1. 更新 PayOrderExtensionDO 支付成功 // 1. 更新 PayOrderExtensionDO 支付成功
PayOrderExtensionDO orderExtension = updatePayOrderExtensionSuccess(notify.getOrderExtensionNo(), rawNotify); PayOrderExtensionDO orderExtension = updateOrderExtensionSuccess(notify);
// 2. 更新 PayOrderDO 支付成功 // 2. 更新 PayOrderDO 支付成功
Pair<Boolean, PayOrderDO> order = updatePayOrderSuccess(channel, orderExtension, notify); Pair<Boolean, PayOrderDO> order = updateOrderExtensionSuccess(channel, orderExtension, notify);
if (order.getKey()) { // 如果之前已经成功回调则直接返回不用重复记录支付通知记录例如说支付平台重复回调 if (order.getKey()) { // 如果之前已经成功回调则直接返回不用重复记录支付通知记录例如说支付平台重复回调
return; return;
} }
@ -250,33 +294,30 @@ public class PayOrderServiceImpl implements PayOrderService {
/** /**
* 更新 PayOrderExtensionDO 支付成功 * 更新 PayOrderExtensionDO 支付成功
* *
* @param no 支付订单号支付模块 * @param notify 通知
* @param rawNotify 通知数据
* @return PayOrderExtensionDO 对象 * @return PayOrderExtensionDO 对象
*/ */
private PayOrderExtensionDO updatePayOrderExtensionSuccess(String no, PayNotifyReqDTO rawNotify) { private PayOrderExtensionDO updateOrderExtensionSuccess(PayOrderRespDTO notify) {
// 1. 查询 PayOrderExtensionDO // 1. 查询 PayOrderExtensionDO
PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(no); PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo());
if (orderExtension == null) { if (orderExtension == null) {
throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_NOT_FOUND); throw exception(PAY_ORDER_EXTENSION_NOT_FOUND);
} }
if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功直接返回不用重复更新 if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功直接返回不用重复更新
log.info("[updatePayOrderSuccess][支付拓展单({}) 已经是已支付,无需更新为已支付]", orderExtension.getId()); log.info("[updateOrderExtensionSuccess][支付拓展单({}) 已经是已支付,无需更新]", orderExtension.getId());
return orderExtension; return orderExtension;
} }
if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态必须是待支付 if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态必须是待支付
throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING); throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
} }
// 2. 更新 PayOrderExtensionDO // 2. 更新 PayOrderExtensionDO
int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(),
PayOrderStatusEnum.WAITING.getStatus(), PayOrderExtensionDO.builder().id(orderExtension.getId()) PayOrderExtensionDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(toJsonString(notify)).build());
.status(PayOrderStatusEnum.SUCCESS.getStatus())
.channelNotifyData(toJsonString(rawNotify)).build());
if (updateCounts == 0) { // 校验状态必须是待支付 if (updateCounts == 0) { // 校验状态必须是待支付
throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING); throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
} }
log.info("[updatePayOrderSuccess][支付拓展单({}) 更新为已支付]", orderExtension.getId()); log.info("[updateOrderExtensionSuccess][支付拓展单({}) 更新为已支付]", orderExtension.getId());
return orderExtension; return orderExtension;
} }
@ -289,20 +330,20 @@ public class PayOrderServiceImpl implements PayOrderService {
* @return key是否之前已经成功回调 * @return key是否之前已经成功回调
* valuePayOrderDO 对象 * valuePayOrderDO 对象
*/ */
private Pair<Boolean, PayOrderDO> updatePayOrderSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension, private Pair<Boolean, PayOrderDO> updateOrderExtensionSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension,
PayOrderNotifyRespDTO notify) { PayOrderRespDTO notify) {
// 1. 判断 PayOrderDO 是否处于待支付 // 1. 判断 PayOrderDO 是否处于待支付
PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId()); PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId());
if (order == null) { if (order == null) {
throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND); throw exception(PAY_ORDER_NOT_FOUND);
} }
if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功直接返回不用重复更新 if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功直接返回不用重复更新
&& Objects.equals(order.getSuccessExtensionId(), orderExtension.getId())) { && Objects.equals(order.getSuccessExtensionId(), orderExtension.getId())) {
log.info("[updatePayOrderSuccess][支付订单({}) 已经是已支付,无需更新为已支付]", order.getId()); log.info("[updateOrderExtensionSuccess][支付订单({}) 已经是已支付,无需更新]", order.getId());
return Pair.of(true, order); return Pair.of(true, order);
} }
if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态必须是待支付 if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态必须是待支付
throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING); throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
} }
// 2. 更新 PayOrderDO // 2. 更新 PayOrderDO
@ -313,10 +354,43 @@ public class PayOrderServiceImpl implements PayOrderService {
.channelOrderNo(notify.getChannelOrderNo()).channelUserId(notify.getChannelUserId()) .channelOrderNo(notify.getChannelOrderNo()).channelUserId(notify.getChannelUserId())
.notifyTime(LocalDateTime.now()).build()); .notifyTime(LocalDateTime.now()).build());
if (updateCounts == 0) { // 校验状态必须是待支付 if (updateCounts == 0) { // 校验状态必须是待支付
throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING); throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
} }
log.info("[updatePayOrderSuccess][支付订单({}) 更新为已支付]", order.getId()); log.info("[updateOrderExtensionSuccess][支付订单({}) 更新为已支付]", order.getId());
return Pair.of(false, order); return Pair.of(false, order);
} }
private void notifyOrderClosed(PayChannelDO channel, PayOrderRespDTO notify) {
updateOrderExtensionClosed(channel, notify);
}
private void updateOrderExtensionClosed(PayChannelDO channel, PayOrderRespDTO notify) {
// 1. 查询 PayOrderExtensionDO
PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo());
if (orderExtension == null) {
throw exception(PAY_ORDER_EXTENSION_NOT_FOUND);
}
if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { // 如果已经是关闭直接返回不用重复更新
log.info("[updateOrderExtensionClosed][支付拓展单({}) 已经是支付关闭,无需更新]", orderExtension.getId());
return;
}
// 一般出现先是支付成功然后支付关闭都是全部退款导致关闭的场景这个情况我们不更新支付拓展单只通过退款流程更新支付单
if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) {
log.info("[updateOrderExtensionClosed][支付拓展单({}) 是已支付,无需更新为支付关闭]", orderExtension.getId());
return;
}
if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态必须是待支付
throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
// 2. 更新 PayOrderExtensionDO
int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(),
PayOrderExtensionDO.builder().status(PayOrderStatusEnum.CLOSED.getStatus()).channelNotifyData(toJsonString(notify))
.channelErrorCode(notify.getChannelErrorCode()).channelErrorMsg(notify.getChannelErrorMsg()).build());
if (updateCounts == 0) { // 校验状态必须是待支付
throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
}
log.info("[updateOrderExtensionClosed][支付拓展单({}) 更新为支付关闭]", orderExtension.getId());
}
} }

View File

@ -1,11 +1,10 @@
package cn.iocoder.yudao.module.pay.service.refund; package cn.iocoder.yudao.module.pay.service.refund;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO; 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.PayRefundExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import java.util.List; import java.util.List;
@ -62,8 +61,7 @@ public interface PayRefundService {
* *
* @param channelId 渠道编号 * @param channelId 渠道编号
* @param notify 通知 * @param notify 通知
* @param rawNotify 通知数据
*/ */
void notifyPayRefund(Long channelId, PayRefundNotifyRespDTO notify, PayNotifyReqDTO rawNotify); void notifyRefund(Long channelId, PayRefundRespDTO notify);
} }

View File

@ -1,32 +1,31 @@
package cn.iocoder.yudao.module.pay.service.refund; package cn.iocoder.yudao.module.pay.service.refund;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.pay.config.PayProperties; import cn.iocoder.yudao.framework.pay.config.PayProperties;
import cn.iocoder.yudao.framework.pay.core.client.PayClient; 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.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayNotifyRefundStatusEnum; import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
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.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.PayRefundExportReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; 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.channel.PayChannelDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; 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.order.PayOrderExtensionDO;
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO; import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper; import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants; import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum; import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
import cn.iocoder.yudao.module.pay.service.app.PayAppService; import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@ -41,12 +40,14 @@ import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
/** /**
* 退款订单 Service 实现类 * 退款订单 Service 实现类
* *
* @author aquan * @author jason
*/ */
@Service @Service
@Slf4j @Slf4j
@ -61,8 +62,6 @@ public class PayRefundServiceImpl implements PayRefundService {
@Resource @Resource
private PayRefundMapper refundMapper; private PayRefundMapper refundMapper;
@Resource
private PayOrderMapper orderMapper; // TODO @jason需要改成不直接操作 db
@Resource @Resource
private PayOrderService orderService; private PayOrderService orderService;
@ -82,7 +81,7 @@ public class PayRefundServiceImpl implements PayRefundService {
@Override @Override
public Long getRefundCountByAppId(Long appId) { public Long getRefundCountByAppId(Long appId) {
return refundMapper.selectCountByApp(appId); return refundMapper.selectCountByAppId(appId);
} }
@Override @Override
@ -98,85 +97,83 @@ public class PayRefundServiceImpl implements PayRefundService {
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Long createPayRefund(PayRefundCreateReqDTO reqDTO) { public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
// 获得 PayOrderDO // 1.1 校验 App
PayOrderDO order = orderService.getOrder(reqDTO.getPayOrderId()); PayAppDO app = appService.validPayApp(reqDTO.getAppId());
// 校验订单是否存在 // 1.2 校验支付订单
if (Objects.isNull(order) ) { PayOrderDO order = validatePayOrderCanRefund(reqDTO);
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND); // 1.3 校验支付渠道是否有效
}
// 校验 App
PayAppDO app = appService.validPayApp(order.getAppId());
// 校验支付渠道是否有效
PayChannelDO channel = channelService.validPayChannel(order.getChannelId()); PayChannelDO channel = channelService.validPayChannel(order.getChannelId());
// 校验支付客户端是否正确初始化
PayClient client = payClientFactory.getPayClient(channel.getId()); PayClient client = payClientFactory.getPayClient(channel.getId());
if (client == null) { if (client == null) {
log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND); throw exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
}
// 1.4 校验退款订单是否已经存在
PayRefundDO refund = refundMapper.selectByAppIdAndMerchantRefundId(
app.getId(), reqDTO.getMerchantRefundId());
if (refund != null) {
throw exception(ErrorCodeConstants.PAY_REFUND_EXISTS);
} }
// TODO 芋艿待实现 // 2.1 插入退款单
String merchantRefundId = RandomUtil.randomNumbers(16); refund = PayRefundConvert.INSTANCE.convert(reqDTO)
.setNo(generateRefundNo()).setOrderId(order.getId())
// 校验退款的条件 .setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode())
validatePayRefund(reqDTO, order); // 商户相关的字段
// 退款类型 .setNotifyUrl(app.getRefundNotifyUrl()).setNotifyStatus(PayNotifyStatusEnum.WAITING.getStatus())
PayRefundTypeEnum refundType = PayRefundTypeEnum.SOME; // 渠道相关字段
if (Objects.equals(reqDTO.getPrice(), order.getPrice())) {
refundType = PayRefundTypeEnum.ALL;
}
PayOrderExtensionDO orderExtensionDO = orderExtensionService.getOrderExtension(order.getSuccessExtensionId());
PayRefundDO payRefundDO = refundMapper.selectByTradeNoAndMerchantRefundNo(orderExtensionDO.getNo(),
merchantRefundId); // TODO 芋艿需要优化
if(Objects.nonNull(payRefundDO)){
// 退款订单已经提交过
//TODO 校验相同退款单的金额
// TODO @jason咱要不封装一个 ObjectUtils.equalsAny
if (Objects.equals(PayRefundStatusEnum.SUCCESS.getStatus(), payRefundDO.getStatus())
|| Objects.equals(PayRefundStatusEnum.CLOSE.getStatus(), payRefundDO.getStatus())) {
//已成功退款
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_SUCCEED);
}
//可以重复提交保证 退款请求号 一致由渠道保证幂等
} else {
// 成功插入退款单 状态为生成.没有和渠道交互
// TODO @jason搞到 convert 一些额外的自动手动 set
payRefundDO = PayRefundDO.builder()
.appId(order.getAppId())
.channelOrderNo(order.getChannelOrderNo())
.channelCode(order.getChannelCode())
.channelId(order.getChannelId())
.orderId(order.getId())
.merchantRefundNo(merchantRefundId) // TODO 芋艿需要优化
.notifyUrl(app.getRefundNotifyUrl())
.payPrice(order.getPrice())
.refundPrice(reqDTO.getPrice())
.userIp(reqDTO.getUserIp())
.merchantOrderId(order.getMerchantOrderId())
.tradeNo(orderExtensionDO.getNo())
.status(PayRefundStatusEnum.CREATE.getStatus())
.reason(reqDTO.getReason())
.notifyStatus(PayOrderNotifyStatusEnum.NO.getStatus())
.type(refundType.getStatus())
.build();
refundMapper.insert(payRefundDO);
}
// TODO @jason搞到 convert 一些额外的自动手动 set
PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO();
unifiedReqDTO.setUserIp(reqDTO.getUserIp())
.setAmount(reqDTO.getPrice())
.setChannelOrderNo(order.getChannelOrderNo()) .setChannelOrderNo(order.getChannelOrderNo())
.setPayTradeNo(orderExtensionDO.getNo()) // 退款相关字段
.setMerchantRefundId(merchantRefundId) // TODO 芋艿需要优化 .setStatus(PayRefundStatusEnum.WAITING.getStatus())
.setNotifyUrl(genChannelPayNotifyUrl(channel)) // TODO 芋艿优化下 notifyUrl .setPayPrice(order.getPrice()).setRefundPrice(reqDTO.getPrice());
refundMapper.insert(refund);
// 2.2 向渠道发起退款申请
PayOrderExtensionDO orderExtension = orderExtensionService.getOrderExtension(order.getSuccessExtensionId());
PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO()
.setPayPrice(order.getPrice())
.setRefundPrice(reqDTO.getPrice())
.setOutTradeNo(orderExtension.getNo())
.setOutRefundNo(refund.getNo())
.setNotifyUrl(genChannelRefundNotifyUrl(channel))
.setReason(reqDTO.getReason()); .setReason(reqDTO.getReason());
// 向渠道发起退款申请 PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); // TODO 增加一个 channelErrorCodechannelErrorMsg 字段
client.unifiedRefund(unifiedReqDTO); // 2.3 处理退款返回
// 检查是否失败失败抛出业务异常 notifyRefund(channel, refundRespDTO);
// TODO 渠道的异常记录
// TODO @jason可以先打个 warn log
// 成功在 退款回调中处理 // 成功在 退款回调中处理
return payRefundDO.getId(); return refund.getId();
}
/**
* 校验支付订单是否可以退款
*
* @param reqDTO 退款申请信息
* @return 支付订单
*/
private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO) {
PayOrderDO order = orderService.getOrder(reqDTO.getAppId(), reqDTO.getMerchantOrderId());
if (order == null) {
throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
}
// 校验状态必须是支付状态
if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_SUCCESS);
}
// 是否已经全额退款
if (PayOrderRefundStatusEnum.ALL.getStatus().equals(order.getRefundStatus())) {
throw exception(ErrorCodeConstants.PAY_REFUND_ALL_REFUNDED);
}
// 校验金额 退款金额不能大于原定的金额
if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){
throw exception(ErrorCodeConstants.PAY_REFUND_PRICE_EXCEED);
}
// 是否有退款中的订单
if (refundMapper.selectCountByAppIdAndOrderId(reqDTO.getAppId(), order.getId(),
PayRefundStatusEnum.WAITING.getStatus()) > 0) {
throw exception(ErrorCodeConstants.PAY_REFUND_HAS_REFUNDING);
}
return order;
} }
/** /**
@ -185,91 +182,84 @@ public class PayRefundServiceImpl implements PayRefundService {
* @param channel 支付渠道 * @param channel 支付渠道
* @return 支付渠道的回调地址 配置地址 + "/" + channel id * @return 支付渠道的回调地址 配置地址 + "/" + channel id
*/ */
private String genChannelPayNotifyUrl(PayChannelDO channel) { private String genChannelRefundNotifyUrl(PayChannelDO channel) {
return payProperties.getCallbackUrl() + "/" + channel.getId(); return payProperties.getRefundNotifyUrl() + "/" + channel.getId();
}
private String generateRefundNo() {
// wx
// 2014
// 10
// 27
// 20
// 09
// 39
// 5522657
// a690389285100
// 目前的算法
// 时间序列年月日时分秒 14
// 纯随机6 TODO 芋艿此处估计是会有问题的后续在调整
return DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss") + // 时间序列
RandomUtil.randomInt(100000, 999999) // 随机为什么是这个范围因为偷懒
;
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void notifyPayRefund(Long channelId, PayRefundNotifyRespDTO notify, PayNotifyReqDTO rawNotify) { public void notifyRefund(Long channelId, PayRefundRespDTO notify) {
// 校验支付渠道是否有效
channelService.validPayChannel(channelId);
// 通知结果
// 校验支付渠道是否有效 // 校验支付渠道是否有效
// TODO 芋艿需要重构下这块的逻辑
PayChannelDO channel = channelService.validPayChannel(channelId); PayChannelDO channel = channelService.validPayChannel(channelId);
if (Objects.equals(PayNotifyRefundStatusEnum.SUCCESS, notify.getStatus())){
payRefundSuccess(notify);
} else {
//TODO 支付异常 支付宝似乎没有支付异常的通知
// TODO @jason那这里可以考虑打个 error logger @芋艿 微信是否存在支付异常通知
}
}
private void payRefundSuccess(PayRefundNotifyRespDTO refundNotify) {
// 校验退款单存在
PayRefundDO refundDO = refundMapper.selectByTradeNoAndMerchantRefundNo(refundNotify.getTradeNo(),
refundNotify.getReqNo());
if (refundDO == null) {
log.error("[payRefundSuccess][不存在 seqNo 为{} 的支付退款单]", refundNotify.getReqNo());
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_NOT_FOUND);
}
// 得到已退金额
PayOrderDO payOrderDO = orderService.getOrder(refundDO.getOrderId());
Long refundedAmount = payOrderDO.getRefundPrice();
PayOrderStatusEnum orderStatus = PayOrderStatusEnum.SUCCESS;
if(Objects.equals(payOrderDO.getPrice(), refundedAmount+ refundDO.getRefundPrice())){
//支付金额 = 已退金额 + 本次退款金额
orderStatus = PayOrderStatusEnum.CLOSED;
}
// 更新支付订单
PayOrderDO updateOrderDO = new PayOrderDO();
updateOrderDO.setId(refundDO.getOrderId())
.setRefundPrice(refundedAmount + refundDO.getRefundPrice())
.setStatus(orderStatus.getStatus())
.setRefundTimes(payOrderDO.getRefundTimes() + 1)
.setRefundStatus(refundDO.getType());
orderMapper.updateById(updateOrderDO);
// 更新退款订单 // 更新退款订单
PayRefundDO updateRefundDO = new PayRefundDO(); TenantUtils.execute(channel.getTenantId(), () -> notifyRefund(channel, notify));
updateRefundDO.setId(refundDO.getId())
.setSuccessTime(refundNotify.getRefundSuccessTime())
.setChannelRefundNo(refundNotify.getChannelOrderNo())
.setTradeNo(refundNotify.getTradeNo())
.setNotifyTime(LocalDateTime.now())
.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
refundMapper.updateById(updateRefundDO);
// 插入退款通知记录
// TODO 通知商户成功或者失败. 现在通知似乎没有实现 只是回调
notifyService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
.type(PayNotifyTypeEnum.REFUND.getType()).dataId(refundDO.getId()).build());
} }
/** private void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) {
* 校验是否进行退款 if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) {
* notifyRefundSuccess(channel, notify);
* @param reqDTO 退款申请信息 } else {
* @param order 原始支付订单信息 notifyRefundFailure(channel, notify);
*/
private void validatePayRefund(PayRefundCreateReqDTO reqDTO, PayOrderDO order) {
// 校验状态必须是支付状态
if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_SUCCESS);
} }
// 是否已经全额退款 }
if (PayRefundTypeEnum.ALL.getStatus().equals(order.getRefundStatus())) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_ALL_REFUNDED); private void notifyRefundSuccess(PayChannelDO channel, PayRefundRespDTO notify) {
// 1.1 查询 PayRefundDO
PayRefundDO refund = refundMapper.selectByAppIdAndNo(
channel.getAppId(), notify.getOutRefundNo());
if (refund == null) {
throw exception(ErrorCodeConstants.PAY_REFUND_NOT_FOUND);
} }
// 校验金额 退款金额不能大于 原定的金额 if (PayRefundStatusEnum.isSuccess(refund.getStatus())) { // 如果已经是成功直接返回不用重复更新
if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){ return;
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_PRICE_PRICE_EXCEED);
} }
// 校验渠道订单号 if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) {
if (StrUtil.isEmpty(order.getChannelOrderNo())) { throw exception(ErrorCodeConstants.PAY_REFUND_STATUS_IS_NOT_WAITING);
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_CHN_ORDER_NO_IS_NULL);
} }
//TODO 退款的期限 退款次数的控制
// 1.2 更新 PayRefundDO
PayRefundDO updateRefundObj = new PayRefundDO()
.setSuccessTime(notify.getSuccessTime())
.setChannelRefundNo(notify.getChannelRefundNo())
.setStatus(PayRefundStatusEnum.SUCCESS.getStatus())
.setChannelNotifyData(toJsonString(notify));
int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj);
if (updateCounts == 0) { // 校验状态必须是等待状态
throw exception(ErrorCodeConstants.PAY_REFUND_STATUS_IS_NOT_WAITING);
}
// 2. 更新订单
orderService.updateOrderRefundPrice(refund.getOrderId(), refund.getRefundPrice());
// 3. 插入退款通知记录
notifyService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
.type(PayNotifyTypeEnum.REFUND.getType()).dataId(refund.getId()).build());
}
private void notifyRefundFailure(PayChannelDO channel, PayRefundRespDTO notify) {
// TODO 芋艿未实现
} }
} }

View File

@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.pay.service.app;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO; import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO; import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO;
import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO; import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO;
@ -18,8 +16,6 @@ import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Map; import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
@ -58,7 +54,7 @@ public class PayAppServiceTest extends BaseDbUnitTest {
// 准备参数 // 准备参数
PayAppCreateReqVO reqVO = randomPojo(PayAppCreateReqVO.class, o -> PayAppCreateReqVO reqVO = randomPojo(PayAppCreateReqVO.class, o ->
o.setStatus((RandomUtil.randomEle(CommonStatusEnum.values()).getStatus())) o.setStatus((RandomUtil.randomEle(CommonStatusEnum.values()).getStatus()))
.setPayNotifyUrl(randomURL()) .setOrderNotifyUrl(randomURL())
.setRefundNotifyUrl(randomURL())); .setRefundNotifyUrl(randomURL()));
// 调用 // 调用
@ -77,7 +73,7 @@ public class PayAppServiceTest extends BaseDbUnitTest {
// 准备参数 // 准备参数
PayAppUpdateReqVO reqVO = randomPojo(PayAppUpdateReqVO.class, o -> { PayAppUpdateReqVO reqVO = randomPojo(PayAppUpdateReqVO.class, o -> {
o.setStatus(CommonStatusEnum.ENABLE.getStatus()); o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setPayNotifyUrl(randomURL()).setRefundNotifyUrl(randomURL()); o.setOrderNotifyUrl(randomURL()).setRefundNotifyUrl(randomURL());
o.setId(dbApp.getId()); // 设置更新的 ID o.setId(dbApp.getId()); // 设置更新的 ID
}); });

View File

@ -13,7 +13,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper; import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
import cn.iocoder.yudao.module.pay.service.app.PayAppService; import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@ -85,9 +85,9 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
o.setSuccessTime(LocalDateTime.of(2018, 1, 1, 10, 10, 2)); o.setSuccessTime(LocalDateTime.of(2018, 1, 1, 10, 10, 2));
o.setNotifyTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15)); o.setNotifyTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
o.setSuccessExtensionId(1L); o.setSuccessExtensionId(1L);
o.setRefundStatus(PayRefundTypeEnum.NO.getStatus()); o.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
o.setRefundTimes(0); o.setRefundTimes(0);
o.setRefundPrice(0L); o.setRefundPrice(0);
o.setChannelUserId("1008611"); o.setChannelUserId("1008611");
o.setChannelOrderNo(channelOrderId); o.setChannelOrderNo(channelOrderId);
o.setUpdateTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15)); o.setUpdateTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
@ -106,7 +106,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
// 测试 status 不匹配 // 测试 status 不匹配
orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()))); orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())));
// 测试 refundStatus 不匹配 // 测试 refundStatus 不匹配
orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayRefundTypeEnum.ALL.getStatus()))); orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayOrderRefundStatusEnum.ALL.getStatus())));
// 测试 createTime 不匹配 // 测试 createTime 不匹配
orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(LocalDateTime.of(2019, 1, 1, 10, 10, orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(LocalDateTime.of(2019, 1, 1, 10, 10,
1)))); 1))));
@ -118,7 +118,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
reqVO.setMerchantOrderId(merchantOrderId); reqVO.setMerchantOrderId(merchantOrderId);
reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus()); reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
reqVO.setRefundStatus(PayRefundTypeEnum.NO.getStatus()); reqVO.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2018, 1, 1, 10, 1, 0), LocalDateTime.of(2018, 1, 1, 10, 1, 0)})); reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2018, 1, 1, 10, 1, 0), LocalDateTime.of(2018, 1, 1, 10, 1, 0)}));
// 调用 // 调用
PageResult<PayOrderDO> pageResult = orderService.getOrderPage(reqVO); PageResult<PayOrderDO> pageResult = orderService.getOrderPage(reqVO);
@ -153,9 +153,9 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
o.setSuccessTime(LocalDateTime.of(2018, 1, 1, 10, 10, 2)); o.setSuccessTime(LocalDateTime.of(2018, 1, 1, 10, 10, 2));
o.setNotifyTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15)); o.setNotifyTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
o.setSuccessExtensionId(1L); o.setSuccessExtensionId(1L);
o.setRefundStatus(PayRefundTypeEnum.NO.getStatus()); o.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
o.setRefundTimes(0); o.setRefundTimes(0);
o.setRefundPrice(0L); o.setRefundPrice(0);
o.setChannelUserId("1008611"); o.setChannelUserId("1008611");
o.setChannelOrderNo(channelOrderId); o.setChannelOrderNo(channelOrderId);
o.setUpdateTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15)); o.setUpdateTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
@ -175,7 +175,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
// 测试 status 不匹配 // 测试 status 不匹配
orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()))); orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())));
// 测试 refundStatus 不匹配 // 测试 refundStatus 不匹配
orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayRefundTypeEnum.ALL.getStatus()))); orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayOrderRefundStatusEnum.ALL.getStatus())));
// 测试 createTime 不匹配 // 测试 createTime 不匹配
orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(LocalDateTime.of(2019, 1, 1, 10, 10, orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(LocalDateTime.of(2019, 1, 1, 10, 10,
1)))); 1))));
@ -187,7 +187,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
reqVO.setMerchantOrderId(merchantOrderId); reqVO.setMerchantOrderId(merchantOrderId);
reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus()); reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
reqVO.setRefundStatus(PayRefundTypeEnum.NO.getStatus()); reqVO.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2018, 1, 1, 10, 1, 0), LocalDateTime.of(2018, 1, 1, 10, 1, 0)})); reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2018, 1, 1, 10, 1, 0), LocalDateTime.of(2018, 1, 1, 10, 1, 0)}));
// 调用 // 调用

View File

@ -11,7 +11,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper; import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum; import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
import cn.iocoder.yudao.module.pay.service.app.PayAppService; import cn.iocoder.yudao.module.pay.service.app.PayAppService;
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService; import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService; import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@ -62,13 +62,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
o.setChannelId(1L); o.setChannelId(1L);
o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); o.setChannelCode(PayChannelEnum.WX_PUB.getCode());
o.setOrderId(1L); o.setOrderId(1L);
o.setTradeNo("OT0000001"); o.setNo("OT0000001");
o.setMerchantOrderId("MOT0000001"); o.setMerchantOrderId("MOT0000001");
o.setMerchantRefundNo("MRF0000001"); o.setMerchantRefundId("MRF0000001");
o.setNotifyUrl("https://www.cancanzi.com"); o.setNotifyUrl("https://www.cancanzi.com");
o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus()); o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()); o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
o.setType(PayRefundTypeEnum.SOME.getStatus());
o.setPayPrice(100); o.setPayPrice(100);
o.setRefundPrice(500); o.setRefundPrice(500);
o.setReason("就是想退款了,你有意见吗"); o.setReason("就是想退款了,你有意见吗");
@ -77,10 +76,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
o.setChannelRefundNo("CHR0000001"); o.setChannelRefundNo("CHR0000001");
o.setChannelErrorCode(""); o.setChannelErrorCode("");
o.setChannelErrorMsg(""); o.setChannelErrorMsg("");
o.setChannelExtras("");
o.setExpireTime(LocalDateTime.of(2021, 1, 1, 10, 10, 30));
o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15)); o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15));
o.setNotifyTime(LocalDateTime.of(2021, 1, 1, 10, 10, 20));
o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10)); o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10));
o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35)); o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35));
}); });
@ -90,14 +86,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
// 测试 channelCode 不匹配 // 测试 channelCode 不匹配
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())));
// 测试 merchantRefundNo 不匹配 // 测试 merchantRefundNo 不匹配
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundNo("MRF1111112"))); refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId("MRF1111112")));
// 测试 notifyStatus 不匹配 // 测试 notifyStatus 不匹配
refundMapper.insert( refundMapper.insert(
cloneIgnoreId(dbRefund, o -> o.setNotifyStatus(PayOrderNotifyStatusEnum.FAILURE.getStatus()))); cloneIgnoreId(dbRefund, o -> o.setNotifyStatus(PayOrderNotifyStatusEnum.FAILURE.getStatus())));
// 测试 status 不匹配 // 测试 status 不匹配
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.CLOSE.getStatus()))); refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus())));
// 测试 type 不匹配
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setType(PayRefundTypeEnum.ALL.getStatus())));
// 测试 createTime 不匹配 // 测试 createTime 不匹配
refundMapper.insert(cloneIgnoreId(dbRefund, o -> refundMapper.insert(cloneIgnoreId(dbRefund, o ->
o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10)))); o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10))));
@ -108,7 +102,6 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
reqVO.setMerchantRefundNo("MRF0000001"); reqVO.setMerchantRefundNo("MRF0000001");
reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus()); reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()); reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
reqVO.setType(PayRefundTypeEnum.SOME.getStatus());
reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2021, 1, 1, 10, 10, 10), LocalDateTime.of(2021, 1, 1, 10, 10, 12)})); reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2021, 1, 1, 10, 10, 10), LocalDateTime.of(2021, 1, 1, 10, 10, 12)}));
// 调用 // 调用
@ -127,13 +120,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
o.setChannelId(1L); o.setChannelId(1L);
o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); o.setChannelCode(PayChannelEnum.WX_PUB.getCode());
o.setOrderId(1L); o.setOrderId(1L);
o.setTradeNo("OT0000001"); o.setNo("OT0000001");
o.setMerchantOrderId("MOT0000001"); o.setMerchantOrderId("MOT0000001");
o.setMerchantRefundNo("MRF0000001"); o.setMerchantRefundId("MRF0000001");
o.setNotifyUrl("https://www.cancanzi.com"); o.setNotifyUrl("https://www.cancanzi.com");
o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus()); o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()); o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
o.setType(PayRefundTypeEnum.SOME.getStatus());
o.setPayPrice(100); o.setPayPrice(100);
o.setRefundPrice(500); o.setRefundPrice(500);
o.setReason("就是想退款了,你有意见吗"); o.setReason("就是想退款了,你有意见吗");
@ -142,10 +134,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
o.setChannelRefundNo("CHR0000001"); o.setChannelRefundNo("CHR0000001");
o.setChannelErrorCode(""); o.setChannelErrorCode("");
o.setChannelErrorMsg(""); o.setChannelErrorMsg("");
o.setChannelExtras("");
o.setExpireTime(LocalDateTime.of(2021, 1, 1, 10, 10, 30));
o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15)); o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15));
o.setNotifyTime(LocalDateTime.of(2021, 1, 1, 10, 10, 20));
o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10)); o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10));
o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35)); o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35));
}); });
@ -155,14 +144,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
// 测试 channelCode 不匹配 // 测试 channelCode 不匹配
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())));
// 测试 merchantRefundNo 不匹配 // 测试 merchantRefundNo 不匹配
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundNo("MRF1111112"))); refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId("MRF1111112")));
// 测试 notifyStatus 不匹配 // 测试 notifyStatus 不匹配
refundMapper.insert( refundMapper.insert(
cloneIgnoreId(dbRefund, o -> o.setNotifyStatus(PayOrderNotifyStatusEnum.FAILURE.getStatus()))); cloneIgnoreId(dbRefund, o -> o.setNotifyStatus(PayOrderNotifyStatusEnum.FAILURE.getStatus())));
// 测试 status 不匹配 // 测试 status 不匹配
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.CLOSE.getStatus()))); refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus())));
// 测试 type 不匹配
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setType(PayRefundTypeEnum.ALL.getStatus())));
// 测试 createTime 不匹配 // 测试 createTime 不匹配
refundMapper.insert(cloneIgnoreId(dbRefund, o -> refundMapper.insert(cloneIgnoreId(dbRefund, o ->
o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10)))); o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10))));
@ -174,7 +161,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
reqVO.setMerchantRefundNo("MRF0000001"); reqVO.setMerchantRefundNo("MRF0000001");
reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus()); reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus()); reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
reqVO.setType(PayRefundTypeEnum.SOME.getStatus()); reqVO.setType(PayOrderRefundStatusEnum.PART.getStatus());
reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2021, 1, 1, 10, 10, 10), LocalDateTime.of(2021, 1, 1, 10, 10, 12)})); reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2021, 1, 1, 10, 10, 10), LocalDateTime.of(2021, 1, 1, 10, 10, 12)}));
// 调用 // 调用

View File

@ -194,7 +194,8 @@ yudao:
- ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求
- ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
pay: pay:
callback-url: http://yunai.natapp1.cc/admin-api/pay/notify/callback order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
access-log: # 访问日志的配置项 access-log: # 访问日志的配置项
enable: false enable: false
error-code: # 错误码相关配置项 error-code: # 错误码相关配置项

View File

@ -142,7 +142,7 @@ yudao:
- /admin-api/system/captcha/check # 校验图片验证码,和租户无关 - /admin-api/system/captcha/check # 校验图片验证码,和租户无关
- /admin-api/infra/file/*/get/** # 获取图片,和租户无关 - /admin-api/infra/file/*/get/** # 获取图片,和租户无关
- /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号 - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
- /admin-api/pay/notify/callback/* # 支付回调通知,不携带租户编号 - /admin-api/pay/notify/** # 支付回调通知,不携带租户编号
- /jmreport/* # 积木报表,无法携带租户编号 - /jmreport/* # 积木报表,无法携带租户编号
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号 - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号
ignore-tables: ignore-tables:

View File

@ -28,14 +28,14 @@ export function deleteChannel(id) {
} }
// 获得支付渠道 // 获得支付渠道
export function getChannel(appId,code) { export function getChannel(appId, code) {
return request({ return request({
url: '/pay/channel/get-channel', url: '/pay/channel/get',
method: 'get',
params:{ params:{
appId:appId, appId,
code:code code
}, },
method: 'get'
}) })
} }

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1627279586085" class="icon" viewBox="0 0 1036 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6737" xmlns:xlink="http://www.w3.org/1999/xlink" width="40.46875" height="40"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix&quot;) format(&quot;embedded-opentype&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2&quot;) format(&quot;woff2&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff&quot;) format(&quot;woff&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf&quot;) format(&quot;truetype&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont&quot;) format(&quot;svg&quot;); }</style></defs><path d="M27.587124 336.619083h69.148134a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916A13.978733 13.978733 0 0 0 96.735258 0.011183H27.587124a13.978733 13.978733 0 0 0-13.792351 13.978733v308.650434a13.978733 13.978733 0 0 0 13.792351 13.978733z m165.880969 0h27.584701a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916a13.978733 13.978733 0 0 0-13.79235-13.978733h-27.584701a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733z m138.109886 322.629167h-110.525185a27.771084 27.771084 0 0 0-27.584701 28.14385v111.829867a27.771084 27.771084 0 0 0 27.584701 28.14385h110.525185a27.957467 27.957467 0 0 0 27.584701-28.14385v-111.829867a27.957467 27.957467 0 0 0-27.584701-28.14385z m484.596091-322.629167h27.584701a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916a13.978733 13.978733 0 0 0-14.537883-13.978733h-27.5847a13.978733 13.978733 0 0 0-13.978734 13.978733v308.650434a13.978733 13.978733 0 0 0 13.978734 13.978733z m-469.871825 0H428.68358a13.978733 13.978733 0 0 0 13.792351-13.978733V13.989916A13.978733 13.978733 0 0 0 428.68358 0.011183h-83.126867a13.978733 13.978733 0 0 0-13.792351 13.978733v308.650434a13.978733 13.978733 0 0 0 13.792351 13.978733z m594.189361 0h69.148134a13.978733 13.978733 0 0 0 13.792351-13.978733V13.989916a13.978733 13.978733 0 0 0-14.537883-13.978733h-69.148135a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733z m-412.279444 126.181367H66.91396A67.470687 67.470687 0 0 0 0.002423 530.830286v425.139878a67.470687 67.470687 0 0 0 66.911537 68.029836h418.802853a67.470687 67.470687 0 0 0 66.911537-68.029836V487.775787a24.788954 24.788954 0 0 0-24.416188-24.975337z m-58.337914 433.899885a42.681733 42.681733 0 0 1-42.495349 43.054498H125.438257a42.681733 42.681733 0 0 1-42.495349-43.054498V590.100115a42.681733 42.681733 0 0 1 42.495349-43.054498h301.940642a42.681733 42.681733 0 0 1 42.495349 43.054498z m525.22761-433.899885a41.749817 41.749817 0 0 0-41.377051 42.122583v55.914934a41.377051 41.377051 0 1 0 82.940485 0v-55.914934a41.749817 41.749817 0 0 0-41.563434-42.122583z m0 223.659734a41.749817 41.749817 0 0 0-41.377051 42.122584V894.65012a45.477479 45.477479 0 0 1-45.291096 45.850246h-159.730327a43.240882 43.240882 0 0 0-43.613649 37.276622A41.9362 41.9362 0 0 0 745.534871 1024h233.538039a57.778765 57.778765 0 0 0 57.405999-58.337914V729.3283a41.749817 41.749817 0 0 0-41.377051-41.9362zM732.488053 322.64035V13.989916a13.978733 13.978733 0 0 0-13.79235-13.978733h-82.940485a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733h82.940485a13.978733 13.978733 0 0 0 13.79235-13.978733zM532.126208 0.011183c-11.36937 0-20.688525 6.337026-20.688526 13.978733v308.650434c0 7.828091 9.319156 13.978733 20.688526 13.978733s20.688525-6.337026 20.688525-13.978733V13.989916c0-7.641708-9.319156-13.978733-20.688525-13.978733z" p-id="6738" fill="#04C361"/><path d="M745.534871 462.80045a41.749817 41.749817 0 0 0-41.377051 42.122583v252.549117a41.377051 41.377051 0 1 0 82.940485 0V504.923033A41.749817 41.749817 0 0 0 745.534871 462.80045" p-id="6739" fill="#04C361"/></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1627279375144" class="icon" viewBox="0 0 1115 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4399" width="43.5546875" height="40" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix&quot;) format(&quot;embedded-opentype&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2&quot;) format(&quot;woff2&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff&quot;) format(&quot;woff&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf&quot;) format(&quot;truetype&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont&quot;) format(&quot;svg&quot;); }</style></defs><path d="M751.388 68.267a34.133 34.133 0 0 1 0-68.267h227.556a91.022 91.022 0 0 1 91.022 91.022v227.556a34.133 34.133 0 1 1-68.266 0V91.022a22.756 22.756 0 0 0-22.756-22.755H751.388M1001.7 705.422a34.133 34.133 0 0 1 68.266 0v227.556A91.022 91.022 0 0 1 978.944 1024H748.885a34.133 34.133 0 0 1 0-68.267H978.49a22.756 22.756 0 0 0 22.755-22.755V705.422M364.09 955.733a34.133 34.133 0 1 1 0 68.267H136.533a91.022 91.022 0 0 1-91.022-91.022V705.422a34.133 34.133 0 0 1 68.267 0v227.556a22.756 22.756 0 0 0 22.755 22.755H364.09M113.778 318.578a34.133 34.133 0 1 1-68.267 0V91.022A91.022 91.022 0 0 1 136.533 0H364.09a34.133 34.133 0 0 1 0 68.267H136.533a22.756 22.756 0 0 0-22.755 22.755v227.556M34.133 477.867a34.133 34.133 0 0 0 0 68.266h168.619v-68.266z m1046.756 0H912.27v68.266h168.619a34.133 34.133 0 0 0 0-68.266zM202.752 157.24h709.746v320.627H202.752z m0 388.893h709.746V866.76H202.752z" fill="#04C361" p-id="4400"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -132,6 +132,14 @@ export const PayChannelEnum = {
"code": "wx_app", "code": "wx_app",
"name": "微信 APP 支付" "name": "微信 APP 支付"
}, },
WX_NATIVE: {
"code": "wx_native",
"name": "微信扫码支付"
},
WX_BAR: {
"code": "wx_bar",
"name": "微信条码支付"
},
ALIPAY_PC: { ALIPAY_PC: {
"code": "alipay_pc", "code": "alipay_pc",
"name": "支付宝 PC 网站支付" "name": "支付宝 PC 网站支付"

View File

@ -1,15 +1,15 @@
<template> <template>
<div> <div>
<el-dialog :visible.sync="transferParam.wechatOpen" :title="title" @close="close" append-to-body width="800px"> <el-dialog :visible.sync="transferParam.wechatOpen" :title="title" @close="close" append-to-body width="800px">
<el-form ref="wechatJsApiForm" :model="form" :rules="rules" size="medium" label-width="100px" <el-form ref="wechatJsApiForm" :model="form" :rules="rules" size="medium" label-width="120px"
v-loading="transferParam.loading"> v-loading="transferParam.loading">
<el-form-item label-width="180px" label="渠道费率" prop="feeRate"> <el-form-item label-width="180px" label="渠道费率" prop="feeRate">
<el-input v-model="form.feeRate" placeholder="请输入渠道费率" clearable :style="{width: '100%'}"> <el-input v-model="form.feeRate" placeholder="请输入渠道费率" clearable :style="{width: '100%'}">
<template slot="append">%</template> <template slot="append">%</template>
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label-width="180px" label="公众号APPID" prop="weChatConfig.appId"> <el-form-item label-width="180px" label="公众号 APPID" prop="weChatConfig.appId">
<el-input v-model="form.weChatConfig.appId" placeholder="请输入公众号APPID" clearable :style="{width: '100%'}"> <el-input v-model="form.weChatConfig.appId" placeholder="请输入公众号 APPID" clearable :style="{width: '100%'}">
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item label-width="180px" label="商户号" prop="weChatConfig.mchId"> <el-form-item label-width="180px" label="商户号" prop="weChatConfig.mchId">
@ -29,29 +29,41 @@
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label-width="180px" label="商户密钥" prop="weChatConfig.mchKey" <div v-if="form.weChatConfig.apiVersion === 'v2'">
v-if="form.weChatConfig.apiVersion === 'v2'"> <el-form-item label-width="180px" label="商户密钥" prop="weChatConfig.mchKey">
<el-input v-model="form.weChatConfig.mchKey" placeholder="请输入商户密钥" clearable <el-input v-model="form.weChatConfig.mchKey" placeholder="请输入商户密钥" clearable
:style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
</el-form-item>
<div v-if="form.weChatConfig.apiVersion === 'v3'">
<el-form-item label-width="180px" label="API V3密钥" prop="weChatConfig.apiV3Key">
<el-input v-model="form.weChatConfig.apiV3Key" placeholder="请输入API V3密钥" clearable
:style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input> :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
</el-form-item> </el-form-item>
<el-form-item label-width="180px" label="apiclient_key.perm证书" prop="weChatConfig.privateKeyContent"> <el-form-item label-width="180px" label="apiclient_cert.p12 证书" prop="weChatConfig.keyContent">
<el-input v-model="form.weChatConfig.keyContent" type="textarea"
placeholder="请上传 apiclient_cert.p12 证书"
readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
</el-form-item>
<el-form-item label-width="180px" label="">
<el-upload :limit="1" accept=".p12" action=""
:before-upload="p12FileBeforeUpload"
:http-request="keyContentUpload">
<el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
</el-upload>
</el-form-item>
</div>
<div v-if="form.weChatConfig.apiVersion === 'v3'">
<el-form-item label-width="180px" label="API V3 密钥" prop="weChatConfig.apiV3Key">
<el-input v-model="form.weChatConfig.apiV3Key" placeholder="请输入 API V3 密钥" clearable
:style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
</el-form-item>
<el-form-item label-width="180px" label="apiclient_key.perm 证书" prop="weChatConfig.privateKeyContent">
<el-input v-model="form.weChatConfig.privateKeyContent" type="textarea" <el-input v-model="form.weChatConfig.privateKeyContent" type="textarea"
placeholder="请上传apiclient_key.perm证书" placeholder="请上传 apiclient_key.perm 证书"
readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input> readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
</el-form-item> </el-form-item>
<el-form-item label-width="180px" label="" prop="privateKeyContentFile"> <el-form-item label-width="180px" label="" prop="privateKeyContentFile">
<el-upload ref="privateKeyContentFile" <el-upload ref="privateKeyContentFile"
:limit="1" :limit="1"
:accept="fileAccept" accept=".pem"
:headers="header"
action="" action=""
:before-upload="pemFileBeforeUpload" :before-upload="pemFileBeforeUpload"
:http-request="privateKeyUpload" :http-request="privateKeyContentUpload"
> >
<el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button> <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
</el-upload> </el-upload>
@ -64,18 +76,17 @@
<el-form-item label-width="180px" label="" prop="privateCertContentFile"> <el-form-item label-width="180px" label="" prop="privateCertContentFile">
<el-upload ref="privateCertContentFile" <el-upload ref="privateCertContentFile"
:limit="1" :limit="1"
:accept="fileAccept" accept=".pem"
:headers="header"
action="" action=""
:before-upload="pemFileBeforeUpload" :before-upload="pemFileBeforeUpload"
:http-request="privateCertUpload" :http-request="privateCertContentUpload"
> >
<el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button> <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
</el-upload> </el-upload>
</el-form-item> </el-form-item>
</div> </div>
<el-form-item label-width="180px" label="备注" prop="remark"> <el-form-item label-width="180px" label="备注" prop="remark">
<el-input v-model="form.remark" :style="{width: '100%'}"></el-input> <el-input v-model="form.remark" :style="{width: '100%'}" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
@ -100,6 +111,7 @@ const defaultForm = {
mchId: '', mchId: '',
apiVersion: '', apiVersion: '',
mchKey: '', mchKey: '',
keyContent: '',
privateKeyContent: '', privateKeyContent: '',
privateCertContent: '', privateCertContent: '',
apiV3Key:'', apiV3Key:'',
@ -159,27 +171,27 @@ export default {
message: '请输入商户密钥', message: '请输入商户密钥',
trigger: 'blur' trigger: 'blur'
}], }],
'weChatConfig.keyContent': [{
required: true,
message: '请上传 apiclient_cert.p12 证书',
trigger: 'blur'
}],
'weChatConfig.privateKeyContent': [{ 'weChatConfig.privateKeyContent': [{
required: true, required: true,
message: '请上传apiclient_key.perm证书', message: '请上传 apiclient_key.perm 证书',
trigger: 'blur' trigger: 'blur'
}], }],
'weChatConfig.privateCertContent': [{ 'weChatConfig.privateCertContent': [{
required: true, required: true,
message: '请上传apiclient_cert.perm证书', message: '请上传 apiclient_cert.perm证 书',
trigger: 'blur' trigger: 'blur'
}], }],
'weChatConfig.apiV3Key': [{ 'weChatConfig.apiV3Key': [{
required: true, required: true,
message: '请上传apiV3密钥值', message: '请上传 api V3 密钥值',
trigger: 'blur' trigger: 'blur'
}], }],
}, },
// header
header: {
"Authorization": null
},
fileAccept: ".pem",
// //
statusDictDatas: getDictDatas(DICT_TYPE.COMMON_STATUS), statusDictDatas: getDictDatas(DICT_TYPE.COMMON_STATUS),
versionDictDatas: getDictDatas(DICT_TYPE.PAY_CHANNEL_WECHAT_VERSION), versionDictDatas: getDictDatas(DICT_TYPE.PAY_CHANNEL_WECHAT_VERSION),
@ -218,6 +230,7 @@ export default {
this.form.weChatConfig.apiVersion = config.apiVersion; this.form.weChatConfig.apiVersion = config.apiVersion;
this.form.weChatConfig.mchId = config.mchId; this.form.weChatConfig.mchId = config.mchId;
this.form.weChatConfig.mchKey = config.mchKey; this.form.weChatConfig.mchKey = config.mchKey;
this.form.weChatConfig.keyContent = config.keyContent;
this.form.weChatConfig.privateKeyContent = config.privateKeyContent; this.form.weChatConfig.privateKeyContent = config.privateKeyContent;
this.form.weChatConfig.privateCertContent = config.privateCertContent; this.form.weChatConfig.privateCertContent = config.privateCertContent;
this.form.weChatConfig.apiV3Key = config.apiV3Key; this.form.weChatConfig.apiV3Key = config.apiV3Key;
@ -241,7 +254,6 @@ export default {
this.$modal.msgSuccess("修改成功"); this.$modal.msgSuccess("修改成功");
this.close(); this.close();
} }
}) })
} else { } else {
@ -255,10 +267,14 @@ export default {
} }
}); });
}, },
pemFileBeforeUpload(file) { /**
* apiclient_cert.p12apiclient_cert.pemapiclient_key.pem 上传前的校验
*/
fileBeforeUpload(file, fileAccept) {
let format = '.' + file.name.split(".")[1]; let format = '.' + file.name.split(".")[1];
if (format !== this.fileAccept) { if (format !== fileAccept) {
this.$message.error('请上传指定格式"' + this.fileAccept + '"文件'); debugger
this.$message.error('请上传指定格式"' + fileAccept + '"文件');
return false; return false;
} }
let isRightSize = file.size / 1024 / 1024 < 2 let isRightSize = file.size / 1024 / 1024 < 2
@ -267,19 +283,41 @@ export default {
} }
return isRightSize return isRightSize
}, },
privateKeyUpload(event) { p12FileBeforeUpload(file) {
this.fileBeforeUpload(file, '.p12')
},
pemFileBeforeUpload(file) {
this.fileBeforeUpload(file, '.pem')
},
/**
* 读取 apiclient_key.pem privateKeyContent 字段
*/
privateKeyContentUpload(event) {
const readFile = new FileReader() const readFile = new FileReader()
readFile.onload = (e) => { readFile.onload = (e) => {
this.form.weChatConfig.privateKeyContent = e.target.result this.form.weChatConfig.privateKeyContent = e.target.result
} }
readFile.readAsText(event.file); readFile.readAsText(event.file);
}, },
privateCertUpload(event) { /**
* 读取 apiclient_cert.pem privateCertContent 字段
*/
privateCertContentUpload(event) {
const readFile = new FileReader() const readFile = new FileReader()
readFile.onload = (e) => { readFile.onload = (e) => {
this.form.weChatConfig.privateCertContent = e.target.result this.form.weChatConfig.privateCertContent = e.target.result
} }
readFile.readAsText(event.file); readFile.readAsText(event.file);
},
/**
* 读取 apiclient_cert.p12 keyContent 字段
*/
keyContentUpload(event) {
const readFile = new FileReader()
readFile.onload = (e) => {
this.form.weChatConfig.keyContent = e.target.result.split(',')[1]
}
readFile.readAsDataURL(event.file); // base64
} }
} }
} }

View File

@ -143,6 +143,30 @@
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="payChannelEnum.WX_NATIVE.name" align="center">
<template v-slot="scope">
<el-button type="success" icon="el-icon-check" circle
v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_NATIVE.code)"
@click="handleUpdateChannel(scope.row,payChannelEnum.WX_NATIVE.code,payType.WECHAT)">
</el-button>
<el-button v-else
type="danger" icon="el-icon-close" circle
@click="handleCreateChannel(scope.row,payChannelEnum.WX_NATIVE.code,payType.WECHAT)">
</el-button>
</template>
</el-table-column>
<el-table-column :label="payChannelEnum.WX_BAR.name" align="center">
<template v-slot="scope">
<el-button type="success" icon="el-icon-check" circle
v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_BAR.code)"
@click="handleUpdateChannel(scope.row,payChannelEnum.WX_BAR.code,payType.WECHAT)">
</el-button>
<el-button v-else
type="danger" icon="el-icon-close" circle
@click="handleCreateChannel(scope.row,payChannelEnum.WX_BAR.code,payType.WECHAT)">
</el-button>
</template>
</el-table-column>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template v-slot="scope"> <template v-slot="scope">
@ -173,8 +197,8 @@
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="支付结果的回调地址" prop="payNotifyUrl"> <el-form-item label="支付结果的回调地址" prop="orderNotifyUrl">
<el-input v-model="form.payNotifyUrl" placeholder="请输入支付结果的回调地址"/> <el-input v-model="form.orderNotifyUrl" placeholder="请输入支付结果的回调地址"/>
</el-form-item> </el-form-item>
<el-form-item label="退款结果的回调地址" prop="refundNotifyUrl"> <el-form-item label="退款结果的回调地址" prop="refundNotifyUrl">
<el-input v-model="form.refundNotifyUrl" placeholder="请输入退款结果的回调地址"/> <el-input v-model="form.refundNotifyUrl" placeholder="请输入退款结果的回调地址"/>
@ -235,7 +259,7 @@ export default {
rules: { rules: {
name: [{required: true, message: "应用名不能为空", trigger: "blur"}], name: [{required: true, message: "应用名不能为空", trigger: "blur"}],
status: [{required: true, message: "开启状态不能为空", trigger: "blur"}], status: [{required: true, message: "开启状态不能为空", trigger: "blur"}],
payNotifyUrl: [{required: true, message: "支付结果的回调地址不能为空", trigger: "blur"}], orderNotifyUrl: [{required: true, message: "支付结果的回调地址不能为空", trigger: "blur"}],
refundNotifyUrl: [{required: true, message: "退款结果的回调地址不能为空", trigger: "blur"}], refundNotifyUrl: [{required: true, message: "退款结果的回调地址不能为空", trigger: "blur"}],
}, },
// //
@ -295,7 +319,7 @@ export default {
name: undefined, name: undefined,
status: undefined, status: undefined,
remark: undefined, remark: undefined,
payNotifyUrl: undefined, orderNotifyUrl: undefined,
refundNotifyUrl: undefined, refundNotifyUrl: undefined,
}; };
this.resetForm("form"); this.resetForm("form");

View File

@ -26,7 +26,7 @@
<!-- 微信支付 --> <!-- 微信支付 -->
<el-descriptions title="选择微信支付" style="margin-top: 20px;" /> <el-descriptions title="选择微信支付" style="margin-top: 20px;" />
<div class="pay-channel-container"> <div class="pay-channel-container">
<div class="box" v-for="channel in channels" v-if="channel.code.indexOf('wx_') === 0" :key="channel.code"> <div class="box" v-for="channel in channels" v-if="channel.code.indexOf('wx_') === 0" :key="channel.code" @click="submit(channel.code)">
<img :src="channel.icon"> <img :src="channel.icon">
<div class="title">{{ channel.name }}</div> <div class="title">{{ channel.name }}</div>
</div> </div>
@ -125,6 +125,14 @@ export default {
name: '微信 App 支付', name: '微信 App 支付',
icon: require("@/assets/images/pay/icon/wx_app.svg"), icon: require("@/assets/images/pay/icon/wx_app.svg"),
code: "wx_app" code: "wx_app"
}, {
name: '微信扫码支付',
icon: require("@/assets/images/pay/icon/wx_native.svg"),
code: "wx_native"
}, {
name: '微信条码支付',
icon: require("@/assets/images/pay/icon/wx_bar.svg"),
code: "wx_bar"
}, { }, {
name: '模拟支付', name: '模拟支付',
icon: require("@/assets/images/pay/icon/mock.svg"), icon: require("@/assets/images/pay/icon/mock.svg"),
@ -195,6 +203,15 @@ export default {
} }
return; return;
} }
if (channelCode === PayChannelEnum.WX_BAR.code) {
this.barCode = {
channelCode: channelCode,
value: '',
title: '“微信”条码支付',
visible: true
}
return;
}
// //
this.submit0(channelCode) this.submit0(channelCode)
@ -207,7 +224,16 @@ export default {
returnUrl: location.href, // {@link returnUrl} returnUrl: location.href, // {@link returnUrl}
...this.buildSubmitParam(channelCode) ...this.buildSubmitParam(channelCode)
}).then(response => { }).then(response => {
//
const data = response.data const data = response.data
if (data.status === PayOrderStatusEnum.SUCCESS.status) {
this.clearQueryInterval();
this.$message.success('支付成功!');
this.goReturnUrl();
return
}
//
if (data.displayMode === PayDisplayModeEnum.URL.mode) { if (data.displayMode === PayDisplayModeEnum.URL.mode) {
this.displayUrl(channelCode, data) this.displayUrl(channelCode, data)
} else if (data.displayMode === PayDisplayModeEnum.QR_CODE.mode) { } else if (data.displayMode === PayDisplayModeEnum.QR_CODE.mode) {
@ -230,11 +256,18 @@ export default {
} }
} }
} }
// BarCode authCode
if (channelCode === PayChannelEnum.WX_BAR.code) {
return {
"channelExtras": {
"authCode": this.barCode.value
}
}
}
return {} return {}
}, },
/** 提交支付后URL 的展示形式 */ /** 提交支付后URL 的展示形式 */
displayUrl(channelCode, data) { displayUrl(channelCode, data) {
// window.open(data.displayContent)
location.href = data.displayContent location.href = data.displayContent
this.submitLoading = false this.submitLoading = false
}, },
@ -298,6 +331,9 @@ export default {
* close支付已关闭 * close支付已关闭
*/ */
goReturnUrl(payResult) { goReturnUrl(payResult) {
//
this.clearQueryInterval();
// //
if (!this.returnUrl) { if (!this.returnUrl) {
this.$tab.closePage(); this.$tab.closePage();

View File

@ -41,7 +41,8 @@
</el-table-column> </el-table-column>
<el-table-column label="退款时间" align="center" prop="refundTime" width="180"> <el-table-column label="退款时间" align="center" prop="refundTime" width="180">
<template v-slot="scope"> <template v-slot="scope">
<span>{{ parseTime(scope.row.refundTime) }}</span> <span v-if="scope.row.refundTime">{{ parseTime(scope.row.refundTime) }}</span>
<span v-else-if="scope.row.payRefundId">退款中等待退款结果</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width"> <el-table-column label="操作" align="center" class-name="small-padding fixed-width">