介绍
苹果的支付流程并不复杂,我们可以在很短的时间内实现正常的支付流程。
但是苹果支付有一个很大特点就是慢,包括唤起支付弹窗,点击完成到服务端验单完成,整个流程很长,任何一个环节都有可能因为断网,或者退出APP导致支付流程中断。
一旦用户输入的支付密码,但是支付流程没有进行完成,这个时候就产生了漏单的情况,也就是说扣了用户的费用,但是购买的东西没有发放到账户。
本篇博客就来介绍苹果支付的漏单处理方案。
支付流程
上面提到任何环节出现中断,都会产生漏单,那么我们就需要清楚整个支付流程分成那几个阶段。
1.发起购买:
通常完整的支付流程,都会拿到用户选择的商品到服务端生成一个预备订单。生成订单完成之后会返回一个订单id preorderId。(这些和自己产品的业务需求相关,每个服务端实现的方案可能不都一样),拿到订单id preorderId,发起苹果支付,商品被添加到购买列表。
2.输入密码:
商品被添加到列表后,会弹出支付弹窗要求用户输入支付密码。
3.支付完成:
用户输入完支付密码之后,提示支付成功,弹窗消失,根据网络情况会在几秒内弹出购买成功的弹窗。
4.服务端验单:
点击购买完成的OK按钮,进入购买完成状态,发起验单请求。
5.购买完成:
验单通过,完成购买流程。
漏单情况
在用户没有输入密码之前取消支付操作,或者其它原因中断支付操作,因为没有进行付款,所以我们可以直接销单,这都没有问题。
但是一旦用户已经输入了密码,也就是从第3步以及以后的步骤,发生了网络中断,或APP退出,那么都算是发生了漏单。
支付过程中我们可以通过paymentQueue方法来监听支付流程和支付结果
//MARK: 购买结果 监听回调
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for tran in transactions {
switch tran.transactionState {
case .purchased://购买完成
CSLog.info(module: "apple pay", "购买完成")
completePay(transaction: tran)
break
case.purchasing://商品添加进列表
CSLog.info(module: "apple pay", "商品添加到列表")
break
case.restored://已经购买过该商品
SKPaymentQueue.default().finishTransaction(tran)
CSLog.info(module: "apple pay", "pay success 1,productId=\(self.proId ?? "")")
self.resultBlock?(true,nil,"已经购买过该商品")
break
case.failed://购买失败
SKPaymentQueue.default().finishTransaction(tran)
CSLog.info(module: "apple pay", "pay failed 4,productId=\(self.proId ?? ""),error=\(String(describing: tran.error))")
self.resultBlock?(false,tran.error,"购买失败")
break
default:
break
}
}
}
发起支付,商品被添加到列表的时候会进入该代理方法,并且有一个transactionState == .purchasing状态的SKPaymentTransaction。
当用户输入密码完成之后并不会调用这个代理,而是当用户看见完成购买的弹窗并点击OK时,才会再次进入该代理方法,并且 transactionState == .purchased,此时我们去服务端进行验单。
那么产生漏单就有两种情况:
- 当用户输入密码但是没有看见完成的弹窗,或者没有点击OK,直接退出APP。
- 还有用户点击OK但是当还没有进行服务端验单,或验单未完成,直接退出APP。
漏单处理
既然已经清楚了漏单的具体情况,那么我们就来着手处理这两种情况。
-
仅剩服务端未处理
先来看比较简单的情况,用户点击OK,但是服务端验单未完成。
购买成功
上面已经提到,当用户点击OK之后会进入代理方法,并且transactionState == .purchased,此时我们认为支付已经完成,开始验单。
//MARK: 购买成功验证凭证
func completePay(transaction:SKPaymentTransaction) {
//获取交易凭证
if let base64Str = self.getRecepitStr(transaction: transaction) {
if let product = self.product {
guard let productId = product.productId else { return }
guard let amount = product.showAmount else { return }
guard let currency = product.currency else { return }
guard let preorderId = preorderId else { return }
guard let transactionId = transaction.transactionIdentifier else { return }
// 服务端校验
verifyReceipt(productId: productId, preorderId: preorderId, amount: amount, currency: currency, receipt: base64Str, transactionId: transactionId, transaction: transaction)
}
}
}
服务端校验
可以看见本次业务中验单需要的数据有很多,但无论有什么数据,处理起来都是一样的,当我们获取到购买成功之后,将验单所需的所有数据保存一份到本地。
///校验凭证
///- Parameters:
/// - productId: 商品id
/// - preorderId: 订单id
/// - amount: 金额
/// - currency: 货币类型
/// - receipt: 凭证
/// - transactionId: 交易id
func verifyReceipt(productId:String,preorderId:String,amount:String,currency:String,receipt:String,transactionId:String,transaction:SKPaymentTransaction) {
if presenter == nil {
presenter = CSWalletPresenter()
}
// 保存
self.saveReciptInfo(productId: productId, receipt: receipt, preorderId: preorderId, amount: amount, currency: currency, transactionId: transactionId)
presenter?.requestApplePayReceipt(productId: productId,preorderId: preorderId, amount: amount, currency: currency, receipt: receipt, adid: "", transactionId: transactionId, extParamater: ["":""]) {[weak self] success,error in
guard let self = self else { return }
if success {
SKPaymentQueue.default().finishTransaction(transaction)
// 清除
AppPayManager.shareInstance.removeReciptInfo()
self.resultBlock?(true,nil,"支付成功")
} else {
self.resultBlock?(false,error,"支付失败")
}
}
}
保存数据
保存数据的代码如下:
/// 保存交易凭证,订单,及其它商品信息
/// - Parameters:
/// - productId: 商品id
/// - receipt: 交易凭证
/// - preorderId: 订单id
/// - amount: 金额
/// - currency: 货币
/// - transactionId: 交易id
func saveReciptInfo(productId:String? = nil,receipt:String? = nil,preorderId:String? = nil,amount:String? = nil,currency:String? = nil,transactionId:String? = nil) {
CSLog.info(module: "apple pay", "存储交易凭证")
let userDefault = UserDefaults.standard
let uid = CSAccountManager.shared.account?.user?.id ?? 0
let dict = userDefault.dictionary(forKey: "\(uid)-receiptInfo")
let dictM = NSMutableDictionary.init(dictionary: dict ?? [:])
if productId != nil {
dictM["productId"] = productId
}
if receipt != nil {
dictM["receipt"] = receipt
}
if preorderId != nil {
dictM["preorderId"] = preorderId
}
if amount != nil {
dictM["amount"] = amount
}
if currency != nil {
dictM["currency"] = currency
}
if transactionId != nil {
dictM["transactionId"] = transactionId
}
userDefault.set(dictM, forKey: "\(uid)-receiptInfo")
userDefault.synchronize()
}
这里根据用户的uid存储了用户收据到userdefault中,是为了避免验单到其他用户,将金币加给其他用户。
清除数据
如果这订单校验完成,那么清除掉收据信息,清除的代码如下:
//MARK: 移除交易凭证,订单,及其它商品信息
func removeReciptInfo() {
CSLog.info(module: "apple pay", "移除交易凭证")
let userDefault = UserDefaults.standard
let uid = CSAccountManager.shared.account?.user?.id ?? 0
userDefault.removeObject(forKey: "\(uid)-receiptInfo")
userDefault.synchronize()
}
开启漏单检测
在启动时AppDelegate中进行漏单检测:
// 进行漏单检测
AppPayManager.shareInstance.validateReceipt()
检测代码如下:
//MARK: 漏单检测
func validateReceipt(transaction:SKPaymentTransaction? = nil) {
//获取交易凭证信息
guard let reciptInfo = self.getReciptInfo() else {
if let transaction = transaction {
CSLog.info(module: "apple pay", "未完成订单 但是没有凭证,直接销单")
SKPaymentQueue.default().finishTransaction(transaction)
}
return
}
// 商品id
guard let productId = reciptInfo["productId"] as? String else { return }
// 订单id
guard let preorderId = reciptInfo["preorderId"] as? String else { return }
// 交易金额
guard let amount = reciptInfo["amount"] as? String else { return }
// 货币类型
guard let currency = reciptInfo["currency"] as? String else { return }
// 交易凭证
var receipt = reciptInfo["receipt"] as? String
if receipt == nil {
CSLog.info(module: "apple pay", "未点击完成的漏单处理")
receipt = self.getRecepitStr()
}
// 交易id
var transactionId = reciptInfo["transactionId"] as? String
var current_transaction:SKPaymentTransaction?
if transaction == nil {
let transactions = SKPaymentQueue.default().transactions
for transaction in transactions {
if let transactionId = transactionId {
if transaction.transactionIdentifier == transactionId {
current_transaction = transaction
CSLog.info(module: "apple pay", "获取到付款完成的交易")
break
}
} else {
transactionId = transaction.transactionIdentifier
current_transaction = transaction
CSLog.info(module: "apple pay", "获取到未付款完成的交易")
}
}
} else {
CSLog.info(module: "apple pay", "用户已经完成购买,直接验单")
current_transaction = transaction
transactionId = transaction?.transactionIdentifier
}
guard let transaction = current_transaction else {
CSLog.info(module: "apple pay", "transaction is nil")
return }
guard let receipt = receipt else {
CSLog.info(module: "apple pay", "receipt is nil")
return }
guard let transactionId = transactionId else {
CSLog.info(module: "apple pay", "transactionId is nil")
return }
/// 服务端校验收据
verifyReceipt(productId: productId, preorderId: preorderId, amount: amount, currency: currency, receipt: receipt, transactionId: transactionId, transaction: transaction)
}
漏单检测的代码看起来有一点复杂,简单一点说:
我们需要从已经保存的数据中获取服务端所需要的数据,比如订单id,商品id,交易id,金额,货币等等。
从SKPaymentQueue.default().transactions交易队列中获取没有消除的交易。
获取到这些数据之后调用服务端验单接口。
输入密码但苹果未响应
当用户输入密码完成之后,理论上会出现支付成功弹窗,点击弹窗的完成后进购买结果的回调方法中。
但是如果因为用户操作,或者网络原因导致没有出现支付成功弹窗。也会中断支付流程,导致充值金币不能到账。
为了处理这种情况,我们需要提前存储服务端验单所需要的信息。
我们将它放置在发起购买时,代码如下:
//MARK: 发起购买请求
func startPay(product:DGProductModel,preorderId:String,resultBlock:((_ success:Bool,_ error:Error?,_ result:String)->Void)?) {
self.resultBlock = resultBlock
self.product = product
self.preorderId = preorderId
if !SKPaymentQueue.canMakePayments() {
CSLog.info(module: "pay", "pay failed2,productId=\(proId)")
resultBlock?(false,nil,"不可使用苹果支付")
return
}
.....
// 存储一些交易信息
guard let productId = product.productId else { return }
guard let amount = product.showAmount else { return }
guard let currency = product.currency else { return }
self.saveReciptInfo(productId: productId,preorderId: preorderId, amount: amount,currency: currency)
}
等到APP重新启动时,会直接触发购买监听的回调方法,进入购买完成状态,进入completePay方法,但此时没有product,以及订单等其他信息,所以我们之间转到validateReceipt方法进行验证,这时候上一步我们存储的信息就派上了用场:
//MARK: 购买成功验证凭证
func completePay(transaction:SKPaymentTransaction) {
//获取交易凭证
if let base64Str = self.getRecepitStr(transaction: transaction) {
if let product = self.product {
guard let productId = product.productId else { return }
guard let amount = product.showAmount else { return }
guard let currency = product.currency else { return }
guard let preorderId = preorderId else { return }
guard let transactionId = transaction.transactionIdentifier else { return }
// 服务端校验
verifyReceipt(productId: productId, preorderId: preorderId, amount: amount, currency: currency, receipt: base64Str, transactionId: transactionId, transaction: transaction)
} else {
validateReceipt(transaction: transaction)
}
}
}
validateReceipt方法的实现和上面的实现是一样的,就不重复粘贴代码了。
结语
以上就是客户端处理漏单的两种情况,这些漏单的处理方案可以大大减少客户端支付出现的漏单问题。通过有效的监控和处理机制,不仅能够提升用户体验,还能提高支付的成功率,保障用户权益和业务的顺利进行。希望这些方法能为开发者在实际应用中提供有价值的参考,确保支付流程的稳定与可靠。