# AttachmentService 集成 FileStorageService > **日期**:2026-01-28 > **类型**:功能集成 > **影响范围**:AttachmentService --- ## 变更概述 将 FileStorageService 集成到 AttachmentService 中,替换临时文件存储逻辑,实现文件去重、引用计数管理和预签名 URL 生成。 --- ## 集成内容 ### 1. 初始化 FileStorageService **修改前**: ```python class AttachmentService: def __init__(self, session: AsyncSession): self.repository = AttachmentRepository(session) self.session = session ``` **修改后**: ```python 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. 文件上传集成 **修改前(临时实现)**: ```python # 暂时使用简单的文件存储逻辑 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)**: ```python # 使用 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. 存储提供商更新 **修改前**: ```python attachment = Attachment( # ... storage_provider='local', # 临时使用 local storage_path=storage_path ) ``` **修改后**: ```python attachment = Attachment( # ... storage_provider=file_meta.storage_provider, # 从 FileStorageService 获取 storage_path=storage_path ) ``` ### 4. 预签名 URL 生成 **修改前(临时实现)**: ```python # 暂时返回文件 URL return attachment.file_url ``` **修改后(FileStorageService)**: ```python # 生成预签名URL return await self.file_storage.get_presigned_url( attachment.storage_path, expires=expires ) ``` **关键变更**: - 使用 `FileStorageService.get_presigned_url()` 生成临时访问链接 - 支持自定义过期时间(默认 3600 秒) - 返回 MinIO 预签名 URL ### 5. 文件删除集成 **修改前(临时实现)**: ```python # 软删除数据库记录 await self.repository.soft_delete(attachment_id) # TODO: 减少文件引用计数 # await self.file_storage.decrease_reference_count(attachment.checksum) ``` **修改后(FileStorageService)**: ```python # 软删除数据库记录 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**: ```python # 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**: ```python # 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 删除附件**: ```python # 1. 软删除 Attachment 记录(user_a) # 2. 减少引用计数: reference_count = 1 # 3. 引用计数 > 0,保留 MinIO 文件 ``` **用户 B 删除附件**: ```python # 1. 软删除 Attachment 记录(user_b) # 2. 减少引用计数: reference_count = 0 # 3. 引用计数 = 0,删除 MinIO 文件 # 4. 删除 file_checksums 记录 ``` --- ## 优势 ### 1. 存储空间节省 - 相同文件只存储一次 - 大幅减少存储成本 - 适用于头像、封面等常见场景 ### 2. 上传速度提升 - 如果文件已存在,跳过上传 - 仅更新引用计数 - 用户体验更好 ### 3. 安全性提升 - 使用预签名 URL 临时访问 - 支持自定义过期时间 - 避免文件 URL 泄露 ### 4. 维护性提升 - 统一的文件存储逻辑 - 自动管理引用计数 - 支持定时清理无引用文件 --- ## 测试验证 ### 1. 代码诊断 ```bash ✅ server/app/services/attachment_service.py: No diagnostics found ``` ### 2. 功能测试 **测试用例 1:上传新文件** ```bash POST /api/v1/attachments/upload - 上传 avatar.jpg - 验证文件上传到 MinIO - 验证 file_checksums 表插入记录 - 验证 Attachment 记录创建 ``` **测试用例 2:上传重复文件** ```bash POST /api/v1/attachments/upload - 上传相同的 avatar.jpg - 验证不重复上传到 MinIO - 验证 reference_count 增加 - 验证 Attachment 记录创建 ``` **测试用例 3:获取下载链接** ```bash GET /api/v1/attachments/{id}/download-url - 验证返回预签名 URL - 验证 URL 可以访问 - 验证 URL 在过期时间后失效 ``` **测试用例 4:删除附件** ```bash DELETE /api/v1/attachments/{id} - 删除 Attachment 记录 - 验证 reference_count 减少 - 如果 reference_count = 0,验证 MinIO 文件删除 ``` --- ## 配置要求 ### 环境变量 确保 `.env` 文件包含以下配置: ```bash # 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 服务已启动: ```bash docker-compose up -d jointo-server-minio ``` --- ## 后续工作 ### 1. 定时清理任务 创建 Celery 定时任务清理无引用文件: ```python @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 生成 - 集成文件删除逻辑 --- ## 相关文档 - [FileStorageService 实现](./2026-01-28-file-storage-service-implementation.md) - [AttachmentService 实现](./2026-01-28-attachment-service-implementation.md) - [FileStorageService 规范](../../requirements/backend/04-services/resource/file-storage-service.md) - [AttachmentService 规范](../../requirements/backend/04-services/resource/attachment-service.md) --- **变更日期**:2026-01-28 **集成完成**:✅ AttachmentService 已完全集成 FileStorageService