Files
MovieLog_Server/lib/db.php
T
laoyang 3a3c5890cc 安装向导 + PHP 8.5 兼容修复
新增 install.php:全新部署时自动接管,用户只需上传文件、打开站点即进入
安装向导。安装页自己做三件事:
  - 建 db/ 与 uploads/{posters,tickets,photos,avatars}/,权限调到 775
  - 逐目录真写临时文件实测可写,不靠 is_writable 猜
  - 建库、建管理员、写 db/.installed 锁、自删本文件

配套改动:
  - config.php 把安装检查前置于建库之前,未安装一律 302 到 install.php。
    原实现无条件调用 initDB(),导致「打开首页」就等于建库并种下 admin/123456。
  - lib/db.php 的 initDB(\$seedDefaultAdmin = true);安装向导传 false,
    管理员由用户自己设,绝不种弱口令。新增 isInstalled()。
  - login.php / nav.php 接上 siteName(),站点名称不再只存不用。
  - lib/settings.php 新增 siteName(),缺省回退「电影日志」。

PHP 8.5 兼容:
  - 删掉 fetch_movie.php 与 lib/storage/http.php 的 curl_close()。
    8.5 起该函数 deprecated,调用会往输出插一行警告,把 JSON 响应污染成
    「警告 + JSON」,前端 JSON.parse 失败 → 电影名联想功能完全失灵。
  - config.php 的 display_errors 由 1 改为 0(生产环境不应向浏览器吐错误),
    错误仍写日志;需要调试时设 MOVIELOG_DEBUG=1 打开。

线上验证(movie.yangyq.net,PHP 8.5.12):
  未安装时 / 、/login.php 均 302 → install.php;
  安装页 200、体检 10/10 通过、自动建好 5 个目录;
  安装后 install.php 404、管理员登录成功、首页 200 无报错;
  联想接口带会话请求「流浪地球/星际穿越/霸王别姬」均返回纯 JSON(首字符 {)。
2026-10-31 21:35:41 +08:00

218 lines
10 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
// ============================================
// 文件: lib/db.php
// 说明: 数据库连接与初始化(建表 / 字段迁移 / 历史数据迁移 / 默认管理员)
// ============================================
// 数据库连接
function getDB() {
try {
$db = new PDO('sqlite:' . DB_PATH);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
return $db;
} catch (PDOException $e) {
die('数据库连接失败: ' . $e->getMessage());
}
}
// 初始化数据库
//
// $seedDefaultAdmin:admins 表为空时要不要插入默认管理员 admin/123456。
// 默认 true —— 保持老站点、老脚本的原有行为不变(它们都靠这个账号首次登录)。
// 安装向导传 false:管理员由用户在 install.php 里自己设,绝不种弱口令。
// ⚠️ 这一点很关键:若安装页也走 true,则「打开安装页」就等于「库已建好且有默认账号」,
// 弱口令会直接暴露在公网,而且「是否已安装」的判断也被这件事本身污染。
function initDB($seedDefaultAdmin = true) {
$db = getDB();
// 管理员表
// theme:该账号选的界面配色主题(见 lib/theme.php 的主题注册表)。
// 留空/NULL 表示用默认主题,所以老账号不需要回填数据。
$db->exec("
CREATE TABLE IF NOT EXISTS admins (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
avatar TEXT,
theme TEXT
)
");
// 电影院表
$db->exec("
CREATE TABLE IF NOT EXISTS cinemas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
place TEXT NOT NULL,
name TEXT NOT NULL,
address TEXT,
color TEXT DEFAULT '#888888'
)
");
// 电影表
$db->exec("
CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
info TEXT,
remark TEXT,
poster_path TEXT,
ticket_path TEXT,
photo_path TEXT,
cinema_id INTEGER,
watch_date DATE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (cinema_id) REFERENCES cinemas(id) ON DELETE SET NULL
)
");
// 观影人表
$db->exec("
CREATE TABLE IF NOT EXISTS persons (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
color TEXT DEFAULT '#888888',
avatar TEXT
)
");
// 观影关联表
$db->exec("
CREATE TABLE IF NOT EXISTS viewers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
movie_id INTEGER NOT NULL,
name TEXT,
person_id INTEGER,
FOREIGN KEY (movie_id) REFERENCES movies(id) ON DELETE CASCADE,
FOREIGN KEY (person_id) REFERENCES persons(id) ON DELETE SET NULL
)
");
// 多照片表
$db->exec("
CREATE TABLE IF NOT EXISTS movie_photos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
movie_id INTEGER NOT NULL,
photo_path TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (movie_id) REFERENCES movies(id) ON DELETE CASCADE
)
");
// 系统设置表(键值结构,用于存储方式等配置)
$db->exec("
CREATE TABLE IF NOT EXISTS settings (
name TEXT PRIMARY KEY,
value TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
");
// 电影评价表
// 评价人是「登录账号」(admins)——不是观影人(persons / viewers):
// 没一起去看的人只要有账号,也能打分、写评价并参与讨论。
// feelings:写评价时选的「观后感受」,与评论里的感受同一套目录、同一种存法。
$db->exec("
CREATE TABLE IF NOT EXISTS reviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
movie_id INTEGER NOT NULL,
admin_id INTEGER NOT NULL,
rating INTEGER NOT NULL,
content TEXT NOT NULL,
feelings TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (movie_id) REFERENCES movies(id) ON DELETE CASCADE,
FOREIGN KEY (admin_id) REFERENCES admins(id) ON DELETE CASCADE
)
");
// 评价下的讨论(评论)。评论是**两级结构**:
// parent_id 为空 → 顶层评论,直接挂在某条评价下;
// parent_id 非空 → 对某条评论的回复,值指向「被回复的那条评论」的 id。
// 页面展示时所有回复都平铺在它所属的顶层评论之下(不再逐层缩进),
// 只把「回复 @谁」写出来;这样无论回复多深,视觉上都只有两层。
// reply_to_admin_id → 被回复者(admins.id),用于显示「回复 @某某」。
// 它存「人」而不是「评论 id」,所以对方那条评论被删掉也不会留下悬空引用。
// feelings 存的是感受 key 的 JSON 数组(如 [\"happy\",\"sad\"]),
// emoji 与文案不入库——统一在 lib/reviews.php 的目录里维护,改文案不用动数据。
$db->exec("
CREATE TABLE IF NOT EXISTS review_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
review_id INTEGER NOT NULL,
admin_id INTEGER NOT NULL,
parent_id INTEGER,
reply_to_admin_id INTEGER,
content TEXT NOT NULL,
feelings TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE,
FOREIGN KEY (admin_id) REFERENCES admins(id) ON DELETE CASCADE
)
");
// 一部电影每个账号只保留一条评价:再次提交就是修改(见 saveReview)
$db->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_reviews_movie_admin ON reviews(movie_id, admin_id)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_reviews_movie ON reviews(movie_id)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_review_comments_review ON review_comments(review_id)");
// ===== 迁移字段 =====
try { $db->exec("ALTER TABLE cinemas ADD COLUMN color TEXT DEFAULT '#888888'"); } catch (PDOException $e) {}
try { $db->exec("ALTER TABLE admins ADD COLUMN avatar TEXT"); } catch (PDOException $e) {}
try { $db->exec("ALTER TABLE persons ADD COLUMN avatar TEXT"); } catch (PDOException $e) {}
try { $db->exec("ALTER TABLE movies ADD COLUMN remark TEXT"); } catch (PDOException $e) {}
try { $db->exec("ALTER TABLE viewers ADD COLUMN person_id INTEGER REFERENCES persons(id) ON DELETE SET NULL"); } catch (PDOException $e) {}
// 新增:管理员关联观影人
try { $db->exec("ALTER TABLE admins ADD COLUMN person_id INTEGER REFERENCES persons(id) ON DELETE SET NULL"); } catch (PDOException $e) {}
// 新增:评价本身也可以带观后感受(评价表先于该字段上线过一版,故需要迁移)
try { $db->exec("ALTER TABLE reviews ADD COLUMN feelings TEXT"); } catch (PDOException $e) {}
// 新增:评论支持回复(两级结构,含义见上面 review_comments 建表处的说明)。
// 评论表同样先于这两个字段上线过一版,线上是老结构,必须能自动补列。
try { $db->exec("ALTER TABLE review_comments ADD COLUMN parent_id INTEGER"); } catch (PDOException $e) {}
try { $db->exec("ALTER TABLE review_comments ADD COLUMN reply_to_admin_id INTEGER"); } catch (PDOException $e) {}
// 新增:每个账号自己的界面配色主题(见 lib/theme.php)。
// 只要空列,不需要给老账号回填任何数据 —— 空值即「用默认主题」。
try { $db->exec("ALTER TABLE admins ADD COLUMN theme TEXT"); } catch (PDOException $e) {}
// 删顶层评论时要按 parent_id 找它下面的回复一并清掉,给它建个索引。
// 这条索引必须放在上面的「补列」之后:老库的 review_comments 是旧结构、没有
// parent_id 列,而上面建表用的是 IF NOT EXISTS(对已存在的表不生效),
// 索引若跑在补列之前会直接抛 "no such column: parent_id" 让整个 initDB 挂掉。
$db->exec("CREATE INDEX IF NOT EXISTS idx_review_comments_parent ON review_comments(parent_id)");
// ===== 迁移旧数据 =====
// 将 viewers.name 迁移到 persons
$stmt = $db->query("SELECT DISTINCT name FROM viewers WHERE name != '' AND name IS NOT NULL");
$names = $stmt->fetchAll(PDO::FETCH_COLUMN);
foreach ($names as $name) {
$chk = $db->prepare("SELECT id FROM persons WHERE name = ?");
$chk->execute([$name]);
if (!$chk->fetch()) {
$ins = $db->prepare("INSERT INTO persons (name) VALUES (?)");
$ins->execute([$name]);
}
}
$update = $db->prepare("UPDATE viewers SET person_id = (SELECT id FROM persons WHERE persons.name = viewers.name) WHERE person_id IS NULL AND name IS NOT NULL");
$update->execute();
// 默认管理员(仅当调用方允许时;安装向导会传 false,由用户自己设账号)
if ($seedDefaultAdmin) {
$stmt = $db->prepare("SELECT COUNT(*) FROM admins");
$stmt->execute();
if ($stmt->fetchColumn() == 0) {
$hashed = password_hash('123456', PASSWORD_DEFAULT);
$stmt = $db->prepare("INSERT INTO admins (username, password) VALUES (?, ?)");
$stmt->execute(['admin', $hashed]);
}
}
}
// 系统是否已完成安装。
// 判据是锁文件 db/.installed(由 install.php 写入),**不看库文件是否存在** ——
// 空库、半成品库都该允许重新安装,用「库在不在」当判据会把用户永久挡在门外。
function isInstalled() {
return is_file(ROOT_DIR . '/db/.installed');
}