# 文件下载 — 架构文档 > 对应 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> └─ NetworksSdkApi (via networkSdkApiProvider) ``` 与 Settings ViewModel 一致,使用手动 `Notifier` + `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)); ```