diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java index 7e6f89d5a..436a9e669 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java @@ -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> constraintViolations = validator.validate(object, groups); if (CollUtil.isNotEmpty(constraintViolations)) { diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java index 4784e926b..dc7380865 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java @@ -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; } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java index 6db0a0040..9362466a6 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java @@ -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 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 params, String body); } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayNotifyReqDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayNotifyReqDTO.java deleted file mode 100644 index 65d0d01ad..000000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayNotifyReqDTO.java +++ /dev/null @@ -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 params; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayOrderNotifyRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayOrderNotifyRespDTO.java deleted file mode 100644 index 239ddda23..000000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayOrderNotifyRespDTO.java +++ /dev/null @@ -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; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayRefundNotifyRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayRefundNotifyRespDTO.java deleted file mode 100644 index 9c05b3ac0..000000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayRefundNotifyRespDTO.java +++ /dev/null @@ -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; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java new file mode 100644 index 000000000..928830f66 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java @@ -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; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java index ba31b08ad..f269d2f8f 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java @@ -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; /** * 支付过期时间 diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedRespDTO.java deleted file mode 100644 index 441b4ece1..000000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedRespDTO.java +++ /dev/null @@ -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; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java new file mode 100644 index 000000000..6d10dc9a0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java @@ -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; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java index e19a04aa0..4f5e203d2 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java @@ -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; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedRespDTO.java deleted file mode 100644 index 104dc2bbe..000000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedRespDTO.java +++ /dev/null @@ -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; -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java index 0225739ca..8be5aa498 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java @@ -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 implemen /** * 渠道编码 */ + @SuppressWarnings("FieldCanBeLocal") private final String channelCode; /** * 支付配置 @@ -46,7 +48,7 @@ public abstract class AbstractPayClient implemen */ public final void init() { doInit(); - log.info("[init][配置({}) 初始化完成]", config); + log.info("[init][客户端({}) 初始化完成]", getId()); } /** @@ -59,7 +61,7 @@ public abstract class AbstractPayClient 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 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 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 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 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; } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java index 60472fefb..f11c88ec0 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java @@ -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 params, String body) throws Throwable { + // 1. 校验回调数据 + Map 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) () -> { + 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 params = rawNotify.getParams(); - Map 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 params, String body) { + // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。 + // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调 + // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有 + // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。 + // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。 + throw new UnsupportedOperationException("支付宝无退款回调"); } // ========== 各种工具方法 ========== @@ -132,21 +151,4 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient 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 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 - * 注意,可通过 {@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; /** diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java index f23942bc7..db35f10ac 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java @@ -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); } // ========== 各种工具方法 ========== diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java deleted file mode 100644 index d9abcc18d..000000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java +++ /dev/null @@ -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, "发起支付失败,原因:{}"); - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java index 1e81d74c5..129c40602 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java @@ -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、微信小程序、微信公众号等,需要做自定义处理的 ; /** diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java index 059010896..6cfcb5236 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java @@ -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()); + } + } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayNotifyRefundStatusEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayNotifyRefundStatusEnum.java deleted file mode 100644 index 5b5387cea..000000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayNotifyRefundStatusEnum.java +++ /dev/null @@ -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; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundRespEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundRespEnum.java deleted file mode 100644 index 693c91a27..000000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundRespEnum.java +++ /dev/null @@ -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; - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java new file mode 100644 index 000000000..74b2f8d7d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java @@ -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()); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java index 093e772e3..984256063 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java @@ -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; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java index 45cabc7fd..0eb354cd0 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java @@ -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"; diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java new file mode 100644 index 000000000..9af11ac3f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java @@ -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 = "SUCCESS"; + 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; + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java new file mode 100644 index 000000000..5e73601c2 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java @@ -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; + } + +} diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/TradeAfterSaleServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/TradeAfterSaleServiceImpl.java index fc86f230a..ab94fb7a2 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/TradeAfterSaleServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/TradeAfterSaleServiceImpl.java @@ -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)); } diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApi.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApi.java index 395ba1220..93dfc5af9 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApi.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApi.java @@ -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); } diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java index b8c8c9c20..f1d5f6eb5 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java @@ -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; + } diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java index 0353afff5..16174285f 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java @@ -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, "示例订单不存在"); diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderStatusEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderStatusEnum.java index 684f42bc6..2ee45a1f4 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderStatusEnum.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderStatusEnum.java @@ -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()); + } + } diff --git a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundStatusEnum.java b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundStatusEnum.java index 3fb8213f5..29fdc923b 100644 --- a/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundStatusEnum.java +++ b/yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundStatusEnum.java @@ -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; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java index d7299926b..a245880ba 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java @@ -22,7 +22,7 @@ public class PayOrderApiImpl implements PayOrderApi { @Override public Long createOrder(PayOrderCreateReqDTO reqDTO) { - return payOrderService.createPayOrder(reqDTO); + return payOrderService.createOrder(reqDTO); } @Override diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApiImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApiImpl.java index e85f07be0..95ca9a966 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApiImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApiImpl.java @@ -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)); } } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java index bf366119c..45f29360f 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java @@ -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 = "退款结果的回调地址不能为空") diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java index 3660eddd5..e37ddfc11 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java @@ -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 params, - @RequestBody(required = false) String body) { - log.info("[notifyCallback][channelId({}) 回调数据({}/{})]", channelId, params, body); + public String notifyOrder(@PathVariable("channelId") Long channelId, + @RequestParam(required = false) Map 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 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"; } } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/PayOrderController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/PayOrderController.java index fc753812a..6acc41ceb 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/PayOrderController.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/PayOrderController.java @@ -86,7 +86,7 @@ public class PayOrderController { @PostMapping("/submit") @Operation(summary = "提交支付订单") public CommonResult submitPayOrder(@RequestBody PayOrderSubmitReqVO reqVO) { - PayOrderSubmitRespVO respVO = payOrderService.submitPayOrder(reqVO, getClientIP()); + PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP()); return success(respVO); } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java index 67fef0393..9a9fad090 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java @@ -76,9 +76,6 @@ public class PayRefundExcelVO { @ExcelProperty("退款成功时间") private LocalDateTime successTime; - @ExcelProperty("退款通知时间") - private LocalDateTime notifyTime; - @ExcelProperty("退款失效时间") private LocalDateTime expireTime; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java index b634909df..9100d3498 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java @@ -40,7 +40,7 @@ public class AppPayOrderController { @PostMapping("/submit") @Operation(summary = "提交支付订单") public CommonResult submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) { - PayOrderSubmitRespVO respVO = payOrderService.submitPayOrder(reqVO, getClientIP()); + PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP()); return success(PayOrderConvert.INSTANCE.convert3(respVO)); } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java index ad45cd905..647fe3dbb 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java @@ -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); diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/refund/PayRefundConvert.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/refund/PayRefundConvert.java index 738c177cd..eb2b2053d 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/refund/PayRefundConvert.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/refund/PayRefundConvert.java @@ -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 convertPage(PageResult page); - List convertList02(List 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); } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java index 9b073cc46..977eff93a 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java @@ -48,7 +48,7 @@ public class PayAppDO extends BaseDO { /** * 支付结果的回调地址 */ - private String payNotifyUrl; + private String orderNotifyUrl; /** * 退款结果的回调地址 */ diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java index c0be2b401..62b43721e 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java @@ -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; // ========== 渠道相关字段 ========== /** diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderExtensionDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderExtensionDO.java index a486e0562..15d3a96ea 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderExtensionDO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderExtensionDO.java @@ -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. 微信支付:对应 JSAPI 支付 的 out_trade_no 字段 + * 2. 支付宝支付:对应 电脑网站支付 的 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 + * 参见 参数说明 */ @TableField(typeHandler = JacksonTypeHandler.class) private Map channelExtras; + /** - * 支付渠道异步通知的内容 + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; + + /** + * 支付渠道的同步/异步通知的内容 * - * 在支持成功后,会记录回调的数据 + * 对应 {@link PayOrderRespDTO#getRawData()} */ private String channelNotifyData; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java index 8330f0075..eaf6cd876 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java @@ -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. 微信退款:对应 申请退款 的 out_refund_no 字段 + * 2. 支付宝退款:对应 的 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. 微信退款:对应 申请退款 的 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; } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/refund/PayRefundMapper.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/refund/PayRefundMapper.java index 35a6bd5a7..629650e0f 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/refund/PayRefundMapper.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/refund/PayRefundMapper.java @@ -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 { + default Long selectCountByAppId(Long appId) { + return selectCount(PayRefundDO::getAppId, appId); + } + + default PayRefundDO selectByAppIdAndMerchantRefundId(Long appId, String merchantRefundId) { + return selectOne(new LambdaQueryWrapperX() + .eq(PayRefundDO::getAppId, appId) + .eq(PayRefundDO::getMerchantRefundId, merchantRefundId)); + } + + default Long selectCountByAppIdAndOrderId(Long appId, Long orderId, Integer status) { + return selectCount(new LambdaQueryWrapperX() + .eq(PayRefundDO::getAppId, appId) + .eq(PayRefundDO::getOrderId, orderId) + .eq(PayRefundDO::getStatus, status)); + } + + default PayRefundDO selectByAppIdAndNo(Long appId, String no) { + return selectOne(new LambdaQueryWrapperX() + .eq(PayRefundDO::getAppId, appId) + .eq(PayRefundDO::getNo, no)); + } + + default int updateByIdAndStatus(Long id, Integer status, PayRefundDO update) { + return update(update, new LambdaQueryWrapper() + .eq(PayRefundDO::getId, id).eq(PayRefundDO::getStatus, status)); + } + default PageResult selectPage(PayRefundPageReqVO reqVO) { return selectPage(reqVO, new QueryWrapperX() .eqIfPresent("app_id", reqVO.getAppId()) @@ -37,16 +67,4 @@ public interface PayRefundMapper extends BaseMapperX { .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); - } - } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundTypeEnum.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderRefundStatusEnum.java similarity index 74% rename from yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundTypeEnum.java rename to yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderRefundStatusEnum.java index 5fb10a399..b9dbcb67b 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundTypeEnum.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderRefundStatusEnum.java @@ -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, "全部退款") ; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/notify/PayNotifyJob.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/notify/PayNotifyJob.java index 2907b75f3..c8b188951 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/notify/PayNotifyJob.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/notify/PayNotifyJob.java @@ -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); } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceImpl.java index 69adddc4d..45604c04c 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceImpl.java @@ -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 diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java index 71968f19b..24e7b7572 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java @@ -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); } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderExtensionService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderExtensionService.java index f707bc0df..2db86a832 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderExtensionService.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderExtensionService.java @@ -7,6 +7,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +// TODO 芋艿:合并到 PayOrder 里; /** * 支付订单 Service 接口 * diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderService.java index 890bacd62..8aadac856 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderService.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderService.java @@ -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 getOrderSubjectMap(Collection idList) { - List list = getOrderSubjectList(idList); + default Map getOrderSubjectMap(Collection ids) { + List 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); } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java index d27c52212..0e5fee42e 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java @@ -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 order = updatePayOrderSuccess(channel, orderExtension, notify); + Pair 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 updatePayOrderSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension, - PayOrderNotifyRespDTO notify) { + private Pair 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()); + } + } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java index 14f525582..ef5c15803 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java @@ -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); } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java index a5f815bdd..7914b70f6 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java @@ -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 芋艿:未实现 } } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceTest.java index 81536aba3..f25bf8942 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceTest.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceTest.java @@ -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 }); diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java index 7d62c3cc4..c225fa5b9 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java @@ -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 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)})); // 调用 diff --git a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java index a3692a4e2..e97cdc027 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java @@ -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)})); // 调用 diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 250a29d29..4cdcb3b52 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -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: # 错误码相关配置项 diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 8108af64f..d7d278273 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -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: diff --git a/yudao-ui-admin/src/api/pay/channel.js b/yudao-ui-admin/src/api/pay/channel.js index 727fde092..3f06805b7 100644 --- a/yudao-ui-admin/src/api/pay/channel.js +++ b/yudao-ui-admin/src/api/pay/channel.js @@ -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' }) } diff --git a/yudao-ui-admin/src/assets/images/pay/icon/wx_bar.svg b/yudao-ui-admin/src/assets/images/pay/icon/wx_bar.svg new file mode 100644 index 000000000..11292e6e7 --- /dev/null +++ b/yudao-ui-admin/src/assets/images/pay/icon/wx_bar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/yudao-ui-admin/src/assets/images/pay/icon/wx_native.svg b/yudao-ui-admin/src/assets/images/pay/icon/wx_native.svg new file mode 100644 index 000000000..bf3ba2b61 --- /dev/null +++ b/yudao-ui-admin/src/assets/images/pay/icon/wx_native.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/yudao-ui-admin/src/utils/constants.js b/yudao-ui-admin/src/utils/constants.js index 10819e385..781b0a0a9 100644 --- a/yudao-ui-admin/src/utils/constants.js +++ b/yudao-ui-admin/src/utils/constants.js @@ -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 网站支付" diff --git a/yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue b/yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue index b28fd4d2b..c3e691f9f 100644 --- a/yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue +++ b/yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue @@ -1,15 +1,15 @@ + + + + + +