最终版本
This commit is contained in:
parent
10d5e821f7
commit
8b05bb367a
@ -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
|
||||
|
||||
@ -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<String, dynamic> map, List<String> keys) {
|
||||
return '';
|
||||
}
|
||||
|
||||
String _readRewardDetailType(Map<String, dynamic> 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<String, dynamic> 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<String> 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<String> tokens) {
|
||||
for (final token in tokens) {
|
||||
if (value.contains(token)) {
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -140,11 +140,14 @@ class _FloatingGameScreenWidgetState extends State<FloatingGameScreenWidget>
|
||||
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<FloatingGameScreenWidget>
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (messageRoomId.isNotEmpty) {
|
||||
SCRoomUtils.goRoom(messageRoomId, currentContext, fromFloting: true);
|
||||
}
|
||||
SCRoomUtils.goRoom(messageRoomId, currentContext, fromFloting: true);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -453,6 +454,10 @@ class _FloatingGameScreenWidgetState extends State<FloatingGameScreenWidget>
|
||||
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) {
|
||||
|
||||
@ -289,7 +289,7 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
return;
|
||||
}
|
||||
final messageRoomId = (widget.message.roomId ?? "").trim();
|
||||
if (messageRoomId.isEmpty) {
|
||||
if (!_isValidRoomId(messageRoomId)) {
|
||||
return;
|
||||
}
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
@ -330,6 +330,10 @@ class _FloatingLuckyGiftGlobalScreenWidgetState
|
||||
_swipeController.forward();
|
||||
}
|
||||
|
||||
bool _isValidRoomId(String roomId) {
|
||||
return roomId.isNotEmpty && roomId != "0";
|
||||
}
|
||||
|
||||
String _winnerName() {
|
||||
return _firstNonBlank([
|
||||
widget.message.userName,
|
||||
|
||||
148
test/sc_first_recharge_reward_res_test.dart
Normal file
148
test/sc_first_recharge_reward_res_test.dart
Normal file
@ -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 = <String, String>{
|
||||
'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');
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user