Commit Graph

22 Commits

Author SHA1 Message Date
浅念 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
浅念 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
浅念 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
浅念 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
浅念 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
浅念 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
浅念 1f67b7c818 feat(mobile): UI-01 upload center, capture and auxiliary pages
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-21 17:20:43 +08:00
浅念 f6929ae18f feat(mobile): UI-01 album cards, photo grid and full viewer
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-21 15:55:44 +08:00
浅念 c94b9e5e58 feat(mobile): UI-01 theme system, shared components and navigation
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-21 14:55:29 +08:00
浅念 8f4ad11bd5 feat(contracts,api): UI-01 cover, preview, scope=own and embed config extensions
契约扩展(全部可选/可空,旧客户端零感知)+ cn/ov 实现 + e2e 权限矩阵:
- AlbumSummary += cover(kind=cover state=effective 最新行的短期签名 URL,
  列表/详情按页批量组装非 N+1;未配置为 null)
- AdminPhoto += preview{thumb,browse}(变体齐备且未删除才签发;隐藏仅
  admin/owner;协作者仅自己上传,scope=own 服务端过滤 uploaderId,
  成员资格逐请求裁决)
- listAlbumPhotos += scope=all|own(缺省 all 行为不变:仅 owner/admin,
  协作者 403;own 任何有效成员)
- EmbedSlotSettings += imageFit/offsetX/offsetBottom/zIndex(cn PATCH 校验
  → slot-state 投影 → ov 公开回读;settings 为 Json 列免迁移,读侧
  normalizeStoredSettings 兜旧形状默认值)
- Flutter 模型同步(SignedUrl/PhotoPreview/AlbumPhotoScope,可空容错)

e2e:ui01-contracts 9 例(封面选取/字节取回/N+1 计数断言、preview 权限
矩阵、scope 过滤与 403/404/400)+ embed ⑨ 配置链路;全量 e2e 257 通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-21 14:09:35 +08:00
浅念 955f9b13c1 feat(mobile): upload queue ui and continuous capture page
Task 9 (P3 plan): queue tab replaces the stub, continuous capture page
enqueues per shot, album detail gains an upload FAB, and the upload
engine lifecycle is now bound to the login session.

- Engine scope: session account drives UploadEngine start/stop; wired
  from app boot, login success, logout, and 401 eviction. Tasks stay in
  the store across logout (accountId hard isolation).
- Queue tab: album x state x time filter bar, task rows with thumbnail,
  state chip, localized error copy, attempt count, per-row progress via
  a broadcast progress hub, state-dependent actions (re-pick retry,
  retryNow, cancel, delete), batch retry bar, 200ms-debounced live
  refresh from drift table updates.
- Capture page: camera preview via injected CaptureSource, one enqueue
  per shutter press, persistent album/category/badges overlay with a
  switch-only-affects-new-shots hint, gallery multi-pick, exit confirm
  while uploads are pending, permission-denied and init-failure views.
- POST_NOTIFICATIONS runtime permission via a small MethodChannel
  (photolive/permissions) + PermissionPlugin.kt: single runtime
  permission need, avoids permission_handler dependency; requested once
  on first enqueue, silent degradation when denied or unavailable.
- Album detail FAB: capture-and-upload / pick-from-gallery sheet.
- l10n: ~50 new keys in zh/en arbs; tests: +45 (filter matrix, engine
  scope lifecycle, permission gate, queue tab, capture page, FAB entry);
  existing page tests updated to inject session/task-store fakes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 04:47:31 +08:00
浅念 42e1c2e34b feat(mobile): ios background upload bridge (compile-gated at G3)
Swift 侧(协议与 Android UploadPlugin.kt 逐字一致,编译/真机留 G3):
- UploadBridge.swift:通道/方法/事件/失败码常量 + registerTask 参数解析
  (镜像 UploadProtocol.kt)
- UploadEventBus.swift:单调 seq + 终态 ack 缓存重放(镜像
  UploadEventBus.kt),iOS 增强——未 ack 终态落盘(系统后台拉起投递的
  回调在进程重启后同样重放,技术方案 §6.2 持久化原生回调)
- UploadTaskStore.swift:taskId→URLSession taskIdentifier 映射 plist 持久化
  (Application Support;对应 Android WorkManager 自持映射)
- UploadPlugin.swift:URLSession background
  (com.photolive.photo_live.uploads)+ delegate 失败分类(2xx→completed+etag
  / 403→needsRenew / 4xx→HTTP_4XX / 网络→重试上限后 NETWORK,镜像
  UploadPutExecutor/UploadWorker)、原生重试(5 次上限、10s 指数退避)、
  进度 500ms 节流、queryTask/cancelTask/ackEvents、静默取消
- AppDelegate.swift:implicit engine 通道注册 + 启动重建会话 +
  handleEventsForBackgroundURLSession completionHandler 暂存
- Info.plist:UIBackgroundModes 有意不声明(background URLSession 无需;
  计划措辞按技术方案 §6.2 修正,见 README-ios.md)
- project.pbxproj 登记新 Swift 文件;README-ios.md(Xcode 步骤 + G3 验证
  清单:实际文件上传/持久化回调窗口/服务端补偿/重启重联校准/强制退出/
  低电量延迟/100 张批量)

Dart:NativeTransport 选择矩阵并入 iOS(Platform.isAndroid || isIOS,
FORCE_DART_TRANSPORT 覆盖不变)——协议两侧一致,无新通道代码。

flutter analyze 0 issue;flutter test 219 全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 03:42:49 +08:00
浅念 4e35bdc0b2 feat(mobile): android background upload bridge via workmanager
Kotlin 侧(MethodChannel photolive/uploads + EventChannel photolive/uploads/events):
- UploadPlugin:registerTask 入队 WorkManager OneTimeWorkRequest(unique
  work=upload-task-{taskId},expedited + OutOfQuotaPolicy 回退、CONNECTED
  约束、指数退避 10s 起)、cancelTask / queryTask / ackEvents
- UploadWorker(CoroutineWorker):dataSync 前台服务 + 进度通知,
  OkHttp PUT(ETag 捕获、64KB 分片进度),结局分类:2xx→completed、
  403→needsRenew、4xx→failed{HTTP_4XX}、网络/5xx→Result.retry(上限
  5 次后 failed{NETWORK|SERVER_ERROR})、缺文件→failed{SOURCE_MISSING}
- UploadEventBus:终态事件单调 seq + 未 ack 缓存重放(上限 200),
  重注册/取消作废旧缓存——进程重启/切后台恢复不丢完成信号
- Manifest:FOREGROUND_SERVICE(_DATA_SYNC)/POST_NOTIFICATIONS 权限 +
  SystemForegroundService tools:node=merge 补 dataSync 类型(已验证
  合并产物)

Dart 侧:Transport 接口(init/complete/renew 留引擎,仅 PUT 委托)——
DartHttpTransport(Task 6 逻辑抽出,403→renew 异常归一)+ NativeTransport
(registerTask→事件流终态/进度映射、终态 ack、取消联动);引擎传输选择:
Android 缺省原生,dart-define FORCE_DART_TRANSPORT=true 强制 Dart 路径;
失败分类与 dio 路径语义对齐(401 短退避/429 网络桶/SOURCE_MISSING 不
自动重试等)

测试:gradle testDebugUnitTest 29 例(executor MockWebServer 分类×10、
协议解析×11、事件总线×8);flutter test 219 全绿(新增 NativeTransport
事件映射/取消/ack/并发路由、引擎×Transport 委托与 needsRenew 重注册、
传输选择矩阵);flutter analyze 0;debug apk 构建通过

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 03:22:42 +08:00
浅念 3ddb0ea2d4 feat(mobile): upload engine with preparation, batching states and retries
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 02:50:40 +08:00
浅念 d96e56f653 feat(mobile): drift upload task store with state machine
- upload_tasks 表(accountId 隔离 + 三个复合索引)、状态列枚举转换器、
  datetime 文本落库(UTC 归一,亚秒精度)
- 纯状态机(task_state.dart):合法迁移表 + canTransition/assertTransition,
  completed/cancelled 终态
- TaskStore 仓储:create(clientTaskId 同账号同相册应用侧唯一)、
  updateState(迁移守护+旁路字段)、账号隔离读路径(硬守护)、
  countsByState 徽章、cleanupCandidates/markCleanable(仅终态可清理——
  评审缺口 1 硬守护,未完成源文件永不清理)
- 测试 26 例(状态机表驱动全边 + 守护/隔离/筛选/保留策略);
  sqlite3 3.x build hooks 在 flutter test 宿主与 Android APK 构建均验证
  (libsqlite3.so 已打包全 ABI,运行时验证留 Task 11)

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 02:12:39 +08:00
浅念 bbea1dd364 feat(mobile): album management pages with qr share
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 01:56:38 +08:00
浅念 f975321b67 feat(mobile): api client, session storage and contract-aligned models
P3 计划 Task 2(apps/mobile):

- core/config.dart:API_BASE/VIEWER_BASE 经 --dart-define 覆盖,缺省
  10.0.2.2 模拟器宿主回环;viewer 相册链接构造 {VIEWER_BASE}/a/{token}。
- core/error_codes.dart:errors.ts 17 码 1:1 镜像(含注释),另设 4 个
  客户端本地伪码(NETWORK_ERROR/NETWORK_TIMEOUT/CANCELLED/HTTP_ERROR)。
- core/api_client.dart:dio 封装——Bearer 注入(登录端点豁免)、401
  驱逐(清会话 + onUnauthorized 回调去重,App 壳注册跳 /login)、
  DioException/错误体 {code,message,requestId} 归一 AppException、
  通用游标分页助手;auth/albums/members/categories/admin/uploads/photos
  全端点方法;riverpod provider 出口。
- core/session_store.dart:SessionStore 抽象 + flutter_secure_storage
  实现(token/expiresAt/account 三键,encode/decode 纯函数可测)。
- features/{auth,albums,uploads,photos}/models.dart:手写 fromJson/
  toJson,逐字段对齐 openapi.yaml(类注释标注契约 schema/operationId);
  locale 按字符串透传(zh-CN 标识符不兼容),业务枚举 Dart 化并在
  未知值 fail-fast。
- 测试 66 项:17 码表驱动 + 网络/超时/取消 + 兜底映射、游标边界
  (含 CursorPageMeta 不变式违约容错)、模型往返(fixture 转写自
  openapi.yaml schema 与 api e2e 断言,来源在测试头注释)、拦截器
  行为(手写 FakeAdapter:Bearer 注入/401 驱逐/登录豁免/并发去重/
  尽力而为 logout/refresh 滑动续期)。flutter analyze 零告警。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 00:40:02 +08:00