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.
 

10 KiB

AttachmentService 集成 FileStorageService

日期:2026-01-28
类型:功能集成
影响范围:AttachmentService


变更概述

将 FileStorageService 集成到 AttachmentService 中,替换临时文件存储逻辑,实现文件去重、引用计数管理和预签名 URL 生成。


集成内容

1. 初始化 FileStorageService

修改前

class AttachmentService:
    def __init__(self, session: AsyncSession):
        self.repository = AttachmentRepository(session)
        self.session = session

修改后

from app.services.file_storage_service import FileStorageService

class AttachmentService:
    def __init__(self, session: AsyncSession):
        self.repository = AttachmentRepository(session)
        self.session = session
        self.file_storage = FileStorageService(session)

2. 文件上传集成

修改前(临时实现)

# 暂时使用简单的文件存储逻辑
import hashlib
import os
checksum = hashlib.sha256(content).hexdigest()
extension = os.path.splitext(file.filename)[1]
file_url = f"/uploads/attachments/{checksum}{extension}"  # 临时 URL
storage_path = f"attachments/{category}/{user_id}/{checksum}{extension}"

修改后(FileStorageService)

# 使用 FileStorageService 上传(带去重)
import os
file_meta = await self.file_storage.upload_file(
    file_content=content,
    filename=file.filename,
    content_type=file.content_type,
    category=f'attachment_{category}',
    user_id=user_id
)

extension = file_meta.extension
checksum = file_meta.checksum
file_url = file_meta.file_url
storage_path = file_meta.storage_path

关键变更

  • 使用 FileStorageService.upload_file() 上传文件
  • 自动计算 SHA256 校验和
  • 自动检查文件是否已存在(去重)
  • 如果文件已存在,增加引用计数并返回已有文件信息
  • 如果文件不存在,上传到 MinIO 并记录到 file_checksums 表

3. 存储提供商更新

修改前

attachment = Attachment(
    # ...
    storage_provider='local',  # 临时使用 local
    storage_path=storage_path
)

修改后

attachment = Attachment(
    # ...
    storage_provider=file_meta.storage_provider,  # 从 FileStorageService 获取
    storage_path=storage_path
)

4. 预签名 URL 生成

修改前(临时实现)

# 暂时返回文件 URL
return attachment.file_url

修改后(FileStorageService)

# 生成预签名URL
return await self.file_storage.get_presigned_url(
    attachment.storage_path,
    expires=expires
)

关键变更

  • 使用 FileStorageService.get_presigned_url() 生成临时访问链接
  • 支持自定义过期时间(默认 3600 秒)
  • 返回 MinIO 预签名 URL

5. 文件删除集成

修改前(临时实现)

# 软删除数据库记录
await self.repository.soft_delete(attachment_id)

# TODO: 减少文件引用计数
# await self.file_storage.decrease_reference_count(attachment.checksum)

修改后(FileStorageService)

# 软删除数据库记录
await self.repository.soft_delete(attachment_id)

# 减少文件引用计数(通过 checksum 查找 FileChecksum 记录)
file_checksum = await self.file_storage.get_by_checksum(attachment.checksum)
if file_checksum:
    await self.file_storage.decrease_reference_count(file_checksum.id)

logger.info(f"附件删除成功: {attachment_id}")

关键变更

  • 使用 FileStorageService.get_by_checksum() 查找文件记录
  • 使用 FileStorageService.decrease_reference_count() 减少引用计数
  • 如果引用计数为 0,FileStorageService 会自动删除 MinIO 中的文件

功能对比

功能 修改前(临时实现) 修改后(FileStorageService)
文件上传 本地计算 checksum,生成临时 URL 上传到 MinIO,自动去重
文件去重 不支持 基于 SHA256 自动去重
引用计数 不支持 自动管理引用计数
存储位置 本地文件系统 MinIO 对象存储
下载链接 直接返回文件 URL 生成预签名 URL(临时访问)
文件删除 仅删除数据库记录 减少引用计数,无引用时删除文件
存储提供商 硬编码 'local' 从配置读取(minio/s3/oss)

工作流程

上传流程

用户上传文件
    ↓
AttachmentService.upload_attachment()
    ↓
验证用户、关联实体、权限、文件类型、文件大小
    ↓
FileStorageService.upload_file()
    ├─ 计算 SHA256 校验和
    ├─ 查询 file_checksums 表
    ├─ 如果存在 → 增加引用计数,返回已有文件信息
    └─ 如果不存在 → 上传到 MinIO,插入新记录
    ↓
创建 Attachment 记录
    ↓
返回附件信息

下载流程

用户请求下载
    ↓
AttachmentService.get_download_url()
    ↓
验证权限
    ↓
增加下载计数
    ↓
FileStorageService.get_presigned_url()
    ├─ 调用 MinIO API
    └─ 生成临时访问链接(有效期 3600 秒)
    ↓
返回预签名 URL

删除流程

用户删除附件
    ↓
AttachmentService.delete_attachment()
    ↓
验证权限
    ↓
软删除 Attachment 记录
    ↓
FileStorageService.decrease_reference_count()
    ├─ 减少引用计数
    ├─ 如果引用计数 > 0 → 更新记录
    └─ 如果引用计数 = 0 → 删除 MinIO 文件 + 删除 file_checksums 记录
    ↓
完成删除

文件去重示例

场景:多个用户上传相同文件

用户 A 上传 avatar.jpg

# 1. 计算 checksum = "abc123..."
# 2. 查询 file_checksums 表 → 不存在
# 3. 上传到 MinIO: attachment_image/user_a/abc123.jpg
# 4. 插入 file_checksums 记录:
#    - checksum: "abc123..."
#    - file_url: "http://minio/jointo/attachment_image/user_a/abc123.jpg"
#    - reference_count: 1
# 5. 创建 Attachment 记录(user_a)

用户 B 上传相同的 avatar.jpg

# 1. 计算 checksum = "abc123..."(相同)
# 2. 查询 file_checksums 表 → 存在!
# 3. 增加引用计数: reference_count = 2
# 4. 返回已有文件信息(不重复上传)
# 5. 创建 Attachment 记录(user_b),使用相同的 file_url

结果

  • MinIO 中只存储一份文件
  • file_checksums 表中 reference_count = 2
  • 两个 Attachment 记录指向同一个文件

用户 A 删除附件

# 1. 软删除 Attachment 记录(user_a)
# 2. 减少引用计数: reference_count = 1
# 3. 引用计数 > 0,保留 MinIO 文件

用户 B 删除附件

# 1. 软删除 Attachment 记录(user_b)
# 2. 减少引用计数: reference_count = 0
# 3. 引用计数 = 0,删除 MinIO 文件
# 4. 删除 file_checksums 记录

优势

1. 存储空间节省

  • 相同文件只存储一次
  • 大幅减少存储成本
  • 适用于头像、封面等常见场景

2. 上传速度提升

  • 如果文件已存在,跳过上传
  • 仅更新引用计数
  • 用户体验更好

3. 安全性提升

  • 使用预签名 URL 临时访问
  • 支持自定义过期时间
  • 避免文件 URL 泄露

4. 维护性提升

  • 统一的文件存储逻辑
  • 自动管理引用计数
  • 支持定时清理无引用文件

测试验证

1. 代码诊断

✅ server/app/services/attachment_service.py: No diagnostics found

2. 功能测试

测试用例 1:上传新文件

POST /api/v1/attachments/upload
- 上传 avatar.jpg
- 验证文件上传到 MinIO
- 验证 file_checksums 表插入记录
- 验证 Attachment 记录创建

测试用例 2:上传重复文件

POST /api/v1/attachments/upload
- 上传相同的 avatar.jpg
- 验证不重复上传到 MinIO
- 验证 reference_count 增加
- 验证 Attachment 记录创建

测试用例 3:获取下载链接

GET /api/v1/attachments/{id}/download-url
- 验证返回预签名 URL
- 验证 URL 可以访问
- 验证 URL 在过期时间后失效

测试用例 4:删除附件

DELETE /api/v1/attachments/{id}
- 删除 Attachment 记录
- 验证 reference_count 减少
- 如果 reference_count = 0,验证 MinIO 文件删除

配置要求

环境变量

确保 .env 文件包含以下配置:

# MinIO 配置
MINIO_ENDPOINT=localhost:6185
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET_NAME=jointo
MINIO_SECURE=false
MINIO_PUBLIC_URL=http://localhost:6185

# 存储提供商
STORAGE_PROVIDER=minio

Docker Compose

确保 MinIO 服务已启动:

docker-compose up -d jointo-server-minio

后续工作

1. 定时清理任务

创建 Celery 定时任务清理无引用文件:

@celery_app.task(name="cleanup_unused_files")
async def cleanup_unused_files():
    """清理30天未访问且无引用的文件"""
    async with async_session_maker() as db:
        file_storage = FileStorageService(db)
        deleted_count = await file_storage.cleanup_unused_files(days=30)
        logger.info(f"清理了 {deleted_count} 个无引用文件")

2. 单元测试

创建 server/tests/unit/test_attachment_service.py

  • 测试文件上传(去重)
  • 测试预签名 URL 生成
  • 测试文件删除(引用计数)

3. 集成测试

创建 server/tests/integration/test_attachment_api.py

  • 测试完整的上传流程
  • 测试文件去重场景
  • 测试下载链接生成
  • 测试删除流程

4. 性能监控

添加性能监控指标:

  • 文件去重率
  • 存储空间节省
  • 上传速度提升
  • 引用计数分布

文件变更

修改文件

  • server/app/services/attachment_service.py
    • 导入 FileStorageService
    • 初始化 file_storage 实例
    • 集成文件上传逻辑
    • 集成预签名 URL 生成
    • 集成文件删除逻辑

相关文档


变更日期:2026-01-28
集成完成 AttachmentService 已完全集成 FileStorageService