import 'dart:io'; import 'package:dio/dio.dart'; import 'package:dio/io.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:miaoji_zhang/shared/services/session_store.dart'; class ApiClient { ApiClient._(); static final ApiClient instance = ApiClient._(); static const _storage = FlutterSecureStorage(); static const _tokenKey = 'auth_token'; static final sessionExpired = ValueNotifier(0); static bool _handlingUnauthorized = false; static const _internalBuild = bool.fromEnvironment('INTERNAL_BUILD'); static const _internalTestBaseUrl = 'https://lt.frp-say.com:38012'; static const _configuredBaseUrl = String.fromEnvironment('API_BASE_URL'); static const _temporaryFrpHost = 'lt.frp-say.com'; static const _temporaryFrpPort = 38012; static const _temporaryFrpCertificateSha1 = '509C3210161E72FB9DA5183D3D2152F63871C31F'; static const String baseUrl = _configuredBaseUrl != '' ? _configuredBaseUrl : _internalBuild ? _internalTestBaseUrl : 'https://api.invalid'; static bool get isInternalBuild => _internalBuild; late final Dio dio = _createDio(); Dio _createDio() { final client = Dio( BaseOptions( baseUrl: baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 120), ), ) ..interceptors.add( InterceptorsWrapper( onRequest: (options, handler) async { final token = await _storage.read(key: _tokenKey); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } handler.next(options); }, onError: (error, handler) async { final unauthorized = error.response?.statusCode == 401; final data = error.response?.data; final aiDenied = error.response?.statusCode == 403 && data is Map && data['code'] == 'AI_PERMISSION_DENIED'; if (aiDenied) { await SessionStore.instance.setAiEnabled(false); } final isAuthRequest = error.requestOptions.path.startsWith( '/api/auth/', ); if (unauthorized && !isAuthRequest && !_handlingUnauthorized) { _handlingUnauthorized = true; await SessionStore.instance.markNeedsReauth(); sessionExpired.value++; Future.delayed( const Duration(seconds: 1), () => _handlingUnauthorized = false, ); } handler.next(error); }, ), ); final apiUri = Uri.tryParse(baseUrl); if (_internalBuild && apiUri?.scheme == 'https' && apiUri?.host == _temporaryFrpHost && apiUri?.port == _temporaryFrpPort) { client.httpClientAdapter = IOHttpClientAdapter( createHttpClient: () { final httpClient = HttpClient(); httpClient.badCertificateCallback = (certificate, host, port) { final certificateSha1 = certificate.sha1 .map((byte) => byte.toRadixString(16).padLeft(2, '0')) .join() .toUpperCase(); return host == _temporaryFrpHost && port == _temporaryFrpPort && certificateSha1 == _temporaryFrpCertificateSha1; }; return httpClient; }, ); } return client; } Future saveToken(String token) => _storage.write(key: _tokenKey, value: token); Future readToken() => _storage.read(key: _tokenKey); Future clearToken() => _storage.delete(key: _tokenKey); } bool isConnectivityError(Object error) => error is DioException && (error.type == DioExceptionType.connectionTimeout || error.type == DioExceptionType.connectionError || error.type == DioExceptionType.receiveTimeout || error.type == DioExceptionType.sendTimeout); String apiErrorMessage(Object e) { if (e is OnlineFeatureRequiredException) return e.message; if (e is StateError) return e.message; if (e is DioException) { final data = e.response?.data; if (data is Map && data['message'] != null) return data['message'] as String; if (e.type == DioExceptionType.connectionTimeout || e.type == DioExceptionType.connectionError || e.type == DioExceptionType.receiveTimeout) { return '无法连接服务器,请检查网络'; } } return '出错了,请稍后再试'; }