iPhone Android Recurring Payments API 如何处理订阅
结论
移动端只负责发起购买,并将购买凭证发送给后端。订阅状态、续费、取消、退款和权限有效期,都应由后端根据商店数据维护。
两端使用的核心凭证不同:
- Google Play:
purchaseToken - Apple App Store:StoreKit 2 的签名交易信息(JWS),以及服务端返回的
originalTransactionId和transactionId
不能仅凭客户端传来的“支付成功”更新订阅。后端需要调用 Google Play Developer API 查询订单状态,或验证 Apple 的签名数据,同时接入两家商店的服务端通知。
推荐的数据流
- 后端配置商品与内部套餐的映射,例如:
google: premium_monthly -> plan_premium_monthly
apple: premium_monthly -> plan_premium_monthly
- App 通过 Google Play Billing Library 或 Apple StoreKit 发起购买。
- App 取得商店凭证后,将其发送给后端。
- 后端验证凭证,并核对商品、应用、订单状态和有效期。
- 后端创建或更新订阅,同时保存商店交易记录。
- Google 订单需要在规定时间内完成 acknowledgement。只有消耗型商品才使用 consume,订阅不能当作消耗型商品处理。
- 后端持续接收续费、取消、退款、撤销和宽限期等服务端通知。
- 用户登录或恢复购买时,后端再次主动查询商店,以修正遗漏的通知。
Google Play 可以获得什么
Android 客户端购买成功后,通常会取得类似以下的数据:
{
"productId": "premium_monthly",
"purchaseToken": "opaque-token",
"orderId": "GPA.1234-5678-9012-34567",
"purchaseTime": 1760000000000,
"purchaseState": 1,
"isAcknowledged": false,
"isAutoRenewing": true
}
具体字段取决于 Billing Library API 和购买类型,客户端对象不能作为最终支付依据。客户端至少应向后端发送以下信息:
{
"platform": "google_play",
"productId": "premium_monthly",
"purchaseToken": "opaque-token"
}
后端使用 Google Play Developer API 查询订阅。服务端响应通常包含:
- 应用包名
- 商品或订阅项
- 订单及关联订单信息
- 购买状态
- 当前订阅有效期
- 是否自动续订
- 取消相关信息
- 付款或价格相关信息
- acknowledgement 状态
- 暂停、宽限期、账号保留等生命周期状态
- 套餐、base plan、offer 等信息
Google Play Developer API 更新迭代时,具体对象和字段也可能变化。新项目应采用当前官方推荐的订阅查询接口,不要按照已经废弃的旧响应结构设计数据库。
后端验证流程可以抽象为:
async function verifyGoogleSubscription(input, userId) {
const subscription = await googlePlayApi.getSubscription({
packageName: "com.example.app",
purchaseToken: input.purchaseToken
});
if (!subscription) {
throw new Error("Subscription not found");
}
const purchasedItem = subscription.lineItems?.find(
item => item.productId === input.productId
);
if (!purchasedItem) {
throw new Error("Product mismatch");
}
await saveStoreTransaction({
userId,
platform: "google_play",
productId: input.productId,
externalTransactionId: subscription.latestOrderId,
purchaseToken: input.purchaseToken,
expiresAt: purchasedItem.expiryTime,
rawPayload: subscription
});
return calculateEntitlement(subscription);
}
calculateEntitlement() 不能只检查 isAutoRenewing。用户关闭自动续订后,通常仍可使用订阅,直到当前已付款周期结束。
Apple App Store 可以获得什么
使用 StoreKit 2 时,客户端可以取得经过 Apple 签名的交易数据。常见交易属性包括:
transactionIdoriginalTransactionIdproductIdpurchaseDateoriginalPurchaseDateexpirationDaterevocationDaterevocationReasonisUpgradedsubscriptionGroupIdentifierenvironmentappAccountToken- 所有权类型等信息
客户端可以发送签名交易 JWS,也可以发送足以让后端查询交易的标识:
{
"platform": "app_store",
"productId": "premium_monthly",
"signedTransactionInfo": "eyJhbGciOiJFUzI1NiIs..."
}
建议在购买时设置由后端生成的 appAccountToken,用来关联 App Store 交易和站内用户。不要直接将邮箱或用户名用于该字段,通常应使用不可预测的 UUID。
后端需要完成以下处理:
- 验证 JWS 签名和证书链。
- 检查
bundleId、environment和商品是否属于自己的应用。 - 检查交易是否已经撤销。
- 使用
originalTransactionId关联同一条订阅链。 - 根据
expirationDate和续订状态计算用户权限。 - 必要时通过 App Store Server API 查询交易历史或订阅状态。
伪代码如下:
async function verifyAppleSubscription(input, userId) {
const transaction = await verifyAppleJws(
input.signedTransactionInfo
);
if (transaction.bundleId !== "com.example.app") {
throw new Error("Bundle ID mismatch");
}
if (transaction.productId !== input.productId) {
throw new Error("Product mismatch");
}
await saveStoreTransaction({
userId,
platform: "app_store",
productId: transaction.productId,
externalTransactionId: transaction.transactionId,
originalTransactionId: transaction.originalTransactionId,
expiresAt: transaction.expiresDate,
revokedAt: transaction.revocationDate,
rawPayload: transaction
});
return {
active:
!transaction.revocationDate &&
new Date(transaction.expiresDate) > new Date()
};
}
实际项目应使用 Apple 提供或认可的服务端库验证 JWS,不能跳过证书链、环境或应用标识校验。
如果仍在维护基于 receipt 的旧实现,服务端也可以验证 App Store receipt。新系统更适合围绕 StoreKit 2、App Store Server API 和 App Store Server Notifications 设计,同时兼容旧交易。
取消、终止和退款是否支持
支持,但取消、终止和退款是三种不同的操作。
用户取消自动续订
用户通常需要前往系统的订阅管理页面取消:
- Google Play:Play Store 的订阅管理页面
- Apple:系统或 App Store 的订阅管理页面
取消自动续订通常不会立即终止服务。用户在当前计费周期内仍然拥有权限,直至 expiryTime 或 expirationDate。
App 可以打开订阅管理页面,但不能自行将跳转操作视为“取消成功”:
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/account/subscriptions")
)
startActivity(intent)
Apple 平台可以使用 StoreKit 提供的订阅管理入口,具体调用方式取决于采用的 StoreKit API 和系统版本。
开发者终止订阅
Google Play Developer API 提供订阅管理相关的服务端操作。具体可以执行哪些操作,取决于订阅状态和当前接口,例如取消订阅或撤销已经授予的权益。
Apple 订阅通常由用户在 Apple 管理界面中取消。开发者不能只把自己后端的状态改为 canceled,就认为 App Store 订阅已经取消。如果需要停止权益,应使用 Apple 当前提供的服务端能力或后台流程,并明确区分“停止续订”和“撤销当前权益”。
退款
退款由商店支付体系处理,App 不应自行向银行卡或其他付款方式退款。
- Google Play 可以通过 Play Console 或适用的 Developer API 处理退款、撤销等操作。
- Apple 退款通常由 Apple 接受申请并进行审核。开发者可以向用户提供 Apple 的退款入口,再通过服务端通知获取退款或撤销结果。
退款后,商店可能发送退款或撤销通知。后端收到通知时,应记录退款交易,再根据通知的具体含义决定立即收回权益,还是将权益保留到周期结束。
“退款政策”不是 API 返回的通用配置对象。政策取决于商店规则、地区法律、商品类型和审核结果,不能通过接口设置一套跨平台通用的退款规则。
服务端通知必须接入
只在购买时验证一次无法持续维护准确的订阅状态。即使 App 没有运行,订阅仍可能发生以下变化:
- 自动续费成功或失败
- 用户关闭或恢复自动续订
- 进入宽限期
- 账号保留或暂停
- 升级、降级或更换套餐
- 退款、撤销或拒付
- 订阅到期
Google Play 使用 Real-time Developer Notifications。通知通常只说明“某个购买发生了变化”,后端收到通知后,仍需根据 purchaseToken 查询最新状态。
Apple 使用 App Store Server Notifications。通知中包含签名数据,后端需要验证签名,再根据通知类型和子类型更新订阅。
通知处理必须具有幂等性:
async function handleStoreEvent(event) {
const eventId = getStableEventId(event);
if (await eventAlreadyProcessed(eventId)) {
return;
}
await database.transaction(async tx => {
await tx.insertStoreEvent(eventId, event);
const verifiedState = await queryOrVerifyLatestStoreState(event);
await tx.upsertSubscription(normalizeSubscription(verifiedState));
await tx.upsertTransactions(normalizeTransactions(verifiedState));
});
}
通知可能重复、延迟或乱序,不能简单地按照接收顺序覆盖状态。更新时应比较交易时间、到期时间和商店交易标识。
后端实体如何设计
Apple 和 Google 的原始对象没有必要共用完全相同的字段。更稳妥的设计是保存一层统一的业务状态,同时保留各平台的原始数据。
Subscription
- id
- user_id
- platform
- product_id
- status
- current_period_start
- current_period_end
- auto_renew_enabled
- original_transaction_id
- purchase_token
- environment
- created_at
- updated_at
StoreTransaction
- id
- subscription_id
- platform
- external_transaction_id
- external_order_id
- product_id
- transaction_type
- purchased_at
- expires_at
- revoked_at
- amount
- currency
- raw_payload
- created_at
StoreEvent
- id
- platform
- external_event_id
- event_type
- occurred_at
- raw_payload
- processed_at
金额和币种未必能从所有订阅状态接口中稳定取得。如果财务对账是硬性需求,还应保存订单数据,并使用商店提供的财务或销售报告。不要假设每条生命周期通知都会包含完整金额。
常见陷阱
- 不要信任客户端提交的
active: true、价格、到期时间或订单状态。 - 不要只凭商品 ID 开通权益,必须验证
purchaseToken或签名交易。 - 一个购买凭证只能绑定一个站内账号。绑定操作需要具备原子性,并设置唯一约束。
- Apple 应使用
originalTransactionId追踪订阅链,不能只保存最新的transactionId。 - Google 的续订订单可能产生新的订单标识,购买令牌及其替换关系需要按照接口返回处理。
- 正确区分生产环境和沙盒环境。
- 取消自动续订不代表订阅立即失效。
- 支付失败也未必会让订阅立即失效,商店可能进入重试或宽限期。
- 不要根据客户端设备时间判断订阅是否过期。
- App 启动时可以同步购买,但服务端通知和主动查询才是最终状态来源。
- 如果 Web、Apple 和 Google 三套订阅共享账号权益,需要明确重复订阅、跨平台取消以及升级降级规则。商店订阅通常不能由另一个支付渠道直接取消。
备注:内容仅供参考。
