iOS应用内购开发疑问:如何判断用户是否已购买指定内购项目?
确认用户内购项目购买状态的可行方案
嘿,这个需求在做内购功能时太常见了!我来给你梳理几个iOS和Android平台都适用的成熟实现方案,确保你能准确判断用户是否已购买非消耗型或自动续期订阅项目:
iOS(App Store)平台方案
1. 利用StoreKit直接查询交易状态
- 如果你用的是StoreKit 2(iOS 15+),可以通过异步API直接获取用户的交易记录,代码非常简洁:
import StoreKit func checkPurchasedStatus() async { do { // 获取所有关联的交易记录 let transactions = try await Transaction.all() for transaction in transactions { guard let productID = transaction.productID else { continue } switch transaction.transactionState { case .purchased, .restored: // 标记该产品为已购买(可以存到UserDefaults或本地数据库) UserDefaults.standard.set(true, forKey: "Purchased_\(productID)") case .expired, .revoked: // 订阅过期或被退款,标记为未购买 UserDefaults.standard.set(false, forKey: "Purchased_\(productID)") default: break } } } catch { print("查询购买状态出错:\(error.localizedDescription)") } } - 对于iOS 14及以下版本,可以用
SKReceiptRefreshRequest刷新凭证后,解析本地凭证文件来判断购买状态。
2. 服务器端验证(必做,提升安全性)
本地验证容易被篡改,所以强烈建议把设备上的交易凭证发送到你的后端服务器,后端调用App Store官方验证接口,验证通过后再把结果返回给客户端。这样能有效防止破解和伪造购买状态。
3. 处理订阅的特殊状态
自动续期订阅需要额外检查:
- 用StoreKit 2的
SubscriptionStatus可以直接获取订阅的当前状态(活跃、过期、待确认等) - 定期刷新订阅状态,因为用户可能关闭自动续订、更换订阅方案或申请退款
Android(Google Play)平台方案
1. 使用Google Play Billing Library查询购买记录
借助Billing Library(推荐v5+),可以异步查询用户已购买的非消耗型产品和订阅:
import com.android.billingclient.api.* fun checkPurchasedProducts(context: Context) { val billingClient = BillingClient.newBuilder(context) .setListener { billingResult, purchases -> // 实时处理购买更新事件 purchases?.let { updatePurchasedStatus(it) } } .enablePendingPurchases() .build() billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(result: BillingResult) { if (result.responseCode == BillingClient.BillingResponseCode.OK) { // 查询非消耗型产品 val inAppParams = QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.INAPP) .build() billingClient.queryPurchasesAsync(inAppParams) { _, purchases -> updatePurchasedStatus(purchases) } // 查询订阅产品 val subParams = QueryPurchasesParams.newBuilder() .setProductType(BillingClient.ProductType.SUBS) .build() billingClient.queryPurchasesAsync(subParams) { _, purchases -> purchases.forEach { purchase -> // 检查订阅是否处于活跃状态 val isActive = purchase.isAutoRenewing || purchase.purchaseState == Purchase.PurchaseState.PURCHASED androidx.preference.PreferenceManager.getDefaultSharedPreferences(context) .edit() .putBoolean("Purchased_\(purchase.products.first())", isActive) .apply() } } } } override fun onBillingServiceDisconnected() { // 断开连接后尝试重连 billingClient.startConnection(this) } }) } private fun updatePurchasedStatus(purchases: List<Purchase>) { purchases.forEach { purchase -> androidx.preference.PreferenceManager.getDefaultSharedPreferences(purchases.first().purchaseToken) .edit() .putBoolean("Purchased_\(purchase.products.first())", true) .apply() } }
2. 服务器端验证
同样,不要只依赖客户端结果,后端调用Google Play Developer API的purchases.products.get或purchases.subscriptions.get接口验证购买凭证,确保状态真实可靠。
通用注意事项
- 进入内购页面时强制刷新状态:不要依赖上次的本地缓存,因为用户可能在其他设备购买、退款或变更订阅。
- 监听退款和状态变更:iOS可以通过App Store服务器通知接收退款、订阅过期等事件;Android可以通过BillingClient的监听接口或Play Developer API的推送通知。
- 本地缓存仅作优化:本地缓存是为了提升页面加载速度,但每次进入页面都要触发一次远程查询或凭证验证,避免状态不一致。
内容的提问来源于stack exchange,提问作者Kriti Sharma
相关产品推荐
相关产品推荐

