mcpField
G
gplay-purchase-verification

gplay-purchase-verification

github.com/tamtom开发52351 次安装13 周前更新
代码仓库

关于 gplay-purchase-verification

gplay-purchase-verification 是指导后端通过 Google Play API 验证应用内购买和订阅的技能,面向实现购买授权与订阅状态管理的开发者。内容还涉及确认或消耗购买、处理退款及订阅事件,并提供 gplay 命令和后端示例。

主要功能

  1. 01查询应用内商品购买详情
  2. 02验证订阅及其状态
  3. 03确认或消耗购买
  4. 04管理订阅取消、延期与撤销
  5. 05设置及解码 RTDN 通知

适用场景

  1. 01在服务端核验购买令牌后授予权益
  2. 02检查订阅状态并处理续订或取消
  3. 03处理退款、撤销及无效购买记录

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/tamtom/gplay-cli-skills --skill gplay-purchase-verification

安装后在 Claude Code 中输入/gplay-purchase-verification确认已加载。

详细介绍

Google Play 购买验证

当你需要从后端服务器验证应用内购买或订阅时,使用此技能。

为什么要在服务器端验证购买?

客户端验证可以被绕过。务必在服务器上验证购买:

  • 防止欺诈和盗版
  • 确保用户确实已付款
  • 检查订阅状态
  • 处理退款和取消

身份验证设置

你的后端需要一个具有购买验证权限的服务账号。

创建服务账号
  1. 前往 Google Cloud Console
  2. 创建服务账号
  3. 授予“Service Account User”角色
  4. 下载 JSON 密钥
授予 API 访问权限
  1. 前往 Play Console
  2. “用户和权限” → “服务账号”
  3. 授予服务账号访问你应用的权限

验证应用内商品购买

获取购买详情
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)。v2 SubscriptionPurchaseV2 模型反映了基础方案和优惠;下面的 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 以接收订阅事件:

  1. 在 Google Cloud Console 中创建 Pub/Sub 主题
  2. 在 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 -
  1. 订阅事件:
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 合规要求)

常见验证流程

  1. 用户在应用中购买
  2. 应用将购买令牌发送到你的服务器
  3. 服务器通过 Google Play API 验证
  4. 服务器确认购买(如果有效)
  5. 服务器向用户授予访问权限/内容
  6. 服务器存储购买令牌,以便日后检查
  7. 服务器监听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