--- name: firstCashout 合并提现 overview: 可以合并:推荐在 Withdraw 入口与 Pay 下单层统一,用 package_id 区分;活动档位状态机仍留在 FreeCreditsLogic,不能把 FC 逻辑硬塞进 WithdrawService::apply 主流程中间。 todos: - id: dto-package-id content: WithdrawApplyDTO 增加 package_id;WithdrawValidator 增加 FC scene(package_id 替代 amount) status: completed - id: withdraw-fc-branch content: WithdrawService::apply 增加 applyFreeCreditsFirstCashout early return(跳过 checkInfo/手续费/黑规则) status: completed - id: controller-unify content: WithdrawController::apply 统一入口;FreeCreditsController::firstCashout 改为兼容 alias status: completed - id: bank-persist content: FC 分支复用 checkBankInfo,与普通提现绑卡行为一致 status: completed - id: pay-rollback-test content: 补单测/集成测:FC 无 withdraw 余额可提交、Pay 失败回滚 ready status: completed isProject: false --- # firstCashout 能否合并进现有提现逻辑 ## 结论(后端) **可以合并,且推荐合并「入口 + 校验 + 绑卡 + Pay 下单」;不应把 Free Credits 编排塞进 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 主流程中间。** 当前仓库状态:**尚未合并**——仍是双入口: | 入口 | 现状 | |------|------| | [`POST /api/free-credits/first-cashout`](slot_console/app/api/controller/FreeCreditsController.php) | `FreeCreditsLogic::firstCashout` 完整编排 | | [`POST /api/withdraw/apply`](slot_console/app/api/controller/WithdrawController.php) | `WithdrawService::apply`,无 `package_id` | Pay 层**已经统一**:两类提现最终都走 [`WithdrawalOrderEntity::apply`](slot_pay/app/entity/WithdrawalOrderEntity.php),靠 `bizType=free_credit_first_cashout` 跳过 `withdrawFrozen` 并走独立 MQ 回调。 --- ## 为什么「可以」合并 C 端参数与需求 §11 一致:独立提现页 = 普通提现页精简版,收款字段相同,仅差: | 字段 | 普通提现 | Free Credits 第一档 | |------|----------|----------------------| | `amount` | 用户输入 | **不传**,服务端取 package | | `package_id` | 无 | **必填** | 因此用 **同一 `POST /api/withdraw/apply` + 可选 `package_id`** 区分业务是合理契约。 --- ## 为什么不能「整段并入」WithdrawService::apply [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 当前顺序(L164–219): 1. `checkBankInfo` 2. **`checkInfo`(校验钱包 withdraw 余额 ≥ amount)** 3. 黑规则、`getAmountAndFee`(手续费) 4. `PayService::apply` Free Credits 第一档资金在**活动池**,不在普通 `withdraw` 余额。若在中间插入 `firstCashout` 而不 **early return**,会先被 `checkInfo` 打成 `Insufficient balance`(评审计划已记录为 P0)。 此外 FC 固定规则与普通提现不同: | 项 | 普通提现 | FC 第一档 | |----|----------|-----------| | 金额来源 | 用户 `amount` | `package->amount_qf` | | 手续费 | `getAmountAndFee` | **0** | | 审核 | 动态 `getAuditType` + 黑规则 | **auditType=2** | | 钱包冻结 | 有 | **无**(pay 侧 bizType 分支) | | 活动状态 | 无 | `markFirstCashoutProcessing` / 失败回滚 | | Pay 失败回滚 | 钱包解冻 | **`handleFirstCashoutResult` 恢复 ready** | 这些差异属于 **Logic 编排**,符合 [`backend-layering`](file:///Users/ray/.cursor/rules/backend-layering.mdc):不应把 `FreeCreditsLogic` 整段搬进 `WithdrawService` 当「又一层 Service」。 --- ## 推荐合并结构 ```mermaid flowchart TB Client["POST /api/withdraw/apply"] Client --> Branch{package_id 存在?} Branch -->|否| Normal["WithdrawService::apply 原逻辑"] Branch -->|是| FC["WithdrawService::applyFreeCreditsFirstCashout"] FC --> Mark["FreeCreditsLogic::markFirstCashoutProcessing"] FC --> Pay["PayService::apply bizType=free_credit_first_cashout"] Normal --> Pay2["PayService::apply 普通"] Pay --> PayEntity["slot_pay WithdrawalOrderEntity"] Pay2 --> PayEntity ``` ### 合并层(推荐做) 1. **[`WithdrawController::apply`](slot_console/app/api/controller/WithdrawController.php)** - 有 `package_id` → 走 FC 分支(或 DTO 带 `package_id` 后交给 Service early return) - 无 `package_id` → 现有普通提现 2. **[`WithdrawValidator`](slot_console/app/api/validator/WithdrawValidator.php)** - 新增 FC scene:与普通 apply 对称,把 `amount` 换成 `package_id`(`SCENE_APPLY_CASH_FC` 等) 3. **[`WithdrawApplyDTO`](slot_console/app/api/dto/request/WithdrawApplyDTO.php)** - 增加可选 `package_id` 4. **绑卡** - FC 复用 `WithdrawService::checkBankInfo`(当前 `firstCashout` 只读绑卡、不写库,合并后应对齐普通提现) 5. **[`FreeCreditsController::firstCashout`](slot_console/app/api/controller/FreeCreditsController.php)** - 保留为 **兼容 alias**(内部转调 withdraw apply),或标记 deprecated ### 保持分离(必须) - [`FreeCreditsLogic`](slot_console/app/api/logic/FreeCreditsLogic.php):`markFirstCashoutProcessing`、`handleFirstCashoutResult`、package 状态机、`assertEligibleParticipant` - [`WithdrawService::applyFreeCreditsFirstCashout`](slot_console/app/service/WithdrawService.php)(新建私有方法): - 跳过 `checkInfo`、黑规则、`getAmountAndFee` - `amount = package->amount_qf`,`fee = 0`,`auditType = 2` - try/catch Pay,失败时 `handleFirstCashoutResult($orderId, false)` ### 已统一、无需再改 - **slot_pay**:`bizType` / `remark` 分支(跳过冻结、Success/Fail/Rejected → Console MQ) - **slot_console EventBus**:`FreeCreditsFirstCashoutSuccess/Fail/Rejected` → `handleFirstCashoutResult` --- ## 不推荐的做法 在 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 中间写: ```php $this->checkInfo(...); // ... if ($package_id > 0) { (new FreeCreditsLogic())->firstCashout(...); } $amountInfo = $this->getAmountAndFee(...); PayService::apply(...); ``` 会导致:余额校验失败、手续费错误、Pay 失败不回滚档位(processing 卡死)。 --- ## 与「单独提现」需求的关系 | 需求 | 合并后是否满足 | |------|----------------| | 不进入普通钱包(§15.3) | 是,仍靠 pay `bizType` | | 固定第一档金额(§11.3) | 是,服务端取 package | | 处理中不可重复提交(§5.6) | 是,仍由 package status 控制 | | 成功/失败/拒绝状态(§20.2) | 是,回调逻辑不变 | 合并的是 **HTTP 入口与收款参数校验**,不是改掉「独立提现」的账务语义。 --- ## 实施要点(若执行) 1. `WithdrawApplyDTO` 增加 `package_id` 2. `WithdrawService::apply` 在绑卡 + Redis 频控后 **early return** 到 `applyFreeCreditsFirstCashout` 3. FC 分支内:先 `markFirstCashoutProcessing`,再 Pay,catch 回滚 4. 单测:FC 在 `withdraw=0` 时可提交;Pay 失败 package 回 ready;普通 apply 无回归 5. 文档:`/api/free-credits/first-cashout` → 指向 `/api/withdraw/apply` + `package_id` --- ## 验收清单 - `package_id>0` 且钱包可提现余额为 0:可成功提交 pay - pay 申请失败:package 回到 `ready`,可重试 - pay 回调成功:package `completed`,player `FIRST_CASH_DONE` - 不传 `package_id`:普通提现与现网一致 - pay 订单 `remark` 仍为 `free_credit_first_cashout`,不触发 `withdrawFrozen`