Commit Graph

307 Commits

Author SHA1 Message Date
浅念 7f8cda1cd3 fix(viewer): 去除头部下方残留的旧广告位实例(位置迁移漏删)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 10:14:55 +08:00
浅念 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
浅念 2261b8aebc style(viewer): 广告轮播移至宣传图与标题之间(after-banner 插槽)
用户定稿:广告位放标题上面——AlbumHeader 增具名插槽注入,
组件不感知广告域;无 banner 时仍居标题上方。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 10:03:15 +08:00
浅念 328140494f docs(plan): picker tap-time duplicate gate (option A) — incremental plan
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 09:51:41 +08:00
浅念 c64db3c53a docs: 广告位轮播验收记录与实施计划 Task 13 勾选(线上部署完成)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 09:35:37 +08:00
浅念 19d4fd143a feat(app): banner crop ratio 3:1 -> 16:9 (output <=1920x1080, preview height aligned with cover)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 09:32:28 +08:00
浅念 bfba76b5e6 fix(ads): 终审修复——staging 前缀+运行时上限核验/失效绑定编辑/白名单去 heic/杂项
- verifyStaging 硬化(services/api/src/ads/ads.service.ts):新增键前缀核验
  (仅 OBJECT_KEY_ROOT/ads/staging/ 布局,防照片域任意键被提升为公开广告)
  与运行时 2MB 上限(head.bytes > AD_UPLOAD_MAX_BYTES → 400 报文含限额值
  ——init 申明管不住实际 PUT 体,create/update 是唯一核验点);e2e ⑰ 以
  测试 storage 直接 presignPut 真实对象覆盖两分支
- 失效绑定编辑 400(admin AdsFormDialog):PATCH 仅在 albumIds 实际变化时
  携带(albumIdsChanged 序敏感深比较,纯函数落 adsLogic + vitest 用例);
  preloadOptions 解析失败的绑定 ID 加「已失效,可移除」后缀(zh/en 新增
  adsManage.form.staleAlbum)——软删绑定不再挡住其它字段保存
- 白名单去 heic(原图直出无变体兜底,Chromium/Firefox 不渲染=破图):
  contracts 三处 enum 收窄 [image/jpeg, image/png] 并重生成类型;dto 定义
  广告域独立 AD_IMAGE_CONTENT_TYPES(与照片域 UPLOAD_CONTENT_TYPES 分离),
  AD_IMAGE_EXTENSIONS 由其派生;init 对 .heic 文件名以广告域集合二次核验
  拒绝(共享 extOfFilename 仍认 heic,杜绝 init 放行→create 中段断裂);
  e2e heic 矩阵 400;admin accept/文案/类型同步收窄
- searchAlbums 补 catch(http 层已弹 toast,本地静默保持旧选项)
- ActivityPage 关闭记忆测试补位(activity.spec):dismissed 不拉取不渲染、
  close 写 sessionStorage 并清空轮播两用例(getPublicAds 入替身防真实 fetch)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 02:20:21 +08:00
浅念 041cfc77b7 docs: 广告位实施计划 Task 5-12 勾选
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 01:59:57 +08:00
浅念 b3e89f7efb fix(admin): 广告位排序并列簇修正——create 追加 max+1/move 纯逻辑抽取/相册下拉预载
评审跟进(C1/I1/M3/M5):

- C1 排序并列簇:新建此前不送 sortOrder(服务端缺省 0)→ 3+ 广告即全 0
  并列,首次上移/下移即产出错误顺序且经 outbox 立即传导 ov。修复双管:
  ① 新建载荷带 sortOrder=父页 max(现有)+1(空列表=0,尾部追加保证全序,
  并列簇从源头不再产生——无存量数据,评审选项 B);② 上移/下移目标值
  计算抽取为纯函数 planAdjacentMove(异值互换/并列错开抬邻居避免负值),
  ≥3 并列簇局限以 spec 文档化用例登记(create 追加后为 dead insurance),
  index.vue 改用纯函数并跳过值未变侧的 no-op PATCH
- I1 相册下拉:对话框每次打开均预载选项(新建态同样,对照 SlotBindDialog
  打开即 searchAlbums('') 先例),消除未输入时误显「无匹配相册」;编辑态
  绑定 ID 兜底回显逻辑合并入 preloadOptions
- M3 en 复数:scopeTargeted 用 vue-i18n 复数 "{n} album | {n} albums"
  (t(key, n))
- M5 预览对象 URL 兜底:onUnmounted(releasePreview) 防非常规卸载路径泄漏

已登记暂缓(非本次):M2 move 时序守卫细化、M4 相册搜索与 SlotBindDialog
的同构抽取、M6 编辑 diff-only 载荷。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 01:35:09 +08:00
浅念 239ef5648f feat(admin): 广告位管理页——上传/链接/绑定相册/开关/排序
Task 11(广告位轮播):admin 后台广告位治理页。

- utils/ads/adsLogic:scopeKind/normalizeAlbumIds/isValidAdLinkUrl 纯逻辑
  (与服务端 dto LINK_URL_PATTERN、albumIds 归一语义同源)+ vitest 6 例
- api/ads + types/api/ads.d.ts:list/create/update/delete/initUpload 五端点
  (对照 embedSlots.ts 逐字风格;列表全量不分页、imageUrl 现签)
- views/system/ads:列表(缩略图/备注名/链接截断/投放范围标签/启停开关
  失败重拉回滚/上移下移 sortOrder 互换/硬删确认)+ 新建编辑对话框
  (staging 直传 init→裸 fetch PUT、Content-Type 与申明一致;相册多选
  远程搜索复用 /v1/albums 分页+本地过滤,绑定 ID 超限/失效以裸 ID 兜底;
  编辑换图才带 image 三字段;albumIds 归一空→null 转全局)
- 路由/菜单:system 组 ads 页(ri:advertisement-line,workspace 分组);
  locales zh/en 补 menus.system.ads + adsManage 子树(键集一致入单测)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 01:21:33 +08:00
浅念 7f9789eb46 polish(viewer): 广告轮播 a11y 补全——aria-current/focus-visible/touch-action/键名对齐
评审跟进(Task 10 a11y 两项 + 三个顺手项):
- 圆点当前项程序化状态 :aria-current(ThumbnailStrip:103 同形),active 圆点
  类名对齐 sibling 约定 ad-dot--active → .active(viewer 无 BEM -- 修饰符)
- .ad-slide/.ad-close/.ad-dot 补 :focus-visible 2px 主色描边
  (AlbumHeader/ThumbnailStrip 同规;主色对暗色衬底/蒙层对比 ≈4:1)
- .ad-carousel 补 touch-action: pan-y(ThumbnailStrip pan-x 对偶:
  水平翻页自持、纵向滚动交还页面)
- sessionStorage 键名 viewer:ads-dismissed → viewer.adsDismissed(sibling
  点号命名约定;仅 ActivityPage 两处引用,测试无引用)
- ad-carousel.spec:类名断言随改 + aria-current 前后迁移断言

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 01:08:13 +08:00
浅念 f1c13bff7c feat(viewer): 相册页广告轮播——4s 自动/暂停/滑动/徽标/会话级关闭
Task 10(广告位轮播实施计划):
- ads/carousel.ts 纯逻辑:nextIndex 环形推进(末→首、单条恒 0)+ swipeStep
  滑动判定(≥40px 且水平分量严格大于垂直),carousel.spec 直测 7 例
- AdCarousel.vue:10:3 轮播,4s 自动推进(本地常量 ROTATE_MS);pointerenter/
  pointerdown 暂停、pointerleave 恢复;圆点跳页;touchstart/touchend 滑动翻页
  (cancel 合成 click);单条静态无圆点;左上角常驻「广告」徽标 + 右上角 ≥44px
  ✕ 关闭(emit close,组件无副作用);视觉一律 var(--viewer-*)(半透明层经
  color-mix 自 token 派生)
- client.ts:PublicAd/PublicAdListResponse 契约类型 + getPublicAds(同形
  apiRequest);__tests__/ad-carousel.spec.ts 组件级 6 例(圆点/关闭/window.open
  spy/假时钟自动轮播与暂停/滑动/单条静态)
- ActivityPage:load() 成功分支独立拉取广告(失败静默、会话级关闭记忆
  viewer:ads-dismissed 先行、沿用 sequence 防跨相册竞态);AlbumHeader 与
  PhotoFeed 之间挂 AdCarousel;i18n zh-CN/en 补 ads.* 词条

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 00:58:48 +08:00
浅念 80405cfcc1 chore(api): 公开广告端点补 enabled 投影不变量注释(Task 9 评审建议)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 00:50:46 +08:00
浅念 373f0fc533 feat(api): GET /v1/public/albums/{token}/ads 公开广告(全局+定向/现签)
ov 公开端点(ads-carousel Task 9,设计 §5.2):相册校验复用
findPublicAlbumOv(不存在/offline/紧急停用统一 404 ALBUM_NOT_PUBLIC);
可见集 = 全局投放(albumIds=null)∨ 绑定本相册,sortOrder 升序、并列
createdAt 升序;imageUrl 逐条现签 GET(TTL=SIGNED_URL_TTL_SECONDS)。
匿名无 guard、不加独立限速档(观众页单次加载一次,与公开相册读同量级,
量级假设 ≤ 百条:全表捞 + 应用层 JSON 过滤)。

public-ov.e2e 追加 ⑧:prismaOv 直种 albums_ov/ads_ov——过滤/排序/现签
(可取回真实字节)/404 同码同 path/空 items/匿名 200;9/9 通过,
ads.e2e 16/16 回归通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 00:44:23 +08:00
浅念 9ec8226577 fix(sync): reconciler 清扫环补 ad——错过的 ad.* 推送恢复自愈通道
SWEEP_ENTITY_TYPES 追加 'ad':生产对账循环阶段一逐类拉 cn reconcile
清单时纳入广告当前态(清单恒全量、确定性排序),重放走既有
content-batches 幂等管道。此前 ad 只有 cn 端点而无调用方——ad.* outbox
事件「投后即失」时 ads_ov 无自愈路径(digest 是相册维度计数,覆盖不了
无相册归属的广告;含 enabled=false 下线与硬删墓碑后的终态收敛)。

sweep() 对各类型均一处理(无 per-type 簿记),'ad' 与 'asset' 同构接入;
文件头/Stats 注释同步(四类→五类)。

测试:reconcile.e2e ⑤ 追加 runOnce 生产环断言——sweptEvents=5
(album+photo×2+ad×2)、replayApplied=2(仅 ad 新 eventId)、
ads_ov 两行投影就位(全局 albumIds NULL/定向数组)、digest 稳态不受
影响。sync 全量 e2e 4 suites/28 tests、api ads 16/16、typecheck/lint 净。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 00:26:21 +08:00
浅念 60b675a908 feat(sync): 对账清单 ad 实体当前态清扫
Task 8(ads-carousel):cn reconcile 增 entityType='ad' 分支——纯 SELECT
当前态映射(sortOrder/createdAt 确定性排序,albumId 恒空串信封约定),
载荷复用 ads/ad-payload.ts 单一出处(与业务事务内即时事件逐字节同形,
纯函数模块无依赖环);DTO SYNC_ENTITY_TYPES 追加 'ad'(对齐契约 enum)。

随附质量审查两项:
- ads.service list() 行→AdminAdItem 映射复用 toItem(删 ~14 行内联重复,
  ⑮ 补 admin list 200 形状断言覆盖重构路径);
- 新增 test/ads-extensions.spec.ts 守护 AD_IMAGE_EXTENSIONS ===
  UPLOAD_CONTENT_TYPES 派生归一扩展名集合(jpeg→jpg),防三处白名单漂移。

测试基建:reset.ts RESET_TABLES 补 'ads'(Task 6 建表遗漏,固定 id 直种
依赖跨 spec 清场);reconcile.e2e-spec 追加 ⑤(红→绿,5/5)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 00:22:13 +08:00
浅念 4979091d2c feat(api): 广告位 CRUD——outbox ad.* 事件/audit/停用高优先级/版本化换图
Task 7(ads-carousel):/v1/admin/ads list/create/patch/delete 四端点。
- 事务三写:ads 行 + outbox(ad.updated 全量快照/ad.deleted 墓碑)+ audit
  (ad.create/update/delete);ad.* 信封 albumId 恒空串;停用迁移 true→false
  与删除均 priority='high';
- 创建/换图:staging head 三核(存在/bytes/contentType,assets complete 同
  口径)→ 事务内 copyIfAbsent 先于行写入(失败=孤儿终键对象,OSS 生命周期
  兜底)→ 版本化终键 adObjectKey(adId, v, ext);
- 更新:embed 乐观重试模式(条件 updateMany WHERE entityVersion),保证换图
  终键版本段与行版本严格对齐;换图三字段同时性校验(缺一 400);albumIds
  去重+相册存在性 count 校验([]/null 归一全局 null);
- 删除:条件 deleteMany 重跑保墓碑 entityVersion=末+1;重复删除 404;
- 载荷构建器独立 ad-payload.ts(Task 8 对账共用,逐字节同形约定);
- dto:AdCreateRequestDto.albumIds 收窄为 string[] | null(JSON null 在
  @IsOptional 下可达,embed settings 先例);
- e2e:describe 更名 ads domain (e2e),追加 ⑨-⑯ 八用例(真图直传/定向/
  非法 linkUrl/字段级 PATCH/换图/DELETE/权限/同步贯通),16/16 通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 23:57:43 +08:00
浅念 121afadf1e feat(api): /admin/ads/uploads 广告图 staging 预签名(2MB/白名单/admin-only)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 23:34:11 +08:00
浅念 4db90b0e9e feat(sync): ov inbox ad 分支与 sender adId 推导(disabled/墓碑=删投影行)
- dto.ts SYNC_ENTITY_TYPES 追加 'ad'(对齐契约 SyncEntityType)。
- decideAndApply ③依赖检查豁免 ad:albumId 恒空串,不豁免则事件被永久
  暂存且无解锁路径(slot-state 同款空 albumId 危害)。
- project() 新增 ad 分支:全量快照直投 ads_ov;墓碑或 enabled=false 均
  deleteMany(disabled=公开不可见=无投影);albumIds 整体替换;createdAt
  取 cn 原值、updatedAt 为 ov 应用时刻(载荷无该字段)。
- sender.toWireEvent 新增 ad 分支:entityId 取载荷 adId(outbox 表无列),
  泛化 categoryId/assetId 兜底之前,缺料 fail-fast。
- e2e ads.e2e-spec:手工构造 outbox 行(绕过 CRUD,Task 7 才有)走真实
  sender→HTTP→inbox 链路,5 用例覆盖快照落位/disable 删行/复活 upsert/
  迟到旧版本不回滚/墓碑删行;reset-ov 增清 ads_ov。
- db-ov schema 附带注释修:ads_ov updatedAt 语义纠正(迁移头过度声明,
  迁移文件不动防 checksum 漂移)+ sync_inbox.entityType 枚举补 ad。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 23:18:37 +08:00
浅念 d86f452bed docs: 广告位设计 404 码 ALBUM_NOT_PUBLIC 纠正入档 + 实施计划 Task 1-4 勾选
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 23:03:32 +08:00
浅念 539597e9f0 feat(storage): 广告对象键布局(staging + 版本化终键)
adsStagingObjectKey(两步上传 presignPut 直传位)+ adObjectKey
(版本段=entityVersion:换图必然 bump 版本→新键,copyIfAbsent
禁止覆盖语义下天然防重复/防串图);index.ts 按名再导出供 Task 6/7
ads.service 消费。设计方案 §2.1/§3 终键布局同步回填(偏差登记)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 23:01:09 +08:00
浅念 318f69dae7 fix(db): photos 查重索引补 schema 声明——消除 migrate dev 误提议 DROP 漂移
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 22:49:43 +08:00
浅念 726f6a1be8 feat(db): ads 写权威表与 ads_ov 投影表(含迁移)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 22:43:25 +08:00
浅念 001d3aaf73 fix(contracts): 广告位 404 码对齐 NOT_FOUND/title 必填/空 PATCH 语义/上限漂移守护
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 22:29:45 +08:00
浅念 ec55d5718b feat(contracts): 广告位 openapi 端点/schema 与 AD_UPLOAD_MAX_BYTES,类型再生
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 22:16:12 +08:00
浅念 af8f3b78dd feat(contracts): ad.updated/ad.deleted 事件类型与 ad 实体映射
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 21:58:32 +08:00
浅念 ac37794d43 fix(app): path-precise duplicate matching; shared file stat; union test
Review follow-ups on 0fa23e0 (Task 2), applied as one commit:

- Path-precise dupPaths (#2, user-visible): DuplicateMatch gains
  sourcePath (local match fills it from the matched pick; unified
  server branch from picks[hit.index].path). Both pages' duplicate
  dialogs derive dupPaths from duplicates' sourcePath instead of
  basename matching, so same-basename siblings (different dir/bytes)
  are no longer mis-flagged — "跳过重复" no longer drops the
  non-duplicate sibling. Stale basename-derivation comment removed.
- Single shared stat (#3): files are stat'd once in
  checkDuplicatesUnified (_statPicks) and the picks feed both the
  local half (extracted _matchLocalHistory; public checkDuplicates
  folded — it had no remaining callers) and the server request.
  Halves stat IO for large batches (was ~2x).
- Union test (#1): new test/features/uploads/enqueue_test.dart pins
  that a file hit by BOTH local and server history yields ONE
  DuplicateMatch with the local uploadedAt (server's echoed time does
  not override). Merge dedup is now keyed by sourcePath
  (putIfAbsent, local first).

Tests: +1 enqueue union test (real-async chain inside one runAsync
window; sync file/dir setup — fake-async body must not await real
IO), +1 album_detail same-basename sibling test (skip-duplicates
enqueues exactly the non-hit sibling, distinguished by bytes since
sourceFilePath becomes the preparer's staging copy).

Gates: flutter analyze clean; full flutter test 429/429 green.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 20:22:37 +08:00
浅念 0fa23e015e feat(app): server-side duplicate pre-check at enqueue moment
Task 2 of docs/plans/2026-09-28-上传查重前置到入队前-实施计划.md:
surface server-side duplicate history at the "click upload" moment,
merged with the local check into the EXISTING dialog.

- ApiClient.checkUploadDuplicates: POST /v1/uploads/duplicates/check
  (commit 805f77b endpoint); 500/chunk sequential requests; chunk-local
  index translated to global; empty list short-circuits (no request);
  errors normalized to AppException via _send.
- enqueue.checkDuplicatesUnified: local TaskStore history ∪ server
  history; server hits map back to filename via global index into the
  stat'd picks; merged, de-duplicated (filename+bytes) list; server
  failure (network/permission/any) silently degrades to local-only —
  init 409 remains the backstop.
- capture_page + album_detail_page _pickFromGallery now call the
  unified entry; dialog/skip/force logic untouched.

Tests: api_client chunking (1200 → 500/500/200) + index translation +
merge + 404 normalization + empty no-request; page-level three states
(hit/no-hit/network failure) incl. fresh-install server-only-hit dialog
on both pages; pre-existing Task 8 dialog tests keep passing via the
degrade path.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 19:17:35 +08:00
浅念 64e1be09d3 test(api): pin earliest-uploadedAt + uploadEnabled-agnostic pre-check; fix comment
评审硬化三项(checkUploadDuplicates 后续,不 amend):
- e2e 补「同 bytes 多条可见命中取最早 uploadedAt」:两条同 bytes 不同
  uploadedAt 的种子行,断言回显更早值。
- e2e 补「uploadEnabled=false 预检仍 200 且照常命中」:钉住只读预览刻意
  不校验开关的偏差——防止未来一致性重构把检查加回来,破坏 App 关册时的
  降级体验(init 入队时仍有 409 兜底)。
- 修正 uploads.service checkDuplicates 的 null-uploadedAt 注释:原「谓词
  命中与否不受影响」不准确,改为「仅直改库等理论形状(bytes>0 且
  uploadedAt=null)下预检可能漏报而 init 仍会 409」。

uploads e2e 30/30 ✅ · api typecheck ✅

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 19:06:47 +08:00
浅念 805f77be76 feat(api): upload duplicate pre-check endpoint (checkUploadDuplicates)
「上传查重前置到入队前」Task 1:POST /v1/uploads/duplicates/check 只读批量
查重预检——用户点上传(确认入队)时刻预判服务端历史重复(跨设备/重装/改名
均可查到),不再等任务在上传中心以 PHOTO_DUPLICATE 失败才暴露。

- 契约纯增量:/uploads/duplicates/check + UploadDuplicateCheck(Item|Request|
  Duplicate|Response) schemas;regen openapi.d.ts。
- 判定谓词与 init ③.5 服务端查重逐字一致:同 albumId + uploaderId=当前用户
  + bytes 相等 + visibility != 'deleted'(filename 刻意不参与),保证「预检
  即真相」;命中条目按请求下标逐项返回(同 bytes 多请求条目各一条)。
- 权限链同 init(canView 404 防枚举 / canUpload 403 / 相册空值兜底 404),
  刻意不校验 uploadEnabled:只读预览,init 真正入队时仍按开关 409 兜底。
- 查询走 idx_photos_dedup:请求条目去重字节集一次 findMany,内存建
  bytes→最早 uploadedAt 映射后按请求下标回射。
- items 1..500(minItems/maxItems + 嵌套逐项校验,越界 400)。
- e2e 追加 describe 7 用例:命中含双条目同 bytes/未命中/uploaderId 隔离/
  deleted 不命中/非成员 404/items 边界 400/未认证 401。

契约 lint ✅ · contracts+api typecheck ✅ · api 单测 56/56 ✅ ·
uploads e2e 28/28 ✅

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 18:56:07 +08:00
浅念 7d744da56e docs: 广告位实施计划——评审修订(inbox 依赖暂存豁免 ad 关键补丁/常量指针纠正/组件测试补齐/compose 全路径)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 18:41:54 +08:00
浅念 4b5940c0d8 test(api): 修复 uploads e2e 与服务端查重(90dcc58)的字节数冲突
90dcc58 引入 init ③.5 服务端查重(同 albumId+uploaderId+bytes 非 deleted
→ 409 PHOTO_DUPLICATE)时未同步更新本 spec:renew/complete describe 内多个
init 沿用 bytes=1000,C-bytes complete 落定 1000 字节照片后,C-type/C-happy/
DEV-1/DEV-2 的 init 即被查重 409 拦截(该提交仅跑了 app 397 测试,api e2e
漏跑)。为各冲突 init 改用互异字节数(1002/1003/1004/1005),并同步
putStaging 与 bytes/expectedAt 断言;各用例语义不变。

本地库补apply 20260928120000_photos_dedup_index(idx_photos_dedup)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 18:38:36 +08:00
浅念 3b7d37110a docs: 广告位设计补轮播图左上角「广告」可见小标签(合规可识别性)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 18:25:49 +08:00
浅念 678225e8b5 docs(plan): upload duplicate pre-check at enqueue moment — incremental plan
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 18:25:24 +08:00
浅念 8453167b39 docs: 广告位设计补部署顺序硬约束(ov 先 cn 后,DTO IsIn 整批拒收已核实)、广告图 2MB 上限、量级假设登记
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 18:24:00 +08:00
浅念 ab412a3630 docs: 广告位设计补 enabled 翻转的 sync inbox 直测用例(评审建议)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 18:16:15 +08:00
浅念 0220a9cc05 docs: 广告位设计评审修订——createdAt 进投影/载荷、inbox disabled=删行、albumId 恒空串、head 校验补 contentType
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 18:14:00 +08:00
浅念 573a559e4a docs: 广告位轮播设计方案(全局+定向投放/两步上传/outbox 同步投影/sessionStorage 关闭)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 18:09:35 +08:00
浅念 46f9d93fd3 fix(app): guard force-column migration against downgrade-then-reupgrade
Review hardening for Task 8 (test-server workflow: testers install older
APKs over a newer DB):

- Old APK opening a v2 database hits drift's downgrade path, which leaves
  the force column in place but writes user_version back to 1. Reinstalling
  the new APK then re-runs onUpgrade(1,2) and addColumn crashes with
  "duplicate column name: force", failing the DB open.
- onUpgrade now checks PRAGMA table_info(upload_tasks) and only adds the
  force column when absent; the normal v1 -> v2 path is unchanged.
- Migration test simulates the downgrade-reupgrade cycle on a file-backed
  database (v2 open -> user_version forced back to 1 -> re-upgrade),
  asserting the open succeeds, legacy rows survive, and the column stays
  writable. RED before the guard (SqliteException: duplicate column name).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 17:27:52 +08:00
浅念 29f3bde9c2 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>
2026-09-28 17:06:19 +08:00
浅念 4f89c25bad fix(app): lock capture page portrait; gate zoom badge on 2 pointers; drop dead getter
- MainActivity locked to portrait: the capture page top bar / bottom
  controls and the preview aspect swap all assume portrait; landscape
  misaligns the preview ratio and control layers.
- Zoom badge now requires details.pointerCount >= 2 on onScaleStart so a
  single stationary tap no longer pops a 1.0x badge for 1s; base-zoom
  rebasing stays unconditional (single-finger-into-pinch still resumes
  from the current level).
- Removed unused CameraPluginSource.currentZoom getter.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 16:33:15 +08:00
浅念 ac602acf47 feat(app): camera preview WYSIWYG aspect + pinch zoom with double-tap reset
Preview now centers at the sensor aspect ratio (letterboxed on the dark
backdrop) instead of FittedBox cover, which cropped ~38% horizontally and
made the preview differ from the full-frame takePicture output.

Zoom capability hangs off CaptureSource as default implementations
(minZoom/maxZoom = 1, setZoom no-op) so existing fakes stay untouched;
CameraPluginSource probes getMin/MaxZoomLevel after initialize (failure
keeps 1/1 and disables the gesture). The page attaches a scale
GestureDetector only when maxZoom > 1: pinch drives setZoom (throttled at
0.05 delta, serialized to prevent out-of-order platform calls), a badge
shows the current level and hides after 1s, double-tap resets to 1.0x,
and camera flip/reopen resets zoom state. _StaticCaptureSource in
album_detail_page_test.dart switches implements -> extends to inherit the
new defaults.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 16:17:07 +08:00
浅念 65cd6849cc docs(plan): Task 9 checklist — GIF/truncated source cases for crop
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 16:02:57 +08:00
浅念 683e19bdc1 feat(app): crop cover & banner before upload (crop_your_image, 16:9 / 3:1)
review fixes: drop GIF from pre-sniff (package maps gif→png decode → never-ready hang); PopScope blocks back during processing

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 16:01:24 +08:00
浅念 97db504e25 docs(plan): fix manifest snippet (VISUAL_USER_SELECTED has no maxSdkVersion) + expand Task 9 checklist
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 15:04:42 +08:00
浅念 2e5fc40636 feat(app): in-app asset picker (wechat_assets_picker) - unlimited multi-select with range selection
Review fixes: per-asset originFile error isolation (failed asset skipped, not whole batch); drop maxSdkVersion from READ_MEDIA_VISUAL_USER_SELECTED (API 34+, no upper bound); originFile doc accuracy (API 30+ direct path vs Android 10 ScopedCache copy).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 15:03:44 +08:00
浅念 aea797c7c7 docs+test(app): correct okhttp version comments; harden throttle test assertions
- UploadPutExecutor.kt / build.gradle.kts:OkHttp 版本注释改为实情——声明 4.12.0,Gradle 冲突仲裁实际解析 5.2.1(fluwx 传递依赖胜出)
- UploadPutExecutorTest.kt:节流测试补 isNotEmpty 守护与 size>=2 断言(堵住过度节流只报终值的盲区)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 14:22:24 +08:00
浅念 45dbde3719 perf(app): throttle upload progress callbacks to 256KB steps
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 14:10:29 +08:00
浅念 203f62e7cb test(app): align mockwebserver with resolved okhttp 5.2.1
fluwx 6.0.2 传递 okhttp 5.x,冲突调解覆盖本模块 4.12 声明(debug
运行时同为 5.2.1);mockwebserver 4.12 在 okhttp 5.x 运行时缺
okhttp3.internal.Util → 单测 setUp 即 NoClassDefFoundError(本改动
前 main 上已复现)。对齐后 11/11 通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 14:10:14 +08:00
浅念 93e7046332 feat(app): enqueue gallery batch concurrently via Future.wait
Parallelize the enqueueForUpload collect loop (preparer's internal
bounded semaphore from the previous task keeps IO in check). Also two
test hardenings from that task's code review: pin that failed and
thrown prepare operations release their concurrency slot (cap=1,
subsequent prepare still completes), and pin the maxConcurrentPrepares
<1 -> 1 clamp (peak 1, no deadlock).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 13:53:42 +08:00