Initial commit
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../../domain/errors.dart';
|
||||
import '../../contracts/error.dart';
|
||||
import '../../domain/ports/server_sync_gateway.dart';
|
||||
import '../../../shared/utils/app_logger.dart';
|
||||
import '../../../shared/utils/url_utils.dart';
|
||||
|
||||
|
||||
class ServerSyncClient implements ServerSyncGateway {
|
||||
static const _tag = 'ServerSync';
|
||||
|
||||
final Dio _dio;
|
||||
|
||||
ServerSyncClient({Dio? dio})
|
||||
: _dio =
|
||||
dio ??
|
||||
Dio(
|
||||
BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 8),
|
||||
receiveTimeout: const Duration(seconds: 15),
|
||||
sendTimeout: const Duration(seconds: 15),
|
||||
headers: {'Accept': 'application/json'},
|
||||
validateStatus: (status) => status != null && status < 500,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<ServerSyncSnapshot> pull({
|
||||
required String baseUrl,
|
||||
required String accessToken,
|
||||
}) async {
|
||||
final url = '${stripTrailingSlash(baseUrl.trim())}/api/user/server-sync';
|
||||
try {
|
||||
final res = await _dio.get<Map<String, dynamic>>(
|
||||
url,
|
||||
options: Options(headers: {'Authorization': 'Bearer $accessToken'}),
|
||||
);
|
||||
final status = res.statusCode ?? 0;
|
||||
if (!_isSuccess(status)) {
|
||||
throw _httpError(status, res.data);
|
||||
}
|
||||
final body = res.data ?? const <String, dynamic>{};
|
||||
return ServerSyncSnapshot(blob: body['blob'] as String?);
|
||||
} on DioException catch (e) {
|
||||
throw _networkError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> push({
|
||||
required String baseUrl,
|
||||
required String accessToken,
|
||||
required String blob,
|
||||
}) async {
|
||||
final url = '${stripTrailingSlash(baseUrl.trim())}/api/user/server-sync';
|
||||
try {
|
||||
final res = await _dio.put<Map<String, dynamic>>(
|
||||
url,
|
||||
data: {'blob': blob},
|
||||
options: Options(headers: {'Authorization': 'Bearer $accessToken'}),
|
||||
);
|
||||
final status = res.statusCode ?? 0;
|
||||
if (!_isSuccess(status)) {
|
||||
throw _httpError(status, res.data);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
throw _networkError(e);
|
||||
}
|
||||
}
|
||||
|
||||
DomainError _httpError(int status, Map<String, dynamic>? body) {
|
||||
final message = (body?['message'] as String?)?.trim();
|
||||
AppLogger.warn(_tag, 'HTTP $status ${message ?? ''}');
|
||||
return switch (status) {
|
||||
401 || 403 => DomainError(
|
||||
ErrorCode.authForbidden,
|
||||
message ?? '账号登录已过期,请重新登录',
|
||||
details: {'status': status},
|
||||
),
|
||||
413 => DomainError(
|
||||
ErrorCode.invalidParams,
|
||||
message ?? '同步数据过大(上限 256KB)',
|
||||
details: {'status': status},
|
||||
),
|
||||
400 => DomainError(
|
||||
ErrorCode.invalidParams,
|
||||
message ?? '请求无效',
|
||||
details: {'status': status},
|
||||
),
|
||||
_ => DomainError(
|
||||
ErrorCode.serverHttpError,
|
||||
message ?? '同步失败(HTTP $status)',
|
||||
retryable: true,
|
||||
details: {'status': status},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
DomainError _networkError(DioException e) {
|
||||
AppLogger.warn(_tag, 'network error', e);
|
||||
return switch (e.type) {
|
||||
DioExceptionType.connectionTimeout ||
|
||||
DioExceptionType.receiveTimeout ||
|
||||
DioExceptionType.sendTimeout => DomainError(
|
||||
ErrorCode.serverTimeout,
|
||||
'连接超时,请检查网络',
|
||||
retryable: true,
|
||||
),
|
||||
_ => DomainError(
|
||||
ErrorCode.serverUnreachable,
|
||||
'无法连接服务器,请检查网络',
|
||||
retryable: true,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
static bool _isSuccess(int status) => status >= 200 && status < 300;
|
||||
}
|
||||
Reference in New Issue
Block a user