Files
photo-live/docs/plans/2026-09-18-P3-Flutter-App.md
T
2026-09-20 15:23:45 +08:00

106 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P3 Flutter App 实施计划
> **✅ 阶段已完成(2026-09-20,tag `p3`,基线 79eb975..e484aa5)。** 完成状态的权威记录是 [docs/plans/progress/P3-验收记录.md](./progress/P3-验收记录.md);Task 1-4/6/7/9/10 的部分步骤勾选框未逐项回填(12/12 任务全部交付),以验收记录为准。G2 真机关口与 D03 分发决策待用户执行/确认。
> **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:** 交付工作人员 Flutter App(Android 优先落地):账号密码登录、相册配置与协作(含分享二维码)、持久化上传队列(准备阶段状态机、账号隔离、连拍入队、跨设备服务端历史)、原生后台上传桥接(Android UIDT/前台服务;iOS 代码就绪待 G3 编译)、App 内语言切换——全部经真实 cn-api 验证(MuMu 模拟器)。
**Architecture:** apps/mobile 单一 Flutter 工程(Android/iOS 双平台配置);Dart 侧 = Riverpod 状态 + drift 本地任务库 + dio 客户端(复用 @photo/contracts?——contracts 是 TS,**不共享运行时**;App 内以手工镜像的 Dart 模型 + openapi 生成的对照文档开发,契约以 openapi.yaml 为准人工对齐,登记偏差);原生侧 = Kotlin WorkManager(UIDT) / Swift URLSession background 经 MethodChannel 桥接。后端零改动(P1 契约已齐)。
**Tech Stack:** Flutter 3.44.7 stable(本机 D:\dev\flutter)· Dart 3.13 · Riverpod · drift(SQLite) · dio · flutter_secure_storage · image_picker · camera · qr_flutter · integration_test + MuMu 模拟器(adb connect 127.0.0.1:16384 或运行时探测)
**对应验收(代码+模拟器级):** A04 A05(模拟器级——锁屏/厂商后台留 G2)A15 A19;A02/A01 联动回归。
**依赖基线:** tag `p2`(79eb975)。**环境前置(已验证 2026-09-18)**:`D:\dev\flutter`(3.44.7)、`D:\dev\android-sdk`(API 34-36/bt36/licenses 已受)、`D:\dev\jdk-21`、pub-cache 在 D:\dev\pub-cache、MuMu 模拟器已安装(D:\application\MuMuPlayer)。**env 约定**:所有 Flutter 命令前置 `source scripts/flutter-env.sh`(仓库根新增:PATH/JAVA_HOME/ANDROID_HOME/PUB_HOSTED_URL=https://pub.flutter-io.cn/FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn/PUB_CACHE=D:\dev\pub-cache)。
**iOS 边界(G3 关口)**:Swift 桥接代码与 Info.plist/backgroundModes 完整编写并提交,但编译/签名/真机验证依赖 macOS——出口时列 G2/G3 清单交用户。
---
## Task 1: 工程脚手架 + 环境脚本
- [ ] `scripts/flutter-env.sh`(上述 env);`apps/mobile` flutter create(org com.photolive,platforms android,ios——ios 目录生成但本机不编译);Android minSdk 24/targetSdk 36(UIDT 可用需 34+,文档注明降级路径=WorkManager+前台服务通知)
- [ ] 依赖接入(Riverpod/drift/dio/secure_storage/image_picker/camera/qr_flutter);l10n(flutter gen-l10n,zh/en arb);基础主题与路由(login→home(albums)→album detail→queue);`flutter analyze` 干净;Android debug 构建 + MuMu 安装冒烟(adb connect → flutter run -d <deviceId> 或 build apk + adb install)
- [ ] Commit `feat(mobile): flutter scaffold with env script and baseline nav`
## Task 2: API 客户端与会话
- [ ] dio 封装(base url 从 --dart-define/env dev=http://10.0.2.2:8080(模拟器宿主回环);Bearer 注入 flutter_secure_storage;401→清会话跳登录;错误码→l10n 文案映射,对照 errors.ts 14 码全量)
- [ ] Dart 模型层(Login/Album/Member/Category/UploadSession/PhotoRecord/AdminPhoto 等,字段对齐 openapi.yaml;lint 规则:模型注释标注契约 operationId)
- [ ] 单测(dio 拦截器/错误映射/模型解析 json fixture——真实 openapi examples 手工转 fixture)
- [ ] Commit `feat(mobile): api client, session storage and contract-aligned models`
## Task 3: 登录与修改密码
- [ ] 登录页(账号/密码、错误本地化含 AUTH_RATE_LIMITED、429 提示重试等待);会话持久(secure storage);退出登录(调 logout 尽力而为);修改密码页(**后端缺口**:契约无修改密码端点——P1 范围内"支持修改密码"是需求 4.1!**本任务顺带后端补**:契约+cn 实现 `POST /v1/auth/password-change`(Bearer+旧密码+新密码,旧密码错→AUTH_INVALID_CREDENTIALS;**成功后撤销其他会话、保留当前**+ e2e);**同一契约批次顺带补 sourceDeviceId**(§6.1:POST /uploads 请求可选 +sourceDeviceId(设备随机标识,App 首次生成持久),upload_sessions 落列,GET /uploads 条目返回——A19 设备来源语义);admin 无 UI(P4a)——契约变更走 gen 流程,登记为 P3 增补)
- [ ] widget 测试 + 模拟器手动冒烟
- [ ] Commit `feat(mobile): login, logout and password change (+backend endpoint)`
## Task 4: 相册列表/创建/编辑 + 二维码
- [ ] 相册列表(游标加载更多、开关状态标签);创建/编辑表单(zh-CN/en 双语 tab、时间选择、时区默认、开关、defaultLocale 校验);详情页(信息+分类管理+成员管理:账号前缀搜索邀请/移除)
- [ ] 分享:观众链接展示 + qr_flutter 二维码(viewer URL 构造:`{VIEWER_BASE}/a/{token}`——VIEWER_BASE dart-define,dev=http://10.0.2.2:5175)
- [ ] widget 测试;MuMu 手动冒烟(对着本地 cn-api + viewer 真链路)
- [ ] Commit `feat(mobile): album management pages with qr share`
## Task 5: 上传域模型与本地任务库(drift)
- [x] drift schema:upload_tasks(id uuid、accountId 隔离键、albumId、categoryId、clientTaskId、源文件路径(受管目录 copy)、filename/bytes/contentType、state 状态机 preparing/preparation_failed/queued/uploading/uploaded-confirming/completed/waiting_network/retryable_failed/cancelled、attempt/nextRetryAt、serverSessionId/photoId、createdAt/updatedAt、账号切换过滤=accountId 索引)
- [x] 状态机单测(合法迁移表驱动;账号隔离查询;任务保留策略=completed 仅可清理标记)
- [ ] Commit `feat(mobile): drift upload task store with state machine`
## Task 6: 上传引擎(Dart 侧)
- [ ] 准备阶段:image_picker/camera 选图/拍摄 → 复制到受管目录(app 支持目录/uploads/{albumId}/)→ 空间检查(磁盘阈值)→ 入队 queued;准备失败独立状态(preparation_failed,可重试);连拍模式:camera 连续拍摄每张自动 preparing→queued,目标相册+分类固定显示于拍摄页
- [ ] 传输:出队 → POST /uploads(幂等 clientTaskId,含 sourceDeviceId)→ presign PUT(dio,进度回调)→ complete → completed;失败分类(网络→waiting_network 指数退避;409 冲突→terminal+提示;凭证过期→renew 重试);取消(未开始直接删;进行中标记取消尽力);**源文件清理执行器**(评审缺口 1:completed 任务源文件按 KEEP_COMPLETED_DAYS(默认 14)自动清理,执行器随队列引擎运行;硬约束=仅清理 completed/cancelled,未完成源文件永不清理——单测覆盖该守护)
- [ ] 服务端历史页(GET /uploads:跨设备记录,显示 state/photo.processingState,**明确标注"本机队列不在此列"**——A19 语义)
- [ ] 引擎单测(fake api:幂等重试/断网恢复/取消/账号隔离/连拍归属不变 A15)
- [ ] Commit `feat(mobile): upload engine with preparation, batching states and retries`
## Task 7: Android 原生后台上传桥接
- [ ] Kotlin 侧:MethodChannel `photolive/uploads`——入队(registerTask: taskId, presignUrl, headers, file, metadata)、查询/取消;实现=WorkManager OneTimeWorkRequest(targetSdk>=34 走 setExpedited+数据传输 foregroundServiceType=dataSync;通知渠道+进度通知);任务映射表(taskId→workId 持久 WorkManager 自持);完成/失败回调 EventChannel→Dart 校准队列状态
- [ ] Dart 侧适配:engine 的传输步骤在 Android 上委托原生(可选开关 dart-define FORCE_DART_TRANSPORT 用于调试);锁屏/切后台行为=系统 WorkManager 语义(模拟器可验:切后台继续)
- [ ] Android 单测(WorkManager test)+ 模拟器验证:上传中切后台/息屏(模拟器电源按钮)→ 队列继续完成
- [ ] Commit `feat(mobile): android background upload bridge via workmanager`
## Task 8: iOS 桥接代码(编译留 G3)
- [x] Swift:URLSession background(withIdentifier:) + AppDelegate wiring + MethodChannel 同协议;Info.plist backgroundModes(fetch/processing 按文档)、上传任务映射持久化(plist/sqlite 简版);README-ios.md(Xcode 步骤、签名、验证清单=技术方案 6.2 要点)——实现偏差:UIBackgroundModes 有意不声明(background URLSession 无需,见 README-ios.md 与 Info.plist 注释)
- [x] 静态保证:dart 侧协议层与平台无关(同一 interface),iOS 分支代码完整、`flutter analyze` 通过(iOS 不编译不阻塞)
- [ ] Commit `feat(mobile): ios background upload bridge (compile-gated at G3)`
## Task 9: 队列 UI 与连拍页
- [ ] 队列页:按相册/状态/**时间**(需求 4.3 三维筛选)筛选、缩略图、进度条、错误原因(本地化)、重试/批量重试/取消;拍摄页:常驻相册名+分类+待传数+失败提示(R17/O01)
- [ ] widget 测试
- [ ] Commit `feat(mobile): upload queue ui and continuous capture page`
## Task 10: App 内语言切换
- [ ] 设置项 zh/en/跟随系统;持久;立即生效(Riverpod locale provider);全部页面文案走 l10n(漏译检查脚本——arb 完整性单测)
- [ ] Commit `feat(mobile): in-app locale switching`
## Task 11: 集成测试(MuMu + 真实后端)
- [x] integration_test 套件(对本地 cn-api + MinIO):登录→建相册(zh+en)→邀成员→传 3 图(含连拍 2 张)→队列全 completed→服务端历史可见→(flushSync+ov)viewer 网页侧可见 3 张→隐藏一张→App 侧管理列表状态同步→修改密码→重登录。断网场景:dio mock 拦截(模拟器断真实网不便)驱动 waiting_network→恢复重试
- [x] 后台延续场景:Android 上传中切后台 30s+ → 完成(原生桥接路径);**上传中杀进程重启**→队列状态完整恢复续传(A05 模拟器级;需持久化 in-flight 状态随任务库)
- [x] Commit `test(mobile): integration suite against real backend on mumu`
## Task 12: 回归门 + P3 验收记录
- [x] 全仓回归绿(web 侧不降级);flutter analyze/test 零警告;debug apk 构建产物留存路径登记
- [x] `docs/plans/progress/P3-验收记录.md`(A04/A05 模拟器级、A15/A19、契约增补 password-change + sourceDeviceId 登记、G2 真机清单(锁屏15min/厂商后台/断网/凭证过期/强制退出/**源文件缺失/存储不足/批量100张真实样本**/HEIC 真机样本——技术方案 6.4 全量)、G3 iOS 编译清单、偏差表(Dart 模型手工对齐契约、模拟器网络回环 10.0.2.2、断网用 mock 拦截、**Android 桥接采用 expedited+dataSync 前台服务实现(§6.3 UIDT 主路径待 G2 真机评估后定)**、源文件保留策略默认 14 天))
- [x] 总纲 P3 行进度注记(代码+模拟器级完成;G2 强制关口待用户真机执行);`git tag p3`;Commit `docs: p3 acceptance record`
---
## 执行注意
- Flutter 命令一律先 `source scripts/flutter-env.sh`(子代理 dispatch 时在 prompt 里显式给出 export 串)。
- 模拟器:MuMu 启动后 `adb connect 127.0.0.1:16384`(失败则探测 5555/7555/21503,MuMu 12 常用 16384);`adb devices` 确认后再 flutter run/install。
- 后端联动默认全本地(compose 套件 + dev:api + flushSync);不得触碰生产资源。
- 契约唯一增补=password-change(Task 3),其余一律复用既有端点;模型与 openapi.yaml 字段分歧=偏差登记。
- 每任务 TDD;提交带 Co-Authored-By trailer;apk 产物不入库(gitignore apps/mobile/build)。