--- name: 第一档提现或保留余额 overview: 为 Free Credits 第一档(定格后首笔金额)增加用户二选一:沿用现有独立提现,或新增「保留到可提现余额」(withdraw_balance、不创建打码任务)。涉及 slot_wallet 新钱包原子能力、slot_lib RPC、slot_console 新 C 端接口与档位状态同步。 todos: - id: wallet-first-keep content: slot_wallet:WalletLogModel + WalletLogic::freeCreditsFirstCashKeep(withdraw 入账、无 createTask)+ 单测 status: completed - id: slot-lib-rpc content: slot_lib WalletService 增加 freeCreditsFirstCashKeep RPC status: completed - id: console-keep-api content: slot_console:FreeCreditsLogic::keepFirstCash + Validator + Controller + 路由 status: completed - id: console-tests content: slot_console 单测 FreeCreditsKeepFirstCashTest;必要时调整 broadcast action status: completed - id: run-phpunit content: php82 容器跑 console + wallet 相关单测 status: completed isProject: false --- # 第一档:提现或保留到可提现余额 ## 背景与现状 当前第一档(`package_type=TYPE_FIRST_CASH`)在累计充值解锁后**仅支持**走 [`WithdrawService::applyFreeCreditsFirstCashout`](slot_console/app/service/WithdrawService.php) → Pay 独立提现(`bizType=free_credit_first_cashout`),**不经过钱包余额**(需求文档 §15.3)。 后续释放档走 [`FreeCreditsLogic::claim`](slot_console/app/api/logic/FreeCreditsLogic.php) → [`WalletLogic::freeCreditsClaim`](slot_wallet/app/api/logic/WalletLogic.php):入账 **deposit_balance** 并 **创建 Y1 打码任务**。 新需求:第一档金额用户可二选一: | 选项 | 行为 | | --- | --- | | **Withdraw** | 保持现有 `/api/withdraw/apply` + `package_id` | | **Keep to balance** | 入账 **withdraw_balance**,**不** `createTask`,金额立即可参与普通提现规则(无其它未完成打码任务时) | 已确认:保留余额入账 **withdraw_balance**(非 deposit)。 ```mermaid flowchart TD firstReady[第一档 status=ready] firstReady --> withdrawPath["POST /api/withdraw/apply"] firstReady --> keepPath["POST /api/free-credits/keep-first-cash"] withdrawPath --> payOrder[Pay 独立提现单] payOrder --> busCallback[EventBus 提现结果] busCallback --> pkgDone[package completed] keepPath --> walletKeep[wallet freeCreditsFirstCashKeep] walletKeep --> pkgDone pkgDone --> playerDone[player STATUS_FIRST_CASH_DONE] ``` ## 目标契约 ### 新接口(slot_console) - **路径**:`POST /api/free-credits/keep-first-cash`(与现有 `claim` 命名风格一致) - **入参**:`package_id`(必填,`free_credits_package.id`,须为第一档且 `status=ready`) - **成功**:`data` 结构同 `status()` / `claim`(`buildStatus`) - **幂等**:`bizId = free_credits_first_keep:{packageId}`;重复请求不重复入账 - **互斥**:同一第一档 `processing/completed` 后不可再提现或 keep;提现处理中不可 keep ### 档位与玩家状态(与提现成功对齐) 完成后与 [`handleFirstCashoutResult(success)`](slot_console/app/api/logic/FreeCreditsLogic.php) 一致: - package → `STATUS_COMPLETED`,写 `completed_time`,`claim_biz_id` 存幂等键(复用字段,无需改表) - player → `STATUS_FIRST_CASH_DONE` - 调用 `refreshPlayerCompletion` - 首页状态条关闭逻辑与「第一档提现成功」相同(C 端仍用 `status` / packages[0] completed) ### 钱包(slot_wallet) 新增 **`freeCreditsFirstCashKeep`**(`WalletLogic::run` 按 `type` 分发): - `inc(withdraw=fee, deposit=0, bonus=0)` 增加 **withdraw_balance** - **不**调用 `createTask` - 流水:`BIZ_TYPE_FREE_CREDITS_FIRST_KEEP = 'freeCreditsFirstCashKeep'`,`WALLET_TYPE_GIFT`(与 freeze/claim 一致) - 事务内完成入账 + `addLog`;成功后 `sendConsoleBus`(可选,用于统计/通知,类型建议 `freeCreditsFirstCashKeep`) 对比现有实现: | 能力 | 入账 | 打码任务 | | --- | --- | --- | | `freeCreditsClaim`(后续档) | deposit | Y1 `createTask` | | **`freeCreditsFirstCashKeep`(新)** | **withdraw** | **无** | ### slot_lib [`WalletService`](slot_lib/src/services/WalletService.php) 增加常量与方法: ```php const WALLET_TYPE_FREE_CREDITS_FIRST_KEEP = 'freeCreditsFirstCashKeep'; public function freeCreditsFirstCashKeep($amount, $bizId = ''): Wallet ``` ## 实现步骤 ### 1. slot_wallet | 文件 | 改动 | | --- | --- | | [`WalletLogModel.php`](slot_wallet/app/model/multi/WalletLogModel.php) | `BIZ_TYPE_FREE_CREDITS_FIRST_KEEP` | | [`WalletLogic.php`](slot_wallet/app/api/logic/WalletLogic.php) | `freeCreditsFirstCashKeep()`:镜像 `freeCreditsClaim` 事务结构,改为 `inc(fee,0,0)`,**删除** `createTask` 调用 | | 单测 | 新增 `WalletFreeCreditsFirstKeepTest`(或扩展现有 wallet 单测):断言 withdraw 增加、无 task 创建(可 mock `createTask` 不被调用) | ### 2. slot_lib - [`WalletService.php`](slot_lib/src/services/WalletService.php):常量 + `freeCreditsFirstCashKeep($amount, $bizId)` ### 3. slot_console — Logic [`FreeCreditsLogic.php`](slot_console/app/api/logic/FreeCreditsLogic.php) 新增 `keepFirstCash(int $uid, int $packageId): array`: 1. `assertEligibleParticipant` 2. 查询 package:`uid`、`TYPE_FIRST_CASH`、`STATUS_READY` 3. `bizId = 'free_credits_first_keep:' . $packageId` 4. package → `STATUS_PROCESSING`,写 `claim_biz_id` 5. `walletService->freeCreditsFirstCashKeep($package->amount_qf, $bizId)` 6. 成功 → package `COMPLETED` + 时间戳;player `STATUS_FIRST_CASH_DONE`;`refreshPlayerCompletion` 7. 失败 → package 回滚 `READY`(同 `claim`) 8. 返回 `buildStatus` **不**改 `advanceByRecharge` / 定格冻结逻辑。 ### 4. slot_console — API 层 | 文件 | 改动 | | --- | --- | | [`FreeCreditsValidator.php`](slot_console/app/api/validator/FreeCreditsValidator.php) | `SCENE_KEEP_FIRST_CASH`:`package_id` require\|integer | | [`FreeCreditsController.php`](slot_console/app/api/controller/FreeCreditsController.php) | `keepFirstCash()` + PHPDoc;类注释补充第二路径 | | 路由配置 | 注册 `POST /api/free-credits/keep-first-cash`(查项目现有 `route` / `config` 中 `free-credits` 注册方式,与 `claim` 并列) | ### 5. 广播 / 统计(小改) - [`buildBroadcastList`](slot_console/app/api/logic/FreeCreditsLogic.php):第一档 completed 若走 keep,广播 `action` 可新增 `keep`(或复用 `claim`);与产品确认文案前可先 `keep` - [`FreeCreditsStatsLogic`](slot_console/app/innerapi/logic/FreeCreditsStatsLogic.php):`first_cashout_*` 按「第一档 completed」统计,**keep 与 withdraw 均计入**(无需区分,除非运营后续要拆指标) ### 6. 单测(slot_console) | 文件 | 内容 | | --- | --- | | 新增 `FreeCreditsKeepFirstCashTest` | harness 注入 mock `WalletService`;断言状态迁移、幂等、非 ready 抛错 | | 可选集成测 | keep 后 package completed + player `FIRST_CASH_DONE` | 运行: ```bash docker exec -w /app/www/slot/slot_console php82 ./vendor/bin/phpunit tests/Unit/FreeCreditsKeepFirstCashTest.php docker exec -w /app/www/slot/slot_wallet php82 ./vendor/bin/phpunit tests/Unit/WalletFreeCreditsFirstKeepTest.php ``` ## C 端约定(供联调,本次可不改 PWA) 第一档 `packages[0].status === 1` 时展示两个入口: - **Withdraw** → 现有 `POST /api/withdraw/apply` + `package_id` - **Keep to Balance** → `POST /api/free-credits/keep-first-cash` + `package_id` 完成后 `packages[0].status === 3`,与提现成功 UI 一致(按钮隐藏 / 成功态)。 ## 不在本次范围 - slot_pwa / gateway 前端页面与文案 - 需求文档 §10–11 全文修订(可后续补「Add to Balance」分支) - 修改后续档 `claim` 的打码规则 - 第一档金额改由后台配置动态展示(仍从 `packages[0].amount` 读取) ## 验收清单 1. 第一档 ready:keep 成功 → withdraw_balance 增加对应千分位,**无**新 WagerTask 2. 同一 package 重复 keep:幂等,不重复加钱 3. keep 成功后:package/player 状态与提现成功一致;`status` 接口 packages[0] 为 completed 4. 第一档 processing(提现中):keep 拒绝 5. 第一档 completed:withdraw / keep 均拒绝 6. 相关单测通过