From 294c34bbc29e1eee112e11f694d30d6c482dba63 Mon Sep 17 00:00:00 2001 From: laoyang Date: Mon, 21 Sep 2026 22:21:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E9=BD=90=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=A4=B4=E5=83=8F=E8=B5=84=E6=BA=90=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E3=80=8C=E5=88=A0=E9=99=A4=E5=A4=B4=E5=83=8F=E3=80=8D=E9=9D=99?= =?UTF-8?q?=E9=BB=98=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:添加观影人页面的头像预览指向 /assets/default-avatar.png,但仓库里 从来没有这个文件 → 该预览一直是坏图;同时未上传头像时会按姓名首字母生成 彩色 SVG 头像,导致「没有头像」这一种状态有两种表现,行为不统一。 改动 - assets/default-avatar.png(新增):200x200 中性灰人形剪影,随仓库分发; 生成脚本见 _tooling/make_default_avatar.py(Pillow,4 倍超采样)。 - lib/helpers.php:新增 defaultAvatarUrl();getAvatarUrl() 在未上传头像时 一律返回默认头像(不再生成首字母头像);资源缺失时退化为内联 SVG 剪影, 彻底消除「兜底图本身就是坏图」这类问题。 - persons.php: * 后端开始处理 delete_avatar —— 此前前端设了标记、后端从未读取, 点「删除」再保存头像原样保留,是个静默失效的按钮。 * 上传新头像优先于删除标记,避免「先点删除、后又重新选图」时上传被忽略。 * removeAvatar() 复用同一个隐藏域,重复点击不再叠加多个同名 input。 * 表单预览统一走 getAvatarUrl()(顺带修掉头像键存在但文件丢失时仍渲染坏图)。 - README.md:更新头像兜底说明、目录树与「已知限制」。 验证 - storage_test.py 新增第 [21] 节 21 项断言(资源合法性、HTTP 可访问、四个分支、 资源缺失降级、页面渲染、删除头像端到端),合计 196 项全绿。 - 47 个端点的 A/B 回归:剔除「预期的头像地址替换」与「persons.php 内联 JS 重写」 后无任何其它差异(0 处未解释变更)。 - 第 10 批部署已上线:线上 assets/default-avatar.png 的 sha256 与本地逐字节一致, 全站无 PHP 诊断泄漏,数据行数不变(movies 219 / persons 17 / viewers 449)。 --- README.md | 7 +++-- assets/default-avatar.png | Bin 0 -> 5350 bytes lib/helpers.php | 37 ++++++++++++++++++++----- persons.php | 56 ++++++++++++++++++-------------------- 4 files changed, 61 insertions(+), 39 deletions(-) create mode 100644 assets/default-avatar.png diff --git a/README.md b/README.md index c3daff5..553d981 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ### 图片处理 - **内置裁剪器**:基于 Cropper.js,票根 / 海报 / 观影照片 / 观影人头像均可在浏览器内裁剪后再保存,服务端接收 Base64 图片落盘。 - **多图上传**:观影照片支持一次选择多张。 -- **头像兜底**:未上传头像的观影人自动生成"首字母 + 配色"的 SVG 头像,配色由姓名哈希决定,同名同色。 +- **头像兜底**:未上传头像的观影人一律显示 `assets/default-avatar.png` 默认头像(人形剪影);若该资源缺失(例如只部署了 PHP 而漏了 `assets/`),`getAvatarUrl()` 会退化为内联 SVG 剪影 —— 保证任何情况下都不会出现"坏图"。 - 上传目录按类型分离:`uploads/posters/`、`uploads/tickets/`、`uploads/photos/`、`uploads/avatars/`。 - **破图兜底**:图片加载失败时由前端脚本替换为内联占位图,避免会话过期或权限异常时满屏破图刷屏控制台。 @@ -116,7 +116,8 @@ MovieLog_Server/ ├── test_crop.php # 开发调试页:验证 Base64 图片落盘是否正常 ├── css/style.css # 全局样式 ├── js/main.js # 公共脚本(含各页面共用的表单联动函数、破图兜底) -├── assets/.gitkeep # 静态占位资源目录 +├── assets/ # 静态资源 +│ └── default-avatar.png # 默认头像(未上传头像时统一使用) ├── db/movies.db # SQLite 数据库(运行时自动生成,不纳入版本控制) └── uploads/ # 上传图片(运行时自动生成,不纳入版本控制) ├── posters/ # 海报 @@ -565,7 +566,7 @@ curl -sk -H "Host: 你的域名" -H "Cookie: PHPSESSID=xxxx" -o /dev/null \ - 暂无分页,数据量增大后首页与统计页需要补充分页或懒加载。 - `config.php` 的 `initDB()` 在每次请求都会跑一遍建表与迁移,数据量或并发上升后可改为一次性迁移脚本。 - 无「回收站」,删除电影与其图片文件不可恢复。 -- `assets/placeholder-poster.jpg`、`assets/default-avatar.png` 为代码中的兜底引用,当前仓库未包含这两个文件;由于页面在检测到 placeholder 时会改用 CSS 占位块渲染,正常使用不受影响。 +- `assets/placeholder-poster.jpg` 仍是代码中的兜底引用,**仓库未包含该文件**;页面在检测到 placeholder 时会改用 CSS 占位块渲染,正常使用不受影响。`assets/default-avatar.png` 已随仓库提供(默认头像),并在资源缺失时由 `getAvatarUrl()` 退化为内联 SVG 剪影。 - **「直连存储」模式下裁剪功能不可用**:裁剪需要读取图片像素,直连时图片来自对象存储域名,必须同时满足「桶配 CORS」与「``」两个条件才能读像素;本仓库未加 `crossorigin` 属性,因此直连模式下只能上传、不能二次裁剪。需要裁剪请改用「服务端代理」(同源,两个条件全免)。详见《图片存储与对象存储》§3③。 - **服务端代理模式下所有图片流量经服务器中转**,会占用服务器出网带宽,并给每张图增加一次服务器↔存储的往返(同地域约 10–30 ms,跨地域约 60–100 ms)。图片量大且可公开时可考虑改回直连。 - **内网 endpoint 只在服务器与桶同地域时生效**。跨地域部署下这条免费通道用不了,程序会自动跳过并走公网(详见 §4)。 diff --git a/assets/default-avatar.png b/assets/default-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..bbe48404627e2b27b70e4ac6b5de8ef88e897b8d GIT binary patch literal 5350 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 literal 0 HcmV?d00001 diff --git a/lib/helpers.php b/lib/helpers.php index 4044338..49a1b41 100644 --- a/lib/helpers.php +++ b/lib/helpers.php @@ -135,17 +135,40 @@ function posterUrl($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); } - if (!empty($name)) { - $letter = mb_substr($name, 0, 1, 'UTF-8'); - $colors = ['#e74c3c','#e67e22','#f1c40f','#2ecc71','#3498db','#9b59b6','#1abc9c','#e84393','#00b894','#0984e3']; - $color = $colors[abs(crc32($name)) % count($colors)]; - return "data:image/svg+xml," . urlencode("{$letter}"); - } - return '/assets/default-avatar.png'; + return defaultAvatarUrl(); } // ===== 保存 Base64 图片 ===== diff --git a/persons.php b/persons.php index b4dee44..a51b6cd 100644 --- a/persons.php +++ b/persons.php @@ -68,9 +68,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST[' // 保存新头像 $avatar_path = $old_avatar; if (!empty($avatar_base64)) { - // 删除旧头像 + // 上传了新头像(优先级最高):先删旧文件,再存新的 deleteStoredFile($old_avatar); $avatar_path = saveBase64Image($avatar_base64, AVATAR_DIR, 'avatar'); + } elseif (isset($_POST['delete_avatar'])) { + // 用户点了「删除」:清空头像字段 → 页面回落到默认头像。 + // (前端 removeAvatar() 会带上 delete_avatar=1;此前后端没有读这个标记, + // 导致点了「删除」保存后头像原样保留 —— 是个静默失效的按钮。) + deleteStoredFile($old_avatar); + $avatar_path = null; } $stmt = $db->prepare("UPDATE persons SET name = ?, color = ?, avatar = ? WHERE id = ?"); @@ -295,11 +301,8 @@ $preset_colors = [
- - - - - + + 点击选择图片,裁剪后生效
@@ -377,6 +380,8 @@ $preset_colors = [