# 文件 URL 存储策略迁移指南 **日期**: 2026-02-06 **版本**: v2.0 **类型**: 数据迁移 --- ## 📋 背景 为了支持多环境部署和 CDN 切换,我们将文件 URL 的存储策略从**完整 URL**改为**相对路径**。 ### 架构变更 **改进前**: ``` 数据库存储: https://oss.example.com/screenplays/xxx.md API 返回: https://oss.example.com/screenplays/xxx.md ``` **改进后**: ``` 数据库存储: screenplays/xxx.md (相对路径) API 返回: https://oss.example.com/screenplays/xxx.md (动态拼接) ``` --- ## ✅ 已完成的代码修改 ### 1. 智能 URL 构建函数 ```python # app/core/storage.py def build_file_url(path_or_url: str) -> str: """ 智能判断输入类型: - 完整 URL → 直接返回(向后兼容) - 相对路径 → 拼接域名 """ if path_or_url.startswith('http://') or path_or_url.startswith('https://'): return path_or_url # 旧数据,直接返回 return f"{settings.S3_PUBLIC_URL}/{path_or_url}" # 新数据,拼接域名 ``` ### 2. 存储服务返回相对路径 ```python # app/core/storage.py class StorageService: async def upload_bytes(self, ...) -> str: """返回相对路径,不含域名""" self.client.put_object(...) return object_name # 例如 "screenplays/xxx.md" ``` ### 3. 文件去重统一返回相对路径 ```python # app/services/file_storage_service.py class FileStorageService: async def upload_file(self, ...) -> FileMetadata: existing = await self.checksum_repo.get_by_checksum(checksum) if existing: # ✅ 使用 storage_path(相对路径),而不是 file_url(可能是旧的完整 URL) return FileMetadata( file_url=existing.storage_path, # 统一返回相对路径 ... ) ``` ### 4. Schema 计算字段 ```python # app/schemas/*.py class ScreenplayResponse(BaseModel): file_url: Optional[str] # 数据库存储相对路径 @computed_field(alias="parsedFileUrl") @property def parsed_file_url(self) -> Optional[str]: """API 返回完整 URL""" return build_file_url(self.file_url) if self.file_url else None ``` --- ## 🔄 当前状态 ### ✅ 新数据(2026-02-06 之后) 所有新上传的文件会自动存储为**相对路径**: - `attachments.file_url` → `attachment_image/019c.../abc123.jpg` - `screenplays.file_url` → `screenplays/019c.../xxx.md` - `file_checksums.file_url` → `attachment_document/019c.../doc.pdf` ### ⚠️ 旧数据(2026-02-06 之前) 仍然存储**完整 URL**: - `https://static.timelab.cn/screenplays/xxx.md` - `https://storage.example.com/attachments/xxx.pdf` **影响**:✅ 无影响!`build_file_url()` 智能判断,旧数据仍然可用。 --- ## 📊 数据迁移(可选) 虽然旧数据不影响功能,但如果您希望数据库统一存储相对路径,可以运行迁移脚本。 ### 迁移前检查 ```bash # 1. 进入容器 docker compose exec app bash # 2. 检查需要迁移的数据量 cd /app python test_relative_path.py ``` ### 执行迁移 ⚠️ **重要:先备份数据库!** ```bash # 备份数据库 docker compose exec postgres pg_dump -U jointo jointo > backup_$(date +%Y%m%d).sql # 执行迁移 docker compose exec app python scripts/migrate_urls_to_relative.py ``` ### 迁移脚本功能 脚本会自动转换以下表的 `file_url` 字段: 1. `attachments` - 附件表 2. `screenplays` - 剧本表 3. `file_checksums` - 文件去重表 **转换规则**: ``` https://domain.com/path/to/file.pdf → path/to/file.pdf https://bucket.s3.region.amazonaws.com/path/file.pdf → path/file.pdf ``` ### 验证迁移结果 ```bash # 再次检查 python test_relative_path.py # 预期输出 # ✅ 相对路径: screenplays/019c.../xxx.md # ✅ 相对路径: attachment_image/019c.../avatar.jpg ``` --- ## 🎯 总结 ### 优势 - ✅ **域名无关** - 便于多环境部署(dev/staging/prod) - ✅ **CDN 切换** - 只需修改配置文件 `S3_PUBLIC_URL` - ✅ **向后兼容** - 旧数据不受影响,自动兼容 - ✅ **零停机** - 无需立即迁移,可选择性清理 ### 注意事项 1. **新数据自动使用相对路径** - 无需手动干预 2. **旧数据可选迁移** - 不影响功能,可延后处理 3. **API 响应不变** - 前端无感知,仍然收到完整 URL ### 相关文档 - [RFC 140 - Screenplay 文件存储重构](../rfcs/140-screenplay-file-storage-refactor.md) - [URL 管理辅助函数](../../../server/app/core/STORAGE_URL_HELPERS.md) - [Changelog 2026-02-06](../changelogs/2026-02-06-screenplay-file-storage-refactor.md)