Commit 68a4ef98ca52361dc31ff861a2eb27903b31b72f
1 parent
66ed61c6
mall + pay:
1. 拆分支付回调、退款回调的 URL 2. 修复微信支付回调的时间解析错误
Showing
14 changed files
with
166 additions
and
76 deletions
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; | @@ -13,14 +13,25 @@ import javax.validation.constraints.NotEmpty; | ||
| 13 | public class PayProperties { | 13 | public class PayProperties { |
| 14 | 14 | ||
| 15 | /** | 15 | /** |
| 16 | - * 回调地址 | 16 | + * 支付回调地址 |
| 17 | * | 17 | * |
| 18 | - * 实际上,对应的 PayNotifyController 的 notifyCallback 方法的 URL | 18 | + * 实际上,对应的 PayNotifyController 的 notifyOrder 方法的 URL |
| 19 | * | 19 | * |
| 20 | - * 注意,支付渠道统一回调到 payNotifyUrl 地址,由支付模块统一处理;然后,自己的支付模块,在回调 PayAppDO.payNotifyUrl 地址 | 20 | + * 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 orderNotifyUrl 地址 => 业务的 PayAppDO.orderNotifyUrl 地址 |
| 21 | */ | 21 | */ |
| 22 | - @NotEmpty(message = "回调地址不能为空") | ||
| 23 | - @URL(message = "回调地址的格式必须是 URL") | ||
| 24 | - private String callbackUrl; | 22 | + @NotEmpty(message = "支付回调地址不能为空") |
| 23 | + @URL(message = "支付回调地址的格式必须是 URL") | ||
| 24 | + private String orderNotifyUrl; | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 退款回调地址 | ||
| 28 | + * | ||
| 29 | + * 实际上,对应的 PayNotifyController 的 notifyRefund 方法的 URL | ||
| 30 | + * | ||
| 31 | + * 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 refundNotifyUrl 地址 => 业务的 PayAppDO.notifyRefundUrl 地址 | ||
| 32 | + */ | ||
| 33 | + @NotEmpty(message = "支付回调地址不能为空") | ||
| 34 | + @URL(message = "支付回调地址的格式必须是 URL") | ||
| 35 | + private String refundNotifyUrl; | ||
| 25 | 36 | ||
| 26 | } | 37 | } |
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
| @@ -22,6 +22,8 @@ public interface PayClient { | @@ -22,6 +22,8 @@ public interface PayClient { | ||
| 22 | */ | 22 | */ |
| 23 | Long getId(); | 23 | Long getId(); |
| 24 | 24 | ||
| 25 | + // ============ 支付相关 ========== | ||
| 26 | + | ||
| 25 | /** | 27 | /** |
| 26 | * 调用支付渠道,统一下单 | 28 | * 调用支付渠道,统一下单 |
| 27 | * | 29 | * |
| @@ -31,6 +33,17 @@ public interface PayClient { | @@ -31,6 +33,17 @@ public interface PayClient { | ||
| 31 | PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO); | 33 | PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO); |
| 32 | 34 | ||
| 33 | /** | 35 | /** |
| 36 | + * 解析 order 回调数据 | ||
| 37 | + * | ||
| 38 | + * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 | ||
| 39 | + * @param body HTTP 回调接口的 request body | ||
| 40 | + * @return 支付订单信息 | ||
| 41 | + */ | ||
| 42 | + PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body); | ||
| 43 | + | ||
| 44 | + // ============ 退款相关 ========== | ||
| 45 | + | ||
| 46 | + /** | ||
| 34 | * 调用支付渠道,进行退款 | 47 | * 调用支付渠道,进行退款 |
| 35 | * | 48 | * |
| 36 | * @param reqDTO 统一退款请求信息 | 49 | * @param reqDTO 统一退款请求信息 |
| @@ -39,16 +52,12 @@ public interface PayClient { | @@ -39,16 +52,12 @@ public interface PayClient { | ||
| 39 | PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO); | 52 | PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO); |
| 40 | 53 | ||
| 41 | /** | 54 | /** |
| 42 | - * 解析回调数据 | 55 | + * 解析 refund 回调数据 |
| 43 | * | 56 | * |
| 44 | * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 | 57 | * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 |
| 45 | * @param body HTTP 回调接口的 request body | 58 | * @param body HTTP 回调接口的 request body |
| 46 | - * @return 回调对象 | ||
| 47 | - * 1. {@link PayRefundRespDTO} 退款通知 | ||
| 48 | - * 2. {@link PayOrderRespDTO} 支付通知 | 59 | + * @return 支付订单信息 |
| 49 | */ | 60 | */ |
| 50 | - default Object parseNotify(Map<String, String> params, String body) { | ||
| 51 | - throw new UnsupportedOperationException("未实现 parseNotify 方法!"); | ||
| 52 | - } | 61 | + PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body); |
| 53 | 62 | ||
| 54 | } | 63 | } |
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
| @@ -97,13 +97,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa | @@ -97,13 +97,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa | ||
| 97 | 97 | ||
| 98 | @Override | 98 | @Override |
| 99 | @SneakyThrows | 99 | @SneakyThrows |
| 100 | - public Object parseNotify(Map<String, String> params, String body) { | ||
| 101 | - // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。 | ||
| 102 | - // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调 | ||
| 103 | - // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有 | ||
| 104 | - // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。 | ||
| 105 | - // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。 | ||
| 106 | - | 100 | + public PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) { |
| 107 | // 1. 校验回调数据 | 101 | // 1. 校验回调数据 |
| 108 | Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); | 102 | Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); |
| 109 | AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(), | 103 | AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(), |
| @@ -127,6 +121,16 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa | @@ -127,6 +121,16 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa | ||
| 127 | .build(); | 121 | .build(); |
| 128 | } | 122 | } |
| 129 | 123 | ||
| 124 | + @Override | ||
| 125 | + public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) { | ||
| 126 | + // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。 | ||
| 127 | + // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调 | ||
| 128 | + // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有 | ||
| 129 | + // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。 | ||
| 130 | + // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。 | ||
| 131 | + throw new UnsupportedOperationException("支付宝无退款回调"); | ||
| 132 | + } | ||
| 133 | + | ||
| 130 | // ========== 各种工具方法 ========== | 134 | // ========== 各种工具方法 ========== |
| 131 | 135 | ||
| 132 | protected String formatAmount(Integer amount) { | 136 | protected String formatAmount(Integer amount) { |
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
| @@ -14,9 +14,11 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; | @@ -14,9 +14,11 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; | ||
| 14 | import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; | 14 | import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; |
| 15 | import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; | 15 | import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; |
| 16 | import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants; | 16 | import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants; |
| 17 | +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; | ||
| 17 | import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; | 18 | import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; |
| 18 | import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; | 19 | import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; |
| 19 | import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result; | 20 | import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result; |
| 21 | +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; | ||
| 20 | import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest; | 22 | import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest; |
| 21 | import com.github.binarywang.wxpay.bean.result.WxPayRefundResult; | 23 | import com.github.binarywang.wxpay.bean.result.WxPayRefundResult; |
| 22 | import com.github.binarywang.wxpay.config.WxPayConfig; | 24 | import com.github.binarywang.wxpay.config.WxPayConfig; |
| @@ -30,8 +32,7 @@ import java.time.ZoneId; | @@ -30,8 +32,7 @@ import java.time.ZoneId; | ||
| 30 | import java.util.Map; | 32 | import java.util.Map; |
| 31 | import java.util.Objects; | 33 | import java.util.Objects; |
| 32 | 34 | ||
| 33 | -import static cn.hutool.core.date.DatePattern.PURE_DATETIME_PATTERN; | ||
| 34 | -import static cn.hutool.core.date.DatePattern.UTC_WITH_XXX_OFFSET_PATTERN; | 35 | +import static cn.hutool.core.date.DatePattern.*; |
| 35 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | 36 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 36 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; | 37 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; |
| 37 | import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; | 38 | import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; |
| @@ -61,9 +62,6 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC | @@ -61,9 +62,6 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC | ||
| 61 | WxPayConfig payConfig = new WxPayConfig(); | 62 | WxPayConfig payConfig = new WxPayConfig(); |
| 62 | BeanUtil.copyProperties(config, payConfig, "keyContent"); | 63 | BeanUtil.copyProperties(config, payConfig, "keyContent"); |
| 63 | payConfig.setTradeType(tradeType); | 64 | payConfig.setTradeType(tradeType); |
| 64 | -// if (WxPayClientConfig.API_VERSION_V2.equals(config.getApiVersion())) { | ||
| 65 | -// payConfig.setSignType(WxPayConstants.SignType.MD5); | ||
| 66 | -// } | ||
| 67 | // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决 | 65 | // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决 |
| 68 | if (Base64.isBase64(config.getKeyContent())) { | 66 | if (Base64.isBase64(config.getKeyContent())) { |
| 69 | payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath()); | 67 | payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath()); |
| @@ -162,8 +160,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC | @@ -162,8 +160,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC | ||
| 162 | } | 160 | } |
| 163 | 161 | ||
| 164 | @Override | 162 | @Override |
| 165 | - public Object parseNotify(Map<String, String> params, String body) { | ||
| 166 | - log.info("[parseNotify][微信支付回调 data 数据: {}]", body); | 163 | + public PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) { |
| 167 | try { | 164 | try { |
| 168 | // 微信支付 v2 回调结果处理 | 165 | // 微信支付 v2 回调结果处理 |
| 169 | switch (config.getApiVersion()) { | 166 | switch (config.getApiVersion()) { |
| @@ -183,21 +180,24 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC | @@ -183,21 +180,24 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC | ||
| 183 | } | 180 | } |
| 184 | 181 | ||
| 185 | private PayOrderRespDTO parseOrderNotifyV2(String body) throws WxPayException { | 182 | private PayOrderRespDTO parseOrderNotifyV2(String body) throws WxPayException { |
| 186 | - WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(body); | ||
| 187 | - Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS"); | ||
| 188 | - // 转换结果 | ||
| 189 | - return PayOrderRespDTO | ||
| 190 | - .builder() | ||
| 191 | - .outTradeNo(notifyResult.getOutTradeNo()) | ||
| 192 | - .channelOrderNo(notifyResult.getTransactionId()) | ||
| 193 | - .channelUserId(notifyResult.getOpenid()) | ||
| 194 | - .successTime(parseDateV2(notifyResult.getTimeEnd())) | 183 | + // 1. 解析回调 |
| 184 | + WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body); | ||
| 185 | + // 2. 构建结果 | ||
| 186 | + return PayOrderRespDTO.builder() | ||
| 187 | + .outTradeNo(response.getOutTradeNo()) | ||
| 188 | + .channelOrderNo(response.getTransactionId()) | ||
| 189 | + .channelUserId(response.getOpenid()) | ||
| 190 | + .status(Objects.equals(response.getResultCode(), "SUCCESS") ? | ||
| 191 | + PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus()) | ||
| 192 | + .successTime(parseDateV2(response.getTimeEnd())) | ||
| 193 | + .rawData(response) | ||
| 195 | .build(); | 194 | .build(); |
| 196 | } | 195 | } |
| 197 | 196 | ||
| 198 | private PayOrderRespDTO parseOrderNotifyV3(String body) throws WxPayException { | 197 | private PayOrderRespDTO parseOrderNotifyV3(String body) throws WxPayException { |
| 199 | WxPayOrderNotifyV3Result notifyResult = client.parseOrderNotifyV3Result(body, null); | 198 | WxPayOrderNotifyV3Result notifyResult = client.parseOrderNotifyV3Result(body, null); |
| 200 | WxPayOrderNotifyV3Result.DecryptNotifyResult result = notifyResult.getResult(); | 199 | WxPayOrderNotifyV3Result.DecryptNotifyResult result = notifyResult.getResult(); |
| 200 | + // TODO 芋艿:翻译下 state | ||
| 201 | // 转换结果 | 201 | // 转换结果 |
| 202 | Assert.isTrue(Objects.equals(notifyResult.getResult().getTradeState(), "SUCCESS"), | 202 | Assert.isTrue(Objects.equals(notifyResult.getResult().getTradeState(), "SUCCESS"), |
| 203 | "支付结果非 SUCCESS"); | 203 | "支付结果非 SUCCESS"); |
| @@ -209,6 +209,49 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC | @@ -209,6 +209,49 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC | ||
| 209 | .build(); | 209 | .build(); |
| 210 | } | 210 | } |
| 211 | 211 | ||
| 212 | + @Override | ||
| 213 | + public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) { | ||
| 214 | + try { | ||
| 215 | + // 微信支付 v2 回调结果处理 | ||
| 216 | + switch (config.getApiVersion()) { | ||
| 217 | + case API_VERSION_V2: | ||
| 218 | + return parseRefundNotifyV2(body); | ||
| 219 | + case WxPayClientConfig.API_VERSION_V3: | ||
| 220 | + return parseRefundNotifyV3(body); | ||
| 221 | + default: | ||
| 222 | + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); | ||
| 223 | + } | ||
| 224 | + } catch (WxPayException e) { | ||
| 225 | + log.error("[parseNotify][params({}) body({}) 解析失败]", params, body, e); | ||
| 226 | +// throw buildPayException(e); | ||
| 227 | + throw new RuntimeException(e); | ||
| 228 | + // TODO 芋艿:缺一个异常翻译 | ||
| 229 | + } | ||
| 230 | + } | ||
| 231 | + | ||
| 232 | + private PayRefundRespDTO parseRefundNotifyV2(String body) throws WxPayException { | ||
| 233 | + // 1. 解析回调 | ||
| 234 | + WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body); | ||
| 235 | + WxPayRefundNotifyResult.ReqInfo reqInfo = response.getReqInfo(); | ||
| 236 | + // 2. 构建结果 | ||
| 237 | + PayRefundRespDTO notify = new PayRefundRespDTO() | ||
| 238 | + .setChannelRefundNo(reqInfo.getRefundId()) | ||
| 239 | + .setOutRefundNo(reqInfo.getOutRefundNo()) | ||
| 240 | + .setRawData(response); | ||
| 241 | + if (Objects.equals("SUCCESS", reqInfo.getRefundStatus())) { | ||
| 242 | + notify.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()) | ||
| 243 | + .setSuccessTime(parseDateV2B(reqInfo.getSuccessTime())); | ||
| 244 | + } else { | ||
| 245 | + notify.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()); | ||
| 246 | + } | ||
| 247 | + return notify; | ||
| 248 | + } | ||
| 249 | + | ||
| 250 | + private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException { | ||
| 251 | + // TODO 芋艿:未实现 | ||
| 252 | + return null; | ||
| 253 | + } | ||
| 254 | + | ||
| 212 | // ========== 各种工具方法 ========== | 255 | // ========== 各种工具方法 ========== |
| 213 | 256 | ||
| 214 | /** | 257 | /** |
| @@ -246,6 +289,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC | @@ -246,6 +289,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC | ||
| 246 | return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN); | 289 | return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN); |
| 247 | } | 290 | } |
| 248 | 291 | ||
| 292 | + static LocalDateTime parseDateV2B(String time) { | ||
| 293 | + return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN); | ||
| 294 | + } | ||
| 295 | + | ||
| 249 | static String formatDateV3(LocalDateTime time) { | 296 | static String formatDateV3(LocalDateTime time) { |
| 250 | return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN); | 297 | return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN); |
| 251 | } | 298 | } |
yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java
| @@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; | 3 | import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; |
| 4 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | 4 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 5 | +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; | ||
| 5 | import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest; | 6 | import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest; |
| 6 | import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest; | 7 | import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest; |
| 7 | import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; | 8 | import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; |
| @@ -9,7 +10,6 @@ import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult; | @@ -9,7 +10,6 @@ import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult; | ||
| 9 | import com.github.binarywang.wxpay.bean.result.WxPayRefundResult; | 10 | import com.github.binarywang.wxpay.bean.result.WxPayRefundResult; |
| 10 | import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; | 11 | import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; |
| 11 | import com.github.binarywang.wxpay.config.WxPayConfig; | 12 | import com.github.binarywang.wxpay.config.WxPayConfig; |
| 12 | -import com.github.binarywang.wxpay.constant.WxPayConstants; | ||
| 13 | import com.github.binarywang.wxpay.exception.WxPayException; | 13 | import com.github.binarywang.wxpay.exception.WxPayException; |
| 14 | import com.github.binarywang.wxpay.service.WxPayService; | 14 | import com.github.binarywang.wxpay.service.WxPayService; |
| 15 | import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; | 15 | import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; |
| @@ -54,6 +54,20 @@ public class WxBarPayClientIntegrationTest { | @@ -54,6 +54,20 @@ public class WxBarPayClientIntegrationTest { | ||
| 54 | } | 54 | } |
| 55 | 55 | ||
| 56 | @Test | 56 | @Test |
| 57 | + public void testParseRefundNotifyV2() throws WxPayException { | ||
| 58 | + // 创建 config 配置 | ||
| 59 | + WxPayConfig config = buildWxPayConfigV2(); | ||
| 60 | + // 创建 WxPayService 客户端 | ||
| 61 | + WxPayService client = new WxPayServiceImpl(); | ||
| 62 | + client.setConfig(config); | ||
| 63 | + | ||
| 64 | + // 执行解析 | ||
| 65 | + String xml = "<xml><return_code>SUCCESS</return_code><appid><![CDATA[wx62056c0d5e8db250]]></appid><mch_id><![CDATA[1545083881]]></mch_id><nonce_str><![CDATA[ed8f02c21d15635cede114a42d0525a0]]></nonce_str><req_info><![CDATA[bGp+wB9DAHjoOO9Nw1iSmmIFdN2zZDhsoRWZBYdf/8bcpjowr4T8i2qjLsbMtvKQeVC5kBZOL/Agal3be6UPwnoantil+L+ojZgvLch7dXFKs/AcoxIYcVYyGka+wmnRJfUmuFRBgzt++8HOFsmJz6e2brYv1EAz+93fP2AsJtRuw1FEzodcg8eXm52hbE0KhLNqC2OyNVkn8AbOOrwIxSYobg2jVbuJ4JllYbEGIQ/6kWzNbVmMKhGJGYBy/NbUGKoQsoe4QeTQqcqQqVp08muxaOfJGThaN3B9EEMFSrog/3yT7ykVV6WQ5+Ygt89LplOf5ucWa4Ird7VJhHWtzI92ZePj4Omy1XkT1TRlwtDegA0S5MeQpM4WZ1taMrhxgmNkTUJ0JXFncx5e2KLQvbvD/HOcccx48Xv1c16JBz6G3501k8E++LWXgZ2TeNXwGsk6FyRZb0ApLyQHIx5ZtPo/UET9z3AmJCPXkrUsZ4WK46fDtbzxVPU2r8nTOcGCPbO0LUsGT6wpsuQVC4CisXDJwoZmL6kKwHfKs6mmUL2YZYzNfgoB/KgpJYSpC96kcpQyFvw+xuwqK2SXGZbAl9lADT+a83z04feQHSSIG3PCrX4QEWzpCZZ4+ySEz1Y34aoU20X9GtX+1LSwUjmQgwHrMBSvFm3/B7+IFM8OUqDB+Uvkr9Uvy7P2/KDvfy3Ih7GFcGd0C5NXpSvVTTfu1IlK/T3/t6MR/8iq78pp/2ZTYvO6eNDRJWaXYU+x6sl2dTs9n+2Z4W4AfYTvEyuxlx+aI19SqCJh7WmaFcAxidFl/9iqDjWiplb9+C6ijZv2hJtVjSCuoptIWpGDYItH7RAqlKHrx6flJD+M/5BceMHBv2w4OWCD9vPRLo8gl9o06ip0iflzO1dixhOAgLFjsQmQHNGFtR3EvCID+iS4FUlilwK+hcKNxrr0wp9Btkl9W1R9aTo289CUiIxx45skfCYzHwb+7Hqj3uTiXnep6zhCKZBAnPsDOvISXfBgXKufcFsTNtts09jX8H5/uMc9wyJ179H1cp+At1mIK2duwfo4Q9asfEoffl6Zn1olGdtEruxHGeVU0NwJ8V7RflC/Cx5RXtJ3sPJ/sHmVnBlVyR0=]]></req_info></xml>"; | ||
| 66 | + WxPayRefundNotifyResult response = client.parseRefundNotifyResult(xml); | ||
| 67 | + System.out.println(response.getReqInfo()); | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + @Test | ||
| 57 | public void testRefundV2() throws WxPayException { | 71 | public void testRefundV2() throws WxPayException { |
| 58 | // 创建 config 配置 | 72 | // 创建 config 配置 |
| 59 | WxPayConfig config = buildWxPayConfigV2(); | 73 | WxPayConfig config = buildWxPayConfigV2(); |
| @@ -101,7 +115,7 @@ public class WxBarPayClientIntegrationTest { | @@ -101,7 +115,7 @@ public class WxBarPayClientIntegrationTest { | ||
| 101 | config.setAppId("wx62056c0d5e8db250"); | 115 | config.setAppId("wx62056c0d5e8db250"); |
| 102 | config.setMchId("1545083881"); | 116 | config.setMchId("1545083881"); |
| 103 | config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim"); | 117 | config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim"); |
| 104 | - config.setSignType(WxPayConstants.SignType.MD5); | 118 | +// config.setSignType(WxPayConstants.SignType.MD5); |
| 105 | config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12"); | 119 | config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12"); |
| 106 | return config; | 120 | return config; |
| 107 | } | 121 | } |
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java
| @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.pay.controller.admin.notify; | @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.pay.controller.admin.notify; | ||
| 3 | import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; | 3 | import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; |
| 4 | import cn.iocoder.yudao.framework.pay.core.client.PayClient; | 4 | import cn.iocoder.yudao.framework.pay.core.client.PayClient; |
| 5 | import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; | 5 | import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; |
| 6 | -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; | ||
| 7 | import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; | 6 | import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; |
| 7 | +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; | ||
| 8 | import cn.iocoder.yudao.module.pay.service.order.PayOrderService; | 8 | import cn.iocoder.yudao.module.pay.service.order.PayOrderService; |
| 9 | import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; | 9 | import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; |
| 10 | import io.swagger.v3.oas.annotations.Operation; | 10 | import io.swagger.v3.oas.annotations.Operation; |
| @@ -18,7 +18,6 @@ import javax.annotation.security.PermitAll; | @@ -18,7 +18,6 @@ import javax.annotation.security.PermitAll; | ||
| 18 | import java.util.Map; | 18 | import java.util.Map; |
| 19 | 19 | ||
| 20 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | 20 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 21 | -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; | ||
| 22 | import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND; | 21 | import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND; |
| 23 | 22 | ||
| 24 | @Tag(name = "管理后台 - 支付通知") | 23 | @Tag(name = "管理后台 - 支付通知") |
| @@ -36,22 +35,14 @@ public class PayNotifyController { | @@ -36,22 +35,14 @@ public class PayNotifyController { | ||
| 36 | @Resource | 35 | @Resource |
| 37 | private PayClientFactory payClientFactory; | 36 | private PayClientFactory payClientFactory; |
| 38 | 37 | ||
| 39 | - /** | ||
| 40 | - * 统一的渠道支付回调,支付宝的退款回调 | ||
| 41 | - * | ||
| 42 | - * @param channelId 渠道编号 | ||
| 43 | - * @param params form 参数 | ||
| 44 | - * @param body request body | ||
| 45 | - * @return 成功返回 "success" | ||
| 46 | - */ | ||
| 47 | - @PostMapping(value = "/callback/{channelId}") | ||
| 48 | - @Operation(summary = "支付渠道的统一回调接口 - 包括支付回调,退款回调") | 38 | + @PostMapping(value = "/order/{channelId}") |
| 39 | + @Operation(summary = "支付渠道的统一【支付】回调") | ||
| 49 | @PermitAll | 40 | @PermitAll |
| 50 | @OperateLog(enable = false) // 回调地址,无需记录操作日志 | 41 | @OperateLog(enable = false) // 回调地址,无需记录操作日志 |
| 51 | - public String notifyCallback(@PathVariable("channelId") Long channelId, | ||
| 52 | - @RequestParam(required = false) Map<String, String> params, | ||
| 53 | - @RequestBody(required = false) String body) { | ||
| 54 | - log.info("[notifyCallback][channelId({}) 回调数据({}/{})]", channelId, params, body); | 42 | + public String notifyOrder(@PathVariable("channelId") Long channelId, |
| 43 | + @RequestParam(required = false) Map<String, String> params, | ||
| 44 | + @RequestBody(required = false) String body) { | ||
| 45 | + log.info("[notifyOrder][channelId({}) 回调数据({}/{})]", channelId, params, body); | ||
| 55 | // 1. 校验支付渠道是否存在 | 46 | // 1. 校验支付渠道是否存在 |
| 56 | PayClient payClient = payClientFactory.getPayClient(channelId); | 47 | PayClient payClient = payClientFactory.getPayClient(channelId); |
| 57 | if (payClient == null) { | 48 | if (payClient == null) { |
| @@ -60,20 +51,30 @@ public class PayNotifyController { | @@ -60,20 +51,30 @@ public class PayNotifyController { | ||
| 60 | } | 51 | } |
| 61 | 52 | ||
| 62 | // 2. 解析通知数据 | 53 | // 2. 解析通知数据 |
| 63 | - Object notify = payClient.parseNotify(params, body); | 54 | + PayOrderRespDTO notify = payClient.parseOrderNotify(params, body); |
| 55 | + orderService.notifyOrder(channelId, notify); | ||
| 56 | + return "success"; | ||
| 57 | + } | ||
| 64 | 58 | ||
| 65 | - // 3. 处理通知 | ||
| 66 | - // 3.1:退款通知 | ||
| 67 | - if (notify instanceof PayRefundRespDTO) { | ||
| 68 | - refundService.notifyRefund(channelId, (PayRefundRespDTO) notify); | ||
| 69 | - return "success"; | ||
| 70 | - } | ||
| 71 | - // 3.2:支付通知 | ||
| 72 | - if (notify instanceof PayOrderRespDTO) { | ||
| 73 | - orderService.notifyOrder(channelId, (PayOrderRespDTO) notify); | ||
| 74 | - return "success"; | 59 | + @PostMapping(value = "/refund/{channelId}") |
| 60 | + @Operation(summary = "支付渠道的统一【退款】回调") | ||
| 61 | + @PermitAll | ||
| 62 | + @OperateLog(enable = false) // 回调地址,无需记录操作日志 | ||
| 63 | + public String notifyRefund(@PathVariable("channelId") Long channelId, | ||
| 64 | + @RequestParam(required = false) Map<String, String> params, | ||
| 65 | + @RequestBody(required = false) String body) { | ||
| 66 | + log.info("[notifyRefund][channelId({}) 回调数据({}/{})]", channelId, params, body); | ||
| 67 | + // 1. 校验支付渠道是否存在 | ||
| 68 | + PayClient payClient = payClientFactory.getPayClient(channelId); | ||
| 69 | + if (payClient == null) { | ||
| 70 | + log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId); | ||
| 71 | + throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND); | ||
| 75 | } | 72 | } |
| 76 | - throw new UnsupportedOperationException("未知通知:" + toJsonString(notify)); | 73 | + |
| 74 | + // 2. 解析通知数据 | ||
| 75 | + PayRefundRespDTO notify = payClient.parseRefundNotify(params, body); | ||
| 76 | + refundService.notifyRefund(channelId, notify); | ||
| 77 | + return "success"; | ||
| 77 | } | 78 | } |
| 78 | 79 | ||
| 79 | } | 80 | } |
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java
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; | @@ -20,11 +20,11 @@ import javax.annotation.Resource; | ||
| 20 | public class PayNotifyJob implements JobHandler { | 20 | public class PayNotifyJob implements JobHandler { |
| 21 | 21 | ||
| 22 | @Resource | 22 | @Resource |
| 23 | - private PayNotifyService payNotifyCoreService; | 23 | + private PayNotifyService payNotifyService; |
| 24 | 24 | ||
| 25 | @Override | 25 | @Override |
| 26 | public String execute(String param) throws Exception { | 26 | public String execute(String param) throws Exception { |
| 27 | - int notifyCount = payNotifyCoreService.executeNotify(); | 27 | + int notifyCount = payNotifyService.executeNotify(); |
| 28 | return String.format("执行支付通知 %s 个", notifyCount); | 28 | return String.format("执行支付通知 %s 个", notifyCount); |
| 29 | } | 29 | } |
| 30 | 30 |
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java
| @@ -125,7 +125,7 @@ public class PayOrderServiceImpl implements PayOrderService { | @@ -125,7 +125,7 @@ public class PayOrderServiceImpl implements PayOrderService { | ||
| 125 | // 创建支付交易单 | 125 | // 创建支付交易单 |
| 126 | order = PayOrderConvert.INSTANCE.convert(reqDTO).setAppId(app.getId()) | 126 | order = PayOrderConvert.INSTANCE.convert(reqDTO).setAppId(app.getId()) |
| 127 | // 商户相关字段 | 127 | // 商户相关字段 |
| 128 | - .setNotifyUrl(app.getPayNotifyUrl()).setNotifyStatus(PayOrderNotifyStatusEnum.NO.getStatus()) | 128 | + .setNotifyUrl(app.getOrderNotifyUrl()).setNotifyStatus(PayOrderNotifyStatusEnum.NO.getStatus()) |
| 129 | // 订单相关字段 | 129 | // 订单相关字段 |
| 130 | .setStatus(PayOrderStatusEnum.WAITING.getStatus()) | 130 | .setStatus(PayOrderStatusEnum.WAITING.getStatus()) |
| 131 | // 退款相关字段 | 131 | // 退款相关字段 |
| @@ -206,7 +206,7 @@ public class PayOrderServiceImpl implements PayOrderService { | @@ -206,7 +206,7 @@ public class PayOrderServiceImpl implements PayOrderService { | ||
| 206 | * @return 支付渠道的回调地址 配置地址 + "/" + channel id | 206 | * @return 支付渠道的回调地址 配置地址 + "/" + channel id |
| 207 | */ | 207 | */ |
| 208 | private String genChannelPayNotifyUrl(PayChannelDO channel) { | 208 | private String genChannelPayNotifyUrl(PayChannelDO channel) { |
| 209 | - return payProperties.getCallbackUrl() + "/" + channel.getId(); | 209 | + return payProperties.getOrderNotifyUrl() + "/" + channel.getId(); |
| 210 | } | 210 | } |
| 211 | 211 | ||
| 212 | private String generateOrderExtensionNo() { | 212 | private String generateOrderExtensionNo() { |
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java
| @@ -134,7 +134,7 @@ public class PayRefundServiceImpl implements PayRefundService { | @@ -134,7 +134,7 @@ public class PayRefundServiceImpl implements PayRefundService { | ||
| 134 | .setRefundPrice(reqDTO.getPrice()) | 134 | .setRefundPrice(reqDTO.getPrice()) |
| 135 | .setOutTradeNo(orderExtension.getNo()) | 135 | .setOutTradeNo(orderExtension.getNo()) |
| 136 | .setOutRefundNo(refund.getNo()) | 136 | .setOutRefundNo(refund.getNo()) |
| 137 | - .setNotifyUrl(genChannelPayNotifyUrl(channel)) // TODO 芋艿:优化下 notifyUrl | 137 | + .setNotifyUrl(genChannelPayNotifyUrl(channel)) |
| 138 | .setReason(reqDTO.getReason()); | 138 | .setReason(reqDTO.getReason()); |
| 139 | PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); // TODO 增加一个 channelErrorCode、channelErrorMsg 字段 | 139 | PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); // TODO 增加一个 channelErrorCode、channelErrorMsg 字段 |
| 140 | // 2.3 处理退款返回 | 140 | // 2.3 处理退款返回 |
| @@ -183,7 +183,7 @@ public class PayRefundServiceImpl implements PayRefundService { | @@ -183,7 +183,7 @@ public class PayRefundServiceImpl implements PayRefundService { | ||
| 183 | * @return 支付渠道的回调地址 配置地址 + "/" + channel id | 183 | * @return 支付渠道的回调地址 配置地址 + "/" + channel id |
| 184 | */ | 184 | */ |
| 185 | private String genChannelPayNotifyUrl(PayChannelDO channel) { | 185 | private String genChannelPayNotifyUrl(PayChannelDO channel) { |
| 186 | - return payProperties.getCallbackUrl() + "/" + channel.getId(); | 186 | + return payProperties.getRefundNotifyUrl() + "/" + channel.getId(); |
| 187 | } | 187 | } |
| 188 | 188 | ||
| 189 | private String generateRefundNo() { | 189 | private String generateRefundNo() { |
yudao-server/src/main/resources/application-local.yaml
| @@ -194,7 +194,8 @@ yudao: | @@ -194,7 +194,8 @@ yudao: | ||
| 194 | - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 | 194 | - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 |
| 195 | - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 | 195 | - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 |
| 196 | pay: | 196 | pay: |
| 197 | - callback-url: http://yunai.natapp1.cc/admin-api/pay/notify/callback | 197 | + order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址 |
| 198 | + refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址 | ||
| 198 | access-log: # 访问日志的配置项 | 199 | access-log: # 访问日志的配置项 |
| 199 | enable: false | 200 | enable: false |
| 200 | error-code: # 错误码相关配置项 | 201 | error-code: # 错误码相关配置项 |
yudao-server/src/main/resources/application.yaml
| @@ -142,7 +142,7 @@ yudao: | @@ -142,7 +142,7 @@ yudao: | ||
| 142 | - /admin-api/system/captcha/check # 校验图片验证码,和租户无关 | 142 | - /admin-api/system/captcha/check # 校验图片验证码,和租户无关 |
| 143 | - /admin-api/infra/file/*/get/** # 获取图片,和租户无关 | 143 | - /admin-api/infra/file/*/get/** # 获取图片,和租户无关 |
| 144 | - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号 | 144 | - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号 |
| 145 | - - /admin-api/pay/notify/callback/* # 支付回调通知,不携带租户编号 | 145 | + - /admin-api/pay/notify/** # 支付回调通知,不携带租户编号 |
| 146 | - /jmreport/* # 积木报表,无法携带租户编号 | 146 | - /jmreport/* # 积木报表,无法携带租户编号 |
| 147 | - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号 | 147 | - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号 |
| 148 | ignore-tables: | 148 | ignore-tables: |
yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue
| 1 | <template> | 1 | <template> |
| 2 | <div> | 2 | <div> |
| 3 | <el-dialog :visible.sync="transferParam.wechatOpen" :title="title" @close="close" append-to-body width="800px"> | 3 | <el-dialog :visible.sync="transferParam.wechatOpen" :title="title" @close="close" append-to-body width="800px"> |
| 4 | - <el-form ref="wechatJsApiForm" :model="form" :rules="rules" size="medium" label-width="100px" | 4 | + <el-form ref="wechatJsApiForm" :model="form" :rules="rules" size="medium" label-width="120px" |
| 5 | v-loading="transferParam.loading"> | 5 | v-loading="transferParam.loading"> |
| 6 | <el-form-item label-width="180px" label="渠道费率" prop="feeRate"> | 6 | <el-form-item label-width="180px" label="渠道费率" prop="feeRate"> |
| 7 | <el-input v-model="form.feeRate" placeholder="请输入渠道费率" clearable :style="{width: '100%'}"> | 7 | <el-input v-model="form.feeRate" placeholder="请输入渠道费率" clearable :style="{width: '100%'}"> |
yudao-ui-admin/src/views/pay/cashier/index.vue
| @@ -331,6 +331,9 @@ export default { | @@ -331,6 +331,9 @@ export default { | ||
| 331 | * ③ close:支付已关闭 | 331 | * ③ close:支付已关闭 |
| 332 | */ | 332 | */ |
| 333 | goReturnUrl(payResult) { | 333 | goReturnUrl(payResult) { |
| 334 | + // 清理任务 | ||
| 335 | + this.clearQueryInterval(); | ||
| 336 | + | ||
| 334 | // 未配置的情况下,只能关闭 | 337 | // 未配置的情况下,只能关闭 |
| 335 | if (!this.returnUrl) { | 338 | if (!this.returnUrl) { |
| 336 | this.$tab.closePage(); | 339 | this.$tab.closePage(); |