From c810e645dcf796cb6c71069cb179935987bbf1d3 Mon Sep 17 00:00:00 2001 From: roxy Date: Sat, 9 May 2026 16:55:08 +0800 Subject: [PATCH] =?UTF-8?q?bug=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/index/index_page.dart | 14 +++++++ lib/services/audio/rtc_manager.dart | 13 +++--- lib/services/audio/rtm_manager.dart | 36 ++++++++++++++++ .../local/floating_screen_manager.dart | 3 ++ .../repositories/sc_user_repository_impl.dart | 9 +++- lib/ui_kit/widgets/room/room_msg_item.dart | 42 +++++++++++++++++-- 需求进度.md | 2 + 7 files changed, 110 insertions(+), 9 deletions(-) diff --git a/lib/modules/index/index_page.dart b/lib/modules/index/index_page.dart index 62d4ef0..433a189 100644 --- a/lib/modules/index/index_page.dart +++ b/lib/modules/index/index_page.dart @@ -115,6 +115,12 @@ class _SCIndexPageState extends State if (state != AppLifecycleState.resumed) { return; } + unawaited( + Provider.of( + context, + listen: false, + ).syncRoomRedPacketBroadcastGroup(), + ); WidgetsBinding.instance.addPostFrameCallback((_) { unawaited(_showEntryPopupIfNeeded()); }); @@ -163,6 +169,14 @@ class _SCIndexPageState extends State return; } OverlayManager().setHomeRootTabsVisible(visible); + if (visible) { + unawaited( + Provider.of( + context, + listen: false, + ).syncRoomRedPacketBroadcastGroup(), + ); + } } @override diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index 378a646..32b0b0e 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -3033,6 +3033,12 @@ class RealTimeCommunicationManager extends ChangeNotifier { } final normalizedPacketId = packetId.trim(); final normalizedSenderName = packetSenderName.trim(); + final currentUser = AccountStorage().getCurrentUser()?.userProfile; + final currentUserId = (currentUser?.id ?? "").trim(); + final dedupKey = + normalizedPacketId.isNotEmpty && currentUserId.isNotEmpty + ? 'room-red-packet-claim-$normalizedPacketId-$currentUserId' + : null; unawaited( rtmProvider?.dispatchMessage( Msg( @@ -3041,11 +3047,8 @@ class RealTimeCommunicationManager extends ChangeNotifier { type: SCRoomMsgType.roomRedPacketClaim, role: currenRoom?.entrants?.roles ?? "", number: amount, - giftBatchId: - normalizedPacketId.isEmpty - ? null - : 'room-red-packet-claim-$normalizedPacketId', - user: AccountStorage().getCurrentUser()?.userProfile, + giftBatchId: dedupKey, + user: currentUser, ), ) ?? Future.value(), diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index 833a710..8cf06f7 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -1164,6 +1164,13 @@ class RealTimeMessagingManager extends ChangeNotifier { ); return; } + if (msg.type == SCRoomMsgType.roomRedPacketClaim && + _hasRoomRedPacketClaimMessage(msg)) { + debugPrint( + '[RoomRedPacket][Claim] skip duplicate key=${_roomRedPacketClaimMessageKey(msg)}', + ); + return; + } roomAllMsgList.insert(0, msg); if (roomAllMsgList.length > 250) { @@ -1280,6 +1287,35 @@ class RealTimeMessagingManager extends ChangeNotifier { return null; } + bool _hasRoomRedPacketClaimMessage(Msg msg) { + final key = _roomRedPacketClaimMessageKey(msg); + if (key == null || key.isEmpty) { + return false; + } + return roomAllMsgList.any( + (item) => + item.type == SCRoomMsgType.roomRedPacketClaim && + _roomRedPacketClaimMessageKey(item) == key, + ); + } + + String? _roomRedPacketClaimMessageKey(Msg msg) { + if (msg.type != SCRoomMsgType.roomRedPacketClaim) { + return null; + } + final stableKey = (msg.giftBatchId ?? '').trim(); + if (stableKey.isNotEmpty) { + return stableKey; + } + final userId = (msg.user?.id ?? '').trim(); + final amount = msg.number?.toString() ?? ''; + final time = msg.time?.toString() ?? ''; + if (userId.isEmpty || amount.isEmpty || time.isEmpty) { + return null; + } + return ['legacy', userId, msg.msg ?? '', amount, time].join('|'); + } + Msg? _mergeGiftMessageIfNeeded(Msg incoming) { if (incoming.type != SCRoomMsgType.gift && incoming.type != SCRoomMsgType.luckGiftAnimOther) { diff --git a/lib/shared/data_sources/sources/local/floating_screen_manager.dart b/lib/shared/data_sources/sources/local/floating_screen_manager.dart index ecbefac..d19145a 100644 --- a/lib/shared/data_sources/sources/local/floating_screen_manager.dart +++ b/lib/shared/data_sources/sources/local/floating_screen_manager.dart @@ -563,6 +563,9 @@ class OverlayManager { if (message.type == 4 && rtcProvider.shouldShowRoomVisualEffects) { return true; } + if (message.type == 4) { + return _homeRootTabsVisible; + } if (SCGlobalConfig.isFloatingBroadcastRoomOnly) { return false; } diff --git a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart index 6cd5300..7d45a01 100644 --- a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart +++ b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart @@ -312,10 +312,17 @@ class SCAccountRepository implements SocialChatUserRepository { ///user/user-profile @override - Future loadUserInfo(String userId) async { + Future loadUserInfo( + String userId, { + bool silentErrorToast = false, + }) async { final result = await http.get( "2fa1d4f56bb726558904ce2a50b83f961e1cf9b945681bdfb3d4cf7a5ecfc943", queryParams: {"userId": userId}, + extra: + silentErrorToast + ? const {BaseNetworkClient.silentErrorToastKey: true} + : null, fromJson: (json) => SocialChatUserProfile.fromJson(json), ); return result; diff --git a/lib/ui_kit/widgets/room/room_msg_item.dart b/lib/ui_kit/widgets/room/room_msg_item.dart index 96cbccd..584161f 100644 --- a/lib/ui_kit/widgets/room/room_msg_item.dart +++ b/lib/ui_kit/widgets/room/room_msg_item.dart @@ -51,6 +51,11 @@ class MsgItem extends StatefulWidget { } class _MsgItemState extends State { + static const Duration _userInfoFailureRetryInterval = Duration(minutes: 5); + static final Map> _userInfoFutures = + {}; + static final Map _userInfoFailedAt = {}; + @override Widget build(BuildContext context) { //gift @@ -1651,14 +1656,19 @@ class _MsgItemState extends State { final userId = widget.msg.user?.id; if ((widget.msg.needUpDataUserInfo) ?? false) { SCRoomUtils.roomUsersMap.remove(userId ?? ""); + if ((userId ?? "").isNotEmpty) { + _userInfoFutures.remove(userId); + _userInfoFailedAt.remove(userId); + } + widget.msg.needUpDataUserInfo = false; } if (userId == null || userId.isEmpty) { return _buildPendingUserInfoLine(context); } return SCRoomUtils.roomUsersMap[userId] == null - ? FutureBuilder( - future: SCAccountRepository().loadUserInfo(userId), - builder: (ct, AsyncSnapshot snapshot) { + ? FutureBuilder( + future: _loadRoomUserInfoSilently(userId), + builder: (ct, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasData) { SocialChatUserProfile user = snapshot.data!; @@ -1672,6 +1682,32 @@ class _MsgItemState extends State { : _buildNewUserInfoLine(context, SCRoomUtils.roomUsersMap[userId]!); } + Future _loadRoomUserInfoSilently(String userId) { + final failedAt = _userInfoFailedAt[userId]; + if (failedAt != null && + DateTime.now().difference(failedAt) < _userInfoFailureRetryInterval) { + return Future.value(null); + } + return _userInfoFutures.putIfAbsent(userId, () async { + try { + final user = await SCAccountRepository().loadUserInfo( + userId, + silentErrorToast: true, + ); + SCRoomUtils.roomUsersMap[user.id ?? userId] = user; + _userInfoFailedAt.remove(userId); + return user; + } catch (error) { + _userInfoFailedAt[userId] = DateTime.now(); + _userInfoFutures.remove(userId); + debugPrint( + '[RoomChat][UserInfo] load failed userId=$userId error=$error', + ); + return null; + } + }); + } + Widget _buildPendingUserInfoLine(BuildContext context) { return Row( children: [ diff --git a/需求进度.md b/需求进度.md index 89b6ae9..29fcae4 100644 --- a/需求进度.md +++ b/需求进度.md @@ -560,6 +560,8 @@ - 已修正延时红包多入口数据不一致:公屏红包消息会同步写入待领缓存,右侧入口和领取弹层遇到同一 `packetId` 会合并接口列表、缓存和聊天 payload 的倒计时字段,避免飘屏 `GO`/右侧入口丢失倒计时;领取成功后右侧入口会优先按本地已领状态隐藏,避免接口刷新滞后导致残留。 - 已补齐红包发送后的 IM 广播:发送成功后除本机兜底展示外,会把 `{type: ROOM_RED_PACKET, data: payload}` 发送到当前房间 IM 群和当前语区 IM 群;同房用户通过房间群看到公屏/飘屏,房外或其它房同语区用户通过语区群看到飘屏,并继续依赖 `packetId` 去重避免重复展示。 - 已修正红包领取成功公屏消息的多语言问题:领取后不再发送已翻译好的普通文本,改为 `ROOM_RED_PACKET_CLAIM` 结构化房间 IM,只传领取金额、红包发送人名和 `packetId`,公屏渲染时按当前客户端语言生成文案,避免英文用户看到阿语消息。 +- 已补强房外延时红包飘屏和领取通知去重:首页四个一级 Tab 可见/回到前台时会重新同步当前语区红包 IM 群;语区红包飘屏在首页 Tab 中不再受“只看房间飘屏”设置影响;领取成功公屏通知按 `packetId + 领取人ID` 生成稳定去重键,避免本地插入和 IM 回包/历史拉取造成两条一模一样的领取消息。 +- 已定位并修复公屏上滑历史反复弹 `user info not found`:历史消息 item 重建时会重复调用用户资料接口,缺失用户返回错误后被网络层 toast;现在房间公屏补拉用户资料改为静默请求,并对同一 userId 的请求/失败结果做缓存,5 分钟内不再反复请求同一个不存在用户。 ## 已知问题 - 已接入语音房表情包素材第一版:桌面 `表情素材` 中的 `fluent_emoji` 与 `monkey` gif 已纳入项目资源,底部聊天入口改为图稿里的短白色胶囊并移除内部输入图标,右侧新增独立表情按钮;同一个房间输入弹层现在可从表情按钮直接打开表情包面板,原 unicode emoji 面板已替换为本地 gif 表情。发送逻辑同步按最新规则分流:用户在麦上发表情时只在自己麦位播放,不进入聊天列表;用户未上麦发表情时进入房间聊天框展示,不触发麦位动画。