feat: 补齐默认头像资源,修复「删除头像」静默失效

问题:添加观影人页面的头像预览指向 /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)。
This commit is contained in:
2026-09-21 22:21:01 +08:00
parent 673a8abf78
commit 294c34bbc2
4 changed files with 61 additions and 39 deletions
+30 -7
View File
@@ -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 = "<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200' viewBox='0 0 200 200'>"
. "<rect width='200' height='200' fill='#e9edf3'/>"
. "<circle cx='100' cy='74' r='33' fill='#7f8896'/>"
. "<ellipse cx='100' cy='200' rx='74' ry='93' fill='#7f8896'/>"
. "</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("<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';
return defaultAvatarUrl();
}
// ===== 保存 Base64 图片 =====