--- name: firstCashout 合并代码评审 overview: 你对「merge post + 统一 WithdrawService::apply」的改法方向正确,但当前 WithdrawService 在 package_id>0 时仍执行 checkInfo/手续费/黑规则,且 Pay 失败无回滚,会导致第一档提现不可用或卡在 processing。 todos: - id: fc-early-return content: WithdrawService::apply 中 package_id>0 走独立 applyFreeCreditsFirstCashout 并 early return status: completed - id: skip-checkinfo-fc content: FC 分支跳过 checkInfo、getAmountAndFee、黑规则(或产品确认保留项) status: completed - id: pay-fail-rollback content: Pay apply 失败时 handleFirstCashoutResult 回滚 processing status: completed - id: fc-withdrawal-info content: FC 使用 package amount_qf、fee=0、auditType=2 组 WithdrawalInfo status: completed isProject: false --- # firstCashout 合并改动 — 代码评审 ## 你的改动(理解正确) ```mermaid sequenceDiagram participant Ctrl as WithdrawController participant FC as FreeCreditsLogic participant WS as WithdrawService participant Pay as slot_pay Ctrl->>FC: mergeFirstCashoutIntoPost Note over FC: amount + bizType + package_id Ctrl->>WS: apply(DTO) WS->>FC: firstCashout(uid, packageId, orderId) Note over FC: markFirstCashoutProcessing WS->>Pay: apply(WithdrawalInfo) ``` - [`WithdrawController::apply`](slot_console/app/api/controller/WithdrawController.php):有 `package_id` 先 merge,再**同一套** `validate($type)` — 符合预期。 - [`mergeFirstCashoutIntoPost`](slot_console/app/api/logic/FreeCreditsLogic.php):补 `amount` / `bizType` / `package_id` — 符合预期。 - [`firstCashout`](slot_console/app/api/logic/FreeCreditsLogic.php) 收窄为只 `markFirstCashoutProcessing` + 外部统一 `PayService::apply` — 思路可行。 --- ## P0:FC 仍会走 `checkInfo`,大概率直接失败 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 当前顺序: ```php $bankInfo = $this->checkBankInfo($applyDTO); $this->checkInfo($applyDTO->type, $applyDTO->amount); // 始终执行 // ... if ($applyDTO->package_id > 0) { firstCashout(...); // 永远走不到(checkInfo 已抛错) } ``` `checkInfo` 会校验**钱包可提现余额** ≥ amount([`WithdrawService.php` L333-335](slot_console/app/service/WithdrawService.php))。 Free Credits 第一档资金在**活动池**,不在普通 `withdraw` 余额里 → 典型报错 **`Insufficient balance`**。 **结论**:与需求「不进入普通钱包」冲突,第一档线上基本提不了现。 **建议**:`package_id > 0`(或 `bizType === free_credit_first_cashout`)时 **跳过** `checkInfo`(及与之绑定的首提 min/max 规则)。 --- ## P0:Pay 失败时档位已置为 processing,无回滚 你现在在 **Pay 之前** 调用 `firstCashout` → `markFirstCashoutProcessing`(status=2)。 原实现是:mark → `PayService::apply` → **catch 时** `handleFirstCashoutResult($orderId, false)` 恢复 ready。 当前 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) L236 调 pay **没有** try/catch 回滚 → 申请失败时 package 会一直 **processing**,用户无法重试。 **建议**: ```php if ($applyDTO->package_id > 0) { $orderId = CommonFn::generateOrderId(4, $userTag->uid); (new FreeCreditsLogic())->firstCashout($userTag->uid, $applyDTO->package_id, $orderId); $withdrawalInfo->orderId = $orderId; $withdrawalInfo->bizType = FreeCreditsLogic::BIZ_TYPE_FIRST_CASHOUT; try { $res = PayService::getInstance()->apply($withdrawalInfo->toArray()); return $res; } catch (\Throwable $e) { (new FreeCreditsLogic())->handleFirstCashoutResult($orderId, false); throw $e; } } ``` (FC 分支应 **early return**,不要继续走下面黑规则 + `getAmountAndFee`。) --- ## P1:FC 仍走普通手续费 / 黑规则 / auditType 合并后 FC 路径仍执行: - `getAuditType($applyDTO->amount)`、`BlackApiService`、提现倍数检测 - `getAmountAndFee($applyDTO->amount, $this->withdrawal)` — 可能扣手续费、按钱包余额改金额 原 [`firstCashout`](slot_console/app/api/logic/FreeCreditsLogic.php) 约定: - `fee = 0` - `auditType = 2`(人工审核) - `amount = package->amount_qf`(千分位,不经手续费逻辑) `merge` 写入的 `amount` 是 **展示大单位**(`getNumberFormat(qf)`),再经 `getAmountAndFee` 可能与 pay 侧期望的千分位不一致。 **建议**:FC 分支单独组 `WithdrawalInfo`: | 字段 | FC 取值 | |------|---------| | amount | 从 package 读 `amount_qf`(或 merge 时额外缓存 `_amount_qf`) | | fee | 0 | | auditType | 2 | | bizType | `free_credit_first_cashout` | | 黑规则 | 跳过(或产品确认是否要对 FC 也做) | --- ## P1:`firstCashout` 与 `markFirstCashoutProcessing` 重复校验 [`firstCashout`](slot_console/app/api/logic/FreeCreditsLogic.php) 内再次查 `STATUS_READY` 后调用 `markFirstCashoutProcessing`,而 `markFirstCashoutProcessing` ** again** 要求 `STATUS_READY`(L343)。 逻辑重复但无害;可简化为只调 `markFirstCashoutProcessing`,或只保留一处校验。 注意:`mergeFirstCashoutIntoPost` 已校验 ready,到 `firstCashout` 时若并发重复提交,第二次会在 mark 阶段失败 — 符合「处理中不可重复提交」。 --- ## P2:行为变化(需产品确认) | 项 | 原 FC | 现统一 apply | |----|--------|----------------| | `is_bind_name` | 不校验 | **校验**(L157) | | Redis 5s 频控 | 无 | **有** | | 返回值 | `{order_id, amount}` | pay `apply` 原始结构 | 若 C 端依赖 `data.order_id` / `data.amount` 展示,需确认 pay 返回是否一致。 --- ## 做得好的部分 - Controller 单入口 + merge post:**正确**。 - 去掉独立 FC validator scene:**正确**。 - [`FreeCreditsController::firstCashout`](slot_console/app/api/controller/FreeCreditsController.php) 兼容路径与主入口一致:**正确**。 - pay 层仍靠 `bizType` 跳过 `withdrawFrozen`:**只要 bizType 确实传到 pay 就没问题**(当前 L184 已设)。 --- ## 推荐修复结构(最小 diff) 在 [`WithdrawService::apply`](slot_console/app/service/WithdrawService.php) 开头(绑卡、频控之后): ```php if ($applyDTO->package_id > 0) { return $this->applyFreeCreditsFirstCashout($applyDTO, $bankInfo); } // 原有普通提现逻辑不变 ``` `applyFreeCreditsFirstCashout` 内: 1. 跳过 `checkInfo`、黑规则、`getAmountAndFee` 2. `orderId` → `firstCashout`(mark)→ 组 `WithdrawalInfo`(qf amount, fee=0, auditType=2, bizType) 3. try/catch pay + `handleFirstCashoutResult` 回滚 **不要**在普通流程中间用 `if ($package_id > 0) { firstCashout; }` 再接着跑普通逻辑 — 这是当前问题的根源。 --- ## 验收清单 - [ ] `package_id>0` 且钱包 withdraw=0:能成功提交 pay(不报 Insufficient balance) - [ ] pay 申请失败:package 回到 ready,可再次提现 - [ ] pay 成功 + 回调:package completed,player `FIRST_CASH_DONE` - [ ] 普通提现无 `package_id`:行为与改前一致 - [ ] `remark`/bizType 在 pay 侧仍为 `free_credit_first_cashout`,不冻结钱包