331 lines
12 KiB
Dart
331 lines
12 KiB
Dart
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');
|
|
}
|
|
}
|
|
} |