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

45 KiB
Raw Blame History

语言房重构方案

文档目的

本文只记录一套可执行的重构方案,本轮不改业务代码。

本次建议的重构范围不是“整套语言房全部推倒重写”,而是优先重构语言房特效子系统,重点解决以下问题:

  • 礼物特效、进场、飞向麦位、房间飘屏并发时主线程抢占严重
  • 特效触发源分散,队列和调度分散,后续越优化越难维护
  • 房间内特效偶发残留到首页/其它页面,生命周期边界不清晰
  • 已做过多轮局部优化,但整体架构仍然偏重,继续打补丁收益会越来越低

本方案的核心原则是:

  • 尽可能保留当前礼物业务逻辑、素材、阈值和视觉表现
  • 先重构特效系统,不一次性推倒 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 表达方式 保留视觉,不一定保留旧实现

建议保留但从硬编码改为配置化的参数

  • 礼物播报槽位数
  • 礼物播报待播上限
  • 进场队列上限
  • 飞向麦位单会话上限
  • 高成本特效队列上限
  • 飘屏丢弃阈值
  • 各类优先级与独占关系

目标架构

架构总览

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/

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,只挂在语言房页面内:

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 先不迁
  • SCGiftVapSvgaManagerGiftAnimationManager 等先照旧使用

关键收益:

  • 从这一阶段开始,特效来源就被收口了

阶段 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 设计过粗,会把应播事件吃掉 区分 dedupKeymergeKey,先留日志,初期采用保守去重
房间退出后仍有旧特效回调 postFrame / delayed / timer / precache 都可能晚到 所有 task 和 layer 引入 sessionTokenroomLifecycleToken 校验
渲染层迁移后层级错乱 根层迁到房间层后z-order 可能改变 RoomEffectStage 中明确 layer 顺序,先对齐现有视觉层级
资源预热过多导致内存抖动 统一预热后若没有预算控制,容易顶高内存 按 lane、资源类型、尺寸做分层缓存和 LRU
幸运礼物口径漂移 这块逻辑复杂,回归风险高 首轮冻结参数与触发口径,不做业务变更
飞向麦位目标丢失 目标座位 GlobalKey 尚未可用时可能丢动画 保留当前重试机制,但统一纳入 lane限制重试次数
飘屏房间范围判断错误 房间飘屏与全局飘屏容易混 明确 roomScopedglobalScoped 两类事件
迁移阶段代码并存时间过长 双轨逻辑存在越久,后续越难收口 每一阶段都指定“完成后可删”的旧文件或旧入口
测试面过大 礼物、幸运礼物、火箭、红包、进场、坐骑交叉很多 用阶段化回归清单,按 lane 逐项验收

回归测试清单

礼物链路

  • 单次送礼 1 次
  • 同一礼物连续送 5 次、10 次
  • customAnimationCount > 1 的批量场景
  • 幸运礼物普通中奖
  • 幸运礼物高倍率中奖
  • 同时出现全屏礼物 + 顶部播报条 + 飞向麦位

进场链路

  • 自己进入房间
  • 别人进入房间
  • 别人带坐骑进入房间
  • 同时 5 人以上快速进入

飘屏链路

  • 房间礼物飘屏
  • 幸运礼物房间飘屏
  • 全局广播幸运礼物飘屏
  • 火箭飘屏
  • 红包飘屏

生命周期

  • 房间内最小化
  • 退出房间
  • 切到首页/消息/我的
  • 从 A 房切到 B 房

性能

  • 普通机型连续送礼 10 秒
  • 低性能机型连续送礼 10 秒
  • 进场和礼物叠加压测
  • 房间内停留 10 分钟观察缓存与内存变化

验收指标建议

建议在重构过程中记录以下指标,避免“感觉快了”但没有客观标准:

  • 语言房高压场景下 FrameTimingbuild/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.dartrtc_manager.dart
C rtm_manager.dartrtc_manager.dartvoice_room_page.dartgift_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
  • CRTM/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

建议把下面这段先发给每一个线程,作为共同上下文。

你现在在 /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 基础层

直接投喂:

在遵守“公共约定 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 承载层

直接投喂:

在遵守“公共约定 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

用途:

  • 负责旧链路入口收口与适配层

直接投喂:

在遵守“公共约定 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 合并后再启动,负责收尾、清理、挂载调整与回归

直接投喂:

在遵守“公共约定 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”开始

  • 先建事件模型和指标
  • 再把所有直接触发动画的入口统一改成投递事件

一旦这一步完成,后面每条特效链路都可以逐条平滑迁移,不需要再一次性大改整间房。