iPhone App 中应如何调用支付服务并获取支付结果?
结论
不要让 App 根据 WebView 的页面内容或浏览器重定向参数判断支付是否成功。
建议采用以下流程:
- App 向业务服务器请求创建订单。
- 服务器计算最终金额,校验优惠券和用户身份,并返回支付页面 URL。
- App 优先使用支付服务商的官方 SDK。如果没有 SDK,则使用
SFSafariViewController打开支付页面。 - 支付结束后,通过 Universal Link 或自定义 URL Scheme 唤回 App。
- App 被唤回后,根据订单号向业务服务器查询支付状态。
- 业务服务器根据支付平台的服务端通知或主动查询结果,返回最终状态。
浏览器回调只能说明支付页面已经返回,不能证明付款成功。支付结果必须以服务器确认的状态为准。
为什么不应直接读取支付页面结果
在 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,服务器负责确认付款是否真正成功。
备注:内容仅供参考。
