最近和不少出海同事聊,发现一个高频坑:用户明明在 App 里从周订阅升到了月订阅,客户端也弹了成功,但你拿最新一笔交易 / 收据里的productId一对,还是旧档。
权益发错、后台对账对不上、客户说「我买的是月卡」——基本都从这儿开始。
这篇文章只讲怎么查。
1. 先分清:你在看的是「哪一个」productId
换档后至少会出现三类 ID,很多人混在一块:
| 来源 | 大致含义 | 能不能当「当前档位」 |
|---|---|---|
某笔 Transaction 的productId | 这一笔交易买的是什么 | 不一定。它描述的是这笔单,不是「此刻订阅应该是什么」 |
renewalInfo.productId | Apple 认为的当前订阅商品 | 多数情况下应优先信这个 |
renewalInfo.autoRenewProductId | 下一计费周期会续到的商品 | 降级延期生效时,它往往是新档,当前周期仍是旧档 |
一句话:
收据 / 最新 Transaction 上的 productId ≠ 当前应发放权益的商品。
尤其是同组升级、降级「当前周期不变、下周期再生效」时,三者可以同时不一致,而且都「合理」。
2. 常见现象(对号入座)
现象 A:刚升级成功,Transaction 仍是旧 SKU
沙盒里更明显。客户端purchase(新 SKU)走完了,但你立刻用本地 Transaction 或只验「刚到手的那张票」,productId还停在旧档。过一会儿再查 Subscription Status,才会对齐。
现象 B:降级后「收据还是高级档」
用户选了更便宜的档,Apple 常常是:本周期继续用贵的,下周期才切到便宜的。
于是:
- 当前权益仍应对高级档
autoRenewProductId已是低级档- 若你只看「用户点的那个新 SKU」去改权益,会提前降权,客诉直接来
现象 C:服务端落库的是请求里的 product_id,和 Apple 权威档不一致
客户端带着「我想买的 SKU」去 verify,服务端如果无条件以请求 body 为准,换档瞬间就会和 Apple 真相打架。正确做法是:以 Apple 订阅状态解析出的权威 SKU 入账,请求里的 ID 只作参考。
3. 排查清单(按这个顺序查)
Step 1:不要只盯「最新一笔购买」
把这条链拉出来:
originalTransactionId(同组订阅的根)- 当前这笔
transactionId - 该订阅组下的Subscription Status(Get All Subscription Statuses)
只 decode 最新一张 JWS Transaction,在换档场景里信息量不够。
Step 2:同时看三个字段
对 Status API 返回里匹配到的那条订阅,解码:
signedTransactionInfo→productId(这笔交易商品)signedRenewalInfo→productId(当前订阅商品)signedRenewalInfo→autoRenewProductId(下周期商品)
对照表:
Step 3:定一条「权威 SKU」规则
落地原则(实现细节可各异,原则尽量统一):
- 有
renewalInfo.productId→ 优先用它作为当前应授予的商店 SKU - 订阅已非活跃时,再回退到 transaction 上的
productId - 活跃但暂时没有
renewal.productId时,再考虑autoRenewProductId/ transaction - 权益标识(entitlement)按「档位能力」设计,同组多档共用同一 entitlement id,换档只换 product,不换「有没有会员」这条线,过渡期才不会漏判/误判
Step 4:ASN 也要同一套逻辑
DID_CHANGE_RENEWAL_PREF、续费类通知进来时,不要只信通知里顺带的旧快照。
和客户端验单一样:能打 Status API 就再确认一次权威 SKU,再写购买记录 / 推 Webhook。
Step 5:客户端怎么测
- 升级:App 内直接
purchase(新 SKU),以服务端返回的当前权益对应商品为准,不要本地用「我刚点的那个 ID」覆盖 UI - 降级:看清是立即生效还是下周期生效;UI 文案要写「本期仍为 xx,下期变为 yy」
- Restore:解决不了「刚在 App 内升级」的同步问题;别把 restore 当换档的主路径
4. 一个最小自检脚本(思路)
snapshot = GetSubscriptionStatus(originalTransactionId or transactionId) authSku = renewal.productId ?? (active ? autoRenewProductId : null) ?? transaction.productId grantEntitlement(mapStoreSkuToEntitlement(authSku)) // 不要: grantEntitlement(mapStoreSkuToEntitlement(clientRequestedProductId))若authSku != clientRequestedProductId,打日志即可——很多「对不上」其实是预期行为,不是 Apple 坏了。
5. 小结
换档后 productId「对不上」,多半不是收据坏了,而是:
- 你把「某一笔交易的商品」当成了「当前订阅商品」
- 降级延期时,当前档和下周期档本来就该不同
- 沙盒 / 通知延迟下,Transaction 会短时间滞后于 Renewal Info
先把三个字段拆开看,再定权威 SKU,权益就会稳很多。
我们后来把「以 Apple Subscription Status 纠正当前 SKU、权益层与商品层分离」写进了自己的订阅基建(SubHub)里,换档验单和 ASN 走同一套规则。若你也在啃 StoreKit 2 + 服务端验单,欢迎评论区交换踩坑;纯技术问题我尽量回。