feat(mobile): upload engine with preparation, batching states and retries

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-19 02:50:40 +08:00
parent d96e56f653
commit 3ddb0ea2d4
23 changed files with 3232 additions and 12 deletions
+14 -2
View File
@@ -81,8 +81,9 @@ class UploadCredential {
/// `initUpload`,POST /uploads):幂等初始化——相同 clientTaskId 且参数
/// 一致时返回原会话,参数不一致 409(UPLOAD_SESSION_CONFLICT)。
///
/// 注:契约增补(P3 计划 Task 3)将顺带加入可选 sourceDeviceId
/// (A19 设备来源语义);当前以契约现状为准,不含该字段。
/// sourceDeviceId 为 P3 Task 3 契约增补(已落地):客户端首次生成并
/// 持久的设备标识(A19 设备来源语义,见 core/device_id.dart);可选,
/// 同一 clientTaskId 幂等重放时服务端保留首次落库的值。
class UploadInitRequest {
const UploadInitRequest({
required this.albumId,
@@ -91,6 +92,7 @@ class UploadInitRequest {
required this.bytes,
required this.contentType,
this.categoryId,
this.sourceDeviceId,
});
/// 目标相册 ID。
@@ -111,6 +113,9 @@ class UploadInitRequest {
/// MIME 类型(白名单 image/jpeg | image/png | image/heic)。
final String contentType;
/// 发起设备标识(可选,minLength 8 / maxLength 64;A19)。
final String? sourceDeviceId;
Map<String, dynamic> toJson() => {
'albumId': albumId,
if (categoryId != null) 'categoryId': categoryId,
@@ -118,6 +123,7 @@ class UploadInitRequest {
'filename': filename,
'bytes': bytes,
'contentType': contentType,
if (sourceDeviceId != null) 'sourceDeviceId': sourceDeviceId,
};
}
@@ -243,6 +249,7 @@ class UploadSessionRecord {
required this.contentType,
required this.createdAt,
required this.photo,
this.sourceDeviceId,
});
factory UploadSessionRecord.fromJson(Map<String, dynamic> json) =>
@@ -257,6 +264,7 @@ class UploadSessionRecord {
photo: UploadSessionPhoto.fromJson(
json['photo'] as Map<String, dynamic>,
),
sourceDeviceId: json['sourceDeviceId'] as String?,
);
/// 上传会话 ID。
@@ -280,6 +288,9 @@ class UploadSessionRecord {
/// 会话创建时间。
final DateTime createdAt;
/// 发起设备标识(init 时未携带为 null;P3 Task 3 契约增补,A19)。
final String? sourceDeviceId;
/// 会话关联照片的当前状态。
final UploadSessionPhoto photo;
@@ -291,6 +302,7 @@ class UploadSessionRecord {
'expectedBytes': expectedBytes,
'contentType': contentType,
'createdAt': createdAt.toUtc().toIso8601String(),
if (sourceDeviceId != null) 'sourceDeviceId': sourceDeviceId,
'photo': photo.toJson(),
};
}