SecHub/db.php
2026-06-01 23:57:52 +08:00

412 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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;
}
}