Files
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

10 KiB
Raw Permalink Blame History

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 原样携带 requiredHeadersContent-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,本机):

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 仓库根执行):

flutter analyze
flutter test
dart format --set-exit-if-changed --output=none .

8. 遗留与交接

  • T3-17(发布页)接入面MediaUploader(注入 CommunityController 同源 repository 即可,其余依赖有生产默认值)+ UploadProgressOverlay + buildAttachRequests(coverIndex:)overallProgress/readyCount 供页级 汇总条;发布 gating 用 allReady05 号 §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