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

122 lines
4.1 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.
# 【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 注释)。