dbPath = $dbPath; $this->jsonDir = $jsonDir; $this->initDatabase(); } /** * 初始化数据库连接 */ private function initDatabase() { try { // 检查数据库文件是否存在 $dbExists = file_exists($this->dbPath); $this->db = new PDO('sqlite:' . $this->dbPath); $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); // 创建同步日志表 $this->createSyncLogTable(); // 如果数据库文件是新创建的,或者没有任何业务数据表,标记需要同步 if (!$dbExists || $this->isEmptyDatabase()) { $this->needsInitialSync = true; } } catch (PDOException $e) { error_log("数据库连接失败: " . $e->getMessage()); throw $e; } } /** * 检查数据库是否为空(没有业务数据表) */ private function isEmptyDatabase() { try { $sql = "SELECT count(*) as table_count FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'json_sync_log'"; $stmt = $this->db->query($sql); $result = $stmt->fetch(); return $result['table_count'] == 0; } catch (Exception $e) { return true; } } /** * 创建同步日志表 */ private function createSyncLogTable() { $sql = "CREATE TABLE IF NOT EXISTS json_sync_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, json_filename TEXT UNIQUE NOT NULL, table_name TEXT NOT NULL, section_no INTEGER DEFAULT 0, last_sync_time DATETIME NOT NULL, json_file_mtime INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )"; $this->db->exec($sql); } /** * 检查并同步JSON数据到数据库 */ public function syncJsonToDatabase() { // 如果是初始同步,强制同步所有文件 if ($this->needsInitialSync) { $jsonFiles = glob($this->jsonDir . '*.json'); foreach ($jsonFiles as $file) { $this->syncSingleFile($file); } $this->needsInitialSync = false; return; } // 正常增量同步 $jsonFiles = glob($this->jsonDir . '*.json'); foreach ($jsonFiles as $file) { $this->syncSingleFile($file); } } /** * 同步单个JSON文件到数据库 */ private function syncSingleFile($filePath) { $filename = basename($filePath); $tableName = pathinfo($filename, PATHINFO_FILENAME); // 检查是否需要更新 if (!$this->shouldUpdate($filePath, $tableName)) { return; } // 读取JSON数据 $data = $this->loadJsonData($filePath); if (empty($data)) { return; } // 获取排序号(从第一个数据项的 no 字段) $sectionNo = $data[0]['no'] ?? 0; // 创建或更新表 $this->createTable($tableName, $data[0]); // 清空旧数据 $this->clearTable($tableName); // 插入新数据(跳过第一个section项) $items = array_slice($data, 1); foreach ($items as $item) { $this->insertItem($tableName, $item, $data[0]['section'] ?? $tableName); } // 更新同步日志(包含排序号) $this->updateSyncLog($filename, $tableName, $sectionNo); } /** * 更新同步日志 */ private function updateSyncLog($filename, $tableName, $sectionNo = 0) { $jsonFile = $this->jsonDir . $filename; $jsonModified = filemtime($jsonFile); $syncTime = date('Y-m-d H:i:s'); // 检查是否已存在记录 $sql = "SELECT id FROM json_sync_log WHERE json_filename = :filename"; $stmt = $this->db->prepare($sql); $stmt->execute([':filename' => $filename]); $exists = $stmt->fetch(); if ($exists) { // 更新现有记录 $sql = "UPDATE json_sync_log SET table_name = :table_name, section_no = :section_no, last_sync_time = :sync_time, json_file_mtime = :mtime WHERE json_filename = :filename"; } else { // 插入新记录 $sql = "INSERT INTO json_sync_log (json_filename, table_name, section_no, last_sync_time, json_file_mtime) VALUES (:filename, :table_name, :section_no, :sync_time, :mtime)"; } $stmt = $this->db->prepare($sql); $stmt->execute([ ':filename' => $filename, ':table_name' => $tableName, ':section_no' => $sectionNo, ':sync_time' => $syncTime, ':mtime' => $jsonModified ]); } /** * 判断是否需要更新 */ private function shouldUpdate($jsonFile, $tableName) { $filename = basename($jsonFile); $jsonModified = filemtime($jsonFile); // 查询该JSON文件的同步记录 $sql = "SELECT * FROM json_sync_log WHERE json_filename = :filename"; $stmt = $this->db->prepare($sql); $stmt->execute([':filename' => $filename]); $log = $stmt->fetch(); // 如果没有同步记录,需要更新 if (!$log) { return true; } // 计算时间差(秒) $timeDiff = $jsonModified - $log['json_file_mtime']; // 如果JSON文件修改时间比记录的晚至少5分钟(300秒),则需要更新 if ($timeDiff >= 300) { return true; } // 否则不需要更新 return false; } /** * 加载JSON数据 */ private function loadJsonData($filePath) { if (!file_exists($filePath)) { return []; } $content = file_get_contents($filePath); if ($content === false) { return []; } $data = json_decode($content, true); if (json_last_error() !== JSON_ERROR_NONE) { error_log("JSON解析错误: " . json_last_error_msg()); return []; } return is_array($data) ? $data : []; } /** * 创建数据表 */ private function createTable($tableName, $firstItem) { // 清理表名,只保留字母、数字和下划线 $tableName = preg_replace('/[^a-zA-Z0-9_]/', '_', $tableName); $sql = "CREATE TABLE IF NOT EXISTS {$tableName} ( id INTEGER PRIMARY KEY AUTOINCREMENT, section TEXT NOT NULL, name TEXT NOT NULL, url TEXT, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP )"; $this->db->exec($sql); } /** * 清空表数据 */ private function clearTable($tableName) { $tableName = preg_replace('/[^a-zA-Z0-9_]/', '_', $tableName); $this->db->exec("DELETE FROM {$tableName}"); } /** * 插入数据项 */ private function insertItem($tableName, $item, $section) { $tableName = preg_replace('/[^a-zA-Z0-9_]/', '_', $tableName); $sql = "INSERT INTO {$tableName} (section, name, url, description) VALUES (:section, :name, :url, :description)"; $stmt = $this->db->prepare($sql); $stmt->execute([ ':section' => $section, ':name' => $item['name'] ?? '', ':url' => $item['url'] ?? '', ':description' => $item['description'] ?? '' ]); } /** * 全局搜索 */ public function globalSearch($keyword) { if (empty($keyword)) { return []; } $tables = $this->getAllTables(); $results = []; foreach ($tables as $table) { $tableName = $table['name']; $sql = "SELECT *, '{$tableName}' as source_table FROM {$tableName} WHERE name LIKE :keyword OR description LIKE :keyword OR url LIKE :keyword ORDER BY name"; $stmt = $this->db->prepare($sql); $stmt->execute([':keyword' => '%' . $keyword . '%']); $items = $stmt->fetchAll(); if (!empty($items)) { $results[$tableName] = [ 'section' => $items[0]['section'] ?? $tableName, 'items' => $items ]; } } return $results; } /** * 按栏目搜索 */ public function searchBySection($section, $keyword) { if (empty($keyword)) { return []; } $tables = $this->getAllTables(); $results = []; foreach ($tables as $table) { $tableName = $table['name']; $sql = "SELECT * FROM {$tableName} WHERE section = :section AND (name LIKE :keyword OR description LIKE :keyword OR url LIKE :keyword) ORDER BY name"; $stmt = $this->db->prepare($sql); $stmt->execute([ ':section' => $section, ':keyword' => '%' . $keyword . '%' ]); $items = $stmt->fetchAll(); if (!empty($items)) { $results[] = $items; } } return array_merge(...$results); } /** * 获取所有栏目配置 */ public function getSectionsConfig() { $tables = $this->getAllTables(); $sections = []; foreach ($tables as $table) { $tableName = $table['name']; // 从同步日志中获取排序号 $sql = "SELECT section_no FROM json_sync_log WHERE table_name = :table_name LIMIT 1"; $stmt = $this->db->prepare($sql); $stmt->execute([':table_name' => $tableName]); $log = $stmt->fetch(); $sectionNo = $log ? $log['section_no'] : 0; // 获取栏目名称 $sql = "SELECT DISTINCT section FROM {$tableName} LIMIT 1"; $stmt = $this->db->query($sql); $row = $stmt->fetch(); if ($row) { $sections[$tableName] = [ 'title' => $row['section'], 'table' => $tableName, 'no' => $sectionNo ]; } } // 按 no 字段排序 uasort($sections, function($a, $b) { return ($a['no'] ?? 0) - ($b['no'] ?? 0); }); return $sections; } /** * 获取指定栏目的所有项目 */ public function getItemsBySection($tableName) { $tableName = preg_replace('/[^a-zA-Z0-9_]/', '_', $tableName); $sql = "SELECT * FROM {$tableName} ORDER BY name"; $stmt = $this->db->query($sql); return $stmt->fetchAll(); } /** * 获取所有表名 */ private function getAllTables() { $sql = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'json_sync_log' ORDER BY name"; $stmt = $this->db->query($sql); return $stmt->fetchAll(); } /** * 关闭数据库连接 */ public function close() { $this->db = null; } }