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 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 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 = []; 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 updateStats({ required int wordsLearned, required int wordsReviewed, required int studyTimeMinutes, required double accuracyRate, List 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> getDailyRecords({ DateTime? startDate, DateTime? endDate, }) async { try { final localData = await _storageService.getString(_dailyRecordsKey); if (localData == null) { return []; } final Map 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 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 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 = []; 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> getUserAchievements() async { try { final localData = await _storageService.getString(_achievementsKey); if (localData != null) { final List jsonList = json.decode(localData); return jsonList.map((json) => Achievement.fromJson(json)).toList(); } // 返回默认成就列表 return _createDefaultAchievements(); } catch (e) { throw Exception('获取成就失败: $e'); } } /// 检查并解锁成就 Future> checkAndUnlockAchievements() async { try { final stats = await getUserStats(); final achievements = await getUserAchievements(); final unlockedAchievements = []; 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 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 _updateDailyRecord({ required DateTime date, required int wordsLearned, required int wordsReviewed, required int studyTimeMinutes, required double accuracyRate, required List vocabularyBookIds, }) async { final dateKey = _formatDateKey(date); final recordsData = await _storageService.getString(_dailyRecordsKey); Map 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 _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 _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 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 dailyRecords, List 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 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 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 _generateMockLeaderboardEntries(LeaderboardType type) { final random = Random(); final entries = []; 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 _saveStatsToLocal(LearningStats stats) async { await _storageService.setString(_statsKey, json.encode(stats.toJson())); } /// 保存成就到本地 Future _saveAchievementsToLocal(List achievements) async { final jsonList = achievements.map((achievement) => achievement.toJson()).toList(); await _storageService.setString(_achievementsKey, json.encode(jsonList)); } }