Files
ai-contract-platform/mobile/lib/shared/network/api_client.dart
T
2026-04-24 13:44:17 +08:00

233 lines
7.4 KiB
Dart

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<AuthSession> 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<AuthSession> 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<List<ReviewHistoryItem>> 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<String, dynamic>>()
.map(ReviewHistoryItem.fromJson)
.toList(growable: false);
}
Future<ReviewDetail> 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<ReviewResultSummary> 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<ReviewReportSummary> 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<Map<String, dynamic>> 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<String, dynamic> _decodeBody(String body) {
final decoded = jsonDecode(body);
if (decoded is Map<String, dynamic>) {
return decoded;
}
throw const ApiException('接口响应格式错误');
}
Map<String, dynamic> _extractMap(Object? value, {required String fallback}) {
if (value is Map<String, dynamic>) {
return value;
}
throw ApiException(fallback);
}
String _extractMessage(Map<String, dynamic> 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;
}