chatapp3-flutter/语言房重构.md
2026-04-22 20:08:45 +08:00

1449 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 语言房重构方案
## 文档目的
本文只记录一套可执行的重构方案,本轮不改业务代码。
本次建议的重构范围不是“整套语言房全部推倒重写”,而是优先重构**语言房特效子系统**,重点解决以下问题:
- 礼物特效、进场、飞向麦位、房间飘屏并发时主线程抢占严重
- 特效触发源分散,队列和调度分散,后续越优化越难维护
- 房间内特效偶发残留到首页/其它页面,生命周期边界不清晰
- 已做过多轮局部优化,但整体架构仍然偏重,继续打补丁收益会越来越低
本方案的核心原则是:
- 尽可能保留当前礼物业务逻辑、素材、阈值和视觉表现
- 先重构特效系统,不一次性推倒 `RTC/RTM/房间业务` 主链路
- 先做“收口与解耦”,再做“性能压测与降级”
---
## 当前建议结论
建议重构方向:
- 保留现有房间业务、消息协议、礼物配置接口和大部分现有素材
- 将当前分散在 `VoiceRoomPage / RTM / RTC / OverlayManager / SCGiftVapSvgaManager / GiftAnimationManager / RoomEntranceQueue` 的特效触发与播放逻辑,统一收口到一个**语言房特效引擎**
- 把“事件归一化、去重、合并、优先级、预算控制、播放调度、生命周期回收”集中管理
不建议本轮做的事:
- 不建议同时重写整套语言房 UI、聊天室、麦位、RTC 状态层
- 不建议先动后端协议
- 不建议一开始就更换全部动画素材格式
---
## 当前架构现状
### 当前核心模块
| 模块 | 当前文件 | 当前职责 | 当前问题 |
| --- | --- | --- | --- |
| 房间主页面 | `lib/modules/room/voice_room_page.dart` | 挂载房间 UI、礼物播报、进场动画、幸运礼物动画、飞向麦位控制器 | 页面层知道太多特效细节,既处理业务又处理动画 |
| 房间消息入口 | `lib/services/audio/rtm_manager.dart` | 处理群消息、礼物消息、进场消息、全服广播、飘屏 | 直接触发 `play()``OverlayManager().addMessage()`,事件与 UI 强耦合 |
| 房间状态入口 | `lib/services/audio/rtc_manager.dart` | 管理进房、退房、麦位、在线用户、房间状态 | 也会直接触发进场坐骑特效,和 RTM 形成双入口 |
| 全屏礼物播放器 | `lib/shared/tools/sc_gift_vap_svga_manager.dart` | 管理高成本 `VAP/SVGA` 全屏特效任务 | 只管理自己这一条链路,不是整个房间特效系统的总调度器 |
| 房间调度器 | `lib/shared/tools/sc_room_effect_scheduler.dart` | 在高成本特效期间延后部分低优先级特效 | 只有“延后”能力,没有统一队列、优先级、预算、去重、丢弃策略 |
| 顶部礼物播报条 | `lib/services/gift/gift_animation_manager.dart` + `lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart` | 4 个槽位的礼物播报、连击合并、幸运礼物奖励框 | 已做局部优化,但仍独立存在,未纳入统一引擎 |
| 进场动画 | `lib/ui_kit/widgets/room/anim/room_entrance_screen.dart` | 房内横向 banner 进场队列 | 又是一套独立队列与独立状态机 |
| 飞向麦位 | `lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart` | 礼物图片从中间飞向目标麦位 | 独立 overlay、独立队列、独立缓存 |
| 飘屏 | `lib/shared/data_sources/sources/local/floating_screen_manager.dart` | 幸运礼物、礼物、火箭、红包、游戏飘屏 | 仍是独立优先队列,与房间特效生命周期分离 |
| 旧进场链路 | `lib/ui_kit/widgets/room/anim/room_entrance_widget.dart` | 旧版 `SVGA` 进场队列与 helper | 当前基本不承担主展示,但代码仍存在,增加维护噪音 |
### 当前核心问题
- 同一套语言房里同时存在多套特效队列和状态机
- `RTM/RTC` 直接知道“怎么播动画”,不是只产出事件
- 房间专属特效有一部分挂在根层 `MaterialApp builder`,不是挂在房间路由内部
- 同类事件存在本地触发与远端回流双路径,天然有重复触发风险
- 当前调度器只在高成本特效时“暂缓别的”,但底层仍是多个系统各播各的
- 历史链路未完全收口,例如旧进场 helper 仍保留
---
## 根因判断
当前卡顿的根因更偏向**结构性问题**,而不是单个控件或单次绘制的问题:
1. 多源头直接驱动 UI
`RTM/RTC/RoomPage/GiftSystem` 都可能直接触发动画。
2. 多条高频动画链路并发
全屏 `VAP/SVGA`、顶部礼物条、进场 banner、飞向麦位、房间飘屏可能同时争抢主线程。
3. 房间特效不是一个完整子系统
现在只有局部管理器,没有一个真正拥有“总入口、总队列、总预算、总生命周期”的 runtime。
4. 业务状态更新和特效播放距离太近
房间麦位刷新、在线用户刷新、消息列表更新,会和特效播放共享同一个页面状态环境。
5. 生命周期边界偏松
房间退出后,根层 overlay 和异步回调仍然需要额外兜底清理。
---
## 重构目标
### 目标
- 让语言房特效具备统一入口、统一调度、统一生命周期
- 在不改现有礼物业务口径的前提下降低并发卡顿
- 让房间特效只在房间内部存在,不再溢出到其它页面
- 让每种特效都可以独立限流、合并、降级、观测
- 让后续继续优化时,能在统一引擎层处理,而不是继续在各个页面打补丁
### 非目标
- 本轮不改变房间消息协议
- 本轮不重做礼物页面或礼物发送接口
- 本轮不调整礼物业务规则口径
- 本轮不强制替换所有动画素材格式
---
## 必须保留的当前礼物/进场业务逻辑
本次重构的前提是:**默认保留当前业务判断口径,只调整结构和调度层**。以下逻辑建议在首轮迁移中原样保留。
| 逻辑项 | 当前口径 | 重构策略 |
| --- | --- | --- |
| 礼物消息来源 | 继续沿用当前房间 RTM 自定义消息 | 保留,不改协议 |
| 全屏礼物触发口径 | 继续按 `gift.special + giftSourceUrl + scGiftHasFullScreenEffect(...)` 判定 | 保留,先抽到策略层 |
| 礼物特效总开关 | 继续沿用 `SCGlobalConfig.isGiftSpecialEffects` | 保留,接入新引擎策略层 |
| 入场坐骑开关 | 继续沿用 `SCGlobalConfig.isEntryVehicleAnimation` | 保留,接入新引擎策略层 |
| 飘屏开关 | 继续沿用 `SCGlobalConfig.isFloatingAnimationInGlobal` | 保留,接入新引擎策略层 |
| 顶部礼物播报条合并规则 | 继续按当前 `labelId` 逻辑合并活动项和待播项 | 保留,迁移时保持视觉与合并口径一致 |
| 顶部礼物播报条槽位数 | 继续保持 `4` 个槽位 | 保留,首轮不改 |
| 顶部礼物播报待播上限 | 继续保持当前上限 `24` | 保留,后续再参数化 |
| 顶部礼物播报空闲消失时间 | 继续保持约 `3200ms` | 保留,后续再参数化 |
| `customAnimationCount` 逻辑 | 继续作为飞向麦位次数和礼物数量步进动画的依据 | 保留,不改视觉节奏 |
| 飞向麦位目标解析 | 继续按当前 `toUser -> account -> 麦位映射` 解析目标 | 保留,迁移时统一封装 |
| 飞向麦位会话去重 | 继续保留当前 `queueTag / sessionKey` 级别清理策略 | 保留 |
| 幸运礼物里程碑特效 | 继续保留当前里程碑资源集合与触发口径 | 保留 |
| 幸运礼物顶部奖励框 | 继续保留当前奖励金额展示方式 | 保留 |
| 幸运礼物 burst 条件 | 继续保持“倍率 `>= 10x` 或单次奖励 `> 5000`” | 保留 |
| 幸运礼物飘屏口径 | 继续保持当前服务端 `globalNews` + 倍率门槛逻辑 | 保留 |
| 大额礼物房间飘屏阈值 | 继续保持当前 `coins > 9999` 才触发房间礼物飘屏 | 保留 |
| 礼物触发麦位刷新 | 继续保留礼物触发后的节流刷新逻辑 | 保留,迁移时挪到 effect bridge 或 event side-effect 层 |
| 房间进场横幅 | 继续保留当前房内进场 banner 表达方式 | 保留视觉,不一定保留旧实现 |
### 建议保留但从硬编码改为配置化的参数
- 礼物播报槽位数
- 礼物播报待播上限
- 进场队列上限
- 飞向麦位单会话上限
- 高成本特效队列上限
- 飘屏丢弃阈值
- 各类优先级与独占关系
---
## 目标架构
### 架构总览
```mermaid
graph TD
A[RTM/RTC/房间本地动作] --> B[RoomEffectEventAdapter]
B --> C[RoomEffectEngine]
C --> D[EffectPolicy]
C --> E[Dedup & Merge]
C --> F[Lane Dispatcher]
F --> G[Fullscreen Lane]
F --> H[Entrance Lane]
F --> I[GiftTicker Lane]
F --> J[SeatFlight Lane]
F --> K[Floating Lane]
G --> L[RoomEffectStage]
H --> L
I --> L
J --> L
K --> L
L --> M[SVGA/VAP/Image/Implicit Animations]
C --> N[Metrics & Trace]
```
### 核心思想
- 所有原始事件先进入 `RoomEffectEventAdapter`
- adapter 只负责把“原始 RTM/RTC/本地动作”变成**标准化特效事件**
- 所有标准化事件再进入 `RoomEffectEngine`
- engine 统一做去重、合并、优先级排序、预算判断、降级和分 lane 调度
- 真正的渲染层只负责展示,不再关心业务来源
---
## 推荐模块拆分图
建议新增一个独立的语言房特效模块,命名可为 `lib/modules/room_effect/``lib/shared/room_effect/`
```text
lib/
modules/
room_effect/
domain/
room_effect_event.dart
room_effect_task.dart
room_effect_lane.dart
room_effect_priority.dart
room_effect_policy_snapshot.dart
application/
room_effect_event_adapter.dart
room_effect_engine.dart
room_effect_deduplicator.dart
room_effect_merger.dart
room_effect_policy.dart
room_effect_metrics.dart
presentation/
room_effect_stage.dart
layers/
room_fullscreen_effect_layer.dart
room_entrance_effect_layer.dart
room_gift_ticker_layer.dart
room_seat_flight_layer.dart
room_floating_effect_layer.dart
infrastructure/
room_effect_asset_preloader.dart
room_effect_player_bridge.dart
room_effect_lifecycle_guard.dart
```
### 各模块职责
| 模块 | 职责 | 说明 |
| --- | --- | --- |
| `RoomEffectEventAdapter` | 事件归一化 | 把 `RTM/RTC/RoomPage` 的原始数据转为统一事件对象 |
| `RoomEffectEngine` | 统一调度核心 | 做入队、去重、合并、优先级、预算、丢弃、lane 分发 |
| `RoomEffectPolicy` | 播放策略 | 根据机型、开关、 backlog、当前播放状态决定播什么、延后什么、降级什么 |
| `RoomEffectStage` | 页面内特效承载层 | 只在语言房路由内部挂载,房间退出就一并销毁 |
| `PlayerBridge` | 播放器桥接层 | 首轮复用现有 `SCGiftVapSvgaManager` / `GiftAnimationManager` / 现有动画组件 |
| `AssetPreloader` | 资源预热 | 统一预热图片、SVGA、VAP本轮不再让每个特效组件各自处理 |
| `Metrics` | 指标与日志 | 记录入队数、丢弃数、等待时长、播放耗时、异常和 backlog |
---
## 目标事件模型
建议统一定义一个标准化事件对象 `RoomEffectEvent`,至少包含以下信息:
- `eventId`
- `roomId`
- `eventType`
- `source`
- `createdAt`
- `priority`
- `dedupKey`
- `mergeKey`
- `userId`
- `targetUserId`
- `giftId`
- `payload`
- `requiresHeavyRenderer`
- `roomScoped`
### 推荐事件类型
- `roomUserJoin`
- `roomMountEntrance`
- `giftTicker`
- `giftFullscreen`
- `giftSeatFlight`
- `giftFloating`
- `luckyGiftMilestone`
- `luckyGiftRewardTicker`
- `luckyGiftBurst`
- `rocketFloating`
- `redPacketFloating`
- `gameFloating`
### 为什么要有标准化事件层
- 后续所有去重和合并都建立在统一事件模型上
- 可把“同一礼物事件触发 3 种视觉表现”拆成 3 个子任务,而不是让 RTM 直接调用 3 个 UI 系统
- 可支持本地自发事件和远端回流事件的统一去重
---
## 目标任务模型
事件进入引擎后,不直接播放,而是先变成 `RoomEffectTask`
一个 task 建议至少包含:
- `taskId`
- `lane`
- `priority`
- `enqueueAt`
- `deadline`
- `exclusive`
- `mergeable`
- `mergeKey`
- `dropPolicy`
- `payload`
- `onStart`
- `onComplete`
- `onCancel`
### 推荐的 lane 划分
| Lane | 作用 | 特点 |
| --- | --- | --- |
| `fullscreen` | 全屏礼物、幸运礼物 burst、进场坐骑等高成本特效 | 可独占,优先级最高 |
| `entrance` | 房间进场 banner | 串行,可合并,可被延后 |
| `giftTicker` | 顶部礼物播报条 | 维持 4 槽位模型 |
| `seatFlight` | 飞向麦位 | 可批量、可会话级去重 |
| `floating` | 房间飘屏、幸运礼物飘屏、火箭、红包、游戏 | 可按消息类型和房间范围过滤 |
---
## 页面挂载方式调整
### 当前问题
当前房间特效有一部分挂在根层 `MaterialApp builder`,例如:
- 全屏 `VapPlusSvgaPlayer`
- `RoomGiftSeatFlightOverlay`
这意味着:
- 特效承载层不完全属于房间页面
- 房间退出后需要额外兜底清理
- 根层 overlay 容易被误复用到非房间页面
### 目标方式
推荐新增 `RoomEffectStage`,只挂在语言房页面内:
```mermaid
graph TD
A[VoiceRoomPage] --> B[RoomScene]
A --> C[RoomEffectStage]
C --> D[Fullscreen Layer]
C --> E[Entrance Layer]
C --> F[GiftTicker Layer]
C --> G[SeatFlight Layer]
C --> H[Floating Layer]
```
### 结果
- 房间特效生命周期跟随 `VoiceRoomPage`
- 退房、最小化、切房时统一收口
- 根层只保留真正全局的东西,房间特效不再越界
---
## 首轮推荐复用现有实现的方式
为尽量保留当前礼物逻辑,首轮不需要把所有 renderer 重写,可以先“新引擎 + 旧 renderer”。
### 可复用项
| 现有实现 | 首轮建议 |
| --- | --- |
| `SCGiftVapSvgaManager` | 作为 `fullscreen lane` 的底层播放器桥接 |
| `GiftAnimationManager` | 作为 `giftTicker lane` 的底层 4 槽位实现 |
| `RoomGiftSeatFlightOverlay` | 作为 `seatFlight lane` 的底层渲染器 |
| `RoomAnimationQueueScreen` | 其视觉可复用,但队列控制迁到新引擎 |
| `OverlayManager` | 首轮可继续承载非房间全局类型;房间类型逐步迁出 |
### 不建议继续保留为长期方案的实现
| 现有实现 | 建议 |
| --- | --- |
| `room_entrance_widget.dart` 的旧 helper/queue | 清理掉,不再作为正式链路 |
| 在 `RTM/RTC` 中直接 `play()``addMessage()` | 改为统一投递特效事件 |
| 在根层 `MaterialApp builder` 挂房间专属特效层 | 迁回房间路由内部 |
---
## 调度与降级策略
### 首轮策略
首轮重构不建议改业务口径,但建议统一调度策略。
#### 建议规则
- `fullscreen lane` 播放时,`entrance / floating / seatFlight` 默认延后
- `giftTicker lane` 继续独立展示,但只保留当前 4 槽位,不无限堆积
- `seatFlight lane` 保持当前 `customAnimationCount` 逻辑,但由引擎统一裁剪上限
- `entrance lane` 超过 backlog 后可合并为“X 人进入房间”
- `floating lane` 在高峰时优先保留高优先级类型,低优先级直接丢弃
### 第二阶段可再加的自适应策略
- backlog 超阈值时,把同礼物多次飞向麦位降成 1 次飞行 + 数量角标
- 进场队列过长时,多条普通进场合并成一条摘要
- 同时存在全屏高成本特效时,房间礼物飘屏暂停或只保留 1 条
- 低性能设备继续保留现有总开关,但高性能设备也可按瞬时 backlog 动态降级
---
## 迁移顺序
建议按“先收口入口,再迁移渲染,再清理历史代码”的顺序做,避免一口气全动。
### 阶段 0建立基线与指标
目标:
- 先补齐特效侧可观测性
- 固化当前礼物逻辑和关键参数,形成“迁移不改口径”的基线
本阶段内容:
- 统计每类特效的入队数、丢弃数、平均等待时长、播放时长
- 统计 `fullscreen/entrance/giftTicker/seatFlight/floating` 的 backlog 峰值
-`FrameTiming` 记录语言房高压场景的 build/raster 情况
本阶段不改:
- 不改现有播放逻辑
- 不改现有视觉
- 不改现有业务阈值
### 阶段 1统一事件入口
目标:
- 引入 `RoomEffectEventAdapter`
- 所有房间特效先变成统一事件对象
本阶段内容:
-`RTM/RTC/VoiceRoomPage/GiftSystem` 里直接触发特效的地方改为“投递事件”
- 本地触发与远端回流先统一走一层去重 key
本阶段不改:
- renderer 先不迁
- `SCGiftVapSvgaManager``GiftAnimationManager` 等先照旧使用
关键收益:
- 从这一阶段开始,特效来源就被收口了
### 阶段 2落地 `RoomEffectEngine`
目标:
- 让所有统一事件进入引擎
- 引擎接管优先级、去重、合并和 lane 分发
本阶段内容:
- 新建 `RoomEffectEngine`
- 新建 lane 模型
- 先做不改变视觉的“旧 renderer 包装”
本阶段不改:
- 不改具体动画 UI
- 不改素材
关键收益:
- 从“多套队列系统”变成“一个总引擎 + 多条 lane”
### 阶段 3把房间特效挂回房间路由内部
目标:
- 新建 `RoomEffectStage`
- 把房间专属层从根层迁回 `VoiceRoomPage`
本阶段内容:
- `RoomEffectStage` 承载房间内所有特效 layer
- 根层只保留真正全局的内容
- 统一退房清理、最小化清理、切房清理
本阶段重点回归:
- 房间退出后不再有礼物图残留
- 房间飘屏不再跑到首页
### 阶段 4迁移 `fullscreen lane`
目标:
- 先把最高成本、影响最大的特效链路收口
本阶段内容:
- `SCGiftVapSvgaManager` 改为由 `fullscreen lane` 调用
- 进场坐骑、全屏礼物、幸运礼物 burst 都走统一 lane
- 自带独占、优先级和预热策略
本阶段必须保持不变:
- 当前全屏礼物触发口径不变
- 当前幸运礼物里程碑和 burst 口径不变
### 阶段 5迁移 `entrance lane`
目标:
- 收口房间进场 banner
- 清理旧进场链路
本阶段内容:
- `RoomAnimationQueueScreen` 只保留渲染,队列逻辑迁到 `entrance lane`
- 清理 `room_entrance_widget.dart` 旧 helper 及残余引用
- 引入本地与远端进场事件去重
本阶段必须保持不变:
- 当前进场 UI 表达不变
- 当前入场后坐骑与横幅仍可共存,但由统一引擎调度
### 阶段 6迁移 `giftTicker lane`
目标:
- 保留当前 4 槽位礼物播报条逻辑,但把队列和状态收回引擎
本阶段内容:
- `GiftAnimationManager` 改为 lane 内 renderer bridge
- 继续保留当前 `labelId` 合并、4 槽位、空闲消失时间、数量步进动画
- 幸运礼物奖励框仍保留当前样式
本阶段必须保持不变:
- 连击合并口径
- `customAnimationCount` 对数量展示的影响
- 幸运礼物奖励框逻辑
### 阶段 7迁移 `seatFlight lane`
目标:
- 把飞向麦位从独立控制器改为统一 lane 管理
本阶段内容:
- 复用现有 `RoomGiftSeatFlightOverlay` 作为渲染层
- `queueTag/sessionKey` 的批量裁剪和清理迁入 engine
- 目标麦位 key 解析改成统一桥接
本阶段必须保持不变:
- 当前会话级去重逻辑
- 当前 `customAnimationCount` 粒度
- 当前图片预热行为
### 阶段 8迁移 `floating lane`
目标:
- 收口房间礼物飘屏、幸运礼物飘屏、火箭、红包、游戏飘屏
本阶段内容:
- 房间内的 floating 类型全部走 `floating lane`
- 真正全局广播的类型与房间类型分开
- `OverlayManager` 只保留全局用法,房间类型从中剥离
本阶段必须保持不变:
- 幸运礼物 `globalNews` 逻辑不变
- 房间礼物 `coins > 9999` 才飘屏的逻辑不变
### 阶段 9清理历史实现与补充自适应降级
目标:
- 删掉双轨实现
- 把当前“局部优化痕迹”收成统一结构
本阶段内容:
- 删除旧进场 helper
- 删除 `RTM/RTC` 中直接 `play()``addMessage()` 的旧入口
- 完成 backlog 降级策略与观测面板
---
## 关键兼容策略
### 1. 先换“控制面”,不先换“渲染面”
首轮不要把所有动画组件重写掉。
推荐策略:
- 新引擎先接管“什么时候播、播哪条、谁先谁后、是否丢弃”
- 旧组件先继续负责“怎么画出来”
这样做的好处:
- 视觉回归风险小
- 礼物逻辑更容易保持一致
- 每一阶段都可以独立回滚
### 2. 消息协议不动
当前房间消息、礼物消息、幸运礼物消息结构先不改。
这样可以:
- 避免前后端联动阻塞
- 避免把“性能问题”变成“协议问题”
### 3. 参数冻结后再迁移
建议先把以下参数冻结成一份配置文档或常量表:
- 幸运礼物里程碑列表
- 幸运礼物 burst 条件
- 房间礼物飘屏阈值
- 顶部礼物播报条槽位与时长
- 飞向麦位上限
- 进场队列上限
这样迁移时只改结构,不改结果。
---
## 风险点列表
| 风险点 | 说明 | 风险等级 | 缓解方式 |
| --- | --- | --- | --- |
| 本地事件与远端回流重复 | 自己送礼、自己进房、进场坐骑等容易双触发 | 高 | 引入 `dedupKey`,按 `roomId + type + sender + target + gift + timeWindow` 去重 |
| 去重过度导致特效漏播 | 某些高频连击若 key 设计过粗,会把应播事件吃掉 | 高 | 区分 `dedupKey``mergeKey`,先留日志,初期采用保守去重 |
| 房间退出后仍有旧特效回调 | `postFrame / delayed / timer / precache` 都可能晚到 | 高 | 所有 task 和 layer 引入 `sessionToken``roomLifecycleToken` 校验 |
| 渲染层迁移后层级错乱 | 根层迁到房间层后z-order 可能改变 | 中 | 在 `RoomEffectStage` 中明确 layer 顺序,先对齐现有视觉层级 |
| 资源预热过多导致内存抖动 | 统一预热后若没有预算控制,容易顶高内存 | 中 | 按 lane、资源类型、尺寸做分层缓存和 LRU |
| 幸运礼物口径漂移 | 这块逻辑复杂,回归风险高 | 高 | 首轮冻结参数与触发口径,不做业务变更 |
| 飞向麦位目标丢失 | 目标座位 GlobalKey 尚未可用时可能丢动画 | 中 | 保留当前重试机制,但统一纳入 lane限制重试次数 |
| 飘屏房间范围判断错误 | 房间飘屏与全局飘屏容易混 | 中 | 明确 `roomScoped``globalScoped` 两类事件 |
| 迁移阶段代码并存时间过长 | 双轨逻辑存在越久,后续越难收口 | 中 | 每一阶段都指定“完成后可删”的旧文件或旧入口 |
| 测试面过大 | 礼物、幸运礼物、火箭、红包、进场、坐骑交叉很多 | 高 | 用阶段化回归清单,按 lane 逐项验收 |
---
## 回归测试清单
### 礼物链路
- 单次送礼 1 次
- 同一礼物连续送 5 次、10 次
- `customAnimationCount > 1` 的批量场景
- 幸运礼物普通中奖
- 幸运礼物高倍率中奖
- 同时出现全屏礼物 + 顶部播报条 + 飞向麦位
### 进场链路
- 自己进入房间
- 别人进入房间
- 别人带坐骑进入房间
- 同时 5 人以上快速进入
### 飘屏链路
- 房间礼物飘屏
- 幸运礼物房间飘屏
- 全局广播幸运礼物飘屏
- 火箭飘屏
- 红包飘屏
### 生命周期
- 房间内最小化
- 退出房间
- 切到首页/消息/我的
- 从 A 房切到 B 房
### 性能
- 普通机型连续送礼 10 秒
- 低性能机型连续送礼 10 秒
- 进场和礼物叠加压测
- 房间内停留 10 分钟观察缓存与内存变化
---
## 验收指标建议
建议在重构过程中记录以下指标,避免“感觉快了”但没有客观标准:
- 语言房高压场景下 `FrameTiming``build/raster` P95
- `fullscreen lane` 平均等待时长
- `entrance/floating/seatFlight` backlog 峰值
- 被丢弃任务数量和占比
- 同事件重复触发数
- 房间退出后残留特效数
### 目标方向
- 房间高压场景下明显减少整页卡顿和掉帧
- 不再出现房间特效溢出到首页
- 不再依赖在多个管理器里各自补清理逻辑
- 新增一个特效类型时,只需要走“标准化事件 -> lane -> layer”链路
---
## 预计实施节奏
如果只重构语言房特效子系统,不推倒整套房间业务,建议节奏如下:
| 阶段 | 预估工作量 |
| --- | --- |
| 阶段 0 ~ 2 | 2 到 3 天 |
| 阶段 3 ~ 5 | 4 到 6 天 |
| 阶段 6 ~ 8 | 4 到 6 天 |
| 阶段 9 与回归 | 2 到 3 天 |
整体建议:
- 保守估计约 `2`
- 如果中途顺手一起重构 `RTC/RTM/RoomPage` 主业务结构,工期会明显变长
---
## 多线程并行执行方案
如果后续不是单线程顺着做,而是你准备把这次重构拆成多个并行线程分别跑,建议采用**“3 个主线程并行 + 1 个收尾线程串行收口”**的方案。
### 并行收益预估
按“最稳的预期”估算:
- 单线程完整落地:约 `20 到 24` 小时
- 多线程并行后:约 `14 到 16` 小时
这里的前提是:
- 先冻结接口命名和模块边界
- 尽量避免多个线程同时改同一批现有文件
- 最后仍保留一个统一收尾和回归线程
不建议的并行方式:
- 不建议 3 个线程同时直接改 `rtm_manager.dart / rtc_manager.dart / voice_room_page.dart`
- 不建议一边改 engine一边让多个线程各自发明自己的事件模型
- 不建议一开始就多个线程同时删旧链路
### 并行原则
1. 先统一接口,再并行开发
至少先约定好以下对象名和职责:
- `RoomEffectEvent`
- `RoomEffectTask`
- `RoomEffectLane`
- `RoomEffectEngine`
- `RoomEffectStage`
- `RoomEffectEventAdapter`
2. 新文件优先并行,旧文件尽量收口到单一线程
新增模块最适合多线程拆分;旧有入口文件应尽量减少多人同时修改。
3. 先搭骨架,再接现有链路
先让 engine 与 stage 的框架稳定,再开始把 `RTM/RTC/VoiceRoomPage` 接上去。
4. 收尾一定要串行
旧链路删除、层级调整、房间生命周期回收、回归修 bug这些必须收口到最后一个线程统一处理。
---
### 推荐线程划分
#### 线程 AEngine 基础层
定位:
- 负责“语言房特效引擎”的核心模型与调度骨架
推荐负责范围:
- `lib/modules/room_effect/domain/`
- `lib/modules/room_effect/application/`
- `lib/modules/room_effect/infrastructure/` 中和调度、预热、生命周期守卫相关的文件
推荐文件所有权:
- `room_effect_event.dart`
- `room_effect_task.dart`
- `room_effect_lane.dart`
- `room_effect_priority.dart`
- `room_effect_engine.dart`
- `room_effect_policy.dart`
- `room_effect_deduplicator.dart`
- `room_effect_merger.dart`
- `room_effect_metrics.dart`
- `room_effect_asset_preloader.dart`
- `room_effect_lifecycle_guard.dart`
本线程不建议碰:
- `rtm_manager.dart`
- `rtc_manager.dart`
- `voice_room_page.dart`
- 现有具体动画 widget 的渲染实现
本线程交付物:
- 可编译的 engine/domain/application 基础骨架
- lane、task、event、policy 的统一接口
- 基本的 backlog、优先级、去重、合并接口
#### 线程 BStage 与渲染承载层
定位:
- 负责“语言房特效舞台”和各 layer 的承载结构
推荐负责范围:
- `lib/modules/room_effect/presentation/`
推荐文件所有权:
- `room_effect_stage.dart`
- `layers/room_fullscreen_effect_layer.dart`
- `layers/room_entrance_effect_layer.dart`
- `layers/room_gift_ticker_layer.dart`
- `layers/room_seat_flight_layer.dart`
- `layers/room_floating_effect_layer.dart`
- 如果需要,可新增 presentation 侧 controller 或 snapshot 文件
本线程可复用但不建议大改的旧文件:
- `lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart`
- `lib/ui_kit/widgets/room/anim/room_entrance_screen.dart`
- `lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart`
- `lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart`
建议策略:
- 先做 layer 容器和接口桥接,不先重写底层动画实现
- 首轮允许通过 bridge 调现有 renderer
本线程不建议碰:
- `rtm_manager.dart`
- `rtc_manager.dart`
- `main.dart`
本线程交付物:
- `RoomEffectStage`
- 各特效 layer 的挂载顺序和层级
- 渲染层与 engine 的消费接口
#### 线程 C旧链路入口收口与适配层
定位:
- 负责把现有 `RTM/RTC/VoiceRoomPage/GiftSystem` 的特效触发点统一改成事件投递
推荐负责范围:
- `lib/services/audio/rtm_manager.dart`
- `lib/services/audio/rtc_manager.dart`
- `lib/modules/room/voice_room_page.dart`
- `lib/services/gift/gift_system_manager.dart`
- 适配层文件,例如:
- `lib/modules/room_effect/application/room_effect_event_adapter.dart`
- 或单独的 bridge/wiring 文件
本线程重点任务:
- 找出所有直接 `play()``OverlayManager().addMessage()`、直接入队的触发点
- 改为投递标准化事件
- 做本地触发和远端回流的去重 key
- 保持现有礼物业务逻辑不变
本线程不建议碰:
- `RoomEffectStage` 内部结构
- 各 layer 的视觉实现
- 大量历史文件删除
本线程交付物:
- 统一事件入口
- `RTM/RTC` 不再直接知道“怎么播动画”
- 旧业务逻辑已迁到 adapter/event 侧表达
#### 线程 D收尾、清理与回归
定位:
- 在 A/B/C 合并后串行执行,不建议一开始就启动
推荐负责范围:
- `lib/main.dart`
- `lib/modules/room/voice_room_page.dart`
- `lib/shared/data_sources/sources/local/floating_screen_manager.dart`
- `lib/shared/tools/sc_room_effect_scheduler.dart`
- `lib/ui_kit/widgets/room/anim/room_entrance_widget.dart`
- 其它需要删除旧逻辑、改挂载层级、补回归日志的文件
本线程重点任务:
- 把房间专属特效层从根层尽量收回房间页内部
- 清理旧进场 helper 和双轨实现
- 清理失效入口
- 跑一轮回归,修明显联调问题
---
### 最稳的执行顺序
推荐按下面顺序跑,而不是 4 个线程同时无序开工。
#### 第 1 步:接口冻结
由任意一个线程先完成,建议控制在 `0.5 到 1` 小时内。
需要先冻结的内容:
- `RoomEffectEvent` 字段
- `RoomEffectTask` 字段
- lane 列表
- engine 对外接口
- stage 对外接口
- adapter 的输入输出职责
这一步完成后,线程 A/B/C 才正式并行。
#### 第 2 步A/B/C 三线程并行
并行期间建议这样分:
- A 只写 engine/domain/application 基础层
- B 只写 stage/presentation/layer 容器
- C 只写现有旧入口收口和事件适配
并行期间禁止多人同时改的文件:
- `lib/services/audio/rtm_manager.dart`
- `lib/services/audio/rtc_manager.dart`
- `lib/modules/room/voice_room_page.dart`
- `lib/main.dart`
这些文件建议默认归线程 C 或线程 D 所有。
#### 第 3 步:先合 A再合 B再合 C
推荐合并顺序:
1. 先合线程 A
先把 engine 和模型定住,避免后续接口继续漂。
2. 再合线程 B
让 stage 和 layer 按 A 的接口接上。
3. 再合线程 C
最后把现有旧入口真正接到 engine 上。
这样做的原因:
- 可以最大程度减少 `A/B/C` 三方接口对撞
- 让旧链路接入时面对的是已经稳定的 engine 与 stage
#### 第 4 步:最后由线程 D 串行收尾
线程 D 再做这些事:
- 根层与房间层的挂载调整
- 删除旧实现
- 生命周期清理
- 房间退出和切房回归
- backlog 观测和简单日志补充
---
### 推荐的文件写入边界
为了减少合并冲突,建议严格遵守下面的写入边界。
| 线程 | 允许重点修改 | 尽量不要修改 |
| --- | --- | --- |
| A | `lib/modules/room_effect/domain/``application/``infrastructure/` 新文件 | 现有房间业务文件 |
| B | `lib/modules/room_effect/presentation/` 新文件 | `rtm_manager.dart``rtc_manager.dart` |
| C | `rtm_manager.dart``rtc_manager.dart``voice_room_page.dart``gift_system_manager.dart`、adapter | `main.dart`、presentation 新文件的大结构 |
| D | `main.dart`、旧 helper 清理、根层挂载调整、遗留逻辑删除 | engine 核心接口 |
---
### 推荐的并行里程碑
建议把并行执行拆成 4 个里程碑,每个里程碑都要求能单独检查。
#### 里程碑 1
- Aengine/domain/task/lane 骨架完成
- Bstage/layer 容器骨架完成
- C梳理所有旧触发点列出事件映射表
#### 里程碑 2
- Alane 调度、去重、合并接口可用
- B各 layer 能消费统一 task 或 snapshot
- C`RTM/RTC/VoiceRoomPage` 已改成先投递事件,不直接触发动画
#### 里程碑 3
- A/Bengine 与 stage 已能联通
- C主要礼物/进场/飘屏入口已接新引擎
#### 里程碑 4
- D旧链路清理、房间退出/切房回归完成
---
### 不同并行强度的建议
#### 最稳方案
- `3` 个主线程并行
- `1` 个收尾线程后置串行
适用场景:
- 你希望尽量减少互相撞车
- 你更在意一次性落稳,而不是追求最短墙钟时间
#### 更激进方案
- `4` 个线程一开始就并行
不太建议,原因是:
- `main.dart`
- `voice_room_page.dart`
- `rtm_manager.dart`
- `rtc_manager.dart`
这些文件很容易形成高冲突区,激进并行最后未必更快。
---
### 最终建议的并行执行口径
如果你后面准备“分别跑”多个线程,建议直接按下面口径执行:
- 线程 A只做 engine/domain/application 基础层
- 线程 B只做 stage/presentation/layer 承载层
- 线程 C只做旧入口收口和事件适配
- 线程 D最后统一清理旧链路、改挂载、跑回归
这是这次语言房特效系统重构里,**最稳、最不容易互相打架**的一种并行拆法。
---
## 可直接投喂 Codex 的执行 Prompts
下面这组 prompt 是给你后面开多个 Codex 线程时直接复制使用的。
推荐使用方式:
1. 先把“公共约定 prompt”复制给每个线程
2. 再把对应线程的专属 prompt 追加给该线程
3. 线程 D 不要一开始就跑,等 A/B/C 合并后再启动
### 公共约定 Prompt
建议把下面这段先发给每一个线程,作为共同上下文。
```text
你现在在 /Users/nigger/Documents/GitHub/chatapp3-flutter 仓库中工作。
当前目标不是重构整个语言房,而是优先重构“语言房特效子系统”。
请先阅读根目录文档:
- /Users/nigger/Documents/GitHub/chatapp3-flutter/语言房重构.md
本轮必须遵守这些硬约束:
1. 尽量保留当前礼物业务逻辑、阈值和视觉表现,不要擅自改业务口径。
2. 不要改房间消息协议,不要改后端接口。
3. 只在你被授权的写入范围内改文件,不要碰其它线程的主要负责文件。
4. 不要大规模格式化无关文件,不要顺手重构无关模块。
5. 如果你发现某个接口命名需要和其它线程统一,请优先遵守本文档中的统一命名,不要自行发明第二套。
统一命名约定如下:
- RoomEffectEvent
- RoomEffectTask
- RoomEffectLane
- RoomEffectPriority
- RoomEffectEngine
- RoomEffectPolicy
- RoomEffectEventAdapter
- RoomEffectStage
统一 lane 命名如下:
- fullscreen
- entrance
- giftTicker
- seatFlight
- floating
必须保留的当前关键逻辑:
- 顶部礼物播报条继续保持 4 个槽位
- 顶部礼物播报条待播上限继续按当前 24 处理
- 顶部礼物播报条空闲消失时长继续保持约 3200ms
- 飞向麦位继续保留 customAnimationCount 的粒度逻辑
- 幸运礼物 burst 继续保持“倍率 >= 10x 或单次奖励 > 5000”
- 房间礼物飘屏继续保持 coins > 9999 才触发
- 继续沿用当前特效总开关:
- SCGlobalConfig.isGiftSpecialEffects
- SCGlobalConfig.isEntryVehicleAnimation
- SCGlobalConfig.isFloatingAnimationInGlobal
完成后请在回复中明确给出:
- 改了哪些文件
- 你负责范围内已经完成了什么
- 还依赖其它线程提供什么
- 有没有你刻意没有动的风险点
```
---
### 线程 A Prompt
用途:
- 负责 engine/domain/application 基础层
直接投喂:
```text
在遵守“公共约定 prompt”的前提下执行线程 A 的任务。
你的职责是:搭建语言房特效系统的 engine/domain/application 基础层,只负责新架构骨架,不接老的 RTM/RTC 入口,不改现有房间业务触发逻辑。
你的主要目标:
1. 新增语言房特效模块基础目录,优先建议放在:
- lib/modules/room_effect/domain/
- lib/modules/room_effect/application/
- lib/modules/room_effect/infrastructure/
2. 建立并实现以下核心对象或等价对象:
- RoomEffectEvent
- RoomEffectTask
- RoomEffectLane
- RoomEffectPriority
- RoomEffectPolicy
- RoomEffectEngine
- RoomEffectDeduplicator
- RoomEffectMerger
- RoomEffectMetrics
- RoomEffectLifecycleGuard
- RoomEffectAssetPreloader
3. engine 至少要具备这些能力:
- 接收 RoomEffectEvent
- 转换为 RoomEffectTask
- 按 lane 分发
- 基于 priority 排序
- 留出去重、合并、丢弃、延后、独占的接口
- 能产出每个 lane 的可消费状态或流
4. 你可以先做“最小可用骨架”,但不要只写空壳文件。至少保证接口能表达真实系统能力。
5. 你需要把这些现有业务约束沉淀到 policy 或 constants 中,但不要改业务阈值:
- giftTicker 4 槽位
- pending 24
- idle dismiss 3200ms
- burst 条件
- 飘屏阈值
你的明确写入范围:
- 可以新增和修改:
- lib/modules/room_effect/domain/**
- lib/modules/room_effect/application/**
- lib/modules/room_effect/infrastructure/**
你的禁止写入范围:
- 不要修改:
- lib/services/audio/rtm_manager.dart
- lib/services/audio/rtc_manager.dart
- lib/modules/room/voice_room_page.dart
- lib/main.dart
- lib/ui_kit/widgets/room/** 的现有渲染文件
你的产出要求:
- 代码必须可编译到“接口层面合理”
- 保持命名稳定,方便线程 B/C 对接
- 在最终回复里列出你定义的关键接口签名和文件列表
```
---
### 线程 B Prompt
用途:
- 负责 `RoomEffectStage` 和 presentation/layer 承载层
直接投喂:
```text
在遵守“公共约定 prompt”的前提下执行线程 B 的任务。
你的职责是:搭建语言房特效系统的 presentation/stage/layer 承载层,重点是把房间内特效层级和挂载方式结构化,但不要改旧的 RTM/RTC 入口文件。
你的主要目标:
1. 新增 presentation 模块,优先建议放在:
- lib/modules/room_effect/presentation/
- lib/modules/room_effect/presentation/layers/
2. 建立并实现以下对象或等价对象:
- RoomEffectStage
- RoomFullscreenEffectLayer
- RoomEntranceEffectLayer
- RoomGiftTickerEffectLayer
- RoomSeatFlightEffectLayer
- RoomFloatingEffectLayer
3. 你的核心任务不是重写所有动画,而是先把“承载层和消费接口”搭好。首轮允许通过 bridge 复用现有 renderer。
4. 你需要明确 layer 的顺序和职责,建议至少能表达:
- fullscreen 层
- entrance 层
- giftTicker 层
- seatFlight 层
- floating 层
5. 你可以为每个 layer 设计最小消费接口,例如:
- 接收 RoomEffectTask 列表
- 接收 lane snapshot
- 接收 controller / stream / notifier
6. 你需要尽量让 stage 未来可以挂回 VoiceRoomPage 内部,但本线程先不要大改 voice_room_page.dart。
你的明确写入范围:
- 可以新增和修改:
- lib/modules/room_effect/presentation/**
- 如有必要,可少量只读参考:
- lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart
- lib/ui_kit/widgets/room/anim/room_entrance_screen.dart
- lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart
- lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart
你的禁止写入范围:
- 不要修改:
- lib/services/audio/rtm_manager.dart
- lib/services/audio/rtc_manager.dart
- lib/modules/room/voice_room_page.dart
- lib/main.dart
- 线程 A 负责的新 engine/domain/application 文件
你的产出要求:
- 交付可读、清晰的 RoomEffectStage 与 layer 结构
- 接口命名必须对齐公共约定
- 在最终回复中列出:
- 你新增的 layer 文件
- 每个 layer 准备承接哪类任务
- 还依赖线程 A 提供哪些 engine 能力
```
---
### 线程 C Prompt
用途:
- 负责旧链路入口收口与适配层
直接投喂:
```text
在遵守“公共约定 prompt”的前提下执行线程 C 的任务。
你的职责是:把现有语言房中的特效触发入口统一改成“事件投递”,尽量不碰 presentation 层结构,不大改渲染细节。
你的主要目标:
1. 梳理并改造当前这些文件里的直接特效触发点:
- lib/services/audio/rtm_manager.dart
- lib/services/audio/rtc_manager.dart
- lib/modules/room/voice_room_page.dart
- lib/services/gift/gift_system_manager.dart
2. 找出以下直接触发方式并逐步收口:
- 直接 SCGiftVapSvgaManager().play(...)
- 直接 OverlayManager().addMessage(...)
- 直接往独立动画队列入队
- 页面层自己拼装多个动画副作用
3. 你的目标不是立刻删掉所有旧逻辑,而是先把“触发入口”统一改成投递 RoomEffectEvent 或等价事件。
4. 你需要新增或实现适配层,建议命名:
- RoomEffectEventAdapter
- 或等价 wiring 文件
5. 需要特别注意去重场景:
- 本地自己进房 + 群消息回流
- 本地自己送礼 + 房间消息回流
- 幸运礼物广播 + 房间群消息双到达
6. 必须保留当前礼物业务判断口径,不要修改:
- 哪些礼物触发全屏
- 哪些礼物触发飘屏
- customAnimationCount 的使用方式
- 幸运礼物 burst 口径
你的明确写入范围:
- 可以修改:
- lib/services/audio/rtm_manager.dart
- lib/services/audio/rtc_manager.dart
- lib/modules/room/voice_room_page.dart
- lib/services/gift/gift_system_manager.dart
- 可以新增:
- lib/modules/room_effect/application/room_effect_event_adapter.dart
- 或你认为更合适但职责清晰的 adapter/binding 文件
你的禁止写入范围:
- 不要修改:
- lib/main.dart
- lib/modules/room_effect/presentation/**
- 线程 B 新增的 stage/layer 文件
- 线程 A 的核心 engine 接口定义
你的产出要求:
- 最终让旧入口尽量先统一走事件
- 即便 engine/stage 还未最终接上,也要把触发点收成统一入口
- 在最终回复中列出:
- 你改掉了哪些直接触发点
- 每类事件映射成了什么 RoomEffectEvent
- 哪些旧逻辑你暂时保留未删
```
---
### 线程 D Prompt
用途:
- A/B/C 合并后再启动,负责收尾、清理、挂载调整与回归
直接投喂:
```text
在遵守“公共约定 prompt”的前提下执行线程 D 的任务。
注意:这个线程默认在 A/B/C 已经完成并合并后再启动。你的职责是最终收尾,不是从零搭骨架。
你的主要目标:
1. 把房间专属特效层尽量从根层收回房间内部,重点检查:
- lib/main.dart
- lib/modules/room/voice_room_page.dart
2. 清理旧双轨逻辑,重点检查:
- lib/ui_kit/widgets/room/anim/room_entrance_widget.dart
- 旧的 RoomEntranceHelper 残留引用
- 不再需要的直接 play/addMessage 入口
3. 统一房间生命周期清理:
- 退房
- 最小化
- 切房
- 房间页面销毁
4. 回归以下重点问题:
- 房间退出后礼物图或飞向麦位残留
- 房间飘屏跑到首页
- 进场与全屏特效并发时队列紊乱
- 幸运礼物逻辑回归
5. 你可以少量补充 metrics 或 debug log但不要无限制新增临时调试代码。
你的明确写入范围:
- 可以修改:
- lib/main.dart
- lib/modules/room/voice_room_page.dart
- lib/shared/data_sources/sources/local/floating_screen_manager.dart
- lib/shared/tools/sc_room_effect_scheduler.dart
- lib/ui_kit/widgets/room/anim/room_entrance_widget.dart
- 其它你确认属于“旧链路清理 / 最终挂载调整 / 生命周期回收”的文件
你的禁止写入范围:
- 不要推翻线程 A 已经稳定的 engine 核心接口
- 不要重命名线程 B 已经落好的 presentation 核心对象
- 不要重新发明第二套 adapter 或第二套 lane
你的产出要求:
- 最终让新链路成为主链路
- 旧链路要么删除,要么明确降级成只读残留且不再实际生效
- 在最终回复中列出:
- 你清理了哪些旧链路
- 你改了哪些根层/房间层挂载
- 你验证了哪些关键回归项
- 你认为还剩哪些适合下一轮继续处理
```
---
### 推荐投喂顺序
建议你后面实际开线程时,按这个顺序喂:
1. 给 A、B、C 同时发送“公共约定 prompt”
2. 再分别发送各自的线程 prompt
3. 等 A、B、C 产出后完成合并
4. 最后再给 D 发送“公共约定 prompt + 线程 D prompt”
### 推荐合并检查点
在真正合并 A/B/C 前,建议你人工确认一次这几个点:
- A 的对象命名和 B/C 预期一致
- B 的 stage/layer 命名没有另起炉灶
- C 没有继续保留太多直接 `play()``addMessage()` 的新入口
- 三边没有同时改坏 `voice_room_page.dart / rtm_manager.dart / rtc_manager.dart`
---
## 最终建议
最终建议仍然是:
- **先重构语言房特效系统**
- **不先重构整套语言房**
- **先统一入口、统一引擎、统一生命周期**
- **尽量保留当前礼物业务逻辑与视觉**
这样做的收益最大,风险也最可控。
如果后续按本文实施,建议第一步从“阶段 0 + 阶段 1”开始
- 先建事件模型和指标
- 再把所有直接触发动画的入口统一改成投递事件
一旦这一步完成,后面每条特效链路都可以逐条平滑迁移,不需要再一次性大改整间房。