资讯动态

用purchases-android替代Google Play Billing:订阅管理与收据验证实战

发布时间:2026/9/9 18:49:55 来源:尧图企业网站定制
简介Android 应用内购买与订阅开源框架面向需要接入 BillingClient 和 RevenueCat 后端的开发者可用于快速实现收据验证、订阅状态跟踪与分析统计适合有 Kotlin 基础的移动端工程师直接移植或二次开发。压缩包共 382 个文件约 772KB其中以 158 个 Kotlin 源码、72 个 XML 配置布局、20 个 ProGuard 混淆规则、19 个 Gradle 构建脚本及 Java、Properties、Markdown 文档为主结构完整便于按模块阅读与集成。目前已有 316 人学习下载。通过这套源码读者可以掌握采购框架的整体封装思路理解服务端到服务端的购买、续订、取消等事件同步机制并借助内置的多种集成把购买数据发送到所需平台省去从零搭建订阅管理系统的繁琐工作。1. 为什么我放弃了手写 Google Play Billing转向 purchases-android上个月接了个App改造的活儿需求很简单把原本的“买断制解锁”改成“订阅制”顺便把一直靠手工对账的支付状态梳理清楚。我跟大多数Android开发者的第一反应一样——直接用Google官方的BillingClient不就行了但真把需求拆完我发现事情没那么简单订阅的自动续费、宽限期、账户保留期、收据校验、以及“用户在A设备买了、B设备要能恢复”这一堆状态同步问题全堆到客户端上代码量直接爆炸。后来我把方案改成了基于purchases-androidRevenueCat 的 Android SDK来做整个过程顺了不少。这篇文章就把我这几周的实战经验整理出来包括这个SDK到底解决了什么问题、怎么快速接入、收据验证和状态跟踪的底层逻辑以及我在真实项目中踩过的坑。如果你是第一次接触Android应用内购买和订阅或者已经接入了 BillingClient 但被各种边缘状态搞到头大这篇应该能帮你省下不少时间。先说结论purchases-android本质上是把 Google Play Billing 封装成了“你只管卖状态和校验交给它”的模式。它在你的 App 和 Google Play 之间加了一层服务端帮你统一处理收据验证、订阅状态追踪、跨端同步这些脏活累活。你不再需要自己维护一番BASE_64_ENCODED_PUBLIC_KEY也不用自己在每个订阅事件里写一堆状态机判断。2. 核心功能拆解SDK到底帮你做了什么2.1 应用内购买与订阅的统一入口purchases-android在官方文档里的定位是“应用购买与订阅管理库”但我觉得更准确的说法是“购买逻辑的代理层”。它把 Google Play Billing 的BillingClient、PurchasesUpdatedListener、SkuDetails这些底层对象全部封装起来对外暴露的是一套更简单的购买流程。举个例子用原生 BillingClient 买一个订阅你需要经历至少五个步骤初始化BillingClient并处理连接状态回调查询SkuDetails拿到商品信息调launchBillingFlow发起购买在onPurchasesUpdated里处理购买结果调consumeAsync或acknowledgePurchase确认消费。如果其中有一步连接失败了你还得自己处理重试。而用purchases-android核心流程就变成了// 初始化在 Application 中 Purchases.configure( PurchasesConfiguration.Builder(context, your_public_sdk_key).build() ) // 发起购买 Purchases.sharedInstance.purchaseWith( PurchaseParams.Builder(activity, offer) .build(), onResult { result - when (result) { is PurchaseResult.Success - { // 购买成功直接在回调里拿到最新的 CustomerInfo用户全部订阅状态 val entitlement result.customerInfo.entitlements[pro] } is PurchaseResult.Error - { // 处理错误 } is PurchaseResult.Cancelled - { // 用户取消 } } } )注意看那个CustomerInfo它包含了这个用户在所有设备上的订阅状态。这意味着你不需要自己在本地数据库里维护“用户是否已订阅”也不需要自己去处理多设备恢复购买的逻辑。用户在另一台手机上重新登录拉一次CustomerInfo就全有了。2.2 “收据验证”到底验证的是什么做过内购的人都知道客户端拿到的Purchase对象里有个purchaseTokenGoogle 要求开发者拿这个 token 去调 Play Developer API 的purchases.subscriptions.get接口才能真正确认这笔交易有效、并且拿到订阅的到期时间。这一步在官方文档里叫收据验证Receipt Validation很多国内团队直接把purchaseToken丢给自己的后端让后端去调 Google 接口逻辑看起来没毛病但有几个隐患你的后端要维护一套完整的 Google OAuth 2.0 认证流程token 过期了要刷新订阅状态不是“查询一次就结束”而是要在用户每次启动App、每次恢复购买、以及 Google Play 每次发来“订阅续费成功”通知时都去查一遍还要处理退款、撤销、暂停订阅等异常状态。purchases-android的解决方案是把这一步直接做了App 端发起购买后SDK 会把收据上传到 RevenueCat 的服务器由他们的服务器去跟 Google Play 验证然后把结构化好的状态推到客户端。客户端看到的是一个EntitlementInfo权益信息里面直接标好了isActive是否生效、expirationTime过期时间、willRenew是否会续费这些字段你直接拿来判断“这个用户有没有会员”就行。2.3 状态跟踪从“死数据”到“活状态”我觉得这个SDK最值钱的地方在状态跟踪。原生 BillingClient 里queryPurchases返回的是一堆历史购买记录你要自己根据purchaseState、acknowledged、autoRenewing等字段判断当前到底处于什么状态。但订阅是一个跨时间维度的东西有大量边缘状态用户首次购买3天免费试用期试用期结束自动转为付费订阅用户不想要了在 Google Play 设置里取消了自动续费但当前周期内还能继续用用户绑定的信用卡余额不足Google 进入宽限期grace period可能续费成功也可能失败用户申请退款并成功订阅被撤销。以上每一种状态在原生 API 里你都得自己用queryPurchasesAsync去拉然后写 if-else 判断。而在purchases-android里这些状态最终都会映射到一个EntitlementInfo上你只需要关心几个关键布尔值。业务场景原生 BillingClient 需要的手工处理purchases-android 中的状态用户购买订阅监听 onPurchasesUpdated判断 purchaseState直接拿 CustomerInfo.entitlements自动续费依赖后端定时查 Play Developer API服务端主动 webhook 推送后同步用户取消自动续费需要 queryPurchases 检查 autoRenewingisActivetruewillRenewfalse宽限期需要自己解析续费通知里的续费类型isActivetrueisInGracePeriodtrue退款/撤销后端定期查询或监听退款推送isActivefalse自动同步到客户端跨设备恢复自己写恢复购买逻辑Purchases.restorePurchases() 一行搞定这种“状态统一映射”的思路避免了开发者陷入 Google Play 各种细微字段的泥沼。订阅这套东西状态字段之间互相影响尤其新手很容易在purchaseState为PURCHASED但acknowledge还没做的时候误以为购买流程没走完白白拦截用户。3. 实操从零接入 purchases-android 的完整步骤3.1 环境准备与依赖配置接入前最好确认你的项目使用的是 Android Studio 较新版本我用的是 Android Studio Hedgehog 之后的版本Gradle 版本 8.x并且 App 的目标版本不低于 Android 5.0API 21SDK 本身对旧的 Android 版本兼容性不错但 Google Play Billing 库现在强制要求用较新的编译版本。在项目级build.gradle里加 Maven 仓库这个一般默认就有然后在模块级build.gradle的 dependencies 里添加dependencies { implementation com.revenuecat.purchases:purchases-android:8.5.1 }注意版本号新版 SDK 把最低 API 级别提到了 21如果你的 App 还在坚持 minSdk 19就需要降到 7.x 版本。依赖加完后同步一下确认没有 Billing 版本冲突即可。3.2 初始化与用户识别初始化需要在 Application 里完成直接把你的 RevenueCat 公共密钥传进去class MyApp : Application() { override fun onCreate() { super.onCreate() Purchases.configure( PurchasesConfiguration.Builder(this, your_public_sdk_key).build() ) // 如果有登录系统在用户登录后调用标识 Purchases.sharedInstance.logIn(userId) { customerInfo, error - // 登录成功后会返回最新的订阅状态 } } }这里有个关键点logIn方法。如果你的 App 有账号体系一定要在用户登录后调用它把 RevenueCat 的匿名用户跟你自己的用户 ID 关联起来。否则用户换设备后用新生成的匿名 ID 恢复购买大概率恢复不到之前的订阅。退了登录就调logOut防止账号串号。3.3 商品配置与购买RevenueCat 控制台里要创建对应的产品Product名字随意但必须绑定 Google Play Console 里建好的商品 ID。代码里通过queryProductDetailsAsync拉取商品Purchases.sharedInstance.queryProductDetailsAsync( listOf(pro_monthly, pro_yearly) ) { productDetails, error - productDetails?.forEach { product - // 展示价格、标题、描述等信息 } }然后把用户选中的商品传给purchaseWith注意传入的activity是当前要在其上弹出 Google 支付对话框的 Activity。购买完成后服务器验证是需要时间的通常几秒内就能拿到结果但极端情况下可能要等更久所以 SDK 也提供了PurchaseResult.Success回调里的customerInfo来刷新界面同时还可以通过后续的customerInfo监听来最终确认。3.4 状态监听与权益判断购买之后最重要的就是正确判断“用户有没有权益”。我见过不少新手直接拿productId存在本地当“会员标记”这种方案在纯买断制里还凑合在订阅制里几乎是必出问题——因为用户可能在 Google Play 那边退订、退款而你本地存了一个“曾经买过”的标记就会误放行。purchases-android的标准做法是通过CustomerInfo里的entitlements来判断。Entitlement 可以理解成“你的 App 里定义的一种权益凭证”一个产品可以对应多个 entitlement一个 entitlement 也可以由多个产品触发。意思就是我可以定义一个premium权益让月卡、年卡、终身买断都能激活它这样客户端只要判断premium有没有效不用关心用户具体买的是哪个商品。Purchases.sharedInstance.getCustomerInfo { customerInfo, error - val isPremium customerInfo?.entitlements?.get(premium)?.isActive true if (isPremium) { // 显示会员界面 } else { // 显示付费引导 } }在需要实时刷新的场景下比如用户切回前台应该有最新状态可以监听Purchases.PurchasesListenerPurchases.sharedInstance.listeners.add(object : UpdatedCustomerInfoListener { override fun onCustomerInfoUpdated(customerInfo: CustomerInfo) { // 这里会在购买成功、续费状态变化、退款等场景被触发 } })4. 收据验证与订阅状态检查的底层逻辑4.1 为什么要依赖服务端验证而不是本地信任先明确一个概念Google Play Billing 收据验证真正安全可靠的方式是服务端验证。客户端即使拿到了purchaseToken也完全可以直接决定“我信了”然后给用户放行——但这就等于把一个商业系统的安全边界放到了别人能任意修改的客户端里静态分析、逆向、改机工具都能轻松绕过。purchases-android的思路是客户端只负责展示和发起购买最终验证由 RevenueCat 的服务端完成再把结果下发。这也是为什么初始化的时候用的是public_sdk_key它是可以暴露在客户端里的。真正有权限去 Google Play 拉取验证结果的 SECRET key永远只存在于 RevenueCat 的服务器。客户端那层就算被逆向拿到的也只是一个对攻击者无用的公钥。4.2 服务端如何与 SDK 协作很多团队担心“是不是用了 purchases-android所有逻辑都被绑架在它家了”。其实不然。这个 SDK 提供了一个可选的Purchases.attribution功能以及一个后端 APIRevenueCat REST API你可以把用户 ID、app user ID 同步到自己的后端用自己的业务服务器去调 RevenueCat 的 API 核对订阅状态。结构上看是这样App 端发购买请求收 CustomerInfo展示商品RevenueCat 服务端接收收据调 Google Play Developer API 做验证存储订阅状态下发结果自己的后端用userId去 RevenueCat API 查询subscription状态比如查询/subscribers/{app_user_id}/entitlements用于封禁/放行自己的业务资源。也就是说这层“验证代理”变成一个可信的中间件你的后端不需要直接跟 Google 对接省去了 OAuth 凭据管理的环节同时业务逻辑完全可以把 RevenueCat 当做数据源来用。4.3 订阅过期时间与续费状态在EntitlementInfo里最有用的几个字段是expirationTime订阅过期时间如果为空表示权益没有到期时间比如按 AI 功能次数或一次买断的权益isActive当前是否生效willRenew是否会自动续费productIdentifier用户购买的具体商品。实际操作中“过期时间 是否自动续费”是判断用户能否继续使用某种云端资源的关键。比如用户订阅了一个月的会员到期时间是月底但他今天在 Google Play 管理页面取消了自动续费。当前周期内他还是会员isActive true但下个月就不一定了willRenew false。很多开发者的错误是看到“还在有效期内”就认为用户一直有权益等用户下个月继续用的时候才傻眼——服务器早就该通过状态感知用户“即将退订”做挽留策略或者限制一些超额资源的使用。用一份简单的判断代码来总结fun checkPremium(customerInfo: CustomerInfo): Boolean { val premium customerInfo.entitlements[premium] ?: return false if (!premium.isActive) return false // 如果用户已经取消自动续费但还在有效期内可以做精细化处理 if (!premium.willRenew premium.expirationTime ! null) { // 业务上可给“即将到期用户”特殊提示或引导续费 } return true }这里有一个容易踩的坑有些订阅是“预付费时长包”prepaid它们同样有expirationTime但没有willRenew字段的自动续费概念。对于这类商品意志上不要拿willRenewfalse去判断“用户取消续费”因为在 Google Play 的模型中预付时长包本身就是不自动续费的你应当先把订阅分组区分开RevenueCat 控制台里可以按产品归属分组。5. 常见问题排查与经验避坑5.1 常见问题速查症状可能原因解决方案购买回调迟迟不返回网络异常或 Google Play 服务未更新检查网络确认设备有 Google Play 服务等待 30 秒后重新获取点了商品没弹窗商品未在控制台激活 / 未审核通过RevenueCat Console 和 Google Play Console 两边确认产品状态测试用户购买时提示“商品不可用”测试账号未添加到许可测试人员或商品未在制品版本中Google Play Console 里添加测试用户与许可测试账号恢复购买后没权限未调 restorePurchases 或调用了 logOut登录过的 appUserId 要一致再调 restorePurchases订阅在测试环境看到自动续费失败沙箱环境本身不会自动扣款测试订阅续费需要用 Google Play 的测试卡/测试时间调整机制建议只看状态流转CustomerInfo 拿不到最新状态SDK 缓存导致手动调 getCustomerInfo 或使用 getCustomerInfoFetchPolicy 为 FETCH_CURRENT买断产品一直显示“待确认”未对购买作 acknowledgementSDK 已自动处理但自制购买流程需确认5.2 我在接入中踩过的具体坑第一个坑是queryProductDetailsAsync的商品 ID 必须和 RevenueCat 控制台里配置的产品标识完全一致。我在早期试验的时候音位手滑在代码里写的是pro_monthly控制台里却是pro_monthly_v2结果列表一直为空排查了整整一下午。第二个坑是 Android 的onResume里不适合直接去刷新订阅状态。Google Play 支付是拉起一个系统弹窗会暂时让当前 Activity 进入onPause如果这时候你去调getCustomerInfo很可能会在弹窗还没关闭时拿到一个旧的缓存状态。正确做法是用addUpdatedCustomerInfoListener监听或者等purchase的回调返回后再刷新。第三个坑是权益判断要放在后端不要只信客户端。purchases-android虽然能正确返回isActive但客户端滞后于服务端是很正常的事。比如用户在 Google Play 网页端申请退款服务端的 webhook 可能会在下一秒就把订阅状态改成失效而客户端该用户如果长时间不打开 App他还是能靠本地缓存的CustomerInfo访问你的资源。真正的防线是你的后端服务器在处理 API 请求时拿userId去 RevenueCat API 查一次实时状态再做业务放行。这样客户端更像是一层有缓存的UI而业务逻辑的安全性由服务端兜底。第四个坑是副屏设备和平板。部分国产安卓平板阉割了 Google Play 服务或者商店应用导致BillingClient无法初始化purchases-android虽然会抛错告诉你市场不可用但你最好在启动时做一个环境判断至少弹出一个“当前设备不支持在应用内购买”的提示而不是让用户一脸懵地点击付费按钮没反应。5.3 如何设计一套可观测的订阅状态最后分享一个我在生产环境用的技巧不要在客户端的请求链路里直接依赖CustomerInfo来决定所有业务权限尤其是那些和资源强相关的操作。我会把 CustomerInfo 的 payload 脱敏后上报到自己的数据仓库里存app_user_id、product_id、is_active、expiration_time这样一个宽表然后定时任务从 RevenueCat API 同步全量订阅状态来做对账。这套机制帮我发现过一次真实的事故某个老版本 App 里客户端用本地缓存判断是否解锁高级功能但缓存没做失效时间用户退了订阅后一年多还能继续白嫖高级服务。换成服务端实时查询后这个问题消失得干干净净。6. 一点个人总结做了这么多年的 Android 付费功能最深的一个体会是应用内购和订阅的难点不在“拉起支付”而在支付之后那一大堆状态的同步与信任边界。purchases-android的价值不是帮你省掉所有工作而是替你处理了最容易出错的 Google Play 收据验证和订阅状态映射让你可以把精力放在真正的业务权益设计上。如果你正在规划一个订阅制的 Android App我建议别一上来就怼 BillingClient 写了三百行“自研收据验证”先认真过一遍purchases-android的初始化、商品配置、权益判断三步再用 RevenueCat 自带的服务器验证兜底。跑通一个最小可用的订阅流程通常比从零搭一座轮子要稳妥得多。另外调试时强烈建议在 RevenueCat Console 打开 “Sandbox Testing” 模式并用测试账号做购买。Google Play 沙箱环境里订阅的到期时间可以手动切成几分钟方便你快速验证“订阅过期后是否还能继续用高级服务”这类场景。实测下来这套组合拳能覆盖大多数订阅业务的核心流程至少目前我还没遇到必须“返回去手写 BillingClient”才能搞定的需求。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价