import 'dart:convert'; import 'dart:io'; import 'dart:ui'; import 'package:dio/dio.dart'; import 'package:qhd_prevention/services/SessionService.dart'; import 'package:qhd_prevention/tools/tools.dart'; /// 全局接口异常 class ApiException implements Exception { final String result; final String message; ApiException(this.result, this.message); @override String toString() => 'ApiException($result): $message'; } /// HTTP 方法枚举 enum Method { get, post, put, delete } /// HTTP 管理器 单例 class HttpManager { HttpManager._internal() { _dio = Dio(BaseOptions( connectTimeout: const Duration(milliseconds: 20000), receiveTimeout: const Duration(milliseconds: 20000), headers: { 'Content-Type': Headers.formUrlEncodedContentType, }, )); _initInterceptors(); } static final HttpManager _instance = HttpManager._internal(); factory HttpManager() => _instance; late final Dio _dio; // 添加401处理回调 static VoidCallback? onUnauthorized; void _initInterceptors() { _dio.interceptors ..add(LogInterceptor(request: true, responseBody: true, error: true)) ..add(InterceptorsWrapper(onError: (err, handler) { // TODO 暂不处理 // 捕获401错误 if (err.response?.statusCode == 401) { // 触发全局登出回调 onUnauthorized?.call(); // 创建自定义异常 final apiException = ApiException( '提示', '您的账号已在其他设备登录,已自动下线' ); // 直接抛出业务异常,跳过后续错误处理 return handler.reject( DioException( requestOptions: err.requestOptions, error: apiException, response: err.response, type: DioExceptionType.badResponse, ), ); } handler.next(err); })); } /// 通用请求方法,返回完整后台 JSON Future> request( String baseUrl, String path, { Method method = Method.post, Map? data, Map? params, CancelToken? cancelToken, String? contentType, // Content-Type,默认为 jsonContentType bool isHeartbeat = false, }) async { printLongString('参数:${jsonEncode(data)}'); Response resp; final url = baseUrl + path; // 动态 headers,默认使用 jsonContentType final String contentTypeValue = contentType ?? Headers.jsonContentType; final headers = { 'Content-Type': contentTypeValue, }; final token = SessionService.instance.token ?? ''; // final token = 'jjb-saas-auth:oauth:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJjbGllbnRJZFwiOlwieGdmemRcIixcImFjY291bnRJZFwiOjE5OTE2NzQ0MzEzMzY4NDk0MDgsXCJ1c2VyVHlwZUVudW1cIjpcIlBMQVRGT1JNXCIsXCJ1c2VySWRcIjoxOTkxNjc0NDI4MjYxNTMxNjQ4LFwidGVuYW50SWRcIjoxOTkxNjc0NDI4MjYxNTMxNjQ4LFwidGVuYW50TmFtZVwiOlwi5Yas5rOz55u45YWz5pa5XCIsXCJ0ZW5hbnRUeXBlSWRcIjoxOTkwNjkzMzg4MDcyMTI0NDE2LFwidGVuYW50UGFyZW50SWRzXCI6XCIwLDE5ODM3NzMwMTMwODYwNDgyNTYsMTk5MTY3NDQyODI2MTUzMTY0OFwiLFwibmFtZVwiOlwi5Yas5rOz55u45YWz5pa5XCIsXCJhY2Nlc3NUaWNrZXRcIjpcIkg0YXBlMkFaRVcxZFR1OTIwOXNzSDREc3pPWjBoTkZ4eEVlZzRmYTJZaFRVUFA0QkZVZXZmSklhTVdoS1wiLFwicmVmcmVzaFRpY2tldFwiOlwiRlRlZUxIaXJVblhueTBMcXNMcUdyc2dFaGpqVlRRN0pncVptVTBLS0JHVkFCU1ExeENtT3RTWmxRbUdpXCIsXCJleHBpcmVJblwiOjYwNDgwMCxcInJlZnJlc2hFeHBpcmVzSW5cIjo2MDQ4MDAsXCJvcmdJZFwiOjE5OTE2NzQ0MjgyNjE1MzE2NDgsXCJvcmdOYW1lXCI6XCLlhqzms7Pnm7jlhbPmlrlcIixcIm9yZ0lkc1wiOlsxOTkxNjc0NDI4MjYxNTMxNjQ4XSxcInJvbGVzVHlwZXNcIjpbXCJHT1ZfQ0hJTERfQUNDT1VOVFwiXSxcInJvbGVJZHNcIjpbMTk5MDY5MjE3NTA2NjgyNDcwNV0sXCJzY29wZXNcIjpbXSxcInJwY1R5cGVFbnVtXCI6XCJIVFRQXCIsXCJiaW5kTW9iaWxlU2lnblwiOlwiRkFMU0VcIn0iLCJpc3MiOiJwcm8tc2VydmVyIiwiZXhwIjoxNzY1OTU4NDIzfQ.RphPGGnh18RdGZ2vB0-2gKHp6bQg3-rKR4xPvDgH1ek'; if (token != null && token.isNotEmpty && !isHeartbeat) { headers['token'] = token; } final options = Options( method: method.name.toUpperCase(), contentType: contentTypeValue, headers: headers, ); try { switch (method) { case Method.get: resp = await _dio.get(url, queryParameters: {...?params, ...?data}, cancelToken: cancelToken, options: options); break; case Method.post: resp = await _dio.post(url, data: data, queryParameters: params, cancelToken: cancelToken, options: options); break; case Method.put: resp = await _dio.put(url, data: data, queryParameters: params, cancelToken: cancelToken, options: options); break; case Method.delete: resp = await _dio.delete(url, queryParameters: params, cancelToken: cancelToken, options: options); break; } } on DioException catch (e) { if (e.error is ApiException) throw e.error as ApiException; throw ApiException('network_error', e.message ?? e.toString()); } final json = resp.data is Map ? resp.data as Map : {}; return json; } } /// 上传文件扩展 extension HttpManagerUpload on HttpManager { Future> uploadImages({ required String baseUrl, required String path, required Map fromData, CancelToken? cancelToken, }) async { fromData['corpinfoId'] = '1983773013086048256'; final form = FormData.fromMap(fromData); final token = SessionService.instance.token ?? ''; final headers = { 'Content-Type': 'multipart/form-data', }; if (token.isNotEmpty) { headers['token'] = token; } try { final resp = await _dio.post( baseUrl + path, data: form, cancelToken: cancelToken, options: Options( method: Method.post.name.toUpperCase(), // contentType 可以省略或保留,multipart/form-data 会被 Dio 正确处理(boundary 自动添加) contentType: 'multipart/form-data', headers: headers, ), ); final json = resp.data is Map ? resp.data as Map : {}; return json; } on DioException catch (e) { // 如果是我们在拦截器里构造的 ApiException(例如 401),则向上抛出该业务异常 if (e.error is ApiException) { throw e.error as ApiException; } // 其它情况统一抛出 ApiException,便于上层统一处理 throw ApiException('network_error', e.message ?? e.toString()); } } }