feat(file-download): 文件下载全量实现 (#14~#18)
- CdnUrlResolver: 相对路径/S3 URL 统一解析为 apiBaseUrl 代理链路 (#14) - FileDownloadManager: Notifier<Map<url, state>>,四态(idle/progress/done/failed), 断点续传 + 取消令牌 + 本地缓存(systemTemp/im_file_cache) (#15) - FileMessageBubble: 三态 UI,文件类型图标/颜色,大小格式化, idle/failed 点击触发下载,done 点击回调 open_filex TODO (#16) - AudioMessageBubble: 语音消息下载框架,静态波形装饰, 播放 TODO(audioplayers 接入后解开) (#17) - VideoMessageBubble: 缩略图 Image.network + CdnUrlResolver, 播放按钮覆盖,上传进度环,video_player 接入 TODO (#18) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
163
Doc/file_download_architecture.md
Normal file
163
Doc/file_download_architecture.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# 文件下载 — 架构文档
|
||||
|
||||
> 对应 Gitea issues #14–#18
|
||||
> 参考实现:`im-client-ios-swift-demo` FileDownloadManager + AudioPlaybackService + ChatMessageBubble
|
||||
|
||||
---
|
||||
|
||||
## 1. 功能范围
|
||||
|
||||
| Issue | 功能 | 状态 |
|
||||
|-------|------|------|
|
||||
| #14 | CDN URL 解析器(CdnUrlResolver) | ✅ 已实现 |
|
||||
| #15 | FileDownloadManager — 下载服务 + 缓存 | ✅ 已实现 |
|
||||
| #16 | FileMessageBubble — 文件气泡三态 UI | ✅ 已实现 |
|
||||
| #17 | AudioMessageBubble — 语音下载框架 | ✅ 框架(播放需 audioplayers) |
|
||||
| #18 | VideoMessageBubble — 视频缩略图框架 | ✅ 框架(播放需 video_player) |
|
||||
|
||||
---
|
||||
|
||||
## 2. 目录结构
|
||||
|
||||
```
|
||||
core/services/
|
||||
├── cdn_url_resolver.dart # CDN 路径 → 完整 URL
|
||||
└── file_download_manager.dart # 下载服务(进度追踪 + 缓存)
|
||||
|
||||
features/chat/view/widgets/
|
||||
├── file_message_bubble.dart # 文件消息气泡(三态 UI)
|
||||
├── audio_message_bubble.dart # 语音消息气泡(下载框架)
|
||||
└── video_message_bubble.dart # 视频消息气泡(缩略图框架)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心概念
|
||||
|
||||
### 3.1 FileDownloadState
|
||||
|
||||
```
|
||||
idle → 未下载(显示下载图标)
|
||||
downloading(progress: 0.0~1.0) → 下载中(进度条)
|
||||
downloaded(localPath: String) → 已缓存(显示打开图标)
|
||||
failed(error: String) → 失败(显示重试图标)
|
||||
```
|
||||
|
||||
### 3.2 CDN URL 路由策略
|
||||
|
||||
```
|
||||
相对路径 Files/xxx.pdf → apiBaseUrl/Files/xxx.pdf
|
||||
完整 HTTP http://xxx.com/f → 直接使用
|
||||
S3 URL *.amazonaws.com → apiBaseUrl + S3 path(通过 API 代理)
|
||||
```
|
||||
|
||||
S3 桶是私有的,直接访问返回 403。所有下载都必须经过 `apiBaseUrl` 代理。
|
||||
|
||||
### 3.3 缓存策略
|
||||
|
||||
```
|
||||
缓存目录:Directory.systemTemp/im_file_cache/
|
||||
文件名: {url.hashCode 8位 hex}_{sanitized_filename}
|
||||
命中条件:文件存在 && 大小 > 0
|
||||
清理策略:由 OS 自动清理 temp 目录(同 iOS Library/Caches 语义)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据流
|
||||
|
||||
### 4.1 文件消息下载
|
||||
|
||||
```
|
||||
FileMessageBubble.onTap
|
||||
└─ fileDownloadManagerProvider.notifier.download(url, fileName)
|
||||
├─ CdnUrlResolver.resolve(url) → 完整下载 URL
|
||||
├─ _cacheKeyFor(url, fileName) → 本地文件名
|
||||
├─ File(cachePath).exists() ?
|
||||
│ true → state = downloaded(cachePath)
|
||||
│ false → NetworksSdkApi.executeDownload(url, savePath, onProgress)
|
||||
│ → state = downloading(progress)
|
||||
│ → state = downloaded(cachePath) / failed(error)
|
||||
└─ 返回 localPath(供 open_filex 打开)
|
||||
```
|
||||
|
||||
### 4.2 语音消息
|
||||
|
||||
```
|
||||
AudioMessageBubble
|
||||
└─ 点击播放按钮
|
||||
└─ FileDownloadManager.download(voiceUrl, 'voice_{cmid}.m4a')
|
||||
→ downloaded(localPath)
|
||||
→ TODO: AudioPlayer().play(DeviceFileSource(localPath))
|
||||
```
|
||||
|
||||
### 4.3 视频消息
|
||||
|
||||
```
|
||||
VideoMessageBubble
|
||||
└─ 显示:Image.network(CdnUrlResolver.resolve(thumbUrl)) // 缩略图
|
||||
└─ 点击播放按钮
|
||||
→ TODO: Navigator 推送全屏视频页(video_player 流式播放)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Provider 设计
|
||||
|
||||
```
|
||||
fileDownloadManagerProvider
|
||||
NotifierProvider<FileDownloadManager, Map<String, FileDownloadState>>
|
||||
└─ NetworksSdkApi (via networkSdkApiProvider)
|
||||
```
|
||||
|
||||
与 Settings ViewModel 一致,使用手动 `Notifier<T>` + `NotifierProvider`,
|
||||
不依赖 `@riverpod` 代码生成。
|
||||
|
||||
---
|
||||
|
||||
## 6. 消息类型路由(rawContent 解析)
|
||||
|
||||
| typ | 类型 | rawContent 格式 |
|
||||
|-----|------|----------------|
|
||||
| 3 | 语音 | `{"url":"...","duration":N}` |
|
||||
| 4 | 视频 | `{"url":"...","thumb":"...","size":N}` |
|
||||
| 6 | 文件 | `{"url":"...","name":"...","size":N}` |
|
||||
| 24 | 视频(秘密) | 同 typ=4 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 待完成
|
||||
|
||||
- **audioplayers 接入**:`AudioMessageBubble` 中 TODO 解开,安装 `audioplayers: ^6.0.0`
|
||||
- **video_player 接入**:`VideoMessageBubble` 点击推送全屏路由,安装 `video_player: ^2.9.0`
|
||||
- **open_filex 接入**:`FileMessageBubble` downloaded 态打开文件,安装 `open_filex: ^3.0.3`
|
||||
- **XOR 解密**:`secret/` 路径文件解密(cipher_guard_sdk 接入时实现)
|
||||
|
||||
### 接入 open_filex 步骤
|
||||
|
||||
```yaml
|
||||
# apps/im_app/pubspec.yaml
|
||||
dependencies:
|
||||
open_filex: ^3.0.3
|
||||
```
|
||||
|
||||
```dart
|
||||
// FileMessageBubble downloaded 态 onTap:
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
await OpenFilex.open(localPath);
|
||||
```
|
||||
|
||||
### 接入 audioplayers 步骤
|
||||
|
||||
```yaml
|
||||
# apps/im_app/pubspec.yaml
|
||||
dependencies:
|
||||
audioplayers: ^6.0.0
|
||||
```
|
||||
|
||||
```dart
|
||||
// AudioMessageBubble 播放:
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
final player = AudioPlayer();
|
||||
await player.play(DeviceFileSource(localPath));
|
||||
```
|
||||
Reference in New Issue
Block a user