Files
patbond-doc/docs/development/iterations/iteration-3/23-media-upload-client.md
T
lixi f5457c2f4c
CI / docs-build (push) Successful in 2m2s
docs: M3 第三波收口——报告 21~27 入档挂导航
- 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>
2026-09-10 14:48:33 +08:00

183 lines
10 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.
# 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 不得被引用)三重保证**:
1. confirm 前的服务端 assetId 只以管线局部变量存在,不落任务状态;
2. 对外快照 `MediaUploadItem.assetId` 与 ready 态**构造期断言绑定**;
3. 交付口 `buildAttachRequests` 在任何非 ready 项在场时抛 `StateError`
移除/reset 后的在途结果一律作废(不 confirm,服务端 uploading 超时清理
兜底,13 号 §6 方案)。
## 2. 弱网 / 失败语义矩阵
| 故障点 | 表现 | 客户端语义 | 自动处置 | 手动 retry 后 |
|--------|------|-----------|---------|--------------|
| 压缩后仍超 10 MiB | 本地判定 | failed **终态** | 无 | no-op |
| createUpload 400/40000mime/大小白名单外) | 参数拒绝 | 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 号 §2MediaController 在 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,本机):
```bash
cd <你的工作区>/patbond-api
./deploy/init-secrets.sh
JAVA_HOME=<你的 JDK17 路径> ./mvnw -DskipTests package # BUILD SUCCESS
docker compose up -d --build # 六容器全部 Uppostgres/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 →
createUploaduser :8082,凭据 uploadUrl 指向 MinIO :9000)→ 预签名 PUT
直传(真实 DioMediaDirectUploadClient)→ confirm → ready assetId →
`buildAttachRequests` 引用发帖(community :8084published)→ 帖子响应中
预签名 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 状态机 20happy path 3、并发顺序 2、弱网失败语义
9、孤儿防护 4、选图容量 4,含凭据过期重取、403 换凭据、42205 重试换新
asset、终态 retry no-op、在途 remove/reset 作废不 confirm、全生命周期快照
assetId 仅 ready 非空);直传层本地 HttpServer 3200 逐字节到达 +
requiredHeaders 原样 + 无 Bearer/设备头泄漏、403 → isCredentialRejected、
半途断连 → 网络型可重试,照埋点队列测试先例);UploadProgressOverlay
widget 6(三态 + confirming 定格 + 终态无重试 + 成功淡出);仓库 media
线路双 adapter 改造 2(计入原有数);FakeCommunityRepository 扩 media 钩子。
验证命令(patbond-flutter 仓库根执行):
```bash
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.md` M3 预登记第 1 项(步骤与通过
标准已补全)。
- flutter_image_compress 的原生压缩行为(HEIC 输入转码、超大图内存)只能
真机验证,随上项一并覆盖。
- uploading 僵尸 asset 服务端清理任务(13 号 §6)仍未实现,客户端弃引用
策略已按其到位为前提设计,无正确性风险(业务侧只认 ready)。
---
**Frontend DeveloperFlutter**
**日期**2026-09-09