*/ namespace app\common\controller; use app\BaseController; use app\enterprise\model\{File as FileModel,Message,User,Emoji}; use app\manage\model\{Config}; use think\facade\Filesystem; use think\facade\Request; use think\File; use FFMpeg\FFMpeg; use FFMpeg\FFProbe; use FFMpeg\Coordinate\TimeCode; class Upload extends BaseController { protected $middleware = ['checkAuth']; protected $disk=''; protected $url=''; public function __construct() { parent::__construct(app()); $this->disk=env('filesystem.driver','local'); $this->url=getDiskUrl().'/'; } /** * 文件上传 */ public function upload($data,$path,$prefix = "",$fileObj = true, $oldFile = false) { $message=$data['message'] ?? ''; if($message){ $message=json_decode($message,true); } $uid=request()->userInfo['user_id'] ?? 1; if($fileObj){ $filePath = $path; }else{ $filePath = new File($path); } $info=$this->getFileInfo($filePath,$path,$fileObj); if($info['ext']=='' && $message){ $pathInfo = pathinfo($message['fileName'] ?? ''); $info['ext'] = $pathInfo['extension']; $info['name'] =$message['fileName'] ?? ''; } $conf=Config::where(['name'=>'fileUpload'])->value('value'); if($conf['size']*1024*1024 < $info['size']){ return shutdown(lang('file.uploadLimit',['size'=>$conf['size']])); } // 兼容uniapp文件上传 if($info['ext']=='' && isset($data['ext'])){ $info['ext']=$data['ext']; } $info['ext']=strtolower($info['ext']); if(!in_array($info['ext'],$conf['fileExt'])){ return shutdown(lang('file.typeNotSupport')); } $fileType=getFileType($info['ext']); $imageInfo=[]; if($fileType==2){ $filecate="image"; $imageInfo=$this->getImageSizeInfo($info['path']); }elseif($fileType==3){ $msgType=$message['type'] ?? ''; // 如果是语音消息,类型才为语音,否者为文件,主要是兼容发送音频文件 if($msgType=='voice'){ $filecate="voice"; }else{ $filecate="file"; } }elseif($fileType==4){ $filecate="video"; }else{ $filecate="file"; } if(!$prefix){ $prefix=$filecate.'/'.date('Y-m-d').'/'.$uid."/"; } $name=str_replace('.'.$info['ext'],'',$info['name']); if ($oldFile && !$fileObj) { $rep_path = str_replace(public_path(),'',$path); $file=FileModel::where(['src'=>$rep_path,'user_id'=>$uid])->find(); if ($file['name']) $name=$file['name']; } else { $file=FileModel::where(['md5'=>$info['md5'],'user_id'=>$uid])->find(); } // 判断文件是否存在,如果有则不再上传 if($filecate == 'image' || !$file){ $newName = uniqid() . '.' . $info['ext']; $object = $prefix . $newName; if($this->disk=='local'){ $object='storage/'.$object; } Filesystem::disk($this->disk)->putFileAs($prefix, $filePath, $newName); }else{ $object = $file['src']; } // 把左边的/去掉再加上,避免有些有/有些没有 $object='/'.ltrim($object,'/'); // 压缩图片 $compress_img = $file['compress_img'] ?? ''; if ($filecate == 'image') { $compress_img = $this->getCompressImg($object, $info['ext'], $prefix); } $ret = [ "src" => $object, "name" => $name, "cate" => $fileType, "size" => $info['size'], "md5" => $info['md5'], "file_type" => $info['mime'], "ext" => $info['ext'], "type" =>2, 'user_id'=>$uid, 'compress_img'=>$compress_img, 'videoInfo'=>$imageInfo ]; if($message){ // 自动获取视频第一帧,视频并且是使用的阿里云 if($message['type']=='video'){ $videoInfo=$this->getVideoCover($filePath); if($videoInfo){ $extends=$videoInfo['videoInfo']; $extends['poster']=$this->url.$videoInfo['src']; $message['extends']=$extends; }else{ $message['extends']['poster']=getMainHost().'/static/common/img/video.png'; } // if($this->disk=='aliyun'){ // $message['extends']['poster']=$this->url.$ret['src'].'?x-oss-process=video/snapshot,t_1000,m_fast,w_800,f_png'; // }else{ // $message['extends']['poster']=getMainHost().'/static/common/img/video.png'; // } } // 如果发送的文件是图片、视频、音频则将消息类型改为对应的类型 if(in_array($fileType,[2,3,4])){ $message['type']=$filecate; } if($message['type']=='image'){ $message['extends']=$imageInfo; } $newFile=new FileModel; // 录音就不保存了 if($message['type']!='voice'){ $newFile->save($ret); } $message['content']=$ret['compress_img'] ?: $ret['src']; $message['file_id']=$newFile->file_id ?? 0; $message['file_cate']=$fileType; $message['file_size']=$info['size']; $message['file_name']= $name.'.'.$info['ext']; $message['user_id']= $uid; $messageModel=new Message(); $data=$messageModel->sendMessage($message,$this->globalConfig); if(!$data){ return shutdown($messageModel->getError()); } return $data; }else{ $fileInfo=new FileModel; $saveData = $ret; if (isset($saveData['type'])) { unset($saveData['type']); } if (isset($saveData['videoInfo'])) { unset($saveData['videoInfo']); } $exists = $fileInfo->where($saveData)->find(); if (!$exists) { $fileInfo->save($saveData); } // 上传视频切片 if ($fileType == 4) { $videoInfo=$this->getVideoCover($filePath, true); if($videoInfo){ $poster=$this->url.$videoInfo['src']; }else{ $poster=getMainHost().'/static/common/img/video.png'; } $ret['poster'] = $poster; } return $ret; } } // 上传一般文件 public function uploadFile(){ $param=$this->request->param(); try{ $file=request()->file('file'); $info=$this->upload($param,$file); return success(lang('file.uploadOk'),$info); } catch(\Exception $e) { return error($e->getMessage().$e->getLine()); } } public function uploadFileImage() { $param=$this->request->param(); try{ $image_url=$param['image_url']??''; if (empty($image_url)) { throw new \Exception(lang('file.exist')); } $type = $param['type'] ?? 'image'; if ($type == 'image') { $data = $this->downloadImage($image_url); if (!$data['status']) { throw new \Exception(lang('file.error')); } $info=$this->upload($param,$data['path'],'', false); } elseif ($type == 'file') { $main_host = getMainHost(); $path = public_path() . '/' . ltrim(str_replace($main_host,'',$image_url),'/'); $info = $this->upload($param,$path,'', false, true); } else { throw new \Exception(lang('file.error')); } return success(lang('file.uploadOk'),$info); } catch(\Exception $e) { return error($e->getMessage()); } } // 获取上传文件的信息 protected function getFileInfo($file,$path,$isObj=false){ $info= [ 'path'=>$file->getRealPath(), 'size'=>$file->getSize(), 'mime'=>$file->getMime(), 'ext'=>$file->extension(), 'md5'=>$file->md5(), ]; if($isObj){ $info['name']=$file->getOriginalName(); }else{ // 根据路径获取文件名 $pathInfo = pathinfo($path); $info['name'] = $pathInfo['basename']; } return $info; } // 上传图片 public function uploadImage(){ $param=request::param(); try{ $file=request()->file('file'); $info=$this->upload($param,$file,'image/'.date('Y-m-d').'/'); $url=$this->url.$info['src']; return success(lang('file.uploadOk'),$url); } catch(\Exception $e) { return error($e->getMessage()); } } // 上传朋友圈图片视频 public function uploadPosts(){ $param=request::param(); try{ $files=request()->file('file'); $returnData = []; if (!is_array($files)) { $files = [$files]; } foreach ($files as $file) { $info=$this->upload($param,$file); $url=$this->url.$info['src']; $data = ['url' => $url, 'poster' => '']; if (isset($info['poster'])) { $data['poster']=$info['poster']; } $returnData[] = $data; } return success(lang('file.uploadOk'),$returnData); } catch(\Exception $e) { return error($e->getMessage()); } } // 普通上传头像 public function uploadAvatar(){ $param=request::param(); try{ $file=request()->file('file'); $uid=request()->userInfo['user_id']; $info=$this->upload($param,$file,'avatar/'.$uid.'/'); User::where(['user_id'=>$uid])->update(['avatar'=>$info['src']]); $url=$this->url.$info['src']; return success(lang('file.uploadOk'),$url); } catch(\Exception $e) { return error($e->getMessage()); } } // 服务器上传头像 public function uploadLocalAvatar($file,$param,$uid){ try{ $info=$this->upload($param,$file,'avatar/'.$uid.'/',false); return $info['src']; } catch(\Exception $e) { return $e->getMessage().$e->getLine(); } } // 上传表情 public function uploadEmoji(){ $param=request::param(); try{ $file=request()->file('file'); $filePath = $file; $uid=request()->userInfo['user_id'] ?? 1; $info=$this->getFileInfo($filePath,$file,true); if($info['ext']==''){ $pathInfo = pathinfo($message['fileName'] ?? ''); $info['ext'] = $pathInfo['extension']; $info['name'] =$message['fileName'] ?? ''; } // 表情不能大于1m if(2*1024*1024 < $info['size']){ return shutdown(lang('file.uploadLimit',['size'=>2])); } // 兼容uniapp文件上传 if($info['ext']=='' && isset($param['ext'])){ $info['ext']=$param['ext']; } $info['ext']=strtolower($info['ext']); if(!in_array($info['ext'],['jpg','jpeg','gif','png'])){ return shutdown(lang('file.typeNotSupport')); } $prefix='emoji/'.$uid.'/'; $name=str_replace('.'.$info['ext'],'',$info['name']); $fileInfo=FileModel::where(['md5'=>$info['md5'],'user_id'=>$uid,'delete_time'=>0])->find(); // 判断文件是否存在,如果有则不再上传 if(!$fileInfo){ $newName = uniqid() . '.' . $info['ext']; $object = $prefix . $newName; if($this->disk=='local'){ $object='storage/'.$object; } Filesystem::disk($this->disk)->putFileAs($prefix, $filePath, $newName); $ret = [ "src" => $object, "name" => $name, "cate" => 1, "size" => $info['size'], "md5" => $info['md5'], "file_type" => $info['mime'], "ext" => $info['ext'], "type" =>2, 'user_id'=>$uid, ]; $fileInfo=new FileModel; $fileInfo->save($ret); }else{ $object = $fileInfo->src; } // 把左边的/去掉再加上,避免有些有/有些没有 $object='/'.ltrim($object,'/'); $emojiInfo=[ 'user_id' => $uid, "src" => $object, "name" => $name, "type" => 2, "file_id" => $fileInfo->file_id, ]; $query = Emoji::where($emojiInfo)->find(); if ($query) { return shutdown(lang('file.emojiExist')); } Emoji::create($emojiInfo); return success('',$this->url.$object); } catch(\Exception $e) { return $e->getMessage().$e->getLine(); } } // 获取图片的尺寸 protected function getImageSizeInfo($file){ $extends=[]; // 如果图片获取图片的尺寸 $imageSize = getimagesize($file); $extends['width']=$imageSize[0]; $extends['height']=$imageSize[1]; // 如果宽大于高则为横图,宽度填充模式,否则为竖图,高度填充模式 if($imageSize[0]>=$imageSize[1]){ $extends['fixMode']=1; // 宽度填充 }else{ $extends['fixMode']=2; // 高度填充 } if($imageSize[0]<200 && $imageSize[1]<240){ $extends['fixMode']=3; // 小图 } return $extends; } // 获取视频封面 public function getVideoCover($filePath, $is_save = false){ $fileName=pathinfo($filePath,PATHINFO_FILENAME).'.jpg'; $ffmpegPath=env('ffmpeg.bin_path',''); if(!$ffmpegPath){ return false; } $path=array( 'ffmpeg.binaries' => $ffmpegPath.'/ffmpeg', 'ffprobe.binaries' => $ffmpegPath.'/ffprobe', 'timeout' => 3600, // 进程超时时间 'ffmpeg.threads' => 12, // FFMpeg应使用的线程数 ); $ffmpeg = FFMpeg::create($path); $ffprobe = FFProbe::create($path); $duration=$ffprobe->format($filePath)->get('duration');// 获取 duration 属性 $video = $ffmpeg->open($filePath); $frame = $video->frame(TimeCode::fromSeconds(1)); $tempPath=root_path().'public/temp'; $savePath=$tempPath. '/' .$fileName; $frame->save($savePath); $info=$this->upload([],$savePath,'cover/'.date('Y-m-d').'/',false); $info['videoInfo']['duration']= ceil($duration); unlink($savePath); if ($info && $is_save) { $fileInfo=new FileModel; $saveInfo = $info; if (isset($saveInfo['type'])) { unset($saveInfo['type']); } if (isset($saveInfo['videoInfo'])) { unset($saveInfo['videoInfo']); } $exists = $fileInfo->where($saveInfo)->find(); if (!$exists) { $fileInfo->save($saveInfo); } } return $info; } // 压缩图片地址 protected function getCompressImg($src, $ext, $prefix) { $newName2 = uniqid() . '.' . $ext; $compress_path = $prefix . $newName2; $fullPath = root_path().'public/'; if($this->disk=='local'){ $compress_path='/storage/'.$compress_path; $fullPath .= 'storage/'; } // 提前创建目录 $fullPath .= $prefix; if (!is_dir($fullPath)) { mkdir($fullPath, 0755, true); } // 原图地址 $filePath = root_path().'public' . $src; if (!file_exists($filePath)) { return ''; } // 处理图地址 $target = root_path().'public/' .ltrim($compress_path,'/'); // 图片模糊处理 $compress_res = $this->slightBlurGD($filePath, $target); if (!$compress_res) { return ''; } return $compress_path; } // 使用GD库添加轻微模糊 protected function slightBlurGD($imageFile, $target) { // 获取图片信息 list($imgWidth, $imgHeight, $imageType) = getimagesize($imageFile); // 根据图片类型创建图像资源 switch ($imageType) { case IMAGETYPE_JPEG: $image = imagecreatefromjpeg($imageFile); break; case IMAGETYPE_PNG: $image = imagecreatefrompng($imageFile); // 保留透明通道 imagealphablending($image, true); imagesavealpha($image, true); break; case IMAGETYPE_GIF: $image = imagecreatefromgif($imageFile); break; default: return false; } // 获取图片尺寸 // 调整选择区域确保在图片范围内 $x = 0; $y = 0; // 应用马赛克效果 $this->applyMosaic($image, $x, $y, $imgWidth, $imgHeight, 5, 2); // 保存图片 switch ($imageType) { case IMAGETYPE_JPEG: imagejpeg($image, $target, 85); break; case IMAGETYPE_PNG: imagepng($image, $target, 8); break; case IMAGETYPE_GIF: imagegif($image, $target); break; case IMAGETYPE_WEBP: imagewebp($image, $target, 85); break; } imagedestroy($image); return true; } /** * 应用马赛克效果 * * @param resource $image 图像资源 * @param int $x 起始X坐标 * @param int $y 起始Y坐标 * @param int $width 区域宽度 * @param int $height 区域高度 * @param int $blockSize 马赛克块大小 * @param int $blurStrength 模糊强度 (1-3) */ protected function applyMosaic($image, $x, $y, $width, $height, $blockSize, $blurStrength) { // 调整马赛克块大小 $adjustedBlockSize = $blockSize * $blurStrength; // 计算马赛克网格 $cols = ceil($width / $adjustedBlockSize); $rows = ceil($height / $adjustedBlockSize); // 处理每个马赛克块 for ($row = 0; $row < $rows; $row++) { for ($col = 0; $col < $cols; $col++) { // 计算当前块的区域 $blockX = $x + $col * $adjustedBlockSize; $blockY = $y + $row * $adjustedBlockSize; $blockWidth = min($adjustedBlockSize, $width - $col * $adjustedBlockSize); $blockHeight = min($adjustedBlockSize, $height - $row * $adjustedBlockSize); // 计算块内像素的平均颜色 $avgColor = $this->calculateAverageColor($image, $blockX, $blockY, $blockWidth, $blockHeight); // 填充整个块为平均颜色 imagefilledrectangle( $image, $blockX, $blockY, $blockX + $blockWidth - 1, $blockY + $blockHeight - 1, $avgColor ); } } } /** * 计算图像区域的平均颜色 * * @param resource $image 图像资源 * @param int $x 起始X坐标 * @param int $y 起始Y坐标 * @param int $width 区域宽度 * @param int $height 区域高度 * @return int 颜色标识符 */ protected function calculateAverageColor($image, $x, $y, $width, $height) { $totalR = 0; $totalG = 0; $totalB = 0; $totalAlpha = 0; $pixelCount = 0; // 遍历区域内的所有像素 for ($i = 0; $i < $width; $i++) { for ($j = 0; $j < $height; $j++) { $px = $x + $i; $py = $y + $j; // 确保像素在图像范围内 if ($px >= imagesx($image)) continue; if ($py >= imagesy($image)) continue; // 获取像素颜色 $color = imagecolorat($image, $px, $py); $rgba = imagecolorsforindex($image, $color); // 累加颜色值 $totalR += $rgba['red']; $totalG += $rgba['green']; $totalB += $rgba['blue']; $totalAlpha += $rgba['alpha']; $pixelCount++; } } // 计算平均值 if ($pixelCount === 0) { return imagecolorallocatealpha($image, 0, 0, 0, 127); } $avgR = round($totalR / $pixelCount); $avgG = round($totalG / $pixelCount); $avgB = round($totalB / $pixelCount); $avgAlpha = round($totalAlpha / $pixelCount); // 返回平均颜色 return imagecolorallocatealpha($image, $avgR, $avgG, $avgB, $avgAlpha); } protected function downloadImage($url, $savePath = 'temp') { try { // 验证URL有效性 if (!filter_var($url, FILTER_VALIDATE_URL)) { throw new \Exception("无效的图片URL"); } // 创建存储目录(递归创建) $savePath .= '/'. date("Y-m-d"); $rootPath = public_path(); $fullPath = $rootPath . $savePath; if (!is_dir($fullPath)) { mkdir($fullPath, 0755, true); } // 获取图片内容(使用cURL) $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 跳过SSL验证 $imageData = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($httpCode !== 200 || !$imageData) { throw new \Exception("图片下载失败,HTTP状态码: {$httpCode}"); } curl_close($ch); // 生成唯一文件名 $ext = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION); $ext = $ext ?: 'jpg'; // 默认jpg格式 $filename = date('YmdHis') . '_' . uniqid() . '.' . $ext; // 保存文件 $localPath = $fullPath . '/' . $filename; if (!file_put_contents($localPath, $imageData)) { throw new \Exception("文件保存失败"); } // 返回相对路径(适用于web访问) return ['status' => 1, 'path' => $savePath . '/' . $filename]; } catch (\Exception $e) { return ['status' => 0]; } } }