完成 Google Play 内购集成所需的全链路实现,包括: - 数据库表结构(google-play-iap.sql) - 实体、Mapper、Service 及 XML 配置 - AI 评论举报实体与业务层 - 集成文档(google-play-iap-integration.md)
7.5 KiB
7.5 KiB
Google Play IAP 服务端集成
系统设计
目标
- 客户端购买成功后,服务端使用
purchaseToken二次校验 Google Play Developer API。 - RTDN 进入后,先做 Pub/Sub 来源校验,再二次查询 Google Play Developer API,同步本地订单、token、权益状态。
- 对订阅与一次性商品统一落库,保证幂等、防重放和可审计。
核心流程
- 客户端发起购买,BillingClient 中建议设置
obfuscatedAccountId = 当前 userId。 - 客户端支付成功后调用
/api/google-play/purchases/verify。 - 服务端调用 Google Play Developer API:
- 订阅:
purchases.subscriptionsv2.get - 一次性商品:
purchases.productsv2.getproductpurchasev2
- 订阅:
- 服务端更新
google_play_purchase_token、google_play_order、google_play_user_entitlement。 - 对需要确认的订单执行 acknowledge / consume。
- Google Play RTDN 推送到
/api/google-play/rtdn。 - webhook 校验 topic / subscription / OIDC JWT 后,再次查询 Developer API,再更新本地状态。
幂等策略
- RTDN 事件以
message_id幂等,重复消息直接忽略。 - 订单以
order_key幂等:- 有
google_order_id时用订单号。 - 没有订单号时回退为
TOKEN:{purchaseToken}。
- 有
- 购买 token 表以
purchase_token唯一。 - 一次性商品发货前先看
delivery_status,已发货不重复发。 - 订阅不做“累加式延长”,而是直接把用户 VIP 到期时间同步为 Google 返回的最新
expiryTime,天然幂等。
本地权益映射
subscription->VIP_SUBSCRIPTION- 一次性商品且
duration_days > 0或unit包含vip/member/会员->VIP_ONE_TIME - 一次性商品且
unit像coin/quota/credit/金币/次数,或商品名/durationValue可解析数值 ->WALLET_TOP_UP - 其他一次性商品 ->
NON_CONSUMABLE
目录结构
src/main/java/com/yolo/keyborad/
├── config/
│ ├── GooglePlayHttpConfig.java
│ └── GooglePlayProperties.java
├── controller/
│ └── GooglePlayController.java
├── googleplay/
│ ├── GooglePlayApiClient.java
│ ├── GooglePlayApiException.java
│ ├── GooglePlayConstants.java
│ ├── GooglePlayEntitlementApplier.java
│ ├── GooglePlayPubSubAuthService.java
│ ├── GooglePlayServiceAccountTokenProvider.java
│ ├── GooglePlayStateService.java
│ └── model/
│ ├── GooglePlayPurchaseSnapshot.java
│ ├── GooglePlaySyncCommand.java
│ └── GooglePlaySyncResult.java
├── mapper/
│ ├── GooglePlayOrderMapper.java
│ ├── GooglePlayPurchaseTokenMapper.java
│ ├── GooglePlayRtdnEventMapper.java
│ └── GooglePlayUserEntitlementMapper.java
├── model/
│ ├── dto/googleplay/
│ │ ├── GooglePlayPubSubPushRequest.java
│ │ └── GooglePlayPurchaseVerifyReq.java
│ ├── entity/googleplay/
│ │ ├── GooglePlayOrder.java
│ │ ├── GooglePlayPurchaseToken.java
│ │ ├── GooglePlayRtdnEvent.java
│ │ └── GooglePlayUserEntitlement.java
│ └── vo/googleplay/
│ └── GooglePlayPurchaseVerifyResp.java
└── service/
├── GooglePlayBillingService.java
└── impl/
└── GooglePlayBillingServiceImpl.java
建表 SQL
- 文件:
src/main/resources/sql/google-play-iap.sql
四张表职责:
google_play_order:每一笔 Google 订单/续费周期的最终状态与发货状态。google_play_purchase_token:一个 purchase token 的最新状态快照。google_play_user_entitlement:用户实际拥有的本地权益。google_play_rtdn_event:RTDN 审计与重试记录。
接口定义
1. 客户端购买校验
POST /api/google-play/purchases/verify- 需要登录态与签名
请求体:
{
"packageName": "com.example.app",
"productId": "vip_monthly",
"productType": "subscription",
"purchaseToken": "xxxx"
}
响应体:
{
"code": 0,
"message": "ok",
"data": {
"userId": 1001,
"productId": "vip_monthly",
"productType": "SUBSCRIPTION",
"purchaseToken": "xxxx",
"orderId": "GPA.1234-5678-9012-34567",
"orderState": "ACTIVE",
"entitlementState": "ACTIVE",
"deliveryStatus": "NOT_REQUIRED",
"accessGranted": true,
"acknowledged": true,
"consumed": false,
"expiryTime": "2026-04-18T10:00:00.000+00:00",
"lastSyncedAt": "2026-03-18T10:01:00.000+00:00"
}
}
2. RTDN Webhook
POST /api/google-play/rtdn- 不需要登录态,不走 app 签名,单独做 Pub/Sub 校验
Pub/Sub Push body:
{
"message": {
"messageId": "136969346945",
"data": "base64-encoded-json"
},
"subscription": "projects/your-project/subscriptions/google-play-rtdn-push"
}
关键请求头:
Authorization: Bearer <OIDC JWT>X-Goog-Topic: projects/your-project/topics/google-play-rtdn
成功响应:
{
"code": 0,
"message": "ok",
"data": true
}
RTDN 状态处理
订阅
-
SUBSCRIPTION_PURCHASED- Developer API 状态通常为
ACTIVE - 新建/更新 token、order、VIP 权益
- 如未 acknowledge,则服务端补 acknowledge
- Developer API 状态通常为
-
SUBSCRIPTION_RENEWED- 再查
subscriptionsv2.get - 取最新
expiryTime覆盖本地 VIP 到期时间 - 不做“在旧时间上加时长”,避免 RTDN 重放导致多发
- 再查
-
SUBSCRIPTION_CANCELED- 若
subscriptionState=CANCELED但expiryTime仍未来 - 本地仍保留 VIP,
autoRenewEnabled=false - 到期后等待
EXPIRED
- 若
-
SUBSCRIPTION_IN_GRACE_PERIOD- 本地继续保留 VIP
- entitlement state 记为
IN_GRACE_PERIOD
-
SUBSCRIPTION_ON_HOLD- 立即取消本地 VIP 可用态
- 保留 token/order 记录,等待恢复或过期
-
SUBSCRIPTION_RESTARTED/SUBSCRIPTION_RECOVERED- 再查最新状态
- 若恢复为
ACTIVE,重新开启 VIP
-
SUBSCRIPTION_PAUSED- 本地取消 VIP 可用态
- 记录
autoResumeTime
-
SUBSCRIPTION_EXPIRED- 本地关闭 VIP
- entitlement state ->
EXPIRED
-
SUBSCRIPTION_REVOKED- 本地关闭 VIP
- entitlement state ->
REVOKED
-
SUBSCRIPTION_PENDING_PURCHASE_CANCELED- 用当前 token 查到
linkedPurchaseToken后,再同步旧 token - 旧订阅权益继续保持
- 用当前 token 查到
一次性商品
-
ONE_TIME_PRODUCT_PURCHASED- 若映射为钱包充值:仅当
delivery_status != DELIVERED才入账 - 若映射为一次性 VIP 或非消耗型权益:同样只发一次
- 钱包型商品在发货后执行 consume
- 非消耗型商品执行 acknowledge
- 若映射为钱包充值:仅当
-
ONE_TIME_PRODUCT_CANCELED- 一般是 pending 订单被取消
- 本地标记为
CANCELED,不发货
-
VoidedPurchaseNotification- 先二次查询 Developer API;若 404,则结合本地 token/order 记录做最终回滚
- 订阅按
REVOKED - 一次性商品按
REFUNDED - 钱包余额不足以回滚时,订单标记
MANUAL_REVIEW,日志显式报错
安全校验
- RTDN 入口校验
X-Goog-Topic - 校验
subscription字段 - 调用
tokeninfo校验 Pub/Sub OIDC JWT - 校验
aud - 校验
email与email_verified - 校验
iss必须是 Google Accounts - 客户端校验接口可要求
obfuscatedExternalAccountId == userId
测试样例
建议执行:
mvn -q test -Dtest=GooglePlay*
覆盖点:
- 订阅首次购买与续费
- 订阅取消但未过期
- 订阅宽限期
- 一次性商品发货幂等
- RTDN 重放幂等