import 'dart:math'; import '../model/song_model.dart'; class RecommendationHelper { /// 基于 Java MusicService 逻辑的智能推荐 /// /// [allSongs]: 全量歌曲库 (SongModel) /// [userMusicList]: 用户成绩列表 (dynamic) /// - 情况A: List,每个 Map 包含 musicId, level, achievement, dx_rating /// - 情况B: List,每个 Map 包含 userMusicDetailList (嵌套结构) /// [userRating]: 用户当前总 Rating (DX Rating),用于确定推荐难度区间 /// [b35MinRating]: 用户当前 B35 中最低的那首曲子的 Rating (用于判断推分是否有意义) /// [count]: 推荐数量 static List getSmartRecommendations({ required List allSongs, required List userMusicList, required int userRating, required int b35MinRating, int count = 6, }) { if (allSongs.isEmpty) return []; final random = Random(); // 1. 解析用户成绩,构建映射: Key = "musicId_level", Value = { achievement, rating } // 使用 HashMap 提高查找速度 final Map> playedMap = {}; for (var group in userMusicList) { if (group is! Map) continue; List details = []; // 兼容两种数据结构: if (group.containsKey('userMusicDetailList')) { details = group['userMusicDetailList'] as List? ?? []; } else if (group.containsKey('achievement') || group.containsKey('dx_rating')) { // 如果当前 group 本身就是成绩对象 details = [group]; } if (details.isEmpty) continue; for (var detail in details) { if (detail is! Map) continue; // 提取关键字段 final int musicId = detail['musicId'] ?? detail['id'] ?? 0; if(musicId>16000) continue; final int level = detail['level'] ?? detail['levelIndex'] ?? 3; // 默认 Master final int achievement = detail['achievement'] ?? 0; // 确保 rating 是 double final dynamic rawRating = detail['dx_rating'] ?? detail['rating'] ?? 0; final double rating = rawRating is num ? rawRating.toDouble() : 0.0; if (musicId == 0) continue; String key = "${musicId}_${level}"; // 只保留该难度下的最高成绩 if (!playedMap.containsKey(key) || achievement > (playedMap[key]!['achievement'] as int)) { playedMap[key] = { 'musicId': musicId, 'level': level, 'achievement': achievement, 'rating': rating, }; } } } // 2. 计算推荐难度区间 (基于 Java logic) double minRecommendedLevel = 0.0; if (userRating >= 15500) { minRecommendedLevel = 13.6; } else if (userRating >= 15000) { minRecommendedLevel = 13.0; } else if (userRating >= 14500) { minRecommendedLevel = 12.5; } else if (userRating >= 14000) { minRecommendedLevel = 12.0; } else if (userRating >= 13000) { minRecommendedLevel = 11.5; } else if (userRating >= 12000) { minRecommendedLevel = 11.0; } else if (userRating >= 11000) { minRecommendedLevel = 10.5; } else { minRecommendedLevel = 9.0; // 新手保护线 } List candidatesForImprovement = []; // 用于推分 List candidatesNew = []; // 新曲/未玩 // 用于快速判断是否已加入结果集,避免 O(N^2) 的 contains 检查 final Set addedSongIds = {}; for (var song in allSongs) { // 过滤无效 ID if (song.id < 100) continue; // 获取 Master (Level 3) 的定数,如果没有则获取 Expert (Level 2) double? masterLevel = _getSongLevel(song, 3); double? expertLevel = _getSongLevel(song, 2); double? targetLevel; int targetLevelIndex = 3; // 默认优先推荐 Master if (masterLevel != null) { targetLevel = masterLevel; } else if (expertLevel != null) { targetLevel = expertLevel; targetLevelIndex = 2; } else { continue; // 没有可用难度数据 } // --- 检查是否已游玩 --- bool isPlayed = false; Map? bestChart; // 尝试直接匹配 (SD 或 基础ID) String keyDirect = "${song.id}_$targetLevelIndex"; if (playedMap.containsKey(keyDirect)) { isPlayed = true; bestChart = playedMap[keyDirect]; } // 尝试匹配 DX 偏移 (DX 歌曲 ID 通常 +10000) else { int dxId = song.id + 10000; String keyDx = "${dxId}_$targetLevelIndex"; if (playedMap.containsKey(keyDx)) { isPlayed = true; bestChart = playedMap[keyDx]; } } if (isPlayed && bestChart != null) { // --- 策略 A: 推分逻辑 (Improvement) --- int currentAch = bestChart['achievement'] as int; // 如果已经 AP+ (100.5%),跳过 if (currentAch >= 1005000) continue; // 计算下一个档位的目标达成率 double nextTargetAch = 0; const List targets = [97.0, 98.0, 99.0, 99.5, 100.0, 100.5]; double currentPercent = currentAch / 10000.0; for (double t in targets) { if (currentPercent < t) { nextTargetAch = t; break; } } if (nextTargetAch == 0) continue; // 计算目标 Rating int targetRating = _calculateRating(targetLevel!, nextTargetAch); // 核心判断:如果推分后的 Rating 大于当前 B35 最低分,则值得推荐 if (targetRating > b35MinRating) { candidatesForImprovement.add(song); } } else { // --- 策略 B: 新曲逻辑 (New) --- // 判断定数是否在用户能力范围内 if (targetLevel != null && targetLevel >= minRecommendedLevel && targetLevel <= 15.0) { candidatesNew.add(song); } } } // 3. 混合推荐结果 List result = []; // 60% 来自“提升空间大”的曲子 candidatesForImprovement.shuffle(random); int improveCount = (count * 0.6).round(); for (int i = 0; i < improveCount && i < candidatesForImprovement.length; i++) { final song = candidatesForImprovement[i]; result.add(song); addedSongIds.add(song.id); } // 40% 来自“新曲/潜力曲” candidatesNew.shuffle(random); int remaining = count - result.length; for (int i = 0; i < remaining && i < candidatesNew.length; i++) { final song = candidatesNew[i]; // 避免重复添加(虽然逻辑上 New 和 Improve 不重叠,但防万一) if (!addedSongIds.contains(song.id)) { result.add(song); addedSongIds.add(song.id); } } // 如果不够,用纯随机补齐 if (result.length < count) { // 复制一份并打乱,避免修改原数组 List fallback = List.from(allSongs); fallback.shuffle(random); for (var s in fallback) { if (result.length >= count) break; // 使用 Set 进行 O(1) 查找,避免 List.contains 的 O(N) 查找 if (!addedSongIds.contains(s.id)) { result.add(s); addedSongIds.add(s.id); } } } return result.take(count).toList(); } /// 辅助:获取歌曲指定难度的定数 (Level Value) static double? _getSongLevel(SongModel song, int levelIndex) { // levelIndex: 0=Basic, 1=Advanced, 2=Expert, 3=Master, 4=Re:Master // 1. 尝试从 SD (Standard) 获取 if (song.sd != null && song.sd is Map) { final sdMap = song.sd as Map; // 键可能是字符串 "3" 或整数 3,这里做兼容 var data = sdMap["$levelIndex"] ?? sdMap[levelIndex]; if (data is Map && data.containsKey("level_value")) { final val = data["level_value"]; if (val is num) return val.toDouble(); } } // 2. 尝试从 DX 获取 if (song.dx != null && song.dx is Map) { final dxMap = song.dx as Map; var data = dxMap["$levelIndex"] ?? dxMap[levelIndex]; if (data is Map && data.containsKey("level_value")) { final val = data["level_value"]; if (val is num) return val.toDouble(); } } return null; } /// 辅助:根据定数和达成率计算 Rating (完全复刻 Java getRatingChart) static int _calculateRating(double diff, double achievementPercent) { double sys = 22.4; double ach = achievementPercent; // 例如 99.5 if (ach >= 100.5000) { return (diff * 22.512).floor(); } if (ach == 100.4999) { sys = 22.2; } else if (ach >= 100.0000) { sys = 21.6; } else if (ach == 99.9999) { sys = 21.4; } else if (ach >= 99.5000) { sys = 21.1; } else if (ach >= 99.0000) { sys = 20.8; } else if (ach >= 98.0000) { sys = 20.3; } else if (ach >= 97.0000) { sys = 20.0; } else if (ach >= 94.0000) { sys = 16.8; } else if (ach >= 90.0000) { sys = 15.2; } else if (ach >= 80.0000) { sys = 13.6; } else if (ach >= 75.0000) { sys = 12.0; } else if (ach >= 70.0000) { sys = 11.2; } else if (ach >= 60.0000) { sys = 9.6; } else if (ach >= 50.0000) { sys = 8.0; } else { sys = 0.0; } if (sys == 0.0) return 0; // Java: (int) (diff * sys * achievement / 100) return (diff * sys * ach / 100).floor(); } }