no message

This commit is contained in:
2026-03-06 11:49:11 +08:00
parent 8a056e2295
commit 43022b8ff6
5 changed files with 392 additions and 110 deletions
+111 -108
View File
@@ -4,11 +4,10 @@ namespace app\Service;
use app\Model\UploadedFiles;
use Webman\Http\Request;
use support\Redis;
/**
* 文件上传服务
* 支持本地存储,可扩展支持 OSS
* 使用阿里云 OSS 存储文件
*/
class UploadService
{
@@ -21,39 +20,17 @@ class UploadService
public const PARTITION_USEDCAR = 'usedcar';
public const PARTITION_OTHER = 'other';
/**
* 基础存储路径
*/
private string $basePath;
/**
* 访问域名
*/
private string $domain;
private OssService $ossService;
private string $tmpPath;
public function __construct()
{
$this->basePath = public_path(); // 存储到 public 目录
$this->domain = $this->getDomain();
$this->ossService = new OssService();
$this->tmpPath = sys_get_temp_dir();
}
/**
* 获取访问域名
*/
private function getDomain(): string
{
// 从环境变量或配置获取
$domain = getenv('MAIN_DOMAIN') ?: getenv('APP_URL') ?: '';
if (empty($domain)) {
$scheme = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$domain = $scheme . '://' . $host;
}
return rtrim($domain, '/');
}
/**
* 上传文件
* 上传文件到 OSS
*
* @param Request $request 请求对象
* @param string $partition 上传分区 (image/avatar/document/usedcar/other)
@@ -73,9 +50,11 @@ class UploadService
// 获取分区配置
$config = UploadedFiles::getPartitionConfig($partition);
// 获取文件大小(必须在移动文件前获取)
// 获取文件信息(必须在移动文件前获取)
$fileSize = $file->getSize();
$originalName = $file->getUploadName();
$mimeType = $file->getUploadMimeType();
$extension = strtolower($file->getUploadExtension());
// 验证文件大小
if ($fileSize > $config['max_size']) {
@@ -84,73 +63,83 @@ class UploadService
}
// 验证文件类型
$mimeType = $file->getUploadMimeType();
if (!empty($config['allowed_types']) && !in_array($mimeType, $config['allowed_types'])) {
throw new \Exception('不支持的文件类型: ' . $mimeType);
}
// 验证文件扩展名
$extension = strtolower($file->getUploadExtension());
if (!empty($config['extensions']) && !in_array($extension, $config['extensions'])) {
throw new \Exception('不支持的文件扩展名: .' . $extension);
}
// 生成存储路径
$datePath = date('Ymd');
$relativePath = '/' . $config['path'] . '/' . $datePath;
$fullPath = $this->basePath . $relativePath;
// 移动到临时文件
$tmpFileName = 'upload_' . uniqid() . '_' . bin2hex(random_bytes(8)) . '.' . $extension;
$tmpFilePath = $this->tmpPath . '/' . $tmpFileName;
// 创建目录
if (!is_dir($fullPath)) {
if (!mkdir($fullPath, 0755, true)) {
throw new \Exception('创建存储目录失败');
if (!$file->move($tmpFilePath)) {
throw new \Exception('文件保存到临时目录失败');
}
try {
// 图片处理(压缩)
if (str_starts_with($mimeType, 'image/')) {
$processedPath = $this->processImage($tmpFilePath);
if ($processedPath !== $tmpFilePath && file_exists($processedPath)) {
@unlink($tmpFilePath);
$tmpFilePath = $processedPath;
// 更新扩展名(如果转为 webp)
$extension = strtolower(pathinfo($tmpFilePath, PATHINFO_EXTENSION));
}
}
// 生成 OSS 对象键
$datePath = date('Ymd');
$fileName = $this->generateFileName($extension);
$objectKey = $this->ossService->getObjectKey("{$config['path']}/{$datePath}/{$fileName}");
// 上传到 OSS
$ossUrl = $this->ossService->uploadFile($tmpFilePath, $objectKey);
// 保存相对路径用于记录
$relativePath = str_replace($this->ossService->getMainDir() . '/', '', $objectKey);
$fileUrl = '/' . $relativePath;
// 记录到数据库
$uploadIp = $request->getRealIp() ?? '0.0.0.0';
$userAgent = $request->header('User-Agent', '');
$uploadedFile = UploadedFiles::create([
'file_name' => $fileName,
'file_path' => $fileUrl,
'file_size' => $fileSize,
'file_type' => $mimeType,
'category' => $category,
'original_name' => $originalName,
'extension' => $extension,
'partition' => $partition,
'upload_ip' => $uploadIp,
'user_agent' => $userAgent,
'user_id' => $userId,
]);
return [
'id' => $uploadedFile->id,
'file_name' => $fileName,
'file_url' => $fileUrl,
'full_url' => $ossUrl,
'oss_url' => $ossUrl,
'file_size' => $fileSize,
'file_type' => $mimeType,
'original_name' => $originalName,
'partition' => $partition,
'category' => $category,
];
} finally {
// 清理临时文件
if (file_exists($tmpFilePath)) {
@unlink($tmpFilePath);
}
}
// 生成文件名
$fileName = $this->generateFileName($file, $extension);
$filePath = $fullPath . '/' . $fileName;
// 移动上传文件
if (!$file->move($filePath)) {
throw new \Exception('文件保存失败');
}
// 图片处理(压缩)
if (str_starts_with($mimeType, 'image/')) {
$this->processImage($filePath, $mimeType);
}
// 记录到数据库
$fileUrl = $relativePath . '/' . $fileName;
$uploadIp = $request->getRealIp() ?? '0.0.0.0';
$userAgent = $request->header('User-Agent', '');
$uploadedFile = UploadedFiles::create([
'file_name' => $fileName,
'file_path' => $fileUrl,
'file_size' => $fileSize,
'file_type' => $mimeType,
'category' => $category,
'original_name' => $originalName,
'extension' => $extension,
'partition' => $partition,
'upload_ip' => $uploadIp,
'user_agent' => $userAgent,
'user_id' => $userId,
]);
return [
'id' => $uploadedFile->id,
'file_name' => $fileName,
'file_url' => $fileUrl,
'full_url' => $this->domain . $fileUrl,
'file_size' => $fileSize,
'file_type' => $mimeType,
'original_name' => $originalName,
'partition' => $partition,
'category' => $category,
];
}
/**
@@ -181,7 +170,7 @@ class UploadService
}
/**
* 删除文件
* 删除文件(OSS + 数据库)
*
* @param int $fileId 文件ID
* @param int|null $userId 用户ID(用于权限验证)
@@ -201,10 +190,18 @@ class UploadService
throw new \Exception('无权删除此文件');
}
// 删除物理文件
$fullPath = $this->basePath . $file->file_path;
if (file_exists($fullPath)) {
@unlink($fullPath);
// 从 file_path 构建完整的 OSS 对象键
// file_path 格式: /images/20250305/file.jpg
// 需要转换为: x_server_mobile/images/20250305/file.jpg
$relativePath = ltrim($file->file_path, '/');
$objectKey = $this->ossService->getObjectKey($relativePath);
try {
// 删除 OSS 文件
$this->ossService->deleteFile($objectKey);
} catch (\Exception $e) {
// OSS 删除失败,记录日志但继续删除数据库记录
error_log('删除 OSS 文件失败: ' . $e->getMessage());
}
// 删除数据库记录
@@ -226,11 +223,17 @@ class UploadService
throw new \Exception('文件不存在');
}
// 生成完整的 OSS URL
$relativePath = ltrim($file->file_path, '/');
$objectKey = $this->ossService->getObjectKey($relativePath);
$ossUrl = $this->ossService->getPublicUrl($objectKey);
return [
'id' => $file->id,
'file_name' => $file->file_name,
'file_url' => $file->file_path,
'full_url' => $this->domain . $file->file_path,
'file_path' => $file->file_path,
'full_url' => $ossUrl,
'oss_url' => $ossUrl,
'file_size' => $file->file_size,
'file_type' => $file->file_type,
'original_name' => $file->original_name,
@@ -243,7 +246,7 @@ class UploadService
/**
* 生成唯一文件名
*/
private function generateFileName($file, string $extension): string
private function generateFileName(string $extension): string
{
$timestamp = time();
$random = bin2hex(random_bytes(4));
@@ -252,12 +255,13 @@ class UploadService
/**
* 处理图片(压缩)
* 返回处理后的文件路径
*/
private function processImage(string $filePath, string $mimeType): void
private function processImage(string $filePath): string
{
// 检查是否支持 imagick 扩展
if (!extension_loaded('imagick')) {
return;
return $filePath;
}
try {
@@ -273,27 +277,26 @@ class UploadService
$image->autoOrient();
}
// 根据类型处理
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
if (in_array($extension, ['png', 'jpeg', 'jpg'])) {
// 转换为 WebP 并压缩
$image->setImageFormat('webp');
$image->setImageCompressionQuality(80);
$webpPath = preg_replace('/\.(png|jpe?g)$/i', '.webp', $filePath);
$image->writeImage($webpPath);
// 转换为 WebP 并压缩
$image->setImageFormat('webp');
$image->setImageCompressionQuality(80);
// 如果转换成功且文件名改变,删除原文件
if ($webpPath !== $filePath && file_exists($webpPath)) {
@unlink($filePath);
}
}
$webpPath = preg_replace('/\.(jpe?g|png)$/i', '.webp', $filePath);
$image->writeImage($webpPath);
$image->clear();
$image->destroy();
// 如果转换成功且文件名改变,返回新路径
if ($webpPath !== $filePath && file_exists($webpPath)) {
return $webpPath;
}
} catch (\Exception $e) {
// 处理失败,保留原图
error_log('图片处理失败: ' . $e->getMessage());
}
return $filePath;
}
/**