--- todos: - id: "expiry-service-mark-only" content: "TrialBalanceExpiryService:新增 markTrialExpiredIfNeeded / isTrialPlayBlocked / ensureCanLaunchGame,停止对外清零编排" status: pending - id: "ws-connect" content: "TrialBalanceExpiryConnectService:仅打 tag + 弹窗,移除 clearExpiredTrialBalanceIfNeeded" status: pending - id: "entry-logic" content: "TrialRewardEntryLogic:移除全路径清零,修正 trial_balance_expired 判定" status: pending - id: "game-launch-guard" content: "GameLogic + GameController::login:体验过期未充值时 BusinessException 拦截" status: pending - id: "constants-copy" content: "TrialRewardEntryConstants:更新弹窗与 launch 拒绝文案" status: pending - id: "verify" content: "docker php -l + slot-backend-completion-report 自检" status: pending isProject: false --- # 体验到期:仅标识、不清余额、禁止进游戏 ## 背景与现状 当前 [`TrialBalanceExpiryConnectService`](slot-console/app/service/trial/TrialBalanceExpiryConnectService.php) 在 WS 连接时: 1. 调用 `clearExpiredTrialBalanceIfNeeded` → wallet `expireTrialBalance` + activity 删 `trial_reward_record` 2. 打 user tag `trial_bonus_expired` 3. 推送弹窗(文案写「Trial balance has been cleared」) [`TrialRewardEntryLogic`](slot-console/app/api/logic/TrialRewardEntryLogic.php) 在 entry/status/withdraw-click 三条路径同样调用清零。 这与需求文档 [`03_wallet_fund_flow.md`](docs/requirements/trial_withdrawal/03_wallet_fund_flow.md)「不再产生 `TRIAL_BONUS_EXPIRE`」不一致。wallet 侧 [`isTrialPhase`](slot-wallet/app/service/WalletService.php) 在超期后已返回 `false`,体验金留在 `frozen_bonus` 但无法用于试玩下注;需在 **console 启动接口** 显式拦截。 ## 目标行为 ```mermaid flowchart TD subgraph wsConnect [WS连接事件] A[未充值且体验期已过] --> B[打 user tag trial_bonus_expired] B --> C[推送 WS 弹窗一次] A --> D[不调用 wallet expireTrialBalance] A --> E[不调用 activity trial-balance/expire] end subgraph gameLaunch [game/launch 与 game/login] F[未充值且体验期已过] --> G[拒绝启动 BusinessException] H[已充值或仍在体验期内] --> I[正常 launch] end ``` | 场景 | 余额 | activity 记录 | user tag | 进游戏 | |------|------|---------------|----------|--------| | 体验期内、未充值 | 保留 | 保留 | 无 | 允许 | | 体验过期、未充值 | **保留 frozen_bonus** | **保留** | 标记过期 | **拒绝** | | 已充值 | 按首充规则 | 按首充规则 | 不处理 | 允许 | ## 实现步骤 ### 1. 收敛「到期处理」到仅标识(`TrialBalanceExpiryService`) 文件:[`slot-console/app/service/trial/TrialBalanceExpiryService.php`](slot-console/app/service/trial/TrialBalanceExpiryService.php) - 新增 `markTrialExpiredIfNeeded(int $uid, string $source, string $currency, int $organization): bool`: - 已充值 → 直接返回 `false` - `resolveWalletSnapshot` + `resolveTrialBalanceExpiry` 未过期 → `false` - 已过期 → 仅 `markTrialBonusExpiredUserTag($uid)`,返回 `true` - **不**调用 `expireTrialBalance`、**不**调用 `markActivityTrialBalanceExpired` - 新增 `isTrialPlayBlocked(int $uid, string $source, string $currency, int $organization): bool`(供 launch 守卫复用): - 逻辑:`!has_recharged && resolveTrialBalanceExpiry(...).expired` - 新增 `ensureCanLaunchGame(...)`:当 `isTrialPlayBlocked` 为真时 `throw new BusinessException(...)` - `clearExpiredTrialBalanceIfNeeded` 保留方法体但 **删除所有调用方**(避免误用);类注释改为「到期判定与标识」,去掉「清零」表述 - `markActivityTrialBalanceExpired` 可保留 private 方法(历史兼容),但本需求下不再被调用 ### 2. 改写 WS 连接处理 文件:[`slot-console/app/service/trial/TrialBalanceExpiryConnectService.php`](slot-console/app/service/trial/TrialBalanceExpiryConnectService.php) - 删除 `clearExpiredTrialBalanceIfNeeded` 及 `wasBalanceCleared` / `hadNoTrialBalance` 分支 - 流程简化为: 1. user tag 已标记 → return(幂等防重复弹窗) 2. 已充值 → return 3. 未过期 → return 4. `markTrialBonusExpiredUserTag` + `WsService::notifyClientPOP` + info 日志 - 更新类 PHPDoc:「检测试玩到期:打 user tag、推送弹窗(不清零余额)」 ### 3. 入口 Logic 停止清零并修正 `trial_balance_expired` 文件:[`slot-console/app/api/logic/TrialRewardEntryLogic.php`](slot-console/app/api/logic/TrialRewardEntryLogic.php) - 在 `buildEntryPayload` / `buildStatusPayload` / `resolveWithdrawClick` 中: - 移除 `clearExpiredTrialBalanceIfNeeded` 调用及 `trialBalanceBeforeClear` 相关刷新逻辑 - `trial_balance_expired` 改为:`$trialExpiry['expired'] && !$walletSnapshot['has_recharged']`(不再依赖 `bonus_amount <= 0`) - 删除 private `clearExpiredTrialBalanceIfNeeded` 方法 - 移除对 [`TrialExpiredBalanceClearCommand`](slot-console/app/api/dto/TrialExpiredBalanceClearCommand.php) 的 use(DTO 可保留文件以免大范围删除,但无引用) ### 4. game/launch 与 game/login 拦截 文件:[`slot-console/app/api/logic/GameLogic.php`](slot-console/app/api/logic/GameLogic.php) - 构造注入或懒加载 `TrialBalanceExpiryService` - `launch()` 开头调用 `ensureCanLaunchGame($dto->uid, source, currency, organization)` - source/currency/organization 从 `GameLaunchDTO` 或 `userEntity` 读取(与 wallet 快照现有参数对齐) - [`GameController::login`](slot-console/app/api/controller/GameController.php) 在调用 `SlotPlatformService::login` 前同样守卫(deprecated 路径仍可能被 `launchByGameId` 使用) 错误处理:抛 `Webman\Exception\BusinessException`,消息使用新常量;[`api/exception/Handler`](slot-console/app/api/exception/Handler.php) 已注册,前端 `useGameLaunch` 会 toast 该 message。 ### 5. 常量与文案 文件:[`slot-console/app/constants/TrialRewardEntryConstants.php`](slot-console/app/constants/TrialRewardEntryConstants.php) - 更新 `POPUP_MESSAGE_TRIAL_BALANCE_EXPIRED`:去掉「balance has been cleared」,改为体验期结束、需充值后才能继续游戏(英文与产品口径一致) - 新增 `GAME_LAUNCH_BLOCKED_MESSAGE`(launch 拒绝文案,可与弹窗语义一致) `CLEAR_SOURCE_*` 常量暂保留(activity 接口仍存在,仅本链路不再调用)。 ### 6. 不改动的服务 - **slot-wallet**:不删 `expireTrialBalance` API(历史兼容),本需求不再调用 - **slot-activity**:`trial-balance/expire` innerapi 保留,本需求不再调用 - **slot-pwa**:按你的选择不拦截 - **lobby**:依赖 launch API 错误 message 即可,无需必改 ## 关键代码对照 WS 连接当前清零调用(将删除): ```65:75:slot-console/app/service/trial/TrialBalanceExpiryConnectService.php $clearOutcome = $this->trialBalanceExpiryService->clearExpiredTrialBalanceIfNeeded( new TrialExpiredBalanceClearCommand( $uid, $channelSource, $currency, $organization, $walletSnapshot, [] ), TrialRewardEntryConstants::CLEAR_SOURCE_WS_CONNECT ); ``` 入口当前过期判定(将修改): ```112:112:slot-console/app/api/logic/TrialRewardEntryLogic.php 'trial_balance_expired' => $walletSnapshot['bonus_amount'] <= 0 && $trialExpiry['expired'], ``` ## 验证要点 1. 模拟未充值用户、`wallet.created_at` 超过配置 `validity_hours`:WS 连接一次 → user tag 写入、收到弹窗、**bonus 余额不变**、无 `TRIAL_BONUS_EXPIRE` 流水 2. 再次 WS 连接 → 无重复弹窗(tag 幂等) 3. 同用户调用 `POST /api/game/launch` → BusinessException,不返回 url 4. 同用户 `GET /api/trial-reward/entry` → `trial_balance_expired=true`,`trial_balance` 仍 > 0 5. 已充值用户超体验窗口 → launch 正常、WS 不弹窗 6. 收尾跑 `slot-backend-completion-report` 与 verify 脚本