feat(commerce): B1-2 恢复购买/退款拒付回收/对账真实比对 + Apple 端口预留
为什么:风险清单 R-03 把"票据校验 + 对账打通"列为上线阻塞项。勘察发现 ReconcileService 与 /admin/reconcile 两个接口 B1 之前就已实现,真缺口是:对账没跟商店比对、差额用 0 兜底(违反 "取不到就用 null"的契约)、退款回收只有人工入口且不幂等。按实际缺口做,不重复造。 - 恢复购买:补专项测试(换设备重提同一票据 → 同一订单、原交付时间不改写、不新增流水); 补单能力沿用 B1-1 的服务端建单路径,恢复列表沿用 GET /app/orders,不新增接口 - 退款/拒付回收:GooglePlayPurchaseClient 增加 listVoidedPurchases(purchases.voidedpurchases, 带分页);新增 StoreRefundService 拉取并按 (platform, orderId) 幂等回收,本地无单计入差异; OrderService 增加 refundByStoreCallback(已退款直接返回,不二次回收/不写第二条流水), 人工 refund() 的既有契约(已退款报错)保持不变 - 对账:reconcile_task 增 store_amount_sum(可空)与 notes 两列(幂等 ALTER); run() 改为集合比对(本地缺票据 + 票据无本地单 + 商店已作废但本地无单),并在同一次 run 里 顺带执行退款回收;storeAmountSum 恒为 null 并在 notes 说明需从 Play Console 财务报告获取; 商店调用失败置 status=FAILED 且不写 SUCCESS+0 - Apple IAP:只留端口/枚举与明确提示,不实现真调用(B2 再实现) - 契约同步 docs/api-phase1.md §1.8/§2.6;新增测试 17 条(commerce 14 + admin-api 3) - 实测:mvn test 71 条全过(commerce 27 + admin-api 3 + app-api 41,B1-1 的 54 条未被打红); 两个入口 package 通过;端到端手动对账返回 storeAmountSum=null + notes 说明缺什么
This commit is contained in:
1 parent
db897d50d6
commit
1dc97b4bf6
17 files changed
+971
-21
No files matched your search
@@ -256,6 +256,10 @@ POST /app/auth/register
|
||||
重复/并发提交由数据库兜底;票据审计写 `store_receipt`(唯一索引 `uk_store_receipt(platform, transaction_id)`),
|
||||
B1-2 的退款/拒付复验依赖它。服务端凭据(`packageName` + service account JSON)走配置/环境变量,不入库。
|
||||
|
||||
**恢复购买**:客户端换设备后重新提交同一个 `purchaseToken`(或调用 `GET /app/orders` 拉已购列表),
|
||||
服务端按 `(platform, storeTransactionId)` 识别"权益已发过",**返回既有订单、不重复发币**;
|
||||
若商店侧有购买而本地没有订单(例如换机前没同步),会**按校验结果补单落库**再发放,不需要人工介入。
|
||||
|
||||
### 1.9 创作者(下一批)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
@@ -417,6 +421,23 @@ B1-2 的退款/拒付复验依赖它。服务端凭据(`packageName` + service
|
||||
| POST | `/admin/wallets/coins` | `wallet:update` | 人工补币/扣回,必须带幂等键,先校验用户存在 |
|
||||
| POST | `/admin/reconcile/run` · GET `/admin/reconcile/page` | `reconcile:run` / `order:list` | 手动对账与历史(默认每天 04:20 自动跑前一天) |
|
||||
|
||||
**对账口径(B1-2 起生效,字段名不变,新增两个字段)**
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| `totalCount` | 当期本地有效订单数(排除 `CREATED`/`CLOSED`) |
|
||||
| `matchedCount` | 其中能在 `store_receipt` 找到同一 `(platform, transaction_id)` 的笔数 |
|
||||
| `diffCount` | 差异笔数 = 本地缺票据 + 票据无本地订单 + **商店已作废但本地无单** |
|
||||
| `amountSum` | 本地订单金额合计(**本地口径**,不是商店结算口径) |
|
||||
| `storeAmountSum` | 商店侧结算金额。Developer API 不提供(要向 Play Console 财务报告取)→ **无数据源时为 `null`** |
|
||||
| `notes` | 说明差异构成、本次退款回收情况、以及"缺哪些数据源"。**契约强制:取不到的指标用 `null` + `notes` 说明,不许用 0 冒充** |
|
||||
| `status` | 商店侧调用失败时为 `FAILED`(不会写成 `SUCCESS` + 一堆 0) |
|
||||
|
||||
`POST /admin/reconcile/run` 同时会**拉取商店已作废交易并回收权益与金币**(退款/拒付回收),
|
||||
回收是幂等的:同一笔作废交易重复出现在时间窗里,只回收一次。
|
||||
口径细节:Google 一次性商品的作废是**整笔**,拿不到部分金额,因此按整单回收;
|
||||
找不到本地订单的作废交易会计入 `diffCount` 并在 `notes` 里点名。
|
||||
|
||||
### 2.7 创作者与内容审核(已实现)
|
||||
|
||||
| 方法 | 路径 | 权限点 | 说明 |
|
||||
|
||||
Reference in new issue
Block a user