You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

4.6 KiB

文件 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 构建函数

# 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. 存储服务返回相对路径

# app/core/storage.py
class StorageService:
    async def upload_bytes(self, ...) -> str:
        """返回相对路径,不含域名"""
        self.client.put_object(...)
        return object_name  # 例如 "screenplays/xxx.md"

3. 文件去重统一返回相对路径

# 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 计算字段

# 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_urlattachment_image/019c.../abc123.jpg
  • screenplays.file_urlscreenplays/019c.../xxx.md
  • file_checksums.file_urlattachment_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() 智能判断,旧数据仍然可用。


📊 数据迁移(可选)

虽然旧数据不影响功能,但如果您希望数据库统一存储相对路径,可以运行迁移脚本。

迁移前检查

# 1. 进入容器
docker compose exec app bash

# 2. 检查需要迁移的数据量
cd /app
python test_relative_path.py

执行迁移

⚠️ 重要:先备份数据库!

# 备份数据库
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

验证迁移结果

# 再次检查
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

相关文档