379 lines
14 KiB
Markdown
379 lines
14 KiB
Markdown
# 语音房大额礼物全局飘窗方案
|
||
|
||
整理时间: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 和时间。
|
||
- TTL:30 秒到 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` 或本地化的“语音房”。
|
||
|