文档红线细化:新增服务端请求合法性验证(§8)+悲观UI/输入-渲染解耦,收紧 shared 与常量准入门槛

- 服务端 04:新增「操作请求合法性验证(不可信客户端)」章节——五层验证栈
  (座位鉴权/阶段门/操作可用性/幂等去重/参数校验)、统一裁决网关默认拒绝、
  availableActions 兼任决策依据与准入白名单、出牌回合门防死代码;后续章节顺延重编号
- 前端 04/05:悲观 UI——点击只发请求包、对局状态表现收包后更新,收包处理器触发源无关,
  AI 托管/他人广播共用同一更新路径;响应/掷骰交互按钮隐藏为受控乐观清除例外
- shared 准入门槛(前后端 04/05 §9/§8):进 shared 的门槛是「前后端都真正用到且必须
  逐字一致」而非「它是玩法逻辑」;前端只展示+悲观UI ⇒ 计算类逻辑不进 shared,默认留服务端
- 硬编码常量准则(服务端 04 §10):魔法字符串默认常量化(单一来源);常量放 shared 亦按
  同一准入门槛判定,仅前后端都用到才进 shared/constants

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 04:12:27 +08:00
co-authored by Claude Opus 4.8
parent 46f65de7ed
commit 96971f3072
5 changed files with 147 additions and 15 deletions
@@ -116,7 +116,80 @@ function handleXxx(data) {
---
## 5. 启动编排
## 5. 输入—渲染解耦:发包只请求,收包才表现
> 专业名:**服务端权威的悲观 UI 更新(Server-Authoritative Pessimistic Rendering)** / 单向数据流下的**输入-渲染解耦**。
**核心时序原则:用户点击只负责发出请求包,绝不直接改动任何对局状态界面;一切随对局状态变化的表现,只在收到服务端下发的结果/推送包后才更新。**
发包与表现是两条独立通道:**点击发「命令」,收包应「事件」,UI 只订阅事件**。界面永远是服务端已确认状态的投影,不做「点击即更新」的乐观预测。
### 5.1 点击回调的职责边界
点击回调**只做两件事**:①组织并发送请求包(走 §1 语义化发包封装);②(可选)纯本地物理反馈。**不得**在点击回调里改动任何对局状态界面。
| 归类 | 例子 | 点击时可否做 |
|------|------|--------------|
| 纯本地物理反馈(不碰领域状态) | 按钮按下高亮/缩放/音效 | ✅ 可即时 |
| **响应/掷骰交互按钮的隐藏**(本玩家点击的碰/杠/过/胡按钮、手动掷骰按钮) | 点击发包即隐藏该按钮(兼作防连点) | ⚠️ **受控例外**允许乐观清除——**前提是服务端合法性验证 + 收包侧兜底**(见 5.6) |
| 其余对局状态表现(领域状态) | 各类提示(等待听牌/报定等待/掷骰提示文字)、当前控制权高亮、启停倒计时、落牌进牌河、阶段/托管图标、他人手牌/副露 | ❌ **禁止**乐观,一律等收包 |
判别标准:**这个变化服务端要不要确认?** 要 → 收包后更新(除 5.6 例外的交互按钮);纯本地物理反馈、服务端根本不关心 → 可即时。
### 5.2 为什么必须如此(与 AI 托管同源)
真人操作与 AI 托管/他人操作**共用同一后半段**:`服务端处理 → 下发结果包 → 前端更新`。
```
真人: 点击 → 发请求包 →┐
├→ 服务端处理 → 下发结果包 → 收包处理器(唯一更新点)
AI 托管:服务端 AI 决策 →┘
```
把界面更新一律挂在「**收包**」这个节点,则无论操作由真人点击还是服务端 AI 自动触发,前端表现都自动一致、**无需区分触发源**(渲染透明)。反之若挂在「点击」节点:AI 托管时**根本没有点击动作**,服务端自动发包后对应更新代码永不触发 → 界面卡死、提示不消失、按钮不刷新。
这正是根目录 CLAUDE.md「AI 托管数据一致性 / 对前端透明」在**前端时序维度**的必然要求,也与「前端不推测、当前玩家由服务端权威字段(如 `nextControlSeat`)给出」一脉相承。
### 5.3 收包处理器必须「触发源无关」
同一收包处理器既服务真人操作回包、也服务 AI 托管与他人操作广播。因此:
- **不得假设「本座刚点过」**:所需数据一律从**包内权威字段**读取,**禁止**依赖「点击时暂存的本地变量」。
- **先判 `data.success`**(§4),再据包字段渲染;无对应点击也能正确渲染。
### 5.4 适用范围
凡「随对局状态变化」的表现均适用,包括但不限于:交互提示(手动掷骰提示文字 / 请出牌 / 等待其他玩家听牌 / 报定等待)、当前控制权高亮(按包内 `nextControlSeat`)、倒计时(按包内剩余时间启停)、手牌/牌河/亮牌、他人副露、阶段与托管图标。
> **例外**:本玩家点击的**响应/掷骰交互按钮**(碰/杠/过/胡按钮、手动掷骰按钮)的**隐藏**允许乐观清除(见 5.6);但这些按钮的**显示**,以及一切**提示**(含掷骰提示文字、等待听牌、报定等待),仍严格收包驱动。
### 5.5 验收标准
每处改动须验**两条路径表现一致**:
1. **真人手动操作**:点击后界面在收到回包时才更新(点击瞬间不抢先变化)。
2. **服务端自动发包**(AI 托管 / 他人操作广播):无任何点击,界面同样在收到推送包后正确更新,且与真人路径**表现完全一致**。
两条都验过且一致,方为合规。
### 5.6 受控例外:响应交互按钮乐观清除 + 服务端合法性验证
对**本玩家点击触发的响应交互按钮**(碰/杠/过/胡 操作按钮、手动掷骰按钮),**允许**在点击发包时**乐观隐藏**该按钮——它同时充当防连点(按钮没了就点不了第二次)与即时反馈。这是对 5.1 悲观 UI 的**受控例外**,成立必须**同时满足**下列三条,缺一不可:
1. **服务端合法性验证兜底(正确性根本)**:正确性**绝不依赖**前端乐观隐藏。服务端对每个操作请求做完整合法性验证(座位鉴权 / 阶段门 / 操作可用性 / 幂等去重),默认拒绝非法/重复/越权/乱序包并回 `success:false` 且**不改状态**。即便乐观隐藏被绕过、或非正规客户端狂发包,服务端也正确拒绝。详见服务端 [04 §8 操作请求合法性验证](../../server/development-guide/04-开发规范与红线.md#8-操作请求合法性验证不可信客户端)。
2. **收包侧兜底同样成立(AI 托管一致)**:因 AI 托管/他人操作**无点击**、不触发乐观隐藏,该按钮的清除**必须**在收包侧同样能发生——碰/杠/吃后操作者收非空 `availableActions` 覆盖刷新、胡后收包隐藏操作按钮、掷骰结果收包 `hideManualDiceUI`、「过」由收包据服务端下发的空 `availableActions` 清(或"进入托管即隐藏交互"覆盖)。**不得因加了乐观清除就删除收包兜底**。
3. **仅限交互按钮、不外扩**:例外只覆盖"玩家自己点击的响应/掷骰交互按钮"。其余一切表现(等待听牌/报定等待/掷骰提示文字等**提示**、当前控制权高亮、倒计时、落牌进牌河、阶段/托管图标、他人手牌/副露)**仍严格收包驱动**。
一句话:**乐观清除是即时反馈 + 防连点,收包侧与服务端验证才是权威——三者并存,不是用乐观清除替代收包/服务端。**
**例外项验收**(5.5 两条路径一致仍成立,只是真人侧多了"乐观隐藏"一步):
- 真人:点击 → 按钮立即隐藏(乐观);服务端验证通过走正常流程,若拒绝(`success:false`)则按钮由收包纠正(重显或按服务端权威保持隐藏)。
- AI 托管:无点击 → 按钮由收包侧隐藏,表现与真人一致(**重点验「过」不残留**)。
- 连点/非法包:服务端拒绝,状态不变。
---
## 6. 启动编排
一局前端的启动由一段**启动编排**一次性完成(经 `Game_Modify.appStart` 触发):
@@ -132,7 +205,7 @@ function handleXxx(data) {
---
## 6. controllers 与 managers 职责
## 7. controllers 与 managers 职责
| 类别 | 角色 | 典型成员 |
|------|------|----------|
@@ -157,7 +230,7 @@ function handleXxx(data) {
---
## 7. 本篇 DO / DON'T
## 8. 本篇 DO / DON'T
| DO ✅ | DON'T ❌ |
|------|---------|
@@ -165,6 +238,8 @@ function handleXxx(data) {
| 收包统一进收包分发器,一 rpc 一处理器 | 在 `Game_Modify.*` 或散点里直接处理推送 |
| 受限入口只解包转交,业务在新架构 handler | 在受限文件堆业务逻辑 |
| 成败只认 `data.success` | 用 `status`/`code` 判成败、写 status 兜底 |
| 点击只发请求包,对局状态表现等收包后更新(响应/掷骰交互按钮乐观清除为 5.6 受控例外) | 提示/控制权/倒计时/落牌等点击即更新(乐观预测)——AI 托管无点击时界面卡死 |
| 收包处理器触发源无关,数据从包字段读 | 依赖「点击时暂存的本地变量」渲染 |
| 重连/重画复用同一路径,据数据重建界面 | 重连单写一套与正常对局不同的渲染 |
| 业务在 controllers/managers,各司其职 | 处理器交叉重造别人的能力 |
@@ -74,6 +74,7 @@
- **服务端权威**:所有核心运算与胜负裁定以服务端为准、数据以服务端为权威;前端只做界面展示与玩家交互,本地数据仅为「渲染副本」,不做权威计算。
- **组件数据自持 + set-refresh**:组件(及其下每个「零件 UI」)的自有数据集中在 `this.data`,**不散落**各处;每个 UI 都有 `setXxx`(只写数据)/`refreshXxx`(只据数据画界面)成对方法,组件另有总 `refresh()` 据 `this.data` 重建整块界面(见 02)。
- **更新时机——发包只请求、收包才表现(悲观 UI / 输入-渲染解耦)**:用户点击**只发请求包**,**绝不**在点击时改动任何对局状态界面(提示显隐、按钮增删、当前控制权、倒计时启停、落牌、阶段/托管图标);这些表现一律在**收到服务端结果/推送包后**更新。点击回调只做「发包 +(可选)纯本地物理反馈(按下高亮/音效、防连点 `disable`)」。收包处理器**触发源无关**——数据从包字段读、不依赖「点击时暂存的本地变量」,故真人操作与 **AI 托管/他人广播共用同一更新路径、对前端透明**(无点击时也正确更新);把更新挂在「点击」而非「收包」会导致 AI 托管时界面卡死。详见 [04 §5](./04-网络对接与启动编排.md#5-输入渲染解耦发包只请求收包才表现)。**受控例外**:本玩家点击的**响应/掷骰交互按钮**(碰/杠/过/胡按钮、手动掷骰按钮)的**隐藏**允许乐观清除(兼作防连点+即时反馈),前提是**服务端合法性验证 + 收包侧兜底(AI 托管一致)**,见 [04 §5.6](./04-网络对接与启动编排.md#56-受控例外响应交互按钮乐观清除--服务端合法性验证) 与服务端 [04 §8](../../server/development-guide/04-开发规范与红线.md#8-操作请求合法性验证不可信客户端);其余提示/控制权/倒计时/落牌仍严格收包驱动。
- **收包节奏**:**先 `setXxx` 写数据 →(必要时 `refresh` 刷静态界面)→ 再播动画 → 动画回调里只刷新界面**;动画的开始/结束/出错等生命周期回调里**绝不设置核心数据**,动画期间**不改数据**。
- **重画随时可用(断线重连 + 硬刷新都要处理)**:重连/页面重载的本质是**恢复数据 → 调各 UI 组件 `setXxx`/`refreshXxx` 恢复数据与界面状态**;任何时候调 `refresh` 都能据 `this.data` 重建正确界面(断线重连、硬刷新/网页重载、切 app 复用**同一条**重画路径,不为重连单写一套渲染,详见 04 §3.3)。
- **动画是体验层**:开发阶段可**先不做动画**只保证静态界面正确;即使动画缺失/卡住/播错,数据、逻辑与界面仍正确、互不影响。
@@ -90,10 +91,10 @@
## 8. shared 同步
- **`shared/` 是子游戏自己的游戏逻辑,与平台无关**:存放本玩法**前后端必须算出完全一致**的纯逻辑(胡牌/听牌/比精/牌型/计分/规则常量等),前端用于即时表现与预校验,服务端做权威裁定。它不是平台代码,平台既不提供也不感知。
- **`shared/` 只放"前后端都真正用到"的那部分逻辑,务必慎重**:进 `shared/` 的门槛不是"它是玩法逻辑",而是"**前端确实会用到、且两端必须算出完全一致的结果**"。因为**前端只做展示、不做核心运算**(服务端权威,见 §5)叠加**悲观 UI**(可操作项/提示/结果由服务端推送、前端不预测,见 §5),**前端实际会重算的逻辑很少**——发牌/胡牌裁定/计分/AI 等计算类逻辑前端根本不算(服务端算好推来展示即可),它们**不进 `shared/`**。只有本地即时预判/高亮所需的校验、或纯展示所需的规则常量/牌型映射等,才把**那一部分**提升进 `shared/`。**默认留服务端**,拿不准就别放。它不是平台代码,平台既不提供也不感知。
- `01_SubGame/codes/shared/` 是服务端 `server/<游戏容器目录>/<游戏>/shared/` 的**同步副本**,前端**只读**(脚本生成)。
- 改共享算法只改服务端权威源,再运行同步脚本覆盖前端副本;**禁止直接编辑前端 `codes/shared/`**。
- 逻辑同源不改变数据权威:前端 `shared/` 算的是表现/预判,最终以服务端为准(数据优先、表现延后,见 §6)。详见服务端 [04 §8 shared 文件同步流程](../../server/development-guide/04-开发规范与红线.md#8-shared-文件同步流程)。
- 逻辑同源不改变数据权威:前端 `shared/` 算的是表现/预判,最终以服务端为准(数据优先、表现延后,见 §6)。详见服务端 [04 §9 shared 文件同步流程](../../server/development-guide/04-开发规范与红线.md#9-shared-文件同步流程)。
---
@@ -125,6 +126,7 @@
| 常量 | 精灵/群组/图层/图片/声音/Spine/坐标/动画/事件全进常量;UI 代码禁硬编码 id 与裸值 |
| 组件 | 继承 `BaseComponent`;事件 `addEventListener`;`destroy` 清动态精灵/定时器 |
| 数据 | 服务端权威(核心运算/裁定在服务端);组件自有数据集中 `this.data`;每个 UI/零件有 set/refresh 对、组件有总 `refresh` |
| 更新时机 | 点击只发请求包,对局状态表现等**收包后**更新(悲观 UI);收包处理器触发源无关、数据从包字段读;AI 托管/他人广播共用同一更新路径。**受控例外**:响应/掷骰交互按钮隐藏可乐观清除(防连点+即时反馈),前提是服务端合法性验证+收包兜底(04 §5.6、服务端 04 §8) |
| 节奏 | 先写数据后表现;动画回调只刷界面、**不写核心数据**;重画可随时据数据还原界面 |
| 成败 | 只认 `data.success`,禁 `status` 兜底 |
| 收发包 | 发走语义化发包封装、收走收包分发器;业务不进 `Game_Modify` |
+1
View File
@@ -66,6 +66,7 @@ BaseComponent / UIManager(ui,组件化与场景) ← gameabc-framework(
- **组件数据自持 + set-refresh**:组件自有数据集中 `this.data`;每个 UI/零件有 `setXxx`(只写数据)/`refreshXxx`(只据数据画)对,组件有总 `refresh()` 可据数据重建界面。
- **UI 组件专职自己的界面**:每个界面的数据与渲染只由其对应 UI 组件实现;别的模块要改/刷该界面一律**调该组件的公开接口**(`setXxx`/`refreshXxx`/语义方法),**禁止**在别处重复实现重叠或类似的界面数据/渲染逻辑。
- **显隐走 `showXxx`/`hideXxx`**:UI 组件/零件的精灵、群组显隐必须由组件暴露的 `showXxx`/`hideXxx` 接口控制;**禁止**别处直接用其精灵 ID/群组 ID 去 `SpriteManager.show/hide` 控显隐。
- **发包只请求、收包才表现(悲观 UI / 输入-渲染解耦)**:用户点击**只发请求包**,对局状态界面(提示/按钮/控制权/倒计时/落牌/阶段)一律**收到服务端结果或推送包后**才更新,点击时不做乐观预测;收包处理器**触发源无关**(数据从包字段读,不依赖点击时的本地变量),故真人操作与 **AI 托管/他人广播共用同一更新路径、对前端透明**——挂在「点击」而非「收包」会导致 AI 托管时界面卡死(详见 04 §5)。**受控例外**:本玩家点击的**响应/掷骰交互按钮**(碰/杠/过/胡、手动掷骰按钮)的隐藏允许乐观清除(兼作防连点+即时反馈),前提是**服务端合法性验证 + 收包侧兜底(AI 托管一致)**,见 04 §5.6 与服务端 04 §8;其余提示/控制权/倒计时/落牌仍严格收包驱动。
- **数据优先、表现延后**:收到推送先 `setXxx` 写数据、再播动画;动画的开始/结束/出错回调里**只刷界面、不写核心数据**。即使动画缺失/卡住/出错,数据、逻辑、界面仍正确、互不影响。
- **重连即重画(断线重连 + 硬刷新都要处理)**:重连/页面重载的本质是**恢复数据 → 调各 UI 组件 set-refresh 恢复数据与界面状态**,复用同一条重画路径,不为重连单写一套渲染(详见 04 §3.3、05 §6)。
- **成败只认 `data.success`**:前端一律 `if (!data.success)` 判成败,**不**用 `status`/`code`,**不**写 `status` 兼容兜底。