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:
@@ -10,6 +10,11 @@
|
||||
/// P0-1 层 1(UI06 分析 v2):入队前**本地查重**——同账号 + 同相册 +
|
||||
/// state=uploaded + filename+bytes 匹配 → 返回 [DuplicateCheck],由
|
||||
/// 调用方决定弹窗确认或跳过。批量场景汇总后单次弹窗(非逐张)。
|
||||
///
|
||||
/// Task 8(服务端查重 App 侧集成):弹窗「全部上传」= 用户确认强传——
|
||||
/// 重复项以 force=true 入队([enqueueForUpload] 的 forcePaths),任务行
|
||||
/// 落库后由引擎在 initUpload 携带 force 跳过服务端查重(仅 owner/admin
|
||||
/// 生效,服务端裁决——协作者强传仍会被 409 拒绝并可见失败文案)。
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
@@ -77,6 +82,13 @@ final multiImagePickerProvider = Provider<MultiImagePicker>(
|
||||
/// 选图软上限(用户要求不设硬限制;9999 为防误触极端值)。
|
||||
const int kPickerSoftMaxAssets = 9999;
|
||||
|
||||
/// 查重弹窗选择结果(Task 8):null=取消(不上传)。
|
||||
///
|
||||
/// 「跳过重复」→ skipPaths=重复项(仅非重复入队);「全部上传」→
|
||||
/// forcePaths=重复项(强传,见库级注释)。两个集合互斥且可为空。
|
||||
typedef DuplicateDialogChoice
|
||||
= ({Set<String> skipPaths, Set<String> forcePaths});
|
||||
|
||||
/// 查重结果(单张):匹配到的已上传任务摘要。
|
||||
class DuplicateMatch {
|
||||
const DuplicateMatch({
|
||||
@@ -162,6 +174,10 @@ Future<List<DuplicateMatch>?> checkDuplicates(
|
||||
|
||||
/// 批量入队:通知权限单次请求 + 逐张准备入队。
|
||||
///
|
||||
/// [skipPaths]:用户选择跳过的重复项;[forcePaths]:查重弹窗「全部上传」
|
||||
/// 确认强传的重复项(Task 8——落到任务行 force 标记,引擎 initUpload
|
||||
/// 携带;仅 owner/admin 生效,服务端裁决)。两者互斥。
|
||||
///
|
||||
/// 返回 [EnqueueResult](含 enqueued 计数与 duplicates 列表)。
|
||||
/// 准备失败的任务已落库为 preparationFailed(连拍页失败徽章/队列页可见,
|
||||
/// 不在此抛出)。
|
||||
@@ -172,6 +188,7 @@ Future<EnqueueResult> enqueueForUpload(
|
||||
String? categoryId,
|
||||
required List<String> sourcePaths,
|
||||
List<String>? skipPaths,
|
||||
List<String>? forcePaths,
|
||||
}) async {
|
||||
// 首次入队动作触发(幂等;拒绝/非 Android 静默降级,见实现注释)。
|
||||
await ref.read(notificationPermissionProvider).ensureRequested();
|
||||
@@ -183,12 +200,16 @@ Future<EnqueueResult> enqueueForUpload(
|
||||
.where((path) => !skipSet.contains(path))
|
||||
.toList();
|
||||
|
||||
// Task 8:「全部上传」确认的强传项(重复项 force=true 落任务行)。
|
||||
final forceSet = forcePaths?.toSet() ?? <String>{};
|
||||
|
||||
final results = await Future.wait(
|
||||
effective.map((path) => preparer.prepare(
|
||||
accountId: accountId,
|
||||
albumId: albumId,
|
||||
categoryId: categoryId,
|
||||
sourcePath: path,
|
||||
force: forceSet.contains(path),
|
||||
)),
|
||||
);
|
||||
final enqueued = results
|
||||
|
||||
Reference in New Issue
Block a user