Android 应用内订阅购买后如何获取买家邮箱?
明确结论
无法通过 Google Play Billing API、Google Play Developer API 或旧版 Google Checkout Notification History API 获取 Android 应用内订阅买家的 Google 账号邮箱。
购买结果中的 order_id、purchase_token 等字段用于验证和管理交易,并不会披露买家身份。Google Play 后台页面即使显示了部分订单信息,也不意味着这些信息会通过 API 提供。
可行的做法是让用户在应用内登录,或由用户主动填写并验证邮箱。购买完成后,再由服务端将应用账号与 purchase_token 关联。
为什么 billing address 不再包含邮箱
buyer-billing-address 来自早期 Google Checkout 的数据结构。Google Checkout 及其 Notification History API 已不属于当前的 Android 应用内购买体系,不能再用它们获取订阅用户资料。
在现代 Google Play Billing 中,付款信息与开发者维护的用户身份是分开的:
- Google Play 处理付款、续订、退款和订阅状态。
- 应用管理登录账号、邮箱和业务身份。
purchase_token用于验证和跟踪购买,但不包含邮箱。order_id是交易标识,无法通过公开 API 反查买家邮箱。- Google Play Developer API 可以返回订阅状态、商品、续订和取消等信息,但不会返回买家的 Google 账号邮箱。
如果 XML 中不再有 email 节点,就不应继续解析其他结算地址字段,试图从中寻找替代信息。
推荐实现方式
1. 在购买前确认应用内用户身份
要求用户先登录应用账号,或者至少完成邮箱验证。购买成功后,将以下信息发送给服务端:
- 应用内部的
userId purchaseTokenproductIdpackageName
邮箱应保存在应用自己的用户系统中,不要尝试从 Google Play 订单中反查。
2. 在发起购买时传递混淆后的账号标识
Play Billing 支持通过 obfuscatedAccountId 传入应用账号的混淆标识。该值可以帮助后端关联购买和用户,也有助于 Google 进行欺诈检测。
不要直接传入邮箱、手机号或其他明文个人信息。可以使用带密钥的 HMAC 处理稳定的内部用户 ID:
import android.util.Base64
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
fun createObfuscatedAccountId(userId: String, secret: ByteArray): String {
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(secret, "HmacSHA256"))
return Base64.encodeToString(
mac.doFinal(userId.toByteArray(Charsets.UTF_8)),
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
)
}
发起订阅购买时设置该标识:
val accountIdentifiers =
BillingFlowParams.ObfuscatedAccountIdParams.newBuilder()
.setObfuscatedAccountId(createObfuscatedAccountId(user.id, secret))
.build()
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.setObfuscatedAccountIdParams(accountIdentifiers)
.build()
billingClient.launchBillingFlow(activity, billingFlowParams)
具体构造方式可能会随 Play Billing Library 的版本变化,应以当前使用版本的官方接口定义为准。原则是传递不可逆或无法直接识别用户的内部账号标识,而不是邮箱明文。
3. 购买成功后上传 purchase token
客户端收到购买结果后,将令牌发送给服务端:
override fun onPurchasesUpdated(
billingResult: BillingResult,
purchases: MutableList<Purchase>?
) {
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
return
}
purchases.orEmpty().forEach { purchase ->
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
api.registerPurchase(
userId = currentUser.id,
purchaseToken = purchase.purchaseToken,
productIds = purchase.products
)
}
}
}
服务端至少应保存类似的关联关系:
user_id
purchase_token
product_id
package_name
latest_order_id
subscription_status
created_at
updated_at
不要只把 order_id 用作订阅的长期主键。续订可能产生新的订单编号,而 purchase_token 更适合用于调用 Google Play Developer API 查询订阅状态。升级、降级或重新购买时,令牌也可能发生变化,因此服务端需要处理新旧购买之间的关系。
4. 由服务端验证订阅
服务端收到 purchaseToken 后,应调用 Google Play Developer API 进行验证,不要直接信任客户端上报的购买状态。例如,可以使用订阅查询接口:
GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{token}
Authorization: Bearer ACCESS_TOKEN
该接口可以确认订阅状态和续订信息,但不会返回买家邮箱。验证成功后,服务端再向已登录的应用用户发放订阅权益。
没有登录系统时怎么办
如果现有应用允许匿名购买,就无法只凭历史 order_id 或 purchase_token 稳定、合规地恢复买家邮箱。可以采用以下补救方式:
- 在应用内增加”绑定邮箱”或”创建账号”流程。
- 验证用户是否拥有该邮箱。
- 恢复并验证设备当前 Google Play 账号下的有效购买。
- 将验证成功的
purchaseToken绑定到应用账号。 - 在服务端阻止同一有效订阅被多个账号重复绑定。
这个流程恢复的是设备当前能够访问的购买记录,并不是从订单中读取 Google 账号邮箱。
注意事项
- 不要假设用户的应用登录邮箱与 Google Play 付款账号邮箱相同。
- 不要根据
order_id、收据文本或结算地址推断用户身份。 - 不要将邮箱明文传入
obfuscatedAccountId或obfuscatedProfileId。 - 购买验证、令牌绑定和订阅权益发放应由服务端完成。
- 服务端应处理退款、撤销、暂停、宽限期、账号保留和续订等状态变化。
- 可以结合 Real-time Developer Notifications 更新订阅状态,但通知同样不会提供买家邮箱。
- 收集邮箱时应说明用途,并遵守适用的隐私政策和数据保护要求。
Google Play 的购买 API 用来证明”这笔订阅有效”,应用自己的账号系统则用来确定”这笔订阅属于谁”。如果业务必须使用邮箱,应在应用内征得用户同意,并自行收集和验证。
备注:内容仅供参考。
