Files
freeshop/avatar_upload.php
2026-08-14 10:55:06 +08:00

164 lines
6.4 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 用户头像上传接口
*
* POST /avatar_upload
* 需登录,支持 JPG/PNG/WebP/GIF,最大 2MB。
* 上传成功后返回新头像 URL,前端可即时更新显示。
*
* 兼容性说明:
* - MIME 校验优先用 getimagesize()(几乎所有环境都可用),
* 再回退到 finfo / mime_content_type,避免某些主机未启用 fileinfo 扩展导致「永远上传失败」。
* - 目录创建与移动文件均做了失败兜底提示,便于定位环境权限问题。
*/
require __DIR__ . '/includes/init.php';
header('Content-Type: application/json; charset=utf-8');
// 必须登录
if (empty($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['ok' => false, 'msg' => '请先登录']);
exit;
}
$user = currentUser();
if (!$user) {
http_response_code(401);
echo json_encode(['ok' => false, 'msg' => '登录已过期']);
exit;
}
// 检查文件上传
if (!isset($_FILES['avatar']) || !is_array($_FILES['avatar']) || $_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
$errMap = [
UPLOAD_ERR_INI_SIZE => '文件超过服务器限制(php.ini 中 upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => '文件超过表单限制',
UPLOAD_ERR_PARTIAL => '文件上传不完整',
UPLOAD_ERR_NO_FILE => '未选择文件',
UPLOAD_ERR_NO_TMP_DIR => '服务器临时目录不存在',
UPLOAD_ERR_CANT_WRITE => '服务器写入失败',
UPLOAD_ERR_EXTENSION => '文件类型被禁止',
];
$code = $_FILES['avatar']['error'] ?? UPLOAD_ERR_NO_FILE;
echo json_encode(['ok' => false, 'msg' => $errMap[$code] ?? '上传错误,请重试']);
exit;
}
$file = $_FILES['avatar'];
// ─── 类型与尺寸校验(多重兜底)──────────────────────────────────────
$allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
// 1) getimagesize:同时拿到尺寸与 MIME,几乎在所有环境可用
$imgInfo = @getimagesize($file['tmp_name']);
$mime = ($imgInfo && !empty($imgInfo['mime'])) ? $imgInfo['mime'] : '';
// 2) 若 getimagesize 没拿到 MIME,再尝试 finfo / mime_content_type
if (!in_array($mime, $allowed, true)) {
if (function_exists('finfo_open')) {
$fi = @finfo_open(FILEINFO_MIME_TYPE);
if ($fi) {
$m = @finfo_file($fi, $file['tmp_name']);
finfo_close($fi);
if ($m) {
$mime = $m;
}
}
}
if (!in_array($mime, $allowed, true) && function_exists('mime_content_type')) {
$mime = @mime_content_type($file['tmp_name']);
}
}
if (!in_array($mime, $allowed, true)) {
echo json_encode(['ok' => false, 'msg' => '仅支持 JPG / PNG / WebP / GIF 图片']);
exit;
}
// 尺寸检查(至少 64x64
if (!$imgInfo || $imgInfo[0] < 64 || $imgInfo[1] < 64) {
echo json_encode(['ok' => false, 'msg' => '图片尺寸不能小于 64×64 像素']);
exit;
}
// 大小检查(2MB
if ($file['size'] > 2 * 1024 * 1024) {
echo json_encode(['ok' => false, 'msg' => '图片大小不能超过 2MB']);
exit;
}
// ─── 确保上传目录存在 ─────────────────────────────────────────────
$uploadDir = __DIR__ . '/storage/uploads/avatars';
if (!is_dir($uploadDir)) {
if (!@mkdir($uploadDir, 0755, true) && !is_dir($uploadDir)) {
echo json_encode(['ok' => false, 'msg' => '服务器无法创建上传目录(请检查 storage/uploads 目录权限)']);
exit;
}
}
if (!is_writable($uploadDir)) {
echo json_encode(['ok' => false, 'msg' => '上传目录不可写(请给 storage/uploads/avatars 目录写入权限)']);
exit;
}
// ─── 生成唯一文件名 ───────────────────────────────────────────────
$extMap = [IMAGETYPE_JPEG => 'jpg', IMAGETYPE_PNG => 'png', IMAGETYPE_GIF => 'gif', IMAGETYPE_WEBP => 'webp'];
$ext = $extMap[$imgInfo[2]] ?? 'png';
$fileName = $user['id'] . '_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$filePath = $uploadDir . '/' . $fileName;
// ─── 移动上传文件(move 失败则回退 copy)─────────────────────────
$moved = false;
if (@move_uploaded_file($file['tmp_name'], $filePath)) {
$moved = true;
} elseif (@copy($file['tmp_name'], $filePath)) {
$moved = true;
}
if (!$moved || !is_file($filePath)) {
echo json_encode(['ok' => false, 'msg' => '文件保存失败(请检查 storage/uploads/avatars 目录权限)']);
exit;
}
// ─── 确保 users 表存在 avatar 列(未跑升级脚本时兜底)─────────────
try {
$chk = db()->query("SHOW COLUMNS FROM " . tn('users') . " LIKE 'avatar'");
if ($chk && $chk->rowCount() === 0) {
db()->exec("ALTER TABLE " . tn('users') . " ADD COLUMN avatar VARCHAR(255) NULL DEFAULT NULL AFTER invited_by");
}
} catch (Throwable $e) {
// 忽略:以实际 UPDATE 结果为准
}
// ─── 删除旧头像 ───────────────────────────────────────────────────
$oldAvatar = $user['avatar'] ?? '';
if ($oldAvatar && strpos($oldAvatar, 'storage/uploads/avatars') !== false) {
$oldPath = __DIR__ . '/' . $oldAvatar;
if (file_exists($oldPath) && is_file($oldPath)) {
@unlink($oldPath);
}
}
// ─── 更新数据库 ───────────────────────────────────────────────────
$dbAvatar = 'storage/uploads/avatars/' . $fileName;
try {
db()->prepare('UPDATE ' . tn('users') . ' SET avatar=? WHERE id=?')
->execute([$dbAvatar, $user['id']]);
} catch (Throwable $e) {
// 列确实不存在且兜底 ALTER 也失败时才走到这里
echo json_encode(['ok' => false, 'msg' => '数据库更新失败:' . $e->getMessage()]);
exit;
}
// 清除会话缓存,使其它页面重新读取最新头像
unset($_SESSION['user_id'], $_SESSION['user']);
Logger::info('用户上传头像', ['user_id' => $user['id'], 'file' => $fileName]);
echo json_encode([
'ok' => true,
'msg' => '头像更新成功',
// 返回相对站点根的路径,前端与导航栏展示保持一致
'data' => ['url' => $dbAvatar]
]);
exit;