Commit e6f414b918c8661adebe7622156e5604b3e137db

Authored by YunaiV
1 parent aff9886a

pay: 接入支付宝 Wap 支付

yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
1 1 package cn.iocoder.yudao.framework.pay.core.client.impl;
2 2  
  3 +import cn.hutool.core.date.DatePattern;
  4 +import cn.hutool.core.date.DateUtil;
  5 +import cn.hutool.core.date.LocalDateTimeUtil;
  6 +import cn.iocoder.yudao.framework.common.util.date.DateUtils;
  7 +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
3 8 import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
4 9 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
5 10 import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
... ... @@ -11,6 +16,9 @@ import lombok.extern.slf4j.Slf4j;
11 16  
12 17 import javax.validation.Validation;
13 18  
  19 +import java.time.LocalDateTime;
  20 +
  21 +import static cn.hutool.core.date.DatePattern.NORM_DATETIME_MS_FORMATTER;
14 22 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
15 23  
16 24 /**
... ... @@ -69,11 +77,6 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
69 77 this.init();
70 78 }
71 79  
72   - // TODO 芋艿:后续抽取到工具类里
73   - protected Double calculateAmount(Integer amount) {
74   - return amount / 100.0;
75   - }
76   -
77 80 @Override
78 81 public Long getId() {
79 82 return channelId;
... ... @@ -113,4 +116,14 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
113 116  
114 117 protected abstract PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
115 118  
  119 +
  120 + protected String formatAmount(Integer amount) {
  121 + return String.valueOf(amount / 100.0);
  122 + }
  123 +
  124 + protected String formatTime(LocalDateTime time) {
  125 + // "yyyy-MM-dd HH:mm:ss"
  126 + return LocalDateTimeUtil.format(time, NORM_DATETIME_MS_FORMATTER);
  127 + }
  128 +
116 129 }
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClient.java
... ... @@ -112,7 +112,7 @@ public abstract class AbstractAlipayClient extends AbstractPayClient&lt;AlipayPayCl
112 112 model.setOutTradeNo(reqDTO.getPayTradeNo());
113 113  
114 114 model.setOutRequestNo(reqDTO.getMerchantRefundId());
115   - model.setRefundAmount(calculateAmount(reqDTO.getAmount() / 2).toString());
  115 + model.setRefundAmount(formatAmount(reqDTO.getAmount() / 2).toString());
116 116 model.setRefundReason(reqDTO.getReason());
117 117  
118 118 AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java
1 1 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
2 2  
3   -import cn.hutool.core.lang.Pair;
4 3 import cn.hutool.core.map.MapUtil;
5   -import cn.hutool.core.util.ObjectUtil;
6 4 import cn.hutool.core.util.StrUtil;
7 5 import cn.hutool.http.Method;
8   -import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
9 6 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
10 7 import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
11 8 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
... ... @@ -18,12 +15,11 @@ import com.alipay.api.request.AlipayTradePagePayRequest;
18 15 import com.alipay.api.response.AlipayTradePagePayResponse;
19 16 import lombok.extern.slf4j.Slf4j;
20 17  
21   -import java.util.HashMap;
22 18 import java.util.Objects;
23 19  
24   -
25 20 /**
26   - * 支付宝【PC网站支付】的 PayClient 实现类
  21 + * 支付宝【PC 网站支付】的 PayClient 实现类
  22 + *
27 23 * 文档:https://opendocs.alipay.com/open/270/105898
28 24 *
29 25 * @author XGD
... ... @@ -32,7 +28,8 @@ import java.util.Objects;
32 28 public class AlipayPcPayClient extends AbstractAlipayClient {
33 29  
34 30 public AlipayPcPayClient(Long channelId, AlipayPayClientConfig config) {
35   - super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config, new AlipayPayCodeMapping());
  31 + super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config,
  32 + new AlipayPayCodeMapping());
36 33 }
37 34  
38 35 @Override
... ... @@ -42,8 +39,10 @@ public class AlipayPcPayClient extends AbstractAlipayClient {
42 39 // ① 通用的参数
43 40 model.setOutTradeNo(reqDTO.getMerchantOrderId());
44 41 model.setSubject(reqDTO.getSubject());
45   - model.setTotalAmount(String.valueOf(calculateAmount(reqDTO.getAmount())));
46   - model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前电脑支付场景下仅支持 FAST_INSTANT_TRADE_PAY
  42 + model.setBody(reqDTO.getBody());
  43 + model.setTotalAmount(formatAmount(reqDTO.getAmount()));
  44 + model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
  45 + model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
47 46 // ② 个性化的参数
48 47 // 参考 https://www.pingxx.com/api/支付渠道 extra 参数说明.html 的 alipay_pc_direct 部分
49 48 model.setQrPayMode(MapUtil.getStr(reqDTO.getChannelExtras(), "qr_pay_mode"));
... ... @@ -71,10 +70,8 @@ public class AlipayPcPayClient extends AbstractAlipayClient {
71 70 }
72 71  
73 72 // 2.2 处理结果
74   - System.out.println(response.getBody());
75 73 PayOrderUnifiedRespDTO respDTO = new PayOrderUnifiedRespDTO()
76 74 .setDisplayMode(displayMode).setDisplayContent(response.getBody());
77   - // 响应为表单格式,前端可嵌入响应的页面或关闭当前支付窗口
78 75 return PayCommonResult.build(StrUtil.blankToDefault(response.getCode(),"10000"),
79 76 response.getMsg(), respDTO, codeMapping);
80 77 }
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java
... ... @@ -31,7 +31,7 @@ public class AlipayQrPayClient extends AbstractAlipayClient {
31 31 model.setOutTradeNo(reqDTO.getMerchantOrderId());
32 32 model.setSubject(reqDTO.getSubject());
33 33 model.setBody(reqDTO.getBody());
34   - model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString()); // 单位:元
  34 + model.setTotalAmount(formatAmount(reqDTO.getAmount()).toString()); // 单位:元
35 35 // TODO 芋艿:userIp + expireTime
36 36 // 构建 AlipayTradePrecreateRequest
37 37 AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java
1 1 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
2 2  
3   -import cn.hutool.core.date.DateUtil;
  3 +import cn.hutool.core.util.ObjectUtil;
  4 +import cn.hutool.core.util.StrUtil;
4 5 import cn.hutool.http.Method;
5 6 import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
6 7 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
  8 +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO;
7 9 import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
  10 +import cn.iocoder.yudao.framework.pay.core.enums.PayDisplayModeEnum;
8 11 import com.alipay.api.AlipayApiException;
9 12 import com.alipay.api.domain.AlipayTradeWapPayModel;
10 13 import com.alipay.api.request.AlipayTradeWapPayRequest;
... ... @@ -14,7 +17,8 @@ import lombok.extern.slf4j.Slf4j;
14 17 import java.util.Objects;
15 18  
16 19 /**
17   - * 支付宝【手机网站】的 PayClient 实现类
  20 + * 支付宝【手机 网站】的 PayClient 实现类
  21 + *
18 22 * 文档:https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
19 23 *
20 24 * @author 芋道源码
... ... @@ -22,31 +26,32 @@ import java.util.Objects;
22 26 @Slf4j
23 27 public class AlipayWapPayClient extends AbstractAlipayClient {
24 28  
25   -
26 29 public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) {
27   - super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config, new AlipayPayCodeMapping());
  30 + super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config,
  31 + new AlipayPayCodeMapping());
28 32 }
29 33  
30 34 @Override
31   - public PayCommonResult<AlipayTradeWapPayResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
32   - // 构建 AlipayTradeWapPayModel 请求
  35 + public PayCommonResult<PayOrderUnifiedRespDTO> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
  36 + // 1.1 构建 AlipayTradeWapPayModel 请求
33 37 AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
  38 + // ① 通用的参数
34 39 model.setOutTradeNo(reqDTO.getMerchantOrderId());
35 40 model.setSubject(reqDTO.getSubject());
36 41 model.setBody(reqDTO.getBody());
37   - model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString());
38   - model.setProductCode("QUICK_WAP_PAY"); // TODO 芋艿:这里咋整
39   - //TODO 芋艿:这里咋整 jason @芋艿 可以去掉吧,
40   - // TODO 芋艿 似乎这里不用传sellerId
41   - // https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
42   - //model.setSellerId("2088102147948060");
43   - model.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(),"yyyy-MM-dd HH:mm:ss"));
44   - // TODO 芋艿:userIp
45   - // 构建 AlipayTradeWapPayRequest
  42 + model.setTotalAmount(formatAmount(reqDTO.getAmount()));
  43 + model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
  44 + // ② 个性化的参数【无】
  45 + // ③ 支付宝 Wap 支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开
  46 + String displayMode = ObjectUtil.defaultIfNull(reqDTO.getDisplayMode(),
  47 + PayDisplayModeEnum.URL.getMode());
  48 +
  49 + // 1.2 构建 AlipayTradeWapPayRequest 请求
46 50 AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
47 51 request.setBizModel(model);
48 52 request.setNotifyUrl(reqDTO.getNotifyUrl());
49   - request.setReturnUrl(reqDTO.getReturnUrl());
  53 + request.setReturnUrl(reqDTO.getReturnUrl()); // TODO 芋艿,待搞
  54 + model.setQuitUrl(reqDTO.getReturnUrl()); // TODO 芋艿,待搞
50 55  
51 56 // 执行请求
52 57 AlipayTradeWapPayResponse response;
... ... @@ -57,21 +62,11 @@ public class AlipayWapPayClient extends AbstractAlipayClient {
57 62 }
58 63 System.out.println(response.getBody());
59 64  
60   - // TODO 芋艿:sub Code
61   - if(response.isSuccess() && Objects.isNull(response.getCode()) && Objects.nonNull(response.getBody())){
62   - //成功alipay wap 成功 code 为 null , body 为form 表单
63   - return PayCommonResult.build("-9999", "Success", response, codeMapping);
64   - }else {
65   - return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
66   - }
  65 + // 2.2 处理结果
  66 + PayOrderUnifiedRespDTO respDTO = new PayOrderUnifiedRespDTO()
  67 + .setDisplayMode(displayMode).setDisplayContent(response.getBody());
  68 + return PayCommonResult.build(StrUtil.blankToDefault(response.getCode(),"10000"),
  69 + response.getMsg(), respDTO, codeMapping);
67 70 }
68 71  
69   -
70   -
71   -
72   -
73   -
74   -
75   -
76   -
77 72 }
... ...
yudao-ui-admin/src/utils/constants.js
... ... @@ -162,6 +162,9 @@ export const PayDisplayModeEnum = {
162 162 },
163 163 FORM: {
164 164 "mode": "form"
  165 + },
  166 + QR_CODE: {
  167 + "mode": "qr_code"
165 168 }
166 169 }
167 170  
... ...
yudao-ui-admin/src/views/pay/order/submit.vue
... ... @@ -44,7 +44,7 @@
44 44 <!-- 展示形式:二维码 URL -->
45 45 <el-dialog :title="qrCode.title" :visible.sync="qrCode.visible" width="350px" append-to-body
46 46 :close-on-press-escape="false">
47   - <qrcode-vue :value="qrCode.url" size="310" level="H" />
  47 + <qrcode-vue :value="qrCode.url" size="310" level="L" />
48 48 </el-dialog>
49 49  
50 50 <!-- 展示形式:IFrame -->
... ... @@ -53,7 +53,7 @@
53 53 <iframe :src="iframe.url" width="100%" />
54 54 </el-dialog>
55 55  
56   - <!-- 展示形式: -->
  56 + <!-- 展示形式:Form -->
57 57 <div ref="formRef" v-html="form.value" />
58 58  
59 59 </div>
... ... @@ -162,20 +162,15 @@ export default {
162 162 ...this.buildSubmitParam(channelCode)
163 163 }).then(response => {
164 164 const data = response.data
165   - if (data.displayMode === 'iframe') {
  165 + if (data.displayMode === PayDisplayModeEnum.IFRAME.mode) {
166 166 this.displayIFrame(channelCode, data)
167   - } else if (data.displayMode === 'url') {
  167 + } else if (data.displayMode === PayDisplayModeEnum.URL.mode) {
168 168 this.displayUrl(channelCode, data)
169   - } else if (data.displayMode === 'form') {
  169 + } else if (data.displayMode === PayDisplayModeEnum.FORM.mode) {
170 170 this.displayForm(channelCode, data)
  171 + } else if (data.displayMode === PayDisplayModeEnum.QR_CODE.mode) {
  172 + this.displayQrCode(channelCode, data)
171 173 }
172   - // 不同的支付,调用不同的策略
173   - // if (channelCode === PayChannelEnum.ALIPAY_QR.code) {
174   - // this.submitAfterAlipayQr(invokeResponse)
175   - // } else if (channelCode === PayChannelEnum.ALIPAY_PC.code
176   - // || channelCode === PayChannelEnum.ALIPAY_WAP.code) {
177   - // this.submitAfterAlipayPc(invokeResponse)
178   - // }
179 174  
180 175 // 打开轮询任务
181 176 this.createQueryInterval()
... ... @@ -183,7 +178,7 @@ export default {
183 178 },
184 179 /** 构建提交支付的额外参数 */
185 180 buildSubmitParam(channelCode) {
186   - // 支付宝网页支付时,有多种展示形态
  181 + // ① 支付宝 PC 支付时,有多种展示形态
187 182 if (channelCode === PayChannelEnum.ALIPAY_PC.code) {
188 183 // 情况【前置模式】:将二维码前置到商户的订单确认页的模式。需要商户在自己的页面中以 iframe 方式请求支付宝页面。具体支持的枚举值有以下几种:
189 184 // 0:订单码-简约前置模式,对应 iframe 宽度不能小于 600px,高度不能小于 300px
... ... @@ -221,16 +216,13 @@ export default {
221 216 // displayMode: PayDisplayModeEnum.FORM.mode
222 217 // }
223 218 }
224   - return {}
225   - },
226   - /** 提交支付后(支付宝扫码支付) */
227   - submitAfterAlipayQr(invokeResponse) {
228   - this.qrCode = {
229   - title: '请使用支付宝“扫一扫”扫码支付',
230   - url: invokeResponse.qrCode,
231   - visible: true
  219 + // ② 支付宝 Wap 支付时,引导手机扫码支付
  220 + if (channelCode === PayChannelEnum.ALIPAY_WAP.code) {
  221 + return {
  222 + displayMode: PayDisplayModeEnum.QR_CODE.mode
  223 + }
232 224 }
233   - this.submitLoading = false
  225 + return {}
234 226 },
235 227 /** 提交支付后,IFrame 内置 URL 的展示形式 */
236 228 displayIFrame(channelCode, data) {
... ... @@ -262,9 +254,26 @@ export default {
262 254 }, 1000);
263 255 });
264 256 },
  257 + /** 提交支付后(支付宝扫码支付) */
  258 + displayQrCode(channelCode, data) {
  259 + let title = '请使用手机浏览器“扫一扫”';
  260 + if (channelCode === PayChannelEnum.ALIPAY_WAP.code) {
  261 + // 考虑到 WAP 测试,所以引导手机浏览器搞
  262 + } else if (channelCode.indexOf('alipay_') === 0) {
  263 + title = '请使用支付宝“扫一扫”扫码支付';
  264 + } else if (channelCode.indexOf('wx_') === 0) {
  265 + title = '请使用微信“扫一扫”扫码支付';
  266 + }
  267 + this.qrCode = {
  268 + title: title,
  269 + url: data.displayContent,
  270 + visible: true
  271 + }
  272 + this.submitLoading = false
  273 + },
265 274 /** 轮询查询任务 */
266 275 createQueryInterval() {
267   - if (!this.interval) {
  276 + if (this.interval) {
268 277 return
269 278 }
270 279 this.interval = setInterval(() => {
... ...