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

442 lines
14 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 '../../../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<List<StudyPlan>> getUserStudyPlans() async {
try {
final localData = await _storageService.getString(_studyPlansKey);
if (localData != null) {
final List<dynamic> jsonList = json.decode(localData);
return jsonList.map((json) => StudyPlan.fromJson(json)).toList();
}
return [];
} catch (e) {
throw Exception('获取学习计划失败: $e');
}
}
/// 创建新的学习计划
Future<StudyPlan> createStudyPlan({
required String name,
String? description,
required StudyPlanType type,
required DateTime startDate,
required DateTime endDate,
required int dailyTarget,
List<String> 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<StudyPlan> 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<void> deleteStudyPlan(String planId) async {
try {
final existingPlans = await getUserStudyPlans();
existingPlans.removeWhere((plan) => plan.id == planId);
await _saveStudyPlansToLocal(existingPlans);
} catch (e) {
throw Exception('删除学习计划失败: $e');
}
}
/// 记录每日学习进度
Future<void> recordDailyProgress({
required String planId,
required DateTime date,
required int wordsLearned,
required int wordsReviewed,
required int studyTimeMinutes,
List<String> 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<StudyPlanStats> 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 = <DailyStudyRecord>[];
// 获取该计划的所有记录
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<List<StudyPlan>> 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<bool> 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<List<StudyPlanTemplate>> 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<StudyPlan> createStudyPlanFromTemplate({
required StudyPlanTemplate template,
required DateTime startDate,
List<String> 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<void> _saveStudyPlansToLocal(List<StudyPlan> studyPlans) async {
final jsonList = studyPlans.map((plan) => plan.toJson()).toList();
await _storageService.setString(_studyPlansKey, json.encode(jsonList));
}
/// 保存每日记录到本地存储
Future<void> _saveDailyRecordsToLocal(Map<String, DailyStudyRecord> records) async {
final jsonMap = records.map((key, record) => MapEntry(key, record.toJson()));
await _storageService.setString(_dailyRecordsKey, json.encode(jsonMap));
}
/// 获取所有每日记录
Future<Map<String, DailyStudyRecord>> _getAllDailyRecords() async {
final localData = await _storageService.getString(_dailyRecordsKey);
if (localData == null) {
return {};
}
final Map<String, dynamic> jsonMap = json.decode(localData);
return jsonMap.map((key, value) => MapEntry(key, DailyStudyRecord.fromJson(value)));
}
/// 更新学习计划进度
Future<void> _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 = <DailyStudyRecord>[];
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<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;
}
/// 格式化日期键
String _formatDateKey(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
}