_<6位随机>[_角色].ext // // 设计: // - 指纹段取图片内容的 md5 前 12 位,便于识别/比对同一张图(同名同图); // - 再拼 6 位随机后缀,保证文件名唯一——避免同一张图被多条记录引用时, // 删掉其一就把共用的文件一起删掉(删除逻辑不做引用计数); // - 角色段沿用原语义(poster / ticket / photo / avatar),便于人工排查。 function fingerprintedFilename($binary, $ext, $role = '') { $ext = strtolower((string)$ext); if (!in_array($ext, allowedImageExts())) { $ext = 'jpg'; } $fingerprint = substr(md5((string)$binary), 0, 12); $rand = bin2hex(random_bytes(3)); // 6 个十六进制字符 $role = trim((string)$role); return $fingerprint . '_' . $rand . ($role !== '' ? '_' . $role : '') . '.' . $ext; } // 文件名是否已经是「该内容的」指纹名(前 12 位 == 内容 md5 前 12 位)。 // 迁移时用它跳过已改过名的文件,保证重复迁移幂等(不会每次都换新随机后缀)。 function isFingerprintedName($filename, $binary) { $base = basename((string)$filename); if (strlen($base) < 12) return false; return substr(md5((string)$binary), 0, 12) === substr($base, 0, 12); } // 生成唯一文件名。传入 $binary 时按内容指纹命名;否则退化为时间戳 + uniqid。 // (保留 $binary 可选,是为了兼容"拿不到内容"的调用场景。) function uniqueFilename($original, $binary = null) { $ext = strtolower(pathinfo($original, PATHINFO_EXTENSION)); if (!in_array($ext, allowedImageExts())) { $ext = 'jpg'; } if ($binary !== null && $binary !== '') { return fingerprintedFilename($binary, $ext); } 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; } // 把绝对目录转换成相对 ROOT_DIR 的键前缀(如 /x/uploads/posters/ → uploads/posters/) function relPathOfDir($absDir) { $rel = str_replace('\\', '/', (string)$absDir); $root = rtrim(str_replace('\\', '/', ROOT_DIR), '/') . '/'; if (strpos($rel, $root) === 0) { $rel = substr($rel, strlen($root)); } return rtrim($rel, '/') . '/'; } // 删除已保存的文件(本地与云端按当前存储方式处理,不存在时静默跳过) function deleteStoredFile($relative_path) { if (empty($relative_path)) return false; return storageDelete($relative_path); } // 处理单个图片上传字段,成功返回相对键(如 uploads/posters/x.jpg),失败返回 null // 注意:$target_dir 为兼容保留,实际落盘位置由存储层根据键推导 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; } $tmpPath = $_FILES[$field]['tmp_name']; $binary = @file_get_contents($tmpPath); $filename = uniqueFilename($_FILES[$field]['name'], $binary === false ? null : $binary); $key = $relative_prefix . $filename; $mime = storageMimeOfFile($tmpPath); if (!storagePutFile($tmpPath, $key, $mime)) { return null; } return $key; } // 处理多张图片上传字段(如 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; } $tmpPath = $files['tmp_name'][$i]; $binary = @file_get_contents($tmpPath); $filename = uniqueFilename($files['name'][$i], $binary === false ? null : $binary); $key = 'uploads/photos/' . $filename; $mime = storageMimeOfFile($tmpPath); if (!storagePutFile($tmpPath, $key, $mime)) { continue; } $stmt = $db->prepare("INSERT INTO movie_photos (movie_id, photo_path) VALUES (?, ?)"); $stmt->execute([$movie_id, $key]); } } // 海报 URL(不存在时回退占位图) function posterUrl($path) { if (!storageExists($path)) { return '/assets/placeholder-poster.jpg'; } return storageUrl($path); } // ===== 头像处理 ===== // 默认头像 URL:未上传头像的人统一使用它。 // 资源随仓库分发(assets/default-avatar.png)。 // 若该文件缺失——比如只部署了 PHP 而漏了 assets/——则退化为内联 SVG 剪影, // 避免出现「兜底图本身就是坏图」:这正是本函数此前的问题(返回了一个并不存在的路径)。 // 注意:这里用 is_file() 探测,路径在站点目录内,不会像站外路径那样触碰 open_basedir 限制。 function defaultAvatarUrl() { static $url = null; if ($url !== null) { return $url; } $root = defined('ROOT_DIR') ? ROOT_DIR : dirname(__DIR__); if (is_file($root . '/assets/default-avatar.png')) { $url = '/assets/default-avatar.png'; return $url; } // 几何比例与 assets/default-avatar.png 保持一致,两种情形视觉无差别 $svg = "" . "" . "" . "" . ""; $url = 'data:image/svg+xml,' . rawurlencode($svg); return $url; } // 头像 URL:已上传 → 存储层 URL;未上传 → 统一的默认头像。 // 说明:$name 是历史遗留参数(早期按姓名首字母生成彩色 SVG 头像), // 现在一律使用默认头像,参数仅保留以兼容既有调用点。 function getAvatarUrl($avatar_path, $name = '') { if (storageExists($avatar_path)) { return storageUrl($avatar_path); } return defaultAvatarUrl(); } // ===== 保存 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 || $binary === '') return false; $mime = storageMimeFromContents($binary); $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 = fingerprintedFilename($binary, $ext, $prefix); $key = relPathOfDir($save_dir) . $filename; if (!storagePutContents($binary, $key, $mime)) return false; return $key; }