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:
@@ -462,13 +462,16 @@ class _CapturePageState extends ConsumerState<CapturePage>
|
||||
);
|
||||
|
||||
Set<String> skipPaths;
|
||||
Set<String> forcePaths;
|
||||
if (duplicates != null && duplicates.isNotEmpty && mounted) {
|
||||
// 批量确认弹窗(单次,非逐张)。
|
||||
final skip = await _showDuplicateDialog(l10n, duplicates, paths);
|
||||
if (skip == null) return; // 取消——不上传。
|
||||
skipPaths = skip;
|
||||
final choice = await _showDuplicateDialog(l10n, duplicates, paths);
|
||||
if (choice == null) return; // 取消——不上传。
|
||||
skipPaths = choice.skipPaths;
|
||||
forcePaths = choice.forcePaths;
|
||||
} else {
|
||||
skipPaths = <String>{};
|
||||
forcePaths = <String>{};
|
||||
}
|
||||
|
||||
final result = await enqueueForUpload(
|
||||
@@ -478,6 +481,7 @@ class _CapturePageState extends ConsumerState<CapturePage>
|
||||
categoryId: _categoryId,
|
||||
sourcePaths: paths,
|
||||
skipPaths: skipPaths.toList(),
|
||||
forcePaths: forcePaths.toList(),
|
||||
);
|
||||
if (!mounted) return;
|
||||
final uploaded = result.enqueued;
|
||||
@@ -493,8 +497,10 @@ class _CapturePageState extends ConsumerState<CapturePage>
|
||||
);
|
||||
}
|
||||
|
||||
/// 批量重复确认弹窗:返回 null=取消 / 空 Set=全部上传 / 非空=跳过项路径集。
|
||||
Future<Set<String>?> _showDuplicateDialog(
|
||||
/// 批量重复确认弹窗(Task 8):null=取消;「跳过重复」→ skipPaths=
|
||||
/// 重复项;「全部上传」→ forcePaths=重复项(用户确认强传,服务端
|
||||
/// force 查重跳过仅 owner/admin 生效——协作者仍会被 409 拒绝)。
|
||||
Future<DuplicateDialogChoice?> _showDuplicateDialog(
|
||||
AppLocalizations l10n,
|
||||
List<DuplicateMatch> duplicates,
|
||||
List<String> allPaths,
|
||||
@@ -535,8 +541,11 @@ class _CapturePageState extends ConsumerState<CapturePage>
|
||||
);
|
||||
|
||||
if (choice == 'cancel' || choice == null) return null;
|
||||
if (choice == 'skip') return dupPaths;
|
||||
return <String>{}; // 'all' = 不跳过任何
|
||||
if (choice == 'skip') {
|
||||
return (skipPaths: dupPaths, forcePaths: <String>{});
|
||||
}
|
||||
// 'all' = 用户确认强传:重复项 force 入队(不跳过任何)。
|
||||
return (skipPaths: <String>{}, forcePaths: dupPaths);
|
||||
}
|
||||
|
||||
void _switchCategory(String? categoryId) {
|
||||
|
||||
Reference in New Issue
Block a user