Files
MovieLog_Server/lib/db.php
T
laoyang ee256adc4b 界面主题:5 套经典配色,按账号记住
系统设置里多一块「🎨 我的界面主题」,点卡片即时预览、保存后全站生效,不用重新登录。

- lib/theme.php(新增):主题注册表 ticket/indigo/forest/wine/midnight,
  分别是「复古票根(暖橙,默认)/ 靛蓝 / 松林 / 绛红 / 暗夜(深色)」。
  currentThemeKey() 以 session 为准,session 里没有就从 admins.theme 补读一次
  —— 升级前就已经登录着的会话也能立刻用上自己账号存的主题。库里是脏值时回落默认主题。
- css/themes.css(新增):5 套调色板,全部以 [data-theme="<key>"] 规则块给出,
  每套 16 个变量一个不少。**必须排在 css/style.css 之后加载**:两者优先级相同
  (都是 0,1,0),靠后出现者胜。另加 --primary-rgb("R, G, B" 三元组),
  页面里的淡色底/光晕一律 rgba(var(--primary-rgb), α),所以加一套主题只需动 CSS,
  页面与 JS 一行都不用改;每套还要声明 color-scheme,否则暗色主题下原生控件
  (日期选择器、下拉、滚动条)仍是刺眼的白底。
- css/style.css::root 保留默认主题那一份作兜底;导航底色改走 --nav-bg,
  滚动条改走变量,按钮光晕改走 --primary-rgb。
- lib/db.php / lib/auth.php:admins 增列 theme(幂等 ALTER,老库自动补,空值 = 默认主题);
  setAdminTheme() 校验 key、写库、同步 session。
- settings.php:主题卡片区。每张预览卡自己套一层 data-theme,用的就是那套主题真实的
  变量,因此「预览所见」与「换上去之后」必然一致,不必再抄一份色值;另有一小段 JS
  在点选时改 <html data-theme> 做即时预览(没脚本时选中后提交同样生效)。
- 14 个页面:<html> 上输出 data-theme,并在 style.css 之后引入 themes.css;
  内联样式里写死的橙色 rgba(230,126,34,…) 全部改走 rgba(var(--primary-rgb),…);
  影院/观影人色板选中态的描边由 #333 改为 var(--text-primary)(暗色主题下才看得见)。
- annual_report.php:报告本身仍是深色「海报」版式,但点缀色跟随主题;
  Chart.js 画在 canvas 上读不到 CSS 变量,改为运行时从 <html> 的计算样式取色。

顺手修的两个坑:
1. 注释里**不能**出现 PHP 起止标签。PHP 的结束标签在 // 注释里同样会关闭 PHP 模式,
   之后若再出现短标签就会被真的执行 —— lib/theme.php 的用法说明里写了完整标签示例,
   导致该文件被 require 时直接 Fatal error、整站白屏,而 php -l 查不出来(只看语法)。
   现在部署脚本里加了一道静态断言专门挡这个。
2. themesCssLink() 的返回值末尾补一个换行:PHP 会吃掉结束标签后面那一个换行,
   不补的话渲染结果里这一行会和下一行黏成一行。

回归:theme_e2e 111/111(含老库自动补列、脏值回落、5 个选项渲染、切换后落库+落 session+
立刻生效、跨账号隔离、非法 key 与注入被拒、themes.css 完整性、:root 与 ticket 一致、
无残留写死主色、无悬空 CSS 变量);reviews_e2e 84/84、fetch_movie 76/76、storage 196/196。
A/B 48 个响应:把「新增 head 行 / data-theme 属性 / 橙色→变量」三类等价替换归一化、
并把设置页新增区块整块删掉之后,**其余部分逐字节一致,零残留差异**。
线上 PHP 7.4.30 / 8.1.7 双版本冒烟全通过(迁移、5 套主题切换、跨账号隔离均验证,
写测试后已把 admin #1 的主题原样还原、其它账号一字未动)。
2026-09-22 20:33:47 +08:00

203 lines
9.0 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();
// 管理员表
// 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();
// 默认管理员
$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]);
}
}