Commit Graph

285 Commits

Author SHA1 Message Date
浅念 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
浅念 b79edf447b feat(app): bounded-parallel prepare (default 3) replaces serial queue
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 13:43:07 +08:00
浅念 dc5ae4a24a feat(app): raise default upload parallelism 2 -> 4
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 13:34:48 +08:00
浅念 cc47278f50 docs: upload speedup plan v2 — 9 tasks after full re-review
- Task 7 camera WYSIWYG aspect + pinch zoom (two-pointer test, throttled setZoom)
- Task 8 server-dedup app integration (force passthrough, PHOTO_DUPLICATE
  classification, l10n) — closes the 409 dead-end from 90dcc58
- Task 6 crop: crop_your_image 2.0.0 callback API + Isolate.run pixel pipeline
- Stale claims fixed (album_detail dedup already wired in 638dd9b), pre-flight
  parallel-session check added

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 13:34:03 +08:00
浅念 441a24a54c docs: upload speed optimization plan draft
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 11:33:04 +08:00
浅念 90dcc58c33 feat(uploads): P0-1 batch 4 — server-side duplicate photo detection (409 + force + index)
契约 UploadInitRequest 加 force(bool,仅 owner/admin 跳过查重)。
服务端 init 事务前置查重:同 albumId + uploaderId + bytes 的非 deleted
照片已存在 → 409 PHOTO_DUPLICATE。photos 表加 idx_photos_dedup 索引
(迁移 20260928120000)。
App UploadInitRequest 加 force 字段 + toJson 序列化。
本地查重(层 1)+ 服务端查重(层 2)双层防线到位。

API typecheck ✅ · app analyze ✅ · 397/397 ✅ · contract lint ✅

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 11:29:29 +08:00
浅念 33f347cdea feat(app): P0 batch 3 — offline detection with engine pause/resume + UI banner
P0-4 离线感知:
  - connectivity_plus → NetworkAvailability(online/offline/unknown)
  - engine_scope 三重绑定:有账号+有网 → start;任一失去 → stop(在途不中断)
  - 网络恢复 → start + onNetworkRestored(waitingNetwork 即时重试)
  - HomePage 顶部离线黄条(wifi_off + i18n 双语)

P0-5 更正:EXIF 方向已由服务端 sharp .rotate() 自动转正——v2 分析误报

397/397 全绿、analyze 0

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 11:23:51 +08:00
浅念 638dd9b45d feat(app): P0 batch 2+2.5 — local upload dedup with batch dialog, album switch check, file size guard
P0-1 层 1 本地查重:
  - checkDuplicates:同账号+同相册+state=completed+filename+bytes 匹配
  - EnqueueResult 返回 enqueued+duplicates+skipped
  - capture_page + album_detail_page:批量单次弹窗(非逐张)三选一
    [跳过重复上传非重复] [全部上传] [取消]

P0-8 相册上传开关预检:album_detail_page 入队前检查 uploadEnabled,
  关闭时提示「该相册已关闭上传」+ 不入队

P1-8 文件大小预检:preparer 限单文件 ≤100MB(磁盘满由 on Object 兜底)

P0-7 更正:上传并发限制已有(kMaxParallelUploads=2),v2 分析误报

397/397 全绿、analyze 0

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 11:06:40 +08:00
浅念 9edcb3f2b1 feat(app): P0 batch 1 — photo viewer zoom, camera lifecycle, image memory management
P0-2 查看器缩放:InteractiveViewer(5x max,放大后平移,1x 时穿透到
  PageView 滑动切页)
P0-3 相机后台释放:WidgetsBindingObserver——paused 时 dispose(省电+
  隐私指示灯),resumed 时重开(沿用前后摄/闪光灯偏好)
P0-6 图片内存管理:PhotoThumbnail 加 maxCacheWidth 参数——网格/列表
  缩略图限制解码宽度(600/400px),查看器大图不限

397/397 全绿、analyze 0

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 10:35:14 +08:00
浅念 98e073d8c8 docs: App potential issues v2 — full analysis with server-side dedup (confirmed, ready to execute)
v2 补齐 v1 自查发现的 8 个漏洞:三重指纹(filename+bytes+CRC32)、批量弹窗
(单次确认非逐张)、photos 表 dedup 索引、force 权限约束(owner/admin)、
上传并发限制(3)、相册开关预检、上传进度百分比、磁盘空间预检。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 10:24:46 +08:00
浅念 984fe8981c docs(viewer): prod verification — share sheet copy-only (native share button removed)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 09:15:30 +08:00
浅念 a5d71686ff feat(viewer): remove native share button from share sheet (user request) — copy link only
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-28 09:08:39 +08:00
浅念 dc8d3ff967 feat(app): launcher icon — brand logo 1024px + adaptive (blue #1A73E8 background)
flutter_launcher_icons 自动生成全尺寸 mipmap(48-192px + 自适应前景/背景)。
正式签名生产环境 APK 已打好。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 17:07:14 +08:00
浅念 2ce529dd5b docs(viewer): prod verification — share sheet without link text
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 16:55:07 +08:00
浅念 d279cefd71 feat(viewer): hide link text under QR in share sheet (user request) — QR + copy button remain
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 16:51:36 +08:00
浅念 643f837bca feat(viewer): hide view count in album header (user request) — counting/reporting unaffected
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 16:24:05 +08:00
浅念 cf490c7892 feat(app): all album date displays date-only — formatAlbumDateTime drops HH:mm
相册列表卡/详情信息/分享页 meta/照片查看器日期一律纯日期(y/M/d)。
全量测试断言同步;397/397 全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 16:20:47 +08:00
浅念 07761b1b26 fix(app): camera preview distortion — swap sensor W/H for portrait
previewSize 报横向(W>H),竖屏直用会把画面拉扁。交换宽高转纵向比例再
FittedBox cover 铺满(裁切不变形)。397/397 全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 16:07:44 +08:00
浅念 c199d0630c feat(embed): photo click opens album page in a new tab (user decision 2026-09-24)
Reverts the in-host detail modal interaction: click now window.open's the album page (/a/:token?lang=, no /p/ deep link) with noopener — same target for middle-click/right-click via the real <a> href. The loader's open-photo modal code remains but is no longer triggered.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 15:25:41 +08:00
浅念 bd4db7b458 style(viewer): 相册 banner 限高 320px + cover 裁切(PC 端观感修正)
height:100% + contain 在桌面宽屏下整图 letterbox 压扁留白——改为限高裁切。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 15:00:12 +08:00
浅念 32eb4ceb8e docs: 观众链接统一境外源定稿(后台分享/嵌入/App二维码均 ov 域名)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 14:46:54 +08:00
浅念 76380477a0 docs: 验收记录补嵌入组件 EMBED_ORIGIN 修正(cn 后台嵌入代码改指 ov 域名)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 14:01:12 +08:00
浅念 e5302e3929 docs: 正式环境双地域部署计划与验收记录
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-24 13:41:47 +08:00