1、Xcode配置

截屏2025-07-18 17.25.44.png
2、代码逻辑
-
获取产品列表(ProductIDs产品id可以让服务端返回,这样比写在本地灵活)
// MARK: - 自定义错误
enum SubscriptionError: LocalizedError {
case cannotMakePayments
case receiptValidationFailed
case productNotFound
case networkError
case unknownError
var errorDescription: String? {
switch self {
case .cannotMakePayments:
return "设备不支持内购"
case .receiptValidationFailed:
return "收据验证失败"
case .productNotFound:
return "产品未找到"
case .networkError:
return "网络错误"
case .unknownError:
return "未知错误"
}
}
}
// MARK: - 产品ID定义
struct ProductIDs {
// 自动续订订阅
static let monthlySubscription = "com.demo.monthly.sub"
static let quarterlySubscription = "com.demo.quarterly.sub"
static let yearlySubscription = "com.demo.yearly.sub"
// 非续订会员
static let onetimeMonthly = "com.demo.onetime.monthly"
static let onetimeQuarterly = "com.demo.onetime.quarterly"
static let onetimeYearly = "com.demo.onetime.yearly"
static let allProducts = [
monthlySubscription,
quarterlySubscription,
yearlySubscription,
onetimeMonthly,
onetimeQuarterly,
onetimeYearly
]
}
// MARK: - 属性
private var products: [String: SKProduct] = [:]
private var productsRequest: SKProductsRequest?
private var purchaseCompletionHandlers: [String: (PurchaseResult) -> Void] = [:]
private var loadProductsCompletion: ((Swift.Result<[SKProduct], Error>) -> Void)?
// 收据刷新完成回调
private var receiptRefreshCompletion: ((Bool) -> Void)?
// 收据刷新超时定时器
private var receiptRefreshTimer: Timer?
// 回调闭包
var onPurchaseSuccess: ((String) -> Void)?
var onPurchaseFailure: ((Error) -> Void)?
var onRestoreComplete: (([String]) -> Void)?
/// 加载所有产品信息
func loadProducts(completion: @escaping (Swift.Result<[SKProduct], Error>) -> Void) {
guard !ProductIDs.allProducts.isEmpty else {
completion(.failure(SubscriptionError.productNotFound))
return
}
loadProductsCompletion = completion
let productIdentifiers = Set(ProductIDs.allProducts)
productsRequest?.cancel()
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productsRequest?.delegate = self
productsRequest?.start()
}
// MARK: - SKProductsRequestDelegate
extension SubscriptionManager: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
let loadedProducts = response.products
DDLogInfo("[\(TAG)] ✅ 获取到所有产品,数量=\(loadedProducts.count)")
// 缓存产品信息
for product in loadedProducts {
products[product.productIdentifier] = product
}
// 打印无效的产品ID
if !response.invalidProductIdentifiers.isEmpty {
DDLogInfo("[\(TAG)] ⚠️ 无效的产品ID: \(response.invalidProductIdentifiers)")
}
// 调用完成回调
DispatchQueue.main.async {
self.loadProductsCompletion?(.success(loadedProducts))
self.loadProductsCompletion = nil
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
if request is SKReceiptRefreshRequest {
DDLogInfo("[\(TAG)] ❌ 收据刷新失败: \(error.localizedDescription)")
} else {
DDLogInfo("[\(TAG)] ❌ 产品请求失败: \(error.localizedDescription)")
DispatchQueue.main.async {
self.loadProductsCompletion?(.failure(error))
self.loadProductsCompletion = nil
}
}
}
}
/// 加载单个产品信息
/// - Parameters:
/// - identifier: 产品标识符
/// - completion: 完成回调,返回产品信息或错误
func loadSingleProduct(with identifier: String, completion: @escaping (Swift.Result<SKProduct, Error>) -> Void) {
// 如果产品已经缓存,直接返回
if let cachedProduct = products[identifier] {
DDLogInfo("[\(TAG)] ✅ 从缓存获取产品: \(identifier)")
completion(.success(cachedProduct))
return
}
DDLogInfo("[\(TAG)] 🔄 开始加载单个产品: \(identifier)")
// 创建临时的完成回调
let originalCompletion = loadProductsCompletion
loadProductsCompletion = { result in
switch result {
case .success(let products):
// 查找目标产品
if let targetProduct = products.first(where: { $0.productIdentifier == identifier }) {
DDLogInfo("[\(self.TAG)] ✅ 单个产品加载成功: \(identifier)")
completion(.success(targetProduct))
} else {
DDLogError("[\(self.TAG)] ❌ 未找到指定产品: \(identifier)")
completion(.failure(SubscriptionError.productNotFound))
}
case .failure(let error):
DDLogError("[\(self.TAG)] ❌ 单个产品加载失败: \(identifier), 错误: \(error.localizedDescription)")
completion(.failure(error))
}
// 恢复原始回调
self.loadProductsCompletion = originalCompletion
}
// 发起请求(只请求指定的产品)
let productIdentifiers = Set([identifier])
productsRequest?.cancel()
productsRequest = SKProductsRequest(productIdentifiers: productIdentifiers)
productsRequest?.delegate = self
productsRequest?.start()
}
/// 获取指定产品
func getProduct(with identifier: String) -> SKProduct? {
return products[identifier]
}
/// 获取所有已加载的产品
func getAllProducts() -> [SKProduct] {
return Array(products.values)
}
-
调用示例
// 调用示例:
SubscriptionManager.shared.loadProducts { [weak self] result in
guard let self = self else { return }
// 隐藏加载状态
// DispatchQueue.main.async {
// self.hideLoadingIndicator()
// }
switch result {
case .success(let products):
print("✅ 成功加载 \(products.count) 个产品")
// 打印产品详情
for product in products {
print("产品ID: \(product.productIdentifier)")
print("产品名称: \(product.localizedTitle)")
print("产品价格: \(product.priceLocale.currencySymbol ?? "")\(product.price)")
print("产品描述: \(product.localizedDescription)")
print("---")
let isIntroductory = SubscriptionManager.shared.hasIntroductoryOffer(for: product.productIdentifier)
if isIntroductory {
let IntroductoryInfo = SubscriptionManager.shared.getIntroductoryOfferInfo(for: product.productIdentifier)
print("介绍性优惠信息:\(IntroductoryInfo ?? "")")
}
}
case .failure(let error):
print("❌ 加载产品失败: \(error.localizedDescription)")
DispatchQueue.main.async {
// 显示错误提示
let alert = UIAlertController(
title: "加载失败",
message: "无法加载订阅产品,请检查网络连接后重试",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "确定", style: .default))
self.present(alert, animated: true)
}
}
}
-
发起购买
/// 购买产品
func purchaseProduct(with identifier: String, completion: @escaping (PurchaseResult) -> Void) {
guard SKPaymentQueue.canMakePayments() else {
completion(.failure(SubscriptionError.cannotMakePayments))
return
}
guard let product = products[identifier] else {
completion(.failure(SubscriptionError.productNotFound))
return
}
purchaseCompletionHandlers[identifier] = completion
let payment = SKMutablePayment(product: product)
SKPaymentQueue.default().add(payment)
}
// SKPaymentTransactionObserver 代理方法
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .purchasing:
DDLogInfo("[\(TAG)] 🔄 正在购买: \(transaction.payment.productIdentifier)")
case .purchased:
DDLogInfo("[\(TAG)] ✅ 购买成功: \(transaction.payment.productIdentifier)")
handlePurchased(transaction)
case .failed:
DDLogInfo("[\(TAG)] ❌ 购买失败: \(transaction.payment.productIdentifier)")
handleFailed(transaction)
case .restored:
DDLogInfo("[\(TAG)] 🔄 恢复购买: \(transaction.payment.productIdentifier)")
handleRestored(transaction)
case .deferred:
DDLogInfo("[\(TAG)] ⏳ 购买延迟: \(transaction.payment.productIdentifier)")
handleDeferred(transaction)
@unknown default:
DDLogInfo("[\(TAG)] ❓ 未知交易状态: \(transaction.transactionState.rawValue)")
break
}
}
}
```
- ######**购买成功处理**
/// 购买成功处理
/// - Parameter transaction: SKPaymentTransaction
private func handlePurchased(_ transaction: SKPaymentTransaction) {
let productIdentifier = transaction.payment.productIdentifier
// 💰 到这里时,用户的钱已经被扣了
// 验证收据
validateReceipt(for: transaction) { [weak self] isValid in
DispatchQueue.main.async {
if isValid {
// 设置购买状态
self?.setPurchased(productIdentifier, purchased: true)
// 调用成功回调
self?.onPurchaseSuccess?(productIdentifier)
self?.completePurchase(for: productIdentifier, result: .success(transaction))
} else {
self?.onPurchaseFailure?(SubscriptionError.receiptValidationFailed)
self?.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.receiptValidationFailed))
}
// 完成交易,释放资源
SKPaymentQueue.default().finishTransaction(transaction)
}
}
}
```
-
购买失败处理
/// 购买失败处理
/// - Parameter transaction: SKPaymentTransaction
private func handleFailed(_ transaction: SKPaymentTransaction) {
let productIdentifier = transaction.payment.productIdentifier
let error = transaction.error
DispatchQueue.main.async {
if let skError = error as? SKError {
switch skError.code {
case .paymentCancelled:
// 用户主动取消购买 - 这是正常行为,不需要显示错误信息
DDLogInfo("[\(self.TAG)] 💡 用户取消购买: \(productIdentifier)")
self.completePurchase(for: productIdentifier, result: .cancelled)
case .paymentNotAllowed:
// 设备不允许内购 - 可能是家长控制限制或设备限制
DDLogInfo("[\(self.TAG)] 🚫 设备不允许内购: \(productIdentifier)")
self.onPurchaseFailure?(SubscriptionError.cannotMakePayments)
self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.cannotMakePayments))
case .paymentInvalid:
// 支付信息无效 - 产品ID错误或产品不存在
DDLogInfo("[\(self.TAG)] ❌ 支付信息无效: \(productIdentifier)")
self.onPurchaseFailure?(SubscriptionError.productNotFound)
self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.productNotFound))
case .cloudServiceNetworkConnectionFailed:
// iCloud服务网络连接失败 - 网络问题
DDLogInfo("[\(self.TAG)] 🌐 iCloud服务网络连接失败: \(productIdentifier)")
self.onPurchaseFailure?(SubscriptionError.networkError)
self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.networkError))
case .cloudServicePermissionDenied:
// iCloud服务权限被拒绝 - 用户未登录iCloud或权限问题
DDLogInfo("[\(self.TAG)] 🔒 iCloud服务权限被拒绝: \(productIdentifier)")
self.onPurchaseFailure?(SubscriptionError.cannotMakePayments)
self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.cannotMakePayments))
case .cloudServiceRevoked:
// iCloud服务被撤销 - 用户的iCloud账户可能被停用
DDLogInfo("[\(self.TAG)] ⚠️ iCloud服务被撤销: \(productIdentifier)")
self.onPurchaseFailure?(SubscriptionError.cannotMakePayments)
self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.cannotMakePayments))
case .privacyAcknowledgementRequired:
// 需要确认隐私协议 - 用户需要在App Store中确认隐私条款
DDLogInfo("[\(self.TAG)] 📋 需要确认隐私协议: \(productIdentifier)")
self.onPurchaseFailure?(SubscriptionError.cannotMakePayments)
self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.cannotMakePayments))
case .unauthorizedRequestData:
// 未授权的请求数据 - 请求包含无效数据
DDLogInfo("[\(self.TAG)] 🔐 未授权的请求数据: \(productIdentifier)")
self.onPurchaseFailure?(SubscriptionError.unknownError)
self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.unknownError))
case .invalidOfferIdentifier:
// 无效的优惠标识符 - 促销优惠ID错误
DDLogInfo("[\(self.TAG)] 🎟️ 无效的优惠标识符: \(productIdentifier)")
self.onPurchaseFailure?(SubscriptionError.productNotFound)
self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.productNotFound))
case .invalidSignature:
// 无效的签名 - 促销优惠签名验证失败
DDLogInfo("[\(self.TAG)] ✍️ 无效的签名: \(productIdentifier)")
self.onPurchaseFailure?(SubscriptionError.receiptValidationFailed)
self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.receiptValidationFailed))
case .missingOfferParams:
// 缺少优惠参数 - 促销优惠必需参数缺失
DDLogInfo("[\(self.TAG)] 📝 缺少优惠参数: \(productIdentifier)")
self.onPurchaseFailure?(SubscriptionError.productNotFound)
self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.productNotFound))
case .invalidOfferPrice:
// 无效的优惠价格 - 促销价格与服务器不匹配
DDLogInfo("[\(self.TAG)] 💰 无效的优惠价格: \(productIdentifier)")
self.onPurchaseFailure?(SubscriptionError.productNotFound)
self.completePurchase(for: productIdentifier, result: .failure(SubscriptionError.productNotFound))
default:
// 其他未知的SKError类型
DDLogInfo("[\(self.TAG)] ❓ 其他SKError类型: \(skError.code.rawValue), \(productIdentifier)")
self.onPurchaseFailure?(error ?? SubscriptionError.unknownError)
self.completePurchase(for: productIdentifier, result: .failure(error ?? SubscriptionError.unknownError))
}
} else {
// 非SKError类型的错误 - 可能是网络错误或其他系统错误
DDLogInfo("[\(self.TAG)] 🔥 非SKError类型错误: \(error?.localizedDescription ?? "未知错误"), \(productIdentifier)")
self.onPurchaseFailure?(error ?? SubscriptionError.unknownError)
self.completePurchase(for: productIdentifier, result: .failure(error ?? SubscriptionError.unknownError))
}
// 完成交易,释放资源
SKPaymentQueue.default().finishTransaction(transaction)
}
-
支付成功收据验证处理(到这里时,用户的钱已经被扣了)
private func validateReceipt(for transaction: SKPaymentTransaction, completion: @escaping (Bool) -> Void) {
guard let receiptURL = Bundle.main.appStoreReceiptURL else {
DDLogInfo("[\(TAG)] ⚠️ 无法获取收据URL")
completion(false)
return
}
guard let receiptData = try? Data(contentsOf: receiptURL) else {
// 如果收据不存在,尝试刷新(带超时处理)
refreshReceiptWithTimeout { success in
if success {
self.validateReceipt(for: transaction, completion: completion)
} else {
completion(false)
}
}
return
}
// 这里应该发送到你的服务器进行验证
// 示例:调用服务器验证API
validateReceiptWithServer(receiptData, transaction: transaction) { isValid in
completion(isValid)
}
}
/// 带超时处理的收据刷新
private func refreshReceiptWithTimeout(completion: @escaping (Bool) -> Void) {
DDLogInfo("[\(TAG)] 🔄 开始刷新收据...")
// 设置超时定时器(15秒)
receiptRefreshTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in
guard let self = self else { return }
DDLogWarn("[\(self.TAG)] ⏰ 收据刷新超时")
self.receiptRefreshCompletion?(false)
self.receiptRefreshCompletion = nil
self.receiptRefreshTimer?.invalidate()
self.receiptRefreshTimer = nil
}
// 保存完成回调
receiptRefreshCompletion = completion
// 开始刷新请求
let request = SKReceiptRefreshRequest()
request.delegate = self
request.start()
}
private func validateReceiptWithServer(_ receiptData: Data, transaction: SKPaymentTransaction, completion: @escaping (Bool) -> Void) {
// 这里是服务器验证的示例实现
// 实际开发中应该调用你的服务器API
let receiptString = receiptData.base64EncodedString(options: [])
// 验证编码结果
guard !receiptString.isEmpty,
Data(base64Encoded: receiptString) != nil else {
DDLogError("[\(TAG)] ❌ 收据base64编码失败")
completion(false)
return
}
#if DEBUG
let previewLength = min(30, receiptString.count)
let preview = String(receiptString.prefix(previewLength))
DDLogInfo("[\(TAG)] 📜 收据数据预览: \(preview)...")
#else
let previewLength = min(10, receiptString.count)
let preview = String(receiptString.prefix(previewLength))
DDLogInfo("[\(TAG)] 📜 收据数据预览: \(preview)...")
#endif
// TODO: 后面替换成调用服务器API校验
// 构建请求参数
let parameters: [String: Any] = [
"receipt-data": receiptString,
"password": "从App Store Connect获取", // 从App Store Connect获取
"exclude-old-transactions": true
]
DDLogInfo("[\(TAG)] 📋 验证参数:")
DDLogInfo("[\(TAG)] - 收据数据: \(receiptData.count) 字节")
DDLogInfo("[\(TAG)] - Base64长度: \(receiptString.count)")
DDLogInfo("[\(TAG)] - 交易ID: \(transaction.transactionIdentifier ?? "无")")
DDLogInfo("[\(TAG)] 📍 从交易收据transactionIdentifier: \(transaction.transactionIdentifier ?? "")")
validateWithAppleServer(parameters: parameters, isProduction: false, completion: completion)
}
/// 向Apple服务器发送验证请求
private func validateWithAppleServer(parameters: [String: Any], isProduction: Bool, completion: @escaping (Bool) -> Void) {
// 优先使用生产环境
validateWithSpecificServer(parameters: parameters, isProduction: true) { [weak self] success, status in
guard let self = self else {
DDLogError("[\("SubscriptionManager")] ❌ 已释放,无法处理请求")
completion(false)
return
}
// 如果生产环境验证成功,直接返回
if success {
completion(true)
return
}
// 如果返回状态码 21007(沙盒收据发送到生产环境),则尝试沙盒环境
if status == 21007 {
DDLogInfo("[\(self.TAG)] 🔄 检测到沙盒收据,切换到沙盒环境验证")
self.validateWithSpecificServer(parameters: parameters, isProduction: false) { success, _ in
completion(success)
}
} else {
// 其他错误直接返回失败
completion(false)
}
}
}
/// 向指定的Apple服务器发送验证请求
private func validateWithSpecificServer(parameters: [String: Any], isProduction: Bool, completion: @escaping (Bool, Int?) -> Void) {
let urlString = isProduction ?
"https://buy.itunes.apple.com/verifyReceipt" :
"https://sandbox.itunes.apple.com/verifyReceipt"
guard let verifyURL = URL(string: urlString) else {
DDLogError("[\(TAG)] ❌ 无效的验证URL: \(urlString)")
completion(false, nil)
return
}
var request = URLRequest(url: verifyURL)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.timeoutInterval = 15.0 // 设置超时时间
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
} catch {
DDLogError("[\(TAG)] ❌ 请求参数序列化失败: \(error)")
completion(false, nil)
return
}
let environment = isProduction ? "生产环境" : "沙盒环境"
DDLogInfo("[\(TAG)] 🌐 向Apple \(environment) 发送验证请求...")
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard let self = self else {
DDLogError("[\("SubscriptionManager")] ❌ 已释放,无法处理请求")
completion(false, nil)
return
}
// 检查网络错误
if let error = error {
DDLogError("[\(self.TAG)] ❌ \(environment) 网络请求失败: \(error.localizedDescription)")
completion(false, nil)
return
}
// 检查HTTP响应
if let httpResponse = response as? HTTPURLResponse {
DDLogInfo("[\(self.TAG)] 📡 \(environment) HTTP状态码: \(httpResponse.statusCode)")
if httpResponse.statusCode != 200 {
completion(false, nil)
return
}
}
// 解析响应数据
guard let data = data else {
DDLogError("[\(self.TAG)] ❌ \(environment) 响应数据为空")
completion(false, nil)
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
self.handleAppleVerificationResponseWithStatus(json, environment: environment, completion: completion)
} else {
DDLogError("[\(self.TAG)] ❌ \(environment) 响应格式无效")
completion(false, nil)
}
} catch {
DDLogError("[\(self.TAG)] ❌ \(environment) 响应解析失败: \(error)")
completion(false, nil)
}
}.resume()
}
/// 处理Apple验证响应(带状态码返回)
private func handleAppleVerificationResponseWithStatus(_ json: [String: Any], environment: String, completion: @escaping (Bool, Int?) -> Void) {
guard let status = json["status"] as? Int else {
DDLogError("[\(TAG)] ❌ \(environment) 响应中没有status字段")
completion(false, nil)
return
}
DDLogInfo("[\(TAG)] 📊 \(environment) 验证状态码: \(status)")
switch status {
case 0:
DDLogInfo("[\(TAG)] ✅ \(environment) 收据验证成功")
// 可选:解析详细的收据信息
if let receipt = json["receipt"] as? [String: Any] {
logReceiptDetails(receipt, environment: environment)
}
setPurchased(purchased: true)
completion(true, status)
case 21007:
DDLogWarn("[\(TAG)] ⚠️ \(environment) 这是沙盒收据但发送到了生产环境")
completion(false, status)
case 21008:
DDLogWarn("[\(TAG)] ⚠️ \(environment) 这是生产收据但发送到了沙盒环境")
completion(false, status)
case 21000:
DDLogError("[\(TAG)] ❌ \(environment) App Store无法读取提供的JSON对象")
completion(false, status)
case 21002:
DDLogError("[\(TAG)] ❌ \(environment) receipt-data属性中的数据格式错误或丢失")
completion(false, status)
case 21003:
DDLogError("[\(TAG)] ❌ \(environment) 收据无法验证")
completion(false, status)
case 21004:
DDLogError("[\(TAG)] ❌ \(environment) 提供的共享密钥与账户文件中的共享密钥不匹配")
completion(false, status)
case 21005:
DDLogError("[\(TAG)] ❌ \(environment) 收据服务器暂时无法提供收据")
completion(false, status)
case 21006:
DDLogError("[\(TAG)] ❌ \(environment) 此收据有效但订阅已过期")
completion(false, status)
case 21010:
DDLogError("[\(TAG)] ❌ \(environment) 此收据无法验证")
completion(false, status)
default:
DDLogError("[\(TAG)] ❌ \(environment) 未知验证状态码: \(status)")
completion(false, status)
}
}
/// 记录收据详细信息(用于调试)
private func logReceiptDetails(_ receipt: [String: Any], environment: String) {
DDLogInfo("[\(TAG)] 📋 \(environment) 收据详细信息:")
if let bundleId = receipt["bundle_id"] as? String {
DDLogInfo("[\(TAG)] - Bundle ID: \(bundleId)")
}
if let applicationVersion = receipt["application_version"] as? String {
DDLogInfo("[\(TAG)] - 应用版本: \(applicationVersion)")
}
if let receiptType = receipt["receipt_type"] as? String {
DDLogInfo("[\(TAG)] - 收据类型: \(receiptType)")
}
if let inApp = receipt["in_app"] as? [[String: Any]] {
DDLogInfo("[\(TAG)] - 内购交易数量: \(inApp.count)")
// 只记录最近的几个交易
for (index, transaction) in inApp.prefix(3).enumerated() {
if let productId = transaction["product_id"] as? String,
let transactionId = transaction["transaction_id"] as? String {
DDLogInfo("[\(TAG)] 交易\(index + 1): \(productId) (\(transactionId))")
}
}
}
}
-
password 从App Store Connect获取

截屏2025-07-22 14.58.19.png
-
支付收据凭证校验处理

截屏2025-07-22 14.32.58.png

截屏2025-07-22 14.33.09.png
-
服务端+客户端开发相关文档
StoreKit框架文档
https://developer.apple.com/documentation/storekit
App Store服务器通知
https://developer.apple.com/documentation/appstoreservernotifications
App Store服务器API
https://developer.apple.com/documentation/appstoreserverapi
收据字段参考
https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
订阅产品配置: https://www.jianshu.com/p/ba4ce240a085
银行账户添加: https://www.jianshu.com/p/b5b2e89210b5
Swift代码: https://www.jianshu.com/p/c26f62b1ac39