This commit is contained in:
ray zhou
2026-05-29 11:21:40 +08:00
parent 5d6d482efe
commit f71a5c59af
447 changed files with 32245 additions and 116 deletions

View File

@@ -0,0 +1,174 @@
---
name: firstCashout 合并提现
overview: 可以合并:推荐在 Withdraw 入口与 Pay 下单层统一,用 package_id 区分;活动档位状态机仍留在 FreeCreditsLogic不能把 FC 逻辑硬塞进 WithdrawService::apply 主流程中间。
todos:
- id: dto-package-id
content: WithdrawApplyDTO 增加 package_idWithdrawValidator 增加 FC scenepackage_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) 当前顺序L164219
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`,再 Paycatch 回滚
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`