import 'dart:io'; import 'package:dio/dio.dart'; import 'package:novyronst/common/api/api_string.dart'; import 'package:novyronst/common/api/ny_network_intercept.dart'; import 'package:novyronst/common/help/help_token.dart'; import 'package:novyronst/tools/widgets/app_toast_util.dart'; /// HTTP 请求工具类(单例) class AppService { static AppService? _instance; late Dio _dio; static AppService get instance { _instance ??= AppService._internal(); return _instance!; } AppService._internal() { _initDio(); } Dio get dio => _dio; /// 初始化 Dio 配置 void _initDio() { _dio = Dio(BaseOptions( baseUrl: ApiString.baseUrl, connectTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 15), sendTimeout: const Duration(seconds: 15), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, responseType: ResponseType.json, )); // 添加自定义拦截器 _dio.interceptors.add(NyNetworkIntercept()); // 开发环境添加日志 // if (!kReleaseMode) { // _dio.interceptors.add(TalkerDioLogger( // settings: TalkerDioLoggerSettings( // printRequestHeaders: true, // printRequestData: true, // printResponseData: true, // printResponseHeaders: false, // ), // )); // } } /// 更新配置 void updateBaseOptions(BaseOptions options) { _dio.options = options; } } /// ============ 请求方法封装 ============ class ApiRequest { static final Dio _dio = AppService.instance.dio; /// GET 请求(带重试) static Future> get( String path, { Map? queryParameters, Options? options, CancelToken? cancelToken, int? retry, }) async { try { final response = await _executeRequest( () => _dio.get( path, queryParameters: queryParameters, options: options, cancelToken: cancelToken, ), ); return HttpResult.fromResponse(response); } on DioException catch (e) { return _handleError(e); } catch (e) { return HttpResult.error(e.toString()); } } /// POST 请求(带重试) static Future> post( String path, { dynamic data, Map? queryParameters, Options? options, CancelToken? cancelToken, int? retry, }) async { try { final response = await _executeRequest( () => _dio.post( path, data: data, queryParameters: queryParameters, options: options, cancelToken: cancelToken, ), ); return HttpResult.fromResponse(response); } on DioException catch (e) { return _handleError(e); } catch (e) { return HttpResult.error(e.toString()); } } /// 文件上传(带重试) static Future> fileUpload( String path, { required File file, String fieldName = 'file', Map? queryParameters, Options? options, CancelToken? cancelToken, ProgressCallback? onSendProgress, int? retry, }) async { try { final formData = FormData.fromMap({ fieldName: await MultipartFile.fromFile(file.path), }); final response = await _executeRequest( () => _dio.post( path, data: formData, queryParameters: queryParameters, options: options ?? Options(contentType: 'multipart/form-data'), cancelToken: cancelToken, onSendProgress: onSendProgress, ), ); return HttpResult.fromResponse(response); } on DioException catch (e) { return _handleError(e); } catch (e) { return HttpResult.error(e.toString()); } } /// ============ 分页请求(原项目的 fetchPage) ============ static Future> fetchPage( String url, { int? page, int? pageSize, Map? extraParams, required T Function(dynamic json) fromJson, bool error = true, String method = 'get', int? retry, }) async { try { final params = { 'current_page': page?.toString() ?? '1', 'page_size': pageSize?.toString() ?? '20', ...?extraParams, }; Response response; if (method.toLowerCase() == 'get') { response = await _executeRequest( () => _dio.get(url, queryParameters: params), ); } else { response = await _executeRequest( () => _dio.post(url, data: params), ); } if (response.statusCode != 200) { throw Exception("HTTP ${response.statusCode}"); } final data = response.data; if (data is Map) { final base = BasePageResponse.fromJson(data); if (!base.success) { if (error) { // 这里可以触发 Toast AppToastUtil.showError(base.msg); } throw Exception(base.msg); } final result = base.data; if (result is List) { return result.map((e) => fromJson(e)).toList(); } else if (result is Map && result['list'] is List) { return (result['list'] as List).map((e) => fromJson(e)).toList(); } else { throw Exception("data error"); } } else { throw Exception("format error"); } } catch (e) { // _handleError(); rethrow; } } /// 直接执行请求,不重试 static Future _executeRequest( Future Function() requestFn, ) async { try { final response = await requestFn(); // 如果状态码不是 200,直接抛出异常 if (response.statusCode != 200) { throw DioException( requestOptions: response.requestOptions, response: response, type: DioExceptionType.badResponse, error: 'HTTP ${response.statusCode}', ); } return response; } catch (e) { // 直接重新抛出异常,不重试 rethrow; } } // ============ 重试机制 ============ static Future _retryRequest( Future Function() requestFn, { int? retries, }) async { int attempt = 0; final maxRetries = retries ?? 2; while (attempt <= maxRetries) { try { final response = await requestFn(); if (response.statusCode == 200) { return response; } // 401/402 特殊处理,由拦截器处理 if (response.statusCode == 401 || response.statusCode == 402) { if (attempt == maxRetries) { throw DioException( requestOptions: response.requestOptions, response: response, type: DioExceptionType.badResponse, error: 'errorCode:${response.statusCode}', ); } } if (attempt == maxRetries) { throw DioException( requestOptions: response.requestOptions, response: response, type: DioExceptionType.badResponse, error: 'HTTP ${response.statusCode}', ); } } catch (e) { if (attempt == maxRetries) { rethrow; } } attempt++; await Future.delayed(const Duration(milliseconds: 300)); } throw Exception("retry failed (unreachable)"); } // ============ 错误处理 ============ static HttpResult _handleError(DioException e) { final Response? resp = e.response; // 尝试从响应中解析错误信息 if (resp != null && resp.data != null) { try { final parsed = HttpResult.fromResponse(resp); if (!parsed.success) { // 401/402 错误,触发重新登录 if (resp.statusCode == 401 || resp.statusCode == 402) { HelpToken().update(); if (resp.statusCode == 402) { AppToastUtil.showError('账号已在其他设备登录,请重新登录'); } } return parsed; } } catch (_) {} } String message = _getErrorMessage(e); return HttpResult.error(message, httpStatus: resp?.statusCode); } static String _getErrorMessage(DioException e) { switch (e.type) { case DioExceptionType.connectionTimeout: return '连接超时,请稍后重试'; case DioExceptionType.sendTimeout: return '发送超时,请稍后重试'; case DioExceptionType.receiveTimeout: return '接收超时,请稍后重试'; case DioExceptionType.badResponse: final statusCode = e.response?.statusCode; if (statusCode == 401) { HelpToken().update(); return '未授权,请重新登录'; } if (statusCode == 402) { HelpToken().update(); return '账号已在其他设备登录,请重新登录'; } return _handleStatusCode(statusCode); case DioExceptionType.cancel: return '请求已取消'; case DioExceptionType.connectionError: return '网络连接失败,请检查网络设置'; default: return '网络错误,请稍后重试'; } } static String _handleStatusCode(int? statusCode) { switch (statusCode) { case 400: return '请求错误'; case 403: return '拒绝访问'; case 404: return '请求资源不存在'; case 500: return '服务器内部错误'; case 502: return '网关错误'; case 503: return '服务不可用'; default: return '请求失败'; } } } /// 统一响应:业务成功见 [isSuccess] class HttpResult { final int? status; final bool success; final String? message; final T? data; final int? errorCode; const HttpResult({ this.status, this.success = false, this.message, this.data, this.errorCode, }); bool get isSuccess => success; factory HttpResult.fromResponse(Response response) { try { final int httpStatus = response.statusCode ?? 0; final dynamic raw = response.data; final Map? root = _jsonMap(raw); // 明确失败包:{ "success": false, "error": { ... } } if (root != null && root['success'] == false) { final payload = ApiErrorPayload.tryParse(root); return HttpResult( status: httpStatus, success: false, message: payload?.message ?? root['message']?.toString() ?? '请求失败', errorCode: payload?.code, data: null, ); } // HTTP 2xx:body 即为业务数据 if (httpStatus >= 200 && httpStatus < 300) { return HttpResult( status: httpStatus, success: true, message: null, data: raw as T?, errorCode: null, ); } // 非 2xx:尽量从 body 里拆 error if (root != null) { final payload = ApiErrorPayload.tryParse(root); if (payload != null) { return HttpResult( status: httpStatus, success: false, message: payload.message, errorCode: payload.code, data: null, ); } } return HttpResult( status: httpStatus, success: false, message: _handleStatusCode(httpStatus), data: null, ); } catch (e) { return HttpResult( status: response.statusCode, success: false, message: '数据解析失败: $e', data: null, ); } } factory HttpResult.error(String message, {int? httpStatus}) { return HttpResult( success: false, status: httpStatus, message: message, ); } factory HttpResult.networkError() { return HttpResult( success: false, status: -1, message: '网络连接失败,请检查网络后重试', ); } // ============ 私有方法 ============ static Map? _jsonMap(dynamic value) { if (value is Map) return value; if (value is Map) { try { return value.cast(); } catch (_) { return Map.from(value as Map); } } return null; } static String _handleStatusCode(int? statusCode) { switch (statusCode) { case 400: return '请求错误'; case 401: return '未授权,请重新登录'; case 403: return '拒绝访问'; case 404: return '请求资源不存在'; case 500: return '服务器内部错误'; case 502: return '网关错误'; case 503: return '服务不可用'; default: return '请求失败'; } } } /// 后台错误体:`{ "error": { "code", "details", "message" }, "success": false }` class ApiErrorPayload { final int? code; final dynamic details; final String? message; const ApiErrorPayload({this.code, this.details, this.message}); static ApiErrorPayload? tryParse(Map? root) { if (root == null) return null; final err = root['error']; if (err is! Map) return null; final m = _jsonMap(err); if (m == null) return null; return ApiErrorPayload( code: _parseInt(m['code']), details: m['details'], message: m['message']?.toString(), ); } static Map? _jsonMap(dynamic value) { if (value is Map) return value; if (value is Map) { try { return value.cast(); } catch (_) { return Map.from(value as Map); } } return null; } static int? _parseInt(dynamic v) { if (v == null) return null; if (v is int) return v; if (v is num) return v.toInt(); return int.tryParse(v.toString()); } } /// 分页响应模型(原项目的 GlimzoGetxBaseResponse 改造) class BasePageResponse { final int code; final String msg; final T? data; BasePageResponse({ required this.code, required this.msg, this.data, }); bool get success => code == 200; factory BasePageResponse.fromJson(Map json) { return BasePageResponse( code: json['code'] ?? -1, msg: json['msg'] ?? '', data: json['data'], ); } } /// Map 数据提取工具(原项目的 GlimzoMapRes 改造) class MapRes { static List getList({ required Map mapData, required String alias, required T Function(Map) fromJson, }) { final rawList = mapData[alias]; if (rawList is List) { return rawList .whereType>() .map(fromJson) .toList(); } return []; } static T toModel({ required Map json, required T Function(Map) fromJson, }) { return fromJson(json); } }