PAYATHON 2026

iPhone App 中应如何调用支付服务并获取支付结果?

支付小周

结论

不要让 App 根据 WebView 的页面内容或浏览器重定向参数判断支付是否成功。

建议采用以下流程:

  1. App 向业务服务器请求创建订单。
  2. 服务器计算最终金额,校验优惠券和用户身份,并返回支付页面 URL。
  3. App 优先使用支付服务商的官方 SDK。如果没有 SDK,则使用 SFSafariViewController 打开支付页面。
  4. 支付结束后,通过 Universal Link 或自定义 URL Scheme 唤回 App。
  5. App 被唤回后,根据订单号向业务服务器查询支付状态。
  6. 业务服务器根据支付平台的服务端通知或主动查询结果,返回最终状态。

浏览器回调只能说明支付页面已经返回,不能证明付款成功。支付结果必须以服务器确认的状态为准。

为什么不应直接读取支付页面结果

在 WKWebView 中监听 URL,确实可以发现页面跳转,但这种做法并不可靠:

  • 支付过程可能跳转到银行、3-D Secure 或其他第三方页面。
  • 页面结构和重定向规则可能发生变化。
  • Cookie、安全策略以及外部 App 跳转的行为,可能与系统浏览器不同。
  • 客户端参数和 JavaScript 都可能被篡改。
  • 用户付款成功后可能直接关闭页面,导致 App 收不到最后一次跳转。

直接使用 Safari 也有类似限制,因为 App 无法可靠读取浏览器中的页面内容。支付流程应设计成 App 被唤回后再向服务器核验,而不是持续追踪页面。

如果支付服务商提供官方 iOS SDK,应优先使用,并按服务商的要求处理支付回调。

推荐的支付流程

1. App 请求服务器创建订单

App 只提交商品、优惠券等业务信息,不应自行决定可信的付款金额:

POST /api/orders
Authorization: Bearer <access-token>
Content-Type: application/json

{
  "items": [
    {
      "productId": "product-123",
      "quantity": 2
    }
  ],
  "couponCode": "SAVE10"
}

服务器负责:

  • 根据登录凭证识别用户,不直接信任客户端提交的 userid。
  • 查询商品的真实价格。
  • 验证优惠券是否有效。
  • 计算最终金额。
  • 创建唯一订单号。
  • 调用支付平台创建支付会话。

服务器随后返回:

{
  "orderId": "order-20260901-001",
  "checkoutURL": "https://pay.example.com/checkout/session_abc"
}

不要将可信的 amount 直接拼进支付 URL。越狱设备、代理工具或经过修改的客户端都可能篡改请求。

2. 在 App 中打开支付页面

没有官方 SDK 时,可以使用 SFSafariViewController:

import SafariServices
import UIKit

final class CheckoutViewController: UIViewController {
    private var orderID: String?

    func startCheckout(orderID: String, checkoutURL: URL) {
        self.orderID = orderID

        let controller = SFSafariViewController(url: checkoutURL)
        present(controller, animated: true)
    }
}

这样既能让用户留在 App 的界面流程中,又能由系统提供的浏览器组件处理支付页面。除非支付服务商明确要求使用嵌入式 WebView,否则不建议自行用 WKWebView 承载完整的支付流程。

3. 支付完成后唤回 App

支付平台完成流程后,可以重定向到业务服务器提供的回调地址,例如:

https://example.com/payment/return?orderId=order-20260901-001

该地址再通过 Universal Link 打开 App。App 接收链接:

func scene(
    _ scene: UIScene,
    continue userActivity: NSUserActivity
) {
    guard
        userActivity.activityType == NSUserActivityTypeBrowsingWeb,
        let url = userActivity.webpageURL
    else {
        return
    }

    handlePaymentReturn(url)
}

private func handlePaymentReturn(_ url: URL) {
    guard
        url.host == "example.com",
        url.path == "/payment/return",
        let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
        let orderID = components.queryItems?
            .first(where: { $0.name == "orderId" })?
            .value
    else {
        return
    }

    checkPaymentStatus(orderID: orderID)
}

如果暂时无法部署 Universal Link,也可以使用自定义 URL Scheme:

myapp://payment/return?orderId=order-20260901-001

Universal Link 通常更合适,因为它使用受域名控制的 HTTPS 链接。无论选择哪种方式,都不要相信回调 URL 中的 status=success。

4. 向服务器查询最终状态

App 收到回调后,只从中提取订单号,然后向业务服务器查询:

enum PaymentStatus: String, Decodable {
    case pending
    case paid
    case failed
    case cancelled
}

struct PaymentStatusResponse: Decodable {
    let orderId: String
    let status: PaymentStatus
}

func checkPaymentStatus(orderID: String) {
    guard let url = URL(
        string: "https://api.example.com/api/orders/\(orderID)/payment-status"
    ) else {
        return
    }

    var request = URLRequest(url: url)
    request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")

    URLSession.shared.dataTask(with: request) { data, response, error in
        guard
            error == nil,
            let httpResponse = response as? HTTPURLResponse,
            httpResponse.statusCode == 200,
            let data,
            let result = try? JSONDecoder().decode(
                PaymentStatusResponse.self,
                from: data
            )
        else {
            return
        }

        DispatchQueue.main.async {
            switch result.status {
            case .paid:
                self.showMessage("Payment successful")
            case .failed, .cancelled:
                self.showMessage("Payment failed")
            case .pending:
                self.showMessage("Payment is being confirmed")
            }
        }
    }.resume()
}

生产代码还需要处理网络超时、身份失效、响应解析失败以及重试次数限制。

服务器如何确认支付成功

业务服务器应使用支付平台提供的服务端机制确认结果,通常需要:

  • 接收支付平台发送的 webhook 或异步通知。
  • 根据支付流水号主动查询支付平台。
  • 校验通知签名、订单号、商户号、币种和金额。
  • 使用数据库事务将订单状态从 pending 更新为 paid。
  • 对重复通知进行幂等处理。

建议至少保存以下信息:

orderId
userId
expectedAmount
currency
providerTransactionId
paymentStatus
createdAt
paidAt

客户端显示支付成功前,服务器应先确认实际付款金额与订单应付金额一致。仅凭浏览器跳转,不能将订单标记为已付款。

用户没有返回 App 时怎么办

用户付款成功后,可能直接关闭浏览器,也可能遇到 App 崩溃或网络暂时中断。因此,系统不能只在收到 URL 回调时检查状态。

还应在以下时机重新查询未完成订单:

  • App 回到前台时。
  • 用户重新进入订单详情页时。
  • 支付页面被关闭时。
  • 订单处于 pending 状态时,经过短暂间隔进行有限次数的重试。

即使 App 当时没有运行,服务器收到支付通知后仍能保存正确结果。用户下次打开 App 时,也可以看到最终状态。

需要特别注意的事项

  • 全程使用 HTTPS,不要在 URL 中放置访问令牌、优惠券密钥或其他敏感数据。
  • 不要信任客户端提交的 userid、amount、折扣金额和支付状态。
  • 每次支付都应使用唯一的订单号和支付会话,避免重复扣款。
  • 创建订单和处理支付通知时都要保证幂等性。
  • 不要将 pending 显示为失败,它可能只是服务端通知尚未到达。
  • Universal Link 或 URL Scheme 只负责导航,不负责认证支付结果。
  • 不要在 App 内保存支付平台的商户私钥或服务端密钥。
  • 如果出售的是供 App 内消费的数字内容、订阅或虚拟功能,需要先确认是否必须使用 Apple 的 In-App Purchase。实体商品或在 App 外消费的服务通常采用外部支付,但仍应根据商品性质和发布地区,核对当时有效的 App Store 审核规则。

支付页面负责完成交互,回调链接负责唤回 App,服务器负责确认付款是否真正成功。

备注:内容仅供参考。