feat(commerce): Google Play 票据校验闭环(校验→建单→发权益 + 幂等/防重放)
为什么:风险清单 R-03 把支付票据校验列为上线阻塞项;/app/orders/verify 此前只有 disabled 占位,且 verifyAndDeliver 要求订单事先存在——而 IAP 没有下单接口,接了校验也走不通。 - 新增 GooglePlayStoreVerifier(校验 purchaseState/productId/orderId)+ StorePlatforms(平台归一化) - 新增 GooglePlayPurchaseClient 端口与 HttpGooglePlayPurchaseClient:JDK HttpClient + 服务账号 RS256 JWT 换 token,不引入 Google SDK 依赖;token 按过期缓存 - StoreVerifier 实现由 fruitshorts.commerce.store.verifier 选择,默认 disabled,无凭据时对外行为不变 - OrderService 补齐闭环:校验通过后按 (platform, storeProductId) 找商品建单、写 store_receipt 审计、 发金币并推进到 DELIVERED;幂等键 (platform, storeTransactionId) 与唯一索引双保险 - 防重放:重复提交同用户返回既有订单且不再发币;票据已被他人使用返回 403;校验失败不写任何数据 - 凭据只走配置/环境变量(packageName + service account JSON),不入库不提交 - 契约同步 docs/api-phase1.md §1.8;金额微单位换算与回退价有单测覆盖 - 实测:mvn test 54 条全过(commerce 13 + app-api 41,M2 的 37 条未被打红); 两个入口 package 通过;默认 disabled 端到端返回 200003 且 orders 表零行
This commit is contained in:
1 parent
27e689c771
commit
db897d50d6
15 files changed
+1187
-24
No files matched your search
+31
-1
@@ -221,11 +221,41 @@ POST /app/auth/register
|
||||
| GET | `/app/wallet` | 金币余额 + 最近流水 |
|
||||
| POST | `/app/episodes/{id}/unlock` | 金币解锁分集;幂等键 `unlock:{userId}:{episodeId}`,重复调用不重复扣币 |
|
||||
| GET | `/app/orders` | 我的订单 |
|
||||
| POST | `/app/orders/verify` | 提交 IAP / Google Play 票据。**本轮只留端口不接商店**,会返回 `200003 尚未配置` |
|
||||
| POST | `/app/orders/verify` | 提交 Google Play 票据:服务端校验 → 建单 → 发权益(详见下方说明) |
|
||||
|
||||
解锁后播放入口与下载入口会同时放行 —— 权益判定由交易模块的 `EntitlementPort` 实现提供,
|
||||
播放域与下载域都不需要改代码。金币变动一律带幂等键并写 `coin_ledger`,钱包余额与流水同事务。
|
||||
|
||||
**`POST /app/orders/verify`(B1-1:Google Play 票据校验闭环)**
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{ "platform": "GOOGLE_PLAY", "productId": "coin_550", "receipt": "<purchaseToken>" }
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `platform` | `GOOGLE` / `GOOGLE_PLAY`(两者等价,入库统一记 `GOOGLE`);Apple 走 B1-2 |
|
||||
| `productId` | 商店商品 id(Google Play 的 `purchases.products.get` 必需),与本平台 `product.store_product_id` 对得上才发权益 |
|
||||
| `receipt` | Google Play 的 `purchaseToken` |
|
||||
|
||||
行为与错误码:
|
||||
|
||||
| 场景 | 返回 |
|
||||
| --- | --- |
|
||||
| 校验通过 | `code=0`,`data` 是订单(`status=DELIVERED`、`coinAmount`=商品金币+赠送、`storeTransactionId`=Google `orderId`) |
|
||||
| 同一票据**重复**提交(同用户) | `code=0` 返回**既有订单**,不重复发币、不重复建单 |
|
||||
| 票据已绑定**其他账号** | `403 该票据已绑定其他账号,不能重复使用`(防跨用户重放) |
|
||||
| 票据无效 / 未购买 / 商品不一致 / 缺 orderId | `200003` + 具体原因;**不产生订单、不发权益** |
|
||||
| 商品未在后台配置 | `200002 该商品未在后台配置` |
|
||||
| 平台不在支持范围 | `400 不支持的商店平台` |
|
||||
| 商店未配置(默认 `verifier=disabled`) | `200003 Apple / Google 商店校验尚未配置`(与 B1-1 之前的行为一致) |
|
||||
|
||||
幂等键是 `(platform, store_transaction_id)`(DB 唯一索引 `uk_orders_store_txn`),
|
||||
重复/并发提交由数据库兜底;票据审计写 `store_receipt`(唯一索引 `uk_store_receipt(platform, transaction_id)`),
|
||||
B1-2 的退款/拒付复验依赖它。服务端凭据(`packageName` + service account JSON)走配置/环境变量,不入库。
|
||||
|
||||
### 1.9 创作者(下一批)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|
||||
Reference in new issue
Block a user