yumi-flutter/docs/voice-room-big-gift-global-floating-plan.md
2026-05-07 10:06:02 +08:00

379 lines
14 KiB
Markdown
Raw Permalink 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.

# 语音房大额礼物全局飘窗方案
整理时间2026-05-06
适用范围:语音房普通大额礼物飘窗;只在一级页面 `home / explore / message / me` 展示;点击进入对应房间。
本轮只做方案整理,不改业务代码。
## 1. 需求目标
在用户送出大额礼物时,新增一个顶部飘窗,展示类似:
> 用户 A 在房间 B 送了 C 礼物
要求:
- 礼物类型:普通礼物大额送礼,明确排除幸运礼物、魔法礼物等走 `giveLuckyGift` 的礼物。
- 展示范围:全局一级页面,但只限首页主框架内的四个 Tab`home / explore / message / me`
- 不在语音房页面、二级页面、设置页、充值页、聊天详情页等页面展示。
- 点击飘窗后进入具体房间。
- 飘窗位置沿用当前幸运礼物/礼物飘屏顶部位置。
## 2. 当前代码现状
### 2.1 顶部飘窗能力已存在
现有飘窗由 `OverlayManager` 统一调度:
- 文件:`lib/shared/data_sources/sources/local/floating_screen_manager.dart`
- 入口:`OverlayManager().addMessage(SCFloatingMessage)`
- 队列:`SCPriorityQueue<SCFloatingMessage>`,按 `priority` 排序。
- 位置:`OverlayEntry` 使用 `Align(topStart)`,再 `Transform.translate(offset: Offset(0, 70.w))`
- 动画组件:
- `type = 0``FloatingLuckGiftScreenWidget`
- `type = 1``FloatingGiftScreenWidget`
- `type = 2`:游戏中奖
- `type = 3`:火箭
- `type = 4`:红包
- `type = 5`VIP 进房
现有 `FloatingLuckGiftScreenWidget``FloatingGiftScreenWidget` 都已经支持:
- 从右往左飘过。
- 左滑快速关闭。
- 点击后调用 `SCRoomUtils.goRoom(roomId, context, fromFloting: true)` 进入房间。
### 2.2 现有普通礼物飘窗是房间内逻辑
普通礼物发送成功后会进入 `GiftPage.sendGiftMsg(...)`
- 文件:`lib/modules/gift/gift_page.dart`
- 当前判断:`gift.giftCandy * quantity > 9999` 时创建 `SCFloatingMessage(type: 1)`
- 当前内容:用户 A 送给用户 B 礼物,字段偏向 `toUserName / toUserAvatarUrl`
- 当前触发范围:发送者本地和当前房间 RTM 收到 `SCRoomMsgType.gift` 后触发。
这条逻辑不适合直接作为“全局大额礼物飘窗”:
- 它是房间消息,不是全服消息。
- 它按收礼人循环,可能一次多目标送礼触发多条。
- UI 文案是“send to 某用户”,不是“在房间 B 送了 C 礼物”。
- `OverlayManager._shouldDisplayMessage(...)` 当前对 `type = 0 / 1 / 5` 要求必须匹配当前房间 ID否则不展示。
### 2.3 全服广播通道已存在
RTM 初始化后会加入全服广播群:
- 文件:`lib/services/audio/rtm_manager.dart`
- `init(...)` 中调用 `joinBigBroadcastGroup()`
- 全服消息在 `_newBroadCastMsgRecv(...)` 处理
- 当前已处理:
- `GAME_LUCKY_GIFT`
- 游戏中奖
- 火箭
- 红包
因此新增大额普通礼物全局飘窗时,最合适的入口是全服广播群,而不是房间群消息。
## 3. 推荐总体方案
推荐采用:
```mermaid
flowchart LR
A["用户在房间送普通礼物"] --> B["后端校验送礼成功"]
B --> C{"是否普通大额礼物"}
C -- "否" --> D["只走现有房间送礼消息"]
C -- "是" --> E["后端推送 BIG_GIFT 到全服广播群"]
E --> F["客户端 _newBroadCastMsgRecv 接收"]
F --> G{"当前是否首页四个一级 Tab"}
G -- "否" --> H["丢弃,不入队"]
G -- "是" --> I["OverlayManager 入队展示顶部飘窗"]
I --> J["点击 SCRoomUtils.goRoom(roomId)"]
```
核心点:
- 大额判断放后端做,前端只负责展示,避免客户端伪造全服大礼物消息。
- 前端新增独立消息类型,例如 `BIG_GIFT`,不复用幸运礼物 `GAME_LUCKY_GIFT`
- 前端新增独立飘窗类型,例如 `SCFloatingMessage.type = 6`,避免影响当前房间内 `type = 1` 的普通礼物飘窗。
- 展示作用域由 `OverlayManager` 或主框架路由状态控制,只允许首页四 Tab 展示。
## 4. 服务端数据建议
建议后端在普通礼物接口送礼成功后判断是否需要推送全服广播:
- 接口链路:`/gift/batch`,前端对应 `SCChatRoomRepository.giveGift(...)`
- 排除链路:`/gift/give/lucky-gift`,前端对应 `giveLuckyGift(...)`
- 推荐阈值:沿用客户端现有房间内逻辑 `totalCoins >= 10000`,最终以产品/后端配置为准。
- 推荐只推送一次送礼行为,不按收礼人数量重复推送。
推荐全服广播 payload
```json
{
"type": "BIG_GIFT",
"data": {
"eventId": "giftSendRecordId or unique id",
"roomId": "room id",
"roomName": "room name",
"sendUserId": "sender user id",
"sendUserName": "sender nickname",
"sendUserAvatar": "sender avatar",
"giftId": "gift id",
"giftName": "gift name",
"giftPhoto": "gift cover url",
"giftQuantity": 1,
"giftCandy": 10000,
"totalCoins": 10000,
"timestamp": 1710000000000
}
}
```
字段说明:
- `eventId`:用于客户端去重。没有则用 `roomId + sendUserId + giftId + giftQuantity + timestamp` 做兜底 key。
- `roomId`:点击飘窗进房必须字段。
- `roomName`:满足“在房间 B”的展示需求。
- `giftName / giftPhoto`:满足“送了 C 礼物”的展示和礼物图标展示。
- `totalCoins`:可用于优先级、样式分级或埋点。
## 5. 前端改造点
### 5.1 新增大额礼物广播模型
建议新增模型:
- `lib/shared/business_logic/models/res/sc_big_gift_broadcast_push.dart`
字段对应服务端 payload。不要把所有字段硬塞进 `SCBroadCastLuckGiftPush`,幸运礼物模型语义不同。
也可以扩展 `SCFloatingMessage`
- 新增 `roomName`
- 新增 `giftName`
- 新增 `eventId`
- 保留 `roomId / userName / userAvatarUrl / giftUrl / giftId / number / coins / priority`
### 5.2 RTM 全服广播接入
`RealTimeMessagingManager._newBroadCastMsgRecv(...)` 增加:
```dart
} else if (type == "BIG_GIFT") {
final push = SCBigGiftBroadcastPush.fromJson(data);
_handleBigGiftGlobalNews(push);
}
```
`_handleBigGiftGlobalNews(...)` 推荐职责:
- 校验 `roomId``sendUserName``giftName` 至少有可展示值。
- 排除幸运礼物或服务端标记的非普通礼物。
- 去重。
- 组装 `SCFloatingMessage(type: 6, priority: 900, ...)`
- 调用 `OverlayManager().addMessage(...)`
优先级建议:
- 幸运礼物:当前 `priority = 1000`
- 大额普通礼物:建议 `priority = 900`
- 普通房间礼物本地飘窗:保持默认或现有逻辑。
### 5.3 新增大额礼物飘窗 Widget
建议新增:
- `lib/ui_kit/widgets/room/floating/floating_big_gift_screen_widget.dart`
原因:
- 当前 `FloatingGiftScreenWidget` 展示的是“用户 A sendTo 用户 B + 礼物图标 x 数量”。
- 新需求是“用户 A 在房间 B 送了 C 礼物”。
- 新增 Widget 可以复用现有动画、点击、左滑关闭逻辑,但不影响房间内普通礼物飘窗。
UI 建议:
- 保持同一位置和相近尺寸:`height 54.w` 左右,`width 320.w - 350.w`
- 左侧:送礼用户头像和昵称。
- 中间:房间名和文案,例如 `在 {roomName} 送出`
- 右侧:礼物图标 + 礼物名,可保留 `x{quantity}`
- 长昵称、长房间名、长礼物名使用 `TextOverflow.ellipsis` 或局部跑马灯,避免遮挡。
- RTL 语言需要检查布局方向,至少保持点击和左滑逻辑与现有组件一致。
`OverlayManager._buildScreenWidget(...)` 增加:
```dart
case 6:
return FloatingBigGiftScreenWidget(
message: message,
onAnimationCompleted: onComplete,
);
```
### 5.4 展示作用域:只允许首页四 Tab
当前 `OverlayManager` 是根 Overlay 展示,天然能覆盖全 App。需要新增“允许展示作用域”否则可能在二级页、房间页也展示。
推荐方案:由 `SCIndexPage` 作为主框架控制作用域。
实现思路:
- `SCIndexPage` 接入 `RouteAware`,订阅全局 `routeObserver`
-`SCIndexPage` 是栈顶时,设置 `OverlayManager().setMainTabsVisible(true)`
- 当从首页 push 到任意二级页面时,`didPushNext` 设置为 false。
- 当从二级页返回首页时,`didPopNext` 设置为 true。
- `SCIndexPage.dispose` 设置为 false。
- `OverlayManager.addMessage(...)``_shouldDisplayMessage(...)``type = 6` 判断 `mainTabsVisible == true`,否则直接丢弃或不入队。
不推荐只用 `navigator.canPop()`
- 可以作为兜底,但语义不够清晰。
- 登录页、启动页也可能是根页面,不能等同于首页四 Tab。
### 5.5 调整现有展示过滤
当前 `OverlayManager._shouldDisplayMessage(...)``type = 0 / 1 / 5` 做当前房间匹配:
```dart
if (message.type != 0 && message.type != 1 && message.type != 5) {
return true;
}
...
return currentRoomId == messageRoomId;
```
新增 `type = 6` 时不要走当前房间匹配,而是走首页四 Tab 匹配:
- `type = 6`:只判断首页四 Tab 是否可见、全局飘屏开关是否开启、必要字段是否完整。
- `type = 0 / 1 / 5`:保留现有房间匹配,避免破坏当前房间内特效体验。
- `type = 2 / 3 / 4`:按现有逻辑评估是否也需要作用域限制,本需求不建议顺手改。
### 5.6 点击进房
新 Widget 点击继续复用:
```dart
SCRoomUtils.goRoom(
message.roomId!,
navigatorKey.currentState!.context,
fromFloting: true,
);
```
现有 `SCRoomUtils.goRoom(...)` 已处理:
- 当前没有房间:弹确认后进房。
- 当前房间最小化且房间相同:打开当前房间。
- 当前已在其他房间:弹确认切房。
这里不需要新增路由能力。
## 6. 客户端兜底方案
如果后端暂时不能推送 `BIG_GIFT`,可以做临时客户端兜底,但不推荐作为长期方案。
临时做法:
-`GiftPage._executeGiftRequest(...)` 普通礼物成功后判断:
- `!request.isLuckyGiftRequest`
- `giftTab` 不是 `LUCK / LUCKY_GIFT / MAGIC`
- `gift.giftCandy * request.quantity >= threshold`
- 构造 `BigBroadcastGroupMessage("BIG_GIFT", SCFloatingMessage(...))`
- 调用 `RtmProvider.sendBigBroadcastGroup(...)`
- 必须只按一次送礼行为发送,不能放在 `sendGiftMsg(...)``acceptUsers` 循环里。
风险:
- 客户端可伪造全服广播,不够可信。
- 多端并发、弱网重试可能重复。
- 阈值和礼物类型判断容易与后端不一致。
因此正式方案仍建议后端推送。
## 7. 去重与队列策略
建议新增去重缓存:
- 位置:`RealTimeMessagingManager``OverlayManager`
- 结构:`LinkedHashMap<String, int>`,保存最近 100 条 event key 和时间。
- TTL30 秒到 60 秒。
- key 优先使用 `eventId`
队列策略:
- 继续使用 `OverlayManager` 现有优先级队列。
- 大额礼物 `priority = 900`,低于幸运礼物大奖 `1000`
- 可选:当队列过长时丢弃低优先级大额礼物,避免首页连续刷屏。
## 8. 开关策略
沿用现有全局飘屏开关:
- `SCGlobalConfig.isFloatingAnimationInGlobal`
- 本地持久化 key`FloatingAnimationInGlobal`
- 低性能设备会通过 `clampVisualEffectPreference(...)` 关闭。
建议:
- 大额礼物全局飘窗受这个开关控制。
- 不受 `isGiftSpecialEffects` 控制,因为它是全屏礼物特效开关。
- 不受 `isLuckGiftSpecialEffects` 控制,因为本需求不是幸运礼物。
## 9. 验收用例
### 9.1 基础展示
- 用户 A 在房间 B 送普通礼物 C总价值达到阈值。
- 当前用户在 `home`:看到顶部飘窗。
- 当前用户在 `explore`:看到顶部飘窗。
- 当前用户在 `message`:看到顶部飘窗。
- 当前用户在 `me`:看到顶部飘窗。
### 9.2 范围限制
- 当前用户在语音房页面:不展示该全局大额礼物飘窗。
- 当前用户在个人资料页、设置页、聊天详情页、充值页:不展示。
- App 在启动页或登录页:不展示。
### 9.3 礼物类型
- 普通礼物达到阈值:展示。
- 普通礼物未达到阈值:不展示。
- 幸运礼物或魔法礼物达到金额:不走 `BIG_GIFT`,继续走现有幸运礼物逻辑。
- 多人收礼的一次送礼:只展示一条。
### 9.4 点击进房
- 当前无房间:点击飘窗,确认后进入房间 B。
- 当前房间已最小化且是房间 B点击后打开当前房间。
- 当前已在其他房间:点击后弹切房确认。
- `roomId` 为空或房间不存在:不展示或进房失败时走现有错误提示。
### 9.5 稳定性
- 连续多条大额礼物:按队列顺序展示,不重叠。
- 同一 `eventId` 重复到达:只展示一次。
- 左滑关闭后下一条能继续展示。
- 关闭全局飘屏开关后不展示。
- 低性能设备默认不展示。
## 10. 建议实施顺序
1. 和后端确认 `BIG_GIFT` 全服广播协议、阈值、是否包含 `roomName / giftName / giftPhoto / eventId`
2. 新增 `SCBigGiftBroadcastPush` 模型和 `SCFloatingMessage` 扩展字段。
3. 新增 `FloatingBigGiftScreenWidget`,复用现有动画、点击、左滑行为。
4. `OverlayManager` 支持 `type = 6`,并新增首页四 Tab 可见性控制。
5. `SCIndexPage``RouteAware` 控制 `OverlayManager` 的主 Tab 展示作用域。
6. `RtmProvider._newBroadCastMsgRecv(...)` 接入 `BIG_GIFT`
7. 加去重和队列上限保护。
8. 做手动验收与必要 Widget/单元测试。
## 11. 需要确认的问题
- 大额阈值最终是多少:沿用 `10000`,还是后端配置?
- 飘窗是否展示收礼人:当前需求文案不需要,只写“用户 A 在房间 B 送了 C 礼物”。
- 多目标送礼是否只展示一次:建议只展示一次。
- 如果发送者自己也在首页四 Tab是否也展示自己刚送的大额礼物建议展示和全服广播一致如产品不希望自显需要后端或前端按 `sendUserId == currentUserId` 过滤。
- 房间 B 名称为空时的兜底文案:建议使用 `roomId` 或本地化的“语音房”。