package com.pcloud.common.utils.aliyun;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.List;

import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.model.GetObjectRequest;
import com.aliyun.oss.model.OSSObject;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.UploadFileRequest;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.mts.model.v20140618.QueryJobListRequest;
import com.aliyuncs.mts.model.v20140618.QueryJobListResponse;
import com.aliyuncs.mts.model.v20140618.QueryJobListResponse.Job;
import com.aliyuncs.mts.model.v20140618.QueryJobListResponse.Job.Output.Properties.Streams.AudioStream;
import com.aliyuncs.mts.model.v20140618.QueryJobListResponse.Job.Output.Properties.Streams.VideoStream;
import com.aliyuncs.mts.model.v20140618.SubmitJobsRequest;
import com.aliyuncs.mts.model.v20140618.SubmitJobsResponse;
import com.aliyuncs.mts.model.v20140618.SubmitMediaInfoJobRequest;
import com.aliyuncs.mts.model.v20140618.SubmitMediaInfoJobResponse;
import com.aliyuncs.mts.model.v20140618.SubmitMediaInfoJobResponse.MediaInfoJob.Properties.Format;
import com.aliyuncs.mts.model.v20140618.SubmitSnapshotJobRequest;
import com.aliyuncs.mts.model.v20140618.SubmitSnapshotJobResponse.SnapshotJob;
import com.aliyuncs.profile.DefaultProfile;
import com.pcloud.common.constant.AliyunConstant;
import com.pcloud.common.constant.FilePathConst;
import com.pcloud.common.constant.UrlConstant;
import com.pcloud.common.dto.OssMediaInfoDTO;
import com.pcloud.common.entity.OSSFileDO;
import com.pcloud.common.entity.UploadResultInfo;
import com.pcloud.common.enums.AliyunEnum;
import com.pcloud.common.enums.ImageTypeEnum;
import com.pcloud.common.exceptions.BizException;
import com.pcloud.common.exceptions.FileException;
import com.pcloud.common.utils.DateUtils;
import com.pcloud.common.utils.FileUtils;
import com.pcloud.common.utils.ImageUtils;
import com.pcloud.common.utils.ListUtils;
import com.pcloud.common.utils.LocalDateUtils;
import com.pcloud.common.utils.UUIDUitl;
import com.pcloud.common.utils.encode.ZSEncode;
import com.pcloud.common.utils.httpclient.UrlUtils;
import com.pcloud.common.utils.rsa.MD5;
import com.pcloud.common.utils.string.StringTools;
import com.pcloud.common.utils.string.StringUtil;

/**
 * @描述：
 * @作者：songx
 * @创建时间：2017年6月26日,下午12:03:44 @版本：1.0
 */
public class OssUtils {

    /**
     * logger
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(OssUtils.class);

    /**
     * ACS客户端，华东地区
     */
    private static DefaultAcsClient acsClient;

    /**
     * ACS客户端，华北地区
     */
    private static DefaultAcsClient acsClientBj;

    /**
     * 获取ACS客户端
     *
     * @param bucketName
     * @return
     */
    private static DefaultAcsClient initAcsClient(String bucketName) {
        try {
            String mtsRegion = AliyunConstant.getMtsRegion(bucketName);
            DefaultProfile.addEndpoint(mtsRegion, mtsRegion, "Mts", AliyunConstant.getMtsEndPoint(bucketName));
            return new DefaultAcsClient(DefaultProfile.getProfile(mtsRegion, AliyunConstant.MAIN_ACCESS_KEY_ID,
                    AliyunConstant.MAIN_ACCESS_KEY_SECRET));
        } catch (Exception e) {
            LOGGER.error("【aliOSS】初始化媒体转码ACS客户端失败:" + e.getMessage(), e);
            throw new FileException(FileException.FILE_READ_FAILURE, "初始化Acs客户端失败");
        }
    }

    /**
     * 获取ACS客户端
     *
     * @param bucketName
     * @return
     */
    private static DefaultAcsClient getAcsClient(String bucketName) {
        if (AliyunConstant.LIVE_BUCKET.equals(bucketName)) {
            if (acsClientBj == null) {
                acsClientBj = initAcsClient(bucketName);
            }
            return acsClientBj;
        } else {
            if (acsClient == null) {
                acsClient = initAcsClient(bucketName);
            }
            return acsClient;
        }
    }

    /**
     * 本地上传文件到aliOSS,isFlag=true，表示上传成功
     *
     * @param filePath 源文件物理地址
     * @param fileName 源文件名称，可为null
     * @return
     * @throws FileException
     */
    public static UploadResultInfo uploadLocalFile(String filePath, String fileName) throws FileException {
        return uploadLocalFile4CustomName(filePath, null);
    }

    /**
     * 本地上传文件到aliOSS,isFlag=true，表示上传成功 <br>
     * 自定义上传后的文件名称
     *
     * @param filePath 源文件物理地址
     * @param fileName 源文件名称，可为null
     * @return
     * @throws FileException
     */
    public static UploadResultInfo uploadLocalFile4CustomName(String filePath, String fileName) throws FileException {
        String fileType = FileUtils.getFileType(filePath);
        String outObjectKey = getOutObjectKey(AliyunEnum.UPLOAD.value, fileName, fileType);
        uploadPointFile2OSS(filePath, AliyunConstant.FILE_BUCKET, outObjectKey);
        return getResultInfo(outObjectKey, new File(filePath).length(), null, AliyunConstant.FILE_BUCKET);
    }

    /**
     * byte[]数组文件流上传 ,isFlag=true，表示上传成功
     *
     * @param buff     源文件字节数组
     * @param fileName 源文件名称不带后缀，可为null
     * @param fileType 源文件后缀
     * @return
     * @throws FileException
     */
    public static UploadResultInfo uploadFileByte(byte[] buff, String fileName, String fileType) throws FileException {
        return uploadFileStream(new ByteArrayInputStream(buff), fileName, fileType);
    }

    /**
     * 文件流上传到aliOSS, isFlag=true，表示上传成功 <br/>
     * 慎用，该方法稳定性不确定，曾出现过上传的文件流丢失的情况
     *
     * @param is       源文件流
     * @param fileName 源文件名称不带后缀，可为null
     * @param fileType 源文件后缀
     * @return
     * @throws FileException
     */
    public static UploadResultInfo uploadFileStream(InputStream is, String fileName, String fileType)
            throws FileException {
        String outObjectKey = getOutObjectKey(AliyunEnum.UPLOAD.value, null, fileType);
        uploadFileStream2OSS(is, AliyunConstant.FILE_BUCKET, outObjectKey);
        return getResultInfo(outObjectKey, null, null, AliyunConstant.FILE_BUCKET);
    }

    /**
     * 文件流上传到aliOSS
     *
     * @param is
     * @param bucketName
     * @param objectKey
     * @return
     * @throws FileException
     */
    private static OSSFileDO uploadFileStream2OSS(InputStream is, String bucketName, String objectKey)
            throws FileException {
        if (is == null) {
            throw new FileException(FileException.FILE_CONTENT_NOT_EXIST, "InputStream is null!");
        }
        OSSClient client = getOSSClient(bucketName);
        OSSFileDO ossFileDO = null;
        try {
            client.putObject(bucketName, objectKey, is);
            ossFileDO = new OSSFileDO(AliyunConstant.getOssRegion(bucketName), bucketName,
                    URLEncoder.encode(objectKey, "utf-8"));
            is.close();
        } catch (Exception e) {
            LOGGER.error("【aliOSS】上传文件失败:" + e.getMessage(), e);
            throw new FileException(FileException.FILE_UPLOAD_FAILURE, "上传文件失败");
        } finally {
            client.shutdown();
        }
        return ossFileDO;
    }

    /**
     * 断点续传上传文件到aliOSS
     *
     * @param filePath
     * @param fileName
     * @return
     */
    private static OSSFileDO uploadPointFile2OSS(String filePath, String bucketName, String objectKey)
            throws FileException {
        if (filePath == null) {
            throw new RuntimeException("filePath is null!");
        }
        OSSClient ossClient = getOSSClient(bucketName);
        OSSFileDO ossFileDO = null;
        try {
            // 设置断点续传请求
            UploadFileRequest uploadFileRequest = new UploadFileRequest(bucketName, objectKey);
            // 指定上传的本地文件
            uploadFileRequest.setUploadFile(filePath);
            // 指定上传并发线程数
            uploadFileRequest.setTaskNum(5);
            // 指定上传的分片大小
            uploadFileRequest.setPartSize(5 * 1024 * 1024);
            // 开启断点续传
            uploadFileRequest.setEnableCheckpoint(true);
            // 断点续传上传
            ossClient.uploadFile(uploadFileRequest);
            ossFileDO = new OSSFileDO(AliyunConstant.getOssRegion(bucketName), bucketName,
                    URLEncoder.encode(objectKey, "utf-8"));
        } catch (Throwable e) {
            LOGGER.error("【aliOSS】上传文件失败:" + e.getMessage(), e);
            throw new FileException(FileException.FILE_UPLOAD_FAILURE, "上传文件失败");
        } finally {
            ossClient.shutdown();
        }
        return ossFileDO;
    }

    /**
     * 提交MP4转码作业
     *
     * @param fileName 源文件的名称不带后缀,可为null
     * @param filePath 源文件本地路径
     * @return
     * @throws FileException
     */
    public static String submitTranscodeJobMp4(String fileName, String filePath) throws FileException {
        String[] result = submitTranscodeJobMp4Ex(fileName, filePath);
        return result[0];
    }

    /**
     * 提交MP4转码作业
     *
     * @param fileName
     * @param filePath
     * @return [0]:jobId,转码作业ID;[1]:转码后的地址
     * @throws FileException
     */
    public static String[] submitTranscodeJobMp4Ex(String fileName, String filePath) throws FileException {
        LOGGER.info("【aliOSS】提交MP4转码作业.[filePath]=" + filePath);
        String fileType = FileUtils.getFileType(filePath);
        String inObjectKey = getInObjectKey(AliyunEnum.TRANSCODE.value, fileName, fileType);
        String inputBucketName = AliyunConstant.INPUT_BUCKET;
        OSSFileDO ossFileDO = uploadPointFile2OSS(filePath, inputBucketName, inObjectKey);
        String outObjectKey = getOutObjectKey(AliyunEnum.TRANSCODE.value, null, "mp4");
        return transcodeJob(ossFileDO, AliyunConstant.getMp4TemplateId(inputBucketName), outObjectKey, null, null);
    }

    /**
     * 提交MP4转码作业,文件流方式
     *
     * @param buff     源文件流
     * @param fileName 源文件的名称不带后缀,可为null
     * @param fileType 源文件的后缀
     * @return
     */
    public static String submitTranscodeJobMp4(byte[] buff, String fileName, String fileType) throws FileException {
        LOGGER.info("【aliOSS】提交MP4转码作业.");
        String inObjectKey = getInObjectKey(AliyunEnum.TRANSCODE.value, fileName, fileType);
        String inputBucketName = AliyunConstant.INPUT_BUCKET;
        OSSFileDO inputFile = uploadFileStream2OSS(new ByteArrayInputStream(buff), inputBucketName, inObjectKey);
        String outObjectKey = getOutObjectKey(AliyunEnum.TRANSCODE.value, null, "mp4");
        String[] result = transcodeJob(inputFile, AliyunConstant.getMp4TemplateId(inputBucketName), outObjectKey, null,
                null);
        return result[0];
    }

    /**
     * 提交MP3转码作业
     *
     * @param fileName 源文件的名称不带后缀,可为null
     * @param filePath 源文件本地路径
     * @return
     * @throws FileException
     */
    public static String submitTranscodeJobMp3(String fileName, String filePath) throws FileException {
        String[] result = submitTranscodeJobMp3Ex(fileName, filePath);
        return result[0];
    }

    /**
     * 提交MP3转码作业
     *
     * @param fileName
     * @param filePath
     * @return [0]:jobId,转码作业ID;[1]:转码后的地址
     * @throws FileException
     */
    public static String[] submitTranscodeJobMp3Ex(String fileName, String filePath) throws FileException {
        LOGGER.info("【aliOSS】提交MP3转码作业.[filePath]=" + filePath);
        String fileType = FileUtils.getFileType(filePath);
        String inObjectKey = getInObjectKey(AliyunEnum.TRANSCODE.value, fileName, fileType);
        String inputBucketName = AliyunConstant.INPUT_BUCKET;
        OSSFileDO ossFileDO = uploadPointFile2OSS(filePath, AliyunConstant.INPUT_BUCKET, inObjectKey);
        String outObjectKey = getOutObjectKey(AliyunEnum.TRANSCODE.value, null, "mp3");
        return transcodeJob(ossFileDO, AliyunConstant.getMp3TemplateId(inputBucketName), outObjectKey, null, null);
    }

    /**
     * 提交MP3转码作业，文件流方式 <br/>
     * 使用byte[]方式上传，文件流上传容易导致读取长度0，造成转码失败
     *
     * @param is       源文件流
     * @param fileName 源文件的名称不带后缀,可为null
     * @param fileType 源文件的后缀
     * @return
     */
    @Deprecated
    public static String submitTranscodeJobMp3(InputStream is, String fileName, String fileType) throws FileException {
        String inObjectKey = getInObjectKey(AliyunEnum.TRANSCODE.value, fileName, fileType);
        String inputBucketName = AliyunConstant.INPUT_BUCKET;
        OSSFileDO inputFile = uploadFileStream2OSS(is, inputBucketName, inObjectKey);
        String outObjectKey = getOutObjectKey(AliyunEnum.TRANSCODE.value, null, "mp3");
        String[] result = transcodeJob(inputFile, AliyunConstant.getMp3TemplateId(inputBucketName), outObjectKey, null,
                null);
        return result[0];
    }

    /**
     * 提交MP3转码作业
     *
     * @param buff     文件
     * @param fileName 源文件的名称不带后缀,可为null
     * @param fileType 上传的文件后缀
     * @return
     * @throws FileException
     */
    public static String submitTranscodeJobMp3(byte[] buff, String fileName, String fileType) throws FileException {
        LOGGER.info("【aliOSS】提交MP3转码作业.");
        String inObjectKey = getInObjectKey(AliyunEnum.TRANSCODE.value, fileName, fileType);
        String inputBucketName = AliyunConstant.INPUT_BUCKET;
        OSSFileDO inputFile = uploadFileStream2OSS(new ByteArrayInputStream(buff), inputBucketName, inObjectKey);
        String outObjectKey = getOutObjectKey(AliyunEnum.TRANSCODE.value, null, "mp3");
        String[] result = transcodeJob(inputFile, AliyunConstant.getMp3TemplateId(inputBucketName), outObjectKey, null,
                null);
        return result[0];
    }

    /**
     * 转码作业，同步的方式
     *
     * @param fileUrl
     * @return
     * @throws FileException
     */
    public static UploadResultInfo submitTranscodeJobSync(String fileUrl) throws FileException {
        String bucketName = AliyunConstant.getBucketName(fileUrl);
        String jobId = transcodeJob(null, fileUrl);
        return transcodeJobResultSync(jobId, bucketName);
    }

    /**
     * 转码作业
     *
     * @param fileName 文件名称
     * @param filePath 文件地址
     * @return
     * @throws FileException
     */
    public static String transcodeJob(String fileName, String filePath) throws FileException {
        String[] result = transcodeJobEx(fileName, filePath, null, null);
        return result[0];
    }

    /**
     * 转码作业
     *
     * @param fileName
     * @param fileUrl          文件地址
     * @param outFileType      输出文件类型
     * @param waterMarkContent 视频水印文本,如果不需要水印传null
     * @return [0]:jobId,[1]:转码后的地址
     */
    public static String[] transcodeJobEx(String fileName, String fileUrl, String outFileType,
                                          String waterMarkContent) {
        String fileType = FileUtils.getFileType(fileUrl);
        if (StringTools.contains(fileUrl, AliyunConstant.OSS_CDN_URLS)) {
            return transcodeJobExistOSS(fileUrl, fileName, fileType, outFileType, waterMarkContent);
        } else {
            return transcodeJobNotExistOSS(fileUrl, fileName, fileType, waterMarkContent);
        }
    }

    /**
     * 已经存在于OSS中的文件转码
     *
     * @param fileUrl
     * @param fileName
     * @param fileType
     * @param outFileType
     * @param waterMarkContent
     * @return
     */
    private static String[] transcodeJobExistOSS(String fileUrl, String fileName, String fileType, String outFileType,
                                                 String waterMarkContent) {
        LOGGER.info("【aliOSS】已经存在于OSS中的文件转码.<START>.[fileUrl]=" + fileUrl + ",[outFileType]=" + outFileType);
        String[] result = null;
        String inObjectKey = splitObjectKey(fileUrl);
        String inputBucketName = getInputBucketName(fileUrl);
        OSSFileDO ossFileDO = new OSSFileDO(AliyunConstant.getOssRegion(inputBucketName), inputBucketName, inObjectKey);
        if (FileUtils.VIDEO.equals(FileUtils.getGatherName(fileType))) {
            outFileType = StringUtil.isEmpty(outFileType) ? "mp4" : outFileType;
            String outObjectKey = getOutObjectKey(AliyunEnum.TRANSCODE.value, fileName, outFileType);
            // 组装水印参数
            JSONArray waterMarkConfigArray = getWaterMarkConfig(waterMarkContent);
            result = transcodeJob(ossFileDO, AliyunConstant.getVideoTemplateId(inputBucketName, outFileType),
                    outObjectKey, waterMarkConfigArray, null);
        } else if (FileUtils.AUDIO.equals(FileUtils.getGatherName(fileType))) {
            String outObjectKey = getOutObjectKey(AliyunEnum.TRANSCODE.value, fileName, "mp3");
            result = transcodeJob(ossFileDO, AliyunConstant.getMp3TemplateId(inputBucketName), outObjectKey, null,
                    null);
        }
        return result;
    }

    /**
     * 不存在于OSS中的文件转码，要先上传
     *
     * @param fileUrl
     * @param fileName
     * @param fileType
     * @param outFileType
     * @param waterMarkContent
     * @return
     */
    private static String[] transcodeJobNotExistOSS(String fileUrl, String fileName, String fileType,
                                                    String waterMarkContent) {
        LOGGER.info("【aliOSS】不存在于OSS中的文件转码.<START>.[fileUrl]=" + fileUrl);
        if (FileUtils.VIDEO.equals(FileUtils.getGatherName(fileType))) {
            return submitTranscodeJobMp4Ex(fileName, fileUrl);
        } else if (FileUtils.AUDIO.equals(FileUtils.getGatherName(fileType))) {
            return submitTranscodeJobMp3Ex(fileName, fileUrl);
        }
        return null;
    }

    /**
     * 获取文件所在的BUCKET名称
     *
     * @param fileUrl
     * @return
     */
    private static String getInputBucketName(String fileUrl) {
        String inputBucketName = AliyunConstant.getBucketName(fileUrl);
        if (AliyunConstant.FILE_BUCKET.equals(inputBucketName)) {
            inputBucketName = fileUrlExist001(fileUrl) ? AliyunConstant.INPUT_BUCKET : inputBucketName;
        }
        return inputBucketName;
    }

    /**
     * 视频文件合并,异步方法
     *
     * @param fileUrl   主文件地址
     * @param mergeUrls 需要合并的子文件列表,最多支持4个
     * @return
     * @throws FileException
     */
    public static String submitMergeJobMp4(String fileUrl, List<String> mergeUrls) throws FileException {
        LOGGER.info("【aliOSS】提交MP4合并作业.<START>.[fileUrl]=" + fileUrl + ",[mergeUrls]=" + mergeUrls);
        String fileType = FileUtils.getFileType(fileUrl);
        if (!FileUtils.VIDEO.equals(FileUtils.getGatherName(fileType)) || ListUtils.isEmpty(mergeUrls)) {
            throw new FileException(FileException.FILE_TYPE_ERROR, "不是有效的视频文件");
        }
        OSSFileDO ossFileDO = null;
        String inputBucketName = null;
        if (StringTools.contains(fileUrl, AliyunConstant.OSS_CDN_URLS)) {
            String inObjectKey = splitObjectKey(fileUrl);
            inputBucketName = AliyunConstant.getBucketName(fileUrl);
            ossFileDO = new OSSFileDO(AliyunConstant.getOssRegion(inputBucketName), inputBucketName, inObjectKey);
        } else {
            inputBucketName = AliyunConstant.FILE_BUCKET;
            String inObjectKey = getInObjectKey(AliyunEnum.MERGE.value, null, fileType);
            ossFileDO = uploadPointFile2OSS(fileUrl, inputBucketName, inObjectKey);
        }
        String outObjectKey = getOutObjectKey(AliyunEnum.MERGE.value, null, "mp4");
        String[] result = transcodeJob(ossFileDO, AliyunConstant.getMp4TemplateId(inputBucketName), outObjectKey, null,
                mergeUrls);
        LOGGER.info("【aliOSS】提交MP4合并作业.<END>.[result]=" + JSON.toJSONString(result));
        return result[0];
    }

    /**
     * 视频文件合并,同步方法
     *
     * @param fileUrl   主文件地址
     * @param mergeUrls 需要合并的子文件列表,最多支持4个
     * @return
     * @throws FileException
     */
    public static UploadResultInfo submitMergeJobMp4Sync(String fileUrl, List<String> mergeUrls) throws FileException {
        String bucketName = AliyunConstant.getBucketName(fileUrl);
        String jobId = submitMergeJobMp4(fileUrl, mergeUrls);
        return transcodeJobResultSync(jobId, bucketName);
    }

    /**
     * 同步的方式获取转码后的结果
     *
     * @param jobId
     * @return
     * @throws FileException
     */
    private static UploadResultInfo transcodeJobResultSync(String jobId, String bucketName) throws FileException {
        LOGGER.info("【aliOSS】同步获取转码后的结果.<START>.[jobId]=" + jobId + ",[bucketName]=" + bucketName);
        int total = 0;
        while (true) {
            // 如果超过2小时,认定转换失败
            if (total > 2 * 60 * 60) {
                return null;
            }
            UploadResultInfo uploadResultInfo = OssUtils.getOSSResult(jobId, bucketName);
            if (uploadResultInfo != null) {
                return uploadResultInfo;
            }
            try {
                Thread.sleep(3 * 1000);
            } catch (Exception e) {
                LOGGER.error("【文件转码】 合并视频线程进入休眠状态失败: " + e.getMessage(), e);
                throw new FileException(FileException.FILE_CONVERT_FAIL, "视频文件合并失败");
            }
            total += 3;
        }
    }

    /**
     * 转码作业
     *
     * @param inputFile            输入文件
     * @param templateId           转码模板
     * @param outObjectKey         转码输出对象地址
     * @param waterMarkConfigArray 水印参数
     * @return
     * @throws FileException
     */
    private static String[] transcodeJob(OSSFileDO inputFile, String templateId, String outObjectKey,
                                         JSONArray waterMarkConfigArray, List<String> mergeUrls) throws FileException {
        try {
            outObjectKey = URLEncoder.encode(outObjectKey, "utf-8");
        } catch (UnsupportedEncodingException e) {
            LOGGER.error("【aliOSS】submitTranscodeJob URL encode failed:" + e.getMessage(), e);
            throw new FileException(FileException.FILE_CONVERT_FAIL, "submitTranscodeJob URL encode failed");
        }
        String inputBucketName = inputFile.getBucket();
        String outputBucketName = AliyunConstant.getOutBucket(inputBucketName);
        // 组装转码参数
        JSONObject jobConfig = new JSONObject();
        jobConfig.put("OutputObject", outObjectKey.replace(".m3u8", ""));
        jobConfig.put("TemplateId", templateId);
        // 添加水印
        if (waterMarkConfigArray != null) {
            jobConfig.put("WaterMarks", waterMarkConfigArray);
        }
        // 合并多个视频文件
        JSONArray mergeJson = generateMergeInput(mergeUrls, inputBucketName);
        if (!ListUtils.isEmpty(mergeJson)) {
            jobConfig.put("MergeList", mergeJson.toJSONString());
        }
        JSONArray jobConfigArray = new JSONArray();
        jobConfigArray.add(jobConfig);
        SubmitJobsRequest request = new SubmitJobsRequest();
        request.setInput(inputFile.toJsonString());
        request.setOutputBucket(outputBucketName);
        request.setOutputs(jobConfigArray.toJSONString());
        request.setPipelineId(AliyunConstant.getPipelineId(inputBucketName));
        request.setOutputLocation(AliyunConstant.getOssRegion(inputBucketName));
        Integer outputJobCount = 1;
        SubmitJobsResponse response = null;
        String[] result = new String[2];
        try {
            response = getAcsClient(inputBucketName).getAcsResponse(request);
            if (response.getJobResultList().size() != outputJobCount) {
                throw new RuntimeException("SubmitJobsRequest Size failed");
            }
            com.aliyuncs.mts.model.v20140618.SubmitJobsResponse.JobResult.Job job = response.getJobResultList().get(0)
                    .getJob();
            result[0] = job.getJobId();
            result[1] = getOSSUrl(URLDecoder.decode(outObjectKey, "utf-8"), outputBucketName);
        } catch (Exception e) {
            LOGGER.error("【aliOSS】【ignoreLog】转码作业失败:" + e.getMessage(), e);
            throw new FileException(FileException.FILE_CONVERT_FAIL, "转码作业失败");
        }
        return result;
    }

    /**
     * 组装视频合并参数
     *
     * @param text
     * @return
     */
    private static JSONArray generateMergeInput(List<String> mergeUrls, String bucketName) {
        if (ListUtils.isEmpty(mergeUrls)) {
            return null;
        }
        JSONObject mergeInput = new JSONObject();
        JSONArray mergeJsons = new JSONArray();
        for (String mergeUrl : mergeUrls) {
            // 如果是本地文件先上传
            if (!StringTools.startsWith(mergeUrl, new String[]{"http://", "https://"})) {
                UploadResultInfo uploadResultInfo = uploadLocalFile(mergeUrl, null);
                if (!uploadResultInfo.isFlag()) {
                    continue;
                }
                // 替换路径为上传后的文件路径
                mergeUrl = uploadResultInfo.getUrl();
            }
            // 地址域名转换
            String ossOriginalUrl = AliyunConstant.getOSSOriginalUrl(bucketName);
            mergeUrl = StringTools.replace(mergeUrl, AliyunConstant.OSS_CDN_URLS, ossOriginalUrl);
            JSONObject mergeUrlJson = new JSONObject();
            mergeUrlJson.put("MergeURL", mergeUrl);
            mergeJsons.add(mergeUrlJson);
        }
        mergeInput.put("MergeList", mergeJsons);
        return mergeJsons;
    }

    /**
     * 组装视频水印参数
     *
     * @param text
     * @return
     */
    private static JSONArray getWaterMarkConfig(String content) {
        if (StringUtil.isEmpty(content)) {
            return null;
        }
        JSONObject waterMarkConfig = new JSONObject();
        waterMarkConfig.put("Type", "Text");
        // 组装水印文本
        JSONObject waterMarkText = new JSONObject();
        waterMarkText.put("content", new String(Base64.encodeBase64(content.getBytes())));
        waterMarkText.put("FontSize", 16);
        waterMarkText.put("Top", 2000);
        waterMarkText.put("Left", 20);
        waterMarkConfig.put("TextWaterMark", waterMarkText);
        JSONArray waterMarkConfigArray = new JSONArray();
        waterMarkConfigArray.add(waterMarkConfig);
        return waterMarkConfigArray;
    }

    /**
     * 提交视频剪切作业
     *
     * @param fileName
     * @param filePath
     * @param seek     开始时间：［0.000,86399.999］
     * @param duration 持续时长 ［0.000,86399.999］
     * @return
     * @throws FileException
     */
    public static String submitClipJob(String fileName, String fileUrl, String seek, String duration)
            throws FileException {
        LOGGER.info("【aliOSS】提交音视频剪切作业.<START>.[fileUrl]=" + fileUrl + ",[seek]=" + seek + ",[duration]=" + duration);
        if (StringUtil.isEmpty(duration)) {
            return null;
        }
        String fileType = FileUtils.getFileType(fileUrl);
        String gatherName = FileUtils.getGatherName(fileType);
        if (!(FileUtils.VIDEO.equals(gatherName) || FileUtils.AUDIO.equals(gatherName))) {
            return null;
        }
        String resultFileUrl = "";
        String outFileType = FileUtils.VIDEO.equals(gatherName) ? "mp4" : "mp3";
        if (StringTools.contains(fileUrl, AliyunConstant.OSS_CDN_URLS)) {
            String inObjectKey = splitObjectKey(fileUrl);
            String inBucketName = fileUrlExist001(fileUrl) ? AliyunConstant.INPUT_BUCKET
                    : AliyunConstant.getBucketName(fileUrl);
            OSSFileDO ossFileDO = new OSSFileDO(AliyunConstant.getOssRegion(inBucketName), inBucketName, inObjectKey);
            String outObjectKey = getOutObjectKey(AliyunEnum.CUT.value, fileName, outFileType);
            resultFileUrl = clipJob(ossFileDO, outObjectKey, seek, duration);
        } else {
            String inObjectKey = getInObjectKey(AliyunEnum.CUT.value, fileName, fileType);
            OSSFileDO ossFileDO = uploadPointFile2OSS(fileUrl, AliyunConstant.FILE_BUCKET, inObjectKey);
            String outObjectKey = getOutObjectKey(AliyunEnum.CUT.value, fileName, outFileType);
            resultFileUrl = clipJob(ossFileDO, outObjectKey, seek, duration);
        }
        LOGGER.info("【aliOSS】提交音视频剪切作业.<END>.[resultFileUrl]=" + resultFileUrl);
        return resultFileUrl;
    }

    /**
     * 提交视频剪切作业
     *
     * @param buff
     * @param fileName
     * @param fileType
     * @param seek     开始时间：［0.000,86399.999］
     * @param duration 持续时长 ［0.000,86399.999］
     * @return
     * @throws FileException
     */
    public static String submitClipJob(byte[] buff, String fileName, String fileType, String seek, String duration)
            throws FileException {
        LOGGER.info("【aliOSS】提交音视频剪切作业.<START>.[fileName]=" + fileName + ",[seek]=" + seek + ",[duration]=" + duration);
        fileName = MD5.getMD5StrLower(fileName);
        String inObjectKey = getInObjectKey(AliyunEnum.CUT.value, fileName, fileType);
        OSSFileDO inputFile = uploadFileStream2OSS(new ByteArrayInputStream(buff), AliyunConstant.FILE_BUCKET,
                inObjectKey);
        String outObjectKey = getOutObjectKey(AliyunEnum.CUT.value, fileName, "mp4");
        String resultFileUrl = clipJob(inputFile, outObjectKey, seek, duration);
        LOGGER.info("【aliOSS】提交音视频剪切作业.<END>.[resultFileUrl]=" + resultFileUrl);
        return resultFileUrl;
    }

    /**
     * 视频剪切作业
     *
     * @param inputFile
     * @param outObjectKey
     * @param seek         开始时间：［0.000,86399.999］
     * @param duration     持续时长 ［0.000,86399.999］
     * @return
     * @throws FileException
     */
    private static String clipJob(OSSFileDO inputFile, String outObjectKey, String seek, String duration)
            throws FileException {
        try {
            outObjectKey = URLEncoder.encode(outObjectKey, "utf-8");
        } catch (UnsupportedEncodingException e) {
            LOGGER.error("【aliOSS】cutJob URL encode failed:" + e.getMessage(), e);
            throw new FileException(FileException.FILE_CONVERT_FAIL, "cutJob URL encode failed");
        }
        String inputBucketName = inputFile.getBucket();
        String outputBucketName = AliyunConstant.getOutBucket(inputBucketName);
        // 组装转码参数
        JSONObject jobConfig = new JSONObject();
        jobConfig.put("OutputObject", outObjectKey);
        jobConfig.put("TemplateId", AliyunConstant.getMp4TemplateId(inputBucketName));
        jobConfig.put("Clip", generateClipInput(seek, duration));
        JSONArray jobConfigArray = new JSONArray();
        jobConfigArray.add(jobConfig);
        // 组装输出参数
        SubmitJobsRequest request = new SubmitJobsRequest();
        request.setInput(inputFile.toJsonString());
        request.setOutputBucket(outputBucketName);
        request.setOutputs(jobConfigArray.toJSONString());
        request.setPipelineId(AliyunConstant.getPipelineId(inputBucketName));
        request.setOutputLocation(AliyunConstant.getOssRegion(inputBucketName));
        Integer outputJobCount = 1;

        String object = null;
        try {
            SubmitJobsResponse response = getAcsClient(inputBucketName).getAcsResponse(request);
            if (response.getJobResultList().size() != outputJobCount) {
                throw new RuntimeException("SubmitJobsRequest Size failed");
            }
            object = response.getJobResultList().get(0).getJob().getOutput().getOutputFile().getObject();
            object = URLDecoder.decode(object, "UTF-8");
        } catch (Exception e) {
            LOGGER.error("【aliOSS】视频剪辑作业失败:" + e.getMessage(), e);
        }
        return getOSSUrl(object, outputBucketName);
    }

    /**
     * 组装剪切参数
     *
     * @param seek     开始时间：sssss[.SSS]
     * @param duration 持续时长 sssss[.SSS]
     * @return
     */
    private static JSONObject generateClipInput(String seek, String duration) {
        LOGGER.info("【aliOSS】组装剪切参数.<START>.[seek]=" + seek + ",[duration]=" + duration);
        JSONObject input = new JSONObject();
        input.put("Seek", seek);
        input.put("Duration", duration);
        JSONObject timeSpanObject = new JSONObject();
        timeSpanObject.put("TimeSpan", input);
        LOGGER.info("【aliOSS】组装剪切参数.<END>");
        return timeSpanObject;
    }

    /**
     * 提交视频截图任务,成功返回图片地址 <br>
     *
     * @param fileUrl
     * @param time    秒
     * @return 成功:图片地址,失败:null
     */
    public static String submitSnapshotJob(String fileUrl, int time) {
        LOGGER.info("【aliOSS】提交MP4截图作业,<START>.[fileUrl]=" + fileUrl);
        String fileType = FileUtils.getFileType(fileUrl);
        if (!FileUtils.VIDEO.equals(FileUtils.getGatherName(fileType))) {
            return null;
        }
        if (!StringTools.contains(fileUrl, AliyunConstant.OSS_CDN_URLS)) {
            return null;
        }
        String inputBucketName = AliyunConstant.getBucketName(fileUrl);
        String objectKey = splitObjectKey(fileUrl);
        try {
            SubmitSnapshotJobRequest request = new SubmitSnapshotJobRequest();
            request.setInput(generateSnapshotInput(inputBucketName, objectKey).toJSONString());
            String outObjectKey = getOutObjectKey(AliyunEnum.SNAPSHOT.value, null, ImageTypeEnum.JPG.value);
            String outputBucketName = AliyunConstant.FILE_BUCKET;
            request.setSnapshotConfig(generateSnapshotConfig(outputBucketName, outObjectKey, time).toJSONString());
            request.setPipelineId(AliyunConstant.getPipelineId(inputBucketName));
            SnapshotJob snapshotJob = getAcsClient(inputBucketName).getAcsResponse(request).getSnapshotJob();
            if ("Success".equalsIgnoreCase(snapshotJob.getState())) {
                String outObject = URLDecoder.decode(snapshotJob.getSnapshotConfig().getOutputFile().getObject(),
                        "UTF-8");
                return getOSSUrl(outObject, outputBucketName);
            }
        } catch (Exception e) {
            LOGGER.error("【aliOSS】视频截图失败:" + e.getMessage(), e);
        }
        return null;
    }

    /**
     * 组装截图参数
     *
     * @param osslocation
     * @param outputobject
     * @param time
     * @return
     */
    private static JSONObject generateSnapshotConfig(String bucketName, String outputobject, int time) {
        JSONObject snapshotConfig = new JSONObject();
        JSONObject output = generateSnapshotInput(bucketName, outputobject);
        snapshotConfig.put("OutputFile", output);
        time = Math.abs(time);
        snapshotConfig.put("Time", (time == 0 ? 1 : time) * 1000);
        // 截图类型，普通帧：normal，I帧：intra，默认值intra
        snapshotConfig.put("FrameType", "normal");
        return snapshotConfig;
    }

    /**
     * 组装截图参数
     *
     * @param ossLocation
     * @param inputObject
     * @return
     */
    private static JSONObject generateSnapshotInput(String bucketName, String inputObject) {
        JSONObject input = new JSONObject();
        input.put("Location", AliyunConstant.OSS_REGION);
        input.put("Bucket", bucketName);
        input.put("Object", inputObject);
        return input;
    }

    /**
     * 组装转换后的地址(华东地区)
     *
     * @param bucket
     * @param object
     * @return
     */
    public static UploadResultInfo getOSSResult(String transcodeJobId) throws FileException {
        LOGGER.info("【aliOSS】组装转换后的地址(华东地区),<START>.[transcodeJobId]=" + transcodeJobId);
        return getOSSResult(transcodeJobId, AliyunConstant.FILE_BUCKET);
    }

    /**
     * 组装转换后的地址,根据BUCKETNAME
     *
     * @param transcodeJobId
     * @return
     * @throws FileException
     */
    public static UploadResultInfo getOSSResultByRegion(String transcodeJobId, String buckectName)
            throws FileException {
        if (AliyunConstant.LIVE_BUCKET.equals(buckectName))
            return getOSSResult(transcodeJobId, AliyunConstant.LIVE_BUCKET);
        else
            return getOSSResult(transcodeJobId, AliyunConstant.FILE_BUCKET);
    }

    /**
     * 组装转换后的地址
     *
     * @param transcodeJobId
     * @return
     * @throws FileException
     */
    private static UploadResultInfo getOSSResult(String transcodeJobId, String outputBucketName) throws FileException {
        if (StringUtil.isEmpty(transcodeJobId)) {
            return new UploadResultInfo("ali", "ali转码作业失败[transcodeJobId=null]", false);
        }
        Job transcodeJob = getTranscodeJob(transcodeJobId, outputBucketName);
        return explainTranscodeResult(transcodeJob);
    }

    /**
     * 获取转换作业情况
     *
     * @param transcodeJobId
     * @return
     */
    private static Job getTranscodeJob(String transcodeJobId, String bucketName) throws FileException {
        QueryJobListRequest request = new QueryJobListRequest();
        request.setJobIds(transcodeJobId);
        QueryJobListResponse response = null;
        try {
            response = getAcsClient(bucketName).getAcsResponse(request);
            return response.getJobList().get(0);
        } catch (Exception e) {
            LOGGER.error("【aliOSS】获取转换作业情况失败:" + e.getMessage(), e);
            throw new FileException(FileException.FILE_CONVERT_FAIL, "获取转换作业情况失败");
        }
    }

    /**
     * 解析转换后的结果
     *
     * @param transcodeJob
     * @return
     */
    private static UploadResultInfo explainTranscodeResult(Job transcodeJob) {
        String status = transcodeJob.getState();
        if ("TranscodeFail".equals(status)) {
            LOGGER.warn("【aliOSS】转换作业失败:[jobId]" + transcodeJob.getJobId() + ",[status]=" + status);
            return new UploadResultInfo("ali", "ali转码作业失败", false);
        }
        if ("TranscodeSuccess".equals(status)) {
            com.aliyuncs.mts.model.v20140618.QueryJobListResponse.Job.Output output = transcodeJob.getOutput();
            String bucketName = output.getOutputFile().getBucket();
            String objectKey = null;
            try {
                objectKey = URLDecoder.decode(output.getOutputFile().getObject(), "UTF-8");
            } catch (Exception e) {
                LOGGER.error("【aliOSS】组装转换后的地址失败:" + e.getMessage(), e);
                return new UploadResultInfo("ali", "ali转码作业失败", false);
            }
            // 文件大小
            String size = output.getProperties().getFormat().getSize();
            Long fileSize = StringUtil.isEmpty(size) ? null : Long.parseLong(size);
            // 视频时长
            List<VideoStream> videoStreams = output.getProperties().getStreams().getVideoStreamList();
            String duration = null;
            if (!ListUtils.isEmpty(videoStreams)) {
                duration = videoStreams.get(0).getDuration();
            }
            // 音频时长
            List<AudioStream> audioStreams = output.getProperties().getStreams().getAudioStreamList();
            duration = null;
            if (!ListUtils.isEmpty(audioStreams)) {
                duration = audioStreams.get(0).getDuration();
            }
            return getResultInfo(objectKey, fileSize, duration, bucketName);
        }
        return null;
    }

    /**
     * 获取媒体信息，仅限经过转码的音视频文件
     *
     * @param ossUrl
     * @return
     * @throws FileException
     */
    public static OssMediaInfoDTO getMediaInfo(String fileUrl) throws FileException {
        LOGGER.info("【aliOSS】获取媒体信息，仅限经过转码的音视频文件,<START>.[fileUrl]=" + fileUrl);
        if (StringUtil.isEmpty(fileUrl)) {
            return null;
        }
        String objectKey = splitObjectKey(fileUrl);
        String bucketName = AliyunConstant.getBucketName(fileUrl);
        SubmitMediaInfoJobRequest request = new SubmitMediaInfoJobRequest();
        OSSFileDO ossFileDO = new OSSFileDO(AliyunConstant.getOssRegion(bucketName), bucketName, objectKey);
        request.setInput(ossFileDO.toJsonString());
        SubmitMediaInfoJobResponse response = null;
        try {
            response = getAcsClient(bucketName).getAcsResponse(request);
        } catch (Exception e) {
            LOGGER.error("【aliOSS】获取媒体信息失败:" + e.getMessage(), e);
        }
        if (response == null) {
            return null;
        }
        // 封装返回对象
        Format format = response.getMediaInfoJob().getProperties().getFormat();
        OssMediaInfoDTO ossMediaInfoDTO = new OssMediaInfoDTO();
        ossMediaInfoDTO.setBucket(bucketName);
        ossMediaInfoDTO.setLocation(AliyunConstant.getOssRegion(bucketName));
        ossMediaInfoDTO.setObejctKey(objectKey);
        ossMediaInfoDTO.setSize(StringUtil.isEmpty(format.getSize()) ? 0L : Long.parseLong(format.getSize()));
        ossMediaInfoDTO.setDuration(
                StringUtil.isEmpty(format.getDuration()) ? 0 : new Double(format.getDuration()).intValue());
        LOGGER.info("【aliOSS】获取媒体信息，仅限经过转码的音视频文件,<END>.[ossMediaInfoDTO]=" + ossMediaInfoDTO);
        return ossMediaInfoDTO;
    }

    /**
     * 获取文件的元信息
     *
     * @param objectKey
     * @return
     */
    public static ObjectMetadata getObjectMetadata(String objectKey, String bucketName) throws FileException {
        // 创建OSSClient实例
        OSSClient ossClient = getOSSClient(bucketName);
        ObjectMetadata objectMetadata = ossClient.getObjectMetadata(bucketName, objectKey);
        // 关闭client
        ossClient.shutdown();
        return objectMetadata;
    }

    /**
     * 下载文件成byte[]
     *
     * @param objectKey
     * @return
     * @throws FileException
     */
    public static byte[] downloadFile2Byte(String fileUrl) throws FileException {
        String objectKey = splitObjectKey(fileUrl);
        String bucketName = AliyunConstant.getBucketName(fileUrl);
        // OSSClient实例
        OSSClient ossClient = null;
        InputStream is = null;
        ByteArrayOutputStream os = null;
        byte[] result = null;
        try {
            ossClient = getOSSClient(bucketName);
            OSSObject ossObject = ossClient.getObject(bucketName, objectKey);
            is = ossObject.getObjectContent();
            os = new ByteArrayOutputStream();
            byte[] buff = new byte[1024];
            int rc = 0;
            while ((rc = is.read(buff, 0, 1024)) != -1) {
                os.write(buff, 0, rc);
            }
            result = os.toByteArray();

        } catch (Exception e) {
            LOGGER.error("【aliOSS】下载文件失败:" + e.getMessage(), e);
            throw new FileException(FileException.FILE_DOWNLOAD_FAILURE, "下载文件失败");
        } finally {
            try {
                if (is != null)
                    is.close();
                if (os != null)
                    os.close();
                if (ossClient != null)
                    ossClient.shutdown();
            } catch (Exception e) {
                LOGGER.error("【aliOSS】下载文件，关闭文件流失败:" + e.getMessage(), e);
            }
        }
        return result;
    }

    /**
     * 下载文件到本地
     *
     * @param objectKey
     * @param filePath
     */
    public static void downloadFile(String fileUrl, String outFilePath) throws FileException {
        String objectKey = splitObjectKey(fileUrl);
        String bucketName = AliyunConstant.getBucketName(fileUrl);
        OSSClient ossClient = null;
        try {
            // 创建OSSClient实例
            ossClient = getOSSClient(bucketName);
            // 下载object到文件
            ossClient.getObject(new GetObjectRequest(bucketName, objectKey), new File(outFilePath));
        } catch (Exception e) {
            LOGGER.error("【aliOSS】下载文件失败:" + e.getMessage(), e);
            throw new FileException(FileException.FILE_DOWNLOAD_FAILURE, "下载文件失败");
        } finally {
            if (ossClient != null)
                ossClient.shutdown();
        }
    }

    /**
     * 组装返回的文件信息 <br>
     * isPrivateKey=true,表示地址可下载,但需要加上密钥,密钥生产使用方法getHashValue或者addUrlKey <br>
     *
     * @param objectKey
     * @param isPrivateKey
     * @return
     */
    private static UploadResultInfo getResultInfo(String objectKey, Long size, String duration,
                                                  String outputBucketName) {
        UploadResultInfo uploadResultInfo = new UploadResultInfo();
        uploadResultInfo.setObjectKey(objectKey);
        uploadResultInfo.setUrl(getOSSUrl(objectKey, outputBucketName));
        if (size == null) {
            uploadResultInfo.setSize(getObjectMetadata(objectKey, outputBucketName).getContentLength());
        } else {
            uploadResultInfo.setSize(size);
        }
        uploadResultInfo.setFileId("ali");
        uploadResultInfo.setFlag(true);
        uploadResultInfo.setFinish(true);
        if (!StringUtil.isEmpty(duration)) {
            uploadResultInfo.setDuration(new Double(duration).intValue());
        }
        return uploadResultInfo;
    }

    /**
     * 组装转换后的地址
     *
     * @param bucket
     * @param object
     * @return
     */
    private static String getOSSUrl(String objectKey, String outputBucketName) {
        if (StringUtil.isEmpty(objectKey)) {
            return null;
        }
        return AliyunConstant.getOSSCDNUrl(outputBucketName) + objectKey;
    }

    /**
     * 从完整的HTTP路径中截取objectName
     *
     * @param objectName
     * @return
     */
    public static String splitObjectKey(String fileUrl) {
        LOGGER.info("【aliOSS】从完整的HTTP路径中截取objectKey,<START>.[fileUrl]=" + fileUrl);
        if (StringUtil.isEmpty(fileUrl)) {
            throw new FileException(FileException.FILE_TYPE_ERROR, "不是有效的文件路径");
        }
        if (!fileUrl.startsWith("http")) {
            return fileUrl;
        }
        if (!StringTools.contains(fileUrl, AliyunConstant.OSS_CDN_URLS)) {
            throw new FileException(FileException.FILE_TYPE_ERROR, "不是有效的文件路径");
        }
        if (fileUrl.contains("?auth_key=")) {
            fileUrl = fileUrl.substring(0, fileUrl.indexOf("?auth_key="));
        }
        if (fileUrl.contains("?x-oss-process=")) {
            fileUrl = fileUrl.substring(0, fileUrl.indexOf("?x-oss-process="));
        }
        if (fileUrl.startsWith("http")) {
            fileUrl = StringTools.replace(fileUrl, AliyunConstant.OSS_CDN_URLS, "");
        }
        LOGGER.info("【aliOSS】从完整的HTTP路径中截取objectKey,<END>.[fileUrl]=" + fileUrl);
        return fileUrl;
    }

    /**
     * 组装输入文件Object路径
     *
     * @param filePath
     * @return
     */
    private static String getInObjectKey(String uploadType, String fileName, String fileType) {
        fileName = StringUtil.isEmpty(fileName) ? UUIDUitl.taskName()
                : (FileUtils.formatName(fileName) + "_" + LocalDateUtils.getYmdhmss());
        String gatherName = FileUtils.getGatherName(fileType);
        gatherName = StringUtil.isEmpty(gatherName) ? "other" : gatherName.toLowerCase();
        return new StringBuilder("oss/").append(uploadType).append("/").append(gatherName).append("/").append(fileType)
                .append("/").append(fileName).append(".").append(fileType).toString();
    }

    /**
     * 组装输出文件Object路径
     *
     * @param uploadType
     * @param fileName
     * @param fileType
     * @return
     */
    private static String getOutObjectKey(String uploadType, String fileName, String fileType) {
        fileName = StringUtil.isEmpty(fileName) ? UUIDUitl.taskName()
                : (FileUtils.formatName(fileName) + "_" + LocalDateUtils.getYmdhmss());
        String gatherName = FileUtils.getGatherName(fileType);
        gatherName = StringUtil.isEmpty(gatherName) ? "other" : gatherName.toLowerCase();
        return new StringBuilder("oss/").append(uploadType).append("/").append(gatherName).append("/").append(fileType)
                .append("/").append(fileName).append(".").append(fileType).toString();
    }

    /**
     * 获取文件下载加密密钥，默认有效期48小时 <br>
     * 密钥需要结合域名https://download.5rs.me使用，可以在浏览器直接下载 <br>
     *
     * @param objectKey
     * @return
     */
    public static String getHashValue(String fileUrl) {
        return getHashValue(fileUrl, AliyunConstant.OSS_DEFAULT_TIME);
    }

    /**
     * 获取文件下载加密密钥，长期有效 <br>
     * 密钥需要结合域名https://download.5rs.me使用，可以在浏览器直接下载 <br>
     *
     * @param fileUrl
     * @return
     */
    public static String getHashValueLong(String fileUrl) {
        return getHashValue(fileUrl, AliyunConstant.OSS_LONG_TIME);
    }

    /**
     * 获取文件下载加密密钥 <br>
     * 密钥需要结合域名https://download.5rs.me使用，可以在浏览器直接下载 <br>
     *
     * @param fileUrl
     * @param second  秒，地址有效期
     * @return
     */
    public static String getHashValue(String fileUrl, long second) {
        String objectKey = splitObjectKey(fileUrl);
        if (!objectKey.startsWith("/")) {
            objectKey = "/" + objectKey;
        }
        // 有效期,默认48小时。为什么要减1800，是因为阿里云比较坑，默默得在有效期上加了1800s，如果不减的话，设置1分钟过期，实际是31分钟后才过期。
        long timestamp = DateUtils.addNowTimeSecond(second) - 1800;
        // 针对路径中含有中文的，要单独Encode编码。是因为阿里云实在太坑了，他们的代码中未对中文进行UTF-8编码，却默认进行了URLEncode编码，导致最后的MD5加密不一直。
        objectKey = ZSEncode.encodeURI(objectKey);
        String key = MD5.getMD5StrLower(objectKey + "-" + timestamp + "-0-0-" + AliyunConstant.OSS_PRIVATE_KEY);
        return "?auth_key=" + timestamp + "-0-0-" + key;
    }

    /**
     * 将文件URL生成带密钥的地址，可以在浏览器直接访问 <br>
     * 默认有效期48小时
     *
     * @param fileUrl 文件URL全路径地址
     * @return
     */
    public static String urlAddKey(String fileUrl) {
        return urlAddKey(fileUrl, AliyunConstant.OSS_DEFAULT_TIME);
    }

    /**
     * 将文件URL生成带密钥的地址，可以在浏览器直接访问 <br>
     * 长期有效
     *
     * @param fileUrl 文件URL全路径地址
     * @return
     */
    public static String urlAddKeyLong(String fileUrl) {
        return urlAddKey(fileUrl, AliyunConstant.OSS_LONG_TIME);
    }

    /**
     * 将文件URL生成带密钥的地址，可以在浏览器直接访问 <br>
     *
     * @param fileUrl 文件URL全路径地址
     * @param second  秒，地址有效期
     * @return
     */
    public static String urlAddKey(String fileUrl, long second) {
        String result = keyCheckFile(fileUrl);
        return ZSEncode.encodeURI(result) + getHashValue(result, second);
    }

    /**
     * 将文件URL生成带密钥的地址，可以在浏览器直接访问(不转义中文) <br>
     *
     * @param fileUrl 文件URL全路径地址
     * @param second  秒，地址有效期
     * @return
     */
    public static String urlAddKeyChina(String fileUrl, long second) {
        String result = keyCheckFile(fileUrl);
        return result + getHashValue(result, second);
    }

    private static String keyCheckFile(String fileUrl) {
        if (StringUtil.isEmpty(fileUrl)) {
            return null;
        }
        if (!(StringTools.contains(fileUrl, AliyunConstant.RAYS_CDN_URLS)
                || fileUrl.contains(AliyunConstant.FILE_CDN_URL_DOWNLOAD))) {
            return fileUrl;
        }
        if (fileUrl.contains("?auth_key=")) {
            return fileUrl;
        }
        // 替换域名为可访问的
        return StringTools.replace(fileUrl, AliyunConstant.RAYS_CDN_URLS, AliyunConstant.FILE_CDN_URL_DOWNLOAD);
    }

    /**
     * 将文件URL生成带密钥的下载地址,返回的地址不带域名 <br>
     * 默认有效期48小时
     *
     * @param fileUrl 文件URL全路径地址
     * @return
     */
    public static String urlAddKey2Sms(String fileUrl) {
        return urlAddKey2Sms(fileUrl, AliyunConstant.OSS_DEFAULT_TIME);
    }

    /**
     * 将文件URL生成带密钥的下载地址,返回的地址不带域名 <br>
     * 长期有效
     *
     * @param fileUrl 文件URL全路径地址
     * @return
     */
    public static String urlAddKeyLong2Sms(String fileUrl) {
        return urlAddKey2Sms(fileUrl, AliyunConstant.OSS_LONG_TIME);
    }

    /**
     * 将文件URL生成带密钥的下载地址,返回的地址不带域名 <br>
     *
     * @param fileUrl 文件URL全路径地址
     * @param second  秒，地址有效期
     * @return
     */
    public static String urlAddKey2Sms(String fileUrl, long second) {
        String keyUrl = urlAddKey(fileUrl, second);
        String shortUrl = UrlUtils.getShortUrl(keyUrl);
        return StringUtil.isEmpty(shortUrl) ? keyUrl : shortUrl.replace(UrlConstant.TB_SHORT_URL, "");
    }

    /**
     * 将文件URL生成带密钥的下载地址,返回的地址不带域名 <br>
     *
     * @param fileUrl
     * @return
     */
    public static String urlAddKeyLong2SmsOwn(String fileUrl) {
        String keyUrl = urlAddKeyChina(fileUrl, AliyunConstant.OSS_LONG_TIME);
        String shortUrl = UrlUtils.getShortUrl4Own(keyUrl);
        return StringUtil.isEmpty(shortUrl) ? keyUrl : shortUrl.replace(UrlConstant.OWN_SHORT_URL, "");
    }

    /**
     * 将文件URL生成带密钥的地址，可以在浏览器直接访问,并转换为短链接<br>
     *
     * @param fileUrl 文件URL全路径地址
     * @return
     */
    public static String urlAddKey2Short(String fileUrl) {
        return urlAddKey2Short(fileUrl, AliyunConstant.OSS_DEFAULT_TIME);
    }

    /**
     * 将文件URL生成带密钥的地址,可以在浏览器直接访问 ,并转换为短链接<br>
     * 长期有效
     *
     * @param fileUrl 文件URL全路径地址
     * @return
     */
    public static String urlAddKeyLong2Short(String fileUrl) {
        return urlAddKey2Short(fileUrl, AliyunConstant.OSS_LONG_TIME);
    }

    /**
     * 将文件URL生成带密钥的地址,可以在浏览器直接访问,并转换为短链接<br>
     *
     * @param fileUrl 文件URL全路径地址
     * @param second  秒，地址有效期
     * @return
     */
    public static String urlAddKey2Short(String fileUrl, long second) {
        String keyUrl = urlAddKeyChina(fileUrl, second);
        String shortUrl = UrlUtils.getShortUrl4Own(keyUrl);
        return StringUtil.isEmpty(shortUrl) ? keyUrl : shortUrl;
    }

    /**
     * 图片增加水印
     *
     * @param fileUrl
     * @return 返回处理后的图片网络地址
     */
    public static String imageWatermark(String fileUrl, String watermarkContent) throws FileException {
        LOGGER.info("【aliOSS】图片增加水印,<START>.[fileUrl]=" + fileUrl + ",[watermarkContent]=" + watermarkContent);
        if (StringUtil.isEmpty(watermarkContent)) {
            throw new FileException(FileException.PARAM_ERROR, "水印内容不能为空");
        }
        ImageUtils.checkIsImage(fileUrl);
        StringBuilder style = new StringBuilder("image/watermark");
        // 水印文本,需要转换成base64格式
        style.append(",text_" + new String(Base64.encodeBase64(watermarkContent.getBytes())));
        // 水印字体大小,位置,偏移量
        style.append(",size_60,g_se,x_20,y_20");
        // 生成新的图片地址
        String localPath = imageHandle(fileUrl, style.toString());
        // 上传处理后的图片,并删除本地文件
        UploadResultInfo uploadResultInfo = uploadLocalFile(localPath, null);
        FileUtils.deleteFile(localPath);
        return uploadResultInfo == null ? null : uploadResultInfo.getUrl();
    }

    /**
     * 图片缩放,按宽度等比例缩放
     *
     * @param fileUrl
     * @param width   目前最大支持1024，超过1024或者小于0都按1024计算
     * @return 返回处理后的图片本地地址
     * @throws FileException
     */
    public static String imageWidthResize(String fileUrl, int width) throws FileException {
        LOGGER.info("【aliOSS】图片缩放,<START>.[fileUrl]=" + fileUrl + ",[width]=" + width);
        ImageUtils.checkIsImage(fileUrl);
        // 图片缩放大小字体大小,偏移量
        if (width < 1 || width > 4096) {
            throw new FileException(FileException.PARAM_ERROR, "缩放后图片的宽度必须介于(0,4096]之间");
        }
        StringBuilder style = new StringBuilder("image/resize");
        style.append(",m_fixed,w_").append(width);
        // 生成新的图片地址
        String localPath = imageHandle(fileUrl, style.toString());
        LOGGER.info("【aliOSS】图片缩放,<START>.[localPath]=" + localPath);
        return localPath;
    }

    /**
     * 图片缩放,按倍数百分比缩放
     *
     * @param fileUrl
     * @param p       1-1000，倍数百分比。 小于100，即是缩小，大于100即是放大。
     * @return 返回处理后的图片本地地址
     * @throws FileException
     */
    public static String imageResize(String fileUrl, int p) throws FileException {
        LOGGER.info("【aliOSS】图片缩放,<START>.[fileUrl]=" + fileUrl + ",[p]=" + p);
        ImageUtils.checkIsImage(fileUrl);
        // 图片缩放大小字体大小,偏移量
        if (p < 1 || p > 1000) {
            p = 100;
        }
        StringBuilder style = new StringBuilder("image/resize");
        style.append(",p_").append(p);
        // 生成新的图片地址
        String localPath = imageHandle(fileUrl, style.toString());
        LOGGER.info("【aliOSS】图片缩放,<START>.[localPath]=" + localPath);
        return localPath;
    }

    /**
     * 图片自动旋转
     *
     * @param fileUrl
     * @param width   图片旋转以后的宽度，最大不能超过4096,传0则按原始宽度旋转
     * @return 返回处理后的图片本地地址
     * @throws FileException
     */
    public static String imageAutoOrient(String fileUrl, int width) throws FileException {
        LOGGER.info("【aliOSS】图片自动旋转,<START>.[fileUrl]=" + fileUrl);
        ImageUtils.checkIsImage(fileUrl);
        // 图片缩放大小字体大小,偏移量
        if (width < 0 || width > 4096) {
            throw new FileException(FileException.PARAM_ERROR, "旋转后的图片的宽度必须介于[0,4096]之间");
        }
        StringBuilder style = new StringBuilder("image");
        if (width > 0) {
            style.append("/resize,w_").append(width);
        }
        style.append("/auto-orient,1");
        // 生成新的图片地址
        String localPath = imageHandle(fileUrl, style.toString());
        LOGGER.info("【aliOSS】图片自动旋转,<START>.[localPath]=" + localPath);
        return localPath;
    }

    /**
     * 图片裁剪,如果图片带有旋转属性，会自动旋转纠正角度后在进行裁剪。
     *
     * @param fileUrl
     * @param width   裁剪的宽度
     * @param height  裁剪的高度
     * @param x       左上角开始， 裁剪的X轴坐标
     * @param y       左上角开始， 裁剪的Y轴坐标
     * @return 返回处理后的图片本地地址
     * @throws FileException
     */
    public static String imageCrop(String fileUrl, int width, int height, int x, int y) throws FileException {
        LOGGER.info("【aliOSS】图片裁剪,<START>.[fileUrl]=" + fileUrl);
        ImageUtils.checkIsImage(fileUrl);
        if (!(width >= 0 && width <= 4096 && height >= 0 && height <= 4096 && x >= 0 && y >= 0)) {
            throw new FileException(FileException.PARAM_ERROR, "裁剪的长宽或者坐标数值必须大于0且长宽必须小于4096");
        }
        StringBuilder style = new StringBuilder("image/auto-orient,1/crop");
        style.append(",w_").append(width);
        style.append(",h_").append(height);
        style.append(",x_").append(x);
        style.append(",y_").append(y);
        // 生成新的图片地址
        String localPath = imageHandle(fileUrl, style.toString());
        LOGGER.info("【aliOSS】图片裁剪,<START>.[localPath]=" + localPath);
        return localPath;
    }

    /**
     * 图片处理
     *
     * @param fileUrl
     * @param style
     */
    private static String imageHandle(String fileUrl, String style) {
        String fileType = FileUtils.getFileType(fileUrl);
        String key = splitObjectKey(fileUrl);
        String bucketName = AliyunConstant.getBucketName(fileUrl);
        GetObjectRequest request = new GetObjectRequest(bucketName, key);
        request.setProcess(style);
        // 创建OSSClient实例
        OSSClient ossClient = getOSSClient(bucketName);
        // 生成新的图片地址
        String localPath = FilePathConst.IMAGE_PATH + UUIDUitl.generateString(32) + "." + fileType;
        FileUtils.creatFiles(localPath);
        try {
            ossClient.getObject(request, new File(localPath));
        } catch (Exception e) {
            FileUtils.deleteFile(localPath);
            LOGGER.error("【aliOSS】图片处理API:" + e.getMessage(), e);
        } finally {
            ossClient.shutdown();
        }
        return localPath;
    }

    /**
     * 图片增加水印(文本)
     *
     * @param fileUrl          原图地址
     * @param fontSize         字体大小
     * @param watermarkContent 水印内容
     * @return
     * @throws FileException
     */
    public static String imageWatermark4Text(String fileUrl, Integer fontSize, String watermarkContent)
            throws FileException {
        LOGGER.info("【aliOSS】图片增加水印(文本),<START>.[fileUrl]=" + fileUrl + ",[fontSize]=" + fontSize
                + ",[watermarkContent]=" + watermarkContent);
        if (StringUtil.isEmpty(watermarkContent)) {
            throw new FileException(FileException.PARAM_ERROR, "水印内容不能为空");
        }
        StringBuilder style = new StringBuilder("image/watermark");
        // 水印字体大小
        fontSize = caleImageFontSize(fileUrl, fontSize);
        // 水印文本
        watermarkContent = "©" + (watermarkContent.length() > 7 ? watermarkContent.substring(0, 7) : watermarkContent);
        style.append(",text_" + new String(Base64.encodeBase64URLSafe(watermarkContent.getBytes())));
        // 水印字体大小
        style.append(",size_").append(fontSize);
        // 水印颜色，位置，偏移量
        style.append(",color_FFFFFF,shadow_100,g_sw,x_10,y_10");
        String result = imageWatermarkHandle(fileUrl, style.toString());
        LOGGER.info("【aliOSS】图片增加水印(文本),<END>.[result]=" + result);
        return result;
    }

    /**
     * 图片增加水印(图片)
     *
     * @param fileUrl           原图地址
     * @param watermarkImageUrl 水印图片地址
     * @return
     * @throws FileException
     */
    public static String imageWatermark4Image(String fileUrl, String watermarkImageUrl) throws FileException {
        LOGGER.info("【aliOSS】图片增加水印(图片),<START>.[fileUrl]=" + fileUrl + ",[watermarkImageUrl]=" + watermarkImageUrl);
        if (StringUtil.isEmpty(watermarkImageUrl)) {
            throw new FileException(FileException.PARAM_ERROR, "水印内容不能为空");
        }
        StringBuilder style = new StringBuilder("image/watermark");
        // 水印图片地址,按主图大小的10%缩放
        watermarkImageUrl = splitObjectKey(watermarkImageUrl) + "?x-oss-process=image/resize,P_10";
        style.append(",image_" + new String(Base64.encodeBase64URLSafe(watermarkImageUrl.getBytes())));
        // 水印颜色，位置，偏移量
        style.append(",g_nw,x_20,y_10");
        System.out.println(style.toString());
        String result = imageWatermarkHandle(fileUrl, style.toString());
        LOGGER.info("【aliOSS】图片增加水印(图片),<END>.[result]=" + result);
        return result;
    }

    /**
     * 图片水印处理
     *
     * @param fileUrl
     * @param style
     */
    private static String imageWatermarkHandle(String fileUrl, String style) {
        String fileType = FileUtils.getFileType(fileUrl);
        // 检查文件合法性
        fileUrl = checkFileUrl(fileUrl, fileType);
        String key = splitObjectKey(fileUrl);
        String bucketName = AliyunConstant.getBucketName(fileUrl);
        GetObjectRequest request = new GetObjectRequest(bucketName, key);
        request.setProcess(style.toString());
        // 创建OSSClient实例
        OSSClient ossClient = getOSSClient(bucketName);
        // 生成带水印的本地图片地址
        String localPath = FilePathConst.IMAGE_WATERMARK_PATH + UUIDUitl.generateString(32) + "." + fileType;
        FileUtils.creatFiles(localPath);
        ossClient.getObject(request, new File(localPath));
        ossClient.shutdown();
        // 上传处理后的图片,并删除本地文件
        UploadResultInfo uploadResultInfo = uploadLocalFile(localPath, null);
        FileUtils.deleteFile(localPath);
        return uploadResultInfo == null ? null : uploadResultInfo.getUrl();
    }

    /**
     * 检查文件合法性，如果是本地文件先上传
     *
     * @param fileUrl
     * @param fileType
     * @return
     */
    private static String checkFileUrl(String fileUrl, String fileType) throws BizException {
        LOGGER.info("【aliOSS】检查文件合法性,<START>.[fileUrl]=" + fileUrl);
        if (!FileUtils.IMAGE.equals(FileUtils.getGatherName(fileType))) {
            throw new FileException(FileException.FILE_TYPE_ERROR, "不是有效的文件");
        }
        // 如果是本地文件先上传
        if (!fileUrl.startsWith("http")) {
            UploadResultInfo uploadResultInfo = uploadLocalFile(fileUrl, null);
            if (uploadResultInfo != null && uploadResultInfo.isFlag()) {
                fileUrl = uploadResultInfo.getUrl();
            } else {
                throw new FileException(FileException.FILE_CONTENT_NOT_EXIST, "不是有效文件");
            }
        }
        LOGGER.info("【aliOSS】检查文件合法性,<END>.[fileUrl]=" + fileUrl);
        return fileUrl;
    }

    /**
     * 计算图片水印文字大小
     *
     * @param fileUrl
     * @param fontSize
     * @return
     */
    private static Integer caleImageFontSize(String fileUrl, Integer fontSize) {
        LOGGER.info("【aliOSS】计算图片水印文字大小,<START>.[fileUrl]=" + fileUrl + ",[fontSize]=" + fontSize);
        if (fontSize != null && fontSize > 1 && fontSize < 100) {
            return fontSize;
        }
        // 计算字体大小
        float imageWidth = ImageUtils.getWidthSize(fileUrl);
        Float result = imageWidth == 0 ? 18 : imageWidth / 750 * 18;
        LOGGER.info("【aliOSS】计算图片水印文字大小,<END>.[result]=" + result);
        return Math.round(result);
    }

    /**
     * 获取OSS客户端
     *
     * @return
     */
    private static OSSClient getOSSClient(String bucketName) {
        return new OSSClient(AliyunConstant.getOSSEndPoint(bucketName), AliyunConstant.MAIN_ACCESS_KEY_ID,
                AliyunConstant.MAIN_ACCESS_KEY_SECRET);
    }

    /**
     * 判断object是否存在
     *
     * @param fileUrl
     * @return
     */
    private static boolean doesObjectExist(String bucketName, String objectKey) {
        OSSClient ossClient = getOSSClient(bucketName);
        boolean found = ossClient.doesObjectExist(bucketName, objectKey);
        // 关闭client
        ossClient.shutdown();
        return found;
    }

    /**
     * 判断文件是存在001中
     *
     * @param fileUrl
     * @return true:001,false:002
     */
    private static boolean fileUrlExist001(String fileUrl) {
        boolean isUploadfe = false;
        String objectKey = splitObjectKey(fileUrl);
        if (doesObjectExist(AliyunConstant.INPUT_BUCKET, objectKey)) {
            isUploadfe = true;
        }
        return isUploadfe;
    }

}
