--- name: Provider Callback Log overview: 在 slot-pwa 按分库分表(对齐 GameResultLogModel)落地 game_provider_callback_log_{NN},并实现 POP 回调全链路(收包落库 → 验签/解析/处理状态)。其余厂商后续 PR 扩展。 todos: - id: shard-ddl-model content: CreateSharedTable 增加 callback_log 分表 DDL;shard/GameProviderCallbackLogModel + Params + IdGenerator status: completed - id: callback-service content: ProviderCallbackLogService(按 shardUid 路由)+ config/game_gateway.php status: completed - id: pop-middleware content: ProviderCallbackLogMiddleware + pop 中间件链 + POPAuthMiddleware 验签回写 status: completed - id: pop-logic content: CashLogic/CashController parse/process 状态(含重复 tid、BALANCE) status: completed - id: tests content: Feature 测试(shard 写入/更新)+ docker phpunit;verify-slot-backend.sh status: completed isProject: false --- # Provider 原始回调日志(02)实现计划 ## 范围(已确认) - **本期**:分库分表 DDL + Shard Model/Service + POP 两条路由接入 + PHPUnit - **后续 PR**:gasea / quick / jsgame / bbgt / jdb **明确不做**(属 [03_provider_tx.md](docs/requirements/game_gateway/03_provider_tx.md)):`game_provider_tx_01`、wallet 改造、Round 聚合。 **文档与 DDL**:已与 [02 §4.1](docs/requirements/game_gateway/02_provider_callback_log.md)、[`slot-pwa/db/game_provider_callback_log_shard.sql`](slot-pwa/db/game_provider_callback_log_shard.sql) 对齐(无 `gateway_version`;含分库分表说明)。 --- ## 现状与约束 | 项 | 现状 | |---|---| | POP 路由 | [`config/route.php`](slot-pwa/config/route.php) `/pop/Cash/Get`、`/pop/Cash/TransferInOut` | | POP 验签 | [`POPAuthMiddleware`](slot-pwa/app/middleware/POPAuthMiddleware.php) 验签在 Controller 前 | | **分表参考(本期主参考)** | [`app/model/shard/GameResultLogModel.php`](slot-pwa/app/model/shard/GameResultLogModel.php) + [`CreateSharedTable`](slot-pwa/app/command/CreateSharedTable.php) | | 非分表参考 | [`GameLaunchSessionModel`](slot-pwa/app/model/game/GameLaunchSessionModel.php) 仅用于 Launch Session,**不**用于 callback_log | | ID 生成 | [`IdGenerator::nextGameResultId()`](slot-pwa/app/service/IdGenerator.php) 同款 Redis 序号 + 毫秒时间戳 | 需求文档表名 `game_provider_callback_log_01` 表示**逻辑表前缀**;物理表为 `user_transaction_{dbNN}.game_provider_callback_log_{tableNN}`(与 `game_result_log_{NN}` 同库族)。 --- ## 分库分表设计 ### 路由规则(对齐 GameResultLog) ```php // GameResultLogModel getPreDatabase() => 'user_transaction' getPreTable() => 'game_provider_callback_log' // 物理表 game_provider_callback_log_01..NN getShardKey() => 'uid' ``` - 构造:`new GameProviderCallbackLogModel($shardUid)`,由 [`ShardService`](slot-pwa/app/service/ShardService.php) `crc32($shardUid) % (库数×表数)` 定位库表。 - 写入:[`insertData()`](slot-pwa/app/model/shard/SelfBaseModel.php)(同 `TransactionLog::insertGameResult`)。 - 更新:[`updateByWhere()`](slot-pwa/app/model/shard/SelfBaseModel.php),条件必须含 `id` + `uid`(分片键),例如 `['id' => $id, 'uid' => $shardUid]`。 ### 分片键 `uid` 的选取(收包时) | 场景 | shardUid | |------|----------| | POP 已带 `userid` | `(int) post.userid` | | 尚未解析 / 验签失败但 body 无 uid | `0`(需求允许 `uid=0`,仍落同一分片桶,保留审计) | | 解析成功后 uid 与收包时不一致 | **不迁移分片**;仅 `UPDATE` 修正行内 `uid` 等业务字段(行已在收包分片,避免跨库搬家) | **Request 上下文**(Middleware 写入,Logic/Middleware 读取): - `providerCallbackLogId` — 雪花主键 - `providerCallbackShardUid` — 分片键,后续所有 UPDATE 必带 ### 与 `GameResultLog` 的差异 | 项 | GameResultLog | callback_log | |----|---------------|----------------| | 汇总库 `s_all.*` | 有 [`app/model/all/GameResultLogModel`](slot-pwa/app/model/all/GameResultLogModel.php) + MQ 同步 | **本期不做**(无跨分片报表需求;后续若要全局检索再加 DBSync) | | 字段 | 精简结果字段 | 需求全文:raw_body/headers、验签/解析/处理状态等 | --- ## 处理顺序 ```mermaid sequenceDiagram participant Provider participant LogMW as ProviderCallbackLogMiddleware participant PopAuth as POPAuthMiddleware participant Logic as CashLogic participant Shard as user_transaction_XX.game_provider_callback_log_YY Provider->>LogMW: POST LogMW->>LogMW: resolveShardUid from post LogMW->>Shard: INSERT id=nextProviderCallbackLogId LogMW->>PopAuth: request + logId + shardUid PopAuth->>Shard: UPDATE verify_status PopAuth->>Logic: handler Logic->>Shard: UPDATE parse/process ``` --- ## 1. 数据库(分表 DDL) ### 修改 [`CreateSharedTable`](slot-pwa/app/command/CreateSharedTable.php) 在 `createShareTable()` 循环内、`createGameResultTable()` 之后增加 `createGameProviderCallbackLogTable($db, $num)`: - 库:`user_transaction_01` … `user_transaction_{share_database_num}` - 表:`game_provider_callback_log_01` … `game_provider_callback_log_{share_table_num}` - 字段/索引以 [02 §4.1](docs/requirements/game_gateway/02_provider_callback_log.md) 为准,**除外**:不建 `gateway_version`、不建 `idx_gateway_version`;其余 KEY(`idx_provider_tx`、`idx_round`、`idx_uid_received`、`idx_body_hash`、`idx_process_status`)保留 - 主键 `id` **非自增**,应用写入 `IdGenerator::nextProviderCallbackLogId()` ### 参考 SQL(文档用,非单库执行) 新增 [`slot-pwa/db/game_provider_callback_log_shard.sql`](slot-pwa/db/game_provider_callback_log_shard.sql):单分片模板 + 注释说明需通过 `php webman createSharedTable`(或项目既有命令)批量建表。 部署:`docker exec ... php webman createSharedTable`(与现有 `user_transaction_log` / `game_result_log` 相同流程)。 --- ## 2. 配置 [`config/game_gateway.php`](slot-pwa/config/game_gateway.php): | 配置项 | 用途 | |--------|------| | `callback_request_id_prefix` | 如 `creq_` | | `route_callback_map` | 路径 → 默认 `callback_type` | | `provider_shard_uid_field` | 厂商 → POST 分片字段(POP:`userid`) | [`RedisKeyManagerService`](slot-pwa/app/service/RedisKeyManagerService.php):`PROVIDER_CALLBACK_LOG_NEXT_ID` [`IdGenerator`](slot-pwa/app/service/IdGenerator.php):`nextProviderCallbackLogId()`(对齐 `nextGameResultId`) --- ## 3. 领域层 ### Shard Model — [`app/model/shard/GameProviderCallbackLogModel.php`](slot-pwa/app/model/shard/GameProviderCallbackLogModel.php) 继承 [`BaseShardModel`](slot-pwa/app/model/shard/BaseShardModel.php): ```php protected function getPreDatabase(): string { return 'user_transaction'; } protected function getPreTable(): string { return 'game_provider_callback_log'; } protected function getShardKey(): string { return 'uid'; } ``` - 状态常量:`VERIFY_*` / `PARSE_*` / `PROCESS_*` - 数据访问(均需构造时传入 `$shardUid`): - `insertReceived(GameProviderCallbackLogReceiveParams $params): void`(内部 `insertData`) - `updateVerifyStatus(int $id, ...)` - `updateParseResult(int $id, GameProviderCallbackLogParseParams $params)` - `updateProcessStatus(int $id, ...)` - `findById(int $id): ?array`(`findOne(['id' => $id, 'uid' => $this->_shardVal])`) ### Value Objects(可放 `app/model/game/` 或 `app/entity/game/`) - `GameProviderCallbackLogReceiveParams` — 含 `shardUid`、落库字段 - `GameProviderCallbackLogParseParams` — 解析后业务字段 ### Service — [`app/service/game/ProviderCallbackLogService.php`](slot-pwa/app/service/game/ProviderCallbackLogService.php) - `recordReceivedFromRequest(Request $request): void` — 解析 `shardUid`、生成 `id`/`request_id`、**同步** `new GameProviderCallbackLogModel($shardUid)->insertReceived(...)`,设置 `$request->providerCallbackLogId` / `providerCallbackShardUid` - `markVerify*(int $shardUid, int $id, ...)` - `markParse*(int $shardUid, int $id, ...)` - `markProcess*(int $shardUid, int $id, ...)` 所有 mark 方法内部统一:`new GameProviderCallbackLogModel($shardUid)->updateByWhere(['id' => $id, 'uid' => $shardUid], ...)` --- ## 4. Middleware(POP 本期) - 新增 [`ProviderCallbackLogMiddleware`](slot-pwa/app/middleware/ProviderCallbackLogMiddleware.php) — **最先**执行 - [`config/middleware.php`](slot-pwa/config/middleware.php) `'pop' => [ProviderCallbackLogMiddleware, POPAuthMiddleware]` - [`POPAuthMiddleware`](slot-pwa/app/middleware/POPAuthMiddleware.php):验签结果写回 callback_log;失败 `VERIFY_FAILED` + `PROCESS_REJECTED` - `/pop/admin/` 跳过落库 --- ## 5. POP Logic [`CashLogic`](slot-pwa/app/pop/logic/CashLogic.php) / [`CashController`](slot-pwa/app/pop/controller/CashController.php): - 从 `$request` 读取 `providerCallbackLogId` + `providerCallbackShardUid` - `balance` / `modifyTransferInOut`:parse/process 状态;重复 tid → `PROCESS_DUPLICATE` - Validate 失败:`PARSE_FAILED` + `PROCESS_REJECTED`(保留已落库的 raw 证据) --- ## 6. 测试 参照分表写入方式(非 LaunchSession 单表): - [`tests/Support/ProviderCallbackLogTestCase.php`](slot-pwa/tests/Support/ProviderCallbackLogTestCase.php) — 固定测试 `shardUid`,`insertReceived` + teardown `delete` - [`tests/Feature/GameProviderCallbackLogShardModelTest.php`](slot-pwa/tests/Feature/GameProviderCallbackLogShardModelTest.php) - 初始 `verify_status=0`、`process_status=0` - `updateByWhere` 后终态、`processed_at` 写入 - 同 `body_hash`、同/不同 `shardUid` 可各插一条 `docker exec -w /app/www/slot/slot-pwa php82 ./vendor/bin/phpunit` --- ## 7. 验收对照(POP + 分表) | 验收项 | 实现 | |--------|------| | 原始请求可追溯 | `raw_body` / `raw_headers` 写入对应分片表 | | 验签/解析失败仍有行 | Middleware 先 INSERT(shardUid 可为 0) | | BALANCE 仅 callback_log | 无 provider_tx | | 重复回调多条 log | 每次 INSERT;tid 重复 → `process_status=3` | | 分库分表 | 与 GameResultLog 相同 `user_transaction_*` 族 | --- ## 8. 后续 PR - 其它厂商 Middleware + `provider_shard_uid_field` - 可选:`app/model/all/GameProviderCallbackLogModel` + DBSync(仅当需要 `s_all` 汇总检索时) --- ## 关键文件 | 操作 | 路径 | |------|------| | 修改 | `app/command/CreateSharedTable.php` | | 新增 | `db/game_provider_callback_log_shard.sql`(模板) | | 新增 | `app/model/shard/GameProviderCallbackLogModel.php` | | 新增 | `app/model/game/GameProviderCallbackLog*Params.php` | | 新增 | `app/service/game/ProviderCallbackLogService.php` | | 新增 | `app/middleware/ProviderCallbackLogMiddleware.php` | | 修改 | `IdGenerator.php`, `RedisKeyManagerService.php`, `middleware.php`, `POPAuthMiddleware.php` | | 修改 | `app/pop/logic/CashLogic.php`, `app/pop/controller/CashController.php` | | 新增 | `tests/Feature/GameProviderCallbackLogShardModelTest.php` | 完成编码后执行 `~/.cursor/hooks/verify-slot-backend.sh` 并粘贴输出。