You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

124 lines
3.9 KiB

2 days ago
<?php
namespace App\Services\Course;
use App\Models\CourseMedia;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use RuntimeException;
class RemoteCourseAssetImporter
{
/**
* 从远程 URL 下载图片到 public 盘并写入 course_media。
*
* @param string|null $referer 可选 Referer部分 CDN 防盗链需要
*/
public function importFromUrl(string $url, string $subdir, ?string $referer = null): CourseMedia
{
$url = trim($url);
if ($url === '' || ! preg_match('#^https?://#i', $url)) {
throw new RuntimeException('图片地址无效');
}
if (! in_array($subdir, ['covers', 'promos'], true)) {
throw new RuntimeException('不支持的资源类型');
}
$headers = [
'User-Agent' => 'Mozilla/5.0 (compatible; SlakeSchool/1.0)',
'Accept' => 'image/*,*/*',
];
if ($referer && preg_match('#^https?://#i', $referer)) {
$headers['Referer'] = $referer;
}
try {
$response = Http::timeout(30)
->withHeaders($headers)
->get($url);
} catch (\Throwable $e) {
throw new RuntimeException('图片下载失败:'.$e->getMessage());
}
if (! $response->successful()) {
throw new RuntimeException('图片下载失败HTTP '.$response->status());
}
$body = $response->body();
if ($body === '') {
throw new RuntimeException('图片内容为空');
}
$contentType = $response->header('Content-Type');
if ($contentType && ! str_contains(strtolower($contentType), 'image')) {
throw new RuntimeException('远程资源不是图片');
}
$ext = $this->guessExtension($url, $contentType);
$filename = basename(parse_url($url, PHP_URL_PATH) ?: '') ?: 'import';
$filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $filename) ?: 'import';
$filename = Str::limit($filename, 64, '');
if (! preg_match('/\.(png|jpe?g|gif|webp)$/i', $filename)) {
$filename .= '.'.$ext;
}
$path = 'courses/'.$subdir.'/'.date('Ym').'/'.Str::random(16).'_'.$filename;
Storage::disk('public')->put($path, $body);
$publicUrl = CourseMedia::publicAssetUrl($path) ?? '';
return CourseMedia::query()->create([
'disk' => 'public',
'path' => $path,
'url' => $publicUrl,
'category' => $subdir,
'original_name' => $filename,
'mime_type' => $this->guessMimeType($ext, $contentType),
'size_bytes' => strlen($body),
]);
}
protected function guessExtension(string $url, ?string $contentType): string
{
if ($contentType) {
$lower = strtolower($contentType);
if (str_contains($lower, 'png')) {
return 'png';
}
if (str_contains($lower, 'jpeg') || str_contains($lower, 'jpg')) {
return 'jpg';
}
if (str_contains($lower, 'gif')) {
return 'gif';
}
if (str_contains($lower, 'webp')) {
return 'webp';
}
}
if (preg_match('#\.(png|jpe?g|gif|webp)(\?|$)#i', $url, $m)) {
return strtolower($m[1] === 'jpeg' ? 'jpg' : $m[1]);
}
return 'jpg';
}
protected function guessMimeType(string $ext, ?string $contentType): ?string
{
if ($contentType && str_contains(strtolower($contentType), 'image')) {
return strtok($contentType, ';') ?: null;
}
return match ($ext) {
'png' => 'image/png',
'jpg', 'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'webp' => 'image/webp',
default => null,
};
}
}