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.
11 KiB
11 KiB
剧本文件存储架构重新设计
文档版本:v1.0
创建日期:2025-01-27
状态:已完成
1. 背景
在初始设计中,剧本文件通过 attachments 表管理,使用 attachments.script_id 字段关联剧本。但这种设计存在以下问题:
- 语义不清晰:剧本文件是剧本的核心内容,不应该作为"附件"存在
- 查询复杂:获取剧本文件需要 JOIN attachments 表
- 设计不一致:videos、project_resources 表都是直接存储文件信息,只有剧本使用附件表
2. 问题分析
2.1 现有设计
-- 剧本表
CREATE TABLE scripts (
script_id BIGINT PRIMARY KEY,
project_id BIGINT NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL, -- 'text' | 'attachment'
content TEXT, -- 文本剧本内容
attachment_id BIGINT REFERENCES attachments(attachment_id), -- 文件剧本关联
...
);
-- 附件表
CREATE TABLE attachments (
attachment_id BIGINT PRIMARY KEY,
script_id BIGINT REFERENCES scripts(script_id), -- 反向关联
file_url TEXT NOT NULL,
...
);
问题:
- 双向关联(scripts.attachment_id + attachments.script_id)
- 剧本文件被当作"附件"处理
- 与 videos、project_resources 表设计不一致
2.2 对比其他表
videos 表:
CREATE TABLE videos (
video_id BIGINT PRIMARY KEY,
video_url TEXT,
file_size BIGINT,
checksum TEXT NOT NULL,
...
);
project_resources 表:
CREATE TABLE project_resources (
project_resource_id BIGINT PRIMARY KEY,
file_url TEXT NOT NULL,
file_size BIGINT,
checksum TEXT NOT NULL,
...
);
结论:videos 和 project_resources 都是直接存储文件信息,不依赖 attachments 表。
3. 方案设计
3.1 方案对比
| 方案 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| 方案A:保持现状 | 继续使用 attachments 表 | 无需改动 | 语义不清晰,设计不一致 |
| 方案B:完全独立 | scripts 表直接存储文件信息,不使用 FileStorageService | 完全独立,无依赖 | 无法复用去重逻辑,代码重复 |
| 方案C:混合方案 | scripts 表直接存储文件信息,但使用 FileStorageService 上传 | 设计一致,复用去重逻辑 | 需要更新代码 |
3.2 最终选择:方案C(混合方案)
理由:
- 设计一致:与 videos、project_resources 表保持一致
- 语义清晰:剧本文件是剧本的核心内容,不是"附件"
- 复用去重:使用 FileStorageService 上传文件,复用去重逻辑
- 查询性能:无需 JOIN attachments 表
4. 实施方案
4.1 数据库变更
4.1.1 更新 scripts 表
-- 移除 attachment_id 字段
ALTER TABLE scripts DROP COLUMN attachment_id;
-- 新增文件字段
ALTER TABLE scripts ADD COLUMN file_url TEXT;
ALTER TABLE scripts ADD COLUMN file_size BIGINT;
ALTER TABLE scripts ADD COLUMN mime_type TEXT;
ALTER TABLE scripts ADD COLUMN checksum TEXT;
ALTER TABLE scripts ADD COLUMN storage_path TEXT;
-- 更新类型枚举
ALTER TYPE script_type RENAME VALUE 'attachment' TO 'file';
-- 新增索引
CREATE INDEX idx_scripts_checksum ON scripts (checksum) WHERE checksum IS NOT NULL;
-- 更新约束
ALTER TABLE scripts DROP CONSTRAINT IF EXISTS scripts_content_check;
ALTER TABLE scripts ADD CONSTRAINT scripts_content_check CHECK (
(type = 'text' AND content IS NOT NULL AND file_url IS NULL) OR
(type = 'file' AND file_url IS NOT NULL AND content IS NULL)
);
4.1.2 更新 attachments 表
-- 移除 script_id 字段
ALTER TABLE attachments DROP COLUMN script_id;
-- 删除相关索引
DROP INDEX IF EXISTS idx_attachments_script_id;
4.2 代码变更
4.2.1 ScriptService 新增方法
async def create_script_from_file(
self,
user_id: int,
project_id: int,
name: str,
file: UploadFile
) -> Script:
"""创建文件剧本"""
# 1. 验证文件类型(支持 TXT、DOC、DOCX、PDF、RTF、Markdown)
if file.content_type not in self.ALLOWED_FILE_TYPES:
raise ValidationError(f"不支持的文件类型: {file.content_type}")
# 2. 读取文件内容
content = await file.read()
file_size = len(content)
# 3. 验证文件大小
if file_size > self.MAX_FILE_SIZE:
raise ValidationError("文件大小超过限制")
# 4. 使用 FileStorageService 上传(带去重)
file_meta = await self.file_storage.upload_file(
file_content=content,
filename=file.filename,
content_type=file.content_type,
category='script',
user_id=user_id
)
# 5. 创建剧本记录
script = Script(
project_id=project_id,
name=name,
type='file',
file_url=file_meta.file_url,
file_size=file_meta.file_size,
mime_type=file.content_type,
checksum=file_meta.checksum,
storage_path=file_meta.storage_path,
status='draft',
created_by=user_id,
updated_by=user_id
)
return await self.repository.create(script)
4.2.2 Script 模型更新
class ScriptType(str, enum.Enum):
TEXT = "text"
FILE = "file" # 改名:attachment -> file
class Script(Base):
__tablename__ = "scripts"
script_id = Column(Integer, primary_key=True)
project_id = Column(Integer, ForeignKey('projects.id'), nullable=False)
name = Column(String(255), nullable=False)
type = Column(Enum(ScriptType), nullable=False)
# 文本剧本字段
content = Column(Text)
# 文件剧本字段(新增)
file_url = Column(String(500))
file_size = Column(BigInteger)
mime_type = Column(String(100))
checksum = Column(String(64))
storage_path = Column(String(500))
# 移除 attachment_id
# attachment_id = Column(Integer, ForeignKey('attachments.id'))
4.3 API 变更
4.3.1 新增接口
POST /api/v1/scripts/upload
请求(multipart/form-data):
project_id: 项目 ID(必填)name: 剧本名称(必填)file: 剧本文件(必填)
响应:
{
"script_id": 2,
"project_id": 1,
"name": "第一集剧本",
"type": "file",
"file_url": "https://storage.jointo.ai/scripts/1/abc123.pdf",
"file_size": 1024000,
"mime_type": "application/pdf",
"checksum": "abc123...",
"version": 1,
"status": "draft",
"created_at": "2025-01-27T10:00:00Z"
}
4.3.2 移除接口
从 AttachmentService 移除:
# 移除此方法
async def get_attachments_by_script(
self,
user_id: int,
script_id: int,
page: int = 1,
page_size: int = 20
) -> Dict[str, Any]:
...
5. 优势总结
5.1 设计一致性
| 表名 | 文件存储方式 | 去重机制 |
|---|---|---|
| videos | 直接存储 file_url, checksum | FileStorageService |
| project_resources | 直接存储 file_url, checksum | FileStorageService |
| scripts | 直接存储 file_url, checksum | FileStorageService |
| attachments | 直接存储 file_url, checksum | FileStorageService |
结论:所有表都使用相同的设计模式,便于维护。
5.2 语义清晰
- 剧本文件:scripts 表自己管理(核心内容)
- 项目素材:project_resources 表管理(角色、场景、道具)
- 项目附件:attachments 表管理(合同、参考资料、设计稿)
5.3 查询性能
旧设计:
-- 获取剧本文件需要 JOIN
SELECT s.*, a.file_url, a.file_size
FROM scripts s
LEFT JOIN attachments a ON s.attachment_id = a.attachment_id
WHERE s.script_id = 1;
新设计:
-- 直接查询,无需 JOIN
SELECT * FROM scripts WHERE script_id = 1;
5.4 去重复用
所有表都使用 FileStorageService 上传文件,统一通过 file_checksums 表去重:
# 统一的上传流程
file_meta = await self.file_storage.upload_file(
file_content=content,
filename=file.filename,
content_type=file.content_type,
category='script', # 或 'video', 'resource', 'attachment'
user_id=user_id
)
6. 数据迁移
6.1 迁移脚本
-- 1. 备份数据
CREATE TABLE scripts_backup AS SELECT * FROM scripts;
CREATE TABLE attachments_backup AS SELECT * FROM attachments;
-- 2. 迁移剧本文件数据
UPDATE scripts s
SET
file_url = a.file_url,
file_size = a.file_size,
mime_type = a.mime_type,
checksum = a.checksum,
storage_path = a.storage_path
FROM attachments a
WHERE s.attachment_id = a.attachment_id
AND s.type = 'attachment';
-- 3. 更新类型枚举
UPDATE scripts SET type = 'file' WHERE type = 'attachment';
-- 4. 删除旧字段
ALTER TABLE scripts DROP COLUMN attachment_id;
ALTER TABLE attachments DROP COLUMN script_id;
-- 5. 验证数据
SELECT COUNT(*) FROM scripts WHERE type = 'file' AND file_url IS NULL; -- 应该为 0
6.2 回滚方案
-- 如果需要回滚
DROP TABLE scripts;
DROP TABLE attachments;
CREATE TABLE scripts AS SELECT * FROM scripts_backup;
CREATE TABLE attachments AS SELECT * FROM attachments_backup;
7. 影响范围
7.1 受影响的文档
- ✅
docs/需求/database-design.md:已更新 scripts 表结构 - ✅
docs/需求/backend/04-services/script-service.md:已更新服务实现和 API - ✅
docs/需求/backend/04-services/attachment-service.md:已移除 script_id 相关内容
7.2 受影响的代码
- ✅
app/models/script.py:Script 模型 - ✅
app/services/script_service.py:ScriptService 类 - ✅
app/services/attachment_service.py:AttachmentService 类 - ✅
app/schemas/script.py:ScriptCreate、ScriptResponse 等 - ✅
app/api/v1/scripts.py:API 路由
7.3 受影响的测试
tests/services/test_script_service.py:需要更新测试用例tests/api/test_scripts.py:需要更新 API 测试
8. 总结
通过此次重构,我们实现了:
- 设计一致性:scripts、videos、project_resources 表使用相同的文件存储模式
- 语义清晰:剧本文件不再作为"附件",而是剧本的核心内容
- 性能优化:无需 JOIN attachments 表,查询更快
- 代码复用:统一使用 FileStorageService 实现文件上传和去重
此次调整符合 DRY(Don't Repeat Yourself)和 SOLID 原则,提升了系统的可维护性和扩展性。
文档版本:v1.0
创建日期:2025-01-27
状态:已完成