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.
72 KiB
72 KiB
Jointo(jointo)PostgreSQL 数据库设计文档
项目名称:Jointo(jointo)
数据库:PostgreSQL 14+
文档版本:v1.2
创建日期:2025-01-27
目录
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(用户表)
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(用户会话表)
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(文件夹表)
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(文件夹成员表)
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(项目表)
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(项目成员表)
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(项目版本表)
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(分镜表)
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(项目素材表)
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(系统资源库表 - 后期扩展)
-- 注意:此表暂不实现,保留设计供后期扩展
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(分镜资源关联表)
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(视频表)
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(音效表)
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(字幕表)
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(配音表)
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(分镜看板轨道表)
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(分镜看板项表)
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 任务表)
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(评论表)
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(通知表)
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(模板表)
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(模板购买记录表)
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(导出任务表)
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(剧本表)
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(剧本版本历史表)
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 枚举)请参考 剧本管理服务文档。
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 枚举)请参考 剧本管理服务文档。
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(附件表)
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(文件去重表)
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 表新增字段:
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 表新增字段:
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 表新增字段:
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 索引策略总结
- 主键索引:自动创建 B-tree 索引
- 外键索引:所有外键列都创建索引(PostgreSQL 不自动创建)
- 唯一索引:唯一约束自动创建索引
- 查询索引:根据常见查询模式创建索引
- 部分索引:对软删除数据使用
WHERE deleted_at IS NULL - 复合索引:多列查询使用复合索引
- GIN 索引:JSONB、数组、全文搜索
- GiST 索引:范围查询(时间范围)
4.2 需要安装的扩展
-- 全文搜索(模糊搜索)
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 文件夹权限检查函数
-- 检查用户是否有文件夹权限(包含继承权限)
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 示例:项目成员权限检查函数
-- 检查用户是否有项目权限
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 分区策略
对于大表,考虑按时间分区:
-- 示例: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 物化视图
对于复杂查询,使用物化视图:
-- 项目统计信息物化视图
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 查询优化建议
- 避免 N+1 查询:使用 JOIN 或批量查询
- 使用 EXPLAIN ANALYZE:分析查询计划
- 合理使用索引:避免过度索引
- 批量操作:使用
COPY或批量INSERT
7. 数据迁移策略
7.1 使用 Alembic(Python)或 Flyway(Java)
推荐使用 Alembic 进行数据库迁移管理。
7.2 迁移脚本示例
-- migrations/001_initial_schema.sql
-- 创建所有表、索引、约束
-- migrations/002_add_ai_jobs.sql
-- 添加 AI 任务表
-- migrations/003_add_comments.sql
-- 添加评论功能
7.3 数据迁移注意事项
- 向后兼容:新字段使用
DEFAULT或NULL - 分步迁移:大表迁移分步进行
- 回滚方案:准备回滚脚本
- 测试环境:先在测试环境验证
8. 安全考虑
8.1 行级安全(RLS)
-- 启用行级安全
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 慢查询监控
-- 启用慢查询日志
ALTER SYSTEM SET log_min_duration_statement = 1000; -- 记录超过 1 秒的查询
9.2 统计信息
-- 更新统计信息
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 设计亮点
- ✅ 规范化设计:遵循 3NF,减少冗余
- ✅ 灵活扩展:JSONB 存储灵活配置
- ✅ 性能优化:合理的索引策略
- ✅ 关联完整:所有关联关系都有外键约束
- ✅ 软删除:支持数据恢复
- ✅ 审计追踪:记录创建和更新时间
- ✅ 多态关联:支持灵活的关联关系
10.2 后续优化方向
- 分区:大表按时间分区
- 缓存:Redis 缓存热点数据
- 读写分离:主从复制
- 分库分表:如果数据量极大
10.3 剧本和附件模块设计亮点
- ✅ 灵活的剧本类型:支持文本和附件两种形式,满足不同输入方式
- ✅ 完整的版本管理:剧本版本历史表支持版本追溯和回滚
- ✅ 结构化剧本数据:角色表和场景表支持剧本的结构化管理
- ✅ 通用附件系统:附件表可复用于多种场景(剧本、合同、参考文档等)
- ✅ AI 生成支持:剧本表关联 AI 任务,支持 AI 辅助创作
- ✅ 审批流程:剧本状态管理支持草稿、审核、批准流程
- ✅ 全文搜索:剧本内容、角色名、场景信息均支持全文搜索
- ✅ 文件安全:附件表支持校验和、访问控制、过期时间
文档版本: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 迁移脚本,初始化数据库