SpringBoot集成支付宝支付的前后端相关逻辑
从支付宝的开发者配置到项目的支付接口实际落地,记录一下Spring Boot集成支付宝支付的全过程。 待完善。。。
前言
本文提供支付宝沙盒环境
开发语言:Spring Boot + Kotlin + Vue (Java开发者肯定能看懂)
业务逻辑代码会部分省略,更多是文字引导思考,请结合自己的业务场景进行实现。
开发筹备 需求参数
只负责对接业务 或 使用本文沙盒环境 的同学,请移步 依赖配置
应用ID
应用AES密钥
应用公钥
应用私钥
支付宝公钥
支付宝根证书
支付申请流程
以下流程按照需求参数的顺序。
登录支付宝开发者平台 ,创建应用,进入应用详情页,左上角有 应用ID
。
点击左边菜单栏的 开发者设置
,点击 接口内容加密方式
会得到一个 AES密钥
。
点击左边菜单栏的 开发者设置
,点击 接口加签方式
选择证书模式然后走官方教程,按照教程会在本地生成得到 应用公钥
和 应用私钥
。
第三步之后,再点击 接口加签方式
,能下载得到 支付宝根证书
和 支付宝公钥
。
至此得到全部参数,请保存好。最后还是在开发者设置
中,将计划的 授权回调地址
填入(也就是后端项目接收支付宝平台回调支付结果的地址)。
还有一件事是去支付宝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 privateKey: /privateKey.txt appCert: /appCert.crt alipayPublicCert: /alipayPublicCert.crt rootCert: /rootCert.crt encryptKey: /encryptKey.txt serverUrl: /serverUrl.txt 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.configimport cn.hutool.core.io.FileUtilimport com.alipay.api.AlipayClientimport com.alipay.api.AlipayConfigimport com.alipay.api.AlipayConstantsimport com.alipay.api.DefaultAlipayClientimport org.springframework.beans.factory.annotation .Valueimport org.springframework.context.annotation .Beanimport org.springframework.context.annotation .Configurationimport org.springframework.core.io.ResourceLoaderimport java.nio.file.Filesimport 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
。
商品服务
随便写几个服务,配合讲解支付宝支付流程。
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.JpaRepositoryimport 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.JpaRepositoryimport 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.JpaRepositoryimport 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 } private fun questALiPay (order: Order ) : String { val shop = shopRepository.findById(order.shopId) 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" ) set ("qr_pay_mode" , 1 ) } val request = AlipayTradePagePayRequest().apply { this .bizContent = JSONUtil.toJsonStr(bizContent) notifyUrl = aliPayConfig.notifyUrl returnUrl = aliPayConfig.returnUrl terminalType = "WEB" prodCode = "FAST_INSTANT_TRADE_PAY" 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)) } }
引入项目 待写。。。