Commit 93d6404befa8b43e3570bf1c8279f7531ec36164

Authored by 王鑫
1 parent b3e721df

fix():添加历史回放视频上传和下载

src/main/java/com/genersoft/iot/vmp/vmanager/jt1078/platform/Jt1078OfCarController.java
... ... @@ -5,6 +5,7 @@
5 5  
6 6 package com.genersoft.iot.vmp.vmanager.jt1078.platform;
7 7  
  8 +import cn.hutool.core.io.resource.InputStreamResource;
8 9 import com.alibaba.fastjson2.JSON;
9 10 import com.alibaba.fastjson2.JSONArray;
10 11 import com.alibaba.fastjson2.JSONException;
... ... @@ -25,17 +26,20 @@ import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
25 26 import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
26 27 import com.genersoft.iot.vmp.vmanager.bean.StreamPlayPath;
27 28 import com.genersoft.iot.vmp.vmanager.jt1078.platform.ben.HttpClientPostEntity;
  29 +import com.genersoft.iot.vmp.vmanager.jt1078.platform.config.FtpConfigBean;
28 30 import com.genersoft.iot.vmp.vmanager.jt1078.platform.config.Jt1078ConfigBean;
29 31 import com.genersoft.iot.vmp.vmanager.jt1078.platform.config.RtspConfigBean;
30 32 import com.genersoft.iot.vmp.vmanager.jt1078.platform.config.TuohuaConfigBean;
  33 +import com.genersoft.iot.vmp.vmanager.jt1078.platform.domain.HistoryEnum;
31 34 import com.genersoft.iot.vmp.vmanager.jt1078.platform.domain.HistoryRecord;
32 35 import com.genersoft.iot.vmp.vmanager.jt1078.platform.domain.PatrolDataReq;
33 36 import com.genersoft.iot.vmp.vmanager.jt1078.platform.handler.HttpClientUtil;
34   -import com.genersoft.iot.vmp.vmanager.jt1078.platform.service.FlowService;
35 37 import com.genersoft.iot.vmp.vmanager.jt1078.platform.service.HisToryRecordService;
36 38 import com.genersoft.iot.vmp.vmanager.jt1078.platform.service.Jt1078OfService;
37 39 import com.genersoft.iot.vmp.vmanager.streamProxy.StreamProxyController;
38 40 import com.genersoft.iot.vmp.vmanager.streamPush.StreamPushController;
  41 +import com.genersoft.iot.vmp.vmanager.util.FtpUtils;
  42 +import com.genersoft.iot.vmp.vmanager.util.MD5Util;
39 43 import org.apache.commons.collections4.CollectionUtils;
40 44 import org.apache.commons.collections4.MapUtils;
41 45 import org.apache.commons.lang3.RandomUtils;
... ... @@ -48,17 +52,23 @@ import org.slf4j.LoggerFactory;
48 52 import org.springframework.beans.factory.annotation.Autowired;
49 53 import org.springframework.beans.factory.annotation.Value;
50 54 import org.springframework.data.redis.core.RedisTemplate;
  55 +import org.springframework.http.HttpHeaders;
  56 +import org.springframework.http.MediaType;
  57 +import org.springframework.http.ResponseEntity;
  58 +import org.springframework.scheduling.annotation.Scheduled;
51 59 import org.springframework.util.Base64Utils;
52 60 import org.springframework.web.bind.annotation.*;
53 61 import sun.misc.Signal;
54 62 import sun.misc.SignalHandler;
55 63  
  64 +import javax.annotation.PostConstruct;
56 65 import javax.annotation.Resource;
57 66 import javax.crypto.Cipher;
58 67 import javax.servlet.http.HttpServletRequest;
59 68 import javax.validation.constraints.NotBlank;
60 69 import java.io.ByteArrayOutputStream;
61 70 import java.io.IOException;
  71 +import java.io.InputStream;
62 72 import java.net.URISyntaxException;
63 73 import java.security.Key;
64 74 import java.security.KeyFactory;
... ... @@ -71,6 +81,7 @@ import java.util.concurrent.ArrayBlockingQueue;
71 81 import java.util.concurrent.ConcurrentHashMap;
72 82 import java.util.concurrent.ThreadPoolExecutor;
73 83 import java.util.concurrent.TimeUnit;
  84 +import java.util.function.Function;
74 85 import java.util.stream.Collectors;
75 86  
76 87 import static com.genersoft.iot.vmp.utils.DateUtil.formatter;
... ... @@ -105,18 +116,22 @@ public class Jt1078OfCarController {
105 116 @Autowired
106 117 private IStreamProxyService streamProxyService;
107 118 @Resource
108   - private FlowService flowService;
109   - @Resource
110 119 private Jt1078OfService jt1078OfService;
111 120 @Resource
112 121 private HisToryRecordService hisToryRecordService;
  122 + @Resource
  123 + private FtpConfigBean ftpConfigBean;
113 124 //存储历史端口 key -->端口 value ---> sim-channel-startTime-endTime-port
114 125 public static final ConcurrentHashMap<Integer, Set<String>> map = new ConcurrentHashMap<>();
115 126  
  127 + public static ConcurrentHashMap<String, HistoryRecord> HISTORY_RECORD_MAP = new ConcurrentHashMap<>();
  128 +
116 129 private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 40, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
117 130  
118 131 @Value("${spring.profiles.active}")
119 132 private String profilesActive;
  133 + @Autowired
  134 + private FtpUtils ftpUtils;
120 135  
121 136  
122 137 public Jt1078OfCarController() {
... ... @@ -153,7 +168,10 @@ public class Jt1078OfCarController {
153 168 return resultMap;
154 169 }
155 170  
156   -
  171 + /**
  172 + * 上传历史视频到FTP
  173 + * @param stream 流名称
  174 + */
157 175 @GetMapping("/history/uploading/{stream}")
158 176 public void uploading(@PathVariable String stream) {
159 177 try {
... ... @@ -161,11 +179,113 @@ public class Jt1078OfCarController {
161 179 String msg = jt1078ConfigBean.formatMessageHistoryUpload(stream);
162 180 HttpClientPostEntity httpClientPost = this.httpClientUtil.doPost(url, msg, (String) null);
163 181 chooseEntity(httpClientPost, url, false);
  182 + String[] split = stream.split("_");
  183 + String sim = split[0];
  184 + String channel = split[1];
  185 + String startTime = split[2];
  186 + String endTime = split[3];
  187 + hisToryRecordService.addOrUpdRecode(
  188 + HistoryRecord.builder().ip(UUID.randomUUID().toString())
  189 + .name(stream)
  190 + .createTime(new Date())
  191 + .sim(sim)
  192 + .channel(channel)
  193 + .startTime(startTime)
  194 + .endTime(endTime)
  195 + .status(HistoryEnum.UPLOADING.getCode())
  196 + .ip(ftpConfigBean.getHost())
  197 + .port(ftpConfigBean.getPort())
  198 + .username(ftpConfigBean.getUsername())
  199 + .password(MD5Util.encrypt(ftpConfigBean.getPassword()))
  200 + .path(StringUtils.join(ftpConfigBean.getBasePath(),"/",sim,"/channel_",channel))
  201 + .build());
  202 + } catch (Exception e) {
  203 + throw new RuntimeException(e);
  204 + }
  205 + }
  206 +
  207 + /**
  208 + * 定时更新
  209 + */
  210 + @Scheduled(cron = "0 */2 * * * *")
  211 + private void HistoryRecordScheduled(){
  212 + updateHistoryRecord();
  213 + }
  214 +
  215 + /**
  216 + * 项目启动更新
  217 + */
  218 + @PostConstruct
  219 + public void init() {
  220 + updateHistoryRecord();
  221 + }
  222 +
  223 + /**
  224 + * 更新历史视频上传记录
  225 + */
  226 + @GetMapping("/history/updateHistoryRecord")
  227 + public void updateHistoryRecord(){
  228 + List<HistoryRecord> list = hisToryRecordService.getHistoryRecordList();
  229 + if (CollectionUtils.isNotEmpty(list)) {
  230 + List<HistoryRecord> expiredList = list.stream().filter(HistoryRecord::checkCreateTimeExpiration).collect(Collectors.toList());
  231 + if (CollectionUtils.isNotEmpty(expiredList)) {
  232 + try {
  233 + expiredList = ftpUtils.batchDelete(expiredList);
  234 + hisToryRecordService.beachUpdRecode(expiredList);
  235 + } catch (IOException e) {
  236 + log.error("删除FTP文件失败");
  237 + throw new RuntimeException(e);
  238 + }
  239 + }
  240 + List<HistoryRecord> uploadedList = list.stream().filter(HistoryRecord::checkCreateTimeNotExpiration)
  241 + .filter(ftpUtils::checkHistoryVideo).collect(Collectors.toList());
  242 + if (CollectionUtils.isNotEmpty(expiredList)) {
  243 + hisToryRecordService.beachUpdRecode(uploadedList);
  244 + }
  245 + HISTORY_RECORD_MAP = hisToryRecordService.getHistoryRecordList().stream().collect(Collectors.toMap(
  246 + HistoryRecord::getName, // 使用名字作为键
  247 + Function.identity(), // 用户对象作为值
  248 + (existing, replacement) -> {
  249 + throw new IllegalArgumentException(String.format("Duplicate name found: %s", existing.getName()));
  250 + },
  251 + ConcurrentHashMap::new // 保持插入顺序
  252 + ));
  253 + }
  254 + }
  255 +
  256 + /**
  257 + * 下载历史视频
  258 + * @param stream 流名称
  259 + */
  260 + @GetMapping("/history/download/{stream}")
  261 + public ResponseEntity<InputStreamResource> download(@PathVariable String stream) {
  262 + try {
  263 + HistoryRecord historyRecord = hisToryRecordService.getHistoryRecord(stream);
  264 + if (historyRecord == null) {
  265 + throw new RuntimeException("没有该流的上传记录,请先上传文件");
  266 + }
  267 + if (!historyRecord.getStatus().equals(HistoryEnum.UPLOADED.getCode())) {
  268 + throw new RuntimeException("该文件未上传完成请稍后重试 !!!");
  269 + }
  270 + InputStream inputStream = ftpUtils.downloadFile(historyRecord);
  271 +
  272 + // 设置HTTP头部信息
  273 + HttpHeaders headers = new HttpHeaders();
  274 + headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + extractFileNameFromPath(historyRecord.getPath()));
  275 + headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
  276 +
  277 + return ResponseEntity.ok()
  278 + .headers(headers)
  279 + .body(new InputStreamResource(inputStream));
164 280 } catch (Exception e) {
165 281 throw new RuntimeException(e);
166 282 }
167 283 }
168 284  
  285 + // 从路径中提取文件名的方法示例
  286 + private String extractFileNameFromPath(String ftpFilePath) {
  287 + return ftpFilePath.substring(ftpFilePath.lastIndexOf('/') + 1);
  288 + }
169 289  
170 290 /**
171 291 * redis清除在线车辆
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/jt1078/platform/config/FtpConfigBean.java
... ... @@ -48,4 +48,8 @@ public class FtpConfigBean {
48 48 * 失败重试间隔时间
49 49 */
50 50 private Integer retryWaitTimes = 3000;
  51 + /**
  52 + * 过期天数自动删除 默认7天
  53 + */
  54 + private long expirationTime = 7;
51 55 }
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/jt1078/platform/controller/HistoryRecordController.java
... ... @@ -27,8 +27,8 @@ public class HistoryRecordController {
27 27 * @param record 历史视频下载记录对象
28 28 */
29 29 @PostMapping("/addRecode")
30   - public String addRecode(@RequestBody HistoryRecord record) {
31   - return hisToryRecordService.addRecode(record);
  30 + public String addOrUpdRecode(@RequestBody HistoryRecord record) {
  31 + return hisToryRecordService.addOrUpdRecode(record);
32 32 }
33 33  
34 34  
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/jt1078/platform/domain/HistoryEnum.java 0 → 100644
  1 +package com.genersoft.iot.vmp.vmanager.jt1078.platform.domain;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Getter;
  5 +import lombok.NoArgsConstructor;
  6 +
  7 +/**
  8 + * 历史视频枚举
  9 + *
  10 + * @Author WangXin
  11 + * @Data 2025/2/23
  12 + * @Version 1.0.0
  13 + */
  14 +@Getter
  15 +@AllArgsConstructor
  16 +@NoArgsConstructor
  17 +public enum HistoryEnum {
  18 + /**
  19 + * 未上传
  20 + */
  21 + NOT_UPLOADED("0","未上传"),
  22 + /**
  23 + * 上传中
  24 + */
  25 + UPLOADING("2","上传中"),
  26 + /**
  27 + * 已上传
  28 + */
  29 + UPLOADED("3","已上传");
  30 +
  31 + /**
  32 + * code码
  33 + */
  34 + private String code;
  35 + /**
  36 + * 名称
  37 + */
  38 + private String name;
  39 +}
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/jt1078/platform/domain/HistoryRecord.java
1 1 package com.genersoft.iot.vmp.vmanager.jt1078.platform.domain;
2 2  
3 3 import com.fasterxml.jackson.annotation.JsonFormat;
  4 +import com.genersoft.iot.vmp.vmanager.jt1078.platform.config.FtpConfigBean;
  5 +import com.genersoft.iot.vmp.vmanager.util.FtpUtils;
4 6 import lombok.AllArgsConstructor;
5 7 import lombok.Data;
6 8 import lombok.NoArgsConstructor;
7 9 import lombok.experimental.SuperBuilder;
8 10 import org.springframework.format.annotation.DateTimeFormat;
9 11  
  12 +import java.time.LocalDate;
  13 +import java.time.ZoneId;
  14 +import java.time.temporal.ChronoUnit;
10 15 import java.util.Date;
11 16  
  17 +import static com.genersoft.iot.vmp.VManageBootstrap.getBean;
  18 +
12 19 /**
13 20 * 历史视频下载记录类
14 21 *
... ... @@ -48,7 +55,7 @@ public class HistoryRecord {
48 55 /**
49 56 * FTP端口
50 57 */
51   - private String port;
  58 + private Integer port;
52 59 /**
53 60 * FTP账号
54 61 */
... ... @@ -58,6 +65,18 @@ public class HistoryRecord {
58 65 */
59 66 private String password;
60 67 /**
  68 + * FTP存储地址
  69 + */
  70 + private String path;
  71 + /**
  72 + * 结束时间
  73 + */
  74 + private String startTime;
  75 + /**
  76 + * 开始时间
  77 + */
  78 + private String endTime;
  79 + /**
61 80 * 修改时间
62 81 */
63 82 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
... ... @@ -82,4 +101,21 @@ public class HistoryRecord {
82 101 */
83 102 private String remark;
84 103  
  104 + public static boolean checkCreateTimeExpiration(HistoryRecord historyRecord) {
  105 + long l = calculateDaysBetween(historyRecord.createTime);
  106 + return l > getBean(FtpConfigBean.class).getExpirationTime();
  107 + }
  108 +
  109 + public static boolean checkCreateTimeNotExpiration(HistoryRecord historyRecord) {
  110 + long l = calculateDaysBetween(historyRecord.createTime);
  111 + return l < getBean(FtpConfigBean.class).getExpirationTime();
  112 + }
  113 +
  114 +
  115 + private static long calculateDaysBetween(Date startDate) {
  116 + // 将Date转换为LocalDate
  117 + LocalDate startLocalDate = startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
  118 + // 计算两个LocalDate对象之间的天数差异
  119 + return ChronoUnit.DAYS.between(startLocalDate, LocalDate.now());
  120 + }
85 121 }
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/jt1078/platform/mapper/HisToryRecordMapper.java
... ... @@ -2,6 +2,9 @@ package com.genersoft.iot.vmp.vmanager.jt1078.platform.mapper;
2 2  
3 3 import com.genersoft.iot.vmp.vmanager.jt1078.platform.domain.HistoryRecord;
4 4 import org.apache.ibatis.annotations.Mapper;
  5 +import org.apache.ibatis.annotations.Param;
  6 +
  7 +import java.util.List;
5 8  
6 9 /**
7 10 * @Author WangXin
... ... @@ -15,5 +18,17 @@ public interface HisToryRecordMapper {
15 18 * 添加历史下载记录
16 19 * @param record 历史视频下载记录对象
17 20 */
18   - Integer addRecode(HistoryRecord record);
  21 + int addOrUpdRecode(HistoryRecord record);
  22 + /**
  23 + * 修改历史视频记录
  24 + */
  25 + int beachUpdRecode(@Param("list") List<HistoryRecord> record);
  26 + /**
  27 + * 历史视频上传记录列表
  28 + */
  29 + List<HistoryRecord> getHistoryRecordList();
  30 + /**
  31 + * 根据stream获取历史视频对象
  32 + */
  33 + HistoryRecord getHistoryRecord(@Param("stream") String stream);
19 34 }
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/jt1078/platform/service/HisToryRecordService.java
... ... @@ -2,6 +2,8 @@ package com.genersoft.iot.vmp.vmanager.jt1078.platform.service;
2 2  
3 3 import com.genersoft.iot.vmp.vmanager.jt1078.platform.domain.HistoryRecord;
4 4  
  5 +import java.util.List;
  6 +
5 7 /**
6 8 * @Author WangXin
7 9 * @Data 2025/2/22
... ... @@ -12,5 +14,18 @@ public interface HisToryRecordService {
12 14 * 添加历史下载记录
13 15 * @param record 历史视频下载记录对象
14 16 */
15   - String addRecode(HistoryRecord record);
  17 + String addOrUpdRecode(HistoryRecord record);
  18 + /**
  19 + * 修改历史视频记录
  20 + */
  21 + String beachUpdRecode(List<HistoryRecord> record);
  22 + /**
  23 + * 历史视频上传记录列表
  24 + */
  25 + List<HistoryRecord> getHistoryRecordList();
  26 + /**
  27 + * 根据stream获取历史视频对象
  28 + */
  29 + HistoryRecord getHistoryRecord(String stream);
  30 +
16 31 }
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/jt1078/platform/service/impl/HisToryRecordServiceImpl.java
... ... @@ -4,8 +4,14 @@ import com.genersoft.iot.vmp.vmanager.jt1078.platform.domain.HistoryRecord;
4 4 import com.genersoft.iot.vmp.vmanager.jt1078.platform.mapper.HisToryRecordMapper;
5 5 import com.genersoft.iot.vmp.vmanager.jt1078.platform.service.HisToryRecordService;
6 6 import lombok.AllArgsConstructor;
  7 +import org.springframework.data.redis.cache.RedisCache;
7 8 import org.springframework.stereotype.Service;
8 9  
  10 +import javax.annotation.Resource;
  11 +import java.util.Collections;
  12 +import java.util.Date;
  13 +import java.util.List;
  14 +
9 15 /**
10 16 * @Author WangXin
11 17 * @Data 2025/2/22
... ... @@ -16,12 +22,35 @@ import org.springframework.stereotype.Service;
16 22 public class HisToryRecordServiceImpl implements HisToryRecordService {
17 23  
18 24 private final HisToryRecordMapper hisToryRecordMapper;
  25 + @Resource
  26 + private RedisCache redisCache;
19 27 /**
20 28 * 添加历史下载记录
21 29 * @param record 历史视频下载记录对象
22 30 */
23 31 @Override
24   - public String addRecode(HistoryRecord record) {
25   - return hisToryRecordMapper.addRecode(record) > 0 ? "success" : "fail";
  32 + public String addOrUpdRecode(HistoryRecord record) {
  33 + return hisToryRecordMapper.addOrUpdRecode(record) > 0 ? "success" : "fail";
  34 + }
  35 + /**
  36 + * 修改历史视频记录
  37 + */
  38 + @Override
  39 + public String beachUpdRecode(List<HistoryRecord> record) {
  40 + return hisToryRecordMapper.beachUpdRecode(record) > 0 ? "success" : "fail";
  41 + }
  42 + /**
  43 + * 历史视频上传记录列表
  44 + */
  45 + @Override
  46 + public List<HistoryRecord> getHistoryRecordList() {
  47 + return hisToryRecordMapper.getHistoryRecordList();
  48 + }
  49 + /**
  50 + * 根据stream获取历史视频对象
  51 + */
  52 + @Override
  53 + public HistoryRecord getHistoryRecord(String stream) {
  54 + return hisToryRecordMapper.getHistoryRecord(stream);
26 55 }
27 56 }
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/util/FtpUtils.java
... ... @@ -2,6 +2,8 @@ package com.genersoft.iot.vmp.vmanager.util;
2 2  
3 3 import cn.hutool.extra.ftp.FtpException;
4 4 import com.genersoft.iot.vmp.vmanager.jt1078.platform.config.FtpConfigBean;
  5 +import com.genersoft.iot.vmp.vmanager.jt1078.platform.domain.HistoryEnum;
  6 +import com.genersoft.iot.vmp.vmanager.jt1078.platform.domain.HistoryRecord;
5 7 import lombok.RequiredArgsConstructor;
6 8 import lombok.extern.log4j.Log4j2;
7 9 import org.apache.commons.lang3.StringUtils;
... ... @@ -10,11 +12,12 @@ import org.apache.commons.net.ftp.FTPFile;
10 12 import org.apache.commons.net.ftp.FTPReply;
11 13 import org.springframework.stereotype.Component;
12 14  
  15 +import java.io.ByteArrayOutputStream;
13 16 import java.io.IOException;
14 17 import java.io.InputStream;
15 18 import java.net.SocketException;
16   -import java.util.LinkedList;
17   -import java.util.List;
  19 +import java.text.SimpleDateFormat;
  20 +import java.util.*;
18 21  
19 22 /**
20 23 * FTP服务工具类
... ... @@ -214,6 +217,10 @@ public class FtpUtils {
214 217 return is;
215 218 }
216 219  
  220 + public InputStream downloadFile(HistoryRecord historyRecord){
  221 + return getInputStreamOfFtpFile(historyRecord.getPath());
  222 + }
  223 +
217 224 /**
218 225 * 删除ftp文件
219 226 * @param ftpFilePath ftp文件路径,根目录开始
... ... @@ -233,5 +240,120 @@ public class FtpUtils {
233 240 return result;
234 241 }
235 242  
  243 + /**
  244 + * 验证指定目录下是否存在至少一个文件。
  245 + *
  246 + * @param directoryPath 目录路径
  247 + * @return 如果目录中存在至少一个文件,则返回文件名;否则返回null
  248 + */
  249 + public String checkDirectoryContainsFiles(String directoryPath){
  250 + FTPClient ftpClient = getFTPClient();
  251 + try {
  252 + // 改变工作目录
  253 + if (ftpClient.changeWorkingDirectory(directoryPath)) {
  254 + // 列出目录中的所有文件和子目录
  255 + FTPFile[] files = ftpClient.listFiles();
  256 + for (FTPFile file : files) {
  257 + if (file.isFile()) { // 检查是否为文件
  258 + return file.getName(); // 找到了至少一个文件
  259 + }
  260 + }
  261 + } else {
  262 + log.error("无法进入目录:{}", directoryPath);
  263 + return null;
  264 + }
  265 + } catch (IOException e) {
  266 + log.error("发生错误:{}", e.getMessage());
  267 + throw new FtpException(String.format("发生错误:{%s}", e.getMessage()));
  268 + } finally {
  269 + closeConnect(ftpClient);
  270 + }
  271 + return null; // 没有找到任何文件
  272 + }
  273 +
  274 + public boolean checkHistoryVideo(HistoryRecord record) {
  275 + String fileName = checkDirectoryContainsFiles(record.getPath());
  276 + boolean flag = fileName != null;
  277 + if (flag){
  278 + record.setPath( String.join(record.getPath(),"/",fileName));
  279 + record.setStatus(HistoryEnum.UPLOADED.getCode());
  280 + }
  281 + return flag;
  282 + }
  283 + /**
  284 + * 批量删除指定路径下的文件和文件夹。
  285 + */
  286 + public List<HistoryRecord> batchDelete(List<HistoryRecord> historyRecordList) throws IOException {
  287 + HashMap<String, List<HistoryRecord>> recordMap = new HashMap<>();
  288 + recordMap.put("true",new ArrayList<>(historyRecordList));
  289 + recordMap.put("false",new ArrayList<>(historyRecordList));
  290 + for (HistoryRecord historyRecord : historyRecordList) {
  291 + boolean b = deleteRecursive(historyRecord.getPath());
  292 + if (!b) {
  293 + historyRecord.setPath(null);
  294 + historyRecord.setStatus(HistoryEnum.NOT_UPLOADED.getCode());
  295 + historyRecord.setCreateTime(new Date());
  296 + recordMap.get("true").add(historyRecord);
  297 + }else {
  298 + recordMap.get("false").add(historyRecord);
  299 + }
  300 + }
  301 + log.debug("====================== FTP 删除过期文件 =====================");
  302 + String format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
  303 + log.debug("==> {} 成功删除文件 {} 个 ", format,recordMap.get("true").size());
  304 + log.debug("==> {} 未删除文件 {} 个", format,recordMap.get("false").size());
  305 + log.debug("===========================================================");
  306 + return recordMap.get("false");
  307 + }
  308 +
  309 + /**
  310 + * 递归删除指定路径下的文件和文件夹。
  311 + *
  312 + * @param path 文件或文件夹路径
  313 + */
  314 + private boolean deleteRecursive(String path) throws IOException {
  315 + FTPClient ftpClient = getFTPClient();
  316 + FTPFile[] files = ftpClient.listFiles(path);
  317 + if (files == null || files.length == 0) {
  318 + // 如果路径不存在或者为空,尝试删除它
  319 + if (!ftpClient.removeDirectory(path)) {
  320 + if (!ftpClient.deleteFile(path)) {
  321 + log.error("无法删除: {}", path);
  322 + return false;
  323 + } else {
  324 + log.info("已删除文件: {}", path);
  325 + return true;
  326 + }
  327 + } else {
  328 + log.info("已删除目录: {}", path);
  329 + return true;
  330 + }
  331 + }
  332 + // 删除文件夹中的所有文件和子文件夹
  333 + for (FTPFile file : files) {
  334 + String filePath = path.endsWith("/") ? path + file.getName() : path + "/" + file.getName();
  335 + if (file.isDirectory()) {
  336 + deleteRecursive(filePath); // 递归删除子文件夹
  337 + } else {
  338 + if (!ftpClient.deleteFile(filePath)) {
  339 + log.error("无法删除文件: {}", filePath);
  340 + return false;
  341 + } else {
  342 + log.info("已删除目录: {}", path);
  343 + return true;
  344 + }
  345 + }
  346 + }
  347 + // 最后尝试删除当前文件夹(如果它是空的)
  348 + if (!ftpClient.removeDirectory(path)) {
  349 + log.error("无法删除目录: {}", path);
  350 + return false;
  351 + } else {
  352 + log.info("已删除目录: {}", path);
  353 + return true;
  354 + }
  355 + }
  356 +
  357 +
236 358 }
237 359  
... ...
src/main/resources/mapper/HisToryRecordMapper.xml
... ... @@ -4,10 +4,142 @@
4 4 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
5 5 <mapper namespace="com.genersoft.iot.vmp.vmanager.jt1078.platform.mapper.HisToryRecordMapper">
6 6  
7   - <insert id="addRecode">
  7 + <resultMap id="HISTORY_RECORD_LIST_MAP" type="com.genersoft.iot.vmp.vmanager.jt1078.platform.domain.HistoryRecord">
  8 + <result property="id" column="id" />
  9 + <result property="name" column="name" />
  10 + <result property="sim" column="sim" />
  11 + <result property="channel" column="channel" />
  12 + <result property="startTime" column="start_time" />
  13 + <result property="endTime" column="end_time" />
  14 + <result property="status" column="status" />
  15 + <result property="ip" column="ip" />
  16 + <result property="port" column="port" />
  17 + <result property="username" column="username" />
  18 + <result property="password" column="password" />
  19 + <result property="path" column="path" />
  20 + <result property="createTime" column="create_time" />
  21 + <result property="updateTime" column="update_time" />
  22 + <result property="createBy" column="create_by" />
  23 + <result property="updateBy" column="update_by" />
  24 + <result property="remark" column="remark" />
  25 + </resultMap>
  26 +
  27 + <sql id="HISTORY_RECORD_LIST_SQL">
  28 + SELECT
  29 + id,
  30 + name,
  31 + sim,
  32 + channel,
  33 + start_time,
  34 + end_time,
  35 + status,
  36 + ip,
  37 + port,
  38 + username,
  39 + password,
  40 + path,
  41 + create_time,
  42 + update_time,
  43 + create_by,
  44 + update_by,
  45 + remark
  46 + FROM `wvp_history_record`
  47 + </sql>
  48 +
  49 + <insert id="addOrUpdRecode">
8 50 INSERT INTO `wvp_history_record`
9   - (`id`, `name`, `sim`, `channel`, `status`, `ip`, `port`, `username`, `password`, `create_time`, `update_time`, `create_by`, `update_by`, `remark`)
10   - VALUES
11   - (#{id}, #{name}, #{sim}, #{channel}, #{status}, #{ip}, #{port}, #{username}, #{password}, #{createTime}, #{updateTime}, #{createBy}, #{updateBy}, #{remark})
  51 + <trim prefix="(" suffix=")" suffixOverrides=",">
  52 + <if test=" id != null and id != '' ">`id` ,</if>
  53 + <if test=" sim != null and sim != '' ">`sim` ,</if>
  54 + <if test=" channel != null and channel != '' ">`channel` ,</if>
  55 + <if test=" startTime != null and startTime != '' ">`strat_time` ,</if>
  56 + <if test=" endTime != null and endTime != '' ">`end_time` ,</if>
  57 + <if test=" status != null and status != '' ">`status` ,</if>
  58 + <if test=" ip != null and ip != '' ">`ip` ,</if>
  59 + <if test=" port != null ">`port` ,</if>
  60 + <if test=" username != null and username != '' ">`username` ,</if>
  61 + <if test=" password != null and password != '' ">`password` ,</if>
  62 + <if test=" path != null and path != '' ">`path` ,</if>
  63 + <if test=" createTime != null ">`create_time` ,</if>
  64 + <if test=" updateTime != null ">`update_time` ,</if>
  65 + <if test=" createBy != null and createBy != '' ">`create_by` ,</if>
  66 + <if test=" updateBy != null and updateBy != '' ">`update_by` ,</if>
  67 + <if test=" remark != null and remark != '' ">`remark` ,</if>
  68 + </trim>
  69 + VALUES
  70 + <trim prefix="(" suffix=")" suffixOverrides=",">
  71 + <if test=" id != null and id != '' "> #{id}, </if>
  72 + <if test=" name != null and name != '' "> #{name}, </if>
  73 + <if test=" sim != null and sim != '' "> #{sim}, </if>
  74 + <if test=" channel != null and channel != '' "> #{channel}, </if>
  75 + <if test=" startTime != null and startTime != '' "> #{startTime}, </if>
  76 + <if test=" endTime != null and endTime != '' "> #{endTime}, </if>
  77 + <if test=" status != null and status != '' "> #{status}, </if>
  78 + <if test=" ip != null and ip != '' "> #{ip}, </if>
  79 + <if test=" port != null "> #{port}, </if>
  80 + <if test=" username != null and username != '' "> #{username}, </if>
  81 + <if test=" password != null and password != '' "> #{password}, </if>
  82 + <if test=" path != null and path != '' "> #{path}, </if>
  83 + <if test=" createTime != null "> #{createTime} , </if>
  84 + <if test=" updateTime != null "> #{updateTime}, </if>
  85 + <if test=" createBy != null and createBy != '' "> #{createBy}, </if>
  86 + <if test=" updateBy != null and updateBy != '' "> #{updateBy}, </if>
  87 + <if test=" remark != null and remark != '' "> #{remark}, </if>
  88 + </trim>
  89 + ON DUPLICATE KEY UPDATE
  90 + <trim suffixOverrides=",">
  91 + <if test=" id != null and id != '' "> `id` = #{id}, </if>
  92 + <if test=" name != null and name != '' "> `name` = #{name}, </if>
  93 + <if test=" sim != null and sim != '' "> `sim` = #{sim}, </if>
  94 + <if test=" channel != null and channel != '' "> `channel` = #{channel}, </if>
  95 + <if test=" startTime != null and startTime != '' "> `strat_time` = #{startTime}, </if>
  96 + <if test=" endTime != null and endTime != '' "> `end_time` = #{endTime}, </if>
  97 + <if test=" status != null and status != '' "> `status` = #{status}, </if>
  98 + <if test=" ip != null and ip != '' "> `ip` = #{ip}, </if>
  99 + <if test=" port != null "> `port` = #{port}, </if>
  100 + <if test=" username != null and username != '' "> `username` = #{username}, </if>
  101 + <if test=" password != null and password != '' "> `password` = #{password}, </if>
  102 + <if test=" path != null and path != '' "> `path` = #{path}, </if>
  103 + <if test=" createTime != null "> `create_time` = #{createTime} , </if>
  104 + <if test=" updateTime != null "> `update_time` = #{updateTime}, </if>
  105 + <if test=" createBy != null and createBy != '' "> `create_by` = #{createBy}, </if>
  106 + <if test=" updateBy != null and updateBy != '' "> `update_by` = #{updateBy}, </if>
  107 + <if test=" remark != null and remark != '' "> `remark` = #{remark}, </if>
  108 + </trim>
12 109 </insert>
  110 +
  111 + <update id="beachUpdRecode">
  112 + <foreach collection="list" item="item" separator=";">
  113 + UPDATE `wvp_history_record`
  114 + <set>
  115 + <if test="item.sim != null and item.sim != '' ">`sim` = #{item.sim}, </if>
  116 + <if test="item.channel != null and item.channel != '' ">`channel` = #{item.channel}, </if>
  117 + <if test="item.startTime != null and item.startTime != '' ">`strat_time` = #{item.startTime}, </if>
  118 + <if test="item.endTime != null and item.endTime != '' ">`end_time` = #{item.endTime}, </if>
  119 + <if test="item.status != null and item.status != '' ">`status` = #{item.status}, </if>
  120 + <if test="item.ip != null and item.ip != '' ">`ip` = #{item.ip}, </if>
  121 + <if test="item.port != null ">`port` = #{item.port}, </if>
  122 + <if test="item.username != null and item.username != '' ">`username` = #{item.username}, </if>
  123 + <if test="item.password != null and item.password != '' ">`password` = #{item.password}, </if>
  124 + <if test="item.path != null and item.path != '' ">`path` = #{item.path}, </if>
  125 + <if test="item.path == null ">`path` = NULL, </if>
  126 + <if test="item.createTime != null ">`create_time` = #{item.createTime} , </if>
  127 + <if test="item.updateTime != null ">`update_time` = #{item.updateTime}, </if>
  128 + <if test="item.createBy != null and item.createBy != '' ">`create_by` = #{item.createBy}, </if>
  129 + <if test="item.updateBy != null and item.updateBy != '' ">`update_by` = #{item.updateBy}, </if>
  130 + <if test="item.remark != null and item.remark != '' ">`remark` = #{item.remark}, </if>
  131 + </set>
  132 + WHERE
  133 + `name` = #{item.name}
  134 + </foreach>
  135 + </update>
  136 + <select id="getHistoryRecordList"
  137 + resultMap="HISTORY_RECORD_LIST_MAP">
  138 + <include refid="HISTORY_RECORD_LIST_SQL" />
  139 + </select>
  140 + <select id="getHistoryRecord"
  141 + resultMap="HISTORY_RECORD_LIST_MAP">
  142 + <include refid="HISTORY_RECORD_LIST_SQL" />
  143 + WHERE `name` = #{stream}
  144 + </select>
13 145 </mapper>
... ...
web_src/src/components/HistoricalRecord.vue
... ... @@ -45,8 +45,11 @@
45 45 </div>
46 46 </div>
47 47 </el-main>
48   - <el-footer style="width: 100%;height: 30%;">
49   - <historical-data :history-data="historyData" @click="clickHistoricalPlay" />
  48 + <el-footer style="width: 100%;height: 30%;background-color: white">
  49 + <history-search-table :table-data="historyData"
  50 + @playHistoryVideo="clickHistoricalPlay"
  51 + @downloadHistoryVideo="downloadFunction"
  52 + @uploadHistoryVideo="uploadHistoryVideo"/>
50 53 </el-footer>
51 54 </el-container>
52 55 </el-container>
... ... @@ -62,15 +65,18 @@ import {parseTime} from &quot;../../utils/ruoyi&quot;;
62 65 import HistoricalData from "./JT1078Components/historical/HistoricalDataTree.vue";
63 66 import HistoryList from "./JT1078Components/HistoryData.vue";
64 67 import Device1078Tree from "./JT1078Components/deviceList/Device1078Tree.vue";
  68 +import HistorySearchTable from "./JT1078Components/HistorySearchTable.vue";
65 69  
66 70 export default {
67 71 //import引入的组件需要注入到对象中才能使用"
68   - components: {HistoryList, Device1078Tree, HistoricalData, CarTree, player},
  72 + components: {HistorySearchTable, HistoryList, Device1078Tree, HistoricalData, CarTree, player},
69 73 props: {},
70 74 data() {
71 75 //这里存放数据"
72 76 return {
73   - historyData: [{name: "aaaa"},{name: "bbbb"}],
  77 + //历史视频列表定时器
  78 + historyTimer: null,
  79 + historyData: [],
74 80 targetValue: [],
75 81 //源列表数据
76 82 sourceValue: [],
... ... @@ -126,13 +132,13 @@ export default {
126 132 * 树点击事件
127 133 */
128 134 nodeClick(data, node) {
129   - if (data.children === undefined && data) {
130   - let split = data.id.split("_");
131   - if (split.length === 3){
132   - this.sim_channel = split[1] + '_' + split[2]
133   - } else {
134   - console.log("node click ==> ",data)
135   - }
  135 + if (data.children === undefined && data) {
  136 + let split = data.id.split("_");
  137 + if (split.length === 3) {
  138 + this.sim_channel = split[1] + '_' + split[2]
  139 + } else {
  140 + console.log("node click ==> ", data)
  141 + }
136 142 }
137 143 },
138 144 /**
... ... @@ -258,16 +264,50 @@ export default {
258 264 this.playHistoryItem(data)
259 265 },
260 266 /**
  267 + * 上传历史视频
  268 + */
  269 + uploadHistoryVideo(data){
  270 + this.$axios({
  271 + method: 'get',
  272 + url: '/api/jt1078/query/history/list/' + data.sim + '/' + data.channel + "/" + this.startTime + "/" + this.endTime
  273 + }).then(res => {
  274 + if (res.data.data.code === -1){
  275 + this.$message.success("视频开始上传,请等待")
  276 + this.searchHistoryList()
  277 + }
  278 + }).cache(err => {
  279 + console.log(err)
  280 + this.$message.error("视频上传失败,请联系管理员")
  281 + })
  282 + },
  283 + /**
261 284 * 下载历史视频
262 285 */
263   - downloadFunction() {
264   - this.getDateTime();
265   - console.log(this.downloadURL);
266   - if (this.isEmpty(this.downloadURL)) {
267   - return;
268   - }
269   - window.open(this.downloadURL, "_download");
  286 + downloadFunction(data) {
  287 + this.$axios({
  288 + url: `/api/jt1078/query/history/download/${data.name}`, // 请求URL
  289 + method: 'GET', // 将FTP文件路径作为参数传递
  290 + responseType: 'blob', // 告知axios以二进制数据流的形式处理响应
  291 + }).then((response) => {
  292 + const blob = new Blob([response.data], { type: 'application/octet-stream' });
  293 + const link = document.createElement('a');
  294 + link.href = window.URL.createObjectURL(blob);
  295 + link.download = ftpFilePath.substring(ftpFilePath.lastIndexOf('/') + 1); // 设置下载文件名
  296 + document.body.appendChild(link);
  297 + link.click();
  298 + document.body.removeChild(link);
  299 + }).catch(error => {
  300 + console.error("下载失败", error);
  301 + });
270 302 },
  303 + // downloadFunction(data) {
  304 + // this.getDateTime();
  305 + // console.log(this.downloadURL);
  306 + // if (this.isEmpty(this.downloadURL)) {
  307 + // return;
  308 + // }
  309 + // window.open(this.downloadURL, "_download");
  310 + // },
271 311 /**
272 312 * 搜索历史视频
273 313 */
... ... @@ -302,7 +342,6 @@ export default {
302 342 url: '/api/jt1078/query/history/list/' + sim + '/' + channel + "/" + this.startTime + "/" + this.endTime
303 343 }).then(
304 344 res => {
305   - console.log(res.data)
306 345 let items = res.data.data.obj.data.items;
307 346 if (res && res.data && res.data.data && res.data.data.obj && res.data.data.code == 1 && res.data.data.obj.data && items) {
308 347 for (let i in items) {
... ... @@ -311,16 +350,17 @@ export default {
311 350 items[i].name = items[i].startTime + '-' + items[i].endTime;
312 351 }
313 352 this.historyData = items;
314   - this.loading = false
  353 + clearInterval(this.historyTimer);
  354 + this.historyTimer = setInterval(() => {
  355 + this.searchHistoryList();
  356 + }, 10000); //
315 357 } else if (res && res.data && res.data.data && res.data.data.msg) {
316 358 this.$message.error(res.data.data.msg);
317   - this.loading = false
318 359 } else if (items === undefined) {
319 360 this.historyData = [];
320   - this.loading = false
321   - } else {
322   - this.loading = false
  361 + this.$message.warning("搜索历史列表为空")
323 362 }
  363 + this.loading = false
324 364 }).cache(res => {
325 365 this.$message.error(res.msg);
326 366 this.loading = false
... ... @@ -332,8 +372,8 @@ export default {
332 372 getDateTime() {
333 373 let date = this.date;
334 374 let timeList = this.timeList;
335   - console.log("date ",date)
336   - console.log("timeList ",timeList)
  375 + console.log("date ", date)
  376 + console.log("timeList ", timeList)
337 377 if (this.isEmpty(date)) {
338 378 this.$message.error("请选择日期")
339 379 return false;
... ... @@ -401,7 +441,6 @@ export default {
401 441 this.closeLoading();
402 442 })
403 443 },
404   -
405 444 /**
406 445 * 实时访问播放地址
407 446 * @param url
... ...
web_src/src/components/JT1078Components/HistorySearchTable.vue 0 → 100644
  1 +<template>
  2 + <el-table
  3 + ref="singleTable"
  4 + :data="tableData"
  5 + highlight-current-row
  6 + @current-change="handleCurrentChange"
  7 + height="250"
  8 + style="width: 100%">
  9 + <el-table-column
  10 + type="index"
  11 + fixed
  12 + width="100">
  13 + </el-table-column>
  14 + <el-table-column
  15 + property="name"
  16 + label="名称">
  17 + </el-table-column>
  18 + <el-table-column
  19 + property="date"
  20 + label="日期">
  21 + </el-table-column>
  22 + <el-table-column
  23 + property="status"
  24 + label="状态">
  25 + </el-table-column>
  26 + <el-table-column
  27 + fixed="right"
  28 + label="操作">
  29 + <template slot-scope="scope">
  30 + <el-button icon="el-icon-video-play" size="small" style="border: none" @click="playHistoryVideo(scope.row)"></el-button>
  31 + <el-button v-if="scope.row.status === '1'" icon="el-icon-upload" size="small" style="border: none" @click="uploadHistoryVideo(scope.row)"></el-button>
  32 + <el-button v-if="scope.row.status === '2'" icon="el-icon-download" size="small" style="border: none" @click="downloadHistoryVideo(scope.row)"></el-button>
  33 + </template>
  34 + </el-table-column>
  35 + </el-table>
  36 +</template>
  37 +
  38 +<script>
  39 +//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等),
  40 +//例如:import 《组件名称》 from '《组件路径》,
  41 + export default {
  42 + //import引入的组件需要注入到对象中才能使用"
  43 + components: {},
  44 + props: {
  45 + tableData: {
  46 + type: Array,
  47 + default: []
  48 + }
  49 + },
  50 + data() {
  51 + //这里存放数据"
  52 + return {
  53 + currentRow: null
  54 + };
  55 + },
  56 + //计算属性 类似于data概念",
  57 + computed: {},
  58 + //监控data中的数据变化",
  59 + watch: {},
  60 + //方法集合",
  61 + methods: {
  62 + /**
  63 + * 选中行
  64 + * @param val
  65 + */
  66 + handleCurrentChange(val) {
  67 + this.currentRow = val;
  68 + },
  69 + setCurrent(row) {
  70 + this.$refs.singleTable.setCurrentRow(row);
  71 + },
  72 + /**
  73 + * 播放历史视频
  74 + * @param row
  75 + */
  76 + playHistoryVideo(row){
  77 + this.$emit("playHistoryVideo", row);
  78 + },
  79 + /**
  80 + * 上传历史视频
  81 + * @param row
  82 + */
  83 + uploadHistoryVideo(row){
  84 + this.$emit('uploadHistoryVideo', row);
  85 + },
  86 + /**
  87 + * 下载历史视频
  88 + * @param row
  89 + */
  90 + downloadHistoryVideo(row){
  91 + this.$emit('downloadHistoryVideo', row);
  92 + }
  93 + },
  94 + //生命周期 - 创建完成(可以访问当前this实例)",
  95 + created() {
  96 + },
  97 + //生命周期 - 挂载完成(可以访问DOM元素)",
  98 + mounted() {
  99 + },
  100 + beforeCreate() {
  101 + }, //生命周期 - 创建之前",
  102 + beforeMount() {
  103 + }, //生命周期 - 挂载之前",
  104 + beforeUpdate() {
  105 + }, //生命周期 - 更新之前",
  106 + updated() {
  107 + }, //生命周期 - 更新之后",
  108 + beforeDestroy() {
  109 + }, //生命周期 - 销毁之前",
  110 + destroyed() {
  111 + }, //生命周期 - 销毁完成",
  112 + activated() {
  113 + } //如果页面有keep-alive缓存功能,这个函数会触发",
  114 + };
  115 +</script>
  116 +<style scoped>
  117 +
  118 +</style>
... ...