Files
ai_english/client/lib/features/vocabulary/services/learning_stats_service.dart
2025-11-17 14:09:17 +08:00

730 lines
24 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: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));
}
}