Files
akmon/video_system_database.sql
2026-01-20 08:04:15 +08:00

1030 lines
40 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 视频系统数据库设计
-- 基于 ak_contents 扩展,支持视频内容、弹幕、评论、收藏、转发、点赞等功能
-- ================================
-- 重要说明:内容类型校验
-- ================================
-- 注意:由于 PostgreSQL 不支持在 CHECK 约束中使用子查询,
-- 以下表的内容类型校验需要在应用层实现:
--
-- 1. ak_video_danmakus: 应确保 content_id 对应的内容类型为 'video' 或 'audio'
-- 2. ak_video_play_records: 应确保 content_id 对应的内容类型为 'video' 或 'audio'
-- 3. ak_image_tags: 应确保 content_id 对应的内容类型为 'image'
-- 4. ak_image_view_records: 应确保 content_id 对应的内容类型为 'image'
--
-- 建议在应用层(如 Supabase RPC 函数)中添加这些校验逻辑
-- ================================
-- 1. 扩展 ak_contents 表,增加多媒体相关字段
ALTER TABLE ak_contents
ADD COLUMN IF NOT EXISTS content_type VARCHAR(20) DEFAULT 'article' CHECK (content_type IN ('article', 'video', 'audio', 'image')),
-- 视频相关字段
ADD COLUMN IF NOT EXISTS video_url TEXT,
ADD COLUMN IF NOT EXISTS video_duration INTEGER, -- 视频时长(秒)
ADD COLUMN IF NOT EXISTS video_poster TEXT, -- 视频封面
ADD COLUMN IF NOT EXISTS video_width INTEGER,
ADD COLUMN IF NOT EXISTS video_height INTEGER,
ADD COLUMN IF NOT EXISTS video_size BIGINT, -- 文件大小(字节)
ADD COLUMN IF NOT EXISTS video_format VARCHAR(10), -- mp4, webm, etc
ADD COLUMN IF NOT EXISTS video_quality VARCHAR(10), -- 720p, 1080p, 4k, etc
-- 音频相关字段
ADD COLUMN IF NOT EXISTS audio_url TEXT,
ADD COLUMN IF NOT EXISTS audio_duration INTEGER, -- 音频时长(秒)
ADD COLUMN IF NOT EXISTS audio_size BIGINT, -- 文件大小(字节)
ADD COLUMN IF NOT EXISTS audio_format VARCHAR(10), -- mp3, wav, flac, etc
ADD COLUMN IF NOT EXISTS audio_bitrate INTEGER, -- 比特率kbps
ADD COLUMN IF NOT EXISTS audio_sample_rate INTEGER, -- 采样率Hz
ADD COLUMN IF NOT EXISTS audio_cover TEXT, -- 音频封面图
-- 图片相关字段
ADD COLUMN IF NOT EXISTS image_url TEXT,
ADD COLUMN IF NOT EXISTS image_width INTEGER,
ADD COLUMN IF NOT EXISTS image_height INTEGER,
ADD COLUMN IF NOT EXISTS image_size BIGINT, -- 文件大小(字节)
ADD COLUMN IF NOT EXISTS image_format VARCHAR(10), -- jpg, png, webp, etc
ADD COLUMN IF NOT EXISTS image_quality VARCHAR(10), -- original, compressed, thumbnail
ADD COLUMN IF NOT EXISTS image_alt_text TEXT, -- 图片alt文本用于无障碍访问
-- 图集字段
ADD COLUMN IF NOT EXISTS images JSONB, -- 图集数据,支持多图片存储
-- 通用字段
ADD COLUMN IF NOT EXISTS allow_danmu BOOLEAN DEFAULT true, -- 视频/音频是否允许弹幕
ADD COLUMN IF NOT EXISTS allow_download BOOLEAN DEFAULT false, -- 是否允许下载
ADD COLUMN IF NOT EXISTS media_metadata JSONB; -- 存储额外的媒体元数据
-- 2. 多媒体弹幕表(支持视频和音频)
CREATE TABLE IF NOT EXISTS ak_video_danmakus (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content_id UUID REFERENCES ak_contents(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
user_name VARCHAR(100) NOT NULL,
text TEXT NOT NULL,
time_point DECIMAL(10,3) NOT NULL, -- 时间点(秒,支持小数),适用于视频和音频
color VARCHAR(7) DEFAULT '#FFFFFF', -- 弹幕颜色
font_size INTEGER DEFAULT 25, -- 字体大小
position_type VARCHAR(10) DEFAULT 'scroll' CHECK (position_type IN ('scroll', 'top', 'bottom')),
speed INTEGER DEFAULT 1, -- 滚动速度
is_visible BOOLEAN DEFAULT true,
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'hidden', 'deleted', 'reviewing')),
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 3. 用户行为表(点赞、收藏、转发等)
CREATE TABLE IF NOT EXISTS ak_user_interactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
content_id UUID REFERENCES ak_contents(id) ON DELETE CASCADE,
interaction_type VARCHAR(20) NOT NULL CHECK (interaction_type IN ('like', 'favorite', 'share', 'view', 'download')),
interaction_data JSONB, -- 额外数据如分享到的平台、收藏夹ID等
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, content_id, interaction_type) -- 防止重复操作
);
-- 4. 评论表(支持多级回复)
CREATE TABLE IF NOT EXISTS ak_content_comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content_id UUID REFERENCES ak_contents(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
user_name VARCHAR(100) NOT NULL,
parent_id UUID REFERENCES ak_content_comments(id) ON DELETE CASCADE, -- 父评论IDNULL表示主评论
reply_to_user_id UUID REFERENCES auth.users(id), -- 回复的用户ID
reply_to_user_name VARCHAR(100), -- 回复的用户名
content TEXT NOT NULL,
like_count INTEGER DEFAULT 0,
reply_count INTEGER DEFAULT 0, -- 回复数量
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'hidden', 'deleted', 'reviewing')),
is_pinned BOOLEAN DEFAULT false, -- 是否置顶
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 5. 评论点赞表
CREATE TABLE IF NOT EXISTS ak_comment_likes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
comment_id UUID REFERENCES ak_content_comments(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(comment_id, user_id) -- 防止重复点赞
);
-- 6. 媒体播放记录表(支持视频、音频)
CREATE TABLE IF NOT EXISTS ak_video_play_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content_id UUID REFERENCES ak_contents(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
play_position DECIMAL(10,3) DEFAULT 0, -- 播放位置(秒)
play_duration DECIMAL(10,3) DEFAULT 0, -- 播放时长(秒)
play_percentage DECIMAL(5,2) DEFAULT 0, -- 播放进度百分比
is_completed BOOLEAN DEFAULT false, -- 是否播放完成
device_type VARCHAR(20), -- 设备类型
resolution VARCHAR(10), -- 播放分辨率(视频专用)
quality VARCHAR(10), -- 播放质量(音频/视频)
play_speed DECIMAL(3,2) DEFAULT 1.0, -- 播放速度
volume INTEGER DEFAULT 100, -- 音量0-100
media_type VARCHAR(10), -- 媒体类型video, audio
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 7. 弹幕举报表
CREATE TABLE IF NOT EXISTS ak_danmu_reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
danmu_id UUID REFERENCES ak_video_danmakus(id) ON DELETE CASCADE,
reporter_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
report_type VARCHAR(20) NOT NULL CHECK (report_type IN ('spam', 'inappropriate', 'harassment', 'advertising')),
report_reason TEXT,
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'processed', 'rejected')),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 8. 内容统计表(缓存统计数据)
CREATE TABLE IF NOT EXISTS ak_content_statistics (
content_id UUID PRIMARY KEY REFERENCES ak_contents(id) ON DELETE CASCADE,
view_count INTEGER DEFAULT 0,
like_count INTEGER DEFAULT 0,
favorite_count INTEGER DEFAULT 0,
share_count INTEGER DEFAULT 0,
comment_count INTEGER DEFAULT 0,
danmu_count INTEGER DEFAULT 0, -- 弹幕数量(视频/音频专用)
play_completion_rate DECIMAL(5,2) DEFAULT 0, -- 播放完成率(视频/音频专用)
average_play_duration DECIMAL(10,3) DEFAULT 0, -- 平均播放时长(视频/音频专用)
download_count INTEGER DEFAULT 0, -- 下载次数
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 9. 图片标签表(用于图片分类和搜索)
CREATE TABLE IF NOT EXISTS ak_image_tags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content_id UUID REFERENCES ak_contents(id) ON DELETE CASCADE,
tag_name VARCHAR(50) NOT NULL,
tag_type VARCHAR(20) DEFAULT 'user' CHECK (tag_type IN ('user', 'auto', 'ai')), -- 标签来源用户添加、自动识别、AI生成
confidence DECIMAL(3,2), -- 置信度0-1适用于AI标签
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(content_id, tag_name)
);
-- 10. 图片浏览记录表
CREATE TABLE IF NOT EXISTS ak_image_view_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content_id UUID REFERENCES ak_contents(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
view_duration INTEGER DEFAULT 0, -- 浏览时长(秒)
zoom_level DECIMAL(4,2) DEFAULT 1.0, -- 缩放级别
device_type VARCHAR(20), -- 设备类型
screen_resolution VARCHAR(20), -- 屏幕分辨率
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ================================
-- 索引优化
-- ================================
-- 内容表索引
CREATE INDEX IF NOT EXISTS idx_contents_type ON ak_contents(content_type);
CREATE INDEX IF NOT EXISTS idx_contents_type_published ON ak_contents(content_type, published_at DESC);
-- 图集索引(使用 GIN 索引支持 JSONB 查询)
CREATE INDEX IF NOT EXISTS idx_contents_images_gin ON ak_contents USING GIN (images);
-- 弹幕表索引
CREATE INDEX IF NOT EXISTS idx_danmakus_content_time ON ak_video_danmakus(content_id, time_point);
CREATE INDEX IF NOT EXISTS idx_danmakus_user ON ak_video_danmakus(user_id);
CREATE INDEX IF NOT EXISTS idx_danmakus_status ON ak_video_danmakus(status);
-- 用户交互表索引
CREATE INDEX IF NOT EXISTS idx_interactions_user_type ON ak_user_interactions(user_id, interaction_type);
CREATE INDEX IF NOT EXISTS idx_interactions_content_type ON ak_user_interactions(content_id, interaction_type);
-- 评论表索引
CREATE INDEX IF NOT EXISTS idx_comments_content ON ak_content_comments(content_id, status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_comments_parent ON ak_content_comments(parent_id);
CREATE INDEX IF NOT EXISTS idx_comments_user ON ak_content_comments(user_id);
-- 播放记录表索引
CREATE INDEX IF NOT EXISTS idx_play_records_user_content ON ak_video_play_records(user_id, content_id);
CREATE INDEX IF NOT EXISTS idx_play_records_content_time ON ak_video_play_records(content_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_play_records_media_type ON ak_video_play_records(media_type);
-- 图片标签表索引
CREATE INDEX IF NOT EXISTS idx_image_tags_content ON ak_image_tags(content_id);
CREATE INDEX IF NOT EXISTS idx_image_tags_name ON ak_image_tags(tag_name);
CREATE INDEX IF NOT EXISTS idx_image_tags_type ON ak_image_tags(tag_type);
-- 图片浏览记录表索引
CREATE INDEX IF NOT EXISTS idx_image_views_user_content ON ak_image_view_records(user_id, content_id);
CREATE INDEX IF NOT EXISTS idx_image_views_content_time ON ak_image_view_records(content_id, created_at DESC);
-- ================================
-- 触发器和函数
-- ================================
-- 更新评论数量的触发器
CREATE OR REPLACE FUNCTION update_comment_counts()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
-- 更新内容统计
INSERT INTO ak_content_statistics (content_id, comment_count)
VALUES (NEW.content_id, 1)
ON CONFLICT (content_id)
DO UPDATE SET
comment_count = ak_content_statistics.comment_count + 1,
updated_at = NOW();
-- 更新父评论回复数
IF NEW.parent_id IS NOT NULL THEN
UPDATE ak_content_comments
SET reply_count = reply_count + 1
WHERE id = NEW.parent_id;
END IF;
ELSIF TG_OP = 'DELETE' THEN
-- 更新内容统计
UPDATE ak_content_statistics
SET comment_count = GREATEST(0, comment_count - 1),
updated_at = NOW()
WHERE content_id = OLD.content_id;
-- 更新父评论回复数
IF OLD.parent_id IS NOT NULL THEN
UPDATE ak_content_comments
SET reply_count = GREATEST(0, reply_count - 1)
WHERE id = OLD.parent_id;
END IF;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tr_comment_counts
AFTER INSERT OR DELETE ON ak_content_comments
FOR EACH ROW EXECUTE FUNCTION update_comment_counts();
-- 更新弹幕数量的触发器
CREATE OR REPLACE FUNCTION update_danmu_counts()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO ak_content_statistics (content_id, danmu_count)
VALUES (NEW.content_id, 1)
ON CONFLICT (content_id)
DO UPDATE SET
danmu_count = ak_content_statistics.danmu_count + 1,
updated_at = NOW();
ELSIF TG_OP = 'DELETE' THEN
UPDATE ak_content_statistics
SET danmu_count = GREATEST(0, danmu_count - 1),
updated_at = NOW()
WHERE content_id = OLD.content_id;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tr_danmu_counts
AFTER INSERT OR DELETE ON ak_video_danmakus
FOR EACH ROW EXECUTE FUNCTION update_danmu_counts();
-- 更新用户交互统计的触发器
CREATE OR REPLACE FUNCTION update_interaction_stats()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO ak_content_statistics (content_id)
VALUES (NEW.content_id)
ON CONFLICT (content_id) DO NOTHING;
UPDATE ak_content_statistics
SET
view_count = CASE WHEN NEW.interaction_type = 'view' THEN view_count + 1 ELSE view_count END,
like_count = CASE WHEN NEW.interaction_type = 'like' THEN like_count + 1 ELSE like_count END,
favorite_count = CASE WHEN NEW.interaction_type = 'favorite' THEN favorite_count + 1 ELSE favorite_count END,
share_count = CASE WHEN NEW.interaction_type = 'share' THEN share_count + 1 ELSE share_count END,
updated_at = NOW()
WHERE content_id = NEW.content_id;
ELSIF TG_OP = 'DELETE' THEN
UPDATE ak_content_statistics
SET
view_count = CASE WHEN OLD.interaction_type = 'view' THEN GREATEST(0, view_count - 1) ELSE view_count END,
like_count = CASE WHEN OLD.interaction_type = 'like' THEN GREATEST(0, like_count - 1) ELSE like_count END,
favorite_count = CASE WHEN OLD.interaction_type = 'favorite' THEN GREATEST(0, favorite_count - 1) ELSE favorite_count END,
share_count = CASE WHEN OLD.interaction_type = 'share' THEN GREATEST(0, share_count - 1) ELSE share_count END,
updated_at = NOW()
WHERE content_id = OLD.content_id;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER tr_interaction_stats
AFTER INSERT OR DELETE ON ak_user_interactions
FOR EACH ROW EXECUTE FUNCTION update_interaction_stats();
-- ================================
-- RLS 安全策略
-- ================================
-- 启用 RLS
ALTER TABLE ak_video_danmakus ENABLE ROW LEVEL SECURITY;
ALTER TABLE ak_user_interactions ENABLE ROW LEVEL SECURITY;
ALTER TABLE ak_content_comments ENABLE ROW LEVEL SECURITY;
ALTER TABLE ak_comment_likes ENABLE ROW LEVEL SECURITY;
ALTER TABLE ak_video_play_records ENABLE ROW LEVEL SECURITY;
ALTER TABLE ak_danmu_reports ENABLE ROW LEVEL SECURITY;
ALTER TABLE ak_image_tags ENABLE ROW LEVEL SECURITY;
ALTER TABLE ak_image_view_records ENABLE ROW LEVEL SECURITY;
-- 弹幕策略
CREATE POLICY "Public can view danmakus" ON ak_video_danmakus
FOR SELECT USING (status = 'active' AND is_visible = true);
CREATE POLICY "Users can insert own danmakus" ON ak_video_danmakus
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own danmakus" ON ak_video_danmakus
FOR UPDATE USING (auth.uid() = user_id);
-- 用户交互策略
CREATE POLICY "Users can view own interactions" ON ak_user_interactions
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own interactions" ON ak_user_interactions
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete own interactions" ON ak_user_interactions
FOR DELETE USING (auth.uid() = user_id);
-- 评论策略
CREATE POLICY "Public can view active comments" ON ak_content_comments
FOR SELECT USING (status = 'active');
CREATE POLICY "Users can insert own comments" ON ak_content_comments
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own comments" ON ak_content_comments
FOR UPDATE USING (auth.uid() = user_id);
-- 播放记录策略
CREATE POLICY "Users can view own play records" ON ak_video_play_records
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own play records" ON ak_video_play_records
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own play records" ON ak_video_play_records
FOR UPDATE USING (auth.uid() = user_id);
-- 图片标签策略
CREATE POLICY "Public can view image tags" ON ak_image_tags
FOR SELECT USING (true);
CREATE POLICY "Users can add image tags" ON ak_image_tags
FOR INSERT WITH CHECK (true); -- 允许所有认证用户添加标签
-- 图片浏览记录策略
CREATE POLICY "Users can view own image records" ON ak_image_view_records
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own image records" ON ak_image_view_records
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- ================================
-- 查询视图
-- ================================
-- 多媒体内容详情视图
CREATE OR REPLACE VIEW vw_media_content_detail AS
SELECT
c.id,
c.content_type,
-- 视频字段
c.video_url,
c.video_duration,
c.video_poster,
c.video_width,
c.video_height,
c.video_size,
c.video_format,
c.video_quality,
-- 音频字段
c.audio_url,
c.audio_duration,
c.audio_size,
c.audio_format,
c.audio_bitrate,
c.audio_sample_rate,
c.audio_cover,
-- 图片字段
c.image_url,
c.image_width,
c.image_height,
c.image_size,
c.image_format,
c.image_quality,
c.image_alt_text,
c.images,
-- 通用字段
c.allow_danmu,
c.allow_download,
c.media_metadata,
-- 统计字段
COALESCE(s.view_count, 0) as view_count,
COALESCE(s.like_count, 0) as like_count,
COALESCE(s.favorite_count, 0) as favorite_count,
COALESCE(s.share_count, 0) as share_count,
COALESCE(s.comment_count, 0) as comment_count,
COALESCE(s.danmu_count, 0) as danmu_count,
COALESCE(s.play_completion_rate, 0) as play_completion_rate,
COALESCE(s.average_play_duration, 0) as average_play_duration,
COALESCE(s.download_count, 0) as download_count,
-- 检查当前用户是否已点赞、收藏
EXISTS(SELECT 1 FROM ak_user_interactions ui WHERE ui.content_id = c.id AND ui.user_id = auth.uid() AND ui.interaction_type = 'like') as is_liked,
EXISTS(SELECT 1 FROM ak_user_interactions ui WHERE ui.content_id = c.id AND ui.user_id = auth.uid() AND ui.interaction_type = 'favorite') as is_favorited
FROM ak_contents c
LEFT JOIN ak_content_statistics s ON c.id = s.content_id;
-- 视频内容详情视图(向后兼容)
CREATE OR REPLACE VIEW vw_video_content_detail AS
SELECT * FROM vw_media_content_detail
WHERE content_type = 'video';
-- 音频内容详情视图
CREATE OR REPLACE VIEW vw_audio_content_detail AS
SELECT * FROM vw_media_content_detail
WHERE content_type = 'audio';
-- 图片内容详情视图
CREATE OR REPLACE VIEW vw_image_content_detail AS
SELECT
c.id,
c.content_type,
c.image_url,
c.image_width,
c.image_height,
c.image_size,
c.image_format,
c.image_quality,
c.image_alt_text,
c.images,
c.allow_download,
c.media_metadata,
COALESCE(s.view_count, 0) as view_count,
COALESCE(s.like_count, 0) as like_count,
COALESCE(s.favorite_count, 0) as favorite_count,
COALESCE(s.share_count, 0) as share_count,
COALESCE(s.comment_count, 0) as comment_count,
COALESCE(s.download_count, 0) as download_count,
-- 图片标签
ARRAY_AGG(DISTINCT it.tag_name) FILTER (WHERE it.tag_name IS NOT NULL) as tags,
-- 检查当前用户是否已点赞、收藏
EXISTS(SELECT 1 FROM ak_user_interactions ui WHERE ui.content_id = c.id AND ui.user_id = auth.uid() AND ui.interaction_type = 'like') as is_liked,
EXISTS(SELECT 1 FROM ak_user_interactions ui WHERE ui.content_id = c.id AND ui.user_id = auth.uid() AND ui.interaction_type = 'favorite') as is_favorited
FROM ak_contents c
LEFT JOIN ak_content_statistics s ON c.id = s.content_id
LEFT JOIN ak_image_tags it ON c.id = it.content_id
WHERE c.content_type = 'image'
GROUP BY c.id, c.content_type, c.image_url, c.image_width, c.image_height, c.image_size, c.image_format, c.image_quality, c.image_alt_text, c.images, c.allow_download, c.media_metadata, s.view_count, s.like_count, s.favorite_count, s.share_count, s.comment_count, s.download_count;
-- 弹幕列表视图(支持视频和音频)
CREATE OR REPLACE VIEW vw_video_danmakus AS
SELECT
d.id,
d.content_id,
d.user_id,
d.user_name,
d.text,
d.time_point,
d.color,
d.font_size,
d.position_type,
d.speed,
d.is_visible,
d.status,
d.created_at,
d.updated_at,
c.content_type,
-- 显示用户敏感信息(仅对自己的弹幕)
CASE WHEN d.user_id = auth.uid() THEN d.ip_address ELSE NULL END as ip_address,
CASE WHEN d.user_id = auth.uid() THEN d.user_agent ELSE NULL END as user_agent
FROM ak_video_danmakus d
JOIN ak_contents c ON d.content_id = c.id
WHERE d.status = 'active' AND d.is_visible = true
ORDER BY d.time_point ASC;
-- 评论树形视图
CREATE OR REPLACE VIEW vw_content_comments AS
WITH RECURSIVE comment_tree AS (
-- 主评论
SELECT
c.*,
0 as level,
ARRAY[c.created_at] as sort_path,
EXISTS(SELECT 1 FROM ak_comment_likes cl WHERE cl.comment_id = c.id AND cl.user_id = auth.uid()) as is_liked_by_user
FROM ak_content_comments c
WHERE c.parent_id IS NULL AND c.status = 'active'
UNION ALL
-- 子评论
SELECT
c.*,
ct.level + 1,
ct.sort_path || c.created_at,
EXISTS(SELECT 1 FROM ak_comment_likes cl WHERE cl.comment_id = c.id AND cl.user_id = auth.uid()) as is_liked_by_user
FROM ak_content_comments c
JOIN comment_tree ct ON c.parent_id = ct.id
WHERE c.status = 'active' AND ct.level < 3 -- 限制评论层级
)
SELECT * FROM comment_tree ORDER BY sort_path;
-- 热门标签视图(图片专用)
CREATE OR REPLACE VIEW vw_popular_image_tags AS
SELECT
tag_name,
COUNT(*) as usage_count,
tag_type,
AVG(confidence) FILTER (WHERE confidence IS NOT NULL) as avg_confidence
FROM ak_image_tags
GROUP BY tag_name, tag_type
ORDER BY usage_count DESC;
-- 用户媒体消费统计视图
CREATE OR REPLACE VIEW vw_user_media_stats AS
SELECT
ui.user_id,
COUNT(CASE WHEN c.content_type = 'video' THEN 1 END) as videos_watched,
COUNT(CASE WHEN c.content_type = 'audio' THEN 1 END) as audios_listened,
COUNT(CASE WHEN c.content_type = 'image' THEN 1 END) as images_viewed,
SUM(CASE WHEN c.content_type IN ('video', 'audio') THEN vpr.play_duration ELSE 0 END) as total_play_time,
AVG(CASE WHEN c.content_type IN ('video', 'audio') THEN vpr.play_percentage ELSE NULL END) as avg_completion_rate
FROM ak_user_interactions ui
LEFT JOIN ak_contents c ON ui.content_id = c.id
LEFT JOIN ak_video_play_records vpr ON ui.content_id = vpr.content_id AND ui.user_id = vpr.user_id
WHERE ui.interaction_type = 'view'
GROUP BY ui.user_id;
-- ================================
-- 兼容性检查和视图优化
-- ================================
-- 创建一个更安全的视图,只使用我们确定存在的列
-- 如果您的 ak_contents 表有更多列(如 title, content, excerpt 等),
-- 可以在部署后手动添加到视图中
-- 检查表结构的辅助函数
CREATE OR REPLACE FUNCTION check_column_exists(table_name TEXT, column_name TEXT)
RETURNS BOOLEAN
LANGUAGE plpgsql
AS $$
BEGIN
RETURN EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = lower(table_name)
AND column_name = lower(column_name)
AND table_schema = current_schema()
);
END;
$$;
-- ================================
-- 示例 RPC 函数:带内容类型校验的数据插入
-- ================================
-- 插入弹幕(带内容类型校验)
CREATE OR REPLACE FUNCTION insert_danmu_with_validation(
p_content_id UUID,
p_content TEXT,
p_time_position DECIMAL,
p_color VARCHAR DEFAULT '#FFFFFF',
p_font_size INTEGER DEFAULT 16,
p_position_type VARCHAR DEFAULT 'scroll'
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_content_type VARCHAR(20);
v_danmu_id UUID;
BEGIN
-- 检查内容类型
SELECT content_type INTO v_content_type
FROM ak_contents
WHERE id = p_content_id;
IF v_content_type IS NULL THEN
RAISE EXCEPTION '内容不存在';
END IF;
IF v_content_type NOT IN ('video', 'audio') THEN
RAISE EXCEPTION '只有视频和音频内容支持弹幕功能';
END IF;
-- 插入弹幕
INSERT INTO ak_video_danmakus (
content_id, user_id, user_name, text, time_point,
color, font_size, position_type
) VALUES (
p_content_id, auth.uid(),
COALESCE((SELECT email FROM auth.users WHERE id = auth.uid()), 'Anonymous'),
p_content, p_time_position,
p_color, p_font_size, p_position_type
) RETURNING id INTO v_danmu_id;
RETURN v_danmu_id;
END;
$$;
-- 插入图片标签(带内容类型校验)
CREATE OR REPLACE FUNCTION insert_image_tag_with_validation(
p_content_id UUID,
p_tag_name VARCHAR,
p_tag_type VARCHAR DEFAULT 'user',
p_confidence DECIMAL DEFAULT NULL
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_content_type VARCHAR(20);
v_tag_id UUID;
BEGIN
-- 检查内容类型
SELECT content_type INTO v_content_type
FROM ak_contents
WHERE id = p_content_id;
IF v_content_type IS NULL THEN
RAISE EXCEPTION '内容不存在';
END IF;
IF v_content_type != 'image' THEN
RAISE EXCEPTION '只有图片内容支持标签功能';
END IF;
-- 插入标签
INSERT INTO ak_image_tags (
content_id, tag_name, tag_type, confidence
) VALUES (
p_content_id, p_tag_name, p_tag_type, p_confidence
) RETURNING id INTO v_tag_id;
RETURN v_tag_id;
END;
$$;
-- 记录播放进度(带内容类型校验)
CREATE OR REPLACE FUNCTION record_play_progress_with_validation(
p_content_id UUID,
p_play_position DECIMAL,
p_play_duration DECIMAL DEFAULT 0,
p_device_type VARCHAR DEFAULT NULL,
p_resolution VARCHAR DEFAULT NULL,
p_quality VARCHAR DEFAULT NULL
)
RETURNS UUID
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
v_content_type VARCHAR(20);
v_record_id UUID;
v_play_percentage DECIMAL(5,2);
v_total_duration DECIMAL(10,3);
BEGIN
-- 检查内容类型和获取时长
SELECT
content_type,
CASE
WHEN content_type = 'video' THEN video_duration
WHEN content_type = 'audio' THEN audio_duration
ELSE NULL
END
INTO v_content_type, v_total_duration
FROM ak_contents
WHERE id = p_content_id;
IF v_content_type IS NULL THEN
RAISE EXCEPTION '内容不存在';
END IF;
IF v_content_type NOT IN ('video', 'audio') THEN
RAISE EXCEPTION '只有视频和音频内容支持播放记录';
END IF;
-- 计算播放百分比
IF v_total_duration IS NOT NULL AND v_total_duration > 0 THEN
v_play_percentage = (p_play_position / v_total_duration) * 100;
v_play_percentage = LEAST(v_play_percentage, 100); -- 限制最大100%
ELSE
v_play_percentage = 0;
END IF;
-- 插入或更新播放记录
INSERT INTO ak_video_play_records (
content_id, user_id, play_position, play_duration,
play_percentage, is_completed, device_type, resolution, quality, media_type
) VALUES (
p_content_id, auth.uid(), p_play_position, p_play_duration,
v_play_percentage, (v_play_percentage >= 95), p_device_type,
p_resolution, p_quality, v_content_type
)
ON CONFLICT (content_id, user_id)
DO UPDATE SET
play_position = EXCLUDED.play_position,
play_duration = EXCLUDED.play_duration,
play_percentage = EXCLUDED.play_percentage,
is_completed = EXCLUDED.is_completed,
updated_at = NOW()
RETURNING id INTO v_record_id;
RETURN v_record_id;
END;
$$;
-- ================================
-- 模拟数据插入
-- ================================
-- 首先插入5个多媒体内容到 ak_contents 表
INSERT INTO ak_contents (
id, title, content, summary, status, content_type, original_language, quality_score,
video_url, video_duration, video_poster, video_width, video_height,
video_size, video_format, video_quality, audio_url, audio_duration, audio_size,
audio_format, audio_bitrate, audio_sample_rate, audio_cover, image_url, image_width,
image_height, image_size, image_format, image_quality, image_alt_text, images,
allow_danmu, allow_download, media_metadata, published_at, updated_at
) VALUES
-- 视频内容1
(
gen_random_uuid(),
'星际征途:未来科幻大片',
'一部震撼人心的科幻电影,讲述了人类在星际时代的冒险故事。导演张导演运用先进的特效技术,为观众呈现了一个令人惊叹的未来世界。这部影片不仅具有视觉冲击力,更在情感层面触动人心,是一部值得反复观看的科幻佳作。',
'震撼人心的科幻电影,展现未来世界的冒险故事',
'published',
'video',
'zh-CN',
8.5,
'https://example.com/videos/sample-video-1.mp4', 1800,
'https://example.com/posters/video-1-poster.jpg', 1920, 1080,
524288000, 'mp4', '1080p',
NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
true, false,
'{"director": "张导演", "genre": "科幻", "rating": 8.5, "year": 2024, "subtitle_languages": ["zh-CN", "en-US"], "tags": ["科幻", "冒险", "未来", "特效"]}',
NOW() - INTERVAL '2 days', NOW()
),
-- 视频内容2
(
gen_random_uuid(),
'血战江湖:经典动作片',
'李导演执导的动作大片,以精彩的打斗场面和紧张的剧情著称。影片采用实拍与特效相结合的方式,为观众带来视觉盛宴。每一个动作场面都经过精心设计,展现了中国功夫的精髓,是动作片爱好者不可错过的经典之作。',
'李导演执导的动作大片,精彩打斗场面引人入胜',
'published',
'video',
'zh-CN',
9.2,
'https://example.com/videos/sample-video-2.mp4', 3600,
'https://example.com/posters/video-2-poster.jpg', 1920, 1080,
1048576000, 'mp4', '1080p',
NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
true, true,
'{"director": "李导演", "genre": "动作", "rating": 9.2, "year": 2024, "subtitle_languages": ["zh-CN", "en-US", "ja-JP"], "tags": ["动作", "功夫", "江湖", "经典"]}',
NOW() - INTERVAL '1 day', NOW()
),
-- 音频内容1
(
gen_random_uuid(),
'夜空中最亮的星 - 王歌手',
'王歌手的最新单曲,旋律优美动听,歌词深情感人。这首歌曲融合了流行与民谣元素,展现了歌手成熟的音乐风格。歌曲以夜空为背景,表达了对理想和爱情的追求,是一首充满正能量的励志歌曲。',
'王歌手最新单曲,旋律优美歌词深情',
'published',
'audio',
'zh-CN',
8.8,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
'https://example.com/audios/sample-audio-1.mp3', 240,
10485760, 'mp3', 320, 44100,
'https://example.com/covers/audio-1-cover.jpg',
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
true, false,
'{"artist": "王歌手", "album": "热门单曲", "genre": "流行", "year": 2024, "lyrics": true, "tags": ["流行", "励志", "爱情", "民谣"]}',
NOW() - INTERVAL '3 days', NOW()
),
-- 图片内容1
(
gen_random_uuid(),
'杭州西湖美景摄影作品',
'陈摄影师使用Canon EOS R5相机拍摄的西湖美景。照片完美捕捉了西湖的宁静与美丽展现了中国古典园林的魅力。作品运用了经典的构图技巧光影效果处理精美是风景摄影的典型代表作品。',
'陈摄影师拍摄的西湖美景,展现古典园林魅力',
'published',
'image',
'zh-CN',
9.0,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL,
'https://example.com/images/sample-image-1.jpg', 2048, 1536,
2097152, 'jpg', 'original', '美丽的风景照片',
NULL,
false, true,
'{"photographer": "陈摄影师", "location": "杭州西湖", "camera": "Canon EOS R5", "taken_at": "2024-06-15", "tags": ["风景", "西湖", "摄影", "古典"]}',
NOW() - INTERVAL '5 days', NOW()
),
-- 图集内容1
(
gen_random_uuid(),
'云南旅行摄影图集',
'李摄影师云南之行的精彩摄影作品集。从海边日落到山顶风光,再到城市夜景,每一张照片都记录了云南的美丽瞬间。这个图集展现了云南的多样性,从自然风光到人文景观,是一次完整的视觉旅行体验。',
'李摄影师云南旅行摄影作品,记录美丽瞬间',
'published',
'image',
'zh-CN',
8.7,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL,
'https://example.com/images/gallery-1-cover.jpg', 1920, 1080,
1572864, 'jpg', 'original', '旅游图集封面',
'[
{
"url": "https://example.com/images/gallery-1-1.jpg",
"width": 1920, "height": 1080, "size": 1572864,
"alt": "海边日落", "order": 1, "description": "云南大理洱海日落美景"
},
{
"url": "https://example.com/images/gallery-1-2.jpg",
"width": 1920, "height": 1080, "size": 1310720,
"alt": "山顶风光", "order": 2, "description": "玉龙雪山山顶壮丽风光"
},
{
"url": "https://example.com/images/gallery-1-3.jpg",
"width": 1920, "height": 1080, "size": 1835008,
"alt": "城市夜景", "order": 3, "description": "昆明市区夜景璀璨灯火"
}
]'::jsonb,
false, true,
'{"photographer": "李摄影师", "trip": "云南旅行", "total_images": 3, "created_date": "2024-06-20", "tags": ["旅行", "云南", "图集", "风光"]}',
NOW() - INTERVAL '4 days', NOW()
);
-- 获取插入的内容ID用于后续插入统计数据
WITH inserted_contents AS (
SELECT id, content_type FROM ak_contents
WHERE created_at >= NOW() - INTERVAL '1 hour'
ORDER BY created_at DESC
LIMIT 5
)
-- 为这些内容初始化统计数据
INSERT INTO ak_content_statistics (
content_id, view_count, like_count, favorite_count, share_count,
comment_count, danmu_count, play_completion_rate, average_play_duration, download_count
)
SELECT
id,
FLOOR(RANDOM() * 1000 + 100)::INTEGER, -- 100-1099 views
FLOOR(RANDOM() * 200 + 10)::INTEGER, -- 10-209 likes
FLOOR(RANDOM() * 50 + 5)::INTEGER, -- 5-54 favorites
FLOOR(RANDOM() * 30 + 2)::INTEGER, -- 2-31 shares
FLOOR(RANDOM() * 20 + 1)::INTEGER, -- 1-20 comments
CASE WHEN content_type IN ('video', 'audio') THEN FLOOR(RANDOM() * 100 + 10)::INTEGER ELSE 0 END, -- 弹幕数
CASE WHEN content_type IN ('video', 'audio') THEN ROUND((RANDOM() * 30 + 70)::NUMERIC, 2) ELSE 0 END, -- 70-100% 完成率
CASE WHEN content_type IN ('video', 'audio') THEN ROUND((RANDOM() * 300 + 60)::NUMERIC, 3) ELSE 0 END, -- 平均播放时长
CASE WHEN content_type = 'image' THEN FLOOR(RANDOM() * 20 + 1)::INTEGER ELSE 0 END -- 下载次数
FROM inserted_contents;
-- 为图片内容添加标签
WITH image_contents AS (
SELECT id FROM ak_contents
WHERE content_type = 'image'
AND created_at >= NOW() - INTERVAL '1 hour'
LIMIT 2
),
image_with_row AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY id) as rn FROM image_contents
),
image_tags AS (
SELECT
id,
CASE
WHEN rn = 1 THEN ARRAY['风景', '西湖', '自然', '摄影']
WHEN rn = 2 THEN ARRAY['旅游', '图集', '云南', '风光', '摄影']
END as tag_array
FROM image_with_row
)
INSERT INTO ak_image_tags (content_id, tag_name, tag_type, confidence)
SELECT
id,
tag_name,
'user',
NULL
FROM image_tags
CROSS JOIN LATERAL unnest(tag_array) AS tag_name;
-- 添加一些示例弹幕(仅为视频和音频内容)
WITH media_contents AS (
SELECT id, content_type FROM ak_contents
WHERE content_type IN ('video', 'audio')
AND created_at >= NOW() - INTERVAL '1 hour'
LIMIT 3
)
INSERT INTO ak_video_danmakus (
content_id, user_id, user_name, text, time_point, color, font_size, position_type, status
)
SELECT
id,
auth.uid(),
'演示用户',
CASE content_type
WHEN 'video' THEN
(ARRAY['太精彩了!', '画质真棒', '这个特效牛', '导演厉害', '期待续集'])[FLOOR(RANDOM() * 5 + 1)]
WHEN 'audio' THEN
(ARRAY['好听!', '单曲循环', '歌手唱功不错', '这首歌很有感觉', '音质很棒'])[FLOOR(RANDOM() * 5 + 1)]
END,
ROUND((RANDOM() * 300)::NUMERIC, 3), -- 随机时间点
(ARRAY['#FFFFFF', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4'])[FLOOR(RANDOM() * 5 + 1)], -- 随机颜色
(ARRAY[16, 18, 20, 22, 24])[FLOOR(RANDOM() * 5 + 1)], -- 随机字体大小
(ARRAY['scroll', 'top', 'bottom'])[FLOOR(RANDOM() * 3 + 1)], -- 随机位置
'active'
FROM media_contents,
generate_series(1, 3) -- 每个媒体内容生成3条弹幕
WHERE id IS NOT NULL;
-- 添加一些示例评论
WITH all_contents AS (
SELECT id FROM ak_contents
WHERE created_at >= NOW() - INTERVAL '1 hour'
LIMIT 5
)
INSERT INTO ak_content_comments (
content_id, user_id, user_name, content, status
)
SELECT
id,
auth.uid(),
'评论用户',
(ARRAY[
'这个内容质量很高,值得推荐!',
'制作精良,很有意思。',
'感谢分享,学到了很多。',
'希望能看到更多这样的作品。',
'内容很棒,期待更新!'
])[FLOOR(RANDOM() * 5 + 1)],
'active'
FROM all_contents,
generate_series(1, 2) -- 每个内容生成2条评论
WHERE id IS NOT NULL;
-- 添加示例用户交互(点赞、收藏等)
WITH all_contents AS (
SELECT id FROM ak_contents
WHERE created_at >= NOW() - INTERVAL '1 hour'
LIMIT 5
)
INSERT INTO ak_user_interactions (
user_id, content_id, interaction_type, interaction_data
)
SELECT
auth.uid(),
id,
interaction_type,
CASE interaction_type
WHEN 'share' THEN jsonb_build_object('platform', 'weibo', 'shared_at', NOW())
WHEN 'favorite' THEN jsonb_build_object('folder', '我的收藏', 'added_at', NOW())
ELSE '{}'::jsonb
END
FROM all_contents
CROSS JOIN (
VALUES ('like'), ('favorite'), ('view'), ('share')
) AS interactions(interaction_type)
WHERE id IS NOT NULL;
-- 模拟数据插入完成
-- 已成功插入5个多媒体内容2个视频、1个音频、2个图片/图集)
-- 包含完整的统计数据、标签、弹幕、评论和用户交互记录
-- 允许所有用户(包括 anon插入播放记录
CREATE POLICY "Anon can insert play records" ON ak_video_play_records
FOR INSERT WITH CHECK (true);
-- 应用策略
ALTER TABLE ak_video_play_records ENABLE ROW LEVEL SECURITY;
-- 认证用户只能插入自己的记录
CREATE POLICY "Users can insert own play records" ON ak_video_play_records
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Anon can insert play records" ON ak_video_play_records
FOR INSERT WITH CHECK (
(auth.role() = 'anon' AND user_id = '00000000-0000-0000-0000-000000000000')
OR (auth.role() = 'anon' AND user_id IS NULL)
);