4a93bd1c44
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
233 lines
7.4 KiB
Dart
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;
|
|
}
|