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.javacpp.BytePointer; import org.bytedeco.javacv.FFmpegFrameGrabber; import org.bytedeco.javacv.Frame; import org.bytedeco.javacv.Java2DFrameConverter; import org.bytedeco.opencv.global.opencv_imgproc; import org.bytedeco.opencv.opencv_core.Size; import org.opencv.core.Mat; import org.opencv.videoio.VideoCapture; 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.*; import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.util.Date; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @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()) { for (String videoPath : videoPathList) { ArdMedia ardMedia = ArdMedia.builder() .id(IdUtils.simpleUUID()) .taskId(flightTask.getTaskId()) .mediaType(1) // 1表示视频 .sourceUrl(videoPath) .fileSize(getFileSizeByUrl(videoPath)) .build(); CompletableFuture.supplyAsync(() -> { try { // 上传视频 String videoUrl = uploadFileToOss(flightTask.getBoxSn(), videoPath, "record", MimeTypeUtils.VIDEO_MP4); ardMedia.setMediaUrl(videoUrl); // 生成缩略图 InputStream ByteArrayInputStream thumbnailStream = generateThumbnailInputStream(videoPath, 2, 200); if (thumbnailStream != null) { String thumbnailUrl = ossServiceContext.upload("record", flightTask.getBoxSn() + "/" + IdUtils.fastSimpleUUID() + ".jpg", thumbnailStream, MimeTypeUtils.IMAGE_JPEG); ardMedia.setThumbnailUrl(thumbnailUrl); } ardMedia.setCreateTime(new Date()); return ardMedia; } catch (Exception e) { e.printStackTrace(); return null; } }, threadPoolTaskExecutor).thenAccept(am -> { if (am != null) { ardMediaMapper.insertArdMedia(am); } }); } } } 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("录像数量不一致,需要更新数据库"); // 需要插入新的视频 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()) { // 插入新的视频记录 for (String videoPath : newVideos) { ArdMedia ardMedia = ArdMedia.builder() .id(IdUtils.simpleUUID()) .taskId(flightTask.getTaskId()) .mediaType(1) // 1表示视频 .fileSize(getFileSizeByUrl(videoPath)) .sourceUrl(videoPath) .build(); CompletableFuture.supplyAsync(() -> { try { // 上传视频 String videoUrl = uploadFileToOss(flightTask.getBoxSn(), videoPath, "record", MimeTypeUtils.VIDEO_MP4); ardMedia.setMediaUrl(videoUrl); // 生成缩略图 InputStream ByteArrayInputStream thumbnailStream = generateThumbnailInputStream(videoPath, 2, 200); if (thumbnailStream != null) { String thumbnailUrl = ossServiceContext.upload("record", flightTask.getBoxSn() + "/" + IdUtils.fastSimpleUUID() + ".jpg", thumbnailStream, MimeTypeUtils.IMAGE_JPEG); ardMedia.setThumbnailUrl(thumbnailUrl); } ardMedia.setCreateTime(new Date()); return ardMedia; } catch (Exception e) { e.printStackTrace(); return null; } }, threadPoolTaskExecutor).thenAccept(am -> { if (am != null) { ardMediaMapper.insertArdMedia(am); } }); } // 更新任务的视频数量 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("图片数量不一致,需要更新数据库"); // 获取本地数据库中已存在的图片记录(类型为 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) { 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(() -> uploadFileToOss(flightTask.getBoxSn(), photo.getImgUrl(), "pic", MimeTypeUtils.IMAGE_JPG), threadPoolTaskExecutor) .thenAccept(url -> { ardMedia.setMediaUrl(url); // 获取上传后的文件 URL ardMedia.setCreateTime(new Date()); ardMediaMapper.insertArdMedia(ardMedia); // 插入数据库 }); } } // 通用的上传方法,支持视频和图片等文件上传 private String uploadFileToOss(String boxSn, String fileUrl, String bucket, String contentType) { InputStream fileInputStream = null; String fileOssUrl = ""; try { // 创建信任所有证书的SSLContext TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { @Override public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } @Override public void checkClientTrusted( java.security.cert.X509Certificate[] certs, String authType) { } @Override public void checkServerTrusted( java.security.cert.X509Certificate[] certs, String authType) { } } }; SSLContext sc = SSLContext.getInstance("SSL"); sc.init(null, trustAllCerts, new java.security.SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); // 创建所有主机名都信任的HostnameVerifier HostnameVerifier allHostsValid = (hostname, session) -> true; HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid); // 现在可以正常访问 // 从URL创建InputStream URL url = new URL(fileUrl); fileInputStream = url.openStream(); // 文件名 String fileName = boxSn + "/" + IdUtils.fastSimpleUUID() + "." + MimeTypeUtils.getExtension(contentType); // 调用OSS服务上下文进行文件上传 fileOssUrl = ossServiceContext.upload(bucket, fileName, fileInputStream, contentType); } catch (Exception e) { e.printStackTrace(); // 异常处理,可以选择记录日志或者抛出业务异常 } finally { // 关闭InputStream if (fileInputStream != null) { try { fileInputStream.close(); } catch (Exception e) { e.printStackTrace(); } } } return fileOssUrl; // 返回上传后的文件OSS URL } // 获取文件大小 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 generateThumbnailInputStream(String videoFile, int skipFrames, int targetWidth) throws Exception { 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()); } finally { if (grabber != null) { try { grabber.stop(); grabber.close(); } catch (Exception e) { e.printStackTrace(); } } } } }