一步步教你如何在SpringBoot项目中引入支付功能
作者:鱼仔
博客首页: https://codeease.top
公众号:Java鱼仔
# (一)引言
支付功能如今已经成为一个需要盈利的网站的基本功能了,如今的网站如果想要做支付功能,往往都是将支付宝或者微信的支付功能集成进来。尽管支付宝已经给出了许多文档和代码,但是这项工作并没有那么简单。今天我就一步步带大家去实现在SpringBoot项目中对支付宝的功能引入。
# (二)功能介绍
我们要实现的功能很简单,当传入用户购买的信息之后,生成一个二维码供支付使用,同时提供一个查询接口查询该笔订单是否已支付,这种支付方式叫做当面付。学会这一种支付方式之后,支付宝的其他的功能也会很容易上手。
首先给出当面付的文档地址:https://opendocs.alipay.com/open/194
文档中很详细地描述了支付的整体逻辑。
# (三)开发前准备
首先需要引入相关的依赖,我把这个项目中会用到的依赖一次性给出:
<!--支付宝依赖-->
<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.10.218.ALL</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.8</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.2.1</version>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
同时官方已经提供了Demo,我们也直接下载下来: https://opendocs.alipay.com/open/54/104506
另外支付功能还涉及到私钥公钥的加签,支付宝给我们提供了密钥加密工具,也需要下载: https://opendocs.alipay.com/open/291/105971
最后还要一个沙箱环境的地址,这个地址用于测试: https://opendocs.alipay.com/open/200/105311
# (四)项目搭建
首先搭建一个SpringBoot项目,这一步就跳过了,打开我们上面下载的Demo文件,里面有一个TradePayDemo和TradePaySDK,TradePaySDK是支付过程中需要调用的一些类,因此需要把TradePaySDK中的代码引进来:
将上面这四个文件引入到我们的项目中:
TradePayDemo中提供了具体代码如何调用的示例,src目录下有一个文件叫做:zfbinfo.properties,把这个文件放入到resource目录下。关于zfbinfo.properties文件,里面有五个参数是需要我们自己去填写的: 注意,由于是测试环境,因此将open_api_domain修改成: https://openapi.alipaydev.com/gateway.do
pid是每个人自己账号的Id,登陆沙箱环境后,点击右上角的账号,选中账户中心,里面的账号ID就是pid。
在沙箱环境中,你还能看到自己的appid,将这个appid赋值到配置文件中的appid处:
接下来就是公钥和私钥以及支付宝的公钥,上面让大家下载了工具,打开后直接用默认的加密方式生成公私钥:
将上面的私钥和公钥分别放入对应的private_key和public_key中。
在沙箱环境中,将上面生成的公钥复制上去,可以得到支付宝的公钥,配置文件就齐全了
# (五)业务开发
完成上面一长串工作后,就可以开始写业务了,Demo文件中还要一个包叫做TradePayDemo的,我们主要参考里面的Main方法。
# 5.1 二维码生成功能
新建一个Service叫做TradeService,再新建他的实体类TradeServiceImpl
@Slf4j
@Service
public class TradeServiceImpl implements TradeService {
// 支付宝当面付2.0服务
private static AlipayTradeService tradeService;
@PostConstruct
private void init(){
/** 一定要在创建AlipayTradeService之前调用Configs.init()设置默认参数
* Configs会读取classpath下的zfbinfo.properties文件配置信息,如果找不到该文件则确认该文件是否在classpath目录
*/
Configs.init("zfbinfo.properties");
/** 使用Configs提供的默认参数
* AlipayTradeService可以使用单例或者为静态成员对象,不需要反复new
*/
tradeService = new AlipayTradeServiceImpl.ClientBuilder().build();
}
@Override
public String tradeQrCode(OrderDetail orderDetail){
//支付二维码的访问路径
String qrCodePath=null;
// (必填) 商户网站订单系统中唯一订单号,64个字符以内,只能包含字母、数字、下划线,
// 需保证商户系统端不能重复,建议通过数据库sequence生成,
String outTradeNo = "tradeprecreate" + System.currentTimeMillis()
+ (long) (Math.random() * 10000000L);
// (必填) 订单标题,粗略描述用户的支付目的。如“xxx品牌xxx门店当面付扫码消费”
String subject = orderDetail.getSubject();
// (必填) 订单总金额,单位为元,不能超过1亿元
// 如果同时传入了【打折金额】,【不可打折金额】,【订单总金额】三者,则必须满足如下条件:【订单总金额】=【打折金额】+【不可打折金额】
String totalAmount = orderDetail.getTotalAmount();
// (可选) 订单不可打折金额,可以配合商家平台配置折扣活动,如果酒水不参与打折,则将对应金额填写至此字段
// 如果该值未传入,但传入了【订单总金额】,【打折金额】,则该值默认为【订单总金额】-【打折金额】
String undiscountableAmount = "0";
// 卖家支付宝账号ID,用于支持一个签约账号下支持打款到不同的收款账号,(打款到sellerId对应的支付宝账号)
// 如果该字段为空,则默认为与支付宝签约的商户的PID,也就是appid对应的PID
String sellerId = "";
// 订单描述,可以对交易或商品进行一个详细地描述,比如填写"购买商品2件共15.00元"
String body = String.format("购买商品%s件共%s元",orderDetail.getGoodsDetail().size(),totalAmount);
// 商户操作员编号,添加此参数可以为商户操作员做销售统计
String operatorId = "javayz";
// (必填) 商户门店编号,通过门店号和商家后台可以配置精准到门店的折扣信息,详询支付宝技术支持
String storeId = "javayz001";
// 业务扩展参数,目前可添加由支付宝分配的系统商编号(通过setSysServiceProviderId方法),详情请咨询支付宝技术支持
ExtendParams extendParams = new ExtendParams();
extendParams.setSysServiceProviderId("2088100200300400500");
// 支付超时,定义为120分钟
String timeoutExpress = "120m";
// 商品明细列表,需填写购买商品详细信息,
List<GoodsDetail> goodsDetailList = new ArrayList<GoodsDetail>();
orderDetail.getGoodsDetail().stream().forEach((item)->{
// 创建一个商品信息,参数含义分别为商品id(使用国标)、名称、单价(单位为分)、数量,如果需要添加商品类别,详见GoodsDetail
GoodsDetail goods1 = GoodsDetail.newInstance(item.getGoodsId(), item.getGoodsName(), Long.valueOf(item.getPrice())*100, Math.toIntExact(item.getQuantity()));
// 创建好一个商品后添加至商品明细列表
goodsDetailList.add(goods1);
});
// 创建扫码支付请求builder,设置请求参数
AlipayTradePrecreateRequestBuilder builder = new AlipayTradePrecreateRequestBuilder()
.setSubject(subject).setTotalAmount(totalAmount).setOutTradeNo(outTradeNo)
.setUndiscountableAmount(undiscountableAmount).setSellerId(sellerId).setBody(body)
.setOperatorId(operatorId).setStoreId(storeId).setExtendParams(extendParams)
.setTimeoutExpress(timeoutExpress)
// .setNotifyUrl("http://www.test-notify-url.com")//支付宝服务器主动通知商户服务器里指定的页面http路径,根据需要设置
.setGoodsDetailList(goodsDetailList);
AlipayF2FPrecreateResult result = tradeService.tradePrecreate(builder);
switch (result.getTradeStatus()) {
case SUCCESS:
log.info("支付宝预下单成功: )");
AlipayTradePrecreateResponse response = result.getResponse();
dumpResponse(response);
// 需要修改为运行机器上的路径
String filePath = String.format("F:/qrcode/static/qrcode/qr-%s.png",
response.getOutTradeNo());
log.info("filePath:" + filePath);
//创建二维码
ZxingUtils.getQRCodeImge(response.getQrCode(), 256, filePath);
qrCodePath=filePath;
break;
case FAILED:
log.error("支付宝预下单失败!!!");
break;
case UNKNOWN:
log.error("系统异常,预下单状态未知!!!");
break;
default:
log.error("不支持的交易状态,交易返回异常!!!");
break;
}
return qrCodePath;
}
// 简单打印应答
private void dumpResponse(AlipayResponse response) {
if (response != null) {
log.info(String.format("code:%s, msg:%s", response.getCode(), response.getMsg()));
if (StringUtils.isNotEmpty(response.getSubCode())) {
log.info(String.format("subCode:%s, subMsg:%s", response.getSubCode(),
response.getSubMsg()));
}
log.info("body:" + response.getBody());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
代码注释已经解释的很清楚了,就是设置参数,生成二维码。
新建一个类OrderController,创建一个Post请求的接口:
@RestController
@RequestMapping("/order")
public class OrderController extends BaseController {
@Autowired
private TradeService tradeService;
//创建支付二维码
@PostMapping("/qrcode")
public CommonResult getQrCode(@RequestBody OrderDetail orderDetail){
String path = tradeService.tradeQrCode(orderDetail);
if (StringUtils.isNotEmpty(path)){
return CommonResult.success(path);
}
return CommonResult.fail();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
访问http://localhost:8190/order/qrcode,参数可参考如下:
输出二维码的地址:
验证支付功能务必要使用沙盒地址中的钱包:
# 5.2 验证订单是否被支付
一样的逻辑,从Demo中找到验证订单的代码,放入TradeServiceImpl中
@Override
public String alipayTradeQuery(String orderSn){
//返回信息
String responseResult="";
// (必填) 商户订单号,通过此商户订单号查询当面付的交易状态
String outTradeNo = orderSn;
// 创建查询请求builder,设置请求参数
AlipayTradeQueryRequestBuilder builder = new AlipayTradeQueryRequestBuilder()
.setOutTradeNo(outTradeNo);
AlipayF2FQueryResult result = tradeService.queryTradeResult(builder);
switch (result.getTradeStatus()) {
case SUCCESS:
responseResult="查询返回该订单支付成功";
log.info("查询返回该订单支付成功: )");
AlipayTradeQueryResponse response = result.getResponse();
dumpResponse(response);
log.info(response.getTradeStatus());
if (Utils.isListNotEmpty(response.getFundBillList())) {
for (TradeFundBill bill : response.getFundBillList()) {
log.info(bill.getFundChannel() + ":" + bill.getAmount());
}
}
break;
case FAILED:
responseResult="查询返回该订单支付失败或被关闭";
log.error("查询返回该订单支付失败或被关闭!!!");
break;
case UNKNOWN:
responseResult="系统异常,订单支付状态未知";
log.error("系统异常,订单支付状态未知!!!");
break;
default:
responseResult="不支持的交易状态,交易返回异常";
log.error("不支持的交易状态,交易返回异常!!!");
break;
}
return responseResult;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
在Controller中加一个接口
//查询订单情况
@PostMapping("/queryOrderStatus")
public CommonResult queryOrderStatus(@RequestParam("orderSn") String orderSn){
String result = tradeService.alipayTradeQuery(orderSn);
if (StringUtils.isEmpty(result)){
return CommonResult.fail();
}else {
return CommonResult.success(result);
}
}
2
3
4
5
6
7
8
9
10
# 5.3 设置一个回调的接口
可以通过5.2中的方法定时轮询订单是否被支付,也可以写一个回调接口给支付宝调用,但是这个接口必须确保能被支付宝外网访问到,这里我给出代码示例:
@PostMapping("payCallback")
public void payCallback(){
Map<String,String> map=new HashMap<>();
Enumeration<String> parameterNames = getRequest().getParameterNames();
while (parameterNames.hasMoreElements()){
String parameter = parameterNames.nextElement();
if (!parameter.toLowerCase().equals("sign_type")){
map.put(parameter,getRequest().getParameter(parameter));
}
}
try {
boolean result = AlipaySignature.rsaCertCheckV2(map, Configs.getPublicKey(), "utf-8", Configs.getSignType());
PrintWriter writer = getResponse().getWriter();
if (result){
writer.print("success");
}else {
writer.print("unSuccess");
}
} catch (AlipayApiException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
然后在生成二维码的代码中增加回调接口
# (六)总结
到这里,当面付的功能我们就开发好了,说简单也不简单,各种步骤都比较繁琐,但是只要把整体逻辑理清楚了,后续其他支付功能的开发就会很简单了。我是鱼仔,我们下期再见!