iOS 接入(Swift)
这套接入适合谁
原生 iOS 应用(Swift / SwiftUI 或 UIKit)需要在自己的界面里展示应付金额与收款信息, 并在买家付款后刷新订单状态。示例用 URLSession + Codable,不引第三方库。
前置准备
- 已拿到 API KEY(见立即接入)。
- 已配置收款地址并设为启用。
- 有一个你自己的服务端。API KEY 不能放进 App。
KEY 不能打进 IPA
iOS 包体可以被反编译,Info.plist 与二进制里的字符串都不安全。App 里的下单请求发往你自己的服务端。
安装
无需额外依赖,系统自带的 URLSession 即可;界面按需用 SwiftUI。
最小可跑示例
1. 你的服务端下单
以 Swift 服务端风格(Vapor / 或任意后端)为代表,核心是"带 KEY 调开放接口":
swift
import Foundation
func createOrder(orderNo: String, amount: String) async throws -> Data {
var req = URLRequest(url: URL(string: "\(ProcessInfo.processInfo.environment["API_BASE"]!)/api/pay/open/createOrder")!)
req.httpMethod = "POST"
req.setValue(ProcessInfo.processInfo.environment["PAY_API_KEY"], forHTTPHeaderField: "X-Pay-Api-Key")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONSerialization.data(withJSONObject: [
"merchantOrderNo": orderNo, // 幂等键
"chainId": 2,
"tokenId": 1,
"amount": amount, // 代币单位小数字符串
"subject": "订单 \(orderNo)",
"notifyUrl": "https://your-shop.com/pay/notify",
])
let (data, _) = try await URLSession.shared.data(for: req)
return data
}2. App 侧:只打你自己的后端
swift
struct CheckoutRequest: Codable { let orderNo: String; let amount: String }
struct CheckoutResponse: Codable {
let code: Int
let orderNo: String?
let payAddress: String?
let payAmountDisplay: String?
let payAmountMinimal: String?
let chainKey: String?
let msg: String?
}
struct StatusResponse: Codable {
let code: Int
let status: Int // 1..6,只认 3/4 为已付款
let statusText: String?
let txHash: String?
}
enum ShopAPI {
static func checkout(_ body: CheckoutRequest) async throws -> CheckoutResponse {
var req = URLRequest(url: URL(string: "https://your-shop.com/api/checkout")!)
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONEncoder().encode(body) // KEY 不在这里
let (data, _) = try await URLSession.shared.data(for: req)
return try JSONDecoder().decode(CheckoutResponse.self, from: data)
}
static func status(orderNo: String) async throws -> StatusResponse {
let url = URL(string: "https://your-shop.com/api/order-status?orderNo=\(orderNo)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(StatusResponse.self, from: data)
}
}swift
@MainActor
final class CheckoutViewModel: ObservableObject {
enum State { case loading, ready(CheckoutResponse), pending(CheckoutResponse),
paid(CheckoutResponse), error(String) }
@Published var state: State = .loading
func start(orderNo: String, amount: String) async {
do {
let o = try await ShopAPI.checkout(.init(orderNo: orderNo, amount: amount))
guard o.code == 0 else { state = .error(o.msg ?? "下单失败"); return }
state = .ready(o)
while true {
try await Task.sleep(for: .seconds(3))
let s = try await ShopAPI.status(orderNo: o.orderNo ?? orderNo)
switch s.status {
case 1: state = .ready(o) // 等待付款
case 2: state = .pending(o) // 已提交,等确认
case 3, 4: state = .paid(o); return
case 5: state = .error("订单已过期"); return
case 6: state = .error("待人工认领,请联系客服"); return
default: break
}
}
} catch {
state = .error(error.localizedDescription)
}
}
}也可以直接打开平台收银台:
{cashierBaseUrl}/pay/cashier/m/{merchantNo}, 两个值从商户侧GET /api/pay/getMyProfile取,用UIApplication.shared.open打开。
回调处理
回调由你的服务端接收。Swift 服务端验签示例:
swift
import CryptoKit
func verify(rawBody: String, ts: String, sig: String, apiKey: String) -> Bool {
let key = SymmetricKey(data: Data(apiKey.utf8))
var mac = HMAC<SHA256>.init(key: key)
mac.update(data: Data(ts.utf8)) // 时间戳 + 原始正文,字符串拼接
mac.update(data: Data(rawBody.utf8))
let want = mac.finalize().map { String(format: "%02x", $0) }.joined()
// 常数时间比较
return constantTimeEqual(want, sig)
}处理要点:
- 幂等:回调会重试,用
merchantOrderNo去重,别重复发货。 - 快返回:约 10 秒超时,耗时逻辑放后台任务。
- 以查询为准:关键动作前用查询订单复核。
唤起钱包与 App 间跳转
- iOS 上多数加密钱包支持通用链接(Universal Link)或自定义 scheme,
UIApplication.shared.open即可尝试唤起。 - 在
Info.plist的LSApplicationQueriesSchemes里声明你要探测的 scheme,才能用canOpenURL判断是否已安装。 - 同时展示二维码作为兜底:没装钱包的用户可以用另一台设备扫码。
常见问题
| 问题 | 说明 |
|---|---|
KEY 能放 Info.plist / Keychain 吗 | 不能。App 端不持有 KEY,下单走后端 |
| 后台被系统挂起导致轮询中断 | 进入后台暂停轮询,scenePhase 回到前台时恢复并立即查一次 |
| 能不能用 WebView 嵌收银页 | 可以,但直接用系统浏览器打开平台收银台更省事 |
| 怎么判断已付款 | 只认状态 3 / 4,不要用数值大小比较 |
| 重复请求会不会多开单 | 不会,同一个 merchantOrderNo 幂等复用 |