This commit is contained in:
sjk
2025-11-17 14:09:17 +08:00
commit 31e46c5bf6
479 changed files with 109324 additions and 0 deletions

View File

@@ -0,0 +1,730 @@
import 'dart:convert';
import 'dart:math';
import '../../../core/network/api_client.dart';
import '../../../core/services/storage_service.dart';
import '../models/learning_stats_model.dart';
/// 学习统计服务
class LearningStatsService {
final ApiClient _apiClient;
final StorageService _storageService;
static const String _statsKey = 'learning_stats';
static const String _achievementsKey = 'achievements';
static const String _dailyRecordsKey = 'daily_records';
LearningStatsService({
required ApiClient apiClient,
required StorageService storageService,
}) : _apiClient = apiClient,
_storageService = storageService;
/// 获取用户学习统计
Future<LearningStats> getUserStats() async {
try {
// 1. 从后端API获取每日、每周、每月统计数据
final today = DateTime.now();
final sevenDaysAgo = today.subtract(const Duration(days: 7));
final thirtyDaysAgo = today.subtract(const Duration(days: 30));
final startDateStr = thirtyDaysAgo.toIso8601String().split('T')[0];
final endDateStr = today.toIso8601String().split('T')[0];
print('🔍 开始获取学习统计: startDate=$startDateStr, endDate=$endDateStr');
print('🔍 请求路径: /vocabulary/study/statistics/history');
// 获取历史统计数据
final response = await _apiClient.get(
'/vocabulary/study/statistics/history',
queryParameters: {
'startDate': startDateStr,
'endDate': endDateStr,
},
);
print('✅ API响应成功: statusCode=${response.statusCode}');
final List<dynamic> historyData = response.data['data'] ?? [];
final dailyRecords = historyData.map((item) => DailyStudyRecord.fromJson(item)).toList();
print('📊 获取到 ${dailyRecords.length} 天的学习记录');
// 2. 计算总体统计
final totalWordsLearned = dailyRecords.fold(0, (sum, record) => sum + record.wordsLearned);
final totalWordsReviewed = dailyRecords.fold(0, (sum, record) => sum + record.wordsReviewed);
final totalStudyTimeMinutes = dailyRecords.fold(0, (sum, record) => sum + record.studyTimeMinutes);
final totalExp = dailyRecords.fold(0, (sum, record) => sum + record.expGained);
// 3. 计算连续学习天数
final currentStreak = _calculateCurrentStreak(dailyRecords);
final maxStreak = _calculateMaxStreak(dailyRecords);
// 4. 计算周统计使用最近7天的记录
final weeklyRecords = dailyRecords.where((record) {
return record.date.isAfter(sevenDaysAgo.subtract(const Duration(days: 1)));
}).toList();
final weeklyStats = _calculateWeeklyStats(weeklyRecords);
print('📅 本周学习记录: ${weeklyRecords.length} 天, 学习 ${weeklyStats.wordsLearned} 个单词');
// 5. 计算月统计使用最近30天的记录
final monthlyRecords = dailyRecords;
// 计算本月的周统计记录(用于柱状图)
final weeklyStatsList = <WeeklyStats>[];
final firstDayOfMonth = DateTime(today.year, today.month, 1);
DateTime currentWeekStart = firstDayOfMonth;
while (currentWeekStart.isBefore(today)) {
final weekEnd = currentWeekStart.add(const Duration(days: 6));
final weekRecords = dailyRecords.where((record) {
return record.date.isAfter(currentWeekStart.subtract(const Duration(days: 1))) &&
record.date.isBefore(weekEnd.add(const Duration(days: 1)));
}).toList();
if (weekRecords.isNotEmpty || currentWeekStart.isBefore(today)) {
weeklyStatsList.add(_calculateWeeklyStats(weekRecords));
}
currentWeekStart = currentWeekStart.add(const Duration(days: 7));
}
final monthlyStats = _calculateMonthlyStats(monthlyRecords, weeklyStatsList);
print('📆 本月学习记录: ${monthlyRecords.length} 天, 学习 ${monthlyStats.wordsLearned} 个单词, ${weeklyStatsList.length} 周数据');
// 6. 计算等级和经验值
final level = calculateLevel(totalExp);
final nextLevelExp = calculateNextLevelExp(level);
final currentExp = calculateCurrentLevelExp(totalExp, level);
// 7. 计算平均值
final studyDays = dailyRecords.length;
final averageDailyWords = studyDays > 0 ? totalWordsLearned / studyDays : 0.0;
final averageDailyMinutes = studyDays > 0 ? totalStudyTimeMinutes / studyDays : 0.0;
// 8. 计算准确率
final totalTests = dailyRecords.fold(0, (sum, record) => sum + record.testsCompleted);
final averageAccuracy = totalTests > 0
? dailyRecords.fold(0.0, (sum, record) => sum + record.accuracyRate) / totalTests
: 0.0;
final stats = LearningStats(
userId: 'current_user',
totalStudyDays: studyDays,
currentStreak: currentStreak,
maxStreak: maxStreak,
totalWordsLearned: totalWordsLearned,
totalWordsReviewed: totalWordsReviewed,
totalStudyTimeMinutes: totalStudyTimeMinutes,
averageDailyWords: averageDailyWords,
averageDailyMinutes: averageDailyMinutes,
accuracyRate: averageAccuracy,
completedBooks: 0,
currentBooks: 0,
masteredWords: 0,
learningWords: 0,
reviewWords: 0,
weeklyStats: weeklyStats,
monthlyStats: monthlyStats,
level: level,
currentExp: currentExp,
nextLevelExp: nextLevelExp,
lastStudyTime: dailyRecords.isNotEmpty ? dailyRecords.last.date : null,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
dailyRecords: dailyRecords,
achievements: [],
leaderboard: null,
);
// 保存到本地缓存
await _saveStatsToLocal(stats);
return stats;
} catch (e, stackTrace) {
print('❌ 从API获取学习统计失败: $e');
print('❌ 错误堆栈: $stackTrace');
// 如果API失败尝试从本地缓存加载
final localData = await _storageService.getString(_statsKey);
if (localData != null) {
print('💾 从本地缓存加载数据');
return LearningStats.fromJson(json.decode(localData));
}
// 如果本地也没有,返回默认数据
print('⚠️ 返回默认数据');
return _createDefaultStats();
}
}
/// 更新学习统计
Future<LearningStats> updateStats({
required int wordsLearned,
required int wordsReviewed,
required int studyTimeMinutes,
required double accuracyRate,
List<String> vocabularyBookIds = const [],
}) async {
try {
final currentStats = await getUserStats();
final today = DateTime.now();
// 更新每日记录
await _updateDailyRecord(
date: today,
wordsLearned: wordsLearned,
wordsReviewed: wordsReviewed,
studyTimeMinutes: studyTimeMinutes,
accuracyRate: accuracyRate,
vocabularyBookIds: vocabularyBookIds,
);
// 计算新的统计数据
final updatedStats = await _calculateUpdatedStats(currentStats);
// 保存到本地
await _saveStatsToLocal(updatedStats);
return updatedStats;
} catch (e) {
throw Exception('更新学习统计失败: $e');
}
}
/// 获取每日学习记录
Future<List<DailyStudyRecord>> getDailyRecords({
DateTime? startDate,
DateTime? endDate,
}) async {
try {
final localData = await _storageService.getString(_dailyRecordsKey);
if (localData == null) {
return [];
}
final Map<String, dynamic> recordsMap = json.decode(localData);
final records = recordsMap.values
.map((json) => DailyStudyRecord.fromJson(json))
.toList();
// 按日期过滤
if (startDate != null || endDate != null) {
return records.where((record) {
if (startDate != null && record.date.isBefore(startDate)) {
return false;
}
if (endDate != null && record.date.isAfter(endDate)) {
return false;
}
return true;
}).toList();
}
return records;
} catch (e) {
throw Exception('获取每日记录失败: $e');
}
}
/// 获取周统计
Future<WeeklyStats> getWeeklyStats([DateTime? weekStart]) async {
try {
final startDate = weekStart ?? _getWeekStart(DateTime.now());
final endDate = startDate.add(const Duration(days: 6));
final dailyRecords = await getDailyRecords(
startDate: startDate,
endDate: endDate,
);
return _calculateWeeklyStats(dailyRecords);
} catch (e) {
throw Exception('获取周统计失败: $e');
}
}
/// 获取月统计
Future<MonthlyStats> getMonthlyStats([DateTime? month]) async {
try {
final targetMonth = month ?? DateTime.now();
final startDate = DateTime(targetMonth.year, targetMonth.month, 1);
final endDate = DateTime(targetMonth.year, targetMonth.month + 1, 0);
final dailyRecords = await getDailyRecords(
startDate: startDate,
endDate: endDate,
);
// 计算周统计
final weeklyRecords = <WeeklyStats>[];
DateTime currentWeekStart = startDate;
while (currentWeekStart.isBefore(endDate)) {
final weekEnd = currentWeekStart.add(const Duration(days: 6));
final weekRecords = dailyRecords.where((record) {
return record.date.isAfter(currentWeekStart.subtract(const Duration(days: 1))) &&
record.date.isBefore(weekEnd.add(const Duration(days: 1)));
}).toList();
weeklyRecords.add(_calculateWeeklyStats(weekRecords));
currentWeekStart = currentWeekStart.add(const Duration(days: 7));
}
return _calculateMonthlyStats(dailyRecords, weeklyRecords);
} catch (e) {
throw Exception('获取月统计失败: $e');
}
}
/// 获取用户成就
Future<List<Achievement>> getUserAchievements() async {
try {
final localData = await _storageService.getString(_achievementsKey);
if (localData != null) {
final List<dynamic> jsonList = json.decode(localData);
return jsonList.map((json) => Achievement.fromJson(json)).toList();
}
// 返回默认成就列表
return _createDefaultAchievements();
} catch (e) {
throw Exception('获取成就失败: $e');
}
}
/// 检查并解锁成就
Future<List<Achievement>> checkAndUnlockAchievements() async {
try {
final stats = await getUserStats();
final achievements = await getUserAchievements();
final unlockedAchievements = <Achievement>[];
for (final achievement in achievements) {
if (!achievement.isUnlocked) {
final shouldUnlock = _checkAchievementCondition(achievement, stats);
if (shouldUnlock) {
final unlockedAchievement = achievement.copyWith(
isUnlocked: true,
unlockedAt: DateTime.now(),
);
unlockedAchievements.add(unlockedAchievement);
}
}
}
if (unlockedAchievements.isNotEmpty) {
// 更新成就列表
final updatedAchievements = achievements.map((achievement) {
final unlocked = unlockedAchievements
.where((a) => a.id == achievement.id)
.firstOrNull;
return unlocked ?? achievement;
}).toList();
await _saveAchievementsToLocal(updatedAchievements);
}
return unlockedAchievements;
} catch (e) {
throw Exception('检查成就失败: $e');
}
}
/// 获取排行榜
Future<Leaderboard> getLeaderboard({
required LeaderboardType type,
required LeaderboardPeriod period,
}) async {
try {
// 模拟排行榜数据
final entries = _generateMockLeaderboardEntries(type);
return Leaderboard(
type: type,
period: period,
entries: entries,
userRank: Random().nextInt(100) + 1,
updatedAt: DateTime.now(),
);
} catch (e) {
throw Exception('获取排行榜失败: $e');
}
}
/// 计算用户等级
int calculateLevel(int totalExp) {
// 等级计算公式level = floor(sqrt(totalExp / 100))
return (sqrt(totalExp / 100)).floor() + 1;
}
/// 计算升级所需经验值
int calculateNextLevelExp(int level) {
// 下一级所需总经验值:(level^2) * 100
return level * level * 100;
}
/// 计算当前等级经验值
int calculateCurrentLevelExp(int totalExp, int level) {
final currentLevelTotalExp = (level - 1) * (level - 1) * 100;
return totalExp - currentLevelTotalExp;
}
/// 更新每日记录
Future<void> _updateDailyRecord({
required DateTime date,
required int wordsLearned,
required int wordsReviewed,
required int studyTimeMinutes,
required double accuracyRate,
required List<String> vocabularyBookIds,
}) async {
final dateKey = _formatDateKey(date);
final recordsData = await _storageService.getString(_dailyRecordsKey);
Map<String, dynamic> records = {};
if (recordsData != null) {
records = json.decode(recordsData);
}
// 获取现有记录或创建新记录
DailyStudyRecord existingRecord;
if (records.containsKey(dateKey)) {
existingRecord = DailyStudyRecord.fromJson(records[dateKey]);
} else {
existingRecord = DailyStudyRecord(
date: DateTime(date.year, date.month, date.day),
wordsLearned: 0,
wordsReviewed: 0,
studyTimeMinutes: 0,
accuracyRate: 0.0,
testsCompleted: 0,
expGained: 0,
vocabularyBookIds: [],
);
}
// 更新记录
final updatedRecord = DailyStudyRecord(
date: existingRecord.date,
wordsLearned: existingRecord.wordsLearned + wordsLearned,
wordsReviewed: existingRecord.wordsReviewed + wordsReviewed,
studyTimeMinutes: existingRecord.studyTimeMinutes + studyTimeMinutes,
accuracyRate: (existingRecord.accuracyRate + accuracyRate) / 2,
testsCompleted: existingRecord.testsCompleted + 1,
expGained: existingRecord.expGained + _calculateExpGained(wordsLearned, wordsReviewed, accuracyRate),
vocabularyBookIds: [...existingRecord.vocabularyBookIds, ...vocabularyBookIds].toSet().toList(),
);
records[dateKey] = updatedRecord.toJson();
await _storageService.setString(_dailyRecordsKey, json.encode(records));
}
/// 计算更新后的统计数据
Future<LearningStats> _calculateUpdatedStats(LearningStats currentStats) async {
final allRecords = await getDailyRecords();
// 计算总数据
final totalWordsLearned = allRecords.fold(0, (sum, record) => sum + record.wordsLearned);
final totalWordsReviewed = allRecords.fold(0, (sum, record) => sum + record.wordsReviewed);
final totalStudyTimeMinutes = allRecords.fold(0, (sum, record) => sum + record.studyTimeMinutes);
final totalExp = allRecords.fold(0, (sum, record) => sum + record.expGained);
// 计算连续学习天数
final currentStreak = _calculateCurrentStreak(allRecords);
final maxStreak = _calculateMaxStreak(allRecords);
// 计算平均值
final studyDays = allRecords.length;
final averageDailyWords = studyDays > 0 ? totalWordsLearned / studyDays : 0.0;
final averageDailyMinutes = studyDays > 0 ? totalStudyTimeMinutes / studyDays : 0.0;
// 计算准确率
final totalTests = allRecords.fold(0, (sum, record) => sum + record.testsCompleted);
final averageAccuracy = totalTests > 0
? allRecords.fold(0.0, (sum, record) => sum + record.accuracyRate) / totalTests
: 0.0;
// 计算等级
final level = calculateLevel(totalExp);
final nextLevelExp = calculateNextLevelExp(level);
final currentExp = calculateCurrentLevelExp(totalExp, level);
// 获取周月统计
final weeklyStats = await getWeeklyStats();
final monthlyStats = await getMonthlyStats();
return currentStats.copyWith(
totalStudyDays: studyDays,
currentStreak: currentStreak,
maxStreak: maxStreak > currentStats.maxStreak ? maxStreak : currentStats.maxStreak,
totalWordsLearned: totalWordsLearned,
totalWordsReviewed: totalWordsReviewed,
totalStudyTimeMinutes: totalStudyTimeMinutes,
averageDailyWords: averageDailyWords,
averageDailyMinutes: averageDailyMinutes,
accuracyRate: averageAccuracy,
weeklyStats: weeklyStats,
monthlyStats: monthlyStats,
level: level,
currentExp: currentExp,
nextLevelExp: nextLevelExp,
lastStudyTime: DateTime.now(),
updatedAt: DateTime.now(),
);
}
/// 创建默认统计数据
LearningStats _createDefaultStats() {
final now = DateTime.now();
return LearningStats(
userId: 'current_user',
totalStudyDays: 0,
currentStreak: 0,
maxStreak: 0,
totalWordsLearned: 0,
totalWordsReviewed: 0,
totalStudyTimeMinutes: 0,
averageDailyWords: 0.0,
averageDailyMinutes: 0.0,
accuracyRate: 0.0,
completedBooks: 0,
currentBooks: 0,
masteredWords: 0,
learningWords: 0,
reviewWords: 0,
weeklyStats: WeeklyStats(
studyDays: 0,
wordsLearned: 0,
wordsReviewed: 0,
studyTimeMinutes: 0,
accuracyRate: 0.0,
dailyRecords: [],
),
monthlyStats: MonthlyStats(
studyDays: 0,
wordsLearned: 0,
wordsReviewed: 0,
studyTimeMinutes: 0,
accuracyRate: 0.0,
completedBooks: 0,
weeklyRecords: [],
),
level: 1,
currentExp: 0,
nextLevelExp: 100,
lastStudyTime: null,
createdAt: now,
updatedAt: now,
dailyRecords: [],
achievements: [],
leaderboard: null,
);
}
/// 创建默认成就列表
List<Achievement> _createDefaultAchievements() {
return [
Achievement(
id: 'first_word',
name: '初学者',
description: '学习第一个单词',
icon: '🌱',
type: AchievementType.wordsLearned,
isUnlocked: false,
progress: 0,
target: 1,
rewardExp: 10,
),
Achievement(
id: 'hundred_words',
name: '百词斩',
description: '累计学习100个单词',
icon: '💯',
type: AchievementType.wordsLearned,
isUnlocked: false,
progress: 0,
target: 100,
rewardExp: 100,
),
Achievement(
id: 'first_streak',
name: '坚持不懈',
description: '连续学习7天',
icon: '🔥',
type: AchievementType.streak,
isUnlocked: false,
progress: 0,
target: 7,
rewardExp: 50,
),
Achievement(
id: 'month_streak',
name: '月度达人',
description: '连续学习30天',
icon: '🏆',
type: AchievementType.streak,
isUnlocked: false,
progress: 0,
target: 30,
rewardExp: 300,
),
];
}
/// 计算周统计
WeeklyStats _calculateWeeklyStats(List<DailyStudyRecord> dailyRecords) {
final studyDays = dailyRecords.length;
final wordsLearned = dailyRecords.fold(0, (sum, record) => sum + record.wordsLearned);
final wordsReviewed = dailyRecords.fold(0, (sum, record) => sum + record.wordsReviewed);
final studyTimeMinutes = dailyRecords.fold(0, (sum, record) => sum + record.studyTimeMinutes);
final totalTests = dailyRecords.fold(0, (sum, record) => sum + record.testsCompleted);
final accuracyRate = totalTests > 0
? dailyRecords.fold(0.0, (sum, record) => sum + record.accuracyRate) / totalTests
: 0.0;
return WeeklyStats(
studyDays: studyDays,
wordsLearned: wordsLearned,
wordsReviewed: wordsReviewed,
studyTimeMinutes: studyTimeMinutes,
accuracyRate: accuracyRate,
dailyRecords: dailyRecords,
);
}
/// 计算月统计
MonthlyStats _calculateMonthlyStats(
List<DailyStudyRecord> dailyRecords,
List<WeeklyStats> weeklyRecords,
) {
final studyDays = dailyRecords.length;
final wordsLearned = dailyRecords.fold(0, (sum, record) => sum + record.wordsLearned);
final wordsReviewed = dailyRecords.fold(0, (sum, record) => sum + record.wordsReviewed);
final studyTimeMinutes = dailyRecords.fold(0, (sum, record) => sum + record.studyTimeMinutes);
final totalTests = dailyRecords.fold(0, (sum, record) => sum + record.testsCompleted);
final accuracyRate = totalTests > 0
? dailyRecords.fold(0.0, (sum, record) => sum + record.accuracyRate) / totalTests
: 0.0;
return MonthlyStats(
studyDays: studyDays,
wordsLearned: wordsLearned,
wordsReviewed: wordsReviewed,
studyTimeMinutes: studyTimeMinutes,
accuracyRate: accuracyRate,
completedBooks: 0, // TODO: 从实际数据计算
weeklyRecords: weeklyRecords,
);
}
/// 计算当前连续学习天数
int _calculateCurrentStreak(List<DailyStudyRecord> records) {
if (records.isEmpty) return 0;
records.sort((a, b) => b.date.compareTo(a.date));
int streak = 0;
DateTime currentDate = DateTime.now();
for (final record in records) {
final daysDiff = currentDate.difference(record.date).inDays;
if (daysDiff == streak) {
streak++;
} else {
break;
}
}
return streak;
}
/// 计算最大连续学习天数
int _calculateMaxStreak(List<DailyStudyRecord> records) {
if (records.isEmpty) return 0;
records.sort((a, b) => a.date.compareTo(b.date));
int maxStreak = 1;
int currentStreak = 1;
for (int i = 1; i < records.length; i++) {
final daysDiff = records[i].date.difference(records[i - 1].date).inDays;
if (daysDiff == 1) {
currentStreak++;
maxStreak = maxStreak > currentStreak ? maxStreak : currentStreak;
} else {
currentStreak = 1;
}
}
return maxStreak;
}
/// 计算获得的经验值
int _calculateExpGained(int wordsLearned, int wordsReviewed, double accuracyRate) {
final baseExp = wordsLearned * 2 + wordsReviewed * 1;
final accuracyBonus = (accuracyRate * baseExp * 0.5).round();
return baseExp + accuracyBonus;
}
/// 检查成就条件
bool _checkAchievementCondition(Achievement achievement, LearningStats stats) {
switch (achievement.type) {
case AchievementType.wordsLearned:
return stats.totalWordsLearned >= achievement.target;
case AchievementType.streak:
return stats.currentStreak >= achievement.target;
case AchievementType.studyDays:
return stats.totalStudyDays >= achievement.target;
case AchievementType.booksCompleted:
return stats.completedBooks >= achievement.target;
case AchievementType.studyTime:
return stats.totalStudyTimeMinutes >= achievement.target;
default:
return false;
}
}
/// 生成模拟排行榜数据
List<LeaderboardEntry> _generateMockLeaderboardEntries(LeaderboardType type) {
final random = Random();
final entries = <LeaderboardEntry>[];
for (int i = 1; i <= 50; i++) {
entries.add(LeaderboardEntry(
rank: i,
userId: 'user_$i',
username: '用户$i',
score: random.nextInt(1000) + 100,
level: random.nextInt(20) + 1,
));
}
return entries;
}
/// 获取周开始日期
DateTime _getWeekStart(DateTime date) {
final weekday = date.weekday;
return date.subtract(Duration(days: weekday - 1));
}
/// 格式化日期键
String _formatDateKey(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
/// 保存统计数据到本地
Future<void> _saveStatsToLocal(LearningStats stats) async {
await _storageService.setString(_statsKey, json.encode(stats.toJson()));
}
/// 保存成就到本地
Future<void> _saveAchievementsToLocal(List<Achievement> achievements) async {
final jsonList = achievements.map((achievement) => achievement.toJson()).toList();
await _storageService.setString(_achievementsKey, json.encode(jsonList));
}
}