- 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>
4.6 KiB
4.6 KiB
文件下载 — 架构文档
对应 Gitea issues #14–#18 参考实现:
im-client-ios-swift-demoFileDownloadManager + 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 接入:
FileMessageBubbledownloaded 态打开文件,安装open_filex: ^3.0.3 - XOR 解密:
secret/路径文件解密(cipher_guard_sdk 接入时实现)
接入 open_filex 步骤
# apps/im_app/pubspec.yaml
dependencies:
open_filex: ^3.0.3
// FileMessageBubble downloaded 态 onTap:
import 'package:open_filex/open_filex.dart';
await OpenFilex.open(localPath);
接入 audioplayers 步骤
# apps/im_app/pubspec.yaml
dependencies:
audioplayers: ^6.0.0
// AudioMessageBubble 播放:
import 'package:audioplayers/audioplayers.dart';
final player = AudioPlayer();
await player.play(DeviceFileSource(localPath));