调用 List Payment API 允许的最短间隔是多少?
支付阿杰
结论
List Payments API 通常不会强制规定两次调用至少间隔多少秒。单台设备每隔 5~10 秒查询一次,频率较低,一般不会因为调用间隔本身出现问题。
需要关注的是 API 的速率限制。如果请求过于频繁,服务端可能返回 429 Too Many Requests。具体限额可能因账户、接口版本或平台策略而调整,应以当前官方文档和实际响应头为准,不要把某个固定的 QPS 数值当作长期不变的保证。
建议的实现方式
查询时用 location_id 限定门店,并通过 begin_time 缩小时间范围。实现时还要注意:
- 保存最后一次成功同步的时间。
- 下一次查询保留几秒的重叠窗口,避免因网络或交易写入延迟而漏单。
- 使用 payment ID 去重,不要只依赖时间戳。
- 正确处理分页游标
cursor,即使每次通常只有 1~3 笔记录。 - 收到
429后暂停固定频率轮询,并根据Retry-After或指数退避策略重试。 - 处理完当前查询及全部分页后,再更新同步时间。
例如,上次成功同步到 10:00:10Z,下次可以从 10:00:05Z 开始查询,不必严格从 10:00:10Z 开始。重复返回的 payment 可通过唯一 ID 排除。
Swift 示例
下面是基本的轮询逻辑。URL、鉴权方式和响应结构需要根据实际使用的 API 版本调整:
import Foundation
struct PaymentsResponse: Decodable {
let payments: [Payment]?
let cursor: String?
}
struct Payment: Decodable {
let id: String
let createdAt: String?
enum CodingKeys: String, CodingKey {
case id
case createdAt = "created_at"
}
}
final class PaymentPoller {
private let accessToken: String
private let locationID: String
private let session = URLSession.shared
private var lastSuccessfulSync = Date()
private var processedPaymentIDs = Set<String>()
init(accessToken: String, locationID: String) {
self.accessToken = accessToken
self.locationID = locationID
}
func poll() async {
// 保留 5 秒重叠窗口,降低漏单风险。
let beginTime = lastSuccessfulSync.addingTimeInterval(-5)
let syncStartedAt = Date()
do {
try await fetchAllPages(beginTime: beginTime)
lastSuccessfulSync = syncStartedAt
} catch {
// 失败时不要推进 lastSuccessfulSync,否则可能漏掉交易。
print("Payment polling failed: \(error)")
}
}
private func fetchAllPages(
beginTime: Date,
cursor: String? = nil
) async throws {
var components = URLComponents(
string: "https://connect.squareup.com/v2/payments"
)!
var queryItems = [
URLQueryItem(name: "location_id", value: locationID),
URLQueryItem(
name: "begin_time",
value: ISO8601DateFormatter().string(from: beginTime)
)
]
if let cursor {
queryItems.append(URLQueryItem(name: "cursor", value: cursor))
}
components.queryItems = queryItems
var request = URLRequest(url: components.url!)
request.httpMethod = "GET"
request.setValue(
"Bearer \(accessToken)",
forHTTPHeaderField: "Authorization"
)
request.setValue("application/json", forHTTPHeaderField: "Accept")
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
if httpResponse.statusCode == 429 {
let retryAfter = httpResponse.value(
forHTTPHeaderField: "Retry-After"
)
print("Rate limited; Retry-After: \(retryAfter ?? "not provided")")
throw URLError(.resourceUnavailable)
}
guard (200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
let result = try JSONDecoder().decode(
PaymentsResponse.self,
from: data
)
for payment in result.payments ?? [] {
guard processedPaymentIDs.insert(payment.id).inserted else {
continue
}
display(payment)
}
if let nextCursor = result.cursor, !nextCursor.isEmpty {
try await fetchAllPages(
beginTime: beginTime,
cursor: nextCursor
)
}
}
private func display(_ payment: Payment) {
print("New payment: \(payment.id)")
}
}
可以用定时任务触发轮询:
let poller = PaymentPoller(
accessToken: "<ACCESS_TOKEN>",
locationID: "<LOCATION_ID>"
)
Task {
while !Task.isCancelled {
await poller.poll()
// 正常情况下每 5 秒执行一次。
try? await Task.sleep(for: .seconds(5))
}
}
速率限制与退避
请求失败后不要立即无限重试。应优先遵循 Retry-After。如果服务端没有返回该响应头,可以采用指数退避:
1s → 2s → 4s → 8s → 16s
请求成功后,再恢复正常的 5~10 秒轮询间隔。也可以加入少量随机抖动,避免多个客户端同时集中重试。
注意事项
begin_time应使用服务端接受的 ISO 8601 时间格式,并统一采用 UTC。- 不要把设备当前时间视为绝对可靠的交易边界,设备时钟偏差可能造成遗漏。
- Payment 的创建、更新和最终状态变化可能发生在不同时间。如果业务只展示已完成订单,需要确认筛选的是正确状态。
- 同步时间和去重信息应持久化,避免应用重启后漏单或大量重复展示。
- 如果服务端提供
updated_at、排序条件或其他增量同步机制,应根据实际接口语义选择同步边界。 - 不宜把访问令牌长期硬编码在公开发布的 iOS 应用中。生产环境中,更稳妥的做法是由受控后端调用支付 API,再向门店设备提供必要的订单数据。
- 轮询间隔采用 5 秒还是 10 秒,应根据实际延迟要求、错误率和
429响应动态调整。没有收到限流响应,不代表可以无限提高调用频率。
备注:内容仅供参考。
