false, 'error' => '']; } if (is_array($file['error'])) { return ['ok' => false, 'error' => '上传参数不对']; } switch ($file['error']) { case UPLOAD_ERR_OK: break; case UPLOAD_ERR_NO_FILE: return ['ok' => false, 'error' => '']; case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: return ['ok' => false, 'error' => '图片太大了(上限 5MB)']; default: return ['ok' => false, 'error' => '上传失败(错误码 ' . (int)$file['error'] . ')']; } if ($file['size'] > self::MAX_BYTES) { return ['ok' => false, 'error' => '图片太大了(上限 5MB)']; } $finfo = new \finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($file['tmp_name']); $allow = ['image/jpeg' => 'imagecreatefromjpeg', 'image/png' => 'imagecreatefrompng', 'image/gif' => 'imagecreatefromgif', 'image/webp' => 'imagecreatefromwebp']; if (!isset($allow[$mime])) { return ['ok' => false, 'error' => '只支持 JPG / PNG / GIF / WebP 图片']; } $src = $allow[$mime]($file['tmp_name']); if (!$src) { return ['ok' => false, 'error' => '图片读不出来']; } $w = imagesx($src); $h = imagesy($src); $side = min($w, $h); $sx = (int)(($w - $side) / 2); $sy = (int)(($h - $side) / 2); $canvas = imagecreatetruecolor(self::SIZE, self::SIZE); // PNG/GIF 的透明区转成纸白,不然头像会是黑底 $bg = imagecolorallocate($canvas, 253, 251, 245); imagefilledrectangle($canvas, 0, 0, self::SIZE, self::SIZE, $bg); imagecopyresampled($canvas, $src, 0, 0, $sx, $sy, self::SIZE, self::SIZE, $side, $side); imagedestroy($src); // 先在临时目录生成,再整体交给 Storage —— 这样本地与 OSS 是同一条路径 $tmpOut = tempnam(sys_get_temp_dir(), 'fbav'); if ($tmpOut === false || !imagejpeg($canvas, $tmpOut, 88)) { imagedestroy($canvas); @unlink((string)$tmpOut); return ['ok' => false, 'error' => '处理图片失败']; } imagedestroy($canvas); $key = Storage::AVATAR_DIR . '/' . bin2hex(random_bytes(12)) . '.jpg'; $res = Storage::putFile($familyId, $tmpOut, $key, 'image/jpeg'); @unlink($tmpOut); if (empty($res['ok'])) { return ['ok' => false, 'error' => '保存失败:' . (string)($res['error'] ?? '')]; } $out = ['ok' => true, 'file' => $key]; if (!empty($res['note'])) { $out['note'] = (string)$res['note']; } return $out; } /** 头像访问地址(无头像时回退内置剪影,保证永远不出现破图) */ public static function avatarUrl(int $familyId, ?string $rel): string { $rel = trim((string)$rel); if ($rel === '') { return asset('/assets/avatar-default.svg'); } return Storage::url($familyId, $rel); } public static function remove(int $familyId, ?string $rel): void { if ($rel) { Storage::delete($familyId, (string)$rel); } } /** 让 Storage 知道自己该用哪个驱动(测试/迁移用) */ public static function driverId(int $familyId): string { return Storage::effectiveDriver($familyId); } }