import 'dart:convert'; import '../../../core/network/api_client.dart'; import '../../../core/services/storage_service.dart'; import '../models/study_plan_model.dart'; import '../models/vocabulary_book_model.dart'; /// 学习计划服务 class StudyPlanService { final ApiClient _apiClient; final StorageService _storageService; static const String _studyPlansKey = 'study_plans'; static const String _dailyRecordsKey = 'daily_study_records'; static const String _templatesKey = 'study_plan_templates'; StudyPlanService({ required ApiClient apiClient, required StorageService storageService, }) : _apiClient = apiClient, _storageService = storageService; /// 获取用户的所有学习计划 Future> getUserStudyPlans() async { try { final localData = await _storageService.getString(_studyPlansKey); if (localData != null) { final List jsonList = json.decode(localData); return jsonList.map((json) => StudyPlan.fromJson(json)).toList(); } return []; } catch (e) { throw Exception('获取学习计划失败: $e'); } } /// 创建新的学习计划 Future createStudyPlan({ required String name, String? description, required StudyPlanType type, required DateTime startDate, required DateTime endDate, required int dailyTarget, List vocabularyBookIds = const [], }) async { try { final studyPlan = StudyPlan( id: DateTime.now().millisecondsSinceEpoch.toString(), name: name, description: description, userId: 'current_user', type: type, status: StudyPlanStatus.active, startDate: startDate, endDate: endDate, dailyTarget: dailyTarget, vocabularyBookIds: vocabularyBookIds, createdAt: DateTime.now(), updatedAt: DateTime.now(), ); final existingPlans = await getUserStudyPlans(); existingPlans.add(studyPlan); await _saveStudyPlansToLocal(existingPlans); return studyPlan; } catch (e) { throw Exception('创建学习计划失败: $e'); } } /// 更新学习计划 Future updateStudyPlan(StudyPlan studyPlan) async { try { final existingPlans = await getUserStudyPlans(); final index = existingPlans.indexWhere((plan) => plan.id == studyPlan.id); if (index == -1) { throw Exception('学习计划不存在'); } final updatedPlan = studyPlan.copyWith(updatedAt: DateTime.now()); existingPlans[index] = updatedPlan; await _saveStudyPlansToLocal(existingPlans); return updatedPlan; } catch (e) { throw Exception('更新学习计划失败: $e'); } } /// 删除学习计划 Future deleteStudyPlan(String planId) async { try { final existingPlans = await getUserStudyPlans(); existingPlans.removeWhere((plan) => plan.id == planId); await _saveStudyPlansToLocal(existingPlans); } catch (e) { throw Exception('删除学习计划失败: $e'); } } /// 记录每日学习进度 Future recordDailyProgress({ required String planId, required DateTime date, required int wordsLearned, required int wordsReviewed, required int studyTimeMinutes, List vocabularyBookIds = const [], }) async { try { final record = DailyStudyRecord( date: DateTime(date.year, date.month, date.day), wordsLearned: wordsLearned, wordsReviewed: wordsReviewed, studyTimeMinutes: studyTimeMinutes, vocabularyBookIds: vocabularyBookIds, ); // 获取现有记录 final allRecords = await _getAllDailyRecords(); final recordKey = '${planId}_${_formatDateKey(date)}'; // 更新或添加记录 allRecords[recordKey] = record; await _saveDailyRecordsToLocal(allRecords); // 更新学习计划的进度 await _updateStudyPlanProgress(planId); } catch (e) { throw Exception('记录学习进度失败: $e'); } } /// 获取学习计划统计信息 Future getStudyPlanStats(String planId) async { try { final studyPlans = await getUserStudyPlans(); final studyPlan = studyPlans.where((plan) => plan.id == planId).firstOrNull; if (studyPlan == null) { throw Exception('学习计划不存在'); } final allRecords = await _getAllDailyRecords(); final planRecords = []; // 获取该计划的所有记录 for (final entry in allRecords.entries) { if (entry.key.startsWith('${planId}_')) { planRecords.add(entry.value); } } // 计算统计信息 final totalDays = studyPlan.endDate.difference(studyPlan.startDate).inDays + 1; final studiedDays = planRecords.length; final totalWordsLearned = planRecords.fold(0, (sum, record) => sum + record.wordsLearned); final totalWordsReviewed = planRecords.fold(0, (sum, record) => sum + record.wordsReviewed); // 计算连续学习天数 final currentStreak = _calculateCurrentStreak(planRecords); final maxStreak = _calculateMaxStreak(planRecords); final completionRate = studyPlan.totalWords > 0 ? studyPlan.completedWords / studyPlan.totalWords : 0.0; final averageDailyWords = studiedDays > 0 ? totalWordsLearned / studiedDays : 0.0; final remainingDays = studyPlan.endDate.difference(DateTime.now()).inDays; final remainingWords = studyPlan.totalWords - studyPlan.completedWords; final lastStudyDate = planRecords.isNotEmpty ? planRecords.map((r) => r.date).reduce((a, b) => a.isAfter(b) ? a : b) : null; return StudyPlanStats( planId: planId, totalDays: totalDays, studiedDays: studiedDays, totalWords: studyPlan.totalWords, learnedWords: totalWordsLearned, reviewedWords: totalWordsReviewed, currentStreak: currentStreak, maxStreak: maxStreak, completionRate: completionRate, averageDailyWords: averageDailyWords, remainingDays: remainingDays > 0 ? remainingDays : 0, remainingWords: remainingWords > 0 ? remainingWords : 0, lastStudyDate: lastStudyDate, dailyRecords: planRecords, ); } catch (e) { throw Exception('获取学习计划统计失败: $e'); } } /// 获取今日学习任务 Future> getTodayStudyTasks() async { try { final allPlans = await getUserStudyPlans(); final today = DateTime.now(); return allPlans.where((plan) { return plan.status == StudyPlanStatus.active && plan.startDate.isBefore(today.add(const Duration(days: 1))) && plan.endDate.isAfter(today.subtract(const Duration(days: 1))); }).toList(); } catch (e) { throw Exception('获取今日学习任务失败: $e'); } } /// 检查今日是否完成目标 Future isTodayTargetAchieved(String planId) async { try { final today = DateTime.now(); final allRecords = await _getAllDailyRecords(); final recordKey = '${planId}_${_formatDateKey(today)}'; final todayRecord = allRecords[recordKey]; if (todayRecord == null) return false; final studyPlans = await getUserStudyPlans(); final studyPlan = studyPlans.where((plan) => plan.id == planId).firstOrNull; if (studyPlan == null) return false; return todayRecord.wordsLearned >= studyPlan.dailyTarget; } catch (e) { return false; } } /// 获取学习计划模板 Future> getStudyPlanTemplates() async { try { // 返回预设的学习计划模板 return [ StudyPlanTemplate( id: 'daily_basic', name: '每日基础学习', description: '每天学习20个新单词,适合初学者', type: StudyPlanType.daily, durationDays: 30, dailyTarget: 20, difficulty: 1, tags: ['基础', '初学者'], isPopular: true, ), StudyPlanTemplate( id: 'weekly_intensive', name: '周集中学习', description: '每周集中学习,每天50个单词', type: StudyPlanType.weekly, durationDays: 7, dailyTarget: 50, difficulty: 3, tags: ['集中', '高强度'], ), StudyPlanTemplate( id: 'exam_prep_cet4', name: 'CET-4考试准备', description: '为大学英语四级考试准备的学习计划', type: StudyPlanType.examPrep, durationDays: 60, dailyTarget: 30, difficulty: 2, tags: ['考试', 'CET-4'], isPopular: true, ), StudyPlanTemplate( id: 'exam_prep_cet6', name: 'CET-6考试准备', description: '为大学英语六级考试准备的学习计划', type: StudyPlanType.examPrep, durationDays: 90, dailyTarget: 40, difficulty: 3, tags: ['考试', 'CET-6'], ), StudyPlanTemplate( id: 'toefl_prep', name: 'TOEFL考试准备', description: '为托福考试准备的高强度学习计划', type: StudyPlanType.examPrep, durationDays: 120, dailyTarget: 60, difficulty: 4, tags: ['考试', 'TOEFL', '出国'], isPopular: true, ), ]; } catch (e) { throw Exception('获取学习计划模板失败: $e'); } } /// 从模板创建学习计划 Future createStudyPlanFromTemplate({ required StudyPlanTemplate template, required DateTime startDate, List vocabularyBookIds = const [], }) async { try { final endDate = startDate.add(Duration(days: template.durationDays - 1)); return await createStudyPlan( name: template.name, description: template.description, type: template.type, startDate: startDate, endDate: endDate, dailyTarget: template.dailyTarget, vocabularyBookIds: vocabularyBookIds, ); } catch (e) { throw Exception('从模板创建学习计划失败: $e'); } } /// 保存学习计划到本地存储 Future _saveStudyPlansToLocal(List studyPlans) async { final jsonList = studyPlans.map((plan) => plan.toJson()).toList(); await _storageService.setString(_studyPlansKey, json.encode(jsonList)); } /// 保存每日记录到本地存储 Future _saveDailyRecordsToLocal(Map records) async { final jsonMap = records.map((key, record) => MapEntry(key, record.toJson())); await _storageService.setString(_dailyRecordsKey, json.encode(jsonMap)); } /// 获取所有每日记录 Future> _getAllDailyRecords() async { final localData = await _storageService.getString(_dailyRecordsKey); if (localData == null) { return {}; } final Map jsonMap = json.decode(localData); return jsonMap.map((key, value) => MapEntry(key, DailyStudyRecord.fromJson(value))); } /// 更新学习计划进度 Future _updateStudyPlanProgress(String planId) async { try { final studyPlans = await getUserStudyPlans(); final index = studyPlans.indexWhere((plan) => plan.id == planId); if (index == -1) return; final studyPlan = studyPlans[index]; final allRecords = await _getAllDailyRecords(); // 计算总完成单词数 int totalCompleted = 0; for (final entry in allRecords.entries) { if (entry.key.startsWith('${planId}_')) { totalCompleted += entry.value.wordsLearned; } } // 计算连续学习天数 final planRecords = []; for (final entry in allRecords.entries) { if (entry.key.startsWith('${planId}_')) { planRecords.add(entry.value); } } final currentStreak = _calculateCurrentStreak(planRecords); final maxStreak = _calculateMaxStreak(planRecords); final updatedPlan = studyPlan.copyWith( completedWords: totalCompleted, currentStreak: currentStreak, maxStreak: maxStreak > studyPlan.maxStreak ? maxStreak : studyPlan.maxStreak, updatedAt: DateTime.now(), ); studyPlans[index] = updatedPlan; await _saveStudyPlansToLocal(studyPlans); } catch (e) { // 忽略更新错误 } } /// 计算当前连续学习天数 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; } /// 格式化日期键 String _formatDateKey(DateTime date) { return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; } }