init
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user