Merge remote-tracking branch 'origin/dev' into cody/netwrok_SDK

# Conflicts:
#	apps/im_app/lib/features/chat/presentation/chat_db_test_view_model.dart
#	apps/im_app/lib/features/login/presentation/login_view_model.dart

修复逻辑漏洞,性能优化
This commit is contained in:
Cody
2026-03-08 20:47:28 +08:00
88 changed files with 5695 additions and 593 deletions

View File

@@ -9,7 +9,14 @@ import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
/// REST API 客户端
/// 基于 Dio提供请求执行入口
///
/// 拦截器链顺序Auth → Encryption → 自定义 → Retry → Logging
/// 拦截器链顺序onRequestAuth → 自定义 → Retry → Logging → Encryption
///
/// Dio 的 onResponse / onError 按 **逆序** 执行,因此实际响应处理为:
/// `Encryption(解密) → Logging → Retry(业务码判断) → 自定义 → Auth`
///
/// EncryptionInterceptor 放最后,保证:
/// - onRequest 最后加密(其他拦截器操作明文)
/// - onResponse 最先解密(其他拦截器看到明文,业务码判断正常工作)
///
/// 使用方式:
/// ```dart
@@ -31,13 +38,15 @@ class ApiClient {
receiveTimeout: const Duration(seconds: 60),
);
// 挂载拦截器顺序Auth → Encryption → 自定义 → Retry → Logging
// 挂载拦截器
// onRequest 顺序Auth → 自定义 → Retry → Logging → Encryption
// onResponse 逆序Encryption(解密) → Logging → Retry(业务码) → 自定义 → Auth
_dio.interceptors.addAll([
AuthInterceptor(config),
EncryptionInterceptor(config),
if (additionalInterceptors != null) ...additionalInterceptors,
RetryInterceptor(config: config, dio: _dio),
LoggingInterceptor(onLog: config.onLog),
EncryptionInterceptor(config),
]);
}

View File

@@ -3,8 +3,12 @@ import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
/// 加密拦截器(预留给 cipher_guard_sdk
///
/// 在拦截器链中位于 Auth 之后、Retry 之前
/// `Auth → Encryption → Custom → Retry → Logging`
/// 在拦截器链中位于最末位
/// onRequest 顺序:`Auth → Custom → Retry → Logging → Encryption`
/// onResponse 逆序:`Encryption(解密) → Logging → Retry(业务码) → Custom → Auth`
///
/// 放最后是因为 Dio onResponse 按逆序执行——加密拦截器最先解密,
/// 后续的 RetryInterceptor 才能正确判断业务错误码、Token 过期等。
///
/// 回调为 null 时自动跳过,不影响正常请求流程。
/// 后续 cipher_guard_sdk 接入后App 层在 ApiConfig 中注入
@@ -20,7 +24,23 @@ import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
///
/// 加密回调接收原始 path、headers、body返回 [EncryptedRequest]
/// 拦截器根据非 null 字段覆盖请求。
///
/// ## Token 重试与重新加密
///
/// 瞬态错误重试5xx / 超时):复用已加密的请求,不重复加密。
/// Token 刷新重试:加密 headers 可能包含过期 token 衍生值
/// (如 X-Token、X-Signature需要恢复加密前状态并用新 token 重新加密。
/// RetryInterceptor 通过 `_needsReEncryption` 标记通知本拦截器重新加密。
class EncryptionInterceptor extends Interceptor {
/// extra 标记键:请求已加密,瞬态重试时跳过
static const _encryptedKey = '__encrypted__';
/// extra 标记键加密前的请求快照path / body / contentType
static const _preEncryptSnapshotKey = '__preEncryptSnapshot__';
/// extra 标记键:加密回调注入的 header key 列表
static const _encryptionAddedHeadersKey = '__encryptionAddedHeaders__';
final ApiConfig _config;
EncryptionInterceptor(this._config);
@@ -36,7 +56,28 @@ class EncryptionInterceptor extends Interceptor {
return;
}
// Token 重试 + 已加密 → 恢复加密前状态,用新 token 上下文重新加密
// 旧的加密 headers如 X-Token、X-Signature可能包含过期 token 信息
if (options.extra[_encryptedKey] == true &&
options.extra['_needsReEncryption'] == true) {
_restorePreEncryptState(options);
options.extra.remove('_needsReEncryption');
}
// 已加密(瞬态错误重试)→ 复用加密请求,不重复加密
if (options.extra[_encryptedKey] == true) {
handler.next(options);
return;
}
try {
// 保存加密前快照Token 重试时恢复
options.extra[_preEncryptSnapshotKey] = {
'path': options.path,
'data': options.data,
'contentType': options.contentType,
};
// 收集当前 headers转为 Map<String, String>
final currentHeaders = <String, String>{};
options.headers.forEach((key, value) {
@@ -54,11 +95,17 @@ class EncryptionInterceptor extends Interceptor {
}
if (result.headers != null) {
options.headers.addAll(result.headers!);
// 记录加密注入的 header keyToken 重试时移除
options.extra[_encryptionAddedHeadersKey] = result.headers!.keys
.toList();
}
if (result.contentType != null) {
options.contentType = result.contentType;
}
// 标记已加密,防止瞬态重试时重复加密
options.extra[_encryptedKey] = true;
_config.onLog?.call(
'Request encrypted: ${options.path}',
tag: 'Encryption',
@@ -76,6 +123,41 @@ class EncryptionInterceptor extends Interceptor {
}
}
/// 恢复加密前的请求状态
///
/// Token 重试场景旧的加密数据path / body / headers可能包含过期 token
/// 需要恢复原始状态后用新 token 上下文重新加密。
///
/// AuthInterceptor 已在本轮重试中注入了新 token headers
/// 这里只需移除上次加密注入的 headers如 X-Token、X-Signature
/// 保留 Auth 设置的新 token。
void _restorePreEncryptState(RequestOptions options) {
final snapshot =
options.extra[_preEncryptSnapshotKey] as Map<String, dynamic>?;
if (snapshot != null) {
options.path = snapshot['path'] as String;
options.data = snapshot['data'];
options.contentType = snapshot['contentType'] as String?;
}
// 移除上次加密注入的 headers
final addedHeaders = options.extra[_encryptionAddedHeadersKey] as List?;
if (addedHeaders != null) {
for (final key in addedHeaders) {
options.headers.remove(key);
}
}
// 清除加密标记和快照
options.extra.remove(_encryptedKey);
options.extra.remove(_encryptionAddedHeadersKey);
_config.onLog?.call(
'Pre-encrypt state restored for token retry',
tag: 'Encryption',
);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) async {
final decrypt = _config.onDecryptResponse;
@@ -90,6 +172,14 @@ class EncryptionInterceptor extends Interceptor {
return;
}
// 响应已是 Map → 未加密health check、非加密端点等跳过解密
// 加密模式下响应通常是 Stringbase64或 List<int>bytes
// TODO: 接入加密后,若服务端所有端点都加密,可移除此判断
if (response.data is Map<String, dynamic>) {
handler.next(response);
return;
}
try {
final decrypted = await decrypt(response.data as Object);
response.data = decrypted;

View File

@@ -78,7 +78,8 @@ class RetryInterceptor extends Interceptor {
if (code != 0 && config.onBusinessError != null) {
final handled = config.onBusinessError!(code, message, requestPath);
if (handled) {
// App 层已处理,正常传递响应
// App 层已处理 → 标记,让 decodeResponse 跳过二次抛错
response.requestOptions.extra['_businessErrorHandled'] = true;
handler.next(response);
return;
}
@@ -115,6 +116,9 @@ class RetryInterceptor extends Interceptor {
options.headers['token'] = newToken;
// 标记为 token 重试请求,防止重试后再次进入 _handleTokenExpired 造成递归
options.extra['_isTokenRetry'] = true;
// 通知 EncryptionInterceptortoken 变了,需要用新 token 上下文重新加密
// 旧的加密 headers如 X-Token、X-Signature可能包含过期 token 信息
options.extra['_needsReEncryption'] = true;
final retryResponse = await dio.fetch(options);
handler.resolve(retryResponse);

View File

@@ -194,6 +194,9 @@ class NetworksSdkMethodChannelDataSource {
///
/// Dio.download 内部用 FileMode.write从头覆盖无法正确续传。
/// 这里手动读流并追加写入文件。
///
/// 如果服务端不支持 Range 请求(返回 200 而非 206
/// 自动回退为覆盖写入,防止文件损坏。
Future<void> _downloadWithResume({
required String url,
required String savePath,
@@ -215,14 +218,33 @@ class NetworksSdkMethodChannelDataSource {
final stream = response.data?.stream;
if (stream == null) return;
// Content-Length 是本次传输量(不含已下载部分)
// 检查服务端是否接受了 Range 请求
// 206 = 支持续传,追加写入
// 200 = 不支持 Range返回完整文件需要覆盖写入
final isPartialContent = response.statusCode == 206;
final effectiveStartBytes = isPartialContent ? startBytes : 0;
if (!isPartialContent) {
apiClient.config.onLog?.call(
'Server does not support Range, falling back to full download',
tag: 'Download',
);
}
// Content-Length 是本次传输量
final contentLength =
int.tryParse(response.headers.value('content-length') ?? '') ?? -1;
final totalBytes = contentLength > 0 ? contentLength + startBytes : -1;
final totalBytes = contentLength > 0
? contentLength + effectiveStartBytes
: -1;
final file = File(savePath);
final raf = file.openSync(mode: FileMode.writeOnlyAppend);
int received = startBytes;
// 不支持续传时用 write 覆盖,支持时用 append 追加
final fileMode = isPartialContent
? FileMode.writeOnlyAppend
: FileMode.writeOnly;
final raf = file.openSync(mode: fileMode);
int received = effectiveStartBytes;
try {
await for (final chunk in stream) {

View File

@@ -16,7 +16,7 @@ import 'package:web_socket_channel/web_socket_channel.dart';
/// - Stream 输出JSON 消息、原始字符串、二进制、连接状态、错误)
/// - 生命周期感知(前后台切换)
/// - Token 热更新(不断连)
/// - 消息加密/解密钩子(预留给 cipher_guard_sdk
/// - 消息加密/解密钩子(预留给 cipher_guard_sdkping/pong 走明文不加密
///
/// ## 使用方式
///
@@ -60,6 +60,15 @@ class SocketClient {
Timer? _reconnectTimer;
final _random = Random();
// ── 消息处理 ──
/// 异步消息处理链,保证解密场景下消息按到达顺序处理
///
/// 无解密回调时不使用(同步处理,天然有序)。
/// 有解密回调时,每条消息的处理链在前一条之后执行,
/// 即使解密耗时不同也不会乱序。
Future<void>? _messageProcessingChain;
// ── Stream Controllers ──
final _messageController = StreamController<Map<String, dynamic>>.broadcast();
final _rawMessageController = StreamController<String>.broadcast();
@@ -324,45 +333,73 @@ class SocketClient {
// 内部 — 消息处理
// ══════════════════════════════════════════════════════════════════════════
void _handleMessage(dynamic data) async {
// 二进制消息
void _handleMessage(dynamic data) {
// 二进制消息不需要解密,直接分发
if (data is List<int>) {
_binaryMessageController.add(
data is Uint8List ? data : Uint8List.fromList(data),
);
if (!_binaryMessageController.isClosed) {
_binaryMessageController.add(
data is Uint8List ? data : Uint8List.fromList(data),
);
}
return;
}
if (data is! String) {
_rawMessageController.add(data.toString());
if (!_rawMessageController.isClosed) {
_rawMessageController.add(data.toString());
}
return;
}
// 解密(如果配置了解密回调)
String text = data;
if (config.onDecryptMessage != null) {
try {
text = await config.onDecryptMessage!(data);
} catch (e) {
_log('Message decryption failed: $e');
_rawMessageController.add(data);
return;
}
}
// 检查 pong 心跳回复(解密后检查,加密场景下也能正确匹配)
if (text == 'pong') {
// pong 是传输层心跳,不经过业务加解密,直接匹配
if (data == 'pong') {
_onPongReceived();
return;
}
// 尝试 JSON 解析
if (config.onDecryptMessage != null) {
// 有解密回调 → 链式异步处理,保证消息按到达顺序分发
// 避免解密耗时不同导致后到的消息先完成解密、先分发
final previous = _messageProcessingChain ?? Future.value();
_messageProcessingChain = previous.then((_) => _processTextMessage(data));
} else {
// 无解密回调 → 同步处理,天然有序
_dispatchTextMessage(data);
}
}
/// 异步处理文本消息(解密 → 分发)
Future<void> _processTextMessage(String data) async {
// dispose 期间可能有残留的链式任务,直接跳过
if (_messageController.isClosed) return;
String text;
try {
text = await config.onDecryptMessage!(data);
} catch (e) {
_log('Message decryption failed: $e');
if (!_rawMessageController.isClosed) {
_rawMessageController.add(data);
}
return;
}
_dispatchTextMessage(text);
}
/// 分发文本消息JSON 解析 → 投递到对应 stream
///
/// pong 已在 `_handleMessage` 中提前拦截,不会到这里。
void _dispatchTextMessage(String text) {
try {
final json = jsonDecode(text) as Map<String, dynamic>;
_messageController.add(json);
if (!_messageController.isClosed) {
_messageController.add(json);
}
} catch (_) {
// JSON 解析失败,走原始消息流
_rawMessageController.add(text);
if (!_rawMessageController.isClosed) {
_rawMessageController.add(text);
}
}
}
@@ -401,26 +438,20 @@ class SocketClient {
_waitingForPong = false;
}
void _sendPing() async {
void _sendPing() {
if (_waitingForPong) return;
_waitingForPong = true;
// 加密场景下 ping 也要加密,与 pong 解密对称
String pingPayload = 'ping';
if (config.onEncryptMessage != null) {
try {
pingPayload = await config.onEncryptMessage!('ping');
} catch (e) {
_log('Ping encryption failed: $e');
}
}
_channel?.sink.add(pingPayload);
// ping/pong 是传输层心跳,不经过业务加解密
// 保证即使加密密钥过期/轮换失败,心跳仍然正常工作
_channel?.sink.add('ping');
_log('♥ ping');
// 启动 pong 超时计时器
_pongTimeoutTimer = Timer(config.pongTimeout, () {
if (_waitingForPong) {
_log('Pong timeout, reconnecting...');
_log('♥ pong timeout, reconnecting...');
_waitingForPong = false;
_emitError(const SocketError.pingTimeout());
_doDisconnect(reason: 'Pong timeout');
@@ -430,6 +461,7 @@ class SocketClient {
}
void _onPongReceived() {
_log('♥ pong');
_waitingForPong = false;
_pongTimeoutTimer?.cancel();
_pongTimeoutTimer = null;
@@ -488,7 +520,9 @@ class SocketClient {
try {
await config.onBeforeReconnect!();
} catch (e) {
_log('onBeforeReconnect failed: $e, skip this reconnect');
_log('onBeforeReconnect failed: $e, schedule next reconnect');
// 重置状态以允许下次 _startReconnect 进入(防止卡死在 reconnecting
_updateConnectionState(SocketConnectionState.disconnected);
_startReconnect();
return;
}

View File

@@ -4,35 +4,36 @@ import 'package:networks_sdk/src/domain/entities/api_error.dart';
import 'package:networks_sdk/src/domain/entities/api_request_type.dart';
import 'package:networks_sdk/src/domain/entities/http_method.dart';
/// API 请求基类
///
/// 使用侧只需:字段 + path + method其余全部有默认实现。
/// 只需 `@ApiRequest` 一个注解,声明字段和构造函数即可:
/// - `toJson()` 由 mixin 自动生成(只序列化类自身字段,不含继承属性)
/// - `path / method / requestType / includeToken` 由 mixin 自动提供
/// - Response 的 fromJson 在 mixin 的 `parameters` getter 中自动注册
/// - **不需要** `@JsonSerializable`**不需要** 手写 `fromJson`
///
/// ```dart
/// @JsonSerializable()
/// class LoginRequest extends ApiRequestable<LoginData> {
/// @ApiRequest(
/// path: ApiPaths.authLogin,
/// method: HttpMethod.post,
/// responseType: LoginData,
/// requestType: ApiRequestType.login,
/// )
/// class LoginRequest extends ApiRequestable<LoginData>
/// with _$LoginRequestApi {
/// final String email;
/// final String password;
///
/// LoginRequest({required this.email, required this.password});
///
/// factory LoginRequest.fromJson(Map<String, dynamic> json) =>
/// _$LoginRequestFromJson(json);
/// @override
/// Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
///
/// @override
/// String get path => '/auth/login';
/// @override
/// HttpMethod get method => HttpMethod.post;
/// @override
/// bool get includeToken => false;
/// // 完毕!一行样板代码都不用写
/// }
///
/// // 文件顶层注册一次(一行)
/// final _reg = registerResponse<LoginData>(LoginData.fromJson);
/// ```
///
/// 字段名映射:在字段上加 `@JsonKey(name: 'server_name')` 即可,
/// 生成器会读取并使用该名称作为 JSON 键。
///
/// 特殊请求(如 upload在类中 override `toJson()` 即可,
/// 类的 override 优先于 mixin。
abstract class ApiRequestable<T> {
/// API 路径(如 '/auth/login'
String get path;
@@ -40,8 +41,8 @@ abstract class ApiRequestable<T> {
/// HTTP 方法
HttpMethod get method;
/// 序列化为 JSON由 @JsonSerializable 自动生成)
/// 子类 override 返回 `_$XxxToJson(this)` 即可
/// 序列化为 JSON由 @ApiRequest 生成器在 mixin 中自动生成)
/// Upload 等特殊请求可在类中 override 返回空 map
Map<String, dynamic> toJson();
/// 请求参数 — 默认调用 toJson()upload 类型返回 null
@@ -95,7 +96,7 @@ abstract class ApiRequestable<T> {
if (fromJsonFunc == null) {
throw StateError(
'fromJson not registered for type $T. '
'Add: final _reg = registerResponse<$T>($T.fromJson);',
'Add: final _reg = registerResponse<$T>($T.fromJson);',
);
}
@@ -106,13 +107,17 @@ abstract class ApiRequestable<T> {
final mapFunc = fromJsonFunc as T Function(Map<String, dynamic>);
return mapFunc(json);
}
throw FormatException('Expected Map<String, dynamic>, got ${json.runtimeType}',);
throw FormatException(
'Expected Map<String, dynamic>, got ${json.runtimeType}',
);
}
final wrapper = ApiResponseWrapper<T>.fromJson(data, fromJsonObject);
// 业务错误码检查
if (wrapper.code != 0) {
// 业务错误码检查RetryInterceptor 已处理的跳过,防止双重抛错)
final handledByInterceptor =
response.requestOptions.extra['_businessErrorHandled'] == true;
if (wrapper.code != 0 && !handledByInterceptor) {
throw ApiError.apiError(
code: wrapper.code,
message: wrapper.message ?? 'API error (code: ${wrapper.code})',
@@ -141,8 +146,9 @@ final fromJsonRegistry = <Type, Function>{};
/// ```dart
/// final _reg = registerResponse<LoginData>(LoginData.fromJson);
/// ```
T Function(Map<String, dynamic>)? registerResponse<T>(T Function(Map<String, dynamic>) fromJson,)
{
T Function(Map<String, dynamic>)? registerResponse<T>(
T Function(Map<String, dynamic>) fromJson,
) {
fromJsonRegistry[T] = fromJson;
return fromJson;
}
@@ -152,9 +158,7 @@ T Function(Map<String, dynamic>)? registerResponse<T>(T Function(Map<String, dyn
/// ```dart
/// final _reg = registerResponseObject<DeviceList>(DeviceList.fromJson);
/// ```
T Function(Object?)? registerResponseObject<T>(
T Function(Object?) fromJson,
) {
T Function(Object?)? registerResponseObject<T>(T Function(Object?) fromJson) {
fromJsonRegistry[T] = fromJson;
return fromJson;
}

View File

@@ -1,20 +1,16 @@
/// API 响应信封解析器
/// API 响应包装解析器
/// 统一处理 { code, message/msg, data } 格式的服务器响应
class ApiResponseWrapper<T> {
final int code;
final String? message;
final T? data;
const ApiResponseWrapper({
required this.code,
this.message,
this.data,
});
const ApiResponseWrapper({required this.code, this.message, this.data});
factory ApiResponseWrapper.fromJson(
Map<String, dynamic> json,
T Function(Object?) fromJsonT,
) {
Map<String, dynamic> json,
T Function(Object?) fromJsonT,
) {
// code 字段:兼容 int 和 String
final int codeValue;
if (json['code'] is int) {
@@ -28,8 +24,7 @@ class ApiResponseWrapper<T> {
}
// message 字段:兼容 message 和 msg
final message =
json['message'] as String? ?? json['msg'] as String?;
final message = json['message'] as String? ?? json['msg'] as String?;
// 解码 datanull-safelogout / delete 等接口可能无 data
final rawData = json['data'];

View File

@@ -4,17 +4,50 @@ import 'package:networks_sdk/src/annotations/api_request.dart';
import 'package:source_gen/source_gen.dart';
import 'package:build/build.dart';
/// @JsonKey 检测器(用于读取字段的 JSON 键名映射)
const _jsonKeyChecker = TypeChecker.fromUrl(
'package:json_annotation/src/json_key.dart#JsonKey',
);
/// @ApiRequest 代码生成器
///
/// 为标注了 `@ApiRequest` 的类自动生成 mixin提供
/// - `path`, `method`, `requestType`, `includeToken` 协议实现
/// - 自动注册响应类型的 `fromJson`(在 `parameters` getter 中触发
/// 保证首次请求前完成注册,无需手动调用 `registerApiResponses()`
/// - `toJson()` — 从类的声明字段自动生成,只序列化自身字段
/// 不含 ApiRequestable 的继承属性,避免递归
/// - 自动注册响应类型的 `fromJson`(在 `parameters` getter 中触发)
///
/// 生成的 mixin 命名规则:`_$<ClassName>Api`
/// 支持 `@JsonKey(name: '...')` 字段重命名。
/// 如有 `@JsonKey(includeToJson: false)` 则跳过该字段。
///
/// ## 使用模式
///
/// Request 类只需 `@ApiRequest` 注解,无需 `@JsonSerializable`
///
/// ```dart
/// @ApiRequest(
/// path: ApiPaths.authLogin,
/// method: HttpMethod.post,
/// responseType: LoginData,
/// requestType: ApiRequestType.login,
/// )
/// class LoginRequest extends ApiRequestable<LoginData>
/// with _$LoginRequestApi {
/// final String email;
/// final String password;
///
/// LoginRequest({required this.email, required this.password});
/// // 完毕toJson / path / method 全部由 mixin 自动生成
/// // Response 的 fromJson 在 parameters getter 中自动注册
/// }
/// ```
///
/// ## mixin 命名规则
///
/// `_$<ClassName>Api`
///
/// ## 生成示例
///
/// 示例输出:
/// ```dart
/// mixin _$LoginRequestApi on ApiRequestable<LoginData> {
/// @override String get path => '/auth/login';
@@ -22,17 +55,29 @@ import 'package:build/build.dart';
/// @override ApiRequestType get requestType => ApiRequestType.login;
/// @override bool get includeToken => false;
/// @override
/// Map<String, dynamic> toJson() => <String, dynamic>{
/// 'email': (this as LoginRequest).email,
/// 'password': (this as LoginRequest).password,
/// };
/// @override
/// Map<String, dynamic>? get parameters {
/// registerResponse<LoginData>(LoginData.fromJson);
/// return super.parameters;
/// }
/// }
/// ```
class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest>
{
///
/// ## Upload 等特殊请求
///
/// 如需自定义 toJson如 upload 返回空 map在类中 override 即可,
/// 类的 override 优先于 mixin。
class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest> {
@override
String generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep,)
{
String generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) {
if (element is! ClassElement) {
throw InvalidGenerationSourceError(
'@ApiRequest can only be applied to classes.',
@@ -40,7 +85,7 @@ class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest>
);
}
final className = element.name;
final className = element.name!;
final path = annotation.read('path').stringValue;
// 读取 HttpMethod 枚举值
@@ -68,6 +113,9 @@ class ApiRequestGenerator extends GeneratorForAnnotation<ApiRequest>
includeToken = requestTypeName != 'login';
}
// 从类的声明字段生成 toJson()
final toJsonBody = _buildToJsonBody(element, className);
return '''
/// Generated by @ApiRequest for [$className]
mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
@@ -80,6 +128,8 @@ mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
@override
bool get includeToken => $includeToken;
@override
Map<String, dynamic> toJson() => $toJsonBody;
@override
Map<String, dynamic>? get parameters {
registerResponse<$responseTypeName>($responseTypeName.fromJson);
return super.parameters;
@@ -88,6 +138,46 @@ mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
''';
}
/// 从类的声明字段构建 toJson() 方法体
///
/// 只读取类自身声明的实例字段(非 static、非 synthetic
/// 不含继承自 ApiRequestable 的属性,避免递归。
/// 支持 @JsonKey(name: '...') 字段重命名,
/// 以及 @JsonKey(includeToJson: false) 跳过字段。
String _buildToJsonBody(ClassElement element, String className) {
final fields = element.fields
.where((f) => !f.isStatic && !f.isSynthetic)
.toList();
if (fields.isEmpty) {
return '<String, dynamic>{}';
}
final entries = <String>[];
for (final field in fields) {
// 检查 @JsonKey 注解
final jsonKeyAnnotation = _jsonKeyChecker.firstAnnotationOfExact(field);
// @JsonKey(includeToJson: false) → 跳过
final includeToJson = jsonKeyAnnotation
?.getField('includeToJson')
?.toBoolValue();
if (includeToJson == false) continue;
// JSON 键名:@JsonKey(name: '...') 或字段名
final jsonName =
jsonKeyAnnotation?.getField('name')?.toStringValue() ?? field.name;
entries.add("'$jsonName': (this as $className).${field.name}");
}
if (entries.isEmpty) {
return '<String, dynamic>{}';
}
return '<String, dynamic>{${entries.join(', ')}}';
}
/// 从 DartObject 提取枚举常量名称
String _readEnumName(dynamic dartObject, String defaultValue) {
final index = dartObject.getField('index')?.toIntValue();
@@ -105,4 +195,4 @@ mixin _\$${className}Api on ApiRequestable<$responseTypeName> {
return defaultValue;
}
}
}

View File

@@ -59,7 +59,15 @@ typedef OnEncryptRequest =
///
/// [responseData] 的实际类型取决于服务端响应格式:
/// - 加密模式下通常是 base64 字符串
/// - 非加密模式下是 `Map<String, dynamic>`
/// - 非加密模式下是 `Map<String, dynamic>`(拦截器会自动跳过,不调用此回调)
///
/// 实现时建议做类型判断兜底,应对非预期的响应格式:
/// ```dart
/// onDecryptResponse: (data) async {
/// if (data is! String) throw FormatException('Expected String, got ${data.runtimeType}');
/// return jsonDecode(aesDecrypt(data));
/// }
/// ```
typedef OnDecryptResponse =
Future<Map<String, dynamic>> Function(Object responseData);