-- =========================================================================== -- artalk-cf —— D1 / SQLite schema -- 执行:npx wrangler d1 execute artalk-cf --remote --file=./schema.sql -- 设计与 Artalk v2 的 gorm 模型对应,但时间统一存 unix 毫秒(INTEGER), -- 输出时再按 Asia/Shanghai 格式化成 "YYYY-MM-DD HH:mm:ss"。 -- =========================================================================== CREATE TABLE IF NOT EXISTS sites ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, urls TEXT NOT NULL DEFAULT '', -- 逗号分隔 created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS pages ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT NOT NULL, -- Artalk 的 page_key,如 /20210911.html site_name TEXT NOT NULL, title TEXT NOT NULL DEFAULT '', admin_only INTEGER NOT NULL DEFAULT 0, pv INTEGER NOT NULL DEFAULT 0, vote_up INTEGER NOT NULL DEFAULT 0, vote_down INTEGER NOT NULL DEFAULT 0, accessible_url TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS idx_pages_key_site ON pages (key, site_name); CREATE INDEX IF NOT EXISTS idx_pages_site ON pages (site_name); CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL, link TEXT NOT NULL DEFAULT '', password TEXT NOT NULL DEFAULT '', -- 空 = 无密码;支持 (pbkdf2) / (md5) / 明文(兼容) badge_name TEXT NOT NULL DEFAULT '', badge_color TEXT NOT NULL DEFAULT '', title_name TEXT NOT NULL DEFAULT '', -- 头衔徽章(金色,优先级仅次于系统徽章) last_ip TEXT NOT NULL DEFAULT '', last_ua TEXT NOT NULL DEFAULT '', is_admin INTEGER NOT NULL DEFAULT 0, receive_email INTEGER NOT NULL DEFAULT 1, is_in_conf INTEGER NOT NULL DEFAULT 0, token_valid_from INTEGER NOT NULL DEFAULT 0, -- 早于此刻签发的 token 失效(改密码后踢下线) created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS idx_users_name_email ON users (name, email); CREATE INDEX IF NOT EXISTS idx_users_email ON users (email); CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, page_key TEXT NOT NULL, site_name TEXT NOT NULL, user_id INTEGER NOT NULL, rid INTEGER NOT NULL DEFAULT 0, -- 直接父节点 id(0 = 顶层) root_id INTEGER NOT NULL DEFAULT 0, -- 顶层祖先 id(顶层为 0) is_collapsed INTEGER NOT NULL DEFAULT 0, is_pending INTEGER NOT NULL DEFAULT 0, is_pinned INTEGER NOT NULL DEFAULT 0, is_verified INTEGER NOT NULL DEFAULT 0, vote_up INTEGER NOT NULL DEFAULT 0, vote_down INTEGER NOT NULL DEFAULT 0, ua TEXT NOT NULL DEFAULT '', ip TEXT NOT NULL DEFAULT '', ip_region TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, deleted_at INTEGER NOT NULL DEFAULT 0 -- 软删除,0 = 未删除 ); CREATE INDEX IF NOT EXISTS idx_comments_page ON comments (page_key, site_name, deleted_at); CREATE INDEX IF NOT EXISTS idx_comments_root ON comments (root_id, deleted_at); CREATE INDEX IF NOT EXISTS idx_comments_user ON comments (user_id, deleted_at); CREATE INDEX IF NOT EXISTS idx_comments_created ON comments (created_at); -- 列表/计数覆盖索引:原来列表要 ORDER BY is_pinned DESC, created_at DESC, -- 没有匹配索引 → SQLite 每次全表扫 ~1000 行(实测列表 940 行/次、计数 1500-2000 行/次, -- 是 D1 免费额度被打爆的主因)。带上这些列后只读命中行。 CREATE INDEX IF NOT EXISTS idx_comments_page_list ON comments (site_name, page_key, deleted_at, is_pending, is_pinned DESC, created_at DESC, id DESC); -- 子评论拉取(root_id IN (...))专用:让 SQLite 按 root_id seek,而不是扫整页 CREATE INDEX IF NOT EXISTS idx_comments_children ON comments (root_id, deleted_at, is_pending, created_at, id); CREATE INDEX IF NOT EXISTS idx_comments_site_list ON comments (site_name, deleted_at, is_pending, created_at DESC, id DESC); CREATE TABLE IF NOT EXISTS votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, target_id INTEGER NOT NULL, user_id INTEGER NOT NULL, type TEXT NOT NULL, -- 'comment' | 'page' created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS idx_votes_unique ON votes (target_id, user_id, type); CREATE TABLE IF NOT EXISTS notifies ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, comment_id INTEGER NOT NULL, is_read INTEGER NOT NULL DEFAULT 0, is_emailed INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_notifies_user ON notifies (user_id, is_read); -- 键值配置:frontend_conf / admin_users / moderator / captcha / ip_region / ... CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL ); -- 简单限流:按 IP + 动作统计,60 秒窗口 CREATE TABLE IF NOT EXISTS rate_limits ( bucket TEXT PRIMARY KEY, -- 如 "comment:1.2.3.4" count INTEGER NOT NULL DEFAULT 0, expires_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_rate_expires ON rate_limits (expires_at); -- 验证码通过状态(按 IP 记,跨域请求带不上 cookie,所以放服务端) CREATE TABLE IF NOT EXISTS captcha_passes ( ip TEXT PRIMARY KEY, expires_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_captcha_expires ON captcha_passes (expires_at); -- 验证码题目:answer 只存哈希,防止有人直接读库 CREATE TABLE IF NOT EXISTS captcha_challenges ( id TEXT PRIMARY KEY, answer_hash TEXT NOT NULL, ip TEXT NOT NULL, expires_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_captcha_ch_expires ON captcha_challenges (expires_at);