# Jointo(jointo)PostgreSQL 数据库设计文档 > **项目名称**:Jointo(jointo) > **数据库**:PostgreSQL 14+ > **文档版本**:v1.2 > **创建日期**:2025-01-27 --- ## 目录 1. [设计原则](#1-设计原则) 2. [核心实体关系图](#2-核心实体关系图) 3. [数据表设计](#3-数据表设计) 4. [索引设计](#4-索引设计) 5. [约束与触发器](#5-约束与触发器) 6. [性能优化策略](#6-性能优化策略) 7. [数据迁移策略](#7-数据迁移策略) --- ## 1. 设计原则 ### 1.1 遵循 PostgreSQL 最佳实践 - ✅ 使用 `BIGINT GENERATED ALWAYS AS IDENTITY` 作为主键 - ✅ 使用 `TIMESTAMPTZ` 存储时间戳 - ✅ 使用 `TEXT` 而非 `VARCHAR(n)` - ✅ 使用 `NUMERIC` 存储精确数值(时间、金额) - ✅ 使用 `JSONB` 存储灵活配置数据 - ✅ 为所有外键创建索引 - ✅ 使用 `NOT NULL` 约束确保数据完整性 - ✅ 使用 `CHECK` 约束验证业务规则 ### 1.2 规范化设计 - **第三范式(3NF)**:消除数据冗余 - **关联表**:多对多关系使用关联表 - **软删除**:使用 `deleted_at` 而非物理删除 - **审计字段**:`created_at`, `updated_at`, `created_by`, `updated_by` ### 1.3 扩展性考虑 - **JSONB 字段**:存储灵活配置,便于扩展 - **版本控制**:支持项目版本管理 - **多租户**:支持未来 SaaS 扩展 - **分区策略**:大表考虑时间分区 --- ## 2. 核心实体关系图 ``` ┌─────────┐ ┌──────────────┐ ┌─────────────┐ │ Users │────────<│ProjectMembers│>────────│ Projects │ └─────────┘ └──────────────┘ └─────────────┘ │ │ │ │ │ │ │ ├──────────┐ │ │ │ │ ▼ │ │ ┌──────────┐ │ │ │Storyboards│ │ │ └──────────┘ │ │ │ │ │ │ │ │ ┌─────────────────────────┼──────────┘ │ │ │ │ ▼ ▼ │ ┌──────────┐ ┌──────────────┐ │ │Resources │<───────────│Storyboard │ │ └──────────┘ │Resources │ │ │ └──────────────┘ │ │ │ │ │ ┌──────────┐ │ │ Videos │ │ └──────────┘ │ │ │ │ │ ┌──────────┐ │ │SoundEffects│ │ └──────────┘ │ │ │ │ │ ┌──────────┐ │ │ Subtitles│ │ └──────────┘ │ │ │ │ │ ┌──────────┐ │ │Voiceovers│ │ └──────────┘ │ │ │ │ │ ┌──────────┐ │ │TimelineTracks│ │ └──────────┘ │ │ │ │ │ ┌──────────┐ │ │ Scripts │◄──────┐ │ └──────────┘ │ │ │ │ │ │ │ │ ┌──────────┐ │ │ │Attachments│──────┘ │ └──────────┘ │ │ │ │ │ ┌──────────┐ │ │TimelineItems│ │ └──────────┘ │ │ ├──────────┐ │ │ │ ▼ │ ┌──────────┐ │ │ AIJobs │ │ └──────────┘ │ │ ├──────────┐ │ │ │ ▼ │ ┌──────────┐ │ │Comments │ │ └──────────┘ │ │ └──────────┐ │ ▼ ┌──────────┐ │Notifications│ └──────────┘ ``` --- ## 3. 数据表设计 ### 3.1 用户与认证 #### 3.1.1 users(用户表) ```sql CREATE TABLE users ( user_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, email TEXT NOT NULL, username TEXT NOT NULL, password_hash TEXT NOT NULL, avatar_url TEXT, subscription_tier TEXT NOT NULL DEFAULT 'free' CHECK (subscription_tier IN ('free', 'pro', 'enterprise')), subscription_expires_at TIMESTAMPTZ, ai_credits_remaining INTEGER NOT NULL DEFAULT 10, ai_credits_total INTEGER NOT NULL DEFAULT 10, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ, CONSTRAINT users_email_unique UNIQUE (email) NULLS NOT DISTINCT, CONSTRAINT users_username_unique UNIQUE (username) NULLS NOT DISTINCT ); -- 索引 CREATE UNIQUE INDEX idx_users_email_lower ON users (LOWER(email)) WHERE deleted_at IS NULL; CREATE INDEX idx_users_username_lower ON users (LOWER(username)) WHERE deleted_at IS NULL; CREATE INDEX idx_users_subscription_tier ON users (subscription_tier) WHERE deleted_at IS NULL; CREATE INDEX idx_users_created_at ON users (created_at) WHERE deleted_at IS NULL; -- 更新时间触发器 CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` **设计说明**: - 使用 `BIGINT GENERATED ALWAYS AS IDENTITY` 作为主键 - 邮箱和用户名使用 `LOWER()` 索引实现大小写不敏感 - 软删除使用 `deleted_at` - 订阅信息存储在用户表中,便于快速查询 - AI 积分系统支持按量付费 #### 3.1.2 user_sessions(用户会话表) ```sql CREATE TABLE user_sessions ( session_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, token TEXT NOT NULL UNIQUE, refresh_token TEXT, expires_at TIMESTAMPTZ NOT NULL, ip_address INET, user_agent TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_used_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_user_sessions_user_id ON user_sessions (user_id); CREATE INDEX idx_user_sessions_token ON user_sessions (token); CREATE INDEX idx_user_sessions_expires_at ON user_sessions (expires_at); ``` ### 3.2 文件夹管理 #### 3.2.1 folders(文件夹表) ```sql CREATE TABLE folders ( folder_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL, description TEXT, parent_folder_id BIGINT REFERENCES folders(folder_id) ON DELETE CASCADE, owner_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, path TEXT, -- 缓存路径,如 "/folder1/folder2/folder3" level INTEGER NOT NULL DEFAULT 0 CHECK (level >= 0 AND level <= 10), -- 层级深度,限制最大10层 sort_order INTEGER NOT NULL DEFAULT 0, -- 同级排序 color TEXT, -- 文件夹颜色(十六进制) icon TEXT, -- 文件夹图标名称 is_shared BOOLEAN NOT NULL DEFAULT false, -- 是否共享文件夹 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ, -- 确保同一父文件夹下名称唯一 CONSTRAINT folders_name_unique UNIQUE (parent_folder_id, name) NULLS NOT DISTINCT ); -- 索引 CREATE INDEX idx_folders_parent_folder_id ON folders (parent_folder_id) WHERE deleted_at IS NULL; CREATE INDEX idx_folders_owner_id ON folders (owner_id) WHERE deleted_at IS NULL; CREATE INDEX idx_folders_path ON folders (path) WHERE deleted_at IS NULL AND path IS NOT NULL; CREATE INDEX idx_folders_level ON folders (level) WHERE deleted_at IS NULL; CREATE INDEX idx_folders_sort_order ON folders (parent_folder_id, sort_order) WHERE deleted_at IS NULL; CREATE INDEX idx_folders_is_shared ON folders (is_shared) WHERE deleted_at IS NULL AND is_shared = true; -- 全文搜索索引 CREATE INDEX idx_folders_name_trgm ON folders USING GIN (name gin_trgm_ops) WHERE deleted_at IS NULL; -- 触发器 CREATE TRIGGER update_folders_updated_at BEFORE UPDATE ON folders FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- 路径更新触发器 CREATE OR REPLACE FUNCTION update_folder_path() RETURNS TRIGGER AS $$ DECLARE parent_path TEXT; BEGIN -- 如果是根文件夹 IF NEW.parent_folder_id IS NULL THEN NEW.path = '/' || NEW.name; NEW.level = 0; ELSE -- 获取父文件夹路径和层级 SELECT f.path, f.level INTO parent_path, NEW.level FROM folders f WHERE f.folder_id = NEW.parent_folder_id; NEW.path = parent_path || '/' || NEW.name; NEW.level = NEW.level + 1; -- 检查层级限制 IF NEW.level > 10 THEN RAISE EXCEPTION '文件夹层级不能超过10层'; END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER update_folder_path_trigger BEFORE INSERT OR UPDATE ON folders FOR EACH ROW EXECUTE FUNCTION update_folder_path(); ``` **设计说明**: - 使用邻接列表模式存储树形结构 - `path` 字段缓存完整路径,提升查询性能 - `level` 字段记录层级深度,限制最大10层 - 支持文件夹颜色和图标自定义 - 同一父文件夹下名称唯一约束 #### 3.2.2 folder_members(文件夹成员表) ```sql CREATE TABLE folder_members ( folder_member_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, folder_id BIGINT NOT NULL REFERENCES folders(folder_id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, role member_role NOT NULL DEFAULT 'viewer', inherited BOOLEAN NOT NULL DEFAULT false, -- 是否从父文件夹继承权限 invited_by BIGINT REFERENCES users(user_id), joined_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT folder_members_unique UNIQUE (folder_id, user_id) NULLS NOT DISTINCT ); -- 索引 CREATE INDEX idx_folder_members_folder_id ON folder_members (folder_id); CREATE INDEX idx_folder_members_user_id ON folder_members (user_id); CREATE INDEX idx_folder_members_role ON folder_members (role); CREATE INDEX idx_folder_members_inherited ON folder_members (inherited); ``` **设计说明**: - 支持文件夹级别的协作权限 - `inherited` 字段标记是否继承父文件夹权限 - 权限角色复用项目的 `member_role` 枚举 ### 3.3 项目管理 #### 3.3.1 projects(项目表) ```sql CREATE TYPE project_type AS ENUM ('mine', 'collab'); CREATE TABLE projects ( project_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL, description TEXT, type project_type NOT NULL DEFAULT 'mine', owner_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE RESTRICT, folder_id BIGINT REFERENCES folders(folder_id) ON DELETE SET NULL, -- 所属文件夹 thumbnail_url TEXT, settings JSONB NOT NULL DEFAULT '{}' CHECK (jsonb_typeof(settings) = 'object'), -- settings 结构: {"resolution": {"width": 1920, "height": 1080}, "frameRate": 30, "duration": 60, "exportFormat": "mp4", "exportQuality": "high"} status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived', 'deleted')), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ, -- 确保同一文件夹下项目名称唯一 CONSTRAINT projects_name_unique UNIQUE (folder_id, name) NULLS NOT DISTINCT ); -- 索引 CREATE INDEX idx_projects_owner_id ON projects (owner_id) WHERE deleted_at IS NULL; CREATE INDEX idx_projects_folder_id ON projects (folder_id) WHERE deleted_at IS NULL AND folder_id IS NOT NULL; CREATE INDEX idx_projects_type ON projects (type) WHERE deleted_at IS NULL; CREATE INDEX idx_projects_status ON projects (status) WHERE deleted_at IS NULL; CREATE INDEX idx_projects_created_at ON projects (created_at) WHERE deleted_at IS NULL; CREATE INDEX idx_projects_updated_at ON projects (updated_at) WHERE deleted_at IS NULL; -- JSONB 索引(用于查询设置) CREATE INDEX idx_projects_settings_gin ON projects USING GIN (settings); -- 全文搜索索引 CREATE INDEX idx_projects_name_trgm ON projects USING GIN (name gin_trgm_ops) WHERE deleted_at IS NULL; -- 触发器 CREATE TRIGGER update_projects_updated_at BEFORE UPDATE ON projects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` **设计说明**: - 新增 `folder_id` 字段关联文件夹 - 项目可以不属于任何文件夹(根目录项目) - 同一文件夹下项目名称唯一约束 **设计说明**: - 使用 `ENUM` 类型定义项目类型 - `settings` 使用 JSONB 存储灵活配置 - 使用 GIN 索引加速 JSONB 查询 - 软删除支持 #### 3.3.2 project_members(项目成员表) ```sql CREATE TYPE member_role AS ENUM ('owner', 'editor', 'viewer'); CREATE TABLE project_members ( member_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, role member_role NOT NULL DEFAULT 'viewer', invited_by BIGINT REFERENCES users(user_id), joined_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT project_members_unique UNIQUE (project_id, user_id) NULLS NOT DISTINCT ); -- 索引 CREATE INDEX idx_project_members_project_id ON project_members (project_id); CREATE INDEX idx_project_members_user_id ON project_members (user_id); CREATE INDEX idx_project_members_role ON project_members (role); ``` **设计说明**: - 使用 `ENUM` 定义角色类型 - 唯一约束确保用户不会重复加入同一项目 - 记录邀请人信息 #### 3.3.3 project_versions(项目版本表) ```sql CREATE TABLE project_versions ( version_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, version_number INTEGER NOT NULL, snapshot_data JSONB NOT NULL, -- 完整的项目快照数据 created_by BIGINT NOT NULL REFERENCES users(user_id), note TEXT, -- 版本备注 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT project_versions_unique UNIQUE (project_id, version_number) ); -- 索引 CREATE INDEX idx_project_versions_project_id ON project_versions (project_id); CREATE INDEX idx_project_versions_created_at ON project_versions (created_at); CREATE INDEX idx_project_versions_snapshot_gin ON project_versions USING GIN (snapshot_data); ``` **设计说明**: - 版本号在项目内唯一 - 使用 JSONB 存储完整快照,便于恢复 - GIN 索引支持快照数据查询 ### 3.4 分镜管理 #### 3.4.1 storyboards(分镜表) ```sql CREATE TABLE storyboards ( storyboard_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, title TEXT NOT NULL, description TEXT NOT NULL, thumbnail_url TEXT, order_index INTEGER NOT NULL, -- 分镜顺序 start_time NUMERIC(10, 3) NOT NULL DEFAULT 0, -- 开始时间(秒,支持毫秒) end_time NUMERIC(10, 3) NOT NULL, -- 结束时间(秒) transition_type TEXT, -- 转场类型 transition_duration NUMERIC(5, 2), -- 转场时长(秒) created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT storyboards_order_unique UNIQUE (project_id, order_index) DEFERRABLE INITIALLY DEFERRED, CONSTRAINT storyboards_time_check CHECK (end_time > start_time) ); -- 索引 CREATE INDEX idx_storyboards_project_id ON storyboards (project_id); CREATE INDEX idx_storyboards_order ON storyboards (project_id, order_index); CREATE INDEX idx_storyboards_time_range ON storyboards USING GIST (numrange(start_time, end_time)); -- 触发器 CREATE TRIGGER update_storyboards_updated_at BEFORE UPDATE ON storyboards FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` **设计说明**: - 使用 `NUMERIC(10, 3)` 精确存储时间(支持毫秒) - 使用 `DEFERRABLE INITIALLY DEFERRED` 处理顺序调整时的唯一约束冲突 - 使用 GiST 索引支持时间范围查询 - 转场信息存储在分镜表中 ### 3.5 资源管理 #### 3.5.1 project_resources(项目素材表) ```sql CREATE TYPE resource_type AS ENUM ('character', 'scene', 'prop', 'real'); CREATE TABLE project_resources ( project_resource_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, name TEXT NOT NULL, type resource_type NOT NULL, -- 文件信息 file_url TEXT NOT NULL, thumbnail_url TEXT, file_size BIGINT, -- 文件大小(字节) mime_type TEXT, width INTEGER, -- 图片宽度 height INTEGER, -- 图片高度 checksum TEXT NOT NULL, -- 文件校验和(SHA256) -- 来源(可选,后期扩展) source_resource_id BIGINT, -- 如果是从资源库复制的(后期扩展) -- AI 生成 ai_job_id BIGINT REFERENCES ai_jobs(ai_job_id) ON DELETE SET NULL, metadata JSONB NOT NULL DEFAULT '{}', -- AI 生成参数等 -- 审计 created_by BIGINT NOT NULL REFERENCES users(user_id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ ); -- 索引 CREATE INDEX idx_project_resources_project_id ON project_resources (project_id) WHERE deleted_at IS NULL; CREATE INDEX idx_project_resources_type ON project_resources (type) WHERE deleted_at IS NULL; CREATE INDEX idx_project_resources_created_by ON project_resources (created_by) WHERE deleted_at IS NULL; CREATE INDEX idx_project_resources_checksum ON project_resources (checksum); CREATE INDEX idx_project_resources_ai_job_id ON project_resources (ai_job_id) WHERE ai_job_id IS NOT NULL; CREATE INDEX idx_project_resources_metadata_gin ON project_resources USING GIN (metadata); -- 全文搜索索引(资源名称) CREATE INDEX idx_project_resources_name_trgm ON project_resources USING GIN (name gin_trgm_ops) WHERE deleted_at IS NULL; -- 触发器 CREATE TRIGGER update_project_resources_updated_at BEFORE UPDATE ON project_resources FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` **设计说明**: - 项目专属素材表,每个素材归属一个项目 - 使用 `ENUM` 定义资源类型: - `character`(角色):AI 生成或上传的角色素材 - `scene`(场景):AI 生成或上传的场景素材 - `prop`(道具):AI 生成或上传的道具素材 - `real`(实拍):用户上传的实拍图片或视频 - 支持 AI 生成素材(关联 ai_job_id) - 使用 checksum 支持去重 - `source_resource_id` 字段预留,供后期资源库功能扩展 #### 3.5.2 resources(系统资源库表 - 后期扩展) ```sql -- 注意:此表暂不实现,保留设计供后期扩展 CREATE TABLE resources ( resource_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL, type resource_type NOT NULL, -- 文件信息 file_url TEXT NOT NULL, thumbnail_url TEXT, file_size BIGINT, mime_type TEXT, width INTEGER, height INTEGER, checksum TEXT NOT NULL, -- 素材市场属性 is_public BOOLEAN NOT NULL DEFAULT false, -- 是否公开到素材市场 is_official BOOLEAN NOT NULL DEFAULT false, -- 是否官方资源 price NUMERIC(10, 2), -- 价格 download_count INTEGER NOT NULL DEFAULT 0, -- 下载次数 rating NUMERIC(3, 2), -- 评分 -- AI 生成 ai_job_id BIGINT REFERENCES ai_jobs(ai_job_id), metadata JSONB NOT NULL DEFAULT '{}', -- 审计 created_by BIGINT REFERENCES users(user_id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ ); -- 索引(后期实现) CREATE INDEX idx_resources_type ON resources (type) WHERE deleted_at IS NULL; CREATE INDEX idx_resources_is_public ON resources (is_public) WHERE deleted_at IS NULL AND is_public = true; CREATE INDEX idx_resources_is_official ON resources (is_official) WHERE deleted_at IS NULL AND is_official = true; CREATE INDEX idx_resources_rating ON resources (rating DESC) WHERE deleted_at IS NULL AND is_public = true; ``` **设计说明**: - 系统资源库 + 素材市场功能 - 支持官方资源(is_official)和用户上传资源(is_public) - 支持定价、评分、下载统计 - **前期开发暂不实现,保留表结构供后续扩展** #### 3.5.3 storyboard_resources(分镜资源关联表) ```sql CREATE TABLE storyboard_resources ( storyboard_resource_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, storyboard_id BIGINT NOT NULL REFERENCES storyboards(storyboard_id) ON DELETE CASCADE, project_resource_id BIGINT NOT NULL REFERENCES project_resources(project_resource_id) ON DELETE CASCADE, resource_type resource_type NOT NULL, -- 冗余字段,便于查询 display_order INTEGER NOT NULL DEFAULT 0, -- 显示顺序 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT storyboard_resources_unique UNIQUE (storyboard_id, project_resource_id) NULLS NOT DISTINCT ); -- 索引 CREATE INDEX idx_storyboard_resources_storyboard_id ON storyboard_resources (storyboard_id); CREATE INDEX idx_storyboard_resources_project_resource_id ON storyboard_resources (project_resource_id); CREATE INDEX idx_storyboard_resources_type ON storyboard_resources (resource_type); ``` **设计说明**: - 分镜与项目素材的多对多关联表 - 关联 project_resources 表(项目专属素材) - `resource_type` 冗余存储,避免 JOIN 查询 - 支持显示顺序 ### 3.6 视频管理 #### 3.6.1 videos(视频表) ```sql CREATE TYPE video_generation_type AS ENUM ('img2video', 'text2video', 'keyframe', 'fusion', 'replace', 'real'); CREATE TABLE videos ( video_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, storyboard_id BIGINT REFERENCES storyboards(storyboard_id) ON DELETE SET NULL, name TEXT NOT NULL, type video_generation_type NOT NULL, video_url TEXT, thumbnail_url TEXT, duration NUMERIC(10, 3), -- 视频时长(秒) file_size BIGINT, -- 文件大小(字节) width INTEGER, height INTEGER, frame_rate NUMERIC(5, 2), -- 帧率 start_time NUMERIC(10, 3) NOT NULL DEFAULT 0, -- 在分镜看板上的开始时间 end_time NUMERIC(10, 3) NOT NULL, -- 在分镜看板上的结束时间 metadata JSONB NOT NULL DEFAULT '{}', -- AI 生成参数、提示词等 -- metadata 结构: {"prompt": "...", "model": "...", "parameters": {...}, "aiJobId": 123} status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')), ai_job_id BIGINT, -- 关联 AI 任务 created_by BIGINT NOT NULL REFERENCES users(user_id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT videos_time_check CHECK (end_time > start_time) ); -- 索引 CREATE INDEX idx_videos_project_id ON videos (project_id); CREATE INDEX idx_videos_storyboard_id ON videos (storyboard_id) WHERE storyboard_id IS NOT NULL; CREATE INDEX idx_videos_type ON videos (type); CREATE INDEX idx_videos_status ON videos (status); CREATE INDEX idx_videos_time_range ON videos USING GIST (numrange(start_time, end_time)); CREATE INDEX idx_videos_created_by ON videos (created_by); CREATE INDEX idx_videos_metadata_gin ON videos USING GIN (metadata); CREATE INDEX idx_videos_ai_job_id ON videos (ai_job_id) WHERE ai_job_id IS NOT NULL; -- 触发器 CREATE TRIGGER update_videos_updated_at BEFORE UPDATE ON videos FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` **设计说明**: - 视频可以关联分镜,也可以独立存在 - 使用 `numrange` 和 GiST 索引支持时间范围查询 - 支持 AI 生成状态跟踪 - 元数据存储 AI 生成参数 ### 3.7 音效管理 #### 3.7.1 sound_effects(音效表) ```sql CREATE TABLE sound_effects ( sound_effect_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, name TEXT NOT NULL, audio_url TEXT, duration NUMERIC(10, 3), -- 音效时长(秒) file_size BIGINT, start_time NUMERIC(10, 3) NOT NULL DEFAULT 0, -- 在分镜看板上的开始时间 end_time NUMERIC(10, 3) NOT NULL, -- 在分镜看板上的结束时间 volume INTEGER NOT NULL DEFAULT 100 CHECK (volume >= 0 AND volume <= 100), -- 音量 0-100 fade_in NUMERIC(5, 2) DEFAULT 0, -- 淡入时长(秒) fade_out NUMERIC(5, 2) DEFAULT 0, -- 淡出时长(秒) metadata JSONB NOT NULL DEFAULT '{}', -- AI 生成参数等 status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')), ai_job_id BIGINT, created_by BIGINT NOT NULL REFERENCES users(user_id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT sound_effects_time_check CHECK (end_time > start_time) ); -- 索引 CREATE INDEX idx_sound_effects_project_id ON sound_effects (project_id); CREATE INDEX idx_sound_effects_time_range ON sound_effects USING GIST (numrange(start_time, end_time)); CREATE INDEX idx_sound_effects_status ON sound_effects (status); CREATE INDEX idx_sound_effects_created_by ON sound_effects (created_by); -- 触发器 CREATE TRIGGER update_sound_effects_updated_at BEFORE UPDATE ON sound_effects FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` ### 3.8 字幕管理 #### 3.8.1 subtitles(字幕表) ```sql CREATE TABLE subtitles ( subtitle_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, text TEXT NOT NULL, start_time NUMERIC(10, 3) NOT NULL DEFAULT 0, end_time NUMERIC(10, 3) NOT NULL, style JSONB NOT NULL DEFAULT '{}', -- 样式配置 -- style 结构: {"fontSize": 16, "color": "#FFFFFF", "position": "bottom", "backgroundColor": "#000000", "fontFamily": "Arial"} metadata JSONB NOT NULL DEFAULT '{}', -- AI 生成参数等 status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')), ai_job_id BIGINT, created_by BIGINT NOT NULL REFERENCES users(user_id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT subtitles_time_check CHECK (end_time > start_time) ); -- 索引 CREATE INDEX idx_subtitles_project_id ON subtitles (project_id); CREATE INDEX idx_subtitles_time_range ON subtitles USING GIST (numrange(start_time, end_time)); CREATE INDEX idx_subtitles_text_trgm ON subtitles USING GIN (text gin_trgm_ops); -- 全文搜索 CREATE INDEX idx_subtitles_status ON subtitles (status); -- 触发器 CREATE TRIGGER update_subtitles_updated_at BEFORE UPDATE ON subtitles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` ### 3.9 配音管理 #### 3.9.1 voiceovers(配音表) ```sql CREATE TABLE voiceovers ( voiceover_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, text TEXT NOT NULL, audio_url TEXT, duration NUMERIC(10, 3), -- 配音时长(秒) file_size BIGINT, start_time NUMERIC(10, 3) NOT NULL DEFAULT 0, end_time NUMERIC(10, 3) NOT NULL, voice_type TEXT, -- 语音类型(如:male_01, female_01) voice_speed NUMERIC(3, 2) NOT NULL DEFAULT 1.0, -- 语速倍数 volume INTEGER NOT NULL DEFAULT 100 CHECK (volume >= 0 AND volume <= 100), metadata JSONB NOT NULL DEFAULT '{}', -- AI 生成参数、语音克隆 ID 等 status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')), ai_job_id BIGINT, created_by BIGINT NOT NULL REFERENCES users(user_id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT voiceovers_time_check CHECK (end_time > start_time) ); -- 索引 CREATE INDEX idx_voiceovers_project_id ON voiceovers (project_id); CREATE INDEX idx_voiceovers_time_range ON voiceovers USING GIST (numrange(start_time, end_time)); CREATE INDEX idx_voiceovers_status ON voiceovers (status); CREATE INDEX idx_voiceovers_voice_type ON voiceovers (voice_type) WHERE voice_type IS NOT NULL; -- 触发器 CREATE TRIGGER update_voiceovers_updated_at BEFORE UPDATE ON voiceovers FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` ### 3.10 分镜看板管理 #### 3.10.1 timeline_tracks(分镜看板轨道表) ```sql CREATE TYPE track_type AS ENUM ('storyboard', 'resource', 'video', 'sound', 'subtitle', 'voice'); CREATE TABLE timeline_tracks ( track_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, type track_type NOT NULL, name TEXT NOT NULL, display_order INTEGER NOT NULL DEFAULT 0, -- 显示顺序 visible BOOLEAN NOT NULL DEFAULT true, locked BOOLEAN NOT NULL DEFAULT false, height INTEGER NOT NULL DEFAULT 60, -- 轨道高度(像素) created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT timeline_tracks_order_unique UNIQUE (project_id, display_order) DEFERRABLE INITIALLY DEFERRED ); -- 索引 CREATE INDEX idx_timeline_tracks_project_id ON timeline_tracks (project_id); CREATE INDEX idx_timeline_tracks_type ON timeline_tracks (type); CREATE INDEX idx_timeline_tracks_order ON timeline_tracks (project_id, display_order); -- 触发器 CREATE TRIGGER update_timeline_tracks_updated_at BEFORE UPDATE ON timeline_tracks FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` #### 3.10.2 timeline_items(分镜看板项表) ```sql CREATE TABLE timeline_items ( timeline_item_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, track_id BIGINT NOT NULL REFERENCES timeline_tracks(track_id) ON DELETE CASCADE, item_type track_type NOT NULL, -- 关联到具体项的 ID(多态关联) storyboard_id BIGINT REFERENCES storyboards(storyboard_id) ON DELETE CASCADE, video_id BIGINT REFERENCES videos(video_id) ON DELETE CASCADE, sound_effect_id BIGINT REFERENCES sound_effects(sound_effect_id) ON DELETE CASCADE, subtitle_id BIGINT REFERENCES subtitles(subtitle_id) ON DELETE CASCADE, voiceover_id BIGINT REFERENCES voiceovers(voiceover_id) ON DELETE CASCADE, resource_id BIGINT REFERENCES resources(resource_id) ON DELETE CASCADE, -- 分镜看板上的位置 start_time NUMERIC(10, 3) NOT NULL DEFAULT 0, end_time NUMERIC(10, 3) NOT NULL, display_order INTEGER NOT NULL DEFAULT 0, -- 同一轨道内的显示顺序 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT timeline_items_time_check CHECK (end_time > start_time), -- 确保只有一个关联项 CONSTRAINT timeline_items_ref_check CHECK ( (item_type = 'storyboard' AND storyboard_id IS NOT NULL AND video_id IS NULL AND sound_effect_id IS NULL AND subtitle_id IS NULL AND voiceover_id IS NULL AND resource_id IS NULL) OR (item_type = 'video' AND video_id IS NOT NULL AND storyboard_id IS NULL AND sound_effect_id IS NULL AND subtitle_id IS NULL AND voiceover_id IS NULL AND resource_id IS NULL) OR (item_type = 'sound' AND sound_effect_id IS NOT NULL AND storyboard_id IS NULL AND video_id IS NULL AND subtitle_id IS NULL AND voiceover_id IS NULL AND resource_id IS NULL) OR (item_type = 'subtitle' AND subtitle_id IS NOT NULL AND storyboard_id IS NULL AND video_id IS NULL AND sound_effect_id IS NULL AND voiceover_id IS NULL AND resource_id IS NULL) OR (item_type = 'voice' AND voiceover_id IS NOT NULL AND storyboard_id IS NULL AND video_id IS NULL AND sound_effect_id IS NULL AND subtitle_id IS NULL AND resource_id IS NULL) OR (item_type = 'resource' AND resource_id IS NOT NULL AND storyboard_id IS NULL AND video_id IS NULL AND sound_effect_id IS NULL AND subtitle_id IS NULL AND voiceover_id IS NULL) ) ); -- 索引 CREATE INDEX idx_timeline_items_track_id ON timeline_items (track_id); CREATE INDEX idx_timeline_items_storyboard_id ON timeline_items (storyboard_id) WHERE storyboard_id IS NOT NULL; CREATE INDEX idx_timeline_items_video_id ON timeline_items (video_id) WHERE video_id IS NOT NULL; CREATE INDEX idx_timeline_items_sound_effect_id ON timeline_items (sound_effect_id) WHERE sound_effect_id IS NOT NULL; CREATE INDEX idx_timeline_items_subtitle_id ON timeline_items (subtitle_id) WHERE subtitle_id IS NOT NULL; CREATE INDEX idx_timeline_items_voiceover_id ON timeline_items (voiceover_id) WHERE voiceover_id IS NOT NULL; CREATE INDEX idx_timeline_items_resource_id ON timeline_items (resource_id) WHERE resource_id IS NOT NULL; CREATE INDEX idx_timeline_items_time_range ON timeline_items USING GIST (numrange(start_time, end_time)); CREATE INDEX idx_timeline_items_type ON timeline_items (item_type); -- 触发器 CREATE TRIGGER update_timeline_items_updated_at BEFORE UPDATE ON timeline_items FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` **设计说明**: - 使用多态关联(多个可空外键)实现不同类型项的关联 - 使用 `CHECK` 约束确保只有一个关联项非空 - 使用 GiST 索引支持时间范围查询 ### 3.11 AI 任务管理 #### 3.11.1 ai_jobs(AI 任务表) ```sql CREATE TYPE ai_job_type AS ENUM ('image', 'video', 'sound', 'subtitle', 'voice', 'resource', 'storyboard_script', 'script_generation'); CREATE TYPE ai_job_status AS ENUM ('pending', 'processing', 'completed', 'failed', 'cancelled'); CREATE TABLE ai_jobs ( ai_job_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, project_id BIGINT REFERENCES projects(project_id) ON DELETE SET NULL, storyboard_id BIGINT REFERENCES storyboards(storyboard_id) ON DELETE SET NULL, job_type ai_job_type NOT NULL, status ai_job_status NOT NULL DEFAULT 'pending', prompt TEXT NOT NULL, model TEXT, -- 使用的 AI 模型 parameters JSONB NOT NULL DEFAULT '{}', -- AI 生成参数 result JSONB, -- 生成结果(URL、ID 等) error_message TEXT, progress INTEGER NOT NULL DEFAULT 0 CHECK (progress >= 0 AND progress <= 100), -- 进度 0-100 estimated_completion_at TIMESTAMPTZ, started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, cost NUMERIC(10, 4), -- 成本(元) credits_used INTEGER NOT NULL DEFAULT 0, -- 使用的积分 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 索引 CREATE INDEX idx_ai_jobs_user_id ON ai_jobs (user_id); CREATE INDEX idx_ai_jobs_project_id ON ai_jobs (project_id) WHERE project_id IS NOT NULL; CREATE INDEX idx_ai_jobs_storyboard_id ON ai_jobs (storyboard_id) WHERE storyboard_id IS NOT NULL; CREATE INDEX idx_ai_jobs_type ON ai_jobs (job_type); CREATE INDEX idx_ai_jobs_status ON ai_jobs (status); CREATE INDEX idx_ai_jobs_created_at ON ai_jobs (created_at); CREATE INDEX idx_ai_jobs_status_created_at ON ai_jobs (status, created_at) WHERE status IN ('pending', 'processing'); CREATE INDEX idx_ai_jobs_parameters_gin ON ai_jobs USING GIN (parameters); CREATE INDEX idx_ai_jobs_result_gin ON ai_jobs USING GIN (result) WHERE result IS NOT NULL; -- 触发器 CREATE TRIGGER update_ai_jobs_updated_at BEFORE UPDATE ON ai_jobs FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` **设计说明**: - 支持多种 AI 任务类型 - 跟踪任务状态和进度 - 记录成本和积分使用 - 支持结果和参数的 JSONB 查询 ### 3.12 协作功能 #### 3.12.1 comments(评论表) ```sql CREATE TABLE comments ( comment_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, -- 多态关联:可以评论项目、分镜、视频等 storyboard_id BIGINT REFERENCES storyboards(storyboard_id) ON DELETE CASCADE, video_id BIGINT REFERENCES videos(video_id) ON DELETE CASCADE, sound_effect_id BIGINT REFERENCES sound_effects(sound_effect_id) ON DELETE CASCADE, subtitle_id BIGINT REFERENCES subtitles(subtitle_id) ON DELETE CASCADE, voiceover_id BIGINT REFERENCES voiceovers(voiceover_id) ON DELETE CASCADE, timeline_item_id BIGINT REFERENCES timeline_items(timeline_item_id) ON DELETE CASCADE, -- 评论内容 content TEXT NOT NULL, -- 父评论 ID(支持回复) parent_comment_id BIGINT REFERENCES comments(comment_id) ON DELETE CASCADE, -- 提及的用户(数组) mentioned_user_ids BIGINT[], -- 作者 author_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, -- 位置信息(分镜看板上的位置) time_position NUMERIC(10, 3), -- 状态 is_resolved BOOLEAN NOT NULL DEFAULT false, resolved_by BIGINT REFERENCES users(user_id), resolved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ, -- 确保只有一个关联项 CONSTRAINT comments_ref_check CHECK ( (storyboard_id IS NOT NULL AND video_id IS NULL AND sound_effect_id IS NULL AND subtitle_id IS NULL AND voiceover_id IS NULL AND timeline_item_id IS NULL) OR (video_id IS NOT NULL AND storyboard_id IS NULL AND sound_effect_id IS NULL AND subtitle_id IS NULL AND voiceover_id IS NULL AND timeline_item_id IS NULL) OR (sound_effect_id IS NOT NULL AND storyboard_id IS NULL AND video_id IS NULL AND subtitle_id IS NULL AND voiceover_id IS NULL AND timeline_item_id IS NULL) OR (subtitle_id IS NOT NULL AND storyboard_id IS NULL AND video_id IS NULL AND sound_effect_id IS NULL AND voiceover_id IS NULL AND timeline_item_id IS NULL) OR (voiceover_id IS NOT NULL AND storyboard_id IS NULL AND video_id IS NULL AND sound_effect_id IS NULL AND subtitle_id IS NULL AND timeline_item_id IS NULL) OR (timeline_item_id IS NOT NULL AND storyboard_id IS NULL AND video_id IS NULL AND sound_effect_id IS NULL AND subtitle_id IS NULL AND voiceover_id IS NULL) OR (storyboard_id IS NULL AND video_id IS NULL AND sound_effect_id IS NULL AND subtitle_id IS NULL AND voiceover_id IS NULL AND timeline_item_id IS NULL) -- 项目级评论 ) ); -- 索引 CREATE INDEX idx_comments_project_id ON comments (project_id) WHERE deleted_at IS NULL; CREATE INDEX idx_comments_storyboard_id ON comments (storyboard_id) WHERE storyboard_id IS NOT NULL AND deleted_at IS NULL; CREATE INDEX idx_comments_video_id ON comments (video_id) WHERE video_id IS NOT NULL AND deleted_at IS NULL; CREATE INDEX idx_comments_parent_comment_id ON comments (parent_comment_id) WHERE parent_comment_id IS NOT NULL; CREATE INDEX idx_comments_author_id ON comments (author_id) WHERE deleted_at IS NULL; CREATE INDEX idx_comments_created_at ON comments (created_at) WHERE deleted_at IS NULL; CREATE INDEX idx_comments_mentioned_user_ids_gin ON comments USING GIN (mentioned_user_ids) WHERE mentioned_user_ids IS NOT NULL; -- 触发器 CREATE TRIGGER update_comments_updated_at BEFORE UPDATE ON comments FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` **设计说明**: - 支持多态关联(评论项目、分镜、视频等) - 支持回复(父评论) - 支持 @ 提及(数组存储) - 支持分镜看板位置标记 #### 3.12.2 notifications(通知表) ```sql CREATE TYPE notification_type AS ENUM ('project', 'comment', 'mention', 'system', 'ai_job_completed', 'collaboration'); CREATE TABLE notifications ( notification_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, type notification_type NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, -- 关联数据(JSONB) related_data JSONB, -- related_data 结构: {"projectId": 123, "commentId": 456, "aiJobId": 789, ...} is_read BOOLEAN NOT NULL DEFAULT false, read_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 索引 CREATE INDEX idx_notifications_user_id ON notifications (user_id); CREATE INDEX idx_notifications_user_id_unread ON notifications (user_id, is_read) WHERE is_read = false; CREATE INDEX idx_notifications_type ON notifications (type); CREATE INDEX idx_notifications_created_at ON notifications (created_at); CREATE INDEX idx_notifications_related_data_gin ON notifications USING GIN (related_data); ``` **设计说明**: - 使用 `ENUM` 定义通知类型 - 使用 JSONB 存储关联数据,灵活支持不同类型 - 部分索引优化未读通知查询 ### 3.13 模板与素材市场 #### 3.13.1 templates(模板表) ```sql CREATE TABLE templates ( template_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL, description TEXT, category TEXT NOT NULL, -- 'advertisement', 'promotion', 'short_video', 'tutorial' thumbnail_url TEXT, template_data JSONB NOT NULL, -- 模板数据(项目结构) is_public BOOLEAN NOT NULL DEFAULT false, is_official BOOLEAN NOT NULL DEFAULT false, -- 是否官方模板 price NUMERIC(10, 2) NOT NULL DEFAULT 0, download_count INTEGER NOT NULL DEFAULT 0, rating NUMERIC(3, 2) NOT NULL DEFAULT 0 CHECK (rating >= 0 AND rating <= 5), rating_count INTEGER NOT NULL DEFAULT 0, created_by BIGINT REFERENCES users(user_id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ ); -- 索引 CREATE INDEX idx_templates_category ON templates (category) WHERE deleted_at IS NULL; CREATE INDEX idx_templates_is_public ON templates (is_public) WHERE deleted_at IS NULL AND is_public = true; CREATE INDEX idx_templates_is_official ON templates (is_official) WHERE deleted_at IS NULL AND is_official = true; CREATE INDEX idx_templates_rating ON templates (rating DESC) WHERE deleted_at IS NULL AND is_public = true; CREATE INDEX idx_templates_download_count ON templates (download_count DESC) WHERE deleted_at IS NULL AND is_public = true; CREATE INDEX idx_templates_template_data_gin ON templates USING GIN (template_data); ``` #### 3.13.2 template_purchases(模板购买记录表) ```sql CREATE TABLE template_purchases ( purchase_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, template_id BIGINT NOT NULL REFERENCES templates(template_id) ON DELETE RESTRICT, user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, price NUMERIC(10, 2) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT template_purchases_unique UNIQUE (template_id, user_id) NULLS NOT DISTINCT ); CREATE INDEX idx_template_purchases_template_id ON template_purchases (template_id); CREATE INDEX idx_template_purchases_user_id ON template_purchases (user_id); ``` ### 3.14 导出任务 #### 3.14.1 export_jobs(导出任务表) ```sql CREATE TYPE export_format AS ENUM ('mp4', 'mov', 'avi', 'webm', 'json', 'pdf'); CREATE TYPE export_quality AS ENUM ('low', 'medium', 'high', 'ultra'); CREATE TYPE export_status AS ENUM ('pending', 'processing', 'completed', 'failed', 'cancelled'); CREATE TABLE export_jobs ( export_job_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, format export_format NOT NULL, quality export_quality NOT NULL DEFAULT 'high', resolution_width INTEGER, resolution_height INTEGER, include_watermark BOOLEAN NOT NULL DEFAULT false, status export_status NOT NULL DEFAULT 'pending', output_url TEXT, -- 导出文件 URL file_size BIGINT, progress INTEGER NOT NULL DEFAULT 0 CHECK (progress >= 0 AND progress <= 100), error_message TEXT, started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, expires_at TIMESTAMPTZ, -- 下载链接过期时间 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- 索引 CREATE INDEX idx_export_jobs_project_id ON export_jobs (project_id); CREATE INDEX idx_export_jobs_user_id ON export_jobs (user_id); CREATE INDEX idx_export_jobs_status ON export_jobs (status); CREATE INDEX idx_export_jobs_status_created_at ON export_jobs (status, created_at) WHERE status IN ('pending', 'processing'); CREATE INDEX idx_export_jobs_expires_at ON export_jobs (expires_at) WHERE expires_at IS NOT NULL; ``` ### 3.15 剧本管理 #### 3.15.1 scripts(剧本表) ```sql CREATE TYPE script_type AS ENUM ('text', 'file'); CREATE TYPE script_status AS ENUM ('draft', 'review', 'approved', 'archived'); CREATE TABLE scripts ( script_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, project_id BIGINT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE, name TEXT NOT NULL, type script_type NOT NULL, -- 文本剧本字段 content TEXT, -- 剧本文本内容(Markdown/纯文本) -- 文件剧本字段 file_url TEXT, file_size BIGINT, mime_type TEXT, checksum TEXT, -- SHA256 校验和,配合 file_checksums 表去重 storage_path TEXT, -- 元数据 version INTEGER NOT NULL DEFAULT 1, -- 版本号 word_count INTEGER DEFAULT 0, -- 字数统计 scene_count INTEGER DEFAULT 0, -- 场景数量 character_count INTEGER DEFAULT 0, -- 角色数量 -- AI 生成相关 ai_job_id BIGINT REFERENCES ai_jobs(ai_job_id) ON DELETE SET NULL, ai_prompt TEXT, -- 状态管理 status script_status NOT NULL DEFAULT 'draft', -- 协作字段 created_by BIGINT NOT NULL REFERENCES users(user_id), updated_by BIGINT REFERENCES users(user_id), approved_by BIGINT REFERENCES users(user_id), approved_at TIMESTAMPTZ, -- 审计字段 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ, -- 约束:content 和 file_url 二选一 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) ) ); -- 索引 CREATE INDEX idx_scripts_project_id ON scripts (project_id) WHERE deleted_at IS NULL; CREATE INDEX idx_scripts_type ON scripts (type) WHERE deleted_at IS NULL; CREATE INDEX idx_scripts_status ON scripts (status) WHERE deleted_at IS NULL; CREATE INDEX idx_scripts_created_by ON scripts (created_by) WHERE deleted_at IS NULL; CREATE INDEX idx_scripts_ai_job_id ON scripts (ai_job_id) WHERE ai_job_id IS NOT NULL; CREATE INDEX idx_scripts_checksum ON scripts (checksum) WHERE checksum IS NOT NULL; -- 全文搜索索引(剧本名称和内容) CREATE INDEX idx_scripts_name_trgm ON scripts USING GIN (name gin_trgm_ops) WHERE deleted_at IS NULL; CREATE INDEX idx_scripts_content_trgm ON scripts USING GIN (content gin_trgm_ops) WHERE deleted_at IS NULL AND content IS NOT NULL; -- 触发器 CREATE TRIGGER update_scripts_updated_at BEFORE UPDATE ON scripts FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` **设计说明**: - 使用 `ENUM` 定义剧本类型(text, file)和状态 - 支持文本和文件两种剧本形式 - 文件剧本支持格式:TXT、DOC、DOCX、PDF、RTF、Markdown - 文件剧本直接存储文件信息(file_url, file_size, checksum 等) - checksum 字段配合 file_checksums 表实现去重 - 与 videos、project_resources 表保持一致的设计模式 - 使用 `CHECK` 约束确保 content 和 file_url 二选一 - 支持版本号、字数统计等元数据 - 支持 AI 生成剧本的关联 - 支持审批流程(draft -> review -> approved -> archived) - 全文搜索支持剧本名称和内容 #### 3.15.2 script_versions(剧本版本历史表) ```sql CREATE TABLE script_versions ( version_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, script_id BIGINT NOT NULL REFERENCES scripts(script_id) ON DELETE CASCADE, version_number INTEGER NOT NULL, content_snapshot TEXT, -- 内容快照 change_summary TEXT, -- 变更摘要 word_count INTEGER DEFAULT 0, scene_count INTEGER DEFAULT 0, character_count INTEGER DEFAULT 0, created_by BIGINT NOT NULL REFERENCES users(user_id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT script_versions_unique UNIQUE (script_id, version_number) ); -- 索引 CREATE INDEX idx_script_versions_script_id ON script_versions (script_id); CREATE INDEX idx_script_versions_created_at ON script_versions (created_at); CREATE INDEX idx_script_versions_created_by ON script_versions (created_by); ``` **设计说明**: - 记录剧本的每个版本快照 - 版本号在剧本内唯一 - 保存变更摘要,便于追溯 - 支持版本对比和回滚 #### 3.15.3 script_characters(剧本角色表) > **注意**:此表设计已过时。最新的剧本管理表设计(使用 UUID v7 和 SMALLINT 枚举)请参考 [剧本管理服务文档](./backend/04-services/project/screenplay-service.md)。 ```sql CREATE TYPE character_role_type AS ENUM ('main', 'supporting', 'extra'); CREATE TABLE script_characters ( character_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, script_id BIGINT NOT NULL REFERENCES scripts(script_id) ON DELETE CASCADE, name TEXT NOT NULL, -- 角色名 description TEXT, -- 角色描述 character_image_url VARCHAR(500), -- 角色形象图片地址 role_type character_role_type NOT NULL DEFAULT 'supporting', line_count INTEGER DEFAULT 0, -- 台词数量 appearance_count INTEGER DEFAULT 0, -- 出场次数 metadata JSONB NOT NULL DEFAULT '{}', -- 额外元数据(性格、背景等) created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT script_characters_name_unique UNIQUE (script_id, name) NULLS NOT DISTINCT ); -- 索引 CREATE INDEX idx_script_characters_script_id ON script_characters (script_id); CREATE INDEX idx_script_characters_role_type ON script_characters (role_type); CREATE INDEX idx_script_characters_name_trgm ON script_characters USING GIN (name gin_trgm_ops); -- 触发器 CREATE TRIGGER update_script_characters_updated_at BEFORE UPDATE ON script_characters FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` **设计说明**: - 管理剧本中的角色信息 - 支持角色类型分类(主角、配角、群演) - 统计台词数量和出场次数 - 同一剧本内角色名称唯一 #### 3.15.4 script_scenes(剧本场景表) > **注意**:此表设计已过时。最新的剧本管理表设计(使用 UUID v7 和 SMALLINT 枚举)请参考 [剧本管理服务文档](./backend/04-services/project/screenplay-service.md)。 ```sql CREATE TYPE time_of_day_type AS ENUM ('dawn', 'morning', 'noon', 'afternoon', 'dusk', 'night'); CREATE TABLE script_scenes ( scene_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, script_id BIGINT NOT NULL REFERENCES scripts(script_id) ON DELETE CASCADE, scene_number INTEGER NOT NULL, -- 场景编号 title TEXT NOT NULL, -- 场景标题 location TEXT, -- 场景地点 time_of_day time_of_day_type, -- 时间(白天/夜晚) description TEXT, -- 场景描述 duration_estimate NUMERIC(10, 3), -- 预估时长(秒) order_index INTEGER NOT NULL, -- 排序 metadata JSONB NOT NULL DEFAULT '{}', -- 额外元数据 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT script_scenes_order_unique UNIQUE (script_id, order_index) DEFERRABLE INITIALLY DEFERRED ); -- 索引 CREATE INDEX idx_script_scenes_script_id ON script_scenes (script_id); CREATE INDEX idx_script_scenes_order ON script_scenes (script_id, order_index); CREATE INDEX idx_script_scenes_scene_number ON script_scenes (script_id, scene_number); -- 触发器 CREATE TRIGGER update_script_scenes_updated_at BEFORE UPDATE ON script_scenes FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` **设计说明**: - 管理剧本中的场景信息 - 支持场景编号和排序 - 记录场景地点、时间、描述 - 预估场景时长,便于制作规划 ### 3.16 附件管理 #### 3.16.1 attachments(附件表) ```sql CREATE TYPE attachment_category AS ENUM ('document', 'image'); CREATE TABLE attachments ( attachment_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name TEXT NOT NULL, -- 文件名 original_name TEXT NOT NULL, -- 原始文件名 file_url TEXT NOT NULL, -- 文件URL file_size BIGINT NOT NULL, -- 文件大小(字节) mime_type TEXT NOT NULL, -- MIME类型 -- 文件类型分类 category attachment_category NOT NULL, -- 文件元数据 extension TEXT, -- 文件扩展名 (.pdf, .docx, .txt, .jpg, .png) checksum TEXT NOT NULL, -- 文件校验和(SHA256) -- 关联信息(一对多) project_id BIGINT REFERENCES projects(project_id) ON DELETE CASCADE, -- 访问控制 is_public BOOLEAN NOT NULL DEFAULT false, -- 是否公开 access_count INTEGER NOT NULL DEFAULT 0, -- 访问次数 download_count INTEGER NOT NULL DEFAULT 0, -- 下载次数 -- 存储信息 storage_provider TEXT, -- 存储提供商(MinIO/S3/OSS) storage_path TEXT, -- 存储路径 -- 审计字段 uploaded_by BIGINT NOT NULL REFERENCES users(user_id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ, expires_at TIMESTAMPTZ -- 过期时间(临时文件) ); -- 索引 CREATE INDEX idx_attachments_project_id ON attachments (project_id) WHERE deleted_at IS NULL AND project_id IS NOT NULL; CREATE INDEX idx_attachments_category ON attachments (category) WHERE deleted_at IS NULL; CREATE INDEX idx_attachments_uploaded_by ON attachments (uploaded_by) WHERE deleted_at IS NULL; CREATE INDEX idx_attachments_created_at ON attachments (created_at) WHERE deleted_at IS NULL; CREATE INDEX idx_attachments_expires_at ON attachments (expires_at) WHERE expires_at IS NOT NULL; CREATE INDEX idx_attachments_checksum ON attachments (checksum); -- 全文搜索索引(文件名) CREATE INDEX idx_attachments_name_trgm ON attachments USING GIN (name gin_trgm_ops) WHERE deleted_at IS NULL; CREATE INDEX idx_attachments_original_name_trgm ON attachments USING GIN (original_name gin_trgm_ops) WHERE deleted_at IS NULL; -- 触发器 CREATE TRIGGER update_attachments_updated_at BEFORE UPDATE ON attachments FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); ``` **设计说明**: - 专注于文档类附件管理 - 分类简化为 document(文档)和 image(图片,用于头像、封面等) - 支持一对多关联(project_id, script_id) - checksum 必填,支持去重 - 移除 user_id 关联(改为业务表存储,见下文) - 记录访问和下载次数 - 支持临时文件(expires_at) #### 3.16.2 file_checksums(文件去重表) ```sql CREATE TABLE file_checksums ( checksum TEXT PRIMARY KEY, file_url TEXT NOT NULL, file_size BIGINT NOT NULL, mime_type TEXT NOT NULL, storage_provider TEXT, storage_path TEXT NOT NULL, reference_count INTEGER NOT NULL DEFAULT 1, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_accessed_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_file_checksums_file_url ON file_checksums (file_url); CREATE INDEX idx_file_checksums_reference_count ON file_checksums (reference_count); CREATE INDEX idx_file_checksums_last_accessed ON file_checksums (last_accessed_at); ``` **设计说明**: - 全局文件去重表 - 跨所有表(attachments, project_resources, videos 等)去重 - 记录引用计数,便于清理无引用文件 - 记录最后访问时间,便于清理过期文件 #### 3.16.3 业务表新增字段(一对一关联) **users 表新增字段**: ```sql ALTER TABLE users ADD COLUMN avatar_id BIGINT REFERENCES attachments(attachment_id) ON DELETE SET NULL; CREATE INDEX idx_users_avatar_id ON users (avatar_id) WHERE avatar_id IS NOT NULL; ``` **projects 表新增字段**: ```sql ALTER TABLE projects ADD COLUMN cover_image_id BIGINT REFERENCES attachments(attachment_id) ON DELETE SET NULL; CREATE INDEX idx_projects_cover_image_id ON projects (cover_image_id) WHERE cover_image_id IS NOT NULL; ``` **storyboards 表新增字段**: ```sql ALTER TABLE storyboards ADD COLUMN thumbnail_id BIGINT REFERENCES attachments(attachment_id) ON DELETE SET NULL; CREATE INDEX idx_storyboards_thumbnail_id ON storyboards (thumbnail_id) WHERE thumbnail_id IS NOT NULL; ``` **设计说明**: - 一对一关系使用业务表存储附件ID - 避免在 attachments 表中使用多态关联 - 语义清晰:users.avatar_id 明确表示"这是我的头像" - 查询性能好:不需要 JOIN attachments 表即可获取ID --- ## 4. 索引设计 ### 4.1 索引策略总结 1. **主键索引**:自动创建 B-tree 索引 2. **外键索引**:所有外键列都创建索引(PostgreSQL 不自动创建) 3. **唯一索引**:唯一约束自动创建索引 4. **查询索引**:根据常见查询模式创建索引 5. **部分索引**:对软删除数据使用 `WHERE deleted_at IS NULL` 6. **复合索引**:多列查询使用复合索引 7. **GIN 索引**:JSONB、数组、全文搜索 8. **GiST 索引**:范围查询(时间范围) ### 4.2 需要安装的扩展 ```sql -- 全文搜索(模糊搜索) CREATE EXTENSION IF NOT EXISTS pg_trgm; -- 如果需要向量搜索(AI 推荐) -- CREATE EXTENSION IF NOT EXISTS vector; ``` --- ## 5. 约束与触发器 ### 5.1 常用约束 - **NOT NULL**:必填字段 - **CHECK**:业务规则验证(时间范围、数值范围等) - **UNIQUE**:唯一性约束 - **FOREIGN KEY**:外键约束,指定 `ON DELETE` 行为 - **ENUM**:枚举类型约束 ### 5.2 触发器 - **updated_at 自动更新**:所有表都有 `updated_at` 字段,通过触发器自动更新 - **软删除**:通过 `deleted_at` 实现软删除 - **路径自动更新**:文件夹路径和层级自动计算 ### 5.3 文件夹权限检查函数 ```sql -- 检查用户是否有文件夹权限(包含继承权限) CREATE OR REPLACE FUNCTION check_folder_permission( p_user_id BIGINT, p_folder_id BIGINT, p_required_role member_role DEFAULT 'viewer' ) RETURNS BOOLEAN AS $$ DECLARE v_user_role member_role; v_folder_owner_id BIGINT; v_parent_folder_id BIGINT; v_role_priority INTEGER; v_required_priority INTEGER; BEGIN -- 检查是否是文件夹所有者 SELECT owner_id INTO v_folder_owner_id FROM folders WHERE folder_id = p_folder_id AND deleted_at IS NULL; IF v_folder_owner_id = p_user_id THEN RETURN true; END IF; -- 检查直接权限 SELECT role INTO v_user_role FROM folder_members WHERE folder_id = p_folder_id AND user_id = p_user_id; -- 如果没有直接权限,检查继承权限 IF v_user_role IS NULL THEN SELECT parent_folder_id INTO v_parent_folder_id FROM folders WHERE folder_id = p_folder_id; -- 递归检查父文件夹权限 IF v_parent_folder_id IS NOT NULL THEN RETURN check_folder_permission(p_user_id, v_parent_folder_id, p_required_role); END IF; RETURN false; END IF; -- 角色优先级比较 v_role_priority := CASE v_user_role WHEN 'owner' THEN 3 WHEN 'editor' THEN 2 WHEN 'viewer' THEN 1 ELSE 0 END; v_required_priority := CASE p_required_role WHEN 'owner' THEN 3 WHEN 'editor' THEN 2 WHEN 'viewer' THEN 1 ELSE 0 END; RETURN v_role_priority >= v_required_priority; END; $$ LANGUAGE plpgsql STABLE; -- 检查用户是否有项目权限(包含文件夹继承权限) CREATE OR REPLACE FUNCTION check_project_permission( p_user_id BIGINT, p_project_id BIGINT, p_required_role member_role DEFAULT 'viewer' ) RETURNS BOOLEAN AS $$ DECLARE v_user_role member_role; v_project_owner_id BIGINT; v_folder_id BIGINT; v_role_priority INTEGER; v_required_priority INTEGER; BEGIN -- 检查是否是项目所有者 SELECT owner_id, folder_id INTO v_project_owner_id, v_folder_id FROM projects WHERE project_id = p_project_id AND deleted_at IS NULL; IF v_project_owner_id = p_user_id THEN RETURN true; END IF; -- 检查项目直接权限 SELECT role INTO v_user_role FROM project_members WHERE project_id = p_project_id AND user_id = p_user_id; IF v_user_role IS NOT NULL THEN v_role_priority := CASE v_user_role WHEN 'owner' THEN 3 WHEN 'editor' THEN 2 WHEN 'viewer' THEN 1 ELSE 0 END; v_required_priority := CASE p_required_role WHEN 'owner' THEN 3 WHEN 'editor' THEN 2 WHEN 'viewer' THEN 1 ELSE 0 END; IF v_role_priority >= v_required_priority THEN RETURN true; END IF; END IF; -- 如果项目在文件夹中,检查文件夹权限 IF v_folder_id IS NOT NULL THEN RETURN check_folder_permission(p_user_id, v_folder_id, p_required_role); END IF; RETURN false; END; $$ LANGUAGE plpgsql STABLE; -- 获取用户可访问的文件夹列表(递归CTE) CREATE OR REPLACE FUNCTION get_user_accessible_folders(p_user_id BIGINT) RETURNS TABLE(folder_id BIGINT, name TEXT, path TEXT, level INTEGER) AS $$ BEGIN RETURN QUERY WITH RECURSIVE accessible_folders AS ( -- 用户拥有的根文件夹 SELECT f.folder_id, f.name, f.path, f.level, f.parent_folder_id FROM folders f WHERE f.owner_id = p_user_id AND f.deleted_at IS NULL AND f.parent_folder_id IS NULL UNION ALL -- 用户有权限的根文件夹 SELECT f.folder_id, f.name, f.path, f.level, f.parent_folder_id FROM folders f INNER JOIN folder_members fm ON f.folder_id = fm.folder_id WHERE fm.user_id = p_user_id AND f.deleted_at IS NULL AND f.parent_folder_id IS NULL UNION ALL -- 递归获取子文件夹 SELECT f.folder_id, f.name, f.path, f.level, f.parent_folder_id FROM folders f INNER JOIN accessible_folders af ON f.parent_folder_id = af.folder_id WHERE f.deleted_at IS NULL ) SELECT af.folder_id, af.name, af.path, af.level FROM accessible_folders af ORDER BY af.path; END; $$ LANGUAGE plpgsql STABLE; -- 防止循环引用的检查函数 CREATE OR REPLACE FUNCTION check_folder_cycle( p_folder_id BIGINT, p_new_parent_id BIGINT ) RETURNS BOOLEAN AS $$ DECLARE v_current_id BIGINT; BEGIN -- 如果新父文件夹为空,不会产生循环 IF p_new_parent_id IS NULL THEN RETURN false; END IF; -- 不能将文件夹移动到自己 IF p_folder_id = p_new_parent_id THEN RETURN true; END IF; -- 检查新父文件夹的祖先链中是否包含当前文件夹 v_current_id := p_new_parent_id; WHILE v_current_id IS NOT NULL LOOP IF v_current_id = p_folder_id THEN RETURN true; -- 发现循环 END IF; SELECT parent_folder_id INTO v_current_id FROM folders WHERE folder_id = v_current_id; END LOOP; RETURN false; -- 无循环 END; $$ LANGUAGE plpgsql STABLE; ``` ### 5.3 示例:项目成员权限检查函数 ```sql -- 检查用户是否有项目权限 CREATE OR REPLACE FUNCTION check_project_permission( p_user_id BIGINT, p_project_id BIGINT, p_required_role member_role DEFAULT 'viewer' ) RETURNS BOOLEAN AS $$ DECLARE v_user_role member_role; v_role_priority INTEGER; v_required_priority INTEGER; BEGIN -- 获取用户角色 SELECT role INTO v_user_role FROM project_members WHERE project_id = p_project_id AND user_id = p_user_id; -- 如果是项目所有者,直接返回 true SELECT owner_id INTO v_user_role FROM projects WHERE project_id = p_project_id; IF v_user_role = p_user_id THEN RETURN true; END IF; -- 角色优先级:owner > editor > viewer v_role_priority := CASE v_user_role WHEN 'owner' THEN 3 WHEN 'editor' THEN 2 WHEN 'viewer' THEN 1 ELSE 0 END; v_required_priority := CASE p_required_role WHEN 'owner' THEN 3 WHEN 'editor' THEN 2 WHEN 'viewer' THEN 1 ELSE 0 END; RETURN v_role_priority >= v_required_priority; END; $$ LANGUAGE plpgsql STABLE; ``` --- ## 6. 性能优化策略 ### 6.1 分区策略 对于大表,考虑按时间分区: ```sql -- 示例:AI 任务表按时间分区(如果数据量很大) CREATE TABLE ai_jobs ( -- ... 字段定义 ) PARTITION BY RANGE (created_at); CREATE TABLE ai_jobs_2024_q1 PARTITION OF ai_jobs FOR VALUES FROM ('2024-01-01') TO ('2024-04-01'); CREATE TABLE ai_jobs_2024_q2 PARTITION OF ai_jobs FOR VALUES FROM ('2024-04-01') TO ('2024-07-01'); -- ... ``` ### 6.2 物化视图 对于复杂查询,使用物化视图: ```sql -- 项目统计信息物化视图 CREATE MATERIALIZED VIEW project_statistics AS SELECT p.project_id, p.name, COUNT(DISTINCT s.storyboard_id) AS storyboard_count, COUNT(DISTINCT v.video_id) AS video_count, COUNT(DISTINCT se.sound_effect_id) AS sound_effect_count, COUNT(DISTINCT st.subtitle_id) AS subtitle_count, COUNT(DISTINCT vo.voiceover_id) AS voiceover_count, COUNT(DISTINCT pm.user_id) AS member_count FROM projects p LEFT JOIN storyboards s ON s.project_id = p.project_id LEFT JOIN videos v ON v.project_id = p.project_id LEFT JOIN sound_effects se ON se.project_id = p.project_id LEFT JOIN subtitles st ON st.project_id = p.project_id LEFT JOIN voiceovers vo ON vo.project_id = p.project_id LEFT JOIN project_members pm ON pm.project_id = p.project_id WHERE p.deleted_at IS NULL GROUP BY p.project_id, p.name; CREATE UNIQUE INDEX ON project_statistics (project_id); -- 定期刷新(通过定时任务) -- REFRESH MATERIALIZED VIEW CONCURRENTLY project_statistics; ``` ### 6.3 连接池配置 - 使用 PgBouncer 或 pgpool-II 进行连接池管理 - 配置合理的连接数限制 ### 6.4 查询优化建议 1. **避免 N+1 查询**:使用 JOIN 或批量查询 2. **使用 EXPLAIN ANALYZE**:分析查询计划 3. **合理使用索引**:避免过度索引 4. **批量操作**:使用 `COPY` 或批量 `INSERT` --- ## 7. 数据迁移策略 ### 7.1 使用 Alembic(Python)或 Flyway(Java) 推荐使用 Alembic 进行数据库迁移管理。 ### 7.2 迁移脚本示例 ```sql -- migrations/001_initial_schema.sql -- 创建所有表、索引、约束 -- migrations/002_add_ai_jobs.sql -- 添加 AI 任务表 -- migrations/003_add_comments.sql -- 添加评论功能 ``` ### 7.3 数据迁移注意事项 1. **向后兼容**:新字段使用 `DEFAULT` 或 `NULL` 2. **分步迁移**:大表迁移分步进行 3. **回滚方案**:准备回滚脚本 4. **测试环境**:先在测试环境验证 --- ## 8. 安全考虑 ### 8.1 行级安全(RLS) ```sql -- 启用行级安全 ALTER TABLE projects ENABLE ROW LEVEL SECURITY; -- 项目访问策略 CREATE POLICY project_access_policy ON projects FOR SELECT USING ( owner_id = current_setting('app.current_user_id')::BIGINT OR project_id IN ( SELECT project_id FROM project_members WHERE user_id = current_setting('app.current_user_id')::BIGINT ) ); ``` ### 8.2 数据加密 - **传输加密**:使用 SSL/TLS - **存储加密**:敏感字段加密存储 - **密码哈希**:使用 `pgcrypto` 扩展 --- ## 9. 监控与维护 ### 9.1 慢查询监控 ```sql -- 启用慢查询日志 ALTER SYSTEM SET log_min_duration_statement = 1000; -- 记录超过 1 秒的查询 ``` ### 9.2 统计信息 ```sql -- 更新统计信息 ANALYZE; -- 查看表大小 SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size FROM pg_tables WHERE schemaname = 'public' ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC; ``` ### 9.3 定期维护 - **VACUUM**:清理死元组 - **REINDEX**:重建索引 - **ANALYZE**:更新统计信息 --- ## 10. 总结 ### 10.1 设计亮点 1. ✅ **规范化设计**:遵循 3NF,减少冗余 2. ✅ **灵活扩展**:JSONB 存储灵活配置 3. ✅ **性能优化**:合理的索引策略 4. ✅ **关联完整**:所有关联关系都有外键约束 5. ✅ **软删除**:支持数据恢复 6. ✅ **审计追踪**:记录创建和更新时间 7. ✅ **多态关联**:支持灵活的关联关系 ### 10.2 后续优化方向 1. **分区**:大表按时间分区 2. **缓存**:Redis 缓存热点数据 3. **读写分离**:主从复制 4. **分库分表**:如果数据量极大 ### 10.3 剧本和附件模块设计亮点 1. ✅ **灵活的剧本类型**:支持文本和附件两种形式,满足不同输入方式 2. ✅ **完整的版本管理**:剧本版本历史表支持版本追溯和回滚 3. ✅ **结构化剧本数据**:角色表和场景表支持剧本的结构化管理 4. ✅ **通用附件系统**:附件表可复用于多种场景(剧本、合同、参考文档等) 5. ✅ **AI 生成支持**:剧本表关联 AI 任务,支持 AI 辅助创作 6. ✅ **审批流程**:剧本状态管理支持草稿、审核、批准流程 7. ✅ **全文搜索**:剧本内容、角色名、场景信息均支持全文搜索 8. ✅ **文件安全**:附件表支持校验和、访问控制、过期时间 --- **文档版本**:v1.2 **最后更新**:2025-01-27 **变更记录**: **v1.2 (2025-01-27)** - 重构资源管理架构: - 新增 `project_resources` 表(项目专属素材) - 保留 `resources` 表设计(系统资源库+素材市场,后期扩展) - 更新 `storyboard_resources` 关联到 `project_resources` - 优化附件管理: - 简化 `attachments` 表分类(document, image) - 移除 `user_id` 关联,改为业务表存储(users.avatar_id, projects.cover_image_id, storyboards.thumbnail_id) - 新增 `script_id` 关联 - checksum 字段改为必填 - 新增 `file_checksums` 表(全局文件去重) - 新增业务表字段: - users.avatar_id - projects.cover_image_id - storyboards.thumbnail_id **v1.1 (2025-01-27)** - 新增剧本管理模块(scripts、script_versions、script_characters、script_scenes) - 新增附件管理模块(attachments) - 更新 AI 任务类型,新增 script_generation - 更新核心实体关系图 **v1.0 (2025-01-27)** - 初始版本 - 完整的数据库设计文档 **下一步**:根据本文档创建 Alembic 迁移脚本,初始化数据库