从支付宝的开发者配置到项目的支付接口实际落地,记录一下Spring Boot集成支付宝支付的全过程。
待完善。。。

前言

  1. 本文提供支付宝沙盒环境
  2. 开发语言:Spring Boot + Kotlin + Vue (Java开发者肯定能看懂)
  3. 业务逻辑代码会部分省略,更多是文字引导思考,请结合自己的业务场景进行实现。

开发筹备

需求参数

只负责对接业务 或 使用本文沙盒环境 的同学,请移步 依赖配置

  1. 应用ID
  2. 应用AES密钥
  3. 应用公钥
  4. 应用私钥
  5. 支付宝公钥
  6. 支付宝根证书

支付申请流程

以下流程按照需求参数的顺序。

  1. 登录支付宝开发者平台,创建应用,进入应用详情页,左上角有 应用ID
  2. 点击左边菜单栏的 开发者设置,点击 接口内容加密方式 会得到一个 AES密钥
  3. 点击左边菜单栏的 开发者设置,点击 接口加签方式 选择证书模式然后走官方教程,按照教程会在本地生成得到 应用公钥应用私钥
  4. 第三步之后,再点击 接口加签方式,能下载得到 支付宝根证书支付宝公钥
  5. 至此得到全部参数,请保存好。最后还是在开发者设置中,将计划的 授权回调地址 填入(也就是后端项目接收支付宝平台回调支付结果的地址)。
  • 还有一件事是去支付宝B端申请开通对应的支付功能产品,比如电脑网站支付、手机网站支付、APP支付、JSAPI支付等。
    没申请的话可以先申请再看下面的教程,节省等待审核的时间。

依赖配置

引入依赖

依赖参考文档

1
2
3
4
5
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.39.134.ALL</version>
</dependency>
1
implementation("com.alipay.sdk:alipay-sdk-java:4.39.134.ALL")
1
implementation group: 'com.alipay.sdk', name: 'alipay-sdk-java', version: '4.39.134.ALL'

配置资源

我这里给出我的配置解决方案,具体请结合自己业务场景储存与使用这些资源,请各显神通。

项目资源与配置
项目资源与配置
application.yml
1
2
3
4
5
6
7
8
9
10
11
pay:
alipay:
appId: /appId.txt # 应用id
privateKey: /privateKey.txt # 应用私钥
appCert: /appCert.crt # 应用公钥
alipayPublicCert: /alipayPublicCert.crt # 支付宝公钥路径
rootCert: /rootCert.crt # 支付宝根证书路径
encryptKey: /encryptKey.txt # 应用AES密钥
serverUrl: /serverUrl.txt # 支付宝网关地址 线上环境:https://openapi.alipay.com/gateway.do
notifyUrl: xxxx # 授权回调地址
returnUrl: xxxx # 支付成功后前端地址

客户端类

都是老司机看使用的类包名就知道干什么的,到了开发这步就不献丑解释代码。请自行设计一个支付宝配置类,然后注入到 Spring Bean 中。
至于我示例代码的 getAbsolutePath 方法,是Linux部署没有resources文件夹,所以用这个方法构造一个虚拟的绝对路径。

通过下面代码我们得到了一个支付宝支付客户端,后文会用来发起支付请求。

ALiPayConfig.kt
1
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
package cn.thatcoder.auto.system.pay.config

import cn.hutool.core.io.FileUtil
import com.alipay.api.AlipayClient
import com.alipay.api.AlipayConfig
import com.alipay.api.AlipayConstants
import com.alipay.api.DefaultAlipayClient
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.ResourceLoader
import java.nio.file.Files
import java.nio.file.StandardCopyOption

@Configuration("支付宝支付配置")
class ALiPayConfig(private val resourceLoader: ResourceLoader) {

@Value("\${pay.alipay.appId}")
lateinit var appId: String

@Value("\${pay.alipay.privateKey}")
lateinit var privateKey: String

@Value("\${pay.alipay.appCert}")
lateinit var appCert: String

@Value("\${pay.alipay.alipayPublicCert}")
lateinit var alipayPublicCert: String

@Value("\${pay.alipay.rootCert}")
lateinit var rootCert: String

@Value("\${pay.alipay.encryptKey}")
lateinit var encryptKey: String

@Value("\${pay.alipay.returnUrl}")
lateinit var returnUrl: String

@Value("\${pay.alipay.notifyUrl}")
lateinit var notifyUrl: String

@Value("\${pay.alipay.serverUrl}")
lateinit var serverUrl: String

var out_no_prefix = "TC_A_"

/**
* 支付宝支付客户端
*/
@Bean
fun alipayClient(): AlipayClient {
val config = AlipayConfig().apply {
this.serverUrl = FileUtil.readUtf8String(getAbsolutePath(this@ALiPayConfig.serverUrl))
this.appId = FileUtil.readUtf8String(getAbsolutePath(this@ALiPayConfig.appId))
this.privateKey = FileUtil.readUtf8String(getAbsolutePath(this@ALiPayConfig.privateKey))
this.appCertPath = getAbsolutePath(this@ALiPayConfig.appCert)
this.format = AlipayConstants.FORMAT_JSON
this.charset = AlipayConstants.CHARSET_UTF8
this.alipayPublicCertPath = getAbsolutePath(this@ALiPayConfig.alipayPublicCert)
rootCertPath = getAbsolutePath(this@ALiPayConfig.rootCert)
signType = AlipayConstants.SIGN_TYPE_RSA2
encryptKey = FileUtil.readUtf8String(getAbsolutePath(this@ALiPayConfig.encryptKey))
encryptType = AlipayConstants.ENCRYPT_TYPE_AES
}
val alipayClient = DefaultAlipayClient(config)
alipayClient.let {
it.setMaxIdleConnections(10) // 连接池最大可缓存空闲数
it.setKeepAliveDuration(10000L) //连接池空闲连接的存活时间
it.setConnectTimeout(3000) //连接超时时间
it.setReadTimeout(15000) //读取超时时间
}
return alipayClient
}

/**
* 构建资源文件缓存堆路径
*/
private fun getAbsolutePath(localPath: String): String {
val resource =
resourceLoader.getResource("classpath:pay/alipay${localPath}")
val fileName = localPath.substring(1)
val tempFile = Files.createTempFile(fileName.split(".").first(), "." + fileName.split(".").last())
Files.copy(resource.inputStream, tempFile, StandardCopyOption.REPLACE_EXISTING)
return tempFile.toAbsolutePath().toString()
}
}

业务逻辑

先上支付宝时序图,后面就知道自己该干嘛。

  • 后端:被前端唤起支付,向支付宝发起支付,给前端返回支付码,等待支付宝告知支付结果。
  • 前端:被用户申请支付,请求后端拿支付码,渲染支付页面,等待(支付宝成功会跳转到returnUrl 或者 轮询后端支付结果)
  • 用户:哥们你管我怎么操作,我点了支付扫码再取消支付你都管不着。诶,就是点着玩儿。
支付宝时序图
支付宝时序图

后端开发

  • 后端:被前端唤起支付,向支付宝发起支付,给前端返回支付码,等待支付宝告知支付结果。
  1. 被前端唤起:前端携带订单号发起请求,后端ALiPayController 接收请求,调用支付宝Service层生成支付码,返回给前端。所以需要 ALiPayService
  2. 向支付宝发起支付:ALiPayService 调用支付宝客户端 alipayClient 根据订单信息生成支付请求,得到支付码。所以需要OrderServiceShopService
  3. 等待支付宝告知支付结果:支付宝支付成功或失败后,支付宝会回调 notifyUrl,通知后端支付详情,记录到数据库。所以 ALiPayController 最起码需要唤起支付和支付回调两个方法,还需要PayDetailService

商品服务

随便写几个服务,配合讲解支付宝支付流程。

Shop.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity
@Table(name = "shop")
@Component("商品")
@Data
class ShopThesis{
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0

@Column(name = "title", length = 255)
var title: String = ""

@Column(name = "desc")
var desc: String = ""

@Column(name = "price")
var price: Long = 0
}
ShopRepository.kt
1
2
3
4
5
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface ShopRepository : JpaRepository<Shop, Long> {}

订单服务

Order.kt
1
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
@Entity
@Table(name = "order")
@Component("订单")
@Data
class OrderThesis{
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0

@Column(name = "order_no")
var orderNo: String = ""

@Column(name = "user_id")
var userId: Long = 0

@Column(name = "pay_id")
var payId: Long = 0

@Column(name = "shop_id")
var shopId: Long = 0

@Column(name = "price")
var price: Long = 0

@Column(name = "status")
var status: Int = 0
}
OrderRepository.kt
1
2
3
4
5
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface OrderRepository : JpaRepository<Order, Long> {}

流水服务

PayDetail.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
@Table(name = "pay_detail")
@Component("支付流水")
@Data
class PayDetailThesis{
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0

@Column(name = "order_id")
var orderId: Long = 0

@Column(name = "pay_id")
var payId: Long = 0

@Column(name = "pay_time")
var payTime: LocalDateTime = LocalDateTime.now()

@Column(name = "status")
var status: Int = 0
}
PayDetailRepository.kt
1
2
3
4
5
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface PayDetailRepository : JpaRepository<PayDetail, Long> {}

支付宝服务

前面分析到 ALiPayController 最起码需要唤起支付和支付回调两个方法,所以服务层最少也要有这两个方法。
官网参数比较多,这里只列出核心参数。方便读者快速测试。

IALiPayService.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 支付宝支付服务接口
*/
interface IALiPayService {
/**
* 发起支付,返回支付码
*/
fun nativePay(orderId: Long): String

/**
* 支付结果回调
*/
fun nativeNotify(request: HttpServletRequest): Boolean
}
ALiPayService.kt
1
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
/**
* 支付宝支付服务实现
*/
@Service
class ALiPayService(
private val alipayClient: AlipayClient,
private val aliPayConfig: ALiPayConfig
private val orderRepository: OrderRepository,
private val shopRepository: ShopRepository,
private val payDetailRepository: PayDetailRepository,
): IALiPayService {

override fun nativePay(orderId: Long): String {
val findOrder = orderRepository.findById(orderId)
// 处理空订单
if (findOrder.isEmpty) {
return "order $orderId not found"
}
val order = findOrder.get()
return questALiPay(order)
}

override fun nativeNotify(request: HttpServletRequest): Boolean {
// 解析支付宝回调参数
val params = request.parameterMap
val notifyParams = HashMap<String, String>()
params.forEach { (k, v) ->
notifyParams[k] = v[0]
}
// 验证签名
val sign = notifyParams.remove("sign")
val signType = notifyParams.remove("sign_type")
val alipayPublicKey = aliPayConfig.alipayPublicCert
val alipayPublicKeyInputStream = FileUtil.getInputStream(alipayPublicKey)
val alipayPublicKeyObject = CertificateFactory.getInstance("X.509").generateCertificate(alipayPublicKeyInputStream)
val publicKey = alipayPublicKeyObject.publicKey
val verifyResult = AlipaySignature.rsaCheckV1(notifyParams, sign, publicKey, signType)
if (!verifyResult) {
return false
}
// 处理支付结果
val tradeNo = notifyParams["trade_no"]
val outTradeNo = notifyParams["out_trade_no"]
val tradeStatus = notifyParams["trade_status"]
val findOrder = orderRepository.findByOrderNo(outTradeNo.removePrefix(aliPayConfig.out_no_prefix))
if (findOrder.isEmpty) {
return false
}
val order = findOrder.get()
val payDetail = PayDetail(
orderId = order.id,
payId = tradeNo.toLong(),
payTime = LocalDateTime.now(),
status = if (tradeStatus == "TRADE_SUCCESS") 1 else 0
)
payDetailRepository.save(payDetail)
return true
}

/**
* 申请支付
* @param order 订单
* @return 支付链接
*/
private fun questALiPay(order: Order): String {
val shop = shopRepository.findById(order.shopId)

// 构建业务参数 JSON 对象
val bizContent = JSONObject().apply {
// 设置商户订单号
set("out_trade_no", aliPayConfig.out_no_prefix + order.orderId.toString())
// 设置支付场景
set("scene", "bar_code")
// 设置订单标题
set("subject", shop.title)
// 设置订单总金额
set("total_amount", order.price * 0.01)
set("product_code", "FAST_INSTANT_TRADE_PAY")
// 设置订单绝对超时时间
set("timeout_express", "120m")
// 设置前端支付页面模式 1 前端嵌入式 2 跳转支付宝官网
set("qr_pay_mode", 1)
}

// 构建请求对象
val request = AlipayTradePagePayRequest().apply {
// 设置业务参数
this.bizContent = JSONUtil.toJsonStr(bizContent)
// 设置通知回调 URL
notifyUrl = aliPayConfig.notifyUrl
// 设置返回支付后前端页面的跳转URL
returnUrl = aliPayConfig.returnUrl
// 设置终端类型
terminalType = "WEB"
// 设置产品代码
prodCode = "FAST_INSTANT_TRADE_PAY"
// 设置是否开启了AES加密
isNeedEncrypt = true
}

return try {
// 发送请求并获取响应
val response: AlipayTradePagePayResponse = alipayClient.pageExecute(request, "POST");
if (response.isSuccess) {
// 返回支付链接
response.body.toString()
} else {
"支付宝支付请求失败,原因: ${response.subMsg}"
}
} catch (e: AlipayApiException) {
"支付宝支付请求失败,原因: ${e.message}"
}
}
}

前端唤起

被前端唤起就是一个Controller接口,前端发起请求,后端调用支付宝Service层实现生成支付码,返回给前端。
可以先写

ALiPayController.kt
1
2
3
4
5
6
7
8
9
@RestController("aliPayController")
@RequestMapping("/pay")
class ALiPayController(private val alipayService: ALiPayService) {
@GetMapping("/ali.pay")
fun aliPay(@RequestParam("orderId") orderId: Long): ResponseEntity<ResponseBody> {
val codeUrl = alipayService.nativePay(orderId)
return Response.success("订单申请成功", mapOf("codeUrl" to codeUrl))
}
}

引入项目

待写。。。


本站由 钟意 使用 Stellar 1.28.1 主题创建。
又拍云 提供CDN加速/云存储服务
vercel 提供托管服务
湘ICP备2023019799号-1
总访问 次 | 本页访问