feat(app): wire server dedup - force passthrough + PHOTO_DUPLICATE classification & l10n
Task 8: close the gap between server-side duplicate detection (90dcc58)
and the app.
- ApiClient.mapDioException: normalize 409 wire body reusing
VALIDATION_FAILED with message=PHOTO_DUPLICATE to
ErrorCodes.photoDuplicate (message-based; auto-compat if the contract
later adds a dedicated code)
- l10n zh/en errorPhotoDuplicate; error_l10n maps the code (queue page
inherits via fallback)
- UploadEngine._classifyFailure: PHOTO_DUPLICATE joins the no-auto-retry
bucket (retrying the same init body would 409 again); force to skip
dedup travels only through the enqueue path
- UploadTasks drift table +force column (schema v2, addColumn migration;
legacy rows default false); UploadTaskDraft/UploadTask/prepare carry it
- enqueueForUpload +forcePaths; duplicate dialog "全部上传" (capture page
+ album detail page) now returns DuplicateDialogChoice so confirmed
duplicates enqueue with force=true (server adjudicates owner/admin)
- engine init body carries force only when true (absent otherwise)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,11 @@ class UploadTasks extends Table {
|
||||
TextColumn get state =>
|
||||
text().map(const _UploadTaskStateConverter())();
|
||||
|
||||
/// 用户已确认强传(P0-1 Task 8):查重弹窗「全部上传」入队的任务置
|
||||
/// true,initUpload 携带 force 跳过服务端查重(仅 owner/admin 生效,
|
||||
/// 服务端裁决)。缺省 false(正常查重)。
|
||||
BoolColumn get force => boolean().withDefault(const Constant(false))();
|
||||
|
||||
/// 已尝试传输次数(退避与上限判断依据)。
|
||||
IntColumn get attempt => integer().withDefault(const Constant(0))();
|
||||
|
||||
@@ -128,8 +133,18 @@ class UploadTaskDb extends _$UploadTaskDb {
|
||||
/// 测试用内存库(drift NativeDatabase.memory,随 close 丢弃)。
|
||||
UploadTaskDb.memory() : super(NativeDatabase.memory());
|
||||
|
||||
/// v1:初始表;v2(Task 8):+force 列(查重弹窗「全部上传」强传标记)。
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
int get schemaVersion => 2;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onUpgrade: (m, from, to) async {
|
||||
if (from == 1) {
|
||||
await m.addColumn(uploadTasks, uploadTasks.force);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 打开生产库:app documents 目录下 upload_tasks.sqlite,后台 isolate 执行
|
||||
@@ -154,6 +169,7 @@ class UploadTaskDraft {
|
||||
required this.filename,
|
||||
required this.bytes,
|
||||
required this.contentType,
|
||||
this.force = false,
|
||||
});
|
||||
|
||||
/// 任务 ID(uuid,调用方生成)。
|
||||
@@ -182,6 +198,9 @@ class UploadTaskDraft {
|
||||
|
||||
/// MIME 类型。
|
||||
final String contentType;
|
||||
|
||||
/// 用户已确认强传(查重弹窗「全部上传」);initUpload 携带 force。
|
||||
final bool force;
|
||||
}
|
||||
|
||||
/// 状态迁移时一并写入的旁路字段(未提供的列保持不变;`null` 与"清除"通过
|
||||
@@ -279,6 +298,7 @@ class TaskStore {
|
||||
filename: draft.filename,
|
||||
bytes: draft.bytes,
|
||||
contentType: draft.contentType,
|
||||
force: Value(draft.force),
|
||||
state: UploadTaskState.preparing,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
|
||||
Reference in New Issue
Block a user