三.苹果支付 - 漏单补单处理

介绍

苹果的支付流程并不复杂,我们可以在很短的时间内实现正常的支付流程。

但是苹果支付有一个很大特点就是慢,包括唤起支付弹窗,点击完成到服务端验单完成,整个流程很长,任何一个环节都有可能因为断网,或者退出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,此时我们去服务端进行验单。

那么产生漏单就有两种情况:

  1. 当用户输入密码但是没有看见完成的弹窗,或者没有点击OK,直接退出APP。
  2. 还有用户点击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方法的实现和上面的实现是一样的,就不重复粘贴代码了。

结语

以上就是客户端处理漏单的两种情况,这些漏单的处理方案可以大大减少客户端支付出现的漏单问题。通过有效的监控和处理机制,不仅能够提升用户体验,还能提高支付的成功率,保障用户权益和业务的顺利进行。希望这些方法能为开发者在实际应用中提供有价值的参考,确保支付流程的稳定与可靠。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/715936.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

kubesphere踩过的坑,持续更新....

踩过的坑 The connection to the server lb.kubesphere.local:6443 was refused - did you specify the right host… 另一篇文档中 dashboard 安装 需要在浏览器中输入thisisunsafe,即可进入登录页面 ingress 安装的问题 问题描述: 安装后通过命令 kubectl g…

Oracle 是否扼杀了开源 MySQL

Oracle 是否无意中扼杀了开源 MySQL Peter Zaitsev是一位俄罗斯软件工程师和企业家,曾在MySQL公司担任性能工程师。大约15年前,当甲骨文收购Sun公司并随后收购MySQL时,有很多关于甲骨文何时“杀死MySQL”的讨论。他曾为甲骨文进行辩护&#…

【系统架构设计师】一、计算机系统基础知识(指令系统|存储系统|输入输出技术|总线结构)

目录 一、指令系统 1.1 计算机指令 1.2 指令寻址方式 1.3 CISC 与 RISC 1.4 指令流水线 二、存储系统 2.1 分级存储体系 2.2 地址映射 2.3 替换算法 2.4 磁盘 2.4.1 磁盘结构和参数 2.4.2 磁盘调度算法 三、输入输出技术 四、总线结构 五、考试真题练习 一、指令…

【PL理论】(29) OOP:面向对象编程 | 案例研究:C++ 中的类 | 继承 | 继承和指针 | Object-oriented Programming

💭 写在前面:本章我们将进入 Object-oriented Programming,面向对象编程的讲解,探讨 C 中的类,继承等。 目录 0x00 面向对象编程 0x01 C语言中的结构体 0x02 案例研究:C 中的类 0x03 术语 0x04 继承&…

12.3 Go 测试覆盖率

💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

关于 python request 的 response 返回 b‘\xa3\xff\xff\x11E .....‘ 类型的数据的解决方案

最近写开发一个爬虫, 程序在本地好好的,返回的是正常的 html, 但是到了生产环境,不知道为什么返回的是一堆乱码 长这样: 查了好几天都没有进展, 对其进行各种转码均无效 今天终于找到解决办法了&#xff…

LabVIEW回热系统热经济性分析及故障诊断

开发了一种利用LabVIEW软件的电厂回热系统热经济性分析和故障诊断系统。该系统针对火电厂回热加热器进行优化,通过实时数据监控与分析,有效提高机组的经济性和安全性,同时降低能耗和维护成本。系统的实施大幅提升了火电厂运行的效率和可靠性&…

永久删除的文件如何恢复?记好这4个方法,轻松恢复文件!

“在清理电脑时,我一不小心把一些还需要的文件永久删除了,不知道大家有没有方法可以恢复这些文件呢?” 在数字时代,我们的生活和工作几乎都离不开电脑和各类存储设备。然而,随着数据的不断增长,误删文件、格…

DNS响应时间分析

目录 什么是DNS响应时间? 为什么DNS响应时间很重要? AnaTraf流量分析仪DNS分析 在当今数字化时代,网络的稳定性和性能对企业的运营至关重要。作为IT运维人员,我们的职责是确保网络顺畅运行,而DNS(域名系…

想做副业,如何发现用户需求呢?

点击下方“JavaEdge”,选择“设为星标” 第一时间关注技术干货! 免责声明~ 任何文章不要过度深思! 万事万物都经不起审视,因为世上没有同样的成长环境,也没有同样的认知水平,更「没有适用于所有人的解决方案…

关于电脑文件的规划思考

概述 设置C、D、E、F 四个盘 C盘:系统数据使用,操作系统、其他软件需要用到的系统性资源 D盘:应用软件区 的使用,数据库、navacat、idea、visual studio、浏览器、向日葵、虚拟机…… E盘:工作区:公司资料…

【云】各家云服务器介绍

文章目录 各主流云服务厂商个人开发者购买什么够用优惠信息 各主流云服务厂商 腾讯云: 优点:腾讯云在程序员圈子中享有“良心云”的美誉,主要是因为其价格性价比高,续费价格也不贵。腾讯云提供了多款云服务器配置,如2核…

物流货运单怎么打印,佳易王货物运单打印查询统计管理系统操作教程

物流货运单怎么打印,佳易王货物运单打印查询统计管理系统操作教程 一、前言 以下软件操作教程以,佳易王物流货运单管理软件为例说明 软件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 软件操作教程 1、物流开单点击导航栏 物流开单…

碉堡了!云原生大佬撰写的K8s学习指南,有点炸裂,建议运维都收藏!

在这个数字化转型的时代,容器化和云原生技术已成为软件开发的重要趋势。【Kubernetes】作为云原生领域的旗舰项目,不仅是一种容器编排工具,更是构建现代、弹性和可扩展应用程序的基础平台。 因此,对于运维人员来说,学…

【第四篇】SpringSecurity的HttpSecurity详解

SpringSecurity的XML文件配置 在配置文件中通过 security:http 等标签来定义了认证需要的相关信息 SpringSecurity的HttpSecurity配置 在SpringBoot项目中,脱离了xml配置文件的方式,在SpringSecurity中提供了HttpSecurity等工具类,这里HttpSecurity就等同于在配置文件中定…

【算法】Graham 凸包扫描算法 ( 凸包概念 | 常用的凸包算法 | 角排序 | 叉积 | Python 代码示例 )

文章目录 一、Graham 凸包扫描算法1、凸包概念2、常用的凸包算法3、Graham 凸包扫描算法 二、Graham 算法前置知识点1、角排序2、叉积3、算法过程分析 三、代码示例1、完整代码示例2、执行结果 使用 Graham 算法绘制的凸包效果 : 博客代码下载 : https://download.csdn.net/d…

【编程语言】Python平台化为何比Java差?

人不走空 🌈个人主页:人不走空 💖系列专栏:算法专题 ⏰诗词歌赋:斯是陋室,惟吾德馨 目录 🌈个人主页:人不走空 💖系列专栏:算法专题 ⏰诗词歌…

[Shell编程学习路线]——if条件语句(单,双,多分支结构)详细语法介绍

🏡作者主页:点击! 🛠️Shell编程专栏:点击! ⏰️创作时间:2024年6月17日7点50分 🀄️文章质量:95分 文章目录 ————前言———— 💯趣站&#x1f4af…

AI大模型在运动项目的深度融合和在穿戴设备的实践及未来运动健康技术发展

文章目录 1. 技术架构2. 模型选择2.1 LSTM(长短期记忆网络)2.2 CNN(卷积神经网络)2.3 Transformer 3. 数据处理数据预处理 4. 实时性要求4.1 边缘计算4.2 模型优化 5. 数据隐私与安全6. 深入分析AI大模型在穿戴设备的应用和未来发…

Harbor镜像中心搭建

文章目录 Harbor镜像中心搭建前置条件下载Harbor创建CA证书配置Harbor开始启动地址映射访问配置本地登录配置外部虚拟机访问 Harbor镜像中心搭建 Harbor是一个镜像中心,我们所熟知的DockerHub就是一个镜像中心,我们可以把我们打包的镜像放在镜像中心中供…