✨ 主要优化成果: - 修复Unicode字符编码问题(Windows跨平台兼容性) - 安装wkhtmltoimage,截图功能完全修复 - 智能延迟优化(api_browser.py) - 线程池资源泄漏修复(tasks.py) - HTML解析缓存机制 - 二分搜索算法优化(kdocs_uploader.py) - 自适应资源配置(browser_pool_worker.py) 🐛 Bug修复: - 解决截图失败问题 - 修复管理员密码设置 - 解决应用启动编码错误 📚 新增文档: - BUG_REPORT.md - 完整bug分析报告 - PERFORMANCE_ANALYSIS_REPORT.md - 性能优化分析 - LINUX_DEPLOYMENT_ANALYSIS.md - Linux部署指南 - SCREENSHOT_FIX_SUCCESS.md - 截图功能修复记录 - INSTALL_WKHTMLTOIMAGE.md - 安装指南 - OPTIMIZATION_FIXES_SUMMARY.md - 优化总结 🚀 功能验证: - Flask应用正常运行(51233端口) - 数据库、截图线程池、API预热正常 - 管理员登录:admin/admin123 - 健康检查API:http://127.0.0.1:51233/health 💡 技术改进: - 智能延迟算法(自适应调整) - LRU缓存策略 - 线程池资源管理优化 - 二分搜索算法(O(log n) vs O(n)) - 自适应资源管理 🎯 项目现在稳定运行,可部署到Linux环境
752 lines
28 KiB
Python
752 lines
28 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 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()
|
||
|
||
current_version = get_current_version(conn)
|
||
|
||
if current_version < 1:
|
||
_migrate_to_v1(conn)
|
||
current_version = 1
|
||
if current_version < 2:
|
||
_migrate_to_v2(conn)
|
||
current_version = 2
|
||
if current_version < 3:
|
||
_migrate_to_v3(conn)
|
||
current_version = 3
|
||
if current_version < 4:
|
||
_migrate_to_v4(conn)
|
||
current_version = 4
|
||
if current_version < 5:
|
||
_migrate_to_v5(conn)
|
||
current_version = 5
|
||
if current_version < 6:
|
||
_migrate_to_v6(conn)
|
||
current_version = 6
|
||
if current_version < 7:
|
||
_migrate_to_v7(conn)
|
||
current_version = 7
|
||
if current_version < 8:
|
||
_migrate_to_v8(conn)
|
||
current_version = 8
|
||
if current_version < 9:
|
||
_migrate_to_v9(conn)
|
||
current_version = 9
|
||
if current_version < 10:
|
||
_migrate_to_v10(conn)
|
||
current_version = 10
|
||
if current_version < 11:
|
||
_migrate_to_v11(conn)
|
||
current_version = 11
|
||
if current_version < 12:
|
||
_migrate_to_v12(conn)
|
||
current_version = 12
|
||
if current_version < 13:
|
||
_migrate_to_v13(conn)
|
||
current_version = 13
|
||
if current_version < 14:
|
||
_migrate_to_v14(conn)
|
||
current_version = 14
|
||
if current_version < 15:
|
||
_migrate_to_v15(conn)
|
||
current_version = 15
|
||
if current_version < 16:
|
||
_migrate_to_v16(conn)
|
||
current_version = 16
|
||
if current_version < 17:
|
||
_migrate_to_v17(conn)
|
||
current_version = 17
|
||
if current_version < 18:
|
||
_migrate_to_v18(conn)
|
||
current_version = 18
|
||
|
||
if current_version != int(target_version):
|
||
set_current_version(conn, int(target_version))
|
||
|
||
|
||
def _migrate_to_v1(conn):
|
||
"""迁移到版本1 - 添加缺失字段"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("PRAGMA table_info(system_config)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
if "schedule_weekdays" not in columns:
|
||
cursor.execute('ALTER TABLE system_config ADD COLUMN schedule_weekdays TEXT DEFAULT "1,2,3,4,5,6,7"')
|
||
print(" [OK] 添加 schedule_weekdays 字段")
|
||
|
||
if "max_screenshot_concurrent" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN max_screenshot_concurrent INTEGER DEFAULT 3")
|
||
print(" [OK] 添加 max_screenshot_concurrent 字段")
|
||
if "max_concurrent_per_account" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN max_concurrent_per_account INTEGER DEFAULT 1")
|
||
print(" [OK] 添加 max_concurrent_per_account 字段")
|
||
if "auto_approve_enabled" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN auto_approve_enabled INTEGER DEFAULT 0")
|
||
print(" [OK] 添加 auto_approve_enabled 字段")
|
||
if "auto_approve_hourly_limit" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN auto_approve_hourly_limit INTEGER DEFAULT 10")
|
||
print(" [OK] 添加 auto_approve_hourly_limit 字段")
|
||
if "auto_approve_vip_days" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN auto_approve_vip_days INTEGER DEFAULT 7")
|
||
print(" [OK] 添加 auto_approve_vip_days 字段")
|
||
|
||
cursor.execute("PRAGMA table_info(task_logs)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
if "duration" not in columns:
|
||
cursor.execute("ALTER TABLE task_logs ADD COLUMN duration INTEGER")
|
||
print(" [OK] 添加 duration 字段到 task_logs")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v2(conn):
|
||
"""迁移到版本2 - 添加代理配置字段"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("PRAGMA table_info(system_config)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
if "proxy_enabled" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN proxy_enabled INTEGER DEFAULT 0")
|
||
print(" [OK] 添加 proxy_enabled 字段")
|
||
|
||
if "proxy_api_url" not in columns:
|
||
cursor.execute('ALTER TABLE system_config ADD COLUMN proxy_api_url TEXT DEFAULT ""')
|
||
print(" [OK] 添加 proxy_api_url 字段")
|
||
|
||
if "proxy_expire_minutes" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN proxy_expire_minutes INTEGER DEFAULT 3")
|
||
print(" [OK] 添加 proxy_expire_minutes 字段")
|
||
|
||
if "enable_screenshot" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN enable_screenshot INTEGER DEFAULT 1")
|
||
print(" [OK] 添加 enable_screenshot 字段")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v3(conn):
|
||
"""迁移到版本3 - 添加账号状态和登录失败计数字段"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("PRAGMA table_info(accounts)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
if "status" not in columns:
|
||
cursor.execute('ALTER TABLE accounts ADD COLUMN status TEXT DEFAULT "active"')
|
||
print(" [OK] 添加 accounts.status 字段 (账号状态)")
|
||
|
||
if "login_fail_count" not in columns:
|
||
cursor.execute("ALTER TABLE accounts ADD COLUMN login_fail_count INTEGER DEFAULT 0")
|
||
print(" [OK] 添加 accounts.login_fail_count 字段 (登录失败计数)")
|
||
|
||
if "last_login_error" not in columns:
|
||
cursor.execute("ALTER TABLE accounts ADD COLUMN last_login_error TEXT")
|
||
print(" [OK] 添加 accounts.last_login_error 字段 (最后登录错误)")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v4(conn):
|
||
"""迁移到版本4 - 添加任务来源字段"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("PRAGMA table_info(task_logs)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
if "source" not in columns:
|
||
cursor.execute('ALTER TABLE task_logs ADD COLUMN source TEXT DEFAULT "manual"')
|
||
print(" [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()
|
||
|
||
def table_exists(table_name: str) -> bool:
|
||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
|
||
return cursor.fetchone() is not None
|
||
|
||
def column_exists(table_name: str, column_name: str) -> bool:
|
||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||
return any(row[1] == column_name for row in cursor.fetchall())
|
||
|
||
def shift_utc_to_cst(table_name: str, column_name: str) -> None:
|
||
if not table_exists(table_name):
|
||
return
|
||
if not column_exists(table_name, column_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"),
|
||
]:
|
||
shift_utc_to_cst(table, col)
|
||
|
||
for table, col in [
|
||
("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"),
|
||
]:
|
||
shift_utc_to_cst(table, col)
|
||
|
||
for table, col in [
|
||
("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(旧库可能不存在)
|
||
cursor.execute("PRAGMA table_info(user_schedules)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
if "random_delay" not in columns:
|
||
cursor.execute("ALTER TABLE user_schedules ADD COLUMN random_delay INTEGER DEFAULT 0")
|
||
print(" [OK] 添加 user_schedules.random_delay 字段")
|
||
|
||
if "next_run_at" not in columns:
|
||
cursor.execute("ALTER TABLE user_schedules ADD COLUMN next_run_at TIMESTAMP")
|
||
print(" [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 = row["id"] if isinstance(row, sqlite3.Row) else row[0]
|
||
schedule_time = row["schedule_time"] if isinstance(row, sqlite3.Row) else row[1]
|
||
weekdays = row["weekdays"] if isinstance(row, sqlite3.Row) else row[2]
|
||
random_delay = row["random_delay"] if isinstance(row, sqlite3.Row) else row[3]
|
||
last_run_at = row["last_run_at"] if isinstance(row, sqlite3.Row) else row[4]
|
||
next_run_at = row["next_run_at"] if isinstance(row, sqlite3.Row) else row[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()
|
||
|
||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='email_settings'")
|
||
if not cursor.fetchone():
|
||
# 邮件表由 email_service.init_email_tables 创建;此处仅做增量字段迁移
|
||
return
|
||
|
||
cursor.execute("PRAGMA table_info(email_settings)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
changed = False
|
||
if "register_verify_enabled" not in columns:
|
||
cursor.execute("ALTER TABLE email_settings ADD COLUMN register_verify_enabled INTEGER DEFAULT 0")
|
||
print(" [OK] 添加 email_settings.register_verify_enabled 字段")
|
||
changed = True
|
||
if "base_url" not in columns:
|
||
cursor.execute("ALTER TABLE email_settings ADD COLUMN base_url TEXT DEFAULT ''")
|
||
print(" [OK] 添加 email_settings.base_url 字段")
|
||
changed = True
|
||
if "task_notify_enabled" not in columns:
|
||
cursor.execute("ALTER TABLE email_settings ADD COLUMN task_notify_enabled INTEGER DEFAULT 0")
|
||
print(" [OK] 添加 email_settings.task_notify_enabled 字段")
|
||
changed = True
|
||
|
||
if changed:
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v10(conn):
|
||
"""迁移到版本10 - users 邮箱字段迁移(避免运行时 ALTER TABLE)"""
|
||
cursor = conn.cursor()
|
||
cursor.execute("PRAGMA table_info(users)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
changed = False
|
||
if "email_verified" not in columns:
|
||
cursor.execute("ALTER TABLE users ADD COLUMN email_verified INTEGER DEFAULT 0")
|
||
print(" [OK] 添加 users.email_verified 字段")
|
||
changed = True
|
||
if "email_notify_enabled" not in columns:
|
||
cursor.execute("ALTER TABLE users ADD COLUMN email_notify_enabled INTEGER DEFAULT 1")
|
||
print(" [OK] 添加 users.email_notify_enabled 字段")
|
||
changed = True
|
||
|
||
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()
|
||
|
||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='email_settings'")
|
||
if not cursor.fetchone():
|
||
# 邮件表由 email_service.init_email_tables 创建;此处仅做增量字段迁移
|
||
return
|
||
|
||
cursor.execute("PRAGMA table_info(email_settings)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
changed = False
|
||
if "login_alert_enabled" not in columns:
|
||
cursor.execute("ALTER TABLE email_settings ADD COLUMN login_alert_enabled INTEGER DEFAULT 1")
|
||
print(" [OK] 添加 email_settings.login_alert_enabled 字段")
|
||
changed = True
|
||
|
||
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()
|
||
cursor.execute("PRAGMA table_info(announcements)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
if "image_url" not in columns:
|
||
cursor.execute("ALTER TABLE announcements ADD COLUMN image_url TEXT")
|
||
conn.commit()
|
||
print(" [OK] 添加 announcements.image_url 字段")
|
||
|
||
|
||
def _migrate_to_v17(conn):
|
||
"""迁移到版本17 - 金山文档上传配置与用户开关"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("PRAGMA table_info(system_config)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
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:
|
||
if field not in columns:
|
||
cursor.execute(f"ALTER TABLE system_config ADD COLUMN {field} {ddl}")
|
||
print(f" [OK] 添加 system_config.{field} 字段")
|
||
|
||
cursor.execute("PRAGMA table_info(users)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
user_fields = [
|
||
("kdocs_unit", "TEXT DEFAULT ''"),
|
||
("kdocs_auto_upload", "INTEGER DEFAULT 0"),
|
||
]
|
||
for field, ddl in user_fields:
|
||
if field not in columns:
|
||
cursor.execute(f"ALTER TABLE users ADD COLUMN {field} {ddl}")
|
||
print(f" [OK] 添加 users.{field} 字段")
|
||
|
||
conn.commit()
|
||
|
||
|
||
def _migrate_to_v18(conn):
|
||
"""迁移到版本18 - 金山文档上传:有效行范围配置"""
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("PRAGMA table_info(system_config)")
|
||
columns = [col[1] for col in cursor.fetchall()]
|
||
|
||
if "kdocs_row_start" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN kdocs_row_start INTEGER DEFAULT 0")
|
||
print(" [OK] 添加 system_config.kdocs_row_start 字段")
|
||
|
||
if "kdocs_row_end" not in columns:
|
||
cursor.execute("ALTER TABLE system_config ADD COLUMN kdocs_row_end INTEGER DEFAULT 0")
|
||
print(" [OK] 添加 system_config.kdocs_row_end 字段")
|
||
|
||
conn.commit()
|