init
This commit is contained in:
209
client/lib/core/network/ai_api_service.dart
Normal file
209
client/lib/core/network/ai_api_service.dart
Normal file
@@ -0,0 +1,209 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../services/storage_service.dart';
|
||||
import 'api_endpoints.dart';
|
||||
import '../config/environment.dart';
|
||||
|
||||
/// AI相关API服务
|
||||
class AIApiService {
|
||||
static String get _baseUrl => EnvironmentConfig.baseUrl;
|
||||
|
||||
/// 获取认证头部
|
||||
Map<String, String> _getAuthHeaders() {
|
||||
final storageService = StorageService.instance;
|
||||
final token = storageService.getString(StorageKeys.accessToken);
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
if (token != null) 'Authorization': 'Bearer $token',
|
||||
};
|
||||
}
|
||||
|
||||
/// 写作批改
|
||||
Future<Map<String, dynamic>> correctWriting({
|
||||
required String content,
|
||||
required String taskType,
|
||||
}) async {
|
||||
try {
|
||||
final headers = _getAuthHeaders();
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/api/v1/ai/writing/correct'),
|
||||
headers: headers,
|
||||
body: json.encode({
|
||||
'content': content,
|
||||
'task_type': taskType,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to correct writing: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error correcting writing: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 口语评估
|
||||
Future<Map<String, dynamic>> evaluateSpeaking({
|
||||
required String audioText,
|
||||
required String prompt,
|
||||
}) async {
|
||||
try {
|
||||
final headers = _getAuthHeaders();
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/api/v1/ai/speaking/evaluate'),
|
||||
headers: headers,
|
||||
body: json.encode({
|
||||
'audio_text': audioText,
|
||||
'prompt': prompt,
|
||||
}),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to evaluate speaking: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error evaluating speaking: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取AI使用统计
|
||||
Future<Map<String, dynamic>> getAIUsageStats() async {
|
||||
try {
|
||||
final headers = _getAuthHeaders();
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/api/v1/ai/stats'),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to get AI stats: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error getting AI stats: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传音频文件
|
||||
Future<Map<String, dynamic>> uploadAudio(File audioFile) async {
|
||||
try {
|
||||
final storageService = StorageService.instance;
|
||||
final token = storageService.getString(StorageKeys.accessToken);
|
||||
final request = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse('$_baseUrl/api/v1/upload/audio'),
|
||||
);
|
||||
|
||||
if (token != null) {
|
||||
request.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
request.files.add(
|
||||
await http.MultipartFile.fromPath('audio', audioFile.path),
|
||||
);
|
||||
|
||||
final streamedResponse = await request.send();
|
||||
final response = await http.Response.fromStream(streamedResponse);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to upload audio: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error uploading audio: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传图片文件
|
||||
Future<Map<String, dynamic>> uploadImage(File imageFile) async {
|
||||
try {
|
||||
final storageService = StorageService.instance;
|
||||
final token = storageService.getString(StorageKeys.accessToken);
|
||||
final request = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse('$_baseUrl/api/v1/upload/image'),
|
||||
);
|
||||
|
||||
if (token != null) {
|
||||
request.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
request.files.add(
|
||||
await http.MultipartFile.fromPath('image', imageFile.path),
|
||||
);
|
||||
|
||||
final streamedResponse = await request.send();
|
||||
final response = await http.Response.fromStream(streamedResponse);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to upload image: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error uploading image: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除文件
|
||||
Future<Map<String, dynamic>> deleteFile(String fileId) async {
|
||||
try {
|
||||
final headers = _getAuthHeaders();
|
||||
final response = await http.delete(
|
||||
Uri.parse('$_baseUrl/api/v1/upload/file/$fileId'),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to delete file: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error deleting file: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取文件信息
|
||||
Future<Map<String, dynamic>> getFileInfo(String fileId) async {
|
||||
try {
|
||||
final headers = _getAuthHeaders();
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/api/v1/upload/file/$fileId'),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to get file info: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error getting file info: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取上传统计
|
||||
Future<Map<String, dynamic>> getUploadStats({int days = 30}) async {
|
||||
try {
|
||||
final headers = _getAuthHeaders();
|
||||
final response = await http.get(
|
||||
Uri.parse('$_baseUrl/api/v1/upload/stats?days=$days'),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return json.decode(response.body);
|
||||
} else {
|
||||
throw Exception('Failed to get upload stats: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error getting upload stats: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
252
client/lib/core/network/api_client.dart
Normal file
252
client/lib/core/network/api_client.dart
Normal file
@@ -0,0 +1,252 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../services/navigation_service.dart';
|
||||
import '../routes/app_routes.dart';
|
||||
|
||||
/// API客户端配置
|
||||
class ApiClient {
|
||||
static ApiClient? _instance;
|
||||
late Dio _dio;
|
||||
late StorageService _storageService;
|
||||
|
||||
ApiClient._internal() {
|
||||
_dio = Dio();
|
||||
}
|
||||
|
||||
static Future<ApiClient> getInstance() async {
|
||||
if (_instance == null) {
|
||||
_instance = ApiClient._internal();
|
||||
_instance!._storageService = await StorageService.getInstance();
|
||||
await _instance!._setupInterceptors();
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
static ApiClient get instance {
|
||||
if (_instance == null) {
|
||||
throw Exception('ApiClient not initialized. Call ApiClient.getInstance() first.');
|
||||
}
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
Dio get dio => _dio;
|
||||
|
||||
/// 配置拦截器
|
||||
Future<void> _setupInterceptors() async {
|
||||
// 基础配置
|
||||
_dio.options = BaseOptions(
|
||||
baseUrl: AppConstants.baseUrl,
|
||||
connectTimeout: Duration(milliseconds: AppConstants.connectTimeout),
|
||||
receiveTimeout: Duration(milliseconds: AppConstants.receiveTimeout),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
);
|
||||
|
||||
// 请求拦截器
|
||||
_dio.interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
// 添加认证token
|
||||
final token = await _storageService.getToken();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
handler.next(options);
|
||||
},
|
||||
onResponse: (response, handler) {
|
||||
handler.next(response);
|
||||
},
|
||||
onError: (error, handler) async {
|
||||
// 处理401错误,尝试刷新token
|
||||
if (error.response?.statusCode == 401) {
|
||||
final refreshed = await _refreshToken();
|
||||
if (refreshed) {
|
||||
// 重新发送请求
|
||||
final options = error.requestOptions;
|
||||
final token = await _storageService.getToken();
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
|
||||
try {
|
||||
final response = await _dio.fetch(options);
|
||||
handler.resolve(response);
|
||||
return;
|
||||
} catch (e) {
|
||||
// 刷新后仍然失败,清除token并跳转登录
|
||||
await _clearTokensAndRedirectToLogin();
|
||||
}
|
||||
} else {
|
||||
// 刷新失败,清除token并跳转登录
|
||||
await _clearTokensAndRedirectToLogin();
|
||||
}
|
||||
}
|
||||
|
||||
handler.next(error);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// 日志拦截器(仅在调试模式下)
|
||||
if (const bool.fromEnvironment('dart.vm.product') == false) {
|
||||
_dio.interceptors.add(
|
||||
LogInterceptor(
|
||||
requestHeader: true,
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
responseHeader: false,
|
||||
error: true,
|
||||
logPrint: (obj) => print(obj),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新token
|
||||
Future<bool> _refreshToken() async {
|
||||
try {
|
||||
final refreshToken = await _storageService.getRefreshToken();
|
||||
if (refreshToken == null || refreshToken.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final response = await _dio.post(
|
||||
'/auth/refresh',
|
||||
data: {'refresh_token': refreshToken},
|
||||
options: Options(
|
||||
headers: {'Authorization': null}, // 移除Authorization头
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
await _storageService.saveToken(data['access_token']);
|
||||
if (data['refresh_token'] != null) {
|
||||
await _storageService.saveRefreshToken(data['refresh_token']);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Token refresh failed: $e');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// 清除token并跳转登录
|
||||
Future<void> _clearTokensAndRedirectToLogin() async {
|
||||
await _storageService.clearTokens();
|
||||
|
||||
// 跳转到登录页面并清除所有历史记录
|
||||
NavigationService.instance.navigateToAndClearStack(Routes.login);
|
||||
|
||||
// 显示提示信息
|
||||
NavigationService.instance.showErrorSnackBar('登录已过期,请重新登录');
|
||||
}
|
||||
|
||||
/// GET请求
|
||||
Future<Response<T>> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
return await _dio.get<T>(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// POST请求
|
||||
Future<Response<T>> post<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
return await _dio.post<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// PUT请求
|
||||
Future<Response<T>> put<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
return await _dio.put<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// DELETE请求
|
||||
Future<Response<T>> delete<T>(
|
||||
String path, {
|
||||
dynamic data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
return await _dio.delete<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// 上传文件
|
||||
Future<Response<T>> upload<T>(
|
||||
String path,
|
||||
FormData formData, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
}) async {
|
||||
return await _dio.post<T>(
|
||||
path,
|
||||
data: formData,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
);
|
||||
}
|
||||
|
||||
/// 下载文件
|
||||
Future<Response> download(
|
||||
String urlPath,
|
||||
String savePath, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) async {
|
||||
return await _dio.download(
|
||||
urlPath,
|
||||
savePath,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
}
|
||||
77
client/lib/core/network/api_endpoints.dart
Normal file
77
client/lib/core/network/api_endpoints.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import '../config/environment.dart';
|
||||
|
||||
/// API端点配置
|
||||
class ApiEndpoints {
|
||||
// 基础URL - 从环境配置获取
|
||||
static String get baseUrl => EnvironmentConfig.baseUrl;
|
||||
|
||||
// 认证相关
|
||||
static const String login = '/auth/login';
|
||||
static const String register = '/auth/register';
|
||||
static const String logout = '/auth/logout';
|
||||
static const String refreshToken = '/auth/refresh';
|
||||
static const String forgotPassword = '/auth/forgot-password';
|
||||
static const String resetPassword = '/auth/reset-password';
|
||||
static const String changePassword = '/auth/change-password';
|
||||
static const String socialLogin = '/auth/social-login';
|
||||
static const String verifyEmail = '/auth/verify-email';
|
||||
static const String resendVerificationEmail = '/auth/resend-verification';
|
||||
|
||||
// 用户相关
|
||||
static const String userInfo = '/user/profile';
|
||||
static const String updateProfile = '/user/profile';
|
||||
static const String uploadAvatar = '/user/avatar';
|
||||
static const String checkUsername = '/user/check-username';
|
||||
static const String checkEmail = '/user/check-email';
|
||||
|
||||
// 学习相关
|
||||
static const String learningProgress = '/learning/progress';
|
||||
static const String learningStats = '/learning/stats';
|
||||
static const String dailyGoal = '/learning/daily-goal';
|
||||
|
||||
// 词汇相关
|
||||
static const String vocabulary = '/vocabulary';
|
||||
static const String vocabularyTest = '/vocabulary/test';
|
||||
static const String vocabularyProgress = '/vocabulary/progress';
|
||||
static const String wordBooks = '/vocabulary/books';
|
||||
static const String wordLists = '/vocabulary/lists';
|
||||
|
||||
// 听力相关
|
||||
static const String listening = '/listening';
|
||||
static const String listeningMaterials = '/listening/materials';
|
||||
static const String listeningRecords = '/listening/records';
|
||||
static const String listeningStats = '/listening/stats';
|
||||
|
||||
// 阅读相关
|
||||
static const String reading = '/reading';
|
||||
static const String readingMaterials = '/reading/materials';
|
||||
static const String readingRecords = '/reading/records';
|
||||
static const String readingStats = '/reading/stats';
|
||||
|
||||
// 写作相关
|
||||
static const String writing = '/writing';
|
||||
static const String writingPrompts = '/writing/prompts';
|
||||
static const String writingSubmissions = '/writing/submissions';
|
||||
static const String writingStats = '/writing/stats';
|
||||
|
||||
// 口语相关
|
||||
static const String speaking = '/speaking';
|
||||
static const String speakingScenarios = '/speaking/scenarios';
|
||||
static const String speakingRecords = '/speaking/records';
|
||||
static const String speakingStats = '/speaking/stats';
|
||||
|
||||
// AI相关
|
||||
static const String aiChat = '/ai/chat';
|
||||
static const String aiCorrection = '/ai/correction';
|
||||
static const String aiSuggestion = '/ai/suggestion';
|
||||
|
||||
// 文件上传
|
||||
static const String upload = '/upload';
|
||||
static const String uploadAudio = '/upload/audio';
|
||||
static const String uploadImage = '/upload/image';
|
||||
|
||||
// 系统相关
|
||||
static const String version = '/version';
|
||||
static const String config = '/system/config';
|
||||
static const String feedback = '/system/feedback';
|
||||
}
|
||||
Reference in New Issue
Block a user