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

4.6 KiB
Raw Blame History

文件下载 — 架构文档

对应 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 步骤

# 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));