Compare commits
1
Commits
40e1bc5e56
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd957d45bd |
@@ -68,7 +68,13 @@
|
||||
|
||||
```
|
||||
MovieLog_Server/
|
||||
├── config.php # 核心:常量、DB 连接、建表与迁移、公共函数、统计与年度报告查询
|
||||
├── config.php # 引导入口:错误报告、会话、路径常量、上传目录,并加载 lib/ 各模块
|
||||
├── lib/ # 功能模块(被 config.php 统一引入,页面无需单独 require)
|
||||
│ ├── db.php # DB 连接、建表、字段与历史数据迁移、默认管理员
|
||||
│ ├── helpers.php # 输出转义、文件名、颜色校验、上传/删除文件、图片 URL
|
||||
│ ├── auth.php # 登录校验、管理员账号增删改
|
||||
│ ├── movies.php # 电影/影院读取、搜索,以及表单关联数据的解析
|
||||
│ └── stats.php # 统计仪表板与年度报告查询
|
||||
├── index.php # 首页:观影列表(瀑布流)+ 快捷搜索
|
||||
├── add.php # 添加电影
|
||||
├── edit.php # 编辑电影
|
||||
@@ -88,7 +94,7 @@ MovieLog_Server/
|
||||
├── fetch_movie.php # OMDb 代理接口,返回 JSON
|
||||
├── test_crop.php # 开发调试页:验证 Base64 图片落盘是否正常
|
||||
├── css/style.css # 全局样式
|
||||
├── js/main.js # 少量公共脚本
|
||||
├── js/main.js # 公共脚本(含各页面共用的表单联动函数)
|
||||
├── assets/.gitkeep # 静态占位资源目录
|
||||
├── db/movies.db # SQLite 数据库(运行时自动生成,不纳入版本控制)
|
||||
└── uploads/ # 上传图片(运行时自动生成,不纳入版本控制)
|
||||
@@ -98,6 +104,8 @@ MovieLog_Server/
|
||||
└── avatars/ # 观影人头像
|
||||
```
|
||||
|
||||
> 页面只需 `require_once 'config.php'`,`lib/` 下的模块会由引导文件统一加载,并保证依赖顺序(db → helpers → auth → movies → stats)。
|
||||
|
||||
---
|
||||
|
||||
## 数据库结构
|
||||
|
||||
@@ -27,42 +27,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
// 票根裁剪数据
|
||||
$ticket_crop = $_POST['ticket_crop'] ?? '';
|
||||
|
||||
// 新建电影院
|
||||
$new_cinema_place = trim($_POST['new_cinema_place'] ?? '');
|
||||
$new_cinema_name = trim($_POST['new_cinema_name'] ?? '');
|
||||
$new_cinema_address = trim($_POST['new_cinema_address'] ?? '');
|
||||
|
||||
if ($cinema_id === 'new' && !empty($new_cinema_name)) {
|
||||
$stmt = $db->prepare("INSERT INTO cinemas (place, name, address) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$new_cinema_place, $new_cinema_name, $new_cinema_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;
|
||||
}
|
||||
// 电影院:选择已有影院、新建影院或留空
|
||||
$cinema_id = resolveCinemaId(
|
||||
$cinema_id,
|
||||
trim($_POST['new_cinema_place'] ?? ''),
|
||||
trim($_POST['new_cinema_name'] ?? ''),
|
||||
trim($_POST['new_cinema_address'] ?? '')
|
||||
);
|
||||
|
||||
if (empty($title)) {
|
||||
$error = '请输入电影名称';
|
||||
} elseif (empty($watch_date)) {
|
||||
$error = '请选择观影日期';
|
||||
} else {
|
||||
// ----- 处理海报(直接上传) -----
|
||||
$poster_path = '';
|
||||
if (isset($_FILES['poster']) && $_FILES['poster']['error'] === UPLOAD_ERR_OK) {
|
||||
$ext = strtolower(pathinfo($_FILES['poster']['name'], PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||
$filename = uniqueFilename($_FILES['poster']['name']);
|
||||
$target = POSTER_DIR . $filename;
|
||||
if (move_uploaded_file($_FILES['poster']['tmp_name'], $target)) {
|
||||
$poster_path = 'uploads/posters/' . $filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ----- 海报:直接上传 -----
|
||||
$poster_path = uploadImageFile('poster', POSTER_DIR, 'uploads/posters/') ?? '';
|
||||
|
||||
// ----- 处理票根(裁剪优先) -----
|
||||
// ----- 票根:裁剪结果优先,其次原始上传 -----
|
||||
$ticket_path = '';
|
||||
if (!empty($ticket_crop)) {
|
||||
$new_path = saveBase64Image($ticket_crop, TICKET_DIR, 'ticket');
|
||||
@@ -71,18 +52,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
} else {
|
||||
$error = '票根裁剪保存失败,请重试';
|
||||
}
|
||||
} elseif (isset($_FILES['ticket']) && $_FILES['ticket']['error'] === UPLOAD_ERR_OK) {
|
||||
$ext = strtolower(pathinfo($_FILES['ticket']['name'], PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||
$filename = uniqueFilename($_FILES['ticket']['name']);
|
||||
$target = TICKET_DIR . $filename;
|
||||
if (move_uploaded_file($_FILES['ticket']['tmp_name'], $target)) {
|
||||
$ticket_path = 'uploads/tickets/' . $filename;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$ticket_path = uploadImageFile('ticket', TICKET_DIR, 'uploads/tickets/') ?? '';
|
||||
}
|
||||
|
||||
// 如果没有错误,继续
|
||||
// 没有错误才继续
|
||||
if (empty($error)) {
|
||||
// 插入电影
|
||||
$stmt = $db->prepare("
|
||||
@@ -92,59 +66,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$stmt->execute([$title, $info, $poster_path, $ticket_path, $cinema_id, $watch_date]);
|
||||
$movie_id = $db->lastInsertId();
|
||||
|
||||
// ---- 处理合影照片(多张) ----
|
||||
if (isset($_FILES['photos']) && is_array($_FILES['photos']['tmp_name'])) {
|
||||
$photo_files = $_FILES['photos'];
|
||||
for ($i = 0; $i < count($photo_files['tmp_name']); $i++) {
|
||||
if ($photo_files['error'][$i] === UPLOAD_ERR_OK) {
|
||||
$ext = strtolower(pathinfo($photo_files['name'][$i], PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||
$filename = uniqueFilename($photo_files['name'][$i]);
|
||||
$target = PHOTO_DIR . $filename;
|
||||
if (move_uploaded_file($photo_files['tmp_name'][$i], $target)) {
|
||||
$photo_path = 'uploads/photos/' . $filename;
|
||||
$stmt2 = $db->prepare("INSERT INTO movie_photos (movie_id, photo_path) VALUES (?, ?)");
|
||||
$stmt2->execute([$movie_id, $photo_path]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ---- 观影照片(多张)----
|
||||
uploadMultiplePhotos('photos', $movie_id);
|
||||
|
||||
// ---- 处理观影人 ----
|
||||
$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;
|
||||
}
|
||||
}
|
||||
$person_ids = array_unique($person_ids);
|
||||
if (!empty($person_ids)) {
|
||||
$stmt = $db->prepare("INSERT INTO viewers (movie_id, person_id, name) VALUES (?, ?, ?)");
|
||||
foreach ($person_ids as $pid) {
|
||||
$stmt->execute([$movie_id, $pid, '']);
|
||||
}
|
||||
}
|
||||
// ---- 一起看的人 ----
|
||||
linkViewers($movie_id, resolvePersonIds($viewer_selects, $viewer_news));
|
||||
|
||||
header('Location: detail.php?id=' . $movie_id);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
@@ -470,7 +403,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
</select>
|
||||
<input type="text" name="viewers_new[]" placeholder="输入新姓名" style="flex:1;display:<?= ($val === 'new') ? 'block' : 'none' ?>;padding:8px;border:1px solid #ddd;border-radius:6px;" value="<?= h($new_name) ?>">
|
||||
<?php if ($idx > 0): ?>
|
||||
<button type="button" class="btn-remove-viewer" onclick="removeViewer(this)">×</button>
|
||||
<button type="button" class="btn-remove-viewer" onclick="removeViewerRow(this)">×</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
@@ -533,22 +466,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/main.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
|
||||
<script>
|
||||
// ---------- 观影人 ----------
|
||||
function toggleNewViewer(select) {
|
||||
var parent = select.parentElement;
|
||||
var input = parent.querySelector('input[name="viewers_new[]"]');
|
||||
if (select.value === 'new') {
|
||||
input.style.display = 'block';
|
||||
input.required = true;
|
||||
} else {
|
||||
input.style.display = 'none';
|
||||
input.required = false;
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function addViewerRow() {
|
||||
var container = document.getElementById('viewerContainer');
|
||||
var div = document.createElement('div');
|
||||
@@ -561,25 +482,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
selectHtml += '<option value="new">➕ 新增</option>';
|
||||
selectHtml += '</select>';
|
||||
var inputHtml = '<input type="text" name="viewers_new[]" placeholder="输入新姓名" style="flex:1;display:none;padding:8px;border:1px solid #ddd;border-radius:6px;">';
|
||||
var removeBtn = '<button type="button" class="btn-remove-viewer" onclick="removeViewer(this)">×</button>';
|
||||
var removeBtn = '<button type="button" class="btn-remove-viewer" onclick="removeViewerRow(this)">×</button>';
|
||||
div.innerHTML = selectHtml + inputHtml + removeBtn;
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function removeViewer(btn) {
|
||||
var container = document.getElementById('viewerContainer');
|
||||
if (container.children.length > 1) {
|
||||
btn.parentElement.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 电影院 ----------
|
||||
function toggleNewCinema() {
|
||||
var select = document.getElementById('cinemaSelect');
|
||||
var group = document.getElementById('newCinemaGroup');
|
||||
group.style.display = select.value === 'new' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
toggleNewCinema();
|
||||
document.querySelectorAll('.viewer-input-group').forEach(function(group) {
|
||||
|
||||
+11
-34
@@ -17,11 +17,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
|
||||
$place = trim($_POST['place'] ?? '');
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$address = trim($_POST['address'] ?? '');
|
||||
$color = trim($_POST['color'] ?? '#888888');
|
||||
// 校验颜色格式
|
||||
if (!preg_match('/^#[a-fA-F0-9]{6}$/', $color)) {
|
||||
$color = '#888888';
|
||||
}
|
||||
$color = normalizeHexColor($_POST['color'] ?? '#888888');
|
||||
|
||||
if (empty($name)) {
|
||||
$error = '影院名称不能为空';
|
||||
@@ -38,10 +34,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
|
||||
$place = trim($_POST['place'] ?? '');
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$address = trim($_POST['address'] ?? '');
|
||||
$color = trim($_POST['color'] ?? '#888888');
|
||||
if (!preg_match('/^#[a-fA-F0-9]{6}$/', $color)) {
|
||||
$color = '#888888';
|
||||
}
|
||||
$color = normalizeHexColor($_POST['color'] ?? '#888888');
|
||||
|
||||
if (empty($name)) {
|
||||
$error = '影院名称不能为空';
|
||||
@@ -93,33 +86,17 @@ if (isset($_GET['action']) && $_GET['action'] === 'edit' && isset($_GET['id']))
|
||||
|
||||
// ---------- 预设 50 色 ----------
|
||||
$preset_colors = [
|
||||
// 红色系 (5)
|
||||
'#e74c3c', '#c0392b', '#e67e22', '#d35400', '#f39c12',
|
||||
// 橙色系 (3)
|
||||
'#f1c40f', '#f7dc6f', '#f5b041',
|
||||
// 黄色系 (3)
|
||||
'#f4d03f', '#f9e79f', '#f8c471',
|
||||
// 绿色系 (5)
|
||||
'#2ecc71', '#27ae60', '#1abc9c', '#16a085', '#58d68d',
|
||||
// 青色系 (4)
|
||||
'#48c9b0', '#45b39d', '#82e0aa', '#a3e4d7',
|
||||
// 蓝色系 (7)
|
||||
'#3498db', '#2980b9', '#2e86c1', '#5dade2', '#85c1e9', '#5499c7', '#aed6f1',
|
||||
// 紫色系 (5)
|
||||
'#9b59b6', '#8e44ad', '#af7ac5', '#bb8fce', '#d2b4de',
|
||||
// 粉色系 (4)
|
||||
'#e84393', '#fd79a8', '#e91e63', '#f06292',
|
||||
// 灰色系 (6)
|
||||
'#95a5a6', '#7f8c8d', '#bdc3c7', '#d5dbdb', '#aab7b8', '#99a3a4',
|
||||
// 棕色/米色系 (4)
|
||||
'#a67c52', '#8d6e63', '#d4a574', '#c9a87c',
|
||||
// 其他 (4)
|
||||
'#00b894', '#0984e3', '#6c5ce7', '#fd79a8'
|
||||
'#f1c40f', '#f7dc6f', '#f5b041', '#f4d03f', '#f9e79f',
|
||||
'#f8c471', '#2ecc71', '#27ae60', '#1abc9c', '#16a085',
|
||||
'#58d68d', '#48c9b0', '#45b39d', '#82e0aa', '#a3e4d7',
|
||||
'#3498db', '#2980b9', '#2e86c1', '#5dade2', '#85c1e9',
|
||||
'#5499c7', '#aed6f1', '#9b59b6', '#8e44ad', '#af7ac5',
|
||||
'#bb8fce', '#d2b4de', '#e84393', '#fd79a8', '#e91e63',
|
||||
'#f06292', '#95a5a6', '#7f8c8d', '#bdc3c7', '#d5dbdb',
|
||||
'#aab7b8', '#99a3a4', '#a67c52', '#8d6e63', '#d4a574',
|
||||
'#c9a87c', '#00b894', '#0984e3', '#6c5ce7', '#fd79a8',
|
||||
];
|
||||
// 只取前50个,如果有重复或不足可调整,但这里正好约50个(实际46个,再补几个)
|
||||
$extra = ['#2d3436', '#636e72', '#b2bec3', '#dfe6e9', '#fab1a0', '#e17055', '#00cec9', '#a29bfe'];
|
||||
$preset_colors = array_merge($preset_colors, $extra);
|
||||
$preset_colors = array_slice($preset_colors, 0, 50); // 确保正好50个
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
+15
-713
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
// ============================================
|
||||
// 文件: config.php (完整版)
|
||||
// 说明: 包含所有核心函数,年度报告支持海报和共同观影人
|
||||
// 文件: config.php
|
||||
// 说明: 应用引导入口。负责错误报告、会话、路径常量、上传目录,
|
||||
// 并加载 lib/ 下的各功能模块,最后初始化数据库。
|
||||
// 所有页面统一通过 require_once 'config.php' 引入本文件。
|
||||
// ============================================
|
||||
|
||||
error_reporting(E_ALL);
|
||||
@@ -9,11 +11,14 @@ ini_set('display_errors', 1);
|
||||
|
||||
session_start();
|
||||
|
||||
// 项目根目录(lib/ 下的模块统一用它拼接绝对路径)
|
||||
define('ROOT_DIR', __DIR__);
|
||||
|
||||
// 数据库路径
|
||||
define('DB_PATH', __DIR__ . '/db/movies.db');
|
||||
define('DB_PATH', ROOT_DIR . '/db/movies.db');
|
||||
|
||||
// 上传目录
|
||||
define('UPLOAD_DIR', __DIR__ . '/uploads/');
|
||||
define('UPLOAD_DIR', ROOT_DIR . '/uploads/');
|
||||
define('POSTER_DIR', UPLOAD_DIR . 'posters/');
|
||||
define('TICKET_DIR', UPLOAD_DIR . 'tickets/');
|
||||
define('PHOTO_DIR', UPLOAD_DIR . 'photos/');
|
||||
@@ -26,715 +31,12 @@ foreach ([POSTER_DIR, TICKET_DIR, PHOTO_DIR, AVATAR_DIR] as $dir) {
|
||||
}
|
||||
}
|
||||
|
||||
// 数据库连接
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
// 登录检查
|
||||
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("<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(__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);
|
||||
}
|
||||
// ===== 功能模块 =====
|
||||
require_once ROOT_DIR . '/lib/db.php'; // 数据库连接与初始化
|
||||
require_once ROOT_DIR . '/lib/helpers.php'; // 通用工具函数
|
||||
require_once ROOT_DIR . '/lib/auth.php'; // 登录校验与账号管理
|
||||
require_once ROOT_DIR . '/lib/movies.php'; // 电影 / 影院 / 搜索
|
||||
require_once ROOT_DIR . '/lib/stats.php'; // 统计与年度报告
|
||||
|
||||
// 初始化数据库
|
||||
initDB();
|
||||
+2
-6
@@ -18,9 +18,7 @@ $movie = $stmt->fetch();
|
||||
if ($movie) {
|
||||
// 删除海报、票根、旧单张照片
|
||||
foreach (['poster_path', 'ticket_path', 'photo_path'] as $field) {
|
||||
if (!empty($movie[$field]) && file_exists(__DIR__ . '/' . $movie[$field])) {
|
||||
unlink(__DIR__ . '/' . $movie[$field]);
|
||||
}
|
||||
deleteStoredFile($movie[$field]);
|
||||
}
|
||||
|
||||
// 删除多张照片(新表)
|
||||
@@ -28,9 +26,7 @@ if ($movie) {
|
||||
$stmt2->execute([$id]);
|
||||
$photos = $stmt2->fetchAll();
|
||||
foreach ($photos as $p) {
|
||||
if (!empty($p['photo_path']) && file_exists(__DIR__ . '/' . $p['photo_path'])) {
|
||||
unlink(__DIR__ . '/' . $p['photo_path']);
|
||||
}
|
||||
deleteStoredFile($p['photo_path']);
|
||||
}
|
||||
|
||||
// 删除电影(viewers 和 movie_photos 会自动级联删除)
|
||||
|
||||
@@ -41,82 +41,56 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$ticket_crop = $_POST['ticket_crop'] ?? '';
|
||||
$photo_crops = $_POST['photo_crop'] ?? [];
|
||||
|
||||
// 新建电影院
|
||||
$new_cinema_place = trim($_POST['new_cinema_place'] ?? '');
|
||||
$new_cinema_name = trim($_POST['new_cinema_name'] ?? '');
|
||||
$new_cinema_address = trim($_POST['new_cinema_address'] ?? '');
|
||||
|
||||
if ($cinema_id === 'new' && !empty($new_cinema_name)) {
|
||||
$stmt = $db->prepare("INSERT INTO cinemas (place, name, address) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$new_cinema_place, $new_cinema_name, $new_cinema_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;
|
||||
}
|
||||
// 电影院:选择已有影院、新建影院或留空
|
||||
$cinema_id = resolveCinemaId(
|
||||
$cinema_id,
|
||||
trim($_POST['new_cinema_place'] ?? ''),
|
||||
trim($_POST['new_cinema_name'] ?? ''),
|
||||
trim($_POST['new_cinema_address'] ?? '')
|
||||
);
|
||||
|
||||
if (empty($title)) {
|
||||
$error = '请输入电影名称';
|
||||
} elseif (empty($watch_date)) {
|
||||
$error = '请选择观影日期';
|
||||
} else {
|
||||
// ----- 处理海报裁剪/上传 -----
|
||||
// ----- 海报:裁剪结果优先,其次上传新文件 -----
|
||||
$poster_path = $movie['poster_path'];
|
||||
if (!empty($poster_crop)) {
|
||||
$new_path = saveBase64Image($poster_crop, POSTER_DIR, 'poster');
|
||||
if ($new_path) {
|
||||
if ($poster_path && file_exists(__DIR__ . '/' . $poster_path)) {
|
||||
unlink(__DIR__ . '/' . $poster_path);
|
||||
}
|
||||
deleteStoredFile($poster_path);
|
||||
$poster_path = $new_path;
|
||||
} else {
|
||||
$error = '海报裁剪保存失败,请重试';
|
||||
}
|
||||
} elseif (isset($_FILES['poster']) && $_FILES['poster']['error'] === UPLOAD_ERR_OK) {
|
||||
// 上传新海报替换
|
||||
$ext = strtolower(pathinfo($_FILES['poster']['name'], PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||
$filename = uniqueFilename($_FILES['poster']['name']);
|
||||
$target = POSTER_DIR . $filename;
|
||||
if (move_uploaded_file($_FILES['poster']['tmp_name'], $target)) {
|
||||
if ($poster_path && file_exists(__DIR__ . '/' . $poster_path)) {
|
||||
unlink(__DIR__ . '/' . $poster_path);
|
||||
}
|
||||
$poster_path = 'uploads/posters/' . $filename;
|
||||
}
|
||||
} else {
|
||||
$uploaded = uploadImageFile('poster', POSTER_DIR, 'uploads/posters/');
|
||||
if ($uploaded) {
|
||||
deleteStoredFile($poster_path);
|
||||
$poster_path = $uploaded;
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 处理票根裁剪/上传 -----
|
||||
// ----- 票根:裁剪结果优先,其次上传新文件 -----
|
||||
$ticket_path = $movie['ticket_path'];
|
||||
if (!empty($ticket_crop)) {
|
||||
$new_path = saveBase64Image($ticket_crop, TICKET_DIR, 'ticket');
|
||||
if ($new_path) {
|
||||
if ($ticket_path && file_exists(__DIR__ . '/' . $ticket_path)) {
|
||||
unlink(__DIR__ . '/' . $ticket_path);
|
||||
}
|
||||
deleteStoredFile($ticket_path);
|
||||
$ticket_path = $new_path;
|
||||
} else {
|
||||
$error = '票根裁剪保存失败,请重试';
|
||||
}
|
||||
} elseif (isset($_FILES['ticket']) && $_FILES['ticket']['error'] === UPLOAD_ERR_OK) {
|
||||
$ext = strtolower(pathinfo($_FILES['ticket']['name'], PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||
$filename = uniqueFilename($_FILES['ticket']['name']);
|
||||
$target = TICKET_DIR . $filename;
|
||||
if (move_uploaded_file($_FILES['ticket']['tmp_name'], $target)) {
|
||||
if ($ticket_path && file_exists(__DIR__ . '/' . $ticket_path)) {
|
||||
unlink(__DIR__ . '/' . $ticket_path);
|
||||
}
|
||||
$ticket_path = 'uploads/tickets/' . $filename;
|
||||
}
|
||||
} else {
|
||||
$uploaded = uploadImageFile('ticket', TICKET_DIR, 'uploads/tickets/');
|
||||
if ($uploaded) {
|
||||
deleteStoredFile($ticket_path);
|
||||
$ticket_path = $uploaded;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果前面处理没有错误,继续更新数据库
|
||||
// 前面处理没有错误,才更新数据库
|
||||
if (empty($error)) {
|
||||
// 更新电影基本信息
|
||||
$stmt = $db->prepare("
|
||||
@@ -126,7 +100,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
");
|
||||
$stmt->execute([$title, $info, $poster_path, $ticket_path, $cinema_id, $watch_date, $id]);
|
||||
|
||||
// ----- 处理合影裁剪 -----
|
||||
// ----- 合影:替换已裁剪的图片 -----
|
||||
if (!empty($photo_crops)) {
|
||||
foreach ($photo_crops as $photo_id => $base64_data) {
|
||||
if (empty($base64_data)) continue;
|
||||
@@ -136,9 +110,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
if ($old) {
|
||||
$new_path = saveBase64Image($base64_data, PHOTO_DIR, 'photo');
|
||||
if ($new_path) {
|
||||
if ($old['photo_path'] && file_exists(__DIR__ . '/' . $old['photo_path'])) {
|
||||
unlink(__DIR__ . '/' . $old['photo_path']);
|
||||
}
|
||||
deleteStoredFile($old['photo_path']);
|
||||
$stmt = $db->prepare("UPDATE movie_photos SET photo_path = ? WHERE id = ?");
|
||||
$stmt->execute([$new_path, $photo_id]);
|
||||
}
|
||||
@@ -147,54 +119,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
// ----- 新增合影照片 -----
|
||||
if (isset($_FILES['new_photos']) && is_array($_FILES['new_photos']['tmp_name'])) {
|
||||
$photo_files = $_FILES['new_photos'];
|
||||
for ($i = 0; $i < count($photo_files['tmp_name']); $i++) {
|
||||
if ($photo_files['error'][$i] === UPLOAD_ERR_OK) {
|
||||
$ext = strtolower(pathinfo($photo_files['name'][$i], PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'])) {
|
||||
$filename = uniqueFilename($photo_files['name'][$i]);
|
||||
$target = PHOTO_DIR . $filename;
|
||||
if (move_uploaded_file($photo_files['tmp_name'][$i], $target)) {
|
||||
$photo_path = 'uploads/photos/' . $filename;
|
||||
$stmt = $db->prepare("INSERT INTO movie_photos (movie_id, photo_path) VALUES (?, ?)");
|
||||
$stmt->execute([$id, $photo_path]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
uploadMultiplePhotos('new_photos', $id);
|
||||
|
||||
// ----- 处理观影人 -----
|
||||
// ----- 一起看的人 -----
|
||||
$stmt = $db->prepare("DELETE FROM viewers WHERE movie_id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$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;
|
||||
}
|
||||
}
|
||||
$person_ids = array_unique($person_ids);
|
||||
if (!empty($person_ids)) {
|
||||
$stmt = $db->prepare("INSERT INTO viewers (movie_id, person_id, name) VALUES (?, ?, ?)");
|
||||
foreach ($person_ids as $pid) {
|
||||
$stmt->execute([$id, $pid, '']);
|
||||
}
|
||||
}
|
||||
linkViewers($id, resolvePersonIds($viewer_selects, $viewer_news));
|
||||
|
||||
header('Location: detail.php?id=' . $id);
|
||||
exit;
|
||||
@@ -480,6 +410,7 @@ if (empty($current_person_ids)) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/main.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
|
||||
<script>
|
||||
// ============================================================
|
||||
@@ -508,31 +439,11 @@ if (empty($current_person_ids)) {
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function toggleNewViewer(select) {
|
||||
var parent = select.parentElement;
|
||||
var input = parent.querySelector('input[name="viewers_new[]"]');
|
||||
if (select.value === 'new') {
|
||||
input.style.display = 'block';
|
||||
input.required = true;
|
||||
} else {
|
||||
input.style.display = 'none';
|
||||
input.required = false;
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function addViewerRow(value) {
|
||||
value = value || '';
|
||||
createViewerRow(value);
|
||||
}
|
||||
|
||||
function removeViewerRow(btn) {
|
||||
var container = document.getElementById('viewerContainer');
|
||||
if (container.children.length > 1) {
|
||||
btn.parentElement.remove();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var container = document.getElementById('viewerContainer');
|
||||
container.innerHTML = '';
|
||||
@@ -547,13 +458,6 @@ if (empty($current_person_ids)) {
|
||||
toggleNewCinema();
|
||||
});
|
||||
|
||||
// ---------- 电影院 ----------
|
||||
function toggleNewCinema() {
|
||||
var select = document.getElementById('cinemaSelect');
|
||||
var group = document.getElementById('newCinemaGroup');
|
||||
group.style.display = select.value === 'new' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// ---------- 裁剪 ----------
|
||||
let cropper = null;
|
||||
let currentTarget = '';
|
||||
|
||||
+39
@@ -1,3 +1,9 @@
|
||||
// ============================================
|
||||
// 文件: js/main.js
|
||||
// 说明: 全站公共脚本。add.php 与 edit.php 的「一起看的人 / 电影院」
|
||||
// 表单联动逻辑集中在此,避免两个页面各写一份。
|
||||
// ============================================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('🎬 电影日志系统已加载');
|
||||
});
|
||||
@@ -9,3 +15,36 @@ function formatDate(dateStr) {
|
||||
String(d.getMonth() + 1).padStart(2, '0') + '月' +
|
||||
String(d.getDate()).padStart(2, '0') + '日';
|
||||
}
|
||||
|
||||
// ---------- 一起看的人 ----------
|
||||
|
||||
// 下拉框切到「新增」时显示姓名输入框
|
||||
function toggleNewViewer(select) {
|
||||
var parent = select.parentElement;
|
||||
var input = parent.querySelector('input[name="viewers_new[]"]');
|
||||
if (select.value === 'new') {
|
||||
input.style.display = 'block';
|
||||
input.required = true;
|
||||
} else {
|
||||
input.style.display = 'none';
|
||||
input.required = false;
|
||||
input.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// 删除一行观影人(至少保留一行)
|
||||
function removeViewerRow(btn) {
|
||||
var container = document.getElementById('viewerContainer');
|
||||
if (container.children.length > 1) {
|
||||
btn.parentElement.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 电影院 ----------
|
||||
|
||||
// 下拉框切到「新建电影院」时显示新建表单
|
||||
function toggleNewCinema() {
|
||||
var select = document.getElementById('cinemaSelect');
|
||||
var group = document.getElementById('newCinemaGroup');
|
||||
group.style.display = select.value === 'new' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
+5
-9
@@ -15,8 +15,7 @@ $error = '';
|
||||
// ---------- 处理添加 ----------
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add') {
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$color = trim($_POST['color'] ?? '#888888');
|
||||
if (!preg_match('/^#[a-fA-F0-9]{6}$/', $color)) $color = '#888888';
|
||||
$color = normalizeHexColor($_POST['color'] ?? '#888888');
|
||||
|
||||
// 头像 base64
|
||||
$avatar_base64 = $_POST['avatar_base64'] ?? '';
|
||||
@@ -47,8 +46,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'edit') {
|
||||
$id = (int)$_POST['id'];
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$color = trim($_POST['color'] ?? '#888888');
|
||||
if (!preg_match('/^#[a-fA-F0-9]{6}$/', $color)) $color = '#888888';
|
||||
$color = normalizeHexColor($_POST['color'] ?? '#888888');
|
||||
|
||||
$avatar_base64 = $_POST['avatar_base64'] ?? '';
|
||||
|
||||
@@ -71,9 +69,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
|
||||
$avatar_path = $old_avatar;
|
||||
if (!empty($avatar_base64)) {
|
||||
// 删除旧头像
|
||||
if ($old_avatar && file_exists(__DIR__ . '/' . $old_avatar)) {
|
||||
unlink(__DIR__ . '/' . $old_avatar);
|
||||
}
|
||||
deleteStoredFile($old_avatar);
|
||||
$avatar_path = saveBase64Image($avatar_base64, AVATAR_DIR, 'avatar');
|
||||
}
|
||||
|
||||
@@ -97,8 +93,8 @@ if (isset($_GET['action']) && $_GET['action'] === 'delete' && isset($_GET['id'])
|
||||
$stmt = $db->prepare("SELECT avatar FROM persons WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
if ($row && $row['avatar'] && file_exists(__DIR__ . '/' . $row['avatar'])) {
|
||||
unlink(__DIR__ . '/' . $row['avatar']);
|
||||
if ($row) {
|
||||
deleteStoredFile($row['avatar']);
|
||||
}
|
||||
$stmt = $db->prepare("DELETE FROM persons WHERE id = ?");
|
||||
$stmt->execute([$id]);
|
||||
|
||||
Reference in New Issue
Block a user