Android In-App Billing V3 订阅验证出现 404
结论
404 purchaseTokenNotFound 表示 Google Play Developer API 找不到与指定 packageName、订阅商品 ID 和 purchaseToken 相匹配的购买记录。它并不是“订阅已取消”或“订阅已过期”的正常状态。
如果仍有部分记录能够验证成功,服务账号授权和 API 调用方式通常没有整体失效。此时应优先检查:
purchaseToken从 App 传到服务器的过程中是否被截断、转义或替换。- 传入的
packageName或订阅商品 ID 是否与该 token 对应的购买一致。 - 客户端是否上报了缓存数据、重复数据或并非由 Google Play 生成的购买数据。
- Google Play Developer API 是否出现临时异常。只有完全相同的请求稍后又能成功,才更符合这种情况。
先检查原始参数
记录失败请求的以下信息,但不要把完整 token 写入普通日志:
packageNameproductIdpurchaseToken的长度- token 的哈希值
- 客户端购买 JSON 中的商品 ID
- 购买时间
- Google API 返回的 HTTP 状态码和
Reason
例如:
using System;
using System.Security.Cryptography;
using System.Text;
static string TokenFingerprint(string token)
{
if (string.IsNullOrEmpty(token))
return "<empty>";
byte[] data = SHA256.HashData(Encoding.UTF8.GetBytes(token));
return Convert.ToHexString(data).Substring(0, 16);
}
日志中记录指纹和长度即可:
logger.LogInformation(
"Verify subscription: package={Package}, product={Product}, tokenLength={Length}, tokenHash={Hash}",
packageName,
productId,
purchaseToken?.Length ?? 0,
TokenFingerprint(purchaseToken));
再对比客户端发送前与服务器接收后的长度和哈希。两边不一致,说明问题出在传输或解析环节。
避免 purchase token 在传输中损坏
purchaseToken 是不透明字符串。不要假设它采用固定的字符格式,也不要自行拆分或规范化。尤其不要:
- 调用
Trim()后再保存; - 将
+自动转换为空格; - 按
.、:、/等字符拆分; - 存入长度不足的数据库字段;
- 使用未经正确编码的查询字符串传递;
- 对 token 重复执行 URL decode。
建议通过 HTTPS 请求体发送 JSON:
{
"packageName": "com.example.app",
"productId": "monthly_subscription",
"purchaseToken": "opaque-token-from-google-play"
}
数据库字段需要留出足够空间,例如使用 TEXT 或较大的字符串字段,不要根据历史 token 的长度设置过小的上限,也不要用旧 token 样本推断未来格式。
确认商品 ID 与 token 匹配
调用:
service.Purchases.Subscriptions.Get(
packageName,
productId,
purchaseToken);
这里的 productId 必须是该次购买对应的订阅商品 ID,不能传入:
- 订单号
orderId; - 基础方案 ID;
- 优惠 ID;
- 本地展示名称;
- 当前用户计划的默认商品 ID;
- 另一个应用或环境中的同名商品。
应从客户端购买结果中读取实际商品信息,再由服务器与允许的商品列表核对。不要只根据用户选择的页面或本地缓存推导 productId。
如果应用有多个包名、测试版、白标版本或经历过应用迁移,还要确认 token 和传入的 packageName 属于同一个 Google Play 应用。
不要把 404 当作订阅失效
订阅取消后,购买记录通常仍然存在。接口会通过返回对象中的状态字段说明续订、到期或取消情况。因此,不能直接把 purchaseTokenNotFound 解释成“用户取消了订阅”。
遇到 404 时,应采用保守的处理方式:
- 不授予新的订阅权益。
- 不立即删除已有权益或购买记录。
- 使用完全相同的参数进行有限次数的重试。
- 如果持续失败,将记录标记为待核查。
- 要求客户端重新查询 Google Play 中的有效购买并再次上报。
示例:
try
{
var request = service.Purchases.Subscriptions.Get(
packageName,
productId,
purchaseToken);
SubscriptionPurchase purchase = request.Execute();
// 根据 purchase 返回的订阅状态决定权益。
}
catch (Google.GoogleApiException ex)
when (ex.HttpStatusCode == System.Net.HttpStatusCode.NotFound)
{
// 不要把 404 直接当作“订阅已取消”。
// 记录 packageName、productId、token 长度和哈希,进入重试或人工核查流程。
throw;
}
重试只能用来排除短暂的服务异常。如果参数有误或 token 已经损坏,反复请求也无法解决问题。
检查非真实购买或异常客户端
如果失败主要出现在特定设备、应用版本、安装来源或用户群体中,还要考虑客户端上报的数据是否真的由 Google Play 签发。例如:
- 应用来自非 Google Play 安装渠道;
- 设备上的支付环境被修改;
- 客户端伪造购买成功回调;
- 本地签名验证实现有误;
- 服务端信任了客户端传入的“验证成功”结果。
签名验证必须使用原始购买 JSON 和 Google Play 公钥完成。服务端最终应根据 Google Play Developer API 的结果判断是否发放权益,不能只凭客户端报告购买成功。
如何判断是否是 Google 侧临时故障
同时出现以下特征时,才更像是临时 API 问题:
packageName、productId和 token 的原始字节完全没有变化;- 同一请求此前成功过;
- 同一请求稍后重试又能成功;
- 异常集中发生在较短时间内;
- 异常与特定应用版本、传输方式或数据库记录无关。
这种情况下,可以保留失败时间、请求参数指纹、响应状态和相关日志,并查询 Google Play 服务状态,或向 Google Play 开发者支持提交案例。
如果同一组参数始终返回 404,而其他购买一直能够正常验证,应先检查 token 是否完整、商品 ID 和包名是否正确,以及购买数据是否真实,而不是先把问题归因于 API 调用代码。
备注:内容仅供参考。
