init
This commit is contained in:
442
client/lib/features/vocabulary/services/study_plan_service.dart
Normal file
442
client/lib/features/vocabulary/services/study_plan_service.dart
Normal file
@@ -0,0 +1,442 @@
|
||||
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')}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user