'', 'name' => '', 'old_name' => '', 'width' => '', 'height' => '', 'size' => 0, 'extension' => 'jpg', 'url' => '', 'merge' => false, 'guid' => '', 'type' => 'image/jpeg', 'thumb_url' => [], 'duration' => '', // 视频时长 ]; /** * 是否切片上传 * * @var bool */ protected $isCut = false; /** * @var DriveInterface */ protected $uploadDrive; /** * @var \League\Flysystem\Filesystem */ protected $filesystem; /** * UploadHelper constructor. * @param array $config * @param string $type 文件类型 * @param bool $superaddition 追加写入 * @throws \Exception */ public function __construct(array $config, $type, $superaddition = false) { // 过滤数据 $this->filter($config, $type); // 设置文件类型 $this->type = $type; // 初始化上传地址 $this->initPaths(); // 判断是否切片上传 if (isset($this->config['chunks']) && isset($this->config['guid'])) { $this->drive = 'local'; $this->isCut = true; } $drive = $this->drive; $config['superaddition'] = $superaddition; $this->uploadDrive = Yii::$app->uploadDrive->$drive($config); $this->filesystem = $this->uploadDrive->entity(); } /** * 验证文件 * * @throws NotFoundHttpException */ public function verifyFile() { $file = UploadedFile::getInstanceByName($this->uploadFileName); if (!$file) { throw new NotFoundHttpException('找不到上传文件'); } if ($file->getHasError()) { throw new NotFoundHttpException('上传失败,请检查文件'); } $this->baseInfo['extension'] = $file->getExtension(); $this->baseInfo['size'] = $file->size; empty($this->baseInfo['name']) && $this->baseInfo['name'] = $file->getBaseName(); $this->baseInfo['old_name'] = $file->baseName; $baseInfoName = $this->baseInfo['name']; if($file->getExtension() === "mp3"){ // 如果类型是MP3,作特殊处理,因为部分苹果手机用小程序,插件识别不了中文名称的音乐 $baseInfoName = $this->zhToAbbr($this->baseInfo['name']); } $this->baseInfo['url'] = $this->paths['relativePath'] . $baseInfoName . '.' . $file->getExtension(); $this->baseInfo['path_dir'] = $this->paths['relativePath']; unset($file); $this->verify(); } /** * MP3中文转首字母,如“我123”,转为“W123” */ public function zhToAbbr($str){ $result = ''; for ($i = 0; $i < mb_strlen($str); $i++) { $char = mb_substr($str, $i, 1, 'UTF-8'); if (preg_match('/[a-zA-Z0-9]/', $char)) { $result .= strtoupper($char); // 字母/数字直接保留 } else { $result .= $this->getFirstCharter($char); // 中文转首字母 } } return $result.time(); } /** * 中文转首字母 */ public function getFirstCharter($str) { if (empty($str)) { return '';} // 静态拼音首字母映射表(GB2312编码区间 → 字母) static $pinyinMap = [ [-20319, -20284, 'A'],[-20283, -19776, 'B'],[-19775, -19219, 'C'],[-19218, -18711, 'D'],[-18710, -18527, 'E'],[-18526, -18240, 'F'],[-18239, -17923, 'G'], [-17922, -17418, 'H'],[-17417, -16475, 'J'],[-16474, -16213, 'K'],[-16212, -15641, 'L'],[-15640, -15166, 'M'],[-15165, -14923, 'N'],[-14922, -14915, 'O'], [-14914, -14631, 'P'],[-14630, -14150, 'Q'],[-14149, -14091, 'R'],[-14090, -13319, 'S'],[-13318, -12839, 'T'],[-12838, -12557, 'W'],[-12556, -11848, 'X'], [-11847, -11056, 'Y'],[-11055, -10247, 'Z'], ]; // 如果是A-Z或a-z直接返回大写 $firstChar = $str[0]; $fchar = ord($firstChar); if (($fchar >= ord('A') && $fchar <= ord('Z')) || ($fchar >= ord('a') && $fchar <= ord('z'))) { return strtoupper($firstChar); } // 转换为GB2312编码计算区间 $s1 = iconv('UTF-8', 'gb2312//IGNORE', $str); $s2 = iconv('gb2312', 'UTF-8//IGNORE', $s1); $s = ($s2 == $str) ? $s1 : $str; if (strlen($s) < 2) { return '';} $asc = ord($s[0]) * 256 + ord($s[1]) - 65536; // 遍历映射表匹配区间 foreach ($pinyinMap as $range) { if ($asc >= $range[0] && $asc <= $range[1]) { return $range[2]; } } return ''; } /** * 验证Url * * @param $url * @return bool * @throws NotFoundHttpException */ public function verifyUrl($url) { $imgUrl = str_replace("&", "&", htmlspecialchars($url)); // http开头验证 if (strpos($imgUrl, "http") !== 0) { throw new NotFoundHttpException('不是一个http地址'); } preg_match('/(^https?:\/\/[^:\/]+)/', $imgUrl, $matches); $host_with_protocol = count($matches) > 1 ? $matches[1] : ''; // 判断是否是合法 url if (!filter_var($host_with_protocol, FILTER_VALIDATE_URL)) { throw new NotFoundHttpException('Url不合法'); } preg_match('/^https?:\/\/(.+)/', $host_with_protocol, $matches); $host_without_protocol = count($matches) > 1 ? $matches[1] : ''; // 此时提取出来的可能是 IP 也有可能是域名,先获取 IP $ip = gethostbyname($host_without_protocol); // 获取请求头并检测死链 $heads = get_headers($imgUrl, 1); if (!(stristr($heads[0], "200") && stristr($heads[0], "OK"))) { throw new NotFoundHttpException('文件获取失败'); } // Content-Type验证 if (!isset($heads['Content-Type']) || !stristr($heads['Content-Type'], "image")) { throw new NotFoundHttpException('格式验证失败'); } $extend = StringHelper::clipping($imgUrl, '.', 1); if (!in_array($extend, Yii::$app->params['uploadConfig']['images']['extensions'])) { $extend = 'jpg'; } $curl = new curl\Curl(); $img = $curl->get($imgUrl); $this->baseInfo['extension'] = $extend; $this->baseInfo['size'] = strlen($img); $this->config['md5'] = md5($img); $this->baseInfo['url'] = $this->paths['relativePath'] . $this->baseInfo['name'] . '.' . $extend; $this->verify(); return $img; } /** * 验证base64格式的内容 * * @param $data * @param $extend * @throws NotFoundHttpException */ public function verifyBase64($data, $extend) { $this->baseInfo['extension'] = $extend; $this->baseInfo['size'] = strlen($data); $this->baseInfo['url'] = $this->paths['relativePath'] . $this->baseInfo['name'] . '.' . $extend; $this->verify(); unset($data, $extend); } /** * 验证文件大小及类型 * * @throws NotFoundHttpException */ protected function verify() { if ($this->baseInfo['size'] > $this->config['maxSize'] && $this->config['maxSize'] > 0) { throw new NotFoundHttpException('文件大小超出网站限制'); } if (!empty($this->config['extensions']) && !in_array($this->baseInfo['extension'], $this->config['extensions'])) { throw new NotFoundHttpException('文件类型不允许'); } // 存储本地进行安全校验 if ($this->drive == Attachment::DRIVE_LOCAL) { if ($this->type == Attachment::UPLOAD_TYPE_FILES && in_array($this->baseInfo['extension'], $this->config['blacklist'])) { throw new NotFoundHttpException('上传的文件类型不允许'); } } } /** * 写入 * * @param bool $data * @throws NotFoundHttpException * @throws \League\Flysystem\FileExistsException * @throws \League\Flysystem\FileNotFoundException */ public function save($data = false) { // 获取域名 $this->baseInfo = $this->uploadDrive->getDomainName($this->baseInfo, $this->drive, $this->config['fullPath']); // 拦截 如果是切片上传就接管 if ($this->isCut == true) { $this->cut(); return; } // 判断如果文件存在就重命名文件名 // if ($this->filesystem->has($this->baseInfo['url'])) { // $name = explode('_', $this->baseInfo['name']); // $this->baseInfo['name'] = $name[0] . '_' . time() . '_' . StringHelper::random(8); // $this->baseInfo['url'] = $this->paths['relativePath'] . $this->baseInfo['name'] . '.' . $this->baseInfo['extension']; // } // 视频上传获取封面图 if ($this->type == Attachment::UPLOAD_TYPE_VIDEOS) { // $this->getVideoPoster(); $this->getVideoInfo(); } // 判断是否直接写入 if (false === $data) { $file = UploadedFile::getInstanceByName($this->uploadFileName); if (!$file->getHasError()) { $stream = fopen($file->tempName, 'r+'); $result = Yii::$app->uploadDrive->local([])->entity()->writeStream($this->baseInfo['url'], $stream); $this->compress(); if($this->drive != 'local'){ $stream = fopen(Yii::getAlias("@attachment/") . $this->baseInfo['url'], 'r+'); $result = $this->filesystem->writeStream($this->baseInfo['url'], $stream); unlink(Yii::getAlias("@attachment/") . $this->baseInfo['url']); } if (!$result) { throw new NotFoundHttpException('文件写入失败'); } if (is_resource($stream)) { fclose($stream); } } else { throw new NotFoundHttpException('上传失败,可能文件太大了'); } } else { $result = $this->filesystem->write($this->baseInfo['url'], $data); if (!$result) { throw new NotFoundHttpException('文件写入失败'); } } // 本地的图片才可执行 if ($this->type == 'images' && $this->drive == 'local') { // 图片水印 $this->watermark(); // 图片压缩 $this->compress(); // 创建缩略图 $this->thumb(); // 获取图片信息 if (empty($this->baseInfo['width']) && empty($this->baseInfo['height']) && $this->filesystem->has($this->baseInfo['url'])) { $imgInfo = getimagesize(Yii::getAlias('@attachment') . '/' . $this->baseInfo['url']); $this->baseInfo['width'] = $imgInfo[0] ?? 0; $this->baseInfo['height'] = $imgInfo[1] ?? 0; } } return; } /** * 获取视频封面图 * * @return bool * @throws \League\Flysystem\FileExistsException * @throws NotFoundHttpException */ public function getVideoPoster() { // use `ffmpeg` get first frame as video poster // save poster local and upload to cloud, return the cloud url $file = UploadedFile::getInstanceByName($this->uploadFileName); if ($file->error === UPLOAD_ERR_OK) { $this->type = Attachment::UPLOAD_TYPE_IMAGES; $name = $this->baseInfo['name']; $this->baseInfo['name'] = $this->baseInfo['name'] . '_poster'; $extension = $this->baseInfo['extension']; $url = $this->baseInfo['url']; $this->baseInfo['extension'] = 'jpg'; $this->baseInfo['url'] = $this->paths['relativePath'] . $this->baseInfo['name'] . '.' . $this->baseInfo['extension']; $tmpPosterFilePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $this->baseInfo['name'] . '.' . $this->baseInfo['extension']; FfmpegHelper::imageResize($file->tempName, $tmpPosterFilePath, 1); if (file_exists($tmpPosterFilePath)) { $stream = fopen($tmpPosterFilePath, 'r+'); $result = $this->filesystem->writeStream($this->baseInfo['url'], $stream); if (!$result) {echo '文件写入失败'; throw new NotFoundHttpException('文件写入失败'); } if (is_resource($stream)) { fclose($stream); } $imgInfo = getimagesize($tmpPosterFilePath); $this->baseInfo['width'] = $imgInfo[0] ?? 0; $this->baseInfo['height'] = $imgInfo[1] ?? 0; unlink($tmpPosterFilePath); // delete tmp file // 还原 $this->type = Attachment::UPLOAD_TYPE_VIDEOS; $this->baseInfo['thumb_url'][] = $this->baseInfo['domain_name'] . $this->baseInfo['url']; $this->baseInfo['url'] = $url; $this->baseInfo['name'] = $name; $this->baseInfo['extension'] = $extension; return true; } } return false; } /** * @name: * @msg: 获取视频时长 * @param {*} * @return {*} */ public function getVideoInfo() { $file = UploadedFile::getInstanceByName($this->uploadFileName); if ($file->error === UPLOAD_ERR_OK) { $tmp_file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $this->baseInfo['name'] . '.' . $this->baseInfo['extension']; copy($file->tempName, $tmp_file); $video_info = FfmpegHelper::getVideoInfo($tmp_file); // dd($video_info); $this->baseInfo['duration_str'] = $video_info['duration']??'0'; $this->baseInfo['duration_seconds'] = $video_info['seconds']??'0'; @unlink($tmp_file); } } /** * 水印 * * @param $fullPathName * @return bool */ protected function watermark() { return true; if (Yii::$app->debris->backendConfig('sys_image_watermark_status') != true) { return true; } // 原图路径 $absolutePath = Yii::getAlias("@attachment/") . $this->baseInfo['url']; $local = Yii::$app->debris->backendConfig('sys_image_watermark_location'); $watermarkImg = StringHelper::getLocalFilePath(Yii::$app->debris->backendConfig('sys_image_watermark_img')); if ($coordinate = DebrisHelper::getWatermarkLocation($absolutePath, $watermarkImg, $local)) { // $aliasName = StringHelper::getAliasUrl($fullPathName, 'watermark'); Image::watermark($absolutePath, $watermarkImg, $coordinate) ->save($absolutePath, ['quality' => 100]); } return true; } /** * 压缩 * * @param $fullPathName * @return bool */ protected function compress() { $compress = false; $mall_id = $this->config['mall_id']??0; $mall_limit = \Yii::$app->services->setting->mall->get($mall_id,'limit'); if(!empty($mall_limit['upload_images_compress'])){ $compress = true; } if ($compress != true) { return true; } // 原图路径 $absolutePath = Yii::getAlias("@attachment/") . $this->baseInfo['url']; $imgInfo = getimagesize($absolutePath); $compressibility = $this->config['compressibility']; $tmpMinSize = 0; if (empty($compressibility)) return true; foreach ($compressibility as $key => $item) { if ($this->baseInfo['size'] >= $tmpMinSize && $this->baseInfo['size'] < $key && $item < 100) { // $aliasName = StringHelper::getAliasUrl($fullPathName, 'compress'); if($imgInfo['mime']=='image/png'){ $imgInfo[0] = ($item+10)/100*$imgInfo[0]; $imgInfo[1] = ($item+10)/100*$imgInfo[1]; } Image::thumbnail($absolutePath, $imgInfo[0], $imgInfo[1]) ->save($absolutePath, ['quality' => $item]); break; } $tmpMinSize = $key; } return true; } /** * 缩略图 * * @return bool */ protected function thumb() { if (empty($this->config['thumb'])) { \Yii::error('thumb empty'); return true; } // 原图路径 $absolutePath = Yii::getAlias("@attachment/") . $this->baseInfo['url']; // 缩略图路径 $path = Yii::getAlias("@attachment/") . $this->paths['thumbRelativePath']; FileHelper::mkdirs($path); $thumbPath = $path . $this->baseInfo['name'] . '.' . $this->baseInfo['extension']; foreach ($this->config['thumb'] as $value) { $thumbFullPath = StringHelper::createThumbUrl($thumbPath, $value['width'], $value['height']); // 裁剪从坐标0,60 裁剪一张300 x 20 的图片,并保存 不设置坐标则从坐标0,0开始 // Image::crop($originalPath, $thumbWidth , $thumbHeight, [0, 60])->save($thumbOriginalPath), ['quality' => 100]); Image::thumbnail($absolutePath, $value['width'], $value['height'])->save($thumbFullPath); $this->baseInfo['thumb_url'][] = $thumbFullPath; } return true; } /** * 切片 * * @throws \League\Flysystem\FileExistsException * @throws \League\Flysystem\FileNotFoundException */ public function cut() { // 切片参数 $chunk = $this->config['chunk'] + 1; $guid = $this->config['guid']; // 临时文件夹路径 $url = $this->paths['tmpRelativePath'] . $chunk . '.' . $this->baseInfo['extension']; // 上传 $file = UploadedFile::getInstanceByName($this->uploadFileName); if ($file->error === UPLOAD_ERR_OK) { $stream = fopen($file->tempName, 'r+'); $result = $this->filesystem->writeStream($url, $stream); fclose($stream); // 判断如果上传成功就去合并文件 $this->baseInfo['chunk'] = $chunk; if ($this->config['chunks'] == $chunk) { // 缓存上传信息等待回调 Yii::$app->cache->set(self::PREFIX_MERGE_CACHE . $guid, [ 'type' => $this->type, 'drive' => $this->drive, 'paths' => $this->paths, 'baseInfo' => $this->baseInfo, 'config' => $this->config, ], 3600); } $this->baseInfo['merge'] = true; $this->baseInfo['guid'] = $guid; } } /** * 切片合并 * * @param int $name * @throws \League\Flysystem\FileExistsException * @throws \League\Flysystem\FileNotFoundException */ public function merge($name = 1) { // 由于合并会附带上一次切片的信息,取消切片判断 $this->isCut = false; $filePath = $this->paths['tmpRelativePath'] . $name . '.' . $this->baseInfo['extension']; if ($this->filesystem->has($filePath) && ($content = $this->filesystem->read($filePath))) { if ($this->filesystem->has($this->baseInfo['url'])) { $this->filesystem->update($this->baseInfo['url'], $content); } else { $this->filesystem->write($this->baseInfo['url'], $content); } unset($content); $this->filesystem->delete($filePath); $name += 1; self::merge($name); } else { // 删除文件夹,如果删除失败重新去合并 $this->filesystem->deleteDir($this->paths['tmpRelativePath']); } } /** * 获取生成路径信息 * * @return array */ protected function initPaths() { if (!empty($this->paths)) { return $this->paths; } $config = $this->config; // 保留原名称 $config['originalName'] == false && $this->baseInfo['name'] = $config['prefix'] . time() . '_' . StringHelper::random(8); // 文件路径 $mall_config = !empty($config['mall_id']) ? $config['mall_id'] . '/' : ''; $filePath = $config['path'] . $mall_config . date($config['subName'], time()) . "/"; // 缩略图 $thumbPath = Yii::$app->params['uploadConfig']['thumb']['path'] . $config['mall_id'] . '/' . date($config['subName'], time()) . "/"; empty($config['guid']) && $config['guid'] = StringHelper::random(8); $tmpPath = 'tmp/' . $config['mall_id'] . '/' . date($config['subName'], time()) . "/" . $config['guid'] . '/'; $this->paths = [ 'relativePath' => $filePath, // 相对路径 'thumbRelativePath' => $thumbPath, // 缩略图相对路径 'tmpRelativePath' => $tmpPath, // 临时相对路径 ]; return $this->paths; } /** * 过滤数据 * * @param $config */ protected function filter($config, $type) { $mall_id = $config['mall_id']; try { // 解密json foreach ($config as $key => &$item) { if (!empty($item) && !is_numeric($item) && !is_array($item)) { !empty(json_decode($item)) && $item = Json::decode($item); } } $config = ArrayHelper::filter($config, $this->filter); $this->config = ArrayHelper::merge(Yii::$app->params['uploadConfig'][$type], $config); // 参数 $this->baseInfo['width'] = $this->config['width'] ?? 0; $this->baseInfo['height'] = $this->config['height'] ?? 0; } catch (\Exception $e) { $this->config = Yii::$app->params['uploadConfig'][$type]; } !empty($this->config['drive']) && $this->drive = $this->config['drive']; $platform_limit = \Yii::$app->services->setting->platform->get('limit'); $mall_limit = \Yii::$app->services->setting->mall->get($mall_id,'limit'); switch ($type){ case 'videos': $upload_type = 'video'; break; case 'images': $upload_type = 'img'; break; } if(isset($upload_type)){ if(!empty($mall_limit[$upload_type])){ $this->config['maxSize'] = $mall_limit[$upload_type]*1024 * 1024; }else{ if(!empty($platform_limit[$upload_type])){ $this->config['maxSize'] = $platform_limit[$upload_type]*1024 * 1024; } } } } /** * 写入目录 * * @param array $paths */ public function setPaths(array $paths) { $this->paths = $paths; } /** * 写入基础信息 * * @param array $baseInfo */ public function setBaseInfo(array $baseInfo) { $this->baseInfo = $baseInfo; } /** * @param mixed $drive */ public function setDrive($drive) { $this->drive = $drive; } /** * @return array * @throws NotFoundHttpException * @throws \League\Flysystem\FileNotFoundException */ public function getBaseInfo($mall_id,$mch_id=0,$group_id=0,$store_id=0,$addons_name='') { // 是否切片 if ($this->isCut == true) { return $this->baseInfo; } // 处理上传的文件信息 $this->baseInfo['type'] = $this->filesystem->getMimetype($this->baseInfo['url']); $this->baseInfo['size'] = $this->filesystem->getSize($this->baseInfo['url']); $this->baseInfo['url'] = $this->baseInfo['domain_name'] . $this->baseInfo['url']; $path = $this->baseInfo['url']; $data = [ 'mall_id' => $mall_id, 'mch_id' => $mch_id, 'group_id' => $group_id, 'store_id' => $store_id, 'drive' => $this->drive, 'upload_type' => $this->type, 'size' => $this->baseInfo['size'], 'width' => $this->baseInfo['width'], 'height' => $this->baseInfo['height'], 'extension' => $this->baseInfo['extension'], 'name' => $this->baseInfo['name'], 'old_name' => $this->baseInfo['old_name'], 'md5' => $this->config['md5'] ?? '', 'url' => $this->baseInfo['url'], 'path' => $path, 'thumb_url' => !empty($this->baseInfo['thumb_url']) ? implode(',',$this->baseInfo['thumb_url']) : '', 'duration_str' => $this->baseInfo['duration_str'] ?? '', 'duration_seconds' => $this->baseInfo['duration_seconds'] ?? 0, 'addons_name' => $addons_name ]; //阿里云oss 设置了cdn:修改图片地址为cdn $oss_cdn = \Yii::$app->params['oss_cdn'] ?? ''; if($oss_cdn && $this->drive == Attachment::DRIVE_OSS){ //替换oss 地址 => cdn 地址 $data['url'] = str_replace($this->baseInfo['domain_name'],$oss_cdn,$data['url']); $data['path'] = str_replace($this->baseInfo['domain_name'],$oss_cdn,$data['path']); $this->baseInfo['url'] = $data['url']; } // 写入数据库 $attachment_id = Yii::$app->services->attachment->create($data); $this->baseInfo['id'] = $attachment_id; $this->baseInfo['formatter_size'] = Yii::$app->formatter->asShortSize($this->baseInfo['size'], 2); $this->baseInfo['upload_type'] = self::formattingFileType($this->baseInfo['type'], $this->baseInfo['extension'], $this->type); return $this->baseInfo; } /** * @param $specific_type * @param $extension * @return string */ public static function formattingFileType($specific_type, $extension, $upload_type) { if (preg_match("/^image/", $specific_type) && $extension != 'psd') { return Attachment::UPLOAD_TYPE_IMAGES; } return $upload_type; } /** * 删除文件 * @param $path * @throws * @return */ public function delete($path) { return $this->filesystem->delete($path); } /** * 本地上传 * @Author: hua * @DateTime: 2021/10/14 10:30 * @Copyright: copyright (c) 2021 广东七件事集团 * @param $relative_filename //相对路径资源 * @return mixed|void * @throws NotFoundHttpException * @throws \League\Flysystem\FileExistsException * @throws \League\Flysystem\FileNotFoundException */ public function upload($relative_filename) { $file = Yii::getAlias('@attachment') . '/' .$relative_filename; $path_info = pathinfo($file); $this->paths['extension'] = $path_info['extension']; $this->baseInfo['url'] = $relative_filename; $stream = fopen($file, 'r+'); $result = $this->filesystem->writeStream($this->baseInfo['url'], $stream); if (!$result) { throw new NotFoundHttpException('文件写入失败'); } if (is_resource($stream)) { fclose($stream); } return $this->baseInfo['url']; } }