14 KiB
语音房大额礼物全局飘窗方案
整理时间: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:FloatingLuckGiftScreenWidgettype = 1:FloatingGiftScreenWidgettype = 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. 推荐总体方案
推荐采用:
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:
{
"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(...) 增加:
} 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(...) 增加:
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 做当前房间匹配:
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 点击继续复用:
SCRoomUtils.goRoom(
message.roomId!,
navigatorKey.currentState!.context,
fromFloting: true,
);
现有 SCRoomUtils.goRoom(...) 已处理:
- 当前没有房间:弹确认后进房。
- 当前房间最小化且房间相同:打开当前房间。
- 当前已在其他房间:弹确认切房。
这里不需要新增路由能力。
6. 客户端兜底方案
如果后端暂时不能推送 BIG_GIFT,可以做临时客户端兜底,但不推荐作为长期方案。
临时做法:
- 在
GiftPage._executeGiftRequest(...)普通礼物成功后判断:!request.isLuckyGiftRequestgiftTab不是LUCK / LUCKY_GIFT / MAGICgift.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. 建议实施顺序
- 和后端确认
BIG_GIFT全服广播协议、阈值、是否包含roomName / giftName / giftPhoto / eventId。 - 新增
SCBigGiftBroadcastPush模型和SCFloatingMessage扩展字段。 - 新增
FloatingBigGiftScreenWidget,复用现有动画、点击、左滑行为。 OverlayManager支持type = 6,并新增首页四 Tab 可见性控制。SCIndexPage用RouteAware控制OverlayManager的主 Tab 展示作用域。RtmProvider._newBroadCastMsgRecv(...)接入BIG_GIFT。- 加去重和队列上限保护。
- 做手动验收与必要 Widget/单元测试。
11. 需要确认的问题
- 大额阈值最终是多少:沿用
10000,还是后端配置? - 飘窗是否展示收礼人:当前需求文案不需要,只写“用户 A 在房间 B 送了 C 礼物”。
- 多目标送礼是否只展示一次:建议只展示一次。
- 如果发送者自己也在首页四 Tab,是否也展示自己刚送的大额礼物:建议展示,和全服广播一致;如产品不希望自显,需要后端或前端按
sendUserId == currentUserId过滤。 - 房间 B 名称为空时的兜底文案:建议使用
roomId或本地化的“语音房”。