Files
MovieLog_Server/lib/db.php
T
laoyang 275bd9d80f 评价区改成「按钮展开表单」,评论支持两级回复
页面默认不出现任何输入框,要写什么就点对应那个按钮:

- detail.php:评价区的「写评价 / 修改我的评价 / 评论 / 回复」都做成按钮,
  用原生 <details>/<summary> 折叠,点开才展开表单,提交后整页刷新自动收起。
  零 JS、键盘可用。没评价过只在顶部有「写评价」;评价过则该按钮消失,改在自己
  那条评价下面出现「修改我的评价」。一条评价都没有时显示「还没有评价,等你来评一个。」
- lib/reviews.php:评论改两级平铺。parent_id 为空 = 顶层评论,非空 = 对某条评论的
  回复;写「回复的回复」时由 addReviewComment 规范化回顶层评论,所以库里最多两层、
  页面永远只缩进一次,来回多少轮都不会越缩越窄。被回复者另存 reply_to_admin_id
  (存「人」不存评论 id,对方那条被删也不会悬空),页面用「回复 @某某」标出。
  deleteReviewComment 删顶层评论时连带清掉它的回复。
- lib/db.php:review_comments 增 parent_id / reply_to_admin_id 两列 + 幂等迁移,
  并给 parent_id 建索引。**修一个会让老库升级白屏的顺序 bug**:新列上的 CREATE INDEX
  必须排在 ALTER TABLE 补列之后 —— 建表用的是 CREATE TABLE IF NOT EXISTS,对线上
  已存在的旧表不生效,索引跑在补列之前会报 no such column: parent_id,整个 initDB
  挂掉。全新安装看不出来,是 e2e 的「造旧库再跑一遍初始化」专测抓到的。
- README.md:命名由「评价与讨论」改为「评价与评论」,补上按钮交互、两级平铺、
  回复 @某某 与新表字段的说明。

回归:reviews_e2e 84/84、fetch_movie 76/76、storage 196/196;A/B 47 个响应中
仅详情页相关 6 个有差异,且经折叠区块比对确认差异全部限定在评价区内。
线上 PHP 7.4.30 / 8.1.7 双版本迁移与写链路(顶层评论→回复→回复的回复→清理)全部通过。
2026-09-22 16:09:48 +08:00

196 lines
8.5 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());
}
}
// 初始化数据库
function initDB() {
$db = getDB();
// 管理员表
$db->exec("
CREATE TABLE IF NOT EXISTS admins (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
avatar 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) {}
// 删顶层评论时要按 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();
// 默认管理员
$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]);
}
}