From bfb4b76611bb669c740e25fe4d36a48a1014477d Mon Sep 17 00:00:00 2001 From: JiaJun <2394389886@qq.com> Date: Wed, 3 Jun 2026 17:21:13 +0800 Subject: [PATCH] =?UTF-8?q?refactor(game):=20=E9=87=8D=E6=9E=84=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=BB=93=E6=9E=84,=E4=BC=98=E5=8C=96=E9=93=BE?= =?UTF-8?q?=E8=B7=AF,=20=E7=A7=BB=E5=8A=A8=E7=AB=AF=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 useGameBoardVm 数据层实施说明文档 - 移除核心玩法与前端规则摘要文档 - 移除游戏模块数据与界面分层第一阶段实施稿文档 - 清理与数据层重构相关的技术方案说明 - 删除关于 PC 和 Mobile 界面分离的设计规划 - 移除 view-model hooks 架构设计相关内容 --- AGENTS.md | 2 +- CLAUDE.md | 2 +- docs/36字花-useGameBoardVm-数据层实施说明.md | 248 ----- docs/36字花-核心玩法与前端规则摘要.md | 396 -------- ...花-游戏模块数据与界面分层第一阶段实施稿.md | 846 ------------------ docs/36字花-移动端接口设计草案.md | 824 ----------------- docs/frontend-baseline-requirements.md | 439 --------- docs/本次代码变更说明.md | 373 -------- figma/img.jpg | Bin 901544 -> 0 bytes figma/img.png | Bin 104831 -> 0 bytes src/{features/auth => }/api/auth-api.ts | 18 +- src/{features/game => }/api/finance-api.ts | 5 +- src/{features/game => }/api/game-api.ts | 47 +- src/api/index.ts | 4 + .../game => }/api/period-history-api.ts | 8 +- src/assets/system/mobile-modal-header.webp | Bin 0 -> 157616 bytes src/components/mobile-center-modal.tsx | 170 ++++ src/constants/game.ts | 2 +- .../{ => desktop}/desktop-auth-form-parts.tsx | 49 - .../{ => desktop}/desktop-login-form-view.tsx | 0 .../{ => desktop}/desktop-login-form.tsx | 2 +- .../desktop-register-form-view.tsx | 0 .../{ => desktop}/desktop-register-form.tsx | 4 +- .../mobile/mobile-auth-form-parts.tsx | 68 ++ .../mobile/mobile-login-form-view.tsx | 145 +++ .../components/mobile/mobile-login-form.tsx | 43 + .../mobile/mobile-register-form-view.tsx | 276 ++++++ .../mobile/mobile-register-form.tsx | 70 ++ src/features/game/api/index.ts | 4 - src/features/game/api/types.ts | 316 ------- src/features/game/audio/audio-config.ts | 7 +- .../desktop/desktop-animal-overlay.tsx | 2 +- .../components/desktop/desktop-animal.tsx | 2 +- .../components/desktop/desktop-control.tsx | 2 +- .../desktop/desktop-game-history.tsx | 2 +- .../components/desktop/desktop-header.tsx | 5 +- .../components/desktop/desktop-status.tsx | 2 +- .../game/components/desktop/desktop-topup.tsx | 4 +- .../components/desktop/desktop-withdraw.tsx | 4 +- src/features/game/components/index.ts | 4 - .../game/components/mobile/mobile-header.tsx | 44 +- .../game/components/mobile/mobile-topup.tsx | 186 ++++ .../components/mobile/mobile-withdraw.tsx | 640 +++++++++++++ .../shared/entry-notice-gate-modal.tsx | 163 +++- .../components/shared/period-history-list.tsx | 2 +- src/features/game/index.ts | 2 - src/features/game/shared/constants.ts | 17 - src/features/game/shared/flower-assets.ts | 8 +- src/features/game/shared/index.ts | 2 - src/features/game/shared/initial-state.ts | 9 +- src/features/game/shared/selectors.ts | 4 +- src/features/game/shared/types.ts | 135 --- .../auth => }/hooks/auth-error-key.ts | 3 +- .../game => }/hooks/use-animal-vm.ts | 3 +- .../game => }/hooks/use-app-language.ts | 0 src/{features/auth => }/hooks/use-auth.ts | 0 .../hooks/use-auto-hosting-runner.ts | 4 +- .../game => }/hooks/use-deposit-tier-list.ts | 3 +- .../hooks/use-deposit-withdraw-config.ts | 3 +- .../game => }/hooks/use-finance-records-vm.ts | 6 +- .../game => }/hooks/use-game-control-vm.ts | 5 +- .../game => }/hooks/use-game-history-vm.ts | 6 +- .../game => }/hooks/use-game-realtime-sync.ts | 23 +- .../game => }/hooks/use-game-status-vm.ts | 0 .../game => }/hooks/use-header-vm.ts | 2 +- .../auth => }/hooks/use-login-form.ts | 9 +- .../game => }/hooks/use-period-history-vm.ts | 16 +- .../auth => }/hooks/use-register-form.ts | 11 +- .../auth => }/hooks/use-send-sms-code.ts | 2 +- src/{features/game => }/hooks/use-topup-vm.ts | 0 .../game => }/hooks/use-wallet-records-vm.ts | 3 +- .../game => }/hooks/use-withdraw-submit.ts | 5 +- .../game => }/hooks/use-withdraw-vm.ts | 5 +- .../auth => }/hooks/zod-form-resolver.ts | 0 src/i18n/index.ts | 2 +- src/lib/api/api-client.ts | 3 +- src/lib/auth/auth-normalizers.ts | 74 ++ src/lib/auth/auth-session.ts | 24 +- src/lib/auth/require-auth.ts | 6 +- src/lib/head/document-metadata.ts | 7 +- src/lib/notify.ts | 8 +- src/locales/en-US.ts | 4 +- src/locales/id-ID.ts | 4 +- src/locales/ms-MY.ts | 4 +- src/locales/zh-CN.ts | 4 +- src/main.tsx | 5 +- .../main-entry-page.tsx} | 88 +- .../game/entry => main}/mobile-entry.tsx | 2 +- .../game/entry => main}/pc-entry.tsx | 4 +- .../desktop/desktop-auto-setting-modal.tsx | 0 .../desktop/desktop-finance-records-tab.tsx | 2 +- .../modal/desktop/desktop-language-modal.tsx | 2 +- .../modal/desktop/desktop-login-modal.tsx | 2 +- .../modal/desktop/desktop-notice-modal.tsx | 2 +- .../desktop/desktop-period-history-drawer.tsx | 2 +- .../desktop/desktop-procedures-modal.tsx | 0 .../modal/desktop/desktop-register-modal.tsx | 2 +- .../modal/desktop/desktop-rules-modal.tsx | 0 .../modal/desktop/desktop-support-modal.tsx | 0 .../modal/desktop/desktop-userInfo-modal.tsx | 6 +- .../desktop/desktop-wallet-records-tab.tsx | 2 +- .../desktop/desktop-withdraw-topup-modal.tsx | 0 .../mobile/mobile-auto-setting-modal.tsx | 229 +++++ .../mobile/mobile-finance-records-tab.tsx | 181 ++++ src/modal/mobile/mobile-language-modal.tsx | 100 +++ src/modal/mobile/mobile-login-modal.tsx | 33 + src/modal/mobile/mobile-notice-modal.tsx | 190 ++++ .../mobile/mobile-period-history-drawer.tsx | 181 ++++ src/modal/mobile/mobile-procedures-modal.tsx | 101 +++ src/modal/mobile/mobile-register-modal.tsx | 33 + src/modal/mobile/mobile-rules-modal.tsx | 53 ++ src/modal/mobile/mobile-support-modal.tsx | 78 ++ src/modal/mobile/mobile-userInfo-modal.tsx | 328 +++++++ .../mobile/mobile-wallet-records-tab.tsx | 140 +++ .../mobile/mobile-withdraw-topup-modal.tsx | 39 + src/routes/$lang/index.tsx | 4 +- src/{features/auth => }/schema/auth-schema.ts | 4 +- src/store/audio/audio-store.ts | 9 +- src/store/auth/auth-store.ts | 32 +- src/store/game/game-auto-hosting-store.ts | 43 +- src/store/game/game-round-store.ts | 98 +- src/store/game/game-session-store.ts | 22 +- src/store/modal/modal-store.ts | 15 +- .../api/finance-types.ts => type/api.type.ts} | 38 +- .../auth/api/types.ts => type/auth.type.ts} | 114 +-- src/type/game.type.ts | 638 +++++++++++++ src/type/index.ts | 21 +- src/type/system.type.ts | 54 ++ src/vite-env.d.ts | 8 - 129 files changed, 4534 insertions(+), 4227 deletions(-) delete mode 100644 docs/36字花-useGameBoardVm-数据层实施说明.md delete mode 100644 docs/36字花-核心玩法与前端规则摘要.md delete mode 100644 docs/36字花-游戏模块数据与界面分层第一阶段实施稿.md delete mode 100644 docs/36字花-移动端接口设计草案.md delete mode 100644 docs/frontend-baseline-requirements.md delete mode 100644 docs/本次代码变更说明.md delete mode 100644 figma/img.jpg delete mode 100644 figma/img.png rename src/{features/auth => }/api/auth-api.ts (97%) rename src/{features/game => }/api/finance-api.ts (99%) rename src/{features/game => }/api/game-api.ts (97%) create mode 100644 src/api/index.ts rename src/{features/game => }/api/period-history-api.ts (87%) create mode 100644 src/assets/system/mobile-modal-header.webp create mode 100644 src/components/mobile-center-modal.tsx rename src/features/auth/components/{ => desktop}/desktop-auth-form-parts.tsx (65%) rename src/features/auth/components/{ => desktop}/desktop-login-form-view.tsx (100%) rename src/features/auth/components/{ => desktop}/desktop-login-form.tsx (95%) rename src/features/auth/components/{ => desktop}/desktop-register-form-view.tsx (100%) rename src/features/auth/components/{ => desktop}/desktop-register-form.tsx (95%) create mode 100644 src/features/auth/components/mobile/mobile-auth-form-parts.tsx create mode 100644 src/features/auth/components/mobile/mobile-login-form-view.tsx create mode 100644 src/features/auth/components/mobile/mobile-login-form.tsx create mode 100644 src/features/auth/components/mobile/mobile-register-form-view.tsx create mode 100644 src/features/auth/components/mobile/mobile-register-form.tsx delete mode 100644 src/features/game/api/index.ts delete mode 100644 src/features/game/api/types.ts delete mode 100644 src/features/game/components/index.ts create mode 100644 src/features/game/components/mobile/mobile-topup.tsx create mode 100644 src/features/game/components/mobile/mobile-withdraw.tsx delete mode 100644 src/features/game/index.ts delete mode 100644 src/features/game/shared/constants.ts delete mode 100644 src/features/game/shared/types.ts rename src/{features/auth => }/hooks/auth-error-key.ts (97%) rename src/{features/game => }/hooks/use-animal-vm.ts (98%) rename src/{features/game => }/hooks/use-app-language.ts (100%) rename src/{features/auth => }/hooks/use-auth.ts (100%) rename src/{features/game => }/hooks/use-auto-hosting-runner.ts (98%) rename src/{features/game => }/hooks/use-deposit-tier-list.ts (90%) rename src/{features/game => }/hooks/use-deposit-withdraw-config.ts (89%) rename src/{features/game => }/hooks/use-finance-records-vm.ts (96%) rename src/{features/game => }/hooks/use-game-control-vm.ts (98%) rename src/{features/game => }/hooks/use-game-history-vm.ts (97%) rename src/{features/game => }/hooks/use-game-realtime-sync.ts (98%) rename src/{features/game => }/hooks/use-game-status-vm.ts (100%) rename src/{features/game => }/hooks/use-header-vm.ts (99%) rename src/{features/auth => }/hooks/use-login-form.ts (86%) rename src/{features/game => }/hooks/use-period-history-vm.ts (82%) rename src/{features/auth => }/hooks/use-register-form.ts (92%) rename src/{features/auth => }/hooks/use-send-sms-code.ts (96%) rename src/{features/game => }/hooks/use-topup-vm.ts (100%) rename src/{features/game => }/hooks/use-wallet-records-vm.ts (98%) rename src/{features/game => }/hooks/use-withdraw-submit.ts (93%) rename src/{features/game => }/hooks/use-withdraw-vm.ts (98%) rename src/{features/auth => }/hooks/zod-form-resolver.ts (100%) create mode 100644 src/lib/auth/auth-normalizers.ts rename src/{features/game/entry/entry-page.tsx => main/main-entry-page.tsx} (63%) rename src/{features/game/entry => main}/mobile-entry.tsx (80%) rename src/{features/game/entry => main}/pc-entry.tsx (88%) rename src/{features/game => }/modal/desktop/desktop-auto-setting-modal.tsx (100%) rename src/{features/game => }/modal/desktop/desktop-finance-records-tab.tsx (98%) rename src/{features/game => }/modal/desktop/desktop-language-modal.tsx (98%) rename src/{features/game => }/modal/desktop/desktop-login-modal.tsx (97%) rename src/{features/game => }/modal/desktop/desktop-notice-modal.tsx (99%) rename src/{features/game => }/modal/desktop/desktop-period-history-drawer.tsx (99%) rename src/{features/game => }/modal/desktop/desktop-procedures-modal.tsx (100%) rename src/{features/game => }/modal/desktop/desktop-register-modal.tsx (96%) rename src/{features/game => }/modal/desktop/desktop-rules-modal.tsx (100%) rename src/{features/game => }/modal/desktop/desktop-support-modal.tsx (100%) rename src/{features/game => }/modal/desktop/desktop-userInfo-modal.tsx (97%) rename src/{features/game => }/modal/desktop/desktop-wallet-records-tab.tsx (98%) rename src/{features/game => }/modal/desktop/desktop-withdraw-topup-modal.tsx (100%) create mode 100644 src/modal/mobile/mobile-auto-setting-modal.tsx create mode 100644 src/modal/mobile/mobile-finance-records-tab.tsx create mode 100644 src/modal/mobile/mobile-language-modal.tsx create mode 100644 src/modal/mobile/mobile-login-modal.tsx create mode 100644 src/modal/mobile/mobile-notice-modal.tsx create mode 100644 src/modal/mobile/mobile-period-history-drawer.tsx create mode 100644 src/modal/mobile/mobile-procedures-modal.tsx create mode 100644 src/modal/mobile/mobile-register-modal.tsx create mode 100644 src/modal/mobile/mobile-rules-modal.tsx create mode 100644 src/modal/mobile/mobile-support-modal.tsx create mode 100644 src/modal/mobile/mobile-userInfo-modal.tsx create mode 100644 src/modal/mobile/mobile-wallet-records-tab.tsx create mode 100644 src/modal/mobile/mobile-withdraw-topup-modal.tsx rename src/{features/auth => }/schema/auth-schema.ts (89%) rename src/{features/game/api/finance-types.ts => type/api.type.ts} (94%) rename src/{features/auth/api/types.ts => type/auth.type.ts} (53%) create mode 100644 src/type/game.type.ts create mode 100644 src/type/system.type.ts diff --git a/AGENTS.md b/AGENTS.md index e4b2b30..cd23de4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **36-character-flower** (2639 symbols, 5033 relationships, 226 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **36-character-flower** (3087 symbols, 5931 relationships, 265 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/CLAUDE.md b/CLAUDE.md index e4b2b30..cd23de4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **36-character-flower** (2639 symbols, 5033 relationships, 226 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **36-character-flower** (3087 symbols, 5931 relationships, 265 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/docs/36字花-useGameBoardVm-数据层实施说明.md b/docs/36字花-useGameBoardVm-数据层实施说明.md deleted file mode 100644 index bca81b1..0000000 --- a/docs/36字花-useGameBoardVm-数据层实施说明.md +++ /dev/null @@ -1,248 +0,0 @@ -# 36字花 useGameBoardVm 数据层实施说明 - -## 1. 目标 - -本说明只服务下一步开发:实现 `useGameBoardVm.ts`。 - -当前目标很单一: - -- 让桌面选号盘从 store 读取真实业务数据 -- 让点击格子时真正触发下注动作 -- 不在 [desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx) 内写业务逻辑 -- 不同时改造 mobile - -本阶段不做: - -- 不改造 mobile -- 不新增 `game-ui-store` -- 不重写 `DesktopAnimal` 整个展示结构 -- 不处理 auto-spin / modal - ---- - -## 2. 相关文件 - -本次只围绕以下文件展开: - -- [src/features/game/components/desktop/desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx) -- [src/features/game/entry/pc-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/pc-entry.tsx) -- [src/store/game/game-round-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-round-store.ts) -- [src/features/game/shared/selectors.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/selectors.ts) - -将新增: - -- `src/features/game/hooks/use-game-board-vm.ts` - ---- - -## 3. 当前问题 - -当前 [pc-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/pc-entry.tsx) 里虽然已经挂载了 `DesktopAnimal`,但还没有把桌面主玩法接到业务链路。 - -当前状态: - -- `DesktopAnimal` 只是展示组件 -- `DesktopAnimal` 支持 `activeId` 和 `onSelect` -- `PcEntry` 直接渲染 `` -- 点击格子不会写入真实下注状态 - -结果是: - -- 控制栏虽然已经接入了部分业务数据 -- 状态栏、历史区也开始接入 store -- 但桌面最核心的“点击动物下注”链路还没有打通 - ---- - -## 4. useGameBoardVm 的职责 - -`useGameBoardVm` 只做 3 件事: - -1. 从 `game-round-store` 读取选号盘需要的业务数据 -2. 组织出桌面选号盘可以直接消费的 view-model -3. 暴露点击格子的业务动作 - -它不负责: - -- 直接渲染 UI -- 做 hover / 动画状态 -- 控制 modal -- 处理移动端布局 - ---- - -## 5. 数据来源 - -`useGameBoardVm` 第一版只从 [game-round-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-round-store.ts) 读取: - -- `cells` -- `round` -- `selections` -- `trends` -- `placeBet` - -可复用的派生逻辑来自 [selectors.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/selectors.ts): - -- `buildGameCellViewModels` - ---- - -## 6. 第一版输出字段 - -第一版不追求一步到位,输出保持最小可用。 - -建议 `useGameBoardVm` 返回: - -```ts -{ - cells: GameCellViewModel[] - activeId: number | null - canPlaceBets: boolean - onCellPress: (cellId: number) => void -} -``` - -### 字段说明 - -#### `cells` - -来源: - -- `buildGameCellViewModels({ cells, round, selections, trends })` - -作用: - -- 给未来第二版 board 组件升级时使用 -- 即使第一版 `DesktopAnimal` 还没完全吃它,也应该先在 hook 里产出来 - -#### `activeId` - -第一版定义: - -- 当前有下注的最后一个格子 id -- 如果没有任何下注,则为 `null` - -作用: - -- 兼容当前 [desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx) 现有接口 -- 因为这个组件当前只支持单个 `activeId`,还不支持多个已选格子 - -#### `canPlaceBets` - -定义: - -- `round.phase === 'betting'` - -作用: - -- 控制点击是否真正触发下注 -- 也为后续 UI 禁用态预留 - -#### `onCellPress` - -定义: - -- 当 `canPlaceBets === true` 时,调用 `placeBet(cellId)` -- 否则不执行 - ---- - -## 7. 第一版实现规则 - -### 7.1 不在 DesktopAnimal 内直接读 store - -[desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx) 必须继续保持展示组件定位。 - -不应该在里面直接写: - -- `useGameRoundStore` -- `placeBet` -- `buildGameCellViewModels` -- `round.phase` 判断 - -### 7.2 业务写在 hook,UI 只接 props - -推荐接线方式: - -在 [pc-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/pc-entry.tsx) 中: - -```tsx -const { activeId, onCellPress } = useGameBoardVm() - - -``` - -### 7.3 第一版先兼容现有 DesktopAnimal 接口 - -当前 `DesktopAnimal` 只支持: - -- `activeId?: number | null` -- `onSelect?: (animalId: number) => void` - -所以第一版不要强行重做它的 props 结构。 - -先兼容现状,把业务链路打通即可。 - ---- - -## 8. 第一版文件改动范围 - -本次改动建议控制在 2 到 3 个文件: - -1. 新增 `src/features/game/hooks/use-game-board-vm.ts` -2. 修改 [pc-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/pc-entry.tsx) -3. 如有必要,微调 [desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx) - -其中第 3 项不是必须,优先争取只改前两个文件。 - ---- - -## 9. 第一版完成标准 - -完成后应满足: - -- `PcEntry` 不再裸挂 `` -- `PcEntry` 通过 `useGameBoardVm` 把选号盘接到 store -- 点击格子会真实调用下注动作 -- 控制栏中的总下注额会随点击变化 -- `DesktopAnimal` 仍然保持展示组件定位 - ---- - -## 10. 第二版演进方向 - -第一版做完后,下一版再考虑升级 `DesktopAnimal` 接口。 - -当前限制: - -- `DesktopAnimal` 只能高亮一个 `activeId` - -第二版建议升级为: - -```ts -{ - items: GameCellViewModel[] - onSelect: (cellId: number) => void -} -``` - -这样就可以支持: - -- 多个已下注格子同时高亮 -- 依据 `status` 区分 `betting / selected / won / lost` -- PC 和 Mobile 共用同一份 board view-model - -但这些都不属于当前这一步。 - ---- - -## 11. 结论 - -当前最正确的操作顺序是: - -1. 先写 `useGameBoardVm.ts` -2. 再在 `pc-entry.tsx` 中接入 -3. 暂时不把业务写进 `desktop-animal.tsx` - -这样可以在最小改动范围内,把桌面端最核心的“点击格子下注”链路打通,并继续保持“数据层在外、UI 层在内”的改造方向。 - diff --git a/docs/36字花-核心玩法与前端规则摘要.md b/docs/36字花-核心玩法与前端规则摘要.md deleted file mode 100644 index c8fe684..0000000 --- a/docs/36字花-核心玩法与前端规则摘要.md +++ /dev/null @@ -1,396 +0,0 @@ -# 36字花核心玩法与前端规则摘要 - -## 1. 项目定位 - -36字花是一个**单期开奖结果、单期号循环运行**的实时开奖类游戏。 - -前端的核心界面是: - -- 36 宫格下注盘 -- 倒计时与当前期状态栏 -- 筹码与确认下注区 -- 开奖历史与走势信息 -- 公告、规则、自动托管、充值提现等外围模块 - -产品运行原则: - -- 全平台共享同一局数据 -- `PC` 与 `Mobile` 只分界面,不分玩法、不分对局 -- 前端必须以服务端状态为准,不能靠本地时间自行判断开奖或封盘 - ---- - -## 2. 核心玩法 - -### 2.1 基本盘面 - -- 游戏共有 **36 个号码格子** -- 每个格子代表一个“字花编号” -- 编号范围为 **1-36** -- 当前前端盘面布局为 **6 x 6** - -相关代码与约束: - -- [src/features/game/shared/constants.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/constants.ts) -- `GAME_GRID_ROWS = 6` -- `GAME_GRID_COLUMNS = 6` -- `GAME_TOTAL_CELLS = 36` - -### 2.2 开奖规则 - -- 每一期只会开出 **1 个中奖号码** -- 开奖号码属于 1-36 中的一个 -- 用户只要下注号码集合中包含该号码,即视为中奖 - -接口口径见: - -- [docs/36字花-移动端接口设计草案.md](/Users/jiaunun/Desktop/36-character-flower/docs/36字花-移动端接口设计草案.md) -- `POST /api/game/placeBet` - -文档原意: - -- 玩家提交的是“号码集合 + 单注金额” -- 系统按“单注金额 × 号码数量”计算本笔总扣款 -- 开奖后只出一个号码 -- 若该号码命中玩家所选集合,则该笔下注中奖 - ---- - -## 3. 下注模型 - -### 3.1 单注结构 - -一笔下注至少包含: - -- `period_no`:下注目标期号 -- `numbers`:本次下注号码集合 -- `single_bet_amount`:单个号码的下注金额 - -说明: - -- `numbers` 是一个号码集合,而不是单个号码 -- 多选号码时,总扣款 = `single_bet_amount × numbers数量` -- 重复号码应去重 - -### 3.2 前端当前数据模型 - -当前前端 store 中的单笔选择数据为: - -- `cellId` -- `chipId` -- `amount` -- `placedAt` -- `source` - -定义见: - -- [src/features/game/shared/types.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/types.ts) -- `BetSelection` - -当前本地 mock 与 store 落地方式是: - -- 每点击一个格子,会追加一条 `selection` -- 每条 `selection` 对应一个格子和一个筹码金额 - -这与接口文档中的“号码集合提交”并不完全一致。 - -因此当前前端可以先采用如下理解: - -- UI 交互阶段:按“逐格选择”记录本地状态 -- 提交到后端阶段:再把本地多个格子聚合成 `numbers` - -这是后续 `confirm bet` 需要承担的转换逻辑。 - ---- - -## 4. 回合状态机 - -### 4.1 当前状态枚举 - -当前项目中定义的回合阶段为: - -- `waiting` -- `betting` -- `locked` -- `revealing` -- `settled` - -定义见: - -- [src/features/game/shared/constants.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/constants.ts) - -接口文档中的状态口径包括: - -- `betting` -- `locked` -- `settling` -- `finished` -- `void` - -说明: - -- 当前前端内部状态和接口文档状态还未完全统一 -- 第一阶段前端可继续沿用内部状态机 -- 后续真实联调时,要补一层接口状态 -> 前端状态的映射 - -### 4.2 各阶段前端规则 - -#### `BETTING` - -- 允许选格子 -- 允许切换筹码 -- 允许清除当前选择 -- 允许重复上一注 -- 允许确认下注 -- 允许开启自动托管 - -#### `LOCKED` - -- 已封盘 -- 前端必须立即停止下注交互 -- 不应等待后端返回后才禁用点击 - -#### `REVEALING / SETTLING` - -- 等待开奖与派彩 -- 展示开奖结果、跑马灯、中奖态 - -#### `SETTLED / FINISHED` - -- 本期结束 -- 准备进入下一轮 -- 历史、走势、余额等应刷新 - -#### `VOID` - -- 本期作废 -- 需要退款待开奖本金 -- 前端要清理本期下注态并正确提示 - ---- - -## 5. 封盘与倒计时规则 - -### 5.1 核心原则 - -- 前端必须以服务端返回的时间和阶段为准 -- 到达封盘时间点时,前端应立即锁盘 -- 即使网络延迟,也不能继续允许下注交互 - -这点在需求文档中是明确要求: - -- [docs/frontend-baseline-requirements.md](/Users/jiaunun/Desktop/36-character-flower/docs/frontend-baseline-requirements.md) - -### 5.2 当前前端倒计时模型 - -当前前端使用: - -- `round.bettingClosesAt` -- `round.revealingAt` -- `round.settledAt` - -并通过: - -- [src/features/game/shared/selectors.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/selectors.ts) -- `getRoundCountdownMs(round)` - -来计算当前倒计时毫秒数。 - -规则如下: - -- `waiting / betting`:倒计时到 `bettingClosesAt` -- `locked / revealing`:倒计时到 `revealingAt` -- 其余:倒计时到 `settledAt` - -### 5.3 Mock 数据口径 - -当前 mock 数据中: - -- 开始后约 18 秒封盘 -- 约 24 秒开奖 -- 约 30 秒结算 - -来源: - -- [src/features/game/shared/mock-data.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/mock-data.ts) - -这只是前端演示节奏,不代表最终真实服务端配置。 - ---- - -## 6. 赔率、金额与筹码规则 - -### 6.1 当前赔率 - -当前 mock 中每个格子赔率固定为: - -- `36` - -来源: - -- [src/features/game/shared/mock-data.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/mock-data.ts) -- `createGameCells()` - -### 6.2 当前默认筹码 - -当前默认筹码面额为: - -- `10` -- `25` -- `50` -- `100` -- `200` -- `500` - -来源: - -- [src/features/game/shared/constants.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/constants.ts) -- `DEFAULT_GAME_CHIP_AMOUNTS` - -### 6.3 下注限制 - -从接口文档与基线需求中可得出以下前端规则: - -- 单次下注号码数量不得超过 `pick_max_number_count` -- 单号码下注金额不得超过 `single_number_max_bet` -- 总下注额不得超过余额 -- 封盘后不能继续下注 - -文档来源: - -- [docs/36字花-移动端接口设计草案.md](/Users/jiaunun/Desktop/36-character-flower/docs/36字花-移动端接口设计草案.md) -- [docs/frontend-baseline-requirements.md](/Users/jiaunun/Desktop/36-character-flower/docs/frontend-baseline-requirements.md) - ---- - -## 7. 中奖、历史与走势 - -### 7.1 历史记录 - -每期开奖后应产生一条历史记录,至少包括: - -- `roundId` -- `winningCellId` -- `settledAt` -- `payoutMultiplier` -- `totalPoolAmount` - -定义见: - -- [src/features/game/shared/types.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/types.ts) -- `HistoryEntry` - -### 7.2 走势 - -前端会基于历史数据派生走势信息,包括: - -- `currentStreak` -- `hitCount` -- `missCount` -- `direction` -- `lastHitRoundId` - -派生逻辑见: - -- [src/features/game/shared/selectors.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/selectors.ts) -- `deriveTrendEntries(history)` - -说明: - -- 走势属于纯派生展示数据 -- 不应由 UI 组件自己重复计算 - ---- - -## 8. 公告、维护与运行开关 - -### 8.1 运行开关 - -接口文档中存在: - -- `runtime_enabled` - -含义: - -- `true`:游戏正常运行 -- `false`:后台维护中,禁止下注 - -规则: - -- 维护中不允许新下注 -- 当前已开盘的一局仍可正常开奖和派彩 -- 前端应禁用下注入口并提示维护状态 - -### 8.2 公告 - -公告是前端大厅的一部分,但不属于主玩法。 - -当前前端已存在公告模型: - -- `AnnouncementState` -- `AnnouncementItem` - -它们属于会话层状态,不应混入下注与回合逻辑。 - ---- - -## 9. 自动托管 - -接口文档中已定义自动托管能力: - -- `POST /api/game/autoSpin` - -入参包括: - -- `action` -- `period_no` -- `numbers` -- `single_bet_amount` -- `rounds` - -说明: - -- 自动托管属于建立在主玩法之上的扩展能力 -- 它依赖同一套选号与下注规则 -- 当前前端可以先保留 UI 壳层,不需要在主玩法没走通前抢先落业务 - ---- - -## 10. 前端当前最应该优先走通的主玩法链路 - -基于上述规则,当前前端最核心、最应该优先走通的是: - -1. 状态栏拿到当前期状态与倒计时 -2. 控制栏拿到当前筹码与总下注额 -3. 选号盘点击格子后写入本地下注选择 -4. 总下注额、选中数量、选号高亮联动刷新 -5. 后续再补确认下注请求与开奖结果回写 - -这也是为什么当前下一步应优先实现: - -- `useGameBoardVm` - -而不是优先改 `mobile` 或外围弹窗。 - ---- - -## 11. 总结 - -36字花这个项目的核心,不是“36 张图摆出来”,而是下面这条实时对局链路: - -- 同一局 -- 同一倒计时 -- 同一开奖结果 -- 36 格可选号码 -- 用户用统一筹码模型下注 -- 封盘、开奖、派奖按服务端状态推进 - -前端实现时必须坚持两点: - -1. **玩法规则统一** - - PC 和 Mobile 只能换壳,不能换规则 - -2. **服务端状态优先** - - 前端可以先做本地交互反馈 - - 但回合状态、封盘、开奖、派彩都必须最终以服务端为准 - diff --git a/docs/36字花-游戏模块数据与界面分层第一阶段实施稿.md b/docs/36字花-游戏模块数据与界面分层第一阶段实施稿.md deleted file mode 100644 index 98caabb..0000000 --- a/docs/36字花-游戏模块数据与界面分层第一阶段实施稿.md +++ /dev/null @@ -1,846 +0,0 @@ -# 36字花游戏模块数据与界面分层第一阶段实施稿 - -## 1. 目标 - -第一阶段的目标不是做一次彻底重构,而是先把当前项目中的“业务数据”和“界面渲染”拆出清晰边界,让同一套数据和交互逻辑可以同时服务 `PC` 界面与 `Mobile` 界面。 - -本阶段只解决以下问题: - -- 一套业务数据,供 `PC` 与 `Mobile` 共同消费 -- 统一业务交互逻辑,避免双端各自维护一份 -- 保持 `PC` 与 `Mobile` 布局、样式、视觉独立 -- 不大范围推翻现有组件,优先在现有代码上增量改造 - -本阶段暂不追求: - -- 所有组件完全抽象成跨端共享组件 -- 一次性去掉所有本地 `useState` -- 一次性重写所有弹窗和表单 -- 大规模目录迁移导致整仓成本过高 - - -## 2. 当前代码现状 - -当前仓库中已经存在一部分良好的基础设施: - -- 数据请求与 DTO 归一化: - - [src/features/game/api/game-api.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/api/game-api.ts) -- 共享领域类型与派生逻辑: - - [src/features/game/shared/types.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/types.ts) - - [src/features/game/shared/selectors.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/selectors.ts) -- 回合与会话状态: - - [src/store/game/game-round-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-round-store.ts) - - [src/store/game/game-session-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-session-store.ts) -- 页面入口已统一在: - - [src/features/game/entry/entry-page.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/entry-page.tsx) - -当前主要问题有 4 个: - -1. `Mobile` 界面直接复用了 `Desktop` 组件 - 例如 [src/features/game/entry/mobile-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/mobile-entry.tsx) 当前直接引用了 `DesktopAnimal`、`DesktopGameHistory`、`DesktopTitle`。这会导致移动端无法形成独立布局层。 - -2. 业务状态和界面状态混杂 - 一些状态是业务性的,例如当前选中筹码、是否允许下注;另一些只是表现性的,例如按钮点击动画、过渡效果。当前这两种状态没有被清晰拆开。 - -3. 组件层直接拼 store 数据或自己维护业务数据 - 例如 [src/features/game/components/desktop/desktop-control.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-control.tsx) 内部自己维护了部分核心控制状态,导致未来 `Mobile` 难以复用。 - -4. 弹窗与 UI 业务状态分散在各组件本地 `useState` - 比如 `auto setting`、`notice`、`user info`、`procedures` 等弹窗,当前都在各自文件内部自管 `open` 或 tab 状态,不利于双端共享和统一调度。 - ---- - -## 3. 第一阶段总体原则 - -第一阶段采用四层结构: - -1. `api / dto / normalize` -2. `store / domain actions` -3. `view-model hooks` -4. `pc ui` 与 `mobile ui` - -每层职责如下: - -### 3.1 API 层 - -职责: - -- 调用后端接口 -- DTO 转前端领域模型 -- 不参与 UI 展示逻辑 - -保留现状: - -- [src/features/game/api/game-api.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/api/game-api.ts) - -### 3.2 Store 层 - -职责: - -- 保存当前页面共享的真实业务状态 -- 提供业务动作 -- 不直接处理视觉布局 - -### 3.3 View-model hooks 层 - -职责: - -- 从 store 中读取数据 -- 聚合派生字段 -- 封装业务事件 -- 返回给 `PC` 与 `Mobile` 可直接渲染的数据结构 - -这层是本次改造的核心。 - -### 3.4 UI 层 - -职责: - -- 只负责布局、视觉、交互表现 -- `PC` 与 `Mobile` 各自独立 -- 不直接拼底层 store - ---- - -## 4. 第一阶段目录改造清单 - -建议目录结构如下。第一阶段以“新增”为主,不要求立刻大规模迁移原文件。 - -```text -src/features/game/ - api/ - game-api.ts - index.ts - types.ts - - shared/ - constants.ts - mock-data.ts - selectors.ts - types.ts - - hooks/ - use-game-header-vm.ts - use-game-status-vm.ts - use-game-board-vm.ts - use-game-history-vm.ts - use-game-control-vm.ts - use-game-announcement-vm.ts - use-game-auto-spin-vm.ts - index.ts - - views/ - pc/ - pc-entry.tsx - mobile/ - mobile-entry.tsx - - components/ - shared/ - game-board.tsx - game-history-list.tsx - game-status-bar.tsx - game-control-panel.tsx - desktop/ - mobile/ - - modal/ - desktop/ - mobile/ -``` - -Store 目录建议补齐成: - -```text -src/store/game/ - game-round-store.ts - game-session-store.ts - game-ui-store.ts - index.ts -``` - -说明: - -- 第一阶段可以继续保留现有 `components/desktop` 与 `entry/` 目录 -- `views/pc` 与 `views/mobile` 可以先作为新目录开始承接之后的容器入口 -- `components/shared` 第一阶段只抽少量共用展示结构,不强行全部收拢 - ---- - -## 5. 第一阶段 Store 设计 - -### 5.1 `game-round-store` - -文件: - -- [src/store/game/game-round-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-round-store.ts) - -该 store 保留为“回合与下注核心业务状态”。 - -#### 现有字段 - -- `cells` -- `chips` -- `history` -- `round` -- `selections` -- `trends` -- `activeChipId` - -#### 现有动作 - -- `hydrateRound` -- `selectChip` -- `placeBet` -- `removeSelectionsForCell` -- `clearSelections` -- `setPhase` -- `syncRound` -- `upsertSelections` - -#### 第一阶段建议新增动作 - -- `setActiveChip(chipId: string)` -- `replaceHistory(history: HistoryEntry[])` -- `replaceTrends(trends: TrendEntry[])` -- `replaceCells(cells: GameCell[])` - -说明: - -- 如果不想增加重复动作,也可以只统一命名 `selectChip` -- 新增 `replaceHistory / replaceTrends / replaceCells` 是为了后续轮询、增量同步更清晰 - -#### 不应该放在这里的内容 - -- 各类 modal 的打开关闭 -- 用户信息 tab -- auto-spin 面板设置 -- 登录注册表单 -- 按钮 hover / 点击动画态 - ---- - -### 5.2 `game-session-store` - -文件: - -- [src/store/game/game-session-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-session-store.ts) - -该 store 保留为“会话、连接、公告、面板摘要信息”。 - -#### 现有字段 - -- `announcements` -- `connection` -- `dashboard` - -#### 现有动作 - -- `hydrateSession` -- `dismissAnnouncement` -- `markAnnouncementRead` -- `setConnectionLatency` -- `setConnectionStatus` -- `syncConnection` -- `syncDashboard` - -#### 第一阶段建议新增字段 - -- `serverTimeIso: string | null` -- `lastBootstrapAt: string | null` - -#### 第一阶段建议新增动作 - -- `setServerTime(serverTimeIso: string)` -- `setLastBootstrapAt(iso: string)` - -说明: - -- [src/features/game/components/desktop/desktop-header.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-header.tsx) 当前展示系统时间,后续建议统一从这里拿 -- `lastBootstrapAt` 方便后续做重连、刷新、数据同步判断 - ---- - -### 5.3 新增 `game-ui-store` - -新文件建议: - -- `src/store/game/game-ui-store.ts` - -该 store 专门管理“共享的 UI 业务状态”。 - -#### 建议字段 - -- `activeModal: GameModalType | null` -- `modalPayload: Record | null` -- `selectedUserInfoTab: 'profile' | 'message'` -- `isBgmEnabled: boolean` -- `isMuted: boolean` -- `autoSpinEnabled: boolean` -- `autoSpinSettings` - - `stopIfBalanceLowerThan: string` - - `stopIfSingleWinExceeds: string` - - `stopOnAnyJackpot: boolean` -- `authForm` - - `loginAccount: string` - - `loginPassword: string` - - `registerAccount: string` - - `registerPassword: string` - - `registerConfirmPassword: string` -- `proceduresType: 'withdraw' | 'topup' | null` - -#### 建议动作 - -- `openModal(type, payload?)` -- `closeModal()` -- `setSelectedUserInfoTab(tab)` -- `toggleBgm()` -- `setAutoSpinEnabled(enabled)` -- `setAutoSpinSetting(key, value)` -- `setAuthField(field, value)` -- `setProceduresType(type)` -- `resetAuthForm()` -- `resetAutoSpinSettings()` - -#### 第一阶段要接管的现有状态来源 - -- [src/features/game/modal/desktop/desktop-auto-setting-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx) -- [src/features/game/modal/desktop/desktop-notice-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-notice-modal.tsx) -- [src/features/game/modal/desktop/desktop-userInfo-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-userInfo-modal.tsx) -- [src/features/game/modal/desktop/desktop-procedures-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-procedures-modal.tsx) -- [src/features/game/modal/desktop/desktop-login-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-login-modal.tsx) -- [src/features/game/modal/desktop/desktop-register-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-register-modal.tsx) -- [src/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx) - -#### 不要放到 `game-ui-store` 的状态 - -- `confirmClicked` -- `clickedId` -- `hidingId` - -这些状态目前位于 [src/features/game/components/desktop/desktop-control.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-control.tsx),属于纯动画与表现状态,继续留在本地组件即可。 - ---- - -## 6. 第一阶段 Hook 设计 - -本阶段重点不是“先抽 shared 组件”,而是先抽统一的 `view-model hooks`。 - -这些 hook 的目标: - -- 统一封装 store 读取 -- 统一组织派生字段 -- 统一输出 PC/Mobile 都能消费的 props - ---- - -### 6.1 `useGameHeaderVm` - -建议文件: - -- `src/features/game/hooks/use-game-header-vm.ts` - -服务组件: - -- [src/features/game/components/desktop/desktop-header.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-header.tsx) -- 未来的 `mobile-header.tsx` - -#### 建议暴露字段 - -- `latencyMs: number | null` -- `latencyLabel: string` -- `connectionStatus: 'connected' | 'reconnecting' | 'offline'` -- `systemTimeLabel: string` -- `username: string` -- `balanceLabel: string` -- `avatarUrl: string | null` -- `unreadMessageCount: number` -- `isBgmEnabled: boolean` - -#### 建议暴露动作 - -- `onOpenRules()` -- `onOpenMessages()` -- `onToggleBgm()` -- `onOpenProfile()` - -#### 说明 - -- `DesktopHeader` 未来不再直接读取 store 或写死文案来源 -- header 只消费“最终展示值”与“用户点击后的动作” - ---- - -### 6.2 `useGameStatusVm` - -建议文件: - -- `src/features/game/hooks/use-game-status-vm.ts` - -服务组件: - -- [src/features/game/components/desktop/desktop-status.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-status.tsx) - -#### 建议暴露字段 - -- `roundId: string` -- `oddsLabel: string` -- `streakLabel: string` -- `limitLabel: string` -- `countdownSeconds: number` -- `countdownMs: number` -- `phase: RoundPhase` -- `phaseLabel: string` -- `phaseTone: 'open' | 'locked' | 'settled'` -- `acceptingBets: boolean` - -#### 说明 - -- 倒计时展示逻辑、回合阶段文案逻辑统一在这里处理 -- `PC` 和 `Mobile` 不各自重复拼 countdown 与 round phase - ---- - -### 6.3 `useGameBoardVm` - -建议文件: - -- `src/features/game/hooks/use-game-board-vm.ts` - -服务组件: - -- [src/features/game/components/desktop/desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx) -- 未来 `mobile-board.tsx` - -#### 建议暴露字段 - -- `cells` - - `id` - - `imageUrl` - - `status` - - `isSelected` - - `isWinningCell` - - `selectionAmount` - - `selectionCount` - - `hitCount` - - `currentStreak` -- `activeCellId: number | null` -- `canPlaceBets: boolean` -- `totalSelectedCount: number` -- `totalSelectedAmount: number` - -#### 建议暴露动作 - -- `onCellPress(cellId: number)` -- `onCellLongPress?(cellId: number)` -- `onCellClear(cellId: number)` - -#### 说明 - -- 优先基于 [src/features/game/shared/selectors.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/selectors.ts) 中的: - - `buildGameCellViewModels` - - `groupSelectionsByCell` - - `getSelectionTotal` -- `DesktopAnimal` 不应该自己管理业务选择逻辑,只负责渲染 cell grid - ---- - -### 6.4 `useGameHistoryVm` - -建议文件: - -- `src/features/game/hooks/use-game-history-vm.ts` - -服务组件: - -- [src/features/game/components/desktop/desktop-game-history.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-game-history.tsx) -- 未来 `mobile-history.tsx` - -#### 建议暴露字段 - -- `items` - - `orderNo` - - `roundId` - - `winningCellId` - - `betAmountLabel` - - `totalAmountLabel` - - `winAmountLabel` - - `statusLabel` - - `createdAtLabel` -- `isEmpty: boolean` -- `emptyText: string` -- `recentWinningCellIds: number[]` - -#### 说明 - -- 当前 [src/features/game/components/desktop/desktop-game-history.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-game-history.tsx) 里还是本地 fake data,第一阶段应切换为 VM 数据 -- `PC` 与 `Mobile` 历史列表的数据格式统一,只是布局不同 - ---- - -### 6.5 `useGameControlVm` - -建议文件: - -- `src/features/game/hooks/use-game-control-vm.ts` - -服务组件: - -- [src/features/game/components/desktop/desktop-control.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-control.tsx) -- 未来 `mobile-control.tsx` - -#### 建议暴露字段 - -- `chips` - - `id` - - `value` - - `amount` - - `color` - - `isSelected` - - `isDefault` -- `selectedChipId: string | null` -- `selectedChipAmountLabel: string` -- `selectionTotalLabel: string` -- `selectedCountLabel: string` -- `actionButtons` - - `id` - - `label` - - `disabled` -- `canConfirm: boolean` -- `canClear: boolean` -- `canRepeat: boolean` -- `canOpenAutoSpin: boolean` - -#### 建议暴露动作 - -- `onChipSelect(chipId: string)` -- `onAddChip()` -- `onReduceChip()` -- `onClearSelections()` -- `onRepeatLastRound()` -- `onOpenAutoSpin()` -- `onConfirmBet()` - -#### 说明 - -- [src/features/game/components/desktop/desktop-control.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-control.tsx) 内部仍可保留点击动画和短暂视觉反馈 -- 但“当前选中筹码、是否可确认、操作是否禁用”等必须来自 VM,而不是继续散在组件内部 - ---- - -### 6.6 `useGameAnnouncementVm` - -建议文件: - -- `src/features/game/hooks/use-game-announcement-vm.ts` - -服务组件: - -- [src/features/game/components/shared/game-announcement-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/shared/game-announcement-modal.tsx) -- header 消息入口 -- notice modal - -#### 建议暴露字段 - -- `activeAnnouncement: AnnouncementItem | null` -- `visibleAnnouncements: AnnouncementItem[]` -- `unreadCount: number` -- `hasUnread: boolean` -- `isOpen: boolean` - -#### 建议暴露动作 - -- `onOpenAnnouncement(id?: string)` -- `onDismissAnnouncement(id: string)` -- `onMarkRead(id: string)` -- `onCloseAnnouncement()` - -#### 说明 - -- 这样公告逻辑不会散落在 [src/features/game/entry/entry-page.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/entry-page.tsx) 和多个具体 modal 中 - ---- - -### 6.7 `useGameAutoSpinVm` - -建议文件: - -- `src/features/game/hooks/use-game-auto-spin-vm.ts` - -服务组件: - -- [src/features/game/modal/desktop/desktop-auto-setting-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx) -- 未来 `mobile-auto-setting-modal.tsx` - -#### 建议暴露字段 - -- `open: boolean` -- `enabled: boolean` -- `stopIfBalanceLowerThan: string` -- `stopIfSingleWinExceeds: string` -- `stopOnAnyJackpot: boolean` -- `canSubmit: boolean` - -#### 建议暴露动作 - -- `onOpen()` -- `onClose()` -- `onToggleEnabled(enabled: boolean)` -- `onChangeBalanceThreshold(value: string)` -- `onChangeSingleWinThreshold(value: string)` -- `onToggleStopOnJackpot(enabled: boolean)` -- `onSubmit()` - -#### 说明 - -- 这是最适合第一阶段打样的一类场景:数据结构固定、PC/Mobile 表现不同、交互规则相同 - ---- - -## 7. 第一阶段共享展示组件建议 - -本阶段不要一口气把所有 `Desktop` 组件变成跨端共享。 - -建议只优先抽 2 到 3 个 shared 展示组件: - -- `src/features/game/components/shared/game-board.tsx` -- `src/features/game/components/shared/game-history-list.tsx` -- `src/features/game/components/shared/game-status-bar.tsx` - -### 7.1 `game-board` - -职责: - -- 只接收 `cells` -- 只接收 `onCellPress` -- 不直接读 store - -这样: - -- `PC` 可以包一层桌面 grid 样式 -- `Mobile` 可以包一层移动端布局样式 - -### 7.2 `game-history-list` - -职责: - -- 只负责历史列表渲染 -- 不关心历史数据怎么来 - -### 7.3 `game-status-bar` - -职责: - -- 渲染赔率、回合、倒计时、当前状态 -- 由 `useGameStatusVm` 驱动 - -不建议第一阶段就抽 shared 的内容: - -- header -- control panel -- 大多数 modal - -因为这些区域 `PC/Mobile` 差异通常更大,先共用 hook 比先共用视觉组件更稳。 - ---- - -## 8. 第一阶段 UI 层改造边界 - -本阶段只定 3 条规则: - -### 8.1 `EntryPage` 可以直接处理 hydration - -文件: - -- [src/features/game/entry/entry-page.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/entry-page.tsx) - -保留它作为: - -- bootstrap 获取入口 -- store hydrate 入口 -- PC/Mobile 分流入口 - -### 8.2 容器组件可以调用 VM hooks - -例如: - -- `PcEntry` -- `MobileEntry` -- 各 section 容器 -- modal host - -这些组件可以: - -- `useGameBoardVm()` -- `useGameControlVm()` -- `useGameStatusVm()` - -### 8.3 纯展示组件不直接碰 store - -比如: - -- [src/features/game/components/desktop/desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx) -- [src/features/game/components/desktop/desktop-game-history.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-game-history.tsx) -- [src/features/game/components/desktop/desktop-status.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-status.tsx) -- [src/features/game/components/desktop/desktop-header.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-header.tsx) - -未来都应该尽量演进为: - -- 只吃 props -- 不主动拼底层 store - ---- - -## 9. 第一阶段实施顺序 - -建议严格按这个顺序执行,避免改动面过大。 - -### Step 1:新增 UI Store 与 Hooks 目录 - -新增: - -- `src/store/game/game-ui-store.ts` -- `src/features/game/hooks/` - -这一步只建壳子,不急着接业务。 - -### Step 2:实现 `useGameBoardVm` 与 `useGameControlVm` - -这两块是主玩法交互核心,优先收益最大。 - -优先接的现有文件: - -- [src/features/game/components/desktop/desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx) -- [src/features/game/components/desktop/desktop-control.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-control.tsx) - -### Step 3:让 `DesktopAnimal` 只吃 props - -目标: - -- 不直接读 store -- 不自己处理业务选择逻辑 - -保留字段形态: - -- `cells` -- `activeId` -- `onSelect` - -### Step 4:让 `DesktopControl` 业务数据来自 VM - -目标: - -- 当前筹码列表、已选筹码、总下注额等来自 `useGameControlVm` -- 点击动画状态仍然保留本地 - -### Step 5:停止 `Mobile` 直接复用 `Desktop` 组件 - -文件: - -- [src/features/game/entry/mobile-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/mobile-entry.tsx) - -当前问题: - -- 直接 import `DesktopAnimal` -- 直接 import `DesktopGameHistory` -- 直接 import `DesktopTitle` - -第一阶段目标: - -- 改成 `MobileBoardSection` -- 改成 `MobileHistorySection` -- 共享 hook,不共享 `Desktop` 布局组件 - -### Step 6:实现 `useGameStatusVm` 与 `useGameHeaderVm` - -接入文件: - -- [src/features/game/components/desktop/desktop-status.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-status.tsx) -- [src/features/game/components/desktop/desktop-header.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-header.tsx) - -### Step 7:实现 `game-ui-store` + `useGameAutoSpinVm` - -先接一个最合适的 modal: - -- [src/features/game/modal/desktop/desktop-auto-setting-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx) - -跑通后,再接: - -- notice -- user info -- procedures -- login/register - ---- - -## 10. 第一阶段优先改造文件清单 - -第一阶段最值得优先改的 6 个文件如下: - -1. [src/features/game/entry/entry-page.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/entry-page.tsx) -2. [src/features/game/entry/mobile-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/mobile-entry.tsx) -3. [src/features/game/components/desktop/desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx) -4. [src/features/game/components/desktop/desktop-control.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-control.tsx) -5. [src/features/game/components/desktop/desktop-status.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-status.tsx) -6. [src/features/game/modal/desktop/desktop-auto-setting-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx) - -原因: - -- 它们覆盖了主玩法、主状态、移动端入口和共享 modal 模式 -- 先把这 6 个改通,数据与界面分层的骨架就建立起来了 - ---- - -## 11. 第一阶段完成后的验收标准 - -做到以下几点,即可视为第一阶段完成: - -- `PC` 与 `Mobile` 都不直接拼 API / store 的底层结构 -- `Mobile` 不再直接 import `DesktopXxx` -- 至少 `board / control / status` 这 3 类区域中的 2 类已经改为 “VM 驱动 + 展示组件消费 props” -- modal 开关与 tab 这类共享 UI 业务状态已进入 `game-ui-store` -- 视觉动画状态与业务状态分层清晰 - ---- - -## 12. 风险与边界说明 - -### 12.1 不要在第一阶段做的事情 - -- 不要一次性把所有桌面组件都改成共享组件 -- 不要同时把全部 modal 状态都收进 store -- 不要同时重做 API 调用方式 -- 不要试图在第一阶段解决所有命名和目录历史包袱 - -### 12.2 第一阶段最容易犯的错误 - -- 把所有状态都塞进 store - 结果动画、hover、点击反馈这些纯表现状态也被全局化,反而变复杂 - -- 先抽 shared UI,而不是先抽 VM - 这样容易做出一堆仍然耦合 store 的“伪共享组件” - -- Mobile 继续复用 Desktop 组件 - 这会让后面任何布局调整都越来越痛苦 - ---- - -## 13. 结论 - -本项目第一阶段的正确方向不是: - -- “PC 一套状态,Mobile 一套状态” - -而是: - -- 一套 `domain state` -- 一套 `domain actions` -- 一套 `view-model hooks` -- 两套 `presentation` - -按本实施稿推进后,后续才适合继续做: - -- 第二阶段:shared UI 组件整理 -- 第三阶段:modal host 统一 -- 第四阶段:实时数据同步、轮询或 websocket 接入 - diff --git a/docs/36字花-移动端接口设计草案.md b/docs/36字花-移动端接口设计草案.md deleted file mode 100644 index 4b48442..0000000 --- a/docs/36字花-移动端接口设计草案.md +++ /dev/null @@ -1,824 +0,0 @@ -# 36字花移动端接口设计草案(V1) - -本文基于 `docs/36字花-数据库与实施计划.md` 与 PRD,先给出移动端可对接的接口清单与字段初设。 -口径遵循:**全平台单期号、单开奖结果**;渠道仅用于归属、分润与风控,不拆分对局。 - -**补充(2026-04)**:§**1.5** 描述服务端 **Redis 热点缓存**(`GameHotDataRedis`),**不改变**各接口 URL、参数与响应字段约定,仅供联调与运维对照。 - -## 1. 设计约定 - -### 1.1 基础约定 -- 协议:HTTPS + JSON -- 接口命名规范:`/api/{module}/{action}`,且必须满足正则 `^/api/[a-z]+/[a-z]+[A-Z][a-zA-Z]*$` -- **请求方法**:所有移动端业务接口(`/api/*`,不含 `/api/v1/authToken`)一律使用 `POST` 调用;查询类接口同时兼容 `GET`(便于浏览器/调试工具直接访问),客户端统一走 `POST` - - `POST` 时请求头 `Content-Type: application/json`,参数放在 JSON body - - `GET` 兼容模式下,参数走 URL query string - - **例外**:公告模块 `/api/notice/noticeList`、`/api/notice/noticeDetail`、`/api/notice/noticeConfirm` 与模拟收银台页 `/api/finance/depositMockPayPage` **仅支持 `GET`**,参数一律走 URL query string - - 鉴权类接口 `/api/v1/authToken` 仍为 `GET` -- 时间:UTC 时间戳(秒) + 服务端时区配置 -- 金额:数字传输(如 `"100.00"`),客户端展示统一保留两位小数(存储仍为 `decimal(18,2)`) -- 幂等:关键写接口要求 `idempotency_key` -- 请求头(必带): - - `auth-token`:通过 `GET /api/v1/authToken` 获取的接口鉴权令牌(含义:接口访问的签名鉴权凭证) - - `user-token`:用户登录态令牌;需要登录的接口必带 -- 语言请求头: - - `lang=zh`:返回中文(默认) - - `lang=en`:返回英文 - -### 1.2 通用响应结构 -```json -{ - "code": 1, - "message": "ok", - "data": {} -} -``` - -- `code=1` 表示成功,非 1 为业务错误 -- `/api/*` 所有接口返回文案支持中英双语:默认中文;请求头 `lang=en` 返回英文,`lang=zh` 返回中文 -- 建议错误码段(按错误性质): - - `1000-1099`:参数错误(字段缺失、类型错误、格式错误、超范围) - - `1100-1199`:鉴权错误(未登录、token 失效、权限不足) - - `2000-2999`:业务错误(余额不足、对局不存在、订单不存在、公告不存在) - - `3000-3099`:流程错误(非法流程/状态不允许,如封盘后下注、重复确认、状态跃迁非法) - - `5000-5999`:系统错误(服务异常、依赖超时、未知错误) - -- 推荐基础错误码(首版): -- `1`:成功 - - `1001`:参数缺失 - - `1002`:参数格式错误 - - `1003`:参数取值非法 - - `1101`:未登录或登录已过期 - - `1103`:无权限操作 - - `2001`:余额不足 - - `2002`:对局不存在 - - `2003`:订单不存在 - - `2004`:公告不存在 - - `3001`:当前流程不允许该操作 - - `3002`:已封盘,禁止下注 - - `3003`:重复请求(幂等冲突) - - `5000`:系统繁忙,请稍后重试 - -### 1.3 鉴权方式 -- **接口鉴权(auth-token)**:所有移动端业务接口请求时必须携带请求头 `auth-token`(由 `/api/v1/authToken` 签发) -- **用户登录鉴权(user-token)**:需要登录的接口携带请求头 `user-token`;token 失效后调用刷新或重新登录 - -### 1.4 获取接口鉴权 Token(auth-token) -- **GET** `/api/v1/authToken` -- 用途:获取 `auth-token`(所有接口请求头必带) - -请求示例: -`/api/v1/authToken?secret=564d14asdasd113e46542asd6das1a2a×tamp=1776331077&device_id=1&signature=AD84C49880896DBC16C59C7B122D1FF7` - -请求参数: -- `secret`:string(含义:客户端密钥;服务端从环境变量 `AUTH_TOKEN_SECRET` 校验) -- `timestamp`:int(含义:请求时间戳;服务端允许与服务器时间误差 ±300 秒) -- `device_id`:string(含义:设备码) -- `signature`:string(含义:签名值) - -签名算法: -- 取参与签名的参数(不含 `signature`):`device_id`、`secret`、`timestamp` -- 按参数名 **a-z** 排序后拼接为字符串:`key=value&key=value...` -- 计算:`signature = strtoupper(md5(拼接字符串))` - -返回参数: -- `auth_token`:string(含义:接口鉴权 token;放到请求头 `auth-token`) -- `expires_in`:int(含义:有效期秒数) -- `server_time`:int(含义:服务器时间戳,用于校时) - -可能错误码: -- `1001` 参数缺失 -- `1002` 参数格式错误 -- `1103` 密钥无效/签名错误 -- `3001` 时间戳无效 - -### 1.5 服务端性能与 Redis 热点缓存(实现说明) - -> **对客户端无契约变更**:请求路径、参数、响应 JSON 形状与错误码均不因缓存而改变;本节仅说明服务端如何降延迟、读路径与一致性注意点。 - -**与「框架文件缓存」的区别** - -| 配置 | 作用域 | -|------|--------| -| `CACHE_DRIVER`(`config/cache.php`,如 `file`) | Think-ORM / `get_sys_config()` 等**系统参数表 `config`** 的模型缓存,落盘在 `runtime/cache`,**不参与**本游戏业务热点路径。 | -| `GAME_HOT_CACHE_*`(`config/game_hot_cache.php`) | 游戏侧 **`user` / `game_config` / `game_record`** 行级 JSON 缓存,走 **`support\Redis`**(`config/redis.php` 连接),键前缀 `dfw:v1:`。 | - -**服务端缓存覆盖(与移动端直接相关的读路径)** - -- **用户**:会员鉴权优先读 Redis 中的 `user` 行快照,未命中再查库并回填。**余额、连胜、打码量等变更**落库后,统一经 **`GameHotDataCoordinator::afterUserCommitted($userId)`**:先 **`GameHotDataRedis::userReplaceCacheFromDb`** 与 DB 对齐,再向 Redis 写队列投递幂等刷新任务(见 `GameHotDataWriteQueue` / `GameHotDataQueueConsumer`),用于削峰而非替代同步回源。 -- **游戏配置**:`game_config` 按 `config_key` 缓存。后台直连 `Db` 更新时须 **`GameHotDataCoordinator::afterGameConfigKeyCommitted($key)`**(模型 `GameConfig` 事件与独立表单控制器已接入);独立保存接口在写入前对同一 `config_key` 使用 **`GameHotDataLock`(`TYPE_GAME_CONFIG`)** 互斥。勿仅删除缓存键而不回源,否则最长不一致窗口为 TTL。 -- **对局**:当前活跃局、按 `id` 的局、最新一条 `game_record` 等;写库后经 **`GameHotDataCoordinator::afterGameRecordCommitted`** 同步刷新相关 Redis 键并入队。开奖/封盘等路径另可按记录 id 使用 **`GameHotDataLock`(`TYPE_GAME_RECORD`)** 串行化。 - -**环境变量(示例见仓库根目录 `.env-example`)** - -- `GAME_HOT_CACHE_ENABLED`:是否启用上述 Redis 热点缓存(`false` 时全程回退数据库)。 -- `GAME_HOT_CACHE_TTL_GAME_CONFIG` / `GAME_HOT_CACHE_TTL_GAME_RECORD` / `GAME_HOT_CACHE_TTL_USER`:各类缓存 TTL(秒);**以写后同步回源为主**,TTL 仅作兜底。 -- `GAME_HOT_CACHE_ENABLE_WRITE_QUEUE` 及队列长度、消费进程间隔等:控制写库后的**幂等刷新任务**是否入队及背压策略(见 `config/game_hot_cache.php`)。 - -**一致性提示(联调/测试)** - -- 任何绕过协调入口、只改 DB 不调用 **`GameHotDataCoordinator`** 的手工脚本,都可能与 Redis 短期不一致;生产环境应避免。 -- **`POST /api/game/betPlace`** 扣款路径使用与后台钱包加减点相同的 **用户维度 Redis 锁**(`GameHotDataRedis::userAdminMutationLockTry`)及 **`WHERE coin = ?` 条件更新**,与并发派彩/后台调账互斥;失败时返回 **§4.2** 所列中文说明。 -- 客户端仍可按 **§3.2 `dictionaryList` 的 `version`** 做本地缓存;服务端字典另有 Redis 加速,二者可同时存在。 - ---- - -## 2. 认证与账户模块(user) - -### 2.1 注册 -- **POST** `/api/user/register` -- 用途:仅手机号注册并绑定邀请归属(admin/channel) - -请求参数: -- `username`:string,手机号(含义:注册账号,仅支持大陆手机号) -- `password`:string,明文经 HTTPS 传输(含义:登录密码,服务端需加盐哈希存储) -- `invite_code`:string,必填(含义:子代理邀请码,用于绑定渠道 `channel_id` 与归属) -- `device_id`:string,可选(含义:设备标识,用于风控与登录记录) - -返回参数: -- `user-token`:string(含义:后续接口登录态令牌;用于需要登录的接口请求头) -- `refresh_token`:string,可选(含义:用于刷新访问令牌) -- `expires_in`:int(秒,含义:令牌有效期) -- `user`:object(仅返回非私密信息,不返回 `id`) - - `uuid`:string(含义:用户对外唯一标识,10 位) - - `username`:string(含义:用户昵称/展示名) - - `coin`:string(含义:当前余额) - - `channel_id`:int(含义:归属渠道 ID) - - `risk_flags`:int(含义:风控状态位) - -### 2.2 登录 -- **POST** `/api/user/login` - -请求参数: -- `username`:string(含义:登录账号,当前支持手机号) -- `password`:string(含义:登录密码) -- `device_id`:string,可选(含义:设备标识,辅助风控) - -返回参数: -- `user-token`:string(含义:访问令牌;用于需要登录的接口请求头) -- `refresh_token`:string,可选(含义:用于刷新访问令牌) -- `expires_in`:int(含义:访问令牌剩余有效秒数) -- `user`:object(仅返回非私密信息,不返回 `id`) - - `uuid`:string(含义:用户对外唯一标识,10 位) - - `username`:string(含义:用户昵称/展示名) - - `coin`:string(含义:当前余额) - - `channel_id`:int(含义:归属渠道 ID) - - `risk_flags`:int(含义:风控状态位) - -### 2.3 获取当前用户信息 -- **POST** `/api/user/profile` - -返回参数(金额类字段统一 2 位小数字符串,与钱包展示口径一致): - -**基础档案** -- `uuid`:string(含义:用户对外唯一标识,10 位) -- `username`:string(含义:昵称) -- `head_image`:string(含义:头像地址) -- `phone`:string(含义:手机号) -- `email`:string(含义:邮箱) -- `register_invite_code`:string(含义:注册邀请码快照) -- `channel_id`:int(含义:归属渠道 ID) -- `risk_flags`:int(含义:风控状态位) -- `current_streak`:int(含义:当前连胜次数) -- `last_bet_period_no`:string(含义:最近一笔有效下注所在期号) -- `create_time`:int(含义:注册时间戳) - -**资金与提现配额** -- `coin` / `coin_balance`:string(含义:当前余额;两字段同值) -- `frozen_balance`:string(含义:冻结余额;无冻结场景,固定 `0.00`) -- `total_deposit_coin`:string(含义:累计充值) -- `total_withdraw_coin`:string(含义:累计提现;受理后累加) -- `bet_flow_coin`:string(含义:打码量/流水;开奖结算后按每注 `total_amount` 1:1 累加) -- `max_withdrawable`:string(含义:**当前允许发起的单笔最大提现金额** = `min(coin_balance, max_withdraw_by_flow)`) -- `withdraw_flow`:object(含义:打码量 / 提现配额诊断快照,此处额外附 `pending_withdraw`) - - `ratio`:string(打码量倍数;`0` 表示不限打码) - - `net_deposit`:string(净充值 = max(0, 累计充值 − 累计提现)) - - `required_bet_flow`:string(按门槛口径所需打码量,纯展示) - - `remaining_bet_flow`:string(按门槛口径还差多少打码量,纯展示) - - `eligible`:bool(是否满足整体门槛,纯展示;真正放行以 `max_withdrawable` 为准) - - `max_withdraw_by_flow`:string/null(仅按打码量折算的上限;`ratio=0` 时为 `null`) - - `flow_unlimited`:bool(是否处于"不限打码"状态) - - `pending_withdraw`:object - - `count`:int(当前待审核提现订单数) - - `max`:int(单用户最多允许的待审核提现数,当前为 `3`;超过 `withdrawCreate` 返回 `code=2004`) - -### 2.4 刷新令牌(可选) -- **POST** `/api/user/refreshToken` - -请求参数: -- `refresh_token`:string(含义:续签访问令牌的凭证) - -返回参数: -- `user-token`:string(含义:新访问令牌) -- `expires_in`:int(含义:新令牌有效期) - ---- - -## 3. 游戏大厅与字典模块(game/lobby) - -### 3.1 获取首页初始化数据 -- **POST** `/api/game/lobbyInit` -- 用途:一次返回本局、配置、36字花字典、用户快捷展示 - -返回参数: -- `server_time`:int(含义:服务端当前时间,用于客户端校时) -- `runtime_enabled`:bool(含义:**游戏运行开关**;`false` 时表示后台维护——**禁止下注**,且 idle 时不会自动创建新期、派彩结束后也不会自动创建下一期;**当前已开盘的局仍会开奖、派彩并结算**。移动端应禁用下注入口并提示「维护」类文案) -- `period`:object - - `period_no`:string(含义:当前全局期号) - - `status`:string(`betting`/`locked`/`settling`/`finished`/`void`,含义:当前期状态;`void` 表示该期已作废) - - `countdown`:int(含义:当前期倒计时秒数) - - `lock_at`:int(含义:封盘时间戳) - - `open_at`:int(含义:预计开奖时间戳) -- `bet_config`:object - - `pick_max_number_count`:int(含义:单注最多可选号码数,来自 `game_config.config_key = pick_max_number_count`,缺省与库内种子一致,通常为 10,合法范围 1–36) - - `chips`:array[string](如 `["1.00","5.00"]`,含义:快捷筹码面额) - - `single_number_max_bet`:string(含义:单号码最大下注额) -- `dictionary`:array - - `number`:int(1-36,含义:字花编号) - - `name`:string(含义:字花名称) - - `category`:string(含义:字花分类) - - `icon`:string(含义:图标资源地址) -- `user_snapshot`:object(`coin`、`current_streak`,含义:用户状态快照) - -### 3.2 获取36字花字典(可缓存) -- **POST** `/api/game/dictionaryList` - -返回参数: -- `version`:string(含义:字典版本号,前端可用于缓存比对) -- `items`:同 `dictionary`(含义:36字花字典明细) - -## 4. 下注与对局模块(game/bet) - -### 4.1 获取当前期详情 -- **POST** `/api/game/periodCurrent` - -返回参数: -- `runtime_enabled`:bool(含义:同 `lobbyInit.runtime_enabled`) -- `period_id`:int(含义:当前期主键 ID) -- `period_no`:string(含义:当前期号) -- `status`:string(含义:当前期状态,含 `void` 已作废) -- `countdown`:int(含义:当前期剩余秒数) -- `bet_close_in`:int(含义:距离封盘剩余秒数) -- `result_number`:int/null(未开奖为 null,含义:开奖号码) - -### 4.2 提交下注 -- **POST** `/api/game/placeBet`(兼容旧路径 `/api/game/betPlace`) -- 用途:单期手动下注;玩家传入**压注号码**与**单注金额 `single_bet_amount`**。服务端按 `single_bet_amount × numbers数量` 计算本笔总扣款(落库 `total_amount`),开奖只出一个号码,若该号码 ∈ 所选号码集合即视为中奖。 - -请求参数: -- `period_no`:string(含义:下注目标期号) -- `numbers`:string(含义:本次压注号码集合,**英文逗号分隔**,如 `1,8,16`;每个号码为 1–36 的整数,数量不超过 `pick_max_number_count`(同 `lobbyInit.bet_config`),重复号码会去重) -- `single_bet_amount`:string(含义:**单注金额**,> 0) -- `bet_amount`:string(兼容字段,含义同 `single_bet_amount`) -- `idempotency_key`:string(必填,含义:防止重复下单) - -返回参数: -- `order_no`:string(含义:下注订单号) -- `period_no`:string(含义:实际落单期号) -- `status`:string(`accepted`/`rejected`,含义:受理结果) -- `single_bet_amount`:string(含义:本次单注金额) -- `numbers_count`:int(含义:本次号码数量) -- `locked_balance`:string(可选,含义:冻结金额) -- `balance_after`:string(含义:下单后余额) -- `current_streak`:int(含义:下单后连胜快照) - -**可能错误码(补充)**(其余见文档头部错误码分段;扣款与缓存一致性强相关): - -- `3001`:游戏已暂停(`runtime_enabled=false`,后台「游戏实时对局」关闭运行开关或作废本局后未重新开启;与非法流程类错误同段) -- `5000`:系统繁忙;或 **用户 Redis 互斥锁**未获取(与后台钱包/并发写同一用户串行,文案与后台一致:「该用户正在被其他管理员操作(钱包/并发保存),请稍后再试」);或 **`coin` 条件更新**未命中(并发下注/派彩/后台已改余额:「扣款失败:该用户余额已被其他请求修改(如下注、派彩或其他管理员已保存),请刷新后重试」)。 - -### 4.3 自动托管 -- **POST** `/api/game/autoSpin` - -请求参数: -- `action`:string(`start`/`stop`) -- `period_no`:string(`action=start` 时必填) -- `numbers`:string(`action=start` 时必填,英文逗号分隔) -- `single_bet_amount`:string(`action=start` 时必填,支持兼容字段 `bet_amount`) -- `rounds`:int(`action=start` 时必填,>=1) - -返回参数: -- `status`:string(`scheduled`/`stopped`) -- `auto_mode`:bool -- `remaining_rounds`:int(仅 `start` 返回) - -### 4.4 查询我的下注记录(最近1个月) -- **POST** `/api/game/betMyOrders` - -请求参数: -- `page`:int(可选,默认 1) -- `page_size`:int(可选,默认 20) - -返回参数: -- `list`:array - - `order_no`:string(含义:下注订单号) - - `period_no`:string(含义:所属期号) - - `numbers`:array[int](含义:下注号码) - - `bet_amount`:string(含义:本笔整笔压注金额,与 `total_amount` 相同) - - `total_amount`:string(含义:本笔整笔压注金额) - - `result_number`:int/null(含义:开奖号码,未开可空) - - `win_amount`:string(含义:中奖金额) - - `status`:string(含义:订单状态) - - `create_time`:int(含义:下注时间) -- `pagination`:object(`page`、`page_size`、`total`,含义:分页信息) - ---- - -## 5. 钱包与资金模块(wallet/finance) - -### 5.1 余额同步口径(已移除独立摘要接口) -- 已移除 `/api/wallet/balanceSummary`。 -- 余额同步来源调整为: - - 下注返回 `placeBet.balance_after` - - WebSocket 推送 `wallet.changed` - - 充值/提现详情接口(如 `depositDetail` / `withdrawDetail`)作为业务单据维度核对 -- 倍数 `ratio` 由后台「游戏配置 → `withdraw_bet_flow_ratio`」维护,修改后对新请求立即生效。 -- 历史累计类字段(`total_deposit_coin` / `total_withdraw_coin` / `bet_flow_coin`)均为累加语义;若后续审核驳回,回冲逻辑由后台审核流程负责。 - -### 5.2 钱包流水 -- **POST** `/api/wallet/recordList` - -请求参数: -- `page`:int(可选,默认 1) -- `page_size`:int(可选,默认 20) -- `type`:string,可选(含义:流水类型筛选,可选值如下;不传表示查询全部) - - `deposit`:充值入账(充值订单成功后,金额入账到玩家余额) - - `withdraw`:提现出账(提现订单受理/打款后,金额从玩家余额扣除或冻结) - - `bet`:下注扣款(提交下注时从玩家余额扣除的投注金额) - - `payout`:开奖派彩(中奖后系统将奖金入账到玩家余额) - - `adjust`:人工调整(后台管理员加/扣点,对应 `biz_type=admin_credit/admin_deduct`) - - `bet_void`:期次作废退款(后台「游戏实时对局」作废本局时,退回待开奖注单本金) - -返回参数: -- `list`:array - - `record_id`:int(含义:钱包流水 ID) - - `biz_type`:string(含义:业务类型) - - `direction`:int(1入2出,含义:资金方向) - - `amount`:string(含义:本次变动金额) - - `balance_before`:string(含义:变动前余额) - - `balance_after`:string(含义:变动后余额) - - `ref_type`:string(含义:关联业务单类型) - - `ref_id`:string(含义:关联业务单标识) - - `create_time`:int(含义:流水时间) - -补充约定: -- 金额字段(`amount`、`balance_before`、`balance_after` 等)客户端显示统一两位小数。 -- 后台管理员加减点会生成 `biz_type=admin_credit/admin_deduct` 的流水记录,备注默认模板:`后台管理员(操作管理员)加点/扣点100(值)`(示例)。 - -### 5.3 充值档位列表 -- **POST** `/api/finance/depositTierList` - -说明: -- 由后台「配置管理 → 充值档位」维护,存放在 `game_config.deposit_tier`(JSON 数组)。 -- 后台表单中的「支付货币」下拉来源于 `game_config.finance_cashier.currencies`(不再前端硬编码)。 -- 初始化/重建档位时按当前 `finance_cashier` 货币集合生成:**每种货币 6 条档位**(运营可再编辑)。 -- 仅返回启用状态(`status=1`)的档位,按 `sort` 升序;玩家仅能从中选择。 -- 档位仅描述"充值规格",不再包含收款账户;具体收款由第三方支付网关返回的 `pay_url` 引导。 -- **多语言**:后台保存 `title`(中文名)、`title_en`(英文名)、`desc`(中文描述)、`desc_en`(英文描述)。接口返回的 `title` / `desc` 会根据请求头 `lang` 自动适配: - - `lang=zh`(默认):返回 `title` / `desc`,若为空则回退到英文 - - `lang=en`:返回 `title_en` / `desc_en`,若为空则回退到中文 - - 移动端客户端仅看到单一 `title` / `desc`,无需自行判断语言 - -请求参数:无(无需 body 与 query) - -返回参数: -- `list`:list,档位列表;每一项结构: - - `id`:string(含义:档位稳定 ID,创建订单时作为 `tier_id` 原样回传;与 `tier_key` 同值) - - `tier_key`:string(含义:与 `id` 相同,兼容旧字段名) - - `title`:string(含义:档位名称,已按 `lang` 头切换;例如 `lang=en` 下返回 `"Starter Pack"`,`lang=zh` 下返回 `"新手首充礼包"`) - - `currency`:string(含义:标价币种,如 `CNY`) - - `pay_amount`:string(2 位小数,含义:对外标价金额,与业务配置一致;展示用) - - `amount`:string(2 位小数,含义:玩家本次需支付的充值金额) - - `bonus_amount`:string(2 位小数,含义:该档位赠送金额,无赠送为 `0.00`) - - `total_amount`:string(2 位小数,含义:到账总额 = amount + bonus_amount,方便前端直接展示"到账 120") - - `desc`:string(含义:档位描述/活动文案,已按 `lang` 头切换;可空) - - `channels`:array(含义:可用支付渠道列表,用于 `depositCreate` 的 `channel_code`;渠道与档位不再做单独绑定,所有启用渠道自动兼容全部档位) - - 每项:`code`(string,渠道代码,小写,与创建订单时传入的 `channel_code` 一致)、`name`(展示名)、`sort`(排序) - -### 5.3A 获取充值/提现配置 -- **POST** `/api/finance/depositWithdrawConfig` -- 兼容旧接口:`POST /api/finance/cashierConfig`(返回结构一致,建议客户端统一切到 `depositWithdrawConfig`) - -用途: -- 一次性返回充值与提现页面所需配置:货币列表、汇率、可用充值渠道、提现银行、提现限额与文案配置。 - -返回参数: -- `platform_coin_label`:string(平台币名称,按 `lang` 适配) -- `currencies`:array - - `code`:string(货币代码) - - `label`:string(货币展示名,按 `lang` 适配) - - `deposit_coins_per_fiat`:string(充值汇率) - - `withdraw_coins_per_fiat`:string(提现汇率) -- `rates`:array(兼容字段) - - `currency`:string - - `diamonds_per_fiat_unit`:string -- `pay_channels`:array(充值渠道) - - `code`:string(渠道代码) - - `name`:string(展示名) - - `sort`:int(排序) - - `status`:int(启用状态,1=启用) - - `tier_ids`:array(兼容字段;当前固定空数组,表示自动兼容全部充值档位) -- `withdraw`:object - - `banks`:array(提现银行) - - `min_ewallet`:string(电子钱包最低提现) - - `min_bank`:string(银行卡最低提现) - - `rate_hint`:string(汇率提示文案) - - `processing_note`:string(到账提示文案) - - `fee_note`:string(手续费提示文案) - - `rate_mode`:string(`fixed` / `live`) - - `fields`:object(提现表单必填项开关) - -### 5.4 创建充值订单 -- **POST** `/api/finance/depositCreate` -- `Content-Type: application/json`(推荐)、`application/x-www-form-urlencoded` 或 **`multipart/form-data`**(如 Apifox 的 form-data);字段名与下表一致即可,服务端通过统一参数名读取,**不限制**为某一种 body 类型。 - -说明(与真实「创建订单 → 调起三方 → 异步回调」一致): -- **创建单**:`depositCreate` 仅写入 **待支付** 订单(`status=pending`),**不在此请求内入账**。返回体中 `paid=false`,`pay_url` 为 **模拟第三方收银台** 完整 URL(HMAC 防篡改,见下 §5.4.1)。 -- **客户端**:在 WebView/系统浏览器中打开 `pay_url`;用户完成支付后(模拟页为「确认支付」按钮),由服务端 `depositMockNotify` 验签后调用 `DepositSettlement::settle` 入账,并推送 `wallet.changed`;客户端可轮询 `depositDetail` 或依赖推送更新余额。 -- **未来接入真实第三方支付**:将 `pay_url` 与回调 URL 替换为真网关,入账仍**仅**在回调/验签成功路径中调用 `DepositSettlement::settle`(与当前模拟回调一致)。 -- 档位与渠道取自 `depositTierList`:创建订单时须选择返回 `channels` 中某一渠道的 `code` 作为 `channel_code` 传入;服务端会校验档位存在、启用且渠道已启用。 -- **HMAC 密钥**:模拟链路与签名校验使用环境变量 **`DEPOSIT_MOCK_HMAC_KEY`**(或 `config('app.deposit_mock_hmac_key')`);生产环境务必配置,与代码中默认值区分。 -- **并发上限**:同一用户最多同时存在 **3 笔待支付充值单**(`status=0`);超过后创建接口返回 `code=2005`。 -- **超时失效**:充值单创建后 **60 秒内未支付**将自动置为失败(`status=failed`),并在订单备注记录失败原因(`[timeout] unpaid over 60s`)。 -- **定时任务兜底**:服务端进程 `depositOrderExpireTicker` 每 **10 秒**主动扫描超时待支付单,保证即使用户不访问任何充值接口也会准时失效。 - -请求参数(**三者缺一不可**,任一为空或空白即 `code=1001` 参数缺失): -- `tier_id`:string,必填(含义:玩家选择的充值档位 ID,取自 `depositTierList` 的 `id`;也可用同义字段名 `tier_key`) -- `channel_code`:string,必填(含义:支付渠道代码,**小写**;须与所选档位在 `depositTierList` 返回的 `channels[].code` 之一一致,例如默认内置渠道常为 `directpay`) -- `idempotency_key`:string,必填,≤64(含义:客户端生成的唯一键,短时间内同 `idempotency_key` 不会重复下单;建议 UUID。**调试工具中若使用变量,请确保解析后非空**) - -> **常见 1001 原因**:只传了 `tier_id` + `idempotency_key`,**漏传 `channel_code`**。请先调 `depositTierList`,用对应档位下 `channels` 中某项的 `code` 作为 `channel_code`。 - -返回参数: -- `order_no`:string(含义:充值订单号) -- `amount`:string(2 位小数,含义:玩家本次支付的充值金额,与所选档位 `amount` 一致) -- `bonus_amount`:string(2 位小数,含义:本次赠送金额,与所选档位 `bonus_amount` 一致,无赠送为 `0.00`) -- `total_amount`:string(2 位小数,含义:实际入账总额 = amount + bonus_amount) -- `pay_channel`:string(含义:支付通道标识,与请求中选择的 `channel_code` 一致,落库在订单上) -- `paid`:bool(含义:当前单据是否已到账;`true` 表示钱包已入账、`status=paid`;`false` 表示待玩家在第三方支付页面完成支付) -- `pay_url`:string(含义:第三方支付收银台地址;**`paid=false`(待支付)** 时返回**完整 URL**(如 `https://你的域名/api/finance/depositMockPayPage?order_no=...&sign=...`);`paid=true` 时为空串) -- `status`:string(`pending`/`paid`/`failed`,含义:本接口创建成功时为 `pending`,入账完成后为 `paid`) -- `create_time`:int(含义:订单创建时间,秒级时间戳) -- `pay_time`:int(含义:订单到账时间,未到账为 0) - -#### 5.4.1 模拟第三方:收银台页与「异步通知」回调(开发/无真网关时使用) - -- **GET** `/api/finance/depositMockPayPage` - - **Query**:`order_no`(与 `depositCreate` 返回一致)、`sign`(HMAC,与 `pay_url` 中一致;**不要自行拼接,须完整使用 `depositCreate` 返回的 `pay_url` 或同接口再次查询到的地址**) - - 无需 `auth-token` / `user-token`(外跳浏览器使用)。 - - 返回:HTML 页面,用户点击 **「确认支付(模拟成功)」** 即提交到下方 `depositMockNotify`。 - -- **POST** `/api/finance/depositMockNotify` - - **Body**(`application/x-www-form-urlencoded` 或 JSON 均可,字段名一致即可):`order_no`、`sign`(与上页/支付链接一致) - - 无需 `user-token`;`auth-token` 可选(当前实现不校验)。 - - 验签成功后:对 `status=0` 的订单执行入账(`DepositSettlement::settle`,`source=third_party` 语义),并推送 `wallet.changed`。已入账订单**幂等**再调返回当前订单信息。 - - 成功响应:与 `depositCreate` 成功体相同结构(`code=1` + `data` 为统一充值订单结构)。 - -错误码约定: -- `1001`:缺少必填参数(`tier_id`(或 `tier_key`)、`channel_code`、`idempotency_key` 任一未传或为空字符串) -- `1002`:`idempotency_key` 过长,或与其他玩家的订单冲突 -- `1003`:模拟回调/链接参数非法(如 `sign` 与 `order_no` 不匹配)——`depositMockNotify` 与无效支付链接 -- `2000`:订单落库或入账失败(事务回滚后返回原始错误描述) -- `2003`:所选 `tier_id` 不存在、已停用或不在启用列表中 -- `2004`:`channel_code` 未配置或已禁用 -- `2005`:待支付充值单超过上限(`data.max_pending`、`data.pending_count`、`data.expire_seconds`) - -### 5.5 查看充值订单详情 -- **POST** `/api/finance/depositDetail` - -请求参数: -- `order_no`:string,必填(含义:充值订单号) - -返回参数(与 `depositCreate` 统一结构): -- `order_no`:string(含义:充值订单号) -- `amount`:string(2 位小数,含义:本单充值金额) -- `bonus_amount`:string(2 位小数,含义:本单赠送金额,无赠送为 `0.00`) -- `total_amount`:string(2 位小数,含义:入账总额) -- `pay_channel`:string(含义:支付通道标识) -- `paid`:bool(含义:是否已到账) -- `pay_url`:string(含义:第三方支付页面地址,已到账为空串) -- `status`:string(`pending`/`paid`/`failed`) -- `create_time`:int(含义:订单创建时间) -- `pay_time`:int(含义:订单到账时间,未到账为 0) - -### 5.6 查询充值订单列表 -- **POST** `/api/finance/depositList` - -用于我的充值记录页的分页列表;列表含订单状态,到账时间/支付通道等完整字段请再调用 `/api/finance/depositDetail` 获取。 - -请求参数: -- `page`:int,选填,默认 `1`(含义:页码,从 1 开始) -- `page_size`:int,选填,默认 `20`,最大 `100`(含义:每页数量,超出范围回退为 `20`) - -返回参数: -- `list`:array(含义:充值订单列表,按 `id desc` 排序) - - `order_no`:string(含义:充值订单号) - - `amount`:string(2 位小数,含义:本单充值金额) - - `bonus_amount`:string(2 位小数,含义:本单赠送金额,无赠送为 `0.00`) - - `status`:string(含义:订单状态,与 `depositDetail` 一致:`pending`/`paid`/`failed`) -- `pagination`:object(含义:分页信息) - - `page`:int(含义:当前页码) - - `page_size`:int(含义:每页数量) - - `total`:int(含义:总记录数) - -### 5.7 提现申请 -- **POST** `/api/finance/withdrawCreate` - -请求参数: -- `withdraw_coin`:string(含义:申请提现金额,必须 > 0) -- `receive_account`:string(含义:收款账号) -- `receive_type`:string(`bank`/`ewallet`/`crypto`,含义:收款类型) -- `idempotency_key`:string(含义:防重复提交提现) - -返回参数: -- `order_no`:string(含义:提现订单号) -- `status`:string(`pending_review`/`processing`,含义:提现状态) -- `fee_coin`:string(含义:手续费) -- `actual_arrival_coin`:string(含义:实到账金额) -- `risk_review_required`:bool(含义:是否命中人工审核) - -校验顺序(任一失败即返回对应错误码,不再创建订单): -1. 参数完整性与金额合法性(`code=1001`;金额必须为数值且 > 0) -2. **待审核订单数限制**:同一用户 `status=0`(待审核)的 `withdraw_order` 不得超过 3 笔,否则 `code=2004 Too many pending withdraw orders`,`data` 中回传: - - `max_pending`:上限值(当前为 `3`) - - `pending_count`:当前待审核订单数 -3. `coin_balance >= withdraw_coin`,否则 `code=2001 Insufficient balance` -4. **单笔上限校验**:`withdraw_coin <= max_withdrawable`,否则 `code=2002 Withdraw exceeds available bet flow`,`data` 中回传: - - `max_withdrawable`:**当前允许的单笔最大提现金额**(= `min(coin_balance, max_withdraw_by_flow)`,前端据此提示"最大可提现金额为 XXX") - - `coin_balance`、`bet_flow_coin`、`total_withdraw_coin`、`ratio` - - `max_withdraw_by_flow`:仅按打码量折算的上限(= `max(0, bet_flow_coin / ratio - total_withdraw_coin)`);`ratio=0` 时为 `null` -5. 以上全通过后在同一事务内: - - `withdraw_order` 写入:`amount` / `fee`(默认 0.5%) / `actual_amount = amount - fee` / `status=0`(待审核) / `channel_id` 取自用户归属渠道快照。 - - `user` 表原子更新:`coin -= withdraw_coin` 且 `total_withdraw_coin += withdraw_coin`(WHERE `coin >= withdraw_coin` 防止并发超额扣减)。 - - `user_wallet_record` 写入 `biz_type=withdraw`、`direction=2`、`amount=withdraw_coin`、`ref_type=withdraw_order`、`idempotency_key=wd_apply_{order_no}`,代表"冻结"动作。 - -说明(打码量即提现配额模型): -- 单笔最大可提现 `max_withdrawable = min(coin_balance, max_withdraw_by_flow)`;每笔提现按 `withdraw_coin × ratio` 消耗打码配额,已消耗部分累积在 `total_withdraw_coin`。 -- `ratio = 0` 时视为"不限打码",单笔上限仅受 `coin_balance` 约束。 -- 采用"申请即冻结"语义:提现在移动端提交后立即从 `user.coin` 中扣减并写出金流水;后台审核 **拒绝** 时由管理端在同一事务中回冲余额、`total_withdraw_coin` 与流水,不出现"等待审核期间用户还能把这笔钱再下注"的漏洞。 -- 后台审核 **通过** 时不再额外触碰余额;若管理员调整了 `amount` 或 `fee`,按新旧差额再生成一条 `withdraw` / `withdraw_refund` 流水以保持账务平衡,并同步修正 `total_withdraw_coin`。 -- `withdraw_bet_flow_ratio` 由后台「游戏配置」维护,默认 `1.00`,修改后对新请求立即生效。 - -### 5.8 查看提现订单详情 -- **POST** `/api/finance/withdrawDetail` - -请求参数: -- `order_no`:string,必填(含义:提现订单号) - -返回参数: -- `order_no`:string(含义:提现订单号) -- `status`:string(`pending_review`/`approved`/`rejected`,含义:审核状态;`status=3 已打款` 暂未对外暴露,合并到 `approved`) -- `withdraw_coin`:string(含义:申请提现金额,与后台 `withdraw_order.amount` 对齐) -- `fee_coin`:string(含义:手续费,与后台 `withdraw_order.fee` 对齐) -- `actual_arrival_coin`:string(含义:实际到账金额 = 申请金额 - 手续费;后台审核调整后会同步刷新) -- `reject_reason`:string/null(含义:拒绝原因,`status=rejected` 时取自 `withdraw_order.remark`,否则为 `null`) -- `create_time`:int(含义:申请时间) -- `review_time`:int/null(含义:审核时间戳,未审核为 `null`) - -### 5.9 查询提现订单列表 -- **POST** `/api/finance/withdrawList` - -用于我的提现记录页的分页列表;列表含审核/打款状态摘要,手续费、实到账、拒绝原因等请再调用 `/api/finance/withdrawDetail` 获取。 - -请求参数: -- `page`:int,选填,默认 `1`(含义:页码,从 1 开始) -- `page_size`:int,选填,默认 `20`,最大 `100`(含义:每页数量,超出范围回退为 `20`) - -返回参数: -- `list`:array(含义:提现订单列表,按 `id desc` 排序) - - `order_no`:string(含义:提现订单号) - - `amount`:string(2 位小数,含义:申请提现金额,与后台 `withdraw_order.amount` 对齐) - - `status`:string(含义:订单状态,与 `withdrawDetail` 一致:`pending_review`/`approved`/`rejected`;后台已打款 `status=3` 合并为 `approved`) -- `pagination`:object(含义:分页信息) - - `page`:int(含义:当前页码) - - `page_size`:int(含义:每页数量) - - `total`:int(含义:总记录数) - ---- - -## 6. 公告与消息模块(operation/notice) - -### 6.1 拉取公告列表 -- **GET** `/api/notice/noticeList` - -请求参数(query string): -- `page`:int(可选,默认 1) -- `page_size`:int(可选,默认 20) - -返回参数: -- `list`:array - - `notice_id`:int(含义:公告 ID) - - `title`:string(含义:公告标题) - - `notice_type`:string(`silent`/`popout`,含义:公告类型) - - `is_read`:bool(含义:当前用户是否已读) - - `publish_time`:int(含义:发布时间) - -### 6.2 公告详情 -- **GET** `/api/notice/noticeDetail` - -请求参数(query string): -- `id`:int,必填(含义:公告 ID) - -返回参数: -- `notice_id`:int(含义:公告 ID) -- `title`:string(含义:公告标题) -- `content`:string(含义:公告正文) -- `notice_type`:string(含义:公告类型) -- `must_confirm`:bool(含义:是否必须手动确认) -- `publish_time`:int(含义:发布时间) - -### 6.3 强弹窗确认已读 -- **GET** `/api/notice/noticeConfirm` - -请求参数(query string): -- `notice_id`:int(含义:待确认公告 ID) - -返回参数: -- `notice_id`:int(含义:已确认公告 ID) -- `confirmed`:bool(含义:确认结果) -- `confirm_time`:int(含义:确认时间) - ---- - -## 7. WebSocket(H5)与状态同步 - -> 本版本已移除 webman/push 频道模式;H5 前端使用原生 WebSocket 直连,HTTP 轮询仅作为弱网兜底。 - -### 7.1 WebSocket 连接与消息 - -- **连接地址**:由服务端配置下发(后台测试页读取 `H5_WEBSOCKET_URL`) -- **客户端**:浏览器原生 `WebSocket`(`ws://` / `wss://`) -- **连接时携带参数(建议)**: - - URL Query:`token`(用户登录态 user-token)、`auth_token`(接口鉴权)、`device_id`(设备标识)、`lang`(`zh/en`) - - 示例:`wss://ws.example.com/game?token=xxx&auth_token=xxx&device_id=ios_001&lang=zh` -- **连接成功返回(服务端首帧建议)**: - - `event`:`ws.connected` - - `connection_id`:连接唯一标识 - - `server_time`:服务器时间戳(秒) - - `heartbeat_interval`:心跳间隔(秒) -- **连接失败返回(建议)**: - - `event`:`ws.error` - - `code`:错误码(如 `1101` 未登录、`1103` 鉴权失败) - - `message`:错误描述 -- **建议消息**: - - 心跳:`{"action":"ping"}` - - 订阅状态流:`{"action":"subscribe","topics":["period.tick","period.opened"]}` - - 订阅资金流:`{"action":"subscribe","topics":["bet.accepted","wallet.changed"]}` - - 订阅托管流:`{"action":"subscribe","topics":["auto.spin.progress","wallet.changed"]}` - -#### 7.1.1 消息协议字段定义(联调口径) - -- 客户端 -> 服务端: - - `action`:动作名(当前约定 `ping` / `subscribe`) - - `topics`:仅 `subscribe` 时必填,表示要订阅的主题列表(数组) -- 服务端 -> 客户端: - - `event`:事件名(如 `period.tick`、`wallet.changed`、`jackpot.hit`) - - `topic`:所属主题(通常与 `event` 一致;用于前端按主题路由) - - `data`:业务载荷(对象) - - `server_time`:服务端时间戳(秒,倒计时与对时基准) - -#### 7.1.2 订阅行为说明 - -- **仅建立连接不会自动下发全部业务消息**;客户端需要发送 `subscribe` 明确订阅主题。 -- 成功订阅后服务端返回:`{"event":"ws.subscribed","topics":[...]}`。 -- 若未订阅主题,通常只能收到握手首帧(`ws.connected`)和心跳回包(`pong`)。 - -#### 7.1.3 推送频率与触发规则(当前实现) - -- `period.tick`:**每秒一次**(用于倒计时、状态同步)。 -- `admin.live.snapshot`:**每秒一次**(后台实时对局页全量快照)。 -- `period.opened` / `period.payout` / `admin.live.opened`:按开奖流程阶段触发(事件触发型,非固定频率)。 -- `wallet.changed`:仅在余额发生变更时推送(如下注扣款、充值入账、派彩入账)。 -- `jackpot.hit`:**仅在本期存在中大奖命中用户时推送**;无命中不推送。 - -### 7.1A 后台连接方式(管理端联调) - -- 后台菜单:仅保留一个菜单 `连接服务器websocket`,用于统一联调 WebSocket -- 后台连接入口: - - `/admin/test.GameCurrentStatus/wsConfig` -- 后台页面能力: - - 读取 `ws_url`、`connect_tip`、`sample_messages` - - 手动连接/断开 WebSocket - - 手动发送订阅与心跳报文 - - 实时查看服务端返回帧内容(用于联调事件格式) - -### 7.2 HTTP 兜底接口 - -- 本版本已移除以下兜底接口:`/api/game/currentStatus`、`/api/game/periodHistory`、`/api/wallet/balanceSummary`。 -- 状态与余额统一以 WebSocket 推送为主,HTTP 仅保留业务动作/详情查询接口(如 `placeBet`、`depositDetail`、`withdrawDetail`)。 - -### 7.3 一致性规则 - -- 倒计时以服务端下发时间为准,不信任本地时钟累计。 -- 下注成功后以 `placeBet` 返回的 `balance_after` 为准,并等待 `wallet.changed` 同步。 -- WebSocket 断线后立即重连并重新订阅主题,不再依赖 `currentStatus/periodHistory/balanceSummary` 回补。 - ---- - -## 8. 移动端完整调用流程 - -## 8.1 首次进入游戏 -1. `GET /api/v1/authToken?secret=xxx×tamp=xxx&device_id=xxx&signature=xxx` 获取 `auth-token` -2. `POST /api/user/login` 登录(请求头带 `auth-token`) -3. `POST /api/game/lobbyInit` 拉首页初始化(请求头带 `auth-token`) -4. 建立 WebSocket(H5)连接,发送订阅消息监听状态流 -5. 用户下注调用 `POST /api/game/placeBet` -6. 下单后以 `placeBet.balance_after` 与 `wallet.changed` 同步余额 -7. 断线或页面回前台时,重连 WebSocket 并重新订阅主题回补实时状态 - -## 8.2 充值到下注到提现闭环 -1. 拉取档位:`POST /api/finance/depositTierList`(玩家选择一档,并记下该档 `channels[].code`) -2. 创建订单:`POST /api/finance/depositCreate`(`tier_id` + `channel_code` + `idempotency_key`,三者为必填;可用 JSON / form-data / x-www-form-urlencoded) - - 返回 `paid=false`、`status=pending`、**非空 `pay_url`**:客户端在 WebView/浏览器中打开 `pay_url`(`GET /api/finance/depositMockPayPage`);用户在模拟页点击确认后,由 `POST /api/finance/depositMockNotify` 完成入账,或轮询 `depositDetail` / 等 `wallet.changed` 再刷新余额 - - 未来接真实第三方:将 `pay_url` 换为真网关,入账仅在支付平台 **异步通知** 中调用 `DepositSettlement::settle`(与当前 `depositMockNotify` 路径一致) -3. 客户端可选轮询 `POST /api/finance/depositDetail` 兜底确认状态;入账成功后会收到 `wallet.changed` -4. 下注:`POST /api/game/placeBet` -5. 监听余额:`wallet.changed`(或按订单详情接口核对) -6. 查询流水:`POST /api/wallet/recordList` -7. 提现:`POST /api/finance/withdrawCreate`(即时冻结 `user.coin` 与写出 `withdraw` 流水) -> `POST /api/finance/withdrawDetail` - -## 8.3 公告强触达流程 -1. 客户端监听 `notice.popout` -2. 拉取详情 `GET /api/notice/noticeDetail` -3. 用户勾选确认 `GET /api/notice/noticeConfirm?notice_id=...` -4. 未确认前可由前端阻断下注入口 - ---- - -## 9. 游戏时序流程图(WebSocket + HTTP兜底) - -```mermaid -flowchart TD - A[用户登录 /api/user/login] --> B[拉初始化 /api/game/lobbyInit] - B --> C[连接 WebSocket 并订阅主题] - C --> D{0-20秒下注期?} - D -- 是 --> E[提交下注 /api/game/placeBet] - E --> F[等待 wallet.changed 同步余额] - D -- 否 --> G[进入封盘与开奖阶段] - G --> H[服务端算票与开奖] - H --> I[WebSocket 推送状态变化] - I --> J[断线重连并重新订阅] - J --> C -``` - ---- - -## 10. 后台渠道分红比例配置(管理端补充) - -> 本节为管理后台 `/admin/channel`「分配比例」弹窗补充口径,用于便于管理员按角色层级设置二次分红比例。 - -### 10.1 角色组展示规则 - -- 表格列顺序调整为:`角色组层级` -> `负责人` -> `状态` -> `分配比例(%)` -- `角色组层级` 在 `负责人` 前展示,降低识别与分配成本 -- 层级路径使用 `/` 拼接,如:`顶级组 / 运营组 / 一组` -- 同一负责人若存在多个角色组,按多标签展示多条路径 -- 无角色组时显示 `-` - -### 10.2 接口:读取渠道管理员分配配置 - -- **GET** `/admin/channel/channelAdminShareList?id={channel_id}` - -返回参数(`data.list[]`)新增: -- `group_paths`:array(负责人所属角色组层级路径列表) -- `group_paths_text`:string(层级路径拼接文本,`|` 分隔,用于兼容纯文本场景) - -返回示例(节选): -```json -{ - "code": 1, - "message": "ok", - "data": { - "channel_id": 1, - "channel_name": "渠道A", - "list": [ - { - "admin_id": 12, - "username": "zhuguan1", - "group_paths": ["顶级组 / 运营组 / A组"], - "group_paths_text": "顶级组 / 运营组 / A组", - "status": 1, - "share_rate": "30.00" - } - ] - } -} -``` - -### 10.3 保存约束(沿用现有) - -- **POST** `/admin/channel/saveChannelAdminShare` -- 仅 `status=1` 的行参与占比汇总 -- 启用项分配比例总和必须严格等于 `100.00` - ---- - -## 11. 需要你确认的实现口径(进入接口开发前) - -1. **登录方式**:仅账号密码,还是要短信/邮箱验证码? -2. **提现收款类型**:首版只做银行卡,还是同时支持电子钱包/加密地址? -3. **自动托管**:是否首期上线;若不上线可先隐藏 `auto-bet` 接口。 -4. **WebSocket 主题定义**:状态流、资金流、托管流的 topic 与消息体是否按本文固定。 -5. **错误码规范**:是否已有公司统一错误码表;若有需对齐替换本草案码段。 - -确认后可进入下一步:按该文档落地 controller + validate + service + 路由。 diff --git a/docs/frontend-baseline-requirements.md b/docs/frontend-baseline-requirements.md deleted file mode 100644 index 8dd8fc6..0000000 --- a/docs/frontend-baseline-requirements.md +++ /dev/null @@ -1,439 +0,0 @@ -# 《36字花》前端开发基线需求文档 - -## 1. 文档目的 - -本文件基于以下两份前期文档整理: - -- `docs/《_36字花_ 前端开发对接与交互逻辑说明书》.docx` -- `docs/《36字花》用户前端 (User Portal) UI_UX .docx` - -目标是移除营销型表述和非关键修辞,只保留后续 UI 设计、前端开发、联调和验收所需的核心设计要求、交互逻辑和技术栈建议。 - -## 2. 核心结论 - -### 2.1 产品形态 - -- 产品为实时开奖类游戏前端,主场景是移动端 H5,同时要求兼容桌面端响应式布局。 -- 页面核心是围绕单局倒计时循环运行的 36 宫格下注界面。 -- 前端必须以服务器状态为准,不能依赖客户端本地时间做开奖判断。 - -### 2.2 设计方向 - -- 视觉风格保留为深色背景、高对比强调色的娱乐场风格。 -- 颜色建议以深海蓝/黑为底,青色和金色作为强调色。 -- 视觉参考中的“神鼎、宝箱、卷轴、巨龙、赛博朋克”等描述,只作为美术方向参考,不作为开发阻塞项。 -- 所有文案容器必须支持多语言伸缩,禁止固定宽度写死。 - -### 2.3 交付优先级 - -后续设计与开发应按以下优先级执行: - -1. 玩法状态正确 -2. 倒计时与服务端同步正确 -3. 下单、封盘、开奖、派彩状态切换正确 -4. 移动端可用性与性能达标 -5. 动效和视觉强化 - -## 3. 推荐技术栈 - -## 3.1 前端框架 - -- `React + TypeScript + Vite` -- 原因:移动 H5 交互复杂、状态密集、联调频繁,Vite 启动快,React 生态成熟,TypeScript 适合管理复杂状态和接口约束。 - -## 3.2 路由与页面组织 - -- `React Router` -- 页面结构建议按大厅主界面、公告弹窗、规则面板、用户侧滑面板拆分。 - -## 3.3 状态管理 - -- `Zustand` 作为全局业务状态容器 -- `XState` 或等价状态机方案用于维护单局生命周期状态 -- 原因:本项目存在明确的 4 段式回合状态机、局内局外状态切换、自动托管覆盖态、网络重连恢复,单纯依赖局部状态容易错乱。 - -建议状态拆分: - -- `roundStore`:期号、阶段、倒计时、开奖数据 -- `betStore`:选中格子、锁定格子、筹码、总注额、按钮状态 -- `userStore`:余额、连赢状态、下注上限、公告状态 -- `uiStore`:弹窗、抽屉、Toast、动画开关、自动托管蒙层 - -## 3.4 数据请求与实时通信 - -- `TanStack Query`:管理普通 HTTP 请求、缓存与重拉 -- `WebSocket`:接收当前局状态、开奖、余额变动、派彩、自动托管进度 -- 如果后端使用 Socket.IO,则前端改用 `socket.io-client` - -## 3.5 样式与动画 - -- 页面布局:`Tailwind CSS` -- 复杂状态样式:`CSS Modules` 或同级方案 -- 盘面高频动画:优先 `CSS Animation / Transition / SVG` -- 全屏粒子特效:`PixiJS` 或 `tsParticles` - -约束: - -- 禁止用 JS 逐帧操作 DOM 做 36 宫格边框动画 -- 动效必须优先使用合成层友好的属性,如 `transform`、`opacity`、`filter` - -## 3.6 国际化与时间处理 - -- `react-i18next` -- `dayjs` - -说明: - -- `dayjs` 只用于显示格式化,不用于决定回合状态。 -- 回合倒计时必须基于服务端时间戳推导。 - -## 3.7 测试与质量保障 - -- 单元测试:按项目需要自行接入 -- 组件测试:按项目需要自行接入 -- 端到端测试:`Playwright` -- 线上监控:建议接入 `Sentry` - -## 4. 页面与模块范围 - -## 4.1 主游戏界面 - -必须包含以下区域: - -- 顶部导航栏 -- 开奖展示区 -- 中控信息区 -- 36 宫格下注区 -- 筹码与操作栏 -- 右侧历史区 -- 底部单双走势区 - -## 4.2 次级界面 - -必须包含以下弹层或侧栏: - -- 强制公告弹窗 -- 自动托管运行浮层 -- 玩法与规则面板 -- 用户 Dashboard 侧滑面板 - -## 5. 单局生命周期状态机 - -后台可配置单局时长,前端默认按 30 秒一局建模,并支持服务端参数覆盖。 - -### 5.1 游戏阶段枚举 - -- `BETTING`:下注期 -- `LOCKED`:封盘锁定 -- `DRAWING`:算票与开奖 -- `PAYOUT`:派彩与收尾 - -### 5.2 各阶段规则 - -`BETTING` - -- 允许选格子、换筹码、清除未确认、重复上一注、确认下注、开启自动托管。 -- 显示倒计时。 -- 跑马灯处于常规速度。 - -`LOCKED` - -- 到达封盘时间点后,前端必须立即锁盘,不等待后端响应。 -- 所有点击事件、按钮、输入行为全部禁用。 -- 所有 `PRE_SELECTED` 未确认格子必须清空。 -- 显示“停止下注”提示。 - -`DRAWING` - -- 等待 WebSocket 推送开奖结果。 -- 收到开奖后,盘面跑马灯加速。 -- 最终定格中奖格子,进入中奖态。 - -`PAYOUT` - -- 接收余额和连赢状态更新。 -- 播放中奖特效。 -- 更新历史和走势。 -- 为下一局做状态重置。 - -## 6. 36 宫格下注区需求 - -## 6.1 网格结构 - -- 固定为 `6 x 6` -- 每个格子包含:编号、动物名、动物图 -- 格子需要支持轻量分组提示,用于区分业务分类,但不影响下注逻辑 - -## 6.2 单格状态枚举 - -建议统一实现 `CellStatus`: - -- `IDLE`:默认闲置 -- `MARQUEE`:跑马灯焦点 -- `HOVER`:PC 悬浮 -- `PRE_SELECTED`:已选未确认 -- `LOCKED`:已确认下注 -- `DISABLED`:不可操作 -- `ERROR`:错误反馈 -- `WINNING`:中奖高亮 -- `LOSER`:未中奖弱化 -- `AUTO_ACTIVE`:自动托管执行态 - -## 6.3 单格交互规则 - -- 玩家单局最多选择 5 个格子。 -- `PRE_SELECTED + LOCKED` 的总数不得超过 5。 -- 达到 5 个后,其余可选格子全部置为 `DISABLED`。 -- 点击第 6 个格子时,不得选中,必须触发错误反馈。 -- 余额不足或超限点击时,必须触发错误反馈。 -- 已确认格子在本局内不可取消。 -- 若本局尚未封盘且未满 5 个,允许继续追加下注并再次确认。 - -## 7. 筹码与下注逻辑 - -## 7.1 筹码区 - -标准筹码档位: - -- `1` -- `5` -- `10` -- `25` -- `50` -- `100` - -## 7.2 统一下注金额同步 - -- 全局维护 `currentChipValue` -- 已处于 `PRE_SELECTED` 的所有格子,其显示筹码必须跟随 `currentChipValue` 实时同步 -- 筹码切换后,总下注金额必须同步刷新 - -## 7.3 连赢上限与余额限制 - -需要实时校验: - -- `selectedCount * currentChipValue <= streakMaxBetLimit` -- `totalBetAmount <= balance` - -当不满足时: - -- 对应不可选的大额筹码必须禁用 -- 确认下注按钮进入错误态或禁用态 -- 格子点击需给出明确错误反馈 - -## 8. 确认下注主按钮状态机 - -按钮需要独立维护以下 4 个状态: - -- `DISABLED`:未选任何格子 -- `READY`:已选格子且余额足够,可点击提交 -- `ERROR`:总下注金额大于余额,文案显示“余额不足” -- `SUCCESS`:下注成功后维持成功态直到本局结束 - -约束: - -- 禁止自动提交 -- 必须由用户手动点击确认 -- 成功后已确认格子不可撤销 - -## 9. 自动托管需求 - -## 9.1 功能行为 - -- 调用自动托管接口提交:下注格子、金额、局数 -- 前端进入 `AUTO_MODE` -- 主键盘和筹码区整体进入不可编辑状态 -- 自动托管中的目标格子显示 `AUTO_ACTIVE` -- 前端展示当前进度,例如 `12 / 50` -- 必须提供显式“停止托管”操作 - -## 9.2 视觉与交互约束 - -- 使用全局玻璃遮罩阻断手动操作 -- 自动托管目标格子需要穿透遮罩高亮显示 -- 自动托管态必须与手动锁定态有视觉区分 - -## 10. 中控信息区需求 - -必须展示以下信息: - -- 当前余额 -- 当前赔率 -- 当前连赢次数 -- 连赢限额提示 -- 当前倒计时 -- 当前期号 -- 当前阶段状态,如 `OPEN` / `CLOSED` - -倒计时要求: - -- 最后 5 秒需要强化提示 -- 倒计时只展示服务器推导结果 - -## 11. 历史与走势需求 - -## 11.1 右侧历史区 - -- 显示最近开奖记录 -- 每条至少包含时间、号码、动物名 -- 最新一条高亮 -- 需要支持滚动 - -## 11.2 底部单双走势 - -- 保留最近 30 局 -- 奇数显示红色圆点 -- 偶数显示蓝色圆点 -- 新增一条数据时,最后一个点需要入场动画 - -## 12. 弹窗与侧栏需求 - -## 12.1 强制公告弹窗 - -功能要求: - -- 首屏进入时请求公告接口 -- 若存在未读公告,必须强制弹出 -- 不允许点击遮罩关闭 -- 不允许提供右上角关闭按钮 -- 勾选“已阅读并同意”前,进入游戏按钮必须禁用 -- 关闭后要记录已读状态 -- 无新公告时,不重复弹出 - -布局要求: - -- 支持图文内容 -- 内容超长时支持滚动 -- 移动端建议复选框和主按钮上下排列,避免多语言挤压 - -## 12.2 规则面板 - -- 提供玩法规则、赔率说明、连胜机制、大奖说明 -- 允许分页或滚动 -- 结构化展示,不得纯长文堆叠 - -## 12.3 用户 Dashboard 侧滑面板 - -至少包含: - -- 资产信息 -- 充值入口 -- 提现入口与手续费说明 -- 最近 1 个月投注历史 -- 站内信列表 -- 公告信箱入口 - -## 13. 异常与容错要求 - -## 13.1 本地锁盘优先 - -- 到达封盘时间点时,前端必须立刻锁盘 -- 即使网络延迟,也不能继续允许下注交互 - -## 13.2 压秒点击失败处理 - -场景: - -- 用户在接近封盘时点击确认 -- 请求发出,但服务器实际已封盘 - -前端处理: - -- 先进入锁盘和加载态 -- 若稍后收到失败响应,必须撤销未成功下注的本地状态 -- 明确提示“网络延迟,下注失败,未扣款” -- 严禁误显示为下注成功 - -## 13.3 断线重连恢复 - -触发条件: - -- 页面重新可见 -- WebSocket 断开后重连 - -前端处理: - -- 立即调用全量状态接口重新同步 -- 重置倒计时、期号、余额、连赢、走势、当前盘面状态 -- 禁止依赖本地累计时间继续运行 - -## 13.4 余额不足场景 - -- 确认按钮进入错误态 -- 充值入口需要有明显引导 -- 相关格子或筹码点击时给出即时反馈 - -## 14. 接口与事件依赖 - -以下为前端开发所需的最小接口能力,命名可与后端协商,但能力不可缺失。 - -## 14.1 HTTP 接口 - -- `GET /api/user/announcement` -- `GET /api/game/current_status` -- `POST /api/bet/place` -- `POST /api/auto_spin` -- `POST /api/announcement/read` - -## 14.2 WebSocket 事件 - -- `round_status`:当前阶段、期号、服务器时间、剩余时间 -- `draw_result`:开奖结果、中奖格子 -- `balance_changed`:余额变化 -- `streak_changed`:连赢状态与限额变化 -- `trend_updated`:最新走势数据 -- `auto_spin_progress`:自动托管局数进度 - -## 15. 性能与实现约束 - -- 移动端目标帧率:`60 FPS` -- 36 宫格状态切换不得出现明显掉帧 -- 高亮、缩放、呼吸、闪烁等动画优先使用 GPU 友好属性 -- 避免大面积重排和重绘 -- 长列表区域应考虑虚拟化或分段渲染 -- 全局状态更新必须避免引起整盘 36 格不必要重渲染 - -## 16. 多语言与响应式要求 - -- 所有按钮、标签、提示文案必须支持长度扩展 -- 不允许固定像素宽度导致文案截断 -- 文本区域需预留至少 40% 的横向伸缩空间 -- 设计需优先保证移动端单手操作 -- PC 端可补充 Hover 态,移动端不依赖 Hover 完成交互 - -## 17. 开发验收基线 - -满足以下条件才可进入测试或交付: - -- 单局状态机完整跑通 -- 封盘时前端能本地立即锁盘 -- 服务端时间同步准确 -- 下注数量限制与统一筹码机制无误 -- 余额与连赢上限限制生效 -- 自动托管可启动、运行、停止 -- 公告弹窗强阻断逻辑正确 -- 断线重连后状态可恢复 -- 走势与历史区数据更新正确 -- 移动端核心流程可稳定使用 - -## 18. 建议的开发顺序 - -1. 搭建项目骨架、路由、状态层、接口层 -2. 先实现 30 秒回合状态机和服务端时间同步 -3. 完成 36 宫格、筹码区、确认按钮的核心下注流程 -4. 接入开奖、派彩、历史、走势 -5. 实现公告弹窗、规则面板、用户侧栏 -6. 实现自动托管 -7. 最后补齐粒子特效、强化动画和视觉细节 - -## 19. 明确降级为“视觉参考”的内容 - -以下内容不应阻塞前端逻辑开发,可在视觉设计阶段再细化: - -- 神坛主体到底是神鼎、宝箱还是法槌 -- 是否采用卷轴、金属边框或全息科技框 -- Jackpot 动画具体表现形式 -- 动物头像是否 3D、半写实或插画风 -- 特效音、粒子素材、品牌化图标样式 - -以上内容只影响视觉表现,不影响本文件定义的交互和功能边界。 diff --git a/docs/本次代码变更说明.md b/docs/本次代码变更说明.md deleted file mode 100644 index 7e6633d..0000000 --- a/docs/本次代码变更说明.md +++ /dev/null @@ -1,373 +0,0 @@ -# 本次代码变更说明 - -本文档总结当前这一次工作中,对仓库代码所做的主要改动,重点说明: - -- 新增了哪些文件 -- 修改了哪些已有文件 -- 每个模块新增了什么能力 -- 当前已经验证过什么 - -说明: - -- 下文基于当前 Git 工作区中的变更整理 -- 其中一部分文件为“新增文件”,一部分为“在已有文件基础上的修改” -- `src/routeTree.gen.ts` 属于路由生成产物,不是手写业务文件 - -## 1. 本次改动目标 - -本次工作的核心目标,是把项目从通用脚手架推进到“36 字花”游戏前端的业务骨架阶段,采用的落地方案是: - -- 统一业务路由,不按设备拆成不同 URL -- 同一路由下按设备加载移动端 / 桌面端不同视图 -- 共用一套游戏状态、数据模型、mock 数据和接口层 -- 将页面内写死的中英文文案逐步收敛到 `react-i18next` 的语言包里 - -## 2. 新增模块总览 - -本次新增的核心模块有 5 个: - -1. `src/features/game/shared` -说明: -- 定义 36 字花游戏的共享常量、类型、mock 数据、派生计算函数 -- 作为游戏业务层的基础模型 - -2. `src/store` -说明: -- 基于 Zustand 实现游戏的状态容器 -- 按模块拆分目录 -- 当前分为 `src/store/auth` 和 `src/store/game` - -3. `src/features/game/api` -说明: -- 建立游戏相关接口层和 DTO 映射 -- 提供 mock bootstrap 获取函数,便于在未接真实后端前先跑 UI - -4. `src/features/game/components` -说明: -- 新增共享展示组件 -- 同时新增移动端页面壳和桌面端页面壳 - -5. `src/features/game/entry` -说明: -- 增加游戏路由适配页 -- 负责把共享状态、共享组件和双端壳层接起来 - -## 3. 新增文件清单与说明 - -### 3.1 通用组件 - -#### [src/components/language-link.tsx](/Users/jiaunun/Desktop/36-character-flower/src/components/language-link.tsx) -新增内容: -- 抽出语言切换按钮组件 -- 从 `/$lang` 布局文件中拆出,避免路由文件内混入过多内部组件 - -#### [src/components/nav-link.tsx](/Users/jiaunun/Desktop/36-character-flower/src/components/nav-link.tsx) -新增内容: -- 抽出顶部导航按钮组件 -- 供语言布局页复用 - -#### [src/components/stat-card.tsx](/Users/jiaunun/Desktop/36-character-flower/src/components/stat-card.tsx) -新增内容: -- 抽出首页信息卡片组件 -- 用于首页业务入口页展示 - -### 3.2 设备识别 - -#### [src/lib/device/use-device-type.ts](/Users/jiaunun/Desktop/36-character-flower/src/lib/device/use-device-type.ts) -新增内容: -- 基于窗口宽度判断当前设备是 `mobile` 还是 `desktop` -- 为同一路由下渲染不同视图提供支持 - -### 3.3 游戏共享模型 - -#### [src/features/game/shared/constants.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/constants.ts) -新增内容: -- 36 宫格基础尺寸常量 -- 回合阶段枚举 -- 格子状态枚举 -- 连接状态枚举 -- 筹码默认配置等 - -#### [src/features/game/shared/types.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/types.ts) -新增内容: -- 游戏核心类型定义 -- 包括格子、筹码、下注、历史、公告、连接、dashboard、bootstrap 快照等 - -#### [src/features/game/shared/mock-data.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/mock-data.ts) -新增内容: -- 游戏 mock 启动数据 -- 包括 36 个格子、筹码、历史记录、当前回合、公告、连接状态、桌面信息 - -#### [src/features/game/shared/selectors.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/selectors.ts) -新增内容: -- 各种纯函数派生逻辑 -- 包括格子 view model、倒计时、公告筛选、趋势计算、下注汇总等 - -#### [src/features/game/shared/untils.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/index.ts) -新增内容: -- 对 `shared` 子模块统一导出 - -### 3.4 游戏状态层 - -说明: -- 游戏相关 store 已统一放入 `src/store` -- 并进一步按模块拆为子目录: - - `src/store/auth` - - `src/store/game` -- `src/features/game/model/untils.ts` 现在仅作为过渡导出层,用于维持 `features/game` 对外接口不变 - -#### [src/store/game/game-round-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-round-store.ts) -新增内容: -- 回合相关状态 -- 包括格子、筹码、回合阶段、历史、趋势、下注选择 -- 提供切换筹码、下注、清空、同步回合等动作 - -#### [src/store/game/game-session-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-session-store.ts) -新增内容: -- 会话类状态 -- 包括公告、连接状态、dashboard 信息 -- 提供已读公告、关闭公告、同步连接、同步 dashboard 等动作 - -#### [src/store/game/game-ui-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-ui-store.ts) -新增内容: -- UI 控制类状态 -- 当前只放了自动托管浮层、dashboard、规则面板开关 - -#### [src/store/auth/auth-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/auth/auth-store.ts) -说明: -- 认证相关 store -- 原有 `auth-store` 已归入 `auth` 子目录 - -#### [src/store/auth/untils.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/auth/index.ts) -说明: -- 统一导出认证模块 store - -#### [src/store/game/untils.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/index.ts) -说明: -- 统一导出游戏模块 store - -#### [src/store/untils.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/index.ts) -新增内容: -- 统一导出 `src/store` 下各模块 -- 当前包括 `auth` 和 `game` - -#### [src/features/game/model/untils.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/model/index.ts) -新增内容: -- 作为过渡导出层,继续对 `features/game` 暴露游戏 store - -### 3.5 游戏接口层 - -#### [src/features/game/api/types.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/api/types.ts) -新增内容: -- 游戏接口 DTO 类型定义 - -#### [src/features/game/api/game-api.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/api/game-api.ts) -新增内容: -- 游戏 API 包装 -- 包括 bootstrap、round feed、announcement 的响应映射 -- 提供 `getMockGameBootstrap()` 用于当前阶段 UI 接线 - -#### [src/features/game/api/untils.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/api/index.ts) -新增内容: -- 对 `api` 子模块统一导出 - -### 3.6 游戏共享 UI 组件 - -#### [src/features/game/components/shared/game-action-bar.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/shared/game-action-bar.tsx) -新增内容: -- 筹码区、主按钮、次按钮、附加 slot 区域 - -#### [src/features/game/components/shared/game-announcement-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/shared/game-announcement-modal.tsx) -新增内容: -- 强制公告弹层骨架 - -#### [src/features/game/components/shared/game-auto-spin-overlay.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/shared/game-auto-spin-overlay.tsx) -新增内容: -- 自动托管运行遮罩骨架 - -#### [src/features/game/components/shared/game-board-cell.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/shared/game-board-cell.tsx) -新增内容: -- 单个格子组件 -- 支持状态、徽标、倍率、点击等展示 - -#### [src/features/game/components/shared/game-board.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/shared/game-board.tsx) -新增内容: -- 36 宫格容器组件 - -#### [src/features/game/components/shared/game-history-list.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/shared/game-history-list.tsx) -新增内容: -- 开奖历史列表组件 - -#### [src/features/game/components/shared/game-panel-card.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/shared/game-panel-card.tsx) -新增内容: -- 通用游戏面板容器 - -#### [src/features/game/components/shared/game-status-card.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/shared/game-status-card.tsx) -新增内容: -- 顶部状态卡片组件 - -#### [src/features/game/components/shared/game-trend-list.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/shared/game-trend-list.tsx) -新增内容: -- 走势列表组件 - -#### [src/features/game/components/shared/types.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/shared/types.ts) -新增内容: -- 共享展示组件的 props 类型 - -#### [src/features/game/components/shared/utils.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/shared/utils.ts) -新增内容: -- 简单的 `cn` 工具 - -### 3.7 双端页面壳 - -#### [src/features/game/components/mobile/mobile-game-page.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/mobile/mobile-game-page.tsx) -新增内容: -- 移动端游戏页壳层 -- 负责纵向编排 - -#### [src/features/game/components/desktop/desktop-game-page.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-game-page.tsx) -新增内容: -- 桌面端游戏页壳层 -- 负责多栏编排 - -#### [src/features/game/components/untils.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/index.ts) -新增内容: -- 对组件模块统一导出 - -### 3.8 游戏入口与总导出 - -#### [src/features/game/entry/entry-page.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/game-route-page.tsx) -新增内容: -- 游戏路由页适配层 -- 负责: - - 读取 Zustand 状态 - - 拉 mock bootstrap - - 组装状态卡片、历史、走势、面板内容 - - 根据设备类型切换移动端 / 桌面端壳层 - - 接公告弹窗、自动托管遮罩 - - 使用 i18n 文案 - -#### [src/features/game/untils.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/index.ts) -新增内容: -- 对 `game` 功能模块统一导出 - -### 3.9 新游戏路由 - -#### [src/routes/$lang/game.tsx](/Users/jiaunun/Desktop/36-character-flower/src/routes/%24lang/game.tsx) -新增内容: -- 新增 `/$lang/game` 路由 -- 作为游戏大厅的统一业务入口 - -## 4. 修改文件清单与说明 - -### 4.1 基础常量与全局信息 - -#### [src/constants/untils.ts](/Users/jiaunun/Desktop/36-character-flower/src/constants/index.ts) -修改内容: -- 将应用名称从通用模板名改为 `36字花` -- 更新默认描述文案 -- 新增桌面断点常量 `DESKTOP_LAYOUT_MIN_WIDTH_PX` - -#### [index.html](/Users/jiaunun/Desktop/36-character-flower/index.html) -修改内容: -- 更新站点标题 -- 更新默认 description / OG / Twitter meta 信息 - -### 4.2 样式层 - -#### [src/styles.css](/Users/jiaunun/Desktop/36-character-flower/src/styles.css) -修改内容: -- 保持 `html/body/#root` 占满视口 -- 增加全局游戏主题变量 -- 增加游戏外壳、面板、光效等 utility 样式 -- 增加深色游戏背景基础视觉 - -### 4.3 路由与页面 - -#### [src/routes/$lang/route.tsx](/Users/jiaunun/Desktop/36-character-flower/src/routes/%24lang/route.tsx) -修改内容: -- 把原来的简单 `Outlet` 布局升级成业务壳层 -- 增加顶部导航 -- 增加语言切换按钮 -- 使用共享导航组件 - -#### [src/routes/$lang/index.tsx](/Users/jiaunun/Desktop/36-character-flower/src/routes/%24lang/index.tsx) -修改内容: -- 把原来的占位首页改成业务入口首页 -- 增加进入游戏大厅 CTA -- 展示当前项目架构说明 - -#### [src/routeTree.gen.ts](/Users/jiaunun/Desktop/36-character-flower/src/routeTree.gen.ts) -修改内容: -- 因新增路由自动重新生成 -- 非手写文件 - -### 4.4 国际化 - -#### [src/locales/zh-CN/common.ts](/Users/jiaunun/Desktop/36-character-flower/src/locales/zh-CN/common.ts) -修改内容: -- 更新首页和壳层文案 -- 新增整套 `game.*` 文案 -- 包括: - - 游戏大厅标题、副标题 - - 状态卡片文案 - - 盘面、历史、走势文案 - - 公告弹窗、自动托管文案 - - 页脚说明文案 - - phase 展示文案 - -#### [src/locales/en-US/common.ts](/Users/jiaunun/Desktop/36-character-flower/src/locales/en-US/common.ts) -修改内容: -- 与中文语言包同步补齐英文版本 `game.*` 文案 - -## 5. 本次特别修复 - -### 5.1 修复 `/$lang/game` 的循环更新错误 - -涉及文件: -- [src/features/game/entry/entry-page.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/game-route-page.tsx) - -修复内容: -- 之前直接把会返回新引用的派生 selector 传给 Zustand hook,导致 React 触发无限更新 -- 现已改成: - - 只从 store 读取原始 state - - 在组件内通过 `useMemo` 派生数据 - -### 5.2 清理页面里的硬编码双语判断 - -涉及文件: -- [src/features/game/entry/entry-page.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/game-route-page.tsx) -- [src/routes/$lang/route.tsx](/Users/jiaunun/Desktop/36-character-flower/src/routes/%24lang/route.tsx) -- [src/locales/zh-CN/common.ts](/Users/jiaunun/Desktop/36-character-flower/src/locales/zh-CN/common.ts) -- [src/locales/en-US/common.ts](/Users/jiaunun/Desktop/36-character-flower/src/locales/en-US/common.ts) - -修复内容: -- 将大量 `i18n.language === 'zh-CN' ? ... : ...` 改为统一走 `t('...')` -- 避免将用户可见文案写死在业务代码里 - -## 6. 当前完成度 - -本次完成的是“业务骨架阶段”,不是最终成品。当前已经具备: - -- 统一游戏路由 -- 双端页面壳 -- 共享游戏状态模型 -- 共享 mock 数据和接口映射 -- 公告、自动托管、历史、走势等模块骨架 -- 国际化接线 - -当前尚未完成的内容包括: - -- 真实后端接口联调 -- WebSocket 实时同步 -- 完整的回合状态机 -- 完整下注规则约束 -- 最终视觉打磨和高级动效 - -## 7. 已验证结果 - -本次代码在当前状态下已完成以下验证: - -- `pnpm lint` 通过 -- `pnpm build` 通过 -- `http://localhost:5174/zh-CN/game` 可正常打开 diff --git a/figma/img.jpg b/figma/img.jpg deleted file mode 100644 index 633cb606f663a5fab27fd43a9fc18c9798c67ad7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 901544 zcmbrlbzD?W^f!KCmsq+5sRfo61ZnBsrCCY|r4eN5?o?2^q!FZ3X%Q)rmPJ~YP66re zAD`#@`n{g#pXc|_@7&kDb9T;|nfKg@opbKY-SpieK&GaGPyw*8008U$0`8WvrV)yY zFLjXGDhLhb{}J>7_XHLI0F;x58&XvP`s}$e6mRi=Qv4^guyS|#FZ{pIeZHrY|K$z< zV|@P?o&T?Bh_#Ko)xE<0{S9-wFZ>=X)jdyT_dh(>f4t@Y@KXQr-X1O<_c}WN@oxG^ z#e3fRp69arKX}XkgST>V`%gdSUPlV$==on+|IvSliENw<^zK9a`r}M*tv=0DwmH|Bn0L@5IHz&EkJ$2flw| z+u8!aVF>^bKL-HH5da`C`Cqd8$NvQz^j^epkIVV~vI86e8vqI*04Kl-;J@dd00Mv@ zAbN)Z6z`k!{~!IA$^X9`?o;1&0Oa_fD%>a#7At^Fjs+sey6Xm*?<;`|x)ssJbQn`jwvnJ+4 zXxN^7>4Yi#-haR@B#g9NIuKD*(z9|4qvcT6w|4hPBH$ENF|Y}bNG_-q^Yn^LN&QsU z^gM0FKjFT4u#-7GF(bl*HEg&{C&Ltfc>TxQuq%POi?e1M))2U zKD*LAtmJ}Gk?`6F9LhExDff8B3?hCki`se?)~iI0-(#2oh(Pycl7q+rS>PO$4$p%v z=5e+KTwMDIgCx%vB>n!mlLcoa_0EFEZ-V{L_pNiT0##iz?f~aOa)xK!0Ye(p__*tf z;ZCO7Y1lCNe;Sug0hjSIZ9W}rAQ|g_uF>%?_i~?TpO|N$!((qGy%*X+;~coRQi)rt zB}>Oko}IXU*Rmd%j~U<3-Fx7I_1E2(y~EOii*fko=|bTkwxjM1CLi%FLwN|}YK-41 zDsFmjA)GvV3#WX$ZxMT!7h=moXooCJu;sI zg*pP~tLg+KA(w%&lq(!M$?OOKMF74&>)q%medJ9h)S6B=?q9iqD1DCDnzCvT${Jo&Pxnmm38w0R`XwaIA{@Fp13BlkoeN-F&}Klzw; z>Dd;Ueu(UL$;LeCst?uJ(^78K1Sz+@4QDqOHqq%5`5h@@s~9jQ!Yz8q`NtqWw%4p% z@@wa#-fBulp_tqsvd{9gWk291Xc3*`p|hi}IoEo`F|qP`uN~=tDguq%eSJXiFCu2A zlR}PF#Xawo^v7G+76oz1+ZMi7<8_~%PVx0&^Kc2!0*?Q= z*pWA1$nCX*&fp!<(;)#X0+P{-*sbzW{Q^P$!NEUb$fO`UunbANCiGe^_G~jO4$ba^sN918g`t!IaimR z*EfP4;2wjffDC#SytAXu2)kldyqt~plChx#ak(QV*Wp>Qv*_3r?5_TG@U9pQhh04F zs(?;`mWx!3D%y23Ps)5VNiKV6_|FOGJg2Rd1Q90B1olF|lK$3l-TX6fVUB#m{cwQ+ z&}MqEIZZfAzYs6+8S1uajv#;i?=fg_-i7^zJYzTG9q{%H6BF|3YiJI7on$^EZ&iaK z3G0|rrdU++`kd^e0C-qzVKbL#rzawne83QK)pgC}v>;ULP!d7e6o{S-3J-x& zodLF5N;}PYlJQ-(pweZ*wu=SHPN|2uka-SUvFKf~ybl9D3@CqRp}tMJvaASJp%D{M`1XsnSy)#oV69!o6*w}e1%SvPb!NrP}%l!}7Q#l7`>0o3F~ z42>`LT`$FR^g!@qSHb9nzIUcYO#12wAi_bkciNg;%mLPm1@ke>@UMxK_*TC3}cSD4(q zD5Ze829XnF*82P+=MG@TiwN|l57$AN>%W%oLcZ--ncST&Ra zj%4}TW!XaQ-6FS?v7re_7HOx`1z7p|C`hpM{6=H#3Arc9OC6B%Kq&|oioG(v@NjkU z=J!Tl?gmp18cz^cODdScKbW!$u}OiI(UG9Kv4Y0Mg6`mL+cU&!D zCK(CA)#vE1A$i|h;ZetX@x4+68IeZt zhF;Jo^g~^M76a4TU^^g_rNH&USmCRdN@%lqrY8oRmT$oyy#AbudU(X7r{+eH{#$B z*wAIp!Ir;Ou(pP|tWP{5BLmaqL_@YHp~M(UA$9iG$-6_2j>25YP8NP5lObh0m#?Fx zB}*p}dQQ9|MKnCuL@TSE7u6l!iAWHd4Tt#%SYwgXcA!_fx?1;GmG^r1Ub`fDJrT_& zO7%#B2qg<%dy`*_lTnbYR&-Lv>owor`t;2ZVd1)|1`O>aOXdy6$Y5K0&2X;GdS6`r ztNH77v9ce-*sRL9Jo;2_$-Z^=*S=%$GaTBzrn6o*Ja9)cIM7AXQSv(UU%){~M(AO9 z2x*AiKUt<(2sB2^g;jMUwkaBY-%F0agg?Qee|EfPReG)5peIL3 z!Gcb18?KDPr*0{0hMVHE*Lusgfxz+Q{-kwG>=h^qJJ4PF+jZ#D{BrQ8=PJKZGT>ik zh(mw7jXWvg?3AcIAq?}Z-T6lN1EJvB;KO3+1vGinL$+XwUrnG^T@Gy>zV=6!{=V4w z`&iv)lo#iVcGAza?tr1wJhQruFz*EXc`ZAhT|k0NB`EMyIOqw!0Fid~MsG~)He^K9DxGN!~9B|O}%5h=5ZH>mY{j9&{J^o=Kbh@63h>T3SdFg zGXxjLS5@Zo_(^B*p=X@Ta7w0t^F`H`I{^QB|0Ch9xg5vArcn2cDj>ipNlRu&2F=|# z_hJC(qp-c>&xnt%TlY8Zn#SgwgeNbvezx@9qirK>F$1kx+QI(8|E885q#tcTmq&_t%aos8U+iY^VCvjLyZ8WvUOs>O} zbG@NJiwZy0c*9Q12i|#y!g0}Qqi@>Z(Su$QQ2W|&?=>%N@KtKXQq369r zd!g-s97KqG(dy!2FZ5!+15aY3i(ld*@6994T{ipoTg{&(|Jk{TvVph-M-r^uTra$i zLxKXRu%YDA>s}ai+``@>o(m;3l(CgIdB%FBoqz%f2^0L1eSMaP-dK1`I79p?=QP3B zrD2)Ky;C&mrgS!AX&fa8#MO1YPm;_7^^S6miVhuoRm5$4-&s+Vml*iF^(@dB&YbSo=hujsX|5Z)TLOipNKA4|XxHKeqF6Z>iO0X209y7UK z6w}Q$k=1++s7G9m8X&L~4AD3}iLnb3b%-!9fPmk9=sdnDKfApHUXV-6g;AcPj)vlx z5-p}8SF>IQ^E09Z-xT-58#-pwY48%-6KAGD5K4B~`JQAjDc%+6?9FJ6Zp^Lpie;dC z$8i!tbhJd5{0oAZ?K`>cyPaNC zL(ul!0nhS^e{Z)_sCumYrE8ukVg9O%tiiKT0HY+9$jyVWgGL%b(;v!J2XW(XF3^Bz zfojNXY}Z#pk`MI@xeu8gLlinCo#5|Z{8;e)zV6Zipb@e$K>}DS_?(xuO174klIb~Z zIn9Z;RjuA%XM#^f3~ycKf=SmpdooMA&G^`+7ABOirF4RWX_vg!Z^r&ok6GcRf8SU9 zcL%)7I5Gkf`?O6}3T(j)ebO6kB$!?&qJ)p$fNJ)wc%CHXW_QA7Nd(BV&K7*F{OS(4 z4C3r#<|ptNti^A6zHfKEUU?q-7cb^w>1O6F4jIwE3J^mx7J8^78~X|-@5(l4(M0N# z*l_-mh(MPTOCZ4c+)9Q+8V|p33)W3e_wU|1`Y|P*3;U!gF|%O@R?*aazPGYSvS{hm zdC(Cn`d4d%HPI@$cV;vsiL0)Z{7?~&|hb3Dv;G1}5o-AecArFn; z?E}>)A*Ya?6=IAE--wa*xaje|7EHdmHHF7z`N@l^i!*kVmCIgfw&vcvbrL$U-#zme z#=3EM*CC3zB}qK&@iCj}1F40+XXFV5Ge#X=B!(`CaP|;&gjr|BL4yq2iBuI@Y~