init
This commit is contained in:
331
client/lib/features/speaking/services/speaking_service.dart
Normal file
331
client/lib/features/speaking/services/speaking_service.dart
Normal file
@@ -0,0 +1,331 @@
|
||||
import '../../../core/models/api_response.dart';
|
||||
import '../../../core/services/enhanced_api_service.dart';
|
||||
import '../models/speaking_scenario.dart';
|
||||
import '../models/conversation.dart';
|
||||
import '../models/speaking_stats.dart';
|
||||
import '../models/pronunciation_assessment.dart';
|
||||
import '../models/pronunciation_item.dart';
|
||||
|
||||
/// 口语训练服务
|
||||
class SpeakingService {
|
||||
static final SpeakingService _instance = SpeakingService._internal();
|
||||
factory SpeakingService() => _instance;
|
||||
SpeakingService._internal();
|
||||
|
||||
final EnhancedApiService _enhancedApiService = EnhancedApiService();
|
||||
|
||||
// 缓存时长配置
|
||||
static const Duration _shortCacheDuration = Duration(minutes: 5);
|
||||
static const Duration _longCacheDuration = Duration(hours: 1);
|
||||
|
||||
/// 获取口语场景列表
|
||||
Future<ApiResponse<List<SpeakingTask>>> getSpeakingScenarios({
|
||||
SpeakingScenario? scenario,
|
||||
SpeakingDifficulty? difficulty,
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<List<SpeakingTask>>(
|
||||
'/speaking/scenarios',
|
||||
queryParameters: {
|
||||
'page': page,
|
||||
'page_size': limit,
|
||||
if (scenario != null) 'category': scenario.name,
|
||||
if (difficulty != null) 'level': difficulty.name,
|
||||
},
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) {
|
||||
final scenarios = data['scenarios'] as List?;
|
||||
if (scenarios == null) return [];
|
||||
return scenarios.map((json) => SpeakingTask.fromJson(json)).toList();
|
||||
},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(message: '获取成功', data: response.data!);
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取口语场景失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取单个口语场景详情
|
||||
Future<ApiResponse<SpeakingTask>> getSpeakingScenario(String scenarioId) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<SpeakingTask>(
|
||||
'/speaking/scenarios/$scenarioId',
|
||||
cacheDuration: _longCacheDuration,
|
||||
fromJson: (data) => SpeakingTask.fromJson(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(message: '获取成功', data: response.data!);
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取口语场景详情失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始口语对话
|
||||
Future<ApiResponse<Conversation>> startConversation(String scenarioId) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.post<Conversation>(
|
||||
'/speaking/records',
|
||||
data: {'scenario_id': scenarioId},
|
||||
fromJson: (data) => Conversation.fromJson(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(message: '对话开始成功', data: response.data!);
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '开始对话失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户口语历史
|
||||
Future<ApiResponse<List<Conversation>>> getUserSpeakingHistory({
|
||||
int page = 1,
|
||||
int limit = 20,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<List<Conversation>>(
|
||||
'/speaking/records',
|
||||
queryParameters: {
|
||||
'page': page,
|
||||
'page_size': limit,
|
||||
},
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) {
|
||||
final records = data['records'] as List?;
|
||||
if (records == null) return [];
|
||||
return records.map((json) => Conversation.fromJson(json)).toList();
|
||||
},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(message: '获取历史记录成功', data: response.data!);
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取历史记录失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取用户口语统计
|
||||
Future<ApiResponse<SpeakingStats>> getUserSpeakingStatistics() async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<SpeakingStats>(
|
||||
'/speaking/stats',
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) => SpeakingStats.fromJson(data['data'] ?? data),
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(message: '获取统计数据成功', data: response.data!);
|
||||
}
|
||||
|
||||
// API失败时返回默认统计数据
|
||||
final stats = SpeakingStats(
|
||||
totalSessions: 45,
|
||||
totalMinutes: 320,
|
||||
averageScore: 85.5,
|
||||
scenarioStats: {
|
||||
SpeakingScenario.dailyConversation: 8,
|
||||
SpeakingScenario.businessMeeting: 5,
|
||||
SpeakingScenario.jobInterview: 3,
|
||||
SpeakingScenario.shopping: 4,
|
||||
SpeakingScenario.restaurant: 3,
|
||||
SpeakingScenario.travel: 2,
|
||||
SpeakingScenario.academic: 2,
|
||||
SpeakingScenario.socializing: 1,
|
||||
},
|
||||
difficultyStats: {
|
||||
SpeakingDifficulty.beginner: 10,
|
||||
SpeakingDifficulty.intermediate: 15,
|
||||
SpeakingDifficulty.advanced: 8,
|
||||
},
|
||||
progressData: [],
|
||||
skillAnalysis: SpeakingSkillAnalysis(
|
||||
criteriaScores: {
|
||||
PronunciationCriteria.accuracy: 85.0,
|
||||
PronunciationCriteria.fluency: 82.0,
|
||||
PronunciationCriteria.completeness: 88.0,
|
||||
PronunciationCriteria.prosody: 80.0,
|
||||
},
|
||||
commonErrors: {},
|
||||
strengths: ['发音准确'],
|
||||
weaknesses: ['语调需要改进'],
|
||||
recommendations: ['多练习语调'],
|
||||
improvementRate: 0.05,
|
||||
lastAnalyzed: DateTime.now(),
|
||||
),
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
|
||||
return ApiResponse.success(message: '获取统计数据成功', data: stats);
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取统计数据失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取推荐任务
|
||||
Future<ApiResponse<List<SpeakingTask>>> getRecommendedTasks() async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<List<SpeakingTask>>(
|
||||
'/speaking/scenarios/recommendations',
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) {
|
||||
final scenarios = data['scenarios'] as List?;
|
||||
if (scenarios == null) return [];
|
||||
return scenarios.map((json) => SpeakingTask.fromJson(json)).toList();
|
||||
},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(message: '获取推荐任务成功', data: response.data!);
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取推荐任务失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取热门任务
|
||||
Future<ApiResponse<List<SpeakingTask>>> getPopularTasks() async {
|
||||
try {
|
||||
// 热门任务可以通过获取场景列表并按某种排序获得
|
||||
final response = await _enhancedApiService.get<List<SpeakingTask>>(
|
||||
'/speaking/scenarios',
|
||||
queryParameters: {
|
||||
'page': 1,
|
||||
'page_size': 10,
|
||||
'sort': 'popular', // 如果后端支持排序
|
||||
},
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) {
|
||||
final scenarios = data['scenarios'] as List?;
|
||||
if (scenarios == null) return [];
|
||||
return scenarios.map((json) => SpeakingTask.fromJson(json)).toList();
|
||||
},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(message: '获取热门任务成功', data: response.data!);
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取热门任务失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取发音练习项目(通过词汇后端数据映射)
|
||||
Future<ApiResponse<List<PronunciationItem>>> getPronunciationItems(
|
||||
PronunciationType type, {
|
||||
int limit = 50,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _enhancedApiService.get<List<PronunciationItem>>(
|
||||
'/vocabulary/study/today',
|
||||
queryParameters: {
|
||||
'limit': limit,
|
||||
},
|
||||
cacheDuration: _shortCacheDuration,
|
||||
fromJson: (data) {
|
||||
dynamic root = data;
|
||||
if (root is Map && root.containsKey('data')) {
|
||||
root = root['data'];
|
||||
}
|
||||
|
||||
List<dynamic> list = const [];
|
||||
if (root is List) {
|
||||
list = root;
|
||||
} else if (root is Map) {
|
||||
final words = root['words'];
|
||||
if (words is List) {
|
||||
list = words;
|
||||
}
|
||||
}
|
||||
|
||||
final items = <PronunciationItem>[];
|
||||
|
||||
for (final w in list) {
|
||||
final id = w['id'].toString();
|
||||
final level = (w['level'] ?? '').toString();
|
||||
final audioUrl = (w['audio_url'] ?? w['audio_us_url'] ?? w['audio_uk_url'] ?? '').toString();
|
||||
final phonetic = (w['phonetic'] ?? w['phonetic_us'] ?? w['phonetic_uk'] ?? '').toString();
|
||||
final createdAtStr = (w['created_at'] ?? DateTime.now().toIso8601String()).toString();
|
||||
final createdAt = DateTime.tryParse(createdAtStr) ?? DateTime.now();
|
||||
|
||||
DifficultyLevel mapLevel(String l) {
|
||||
switch (l) {
|
||||
case 'beginner':
|
||||
case 'elementary':
|
||||
return DifficultyLevel.beginner;
|
||||
case 'intermediate':
|
||||
return DifficultyLevel.intermediate;
|
||||
case 'advanced':
|
||||
case 'expert':
|
||||
return DifficultyLevel.advanced;
|
||||
default:
|
||||
return DifficultyLevel.intermediate;
|
||||
}
|
||||
}
|
||||
|
||||
if (type == PronunciationType.word) {
|
||||
items.add(PronunciationItem(
|
||||
id: id,
|
||||
text: (w['word'] ?? '').toString(),
|
||||
phonetic: phonetic,
|
||||
audioUrl: audioUrl,
|
||||
type: PronunciationType.word,
|
||||
difficulty: mapLevel(level),
|
||||
category: level.isEmpty ? '词汇练习' : level,
|
||||
tips: const [],
|
||||
createdAt: createdAt,
|
||||
));
|
||||
} else if (type == PronunciationType.sentence) {
|
||||
final examples = (w['examples'] as List<dynamic>?) ?? [];
|
||||
for (var i = 0; i < examples.length; i++) {
|
||||
final ex = examples[i] as Map<String, dynamic>;
|
||||
final exAudio = (ex['audio_url'] ?? '').toString();
|
||||
final exCreated = DateTime.tryParse((ex['created_at'] ?? createdAtStr).toString()) ?? createdAt;
|
||||
items.add(PronunciationItem(
|
||||
id: '${id}_ex_$i',
|
||||
text: (ex['sentence'] ?? ex['sentence_en'] ?? '').toString(),
|
||||
phonetic: '',
|
||||
audioUrl: exAudio,
|
||||
type: PronunciationType.sentence,
|
||||
difficulty: mapLevel(level),
|
||||
category: '例句',
|
||||
tips: const [],
|
||||
createdAt: exCreated,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
return ApiResponse.success(message: '获取发音练习成功', data: response.data!);
|
||||
} else {
|
||||
return ApiResponse.error(message: response.message);
|
||||
}
|
||||
} catch (e) {
|
||||
return ApiResponse.error(message: '获取发音练习失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user