ok
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
项目背景:
|
||||
- 项目使用 PHP 8.1+
|
||||
- 主要业务目录包括 slot_*、backend/**
|
||||
- 主要业务目录包括 slot*、backend/**
|
||||
- 本项目要求新增或修改的 class / interface / trait / enum、方法、类常量、新增属性都必须有合格的 PHPDoc
|
||||
- PHP 8+ 已经有类型声明时,@param / @return 仍然必须保留
|
||||
|
||||
|
||||
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. 文档
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,46 +2,46 @@
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"babysit": {
|
||||
"lastSyncedAt": 1780048673498
|
||||
"lastSyncedAt": 1780053556720
|
||||
},
|
||||
"canvas": {
|
||||
"lastSyncedAt": 1780048673498
|
||||
"lastSyncedAt": 1780053556720
|
||||
},
|
||||
"create-hook": {
|
||||
"lastSyncedAt": 1780048673498
|
||||
"lastSyncedAt": 1780053556720
|
||||
},
|
||||
"create-rule": {
|
||||
"lastSyncedAt": 1780048673498
|
||||
"lastSyncedAt": 1780053556720
|
||||
},
|
||||
"create-skill": {
|
||||
"lastSyncedAt": 1780048673498
|
||||
"lastSyncedAt": 1780053556720
|
||||
},
|
||||
"create-subagent": {
|
||||
"lastSyncedAt": 1780048673498
|
||||
"lastSyncedAt": 1780053556720
|
||||
},
|
||||
"sdk": {
|
||||
"lastSyncedAt": 1780048673498
|
||||
"lastSyncedAt": 1780053556720
|
||||
},
|
||||
"migrate-to-skills": {
|
||||
"lastSyncedAt": 1780048673498
|
||||
"lastSyncedAt": 1780053556720
|
||||
},
|
||||
"shell": {
|
||||
"lastSyncedAt": 1780048673498
|
||||
"lastSyncedAt": 1780053556720
|
||||
},
|
||||
"split-to-prs": {
|
||||
"lastSyncedAt": 1780048673498
|
||||
"lastSyncedAt": 1780053556720
|
||||
},
|
||||
"statusline": {
|
||||
"lastSyncedAt": 1780048673498
|
||||
"lastSyncedAt": 1780053556720
|
||||
},
|
||||
"update-cli-config": {
|
||||
"lastSyncedAt": 1780048673498
|
||||
"lastSyncedAt": 1780053556720
|
||||
},
|
||||
"update-cursor-settings": {
|
||||
"lastSyncedAt": 1780048673498
|
||||
"lastSyncedAt": 1780053556720
|
||||
},
|
||||
"loop": {
|
||||
"lastSyncedAt": 1780048673498
|
||||
"lastSyncedAt": 1780053556720
|
||||
}
|
||||
},
|
||||
"lastInventoryAt": 1780014510207
|
||||
|
||||
Reference in New Issue
Block a user