21 lines
34 KiB
JSON
21 lines
34 KiB
JSON
{"role":"user","message":{"content":[{"type":"text","text":"<user_query>\n帮我看看slot-wallet,现在下注扣款的顺序是怎么样的\n</user_query>"}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"正在梳理 `slot-wallet` 中下注扣款的流程与顺序。\n\n[REDACTED]"},{"type":"tool_use","name":"Grep","input":{"pattern":"下注|扣款|bet|deduct|debit","path":"/Users/ray/Documents/project/www/ray/slot-wallet","glob":"**/*.{php}","head_limit":80}},{"type":"tool_use","name":"Glob","input":{"glob_pattern":"**/*Bet*","target_directory":"/Users/ray/Documents/project/www/ray/slot-wallet"}},{"type":"tool_use","name":"Glob","input":{"glob_pattern":"**/*Wallet*","target_directory":"/Users/ray/Documents/project/www/ray/slot-wallet"}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/service/wallet/BetService.php"}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletBetWinLogic.php","limit":150}},{"type":"tool_use","name":"Grep","input":{"pattern":"§10|扣款|组优先|hit_side|deduct","path":"/Users/ray/Documents/project/www/ray/slot-wallet","glob":"**/*.{php,md}"}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"offset":270,"limit":80,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/model/multi/WalletFundLotModel.php"}},{"type":"tool_use","name":"Grep","input":{"glob":"doc/wallet.md","path":"/Users/ray/Documents/project/www/ray/slot-wallet","pattern":"§10|扣款|组优先|Withdrawable|Deposit|Bonus|Cash"}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/.cursor/rules/slot-wallet-prd-and-money.mdc","limit":120}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/doc/wallet.md","pattern":"扣款|§9|§10|组优先|Withdrawable|Locked Deposit|Bonus"}},{"type":"tool_use","name":"Read","input":{"limit":80,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/tests/Feature/WalletGroupWageringTest.php"}},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/model/multi/WalletFundLotModel.php","pattern":"LOT_TYPE_"}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"limit":100,"offset":80,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/tests/Feature/WalletGroupWageringTest.php"}},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet","pattern":"组优先|Cash 最后|Withdrawable.*Deposit.*Bonus|splitBetByWagerPriority"}},{"type":"tool_use","name":"Read","input":{"limit":50,"offset":400,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/doc/wallet.md"}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"limit":100,"offset":260,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/api/logic/WalletBetWinLogic.php"}},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/model/multi/WalletFundLotGroupModel.php","pattern":"lockEarliestActiveGroup","-A":20}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"`slot-wallet` 的下注扣款,核心在 `WalletBetWinLogic::bet` 做分支,`BetService::execute` 负责真实资金拆分。下面按**实际代码**说明顺序,并标出与 PRD 的差异。\n\n## 1. 入口分支(先判场景,再扣款)\n\n`WalletBetWinLogic::bet` 在事务内大致走:\n\n1. 锁钱包行\n2. 按 `biz_id` 幂等(重复请求直接返回,不再扣)\n3. 按场景选扣款路径:\n - **试玩期** → 只扣 `frozen_bonus`(Trial 体验金)\n - **FreeSpin 代付** → 不扣用户余额(平台代付)\n - **正常下注** → 走 `BetService::execute`\n\n```85:101:slot-wallet/app/api/logic/WalletBetWinLogic.php\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\n---\n\n## 2. 正常下注的资金扣款顺序(当前实现)\n\n`BetService` 的策略是:**组优先、Cash 最后**(不是 PRD §9 写的「可提现优先」)。\n\n### 扣款来源顺序\n\n| 优先级 | 来源 | 说明 |\n|--------|------|------|\n| 1 | 各组 Lot(按打码顺序) | 先最早组,再下一组… |\n| 1.1 | 组内 **Deposit Lot** | `lot_type=1`,同组内先于 Bonus |\n| 1.2 | 组内 **Bonus Lot** | `lot_type=2`;Waiting 会在扣减时激活 |\n| 2 | **Withdrawable(Cash)** | `withdraw_balance`,Lot 不够时才用 |\n\nLot 查询排序:\n\n```297:309:slot-wallet/app/model/multi/WalletFundLotModel.php\n public function lockConsumableLotsInWagerOrder(string $currency): array\n {\n return $this->getDb()\n ->where('uid', '=', $this->_shardVal)\n ->where('currency', '=', $currency)\n ->whereIn('status', [self::STATUS_WAITING, self::STATUS_ACTIVE])\n ->where('remaining_amount', '>', 0)\n ->order('consume_priority_at', 'asc')\n ->order('lot_type', 'asc')\n ->order('id', 'asc')\n ->lock(true)\n ->select()\n ->toArray();\n }\n```\n\n等价于:\n\n```\n最早组 Deposit → 最早组 Bonus → 次组 Deposit → 次组 Bonus → … → Withdrawable(Cash)\n```\n\n拆分逻辑:\n\n```120:168:slot-wallet/app/service/wallet/BetService.php\n protected function splitBetByWagerPriority(WalletUpdateRequestDTO $requestDTO, WalletAccountModel $wallet): array\n {\n $remaining = $requestDTO->fee;\n $consumptions = [];\n\n $orderedLots = $this->walletFundLotModel->lockConsumableLotsInWagerOrder($requestDTO->currency);\n foreach ($orderedLots as $lot) {\n // ... 按 Lot 顺序逐个扣,直到 remaining=0\n }\n\n if ($remaining > 0) {\n $withdrawUse = min((int)$wallet->withdraw_balance, $remaining);\n if ($withdrawUse > 0) {\n $consumptions[] = [\n 'fund_type' => self::FUND_SOURCE_WITHDRAW,\n // ...\n ];\n $remaining -= $withdrawUse;\n }\n }\n```\n\n### 举例(注册送 100 Deposit + 100 Bonus,无 Cash,下注 150)\n\n测试 `testGroupBetConsumesDepositBeforeBonusAndAdvancesGroupWager` 验证的就是:\n\n- 先扣 Deposit 100\n- 再扣 Bonus 50\n- Cash 为 0,不参与\n\n---\n\n## 3. 单次下注内部执行顺序(`BetService::execute`)\n\n正常下注在事务里按这个顺序跑:\n\n```74:96:slot-wallet/app/service/wallet/BetService.php\n $progressGroup = $this->walletFundLotGroupModel->lockEarliestActiveGroup($requestDTO->currency);\n $progressGroupNo = $progressGroup !== null ? (string)$progressGroup['group_no'] : '';\n\n $consumptions = $this->splitBetByWagerPriority($requestDTO, $wallet);\n $totals = $this->sumConsumptionByFundType($consumptions);\n\n $updated = $this->walletAccountModel->decBalance(\n $totals['withdraw'],\n $totals['deposit'],\n $totals['bonus'],\n // ...\n );\n\n $this->consumeMemberLots($consumptions);\n $this->promoteNextWaitingFundLotIfNeeded($requestDTO->currency);\n $this->accumulateProgressGroupWager($progressGroup, $requestDTO->fee);\n```\n\n1. 锁定 **FIFO 最早 Active 打码组**(`progress_group`)\n2. 按上面顺序拆分扣款来源\n3. 扣聚合余额(`withdraw_balance` / `deposit_balance` / `frozen_bonus`)\n4. 逐 Lot 扣 `remaining_amount`,Waiting Lot 先激活\n5. 若无 Active Lot,把最早 Waiting Lot 晋级为 Active\n6. **整笔下注额**累加到 `progress_group.current_wager`(含 Cash 部分)\n7. 计算 `hit_side`(供派奖归属)\n\n---\n\n## 4. 打码进度 vs 扣款顺序\n\n- **扣款顺序**:Lot 优先(组内 Deposit → Bonus),Cash 最后\n- **打码进度**:整笔 `fee` 都计入最早 Active 组的 `current_wager`(Cash 也计入组进度)\n- **Lot 自身进度**:只有从该 Lot 出的钱才推进该 Lot 的 `current_wager`\n\n---\n\n## 5. 派奖归属(`hit_side`)\n\n扣完后会算本局命中侧,供 `win` 反查:\n\n- 命中当前 `progress_group` 的 **Deposit** → `hit_side = deposit`\n- 否则命中该组 **Bonus** → `hit_side = bonus`\n- 都没命中(纯 Cash / 无 Active 组)→ `hit_side = cash`\n\n---\n\n## 6. 与 PRD 文档的差异(重要)\n\n`doc/wallet.md` §9 写的是:\n\n> **Withdrawable → Locked Deposit → Bonus Credits**(可提现优先)\n\n但当前代码是:\n\n> **组 Lot 优先(Deposit → Bonus)→ Withdrawable 最后**\n\n`.cursor/rules/slot-wallet-prd-and-money.mdc` 里还写着旧的 `Bonus → Deposit → Withdrawable`,也和代码不一致。\n\n**以代码为准的话**,当前扣款顺序是:\n\n```\n最早组 Deposit → 最早组 Bonus → … → 次组 … → Withdrawable(Cash)\n```\n\n---\n\n## 7. 特殊场景速查\n\n| 场景 | 扣什么 |\n|------|--------|\n| 试玩期 | 只扣 `frozen_bonus` |\n| FreeSpin 代付 | 不扣用户余额 |\n| 正常下注 | 走 `BetService`,组 Lot 优先、Cash 最后 |\n| 余额不足 | 抛 `有效余额不足` |\n\n如果你关心的是「某用户当前余额下,下一笔 100 会从哪几个桶扣」,可以说一下 uid/币种/余额结构,我可以按这套规则帮你推一笔具体拆分。\n\n[REDACTED]"}]}}
|
||
{"role":"user","message":{"content":[{"type":"text","text":"<user_query>\n正常下注,我想额外增加一个扣款次序。相当于两种模式,通过配置文件的方式来选择使用那种,这样可行吗?\n</user_query>"}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"可行。我先看一下现有配置方式和扣款逻辑的集中点,再给你具体方案。\n\n[REDACTED]"},{"type":"tool_use","name":"Grep","input":{"pattern":"config\\(","glob":"**/BetService.php","path":"/Users/ray/Documents/project/www/ray/slot-wallet"}},{"type":"tool_use","name":"Glob","input":{"glob_pattern":"**/config/*.php","target_directory":"/Users/ray/Documents/project/www/ray/slot-wallet"}},{"type":"tool_use","name":"Grep","input":{"pattern":"bet.*order|deduct|wager.*priority|consume_priority","glob":"**/*.{php,php.example}","path":"/Users/ray/Documents/project/www/ray/slot-wallet","-i":true}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/config/app.php"}},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet","pattern":"ShareConfigService|get\\(","glob":"**/*.php","head_limit":30}},{"type":"tool_use","name":"Read","input":{"limit":50,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/service/wallet/WinService.php"}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Grep","input":{"glob":"**/*","path":"/Users/ray/Documents/project/www/ray/slot-wallet","pattern":"wagering_configuration|bet_deduct|deduction_mode"}},{"type":"tool_use","name":"Read","input":{"limit":60,"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/service/ShareConfigService.php"}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"**可行**,而且改动面相对集中。扣款顺序目前几乎都在 `BetService::splitBetByWagerPriority()`,很适合做成「策略 + 配置切换」。\n\n## 为什么可行\n\n正常下注链路已经分层清楚:\n\n```\nWalletBetWinLogic::bet\n └─ BetService::execute\n ├─ splitBetByWagerPriority() ← 唯一决定「从哪扣、扣多少」\n ├─ decBalance() ← 按汇总结果扣聚合余额\n ├─ consumeMemberLots() ← 扣 Lot 明细\n ├─ accumulateProgressGroupWager() ← 整笔 fee 计入打码组\n └─ resolveHitSide() ← 派奖归属\n```\n\n只要 `splitBetByWagerPriority()` 能按模式产出不同的 `consumptions` 数组,后面流程基本不用动。`fund_detail`、`hit_side`、打码进度都会跟着实际扣款明细走。\n\n---\n\n## 两种模式怎么定义\n\n| 模式 | 扣款顺序 | 对应关系 |\n|------|----------|----------|\n| **A(当前实现)** | 组 Lot 优先 → Cash 最后 | 最早组 Deposit → 最早组 Bonus → … → Withdrawable |\n| **B(PRD §9)** | Cash 优先 → Lot 次之 | Withdrawable → Deposit Lot → Bonus Lot |\n\n组内 Lot 的 FIFO(`consume_priority_at` + `lot_type` + `id`)两种模式都可以共用,只是 **Withdrawable 放在最前还是最后** 的差别。\n\n---\n\n## 配置放哪\n\n按需求选一种即可:\n\n**1. 本地配置(最简单,适合全站统一)**\n\n```php\n// config/wallet.php\nreturn [\n 'bet_deduction_mode' => getenv('BET_DEDUCTION_MODE') ?: 'group_lot_first',\n];\n```\n\n枚举建议:\n\n- `group_lot_first` — 当前行为(默认,保证线上不变)\n- `withdrawable_first` — PRD §9\n\n**2. Center / ShareConfig(适合按渠道、组织差异化)**\n\n项目里已有 `ShareConfigService`,Trial 等也是走 center 配置。若需要「A 渠道用模式 A、B 渠道用模式 B」,可以:\n\n- center 下发 `betDeductionMode`\n- `BetService` 按 `source` / `organization` 解析\n\n**3. 不建议** 做成用户级配置 — 同一用户在不同局之间切换,对账和体验都容易乱。\n\n---\n\n## 推荐实现方式\n\n在 `BetService` 里做策略分发,改动最小:\n\n```php\nprotected function splitBetByWagerPriority(...): array\n{\n $mode = $this->resolveBetDeductionMode($requestDTO);\n\n return match ($mode) {\n 'withdrawable_first' => $this->splitBetWithdrawableFirst($requestDTO, $wallet),\n default => $this->splitBetGroupLotFirst($requestDTO, $wallet), // 现有逻辑\n };\n}\n```\n\n- 把现有逻辑挪到 `splitBetGroupLotFirst()`\n- 新增 `splitBetWithdrawableFirst()`:先扣 `withdraw_balance`,再用 `lockConsumableLotsInWagerOrder()` 扣 Lot\n- `resolveBetDeductionMode()` 读 `config('wallet.bet_deduction_mode')` 或 ShareConfig\n\n不必单独建 Service,除非后面模式会超过 2 种。\n\n---\n\n## 不需要改的部分\n\n这些和「扣款顺序」解耦,两种模式共用即可:\n\n- `decBalance()` / `consumeMemberLots()` — 只吃 `consumptions` 汇总\n- `accumulateProgressGroupWager()` — 整笔 `fee` 仍计入最早 Active 组\n- `resolveHitSide()` — 仍按 `consumptions` 里是否命中 `progress_group` 的 Deposit/Bonus 侧\n- Trial / FreeSpin 分支 — 不走 `BetService` 正常扣款\n\n---\n\n## 需要注意的点\n\n**1. 测试要覆盖两种模式**\n\n`WalletGroupWageringTest` 目前验证的是「Deposit 先于 Bonus、Cash 最后」。加模式 B 后需要:\n\n- 现有测试保持 mode A\n- 新增 mode B 用例(例如:有 Cash 时先扣 Cash,组进度仍 +150)\n\n**2. 文档要对齐**\n\n`doc/wallet.md` §9 写的是 Withdrawable 优先,代码是 Lot 优先。加配置后建议在 PRD 里写清:\n\n- 两种模式语义\n- 默认用哪种\n- 切换是否允许线上热切\n\n**3. 线上切换风险**\n\n中途改模式会改变用户「钱从哪个桶出」,但不影响总余额是否够下注。若已有用户习惯当前行为,建议:\n\n- 默认保持 `group_lot_first`\n- 新环境 / 新渠道再开 `withdrawable_first`\n\n**4. 前端 / 任务进度展示**\n\n`player-task-progress` 文档里提到排序应与扣款顺序一致。若前端有「预计从哪扣」类展示,需要知道当前模式;只读进度接口本身不受影响。\n\n**5. 审计字段**\n\n`fund_detail.deductions[].deduct_order` 会如实记录实际顺序,两种模式对账都够用,这点设计是合理的。\n\n---\n\n## 结论\n\n- **技术上完全可行**,核心改动集中在 `BetService::splitBetByWagerPriority()` 一处。\n- **配置用 `config/wallet.php` + env 即可**;要按渠道差异化再上 ShareConfig。\n- **默认保持现有 `group_lot_first`**,新模式(如 PRD 的 `withdrawable_first`)作为可选项,风险可控。\n\n如果你确定要做,我可以直接帮你落一版:加配置、`BetService` 双策略,以及两种模式各一条 Feature 测试。你更倾向 **全站统一配置**,还是 **按 source/organization 从 center 读**?\n\n[REDACTED]"}]}}
|
||
{"role":"user","message":{"content":[{"type":"text","text":"<user_query>\n先本地配置吧。 现在代码里面是 ”“”最早组 Deposit → 最早组 Bonus → … → 次组 … → Withdrawable(Cash)“”“,现在增加一种,跟原来的区别是 ”“”Withdrawable(Cash)“”“这个放到最前面,其余的不变\n</user_query>"}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"开始实现:新增本地配置,并在 `BetService` 中支持「Cash 优先」扣款模式。\n\n[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/config/autoload.php"}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/service/wallet/BetService.php"}},{"type":"tool_use","name":"Glob","input":{"glob_pattern":"**/.env*","target_directory":"/Users/ray/Documents/project/www/ray/slot-wallet"}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet","pattern":"config\\('","head_limit":15}},{"type":"tool_use","name":"Glob","input":{"glob_pattern":"**/tests/**/*Bet*","target_directory":"/Users/ray/Documents/project/www/ray/slot-wallet"}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/.cursor/skills/slot-backend-completion-report/SKILL.md","limit":80}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Grep","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/.env.develop","pattern":"TASK_OFF|getenv"}},{"type":"tool_use","name":"Glob","input":{"glob_pattern":"**/*Unit*","target_directory":"/Users/ray/Documents/project/www/ray/slot-wallet"}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"正在添加配置文件并重构 `BetService`。\n\n[REDACTED]"},{"type":"tool_use","name":"Write","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/config/wallet.php","contents":"<?php\n\n/**\n * slot-wallet 资金域本地配置。\n */\nreturn [\n /**\n * 正常下注扣款模式(Trial / FreeSpin 不受影响):\n * - group_lot_first(默认):最早组 Deposit → 最早组 Bonus → … → Withdrawable 最后\n * - withdrawable_first:Withdrawable 优先,其余 Lot 顺序与 group_lot_first 一致\n */\n 'bet_deduction_mode' => getenv('BET_DEDUCTION_MODE') ?: 'group_lot_first',\n];\n"}},{"type":"tool_use","name":"StrReplace","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/service/wallet/BetService.php","old_string":"/**\n * 游戏下注资金拆分、组优先扣款与命中归属服务(§10)。\n */\nclass BetService\n{\n /** 出资来源:锁定充值。 */\n private const FUND_SOURCE_DEPOSIT = WalletFundLotModel::LOT_TYPE_DEPOSIT;","new_string":"/**\n * 游戏下注资金拆分、可配置扣款顺序与命中归属服务(§10)。\n */\nclass BetService\n{\n /** 下注扣款模式:组 Lot 优先、Withdrawable 最后(默认,与线上一致)。 */\n private const DEDUCTION_MODE_GROUP_LOT_FIRST = 'group_lot_first';\n\n /** 下注扣款模式:Withdrawable 优先,其余 Lot 扣款顺序不变。 */\n private const DEDUCTION_MODE_WITHDRAWABLE_FIRST = 'withdrawable_first';\n\n /** 出资来源:锁定充值。 */\n private const FUND_SOURCE_DEPOSIT = WalletFundLotModel::LOT_TYPE_DEPOSIT;"}},{"type":"tool_use","name":"StrReplace","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/service/wallet/BetService.php","old_string":" /**\n * 执行下注:按「组优先、Cash 最后」扣款(§10.3),整笔有效投注只计 progress_group(§10.1),\n * 并产出本局命中明细(fund_detail)与命中归属(progress_group_no / hit_side)供派奖反查(§11)。","new_string":" /**\n * 执行下注:按配置扣款模式拆分资金来源(§10.3),整笔有效投注只计 progress_group(§10.1),\n * 并产出本局命中明细(fund_detail)与命中归属(progress_group_no / hit_side)供派奖反查(§11)。"}},{"type":"tool_use","name":"StrReplace","input":{"path":"/Users/ray/Documents/project/www/ray/slot-wallet/app/service/wallet/BetService.php","old_string":" /**\n * 按「组优先、Cash 最后」拆分本次下注:先按打码顺序消费各组 Lot(组内 Deposit 在前),再用 Cash 补足;余额不足抛异常。\n *\n * @param WalletUpdateRequestDTO $requestDTO 请求DTO\n * @param WalletAccountModel $wallet 已锁定钱包\n * @return array<int, array<string, mixed>> 各资金来源出资明细(按扣款顺序)\n * @throws WalletException\n */\n protected function splitBetByWagerPriority(WalletUpdateRequestDTO $requestDTO, WalletAccountModel $wallet): array\n {\n $remaining = $requestDTO->fee;\n $consumptions = [];\n\n $orderedLots = $this->walletFundLotModel->lockConsumableLotsInWagerOrder($requestDTO->currency);\n foreach ($orderedLots as $lot) {\n if ($remaining <= 0) {\n break;\n }\n $available = (int)$lot['remaining_amount'];\n if ($available <= 0) {\n continue;\n }\n\n $consumeAmount = min($available, $remaining);\n $validBetAmount = $consumeAmount;\n\n $consumptions[] = [\n 'fund_type' => (int)$lot['lot_type'],\n 'lot_id' => (int)$lot['id'],\n 'lot_no' => (string)($lot['lot_no'] ?? ''),\n 'group_no' => (string)($lot['group_no'] ?? ''),\n 'bet_amount' => $consumeAmount,\n 'valid_bet_amount' => $validBetAmount,\n 'version' => (int)$lot['version'],\n 'need_activate' => (int)$lot['status'] === WalletFundLotModel::STATUS_WAITING,\n 'next_status' => $this->resolveNextStatus($lot, $validBetAmount),\n ];\n $remaining -= $consumeAmount;\n }\n\n if ($remaining > 0) {\n $withdrawUse = min((int)$wallet->withdraw_balance, $remaining);\n if ($withdrawUse > 0) {\n $consumptions[] = [\n 'fund_type' => self::FUND_SOURCE_WITHDRAW,\n 'lot_id' => 0,\n 'lot_no' => '',\n 'group_no' => '',\n 'bet_amount' => $withdrawUse,\n 'valid_bet_amount' => $withdrawUse,\n 'version' => 0,\n 'need_activate' => false,\n 'next_status' => WalletFundLotModel::STATUS_ACTIVE,\n ];\n $remaining -= $withdrawUse;\n }\n }\n\n if ($remaining > 0) {\n LoggerService::warning(__METHOD__, [\n 'uid' => $requestDTO->uid,\n 'currency' => $requestDTO->currency,\n 'fee' => $requestDTO->fee,\n 'shortfall' => $remaining,\n ]);\n throw new WalletException('有效余额不足', ErrorCode::WALLET_BALANCE_NOT_ENOUGH);\n }\n\n return $consumptions;\n }","new_string":" /**\n * 按本地配置的扣款模式拆分本次下注;余额不足抛异常。\n *\n * @param WalletUpdateRequestDTO $requestDTO 请求DTO\n * @param WalletAccountModel $wallet 已锁定钱包\n * @return array<int, array<string, mixed>> 各资金来源出资明细(按扣款顺序)\n * @throws WalletException\n */\n protected function splitBetByWagerPriority(WalletUpdateRequestDTO $requestDTO, WalletAccountModel $wallet): array\n {\n $deductionMode = $this->resolveBetDeductionMode();\n\n if ($deductionMode === self::DEDUCTION_MODE_WITHDRAWABLE_FIRST) {\n return $this->splitBetWithdrawableFirst($requestDTO, $wallet);\n }\n\n return $this->splitBetGroupLotFirst($requestDTO, $wallet);\n }\n\n /**\n * 解析正常下注扣款模式(非法值回退为 group_lot_first,保证线上默认行为不变)。\n *\n * @return string 见 DEDUCTION_MODE_* 常量\n */\n private function resolveBetDeductionMode(): string\n {\n $configuredMode = (string)config('wallet.bet_deduction_mode', self::DEDUCTION_MODE_GROUP_LOT_FIRST);\n if ($configuredMode === self::DEDUCTION_MODE_WITHDRAWABLE_FIRST) {\n return self::DEDUCTION_MODE_WITHDRAWABLE_FIRST;\n }\n\n return self::DEDUCTION_MODE_GROUP_LOT_FIRST;\n }\n\n /**\n * 组 Lot 优先扣款:最早组 Deposit → 最早组 Bonus → … → Withdrawable 最后补足。\n *\n * @param WalletUpdateRequestDTO $requestDTO 请求DTO\n * @param WalletAccountModel $wallet 已锁定钱包\n * @return array<int, array<string, mixed>> 各资金来源出资明细(按扣款顺序)\n * @throws WalletException\n */\n private function splitBetGroupLotFirst(WalletUpdateRequestDTO $requestDTO, WalletAccountModel $wallet): array\n {\n $remaining = $requestDTO->fee;\n $consumptions = [];\n\n $remaining = $this->appendLotConsumptions($consumptions, $requestDTO, $remaining);\n $remaining = $this->appendWithdrawConsumption($consumptions, $wallet, $remaining);\n $this->ensureBetFullyFunded($requestDTO, $remaining);\n\n return $consumptions;\n }\n\n /**\n * Withdrawable 优先扣款:先消费可提现余额,再按打码顺序消费各组 Lot(组内 Deposit 在前)。\n *\n * @param WalletUpdateRequestDTO $requestDTO 请求DTO\n * @param WalletAccountModel $wallet 已锁定钱包\n * @return array<int, array<string, mixed>> 各资金来源出资明细(按扣款顺序)\n * @throws WalletException\n */\n private function splitBetWithdrawableFirst(WalletUpdateRequestDTO $requestDTO, WalletAccountModel $wallet): array\n {\n $remaining = $requestDTO->fee;\n $consumptions = [];\n\n $remaining = $this->appendWithdrawConsumption($consumptions, $wallet, $remaining);\n $remaining = $this->appendLotConsumptions($consumptions, $requestDTO, $remaining);\n $this->ensureBetFullyFunded($requestDTO, $remaining);\n\n return $consumptions;\n }\n\n /**\n * 按打码顺序将 Lot 出资追加到 consumptions,并返回尚未满足的剩余下注额。\n *\n * @param array<int, array<string, mixed>> $consumptions 出资明细(按引用追加)\n * @param WalletUpdateRequestDTO $requestDTO 请求DTO\n * @param int $remaining 待扣减剩余下注额\n * @return int 追加 Lot 出资后仍未满足的剩余下注额\n */\n private function appendLotConsumptions(\n array &$consumptions,\n WalletUpdateRequestDTO $requestDTO,\n int $remaining\n ): int {\n if ($remaining <= 0) {\n return 0;\n }\n\n $orderedLots = $this->walletFundLotModel->lockConsumableLotsInWagerOrder($requestDTO->currency);\n foreach ($orderedLots as $lot) {\n if ($remaining <= 0) {\n break;\n }\n $available = (int)$lot['remaining_amount'];\n if ($available <= 0) {\n continue;\n }\n\n $consumeAmount = min($available, $remaining);\n $validBetAmount = $consumeAmount;\n\n $consumptions[] = [\n 'fund_type' => (int)$lot['lot_type'],\n 'lot_id' => (int)$lot['id'],\n 'lot_no' => (string)($lot['lot_no'] ?? ''),\n 'group_no' => (string)($lot['group_no'] ?? ''),\n 'bet_amount' => $consumeAmount,\n 'valid_bet_amount' => $validBetAmount,\n 'version' => (int)$lot['version'],\n 'need_activate' => (int)$lot['status'] === WalletFundLotModel::STATUS_WAITING,\n 'next_status' => $this->resolveNextStatus($lot, $validBetAmount),\n ];\n $remaining -= $consumeAmount;\n }\n\n return $remaining;\n }\n\n /**\n * 将可提现余额出资追加到 consumptions,并返回尚未满足的剩余下注额。\n *\n * @param array<int, array<string, mixed>> $consumptions 出资明细(按引用追加)\n * @param WalletAccountModel $wallet 已锁定钱包\n * @param int $remaining 待扣减剩余下注额\n * @return int 追加 Withdrawable 出资后仍未满足的剩余下注额\n */\n private function appendWithdrawConsumption(\n array &$consumptions,\n WalletAccountModel $wallet,\n int $remaining\n ): int {\n if ($remaining <= 0) {\n return 0;\n }\n\n $withdrawUse = min((int)$wallet->withdraw_balance, $remaining);\n if ($withdrawUse <= 0) {\n return $remaining;\n }\n\n $consumptions[] = [\n 'fund_type' => self::FUND_SOURCE_WITHDRAW,\n 'lot_id' => 0,\n 'lot_no' => '',\n 'group_no' => '',\n 'bet_amount' => $withdrawUse,\n 'valid_bet_amount' => $withdrawUse,\n 'version' => 0,\n 'need_activate' => false,\n 'next_status' => WalletFundLotModel::STATUS_ACTIVE,\n ];\n\n return $remaining - $withdrawUse;\n }\n\n /**\n * 校验剩余下注额是否已扣清,不足时记录日志并抛业务异常。\n *\n * @param WalletUpdateRequestDTO $requestDTO 请求DTO\n * @param int $remaining 尚未满足的剩余下注额\n * @throws WalletException 有效余额不足时\n */\n private function ensureBetFullyFunded(WalletUpdateRequestDTO $requestDTO, int $remaining): void\n {\n if ($remaining <= 0) {\n return;\n }\n\n LoggerService::warning(__METHOD__, [\n 'uid' => $requestDTO->uid,\n 'currency' => $requestDTO->currency,\n 'fee' => $requestDTO->fee,\n 'shortfall' => $remaining,\n 'bet_deduction_mode' => $this->resolveBetDeductionMode(),\n ]);\n throw new WalletException('有效余额不足', ErrorCode::WALLET_BALANCE_NOT_ENOUGH);\n }"}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Shell","input":{"command":"SLOT_ROOT=\"/Users/ray/Documents/project/www/ray/slot-wallet\" ~/.cursor/skills/slot-backend-completion-report/scripts/report.sh","description":"Run slot backend completion report script"}},{"type":"tool_use","name":"ReadLints","input":{"paths":["/Users/ray/Documents/project/www/ray/slot-wallet/app/service/wallet/BetService.php","/Users/ray/Documents/project/www/ray/slot-wallet/config/wallet.php"]}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Shell","input":{"command":"docker exec -w /app/www/ray/slot-wallet php82 php -l app/service/wallet/BetService.php && docker exec -w /app/www/ray/slot-wallet php82 php -l config/wallet.php","description":"Syntax check changed PHP files in docker"}}]}}
|
||
{"role":"assistant","message":{"content":[{"type":"text","text":"已实现本地配置切换两种正常下注扣款模式。\n\n## 改动说明\n\n**1. 新增 `config/wallet.php`**\n\n```php\n'bet_deduction_mode' => getenv('BET_DEDUCTION_MODE') ?: 'group_lot_first',\n```\n\n**2. 重构 `BetService`**\n\n| 模式 | 值 | 扣款顺序 |\n|------|-----|----------|\n| 默认(当前线上行为) | `group_lot_first` | 最早组 Deposit → 最早组 Bonus → … → **Withdrawable 最后** |\n| 新增 | `withdrawable_first` | **Withdrawable 最先** → 最早组 Deposit → 最早组 Bonus → … |\n\nLot 内部顺序(`consume_priority_at` + Deposit 先于 Bonus)两种模式一致,只是 Cash 在最前还是最后不同。\n\nTrial / FreeSpin 不受影响,仍走各自分支。\n\n---\n\n## 如何切换\n\n在 `.env` 里加:\n\n```bash\nBET_DEDUCTION_MODE=withdrawable_first\n```\n\n不配或配成其他非法值,都会回退到 `group_lot_first`,保证线上默认行为不变。\n\n改配置后需重启 Webman:\n\n```bash\ndocker exec -w /app/www/ray/slot-wallet php82 php webman restart -d\n```\n\n---\n\n## 行为说明\n\n- 打码进度:两种模式下整笔 `fee` 仍计入最早 Active 组\n- 派奖归属:仍按 `consumptions` 是否命中 progress_group 的 Deposit/Bonus 侧\n- 审计:`fund_detail.deductions[].deduct_order` 会记录实际扣款顺序\n\n---\n\n## 检测结果\n\n### 门禁脚本\n```\n=== slot-backend-completion-report ===\nPASS (verify + php -l)\n```\n\n### PHP 语法(docker php -l)\n```\nNo syntax errors detected in app/service/wallet/BetService.php\nNo syntax errors detected in config/wallet.php\n```\n\n### 规范对照\n| 项 | 结果 | 说明 |\n|----|------|------|\n| PHPDoc 中文首行 | 通过 | 新增方法均有中文说明 |\n| 类常量注释 | 通过 | `DEDUCTION_MODE_*` 均有中文注释 |\n| Logic/Service 返回值 | 通过 | 未改签名,BetService 仍沿用既有 array 豁免 |\n| 分层 | 通过 | 逻辑集中在 Service,配置在 config |\n\n**PHPDoc: checked**\n\n**结论:PASS**\n\n[REDACTED]"}]}} |