This commit is contained in:
ray zhou
2026-05-29 17:23:17 +08:00
parent f71a5c59af
commit 1bcb6120dd
139 changed files with 4229 additions and 523 deletions

View File

@@ -0,0 +1,138 @@
---
name: DailyRebate max_rate
overview: 将每日返水 C 端 `info` 接口的 `tiers` 数组改为返回档位中最高返水比例 `max_rate`float并清理不再使用的格式化方法。
todos:
- id: update-info-return
content: 修改 DailyRebateLogic::info():移除 tiers 返回,新增 max_rate
status: completed
- id: add-resolve-max-rate
content: 新增 resolveMaxRateFromTiers(),删除 formatTiersForClient()
status: completed
- id: sync-phpdoc
content: 更新 info() PHPDoc 与 Controller 注释
status: completed
- id: verify
content: 执行 verify-slot-backend.sh 验证
status: completed
isProject: false
---
# 每日返水 info 返回 max_rate
## 背景
当前 [`DailyRebateLogic::info()`](slot_console/app/api/logic/DailyRebateLogic.php) 会把全部档位格式化为 C 端展示数组:
```php
$tierDisplay = $this->formatTiersForClient($tiers);
// ...
'tiers' => $tierDisplay,
```
C 端不需要完整 `tiers`,只需要**所有档位里 `rate_percent` 最大的那个比例**,字段名定为 **`max_rate`**float`3.0` 表示 3%)。
## 改动范围
仅改 [`slot_console/app/api/logic/DailyRebateLogic.php`](slot_console/app/api/logic/DailyRebateLogic.php)Controller 注释可选同步一行)。
`tiers` 仍保留在内部用于 `buildInfoViewContext()` 计算近 7 日返水,**结算/领取逻辑不动**。
## 实现步骤
### 1. 修改 `info()` 返回结构
`info()` 中:
- 删除 `$tierDisplay = $this->formatTiersForClient($tiers);`
- 新增 `$maxRate = $this->resolveMaxRateFromTiers($tiers);`
- 返回字段由 `'tiers' => $tierDisplay` 改为 `'max_rate' => $maxRate`
更新 `info()` PHPDoc 返回结构:
```php
/**
* @return array{
* ...
* max_rate: float,
* records: list<...>
* }
*/
```
### 2. 新增私有方法 `resolveMaxRateFromTiers()`
替换原 `formatTiersForClient()`,语义更清晰:
```php
/**
* 从档位配置中取最高返水比例C 端展示用)。
*
* @param list<array{sort:int,min_bet:int,max_bet:?int,rate_percent:float}> $tiers
*/
protected function resolveMaxRateFromTiers(array $tiers): float
{
if ($tiers === []) {
return 0.0;
}
$maxRatePercent = 0.0;
foreach ($tiers as $tier) {
$maxRatePercent = max($maxRatePercent, (float) $tier['rate_percent']);
}
return $maxRatePercent;
}
```
边界处理:
- 无活动 / `$tiers === []` → 返回 `0.0`
- 多档同比例 → 取该值即可(`max()` 自然处理)
### 3. 删除 `formatTiersForClient()`
该方法仅在 `info()` 使用,删除避免 dead code。
### 4. 同步 Controller 注释(可选)
[`DailyRebateController::info()`](slot_console/app/api/controller/DailyRebateController.php) 注释「档位、近 7 日记录…」改为「最高返水比例、近 7 日记录…」。
## 接口变更Breaking Change
| 变更前 | 变更后 |
| --- | --- |
| `tiers: [{min, max, rate}, ...]` | **移除** |
| — | `max_rate: 3.0` |
示例(有 4 档配置,最高 3%
```json
{
"unlocked": true,
"title": "...",
"max_rate": 3.0,
"records": [...]
}
```
当前仓库内 **slot_pwa 尚未引用 `tiers`**后端先改即可C 端接入时用 `max_rate` 展示「最高 X% 返水」类文案。
## 数据流(不变部分)
```mermaid
flowchart LR
config[DailyRebateConfigService.resolveForSource]
tiers[tiers内部数组]
info[DailyRebateLogic.info]
maxRate[max_rate]
records[records近7日]
config --> tiers
tiers --> info
info --> maxRate
info --> records
```
## 验证
-`~/.cursor/hooks/verify-slot-backend.sh`
- 手动调 `DailyRebate info`:有活动时应返回 `max_rate` 等于配置中最大 `rate_percent`;无活动时为 `0.0`

View File

@@ -0,0 +1,93 @@
---
name: Launch Logic 参数与规范
overview: 重构 `GameLaunchSessionLogic` 中仅为日志传递的 `$gameCode` 参数,并在用户级 `php-clean-code.mdc` / `agent-completion-gate.mdc` 中增加「参数必须服务于业务」的硬性约束与完成前自查项。
todos:
- id: refactor-find-methods
content: 重构 GameLaunchSessionLogic去掉 find* 中仅用于日志的 gameCode 参数,日志改用本步真实字段
status: completed
- id: update-php-clean-code
content: php-clean-code.mdc §3 增加「参数与日志」、§8 增加自查项
status: pending
- id: update-completion-gate
content: agent-completion-gate.mdc 增加 Logic 自查与 SLOT_ROOT=www/ray 校验说明
status: pending
- id: verify
content: docker php -l + SLOT_ROOT verify-slot-backend.sh回复粘贴完整输出
status: completed
isProject: false
---
# Launch Logic 参数重构 + 规范加固
## 问题
[`GameLaunchSessionLogic.php`](slot-pwa/app/api/logic/GameLaunchSessionLogic.php) 中以下方法将 `$gameCode` 作为入参,但**不参与查询/判断**,仅用于 `Log::error`
- `findActivePlatformMapping(GGameModel $gameModel, string $gameCode)`
- `findPlatformById(int $platformId, string $gameCode)`
- `findProviderGameService(GPlatformModel $platformModel, string $gameCode)`
违反 [`php-clean-code.mdc`](/Users/ray/.cursor/rules/php-clean-code.mdc) §3参数应表达业务需要、禁止误导性签名
## 一、代码重构(企业做法:查询方法只收查询条件)
**改动文件:** 仅 [`slot-pwa/app/api/logic/GameLaunchSessionLogic.php`](slot-pwa/app/api/logic/GameLaunchSessionLogic.php)
| 方法 | 调整后签名 | 失败日志字段(用已有入参) |
|------|------------|---------------------------|
| `findActivePlatformMapping` | `(GGameModel $gameModel)` | `game_id``active_platform_id` |
| `findPlatformById` | `(int $platformId)` | `platform_id` |
| `findProviderGameService` | `(GPlatformModel $platformModel)` | `provider_code``$platformModel->code` |
`resolveGameLaunchContext` 调用改为:
```php
$gameModel = $this->findGameByCode($gameLaunchDto->gameCode);
$platformMapping = $this->findActivePlatformMapping($gameModel);
$platformModel = $this->findPlatformById((int) $gameModel->active_platform_id);
$providerGameService = $this->findProviderGameService($platformModel);
```
**不引入** `GameLaunchResolveContext`(当前仅 3 步解析,编排层已有 `gameLaunchDto->gameCode`,避免过度设计)。
**保留** `findGameByCode(string $gameCode)``gameCode` 即查询条件,合理。
**可选增强(本计划内做):**`resolveGameLaunchContext` 最外层若需串联排障,可在 public 入口 `launchWithSession` 的 catch 中统一补 `game_code`(已有 `launch game session fail` 日志,解析阶段失败由各 `find*` 用自身字段即可)。
## 二、用户级规范更新
### 1. [`php-clean-code.mdc`](/Users/ray/.cursor/rules/php-clean-code.mdc)
**§3 方法规则** 末尾新增小节 **「参数与日志」**
- **禁止**为打日志、排障单独增加与该方法业务无关的参数(反例:`findPlatformById($id, $gameCode)``$gameCode` 只出现在 `Log::error`)。
- **查询/校验类方法**`find*``ensure*`)的参数必须等于该步骤的查询条件或判断依据。
- 跨多步共享的排障字段(如 `game_code`)应在 **用例编排方法**public Logic 入口或 `resolveXxx` 编排 private集中记录子步骤日志只写本子步骤真实使用的字段`platform_id``provider_code` 等)。
- 若多步都需要同一追溯上下文且步骤 ≥4再引入 readonly `XxxResolveContext`**禁止**向每个 private 方法重复挂相同标量。
**§8 Agent 自查** 增加一项:
- 是否存在「仅用于日志」的多余参数?
### 2. [`agent-completion-gate.mdc`](/Users/ray/.cursor/rules/agent-completion-gate.mdc)
**必须** 列表增加:
5. 修改 `app/**/Logic/**/*.php` 时,完成前对照 `php-clean-code` **§3 参数与日志** 与 **§8 自查**(不仅依赖 verify 脚本)。
6. 工作区在 `www/ray` 时,执行校验须:`SLOT_ROOT=/Users/ray/Documents/project/www/ray ~/.cursor/hooks/verify-slot-backend.sh`(避免 `PASS (no changed files)` 误判)。
### 3. 不改动 verify 脚本(本计划)
`verify-slot-backend.sh` 难以可靠检测「参数仅用于日志」;以 **规则 + Agent 自查** 为主。若后续误报多,再考虑启发式检查。
## 三、验收
1. `findPlatformById` / `findActivePlatformMapping` / `findProviderGameService` 签名中无仅日志用的 `$gameCode`
2. `docker exec -w /app/www/ray/slot-pwa php82 php -l app/api/logic/GameLaunchSessionLogic.php` 通过。
3. `SLOT_ROOT=/Users/ray/Documents/project/www/ray ~/.cursor/hooks/verify-slot-backend.sh` 输出 `PASS`(有 diff 时)。
4. 规范文件已更新Agent 自查项可对照执行。
## 范围外
- 不重构 `GameController` 响应格式。
- 不新增 `GameLaunchResolveContext` DTO除非实现时发现步骤继续增加

View File

@@ -0,0 +1,132 @@
---
name: PHPDoc 规范收紧
overview: 调整 [`php-clean-code.mdc`](/Users/ray/.cursor/rules/php-clean-code.mdc) 第 5 节 PHPDoc 规则:除纯 getter/setter 外Logic/Service/Model 的 public 方法一律必须写中文 PHPDoc删除「方法名够清晰可不写」的豁免并同步 Agent 自查清单。
todos:
- id: revise-section-5
content: 重写 php-clean-code.mdc §5默认必须、getter/setter 豁免、中文首行、删除旧豁免句
status: completed
- id: tighten-must-list
content: 将原「以下情况必须写」改为「附加要求」,避免被理解为可选项
status: completed
- id: update-agent-checklist
content: 更新 §8 Agent 自查:中文 PHPDoc 全覆盖检查项
status: completed
- id: verify-consistency
content: 通读 Model @property 小节与 §5确认无冲突表述
status: completed
isProject: false
---
# PHPDoc 规范收紧计划
## 背景
当前 [`php-clean-code.mdc`](/Users/ray/.cursor/rules/php-clean-code.mdc) 存在两条互相削弱的规则:
- 第 165 行:`Logic、Service、Model 的 public 方法**建议**写 PHPDoc`
- 第 167174 行:仅列举部分场景「**必须**写」
- 第 203 行:`简单 getter/setter、方法名和类型已足够清晰时**不强制** PHPDoc`
团队诉求:**除纯 getter/setter 外,其余方法都要写 PHPDoc且以中文说明业务含义**(很多人看不懂英文方法名)。这与已存在的 [Model 类 `@property` 规范](/Users/ray/.cursor/rules/php-clean-code.mdc)(约 123159 行)方向一致,需统一到同一套原则。
## 修改范围
**仅改规则文件**(不批量改历史代码):
- [`/Users/ray/.cursor/rules/php-clean-code.mdc`](/Users/ray/.cursor/rules/php-clean-code.mdc) — §5 PHPDoc 与注释、§8 Agent 自查
[`agent-completion-gate.mdc`](/Users/ray/.cursor/rules/agent-completion-gate.mdc) 仍引用「php-code PHPDoc 章节」,无需改路径;完成门禁时 Agent 按更新后的 §5 执行即可。
## §5 改写要点
### 1. 默认规则:从「建议」改为「必须」
将第 165 行改为明确默认值:
> **Logic / Service / Model 的 `public` 方法必须写 PHPDoc**(首行中文说明业务动作);`protected` 方法若承载业务步骤,同样必须。
### 2. 唯一豁免:纯 getter / setter
用白名单定义豁免,替代原第 203 行「方法名够清晰可不写」:
| 可豁免 | 不可豁免 |
|--------|----------|
| 无业务分支、无事务、无外部调用的 `getXxx()` / `setXxx()` | `findActiveBySessionId``createActiveSession``launchWithSession` 等 |
| 只读/写入单个属性或 DTO 字段 | 名称像 getter 但含查询、状态判断、写入库表 |
| | `isXxx()` / `hasXxx()` / `ensureXxx()` / `markXxx()` |
示例(写入规范正文):
```php
// 可豁免
public function getUid(): int { return $this->uid; }
// 不可豁免 — 必须中文 PHPDoc
public static function findActiveBySessionId(string $sessionId): ?self
```
### 3. 方法 PHPDoc 格式要求(中文优先)
规定最小合格格式(与现有 Model `@property` 风格一致):
```php
/**
* 按对外 session_id 查询未过期且有效的 Launch Session。
*
* @throws BusinessException 当 ...
*/
public static function findActiveBySessionId(string $sessionId): ?self
```
- **首行必须是中文**,说明「做什么 / 业务结果」,不能只重复英文方法名。
- 保留现有硬性要求:`array` 结构、`@throws`、状态流转等(作为**附加**要求,不是唯一触发条件)。
- **禁止废话**:禁止只写 `/** get user */``/** @param int $uid */` 而无中文业务说明;禁止与签名完全重复的英文复述。
### 4. 删除 / 替换第 203 行
删除:
> 不要写废话 PHPDoc。简单 getter/setter、方法名和类型已足够清晰时不强制 PHPDoc。
替换为两条并列原则:
- **禁止废话**:无信息增量、纯英文复述签名 → 不合格。
- **除纯 getter/setter 外一律必须**:英文命名不能替代中文 PHPDoc。
保留第 205 行「注释解释为什么,不解释做什么」——仅适用于**行内注释**;方法 PHPDoc 首行仍要写「做什么」(中文),复杂处再用行内注释写「为什么」。
### 5. 收紧原「以下情况必须写」列表
原列表(返回 array、状态流转等改为 **「以下情况除满足通用要求外,还须额外写明…」**,避免读者误以为「不在列表里就可以不写」。
结构示意:
```mermaid
flowchart TD
method[public方法] --> isGetter{纯getter/setter?}
isGetter -->|是| exempt[可省略PHPDoc]
isGetter -->|否| required[必须中文PHPDoc首行]
required --> extra{array或throws等?}
extra -->|是| addTags[补充结构或throws]
```
## §8 Agent 自查增补
在 §8 增加 / 调整检查项(约 245 行后):
- Logic / Service / Model 的 `public` 方法是否均有**中文** PHPDoc除纯 getter/setter
- 是否存在「只有 `@param`/`@return` 类型、无中文业务说明」的 PHPDoc
可将原「返回 array / 复杂数组 / throws」三条合并表述为「在通用要求之上是否满足附加结构」避免清单过长。
## 不纳入本次范围
- **不**批量给存量 PHP 文件补 PHPDoc体量大另开重构任务
- **不**强制 Controller / Validate / DTO 全量 PHPDoc当前 §5 范围是 Logic/Service/Model若需扩大可后续单独立项
- **不**新增 CI 自动检测脚本(可选后续:`scripts/check-phpdoc.sh`)。
## 验收
- `php-clean-code.mdc` §5 不再出现「方法名够清晰可不写 PHPDoc」。
- 新规则与 Model `@property` 小节无矛盾。
- Agent 自查清单覆盖「中文 PHPDoc + getter/setter 豁免」。

View File

@@ -0,0 +1,137 @@
---
name: StartDTO 去重与规范
overview: 收紧 `GameLaunchSessionStartDTO` 为仅组合两个子 DTO + accessor 派生字段,简化 `launchWithSession` 编排;并在 `php-clean-code.mdc` 增加「禁止编排层双份拷贝 / DTO 重复标量」规则。
todos:
- id: refactor-start-dto
content: GameLaunchSessionStartDTO仅 2 子 DTO + uid/providerCode/gameCode accessor
status: completed
- id: refactor-launch-logic
content: 简化 launchWithSessionfindReusableLaunch 收 StartDTOcreateLaunchSession 用 accessor
status: completed
- id: update-rules-dto-dup
content: php-clean-code §3 增加「编排上下文与聚合 DTO」§8 与 agent-completion-gate 补充自查
status: completed
- id: verify-start-dto
content: docker php -l + SLOT_ROOT verify-slot-backend.sh
status: completed
isProject: false
---
# StartDTO 去重重构 + 规范加固
## 问题
[`launchWithSession`](slot-pwa/app/api/logic/GameLaunchSessionLogic.php) 中:
```php
$uid = (int) $gameLaunchDto->uid;
$providerCode = $gameLaunchContext->providerCode;
$gameCode = $gameLaunchDto->gameCode;
// ...
new GameLaunchSessionStartDTO($gameLaunchDto, $gameLaunchContext, $uid, $providerCode, $gameCode);
```
[`GameLaunchSessionStartDTO`](slot-pwa/app/api/dto/GameLaunchSessionStartDTO.php) 同时持有 `gameLaunchDto``gameLaunchContext` **以及** 可从二者推导的 `uid` / `providerCode` / `gameCode`,属于 **编排层双份拷贝**,违反 DRY 与 [`php-clean-code.mdc`](/Users/ray/.cursor/rules/php-clean-code.mdc)「参数应表达业务需要、聚合 DTO 不重复字段」精神。
```mermaid
flowchart LR
subgraph before [当前]
DTO[GameLaunchDTO]
CTX[GameLaunchContextDTO]
locals[uid providerCode gameCode]
Start[GameLaunchSessionStartDTO 5 fields]
DTO --> locals
CTX --> locals
DTO --> Start
CTX --> Start
locals --> Start
end
```
## 一、代码重构
### 1. 收紧 `GameLaunchSessionStartDTO`
**文件:** [`slot-pwa/app/api/dto/GameLaunchSessionStartDTO.php`](slot-pwa/app/api/dto/GameLaunchSessionStartDTO.php)
- 构造函数仅保留:
- `GameLaunchDTO $gameLaunchDto`
- `GameLaunchContextDTO $gameLaunchContext`
- 派生字段通过 **accessor** 提供(单一数据源):
- `uid(): int``(int) $gameLaunchDto->uid`
- `providerCode(): string``$gameLaunchContext->providerCode`
- `gameCode(): string``$gameLaunchDto->gameCode`
### 2. 简化 `launchWithSession`
**文件:** [`slot-pwa/app/api/logic/GameLaunchSessionLogic.php`](slot-pwa/app/api/logic/GameLaunchSessionLogic.php)
目标形态(可读性优先):
```php
public function launchWithSession(GameLaunchDTO $gameLaunchDto): GameLaunchSessionResultDTO
{
$launchStart = new GameLaunchSessionStartDTO(
$gameLaunchDto,
$this->resolveGameLaunchContext($gameLaunchDto)
);
$reusedLaunch = $this->findReusableLaunch($launchStart);
if ($reusedLaunch !== null) {
return $reusedLaunch;
}
return $this->createLaunchSessionAndFetchUrl($launchStart);
}
```
- 删除 `$gameLaunchContext` / `$uid` / `$providerCode` / `$gameCode` 四个中间局部变量。
### 3. `findReusableLaunch` 收参为 StartDTO
将签名由 `(int $uid, string $providerCode, string $gameCode)` 改为:
```php
private function findReusableLaunch(GameLaunchSessionStartDTO $launchStart): ?GameLaunchSessionResultDTO
```
方法内使用 `$launchStart->uid()``providerCode()``gameCode()`,避免再次拆三个标量传递。
### 4. `createLaunchSessionAndFetchUrl` 统一 accessor
- `$launchStart->uid` 等改为 `$launchStart->uid()`(及 `providerCode()``gameCode()`
- Provider `launch()` 调用统一为 `$launchStart->uid()` + `$launchStart->gameLaunchContext->externalGameCode`(不再混用 `$launchStart->gameLaunchDto->uid`
**改动范围:** 仅上述 2 个 PHP 文件,无 API / 路由变更。
## 二、用户级规范更新
### [`php-clean-code.mdc`](/Users/ray/.cursor/rules/php-clean-code.mdc)
**§3「参数与日志」** 后增加小节 **「编排上下文与聚合 DTO」**
- **禁止**编排方法先把子 DTO/Context 字段拆成局部变量,再原样传入聚合 DTO双份拷贝。反例`$uid = $dto->uid``new StartDTO($dto, $ctx, $uid, ...)`
- **禁止**聚合 DTO 构造函数同时接收「可从已有只读字段推导」的重复标量。反例:同时存 `gameLaunchDto``gameCode`
- 聚合上下文 DTO 应 **只组合** 子对象;派生值用 `uid()` / `providerCode()` 等 accessor 从子对象读取,或在用例方法内直接使用子 DTO 字段(二选一,不并存)。
- 子步骤 private 方法若需多字段,优先传 **一个** 聚合 DTO而不是再拆 3 个标量参数。
**§8 Agent 自查** 增加:
- 是否存在「局部变量 + 聚合 DTO」双份承载同一业务字段
- 聚合 DTO 是否包含可推导的重复标量?
### [`agent-completion-gate.mdc`](/Users/ray/.cursor/rules/agent-completion-gate.mdc)
在 Logic 自查条(现有第 5 条)中补充:**§3 编排上下文与聚合 DTO**。
## 三、验收
1. `launchWithSession` 无仅用于组 DTO 的 `$uid` / `$providerCode` / `$gameCode` 局部变量。
2. `GameLaunchSessionStartDTO` 构造函数仅 2 个参数。
3. `docker exec -w /app/www/ray/slot-pwa php82 php -l` 对相关文件通过。
4. `SLOT_ROOT=/Users/ray/Documents/project/www/ray ~/.cursor/hooks/verify-slot-backend.sh``PASS`
## 范围外
- 不改动 `GameLaunchContextDTO` / `GameLaunchSessionCreateParams` 结构。
- 不修改 verify 脚本(仍依赖规则 + 自查)。

View File

@@ -0,0 +1,184 @@
---
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.sqlDDL 与需求一致)
status: completed
- id: model-launch-session
content: 新增 GameLaunchSessionModel + 状态常量与查询/更新方法
status: completed
- id: logic-launch-session
content: 新增 GameLaunchSessionLogicRedis 去重、创建/关闭 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 委托新 LogicIdGenerator/RedisKey 扩展;跑 verify 脚本
status: completed
isProject: false
---
# 实现子需求 01Launch Sessionslot-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<pwd> <db> < slot-pwa/db/game_launch_session.sql
# Launch
docker exec -w /app/www/slot/slot-pwa php82 php webman game:launch <uid> -c <gameCode>
# 过期
docker exec -w /app/www/slot/slot-pwa php82 php webman game:launch-session:expire
```
手工:连续两次 launch8s 内)应复用 session`POST /slot-game/session/close` 后 DB `status=2`
## 完成门禁
实现 PHP 后执行 `~/.cursor/hooks/verify-slot-backend.sh`,最终回复粘贴完整输出。

View File

@@ -0,0 +1,184 @@
---
name: 拆分风控基础需求
overview: 将 [game_gateway_risk_prd_v_2.md](docs/requirements/game_gateway_risk_prd_v_2.md) 中 3.1 第 14 项拆成 4 份可独立评审/开发的子需求文档,放在 `docs/requirements/game_gateway/` 下,并精简主文档为索引 + 交叉引用。
todos:
- id: create-subdir-readme
content: 新建 docs/requirements/game_gateway/ 与 README.md索引 + 依赖图 + 实施顺序)
status: completed
- id: doc-01-launch
content: 编写 01_launch_session.md迁移 §4.1/4.2、§8.1、3.1-12、相关验收
status: completed
- id: doc-02-callback
content: 编写 02_provider_callback_log.md迁移 §8.2、§5.1 相关行、callback 主链路步骤、§15.4-1
status: completed
- id: doc-03-tx
content: 编写 03_provider_tx.md迁移 §8.4、§7.2、§10、tx 流程片段、§15.1
status: completed
- id: doc-04-round
content: 编写 04_game_round.md迁移 §2.3、§4.4.1、§7.1、§8.3、round 流程、§15.2
status: completed
- id: slim-master-prd
content: 精简 game_gateway_risk_prd_v_2.md§3.1 改链接、删重复 DDL/章节、更新 §14/§15/§16
status: completed
isProject: false
---
# 拆分 Game Gateway 风控 PRD3.1 项 14
## 背景
主文档 [`docs/requirements/game_gateway_risk_prd_v_2.md`](docs/requirements/game_gateway_risk_prd_v_2.md) 第 3.1 节「第一期必须实现」前 4 项为**基础数据与链路能力**,彼此有依赖但可分期交付:
| 3.1 序号 | 能力 | 核心表 | 主文档现有章节 |
|---------|------|--------|----------------|
| 1 | 游戏启动 Session 记录 | `game_launch_session` | 4.1、4.2、8.13.1-12 |
| 2 | Provider 原始回调日志 | `game_provider_callback_log_01` | 5.1原始回调落库、8.2 |
| 3 | Provider Tx 幂等交易表 | `game_provider_tx_01` | 4.3/4.4tx 部分、7.2、8.4、10、15.1 |
| 4 | Round 聚合状态表 | `game_round_01` | 2.3、4.3/4.4round 部分、4.4.1、7.1、8.3、15.2 |
当前 `docs/requirements/` 仅有这一份 PRD需新建子目录承载拆分结果。
## 目标目录结构
```text
docs/requirements/
├── game_gateway_risk_prd_v_2.md # 总览 PRD保留全局原则、516 章)
└── game_gateway/
├── README.md # 子需求索引与依赖关系
├── 01_launch_session.md
├── 02_provider_callback_log.md
├── 03_provider_tx.md
└── 04_game_round.md
```
命名约定:`{序号}_{英文表名/能力}.md`,便于与建表、代码模块对齐。
## 子文档统一模板(每份独立可交付)
每份子需求包含以下固定章节(从主文档**迁移**而非重写业务含义):
1. **文档信息**:标题、版本、父文档链接、依赖子文档
2. **目标与范围**:对应 3.1 单项 + In/Out of scope
3. **依赖与边界**slot-pwa / wallet / risk 职责(摘录主文档 §2 相关原则)
4. **业务流程**:该能力专属流程图/步骤
5. **状态定义**:仅该表相关状态(如 Round §7.1、Provider Tx §7.2
6. **数据表设计**:完整 DDL + 字段说明 + 索引/幂等约束
7. **与主链路衔接**:在 BET/WIN 中的写入时机(引用主文档 §4不重复全文
8. **验收标准**:从 §15 拆出与本能力相关的条目
9. **第一期 Out of scope**:明确不属于本子需求的表/能力(交叉引用主文档 §3.2
## 各子文档内容映射
### [`01_launch_session.md`](docs/requirements/game_gateway/01_launch_session.md)
**迁入内容:**
- §4.1 游戏启动 Launch 流程(含 `game_gateway_control` / `game_user_risk_profile_01` 检查表)
- §4.2 用户退出流程 + 过期 SQL
- §8.1 `game_launch_session` 完整 DDL
- 3.1 第 12 条:退出只关 Session、不触发资金结算
- 验收§15.2 第 4 条(退出不触发 Round Final
**接口提示(文档级,非实现):**
- Launch 创建 Session、`POST /slot-game/session/close`
- Redis 短窗口防重复 Launch`uid + provider_code + game_code + 5~10s`
**依赖说明:** 不依赖 callback_log / provider_tx / round可被后续回调通过 `session_id` 关联(若主链路需要,在子文档中写「可选关联字段」占位,不扩表)。
---
### [`02_provider_callback_log.md`](docs/requirements/game_gateway/02_provider_callback_log.md)
**迁入内容:**
- §8.2 `game_provider_callback_log_01` 完整 DDL + 「不加唯一键、幂等由 provider_tx 负责」说明
- §5.1 表中「原始回调落库」「验签 / IP 校验」「参数解析」三行及原因
- 主链路第一步:`Provider * callback → 同步写 callback_log`(从 §4.3、§4.4 摘取,各类型回调通用)
- BALANCE 只记 callback_log、不进 provider_tx§8.4 说明)
- 验收§15.4 第 1 条(资金交易可追溯到原始回调)
**依赖:**`03_provider_tx` 通过 `callback_log_id` 引用。
---
### [`03_provider_tx.md`](docs/requirements/game_gateway/03_provider_tx.md)
**迁入内容:**
- §8.4 `game_provider_tx_01` 完整 DDL + 幂等键 `provider_code + provider_tx_id + tx_type`
- §7.2 Provider Tx 状态
- §4.3 / §4.4 中与 `game_provider_tx_01` 相关的步骤(创建/查询、状态更新、BET 成功后 status
- §10 wallet 超时与补偿(全文;与 tx 强绑定)
- §3.2 不建 `game_wallet_call_log_01` 的原因wallet 结果记在 provider_tx
- 验收§15.1 全部 + §15.4 第 2 条
**依赖:** 必须先有 `02``callback_log_id`);与 `04` 并行更新 round。
---
### [`04_game_round.md`](docs/requirements/game_gateway/04_game_round.md)
**迁入内容:**
- §2.3 Round Final 才能做最终任务结算(原则)
- §4.4.1 Round 结算模式(`AGGREGATE_FINAL` / `TX_REALTIME_FINALIZE` + wallet 调用类型)
- §7.1 Round 状态
- §8.3 `game_round_01` 完整 DDL
- §4.3 BET 成功后 round 字段更新§4.4 WIN/FINAL 后 round 更新(摘取)
- §9 中 `game_round_final_settled_event` 事件说明(仅 round final 相关段落)
- 验收§15.2 全部
**依赖:**`03` 在回调主链路中协同;不重复写 wallet 补偿细节(链接 `03`)。
---
### [`README.md`](docs/requirements/game_gateway/README.md)
简短索引 + 依赖图:
```mermaid
flowchart LR
launch[01_launch_session]
callback[02_provider_callback_log]
tx[03_provider_tx]
round[04_game_round]
callback --> tx
tx --> round
launch -.->|Launch准入| callback
```
建议实施顺序:`02 → 03 → 04` 可同迭代;`01` 可与 `02` 并行。
## 主文档 [`game_gateway_risk_prd_v_2.md`](docs/requirements/game_gateway_risk_prd_v_2.md) 调整
1. **§3.1 第 14 条**改为链接列表,例如:
- `1. [游戏启动 Session](game_gateway/01_launch_session.md)`
-
2. **删除或收缩**已迁入子文档的重复正文:
- §4.1、§4.2 → 保留 1 段摘要 + 链接
- §8.1§8.4 → 改为表名清单 + 链接DDL 只在子文档保留一份)
- §7.1、§7.2 → 移至子文档;主文档 §7 仅保留 §7.3 熔断状态(属 3.1-5
- §10 → 移至 `03`;主文档保留一句「见 provider_tx 子需求」
- §15.1、§15.2 部分条目 → 子文档各持一份;主文档 §15 保留汇总 checklist 并链接子文档
3. **§4.3、§4.4** 保留端到端主链路(便于理解全局),但表字段/SQL/状态机细节改为「详见子需求」引用,避免双份维护。
4. **§14 建表清单**:前 4 张表标注对应子文档路径。
5. **§16 总结**:保留一句话架构;补充子需求目录说明。
**不拆分范围(仍留主文档):** 3.1 第 512 项及 §8.5 以后、§11§13、熔断/RTP/风险画像/MQ 等。
## 交叉引用规范
- 子文档文首:`> 父文档:[Game Gateway 游戏风控需求 V1.0](../game_gateway_risk_prd_v_2.md)`
- 主文档与子文档互相使用相对路径 Markdown 链接
- 涉及 wallet/risk 的段落写「行为以父文档 §2 为准」,不在 4 份子文档重复整章原则
## 质量检查
- 4 份子文档均含完整 DDL与当前主文档 §8 SQL **逐字一致**(仅拆位置,不改语义)
- 主文档通读无断链§3.1 14 在子文档有对应验收标准
- `README.md` 列出推荐开发顺序与表依赖
## 工作量说明
纯文档重构,不涉及代码与迁移脚本;预计 4 份子文档 + 1 索引 + 主文档瘦身,约 1 次集中编辑即可完成。

View File

@@ -0,0 +1,101 @@
---
name: 首充定格已解锁列
overview: 在 Free Credits 统计(首充定格)列表页增加「已解锁金额」列;后端 `claimed_amount_qf` 已按第二档及以后 release 档已完成金额聚合,主要改 slot_admin_vue 展示即可。
todos:
- id: vue-column
content: 在 freeCreditsStats/index.vue 增加 claimed_amount_qf 列(标题:已解锁金额)及金额展示模板
status: completed
- id: verify-ui
content: 本地打开页面核对列值与 remaining_amount_qf 公式;跑 verify-slot-backend.sh
status: completed
isProject: false
---
# 首充定格列表增加「已解锁金额」列
## 背景
目标页面为 **Free Credits 统计**(菜单:活动管理 → Free Credits 统计):
- 前端:[slot_admin_vue/src/views/game/freeCreditsStats/index.vue](backend/slot_admin_vue/src/views/game/freeCreditsStats/index.vue)
- 后端:[slot_admin/app/game/controller/FreeCreditsStatsController.php](backend/slot_admin/app/game/controller/FreeCreditsStatsController.php) → [FreeCreditsStatsLogic](backend/slot_admin/app/game/logic/FreeCreditsStatsLogic.php)
需求文档 §18.2 中对应字段为 **「已领取金额」**(后续已 Claim 金额);产品侧列名为 **「已解锁金额」**,语义一致:**第二档(`package_type=release`)起用户已成功领取的金额**,不含第一档免打码提现。
## 现状(无需重复造数)
后端 `list()` 已在每行返回 `claimed_amount_qf`,聚合逻辑在 `aggregatePackages()`
```231:232:backend/slot_admin/app/game/logic/FreeCreditsStatsLogic.php
SUM(CASE WHEN package_type = {$typeFirstCash} AND status = {$statusCompleted} THEN amount_qf ELSE 0 END) AS first_cashout_amount_qf,
SUM(CASE WHEN package_type = {$typeRelease} AND status = {$statusCompleted} THEN amount_qf ELSE 0 END) AS claimed_amount_qf,
```
- `TYPE_FIRST_CASH = 1`:第一档提现
- `TYPE_RELEASE = 2`:第二档及以后 Claim
- 仅 `status = STATUS_COMPLETED` 计入
`remaining_amount_qf` 已用该字段参与计算:`frozen - first_cashout - claimed`。
顶部汇总区已有「已领取金额」展示 `stats.claimed_amount_qf`,与行字段同源。
```mermaid
flowchart LR
subgraph api [GET /game/freeCreditsStats/index]
list[list.data]
stats[otherData.statistics]
end
subgraph agg [aggregatePackages]
release["SUM release completed amount_qf"]
end
release --> claimed["claimed_amount_qf per row"]
claimed --> list
release --> stats
```
## 实现方案
### 1. slot_admin_vue主要改动
文件:[index.vue](backend/slot_admin_vue/src/views/game/freeCreditsStats/index.vue)
在 `columns` 中 **「完成进度」与「剩余未释放金额」之间** 插入一列:
| 列标题 | dataIndex | 说明 |
|--------|-----------|------|
| 已解锁金额 | `claimed_amount_qf` | 千分位转美元,与定格金额等列一致 |
新增 slot 模板(复用现有 `qfToDollar`
```vue
<template #claimed_amount_qf="{ record }">
${{ qfToDollar(record.claimed_amount_qf) }}
</template>
```
无需改 [freeCreditsStats.js](backend/slot_admin_vue/src/api/game/freeCreditsStats.js) API 封装。
### 2. slot_admin 后端(可选 / 建议不改)
[FreeCreditsStatsLogic.php](backend/slot_admin/app/game/logic/FreeCreditsStatsLogic.php) **已满足需求**,无需新增接口或 SQL。
若希望 API 字段名与列标题一致,可额外返回别名 `unlocked_amount_qf`(与 `claimed_amount_qf` 同值),但会增加前后端两套字段,**不建议**,前端直接用 `claimed_amount_qf` 即可。
### 3. 命名对齐(可选)
- 列表列名:**已解锁金额**(按产品要求)
- 顶部汇总仍为「已领取金额」;与需求文档 §18.1 一致。若产品要求顶部也改名,可一并改为「已解锁金额」,本次可只做列表列。
## 验证
1. 打开 Free Credits 统计页,确认新列有值;未领取第二档的用户为 `$0.00`。
2. 抽查:某用户第二档及以后 `free_credits_package` 中 `package_type=2` 且 `status=3` 的 `amount_qf` 之和 = 列表「已解锁金额」× 1000。
3. 确认「剩余未释放金额」= 定格金额 第一档已提现 已解锁金额(与现有公式一致)。
4. 执行 `~/.cursor/hooks/verify-slot-backend.sh`(若仅改 Vue预期 `PASS (no changed files)` 或仅前端相关检查)。
## 改动范围小结
| 仓库 | 文件 | 改动 |
|------|------|------|
| slot_admin_vue | `src/views/game/freeCreditsStats/index.vue` | 新增列 + 金额模板 |
| slot_admin | — | 无(数据已返回) |