import 'dart:convert'; import 'package:http/http.dart' as http; import '../../app/config/app_config.dart'; import '../../features/auth/domain/auth_session.dart'; import '../../features/review/domain/review_draft.dart'; import '../../features/review_history/domain/review_detail.dart'; import '../../features/review_history/domain/review_history_item.dart'; import '../../features/review_history/domain/review_result_summary.dart'; import '../constants/api_endpoints.dart'; class ApiClient { const ApiClient(); String buildUrl(String path) => '${AppConfig.apiBaseUrl}$path'; String get reviewTasksUrl => buildUrl(ApiEndpoints.reviewTasks); String get loginUrl => buildUrl(ApiEndpoints.login); String get currentUserUrl => buildUrl(ApiEndpoints.currentUser); Future login({required String account, required String password}) async { final response = await http.post( Uri.parse(loginUrl), headers: const {'Content-Type': 'application/json'}, body: jsonEncode({'account': account, 'password': password}), ); final body = _decodeBody(response.body); if (response.statusCode != 200) { throw ApiException(_extractMessage(body, fallback: '登录失败')); } final data = _extractMap(body['data'], fallback: '登录响应缺少 data'); final user = _extractMap(data['user'], fallback: '登录响应缺少用户信息'); final accessToken = data['access_token']?.toString() ?? ''; if (accessToken.isEmpty) { throw const ApiException('登录响应缺少 access_token'); } return AuthSession( accessToken: accessToken, userId: user['id']?.toString() ?? '', tenantId: user['tenant_id']?.toString() ?? '', username: user['username']?.toString() ?? account, userDisplayName: user['display_name']?.toString() ?? account, role: user['role']?.toString() ?? 'unknown', status: user['status']?.toString() ?? 'unknown', ); } Future fetchCurrentUser(String accessToken) async { final response = await http.get( Uri.parse(currentUserUrl), headers: {'Authorization': 'Bearer $accessToken'}, ); final body = _decodeBody(response.body); if (response.statusCode != 200) { throw ApiException(_extractMessage(body, fallback: '获取当前用户失败')); } final user = _extractMap(body['data'], fallback: '当前用户响应缺少 data'); return AuthSession( accessToken: accessToken, userId: user['id']?.toString() ?? '', tenantId: user['tenant_id']?.toString() ?? '', username: user['username']?.toString() ?? '', userDisplayName: user['display_name']?.toString() ?? user['username']?.toString() ?? '', role: user['role']?.toString() ?? 'unknown', status: user['status']?.toString() ?? 'unknown', ); } Future> fetchReviewHistory(String accessToken) async { final response = await http.get( Uri.parse(reviewTasksUrl), headers: {'Authorization': 'Bearer $accessToken'}, ); final body = _decodeBody(response.body); if (response.statusCode != 200) { throw ApiException(_extractMessage(body, fallback: '获取审查记录失败')); } final data = _extractMap(body['data'], fallback: '审查记录响应缺少 data'); final items = data['items']; if (items is! List) { throw const ApiException('审查记录响应缺少 items'); } return items .whereType>() .map(ReviewHistoryItem.fromJson) .toList(growable: false); } Future fetchReviewDetail({ required String accessToken, required String reviewId, }) async { final response = await http.get( Uri.parse('$reviewTasksUrl/$reviewId'), headers: {'Authorization': 'Bearer $accessToken'}, ); final body = _decodeBody(response.body); if (response.statusCode != 200) { throw ApiException(_extractMessage(body, fallback: '获取审查详情失败')); } final data = _extractMap(body['data'], fallback: '审查详情响应缺少 data'); return ReviewDetail.fromJson(data); } Future fetchReviewResult({ required String accessToken, required String reviewId, }) async { final response = await http.get( Uri.parse('$reviewTasksUrl/$reviewId/result'), headers: {'Authorization': 'Bearer $accessToken'}, ); final body = _decodeBody(response.body); if (response.statusCode != 200) { throw ApiException(_extractMessage(body, fallback: '获取审查结果失败')); } final data = _extractMap(body['data'], fallback: '审查结果响应缺少 data'); return ReviewResultSummary.fromJson(data); } Future fetchReviewReport({ required String accessToken, required String reviewId, }) async { final response = await http.get( Uri.parse('$reviewTasksUrl/$reviewId/report'), headers: {'Authorization': 'Bearer $accessToken'}, ); final body = _decodeBody(response.body); if (response.statusCode != 200) { throw ApiException(_extractMessage(body, fallback: '获取报告信息失败')); } final data = _extractMap(body['data'], fallback: '报告响应缺少 data'); return ReviewReportSummary( fileName: data['file_name']?.toString() ?? '', downloadUrl: _normalizeUrl(data['download_url']?.toString() ?? ''), ); } Future> submitReview({ required String accessToken, required ReviewDraft draft, }) async { final request = http.MultipartRequest('POST', Uri.parse(reviewTasksUrl)) ..headers['Authorization'] = 'Bearer $accessToken' ..fields.addAll(draft.toFormFields()); final attachment = draft.attachment; if (attachment != null) { request.files.add( http.MultipartFile.fromBytes( 'file', attachment.bytes, filename: attachment.name, ), ); } final streamed = await request.send(); final response = await http.Response.fromStream(streamed); final body = _decodeBody(response.body); if (response.statusCode != 200) { throw ApiException(_extractMessage(body, fallback: '提交审查失败')); } final code = body['code']; if (code is num && code != 0) { throw ApiException(_extractMessage(body, fallback: '提交审查失败')); } return _extractMap(body['data'], fallback: '审查响应缺少 data'); } Map _decodeBody(String body) { final decoded = jsonDecode(body); if (decoded is Map) { return decoded; } throw const ApiException('接口响应格式错误'); } Map _extractMap(Object? value, {required String fallback}) { if (value is Map) { return value; } throw ApiException(fallback); } String _extractMessage(Map body, {required String fallback}) { final detail = body['detail']?.toString(); if (detail != null && detail.isNotEmpty) { return detail; } final message = body['message']?.toString(); if (message != null && message.isNotEmpty) { return message; } return fallback; } String _normalizeUrl(String path) { if (path.startsWith('http://') || path.startsWith('https://')) { return path; } return buildUrl(path); } } class ApiException implements Exception { const ApiException(this.message); final String message; @override String toString() => message; }