Files
App.K12.Student.Miniapp_Flu…/docs/demos/oss_upload_demo.md
T
2026-09-04 15:22:20 +08:00

4.1 KiB
Raw Blame History

【Demo】OSS 直传(客户端签名 + PUT 上传)

源文件:lib/pages/demo/oss_upload_demo.dart | 分类:网络 一句话:选图 → 后端分配路径 → 拿 STS → V4 签名 → PUT 直传 OSS,全程带进度。


一、这个 Demo 解决什么问题

图片上传不走"客户端 → 业务服务器 → OSS"的转发链路(浪费带宽、服务器压力大), 而是 STS 临时凭证 + 客户端直传 OSS:后端只负责分配路径和签发凭证,图片字节直接传阿里云。

完整链路(与 UniApp ossUpload.js 对照)

选图 → 后端分配路径(GetXxxOSSUploadUrl)
     → 拿 STS 凭证(GetAliOSSSetting)
     → V4 签名
     → PUT 直传 OSS(带进度)
     → 返回 CDN URL

二、怎么运行 / 演示内容

runApp(const MaterialApp(home: OssUploadDemoPage()));
  1. 选择上传场景:头像 / 笔记 / AI判题(UploadScene 枚举,对应后端 mode);
  2. "选择图片"(走 PickerService,见 note_picker_demo.md);
  3. "上传到 OSS":读字节 → 4 步链路 → 显示 CDN URL + 进度。

⚠ 需要后端 OSS 接口可访问(本地/测试环境)。


三、核心知识点(新手向)

1. 双端读取图片字节(不用 dart:io!)

// web  :image_picker 已读好字节
// 原生 :photo_manager 的 AssetEntity.originBytes(纯 Dart API)
final Uint8List bytes = image.bytes ??
    await image.entity?.originBytes ??
    Uint8List(0);
if (bytes.isEmpty) { /* 提示重新选择 */ }

关键:页面不 import dart:io,Web 才能编译(dart:io 在 Web 是禁用的)。 双端都能拿字节 → 上传逻辑统一。

2. 上传入口(OssUploader 封装)

final url = await OssUploader.uploadImage(
  bytes: bytes,
  fileName: fileName,
  scene: _scene, // UploadScene 枚举:头像/笔记/AI判题
  onSendProgress: (sent, total) {
    if (total > 0) {
      setState(() => _progress = sent / total); // total<=0 兜底
    }
  },
);

3. 403 的含义

403 = 签名或 STS 有问题 —— 看控制台 LogInterceptor 打印的响应体定位:

  • AccessDenied:签名计算不一致(method/path/时间戳对不上);
  • InvalidAccessKeyId / ExpiredToken:STS 凭证失效;
  • SignatureDoesNotMatch:V4 签名算法细节错。

四、关键代码剖析

// 场景名映射(switch 表达式,Dart 3)
static String _sceneName(UploadScene s) => switch (s) {
      UploadScene.avatar => '头像',
      UploadScene.note => '笔记',
      UploadScene.aiGrading => 'AI判题',
    };

// 场景选择(上传中禁用切换)
Wrap(
  spacing: 10.w,
  children: UploadScene.values
      .map((s) => ChoiceChip(
            label: Text(_sceneName(s)),
            selected: _scene == s,
            onSelected: _uploading ? null : (_) => setState(() => _scene = s),
          ))
      .toList(),
)

// 上传按钮(没选图或上传中 → 禁用)
FilledButton.icon(
  onPressed: (_image == null || _uploading) ? null : _upload,
  icon: const Icon(Icons.cloud_upload_outlined),
  label: Text(_uploading ? '上传中...' : '上传到 OSS'),
)

五、踩坑记录

  1. 别用 Image.file 双端判断 —— dart:io 在 Web 编译不过,统一走 bytes;
  2. 403 优先查签名:method/URL 编码/时间格式与后端约定不一致是头号原因;
  3. total <= 0 时进度要兜底(和 dio_pro_demo 同一个坑);
  4. 上传中要禁用场景切换和重复点击(_uploading 统一控制);
  5. 文件名生成:image.name ?? 'image_时间戳.jpg',避免空名。

六、扩展思考

  • 防盗链/私有读:OSS 私有 Bucket 的 URL 需要签名,展示时再向后端换"临时公开 URL";
  • 断点续传/分片:大文件(视频)用 OSS 分片上传 API,uploadFile 封装在 OssUploader 里扩展;
  • 与 dio_pro_demo.md 对照:PUT 直传的进度就是 onSendProgress 的应用;
  • 笔记图片多图上传:先全部直传拿 URL,再一次性提交给"创建笔记"接口(见 note_picker_demo 的 TODO 注释)。