关于 gplay-purchase-verification
gplay-purchase-verification 是指导后端通过 Google Play API 验证应用内购买和订阅的技能,面向实现购买授权与订阅状态管理的开发者。内容还涉及确认或消耗购买、处理退款及订阅事件,并提供 gplay 命令和后端示例。
主要功能
- 01查询应用内商品购买详情
- 02验证订阅及其状态
- 03确认或消耗购买
- 04管理订阅取消、延期与撤销
- 05设置及解码 RTDN 通知
适用场景
- 01在服务端核验购买令牌后授予权益
- 02检查订阅状态并处理续订或取消
- 03处理退款、撤销及无效购买记录
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ tamtom/ gplay-cli-skills --skill gplay-purchase-verification安装后在 Claude Code 中输入/gplay-purchase-verification确认已加载。
详细介绍
Google Play 购买验证
当你需要从后端服务器验证应用内购买或订阅时,使用此技能。
为什么要在服务器端验证购买?
客户端验证可以被绕过。务必在服务器上验证购买:
- 防止欺诈和盗版
- 确保用户确实已付款
- 检查订阅状态
- 处理退款和取消
身份验证设置
你的后端需要一个具有购买验证权限的服务账号。
创建服务账号
- 前往 Google Cloud Console
- 创建服务账号
- 授予“Service Account User”角色
- 下载 JSON 密钥
授予 API 访问权限
- 前往 Play Console
- “用户和权限” → “服务账号”
- 授予服务账号访问你应用的权限
验证应用内商品购买
获取购买详情
gplay purchases products get \
--package com.example.app \
--product-id premium_upgrade \
--token <PURCHASE_TOKEN>
响应
{
"kind": "androidpublisher#productPurchase",
"purchaseTimeMillis": "1706400000000",
"purchaseState": 0,
"consumptionState": 0,
"developerPayload": "user_123",
"orderId": "GPA.1234-5678-9012-34567",
"purchaseType": 0
}
购买状态
0= 已购买1= 已取消2= 待处理
消耗状态
0= 尚未消耗1= 已消耗
确认购买
验证后,确认购买:
gplay purchases products acknowledge \
--package com.example.app \
--product-id premium_upgrade \
--token <PURCHASE_TOKEN>
重要: 未确认的购买将在 3 天后退款。
消耗购买(适用于消耗型商品)
对于消耗型商品(金币、宝石等):
gplay purchases products consume \
--package com.example.app \
--product-id coins_100 \
--token <PURCHASE_TOKEN>
验证订阅
优先使用 v2 API。 对于新的集成,请使用
gplay purchases subscriptionsv2 get(以及subscriptionsv2 cancel/defer/revoke、purchases productsv2 get)。v2SubscriptionPurchaseV2模型反映了基础方案和优惠;下面的 v1 端点仍然可用,但采用的是旧版结构。
获取订阅详情(v2,推荐)
gplay purchases subscriptionsv2 get \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>
获取订阅详情(v1,旧版)
gplay purchases subscriptions get \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>
响应
{
"kind": "androidpublisher#subscriptionPurchase",
"startTimeMillis": "1706400000000",
"expiryTimeMillis": "1709000000000",
"autoRenewing": true,
"priceCurrencyCode": "USD",
"priceAmountMicros": "4990000",
"paymentState": 1,
"cancelReason": null,
"userCancellationTimeMillis": null,
"orderId": "GPA.1234-5678-9012-34567",
"linkedPurchaseToken": null,
"subscriptionState": 0
}
订阅状态
0= 有效1= 已取消(到期前仍然有效)2= 处于宽限期3= 暂停使用(付款失败,正在重试)4= 已暂停5= 已过期
付款状态
0= 付款待处理1= 已收到付款2= 免费试用3= 待处理的延期升级/降级
后端实现示例
Node.js/Express
const { google } = require('googleapis');
async function verifyPurchase(packageName, productId, token) {
const auth = new google.auth.GoogleAuth({
keyFile: '/path/to/service-account.json',
scopes: ['https://www.googleapis.com/auth/androidpublisher'],
});
const androidpublisher = google.androidpublisher({
version: 'v3',
auth: await auth.getClient(),
});
const result = await androidpublisher.purchases.products.get({
packageName: packageName,
productId: productId,
token: token,
});
return result.data;
}
// Endpoint
app.post('/verify-purchase', async (req, res) => {
const { packageName, productId, token } = req.body;
try {
const purchase = await verifyPurchase(packageName, productId, token);
if (purchase.purchaseState === 0) {
// Purchase is valid
// Grant access to user
// Acknowledge purchase
res.json({ valid: true, purchase });
} else {
res.json({ valid: false });
}
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Python/Flask
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = ['https://www.googleapis.com/auth/androidpublisher']
SERVICE_ACCOUNT_FILE = '/path/to/service-account.json'
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES)
androidpublisher = build('androidpublisher', 'v3', credentials=credentials)
@app.route('/verify-purchase', methods=['POST'])
def verify_purchase():
data = request.json
package_name = data['packageName']
product_id = data['productId']
token = data['token']
try:
result = androidpublisher.purchases().products().get(
packageName=package_name,
productId=product_id,
token=token
).execute()
if result['purchaseState'] == 0:
# Purchase is valid
return jsonify({'valid': True, 'purchase': result})
else:
return jsonify({'valid': False})
except Exception as e:
return jsonify({'error': str(e)}), 400
处理订阅事件
实时开发者通知(RTDN)
设置 Pub/Sub 以接收订阅事件:
- 在 Google Cloud Console 中创建 Pub/Sub 主题
- 在 Play Console 中配置:
- “变现设置” → “实时开发者通知”
- 输入主题名称
gplay 可以生成 RTDN 框架并解码,无需手动编写 base64/JSON 解析代码:
# Print the Pub/Sub topic + Play Console setup steps
gplay rtdn setup --package com.example.app
# Decode an RTDN payload into readable JSON (notification type, token, etc.)
# Accepts a full Pub/Sub envelope (message.data is base64) or the raw notification.
gplay rtdn decode --data '{"message":{"data":"<BASE64_DATA>"}}'
cat payload.json | gplay rtdn decode --file -
- 订阅事件:
from google.cloud import pubsub_v1
subscriber = pubsub_v1.SubscriberClient()
subscription_path = subscriber.subscription_path(project_id, subscription_id)
def callback(message):
data = json.loads(message.data)
if 'subscriptionNotification' in data:
notification = data['subscriptionNotification']
notification_type = notification['notificationType']
purchase_token = notification['purchaseToken']
# Handle different events
if notification_type == 1: # SUBSCRIPTION_RECOVERED
# Subscription was recovered from account hold
pass
elif notification_type == 2: # SUBSCRIPTION_RENEWED
# Subscription renewed successfully
pass
elif notification_type == 3: # SUBSCRIPTION_CANCELED
# User canceled subscription
pass
elif notification_type == 4: # SUBSCRIPTION_PURCHASED
# New subscription purchase
pass
elif notification_type == 7: # SUBSCRIPTION_EXPIRED
# Subscription expired
pass
elif notification_type == 10: # SUBSCRIPTION_PAUSED
# Subscription paused
pass
elif notification_type == 12: # SUBSCRIPTION_REVOKED
# Subscription revoked (refunded)
pass
message.ack()
subscriber.subscribe(subscription_path, callback=callback)
订阅管理
取消订阅
gplay purchases subscriptions cancel \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>
延期订阅
gplay purchases subscriptions defer \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN> \
--json @defer.json
defer.json
{
"deferralInfo": {
"expectedExpiryTimeMillis": "1709000000000"
}
}
撤销订阅(退款)
gplay purchases subscriptions revoke \
--package com.example.app \
--token <SUBSCRIPTION_TOKEN>
检查已作废的购买
获取已退款/已取消购买的列表:
gplay purchases voided list \
--package com.example.app \
--start-time 1706400000000 \
--end-time 1709000000000
在后端移除这些购买对应的权益。
订单信息
获取订单详情
gplay orders get \
--package com.example.app \
--order-id GPA.1234-5678-9012-34567
批量获取订单
gplay orders batch-get \
--package com.example.app \
--order-ids "GPA.1234,GPA.5678,GPA.9012"
退款订单
orders refund 是一项具有破坏性的写入操作,必须使用 --confirm——否则命令会拒绝运行。
gplay orders refund \
--package com.example.app \
--order-id GPA.1234-5678-9012-34567 \
--revoke \ # Also revoke entitlement/access
--confirm # Required — refund is irreversible
安全最佳实践
应该:
- ✅ 始终在服务器上验证,绝不信任客户端
- ✅ 安全存储购买令牌
- ✅ 在 3 天内确认购买
- ✅ 处理退款和取消
- ✅ 所有 API 调用都使用 HTTPS
- ✅ 对验证端点进行速率限制
- ✅ 记录所有验证尝试
不应该:
- ❌ 只在客户端验证购买
- ❌ 暴露服务账号凭据
- ❌ 跳过购买确认
- ❌ 验证前就授予访问权限
- ❌ 忽略已作废的购买
- ❌ 存储信用卡信息(PCI 合规要求)
常见验证流程
- 用户在应用中购买
- 应用将购买令牌发送到你的服务器
- 服务器通过 Google Play API 验证
- 服务器确认购买(如果有效)
- 服务器向用户授予访问权限/内容
- 服务器存储购买令牌,以便日后检查
- 服务器监听RTDN 事件(取消、续订)
错误处理
常见错误
401 Unauthorized- 服务账号未经授权404 Not Found- 购买令牌无效或已过期410 Gone- 购买已退款/取消
重试逻辑
async function verifyWithRetry(packageName, productId, token, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await verifyPurchase(packageName, productId, token);
} catch (error) {
if (error.code === 404 || error.code === 410) {
throw error; // Don't retry if purchase is invalid
}
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
测试
测试购买
使用 Google Play 的测试账号进行测试购买,无需支付真实费用。
测试验证
# Verify test purchase
gplay purchases products get \
--package com.example.app \
--product-id android.test.purchased \
--token <TEST_TOKEN>
监控
跟踪以下指标:
- 购买验证成功率
- 确认率
- 退款率
- 订阅流失率
- 支付失败率
使用这些数据改进你的变现策略。
评论
支持 Markdown