SpringBoot + Vue 结合支付宝支付(3)--调用api

上一篇 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 {

    @Resource
    private Environment config;

    @Test
    public 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 {

    @Resource
    private Environment config;

    @Bean
    public AlipayClient alipayClient() throws AlipayApiException {
        AlipayConfig alipayConfig = new AlipayConfig();
        //设置网关地址
        alipayConfig.setServerUrl(config.getProperty("alipay.gateway-url"));
        //设置应用ID
        alipayConfig.setAppId(config.getProperty("alipay.app-id"));
        //设置应用私钥
        alipayConfig.setPrivateKey(config.getProperty("alipay.merchant-private-key"));
        //设置请求格式,固定值json
        alipayConfig.setFormat(AlipayConstants.FORMAT_JSON);
        //设置字符集
        alipayConfig.setCharset(AlipayConstants.CHARSET_UTF8);
        //设置支付宝公钥
        alipayConfig.setAlipayPublicKey(config.getProperty("alipay.alipay-public-key"));
        //设置签名类型
        alipayConfig.setSignType(AlipayConstants.SIGN_TYPE_RSA2);
        //构造client
        AlipayClient 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"
          round
          style="width: 280px; height: 44px; font-size: 18px"
          @click="toPay()"
        >
          确认支付(支付宝和微信V3)
        </el-button>
        <el-button
          :disabled="payBtnDisabled"
          type="warning"
          round
          style="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: '', //商品id
        payType: 'wxpay', //支付方式
      },
      codeUrl: '', // 二维码
      orderNo: '', //订单号
      timer: null, // 定时器
    }
  },

  //页面加载时执行
  created() {
    //获取商品列表
    productApi.list().then((response) => {
      this.productList = response.data.productList
      this.payOrder.productId = this.productList[0].id
    })
  },

  methods: {
    //选择商品
    selectItem(productId) {
      console.log('商品id:' + productId)
      this.payOrder.productId = productId
      console.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.codeUrl
          this.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.codeUrl
          this.orderNo = response.data.orderNo

          //打开二维码弹窗
          this.codeDialogVisible = true

          //启动定时器
          this.timer = setInterval(() => {
            //查询订单是否支付成功
            this.queryOrderStatus()
          }, 3000)
        })
      }
    },

    //关闭微信支付二维码对话框时让“确认支付”按钮可用
    closeDialog() {
      console.log('close.................')
      this.payBtnDisabled = false
      console.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 {

    @Resource
    private 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 {

    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private AlipayClient alipayClient;

    @Resource
    private Environment config;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public 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)
    @Override
    public 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
     */
    @Override
    public 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
     */
    @Override
    public 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 返回订单查询结果
     */
    @Override
    public 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 {

    @Resource
    private OrderInfoService orderInfoService;

    @Resource
    private 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
     */
    @Override
    public 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)
    @Override
    public 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
     */
    @Override
    public 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 退款状态
     */
    @Override
    public 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
     */
    @Override
    public 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
     */
    @Override
    public 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;
    }
  • 4
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值