一、优化后的方案架构
1. 数据流设计
复制
App → 发起订阅 → 苹果服务器 → 返回收据 → 本地存储
App → 发送收据 → 服务器 → 验证收据 → 返回订阅状态
App ← 定期检查 ← 服务器 ← 订阅状态更新
2. 防止丢单策略
- 本地收据缓存
- 失败重试机制
- 定期状态同步
- 服务器端状态备份
二、代码实现(Swift)
1. 订阅管理器核心类
import StoreKit
class SubscriptionManager: NSObject {
static let shared = SubscriptionManager()
private var availableSubscriptions = [SKProduct]()
private let receiptRefreshQueue = DispatchQueue(label: "com.yourapp.receiptRefresh")
// 订阅商品ID
let subscriptionIds: Set<String> = [
"com.yourapp.monthly",
"com.yourapp.yearly"
]
override init() {
super.init()
SKPaymentQueue.default().add(self)
startReceiptMonitoring()
}
deinit {
SKPaymentQueue.default().remove(self)
}
private func startReceiptMonitoring() {
// 每12小时检查一次订阅状态
Timer.scheduledTimer(withTimeInterval: 43200, repeats: true) { _ in
self.validateReceiptWithServer()
}
}
}
2. 订阅状态管理
extension SubscriptionManager {
enum SubscriptionStatus {
case active
case expired
case inGracePeriod
case unknown
}
struct SubscriptionInfo {
let productId: String
let expiresDate: Date
let isInTrial: Bool
let isAutoRenewing: Bool
}
private var currentSubscription: SubscriptionInfo?
func getSubscriptionStatus() -> SubscriptionStatus {
guard let subscription = currentSubscription else { return .unknown }
if subscription.isAutoRenewing {
return .active
}
if Date() < subscription.expiresDate {
return .inGracePeriod
}
return .expired
}
}
3. 收据验证与防丢单
extension SubscriptionManager {
private func validateReceiptWithServer() {
guard let receiptData = fetchReceiptData() else {
refreshReceipt()
return
}
let receiptString = receiptData.base64EncodedString()
// 发送到服务器验证
sendReceiptToServer(receiptString) { [weak self] result in
switch result {
case .success(let subscriptionInfo):
self?.currentSubscription = subscriptionInfo
self?.storeReceiptLocally(receiptData)
case .failure(let error):
self?.handleValidationError(error)
}
}
}
private func fetchReceiptData() -> Data? {
guard let receiptURL = Bundle.main.appStoreReceiptURL else { return nil }
return try? Data(contentsOf: receiptURL)
}
private func storeReceiptLocally(_ data: Data) {
// 存储到UserDefaults或Keychain
UserDefaults.standard.set(data, forKey: "latestReceipt")
}
private func handleValidationError(_ error: Error) {
// 错误处理策略
if let storedReceipt = UserDefaults.standard.data(forKey: "latestReceipt") {
// 使用本地缓存的收据重试
let receiptString = storedReceipt.base64EncodedString()
sendReceiptToServer(receiptString)
}
}
}
4. 服务器通信
extension SubscriptionManager {
private func sendReceiptToServer(_ receipt: String, completion: @escaping (Result<SubscriptionInfo, Error>) -> Void) {
let requestBody: [String: Any] = [
"receipt-data": receipt,
"password": "your_shared_secret", // 从App Store Connect获取
"exclude-old-transactions": true
]
let url = URL(string: "https://your-server.com/validate-receipt")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = try? JSONSerialization.data(withJSONObject: requestBody)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data,
let response = try? JSONDecoder().decode(ServerResponse.self, from: data) else {
completion(.failure(ValidationError.invalidResponse))
return
}
// 解析订阅信息
if let subscription = self.parseSubscription(from: response) {
completion(.success(subscription))
} else {
completion(.failure(ValidationError.invalidSubscription))
}
}.resume()
}
private struct ServerResponse: Codable {
let latestReceiptInfo: [ReceiptInfo]
let pendingRenewalInfo: [RenewalInfo]
}
private struct ReceiptInfo: Codable {
let productId: String
let expiresDate: String
let isTrialPeriod: String
}
private struct RenewalInfo: Codable {
let autoRenewProductId: String
let autoRenewStatus: String
}
}
5. 订阅生命周期管理
extension SubscriptionManager: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchased, .restored:
handleSuccessfulTransaction(transaction)
case .failed:
handleFailedTransaction(transaction)
default:
break
}
}
}
private func handleSuccessfulTransaction(_ transaction: SKPaymentTransaction) {
// 立即验证收据
validateReceiptWithServer()
// 标记交易完成
SKPaymentQueue.default().finishTransaction(transaction)
// 记录交易ID防止重复处理
UserDefaults.standard.set(transaction.transactionIdentifier, forKey: "lastProcessedTransactionId")
}
private func handleFailedTransaction(_ transaction: SKPaymentTransaction) {
if let error = transaction.error as? SKError {
switch error.code {
case .paymentCancelled:
print("用户取消支付")
default:
// 记录失败交易以便重试
saveFailedTransaction(transaction)
}
}
SKPaymentQueue.default().finishTransaction(transaction)
}
private func saveFailedTransaction(_ transaction: SKPaymentTransaction) {
// 将失败交易存储到本地
let failedTransaction = [
"productId": transaction.payment.productIdentifier,
"transactionId": transaction.transactionIdentifier ?? "",
"date": Date().timeIntervalSince1970
] as [String : Any]
var failedTransactions = UserDefaults.standard.array(forKey: "failedTransactions") as? [[String: Any]] ?? []
failedTransactions.append(failedTransaction)
UserDefaults.standard.set(failedTransactions, forKey: "failedTransactions")
}
func retryFailedTransactions() {
guard let failedTransactions = UserDefaults.standard.array(forKey: "failedTransactions") as? [[String: Any]] else { return }
for transaction in failedTransactions {
if let productId = transaction["productId"] as? String,
let product = availableSubscriptions.first(where: { $0.productIdentifier == productId }) {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
}
// 清空失败记录
UserDefaults.standard.removeObject(forKey: "failedTransactions")
}
}
三、服务器端验证流程
1. 验证接口设计
POST /validate-receipt
{
"receipt-data": "base64_receipt",
"password": "shared_secret"
}
2. 响应处理
{
"status": 0,
"latest_receipt_info": [
{
"product_id": "com.yourapp.monthly",
"expires_date": "2023-12-31 23:59:59 Etc/GMT",
"is_trial_period": "false"
}
],
"pending_renewal_info": [
{
"auto_renew_product_id": "com.yourapp.monthly",
"auto_renew_status": "1"
}
]
}
3. 状态码处理
- 0: 成功
- 21000-21010: 各种验证错误
- 21100-21199: 内部服务器错误
四、防止丢单策略实现
1. 本地缓存机制
- 存储最新收据
- 记录最后处理的交易ID
- 保存失败交易记录
2. 重试机制
- 网络错误自动重试
- 定期检查未完成交易
- 应用启动时验证订阅状态
3. 服务器端备份
- 存储用户订阅历史
- 定期与苹果服务器同步
- 提供状态查询接口
五、测试与调试建议
- 使用StoreKit Testing进行本地测试
- 模拟各种网络错误场景
- 测试订阅续期和过期流程
- 验证恢复购买功能
- 测试跨设备同步
这个优化后的方案专注于订阅管理,提供了完整的防丢单机制,并通过服务器端验证确保订阅状态的准确性。实际部署时,请根据具体业务需求调整细节。