mirror of
https://gitee.com/huangge1199_admin/vue-pro.git
synced 2025-02-17 01:30:33 +08:00
Merge remote-tracking branch 'yudao/feature/mall_product' into feature/mall_product
This commit is contained in:
commit
d80bf5a368
@ -1,10 +1,12 @@
|
||||
package cn.iocoder.yudao.framework.common.util.validation;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import javax.validation.Validation;
|
||||
import javax.validation.Validator;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
@ -37,6 +39,12 @@ public class ValidationUtils {
|
||||
&& 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) {
|
||||
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
|
||||
if (CollUtil.isNotEmpty(constraintViolations)) {
|
||||
|
@ -13,14 +13,25 @@ import javax.validation.constraints.NotEmpty;
|
||||
public class PayProperties {
|
||||
|
||||
/**
|
||||
* 回调地址
|
||||
* 支付回调地址
|
||||
*
|
||||
* 实际上,对应的 PayNotifyController 的 notifyCallback 方法的 URL
|
||||
* 实际上,对应的 PayNotifyController 的 notifyOrder 方法的 URL
|
||||
*
|
||||
* 注意,支付渠道统一回调到 payNotifyUrl 地址,由支付模块统一处理;然后,自己的支付模块,在回调 PayAppDO.payNotifyUrl 地址
|
||||
* 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 orderNotifyUrl 地址 => 业务的 PayAppDO.orderNotifyUrl 地址
|
||||
*/
|
||||
@NotEmpty(message = "回调地址不能为空")
|
||||
@URL(message = "回调地址的格式必须是 URL")
|
||||
private String callbackUrl;
|
||||
@NotEmpty(message = "支付回调地址不能为空")
|
||||
@URL(message = "支付回调地址的格式必须是 URL")
|
||||
private String orderNotifyUrl;
|
||||
|
||||
/**
|
||||
* 退款回调地址
|
||||
*
|
||||
* 实际上,对应的 PayNotifyController 的 notifyRefund 方法的 URL
|
||||
*
|
||||
* 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 refundNotifyUrl 地址 => 业务的 PayAppDO.notifyRefundUrl 地址
|
||||
*/
|
||||
@NotEmpty(message = "支付回调地址不能为空")
|
||||
@URL(message = "支付回调地址的格式必须是 URL")
|
||||
private String refundNotifyUrl;
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,11 @@
|
||||
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.notify.PayOrderNotifyRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
|
||||
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.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.PayRefundUnifiedRespDTO;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能
|
||||
@ -22,31 +21,42 @@ public interface PayClient {
|
||||
*/
|
||||
Long getId();
|
||||
|
||||
// ============ 支付相关 ==========
|
||||
|
||||
/**
|
||||
* 调用支付渠道,统一下单
|
||||
*
|
||||
* @param reqDTO 下单信息
|
||||
* @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 统一退款请求信息
|
||||
* @return 各支付渠道的统一返回结果
|
||||
* @return 退款信息
|
||||
*/
|
||||
PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
|
||||
PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 解析回调数据
|
||||
* 解析 refund 回调数据
|
||||
*
|
||||
* @param rawNotify 通知内容
|
||||
* @return 回调对象
|
||||
* 1. {@link PayRefundNotifyRespDTO} 退款通知
|
||||
* 2. {@link PayOrderNotifyRespDTO} 支付通知
|
||||
* @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
|
||||
* @param body HTTP 回调接口的 request body
|
||||
* @return 支付订单信息
|
||||
*/
|
||||
default Object parseNotify(PayNotifyReqDTO rawNotify) {
|
||||
throw new UnsupportedOperationException("未实现 parseNotify 方法!");
|
||||
}
|
||||
PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建【SUCCESS】或【CLOSED】状态的订单返回,适合支付渠道回调时
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@ -28,10 +28,12 @@ public class PayOrderUnifiedReqDTO {
|
||||
// ========== 商户相关字段 ==========
|
||||
|
||||
/**
|
||||
* 商户订单编号
|
||||
* 外部订单号
|
||||
*
|
||||
* 对应 PayOrderExtensionDO 的 no 字段
|
||||
*/
|
||||
@NotEmpty(message = "商户订单编号不能为空")
|
||||
private String merchantOrderId;
|
||||
@NotEmpty(message = "外部订单编号不能为空")
|
||||
private String outTradeNo;
|
||||
/**
|
||||
* 商品标题
|
||||
*/
|
||||
@ -41,7 +43,6 @@ public class PayOrderUnifiedReqDTO {
|
||||
/**
|
||||
* 商品描述信息
|
||||
*/
|
||||
@NotEmpty(message = "商品描述信息不能为空")
|
||||
@Length(max = 128, message = "商品描述信息长度不能超过128")
|
||||
private String body;
|
||||
/**
|
||||
@ -63,7 +64,7 @@ public class PayOrderUnifiedReqDTO {
|
||||
*/
|
||||
@NotNull(message = "支付金额不能为空")
|
||||
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
||||
private Integer amount;
|
||||
private Integer price;
|
||||
|
||||
/**
|
||||
* 支付过期时间
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -24,33 +24,20 @@ import javax.validation.constraints.NotNull;
|
||||
public class PayRefundUnifiedReqDTO {
|
||||
|
||||
/**
|
||||
* 用户 IP
|
||||
* 外部订单号
|
||||
*
|
||||
* 对应 PayOrderExtensionDO 的 no 字段
|
||||
*/
|
||||
private String userIp;
|
||||
|
||||
// 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;
|
||||
@NotEmpty(message = "外部订单编号不能为空")
|
||||
private String outTradeNo;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@NotEmpty(message = "退款请求单号不能为空")
|
||||
private String outRefundNo;
|
||||
|
||||
/**
|
||||
* 退款原因
|
||||
@ -58,16 +45,25 @@ public class PayRefundUnifiedReqDTO {
|
||||
@NotEmpty(message = "退款原因不能为空")
|
||||
private String reason;
|
||||
|
||||
/**
|
||||
* 支付金额,单位:分
|
||||
*
|
||||
* 目前微信支付在退款的时候,必须传递该字段
|
||||
*/
|
||||
@NotNull(message = "支付金额不能为空")
|
||||
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
||||
private Integer payPrice;
|
||||
/**
|
||||
* 退款金额,单位:分
|
||||
*/
|
||||
@NotNull(message = "退款金额不能为空")
|
||||
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
|
||||
private Integer amount;
|
||||
private Integer refundPrice;
|
||||
|
||||
/**
|
||||
* 退款结果 notify 回调地址, 支付宝退款不需要回调地址, 微信需要
|
||||
* 退款结果的 notify 回调地址
|
||||
*/
|
||||
@NotEmpty(message = "支付结果的回调地址不能为空")
|
||||
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
|
||||
private String notifyUrl;
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -1,16 +1,17 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.exception.ServiceException;
|
||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
|
||||
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.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.PayRefundUnifiedRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.validation.Validation;
|
||||
import java.util.Map;
|
||||
|
||||
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;
|
||||
/**
|
||||
* 支付配置
|
||||
@ -46,7 +48,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
*/
|
||||
public final void init() {
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
log.info("[refresh][配置({})发生变化,重新初始化]", config);
|
||||
log.info("[refresh][客户端({})发生变化,重新初始化]", getId());
|
||||
this.config = config;
|
||||
// 初始化
|
||||
this.init();
|
||||
@ -70,32 +72,47 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
return channelId;
|
||||
}
|
||||
|
||||
// ============ 支付相关 ==========
|
||||
|
||||
@Override
|
||||
public final PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO);
|
||||
public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
|
||||
ValidationUtils.validate(reqDTO);
|
||||
// 执行统一下单
|
||||
PayOrderUnifiedRespDTO resp;
|
||||
PayOrderRespDTO resp;
|
||||
try {
|
||||
resp = doUnifiedOrder(reqDTO);
|
||||
} catch (ServiceException ex) {
|
||||
// 业务异常,都是实现类已经翻译,所以直接抛出即可
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
// 系统异常,则包装成 PayException 异常抛出
|
||||
log.error("[unifiedRefund][request({}) 发起支付异常]", toJsonString(reqDTO), ex);
|
||||
throw buildException(ex);
|
||||
log.error("[unifiedRefund][客户端({}) request({}) 发起支付异常]",
|
||||
getId(), toJsonString(reqDTO), ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
protected abstract PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
|
||||
protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
|
||||
throws Throwable;
|
||||
|
||||
@Override
|
||||
public PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO);
|
||||
public PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
|
||||
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 {
|
||||
resp = doUnifiedRefund(reqDTO);
|
||||
} catch (ServiceException ex) {
|
||||
@ -103,17 +120,18 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
// 系统异常,则包装成 PayException 异常抛出
|
||||
log.error("[unifiedRefund][request({}) 发起退款异常]", toJsonString(reqDTO), ex);
|
||||
throw buildException(ex);
|
||||
log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]",
|
||||
getId(), toJsonString(reqDTO), ex);
|
||||
throw buildPayException(ex);
|
||||
}
|
||||
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) {
|
||||
return (PayException) ex;
|
||||
}
|
||||
|
@ -2,16 +2,19 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
|
||||
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.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.refund.PayNotifyRefundStatusEnum;
|
||||
import com.alipay.api.*;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
|
||||
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.internal.util.AlipaySignature;
|
||||
import com.alipay.api.request.AlipayTradeRefundRequest;
|
||||
@ -22,11 +25,11 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0;
|
||||
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);
|
||||
}
|
||||
|
||||
// ============ 支付相关 ==========
|
||||
|
||||
/**
|
||||
* 构造支付关闭的 {@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
|
||||
*
|
||||
* @param reqDTO 退款请求 request DTO
|
||||
* @return 退款请求 Response
|
||||
*/
|
||||
@Override
|
||||
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
AlipayTradeRefundModel model=new AlipayTradeRefundModel();
|
||||
model.setTradeNo(reqDTO.getChannelOrderNo());
|
||||
model.setOutTradeNo(reqDTO.getPayTradeNo());
|
||||
|
||||
model.setOutRequestNo(reqDTO.getMerchantRefundId());
|
||||
model.setRefundAmount(formatAmount(reqDTO.getAmount()));
|
||||
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
// 1.1 构建 AlipayTradeRefundModel 请求
|
||||
AlipayTradeRefundModel model = new AlipayTradeRefundModel();
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setOutRequestNo(reqDTO.getOutRefundNo());
|
||||
model.setRefundAmount(formatAmount(reqDTO.getRefundPrice()));
|
||||
model.setRefundReason(reqDTO.getReason());
|
||||
|
||||
AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
|
||||
refundRequest.setBizModel(model);
|
||||
refundRequest.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
refundRequest.setReturnUrl(reqDTO.getNotifyUrl());
|
||||
// 1.2 构建 AlipayTradePayRequest 请求
|
||||
AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
|
||||
request.setBizModel(model);
|
||||
try {
|
||||
AlipayTradeRefundResponse response = client.execute(refundRequest);
|
||||
log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response));
|
||||
// 2.1 执行请求
|
||||
AlipayTradeRefundResponse response = client.execute(request);
|
||||
// 2.2 创建返回结果
|
||||
PayRefundRespDTO refund = new PayRefundRespDTO()
|
||||
.setOutRefundNo(reqDTO.getOutRefundNo())
|
||||
.setRawData(response);
|
||||
// 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。
|
||||
// 另外,支付宝没有退款单号,所以不用设置
|
||||
if (response.isSuccess()) {
|
||||
//退款导致触发的异步通知是发送到支付接口中设置的notify_url
|
||||
//支付宝不返回退款单号,设置为空
|
||||
PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
|
||||
respDTO.setChannelRefundId("");
|
||||
// return PayCommonResult.build(response.getCode(), response.getMsg(), respDTO, codeMapping); TODO
|
||||
return null;
|
||||
refund.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
|
||||
.setSuccessTime(LocalDateTimeUtil.of(response.getGmtRefundPay()));
|
||||
Assert.notNull(refund.getSuccessTime(), "退款成功时间不能为空");
|
||||
} else {
|
||||
refund.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus());
|
||||
}
|
||||
// 失败。需要抛出异常
|
||||
// return PayCommonResult.build(response.getCode(), response.getMsg(), null, codeMapping); TODO
|
||||
return null;
|
||||
return refund;
|
||||
} catch (AlipayApiException e) {
|
||||
// TODO 记录异常日志
|
||||
log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);
|
||||
// return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping); TODO
|
||||
log.error("[doUnifiedRefund][request({}) 发起退款异常]", toJsonString(reqDTO), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public Object parseNotify(PayNotifyReqDTO rawNotify) {
|
||||
// 1. 校验回调数据
|
||||
String body = rawNotify.getBody();
|
||||
Map<String, String> params = rawNotify.getParams();
|
||||
Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
|
||||
AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
|
||||
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();
|
||||
public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
|
||||
// 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
|
||||
// ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
|
||||
// ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
|
||||
// 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。
|
||||
// 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。
|
||||
throw new UnsupportedOperationException("支付宝无退款回调");
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
@ -132,21 +151,4 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
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.PayOrderUnifiedRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
@ -27,14 +27,14 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradeAppPayModel 请求
|
||||
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getMerchantOrderId());
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setProductCode(" QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品
|
||||
// ② 个性化的参数【无】
|
||||
// ③ 支付宝扫码支付只有一种展示
|
||||
@ -49,8 +49,11 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
|
||||
// 2.1 执行请求
|
||||
AlipayTradeAppPayResponse response = client.execute(request);
|
||||
// 2.2 处理结果
|
||||
validateUnifiedOrderResponse(request, response);
|
||||
return new PayOrderUnifiedRespDTO(displayMode, "");
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
return new PayOrderRespDTO(displayMode, "",
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
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.PayOrderUnifiedRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
@ -30,7 +30,7 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code");
|
||||
if (StrUtil.isEmpty(authCode)) {
|
||||
throw exception0(BAD_REQUEST.getCode(), "条形码不能为空");
|
||||
@ -39,10 +39,10 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
|
||||
// 1.1 构建 AlipayTradePayModel 请求
|
||||
AlipayTradePayModel model = new AlipayTradePayModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getMerchantOrderId());
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setScene("bar_code"); // 当面付条码支付场景
|
||||
// ② 个性化的参数
|
||||
model.setAuthCode(authCode);
|
||||
@ -55,11 +55,15 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
request.setReturnUrl(reqDTO.getReturnUrl());
|
||||
|
||||
// TODO 芋艿:各种边界的处理
|
||||
// 2.1 执行请求
|
||||
AlipayTradePayResponse response = client.execute(request);
|
||||
// 2.2 处理结果
|
||||
validateUnifiedOrderResponse(request, response);
|
||||
return new PayOrderUnifiedRespDTO(displayMode, "");
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
return new PayOrderRespDTO(displayMode, "",
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
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.PayOrderUnifiedRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
@ -29,14 +29,14 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradePagePayModel 请求
|
||||
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getMerchantOrderId());
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
|
||||
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());
|
||||
}
|
||||
// 2.2 处理结果
|
||||
validateUnifiedOrderResponse(request, response);
|
||||
return new PayOrderUnifiedRespDTO(displayMode, response.getBody());
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
return new PayOrderRespDTO(displayMode, response.getBody(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
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.PayOrderUnifiedRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
@ -25,14 +25,14 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradePrecreateModel 请求
|
||||
AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getMerchantOrderId());
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT
|
||||
// ② 个性化的参数【无】
|
||||
// ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开
|
||||
@ -47,8 +47,11 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
|
||||
// 2.1 执行请求
|
||||
AlipayTradePrecreateResponse response = client.execute(request);
|
||||
// 2.2 处理结果
|
||||
validateUnifiedOrderResponse(request, response);
|
||||
return new PayOrderUnifiedRespDTO(displayMode, response.getQrCode());
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
return new PayOrderRespDTO(displayMode, response.getQrCode(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
|
||||
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.PayOrderUnifiedRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
@ -26,14 +26,14 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
|
||||
// 1.1 构建 AlipayTradeWapPayModel 请求
|
||||
AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
|
||||
// ① 通用的参数
|
||||
model.setOutTradeNo(reqDTO.getMerchantOrderId());
|
||||
model.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
model.setSubject(reqDTO.getSubject());
|
||||
model.setBody(reqDTO.getBody());
|
||||
model.setTotalAmount(formatAmount(reqDTO.getAmount()));
|
||||
model.setTotalAmount(formatAmount(reqDTO.getPrice()));
|
||||
model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
|
||||
// ② 个性化的参数【无】
|
||||
// ③ 支付宝 Wap 支付只有一种展示:URL
|
||||
@ -48,10 +48,12 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
|
||||
|
||||
// 2.1 执行请求
|
||||
AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name());
|
||||
|
||||
// 2.2 处理结果
|
||||
validateUnifiedOrderResponse(request, response);
|
||||
return new PayOrderUnifiedRespDTO(displayMode, response.getBody());
|
||||
if (!response.isSuccess()) {
|
||||
return buildClosedPayOrderRespDTO(reqDTO, response);
|
||||
}
|
||||
return new PayOrderRespDTO(displayMode, response.getBody(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,19 +1,26 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.date.TemporalAccessorUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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.notify.PayOrderNotifyRespDTO;
|
||||
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.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.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.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.exception.WxPayException;
|
||||
import com.github.binarywang.wxpay.service.WxPayService;
|
||||
@ -22,12 +29,11 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static cn.hutool.core.date.DatePattern.PURE_DATETIME_PATTERN;
|
||||
import static cn.hutool.core.date.DatePattern.UTC_WITH_XXX_OFFSET_PATTERN;
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.*;
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
import static cn.hutool.core.date.DatePattern.*;
|
||||
import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2;
|
||||
|
||||
/**
|
||||
* 微信支付抽象类,实现微信统一的接口、以及部分实现(退款)
|
||||
@ -53,12 +59,14 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
|
||||
WxPayConfig payConfig = new WxPayConfig();
|
||||
BeanUtil.copyProperties(config, payConfig, "keyContent");
|
||||
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())) {
|
||||
// weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
|
||||
payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
|
||||
}
|
||||
if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
|
||||
// weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
|
||||
payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
|
||||
}
|
||||
|
||||
@ -67,11 +75,13 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
|
||||
client.setConfig(payConfig);
|
||||
}
|
||||
|
||||
// ============ 支付相关 ==========
|
||||
|
||||
@Override
|
||||
protected PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
|
||||
protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
|
||||
try {
|
||||
switch (config.getApiVersion()) {
|
||||
case WxPayClientConfig.API_VERSION_V2:
|
||||
case API_VERSION_V2:
|
||||
return doUnifiedOrderV2(reqDTO);
|
||||
case WxPayClientConfig.API_VERSION_V3:
|
||||
return doUnifiedOrderV3(reqDTO);
|
||||
@ -79,7 +89,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
} 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 下单信息
|
||||
* @return 各支付渠道的返回结果
|
||||
*/
|
||||
protected abstract PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
|
||||
throws WxPayException;
|
||||
protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
|
||||
throws Exception;
|
||||
|
||||
/**
|
||||
* 【V3】调用支付渠道,统一下单
|
||||
@ -98,87 +111,177 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
|
||||
* @param reqDTO 下单信息
|
||||
* @return 各支付渠道的返回结果
|
||||
*/
|
||||
protected abstract PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
|
||||
protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
|
||||
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
|
||||
public Object parseNotify(PayNotifyReqDTO rawNotify) {
|
||||
log.info("[parseNotify][微信支付回调 data 数据: {}]", rawNotify.getBody());
|
||||
protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
try {
|
||||
// 微信支付 v2 回调结果处理
|
||||
switch (config.getApiVersion()) {
|
||||
case WxPayClientConfig.API_VERSION_V2:
|
||||
return parseOrderNotifyV2(rawNotify);
|
||||
case API_VERSION_V2:
|
||||
return doUnifiedRefundV2(reqDTO);
|
||||
case WxPayClientConfig.API_VERSION_V3:
|
||||
return parseOrderNotifyV3(rawNotify);
|
||||
return doUnifiedRefundV3(reqDTO);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
|
||||
}
|
||||
} catch (WxPayException e) {
|
||||
log.error("[parseNotify][rawNotify({}) 解析失败]", toJsonString(rawNotify), e);
|
||||
// throw buildPayException(e);
|
||||
// todo 芋艿:异常的处理;
|
||||
// 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);
|
||||
// TODO 芋艿:缺一个异常翻译
|
||||
}
|
||||
}
|
||||
|
||||
private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyReqDTO data) throws WxPayException {
|
||||
WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody());
|
||||
Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");
|
||||
// 转换结果
|
||||
return PayOrderNotifyRespDTO
|
||||
.builder()
|
||||
.orderExtensionNo(notifyResult.getOutTradeNo())
|
||||
.channelOrderNo(notifyResult.getTransactionId())
|
||||
.channelUserId(notifyResult.getOpenid())
|
||||
.successTime(parseDateV2(notifyResult.getTimeEnd()))
|
||||
.build();
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private PayRefundRespDTO parseRefundNotifyV2(String body) throws WxPayException {
|
||||
// 1. 解析回调
|
||||
WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
|
||||
WxPayRefundNotifyResult.ReqInfo responseResult = response.getReqInfo();
|
||||
// 2. 构建结果
|
||||
PayRefundRespDTO notify = new PayRefundRespDTO()
|
||||
.setChannelRefundNo(responseResult.getRefundId())
|
||||
.setOutRefundNo(responseResult.getOutRefundNo())
|
||||
.setRawData(response);
|
||||
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 {
|
||||
WxPayOrderNotifyV3Result notifyResult = client.parseOrderNotifyV3Result(data.getBody(), null);
|
||||
WxPayOrderNotifyV3Result.DecryptNotifyResult result = notifyResult.getResult();
|
||||
// 转换结果
|
||||
Assert.isTrue(Objects.equals(notifyResult.getResult().getTradeState(), "SUCCESS"),
|
||||
"支付结果非 SUCCESS");
|
||||
return PayOrderNotifyRespDTO.builder()
|
||||
.orderExtensionNo(result.getOutTradeNo())
|
||||
.channelOrderNo(result.getTradeState())
|
||||
.channelUserId(result.getPayer() != null ? result.getPayer().getOpenid() : null)
|
||||
.successTime(parseDateV3(result.getSuccessTime()))
|
||||
.build();
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException {
|
||||
// 1. 解析回调
|
||||
WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null);
|
||||
WxPayRefundNotifyV3Result.DecryptNotifyResult responseResult = response.getResult();
|
||||
// 2. 构建结果
|
||||
PayRefundRespDTO notify = new PayRefundRespDTO()
|
||||
.setChannelRefundNo(responseResult.getRefundId())
|
||||
.setOutRefundNo(responseResult.getOutRefundNo())
|
||||
.setRawData(response);
|
||||
if (Objects.equals("SUCCESS", responseResult.getRefundStatus())) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
static LocalDateTime parseDateV2B(String time) {
|
||||
return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN);
|
||||
}
|
||||
|
||||
static String formatDateV3(LocalDateTime time) {
|
||||
return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN);
|
||||
}
|
||||
@ -195,4 +302,24 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
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.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 com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
@ -21,17 +19,12 @@ public class WxAppPayClient extends AbstractWxPayClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -4,12 +4,8 @@ import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
|
||||
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.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.order.PayOrderDisplayModeEnum;
|
||||
import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
|
||||
@ -23,6 +19,7 @@ import java.time.LocalDateTime;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
* 微信支付【付款码支付】的 PayClient 实现类
|
||||
@ -49,7 +46,7 @@ public class WxBarPayClient extends AbstractWxPayClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 由于付款码需要不断轮询,所以需要在较短的时间完成支付
|
||||
LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE);
|
||||
if (expireTime.isAfter(reqDTO.getExpireTime())) {
|
||||
@ -57,29 +54,25 @@ public class WxBarPayClient extends AbstractWxPayClient {
|
||||
}
|
||||
// 构建 WxPayMicropayRequest 对象
|
||||
WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
|
||||
.outTradeNo(reqDTO.getMerchantOrderId())
|
||||
.outTradeNo(reqDTO.getOutTradeNo())
|
||||
.body(reqDTO.getSubject())
|
||||
.detail(reqDTO.getBody())
|
||||
.totalFee(reqDTO.getAmount()) // 单位分
|
||||
.totalFee(reqDTO.getPrice()) // 单位分
|
||||
.timeExpire(formatDateV2(expireTime))
|
||||
.spbillCreateIp(reqDTO.getUserIp())
|
||||
.authCode(getAuthCode(reqDTO))
|
||||
.build();
|
||||
// 执行请求,重试直到失败(过期),或者成功
|
||||
WxPayException lastWxPayException = null;
|
||||
for (int i = 1; i < Byte.MAX_VALUE; i++) {
|
||||
try {
|
||||
WxPayMicropayResult response = client.micropay(request);
|
||||
// 支付成功(例如说,用户输入了密码)
|
||||
PayOrderNotifyRespDTO notify = PayOrderNotifyRespDTO.builder()
|
||||
.orderExtensionNo(response.getOutTradeNo())
|
||||
.channelOrderNo(response.getTransactionId())
|
||||
.channelUserId(response.getOpenid())
|
||||
.successTime(parseDateV2(response.getTimeEnd()))
|
||||
.build();
|
||||
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.BAR_CODE.getMode(),
|
||||
JsonUtils.toJsonString(response))
|
||||
.setNotify(notify);
|
||||
// 支付成功,例如说:1)用户输入了密码;2)
|
||||
return new PayOrderRespDTO(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
|
||||
response.getOutTradeNo(), response)
|
||||
.setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode());
|
||||
} catch (WxPayException ex) {
|
||||
lastWxPayException = ex;
|
||||
// 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理
|
||||
// 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。
|
||||
// 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
|
||||
@ -89,23 +82,18 @@ public class WxBarPayClient extends AbstractWxPayClient {
|
||||
}
|
||||
// 等待 5 秒,继续下一轮重新发起支付
|
||||
log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i,
|
||||
JsonUtils.toJsonString(request), ex.getMessage());
|
||||
toJsonString(request), ex.getMessage());
|
||||
ThreadUtil.sleep(5, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("微信 Bar 支付,重试多次失败");
|
||||
throw lastWxPayException;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
return doUnifiedOrderV2(reqDTO);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
||||
static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) {
|
||||
|
@ -1,9 +1,7 @@
|
||||
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.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 com.github.binarywang.wxpay.constant.WxPayConstants;
|
||||
import com.github.binarywang.wxpay.exception.WxPayException;
|
||||
@ -21,17 +19,12 @@ public class WxH5PayClient extends AbstractWxPayClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
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.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.order.PayOrderDisplayModeEnum;
|
||||
import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
|
||||
@ -34,14 +32,14 @@ public class WxNativePayClient extends AbstractWxPayClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
|
||||
.outTradeNo(reqDTO.getMerchantOrderId())
|
||||
.outTradeNo(reqDTO.getOutTradeNo())
|
||||
.body(reqDTO.getSubject())
|
||||
.detail(reqDTO.getBody())
|
||||
.totalFee(reqDTO.getAmount()) // 单位分
|
||||
.productId(reqDTO.getMerchantOrderId())
|
||||
.totalFee(reqDTO.getPrice()) // 单位分
|
||||
.productId(reqDTO.getOutTradeNo())
|
||||
.timeExpire(formatDateV2(reqDTO.getExpireTime()))
|
||||
.spbillCreateIp(reqDTO.getUserIp())
|
||||
.notifyUrl(reqDTO.getNotifyUrl())
|
||||
@ -50,31 +48,26 @@ public class WxNativePayClient extends AbstractWxPayClient {
|
||||
WxPayNativeOrderResult response = client.createOrder(request);
|
||||
|
||||
// 转换结果
|
||||
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.QR_CODE_URL.getMode(),
|
||||
response.getCodeUrl());
|
||||
return new PayOrderRespDTO(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
|
||||
request.setOutTradeNo(reqDTO.getMerchantOrderId());
|
||||
request.setDescription(reqDTO.getBody());
|
||||
request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分
|
||||
request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
|
||||
request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
|
||||
request.setNotifyUrl(reqDTO.getNotifyUrl());
|
||||
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request()
|
||||
.setOutTradeNo(reqDTO.getOutTradeNo())
|
||||
.setDescription(reqDTO.getSubject())
|
||||
.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())) // 单位分
|
||||
.setTimeExpire(formatDateV3(reqDTO.getExpireTime()))
|
||||
.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()))
|
||||
.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(),
|
||||
response.getCodeUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
|
||||
return null;
|
||||
return new PayOrderRespDTO(PayOrderDisplayModeEnum.QR_CODE.getMode(), response,
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -37,16 +37,17 @@ public class WxPayClientConfig implements PayClientConfig {
|
||||
*
|
||||
* 只有公众号或小程序需要该字段
|
||||
*/
|
||||
@NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
|
||||
private String appId;
|
||||
/**
|
||||
* 商户号
|
||||
*/
|
||||
@NotBlank(message = "商户号 不能为空", groups = {V2.class, V3.class})
|
||||
@NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class})
|
||||
private String mchId;
|
||||
/**
|
||||
* API 版本
|
||||
*/
|
||||
@NotBlank(message = "API 版本 不能为空", groups = {V2.class, V3.class})
|
||||
@NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class})
|
||||
private String apiVersion;
|
||||
|
||||
// ========== V2 版本的参数 ==========
|
||||
@ -54,36 +55,31 @@ public class WxPayClientConfig implements PayClientConfig {
|
||||
/**
|
||||
* 商户密钥
|
||||
*/
|
||||
@NotBlank(message = "商户密钥 不能为空", groups = V2.class)
|
||||
@NotBlank(message = "商户密钥不能为空", groups = V2.class)
|
||||
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 版本的参数 ==========
|
||||
/**
|
||||
* apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
|
||||
* 对应的字符串
|
||||
* 注意,可通过 {@link #main(String[])} 读取
|
||||
* apiclient_key.pem 证书文件的对应字符串
|
||||
*/
|
||||
@NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
|
||||
private String privateKeyContent;
|
||||
/**
|
||||
* apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
|
||||
* 对应的字符串
|
||||
* <p>
|
||||
* 注意,可通过 {@link #main(String[])} 读取
|
||||
* apiclient_cert.pem 证书文件的对应的字符串
|
||||
*/
|
||||
@NotBlank(message = "apiclient_cert 不能为空", groups = V3.class)
|
||||
private String privateCertContent;
|
||||
/**
|
||||
* apiV3 密钥值
|
||||
*/
|
||||
@NotBlank(message = "apiV3 密钥值 不能为空", groups = V3.class)
|
||||
@NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
|
||||
private String apiV3Key;
|
||||
|
||||
/**
|
||||
|
@ -2,11 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
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.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.order.PayOrderDisplayModeEnum;
|
||||
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 static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
* 微信支付(公众号)的 PayClient 实现类
|
||||
@ -44,13 +42,13 @@ public class WxPubPayClient extends AbstractWxPayClient {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
|
||||
.outTradeNo(reqDTO.getMerchantOrderId())
|
||||
.outTradeNo(reqDTO.getOutTradeNo())
|
||||
.body(reqDTO.getSubject())
|
||||
.detail(reqDTO.getBody())
|
||||
.totalFee(reqDTO.getAmount()) // 单位分
|
||||
.totalFee(reqDTO.getPrice()) // 单位分
|
||||
.timeExpire(formatDateV2(reqDTO.getExpireTime()))
|
||||
.spbillCreateIp(reqDTO.getUserIp())
|
||||
.openid(getOpenid(reqDTO))
|
||||
@ -60,17 +58,17 @@ public class WxPubPayClient extends AbstractWxPayClient {
|
||||
WxPayMpOrderResult response = client.createOrder(request);
|
||||
|
||||
// 转换结果
|
||||
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.CUSTOM.getMode(),
|
||||
JsonUtils.toJsonString(response));
|
||||
return new PayOrderRespDTO(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
|
||||
// 构建 WxPayUnifiedOrderRequest 对象
|
||||
WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
|
||||
request.setOutTradeNo(reqDTO.getMerchantOrderId());
|
||||
request.setOutTradeNo(reqDTO.getOutTradeNo());
|
||||
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.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
|
||||
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);
|
||||
|
||||
// 转换结果
|
||||
return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.CUSTOM.getMode(),
|
||||
JsonUtils.toJsonString(response));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
// TODO 需要实现
|
||||
throw new UnsupportedOperationException();
|
||||
return new PayOrderRespDTO(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
|
||||
reqDTO.getOutTradeNo(), response);
|
||||
}
|
||||
|
||||
// ========== 各种工具方法 ==========
|
||||
|
@ -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, "发起支付失败,原因:{}");
|
||||
|
||||
}
|
@ -18,8 +18,7 @@ public enum PayOrderDisplayModeEnum {
|
||||
QR_CODE("qr_code"), // 二维码的文字内容
|
||||
QR_CODE_URL("qr_code_url"), // 二维码的图片链接
|
||||
BAR_CODE("bar_code"), // 条形码
|
||||
APP("app"), // 应用【目前暂时用不到】
|
||||
CUSTOM("custom"), // 自定义:每种支付方式,做个性化处理;例如说,微信公众号支付时,调用 JSAPI 接口
|
||||
APP("app"), // 应用:Android、iOS、微信小程序、微信公众号等,需要做自定义处理的
|
||||
;
|
||||
|
||||
/**
|
||||
|
@ -3,10 +3,12 @@ package cn.iocoder.yudao.framework.pay.core.enums.order;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 渠道的支付状态枚举
|
||||
*
|
||||
* @author 遇到源码
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@ -20,4 +22,24 @@ public enum PayOrderStatusRespEnum {
|
||||
private final Integer status;
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -121,10 +121,10 @@ public class PayClientFactoryImplIntegrationTest {
|
||||
|
||||
private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() {
|
||||
PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO();
|
||||
reqDTO.setAmount(123);
|
||||
reqDTO.setPrice(123);
|
||||
reqDTO.setSubject("IPhone 13");
|
||||
reqDTO.setBody("biubiubiu");
|
||||
reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
|
||||
reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
|
||||
reqDTO.setUserIp("127.0.0.1");
|
||||
reqDTO.setNotifyUrl("http://127.0.0.1:8080");
|
||||
return reqDTO;
|
||||
|
@ -1,8 +1,6 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
|
||||
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.PayOrderUnifiedRespDTO;
|
||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.alipay.api.AlipayApiException;
|
||||
import com.alipay.api.DefaultAlipayClient;
|
||||
@ -74,8 +72,8 @@ public class AlipayQrPayClientTest extends BaseMockitoUnitTest {
|
||||
// 这里,设置可以直接随机整个对象。
|
||||
Long shopOrderId = System.currentTimeMillis();
|
||||
PayOrderUnifiedReqDTO reqDTO=new PayOrderUnifiedReqDTO();
|
||||
reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
|
||||
reqDTO.setAmount(1);
|
||||
reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
|
||||
reqDTO.setPrice(1);
|
||||
reqDTO.setBody("内容:" + shopOrderId);
|
||||
reqDTO.setSubject("标题:"+shopOrderId);
|
||||
String notify="http://niubi.natapp1.cc/api/pay/order/notify";
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -351,7 +351,7 @@ public class TradeAfterSaleServiceImpl implements TradeAfterSaleService, AfterSa
|
||||
public void afterCommit() {
|
||||
// 创建退款单
|
||||
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));
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ public interface PayRefundApi {
|
||||
* @param reqDTO 创建请求
|
||||
* @return 退款单编号
|
||||
*/
|
||||
Long createPayRefund(@Valid PayRefundCreateReqDTO reqDTO);
|
||||
Long createRefund(@Valid PayRefundCreateReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 获得退款单
|
||||
@ -26,6 +26,6 @@ public interface PayRefundApi {
|
||||
* @param id 退款单编号
|
||||
* @return 退款单
|
||||
*/
|
||||
PayRefundRespDTO getPayRefund(Long id);
|
||||
PayRefundRespDTO getRefund(Long id);
|
||||
|
||||
}
|
||||
|
@ -27,26 +27,32 @@ public class PayRefundCreateReqDTO {
|
||||
private String userIp;
|
||||
|
||||
// ========== 商户相关字段 ==========
|
||||
/**
|
||||
* 商户订单编号
|
||||
*/
|
||||
@NotEmpty(message = "商户订单编号不能为空")
|
||||
private String merchantOrderId;
|
||||
|
||||
/**
|
||||
* 商户退款编号
|
||||
*/
|
||||
@NotEmpty(message = "商户退款编号不能为空")
|
||||
private String merchantRefundId;
|
||||
|
||||
/**
|
||||
* 退款描述
|
||||
*/
|
||||
@NotEmpty(message = "退款描述不能为空")
|
||||
@Length(max = 128, message = "退款描述长度不能超过128")
|
||||
@Length(max = 128, message = "退款描述长度不能超过 128")
|
||||
private String reason;
|
||||
|
||||
// ========== 订单相关字段 ==========
|
||||
|
||||
/**
|
||||
* 支付单号
|
||||
*/
|
||||
@NotNull(message = "支付单号不能为空")
|
||||
private Long payOrderId;
|
||||
|
||||
/**
|
||||
* 退款金额,单位:分
|
||||
*/
|
||||
@NotNull(message = "退款金额不能为空")
|
||||
@Min(value = 1, message = "退款金额必须大于零")
|
||||
private Integer price;
|
||||
|
||||
}
|
||||
|
@ -27,17 +27,19 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PAY_ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付");
|
||||
ErrorCode PAY_ORDER_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007002002, "支付订单不处于已支付");
|
||||
ErrorCode PAY_ORDER_IS_EXPIRED = new ErrorCode(1007002003, "支付订单已经过期");
|
||||
ErrorCode PAY_ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1007002004, "发起支付报错,错误码:{},错误提示:{}");
|
||||
|
||||
// ========== ORDER 模块(拓展单) 1007003000 ==========
|
||||
ErrorCode PAY_ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在");
|
||||
ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付");
|
||||
|
||||
// ========== 支付模块(退款) 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_CHN_ORDER_NO_IS_NULL = new ErrorCode(1007006002, "该订单的渠道订单为空");
|
||||
ErrorCode PAY_REFUND_SUCCEED = new ErrorCode(1007006003, "已经退款成功");
|
||||
ErrorCode PAY_REFUND_HAS_REFUNDING = new ErrorCode(1007006002, "已经有退款在处理中");
|
||||
ErrorCode PAY_REFUND_EXISTS = new ErrorCode(1007006003, "已经存在退款单");
|
||||
ErrorCode PAY_REFUND_NOT_FOUND = new ErrorCode(1007006004, "支付退款单不存在");
|
||||
ErrorCode PAY_REFUND_STATUS_IS_NOT_WAITING = new ErrorCode(1007006005, "支付退款单不处于待退款");
|
||||
|
||||
// ========== 示例订单 1007900000 ==========
|
||||
ErrorCode PAY_DEMO_ORDER_NOT_FOUND = new ErrorCode(1007900000, "示例订单不存在");
|
||||
|
@ -38,4 +38,14 @@ public enum PayOrderStatusEnum implements IntArrayValuable {
|
||||
return Objects.equals(status, SUCCESS.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否支付关闭
|
||||
*
|
||||
* @param status 状态
|
||||
* @return 是否支付关闭
|
||||
*/
|
||||
public static boolean isClosed(Integer status) {
|
||||
return Objects.equals(status, CLOSED.getStatus());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,14 +5,18 @@ import lombok.Getter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 渠道的退款状态枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayRefundStatusEnum {
|
||||
|
||||
CREATE(0, "退款订单生成"),
|
||||
SUCCESS(1, "退款成功"),
|
||||
FAILURE(2, "退款失败"),
|
||||
CLOSE(99, "退款关闭");
|
||||
WAITING(0, "未退款"),
|
||||
SUCCESS(10, "退款成功"),
|
||||
FAILURE(20, "退款失败");
|
||||
|
||||
private final Integer status;
|
||||
private final String name;
|
||||
|
@ -22,7 +22,7 @@ public class PayOrderApiImpl implements PayOrderApi {
|
||||
|
||||
@Override
|
||||
public Long createOrder(PayOrderCreateReqDTO reqDTO) {
|
||||
return payOrderService.createPayOrder(reqDTO);
|
||||
return payOrderService.createOrder(reqDTO);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
|
||||
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@ -21,14 +22,13 @@ public class PayRefundApiImpl implements PayRefundApi {
|
||||
private PayRefundService payRefundService;
|
||||
|
||||
@Override
|
||||
public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
|
||||
public Long createRefund(PayRefundCreateReqDTO reqDTO) {
|
||||
return payRefundService.createPayRefund(reqDTO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayRefundRespDTO getPayRefund(Long id) {
|
||||
// TODO 芋艿:暂未实现
|
||||
return null;
|
||||
public PayRefundRespDTO getRefund(Long id) {
|
||||
return PayRefundConvert.INSTANCE.convert02(payRefundService.getRefund(id));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ public class PayAppBaseVO {
|
||||
@Schema(description = "支付结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/pay-callback")
|
||||
@NotNull(message = "支付结果的回调地址不能为空")
|
||||
@URL(message = "支付结果的回调地址必须为 URL 格式")
|
||||
private String payNotifyUrl;
|
||||
private String orderNotifyUrl;
|
||||
|
||||
@Schema(description = "退款结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/refund-callback")
|
||||
@NotNull(message = "退款结果的回调地址不能为空")
|
||||
|
@ -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.pay.core.client.PayClient;
|
||||
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.notify.PayOrderNotifyRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
|
||||
import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@ -19,7 +18,6 @@ import javax.annotation.security.PermitAll;
|
||||
import java.util.Map;
|
||||
|
||||
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;
|
||||
|
||||
@Tag(name = "管理后台 - 支付通知")
|
||||
@ -37,22 +35,14 @@ public class PayNotifyController {
|
||||
@Resource
|
||||
private PayClientFactory payClientFactory;
|
||||
|
||||
/**
|
||||
* 统一的渠道支付回调,支付宝的退款回调
|
||||
*
|
||||
* @param channelId 渠道编号
|
||||
* @param params form 参数
|
||||
* @param body request body
|
||||
* @return 成功返回 "success"
|
||||
*/
|
||||
@PostMapping(value = "/callback/{channelId}")
|
||||
@Operation(summary = "支付渠道的统一回调接口 - 包括支付回调,退款回调")
|
||||
@PostMapping(value = "/order/{channelId}")
|
||||
@Operation(summary = "支付渠道的统一【支付】回调")
|
||||
@PermitAll
|
||||
@OperateLog(enable = false) // 回调地址,无需记录操作日志
|
||||
public String notifyCallback(@PathVariable("channelId") Long channelId,
|
||||
@RequestParam(required = false) Map<String, String> params,
|
||||
@RequestBody(required = false) String body) {
|
||||
log.info("[notifyCallback][channelId({}) 回调数据({}/{})]", channelId, params, body);
|
||||
public String notifyOrder(@PathVariable("channelId") Long channelId,
|
||||
@RequestParam(required = false) Map<String, String> params,
|
||||
@RequestBody(required = false) String body) {
|
||||
log.info("[notifyOrder][channelId({}) 回调数据({}/{})]", channelId, params, body);
|
||||
// 1. 校验支付渠道是否存在
|
||||
PayClient payClient = payClientFactory.getPayClient(channelId);
|
||||
if (payClient == null) {
|
||||
@ -61,21 +51,30 @@ public class PayNotifyController {
|
||||
}
|
||||
|
||||
// 2. 解析通知数据
|
||||
PayNotifyReqDTO rawNotify = PayNotifyReqDTO.builder().params(params).body(body).build();
|
||||
Object notify = payClient.parseNotify(rawNotify);
|
||||
PayOrderRespDTO notify = payClient.parseOrderNotify(params, body);
|
||||
orderService.notifyOrder(channelId, notify);
|
||||
return "success";
|
||||
}
|
||||
|
||||
// 3. 处理通知
|
||||
// 3.1:退款通知
|
||||
if (notify instanceof PayRefundNotifyRespDTO) {
|
||||
refundService.notifyPayRefund(channelId, (PayRefundNotifyRespDTO) notify, rawNotify);
|
||||
return "success";
|
||||
@PostMapping(value = "/refund/{channelId}")
|
||||
@Operation(summary = "支付渠道的统一【退款】回调")
|
||||
@PermitAll
|
||||
@OperateLog(enable = false) // 回调地址,无需记录操作日志
|
||||
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) {
|
||||
orderService.notifyPayOrder(channelId, (PayOrderNotifyRespDTO) notify, rawNotify);
|
||||
return "success";
|
||||
}
|
||||
throw new UnsupportedOperationException("未知通知:" + toJsonString(notify));
|
||||
|
||||
// 2. 解析通知数据
|
||||
PayRefundRespDTO notify = payClient.parseRefundNotify(params, body);
|
||||
refundService.notifyRefund(channelId, notify);
|
||||
return "success";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ public class PayOrderController {
|
||||
@PostMapping("/submit")
|
||||
@Operation(summary = "提交支付订单")
|
||||
public CommonResult<PayOrderSubmitRespVO> submitPayOrder(@RequestBody PayOrderSubmitReqVO reqVO) {
|
||||
PayOrderSubmitRespVO respVO = payOrderService.submitPayOrder(reqVO, getClientIP());
|
||||
PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP());
|
||||
return success(respVO);
|
||||
}
|
||||
|
||||
|
@ -76,9 +76,6 @@ public class PayRefundExcelVO {
|
||||
@ExcelProperty("退款成功时间")
|
||||
private LocalDateTime successTime;
|
||||
|
||||
@ExcelProperty("退款通知时间")
|
||||
private LocalDateTime notifyTime;
|
||||
|
||||
@ExcelProperty("退款失效时间")
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
|
@ -40,7 +40,7 @@ public class AppPayOrderController {
|
||||
@PostMapping("/submit")
|
||||
@Operation(summary = "提交支付订单")
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -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.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.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*;
|
||||
@ -93,7 +92,8 @@ public interface PayOrderConvert {
|
||||
|
||||
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);
|
||||
|
||||
|
@ -1,12 +1,11 @@
|
||||
package cn.iocoder.yudao.module.pay.convert.refund;
|
||||
|
||||
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.dal.dataobject.order.PayOrderDO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Mappings;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@ -44,8 +43,6 @@ public interface PayRefundConvert {
|
||||
|
||||
PageResult<PayRefundRespVO> convertPage(PageResult<PayRefundDO> page);
|
||||
|
||||
List<PayRefundExcelVO> convertList02(List<PayRefundDO> list);
|
||||
|
||||
/**
|
||||
* 退款订单DO 转 导出excel VO
|
||||
*
|
||||
@ -60,20 +57,18 @@ public interface PayRefundConvert {
|
||||
PayRefundExcelVO payRefundExcelVO = new PayRefundExcelVO();
|
||||
|
||||
payRefundExcelVO.setId(bean.getId());
|
||||
payRefundExcelVO.setTradeNo(bean.getTradeNo());
|
||||
payRefundExcelVO.setTradeNo(bean.getNo());
|
||||
payRefundExcelVO.setMerchantOrderId(bean.getMerchantOrderId());
|
||||
payRefundExcelVO.setMerchantRefundNo(bean.getMerchantRefundNo());
|
||||
// TODO 芋艿:晚点在改
|
||||
// payRefundExcelVO.setMerchantRefundNo(bean.getMerchantRefundNo());
|
||||
payRefundExcelVO.setNotifyUrl(bean.getNotifyUrl());
|
||||
payRefundExcelVO.setNotifyStatus(bean.getNotifyStatus());
|
||||
payRefundExcelVO.setStatus(bean.getStatus());
|
||||
payRefundExcelVO.setType(bean.getType());
|
||||
payRefundExcelVO.setReason(bean.getReason());
|
||||
payRefundExcelVO.setUserIp(bean.getUserIp());
|
||||
payRefundExcelVO.setChannelOrderNo(bean.getChannelOrderNo());
|
||||
payRefundExcelVO.setChannelRefundNo(bean.getChannelRefundNo());
|
||||
payRefundExcelVO.setExpireTime(bean.getExpireTime());
|
||||
payRefundExcelVO.setSuccessTime(bean.getSuccessTime());
|
||||
payRefundExcelVO.setNotifyTime(bean.getNotifyTime());
|
||||
payRefundExcelVO.setCreateTime(bean.getCreateTime());
|
||||
|
||||
BigDecimal multiple = new BigDecimal(100);
|
||||
@ -85,12 +80,8 @@ public interface PayRefundConvert {
|
||||
return payRefundExcelVO;
|
||||
}
|
||||
|
||||
//TODO 太多需要处理了, 暂时不用
|
||||
@Mappings(value = {
|
||||
@Mapping(source = "price", target = "payPrice"),
|
||||
@Mapping(source = "id", target = "orderId"),
|
||||
@Mapping(target = "status",ignore = true)
|
||||
})
|
||||
PayRefundDO convert(PayOrderDO orderDO);
|
||||
PayRefundDO convert(PayRefundCreateReqDTO bean);
|
||||
|
||||
PayRefundRespDTO convert02(PayRefundDO bean);
|
||||
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ public class PayAppDO extends BaseDO {
|
||||
/**
|
||||
* 支付结果的回调地址
|
||||
*/
|
||||
private String payNotifyUrl;
|
||||
private String orderNotifyUrl;
|
||||
/**
|
||||
* 退款结果的回调地址
|
||||
*/
|
||||
|
@ -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.enums.order.PayOrderNotifyStatusEnum;
|
||||
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.TableName;
|
||||
import lombok.*;
|
||||
@ -55,7 +55,8 @@ public class PayOrderDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 商户订单编号
|
||||
* 例如说,内部系统 A 的订单号。需要保证每个 PayMerchantDO 唯一
|
||||
*
|
||||
* 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一
|
||||
*/
|
||||
private String merchantOrderId;
|
||||
/**
|
||||
@ -126,7 +127,7 @@ public class PayOrderDO extends BaseDO {
|
||||
/**
|
||||
* 退款状态
|
||||
*
|
||||
* 枚举 {@link PayRefundTypeEnum}
|
||||
* 枚举 {@link PayOrderRefundStatusEnum}
|
||||
*/
|
||||
private Integer refundStatus;
|
||||
/**
|
||||
@ -136,7 +137,7 @@ public class PayOrderDO extends BaseDO {
|
||||
/**
|
||||
* 退款总金额,单位:分
|
||||
*/
|
||||
private Long refundPrice;
|
||||
private Integer refundPrice;
|
||||
|
||||
// ========== 渠道相关字段 ==========
|
||||
/**
|
||||
|
@ -1,5 +1,6 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.order;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
|
||||
import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
@ -32,10 +33,11 @@ public class PayOrderExtensionDO extends BaseDO {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@ -64,20 +66,29 @@ public class PayOrderExtensionDO extends BaseDO {
|
||||
* 支付状态
|
||||
*
|
||||
* 枚举 {@link PayOrderStatusEnum}
|
||||
* 注意,只包含上述枚举的 WAITING 和 SUCCESS
|
||||
*/
|
||||
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)
|
||||
private Map<String, String> channelExtras;
|
||||
|
||||
/**
|
||||
* 支付渠道异步通知的内容
|
||||
* 调用渠道的错误码
|
||||
*/
|
||||
private String channelErrorCode;
|
||||
/**
|
||||
* 调用渠道报错时,错误信息
|
||||
*/
|
||||
private String channelErrorMsg;
|
||||
|
||||
/**
|
||||
* 支付渠道的同步/异步通知的内容
|
||||
*
|
||||
* 在支持成功后,会记录回调的数据
|
||||
* 对应 {@link PayOrderRespDTO#getRawData()}
|
||||
*/
|
||||
private String channelNotifyData;
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
package cn.iocoder.yudao.module.pay.dal.dataobject.refund;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
|
||||
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.enums.notify.PayNotifyStatusEnum;
|
||||
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.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
@ -37,6 +38,14 @@ public class PayRefundDO extends BaseDO {
|
||||
*/
|
||||
@TableId
|
||||
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;
|
||||
|
||||
/**
|
||||
* 交易订单号,根据规则生成
|
||||
* 调用支付渠道时,使用该字段作为对接的订单号。
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 商户退款订单号, 由商户系统产生, 由他们保证唯一,不能为空,通知商户时会传该字段。
|
||||
* 例如说,内部系统 A 的退款订单号。需要保证每个 PayMerchantDO 唯一
|
||||
* 个商户退款订单,对应一条退款请求记录。可多次提交。 渠道保持幂等
|
||||
* 使用商户退款单,作为退款请求号
|
||||
* 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
|
||||
* 商户退款订单号
|
||||
*
|
||||
* 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一
|
||||
*/
|
||||
// TODO @jason:merchantRefundNo =》merchantRefundOId
|
||||
private String merchantRefundNo;
|
||||
|
||||
private String merchantRefundId;
|
||||
/**
|
||||
* 异步通知地址
|
||||
*/
|
||||
private String notifyUrl;
|
||||
|
||||
/**
|
||||
* 通知商户退款结果的回调状态
|
||||
* TODO 0 未发送 1 已发送
|
||||
*
|
||||
* 枚举 {@link PayNotifyStatusEnum}
|
||||
*/
|
||||
private Integer notifyStatus;
|
||||
|
||||
@ -115,12 +104,6 @@ public class PayRefundDO extends BaseDO {
|
||||
*/
|
||||
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;
|
||||
/**
|
||||
* 微信中的 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 LocalDateTime successTime;
|
||||
|
||||
/**
|
||||
* 调用渠道的错误码
|
||||
*/
|
||||
private String channelErrorCode;
|
||||
|
||||
/**
|
||||
* 调用渠道报错时,错误信息
|
||||
*/
|
||||
private String channelErrorMsg;
|
||||
|
||||
/**
|
||||
* 支付渠道的额外参数
|
||||
* 参见 https://www.pingxx.com/api/Refunds%20退款概述.html
|
||||
* 支付渠道的同步/异步通知的内容
|
||||
*
|
||||
* 对应 {@link PayRefundRespDTO#getRawData()}
|
||||
*/
|
||||
private String channelExtras;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* 退款失效时间
|
||||
*/
|
||||
private LocalDateTime expireTime;
|
||||
/**
|
||||
* 退款成功时间
|
||||
*/
|
||||
private LocalDateTime successTime;
|
||||
/**
|
||||
* 退款通知时间
|
||||
*/
|
||||
private LocalDateTime notifyTime;
|
||||
private String channelNotifyData;
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
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.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.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 com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
@ -13,6 +15,34 @@ import java.util.List;
|
||||
@Mapper
|
||||
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) {
|
||||
return selectPage(reqVO, new QueryWrapperX<PayRefundDO>()
|
||||
.eqIfPresent("app_id", reqVO.getAppId())
|
||||
@ -37,16 +67,4 @@ public interface PayRefundMapper extends BaseMapperX<PayRefundDO> {
|
||||
.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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 lombok.AllArgsConstructor;
|
||||
@ -11,10 +11,10 @@ import lombok.Getter;
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PayRefundTypeEnum implements IntArrayValuable {
|
||||
public enum PayOrderRefundStatusEnum implements IntArrayValuable {
|
||||
|
||||
NO(0, "未退款"),
|
||||
SOME(10, "部分退款"),
|
||||
PART(10, "部分退款"),
|
||||
ALL(20, "全部退款")
|
||||
;
|
||||
|
@ -20,11 +20,11 @@ import javax.annotation.Resource;
|
||||
public class PayNotifyJob implements JobHandler {
|
||||
|
||||
@Resource
|
||||
private PayNotifyService payNotifyCoreService;
|
||||
private PayNotifyService payNotifyService;
|
||||
|
||||
@Override
|
||||
public String execute(String param) throws Exception {
|
||||
int notifyCount = payNotifyCoreService.executeNotify();
|
||||
int notifyCount = payNotifyService.executeNotify();
|
||||
return String.format("执行支付通知 %s 个", notifyCount);
|
||||
}
|
||||
|
||||
|
@ -83,17 +83,20 @@ public class PayChannelServiceImpl implements PayChannelService {
|
||||
*/
|
||||
@Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
|
||||
public void refreshLocalCache() {
|
||||
// 情况一:如果缓存里没有数据,则直接刷新缓存
|
||||
if (CollUtil.isEmpty(channelCache)) {
|
||||
initLocalCache();
|
||||
return;
|
||||
}
|
||||
// 注意:忽略自动多租户,因为要全局初始化缓存
|
||||
TenantUtils.executeIgnore(() -> {
|
||||
// 情况一:如果缓存里没有数据,则直接刷新缓存
|
||||
if (CollUtil.isEmpty(channelCache)) {
|
||||
initLocalCache();
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存
|
||||
LocalDateTime maxTime = CollectionUtils.getMaxValue(channelCache, PayChannelDO::getUpdateTime);
|
||||
if (channelMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
|
||||
initLocalCache();
|
||||
}
|
||||
// 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存
|
||||
LocalDateTime maxTime = CollectionUtils.getMaxValue(channelCache, PayChannelDO::getUpdateTime);
|
||||
if (channelMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
|
||||
initLocalCache();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -184,12 +184,17 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
|
||||
// 1. 校验订单是否可以退款
|
||||
PayDemoOrderDO order = validateDemoOrderCanRefund(id);
|
||||
|
||||
// 2.1 创建退款单
|
||||
Long payRefundId = payRefundApi.createPayRefund(new PayRefundCreateReqDTO()
|
||||
// 2.1 生成退款单号
|
||||
// 一般来说,用户发起退款的时候,都会单独插入一个售后维权表,然后使用该表的 id 作为 refundId
|
||||
// 这里我们是个简单的 demo,所以没有售后维权表,直接使用订单 id + "-refund" 来演示
|
||||
String refundId = order.getId() + "-refund";
|
||||
// 2.2 创建退款单
|
||||
Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO()
|
||||
.setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
|
||||
.setPayOrderId(order.getPayOrderId()) // 支付单号
|
||||
.setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
|
||||
.setMerchantRefundId(refundId)
|
||||
.setReason("想退钱").setPrice(order.getPrice()));// 价格信息
|
||||
// 2.2 更新退款单到 demo 订单
|
||||
// 2.3 更新退款单到 demo 订单
|
||||
payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id)
|
||||
.setPayRefundId(payRefundId).setRefundPrice(order.getPrice()));
|
||||
}
|
||||
@ -234,7 +239,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
|
||||
}
|
||||
|
||||
// 2.1 校验退款订单
|
||||
PayRefundRespDTO payRefund = payRefundApi.getPayRefund(payRefundId);
|
||||
PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId);
|
||||
if (payRefund == null) {
|
||||
throw exception(PAY_DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
// TODO 芋艿:合并到 PayOrder 里;
|
||||
/**
|
||||
* 支付订单 Service 接口
|
||||
*
|
||||
|
@ -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.util.collection.CollectionUtils;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
|
||||
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO;
|
||||
@ -32,6 +31,15 @@ public interface PayOrderService {
|
||||
*/
|
||||
PayOrderDO getOrder(Long id);
|
||||
|
||||
/**
|
||||
* 获得支付订单
|
||||
*
|
||||
* @param appId 应用编号
|
||||
* @param merchantOrderId 商户订单编号
|
||||
* @return 支付订单
|
||||
*/
|
||||
PayOrderDO getOrder(Long appId, String merchantOrderId);
|
||||
|
||||
/**
|
||||
* 获得指定应用的订单数量
|
||||
*
|
||||
@ -71,11 +79,11 @@ public interface PayOrderService {
|
||||
/**
|
||||
* 根据订单 ID 集合获取订单商品名称Map集合
|
||||
*
|
||||
* @param idList 订单 ID 集合
|
||||
* @param ids 订单 ID 集合
|
||||
* @return 订单商品 map 集合
|
||||
*/
|
||||
default Map<Long, PayOrderDO> getOrderSubjectMap(Collection<Long> idList) {
|
||||
List<PayOrderDO> list = getOrderSubjectList(idList);
|
||||
default Map<Long, PayOrderDO> getOrderSubjectMap(Collection<Long> ids) {
|
||||
List<PayOrderDO> list = getOrderSubjectList(ids);
|
||||
return CollectionUtils.convertMap(list, PayOrderDO::getId);
|
||||
}
|
||||
|
||||
@ -85,7 +93,7 @@ public interface PayOrderService {
|
||||
* @param reqDTO 创建请求
|
||||
* @return 支付单编号
|
||||
*/
|
||||
Long createPayOrder(@Valid PayOrderCreateReqDTO reqDTO);
|
||||
Long createOrder(@Valid PayOrderCreateReqDTO reqDTO);
|
||||
|
||||
/**
|
||||
* 提交支付
|
||||
@ -95,16 +103,23 @@ public interface PayOrderService {
|
||||
* @param userIp 提交 IP
|
||||
* @return 提交结果
|
||||
*/
|
||||
PayOrderSubmitRespVO submitPayOrder(@Valid PayOrderSubmitReqVO reqVO,
|
||||
@NotEmpty(message = "提交 IP 不能为空") String userIp);
|
||||
PayOrderSubmitRespVO submitOrder(@Valid PayOrderSubmitReqVO reqVO,
|
||||
@NotEmpty(message = "提交 IP 不能为空") String userIp);
|
||||
|
||||
/**
|
||||
* 通知支付单成功
|
||||
*
|
||||
* @param channelId 渠道编号
|
||||
* @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);
|
||||
|
||||
}
|
||||
|
@ -4,15 +4,15 @@ import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.lang.Pair;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
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.util.date.LocalDateTimeUtils;
|
||||
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.PayClientFactory;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
|
||||
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.PayOrderUnifiedRespDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
|
||||
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||
import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
|
||||
@ -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.mysql.order.PayOrderExtensionMapper;
|
||||
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.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.refund.PayRefundTypeEnum;
|
||||
import cn.iocoder.yudao.module.pay.service.app.PayAppService;
|
||||
import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
|
||||
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
|
||||
@ -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.util.json.JsonUtils.toJsonString;
|
||||
import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 支付订单 Service 实现类
|
||||
@ -82,6 +82,11 @@ public class PayOrderServiceImpl implements PayOrderService {
|
||||
return orderMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayOrderDO getOrder(Long appId, String merchantOrderId) {
|
||||
return orderMapper.selectByAppIdAndMerchantOrderId(appId, merchantOrderId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getOrderCountByAppId(Long appId) {
|
||||
return orderMapper.selectCountByAppId(appId);
|
||||
@ -104,7 +109,7 @@ public class PayOrderServiceImpl implements PayOrderService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long createPayOrder(PayOrderCreateReqDTO reqDTO) {
|
||||
public Long createOrder(PayOrderCreateReqDTO reqDTO) {
|
||||
// 校验 App
|
||||
PayAppDO app = appService.validPayApp(reqDTO.getAppId());
|
||||
|
||||
@ -112,7 +117,7 @@ public class PayOrderServiceImpl implements PayOrderService {
|
||||
PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId(
|
||||
reqDTO.getAppId(), reqDTO.getMerchantOrderId());
|
||||
if (order != null) {
|
||||
log.warn("[createPayOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
|
||||
log.warn("[createOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
|
||||
order.getMerchantOrderId(), toJsonString(order)); // 理论来说,不会出现这个情况
|
||||
return order.getId();
|
||||
}
|
||||
@ -120,20 +125,19 @@ public class PayOrderServiceImpl implements PayOrderService {
|
||||
// 创建支付交易单
|
||||
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())
|
||||
// 退款相关字段
|
||||
.setRefundStatus(PayRefundTypeEnum.NO.getStatus()).setRefundTimes(0).setRefundPrice(0L);
|
||||
.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus()).setRefundTimes(0).setRefundPrice(0);
|
||||
orderMapper.insert(order);
|
||||
return order.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PayOrderSubmitRespVO submitPayOrder(PayOrderSubmitReqVO reqVO, String userIp) {
|
||||
@Override // 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtensionDO 回滚了
|
||||
public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) {
|
||||
// 1. 获得 PayOrderDO ,并校验其是否存在
|
||||
PayOrderDO order = validatePayOrderCanSubmit(reqVO.getId());
|
||||
PayOrderDO order = validateOrderCanSubmit(reqVO.getId());
|
||||
// 1.2 校验支付渠道是否有效
|
||||
PayChannelDO channel = validatePayChannelCanSubmit(order.getAppId(), reqVO.getChannelCode());
|
||||
PayClient client = payClientFactory.getPayClient(channel.getId());
|
||||
@ -148,35 +152,38 @@ public class PayOrderServiceImpl implements PayOrderService {
|
||||
// 3. 调用三方接口
|
||||
PayOrderUnifiedReqDTO unifiedOrderReqDTO = PayOrderConvert.INSTANCE.convert2(reqVO, userIp)
|
||||
// 商户相关的字段
|
||||
.setMerchantOrderId(orderExtension.getNo()) // 注意,此处使用的是 PayOrderExtensionDO.no 属性!
|
||||
.setOutTradeNo(orderExtension.getNo()) // 注意,此处使用的是 PayOrderExtensionDO.no 属性!
|
||||
.setSubject(order.getSubject()).setBody(order.getBody())
|
||||
.setNotifyUrl(genChannelPayNotifyUrl(channel))
|
||||
.setNotifyUrl(genChannelOrderNotifyUrl(channel))
|
||||
.setReturnUrl(reqVO.getReturnUrl())
|
||||
// 订单相关字段
|
||||
.setAmount(order.getPrice()).setExpireTime(order.getExpireTime());
|
||||
PayOrderUnifiedRespDTO unifiedOrderRespDTO = client.unifiedOrder(unifiedOrderReqDTO);
|
||||
.setPrice(order.getPrice()).setExpireTime(order.getExpireTime());
|
||||
PayOrderRespDTO unifiedOrderResp = client.unifiedOrder(unifiedOrderReqDTO);
|
||||
|
||||
// 4. 如果调用直接支付成功,则直接更新支付单状态为成功。例如说:付款码支付,免密支付时,就直接验证支付成功
|
||||
if (unifiedOrderRespDTO.getNotify() != null) {
|
||||
notifyPayOrderSuccess(channel, unifiedOrderRespDTO.getNotify(), null);
|
||||
if (unifiedOrderResp != 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());
|
||||
}
|
||||
|
||||
// 返回成功
|
||||
return PayOrderConvert.INSTANCE.convert(order, unifiedOrderRespDTO);
|
||||
return PayOrderConvert.INSTANCE.convert(order, unifiedOrderResp);
|
||||
}
|
||||
|
||||
private PayOrderDO validatePayOrderCanSubmit(Long id) {
|
||||
private PayOrderDO validateOrderCanSubmit(Long id) {
|
||||
PayOrderDO order = orderMapper.selectById(id);
|
||||
if (order == null) { // 是否存在
|
||||
throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
|
||||
throw exception(PAY_ORDER_NOT_FOUND);
|
||||
}
|
||||
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())) { // 校验是否过期
|
||||
throw exception(ErrorCodeConstants.PAY_ORDER_IS_EXPIRED);
|
||||
throw exception(PAY_ORDER_IS_EXPIRED);
|
||||
}
|
||||
return order;
|
||||
}
|
||||
@ -184,14 +191,12 @@ public class PayOrderServiceImpl implements PayOrderService {
|
||||
private PayChannelDO validatePayChannelCanSubmit(Long appId, String channelCode) {
|
||||
// 校验 App
|
||||
appService.validPayApp(appId);
|
||||
|
||||
// 校验支付渠道是否有效
|
||||
PayChannelDO channel = channelService.validPayChannel(appId, channelCode);
|
||||
// 校验支付客户端是否正确初始化
|
||||
PayClient client = payClientFactory.getPayClient(channel.getId());
|
||||
if (client == null) {
|
||||
log.error("[validatePayChannelCanSubmit][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
|
||||
throw exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
|
||||
throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
@ -202,8 +207,8 @@ public class PayOrderServiceImpl implements PayOrderService {
|
||||
* @param channel 支付渠道
|
||||
* @return 支付渠道的回调地址 配置地址 + "/" + channel id
|
||||
*/
|
||||
private String genChannelPayNotifyUrl(PayChannelDO channel) {
|
||||
return payProperties.getCallbackUrl() + "/" + channel.getId();
|
||||
private String genChannelOrderNotifyUrl(PayChannelDO channel) {
|
||||
return payProperties.getOrderNotifyUrl() + "/" + channel.getId();
|
||||
}
|
||||
|
||||
private String generateOrderExtensionNo() {
|
||||
@ -226,18 +231,57 @@ public class PayOrderServiceImpl implements PayOrderService {
|
||||
|
||||
@Override
|
||||
@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);
|
||||
// 更新支付订单为已支付
|
||||
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 支付成功
|
||||
PayOrderExtensionDO orderExtension = updatePayOrderExtensionSuccess(notify.getOrderExtensionNo(), rawNotify);
|
||||
PayOrderExtensionDO orderExtension = updateOrderExtensionSuccess(notify);
|
||||
// 2. 更新 PayOrderDO 支付成功
|
||||
Pair<Boolean, PayOrderDO> order = updatePayOrderSuccess(channel, orderExtension, notify);
|
||||
Pair<Boolean, PayOrderDO> order = updateOrderExtensionSuccess(channel, orderExtension, notify);
|
||||
if (order.getKey()) { // 如果之前已经成功回调,则直接返回,不用重复记录支付通知记录;例如说:支付平台重复回调
|
||||
return;
|
||||
}
|
||||
@ -250,33 +294,30 @@ public class PayOrderServiceImpl implements PayOrderService {
|
||||
/**
|
||||
* 更新 PayOrderExtensionDO 支付成功
|
||||
*
|
||||
* @param no 支付订单号(支付模块)
|
||||
* @param rawNotify 通知数据
|
||||
* @param notify 通知
|
||||
* @return PayOrderExtensionDO 对象
|
||||
*/
|
||||
private PayOrderExtensionDO updatePayOrderExtensionSuccess(String no, PayNotifyReqDTO rawNotify) {
|
||||
private PayOrderExtensionDO updateOrderExtensionSuccess(PayOrderRespDTO notify) {
|
||||
// 1. 查询 PayOrderExtensionDO
|
||||
PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(no);
|
||||
PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo());
|
||||
if (orderExtension == null) {
|
||||
throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_NOT_FOUND);
|
||||
throw exception(PAY_ORDER_EXTENSION_NOT_FOUND);
|
||||
}
|
||||
if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功,直接返回,不用重复更新
|
||||
log.info("[updatePayOrderSuccess][支付拓展单({}) 已经是已支付,无需更新为已支付]", orderExtension.getId());
|
||||
log.info("[updateOrderExtensionSuccess][支付拓展单({}) 已经是已支付,无需更新]", orderExtension.getId());
|
||||
return orderExtension;
|
||||
}
|
||||
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
|
||||
int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(),
|
||||
PayOrderStatusEnum.WAITING.getStatus(), PayOrderExtensionDO.builder().id(orderExtension.getId())
|
||||
.status(PayOrderStatusEnum.SUCCESS.getStatus())
|
||||
.channelNotifyData(toJsonString(rawNotify)).build());
|
||||
int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(),
|
||||
PayOrderExtensionDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(toJsonString(notify)).build());
|
||||
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;
|
||||
}
|
||||
|
||||
@ -289,20 +330,20 @@ public class PayOrderServiceImpl implements PayOrderService {
|
||||
* @return key:是否之前已经成功回调
|
||||
* value:PayOrderDO 对象
|
||||
*/
|
||||
private Pair<Boolean, PayOrderDO> updatePayOrderSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension,
|
||||
PayOrderNotifyRespDTO notify) {
|
||||
private Pair<Boolean, PayOrderDO> updateOrderExtensionSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension,
|
||||
PayOrderRespDTO notify) {
|
||||
// 1. 判断 PayOrderDO 是否处于待支付
|
||||
PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId());
|
||||
if (order == null) {
|
||||
throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
|
||||
throw exception(PAY_ORDER_NOT_FOUND);
|
||||
}
|
||||
if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功,直接返回,不用重复更新
|
||||
&& Objects.equals(order.getSuccessExtensionId(), orderExtension.getId())) {
|
||||
log.info("[updatePayOrderSuccess][支付订单({}) 已经是已支付,无需更新为已支付]", order.getId());
|
||||
log.info("[updateOrderExtensionSuccess][支付订单({}) 已经是已支付,无需更新]", order.getId());
|
||||
return Pair.of(true, order);
|
||||
}
|
||||
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
|
||||
@ -313,10 +354,43 @@ public class PayOrderServiceImpl implements PayOrderService {
|
||||
.channelOrderNo(notify.getChannelOrderNo()).channelUserId(notify.getChannelUserId())
|
||||
.notifyTime(LocalDateTime.now()).build());
|
||||
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);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
package cn.iocoder.yudao.module.pay.service.refund;
|
||||
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
|
||||
import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
|
||||
import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
|
||||
|
||||
import java.util.List;
|
||||
@ -62,8 +61,7 @@ public interface PayRefundService {
|
||||
*
|
||||
* @param channelId 渠道编号
|
||||
* @param notify 通知
|
||||
* @param rawNotify 通知数据
|
||||
*/
|
||||
void notifyPayRefund(Long channelId, PayRefundNotifyRespDTO notify, PayNotifyReqDTO rawNotify);
|
||||
void notifyRefund(Long channelId, PayRefundRespDTO notify);
|
||||
|
||||
}
|
||||
|
@ -1,32 +1,31 @@
|
||||
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.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
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.PayClientFactory;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
|
||||
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.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.controller.admin.refund.vo.PayRefundExportReqVO;
|
||||
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.channel.PayChannelDO;
|
||||
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.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.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.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.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.channel.PayChannelService;
|
||||
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
|
||||
@ -41,12 +40,14 @@ import org.springframework.validation.annotation.Validated;
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
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 实现类
|
||||
*
|
||||
* @author aquan
|
||||
* @author jason
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@ -61,8 +62,6 @@ public class PayRefundServiceImpl implements PayRefundService {
|
||||
|
||||
@Resource
|
||||
private PayRefundMapper refundMapper;
|
||||
@Resource
|
||||
private PayOrderMapper orderMapper; // TODO @jason:需要改成不直接操作 db;
|
||||
|
||||
@Resource
|
||||
private PayOrderService orderService;
|
||||
@ -82,7 +81,7 @@ public class PayRefundServiceImpl implements PayRefundService {
|
||||
|
||||
@Override
|
||||
public Long getRefundCountByAppId(Long appId) {
|
||||
return refundMapper.selectCountByApp(appId);
|
||||
return refundMapper.selectCountByAppId(appId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -98,85 +97,83 @@ public class PayRefundServiceImpl implements PayRefundService {
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
|
||||
// 获得 PayOrderDO
|
||||
PayOrderDO order = orderService.getOrder(reqDTO.getPayOrderId());
|
||||
// 校验订单是否存在
|
||||
if (Objects.isNull(order) ) {
|
||||
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
|
||||
}
|
||||
// 校验 App
|
||||
PayAppDO app = appService.validPayApp(order.getAppId());
|
||||
// 校验支付渠道是否有效
|
||||
// 1.1 校验 App
|
||||
PayAppDO app = appService.validPayApp(reqDTO.getAppId());
|
||||
// 1.2 校验支付订单
|
||||
PayOrderDO order = validatePayOrderCanRefund(reqDTO);
|
||||
// 1.3 校验支付渠道是否有效
|
||||
PayChannelDO channel = channelService.validPayChannel(order.getChannelId());
|
||||
// 校验支付客户端是否正确初始化
|
||||
PayClient client = payClientFactory.getPayClient(channel.getId());
|
||||
if (client == null) {
|
||||
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 芋艿:待实现
|
||||
String merchantRefundId = RandomUtil.randomNumbers(16);
|
||||
|
||||
// 校验退款的条件
|
||||
validatePayRefund(reqDTO, order);
|
||||
// 退款类型
|
||||
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())
|
||||
// 2.1 插入退款单
|
||||
refund = PayRefundConvert.INSTANCE.convert(reqDTO)
|
||||
.setNo(generateRefundNo()).setOrderId(order.getId())
|
||||
.setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode())
|
||||
// 商户相关的字段
|
||||
.setNotifyUrl(app.getRefundNotifyUrl()).setNotifyStatus(PayNotifyStatusEnum.WAITING.getStatus())
|
||||
// 渠道相关字段
|
||||
.setChannelOrderNo(order.getChannelOrderNo())
|
||||
.setPayTradeNo(orderExtensionDO.getNo())
|
||||
.setMerchantRefundId(merchantRefundId) // TODO 芋艿:需要优化
|
||||
.setNotifyUrl(genChannelPayNotifyUrl(channel)) // TODO 芋艿:优化下 notifyUrl
|
||||
// 退款相关字段
|
||||
.setStatus(PayRefundStatusEnum.WAITING.getStatus())
|
||||
.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());
|
||||
// 向渠道发起退款申请
|
||||
client.unifiedRefund(unifiedReqDTO);
|
||||
// 检查是否失败,失败抛出业务异常。
|
||||
// TODO 渠道的异常记录。
|
||||
// TODO @jason:可以先打个 warn log 哈;
|
||||
PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); // TODO 增加一个 channelErrorCode、channelErrorMsg 字段
|
||||
// 2.3 处理退款返回
|
||||
notifyRefund(channel, refundRespDTO);
|
||||
|
||||
// 成功在 退款回调中处理
|
||||
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 支付渠道
|
||||
* @return 支付渠道的回调地址 配置地址 + "/" + channel id
|
||||
*/
|
||||
private String genChannelPayNotifyUrl(PayChannelDO channel) {
|
||||
return payProperties.getCallbackUrl() + "/" + channel.getId();
|
||||
private String genChannelRefundNotifyUrl(PayChannelDO channel) {
|
||||
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
|
||||
@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);
|
||||
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();
|
||||
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());
|
||||
TenantUtils.execute(channel.getTenantId(), () -> notifyRefund(channel, notify));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验是否进行退款
|
||||
*
|
||||
* @param reqDTO 退款申请信息
|
||||
* @param order 原始支付订单信息
|
||||
*/
|
||||
private void validatePayRefund(PayRefundCreateReqDTO reqDTO, PayOrderDO order) {
|
||||
// 校验状态,必须是支付状态
|
||||
if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
|
||||
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_SUCCESS);
|
||||
private void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) {
|
||||
if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) {
|
||||
notifyRefundSuccess(channel, notify);
|
||||
} else {
|
||||
notifyRefundFailure(channel, notify);
|
||||
}
|
||||
// 是否已经全额退款
|
||||
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 (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){
|
||||
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_PRICE_PRICE_EXCEED);
|
||||
if (PayRefundStatusEnum.isSuccess(refund.getStatus())) { // 如果已经是成功,直接返回,不用重复更新
|
||||
return;
|
||||
}
|
||||
// 校验渠道订单号
|
||||
if (StrUtil.isEmpty(order.getChannelOrderNo())) {
|
||||
throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_CHN_ORDER_NO_IS_NULL);
|
||||
if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) {
|
||||
throw exception(ErrorCodeConstants.PAY_REFUND_STATUS_IS_NOT_WAITING);
|
||||
}
|
||||
//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 芋艿:未实现
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.pay.service.app;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
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.util.RandomUtils;
|
||||
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.PayAppUpdateReqVO;
|
||||
@ -18,8 +16,6 @@ import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
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 ->
|
||||
o.setStatus((RandomUtil.randomEle(CommonStatusEnum.values()).getStatus()))
|
||||
.setPayNotifyUrl(randomURL())
|
||||
.setOrderNotifyUrl(randomURL())
|
||||
.setRefundNotifyUrl(randomURL()));
|
||||
|
||||
// 调用
|
||||
@ -77,7 +73,7 @@ public class PayAppServiceTest extends BaseDbUnitTest {
|
||||
// 准备参数
|
||||
PayAppUpdateReqVO reqVO = randomPojo(PayAppUpdateReqVO.class, o -> {
|
||||
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
o.setPayNotifyUrl(randomURL()).setRefundNotifyUrl(randomURL());
|
||||
o.setOrderNotifyUrl(randomURL()).setRefundNotifyUrl(randomURL());
|
||||
o.setId(dbApp.getId()); // 设置更新的 ID
|
||||
});
|
||||
|
||||
|
@ -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.enums.order.PayOrderNotifyStatusEnum;
|
||||
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.channel.PayChannelService;
|
||||
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.setNotifyTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
|
||||
o.setSuccessExtensionId(1L);
|
||||
o.setRefundStatus(PayRefundTypeEnum.NO.getStatus());
|
||||
o.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
|
||||
o.setRefundTimes(0);
|
||||
o.setRefundPrice(0L);
|
||||
o.setRefundPrice(0);
|
||||
o.setChannelUserId("1008611");
|
||||
o.setChannelOrderNo(channelOrderId);
|
||||
o.setUpdateTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
|
||||
@ -106,7 +106,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
|
||||
// 测试 status 不匹配
|
||||
orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())));
|
||||
// 测试 refundStatus 不匹配
|
||||
orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayRefundTypeEnum.ALL.getStatus())));
|
||||
orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayOrderRefundStatusEnum.ALL.getStatus())));
|
||||
// 测试 createTime 不匹配
|
||||
orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(LocalDateTime.of(2019, 1, 1, 10, 10,
|
||||
1))));
|
||||
@ -118,7 +118,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
|
||||
reqVO.setMerchantOrderId(merchantOrderId);
|
||||
reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.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)}));
|
||||
// 调用
|
||||
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.setNotifyTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
|
||||
o.setSuccessExtensionId(1L);
|
||||
o.setRefundStatus(PayRefundTypeEnum.NO.getStatus());
|
||||
o.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
|
||||
o.setRefundTimes(0);
|
||||
o.setRefundPrice(0L);
|
||||
o.setRefundPrice(0);
|
||||
o.setChannelUserId("1008611");
|
||||
o.setChannelOrderNo(channelOrderId);
|
||||
o.setUpdateTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
|
||||
@ -175,7 +175,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
|
||||
// 测试 status 不匹配
|
||||
orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())));
|
||||
// 测试 refundStatus 不匹配
|
||||
orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayRefundTypeEnum.ALL.getStatus())));
|
||||
orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayOrderRefundStatusEnum.ALL.getStatus())));
|
||||
// 测试 createTime 不匹配
|
||||
orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(LocalDateTime.of(2019, 1, 1, 10, 10,
|
||||
1))));
|
||||
@ -187,7 +187,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
|
||||
reqVO.setMerchantOrderId(merchantOrderId);
|
||||
reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.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)}));
|
||||
|
||||
// 调用
|
||||
|
@ -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.enums.order.PayOrderNotifyStatusEnum;
|
||||
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.channel.PayChannelService;
|
||||
import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
|
||||
@ -62,13 +62,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
|
||||
o.setChannelId(1L);
|
||||
o.setChannelCode(PayChannelEnum.WX_PUB.getCode());
|
||||
o.setOrderId(1L);
|
||||
o.setTradeNo("OT0000001");
|
||||
o.setNo("OT0000001");
|
||||
o.setMerchantOrderId("MOT0000001");
|
||||
o.setMerchantRefundNo("MRF0000001");
|
||||
o.setMerchantRefundId("MRF0000001");
|
||||
o.setNotifyUrl("https://www.cancanzi.com");
|
||||
o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
|
||||
o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
|
||||
o.setType(PayRefundTypeEnum.SOME.getStatus());
|
||||
o.setPayPrice(100);
|
||||
o.setRefundPrice(500);
|
||||
o.setReason("就是想退款了,你有意见吗");
|
||||
@ -77,10 +76,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
|
||||
o.setChannelRefundNo("CHR0000001");
|
||||
o.setChannelErrorCode("");
|
||||
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.setNotifyTime(LocalDateTime.of(2021, 1, 1, 10, 10, 20));
|
||||
o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10));
|
||||
o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35));
|
||||
});
|
||||
@ -90,14 +86,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
|
||||
// 测试 channelCode 不匹配
|
||||
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())));
|
||||
// 测试 merchantRefundNo 不匹配
|
||||
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundNo("MRF1111112")));
|
||||
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId("MRF1111112")));
|
||||
// 测试 notifyStatus 不匹配
|
||||
refundMapper.insert(
|
||||
cloneIgnoreId(dbRefund, o -> o.setNotifyStatus(PayOrderNotifyStatusEnum.FAILURE.getStatus())));
|
||||
// 测试 status 不匹配
|
||||
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.CLOSE.getStatus())));
|
||||
// 测试 type 不匹配
|
||||
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setType(PayRefundTypeEnum.ALL.getStatus())));
|
||||
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus())));
|
||||
// 测试 createTime 不匹配
|
||||
refundMapper.insert(cloneIgnoreId(dbRefund, o ->
|
||||
o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10))));
|
||||
@ -108,7 +102,6 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
|
||||
reqVO.setMerchantRefundNo("MRF0000001");
|
||||
reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.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)}));
|
||||
|
||||
// 调用
|
||||
@ -127,13 +120,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
|
||||
o.setChannelId(1L);
|
||||
o.setChannelCode(PayChannelEnum.WX_PUB.getCode());
|
||||
o.setOrderId(1L);
|
||||
o.setTradeNo("OT0000001");
|
||||
o.setNo("OT0000001");
|
||||
o.setMerchantOrderId("MOT0000001");
|
||||
o.setMerchantRefundNo("MRF0000001");
|
||||
o.setMerchantRefundId("MRF0000001");
|
||||
o.setNotifyUrl("https://www.cancanzi.com");
|
||||
o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
|
||||
o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
|
||||
o.setType(PayRefundTypeEnum.SOME.getStatus());
|
||||
o.setPayPrice(100);
|
||||
o.setRefundPrice(500);
|
||||
o.setReason("就是想退款了,你有意见吗");
|
||||
@ -142,10 +134,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
|
||||
o.setChannelRefundNo("CHR0000001");
|
||||
o.setChannelErrorCode("");
|
||||
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.setNotifyTime(LocalDateTime.of(2021, 1, 1, 10, 10, 20));
|
||||
o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10));
|
||||
o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35));
|
||||
});
|
||||
@ -155,14 +144,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
|
||||
// 测试 channelCode 不匹配
|
||||
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())));
|
||||
// 测试 merchantRefundNo 不匹配
|
||||
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundNo("MRF1111112")));
|
||||
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId("MRF1111112")));
|
||||
// 测试 notifyStatus 不匹配
|
||||
refundMapper.insert(
|
||||
cloneIgnoreId(dbRefund, o -> o.setNotifyStatus(PayOrderNotifyStatusEnum.FAILURE.getStatus())));
|
||||
// 测试 status 不匹配
|
||||
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.CLOSE.getStatus())));
|
||||
// 测试 type 不匹配
|
||||
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setType(PayRefundTypeEnum.ALL.getStatus())));
|
||||
refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus())));
|
||||
// 测试 createTime 不匹配
|
||||
refundMapper.insert(cloneIgnoreId(dbRefund, o ->
|
||||
o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10))));
|
||||
@ -174,7 +161,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
|
||||
reqVO.setMerchantRefundNo("MRF0000001");
|
||||
reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.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)}));
|
||||
|
||||
// 调用
|
||||
|
@ -194,7 +194,8 @@ yudao:
|
||||
- ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求
|
||||
- ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
|
||||
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: # 访问日志的配置项
|
||||
enable: false
|
||||
error-code: # 错误码相关配置项
|
||||
|
@ -142,7 +142,7 @@ yudao:
|
||||
- /admin-api/system/captcha/check # 校验图片验证码,和租户无关
|
||||
- /admin-api/infra/file/*/get/** # 获取图片,和租户无关
|
||||
- /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
|
||||
- /admin-api/pay/notify/callback/* # 支付回调通知,不携带租户编号
|
||||
- /admin-api/pay/notify/** # 支付回调通知,不携带租户编号
|
||||
- /jmreport/* # 积木报表,无法携带租户编号
|
||||
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号
|
||||
ignore-tables:
|
||||
|
@ -28,14 +28,14 @@ export function deleteChannel(id) {
|
||||
}
|
||||
|
||||
// 获得支付渠道
|
||||
export function getChannel(appId,code) {
|
||||
export function getChannel(appId, code) {
|
||||
return request({
|
||||
url: '/pay/channel/get-channel',
|
||||
url: '/pay/channel/get',
|
||||
method: 'get',
|
||||
params:{
|
||||
appId:appId,
|
||||
code:code
|
||||
appId,
|
||||
code
|
||||
},
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
|
1
yudao-ui-admin/src/assets/images/pay/icon/wx_bar.svg
Normal file
1
yudao-ui-admin/src/assets/images/pay/icon/wx_bar.svg
Normal 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("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }</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 |
1
yudao-ui-admin/src/assets/images/pay/icon/wx_native.svg
Normal file
1
yudao-ui-admin/src/assets/images/pay/icon/wx_native.svg
Normal 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("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }</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 |
@ -132,6 +132,14 @@ export const PayChannelEnum = {
|
||||
"code": "wx_app",
|
||||
"name": "微信 APP 支付"
|
||||
},
|
||||
WX_NATIVE: {
|
||||
"code": "wx_native",
|
||||
"name": "微信扫码支付"
|
||||
},
|
||||
WX_BAR: {
|
||||
"code": "wx_bar",
|
||||
"name": "微信条码支付"
|
||||
},
|
||||
ALIPAY_PC: {
|
||||
"code": "alipay_pc",
|
||||
"name": "支付宝 PC 网站支付"
|
||||
|
@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<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">
|
||||
<el-form-item label-width="180px" label="渠道费率" prop="feeRate">
|
||||
<el-input v-model="form.feeRate" placeholder="请输入渠道费率" clearable :style="{width: '100%'}">
|
||||
<template slot="append">%</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<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-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>
|
||||
</el-form-item>
|
||||
<el-form-item label-width="180px" label="商户号" prop="weChatConfig.mchId">
|
||||
@ -29,29 +29,41 @@
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label-width="180px" label="商户密钥" prop="weChatConfig.mchKey"
|
||||
v-if="form.weChatConfig.apiVersion === 'v2'">
|
||||
<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
|
||||
<div 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
|
||||
: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-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"
|
||||
placeholder="请上传apiclient_key.perm证书"
|
||||
placeholder="请上传 apiclient_key.perm 证书"
|
||||
readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label-width="180px" label="" prop="privateKeyContentFile">
|
||||
<el-upload ref="privateKeyContentFile"
|
||||
:limit="1"
|
||||
:accept="fileAccept"
|
||||
:headers="header"
|
||||
accept=".pem"
|
||||
action=""
|
||||
:before-upload="pemFileBeforeUpload"
|
||||
:http-request="privateKeyUpload"
|
||||
:http-request="privateKeyContentUpload"
|
||||
>
|
||||
<el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
|
||||
</el-upload>
|
||||
@ -64,18 +76,17 @@
|
||||
<el-form-item label-width="180px" label="" prop="privateCertContentFile">
|
||||
<el-upload ref="privateCertContentFile"
|
||||
:limit="1"
|
||||
:accept="fileAccept"
|
||||
:headers="header"
|
||||
accept=".pem"
|
||||
action=""
|
||||
:before-upload="pemFileBeforeUpload"
|
||||
:http-request="privateCertUpload"
|
||||
:http-request="privateCertContentUpload"
|
||||
>
|
||||
<el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<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>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
@ -100,6 +111,7 @@ const defaultForm = {
|
||||
mchId: '',
|
||||
apiVersion: '',
|
||||
mchKey: '',
|
||||
keyContent: '',
|
||||
privateKeyContent: '',
|
||||
privateCertContent: '',
|
||||
apiV3Key:'',
|
||||
@ -159,27 +171,27 @@ export default {
|
||||
message: '请输入商户密钥',
|
||||
trigger: 'blur'
|
||||
}],
|
||||
'weChatConfig.keyContent': [{
|
||||
required: true,
|
||||
message: '请上传 apiclient_cert.p12 证书',
|
||||
trigger: 'blur'
|
||||
}],
|
||||
'weChatConfig.privateKeyContent': [{
|
||||
required: true,
|
||||
message: '请上传apiclient_key.perm证书',
|
||||
message: '请上传 apiclient_key.perm 证书',
|
||||
trigger: 'blur'
|
||||
}],
|
||||
'weChatConfig.privateCertContent': [{
|
||||
required: true,
|
||||
message: '请上传apiclient_cert.perm证书',
|
||||
message: '请上传 apiclient_cert.perm证 书',
|
||||
trigger: 'blur'
|
||||
}],
|
||||
'weChatConfig.apiV3Key': [{
|
||||
required: true,
|
||||
message: '请上传apiV3密钥值',
|
||||
message: '请上传 api V3 密钥值',
|
||||
trigger: 'blur'
|
||||
}],
|
||||
},
|
||||
// 文件上传的header
|
||||
header: {
|
||||
"Authorization": null
|
||||
},
|
||||
fileAccept: ".pem",
|
||||
// 渠道状态 数据字典
|
||||
statusDictDatas: getDictDatas(DICT_TYPE.COMMON_STATUS),
|
||||
versionDictDatas: getDictDatas(DICT_TYPE.PAY_CHANNEL_WECHAT_VERSION),
|
||||
@ -218,6 +230,7 @@ export default {
|
||||
this.form.weChatConfig.apiVersion = config.apiVersion;
|
||||
this.form.weChatConfig.mchId = config.mchId;
|
||||
this.form.weChatConfig.mchKey = config.mchKey;
|
||||
this.form.weChatConfig.keyContent = config.keyContent;
|
||||
this.form.weChatConfig.privateKeyContent = config.privateKeyContent;
|
||||
this.form.weChatConfig.privateCertContent = config.privateCertContent;
|
||||
this.form.weChatConfig.apiV3Key = config.apiV3Key;
|
||||
@ -241,7 +254,6 @@ export default {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
this.close();
|
||||
}
|
||||
|
||||
})
|
||||
} else {
|
||||
|
||||
@ -255,10 +267,14 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
pemFileBeforeUpload(file) {
|
||||
/**
|
||||
* apiclient_cert.p12、apiclient_cert.pem、apiclient_key.pem 上传前的校验
|
||||
*/
|
||||
fileBeforeUpload(file, fileAccept) {
|
||||
let format = '.' + file.name.split(".")[1];
|
||||
if (format !== this.fileAccept) {
|
||||
this.$message.error('请上传指定格式"' + this.fileAccept + '"文件');
|
||||
if (format !== fileAccept) {
|
||||
debugger
|
||||
this.$message.error('请上传指定格式"' + fileAccept + '"文件');
|
||||
return false;
|
||||
}
|
||||
let isRightSize = file.size / 1024 / 1024 < 2
|
||||
@ -267,19 +283,41 @@ export default {
|
||||
}
|
||||
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()
|
||||
readFile.onload = (e) => {
|
||||
this.form.weChatConfig.privateKeyContent = e.target.result
|
||||
}
|
||||
readFile.readAsText(event.file);
|
||||
},
|
||||
privateCertUpload(event) {
|
||||
/**
|
||||
* 读取 apiclient_cert.pem 到 privateCertContent 字段
|
||||
*/
|
||||
privateCertContentUpload(event) {
|
||||
const readFile = new FileReader()
|
||||
readFile.onload = (e) => {
|
||||
this.form.weChatConfig.privateCertContent = e.target.result
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -143,6 +143,30 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</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 label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template v-slot="scope">
|
||||
@ -173,8 +197,8 @@
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="支付结果的回调地址" prop="payNotifyUrl">
|
||||
<el-input v-model="form.payNotifyUrl" placeholder="请输入支付结果的回调地址"/>
|
||||
<el-form-item label="支付结果的回调地址" prop="orderNotifyUrl">
|
||||
<el-input v-model="form.orderNotifyUrl" placeholder="请输入支付结果的回调地址"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="退款结果的回调地址" prop="refundNotifyUrl">
|
||||
<el-input v-model="form.refundNotifyUrl" placeholder="请输入退款结果的回调地址"/>
|
||||
@ -235,7 +259,7 @@ export default {
|
||||
rules: {
|
||||
name: [{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"}],
|
||||
},
|
||||
// 数据字典
|
||||
@ -295,7 +319,7 @@ export default {
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
remark: undefined,
|
||||
payNotifyUrl: undefined,
|
||||
orderNotifyUrl: undefined,
|
||||
refundNotifyUrl: undefined,
|
||||
};
|
||||
this.resetForm("form");
|
||||
|
@ -26,7 +26,7 @@
|
||||
<!-- 微信支付 -->
|
||||
<el-descriptions title="选择微信支付" style="margin-top: 20px;" />
|
||||
<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">
|
||||
<div class="title">{{ channel.name }}</div>
|
||||
</div>
|
||||
@ -125,6 +125,14 @@ export default {
|
||||
name: '微信 App 支付',
|
||||
icon: require("@/assets/images/pay/icon/wx_app.svg"),
|
||||
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: '模拟支付',
|
||||
icon: require("@/assets/images/pay/icon/mock.svg"),
|
||||
@ -195,6 +203,15 @@ export default {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (channelCode === PayChannelEnum.WX_BAR.code) {
|
||||
this.barCode = {
|
||||
channelCode: channelCode,
|
||||
value: '',
|
||||
title: '“微信”条码支付',
|
||||
visible: true
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 默认的提交处理
|
||||
this.submit0(channelCode)
|
||||
@ -207,7 +224,16 @@ export default {
|
||||
returnUrl: location.href, // 支付成功后,支付渠道跳转回当前页;再由当前页,跳转回 {@link returnUrl} 对应的地址
|
||||
...this.buildSubmitParam(channelCode)
|
||||
}).then(response => {
|
||||
// 直接返回已支付的情况,例如说扫码支付
|
||||
const data = response.data
|
||||
if (data.status === PayOrderStatusEnum.SUCCESS.status) {
|
||||
this.clearQueryInterval();
|
||||
this.$message.success('支付成功!');
|
||||
this.goReturnUrl();
|
||||
return
|
||||
}
|
||||
|
||||
// 展示对应的界面
|
||||
if (data.displayMode === PayDisplayModeEnum.URL.mode) {
|
||||
this.displayUrl(channelCode, data)
|
||||
} 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 {}
|
||||
},
|
||||
/** 提交支付后,URL 的展示形式 */
|
||||
displayUrl(channelCode, data) {
|
||||
// window.open(data.displayContent)
|
||||
location.href = data.displayContent
|
||||
this.submitLoading = false
|
||||
},
|
||||
@ -298,6 +331,9 @@ export default {
|
||||
* ③ close:支付已关闭
|
||||
*/
|
||||
goReturnUrl(payResult) {
|
||||
// 清理任务
|
||||
this.clearQueryInterval();
|
||||
|
||||
// 未配置的情况下,只能关闭
|
||||
if (!this.returnUrl) {
|
||||
this.$tab.closePage();
|
||||
|
@ -41,7 +41,8 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="退款时间" align="center" prop="refundTime" width="180">
|
||||
<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>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
|
Loading…
Reference in New Issue
Block a user