feat: init

This commit is contained in:
zhenyi
2026-06-07 11:30:56 +08:00
commit 563381c1ca
361 changed files with 41327 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
-- 011: Announcement Channel — articles, comments, reactions, follows, cross-posts
--
-- New tables:
-- article, article_comment, article_reaction,
-- channel_follow, article_cross_post
-- ============================================================
-- 1. Articles
-- ============================================================
-- models/channels/articles.rs → article
CREATE TABLE IF NOT EXISTS article (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
author_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
title TEXT NOT NULL,
slug TEXT NOT NULL,
summary TEXT NULL,
body TEXT NOT NULL,
cover_image_url TEXT NULL,
status TEXT NOT NULL,
visibility TEXT NOT NULL,
tags TEXT[] NOT NULL,
published_at TIMESTAMPTZ NULL,
published_by UUID NULL REFERENCES "user"(id) ON DELETE SET NULL,
scheduled_at TIMESTAMPTZ NULL,
unpublished_at TIMESTAMPTZ NULL,
views_count BIGINT NOT NULL DEFAULT 0,
comments_count BIGINT NOT NULL DEFAULT 0,
reactions_count BIGINT NOT NULL DEFAULT 0,
cross_posted BOOLEAN NOT NULL DEFAULT FALSE,
cross_posted_from UUID NULL REFERENCES article(id) ON DELETE SET NULL,
metadata JSONB NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
deleted_at TIMESTAMPTZ NULL,
CONSTRAINT uq_article_channel_slug UNIQUE (channel_id, slug)
);
CREATE INDEX IF NOT EXISTS idx_article_channel_id ON article (channel_id);
CREATE INDEX IF NOT EXISTS idx_article_author_id ON article (author_id);
CREATE INDEX IF NOT EXISTS idx_article_status ON article (status);
CREATE INDEX IF NOT EXISTS idx_article_published_at ON article (published_at DESC) WHERE published_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_article_cross_posted_from ON article (cross_posted_from) WHERE cross_posted_from IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_article_deleted ON article (deleted_at) WHERE deleted_at IS NOT NULL;
-- ============================================================
-- 2. Article Comments
-- ============================================================
-- models/channels/article_comments.rs → article_comment
CREATE TABLE IF NOT EXISTS article_comment (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
article_id UUID NOT NULL REFERENCES article(id) ON DELETE CASCADE,
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
author_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
parent_comment_id UUID NULL REFERENCES article_comment(id) ON DELETE CASCADE,
body TEXT NOT NULL,
edited_at TIMESTAMPTZ NULL,
deleted_at TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_article_comment_article_id ON article_comment (article_id);
CREATE INDEX IF NOT EXISTS idx_article_comment_author_id ON article_comment (author_id);
CREATE INDEX IF NOT EXISTS idx_article_comment_parent ON article_comment (parent_comment_id) WHERE parent_comment_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_article_comment_deleted ON article_comment (deleted_at) WHERE deleted_at IS NOT NULL;
-- ============================================================
-- 3. Article Reactions
-- ============================================================
-- models/channels/article_reactions.rs → article_reaction
CREATE TABLE IF NOT EXISTS article_reaction (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
article_id UUID NOT NULL REFERENCES article(id) ON DELETE CASCADE,
channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
content TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
CONSTRAINT uq_article_reaction UNIQUE (article_id, user_id, content)
);
CREATE INDEX IF NOT EXISTS idx_article_reaction_article_id ON article_reaction (article_id);
CREATE INDEX IF NOT EXISTS idx_article_reaction_user_id ON article_reaction (user_id);
-- ============================================================
-- 4. Channel Follows
-- ============================================================
-- models/channels/channel_follows.rs → channel_follow
CREATE TABLE IF NOT EXISTS channel_follow (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_channel_id UUID NOT NULL REFERENCES channel(id) ON DELETE CASCADE,
target_workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
target_channel_id UUID NULL REFERENCES channel(id) ON DELETE SET NULL,
webhook_url TEXT NULL,
webhook_secret_ciphertext TEXT NULL,
enabled BOOLEAN NOT NULL,
followed_by UUID NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
unfollowed_at TIMESTAMPTZ NULL,
last_delivery_at TIMESTAMPTZ NULL,
last_delivery_status TEXT NULL,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
CONSTRAINT uq_channel_follow_source_target UNIQUE (source_channel_id, target_workspace_id, target_channel_id)
);
CREATE INDEX IF NOT EXISTS idx_channel_follow_source ON channel_follow (source_channel_id);
CREATE INDEX IF NOT EXISTS idx_channel_follow_target_ws ON channel_follow (target_workspace_id);
CREATE INDEX IF NOT EXISTS idx_channel_follow_target_channel ON channel_follow (target_channel_id) WHERE target_channel_id IS NOT NULL;
-- ============================================================
-- 5. Article Cross-Posts
-- ============================================================
-- models/channels/article_cross_posts.rs → article_cross_post
CREATE TABLE IF NOT EXISTS article_cross_post (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
article_id UUID NOT NULL REFERENCES article(id) ON DELETE CASCADE,
follow_id UUID NOT NULL REFERENCES channel_follow(id) ON DELETE CASCADE,
target_workspace_id UUID NOT NULL REFERENCES workspace(id) ON DELETE CASCADE,
target_channel_id UUID NULL REFERENCES channel(id) ON DELETE SET NULL,
status TEXT NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
last_error TEXT NULL,
sent_at TIMESTAMPTZ NULL,
delivered_at TIMESTAMPTZ NULL,
failed_at TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_article_cross_post_article_id ON article_cross_post (article_id);
CREATE INDEX IF NOT EXISTS idx_article_cross_post_follow_id ON article_cross_post (follow_id);
CREATE INDEX IF NOT EXISTS idx_article_cross_post_status ON article_cross_post (status);
CREATE INDEX IF NOT EXISTS idx_article_cross_post_target_ws ON article_cross_post (target_workspace_id);
-- ============================================================
-- 6. Triggers — auto-refresh updated_at
-- ============================================================
DROP TRIGGER IF EXISTS trg_article_updated_at ON article;
CREATE TRIGGER trg_article_updated_at BEFORE UPDATE ON article FOR EACH ROW EXECUTE FUNCTION set_updated_at();
DROP TRIGGER IF EXISTS trg_article_comment_updated_at ON article_comment;
CREATE TRIGGER trg_article_comment_updated_at BEFORE UPDATE ON article_comment FOR EACH ROW EXECUTE FUNCTION set_updated_at();
DROP TRIGGER IF EXISTS trg_channel_follow_updated_at ON channel_follow;
CREATE TRIGGER trg_channel_follow_updated_at BEFORE UPDATE ON channel_follow FOR EACH ROW EXECUTE FUNCTION set_updated_at();