diff --git a/lib/app/config/configs/sc_variant1_config.dart b/lib/app/config/configs/sc_variant1_config.dart index c31ecf2..e4502ee 100644 --- a/lib/app/config/configs/sc_variant1_config.dart +++ b/lib/app/config/configs/sc_variant1_config.dart @@ -16,7 +16,7 @@ class SCVariant1Config implements AppConfig { @override String get apiHost => const String.fromEnvironment( 'API_HOST', - defaultValue: 'http://192.168.110.64:1100/', + defaultValue: 'https://jvapi.haiyihy.com/', ); // 默认连本地接口,本地调试可通过 --dart-define=API_HOST 覆盖,线上“https://jvapi.haiyihy.com/” @override diff --git a/lib/shared/business_logic/models/res/sc_first_recharge_reward_res.dart b/lib/shared/business_logic/models/res/sc_first_recharge_reward_res.dart index 0b60918..35399dc 100644 --- a/lib/shared/business_logic/models/res/sc_first_recharge_reward_res.dart +++ b/lib/shared/business_logic/models/res/sc_first_recharge_reward_res.dart @@ -141,6 +141,7 @@ class SCFirstRechargeRewardItem { const SCFirstRechargeRewardItem({ required this.id, required this.type, + required this.detailType, required this.typeName, required this.name, required this.content, @@ -153,6 +154,7 @@ class SCFirstRechargeRewardItem { factory SCFirstRechargeRewardItem.fromJson(dynamic json) { final map = _asMap(json); final content = _readString(map['content']); + final detailType = _readRewardDetailType(map); final badgeText = _firstString(map, const [ 'badgeText', 'cornerText', @@ -176,6 +178,7 @@ class SCFirstRechargeRewardItem { return SCFirstRechargeRewardItem( id: _readString(map['id']), type: _readString(map['type']), + detailType: detailType, typeName: _firstString(map, const [ 'typeName', 'rewardTypeName', @@ -190,7 +193,7 @@ class SCFirstRechargeRewardItem { name: _readString(map['name']), content: content, quantity: _readInt(map['quantity']), - cover: _readString(map['cover']), + cover: _readRewardCover(map, detailType: detailType), badgeText: badgeText, days: explicitDays > 0 @@ -204,6 +207,7 @@ class SCFirstRechargeRewardItem { final String id; final String type; + final String detailType; final String typeName; final String name; final String content; @@ -238,6 +242,7 @@ class SCFirstRechargeRewardItem { } return _firstNonEmpty([ _inferRewardTypeName( + detailType: detailType, typeName: typeName, type: type, name: name, @@ -254,7 +259,52 @@ class SCFirstRechargeRewardItem { if (isCoinReward) { return ''; } - return '${days > 0 ? days : 1}d'; + final directBadgeText = badgeText.trim(); + if (directBadgeText.isNotEmpty) { + return directBadgeText; + } + if (isCountBasedReward) { + return 'x${quantity > 0 ? quantity : 1}'; + } + if (usesQuantityAsDuration) { + final durationDays = _firstPositiveInt([days, quantity]); + return '${durationDays > 0 ? durationDays : 1}d'; + } + return days > 0 ? '${days}d' : ''; + } + + bool get isCountBasedReward { + return _isGiftRewardType(_rewardTypeSearchText); + } + + bool get usesQuantityAsDuration { + final explicitTypeSearchText = + [ + detailType, + typeName, + type, + ].where((value) => value.trim().isNotEmpty).join(' ').toLowerCase(); + if (_isGiftRewardType(explicitTypeSearchText) || + _isBadgeRewardType(explicitTypeSearchText)) { + return false; + } + if (_usesQuantityAsDuration(explicitTypeSearchText)) { + return true; + } + if (_isBadgeRewardType(_rewardTypeSearchText)) { + return false; + } + return _usesQuantityAsDuration(_rewardTypeSearchText); + } + + String get _rewardTypeSearchText { + return [ + detailType, + typeName, + type, + name, + content, + ].where((value) => value.trim().isNotEmpty).join(' ').toLowerCase(); } } @@ -340,6 +390,106 @@ String _firstString(Map map, List keys) { return ''; } +String _readRewardDetailType(Map map) { + final direct = _firstString(map, const [ + 'detailType', + 'rewardDetailType', + 'itemDetailType', + 'propType', + 'propsType', + 'goodsType', + 'categoryType', + 'materialType', + 'resourceType', + 'resourceTypeCode', + ]); + if (direct.isNotEmpty) { + return direct; + } + + for (final key in const ['propsResources', 'propsResource', 'resource']) { + final nested = _asMap(map[key]); + final nestedType = _firstString(nested, const [ + 'detailType', + 'type', + 'propType', + 'propsType', + 'materialType', + 'resourceType', + ]); + if (nestedType.isNotEmpty) { + return nestedType; + } + } + return ''; +} + +String _readRewardCover( + Map map, { + required String detailType, +}) { + final typeName = _normalizeRewardToken(detailType); + if (typeName == 'noblevip' || typeName == 'vip') { + final directShortBadge = _firstString(map, const [ + 'shortBadgeCover', + 'shortBadgeCoverUrl', + 'shortBadgeUrl', + 'vipShortBadgeCover', + 'vipShortBadgeUrl', + 'badgeCover', + 'badgeCoverUrl', + ]); + if (directShortBadge.isNotEmpty) { + return directShortBadge; + } + + final nestedShortBadge = _readNestedCover(map['shortBadge']); + if (nestedShortBadge.isNotEmpty) { + return nestedShortBadge; + } + } + + final direct = _firstString(map, const [ + 'cover', + 'coverUrl', + 'imageUrl', + 'iconUrl', + 'photo', + 'picture', + 'giftPhoto', + 'resourceUrl', + 'url', + ]); + if (direct.isNotEmpty) { + return direct; + } + + for (final key in const ['propsResources', 'propsResource', 'resource']) { + final nestedCover = _readNestedCover(map[key]); + if (nestedCover.isNotEmpty) { + return nestedCover; + } + } + return ''; +} + +String _readNestedCover(dynamic value) { + final nested = _asMap(value); + if (nested.isEmpty) { + return ''; + } + return _firstString(nested, const [ + 'cover', + 'coverUrl', + 'imageUrl', + 'iconUrl', + 'photo', + 'picture', + 'url', + 'resourceUrl', + ]); +} + String _firstNonEmpty(List values) { for (final value in values) { final text = value.trim(); @@ -461,12 +611,18 @@ String _formatRewardTypeName(String type) { } String _inferRewardTypeName({ + required String detailType, required String typeName, required String type, required String name, required String content, required String cover, }) { + final detailTypeName = _normalizeSpecificRewardTypeName(detailType); + if (detailTypeName.isNotEmpty) { + return detailTypeName; + } + final explicitTypeName = _normalizeSpecificRewardTypeName(typeName); if (explicitTypeName.isNotEmpty) { return explicitTypeName; @@ -474,6 +630,8 @@ String _inferRewardTypeName({ final searchable = [ + detailType, + typeName, type, name, content, @@ -506,6 +664,37 @@ String _inferRewardTypeName({ ])) { return 'Vehicles'; } + if (_containsAny(searchable, const [ + 'chat_bubble', + 'chatbubble', + 'bubble', + '聊天气泡', + '气泡', + ])) { + return 'Chat Bubble'; + } + if (_containsAny(searchable, const [ + 'data_card', + 'datacard', + 'profile_card', + '资料卡', + ])) { + return 'Data Card'; + } + final textSearchable = + [ + detailType, + typeName, + type, + name, + content, + ].where((value) => value.trim().isNotEmpty).join(' ').toLowerCase(); + if (_containsAny(textSearchable, const ['gift', 'present', '礼物'])) { + return 'Gift'; + } + if (_containsAny(textSearchable, const ['badge', 'medal', '徽章', '勋章'])) { + return 'Badge'; + } if (_containsAny(searchable, const [ 'vip', 'svip', @@ -528,6 +717,13 @@ String _normalizeSpecificRewardTypeName(String value) { if (normalized == 'prop' || normalized == 'props' || normalized == '道具') { return ''; } + if (normalized == 'noblevip' || + normalized == 'vip' || + normalized.contains('svip') || + text.contains('贵族') || + text.contains('会员')) { + return 'VIP'; + } if (normalized.contains('frame') || text.contains('框')) { return 'Frames'; } @@ -539,14 +735,76 @@ String _normalizeSpecificRewardTypeName(String value) { text.contains('车辆')) { return 'Vehicles'; } - if (normalized.contains('vip') || - text.contains('贵族') || - text.contains('会员')) { - return 'VIP'; + if (normalized == 'gift' || + normalized == 'gifts' || + normalized.contains('present') || + text.contains('礼物')) { + return 'Gift'; + } + if (normalized == 'chatbubble' || + normalized.contains('chatbubble') || + normalized.contains('bubble') || + text.contains('聊天气泡') || + text.contains('气泡')) { + return 'Chat Bubble'; + } + if (normalized == 'datacard' || + normalized.contains('datacard') || + normalized.contains('profilecard') || + text.contains('资料卡')) { + return 'Data Card'; + } + if (normalized == 'badge' || + normalized.contains('medal') || + text.contains('徽章') || + text.contains('勋章')) { + return 'Badge'; } return text; } +String _normalizeRewardToken(String value) { + return value.trim().toLowerCase().replaceAll(RegExp(r'[_\-\s]+'), ''); +} + +bool _isGiftRewardType(String value) { + final normalized = _normalizeRewardToken(value); + return normalized.contains('gift') || + normalized.contains('present') || + value.contains('礼物'); +} + +bool _isBadgeRewardType(String value) { + final normalized = _normalizeRewardToken(value); + return normalized.contains('badge') || + normalized.contains('medal') || + value.contains('徽章') || + value.contains('勋章'); +} + +bool _usesQuantityAsDuration(String value) { + final normalized = _normalizeRewardToken(value); + return normalized.contains('noblevip') || + normalized.contains('vip') || + normalized.contains('ride') || + normalized.contains('vehicle') || + normalized.contains('mount') || + normalized.contains('avatarframe') || + normalized.contains('frame') || + normalized.contains('chatbubble') || + normalized.contains('bubble') || + normalized.contains('datacard') || + normalized.contains('profilecard') || + value.contains('贵族') || + value.contains('会员') || + value.contains('座驾') || + value.contains('坐骑') || + value.contains('头像框') || + value.contains('聊天气泡') || + value.contains('气泡') || + value.contains('资料卡'); +} + bool _containsAny(String value, List tokens) { for (final token in tokens) { if (value.contains(token)) { diff --git a/lib/ui_kit/widgets/first_recharge/first_recharge_dialog.dart b/lib/ui_kit/widgets/first_recharge/first_recharge_dialog.dart index e35a07f..47faba4 100644 --- a/lib/ui_kit/widgets/first_recharge/first_recharge_dialog.dart +++ b/lib/ui_kit/widgets/first_recharge/first_recharge_dialog.dart @@ -844,6 +844,9 @@ class _RewardCover extends StatelessWidget { if (title.contains('gift')) { return _FirstRechargeAssets.rewardGift; } + if (title.contains('badge') || title.contains('medal')) { + return _FirstRechargeAssets.rewardBadge; + } return ''; } } @@ -983,6 +986,7 @@ class _FirstRechargeAssets { 'sc_images/first_recharge/reward_coin_stack.png'; static const String rewardGift = 'sc_images/first_recharge/reward_gift.png'; static const String rewardVip = 'sc_images/vip/sc_vip_badge_1.png'; + static const String rewardBadge = 'sc_images/index/sc_icon_medals.png'; static const String buyNowButton = 'sc_images/first_recharge/buy_now_button.png'; static const String discountBadge = diff --git a/lib/ui_kit/widgets/room/floating/floating_game_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_game_screen_widget.dart index 922482a..faff6a6 100644 --- a/lib/ui_kit/widgets/room/floating/floating_game_screen_widget.dart +++ b/lib/ui_kit/widgets/room/floating/floating_game_screen_widget.dart @@ -140,11 +140,14 @@ class _FloatingGameScreenWidgetState extends State final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; final messageRoomId = (widget.message.roomId ?? "").trim(); + if (!_isValidRoomId(messageRoomId)) { + return; + } final isSameVisibleRoom = rtcProvider.shouldShowRoomVisualEffects && rtcProvider.isVoiceRoomRouteVisible && currentRoomId.isNotEmpty && - (messageRoomId.isEmpty || currentRoomId == messageRoomId); + currentRoomId == messageRoomId; if (isSameVisibleRoom) { showRoomGameListSheet( @@ -153,9 +156,7 @@ class _FloatingGameScreenWidgetState extends State ); return; } - if (messageRoomId.isNotEmpty) { - SCRoomUtils.goRoom(messageRoomId, currentContext, fromFloting: true); - } + SCRoomUtils.goRoom(messageRoomId, currentContext, fromFloting: true); }, ); } @@ -453,6 +454,10 @@ class _FloatingGameScreenWidgetState extends State return gameId.isEmpty ? null : gameId; } + bool _isValidRoomId(String roomId) { + return roomId.isNotEmpty && roomId != "0"; + } + String _formatCoins(num? value) { final coins = value ?? 0; if (coins >= 100000) { diff --git a/lib/ui_kit/widgets/room/floating/floating_lucky_gift_global_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_lucky_gift_global_screen_widget.dart index 9b0c82a..50c1075 100644 --- a/lib/ui_kit/widgets/room/floating/floating_lucky_gift_global_screen_widget.dart +++ b/lib/ui_kit/widgets/room/floating/floating_lucky_gift_global_screen_widget.dart @@ -289,7 +289,7 @@ class _FloatingLuckyGiftGlobalScreenWidgetState return; } final messageRoomId = (widget.message.roomId ?? "").trim(); - if (messageRoomId.isEmpty) { + if (!_isValidRoomId(messageRoomId)) { return; } final rtcProvider = Provider.of( @@ -330,6 +330,10 @@ class _FloatingLuckyGiftGlobalScreenWidgetState _swipeController.forward(); } + bool _isValidRoomId(String roomId) { + return roomId.isNotEmpty && roomId != "0"; + } + String _winnerName() { return _firstNonBlank([ widget.message.userName, diff --git a/test/sc_first_recharge_reward_res_test.dart b/test/sc_first_recharge_reward_res_test.dart new file mode 100644 index 0000000..0d2b67e --- /dev/null +++ b/test/sc_first_recharge_reward_res_test.dart @@ -0,0 +1,148 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_first_recharge_reward_res.dart'; + +void main() { + group('SCFirstRechargeRewardItem display mapping', () { + test('maps known non-coin detail types', () { + final cases = { + 'NOBLE_VIP': 'VIP', + 'RIDE': 'Vehicles', + 'AVATAR_FRAME': 'Frames', + 'CHAT_BUBBLE': 'Chat Bubble', + 'DATA_CARD': 'Data Card', + 'GIFT': 'Gift', + 'BADGE': 'Badge', + }; + + cases.forEach((detailType, expectedTitle) { + final item = SCFirstRechargeRewardItem.fromJson({ + 'id': detailType, + 'type': 'PROPS', + 'detailType': detailType, + 'name': 'Concrete material name', + 'quantity': 7, + 'cover': 'https://example.com/$detailType.png', + }); + + expect(item.displayTitle, expectedTitle); + }); + }); + + test('uses quantity as days for duration based props', () { + for (final detailType in const [ + 'NOBLE_VIP', + 'RIDE', + 'AVATAR_FRAME', + 'CHAT_BUBBLE', + 'DATA_CARD', + ]) { + final item = SCFirstRechargeRewardItem.fromJson({ + 'id': detailType, + 'type': 'PROPS', + 'detailType': detailType, + 'quantity': 9, + }); + + expect(item.displayBadgeText, '9d'); + } + }); + + test('treats vip aliases as duration rewards', () { + final item = SCFirstRechargeRewardItem.fromJson({ + 'id': 11, + 'type': 'PROPS', + 'typeName': 'VIP', + 'quantity': 5, + 'shortBadgeCoverUrl': 'https://example.com/vip-short.png', + }); + + expect(item.displayTitle, 'VIP'); + expect(item.displayBadgeText, '5d'); + }); + + test('uses detailType before material name for vehicle rewards', () { + final item = SCFirstRechargeRewardItem.fromJson({ + 'id': 1, + 'type': 'PROPS', + 'detailType': 'RIDE', + 'name': 'Meteor Trail', + 'content': '1962463978629165057', + 'quantity': 7, + 'cover': 'https://example.com/material.png', + }); + + expect(item.displayTitle, 'Vehicles'); + expect(item.displayBadgeText, '7d'); + }); + + test('uses quantity badge for gift rewards', () { + final item = SCFirstRechargeRewardItem.fromJson({ + 'id': 2, + 'type': 'PROPS', + 'detailType': 'GIFT', + 'name': 'Rose', + 'content': '1001', + 'quantity': 3, + 'cover': 'https://example.com/rose.png', + }); + + expect(item.displayTitle, 'Gift'); + expect(item.displayBadgeText, 'x3'); + }); + + test('does not invent a duration badge for badge rewards', () { + final item = SCFirstRechargeRewardItem.fromJson({ + 'id': 22, + 'type': 'PROPS', + 'detailType': 'BADGE', + 'name': 'VIP Medal', + 'quantity': 7, + 'cover': 'https://example.com/badge.png', + }); + + expect(item.displayTitle, 'Badge'); + expect(item.displayBadgeText, ''); + }); + + test('keeps explicit corner text from the API', () { + final item = SCFirstRechargeRewardItem.fromJson({ + 'id': 3, + 'type': 'PROPS', + 'detailType': 'GIFT', + 'name': 'Rose', + 'quantity': 3, + 'cornerText': 'x8', + }); + + expect(item.displayBadgeText, 'x8'); + }); + + test('reads nested resource type when direct detailType is missing', () { + final item = SCFirstRechargeRewardItem.fromJson({ + 'id': 4, + 'type': 'PROPS', + 'name': 'Meteor Trail', + 'quantity': 3, + 'propsResources': {'type': 'RIDE'}, + }); + + expect(item.displayTitle, 'Vehicles'); + expect(item.displayBadgeText, '3d'); + }); + + test('uses vip short badge cover before generic cover', () { + final item = SCFirstRechargeRewardItem.fromJson({ + 'id': 5, + 'type': 'PROPS', + 'detailType': 'NOBLE_VIP', + 'quantity': 7, + 'cover': 'https://example.com/long-badge.png', + 'shortBadge': {'coverUrl': 'https://example.com/short-badge.png'}, + }); + + expect(item.displayTitle, 'VIP'); + expect(item.cover, 'https://example.com/short-badge.png'); + expect(item.displayBadgeText, '7d'); + }); + }); +}