即时系统
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.
 
 
 
 
 

516 lines
18 KiB

<?php
/**
* lvzheAdmin [a web admin based ThinkPHP5]
* @author xiekunyu<raingad@foxmail.com>
*/
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)
{
$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']);
$file=FileModel::where(['md5'=>$info['md5']])->find();
// 判断文件是否存在,如果有则不再上传
if(!$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' && empty($compress_img) && $message) {
$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());
}
}
// 获取上传文件的信息
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']])->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,
];
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;
if($this->disk=='local'){
$compress_path='/storage/'.$compress_path;
}
// 原图地址
$filePath = root_path().'public' . $src;
// 处理图地址
$target = root_path().'public/' .ltrim($compress_path,'/');
// 图片模糊处理
if (class_exists('Imagick')) {
$compress_res = $this->slightBlurImagick($filePath, $target, 1);
} else {
$compress_res = $this->slightBlurGD($filePath, $target, 1);
}
if (!$compress_res) {
return '';
}
return $compress_path;
}
// 使用Imagick添加轻微模糊
protected function slightBlurImagick($source, $target, $blurLevel)
{
try {
$imagick = new \Imagick($source);
// 轻微模糊 (半径0.5-2.0)
$radius = min(2.0, max(0.5, $blurLevel * 0.5));
$imagick->gaussianBlurImage($radius, 1);
// 保存时降低质量
$extension = strtolower(pathinfo($target, PATHINFO_EXTENSION));
switch ($extension) {
case 'jpg':
case 'jpeg':
$imagick->setImageCompressionQuality(85);
$imagick->setImageFormat('jpeg');
break;
case 'webp':
case 'png':
$imagick->setImageCompressionQuality(85);
break;
}
return $imagick->writeImage($target);
} catch (\Exception $e) {
\think\facade\Log::error('Imagick模糊处理失败: ' . $e->getMessage());
return false;
}
}
// 使用GD库添加轻微模糊
protected function slightBlurGD($source, $target, $blurLevel)
{
$info = getimagesize($source);
$type = $info[2];
// 创建图像资源
switch ($type) {
case IMAGETYPE_JPEG:
$image = imagecreatefromjpeg($source);
break;
case IMAGETYPE_PNG:
$image = imagecreatefrompng($source);
break;
case IMAGETYPE_GIF:
$image = imagecreatefromgif($source);
break;
case IMAGETYPE_WEBP:
$image = imagecreatefromwebp($source);
break;
default:
return false;
}
// 应用轻微模糊
for ($i = 0; $i < $blurLevel; $i++) {
imagefilter($image, IMG_FILTER_GAUSSIAN_BLUR);
}
// 保存图片
switch ($type) {
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;
}
}