七牛云上传文件

发布于:2024-06-26 ⋅ 阅读:(16) ⋅ 点赞:(0)
package com.jimi.tracker.util.qiniu;

import com.alibaba.fastjson.JSONObject;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.xxx.route.client.pool.RouteClient;
import com.xxx.tracker.trans.netty.handler.AbstractHandler;
import com.xxx.tracker.util.ByteUtil;
import com.xxx.tracker.util.ConfigUtil;
import com.xxx.tracker.util.Des;
import com.xxx.tracker.util.cache.DcImeiAppIdCache;
import com.xxx.tracker.util.metrics.MonitorMetrics;
import com.xxx.utils.StringUtils;
import com.xxx.utils.Tuple;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.util.Auth;
import com.qiniu.util.StringMap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.AttributeKey;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.*;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Description:
 * <p>
 * 上传文件,数据包大小最多4M,超过4M建议外挂磁盘,数据写入本地,防止内存暂用,在用Qiniu云本身内部对大文件做分片上传(会有断点续传功能)
 * 线程安全依托于Channel通道,一个通道连接数据包上传是线程安全的,文件上传做异步处理,不阻塞Netty主线程
 * </p>
 *
 * @Author: leo.xiong
 * @CreateDate: 2024/4/22 10:09
 * @Email: xionglang@xxx.com
 * @Since:
 */
public class QiniuUtil {
    private static final Logger log = LoggerFactory.getLogger(QiniuUtil.class);
    /**
     * 超过4M,七牛云上传文件会做断点续传
     */
    private static final String QINIU_LOCAL_TEM_DIRECTORY = "qiniu.local.tmp.directory";
    /**
     * 使用本地落盘操作,数据包必须要顺序
     */
    private static final String QINIU_LOCAL_ORDERING = "qiniu.local.ordering";
    private static final String MAX_FILE_LENGTH_KEY = "max.file.length";
    private static final String HTTP = "http://";
    private static final String HTTPS = "https://";
    private static final String QINIU_ACCESS_KEY = "qiniu.accessKey";
    private static final String QINIU_SECRET_KEY = "qiniu.secretKey";
    /**
     * APP ID 不同,配置可能不同
     */
    private static final String APP_ID_KEY = "qiniu.app.id";
    private static final String BUCKET = "qiniu.bucket";
    private static final String CALLBACK_HOST = "qiniu.callback.host";
    private static final String CALLBACK_URL = "qiniu.callback.url";
    private static final String VIDEO_CALLBACK_URL = "qiniu.video.callback.url";
    private static final String CALLBACK_BODY = "qiniu.callback.body";
    private static final String CALLBACK_BODY_TYPE = "qiniu.callback.body.type";
    private static final String CALLBACK_DES_KEY = "qiniu.callback.des.key";
    private static final String TOKEN_EXPIRE_SECOND = "qiniu.token.expire.second";
    private static final String DOWN_LOAD_URL = "qiniu.download.url";
    private static final String VIDEO_OPS = "qiniu.video.ops";
    private static final String VIDEO_PIPELINE = "qiniu.video.pipeline";
    /**
     * 七牛云文件存储时长,配合七牛云生命周期处理
     */
    private static final String EXPIRED_DAY = "qiniu.expired.day";
    /**
     * 打印上报索引信息
     */
    private static final String VOICE_ENABLE_LOG = "qiniu.enable.log";

    private static final AttributeKey<QiniuConf> FILE_CONF;

    private static final String LOCAL_TEM_DIRECTORY;

    private static final DateTimeFormatter DATE_TIME_FORMATTER_DAY;

    private static final DateTimeFormatter DATE_TIME_FORMATTER_FILE;

    private static final DateTimeFormatter DATE_TIME_FORMATTER_TOKEN;
    /**
     * 七牛云服务器域名配置
     */
    private static final Configuration CONFIGURATION;

    private static final Cache<String, Map<Integer, FileData>> FILE_INDEX_DATA_CACHE;

    private static final LoadingCache<String, Optional<CommonConf>> APP_ID_COMMON_CONF_MAP_CACHE;
    /**
     * 最大支持4M文件上传
     */
    private static final int MAX_FILE_LENGTH;

    private static final Auth AUTH;

    private static final String DEFAULT_APP_ID;
    /**
     * 自定义Upload线程池,队列长度为100000,超过队列长度,等待任务执行
     */
    private static final ThreadPoolExecutor UPLOAD_FILE_POOL = new ThreadPoolExecutor(100, 100, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100000), new ThreadFactoryBuilder().setNameFormat("pool-upload-consume-thread-%d").build(), (r, executor) -> {
        try {
            long startTime = System.currentTimeMillis();
            executor.getQueue().put(r);
            log.info("too many upload task,blocking the upload {} ms", System.currentTimeMillis() - startTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    /**
     * 移除临时文件夹下的文件信息
     */
    private static final ScheduledThreadPoolExecutor DELETE_TMP_FILE_THREAD_POOL = new ScheduledThreadPoolExecutor(1, (r) -> {
        Thread thread = new Thread(r);
        thread.setName("remove.tmp.file");
        return thread;
    });

    static {
        ConfigUtil.apolloChange(Sets.newHashSet(APP_ID_KEY, BUCKET, TOKEN_EXPIRE_SECOND, CALLBACK_HOST, CALLBACK_URL, VIDEO_CALLBACK_URL, CALLBACK_BODY, CALLBACK_BODY_TYPE, CALLBACK_DES_KEY, DOWN_LOAD_URL, VIDEO_OPS, VIDEO_PIPELINE, VOICE_ENABLE_LOG, EXPIRED_DAY, QINIU_LOCAL_ORDERING));
        /**
         * 超过4M的文件,需要使用断点续传,需要写本地文件
         */
        MAX_FILE_LENGTH = ConfigUtil.getInt(MAX_FILE_LENGTH_KEY, 4 * 1024 * 1024);
        String accessKey = ConfigUtil.getString(QINIU_ACCESS_KEY, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
        String secretKey = ConfigUtil.getString(QINIU_SECRET_KEY, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
        AUTH = Auth.create(accessKey, secretKey);
        String[] gateNames = AbstractHandler.gateId.split("-");
        String projectName = null;
        if (gateNames.length <= 2) {
            projectName = AbstractHandler.gateId;
        } else {
            projectName = gateNames[0] + "-" + gateNames[1];
        }
        String tmpDirectory = ConfigUtil.getString(QINIU_LOCAL_TEM_DIRECTORY, "/tmp/qiniu/");
        if (!tmpDirectory.endsWith("/")) {
            tmpDirectory = tmpDirectory + "/";
        }
        LOCAL_TEM_DIRECTORY = tmpDirectory + projectName;
        FILE_INDEX_DATA_CACHE = CacheBuilder.newBuilder().expireAfterAccess(20, TimeUnit.MINUTES).build();
        APP_ID_COMMON_CONF_MAP_CACHE = CacheBuilder.newBuilder().expireAfterAccess(30, TimeUnit.MINUTES).build(new CacheLoader<String, Optional<CommonConf>>() {
            @Override
            public Optional<CommonConf> load(String appId) {
                try {
                    return Optional.of(CommonConf.getCommonConf(appId));
                } catch (Exception e) {
                    return Optional.empty();
                }
            }
        });
        DEFAULT_APP_ID = "TRACKER";
        CONFIGURATION = new Configuration(Region.region0());
        FILE_CONF = AttributeKey.valueOf("FILE_CONF");
        DATE_TIME_FORMATTER_DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        DATE_TIME_FORMATTER_FILE = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
        DATE_TIME_FORMATTER_TOKEN = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        monitorCache();
        DELETE_TMP_FILE_THREAD_POOL.scheduleAtFixedRate(() -> deleteTmpFile(), 3, 3, TimeUnit.HOURS);
    }

    /**
     * 监控Qiniu云上传缓存数据信息
     */
    private static void monitorCache() {
        //缓存IMEI个数
        MonitorMetrics.registerGauge("qiniu." + AbstractHandler.gateId + ".cache.imei.size", () -> FILE_INDEX_DATA_CACHE == null ? 0 : FILE_INDEX_DATA_CACHE.size());
        //缓存中数据byte大小
        MonitorMetrics.registerGauge("qiniu." + AbstractHandler.gateId + ".cache.data.size", () -> memoryCacheData());
        if (ConfigUtil.getApolloValue(QINIU_LOCAL_ORDERING, true)) {
            //如果外挂基础文件夹存在,表示
            File file = new File(LOCAL_TEM_DIRECTORY);
            if (!file.exists()) {
                file.mkdirs();
            }
            MonitorMetrics.registerGauge("qiniu." + AbstractHandler.gateId + ".tmp.file.data", () -> uploadLocalFileLength());
        }
        MonitorMetrics.registerGauge("qiniu." + AbstractHandler.gateId + ".upload.imei.size", () -> uploadImeiSize());
    }

    /**
     * 直接Guava缓存中数据字节大小
     *
     * @return
     */
    private static long memoryCacheData() {
        String imei = null;
        try {
            if (FILE_INDEX_DATA_CACHE == null || FILE_INDEX_DATA_CACHE.size() <= 0) {
                return 0;
            }
            long len = 0;
            Map<Integer, FileData> indexFileDataMap = null;
            for (Map.Entry<String, Map<Integer, FileData>> imeiMapEntry : FILE_INDEX_DATA_CACHE.asMap().entrySet()) {
                imei = imeiMapEntry.getKey();
                indexFileDataMap = imeiMapEntry.getValue();
                if (indexFileDataMap == null || indexFileDataMap.isEmpty()) {
                    continue;
                }
                len += indexFileDataMap.values().parallelStream().mapToLong(fileData -> {
                    if (fileData.content == null || fileData.content.length == 0) {
                        return 0;
                    }
                    return fileData.content.length;
                }).sum();
            }
            return len;
        } catch (Exception e) {
            log.warn("统计缓存数据错误 imei:{}", imei, e);
            return 0;
        }
    }

    /**
     * 上传是本地空间大小
     *
     * @return
     */
    private static long uploadLocalFileLength() {
        try {
            File[] list = new File(LOCAL_TEM_DIRECTORY).listFiles();
            if (list == null || list.length <= 0) {
                return 0;
            }
            long len = 0;
            for (File fileValue : list) {
                len += fileValue.length();
            }
            return len;
        } catch (Exception e) {
            log.warn("统计文件大小错误", e);
            return 0;
        }
    }

    /**
     * 删除2天前的临时文件
     */
    private static void deleteTmpFile() {
        try {
            //清除2两天前存在的文件信息
            LocalDateTime pre2DateTime = LocalDateTime.now().minusDays(2);
            String date = pre2DateTime.format(DATE_TIME_FORMATTER_DAY);
            File dir = new File(LOCAL_TEM_DIRECTORY + "/" + date);
            if (!dir.exists()) {
                return;
            }
            deleteDir(dir);
        } catch (Exception e) {
            log.warn("删除文件夹失败", e);
        }
    }

    /**
     * 删除文件夹
     *
     * @param dir
     * @return
     */
    private static boolean deleteDir(File dir) {
        if (dir.isDirectory()) {
            String[] children = dir.list();
            for (int i = 0; i < children.length; i++) {
                boolean success = deleteDir(new File(dir, children[i]));
                if (!success) {
                    // 如果子目录或文件删除失败,可以抛出异常或记录错误
                    log.warn("Failed to delete " + dir.getPath() + "/" + children[i]);
                }
            }
        }

        // 目录现在是空的,或者它不是一个目录
        boolean deleteSuccess = dir.delete();
        if (!deleteSuccess) {
            log.warn("Failed to delete " + dir.getPath());
        }
        return true;
    }

    /**
     * 通道上上传的IMEI个数
     *
     * @return
     */
    private static int uploadImeiSize() {
        try {
            Map<String, Channel> channelMap = RouteClient.getAllChannel();
            if (channelMap == null || channelMap.isEmpty()) {
                return 0;
            }
            int size = 0;
            Channel channel = null;
            QiniuConf qiniuConf = null;
            for (Map.Entry<String, Channel> imeiChannelEntry : channelMap.entrySet()) {
                channel = imeiChannelEntry.getValue();
                if (channel == null || !channel.isActive()) {
                    continue;
                }
                if (channel.attr(FILE_CONF) == null) {
                    continue;
                }
                qiniuConf = channel.attr(FILE_CONF).get();
                if (qiniuConf == null || StringUtils.isEmpty(qiniuConf.fileName)) {
                    continue;
                }
                size++;
            }
            return size;
        } catch (Exception e) {
            log.warn("统计上传IMEI大小错误", e);
            return 0;
        }
    }

    /**
     * 文件上传和处理
     *
     * @param ctx      通道信息
     * @param fileData 当前数据包
     * @return 当接收到最后一个包序列号时,返回需要补传的包序号(是否处理由业务端自己定义,其余的时候返回空)
     */
    public static List<Integer> upload(ChannelHandlerContext ctx, FileData fileData) {
        return upload(ctx, null, fileData, null, null);
    }

    /**
     * 文件上传和处理
     *
     * @param ctx                通道信息
     * @param fileData           当前数据包
     * @param verifyFileFunction 校验文件是否已经不是完整包了,如果是,需要丢弃前面的文件缓存,所以丢弃前面的文件缓存,默认当前包的总包数+"-"+总大小不同,就说明文件包已无效
     * @return 当接收到最后一个包序列号时,返回需要补传的包序号(是否处理由业务端自己定义,其余的时候返回空)
     */
    public static List<Integer> upload(ChannelHandlerContext ctx, FileData fileData, Function<FileData, String> verifyFileFunction) {
        return upload(ctx, null, fileData, verifyFileFunction);
    }

    /**
     * 文件上传和处理
     *
     * @param ctx                通道信息
     * @param fileTypeEnum       文件类型,以最后一个包为准
     * @param fileData           当前数据包
     * @param verifyFileFunction 校验文件是否已经不是完整包了,如果是,需要丢弃前面的文件缓存,默认当前包的总包数+"-"+总大小不同,就说明文件包已无效
     * @return 当接收到最后一个包序列号时,返回需要补传的包序号(是否处理由业务端自己定义,其余的时候返回空)
     */
    public static List<Integer> upload(ChannelHandlerContext ctx, FileTypeEnum fileTypeEnum, FileData fileData, Function<FileData, String> verifyFileFunction) {
        return upload(ctx, fileTypeEnum, fileData, verifyFileFunction, null);
    }

    /**
     * 文件上传和处理
     *
     * @param ctx          通道信息
     * @param fileTypeEnum 文件类型,以最后一个包为准
     * @param fileData     当前数据包
     * @return 当接收到最后一个包序列号时,返回需要补传的包序号(是否处理由业务端自己定义,其余的时候返回空)
     */
    public static List<Integer> upload(ChannelHandlerContext ctx, FileTypeEnum fileTypeEnum, FileData fileData) {
        return upload(ctx, fileTypeEnum, fileData, null, null);
    }

    /**
     * 文件上传和处理
     *
     * @param ctx         通道信息
     * @param fileData    当前数据包
     * @param biPredicate 上传完成之后的回调操作,第一个入参 true:上传成功,false:上传失败,回调结果目前暂无用处
     * @return 当接收到最后一个包序列号时,返回需要补传的包序号(是否处理由业务端自己定义,其余的时候返回空)
     */
    public static List<Integer> upload(ChannelHandlerContext ctx, FileData fileData, BiPredicate<Boolean, QiniuConf> biPredicate) {
        return upload(ctx, null, fileData, null, biPredicate);
    }

    /**
     * 文件上传和处理
     *
     * @param ctx                通道信息
     * @param fileData           当前数据包
     * @param verifyFileFunction 校验文件是否已经不是完整包了,如果是,需要丢弃前面的文件缓存,默认当前包的总包数+"-"+总大小不同,就说明文件包已无效
     * @param biPredicate        上传完成之后的回调操作,第一个入参 true:上传成功,false:上传失败,回调结果目前暂无用处
     * @return 当接收到最后一个包序列号时,返回需要补传的包序号(是否处理由业务端自己定义,其余的时候返回空)
     */
    public static List<Integer> upload(ChannelHandlerContext ctx, FileData fileData, Function<FileData, String> verifyFileFunction, BiPredicate<Boolean, QiniuConf> biPredicate) {
        return upload(ctx, null, fileData, verifyFileFunction, biPredicate);
    }

    /**
     * 文件上传和处理
     *
     * @param ctx          通道信息
     * @param fileTypeEnum 文件类型,以最后一个包为准
     * @param fileData     当前数据包
     * @param biPredicate  上传完成之后的回调操作,第一个入参 true:上传成功,false:上传失败,回调结果目前暂无用处
     * @return 当接收到最后一个包序列号时,返回需要补传的包序号(是否处理由业务端自己定义,其余的时候返回空)
     */
    public static List<Integer> upload(ChannelHandlerContext ctx, FileTypeEnum fileTypeEnum, FileData fileData, BiPredicate<Boolean, QiniuConf> biPredicate) {
        return upload(ctx, fileTypeEnum, fileData, null, biPredicate);
    }

    /**
     * 文件上传和处理
     *
     * @param ctx                通道信息
     * @param fileTypeEnum       文件类型,以最后一个包为准
     * @param fileData           当前数据包
     * @param verifyFileFunction 校验文件是否已经不是完整包了,如果是,需要丢弃前面的文件缓存,默认当前包的总包数+"-"+总大小不同,就说明文件包已无效
     * @param biPredicate        上传完成之后的回调操作,第一个入参 true:上传成功,false:上传失败,回调结果目前暂无用处
     * @return 当接收到最后一个包序列号时,返回需要补传的包序号(是否处理由业务端自己定义,其余的时候返回空)
     */
    public static List<Integer> upload(ChannelHandlerContext ctx, FileTypeEnum fileTypeEnum, FileData fileData, Function<FileData, String> verifyFileFunction, BiPredicate<Boolean, QiniuConf> biPredicate) {
        QiniuConf qiniuConf = null;
        String imei = null;
        try {
            imei = getImei(ctx);
        } catch (Exception e) {
            log.warn("上传文件,获取IMEI号失败", e);
            return null;
        }
        try {
            qiniuConf = qiniuConf(ctx, fileTypeEnum, fileData);
            Tuple.TwoTuple<Boolean, Boolean> tuple2 = verifyFile(qiniuConf, fileTypeEnum, fileData, verifyFileFunction);
            if (tuple2._1()) {
                if (tuple2._2()) {
                    after(ctx);
                    qiniuConf.fileTypeEnum(fileTypeEnum).builder();
                } else {
                    qiniuConf.builder();
                    updateCache(ctx);
                }
            }
            if (fileData.promptlyUpload == null || !fileData.promptlyUpload) {
                isWriteLocalOrderingFile(fileData, qiniuConf);
                byte[] data = null;
                if (qiniuConf.localOrdering) {
                    //非乱序,数据直接落盘
                    data = fileData.content;
                    fileData.currentLength = data.length;
                    fileData.content = null;
                }
                //文件进行了分包,需要组包
                Map<Integer, FileData> copyMap = null;
                String fileName = null;
                File temFile = null;
                synchronized (imei.intern()) {
                    Map<Integer, FileData> indexDataMap = FILE_INDEX_DATA_CACHE.getIfPresent(qiniuConf.fileName);
                    if (indexDataMap == null) {
                        if (fileData.totalCount > 0) {
                            indexDataMap = Maps.newLinkedHashMapWithExpectedSize(fileData.totalCount);
                        } else {
                            indexDataMap = Maps.newLinkedHashMapWithExpectedSize(30);
                        }
                        FILE_INDEX_DATA_CACHE.put(qiniuConf.fileName, indexDataMap);
                    }
                    if (qiniuConf.localOrdering && !indexDataMap.containsKey(fileData.currentIndex) && data != null && data.length > 0) {
                        //顺序的数据包,没有上传过的索引,直接写入文件
                        writeLocalFile(qiniuConf, data, fileData);
                    }
                    indexDataMap.put(fileData.currentIndex, fileData);
                    if (ConfigUtil.getApolloValue(VOICE_ENABLE_LOG, true)) {
                        log.info("imei: {} 录音数据包总数:{} 已经上报的数据量:{} 当前上报的索引:{}", qiniuConf.imei, fileData.totalCount, indexDataMap.size(), fileData.currentIndex);
                    }
                    if (indexDataMap.size() < fileData.totalCount) {
                        if (fileData.totalCount > 0 && fileData.currentIndex == fileData.totalCount) {
                            List<Integer> supplementalIdList = Lists.newArrayListWithExpectedSize(fileData.totalCount);
                            //如果当前包是最后一个数据包
                            for (int i = 1; i <= fileData.totalCount; i++) {
                                if (indexDataMap.containsKey(i)) {
                                    continue;
                                }
                                supplementalIdList.add(i);
                            }
                            return supplementalIdList.isEmpty() ? null : supplementalIdList;
                        }
                        return null;
                    }
                    //复制数据,立即释放资源
                    copyMap = Maps.newHashMap(indexDataMap);
                    fileName = qiniuConf.fileName;
                    if (qiniuConf.file != null) {
                        temFile = qiniuConf.file;
                    }
                    after(ctx);
                }
                List<FileData> sortDataList = copyMap.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue).collect(Collectors.toList());
                if (temFile != null) {
                    asyncUpload(biPredicate, qiniuConf, fileName, temFile, sortDataList);
                } else {
                    byte[] content = new byte[0];
                    for (FileData querFileData : sortDataList) {
                        content = ByteUtil.concat(content, querFileData.content);
                    }
                    asyncUpload(biPredicate, qiniuConf, fileName, content, null);
                }
            } else if (fileData.uploadFile != null) {
                //直接上传文件
                asyncUpload(biPredicate, qiniuConf, fileData.uploadFile);
                after(ctx);
            } else {
                //一个文件就是一个完成的数据包
                asyncUpload(biPredicate, qiniuConf, fileData.content);
                after(ctx);
            }
            return null;
        } catch (Exception e) {
            log.warn("文件上传失败 imei:{}", imei, e);
            callBack(biPredicate, qiniuConf, false);
            after(ctx);
            return null;
        }
    }

    /**
     * 异步上传文件信息
     *
     * @param biPredicate 上传后的回调方法
     * @param qiniuConf
     * @param data        file 或 byte数组
     */
    private static void asyncUpload(BiPredicate<Boolean, QiniuConf> biPredicate, QiniuConf qiniuConf, Object data) {
        asyncUpload(biPredicate, qiniuConf, qiniuConf.fileName, data, null);
    }

    /**
     * 异步上传文件信息
     *
     * @param biPredicate 上传后的回调方法
     * @param qiniuConf
     * @param fileName    上传文件名称
     * @param data        file 或 byte数组
     */
    private static void asyncUpload(BiPredicate<Boolean, QiniuConf> biPredicate, QiniuConf qiniuConf, String fileName, Object data, List<FileData> sortDataList) {
        final String imei = qiniuConf.imei;
        final String token = qiniuConf.token;
        UPLOAD_FILE_POOL.submit(() -> {
            boolean uploadResult = uploadQiniu(imei, fileName, token, data, sortDataList);
            callBack(biPredicate, qiniuConf, uploadResult);
        });
    }

    /**
     * 校验是否因为少包,需要丢弃前面缓存的包
     *
     * @param qiniuConf
     * @param fileTypeEnum
     * @param fileData
     * @param verifyFileFunction
     * @return 是否需要修改配置信息,是否移除已存在的缓存信息
     */
    public static Tuple.TwoTuple<Boolean, Boolean> verifyFile(QiniuConf qiniuConf, FileTypeEnum fileTypeEnum, FileData fileData, Function<FileData, String> verifyFileFunction) {
        if (qiniuConf == null || fileData == null) {
            return new Tuple.TwoTuple<>(false, false);
        }
        String verifyFile = null;
        if (verifyFileFunction == null) {
            verifyFile = fileData.totalCount + "-" + fileData.length;
        } else {
            verifyFile = verifyFileFunction.apply(fileData);
            if (StringUtils.isEmpty(verifyFile)) {
                verifyFile = fileData.totalCount + "-" + fileData.length;
            }
        }
        if (StringUtils.isEmpty(qiniuConf.verifyFile)) {
            qiniuConf.verifyFile = verifyFile;
            if (fileTypeEnum != null && !fileTypeEnum.equals(qiniuConf.fileTypeEnum)) {
                //当使用了默认的文件类型,后面的数据包又上传了文件类型(一般情况不存在),这时候需要更新文件信息
                qiniuConf.fileTypeEnum = fileTypeEnum;
                return new Tuple.TwoTuple<>(true, false);
            }
            return new Tuple.TwoTuple<>(false, false);
        }
        if (qiniuConf.verifyFile.equals(verifyFile)) {
            if (fileTypeEnum != null && !fileTypeEnum.equals(qiniuConf.fileTypeEnum)) {
                //当使用了默认的文件类型,后面的数据包又上传了文件类型(一般情况不存在),这时候需要更新文件信息
                qiniuConf.fileTypeEnum = fileTypeEnum;
                return new Tuple.TwoTuple<>(true, false);
            }
            return new Tuple.TwoTuple<>(false, false);
        }
        log.warn("文件属性以改变,只能丢弃前面的数据包信息 old: {} new:{}", qiniuConf.verifyFile, verifyFile);
        qiniuConf.verifyFile = verifyFile;
        return new Tuple.TwoTuple<>(true, true);
    }

    /**
     * 回调业务方法
     *
     * @param biPredicate
     * @param qiniuConf
     * @param uploadResult
     * @return
     */
    private static boolean callBack(BiPredicate<Boolean, QiniuConf> biPredicate, QiniuConf qiniuConf, boolean uploadResult) {
        if (biPredicate == null) {
            //无需回调
            return true;
        }
        try {
            return biPredicate.test(uploadResult, qiniuConf);
        } catch (Exception e) {
            log.warn("回调失败 qiniuConf:{}", qiniuConf, e);
            return false;
        }
    }

    /**
     * 执行完成,移除缓存的文件信息
     *
     * @param ctx
     */
    public static void after(ChannelHandlerContext ctx) {
        try {
            after(qiniuConf(ctx, null));
        } catch (Exception e) {
            log.warn("回收资源错误 imei:{}", AbstractHandler.getImeiByCtx(ctx), e);
        }
    }

    /**
     * 文件上传成功后的处理操作
     *
     * @param qiniuConf
     */
    private static void after(QiniuConf qiniuConf) {
        if (qiniuConf == null) {
            return;
        }
        FILE_INDEX_DATA_CACHE.invalidate(qiniuConf.fileName);
        qiniuConf.fileName = null;
        qiniuConf.offset = 0;
        qiniuConf.file = null;
        qiniuConf.localOrdering = null;
        qiniuConf.downloadUrl = null;
        qiniuConf.fileTypeEnum = null;
        qiniuConf.fileNameTime = null;
        qiniuConf.verifyFile = null;
    }

    private static void updateCache(ChannelHandlerContext ctx) {
        try {
            QiniuConf qiniuConf = qiniuConf(ctx, null);
            String fileName = qiniuConf.fileName;
            if (StringUtils.isEmpty(fileName)) {
                return;
            }
            qiniuConf.builder();
            if (fileName.equals(qiniuConf.fileName)) {
                //文件名相同,无需更新缓存信息
                return;
            }
            //获取前面已存储的录音信息,缓存到新的录音文件key上,失效老的文件信息
            Map<Integer, FileData> indexDataMap = FILE_INDEX_DATA_CACHE.getIfPresent(fileName);
            if (indexDataMap == null || indexDataMap.isEmpty()) {
                return;
            }
            FILE_INDEX_DATA_CACHE.put(qiniuConf.fileName, indexDataMap);
            FILE_INDEX_DATA_CACHE.invalidate(fileName);
        } catch (Exception e) {
            log.warn("回收资源错误 imei:{}", AbstractHandler.getImeiByCtx(ctx), e);
        }
    }

    /**
     * 指令下发时,协议区分不开是否是同一个录音文件,所以只能有一个录音文件能上传
     *
     * @return
     */
    public static boolean hasRecordMsg(ChannelHandlerContext ctx) {
        try {
            QiniuConf qiniuConf = qiniuConf(ctx, null);
            if (StringUtils.isEmpty(qiniuConf.fileName)) {
                return false;
            }
            return FILE_INDEX_DATA_CACHE.getIfPresent(qiniuConf.fileName) != null;
        } catch (Exception e) {
            log.warn("判断录音信息错误 imei:{}", AbstractHandler.getImeiByCtx(ctx), e);
            return false;
        }
    }

    /**
     * 通道断开,移除前面的缓存的录音消息包信息
     *
     * @param ctx
     */
    public static void removeCacheFile(ChannelHandlerContext ctx) {
        try {
            QiniuConf qiniuConf = ctx.channel().attr(FILE_CONF).get();
            String fileName = qiniuConf.fileName;
            if (StringUtils.isEmpty(fileName)) {
                return;
            }
            FILE_INDEX_DATA_CACHE.invalidate(fileName);
        } catch (Exception e) {
            log.warn("回收资源错误 imei:{}", AbstractHandler.getImeiByCtx(ctx), e);
        }
    }

    /**
     * 移除缓存信息
     *
     * @param channel
     */
    public static void removeCacheFile(Channel channel) {
        String imei = null;
        try {
            if (channel == null) {
                return;
            }
            imei = channel.attr(AbstractHandler.loginImeiKey).get();
            after(channel.attr(FILE_CONF).get());
        } catch (Exception e) {
            log.warn("回收资源错误 imei:{}", imei, e);
        }
    }

    /**
     * 直接上传
     *
     * @param imei     IMEI
     * @param fileName 上传后的文件名
     * @param token    上传TOKEN
     * @param data     数据类型为 File和 byte[]
     * @return
     */
    public static boolean uploadQiniu(final String imei, final String fileName, final String token, Object data, List<FileData> sortDataList) {
        try {
            Response response = null;
            if (data == null) {
                log.error("上传文件失败 imei:{} 数据为空", imei);
                return false;
            }
            if (data instanceof File) {
                File file = (File) data;
                if (!file.isFile()) {
                    log.error("上传文件失败 imei:{} 文件不存在", file.getPath());
                    return false;
                }
                if (sortDataList != null) {
                    file = organizeFiles(file, sortDataList);
                }
                response = new UploadManager(CONFIGURATION).put(file, fileName, token);
                //上传完成,清除本地文件
                file.delete();
            } else {
                response = new UploadManager(CONFIGURATION).put((byte[]) data, fileName, token);
            }
            log.info("文件上传信息 fileName: {} response: {}", fileName, response.getInfo());
            return true;
        } catch (QiniuException e) {
            if (e.response != null && e.response.getInfo().contains("file exists")) {
                //文件已上传,无需重复上传
                return true;
            }
            log.error("上传文件失败 imei:{} case: {}", imei, (null == e.response ? "" : e.response.toString()), e);
            return false;
        } catch (Exception e) {
            log.error("上传文件失败 imei:{} ", imei, e);
            return false;
        }
    }

    @NotNull
    private static QiniuConf qiniuConf(ChannelHandlerContext ctx, FileTypeEnum fileTypeEnum, FileData fileData) throws Exception {
        if (fileData == null) {
            throw new Exception("上传fileData为空");
        }
        if (fileData.content == null && fileData.uploadFile == null) {
            throw new Exception("上传数据为空");
        }
        if (fileData.content != null && fileData.content.length > MAX_FILE_LENGTH && !isWriteLocalOrderingFile(fileData, null)) {
            throw new Exception("大于4M的文件,数据包必须要顺序,上报时会直接写本地磁盘,最后直接上传文件");
        }
        return qiniuConf(ctx, fileTypeEnum);
    }

    /**
     * 获取Qiniu云配置信息
     *
     * @param ctx
     * @param fileTypeEnum
     * @return
     * @throws Exception
     */
    public static QiniuConf qiniuConf(ChannelHandlerContext ctx, FileTypeEnum fileTypeEnum) throws Exception {
        if (ctx == null || ctx.channel() == null || !ctx.channel().isActive()) {
            throw new Exception("操作七牛云时,连接已断开,上传失败");
        }
        String imei = getImei(ctx);
        QiniuConf qiniuConf = ctx.channel().attr(FILE_CONF).get();
        if (qiniuConf == null) {
            qiniuConf = QiniuConf.build(imei);
            ctx.channel().attr(FILE_CONF).set(qiniuConf);
        }
        boolean updateFileTypeFlag = fileTypeEnum != null && (qiniuConf.fileTypeEnum == null || !fileTypeEnum.equals(qiniuConf.fileTypeEnum));
        if (updateFileTypeFlag) {
            qiniuConf.fileTypeEnum(fileTypeEnum);
            if (StringUtils.isNotEmpty(qiniuConf.fileName)) {
                qiniuConf.updateFileName(fileTypeEnum);
            }
        }
        if (StringUtils.isEmpty(qiniuConf.fileName) || StringUtils.isEmpty(qiniuConf.token)) {
            //文件名 || token不存在,需要重新生成TOKEN
            qiniuConf.builder();
        }
        if (StringUtils.isEmpty(qiniuConf.token)) {
            throw new Exception("获取TOKEN失败");
        }
        if (StringUtils.isEmpty(qiniuConf.fileName)) {
            throw new Exception("上传文件名不能为空");
        }
        return qiniuConf;
    }

    @NotNull
    private static String getImei(ChannelHandlerContext ctx) throws Exception {
        if (ctx == null) {
            throw new Exception("通道已断开");
        }
        String imei = AbstractHandler.getImeiByCtx(ctx);
        if (StringUtils.isEmpty(imei)) {
            throw new Exception("上传IMEI为空,请检查设备是否已登录");
        }
        return imei;
    }

    private static Object getRegionString(Region region, String key) {
        Class<?> targetClass = region.getClass();
        Field field;
        try {
            field = targetClass.getDeclaredField(key);
            //访问私有必须调用
            field.setAccessible(true);
            return field.get(region);
        } catch (Exception e) {
            log.error("Server get region error:", e);
            return null;
        }
    }

    /**
     * 追加文件写入
     *
     * @param qiniuConf
     * @param data
     * @param fileData
     * @return
     */
    private static boolean writeLocalFile(QiniuConf qiniuConf, byte[] data, FileData fileData) {
        if (qiniuConf.file == null) {
            qiniuConf.file = new File(LOCAL_TEM_DIRECTORY + "/" + qiniuConf.fileDay + "/" + qiniuConf.fileName);
            try {
                if (!qiniuConf.file.getParentFile().exists()) {
                    qiniuConf.file.getParentFile().mkdirs();
                }
                qiniuConf.file.createNewFile();
            } catch (IOException e) {
                log.warn("创建文件失败", e);
                return false;
            }
        }
        if (data == null || data.length <= 0) {
            return true;
        }
        try (RandomAccessFile out = new RandomAccessFile(qiniuConf.file, "rw")) {
            out.seek(qiniuConf.offset);
            out.write(data);
            fileData.adjustByteOrder = new AdjustByteOrder(qiniuConf.offset, qiniuConf.offset + data.length);
            qiniuConf.offset = qiniuConf.offset + data.length;
            return true;
        } catch (FileNotFoundException e) {
            log.error("Cache File {} not found, Error: {}", qiniuConf.fileName, e.getMessage());
        } catch (IOException e) {
            log.error("Cache File {} Error: {}", qiniuConf.fileName, e.getMessage());
        }
        return false;
    }

    /**
     * 整理文件,如果文件乱序写入,需要重新整理
     *
     * @param tempFile
     * @param sortDataList
     * @return
     */
    private static File organizeFiles(File tempFile, List<FileData> sortDataList) {
        if (tempFile == null) {
            return null;
        }
        boolean isOrganizeFlag = false;
        long firstIndex = 0;
        for (FileData fileData : sortDataList) {
            if (fileData.adjustByteOrder == null) {
                return tempFile;
            }
            if (fileData.adjustByteOrder.isOrganize(firstIndex)) {
                //文件index存在乱序,需要重整文件
                isOrganizeFlag = true;
                break;
            }
            firstIndex = firstIndex + (fileData.adjustByteOrder.tempEndIndex - fileData.adjustByteOrder.tempFirstIndex);
        }
        if (!isOrganizeFlag) {
            return tempFile;
        }
        String fileName = tempFile.getName();
        int indexLast = fileName.lastIndexOf(".");
        String tmpFileName = fileName.substring(0, indexLast) + "_tmp" + fileName.substring(indexLast, fileName.length());
        File newTempFile = new File(tempFile.getPath().replace(fileName, tmpFileName));
        try {
            newTempFile.createNewFile();
        } catch (IOException e) {
            log.warn("创建临时文件失败 path:{}", tempFile.getPath(), e);
            return tempFile;
        }
        try (RandomAccessFile sourceFile = new RandomAccessFile(tempFile, "r");
             FileChannel sourceChannel = sourceFile.getChannel();
             RandomAccessFile targetFile = new RandomAccessFile(newTempFile, "rw");) {
            // 创建一个缓冲区来读取数据
            ByteBuffer buffer = null;
            long offset = 0;
            byte[] bytes = null;
            for (FileData fileData : sortDataList) {
                buffer = ByteBuffer.allocate((int) (fileData.adjustByteOrder.tempEndIndex - fileData.adjustByteOrder.tempFirstIndex));
                // 将文件指针移动到开始位置
                sourceChannel.position(fileData.adjustByteOrder.tempFirstIndex);
                // 读取数据到缓冲区
                while (buffer.hasRemaining() && sourceChannel.read(buffer) != -1) {
                    // 如果文件结束,退出循环
                    break;
                }
                // 切换缓冲区的模式为读模式
                buffer.flip();
                targetFile.seek(offset);
                bytes = buffer.array();
                targetFile.write(bytes);
                offset = offset + bytes.length;
            }
        } catch (IOException e) {
            log.warn("重整文件失败 path:{}", tempFile.getPath(), e);
            return tempFile;
        }
        try {
            tempFile.delete();
        } catch (Exception e) {
        }
        return newTempFile;
    }

    /**
     * 是否写本地盘
     * 没有配置任何信息,且不传offset,默认支持写本地盘
     *
     * @param fileData
     * @param qiniuConf
     * @return
     */
    private static boolean isWriteLocalOrderingFile(FileData fileData, QiniuConf qiniuConf) {
        if (qiniuConf == null) {
            if (fileData != null && fileData.offset > 0) {
                return true;
            }
            return ConfigUtil.getApolloValue(QINIU_LOCAL_ORDERING, true);
        }
        if (qiniuConf.localOrdering != null) {
            return qiniuConf.localOrdering;
        }
        if (fileData.localOrdering != null) {
            qiniuConf.localOrdering = fileData.localOrdering;
            return qiniuConf.localOrdering;
        }
        if (fileData.offset < 0) {
            //如果协议不上传offset偏移量,写本地盘就必须要设备数据包顺序上传,根据协议确定
            qiniuConf.localOrdering = ConfigUtil.getApolloValue(QINIU_LOCAL_ORDERING, true);
            return qiniuConf.localOrdering;
        }
        if (fileData.offset > 0) {
            qiniuConf.localOrdering = true;
            return true;
        }
        qiniuConf.localOrdering = false;
        return false;
    }

    public static String downLoad(String fileName) {
        return downLoad(HTTP + ConfigUtil.getApolloValue(DOWN_LOAD_URL, "xxxx.xxxxxx.xxxxx"), fileName);
    }

    public static String downLoad(String downDomain, String fileName) {
        return downDomain + "/" + fileName;
    }

    /**
     * 公共配置
     * 共用
     */
    private static class CommonConf {
        /**
         * 七牛云域名信息
         */
        private String host;
        /**
         * 空间信息
         */
        private String bucket;
        /**
         * 回调域名
         */
        private String callbackHost;
        /**
         * 回调URL
         */
        private String callbackUrl;
        /**
         * Video回调
         */
        private String callbackVideoUrl;
        /**
         * 回调BODY
         */
        private String callbackBody;
        /**
         * 回调类型
         */
        private String callbackBodyType;
        /**
         * uploadToken过期时间 25分钟
         */
        private int expiresSecond;
        /**
         * 上传失败重复资数
         */
        private int retryTimes;
        /**
         * 七牛云回调加密KEY
         */
        private String callbackDesKey;
        /**
         * Video 回调参数
         */
        private String videoOps;
        /**
         * Video 回调参数
         */
        private String videoPipeline;

        private CommonConf() {
        }

        /**
         * 获取Qiniu云基础配置信息
         */
        private static CommonConf getCommonConf(String appId) {
            try {
                String bucket = ConfigUtil.getApolloValue(BUCKET, "");
                if (StringUtils.isEmpty(bucket)) {
                    log.error("上传的Bucket不能为空");
                    return null;
                }
                CommonConf commonConf = new CommonConf();
                List<String> srcUpHosts = (List<String>) getRegionString(CONFIGURATION.region, "srcUpHosts");
                String domain = Configuration.defaultRsHost;
                if (null != srcUpHosts && !srcUpHosts.isEmpty()) {
                    domain = srcUpHosts.get(0);
                }
                String callbackHost = ConfigUtil.getApolloValue(CALLBACK_HOST, "xxxxxxxxx");
                String callbackUrl = ConfigUtil.getApolloValue(CALLBACK_URL, "xxxxxxxxx");
                String callbackVideoUrl = ConfigUtil.getApolloValue(VIDEO_CALLBACK_URL, "xxxxxxxx");
                String callbackBody = ConfigUtil.getApolloValue(CALLBACK_BODY, "xxxxxxxx");
                String callbackBodyType = ConfigUtil.getApolloValue(CALLBACK_BODY_TYPE, "application/json");
                String callbackDesKey = ConfigUtil.getApolloValue(CALLBACK_DES_KEY, "xxxxxx");
                String videoOps = ConfigUtil.getApolloValue(VIDEO_OPS, "xxxxxxxx");
                String videoPipeline = ConfigUtil.getApolloValue(VIDEO_PIPELINE, "matrix");
                int expiresSecond = ConfigUtil.getApolloValue(TOKEN_EXPIRE_SECOND, 25) * 60;
                commonConf.bucket = getValueByAppId(bucket, appId);
                commonConf.host = HTTP + domain;
                commonConf.retryTimes = 3;
                commonConf.expiresSecond = expiresSecond;
                commonConf.callbackHost = getValueByAppId(callbackHost, appId);
                commonConf.callbackUrl = commonConf.callbackHost + getValueByAppId(callbackUrl, appId);
                commonConf.callbackVideoUrl = commonConf.callbackHost + getValueByAppId(callbackVideoUrl, appId);
                if (!commonConf.callbackHost.contains(HTTP) && !commonConf.callbackHost.contains(HTTPS)) {
                    commonConf.callbackUrl = HTTP + commonConf.callbackUrl;
                    commonConf.callbackVideoUrl = HTTP + commonConf.callbackVideoUrl;
                }
                commonConf.callbackBody = getValueByAppId(callbackBody, appId);
                commonConf.callbackBodyType = getValueByAppId(callbackBodyType, appId);
                commonConf.callbackDesKey = getValueByAppId(callbackDesKey, appId);
                commonConf.videoOps = getValueByAppId(videoOps, appId);
                commonConf.videoPipeline = getValueByAppId(videoPipeline, appId);
                return commonConf;
            } catch (Exception e) {
                log.warn("加载共用配置错误 appId:{}", appId);
                return null;
            }
        }

        private static String getValueByAppId(String value, String appId) {
            if (!value.contains(",")) {
                return value;
            }
            if (StringUtils.isEmpty(appId)) {
                appId = DEFAULT_APP_ID;
            }
            String[] appIdValues = value.split(",");
            appId = appId.toUpperCase();
            String defaultValue = null;
            for (String appIdValue : appIdValues) {
                if (appIdValue.toUpperCase().startsWith(appId + "|")) {
                    return appIdValue.substring((appId + "|").length());
                }
                if (appIdValue.toUpperCase().startsWith(DEFAULT_APP_ID + "|")) {
                    defaultValue = appIdValue.substring(8);
                }
            }
            //未配置使用默认的配置
            return defaultValue;
        }
    }

    /**
     * 存储于Channel上的文件信息
     */
    public static class QiniuConf {
        /**
         * 公共配置信息
         */
        private CommonConf commonConf;
        /**
         * 设备IMEI号
         */
        private String imei;
        /**
         * TOKEN
         */
        private String token;
        /**
         * 校验文件包是否已经变更,需要移除缓存信息
         */
        private String verifyFile;
        /**
         * 文件名称
         */
        private String fileName;
        /**
         * 文件时间
         */
        private LocalDateTime fileNameTime;
        /**
         * 上传文件时间
         */
        private String fileDay;
        /**
         * 文件云存储时长
         */
        private Integer expiredDay;
        /**
         * 任务ID
         */
        private String taskId;
        /**
         * 平台ID
         */
        private String platformId;
        /**
         * endUser
         */
        private String endUser;
        /**
         * 下载URL
         */
        private String downloadUrl;
        /**
         * 文件类型
         */
        private FileTypeEnum fileTypeEnum;

        private Long expiresSecondTime;

        private boolean upload = false;
        /**
         * 写入本地磁盘的offset,从0开始
         */
        private int offset;

        private File file;
        /**
         * 是否写本地磁盘
         */
        private Boolean localOrdering;

        private QiniuConf() {
        }

        public static QiniuConf build(String imei) {
            QiniuConf qiniuConf = new QiniuConf();
            qiniuConf.imei = imei;
            try {
                qiniuConf.commonConf = APP_ID_COMMON_CONF_MAP_CACHE.get(QiniuConf.getAppId(qiniuConf.imei)).get();
            } catch (ExecutionException e) {
                throw new RuntimeException("Qiniu云配置信息错误 imei:" + qiniuConf.imei, e);
            }
            return qiniuConf;
        }

        public QiniuConf token(String token) {
            if (StringUtils.isEmpty(token) && StringUtils.isNotEmpty(this.token)) {
                return this;
            }
            this.token = token;
            return this;
        }

        public QiniuConf verifyFile(String verifyFile) {
            this.verifyFile = verifyFile;
            return this;
        }

        public QiniuConf fileName(String fileName) {
            this.fileName = fileName;
            return this;
        }

        public QiniuConf fileTypeEnum(FileTypeEnum fileTypeEnum) {
            if (fileTypeEnum == null && this.fileTypeEnum != null) {
                return this;
            }
            this.fileTypeEnum = fileTypeEnum;
            return this;
        }

        public QiniuConf expiredDay(Integer expiredDay) {
            this.expiredDay = expiredDay;
            return this;
        }

        public QiniuConf taskId(String taskId) {
            this.taskId = taskId;
            return this;
        }

        public QiniuConf platformId(String platformId) {
            this.platformId = platformId;
            return this;
        }

        public QiniuConf expiresSecondTime(Long expiresSecondTime) {
            this.expiresSecondTime = expiresSecondTime;
            return this;
        }

        public String getImei() {
            return imei;
        }

        public String getToken() {
            return token;
        }

        public Long getExpiresSecondTime() {
            return expiresSecondTime;
        }

        public String getFileName() {
            return fileName;
        }

        public String getTaskId() {
            return taskId;
        }

        public String getPlatformId() {
            return platformId;
        }

        public String getEndUser() {
            return endUser;
        }

        public String getDownloadUrl() {
            return downloadUrl;
        }

        public FileTypeEnum getFileTypeEnum() {
            return fileTypeEnum;
        }

        private QiniuConf builder() {
            try {
                if (this.fileTypeEnum == null) {
                    log.info("上传文件类型为空,使用默认信息");
                    fileTypeEnum = FileTypeEnum.AUDIO_ALARM;
                }
                if (this.fileName == null) {
                    if (this.fileNameTime == null) {
                        this.fileNameTime = LocalDateTime.now();
                        this.fileDay = this.fileNameTime.format(DATE_TIME_FORMATTER_DAY);
                    }
                    updateFileName(this.fileTypeEnum);
                }
                if (StringUtils.isNotEmpty(taskId) && StringUtils.isEmpty(platformId)) {
                    this.platformId = taskId.indexOf("@") > 0 ? taskId.substring(0, taskId.indexOf("@")) : "";
                }
                this.downloadUrl = StringUtils.defaultString(this.downloadUrl, HTTP + ConfigUtil.getApolloValue(DOWN_LOAD_URL, "xxx.xxx.xxxx") + "/" + this.fileName);
                if (StringUtils.isEmpty(token) || System.currentTimeMillis() >= expiresSecondTime) {
                    this.commonConf = APP_ID_COMMON_CONF_MAP_CACHE.get(QiniuConf.getAppId(this.imei)).get();
                    if (this.commonConf == null) {
                        log.warn("获取Qiniu云公共配置为空 imei:{}", imei);
                        throw new RuntimeException("获取七牛云公共配置失败 imei:" + imei);
                    }
                    getToken(this, this.fileNameTime);
                }
            } catch (Exception e) {
                log.warn("获取TOKEN信息失败 this", JSONObject.toJSONString(this), e);
            }
            return this;
        }

        private String updateFileName(FileTypeEnum fileTypeEnum) {
            if (this.expiredDay == null) {
                this.expiredDay = ConfigUtil.getApolloValue(EXPIRED_DAY, 186);
            }
            this.fileName = this.expiredDay + "-" + this.imei + "-" + fileTypeEnum + "-" + this.fileNameTime.format(DATE_TIME_FORMATTER_FILE) + fileTypeEnum.suffix;
            //更新TOKEN
            getToken(this, this.fileNameTime);
            this.downloadUrl = HTTP + ConfigUtil.getApolloValue(DOWN_LOAD_URL, "statics.aichezaixian.com") + "/" + this.fileName;
            return this.fileName;
        }

        /**
         * 获取TOKEN信息
         * 如果需要修改Qiniu云默认的配置,可以通过先执行init方法,之后
         *
         * @param localDateTime
         * @return
         */
        public static String getToken(QiniuConf qiniuConf, LocalDateTime localDateTime) {
            try {
                if (StringUtils.isEmpty(qiniuConf.commonConf.bucket)) {
                    log.error("获取Qiniu云TOKEN时bucket 不能为空");
                    return null;
                }
                Des des = new Des(qiniuConf.commonConf.callbackDesKey);
                StringBuffer sb = new StringBuffer(qiniuConf.imei).append("#").append(Optional.ofNullable(qiniuConf.platformId).orElse("")).append("#");
                if (qiniuConf.fileTypeEnum == FileTypeEnum.AUDIO_ORDINARY) {
                    sb.append(TriggerTypeEnum.MANUAL.value);
                } else {
                    sb.append(TriggerTypeEnum.PHONIC.value);
                }
                qiniuConf.endUser = sb.append("#").append(localDateTime.format(DATE_TIME_FORMATTER_TOKEN)).append("#").append(Optional.ofNullable(qiniuConf.taskId).orElse("")).append("#").append(qiniuConf.fileTypeEnum.type).toString();
                // 视频截图指令
                StringMap policy = new StringMap().put("callbackUrl", qiniuConf.commonConf.callbackUrl).put("callbackHost", qiniuConf.commonConf.callbackHost).put("callbackBody", qiniuConf.commonConf.callbackBody).put("persistentOps", qiniuConf.commonConf.videoOps)
                        // 视频截图队列
                        .put("persistentPipeline", qiniuConf.commonConf.videoPipeline)
                        // 视频截图回调
                        .put("persistentNotifyUrl", qiniuConf.commonConf.callbackVideoUrl).put("endUser", des.encrypt(qiniuConf.endUser));
                // 生成token
                qiniuConf.token = AUTH.uploadToken(qiniuConf.commonConf.bucket, null, qiniuConf.commonConf.expiresSecond, policy);
                if (StringUtils.isEmpty(qiniuConf.token)) {
                    throw new Exception("获取七牛云TOKEN失败");
                }
                // 提前5分钟刷新token
                qiniuConf.expiresSecondTime = System.currentTimeMillis() + (qiniuConf.commonConf.expiresSecond - 5 * 60) * 1000;
                return qiniuConf.token;
            } catch (Exception e) {
                log.error("获取Qiniu云TOKEN错误", e);
                return null;
            }
        }

        private static String getAppId(String imei) {
            String allAppId = ConfigUtil.getApolloValue(APP_ID_KEY, DEFAULT_APP_ID);
            String appId = null;
            if (!allAppId.equals(DEFAULT_APP_ID)) {
                appId = DcImeiAppIdCache.getAppIdWithCache(imei);
                log.info("获取 imei:{} appId:{}", imei, appId);
            }
            if (StringUtils.isEmpty(appId)) {
                appId = DEFAULT_APP_ID;
            }
            return appId;
        }

        @Override
        public String toString() {
            return "QiniuConf{" + "token='" + token + '\'' + ", imei='" + imei + '\'' + ", fileName='" + fileName + '\'' + ", taskId='" + taskId + '\'' + ", platformId='" + platformId + '\'' + ", endUser='" + endUser + '\'' + ", downloadUrl='" + downloadUrl + '\'' + ", fileTypeEnum=" + fileTypeEnum + ", expiresSecondTime=" + expiresSecondTime + '}';
        }
    }

    /**
     * 文件类型
     */
    public enum FileTypeEnum {
        /**
         * JPG 图片
         */
        IMG((byte) 0x00, ".jpg"),
        /**
         * PNG 图片
         */
        PNG((byte) 0x00, ".png"),
        /**
         * WAV 录音
         */
        AUDIO((byte) 0x01, ".wav"),
        /**
         * 普通录音
         */
        AUDIO_ORDINARY((byte) 0x03, ".amr"),
        /**
         * 告警录音
         */
        AUDIO_ALARM((byte) 0x02, ".amr"),
        /**
         * MP4视频
         */
        MP4((byte) 0x02, ".mp4"),
        /**
         * 可执行bin文件
         */
        TEXT((byte) 0x03, ".bin"),
        /**
         * 其他文件
         */
        OTHER((byte) 0x04, ".tmp");

        byte type;
        String suffix;

        FileTypeEnum(byte type, String suffix) {
            this.type = type;
            this.suffix = suffix;
        }

        public static FileTypeEnum getSuffixEnum(String suffix) {
            for (FileTypeEnum item : FileTypeEnum.values()) {
                if (item.getSuffix().equalsIgnoreCase(suffix)) {
                    return item;
                }
            }
            return OTHER;
        }

        public byte getType() {
            return type;
        }

        public String getSuffix() {
            return suffix;
        }

        public void setType(byte type) {
            this.type = type;
        }

        public void setSuffix(String value) {
            this.suffix = value;
        }

        @Override
        public String toString() {
            return this.type + "-" + this.suffix.hashCode();
        }
    }

    /**
     * 触发类型,是否途强
     */
    public enum TriggerTypeEnum {
        /**
         * 发送指令触发
         */
        MANUAL("MANUAL", "发送指令触发"),
        /**
         * 针对无法区分的震动触发和声控触发类型
         */
        ACTIVE("ACTIVE", "自动触发统称"),
        /**
         * 震动触发
         */
        SHAKE("SHAKE", "震动触发"),
        /**
         * 声控触发
         */
        PHONIC("PHONIC", "声控触发");

        private String value;
        private String name;

        private TriggerTypeEnum(String value, String name) {
            this.value = value;
            this.name = name;
        }

        @Override
        public String toString() {
            return name;
        }

        public String getValue() {
            return value;
        }

        public String getName() {
            return name;
        }

        /**
         * 判断触发类型是否合法
         *
         * @return true-合法/false-不合法
         */
        public static boolean ifTriggerType(String value) {
            for (TriggerTypeEnum triggerTypeEnum : TriggerTypeEnum.values()) {
                if (triggerTypeEnum.getValue().equals(value)) {
                    return true;
                }
            }
            return false;
        }

        public static TriggerTypeEnum getTriggerType(String value) {
            for (TriggerTypeEnum triggerTypeEnum : TriggerTypeEnum.values()) {
                if (triggerTypeEnum.getValue().equals(value)) {
                    return triggerTypeEnum;
                }
            }
            return MANUAL;
        }
    }

    /**
     * 文件数据包
     */
    public static class FileData {
        /**
         * 协议上传的字节开始偏移量
         * -1表示协议没有上传偏移量
         */
        private int offset = -1;
        /**
         * 文件总大小
         */
        private int length;
        /**
         * 文件包数量
         */
        private int totalCount;
        /**
         * 文件包当前包序号
         */
        private int currentIndex;
        /**
         * 当前内容长度,当前包长度和offset至少需要有一个
         */
        private int currentLength;
        /**
         * 数据信息
         */
        private byte[] content;
        /**
         * 是否立即上传
         */
        private Boolean promptlyUpload;
        /**
         * 是否文件上传
         */
        private File uploadFile;
        /**
         * 是否写本地盘,如果不传,就根据默认规则判断 <br/>
         * QiniuUtil.isWriteLocalOrderingFile 方法
         */
        private Boolean localOrdering;
        /**
         * 乱序文件,需要调整写入的索引位置
         */
        private AdjustByteOrder adjustByteOrder;

        private FileData() {
        }

        public FileData offset(int offset) {
            this.offset = offset;
            return this;
        }

        public FileData length(int length) {
            this.length = length;
            return this;
        }

        public FileData totalCount(int totalCount) {
            this.totalCount = totalCount;
            return this;
        }

        public FileData currentIndex(int currentIndex) {
            this.currentIndex = currentIndex;
            return this;
        }

        public FileData currentLength(int currentLength) {
            this.currentLength = currentLength;
            return this;
        }

        public FileData content(byte[] content) {
            this.content = content;
            return this;
        }

        public FileData uploadFile(File uploadFile) {
            this.uploadFile = uploadFile;
            this.promptlyUpload = true;
            return this;
        }

        public FileData promptlyUpload(Boolean promptlyUpload) {
            this.promptlyUpload = promptlyUpload;
            return this;
        }

        public FileData localOrdering(Boolean localOrdering) {
            this.localOrdering = localOrdering;
            return this;
        }

        public int getOffset() {
            return offset;
        }

        public int getLength() {
            return length;
        }

        public int getTotalCount() {
            return totalCount;
        }

        public int getCurrentIndex() {
            return currentIndex;
        }

        public int getCurrentLength() {
            return currentLength;
        }

        public byte[] getContent() {
            return content;
        }

        public Boolean getPromptlyUpload() {
            return promptlyUpload;
        }

        public File getUploadFile() {
            return uploadFile;
        }

        public static FileData build() {
            return new FileData();
        }
    }

    /**
     * 文件上报乱序,需要调整文件byte
     */
    private static class AdjustByteOrder {
        private Long tempFirstIndex;

        private Long tempEndIndex;

        public AdjustByteOrder(long tempFirstIndex, long tempEndIndex) {
            this.tempFirstIndex = tempFirstIndex;
            this.tempEndIndex = tempEndIndex;
        }

        private boolean isOrganize(long firstIndex) {
            if (this.tempFirstIndex == null) {
                return false;
            }
            return tempFirstIndex != firstIndex;
        }
    }
}