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:
Zhou Weidong committed 2026-10-16 11:03:33 +08:00
1 parent 27e689c771
commit db897d50d6
15 files changed
+1187 -24

No files matched your search

@@ -53,6 +53,16 @@ fruitshorts:
license-days: 30
wifi-only-default: true
max-quality: AUTO
commerce:
store:
# 商店票据校验实现:disabled = 明确报"未配置"(默认,无凭据时对外行为不变)
# google-play = 走 Google Play Developer API
verifier: ${STORE_VERIFIER:disabled}
# 下面两项属付费凭据:只从环境变量/配置注入,不入库、不提交、不打进镜像
google-play-package-name: ${GOOGLE_PLAY_PACKAGE_NAME:}
# 服务账号 JSON:文件路径 \| JSON 内容 \| base64(建议用环境变量给 base64)
google-play-service-account-json: ${GOOGLE_PLAY_SERVICE_ACCOUNT_JSON:}
google-play-timeout-seconds: 10
media:
root-dir: ./data/uploads
public-prefix: /files