412 lines
12 KiB
PHP
412 lines
12 KiB
PHP
<?php
|
||
/**
|
||
* SecHub 数据库管理类
|
||
* 负责JSON数据到SQLite的转换和管理
|
||
*/
|
||
|
||
class SecHubDatabase {
|
||
private $dbPath;
|
||
private $jsonDir;
|
||
private $db;
|
||
private $needsInitialSync = false;
|
||
|
||
public function __construct($dbPath, $jsonDir) {
|
||
$this->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;
|
||
}
|
||
} |