dsx 2 år sedan
förälder
incheckning
682e79ade5

+ 55 - 0
src/main/java/com/fdkankan/scene/bean/CurrentDownloadNumUtil.java

@@ -0,0 +1,55 @@
+package com.fdkankan.scene.bean;
+
+import cn.hutool.core.collection.ConcurrentHashSet;
+
+/**
+ * <p>
+ * TODO
+ * </p>
+ *
+ * @author dengsixing
+ * @since 2022/3/29
+ **/
+public class CurrentDownloadNumUtil {
+
+    /**
+     * 正在下载的场景码
+     */
+    private static ConcurrentHashSet<String> downLoadingNumSet = new ConcurrentHashSet<>();
+
+    /**
+     * 正在下载的v3场景码
+     */
+    private static ConcurrentHashSet<String> downLoadingV3NumSet = new ConcurrentHashSet<>();
+
+    public static void addSceneNum(String num, String version){
+        if("v3".equals(version)){
+            downLoadingV3NumSet.add(num);
+            return;
+        }
+        downLoadingNumSet.add(num);
+    }
+
+    public static boolean containSceneNum(String num, String version){
+        if("v3".equals(version)){
+            return downLoadingV3NumSet.contains(num);
+        }
+        return downLoadingNumSet.contains(num);
+    }
+
+    public static void removeSceneNum(String num, String version){
+        if("v3".equals(version)){
+            downLoadingV3NumSet.remove(num);
+            return;
+        }
+        downLoadingNumSet.remove(num);
+    }
+
+    public static int cntDownloadingLocal(String version){
+        if("v3".equals(version)){
+            return downLoadingV3NumSet.size();
+        }
+        return downLoadingNumSet.size();
+    }
+
+}

+ 30 - 0
src/main/java/com/fdkankan/scene/bean/DownLoadProgressBean.java

@@ -0,0 +1,30 @@
+package com.fdkankan.scene.bean;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * 场景下载进度
+ * </p>
+ *
+ * @author dengsixing
+ * @since 2022/2/22
+ **/
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DownLoadProgressBean implements Serializable {
+
+    private String url;
+
+    private Integer percent;
+
+    private Integer status;
+
+}

+ 32 - 0
src/main/java/com/fdkankan/scene/bean/DownLoadTaskBean.java

@@ -0,0 +1,32 @@
+package com.fdkankan.scene.bean;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * TODO
+ * </p>
+ *
+ * @author dengsixing
+ * @since 2022/2/22
+ **/
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DownLoadTaskBean implements Serializable {
+
+    private Long userId;
+
+    private String num;
+
+    private String sceneNum;
+
+    private String type;
+
+}

+ 28 - 0
src/main/java/com/fdkankan/scene/bean/ImageType.java

@@ -0,0 +1,28 @@
+package com.fdkankan.scene.bean;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * <p>
+ * TODO
+ * </p>
+ *
+ * @author dengsixing
+ * @since 2022/2/24
+ **/
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ImageType {
+
+    private String name;
+
+    private String size;
+
+    private String[] ranges;
+
+}

+ 27 - 0
src/main/java/com/fdkankan/scene/bean/ImageTypeDetail.java

@@ -0,0 +1,27 @@
+package com.fdkankan.scene.bean;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * <p>
+ * TODO
+ * </p>
+ *
+ * @author dengsixing
+ * @since 2022/2/24
+ **/
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ImageTypeDetail {
+
+    private String i;
+    private String j;
+    private String x;
+    private String y;
+
+}

+ 52 - 0
src/main/java/com/fdkankan/scene/config/TaskPoolConfig.java

@@ -0,0 +1,52 @@
+package com.fdkankan.scene.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * <p>
+ * TODO
+ * </p>
+ *
+ * @author dengsixing
+ * @since 2022/2/22
+ **/
+@Configuration
+public class TaskPoolConfig {
+
+    @Bean("sceneDownLoadExecutror")
+    public ThreadPoolTaskExecutor sceneDownLoadExecutror(){
+        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
+        taskExecutor.setCorePoolSize(3);
+        taskExecutor.setMaxPoolSize(3);
+        taskExecutor.setQueueCapacity(3);
+        taskExecutor.setKeepAliveSeconds(60);
+        taskExecutor.setThreadNamePrefix("sceneDownLoadThread--");
+        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
+        taskExecutor.setAwaitTerminationSeconds(60);
+        return taskExecutor;
+    }
+
+//    @Bean
+//    public ExecutorCompletionService completionService(ThreadPoolTaskExecutor sceneDownLoadExecutror){
+//        return new ExecutorCompletionService<Integer>(sceneDownLoadExecutror);
+//    }
+
+    @Bean
+    public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
+        return new RestTemplate(factory);
+    }
+
+    @Bean
+    public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
+        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+        factory.setReadTimeout(5000);//ms
+        factory.setConnectTimeout(15000);//ms
+        return factory;
+    }
+
+}

+ 8 - 0
src/main/java/com/fdkankan/scene/oss/OssUtil.java

@@ -15,6 +15,7 @@ import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.regex.Matcher;
+import java.util.stream.Collectors;
 
 
 @Component
@@ -434,4 +435,11 @@ public class OssUtil {
     public String getFileContent(String pathKey) {
         return FileUtil.readUtf8String(FdkkLaserConfig.getProfile() + File.separator + pathKey);
     }
+
+    public List<String> listFiles(String pathKey) {
+        return FileUtil.loopFiles(
+                FdkkLaserConfig.getProfile() + File.separator + pathKey).stream().map(file->{
+            return file.getAbsolutePath().replace(FdkkLaserConfig.getProfile() + File.separator, "");
+        }).collect(Collectors.toList());
+    }
 }

+ 17 - 0
src/main/java/com/fdkankan/scene/schedule/ScheduleJob.java

@@ -1,6 +1,8 @@
 package com.fdkankan.scene.schedule;
 
+import com.fdkankan.redis.util.RedisUtil;
 import com.fdkankan.scene.service.ISceneAsynOperLogService;
+import com.fdkankan.scene.service.ISceneDownLoadService;
 import lombok.extern.log4j.Log4j2;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Scheduled;
@@ -12,6 +14,10 @@ public class ScheduleJob {
 
     @Autowired
     private ISceneAsynOperLogService sceneAsynOperLogService;
+    @Autowired
+    RedisUtil redisUtil;
+    @Autowired
+    ISceneDownLoadService sceneDownLoadService;
 
     /**
      * 每天凌晨一点执行
@@ -22,4 +28,15 @@ public class ScheduleJob {
         sceneAsynOperLogService.cleanDownloadPanorama();
         log.info("定时清除全景图压缩包完毕");
     }
+
+    @Scheduled(cron = "0/5 * * * * ? ")
+    public void job4SceneDownload() throws Exception {
+        sceneDownLoadService.process();
+    }
+
+    @Scheduled(cron = "0/5 * * * * ? ")
+    public void job4SceneV3Download() throws Exception {
+        sceneDownLoadService.processV3();
+    }
+
 }

+ 17 - 0
src/main/java/com/fdkankan/scene/service/ISceneDownLoadService.java

@@ -0,0 +1,17 @@
+package com.fdkankan.scene.service;
+
+/**
+ * <p>
+ * 场景下载接口
+ * </p>
+ *
+ * @author dengsixing
+ * @since 2022/2/22
+ **/
+public interface ISceneDownLoadService {
+
+    void process() throws Exception;
+
+    void processV3() throws Exception;
+
+}

+ 92 - 0
src/main/java/com/fdkankan/scene/service/impl/CheckProgressRunnerImpl.java

@@ -0,0 +1,92 @@
+package com.fdkankan.scene.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import com.alibaba.fastjson.JSON;
+import com.fdkankan.scene.bean.DownLoadTaskBean;
+import com.fdkankan.redis.constant.RedisKey;
+import com.fdkankan.redis.constant.RedisLockKey;
+import com.fdkankan.redis.util.RedisLockUtil;
+import com.fdkankan.redis.util.RedisUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * <p>
+ * 应用启动校验是否有下载任务正在进行,如有过就重新入队头,从新下载
+ * </p>
+ *
+ * @author dengsixing
+ * @since 2022/2/22
+ **/
+@Component
+public class CheckProgressRunnerImpl implements CommandLineRunner {
+
+    @Autowired
+    RedisUtil redisUtil;
+    @Autowired
+    RedisTemplate redisTemplate;
+    @Autowired
+    RedisLockUtil redisLockUtil;
+
+    @Override
+    public void run(String... args) throws Exception {
+
+        //这里考虑到如果是集群部署,可能多个节点启动时,只需要一个节点进来就行了,所以加一个分布式锁
+        String lockKey = String.format(RedisLockKey.LOCK_SCENE_DOWNLOAD_ING);
+        boolean lock = redisLockUtil.lock(lockKey, RedisKey.EXPIRE_TIME_1_MINUTE);
+        if(!lock){
+            return;
+        }
+        try {
+            //v4场景回队
+            this.pushBackDownloadTask();
+
+            //v3场景回队
+            this.pushBackDownloadTaskV3();
+
+        }finally {
+            redisLockUtil.unlockLua(lockKey);
+        }
+    }
+
+    private void pushBackDownloadTask(){
+        //查询还没下载完毕的场景
+        List<String> downLoadList = redisUtil.lGet(RedisKey.SCENE_DOWNLOAD_ING, 0, -1);
+        if(CollUtil.isEmpty(downLoadList)){
+            return;
+        }
+        //删除还没执行完毕的场景缓存
+        redisUtil.del(RedisKey.SCENE_DOWNLOAD_ING);
+
+        List<String> taskList = downLoadList.stream().map(num -> {
+            return JSON.toJSONString(DownLoadTaskBean.builder().num(num).type("local").build());
+        }).collect(Collectors.toList());
+
+        //从新入队
+        redisUtil.lLeftPushAll(RedisKey.SCENE_DOWNLOADS_TASK_V4, taskList);
+    }
+
+    private void pushBackDownloadTaskV3(){
+        //查询还没下载完毕的场景
+        List<String> downLoadList = redisUtil.lGet(RedisKey.SCENE_V3_DOWNLOAD_ING, 0, -1);
+        if(CollUtil.isEmpty(downLoadList)){
+            return;
+        }
+        //删除还没执行完毕的场景缓存
+        redisUtil.del(RedisKey.SCENE_V3_DOWNLOAD_ING);
+
+        List<String> taskList = downLoadList.stream().map(num -> {
+            return JSON.toJSONString(DownLoadTaskBean.builder().num(num).type("local").build());
+        }).collect(Collectors.toList());
+
+        //从新入队
+        redisUtil.lLeftPushAll(RedisKey.DOWNLOAD_TASK, taskList);
+    }
+
+
+}

+ 114 - 0
src/main/java/com/fdkankan/scene/service/impl/SceneDownLoadServiceImpl.java

@@ -0,0 +1,114 @@
+package com.fdkankan.scene.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.fdkankan.redis.constant.RedisKey;
+import com.fdkankan.redis.util.RedisUtil;
+import com.fdkankan.scene.bean.CurrentDownloadNumUtil;
+import com.fdkankan.scene.bean.DownLoadTaskBean;
+import com.fdkankan.scene.service.ISceneDownLoadService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.util.Objects;
+
+/**
+ * <p>
+ * TODO
+ * </p>
+ *
+ * @author dengsixing
+ * @since 2022/2/22
+ **/
+@Service
+public class SceneDownLoadServiceImpl implements ISceneDownLoadService {
+
+    @Value("${scene.download.threadMax:3}")
+    private Integer downloadThreadMax;
+
+    @Autowired
+    RedisUtil redisUtil;
+
+    @Autowired
+    SceneDownloadHandlerServiceImpl handlerService;
+
+
+    @Override
+    public void process() throws Exception {
+
+        //统计本节点正在下载任务数量
+        int downloadIngCnt = CurrentDownloadNumUtil.cntDownloadingLocal("v4");
+        //如果正在下载的场景大于最大线程数,不往下执行
+        if(downloadIngCnt >= downloadThreadMax){
+            return;
+        }
+
+        for(int i = 0; i < downloadThreadMax - downloadIngCnt; i++){
+            DownLoadTaskBean downLoadTaskBean = this.getTaskSceneNum("v4");
+            //获取任务队列中队头场景码,如果是空,标识没有场景要下载,则退出程序
+            if(Objects.isNull(downLoadTaskBean)){
+                continue;
+            }
+            handlerService.download(downLoadTaskBean);
+        }
+
+    }
+
+    @Override
+    public void processV3() throws Exception {
+
+        //统计本节点正在下载任务数量
+        int downloadIngCnt = CurrentDownloadNumUtil.cntDownloadingLocal("v3");
+        //如果正在下载的场景大于最大线程数,不往下执行
+        if(downloadIngCnt >= downloadThreadMax){
+            return;
+        }
+
+        for(int i = 0; i < downloadThreadMax - downloadIngCnt; i++){
+            DownLoadTaskBean downLoadTaskBean = this.getTaskSceneNum("v3");
+            //获取任务队列中队头场景码,如果是空,标识没有场景要下载,则退出程序
+            if(Objects.isNull(downLoadTaskBean)){
+                continue;
+            }
+            handlerService.downloadV3(downLoadTaskBean);
+        }
+
+    }
+
+    private DownLoadTaskBean getTaskSceneNum(String version) throws Exception{
+        //redis待下载任务出队
+        String taskkey = RedisKey.SCENE_DOWNLOADS_TASK_V4;
+        if("v3".equals(version)){
+            taskkey = RedisKey.DOWNLOAD_TASK;
+//            taskkey = "downloads:task:v3";
+        }
+        String downloadTask = redisUtil.lLeftPop(taskkey);
+        if(StrUtil.isEmpty(downloadTask)){
+            return null;
+        }
+        DownLoadTaskBean downLoadTaskBean = JSONUtil.toBean(downloadTask, DownLoadTaskBean.class);
+        String num = downLoadTaskBean.getNum();
+        if("v3".equals(version)){
+            num = downLoadTaskBean.getSceneNum();
+        }
+        if(downLoadTaskBean == null || StrUtil.isEmpty(num) || !"local".equals(downLoadTaskBean.getType())){
+            throw new Exception("下载任务数据不正确,downloadTask:" +  downloadTask);
+        }
+        //如果场景正在下载中,就直接丢弃
+        if(CurrentDownloadNumUtil.containSceneNum(num, version)){
+            return null;
+        }
+        //本地缓存入队
+        CurrentDownloadNumUtil.addSceneNum(num, version);
+        //正在下载任务入队
+        String downloadingKey = RedisKey.SCENE_DOWNLOAD_ING;
+        if("v3".equals(version)){
+            downloadingKey = RedisKey.SCENE_V3_DOWNLOAD_ING;
+        }
+        redisUtil.lLeftPush(downloadingKey, num);
+        return downLoadTaskBean;
+    }
+
+
+}

+ 685 - 0
src/main/java/com/fdkankan/scene/service/impl/SceneDownloadHandlerServiceImpl.java

@@ -0,0 +1,685 @@
+package com.fdkankan.scene.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ConcurrentHashSet;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.date.TimeInterval;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.ZipUtil;
+import cn.hutool.http.HttpUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.fdkankan.common.constant.ErrorCode;
+import com.fdkankan.common.constant.SceneDownloadProgressStatus;
+import com.fdkankan.common.constant.SceneKind;
+import com.fdkankan.common.constant.ServerCode;
+import com.fdkankan.common.exception.BusinessException;
+import com.fdkankan.common.util.FileUtils;
+import com.fdkankan.scene.bean.*;
+import com.fdkankan.scene.entity.ScenePlus;
+import com.fdkankan.scene.entity.ScenePlusExt;
+import com.fdkankan.scene.entity.ScenePro;
+import com.fdkankan.scene.oss.OssUtil;
+import com.fdkankan.scene.service.IScenePlusExtService;
+import com.fdkankan.scene.service.IScenePlusService;
+import com.fdkankan.scene.service.ISceneProService;
+import com.fdkankan.model.constants.UploadFilePath;
+import com.fdkankan.redis.constant.RedisKey;
+import com.fdkankan.redis.util.RedisUtil;
+import com.fdkankan.scene.bean.ImageType;
+import com.google.common.collect.Lists;
+import lombok.extern.slf4j.Slf4j;
+import lombok.var;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.io.File;
+import java.io.FileInputStream;
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+/**
+ * <p>
+ * TODO
+ * </p>
+ *
+ * @author dengsixing
+ * @since 2022/2/22
+ **/
+@Slf4j
+@Service
+public class SceneDownloadHandlerServiceImpl {
+
+    private static final String[] prefixArr = new String[]{
+        UploadFilePath.DATA_VIEW_PATH,
+        UploadFilePath.VOICE_VIEW_PATH,
+        UploadFilePath.VIDEOS_VIEW_PATH,
+        UploadFilePath.IMG_VIEW_PATH,
+        UploadFilePath.USER_VIEW_PATH,
+    };
+
+    private static final String[] prefixArr4v3 = new String[]{
+        "data/data%s/", "images/images%s/", "voice/voice%s/", "video/video%s/"
+    };
+
+    private static final List<ImageType> imageTypes = Lists.newArrayList();
+    static{
+        imageTypes.add(ImageType.builder().name("4k_face").size("4096").ranges(new String[]{"0", "511", "1023", "1535", "2047","2559","3071","3583"}).build());
+        imageTypes.add(ImageType.builder().name("2k_face").size("2048").ranges(new String[]{"0", "511", "1023", "1535"}).build());
+        imageTypes.add(ImageType.builder().name("1k_face").size("1024").ranges(new String[]{"0", "511"}).build());
+        imageTypes.add(ImageType.builder().name("512_face").size("512").ranges(new String[]{"0"}).build());
+    }
+
+    @Value("${url.v3.getInfo}")
+    private String v3GetInfoUrl;
+    @Value("${path.v4school}")
+    private String v4localPath;
+    @Value("${path.v3school}")
+    private String v3localPath;
+    @Value("${path.zip-local}")
+    private String zipLocalFormat;
+    @Value("${path.source-local}")
+    private String sourceLocal;
+    @Value("${path.zip-oss}")
+    private String zipOssFormat;
+    @Value("${path.zip-root}")
+    private String wwwroot;
+    @Value("${zip.nThreads}")
+    private int zipNthreads;
+    @Value("${fyun.bucket:4dkankan}")
+    private String bucket;
+    @Value("${fyun.type}")
+    private String uploadType;
+    @Value("${download.config.resource-url}")
+    private String resourceUrl;
+    @Value("${download.config.public-url}")
+    private String publicUrl;
+    @Value("${download.config.exe-name}")
+    private String exeName;
+    @Value("${download.config.exe-content}")
+    private String exeContent;
+    @Value("${download.config.exe-content-v3}")
+    private String exeContentV3;
+
+    @Autowired
+    private RedisUtil redisUtil;
+    @Resource
+    private OssUtil ossUtil;
+    @Autowired
+    private IScenePlusService scenePlusService;
+    @Autowired
+    private IScenePlusExtService scenePlusExtService;
+    @Autowired
+    private ISceneProService sceneProService;
+
+    @Async("sceneDownLoadExecutror")
+    public void download(DownLoadTaskBean downLoadTaskBean){
+        //场景码
+        String num = null;
+
+        try {
+            num = downLoadTaskBean.getNum();
+
+            log.info("场景下载开始 - num[{}] - threadName[{}]", num, Thread.currentThread().getName());
+
+            long startTime = Calendar.getInstance().getTimeInMillis();
+
+            //执行场景下载逻辑
+            this.downloadHandler(downLoadTaskBean);
+
+            //耗时
+            long consumeTime = Calendar.getInstance().getTimeInMillis() - startTime;
+
+            log.info("场景下载结束 - num[{}] - threadName[{}] - consumeTime[{}]", num, Thread.currentThread().getName(), consumeTime);
+
+        }catch (Exception e){
+            log.error(ExceptionUtil.stacktraceToString(e));
+        }finally {
+            if(StrUtil.isNotEmpty(num)){
+                //本地正在下载任务出队
+                CurrentDownloadNumUtil.removeSceneNum(num, "v4");
+                //删除正在下载任务
+                redisUtil.lRemove(RedisKey.SCENE_DOWNLOAD_ING, 1, num);
+            }
+        }
+    }
+
+    @Async("sceneDownLoadExecutror")
+    public void downloadV3(DownLoadTaskBean downLoadTaskBean){
+        //场景码
+        String num = null;
+
+        try {
+            num = downLoadTaskBean.getSceneNum();
+
+            log.info("v3场景下载开始 - num[{}] - threadName[{}]", num, Thread.currentThread().getName());
+
+            long startTime = Calendar.getInstance().getTimeInMillis();
+
+            //执行场景下载逻辑
+            this.downloadHandlerV3(downLoadTaskBean);
+
+            //耗时
+            long consumeTime = Calendar.getInstance().getTimeInMillis() - startTime;
+
+            log.info("v3场景下载结束 - num[{}] - threadName[{}] - consumeTime[{}]", num, Thread.currentThread().getName(), consumeTime);
+
+        }catch (Exception e){
+            log.error(ExceptionUtil.stacktraceToString(e));
+        }finally {
+            if(StrUtil.isNotEmpty(num)){
+                //本地正在下载任务出队
+                CurrentDownloadNumUtil.removeSceneNum(num, "v3");
+                //删除正在下载任务
+                redisUtil.lRemove(RedisKey.SCENE_V3_DOWNLOAD_ING, 1, num);
+            }
+        }
+    }
+
+    public void downloadHandler(DownLoadTaskBean downLoadTaskBean) throws Exception{
+
+        String num = downLoadTaskBean.getNum();
+        //zip包路径
+        String zipPath = null;
+        try {
+            TimeInterval timer = DateUtil.timer();
+
+            //删除资源目录
+            FileUtil.del(String.format(this.sourceLocal, num, ""));
+
+            ScenePlus scenePlus = scenePlusService.getScenePlusByNum(num);
+            if(Objects.isNull(scenePlus))
+                throw new BusinessException(ErrorCode.FAILURE_CODE_5005);
+            ScenePlusExt scenePlusExt = scenePlusExtService.getScenePlusExtByPlusId(scenePlus.getId());
+            String bucket = scenePlusExt.getYunFileBucket();
+
+            Set<String> cacheKeys = new ConcurrentHashSet<>();
+
+            Map<String, List<String>> allFiles = this.getAllFiles(num, v4localPath, bucket);
+            List<String> ossFilePaths = allFiles.get("ossFilePaths");
+            List<String> v4localFilePaths = allFiles.get("localFilePaths");
+
+            //key总个数
+            int total = ossFilePaths.size() + v4localFilePaths.size();
+            AtomicInteger count = new AtomicInteger(0);
+
+            //定义压缩包
+            zipPath = String.format(this.zipLocalFormat, num);
+            File zipFile = new File(zipPath);
+            if(!zipFile.getParentFile().exists()){
+                zipFile.getParentFile().mkdirs();
+            }
+
+            String sceneJsonData = ossUtil.getFileContent(String.format(UploadFilePath.DATA_VIEW_PATH, num) + "scene.json");
+            JSONObject sceneJson = JSONUtil.parseObj(sceneJsonData);
+            String resolution = sceneJson.getStr("sceneResolution");
+            //国际版存在已经切好图的情况,下载时不需要再切图,只需要把文件直接下载下来打包就可以了
+            if(SceneKind.FACE.code().equals(sceneJson.getStr("sceneKind"))){
+                resolution = "notNeadCut";
+            }
+
+            int imagesVersion = -1;
+            Integer version = sceneJson.getInt("version");
+            if(Objects.nonNull(version)){
+                imagesVersion = version;
+            }
+
+            //固定文件写入
+            this.zipLocalFiles(v4localFilePaths, v4localPath, num, count, total, "v4");
+            log.info("打包固定文件耗时, num:{}, time:{}", num, timer.intervalRestart());
+
+            //oss文件写入
+            this.zipOssFiles(ossFilePaths, num, count, total, resolution, imagesVersion, cacheKeys, "v4");
+            log.info("打包oss文件耗时, num:{}, time:{}", num, timer.intervalRestart());
+
+            //重新写入scene.json(去掉密码访问设置)
+            this.zipSceneJson(num, sceneJson);
+
+            //写入启动命令
+            this.zipBat(num, "v4");
+
+            //打压缩包
+            ZipUtil.zip(String.format(this.sourceLocal, num, ""), zipPath);
+
+            //上传压缩包
+            String uploadPath = String.format(this.zipOssFormat, num);
+            ossUtil.uploadFile(uploadPath, zipPath, false);
+
+            //更新进度100
+            String url = this.publicUrl + uploadPath + "?t=" + Calendar.getInstance().getTimeInMillis();
+            this.updateProgress(null, num, SceneDownloadProgressStatus.DOWNLOAD_SUCCESS.code(), url, "v4");
+
+        }catch (Exception e){
+            //更新进度为下载失败
+            this.updateProgress( null, num, SceneDownloadProgressStatus.DOWNLOAD_FAILED.code(), null, "v4");
+            throw e;
+        }finally {
+            FileUtil.del(zipPath);
+            FileUtil.del(String.format(this.sourceLocal, num, ""));
+        }
+    }
+
+    public void downloadHandlerV3(DownLoadTaskBean downLoadTaskBean) throws Exception{
+
+        String num = downLoadTaskBean.getSceneNum();
+        //zip包路径
+        String zipPath = null;
+
+        try {
+            TimeInterval timer = DateUtil.timer();
+
+            //删除资源目录
+            FileUtil.del(String.format(this.sourceLocal, num, ""));
+
+            ScenePro scenePro = sceneProService.getByNum(num);
+            if(Objects.isNull(scenePro))
+                throw new BusinessException(ErrorCode.FAILURE_CODE_5005);
+
+            Set<String> cacheKeys = new ConcurrentHashSet<>();
+
+            Map<String, List<String>> allFiles = this.getAllFilesV3(num, v3localPath, bucket);
+            List<String> ossFilePaths = allFiles.get("ossFilePaths");
+            List<String> v3localFilePaths = allFiles.get("localFilePaths");
+
+            //key总个数
+            int total = ossFilePaths.size() + v3localFilePaths.size();
+            AtomicInteger count = new AtomicInteger(0);
+            //定义压缩包
+            zipPath = String.format(this.zipLocalFormat, num);
+            File zipFile = new File(zipPath);
+            if(!zipFile.getParentFile().exists()){
+                zipFile.getParentFile().mkdirs();
+            }
+
+            int imagesVersion =0;
+            String resolution = "2k";
+            JSONObject getInfoJson = this.getInfo(num);
+            imagesVersion = getInfoJson.getInt("imagesVersion");
+            // 转台、激光显示4k图片
+            if(getInfoJson.getInt("sceneSource") == 3 || getInfoJson.getInt("sceneSource") == 4){
+                resolution = "4k";
+            }
+
+            //固定文件写入
+            timer.intervalRestart();
+            this.zipLocalFiles(v3localFilePaths, v3localPath, num, count, total, "v3");
+            log.info("打包固定文件耗时, num:{}, time:{}", num, timer.intervalRestart());
+
+            //oss文件写入
+            this.zipOssFiles(ossFilePaths, num, count, total, resolution, imagesVersion, cacheKeys, "v3");
+            log.info("打包oss文件耗时, num:{}, time:{}", num, timer.intervalRestart());
+
+            //重新写入scene.json(去掉密码访问设置)
+            this.zipGetInfoJson(num, getInfoJson);
+
+            //写入启动命令
+            this.zipBat(num, "v3");
+
+            //打压缩包
+            ZipUtil.zip(String.format(this.sourceLocal, num, ""), zipPath);
+
+            //上传压缩包
+            String uploadPath = String.format(this.zipOssFormat, num);
+            ossUtil.uploadFile(uploadPath, zipPath, false);
+
+            //更新进度100
+            String url = this.publicUrl + uploadPath + "?t=" + Calendar.getInstance().getTimeInMillis();
+            this.updateProgress(null, num, SceneDownloadProgressStatus.DOWNLOAD_SUCCESS.code(), url, "v3");
+
+        }catch (Exception e){
+            //更新进度为下载失败
+            this.updateProgress( null, num, SceneDownloadProgressStatus.DOWNLOAD_FAILED.code(), null, "v3");
+            throw e;
+        }finally {
+            FileUtil.del(zipPath);
+            FileUtil.del(String.format(this.sourceLocal, num, ""));
+        }
+    }
+
+    private JSONObject getInfo(String num){
+        String url = String.format(v3GetInfoUrl, num);
+        String getInfoResult = HttpUtil.get(url);
+        JSONObject jsonObject = JSONUtil.parseObj(getInfoResult);
+        if(Objects.isNull(jsonObject)
+            || !ServerCode.SUCCESS.code().equals(jsonObject.getInt("code"))
+            || Objects.isNull(jsonObject.getJSONObject("data"))){
+            throw new RuntimeException("获取getInfo信息失败,url=" + url);
+        }
+        JSONObject data = jsonObject.getJSONObject("data");
+        if (data.getInt("sceneSource") != 2)
+        {
+            data.set("sceneScheme", 3);
+        }
+        data.set("needKey", 0);
+        data.set("sceneKey", "");
+        return data;
+    }
+
+    private void zipOssFiles(List<String> ossFilePaths, String num, AtomicInteger count,
+        int total, String resolution, int imagesVersion, Set<String> cacheKeys, String version) throws Exception{
+        if(CollUtil.isEmpty(ossFilePaths)){
+            return;
+        }
+        String imageNumPath = String.format(UploadFilePath.IMG_VIEW_PATH, num);
+        if("v3".equals(version)){
+            imageNumPath = String.format("images/images%s/", num);
+        }
+        ExecutorService executorService = Executors.newFixedThreadPool(this.zipNthreads);
+        List<Future> futureList = new ArrayList<>();
+        for (String filePath : ossFilePaths) {
+            String finalImageNumPath = imageNumPath;
+            Callable<Boolean> call = new Callable() {
+                @Override
+                public Boolean call() throws Exception {
+                    zipOssFilesHandler(num, count, total, resolution,
+                        imagesVersion, cacheKeys,filePath, finalImageNumPath, version);
+                    return true;
+                }
+            };
+            futureList.add(executorService.submit(call));
+        }
+        //这里一定要加阻塞,不然会导致oss文件还没打包好,主程序已经结束返回了
+        Boolean zipSuccess = true;
+        for (Future future : futureList) {
+            try {
+                future.get();
+            }catch (Exception e){
+                log.error("打包oss文件失败", e);
+                zipSuccess = false;
+            }
+        }
+        if(!zipSuccess){
+            throw new Exception("打包oss文件失败");
+        }
+    }
+
+    private void zipOssFilesHandler(String num,
+        AtomicInteger count, int total, String resolution,
+        int imagesVersion, Set<String> cacheKeys,
+        String filePath, String imageNumPath, String version) throws Exception{
+
+        if(filePath.endsWith("/")){
+            //更新进度
+            this.updateProgress(new BigDecimal(count.incrementAndGet()).divide(new BigDecimal(total), 6, BigDecimal.ROUND_HALF_UP),
+                num, SceneDownloadProgressStatus.DOWNLOADING.code(), null, version);
+            return;
+        }
+
+        //某个目录不需要打包
+        if(filePath.contains(imageNumPath + "panorama/panorama_edit/"))
+            return;
+
+        //切图
+        if(!"notNeadCut".equals(resolution)){
+            if((filePath.contains(imageNumPath + "panorama/") && filePath.contains("tiles/" + resolution))
+                || filePath.contains(imageNumPath + "tiles/" + resolution + "/")) {
+                this.processImage(num, filePath, resolution, imagesVersion, cacheKeys);
+                //更新进度
+                this.updateProgress(new BigDecimal(count.incrementAndGet()).divide(new BigDecimal(total), 6, BigDecimal.ROUND_HALF_UP),
+                    num, SceneDownloadProgressStatus.DOWNLOADING.code(), null, version);
+                return;
+            }
+        }
+
+        //其他文件打包
+        this.ProcessFiles(num, filePath, this.wwwroot, cacheKeys);
+
+        //更新进度
+        this.updateProgress(new BigDecimal(count.incrementAndGet()).divide(new BigDecimal(total), 6, BigDecimal.ROUND_HALF_UP),
+            num, SceneDownloadProgressStatus.DOWNLOADING.code(), null, version);
+    }
+
+    private void zipLocalFiles(List<String> localFilePaths, String v3localPath, String num, AtomicInteger count, int total, String version) throws Exception{
+        String sourcePath = String.format(this.sourceLocal, num, "");
+        String localPath = "v4".equals(version) ? this.v4localPath : this.v3localPath;
+        for (String localFilePath : localFilePaths) {
+            try (FileInputStream in = new FileInputStream(new File(localFilePath));){
+//                this.zipInputStream(out, localFilePath.replace(v3localPath, ""), in);
+                FileUtil.copy(localFilePath, localFilePath.replace(localPath, sourcePath), true);
+            }catch (Exception e){
+                throw e;
+            }
+            //更新进度
+            this.updateProgress(
+                new BigDecimal(count.incrementAndGet()).divide(new BigDecimal(total), 6, BigDecimal.ROUND_HALF_UP),
+                num, SceneDownloadProgressStatus.DOWNLOAD_COMPRESSING.code(), null, version);
+        }
+        //写入code.txt
+//        this.zipBytes(out, "code.txt", num.getBytes());
+        FileUtil.writeUtf8String(num, String.format(sourceLocal, num, "code.txt"));
+    }
+
+    private void zipBat(String num, String version) throws Exception{
+        String batContent = String.format(this.exeContent, num);
+        if("v3".equals(version)){
+            batContent = String.format(this.exeContentV3, num);
+        }
+//        this.zipBytes(out, exeName, batContent.getBytes());
+        FileUtil.writeUtf8String(batContent, String.format(this.sourceLocal, num, exeName));
+
+        //更新进度为90%
+        this.updateProgress(new BigDecimal("0.9").divide(new BigDecimal("0.8"), 6, BigDecimal.ROUND_HALF_UP), num,
+            SceneDownloadProgressStatus.DOWNLOAD_COMPRESSING.code(), null, version);
+    }
+
+    private Map<String, List<String>> getAllFiles(String num, String v4localPath, String bucket) throws Exception{
+        //列出oss所有文件路径
+        List<String> ossFilePaths = new ArrayList<>();
+        for (String prefix : prefixArr) {
+            prefix = String.format(prefix, num);
+            List<String> keys = ossUtil.listFiles(prefix);
+            if(CollUtil.isEmpty(keys)){
+                continue;
+            }
+            keys = keys.stream().filter(key->{
+                if(key.contains("x-oss-process")){
+                    return false;
+                }
+                return true;
+            }).collect(Collectors.toList());
+            ossFilePaths.addAll(keys);
+        }
+
+        //列出v3local所有文件路径
+        File file = new File(v4localPath);
+        List<String> localFilePaths = FileUtils.list(file);
+
+        HashMap<String, List<String>> map = new HashMap<>();
+        map.put("ossFilePaths", ossFilePaths);
+        map.put("localFilePaths", localFilePaths);
+
+        return map;
+    }
+
+    private Map<String, List<String>> getAllFilesV3(String num, String v3localPath, String bucket) throws Exception{
+        //列出oss所有文件路径
+        List<String> ossFilePaths = new ArrayList<>();
+        for (String prefix : prefixArr4v3) {
+            prefix = String.format(prefix, num);
+            List<String> keys = ossUtil.listFiles(prefix);
+            if(CollUtil.isEmpty(keys)){
+                continue;
+            }
+            keys = keys.stream().filter(key->{
+                if(key.contains("x-oss-process")){
+                    return false;
+                }
+                return true;
+            }).collect(Collectors.toList());
+            ossFilePaths.addAll(keys);
+        }
+
+        //列出v3local所有文件路径
+        File file = new File(v3localPath);
+        List<String> localFilePaths = FileUtils.list(file);
+
+        HashMap<String, List<String>> map = new HashMap<>();
+        map.put("ossFilePaths", ossFilePaths);
+        map.put("localFilePaths", localFilePaths);
+
+        return map;
+    }
+
+    private void zipSceneJson(String num, JSONObject sceneJson) throws Exception{
+        //访问密码置0
+        JSONObject controls = sceneJson.getJSONObject("controls");
+        controls.set("showLock", 0);
+        String sceneJsonPath = String.format(UploadFilePath.DATA_VIEW_PATH, num) + "scene.json";
+        String sceneJsonStr = sceneJson.toString().replace(this.publicUrl, "");
+        FileUtil.writeUtf8String(sceneJsonStr, String.format(this.sourceLocal, num, this.wwwroot + sceneJsonPath));
+    }
+
+    private void zipGetInfoJson(String num, JSONObject getInfo) throws Exception{
+
+        //访问密码置0
+        String getInfoKey = String.format("data/data%s/", num) + "getInfo.json";
+        String getInfoStr = getInfo.toString().replace(this.publicUrl, "");
+        FileUtil.writeUtf8String(getInfoStr, String.format(this.sourceLocal, num, this.wwwroot + getInfoKey));
+    }
+
+    private void processImage(String sceneNum, String key, String resolution, int imagesVersion, Set<String> imgKeys) throws Exception{
+
+        if(key.contains("x-oss-process") || key.endsWith("/")){
+            return;
+        }
+
+        String fileName = key.substring(key.lastIndexOf("/")+1, key.indexOf("."));
+        String ext = key.substring(key.lastIndexOf("."));
+        String[] arr = fileName.split("_skybox");
+        String dir = arr[0];
+        String num = arr[1];
+        if(StrUtil.isEmpty(fileName)
+            || StrUtil.isEmpty(ext)
+            || (".jpg".equals(ext) && ".png".equals(ext))
+            ||  StrUtil.isEmpty(dir)
+            || StrUtil.isEmpty(num)){
+            throw new Exception("本地下载图片资源不符合规则,key:" + key);
+        }
+        for (ImageType imageType : imageTypes) {
+
+            if(imageType.getName().equals("4k_face") && !"4k".equals(resolution)){
+                continue;
+            }
+
+            List<ImageTypeDetail> items = Lists.newArrayList();
+            String[] ranges = imageType.getRanges();
+            for(int i = 0; i < ranges.length; i++){
+                String x = ranges[i];
+                for(int j = 0; j < ranges.length; j++){
+                    String y = ranges[j];
+                    items.add(
+                        ImageTypeDetail.builder()
+                            .i(String.valueOf(i))
+                            .j(String.valueOf(j))
+                            .x(x)
+                            .y(y)
+                            .build()
+                    );
+                }
+            }
+            for (ImageTypeDetail item : items) {
+                String par = "?x-oss-process=image/resize,m_lfit,w_" + imageType.getSize() + "/crop,w_512,h_512,x_" + item.getX() + ",y_" + item.getY();
+
+                var url = this.resourceUrl + key + par;
+                var fky = key.split("/" + resolution + "/")[0] + "/" + dir + "/" + imageType.getName() +  num + "_" + item.getI()  + "_" + item.getJ() + ext;
+                if(imgKeys.contains(fky)){
+                    continue;
+                }
+                imgKeys.add(fky);
+//                HttpUtil.downloadFile(url, String.format(sourceLocal, sceneNum, this.wwwroot + fky));
+                this.downloadFile(url, String.format(sourceLocal, sceneNum, this.wwwroot + fky));
+            }
+
+        }
+
+    }
+
+    public void downloadFile(String url, String path){
+        File file = new File(path);
+        if(!file.getParentFile().exists()){
+            file.getParentFile().mkdirs();
+        }
+        HttpUtil.downloadFile(url, path);
+    }
+
+    public void ProcessFiles(String num, String key, String prefix, Set<String> cacheKeys) throws Exception{
+        if(cacheKeys.contains(key)){
+            return;
+        }
+        if(key.equals(String.format(UploadFilePath.DATA_VIEW_PATH, num) + "scene.json")){
+            return;
+        }
+        cacheKeys.add(key);
+        String fileName = key.substring(key.lastIndexOf("/") + 1);
+//        String url = this.resourceUrl + key.replace(fileName, URLEncoder.encode(fileName, "UTF-8")) + "?t=" + Calendar.getInstance().getTimeInMillis();
+        if(key.contains("hot.json") || key.contains("link-scene.json")){
+            String content = ossUtil.getFileContent(key);
+            if(StrUtil.isEmpty(content)){
+                return;
+            }
+            content = content.replace(publicUrl, "")
+//                .replace(publicUrl+"v3/", "")
+                .replace("https://spc.html","spc.html")
+                .replace("https://smobile.html", "smobile.html");
+
+            FileUtil.writeUtf8String(content, String.format(sourceLocal, num, prefix + key));
+        }else{
+//            HttpUtil.downloadFile(url, String.format(sourceLocal, num, prefix + key));
+            try {
+                ossUtil.downloadFile(key, String.format(sourceLocal, num, prefix + key));
+//                this.downloadFile(url, String.format(sourceLocal, num, prefix + key));
+            }catch (Exception e){
+                log.info("下载文件报错,path:{}", String.format(sourceLocal, num, prefix + key));
+            }
+        }
+    }
+
+
+    public void updateProgress(BigDecimal precent, String num, Integer status, String url, String version){
+
+        SceneDownloadProgressStatus progressStatus = SceneDownloadProgressStatus.get(status);
+        switch (progressStatus){
+            case DOWNLOAD_SUCCESS:
+                precent = new BigDecimal("100");
+                break;
+            case DOWNLOAD_FAILED:
+                precent = new BigDecimal("0");
+                break;
+            default:
+                precent = precent.multiply(new BigDecimal("0.8")).multiply(new BigDecimal("100"));
+        }
+
+        DownLoadProgressBean progress = null;
+        String key = String.format(RedisKey.PREFIX_DOWNLOAD_PROGRESS_V4, num);
+        if("v3".equals(version)){
+            key = String.format(RedisKey.PREFIX_DOWNLOAD_PROGRESS, num);
+        }
+        String progressStr = redisUtil.get(key);
+        if(StrUtil.isEmpty(progressStr)){
+            progress =  DownLoadProgressBean.builder().percent(precent.intValue()).status(status).url(url).build();
+        }else{
+            progress = JSONUtil.toBean(progressStr, DownLoadProgressBean.class);
+            //如果下载失败,进度不变
+            if(status == SceneDownloadProgressStatus.DOWNLOAD_FAILED.code() && progress.getPercent() != null){
+                precent = new BigDecimal(progress.getPercent());
+            }
+            progress.setPercent(precent.intValue());
+            progress.setStatus(status);
+            progress.setUrl(url);
+        }
+        redisUtil.set(key, JSONUtil.toJsonStr(progress));
+
+    }
+
+}

+ 28 - 0
src/main/resources/application-standAloneProd.yml

@@ -90,5 +90,33 @@ scene:
     new:
       url: smg.html?m=
 
+##场景下载相关-----------------------------------------start
+zip:
+  nThreads: 10
+path:
+  v4school: /home/backend/4dkankan_v4/download/v4local/
+  v3school: /home/backend/4dkankan_v4/download/v3local/
+  source-local: /home/backend/downloads/scenes/%s/%s
+  zip-root: wwwroot/
+  zip-local: /home/backend/downloads/scenes/%s.zip
+  zip-oss: downloads/scenes/%s.zip
+download:
+  config:
+    public-url: https://4dkk.4dage.com/
+    resource-url:
+    exe-name: start-browser.bat
+    exe-content: | # | 表示不转义特殊字符
+      taskkill /f /t /im http.exe
+      start http://127.0.0.1:9000/spg.html?m=%s
+      http.exe -nc -p 9000 -r wwwroot
+    exe-content-v3: | # | 表示不转义特殊字符
+      taskkill /f /t /im http.exe
+      start http://127.0.0.1:9000/spc.html?m=%s
+      http.exe -nc -p 9000 -r wwwroot
+url:
+  v3:
+    getInfo:
+##场景下载相关-----------------------------------------end
+