- 21~26 Flutter 社区接入五单 + 字典 v3 白名单(flutter 286→502、api 325→334) - 27 收口总表:社区 demo 三页消亡、M3 四条验收标准逐条取证、 乐观更新与媒体链路端到端、实现期修正记录 - device-verification.md 的 M3 四项真机步骤已由各单收口补全 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
10 KiB
23 M3 第三波:媒体上传客户端(T3-13)
执行日期:2026-09-09
工单:T3-13 媒体上传客户端(L,关键路径)——选图到确认的完整客户端链路,T3-17 发布页依赖本单
协议依据:13 号报告 §3 凭据形态定型表 + §4 偏差清单(两步上传协议权威描述)、契约 v1.3.0
提交:patbond-flutter dev 1441f01(基线 19bd8c1)
0. 概要
在 T3-12 数据层(createUpload / confirm 协议层)之上补齐直传 PUT 本体与编排, 交付五个生产文件 + 五个测试文件:
| 文件 | 职责 |
|---|---|
lib/features/community/media_uploader.dart |
MediaUploader 编排状态机(本单核心,接口按 03 号评估 §4.3 冻结稿定稿) |
lib/features/community/media_picking.dart |
选图抽象 + image_picker 系统选择器实现 |
lib/features/community/media_compression.dart |
压缩抽象 + flutter_image_compress 原生实现(长边 ≤2048、统一转码 jpeg、不保留 EXIF) |
lib/features/community/media_direct_upload.dart |
预签名 PUT 直传客户端(裸 Dio,无鉴权拦截器,进度回调) |
lib/core/widgets/upload_progress_overlay.dart |
可复用进度覆盖层(05 号规范 §3.3 四态) |
并发现并修正一处 T3-12 遗留缺陷(§4):media 两步上传端点误挂 community 客户端。compose 六容器真链路实测通过(§5)。
质量门禁:flutter test 379/379 全绿(基线 347,+32;另有 1 个默认跳过的
compose 冒烟测试)、flutter analyze 0 问题、dart format --set-exit-if-changed
无 diff。
1. MediaUploader 状态机
单张图生命周期(MediaItemPhase):
queued ──► compressing ──► uploading(progress 0..1) ──► confirming ──► ready(assetId)
│ │ │ │
│ 超限终态失败 网络中断/存储拒绝 42205 / 网络异常
│ ▼ ▼ ▼
└──────► failed(retryable?) ◄─────┴───────────────────────┘
│ retry(仅 retryable)
└──► queued(复用压缩产物,从 createUpload 全新开始,换新 assetId)
uploader 级另有 isPicking(系统选择器拉起中)。编排要点:
- 压缩策略(03 号 §4.1 + 13 号偏差 #6):长边 ≤2048 重采样、统一转码
JPEG、降质阶梯 80 → 60;两档后仍超 10 MiB → 终态失败(不可重试),
不发起任何网络调用。
keepExif保持关闭,顺带剥离 GPS 隐私;autoCorrectionAngle矫正方向。 - 多图并发与顺序保持:并发槽位默认 2(信号量覆盖压缩到 confirm 全段);
items 顺序 = 加入顺序 = position 语义,完成先后乱序不影响
buildAttachRequests发号(测试实证第 2 张先 ready 仍归位 index 1)。 单图失败不拖垮整批,其余照常 ready。 - 凭据纪律:预签名凭据只以局部变量存在、用完即弃,不持久化(沿用纪律);
直传 PUT 原样携带
requiredHeaders(Content-Type 已签进签名)。 - 孤儿防护(未 confirm 的 asset 不得被引用)三重保证:
- confirm 前的服务端 assetId 只以管线局部变量存在,不落任务状态;
- 对外快照
MediaUploadItem.assetId与 ready 态构造期断言绑定; - 交付口
buildAttachRequests在任何非 ready 项在场时抛StateError。 移除/reset 后的在途结果一律作废(不 confirm,服务端 uploading 超时清理 兜底,13 号 §6 方案)。
2. 弱网 / 失败语义矩阵
| 故障点 | 表现 | 客户端语义 | 自动处置 | 手动 retry 后 |
|---|---|---|---|---|
| 压缩后仍超 10 MiB | 本地判定 | failed 终态 | 无 | no-op |
| createUpload 400/40000(mime/大小白名单外) | 参数拒绝 | failed 终态 | 无 | no-op |
| createUpload 网络异常 | — | failed 可重试 | 无 | 全新 createUpload |
| PUT 前凭据已过期(30s 安全边距预检) | 本地判定 | 透明恢复 | 重新 createUpload 一次(换新 assetId/凭据),仍过期才 failed | 全新 createUpload |
| 直传 PUT 403(签名过期/被改动) | 存储侧拒绝 | 透明恢复 | 重新 createUpload 一次并重传,再 403 才 failed(可重试) | 全新 createUpload |
| 直传 PUT 断连/超时 | 网络型 | failed 可重试 | 无 | 全新 createUpload |
| confirm 42205(对象未上传,服务端保持 uploading) | 可恢复 | failed 可重试 | 无 | 全新 createUpload |
| confirm 42205(内容不符,服务端置 failed 终态) | 不可恢复 | failed 可重试* | 无 | 全新 createUpload |
| confirm 返回非 ready(防御分支) | — | failed 可重试 | 无 | 全新 createUpload |
* 两种 42205 客户端不可区分(同码同形态),统一按「可重试 + 重试换新 asset」处理:对「保持 uploading」分支旧 asset 成为服务端可清理的 uploading 僵尸,对「置 failed」分支旧 asset 本就终态——两分支都正确收敛,旧 assetId 一律弃引用(孤儿防护保证其不会被发帖引用)。重试复用压缩产物(不重压缩)。
3. 可复用进度组件
UploadProgressOverlay(05 号 §3.3 逐条落位):排队(ink 40% scrim +
「等待中」白字衬 ink 80% 胶囊)/ 上传中(白色环形进度 36 value 态 + 百分比
胶囊;confirming 定格 100%)/ 成功(scrim 150ms 淡出无残留,IgnorePointer
不拦截点击)/ 失败(error 12% scrim + errorDark 图标 + 底部「重试」通栏,
整格点按重试;终态失败不显示重试通栏)。九宫格组装与页级线性汇总条
(overallProgress 已暴露)留 T3-17。
4. T3-12 遗留缺陷修正:media 端点线路
发现:media 两步上传端点(POST /api/v1/media/uploads[...])由 user
服务提供(13 号 §2,MediaController 在 patbond-user :8082),community
服务只有帖子/评论路由与媒体读取侧签名(MediaUrlSigner);而 T3-12 的
ApiCommunityRepository 把 19 操作全部挂在 community 客户端(:8084)——
media 两操作真链路必 404(T3-12 只做了协议层,无实测暴露点)。
修正:ApiCommunityRepository 增可选 mediaApi 客户端,media 两方法
走它(未提供回落主客户端,既有测试桩不受影响);app.dart 装配处为其构建
user 服务基址(patbondUserApiBaseUrl)的第二 ApiClient,共享
SessionManager 与单飞 TokenRefresher。仓库测试改为双 adapter 断言线路不串。
compose 真链路实测(§5)证实修正必要且有效。未动 patbond-api。
5. compose 六容器真链路实测
实测记录(2026-09-09,本机):
cd <你的工作区>/patbond-api
./deploy/init-secrets.sh
JAVA_HOME=<你的 JDK17 路径> ./mvnw -DskipTests package # BUILD SUCCESS
docker compose up -d --build # 六容器全部 Up,postgres/minio healthy
cd <你的工作区>/patbond-flutter
PATBOND_MEDIA_SMOKE=1 flutter test test/smoke/media_upload_smoke_test.dart
# 00:01 +1: All tests passed!
cd <你的工作区>/patbond-api && docker compose down # 干净退出
冒烟测试(test/smoke/media_upload_smoke_test.dart,默认 skip 不计入常规
套件)驱动真实 MediaUploader 走完整链路:注册一次性账号取 token →
createUpload(user :8082,凭据 uploadUrl 指向 MinIO :9000)→ 预签名 PUT
直传(真实 DioMediaDirectUploadClient)→ confirm → ready assetId →
buildAttachRequests 引用发帖(community :8084,published)→ 帖子响应中
预签名 GET URL 回读 200 且字节与上传逐字节一致 → 删帖收尾。压缩层用
透传实现(flutter test VM 无原生编解码平台通道),其余全为生产实现。
期间修正一处冒烟脚本自身问题(注册手机号须 E.164 格式)。
6. 依赖新增说明
| 依赖 | 版本 | 理由 |
|---|---|---|
image_picker |
^1.2.0 | 03 号评估 §4.1 选型:官方维护、pickMultiImage 多选;不引入重型相册组件 |
flutter_image_compress |
^2.4.0 | 同上:原生编解码(纯 Dart image 包中端机秒级卡顿排除);质量 + 尺寸重采样 + EXIF 方向矫正 |
直传 PUT 未新增依赖(复用既有 dio,独立裸实例)。桌面平台 generated plugin 注册文件随 pub get 更新一并入库。
7. 测试数变化
| 项 | 基线 | 本单后 |
|---|---|---|
| flutter test | 347 | 379(+32,另 1 个默认跳过的 compose 冒烟) |
| flutter analyze | 0 | 0 |
| dart format | 无 diff | 无 diff |
新增分布:MediaUploader 状态机 20(happy path 3、并发顺序 2、弱网失败语义 9、孤儿防护 4、选图容量 4,含凭据过期重取、403 换凭据、42205 重试换新 asset、终态 retry no-op、在途 remove/reset 作废不 confirm、全生命周期快照 assetId 仅 ready 非空);直传层本地 HttpServer 3(200 逐字节到达 + requiredHeaders 原样 + 无 Bearer/设备头泄漏、403 → isCredentialRejected、 半途断连 → 网络型可重试,照埋点队列测试先例);UploadProgressOverlay widget 6(三态 + confirming 定格 + 终态无重试 + 成功淡出);仓库 media 线路双 adapter 改造 2(计入原有数);FakeCommunityRepository 扩 media 钩子。
验证命令(patbond-flutter 仓库根执行):
flutter analyze
flutter test
dart format --set-exit-if-changed --output=none .
8. 遗留与交接
- T3-17(发布页)接入面:
MediaUploader(注入 CommunityController 同源 repository 即可,其余依赖有生产默认值)+UploadProgressOverlay+buildAttachRequests(coverIndex:);overallProgress/readyCount供页级 汇总条;发布 gating 用allReady(05 号 §2.2:全部 ready 才放行提交)。 - 真机专属项:蜂窝/弱 Wi-Fi 实测已按维护约定登记到
docs/development/device-verification.mdM3 预登记第 1 项(步骤与通过 标准已补全)。 - flutter_image_compress 的原生压缩行为(HEIC 输入转码、超大图内存)只能 真机验证,随上项一并覆盖。
- uploading 僵尸 asset 服务端清理任务(13 号 §6)仍未实现,客户端弃引用 策略已按其到位为前提设计,无正确性风险(业务侧只认 ready)。
Frontend Developer(Flutter) 日期:2026-09-09