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]); } } // 登录检查 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]); } // 生成唯一文件名 function uniqueFilename($original) { $ext = strtolower(pathinfo($original, PATHINFO_EXTENSION)); $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp']; if (!in_array($ext, $allowed)) { $ext = 'jpg'; } return date('YmdHis') . '_' . uniqid() . '.' . $ext; } // 安全输出 function h($str) { return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } // 海报 URL function posterUrl($path) { if ($path && file_exists(__DIR__ . '/' . $path)) { return '/' . $path; } return '/assets/placeholder-poster.jpg'; } // 图片 URL function imageUrl($path) { if ($path && file_exists(__DIR__ . '/' . $path)) { return '/' . $path; } return null; } // ===== 头像处理 ===== function getAvatarUrl($avatar_path, $name = '') { if ($avatar_path && file_exists(__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("{$letter}"); } 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(__DIR__ . '/', '', $filepath); } // ===== 电影列表 ===== 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; } // ===== 统计 ===== 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); } // 初始化数据库 initDB();