--- name: 第一档提现统计入账 overview: 第一档免打码提现成功后,Pay 的 `EventWithdrawal` 在发 Console 总线之外,需经 slot_lib 调用 wallet 新增「仅累加 total_withdraw」接口,把提现金额记入分片 `wallet_stat_xx`,与普通提现 `withdrawSuccess` 的统计口径对齐。 todos: - id: wallet-stat-method content: slot_wallet:WalletLogModel 常量 + WalletLogic::freeCreditsFirstCashoutStat(inc total_withdraw + 幂等) status: completed - id: slot-lib-client content: slot_lib WalletService:常量 + freeCreditsFirstCashoutStat() 封装 HTTP update status: completed - id: pay-event-hook content: slot_pay EventWithdrawal:第一档成功分支调用 freeCreditsFirstCashoutStat(order_id, amount) status: completed - id: tests content: wallet 单测幂等与 stat inc;pay 单测/mock 验证调用链 status: completed - id: backfill-optional content: (可选)历史成功第一档订单回填 total_withdraw 脚本说明 status: completed isProject: false --- # 第一档提现成功:wallet_stat 累计提现额补齐 ## 背景与缺口 首充定格第一档(`free_credit_first_cashout`)在 [EventWithdrawal.php](slot_pay/app/command/EventWithdrawal.php) 打款成功时已按设计 **跳过** `withdrawSuccess` / `withdrawFail`,只发 Console 总线: ```117:122:slot_pay/app/command/EventWithdrawal.php if (WithdrawalOrderEntity::isFreeCreditFirstCashoutOrder($order)) { ConsoleMqService::getInstance()->sendConsoleBusEvent($order->uid, MQBusEntity::TYPE_FREE_CREDITS_FIRST_CASHOUT_SUCCESS, [ 'order_id' => $order->order_id, ]); } else { $walletService->withdrawSuccess($withdrawalAmount, $order->order_id); ``` 普通提现的 `total_withdraw` 仅在 [WalletLogic::withdraw()](slot_wallet/app/api/logic/WalletLogic.php)(`type=withdraw`)里 `WalletStatModel::inc(..., 'total_withdraw', fee)`,且会 `finishWithdraw` 扣减 `withdraw_lock`。 第一档 **不冻结** 普通钱包([WithdrawalOrderEntity::apply](slot_pay/app/entity/WithdrawalOrderEntity.php) 已跳过 `withdrawFrozen`),因此 **不能** 直接调 `withdrawSuccess`——会动 `withdraw_lock` 并可能失败。 遗留问题:**`wallet_stat_{xx}.total_withdraw` 未累加**,后台用户列表/盈亏等读 `tw`/`total_withdraw` 会偏小。 ```mermaid sequenceDiagram participant Pay as slot_pay EventWithdrawal participant Console as slot_console EventBus participant Wallet as slot_wallet WalletLogic Note over Pay: 现状(成功) Pay->>Console: FreeCreditsFirstCashoutSuccess Note over Wallet: total_withdraw 未更新 Note over Pay: 目标(成功) Pay->>Console: FreeCreditsFirstCashoutSuccess Pay->>Wallet: freeCreditsFirstCashoutStat(order_id, amount) Wallet->>Wallet: inc total_withdraw + wallet_log ``` --- ## 推荐方案:wallet 新增「仅统计」类型 与现有 `freeCreditsFreeze` / `freeCreditsFirstCashKeep` 一致:走 `POST api/wallet/update`,`WalletLogic::run()` 按 `type` 分发,**不改** `wallet_account` 余额与 `withdraw_lock`。 ### 1. slot_wallet | 文件 | 改动 | |------|------| | [WalletLogModel.php](slot_wallet/app/model/multi/WalletLogModel.php) | 新增 `BIZ_TYPE_FREE_CREDITS_FIRST_CASHOUT_STAT = 'freeCreditsFirstCashoutStat'` | | [WalletLogic.php](slot_wallet/app/api/logic/WalletLogic.php) | 新增 `freeCreditsFirstCashoutStat()`:`fee>0` 时事务内 `WalletStatModel::inc(currency, 'total_withdraw', fee)` + `addLog(0, balance, 新 biz_type, WALLET_TYPE_GIFT)`(流水 amount=0 或记负向备注,与 first_keep 风格一致) | | 幂等 | `biz_id` 使用 **提现订单号** `order_id`(与 `withdrawSuccess` 一致);`wallet_log` 表已有 `uniq_uid_bizid_type (uid, biz_id, biz_type)`。重复 MQ 消费时 `addLog` 唯一键冲突应 **视为成功**(查已有流水则直接返回当前 wallet 快照,不二次 `inc`)——可参考项目内其它 Free Credits 写法的异常处理,若无统一模式则在方法开头 `findOne(['biz_id','biz_type'])` 短路 | **刻意不做:** - 不调 `finishWithdraw` / `doneWithdraw` - 不 `sendConsoleBus('Withdrawal')`(避免与普通提现总线混淆) - 失败/拒绝路径 **不** dec `total_withdraw`(申请时未 inc,与 `manualRefund` 对称性无关) ### 2. slot_lib | 文件 | 改动 | |------|------| | [WalletService.php](slot_lib/src/services/WalletService.php) | 常量 `WALLET_TYPE_FREE_CREDITS_FIRST_CASHOUT_STAT = 'freeCreditsFirstCashoutStat'` + 方法 `freeCreditsFirstCashoutStat($amount, $bizId = '')` | Pay 继续用现有 `slotLib\services\WalletService`(与 `withdrawSuccess` 同路径),**不**在本需求引入 `slot_sdk`(与当前 pay→wallet 一致)。 ### 3. slot_pay | 文件 | 改动 | |------|------| | [EventWithdrawal.php](slot_pay/app/command/EventWithdrawal.php) | 第一档成功分支:在发 Console 总线 **之后**(或之前,顺序无关)调用 `$walletService->freeCreditsFirstCashoutStat($withdrawalAmount, $order->order_id)` | | 可选对齐 | 普通成功会 `UserTagService::incSuccessWithdrawalInfo`;若产品希望第一档也计入「成功提现次数/金额」标签,可同分支补上;**若仅关心 wallet_stat,可不加**(需你确认时可单独加) | 失败/拒绝分支 **保持现状**(只发 Fail/Rejected 总线,不调 wallet)。 ### 4. 测试 | 仓库 | 内容 | |------|------| | `slot_wallet` | 单测:`freeCreditsFirstCashoutStat` 累加 `total_withdraw`、相同 `biz_id` 幂等 | | `slot_pay` | 扩展 [WithdrawalOrderFreeCreditsTest.php](slot_pay/tests/Unit/WithdrawalOrderFreeCreditsTest.php) 或 mock `EventWithdrawal::updateOrder`:第一档成功应调用新方法、不调 `withdrawSuccess` | --- ## 历史数据 已打款成功、但 `total_withdraw` 未记的第一档订单:可写一次性运维脚本(按 `free_credits_package.withdraw_order_id` + pay 订单金额)批量调新接口或 SQL `inc`,`biz_id` 用原 `order_id` 保证幂等。 --- ## 涉及文件小结 - [slot_wallet/app/api/logic/WalletLogic.php](slot_wallet/app/api/logic/WalletLogic.php) - [slot_wallet/app/model/multi/WalletLogModel.php](slot_wallet/app/model/multi/WalletLogModel.php) - [slot_lib/src/services/WalletService.php](slot_lib/src/services/WalletService.php) - [slot_pay/app/command/EventWithdrawal.php](slot_pay/app/command/EventWithdrawal.php) - 测试:`slot_wallet/tests/Unit/...`、`slot_pay/tests/Unit/WithdrawalOrderFreeCreditsTest.php` **不改:** `FreeCreditsLogic::handleFirstCashoutResult`(档位状态已覆盖)、Console EventBus 消费端。