赞
踩
目录
https://github.com/QiuFengYanMeng/alipay-demo
最近有朋友咨询第三方支付相关的开发,心血来潮想疾书一番。
正好支付宝有提供沙箱环境,我们个人开发者也可以进行开发、测试,不需要你有企业资质,降低了学习门槛。支付宝的Java SDK换成了较新的版本,我个人觉得新版本的SDK和旧版本相比很简洁,值得更换。
但是现在网上大量的教程,普遍都是老版本SDK,这是因为新版本才刚出来没多久。所以笔者也想趁这个时机,写下此文,希望后来者集成支付宝的时候能少走弯路。
1、支付宝支付官网:支付宝 知托付!
2、支付宝开放平台-文档中心:创建应用 | 网页&移动应用
3、natapp内网穿透工具官网:NATAPP-内网穿透 基于ngrok的国内高速内网映射工具
图片来自支付宝开放平台。
上面的流程图,已经大致描述了发起支付的过程,这里笔者用自己的语言,详细地转化一下:
支付宝有一个供开发者测试使用的沙箱环境,会提供一个沙箱版的支付宝app、一个商家账户、一个买家账户。有了这个,可以让我们跳过商家入驻、企业资质审核等过程,开箱即用,降低了学习成本。
现在,让我们简单的配置沙箱环境。
1、进入到支付宝支付官网,点击“我是开发者”,在新的页面右上角,用你自己的支付宝扫码登录,再点击开发服务中的研发服务:
此时就到了沙箱应用的控制台,会为你分配一个APPID:
2、出于安全性的考虑,我们需要生成一份公钥和私钥,公钥提供给支付宝,支付宝对数据进行加密;私钥用于解析支付宝传来的加密数据,由我们自行保管。支付宝提供了公钥和私钥的生成工具,在文档中心点击“开发工具”:
然后在这个地方根据你的系统下载工具:
我们打开下载好的工具,在以下地方点击“生成密钥”:
把私钥和公钥保存好。
3、回到沙箱应用控制台,点击这个地方设置公钥(注意不是公钥证书哦,我们用的不是证书的方式):
在弹出的窗口,把刚才生成的公钥复制粘贴进去,点击确定:
其中支付宝公钥就是我们上传的公钥,应用公钥是支付宝为我们自动生成的。
在沙箱应用控制台,扫码下载沙箱版的支付宝(目前只有安卓版),提供了一个商家号、一个买家号,其中买家号里面有10万块钱(可惜不能提现呵呵呵),后面支付的时候会用。
创建Spring Boot项目的过程我就省略了,我前面的文章都有,有问题也可以联系我,联系方式在文末。
这里只贴一下我的pom.xml依赖:
- <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>
- <exclusions>
- <exclusion>
- <groupId>org.junit.vintage</groupId>
- <artifactId>junit-vintage-engine</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-easysdk -->
- <dependency>
- <groupId>com.alipay.sdk</groupId>
- <artifactId>alipay-easysdk</artifactId>
- <version>2.0.2</version>
- </dependency>
-
- </dependencies>

只有基本的web、lombok、支付宝的sdk依赖,下面介绍一下我的项目结构:
在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获取临时域名,很简单,我在之前已经讲过了,所以不再赘述,有不懂到可以去看我到这篇文章:
映射完成之后,启动项目,用临时域名测试一下上面的页面:
在application.yml配置文件,将公钥、私钥、APPID、支付宝的网关、页面回调、接口回调等信息配置进来:
- alipay:
- appId: 换成你自己的APPID
- privateKey: 换成你自己的私钥
- publicKey: 换成你自己的公钥
- #支付网关配置,这一项是写死的,正式环境是openapi.alipay.com
- gateway: openapi.alipaydev.com
- #支付成功之后的回调页面,只是一个友好页面。主要换成你自己映射的临时域名
- returnUrl: http://frnqxw.natappfree.cc/return.html
- #支付成功的接口回调,我们还没写,先空着
- notifyUrl:
接下来我们就要读取支付宝的文档了:
新版本的SDK 把大部分操作都封装到了Factory类中,但是Factory类在使用之前需要我们配置一下参数,且全局配置一次即可,所以我们把这部分代码拿到ProjectInit类中,在项目启动时执行一次:
- package com.course.java.alipay.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初始化完成**********");
- }
-
- 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;
- }
- }

再次启动项目:
既然要下单,肯定少不了传参,所以我们这里简单的声明一个PaymentBO类:
- package com.course.java.alipay.bo;
-
- import lombok.Data;
-
- import java.math.BigDecimal;
-
- /**
- * 发起支付时的参数
- * */
- @Data
- public class PaymentBO {
- //省略其他的业务参数,如商品id、购买数量等
-
- //商品名称
- private String subject;
-
- //总金额
- private BigDecimal total = BigDecimal.ZERO;
- }

然后在OrderUtil编写如下代码:
- package com.course.java.alipay.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);
- }
- }

接下来,继续看文档:
按照文档描述,我们只需要Factory.Payment.Page.pay就可以发起一个支付请求,比之前还是简洁了不少,其他操作都封装好了。所以我们在PayService编写以下代码:
- package com.course.java.alipay.service;
-
- import com.alipay.easysdk.factory.Factory;
- import com.alipay.easysdk.payment.facetoface.models.AlipayTradePayResponse;
- import com.alipay.easysdk.payment.page.models.AlipayTradePagePayResponse;
- import com.course.java.alipay.bo.PaymentBO;
- import com.course.java.alipay.util.OrderUtil;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.stereotype.Service;
-
- import java.math.BigDecimal;
-
- /**
- * 支付宝支付,业务实现
- *
- * @author wangziyang
- * @date 2020-09-08
- * */
- @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.course.java.alipay.controller;
-
- import com.course.java.alipay.bo.PaymentBO;
- import com.course.java.alipay.service.PayService;
- import lombok.AllArgsConstructor;
- import org.springframework.web.bind.annotation.*;
-
- import java.util.Map;
-
- /**
- * 支付宝支付,控制器
- *
- * @author wangziyang
- * */
- @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 {
- //这个接口其实应该是post方式的,但是我这里图方便,直接以get方式访问,
- //且返回格式是text/html,这样前端页面就能直接显示支付宝返回的html片段
- //真实场景下由post方式请求,返回code、msg、data那种格式的标准结构,让前端拿到data里的
- //html片段之后自行加载
-
- //由于我这里并没有真正的传参数,所以象征性的new一下,避免空指针
- 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>

运行项目,点击确认支付就看到了以下页面(有时候下面的页面会提示有钓鱼风险,换一个浏览器就好,或者关了浏览器重来,这个无解):
在手机上打开沙箱版支付宝,登录买家帐号,扫码支付:
上面是最简单的支付。大家可以在API文档看到,下单支付还有很多参数可选,如订单描述、商品类型、订单商品列表的具体信息等等……
公共参数就不用说了,Factory都为我们封装好了;但是下面的业务请求参数,也就是公共参数里的biz_content,如何传呢?pay()方法只提供了基本的4个参数,那可选的请求参数怎么传呢?
对于这个问题,新版的SDK指出需要由我们自己扩展,方式在SDK说明中有:
这个还是挺简单的,就是往Map里放参数,最后通过optional或batchOptional方法进行设置:
所以在这里我就不展示了,大家下去自己扩展就行,有问题联系我。
前面的回调只是在页面上回显信息,我们需要提供一个接口,供支付宝调用,接收支付宝传来的参数。
首先,我们看文档,看支付宝都返回了哪些信息:
参数有很多,我就不一一列出了。主要的参数是商户订单号(即我们自己生成的订单号)和交易状态, 我们根据订单号是否交易成功,进行后面的业务逻辑,比如给用户加积分、将订单状态变为已支付、发送业务短信、通知商家接单……等等。
参数很多,所以我就不新建实体类了,简单的用Map接一下,在PayController新增以下接口(注意是post方式):
- package com.course.java.alipay.controller;
-
- import com.course.java.alipay.bo.PaymentBO;
- import com.course.java.alipay.service.PayService;
- import lombok.AllArgsConstructor;
- import org.springframework.web.bind.annotation.*;
-
- import javax.servlet.http.HttpServletRequest;
- import java.util.Map;
-
- /**
- * 支付宝支付,控制器
- *
- * @author wangziyang
- * */
- @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的配置加上:
然后再重新支付一下,我打个断点看一下Map中接到的参数:
可以看到,我们已经接收到了支付宝传来的参数,接下来就是根据自己的业务进行处理了,笔者这里就不再往下写了。
如果想再次进行确认,也可调用下面这个接口,查询订单状态:
以上就是Spring Boot整合支付宝支付的流程,代码已经上传到GitHub,有问题欢迎联系
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。