import 'dart:typed_data'; import 'package:json_annotation/json_annotation.dart'; import 'package:networks_sdk/networks_sdk.dart'; import '../../../core/foundation/api_paths.dart'; part 'upload_file_request.g.dart'; /// # /upload/file — 文件上传(Upload 请求示例) /// /// 演示两种上传模式: /// /// ## 模式 A: FormData 上传到自有后端 /// 适用于后端直接接收文件的场景。 /// 使用 [UploadFileRequest] — path 为相对路径,SDK 自动拼 baseURL。 /// /// ## 模式 B: 二进制上传到 S3 presigned URL /// 适用于先向后端获取 presigned URL,再直接上传到 S3 的场景。 /// 使用 [S3UploadRequest] — path 为完整 URL,override decodeResponse。 /// /// ## Upload 与普通请求的区别 /// /// | 普通请求 | Upload 请求 | /// |---------|-----------| /// | `toJson()` → JSON body | `uploadData` → FormData / Uint8List | /// | `requestType: request` | `requestType: upload` | /// | `parameters` 有值 | `parameters` 返回 null | /// | 标准 `{ code, msg, data }` 响应 | 可能需要 override `decodeResponse` | // ───────────────────────────────────────────── // Response DTO // ───────────────────────────────────────────── /// 文件上传接口的业务响应数据(对应服务端 `data` 字段)。纯 Dart 类,无需任何注解。 class UploadResult { final String url; @JsonKey(name: 'file_id') final String fileId; const UploadResult({required this.url, required this.fileId}); } // ═════════════════════════════════════════════ // 模式 A: FormData 上传到自有后端 // ═════════════════════════════════════════════ /// FormData 上传请求 /// /// 上传到自有后端 `/upload/file`,响应为标准 `{ code, message, data }` 格式。 /// 无需 override `decodeResponse`。 @ApiRequest( path: ApiPaths.uploadFile, method: HttpMethod.post, responseType: UploadResult, requestType: ApiRequestType.upload, ) class UploadFileRequest extends ApiRequestable with _$UploadFileRequestApi { final String filePath; final String? fileName; UploadFileRequest({required this.filePath, this.fileName}); @override Map toJson() => {}; /// FormData — SDK 通过 uploadData 获取上传数据 @override Object? get uploadData { return FormData.fromMap({ 'file': MultipartFile.fromFileSync(filePath, filename: fileName), }); } } // ═════════════════════════════════════════════ // 模式 B: 二进制上传到 S3 presigned URL // ═════════════════════════════════════════════ /// S3 presigned URL 上传响应 class S3UploadResponse { final bool success; final String? message; const S3UploadResponse({this.success = true, this.message}); } /// S3 presigned URL 上传请求 /// /// 特点: /// - path 为完整的 presigned URL(SDK 检测到 http 开头不拼 baseURL) /// - uploadData 为 Uint8List 二进制数据 /// - 自定义 headers(Content-Type: application/octet-stream) /// - override decodeResponse — S3 返回 204 No Content 或 XML,不是标准响应格式 class S3UploadRequest extends ApiRequestable { final Uint8List data; final String presignedURL; S3UploadRequest({required this.data, required this.presignedURL}); @override String get path => presignedURL; @override HttpMethod get method => HttpMethod.put; @override ApiRequestType get requestType => ApiRequestType.upload; @override Map? get customHeaders => { 'Content-Type': 'application/octet-stream', }; @override Map toJson() => {}; /// 二进制数据 @override Object? get uploadData => data; /// S3 响应不走标准 { code, message, data } 格式,需要自定义解码 /// /// 可能的响应: /// - 204 No Content(空 body)→ 成功 /// - 200 + XML body → 成功 /// - 200 + JSON body → 尝试解码 @override S3UploadResponse? decodeResponse(Response response) { // 空响应或 2xx 状态码 → 成功 if (response.data == null || (response.data is List && (response.data as List).isEmpty)) { return const S3UploadResponse(success: true); } // JSON 响应 → 尝试解码 if (response.data is Map) { final json = response.data as Map; return S3UploadResponse( success: true, message: json['message'] as String?, ); } // 2xx 状态码 → 成功 if (response.statusCode != null && response.statusCode! >= 200 && response.statusCode! < 300) { return const S3UploadResponse(success: true); } return const S3UploadResponse(success: true); } }