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);
	}

	/**
	 * 父子文件上传，子文件全路径和父文件相同，文件后缀类型自定义
	 * 
	 * @param filePath
	 *            子文件本地路径
	 * @param parentFileUrl
	 *            仅限OSS的全路径，其他本地文件路径均不支持
	 * @return
	 * @throws FileException
	 */
	public static UploadResultInfo uploadLocalFile4Child(String filePath, String parentFileUrl) throws FileException {
		String fileType = FileUtils.getFileType(filePath);
		String outObjectKey = splitObjectKey(parentFileUrl) + "." + 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 (fileUrl.startsWith("oss/")) {
			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
	 */
	public static boolean fileUrlExist001(String fileUrl) {
		boolean isUploadfe = false;
		String objectKey = splitObjectKey(fileUrl);
		if (doesObjectExist(AliyunConstant.INPUT_BUCKET, objectKey)) {
			isUploadfe = true;
		}
		return isUploadfe;
	}

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

}
