更新说明:\n1. 新增用户端与管理员端 Passkey 登录/注册/设备管理(最多3台,支持设备备注、删除设备)。\n2. 修复 Passkey 注册与登录流程中的浏览器/证书/CSRF相关问题,增强错误提示。\n3. 前台登录页改为独立入口,首屏仅加载必要资源,其他页面按需加载。\n4. 系统配置页改为静默获取金山文档状态,避免首屏阻塞,并优化状态展示为“检测中/已登录/未登录/异常”。\n5. 补充后端接口与页面渲染适配,修复多入口下样式依赖注入问题。\n6. 同步更新前后台构建产物与相关静态资源。
936 lines
30 KiB
Python
936 lines
30 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
from __future__ import annotations
|
||
|
||
import sqlite3
|
||
|
||
from db.utils import get_cst_now_str
|
||
|
||
|
||
def get_current_version(conn) -> int:
|
||
cursor = conn.cursor()
|
||
cursor.execute("SELECT version FROM db_version WHERE id = 1")
|
||
row = cursor.fetchone()
|
||
if not row:
|
||
return 0
|
||
try:
|
||
return int(row["version"])
|
||
except Exception:
|
||
try:
|
||
return int(row[0])
|
||
except Exception:
|
||
return 0
|
||
|
||
|
||
def set_current_version(conn, version: int) -> None:
|
||
cursor = conn.cursor()
|
||
cursor.execute("UPDATE db_version SET version = ?, updated_at = ? WHERE id = 1", (int(version), get_cst_now_str()))
|
||
conn.commit()
|
||
|
||
|
||
def _table_exists(cursor, table_name: str) -> bool:
|
||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (str(table_name),))
|
||
return cursor.fetchone() is not None
|
||
|
||
|
||
def _get_table_columns(cursor, table_name: str) -> set[str]:
|
||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||
return {col[1] for col in cursor.fetchall()}
|
||
|
||
|
||
def _add_column_if_missing(cursor, table_name: str, columns: set[str], column_name: str, column_ddl: str, *, ok_message: str) -> bool:
|
||
if column_name in columns:
|
||
return False
|
||
cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_ddl}")
|
||
columns.add(column_name)
|
||
print(ok_message)
|
||
return True
|
||
|
||
|
||
def _read_row_value(row, key: str, index: int):
|
||
if isinstance(row, sqlite3.Row):
|
||
return row[key]
|
||
return row[index]
|
||
|
||
|
||
def _get_migration_steps():
|
||
return [
|
||
(1, _migrate_to_v1),
|
||
(2, _migrate_to_v2),
|
||
(3, _migrate_to_v3),
|
||
(4, _migrate_to_v4),
|
||
(5, _migrate_to_v5),
|
||
(6, _migrate_to_v6),
|
||
(7, _migrate_to_v7),
|
||
(8, _migrate_to_v8),
|
||
(9, _migrate_to_v9),
|
||
(10, _migrate_to_v10),
|
||
(11, _migrate_to_v11),
|
||
(12, _migrate_to_v12),
|
||
(13, _migrate_to_v13),
|
||
(14, _migrate_to_v14),
|
||
(15, _migrate_to_v15),
|
||
(16, _migrate_to_v16),
|
||
(17, _migrate_to_v17),
|
||
(18, _migrate_to_v18),
|
||
(19, _migrate_to_v19),
|
||
(20, _migrate_to_v20),
|
||
(21, _migrate_to_v21),
|
||
]
|
||
|
||
|
||
def migrate_database(conn, target_version: int) -> None:
|
||
"""数据库迁移:按版本增量升级(向前兼容)。"""
|
||
cursor = conn.cursor()
|
||
cursor.execute("INSERT OR IGNORE INTO db_version (id, version, updated_at) VALUES (1, 0, ?)", (get_cst_now_str(),))
|
||
conn.commit()
|
||
|
||
target_version = int(target_version)
|
||
current_version = get_current_version(conn)
|
||
|
||
for version, migrate_fn in _get_migration_steps():
|
||
if version > target_version or current_version >= version:
|
||
continue
|
||
migrate_fn(conn)
|
||
current_version = version
|
||
|
||
stored_version = get_current_version(conn)
|
||
if stored_version != current_version:
|
||
set_current_version(conn, current_version)
|
||
|
||
if current_version != target_version:
|
||
print(f" [WARN] 目标版本 {target_version} 未完全可达,当前停留在 {current_version}")
|
||
|
||
|
||
def _migrate_to_v1(conn):
|
||
"""迁移到版本1 - 添加缺失字段"""
|
||
cursor = conn.cursor()
|
||
|
||
system_columns = _get_table_columns(cursor, "system_config")
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"system_config",
|
||
system_columns,
|
||
"schedule_weekdays",
|
||
'TEXT DEFAULT "1,2,3,4,5,6,7"',
|
||
ok_message=" [OK] 添加 schedule_weekdays 字段",
|
||
)
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"system_config",
|
||
system_columns,
|
||
"max_screenshot_concurrent",
|
||
"INTEGER DEFAULT 3",
|
||
ok_message=" [OK] 添加 max_screenshot_concurrent 字段",
|
||
)
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"system_config",
|
||
system_columns,
|
||
"max_concurrent_per_account",
|
||
"INTEGER DEFAULT 1",
|
||
ok_message=" [OK] 添加 max_concurrent_per_account 字段",
|
||
)
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"system_config",
|
||
system_columns,
|
||
"auto_approve_enabled",
|
||
"INTEGER DEFAULT 0",
|
||
ok_message=" [OK] 添加 auto_approve_enabled 字段",
|
||
)
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"system_config",
|
||
system_columns,
|
||
"auto_approve_hourly_limit",
|
||
"INTEGER DEFAULT 10",
|
||
ok_message=" [OK] 添加 auto_approve_hourly_limit 字段",
|
||
)
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"system_config",
|
||
system_columns,
|
||
"auto_approve_vip_days",
|
||
"INTEGER DEFAULT 7",
|
||
ok_message=" [OK] 添加 auto_approve_vip_days 字段",
|
||
)
|
||
|
||
task_log_columns = _get_table_columns(cursor, "task_logs")
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"task_logs",
|
||
task_log_columns,
|
||
"duration",
|
||
"INTEGER",
|
||
ok_message=" [OK] 添加 duration 字段到 task_logs",
|
||
)
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v2(conn):
|
||
"""迁移到版本2 - 添加代理配置字段"""
|
||
cursor = conn.cursor()
|
||
|
||
columns = _get_table_columns(cursor, "system_config")
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"system_config",
|
||
columns,
|
||
"proxy_enabled",
|
||
"INTEGER DEFAULT 0",
|
||
ok_message=" [OK] 添加 proxy_enabled 字段",
|
||
)
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"system_config",
|
||
columns,
|
||
"proxy_api_url",
|
||
'TEXT DEFAULT ""',
|
||
ok_message=" [OK] 添加 proxy_api_url 字段",
|
||
)
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"system_config",
|
||
columns,
|
||
"proxy_expire_minutes",
|
||
"INTEGER DEFAULT 3",
|
||
ok_message=" [OK] 添加 proxy_expire_minutes 字段",
|
||
)
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"system_config",
|
||
columns,
|
||
"enable_screenshot",
|
||
"INTEGER DEFAULT 1",
|
||
ok_message=" [OK] 添加 enable_screenshot 字段",
|
||
)
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v3(conn):
|
||
"""迁移到版本3 - 添加账号状态和登录失败计数字段"""
|
||
cursor = conn.cursor()
|
||
|
||
columns = _get_table_columns(cursor, "accounts")
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"accounts",
|
||
columns,
|
||
"status",
|
||
'TEXT DEFAULT "active"',
|
||
ok_message=" [OK] 添加 accounts.status 字段 (账号状态)",
|
||
)
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"accounts",
|
||
columns,
|
||
"login_fail_count",
|
||
"INTEGER DEFAULT 0",
|
||
ok_message=" [OK] 添加 accounts.login_fail_count 字段 (登录失败计数)",
|
||
)
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"accounts",
|
||
columns,
|
||
"last_login_error",
|
||
"TEXT",
|
||
ok_message=" [OK] 添加 accounts.last_login_error 字段 (最后登录错误)",
|
||
)
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v4(conn):
|
||
"""迁移到版本4 - 添加任务来源字段"""
|
||
cursor = conn.cursor()
|
||
|
||
columns = _get_table_columns(cursor, "task_logs")
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"task_logs",
|
||
columns,
|
||
"source",
|
||
'TEXT DEFAULT "manual"',
|
||
ok_message=" [OK] 添加 task_logs.source 字段 (任务来源: manual/scheduled/immediate)",
|
||
)
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v5(conn):
|
||
"""迁移到版本5 - 添加用户定时任务表"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='user_schedules'")
|
||
if not cursor.fetchone():
|
||
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,
|
||
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
|
||
)
|
||
"""
|
||
)
|
||
print(" [OK] 创建 user_schedules 表 (用户定时任务)")
|
||
|
||
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
|
||
)
|
||
"""
|
||
)
|
||
print(" [OK] 创建 schedule_execution_logs 表 (定时任务执行日志)")
|
||
|
||
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)")
|
||
print(" [OK] 创建 user_schedules 表索引")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v6(conn):
|
||
"""迁移到版本6 - 添加公告功能相关表"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='announcements'")
|
||
if not cursor.fetchone():
|
||
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
|
||
)
|
||
"""
|
||
)
|
||
print(" [OK] 创建 announcements 表 (公告)")
|
||
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)")
|
||
print(" [OK] 创建 announcements 表索引")
|
||
|
||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='announcement_dismissals'")
|
||
if not cursor.fetchone():
|
||
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
|
||
)
|
||
"""
|
||
)
|
||
print(" [OK] 创建 announcement_dismissals 表 (公告永久关闭记录)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_announcement_dismissals_user ON announcement_dismissals(user_id)")
|
||
print(" [OK] 创建 announcement_dismissals 表索引")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v7(conn):
|
||
"""迁移到版本7 - 统一存储北京时间(将历史UTC时间字段整体+8小时)"""
|
||
cursor = conn.cursor()
|
||
columns_cache: dict[str, set[str]] = {}
|
||
|
||
def shift_utc_to_cst(table_name: str, column_name: str) -> None:
|
||
if not _table_exists(cursor, table_name):
|
||
return
|
||
|
||
if table_name not in columns_cache:
|
||
columns_cache[table_name] = _get_table_columns(cursor, table_name)
|
||
if column_name not in columns_cache[table_name]:
|
||
return
|
||
|
||
cursor.execute(
|
||
f"""
|
||
UPDATE {table_name}
|
||
SET {column_name} = datetime({column_name}, '+8 hours')
|
||
WHERE {column_name} IS NOT NULL AND {column_name} != ''
|
||
"""
|
||
)
|
||
|
||
for table, col in [
|
||
("users", "created_at"),
|
||
("users", "approved_at"),
|
||
("admins", "created_at"),
|
||
("accounts", "created_at"),
|
||
("password_reset_requests", "created_at"),
|
||
("password_reset_requests", "processed_at"),
|
||
("smtp_configs", "created_at"),
|
||
("smtp_configs", "updated_at"),
|
||
("smtp_configs", "last_success_at"),
|
||
("email_settings", "updated_at"),
|
||
("email_tokens", "created_at"),
|
||
("email_logs", "created_at"),
|
||
("email_stats", "last_updated"),
|
||
("task_checkpoints", "created_at"),
|
||
("task_checkpoints", "updated_at"),
|
||
("task_checkpoints", "completed_at"),
|
||
]:
|
||
shift_utc_to_cst(table, col)
|
||
|
||
conn.commit()
|
||
print(" [OK] 时区迁移:历史UTC时间已转换为北京时间")
|
||
|
||
|
||
def _migrate_to_v8(conn):
|
||
"""迁移到版本8 - 用户定时 next_run_at 随机延迟落库(O-08)"""
|
||
cursor = conn.cursor()
|
||
|
||
# 1) 增量字段:random_delay(旧库可能不存在)
|
||
columns = _get_table_columns(cursor, "user_schedules")
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"user_schedules",
|
||
columns,
|
||
"random_delay",
|
||
"INTEGER DEFAULT 0",
|
||
ok_message=" [OK] 添加 user_schedules.random_delay 字段",
|
||
)
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"user_schedules",
|
||
columns,
|
||
"next_run_at",
|
||
"TIMESTAMP",
|
||
ok_message=" [OK] 添加 user_schedules.next_run_at 字段",
|
||
)
|
||
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_schedules_next_run ON user_schedules(next_run_at)")
|
||
conn.commit()
|
||
|
||
# 2) 为历史 enabled schedule 补算 next_run_at(保证索引驱动可用)
|
||
try:
|
||
from services.schedule_utils import compute_next_run_at, format_cst
|
||
from services.time_utils import get_beijing_now
|
||
|
||
now_dt = get_beijing_now()
|
||
now_str = format_cst(now_dt)
|
||
|
||
cursor.execute(
|
||
"""
|
||
SELECT id, schedule_time, weekdays, random_delay, last_run_at, next_run_at
|
||
FROM user_schedules
|
||
WHERE enabled = 1
|
||
"""
|
||
)
|
||
rows = cursor.fetchall() or []
|
||
|
||
fixed = 0
|
||
for row in rows:
|
||
try:
|
||
schedule_id = _read_row_value(row, "id", 0)
|
||
schedule_time = _read_row_value(row, "schedule_time", 1)
|
||
weekdays = _read_row_value(row, "weekdays", 2)
|
||
random_delay = _read_row_value(row, "random_delay", 3)
|
||
last_run_at = _read_row_value(row, "last_run_at", 4)
|
||
next_run_at = _read_row_value(row, "next_run_at", 5)
|
||
except Exception:
|
||
continue
|
||
|
||
next_run_text = str(next_run_at or "").strip()
|
||
# 若 next_run_at 为空/非法/已过期,则重算
|
||
if (not next_run_text) or (next_run_text <= now_str):
|
||
next_dt = compute_next_run_at(
|
||
now=now_dt,
|
||
schedule_time=str(schedule_time or "08:00"),
|
||
weekdays=str(weekdays or "1,2,3,4,5"),
|
||
random_delay=int(random_delay or 0),
|
||
last_run_at=str(last_run_at or "") if last_run_at else None,
|
||
)
|
||
next_run_text = format_cst(next_dt)
|
||
cursor.execute(
|
||
"UPDATE user_schedules SET next_run_at = ?, updated_at = ? WHERE id = ?",
|
||
(next_run_text, get_cst_now_str(), int(schedule_id)),
|
||
)
|
||
fixed += 1
|
||
|
||
conn.commit()
|
||
if fixed:
|
||
print(f" [OK] 已为 {fixed} 条启用定时任务补算 next_run_at")
|
||
except Exception as e:
|
||
# 迁移过程中不阻断主流程;上线后由 worker 兜底补算
|
||
print(f" ⚠ v8 迁移补算 next_run_at 失败: {e}")
|
||
|
||
|
||
def _migrate_to_v9(conn):
|
||
"""迁移到版本9 - 邮件设置字段迁移(清理 email_service scattered ALTER TABLE)"""
|
||
cursor = conn.cursor()
|
||
|
||
if not _table_exists(cursor, "email_settings"):
|
||
# 邮件表由 email_service.init_email_tables 创建;此处仅做增量字段迁移
|
||
return
|
||
|
||
columns = _get_table_columns(cursor, "email_settings")
|
||
|
||
changed = False
|
||
changed = (
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"email_settings",
|
||
columns,
|
||
"register_verify_enabled",
|
||
"INTEGER DEFAULT 0",
|
||
ok_message=" [OK] 添加 email_settings.register_verify_enabled 字段",
|
||
)
|
||
or changed
|
||
)
|
||
changed = (
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"email_settings",
|
||
columns,
|
||
"base_url",
|
||
"TEXT DEFAULT ''",
|
||
ok_message=" [OK] 添加 email_settings.base_url 字段",
|
||
)
|
||
or changed
|
||
)
|
||
changed = (
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"email_settings",
|
||
columns,
|
||
"task_notify_enabled",
|
||
"INTEGER DEFAULT 0",
|
||
ok_message=" [OK] 添加 email_settings.task_notify_enabled 字段",
|
||
)
|
||
or changed
|
||
)
|
||
|
||
if changed:
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v10(conn):
|
||
"""迁移到版本10 - users 邮箱字段迁移(避免运行时 ALTER TABLE)"""
|
||
cursor = conn.cursor()
|
||
columns = _get_table_columns(cursor, "users")
|
||
|
||
changed = False
|
||
changed = (
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"users",
|
||
columns,
|
||
"email_verified",
|
||
"INTEGER DEFAULT 0",
|
||
ok_message=" [OK] 添加 users.email_verified 字段",
|
||
)
|
||
or changed
|
||
)
|
||
changed = (
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"users",
|
||
columns,
|
||
"email_notify_enabled",
|
||
"INTEGER DEFAULT 1",
|
||
ok_message=" [OK] 添加 users.email_notify_enabled 字段",
|
||
)
|
||
or changed
|
||
)
|
||
|
||
if changed:
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v11(conn):
|
||
"""迁移到版本11 - 取消注册待审核:历史 pending 用户直接置为 approved"""
|
||
cursor = conn.cursor()
|
||
now_str = get_cst_now_str()
|
||
|
||
try:
|
||
cursor.execute(
|
||
"""
|
||
UPDATE users
|
||
SET status = 'approved',
|
||
approved_at = COALESCE(NULLIF(approved_at, ''), ?)
|
||
WHERE status = 'pending'
|
||
""",
|
||
(now_str,),
|
||
)
|
||
updated = cursor.rowcount
|
||
conn.commit()
|
||
|
||
if updated:
|
||
print(f" [OK] 已将 {updated} 个 pending 用户迁移为 approved")
|
||
except sqlite3.OperationalError as e:
|
||
print(f" ⚠️ v11 迁移跳过: {e}")
|
||
|
||
|
||
def _migrate_to_v12(conn):
|
||
"""迁移到版本12 - 登录设备/IP记录表"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS login_fingerprints (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
user_agent TEXT NOT NULL,
|
||
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
last_ip TEXT DEFAULT '',
|
||
UNIQUE (user_id, user_agent),
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
"""
|
||
)
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS login_ips (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER NOT NULL,
|
||
ip TEXT NOT NULL,
|
||
first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE (user_id, ip),
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
"""
|
||
)
|
||
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_login_fingerprints_user ON login_fingerprints(user_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_login_ips_user ON login_ips(user_id)")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v13(conn):
|
||
"""迁移到版本13 - 安全防护:威胁检测相关表"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS threat_events (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
threat_type TEXT NOT NULL,
|
||
score INTEGER NOT NULL DEFAULT 0,
|
||
rule TEXT,
|
||
field_name TEXT,
|
||
matched TEXT,
|
||
value_preview TEXT,
|
||
ip TEXT,
|
||
user_id INTEGER,
|
||
request_method TEXT,
|
||
request_path TEXT,
|
||
user_agent TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||
)
|
||
"""
|
||
)
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_created_at ON threat_events(created_at)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_ip ON threat_events(ip)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_user_id ON threat_events(user_id)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_events_type ON threat_events(threat_type)")
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS ip_risk_scores (
|
||
ip TEXT PRIMARY KEY,
|
||
risk_score INTEGER NOT NULL DEFAULT 0,
|
||
last_seen TIMESTAMP,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""
|
||
)
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_ip_risk_scores_score ON ip_risk_scores(risk_score)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_ip_risk_scores_updated_at ON ip_risk_scores(updated_at)")
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS user_risk_scores (
|
||
user_id INTEGER PRIMARY KEY,
|
||
risk_score INTEGER NOT NULL DEFAULT 0,
|
||
last_seen 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 INDEX IF NOT EXISTS idx_user_risk_scores_score ON user_risk_scores(risk_score)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_risk_scores_updated_at ON user_risk_scores(updated_at)")
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS ip_blacklist (
|
||
ip TEXT PRIMARY KEY,
|
||
reason TEXT,
|
||
is_active INTEGER DEFAULT 1,
|
||
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
expires_at TIMESTAMP
|
||
)
|
||
"""
|
||
)
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_ip_blacklist_active ON ip_blacklist(is_active)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_ip_blacklist_expires ON ip_blacklist(expires_at)")
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS threat_signatures (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name TEXT UNIQUE NOT NULL,
|
||
threat_type TEXT NOT NULL,
|
||
pattern TEXT NOT NULL,
|
||
pattern_type TEXT DEFAULT 'regex',
|
||
score INTEGER DEFAULT 0,
|
||
is_active INTEGER DEFAULT 1,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""
|
||
)
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_signatures_type ON threat_signatures(threat_type)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_threat_signatures_active ON threat_signatures(is_active)")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v14(conn):
|
||
"""迁移到版本14 - 安全防护:用户黑名单表"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS user_blacklist (
|
||
user_id INTEGER PRIMARY KEY,
|
||
reason TEXT,
|
||
is_active INTEGER DEFAULT 1,
|
||
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
expires_at TIMESTAMP
|
||
)
|
||
"""
|
||
)
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_blacklist_active ON user_blacklist(is_active)")
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_user_blacklist_expires ON user_blacklist(expires_at)")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v15(conn):
|
||
"""迁移到版本15 - 邮件设置:新设备登录提醒全局开关"""
|
||
cursor = conn.cursor()
|
||
|
||
if not _table_exists(cursor, "email_settings"):
|
||
# 邮件表由 email_service.init_email_tables 创建;此处仅做增量字段迁移
|
||
return
|
||
|
||
columns = _get_table_columns(cursor, "email_settings")
|
||
|
||
changed = False
|
||
changed = (
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"email_settings",
|
||
columns,
|
||
"login_alert_enabled",
|
||
"INTEGER DEFAULT 1",
|
||
ok_message=" [OK] 添加 email_settings.login_alert_enabled 字段",
|
||
)
|
||
or changed
|
||
)
|
||
|
||
try:
|
||
cursor.execute("UPDATE email_settings SET login_alert_enabled = 1 WHERE login_alert_enabled IS NULL")
|
||
if cursor.rowcount:
|
||
changed = True
|
||
except sqlite3.OperationalError:
|
||
# 列不存在等情况由上方迁移兜底;不阻断主流程
|
||
pass
|
||
|
||
if changed:
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v16(conn):
|
||
"""迁移到版本16 - 公告支持图片字段"""
|
||
cursor = conn.cursor()
|
||
columns = _get_table_columns(cursor, "announcements")
|
||
|
||
if _add_column_if_missing(
|
||
cursor,
|
||
"announcements",
|
||
columns,
|
||
"image_url",
|
||
"TEXT",
|
||
ok_message=" [OK] 添加 announcements.image_url 字段",
|
||
):
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v17(conn):
|
||
"""迁移到版本17 - 金山文档上传配置与用户开关"""
|
||
cursor = conn.cursor()
|
||
|
||
system_columns = _get_table_columns(cursor, "system_config")
|
||
system_fields = [
|
||
("kdocs_enabled", "INTEGER DEFAULT 0"),
|
||
("kdocs_doc_url", "TEXT DEFAULT ''"),
|
||
("kdocs_default_unit", "TEXT DEFAULT ''"),
|
||
("kdocs_sheet_name", "TEXT DEFAULT ''"),
|
||
("kdocs_sheet_index", "INTEGER DEFAULT 0"),
|
||
("kdocs_unit_column", "TEXT DEFAULT 'A'"),
|
||
("kdocs_image_column", "TEXT DEFAULT 'D'"),
|
||
("kdocs_admin_notify_enabled", "INTEGER DEFAULT 0"),
|
||
("kdocs_admin_notify_email", "TEXT DEFAULT ''"),
|
||
]
|
||
for field, ddl in system_fields:
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"system_config",
|
||
system_columns,
|
||
field,
|
||
ddl,
|
||
ok_message=f" [OK] 添加 system_config.{field} 字段",
|
||
)
|
||
|
||
user_columns = _get_table_columns(cursor, "users")
|
||
user_fields = [
|
||
("kdocs_unit", "TEXT DEFAULT ''"),
|
||
("kdocs_auto_upload", "INTEGER DEFAULT 0"),
|
||
]
|
||
for field, ddl in user_fields:
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"users",
|
||
user_columns,
|
||
field,
|
||
ddl,
|
||
ok_message=f" [OK] 添加 users.{field} 字段",
|
||
)
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v18(conn):
|
||
"""迁移到版本18 - 金山文档上传:有效行范围配置"""
|
||
cursor = conn.cursor()
|
||
|
||
columns = _get_table_columns(cursor, "system_config")
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"system_config",
|
||
columns,
|
||
"kdocs_row_start",
|
||
"INTEGER DEFAULT 0",
|
||
ok_message=" [OK] 添加 system_config.kdocs_row_start 字段",
|
||
)
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"system_config",
|
||
columns,
|
||
"kdocs_row_end",
|
||
"INTEGER DEFAULT 0",
|
||
ok_message=" [OK] 添加 system_config.kdocs_row_end 字段",
|
||
)
|
||
|
||
conn.commit()
|
||
|
||
|
||
|
||
def _migrate_to_v19(conn):
|
||
"""迁移到版本19 - 报表与调度查询复合索引优化"""
|
||
cursor = conn.cursor()
|
||
|
||
index_statements = [
|
||
"CREATE INDEX IF NOT EXISTS idx_users_status_created_at ON users(status, created_at)",
|
||
"CREATE INDEX IF NOT EXISTS idx_task_logs_status_created_at ON task_logs(status, created_at)",
|
||
"CREATE INDEX IF NOT EXISTS idx_user_schedules_enabled_next_run ON user_schedules(enabled, next_run_at)",
|
||
"CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_status_created_at ON bug_feedbacks(status, created_at)",
|
||
"CREATE INDEX IF NOT EXISTS idx_bug_feedbacks_user_created_at ON bug_feedbacks(user_id, created_at)",
|
||
]
|
||
|
||
for statement in index_statements:
|
||
cursor.execute(statement)
|
||
|
||
conn.commit()
|
||
|
||
|
||
|
||
def _migrate_to_v20(conn):
|
||
"""迁移到版本20 - 慢SQL阈值系统配置"""
|
||
cursor = conn.cursor()
|
||
|
||
columns = _get_table_columns(cursor, "system_config")
|
||
_add_column_if_missing(
|
||
cursor,
|
||
"system_config",
|
||
columns,
|
||
"db_slow_query_ms",
|
||
"INTEGER DEFAULT 120",
|
||
ok_message=" [OK] 添加 system_config.db_slow_query_ms 字段",
|
||
)
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v21(conn):
|
||
"""迁移到版本21 - Passkey 认证设备表"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute(
|
||
"""
|
||
CREATE TABLE IF NOT EXISTS passkeys (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
owner_type TEXT NOT NULL,
|
||
owner_id INTEGER NOT NULL,
|
||
device_name TEXT NOT NULL,
|
||
credential_id TEXT UNIQUE NOT NULL,
|
||
public_key TEXT NOT NULL,
|
||
sign_count INTEGER DEFAULT 0,
|
||
transports TEXT DEFAULT '',
|
||
aaguid TEXT DEFAULT '',
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
last_used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""
|
||
)
|
||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_passkeys_owner ON passkeys(owner_type, owner_id)")
|
||
cursor.execute(
|
||
"CREATE INDEX IF NOT EXISTS idx_passkeys_owner_last_used ON passkeys(owner_type, owner_id, last_used_at)"
|
||
)
|
||
|
||
conn.commit()
|