// api.dart import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/modules/auth/login_route.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/shared/tools/sc_loading_manager.dart'; import 'package:yumi/main.dart'; import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; import '../../../models/enum/sc_erro_code.dart'; export 'package:dio/dio.dart'; _parseAndDecode(String response) => jsonDecode(response); parseJson(String text) => compute(_parseAndDecode, text); String _normalizeAuthorizationHeader(Object? authorization) { if (authorization == null) { return ""; } if (authorization is Iterable) { return authorization.map((value) => value.toString()).join(","); } return authorization.toString().trim(); } bool _shouldLogoutForUnauthorized(DioException e) { final currentToken = AccountStorage().getToken(); if (currentToken.isEmpty) { return false; } final requestAuthorization = _normalizeAuthorizationHeader( e.requestOptions.headers["Authorization"], ); if (requestAuthorization.isEmpty) { return false; } return requestAuthorization == "Bearer $currentToken"; } Future _isCurrentSessionStillValid(DioException e) async { if (!_shouldLogoutForUnauthorized(e)) { return true; } final verificationHeaders = Map.from(e.requestOptions.headers) ..remove("Content-Length") ..remove("content-length"); final verificationClient = Dio( BaseOptions( baseUrl: e.requestOptions.baseUrl.isNotEmpty ? e.requestOptions.baseUrl : SCGlobalConfig.apiHost, connectTimeout: const Duration(seconds: 5), receiveTimeout: const Duration(seconds: 5), sendTimeout: const Duration(seconds: 5), responseType: ResponseType.json, validateStatus: (status) => status != null && status < 500, ), ); try { final verificationResponse = await verificationClient.get( "/app/h5/identity", options: Options(headers: verificationHeaders), ); final responseData = verificationResponse.data; if (verificationResponse.statusCode == 401) { return false; } if (responseData is Map) { if (responseData["status"] == true) { return true; } if (responseData["errorCode"] == SCErroCode.authUnauthorized.code) { return false; } } } on DioException catch (verificationError) { final responseData = verificationError.response?.data; if (verificationError.response?.statusCode == 401) { return false; } if (responseData is Map && responseData["errorCode"] == SCErroCode.authUnauthorized.code) { return false; } } catch (_) {} // 校验结果不明确时不要主动登出,避免单条接口抖动把整次登录踢掉。 return true; } class BaseNetworkClient { final Dio dio; static const String silentErrorToastKey = 'silentErrorToast'; static const String baseUrlOverrideKey = 'baseUrlOverride'; BaseNetworkClient() : dio = Dio() { _confD(); init(); } void _confD() { dio.transformer = BackgroundTransformer()..jsonDecodeCallback = parseJson; dio.options = BaseOptions( connectTimeout: const Duration(seconds: 12), receiveTimeout: const Duration(seconds: 30), contentType: 'application/json; charset=UTF-8', ); } void init() {} // 通用 GET 请求 Future get( String path, { Map? queryParams, Map? extra, required T Function(dynamic) fromJson, CancelToken? cancelToken, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { return _req( path, method: 'GET', queryParams: queryParams, extra: extra, fromJson: fromJson, cancelToken: cancelToken, onSendProgress: onSendProgress, onReceiveProgress: onReceiveProgress, ); } // 通用 put 请求 Future put( String path, { dynamic data, Map? queryParams, Map? extra, required T Function(dynamic) fromJson, CancelToken? cancelToken, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { return _req( path, method: 'PUT', data: data, queryParams: queryParams, extra: extra, fromJson: fromJson, cancelToken: cancelToken, onSendProgress: onSendProgress, onReceiveProgress: onReceiveProgress, ); } // 通用 POST 请求 Future post( String path, { dynamic data, Map? queryParams, Map? extra, required T Function(dynamic) fromJson, CancelToken? cancelToken, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { return _req( path, method: 'POST', data: data, queryParams: queryParams, extra: extra, fromJson: fromJson, cancelToken: cancelToken, onSendProgress: onSendProgress, onReceiveProgress: onReceiveProgress, ); } // 通用 POST 请求 Future delete( String path, { dynamic data, Map? queryParams, Map? extra, required T Function(dynamic) fromJson, CancelToken? cancelToken, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { return _req( path, method: 'DELETE', data: data, queryParams: queryParams, extra: extra, fromJson: fromJson, cancelToken: cancelToken, onSendProgress: onSendProgress, onReceiveProgress: onReceiveProgress, ); } // 核心请求方法 Future _req( String path, { required String method, dynamic data, Map? queryParams, Map? extra, CancelToken? cancelToken, ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, required T Function(dynamic) fromJson, }) async { try { final response = await dio.request( path, data: data, queryParameters: queryParams, options: Options(method: method, extra: extra), cancelToken: cancelToken, onSendProgress: onSendProgress, onReceiveProgress: onReceiveProgress, ); // 处理基础响应 final baseResponse = ResponseData.fromJson( response.data as Map, fromJsonT: fromJson, ); // 业务逻辑成功判断 if (baseResponse.success) { if (baseResponse.body != null) { return baseResponse.body!; } else { if (T == bool) return false as T; if (T == int) return 0 as T; if (T == String) return "" as T; SCLoadingManager.hide(); throw Exception('Response data is null'); } } else { // 业务逻辑错误 SCLoadingManager.hide(); throw DioException( requestOptions: response.requestOptions, response: response, error: '业务错误: ${baseResponse.errorMsg}', ); } } on DioException catch (e) { // 网络错误处理 throw await _hdlErr(e); } catch (e) { SCLoadingManager.hide(); throw Exception('未知错误: $e'); } } // 错误处理 Future _hdlErr(DioException e) async { SCLoadingManager.hide(); final bool silentErrorToast = e.requestOptions.extra[BaseNetworkClient.silentErrorToastKey] == true; switch (e.type) { case DioExceptionType.connectionTimeout: return DioException(requestOptions: e.requestOptions, error: '连接超时'); case DioExceptionType.sendTimeout: return DioException(requestOptions: e.requestOptions, error: '发送超时'); case DioExceptionType.receiveTimeout: return DioException(requestOptions: e.requestOptions, error: '接收超时'); case DioExceptionType.badResponse: var errorCode = e.response?.data["errorCode"]; var errorMsg = e.response?.data["errorMsg"]; if (errorCode == SCErroCode.userNotRegistered.code) { //用户还没有注册 SCTts.show("Please register an account first."); BuildContext? context = navigatorKey.currentContext; if (context != null) { SCNavigatorUtils.push( context, LoginRouter.editProfile, replace: false, ); } } else if (errorCode == SCErroCode.authUnauthorized.code) { //token过期 final shouldLogout = !SCNavigatorUtils.inLoginPage && !await _isCurrentSessionStillValid(e); final BuildContext? context = navigatorKey.currentContext; if (context != null && shouldLogout) { AccountStorage().logout(context); SCNavigatorUtils.inLoginPage = true; } } else { if (errorMsg.toString().endsWith("balance not made")) { BuildContext? context = navigatorKey.currentContext; if (context != null) { SCRoomUtils.goRecharge(context); } } else { if (!silentErrorToast) { SCTts.show(errorMsg); } } } return DioException( requestOptions: e.requestOptions, response: e.response, error: '服务器错误: ${e.response?.data["errorCode"]}', ); case DioExceptionType.cancel: return DioException(requestOptions: e.requestOptions, error: 'Cancel'); default: return DioException( requestOptions: e.requestOptions, error: 'Net fail', ); } } } class NotSuccessException implements Exception { final String message; NotSuccessException(this.message); factory NotSuccessException.fromRespData(ResponseData respData) { return NotSuccessException(respData.errorMsg ?? "操作失败"); } }