# 剧本表结构创建 > **变更日期**:2026-02-03 > **变更类型**:数据库迁移 > **影响范围**:后端数据库、剧本管理功能 --- ## 变更概述 创建剧本管理相关的数据库表结构,支持剧本文件上传、解析和元素标签管理。 ## 变更内容 ### 1. 新增表结构 #### 1.1 screenplays 表 剧本主表,存储剧本基本信息和内容。 **核心字段**: | 字段名 | 类型 | 说明 | |--------|------|------| | `screenplay_id` | UUID | 主键(UUID v7) | | `project_id` | UUID | 所属项目 ID | | `name` | TEXT | 剧本名称 | | `type` | SMALLINT | 剧本类型:1=file, 2=manual | | `file_url` | TEXT | 文件 URL(type=1 时) | | `file_name` | TEXT | 原始文件名 | | `file_size` | BIGINT | 文件大小(字节) | | `mime_type` | TEXT | 文件 MIME 类型 | | `content` | TEXT | 剧本文本内容(Markdown) | | `word_count` | INTEGER | 字数统计 | | `parsing_status` | SMALLINT | 解析状态:0=idle, 1=pending, 2=parsing, 3=completed, 4=failed | | `parsing_error` | TEXT | 解析错误信息 | | `parsed_at` | TIMESTAMPTZ | 解析完成时间 | | `display_order` | INTEGER | 显示顺序 | | `created_at` | TIMESTAMPTZ | 创建时间 | | `updated_at` | TIMESTAMPTZ | 更新时间 | | `deleted_at` | TIMESTAMPTZ | 软删除时间 | **索引**: - `idx_screenplays_project_id` - 项目查询 - `idx_screenplays_type` - 类型筛选 - `idx_screenplays_parsing_status` - 解析状态查询(部分索引) - `idx_screenplays_order` - 排序查询 - `idx_screenplays_deleted` - 软删除过滤(部分索引) - `idx_screenplays_name_trgm` - 名称全文搜索(GIN) - `idx_screenplays_content_trgm` - 内容全文搜索(GIN) **约束**: - `screenplays_type_check` - 类型范围:1-2 - `screenplays_parsing_status_check` - 解析状态范围:0-4 - `screenplays_word_count_check` - 字数非负 - `screenplays_content_check` - 内容完整性:file 类型必须有 file_url,manual 类型必须有 content **触发器**: - `update_screenplays_updated_at` - 自动更新 updated_at 字段 #### 1.2 screenplay_tags 表 剧本元素标签表,存储剧本中的角色、场景、道具等元素及其标签。 **核心字段**: | 字段名 | 类型 | 说明 | |--------|------|------| | `tag_id` | UUID | 主键(UUID v7) | | `screenplay_id` | UUID | 所属剧本 ID | | `element_type` | SMALLINT | 元素类型:1=character, 2=scene, 3=prop, 4=sound, 5=other | | `element_name` | TEXT | 元素名称(如"孙悟空"、"咖啡厅") | | `tag_label` | TEXT | 标签名称(如"少年"、"白天") | | `description` | TEXT | 元素描述 | | `metadata` | JSONB | 扩展字段 | | `display_order` | INTEGER | 显示顺序 | | `created_at` | TIMESTAMPTZ | 创建时间 | | `updated_at` | TIMESTAMPTZ | 更新时间 | **索引**: - `idx_screenplay_tags_screenplay_id` - 剧本查询 - `idx_screenplay_tags_element_type` - 元素类型筛选 - `idx_screenplay_tags_element_name` - 元素名称查询 - `idx_screenplay_tags_order` - 排序查询 - `idx_screenplay_tags_metadata_gin` - JSONB 字段查询(GIN) - `idx_screenplay_tags_element_name_trgm` - 元素名称全文搜索(GIN) - `idx_screenplay_tags_tag_label_trgm` - 标签名称全文搜索(GIN) **约束**: - `screenplay_tags_element_type_check` - 元素类型范围:1-5 - `screenplay_tags_unique` - 唯一约束:(screenplay_id, element_type, element_name, tag_label) **触发器**: - `update_screenplay_tags_updated_at` - 自动更新 updated_at 字段 ### 2. 设计原则 #### 2.1 符合 jointo-tech-stack 规范 - ✅ **UUID v7**:所有主键使用 UUID v7(应用层生成) - ✅ **TIMESTAMPTZ**:所有时间字段使用 `TIMESTAMP(timezone=True)` - ✅ **SMALLINT 枚举**:类型字段使用 SMALLINT 存储枚举值 - ✅ **应用层外键**:无物理外键约束,应用层保证引用完整性 - ✅ **索引策略**:合理使用 B-Tree、GIN、部分索引 - ✅ **触发器**:自动更新 updated_at 字段 #### 2.2 文件解析状态管理 支持完整的文件解析生命周期: ``` idle (0) → pending (1) → parsing (2) → completed (3) ↘ failed (4) ``` - **idle**:未解析(TXT/Markdown 直接读取后跳过此状态) - **pending**:待解析(任务已提交,等待 Celery Worker 处理) - **parsing**:解析中(Worker 正在处理) - **completed**:解析完成(content 字段已填充) - **failed**:解析失败(parsing_error 字段记录错误信息) #### 2.3 多种剧本类型支持 - **type=1 (file)**:上传文件类型 - 支持格式:TXT、Markdown、DOCX、PDF、RTF、DOC - 必须字段:file_url, file_name, mime_type - 解析后填充:content, word_count - **type=2 (manual)**:手动输入类型 - 用户直接输入剧本文本 - 必须字段:content - 自动计算:word_count #### 2.4 全文搜索支持 使用 PostgreSQL 的 `pg_trgm` 扩展实现全文搜索: - **screenplays.name** - 剧本名称搜索 - **screenplays.content** - 剧本内容搜索 - **screenplay_tags.element_name** - 元素名称搜索 - **screenplay_tags.tag_label** - 标签名称搜索 ### 3. 迁移文件 **文件路径**:`server/alembic/versions/20260203_1200_create_screenplays_tables.py` **Revision ID**:`20260203_1200` **依赖**:`20260203_1100` (create_storyboard_resources_tables) ## 使用示例 ### 1. 创建文件类型剧本 ```python from uuid import uuid7 from datetime import datetime, UTC screenplay = { 'screenplay_id': uuid7(), 'project_id': project_id, 'name': '第一集剧本', 'type': 1, # file 'file_url': 'https://storage.jointo.ai/screenplays/1/abc123.pdf', 'file_name': '第一集剧本.pdf', 'file_size': 1024000, 'mime_type': 'application/pdf', 'parsing_status': 1, # pending 'display_order': 0, 'created_at': datetime.now(UTC), 'updated_at': datetime.now(UTC) } ``` ### 2. 更新解析状态 ```python # 解析完成 await screenplay_repo.update(screenplay_id, { 'content': '# 第一场 咖啡厅 下午\n\n张三坐在咖啡厅里...', 'word_count': 5000, 'parsing_status': 3, # completed 'parsed_at': datetime.now(UTC) }) # 解析失败 await screenplay_repo.update(screenplay_id, { 'parsing_status': 4, # failed 'parsing_error': 'PDF 文件损坏,无法解析' }) ``` ### 3. 创建剧本元素标签 ```python # 角色标签 character_tag = { 'tag_id': uuid7(), 'screenplay_id': screenplay_id, 'element_type': 1, # character 'element_name': '孙悟空', 'tag_label': '少年', 'description': '主角,活泼好动', 'metadata': { 'appearance': '金色头发,红色衣服', 'personality': '勇敢、正义', 'age': '16' }, 'display_order': 0, 'created_at': datetime.now(UTC), 'updated_at': datetime.now(UTC) } # 场景标签 scene_tag = { 'tag_id': uuid7(), 'screenplay_id': screenplay_id, 'element_type': 2, # scene 'element_name': '咖啡厅', 'tag_label': '白天', 'description': '温馨的咖啡厅', 'metadata': { 'location_type': 'indoor', 'lighting': 'natural', 'atmosphere': 'cozy' }, 'display_order': 1, 'created_at': datetime.now(UTC), 'updated_at': datetime.now(UTC) } ``` ### 4. 全文搜索 ```sql -- 搜索剧本名称 SELECT * FROM screenplays WHERE name % '第一集' ORDER BY similarity(name, '第一集') DESC; -- 搜索剧本内容 SELECT * FROM screenplays WHERE content % '咖啡厅' ORDER BY similarity(content, '咖啡厅') DESC; -- 搜索元素名称 SELECT * FROM screenplay_tags WHERE element_name % '孙悟空' ORDER BY similarity(element_name, '孙悟空') DESC; ``` ## 后续工作 ### 1. 创建 Model 类 - [ ] `server/app/models/screenplay.py` - Screenplay 模型 - [ ] `server/app/models/screenplay_tag.py` - ScreenplayTag 模型 ### 2. 创建 Repository 类 - [ ] `server/app/repositories/screenplay_repository.py` - [ ] `server/app/repositories/screenplay_tag_repository.py` ### 3. 创建 Schema 类 - [ ] `server/app/schemas/screenplay.py` - 请求/响应 Schema ### 4. 完善 Service 类 - [ ] 补充 `ScreenplayFileParserService` 的数据库操作 - [ ] 创建 `ScreenplayService` - 剧本管理服务 - [ ] 创建 `ScreenplayTagService` - 标签管理服务 ### 5. 创建 API 路由 - [ ] `server/app/api/v1/screenplays.py` - 剧本 CRUD API - [ ] `server/app/api/v1/screenplay_tags.py` - 标签 CRUD API ### 6. 创建 Celery 任务 - [ ] `server/app/tasks/screenplay_tasks.py` - 文件解析异步任务 ## 测试建议 ### 1. 数据库迁移测试 ```bash # 升级 docker exec jointo-server-app alembic upgrade head # 验证表结构 docker exec jointo-server-postgres psql -U jointoAI -d jointo -c "\d screenplays" docker exec jointo-server-postgres psql -U jointoAI -d jointo -c "\d screenplay_tags" # 降级测试 docker exec jointo-server-app alembic downgrade -1 docker exec jointo-server-app alembic upgrade head ``` ### 2. 约束测试 ```sql -- 测试类型约束 INSERT INTO screenplays (screenplay_id, project_id, name, type) VALUES (gen_random_uuid(), gen_random_uuid(), 'Test', 3); -- 应失败 -- 测试内容完整性约束 INSERT INTO screenplays (screenplay_id, project_id, name, type) VALUES (gen_random_uuid(), gen_random_uuid(), 'Test', 1); -- 应失败(缺少 file_url) -- 测试唯一约束 INSERT INTO screenplay_tags (tag_id, screenplay_id, element_type, element_name, tag_label) VALUES (gen_random_uuid(), 'xxx', 1, '孙悟空', '少年'), (gen_random_uuid(), 'xxx', 1, '孙悟空', '少年'); -- 应失败(重复) ``` ### 3. 索引性能测试 ```sql -- 测试全文搜索性能 EXPLAIN ANALYZE SELECT * FROM screenplays WHERE name % '第一集'; -- 测试 JSONB 查询性能 EXPLAIN ANALYZE SELECT * FROM screenplay_tags WHERE metadata @> '{"age": "16"}'; ``` ## 相关文档 - [剧本文件解析服务](../../requirements/backend/04-services/project/screenplay-file-parser-service.md) - [数据库设计规范](../../requirements/database-design.md) - [jointo-tech-stack 规范](.claude/skills/jointo-tech-stack/SKILL.md) --- **变更日期**:2026-02-03 **迁移文件**:`20260203_1200_create_screenplays_tables.py` **状态**:✅ 已完成