skywood/lib/global/request/app_request.dart
2026-07-01 17:05:22 +08:00

627 lines
16 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.

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:skywood/global/request/api_const.dart';
import 'package:skywood/global/request/network_interceptor.dart';
import 'package:skywood/utils/tools/app_toast.dart';
import '../../utils/tools/token_manage.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: ApiConst.apiLink,
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(NetworkInterceptor());
// 开发环境添加日志
// if (!kReleaseMode) {
// _dio.interceptors.add(TalkerDioLogger(
// settings: TalkerDioLoggerSettings(
// printRequestHeaders: true,
// printRequestData: true,
// printResponseData: true,
// printResponseHeaders: false,
// ),
// ));
// }
}
/// 更新配置
void updateBaseOptions(BaseOptions options) {
_dio.options = options;
}
}
/// ============ 请求方法封装 ============
class AppRequest {
static final Dio _dio = AppService.instance.dio;
/// GET 请求(带重试)
static Future<HttpResult<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
int? retry,
}) async {
try {
final response = await _executeRequest(
() => _dio.get(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
),
);
return HttpResult<T>.fromResponse(response);
} on DioException catch (e) {
return _handleError<T>(e);
} catch (e) {
return HttpResult<T>.error(e.toString());
}
}
/// POST 请求(带重试)
static Future<HttpResult<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? 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<T>.fromResponse(response);
} on DioException catch (e) {
return _handleError<T>(e);
} catch (e) {
return HttpResult<T>.error(e.toString());
}
}
/// PUT 请求(带重试)
static Future<HttpResult<T>> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
int? retry,
}) async {
try {
final response = await _executeRequest(
() => _dio.put(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
),
);
return HttpResult<T>.fromResponse(response);
} on DioException catch (e) {
return _handleError<T>(e);
} catch (e) {
return HttpResult<T>.error(e.toString());
}
}
/// DELETE 请求(带重试)
static Future<HttpResult<T>> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
int? retry,
}) async {
try {
final response = await _executeRequest(
// retries: retry,
() => _dio.delete(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
),
);
return HttpResult<T>.fromResponse(response);
} on DioException catch (e) {
return _handleError<T>(e);
} catch (e) {
return HttpResult<T>.error(e.toString());
}
}
/// 文件上传(带重试)
static Future<HttpResult<T>> fileUpload<T>(
String path, {
required File file,
String fieldName = 'file',
Map<String, dynamic>? 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<T>.fromResponse(response);
} on DioException catch (e) {
return _handleError<T>(e);
} catch (e) {
return HttpResult<T>.error(e.toString());
}
}
/// ============ 分页请求(原项目的 fetchPage ============
static Future<List<T>> fetchPage<T>(
String url, {
int? page,
int? pageSize,
Map<String, dynamic>? 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<String, dynamic>) {
final base = BasePageResponse.fromJson(data);
if (!base.success) {
if (error) {
// 这里可以触发 Toast
AppToast.showError(base.msg);
}
throw Exception(base.msg);
}
final result = base.data;
if (result is List) {
return result.map<T>((e) => fromJson(e)).toList();
} else if (result is Map && result['list'] is List) {
return (result['list'] as List).map<T>((e) => fromJson(e)).toList();
} else {
throw Exception("data error");
}
} else {
throw Exception("format error");
}
} catch (e) {
// _handleError();
rethrow;
}
}
/// 直接执行请求,不重试
static Future<Response> _executeRequest(
Future<Response> 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<Response> _retryRequest(
Future<Response> 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<T> _handleError<T>(DioException e) {
final Response? resp = e.response;
// 尝试从响应中解析错误信息
if (resp != null && resp.data != null) {
try {
final parsed = HttpResult<T>.fromResponse(resp);
if (!parsed.success) {
// 401/402 错误,触发重新登录
if (resp.statusCode == 401 || resp.statusCode == 402) {
TokenManager().update();
if (resp.statusCode == 402) {
AppToast.showError('账号已在其他设备登录,请重新登录');
}
}
return parsed;
}
} catch (_) {}
}
String message = _getErrorMessage(e);
return HttpResult<T>.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) {
TokenManager().update();
return '未授权,请重新登录';
}
if (statusCode == 402) {
TokenManager().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<T> {
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<String, dynamic>? root = _jsonMap(raw);
// 明确失败包:{ "success": false, "error": { ... } }
if (root != null && root['success'] == false) {
final payload = ApiErrorPayload.tryParse(root);
return HttpResult<T>(
status: httpStatus,
success: false,
message: payload?.message ?? root['message']?.toString() ?? '请求失败',
errorCode: payload?.code,
data: null,
);
}
// HTTP 2xxbody 即为业务数据
if (httpStatus >= 200 && httpStatus < 300) {
return HttpResult<T>(
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<T>(
status: httpStatus,
success: false,
message: payload.message,
errorCode: payload.code,
data: null,
);
}
}
return HttpResult<T>(
status: httpStatus,
success: false,
message: _handleStatusCode(httpStatus),
data: null,
);
} catch (e) {
return HttpResult<T>(
status: response.statusCode,
success: false,
message: '数据解析失败: $e',
data: null,
);
}
}
factory HttpResult.error(String message, {int? httpStatus}) {
return HttpResult<T>(
success: false,
status: httpStatus,
message: message,
);
}
factory HttpResult.networkError() {
return HttpResult<T>(
success: false,
status: -1,
message: '网络连接失败,请检查网络后重试',
);
}
// ============ 私有方法 ============
static Map<String, dynamic>? _jsonMap(dynamic value) {
if (value is Map<String, dynamic>) return value;
if (value is Map) {
try {
return value.cast<String, dynamic>();
} catch (_) {
return Map<String, dynamic>.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<String, dynamic>? 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<String, dynamic>? _jsonMap(dynamic value) {
if (value is Map<String, dynamic>) return value;
if (value is Map) {
try {
return value.cast<String, dynamic>();
} catch (_) {
return Map<String, dynamic>.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<T> {
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<String, dynamic> json) {
return BasePageResponse(
code: json['code'] ?? -1,
msg: json['msg'] ?? '',
data: json['data'],
);
}
}
/// Map 数据提取工具(原项目的 GlimzoMapRes 改造)
class MapRes {
static List<T> getList<T>({
required Map<dynamic, dynamic> mapData,
required String alias,
required T Function(Map<String, dynamic>) fromJson,
}) {
final rawList = mapData[alias];
if (rawList is List) {
return rawList
.whereType<Map<String, dynamic>>()
.map(fromJson)
.toList();
}
return <T>[];
}
static T toModel<T>({
required Map<String, dynamic> json,
required T Function(Map<String, dynamic>) fromJson,
}) {
return fromJson(json);
}
}