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<ArdFlightTask> selectArdFlightTask(ArdFlightTask ardFlightTask) {
|
return ardFlightTaskMapper.selectArdFlightTask(ardFlightTask);
|
}
|
|
/**
|
* 同步所有服务的飞行历史
|
*/
|
@Scheduled(fixedDelay = 10000)
|
public void syncAllFlightHistory() {
|
List<String> 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<BoxHistoryReceiver> 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<BoxHistoryReceiver> getBoxHistory(String serverId, BoxHistoryDTO boxHistoryDTO) {
|
TyHttpCommonReceiver<List<BoxHistoryReceiver>> boxHistory = tyClient.getBoxHistory(serverId, boxHistoryDTO);
|
return boxHistory.getContent();
|
}
|
|
//调用接口获取架次图片
|
private List<BoxPhotosReceiver> getBoxPhotos(String serverId, BoxPhotosDTO boxPhotosDTO) {
|
TyHttpCommonReceiver<List<BoxPhotosReceiver>> boxPhotos = tyClient.getBoxPhotos(serverId, boxPhotosDTO);
|
return boxPhotos.getContent();
|
}
|
|
|
// 插入视频记录
|
private void insertVideoToDatabase(ArdFlightTask flightTask, List<String> 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<String> 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<String> 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<String> videoPathList) {
|
if (videoPathList == null) {
|
return;
|
}
|
Integer count = flightTask.getVideoCount();//获取本地库视频数量
|
int size = videoPathList.size();//获取接口返回的视频数量
|
if (count != null && size > count) {
|
log.info("任务【{}】录像数量不一致,需要更新数据库", flightTask.getTaskId());
|
// 需要插入新的视频
|
List<ArdMedia> existingVideos = ardMediaMapper.selectArdMediaByTaskIdAndType(flightTask.getTaskId(), 1);
|
// 提取现有的视频路径
|
List<String> existingVideoPaths = existingVideos.stream()
|
.map(ArdMedia::getSourceUrl) // 获取每个视频的 URL
|
.collect(Collectors.toList());
|
|
// 从接口返回的视频路径中找出新的视频
|
List<String> 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<String> 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<String> 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<BoxPhotosReceiver> 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<List<BoxPhotosReceiver>> boxPhotos = tyClient.getBoxPhotos(serverId, boxPhotosDTO);
|
List<BoxPhotosReceiver> photoList = boxPhotos.getContent();
|
int size = photoList.size();
|
|
if (size > count) {
|
log.info("任务【{}】图片数量不一致,需要更新数据库", flightTask.getTaskId());
|
// 获取本地数据库中已存在的图片记录(类型为 0 表示图片)
|
List<ArdMedia> existingPhotos = ardMediaMapper.selectArdMediaByTaskIdAndType(flightTask.getTaskId(), 0);
|
// 需要插入新的视频
|
// 提取现有的图片路径(根据 imgUrl 来判断是否重复)
|
List<String> existingPhotoUrls = existingPhotos.stream()
|
.map(ArdMedia::getSourceUrl) // 提取 ArdMedia 中的图片 URL
|
.collect(Collectors.toList());
|
|
// 筛选出 photoList 中没有出现在 existingPhotos 中的新图片
|
List<BoxPhotosReceiver> 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<BoxPhotosReceiver> 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;
|
}
|
}
|
|
}
|