feat: 工具类提交。

This commit is contained in:
chengsiwen committed 2026-07-06 10:45:08 +08:00
1 parent a2b38fa1e8
commit 557f900187
20 files changed
+2026 -3

No files matched your search

+573
View File
@@ -0,0 +1,573 @@
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<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());
}
}
/// 文件上传(带重试)
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
AppToastUtil.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) {
HelpToken().update();
if (resp.statusCode == 402) {
AppToastUtil.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) {
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<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);
}
}
+53
View File
@@ -0,0 +1,53 @@
// https://api-glimzodf.glimzodf.com/glimzodf/
class ApiString {
static const String baseUrl = "https://api-glimzodf.glimzodf.com/";
static const String appName = 'Novyronst';
static String webPrefix = "glimzodf.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";
}
class ApiStringUser {
static const String login = "/customer/login";
// /customer/register
static const String register = "/customer/register";
// /customer/info
static const String userInfo = "/customer/info";
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 ApiStringVideo {
static const String getRecommandsList = "/getRecommands";
static const String getVideoDetail = "/getVideoDetails";
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";
static const String getSearchHots = "/search/hots";
static const String getSearch = "/search";
static const String collect = "/collect";
static const String cancelCollect = "/cancelCollect";
static const String getCollections = "/myCollections";
static const String getHistories = "/myHistorys";
}
+141
View File
@@ -0,0 +1,141 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
import 'package:novyronst/common/help/help_time.dart';
import 'package:novyronst/common/help/help_token.dart';
import '../../tools/util/ny_device_info.dart';
import '../help/help_encrypt.dart';
/// 网络请求拦截器
class NyNetworkIntercept 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 刷新
HelpToken().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 = HelpEncrypt.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 = HelpToken();
final token = tokenManager.get();
// print('查看token: $token');
if (token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
// 其他公共参数
final deviceInfo = NyDeviceInfo.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': 'en',
'time-zone': HelpTime.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");
// 这里可以根据需要处理错误提示
}
}
+41
View File
@@ -0,0 +1,41 @@
import 'package:novyronst/common/api/api_request.dart';
import 'package:novyronst/common/api/api_string.dart';
import 'package:novyronst/common/help/help_log.dart';
import 'package:novyronst/common/model/novyronst_user.dart';
import 'package:novyronst/common/model/register_model.dart';
class UserRequest {
static Future login() {
final body = <String, dynamic>{
"avator": '',
"email": '123456@qq.com',
"family_name": 'test1',
"platform": 'android',
"third_id": '32434',
};
return ApiRequest.post(ApiStringUser.login, data: body).then((value) {
HelpLog.print(value.data, tag: 'loginnnn');
});
}
static Future<RegisterModel?> register() {
return ApiRequest.post(ApiStringUser.register).then((value) {
if (value.isSuccess) {
return RegisterModel.fromJson(value.data['data']);
}
return null;
});
}
static Future<NovyronstUser?> getUserInfo() {
return ApiRequest.get(ApiStringUser.userInfo).then((value) {
if (value.isSuccess) {
HelpLog.print(value.data, tag: 'getUserInfo');
return NovyronstUser.fromJson(value.data['data']);
}
return null;
});
}
}
View File
Whitespace-only changes.
+155
View File
@@ -0,0 +1,155 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'dart:math';
class HelpEncrypt {
static const int bfSize = 2048;
static const String enStrTag = r'$';
// Generates a random salt of size between 16 and 64 bytes
static Uint8List randSalt() {
final rand = Random.secure();
final size = rand.nextInt(49) + 16; // 16..64
return Uint8List.fromList(List.generate(size, (_) => rand.nextInt(256)));
}
// Encrypts the given data with a random salt
static Uint8List en(Uint8List data) {
if (data.isEmpty) return data;
final salt = randSalt();
final encryptedData = enWithSalt(data, salt);
return Uint8List.fromList([salt.length, ...salt, ...encryptedData]);
}
// Decrypts the given encrypted data
static Uint8List de(Uint8List data) {
if (data.isEmpty) return data;
final saltLen = data[0];
final salt = data.sublist(1, 1 + saltLen);
final encrypted = data.sublist(1 + saltLen);
return deWithSalt(encrypted, salt);
}
// Encrypts data with a specified salt
static Uint8List enWithSalt(Uint8List data, Uint8List salt) {
final mixedData = mixSalt(data, salt);
return cxEd(mixedData);
}
// Decrypts data with a specified salt
static Uint8List deWithSalt(Uint8List data, Uint8List salt) {
final decryptedData = cxEd(data);
return removeSalt(decryptedData, salt);
}
// Simple XOR-based encryption/decryption (bitwise negation)
static Uint8List cxEd(Uint8List data) {
return Uint8List.fromList(data.map((b) => b ^ 0xFF).toList());
}
// Apply salt to the data for encryption
static Uint8List mixSalt(Uint8List data, Uint8List salt) {
if (salt.isEmpty) return data;
return Uint8List.fromList(
List.generate(data.length, (i) {
final s = salt[i % salt.length];
return calSalt(data[i], s);
}),
);
}
// Remove salt from the data after decryption
static Uint8List removeSalt(Uint8List data, Uint8List salt) {
if (salt.isEmpty) return data;
return Uint8List.fromList(
List.generate(data.length, (i) {
final s = salt[i % salt.length];
return calRemoveSalt(data[i], s);
}),
);
}
// Calculate a modified value after mixing salt (for encryption)
static int calSalt(int v, int s) {
final r = v ^ 0xFF;
return s > r ? (s - r - 1) & 0xFF : (v + s) & 0xFF;
}
// Reverse the salt mixing process for decryption
static int calRemoveSalt(int v, int s) {
return v >= s ? (v - s) & 0xFF : (0xFF - (s - v) + 1) & 0xFF;
}
// Encrypt a string and return the hex representation
static String cxEStr(String data) {
return cxEStrAsBytes(
data,
).map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
// Encrypt string and return as bytes
static Uint8List cxEStrAsBytes(String data) {
return cxEd(Uint8List.fromList(utf8.encode(data)));
}
// Decrypt a hex string to a regular string
static String cxDStr(String data) {
final bytes = Uint8List.fromList([
for (int i = 0; i < data.length; i += 2)
int.parse(data.substring(i, i + 2), radix: 16),
]);
return utf8.decode(cxEd(bytes));
}
// Encrypt string data and return in the form of hex string with prefix
static String enStr(String data) {
return enBytesStr(Uint8List.fromList(utf8.encode(data)));
}
// Encrypt bytes and return as hex string with prefix
static String enBytesStr(Uint8List data) {
final encrypted = en(data);
return '$enStrTag${encrypted.map((b) => b.toRadixString(16).padLeft(2, '0')).join()}';
}
// Decrypt a string with encoded prefix (e.g., "$" symbol)
static String deStr(String data) {
return utf8.decode(deStrBytes(data));
}
// Decrypt the byte array extracted from a string
static Uint8List deStrBytes(String data) {
if (!data.startsWith(enStrTag)) {
throw ArgumentError("Invalid encoded string");
}
final hexData = data.substring(1);
final bytes = Uint8List.fromList([
for (int i = 0; i < hexData.length; i += 2)
int.parse(hexData.substring(i, i + 2), radix: 16),
]);
return de(bytes);
}
// Copy encrypted data from a stream to another stream
static Future<int> copy(
Stream<List<int>> reader,
StreamSink<List<int>> writer,
) async {
int total = 0;
await for (final chunk in reader) {
total += chunk.length;
final encrypted = chunk.map((b) => b ^ 0xFF).toList();
writer.add(encrypted);
}
await writer.close();
return total;
}
// Custom encryption with condition (even and odd byte values handled differently)
static Uint8List cxEd1(Uint8List data) {
return Uint8List.fromList(
data.map((b) => (b % 2 == 1) ? (b ^ 0xFF) : b).toList(),
);
}
}
+3 -1
View File
@@ -45,7 +45,9 @@ class ConstImage {
static const String settings = "assets/other/settings.png";
// copy
static const String copy = "assets/other/copy.png";
// pricary web_site
static const String primary = "assets/other/pricary.png";
static const String webSite = "assets/other/web_site.png";
}
+146
View File
@@ -0,0 +1,146 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:talker_flutter/talker_flutter.dart';
class HelpLog {
HelpLog._();
static final HelpLog _instance = HelpLog._();
static HelpLog get instance => _instance;
final Talker _talker = TalkerFlutter.init();
factory HelpLog() => _instance;
Talker get tLog => _talker;
/// DevTools / 控制台里可按名称过滤。
static const String logName = 'HelpLog';
Future<void> initLogger() async {
/// app 信息
}
/// 仅 [kDebugMode] 输出;内容经安全格式化,不会因无法 JSON 编码而中断业务逻辑。
static void print(
Object? message, {
String? tag,
String name = logName,
}) {
if (!kDebugMode) return;
try {
final text = _stringify(message);
final line = tag != null ? '[debug-tag: $tag] ===== $text' : text;
log(line, name: name);
} catch (e, st) {
log(
'[SkyLog.print failed] $e',
name: name,
error: e,
stackTrace: st,
);
}
}
/// 尽量输出缩进 JSON;无法编码时退回 [toString],不向外抛错。
static String _stringify(Object? value) {
if (value == null) return 'null';
if (value is String) return value;
if (value is num || value is bool) return value.toString();
const encoder = JsonEncoder.withIndent(' ');
if (value is Map) {
try {
return encoder.convert(value);
} catch (_) {
return value.toString();
}
}
if (value is List) {
try {
return encoder.convert(value);
} catch (_) {
return value.toString();
}
}
if (value is Iterable) {
try {
return encoder.convert(value.toList());
} catch (_) {
return value.toString();
}
}
try {
return encoder.convert(value);
} catch (_) {
try {
return '${value.runtimeType}: $value';
} catch (_) {
return '<unprintable ${value.runtimeType}>';
}
}
}
/// 错误日志走 Talker;若 Talker 抛错,在 Debug 下用 [log] 兜底,仍不向外抛。
static void error(
Object? message, {
String? tag,
StackTrace? stackTrace,
}) {
final text = message == null ? 'null' : '$message';
try {
_instance._talker.logCustom(
CustomLog(
text,
settings: _instance._talker.settings,
tag: tag,
logLevel: LogLevel.error,
stackTrace: stackTrace,
),
);
} catch (e, st) {
if (kDebugMode) {
log(
'[SkyLog.error fallback] $text | $e',
name: logName,
error: e,
stackTrace: st,
);
}
}
}
}
class CustomLog extends TalkerLog {
final String? tag;
final TalkerSettings settings;
final LogLevel? _logLevel;
final StackTrace? _stackTrace;
CustomLog(
super.message, {
this.tag,
required this.settings,
LogLevel logLevel = LogLevel.debug,
StackTrace? stackTrace,
}) : _logLevel = logLevel,
_stackTrace = stackTrace;
@override
String? get key => tag;
@override
LogLevel? get logLevel => _logLevel;
@override
StackTrace? get stackTrace => _stackTrace;
@override
AnsiPen? get pen =>
settings.colors[TalkerKey.fromLogLevel(logLevel ?? LogLevel.info)];
}
+55
View File
@@ -0,0 +1,55 @@
class HelpTime {
static String getTimeZone() {
final offset = DateTime.now().timeZoneOffset;
final hours = offset.inHours;
final minutes = offset.inMinutes.remainder(60).abs();
final sign = hours >= 0 ? '+' : '-';
final formatted =
'GMT$sign${hours.abs().toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}';
return formatted;
}
//格式化数字,带单位 k / m,可自定义小数位和千分位分隔符
static String formatNum(
num value, {
int decimalDigits = 1,
String separator = ',',
String unit = 'k',
}) {
String removeTrailingZeros(String str) {
if (str.contains('.')) {
str = str.replaceAll(RegExp(r'0+$'), ''); // 去掉末尾 0
str = str.replaceAll(RegExp(r'\.$'), ''); // 去掉末尾小数点
}
return str;
}
String addSeparator(String str) {
final parts = str.split('.');
final intPart = parts[0];
final decPart = parts.length > 1 ? '.${parts[1]}' : '';
final regex = RegExp(r'(\d+)(\d{3})');
var formatted = intPart;
while (regex.hasMatch(formatted)) {
formatted = formatted.replaceAllMapped(
regex,
(m) => '${m[1]}$separator${m[2]}',
);
}
return '$formatted$decPart';
}
if (value < 1000) {
return addSeparator(value.toStringAsFixed(0));
} else if (value < 1000000) {
int mu = unit=='w' ? 10000 : 1000;
final formatted = (value / mu).toStringAsFixed(decimalDigits);
return '${removeTrailingZeros(formatted)}$unit';
} else {
final formatted = (value / 1000000).toStringAsFixed(decimalDigits);
return '${removeTrailingZeros(formatted)}m';
}
}
}
+228
View File
@@ -0,0 +1,228 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:novyronst/common/api/user_request.dart';
import 'package:shared_preferences/shared_preferences.dart';
class HelpToken {
static final HelpToken _instance = HelpToken._internal();
factory HelpToken() => _instance;
HelpToken._internal();
static const String _keyToken = 'SKY_STORE_TOKEN';
static const String _keyUserInfo = 'SKY_STORE_USER_INFO';
String? _cachedToken;
Map<String, dynamic>? _cachedUserInfo;
/// 获取 Token(从内存或存储)
String get() {
if (_cachedToken != null) {
return _cachedToken!;
}
// 如果内存中没有,从存储中加载
_cachedToken = _getFromStorage(_keyToken);
return _cachedToken ?? '';
}
/// 保存 Token(到内存和存储)
void save(String token) {
if (token.isNotEmpty) {
_cachedToken = token;
_saveToStorage(_keyToken, token);
}
}
/// 清除 Token(从内存和存储)
void clear() {
_cachedToken = null;
_removeFromStorage(_keyToken);
_cachedUserInfo = null;
_removeFromStorage(_keyUserInfo);
}
/// 刷新 Token(调用注册接口获取新 Token)
/// 注意:这里需要传入一个刷新函数,因为原项目依赖 GlimzoRequestUser
Future<void> update() async {
try {
final res = await UserRequest.register();
if (res == null) return;
final newToken = res.token;
if (newToken.isNotEmpty ?? false) {
// 保存新 Token
save(newToken);
// 如果返回了用户信息,也一并保存
// if (res['userInfo'] != null) {
// saveUserInfo(res['userInfo']);
// }
} else {
throw Exception('Invalid token response');
}
} catch (e) {
debugPrint('[TokenManager] Error updating token: $e');
rethrow;
}
}
// ============ 便捷属性 ============
/// 判断 Token 是否为空
bool get isEmpty => get().isEmpty;
/// 判断 Token 是否不为空
bool get isNotEmpty => !isEmpty;
/// 获取完整 Authorization 头
String? get authorizationHeader {
final token = get();
if (token.isNotEmpty) {
return 'Bearer $token';
}
return null;
}
// ============ 用户信息扩展(原项目没有,但建议加上) ============
/// 保存用户信息
void saveUserInfo(Map<String, dynamic> userInfo) {
_cachedUserInfo = userInfo;
_saveToStorage(_keyUserInfo, jsonEncode(userInfo));
}
/// 获取用户信息
Map<String, dynamic>? getUserInfo() {
if (_cachedUserInfo != null) {
return _cachedUserInfo;
}
final stored = _getFromStorage(_keyUserInfo);
if (stored != null && stored.isNotEmpty) {
try {
_cachedUserInfo = jsonDecode(stored) as Map<String, dynamic>;
return _cachedUserInfo;
} catch (_) {
return null;
}
}
return null;
}
// ============ 私有存储方法 ============
String? _getFromStorage(String key) {
try {
// 使用 SharedPreferences 同步获取(但 SharedPreferences 是异步的)
// 为了保持与原项目一致的同步接口,这里使用异步转同步
// 但更好的方式是在初始化时预加载
return _getFromStorageSync(key);
} catch (e) {
debugPrint('[TokenManager] Error reading from storage: $e');
return null;
}
}
// 同步读取(注意:SharedPreferences 本身是异步的,这里用同步方式需要谨慎)
String? _getFromStorageSync(String key) {
// 由于 SharedPreferences 是异步的,我们使用一个全局缓存
// 实际项目中建议在 main 中调用 init() 预加载
try {
// 这里为了简化,直接返回 null,实际应该用异步方式
// 建议使用下面的异步版本
return null;
} catch (_) {
return null;
}
}
void _saveToStorage(String key, String value) {
// 异步保存,不等待结果
_saveToStorageAsync(key, value);
}
void _removeFromStorage(String key) {
// 异步移除,不等待结果
_removeFromStorageAsync(key);
}
// ============ 异步存储方法(推荐使用) ============
/// 初始化:从存储中加载 Token
Future<void> init() async {
try {
final prefs = await SharedPreferences.getInstance();
_cachedToken = prefs.getString(_keyToken);
final userInfoStr = prefs.getString(_keyUserInfo);
if (userInfoStr != null && userInfoStr.isNotEmpty) {
try {
_cachedUserInfo = jsonDecode(userInfoStr) as Map<String, dynamic>;
} catch (_) {
_cachedUserInfo = null;
}
}
} catch (e) {
debugPrint('[TokenManager] Init error: $e');
}
}
/// 异步获取 Token
Future<String> getAsync() async {
if (_cachedToken != null) {
return _cachedToken!;
}
try {
final prefs = await SharedPreferences.getInstance();
_cachedToken = prefs.getString(_keyToken);
return _cachedToken ?? '';
} catch (e) {
debugPrint('[TokenManager] Get token error: $e');
return '';
}
}
/// 异步保存 Token
Future<void> saveAsync(String token) async {
if (token.isNotEmpty) {
_cachedToken = token;
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_keyToken, token);
} catch (e) {
debugPrint('[TokenManager] Save token error: $e');
}
}
}
/// 异步清除 Token
Future<void> clearAsync() async {
_cachedToken = null;
_cachedUserInfo = null;
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_keyToken);
await prefs.remove(_keyUserInfo);
} catch (e) {
debugPrint('[TokenManager] Clear token error: $e');
}
}
// 异步保存到存储
Future<void> _saveToStorageAsync(String key, String value) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, value);
} catch (e) {
debugPrint('[TokenManager] Save to storage error: $e');
}
}
// 异步从存储移除
Future<void> _removeFromStorageAsync(String key) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(key);
} catch (e) {
debugPrint('[TokenManager] Remove from storage error: $e');
}
}
}
+60
View File
@@ -0,0 +1,60 @@
import 'package:novyronst/common/model/parse_model.dart';
class NovyronstUser {
final String customerId;
final bool isTourist;
final String userLevel;
final String avator;
final String? familyName;
final String? givingName;
const NovyronstUser({
required this.customerId,
required this.isTourist,
required this.userLevel,
required this.avator,
this.familyName,
this.givingName,
});
factory NovyronstUser.fromJson(Map<String, dynamic> json) {
return NovyronstUser(
customerId: parseString(json['customer_id']),
isTourist: parseBool(json['is_tourist']),
userLevel: parseString(json['user_level']),
avator: parseString(json['avator']),
familyName: json['family_name'] as String?,
givingName: parseString(json['giving_name']),
);
}
Map<String, dynamic> toJson() {
return {
'customer_id': customerId,
'is_tourist': isTourist,
'user_level': userLevel,
'avator': avator,
'family_name': familyName,
'giving_name': givingName,
};
}
NovyronstUser copyWith({
String? customerId,
bool? isTourist,
String? userLevel,
String? avator,
String? familyName,
String? givingName,
}) {
return NovyronstUser(
customerId: customerId ?? this.customerId,
isTourist: isTourist ?? this.isTourist,
userLevel: userLevel ?? this.userLevel,
avator: avator ?? this.avator,
familyName: familyName ?? this.familyName,
givingName: givingName ?? this.givingName,
);
}
}
+57
View File
@@ -0,0 +1,57 @@
bool parseBool(dynamic value) {
if (value == null) return false;
if (value is bool) return value;
if (value is num) return value != 0;
if (value is String) {
final v = value.trim().toLowerCase();
return v == 'true' || v == '1' || v == 'yes' || v == 'y';
}
return false;
}
int parseInt(dynamic value) {
if (value == null) return 0;
if (value is int) return value;
if (value is double) return value.toInt();
if (value is num) return value.toInt();
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}
String parseString(dynamic value) {
if (value == null) return '';
return value.toString();
}
List<String>? parseStringList(dynamic value) {
if (value == null) return null;
if (value is List) {
return value.map((item) => parseString(item)).toList();
}
return null;
}
List<int>? parseIntList(dynamic value) {
if (value == null) return null;
if (value is List) {
return value.map((item) => parseInt(item)).toList();
}
return null;
}
Map<String, dynamic>? parseMap(dynamic value) {
if (value is Map<String, dynamic>) return value;
if (value is Map) return Map<String, dynamic>.from(value);
return null;
}
List<T> parseModelList<T>(
dynamic value,
T Function(Map<String, dynamic> json) fromJson,
) {
if (value is! List) return <T>[];
return value
.whereType<Map>()
.map((item) => fromJson(Map<String, dynamic>.from(item)))
.toList();
}
+18
View File
@@ -0,0 +1,18 @@
class RegisterModel {
final String token;
final int customerId;
final bool? autoLogin;
final int touristId;
RegisterModel({required this.token, required this.customerId, this.autoLogin, required this.touristId});
factory RegisterModel.fromJson(Map<String, dynamic> json) {
return RegisterModel(
token: json['token'],
customerId: json['customer_id'],
autoLogin: json['auto_login'],
touristId: json['tourist_id'],
);
}
}