- 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>
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
# 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/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,本机):
|
||||
|
||||
```bash
|
||||
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 仓库根执行):
|
||||
|
||||
```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 Developer(Flutter)**
|
||||
**日期**:2026-09-09
|
||||
Reference in New Issue
Block a user