feat: 整体常用控件封装,常用工具类。首页数据对接
This commit is contained in:
1 parent
d90d023e85
commit
fbcd33ea2b
50 files changed
+4558
-264
No files matched your search
@@ -0,0 +1,99 @@
|
||||
final class ApiConst {
|
||||
ApiConst._();
|
||||
|
||||
static const appName = 'skywood';
|
||||
static const int pageSize = 12;
|
||||
static const String apiLink = 'https://api-quickeltv.quickeltv.com/quickeltv';
|
||||
|
||||
static String webPrefix = "quickeltv.com";
|
||||
|
||||
static String webIndex = "https://$webPrefix";
|
||||
|
||||
static String webPrivacy = "https://$webPrefix/private";
|
||||
|
||||
static String webAgreement = "https://$webPrefix/user_policy";
|
||||
|
||||
static String webmemberAgreement = "https://$webPrefix/member_ship_agreement";
|
||||
|
||||
static String webHelpIndex = "https://campaign.$webPrefix/pages/leave/index";
|
||||
|
||||
static String webHelpList = "https://campaign.$webPrefix/pages/leave/list";
|
||||
|
||||
static String webHelpDetail =
|
||||
"https://campaign.$webPrefix/pages/leave/detail";
|
||||
|
||||
static String iosDownloadUrl = "";
|
||||
|
||||
static String androidDownloadUrl = "";
|
||||
}
|
||||
|
||||
class ApiString {
|
||||
static const String login = "/customer/login";
|
||||
// /customer/register
|
||||
static const String register = "/customer/register";
|
||||
// /customer/info
|
||||
static const String userInfo = "/customer/info";
|
||||
// /customer/signout
|
||||
static const String signOut = "/customer/signout";
|
||||
// /customer/logoff
|
||||
static const String logOff = "/customer/logoff";
|
||||
|
||||
/// events
|
||||
// eventCreateHistory POST /createHistory 上报播放历史
|
||||
// eventUserEnterApp POST /customer/enterTheApp 用户进入 App
|
||||
// eventUserOnline POST /customer/onLine 用户在线,可带 prevToken
|
||||
// eventUserLeaveApp POST /customer/leaveApp 用户离开 App,可带 prevToken
|
||||
// eventPlaySeconds POST /uploadHistorySeconds 上报单集播放秒数
|
||||
// eventWatchTime POST /watchShortDuration 上报短剧观看时长
|
||||
// eventReport POST /event/add 通用事件上报,目前枚举只有 force_update
|
||||
static const String eventCreateHistory = "/createHistory";
|
||||
static const String eventUserEnterApp = "/customer/enterTheApp";
|
||||
static const String eventUserOnline = "/customer/onLine";
|
||||
static const String eventUserLeaveApp = "/customer/leaveApp";
|
||||
static const String eventPlaySeconds = "/uploadHistorySeconds";
|
||||
static const String eventWatchTime = "/watchShortDuration";
|
||||
static const String eventReport = "/event/add";
|
||||
}
|
||||
|
||||
class ApiStringPlay {
|
||||
// getRecommandsList GET /getRecommands 推荐列表,分页参数 current_page/page_size,可带 revolution
|
||||
// getVideoDetail GET /getVideoDetails 获取剧详情,query: short_play_id, video_id
|
||||
// getDetailsRecommand GET /getDetailsRecommand 详情页推荐列表;注意方法参数 page/pageSize/data 当前没有传入请求
|
||||
static const String getRecommandsList = "/getRecommands";
|
||||
static const String getVideoDetail = "/getVideoDetails";
|
||||
static const String getDetailsRecommand = "/getDetailsRecommand";
|
||||
|
||||
// getHomeModules GET /home/all-modules 首页全部模块数据
|
||||
// getRankingList POST /homeRanking 首页榜单,body 带 type,支持 most_trending/top_searched/new_releases
|
||||
// getCategoriesList GET /getCategories 分类列表
|
||||
// getCategoriesDetail GET /videoList 分类详情/剧列表,分页 + extraParams
|
||||
// getWaterflowList POST /newShortPlay 首页瀑布流,分页 + extraParams
|
||||
static const String getHomeModules = "/home/all-modules";
|
||||
static const String getRankingList = "/homeRanking";
|
||||
static const String getCategoriesList = "/getCategories";
|
||||
static const String getCategoriesDetail = "/videoList";
|
||||
static const String getWaterflowList = "/newShortPlay";
|
||||
|
||||
// getSearchHots GET /search/hots 搜索热门列表
|
||||
// getSearch GET /search 关键词搜索,query: search
|
||||
// collect POST /collect 收藏,body: short_play_id, video_id
|
||||
// cancelCollect POST /cancelCollect 取消收藏,body: short_play_id
|
||||
// getMyCollections GET /myCollections 我的收藏列表,分页
|
||||
// getMyHistorys GET /myHistorys 播放历史列表,分页
|
||||
// getNoticeNum POST /noticeNum 获取通知数量
|
||||
// versionControl GET /customer/versionControl 版本检查
|
||||
// getLanguges GET /languges 获取语言列表,接口名拼写就是 languges
|
||||
// getTranslates GET /translates 获取语言包,query: lang_key,默认 en
|
||||
static const String getSearchHots = "/search/hots";
|
||||
static const String getSearch = "/search";
|
||||
static const String collect = "/collect";
|
||||
static const String cancelCollect = "/cancelCollect";
|
||||
static const String getMyCollections = "/myCollections";
|
||||
static const String getMyHistorys = "/myHistorys";
|
||||
static const String getNoticeNum = "/noticeNum";
|
||||
static const String versionControl = "/customer/versionControl";
|
||||
static const String getLanguges = "/languges";
|
||||
static const String getTranslates = "/translates";
|
||||
}
|
||||
|
||||
|
||||
+617
-142
@@ -1,154 +1,629 @@
|
||||
import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.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 'package:talker_dio_logger/talker_dio_logger.dart';
|
||||
|
||||
class AppRequest {
|
||||
import '../../utils/tools/token_manage.dart';
|
||||
|
||||
static const Duration defaultTimeout = Duration(seconds: 15);
|
||||
static final AppRequest instance = AppRequest();
|
||||
final Dio dio;
|
||||
/// HTTP 请求工具类(单例)
|
||||
class AppService {
|
||||
static AppService? _instance;
|
||||
late Dio _dio;
|
||||
|
||||
AppRequest({
|
||||
Dio? dio,
|
||||
String baseUrl = '',
|
||||
Duration connectTimeout = defaultTimeout,
|
||||
Duration sendTimeout = defaultTimeout,
|
||||
Duration receiveTimeout = defaultTimeout,
|
||||
}) : dio =
|
||||
dio ??
|
||||
Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
connectTimeout: connectTimeout,
|
||||
sendTimeout: sendTimeout,
|
||||
receiveTimeout: receiveTimeout,
|
||||
responseType: ResponseType.json,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
|
||||
void setBaseUrl(String baseUrl) {
|
||||
dio.options.baseUrl = baseUrl;
|
||||
static AppService get instance {
|
||||
_instance ??= AppService._internal();
|
||||
return _instance!;
|
||||
}
|
||||
|
||||
void setHeader(String key, Object? value) {
|
||||
dio.options.headers[key] = value;
|
||||
AppService._internal() {
|
||||
_initDio();
|
||||
}
|
||||
|
||||
void removeHeader(String key) {
|
||||
dio.options.headers.remove(key);
|
||||
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 setToken(String token, {String prefix = 'Bearer'}) {
|
||||
final value = prefix.isEmpty ? token : '$prefix $token';
|
||||
setHeader('Authorization', value);
|
||||
}
|
||||
|
||||
void clearToken() {
|
||||
removeHeader('Authorization');
|
||||
}
|
||||
|
||||
Future<T?> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) {
|
||||
return request<T>(
|
||||
path,
|
||||
method: 'GET',
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
Future<T?> post<T>(
|
||||
String path, {
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) {
|
||||
return request<T>(
|
||||
path,
|
||||
method: 'POST',
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
Future<T?> put<T>(
|
||||
String path, {
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) {
|
||||
return request<T>(
|
||||
path,
|
||||
method: 'PUT',
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
}
|
||||
|
||||
Future<T?> delete<T>(
|
||||
String path, {
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
}) {
|
||||
return request<T>(
|
||||
path,
|
||||
method: 'DELETE',
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
Future<T?> request<T>(
|
||||
String path, {
|
||||
required String method,
|
||||
Object? data,
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Options? options,
|
||||
CancelToken? cancelToken,
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) async {
|
||||
final response = await dio.request<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: _withMethod(options, method),
|
||||
cancelToken: cancelToken,
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
Options _withMethod(Options? options, String method) {
|
||||
return options == null
|
||||
? Options(method: method)
|
||||
: options.copyWith(method: method);
|
||||
/// 更新配置
|
||||
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 2xx:body 即为业务数据
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:skywood/global/request/api_const.dart';
|
||||
import 'package:skywood/utils/tools/sky_device_info.dart';
|
||||
import 'package:skywood/utils/tools/sky_time_tool.dart';
|
||||
import 'package:skywood/utils/tools/app_sp.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import '../../utils/tools/token_manage.dart';
|
||||
import '../tool/sky_encrypt.dart';
|
||||
|
||||
/// 网络请求拦截器
|
||||
class NetworkInterceptor extends Interceptor {
|
||||
final bool enableLog = !kReleaseMode;
|
||||
final Logger _logger = Logger();
|
||||
|
||||
@override
|
||||
Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
|
||||
// 添加公共请求头
|
||||
await _addRequestHeaders(options);
|
||||
|
||||
// 打印请求日志
|
||||
_logInfo("🔶 REQUEST [${options.method.toUpperCase()}] ${options.uri}");
|
||||
_logDebug("Headers: ${options.headers}");
|
||||
if (options.data != null) {
|
||||
_logDebug("Payload: ${options.data}");
|
||||
}
|
||||
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
_logInfo("🟢 RESPONSE [${response.statusCode}] ${response.requestOptions.uri}");
|
||||
// _logDebug("Body: ${response.data}");
|
||||
|
||||
// 处理 401 / 402 Token 过期
|
||||
if (response.statusCode == 401 || response.statusCode == 402) {
|
||||
// 触发 Token 刷新
|
||||
TokenManager().update();
|
||||
if (response.statusCode == 402) {
|
||||
_handleError('errorCode:${response.statusCode}', true);
|
||||
}
|
||||
handler.reject(DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
type: DioExceptionType.badResponse,
|
||||
error: 'errorCode:${response.statusCode}',
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// Release 模式解密响应体
|
||||
if (kReleaseMode && response.data is String) {
|
||||
try {
|
||||
final deStr = SkyEncrypt.deStr(response.data);
|
||||
response.data = jsonDecode(deStr);
|
||||
} catch (e) {
|
||||
_logError("解密失败: $e");
|
||||
}
|
||||
}
|
||||
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
_logError("🔴 Error: ${err.message}");
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
// ============ 私有方法 ============
|
||||
|
||||
Future<void> _addRequestHeaders(RequestOptions options) async {
|
||||
// 开发模式添加 security: false
|
||||
if (!kReleaseMode) {
|
||||
options.headers['security'] = 'false';
|
||||
}
|
||||
|
||||
// 注入 Token
|
||||
final tokenManager = TokenManager();
|
||||
final token = tokenManager.get();
|
||||
// print('查看token: $token');
|
||||
if (token.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
// 其他公共参数
|
||||
String langKey = await AppSp().getString('GLIMZO_STORE_LANGKEY') ?? 'en';
|
||||
final deviceInfo = SkyDeviceInfo.instance;
|
||||
|
||||
options.headers.addAll({
|
||||
'product-prefix': 'glimzo',
|
||||
'device-id': deviceInfo.deviceId,
|
||||
'system-type': deviceInfo.systemType,
|
||||
'model': deviceInfo.model,
|
||||
'system-version': deviceInfo.systemVersion,
|
||||
'brand': deviceInfo.brand,
|
||||
'app-version': deviceInfo.appVersion,
|
||||
'app-name': deviceInfo.appName,
|
||||
'lang-key': langKey,
|
||||
'time-zone': SkyTimeTool.getTimeZone(),
|
||||
'idfa': '',
|
||||
'idfv': deviceInfo.idfv,
|
||||
'device-gaid': '',
|
||||
});
|
||||
|
||||
// 处理 prevToken(从请求体中提取)
|
||||
await _handleOldToken(options);
|
||||
}
|
||||
|
||||
Future<void> _handleOldToken(RequestOptions options) async {
|
||||
try {
|
||||
if (options.data is Map<String, dynamic>) {
|
||||
final body = options.data as Map<String, dynamic>;
|
||||
final prevToken = body['prevToken'];
|
||||
if (prevToken != null && prevToken.toString().isNotEmpty) {
|
||||
options.headers['Authorization'] = prevToken.toString();
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// 非 JSON 请求跳过
|
||||
}
|
||||
}
|
||||
|
||||
void _logInfo(String message) {
|
||||
if (enableLog) _logger.i(message);
|
||||
}
|
||||
|
||||
void _logDebug(String message) {
|
||||
if (enableLog) _logger.d('💡 $message');
|
||||
}
|
||||
|
||||
void _logError(String message) {
|
||||
if (enableLog) _logger.e(message);
|
||||
}
|
||||
|
||||
void _handleError(dynamic e, bool error) {
|
||||
_logError("🔴 Error: $e");
|
||||
// 这里可以根据需要处理错误提示
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user