一笔购买完成,客户拿到了商品,但三天后一切又消失了。Google Play 会撤销这笔交易并收回权益,既不是客户主动要求的,也没有你的支持团队成员动手操作。这笔购买之所以悄无声息地失败,是因为你的服务器从未告知 Google 这笔订单已经处理完毕。
规则就是这么简单:必须在三天内确认(acknowledge)一笔 Google Play 购买,否则 Google 会自动将其退款并撤销。每次都是如此,没有例外。在两个应用商店所有可能导致订单被撤销的情形中,这是极少数完全取决于你自己集成方式的情形之一,这也使它成为最悄无声息地流失营收的方式之一。
这不是欺诈检测,也不是政策判定,而是一次从未触发的回调。修复起来并不复杂,但跳过它造成的损失却不小,因此有必要准确了解这条规则到底是怎么规定的、购买为何会漏掉它,以及每一笔消失的订单还会连带拖垮什么。
Google 文档究竟要求什么
Play Billing 要求你的应用在授予权益并告知用户购买已完成后通知 Google。Google 的文档给出了一个明确的数字:这项通知“必须在三天内完成,以免购买被自动退款并撤销权益”。一次性商品的文档毫不含糊地重复了同样的说法:跳过确认,“用户将自动获得退款,Google Play 会撤销该笔购买”。订阅的首次购买遵循完全相同的规则。
确认不是走流程的文书工作,而是 Google 判断商品是否真正交付的唯一信号。没有信号,Google 就会认为什么都没有交付,因此会在不征求你意见的情况下,为买家撤销整笔交易。买家看到的是一笔意外退款,你看到的则是一笔从账目上凭空消失的订单。
关键要点
让一笔订单保持 72 小时未经确认,撤销就会自动发生:买家被退款,权益被收回,整个过程无人过问。这不是任何购物者触发的,而是你的后端因为保持沉默而导致的,同样,你的后端也完全有能力阻止这件事。
进入 PURCHASED 状态才会启动倒计时,这意味着结账那一刻与你的截止期限并无关系。任何停留在 PENDING 状态的订单都没有附带计时器,此时也不该发出确认调用。
两种调用中的任何一种都能履行这项义务。对消耗型商品执行 purchases.products.consume,对非消耗型商品则使用 purchases.products.acknowledge 和 purchases.subscriptions.acknowledge。Google 会将每一种都视为商品已送达的证明。
订阅只会在注册时要求你做这件事一次。此后的每个计费周期都由 Google 自行处理,会被标记为已确认,无需你的代码做任何事。
如果销售的预付套餐时长不足七天呢?你的宽限期会缩减为套餐时长的百分之五十。商品周期越短,留给你的时间也越短。
退还给购物者的只是标价本身。计算资源、模型 API 开销、存储费用、创作者分成这些钱在履约过程中就已经从你的账户中支出,一分也不会回来。真实损失远超退款记录上显示的数字。
iOS 上没有任何机制以这种方式运作。StoreKit 会在每次启动时重新呈现未完成的交易,并耐心等待,从不会撤销扣款。这种特定的风险只存在于 Google Play,别无他处。
截止期限并非从结账那一刻开始计算
点击“购买”并不会启动计时器。购买状态必须先转变为 PURCHASED,而在此之前,有些购买会停留在 PENDING 状态。现金支付方式会出现这种情况,缓慢的银行转账和家长批准请求也是如此。Google 直接声明:“三天确认窗口只会在购买状态从 PENDING 转变为 PURCHASED 时才开始计算。”
这对你构建流程方式有两点意义。只有在状态显示为 PURCHASED 时才授予权益,绝不要在 PENDING 状态下授予,否则你可能会为一笔实际上可能收不到的款项发放商品。同样也不要确认一笔处于 PENDING 状态的购买,因为此时根本没有什么可确认的。在设置 BillingClient 时调用 enablePendingPurchases(),等待状态转变,只有在状态落到 PURCHASED 时才开始计时。
把 PENDING 理解为“尚未售出”。此时什么都不欠:不需要权益,不需要确认,也不应记为营收。一旦它转变为 PURCHASED,权益和三天倒计时都会立刻成为你需要处理的问题。
针对所售商品选择正确的 API 调用
有两条 API 路径可以解除这个截止期限,具体适用哪一条取决于你卖的是什么商品。两者在满足这项要求上是等效的,区别在于它们之后对商品还会做些什么。
消耗型商品要被消耗(consume)。也就是在安全的后端调用 purchases.products.consume,或在客户端通过 Play Billing Library 调用 consumeAsync()。消耗这个动作一举两得,既满足了确认要求,又让该商品重新变得可购买。对于金币、点数,或是客户可能反复购买的一次性生成项目来说,这正是你想要的效果。
非消耗型商品和订阅则需要被确认(acknowledge),方式是调用 purchases.products.acknowledge、purchases.subscriptions.acknowledge,或在客户端调用 acknowledgePurchase()。这样做可以解除截止期限,但不会重新开放该商品供购买。
以下是各商品类型对应的调用方式和截止期限:
对于消耗型商品,调用 purchases.products.consume 或 consumeAsync()。这同时也会让该商品重新变得可购买,截止期限是从 PURCHASED 起算的三天。
对于非消耗型商品,调用 purchases.products.acknowledge 或 acknowledgePurchase()。权益会被确认,但不可重复购买,截止期限是从 PURCHASED 起算的三天。
对于订阅的首次购买,调用 purchases.subscriptions.acknowledge 或 acknowledgePurchase()。这样可以确认新订阅,截止期限是从 PURCHASED 起算的三天。
对于订阅续订,你完全不需要任何调用。Google 会自行将其标记为已确认,因此不存在截止期限。
对于时长不足一周的预付套餐,使用相同的确认调用,但截止期限会缩短为套餐时长的一半。
这项义务只存在于首次付款
订阅续订根本不需要确认。Google 直接说明:“你不需要确认订阅续订”,并会自行将每一次续订标记为已确认。只有订阅的最初那笔购买会以待确认状态到达,并一直留在你这里,直到你完成确认。在操作之前,先在服务器端检查确认状态,或在客户端使用 isAcknowledged()。这样你就不会重复发送确认调用。
短期预付套餐的更紧迫时限
时长达到一周或更长的预付套餐遵循标准的三天规则,更短的套餐则不然。Google 的文档指出,时长“不足一周的套餐必须在套餐时长的一半之内完成确认”。一个三天的预付套餐只给你一天半的时间,而不是完整的窗口期。如果短期预付充值也是你商品目录的一部分,确认路径就必须快速且由服务器驱动,绝不能依赖客户重新打开应用。
确认调用通常在哪里丢失
没有人会故意跳过这一步,它之所以丢失,是因为确认逻辑被绑定到了错误的触发条件上。常见的反模式是:客户端只在购买流程返回前台时才执行确认。这能覆盖完成购买后立刻继续使用应用的客户,却覆盖不到其他任何人。
开发者不断遇到这个问题。Google 自己的开发者社区里的帖子每次都在重复同一个剧本:某种版本的用户在购买三天后被自动退款,或是请求解释为什么付款总是在第三天自动撤销。诊断结果几乎从不改变:确认调用被绑定到了一个从未发生的用户操作上,因此它根本就没有触发过。
免费试用,以及再也没有打开过应用的客户
最糟糕的情形是免费试用,或者是某人最后一次关闭你的应用之前的最后时刻完成的购买。如果确认要等下一次打开应用才会发生,而下一次打开应用永远不会到来,那笔购买就会自然到期失效。Google 会在第三天将其退款并撤销。对于本该顺利转化的试用来说,你失去的是从未成功收取的第一笔账单,一个客户的权益也随之悄然蒸发。这两件事都不会出现在你的支持工单队列里,但只要你去查,它们都会显示为一笔被撤销的购买。
依赖下一次打开应用才能完成的确认,意味着每当客户三天不打开应用,你就会失去这笔订单。而试用和一键购买恰恰正是客户最容易三天不回来的时刻。
一笔未确认退款背后的真实代价
退款记录低估了你实际的损失。Google 撤销这笔交易,你退还了价格,这就是每个人都能看到的数字,但这并不是全部的账单。
以某个在有人购买的瞬间就会启动实际工作的消耗型商品为例:一批图片生成、一连串对模型提供商 API 的调用、一次视频导出、一笔应付给创作者的分成。这些计算资源、API 调用、存储空间和分成款项在使用当时你就已经支付了。退款只是把售价还给了客户,却不会把供应商的账单退还给你。真实成本已经花出去了,没有任何东西能收回来。
对于订阅和试用来说,损失是你从未收到的第一笔账单,再加上一段还没真正开始就已经结束的客户关系。而且风险即将加大:从 2026 年 8 月 3 日起,Google Play 会将拒付(chargeback)的购买价格和银行手续费转嫁给开发者,适用于该日期之后产生的订单,这意味着任何可以避免的漏洞都值得现在就堵上,而不是以后再处理。未确认导致的退款并不是拒付,但它传达的是同一个道理:你已经花出去的钱,并不会自动变成你能留住的钱。
把确认环节构建成不可能被遗漏的步骤
可靠的模式会把你的应用完全排除在关键路径之外。应该在你的后端运行这一步,由通知驱动,而不是取决于是否有人重新打开某个页面。
订阅实时开发者通知(real-time developer notifications)。一次性商品购买事件,或订阅购买事件,会在 Google 记录这笔交易的瞬间就到达你的服务器,无论应用是否在运行。
通过 Play Developer API 验证购买令牌(purchase token),并确认状态显示为 PURCHASED 而不是 PENDING。
在你自己的系统中记录该权益,并与该客户的账户关联起来。
立即执行消耗或确认操作。消耗型商品执行消耗,非消耗型商品和订阅的首次购买执行确认。操作前先读取确认状态,确保调用不会被重复发出。
保留一个客户端回补机制。在 onResume() 中调用 queryPurchasesAsync(),可以捕获应用关闭期间已经完成的任何购买。把它当作保险措施,而不是主要路径。
整个关键在于,确认是由 Google 交给你的一个事件触发的,而不是你根本无法保证会发生的用户操作。即便有人只购买一次就再也没回来,也依然能被完全覆盖,因为你的服务器在购买到达的那一刻就已经完成了相应的工作。
为仍然可能漏网的情况设置一道保障
无论这条流水线构建得多么完善,底层仍然值得设置一道安全网。Google 的 Voided Purchases API 会列举所有以退款、撤销或拒付结束的订单,并专门标出那种原因是“开发者从未确认该购买,因此该购买可能不存在于开发者记录中”的具体场景。按固定节奏查询这个接口,你就能对任何被 Google 在你背后悄悄撤销订单的用户撤销访问权限。不过要留意边界:只能检索最近 30 天内的数据,这意味着这应该是一项定期调度的任务,而不是每个季度抽空处理一次的事情。
iOS 从来不会这样对你
换到 App Store,这一切都不适用。Apple 也设计了自己的收尾步骤,即完成一笔交易(finishing a transaction),但跳过它所带来的后果却是相反的。一笔未完成的交易会一直停留在 Apple 的队列中,每次应用启动时都会重新出现,每当观察者(observer)连接上时也会再次出现,给你反复交付客户已付款商品的机会。Apple 不会因此撤销任何交易。该平台上并不存在类似于 72 小时退款触发机制的东西。
这正是为什么一套心理模型无法同时套用在两个应用商店上。Android 给你的是一个倒计时,一旦错过,订单就会消失;iOS 给你的则是一个持续存在的提醒,以及无限的时间去应对它。把 iOS 的习惯带进 Android 的代码库,早晚你会面对一堆没人能解释清楚的被撤销订单。
弥补这个缺口正是 Refund Sensor 要做的事情。确认会直接在商店通知到达后的瞬间触发,同时一道针对 Voided Purchases API 的常态化对账流程,会在 Google 已经在你背后悄悄撤销订单的任何地方撤回访问权限。一个你可能会错过的倒计时,变成了一个在你想起来要检查之前就已经勾选好的选项。
这些规则的官方文档出处
常见问题
你的服务器没有及时发送确认调用。一旦购买进入 PURCHASED 状态后超过三天仍未被确认,Google 就会自动撤销这笔交易并收回权益。这不是客户做了什么,也不是 Google 施加的惩罚,而是缺少了一次确认调用。从商店通知在服务器端进行确认,可以彻底消除这个问题。
两者都能停止三天倒计时,但会让商品处于不同的状态。消耗(通过 purchases.products.consume 或 consumeAsync())适用于消耗型商品,它会解除截止期限,并让商品重新上架可供购买。确认(通过 purchases.products.acknowledge、purchases.subscriptions.acknowledge 或 acknowledgePurchase())适用于非消耗型商品和订阅,它会确认权益,但不会让商品重新开放购买。
不需要,只有首次购买需要。Google 不要求对续订进行确认,会自动将其标记为已确认。只有订阅的首次购买会以待确认状态到达,这才是你需要负责处理的,直到你完成确认为止。
不可以,要等到 PURCHASED 状态。像现金支付或家长批准请求这类处于 PENDING 状态的购买,根本还没有启动三天倒计时。只有当状态真正从 PENDING 转变为 PURCHASED 之后,才应该授予权益并发送确认。
没有,在你假设 Android 和 iOS 行为一致之前,这一点值得了解。一笔未完成的 StoreKit 交易只会在每次应用启动时被重新投递,直到你完成它为止,Apple 从不会自动退款。三天自动退款是 Google Play 独有的机制,因此这两个平台需要采用不同的处理方式。
退款本身无法撤销,但你可以做的是对账。Voided Purchases API 会列出过去 30 天内被退款和撤销的购买,并专门标记那些因为从未被确认而被撤销的购买,这样你就可以收回已经授予的权益。往后,从商店通知进行确认,才能防止下一笔订单再次漏网。



