一些重要的网址或文档
  • 支付宝支付官网:https://www.alipay.com/
  • 支付宝开放平台-文档中心:https://opendocs.alipay.com/open/54/00y8k9
  • natapp内网穿透工具官网:https://natapp.cn/
发起支付的业务流程图

图片来自支付宝开放平台。

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_支付宝

上面的流程图,已经大致描述了发起支付的过程,这里笔者用自己的语言,详细地转化一下:

下单。这里的下单,在项目中往往表现为用户确认了订单、然后选择支付方式、点击立即支付,即当用户点击“立即支付”时才产生的动作。 这一步要发送一个请求到我们的服务器,假如说系统中立即支付的API路径是/pay,参数一般就是订单包含的订单项,如商品id、购买数量、优惠券id等。

服务器端处理用户的下单请求。在这一步,是业务比较多的一步,也是关键步骤。我们根据客户端传来的商品id、数量、优惠券等参数,计算出每一个商品的单价、订单的总金额等(金额是不应该由客户端直接传的,应该由服务器端计算,更安全)。再生成一个订单号。然后调用一个接口,把这些参数(如商品名称、支付金额、订单号等)打包传给支付宝,也就是对应上图中的1.1。

依然是处理用户的下单请求,只是我嫌段落太长,换一段。在图中的1.1,我们调用了支付宝的接口。其实图中还少了一步1.2,那就是支付宝会返回一段html文档,就是大家经常看到的带二维码的PC端支付页面。请注意,到这一步,对客户端来说依然是在访问/pay接口,因为我们还没有对请求作出响应,只是一直在和支付宝交互罢了。所以,我们把支付宝返回的html文档作为响应信息,返回给客户端,客户端直接加载这一段html文档,用户就看到了一个支付页面。

到了这里,对应上图中的2、3、4、5步,和我们的服务器就没关系了,完全是用户在进行支付的过程。

到了第6步,用户支付完成,支付宝会跳转到一个我们指定的网页,这个网页一般是我们自定义的页面,比如在网页上显示“您已成功购买xxx商品,感谢您的支持”等。注意,此处的returnUrl只是跳转到我们自定义的友好页面,告诉用户支付成功了,只做展示用途,并非真正的支付回调。

到了第7步,支付宝会调用我们指定的回调API接口,将一些参数传给我们,如交易流水号、订单号,我们可以根据这些信息查询交易是否真的成功了(第8步),从而执行后续的业务,比如将订单状态变为已支付、给用户增加积分、扣减优惠券……等。

沙箱环境简介&基础开发配置

支付宝有一个供开发者测试使用的沙箱环境,会提供一个沙箱版的支付宝app、一个商家账户、一个买家账户。有了这个,可以让我们跳过商家入驻、企业资质审核等过程,开箱即用,降低了学习成本。

现在,让我们简单的配置沙箱环境。

1、进入到支付宝支付官网,点击“我是开发者”,在新的页面右上角,用你自己的支付宝扫码登录,再点击开发服务中的研发服务:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_spring_02

此时就到了沙箱应用的控制台,会为你分配一个APPID:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_支付宝_03

2、出于安全性的考虑,我们需要生成一份公钥和私钥,公钥提供给支付宝,支付宝对数据进行加密;私钥用于解析支付宝传来的加密数据,由我们自行保管。支付宝提供了公钥和私钥的生成工具,在文档中心点击“开发工具”:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_http_04

我们打开下载好的工具,在以下地方点击“生成密钥”:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_spring_05

把私钥和公钥保存好。

3、回到沙箱应用控制台,点击这个地方设置公钥(注意不是公钥证书哦,我们用的不是证书的方式):

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_支付宝_06

在弹出的窗口,把刚才生成的公钥复制粘贴进去,点击确定:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_html_07


其中支付宝公钥就是我们上传的公钥,应用公钥是支付宝为我们自动生成的。在沙箱应用控制台,扫码下载沙箱版的支付宝(目前只有安卓版),提供了一个商家号、一个买家号,其中买家号里面有10万块钱(可惜不能提现呵呵呵),后面支付的时候会用。

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_支付宝_08

创建Spring Boot项目

创建Spring Boot项目的过程我就省略了~~~~

这里只贴一下我的pom.xml依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.chill</groupId>
    <artifactId>alipay</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>alipay</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-easysdk -->
        <dependency>
            <groupId>com.alipay.sdk</groupId>
            <artifactId>alipay-easysdk</artifactId>
            <version>2.2.0</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

只有基本的web、lombok、支付宝的sdk依赖,下面介绍一下我的项目结构:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_html_09

  • PaymentBO是接收controller层的入参,如商品id、购买数量等等,结合具体的业务而定,我在这里只是使项目更规范,所以这个类省略了很多字段;
  • PayController是控制器,负责分发请求,主要是处理用户的支付请求,以及支付宝的回调;
  • ProjectInit是项目初始化时执行的一些操作,适合在整个过程中只需要执行一次的业务;
  • PayService是真正的服务层,主要的业务代码在这里;
  • OrderUtil是一个订单工具类,主要是生成随机订单号,当然这个类只是简单的实现一下;
  • index.htm是模拟下单支付的页面;
  • return.html是支付完成之后的友好页面。
编写支付页面和回调页面

在index.html编写如下代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>确认订单</title>
</head>
<body>
<div>
    您的订单信息如下,请确认无误后支付:<br />
    xxxxxxxxxx省略订单信息xxxxxxxxxx

    <form>
        <button type="submit">确认支付</button>
    </form>
</div>
</body>
</html>

在return.html编写以下代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>支付成功</title>
</head>
<body>
    <div>
        您已成功购买此商品,感谢您的支持
    </div>
</body>
</html>
搭建内网穿透

回调地址肯定要外网可以访问,要不然支付宝怎么会调用得到呢?但是我们作为开发者可能没有自己到服务器和域名,所以我们使用内网穿透工具natapp获取临时域名,很简单:

  • 1.去https://natapp.cn/ 注册一个账号
  • android studio设置支付宝沙箱环境 支付宝沙箱支付流程_http_10

  • 注册完之后登录
  • 2.登录页面
  • android studio设置支付宝沙箱环境 支付宝沙箱支付流程_支付宝_11

  • 我这个是已经弄过免费隧道了 一开始是没有这个免费隧道的 实名认证完毕后获取免费隧道 获取成功后先修改配置 因为boot项目的端口我默认是8080
    不知道你们是什么 可以通过自己的适配自行修改
  • android studio设置支付宝沙箱环境 支付宝沙箱支付流程_spring_12

  • 3.点击下载客户端 先去下载客户端 做映射
  • android studio设置支付宝沙箱环境 支付宝沙箱支付流程_http_13

  • 安装成功后解压安装包 运行 natapp.exe 会弹出以下界面:
  • android studio设置支付宝沙箱环境 支付宝沙箱支付流程_spring_14

  • 然后在这个cmd窗口输入命令 natapp -authtoken 你的token,回车执行,会出现以下内容:

注意:你的token需要从natapp上获取,就是刚才免费隧道那里的authtoken。

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_spring_15


注意:在项目运行时 这个cmd窗口不能关闭 一旦关闭 通过以上域名去访问boot项目的资源时会404 找不到该页面映射完成之后,启动项目,用临时域名测试一下上面的页面:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_http_16

配置Spring Boot项目&初始化支付宝SDK

application.yml配置文件,将公钥、私钥、APPID、支付宝的网关、页面回调、接口回调等信息配置进来:

alipay:
  appId: 换成你自己的APPID
  privateKey: 换成你自己的私钥
  publicKey: 换成你自己的公钥
  #支付网关配置,这一项是写死的,正式环境是openapi.alipay.com
  gateway: openapi.alipaydev.com
  #支付成功之后的回调页面,只是一个友好页面。主要换成你自己映射的临时域名
  returnUrl: http://frnqxw.natappfree.cc/return.html
  #支付成功的接口回调,我们还没写,先空着
  notifyUrl:

接下来我们就要读取支付宝的文档了:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_html_17

新版本的SDK 把大部分操作都封装到了Factory类中,但是Factory类在使用之前需要我们配置一下参数,且全局配置一次即可,所以我们把这部分代码拿到ProjectInit类中,在项目启动时执行一次:

@Component
package com.chill.init;
 
import com.alipay.easysdk.factory.Factory;
import com.alipay.easysdk.kernel.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
 
/**
 *  项目初始化
 *
 * @author wangziyang
 * @date 2020-09-08
 * */
@Component
public class ProjectInit implements ApplicationRunner {
 
    //应用id
    @Value("${alipay.appId}")
    private String appId;
 
    //私钥
    @Value("${alipay.privateKey}")
    private String privateKey;
 
    //公钥
    @Value("${alipay.publicKey}")
    private String publicKey;
 
    //支付宝网关
    @Value("${alipay.gateway}")
    private String gateway;
 
    //支付成功后的接口回调地址,不是回调的友好页面,不要弄混了
    @Value("${alipay.notifyUrl}")
    private String notifyUrl;
 
    /**
     *  项目初始化事件
     * */
    @Override
    public void run(ApplicationArguments args) throws Exception {
        //初始化支付宝SDK
        Factory.setOptions(getOptions());
        System.out.println("**********支付宝SDK初始化完成**********");
    }

    /**
     * 设置一些选项
     * @return
     */
    private Config getOptions() {
        //这里省略了一些不必要的配置,可参考文档的说明
        Config config = new Config();
        //设置请求的协议
        config.protocol = "https";
        //设置网关端口
        config.gatewayHost = this.gateway;
        //设置标记类型
        config.signType = "RSA2";
 
        config.appId = this.appId;
 
        // 为避免私钥随源码泄露,推荐从文件中读取私钥字符串而不是写入源码中
        config.merchantPrivateKey = this.privateKey;
 
        //注:如果采用非证书模式,则无需赋值上面的三个证书路径,改为赋值如下的支付宝公钥字符串即可
        config.alipayPublicKey = this.publicKey;
 
        //可设置异步通知接收服务地址(可选)
        config.notifyUrl = notifyUrl;
 
        return config;
    }
}

再次启动项目:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_html_18

编写下单支付接口
既然要下单,肯定少不了传参,所以我们这里简单的声明一个PaymentBO类:

package com.chill.bo;
 
import lombok.Data;
import java.math.BigDecimal;
 
/**
 *  发起支付时的参数
 * */
@Data
public class PaymentBO {
    //省略其他的业务参数,如商品id、购买数量等
 
    //商品名称
    private String subject;
 
    //总金额
    private BigDecimal total = BigDecimal.ZERO;
}

然后在OrderUtil编写如下代码:

package com.chill.util;
 
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
 
public class OrderUtil {
 
    /**
     *  根据时间戳生成订单号
     * */
    public static String getOrderNo () {
        DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
        LocalDateTime localDateTime = Instant.ofEpochMilli(System.currentTimeMillis()).atZone(ZoneOffset.ofHours(8)).toLocalDateTime();
        return df.format(localDateTime);
    }
}

接下来,继续看文档:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_http_19


android studio设置支付宝沙箱环境 支付宝沙箱支付流程_spring_20

按照文档描述,我们只需要Factory.Payment.Page.pay就可以发起一个支付请求,比之前还是简洁了不少,其他操作都封装好了。所以我们在PayService编写以下代码:

package com.chill.service;

import com.alipay.easysdk.factory.Factory;
import com.alipay.easysdk.payment.page.models.AlipayTradePagePayResponse;
import com.chill.bo.PaymentBO;
import com.chill.util.OrderUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

/**
 *  支付宝支付,业务实现
 *
 * @author chill
 * */
@Service
public class PayService {

    //支付成功后要跳转的页面
    @Value("${alipay.returnUrl}")
    private String returnUrl;

    /**
     *  下单支付
     * */
    public Object pay (PaymentBO bo) throws Exception {

        //从存储介质(如MySQL、Redis)查询商品信息、总金额等敏感信息

        //…………省略相关代码,这里直接赋值…………

        bo.setSubject("测试商品");
        bo.setTotal(new BigDecimal(10.00));

        //调用sdk,发起支付
        AlipayTradePagePayResponse response = Factory.Payment
                //选择网页支付平台
                .Page()
                //调用支付方法,设置订单名称、我们自己系统中的订单号、金额、回调页面
                .pay(bo.getSubject() , OrderUtil.getOrderNo(), bo.getTotal().toString() , returnUrl);

        //这里的response.body,就是一个可以直接加载的html片段,
        // 这里为了简单我直接返回这个片段,前端直接
        return response.body;
    }
}

再往下,编写PayController:

package com.chill.controller;
 
import com.chill.bo.PaymentBO;
import com.chill.service.PayService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;

/**
 *  支付宝支付,控制器
 *
 * @author chill
 * */
@RestController
@RequestMapping(value = "/pay")
@AllArgsConstructor
public class PayController {

    private PayService payService;
 
    /**
     *  下单支付
     * */
    @GetMapping(value = "/confirm" , produces = {"text/html;charset=UTF-8"})
    public Object pay () throws Exception {
        //这个接口其实应该是post方式的,但是我这里图方便,直接以get方式访问,
        //且返回格式是text/html,这样前端页面就能直接显示支付宝返回的html片段
        //真实场景下由post方式请求,返回code、msg、data那种格式的标准结构,让前端拿到data里的        
        //html片段之后自行加载
 
        //由于我这里并没有真正的传参数,所以象征性的new一下,避免空指针
        PaymentBO bo = new PaymentBO();
        return payService.pay(bo);
    }
}

在index.html页面做如下修改,点击支付的时候访问我们的/pay/confirm接口:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>确认订单</title>
</head>
<body>
    <div>
        您的订单信息如下,请确认无误后支付:<br />
        xxxxxxxxxx省略订单信息xxxxxxxxxx
 
        <form enctype="multipart/form-data" action="/pay/confirm" method="get">
            <button type="submit">确认支付</button>
        </form>
    </div>
</body>
</html>

运行项目,点击确认支付就看到了以下页面(有时候下面的页面会提示有钓鱼风险,换一个浏览器就好,或者关了浏览器重来,这个无解):

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_html_21

在手机上打开沙箱版支付宝,登录买家帐号,扫码支付:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_支付宝_22


android studio设置支付宝沙箱环境 支付宝沙箱支付流程_spring_23


android studio设置支付宝沙箱环境 支付宝沙箱支付流程_http_24

下单支付的定制化参数

上面是最简单的支付。大家可以在API文档看到,下单支付还有很多参数可选,如订单描述、商品类型、订单商品列表的具体信息等等……

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_支付宝_25

公共参数就不用说了,Factory都为我们封装好了;但是下面的业务请求参数,也就是公共参数里的biz_content,如何传呢?pay()方法只提供了基本的4个参数,那可选的请求参数怎么传呢?

对于这个问题,新版的SDK指出需要由我们自己扩展,方式在SDK说明中有:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_支付宝_26

这个还是挺简单的,就是往Map里放参数,最后通过optional或batchOptional方法进行设置:

Factory.Payment
                .Page()
                .batchOptional(Collections.emptyMap());

所以在这里我就不展示了,大家下去自己扩展就行,有问题联系我。

支付完成的回调

前面的回调只是在页面上回显信息,我们需要提供一个接口,供支付宝调用,接收支付宝传来的参数。

首先,我们看文档,看支付宝都返回了哪些信息:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_spring_27

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_spring_28

参数有很多,我就不一一列出了。主要的参数是商户订单号(即我们自己生成的订单号)和交易状态, 我们根据订单号是否交易成功,进行后面的业务逻辑,比如给用户加积分、将订单状态变为已支付、发送业务短信、通知商家接单……等等。

参数很多,所以我就不新建实体类了,简单的用Map接一下,在PayController新增以下接口(注意是post方式):

package com.chill.controller;
 
import com.chill.bo.PaymentBO;
import com.chill.service.PayService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;
 
/**
 *  支付宝支付,控制器
 *
 * @author chill
 * */
@RestController
@RequestMapping(value = "/pay")
@AllArgsConstructor
public class PayController {
 
    private PayService payService;
 
    /**
     *  下单支付
     * */
    @GetMapping(value = "/confirm" , produces = {"text/html;charset=UTF-8"})
    public Object pay (@RequestParam(required = false) PaymentBO bo) throws Exception {
        bo = new PaymentBO();
        return payService.pay(bo);
    }
 
    /**
     * 支付成功的回调 这个回调接口一般使用在支付完成后需要执行的业务比	
     *  如 删除该订单 商品数减去相应的数量等等		
     * */
    @PostMapping(value = "/fallback")
    public Object fallback (HttpServletRequest request) {
        Map map = request.getParameterMap();
        System.out.println("进入了回调");
        return null;
    }
}

然后在配置文件,将notifyUrl的配置加上:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_html_29

然后再重新支付一下,我打个断点看一下Map中接到的参数:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_spring_30

可以看到,我们已经接收到了支付宝传来的参数,接下来就是根据自己的业务进行处理了,笔者这里就不再往下写了。

如果想再次进行确认,也可调用下面这个接口,查询订单状态:

android studio设置支付宝沙箱环境 支付宝沙箱支付流程_spring_31

完结啦~~~ 总体来说还是比较简单的 只要跟着步骤来