Files
UnionApp/lib/service/recommendation_helper.dart
spasolreisa fed18d264a 0417 0022
更新
2026-04-17 00:22:43 +08:00

293 lines
9.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:math';
import '../model/song_model.dart';
class RecommendationHelper {
/// 基于 Java MusicService 逻辑的智能推荐
///
/// [allSongs]: 全量歌曲库 (SongModel)
/// [userMusicList]: 用户成绩列表 (dynamic)
/// - 情况A: List<Map>,每个 Map 包含 musicId, level, achievement, dx_rating
/// - 情况B: List<Map>,每个 Map 包含 userMusicDetailList (嵌套结构)
/// [userRating]: 用户当前总 Rating (DX Rating),用于确定推荐难度区间
/// [b35MinRating]: 用户当前 B35 中最低的那首曲子的 Rating (用于判断推分是否有意义)
/// [count]: 推荐数量
static List<SongModel> getSmartRecommendations({
required List<SongModel> allSongs,
required List<dynamic> 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<String, Map<String, dynamic>> playedMap = {};
for (var group in userMusicList) {
if (group is! Map) continue;
List<dynamic> 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<SongModel> candidatesForImprovement = []; // 用于推分
List<SongModel> candidatesNew = []; // 新曲/未玩
// 用于快速判断是否已加入结果集,避免 O(N^2) 的 contains 检查
final Set<int> 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<String, dynamic>? 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<double> 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<SongModel> 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<SongModel> 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();
}
}