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'); }