# 【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 ``` --- ## 二、怎么运行 / 演示内容 ```dart runApp(const MaterialApp(home: OssUploadDemoPage())); ``` 1. 选择上传场景:头像 / 笔记 / AI判题(`UploadScene` 枚举,对应后端 mode); 2. "选择图片"(走 PickerService,见 [note_picker_demo.md](note_picker_demo.md)); 3. "上传到 OSS":读字节 → 4 步链路 → 显示 CDN URL + 进度。 > ⚠ 需要后端 OSS 接口可访问(本地/测试环境)。 --- ## 三、核心知识点(新手向) ### 1. 双端读取图片字节(不用 dart:io!) ```dart // 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 封装) ```dart 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 签名算法细节错。 --- ## 四、关键代码剖析 ```dart // 场景名映射(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](dio_pro_demo.md) 对照:PUT 直传的进度就是 `onSendProgress` 的应用; - 笔记图片多图上传:先全部直传拿 URL,再一次性提交给"创建笔记"接口(见 note_picker_demo 的 TODO 注释)。