所有文章
Deep dive阅读需 8 分钟

三天内不确认 Google Play 购买,Google 就会退款,这会让你付出什么代价

只要你的服务器在三天内没有确认某笔购买,Google Play 就会自动退款并撤销这笔购买。这是集成失误,不是客户的决定,而且完全可以避免。下面讲清楚具体规则、它为什么会触发,以及每一笔流失的销售真正的代价。

一个沙漏在智能手机旁流尽,说明在 Google Play 购买被自动退款之前,你只有三天时间去确认它

要点

  • 如果你的应用在三天内没有确认购买,Google Play 会自动向买家退款并撤销这笔购买。这是集成失误,不是客户的决定,而且完全可以在服务器端避免。
  • 三天倒计时从购买状态变为 PURCHASED 时开始,而不是从结账开始时。停留在 PENDING 的购买还没有启动倒计时,此时绝不能去确认它。
  • 两种调用都能满足要求。通过 purchases.products.consume 消耗一件消耗型商品,以及通过 purchases.products.acknowledge 或 purchases.subscriptions.acknowledge 确认一件非消耗型商品或订阅,两者都算作确认。
  • 只有首次订阅购买需要确认。续订不需要,Google 会自动将它们标记为 ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED。
  • 时长短于一周的预付费套餐,必须在套餐时长一半的时间内确认,这个期限比标准的三天更紧。
  • 退款追回的是销售价格,但你在交付产品时已经花掉的算力、模型 API 调用、存储和创作者分成并不会退还给你。你损失的钱比账面上那一项要多。
  • Apple 没有对应机制。一笔未完成的 StoreKit 交易会被反复重新投递,直到你完成它,但 Apple 绝不会自动退款。这种故障模式只存在于 Google Play。

一位客户购买了你的产品,扣款成功,三天后 Google Play 悄悄退了款,并收回了你已经交付的东西。没有人要求这笔退款。客户没有申请,也没有任何客服人员批准过。它之所以触发,是因为你的服务器从未告诉 Google 这笔购买已被处理。如果你没有在三天内确认一笔 Google Play 购买,Google 每次都会向买家退款并撤销购买。这是两大商店里为数不多、完全由你的集成来预防的退款之一,也是最悄无声息的营收流失方式之一。

这不是欺诈问题,也不是政策争议。它只是一个漏掉的回调。修复很小,而跳过它的代价是真金白银,所以值得把规则弄清楚:它到底是什么,购买为什么会在未确认的情况下溜走,以及每一笔流失的销售实际上带走了什么。

三天规则到底说了什么

Google 的 Play Billing 文档对此毫不含糊。在你的应用授予权益并告诉用户购买成功之后,它必须通知 Google 这笔购买已被处理。用 Google 的原话说,这"必须在三天内完成,否则购买将被自动退款、权益被撤销"。一次性商品页面重复了同样的说法,没有任何缓和:"如果你没有在三天内确认购买,用户会自动收到退款,Google Play 会撤销该购买。"订阅对首次购买适用完全相同的规则。

确认是一个信号,不是走过场。它告诉 Google 权益已经到达用户手中。Google 把这个信号的缺失视为一次从未发生过的交付,并代客户撤销这笔交易。从买家的角度看,这像是一笔他们从未申请过的免费退款。从你的角度看,这像是一笔凭空蒸发的销售。

倒计时从 PURCHASED 开始,不是从结账开始

三天窗口不是从用户点下购买时开始的。它从购买状态转变为 PURCHASED 时开始。购买可能会先停留在 PENDING,现金支付、缓慢的银行转账,或家长批准孩子的请求都会出现这种情况。Google 说得很明确:"三天确认窗口只有在购买状态从 PENDING 转变为 PURCHASED 时才开始。"

这有两个后果。只在状态为 PURCHASED 时授予权益,绝不在 PENDING 时授予,否则你就是在为一笔可能永远不会完成的付款发放产品。同样,也不要去确认一笔 PENDING 的购买。你在构建 BillingClient 时调用 enablePendingPurchases(),等待状态转变,然后才在心里开始计算确认倒计时。

确认还是消耗,你该做哪一个

满足要求有两种方式,用哪一种取决于产品类型。两者都能赶上三天期限。区别在于它们还会做什么。

对于消耗型商品,你要消耗它。在安全的后端上这是 purchases.products.consume,或在 Play Billing Library 里客户端的 consumeAsync()。消耗既确认了购买,又让产品重新可购买,这正是你想要的效果,适用于金币、点数或一次性生成。对于非消耗型商品或订阅,你要确认它:后端上用 purchases.products.acknowledge 或 purchases.subscriptions.acknowledge,或客户端的 acknowledgePurchase()。确认赶上期限,但不会让产品重新可供再次购买。

购买类型满足期限的调用它还会做什么期限
消耗型商品purchases.products.consume 或 consumeAsync()同时让产品可再次购买从 PURCHASED 起 3 天
非消耗型商品purchases.products.acknowledge 或 acknowledgePurchase()标记权益已授予,不可再次购买从 PURCHASED 起 3 天
订阅,首次购买purchases.subscriptions.acknowledge 或 acknowledgePurchase()确认新订阅从 PURCHASED 起 3 天
订阅续订无需任何操作由 Google 自动标记为已确认不适用
时长不足一周的预付费套餐按上述方式确认确认权益套餐时长的一半

续订已经处理好了,首次购买没有

你只需对订阅的首次购买负责确认。Google 明确表示"你不需要确认订阅续订",并且它会自行把续订标记为 ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED。一笔新购买以 ACKNOWLEDGEMENT_STATE_PENDING 到达,在你处理掉它之前一直是你的责任。在确认之前,先在后端检查 acknowledgementState,或在客户端检查 isAcknowledged(),以免重复确认。

预付费套餐的引信更短

预付费订阅套餐收紧了窗口。Google 的规则是:时长一周或更长的预付费套餐必须在三天内确认,但"时长短于一周的预付费套餐必须在套餐时长一半的时间内确认"。一个三天的预付费套餐只给你一天半,而不是三天。如果你销售短时的预付费充值,你的确认路径必须快速且由服务器驱动,不能依赖用户重新打开应用。

购买一开始为什么会没被确认

没有人是故意跳过确认的。它之所以溜走,是因为负责确认的代码放错了地方。常见的反模式是:客户端只在购买流程回到前台时才确认。这对一个完成购买、并继续使用应用的用户是有效的。但对其他所有人都失效。

开发者不断遇到这个问题。Google 自己的开发者社区里的帖子每次读起来都一个样,都是"某个用户在我的应用里购买后三天被自动退款了"和"为什么付款在三天后被自动退款"这类说法的变体。答案几乎总是一样:确认调用从未触发,因为应用从来没有处于能触发它的状态。

免费试用和那个再也不回来的用户

最糟糕的情形是免费试用,或者用户在彻底关闭应用之前的一笔购买。如果你的确认依赖于下一次打开应用,而根本没有下一次打开,这笔购买就会过期作废。到了第三天 Google 就退款并撤销它。对于一个本会转为付费订阅的试用,你损失的是那笔你从未收到的首期账单,外加一份悄悄消失的客户权益。这两件事都不会以工单的形式出现。它们表现为一笔你得主动去翻找才能发现的作废购买。

一笔未确认退款实际的代价

退款那一行低估了损失。当 Google 撤销这笔销售时,你退回了价格,那是看得见的数字。它并不是全部账单。

想想一件在购买瞬间就触发真实工作的消耗型商品。一批图像生成、一连串对模型提供商 API 的调用、一次视频导出、一笔给创作者的分成。你在使用时就已经为那些算力、那些 API 调用、那些存储和那些分成付了钱。退款把销售价格退给了客户。它不会把供应商的账单退给你。你交付了一笔真实的成本,却什么也没换回来。

对于订阅和试用,损失是那笔你从未收到的首期账单,以及一段还没开始就结束了的客户关系。而且从 2026 年 8 月 3 日起,Google Play 会把在该日期之后下单的订单的退单购买价格和银行手续费转嫁给开发者,这让任何可避免的营收流失都值得现在就堵上,而不是拖到以后。未确认退款不是退单,但它是同一个教训:你已经花掉的钱,不会自动变成你留住的钱。

一张折叠的收据和一些硬币被拉回到一张深色桌子的另一端,说明当一笔未确认的 Google Play 购买被退款并撤销时,你已经花掉的钱

如何在服务器端确认一笔 Google Play 购买

可靠的做法是把应用从关键路径上移除。在你的后端上完成它,由通知驱动,而不是由用户重新打开界面驱动。

  • 监听 Real-time developer notifications。一个 ONE_TIME_PRODUCT 购买或一个 SUBSCRIPTION_PURCHASED 事件,会在 Google 一知道的瞬间就告诉你的服务器有一笔购买存在,无论应用是否打开。
  • 用 Play Developer API 校验购买令牌,并确认状态是 PURCHASED,不是 PENDING。
  • 在你自己的记录里授予权益,以用户为键。
  • 立即确认或消耗。消耗型商品用消耗,非消耗型商品和首次订阅用确认。先检查 acknowledgementState,这样你就绝不会确认两次。
  • 在客户端也做补漏。在 onResume() 里调用 queryPurchasesAsync(),让任何在应用关闭期间完成的购买仍然得到处理。这是一张安全网,不是主路径。

关键在于,确认是由 Google 发给你的事件触发的,而不是由你无法指望的用户操作触发的。一个购买后再也不回来的用户被完全覆盖了,因为你的服务器在购买落地的那一刻就采取了行动。

给漏网之鱼准备的对账网

即便是干净的管道也能从一次核对中获益。Voided Purchases API 会列出被退款、撤销或退单的购买,并点名那种原因是购买"从未被开发者确认,因此可能不存在于开发者记录中"的情形。轮询它,你就能撤销你为任何 Google 已经撤销的东西所授予的权益。注意限制:该 API 只返回过去 30 天内的作废购买,所以对账必须按计划定期运行,不能一个季度才跑一次。

Apple 没有对应机制,这很重要

这是一个专属于 Google Play 的问题。Apple 的 StoreKit 也有一个收尾步骤,即完成一笔交易,但它在失败时做的是相反的事。如果你从不完成一笔 StoreKit 交易,Apple 会把它保留在队列里,并在每次你的应用启动或观察者附加时重新投递它,这样你就又有一次机会去授予权益。Apple 不会为一笔未完成的交易退款。App Store 上没有三天自动退款。

所以心智模型必须保持平台特定。在 Google Play 上,一笔未处理的购买是一场随时会发生的退款,是一个你正在追赶的期限。在 App Store 上,一笔未处理的购买是一次随时会发生的重新投递,根本没有倒计时。把 Apple 的假设照搬到 Android,正是团队最终堆起一墙无法解释的未确认退款的原因。

这正是为什么 RefundHalt 会在商店通知一到达的那一刻就自动确认 Google Play 购买,并对照 Voided Purchases API 进行对账,好让为一笔 Google 后来撤销的购买所授予的权益不会继续生效。三天规则不再是一场你可能会输的比赛,而变成一个早已完成的步骤。

常见问题解答

为什么我的 Google Play 购买在三天后被自动退款了?
因为你的应用没有及时确认它。Google Play 会自动向买家退款并撤销任何在到达 PURCHASED 状态后三天内未被确认的购买。这不是客户的申请,也不是 Google 的处罚,而是缺失了一个确认调用,从商店通知在服务器端进行确认就能消除它。
确认购买和消耗购买有什么区别?
两者都能满足三天要求。你通过 purchases.products.consume 或 consumeAsync() 消耗一件消耗型商品,这同时也让产品可再次购买。你通过 purchases.products.acknowledge、purchases.subscriptions.acknowledge 或 acknowledgePurchase() 确认一件非消耗型商品或订阅,这确认了权益,但不会让产品可供再次购买。
我需要确认 Google Play 订阅续订吗?
不需要。只有首次订阅购买需要确认。Google 不要求确认续订,并会自动将它们标记为 ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED。一笔新购买以 ACKNOWLEDGEMENT_STATE_PENDING 到达,在你处理掉它之前一直是你的责任。
我可以在购买还处于 PENDING 时就确认它吗?
不可以。你应该只在购买状态为 PURCHASED 时才确认。一笔 PENDING 的购买,比如现金支付或家长批准请求,还没有启动三天倒计时。只在状态从 PENDING 转变为 PURCHASED 之后再授予权益和确认。
对于我没有完成的购买,Apple 会退款吗?
不会。Apple 的 StoreKit 会在每次你的应用启动时重新投递一笔未完成的交易,直到你完成它,但它绝不会自动退款。针对未确认购买的三天自动退款是 Google Play 独有的,所以这两个平台需要不同的处理方式。
如果一笔购买已经因为未确认而被退款,我该如何补救?
你无法撤销这笔退款,但你可以对账。轮询 Voided Purchases API,它会列出过去 30 天内被退款和撤销的购买,并标出那些因为从未被确认而作废的购买,然后撤销你已授予的权益。往后,从商店通知进行确认,这样下一笔就不会溜走。

来源和延伸阅读

RefundHalt

App Store 和 Google Play 退款自动驾驶

继续阅读

下一项退款请求已经在路上。

读完又一封关于未能申辩退款的支持邮件所需的时间,足够您完成 RefundHalt 设置。