Commit e6f414b918c8661adebe7622156e5604b3e137db
1 parent
aff9886a
pay: 接入支付宝 Wap 支付
Showing
7 changed files
with
89 additions
and
72 deletions
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<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
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(() => { | ... | ... |