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

746 lines
23 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:math';
import '../models/word_model.dart';
import '../models/vocabulary_book_model.dart';
import '../models/study_session_model.dart';
import '../models/daily_stats_model.dart';
import '../../../core/network/api_client.dart';
import '../../../core/services/storage_service.dart';
import '../../../core/services/enhanced_api_service.dart';
import '../../../core/models/api_response.dart';
/// 单词学习服务
class VocabularyService {
final ApiClient _apiClient;
final StorageService _storageService;
final EnhancedApiService _enhancedApiService = EnhancedApiService();
final Random _random = Random();
VocabularyService({
required ApiClient apiClient,
required StorageService storageService,
}) : _apiClient = apiClient,
_storageService = storageService;
// 缓存时长配置
static const Duration _shortCacheDuration = Duration(minutes: 5);
static const Duration _longCacheDuration = Duration(hours: 1);
// ==================== 词汇书相关 ====================
/// 获取系统词汇书列表
Future<List<VocabularyBook>> getSystemVocabularyBooks({
VocabularyBookDifficulty? difficulty,
String? category,
int page = 1,
int limit = 20,
}) async {
try {
final response = await _enhancedApiService.get<List<VocabularyBook>>(
'/vocabulary/books/system',
queryParameters: {
if (difficulty != null) 'difficulty': difficulty.name,
if (category != null) 'category': category,
'page': page,
'limit': limit,
},
cacheDuration: Duration.zero, // 暂时禁用缓存,避免旧数据解析错误
fromJson: (data) {
// 处理分页响应结构: data.items
final responseData = data['data'];
final List<dynamic> list = responseData is Map
? (responseData['items'] ?? [])
: (data['data'] ?? []);
return list.map((json) => VocabularyBook.fromJson(json)).toList();
},
);
if (response.success && response.data != null) {
return response.data!;
} else {
throw Exception(response.message);
}
} catch (e) {
throw Exception('获取系统词汇书失败: $e');
}
}
/// 获取词汇书分类列表
Future<List<Map<String, dynamic>>> getVocabularyBookCategories() async {
try {
final response = await _enhancedApiService.get<List<Map<String, dynamic>>>(
'/vocabulary/books/categories',
cacheDuration: _longCacheDuration,
fromJson: (data) {
final List<dynamic> list = data['data'] ?? [];
return list.map((item) => item as Map<String, dynamic>).toList();
},
);
if (response.success && response.data != null) {
return response.data!;
} else {
throw Exception(response.message);
}
} catch (e) {
throw Exception('获取词汇书分类失败: $e');
}
}
/// 获取用户词汇书列表
Future<List<VocabularyBook>> getUserVocabularyBooks() async {
try {
final response = await _enhancedApiService.get<List<VocabularyBook>>(
'/vocabulary/books/user',
cacheDuration: _shortCacheDuration,
fromJson: (data) {
final List<dynamic> list = data['data'] ?? [];
return list.map((json) => VocabularyBook.fromJson(json)).toList();
},
);
if (response.success && response.data != null) {
return response.data!;
} else {
throw Exception(response.message);
}
} catch (e) {
throw Exception('获取用户词汇书失败: $e');
}
}
/// 获取词汇书详情
Future<VocabularyBook> getVocabularyBookDetail(String bookId) async {
try {
final response = await _enhancedApiService.get<VocabularyBook>(
'/vocabulary/books/$bookId',
cacheDuration: _longCacheDuration,
fromJson: (data) => VocabularyBook.fromJson(data['data']),
);
if (response.success && response.data != null) {
return response.data!;
} else {
throw Exception(response.message);
}
} catch (e) {
throw Exception('获取词汇书详情失败: $e');
}
}
/// 获取词汇书单词列表
Future<List<VocabularyBookWord>> getVocabularyBookWords(
String bookId, {
int page = 1,
int limit = 50,
}) async {
try {
final response = await _enhancedApiService.get<List<VocabularyBookWord>>(
'/vocabulary/books/$bookId/words',
queryParameters: {
'page': page,
'limit': limit,
},
cacheDuration: Duration.zero, // 暂时禁用缓存
fromJson: (data) {
// 处理分页响应结构: data.items
final responseData = data['data'];
final List<dynamic> list = responseData is Map
? (responseData['items'] ?? [])
: (data['data'] ?? []);
return list.map((json) => VocabularyBookWord.fromJson(json)).toList();
},
);
if (response.success && response.data != null) {
return response.data!;
} else {
throw Exception(response.message);
}
} catch (e) {
throw Exception('获取词汇书单词失败: $e');
}
}
/// 获取用户在某词汇书中的学习进度后端API优先失败时兜底为0
Future<UserVocabularyBookProgress> getVocabularyBookProgress(
String bookId, {
bool forceRefresh = false, // 是否强制刷新(跳过缓存)
}) async {
try {
final response = await _enhancedApiService.get<UserVocabularyBookProgress>(
'/vocabulary/books/$bookId/progress',
cacheDuration: forceRefresh ? Duration.zero : _shortCacheDuration,
fromJson: (data) => UserVocabularyBookProgress.fromJson(data['data'] ?? data),
);
if (response.success && response.data != null) {
return response.data!;
} else {
throw Exception(response.message);
}
} catch (e) {
// 当后端暂未提供词书进度接口时,返回一个默认的进度对象,避免前端崩溃
final now = DateTime.now();
return UserVocabularyBookProgress(
id: 'progress_$bookId',
userId: 'current_user',
vocabularyBookId: bookId,
learnedWords: 0,
masteredWords: 0,
progressPercentage: 0.0,
streakDays: 0,
totalStudyDays: 0,
averageDailyWords: 0.0,
estimatedCompletionDate: null,
isCompleted: false,
completedAt: null,
startedAt: now,
lastStudiedAt: now,
);
}
}
/// 添加词汇书到用户库
Future<void> addVocabularyBookToUser(String bookId) async {
try {
await _apiClient.post('/vocabulary/books/$bookId/add');
} catch (e) {
throw Exception('添加词汇书失败: $e');
}
}
/// 创建自定义词汇书
Future<VocabularyBook> createCustomVocabularyBook({
required String name,
String? description,
List<String> wordIds = const [],
}) async {
try {
final response = await _apiClient.post(
'/vocabulary/books/custom',
data: {
'name': name,
'description': description,
'wordIds': wordIds,
},
);
return VocabularyBook.fromJson(response.data['data']);
} catch (e) {
throw Exception('创建词汇书失败: $e');
}
}
// ==================== 单词相关 ====================
/// 搜索单词
Future<List<Word>> searchWords(
String query, {
int page = 1,
int limit = 20,
}) async {
try {
final response = await _apiClient.get(
'/vocabulary/words/search',
queryParameters: {
'q': query,
'page': page,
'limit': limit,
},
);
final List<dynamic> data = response.data['data'] ?? [];
return data.map((json) => Word.fromJson(json)).toList();
} catch (e) {
throw Exception('搜索单词失败: $e');
}
}
/// 获取单词详情
Future<Word> getWordDetail(String wordId) async {
try {
final response = await _apiClient.get('/vocabulary/words/$wordId');
return Word.fromJson(response.data['data']);
} catch (e) {
throw Exception('获取单词详情失败: $e');
}
}
/// 获取用户单词学习进度
Future<UserWordProgress?> getUserWordProgress(String wordId) async {
try {
final response = await _apiClient.get('/vocabulary/words/$wordId/progress');
final data = response.data['data'];
return data != null ? UserWordProgress.fromJson(data) : null;
} catch (e) {
throw Exception('获取单词学习进度失败: $e');
}
}
/// 更新用户单词学习进度
Future<UserWordProgress> updateUserWordProgress({
required String wordId,
required LearningStatus status,
required bool isCorrect,
int responseTime = 0,
}) async {
try {
final response = await _apiClient.put(
'/vocabulary/words/$wordId/progress',
data: {
'status': status.name,
'isCorrect': isCorrect,
'responseTime': responseTime,
},
);
return UserWordProgress.fromJson(response.data['data']);
} catch (e) {
throw Exception('更新单词学习进度失败: $e');
}
}
// ==================== 学习会话相关 ====================
/// 开始学习会话
Future<StudySession> startStudySession({
String? vocabularyBookId,
required StudyMode mode,
required int targetWordCount,
}) async {
try {
final response = await _apiClient.post(
'/vocabulary/study/sessions',
data: {
'vocabularyBookId': vocabularyBookId,
'mode': mode.name,
'targetWordCount': targetWordCount,
},
);
return StudySession.fromJson(response.data['data']);
} catch (e) {
throw Exception('开始学习会话失败: $e');
}
}
/// 结束学习会话
Future<StudySession> endStudySession(
String sessionId, {
required int durationSeconds,
required List<WordExerciseRecord> exercises,
}) async {
try {
final response = await _apiClient.put(
'/vocabulary/study/sessions/$sessionId/end',
data: {
'durationSeconds': durationSeconds,
'exercises': exercises.map((e) => e.toJson()).toList(),
},
);
return StudySession.fromJson(response.data['data']);
} catch (e) {
throw Exception('结束学习会话失败: $e');
}
}
/// 获取学习会话历史
Future<List<StudySession>> getStudySessionHistory({
int page = 1,
int limit = 20,
DateTime? startDate,
DateTime? endDate,
}) async {
try {
final response = await _apiClient.get(
'/vocabulary/study/sessions',
queryParameters: {
'page': page,
'limit': limit,
if (startDate != null) 'startDate': startDate.toIso8601String(),
if (endDate != null) 'endDate': endDate.toIso8601String(),
},
);
final List<dynamic> data = response.data['data'] ?? [];
return data.map((json) => StudySession.fromJson(json)).toList();
} catch (e) {
throw Exception('获取学习会话历史失败: $e');
}
}
// ==================== 学习统计相关 ====================
/// 获取学习统计
Future<StudyStatistics> getStudyStatistics(DateTime date) async {
try {
final response = await _apiClient.get(
'/vocabulary/study/statistics',
queryParameters: {
'date': date.toIso8601String().split('T')[0],
},
);
return StudyStatistics.fromJson(response.data['data']);
} catch (e) {
throw Exception('获取学习统计失败: $e');
}
}
/// 获取每日词汇统计wordsLearned、studyTimeMinutes
Future<DailyStats> getDailyVocabularyStats({required String userId}) async {
try {
final response = await _enhancedApiService.get<DailyStats>(
'/vocabulary/daily',
queryParameters: {
'user_id': userId,
},
useCache: false,
fromJson: (data) => DailyStats.fromJson(data['data'] ?? data),
);
if (response.success && response.data != null) {
return response.data!;
} else {
throw Exception(response.message);
}
} catch (e) {
throw Exception('获取每日词汇统计失败: $e');
}
}
/// 获取用户词汇整体统计total_studied、accuracy_rate、mastery_stats等
Future<Map<String, dynamic>> getUserVocabularyStats() async {
try {
final response = await _enhancedApiService.get<Map<String, dynamic>>(
'/vocabulary/stats',
fromJson: (data) => Map<String, dynamic>.from(data['data'] ?? data),
);
if (response.success && response.data != null) {
return response.data!;
} else {
throw Exception(response.message);
}
} catch (e) {
throw Exception('获取用户词汇整体统计失败: $e');
}
}
/// 获取学习统计历史
Future<List<StudyStatistics>> getStudyStatisticsHistory({
required DateTime startDate,
required DateTime endDate,
}) async {
try {
final response = await _apiClient.get(
'/vocabulary/study/statistics/history',
queryParameters: {
'startDate': startDate.toIso8601String().split('T')[0],
'endDate': endDate.toIso8601String().split('T')[0],
},
);
final List<dynamic> data = response.data['data'] ?? [];
return data.map((json) => StudyStatistics.fromJson(json)).toList();
} catch (e) {
throw Exception('获取学习统计历史失败: $e');
}
}
// ==================== 智能学习算法 ====================
/// 获取今日需要学习的单词
Future<List<Word>> getTodayStudyWords({
String? vocabularyBookId,
int limit = 20,
}) async {
try {
final response = await _apiClient.get(
'/vocabulary/study/today',
queryParameters: {
if (vocabularyBookId != null) 'vocabularyBookId': vocabularyBookId,
'limit': limit,
},
);
print('=== getTodayStudyWords API响应 ===');
print('原始响应: ${response.data}');
print('data字段类型: ${response.data['data'].runtimeType}');
// 后端返回的结构是 {"data": {"words": []}}
final responseData = response.data['data'];
List<dynamic> data;
if (responseData is Map) {
print('responseData是Map尝试获取words字段');
final words = responseData['words'];
print('words字段类型: ${words.runtimeType}');
data = words is List ? words : [];
} else if (responseData is List) {
print('responseData是List直接使用');
data = responseData;
} else {
print('responseData类型未知: ${responseData.runtimeType}');
data = [];
}
print('最终数据条数: ${data.length}');
if (data.isNotEmpty) {
print('第一条数据: ${data[0]}');
}
return data.map((json) => Word.fromJson(json as Map<String, dynamic>)).toList();
} catch (e, stackTrace) {
// API调用失败返回空列表让UI显示空状态
print('getTodayStudyWords API调用失败: $e');
print('堆栈跟踪: $stackTrace');
return [];
}
}
/// 获取需要复习的单词
Future<List<Word>> getReviewWords({
String? vocabularyBookId,
int limit = 20,
}) async {
try {
final response = await _apiClient.get(
'/vocabulary/study/review',
queryParameters: {
if (vocabularyBookId != null) 'vocabularyBookId': vocabularyBookId,
'limit': limit,
},
);
// 后端返回的结构可能是 {"data": {"words": []}} 或 {"data": []}
final responseData = response.data['data'];
final List<dynamic> data = responseData is Map
? (responseData['words'] ?? [])
: (responseData ?? []);
print('=== getReviewWords API响应 ===');
print('数据条数: ${data.length}');
return data.map((json) => Word.fromJson(json)).toList();
} catch (e) {
// API调用失败返回空列表让UI显示空状态
print('getReviewWords API调用失败: $e');
return [];
}
}
/// 生成单词练习题
Future<Map<String, dynamic>> generateWordExercise(
String wordId,
ExerciseType exerciseType,
) async {
try {
final response = await _apiClient.post(
'/vocabulary/study/exercise',
data: {
'wordId': wordId,
'exerciseType': exerciseType.name,
},
);
return response.data['data'];
} catch (e) {
throw Exception('生成练习题失败: $e');
}
}
// ==================== 本地缓存相关 ====================
/// 缓存词汇书到本地
Future<void> cacheVocabularyBook(VocabularyBook book) async {
try {
await _storageService.setString(
'vocabulary_book_${book.id}',
book.toJson().toString(),
);
} catch (e) {
throw Exception('缓存词汇书失败: $e');
}
}
/// 从本地获取缓存的词汇书
Future<VocabularyBook?> getCachedVocabularyBook(String bookId) async {
try {
final cached = await _storageService.getString('vocabulary_book_$bookId');
if (cached != null) {
// 这里需要实际的JSON解析逻辑
// return VocabularyBook.fromJson(jsonDecode(cached));
}
return null;
} catch (e) {
return null;
}
}
/// 清除本地缓存
Future<void> clearCache() async {
try {
// 清除所有词汇相关的缓存
await _storageService.remove('vocabulary_books');
await _storageService.remove('study_statistics');
} catch (e) {
throw Exception('清除缓存失败: $e');
}
}
// ==================== 工具方法 ====================
/// 计算单词熟练度
int calculateWordProficiency(UserWordProgress progress) {
if (progress.studyCount == 0) return 0;
final accuracy = progress.accuracy;
final studyCount = progress.studyCount;
final reviewInterval = progress.reviewInterval;
// 基于准确率、学习次数和复习间隔计算熟练度
int proficiency = (accuracy * 50).round();
proficiency += (studyCount * 5).clamp(0, 30);
proficiency += (reviewInterval * 2).clamp(0, 20);
return proficiency.clamp(0, 100);
}
/// 计算下次复习时间
DateTime calculateNextReviewTime(UserWordProgress progress) {
final now = DateTime.now();
final accuracy = progress.accuracy;
// 基于准确率调整复习间隔
int intervalDays = progress.reviewInterval;
if (accuracy >= 0.9) {
intervalDays = (intervalDays * 2).clamp(1, 30);
} else if (accuracy >= 0.7) {
intervalDays = (intervalDays * 1.5).round().clamp(1, 14);
} else if (accuracy >= 0.5) {
intervalDays = intervalDays.clamp(1, 7);
} else {
intervalDays = 1;
}
return now.add(Duration(days: intervalDays));
}
/// 随机选择练习类型
ExerciseType getRandomExerciseType() {
final types = ExerciseType.values;
return types[_random.nextInt(types.length)];
}
/// 生成干扰选项
List<String> generateDistractors(
String correctAnswer,
List<String> wordPool,
int count,
) {
final distractors = <String>[];
final shuffled = List<String>.from(wordPool)..shuffle(_random);
for (final word in shuffled) {
if (word != correctAnswer && !distractors.contains(word)) {
distractors.add(word);
if (distractors.length >= count) break;
}
}
return distractors;
}
// ==================== 本地兜底(无后端时) ====================
/// 当后端不可用或接口缺失时,生成基础示例单词(仅用于开发调试)
List<Word> _generateSampleWords(int limit) {
final now = DateTime.now();
final samples = [
{'w': 'apple', 'cn': '苹果', 'def': 'a round fruit of a tree', 'type': 'noun'},
{'w': 'run', 'cn': '跑步', 'def': 'move fast by using ones feet', 'type': 'verb'},
{'w': 'happy', 'cn': '开心的', 'def': 'feeling or showing pleasure', 'type': 'adjective'},
{'w': 'quickly', 'cn': '快速地', 'def': 'at a fast speed', 'type': 'adverb'},
{'w': 'book', 'cn': '', 'def': 'a written or printed work', 'type': 'noun'},
{'w': 'study', 'cn': '学习', 'def': 'apply oneself to learning', 'type': 'verb'},
{'w': 'blue', 'cn': '蓝色的', 'def': 'of the color blue', 'type': 'adjective'},
{'w': 'slowly', 'cn': '缓慢地', 'def': 'at a slow speed', 'type': 'adverb'},
{'w': 'friend', 'cn': '朋友', 'def': 'a person you know well', 'type': 'noun'},
{'w': 'listen', 'cn': '', 'def': 'give attention to sound', 'type': 'verb'},
{'w': 'green', 'cn': '绿色的', 'def': 'of the color green', 'type': 'adjective'},
{'w': 'often', 'cn': '经常', 'def': 'frequently; many times', 'type': 'adverb'},
{'w': 'school', 'cn': '学校', 'def': 'a place for learning', 'type': 'noun'},
{'w': 'write', 'cn': '写作', 'def': 'mark letters or words on a surface', 'type': 'verb'},
{'w': 'smart', 'cn': '聪明的', 'def': 'intelligent or clever', 'type': 'adjective'},
{'w': 'carefully', 'cn': '小心地', 'def': 'with care or attention', 'type': 'adverb'},
{'w': 'music', 'cn': '音乐', 'def': 'art of arranging sounds', 'type': 'noun'},
{'w': 'learn', 'cn': '学习', 'def': 'gain knowledge or skill', 'type': 'verb'},
{'w': 'bright', 'cn': '明亮的/聪明的', 'def': 'giving much light; intelligent', 'type': 'adjective'},
{'w': 'rarely', 'cn': '很少', 'def': 'not often; seldom', 'type': 'adverb'},
];
WordType _parseType(String t) {
switch (t) {
case 'noun':
return WordType.noun;
case 'verb':
return WordType.verb;
case 'adjective':
return WordType.adjective;
case 'adverb':
return WordType.adverb;
default:
return WordType.noun;
}
}
final list = <Word>[];
for (var i = 0; i < samples.length && list.length < limit; i++) {
final s = samples[i];
list.add(
Word(
id: 'sample_${s['w']}',
word: s['w']!,
definitions: [
WordDefinition(
type: _parseType(s['type']!),
definition: s['def']!,
translation: s['cn']!,
frequency: 3,
),
],
examples: [
WordExample(
sentence: 'I like ${s['w']}.',
translation: '我喜欢${s['cn']}.',
),
],
synonyms: const [],
antonyms: const [],
etymology: null,
difficulty: WordDifficulty.beginner,
frequency: 5,
imageUrl: null,
memoryTip: null,
createdAt: now,
updatedAt: now,
),
);
}
return list;
}
/// 根据ID获取单词详情
Future<Word> getWordById(String wordId) async {
try {
final response = await _apiClient.get('/vocabulary/$wordId');
final data = response.data['data'];
return Word.fromJson(data);
} catch (e) {
print('获取单词详情失败: $e');
rethrow;
}
}
}