refactor(game): 重构项目结构,优化链路, 移动端适配

- 移除 useGameBoardVm 数据层实施说明文档
- 移除核心玩法与前端规则摘要文档
- 移除游戏模块数据与界面分层第一阶段实施稿文档
- 清理与数据层重构相关的技术方案说明
- 删除关于 PC 和 Mobile 界面分离的设计规划
- 移除 view-model hooks 架构设计相关内容
This commit is contained in:
JiaJun
2026-06-03 17:21:13 +08:00
parent 3efcb3bba6
commit bfb4b76611
129 changed files with 4534 additions and 4227 deletions

View File

@@ -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` 直接渲染 `<DesktopAnimal />`
- 点击格子不会写入真实下注状态
结果是:
- 控制栏虽然已经接入了部分业务数据
- 状态栏、历史区也开始接入 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 业务写在 hookUI 只接 props
推荐接线方式:
在 [pc-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/pc-entry.tsx) 中:
```tsx
const { activeId, onCellPress } = useGameBoardVm()
<DesktopAnimal activeId={activeId} onSelect={onCellPress} />
```
### 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` 不再裸挂 `<DesktopAnimal />`
- `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 层在内”的改造方向。

View File

@@ -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. **服务端状态优先**
- 前端可以先做本地交互反馈
- 但回合状态、封盘、开奖、派彩都必须最终以服务端为准

View File

@@ -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<string, unknown> | 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 接入

View File

@@ -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 获取接口鉴权 Tokenauth-token
- **GET** `/api/v1/authToken`
- 用途:获取 `auth-token`(所有接口请求头必带)
请求示例:
`/api/v1/authToken?secret=564d14asdasd113e46542asd6das1a2a&timestamp=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合法范围 136
- `chips`array[string](如 `["1.00","5.00"]`,含义:快捷筹码面额)
- `single_number_max_bet`string含义单号码最大下注额
- `dictionary`array<object>
- `number`int1-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`;每个号码为 136 的整数,数量不超过 `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<object>
- `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<object>
- `record_id`int含义钱包流水 ID
- `biz_type`string含义业务类型
- `direction`int1入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`string2 位小数,含义:对外标价金额,与业务配置一致;展示用)
- `amount`string2 位小数,含义:玩家本次需支付的充值金额)
- `bonus_amount`string2 位小数,含义:该档位赠送金额,无赠送为 `0.00`
- `total_amount`string2 位小数,含义:到账总额 = 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`**模拟第三方收银台** 完整 URLHMAC 防篡改,见下 §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`string2 位小数,含义:玩家本次支付的充值金额,与所选档位 `amount` 一致)
- `bonus_amount`string2 位小数,含义:本次赠送金额,与所选档位 `bonus_amount` 一致,无赠送为 `0.00`
- `total_amount`string2 位小数,含义:实际入账总额 = 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`string2 位小数,含义:本单充值金额)
- `bonus_amount`string2 位小数,含义:本单赠送金额,无赠送为 `0.00`
- `total_amount`string2 位小数,含义:入账总额)
- `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`string2 位小数,含义:本单充值金额)
- `bonus_amount`string2 位小数,含义:本单赠送金额,无赠送为 `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`string2 位小数,含义:申请提现金额,与后台 `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<object>
- `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. WebSocketH5与状态同步
> 本版本已移除 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&timestamp=xxx&device_id=xxx&signature=xxx` 获取 `auth-token`
2. `POST /api/user/login` 登录(请求头带 `auth-token`
3. `POST /api/game/lobbyInit` 拉首页初始化(请求头带 `auth-token`
4. 建立 WebSocketH5连接发送订阅消息监听状态流
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<string>(负责人所属角色组层级路径列表)
- `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 + 路由。

View File

@@ -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、半写实或插画风
- 特效音、粒子素材、品牌化图标样式
以上内容只影响视觉表现,不影响本文件定义的交互和功能边界。

View File

@@ -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` 可正常打开