PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ); if ($driver === 'mysql') { $options[PDO::ATTR_EMULATE_PREPARES] = false; $m = $cfg['db']['mysql']; $dsn = 'mysql:host=' . $m['host'] . ';port=' . $m['port'] . ';dbname=' . $m['database'] . ';charset=' . $m['charset']; $pdo = new PDO($dsn, $m['username'], $m['password'], $options); } else { $path = $cfg['db']['sqlite']['path']; $dir = dirname($path); if (!is_dir($dir)) { @mkdir($dir, 0777, true); } $pdo = new PDO('sqlite:' . $path, null, null, $options); // 使用回滚日志(非 WAL):事务提交即写入主库文件, // 避免 -wal/-shm 在站点迁移、文件覆盖等场景下丢失,导致写入看似成功但未真正保存 $pdo->exec('PRAGMA journal_mode = DELETE'); $pdo->exec('PRAGMA synchronous = NORMAL'); $pdo->exec('PRAGMA busy_timeout = 5000'); $pdo->exec('PRAGMA foreign_keys = ON'); } init_schema($pdo, $driver); return $pdo; } /** * 建表(幂等) */ function init_schema($pdo, $driver) { if ($driver === 'mysql') { $pdo->exec("CREATE TABLE IF NOT EXISTS users ( id VARCHAR(32) NOT NULL, username VARCHAR(32) NOT NULL, password_hash VARCHAR(255) NOT NULL, role VARCHAR(16) NOT NULL DEFAULT 'user', disabled TINYINT NOT NULL DEFAULT 0, must_change_password TINYINT NOT NULL DEFAULT 0, created_at VARCHAR(32) DEFAULT NULL, last_login_at VARCHAR(32) DEFAULT NULL, login_count INT NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_users_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $pdo->exec("CREATE TABLE IF NOT EXISTS sessions ( token VARCHAR(64) NOT NULL, user_id VARCHAR(32) NOT NULL, created_at VARCHAR(32) DEFAULT NULL, expires_at BIGINT NOT NULL, ip VARCHAR(45) DEFAULT NULL, user_agent VARCHAR(255) DEFAULT NULL, PRIMARY KEY (token), KEY idx_sessions_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $pdo->exec("CREATE TABLE IF NOT EXISTS topologies ( id VARCHAR(32) NOT NULL, name VARCHAR(120) NOT NULL, owner_id VARCHAR(32) NOT NULL, visibility VARCHAR(16) NOT NULL DEFAULT 'private', data LONGTEXT, node_count INT NOT NULL DEFAULT 0, edge_count INT NOT NULL DEFAULT 0, created_at VARCHAR(32) DEFAULT NULL, updated_at VARCHAR(32) DEFAULT NULL, PRIMARY KEY (id), KEY idx_topo_owner (owner_id), KEY idx_topo_updated (updated_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $pdo->exec("CREATE TABLE IF NOT EXISTS activity_logs ( id INT NOT NULL AUTO_INCREMENT, user_id VARCHAR(32) DEFAULT NULL, username VARCHAR(32) DEFAULT NULL, action VARCHAR(40) DEFAULT NULL, target VARCHAR(64) DEFAULT NULL, detail VARCHAR(255) DEFAULT NULL, ip VARCHAR(45) DEFAULT NULL, created_at VARCHAR(32) DEFAULT NULL, PRIMARY KEY (id), KEY idx_logs_created (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); $pdo->exec("CREATE TABLE IF NOT EXISTS settings ( skey VARCHAR(64) NOT NULL, sval LONGTEXT, updated_at VARCHAR(32) DEFAULT NULL, PRIMARY KEY (skey) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); return; } /* ---------- SQLite ---------- */ $pdo->exec("CREATE TABLE IF NOT EXISTS users ( id TEXT NOT NULL PRIMARY KEY, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', disabled INTEGER NOT NULL DEFAULT 0, must_change_password INTEGER NOT NULL DEFAULT 0, created_at TEXT, last_login_at TEXT, login_count INTEGER NOT NULL DEFAULT 0 )"); $pdo->exec("CREATE TABLE IF NOT EXISTS sessions ( token TEXT NOT NULL PRIMARY KEY, user_id TEXT NOT NULL, created_at TEXT, expires_at INTEGER NOT NULL, ip TEXT, user_agent TEXT )"); $pdo->exec("CREATE TABLE IF NOT EXISTS topologies ( id TEXT NOT NULL PRIMARY KEY, name TEXT NOT NULL, owner_id TEXT NOT NULL, visibility TEXT NOT NULL DEFAULT 'private', data TEXT, node_count INTEGER NOT NULL DEFAULT 0, edge_count INTEGER NOT NULL DEFAULT 0, created_at TEXT, updated_at TEXT )"); $pdo->exec("CREATE TABLE IF NOT EXISTS activity_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT, username TEXT, action TEXT, target TEXT, detail TEXT, ip TEXT, created_at TEXT )"); $pdo->exec("CREATE TABLE IF NOT EXISTS settings ( skey TEXT NOT NULL PRIMARY KEY, sval TEXT, updated_at TEXT )"); $pdo->exec("CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)"); $pdo->exec("CREATE INDEX IF NOT EXISTS idx_topo_owner ON topologies(owner_id)"); $pdo->exec("CREATE INDEX IF NOT EXISTS idx_logs_created ON activity_logs(created_at)"); }