上一篇 SpringBoot + Vue 结合支付宝支付(2)-- 项目搭建

项目 demo 地址:https://gitee.com/manster1231

1、配置

首先我们将我们的 阿里支付 配置文件引入到项目中 resources 目录下,然后我们为其创建配置类

package com.manster.pay.config;import com.alipay.api.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;import javax.annotation.Resource;/*** @Author manster* @Date 2022/6/4**/
@Configuration
@PropertySource("classpath:alipay-sandbox.properties")
public class AlipayClientConfig {}

首先我们先对其进行配置的测试,新建一个测试类进行测试

package com.manster.pay;import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;import javax.annotation.Resource;/*** @Author manster* @Date 2022/6/4**/
@SpringBootTest
@Slf4j
public class AlipayTests {@Resourceprivate Environment config;@Testpublic void testAlipayConfig(){log.info(config.getProperty("alipay.app-id"));}}

2、引入 SDK

1、引入依赖

然后我们导入 alipay 的 jar 包

        <dependency><groupId>com.alipay.sdk</groupId><artifactId>alipay-sdk-java</artifactId><version>4.27.1.ALL</version></dependency>

2、创建客户端连接对象

最后我们使用支付宝 SDK 签名进行验签,我们根据文档对其进行配置

package com.manster.pay.config;import com.alipay.api.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;import javax.annotation.Resource;/*** @Author manster* @Date 2022/6/4**/
@Configuration
@PropertySource("classpath:alipay-sandbox.properties")
public class AlipayClientConfig {@Resourceprivate Environment config;@Beanpublic AlipayClient alipayClient() throws AlipayApiException {AlipayConfig alipayConfig = new AlipayConfig();//设置网关地址alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));//设置应用IDalipayConfig.setAppId(config.getProperty("alipay.app-id"));//设置应用私钥alipayConfig.setPrivateKey(config.getProperty("alipay.merchant-private-key"));//设置请求格式,固定值jsonalipayConfig.setFormat(AlipayConstants.FORMAT_JSON);//设置字符集alipayConfig.setCharset(AlipayConstants.CHARSET_UTF8);//设置支付宝公钥alipayConfig.setAlipayPublicKey(config.getProperty("alipay.alipay-public-key"));//设置签名类型alipayConfig.setSignType(AlipayConstants.SIGN_TYPE_RSA2);//构造clientAlipayClient alipayClient = new DefaultAlipayClient(alipayConfig);return alipayClient;}}

3、支付功能

沙箱接入注意事项

  • 电脑网站支付支持沙箱接入;在沙箱调通接口后,必须在线上进行测试与验收,所有返回码及业务逻辑以线上为准。
  • 电脑网站支付只支持余额支付,不支持银行卡、余额宝等其他支付方式。
  • 支付时,请使用沙箱买家账号支付。
  • 如果扫二维码付款时,请使用沙箱支付宝客户端扫码付款。

Alipay API https://opendocs.alipay.com/open/028r8t?scene=22

电脑网站支付的支付接口 alipay.trade.page.pay(统一收单下单并支付页面接口)调用时序图如下:

调用流程如下:

  1. 商户系统调用 alipay.trade.page.pay(统一收单下单并支付页面接口)向支付宝发起支付请求,支付宝对商户请求参数进行校验,而后重新定向至用户登录页面。
  2. 用户确认支付后,支付宝通过 get 请求 returnUrl(商户入参传入),返回同步返回参数。
  3. 交易成功后,支付宝通过 post 请求 notifyUrl(商户入参传入),返回异步通知参数。
  4. 若由于网络等原因,导致商户系统没有收到异步通知,商户可自行调用 alipay.trade.query(统一收单线下交易查询)接口查询交易以及支付信息(商户也可以直接调用该查询接口,不需要依赖异步通知)。

注意

  • 由于同步返回的不可靠性,支付结果必须以异步通知或查询接口返回为准,不能依赖同步跳转。
  • 商户系统接收到异步通知以后,必须通过验签(验证通知中的 sign 参数)来确保支付通知是由支付宝发送的。详细验签规则请参见 异步通知验签。
  • 接收到异步通知并验签通过后,请务必核对通知中的 app_id、out_trade_no、total_amount 等参数值是否与请求中的一致,并根据 trade_status 进行后续业务处理。
  • 在支付宝端,partnerId 与 out_trade_no 唯一对应一笔单据,商户端保证不同次支付 out_trade_no 不可重复;若重复,支付宝会关联到原单据,基本信息一致的情况下会以原单据为准进行支付。

支付常量

package com.manster.pay.enums;import lombok.AllArgsConstructor;
import lombok.Getter;
import sun.net.spi.nameservice.dns.DNSNameServiceDescriptor;/*** @Author manster* @Date 2022/6/5**/
@AllArgsConstructor
@Getter
public enum OrderStatus {/*** 未支付*/NOTPAY("未支付"),/*** 支付成功*/SUCCESS("支付成功"),/*** 已关闭*/CLOSED("超时已关闭"),/*** 已取消*/CANCEL("用户已取消"),/*** 退款中*/REFUND_PROCESSING("退款中"),/*** 已退款*/REFUND_SUCCESS("已退款"),/*** 退款异常*/REFUND_ABNORMAL("退款异常");private final String type;
}
package com.manster.pay.enums;import lombok.AllArgsConstructor;
import lombok.Getter;/*** @Author manster* @Date 2022/6/5**/
@AllArgsConstructor
@Getter
public enum PayType {/*** 微信*/WXPAY("微信"),/*** 支付宝*/ALIPAY("支付宝");private final String type;}

1、统一收单下单并支付页面

https://opendocs.alipay.com/open/028r8t?scene=22

  • 前端点击下单

  • 然后会调用后端请求统一收单下单并支付页面接口

  • 支付宝接口返回表单

  • 后端将表单返回给前端

  • 前端直接执行表单提交到支付宝

  • 支付宝就会展示支付页面(扫码,或者登陆)

1、前端

我们先整理一下前端的思路

  • 首先我们将商品信息列出来(包含商品id,选中商品就会将 id 设置到对象 payOrder中)
  • 选择支付方式(点击就将方式设置到对象 payOrder中)
  • 然后我们点击支付按钮,此时我们获取到支付方式,然后执行支付方法 toPay() 调用对应的 api 接口去请求后端
  • 后端处理完之后,会返回一个字符串形式表单,此时我们将支付宝返回的表单字符串写在浏览器中,表单会自动触发submit提交跳转到支付页面

首先我们编写前端页面代码

<template><div class="bg-fa of"><section id="index" class="container"><header class="comm-title"><h2 class="fl tac"><span class="c-333">课程列表</span></h2></header><ul><li v-for="product in productList" :key="product.id"><a:class="['orderBtn',{ current: payOrder.productId === product.id },]"@click="selectItem(product.id)"href="javascript:void(0);">{{ product.title }}¥{{ product.price / 100 }}</a></li></ul><div class="PaymentChannel_payment-channel-panel"><h3 class="PaymentChannel_title">选择支付方式</h3><div class="PaymentChannel_channel-options"><!-- 选择微信 --><div:class="['ChannelOption_payment-channel-option',{ current: payOrder.payType === 'wxpay' },]"@click="selectPayType('wxpay')"><div class="ChannelOption_channel-icon"><img src="../assets/img/wxpay.png" class="ChannelOption_icon" /></div><div class="ChannelOption_channel-info"><div class="ChannelOption_channel-label"><div class="ChannelOption_label">微信支付</div><div class="ChannelOption_sub-label"></div><div class="ChannelOption_check-option"></div></div></div></div><!-- 选择支付宝 --><div:class="['ChannelOption_payment-channel-option',{ current: payOrder.payType === 'alipay' },]"@click="selectPayType('alipay')"><div class="ChannelOption_channel-icon"><img src="../assets/img/alipay.png" class="ChannelOption_icon" /></div><div class="ChannelOption_channel-info"><div class="ChannelOption_channel-label"><div class="ChannelOption_label">支付宝</div><div class="ChannelOption_sub-label"></div><div class="ChannelOption_check-option"></div></div></div></div></div></div><div class="payButtom"><el-button:disabled="payBtnDisabled"type="warning"roundstyle="width: 280px; height: 44px; font-size: 18px"@click="toPay()">确认支付(支付宝和微信V3)</el-button><el-button:disabled="payBtnDisabled"type="warning"roundstyle="width: 280px; height: 44px; font-size: 18px"@click="toPayV2()">确认支付(微信V2)</el-button></div></section><!-- 微信支付二维码 --><el-dialog:visible.sync="codeDialogVisible":show-close="false"@close="closeDialog"width="350px"center><qriously :value="codeUrl" :size="300" /><!-- <img src="../assets/img/code.png" alt="" style="width:100%"><br> -->使用微信扫码支付</el-dialog></div>
</template><script>
import productApi from '../api/product'
import wxPayApi from '../api/wxPay'
import aliPayApi from '../api/aliPay'
import orderInfoApi from '../api/orderInfo'export default {data() {return {payBtnDisabled: false, //确认支付按钮是否禁用codeDialogVisible: false, //微信支付二维码弹窗productList: [], //商品列表payOrder: {//订单信息productId: '', //商品idpayType: 'wxpay', //支付方式},codeUrl: '', // 二维码orderNo: '', //订单号timer: null, // 定时器}},//页面加载时执行created() {//获取商品列表productApi.list().then((response) => {this.productList = response.data.productListthis.payOrder.productId = this.productList[0].id})},methods: {//选择商品selectItem(productId) {console.log('商品id:' + productId)this.payOrder.productId = productIdconsole.log(this.payOrder)//this.$router.push({ path: '/order' })},//选择支付方式selectPayType(type) {console.log('支付方式:' + type)this.payOrder.payType = type//this.$router.push({ path: '/order' })},//确认支付toPay() {//禁用按钮,防止重复提交this.payBtnDisabled = true//微信支付if (this.payOrder.payType === 'wxpay') {//调用统一下单接口wxPayApi.nativePay(this.payOrder.productId).then((response) => {this.codeUrl = response.data.codeUrlthis.orderNo = response.data.orderNo//打开二维码弹窗this.codeDialogVisible = true//启动定时器this.timer = setInterval(() => {//查询订单是否支付成功this.queryOrderStatus()}, 3000)})//支付宝支付} else if (this.payOrder.payType === 'alipay') {//调用支付宝统一收单下单并支付页面接口aliPayApi.tradePagePay(this.payOrder.productId).then((response) => {//将支付宝返回的表单字符串写在浏览器中,表单会自动触发submit提交document.write(response.data.formStr)})}},//确认支付toPayV2() {//禁用按钮,防止重复提交this.payBtnDisabled = true//微信支付if (this.payOrder.payType === 'wxpay') {//调用统一下单接口wxPayApi.nativePayV2(this.payOrder.productId).then((response) => {this.codeUrl = response.data.codeUrlthis.orderNo = response.data.orderNo//打开二维码弹窗this.codeDialogVisible = true//启动定时器this.timer = setInterval(() => {//查询订单是否支付成功this.queryOrderStatus()}, 3000)})}},//关闭微信支付二维码对话框时让“确认支付”按钮可用closeDialog() {console.log('close.................')this.payBtnDisabled = falseconsole.log('清除定时器')clearInterval(this.timer)},// 查询订单状态queryOrderStatus() {orderInfoApi.queryOrderStatus(this.orderNo).then((response) => {console.log('查询订单状态:' + response.code)// 支付成功后的页面跳转if (response.code === 0) {console.log('清除定时器')clearInterval(this.timer)// 三秒后跳转到支付成功页面setTimeout(() => {this.$router.push({ path: '/success' })}, 3000)}})},},
}
</script>

其中 aliPay.js 中的接口

// axios 发送ajax请求
import request from '@/utils/request'export default{//发起支付请求tradePagePay(productId) {return request({url: '/api/ali-pay/trade/page/pay/' + productId,method: 'post'})}
}

2、后端

然后我们编写支付接口

  • 此时我们获得了商品 id,并以此进行订单的创建,然后对支付宝发送请求(携带支付成功后的跳转页面),得到响应的数据给予前端
package com.manster.pay.controller;import com.manster.pay.service.AliPayService;
import com.manster.pay.util.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;/*** @Author manster* @Date 2022/6/5**/
@CrossOrigin
@RestController
@RequestMapping("/api/ali-pay")
@Api(tags = "支付宝支付")
@Slf4j
public class AliPayController {@Resourceprivate AliPayService aliPayService;@ApiOperation("统一收单下单并支付页面接口")@PostMapping("/trade/page/pay/{productId}")public R tradePagePay(@PathVariable("productId") Long productId){log.info("统一收单下单并支付页面接口调用");//请求支付页面接口返回表单String formStr = aliPayService.tradeCreate(productId);//将form表单脚本返回前端,前端自动提交跳转支付页面return R.ok().data("formStr", formStr);}}

我们对订单进行创建,并携带支付后的跳转路径

package com.manster.pay.service.impl;import com.alibaba.fastjson.JSONObject;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.response.AlipayTradePagePayResponse;
import com.manster.pay.entity.OrderInfo;
import com.manster.pay.service.AliPayService;
import com.manster.pay.service.OrderInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;
import java.math.BigDecimal;/*** @Author manster* @Date 2022/6/5**/
@Service
@Slf4j
public class AliPayServiceImpl implements AliPayService {@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate AlipayClient alipayClient;@Resourceprivate Environment config;@Transactional(rollbackFor = Exception.class)@Overridepublic String tradeCreate(Long productId) {try {//创建订单OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.ALIPAY.getType());//调用支付宝接口AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();//配置需要的公共请求参数//设置支付成功消息异步通知接口request.setNotifyUrl(config.getProperty("alipay.notify-url"));//设置支付完成的返回页面request.setReturnUrl(config.getProperty("alipay.return-url"));JSONObject bizContent = new JSONObject();bizContent.put("out_trade_no", orderInfo.getOrderNo());BigDecimal total = new BigDecimal(orderInfo.getTotalFee().toString()).divide(new BigDecimal(100));bizContent.put("total_amount", total);bizContent.put("subject", orderInfo.getTitle());bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");request.setBizContent(bizContent.toString());//发送请求,调用支付宝AlipayTradePagePayResponse response = alipayClient.pageExecute(request);if(response.isSuccess()){log.info("调用成功 ===>" + response.getBody());return response.getBody();} else {log.info("调用失败 ===>" + response.getCode() + ",返回描述 ===>" + response.getMsg());throw new RuntimeException("创建支付交易失败");}} catch (AlipayApiException e) {e.printStackTrace();throw new RuntimeException("创建支付交易失败");}}
}

2、支付结果通知

我们需要在支付宝端完成支付的情况下,将订单的状态修改为已支付,所以我们需要异步通知(在成功支付后支付宝向我们发送请求),注意:此处我们需要使用到内网穿透

我们需要在创建支付宝请求对象时就将我们的异步通知返回接口写好

对于 PC 网站支付的交易,在用户支付完成之后,支付宝会根据 API 中商户传入的 notify_url,通过 POST 请求的形式将支付结果作为参数通知到商户系统。

            //设置支付成功消息异步通知接口request.setNotifyUrl(config.getProperty("alipay.notify-url"));

然后我们根据支付宝官方的建议编写对应的接口来修改订单状态(根据我们的封装我们只需要进行第五步的校验即可

某商户设置的通知地址为 https://商家网站通知地址,对应接收到通知的示例如下:

https: //商家网站通知地址?voucher_detail_list=[{"amount":"0.20","merchantContribute":"0.00","name":"5折券","otherContribute":"0.20","type":"ALIPAY_DISCOUNT_VOUCHER","voucherId":"2016101200073002586200003BQ4"}]&fund_bill_list=[{"amount":"0.80","fundChannel":"ALIPAYACCOUNT"},{"amount":"0.20","fundChannel":"MDISCOUNT"}]&subject=PC网站支付交易&trade_no=2016101221001004580200203978&gmt_create=2016-10-12 21:36:12&notify_type=trade_status_sync&total_amount=1.00&out_trade_no=mobile_rdm862016-10-12213600&invoice_amount=0.80&seller_id=2088201909970555&notify_time=2016-10-12 21:41:23&trade_status=TRADE_SUCCESS&gmt_payment=2016-10-12 21:37:19&receipt_amount=0.80&passback_params=passback_params123&buyer_id=2088102114562585&app_id=2016092101248425&notify_id=7676a2e1e4e737cff30015c4b7b55e3kh6& sign_type=RSA2&buyer_pay_amount=0.80&sign=***&point_amount=0.00

第一步: 在通知返回参数列表中,除去 sign、sign_type 两个参数外,凡是通知返回回来的参数皆是待验签的参数。

第二步: 将剩下参数进行 url_decode,然后进行字典排序,组成字符串,得到待签名字符串:

app_id=2016092101248425&buyer_id=2088102114562585&buyer_pay_amount=0.80&fund_bill_list=[{"amount":"0.80","fundChannel":"ALIPAYACCOUNT"},{"amount":"0.20","fundChannel":"MDISCOUNT"}]&gmt_create=2016-10-12 21:36:12&gmt_payment=2016-10-12 21:37:19&invoice_amount=0.80&notify_id=7676a2e1e4e737cff30015c4b7b55e3kh6&notify_time=2016-10-12 21:41:23&notify_type=trade_status_sync&out_trade_no=mobile_rdm862016-10-12213600&passback_params=passback_params123&point_amount=0.00&receipt_amount=0.80&seller_id=2088201909970555&subject=PC网站支付交易&total_amount=1.00&trade_no=2016101221001004580200203978&trade_status=TRADE_SUCCESS&voucher_detail_list=[{"amount":"0.20","merchantContribute":"0.00","name":"5折券","otherContribute":"0.20","type":"ALIPAY_DISCOUNT_VOUCHER","voucherId":"2016101200073002586200003BQ4"}]

第三步: 将签名参数(sign)使用 base64 解码为字节码串。

第四步: 使用 RSA 的验签方法,通过签名字符串、签名参数(经过 base64 解码)及支付宝公钥验证签名。

第五步:需要严格按照如下描述校验通知数据的正确性:

  1. 商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号;
  2. 判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额);
  3. 校验通知中的 seller_id(或者 seller_email) 是否为 out_trade_no 这笔单据的对应的操作方(有的时候,一个商户可能有多个 seller_id/seller_email);
  4. 验证 app_id 是否为该商户本身。

上述 1、2、3、4 有任何一个验证不通过,则表明本次通知是异常通知,务必忽略。 在上述验证通过后商户必须根据支付宝不同类型的业务通知,正确的进行不同的业务处理,并且过滤重复的通知结果数据。在支付宝的业务通知中,只有交易通知状态为 TRADE_SUCCESS 或 TRADE_FINISHED 时,支付宝才会认定为买家付款成功。

注意

  • 状态 TRADE_SUCCESS 的通知触发条件是商户签约的产品支持退款功能的前提下,买家付款成功;
  • 交易状态 TRADE_FINISHED 的通知触发条件是商户签约的产品不支持退款功能的前提下,买家付款成功;或者,商户签约的产品支持退款功能的前提下,交易已经成功并且已经超过可退款期限。
    @ApiOperation("支付通知")@PostMapping("/trade/notify")public String tradeNotify(@RequestParam Map<String, String> params) {log.info("========支付通知=========");String result = "failure";try {//异步通知验签boolean signVerified = AlipaySignature.rsaCheckV1(params,config.getProperty("alipay.alipay-public-key"),AlipayConstants.CHARSET_UTF8,AlipayConstants.SIGN_TYPE_RSA2); //调用SDK验证签名if(!signVerified) {//验签失败则记录异常日志,并在response中返回failure.log.error("支付成功异步验签失败");return result;}//验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验,log.info("支付成功异步验签失败");//1.商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号String outTradeNo = params.get("out_trade_no");OrderInfo order = orderInfoService.getOrderByOrderNo(outTradeNo);if(order == null){log.error("订单不存在");return result;}//2.判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额)String totalAmount = params.get("total_amount");int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal(100)).intValue();int totalFeeInt = order.getTotalFee().intValue();if(totalAmountInt != totalFeeInt){log.error("金额校验失败");return result;}//3.校验通知中的 seller_id(或者 seller_email) 是否为 out_trade_no 这笔单据的对应的// 操作方(有的时候,一个商户可能有多个 seller_id/seller_email)String sellerId = params.get("seller_id");String sellerIdProperty = config.getProperty("alipay.seller-id");if(!sellerId.equals(sellerIdProperty)){log.error("商家pid校验失败");return result;}//4.验证 app_id 是否为该商户本身。String appId = params.get("app_id");String appIdProperty = config.getProperty("alipay.app-id");if(!appId.equals(appIdProperty)){log.error("app-id校验失败");return result;}//在支付宝的业务通知中,只有交易通知状态为 TRADE_SUCCESS支付宝才会认定为买家付款成功。String tradeStatus = params.get("trade_status");if(!"TRADE_SUCCESS".equals(tradeStatus)){log.error("支付未成功");return result;}//处理业务。修改订单状态,记录支付日志aliPayService.processOrder(params);//校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure//向支付宝反馈,否则会不断发送通知(25小时内8次),对此我们处理重复通知result = "success";} catch (AlipayApiException e) {e.printStackTrace();}return result;}

其中我们的业务处理 processOrder() 进行方法的封装

  • 为了避免重复,我们只在未支付的状态下才进行重复请求
  • 并且为了防止日志多次记录,我们设置一把锁,避免同时多个线程进来,都检测到是NOTPAY状态后,都要执行记录日志
    private final ReentrantLock lock = new ReentrantLock();/*** 处理订单* @param params*/@Transactional(rollbackFor = Exception.class)@Overridepublic void processOrder(Map<String, String> params) {//获取订单号String orderNo = params.get("out_trade_no");//避免同时多个线程进来,都检测到是NOTPAY状态后,都要执行记录日志if(lock.tryLock()){try{//处理重复通知//接口幂等性:无论接口调用多少次,以下只执行一次String orderStatus = orderInfoService.getOrderStatus(orderNo);if(!OrderStatus.NOTPAY.getType().equals(orderStatus)){return;}//更新订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);//记录支付日志paymentInfoService.createPaymentInfoForAliPay(params);}finally {//主动释放锁lock.unlock();}}}

记录支付日志

package com.manster.pay.service.impl;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.gson.Gson;
import com.manster.pay.entity.PaymentInfo;
import com.manster.pay.enums.PayType;
import com.manster.pay.mapper.PaymentInfoMapper;
import com.manster.pay.service.PaymentInfoService;
import org.springframework.stereotype.Service;import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;/*** @Author manster* @Date 2022/6/4**/
@Service
public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, PaymentInfo> implements PaymentInfoService {/*** 支付宝记录支付日志* @param params*/@Overridepublic void createPaymentInfoForAliPay(Map<String, String> params) {//获取订单号String orderNo = params.get("out_trade_no");//支付系统交易编号String transactionId = params.get("trade_no");//交易状态String tradeStatus = params.get("trade_status");//交易金额String totalAmount = params.get("total_amount");int totalAmountInt = new BigDecimal(totalAmount).multiply(new BigDecimal(100)).intValue();PaymentInfo paymentInfo = new PaymentInfo();paymentInfo.setOrderNo(orderNo);paymentInfo.setPaymentType(PayType.ALIPAY.getType());paymentInfo.setTransactionId(transactionId);paymentInfo.setTradeType("电脑网站支付");paymentInfo.setTradeState(tradeStatus);paymentInfo.setPayerTotal(totalAmountInt);Gson gson = new Gson();String json = gson.toJson(params, HashMap.class);paymentInfo.setContent(json);baseMapper.insert(paymentInfo);}
}

3、统一收单交易关闭

https://opendocs.alipay.com/open/028wob

通常交易关闭是通过 alipay.trade.page.pay 中的超时时间来控制,支付宝也提供给商户 alipay.trade.close(统一收单交易关闭接口)。若用户一直未支付,商户可以调用该接口关闭指定交易;成功关闭交易后该交易不可支付。

交易关闭接口的调用时序图 alipay.trade.close(统一收单交易关闭接口)如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xxWtfhhB-1654512260847)(http://mdn.alipayobjects.com/afts/img/A*HkUJSLhOKjYAAAAAAAAAAAAAAa8wAA/original?bz=openpt_doc&t=MdzERc-hiSLTtwmo1N82cAAAAABkMK8AAAAA)]

此过程中可能会产生 “交易不存在” 的错误,这是因为在沙箱支付时,只有我们使用用户名密码登录成功或者手机扫码成功之后,支付宝才会创建这个订单,我们只是到了支付页面没有进行操作,在支付宝方该订单就是不存在的

首先我们创建取消订单的接口

    @ApiOperation("用户取消订单")@PostMapping("/trade/close/{orderNo}")public R cancel(@PathVariable String orderNo) {log.info("取消订单");aliPayService.cancelOrder(orderNo);return R.ok().setMessage("订单已取消");}

然后我们实现取消订单

    /*** 取消订单* @param orderNo*/@Overridepublic void cancelOrder(String orderNo) {//调用支付宝提供的统一收单关闭this.closeOrder(orderNo);//更新用户订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CANCEL);}/*** 用支付宝提供的统一收单关闭* @param orderNo 订单号*/private void closeOrder(String orderNo) {try {log.info("关单接口调用,订单号==> {}", orderNo);AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();JSONObject bizContent = new JSONObject();bizContent.put("out_trade_no", orderNo);request.setBizContent(bizContent.toString());AlipayTradeCloseResponse response = alipayClient.execute(request);if(response.isSuccess()){log.info("调用成功 ===>" + response.getBody());} else {log.info("调用失败 ===>" + response.getCode() + ",返回描述 ===>" + response.getMsg());}} catch (AlipayApiException e) {e.printStackTrace();throw new RuntimeException("关单接口调用失败");}}

4、统一收单线下交易查询

https://opendocs.alipay.com/open/028woa

可能在网络通信的过程中,支付宝方已经完成了交易,但是返回信息给我们的时候出现了问题导致结果没有通知过来,此时我们就需要进行查单操作了。

编写接口

    @ApiOperation("查询订单")@GetMapping("/trade/query/{orderNo}")public R queryOrder(@PathVariable String orderNo){log.info("查询订单");String result = aliPayService.queryOrder(orderNo);return R.ok().setMessage("查询成功").data("result", result);}

实现查单

    /*** 查询订单* @param orderNo 订单号* @return 返回订单查询结果*/@Overridepublic String queryOrder(String orderNo) {try {log.info("查询订单接口 ==> {}", orderNo);AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();JSONObject bizContent = new JSONObject();bizContent.put("out_trade_no", orderNo);request.setBizContent(bizContent.toString());AlipayTradeQueryResponse response = alipayClient.execute(request);if(response.isSuccess()){log.info("调用成功 ===>" + response.getBody());return response.getBody();} else {log.info("调用失败 ===>" + response.getCode() + ",返回描述 ===>" + response.getMsg());return null;}} catch (AlipayApiException e) {e.printStackTrace();throw new RuntimeException("查单接口调用失败");}}

5、商户定时查询本地订单

在进行了订单的相关操作以后,很可能在一定情况下网络出现问题,例如:

  • 我们已经将订单进行了支付,但是支付宝端在将支付成功的信息发送给我们的异步接收接口时出现了问题,那么我们本地还是未支付的状态,但是实际上已经进行了支付,此时就会出现问题。所以我们使用定时任务,定时查看支付宝端我们的订单状态,并根据其状态来进行不同的操作
  • 订单未创建,更新商户端订单状态
  • 订单未支付,调用关单接口。更新商户端订单状态
  • 订单已支付,更新商户端订单状态,记录支付日志

首先我们需要开启定时任务

@EnableScheduling

然后我们编写对应的定时任务

package com.manster.pay.task;import com.manster.pay.entity.OrderInfo;
import com.manster.pay.enums.PayType;
import com.manster.pay.service.AliPayService;
import com.manster.pay.service.OrderInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.List;/*** @Author manster* @Date 2022/6/6**/
@Slf4j
@Component
public class AliPayTask {@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate AliPayService aliPayService;/*** 从第0秒开始每隔30秒查询一次,查询创建超过5分钟并且未支付的订单*/@Scheduled(cron = "0/30 * * * * ?")public void orderConfirm() {log.info("========执行订单定时查询========");List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1, PayType.ALIPAY.getType());for (OrderInfo orderInfo : orderInfoList) {String orderNo = orderInfo.getOrderNo();log.warn("超时订单==> {}", orderNo);//核实订单状态,调用支付宝查单接口aliPayService.checkOrderStatus(orderNo);}}}

我们需要一个枚举类来判断支付宝方的订单情况

package com.manster.pay.enums.alipay;import lombok.AllArgsConstructor;
import lombok.Getter;/*** @Author manster* @Date 2022/6/6**/
@AllArgsConstructor
@Getter
public enum AliPayTradeState {/*** 交易创建,等待买家付款*/NOTPAY("WAIT_BUYER_PAY"),/*** 未付款交易超时关闭,或支付完成后全额退款*/CLOSED("TRADE_CLOSED"),/*** 交易支付成功*/SUCCESS("TRADE_SUCCESS");private final String type;}

最后我们实现根据远程支付宝端的订单状态修改本地订单状态

    /*** 根据订单号调用支付宝支付查单接口,核实订单状态* 订单未创建,更新商户端订单状态* 订单未支付,调用关单接口。更新商户端订单状态* 订单已支付,更新商户端订单状态,记录支付日志* @param orderNo*/@Overridepublic void checkOrderStatus(String orderNo) {log.warn("根据订单号核实订单状态 ===> {}", orderNo);String result = this.queryOrder(orderNo);//订单未创建if(result == null){log.warn("核实订单未创建 ===> {}", orderNo);//更新本地订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);}//解析查询订单返回的结果Gson gson = new Gson();HashMap<String, LinkedTreeMap> resultMap = gson.fromJson(result, HashMap.class);LinkedTreeMap alipayTradeQueryResponse = resultMap.get("alipay_trade_query_response");String tradeStatus = (String) alipayTradeQueryResponse.get("trade_status");//订单未支付if(AliPayTradeState.NOTPAY.getType().equals(tradeStatus)){log.warn("核实订单未支付 ===> {}", orderNo);//订单未支付进行关单this.closeOrder(orderNo);//更新商户端订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.CLOSED);}//订单已支付if(AliPayTradeState.SUCCESS.getType().equals(tradeStatus)){log.warn("核实订单已支付 ===> {}", orderNo);//更新商户端订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);//记录支付日志paymentInfoService.createPaymentInfoForAliPay(alipayTradeQueryResponse);}}

6、统一收单交易退款

https://opendocs.alipay.com/open/028sm9

当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,支付宝将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退回到买家账号上。

商户可调用 alipay.trade.refund(统一收单交易退款查询接口)接口进行退款,支付宝同步返回退款参数。调用时序图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-03LiU1Hu-1654512260848)(http://mdn.alipayobjects.com/afts/img/A*uhTNTY136OMAAAAAAAAAAAAAAa8wAA/original?bz=openpt_doc&t=v6t9V3bhEVJ0nzGYXCoudQAAAABkMK8AAAAA)]

若退款接口由于网络等原因返回异常,商户可调用退款查询接口 alipay.trade.fastpay.refund.query(统一收单交易退款查询接口)查询指定交易的退款信息。

支付宝退款支持单笔交易分多次退款,多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。

注意

  • **退款周期:**12 个月,即交易发生后 12 个月内可发起退款,超过 12 个月则不可发起退款。
  • **退款方式:**资金原路返回用户账户。
  • **退款退费:**退款时手续费不退回。
  • 一笔退款失败后重新提交,要采用原来的退款单号。
  • 总退款金额不能超过用户实际支付金额。
  • 退款信息以退款接口同步返回或者退款查询接口 alipay.trade.fastpay.refund.query(统一收单交易退款查询)为准。

首先我们编写退款接口

    @ApiOperation("申请退款")@PostMapping("/trade/refund/{orderNo}/{reason}")public R refunds(@PathVariable String orderNo, @PathVariable String reason){log.info("申请退款");aliPayService.refund(orderNo, reason);return R.ok();}

然后我们实现退款业务

    /*** 退款* @param orderNo* @param reason*/@Transactional(rollbackFor = Exception.class)@Overridepublic void refund(String orderNo, String reason) {try {//创建退款单RefundInfo refundInfo = refundInfoService.createRefundByOrderNoForAliPay(orderNo, reason);//调用退款接口AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();//组装业务对象JSONObject bizContent = new JSONObject();bizContent.put("out_trade_no", orderNo);//订单编号BigDecimal refund = new BigDecimal(refundInfo.getRefund().toString()).divide(new BigDecimal("100"));bizContent.put("refund_amount", refund);//退款金额bizContent.put("refund_reason", reason);//退款原因request.setBizContent(bizContent.toString());AlipayTradeRefundResponse response = alipayClient.execute(request);if(response.isSuccess()){log.info("调用成功 ===>" + response.getBody());//更新订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_SUCCESS);//更新退款单refundInfoService.updateRefundForAliPay(refundInfo.getRefundNo(),response.getBody(),AliPayTradeState.REFUND_SUCCESS.getType());} else {log.info("调用失败 ===>" + response.getCode() + ",返回描述 ===>" + response.getMsg());//更新订单状态orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.REFUND_ABNORMAL);//更新退款单refundInfoService.updateRefundForAliPay(refundInfo.getRefundNo(),response.getBody(),AliPayTradeState.REFUND_ERROR.getType());}} catch (AlipayApiException e) {e.printStackTrace();throw new RuntimeException("退款接口调用失败");}}

其中创建退款单和修改退款单方法为:

    /*** 创建退款单* @param orderNo 订单号* @param reason 原因* @return*/@Overridepublic RefundInfo createRefundByOrderNoForAliPay(String orderNo, String reason) {//获取订单信息OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);//生成退款订单RefundInfo refundInfo = new RefundInfo();refundInfo.setOrderNo(orderNo);refundInfo.setRefundNo(OrderNoUtils.getRefundNo());//退款订单号refundInfo.setTotalFee(orderInfo.getTotalFee());//原订单金额refundInfo.setRefund(orderInfo.getTotalFee());//退款金额refundInfo.setReason(reason);baseMapper.insert(refundInfo);return refundInfo;}/*** 修改退款单状态* @param refundNo 退款单* @param content 退款响应* @param refundStatus 退款状态*/@Overridepublic void updateRefundForAliPay(String refundNo, String content, String refundStatus) {//根据退款单编号进行退款QueryWrapper<RefundInfo> queryWrapper = new QueryWrapper<>();queryWrapper.eq("refund_no", refundNo);//设置要修改的字段RefundInfo refundInfo = new RefundInfo();refundInfo.setRefundStatus(refundStatus);refundInfo.setContentReturn(content);//更新退款单baseMapper.update(refundInfo, queryWrapper);}

退款状态的枚举类为

    /***  退款成功*/REFUND_SUCCESS("REFUND_SUCCESS"),/***  退款失败*/REFUND_ERROR("REFUND_ERROR"),

7、统一收单交易退款查询

https://opendocs.alipay.com/open/028sma

编写退款接口

    @ApiOperation("查询退款")@PostMapping("/trade/fastpay/refund/{orderNo}")public R queryRefund(@PathVariable String orderNo){log.info("申请退款");String result = aliPayService.queryRefund(orderNo);return R.ok().setMessage("查询成功").data("result", result);}

实现退款业务

    /*** 查询退款* @param orderNo* @return*/@Overridepublic String queryRefund(String orderNo) {try {AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest();JSONObject bizContent = new JSONObject();bizContent.put("out_trade_no", orderNo);bizContent.put("out_request_no", orderNo);request.setBizContent(bizContent.toString());AlipayTradeFastpayRefundQueryResponse response = alipayClient.execute(request);if(response.isSuccess()){log.info("调用成功 ===>" + response.getBody());return response.getBody();} else {log.info("调用失败 ===>" + response.getCode() + ",返回描述 ===>" + response.getMsg());return null;//订单不存在}} catch (AlipayApiException e) {e.printStackTrace();throw new RuntimeException("查退单接口调用失败");}}

8、对账

https://opendocs.alipay.com/open/028woc

点击选择日期后,点击不同类型的按钮即可发送请求,后端会返回账单所在的下载地址,此时我们直接创建一个超链接元素,并为其赋值链接地址和下载文件名称,并进行点击操作,账单即可进行下载。

<template><div class="bg-fa of"><section id="index" class="container"><header class="comm-title"><h2 class="fl tac"><span class="c-333">微信账单申请</span></h2></header><el-form :inline="true" ><el-form-item><el-date-picker v-model="billDate" value-format="yyyy-MM-dd" placeholder="选择账单日期" /></el-form-item><el-form-item><el-button type="primary" @click="downloadBill('tradebill')">下载交易账单</el-button></el-form-item><el-form-item><el-button type="primary" @click="downloadBill('fundflowbill')">下载资金账单</el-button></el-form-item></el-form></section><section id="index" class="container"><header class="comm-title"><h2 class="fl tac"><span class="c-333">支付宝账单申请</span></h2></header><el-form :inline="true" ><el-form-item><el-date-picker v-model="billDate_alipay" value-format="yyyy-MM-dd" placeholder="选择账单日期" /></el-form-item><el-form-item><el-button type="primary" @click="downloadBillAliPay('trade')">下载交易账单</el-button></el-form-item><el-form-item><el-button type="primary" @click="downloadBillAliPay('signcustomer')">下载资金账单</el-button></el-form-item></el-form></section></div>
</template><script>
import billApi from '../api/bill'export default {data () {return {billDate: '', //微信支付账单日期billDate_alipay: '' //支付宝账单日期}},methods: {//下载账单:微信支付downloadBill(type){//获取账单内容billApi.downloadBillWxPay(this.billDate, type).then(response => {console.log(response)const element = document.createElement('a')element.setAttribute('href', 'data:application/vnd.ms-excel;charset=utf-8,' + encodeURIComponent(response.data.result))element.setAttribute('download', this.billDate + '-' + type)element.style.display = 'none'element.click()})},//下载账单:支付宝downloadBillAliPay(type){billApi.downloadBillAliPay(this.billDate_alipay, type).then(response => {console.log(response.data.downloadUrl)const element = document.createElement('a')element.setAttribute('href', response.data.downloadUrl)element.setAttribute('download', this.billDate_alipay + '-' + type)element.style.display = 'none'element.click()})}}
}
</script>

bill.js

import request from '@/utils/request'export default{downloadBillWxPay(billDate, type) {return request({url: '/api/wx-pay/downloadbill/' + billDate + '/' + type,method: 'get'})},downloadBillAliPay(billDate, type) {return request({url: '/api/ali-pay/bill/downloadurl/query/' + billDate + '/' + type,method: 'get'})},
}

在点击请求后我们进行接口的编写

    @ApiOperation("获取账单url")@GetMapping("/bill/downloadurl/query/{billDate}/{type}")public R queryTradeBill(@PathVariable String billDate, @PathVariable String type){log.info("获取账单url");String downloadUrl = aliPayService.queryBill(billDate, type);return R.ok().setMessage("获取账单url成功").data("downloadUrl", downloadUrl);}
    /*** 根据日期和类型获取账单url* @param billDate 日期* @param type 类型* @return 账单url*/@Overridepublic String queryBill(String billDate, String type) {try {AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();JSONObject bizContent = new JSONObject();bizContent.put("bill_type", type);bizContent.put("bill_date", billDate);request.setBizContent(bizContent.toString());AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);if(response.isSuccess()){log.info("调用成功 ===>" + response.getBody());Gson gson = new Gson();HashMap<String, LinkedTreeMap> resultMap = gson.fromJson(response.getBody(), HashMap.class);LinkedTreeMap billDownloadurlQueryResponse = resultMap.get("alipay_data_dataservice_bill_downloadurl_query_response");String billDownloadUrl = (String) billDownloadurlQueryResponse.get("bill_download_url");return billDownloadUrl;} else {log.info("调用失败 ===>" + response.getCode() + ",返回描述 ===>" + response.getMsg());}} catch (AlipayApiException e) {e.printStackTrace();throw new RuntimeException("申请账单失败");}return null;}

SpringBoot + Vue 结合支付宝支付(3)--调用api相关推荐

  1. SpringBoot+Vue整合支付宝沙箱支付

    SpringBoot+Vue2整合实现支付宝沙箱支付 原创不易,转载请注明!!原创不易,转载请注明!!原创不易,转载请注明!!原创不易,转载请注明!!原创不易,转载请注明!! 在进行电脑网站开发时我们 ...

  2. SpringBoot+vue3对接支付宝支付详细教程

    SpringBoot+vue3对接支付宝支付详细教程 本人也是第一次做这个,是一个刚刚学习自学支付的萌新,目的是在于学习,只是为了记录自己的学习过程,怕以后会忘记,因为我没有企业账号,所以用的是自己的 ...

  3. 支付宝支付接口调用实现支付功能

    支付宝支付接口调用实现支付功能 支付宝支付 (沙箱版) 1.进入支付宝开发者平台(https://open.alipay.com/platform/home.htm) 2.进入沙箱研发服用应用 3.设 ...

  4. UniApp + SpringBoot 实现接入支付宝支付功能和退款功能

    一.支付宝开放平台设置 注册支付宝支付功能需要个体工商户或企业才可以!需要有营业执照才能去申请哦! 1.登录到控制台 进入支付宝开放平台 控制台 2.开发设置 3.产品绑定APP支付 如果没有绑定AP ...

  5. vue使用支付宝支付

    使用wap浏览器调起支付宝流程: 点击支付宝支付调用后台接口,后台会返回一个form表单,只要在vue里面创建新节点提交就可以唤起支付宝支付了 const div = document.createE ...

  6. 网站支付宝支付接口调用

    1.登录蚂蚁金服,创建应用,并且添加支付功能,支付功能必须是商户才能签约成功, 添加应用后,需要设置密钥,按支付宝提供的工具生成 支付宝公钥和商家私钥.将支付宝公钥填入rsa2密钥中即可. 2. 因为 ...

  7. springboot整合阿里支付宝支付

    支付宝支付官网 demo 沙箱环境 原理 执行过程 以下示例省略商品订单待支付界面直接模拟数据进行调用支付接口 同时未操作数据库(后续读者可自己添加即可) 代码 AliPayUtil @Slf4j p ...

  8. SpringBoot实现用支付宝(支付、查询、退款(附源码))

    SpringBoot继承支付宝(支付.查询.退款) 首先推荐个视频给各位先熟悉熟悉,怎么将支付宝的给你的老版demo运行起来 https://www.bilibili.com/video/BV1Ft4 ...

  9. python支付宝支付_python 调用支付宝支付

    支付宝接口集成,本地环境调试支付请求 1.首先登录 蚂蚁金服网站,也就是支付宝开发者平台 登录蚂蚁金服开发者平台后,创建沙箱应用 开发者平台 也就是支付宝给开发者提供的调试环境应用 沙箱测试应用 2. ...

  10. .NET ASP.NET支付宝支付接口调用实现

    此接口APP_Code下一共五个类介绍注释详细,代码如下,连个web窗体分别为同步异步,和点击去支付的点击事件,log文件夹,本人个人自己新添加的注释都有提出标注,接口压缩包地址也会留在下方. Ali ...

最新文章

  1. Docker创建 tomcat/weblogic 集群
  2. Codeforces 1322D Reality Show (DP)
  3. JAVA多线程-基础Lock Condition 并发集合
  4. 《Code:The Hidden Language Of Computer Hardware and Software》 ——笔记
  5. 如何正确使用广告素材、优化Facebook广告
  6. synchronized的使用(一)
  7. 北语在SemEval 2022释义建模任务上斩获佳绩
  8. 设计模式Demo制作bug以及反思集锦
  9. 数字图像处理-美图秀秀:大眼算法
  10. Visio 安装与操作小结
  11. 怎么做真人qq秀_【假期怎么过】看完这8部真人秀,再去英国留学!
  12. python Word批量转PDF
  13. C#选择文件的对话框和选择文件夹的对话框
  14. Android开启指纹验证
  15. java微信二维码登录
  16. 第十三章:Sqlserver2019数据库之Transact-SQL 语法基础及常用 SQL 函数总结
  17. deepin 安装到移动硬盘_系统装机|硬盘里的Linux之deepin 20系统安装教程
  18. settextstyle
  19. 单元测试cpp:Stub
  20. 炸!1024我的故事,一个写了两年博客的大厂码农!

热门文章

  1. 摆脱五彩斑斓的黑,成为七彩程序员!
  2. Ultimate Tic-Tac-Toe
  3. smartbi和第三方集成策略
  4. Zoommy for mac(图片素材搜索下载软件)
  5. P2380 狗哥采矿【普及+提高】棋盘DP
  6. MongoDB实验练习题
  7. C语言排序算法之“选择排序法”
  8. Codeforces 273D Dima and Figure
  9. 服务器手机信息报警,广东肇庆110全面开通手机短信报警服务
  10. python中interval_Python 数值区间处理_对interval 库的快速入门详解