纯结构性重构,不改变任何功能。 - config.php 由 739 行缩减为 42 行,改为纯引导入口;新增 ROOT_DIR 常量 统一拼接绝对路径(lib/ 内模块的 __DIR__ 会指向 lib/,必须替换) - 新增 lib/ 五个模块 db.php / helpers.php / auth.php / movies.php / stats.php, 由 config.php 按 db → helpers → auth → movies → stats 顺序加载 - add.php、edit.php 各自约 80 行重复的 POST 处理改为调用 resolveCinemaId / resolvePersonIds / linkViewers / uploadImageFile / saveBase64Image / uploadMultiplePhotos / deleteStoredFile - cinemas.php、persons.php 中重复 4 次的颜色校验改用 normalizeHexColor() - toggleNewViewer / removeViewerRow / toggleNewCinema 下沉至 js/main.js, add.php 中的 removeViewer 统一更名为 removeViewerRow - 删除死代码:imageUrl()(全项目无调用);cinemas.php 配色数组中本就会被 array_slice 丢弃的 extra 颜色逻辑,简化为单条扁平 50 元素数组 - README 目录结构补充 lib/ 说明 验证方式:以便携 PHP 8.4 搭建 A/B 回归环境,播种固定数据后遍历 47 个 GET/POST 端点录制「状态码 + 响应体」。重构前后 42 个逐字节相同、2 个仅 上传文件名时间戳不同、3 个差异均为内联脚本下沉(已单独校验函数体逐字符 等价,剥离脚本后 HTML 标记完全一致)。24 个 PHP 文件语法检查全部通过。
132 lines
4.6 KiB
PHP
132 lines
4.6 KiB
PHP
<?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);
|
|
}
|