#!/usr/bin/env python3 # -*- coding: utf-8 -*- from __future__ import annotations import sqlite3 from db.utils import get_cst_now_str def ensure_schema(conn) -> None: """创建当前版本所需的所有表与索引(新库可直接得到完整 schema)。""" cursor = conn.cursor() # 管理员表 cursor.execute( """ CREATE TABLE IF NOT EXISTS admins ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """ ) # 用户表 cursor.execute( """ CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, email TEXT, email_verified INTEGER DEFAULT 0, email_notify_enabled INTEGER DEFAULT 1, status TEXT DEFAULT 'approved', vip_expire_time TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, approved_at TIMESTAMP ) """ ) # 账号表(关联用户) cursor.execute( """ CREATE TABLE IF NOT EXISTS accounts ( id TEXT PRIMARY KEY, user_id INTEGER NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL, remember INTEGER DEFAULT 1, remark TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) """ ) # VIP配置表 cursor.execute( """ CREATE TABLE IF NOT EXISTS vip_config ( id INTEGER PRIMARY KEY CHECK (id = 1), default_vip_days INTEGER DEFAULT 0, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """ ) # 系统配置表 cursor.execute( """ CREATE TABLE IF NOT EXISTS system_config ( id INTEGER PRIMARY KEY CHECK (id = 1), max_concurrent_global INTEGER DEFAULT 2, schedule_enabled INTEGER DEFAULT 0, schedule_time TEXT DEFAULT '02:00', schedule_browse_type TEXT DEFAULT '应读', proxy_enabled INTEGER DEFAULT 0, proxy_api_url TEXT DEFAULT '', proxy_expire_minutes INTEGER DEFAULT 3, max_screenshot_concurrent INTEGER DEFAULT 3, max_concurrent_per_account INTEGER DEFAULT 1, schedule_weekdays TEXT DEFAULT '1,2,3,4,5,6,7', enable_screenshot INTEGER DEFAULT 1, auto_approve_enabled INTEGER DEFAULT 0, auto_approve_hourly_limit INTEGER DEFAULT 10, auto_approve_vip_days INTEGER DEFAULT 7, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """ ) # 任务日志表 cursor.execute( """ CREATE TABLE IF NOT EXISTS task_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, account_id TEXT NOT NULL, username TEXT NOT NULL, browse_type TEXT NOT NULL, status TEXT NOT NULL, total_items INTEGER DEFAULT 0, total_attachments INTEGER DEFAULT 0, error_message TEXT, duration INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, source TEXT DEFAULT 'manual', FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) """ ) # 密码重置申请表 cursor.execute( """ CREATE TABLE IF NOT EXISTS password_reset_requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, new_password_hash TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, processed_at TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) """ ) # 数据库版本表 cursor.execute( """ CREATE TABLE IF NOT EXISTS db_version ( id INTEGER PRIMARY KEY CHECK (id = 1), version INTEGER NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """ ) # Bug反馈表 cursor.execute( """ CREATE TABLE IF NOT EXISTS bug_feedbacks ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, username TEXT NOT NULL, title TEXT NOT NULL, description TEXT NOT NULL, contact TEXT, status TEXT DEFAULT 'pending', admin_reply TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, replied_at TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) """ ) # 公告表 cursor.execute( """ CREATE TABLE IF NOT EXISTS announcements ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, is_active INTEGER DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """ ) # 公告永久关闭记录表(用户维度) cursor.execute( """ CREATE TABLE IF NOT EXISTS announcement_dismissals ( user_id INTEGER NOT NULL, announcement_id INTEGER NOT NULL, dismissed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, announcement_id), FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, FOREIGN KEY (announcement_id) REFERENCES announcements (id) ON DELETE CASCADE ) """ ) # 用户定时任务表 cursor.execute( """ CREATE TABLE IF NOT EXISTS user_schedules ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, name TEXT DEFAULT '我的定时任务', enabled INTEGER DEFAULT 0, schedule_time TEXT NOT NULL DEFAULT '08:00', weekdays TEXT NOT NULL DEFAULT '1,2,3,4,5', browse_type TEXT NOT NULL DEFAULT '应读', enable_screenshot INTEGER DEFAULT 1, random_delay INTEGER DEFAULT 0, account_ids TEXT, last_run_at TIMESTAMP, next_run_at TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) """ ) # 定时任务执行日志表(历史上在迁移中创建;这里补齐,避免新库缺表) cursor.execute( """ CREATE TABLE IF NOT EXISTS schedule_execution_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, schedule_id INTEGER NOT NULL, user_id INTEGER NOT NULL, schedule_name TEXT, execute_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, total_accounts INTEGER DEFAULT 0, success_accounts INTEGER DEFAULT 0, failed_accounts INTEGER DEFAULT 0, total_items INTEGER DEFAULT 0, total_attachments INTEGER DEFAULT 0, total_screenshots INTEGER DEFAULT 0, duration_seconds INTEGER DEFAULT 0, status TEXT DEFAULT 'running', error_message TEXT, FOREIGN KEY (schedule_id) REFERENCES user_schedules (id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ) """ ) # ========== 创建索引 ========== cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_status ON users(status)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_users_vip_expire ON users(vip_expire_time)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_accounts_user_id ON accounts(user_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_accounts_username ON accounts(username)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_task_logs_user_id ON task_logs(user_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_task_logs_status ON task_logs(status)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_task_logs_created_at ON task_logs(created_at)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_task_logs_user_date ON task_logs(user_id, created_at)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_password_reset_status ON password_reset_requests(status)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_password_reset_user_id ON password_reset_requests(user_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_user_id ON bug_feedbacks(user_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_status ON bug_feedbacks(status)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_created_at ON bug_feedbacks(created_at)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_announcements_active ON announcements(is_active)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_announcements_created_at ON announcements(created_at)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_announcement_dismissals_user ON announcement_dismissals(user_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_schedules_user_id ON user_schedules(user_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_schedules_enabled ON user_schedules(enabled)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_schedules_next_run ON user_schedules(next_run_at)") # 初始化VIP配置(幂等) try: cursor.execute("INSERT INTO vip_config (id, default_vip_days) VALUES (1, 0)") except sqlite3.IntegrityError: pass # 初始化系统配置(幂等) try: cursor.execute( """ INSERT INTO system_config ( id, max_concurrent_global, max_concurrent_per_account, max_screenshot_concurrent, schedule_enabled, schedule_time, schedule_browse_type, schedule_weekdays, proxy_enabled, proxy_api_url, proxy_expire_minutes, enable_screenshot, auto_approve_enabled, auto_approve_hourly_limit, auto_approve_vip_days ) VALUES (1, 2, 1, 3, 0, '02:00', '应读', '1,2,3,4,5,6,7', 0, '', 3, 1, 0, 10, 7) """ ) except sqlite3.IntegrityError: pass # 确保 db_version 记录存在(默认 0,由迁移统一更新) cursor.execute("INSERT OR IGNORE INTO db_version (id, version, updated_at) VALUES (1, 0, ?)", (get_cst_now_str(),)) conn.commit()