package com.ard.work.service.impl; import com.ard.common.utils.DateUtils; import com.ard.common.utils.file.MimeTypeUtils; import com.ard.common.utils.uuid.IdUtils; import com.ard.oss.service.impl.OssServiceContext; import com.ard.work.component.forest.TyClient; import com.ard.work.domain.dto.BoxHistoryDTO; import com.ard.work.domain.dto.BoxPhotosDTO; import com.ard.work.domain.entity.ArdFlightTask; import com.ard.work.domain.entity.ArdMedia; import com.ard.work.domain.receiver.BoxHistoryReceiver; import com.ard.work.domain.receiver.BoxPhotosReceiver; import com.ard.work.domain.receiver.TyHttpCommonReceiver; import com.ard.work.mapper.ArdFlightTaskMapper; import com.ard.work.mapper.ArdMediaMapper; import com.ard.work.service.IArdFlightTaskService; import com.ard.work.service.IArdServerInfoService; import lombok.extern.slf4j.Slf4j; import org.bytedeco.javacv.FFmpegFrameGrabber; import org.bytedeco.javacv.Frame; import org.bytedeco.javacv.Java2DFrameConverter; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; import javax.annotation.Resource; import javax.imageio.ImageIO; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.security.cert.X509Certificate; import java.util.Date; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @Slf4j @Service public class ArdFlightTaskServiceServiceImpl implements IArdFlightTaskService { @Resource private ArdFlightTaskMapper ardFlightTaskMapper; @Resource private ArdMediaMapper ardMediaMapper; @Resource private IArdServerInfoService ardServerInfoService; @Resource private TyClient tyClient; @Resource private OssServiceContext ossServiceContext; @Resource private ThreadPoolTaskExecutor threadPoolTaskExecutor; // 使用线程池执行异步任务 /** * 查询飞行架次列表 * * @param ardFlightTask 飞行架次 * @return 飞行架次集合 */ @Override public List selectArdFlightTask(ArdFlightTask ardFlightTask) { return ardFlightTaskMapper.selectArdFlightTask(ardFlightTask); } /** * 同步所有服务的飞行历史 */ @Scheduled(fixedDelay = 10000) public void syncAllFlightHistory() { List serverIds = ardServerInfoService.getServerIdsList(); for (String serverId : serverIds) { // 获取上次同步的时间(即数据库中最大 startTime) Date lastSyncTime = getLastSyncTimeForServer(serverId); // 根据上次同步时间同步飞行历史 syncFlightHistory(serverId, lastSyncTime); } } // 获取上次同步时间(获取最大 startTime) private Date getLastSyncTimeForServer(String serverId) { Date maxStartTime = ardFlightTaskMapper.findMaxStartTimeByServerId(serverId); return maxStartTime != null ? maxStartTime : new Date(0); // 如果没有数据,返回 Unix 纪元时间作为起点 } // 根据上次同步时间同步飞行历史 public void syncFlightHistory(String serverId, Date startTime) { // 创建查询条件 BoxHistoryDTO,并设置 startTime 作为过滤条件 BoxHistoryDTO boxHistoryDTO = BoxHistoryDTO.builder() .startTime(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, startTime)) .build(); // 获取飞行历史 List historyList = getBoxHistory(serverId, boxHistoryDTO); // 如果获取到数据,处理并同步到本地数据库 if (historyList != null && !historyList.isEmpty()) { for (BoxHistoryReceiver boxHistory : historyList) { syncToLocalDatabase(serverId, boxHistory); } } } // 将数据同步到本地数据库 private void syncToLocalDatabase(String serverId, BoxHistoryReceiver boxHistoryReceiver) { // 检查是否已经存在相同任务ID的记录 ArdFlightTask ardFlightTask = ardFlightTaskMapper.getArdFlightTaskById(boxHistoryReceiver.getTaskId()); if (ardFlightTask == null) { // 如果不存在,则插入新记录 log.debug("insert task {}", boxHistoryReceiver.getTaskId()); // 创建FlightTask实体对象并填充数据 ArdFlightTask flightTask = ArdFlightTask.builder() .taskId(boxHistoryReceiver.getTaskId()) .boxSn(boxHistoryReceiver.getBoxSn()) .boxName(boxHistoryReceiver.getBoxName()) .taskName(boxHistoryReceiver.getTaskName()) .photoCount(Integer.parseInt(boxHistoryReceiver.getPhotoCount())) .videoCount((boxHistoryReceiver.getVideoPathList() != null) ? boxHistoryReceiver.getVideoPathList().size() : 0) .videoPath(boxHistoryReceiver.getVideoPath()) .serverId(serverId) .startTime(DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS, boxHistoryReceiver.getStartTime())) .endTime(DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS, boxHistoryReceiver.getEndTime())) .build(); flightTask.setCreateTime(DateUtils.getNowDate()); // 插入新记录 int rows = ardFlightTaskMapper.insertArdFlightTask(flightTask); if (rows > 0) { // 删除旧的媒体记录,防止重复插入 ardMediaMapper.deleteArdMediaByTaskId(flightTask.getTaskId()); // 插入视频 insertVideoToDatabase(flightTask, boxHistoryReceiver.getVideoPathList()); // 插入图片 insertPhotoToDatabase(flightTask, serverId, boxHistoryReceiver.getBoxSn()); } } else { // 如果存在,则更新记录 log.debug("Updating task {}", boxHistoryReceiver.getTaskId()); // 增量更新 updateVideoToDatabase(ardFlightTask, boxHistoryReceiver.getVideoPathList()); // 增量更新图片 updatePhotoToDatabase(ardFlightTask, serverId, boxHistoryReceiver.getBoxSn()); } } // 调用接口获取飞行历史数据 private List getBoxHistory(String serverId, BoxHistoryDTO boxHistoryDTO) { TyHttpCommonReceiver> boxHistory = tyClient.getBoxHistory(serverId, boxHistoryDTO); return boxHistory.getContent(); } //调用接口获取架次图片 private List getBoxPhotos(String serverId, BoxPhotosDTO boxPhotosDTO) { TyHttpCommonReceiver> boxPhotos = tyClient.getBoxPhotos(serverId, boxPhotosDTO); return boxPhotos.getContent(); } // 插入视频记录 private void insertVideoToDatabase(ArdFlightTask flightTask, List videoPathList) { if (videoPathList == null || videoPathList.isEmpty()) { return; } String boxSn = flightTask.getBoxSn(); for (String videoPath : videoPathList) { ArdMedia media = ArdMedia.builder() .id(IdUtils.simpleUUID()) .taskId(flightTask.getTaskId()) .mediaType(1) .sourceUrl(videoPath) .fileSize(getFileSizeByUrl(videoPath)) .build(); // ---- 1. 视频上传(失败不抛异常,返回 null) ---- CompletableFuture videoFuture = CompletableFuture.supplyAsync(() -> { try (InputStream inputStream = openUrlInputStream(videoPath, 5000, 10000)) { if (inputStream != null) { return ossServiceContext.upload( "record", boxSn + "/" + media.getId() + ".mp4", inputStream, MimeTypeUtils.VIDEO_MP4 ); } } catch (Exception e) { e.printStackTrace(); } return null; }, threadPoolTaskExecutor); // ---- 2. 缩略图上传(失败不抛异常,返回 null) ---- CompletableFuture thumbFuture = CompletableFuture.supplyAsync(() -> { try { try (ByteArrayInputStream thumbStream = generateThumbnail(videoPath, 2, 200)) { if (thumbStream != null) { return ossServiceContext.upload( "record", boxSn + "/" + media.getId() + ".jpg", thumbStream, MimeTypeUtils.IMAGE_JPEG ); } } } catch (Exception e) { e.printStackTrace(); } return null; }, threadPoolTaskExecutor); // 两个完成后写数据库(任意失败不阻断流程) videoFuture.thenCombine(thumbFuture, (vUrl, tUrl) -> { media.setMediaUrl(vUrl); media.setThumbnailUrl(tUrl); media.setCreateTime(new Date()); return media; }).thenAcceptAsync(ardMediaMapper::insertArdMedia, threadPoolTaskExecutor); } } private void updateVideoToDatabase(ArdFlightTask flightTask, List videoPathList) { if (videoPathList == null) { return; } Integer count = flightTask.getVideoCount();//获取本地库视频数量 int size = videoPathList.size();//获取接口返回的视频数量 if (count != null && size > count) { log.info("任务【{}】录像数量不一致,需要更新数据库", flightTask.getTaskId()); // 需要插入新的视频 List existingVideos = ardMediaMapper.selectArdMediaByTaskIdAndType(flightTask.getTaskId(), 1); // 提取现有的视频路径 List existingVideoPaths = existingVideos.stream() .map(ArdMedia::getSourceUrl) // 获取每个视频的 URL .collect(Collectors.toList()); // 从接口返回的视频路径中找出新的视频 List newVideos = videoPathList.stream() .filter(videoPath -> !existingVideoPaths.contains(videoPath)) // 过滤出新的视频 .collect(Collectors.toList()); if (!newVideos.isEmpty()) { String boxSn = flightTask.getBoxSn(); // 插入新的视频记录 for (String videoPath : newVideos) { ArdMedia ardMedia = ArdMedia.builder() .id(IdUtils.simpleUUID()) .taskId(flightTask.getTaskId()) .mediaType(1) // 1表示视频 .fileSize(getFileSizeByUrl(videoPath)) .sourceUrl(videoPath) .build(); // ---- 1. 视频上传(失败不抛异常,返回 null) ---- CompletableFuture videoFuture = CompletableFuture.supplyAsync(() -> { try (InputStream inputStream = openUrlInputStream(videoPath, 5000, 10000)) { if (inputStream != null) { return ossServiceContext.upload( "record", boxSn + "/" + ardMedia.getId() + ".mp4", inputStream, MimeTypeUtils.VIDEO_MP4 ); } } catch (Exception e) { e.printStackTrace(); } return null; }, threadPoolTaskExecutor); // ---- 2. 缩略图上传(失败不抛异常,返回 null) ---- CompletableFuture thumbFuture = CompletableFuture.supplyAsync(() -> { try { try (ByteArrayInputStream thumbStream = generateThumbnail(videoPath, 2, 200)) { if (thumbStream != null) { return ossServiceContext.upload( "record", boxSn + "/" + ardMedia.getId() + ".jpg", thumbStream, MimeTypeUtils.IMAGE_JPEG ); } } } catch (Exception e) { e.printStackTrace(); } return null; }, threadPoolTaskExecutor); // 两个完成后写数据库(任意失败不阻断流程) videoFuture.thenCombine(thumbFuture, (vUrl, tUrl) -> { ardMedia.setMediaUrl(vUrl); ardMedia.setThumbnailUrl(tUrl); ardMedia.setCreateTime(new Date()); return ardMedia; }).thenAcceptAsync(ardMediaMapper::insertArdMedia, threadPoolTaskExecutor); } // 更新任务的视频数量 flightTask.setVideoCount(flightTask.getVideoCount() + newVideos.size()); ardFlightTaskMapper.updateArdFlightTask(flightTask); } } } // 插入图片记录 private void insertPhotoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) { // 查询图片 BoxPhotosDTO boxPhotosDTO = new BoxPhotosDTO(); boxPhotosDTO.setTaskId(flightTask.getTaskId()); boxPhotosDTO.setBoxSn(boxSn); boxPhotosDTO.setPageIndex(1); boxPhotosDTO.setPageSize(500); List photoList = getBoxPhotos(serverId, boxPhotosDTO); if (photoList != null && !photoList.isEmpty()) { addPhotos(flightTask, photoList); } } private void updatePhotoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) { int count = flightTask.getPhotoCount(); //获取本地库图片数量 // 查询图片 BoxPhotosDTO boxPhotosDTO = BoxPhotosDTO.builder() .taskId(flightTask.getTaskId()) .boxSn(boxSn) .pageIndex(1) .pageSize(500) .build(); TyHttpCommonReceiver> boxPhotos = tyClient.getBoxPhotos(serverId, boxPhotosDTO); List photoList = boxPhotos.getContent(); int size = photoList.size(); if (size > count) { log.info("任务【{}】图片数量不一致,需要更新数据库", flightTask.getTaskId()); // 获取本地数据库中已存在的图片记录(类型为 0 表示图片) List existingPhotos = ardMediaMapper.selectArdMediaByTaskIdAndType(flightTask.getTaskId(), 0); // 需要插入新的视频 // 提取现有的图片路径(根据 imgUrl 来判断是否重复) List existingPhotoUrls = existingPhotos.stream() .map(ArdMedia::getSourceUrl) // 提取 ArdMedia 中的图片 URL .collect(Collectors.toList()); // 筛选出 photoList 中没有出现在 existingPhotos 中的新图片 List newPhotos = photoList.stream() .filter(photo -> !existingPhotoUrls.contains(photo.getImgUrl())) // 过滤出新图片 .collect(Collectors.toList()); if (!newPhotos.isEmpty()) { // 插入新的视频记录 addPhotos(flightTask, newPhotos); // 更新任务的视频数量 flightTask.setPhotoCount(flightTask.getPhotoCount() + newPhotos.size()); ardFlightTaskMapper.updateArdFlightTask(flightTask); } } } private void addPhotos(ArdFlightTask flightTask, List photoList) { String boxSn = flightTask.getBoxSn(); for (BoxPhotosReceiver photo : photoList) { ArdMedia ardMedia = ArdMedia.builder() .id(IdUtils.simpleUUID()) .taskId(flightTask.getTaskId()) .mediaType(0) // 0表示图片 .fileSize(photo.getFileSize()) .fileMd5(photo.getFileMd5()) .sourceUrl(photo.getImgUrl()) .photoId(photo.getPhotoId()) .longitude(photo.getLng()) .latitude(photo.getLat()) .height(photo.getHeight()) .captureTime(DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS, photo.getTimeStamp())) .build(); // 异步上传文件到OSS,并在上传完成后插入数据库 CompletableFuture.supplyAsync(() -> { try (InputStream inputStream = openUrlInputStream(photo.getImgUrl(), 5000, 10000)) { if (inputStream != null) { return ossServiceContext.upload( "pic", boxSn + "/" + ardMedia.getId() + ".jpg", inputStream, MimeTypeUtils.IMAGE_JPG ); } } catch (Exception e) { e.printStackTrace(); } return null; }, threadPoolTaskExecutor) .thenAccept(url -> { ardMedia.setMediaUrl(url); ardMedia.setCreateTime(new Date()); ardMediaMapper.insertArdMedia(ardMedia); }); } } // 获取文件大小 private long getFileSizeByUrl(String fileUrl) { try { URL url = new URL(fileUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("HEAD"); conn.connect(); return conn.getContentLengthLong(); // 返回字节大小 } catch (Exception e) { return -1; } } // 生成视频缩略图 public ByteArrayInputStream generateThumbnail(String videoFile, int skipFrames, int targetWidth) { long start = System.currentTimeMillis(); FFmpegFrameGrabber grabber = null; try { grabber = new FFmpegFrameGrabber(videoFile); grabber.start(); int totalFrames = grabber.getLengthInFrames(); System.out.println("视频总帧数: " + totalFrames); System.out.println("视频时长: " + grabber.getLengthInTime() / 1000000 + "秒"); Frame frame = null; int currentFrame = 0; // 读取指定帧数后的第一帧有效图像 while (currentFrame < totalFrames) { frame = grabber.grabImage(); if (frame == null) { break; } if (currentFrame > skipFrames && frame.image != null) { break; } currentFrame++; } if (frame == null || frame.image == null) { throw new Exception("无法获取有效视频帧"); } // 转换 Frame 为 BufferedImage Java2DFrameConverter converter = new Java2DFrameConverter(); BufferedImage image = converter.getBufferedImage(frame); // 按目标宽度等比缩放 int targetHeight = (int) ((double) targetWidth / image.getWidth() * image.getHeight()); BufferedImage scaledImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_3BYTE_BGR); scaledImage.getGraphics().drawImage(image, 0, 0, targetWidth, targetHeight, null); // 转成 ByteArrayInputStream ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(scaledImage, "jpg", baos); long cost = System.currentTimeMillis() - start; System.out.println("处理耗时: " + cost + "ms"); return new ByteArrayInputStream(baos.toByteArray()); } catch (Exception e) { throw new RuntimeException(e); } finally { if (grabber != null) { try { grabber.stop(); grabber.close(); } catch (Exception e) { e.printStackTrace(); } } } } /** * 将 URL 转成 InputStream * * @param fileUrl 文件 URL * @param connectTimeoutMillis 连接超时(毫秒) * @param readTimeoutMillis 读取超时(毫秒) * @return InputStream,如果失败返回 null */ public InputStream openUrlInputStream(String fileUrl, int connectTimeoutMillis, int readTimeoutMillis) { try { URL url = new URL(fileUrl); // 支持 HTTPS 信任所有证书(如果需要访问自签名证书) if ("https".equalsIgnoreCase(url.getProtocol())) { TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) { } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) { } @Override public X509Certificate[] getAcceptedIssuers() { return null; } } }; SSLContext sc = SSLContext.getInstance("SSL"); sc.init(null, trustAllCerts, new java.security.SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true); } URLConnection connection = url.openConnection(); connection.setConnectTimeout(connectTimeoutMillis); connection.setReadTimeout(readTimeoutMillis); return connection.getInputStream(); } catch (Exception e) { e.printStackTrace(); return null; } } }