2026-05-12 10:56:18 +08:00

433 lines
13 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<bool> _isCurrentSessionStillValid(DioException e) async {
if (!_shouldLogoutForUnauthorized(e)) {
return true;
}
final verificationHeaders =
Map<String, dynamic>.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<String, dynamic>) {
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<String, dynamic> &&
responseData["errorCode"] == SCErroCode.authUnauthorized.code) {
return false;
}
} catch (_) {}
// 校验结果不明确时不要主动登出,避免单条接口抖动把整次登录踢掉。
return true;
}
class BaseNetworkClient {
final Dio dio;
static const String silentErrorToastKey = 'silentErrorToast';
static const String suppressAuthLogoutKey = 'suppressAuthLogout';
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<T> get<T>(
String path, {
Map<String, dynamic>? queryParams,
Map<String, dynamic>? extra,
bool allowNullBody = false,
required T Function(dynamic) fromJson,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
return _req<T>(
path,
method: 'GET',
queryParams: queryParams,
extra: extra,
allowNullBody: allowNullBody,
fromJson: fromJson,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
// 通用 put 请求
Future<T> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParams,
Map<String, dynamic>? extra,
bool allowNullBody = false,
required T Function(dynamic) fromJson,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
return _req<T>(
path,
method: 'PUT',
data: data,
queryParams: queryParams,
extra: extra,
allowNullBody: allowNullBody,
fromJson: fromJson,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
// 通用 POST 请求
Future<T> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParams,
Map<String, dynamic>? extra,
bool allowNullBody = false,
required T Function(dynamic) fromJson,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
return _req<T>(
path,
method: 'POST',
data: data,
queryParams: queryParams,
extra: extra,
allowNullBody: allowNullBody,
fromJson: fromJson,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
// 通用 POST 请求
Future<T> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParams,
Map<String, dynamic>? extra,
bool allowNullBody = false,
required T Function(dynamic) fromJson,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async {
return _req<T>(
path,
method: 'DELETE',
data: data,
queryParams: queryParams,
extra: extra,
allowNullBody: allowNullBody,
fromJson: fromJson,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}
// 核心请求方法
Future<T> _req<T>(
String path, {
required String method,
dynamic data,
Map<String, dynamic>? queryParams,
Map<String, dynamic>? extra,
bool allowNullBody = false,
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<String, dynamic>,
fromJsonT: fromJson,
);
// 业务逻辑成功判断
if (baseResponse.success) {
if (baseResponse.body != null) {
return baseResponse.body!;
} else if (allowNullBody && null is T) {
return null as T;
} 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<DioException> _hdlErr(DioException e) async {
SCLoadingManager.hide();
final bool silentErrorToast =
e.requestOptions.extra[BaseNetworkClient.silentErrorToastKey] == true;
final handledBusinessError = await _handleBusinessErrorResponse(
e,
silentErrorToast: silentErrorToast,
);
if (handledBusinessError != null) {
return handledBusinessError;
}
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:
return DioException(
requestOptions: e.requestOptions,
response: e.response,
error: e.error ?? e.message ?? 'Server fail',
);
case DioExceptionType.cancel:
return DioException(
requestOptions: e.requestOptions,
response: e.response,
type: DioExceptionType.cancel,
error: e.error ?? 'Cancel',
);
default:
return DioException(
requestOptions: e.requestOptions,
response: e.response,
type: e.type,
error: e.error ?? e.message ?? 'Net fail',
message: e.message,
);
}
}
Future<DioException?> _handleBusinessErrorResponse(
DioException e, {
required bool silentErrorToast,
}) async {
final responseData = e.response?.data;
if (responseData == null) {
return null;
}
final errorCode = _readErrorCode(responseData);
final errorMsg = _readErrorMessage(responseData);
if (errorCode == null && errorMsg == null) {
return null;
}
if (errorCode == SCErroCode.userNotRegistered.code) {
// 用户还没有注册Google/Apple 登录后需要继续补充资料。
SCTts.show("Please register an account first.");
final BuildContext? context = navigatorKey.currentContext;
if (context != null && context.mounted) {
SCNavigatorUtils.push(context, LoginRouter.editProfile, replace: false);
}
} else if (errorCode == SCErroCode.authUnauthorized.code) {
// token过期
if (e.requestOptions.extra[BaseNetworkClient.suppressAuthLogoutKey] ==
true) {
return DioException(
requestOptions: e.requestOptions,
response: e.response,
type: e.type,
error: '服务器错误: ${errorCode ?? ""} ${errorMsg ?? ""}'.trim(),
message: e.message,
);
}
final shouldLogout =
!SCNavigatorUtils.inLoginPage &&
!await _isCurrentSessionStillValid(e);
final BuildContext? context = navigatorKey.currentContext;
if (context != null && context.mounted && shouldLogout) {
AccountStorage().logout(context);
SCNavigatorUtils.inLoginPage = true;
}
} else {
final message = errorMsg ?? "";
if (message.endsWith("balance not made")) {
final BuildContext? context = navigatorKey.currentContext;
if (context != null && context.mounted) {
SCRoomUtils.goRecharge(context);
}
} else if (!silentErrorToast && message.isNotEmpty) {
SCTts.show(message);
}
}
return DioException(
requestOptions: e.requestOptions,
response: e.response,
type: e.type,
error: '服务器错误: ${errorCode ?? ""} ${errorMsg ?? ""}'.trim(),
message: e.message,
);
}
int? _readErrorCode(dynamic responseData) {
if (responseData is! Map) {
return null;
}
final value = responseData["errorCode"] ?? responseData["code"];
if (value is int) {
return value;
}
if (value is num) {
return value.toInt();
}
if (value is String) {
return int.tryParse(value);
}
return null;
}
String? _readErrorMessage(dynamic responseData) {
if (responseData is Map) {
return responseData["errorMsg"]?.toString() ??
responseData["message"]?.toString();
}
final message = responseData?.toString().trim();
return message == null || message.isEmpty ? null : message;
}
}
class NotSuccessException implements Exception {
final String message;
NotSuccessException(this.message);
factory NotSuccessException.fromRespData(ResponseData respData) {
return NotSuccessException(respData.errorMsg ?? "操作失败");
}
}