Files
ai_english/client/lib/features/vocabulary/services/study_plan_service.dart

442 lines
14 KiB
Dart
Raw Normal View History

2025-11-17 14:09:17 +08:00
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')}';
}
}