2026-09-21 12:56:30 +08:00
<? 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 ();
// 管理员表
2026-09-22 20:33:47 +08:00
// theme:该账号选的界面配色主题(见 lib/theme.php 的主题注册表)。
// 留空/NULL 表示用默认主题,所以老账号不需要回填数据。
2026-09-21 12:56:30 +08:00
$db -> exec ( "
CREATE TABLE IF NOT EXISTS admins (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
2026-09-22 20:33:47 +08:00
avatar TEXT,
theme TEXT
2026-09-21 12:56:30 +08:00
)
" );
// 电影院表
$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
)
" );
2026-09-21 22:06:02 +08:00
// 系统设置表(键值结构,用于存储方式等配置)
$db -> exec ( "
CREATE TABLE IF NOT EXISTS settings (
name TEXT PRIMARY KEY,
value TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
" );
2026-09-22 11:42:27 +08:00
// 电影评价表
// 评价人是「登录账号」(admins)——不是观影人(persons / viewers):
// 没一起去看的人只要有账号,也能打分、写评价并参与讨论。
2026-09-22 13:03:04 +08:00
// feelings:写评价时选的「观后感受」,与评论里的感受同一套目录、同一种存法。
2026-09-22 11:42:27 +08:00
$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,
2026-09-22 13:03:04 +08:00
feelings TEXT,
2026-09-22 11:42:27 +08:00
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
)
" );
2026-09-22 16:09:48 +08:00
// 评价下的讨论(评论)。评论是**两级结构**:
// parent_id 为空 → 顶层评论,直接挂在某条评价下;
// parent_id 非空 → 对某条评论的回复,值指向「被回复的那条评论」的 id。
// 页面展示时所有回复都平铺在它所属的顶层评论之下(不再逐层缩进),
// 只把「回复 @谁」写出来;这样无论回复多深,视觉上都只有两层。
// reply_to_admin_id → 被回复者(admins.id),用于显示「回复 @某某」。
// 它存「人」而不是「评论 id」,所以对方那条评论被删掉也不会留下悬空引用。
2026-09-22 11:42:27 +08:00
// 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,
2026-09-22 16:09:48 +08:00
parent_id INTEGER,
reply_to_admin_id INTEGER,
2026-09-22 11:42:27 +08:00
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)" );
2026-09-21 12:56:30 +08:00
// ===== 迁移字段 =====
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 ) {}
2026-09-22 13:03:04 +08:00
// 新增:评价本身也可以带观后感受(评价表先于该字段上线过一版,故需要迁移)
try { $db -> exec ( "ALTER TABLE reviews ADD COLUMN feelings TEXT" ); } catch ( PDOException $e ) {}
2026-09-22 16:09:48 +08:00
// 新增:评论支持回复(两级结构,含义见上面 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 ) {}
2026-09-22 20:33:47 +08:00
// 新增:每个账号自己的界面配色主题(见 lib/theme.php)。
// 只要空列,不需要给老账号回填任何数据 —— 空值即「用默认主题」。
try { $db -> exec ( "ALTER TABLE admins ADD COLUMN theme TEXT" ); } catch ( PDOException $e ) {}
2026-09-22 16:09:48 +08:00
// 删顶层评论时要按 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)" );
2026-09-21 12:56:30 +08:00
// ===== 迁移旧数据 =====
// 将 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 ]);
}
}