Files
customer-im-client-dev/Doc/file_download_architecture.md
pp-bot 83774f5f61 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>
2026-03-23 20:02:46 +09:00

164 lines
4.6 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.
# 文件下载 — 架构文档
> 对应 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));
```