#!/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), ] 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 if current_version != target_version: set_current_version(conn, target_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()