refactor: 拆分 config.php 为 lib/ 模块,消除页面层重复代码

纯结构性重构,不改变任何功能。

- config.php 由 739 行缩减为 42 行,改为纯引导入口;新增 ROOT_DIR 常量
  统一拼接绝对路径(lib/ 内模块的 __DIR__ 会指向 lib/,必须替换)
- 新增 lib/ 五个模块 db.php / helpers.php / auth.php / movies.php / stats.php,
  由 config.php 按 db → helpers → auth → movies → stats 顺序加载
- add.php、edit.php 各自约 80 行重复的 POST 处理改为调用
  resolveCinemaId / resolvePersonIds / linkViewers / uploadImageFile /
  saveBase64Image / uploadMultiplePhotos / deleteStoredFile
- cinemas.php、persons.php 中重复 4 次的颜色校验改用 normalizeHexColor()
- toggleNewViewer / removeViewerRow / toggleNewCinema 下沉至 js/main.js,
  add.php 中的 removeViewer 统一更名为 removeViewerRow
- 删除死代码:imageUrl()(全项目无调用);cinemas.php 配色数组中本就会被
  array_slice 丢弃的 extra 颜色逻辑,简化为单条扁平 50 元素数组
- README 目录结构补充 lib/ 说明

验证方式:以便携 PHP 8.4 搭建 A/B 回归环境,播种固定数据后遍历 47 个
GET/POST 端点录制「状态码 + 响应体」。重构前后 42 个逐字节相同、2 个仅
上传文件名时间戳不同、3 个差异均为内联脚本下沉(已单独校验函数体逐字符
等价,剥离脚本后 HTML 标记完全一致)。24 个 PHP 文件语法检查全部通过。
This commit is contained in:
2026-09-21 12:56:30 +08:00
parent 40e1bc5e56
commit dd957d45bd
13 changed files with 1002 additions and 1022 deletions
+69
View File
@@ -0,0 +1,69 @@
<?php
// ============================================
// 文件: lib/auth.php
// 说明: 登录校验与管理员账号管理
// ============================================
// 登录检查
function checkLogin() {
if (!isset($_SESSION['admin_id'])) {
header('Location: login.php');
exit;
}
}
// 获取当前管理员(含关联观影人信息)
function getCurrentAdmin() {
if (isset($_SESSION['admin_id'])) {
$db = getDB();
$stmt = $db->prepare("
SELECT a.*, p.id as person_id, p.name as person_name, p.color as person_color, p.avatar as person_avatar
FROM admins a
LEFT JOIN persons p ON a.person_id = p.id
WHERE a.id = ?
");
$stmt->execute([$_SESSION['admin_id']]);
return $stmt->fetch();
}
return null;
}
// 获取所有管理员(含关联观影人)
function getAllAdmins() {
$db = getDB();
$stmt = $db->query("
SELECT a.id, a.username, a.avatar, a.person_id, p.name as person_name
FROM admins a
LEFT JOIN persons p ON a.person_id = p.id
ORDER BY a.id
");
return $stmt->fetchAll();
}
// 添加管理员(支持关联观影人)
function addAdmin($username, $password, $avatar = null, $person_id = null) {
$db = getDB();
$stmt = $db->prepare("SELECT COUNT(*) FROM admins WHERE username = ?");
$stmt->execute([$username]);
if ($stmt->fetchColumn() > 0) return false;
$hashed = password_hash($password, PASSWORD_DEFAULT);
$stmt = $db->prepare("INSERT INTO admins (username, password, avatar, person_id) VALUES (?, ?, ?, ?)");
return $stmt->execute([$username, $hashed, $avatar, $person_id]);
}
// 删除管理员
function deleteAdmin($id) {
$db = getDB();
$stmt = $db->query("SELECT COUNT(*) FROM admins");
if ($stmt->fetchColumn() <= 1) return false;
if ($id == $_SESSION['admin_id']) return false;
$stmt = $db->prepare("DELETE FROM admins WHERE id = ?");
return $stmt->execute([$id]);
}
// 更新管理员的观影人关联
function updateAdminPersonId($admin_id, $person_id) {
$db = getDB();
$stmt = $db->prepare("UPDATE admins SET person_id = ? WHERE id = ?");
return $stmt->execute([$person_id, $admin_id]);
}
+126
View File
@@ -0,0 +1,126 @@
<?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
)
");
// ===== 迁移字段 =====
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) {}
// ===== 迁移旧数据 =====
// 将 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]);
}
}
+131
View File
@@ -0,0 +1,131 @@
<?php
// ============================================
// 文件: lib/helpers.php
// 说明: 通用工具函数(输出转义、文件名、颜色、上传文件读写、图片 URL)
// ============================================
// 允许上传的图片扩展名
function allowedImageExts() {
return ['jpg', 'jpeg', 'png', 'gif', 'webp'];
}
// 安全输出
function h($str) {
return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
}
// 生成唯一文件名
function uniqueFilename($original) {
$ext = strtolower(pathinfo($original, PATHINFO_EXTENSION));
if (!in_array($ext, allowedImageExts())) {
$ext = 'jpg';
}
return date('YmdHis') . '_' . uniqid() . '.' . $ext;
}
// 校验并归一化十六进制颜色,非法值回退为灰色
function normalizeHexColor($color, $fallback = '#888888') {
$color = trim((string)$color);
return preg_match('/^#[a-fA-F0-9]{6}$/', $color) ? $color : $fallback;
}
// 删除项目内已存在的上传文件(文件不存在时静默跳过)
function deleteStoredFile($relative_path) {
if (!empty($relative_path)) {
$full = ROOT_DIR . '/' . $relative_path;
if (file_exists($full)) {
return unlink($full);
}
}
return false;
}
// 处理单个图片上传字段,成功返回相对路径,失败返回 null
function uploadImageFile($field, $target_dir, $relative_prefix) {
if (!isset($_FILES[$field]) || $_FILES[$field]['error'] !== UPLOAD_ERR_OK) {
return null;
}
$ext = strtolower(pathinfo($_FILES[$field]['name'], PATHINFO_EXTENSION));
if (!in_array($ext, allowedImageExts())) {
return null;
}
$filename = uniqueFilename($_FILES[$field]['name']);
if (!move_uploaded_file($_FILES[$field]['tmp_name'], $target_dir . $filename)) {
return null;
}
return $relative_prefix . $filename;
}
// 处理多张图片上传字段(如 photos[] / new_photos[]),写入 movie_photos 表
function uploadMultiplePhotos($field, $movie_id) {
if (!isset($_FILES[$field]) || !is_array($_FILES[$field]['tmp_name'])) {
return;
}
$db = getDB();
$files = $_FILES[$field];
for ($i = 0; $i < count($files['tmp_name']); $i++) {
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
continue;
}
$ext = strtolower(pathinfo($files['name'][$i], PATHINFO_EXTENSION));
if (!in_array($ext, allowedImageExts())) {
continue;
}
$filename = uniqueFilename($files['name'][$i]);
if (!move_uploaded_file($files['tmp_name'][$i], PHOTO_DIR . $filename)) {
continue;
}
$stmt = $db->prepare("INSERT INTO movie_photos (movie_id, photo_path) VALUES (?, ?)");
$stmt->execute([$movie_id, 'uploads/photos/' . $filename]);
}
}
// 海报 URL
function posterUrl($path) {
if ($path && file_exists(ROOT_DIR . '/' . $path)) {
return '/' . $path;
}
return '/assets/placeholder-poster.jpg';
}
// ===== 头像处理 =====
function getAvatarUrl($avatar_path, $name = '') {
if ($avatar_path && file_exists(ROOT_DIR . '/' . $avatar_path)) {
return '/' . $avatar_path;
}
if (!empty($name)) {
$letter = mb_substr($name, 0, 1, 'UTF-8');
$colors = ['#e74c3c','#e67e22','#f1c40f','#2ecc71','#3498db','#9b59b6','#1abc9c','#e84393','#00b894','#0984e3'];
$color = $colors[abs(crc32($name)) % count($colors)];
return "data:image/svg+xml," . urlencode("<svg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'><rect width='40' height='40' fill='{$color}'/><text x='20' y='28' text-anchor='middle' font-size='20' fill='#fff' font-family='sans-serif' font-weight='bold'>{$letter}</text></svg>");
}
return '/assets/default-avatar.png';
}
// ===== 保存 Base64 图片 =====
function saveBase64Image($base64_data, $save_dir, $prefix = 'avatar') {
if (strpos($base64_data, 'base64,') !== false) {
$base64_data = substr($base64_data, strpos($base64_data, 'base64,') + 7);
}
$binary = base64_decode($base64_data);
if ($binary === false) return false;
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_buffer($finfo, $binary);
finfo_close($finfo);
$ext_map = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp'
];
if (!isset($ext_map[$mime])) return false;
$ext = $ext_map[$mime];
$filename = date('YmdHis') . '_' . uniqid() . '_' . $prefix . '.' . $ext;
$filepath = $save_dir . $filename;
if (file_put_contents($filepath, $binary) === false) return false;
return str_replace(ROOT_DIR . '/', '', $filepath);
}
+196
View File
@@ -0,0 +1,196 @@
<?php
// ============================================
// 文件: lib/movies.php
// 说明: 电影 / 影院读取与搜索,以及表单提交时的关联数据处理
// ============================================
// ===== 电影列表 =====
function getMovies() {
$db = getDB();
$stmt = $db->query("
SELECT m.*, c.place, c.name as cinema_name, c.address as cinema_address, c.color as cinema_color
FROM movies m
LEFT JOIN cinemas c ON m.cinema_id = c.id
ORDER BY m.watch_date DESC, m.created_at DESC
");
$movies = $stmt->fetchAll();
attachViewersToMovies($movies);
return $movies;
}
function getMovie($id) {
$db = getDB();
$stmt = $db->prepare("
SELECT m.*, c.place, c.name as cinema_name, c.address as cinema_address, c.color as cinema_color
FROM movies m
LEFT JOIN cinemas c ON m.cinema_id = c.id
WHERE m.id = ?
");
$stmt->execute([$id]);
$movie = $stmt->fetch();
if ($movie) {
$stmt2 = $db->prepare("
SELECT v.id, v.movie_id, v.person_id,
COALESCE(p.name, v.name) as name,
p.color as person_color,
p.avatar as person_avatar
FROM viewers v
LEFT JOIN persons p ON v.person_id = p.id
WHERE v.movie_id = ?
ORDER BY v.id
");
$stmt2->execute([$id]);
$movie['viewers'] = $stmt2->fetchAll();
$stmt3 = $db->prepare("SELECT * FROM movie_photos WHERE movie_id = ? ORDER BY id");
$stmt3->execute([$id]);
$movie['photos'] = $stmt3->fetchAll();
}
return $movie;
}
function attachViewersToMovies(&$movies) {
if (empty($movies)) return;
$db = getDB();
$movie_ids = array_column($movies, 'id');
if (empty($movie_ids)) return;
$placeholders = implode(',', array_fill(0, count($movie_ids), '?'));
$stmt = $db->prepare("
SELECT v.movie_id, p.id as person_id, p.name, p.color, p.avatar
FROM viewers v
LEFT JOIN persons p ON v.person_id = p.id
WHERE v.movie_id IN ($placeholders)
ORDER BY v.id
");
$stmt->execute($movie_ids);
$rows = $stmt->fetchAll();
$viewers_map = [];
foreach ($rows as $row) {
$movie_id = $row['movie_id'];
if (!isset($viewers_map[$movie_id])) $viewers_map[$movie_id] = [];
$viewers_map[$movie_id][] = [
'person_id' => $row['person_id'],
'name' => $row['name'] ?? '未知',
'color' => $row['color'] ?? '#888888',
'avatar' => $row['avatar']
];
}
foreach ($movies as &$movie) {
$movie['viewers'] = $viewers_map[$movie['id']] ?? [];
}
}
// ===== 影院 =====
function getCinemas() {
$db = getDB();
$stmt = $db->query("SELECT * FROM cinemas ORDER BY place, name");
return $stmt->fetchAll();
}
// ===== 搜索 =====
function searchMovies($keyword) {
return advancedSearch(['title' => $keyword]);
}
function advancedSearch($params) {
$db = getDB();
$sql = "
SELECT DISTINCT m.*,
c.place, c.name as cinema_name, c.address as cinema_address, c.color as cinema_color
FROM movies m
LEFT JOIN cinemas c ON m.cinema_id = c.id
LEFT JOIN viewers v ON m.id = v.movie_id
LEFT JOIN persons p ON v.person_id = p.id
WHERE 1=1
";
$bind = [];
if (!empty($params['title'])) {
$sql .= " AND m.title LIKE :title";
$bind[':title'] = '%' . $params['title'] . '%';
}
if (!empty($params['cinema_id']) && is_numeric($params['cinema_id'])) {
$sql .= " AND m.cinema_id = :cinema_id";
$bind[':cinema_id'] = (int)$params['cinema_id'];
}
if (!empty($params['person_id']) && is_numeric($params['person_id'])) {
$sql .= " AND v.person_id = :person_id";
$bind[':person_id'] = (int)$params['person_id'];
}
if (!empty($params['date_from'])) {
$sql .= " AND m.watch_date >= :date_from";
$bind[':date_from'] = $params['date_from'];
}
if (!empty($params['date_to'])) {
$sql .= " AND m.watch_date <= :date_to";
$bind[':date_to'] = $params['date_to'];
}
$sql .= " ORDER BY m.watch_date DESC, m.created_at DESC";
$stmt = $db->prepare($sql);
$stmt->execute($bind);
$movies = $stmt->fetchAll();
attachViewersToMovies($movies);
return $movies;
}
// ===== 表单提交时的关联数据处理 =====
/**
* 解析表单提交的电影院选择。
* 选择「新建」且填了名称时创建影院并返回新 id;未选择或信息不足时返回 null。
*/
function resolveCinemaId($cinema_id, $new_place, $new_name, $new_address) {
$db = getDB();
if ($cinema_id === 'new' && !empty($new_name)) {
$stmt = $db->prepare("INSERT INTO cinemas (place, name, address) VALUES (?, ?, ?)");
$stmt->execute([$new_place, $new_name, $new_address]);
$cinema_id = $db->lastInsertId();
} elseif ($cinema_id === 'new') {
$cinema_id = null;
} elseif (!empty($cinema_id) && is_numeric($cinema_id)) {
$cinema_id = (int)$cinema_id;
} else {
$cinema_id = null;
}
return $cinema_id;
}
/**
* 解析「一起看的人」表单行:已有观影人取 id,选择「新增」则按姓名建库或复用。
* 返回去重后的观影人 id 列表。
*/
function resolvePersonIds($viewer_selects, $viewer_news) {
$db = getDB();
$person_ids = [];
foreach ($viewer_selects as $idx => $val) {
if ($val === 'new') {
$new_name = trim($viewer_news[$idx] ?? '');
if (!empty($new_name)) {
$chk = $db->prepare("SELECT id FROM persons WHERE name = ?");
$chk->execute([$new_name]);
$existing = $chk->fetch();
if ($existing) {
$person_ids[] = $existing['id'];
} else {
$ins = $db->prepare("INSERT INTO persons (name) VALUES (?)");
$ins->execute([$new_name]);
$person_ids[] = $db->lastInsertId();
}
}
} elseif (is_numeric($val) && $val > 0) {
$person_ids[] = (int)$val;
}
}
return array_unique($person_ids);
}
// 把观影人写入电影关联表
function linkViewers($movie_id, $person_ids) {
if (empty($person_ids)) return;
$db = getDB();
$stmt = $db->prepare("INSERT INTO viewers (movie_id, person_id, name) VALUES (?, ?, ?)");
foreach ($person_ids as $pid) {
$stmt->execute([$movie_id, $pid, '']);
}
}
+329
View File
@@ -0,0 +1,329 @@
<?php
// ============================================
// 文件: lib/stats.php
// 说明: 统计仪表板与年度报告的查询逻辑
// ============================================
// ===== 统计 =====
function getStatsOverview() {
$db = getDB();
$stats = [];
$stmt = $db->query("SELECT COUNT(*) FROM movies");
$stats['total_movies'] = $stmt->fetchColumn();
$stmt = $db->query("SELECT COUNT(*) FROM cinemas");
$stats['total_cinemas'] = $stmt->fetchColumn();
$stmt = $db->query("SELECT COUNT(*) FROM persons");
$stats['total_persons'] = $stmt->fetchColumn();
$stmt = $db->query("SELECT COUNT(*) FROM viewers");
$stats['total_viewership'] = $stmt->fetchColumn();
$stmt = $db->query("SELECT MIN(watch_date) as first, MAX(watch_date) as last FROM movies WHERE watch_date IS NOT NULL");
$row = $stmt->fetch();
$stats['first_date'] = $row['first'];
$stats['last_date'] = $row['last'];
return $stats;
}
function statsByCinema() {
$db = getDB();
$stmt = $db->query("
SELECT c.id, c.name, c.color, COUNT(m.id) as count
FROM cinemas c
LEFT JOIN movies m ON c.id = m.cinema_id
GROUP BY c.id
ORDER BY count DESC
");
return $stmt->fetchAll();
}
function statsByPerson() {
$db = getDB();
$stmt = $db->query("
SELECT p.id, p.name, p.color, p.avatar, COUNT(v.movie_id) as count
FROM persons p
LEFT JOIN viewers v ON p.id = v.person_id
GROUP BY p.id
ORDER BY count DESC
");
return $stmt->fetchAll();
}
function statsByMonth($limit = 12) {
$db = getDB();
$stmt = $db->prepare("
SELECT strftime('%Y-%m', watch_date) as month, COUNT(*) as count
FROM movies
WHERE watch_date IS NOT NULL
GROUP BY month
ORDER BY month DESC
LIMIT ?
");
$stmt->execute([$limit]);
return $stmt->fetchAll();
}
function statsByYear() {
$db = getDB();
$stmt = $db->query("
SELECT strftime('%Y', watch_date) as year, COUNT(*) as count
FROM movies
WHERE watch_date IS NOT NULL
GROUP BY year
ORDER BY year DESC
");
return $stmt->fetchAll();
}
// ===== 年度报告 =====
function getAnnualStats($year) {
$db = getDB();
$stats = [];
// 获取当前管理员
$admin = getCurrentAdmin();
$self_person_id = $admin['person_id'] ?? null;
$stats['self_person_name'] = $admin['person_name'] ?? null;
$stats['self_person_avatar'] = $admin['person_avatar'] ?? null;
if (!$self_person_id) {
// 未关联观影人,返回空数据
$stats['total'] = 0;
$stats['days'] = 0;
$stats['top_cinema'] = '无';
$stats['top_cinema_count'] = 0;
$stats['top_person'] = '无';
$stats['top_person_count'] = 0;
$stats['first_date'] = null;
$stats['last_date'] = null;
$stats['most_viewers_movie'] = '无';
$stats['most_viewers_count'] = 0;
$stats['month_distribution'] = array_fill(1, 12, 0);
$stats['weekday_distribution'] = ['日'=>0,'一'=>0,'二'=>0,'三'=>0,'四'=>0,'五'=>0,'六'=>0];
$stats['avg_per_month'] = 0;
$stats['avg_days_between'] = 0;
$stats['keywords'] = [];
$stats['recent_movies'] = [];
$stats['my_movies'] = [];
$stats['common_viewers'] = [];
return $stats;
}
// 1. 总观影数(当前用户参与的电影)
$stmt = $db->prepare("
SELECT COUNT(DISTINCT m.id)
FROM movies m
JOIN viewers v ON m.id = v.movie_id
WHERE strftime('%Y', m.watch_date) = ?
AND v.person_id = ?
");
$stmt->execute([$year, $self_person_id]);
$stats['total'] = (int)$stmt->fetchColumn();
// 2. 观影天数
$stmt = $db->prepare("
SELECT COUNT(DISTINCT m.watch_date)
FROM movies m
JOIN viewers v ON m.id = v.movie_id
WHERE strftime('%Y', m.watch_date) = ?
AND v.person_id = ?
AND m.watch_date IS NOT NULL
");
$stmt->execute([$year, $self_person_id]);
$stats['days'] = (int)$stmt->fetchColumn();
// 3. 最常去的影院
$stmt = $db->prepare("
SELECT c.name, COUNT(m.id) as count
FROM movies m
JOIN viewers v ON m.id = v.movie_id
LEFT JOIN cinemas c ON m.cinema_id = c.id
WHERE strftime('%Y', m.watch_date) = ?
AND v.person_id = ?
AND m.cinema_id IS NOT NULL
GROUP BY m.cinema_id
ORDER BY count DESC LIMIT 1
");
$stmt->execute([$year, $self_person_id]);
$top_cinema = $stmt->fetch();
$stats['top_cinema'] = $top_cinema ? $top_cinema['name'] : '无';
$stats['top_cinema_count'] = $top_cinema ? (int)$top_cinema['count'] : 0;
// 4. 最常一起观影的人(与当前用户同场最多的人)
// 先获取当前用户看过的所有电影ID
$stmt = $db->prepare("
SELECT DISTINCT m.id
FROM movies m
JOIN viewers v ON m.id = v.movie_id
WHERE strftime('%Y', m.watch_date) = ?
AND v.person_id = ?
");
$stmt->execute([$year, $self_person_id]);
$movie_ids = $stmt->fetchAll(PDO::FETCH_COLUMN);
// 共同观影人统计
$common_viewers = [];
if (!empty($movie_ids)) {
$placeholders = implode(',', array_fill(0, count($movie_ids), '?'));
$params = array_merge($movie_ids, [$self_person_id]);
$stmt = $db->prepare("
SELECT p.id, p.name, p.avatar, COUNT(*) as count
FROM viewers v
JOIN persons p ON v.person_id = p.id
WHERE v.movie_id IN ($placeholders)
AND v.person_id != ?
GROUP BY v.person_id
ORDER BY count DESC
");
$stmt->execute($params);
$common_viewers = $stmt->fetchAll();
}
$stats['common_viewers'] = $common_viewers;
// 最常一起观影的人(取第一个)
$stats['top_person'] = !empty($common_viewers) ? $common_viewers[0]['name'] : '无';
$stats['top_person_count'] = !empty($common_viewers) ? (int)$common_viewers[0]['count'] : 0;
// 5. 最早和最晚日期
$stmt = $db->prepare("
SELECT MIN(m.watch_date) as first, MAX(m.watch_date) as last
FROM movies m
JOIN viewers v ON m.id = v.movie_id
WHERE strftime('%Y', m.watch_date) = ?
AND v.person_id = ?
AND m.watch_date IS NOT NULL
");
$stmt->execute([$year, $self_person_id]);
$row = $stmt->fetch();
$stats['first_date'] = $row['first'];
$stats['last_date'] = $row['last'];
// 6. 单次最多人的电影(当前用户参与的电影中)
$stmt = $db->prepare("
SELECT m.id, m.title, COUNT(v.id) as viewer_count
FROM movies m
JOIN viewers v ON m.id = v.movie_id
WHERE strftime('%Y', m.watch_date) = ?
AND m.id IN (SELECT movie_id FROM viewers WHERE person_id = ?)
GROUP BY m.id
ORDER BY viewer_count DESC LIMIT 1
");
$stmt->execute([$year, $self_person_id]);
$most_viewers = $stmt->fetch();
$stats['most_viewers_movie'] = $most_viewers ? $most_viewers['title'] : '无';
$stats['most_viewers_count'] = $most_viewers ? (int)$most_viewers['viewer_count'] : 0;
// 7. 月份分布
$stmt = $db->prepare("
SELECT strftime('%m', m.watch_date) as month, COUNT(*) as count
FROM movies m
JOIN viewers v ON m.id = v.movie_id
WHERE strftime('%Y', m.watch_date) = ?
AND v.person_id = ?
AND m.watch_date IS NOT NULL
GROUP BY month
ORDER BY month
");
$stmt->execute([$year, $self_person_id]);
$month_data = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
$stats['month_distribution'] = [];
for ($m=1; $m<=12; $m++) {
$key = str_pad($m, 2, '0', STR_PAD_LEFT);
$stats['month_distribution'][$m] = isset($month_data[$key]) ? (int)$month_data[$key] : 0;
}
// 8. 星期分布
$stmt = $db->prepare("
SELECT strftime('%w', m.watch_date) as wday, COUNT(*) as count
FROM movies m
JOIN viewers v ON m.id = v.movie_id
WHERE strftime('%Y', m.watch_date) = ?
AND v.person_id = ?
AND m.watch_date IS NOT NULL
GROUP BY wday
ORDER BY wday
");
$stmt->execute([$year, $self_person_id]);
$wday_data = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
$weekdays = ['日','一','二','三','四','五','六'];
$stats['weekday_distribution'] = [];
foreach ($weekdays as $idx => $name) {
$stats['weekday_distribution'][$name] = isset($wday_data[$idx]) ? (int)$wday_data[$idx] : 0;
}
// 9. 月均和频率
$stats['avg_per_month'] = $stats['total'] > 0 ? round($stats['total'] / 12, 1) : 0;
$stats['avg_days_between'] = $stats['total'] > 0 ? round(365 / $stats['total'], 1) : 0;
// 10. 关键词(基于当前用户电影的备注)
$stmt = $db->prepare("
SELECT m.remark
FROM movies m
JOIN viewers v ON m.id = v.movie_id
WHERE strftime('%Y', m.watch_date) = ?
AND v.person_id = ?
AND m.remark IS NOT NULL
AND m.remark != ''
");
$stmt->execute([$year, $self_person_id]);
$remarks = $stmt->fetchAll(PDO::FETCH_COLUMN);
$word_count = [];
foreach ($remarks as $text) {
$words = preg_split('/[\s,。.!?、;;:]+/', $text, -1, PREG_SPLIT_NO_EMPTY);
foreach ($words as $w) {
$w = trim($w);
if (mb_strlen($w) >= 2) {
if (!isset($word_count[$w])) $word_count[$w] = 0;
$word_count[$w]++;
}
}
}
arsort($word_count);
$stats['keywords'] = array_slice(array_keys($word_count), 0, 5);
// 11. 最近5部(当前用户参与的)
$stmt = $db->prepare("
SELECT DISTINCT m.title
FROM movies m
JOIN viewers v ON m.id = v.movie_id
WHERE strftime('%Y', m.watch_date) = ?
AND v.person_id = ?
ORDER BY m.watch_date DESC
LIMIT 5
");
$stmt->execute([$year, $self_person_id]);
$stats['recent_movies'] = $stmt->fetchAll(PDO::FETCH_COLUMN);
// 12. 我的观影记录(含海报)
$stmt = $db->prepare("
SELECT m.id, m.title, m.watch_date, m.poster_path
FROM movies m
JOIN viewers v ON m.id = v.movie_id
WHERE strftime('%Y', m.watch_date) = ?
AND v.person_id = ?
GROUP BY m.id
ORDER BY m.watch_date DESC
");
$stmt->execute([$year, $self_person_id]);
$movie_list = $stmt->fetchAll();
// 对每个电影查询一起观影的人(含头像)
foreach ($movie_list as &$movie) {
$stmt2 = $db->prepare("
SELECT p.id, p.name, p.avatar
FROM viewers v
LEFT JOIN persons p ON v.person_id = p.id
WHERE v.movie_id = ?
AND v.person_id != ?
");
$stmt2->execute([$movie['id'], $self_person_id]);
$movie['viewers'] = $stmt2->fetchAll();
}
$stats['my_movies'] = $movie_list;
return $stats;
}
function getYearsWithData() {
$db = getDB();
$stmt = $db->query("SELECT DISTINCT strftime('%Y', watch_date) as year FROM movies WHERE watch_date IS NOT NULL ORDER BY year DESC");
return $stmt->fetchAll(PDO::FETCH_COLUMN);
}