{"role":"user","message":{"content":[{"type":"text","text":"\nwallet服,现在几乎所有的logic都在WalletLogic文件里面,导致这个文件都快2000行了,是不是可以拆分一下\n"}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"我先查看 wallet 服务的 `WalletLogic` 结构和项目里其他 Logic 的拆分方式,再给出可执行的拆分方案。\n\n[REDACTED]"},{"type":"tool_use","name":"Glob","input":{"glob_pattern":"**/WalletLogic.php"}},{"type":"tool_use","name":"Glob","input":{"glob_pattern":"**/slot_wallet/**/logic/**/*.php"}},{"type":"tool_use","name":"Grep","input":{"pattern":"class WalletLogic","glob":"**/*.php"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php","limit":150}},{"type":"tool_use","name":"Glob","input":{"glob_pattern":"**/slot-wallet/app/**/logic/*.php"}},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php","pattern":"^\\s*(public|private|protected)\\s+function\\s+"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/GrantLogic.php","limit":80}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/PlayerTaskLogic.php","limit":80}},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet","pattern":"WalletLogic"}},{"type":"tool_use","name":"Read","input":{"offset":150,"limit":200,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php","pattern":"^\\s*(public|private|protected)\\s+static\\s+function"}},{"type":"tool_use","name":"Read","input":{"limit":100,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/controller/WalletController.php"}},{"type":"tool_use","name":"Glob","input":{"glob_pattern":"**/slot-*/app/api/logic/*.php"}},{"type":"tool_use","name":"Read","input":{"limit":100,"offset":1750,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Glob","input":{"glob_pattern":"**/slot-wallet/app/service/wallet/*.php"}},{"type":"tool_use","name":"Read","input":{"limit":200,"offset":540,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php"}},{"type":"tool_use","name":"Read","input":{"limit":200,"offset":1040,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php"}},{"type":"tool_use","name":"Read","input":{"limit":200,"offset":1240,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"limit":80,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/service/wallet/BetService.php"}},{"type":"tool_use","name":"Read","input":{"limit":80,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/service/wallet/WinService.php"}},{"type":"tool_use","name":"Read","input":{"limit":100,"offset":350,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php"}},{"type":"tool_use","name":"Read","input":{"limit":100,"offset":750,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php"}},{"type":"tool_use","name":"Read","input":{"limit":100,"offset":1570,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet","pattern":"->minus\\(|'minus'"}},{"type":"tool_use","name":"Read","input":{"limit":80,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/README.md","offset":40}},{"type":"tool_use","name":"Shell","input":{"command":"wc -l /Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php","description":"Count WalletLogic.php lines"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet","pattern":"switchBonus|bonusToDeposit|grantTrial|expireTrial|runRechargeOrder"}},{"type":"tool_use","name":"Read","input":{"limit":120,"path":"/Users/ray/Documents/project/www/ray/slot-console/app/api/logic/GameLogic.php"}},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet","pattern":"ACTION_METHOD_MAP|BIZ_TYPE_","glob":"**/WalletLogModel.php"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray","pattern":"switchBonus|BIZ_TYPE_BONUS_CONVERTED","glob":"**/*.{php,md}"}},{"type":"tool_use","name":"Read","input":{"limit":50,"offset":1848,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"结构已理清:`WalletLogic` 约 1885 行,且项目里已有 `GrantLogic` 等按用例拆分的先例;接下来给出拆分方案。\n\n[REDACTED]"},{"type":"tool_use","name":"AskQuestion","input":{"title":"WalletLogic 拆分偏好","questions":[{"id":"migration_style","prompt":"拆分方式你更倾向哪种?","options":[{"id":"phased","label":"分阶段迁移(推荐):先抽 Query/Register,再 BetWin、Recharge、Withdraw/Trial;每步可独立上线"},{"id":"one_shot","label":"一次性大拆:一次 PR 拆完所有域,WalletLogic 只保留 run 分发门面"}]},{"id":"controller_change","prompt":"Controller 层是否也要改?","options":[{"id":"facade_only","label":"保持现状(推荐):WalletController 仍只注入 WalletLogic,内部分发到子 Logic"},{"id":"inject_sub_logics","label":"Controller 直接注入各子 Logic(如 BetWinLogic、RechargeLogic),WalletLogic 逐步废弃"}]}]}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"CreatePlan","input":{"name":"WalletLogic 分阶段拆分","overview":"将约 1885 行的 WalletLogic 按业务域拆成多个子 Logic,WalletLogic 保留为 run/runRechargeOrder 分发门面;分 4 个阶段迁移,Controller 与对外 API 不变。","plan":"# WalletLogic 分阶段拆分方案\n\n## 现状诊断\n\n[`slot-wallet/app/api/logic/WalletLogic.php`](slot-wallet/app/api/logic/WalletLogic.php) 当前 **1885 行**,承担了钱包服务几乎全部资金用例编排:\n\n| 域 | 主要方法 | 约行数 | 已有下沉 |\n|---|---|---|---|\n| 路由门面 | `run`, `runRechargeOrder`, `ACTION_METHOD_MAP` | ~80 | — |\n| 注册 | `register`, `initWalletWithoutMoney` | ~120 | [`RegisterService`](slot-wallet/app/service/wallet/RegisterService.php) |\n| 下注/派奖 | `bet`, `win`, pending win Redis, bet split | ~450 | [`BetService`](slot-wallet/app/service/wallet/BetService.php), [`WinService`](slot-wallet/app/service/wallet/WinService.php) |\n| 入账/充值 | `inc`, `recharge`, `rechargeSign`, `reward`, Trial Pool | ~400 | [`RechargeFundLotService`](slot-wallet/app/service/wallet/RechargeFundLotService.php) |\n| 提现/Bonus 转换 | `withdraw*`, `rollbackWithdraw`, `bonusToDeposit`, `switchBonus` | ~350 | [`WithdrawTaskService`](slot-wallet/app/service/wallet/WithdrawTaskService.php) |\n| Trial 体验金 | `grantTrial`, `expireTrial` | ~250 | — |\n| 查询 | `query`, `queryBizLog` (static) | ~70 | — |\n| 公共能力 | `addLog`, `maxRetry`, `sendConsoleBus`, `getBalance` 等 | ~200 | — |\n\n**问题**:单文件混合 6+ 业务域,事务边界、幂等、Redis 辅助状态交织,Review 和改动风险都高。\n\n**已有良好先例**:同目录 [`GrantLogic`](slot-wallet/app/api/logic/GrantLogic.php)、[`PlayerTaskLogic`](slot-wallet/app/api/logic/PlayerTaskLogic.php) 已是「一用例一 Logic」;核心扣款/派奖细节已在 Service 层,Logic 层再拆是合理下一步。\n\n**额外发现**(非本次必做):`minus()` 未出现在 `ACTION_METHOD_MAP` 且无调用方,疑似历史遗留,可在 Phase 0 确认后删除。\n\n---\n\n## 目标架构\n\n```mermaid\nflowchart TB\n subgraph controller [Controller 不变]\n WC[WalletController]\n end\n\n subgraph facade [门面层]\n WL[WalletLogic]\n end\n\n subgraph domain [子 Logic 按域]\n WQ[WalletQueryLogic]\n WRg[WalletRegisterLogic]\n WBw[WalletBetWinLogic]\n WRe[WalletRechargeLogic]\n WWd[WalletWithdrawLogic]\n WTr[WalletTrialLogic]\n end\n\n subgraph shared [共享]\n Base[AbstractWalletMutationLogic]\n end\n\n subgraph service [已有 Service 不动]\n BS[BetService]\n WS[WinService]\n RS[RegisterService]\n end\n\n WC --> WL\n WL --> WRg & WBw & WRe & WWd & WTr\n GrantLogic --> WQ\n WL --> WQ\n WRg & WBw & WRe & WWd & WTr --> Base\n WBw --> BS & WS\n WRg --> RS\n```\n\n**原则**(对齐 [`backend-layering`](.cursor/rules/backend-layering.mdc)):\n\n- **Logic 拆用例、Service 保公共能力**:不再新建「只做 Model 转发」的 Service。\n- **WalletLogic 只做分发 + 缓存清理**:保留 `run()` / `runRechargeOrder()` 对外签名;`ACTION_METHOD_MAP` 改为指向子 Logic 方法。\n- **Controller 不改**:继续只注入 `WalletLogic`(你已确认)。\n- **共享状态收敛**:子 Logic 继承抽象基类,统一持有 `WalletUpdateRequestDTO`;`rechargeOrderDto` 仅留在 `WalletRechargeLogic`。\n\n---\n\n## 共享基类设计\n\n新建 [`slot-wallet/app/api/logic/AbstractWalletMutationLogic.php`](slot-wallet/app/api/logic/AbstractWalletMutationLogic.php):\n\n```php\nabstract class AbstractWalletMutationLogic\n{\n protected WalletUpdateRequestDTO $requestDTO;\n\n /** 绑定本次 mutation 请求上下文 */\n protected function bindRequest(WalletUpdateRequestDTO $requestDTO): void { ... }\n\n /** 写 wallet_log、PWA 流水、乐观锁重试、Console Bus 等 */\n protected function addLog(...): int { ... }\n protected function addTransactionRecord(...): void { ... }\n protected function getBalance(WalletAccountModel $model): int { ... }\n protected function maxRetry(callable $callback, int $retryCount = 3): WalletEntity { ... }\n protected function sendConsoleBus(string $type, int $amount): void { ... }\n protected function initWalletWithoutMoney(): ?WalletAccountModel { ... }\n}\n```\n\n- 从现有 `WalletLogic` **原样迁移**上述 protected/private 方法,避免行为变化。\n- 子 Logic 方法签名改为显式接收 DTO 或在门面 `bindRequest()` 后调用,**不再依赖 `$this->requestDTO` 隐式全局状态跨文件散落**。\n\n---\n\n## 子 Logic 划分\n\n| 新类 | 职责 | 从 WalletLogic 迁出的方法 |\n|---|---|---|\n| `WalletQueryLogic` | 只读查询 | `query`, `queryBizLog` |\n| `WalletRegisterLogic` | 注册赠金 | `register` |\n| `WalletBetWinLogic` | 游戏下注/派奖编排 | `bet`, `win`, `executeTrialBet/Win`, `executeFreeSpinPaidBet`, pending win / bet split Redis, `getWinAllocationByRound`, `buildBetSplitRemark` |\n| `WalletRechargeLogic` | 入账/充值 | `inc`, `executeIncWithTransaction`, `recharge`, `rechargeSign`, `reward`, Trial Pool 扣减链, `resolveRechargeOrderDtoForLots`, `insertRechargeFundLotsIfNeeded` |\n| `WalletWithdrawLogic` | 提现与 Bonus 桶转换 | `withdraw`, `withdrawFrozen`, `rollbackWithdraw`, `doneWithdraw`, `bonusToDeposit`, `switchBonus` |\n| `WalletTrialLogic` | Trial 体验金生命周期 | `grantTrial`, `expireTrial`, `resolveTrialAmount`, `makeTrialPoolBizId`, `buildTrialPoolRemark`, `deductTrialPoolInRechargeTransaction`(若仍被 Recharge 调用则保留在 Recharge 或抽 `TrialPoolSupport` trait) |\n\n**Trial Pool 边界**:`planTrialPoolDeductionForRecharge` / `finalizeTrialPoolAfterRechargeInc` 与充值事务强耦合,建议 **Phase 3 随 RechargeLogic 一起迁**;Trial 发放/过期独立进 `WalletTrialLogic`。\n\n---\n\n## 门面 WalletLogic(拆分后 ~150 行)\n\n```php\nclass WalletLogic\n{\n public function __construct(\n private WalletRegisterLogic $registerLogic,\n private WalletBetWinLogic $betWinLogic,\n private WalletRechargeLogic $rechargeLogic,\n private WalletWithdrawLogic $withdrawLogic,\n private WalletTrialLogic $trialLogic,\n ) {}\n\n public function run(WalletUpdateRequestDTO $requestDTO) { /* 分发 + deleteWalletCache */ }\n public function runRechargeOrder(WalletRechargeRequestDTO $dto) { /* 委托 rechargeLogic */ }\n\n // 兼容 GrantLogic / 外部 static 调用\n public static function query(...) { return WalletQueryLogic::query(...); }\n public static function queryBizLog(...) { return WalletQueryLogic::queryBizLog(...); }\n}\n```\n\n`ACTION_METHOD_MAP` 改为 **Logic 实例 + 方法名** 或 **callable 数组**,例如:\n\n```php\nWalletLogModel::BIZ_TYPE_BET => [$this->betWinLogic, 'bet'],\n```\n\nWebman DI 需在 [`config/`](slot-wallet/config/) 或现有容器绑定中注册子 Logic(若项目无自动注入,门面构造函数内 `new` 亦可,与当前 `GrantLogic` 风格一致)。\n\n---\n\n## 分阶段实施(你已选 phased + facade)\n\n### Phase 0:准备(低风险)\n\n- 为 `WalletLogic` 现有 public 方法补全/核对中文 PHPDoc(拆分时会移动代码,趁此对齐 [`php-clean-code`](.cursor/rules/php-clean-code.mdc))。\n- 确认 `minus()` 无调用后删除(或加 `@deprecated` 注释待下一版删)。\n- 更新 [`README.md`](slot-wallet/README.md)「实现入口」一节,列出子 Logic 索引。\n\n### Phase 1:Query + Register + 基类\n\n- 新建 `AbstractWalletMutationLogic`、`WalletQueryLogic`、`WalletRegisterLogic`。\n- `WalletLogic::query/queryBizLog` 改为委托;[`GrantLogic`](slot-wallet/app/api/logic/GrantLogic.php) 可继续 `WalletLogic::query()` 不动。\n- `register` 从 WalletLogic 迁出;`run()` 分发到 `WalletRegisterLogic`。\n- **验收**:注册接口、余额查询、Grant 通知链路回归。\n\n### Phase 2:BetWin(最高复杂度)\n\n- 新建 `WalletBetWinLogic`,迁移 `bet`/`win` 及 Redis round 状态方法。\n- `WalletController` 的 `bet`/`win` 专用入口仍走 `walletLogic->run()`,行为不变。\n- **注意**:`win()` 内对 `$requestDTO->fee` 的临时修改必须在 BetWinLogic 内用 `try/finally` 保持,避免泄漏到其他域。\n- **验收**:下注/派奖、Trial 阶段 bet/win、FreeSpin 代付、中间派奖 `is_end=0`、幂等 hit。\n\n### Phase 3:Recharge / Inc\n\n- 新建 `WalletRechargeLogic`,迁移 `inc` 全链路 + `recharge`/`rechargeSign`/`reward`。\n- `runRechargeOrder` 委托给 RechargeLogic(含 `rechargeOrderDto` 生命周期)。\n- **验收**:充值主链路、首充 Trial Pool 扣减、Fund Lot 落库、签到充值 `sign`。\n\n### Phase 4:Withdraw + Trial\n\n- 新建 `WalletWithdrawLogic`、`WalletTrialLogic`。\n- 补齐 `ACTION_METHOD_MAP` 中缺失的 `switchBonus` 映射(若 slot-sdk 仍在调 `type=switchBonus`,当前可能未路由到方法——拆分时可一并修复)。\n- **验收**:提现冻结/成功/失败回滚、Bonus 转 Deposit、Trial 发放/过期。\n\n---\n\n## 不建议的做法\n\n- **不要把子用例再拆成 Logic 型 Service**(如 `WithdrawLogicService`),违反分层红线。\n- **不要一次性改 Controller 注入多个 Logic**,你已选 facade,保持对外稳定。\n- **不要把 BetService/WinService 的业务编排再搬回 Logic 后又复制一份**——子 Logic 只做事务、幂等、流水、MQ,核心算法继续调用现有 Service。\n\n---\n\n## 预期收益\n\n- 单文件从 ~1900 行降至 **门面 ~150 行 + 各子 Logic 200–500 行**。\n- 改动 bet 不再误触 recharge 事务代码,Review 范围清晰。\n- 与 `GrantLogic` 风格统一,新人可按域定位代码。\n- 对外 API、`WalletLogic::run()` 签名、SDK 调用路径 **零破坏**。\n\n---\n\n## 风险与缓解\n\n| 风险 | 缓解 |\n|---|---|\n| 拆分引入行为回归 | 每 Phase 独立 PR;优先复制代码再删旧代码,不做顺手重构 |\n| `$requestDTO` 可变状态 | 基类 `bindRequest()` + 子 Logic 不跨用例复用实例 |\n| Webman 构造注入 | 若无 DI,Phase 1 先在 WalletLogic 内 `new` 子 Logic,后续再接线 |\n| Trial Pool 跨 Recharge/Trial | Phase 3 完成 Recharge 后再动 Trial;必要时抽 `TrialPoolDeductionSupport` trait 供 Recharge 专用 |\n\n---\n\n## 涉及文件\n\n**新建**(均在 `slot-wallet/app/api/logic/`):\n\n- `AbstractWalletMutationLogic.php`\n- `WalletQueryLogic.php`\n- `WalletRegisterLogic.php`\n- `WalletBetWinLogic.php`\n- `WalletRechargeLogic.php`\n- `WalletWithdrawLogic.php`\n- `WalletTrialLogic.php`\n\n**修改**:\n\n- [`WalletLogic.php`](slot-wallet/app/api/logic/WalletLogic.php) — 瘦身为门面\n- [`README.md`](slot-wallet/README.md) — 更新入口索引(可选 `.cursor/rules/slot-wallet-prd-and-money.mdc` 一行指向)\n\n**不改**:\n\n- [`WalletController.php`](slot-wallet/app/api/controller/WalletController.php)\n- 现有 `app/service/wallet/*Service.php`\n","todos":[{"id":"phase0-prep","content":"Phase 0:确认 minus() 死代码、补 PHPDoc、更新 README 入口索引"},{"id":"phase1-base-query-register","content":"Phase 1:新建 AbstractWalletMutationLogic + WalletQueryLogic + WalletRegisterLogic,WalletLogic 委托 register/query"},{"id":"phase2-bet-win","content":"Phase 2:新建 WalletBetWinLogic,迁移 bet/win 及 Redis round 辅助逻辑"},{"id":"phase3-recharge","content":"Phase 3:新建 WalletRechargeLogic,迁移 inc/recharge/reward 与 Trial Pool 充值链"},{"id":"phase4-withdraw-trial","content":"Phase 4:新建 WalletWithdrawLogic + WalletTrialLogic,修复 switchBonus 路由(如需要)"}]}}]}}
{"role":"user","message":{"content":[{"type":"text","text":"\nWalletLogic 分阶段拆分方案\n\nImplement the plan as specified, it is attached for your reference. Do NOT edit the plan file itself.\n\nTo-do's from the plan have already been created. Do not create them again. Mark them as in_progress as you work, starting with the first one. Don't stop until you have completed all the to-dos.\n"}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"按计划分阶段拆分 `WalletLogic`,先从 Phase 0 准备并通读现有实现。\n\n[REDACTED]"},{"type":"tool_use","name":"TodoWrite","input":{"merge":true,"todos":[{"id":"phase0-prep","content":"Phase 0:确认 minus() 死代码、补 PHPDoc、更新 README 入口索引","status":"in_progress"}]}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php"}},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet","pattern":"minus\\("}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"正在创建拆分后的各 Logic 文件,并重构 `WalletLogic` 为门面。\n\n[REDACTED]"},{"type":"tool_use","name":"Read","input":{"limit":60,"offset":45,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/README.md"}},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet","pattern":"switchBonus"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Write","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/AbstractWalletMutationLogic.php","contents":"requestDTO = $requestDTO;\n }\n\n /**\n * 初始化空钱包账户与统计行(无初始余额)。\n *\n * @return WalletAccountModel|null 初始化后的币种钱包行;失败时 null\n */\n protected function initWalletWithoutMoney(): ?WalletAccountModel\n {\n $walletAccountModel = new WalletAccountModel($this->requestDTO->uid);\n $walletAccountModel->startTrans();\n try {\n $initialized = $walletAccountModel->initByDepositAndBonus(0, 0, $this->requestDTO->currency);\n if (!$initialized) {\n LoggerService::error(__METHOD__, '初始化钱包失败');\n throw new WalletException('初始化钱包失败', ErrorCode::SYSTEM_ERROR);\n }\n $walletStatModel = new WalletStatModel($this->requestDTO->uid);\n $statInserted = $walletStatModel->insertData([\n 'uid' => $this->requestDTO->uid,\n 'currency' => $this->requestDTO->currency,\n ]);\n if (!$statInserted) {\n LoggerService::error(__METHOD__, '初始化钱包状态失败');\n throw new WalletException('初始化钱包状态失败', ErrorCode::SYSTEM_ERROR);\n }\n $walletAccountModel->commit();\n\n return $walletAccountModel->wallet($this->requestDTO->currency);\n } catch (Throwable $exception) {\n $walletAccountModel->rollback();\n LoggerService::error(__METHOD__, $exception);\n }\n\n return null;\n }\n\n /**\n * 推送 PWA 用户交易流水(失败仅记日志,不影响主链路)。\n *\n * @param string $subject 交易主题\n * @param int $reward 变动金额\n * @param int $balance 变动后余额\n * @param int $type PWA 交易类型\n * @return void\n */\n protected function addTransactionRecord(string $subject, int $reward, int $balance, int $type): void\n {\n LoggerService::info(__METHOD__, [\n 'uid' => $this->requestDTO->uid,\n 'subject' => $subject,\n 'reward' => $reward,\n 'balance' => $balance,\n ]);\n $transactionLogEntity = new UserTransactionLogEntity([\n 'source' => $this->requestDTO->source,\n 'uid' => $this->requestDTO->uid,\n 'subject' => $subject,\n 'transaction_id' => substr((string)$this->requestDTO->uid, 0, 3) . $this->requestDTO->biz_id,\n 'amount' => $reward,\n 'organization' => $this->requestDTO->organization,\n 'game_id' => 0,\n 'vendor_id' => 0,\n 'currency' => $this->requestDTO->currency,\n 'round_id' => '',\n 'balance_after' => $balance,\n 'transaction_type' => $type,\n 'create_time' => date('Y-m-d H:i:s'),\n ]);\n try {\n PWAService::sendUserTransactionLog($transactionLogEntity);\n } catch (Throwable $exception) {\n LoggerService::error(__METHOD__, $exception);\n }\n }\n\n /**\n * 统一钱包总余额口径:Withdrawable + Locked Deposit + Bonus。\n *\n * @param WalletAccountModel $walletAccountModel 钱包聚合账户\n * @return int 总余额(最小货币单位)\n */\n protected function getBalance(WalletAccountModel $walletAccountModel): int\n {\n return (int)$walletAccountModel->withdraw_balance\n + (int)$walletAccountModel->deposit_balance\n + (int)$walletAccountModel->frozen_bonus;\n }\n\n /**\n * 构造 Trial 阶段流水 remark(标注 trial 阶段与动作类型,便于审计与排查)。\n *\n * @param string $roundId 游戏局号\n * @param int $amount 本次变动的 Trial 金额\n * @param string $kind 动作类型:bet / win / grant / expire\n * @return string JSON remark\n */\n protected function buildTrialRemark(string $roundId, int $amount, string $kind): string\n {\n return json_encode([\n 'round_id' => $roundId,\n 'phase' => 'trial',\n 'kind' => $kind,\n 'bonus' => $amount,\n ], JSON_UNESCAPED_UNICODE);\n }\n\n /**\n * 写入 wallet_log 流水。\n *\n * @param int $amount 变动金额(负数为扣款)\n * @param int $balance 变动后总余额\n * @param string $bizType 业务类型\n * @param int $walletType 钱包流水分类\n * @param string $remark 备注 JSON 或文本\n * @return int 写入结果(1 成功)\n */\n protected function addLog(\n int $amount,\n int $balance,\n string $bizType,\n int $walletType,\n string $remark = ''\n ): int {\n if ($remark === '') {\n $remark = \"bonus:{$this->requestDTO->bonus},amount:{$amount}\";\n }\n $walletLogEntity = new WalletLogEntity([\n 'uid' => $this->requestDTO->uid,\n 'currency' => $this->requestDTO->currency,\n 'biz_type' => $bizType,\n 'biz_id' => $this->requestDTO->biz_id,\n 'amount' => $amount,\n 'wallet_type' => $walletType,\n 'remark' => $remark,\n 'trace_id' => $this->requestDTO->trace_id,\n 'balance' => $balance,\n ]);\n\n return WalletLogService::getInstance()->create($walletLogEntity);\n }\n\n /**\n * 乐观锁冲突时重试执行 callback。\n *\n * @param callable $callback 业务回调,应返回 WalletEntity|bool|null\n * @param int $retryCount 最大重试次数\n * @return WalletEntity|bool|null\n * @throws WalletException 重试耗尽或业务异常时\n */\n protected function maxRetry(callable $callback, int $retryCount = 3): WalletEntity|bool|null\n {\n $errorMessage = '';\n for ($attempt = 0; $attempt < $retryCount; $attempt++) {\n try {\n $callbackResult = $callback();\n if ($callbackResult) {\n return $callbackResult;\n }\n usleep(100);\n } catch (Throwable $exception) {\n LoggerService::error(__METHOD__, $exception);\n if ($exception instanceof WalletException) {\n $errorMessage = $exception->getMessage();\n LoggerService::error(__METHOD__, $exception->getMessage());\n break;\n }\n usleep(100);\n }\n }\n\n throw new WalletException($errorMessage, ErrorCode::SYSTEM_ERROR);\n }\n\n /**\n * 推送 Console 统计总线事件。\n *\n * @param string $type 事件类型\n * @param int|float $amount 金额\n * @return void\n */\n protected function sendConsoleBus(string $type, int|float $amount): void\n {\n RabbitMqService::getInstance(MQKeyManagerService::EXCHANGE_CONSOLE, MQKeyManagerService::QUEUE_CONSOLE_BUS)\n ->sendMessage(json_encode([\n 'uid' => $this->requestDTO->uid,\n 'type' => $type,\n 'data' => [\n 'source' => $this->requestDTO->source,\n 'amount' => $amount,\n ],\n ], JSON_UNESCAPED_UNICODE));\n }\n}\n"}},{"type":"tool_use","name":"Write","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletQueryLogic.php","contents":"walletByCache($currency);\n if ($walletRow === null) {\n return new WalletEntity([]);\n }\n\n $totalDeposit = 0;\n $totalWithdraw = 0;\n if ($needStat === 1) {\n $walletStatModel = new WalletStatModel($uid);\n $statRow = $walletStatModel->findOne(['uid' => $uid]);\n if ($statRow !== null) {\n $totalDeposit = (int)$statRow->total_deposit;\n $totalWithdraw = (int)$statRow->total_withdraw;\n }\n }\n\n $casinoCredits = $walletService->isTrialPhase($walletRow)\n ? (int)$walletRow->frozen_bonus\n : (int)$walletRow->deposit_balance + (int)$walletRow->frozen_bonus;\n\n return new WalletEntity([\n 'r' => $totalDeposit,\n 'tw' => $totalWithdraw,\n 'd' => $walletRow->deposit_balance,\n 'deposit' => $walletRow->deposit_balance,\n 'w' => $walletRow->withdraw_balance,\n 'withdraw' => $walletRow->withdraw_balance,\n 'ub' => $walletRow->available_bonus,\n 'b' => $walletRow->frozen_bonus,\n 'casino_credits' => $casinoCredits,\n 'wallet_created_at' => (string)$walletRow->created_at,\n ]);\n }\n\n /**\n * 按 biz_id + biz_type 查询 wallet 幂等流水(供 gateway 补偿与审计追踪)。\n *\n * @param int $uid 用户 ID\n * @param string $bizId 幂等业务 ID(如 wallet_request_id)\n * @param string $bizType 幂等业务类型(bet/win 等)\n * @return array{found: bool, ledger_id: int, wallet_lot_id: int}\n */\n public static function queryBizLog(int $uid, string $bizId, string $bizType): array\n {\n $walletLogModel = new WalletLogModel($uid);\n $logRow = $walletLogModel->findByBizIdAndType($bizId, $bizType);\n if ($logRow === null) {\n return ['found' => false, 'ledger_id' => 0, 'wallet_lot_id' => 0];\n }\n\n $fundLotRow = (new WalletFundLotModel($uid))->findRowBySource(\n WalletFundLotModel::SOURCE_TYPE_DIRECT_REWARD_GRANT,\n $bizId\n );\n\n return [\n 'found' => true,\n 'ledger_id' => (int)($logRow['id'] ?? 0),\n 'wallet_lot_id' => (int)($fundLotRow['id'] ?? 0),\n ];\n }\n}\n"}},{"type":"tool_use","name":"Write","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletRegisterLogic.php","contents":"bindRequest($requestDTO);\n $walletAccountModel = new WalletAccountModel($this->requestDTO->uid);\n $walletAccountModel->startTrans();\n try {\n $registerService = new RegisterService($this->requestDTO->uid);\n $registerResult = $registerService->execute($this->requestDTO);\n\n if ($registerResult->totalIn > 0) {\n $logWritten = $this->addLog(\n $registerResult->totalIn,\n $registerResult->totalIn,\n $this->requestDTO->type,\n WalletLogModel::WALLET_TYPE_GIFT,\n $registerResult->remark\n );\n if (!$logWritten) {\n LoggerService::error(__METHOD__, '记录钱包日志失败');\n throw new WalletException('记录钱包日志失败', ErrorCode::SYSTEM_ERROR);\n }\n }\n\n $walletAccountModel->commit();\n $this->addTransactionRecord('register', $registerResult->totalIn, $registerResult->totalIn, 11);\n\n return new WalletEntity([\n 'd' => $registerResult->fee,\n 'deposit' => $registerResult->fee,\n 'w' => 0,\n 'b' => $registerResult->bonus,\n ]);\n } catch (Throwable $exception) {\n LoggerService::error(__METHOD__, $exception);\n $walletAccountModel->rollback();\n\n return null;\n }\n }\n}\n"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Write","input":{"contents":"bindRequest($requestDTO);\n\n return $this->maxRetry(function () {\n $walletAccountModel = new WalletAccountModel($this->requestDTO->uid);\n $walletLogModel = new WalletLogModel($this->requestDTO->uid);\n $walletRow = $walletAccountModel->wallet($this->requestDTO->currency);\n if ($walletRow === null) {\n $this->initWalletWithoutMoney();\n }\n $walletAccountModel->startTrans();\n try {\n $walletRow = $walletAccountModel->lockWallet($this->requestDTO->currency);\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_NOT_FOUND);\n }\n\n $duplicatedLog = $walletLogModel->findByBizIdAndType(\n $this->requestDTO->biz_id,\n WalletLogModel::BIZ_TYPE_BET\n );\n if ($duplicatedLog !== null) {\n $walletAccountModel->rollback();\n LoggerService::warning(__METHOD__, [\n 'uid' => $this->requestDTO->uid,\n 'biz_id' => $this->requestDTO->biz_id,\n 'round_id' => $this->requestDTO->round_id,\n 'msg' => 'bet idempotent hit',\n ]);\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance,\n 'deposit' => $walletRow->deposit_balance,\n 'w' => $walletRow->withdraw_balance,\n 'withdraw' => $walletRow->withdraw_balance,\n 'b' => $walletRow->frozen_bonus,\n 'ub' => $walletRow->available_bonus,\n ]);\n }\n\n $walletService = new WalletService($walletAccountModel);\n $requestSource = (string)$this->requestDTO->source;\n $isTrialPhase = $walletService->isTrialPhase($walletRow, $requestSource);\n $isFreeSpinPaidBet = false;\n $freeSpinAccountId = 0;\n if ($isTrialPhase) {\n $betResult = $this->executeTrialBet($walletAccountModel, $walletRow);\n } else {\n $freeSpinConsume = (new TrialFreeSpinBetService())->tryConsumeFreeSpin($this->requestDTO);\n if ($freeSpinConsume['consumed']) {\n $betResult = $this->executeFreeSpinPaidBet($walletRow, $freeSpinConsume);\n $isFreeSpinPaidBet = true;\n $freeSpinAccountId = (int)($freeSpinConsume['fs_account_id'] ?? 0);\n } else {\n $betService = new BetService($this->requestDTO->uid);\n $betResult = $betService->execute($this->requestDTO, $walletRow);\n }\n }\n\n $walletLogAmount = $isFreeSpinPaidBet ? 0 : ($this->requestDTO->fee * -1);\n $logWritten = $this->addLog(\n $walletLogAmount,\n $betResult['balance_after'],\n WalletLogModel::BIZ_TYPE_BET,\n WalletLogModel::WALLET_TYPE_GAME,\n $betResult['remark']\n );\n if ($logWritten !== 1) {\n throw new WalletException('记录下注日志失败', ErrorCode::SYSTEM_ERROR);\n }\n\n $walletAccountModel->commit();\n $this->sendConsoleBus('bet', $this->requestDTO->fee);\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance - $betResult['deposit'],\n 'deposit' => $walletRow->deposit_balance - $betResult['deposit'],\n 'w' => $walletRow->withdraw_balance - $betResult['withdraw'],\n 'withdraw' => $walletRow->withdraw_balance - $betResult['withdraw'],\n 'b' => $walletRow->frozen_bonus - $betResult['bonus'],\n 'ub' => $walletRow->available_bonus,\n 'wallet_created_at' => (string)$walletRow->created_at,\n 'is_fs_paid_bet' => $isFreeSpinPaidBet ? 1 : 0,\n 'fs_account_id' => $freeSpinAccountId,\n ]);\n } catch (Throwable $exception) {\n $walletAccountModel->rollback();\n throw $exception;\n }\n });\n }\n\n /**\n * 执行游戏派奖(含中间派奖累计、Trial 阶段与幂等)。\n *\n * @param WalletUpdateRequestDTO $requestDTO 已校验请求\n * @return WalletEntity 派奖后余额\n * @throws WalletException\n */\n public function win(WalletUpdateRequestDTO $requestDTO): WalletEntity\n {\n $this->bindRequest($requestDTO);\n\n return $this->maxRetry(function () {\n $walletAccountModel = new WalletAccountModel($this->requestDTO->uid);\n $walletLogModel = new WalletLogModel($this->requestDTO->uid);\n $walletRow = $walletAccountModel->wallet($this->requestDTO->currency);\n if ($walletRow === null) {\n $walletRow = $this->initWalletWithoutMoney();\n }\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_NOT_FOUND);\n }\n\n if ((int)$this->requestDTO->is_end === 0) {\n $this->cachePendingWinByRound();\n $pendingRoundAmount = $this->getPendingWinByRound();\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance,\n 'deposit' => $walletRow->deposit_balance,\n 'w' => $walletRow->withdraw_balance + $pendingRoundAmount,\n 'withdraw' => $walletRow->withdraw_balance + $pendingRoundAmount,\n 'b' => $walletRow->frozen_bonus,\n 'ub' => $walletRow->available_bonus,\n ]);\n }\n\n $walletAccountModel->startTrans();\n try {\n $walletRow = $walletAccountModel->lockWallet($this->requestDTO->currency);\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_NOT_FOUND);\n }\n\n $duplicatedLog = $walletLogModel->findByBizIdAndType(\n $this->requestDTO->biz_id,\n WalletLogModel::BIZ_TYPE_WIN\n );\n if ($duplicatedLog !== null) {\n $walletAccountModel->rollback();\n LoggerService::warning(__METHOD__, [\n 'uid' => $this->requestDTO->uid,\n 'biz_id' => $this->requestDTO->biz_id,\n 'round_id' => $this->requestDTO->round_id,\n 'msg' => 'win idempotent hit',\n ]);\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance,\n 'deposit' => $walletRow->deposit_balance,\n 'w' => $walletRow->withdraw_balance,\n 'withdraw' => $walletRow->withdraw_balance,\n 'b' => $walletRow->frozen_bonus,\n 'ub' => $walletRow->available_bonus,\n ]);\n }\n\n $originFee = $this->requestDTO->fee;\n $pendingWin = $this->getPendingWinByRound();\n $this->requestDTO->fee = $originFee + $pendingWin;\n\n $walletService = new WalletService($walletAccountModel);\n $requestSource = (string)$this->requestDTO->source;\n $isTrialPhase = $walletService->isTrialPhase($walletRow, $requestSource);\n if ($isTrialPhase) {\n $winResult = $this->executeTrialWin($walletAccountModel, $walletRow);\n } else {\n $winService = new WinService($this->requestDTO->uid);\n $winResult = $winService->execute($this->requestDTO, $walletRow);\n }\n\n $logWritten = $this->addLog(\n $this->requestDTO->fee,\n $winResult['balance_after'],\n WalletLogModel::BIZ_TYPE_WIN,\n WalletLogModel::WALLET_TYPE_GAME,\n $winResult['remark']\n );\n if ($logWritten !== 1) {\n throw new WalletException('记录派奖日志失败', ErrorCode::SYSTEM_ERROR);\n }\n\n $walletAccountModel->commit();\n $this->clearPendingWinByRound();\n $this->sendConsoleBus('win', $this->requestDTO->fee);\n /** @var WalletAccountModel $finalWalletRow */\n $finalWalletRow = $winResult['wallet'];\n\n return new WalletEntity([\n 'd' => $finalWalletRow->deposit_balance,\n 'deposit' => $finalWalletRow->deposit_balance,\n 'w' => $finalWalletRow->withdraw_balance,\n 'withdraw' => $finalWalletRow->withdraw_balance,\n 'b' => $finalWalletRow->frozen_bonus,\n 'ub' => $finalWalletRow->available_bonus,\n 'wallet_created_at' => (string)$finalWalletRow->created_at,\n ]);\n } catch (Throwable $exception) {\n $walletAccountModel->rollback();\n throw $exception;\n } finally {\n $this->requestDTO->fee = $originFee ?? $this->requestDTO->fee;\n }\n });\n }\n\n /**\n * FS 代付下注:不扣用户余额,由平台承担下注额;派奖仍走正常 win 流程。\n *\n * @param WalletAccountModel $walletRow 已锁定钱包行\n * @param array{consumed: bool, paid_amount: int, remaining_count: int, fs_account_id: int} $freeSpinConsume FS 消耗结果\n * @return array{bonus: int, deposit: int, withdraw: int, balance_after: int, remark: string}\n */\n private function executeFreeSpinPaidBet(WalletAccountModel $walletRow, array $freeSpinConsume): array\n {\n $paidAmount = (int)($freeSpinConsume['paid_amount'] ?? 0);\n $balanceAfter = (int)$walletRow->withdraw_balance\n + (int)$walletRow->deposit_balance\n + (int)$walletRow->frozen_bonus;\n\n return [\n 'bonus' => 0,\n 'deposit' => 0,\n 'withdraw' => 0,\n 'balance_after' => $balanceAfter,\n 'remark' => json_encode([\n 'round_id' => $this->requestDTO->round_id,\n 'phase' => 'free_spin',\n 'kind' => 'bet',\n 'fs_account_id' => (int)($freeSpinConsume['fs_account_id'] ?? 0),\n 'paid_amount' => $paidAmount,\n 'remaining_count' => (int)($freeSpinConsume['remaining_count'] ?? 0),\n ], JSON_UNESCAPED_UNICODE),\n ];\n }\n\n /**\n * 试玩阶段下注:仅扣 frozen_bonus(Trial 体验金),不建 Lot、不挂打码组、不可提现。\n *\n * @param WalletAccountModel $walletAccountModel 钱包聚合账户(事务内)\n * @param WalletAccountModel $walletRow 已锁定钱包行\n * @return array{bonus: int, deposit: int, withdraw: int, balance_after: int, remark: string}\n * @throws WalletException 下注金额非法、余额不足或并发冲突时\n */\n private function executeTrialBet(WalletAccountModel $walletAccountModel, WalletAccountModel $walletRow): array\n {\n $betFee = (int)$this->requestDTO->fee;\n if ($betFee <= 0) {\n throw new WalletException('下注金额必须大于0', ErrorCode::PARAMS_ERROR);\n }\n if ((int)$walletRow->frozen_bonus < $betFee) {\n throw new WalletException('有效余额不足', ErrorCode::WALLET_BALANCE_NOT_ENOUGH);\n }\n\n $updated = $walletAccountModel->decBalance(\n 0,\n 0,\n $betFee,\n 0,\n (int)$walletRow->version,\n $this->requestDTO->currency\n );\n if ($updated !== 1) {\n throw new WalletException('钱包并发冲突', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n\n $balanceAfter = (int)$walletRow->withdraw_balance\n + (int)$walletRow->deposit_balance\n + (int)$walletRow->frozen_bonus\n - $betFee;\n\n return [\n 'bonus' => $betFee,\n 'deposit' => 0,\n 'withdraw' => 0,\n 'balance_after' => $balanceAfter,\n 'remark' => $this->buildTrialRemark($this->requestDTO->round_id, $betFee, 'bet'),\n ];\n }\n\n /**\n * 试玩阶段派奖:整笔回到 frozen_bonus(Trial 体验金),不路由可提现/锁定充值、不跑解锁与 PlayedOut。\n *\n * @param WalletAccountModel $walletAccountModel 钱包聚合账户(事务内)\n * @param WalletAccountModel $walletRow 已锁定钱包行\n * @return array{bucket: string, amount: int, balance_after: int, remark: string, wallet: WalletAccountModel}\n * @throws WalletException 并发冲突或钱包丢失时\n */\n private function executeTrialWin(WalletAccountModel $walletAccountModel, WalletAccountModel $walletRow): array\n {\n $payoutAmount = (int)$this->requestDTO->fee;\n if ($payoutAmount > 0) {\n $updated = $walletAccountModel->incWinBalance(\n 0,\n 0,\n $payoutAmount,\n (int)$walletRow->version,\n $this->requestDTO->currency\n );\n if ($updated !== 1) {\n throw new WalletException('钱包派奖并发冲突', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n $walletRow = $walletAccountModel->lockWallet($this->requestDTO->currency);\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_NOT_FOUND);\n }\n }\n\n $balanceAfter = (int)$walletRow->withdraw_balance\n + (int)$walletRow->deposit_balance\n + (int)$walletRow->frozen_bonus;\n\n return [\n 'bucket' => 'bonus',\n 'amount' => $payoutAmount,\n 'balance_after' => $balanceAfter,\n 'remark' => $this->buildTrialRemark($this->requestDTO->round_id, $payoutAmount, 'win'),\n 'wallet' => $walletRow,\n ];\n }\n\n /**\n * 记录中间派奖(is_end=0)累计值,按 biz_id 做严格幂等。\n *\n * @return void\n */\n private function cachePendingWinByRound(): void\n {\n $dedupeKey = RedisKeyManagerService::getWinDedupeKey(\n $this->requestDTO->uid,\n $this->requestDTO->currency,\n $this->requestDTO->round_id,\n $this->requestDTO->biz_id\n );\n\n $setOk = Redis::setNx($dedupeKey, '1');\n if (!$setOk) {\n LoggerService::warning(__METHOD__, [\n 'uid' => $this->requestDTO->uid,\n 'biz_id' => $this->requestDTO->biz_id,\n 'round_id' => $this->requestDTO->round_id,\n 'msg' => 'win pending idempotent hit',\n ]);\n\n return;\n }\n Redis::expire($dedupeKey, 259200);\n\n if ($this->requestDTO->fee <= 0) {\n return;\n }\n\n $pendingKey = RedisKeyManagerService::getWinPendingRoundKey(\n $this->requestDTO->uid,\n $this->requestDTO->currency,\n $this->requestDTO->round_id\n );\n Redis::incrBy($pendingKey, $this->requestDTO->fee);\n Redis::expire($pendingKey, 172800);\n }\n\n /**\n * 读取当前局待结算中间派奖累计。\n *\n * @return int 待结算派奖金额\n */\n private function getPendingWinByRound(): int\n {\n $pendingKey = RedisKeyManagerService::getWinPendingRoundKey(\n $this->requestDTO->uid,\n $this->requestDTO->currency,\n $this->requestDTO->round_id\n );\n\n return (int)Redis::get($pendingKey);\n }\n\n /**\n * 最终结算成功后清理本局待结算累计。\n *\n * @return void\n */\n private function clearPendingWinByRound(): void\n {\n $pendingKey = RedisKeyManagerService::getWinPendingRoundKey(\n $this->requestDTO->uid,\n $this->requestDTO->currency,\n $this->requestDTO->round_id\n );\n Redis::del($pendingKey);\n }\n}\n","path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletBetWinLogic.php"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Write","input":{"contents":"bindRequest($requestDTO);\n\n return $this->maxRetry(function () {\n $walletAccountModel = new WalletAccountModel($this->requestDTO->uid);\n $walletService = new WalletService($walletAccountModel);\n $walletRow = $walletService->wallet($this->requestDTO->currency);\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_ERROR);\n }\n if ($this->requestDTO->fee <= 0) {\n return false;\n }\n $balanceAfter = $this->getBalance($walletRow);\n $walletAccountModel->startTrans();\n try {\n $updated = $walletService->rollbackWithdraw(\n $this->requestDTO->fee,\n (int)$walletRow->version,\n $this->requestDTO->currency\n );\n if ($updated !== 1) {\n throw new Exception('retry', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n $withdrawTaskService = new WithdrawTaskService($this->requestDTO->uid, $this->requestDTO->biz_id);\n $withdrawTaskService->rollbackWithdrawTasks();\n $balanceAfter += $this->requestDTO->fee;\n $logWritten = $this->addLog($this->requestDTO->fee, $balanceAfter, $this->requestDTO->type, 0);\n if ($logWritten !== 1) {\n throw new Exception('retry', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n\n $this->addTransactionRecord('back to withdraw', $this->requestDTO->fee, $balanceAfter, 8);\n\n $walletAccountModel->commit();\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance,\n 'deposit' => $walletRow->deposit_balance,\n 'w' => $walletRow->withdraw_balance + $this->requestDTO->fee,\n 'withdraw' => $walletRow->withdraw_balance + $this->requestDTO->fee,\n 'ub' => $walletRow->available_bonus,\n 'b' => $walletRow->frozen_bonus,\n ]);\n } catch (Throwable $exception) {\n LoggerService::error(__METHOD__, $exception);\n $walletAccountModel->rollback();\n throw $exception;\n }\n });\n }\n\n /**\n * 冻结 Bonus 转入锁定充值余额。\n *\n * @param WalletUpdateRequestDTO $requestDTO 已校验请求\n * @return WalletEntity|bool 转换后余额;bonus 无效时 false\n * @throws WalletException\n */\n public function bonusToDeposit(WalletUpdateRequestDTO $requestDTO): WalletEntity|bool\n {\n $this->bindRequest($requestDTO);\n\n return $this->maxRetry(function () {\n $walletAccountModel = new WalletAccountModel($this->requestDTO->uid);\n $walletService = new WalletService($walletAccountModel);\n $walletRow = $walletService->wallet($this->requestDTO->currency);\n if ($walletRow === null) {\n $walletRow = $this->initWalletWithoutMoney();\n }\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_ERROR);\n }\n if ($this->requestDTO->bonus <= 0) {\n return false;\n }\n $balanceAfter = $this->getBalance($walletRow);\n $walletAccountModel->startTrans();\n try {\n $balanceAfter += $this->requestDTO->bonus;\n $updated = $walletService->bonusToDeposit(\n $this->requestDTO->bonus,\n (int)$walletRow->version,\n $this->requestDTO->currency\n );\n if ($updated !== 1) {\n throw new Exception('retry', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n $logWritten = $this->addLog($this->requestDTO->bonus, $balanceAfter, $this->requestDTO->type, 0);\n if ($logWritten !== 1) {\n throw new Exception('retry', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n\n $walletAccountModel->commit();\n $this->addTransactionRecord('bonus to deposit', $this->requestDTO->bonus, $balanceAfter, 5);\n $this->sendConsoleBus('bonusToDeposit', $this->requestDTO->bonus);\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance + $this->requestDTO->bonus,\n 'deposit' => $walletRow->deposit_balance + $this->requestDTO->bonus,\n 'w' => $walletRow->withdraw_balance,\n 'withdraw' => $walletRow->withdraw_balance,\n 'ub' => $walletRow->available_bonus - $this->requestDTO->bonus,\n 'b' => $walletRow->frozen_bonus,\n ]);\n } catch (Throwable $exception) {\n $walletAccountModel->rollback();\n throw $exception;\n }\n });\n }\n\n /**\n * 提现冻结:扣减可提现余额并写入打码任务明细。\n *\n * @param WalletUpdateRequestDTO $requestDTO 已校验请求\n * @return WalletEntity 冻结后余额\n * @throws WalletException\n */\n public function withdrawFrozen(WalletUpdateRequestDTO $requestDTO): WalletEntity\n {\n $this->bindRequest($requestDTO);\n\n return $this->maxRetry(function () {\n $walletAccountModel = new WalletAccountModel($this->requestDTO->uid);\n $walletService = new WalletService($walletAccountModel);\n $walletRow = $walletService->wallet($this->requestDTO->currency);\n if ($walletRow === null) {\n $walletRow = $this->initWalletWithoutMoney();\n }\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_ERROR);\n }\n if ($this->requestDTO->fee <= 0 || !$walletService->checkWithdraw($this->requestDTO->fee, $walletRow)) {\n throw new WalletException('余额不足', ErrorCode::PARAMS_ERROR);\n }\n $balanceAfter = $this->getBalance($walletRow);\n $walletAccountModel->startTrans();\n try {\n $updated = $walletService->withdrawFrozen(\n $this->requestDTO->fee,\n (int)$walletRow->version,\n $this->requestDTO->currency\n );\n if ($updated !== 1) {\n throw new Exception('retry', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n\n $withdrawTaskService = new WithdrawTaskService($this->requestDTO->uid, $this->requestDTO->biz_id);\n $withdrawTaskService->writeWithdrawTaskDetail($this->requestDTO->fee);\n\n $balanceAfter -= $this->requestDTO->fee;\n $logWritten = $this->addLog($this->requestDTO->fee, $balanceAfter, $this->requestDTO->type, 0);\n if ($logWritten !== 1) {\n throw new Exception('retry', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n\n $this->addTransactionRecord('withdraw frozen', $this->requestDTO->fee * -1, $balanceAfter, 7);\n\n $walletAccountModel->commit();\n $this->sendConsoleBus('withdrawFrozen', $this->requestDTO->fee);\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance,\n 'deposit' => $walletRow->deposit_balance,\n 'w' => $walletRow->withdraw_balance - $this->requestDTO->fee,\n 'withdraw' => $walletRow->withdraw_balance - $this->requestDTO->fee,\n 'b' => $walletRow->frozen_bonus + $this->requestDTO->bonus,\n 'ub' => $walletRow->available_bonus,\n ]);\n } catch (Throwable $exception) {\n $walletAccountModel->rollback();\n throw $exception;\n }\n });\n }\n\n /**\n * 提现完成:确认出款并更新累计提现统计。\n *\n * @param WalletUpdateRequestDTO $requestDTO 已校验请求\n * @return WalletEntity 完成后余额\n * @throws WalletException\n */\n public function withdraw(WalletUpdateRequestDTO $requestDTO): WalletEntity\n {\n $this->bindRequest($requestDTO);\n\n return $this->maxRetry(function () {\n $walletAccountModel = new WalletAccountModel($this->requestDTO->uid);\n $walletService = new WalletService($walletAccountModel);\n $walletRow = $walletService->wallet($this->requestDTO->currency);\n if ($walletRow === null) {\n $walletRow = $this->initWalletWithoutMoney();\n }\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_ERROR);\n }\n $balanceAfter = $this->getBalance($walletRow);\n $walletAccountModel->startTrans();\n try {\n $withdrawTaskService = new WithdrawTaskService($this->requestDTO->uid, $this->requestDTO->biz_id);\n if ($this->requestDTO->type === WalletLogModel::BIZ_TYPE_WITHDRAW) {\n $updated = $walletService->finishWithdraw(\n $this->requestDTO->fee,\n (int)$walletRow->version,\n $this->requestDTO->currency\n );\n $walletStatModel = new WalletStatModel($this->requestDTO->uid);\n $walletStatModel->inc($this->requestDTO->currency, 'total_withdraw', $this->requestDTO->fee);\n $withdrawTaskService->doneWithdrawTasks();\n } else {\n $withdrawTaskService->doneWithdrawTasks();\n $updated = $walletService->doneWithdraw(\n $this->requestDTO->fee,\n (int)$walletRow->version,\n $this->requestDTO->currency\n );\n }\n if ($updated !== 1) {\n throw new Exception('retry', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n $logWritten = $this->addLog($this->requestDTO->fee, $balanceAfter, $this->requestDTO->type, 0);\n if ($logWritten !== 1) {\n throw new Exception('retry', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n\n $walletAccountModel->commit();\n if ($this->requestDTO->type === WalletLogModel::BIZ_TYPE_WITHDRAW) {\n $this->addTransactionRecord('finish withdraw', $this->requestDTO->fee, $balanceAfter, 2);\n $this->sendConsoleBus('Withdrawal', $this->requestDTO->fee);\n }\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance,\n 'deposit' => $walletRow->deposit_balance,\n 'w' => $walletRow->withdraw_balance,\n 'withdraw' => $walletRow->withdraw_balance,\n 'b' => $walletRow->frozen_bonus + $this->requestDTO->bonus,\n 'ub' => $walletRow->available_bonus,\n ]);\n } catch (Throwable $exception) {\n $walletAccountModel->rollback();\n throw $exception;\n }\n });\n }\n\n /**\n * 提现完成(不退款变体,复用 withdraw 流程)。\n *\n * @param WalletUpdateRequestDTO $requestDTO 已校验请求\n * @return WalletEntity 完成后余额\n * @throws WalletException\n */\n public function doneWithdraw(WalletUpdateRequestDTO $requestDTO): WalletEntity\n {\n return $this->withdraw($requestDTO);\n }\n\n /**\n * 冻结 Bonus 转为可领取 Bonus。\n *\n * @param WalletUpdateRequestDTO $requestDTO 已校验请求\n * @return WalletEntity|bool 转换后余额;bonus 无效时 false\n * @throws WalletException\n */\n public function switchBonus(WalletUpdateRequestDTO $requestDTO): WalletEntity|bool\n {\n $this->bindRequest($requestDTO);\n\n return $this->maxRetry(function () {\n $walletAccountModel = new WalletAccountModel($this->requestDTO->uid);\n $walletService = new WalletService($walletAccountModel);\n $walletRow = $walletService->wallet($this->requestDTO->currency);\n if ($walletRow === null) {\n $walletRow = $this->initWalletWithoutMoney();\n }\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_ERROR);\n }\n if ($this->requestDTO->bonus <= 0) {\n return false;\n }\n $balanceAfter = $this->getBalance($walletRow);\n $walletAccountModel->startTrans();\n try {\n $updated = $walletService->switchBonus(\n $this->requestDTO->bonus,\n (int)$walletRow->version,\n $this->requestDTO->currency\n );\n if ($updated !== 1) {\n throw new Exception('retry', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n $logWritten = $this->addLog($this->requestDTO->fee, $balanceAfter, $this->requestDTO->type, 0);\n if ($logWritten !== 1) {\n throw new Exception('retry', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n $walletAccountModel->commit();\n $this->sendConsoleBus('switchBonus', $this->requestDTO->bonus);\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance,\n 'deposit' => $walletRow->deposit_balance,\n 'w' => $walletRow->withdraw_balance,\n 'withdraw' => $walletRow->withdraw_balance,\n 'ub' => $walletRow->available_bonus + $this->requestDTO->bonus,\n 'b' => $walletRow->frozen_bonus - $this->requestDTO->bonus,\n ]);\n } catch (Throwable $exception) {\n $walletAccountModel->rollback();\n throw $exception;\n }\n });\n }\n}\n","path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletWithdrawLogic.php"}},{"type":"tool_use","name":"Write","input":{"contents":"bindRequest($requestDTO);\n\n return $this->maxRetry(function () {\n $walletAccountModel = new WalletAccountModel($this->requestDTO->uid);\n $walletLogModel = new WalletLogModel($this->requestDTO->uid);\n\n if ($walletAccountModel->wallet($this->requestDTO->currency) === null) {\n $this->initWalletWithoutMoney();\n }\n\n $trialAmount = $this->resolveTrialAmount();\n if ($trialAmount <= 0) {\n throw new WalletException('Trial 体验金金额必须大于0', ErrorCode::PARAMS_ERROR);\n }\n\n $walletAccountModel->startTrans();\n try {\n $walletRow = $walletAccountModel->lockWallet($this->requestDTO->currency);\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_NOT_FOUND);\n }\n\n $duplicatedLog = $walletLogModel->findByBizIdAndType(\n $this->requestDTO->biz_id,\n WalletLogModel::BIZ_TYPE_TRIAL_GRANT\n );\n if ($duplicatedLog !== null) {\n $walletAccountModel->rollback();\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance,\n 'deposit' => $walletRow->deposit_balance,\n 'w' => $walletRow->withdraw_balance,\n 'withdraw' => $walletRow->withdraw_balance,\n 'b' => $walletRow->frozen_bonus,\n 'ub' => $walletRow->available_bonus,\n ]);\n }\n\n $updated = $walletAccountModel->incBalance(\n 0,\n 0,\n $trialAmount,\n (int)$walletRow->version,\n $this->requestDTO->currency\n );\n if ($updated !== 1) {\n throw new WalletException('数据冲突retry', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n\n $balanceAfter = (int)$walletRow->withdraw_balance\n + (int)$walletRow->deposit_balance\n + (int)$walletRow->frozen_bonus\n + $trialAmount;\n $logWritten = $this->addLog(\n $trialAmount,\n $balanceAfter,\n WalletLogModel::BIZ_TYPE_TRIAL_GRANT,\n WalletLogModel::WALLET_TYPE_GIFT,\n $this->buildTrialRemark('', $trialAmount, 'grant')\n );\n if ($logWritten !== 1) {\n throw new WalletException('记录 Trial 发放日志失败', ErrorCode::SYSTEM_ERROR);\n }\n\n $walletAccountModel->commit();\n $this->sendConsoleBus('trialGrant', $trialAmount);\n (new TrialTaskCompletedMqService())->notifyIfBalanceReached(\n $this->requestDTO,\n $balanceAfter\n );\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance,\n 'deposit' => $walletRow->deposit_balance,\n 'w' => $walletRow->withdraw_balance,\n 'withdraw' => $walletRow->withdraw_balance,\n 'b' => $walletRow->frozen_bonus + $trialAmount,\n 'ub' => $walletRow->available_bonus,\n ]);\n } catch (Throwable $exception) {\n $walletAccountModel->rollback();\n throw $exception;\n }\n });\n }\n\n /**\n * 体验金有效期届满清零:扣减全部 frozen_bonus,不建 Lot;按 biz_id 幂等。\n *\n * @param WalletUpdateRequestDTO $requestDTO 已校验请求\n * @return WalletEntity 清零后余额\n * @throws WalletException\n */\n public function expireTrial(WalletUpdateRequestDTO $requestDTO): WalletEntity\n {\n $this->bindRequest($requestDTO);\n\n return $this->maxRetry(function () {\n if (RechargeExchangeService::isRecharge($this->requestDTO->uid)) {\n $walletAccountModel = new WalletAccountModel($this->requestDTO->uid);\n $walletRow = $walletAccountModel->wallet($this->requestDTO->currency);\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_NOT_FOUND);\n }\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance,\n 'deposit' => $walletRow->deposit_balance,\n 'w' => $walletRow->withdraw_balance,\n 'withdraw' => $walletRow->withdraw_balance,\n 'b' => $walletRow->frozen_bonus,\n 'ub' => $walletRow->available_bonus,\n ]);\n }\n\n $walletAccountModel = new WalletAccountModel($this->requestDTO->uid);\n $walletLogModel = new WalletLogModel($this->requestDTO->uid);\n $bizId = trim($this->requestDTO->biz_id) !== ''\n ? trim($this->requestDTO->biz_id)\n : 'trial:expire:' . $this->requestDTO->uid;\n\n $walletAccountModel->startTrans();\n try {\n $walletRow = $walletAccountModel->lockWallet($this->requestDTO->currency);\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_NOT_FOUND);\n }\n\n $duplicatedLog = $walletLogModel->findByBizIdAndType($bizId, WalletLogModel::BIZ_TYPE_TRIAL_BONUS_EXPIRE);\n if ($duplicatedLog !== null) {\n $walletAccountModel->rollback();\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance,\n 'deposit' => $walletRow->deposit_balance,\n 'w' => $walletRow->withdraw_balance,\n 'withdraw' => $walletRow->withdraw_balance,\n 'b' => $walletRow->frozen_bonus,\n 'ub' => $walletRow->available_bonus,\n ]);\n }\n\n $currentBonus = max(0, (int)$walletRow->frozen_bonus);\n if ($currentBonus <= 0) {\n $walletAccountModel->rollback();\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance,\n 'deposit' => $walletRow->deposit_balance,\n 'w' => $walletRow->withdraw_balance,\n 'withdraw' => $walletRow->withdraw_balance,\n 'b' => 0,\n 'ub' => $walletRow->available_bonus,\n ]);\n }\n\n $updated = $walletAccountModel->decBalance(\n 0,\n 0,\n $currentBonus,\n 0,\n (int)$walletRow->version,\n $this->requestDTO->currency\n );\n if ($updated !== 1) {\n throw new WalletException('数据冲突retry', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n\n $balanceAfter = (int)$walletRow->withdraw_balance + (int)$walletRow->deposit_balance;\n $this->requestDTO->biz_id = $bizId;\n $logWritten = $this->addLog(\n $currentBonus,\n $balanceAfter,\n WalletLogModel::BIZ_TYPE_TRIAL_BONUS_EXPIRE,\n WalletLogModel::WALLET_TYPE_GIFT,\n $this->buildTrialRemark('', $currentBonus, 'expire')\n );\n if ($logWritten !== 1) {\n throw new WalletException('记录 Trial 过期清零日志失败', ErrorCode::SYSTEM_ERROR);\n }\n\n $walletAccountModel->commit();\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance,\n 'deposit' => $walletRow->deposit_balance,\n 'w' => $walletRow->withdraw_balance,\n 'withdraw' => $walletRow->withdraw_balance,\n 'b' => 0,\n 'ub' => $walletRow->available_bonus,\n ]);\n } catch (Throwable $exception) {\n $walletAccountModel->rollback();\n throw $exception;\n }\n });\n }\n\n /**\n * 解析 Trial 体验金金额:优先请求传入的 bonus,缺省回落共享配置 trialBalanceAmount。\n *\n * @return int 最小货币单位金额\n */\n private function resolveTrialAmount(): int\n {\n $requestAmount = max(0, (int)$this->requestDTO->bonus);\n if ($requestAmount > 0) {\n return $requestAmount;\n }\n\n return max(0, (int)ShareConfigService::get('trialBalanceAmount', 0));\n }\n}\n","path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletTrialLogic.php"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Write","input":{"contents":"rechargeOrderDto = $rechargeOrderDto;\n try {\n $updateRequest = new WalletUpdateRequestDTO([\n 'uid' => $rechargeOrderDto->uid,\n 'currency' => $rechargeOrderDto->currency,\n 'biz_id' => $rechargeOrderDto->order_no,\n 'source' => $rechargeOrderDto->source,\n 'organization' => $rechargeOrderDto->organization,\n 'trace_id' => $rechargeOrderDto->trace_id,\n 'fee' => max(0, (int)$rechargeOrderDto->deposit_amount),\n 'bonus' => max(0, (int)$rechargeOrderDto->bonus_amount),\n 'recharge' => max(0, (int)$rechargeOrderDto->deposit_amount),\n 'is_masked' => 1,\n 'type' => WalletLogModel::BIZ_TYPE_RECHARGE,\n ]);\n\n $rechargeResult = $this->recharge($updateRequest);\n if (!$rechargeResult instanceof WalletEntity) {\n throw new WalletException('wallet recharge failed', ErrorCode::SYSTEM_ERROR);\n }\n\n return $rechargeResult;\n } finally {\n $this->rechargeOrderDto = null;\n }\n }\n\n /**\n * 充值入账:加钱后更新统计、兑换记录与 Console 事件。\n *\n * @param WalletUpdateRequestDTO $requestDTO 已校验请求\n * @return WalletEntity 入账后余额\n * @throws WalletException\n */\n public function recharge(WalletUpdateRequestDTO $requestDTO): WalletEntity\n {\n $this->bindRequest($requestDTO);\n $incResult = $this->inc();\n $totalBalance = $incResult->w + $incResult->d + $incResult->b;\n try {\n $rewardAmount = max(\n (int)$this->requestDTO->bonus,\n max($this->requestDTO->fee - $this->requestDTO->recharge, 0)\n );\n if ($this->requestDTO->recharge > 0) {\n $this->addTransactionRecord('recharge', $this->requestDTO->recharge, $totalBalance - $rewardAmount, 1);\n }\n if ($rewardAmount > 0) {\n $this->addTransactionRecord('recharge gift', $rewardAmount, $totalBalance, 12);\n }\n\n $this->sendConsoleBus('Recharge', $this->requestDTO->recharge);\n $walletStatModel = new WalletStatModel($this->requestDTO->uid);\n $walletStatModel->inc($this->requestDTO->currency, 'total_deposit', $this->requestDTO->recharge);\n $rechargeExchangeService = new RechargeExchangeService($this->requestDTO->uid);\n $rechargeExchangeService->recharge($this->requestDTO->recharge);\n\n if ($rewardAmount > 0) {\n $this->sendConsoleBus('reward', $rewardAmount);\n }\n } catch (Throwable $exception) {\n LoggerService::error(__METHOD__, $exception);\n }\n\n return $incResult;\n }\n\n /**\n * 签到充值:仅更新统计与 PWA 流水,不走 inc 加钱链路。\n *\n * @param WalletUpdateRequestDTO $requestDTO 已校验请求\n * @return bool 始终 true(历史兼容)\n */\n public function rechargeSign(WalletUpdateRequestDTO $requestDTO): bool\n {\n $this->bindRequest($requestDTO);\n try {\n $walletAccountModel = new WalletAccountModel($this->requestDTO->uid);\n $walletService = new WalletService($walletAccountModel);\n $walletRow = $walletService->wallet($this->requestDTO->currency);\n if ($walletRow === null) {\n $walletRow = $this->initWalletWithoutMoney();\n }\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_ERROR);\n }\n $balanceAfter = $this->getBalance($walletRow);\n $walletStatModel = new WalletStatModel($this->requestDTO->uid);\n $walletStatModel->inc($this->requestDTO->currency, 'total_deposit', $this->requestDTO->recharge);\n $rechargeExchangeService = new RechargeExchangeService($this->requestDTO->uid);\n $rechargeExchangeService->recharge($this->requestDTO->recharge);\n $this->addTransactionRecord('recharge', $this->requestDTO->fee, $balanceAfter, 1);\n $this->sendConsoleBus('Recharge', $this->requestDTO->recharge);\n } catch (Throwable $exception) {\n LoggerService::error(__METHOD__, $exception);\n }\n\n return true;\n }\n\n /**\n * 奖励入账:加钱后推送 PWA 流水与 Console 事件。\n *\n * @param WalletUpdateRequestDTO $requestDTO 已校验请求\n * @return WalletEntity 入账后余额\n * @throws WalletException\n */\n public function reward(WalletUpdateRequestDTO $requestDTO): WalletEntity\n {\n $this->bindRequest($requestDTO);\n $incResult = $this->inc();\n\n try {\n if ($this->requestDTO->fee > 0) {\n $balanceAfter = $incResult->d + $incResult->w;\n $transactionType = 6;\n if ($this->requestDTO->reward_type > 0) {\n $transactionType = $this->requestDTO->reward_type;\n }\n $this->addTransactionRecord('reward', $this->requestDTO->fee, $balanceAfter, $transactionType);\n $this->sendConsoleBus('reward', $this->requestDTO->fee);\n }\n } catch (Throwable $exception) {\n LoggerService::error(__METHOD__, $exception);\n }\n\n return $incResult;\n }\n\n /**\n * 加钱(充值 / 奖励等);首充试玩阶段在充值入账同事务扣体验金转 Trial Pool。\n *\n * @return WalletEntity 入账后余额\n * @throws WalletException\n */\n public function inc(): WalletEntity\n {\n return $this->maxRetry(function () {\n return $this->executeIncWithTransaction();\n });\n }\n\n /**\n * 执行一次加钱事务(乐观锁冲突时由 maxRetry 重试)。\n *\n * @return WalletEntity 入账后余额\n * @throws Throwable\n */\n private function executeIncWithTransaction(): WalletEntity\n {\n $walletAccountModel = new WalletAccountModel($this->requestDTO->uid);\n $walletService = new WalletService($walletAccountModel);\n $walletRow = $this->ensureWalletRowExists($walletService);\n $balanceBeforeInc = $this->getBalance($walletRow);\n $trialPoolPlan = $this->planTrialPoolDeductionForRecharge($walletService, $walletRow);\n\n $walletAccountModel->startTrans();\n try {\n $incAllocation = $this->applyIncBalanceChange($walletService, $walletRow);\n $this->assertWalletMutationSucceeded($incAllocation['ok'], 'retry inc');\n\n $trialPoolAmount = $this->finalizeTrialPoolAfterRechargeInc($walletAccountModel, $trialPoolPlan);\n\n $this->publishBonusConsoleEventIfNeeded();\n $this->insertRechargeFundLotsIfNeeded();\n\n $balanceAfterInc = $balanceBeforeInc + $this->requestDTO->fee;\n $this->assertWalletMutationSucceeded(\n $this->addLog($this->requestDTO->fee, $balanceAfterInc, $this->requestDTO->type, 0),\n 'retry addLog'\n );\n\n $walletAccountModel->commit();\n\n return $this->buildWalletEntityAfterInc(\n $walletRow,\n $incAllocation,\n $trialPoolAmount,\n $trialPoolPlan->isTrialPoolProcessed()\n );\n } catch (Throwable $exception) {\n $walletAccountModel->rollback();\n LoggerService::error(__METHOD__, $exception);\n throw $exception;\n }\n }\n\n /**\n * 加载币种钱包行,不存在则初始化空钱包。\n *\n * @param WalletService $walletService 钱包服务\n * @return WalletAccountModel 币种钱包行\n * @throws WalletException 初始化失败时\n */\n private function ensureWalletRowExists(WalletService $walletService): WalletAccountModel\n {\n $walletRow = $walletService->wallet($this->requestDTO->currency);\n if ($walletRow === null) {\n $walletRow = $this->initWalletWithoutMoney();\n }\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_ERROR);\n }\n\n return $walletRow;\n }\n\n /**\n * 首充充值入账前规划 Trial Pool 扣款:已落幂等流水则复用金额,否则标记待同事务扣减。\n *\n * @param WalletService $walletService 钱包服务\n * @param WalletAccountModel $walletRow 入账前钱包行\n * @return TrialPoolDeductionPlanEntity 扣款计划\n */\n private function planTrialPoolDeductionForRecharge(\n WalletService $walletService,\n WalletAccountModel $walletRow\n ): TrialPoolDeductionPlanEntity {\n if ($this->requestDTO->type !== WalletLogModel::BIZ_TYPE_RECHARGE) {\n return TrialPoolDeductionPlanEntity::none();\n }\n if (!$walletService->isTrialPhase($walletRow, (string)$this->requestDTO->source)) {\n return TrialPoolDeductionPlanEntity::none();\n }\n\n $preTrialBonus = max(0, (int)$walletRow->frozen_bonus);\n $walletLogModel = new WalletLogModel($this->requestDTO->uid);\n $trialPoolBizId = $this->makeTrialPoolBizId(\n $this->requestDTO->uid,\n (string)$this->requestDTO->biz_id\n );\n $existingTrialPoolLog = $walletLogModel->findByBizIdAndType(\n $trialPoolBizId,\n WalletLogModel::BIZ_TYPE_TRIAL_POOL_CREATE\n );\n if ($existingTrialPoolLog !== null) {\n return TrialPoolDeductionPlanEntity::fromExistingLog((int)$existingTrialPoolLog->amount);\n }\n\n return TrialPoolDeductionPlanEntity::pendingDeduction($preTrialBonus);\n }\n\n /**\n * 按 biz_type 将金额写入 withdraw / deposit / frozen_bonus,并返回各桶增量。\n *\n * @param WalletService $walletService 钱包服务\n * @param WalletAccountModel $walletRow 入账前钱包行(含 version)\n * @return array{deposit: int, withdraw: int, available_bonus: int, ok: int}\n * @throws Exception 乐观锁冲突时抛出 retry\n */\n private function applyIncBalanceChange(\n WalletService $walletService,\n WalletAccountModel $walletRow\n ): array {\n $allocation = [\n 'deposit' => 0,\n 'withdraw' => 0,\n 'available_bonus' => 0,\n 'ok' => 1,\n ];\n\n if ($this->requestDTO->type === WalletLogModel::BIZ_TYPE_WIN) {\n if ($this->requestDTO->fee > 0) {\n $winAllocation = $this->getWinAllocationByRound($this->requestDTO->fee);\n $allocation['withdraw'] = $winAllocation['withdraw'];\n $allocation['deposit'] = $winAllocation['deposit'];\n $allocation['available_bonus'] = $winAllocation['bonus'];\n $allocation['ok'] = $walletService->win(\n $allocation['withdraw'],\n $allocation['deposit'],\n $allocation['available_bonus'],\n (int)$walletRow->version,\n $this->requestDTO->currency\n );\n }\n\n return $allocation;\n }\n\n if ($this->requestDTO->type === WalletLogModel::BIZ_TYPE_RECHARGE) {\n $allocation['deposit'] = $this->requestDTO->fee;\n $allocation['ok'] = $walletService->inc(\n 0,\n $this->requestDTO->fee,\n $this->requestDTO->bonus,\n (int)$walletRow->version,\n $this->requestDTO->currency\n );\n\n return $allocation;\n }\n\n $allocation['deposit'] = $this->requestDTO->fee;\n $allocation['ok'] = $walletService->inc(\n $this->requestDTO->fee,\n 0,\n $this->requestDTO->bonus,\n (int)$walletRow->version,\n $this->requestDTO->currency\n );\n\n return $allocation;\n }\n\n /**\n * 首充 recharge 入账后执行 Trial Pool 扣款(方案 C,已在同一事务内)。\n *\n * @param WalletAccountModel $walletAccountModel 钱包账户模型\n * @param TrialPoolDeductionPlanEntity $trialPoolPlan 扣款计划\n * @return int 转入 Pool 的体验金(最小货币单位)\n */\n private function finalizeTrialPoolAfterRechargeInc(\n WalletAccountModel $walletAccountModel,\n TrialPoolDeductionPlanEntity $trialPoolPlan\n ): int {\n if (!$trialPoolPlan->shouldDeduct) {\n return $trialPoolPlan->trialPoolAmount;\n }\n\n return $this->deductTrialPoolInRechargeTransaction(\n $walletAccountModel,\n $trialPoolPlan->preTrialBonus,\n (string)$this->requestDTO->biz_id,\n (string)($this->requestDTO->trace_id ?? '')\n );\n }\n\n /**\n * 赠金入账时推送 console 统计事件。\n *\n * @return void\n */\n private function publishBonusConsoleEventIfNeeded(): void\n {\n if ($this->requestDTO->bonus <= 0) {\n return;\n }\n\n $this->sendConsoleBus('addBonus', $this->requestDTO->bonus);\n }\n\n /**\n * 充值入账成功后写入 recharge fund lot(真金打码批次)。\n *\n * @return void\n */\n private function insertRechargeFundLotsIfNeeded(): void\n {\n if ($this->requestDTO->type !== WalletLogModel::BIZ_TYPE_RECHARGE) {\n return;\n }\n\n $rechargeFundLotService = new RechargeFundLotService($this->requestDTO->uid);\n $rechargeFundLotService->insertRechargeFundLots($this->resolveRechargeOrderDtoForLots());\n }\n\n /**\n * 校验钱包行更新或流水写入结果,失败则抛 retry 供 maxRetry 重试。\n *\n * @param int $mutationResult 底层 update 影响行数\n * @param string $retryLogMessage 冲突时日志文案\n * @return void\n * @throws Exception 影响行数不为 1 时\n */\n private function assertWalletMutationSucceeded(int $mutationResult, string $retryLogMessage): void\n {\n if ($mutationResult === 1) {\n return;\n }\n\n LoggerService::error(__METHOD__, $retryLogMessage);\n throw new Exception('retry', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n\n /**\n * 根据入账前快照与各桶增量组装加钱后的 WalletEntity(含首充 trial_pool_amount)。\n *\n * @param WalletAccountModel $walletRow 入账前钱包行\n * @param array{deposit: int, withdraw: int, available_bonus: int, ok: int} $incAllocation 各桶增量\n * @param int $trialPoolAmount 转入 Trial Pool 的体验金\n * @param bool $trialPoolProcessed 是否已处理 Trial Pool(含幂等复用)\n * @return WalletEntity\n */\n private function buildWalletEntityAfterInc(\n WalletAccountModel $walletRow,\n array $incAllocation,\n int $trialPoolAmount,\n bool $trialPoolProcessed\n ): WalletEntity {\n $frozenBonusAfter = $walletRow->frozen_bonus + $incAllocation['available_bonus'];\n if ($this->requestDTO->type === WalletLogModel::BIZ_TYPE_RECHARGE) {\n $frozenBonusAfter += (int)$this->requestDTO->bonus;\n if ($trialPoolProcessed) {\n $frozenBonusAfter = max(0, (int)$this->requestDTO->bonus);\n }\n }\n\n return new WalletEntity([\n 'd' => $walletRow->deposit_balance + $incAllocation['deposit'],\n 'deposit' => $walletRow->deposit_balance + $incAllocation['deposit'],\n 'w' => $walletRow->withdraw_balance + $incAllocation['withdraw'],\n 'withdraw' => $walletRow->withdraw_balance + $incAllocation['withdraw'],\n 'ub' => $walletRow->available_bonus,\n 'b' => $frozenBonusAfter,\n 'trial_pool_amount' => $trialPoolAmount,\n ]);\n }\n\n /**\n * 按本局下注拆分比例分配派奖到各资金桶(兼容旧 inc+win 路径)。\n *\n * @param int $winAmount 派奖总额\n * @return array{withdraw: int, deposit: int, bonus: int}\n */\n private function getWinAllocationByRound(int $winAmount): array\n {\n $defaultAllocation = [\n 'withdraw' => $winAmount,\n 'deposit' => 0,\n 'bonus' => 0,\n ];\n if ($winAmount <= 0) {\n return $defaultAllocation;\n }\n\n if ($this->requestDTO->round_id === '') {\n return $defaultAllocation;\n }\n $betSplitKey = RedisKeyManagerService::getBetSplitRoundKey(\n $this->requestDTO->uid,\n $this->requestDTO->currency,\n $this->requestDTO->round_id\n );\n $cachedSplit = Redis::get($betSplitKey);\n if (empty($cachedSplit)) {\n return $defaultAllocation;\n }\n $decodedSplit = json_decode($cachedSplit, true);\n if (!is_array($decodedSplit)) {\n return $defaultAllocation;\n }\n\n $betSplit = [\n 'bonus' => max((int)($decodedSplit['bonus'] ?? 0), 0),\n 'deposit' => max((int)($decodedSplit['deposit'] ?? 0), 0),\n 'withdraw' => max((int)($decodedSplit['withdraw'] ?? 0), 0),\n ];\n $betTotal = array_sum($betSplit);\n if ($betTotal <= 0) {\n return $defaultAllocation;\n }\n\n $allocation = [\n 'bonus' => 0,\n 'deposit' => 0,\n 'withdraw' => 0,\n ];\n $remainder = $winAmount;\n $firstBucketKey = '';\n foreach (['bonus', 'deposit', 'withdraw'] as $bucketKey) {\n $bucketPart = $betSplit[$bucketKey];\n if ($bucketPart <= 0) {\n continue;\n }\n if ($firstBucketKey === '') {\n $firstBucketKey = $bucketKey;\n }\n $allocatedPart = (int)floor(($winAmount * $bucketPart) / $betTotal);\n $allocation[$bucketKey] = $allocatedPart;\n $remainder -= $allocatedPart;\n }\n\n if ($firstBucketKey === '') {\n return $defaultAllocation;\n }\n $allocation[$firstBucketKey] += $remainder;\n\n return $allocation;\n }\n\n /**\n * 组装充值 Lot 落库 DTO(优先专用 recharge 入口,兼容旧 update 充值)。\n *\n * @return WalletRechargeRequestDTO\n */\n private function resolveRechargeOrderDtoForLots(): WalletRechargeRequestDTO\n {\n if ($this->rechargeOrderDto !== null) {\n return $this->rechargeOrderDto;\n }\n\n return new WalletRechargeRequestDTO([\n 'uid' => $this->requestDTO->uid,\n 'currency' => $this->requestDTO->currency,\n 'order_no' => $this->requestDTO->biz_id,\n 'deposit_amount' => max(0, (int)$this->requestDTO->fee),\n 'bonus_amount' => max(0, (int)$this->requestDTO->bonus),\n 'source' => $this->requestDTO->source,\n 'organization' => (int)$this->requestDTO->organization,\n 'trace_id' => $this->requestDTO->trace_id,\n 'wager_rate' => 1,\n ]);\n }\n\n /**\n * 生成首充清零体验金的确定性幂等 biz_id(每用户每首充订单只清零一次)。\n *\n * @param int $uid 用户 ID\n * @param string $orderNo 首充订单号\n * @return string 幂等业务 ID\n */\n private function makeTrialPoolBizId(int $uid, string $orderNo): string\n {\n return 'trialpool:' . $uid . ':' . $orderNo;\n }\n\n /**\n * 构造 Trial Pool 清零流水 remark(记录订单、快照与实际清零额,便于审计与对账)。\n *\n * @param string $orderNo 首充订单号\n * @param int $snapshot 充值前体验金快照\n * @param int $cleared 实际清零金额\n * @return string JSON 字符串\n */\n private function buildTrialPoolRemark(string $orderNo, int $snapshot, int $cleared): string\n {\n return json_encode([\n 'order_no' => $orderNo,\n 'phase' => 'trial',\n 'kind' => 'pool_create',\n 'snapshot' => $snapshot,\n 'cleared' => $cleared,\n ], JSON_UNESCAPED_UNICODE);\n }\n\n /**\n * 在充值入账事务内扣减充值前体验金并写入 Trial Pool 幂等流水(方案 C,选项 B 排除本次充值赠送)。\n *\n * @param WalletAccountModel $walletAccountModel 钱包账户模型(已 startTrans)\n * @param int $preTrialBonus 充值前体验金(inc 前快照)\n * @param string $orderNo 首充订单号\n * @param string $traceId 链路追踪 ID\n * @return int 实际转入 Pool 的金额(最小货币单位)\n * @throws WalletException 流水落库失败时\n */\n private function deductTrialPoolInRechargeTransaction(\n WalletAccountModel $walletAccountModel,\n int $preTrialBonus,\n string $orderNo,\n string $traceId = ''\n ): int {\n $uid = $this->requestDTO->uid;\n $currency = $this->requestDTO->currency;\n $bizId = $this->makeTrialPoolBizId($uid, $orderNo);\n $walletLogModel = new WalletLogModel($uid);\n\n $duplicatedLog = $walletLogModel->findByBizIdAndType($bizId, WalletLogModel::BIZ_TYPE_TRIAL_POOL_CREATE);\n if ($duplicatedLog !== null) {\n return (int)$duplicatedLog->amount;\n }\n\n $walletRow = $walletAccountModel->lockWallet($currency);\n if ($walletRow === null) {\n throw new WalletException('钱包不存在', ErrorCode::PARAMS_NOT_FOUND);\n }\n\n $currentBonus = (int)$walletRow->frozen_bonus;\n $clearedAmount = min($preTrialBonus, $currentBonus);\n if ($clearedAmount > 0) {\n $updated = $walletAccountModel->decBalance(\n 0,\n 0,\n $clearedAmount,\n 0,\n (int)$walletRow->version,\n $currency\n );\n if ($updated !== 1) {\n throw new Exception('retry', ErrorCode::WALLET_CONFLICT_ERROR);\n }\n $currentBonus -= $clearedAmount;\n }\n\n $balanceAfter = (int)$walletRow->withdraw_balance\n + (int)$walletRow->deposit_balance\n + $currentBonus;\n $logWritten = WalletLogService::getInstance()->create(new WalletLogEntity([\n 'uid' => $uid,\n 'currency' => $currency,\n 'biz_type' => WalletLogModel::BIZ_TYPE_TRIAL_POOL_CREATE,\n 'biz_id' => $bizId,\n 'amount' => $clearedAmount,\n 'wallet_type' => WalletLogModel::WALLET_TYPE_GIFT,\n 'remark' => $this->buildTrialPoolRemark($orderNo, $preTrialBonus, $clearedAmount),\n 'trace_id' => $traceId,\n 'balance' => $balanceAfter,\n ]));\n if ($logWritten !== 1) {\n throw new WalletException('记录 Trial Pool 清零日志失败', ErrorCode::SYSTEM_ERROR);\n }\n\n return $clearedAmount;\n }\n}\n","path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletRechargeLogic.php"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Write","input":{"contents":"registerLogic = $registerLogic ?? new WalletRegisterLogic();\n $this->betWinLogic = $betWinLogic ?? new WalletBetWinLogic();\n $this->rechargeLogic = $rechargeLogic ?? new WalletRechargeLogic();\n $this->withdrawLogic = $withdrawLogic ?? new WalletWithdrawLogic();\n $this->trialLogic = $trialLogic ?? new WalletTrialLogic();\n }\n\n /**\n * 充值入账(存款 + 首充奖励)专用入口。\n *\n * @param WalletRechargeRequestDTO $rechargeOrderDto 已校验充值订单\n * @return WalletEntity 入账后余额\n * @throws WalletException\n */\n public function runRechargeOrder(WalletRechargeRequestDTO $rechargeOrderDto): WalletEntity\n {\n $rechargeResult = $this->rechargeLogic->runRechargeOrder($rechargeOrderDto);\n $this->deleteWalletCache($rechargeOrderDto->uid, $rechargeOrderDto->currency);\n\n return $rechargeResult;\n }\n\n /**\n * 钱包统一更新入口:按 type 分发至对应域 Logic。\n *\n * @param WalletUpdateRequestDTO $requestDTO 已校验请求\n * @return WalletEntity|bool|null 业务结果\n * @throws WalletException 未知 type 或业务失败时\n */\n public function run(WalletUpdateRequestDTO $requestDTO): WalletEntity|bool|null\n {\n $handler = $this->resolveHandler($requestDTO->type);\n if ($handler === null) {\n LoggerService::error(__METHOD__, '业务不存在' . $requestDTO->type);\n throw new WalletException('业务不存在', ErrorCode::PARAMS_METHOD_NOT_FOUND);\n }\n\n $result = $handler($requestDTO);\n $this->deleteWalletCache($requestDTO->uid, $requestDTO->currency);\n\n return $result;\n }\n\n /**\n * 查询用户钱包余额与可选累计充提统计。\n *\n * @param int $uid 用户 ID\n * @param string $currency 币种\n * @param int $needStat 是否附带累计充值/提现(1 是)\n * @return WalletEntity 余额实体\n */\n public static function query(int $uid, string $currency, int $needStat = 0): WalletEntity\n {\n return WalletQueryLogic::query($uid, $currency, $needStat);\n }\n\n /**\n * 按 biz_id + biz_type 查询 wallet 幂等流水(供 gateway 补偿与审计追踪)。\n *\n * @param int $uid 用户 ID\n * @param string $bizId 幂等业务 ID\n * @param string $bizType 幂等业务类型\n * @return array{found: bool, ledger_id: int, wallet_lot_id: int}\n */\n public static function queryBizLog(int $uid, string $bizId, string $bizType): array\n {\n return WalletQueryLogic::queryBizLog($uid, $bizId, $bizType);\n }\n\n /**\n * 解析 biz_type 对应的子 Logic 处理方法。\n *\n * @param string $bizType 业务类型\n * @return callable|null 可调用 handler\n */\n private function resolveHandler(string $bizType): ?callable\n {\n return match ($bizType) {\n WalletLogModel::BIZ_TYPE_REGISTER => $this->registerLogic->register(...),\n WalletLogModel::BIZ_TYPE_RECHARGE => $this->rechargeLogic->recharge(...),\n WalletLogModel::BIZ_TYPE_BONUS_TO_DEPOSIT => $this->withdrawLogic->bonusToDeposit(...),\n WalletLogModel::BIZ_TYPE_WITHDRAW => $this->withdrawLogic->withdraw(...),\n WalletLogModel::BIZ_TYPE_WITHDRAW_FROZEN => $this->withdrawLogic->withdrawFrozen(...),\n WalletLogModel::BIZ_TYPE_WITHDRAW_FAIL => $this->withdrawLogic->rollbackWithdraw(...),\n WalletLogModel::BIZ_TYPE_REWARD,\n WalletLogModel::BIZ_TYPE_EMAIL => $this->rechargeLogic->reward(...),\n WalletLogModel::BIZ_TYPE_BET => $this->betWinLogic->bet(...),\n WalletLogModel::BIZ_TYPE_WIN => $this->betWinLogic->win(...),\n WalletLogModel::BIZ_TYPE_SIGN => $this->rechargeLogic->rechargeSign(...),\n WalletLogModel::BIZ_TYPE_TRIAL_GRANT => $this->trialLogic->grantTrial(...),\n WalletLogModel::BIZ_TYPE_TRIAL_BONUS_EXPIRE => $this->trialLogic->expireTrial(...),\n self::BIZ_TYPE_SWITCH_BONUS => $this->withdrawLogic->switchBonus(...),\n default => null,\n };\n }\n\n /**\n * 清理用户钱包 Redis 缓存。\n *\n * @param int $uid 用户 ID\n * @param string $currency 币种\n * @return void\n */\n private function deleteWalletCache(int $uid, string $currency): void\n {\n $walletAccountModel = new WalletAccountModel($uid);\n $walletService = new WalletService($walletAccountModel);\n $walletService->deleteWalletCache($currency);\n }\n}\n","path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php"}},{"type":"tool_use","name":"StrReplace","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletRechargeLogic.php","old_string":" /** update 接口 type=switchBonus 时的路由键(与 slot-sdk 一致) */\n public const BIZ_TYPE_SWITCH_BONUS = 'switchBonus';\n\n protected ?WalletRechargeRequestDTO $rechargeOrderDto = null;","new_string":" protected ?WalletRechargeRequestDTO $rechargeOrderDto = null;"}},{"type":"tool_use","name":"StrReplace","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/README.md","old_string":"### 2.2 本仓库实现入口(对照 PRD 用)\n\n- 钱包编排:[`app/api/logic/WalletLogic.php`](app/api/logic/WalletLogic.php)。\n- 下注:[`app/service/wallet/BetService.php`](app/service/wallet/BetService.php)。\n- 注册入账与 Lot:[`app/service/wallet/RegisterService.php`](app/service/wallet/RegisterService.php)。\n- Lot 模型与来源常量:[`app/model/multi/WalletFundLotModel.php`](app/model/multi/WalletFundLotModel.php)。","new_string":"### 2.2 本仓库实现入口(对照 PRD 用)\n\n- 钱包统一入口(门面):[`app/api/logic/WalletLogic.php`](app/api/logic/WalletLogic.php) — `run()` / `runRechargeOrder()` 分发。\n- 子域 Logic(按用例):\n - 查询:[`WalletQueryLogic.php`](app/api/logic/WalletQueryLogic.php)\n - 注册:[`WalletRegisterLogic.php`](app/api/logic/WalletRegisterLogic.php)\n - 下注/派奖:[`WalletBetWinLogic.php`](app/api/logic/WalletBetWinLogic.php)\n - 充值/入账:[`WalletRechargeLogic.php`](app/api/logic/WalletRechargeLogic.php)\n - 提现/Bonus 转换:[`WalletWithdrawLogic.php`](app/api/logic/WalletWithdrawLogic.php)\n - Trial 体验金:[`WalletTrialLogic.php`](app/api/logic/WalletTrialLogic.php)\n - 公共能力:[`AbstractWalletMutationLogic.php`](app/api/logic/AbstractWalletMutationLogic.php)\n- 下注核心算法:[`app/service/wallet/BetService.php`](app/service/wallet/BetService.php)。\n- 注册入账与 Lot:[`app/service/wallet/RegisterService.php`](app/service/wallet/RegisterService.php)。\n- Lot 模型与来源常量:[`app/model/multi/WalletFundLotModel.php`](app/model/multi/WalletFundLotModel.php)。"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/.cursor/skills/slot-backend-completion-report/SKILL.md"}},{"type":"tool_use","name":"Shell","input":{"command":"cd /Users/ray/Documents/project/docker && docker compose exec -T php82 bash -c 'for f in AbstractWalletMutationLogic WalletQueryLogic WalletRegisterLogic WalletBetWinLogic WalletRechargeLogic WalletWithdrawLogic WalletTrialLogic WalletLogic; do php -l /app/www/slot/slot-wallet/app/api/logic/${f}.php || exit 1; done'","description":"Syntax check all new Wallet logic PHP files"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Shell","input":{"command":"cd /Users/ray/Documents/project/docker && docker compose exec -T php82 bash -c 'ls /app/www/ray/slot-wallet/app/api/logic/ 2>/dev/null || ls /app/company/ray/slots/slot-wallet/app/api/logic/ 2>/dev/null || find /app -name WalletLogic.php 2>/dev/null | head -5'","description":"Find slot-wallet path inside Docker container"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Shell","input":{"command":"cd /Users/ray/Documents/project/docker && docker compose exec -T php82 bash -c 'for f in AbstractWalletMutationLogic WalletQueryLogic WalletRegisterLogic WalletBetWinLogic WalletRechargeLogic WalletWithdrawLogic WalletTrialLogic WalletLogic; do php -l /app/www/ray/slot-wallet/app/api/logic/${f}.php || exit 1; done'","description":"Re-run php -l on all Wallet logic files"}},{"type":"tool_use","name":"Shell","input":{"command":"SLOT_ROOT=\"/Users/ray/Documents/project/www/ray\" ~/.cursor/skills/slot-backend-completion-report/scripts/report.sh","description":"Run slot backend completion report script"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/.cursor/rules/slot-wallet-prd-and-money.mdc","limit":40}},{"type":"tool_use","name":"Shell","input":{"command":"wc -l /Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/*.php","description":"Count lines in all logic files"}},{"type":"tool_use","name":"TodoWrite","input":{"merge":true,"todos":[{"id":"phase0-prep","content":"Phase 0:确认 minus() 死代码、补 PHPDoc、更新 README 入口索引","status":"completed"},{"id":"phase1-base-query-register","content":"Phase 1:新建 AbstractWalletMutationLogic + WalletQueryLogic + WalletRegisterLogic,WalletLogic 委托 register/query","status":"completed"},{"id":"phase2-bet-win","content":"Phase 2:新建 WalletBetWinLogic,迁移 bet/win 及 Redis round 辅助逻辑","status":"completed"},{"id":"phase3-recharge","content":"Phase 3:新建 WalletRechargeLogic,迁移 inc/recharge/reward 与 Trial Pool 充值链","status":"completed"},{"id":"phase4-withdraw-trial","content":"Phase 4:新建 WalletWithdrawLogic + WalletTrialLogic,修复 switchBonus 路由(如需要)","status":"completed"}]}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"StrReplace","input":{"new_string":"## 代码入口(对照 PRD)\n\n- `app/api/logic/WalletLogic.php` — 统一入口门面(`run` / `runRechargeOrder`)\n- `app/api/logic/WalletQueryLogic.php` — 余额与幂等流水查询\n- `app/api/logic/WalletRegisterLogic.php` — 注册赠金\n- `app/api/logic/WalletBetWinLogic.php` — 下注 / 派奖编排\n- `app/api/logic/WalletRechargeLogic.php` — 充值 / 入账 / 奖励\n- `app/api/logic/WalletWithdrawLogic.php` — 提现 / Bonus 转换\n- `app/api/logic/WalletTrialLogic.php` — Trial 体验金\n- `app/service/wallet/BetService.php` — 下注\n- `app/service/wallet/RegisterService.php` — 注册与 Lot\n- `app/model/multi/WalletFundLotModel.php` — Lot 来源与常量","old_string":"## 代码入口(对照 PRD)\n\n- `app/api/logic/WalletLogic.php` — 编排\n- `app/service/wallet/BetService.php` — 下注\n- `app/service/wallet/RegisterService.php` — 注册与 Lot\n- `app/model/multi/WalletFundLotModel.php` — Lot 来源与常量","path":"/Users/ray/Documents/project/www/ray/slot-wallet/.cursor/rules/slot-wallet-prd-and-money.mdc"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"`WalletLogic` 已从约 1885 行拆成门面 + 7 个子 Logic,对外 API 不变。\n\n## 拆分结果\n\n| 文件 | 行数 | 职责 |\n|---|---|---|\n| [`WalletLogic.php`](slot-wallet/app/api/logic/WalletLogic.php) | 149 | 门面:`run()` / `runRechargeOrder()` 分发 + 清缓存 |\n| [`AbstractWalletMutationLogic.php`](slot-wallet/app/api/logic/AbstractWalletMutationLogic.php) | 229 | 公共能力:流水、重试、Console Bus、初始化钱包 |\n| [`WalletQueryLogic.php`](slot-wallet/app/api/logic/WalletQueryLogic.php) | 90 | `query` / `queryBizLog` |\n| [`WalletRegisterLogic.php`](slot-wallet/app/api/logic/WalletRegisterLogic.php) | 66 | 注册赠金 |\n| [`WalletBetWinLogic.php`](slot-wallet/app/api/logic/WalletBetWinLogic.php) | 425 | 下注 / 派奖 + Redis 中间派奖 |\n| [`WalletRechargeLogic.php`](slot-wallet/app/api/logic/WalletRechargeLogic.php) | 644 | 充值 / inc / reward + Trial Pool |\n| [`WalletWithdrawLogic.php`](slot-wallet/app/api/logic/WalletWithdrawLogic.php) | 347 | 提现 / Bonus 转换 |\n| [`WalletTrialLogic.php`](slot-wallet/app/api/logic/WalletTrialLogic.php) | 243 | Trial 发放 / 过期 |\n\n## 其他变更\n\n- **删除** 死代码 `minus()`(无调用方、不在路由表)\n- **修复** `switchBonus` 路由(原先缺失,slot-sdk 调用会报「业务不存在」)\n- **更新** [`README.md`](slot-wallet/README.md) 与 [`.cursor/rules/slot-wallet-prd-and-money.mdc`](slot-wallet/.cursor/rules/slot-wallet-prd-and-money.mdc) 入口索引\n- **Controller 未改**:仍只注入 `WalletLogic`;`WalletLogic::query()` 静态方法保持兼容\n\n## 检测结果\n\n### 门禁脚本\n```\n=== slot-backend-completion-report ===\nSLOT_ROOT=/Users/ray/Documents/project/www/ray\nSLOT_DOCKER_HOST_ROOT=/Users/ray/Documents/project\n\n--- verify-slot-backend ---\n=== verify-slot-backend ===\nchanged: 9 file(s), 6 php\nPASS\n\n--- php -l (docker) ---\nNo syntax errors detected in app/entity/WalletEntity.php\nNo syntax errors detected in app/service/MQKeyManagerService.php\nNo syntax errors detected in app/service/WalletService.php\nNo syntax errors detected in app/api/logic/WalletLogic.php\nNo syntax errors detected in app/entity/WalletEntity.php\nNo syntax errors detected in app/service/trial/TrialFreeSpinBetService.php\n\n=== summary ===\nPASS (verify + php -l)\n```\n\n### PHP 语法(docker php -l)\n```\nNo syntax errors detected in AbstractWalletMutationLogic.php\nNo syntax errors detected in WalletQueryLogic.php\nNo syntax errors detected in WalletRegisterLogic.php\nNo syntax errors detected in WalletBetWinLogic.php\nNo syntax errors detected in WalletRechargeLogic.php\nNo syntax errors detected in WalletWithdrawLogic.php\nNo syntax errors detected in WalletTrialLogic.php\nNo syntax errors detected in WalletLogic.php\n```\n\n### 规范对照\n| 项 | 结果 | 说明 |\n|----|------|------|\n| PHPDoc / 常量注释 | 通过 | 新建 Logic 方法均有中文 PHPDoc;门面 `BIZ_TYPE_SWITCH_BONUS` 有注释 |\n| Logic/Service 返回 Entity | 通过 | 对外仍返回 `WalletEntity`,无新增 array 返回 |\n| 分层 | 通过 | Controller 不变;子 Logic 编排,核心算法仍走 BetService/WinService 等 |\n| Logic §3 / §8 | 通过 | 子 Logic 显式接收 DTO;无仅用于日志的多余参数 |\n\n### 结论\n通过\n\nPHPDoc: checked\n\n[REDACTED]"}]}}
{"role":"user","message":{"content":[{"type":"text","text":"\n“”“register(...)”“” 这三个点是什么意思?\n"}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletLogic.php","offset":100,"limit":40}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"`register(...)` 里的 **`(...)` 不是省略号**,而是 **PHP 8.1 的「一等 callable 语法」**(First-class Callable Syntax)。\n\n在 [`WalletLogic.php`](slot-wallet/app/api/logic/WalletLogic.php) 里:\n\n```118:118:slot-wallet/app/api/logic/WalletLogic.php\n WalletLogModel::BIZ_TYPE_REGISTER => $this->registerLogic->register(...),\n```\n\n含义是:**把 `$this->registerLogic->register` 这个方法「包装成一个可调用的函数/闭包」**,并且 **调用时传入的参数会原样转发**给 `register`。\n\n等价写法大致是:\n\n```php\n// 旧写法:匿名函数转发\nfn($requestDTO) => $this->registerLogic->register($requestDTO)\n\n// 或者\nfunction ($requestDTO) {\n return $this->registerLogic->register($requestDTO);\n}\n```\n\n在 `run()` 里就是这样用的:\n\n```php\n$handler = $this->resolveHandler($requestDTO->type); // 得到 callable\n$result = $handler($requestDTO); // 等价于 registerLogic->register($requestDTO)\n```\n\n**为什么要这么写?**\n\n- `resolveHandler()` 返回的是 **`callable`**,不是立刻执行方法\n- `register(...)` 只「绑定方法 + 对象」,**不马上调用**\n- 参数个数/类型由后面的 `$handler($requestDTO)` 决定,`(...)` 表示「有多少参数就传多少」\n\n**和 PHP 里其他 `...` 的区别:**\n\n| 写法 | 含义 |\n|---|---|\n| `register(...)` | 把方法变成 callable(PHP 8.1+) |\n| `...$args` | 展开数组当参数(spread operator) |\n| `register()` | 直接调用方法 |\n\n所以这里的三个点 **不是**「参数省略」或「等等」,而是 **「生成一个可转发参数的 callable」** 的语法糖。\n\n[REDACTED]"}]}}