Files
ray zhou f71a5c59af ok
2026-05-29 11:21:40 +08:00

16 lines
19 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{"role":"user","message":{"content":[{"type":"text","text":"<user_query>\nInvestigate wallet balance operations related to \"首充定格\" / free credits freeze in slot_wallet. Find:\n1. How total balance is decreased during freeze\n2. Interaction with console service\n3. What happens if console callback/event fails\n4. Transaction and rollback behavior\n\nSearch in /Users/ray/Documents/project/www/slot/slot_wallet and /Users/ray/Documents/project/www/slot/slot_lib. Return file paths, key methods, and the fund operation sequence.\n</user_query>"}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"Investigating wallet freeze flow for first-deposit free credits — searching slot_wallet and slot_lib for freeze, console, and balance logic.\n\n[REDACTED]"},{"type":"tool_use","name":"updateCurrentStep","input":{"current_step":"Searching freeze balance flow"}},{"type":"tool_use","name":"Grep","input":{"pattern":"首充|定格|freeze|FreeCredits|free_credits|freeCredits","path":"/Users/ray/Documents/project/www/slot/slot_wallet","-i":true}},{"type":"tool_use","name":"Grep","input":{"pattern":"首充|定格|freeze|FreeCredits|free_credits|freeCredits","path":"/Users/ray/Documents/project/www/slot/slot_lib","-i":true}},{"type":"tool_use","name":"Grep","input":{"pattern":"console.*freeze|freeze.*console|FreeCredits","path":"/Users/ray/Documents/project/www/slot/slot_wallet","-i":true}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php","offset":560,"limit":200}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_lib/src/services/WalletService.php"}},{"type":"tool_use","name":"Grep","input":{"pattern":"freeCreditsFreeze|sendConsoleBus|FREE_CREDITS","path":"/Users/ray/Documents/project/www/slot/slot_wallet"}},{"type":"tool_use","name":"Grep","input":{"pattern":"freeCreditsFreeze|freeCreditsClaim","path":"/Users/ray/Documents/project/www/slot/slot_console"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php","offset":1,"limit":120}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php","offset":960,"limit":60}},{"type":"tool_use","name":"Grep","input":{"pattern":"freeCreditsFreeze|getBalance|function update|maxRetry","path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/api"}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php","offset":400,"limit":80}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php","offset":170,"limit":100}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php","offset":895,"limit":70}},{"type":"tool_use","name":"Grep","input":{"pattern":"function minus|function inc|function wallet","path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/service"}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php","offset":1,"limit":120}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/service/WalletService.php"}},{"type":"tool_use","name":"Grep","input":{"pattern":"function addLog|biz_id|idempotent","path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php"}},{"type":"tool_use","name":"Grep","input":{"pattern":"handleRecharge|freezeFirstRecharge","path":"/Users/ray/Documents/project/www/slot/slot_console"}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/api/controller/WalletController.php","offset":25,"limit":40}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php","offset":140,"limit":100}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php","offset":870,"limit":35}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_console/app/command/event/RechargeEvent.php"}},{"type":"tool_use","name":"Grep","input":{"pattern":"decBalance|incBalance|biz_id","path":"/Users/ray/Documents/project/www/slot/slot_wallet/app"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Grep","input":{"pattern":"function create|duplicate|uniq|已存在","path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/service/WalletLogService.php"}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/model/multi/WalletAccountModel.php","offset":70,"limit":140}},{"type":"tool_use","name":"Grep","input":{"pattern":"RechargeEvent|QUEUE_CONSOLE|CONSOLE_BUS","path":"/Users/ray/Documents/project/www/slot/slot_console"}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_wallet/scripts/dev_free_credits_freeze.php"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/service/WalletLogService.php"}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_console/app/command/EventBus.php","offset":1,"limit":120}},{"type":"tool_use","name":"Grep","input":{"pattern":"freeCreditsFreeze|freeCreditsClaim|Recharge","path":"/Users/ray/Documents/project/www/slot/slot_console/app/command/EventBus.php"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Grep","input":{"pattern":"Recharge|RechargeEvent|freeCredits","path":"/Users/ray/Documents/project/www/slot/slot_console/app/command/EventBus.php"}},{"type":"tool_use","name":"Grep","input":{"pattern":"insertData|Duplicate|1062|uniq_uid_bizid","path":"/Users/ray/Documents/project/www/slot/slot_wallet"}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_console/app/command/EventBus.php","offset":115,"limit":100}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/model/SelfBaseModel.php","offset":20,"limit":40}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_console/tests/Unit/FreeCreditsHandleRechargeTest.php","offset":60,"limit":40}},{"type":"tool_use","name":"Grep","input":{"pattern":"freeCreditsClaim|freeCreditsFreeze","path":"/Users/ray/Documents/project/www/slot/slot_console/app/command"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Glob","input":{"glob_pattern":"**/freeCredits*.php","target_directory":"/Users/ray/Documents/project/www/slot/slot_console"}},{"type":"tool_use","name":"Grep","input":{"pattern":"freeCreditsClaim","path":"/Users/ray/Documents/project/www/slot/slot_console"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Grep","input":{"pattern":"postAction|function update","path":"/Users/ray/Documents/project/www/slot/slot_lib/src/basic/BaseApiService.php"}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_wallet/app/model/multi/WalletLogModel.php","offset":35,"limit":25}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_lib/src/basic/BaseApiService.php","offset":20,"limit":50}},{"type":"tool_use","name":"Read","input":{"path":"/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php","offset":220,"limit":30}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"[REDACTED]"},{"type":"tool_use","name":"updateCurrentStep","input":{"current_step":"Compiling freeze flow report","final_summary":"首充定格在 slot_wallet 内通过 `freeCreditsFreeze` 从 deposit+withdraw 扣减总额(先 deposit 后 withdraw单库事务 + 乐观锁重试;**定格本身不向 Console 发 MQ**。Console 在充值成功 MQ`Recharge`)后异步调用 `FreeCreditsLogic::freezeFirstRecharge` 发起 RPCMQ/定格失败仅记日志且消息仍 ack存在与充值已入账但未定格的窗口Console DB 与钱包非分布式事务,存在跨服务不一致风险。","completed_subtitle":"Mapped freeze balance flow"}}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"## Executive summary\n\n**首充定格freeze** is a wallet RPC (`freeCreditsFreeze`) that deducts **deposit + withdraw** (not bonus). It runs **after** first recharge credits the wallet, triggered **asynchronously** by Console via the `Recharge` MQ bus. **Freeze does not call `sendConsoleBus`**. Console/event failures are mostly **log-only**; wallet and console DB are **not** one distributed transaction.\n\n---\n\n## 1. How total balance is decreased during freeze\n\n### Entry & routing\n\n| Layer | File | Method |\n|-------|------|--------|\n| HTTP client (lib) | `/Users/ray/Documents/project/www/slot/slot_lib/src/services/WalletService.php` | `freeCreditsFreeze()` → `update(WALLET_TYPE_FREE_CREDITS_FREEZE, ...)` |\n| API | `/Users/ray/Documents/project/www/slot/slot_wallet/app/api/controller/WalletController.php` | `update()` |\n| Logic | `/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php` | `run()` → `freeCreditsFreeze()` |\n\n`WalletLogic::run()` dispatches by `requestDTO->type` (`freeCreditsFreeze`), then clears wallet cache.\n\n### Balance definition & deduction split\n\n```178:181:/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php\n private function getBalance(WalletAccountModel $model): int\n {\n return $model->withdraw_balance + $model->deposit_balance;\n }\n```\n\nIn `freeCreditsFreeze()`:\n\n1. Pre-check: `getBalance($wallet) >= fee` (else `WALLET_BALANCE_NOT_ENOUGH`).\n2. Split (deposit first, same pattern as bet):\n - `deposit = min(deposit_balance, fee)`\n - `withdraw = fee - deposit`\n3. DB update: `WalletService::minus($withdraw, $deposit, 0, version, currency)` → `WalletAccountModel::decBalance()` (optimistic `version`, column guards).\n4. Log: `addLog(-fee, balance - fee, BIZ_TYPE_FREE_CREDITS_FREEZE, WALLET_TYPE_GIFT)`.\n\n**Bonus is untouched.** “Total balance” visible to the user = `deposit_balance + withdraw_balance` only.\n\n### Persistence\n\n- Account: `/Users/ray/Documents/project/www/slot/slot_wallet/app/model/multi/WalletAccountModel.php` — `decBalance()`\n- Log biz type: `/Users/ray/Documents/project/www/slot/slot_wallet/app/model/multi/WalletLogModel.php` — `BIZ_TYPE_FREE_CREDITS_FREEZE`\n- Idempotency key: `(uid, biz_id, biz_type)` unique — `biz_id` e.g. `free_credits_freeze:{orderId}`\n\n---\n\n## 2. Interaction with Console service\n\n### Trigger chain (async, not synchronous with freeze RPC)\n\n```mermaid\nsequenceDiagram\n participant Pay as Pay/Recharge caller\n participant Wallet as slot_wallet\n participant MQ as RabbitMQ console_bus\n participant Console as slot_console EventBus\n participant FC as FreeCreditsLogic\n\n Pay->>Wallet: recharge (inc deposit)\n Wallet->>Wallet: commit + addLog\n Wallet->>MQ: sendConsoleBus('Recharge')\n MQ->>Console: RechargeEvent::handle\n Console->>FC: handleRecharge(uid, wallet_amount, orderId, isFirstRecharge)\n FC->>FC: calcFrozenAmount(balance - recharge)\n FC->>FC: freezeFirstRecharge (console DB txn)\n FC->>Wallet: HTTP freeCreditsFreeze (RPC)\n```\n\n### Console orchestration\n\n| File | Role |\n|------|------|\n| `/Users/ray/Documents/project/www/slot/slot_console/app/command/event/RechargeEvent.php` | Consumes `Recharge` bus; calls `FreeCreditsLogic::handleRecharge()` |\n| `/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php` | `handleRecharge()`, `freezeFirstRecharge()`, `calcFrozenAmount()` |\n| `/Users/ray/Documents/project/www/slot/slot_console/app/command/EventBus.php` | Routes `Recharge` → `RechargeEvent` (dynamic `RechargeEvent` class) |\n\n**Frozen amount formula** (`FreeCreditsLogic::calcFrozenAmount`):\n\n`frozenAmount = max(balanceAfterRecharge - rechargeAmount, 0)` \n= pre-recharge “free” balance (千分位).\n\n**`freezeFirstRecharge()`** (inside `Db::transaction`):\n\n1. Upsert `free_credits_player` (`frozen_amount_qf`, first cash, status, …)\n2. `createPackages()` (first_cash + release tiers)\n3. **`$walletService->freeCreditsFreeze($frozenAmount, 'free_credits_freeze:' . $orderId)`** — HTTP to wallet **inside** console txn (not 2PC)\n\n### Wallet → Console direction\n\n- **`freeCreditsFreeze`**: no `sendConsoleBus` (no outbound MQ from wallet for freeze).\n- **`recharge`**: `sendConsoleBus('Recharge', recharge)` → drives freeze downstream.\n- **`freeCreditsClaim`**: after commit, `sendConsoleBus('freeCreditsClaim', fee)` + `createTask()` (claim path; separate from freeze).\n\n`sendConsoleBus` implementation: `/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php` — `sendConsoleBus()` → `EXCHANGE_CONSOLE` / `QUEUE_CONSOLE_BUS`.\n\n---\n\n## 3. What happens if Console callback/event fails\n\n### A. `Recharge` MQ / `RechargeEvent` fails (freeze never runs)\n\n```73:82:/Users/ray/Documents/project/www/slot/slot_console/app/command/event/RechargeEvent.php\n try {\n (new FreeCreditsLogic())->handleRecharge(...);\n } catch (\\Throwable $e) {\n logger()->error('FreeCredits handleRecharge failed: ' . $e->getMessage(), ['uid' => $busEntity->uid]);\n }\n```\n\n- Exception is **swallowed** (logged only).\n- **`EventBus::deal()` always `$message->ack()`** even on handler errors — **no automatic retry** from ack behavior shown.\n- **Wallet recharge already committed**; user keeps full balance; **no freeze**, no console player/packages.\n\n### B. `freezeFirstRecharge` wallet RPC fails\n\n- Console `Db::transaction` **rolls back** player + packages.\n- Wallet unchanged (RPC failed or never committed).\n\n### C. Wallet freeze succeeds, Console DB commit fails\n\n- Wallet deduction **committed** (separate service/txn).\n- Console player/packages **rolled back** → **cross-service inconsistency** (money gone, no Free Credits state). No compensating rollback in wallet from this path.\n\n### D. Duplicate / retry\n\n- Wallet: same `biz_id` → unique on `wallet_log` → insert fails → wallet txn **rollback** (balance restore).\n- Console: `frozen_amount_qf <= 0` guard skips re-freeze if player already frozen.\n\n### E. `freeCreditsClaim` bus (related, not freeze)\n\n- Wallet fires `sendConsoleBus('freeCreditsClaim', …)` post-commit; failures **only logged** (same pattern as `recharge` side effects).\n- No `freeCreditsClaimEvent` class in console — EventBus default path likely logs **未知消息**; does not undo wallet claim.\n\n---\n\n## 4. Transaction and rollback behavior\n\n### slot_wallet `freeCreditsFreeze` (local ACID)\n\n```600:625:/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php\n $model->startTrans();\n try {\n $ok = $walletService->minus(...);\n ...\n $ok = $this->addLog($this->requestDTO->fee * -1, $balance, ...);\n ...\n $model->commit();\n return new WalletEntity([...]);\n } catch (Throwable $e) {\n $model->rollback();\n throw $e;\n }\n```\n\n- Wrapped in **`maxRetry()`** (default 3): retries on `WALLET_CONFLICT_ERROR` / version conflict; other `WalletException` codes fail fast.\n- `decBalance` / `addLog` must both return `1` or throws `retry` → rollback.\n- **No post-commit side effects** for freeze (unlike `freeCreditsClaim` / `recharge`).\n\n### slot_console `freezeFirstRecharge`\n\n- ThinkPHP `Db::transaction` around player + packages + wallet HTTP.\n- Wallet HTTP is **not** enrolled in that transaction → **best-effort distributed flow**, not XA.\n\n### `freeCreditsClaim` (contrast)\n\n- Wallet: txn commit first; `createTask` + `sendConsoleBus` in outer try/catch — **failures do not roll back wallet**.\n- Console `claim()`: on wallet failure, package status reverted to `READY`.\n\n---\n\n## Fund operation sequence (end-to-end)\n\n| Step | Service | Operation |\n|------|---------|-----------|\n| 1 | Pay / caller | `WalletService::recharge()` → wallet `recharge()` |\n| 2 | slot_wallet | `inc(0, fee, 0)` + recharge logs; **commit** |\n| 3 | slot_wallet | MQ `Recharge` (async) |\n| 4 | slot_console | `RechargeEvent` → `handleRecharge` |\n| 5 | slot_console | `frozenAmount = balance - walletAmount` |\n| 6 | slot_console | DB: player + packages |\n| 7 | slot_wallet | `freeCreditsFreeze`: `minus(withdraw, deposit)` + negative log; **commit** |\n| 8 | slot_console | DB commit (if step 7 OK) |\n\nLater **claim** (not freeze): Console `freeCreditsClaim` RPC → wallet `inc(0, fee)` + log → post-commit wager task + optional bus.\n\n---\n\n## Key file index\n\n**slot_wallet**\n\n- `/Users/ray/Documents/project/www/slot/slot_wallet/app/api/logic/WalletLogic.php` — `freeCreditsFreeze`, `freeCreditsClaim`, `recharge`, `sendConsoleBus`, `maxRetry`, `addLog`\n- `/Users/ray/Documents/project/www/slot/slot_wallet/app/api/controller/WalletController.php` — `update()`\n- `/Users/ray/Documents/project/www/slot/slot_wallet/app/service/WalletService.php` — `minus` / `inc`\n- `/Users/ray/Documents/project/www/slot/slot_wallet/app/model/multi/WalletAccountModel.php` — `decBalance` / `incBalance`\n- `/Users/ray/Documents/project/www/slot/slot_wallet/app/model/multi/WalletLogModel.php` — biz types\n- `/Users/ray/Documents/project/www/slot/slot_wallet/scripts/dev_free_credits_freeze.php` — local freeze test script\n\n**slot_lib**\n\n- `/Users/ray/Documents/project/www/slot/slot_lib/src/services/WalletService.php` — `WALLET_TYPE_FREE_CREDITS_FREEZE`, `freeCreditsFreeze()`, `freeCreditsClaim()`\n\n**slot_console** (orchestration; outside your two roots but part of the flow)\n\n- `/Users/ray/Documents/project/www/slot/slot_console/app/api/logic/FreeCreditsLogic.php`\n- `/Users/ray/Documents/project/www/slot/slot_console/app/command/event/RechargeEvent.php`\n- `/Users/ray/Documents/project/www/slot/slot_console/app/command/EventBus.php`\n\n[REDACTED]"}]}}