Files
photo-live/docs/plans/2026-09-29-选图时已传照片点选拦截-实施计划.md
浅念 7e5c550fb3 feat(app): block selecting already-uploaded photos at picker tap (double-tap to force)
方案 A(选图点选拦截):multiImagePickerProvider 经 wechat_assets_picker
10.1.3 官方钩子 AssetPickerConfig.selectPredicate 挂 PickerDuplicateGate——
点选「已上传到目标相册」的照片当场拦下(SnackBar 提示),再次点击同一张
强制选择(escape hatch)。

- MultiImagePicker typedef 增参 accountId/albumId(拦截判定范围=当前
  目标相册;相册详情/连拍页两处调用点传入);
- 判定口径同 checkDuplicatesUnified:本机 TaskStore(同账号+相册+
  completed,filename+bytes)∪ 服务器单项预检(bytes;本机命中短路
  服务器,点选延迟考量);任一半场异常按该半场无命中,fileSize 失败等
  整体异常直接放行——enqueue 弹窗 + init 409 兜底不变;
- per-asset 判定缓存(二次点击瞬回)+ 二次点击放行集;取消选择恒放行
  且不预热放行集;
- AssetEntity 事实适配(PickerAssetFact 测试接缝):fileSize 为
  photo_manager 3.12 的 Future<int> 平台通道 getter(entity.dart:778),
  惰性求值;title 为 String?(Android 恒有值;iOS 需 needTitle——null
  时本机半场跳过,服务器按 bytes 仍可判);
- l10n pickerDuplicateBlocked(zh/en);测试替身签名同步
  (album_detail/capture_page 页测 + integration_test app_boot),
  页测补 (accountId, albumId) 接线断言;gate 五态用例入 enqueue_test。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 10:10:49 +08:00

3.9 KiB
Raw Permalink Blame History

选图时已上传照片「点选拦截」实施计划(方案 A)

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 相册选图器里点选「已上传到目标相册」的照片时当场拦下并提示;再次点击同一张可强制选择(保留活口)。判定口径与现有查重一致(本机历史 ∪ 服务器预检,当前相册,bytes 为主)。

Architecture: 用 wechat_assets_picker 10.1.3 的官方钩子 AssetPickerConfig.selectPredicate(源码确认:每次点选/取消时调用,FutureOr<bool>,返回 false 静默不选)。拦截器持有 per-asset 结果缓存 + 二次点击放行集;服务器/本地查询失败一律放行(enqueue 弹窗 + init 409 兜底不变)。不做视觉置灰(那是方案 B,另行评估)。

已确认决策(2026-09-29): 方案 A;保留强制口子(提示「已上传过——再点一次可强制选择」,二次点击放行);判定范围=当前目标相册。

设计要点:

  • MultiImagePicker typedef 增参:(BuildContext context, {required String accountId, required String albumId})(拦截判定需要账号+相册;两处调用点都已持有)。albumId/accountId 缺省场景不存在——两个调用点必传。
  • 拦截器 _PickerDuplicateGate(enqueue.dart 私有类):
    • decide(ctx, asset, selected):取消选择(selected=true)恒放行;已缓存判定直接用;
    • 判定 = 本机 TaskStore(同账号+相册+completed,filename+bytes)∪ 服务器 checkUploadDuplicates(单项,filename=asset.title,bytes=asset 文件大小);两路任一命中=重复;
    • 查询异常(网络/存储)→ 放行(不能因预检故障挡住正常选图);
    • 重复 → 首次 false + SnackBar 提示(用谓词收到的 ctx,ScaffoldMessenger);该 asset.id 记入放行集 → 二次点击 true;
    • per-asset 判定缓存(Map<assetId, bool>):避免重复查询同一张。
  • AssetEntity 文件大小 API 以 photo_manager 3.12 源码为准(实现前先查清是 length 还是异步方法);asset.title 可能为 null → 本机跳过(服务器按 bytes 仍可判)。
  • l10n:pickerDuplicateBlocked(zh「这张已上传到该相册——再点一次可强制选择」/ en 对应)。
  • 测试替身同步改签名:album_detail_page_test / capture_page_test / integration_test helpers/app_boot.dart。

Task 1: 点选拦截器 + 双页接线(TDD,单任务单提交)

Files:

  • Modify: apps/mobile/lib/features/uploads/enqueue.dart(typedef + gate + provider)

  • Modify: apps/mobile/lib/pages/album_detail_page.dart、capture_page.dart(传 accountId/albumId)

  • Modify: apps/mobile/lib/l10n/app_zh.arb、app_en.arb(+pickerDuplicateBlocked)

  • Test: apps/mobile/test/features/uploads/enqueue_test.dart(gate 用例)、test/pages/{album_detail_page,capture_page}_test.dart、integration_test/helpers/app_boot.dart(替身签名)

  • Step 1: 失败测试(gate 五态:本机命中首次拒/二次放行;服务器单项命中拒;两路空放行;服务器异常放行;取消选择恒放行——用 ProviderScope override 注入 fake taskStore/apiClient)

  • Step 2: 确认失败 → 实现(先查 photo_manager AssetEntity 文件大小 API 与 title 语义)

  • Step 3: 全绿(flutter analyze + 全量 test)

  • Step 4: Commit feat(app): block selecting already-uploaded photos at picker tap (double-tap to force)

风险与回退: 谓词异步查询在点选瞬间有 ~100-300ms 延迟(有缓存后二次即瞬回);查询失败放行=最坏回到现状(弹窗+409 兜底);revert 单 commit 即回退。置灰视觉(方案 B)不在本次。