175 lines
8.4 KiB
Markdown
175 lines
8.4 KiB
Markdown
---
|
||
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 常量
|