zhaowenyu
2026-01-06 4e8a1c02b45f72f6377603778e8c8746ed82f374
Merge remote-tracking branch 'origin/dev' into dev
9 files added
48 files modified
3 files deleted
2925 ■■■■■ changed files
ard-admin/src/main/java/com/ard/web/controller/data/ArdMapController.java 7 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/java/com/ard/web/controller/data/ArdModelController.java 7 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/java/com/ard/web/controller/data/ArdNoFlyZoneController.java 7 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/java/com/ard/web/controller/data/ArdTileController.java 7 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/java/com/ard/web/controller/work/ArdDroneInfoController.java 8 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/java/com/ard/web/controller/work/ArdFlightTaskController.java 78 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/java/com/ard/web/controller/work/ArdServerInfoController.java 12 ●●●●● patch | view | raw | blame | history
ard-admin/src/main/java/com/ard/web/controller/work/UavBaseController.java 30 ●●●●● patch | view | raw | blame | history
ard-device/src/main/java/com/ard/device/camera/domain/ArdCamera.java 2 ●●●●● patch | view | raw | blame | history
ard-device/src/main/java/com/ard/device/camera/service/impl/ArdCameraServiceImpl.java 86 ●●●●● patch | view | raw | blame | history
ard-device/src/main/java/com/ard/device/channel/service/impl/ArdChannelServiceImpl.java 107 ●●●●● patch | view | raw | blame | history
ard-device/src/main/java/com/ard/device/component/sdk/common/GlobalVariable.java 3 ●●●●● patch | view | raw | blame | history
ard-device/src/main/java/com/ard/device/component/sdk/dh/service/impl/DaHuaSDK.java 39 ●●●●● patch | view | raw | blame | history
ard-device/src/main/java/com/ard/device/component/sdk/hik/service/impl/HikVisionSDK.java 40 ●●●●● patch | view | raw | blame | history
ard-device/src/main/java/com/ard/device/component/sdk/service/CameraSDK.java 6 ●●●●● patch | view | raw | blame | history
ard-device/src/main/java/com/ard/device/component/sdk/service/CameraSDKService.java 28 ●●●●● patch | view | raw | blame | history
ard-framework/src/main/java/com/ard/framework/config/ThreadPoolConfig.java 28 ●●●●● patch | view | raw | blame | history
ard-quartz/pom.xml 8 ●●●●● patch | view | raw | blame | history
ard-quartz/src/main/java/com/ard/quartz/task/DeviceTask.java 258 ●●●●● patch | view | raw | blame | history
ard-quartz/src/main/java/com/ard/quartz/task/MediaTask.java 353 ●●●●● patch | view | raw | blame | history
ard-route/src/main/java/com/ard/route/service/impl/ArdRouteInfoServiceImpl.java 35 ●●●●● patch | view | raw | blame | history
ard-route/src/main/java/com/ard/route/util/RouteFileUtils.java 4 ●●●●● patch | view | raw | blame | history
ard-sample/src/main/java/com/ard/sample/manage/service/impl/LiveStreamServiceImpl.java 1 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/map/mapper/ArdMapMapper.java 10 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/map/service/IArdMapService.java 9 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/map/service/impl/ArdMapServiceImpl.java 21 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/model/mapper/ArdModelMapper.java 10 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/model/service/IArdModelService.java 9 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/model/service/impl/ArdModelServiceImpl.java 21 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/noflyzone/mapper/ArdNoFlyZoneMapper.java 9 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/noflyzone/service/IArdNoFlyZoneService.java 8 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/noflyzone/service/impl/ArdNoFlyZoneServiceImpl.java 41 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/tile/mapper/ArdTileMapper.java 10 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/tile/service/IArdTileService.java 9 ●●●●● patch | view | raw | blame | history
ard-system/src/main/java/com/ard/data/tile/service/impl/ArdTileServiceImpl.java 21 ●●●●● patch | view | raw | blame | history
ard-system/src/main/resources/mapper/data/ArdMapMapper.xml 5 ●●●●● patch | view | raw | blame | history
ard-system/src/main/resources/mapper/data/ArdModelMapper.xml 5 ●●●●● patch | view | raw | blame | history
ard-system/src/main/resources/mapper/data/ArdNoFlyZoneMapper.xml 5 ●●●●● patch | view | raw | blame | history
ard-system/src/main/resources/mapper/data/ArdTileMapper.xml 5 ●●●●● patch | view | raw | blame | history
ard-vtdu/src/main/java/com/ard/vtdu/service/impl/ArdVtduServiceImpl.java 8 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/domain/entity/ArdMedia.java 30 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaPic.java 28 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/domain/entity/ArdMediaVideo.java 30 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/mapper/ArdMediaMapper.java 40 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/mapper/ArdMediaPicMapper.java 60 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/mapper/ArdMediaVideoMapper.java 59 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/IArdDroneInfoService.java 2 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/IArdMediaService.java 20 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/IArdServerInfoService.java 2 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/ITyService.java 2 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/ArdDroneInfoServiceImpl.java 78 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskPartServiceImpl.java 14 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskServiceImpl.java 469 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/ArdMediaServiceImpl.java 275 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/ArdServerInfoServiceImpl.java 24 ●●●●● patch | view | raw | blame | history
ard-work/src/main/java/com/ard/work/service/impl/TyServiceImpl.java 5 ●●●●● patch | view | raw | blame | history
ard-work/src/main/resources/mapper/ArdFlightTaskMapper.xml 2 ●●●●● patch | view | raw | blame | history
ard-work/src/main/resources/mapper/ArdMediaMapper.xml 202 ●●●●● patch | view | raw | blame | history
ard-work/src/main/resources/mapper/ArdMediaPicMapper.xml 115 ●●●●● patch | view | raw | blame | history
ard-work/src/main/resources/mapper/ArdMediaVideoMapper.xml 108 ●●●●● patch | view | raw | blame | history
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 &gt;= #{startTime}</if>
            <if test="endTime != null">AND end_time &lt;= #{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>