Files
photo-live/docs/plans/progress/P3-验收记录.md
T
浅念 e484aa5427 docs: p3 acceptance record
P3 收口(Task 12):回归门全绿实测数字、MuMu 设备集成证据(阶段 1
十二腿 + 阶段 2 双场景,引用 it-logs 存档原文)、29+3 轮排障 13 根因
展开表、契约增补(password-change + sourceDeviceId)登记、G2 真机
清单(计划全量 + 集成期新增 5 项)、G3 引用 README-ios、偏差表(计划
内 13 + 计划外 5);总纲 P3 行进度注记(G2 待用户真机执行、D03 待确
认);P3 计划 Task 11/12 勾选。

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

22 KiB
Raw Blame History

P3 验收记录

  • 阶段:P3 Flutter App(计划 docs/plans/2026-09-18-P3-Flutter-App.md,Task 1–12)
  • 日期:2026-09-20(实施 2026-09-18/19,设备集成收敛 2026-09-20)
  • 基线 commits:79eb975..HEAD(tag p2 → d41be36,共 13 个提交 = 1 计划评审 + 12 实施,不含本记录提交)
  • 执行模式:子代理驱动 + 双审(每任务先写失败测试→实现→通过→提交;Task 11 集成排障由主代理在真机(MuMu)上 29 轮阶段 1 + 3 轮阶段 2 迭代收敛)
  • 回归门:web 侧 pnpm lint && pnpm typecheck && pnpm test && pnpm build && pnpm e2e 全绿(lint 9/9、typecheck 13/13、build 9/9 任务;数字见文末统计表);mobile 侧 flutter analyze 0 issue + flutter test 280/280 + :app:testDebugUnitTest 29/29;设备集成套件不重跑(2026-09-20 已全绿,证据=存档日志,见下)
  • 基础设施:apps/mobile Flutter 3.44.7 单工程(Android 优先落地;iOS 目录完整、编译留 G3);编排脚本 scripts/run-mobile-it.sh(MuMu adb 127.0.0.1:16384 / Android 15 / adb reverse tcp:9000 / 10.0.2.2 回环 / compose --profile ov / 四个 dev 服务 / 时间戳专属账号数据隔离);debug APK 产物路径 apps/mobile/build/app/outputs/flutter-apk/app-debug.apk(不入库)

任务清单与提交

Task 内容 Commit
1 工程脚手架 + 环境脚本(flutter-env.sh、minSdk 24/targetSdk 36、l10n、基线路由) 8bc50e0
2 API 客户端与会话(dio 封装、401 驱逐、错误码全量映射、契约对齐模型) f975321
3 登录与修改密码(含后端契约增补批次 password-change + sourceDeviceId) 31ba03d(后端)+ 8be21b8(App 页面)
4 相册列表/创建/编辑 + 分类/成员管理 + 分享二维码 bbea1dd
5 drift 上传任务库(状态机 9 态、accountId 隔离、保留策略) d96e56f
6 上传引擎(准备阶段、批量传输、失败分类、退避重试、源文件清理守护) 3ddb0ea
7 Android 原生后台桥接(WorkManager expedited + dataSync 前台服务) 4e35bdc
8 iOS 桥接代码(URLSession background,编译/签名留 G3) 42e1c2e
9 队列 UI(相册/状态/时间三维筛选)+ 连拍拍摄页 955f9b1
10 App 内语言切换(zh/en/跟随系统,provider 立即生效,arb 完整性门) 8f3966d
11 集成测试(MuMu + 真实后端全栈,阶段 1 主流程 + 阶段 2 后台/杀进程) fb3c278 + d41be36(29+3 轮设备排障收敛)
12 回归门 + 本记录 本提交

集成测试证据(关键章节)

日志存档于 apps/mobile/build/it-logs/(gitignore,不入库):

  • 阶段 1 通过日志:phase1-20260920-135106.log
  • 阶段 2 通过日志:phase2-console.log(运行戳 20260920-145310;分日志 bg-home-prepare/verify-*、kill-restart-prepare/verify-*)

工具链

项 值
设备 MuMu 模拟器,adb 127.0.0.1:16384,Android 15(ro.build.version.release=15 实测)
App→宿主网络 10.0.2.2 回环(API_BASE=http://10.0.2.2:8080、OV_BASE=:8081、VIEWER_BASE=:5175,dart-define 注入)
MinIO 上传网络 adb reverse tcp:9000 tcp:9000——presign URL 内嵌服务端视角 localhost:9000,模拟器的 localhost 是自己,直连必挂;反向隧道后 URL 与 SigV4 host 签名零改动
后端 infra/docker-compose.yml --profile ov(mysql/redis/minio 双地域)+ dev:api(8080)/dev:ov(8081)/dev:worker/dev:sync 全真实栈(真实 presign PUT、真实发布、真实跨境同步)
数据隔离 集成测试不做 DB reset(dev 库共享):每次运行 seed:it 幂等预置 itmain_/itstaff_/itbg1_/itbg2_ + 时间戳专属账号,相册标题带同一时间戳后缀
素材 测试图 adb push → run-as 拷入 App files 目录(首选)/ /data/local/tmp(回退,卸装存活);大图 2 张(~14-15MB)驱动阶段 2

阶段 1:主流程十二腿(p3_flow_test.dart,1 spec 串行全流程)

日志原文逐腿引用(phase1-20260920-135106.log):

腿 验证内容 日志原文
1 专属账号登录 [IT] leg1 登录 OK:itmain_20260920-135106
2 建相册(zh+en 双语标题 + Material 日期/时间选择器) [IT] leg2 建相册 OK:集成测试相册 20260920-135106([IT] albumId=cmu9eh8oc00fwwxy3mlhf7bmk publicToken=tw3knygd9pkmoanp4froftid)
3 相册详情 + 建分类 [IT] leg3 分类 OK
4 邀请成员(staff 账号脚本预建) [IT] leg4 邀请 OK:itstaff_20260920-135106
5 上传 3 图(选图替身 → 真实准备/队列/引擎/原生 WorkManager 桥接/真实 presign PUT/complete) [IT] leg5 队列 3 张全部 completed
6 服务端上传历史(跨设备记录 + 本设备标记,A19) [IT] leg6 服务端历史 OK:3 条本设备记录
7 ov 公开相册可见 3 张(真实 worker 发布 + 跨境同步管线) [IT] leg7 ov 公开相册 OK:3 张
8 隐藏一张 → App 管理列表「已隐藏/已发布」状态同步 [IT] leg8 隐藏同步 OK
9 断网(DIO 故障注入)→ waiting_network(UI+store 双断言)→ 恢复重试 [IT] leg9 故障闸门关闭(模拟断网) / [IT] leg9 waitingNetwork 断言 OK(UI+store) / [IT] leg9 网络恢复重试 OK:4 张全部 completed
10 修改密码(服务端保留当前会话 → refresh 仍成功) [IT] leg10 修改密码 OK(会话保留,refresh 成功)
11 退出登录 → 新密码重登录 [IT] leg11 新密码重登录 OK
12 App 内语言切换 zh→en→zh(provider→MaterialApp 全局重构链路) [IT] leg12 语言切换 en OK / [IT] leg12 语言切回 zh OK —— 全流程完成

结果行:00:36 +1: All tests passed!

阶段 2:后台延续 + 杀进程恢复(p3_background_test.dart,prepare/verify 两段式 + 脚本 adb 设备动作)

App 无法自己 HOME/杀自己——prepare 阶段打 MARKER:UPLOADING 后由脚本执行设备动作(计划明示的 adb 编排场景)。

场景 1 bg-home(A04 后台延续,模拟器级):

  • [run-mobile-it 20260920-145310] bg-home:HOME 键切后台(上传仍进行中)
  • [run-mobile-it 20260920-145310] bg-home:服务端旁证 OK——后台(WorkManager 路径)2 张 confirmed(脚本以 curl 登录真实账号轮询 GET /v1/uploads 服务端权威确认,非 UI 自证;2 张 ~14-15MB 大图)
  • [IT] verify:服务端上传历史 2 条 confirmed OK + 00:02 +1: All tests passed!(verify = 全新进程登录→队列收口→服务端历史)

场景 2 kill-restart(A05 杀进程恢复,模拟器级):

  • [run-mobile-it 20260920-145310] kill-restart:force-stop 杀进程(上传中)
  • [IT] verify:服务端上传历史 2 条 confirmed OK + 00:02 +1: All tests passed!(新进程冷启动后 in-flight 任务经 drift 持久化归位续传完成)
  • 收口行:[run-mobile-it 20260920-145310] 阶段 2 全部通过 ✔(A04 后台延续 + A05 杀进程恢复,模拟器级)

A04 / A05(模拟器级)

  • A04 后台延续:HOME 切后台后传输由原生 WorkManager 承接(expedited + dataSync 前台服务),服务端旁证 2 张 confirmed;锁屏、厂商后台管理留 G2 真机(计划明示边界)。
  • A05 杀进程恢复:上传中 am force-stop → 新进程登录 → 任务库恢复 in-flight 状态(uploading/uploadedConfirming 归位重试,单测另有「卡在 uploading 的任务 drain 前归位并完成」固化)→ 服务端 2 条 confirmed(clientTaskId 幂等下无重复记录)。

A15 / A19(代码级 + 集成佐证)

  • A15 连拍归属不变:
    • 引擎不可变测试:test/features/uploads/upload_engine_test.dart 「A15:连拍中途切换相册/分类不影响既有任务(引擎只读行内目标)」——目标相册/分类在入队时落任务行,引擎只读行内值,全局切换不影响在途任务。
    • 拍摄页语义:test/pages/capture_page_test.dart 「分类切换:hint 提示 + 仅影响之后拍摄(A15 归属不变)」+「连拍两张串行入队(不逐张确认)」+「常驻信息层:相册名 + 分类候选」。
  • A19 跨设备服务端历史:
    • 页面测试:test/pages/server_history_page_test.dart 「A19 横幅 + 行内容(短码/KB/状态标签/设备短码/本设备标记)」(横幅明示「本机队列不在此列」语义)+「游标加载更多」。
    • 设备标识:test/core/device_id_test.dart 8 例(首生成 32 位 hex、跨实例稳定、并发单次落盘、异安装互异、非法值再生)。
    • 集成佐证:leg6 3 条本设备记录(sourceDeviceId 随 init 上送、GET /uploads 回显比对)。

A01 / A02 联动(App 端)

  • A01 认证与会话:App 端会话链全程真实——登录(leg1)、secure storage 持久、改密后当前会话保留(leg10 refresh 成功)、新密码重登录(leg11)、改密撤销其他会话(服务端 e2e,见契约增补节)。单测:api_client_test.dart(Bearer 注入/业务 401 清会话+通知一次/并发 401 去重/登录端点 401 不驱逐/refresh 令牌保留/changePassword 旧密码错不驱逐)、session_store_test.dart(全字段往返/令牌缺失=null/过期时间不可解析容错)。
  • A02 权限逐次校验:App 无本地权限缓存——无会话裸发由服务端 401 裁决(api_client_test.dart「无会话时不注入 Authorization」),每个请求携带 Bearer 由服务端逐次校验;集成全程走真实权限链(邀请 staff、owner 上传、隐藏经真实契约端点),P1a 的 permissions/full-chain e2e 仍是权威证据。

排障实录(29 + 3 轮设备调试——本阶段最有价值资产)

阶段 1 共 29 轮、阶段 2 共 3 轮迭代定位,13 个根因全部修复后全绿(对应 d41be36 提交信息,展开成表):

# 现象 根因 修复
1 adb shell mkdir 报 'C:'/'Files' 只读、adb push 无限悬挂 Git Bash/MSYS 把传给 adb.exe 的安卓设备绝对路径(/data/...)改写为 C:\Program Files\Git\data\... 再被 Android shell 拆词 按命令 MSYS_NO_PATHCONV=1 仅对含设备路径的 adb 调用禁用转化;宿主侧路径经 winpath()(cygpath)显式转 Windows 形式(run-mobile-it.sh 头部)
2 上传卡 uploading 永久挂起 presign URL 内嵌服务端视角的 MinIO 地址 localhost:9000——模拟器的 localhost 是自己,永远连不上 adb reverse tcp:9000 tcp:9000 反向隧道;URL 与 SigV4 host 签名零改动
3 ov-api 一片 500、sync 推送连败 compose 启动未带 --profile ov,Docker Desktop 重启后 ov 侧容器不会自动回来 脚本统一 docker compose --profile ov up -d(曾排查整轮才定位)
4 对话框「保存」点击落空、对话框滞留、后续腿全断 软键盘遮挡对话框按钮 dismissKeyboard + ensureVisible 后再点击(ui_steps.dart)
5 goBack 点中被压在下面的页面返回键,腿 9 莫名退栈 栈叠路由多页面同名 BackButton,.first 命中底层页面 goBack 取栈顶 hit-testable 的返回键
6 底部弹层选项两次竞态失败 弹层入场动画期间选项 hitTestable=false 等待选项 hit-testable 再操作
7 语言切换弹层点击不稳定 弹层选项与顶栏切换器同文案,finder 命中不稳 改驱动真实 LocaleSettings provider(弹层交互已由 Task 10 widget 测试覆盖;本腿验证 provider→MaterialApp 全局链路)
8 改密提交点击偶发失效 / ensureVisible 抛 No element 「修改密码」按钮与 AppBar 标题同文案 + 提交中 spinner 瞬时替换文本致 finder 为空 descendant(of: FilledButton) 限定 + 重试点击直至成功提示出现
9 改密成功后再 goBack 多退一页 改密页成功后自行 pop(Task 3 语义),额外返回会退出设置页 去掉多余 goBack
10 bg verify 断言设备侧队列状态失败 verify 是全新进程,测试装配的任务库为内存实例(跨进程本就不共享) verify 改服务端权威断言(GET /uploads confirmed 计数);设备侧 drift 持久化由 prepare 阶段真实落库验证
11 重跑后测试图全部「源文件缺失」 flutter test 每次重装 APK 清掉 App 数据(files 目录素材全没) 图片目录逐候选容错后回退 /data/local/tmp(adb push 直读、全员可读、卸装存活,app_boot.dart)
12 run-as cp 失败无感知,后续腿才炸 失败被静默吞掉 cp 失败即 die——测试图为必需件,不再吞错
13 阶段 2 启动即配置失败 ItConfig.assertValid 要求全量 dart-define,bg 剧本不用 staff 但缺 IT_STAFF_ACCOUNT 脚本补 IT_STAFF_ACCOUNT=bg-unused-* 占位(run-mobile-it.sh bg_scenario)

契约增补登记(P3 唯一增补批次,31ba03d)

增补 契约 语义 e2e
修改密码 POST /v1/auth/password-change(operationId changePassword) Bearer + {oldPassword 1-128, newPassword 8-128} → 204;旧密码错 401 AUTH_INVALID_CREDENTIALS;成功后事务内更新 argon2 哈希、撤销该用户除当前会话外全部其他会话(本人不下线)、审计 auth.password_change(需求 4.1 的 P1 契约缺口补齐) auth +4 例(未认证 401 / 新密码过短 400 / 旧密码错且会话无副作用 / 成功后他端撤销当前保留 + 新旧密码裁决 + 审计)
设备来源 sourceDeviceId(A19) UploadInitRequest 可选 8-64 位设备标识(App 首次生成持久);upload_sessions 落列(VARCHAR(64),可空,存量行 NULL 兼容);GET /uploads 条目回显;幂等重放不比对(first-writer,首次落库值不随重放漂移) uploads +1 例(携带落库回显、换设备重放保留首值、缺省 null、长度越界 400)

api e2e 179 → 184;openapi.yaml 单一事实来源 + gen + redocly 门全绿;admin 无 UI(P4a)。


G2 真机验证清单(强制关口,待用户执行)

计划 Task 12 全量(对齐技术方案 §6.4)+ 集成期新增发现:

计划内(技术方案 6.4 全量)

  • 锁屏 ≥15min(上传中息屏,恢复后续传/完成)
  • 厂商后台管理(厂商「省电/后台限制」开启下的续传存活)
  • 断网 / 凭证过期恢复(真实网络切换 + presign 403 → renew 换 URL)
  • 强制退出(用户强杀后重启恢复,UI 不承诺后台完成)
  • 源文件缺失(SOURCE_MISSING 不自动重试)
  • 存储不足(准备阶段空间检查失败路径)
  • 批量 100 张真实样本(重复记录数应为 0——clientTaskId 幂等)
  • HEIC 真机样本解码(§5.4)

集成期新增发现

  • expedited 配额行为:setExpedited 在真机系统配额耗尽时的降级路径(当前 expedited + dataSync 前台服务实现,§6.3 UIDT 主路径待 G2 评估后定)
  • 厂商杀后台:WorkManager 在激进 ROM(MIUI/EMUI/ColorOS 等)上的实际存活与重排
  • dataSync 6h 上限:Android 15 对 foregroundServiceType=dataSync 的 6 小时系统上限行为(长队列场景)
  • POST_NOTIFICATIONS 授权 UX:集成脚本预授权绕过了弹窗——真机首次未授权时上传通知可见性与引导
  • 真机 adb 直连(无 reverse/10.0.2.2 便利)下的 API/MinIO 地址配置复核

最小验证集(总纲注记引用):真机装 APK → 登录 → 批量 100 张真实样本上传中锁屏 ≥15min + 切后台 + 断网恢复 → 厂商后台清理/强杀重启恢复 → HEIC 样本 → expedited 配额与通知授权 UX。

G3 iOS 编译清单(资源关口)

代码已完整提交、协议与 Android 逐字一致(Dart 侧同一 NativeTransport 驱动双平台);清单不复制,见 apps/mobile/ios/README-ios.md:代码清单表(UploadBridge/UploadEventBus/UploadTaskStore/UploadPlugin/AppDelegate)、Background Modes 偏差(有意不声明 UIBackgroundModes)、Xcode 首次构建五步(预期零改动通过)、G3 验证清单 10 项(实际文件上传/持久化原生回调/重启重联校准/强制退出/低电量调度/凭证过期/原生重试上限/进度节流/批量样本/HEIC + 可选 XCTest)。


偏差与发现登记

计划内已登记(计划/测试源码明示)

# 事项 说明
1 Dart 手工模型对齐契约 contracts 是 TS 不共享运行时;App 内手工镜像 Dart 模型(注释标注契约 operationId),以 openapi.yaml 为准人工对齐,openapi 生成物作对照文档
2 模拟器网络回环 10.0.2.2 API/OV/VIEWER_BASE 经 dart-define 指向 10.0.2.2 宿主回环(计划 Task 2 明示)
3 断网用 DIO 故障注入闸门 模拟器断真实网不便,fault_gate.dart 注入 SocketException 驱动 waiting_network(计划 Task 11 明示)
4 语言切换腿改 provider 驱动 弹层交互已有 widget 测试覆盖,IT 腿驱动 LocaleSettings provider 验证全局链路(排障 #7)
5 bg verify 改服务端权威断言 全新进程内存库不共享,以 GET /uploads confirmed 为准(排障 #10)
6 分类/相册表单腿的键盘收起 软键盘遮挡对话框按钮,dismissKeyboard + ensureVisible(排障 #4)
7 选图/连拍走多选替身 enqueueViaGallery 多选替身入队(真实准备/队列/引擎/原生桥接/后端全真);拍摄链路由拍摄页 widget 测试覆盖
8 拍摄腿未在 IT 驱动 camera 实拍无法在编排内驱动,连拍语义 = 引擎不可变测试 + 拍摄页 widget 测试 + IT 多选替身组合覆盖
9 viewer 腿用 ov 公开 API 替代网页断言 ov 公开相册 API 见 3 张(真实同步管线),网页侧断言由 web e2e 承担
10 隐藏一张经 ApiClient 调真实端点 App 照片页为 Task 4 占位无操作按钮;同一 ApiClient 调真实契约端点后回 UI 断言状态同步
11 Android 桥接 expedited + dataSync 计划原文 UIDT 主路径(§6.3)落地为 WorkManager expedited + dataSync 前台服务,UIDT 待 G2 真机评估后定(计划 Task 12 明示)
12 源文件保留策略默认 14 天 KEEP_COMPLETED_DAYS=14,仅清理 completed/cancelled,未完成源文件永不清理(守护单测)
13 iOS UIBackgroundModes 有意不声明 background URLSession 无需声明,多声明反引入审核 scrutiny(README-ios.md,对计划原文的修正)

计划外新发现(集成期,本记录补登)

# 事项 影响/处理
14 adb reverse tcp:9000 presign URL 内嵌服务端 localhost MinIO host(见排障 #2)——真机/CI 编排同样需要解决「客户端可达的 MinIO 地址」,G2 真机清单已登记复核项
15 compose 必须 --profile ov Docker 重启后 ov 侧容器不自动回来(排障 #3);脚本已固化,运维同样适用
16 flutter test 重装清数据 每次运行重装 APK 清 App 数据,测试素材回退 /data/local/tmp(排障 #11);也意味着集成运行间 App 本地数据不保留(dev 库共享数据隔离设计与此兼容)
17 phase2 staff define 占位 ItConfig.assertValid 全量校验要求非 staff 剧本也传 IT_STAFF_ACCOUNT(排障 #13)
18 MSYS 路径转化按命令禁用 Windows Git Bash 编排 adb 设备路径的通用坑(排障 #1),方案沉淀在 run-mobile-it.sh 头部注释

测试统计汇总(2026-09-20 回归门实测)

Web 侧(P3 未降级;仅 api e2e 因契约增补 +5)

包 单测 suites(files)/tests e2e suites(tests)
@photo/contracts 3 / 22 –
@photo/storage 1 / 6 –
@photo/api-cn 7 / 51 16 / 184(P2 179 → +5:password-change 4 + sourceDeviceId 1)
@photo/worker 1 / 8 1 / 7
@photo/sync –(passWithNoTests) 4 / 27
@photo/viewer vitest 16 / 105 playwright 6 spec / 21
@photo/admin vitest 4 / 15 playwright 2 spec / 7
web 合计 32 / 207 29 / 246(jest 21 suites / 218 + playwright 8 spec / 28)
  • 附加门:lint 9/9、typecheck 13/13、build 9/9 任务全绿。

移动端

套件 结果
flutter analyze No issues found!(0 issue)
flutter test(单测/widget,35 文件) 280 / 280 全过(00:19 +280: All tests passed!)
Android :app:testDebugUnitTest(gradle) 29 / 29(UploadEventBus 8 + UploadProtocol 11 + UploadPutExecutor 10,0 失败 0 错误)

注:不带模块限定的 gradlew test 会连带执行第三方插件 image_picker_android 自带测试套件(本机 10 败)——非本仓代码,App 模块测试以 :app:testDebugUnitTest 为准。

设备集成(2026-09-20 通过,日志存档不重跑)

套件 规模 结果
阶段 1 主流程(p3_flow_test.dart) 1 spec / 12 腿 00:36 +1: All tests passed!(29 轮迭代后收敛)
阶段 2 后台/杀进程(p3_background_test.dart) 2 场景 ×(prepare+verify) 两场景 00:02 +1: All tests passed!(3 轮迭代后收敛)

总量:自动化 207 + 246(web)+ 280 + 29(mobile)= 762(P2 出口 448 → +314:web +5、mobile +309),另设备集成 12 腿 + 2 场景全绿。