从支付宝的开发者配置到项目的支付接口实际落地,记录一下Spring Boot集成支付宝支付的全过程。
待完善。。。
前言
- 本文提供支付宝沙盒环境
- 开发语言:Spring Boot + Kotlin + Vue (Java开发者肯定能看懂)
- 业务逻辑代码会部分省略,更多是文字引导思考,请结合自己的业务场景进行实现。
开发筹备
需求参数
只负责对接业务 或 使用本文沙盒环境 的同学,请移步 依赖配置
- 应用ID
- 应用AES密钥
- 应用公钥
- 应用私钥
- 支付宝公钥
- 支付宝根证书
支付申请流程
以下流程按照需求参数的顺序。
- 登录支付宝开发者平台,创建应用,进入应用详情页,左上角有
应用ID。 - 点击左边菜单栏的
开发者设置,点击接口内容加密方式会得到一个AES密钥。 - 点击左边菜单栏的
开发者设置,点击接口加签方式选择证书模式然后走官方教程,按照教程会在本地生成得到应用公钥和应用私钥。 - 第三步之后,再点击
接口加签方式,能下载得到支付宝根证书和支付宝公钥。 - 至此得到全部参数,请保存好。最后还是在
开发者设置中,将计划的授权回调地址填入(也就是后端项目接收支付宝平台回调支付结果的地址)。
- 还有一件事是去支付宝B端申请开通对应的支付功能产品,比如电脑网站支付、手机网站支付、APP支付、JSAPI支付等。
没申请的话可以先申请再看下面的教程,节省等待审核的时间。
依赖配置
引入依赖
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.39.134.ALL</version>
</dependency>
implementation("com.alipay.sdk:alipay-sdk-java:4.39.134.ALL")
implementation group: 'com.alipay.sdk', name: 'alipay-sdk-java', version: '4.39.134.ALL'
{% endtabs %}
配置资源
我这里给出我的配置解决方案,具体请结合自己业务场景储存与使用这些资源,请各显神通。
{% image https://upyun.thatcdn.cn/myself/typora/b9aaba6bbab7e303e5fc4c5f14b0f822.png 项目资源与配置 ratio:1512/422 %}
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文件夹,所以用这个方法构造一个虚拟的绝对路径。
通过下面代码我们得到了一个支付宝支付客户端,后文会用来发起支付请求。
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 或者 轮询后端支付结果)
- 用户:哥们你管我怎么操作,我点了支付扫码再取消支付你都管不着。诶,就是点着玩儿。
后端开发
- 后端:被前端唤起支付,向支付宝发起支付,给前端返回支付码,等待支付宝告知支付结果。
- 被前端唤起:前端携带订单号发起请求,后端
ALiPayController接收请求,调用支付宝Service层生成支付码,返回给前端。所以需要ALiPayService。 - 向支付宝发起支付:
ALiPayService调用支付宝客户端alipayClient根据订单信息生成支付请求,得到支付码。所以需要OrderService和ShopService。 - 等待支付宝告知支付结果:支付宝支付成功或失败后,支付宝会回调
notifyUrl,通知后端支付详情,记录到数据库。所以ALiPayController最起码需要唤起支付和支付回调两个方法,还需要PayDetailService。
商品服务
随便写几个服务,配合讲解支付宝支付流程。
@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
}
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface ShopRepository : JpaRepository<Shop, Long> {}
订单服务
@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
}
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface OrderRepository : JpaRepository<Order, Long> {}
流水服务
@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
}
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface PayDetailRepository : JpaRepository<PayDetail, Long> {}
支付宝服务
前面分析到
ALiPayController最起码需要唤起支付和支付回调两个方法,所以服务层最少也要有这两个方法。
官网参数比较多,这里只列出核心参数。方便读者快速测试。
/**
* 支付宝支付服务接口
*/
interface IALiPayService {
/**
* 发起支付,返回支付码
*/
fun nativePay(orderId: Long): String
/**
* 支付结果回调
*/
fun nativeNotify(request: HttpServletRequest): Boolean
}
/**
* 支付宝支付服务实现
*/
@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层实现生成支付码,返回给前端。
可以先写
@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))
}
}
引入项目
待写。。。
浙公网安备 33010602011771号