--- name: 实现 Launch Session overview: 在 slot-pwa 中实现子需求 01:基于现有 GameLogic.launch 链路增加 game_launch_session 落库、Redis 短窗口防重复、Session 关闭接口与过期定时任务;本期跳过 game_gateway_control / 风险画像检查。 todos: - id: ddl-launch-session content: 新增 slot-pwa/db/game_launch_session.sql(DDL 与需求一致) status: completed - id: model-launch-session content: 新增 GameLaunchSessionModel + 状态常量与查询/更新方法 status: completed - id: logic-launch-session content: 新增 GameLaunchSessionLogic:Redis 去重、创建/关闭 Session、编排 Provider launch status: completed - id: api-routes content: 改造 GameController.launch 响应;新增 close 接口与 route/Validate/DTO status: completed - id: expire-command content: 新增 game:launch-session:expire 命令 + game.php TTL 配置 status: completed - id: refactor-game-logic content: GameLogic 委托新 Logic;IdGenerator/RedisKey 扩展;跑 verify 脚本 status: completed isProject: false --- # 实现子需求 01:Launch Session(slot-pwa) ## 现状 - 需求文档:[01_launch_session.md](docs/requirements/game_gateway/01_launch_session.md) - 服务仓库:`slot-pwa`(PRD 中的 slot-pwa) - **已有**:[`GameController::launch`](slot-pwa/app/api/controller/GameController.php) → [`GameLogic::launch`](slot-pwa/app/api/logic/GameLogic.php) → 查 `g_game` / `g_platform` → 调 Provider `launch()` 返回 URL - **缺失**:`game_launch_session` 表、Model、Session 创建/关闭/过期、Redis 去重、`POST /slot-game/session/close` 路由 - **本期不做**(你已确认):`game_gateway_control`、`game_user_risk_profile_01` 准入检查 ```mermaid sequenceDiagram participant Client participant GameController participant GameLaunchSessionLogic participant Redis participant DB as game_launch_session participant Provider Client->>GameController: launch(gameCode) GameController->>GameLaunchSessionLogic: launchWithSession GameLaunchSessionLogic->>Redis: 短窗口去重 alt 命中有效 Session GameLaunchSessionLogic-->>Client: 复用 launch_url 或重新拉 URL 策略见下 else 新启动 GameLaunchSessionLogic->>DB: INSERT status=1 GameLaunchSessionLogic->>Provider: launch GameLaunchSessionLogic->>DB: 更新 launch_url_hash GameLaunchSessionLogic-->>Client: url + session_id end Client->>GameController: session/close(session_id) GameController->>GameLaunchSessionLogic: closeSession GameLaunchSessionLogic->>DB: status=2, closed_at ``` ## 实现策略 ### 1. 数据库迁移 新增 SQL:[slot-pwa/db/game_launch_session.sql](slot-pwa/db/game_launch_session.sql) - DDL 与需求文档 **逐字一致**(`game_launch_session` 全字段 + 索引) - 部署:在 Docker MySQL 中执行(`goMysql` / 项目 `mysql` 连接) ### 2. Model 层 新建 [`slot-pwa/app/model/game/GameLaunchSessionModel.php`](slot-pwa/app/model/game/GameLaunchSessionModel.php) - `connection = mysql`,`table = game_launch_session` - 状态常量:`STATUS_ACTIVE=1` … `STATUS_RISK_BLOCKED=6`(与 PRD 一致) - 查询方法(业务语义命名): - `findActiveBySessionId(string $sessionId)` - `findLatestActiveByUidProviderGame(int $uid, string $providerCode, string $gameCode)`(Redis 未命中时兜底) - `markClosed(int $id): void` / `markExpiredBatch(): int`(供定时任务) ### 3. Logic 层(核心) 新建 [`slot-pwa/app/api/logic/GameLaunchSessionLogic.php`](slot-pwa/app/api/logic/GameLaunchSessionLogic.php),从 [`GameLogic`](slot-pwa/app/api/logic/GameLogic.php) 抽离并编排: | 步骤 | 说明 | |------|------| | 解析游戏 | 复用现有 `GGameModel` / `GGamePlatformMappingModel` / `GPlatformModel` 校验;`provider_code` = `GPlatformModel.code` | | Redis 去重 | Key:`game:launch:dedup:{uid}:{provider_code}:{game_code}`,TTL **8s**(需求 5~10s 取中值);Value:`session_id` | | 命中去重 | 查 DB 中 `status=1` 且未过期的 Session;**若存在且 Provider 允许**,直接返回已有 `session_id` + 不再请求 Provider(避免重复 LaunchURL);若 Session 无效则走新启动 | | 创建 Session | 生成 `session_id`、`launch_request_id`(`bin2hex(random_bytes(16))` 或类似 UUID 风格);`id` 复用 [`IdGenerator`](slot-pwa/app/service/IdGenerator.php) 模式新增 `nextLaunchSessionId()` | | 调 Provider | 调用现有 `BaseGameService::getGameService()->launch()` | | 写回 | `launch_url_hash = hash('sha256', $url)`;失败时 `status=4` + `remark` | | 关闭 | `closeSession(uid, session_id)`:校验归属 → `status=2`,`closed_at=now`;**不**调 wallet / round / 盈利统计 | 重构 [`GameLogic::launch`](slot-pwa/app/api/logic/GameLogic.php): - 薄封装:委托 `GameLaunchSessionLogic::launchWithSession(GameLaunchDTO)`,保持 `game:launch` 命令兼容 ### 4. API 层 **改造 Launch 响应**([`GameController::launch`](slot-pwa/app/api/controller/GameController.php)): ```php // 返回扩展示例 ['url' => $url, 'session_id' => $sessionId, 'launch_request_id' => $launchRequestId] ``` **新增关闭接口**: | 项 | 内容 | |----|------| | 路由 | `POST /slot-game/session/close`(写入 [`config/route.php`](slot-pwa/config/route.php)) | | Controller | 新建 `GameLaunchSessionController` 或扩展现有 `GameController` | | Validate | `session_id` require;`uid` 从 Auth 中间件 / 请求体(与现有 launch 一致用 body `uid`) | | DTO | `GameLaunchSessionCloseDTO` | 错误处理:将 `RuntimeException` 逐步改为 `Webman\Exception\BusinessException` + 明确错误码(与项目 exception handler 对齐);关键日志带 `uid`、`game_code`、`session_id`。 ### 5. Redis Key 管理 在 [`ShareRedisKeyManagerService`](slot-pwa/app/service/ShareRedisKeyManagerService.php) 或 [`RedisKeyManagerService`](slot-pwa/app/service/RedisKeyManagerService.php) 增加: ```php getGameLaunchDedupKey(int $uid, string $providerCode, string $gameCode): string ``` ### 6. Session 过期任务 新建 Console 命令 [`slot-pwa/app/command/GameLaunchSessionExpire.php`](slot-pwa/app/command/GameLaunchSessionExpire.php): - 名称:`game:launch-session:expire` - SQL 逻辑同需求文档:`status=1 AND expired_at < NOW(3)` → `status=3` - 可由 crontab / 运维定时 `docker exec ... php webman game:launch-session:expire` 配置项([`config/game.php`](slot-pwa/config/game.php) 或 `params.php`): - `launch_session_ttl_seconds`(默认如 7200,写入 `expired_at`) - `launch_dedup_ttl_seconds`(默认 8) ### 7. 验收对照(子需求 §6) | 验收项 | 实现方式 | |--------|----------| | session_id / launch_request_id 唯一 | DB UNIQUE + 生成逻辑 | | Redis 短窗口不重复 LaunchURL | dedup key + 复用 active session | | 退出 status=2,无资金副作用 | close 仅 UPDATE session | | 过期 status=3 | 定时命令 | ## 文件清单(新增/修改) | 操作 | 路径 | |------|------| | 新增 | `slot-pwa/db/game_launch_session.sql` | | 新增 | `slot-pwa/app/model/game/GameLaunchSessionModel.php` | | 新增 | `slot-pwa/app/api/logic/GameLaunchSessionLogic.php` | | 新增 | `slot-pwa/app/api/controller/GameLaunchSessionController.php`(或扩展 GameController) | | 新增 | `slot-pwa/app/api/dto/GameLaunchSessionCloseDTO.php` | | 新增 | `slot-pwa/app/api/validate/GameLaunchSessionValidate.php` | | 新增 | `slot-pwa/app/command/GameLaunchSessionExpire.php` | | 修改 | `slot-pwa/app/api/logic/GameLogic.php` | | 修改 | `slot-pwa/app/api/controller/GameController.php` | | 修改 | `slot-pwa/config/route.php` | | 修改 | `slot-pwa/app/service/IdGenerator.php`(+ RedisKey 常量) | | 修改 | `slot-pwa/config/game.php`(TTL 配置) | ## 测试建议(实现后) ```bash # 建表 docker exec -i goMysql mysql -uroot -p < slot-pwa/db/game_launch_session.sql # Launch docker exec -w /app/www/slot/slot-pwa php82 php webman game:launch -c # 过期 docker exec -w /app/www/slot/slot-pwa php82 php webman game:launch-session:expire ``` 手工:连续两次 launch(8s 内)应复用 session;`POST /slot-game/session/close` 后 DB `status=2`。 ## 完成门禁 实现 PHP 后执行 `~/.cursor/hooks/verify-slot-backend.sh`,最终回复粘贴完整输出。