157 lines
5.1 KiB
Dart
157 lines
5.1 KiB
Dart
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<UploadResult>
|
||
with _$UploadFileRequestApi {
|
||
final String filePath;
|
||
final String? fileName;
|
||
|
||
UploadFileRequest({required this.filePath, this.fileName});
|
||
|
||
@override
|
||
Map<String, dynamic> 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<S3UploadResponse> {
|
||
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<String, String>? get customHeaders => {
|
||
'Content-Type': 'application/octet-stream',
|
||
};
|
||
|
||
@override
|
||
Map<String, dynamic> 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<String, dynamic>) {
|
||
final json = response.data as Map<String, dynamic>;
|
||
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);
|
||
}
|
||
}
|