ard-admin/src/main/java/com/ard/web/controller/data/ArdMapController.java
@@ -3,6 +3,7 @@ import java.util.List; import javax.servlet.http.HttpServletResponse; import com.ard.common.constant.DeviceConstants; import com.ard.data.map.domain.ArdMap; import com.ard.data.map.service.IArdMapService; import io.swagger.annotations.Api; @@ -81,6 +82,9 @@ @Log(title = "地图管理", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@RequestBody ArdMap ardMap) { if (DeviceConstants.NOT_UNIQUE.equals(ardMapService.checkArdMapNameUnique(ardMap))) { return error("新增地图'" + ardMap.getMapName() + "'失败,已存在"); } return toAjax(ardMapService.insertArdMap(ardMap)); } @@ -92,6 +96,9 @@ @Log(title = "地图管理", businessType = BusinessType.UPDATE) @PutMapping public AjaxResult edit(@RequestBody ArdMap ardMap) { if (DeviceConstants.NOT_UNIQUE.equals(ardMapService.checkArdMapNameUnique(ardMap))) { return error("修改地图'" + ardMap.getMapName() + "'失败,已存在"); } return toAjax(ardMapService.updateArdMap(ardMap)); } ard-admin/src/main/java/com/ard/web/controller/data/ArdModelController.java
@@ -3,6 +3,7 @@ import java.util.List; import javax.servlet.http.HttpServletResponse; import com.ard.common.constant.DeviceConstants; import com.ard.data.model.domain.ArdModel; import com.ard.data.model.service.IArdModelService; import io.swagger.annotations.Api; @@ -81,6 +82,9 @@ @Log(title = "模型管理", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@RequestBody ArdModel ardModel) { if (DeviceConstants.NOT_UNIQUE.equals(ardModelService.checkArdModelNameUnique(ardModel))) { return error("新增模型'" + ardModel.getModelName() + "'失败,已存在"); } return toAjax(ardModelService.insertArdModel(ardModel)); } @@ -92,6 +96,9 @@ @Log(title = "模型管理", businessType = BusinessType.UPDATE) @PutMapping public AjaxResult edit(@RequestBody ArdModel ardModel) { if (DeviceConstants.NOT_UNIQUE.equals(ardModelService.checkArdModelNameUnique(ardModel))) { return error("修改模型'" + ardModel.getModelName() + "'失败,已存在"); } return toAjax(ardModelService.updateArdModel(ardModel)); } ard-admin/src/main/java/com/ard/web/controller/data/ArdNoFlyZoneController.java
@@ -1,6 +1,7 @@ package com.ard.web.controller.data; import com.alibaba.fastjson2.JSON; import com.ard.common.constant.DeviceConstants; import com.ard.common.utils.uuid.IdUtils; import com.ard.data.noflyzone.domain.FeatureCollection; import io.swagger.annotations.Api; @@ -82,6 +83,9 @@ @PostMapping public AjaxResult add(@RequestBody ArdNoFlyZone ardNoFlyZone) { if (DeviceConstants.NOT_UNIQUE.equals(ardNoFlyZoneService.checkArdNoFlyZoneNameUnique(ardNoFlyZone))) { return error("新增禁飞区'" + ardNoFlyZone.getName() + "'失败,已存在"); } return toAjax(ardNoFlyZoneService.insertArdNoFlyZone(ardNoFlyZone)); } @@ -94,6 +98,9 @@ @PutMapping public AjaxResult edit(@RequestBody ArdNoFlyZone ardNoFlyZone) { if (DeviceConstants.NOT_UNIQUE.equals(ardNoFlyZoneService.checkArdNoFlyZoneNameUnique(ardNoFlyZone))) { return error("修改禁飞区'" + ardNoFlyZone.getName() + "'失败,已存在"); } return toAjax(ardNoFlyZoneService.updateArdNoFlyZone(ardNoFlyZone)); } ard-admin/src/main/java/com/ard/web/controller/data/ArdTileController.java
@@ -3,6 +3,7 @@ import java.util.List; import javax.servlet.http.HttpServletResponse; import com.ard.common.constant.DeviceConstants; import com.ard.data.tile.domain.ArdTile; import com.ard.data.tile.service.IArdTileService; import io.swagger.annotations.Api; @@ -81,6 +82,9 @@ @Log(title = "tile管理", businessType = BusinessType.INSERT) @PostMapping public AjaxResult add(@RequestBody ArdTile ardTile) { if (DeviceConstants.NOT_UNIQUE.equals(ardTileService.checkArdTileNameUnique(ardTile))) { return error("新增tile'" + ardTile.getTilesName() + "'失败,已存在"); } return toAjax(ardTileService.insertArdTile(ardTile)); } @@ -92,6 +96,9 @@ @Log(title = "tile管理", businessType = BusinessType.UPDATE) @PutMapping public AjaxResult edit(@RequestBody ArdTile ardTile) { if (DeviceConstants.NOT_UNIQUE.equals(ardTileService.checkArdTileNameUnique(ardTile))) { return error("修改tile'" + ardTile.getTilesName() + "'失败,已存在"); } return toAjax(ardTileService.updateArdTile(ardTile)); } ard-admin/src/main/java/com/ard/web/controller/work/ArdDroneInfoController.java
@@ -112,10 +112,12 @@ @ApiOperation("批量新增无人机盒子") @PreAuthorize("@ss.hasPermi('work:drone:insertArdDroneLs')") @Log(title = "无人机管理", businessType = BusinessType.DELETE) @PostMapping("insertArdDroneLs/{boxSn}/{deptId}") public AjaxResult insertArdDroneLs(@PathVariable String[] boxSn, @PathVariable Long deptId) { @PostMapping("insertArdDroneLs/{boxSn}/{deptId}/{serverId}") public AjaxResult insertArdDroneLs(@PathVariable String[] boxSn, @PathVariable Long deptId, @PathVariable String serverId) { try { int result = ardDroneInfoService.insertArdDroneLs(boxSn, deptId); int result = ardDroneInfoService.insertArdDroneLs(boxSn,deptId,serverId); if (result > 0) { return AjaxResult.success("成功新增 " + result + " 个无人机盒子"); } else { ard-admin/src/main/java/com/ard/web/controller/work/ArdFlightTaskController.java
New file @@ -0,0 +1,78 @@ package com.ard.web.controller.work; import com.ard.common.core.controller.BaseController; import com.ard.common.core.page.TableDataInfo; import com.ard.work.domain.dto.*; import com.ard.work.domain.entity.ArdFlightTask; import com.ard.work.domain.entity.ArdFlightTaskPart; import com.ard.work.domain.entity.ArdMediaPic; import com.ard.work.domain.entity.ArdMediaVideo; import com.ard.work.service.IArdFlightTaskPartService; import com.ard.work.service.IArdFlightTaskService; import com.ard.work.service.IArdMediaService; import com.ard.work.service.ITyService; import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; import com.github.xiaoymin.knife4j.annotations.ApiSort; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; /** * 飞行架次管理接口 * ArdFlightController * * @author liusuyi * @date 2024年10月21日13:52 **/ @Api(tags = "飞行架次管理接口") @ApiSort(value = 2) @RequestMapping("/uav/base") @RestController public class ArdFlightTaskController extends BaseController { @Resource private IArdFlightTaskService ardFlightTaskService; @Resource private IArdFlightTaskPartService ardFlightTaskPartService; @Resource private IArdMediaService ardMediaService; @ApiOperation("获取飞行架次") @ApiOperationSupport(order = 1) @GetMapping("/getFlightTask") public TableDataInfo getFlightTask(ArdFlightTask ardFlightTask) { startPage(); List<ArdFlightTask> list = ardFlightTaskService.selectArdFlightTask(ardFlightTask); return getDataTable(list); } @ApiOperation("获取架次飞行轨迹") @ApiOperationSupport(order = 2) @GetMapping("/getCompleteFlightData") public TableDataInfo getCompleteFlightData(ArdFlightTaskPart ardFlightTaskPart) { List<UavFlightStateDTO> completeFlightData = ardFlightTaskPartService.getCompleteFlightData(ardFlightTaskPart); return getDataTable(completeFlightData); } @ApiOperation("获取架次图片数据") @ApiOperationSupport(order = 3) @GetMapping("/getMediaPicList") public TableDataInfo getMediaPicList(ArdMediaPic ardMedia) { startPage(); List<ArdMediaPic> list = ardMediaService.selectArdMediaPicList(ardMedia); return getDataTable(list); } @ApiOperation("获取架次录像数据") @ApiOperationSupport(order = 4) @GetMapping("/getMediaVideoList") public TableDataInfo getMediaVideoList(ArdMediaVideo ardMedia) { startPage(); List<ArdMediaVideo> list = ardMediaService.selectArdMediaVideoList(ardMedia); return getDataTable(list); } } ard-admin/src/main/java/com/ard/web/controller/work/ArdServerInfoController.java
@@ -51,18 +51,6 @@ } /** * 查询服务器管理列表 */ @ApiOperation("查询服务器管理列表") @PreAuthorize("@ss.hasPermi('work:server:listNo')") @GetMapping("/listNo") public TableDataInfo listNo(ArdServerInfo ardServerInfo) { startPage(); List<ArdServerInfo> list = ardServerInfoService.selectArdServerInfoListNo(ardServerInfo); return getDataTable(list); } /** * 导出服务器管理列表 */ @ApiOperation("导出服务器管理列表") ard-admin/src/main/java/com/ard/web/controller/work/UavBaseController.java
@@ -5,9 +5,7 @@ import com.ard.common.core.page.TableDataInfo; import com.ard.work.domain.dto.*; import com.ard.work.domain.dto.telemetry.TelemetryData; import com.ard.work.domain.entity.ArdFlightTask; import com.ard.work.domain.entity.ArdFlightTaskPart; import com.ard.work.domain.entity.ArdMedia; import com.ard.work.domain.entity.*; import com.ard.work.domain.receiver.*; import com.ard.work.service.IArdFlightTaskPartService; import com.ard.work.service.IArdFlightTaskService; @@ -70,32 +68,6 @@ public AjaxResult getBoxHistory(@RequestBody BoxHistoryDTO boxHistoryDTO) { List<BoxHistoryReceiver> boxHistory = tyService.getBoxHistory(boxHistoryDTO); return AjaxResult.success(boxHistory); } @ApiOperation("获取飞行架次") @ApiOperationSupport(order = 4) @GetMapping("/getFlightTask") public TableDataInfo getFlightTask(ArdFlightTask ardFlightTask) { startPage(); List<ArdFlightTask> list = ardFlightTaskService.selectArdFlightTask(ardFlightTask); return getDataTable(list); } @ApiOperation("获取架次飞行轨迹") @ApiOperationSupport(order = 4) @GetMapping("/getCompleteFlightData") public TableDataInfo getCompleteFlightData(ArdFlightTaskPart ardFlightTaskPart) { List<UavFlightStateDTO> completeFlightData = ardFlightTaskPartService.getCompleteFlightData(ardFlightTaskPart); return getDataTable(completeFlightData); } @ApiOperation("获取媒体数据") @ApiOperationSupport(order = 4) @GetMapping("/getMediaList") public TableDataInfo getMediaList(ArdMedia ardMedia) { startPage(); List<ArdMedia> list = ardMediaService.selectArdMediaList(ardMedia); return getDataTable(list); } @ApiOperation("获取任务照片") ard-device/src/main/java/com/ard/device/camera/domain/ArdCamera.java
@@ -4,6 +4,7 @@ import com.ard.common.core.domain.BaseEntity; import com.ard.device.channel.domain.ArdChannel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @@ -17,6 +18,7 @@ @Data @AllArgsConstructor @NoArgsConstructor @Builder public class ArdCamera extends BaseEntity { private static final long serialVersionUID = 1L; ard-device/src/main/java/com/ard/device/camera/service/impl/ArdCameraServiceImpl.java
@@ -163,7 +163,7 @@ ardCamera.setUpdateBy(SecurityUtils.getUsername()); int res = ardCameraMapper.updateArdCamera(ardCamera); if (res > 0) { cameraSDKService.logout(ardCamera.getId());//sdk注销 cameraSDKService.logout(ardCamera);//sdk注销 cameraSDKService.login(ardCamera);//sdk登录获取通道信息 } return res; @@ -186,7 +186,7 @@ //删除当前相机的所有通道 ardChannelService.deleteArdChannelByDeviceId(id); //sdk注销 cameraSDKService.logout(id); cameraSDKService.logout(camera); } } return ardCameraMapper.deleteArdCameraByIds(ids); @@ -207,7 +207,7 @@ //删除当前相机的所有通道 ardChannelService.deleteArdChannelByDeviceId(id); //sdk注销 cameraSDKService.logout(id); cameraSDKService.logout(ArdCamera.builder().id(id).build()); } return i; } @@ -373,84 +373,4 @@ redisService.setCacheObject(getCacheKey(camera.getId()), camera); } /** * 同步设备通道与本地通道库信息 * * @author 刘苏义 * @date 2024/8/22 14:37 */ @Scheduled(cron = "0/10 * * * * ?") public void syncChannel() { try { log.debug("定时执行同步通道任务"); List<ArdCamera> ardCameraList = this.selectArdCameraList(new ArdCamera()); if (!ardCameraList.isEmpty()) { ardCameraList .forEach(ardCamera -> { //SDK获取实际通道 List<ArdChannel> ardChannelListSDK = cameraSDKService.getChannels(ardCamera); //数据库获取通道 ArdChannel ardChannel = new ArdChannel(); ardChannel.setDeviceId(ardCamera.getId()); List<ArdChannel> ardChannelListDb = ardChannelService.selectArdChannelList(ardChannel); //同步通道 asyncChannel(ardChannelListDb, ardChannelListSDK); }); } } catch (Exception ex) { log.error("定时执行同步通道任务异常:{}", ex.getMessage()); } } /** * 定时执行登录失败相机重新登录 * * @author 刘苏义 * @date 2024/8/22 14:37 */ @Scheduled(cron = "0/30 * * * * ?") public void failedReconnect() { try { log.debug("定时执行失败相机重连任务"); List<Object> ardCameras = redisService.getListKey(CacheConstants.CAMERA_LIST_KEY); ardCameras.stream() .map(object -> (ArdCamera) object) // 将Object转换为ArdCameras .filter(camera -> !GlobalVariable.loginMap.containsKey(camera.getId())) .forEach(camera -> cameraSDKService.login(camera)); } catch (Exception ex) { log.error("定时执行失败相机重连任务异常:{}", ex.getMessage()); } } /** * 同步相机在线状态 * 刘苏义 * 2023/10/8 14:54:41 */ @Scheduled(cron = "0/10 * * * * ?") public void cameraState() { try { log.debug("定时执行同步相机在线状态任务"); //调用查询方法 List<ArdCamera> ardCameraList = selectArdCameraList(new ArdCamera()); ardCameraList .forEach(camera -> { CameraCmd cmd = new CameraCmd(); cmd.setCameraId(camera.getId()); boolean onLineNew = cameraSDKService.isOnLine(cmd); boolean onLineOld = "1".equals(camera.getState()); if (onLineNew != onLineOld) { if (onLineNew) { camera.setState("1"); } else { camera.setState("0"); } camera.setUpdateTime(DateUtils.getNowDate()); ardCameraMapper.updateArdCamera(camera); } }); } catch (Exception ex) { log.error("同步相机在线状态异常:{}", ex.getMessage()); } } } ard-device/src/main/java/com/ard/device/channel/service/impl/ArdChannelServiceImpl.java
@@ -1,6 +1,8 @@ package com.ard.device.channel.service.impl; import com.ard.common.constant.CacheConstants; import com.ard.common.constant.UserConstants; import com.ard.common.core.redis.RedisCache; import com.ard.common.utils.uuid.IdUtils; import com.ard.device.camera.domain.ArdCamera; import com.ard.device.camera.service.IArdCameraService; @@ -11,6 +13,7 @@ import com.ard.vtdu.domain.ArdVtdu; import com.ard.vtdu.service.IArdVtduService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -27,14 +30,9 @@ @Slf4j @Service public class ArdChannelServiceImpl implements IArdChannelService { @Resource private IArdCameraService ardCameraService; @Resource private ArdChannelMapper ardChannelMapper; @Resource private IArdVtduService ardVtduService; @Resource private MediamtxConfig mediamtxConfig; /** * 查询通道管理 @@ -138,102 +136,5 @@ return UserConstants.NOT_UNIQUE; } return UserConstants.UNIQUE; } @Scheduled(cron = "0/5 * * * * ?") public void syncVtdu() { log.debug("定时同步Vtdu任务"); List<ArdChannel> channels = this.selectArdChannelList(new ArdChannel()); if (channels == null || channels.isEmpty()) { // log.warn("没有获取到通道数据,退出同步"); return; } List<ArdVtdu> ardVtduList = ardVtduService.selectArdVtduList(new ArdVtdu()); Set<String> validNames = channels.stream() .map(channel -> channel.getDeviceId() + "_" + channel.getChanNo()) .collect(Collectors.toSet()); // 2. 合并操作:增、删、改 channels.forEach(channel -> { String name = channel.getDeviceId() + "_" + channel.getChanNo(); // 生成媒体表的 name String cameraId = channel.getDeviceId(); // cameraId 就是通道的 deviceId String rtspSourceUrl = getRtspSource(channel); String rtspUrl = "rtsp://" + mediamtxConfig.getPlayHost() + ":" + mediamtxConfig.getRtspPort() + "/" + name; // // 拼接 webrtcUrl String rtmpUrl = "rtmp://" + mediamtxConfig.getPlayHost() + ":" + mediamtxConfig.getRtmpPort() + "/" + name; // 拼接 // webrtcUrl String protocol = mediamtxConfig.isHttps() ? "https://" : "http://"; String webrtcUrl = protocol + mediamtxConfig.getPlayHost() + ":" + mediamtxConfig.getWebrtcPort() + "/" + name; // 拼接 webrtcUrl // 先检查媒体表是否已有此记录 Optional<ArdVtdu> existingMedia = ardVtduList.stream() .filter(media -> media.getName().equals(name)) // 查找是否已有对应的媒体记录 .findFirst(); if (existingMedia.isPresent()) { // 如果已有,更新现有记录 ArdVtdu vtdu = existingMedia.get(); // 更新前后内容相同则不进行更新 if (!vtdu.getRtspSource().equals(rtspSourceUrl) || !vtdu.getWebrtcUrl().equals(webrtcUrl) || !vtdu.getRtspUrl().equals(rtspUrl) || !vtdu.getRtmpUrl().equals(rtmpUrl)) { vtdu.setRtspUrl(rtspUrl); vtdu.setRtmpUrl(rtmpUrl); vtdu.setWebrtcUrl(webrtcUrl); ardVtduService.updateArdVtdu(vtdu); log.info("更新媒体记录:{}", vtdu.getName()); } } else { // 如果没有,插入新的记录 ArdVtdu vtdu = new ArdVtdu(); vtdu.setName(name); vtdu.setRtspSource(rtspSourceUrl); vtdu.setCameraId(cameraId); vtdu.setRtspUrl(rtspUrl); vtdu.setRtmpUrl(rtmpUrl); vtdu.setWebrtcUrl(webrtcUrl); ardVtduService.insertArdVtdu(vtdu); // 保存到媒体表 log.info("插入新媒体记录:{}", vtdu.getName()); } }); // 3. 删除操作:删除媒体表中多余的记录 List<String> invalidNames = ardVtduList.stream() .filter(vtdu -> !validNames.contains(vtdu.getName())) .filter(vtdu -> !"publisher".equalsIgnoreCase(vtdu.getRtspSource())) // 不是机场流 .map(ArdVtdu::getName) .collect(Collectors.toList()); if (!invalidNames.isEmpty()) { // 将 List<String> 转换为 String[] String[] namesArray = invalidNames.toArray(new String[0]); ardVtduService.deleteArdVtduByNames(namesArray); // 传递数组 log.info("删除不再存在的媒体记录:{}", Arrays.toString(namesArray)); } } // 根据通道信息获取rtsp源地址 private String getRtspSource(ArdChannel channel) { String rtspSource = ""; ArdCamera ardCamera = ardCameraService.selectArdCameraById(channel.getDeviceId()); if (ardCamera != null) { switch (ardCamera.getFactory()) { case "1": rtspSource = "rtsp://" + ardCamera.getUsername() + ":" + ardCamera.getPassword() + "@" + ardCamera.getIp() + ":" + ardCamera.getRtspPort() + "/h264/ch" + channel.getChanNo() + "/main/av_stream"; break; case "2": rtspSource = "rtsp://" + ardCamera.getUsername() + ":" + ardCamera.getPassword() + "@" + ardCamera.getIp() + ":" + ardCamera.getRtspPort() + "/cam/realmonitor?channel=" + channel.getChanNo() + "&subtype=0"; break; } } return rtspSource; } } ard-device/src/main/java/com/ard/device/component/sdk/common/GlobalVariable.java
@@ -16,8 +16,7 @@ **/ @Data public class GlobalVariable { //保存相机登录信息(cameraId:loginId) public static Map<String, Object> loginMap = new ConcurrentHashMap<>(); //保存相机登录信息(loginId:Camera) public static Map<Integer, ArdCamera> hikLoginMap = new ConcurrentHashMap<>(); } ard-device/src/main/java/com/ard/device/component/sdk/dh/service/impl/DaHuaSDK.java
@@ -17,6 +17,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.io.*; @@ -102,7 +103,8 @@ public void login(ArdCamera camera) { try { NetSDKLib.NET_DEVICEINFO_Ex m_stDeviceInfo = new NetSDKLib.NET_DEVICEINFO_Ex(); NetSDKLib.LLong loginId = LoginModule.login(camera.getIp(), camera.getPort(), camera.getUsername(), camera.getPassword(), m_stDeviceInfo); NetSDKLib.LLong loginId = LoginModule.login(camera.getIp(), camera.getPort(), camera.getUsername(), camera.getPassword(), m_stDeviceInfo); if (loginId.longValue() <= 0) { camera.setChanNum(0); camera.setLoginId(-1L); @@ -116,7 +118,7 @@ camera.setChanNum(m_stDeviceInfo.byChanNum); camera.setStartChan(1); camera.setLoginId(loginId.longValue()); GlobalVariable.loginMap.put(camera.getId(), loginId); //获取最新通道 List<ArdChannel> ardChannelList = getChannels(camera); if (!ardChannelList.isEmpty()) { @@ -135,11 +137,8 @@ * 2023/10/17 8:28:13 */ @Override public void logout(String cameraId) { if (!GlobalVariable.loginMap.containsKey(cameraId)) { return; } NetSDKLib.LLong loginId = (NetSDKLib.LLong) GlobalVariable.loginMap.get(cameraId); public void logout(ArdCamera ardCamera) { NetSDKLib.LLong loginId = new NetSDKLib.LLong(ardCamera.getLoginId()); LoginModule.logout(loginId); } @@ -149,13 +148,9 @@ * 2023/10/17 8:28:13 */ @Override public boolean isOnLine(CameraCmd cmd) { public boolean isOnLine(ArdCamera ardCamera) { try { String cameraId = cmd.getCameraId(); if (!GlobalVariable.loginMap.containsKey(cameraId)) { return false; } NetSDKLib.LLong loginId = (NetSDKLib.LLong) GlobalVariable.loginMap.get(cameraId); NetSDKLib.LLong loginId = new NetSDKLib.LLong(ardCamera.getLoginId()); NetSDKLib.NET_WORKSTATE dh_ptz_location_info = new NetSDKLib.NET_WORKSTATE(); boolean b = ConfigModule.queryDevState(loginId, NET_DEVSTATE_ONLINE, dh_ptz_location_info); if (!b) { @@ -185,8 +180,8 @@ channel.setDeviceId(camera.getId()); channel.setChanNo(i); //获取视频编码 CameraCmd cameraCmd = new CameraCmd(camera.getId(), channel.getChanNo()); VideoCompressionCfg videoCompressionCfg = getVideoCompressionCfg(cameraCmd); camera.setChanNo(i); VideoCompressionCfg videoCompressionCfg = getVideoCompressionCfg(camera); if (StringUtils.isNotNull(videoCompressionCfg)) { channel.setVideoCode(videoCompressionCfg.getVideoEncType()); } @@ -202,16 +197,12 @@ } @Override public VideoCompressionCfg getVideoCompressionCfg(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); if (!GlobalVariable.loginMap.containsKey(cameraId)) { return null; } NetSDKLib.LLong loginId = (NetSDKLib.LLong) GlobalVariable.loginMap.get(cameraId); public VideoCompressionCfg getVideoCompressionCfg(ArdCamera ardCamera) { NetSDKLib.LLong loginId = new NetSDKLib.LLong(ardCamera.getLoginId()); NetSDKLib.CFG_ENCODE_INFO cfg_encode_info = new NetSDKLib.CFG_ENCODE_INFO(); cfg_encode_info.nChannelID = chanNo - 1; boolean b = ConfigModule.GetNewDevConfig(loginId, chanNo - 1, CFG_CMD_ENCODE, cfg_encode_info); cfg_encode_info.nChannelID = ardCamera.getChanNo() - 1; boolean b = ConfigModule.GetNewDevConfig(loginId, ardCamera.getChanNo() - 1, CFG_CMD_ENCODE, cfg_encode_info); if (b) { int emCompression = cfg_encode_info.stuMainStream[0].stuVideoFormat.emCompression;//视频格式 String videoEncType; ard-device/src/main/java/com/ard/device/component/sdk/hik/service/impl/HikVisionSDK.java
@@ -9,6 +9,7 @@ import com.ard.device.component.sdk.common.ResolutionEnum; import com.ard.device.component.sdk.common.SdkErrorCodeEnum; import com.ard.device.component.sdk.common.osSelect; import com.ard.device.component.sdk.dh.lib.NetSDKLib; import com.ard.device.component.sdk.event.LoginEvent; import com.ard.device.component.sdk.hik.lib.ExceptionCallBack; import com.ard.device.component.sdk.hik.lib.HCNetSDK; @@ -109,8 +110,6 @@ return; } log.info("Login Success 【 {}:{} 】", camera.getIp(), camera.getPort()); GlobalVariable.loginMap.put(camera.getId(), loginId); GlobalVariable.hikLoginMap.put(loginId, camera); camera.setLoginId((long) loginId); camera.setState("1"); @@ -138,26 +137,15 @@ * 注销 */ @Override public void logout(String cameraId) { if (!GlobalVariable.loginMap.containsKey(cameraId)) { return; } Integer userId = (Integer) GlobalVariable.loginMap.get(cameraId); boolean b = hCNetSDK.NET_DVR_Logout(userId); if (b) { GlobalVariable.loginMap.remove(cameraId); } public void logout(ArdCamera ardCamera) { hCNetSDK.NET_DVR_Logout(ardCamera.getLoginId().intValue()); } @Override public boolean isOnLine(CameraCmd cmd) { public boolean isOnLine(ArdCamera ardCamera) { try { String cameraId = cmd.getCameraId(); if (!GlobalVariable.loginMap.containsKey(cameraId)) { return false; } Integer userId = (Integer) GlobalVariable.loginMap.get(cameraId); return hCNetSDK.NET_DVR_RemoteControl(userId, NET_DVR_CHECK_USER_STATUS, null, 0); return hCNetSDK.NET_DVR_RemoteControl(ardCamera.getLoginId().intValue(), NET_DVR_CHECK_USER_STATUS, null, 0); } catch (Exception ex) { log.error("检测在线异常:{}", ex.getMessage()); return false; @@ -169,19 +157,13 @@ * */ @Override public VideoCompressionCfg getVideoCompressionCfg(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); if (!GlobalVariable.loginMap.containsKey(cameraId)) { return null; } Integer userId = (Integer) GlobalVariable.loginMap.get(cameraId); public VideoCompressionCfg getVideoCompressionCfg(ArdCamera ardCamera) { Integer chanNo = ardCamera.getChanNo(); int userId = ardCamera.getLoginId().intValue(); HCNetSDK.NET_DVR_COMPRESSIONCFG_V30 compressionCfg = new HCNetSDK.NET_DVR_COMPRESSIONCFG_V30(); compressionCfg.write(); Pointer point = compressionCfg.getPointer(); IntByReference ibrBytesReturned = new IntByReference(0); boolean bool = hCNetSDK.NET_DVR_GetDVRConfig(userId, HCNetSDK.NET_DVR_GET_COMPRESSCFG_V30, chanNo, point, compressionCfg.size(), ibrBytesReturned); if (bool) { @@ -432,8 +414,8 @@ channel.setName(channelName); channel.setChanNo(chanNo); //获取视频编码 CameraCmd cameraCmd = new CameraCmd(camera.getId(), chanNo); VideoCompressionCfg videoCompressionCfg = getVideoCompressionCfg(cameraCmd); camera.setChanNo(chanNo); VideoCompressionCfg videoCompressionCfg = getVideoCompressionCfg(camera); if (StringUtils.isNotNull(videoCompressionCfg)) { channel.setVideoCode(videoCompressionCfg.getVideoEncType()); } ard-device/src/main/java/com/ard/device/component/sdk/service/CameraSDK.java
@@ -20,13 +20,13 @@ void login(ArdCamera cameras); //注销 void logout(String cameraId); void logout(ArdCamera ardCamera); //在线状态 boolean isOnLine(CameraCmd cmd); boolean isOnLine(ArdCamera ardCamera); //获取码流压缩参数 VideoCompressionCfg getVideoCompressionCfg(CameraCmd cmd); VideoCompressionCfg getVideoCompressionCfg(ArdCamera ardCamera); //获取相机通道信息 List<ArdChannel> getChannels(ArdCamera camera); ard-device/src/main/java/com/ard/device/component/sdk/service/CameraSDKService.java
@@ -55,15 +55,12 @@ //注销 @Override public void logout(String cameraId) { public void logout(ArdCamera ardCamera) { try { ArdCamera ardCamera = redisService.getCacheObject(CacheConstants.CAMERA_LIST_KEY + cameraId); if (ardCamera != null) { String factory = ardCamera.getFactory(); CameraSDK cameraSDK = cameraSDKFactory.createCameraSDK(factory); cameraSDK.logout(cameraId); GlobalVariable.loginMap.remove(cameraId);//移除相机登录id缓存 } cameraSDK.logout(ardCamera); } catch (Exception ex) { log.error("注销异常:{}", ex.getMessage()); } @@ -71,17 +68,11 @@ //在线检测 @Override public boolean isOnLine(CameraCmd cmd) { public boolean isOnLine(ArdCamera ardCamera) { try { boolean onLine = false; String cameraId = cmd.getCameraId(); ArdCamera ardCamera = redisService.getCacheObject(CacheConstants.CAMERA_LIST_KEY + cameraId); if (ardCamera != null) { String factory = ardCamera.getFactory(); CameraSDK cameraSDK = cameraSDKFactory.createCameraSDK(factory); onLine = cameraSDK.isOnLine(cmd); } return onLine; return cameraSDK.isOnLine(ardCamera); } catch (Exception ex) { log.error("检测在线异常:{}", ex.getMessage()); return false; @@ -90,16 +81,11 @@ @Override //获取码流压缩参数 public VideoCompressionCfg getVideoCompressionCfg(CameraCmd cmd) { public VideoCompressionCfg getVideoCompressionCfg(ArdCamera ardCamera) { try { String cameraId = cmd.getCameraId(); ArdCamera ardCamera = redisService.getCacheObject(CacheConstants.CAMERA_LIST_KEY + cameraId); if (ardCamera == null) { return null; } String factory = ardCamera.getFactory(); CameraSDK cameraSDK = cameraSDKFactory.createCameraSDK(factory); return cameraSDK.getVideoCompressionCfg(cmd); return cameraSDK.getVideoCompressionCfg(ardCamera); } catch (Exception ex) { log.error("获取码流压缩参数异常:{}", ex.getMessage()); return null; ard-framework/src/main/java/com/ard/framework/config/ThreadPoolConfig.java
@@ -62,4 +62,32 @@ } }; } @Bean("mediaVideoExecutor") public ThreadPoolTaskExecutor mediaVideoExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); // 核心线程数:同时处理视频的核心线程数 executor.setMaxPoolSize(10); // 最大线程数:线程池不会扩展超过这个数量 executor.setQueueCapacity(50); // 队列容量:等待执行的任务数量,之前20太小可能会拒绝任务 executor.setThreadNamePrefix("media-video-"); // 当线程池和队列都满时的拒绝策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } @Bean("mediaPicExecutor") public ThreadPoolTaskExecutor mediaPicExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); // 核心线程数:同时处理视频的核心线程数 executor.setMaxPoolSize(10); // 最大线程数:线程池不会扩展超过这个数量 executor.setQueueCapacity(50); // 队列容量:等待执行的任务数量,之前20太小可能会拒绝任务 executor.setThreadNamePrefix("media-pic-"); // 当线程池和队列都满时的拒绝策略 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } } ard-quartz/pom.xml
@@ -34,6 +34,14 @@ <groupId>com.ard</groupId> <artifactId>ard-common</artifactId> </dependency> <dependency> <groupId>com.ard</groupId> <artifactId>ard-work</artifactId> </dependency> <dependency> <groupId>com.ard</groupId> <artifactId>ard-device</artifactId> </dependency> </dependencies> ard-quartz/src/main/java/com/ard/quartz/task/DeviceTask.java
New file @@ -0,0 +1,258 @@ package com.ard.quartz.task; import com.ard.common.constant.CacheConstants; import com.ard.common.core.redis.RedisCache; import com.ard.common.utils.DateUtils; import com.ard.common.utils.file.MimeTypeUtils; import com.ard.device.camera.domain.ArdCamera; import com.ard.device.camera.service.IArdCameraService; import com.ard.device.channel.domain.ArdChannel; import com.ard.device.channel.service.IArdChannelService; import com.ard.device.component.sdk.service.CameraSDKService; import com.ard.oss.service.impl.OssServiceContext; import com.ard.vtdu.config.MediamtxConfig; import com.ard.vtdu.domain.ArdVtdu; import com.ard.vtdu.service.IArdMediaService; import com.ard.vtdu.service.IArdVtduService; import com.ard.work.domain.entity.ArdMediaPic; import com.ard.work.domain.entity.ArdMediaVideo; import com.ard.work.mapper.ArdMediaPicMapper; import com.ard.work.mapper.ArdMediaVideoMapper; import com.ard.work.util.osSelect; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; import javax.annotation.Resource; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; import java.security.cert.X509Certificate; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; /** * 定时同步设备任务 * * @author ard */ @Component("DeviceTask") @Slf4j public class DeviceTask { @Resource private RedisCache redisCache; @Resource private IArdChannelService ardChannelService; @Resource private IArdVtduService ardVtduService; @Resource private MediamtxConfig mediamtxConfig; @Resource private CameraSDKService cameraSDKService; @Resource private IArdCameraService ardCameraService; @Resource private IArdMediaService ardMediaService; /** * 定时执行登录失败相机重新登录 */ public void failedReconnect() { try { log.debug("定时执行失败相机重连任务"); // 从数据库取全部设备 ArdCamera ardCamera = new ArdCamera(); ardCamera.setUseDataScope(false); List<ArdCamera> cameraList = ardCameraService.selectArdCameraList(ardCamera); cameraList.stream() // 只处理登录失败或离线的设备 .filter(camera -> camera.getLoginId() == null || camera.getLoginId() < 0 || "0".equals(camera.getState())) .forEach(camera -> { log.info("尝试重新登录设备 {}:{}", camera.getIp(), camera.getPort()); cameraSDKService.login(camera); }); } catch (Exception ex) { log.error("定时执行失败相机重连任务异常:{}", ex.getMessage()); } } /** * 同步相机在线状态 */ public void syncCameraState() { try { log.debug("定时执行同步相机在线状态任务"); //调用查询方法 ArdCamera ardCamera = new ArdCamera(); ardCamera.setUseDataScope(false); List<ArdCamera> ardCameraList = ardCameraService.selectArdCameraList(ardCamera); ardCameraList .forEach(camera -> { boolean onLineNew = cameraSDKService.isOnLine(camera); boolean onLineOld = "1".equals(camera.getState()); if (onLineNew != onLineOld) { if (onLineNew) { camera.setState("1"); } else { camera.setState("0"); } camera.setUpdateTime(DateUtils.getNowDate()); ardCameraService.updateArdCamera(camera); } }); } catch (Exception ex) { log.error("同步相机在线状态异常:{}", ex.getMessage()); } } /** * 同步相机通道 */ public void syncChannel() { try { log.debug("定时执行同步通道任务"); ArdCamera camera = new ArdCamera(); camera.setUseDataScope(false); List<ArdCamera> ardCameraList = ardCameraService.selectArdCameraList(camera); if (!ardCameraList.isEmpty()) { ardCameraList .forEach(ardCamera -> { //SDK获取实际通道 List<ArdChannel> ardChannelListSDK = cameraSDKService.getChannels(ardCamera); //数据库获取通道 ArdChannel ardChannel = new ArdChannel(); ardChannel.setUseDataScope(false); ardChannel.setDeviceId(ardCamera.getId()); List<ArdChannel> ardChannelListDb = ardChannelService.selectArdChannelList(ardChannel); //同步通道 ardCameraService.asyncChannel(ardChannelListDb, ardChannelListSDK); }); } } catch (Exception ex) { log.error("定时执行同步通道任务异常:{}", ex.getMessage()); } } /** * 同步通道流媒体 */ public void syncVtdu() { log.debug("定时同步Vtdu任务"); ArdChannel ardChannel = new ArdChannel(); ardChannel.setUseDataScope(false); List<ArdChannel> channels = ardChannelService.selectArdChannelList(ardChannel); if (channels == null || channels.isEmpty()) { // log.warn("没有获取到通道数据,退出同步"); return; } List<ArdVtdu> ardVtduList = ardVtduService.selectArdVtduList(new ArdVtdu()); Set<String> validNames = channels.stream() .map(channel -> channel.getDeviceId() + "_" + channel.getChanNo()) .collect(Collectors.toSet()); // 2. 合并操作:增、删、改 channels.forEach(channel -> { String name = channel.getDeviceId() + "_" + channel.getChanNo(); // 生成媒体表的 name String cameraId = channel.getDeviceId(); // cameraId 就是通道的 deviceId String rtspSourceUrl = getRtspSource(channel); String rtspUrl = "rtsp://" + mediamtxConfig.getPlayHost() + ":" + mediamtxConfig.getRtspPort() + "/" + name; // // 拼接 webrtcUrl String rtmpUrl = "rtmp://" + mediamtxConfig.getPlayHost() + ":" + mediamtxConfig.getRtmpPort() + "/" + name; // 拼接 // webrtcUrl String protocol = mediamtxConfig.isHttps() ? "https://" : "http://"; String webrtcUrl = protocol + mediamtxConfig.getPlayHost() + ":" + mediamtxConfig.getWebrtcPort() + "/" + name; // 拼接 webrtcUrl // 先检查媒体表是否已有此记录 Optional<ArdVtdu> existingMedia = ardVtduList.stream() .filter(media -> media.getName().equals(name)) // 查找是否已有对应的媒体记录 .findFirst(); if (existingMedia.isPresent()) { // 如果已有,更新现有记录 ArdVtdu vtdu = existingMedia.get(); // 更新前后内容相同则不进行更新 if (!vtdu.getRtspSource().equals(rtspSourceUrl) || !vtdu.getWebrtcUrl().equals(webrtcUrl) || !vtdu.getRtspUrl().equals(rtspUrl) || !vtdu.getRtmpUrl().equals(rtmpUrl)) { vtdu.setRtspUrl(rtspUrl); vtdu.setRtmpUrl(rtmpUrl); vtdu.setWebrtcUrl(webrtcUrl); ardVtduService.updateArdVtdu(vtdu); log.info("更新媒体记录:{}", vtdu.getName()); } } else { // 如果没有,插入新的记录 ArdVtdu vtdu = new ArdVtdu(); vtdu.setName(name); vtdu.setRtspSource(rtspSourceUrl); vtdu.setCameraId(cameraId); vtdu.setRtspUrl(rtspUrl); vtdu.setRtmpUrl(rtmpUrl); vtdu.setWebrtcUrl(webrtcUrl); ardVtduService.insertArdVtdu(vtdu); // 保存到媒体表 log.info("插入新媒体记录:{}", vtdu.getName()); } }); // 3. 删除操作:删除媒体表中多余的记录 List<String> invalidNames = ardVtduList.stream() .filter(vtdu -> !validNames.contains(vtdu.getName())) .filter(vtdu -> !"publisher".equalsIgnoreCase(vtdu.getRtspSource())) // 不是机场流 .map(ArdVtdu::getName) .collect(Collectors.toList()); if (!invalidNames.isEmpty()) { // 将 List<String> 转换为 String[] String[] namesArray = invalidNames.toArray(new String[0]); ardVtduService.deleteArdVtduByNames(namesArray); // 传递数组 log.info("删除不再存在的媒体记录:{}", Arrays.toString(namesArray)); } } // 根据通道信息获取rtsp源地址 private String getRtspSource(ArdChannel channel) { String rtspSource = ""; ArdCamera ardCamera = redisCache.getCacheObject(CacheConstants.CAMERA_LIST_KEY + channel.getDeviceId()); if (ardCamera != null) { switch (ardCamera.getFactory()) { case "1": rtspSource = "rtsp://" + ardCamera.getUsername() + ":" + ardCamera.getPassword() + "@" + ardCamera.getIp() + ":" + ardCamera.getRtspPort() + "/h264/ch" + channel.getChanNo() + "/main/av_stream"; break; case "2": rtspSource = "rtsp://" + ardCamera.getUsername() + ":" + ardCamera.getPassword() + "@" + ardCamera.getIp() + ":" + ardCamera.getRtspPort() + "/cam/realmonitor?channel=" + channel.getChanNo() + "&subtype=0"; break; } } return rtspSource; } /** * 同步mediamtx */ public void syncMediamtx() { log.debug("定时同步MediaMTX任务"); List<String> nameList = ardMediaService.getNameList(); List<ArdVtdu> ardVtduList = ardVtduService.selectArdVtduList(new ArdVtdu()); ardVtduService.asyncMediaMTX(ardVtduList, nameList); } } ard-quartz/src/main/java/com/ard/quartz/task/MediaTask.java
New file @@ -0,0 +1,353 @@ package com.ard.quartz.task; import com.ard.common.utils.StringUtils; import com.ard.common.utils.file.MimeTypeUtils; import com.ard.oss.service.impl.OssServiceContext; import com.ard.work.domain.entity.ArdMediaPic; import com.ard.work.domain.entity.ArdMediaVideo; import com.ard.work.mapper.ArdMediaPicMapper; import com.ard.work.mapper.ArdMediaVideoMapper; import com.ard.work.util.osSelect; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; import javax.annotation.Resource; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * 定时同步媒体任务 * * @author ard */ @Component("MediaTask") @Slf4j public class MediaTask { @Resource private ArdMediaPicMapper ardMediaPicMapper; @Resource private ArdMediaVideoMapper ardMediaVideoMapper; @Resource private OssServiceContext ossServiceContext; @Resource(name = "mediaVideoExecutor") private ThreadPoolTaskExecutor mediaVideoExecutor; @Resource(name = "mediaPicExecutor") private ThreadPoolTaskExecutor mediaPicExecutor; private final AtomicBoolean runningVideo = new AtomicBoolean(false); private final AtomicBoolean runningPic = new AtomicBoolean(false); public void syncVideoMediaUrl() { log.warn("开始同步视频媒体"); if (!runningVideo.compareAndSet(false, true)) { log.warn("上一次同步还没完成,跳过本次调度"); return; } try { while (true) { List<ArdMediaVideo> videos = ardMediaVideoMapper.selectPendingVideos(10); if (videos.isEmpty()) { break; // 没有待处理视频,退出循环 } for (ArdMediaVideo video : videos) { int updated = ardMediaVideoMapper.markProcessing(video.getId()); if (updated != 1) continue; mediaVideoExecutor.execute(() -> processOneVideo(video)); } Thread.sleep(200); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { runningVideo.set(false); } } public void syncPicMediaUrl() { log.warn("开始同步图片媒体"); if (!runningPic.compareAndSet(false, true)) { log.warn("上一次同步还没完成,跳过本次调度"); return; } try { while (true) { List<ArdMediaPic> pics = ardMediaPicMapper.selectPendingPics(10); // 查询待处理的图片 if (pics.isEmpty()) { break; // 没有待处理图片,退出循环 } for (ArdMediaPic pic : pics) { // 🔒 原子抢占:0 → 1 int updated = ardMediaPicMapper.markProcessing(pic.getId()); if (updated != 1) { continue; // 已被别的任务抢占 } mediaPicExecutor.execute(() -> processOnePic(pic)); } // 短暂 sleep,避免无限循环CPU空转 Thread.sleep(200); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { runningPic.set(false); } } private void processOneVideo(ArdMediaVideo video) { int maxRetries = 3; // 最大重试次数 int retryDelay = 3000; // 每次重试等待 3 秒 int attempt = 0; while (attempt < maxRetries) { attempt++; try (InputStream is = openUrlInputStream(video.getSourceUrl())) { if (is != null) { String mediaUrl = ossServiceContext.upload( "record", video.getTaskId() + "/" + video.getId() + ".mp4", is, MimeTypeUtils.VIDEO_MP4 ); video.setMediaUrl(mediaUrl); // 生成视频缩略图 processVideoThumbnail(video); video.setSyncStatus(2); // 成功 break; // 上传成功,退出循环 } else { log.warn("视频尚未生成,尝试重试 {}/{}", attempt, maxRetries); } } catch (Exception e) { log.error("视频同步异常 videoId={}, 尝试 {}/{}", video.getId(), attempt, maxRetries, e); } if (attempt < maxRetries) { try { Thread.sleep(retryDelay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } else { video.setSyncStatus(3); // 达到最大重试仍失败 } } video.setUpdateTime(new Date()); try { ardMediaVideoMapper.updateArdMediaVideo(video); } catch (Exception e) { log.error("更新视频状态失败 videoId={}", video.getId(), e); } } private void processOnePic(ArdMediaPic pic) { int maxRetries = 3; // 最大重试次数 int retryDelay = 2000; // 每次重试等待 2 秒 int attempt = 0; while (attempt < maxRetries) { attempt++; try (InputStream inputStream = openUrlInputStream(pic.getSourceUrl())) { if (inputStream != null) { String url = ossServiceContext.upload( "pic", pic.getTaskId() + "/" + pic.getId() + ".jpg", inputStream, MimeTypeUtils.IMAGE_JPG ); pic.setMediaUrl(url); pic.setSyncStatus(2); // 成功 break; // 上传成功,退出循环 } else { log.warn("图片尚未生成,尝试重试 {}/{}", attempt, maxRetries); } } catch (Exception e) { log.error("图片同步异常 taskId={}, photoId={}, 尝试 {}/{}", pic.getTaskId(), pic.getId(), attempt, maxRetries, e); } if (attempt < maxRetries) { try { Thread.sleep(retryDelay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } else { pic.setSyncStatus(3); // 达到最大重试仍失败 } } pic.setUpdateTime(new Date()); try { ardMediaPicMapper.updatePic(pic); } catch (Exception e) { log.error("更新图片状态失败 photoId={}", pic.getId(), e); } } /** * 处理视频缩略图 */ private void processVideoThumbnail(ArdMediaVideo video) { // 生成缩略图 try (ByteArrayInputStream in = generateThumbnail(video.getSourceUrl())) { String thumbUrl = ossServiceContext.upload( "record", video.getTaskId() + "/" + video.getId() + ".jpg", in, MimeTypeUtils.IMAGE_JPEG ); video.setThumbnailUrl(thumbUrl); } catch (Exception e) { log.warn("视频缩略图生成或上传失败 taskId={}, videoId={}", video.getTaskId(), video.getId(), e); } // 获取文件大小 try { long videoSize = getFileSizeFromUrl(video.getSourceUrl()); video.setFileSize(videoSize); } catch (Exception e) { log.warn("获取视频大小失败 url={}", video.getSourceUrl(), e); } } private long getFileSizeFromUrl(String fileUrl) throws IOException { URL url = new URL(fileUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("HEAD"); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); conn.connect(); long size = conn.getContentLengthLong(); // 返回字节数 conn.disconnect(); return size; } private ByteArrayInputStream generateThumbnail(String videoFile) { String ffmpeg = osSelect.isWindows() ? "ffmpeg" : "/usr/local/ffmpeg/bin/ffmpeg"; Path tempImg = null; try { tempImg = Files.createTempFile("thumb_", ".jpg"); List<String> cmd = new ArrayList<>(); cmd.add(ffmpeg); cmd.add("-y"); cmd.add("-loglevel"); cmd.add("error"); cmd.add("-i"); cmd.add(videoFile); cmd.add("-ss"); cmd.add(String.valueOf(1)); cmd.add("-vframes"); cmd.add("1"); cmd.add("-vf"); cmd.add("scale=" + 200 + ":-1"); cmd.add(tempImg.toAbsolutePath().toString()); log.info("ffmpeg cmd: {}", String.join(" ", cmd)); Process process = new ProcessBuilder(cmd).start(); drain(process.getErrorStream()); boolean finished = process.waitFor(15, TimeUnit.SECONDS); // 超时保护 if (!finished) { process.destroyForcibly(); throw new RuntimeException("ffmpeg timeout"); } if (process.exitValue() != 0) { throw new RuntimeException("ffmpeg exit=" + process.exitValue()); } byte[] data = Files.readAllBytes(tempImg); return new ByteArrayInputStream(data); } catch (Exception e) { throw new RuntimeException("生成缩略图失败", e); } finally { if (tempImg != null) { try { Files.deleteIfExists(tempImg); } catch (IOException ignore) { } } } } private static void drain(InputStream is) { new Thread(() -> { try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) { while (br.readLine() != null) { } } catch (IOException ignore) { } }, "ffmpeg-stderr").start(); } /** * 将 URL 转成 InputStream * * @param fileUrl 文件 URL * @return InputStream,如果失败返回 null */ private InputStream openUrlInputStream(String fileUrl) { 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(5000); connection.setReadTimeout(10000); return connection.getInputStream(); } catch (Exception e) { e.printStackTrace(); return null; } } } ard-route/src/main/java/com/ard/route/service/impl/ArdRouteInfoServiceImpl.java
@@ -81,8 +81,10 @@ public ArdRouteInfo selectArdRouteInfoById(String id) { ArdRouteInfo ardRouteInfo = ardRouteInfoMapper.selectArdRouteInfoById(id); try { if (ardRouteInfo != null) { if (ardRouteInfo.getRouteData() != null){ ardRouteInfo.setUavRouteReq((UavRouteReq) deserialize(ardRouteInfo.getRouteData())); } } } catch (IOException | ClassNotFoundException e) { throw new RuntimeException(e); @@ -144,18 +146,7 @@ } ardRouteInfo.getUavRouteReq().setDroneType(subStringDroneType(ardRouteInfo.getAircraft())); KmzResultVo kmzResult = routeService.buildKmz(ardRouteInfo.getUavRouteReq(),ardRouteInfo.getName()); if (M350_RTK.getValue().equals(ardRouteInfo.getUavRouteReq().getDroneType())) { // 变焦数值加工 for (RoutePointReq routePointReq : ardRouteInfo.getUavRouteReq().getRoutePointList()) { for (ActionGroupReq actionGroupReq : routePointReq.getActionGroupList()) { for (PointActionReq action : actionGroupReq.getActions()) { if (action.getZoom() != null){ action.setZoom(divided_23(action.getZoom())); } } } } } zoomProcessing(ardRouteInfo); ardRouteInfo.setPath(kmzResult.getPath()); ardRouteInfo.setReadOnly("0"); ardRouteInfoMapper.insertArdRouteInfo(dataProcessing(ardRouteInfo)); @@ -179,6 +170,8 @@ // 先删除航线 delOssService(route); // 再生成航线 ardRouteInfo.getUavRouteReq().setDroneType(subStringDroneType(ardRouteInfo.getAircraft())); zoomProcessing(ardRouteInfo); kmzResult = routeService.buildKmz(ardRouteInfo.getUavRouteReq(), ardRouteInfo.getName()); if ("更新航线kmz文件失败".equals(kmzResult.getData())) { return 0; @@ -186,6 +179,22 @@ ardRouteInfo.setPath(kmzResult.getPath()); } return ardRouteInfoMapper.updateArdRouteInfo(dataProcessing(ardRouteInfo)); } // 变倍加工方法 private void zoomProcessing(ArdRouteInfo ardRouteInfo) { if (M350_RTK.getValue().equals(ardRouteInfo.getUavRouteReq().getDroneType())) { // 变焦数值加工 for (RoutePointReq routePointReq : ardRouteInfo.getUavRouteReq().getRoutePointList()) { for (ActionGroupReq actionGroupReq : routePointReq.getActionGroupList()) { for (PointActionReq action : actionGroupReq.getActions()) { if (action.getZoom() != null){ action.setZoom(divided_23(action.getZoom())); } } } } } } // 新增信息加工 @@ -413,7 +422,7 @@ public Boolean routeBegins(String id,String boxSn) { ArdRouteInfo route = ardRouteInfoMapper.selectArdRouteInfoById(id); // 判断是否循环航线 if ("1".equals(route.getIsLoop())) { if (null != route && "0".equals(route.getIsLoop())){ deleteArdRouteInfoById(id); } WayLineBeginDTO wayLineBeginDTO = new WayLineBeginDTO(boxSn); ard-route/src/main/java/com/ard/route/util/RouteFileUtils.java
@@ -541,7 +541,11 @@ }else { kmlActionActuatorFuncParam.setFileSuffix("panoramicPhoto_360"); } if (pointActionReq.getUseGlobalImageFormat() != null) { kmlActionActuatorFuncParam.setUseGlobalPayloadLensIndex(String.valueOf(pointActionReq.getUseGlobalImageFormat())); }else { kmlActionActuatorFuncParam.setUseGlobalPayloadLensIndex("0"); } if (ObjectUtil.equals(pointActionReq.getUseGlobalImageFormat(), 0)) { kmlActionActuatorFuncParam.setPayloadLensIndex(pointActionReq.getImageFormat()); } else { ard-sample/src/main/java/com/ard/sample/manage/service/impl/LiveStreamServiceImpl.java
@@ -21,7 +21,6 @@ import com.ard.sample.manage.service.ILiveStreamService; import com.ard.vtdu.config.MediamtxConfig; import com.ard.vtdu.domain.ArdVtdu; import com.ard.vtdu.service.IArdMediaService; import com.ard.vtdu.service.IArdVtduService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; ard-system/src/main/java/com/ard/data/map/mapper/ArdMapMapper.java
@@ -1,6 +1,7 @@ package com.ard.data.map.mapper; import com.ard.data.map.domain.ArdMap; import org.apache.ibatis.annotations.Param; import java.util.List; @@ -59,4 +60,13 @@ * @return 结果 */ public int deleteArdMapByIds(String[] ids); /** * 校验地图管理信息名称是否唯一 * * @param mapName 名称 * @return 结果 */ public ArdMap checkArdMapNameUnique(@Param("mapName") String mapName); } ard-system/src/main/java/com/ard/data/map/service/IArdMapService.java
@@ -59,4 +59,13 @@ * @return 结果 */ public int deleteArdMapById(String id); /** * 校验地图管理信息名称是否唯一 * * @param ardMap 地图管理 * @return 结果 */ public String checkArdMapNameUnique(ArdMap ardMap); } ard-system/src/main/java/com/ard/data/map/service/impl/ArdMapServiceImpl.java
@@ -1,5 +1,6 @@ package com.ard.data.map.service.impl; import com.ard.common.constant.DeviceConstants; import com.ard.common.utils.uuid.IdUtils; import com.ard.data.map.domain.ArdMap; import com.ard.data.map.mapper.ArdMapMapper; @@ -87,4 +88,24 @@ public int deleteArdMapById(String id) { return ardMapMapper.deleteArdMapById(id); } /** * 校验地图管理信息名称是否唯一 * * @param ardMap 地图管理 * @return 结果 */ @Override public String checkArdMapNameUnique(ArdMap ardMap) { String id = ardMap.getId(); String mapName = ardMap.getMapName(); ArdMap map = ardMapMapper.checkArdMapNameUnique(mapName); if (map != null && (id == null || !id.equals(map.getId()))) { return DeviceConstants.NOT_UNIQUE; } return DeviceConstants.UNIQUE; } } ard-system/src/main/java/com/ard/data/model/mapper/ArdModelMapper.java
@@ -1,6 +1,7 @@ package com.ard.data.model.mapper; import com.ard.data.model.domain.ArdModel; import org.apache.ibatis.annotations.Param; import java.util.List; @@ -59,4 +60,13 @@ * @return 结果 */ public int deleteArdModelByIds(String[] ids); /** * 校验模型管理信息名称是否唯一 * * @param modelName 名称 * @return 结果 */ public ArdModel checkArdModelNameUnique(@Param("modelName") String modelName); } ard-system/src/main/java/com/ard/data/model/service/IArdModelService.java
@@ -59,4 +59,13 @@ * @return 结果 */ public int deleteArdModelById(String id); /** * 校验模型管理信息名称是否唯一 * * @param ardModel 模型管理 * @return 结果 */ public String checkArdModelNameUnique(ArdModel ardModel); } ard-system/src/main/java/com/ard/data/model/service/impl/ArdModelServiceImpl.java
@@ -2,6 +2,7 @@ import java.util.List; import com.ard.common.constant.DeviceConstants; import com.ard.common.utils.uuid.IdUtils; import com.ard.data.model.domain.ArdModel; import com.ard.data.model.mapper.ArdModelMapper; @@ -87,4 +88,24 @@ public int deleteArdModelById(String id) { return ardModelMapper.deleteArdModelById(id); } /** * 校验模型管理信息名称是否唯一 * * @param ardModel 模型管理 * @return 结果 */ @Override public String checkArdModelNameUnique(ArdModel ardModel) { String id = ardModel.getId(); String modelName = ardModel.getModelName(); ArdModel model = ardModelMapper.checkArdModelNameUnique(modelName); if (model != null && (id == null || !id.equals(model.getId()))) { return DeviceConstants.NOT_UNIQUE; } return DeviceConstants.UNIQUE; } } ard-system/src/main/java/com/ard/data/noflyzone/mapper/ArdNoFlyZoneMapper.java
@@ -2,6 +2,7 @@ import java.util.List; import com.ard.data.noflyzone.domain.ArdNoFlyZone; import org.apache.ibatis.annotations.Param; /** * 禁飞区管理Mapper接口 @@ -58,4 +59,12 @@ * @return 结果 */ public int deleteArdNoFlyZoneByIds(String[] ids); /** * 校验禁飞区名称是否唯一 * * @param name 禁飞区名称 * @return 禁飞区信息 */ public ArdNoFlyZone checkArdNoFlyZoneNameUnique(@Param("name") String name); } ard-system/src/main/java/com/ard/data/noflyzone/service/IArdNoFlyZoneService.java
@@ -58,4 +58,12 @@ * @return 结果 */ public int deleteArdNoFlyZoneById(String id); /** * 校验禁飞区名称是否唯一 * * @param ardNoFlyZone 禁飞区实体 * @return 结果 */ public String checkArdNoFlyZoneNameUnique(ArdNoFlyZone ardNoFlyZone); } ard-system/src/main/java/com/ard/data/noflyzone/service/impl/ArdNoFlyZoneServiceImpl.java
@@ -1,8 +1,11 @@ package com.ard.data.noflyzone.service.impl; import java.util.List; import com.ard.common.constant.DeviceConstants; import com.ard.common.utils.DateUtils; import com.ard.common.utils.SecurityUtils; import com.ard.common.utils.StringUtils; import com.ard.common.utils.uuid.IdUtils; import com.ard.data.noflyzone.service.IArdNoFlyZoneService; import org.springframework.stereotype.Service; @@ -19,8 +22,7 @@ * @date 2025-10-14 */ @Service public class ArdNoFlyZoneServiceImpl implements IArdNoFlyZoneService { public class ArdNoFlyZoneServiceImpl implements IArdNoFlyZoneService { @Resource private ArdNoFlyZoneMapper ardNoFlyZoneMapper; @@ -31,8 +33,7 @@ * @return 禁飞区管理 */ @Override public ArdNoFlyZone selectArdNoFlyZoneById(String id) { public ArdNoFlyZone selectArdNoFlyZoneById(String id) { return ardNoFlyZoneMapper.selectArdNoFlyZoneById(id); } @@ -43,8 +44,7 @@ * @return 禁飞区管理 */ @Override public List<ArdNoFlyZone> selectArdNoFlyZoneList(ArdNoFlyZone ardNoFlyZone) { public List<ArdNoFlyZone> selectArdNoFlyZoneList(ArdNoFlyZone ardNoFlyZone) { return ardNoFlyZoneMapper.selectArdNoFlyZoneList(ardNoFlyZone); } @@ -55,8 +55,7 @@ * @return 结果 */ @Override public int insertArdNoFlyZone(ArdNoFlyZone ardNoFlyZone) { public int insertArdNoFlyZone(ArdNoFlyZone ardNoFlyZone) { ardNoFlyZone.setId(IdUtils.simpleUUID()); ardNoFlyZone.setCreateTime(DateUtils.getNowDate()); ardNoFlyZone.setCreateBy(SecurityUtils.getUsername()); @@ -70,8 +69,7 @@ * @return 结果 */ @Override public int updateArdNoFlyZone(ArdNoFlyZone ardNoFlyZone) { public int updateArdNoFlyZone(ArdNoFlyZone ardNoFlyZone) { ardNoFlyZone.setUpdateBy(SecurityUtils.getUsername()); ardNoFlyZone.setUpdateTime(DateUtils.getNowDate()); return ardNoFlyZoneMapper.updateArdNoFlyZone(ardNoFlyZone); @@ -84,8 +82,7 @@ * @return 结果 */ @Override public int deleteArdNoFlyZoneByIds(String[] ids) { public int deleteArdNoFlyZoneByIds(String[] ids) { return ardNoFlyZoneMapper.deleteArdNoFlyZoneByIds(ids); } @@ -96,10 +93,26 @@ * @return 结果 */ @Override public int deleteArdNoFlyZoneById(String id) { public int deleteArdNoFlyZoneById(String id) { return ardNoFlyZoneMapper.deleteArdNoFlyZoneById(id); } /** * 校验禁飞区名称是否唯一 * * @param ardNoFlyZone 禁飞区实体 * @return 结果 */ @Override public String checkArdNoFlyZoneNameUnique(ArdNoFlyZone ardNoFlyZone) { String id = ardNoFlyZone.getId(); String name = ardNoFlyZone.getName(); ArdNoFlyZone info = ardNoFlyZoneMapper.checkArdNoFlyZoneNameUnique(name); if (info != null && (id == null || !id.equals(info.getId()))) { return DeviceConstants.NOT_UNIQUE; } return DeviceConstants.UNIQUE; } } ard-system/src/main/java/com/ard/data/tile/mapper/ArdTileMapper.java
@@ -1,6 +1,7 @@ package com.ard.data.tile.mapper; import com.ard.data.tile.domain.ArdTile; import org.apache.ibatis.annotations.Param; import java.util.List; @@ -59,4 +60,13 @@ * @return 结果 */ public int deleteArdTileByIds(String[] ids); /** * 校验tile管理信息名称是否唯一 * * @param tilesName 名称 * @return 结果 */ public ArdTile checkArdTileNameUnique(@Param("tilesName") String tilesName); } ard-system/src/main/java/com/ard/data/tile/service/IArdTileService.java
@@ -59,4 +59,13 @@ * @return 结果 */ public int deleteArdTileById(String id); /** * 校验tile管理信息名称是否唯一 * * @param ardTile tile管理 * @return 结果 */ public String checkArdTileNameUnique(ArdTile ardTile); } ard-system/src/main/java/com/ard/data/tile/service/impl/ArdTileServiceImpl.java
@@ -2,6 +2,7 @@ import java.util.List; import com.ard.common.constant.DeviceConstants; import com.ard.common.utils.uuid.IdUtils; import com.ard.data.tile.domain.ArdTile; import com.ard.data.tile.mapper.ArdTileMapper; @@ -87,4 +88,24 @@ public int deleteArdTileById(String id) { return ardTileMapper.deleteArdTileById(id); } /** * 校验tile管理信息名称是否唯一 * * @param ardTile tile管理 * @return 结果 */ @Override public String checkArdTileNameUnique(ArdTile ardTile) { String id = ardTile.getId(); String tilesName = ardTile.getTilesName(); ArdTile tile = ardTileMapper.checkArdTileNameUnique(tilesName); if (tile != null && (id == null || !id.equals(tile.getId()))) { return DeviceConstants.NOT_UNIQUE; } return DeviceConstants.UNIQUE; } } ard-system/src/main/resources/mapper/data/ArdMapMapper.xml
@@ -85,4 +85,9 @@ #{id} </foreach> </delete> <select id="checkArdMapNameUnique" resultMap="ArdMapResult"> select * from ard_map where map_name = #{mapName} limit 1 </select> </mapper> ard-system/src/main/resources/mapper/data/ArdModelMapper.xml
@@ -87,4 +87,9 @@ #{id} </foreach> </delete> <select id="checkArdModelNameUnique" parameterType="String" resultMap="ArdModelResult"> select * from ard_model where model_name = #{modelName} limit 1 </select> </mapper> ard-system/src/main/resources/mapper/data/ArdNoFlyZoneMapper.xml
@@ -83,4 +83,9 @@ #{id} </foreach> </delete> <select id="checkArdNoFlyZoneNameUnique" resultMap="ArdNoFlyZoneResult"> select * from ard_no_fly_zone where name = #{name} limit 1 </select> </mapper> ard-system/src/main/resources/mapper/data/ArdTileMapper.xml
@@ -104,4 +104,9 @@ #{id} </foreach> </delete> <select id="checkArdTileNameUnique" parameterType="String" resultMap="ArdTileResult"> select * from ard_tile where tiles_name = #{tilesName} limit 1 </select> </mapper> ard-vtdu/src/main/java/com/ard/vtdu/service/impl/ArdVtduServiceImpl.java
@@ -38,14 +38,6 @@ mediaService.clearAll(); } @Scheduled(cron = "0/5 * * * * ?") public void syncMediamtx() { log.debug("定时同步MediaMTX任务"); List<String> nameList = mediaService.getNameList(); List<ArdVtdu> ardVtduList = selectArdVtduList(new ArdVtdu()); asyncMediaMTX(ardVtduList, nameList); } /** * 查询流媒体管理 * ard-work/src/main/java/com/ard/work/domain/entity/ArdMedia.java
File was deleted ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaPic.java
New file @@ -0,0 +1,28 @@ package com.ard.work.domain.entity; import com.ard.common.core.domain.BaseEntity; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.*; import java.util.Date; @EqualsAndHashCode(callSuper = true) @Data @Builder @AllArgsConstructor @NoArgsConstructor public class ArdMediaPic extends BaseEntity { private Long id; private Integer taskId; private Long fileSize; private String fileMd5; private String mediaUrl; private String sourceUrl; private Double longitude; private Double latitude; private Double height; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date captureTime; //同步状态 0待处理 1处理中 2成功 3失败 private Integer syncStatus; } ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaVideo.java
New file @@ -0,0 +1,30 @@ package com.ard.work.domain.entity; import com.ard.common.core.domain.BaseEntity; import com.fasterxml.jackson.annotation.JsonFormat; import lombok.*; import java.util.Date; @EqualsAndHashCode(callSuper = true) @Data @Builder @AllArgsConstructor @NoArgsConstructor public class ArdMediaVideo extends BaseEntity { private Long id; private String name; private Long fileSize; private String mediaUrl; private String sourceUrl; private String thumbnailUrl; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date startTime; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date endTime; private Integer spanSeconds; private Integer taskId; //同步状态 0待处理 1处理中 2成功 3失败 private Integer syncStatus; } ard-work/src/main/java/com/ard/work/mapper/ArdMediaMapper.java
File was deleted ard-work/src/main/java/com/ard/work/mapper/ArdMediaPicMapper.java
New file @@ -0,0 +1,60 @@ package com.ard.work.mapper; import com.ard.work.domain.entity.ArdMediaPic; import com.ard.work.domain.entity.ArdMediaVideo; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; @Mapper public interface ArdMediaPicMapper { /** * 查询图片列表(条件查询) */ List<ArdMediaPic> selectPicList(ArdMediaPic query); /** * 查询待处理图片(sync_status = 0) */ List<ArdMediaPic> selectPendingPics(@Param("limit") int limit); /** * 根据任务ID查询图片 */ List<ArdMediaPic> selectPicByTaskId(@Param("taskId") Integer taskId); /** * 新增图片 */ int insertPic(ArdMediaPic pic); /** * 更新图片 */ int updatePic(ArdMediaPic pic); /** * 原子抢占:0 → 1 */ int markProcessing(@Param("id") Long id); /** * 更新同步状态 * 0 = 待处理 * 1 = 处理中 * 2 = 已完成 */ int updateSyncStatus(@Param("id") Long id, @Param("status") Integer status); /** * 根据ID删除图片 */ int deletePicById(@Param("id") String id); /** * 根据任务ID删除图片 */ int deletePicByTaskId(@Param("taskId") Integer taskId); } ard-work/src/main/java/com/ard/work/mapper/ArdMediaVideoMapper.java
New file @@ -0,0 +1,59 @@ package com.ard.work.mapper; import com.ard.work.domain.entity.ArdMediaVideo; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; @Mapper public interface ArdMediaVideoMapper { /** * 查询视频列表(条件查询) */ List<ArdMediaVideo> selectArdMediaVideoList(ArdMediaVideo query); /** * 查询待处理视频(sync_status = 0) */ List<ArdMediaVideo> selectPendingVideos(@Param("limit") int limit); /** * 原子抢占:0 → 1 */ int markProcessing(@Param("id") Long id); /** * 更新同步状态 * 0 = 待处理 * 1 = 处理中 * 2 = 已完成 */ int updateSyncStatus(@Param("id") Long id, @Param("status") Integer status); /** * 根据任务ID查询视频 */ List<ArdMediaVideo> selectArdMediaVideoByTaskId(@Param("taskId") Integer taskId); /** * 新增视频 */ int insertArdMediaVideo(ArdMediaVideo video); /** * 更新视频 */ int updateArdMediaVideo(ArdMediaVideo video); /** * 根据ID删除视频 */ int deleteArdMediaVideoById(@Param("id") String id); /** * 根据任务ID删除视频 */ int deleteArdMediaVideoByTaskId(@Param("taskId") Integer taskId); } ard-work/src/main/java/com/ard/work/service/IArdDroneInfoService.java
@@ -77,7 +77,7 @@ * @param deptId 部门ID * @return 结果 */ public int insertArdDroneLs(String[] boxSn,Long deptId); public int insertArdDroneLs(String[] boxSn,Long deptId,String serverId); /** * 修改无人机状态 ard-work/src/main/java/com/ard/work/service/IArdMediaService.java
@@ -1,7 +1,8 @@ package com.ard.work.service; import com.ard.work.domain.entity.ArdMedia; import com.ard.work.domain.entity.ArdFlightTask; import com.ard.work.domain.entity.ArdMediaPic; import com.ard.work.domain.entity.ArdMediaVideo; import java.util.List; /** @@ -10,11 +11,12 @@ * Date: 2025年11月18日13:15 **/ public interface IArdMediaService { /** * 查询媒体数据列表 * * @param ardMedia 媒体数据 * @return 媒体数据集合 */ public List<ArdMedia> selectArdMediaList(ArdMedia ardMedia); List<ArdMediaPic> selectArdMediaPicList(ArdMediaPic ardMediaPic); List<ArdMediaVideo> selectArdMediaVideoList(ArdMediaVideo ardMediaVideo); void insertPhotoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn); void insertVideoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn); void syncPicToDatabase(ArdFlightTask flightTask, String serverId, String boxSn); void syncVideoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn); } ard-work/src/main/java/com/ard/work/service/IArdServerInfoService.java
@@ -28,8 +28,6 @@ */ public List<ArdServerInfo> selectArdServerInfoList(ArdServerInfo ardServerInfo); public List<ArdServerInfo> selectArdServerInfoListNo(ArdServerInfo ardServerInfo); /** * 新增服务器管理 * ard-work/src/main/java/com/ard/work/service/ITyService.java
@@ -36,7 +36,7 @@ * * @return */ AccessTokenReceiver getAccessToken(String ServerId); AccessTokenReceiver getAccessToken(String serverId); /** * 获取全部服务器云盒列表 ard-work/src/main/java/com/ard/work/service/impl/ArdDroneInfoServiceImpl.java
@@ -164,7 +164,7 @@ * @return 成功插入的记录数 */ @Override public int insertArdDroneLs(String[] boxSn, Long deptId) { public int insertArdDroneLs(String[] boxSn,Long deptId,String serverId) { // 参数校验 if (boxSn == null || boxSn.length == 0) { log.warn("传入的盒子编码为空"); @@ -176,10 +176,12 @@ try { // 1. 检查已存在的无人机盒子 List<ArdDroneInfo> existingDrones = getExistingDrones(boxSn); // 1.1 新增时不能添加重复的boxSn checkDuplicateBox(existingDrones, boxSn); // 2. 授权数量验证 validateAuthorization(existingDrones.size(), boxSn.length); // 3. 获取并组装新的无人机信息 List<ArdDroneInfo> newDrones = assembleNewDrones(boxSn, deptId); List<ArdDroneInfo> newDrones = assembleNewDrones(boxSn,deptId,serverId); // 4. 批量插入 return batchInsertDrones(newDrones); } catch (Exception e) { @@ -204,6 +206,29 @@ .map(ArdDroneInfoMapper::selectArdDroneByBoxSn) .filter(Objects::nonNull) .collect(Collectors.toList()); } /** * 检查是否有重复的盒子编码 */ private void checkDuplicateBox(List<ArdDroneInfo> existingDrone,String[] boxSn) { Set<String> existingBoxSnSet = new HashSet<>(); for (ArdDroneInfo droneInfo : existingDrone) { existingBoxSnSet.add(droneInfo.getBoxSn()); } // 检查是否有重复的盒子编码 List<String> duplicateBoxSns = new ArrayList<>(); for (String sn : boxSn) { if (existingBoxSnSet.contains(sn)) { duplicateBoxSns.add(sn); } } if (!duplicateBoxSns.isEmpty()) { log.warn("存在重复的无人机盒子编码: {}", duplicateBoxSns); throw new ServiceException("以下无人机盒子编码已存在: " + String.join(", ", duplicateBoxSns)); } } /** @@ -236,19 +261,19 @@ /** * 组装新的无人机信息 */ private List<ArdDroneInfo> assembleNewDrones(String[] boxSn, Long deptId) { private List<ArdDroneInfo> assembleNewDrones(String[] boxSn,Long deptId,String serverId) { List<BoxInfoReceiver> allBoxes = tyService.getBoxList(); Set<String> targetBoxSnSet = new HashSet<>(Arrays.asList(boxSn)); return allBoxes.stream() .filter(box -> targetBoxSnSet.contains(box.getBoxSn())) .map(box -> createDroneInfo(box, deptId)) .map(box -> createDroneInfo(box, deptId, serverId)) .collect(Collectors.toList()); } /** * 创建无人机信息对象 */ private ArdDroneInfo createDroneInfo(BoxInfoReceiver box, Long deptId) { private ArdDroneInfo createDroneInfo(BoxInfoReceiver box, Long deptId ,String serverId) { ArdDroneInfo drone = new ArdDroneInfo(); drone.setId(IdUtils.simpleUUID()); drone.setBoxSn(box.getBoxSn()); @@ -259,21 +284,13 @@ drone.setVideoPort(box.getVideoPort()); drone.setLastTaskId(box.getLastTaskId()); drone.setDeptId(deptId); // 设置创建信息和更新信息 setCreateAndUpdateInfo(drone); drone.setCreateTime(new Date()); drone.setUpdateTime(new Date()); drone.setServerId(serverId); drone.setBoxVersion(box.getBoxVersion()); return drone; } /** * 设置创建和更新信息 */ private void setCreateAndUpdateInfo(ArdDroneInfo drone) { Date currentTime = new Date(); drone.setCreateTime(currentTime); drone.setUpdateTime(currentTime); } /** * 批量插入无人机信息 @@ -345,32 +362,5 @@ } /** * 更新 localBoxInfo 的字段,如果有变化返回 true,否则返回 false */ private boolean updateChangedAttributes(ArdDroneInfo localBoxInfo, BoxInfoReceiver remoteArdDrone) { boolean changed = false; if (!Objects.equals(localBoxInfo.getBoxName(), remoteArdDrone.getBoxName())) { localBoxInfo.setBoxName(remoteArdDrone.getBoxName()); changed = true; } if (!Objects.equals(localBoxInfo.getBoxVersion(), remoteArdDrone.getBoxVersion())) { localBoxInfo.setBoxVersion(remoteArdDrone.getBoxVersion()); changed = true; } if (!Objects.equals(localBoxInfo.getLastTaskId(), remoteArdDrone.getLastTaskId())) { localBoxInfo.setLastTaskId(remoteArdDrone.getLastTaskId()); changed = true; } if (!Objects.equals(localBoxInfo.getOnLine(), remoteArdDrone.getOnLine())) { localBoxInfo.setOnLine(remoteArdDrone.getOnLine()); changed = true; } // TODO: 更新其他属性时,如果变化也设置 changed = true return changed; } } ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskPartServiceImpl.java
@@ -1,6 +1,7 @@ package com.ard.work.service.impl; import cn.hutool.core.util.SerializeUtil; import com.ard.common.utils.DateUtils; import com.ard.work.domain.cache.UavStateCache; import com.ard.work.domain.cache.UavTelemetryDataCache; import com.ard.work.domain.dto.UavFlightStateDTO; @@ -44,22 +45,22 @@ /** * 定时任务:每秒读取一次设备状态数据 * 注意:这里模拟读取状态,实际应该调用设备接口或缓存 */ @Scheduled(fixedRate = 1000) public void readTelemetryTask() { long now = System.currentTimeMillis(); UavTelemetryDataCache.forEach((boxSn, telemetryData) -> { // 1️⃣ 从状态缓存里取当前状态 UavState uavState = UavStateCache.get(boxSn); if (uavState == null) { log.warn("状态不存在,不记录"); return; // 状态不存在,不记录 } // 2️⃣ 使用遥测自身时间戳判断是否还活着 long dataTs = telemetryData.getTimestamp(); // 设备时间 / 服务器接收时间 if (now - dataTs > 3000) { log.warn("超过 3 秒没有新遥测 → 认为已离线或冻结,不记录:{}", new Date(dataTs)); return; // 超过 3 秒没有新遥测 → 认为已离线或冻结,不记录 } @@ -71,18 +72,13 @@ flushRemaining(boxSn); } lastFlightStatusMap.put(boxSn, currentStatus); log.debug("设备 {} 当前状态为:{}", boxSn, currentStatus); // 只有飞行中才添加缓存 if (currentStatus == 2) { log.warn("设备 {} 当前状态为飞行中,添加遥测数据到缓存", boxSn); UavFlightStateDTO dto = new UavFlightStateDTO(uavState, telemetryData, dataTs); addTelemetryData(dto); } UavFlightStateDTO dto = new UavFlightStateDTO( uavState, telemetryData, dataTs ); addTelemetryData(dto); }); } ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskServiceImpl.java
@@ -4,47 +4,23 @@ import com.ard.common.core.domain.entity.SysUser; import com.ard.common.core.redis.RedisCache; 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.IArdMediaService; import com.ard.work.service.IArdServerInfoService; import com.ard.work.service.control.UavControlService; import com.ard.work.util.osSelect; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; import javax.annotation.Resource; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @Slf4j @Service @@ -53,19 +29,16 @@ @Resource private ArdFlightTaskMapper ardFlightTaskMapper; @Resource private ArdMediaMapper ardMediaMapper; @Resource private IArdServerInfoService ardServerInfoService; @Resource private TyClient tyClient; @Resource private OssServiceContext ossServiceContext; @Resource private ThreadPoolTaskExecutor threadPoolTaskExecutor; // 使用线程池执行异步任务 @Resource private UavControlService uavControlService; @Autowired @Resource private IArdMediaService ardMediaService; @Resource private RedisCache redisCache; @Resource private TyClient tyClient; /** * 查询飞行架次列表 * @@ -80,7 +53,7 @@ /** * 同步所有服务的飞行历史 */ @Scheduled(fixedDelay = 10000) @Scheduled(fixedDelay = 5000) public void syncAllFlightHistory() { List<String> serverIds = ardServerInfoService.getServerIdsList(); for (String serverId : serverIds) { @@ -97,10 +70,18 @@ return maxStartTime != null ? maxStartTime : new Date(0); // 如果没有数据,返回 Unix 纪元时间作为起点 } // 调用接口获取飞行历史数据 private List<BoxHistoryReceiver> getBoxHistory(String serverId, BoxHistoryDTO boxHistoryDTO) { TyHttpCommonReceiver<List<BoxHistoryReceiver>> boxHistory = tyClient.getBoxHistory(serverId, boxHistoryDTO); return boxHistory.getContent(); } // 根据上次同步时间同步飞行历史 public void syncFlightHistory(String serverId, Date startTime) { // 创建查询条件 BoxHistoryDTO,并设置 startTime 作为过滤条件 BoxHistoryDTO boxHistoryDTO = BoxHistoryDTO.builder() .pageIndex(1) .pageSize(20) .startTime(DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, startTime)) .build(); @@ -121,7 +102,11 @@ ArdFlightTask ardFlightTask = ardFlightTaskMapper.getArdFlightTaskById(boxHistoryReceiver.getTaskId()); if (ardFlightTask == null) { // 如果不存在,则插入新记录 log.debug("insert task {}", boxHistoryReceiver.getTaskId()); log.info("insert task {}", boxHistoryReceiver.getTaskId()); Date endTime = null; if (boxHistoryReceiver.getEndTime() != null) { endTime = DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS, boxHistoryReceiver.getEndTime()); } // 创建FlightTask实体对象并填充数据 ArdFlightTask flightTask = ArdFlightTask.builder() .taskId(boxHistoryReceiver.getTaskId()) @@ -134,7 +119,7 @@ .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())) .endTime(endTime) .flightMileage(boxHistoryReceiver.getFlightMileage()) .flightDuration(boxHistoryReceiver.getFlightDuration()) .aveSpeed(boxHistoryReceiver.getAveSpeed()) @@ -152,35 +137,37 @@ // 插入新记录 int rows = ardFlightTaskMapper.insertArdFlightTask(flightTask); if (rows > 0) { // 删除旧的媒体记录,防止重复插入 ardMediaMapper.deleteArdMediaByTaskId(flightTask.getTaskId()); // 插入视频 insertVideoToDatabase(flightTask, boxHistoryReceiver.getVideoPathList()); ardMediaService.insertVideoToDatabase(flightTask, serverId, boxHistoryReceiver.getBoxSn()); // 插入图片 insertPhotoToDatabase(flightTask, serverId, boxHistoryReceiver.getBoxSn()); ardMediaService.insertPhotoToDatabase(flightTask, serverId, boxHistoryReceiver.getBoxSn()); } } else { // 如果存在,则更新记录 log.debug("Updating task {}", boxHistoryReceiver.getTaskId()); log.debug("sync task {}", boxHistoryReceiver.getTaskId()); updateFlightTask(ardFlightTask, boxHistoryReceiver); // 增量更新 updateVideoToDatabase(ardFlightTask, boxHistoryReceiver.getVideoPathList()); // 增量更新图片 updatePhotoToDatabase(ardFlightTask, serverId, boxHistoryReceiver.getBoxSn()); // 同步任务图片 ardMediaService.syncPicToDatabase(ardFlightTask, serverId, boxHistoryReceiver.getBoxSn()); // 同步任务视频 ardMediaService.syncVideoToDatabase(ardFlightTask, serverId, boxHistoryReceiver.getBoxSn()); } } } } private void updateFlightTask(ArdFlightTask local, BoxHistoryReceiver remote) { boolean needUpdate = false; // endTime 更新(重要!!!) if (remote.getEndTime() != null) { Date newEnd = DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS, remote.getEndTime()); if (newEnd != null && (local.getEndTime() == null || !local.getEndTime().equals(newEnd))) { if (local.getEndTime() == null || !local.getEndTime().equals(newEnd)) { local.setEndTime(newEnd); local.setAveSpeed(remote.getAveSpeed()); local.setMaxSpeed(remote.getMaxSpeed()); local.setFlightDuration(remote.getFlightDuration()); local.setFlightMileage(remote.getFlightMileage()); needUpdate = true; } } // photoCount 更新 @@ -199,386 +186,6 @@ if (needUpdate) { ardFlightTaskMapper.updateArdFlightTask(local); } } // 调用接口获取飞行历史数据 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) { //videoPath = "https://192.168.1.3:9001/record/DBM250974065008/8212fd8e89814d66b6c32f48d05f5101.mp4"; 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, 1, 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; } // 需要插入新的视频 List<ArdMedia> existingVideos = ardMediaMapper.selectArdMediaByTaskIdAndType(flightTask.getTaskId(), 1); Integer count = existingVideos.size();//获取本地库视频数量 int size = videoPathList.size();//获取接口返回的视频数量 if (count != null && size > count) { log.info("任务【{}】录像数量不一致,需要更新数据库", flightTask.getTaskId()); // 提取现有的视频路径 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, 1, 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) { // 获取本地数据库中已存在的图片记录(类型为 0 表示图片) List<ArdMedia> existingPhotos = ardMediaMapper.selectArdMediaByTaskIdAndType(flightTask.getTaskId(), 0); int count = existingPhotos.size(); //获取本地库图片数量 // 查询图片 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()); // 需要插入新的视频 // 提取现有的图片路径(根据 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 skipSeconds, int targetWidth) { String ffmpegPath = ""; if (osSelect.isWindows()) { ffmpegPath = "ffmpeg"; } else if (osSelect.isLinux()) { ffmpegPath = "/usr/local/ffmpeg/bin/ffmpeg"; } try { List<String> command = new ArrayList<>(); command.add(ffmpegPath); command.add("-timeout"); command.add("3000000"); command.add("-rw_timeout"); command.add("3000000"); command.add("-ss"); command.add(String.format("00:00:%02d", skipSeconds)); command.add("-i"); command.add(videoFile); command.add("-vframes"); command.add("1"); // 等比缩放 command.add("-vf"); command.add("scale=" + targetWidth + ":-1"); // **确保输出 JPEG** command.add("-f"); command.add("mjpeg"); // 关键点:输出格式固定为 JPEG command.add("-vcodec"); command.add("mjpeg"); // 输出到管道 command.add("pipe:1"); // ⭐ 打印最终命令(可复制) log.info("ffmpeg command: {}", String.join(" ", command)); ProcessBuilder pb = new ProcessBuilder(command); pb.redirectErrorStream(false); Process process = pb.start(); // 读取 FFmpeg 输出(JPEG 数据)到内存 ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (InputStream is = process.getInputStream()) { byte[] buffer = new byte[8192]; int len; while ((len = is.read(buffer)) != -1) { baos.write(buffer, 0, len); } } int exitCode = process.waitFor(); if (exitCode != 0) { throw new RuntimeException("FFmpeg 执行失败,退出码: " + exitCode); } return new ByteArrayInputStream(baos.toByteArray()); } catch (IOException | InterruptedException e) { throw new RuntimeException("FFmpeg 生成缩略图失败", e); } } /** * 将 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; } } ard-work/src/main/java/com/ard/work/service/impl/ArdMediaServiceImpl.java
@@ -1,31 +1,280 @@ package com.ard.work.service.impl; import com.ard.work.domain.entity.ArdMedia; import com.ard.work.mapper.ArdMediaMapper; import com.ard.common.utils.DateUtils; import com.ard.common.utils.StringUtils; import com.ard.common.utils.file.MimeTypeUtils; import com.ard.oss.service.impl.OssServiceContext; import com.ard.work.component.forest.TyClient; import com.ard.work.domain.dto.BoxPhotosDTO; import com.ard.work.domain.dto.BoxVideosDTO; import com.ard.work.domain.entity.ArdFlightTask; import com.ard.work.domain.entity.ArdMediaPic; import com.ard.work.domain.entity.ArdMediaVideo; import com.ard.work.domain.receiver.BoxPhotosReceiver; import com.ard.work.domain.receiver.BoxVideosReceiver; import com.ard.work.domain.receiver.TyHttpCommonReceiver; import com.ard.work.mapper.ArdFlightTaskMapper; import com.ard.work.mapper.ArdMediaPicMapper; import com.ard.work.mapper.ArdMediaVideoMapper; import com.ard.work.service.IArdMediaService; import com.ard.work.util.osSelect; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.List; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; import java.nio.file.Files; import java.nio.file.Path; import java.security.cert.X509Certificate; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * Description: * Author: lsy * Date: 2025年11月18日13:17 **/ @Slf4j @Service public class ArdMediaServiceImpl implements IArdMediaService { @Resource private ArdMediaMapper ardMediaMapper; /** * 查询媒体数据列表 * * @param ardMedia 媒体数据 * @return 媒体数据集合 */ @Resource private TyClient tyClient; @Resource private ArdMediaPicMapper ardMediaPicMapper; @Resource private ArdMediaVideoMapper ardMediaVideoMapper; @Resource private ArdFlightTaskMapper ardFlightTaskMapper; // 调用接口获取架次图片 private List<BoxPhotosReceiver> getBoxPhotos(String serverId, BoxPhotosDTO boxPhotosDTO) { TyHttpCommonReceiver<List<BoxPhotosReceiver>> boxPhotos = tyClient.getBoxPhotos(serverId, boxPhotosDTO); return boxPhotos.getContent(); } // 调用接口获取架次视频 private List<BoxVideosReceiver> getBoxVideos(String serverId, BoxVideosDTO boxVideosDTO) { TyHttpCommonReceiver<List<BoxVideosReceiver>> boxVideos = tyClient.getBoxVideos(serverId, boxVideosDTO); return boxVideos.getContent(); } @Override public List<ArdMedia> selectArdMediaList(ArdMedia ardMedia) { return ardMediaMapper.selectArdMedia(ardMedia); public List<ArdMediaPic> selectArdMediaPicList(ArdMediaPic ardMediaPic) { return ardMediaPicMapper.selectPicList(ardMediaPic); } @Override public List<ArdMediaVideo> selectArdMediaVideoList(ArdMediaVideo ardMediaVideo) { return ardMediaVideoMapper.selectArdMediaVideoList(ardMediaVideo); } @Override public void insertPhotoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) { log.warn("开始获取图片信息"); // 查询图片 BoxPhotosDTO boxPhotosDTO = new BoxPhotosDTO(); boxPhotosDTO.setTaskId(flightTask.getTaskId()); boxPhotosDTO.setBoxSn(boxSn); boxPhotosDTO.setPageIndex(1); boxPhotosDTO.setPageSize(100); List<BoxPhotosReceiver> photoList = getBoxPhotos(serverId, boxPhotosDTO); if (photoList != null && !photoList.isEmpty()) { addPhotos(flightTask, photoList); } } @Override public void insertVideoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) { log.warn("开始获取视频信息"); BoxVideosDTO boxVideosDTO = new BoxVideosDTO(); boxVideosDTO.setTaskId(flightTask.getTaskId()); boxVideosDTO.setBoxSn(boxSn); boxVideosDTO.setPageIndex(1); boxVideosDTO.setPageSize(100); List<BoxVideosReceiver> videoList = getBoxVideos(serverId, boxVideosDTO); if (videoList != null && !videoList.isEmpty()) { addVideos(flightTask, videoList); } } @Override public void syncPicToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) { // 1️⃣ 查第三方图片列表 BoxPhotosDTO dto = BoxPhotosDTO.builder() .taskId(flightTask.getTaskId()) .boxSn(boxSn) .pageIndex(1) .pageSize(100) .build(); TyHttpCommonReceiver<List<BoxPhotosReceiver>> resp = tyClient.getBoxPhotos(serverId, dto); List<BoxPhotosReceiver> photoList = resp.getContent(); if (photoList == null || photoList.isEmpty()) { return; } // 2️⃣ 本地图片转 Map(用“业务唯一键”) List<ArdMediaPic> existingList = ardMediaPicMapper.selectPicByTaskId(flightTask.getTaskId()); Map<Long, ArdMediaPic> localPicMap = existingList.stream() .filter(p -> p.getId() != null) .collect(Collectors.toMap( ArdMediaPic::getId, // ✅ 远端 photoId p -> p, (a, b) -> a )); // 3️⃣ 逐条处理(新增 or 补全) for (BoxPhotosReceiver remote : photoList) { String imgUrl = remote.getImgUrl(); if (StringUtils.isBlank(imgUrl)) { continue; // 第三方字段还没准备好 } ArdMediaPic local = localPicMap.get(remote.getPhotoId()); if (local == null) { // ✅ 新图片 → 插入 addPhotos(flightTask, List.of(remote)); continue; } // ✅ 已存在 → 补字段 boolean needUpdate = false; if (local.getCaptureTime() == null && remote.getTimeStamp() != null) { local.setCaptureTime(DateUtils.parseDate(remote.getTimeStamp())); needUpdate = true; } if (local.getFileSize() == 0 && remote.getFileSize() != 0) { local.setFileSize(remote.getFileSize()); needUpdate = true; } if ("0".equals(local.getFileMd5())&& !"0".equals(remote.getFileMd5())) { local.setFileMd5(remote.getFileMd5()); needUpdate = true; } if (needUpdate) { local.setUpdateTime(new Date()); ardMediaPicMapper.updatePic(local); } } } @Override public void syncVideoToDatabase(ArdFlightTask flightTask, String serverId, String boxSn) { // 1️⃣ 拉取远端视频列表 BoxVideosDTO dto = BoxVideosDTO.builder() .taskId(flightTask.getTaskId()) .boxSn(boxSn) .pageIndex(1) .pageSize(100) .build(); TyHttpCommonReceiver<List<BoxVideosReceiver>> resp = tyClient.getBoxVideos(serverId, dto); List<BoxVideosReceiver> videoList = resp.getContent(); if (videoList == null || videoList.isEmpty()) { return; } // 2️⃣ 本地视频转 Map(videoId 为唯一键) List<ArdMediaVideo> existingVideos = ardMediaVideoMapper.selectArdMediaVideoByTaskId(flightTask.getTaskId()); Map<Long, ArdMediaVideo> localVideoMap = existingVideos.stream() .filter(v -> v.getId() != null) .collect(Collectors.toMap( ArdMediaVideo::getId, // 远端 videoId v -> v, (a, b) -> a )); // 3️⃣ 遍历远端视频,逐条处理 for (BoxVideosReceiver remote : videoList) { ArdMediaVideo local = localVideoMap.get(remote.getId()); if (local == null) { // 新视频 → 立即插入 addVideos(flightTask, List.of(remote)); localVideoMap.put(remote.getId(), new ArdMediaVideo()); // 占位 flightTask.setVideoCount(flightTask.getVideoCount() + 1); ardFlightTaskMapper.updateArdFlightTask(flightTask); continue; } // 已存在 → 补字段 boolean needUpdate = false; if (local.getName() == null && remote.getName() != null) { local.setName(remote.getName()); needUpdate = true; } if (local.getStartTime() == null && remote.getStartTime() != null) { local.setStartTime(DateUtils.parseDate(remote.getStartTime())); needUpdate = true; } if (local.getEndTime() == null && remote.getEndTime() != null) { local.setEndTime(DateUtils.parseDate(remote.getEndTime())); needUpdate = true; } if (local.getSpanSeconds() == null && remote.getSpanSeconds() != null) { local.setSpanSeconds(Integer.parseInt(remote.getSpanSeconds())); needUpdate = true; } if (needUpdate) { local.setUpdateTime(new Date()); ardMediaVideoMapper.updateArdMediaVideo(local); } } } private void addPhotos(ArdFlightTask flightTask, List<BoxPhotosReceiver> photoList) { log.warn("开始添加图片"); for (BoxPhotosReceiver photo : photoList) { ArdMediaPic ardMedia = ArdMediaPic.builder() .id(photo.getPhotoId()) .taskId(flightTask.getTaskId()) .fileSize(photo.getFileSize()) .fileMd5(photo.getFileMd5()) .sourceUrl(photo.getImgUrl()) .longitude(photo.getLng()) .latitude(photo.getLat()) .height(photo.getHeight()) .captureTime(DateUtils.parseDate(photo.getTimeStamp())) .build(); ardMedia.setCreateTime(new Date()); ardMediaPicMapper.insertPic(ardMedia); } } private void addVideos(ArdFlightTask flightTask, List<BoxVideosReceiver> videoList) { for (BoxVideosReceiver video : videoList) { ArdMediaVideo ardMedia = ArdMediaVideo.builder() .id(video.getId()) .name(video.getName()) .taskId(flightTask.getTaskId()) // .fileSize(getFileSizeByUrl(video.getImgUrl())) .sourceUrl(video.getImgUrl()) .startTime(DateUtils.parseDate(video.getStartTime())) .endTime(DateUtils.parseDate(video.getEndTime())) .spanSeconds(Integer.parseInt(video.getSpanSeconds())) .build(); ardMedia.setCreateTime(new Date()); ardMediaVideoMapper.insertArdMediaVideo(ardMedia); } } } ard-work/src/main/java/com/ard/work/service/impl/ArdServerInfoServiceImpl.java
@@ -5,14 +5,11 @@ import com.ard.work.component.forest.TyClient; import com.ard.work.component.netty.TyTcpClientManager; import com.ard.work.domain.dto.TokenDTO; import com.ard.work.domain.entity.ArdDroneInfo; import com.ard.work.domain.entity.ArdServerInfo; import com.ard.work.domain.receiver.AccessTokenReceiver; import com.ard.work.domain.receiver.TyHttpCommonReceiver; import com.ard.work.mapper.ArdDroneInfoMapper; import com.ard.work.mapper.ArdServerInfoMapper; import com.ard.work.service.IArdServerInfoService; import com.dtflys.forest.exceptions.ForestRuntimeException; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils; @@ -40,9 +37,6 @@ @Resource private TyTcpClientManager tyTcpClientManager; @Resource private ArdDroneInfoMapper ArdDroneInfoMapper; /** * 查询服务器管理 * @@ -63,24 +57,6 @@ @Override @DataScope(deptAlias = "d", userAlias = "u") public List<ArdServerInfo> selectArdServerInfoList(ArdServerInfo ardServerInfo) { return ardServerInfoMapper.selectArdServerInfoList(ardServerInfo); } @Override @DataScope(deptAlias = "d", userAlias = "u") public List<ArdServerInfo> selectArdServerInfoListNo(ArdServerInfo ardServerInfo) { // List<ArdDroneInfo> ardDroneInfos = ArdDroneInfoMapper.selectArdDroneList(new ArdDroneInfo()); // for (ArdDroneInfo ardDroneInfo : ardDroneInfos) { // ardDroneInfo.getBoxSn(); // } // List<ArdServerInfo> servers = ardServerInfoMapper.selectArdServerInfoList(ardServerInfo); // for (ArdServerInfo server : servers) { // for (ArdDroneInfo ardDroneInfo : server.getArdDroneInfoList()) { // ardDroneInfo.getBoxSn(); // } // } return ardServerInfoMapper.selectArdServerInfoList(ardServerInfo); } ard-work/src/main/java/com/ard/work/service/impl/TyServiceImpl.java
@@ -606,6 +606,10 @@ if (ardDroneInfo == null) { throw new ControlException("未找到对应的飞行器信息,boxSn:{}" + boxSn); } //打点之前先停止打点 FlyPointControlCancelCmd flyPointControlCancelCmd = new FlyPointControlCancelCmd(FlyPointControlCancelDTO.builder().boxSn(boxSn).build()); this.sendMessage(ardDroneInfo.getServerId(), flyPointControlCancelCmd); FlyPointControlCmd flyPointControlCmd = new FlyPointControlCmd(flyPointControlDTO); this.sendMessage(ardDroneInfo.getServerId(), flyPointControlCmd); return true; @@ -974,7 +978,6 @@ // 处理无人机遥测消息 UavTelemetryDataCmd uavTelemetryDataCmd = (UavTelemetryDataCmd) tyCommand; TelemetryData telemetryData = uavTelemetryDataCmd.getTelemetryData(); telemetryData.setTimestamp(System.currentTimeMillis());//由于六合提供的状态时间戳不准,先以接收状态作为时间戳 UavTelemetryDataCache.put(telemetryData); //logPrint("无人机遥测", uavTelemetryDataCmd.getTelemetryData().getBoxSn()); //WebSocketUtil.sendMessageAll(uavTelemetryDataCmd.getTelemetryData()); ard-work/src/main/resources/mapper/ArdFlightTaskMapper.xml
@@ -146,13 +146,13 @@ WHERE task_id = #{taskId} </select> <!-- 获取某个时间戳后的任务 --> <select id="getTasksAfter" resultType="ArdFlightTask"> SELECT * FROM ard_flight_task WHERE end_time > #{lastSyncTime} </select> <select id="findMaxStartTimeByServerId" resultType="java.util.Date"> SELECT MAX(start_time) FROM ard_flight_task ard-work/src/main/resources/mapper/ArdMediaMapper.xml
File was deleted ard-work/src/main/resources/mapper/ArdMediaPicMapper.xml
New file @@ -0,0 +1,115 @@ <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ard.work.mapper.ArdMediaPicMapper"> <resultMap id="ArdMediaPicResult" type="ArdMediaPic"> <result property="id" column="id"/> <result property="fileSize" column="file_size"/> <result property="fileMd5" column="file_md5"/> <result property="mediaUrl" column="media_url"/> <result property="sourceUrl" column="source_url"/> <result property="longitude" column="longitude"/> <result property="latitude" column="latitude"/> <result property="height" column="height"/> <result property="taskId" column="task_id"/> <result property="captureTime" column="capture_time"/> <result property="createTime" column="create_time"/> <result property="updateTime" column="update_time"/> <result property="syncStatus" column="sync_status"/> </resultMap> <!-- 查询图片列表 --> <select id="selectPicList" resultMap="ArdMediaPicResult"> SELECT * FROM ard_media_pic <where> <if test="taskId != null">AND task_id = #{taskId}</if> <if test="syncStatus != null">AND sync_status = #{syncStatus}</if> </where> ORDER BY capture_time DESC </select> <!-- 查询待处理的图片 --> <select id="selectPendingPics" resultMap="ArdMediaPicResult"> SELECT * FROM ard_media_pic WHERE sync_status = 0 ORDER BY id LIMIT #{limit} </select> <!-- 根据任务ID查询图片 --> <select id="selectPicByTaskId" resultMap="ArdMediaPicResult"> SELECT * FROM ard_media_pic WHERE task_id = #{taskId} ORDER BY capture_time DESC </select> <!-- 插入图片 --> <insert id="insertPic" parameterType="ArdMediaPic"> INSERT INTO ard_media_pic ( id, task_id, file_size, file_md5, media_url, source_url, longitude, latitude, height, capture_time, create_time,sync_status ) VALUES ( #{id}, #{taskId}, #{fileSize}, #{fileMd5}, #{mediaUrl}, #{sourceUrl}, #{longitude}, #{latitude}, #{height}, #{captureTime}, #{createTime},0 ) </insert> <!-- 更新图片(按需补全字段) --> <update id="updatePic" parameterType="ArdMediaPic"> UPDATE ard_media_pic <set> <if test="mediaUrl != null"> media_url = #{mediaUrl}, </if> <if test="fileSize != null"> file_size = #{fileSize}, </if> <if test="fileMd5 != null"> file_md5 = #{fileMd5}, </if> <if test="captureTime != null"> capture_time = #{captureTime}, </if> <if test="longitude != null"> longitude = #{longitude}, </if> <if test="latitude != null"> latitude = #{latitude}, </if> <if test="height != null"> height = #{height}, </if> <if test="syncStatus != null"> sync_status = #{syncStatus}, </if> update_time = #{updateTime} </set> WHERE id = #{id} </update> <!-- 标记为处理中 --> <update id="markProcessing"> UPDATE ard_media_pic SET sync_status = 1 WHERE id = #{id} AND sync_status = 0 </update> <!-- 更新同步状态 --> <update id="updateSyncStatus"> UPDATE ard_media_pic SET sync_status = #{status},update_time = NOW() WHERE id = #{id} </update> <delete id="deletePicById"> DELETE FROM ard_media_pic WHERE id = #{id} </delete> <delete id="deletePicByTaskId"> DELETE FROM ard_media_pic WHERE task_id = #{taskId} </delete> </mapper> ard-work/src/main/resources/mapper/ArdMediaVideoMapper.xml
New file @@ -0,0 +1,108 @@ <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.ard.work.mapper.ArdMediaVideoMapper"> <!-- 结果映射 --> <resultMap id="ArdMediaVideoResult" type="ArdMediaVideo"> <id property="id" column="id"/> <result property="name" column="name"/> <result property="fileSize" column="file_size"/> <result property="mediaUrl" column="media_url"/> <result property="sourceUrl" column="source_url"/> <result property="thumbnailUrl" column="thumbnail_url"/> <result property="startTime" column="start_time"/> <result property="endTime" column="end_time"/> <result property="spanSeconds" column="span_seconds"/> <result property="taskId" column="task_id"/> <result property="createTime" column="create_time"/> <result property="updateTime" column="update_time"/> <result property="syncStatus" column="sync_status"/> </resultMap> <!-- 查询视频列表 --> <select id="selectArdMediaVideoList" resultMap="ArdMediaVideoResult"> SELECT id, name, file_size, media_url, source_url, thumbnail_url, start_time, end_time, span_seconds, task_id, create_time, update_time,sync_status FROM ard_media_video <where> <if test="taskId != null">AND task_id = #{taskId}</if> <if test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</if> <if test="startTime != null">AND start_time >= #{startTime}</if> <if test="endTime != null">AND end_time <= #{endTime}</if> <if test="syncStatus != null">AND sync_status = #{syncStatus}</if> </where> ORDER BY start_time DESC </select> <!-- 查询待处理的视频 --> <select id="selectPendingVideos" resultMap="ArdMediaVideoResult"> SELECT * FROM ard_media_video WHERE sync_status = 0 ORDER BY id LIMIT #{limit} </select> <!-- 根据任务ID查询 --> <select id="selectArdMediaVideoByTaskId" resultMap="ArdMediaVideoResult"> SELECT * FROM ard_media_video WHERE task_id = #{taskId} ORDER BY start_time DESC </select> <!-- 新增视频 --> <insert id="insertArdMediaVideo" parameterType="com.ard.work.domain.entity.ArdMediaVideo"> INSERT INTO ard_media_video (id, name, file_size, media_url, source_url, thumbnail_url, start_time, end_time, span_seconds, task_id, create_time, update_time, sync_status) VALUES (#{id}, #{name}, #{fileSize}, #{mediaUrl}, #{sourceUrl}, #{thumbnailUrl}, #{startTime}, #{endTime}, #{spanSeconds}, #{taskId}, #{createTime}, #{updateTime}, 0) </insert> <!-- 更新视频 --> <update id="updateArdMediaVideo"> UPDATE ard_media_video <set> <if test="name != null">name = #{name},</if> <if test="fileSize != null">file_size = #{fileSize},</if> <if test="mediaUrl != null">media_url = #{mediaUrl},</if> <if test="sourceUrl != null">source_url = #{sourceUrl},</if> <if test="thumbnailUrl != null">thumbnail_url = #{thumbnailUrl},</if> <if test="startTime != null">start_time = #{startTime},</if> <if test="endTime != null">end_time = #{endTime},</if> <if test="spanSeconds != null">span_seconds = #{spanSeconds},</if> <if test="updateTime != null">update_time = #{updateTime},</if> <if test="syncStatus != null">sync_status = #{syncStatus},</if> </set> WHERE id = #{id} </update> <!-- 标记为处理中 --> <update id="markProcessing"> UPDATE ard_media_video SET sync_status = 1 WHERE id = #{id} AND sync_status = 0 </update> <!-- 更新同步状态 --> <update id="updateSyncStatus"> UPDATE ard_media_video SET sync_status = #{status},update_time = NOW() WHERE id = #{id} </update> <!-- 删除 --> <delete id="deleteArdMediaVideoById"> DELETE FROM ard_media_video WHERE id = #{id} </delete> <delete id="deleteArdMediaVideoByTaskId"> DELETE FROM ard_media_video WHERE task_id = #{taskId} </delete> </mapper>