ok
This commit is contained in:
169
plans/callback_log_验签顺序_537cb63e.plan.md
Normal file
169
plans/callback_log_验签顺序_537cb63e.plan.md
Normal file
@@ -0,0 +1,169 @@
|
||||
---
|
||||
name: callback_log 验签顺序
|
||||
overview: 修订需求文档与 slot-pwa 实现:将「完整 callback_log 落库」从「收包第一时间」调整为「验签通过后」;验签失败仅保留应用日志,避免未授权请求刷写分库分表。
|
||||
todos:
|
||||
- id: update-docs-02
|
||||
content: 修订 02_provider_callback_log.md:流程图、§5 衔接、§6 验收、安全说明
|
||||
status: completed
|
||||
- id: update-docs-parent
|
||||
content: 同步 game_gateway_risk_prd_v_2.md §4.3/§4.4 流程顺序
|
||||
status: completed
|
||||
- id: reorder-middleware
|
||||
content: slot-pwa:POPAuth 在前,ProviderCallbackLog 在后;POPAuth 去掉 DB 验签回写
|
||||
status: in_progress
|
||||
- id: insert-verify-passed
|
||||
content: insertReceived 验签通过态写入;清理 markVerifyPassed/Failed 在 POP 路径的调用
|
||||
status: pending
|
||||
- id: tests-verify
|
||||
content: 更新/补充 callback_log 相关测试并跑 verify-slot-backend.sh
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# callback_log 改为验签通过后落库
|
||||
|
||||
## 问题判断
|
||||
|
||||
你的顾虑成立。当前设计与实现是:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant P as Provider
|
||||
participant L as ProviderCallbackLogMiddleware
|
||||
participant A as POPAuthMiddleware
|
||||
participant C as Controller
|
||||
|
||||
P->>L: HTTP callback
|
||||
L->>L: INSERT callback_log verify_status=0
|
||||
L->>A: next
|
||||
A->>A: verify signature
|
||||
alt verify fail
|
||||
A->>L: UPDATE verify_failed
|
||||
A-->>P: 401 JSON
|
||||
else verify pass
|
||||
A->>C: next
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
|
||||
- 需求:[02_provider_callback_log.md](docs/requirements/game_gateway/02_provider_callback_log.md) §3.1 / §5 要求「先 INSERT,再验签」
|
||||
- 实现:[middleware.php](slot-pwa/config/middleware.php) 中 `ProviderCallbackLogMiddleware` 排在 `POPAuthMiddleware` **之前**;[ProviderCallbackLogMiddleware.php](slot-pwa/app/middleware/ProviderCallbackLogMiddleware.php) 注释也写明「须在验签之前」
|
||||
- 验收 §6.2 要求「验签失败仍有 callback_log」——与防刷目标冲突
|
||||
|
||||
**原设计意图(合理但需边界)**:把每一次到达网关的请求都当作「证据」,包括验签失败,便于和 Provider 对账「对方声称发过回调」。代价是:公开 URL + 无 IP 白名单/限流时,攻击者可对 `user_transaction_*.game_provider_callback_log_`* 灌入大量 `MEDIUMTEXT`(`raw_body`/`raw_headers`)。
|
||||
|
||||
**你已确认的产品取舍**:验签通过后才 INSERT **完整** callback_log;验签失败不落库(仅应用日志;后续可选轻量 reject 表,本期不做)。
|
||||
|
||||
这与资金安全主链路一致:父文档 [game_gateway_risk_prd_v_2.md](docs/requirements/game_gateway_risk_prd_v_2.md) §5.1 将「验签 / IP 校验」的目的写明为「防止非法回调」——应先挡住非法流量,再落审计库。
|
||||
|
||||
---
|
||||
|
||||
## 目标流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant P as Provider
|
||||
participant A as POPAuthMiddleware
|
||||
participant L as ProviderCallbackLogMiddleware
|
||||
participant C as Controller
|
||||
|
||||
P->>A: HTTP callback
|
||||
A->>A: verify signature
|
||||
alt verify fail
|
||||
A->>A: structured error log only
|
||||
A-->>P: auth error JSON
|
||||
else verify pass
|
||||
A->>L: next
|
||||
L->>L: INSERT callback_log verify_status=1
|
||||
L->>C: next
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
|
||||
要点:
|
||||
|
||||
- **验签失败**:`Log::error` 保留 `route`、`IP`、`body_hash`(可选)、`error`;**不**调用 `markVerifyFailed`(因无 `log_id`)
|
||||
- **验签通过**:`insertReceived` 时直接 `verify_status = VERIFY_PASSED(1)`,可删除事后 `markVerifyPassed` 的 UPDATE(减少一次写)
|
||||
- **解析失败**:仍在验签通过且已落库后,由 Logic 更新 `parse_status` / `process_status`(现有 `markParseFailed` 等不变)
|
||||
|
||||
---
|
||||
|
||||
## 文档修订(必须)
|
||||
|
||||
### 1. [02_provider_callback_log.md](docs/requirements/game_gateway/02_provider_callback_log.md)
|
||||
|
||||
|
||||
| 章节 | 修改 |
|
||||
| ------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| §1.2 In scope | 「第一时间落库」改为「**验签通过后**同步落库完整原始请求」 |
|
||||
| §3.1 流程图 | `Provider callback → 验签 → 同步写 callback_log(verify_status=1) → 解析 → …` |
|
||||
| §5 衔接表 | 「收到任意回调先 INSERT」改为「验签通过后 INSERT」;删除「验签完成再 UPDATE verify_status」作为主路径(保留解析/处理状态更新) |
|
||||
| §6 验收 | **改写 §6.2**:验签失败不落 callback_log,但应用日志可按 `request_id`/时间检索;**新增**:任意成功进入业务链路的 `provider_tx` 仍可 `callback_log_id` 追溯 `raw_body` |
|
||||
| 新增 §(安全说明) | 明确:防刷靠「验签门禁 +(建议)网关 IP 白名单/限流」;若未来需要攻击审计,用独立 reject 表或日志平台,不污染主 callback_log |
|
||||
|
||||
|
||||
### 2. [game_gateway_risk_prd_v_2.md](docs/requirements/game_gateway_risk_prd_v_2.md)
|
||||
|
||||
- §4.3 / §4.4 流程图中「同步写 callback_log」节点移到「验签」**之后**(与 02 子文档一致)
|
||||
|
||||
---
|
||||
|
||||
## 代码修订(slot-pwa)
|
||||
|
||||
### 1. 调整 Middleware 顺序
|
||||
|
||||
[config/middleware.php](slot-pwa/config/middleware.php) `pop` 组改为:
|
||||
|
||||
```php
|
||||
'pop' => [
|
||||
\app\middleware\POPAuthMiddleware::class,
|
||||
\app\middleware\ProviderCallbackLogMiddleware::class,
|
||||
],
|
||||
```
|
||||
|
||||
### 2. [POPAuthMiddleware.php](slot-pwa/app/middleware/POPAuthMiddleware.php)
|
||||
|
||||
- 验签失败:仅 `Log::error` + 返回 JSON;**移除** `markVerifyFailed`(依赖已落库的 `log_id`)
|
||||
- 验签成功:**移除** `markVerifyPassed`;直接 `$handler($request)` 进入下一 Middleware 落库
|
||||
- 平台配置缺失:同样不落库,只打日志返回错误
|
||||
|
||||
### 3. [ProviderCallbackLogMiddleware.php](slot-pwa/app/middleware/ProviderCallbackLogMiddleware.php)
|
||||
|
||||
- 更新类注释:在验签 Middleware **之后**执行,仅对验签通过的请求落库
|
||||
- `recordReceivedFromRequest` 失败时:当前 catch 后仍 `return $handler`——建议改为 **记录错误并返回 5xx**(避免无 callback_log 却继续扣款);与产品确认可放在实现阶段一并收紧
|
||||
|
||||
### 4. [ProviderCallbackLogService.php](slot-pwa/app/service/game/ProviderCallbackLogService.php) + [GameProviderCallbackLogModel.php](slot-pwa/app/model/shard/GameProviderCallbackLogModel.php)
|
||||
|
||||
- `insertReceived` 增加参数或拆方法:`insertReceivedAfterVerifyPassed()`,写入时 `verify_status = VERIFY_PASSED`
|
||||
- `markVerifyPassed` / `markVerifyFailed`:保留方法但标注「仅兼容旧路径」或删除未引用调用(POP 路径不再使用 failed 回写)
|
||||
- `recordReceivedFromRequest` 仅在验签后 Middleware 调用,语义与命名可在 PHPDoc 中写清
|
||||
|
||||
### 5. 测试
|
||||
|
||||
- [GameProviderCallbackLogShardModelTest.php](slot-pwa/tests/Feature/GameProviderCallbackLogShardModelTest.php) / [ProviderCallbackLogTestCase.php](slot-pwa/tests/Support/ProviderCallbackLogTestCase.php):若直接测 `insertReceived`,默认 `VERIFY_PENDING` 仍可用于 Model 单测;新增或调整用例覆盖「验签后插入为 PASSED」
|
||||
- 可选:Middleware 顺序集成测试(验签失败不触发 insert)——若项目已有 HTTP 测试基建则补一条
|
||||
|
||||
---
|
||||
|
||||
## 不在本期但必须知晓的风险
|
||||
|
||||
|
||||
| 风险 | 缓解 |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| 验签失败无 DB 记录,与 Provider 扯皮时只能靠日志 | 日志字段标准化:`provider_code`、`source_ip`、`body_hash`、`x-request-id`;日志保留期与 Provider 对齐 |
|
||||
| CPU 层仍可被刷验签 | 网关层 Provider IP 白名单 + rate limit(nginx/云 WAF);本期文档 § 安全说明中写为 **建议项** |
|
||||
| `uid=0` 分片仍可能被合法但解析失败的请求写入 | 仅发生在验签通过后,可接受;与刷库攻击不同类 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 实施顺序建议
|
||||
|
||||
1. 先改 02 子文档 + 父文档流程图(避免实现与需求再漂移)
|
||||
2. 调整 middleware 顺序与 POP / CallbackLog 职责
|
||||
3. Model/Service 插入时带 `VERIFY_PASSED`
|
||||
4. `按php-clean-code.mdc 文件验证代码规范`
|
||||
5. 跑 `verify-slot-backend.sh` + 相关 PHPUnit
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
---
|
||||
name: 首充剩余分档规则
|
||||
overview: 将 Free Credits「剩余金额(定格额 − 第一档 $20)」从固定 package_amount 均分,改为按 10/20/30/50/100 元阶梯瀑布拆分;后续档解锁改为「累计充值 ≥ 每档门槛(标准档=面额−$1;尾档 unlock 按大单位整数向下取整,如 15340qf→15000qf)」。
|
||||
overview: 将 Free Credits 剩余金额改为 10/20/30/50/100 瀑布分档;后续档解锁为「每档独立的新增累计充值」(自上一档解锁后重新计数,非终身 totalR 一刀切);尾档 unlock 按大单位整元向下取整。
|
||||
todos:
|
||||
- id: split-algorithm
|
||||
content: 实现 buildReleasePackages 瀑布拆分,替换 splitReleasePackageAmounts + createPackages
|
||||
status: pending
|
||||
status: completed
|
||||
- id: db-unlock-qf
|
||||
content: free_credits_package 增加 unlock_recharge_qf;install.sql + 迁移脚本
|
||||
status: pending
|
||||
content: package 增 unlock_recharge_qf(本档所需新增累计充值);player 增 recharge_baseline_qf;迁移脚本
|
||||
status: completed
|
||||
- id: advance-unlock
|
||||
content: advanceByRecharge 按累计充值+unlock_recharge_qf 解锁;存量 unlock=0 回退旧逻辑
|
||||
status: pending
|
||||
content: advanceByRecharge 用 (totalR−baseline)≥unlock 逐档解锁并重置 baseline;存量回退旧逻辑
|
||||
status: completed
|
||||
- id: admin-config
|
||||
content: 废弃 package_amount/subsequent_min 后台必填;更新 ActivityValidate
|
||||
status: pending
|
||||
status: completed
|
||||
- id: tests-docs
|
||||
content: 单测/集成测 + 需求文档 §5.7/5.8 更新
|
||||
status: pending
|
||||
status: completed
|
||||
isProject: false
|
||||
---
|
||||
|
||||
@@ -38,14 +38,16 @@ isProject: false
|
||||
|
||||
按顺序「吃掉」剩余金额(瀑布式,非按总额选一档):
|
||||
|
||||
| 阶段 | 单档面额 | 本阶段最多档数 | 累计充值解锁门槛(每档) |
|
||||
|------|---------|---------------|------------------------|
|
||||
| 1 | $10 | 3 | ≥ $9 |
|
||||
| 2 | $20 | 5 | ≥ $19 |
|
||||
| 3 | $30 | 10 | ≥ $29 |
|
||||
| 4 | $50 | 10 | ≥ $49 |
|
||||
| 5 | $100 | 直到剩余 < $100 | ≥ $99 |
|
||||
| 6 | 尾档 | 剩余 < $100 的余额,**一整档**(`amount_qf` 可为小数大单位,如 $15.34) | **累计充值门槛 = 面额按大单位整数向下取整**(见下) |
|
||||
| 阶段 | 单档面额 | 本阶段最多档数 | 本档所需**新增**累计充值(自上一档解锁后起算) |
|
||||
|------|---------|---------------|------------------------------------------|
|
||||
| 1 | $10 | 3 | 每档再充 ≥ $9 |
|
||||
| 2 | $20 | 5 | 每档再充 ≥ $19 |
|
||||
| 3 | $30 | 10 | 每档再充 ≥ $29 |
|
||||
| 4 | $50 | 10 | 每档再充 ≥ $49 |
|
||||
| 5 | $100 | 直到剩余 < $100 | 每档再充 ≥ $99 |
|
||||
| 6 | 尾档 | 剩余 < $100 的一整档 | 再充 ≥ **面额整元向下取整**(见下) |
|
||||
|
||||
**重要**:表中金额为「每一档单独重新累计」的充值要求,**不是**钱包终身 `totalRecharge` 达到该绝对值。上一档变为 `ready` 后,充值计数归零,从当前 `totalRecharge` 重新累计下一档。
|
||||
|
||||
金额在代码中为 **千分位整数(qf)**:$10 → `10000`。
|
||||
|
||||
@@ -70,7 +72,7 @@ unlock_recharge_qf = intdiv(amount_qf, 1000) * 1000 // 去掉 qf 余数,等
|
||||
|
||||
| amount_qf(小单位/千分位) | 档位展示面额 | unlock_recharge_qf(累计充值门槛) |
|
||||
|-------------------------|-------------|-----------------------------------|
|
||||
| `15340` | $15.34 | `15000`($15,非 $15.34) |
|
||||
| `15340` | $15.34 | `15000`(本档需**新增**累计充 $15,非 $15.34) |
|
||||
| `8500` | $8.50 | `8000`($8) |
|
||||
| `10000` | $10.00 | `10000`($10) |
|
||||
|
||||
@@ -108,31 +110,58 @@ public static function buildReleasePackages(int $leftAmountQf): array
|
||||
|
||||
`createPackages()` 改为遍历上述结构写入 `free_credits_package`,不再读 `package_amount` 配置。
|
||||
|
||||
### 2. 库表:每档解锁门槛
|
||||
### 2. 库表:本档门槛 + 充值基线
|
||||
|
||||
[`free_credits_package`](slot_console/db/install.sql) 当前无解锁门槛字段。建议新增:
|
||||
**`free_credits_package`** 新增:
|
||||
|
||||
```sql
|
||||
unlock_recharge_qf bigint unsigned NOT NULL DEFAULT 0 COMMENT '解锁本档所需累计真实充值,千分位'
|
||||
unlock_recharge_qf bigint unsigned NOT NULL DEFAULT 0
|
||||
COMMENT '解锁本档所需新增累计真实充值(千分位),自上一档解锁后起算'
|
||||
```
|
||||
|
||||
- 定格写入时一并落库
|
||||
- **存量用户**(默认策略):已有行 `unlock_recharge_qf=0` 时,`advanceByRecharge` 回退旧逻辑(`subsequent_min_recharge` + 单笔充值),避免行为突变
|
||||
- **新定格**:一律写入新门槛
|
||||
**`free_credits_player`** 新增:
|
||||
|
||||
(若产品确认「存量也迁移」,另做一次性 SQL/脚本按 player 重算 package 行 — 需单独评估已解锁/已领取档。)
|
||||
```sql
|
||||
recharge_baseline_qf bigint unsigned NOT NULL DEFAULT 0
|
||||
COMMENT '后续档计数起点:wallet.totalRecharge 快照(千分位)'
|
||||
```
|
||||
|
||||
### 3. 解锁逻辑(`advanceByRecharge`)
|
||||
- 定格时:写入各档 `unlock_recharge_qf`;`recharge_baseline_qf = 定格时 wallet.totalRecharge`(首笔 release 档从定格后**新增**充值开始计)
|
||||
- **存量**(grandfather):`unlock_recharge_qf=0` 的包仍走旧逻辑(单笔 `subsequent_min_recharge`)
|
||||
|
||||
后续 release 档变更:
|
||||
### 3. 解锁逻辑(`advanceByRecharge`)— 每档「新的」累计充值
|
||||
|
||||
第一档(免打码提现)**不变**:仍用终身 `totalRecharge >= recharge_unlock_amount`(默认 $50)。
|
||||
|
||||
后续 release 档:
|
||||
|
||||
```text
|
||||
incrementalRecharge = wallet.totalRecharge - player.recharge_baseline_qf
|
||||
若 incrementalRecharge >= nextPackage.unlock_recharge_qf → 解锁该档为 ready
|
||||
→ recharge_baseline_qf = wallet.totalRecharge // 下一档重新累计
|
||||
```
|
||||
|
||||
循环至多 `max_unlock_per_recharge` 次(同一笔充值回调内可连续解锁多档,但每解锁一档都会 **重置 baseline**,故单笔 $27 通常只够解锁一档 $9 档)。
|
||||
|
||||
| 项 | 旧 | 新 |
|
||||
|----|----|-----|
|
||||
| 条件 | `rechargeAmount >= subsequent_min` | `totalRecharge >= package.unlock_recharge_qf` |
|
||||
| 顺序 | 仍按 `package_no` 逐档 | 不变 |
|
||||
| 单笔上限 | `max_unlock_per_recharge` | **保留**(大额充值仍最多解锁 N 档) |
|
||||
| 条件 | 本笔 `rechargeAmount >= subsequent_min` | `(totalR - baseline) >= unlock_recharge_qf` |
|
||||
| 计数范围 | 单笔 | **档间新增累计**(解锁后 baseline 前移) |
|
||||
| 顺序 | `package_no` 升序 | 不变 |
|
||||
|
||||
第一档逻辑不变(仍用 `recharge_unlock_amount`,默认 $50)。
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Wallet
|
||||
participant Logic
|
||||
participant Player
|
||||
Wallet->>Logic: totalRecharge=50000
|
||||
Note over Player: baseline=41000
|
||||
Logic->>Logic: incremental=9000 >= unlock 9000
|
||||
Logic->>Player: baseline=50000
|
||||
Note over Logic: 下一档需再新增累计充值
|
||||
```
|
||||
|
||||
**与「领取 Claim」**:Claim 仍要求档位 `ready`;解锁条件只影响何时变 `ready`,不改变 Claim 入账逻辑。
|
||||
|
||||
### 4. 配置与后台
|
||||
|
||||
@@ -154,7 +183,10 @@ unlock_recharge_qf bigint unsigned NOT NULL DEFAULT 0 COMMENT '解锁本档所
|
||||
|
||||
集成测 [`FreeCreditsFirstRechargeFreezeTest`](slot_console/tests/Integration/FreeCreditsFirstRechargeFreezeTest.php) 断言 package 行数与金额。
|
||||
|
||||
解锁:在 harness 中 mock `totalRecharge`,验证 `unlock_recharge_qf` 达标后变 `ready`。
|
||||
解锁单测(增量累计):
|
||||
- baseline=41000,totalR=50000,unlock=9000 → 解锁;baseline 更新为 50000
|
||||
- 同上后再充到 59000 → 第二档 unlock=9000 可解锁;一笔 50000→59000 只够第二档若 baseline 已重置
|
||||
- 尾档 unlock=15000,amount=15340:需新增累计 15000,非 15340
|
||||
|
||||
### 7. 文档
|
||||
|
||||
|
||||
Reference in New Issue
Block a user