فهرست منبع

场景批量下载

dsx 1 سال پیش
والد
کامیت
00c2bd5b87

+ 11 - 0
pom.xml

@@ -195,6 +195,17 @@
             <version>6.4.1</version>
         </dependency>
 
+        <dependency>
+            <groupId>com.fdkankan</groupId>
+            <artifactId>4dkankan-utils-model</artifactId>
+            <version>3.0.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fdkankan</groupId>
+            <artifactId>4dkankan-utils-fyun-oss</artifactId>
+            <version>3.0.0-SNAPSHOT</version>
+        </dependency>
 
     </dependencies>
 

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

@@ -0,0 +1,28 @@
+package com.fdkankan.download.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/download/bean/ImageTypeDetail.java

@@ -0,0 +1,27 @@
+package com.fdkankan.download.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;
+
+}

+ 109 - 0
src/main/java/com/fdkankan/download/bean/SceneEditControlsBean.java

@@ -0,0 +1,109 @@
+package com.fdkankan.download.bean;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * <p>
+ * TODO
+ * </p>
+ *
+ * @author dengsixing
+ * @since 2022/1/18
+ **/
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SceneEditControlsBean implements Serializable {
+
+    /**
+     * 是否展示小地图(0-不展示,1-展示)
+     */
+    private Integer showMap;
+
+    /**
+     * 是否需要密码(0-不需要,1-需要)
+     */
+    private Integer showLock;
+
+    /**
+     * 是否展示标题(0-不需要,1-需要)
+     */
+    private Integer showTitle;
+
+    /**
+     * 是否展示漫游按钮(0-不需要,1-需要)
+     */
+    private Integer showPanorama;
+
+    /**
+     * 是否展示3D按钮(0-不需要,1-需要)
+     */
+    private Integer showDollhouse;
+
+    /**
+     * 是否展示2D按钮(0-不需要,1-需要)
+     */
+    private Integer showFloorplan;
+
+    /**
+     * 是否展示VR(0-不需要,1-需要)
+     */
+    private Integer showVR;
+
+    /**
+     * 是否展示自动导览(0-不需要,1-需要)
+     */
+    private Integer showTour;
+
+    /**
+     * 是否展示测量线(0-不需要,1-需要)
+     */
+    private Integer showRule;
+
+    /**
+     * 是否展示标尺(0-不需要,1-需要)
+     */
+    private Integer showScale;
+
+    /**
+     * 是否展示分享场景(0-不需要,1-需要)
+     */
+    private Integer showShare;
+
+    /**
+     * 是否展示分享热点(0-不需要,1-需要)
+     */
+    private Integer showTagshare;
+
+    /**
+     * 是否展示合照开关(0-不需要,1-需要)
+     */
+    private Integer showCapture;
+
+    /**
+     * 多媒体标签标题
+     */
+    private Integer showTagTitle;
+
+    /**
+     * 指示牌标签标题
+     */
+    private Integer showBillboardTitle;
+
+    /**
+     * 视频监控标签标题
+     */
+    private Integer showCameraTitle;
+
+    /**
+     * 场景关联标签标题
+     */
+    private Integer showLinkTitle;
+
+}

+ 250 - 0
src/main/java/com/fdkankan/download/bean/SceneViewInfoBean.java

@@ -0,0 +1,250 @@
+package com.fdkankan.download.bean;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * <p>
+ * TODO
+ * </p>
+ *
+ * @author dengsixing
+ * @since 2022/1/19
+ **/
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SceneViewInfoBean implements Serializable {
+
+    private static final long serialVersionUID = 1l;
+
+    /**
+     * 场景码
+     */
+    private String num;
+
+    /**
+     * 地面logo名称
+     */
+    private String floorLogo;
+
+    /**
+     * 地面logo大小
+     */
+    private Integer floorLogoSize;
+
+    /**
+     * 地面logo文件名称
+     */
+    private String floorLogoFile;
+
+    /**
+     * 背景音乐
+     */
+    private String music;
+
+    /**
+     * 背景音乐文件名称
+     */
+    private String musicFile;
+
+    /**
+     * 浏览密码
+     */
+    private String scenePassword;
+
+    /**
+     * 场景标题
+     */
+    private String title;
+
+    /**
+     * 场景描述
+     */
+    private String description;
+
+    private SceneEditControlsBean controls;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+//    /**
+//     * 点位数量
+//     */
+//    private Integer panoCount;
+//
+//    /**
+//     * 球幕视频数量
+//     */
+//    private Integer videoCount;
+
+    /**
+     * 版本
+     */
+    private Integer version;
+
+    /**
+     * 图片版本
+     */
+    private Integer imgVersion;
+
+    /**
+     * 场景关联版本
+     */
+    private Integer linkVersion;
+
+    /**
+     * 是否上传了户型图(0-否,1-是)
+     */
+    private Byte floorPlanUser;
+
+//    private String cadInfo;
+//
+//    private Byte isUploadObj;
+//
+//    private Integer floorEditVer;
+//
+//    private Integer floorPublishVer;
+
+    /**
+     * 初始点信息
+     */
+    private String entry;
+
+    /**
+     * 全景图加载方式,tiles/1k:1k瓦片图,tiles/2:2k瓦片图,tiles/4k:4k瓦片图,pan:全景图 ,local:本地切片,cube:立体图
+     */
+    private String sceneResolution;
+
+    /**
+     * 场景来源,lite:双目lite相机,pro:八目相机,minion:双面转台相机,laser:激光相机,virtual:虚拟场景,sketch:图片建模场景
+     */
+    private String sceneFrom;
+
+    /**
+     * 切图方式(tiles:瓦片图,face:切片图,pan:全景图 ,local:本地切片,cube:立体图)
+     */
+    private String sceneKind;
+
+    /**
+     * 算法模型类型(dam,3dtiles)
+     */
+    private String modelKind;
+
+    /**
+     * 空间视频数据
+     */
+    private String boxVideos;
+    /**
+     * 空间贴图数据
+     */
+    private String boxPhotos;
+
+    /**
+     * 空间模型数据
+     */
+    private String boxModels;
+
+    /**
+     *点位视频
+     */
+    private String videos;
+
+    /**
+     * 是否有热点数据
+     */
+    private Integer tags;
+
+    /**
+     * 加载logo名
+     */
+    private String loadingLogo;
+
+    /**
+     * 加载logo文件名
+     */
+    private String loadingLogoFile;
+
+    /**
+     * 数据同步方式
+     */
+    private String dataSync;
+
+    /**
+     * 户型角度
+     */
+    private Float floorPlanAngle;
+
+    /**
+     * 指南针角度
+     */
+    private Float floorPlanCompass;
+
+    /**
+     * 用户上传自定义平面图
+     */
+    private JSONArray floorPlanUpload;
+
+    /**
+     * 是否保存导览
+     */
+    private Integer tours;
+
+    /**
+     * 是否有马赛克
+     */
+    private Integer mosaic;
+
+    /**
+     * 马赛克列表
+     */
+    private List<JSONObject> mosaicList;
+
+    /**
+     * 水印文件名
+     */
+    private String waterMark;
+
+    /**
+     * 是否有场景关联(0-否,1-是)
+     */
+    private Integer links;
+
+    /**
+     * 是否有滤镜(0-否,1-是)
+     */
+    private Integer filters;
+
+    /**
+     * 是否有监控摄像头数据
+     */
+    private Integer surveillances;
+
+    /**
+     * 场景容量 单位 MB
+     */
+    private Integer space;
+
+    /**
+     * 分享信息
+     */
+    private JSONObject sns;
+
+    /**
+     * 是否有指示牌(0-否,1-是)
+     */
+    private Integer billboards;
+
+
+}

+ 10 - 0
src/main/java/com/fdkankan/download/entity/DownloadLog.java

@@ -36,10 +36,20 @@ public class DownloadLog implements Serializable {
     private Integer isDelete;
 
     /**
+     * -1失败,1成功
+     */
+    private Integer status;
+
+    /**
      * kankan,laser
      */
     private String type;
 
+    /**
+     * 失败原因
+     */
+    private String reason;
+
     private Timestamp createTime;
 
     private Timestamp updateTime;

+ 6 - 0
src/main/java/com/fdkankan/download/service/IDownloadLogService.java

@@ -15,5 +15,11 @@ public interface IDownloadLogService extends IService<DownloadLog> {
 
     List<DownloadLog> getByNum(String num);
 
+    /**
+     * @param num 场景码
+     * @param type 类型 kankan、laser
+     */
+    void saveLog(String num, String type, Integer status, String reason);
+
 
 }

+ 7 - 0
src/main/java/com/fdkankan/download/service/IDownloadService.java

@@ -0,0 +1,7 @@
+package com.fdkankan.download.service;
+
+public interface IDownloadService {
+    void downloadHandler(String num) throws Exception;
+
+}
+

+ 10 - 0
src/main/java/com/fdkankan/download/service/impl/DownloadLogServiceImpl.java

@@ -22,4 +22,14 @@ public class DownloadLogServiceImpl extends ServiceImpl<DownloadLogMapper, Downl
     public List<DownloadLog> getByNum(String num) {
         return this.list(QueryWrapper.create().eq(DownloadLog::getNum, num));
     }
+
+    @Override
+    public void saveLog(String num, String type, Integer status, String reason) {
+        DownloadLog downloadLog = new DownloadLog();
+        downloadLog.setNum(num);
+        downloadLog.setType(type);
+        downloadLog.setStatus(status);
+        downloadLog.setReason(reason);
+        this.save(downloadLog);
+    }
 }

+ 457 - 0
src/main/java/com/fdkankan/download/service/impl/DownloadServiceImpl.java

@@ -0,0 +1,457 @@
+package com.fdkankan.download.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.io.FileUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.ZipUtil;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.fdkankan.common.constant.CommonStatus;
+import com.fdkankan.common.constant.ErrorCode;
+import com.fdkankan.common.constant.SceneDownloadProgressStatus;
+import com.fdkankan.common.constant.SceneKind;
+import com.fdkankan.common.exception.BusinessException;
+import com.fdkankan.common.util.FileUtils;
+import com.fdkankan.download.bean.ImageType;
+import com.fdkankan.download.bean.ImageTypeDetail;
+import com.fdkankan.download.bean.SceneEditControlsBean;
+import com.fdkankan.download.bean.SceneViewInfoBean;
+import com.fdkankan.download.entity.ScenePlus;
+import com.fdkankan.download.entity.ScenePlusExt;
+import com.fdkankan.download.service.IDownloadService;
+import com.fdkankan.download.service.IScenePlusExtService;
+import com.fdkankan.download.service.IScenePlusService;
+import com.fdkankan.redis.constant.RedisKey;
+import com.fdkankan.redis.util.RedisUtil;
+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.stereotype.Service;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.math.BigDecimal;
+import java.net.URLEncoder;
+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;
+import com.fdkankan.model.constants.UploadFilePath;
+import com.fdkankan.fyun.face.FYunFileServiceInterface;
+import com.fdkankan.fyun.constant.FYunTypeEnum;
+
+import javax.annotation.Resource;
+
+@Slf4j(topic = "IDownloadService")
+@Service
+public class DownloadServiceImpl implements IDownloadService {
+
+    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 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("${path.v4school}")
+    private String v4localPath;
+    @Value("${fyun.type:oss}")
+    private String uploadType;
+    @Value("${path.zip-local}")
+    private String zipLocalFormat;
+    @Value("${path.source-local}")
+    private String sourceLocal;
+    @Value("${fyun.bucket:4dkankan}")
+    private String bucket;
+    @Value("${download.config.public-url}")
+    private String publicUrl;
+    @Value("${path.v3school:#{null}}")
+    private String v3localPath;
+    @Value("${zip.nThreads}")
+    private int zipNthreads;
+    @Value("${download.config.resource-url}")
+    private String resourceUrl;
+    @Value("${path.zip-root}")
+    private String wwwroot;
+    @Value("${download.config.exe-content}")
+    private String exeContent;
+    @Value("${download.config.exe-content-v3:#{null}}")
+    private String exeContentV3;
+    @Value("${download.config.exe-name}")
+    private String exeName;
+    @Value("${path.zip-oss}")
+    private String zipOssFormat;
+
+    @Autowired
+    private IScenePlusService scenePlusService;
+    @Autowired
+    private IScenePlusExtService scenePlusExtService;
+    @Resource
+    private FYunFileServiceInterface fYunFileService;
+    @Autowired
+    private RedisUtil redisUtil;
+
+
+    @Override
+    public void downloadHandler(String num) throws Exception {
+
+        //zip包路径
+        String zipPath = null;
+        try {
+            TimeInterval timer = DateUtil.timer();
+
+            //删除资源目录
+            FileUtil.del(String.format(this.sourceLocal, num, ""));
+
+            ScenePlus scenePlus = scenePlusService.getByNum(num);
+            if(Objects.isNull(scenePlus))
+                throw new BusinessException(ErrorCode.FAILURE_CODE_5005);
+            ScenePlusExt scenePlusExt = scenePlusExtService.getByPlusId(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();
+            }
+
+            SceneViewInfoBean sceneViewInfo = this.getSceneJson(num);
+            String resolution = sceneViewInfo.getSceneResolution();
+            //国际版存在已经切好图的情况,下载时不需要再切图,只需要把文件直接下载下来打包就可以了
+            if(SceneKind.FACE.code().equals(sceneViewInfo.getSceneKind())){
+                resolution = "notNeadCut";
+            }
+
+            int imagesVersion = -1;
+            Integer version = sceneViewInfo.getVersion();
+            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, sceneViewInfo);
+
+            //写入启动命令
+            this.zipBat(num, "v4");
+
+            //打压缩包
+            ZipUtil.zip(String.format(this.sourceLocal, num, ""), zipPath);
+
+            // TODO: 2024/1/4 生成的压缩包放哪里待定
+//            String uploadPath = String.format(this.zipOssFormat, num);
+//            fYunFileService.uploadFileByCommand(bucket, zipPath, uploadPath);
+
+        }catch (Exception e){
+            //更新进度为下载失败
+            throw e;
+        }finally {
+            FileUtil.del(zipPath);
+            FileUtil.del(String.format(this.sourceLocal, num, ""));
+        }
+
+    }
+
+    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));
+    }
+
+    private void zipSceneJson(String num, SceneViewInfoBean sceneViewInfo) throws Exception{
+        //访问密码置0
+        SceneEditControlsBean controls = sceneViewInfo.getControls();
+        controls.setShowLock(CommonStatus.NO.code().intValue());
+        String sceneJsonPath = String.format(UploadFilePath.DATA_VIEW_PATH, num) + "scene.json";
+        FileUtil.writeUtf8String(JSON.toJSONString(sceneViewInfo, SerializerFeature.WriteNullStringAsEmpty, SerializerFeature.WriteNullNumberAsZero), String.format(this.sourceLocal, num, this.wwwroot + sceneJsonPath));
+    }
+
+    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("/")){
+            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);
+                return;
+            }
+        }
+
+        //其他文件打包
+        this.ProcessFiles(num, filePath, this.wwwroot, cacheKeys);
+
+    }
+
+    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 = fYunFileService.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{
+            try {
+                this.downloadFile(url, String.format(sourceLocal, num, prefix + key));
+            }catch (Exception e){
+                log.info("下载文件报错,path:{}", String.format(sourceLocal, num, prefix + key));
+            }
+        }
+    }
+
+    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("."));//0_skybox0.jpg
+        String ext = key.substring(key.lastIndexOf("."));
+        String[] arr = fileName.split("_skybox");
+        String dir = arr[0]; //0
+        String num = arr[1]; //0
+        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;
+            }
+
+//            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());
+
+            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();
+                if(FYunTypeEnum.AWS.code().equals(uploadType)){
+                    par += "&imagesVersion="+ imagesVersion;
+                }
+
+                var url = this.resourceUrl + key;
+                FYunTypeEnum storageType = FYunTypeEnum.get(uploadType);
+                switch (storageType){
+                    case OSS:
+                        url += par;
+                        break;
+                    case AWS:
+                        url += URLEncoder.encode(par.replace("/", "@"), "UTF-8");
+                        break;
+                }
+                //scene_view_data/t-jp-WXWxmOuj4Kf/images/tiles/0/4k_face_0_0_0.jpg
+                var fky = key.split("/" + resolution + "/")[0] + "/" + dir + "/" + imageType.getName() +  num + "_" + item.getI()  + "_" + item.getJ() + ext;
+                if(imgKeys.contains(fky)){
+                    continue;
+                }
+                imgKeys.add(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);
+    }
+
+    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));){
+                FileUtil.copy(localFilePath, localFilePath.replace(localPath, sourcePath), true);
+            }catch (Exception e){
+                throw e;
+            }
+        }
+        //写入code.txt
+        FileUtil.writeUtf8String(num, String.format(sourceLocal, num, "code.txt"));
+    }
+
+    private SceneViewInfoBean getSceneJson(String num){
+        String sceneJsonData = redisUtil.get(String.format(RedisKey.SCENE_JSON, num));
+        if(StrUtil.isEmpty(sceneJsonData)){
+            sceneJsonData = fYunFileService.getFileContent(bucket, String.format(UploadFilePath.DATA_VIEW_PATH, num) + "scene.json");
+        }
+        sceneJsonData = sceneJsonData.replace(this.publicUrl, "");
+        SceneViewInfoBean sceneInfoVO = JSON.parseObject(sceneJsonData, SceneViewInfoBean.class);
+        sceneInfoVO.setScenePassword(null);
+        if(Objects.isNull(sceneInfoVO.getFloorPlanAngle())){
+            sceneInfoVO.setFloorPlanAngle(0f);
+        }
+        if(Objects.isNull(sceneInfoVO.getFloorPlanCompass())){
+            sceneInfoVO.setFloorPlanCompass(0f);
+        }
+        SceneEditControlsBean controls = sceneInfoVO.getControls();
+        if(Objects.isNull(controls.getShowShare())){
+            controls.setShowShare(CommonStatus.YES.code().intValue());
+        }
+        if(Objects.isNull(controls.getShowCapture())){
+            controls.setShowCapture(CommonStatus.YES.code().intValue());
+        }
+        if(Objects.isNull(controls.getShowBillboardTitle())){
+            controls.setShowBillboardTitle(CommonStatus.YES.code().intValue());
+        }
+
+        return sceneInfoVO;
+    }
+
+    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 = fYunFileService.listRemoteFiles(bucket, prefix);
+            if(CollUtil.isEmpty(keys)){
+                continue;
+            }
+            if(FYunTypeEnum.AWS.code().equals(this.uploadType)){
+                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;
+    }
+}

+ 9 - 4
src/main/java/com/fdkankan/download/service/impl/GenSceneRunnerImpl.java

@@ -1,11 +1,13 @@
 package com.fdkankan.download.service.impl;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
 import cn.hutool.core.thread.ExecutorBuilder;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.system.HostInfo;
 import cn.hutool.system.SystemUtil;
 import com.fdkankan.common.constant.CommonStatus;
+import com.fdkankan.common.constant.CommonSuccessStatus;
 import com.fdkankan.common.constant.SceneSource;
 import com.fdkankan.download.entity.*;
 import com.fdkankan.download.service.*;
@@ -40,6 +42,8 @@ public class GenSceneRunnerImpl implements CommandLineRunner {
     private RedisUtil redisUtil;
     @Autowired
     private RedisLockUtil redisLockUtil;
+    @Autowired
+    private IDownloadService downloadService;
 
     private final static ThreadPoolExecutor threadPoolExecutor = ExecutorBuilder.create().setCorePoolSize(5).setMaxPoolSize(5).build();
 
@@ -80,10 +84,11 @@ public class GenSceneRunnerImpl implements CommandLineRunner {
                         if(!types.contains("laser")){
                             threadPoolExecutor.submit(() -> {
                                 try {
-
                                     // TODO: 2024/1/3 文杰实现
 
+                                    downloadLogService.saveLog(scenePlus.getNum(), "laser", CommonSuccessStatus.SUCCESS.code(), null);
                                 } catch (Exception e) {
+                                    downloadLogService.saveLog(scenePlus.getNum(), "laser", CommonSuccessStatus.FAIL.code(), ExceptionUtil.stacktraceToString(e, 3000));
                                     log.error("点云场景打包失败,num:{}", scenePlus.getNum(), e);
                                 }
                             });
@@ -98,10 +103,10 @@ public class GenSceneRunnerImpl implements CommandLineRunner {
                         if(!types.contains("kankan")) {
                             threadPoolExecutor.submit(() -> {
                                 try {
-
-                                    // TODO: 2024/1/3 老邓实现
-
+                                    downloadService.downloadHandler(scenePlus.getNum());
+                                    downloadLogService.saveLog(scenePlus.getNum(), "kankan", CommonSuccessStatus.SUCCESS.code(), null);
                                 } catch (Exception e) {
+                                    downloadLogService.saveLog(scenePlus.getNum(), "kankan", CommonSuccessStatus.FAIL.code(), ExceptionUtil.stacktraceToString(e, 3000));
                                     log.error("看看场景打包失败,num:{}", scenePlus.getNum(), e);
                                 }
                             });

+ 7 - 14
src/main/java/com/fdkankan/download/util/ImgUtil.java

@@ -3,28 +3,21 @@ package com.fdkankan.download.util;
 import cn.hutool.system.HostInfo;
 import cn.hutool.system.SystemUtil;
 import cn.hutool.system.oshi.OshiUtil;
+import com.fdkankan.common.util.CmdUtils;
 import oshi.software.os.OperatingSystem;
 import oshi.software.os.windows.WindowsOperatingSystem;
 
 public class ImgUtil {
 
-    public static void cutImage(int x, int y, int width, int height, String input, String output){
-        String cmd = null;
+    public static void cutImage(String input, String output) throws Exception {
+        String cmd = "%s %s %s";
         OperatingSystem os = OshiUtil.getOs();
         if (os instanceof WindowsOperatingSystem){
-            cmd = "go_build_crop_win.exe";
+            cmd = String.format("cropFolder.exe", input, output);
+            CmdUtils.callLine(cmd);
         }else{
-            cmd = " /opt/crop/go_crop";
+            cmd = String.format("/opt/crop/cropFolder", input, output);
+            CmdUtils.callLineSh(cmd);
         }
-        cmd = cmd + " ";
-
-
-
     }
-
-    public static void main(String[] args) {
-        HostInfo hostInfo = SystemUtil.getHostInfo();
-        System.out.println(hostInfo.toString());
-    }
-
 }

+ 44 - 5
src/main/resources/application.yml

@@ -3,6 +3,19 @@ server:
 spring:
   application:
     name: 4dkankan-scene-download-tool
+  redis:
+    host: 120.24.144.164
+    port: 6379
+    timeout: 6000ms
+    password: bgh0cae240
+    jedis:
+      pool:
+        max-active: 3  #连接池最大连接数(使用负值表示没有限制)
+        max-idle: 3 # 连接池中的最大空闲连接
+        min-idle: 1 # 连接池中的最小空闲连接
+        max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
+    lettuce:
+      shutdown-timeout: 0ms
 
 mybatis-flex:
   datasource:
@@ -29,11 +42,6 @@ api:
   downScene: /service/manage/scene/downScene?num=%s
   downloadProcess: /service/manage/scene/downloadProcess?num=%s
 
-download:
-  dir: D:\test2\
-  task: ${download.dir}task.txt
-  id: ${download.dir}id.txt
-
 forest:
   ## 日志总开关,打开/关闭Forest请求/响应日志(默认为 true)
   log-enabled: true
@@ -51,6 +59,37 @@ forest:
 logging:
   path: /home/backend/4dkankan_v4
 
+fyun:
+  type: oss
+  key: LTAIUrvuHqj8pvry
+  secret: JLOVl0k8Ke0aaM8nLMMiUAZ3EiiqI4
+  bucket: 4dkankan
+  #  endPoint: http://oss-cn-shenzhen-internal.aliyuncs.com
+  endPoint: http://oss-cn-shenzhen.aliyuncs.com
+  host: https://4dkk.4dage.com/
+
+zip:
+  nThreads: 10
+path:
+  v4school: /mnt/scene_download_statics/v4local/
+  source-local: /home/backend/downloads/scenes/%s/%s
+  zip-local: /home/backend/downloads/scenes/%s.zip
+  zip-root: wwwroot/
+  zip-oss: downloads/scenes/%s.zip
+download:
+  dir: D:\test2\
+  task: ${download.dir}task.txt
+  id: ${download.dir}id.txt
+  config:
+    public-url: https://4dkk.4dage.com/
+    resource-url: https://4dkankan.oss-cn-shenzhen-internal.aliyuncs.com/
+    # resource-url: https://4dkankan.oss-cn-shenzhen.aliyuncs.com/
+    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
+