179 lines
6.0 KiB
PHP
179 lines
6.0 KiB
PHP
<?php
|
|||
|
|
/**
|
||
|
|
* SQLite 数据库初始化与迁移
|
||
|
|
*/
|
||
|
|
if (!defined('FB_ROOT')) { http_response_code(403); exit('forbidden'); }
|
||
|
|
|
||
|
|
/** 打开(或创建)数据库连接,单例 */
|
||
|
|
function fb_db_init($cfg) {
|
||
|
|
static $pdo = null;
|
||
|
|
if ($pdo instanceof PDO) { return $pdo; }
|
||
|
|
|
||
|
|
$path = (string)$cfg['db_path'];
|
||
|
|
$dir = dirname($path);
|
||
|
|
if (!is_dir($dir)) { @mkdir($dir, 0755, true); }
|
||
|
|
|
||
|
|
$fresh = !file_exists($path);
|
||
|
|
|
||
|
|
$pdo = new PDO('sqlite:' . $path, null, null, [
|
||
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||
|
|
]);
|
||
|
|
$pdo->exec('PRAGMA journal_mode = WAL');
|
||
|
|
$pdo->exec('PRAGMA foreign_keys = ON');
|
||
|
|
$pdo->exec('PRAGMA busy_timeout = 5000');
|
||
|
|
|
||
|
|
fb_db_migrate($pdo);
|
||
|
|
|
||
|
|
if ($fresh) { @chmod($path, 0640); }
|
||
|
|
return $pdo;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 便捷访问器 */
|
||
|
|
function fb_db() {
|
||
|
|
return fb_db_init(fb_cfg());
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 建表(幂等) */
|
||
|
|
function fb_db_migrate(PDO $pdo) {
|
||
|
|
$pdo->exec(<<<'SQL'
|
||
|
|
CREATE TABLE IF NOT EXISTS threads (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
title TEXT NOT NULL,
|
||
|
|
body TEXT NOT NULL,
|
||
|
|
category TEXT NOT NULL DEFAULT 'other',
|
||
|
|
author_name TEXT NOT NULL,
|
||
|
|
author_email TEXT NOT NULL,
|
||
|
|
author_phone TEXT,
|
||
|
|
status TEXT NOT NULL DEFAULT 'open',
|
||
|
|
reply_count INTEGER NOT NULL DEFAULT 0,
|
||
|
|
created_at INTEGER NOT NULL,
|
||
|
|
updated_at INTEGER NOT NULL,
|
||
|
|
ip_hash TEXT
|
||
|
|
);
|
||
|
|
SQL
|
||
|
|
);
|
||
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_threads_updated ON threads(updated_at DESC)');
|
||
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_threads_cat ON threads(category)');
|
||
|
|
|
||
|
|
$pdo->exec(<<<'SQL'
|
||
|
|
CREATE TABLE IF NOT EXISTS replies (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
thread_id INTEGER NOT NULL,
|
||
|
|
body TEXT NOT NULL,
|
||
|
|
author_name TEXT NOT NULL,
|
||
|
|
author_email TEXT,
|
||
|
|
is_admin INTEGER NOT NULL DEFAULT 0,
|
||
|
|
created_at INTEGER NOT NULL,
|
||
|
|
ip_hash TEXT,
|
||
|
|
FOREIGN KEY(thread_id) REFERENCES threads(id) ON DELETE CASCADE
|
||
|
|
);
|
||
|
|
SQL
|
||
|
|
);
|
||
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_replies_thread ON replies(thread_id, created_at)');
|
||
|
|
|
||
|
|
$pdo->exec(<<<'SQL'
|
||
|
|
CREATE TABLE IF NOT EXISTS notify_log (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
thread_id INTEGER NOT NULL,
|
||
|
|
reply_id INTEGER NOT NULL,
|
||
|
|
to_email TEXT NOT NULL,
|
||
|
|
created_at INTEGER NOT NULL,
|
||
|
|
ok INTEGER NOT NULL DEFAULT 0,
|
||
|
|
tries INTEGER NOT NULL DEFAULT 0,
|
||
|
|
err TEXT
|
||
|
|
);
|
||
|
|
SQL
|
||
|
|
);
|
||
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_notify_thread ON notify_log(thread_id, to_email)');
|
||
|
|
|
||
|
|
// 兼容早期建库:确保 notify_log.tries 存在
|
||
|
|
$cols = $pdo->query('PRAGMA table_info(notify_log)')->fetchAll();
|
||
|
|
$has = false;
|
||
|
|
foreach ($cols as $c) { if (isset($c['name']) && $c['name'] === 'tries') { $has = true; break; } }
|
||
|
|
if (!$has) { $pdo->exec('ALTER TABLE notify_log ADD COLUMN tries INTEGER NOT NULL DEFAULT 0'); }
|
||
|
|
|
||
|
|
$pdo->exec(<<<'SQL'
|
||
|
|
CREATE TABLE IF NOT EXISTS attachments (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
thread_id INTEGER NOT NULL,
|
||
|
|
reply_id INTEGER,
|
||
|
|
kind TEXT NOT NULL DEFAULT 'image',
|
||
|
|
oss_key TEXT NOT NULL,
|
||
|
|
mime TEXT,
|
||
|
|
size INTEGER NOT NULL DEFAULT 0,
|
||
|
|
width INTEGER,
|
||
|
|
height INTEGER,
|
||
|
|
orig_name TEXT,
|
||
|
|
seq INTEGER NOT NULL DEFAULT 0,
|
||
|
|
created_at INTEGER NOT NULL,
|
||
|
|
FOREIGN KEY(thread_id) REFERENCES threads(id) ON DELETE CASCADE,
|
||
|
|
FOREIGN KEY(reply_id) REFERENCES replies(id) ON DELETE CASCADE
|
||
|
|
);
|
||
|
|
SQL
|
||
|
|
);
|
||
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_att_thread ON attachments(thread_id, seq)');
|
||
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_att_reply ON attachments(reply_id)');
|
||
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_att_age ON attachments(created_at)');
|
||
|
|
|
||
|
|
$pdo->exec(<<<'SQL'
|
||
|
|
CREATE TABLE IF NOT EXISTS rate_limit (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
ip_hash TEXT NOT NULL,
|
||
|
|
action TEXT NOT NULL,
|
||
|
|
created_at INTEGER NOT NULL
|
||
|
|
);
|
||
|
|
SQL
|
||
|
|
);
|
||
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_rl ON rate_limit(ip_hash, action, created_at)');
|
||
|
|
|
||
|
|
/* ---- 站点设置(后台可改,覆盖 config.php 的同名项)----
|
||
|
|
* 只存「后台允许修改」的键,见 util.php 的 fb_setting_keys()。
|
||
|
|
* value 存 JSON,读取时自动还原类型。
|
||
|
|
*/
|
||
|
|
$pdo->exec(<<<'SQL'
|
||
|
|
CREATE TABLE IF NOT EXISTS settings (
|
||
|
|
key TEXT PRIMARY KEY,
|
||
|
|
value TEXT,
|
||
|
|
updated_at INTEGER
|
||
|
|
);
|
||
|
|
SQL
|
||
|
|
);
|
||
|
|
|
||
|
|
/* ---- 管理员操作日志 ---- */
|
||
|
|
$pdo->exec(<<<'SQL'
|
||
|
|
CREATE TABLE IF NOT EXISTS mod_log (
|
||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
|
|
thread_id INTEGER,
|
||
|
|
action TEXT NOT NULL,
|
||
|
|
detail TEXT,
|
||
|
|
created_at INTEGER NOT NULL,
|
||
|
|
ip_hash TEXT
|
||
|
|
);
|
||
|
|
SQL
|
||
|
|
);
|
||
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_modlog_thread ON mod_log(thread_id, created_at DESC)');
|
||
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_modlog_time ON mod_log(created_at DESC)');
|
||
|
|
|
||
|
|
// ---- 审核(先审后显)----
|
||
|
|
// visible:0=待审核/未公开,1=审核通过已公开
|
||
|
|
// mod_note:审核说明(关闭时写给提问人的解释,会随邮件发出)
|
||
|
|
// moderated_at:审核时间
|
||
|
|
$tcols = $pdo->query('PRAGMA table_info(threads)')->fetchAll();
|
||
|
|
$tnames = [];
|
||
|
|
foreach ($tcols as $c) { if (isset($c['name'])) { $tnames[(string)$c['name']] = true; } }
|
||
|
|
|
||
|
|
if (!isset($tnames['visible'])) {
|
||
|
|
$pdo->exec('ALTER TABLE threads ADD COLUMN visible INTEGER NOT NULL DEFAULT 0');
|
||
|
|
// 功能上线前已存在的内容视为"已审核通过",保持原来的可见性
|
||
|
|
$pdo->exec('UPDATE threads SET visible = 1');
|
||
|
|
}
|
||
|
|
if (!isset($tnames['mod_note'])) {
|
||
|
|
$pdo->exec('ALTER TABLE threads ADD COLUMN mod_note TEXT');
|
||
|
|
}
|
||
|
|
if (!isset($tnames['moderated_at'])) {
|
||
|
|
$pdo->exec('ALTER TABLE threads ADD COLUMN moderated_at INTEGER');
|
||
|
|
}
|
||
|
|
$pdo->exec('CREATE INDEX IF NOT EXISTS idx_threads_visible ON threads(visible, created_at DESC)');
|
||
|
|
}
|