--- name: 第一档提现审核拒绝 overview: 补齐管理后台「拒绝退钱/拒绝扣钱」对 Free Credits 第一档独立提现(free_credit_first_cashout)的处理:Pay 侧识别业务类型、避免误动钱包,并通过已有 Console 总线回调恢复档位状态。 todos: - id: persist-biz-type content: WithdrawalOrderEntity::creatOrder 写入 data_snapshot.biz_type;新增 isFreeCreditFirstCashout 判断 status: in_progress - id: apply-skip-freeze content: apply() 第一档跳过 withdrawFrozen 与 incTodayWithdrawalInfo status: pending - id: audit-reject-branch content: audit() 拒绝退钱/扣钱:第一档跳过钱包,发 Fail/Rejected 总线;普通单保持原逻辑 status: pending - id: mq-constants content: slot_lib MQBusEntity 增加三条 FreeCredits 事件常量 status: pending - id: event-withdrawal content: EventWithdrawal 成功/失败按 biz_type 发 Success/Fail 总线,避免误 withdrawSuccess/Fail status: pending - id: tests content: 补充 slot_pay 单测 + 手工验证后台拒绝退钱/扣钱 status: pending isProject: false --- # 第一档提现:后台审核拒绝处理 ## 问题确认 管理后台 [`slot_admin_vue/.../withdrawal/index.vue`](backend/slot_admin_vue/src/views/game/order/withdrawal/index.vue) 点「拒绝退钱」(`audit_status=2`) / 「拒绝扣钱」(`audit_status=3`) → [`WithdrawalOrderController::audit`](backend/slot_admin/app/game/controller/WithdrawalOrderController.php) → `PayService::withdrawalAudit` → [`WithdrawalOrderEntity::audit`](slot_pay/app/entity/WithdrawalOrderEntity.php)。 当前 `audit()` **一律**对普通提现调用 `withdrawFail` / `withdrawDone`,**不**通知 `slot_console` 的 `FreeCreditsLogic::handleFirstCashoutResult`。 第一档提现([`FreeCreditsLogic::applyFirstCashoutWithdraw`](slot_console/app/api/logic/FreeCreditsLogic.php),`bizType=free_credit_first_cashout`)在提交时已 `markFirstCashoutProcessing`,审核拒绝后档位会卡在 **processing**,C 端无法再次提现。 需求文档约定([§20.2](docs/requirements/首充前免费余额定格与分档释放需求文档.md)): | 后台操作 | 产品语义 | 应对 `handleFirstCashoutResult` | |----------|----------|----------------------------------| | 拒绝退钱 | 失败,可重试 | `success=false, rejected=false` → 档位 **ready** | | 拒绝扣钱 | 风控拒绝 | `success=false, rejected=true` → 档位 **rejected** | `slot_console` 已有消费端([`EventBus.php`](slot_console/app/command/EventBus.php) L133–140),但 **全库无生产端** 发送 `FreeCreditsFirstCashoutFail` / `Rejected` / `Success`。 ```mermaid flowchart TB subgraph today [现状] adminReject[后台拒绝退钱] payAudit[Pay audit] walletFail[withdrawFail] adminReject --> payAudit --> walletFail end subgraph target [目标] adminReject2[后台拒绝退钱] payAudit2[Pay audit 识别 bizType] skipWallet[跳过钱包退冻] mqFail[MQ FreeCreditsFirstCashoutFail] fcLogic[handleFirstCashoutResult ready] adminReject2 --> payAudit2 --> skipWallet payAudit2 --> mqFail --> fcLogic end ``` ## 改造范围(主改 `slot_pay`,常量补 `slot_lib`) ### 1. 订单落库时持久化 `biz_type` 文件:[`slot_pay/app/entity/WithdrawalOrderEntity.php`](slot_pay/app/entity/WithdrawalOrderEntity.php) — `creatOrder()` - 若 `WithdrawalInfo::$bizType` 非空,写入 `data_snapshot.biz_type`(表无独立字段,用现有 JSON 即可)。 - 常量与 C 端一致:`free_credit_first_cashout`(见 [`FreeCreditsLogic::BIZ_TYPE_FIRST_CASHOUT`](slot_console/app/api/logic/FreeCreditsLogic.php))。 新增私有方法(同文件): ```php private function isFreeCreditFirstCashout(?WithdrawalInfo $info = null, ?self $order = null): bool ``` - 优先读 `$info->bizType`;审核/回调阶段读 `$order->data_snapshot['biz_type']`。 ### 2. 申请提现:第一档不冻普通钱包 文件:同上 — `apply()` - `isFreeCreditFirstCashout($info)` 为 true 时: - **跳过** `withdrawFrozen`、`UserTagService::incTodayWithdrawalInfo`。 - 仍 `creatOrder`、人工审核分支、`WebSocketMqService::sendWithdrawalUrl` 保持不变。 - `catch` 中仅当曾冻结时才 `withdrawFail`(与现逻辑一致,第一档不会进入)。 对齐 [`WithdrawalInfo` 注释](slot_lib/src/entity/data/WithdrawalInfo.php) 与需求「第一档不进入普通钱包 withdraw」。 ### 3. 审核拒绝/扣钱:分支 + 发总线(核心) 文件:同上 — `audit()` 在 `audit_status` 设为 2 或 3 后: | 条件 | 钱包 | Console 总线 `type` | `handleFirstCashoutResult` | |------|------|---------------------|----------------------------| | 普通提现 + 拒绝退钱 | `withdrawFail` | 无 | — | | 普通提现 + 拒绝扣钱 | `withdrawDone` | 无 | — | | 第一档 + 拒绝退钱 | **不调**钱包;**不调** `decTodayWithdrawalInfo` | `FreeCreditsFirstCashoutFail` | ready,可重提 | | 第一档 + 拒绝扣钱 | **不调**钱包 | `FreeCreditsFirstCashoutRejected` | rejected | 实现方式(Pay 依赖已有 `ConsoleMqService`): ```php use slotLib\common\mq\ConsoleMqService; // audit 拒绝分支内: if ($this->isFreeCreditFirstCashout(null, $order)) { $event = $type === self::AUDIT_STATUS_REVIEW ? MQBusEntity::TYPE_FREE_CREDITS_FIRST_CASHOUT_FAIL : MQBusEntity::TYPE_FREE_CREDITS_FIRST_CASHOUT_REJECTED; ConsoleMqService::getInstance()->sendConsoleBusEvent($order->uid, $event, [ 'order_id' => $order->order_id, ]); } else { // 现有 withdrawFail / withdrawDone + decTodayWithdrawalInfo } ``` 站内信逻辑可保留(第一档同样通知用户)。 ### 4. 总线常量集中到 `slot_lib` 文件:[`slot_lib/src/entity/mq/MQBusEntity.php`](slot_lib/src/entity/mq/MQBusEntity.php) 新增(与 console 现有字符串一致): - `TYPE_FREE_CREDITS_FIRST_CASHOUT_SUCCESS = 'FreeCreditsFirstCashoutSuccess'` - `TYPE_FREE_CREDITS_FIRST_CASHOUT_FAIL = 'FreeCreditsFirstCashoutFail'` - `TYPE_FREE_CREDITS_FIRST_CASHOUT_REJECTED = 'FreeCreditsFirstCashoutRejected'` [`slot_console/app/entity/mq/MQBusEntity.php`](slot_console/app/entity/mq/MQBusEntity.php) 可改为 `use slotLib\entity\mq\MQBusEntity as LibMQBusEntity` 引用常量(可选,避免双份字符串)。 ### 5. 顺带补齐:打款结果回调(建议同 PR) [`EventWithdrawal::updateOrder()`](slot_pay/app/command/EventWithdrawal.php) 在成功/失败时同样未区分第一档,会误调 `withdrawSuccess` / `withdrawFail`。 在同一 PR 中按 `data_snapshot.biz_type` 分支: - 成功 → 发 `FreeCreditsFirstCashoutSuccess`,**不** `withdrawSuccess` - 失败 → 发 `FreeCreditsFirstCashoutFail`,**不** `withdrawFail` - 普通单保持现状 否则后台「通过」后渠道失败/成功,档位状态仍会错乱。 ## 不改动的部分 - **管理后台 UI**:无需改,仍调 `audit(record, 2|3)`。 - **`FreeCreditsLogic::handleFirstCashoutResult`**:逻辑已满足需求,只补 Pay 侧触发。 - **数据库 DDL**:用 `data_snapshot`,无需加列。 ## 历史订单 已产生、且 `data_snapshot` 无 `biz_type` 的第一档单: - 拒绝时仍会走旧 `withdrawFail`(若当时已冻钱包则退钱正确,但档位仍卡 processing)。 - 运维可对已知 `order_id` 在 console 手工调用 `handleFirstCashoutResult`,或一次性 SQL 回填 `data_snapshot.biz_type`(按 `free_credits_package.withdraw_order_id` 关联)。 ## 测试 | 层级 | 内容 | |------|------| | `slot_pay` 单测 | `creatOrder` 写入 `biz_type`;`isFreeCreditFirstCashout`;`audit(2)` 第一档 mock 不发 `withdrawFail`、发 MQ(mock `ConsoleMqService`) | | `slot_console` 已有 | [`FreeCreditsLogicCashoutResultTest`](slot_console/tests/Integration/FreeCreditsLogicCashoutResultTest.php) 覆盖 `handleFirstCashoutResult` | | 手工 | 后台对第一档人工单:拒绝退钱 → C 端档位回 ready 可再提;拒绝扣钱 → rejected | ## 涉及文件小结 - [`slot_pay/app/entity/WithdrawalOrderEntity.php`](slot_pay/app/entity/WithdrawalOrderEntity.php) — `creatOrder` / `apply` / `audit` + helper - [`slot_pay/app/command/EventWithdrawal.php`](slot_pay/app/command/EventWithdrawal.php) — 打款结果分支(建议同 PR) - [`slot_lib/src/entity/mq/MQBusEntity.php`](slot_lib/src/entity/mq/MQBusEntity.php) — 事件常量 - (可选)[`slot_console/app/entity/mq/MQBusEntity.php`](slot_console/app/entity/mq/MQBusEntity.php) — 引用 lib 常量