Просмотр исходного кода

Merge branch 'fyc-issue' of uskycloud/usky-modules into server-165

fuyuchuan 1 неделя назад
Родитель
Сommit
e64cac7cb3
80 измененных файлов с 3991 добавлено и 38 удалено
  1. 3 1
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseDataTransferService.java
  2. 109 10
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/CdiDeliveryLogServiceImpl.java
  3. 93 15
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/IotDataTransferService.java
  4. 12 1
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceDataQuery.java
  5. 49 0
      service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/GeneratorMonitoringVO.java
  6. 6 0
      service-issue/service-issue-biz/pom.xml
  7. 8 7
      service-issue/service-issue-biz/src/main/java/com/usky/issue/IssueApplication.java
  8. 163 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/api/CloudIntegrationApiController.java
  9. 68 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudConfigController.java
  10. 50 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudSyncController.java
  11. 49 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudTriggerEventController.java
  12. 36 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/CloudAuditEntity.java
  13. 44 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueCloudConfig.java
  14. 30 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueOperationLog.java
  15. 33 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncCursor.java
  16. 38 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncData.java
  17. 32 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncDetail.java
  18. 34 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncDownQueue.java
  19. 40 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncStatus.java
  20. 46 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncTask.java
  21. 33 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueTriggerEvent.java
  22. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueCloudConfigMapper.java
  23. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueOperationLogMapper.java
  24. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncCursorMapper.java
  25. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncDataMapper.java
  26. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncDetailMapper.java
  27. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncDownQueueMapper.java
  28. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncStatusMapper.java
  29. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncTaskMapper.java
  30. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueTriggerEventMapper.java
  31. 16 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudAuthService.java
  32. 35 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudConfigService.java
  33. 12 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudOperationLogService.java
  34. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncReceiveService.java
  35. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncService.java
  36. 16 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncTaskRunner.java
  37. 20 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudTriggerEventService.java
  38. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalPullService.java
  39. 19 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalPushService.java
  40. 19 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalSyncAgent.java
  41. 260 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/client/CloudPlatformClient.java
  42. 30 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/config/CloudSyncExecutorConfig.java
  43. 47 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/constant/CloudIntegrationConstants.java
  44. 31 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncDirection.java
  45. 40 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncTypeEnum.java
  46. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/TaskStatusEnum.java
  47. 26 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/TriggerEventCodeEnum.java
  48. 21 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/exception/CloudIntegrationErrorCode.java
  49. 91 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudAuthServiceImpl.java
  50. 346 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudConfigServiceImpl.java
  51. 42 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudOperationLogServiceImpl.java
  52. 162 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncReceiveServiceImpl.java
  53. 190 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncServiceImpl.java
  54. 148 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncTaskRunnerImpl.java
  55. 93 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudTriggerEventServiceImpl.java
  56. 185 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalPullServiceImpl.java
  57. 176 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalPushServiceImpl.java
  58. 81 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalSyncAgentImpl.java
  59. 0 4
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/SpRtu2017ServiceImpl.java
  60. 33 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudDataChangeListener.java
  61. 16 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudOrgChangedEvent.java
  62. 16 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudUserChangedEvent.java
  63. 52 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/support/CloudEntitySupport.java
  64. 85 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/AesGcmCipher.java
  65. 23 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/CredentialMaskUtil.java
  66. 31 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/HmacSignUtil.java
  67. 28 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConfigResponse.java
  68. 28 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConfigSaveRequest.java
  69. 24 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConnectionTestRequest.java
  70. 26 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConnectionTestResponse.java
  71. 81 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConnectionTestResult.java
  72. 23 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudDisableResponse.java
  73. 30 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncPacket.java
  74. 24 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncResponse.java
  75. 34 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncResult.java
  76. 23 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncStatusResponse.java
  77. 28 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncTaskResponse.java
  78. 20 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/TriggerEventItemRequest.java
  79. 18 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/TriggerEventResponse.java
  80. 21 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/TriggerEventSaveRequest.java

+ 3 - 1
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/BaseDataTransferService.java

@@ -171,7 +171,7 @@ public class BaseDataTransferService {
                 vo.setPublishTime(getCurrentTime());
             }
 
-            String imagePath = "D://BSP0-0103.jpg";
+            String imagePath = "D://142.jpg";
             // 将图片文件读取为字节数组
             byte[] imageBytes = Files.readAllBytes(Paths.get(imagePath));
 
@@ -264,6 +264,8 @@ public class BaseDataTransferService {
             // userIdToName.put(713, 36);
             userIdToName.put(714, 37);
             userIdToName.put(716, 26);
+            // 柴油发电机蓄电池监测传感器
+            userIdToName.put(720, 12);
 
             HashMap<String, Object> map = new HashMap<>();
             map.put("dataPacketID", generateDataPacketID());

+ 109 - 10
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/CdiDeliveryLogServiceImpl.java

@@ -36,6 +36,7 @@ import org.springframework.beans.factory.annotation.Value;
 import javax.annotation.PostConstruct;
 import javax.imageio.ImageIO;
 import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
@@ -636,19 +637,70 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
         String time = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");
 
         List<FloorPlaneVO> result = new ArrayList<>(filteredPlaneList.size());
+        int skipCount = 0;
+
         for (BaseBuildPlane buildPlane : filteredPlaneList) {
-            String planeViewUrl = buildPlane.getPlaneViewUrl();
-            FloorPlaneVO vo = new FloorPlaneVO();
-            checkFileSize(vo, planeViewUrl);
-            fillImageInfo(vo, planeViewUrl);
+            try {
+                String planeViewUrl = buildPlane.getPlaneViewUrl();
 
-            vo.setDataPacketID(generateDataPacketID());
-            vo.setEngineeringID(engineeringId);
-            vo.setFloor(buildPlane.getFloor());
-            vo.setFloorFileID(Long.valueOf(buildPlane.getId()));
-            vo.setPublishTime(time);
+                // ✅ 修复1: 增加详细的处理日志
+                log.info("正在处理楼层平面图:ID={}, Floor={}, URL={}",
+                        buildPlane.getId(), buildPlane.getFloor(), planeViewUrl);
 
-            result.add(vo);
+                FloorPlaneVO vo = new FloorPlaneVO();
+
+                // ✅ 修复2: 单条记录异常不中断整个批次
+                if (StrUtil.isBlank(planeViewUrl)) {
+                    log.warn("楼层平面图URL为空,跳过该记录:ID={}, Floor={}", buildPlane.getId(), buildPlane.getFloor());
+                    skipCount++;
+                    continue;
+                }
+
+                // 读取文件并校验大小
+                checkFileSize(vo, planeViewUrl);
+
+                // ✅ 修复3: 复用已读取的文件数据来获取图片尺寸(避免重复I/O)
+                fillImageInfoWithBytes(vo, planeViewUrl, vo.getFloorFile());
+
+                // 设置基础字段
+                vo.setDataPacketID(generateDataPacketID());
+                vo.setEngineeringID(engineeringId);
+                vo.setFloor(convertFloor(buildPlane.getFloor()));
+                vo.setFloorFileID(Long.valueOf(buildPlane.getId()));
+                vo.setPublishTime(time);
+
+                // ✅ 修复4: 校验关键字段完整性
+                if (vo.getFloorFile() == null || vo.getFloorFile().length == 0) {
+                    log.warn("楼层平面图文件为空,跳过该记录:ID={}, Floor={}", buildPlane.getId(), buildPlane.getFloor());
+                    skipCount++;
+                    continue;
+                }
+
+                // ✅ 修复5: 记录成功处理的详细信息
+                log.info("楼层平面图处理成功:FileID={}, Floor={}, FileName={}, FileSize={}bytes, Pix={}x{}",
+                        vo.getFloorFileID(),
+                        vo.getFloor(),
+                        vo.getFloorFileName(),
+                        vo.getFloorFile().length,
+                        vo.getFilePixWidth(),
+                        vo.getFilePixHeight());
+
+                result.add(vo);
+
+            } catch (BusinessException e) {
+                log.error("楼层平面图业务校验失败,跳过该记录:ID={}, Floor={}, 错误={}",
+                        buildPlane.getId(), buildPlane.getFloor(), e.getMessage());
+                skipCount++;
+            } catch (Exception e) {
+                log.error("楼层平面图处理异常,跳过该记录:ID={}, Floor={}",
+                        buildPlane.getId(), buildPlane.getFloor(), e);
+                skipCount++;
+            }
+        }
+
+        if (skipCount > 0) {
+            log.warn("楼层平面图处理完成:成功{}条,跳过{}条(文件不存在/超限/读取失败等)",
+                    result.size(), skipCount);
         }
 
         return result;
@@ -720,6 +772,21 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
         }
     }
 
+    private String convertFloor(String floor) {
+        if (floor == null || floor.trim().isEmpty()) {
+            return floor;
+        }
+        try {
+            int floorNum = Integer.parseInt(floor.trim());
+            if (floorNum < 0) {
+                return "B" + Math.abs(floorNum);
+            }
+        } catch (NumberFormatException e) {
+            log.warn("楼层格式转换失败,原始值: {}", floor);
+        }
+        return floor;
+    }
+
     private String encodeUrl(String url) throws UnsupportedEncodingException {
         if (StrUtil.isBlank(url)) {
             return url;
@@ -753,6 +820,38 @@ public class CdiDeliveryLogServiceImpl extends AbstractCrudService<CdiDeliveryLo
         return protocol + hostAndPort + encodedPath.toString();
     }
 
+    /**
+     * 填充图片信息(使用已读取的字节数组,避免重复I/O)
+     * @param vo 楼层平面图VO
+     * @param imageUrl 图片URL或本地路径(用于提取文件名和后缀)
+     * @param imageBytes 已读取的图片字节数组
+     */
+    private void fillImageInfoWithBytes(FloorPlaneVO vo, String imageUrl, byte[] imageBytes) {
+        if (StrUtil.isBlank(imageUrl)) {
+            return;
+        }
+
+        // 从URL/路径中提取文件名和后缀
+        String fileName = FileUtil.getName(imageUrl);
+        vo.setFloorFileName(FileUtil.mainName(fileName));
+        vo.setFloorFileSuffix(FileUtil.extName(fileName));
+
+        // ✅ 优化:直接从已读取的byte[]获取尺寸,避免重复读取文件
+        try {
+            if (imageBytes != null && imageBytes.length > 0) {
+                ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes);
+                BufferedImage image = ImageIO.read(bais);
+
+                if (image != null) {
+                    vo.setFilePixWidth(image.getWidth());
+                    vo.setFilePixHeight(image.getHeight());
+                }
+            }
+        } catch (IOException e) {
+            log.error("从字节数组获取图片尺寸失败: {}", imageUrl, e);
+        }
+    }
+
     private void fillImageInfo(FloorPlaneVO vo, String imageUrl) {
         if (StrUtil.isBlank(imageUrl)) {
             return;

+ 93 - 15
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/impl/IotDataTransferService.java

@@ -1091,7 +1091,7 @@ public class IotDataTransferService {
 
         try {
 
-            log.info("开始推送集水井水位数据,设备类型:{},设备数量:{},获取到的数据条数:{}",
+            log.info("开始推送集水井水位数据,设备类型:{},设备数量:{},获取到的数据条数:{}",
                     deviceType, totalDevices, deviceData.size());
 
             if (deviceData.isEmpty()) {
@@ -1154,6 +1154,92 @@ public class IotDataTransferService {
         }
     }
 
+    /**
+     * 发送发电机蓄电池监测数据(720)
+     *
+     * @return 推送结果,包含成功数和失败数
+     **/
+    public Map<String, Integer> sendGeneratorMonitoring(IotDataTransferVO transferVO) {
+        Map<String, Integer> result = new HashMap<>();
+        result.put("successCount", 0);
+        result.put("failureCount", 0);
+
+        if (!validateMqttGateway(transferVO.getUsername())) {
+            return result;
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        long startTime = System.currentTimeMillis();
+        long endTime;
+
+        List<JSONObject> deviceData = deviceDataQuery.getDeviceData(transferVO);
+        Integer deviceType = transferVO.getDeviceType();
+        Integer totalDevices = transferVO.getDevices().size();
+
+        try {
+
+            log.info("开始推送【战时柴油发电机每日启动时长】数据,设备类型:{},设备数量:{},获取到的数据条数:{}",
+                    deviceType, totalDevices, deviceData.size());
+
+            if (deviceData.isEmpty()) {
+                log.warn("没有获取到战时柴油发电机每日启动时长数据!设备类型:{}", deviceType);
+                result.put("failureCount", totalDevices);
+                return result;
+            }
+
+            Long engineeringId = transferVO.getEngineeringId();
+            for (JSONObject deviceDataItem : deviceData) {
+                LocalDateTime dataEndTime = parseDataTime(deviceDataItem);
+                if (dataEndTime == null) {
+                    result.put("failureCount", result.get("failureCount") + 1);
+                    continue;
+                }
+                dataEndTime = addTimeOffset(dataEndTime);
+
+                Integer deviceId = deviceDataItem.getIntValue("device_id");
+                Integer startDuration = deviceDataItem.get("startDuration") == null ? 0 : deviceDataItem.getIntValue("startDuration");
+
+                GeneratorMonitoringVO vo = new GeneratorMonitoringVO();
+                vo.setDataPacketID(generateDataPacketID());
+                vo.setSensorID(deviceId);
+                vo.setEngineeringID(engineeringId);
+                vo.setPublishTime(getCurrentTime());
+                vo.setDataEndTime(dataEndTime);
+                vo.setSensorDate(dataEndTime.toLocalDate());
+                vo.setStartDuration(startDuration);
+                if (deviceDataItem.containsKey("batteryVoltage")) {
+                    vo.setBatteryVoltage(deviceDataItem.getDouble("batteryVoltage"));
+                }
+
+                try {
+                    log.info("【战时柴油发电机每日启动时长】开始推送,设备ID:{},数据:{}", deviceId, JSON.toJSONString(vo));
+                    sendMqttMessage(MqttTopics.IotInfo.ALTERNATOR_STARTUP_TIME.getTopic(), vo, MqttTopics.IotInfo.ALTERNATOR_STARTUP_TIME.getDesc(), transferVO.getUsername());
+                    result.put("successCount", result.get("successCount") + 1);
+                } catch (Exception e) {
+                    log.warn("设备{}的战时柴油发电机每日启动时长数据推送失败:{}", deviceId, e.getMessage());
+                    result.put("failureCount", result.get("failureCount") + 1);
+                }
+            }
+
+            log.info("战时柴油发电机每日启动时长数据推送完成,设备类型:{},成功:{},失败:{}",
+                    deviceType, result.get("successCount"), result.get("failureCount"));
+
+            endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, totalDevices,
+                    result.get("successCount"), result.get("failureCount"),
+                    totalDevices - result.get("successCount") - result.get("failureCount"), 1, SecurityUtils.getUsername());
+            return result;
+        } catch (Exception e) {
+            log.error("战时柴油发电机每日启动时长数据推送发生异常", e);
+            result.put("failureCount", transferVO.getDevices().size());
+            endTime = System.currentTimeMillis();
+            saveLog(transferVO, now, startTime, endTime, totalDevices,
+                    result.get("successCount"), result.get("failureCount"),
+                    totalDevices - result.get("successCount") - result.get("failureCount"), 0, SecurityUtils.getUsername());
+            return result;
+        }
+    }
+
     /**
      * 同步设备数据
      * @param tenantId 租户ID
@@ -1247,6 +1333,7 @@ public class IotDataTransferService {
                     }
                     result = sendPersonPresence(transferVO);
                     break;
+                case 705:
                 case 704:
                     if (tenantId == 1205) {
                         // 设置默认值,避免空指针
@@ -1271,6 +1358,9 @@ public class IotDataTransferService {
                 case 719:
                     result = sendPersonCount(transferVO);
                     break;
+                case 720:
+                    result = sendGeneratorMonitoring(transferVO);
+                    break;
                 default:
                     log.debug("不支持的设备类型:{}", deviceType);
                     continue;
@@ -1428,8 +1518,7 @@ public class IotDataTransferService {
      * 序列化楼层平面图VO(将 floorFile byte[] 转为 Base64 字符串)
      */
     private String serializeFloorPlaneVO(com.usky.cdi.service.vo.info.FloorPlaneVO vo) {
-        com.alibaba.fastjson.JSONObject jsonObject = new com.alibaba.fastjson.JSONObject();
-
+        Map<String, Object> jsonObject = new HashMap<>();
         jsonObject.put("dataPacketID", vo.getDataPacketID());
         jsonObject.put("engineeringID", vo.getEngineeringID());
         jsonObject.put("floor", vo.getFloor());
@@ -1439,18 +1528,7 @@ public class IotDataTransferService {
         jsonObject.put("filePixWidth", vo.getFilePixWidth());
         jsonObject.put("filePixHeight", vo.getFilePixHeight());
         jsonObject.put("publishTime", vo.getPublishTime());
-
-        // 关键:将 byte[] 转为 Base64 字符串
-        if (vo.getFloorFile() != null) {
-            String base64File = java.util.Base64.getEncoder().encodeToString(vo.getFloorFile());
-            jsonObject.put("floorFile", base64File);
-            log.info("平面图文件转换Base64成功,FileID: {}, 原始大小: {} bytes, Base64长度: {}",
-                    vo.getFloorFileID(), vo.getFloorFile().length, base64File.length());
-        } else {
-            jsonObject.put("floorFile", "");
-            log.warn("平面图文件为空,FileID: {}", vo.getFloorFileID());
-        }
-
+        jsonObject.put("floorFile", vo.getFloorFile());
         Gson gson = new Gson();
         return gson.toJson(jsonObject);
     }

+ 12 - 1
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/util/DeviceDataQuery.java

@@ -501,7 +501,12 @@ public class DeviceDataQuery {
                         double value = FixedWaterLevelGenerator.getSensorValue(deviceId1);
                         simData.put("sensorValue", formatNumber(value, FORMAT_1_2));
                         break;
-
+                    // case 719:
+                    //     break;
+                    case 720:
+                        simData.put("startDuration", 0);
+                        simData.put("batteryVoltage", randomDouble261_266());
+                        break;
                     default:
                         log.warn("未知设备类型:{},无法生成模拟数据", deviceType);
                         continue;
@@ -515,6 +520,12 @@ public class DeviceDataQuery {
         return simulationList;
     }
 
+    private double randomDouble261_266() {
+        Random random = new Random();
+        int intVal = 2610 + random.nextInt(51);
+        return intVal / 100.0;
+    }
+
     private void addNoiseToNumericFields(JSONObject data, Integer deviceType) {
         Map<String, Double> noiseConfig = getNoiseConfigByDeviceType(deviceType);
 

+ 49 - 0
service-cdi/service-cdi-biz/src/main/java/com/usky/cdi/service/vo/info/GeneratorMonitoringVO.java

@@ -0,0 +1,49 @@
+package com.usky.cdi.service.vo.info;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import com.usky.cdi.service.vo.base.BaseEnvMonitorPushVO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDate;
+
+/**
+ * 发电机蓄电池监测数据
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/6/18
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class GeneratorMonitoringVO extends BaseEnvMonitorPushVO {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 监测日期(必填)
+     * 类型:Date(yyyy-MM-dd)
+     */
+    @JSONField(format = "yyyy-MM-dd")
+    private LocalDate sensorDate;
+
+    /**
+     * 启动时长(必填)
+     * 类型:Int,长度4,单位分钟
+     */
+    private Integer startDuration;
+
+    /**
+     * 蓄电池电压(非必填)
+     * 类型:Float(2,2),单位V
+     */
+    private Double batteryVoltage;
+
+    @Override
+    public Number getSensorValue() {
+        return null;
+    }
+
+    @Override
+    protected void validateSensorValue() {
+
+    }
+}

+ 6 - 0
service-issue/service-issue-biz/pom.xml

@@ -66,6 +66,12 @@
             <artifactId>ruoyi-common-swagger</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 8 - 7
service-issue/service-issue-biz/src/main/java/com/usky/issue/RuoYiSystemApplication.java → service-issue/service-issue-biz/src/main/java/com/usky/issue/IssueApplication.java

@@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.scheduling.annotation.EnableScheduling;
 import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.context.annotation.ComponentScan;
 import org.springframework.core.env.Environment;
@@ -24,25 +25,25 @@ import java.net.UnknownHostException;
 
 //@EnableSwagger2
 @EnableCustomSwagger2
+@EnableScheduling
 @EnableFeignClients(basePackages = "com.usky")
 @MapperScan(value = "com.usky.issue.mapper")
 @ComponentScan("com.usky")
 @SpringBootApplication
-public class RuoYiSystemApplication
-{
-    private static final Logger LOGGER = LoggerFactory.getLogger(RuoYiSystemApplication.class);
+public class IssueApplication {
+    private static final Logger LOGGER = LoggerFactory.getLogger(IssueApplication.class);
 
     public static void main(String[] args) throws UnknownHostException {
-        ConfigurableApplicationContext application = SpringApplication.run(RuoYiSystemApplication.class, args);
+        ConfigurableApplicationContext application = SpringApplication.run(IssueApplication.class, args);
         Environment env = application.getEnvironment();
         String ip = InetAddress.getLocalHost().getHostAddress();
         String port = env.getProperty("server.port");
         String path = env.getProperty("server.servlet.context-path");
         LOGGER.info("\n----------------------------------------------------------\n\t" +
                 "Application is running! Access URLs:\n\t" +
-                "Local: \t\thttp://localhost:" + port + (null==path?"":path) + "/\n\t" +
-                "External: \thttp://" + ip + ":" + port + (null==path?"":path) + "/\n\t" +
-                "Api: \t\thttp://" + ip + ":" + port + (null==path?"":path) + "/swagger-ui/index.html\n\t" +
+                "Local: \t\thttp://localhost:" + port + (null == path ? "" : path) + "/\n\t" +
+                "External: \thttp://" + ip + ":" + port + (null == path ? "" : path) + "/\n\t" +
+                "Api: \t\thttp://" + ip + ":" + port + (null == path ? "" : path) + "/swagger-ui/index.html\n\t" +
                 "----------------------------------------------------------");
     }
 }

+ 163 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/api/CloudIntegrationApiController.java

@@ -0,0 +1,163 @@
+package com.usky.issue.controller.api;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.issue.service.CloudAuthService;
+import com.usky.issue.service.CloudConfigService;
+import com.usky.issue.service.CloudSyncReceiveService;
+import com.usky.issue.service.constant.CloudIntegrationConstants;
+import com.usky.issue.service.vo.CloudConnectionTestResponse;
+import com.usky.issue.service.vo.SyncPacket;
+import com.usky.issue.service.vo.SyncResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 云平台集成 API(供本地服务器调用:连通性测试 + 双向同步)
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@RestController
+@RequestMapping("/cloud/integration")
+public class CloudIntegrationApiController {
+
+    @Autowired
+    private CloudConfigService cloudConfigService;
+    @Autowired
+    private CloudAuthService cloudAuthService;
+    @Autowired
+    private CloudSyncReceiveService cloudSyncReceiveService;
+
+    /**
+     * 网络层探测
+     */
+    @GetMapping("/ping")
+    public String ping() {
+        return "PONG";
+    }
+
+    /**
+     * 认证层 + 租户层检测
+     */
+    @GetMapping("/auth-check")
+    public ResponseEntity<String> authCheck(
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_API_KEY, required = false) String apiKey,
+            @RequestParam Integer tenantId) {
+        if (!cloudAuthService.validateToken(apiKey, tenantId)) {
+            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("AUTH_INVALID");
+        }
+        CloudConnectionTestResponse tenantResult = cloudConfigService.validateTenantConnection(tenantId);
+        if (!tenantResult.isSuccess()) {
+            return ResponseEntity.ok(CloudIntegrationConstants.AUTH_CHECK_TENANT_MISSING);
+        }
+        return ResponseEntity.ok(CloudIntegrationConstants.AUTH_CHECK_VALID);
+    }
+
+    /**
+     * 连通性测试入口(兼容旧调用方式)
+     */
+    @PostMapping("/testConnection")
+    public ApiResult<CloudConnectionTestResponse> testConnection(
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_TENANT_ID, required = false) String tenantIdHeader,
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_API_KEY, required = false) String apiKey) {
+        Integer tenantId = parseTenantId(tenantIdHeader);
+        if (tenantId != null && !cloudAuthService.validateToken(apiKey, tenantId)) {
+            CloudConnectionTestResponse response = new CloudConnectionTestResponse();
+            response.setSuccess(false);
+            response.setConnectionStatus(2);
+            response.setMessage(CloudIntegrationConstants.MSG_AUTH_INVALID);
+            response.setNetworkReachable(true);
+            response.setAuthValid(false);
+            response.setTenantExists(false);
+            return ApiResult.success(response);
+        }
+        CloudConnectionTestResponse response = cloudConfigService.validateTenantConnection(tenantId);
+        response.setNetworkReachable(true);
+        response.setAuthValid(tenantId == null || cloudAuthService.validateToken(apiKey, tenantId));
+        response.setTenantExists(response.isSuccess());
+        return ApiResult.success(response);
+    }
+
+    /**
+     * 接收本地推送数据(本地→云端)
+     */
+    @PostMapping("/sync/receive")
+    public SyncResponse receive(
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_API_KEY, required = false) String apiKey,
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_SIGN, required = false) String sign,
+            @RequestBody SyncPacket packet) {
+
+        if (!cloudAuthService.validate(apiKey, sign, packet)) {
+            return SyncResponse.builder().success(false).message("认证失败或请求已过期").build();
+        }
+        if (!cloudSyncReceiveService.validateTenant(packet.getTenantId())) {
+            return SyncResponse.builder().success(false).message("租户不存在或已停用").build();
+        }
+        try {
+            int count = cloudSyncReceiveService.batchUpsert(packet);
+            return SyncResponse.builder()
+                    .success(true)
+                    .message("接收成功")
+                    .acceptedCount(count)
+                    .confirmedVersion(packet.getBatchVersion())
+                    .build();
+        } catch (Exception e) {
+            return SyncResponse.builder().success(false).message("写入失败:" + e.getMessage()).build();
+        }
+    }
+
+    /**
+     * 下发数据轮询(云端→本地)
+     */
+    @GetMapping("/sync/poll")
+    public ApiResult<SyncPacket> poll(
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_API_KEY, required = false) String apiKey,
+            @RequestParam String tenantId,
+            @RequestParam String tableName,
+            @RequestParam(required = false) Long lastVersion) {
+
+        if (!cloudAuthService.validateToken(apiKey, Integer.valueOf(tenantId))) {
+            return ApiResult.success(null);
+        }
+        return ApiResult.success(cloudSyncReceiveService.pollDownQueue(tenantId, tableName, lastVersion));
+    }
+
+    /**
+     * 确认下发数据已消费
+     */
+    @PostMapping("/sync/poll/ack")
+    public SyncResponse ackPoll(
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_API_KEY, required = false) String apiKey,
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_TENANT_ID, required = false) String tenantIdHeader,
+            @RequestBody List<Long> queueIds) {
+
+        Integer tenantId = parseTenantId(tenantIdHeader);
+        if (tenantId != null && !cloudAuthService.validateToken(apiKey, tenantId)) {
+            return SyncResponse.builder().success(false).message("认证失败").build();
+        }
+        cloudSyncReceiveService.ackDownQueue(queueIds);
+        return SyncResponse.builder().success(true).message("确认成功").build();
+    }
+
+    private Integer parseTenantId(String tenantIdHeader) {
+        if (!StringUtils.hasText(tenantIdHeader)) {
+            return null;
+        }
+        try {
+            return Integer.valueOf(tenantIdHeader.trim());
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+}

+ 68 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudConfigController.java

@@ -0,0 +1,68 @@
+package com.usky.issue.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.issue.service.vo.CloudConfigResponse;
+import com.usky.issue.service.vo.CloudConfigSaveRequest;
+import com.usky.issue.service.vo.CloudConnectionTestRequest;
+import com.usky.issue.service.vo.CloudConnectionTestResponse;
+import com.usky.issue.service.vo.CloudDisableResponse;
+import com.usky.issue.service.CloudConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.Valid;
+
+/**
+ * 云平台配置接口
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Validated
+@RestController
+@RequestMapping("/cloud")
+public class CloudConfigController {
+
+    @Autowired
+    private CloudConfigService cloudConfigService;
+
+    /**
+     * 获取云平台配置
+     */
+    @GetMapping("/config")
+    public ApiResult<CloudConfigResponse> getConfig() {
+        return ApiResult.success(cloudConfigService.getConfig());
+    }
+
+    /**
+     * 保存、修改云平台配置
+     */
+    @PutMapping("/config")
+    public ApiResult<CloudConfigResponse> saveConfig(
+            @Valid @RequestBody CloudConfigSaveRequest request) {
+        return ApiResult.success(cloudConfigService.saveConfig(request));
+    }
+
+
+    /**
+     * 分层连通性测试:网络层 → 认证层 → 租户层
+     */
+    @PostMapping("/config/testConnection")
+    public ApiResult<CloudConnectionTestResponse> testConnection(
+            @Valid @RequestBody CloudConnectionTestRequest request
+    ) {
+        return ApiResult.success(cloudConfigService.testConnection(request));
+    }
+
+    /**
+     * 启用/禁用云平台(status: 0-禁用, 1-启用)
+     */
+    @PutMapping("/config/disable")
+    public ApiResult<CloudDisableResponse> disable(@RequestParam Integer status) {
+        return ApiResult.success(cloudConfigService.disable(status));
+    }
+
+}

+ 50 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudSyncController.java

@@ -0,0 +1,50 @@
+package com.usky.issue.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.issue.service.vo.SyncStatusResponse;
+import com.usky.issue.service.vo.SyncTaskResponse;
+import com.usky.issue.service.CloudSyncService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+
+/**
+ * 云平台数据同步接口
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@RestController
+@RequestMapping("/cloud")
+public class CloudSyncController {
+
+    @Autowired
+    private CloudSyncService cloudSyncService;
+
+    @GetMapping("/sync/status")
+    public ApiResult<List<SyncStatusResponse>> listStatus() {
+        return ApiResult.success(cloudSyncService.listSyncStatus());
+    }
+
+    @PostMapping("/sync/{type}")
+    public ApiResult<SyncTaskResponse> manualSync(@PathVariable String type, HttpServletRequest request) {
+        return ApiResult.success(cloudSyncService.triggerManualSync(type));
+    }
+
+    @GetMapping("/sync/task/{taskId}")
+    public ApiResult<SyncTaskResponse> taskProgress(@PathVariable Long taskId) {
+        return ApiResult.success(cloudSyncService.getTaskProgress(taskId));
+    }
+
+    // private String resolveOperator(HttpServletRequest request) {
+    //     String user = request.getHeader("X-User-Name");
+    //     return user != null ? user : "system";
+    // }
+}

+ 49 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudTriggerEventController.java

@@ -0,0 +1,49 @@
+package com.usky.issue.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.issue.service.vo.TriggerEventResponse;
+import com.usky.issue.service.vo.TriggerEventSaveRequest;
+import com.usky.issue.service.CloudTriggerEventService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * 云平台触发事件接口
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Validated
+@RestController
+@RequestMapping("/cloud")
+public class CloudTriggerEventController {
+
+    @Autowired
+    private CloudTriggerEventService cloudTriggerEventService;
+
+    @GetMapping("/triggerEvents")
+    public ApiResult<List<TriggerEventResponse>> listEvents() {
+        return ApiResult.success(cloudTriggerEventService.listEvents());
+    }
+
+    @PutMapping("/triggerEvents")
+    public ApiResult<Void> saveEvents(@Valid @RequestBody TriggerEventSaveRequest request,
+                                      HttpServletRequest httpRequest) {
+        cloudTriggerEventService.saveEvents(request, resolveOperator(httpRequest), httpRequest.getRemoteAddr());
+        return ApiResult.success();
+    }
+
+    private String resolveOperator(HttpServletRequest request) {
+        String user = request.getHeader("X-User-Name");
+        return user != null ? user : "system";
+    }
+}

+ 36 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/CloudAuditEntity.java

@@ -0,0 +1,36 @@
+package com.usky.issue.domain;
+
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 云平台实体审计字段基类
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+public abstract class CloudAuditEntity implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 租户ID */
+    private Integer tenantId;
+
+    private String createdBy;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createdTime;
+
+    private String updatedBy;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updatedTime;
+
+    @TableLogic(value = "0", delval = "1")
+    private Integer isDeleted;
+}

+ 44 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueCloudConfig.java

@@ -0,0 +1,44 @@
+package com.usky.issue.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 云平台配置
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("issue_cloud_config")
+public class IssueCloudConfig extends CloudAuditEntity {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /** 云平台地址 */
+    private String cloudAddress;
+
+    /** AES-256-GCM加密后的凭证密钥 */
+    private String credentialKey;
+
+    /** 0禁用 1启用 */
+    private Integer status;
+
+    /** 0未知 1成功 2失败 */
+    private Integer connectionStatus;
+
+    // @Version
+    private Integer version;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime lastTestTime;
+
+    @TableField(exist = false)
+    private String token;
+}

+ 30 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueOperationLog.java

@@ -0,0 +1,30 @@
+package com.usky.issue.domain;

+

+import com.baomidou.mybatisplus.annotation.IdType;

+import com.baomidou.mybatisplus.annotation.TableId;

+import com.baomidou.mybatisplus.annotation.TableName;

+import lombok.Data;

+import lombok.EqualsAndHashCode;

+

+/**

+ * 操作审计日志

+ *

+ * @author fyc

+ * @date 2026-05-21

+ */

+@Data

+@EqualsAndHashCode(callSuper = true)

+@TableName("issue_operation_log")

+public class IssueOperationLog extends CloudAuditEntity {

+

+    @TableId(value = "id", type = IdType.AUTO)

+    private Long id;

+

+    private Long configId;

+

+    private String operationType;

+

+    private String operationDetail;

+

+    private String requestIp;

+}


+ 33 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncCursor.java

@@ -0,0 +1,33 @@
+package com.usky.issue.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 同步游标
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("issue_sync_cursor")
+public class IssueSyncCursor extends CloudAuditEntity {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private String tableName;
+
+    /** UP/DOWN */
+    private String direction;
+
+    private Long lastSyncVersion;
+
+    private LocalDateTime lastSyncTime;
+}

+ 38 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncData.java

@@ -0,0 +1,38 @@
+package com.usky.issue.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 本地同步业务数据
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("issue_sync_data")
+public class IssueSyncData extends CloudAuditEntity {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private String tableName;
+
+    private String dataKey;
+
+    private String payload;
+
+    private Long syncVersion;
+
+    /** 0:未同步 1:已推送 2:推送失败 */
+    private Integer syncStatus;
+
+    /** LOCAL/CLOUD */
+    private String syncDirection;
+}

+ 32 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncDetail.java

@@ -0,0 +1,32 @@
+package com.usky.issue.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 同步任务明细
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("issue_sync_detail")
+public class IssueSyncDetail extends CloudAuditEntity {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private Long taskId;
+
+    private String dataId;
+
+    private String detailStatus;
+
+    private Integer retryCount;
+
+    private String errorMessage;
+}

+ 34 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncDownQueue.java

@@ -0,0 +1,34 @@
+package com.usky.issue.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 云端下发队列(云端→本地)
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("issue_sync_down_queue")
+public class IssueSyncDownQueue extends CloudAuditEntity {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private String tableName;
+
+    /** INSERT/UPDATE/DELETE */
+    private String operation;
+
+    private String payload;
+
+    private Long syncVersion;
+
+    /** 0:待拉取 1:已确认 2:失败 */
+    private Integer status;
+}

+ 40 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncStatus.java

@@ -0,0 +1,40 @@
+package com.usky.issue.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 同步状态统计
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("issue_sync_status")
+public class IssueSyncStatus extends CloudAuditEntity {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private Long configId;
+
+    private String syncType;
+
+    private String typeName;
+
+    private Integer totalCount;
+
+    private Integer successCount;
+
+    private Integer failureCount;
+
+    private Integer unsyncedCount;
+
+    private LocalDateTime lastSyncTime;
+}

+ 46 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncTask.java

@@ -0,0 +1,46 @@
+package com.usky.issue.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 同步任务
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("issue_sync_task")
+public class IssueSyncTask extends CloudAuditEntity {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private Long configId;
+
+    private String syncType;
+
+    private String taskStatus;
+
+    private String triggerMode;
+
+    private Integer totalCount;
+
+    private Integer processedCount;
+
+    private Integer successCount;
+
+    private Integer failureCount;
+
+    private String errorSummary;
+
+    private LocalDateTime startTime;
+
+    private LocalDateTime finishTime;
+}

+ 33 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueTriggerEvent.java

@@ -0,0 +1,33 @@
+package com.usky.issue.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 触发事件配置
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("issue_trigger_event")
+public class IssueTriggerEvent extends CloudAuditEntity {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    private Long configId;
+
+    private String eventCode;
+
+    private String eventName;
+
+    private String description;
+
+    /** 0否 1是 */
+    private Integer enabled;
+}

+ 17 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueCloudConfigMapper.java

@@ -0,0 +1,17 @@
+package com.usky.issue.mapper;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.issue.domain.IssueCloudConfig;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 云平台配置 Mapper
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@DS("usky-cloud")
+@Repository
+public interface IssueCloudConfigMapper extends CrudMapper<IssueCloudConfig> {
+}

+ 17 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueOperationLogMapper.java

@@ -0,0 +1,17 @@
+package com.usky.issue.mapper;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.issue.domain.IssueOperationLog;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 操作日志 Mapper
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@DS("usky-cloud")
+@Repository
+public interface IssueOperationLogMapper extends CrudMapper<IssueOperationLog> {
+}

+ 17 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncCursorMapper.java

@@ -0,0 +1,17 @@
+package com.usky.issue.mapper;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.issue.domain.IssueSyncCursor;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 同步游标 Mapper
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@DS("usky-cloud")
+@Repository
+public interface IssueSyncCursorMapper extends CrudMapper<IssueSyncCursor> {
+}

+ 17 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncDataMapper.java

@@ -0,0 +1,17 @@
+package com.usky.issue.mapper;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.issue.domain.IssueSyncData;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 同步业务数据 Mapper
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@DS("usky-cloud")
+@Repository
+public interface IssueSyncDataMapper extends CrudMapper<IssueSyncData> {
+}

+ 17 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncDetailMapper.java

@@ -0,0 +1,17 @@
+package com.usky.issue.mapper;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.issue.domain.IssueSyncDetail;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 同步明细 Mapper
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@DS("usky-cloud")
+@Repository
+public interface IssueSyncDetailMapper extends CrudMapper<IssueSyncDetail> {
+}

+ 17 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncDownQueueMapper.java

@@ -0,0 +1,17 @@
+package com.usky.issue.mapper;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.issue.domain.IssueSyncDownQueue;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 云端下发队列 Mapper
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@DS("usky-cloud")
+@Repository
+public interface IssueSyncDownQueueMapper extends CrudMapper<IssueSyncDownQueue> {
+}

+ 17 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncStatusMapper.java

@@ -0,0 +1,17 @@
+package com.usky.issue.mapper;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.issue.domain.IssueSyncStatus;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 同步状态 Mapper
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@DS("usky-cloud")
+@Repository
+public interface IssueSyncStatusMapper extends CrudMapper<IssueSyncStatus> {
+}

+ 17 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncTaskMapper.java

@@ -0,0 +1,17 @@
+package com.usky.issue.mapper;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.issue.domain.IssueSyncTask;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 同步任务 Mapper
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@DS("usky-cloud")
+@Repository
+public interface IssueSyncTaskMapper extends CrudMapper<IssueSyncTask> {
+}

+ 17 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueTriggerEventMapper.java

@@ -0,0 +1,17 @@
+package com.usky.issue.mapper;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.issue.domain.IssueTriggerEvent;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 触发事件 Mapper
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@DS("usky-cloud")
+@Repository
+public interface IssueTriggerEventMapper extends CrudMapper<IssueTriggerEvent> {
+}

+ 16 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudAuthService.java

@@ -0,0 +1,16 @@
+package com.usky.issue.service;
+
+import com.usky.issue.service.vo.SyncPacket;
+
+/**
+ * 云平台认证服务
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+public interface CloudAuthService {
+
+    boolean validateToken(String apiKey, Integer tenantId);
+
+    boolean validate(String apiKey, String sign, SyncPacket packet);
+}

+ 35 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudConfigService.java

@@ -0,0 +1,35 @@
+package com.usky.issue.service;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.service.vo.CloudConfigResponse;
+import com.usky.issue.service.vo.CloudConfigSaveRequest;
+import com.usky.issue.service.vo.CloudConnectionTestRequest;
+import com.usky.issue.service.vo.CloudConnectionTestResponse;
+import com.usky.issue.service.vo.CloudDisableResponse;
+
+/**
+ * 云平台配置服务
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@DS("usky-cloud")
+public interface CloudConfigService {
+
+    CloudConfigResponse getConfig();
+
+    CloudConfigResponse saveConfig(CloudConfigSaveRequest request);
+
+    CloudConnectionTestResponse testConnection(CloudConnectionTestRequest request);
+
+    /**
+     * 云平台侧租户配置校验(供集成 API 调用,查询云库后返回结果,不再向外发起 HTTP)
+     */
+    CloudConnectionTestResponse validateTenantConnection(Integer tenantId);
+
+    CloudDisableResponse disable(Integer status);
+
+    /** 获取当前生效配置,不存在则抛业务异常 */
+    IssueCloudConfig requireActiveConfig();
+}

+ 12 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudOperationLogService.java

@@ -0,0 +1,12 @@
+package com.usky.issue.service;
+
+/**
+ * 操作审计日志服务
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+public interface CloudOperationLogService {
+
+    void log(Long configId, String operationType, String detail, String operator, String requestIp);
+}

+ 25 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncReceiveService.java

@@ -0,0 +1,25 @@
+package com.usky.issue.service;
+
+import com.usky.issue.service.vo.SyncPacket;
+import com.usky.issue.service.vo.SyncResponse;
+
+import java.util.List;
+
+/**
+ * 云端同步接收服务(接收推送、下发轮询)
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+public interface CloudSyncReceiveService {
+
+    boolean validateTenant(String tenantId);
+
+    int batchUpsert(SyncPacket packet);
+
+    SyncPacket pollDownQueue(String tenantId, String tableName, Long lastVersion);
+
+    void ackDownQueue(List<Long> queueIds);
+
+    int countPendingDownQueue(Integer tenantId, String tableName);
+}

+ 25 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncService.java

@@ -0,0 +1,25 @@
+package com.usky.issue.service;

+

+import com.usky.issue.service.vo.SyncStatusResponse;

+import com.usky.issue.service.vo.SyncTaskResponse;

+

+import java.util.List;

+

+/**

+ * 云平台数据同步服务

+ *

+ * @author fyc

+ * @date 2026-05-21

+ */

+public interface CloudSyncService {

+

+    List<SyncStatusResponse> listSyncStatus();

+

+    SyncTaskResponse triggerManualSync(String syncType);

+

+    SyncTaskResponse getTaskProgress(Long taskId);

+

+    void onOrgChanged();

+

+    void onUserChanged();

+}


+ 16 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncTaskRunner.java

@@ -0,0 +1,16 @@
+package com.usky.issue.service;
+
+import com.usky.issue.service.enums.SyncTypeEnum;
+
+/**
+ * 同步任务执行器(独立 Bean 保证事务生效)
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+public interface CloudSyncTaskRunner {
+
+    com.usky.issue.domain.IssueSyncTask createTask(Long configId, SyncTypeEnum type, String triggerMode);
+
+    void executeSyncTask(Long taskId, SyncTypeEnum type);
+}

+ 20 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudTriggerEventService.java

@@ -0,0 +1,20 @@
+package com.usky.issue.service;
+
+
+import com.usky.issue.service.vo.TriggerEventResponse;
+import com.usky.issue.service.vo.TriggerEventSaveRequest;
+
+import java.util.List;
+
+/**
+ * 触发事件配置服务
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+public interface CloudTriggerEventService {
+
+    List<TriggerEventResponse> listEvents();
+
+    void saveEvents(TriggerEventSaveRequest request, String operator, String requestIp);
+}

+ 17 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalPullService.java

@@ -0,0 +1,17 @@
+package com.usky.issue.service;
+
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.service.vo.SyncResult;
+
+/**
+ * 本地拉取服务(云端→本地)
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+public interface LocalPullService {
+
+    SyncResult pullAndApply(String tableName, IssueCloudConfig config);
+
+    SyncResult pullByTenant(Integer tenantId, String tableName, IssueCloudConfig config);
+}

+ 19 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalPushService.java

@@ -0,0 +1,19 @@
+package com.usky.issue.service;
+
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.service.vo.SyncResult;
+
+/**
+ * 本地推送服务(本地→云端)
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+public interface LocalPushService {
+
+    SyncResult pushAllPending(String tableName, IssueCloudConfig config);
+
+    SyncResult pushByTenant(Integer tenantId, String tableName, IssueCloudConfig config);
+
+    void resetCursor(Integer tenantId, String tableName);
+}

+ 19 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalSyncAgent.java

@@ -0,0 +1,19 @@
+package com.usky.issue.service;
+
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.service.enums.SyncDirection;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import com.usky.issue.service.vo.SyncResult;
+
+/**
+ * 本地同步 Agent
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+public interface LocalSyncAgent {
+
+    SyncResult executeSync(SyncTypeEnum syncType, IssueCloudConfig config, SyncDirection direction, boolean fullSync);
+
+    int countPending(SyncTypeEnum syncType, IssueCloudConfig config);
+}

+ 260 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/client/CloudPlatformClient.java

@@ -0,0 +1,260 @@
+package com.usky.issue.service.client;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.service.constant.CloudIntegrationConstants;
+import com.usky.issue.service.util.AesGcmCipher;
+import com.usky.issue.service.util.HmacSignUtil;
+import com.usky.issue.service.vo.CloudConnectionTestResult;
+import com.usky.issue.service.vo.SyncPacket;
+import com.usky.issue.service.vo.SyncResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.HttpServerErrorException;
+import org.springframework.web.client.ResourceAccessException;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.time.Duration;
+import java.util.List;
+
+/**
+ * 云平台 HTTP 客户端(连通性测试 + 双向同步)
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Slf4j
+@Component
+public class CloudPlatformClient {
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    private final RestTemplate restTemplate;
+    private final AesGcmCipher aesGcmCipher;
+
+    private static final String URL_END = "/service-issue/cloud/integration/testConnection";
+    private static final String INTEGRATION_PATH = "/service-issue/cloud/integration";
+
+    public CloudPlatformClient(RestTemplateBuilder restTemplateBuilder, AesGcmCipher aesGcmCipher) {
+        this.restTemplate = restTemplateBuilder
+                .setConnectTimeout(Duration.ofMillis(CloudIntegrationConstants.CONNECTION_TEST_TIMEOUT_MS))
+                .setReadTimeout(Duration.ofMillis(CloudIntegrationConstants.CONNECTION_TEST_TIMEOUT_MS))
+                .build();
+        this.aesGcmCipher = aesGcmCipher;
+    }
+
+    /**
+     * 使用请求参数发起连通性测试:cloudAddress + URL_END
+     */
+    public CloudConnectionTestResult testConnection(String cloudAddress, Integer tenantId,
+                                                    String credentialKey, String token) {
+        if (!StringUtils.hasText(cloudAddress) || tenantId == null || !StringUtils.hasText(credentialKey)) {
+            return CloudConnectionTestResult.failure(false, "测试参数不完整");
+        }
+
+        String testUrl = buildTestConnectionUrl(cloudAddress);
+        long start = System.currentTimeMillis();
+
+        try {
+            HttpHeaders headers = buildAuthHeaders(tenantId, credentialKey, token);
+            ResponseEntity<String> response = restTemplate.exchange(
+                    testUrl, HttpMethod.POST, new HttpEntity<>(headers), String.class);
+            return parseConnectionTestResult(response.getBody(), System.currentTimeMillis() - start);
+        } catch (ResourceAccessException ex) {
+            log.error("云平台网络不可达: url={}, error={}", testUrl, ex.getMessage());
+            return CloudConnectionTestResult.networkFailure(CloudIntegrationConstants.MSG_NETWORK_UNREACHABLE);
+        } catch (HttpClientErrorException.Unauthorized ex) {
+            return CloudConnectionTestResult.authFailure(System.currentTimeMillis() - start, "认证失败:凭证无效");
+        } catch (HttpClientErrorException | HttpServerErrorException ex) {
+            log.error("云平台连接测试失败: url={}, status={}, body={}",
+                    testUrl, ex.getStatusCode(), ex.getResponseBodyAsString());
+            return CloudConnectionTestResult.authFailure(System.currentTimeMillis() - start,
+                    "云平台返回异常:" + ex.getStatusCode());
+        } catch (Exception ex) {
+            log.error("云平台连接测试失败: url={}, error={}", testUrl, ex.getMessage(), ex);
+            return CloudConnectionTestResult.failure(false, ex.getMessage());
+        }
+    }
+
+    /**
+     * 使用已保存配置发起连通性测试(同步等场景)
+     */
+    public CloudConnectionTestResult testConnection(IssueCloudConfig config) {
+        if (config == null || config.getTenantId() == null || !StringUtils.hasText(config.getCloudAddress())
+                || config.getCredentialKey() == null) {
+            log.warn("云平台配置不完整");
+            return CloudConnectionTestResult.failure(false, "云平台配置不完整");
+        }
+        try {
+            String credential = aesGcmCipher.decrypt(config.getCredentialKey());
+            return testConnection(config.getCloudAddress(), config.getTenantId(), credential, config.getToken());
+        } catch (Exception ex) {
+            log.error("凭证解密失败 tenantId={}", config.getTenantId(), ex);
+            return CloudConnectionTestResult.failure(false, "凭证解密失败");
+        }
+    }
+
+    public SyncResponse pushToCloud(SyncPacket packet, IssueCloudConfig config) {
+        try {
+            String credential = aesGcmCipher.decrypt(config.getCredentialKey());
+            String body = OBJECT_MAPPER.writeValueAsString(packet);
+            HttpHeaders headers = buildAuthHeaders(config, credential);
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            headers.add(CloudIntegrationConstants.HEADER_SIGN, HmacSignUtil.sign(body, credential));
+
+            ResponseEntity<String> response = restTemplate.exchange(
+                    resolveIntegrationBaseUrl(config) + CloudIntegrationConstants.PATH_SYNC_RECEIVE,
+                    HttpMethod.POST,
+                    new HttpEntity<>(body, headers),
+                    String.class);
+            return parseSyncResponse(response.getBody());
+        } catch (Exception e) {
+            log.error("推送云端网络异常", e);
+            return SyncResponse.builder().success(false).message(e.getMessage()).build();
+        }
+    }
+
+    public SyncPacket pollCloud(Integer tenantId, String tableName, Long lastVersion, IssueCloudConfig config) {
+        try {
+            String credential = aesGcmCipher.decrypt(config.getCredentialKey());
+            HttpHeaders headers = buildAuthHeaders(config, credential);
+            String url = UriComponentsBuilder
+                    .fromHttpUrl(resolveIntegrationBaseUrl(config) + CloudIntegrationConstants.PATH_SYNC_POLL)
+                    .queryParam("tenantId", tenantId)
+                    .queryParam("tableName", tableName)
+                    .queryParam("lastVersion", lastVersion == null ? 0L : lastVersion)
+                    .toUriString();
+
+            ResponseEntity<String> response = restTemplate.exchange(
+                    url, HttpMethod.GET, new HttpEntity<>(headers), String.class);
+            if (!StringUtils.hasText(response.getBody())) {
+                return null;
+            }
+            JsonNode json = OBJECT_MAPPER.readTree(response.getBody());
+            JsonNode dataNode = json.has("data") ? json.get("data") : json;
+            if (dataNode == null || dataNode.isNull()) {
+                return null;
+            }
+            return OBJECT_MAPPER.treeToValue(dataNode, SyncPacket.class);
+        } catch (Exception e) {
+            log.error("轮询云端失败", e);
+            return null;
+        }
+    }
+
+    public SyncResponse ackPoll(List<Long> queueIds, IssueCloudConfig config) {
+        try {
+            String credential = aesGcmCipher.decrypt(config.getCredentialKey());
+            HttpHeaders headers = buildAuthHeaders(config, credential);
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            String body = OBJECT_MAPPER.writeValueAsString(queueIds);
+
+            ResponseEntity<String> response = restTemplate.exchange(
+                    resolveIntegrationBaseUrl(config) + CloudIntegrationConstants.PATH_SYNC_POLL_ACK,
+                    HttpMethod.POST,
+                    new HttpEntity<>(body, headers),
+                    String.class);
+            return parseSyncResponse(response.getBody());
+        } catch (Exception e) {
+            log.error("ACK云端失败", e);
+            return SyncResponse.builder().success(false).message(e.getMessage()).build();
+        }
+    }
+
+    private HttpHeaders buildAuthHeaders(IssueCloudConfig config, String credential) {
+        return buildAuthHeaders(config.getTenantId(), credential, config.getToken());
+    }
+
+    private HttpHeaders buildAuthHeaders(Integer tenantId, String credential, String token) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.add(CloudIntegrationConstants.HEADER_TENANT_ID, String.valueOf(tenantId));
+        headers.add(CloudIntegrationConstants.HEADER_API_KEY, credential);
+        if (StringUtils.hasText(token)) {
+            headers.add("Authorization", "Bearer " + token);
+        }
+        return headers;
+    }
+
+    private CloudConnectionTestResult parseConnectionTestResult(String body, long costTime) {
+        if (!StringUtils.hasText(body)) {
+            return CloudConnectionTestResult.failure(true, "云端返回空响应");
+        }
+        try {
+            JsonNode json = OBJECT_MAPPER.readTree(body);
+            JsonNode dataNode = json.has("data") ? json.get("data") : json;
+            if (dataNode == null || dataNode.isNull()) {
+                return CloudConnectionTestResult.failure(true, "云端返回空数据");
+            }
+            CloudConnectionTestResult result = new CloudConnectionTestResult();
+            result.setSuccess(dataNode.path("success").asBoolean(false));
+            result.setMessage(dataNode.path("message").asText(null));
+            result.setNetworkReachable(dataNode.has("networkReachable")
+                    ? dataNode.get("networkReachable").asBoolean() : true);
+            result.setAuthValid(dataNode.has("authValid")
+                    ? dataNode.get("authValid").asBoolean() : result.isSuccess());
+            result.setTenantExists(dataNode.has("tenantExists")
+                    ? dataNode.get("tenantExists").asBoolean() : result.isSuccess());
+            result.setCostTime(dataNode.has("costTime") ? dataNode.get("costTime").asLong() : costTime);
+            if (!StringUtils.hasText(result.getMessage())) {
+                result.setMessage(result.isSuccess()
+                        ? CloudIntegrationConstants.MSG_CONNECTION_SUCCESS
+                        : "连接测试未通过");
+            }
+            return result;
+        } catch (Exception e) {
+            log.error("连接测试响应解析失败: {}", body, e);
+            return CloudConnectionTestResult.failure(true, "响应解析失败");
+        }
+    }
+
+    private String buildTestConnectionUrl(String cloudAddress) {
+        return normalizeCloudBaseUrl(cloudAddress) + URL_END;
+    }
+
+    private String resolveIntegrationBaseUrl(IssueCloudConfig config) {
+        if (config == null || !StringUtils.hasText(config.getCloudAddress())) {
+            return "";
+        }
+        return normalizeCloudBaseUrl(config.getCloudAddress()) + INTEGRATION_PATH;
+    }
+
+    private String normalizeCloudBaseUrl(String cloudAddress) {
+        String url = cloudAddress.trim();
+        if (url.endsWith("/")) {
+            url = url.substring(0, url.length() - 1);
+        }
+        if (url.endsWith(URL_END)) {
+            return url.substring(0, url.length() - URL_END.length());
+        }
+        if (url.endsWith(INTEGRATION_PATH)) {
+            return url.substring(0, url.length() - INTEGRATION_PATH.length());
+        }
+        if (url.endsWith(CloudIntegrationConstants.PATH_TEST_CONNECTION)) {
+            return url.substring(0, url.length() - CloudIntegrationConstants.PATH_TEST_CONNECTION.length());
+        }
+        return url;
+    }
+
+    private SyncResponse parseSyncResponse(String body) {
+        if (!StringUtils.hasText(body)) {
+            return SyncResponse.builder().success(false).message("云端返回空响应").build();
+        }
+        try {
+            JsonNode json = OBJECT_MAPPER.readTree(body);
+            JsonNode dataNode = json.has("data") ? json.get("data") : json;
+            return OBJECT_MAPPER.treeToValue(dataNode, SyncResponse.class);
+        } catch (Exception e) {
+            return SyncResponse.builder().success(false).message("响应解析失败").build();
+        }
+    }
+}

+ 30 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/config/CloudSyncExecutorConfig.java

@@ -0,0 +1,30 @@
+package com.usky.issue.service.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 云平台大批量同步线程池
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Configuration
+public class CloudSyncExecutorConfig {
+
+    @Bean(name = "cloudSyncExecutor")
+    public Executor cloudSyncExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(2);
+        executor.setMaxPoolSize(4);
+        executor.setQueueCapacity(100);
+        executor.setThreadNamePrefix("cloud-sync-");
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        executor.initialize();
+        return executor;
+    }
+}

+ 47 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/constant/CloudIntegrationConstants.java

@@ -0,0 +1,47 @@
+package com.usky.issue.service.constant;
+
+/**
+ * 云平台集成常量
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+public final class CloudIntegrationConstants {
+
+    private CloudIntegrationConstants() {
+    }
+
+    public static final String LOCK_SYNC_PREFIX = "cloud:sync:";
+    public static final long LOCK_SYNC_SECONDS = 30L;
+
+    public static final String CACHE_STATUS_PREFIX = "cloud:sync:status:";
+    public static final long CACHE_STATUS_TTL_SECONDS = 5L;
+
+    public static final int CONNECTION_TEST_TIMEOUT_MS = 10_000;
+
+    public static final String HEADER_TENANT_ID = "X-Tenant-Id";
+    public static final String HEADER_API_KEY = "X-Api-Key";
+    public static final String HEADER_SIGN = "X-Sign";
+
+    public static final String PATH_TEST_CONNECTION = "/testConnection";
+    public static final String PATH_PING = "/ping";
+    public static final String PATH_AUTH_CHECK = "/auth-check";
+    public static final String PATH_SYNC_RECEIVE = "/sync/receive";
+    public static final String PATH_SYNC_POLL = "/sync/poll";
+    public static final String PATH_SYNC_POLL_ACK = "/sync/poll/ack";
+
+    public static final String AUTH_CHECK_VALID = "VALID";
+    public static final String AUTH_CHECK_TENANT_MISSING = "TENANT_MISSING";
+
+    public static final String MSG_TENANT_CONFIG_MISSING = "租户配置缺失";
+    public static final String MSG_AUTH_INVALID = "认证失败:API Key无效";
+    public static final String MSG_CONNECTION_SUCCESS = "连接成功";
+    public static final String MSG_NETWORK_UNREACHABLE = "云平台网络不可达";
+
+    public static final String OPERATION_SAVE_CONFIG = "SAVE_CONFIG";
+    public static final String OPERATION_TEST_CONNECTION = "TEST_CONNECTION";
+    public static final String OPERATION_DISABLE = "DISABLE_CONFIG";
+    public static final String OPERATION_ENABLE = "ENABLE_CONFIG";
+    public static final String OPERATION_MANUAL_SYNC = "MANUAL_SYNC";
+    public static final String OPERATION_SAVE_TRIGGER = "SAVE_TRIGGER";
+}

+ 31 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncDirection.java

@@ -0,0 +1,31 @@
+package com.usky.issue.service.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 同步方向
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Getter
+@AllArgsConstructor
+public enum SyncDirection {
+
+    UP("UP", "本地→云端"),
+    DOWN("DOWN", "云端→本地"),
+    BOTH("BOTH", "双向");
+
+    private final String code;
+    private final String desc;
+
+    public static SyncDirection fromCode(String code) {
+        for (SyncDirection direction : values()) {
+            if (direction.code.equalsIgnoreCase(code)) {
+                return direction;
+            }
+        }
+        throw new IllegalArgumentException("未知同步方向: " + code);
+    }
+}

+ 40 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncTypeEnum.java

@@ -0,0 +1,40 @@
+package com.usky.issue.service.enums;
+
+import lombok.Getter;
+
+/**
+ * 同步数据类型
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Getter
+public enum SyncTypeEnum {
+
+    TENANT("TENANT", "租户", false),
+    USER("USER", "用户", false),
+    ALARM("ALARM", "告警", false),
+    BUILDING("BUILDING", "建筑工程数据", true),
+    IOT_FACILITY("IOT_FACILITY", "物联设施数据", true),
+    IOT_MONITOR("IOT_MONITOR", "物联监测数据", true);
+
+    private final String code;
+    private final String label;
+    /** 是否异步大批量同步 */
+    private final boolean async;
+
+    SyncTypeEnum(String code, String label, boolean async) {
+        this.code = code;
+        this.label = label;
+        this.async = async;
+    }
+
+    public static SyncTypeEnum fromCode(String code) {
+        for (SyncTypeEnum type : values()) {
+            if (type.code.equalsIgnoreCase(code)) {
+                return type;
+            }
+        }
+        throw new IllegalArgumentException("未知同步类型: " + code);
+    }
+}

+ 25 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/TaskStatusEnum.java

@@ -0,0 +1,25 @@
+package com.usky.issue.service.enums;
+
+import lombok.Getter;
+
+/**
+ * 同步任务状态
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Getter
+public enum TaskStatusEnum {
+
+    PENDING("PENDING"),
+    RUNNING("RUNNING"),
+    SUCCESS("SUCCESS"),
+    FAILED("FAILED"),
+    CANCELLED("CANCELLED");
+
+    private final String code;
+
+    TaskStatusEnum(String code) {
+        this.code = code;
+    }
+}

+ 26 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/TriggerEventCodeEnum.java

@@ -0,0 +1,26 @@
+package com.usky.issue.service.enums;
+
+import lombok.Getter;
+
+/**
+ * 触发事件编码
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Getter
+public enum TriggerEventCodeEnum {
+
+    SYNC_ORG("SYNC_ORG", "启用同步组织", "新增、删除、修改组织信息时触发同步"),
+    SYNC_USER("SYNC_USER", "启用同步用户", "新增、删除、修改用户信息时触发同步");
+
+    private final String code;
+    private final String name;
+    private final String description;
+
+    TriggerEventCodeEnum(String code, String name, String description) {
+        this.code = code;
+        this.name = name;
+        this.description = description;
+    }
+}

+ 21 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/exception/CloudIntegrationErrorCode.java

@@ -0,0 +1,21 @@
+package com.usky.issue.service.exception;
+
+/**
+ * 云平台集成错误码
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+public final class CloudIntegrationErrorCode {
+
+    private CloudIntegrationErrorCode() {
+    }
+
+    public static final int CONFIG_NOT_FOUND = 40001;
+    public static final int CONFIG_VERSION_CONFLICT = 40002;
+    public static final int CONFIG_DISABLED = 40003;
+    public static final int SYNC_IN_PROGRESS = 40004;
+    public static final int SYNC_TYPE_INVALID = 40005;
+    public static final int TASK_NOT_FOUND = 40006;
+    public static final int CONNECTION_TEST_FAILED = 40007;
+}

+ 91 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudAuthServiceImpl.java

@@ -0,0 +1,91 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.mapper.IssueCloudConfigMapper;
+import com.usky.issue.service.CloudAuthService;
+import com.usky.issue.service.util.AesGcmCipher;
+import com.usky.issue.service.util.HmacSignUtil;
+import com.usky.issue.service.vo.SyncPacket;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+/**
+ * 云平台认证服务实现
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Slf4j
+@Service
+public class CloudAuthServiceImpl implements CloudAuthService {
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    @Autowired
+    private IssueCloudConfigMapper configMapper;
+    @Autowired
+    private AesGcmCipher aesGcmCipher;
+
+    @Override
+    public boolean validateToken(String apiKey, Integer tenantId) {
+        if (!StringUtils.hasText(apiKey) || tenantId == null) {
+            return false;
+        }
+        IssueCloudConfig config = findActiveConfig(tenantId);
+        if (config == null) {
+            return false;
+        }
+        return matchCredential(apiKey, config);
+    }
+
+    @Override
+    public boolean validate(String apiKey, String sign, SyncPacket packet) {
+        if (!validateToken(apiKey, parseTenantId(packet))) {
+            return false;
+        }
+        if (!StringUtils.hasText(sign) || packet == null) {
+            return false;
+        }
+        try {
+            String expected = HmacSignUtil.sign(OBJECT_MAPPER.writeValueAsString(packet), apiKey);
+            return expected.equals(sign);
+        } catch (Exception e) {
+            log.warn("签名校验失败: {}", e.getMessage());
+            return false;
+        }
+    }
+
+    private Integer parseTenantId(SyncPacket packet) {
+        if (packet == null || !StringUtils.hasText(packet.getTenantId())) {
+            return null;
+        }
+        try {
+            return Integer.valueOf(packet.getTenantId());
+        } catch (NumberFormatException ex) {
+            return null;
+        }
+    }
+
+    private IssueCloudConfig findActiveConfig(Integer tenantId) {
+        return configMapper.selectOne(new QueryWrapper<IssueCloudConfig>()
+                .eq("is_deleted", 0)
+                .eq("tenant_id", tenantId)
+                .eq("status", 1)
+                .orderByDesc("id")
+                .last("LIMIT 1"));
+    }
+
+    private boolean matchCredential(String apiKey, IssueCloudConfig config) {
+        try {
+            String decrypted = aesGcmCipher.decrypt(config.getCredentialKey());
+            return apiKey.equals(decrypted);
+        } catch (Exception e) {
+            log.warn("凭证解密失败 tenantId={}", config.getTenantId());
+            return false;
+        }
+    }
+}

+ 346 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudConfigServiceImpl.java

@@ -0,0 +1,346 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.domain.IssueSyncStatus;
+import com.usky.issue.domain.IssueSyncTask;
+import com.usky.issue.domain.IssueTriggerEvent;
+import com.usky.issue.mapper.IssueCloudConfigMapper;
+import com.usky.issue.mapper.IssueSyncStatusMapper;
+import com.usky.issue.mapper.IssueSyncTaskMapper;
+import com.usky.issue.mapper.IssueTriggerEventMapper;
+import com.usky.issue.service.CloudConfigService;
+import com.usky.issue.service.CloudOperationLogService;
+import com.usky.issue.service.client.CloudPlatformClient;
+import com.usky.issue.service.constant.CloudIntegrationConstants;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import com.usky.issue.service.enums.TaskStatusEnum;
+import com.usky.issue.service.enums.TriggerEventCodeEnum;
+import com.usky.issue.service.support.CloudEntitySupport;
+import com.usky.issue.service.util.AesGcmCipher;
+import com.usky.issue.service.util.CredentialMaskUtil;
+import com.usky.issue.service.vo.CloudConfigResponse;
+import com.usky.issue.service.vo.CloudConfigSaveRequest;
+import com.usky.issue.service.vo.CloudConnectionTestRequest;
+import com.usky.issue.service.vo.CloudConnectionTestResponse;
+import com.usky.issue.service.vo.CloudConnectionTestResult;
+import com.usky.issue.service.vo.CloudDisableResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 云平台配置服务实现
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Service
+public class CloudConfigServiceImpl implements CloudConfigService {
+
+    @Autowired
+    private IssueCloudConfigMapper configMapper;
+    @Autowired
+    private IssueSyncStatusMapper syncStatusMapper;
+    @Autowired
+    private IssueTriggerEventMapper triggerEventMapper;
+    @Autowired
+    private IssueSyncTaskMapper syncTaskMapper;
+    @Autowired
+    private AesGcmCipher aesGcmCipher;
+    @Autowired
+    private CloudPlatformClient cloudPlatformClient;
+    @Autowired
+    private CloudOperationLogService operationLogService;
+
+    @Override
+    public CloudConfigResponse getConfig() {
+        IssueCloudConfig config = findActiveConfig();
+        if (config == null) {
+            return null;
+        }
+        return toResponse(config);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public CloudConfigResponse saveConfig(CloudConfigSaveRequest request) {
+        Integer tenantId = SecurityUtils.getTenantId();
+        String operator = CloudEntitySupport.resolveOperator();
+        String requestIp = CloudEntitySupport.resolveRequestIp();
+
+        if (!request.getTenantId().equals(tenantId)) {
+            throw new BusinessException("输入租户ID与登录租户不一致,请确认后重试!");
+        }
+
+        IssueCloudConfig existing = findActiveConfig();
+        if (existing == null) {
+            IssueCloudConfig created = new IssueCloudConfig();
+            created.setTenantId(request.getTenantId());
+            created.setCredentialKey(aesGcmCipher.encrypt(request.getCredentialKey()));
+            created.setStatus(1);
+            created.setConnectionStatus(0);
+            created.setVersion(1);
+            CloudEntitySupport.fillOnInsert(created);
+            configMapper.insert(created);
+            initSyncStatusRows(created.getId(), created.getTenantId());
+            initTriggerEvents(created.getId(), created.getTenantId());
+            operationLogService.log(created.getId(), CloudIntegrationConstants.OPERATION_SAVE_CONFIG,
+                    "新建配置 tenantId=" + request.getTenantId(), operator, requestIp);
+            return toResponse(created);
+        }
+        Integer version = existing.getVersion();
+        if (request.getVersion() != null && !request.getVersion().equals(version)) {
+            throw new BusinessException("配置版本冲突,请刷新后重试");
+        }
+        existing.setTenantId(request.getTenantId());
+        existing.setCredentialKey(aesGcmCipher.encrypt(request.getCredentialKey()));
+        existing.setStatus(1);
+        existing.setVersion(version + 1);
+        CloudEntitySupport.fillOnUpdate(existing);
+        int updated = configMapper.updateById(existing);
+        if (updated == 0) {
+            throw new BusinessException("配置版本冲突,请刷新后重试");
+        }
+        operationLogService.log(existing.getId(), CloudIntegrationConstants.OPERATION_SAVE_CONFIG,
+                "更新配置 tenantId=" + CredentialMaskUtil.mask(String.valueOf(request.getTenantId())), operator, requestIp);
+        return toResponse(configMapper.selectById(existing.getId()));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public CloudConnectionTestResponse testConnection(CloudConnectionTestRequest request) {
+        String operator = CloudEntitySupport.resolveOperator();
+        String requestIp = CloudEntitySupport.resolveRequestIp();
+        Integer tenantId = request.getTenantId();
+        Integer loginTenantId = SecurityUtils.getTenantId();
+        if (loginTenantId != null && !tenantId.equals(loginTenantId)) {
+            throw new BusinessException("输入租户ID与登录租户不一致,请确认后重试!");
+        }
+
+        IssueCloudConfig config = findConfigByTenantId(tenantId);
+        if (config == null) {
+            throw new BusinessException("云平台配置不存在,请先保存配置");
+        }
+        if (config.getStatus() != null && config.getStatus() == 0) {
+            throw new BusinessException("云平台集成已禁用");
+        }
+        validateCredentialKey(config, request.getCredentialKey());
+
+        CloudConnectionTestResult result = cloudPlatformClient.testConnection(
+                request.getCloudAddress(), request.getTenantId(), request.getCredentialKey(),
+                SecurityUtils.getToken());
+
+        config.setConnectionStatus(result.isSuccess() ? 1 : 2);
+        config.setLastTestTime(LocalDateTime.now());
+        CloudEntitySupport.fillOnUpdate(config);
+        configMapper.updateById(config);
+        operationLogService.log(config.getId(), CloudIntegrationConstants.OPERATION_TEST_CONNECTION,
+                "连接测试" + (result.isSuccess() ? "成功" : "失败") + ":" + result.getMessage(), operator, requestIp);
+        return toConnectionTestResponse(result);
+    }
+
+    private void validateCredentialKey(IssueCloudConfig config, String requestKey) {
+        try {
+            String storedKey = aesGcmCipher.decrypt(config.getCredentialKey());
+            if (!storedKey.equals(requestKey)) {
+                throw new BusinessException("凭证密钥与已保存配置不一致");
+            }
+        } catch (BusinessException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            throw new BusinessException("凭证密钥校验失败");
+        }
+    }
+
+    @Override
+    public CloudConnectionTestResponse validateTenantConnection(Integer tenantId) {
+        CloudConnectionTestResponse response = new CloudConnectionTestResponse();
+        if (tenantId == null) {
+            response.setSuccess(false);
+            response.setConnectionStatus(2);
+            response.setMessage("租户ID不能为空");
+            response.setNetworkReachable(true);
+            response.setAuthValid(false);
+            response.setTenantExists(false);
+            return response;
+        }
+
+        IssueCloudConfig config = findConfigByTenantId(tenantId);
+        if (config == null) {
+            response.setSuccess(false);
+            response.setConnectionStatus(2);
+            response.setMessage(CloudIntegrationConstants.MSG_TENANT_CONFIG_MISSING);
+            response.setNetworkReachable(true);
+            response.setAuthValid(true);
+            response.setTenantExists(false);
+            return response;
+        }
+        if (config.getStatus() != null && config.getStatus() == 0) {
+            response.setSuccess(false);
+            response.setConnectionStatus(2);
+            response.setMessage("租户配置已禁用");
+            response.setNetworkReachable(true);
+            response.setAuthValid(true);
+            response.setTenantExists(false);
+            return response;
+        }
+
+        response.setSuccess(true);
+        response.setConnectionStatus(1);
+        response.setMessage(CloudIntegrationConstants.MSG_CONNECTION_SUCCESS);
+        response.setNetworkReachable(true);
+        response.setAuthValid(true);
+        response.setTenantExists(true);
+        return response;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public CloudDisableResponse disable(Integer status) {
+        if (status == null || (status != 0 && status != 1)) {
+            throw new BusinessException("status 只能为 0(禁用)或 1(启用)");
+        }
+        String operator = CloudEntitySupport.resolveOperator();
+        String requestIp = CloudEntitySupport.resolveRequestIp();
+        IssueCloudConfig config = requireConfig();
+
+        int affected = 0;
+        config.setStatus(status);
+        CloudEntitySupport.fillOnUpdate(config);
+        configMapper.updateById(config);
+
+        if (status == 0) {
+            affected = cancelRunningTasks(config.getId());
+            operationLogService.log(config.getId(), CloudIntegrationConstants.OPERATION_DISABLE,
+                    "禁用集成,终止任务数=" + affected, operator, requestIp);
+        } else {
+            operationLogService.log(config.getId(), CloudIntegrationConstants.OPERATION_ENABLE,
+                    "启用集成", operator, requestIp);
+        }
+
+        CloudDisableResponse response = new CloudDisableResponse();
+        response.setDisabled(status == 0);
+        response.setAffectedTasks(affected);
+        return response;
+    }
+
+    private IssueCloudConfig requireConfig() {
+        IssueCloudConfig config = findActiveConfig();
+        if (config == null) {
+            throw new BusinessException("云平台配置不存在,请先保存配置");
+        }
+        return config;
+    }
+
+    @Override
+    public IssueCloudConfig requireActiveConfig() {
+        IssueCloudConfig config = findActiveConfig();
+        if (config == null) {
+            throw new BusinessException("云平台配置不存在,请先保存配置");
+        }
+        if (config.getStatus() != null && config.getStatus() == 0) {
+            throw new BusinessException("云平台集成已禁用");
+        }
+        return config;
+    }
+
+    private IssueCloudConfig findActiveConfig() {
+        return findConfigByTenantId(SecurityUtils.getTenantId());
+    }
+
+    private IssueCloudConfig findConfigByTenantId(Integer tenantId) {
+        if (tenantId == null) {
+            return null;
+        }
+        return configMapper.selectOne(new QueryWrapper<IssueCloudConfig>()
+                .eq("is_deleted", 0)
+                .eq("tenant_id", tenantId)
+                .orderByDesc("id")
+                .last("LIMIT 1"));
+    }
+
+    private CloudConnectionTestResponse toConnectionTestResponse(CloudConnectionTestResult result) {
+        CloudConnectionTestResponse response = new CloudConnectionTestResponse();
+        response.setSuccess(result.isSuccess());
+        response.setConnectionStatus(result.isSuccess() ? 1 : 2);
+        response.setMessage(result.getMessage());
+        response.setNetworkReachable(result.isNetworkReachable());
+        response.setAuthValid(result.isAuthValid());
+        response.setTenantExists(result.isTenantExists());
+        response.setCostTime(result.getCostTime());
+        return response;
+    }
+
+    private int cancelRunningTasks(Long configId) {
+        List<IssueSyncTask> running = syncTaskMapper.selectList(new QueryWrapper<IssueSyncTask>()
+                .eq("config_id", configId)
+                .eq("is_deleted", 0)
+                .in("task_status",
+                        TaskStatusEnum.PENDING.getCode(), TaskStatusEnum.RUNNING.getCode()));
+        for (IssueSyncTask task : running) {
+            task.setTaskStatus(TaskStatusEnum.CANCELLED.getCode());
+            task.setFinishTime(LocalDateTime.now());
+            task.setErrorSummary("集成已禁用,任务终止");
+            CloudEntitySupport.fillOnUpdate(task);
+            syncTaskMapper.updateById(task);
+        }
+        return running.size();
+    }
+
+    private void initSyncStatusRows(Long configId, Integer tenantId) {
+        for (SyncTypeEnum type : SyncTypeEnum.values()) {
+            IssueSyncStatus row = new IssueSyncStatus();
+            row.setConfigId(configId);
+            row.setTenantId(tenantId);
+            row.setSyncType(type.getCode());
+            row.setTypeName(type.getLabel());
+            row.setTotalCount(0);
+            row.setSuccessCount(0);
+            row.setFailureCount(0);
+            row.setUnsyncedCount(0);
+            CloudEntitySupport.fillOnInsert(row);
+            syncStatusMapper.insert(row);
+        }
+    }
+
+    private void initTriggerEvents(Long configId, Integer tenantId) {
+        Arrays.stream(TriggerEventCodeEnum.values()).forEach(code -> {
+            IssueTriggerEvent event = new IssueTriggerEvent();
+            event.setConfigId(configId);
+            event.setTenantId(tenantId);
+            event.setEventCode(code.getCode());
+            event.setEventName(code.getName());
+            event.setDescription(code.getDescription());
+            event.setEnabled(0);
+            CloudEntitySupport.fillOnInsert(event);
+            triggerEventMapper.insert(event);
+        });
+    }
+
+    private CloudConfigResponse toResponse(IssueCloudConfig config) {
+        CloudConfigResponse resp = new CloudConfigResponse();
+        resp.setId(config.getId());
+        resp.setTenantId(String.valueOf(config.getTenantId()));
+        try {
+            resp.setCredentialKey(aesGcmCipher.decrypt(config.getCredentialKey()));
+        } catch (Exception e) {
+            resp.setCredentialKey(null);
+        }
+        resp.setStatus(config.getStatus());
+        resp.setConnectionStatus(config.getConnectionStatus());
+        resp.setVersion(config.getVersion());
+        resp.setLastTestTime(config.getLastTestTime());
+        resp.setUpdatedTime(config.getUpdatedTime());
+        resp.setUpdatedBy(config.getUpdatedBy());
+        resp.setCreatedTime(config.getCreatedTime());
+        resp.setCreatedBy(config.getCreatedBy());
+        return resp;
+    }
+}

+ 42 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudOperationLogServiceImpl.java

@@ -0,0 +1,42 @@
+package com.usky.issue.service.impl;
+
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.domain.IssueOperationLog;
+import com.usky.issue.mapper.IssueCloudConfigMapper;
+import com.usky.issue.mapper.IssueOperationLogMapper;
+import com.usky.issue.service.CloudOperationLogService;
+import com.usky.issue.service.support.CloudEntitySupport;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 操作审计日志实现
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Service
+public class CloudOperationLogServiceImpl implements CloudOperationLogService {
+
+    @Autowired
+    private IssueOperationLogMapper operationLogMapper;
+
+    @Autowired
+    private IssueCloudConfigMapper configMapper;
+
+    @Override
+    public void log(Long configId, String operationType, String detail, String operator, String requestIp) {
+        IssueOperationLog log = new IssueOperationLog();
+        log.setConfigId(configId);
+        IssueCloudConfig config = configMapper.selectById(configId);
+        if (config != null) {
+            log.setTenantId(config.getTenantId());
+        }
+        log.setOperationType(operationType);
+        log.setOperationDetail(detail);
+        log.setRequestIp(requestIp);
+        CloudEntitySupport.fillOnInsert(log);
+        log.setCreatedBy(operator);
+        operationLogMapper.insert(log);
+    }
+}

+ 162 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncReceiveServiceImpl.java

@@ -0,0 +1,162 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.issue.domain.IssueSyncData;
+import com.usky.issue.domain.IssueSyncDownQueue;
+import com.usky.issue.mapper.IssueSyncDataMapper;
+import com.usky.issue.mapper.IssueSyncDownQueueMapper;
+import com.usky.issue.service.CloudConfigService;
+import com.usky.issue.service.CloudSyncReceiveService;
+import com.usky.issue.service.enums.SyncDirection;
+import com.usky.issue.service.support.CloudEntitySupport;
+import com.usky.issue.service.vo.SyncPacket;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 云端同步接收服务实现
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Slf4j
+@Service
+public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
+
+    private static final int POLL_BATCH_SIZE = 100;
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    @Autowired
+    private IssueSyncDataMapper syncDataMapper;
+    @Autowired
+    private IssueSyncDownQueueMapper downQueueMapper;
+    @Autowired
+    private CloudConfigService cloudConfigService;
+
+    @Override
+    public boolean validateTenant(String tenantId) {
+        if (!StringUtils.hasText(tenantId)) {
+            return false;
+        }
+        return cloudConfigService.validateTenantConnection(Integer.valueOf(tenantId)).isSuccess();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int batchUpsert(SyncPacket packet) {
+        if (packet == null || packet.getDataList() == null || packet.getDataList().isEmpty()) {
+            return 0;
+        }
+        int count = 0;
+        for (Object item : packet.getDataList()) {
+            try {
+                IssueSyncData incoming = convertToSyncData(item, packet.getTableName());
+                if (incoming == null) {
+                    continue;
+                }
+                IssueSyncData exist = syncDataMapper.selectOne(new LambdaQueryWrapper<IssueSyncData>()
+                        .eq(IssueSyncData::getTenantId, incoming.getTenantId())
+                        .eq(IssueSyncData::getTableName, incoming.getTableName())
+                        .eq(IssueSyncData::getDataKey, incoming.getDataKey())
+                        .eq(IssueSyncData::getIsDeleted, 0));
+                if (exist == null) {
+                    incoming.setSyncStatus(1);
+                    incoming.setSyncDirection("LOCAL");
+                    CloudEntitySupport.fillOnInsert(incoming);
+                    syncDataMapper.insert(incoming);
+                    count++;
+                } else if (incoming.getSyncVersion() >= exist.getSyncVersion()) {
+                    incoming.setId(exist.getId());
+                    incoming.setSyncStatus(1);
+                    incoming.setSyncDirection("LOCAL");
+                    CloudEntitySupport.fillOnUpdate(incoming);
+                    syncDataMapper.updateById(incoming);
+                    count++;
+                }
+            } catch (Exception e) {
+                log.error("云端写入同步数据失败", e);
+            }
+        }
+        return count;
+    }
+
+    @Override
+    public SyncPacket pollDownQueue(String tenantId, String tableName, Long lastVersion) {
+        if (lastVersion == null) {
+            lastVersion = 0L;
+        }
+        Integer tenant = Integer.valueOf(tenantId);
+        List<IssueSyncDownQueue> queueList = downQueueMapper.selectList(new LambdaQueryWrapper<IssueSyncDownQueue>()
+                .eq(IssueSyncDownQueue::getTenantId, tenant)
+                .eq(IssueSyncDownQueue::getTableName, tableName)
+                .eq(IssueSyncDownQueue::getStatus, 0)
+                .gt(IssueSyncDownQueue::getSyncVersion, lastVersion)
+                .eq(IssueSyncDownQueue::getIsDeleted, 0)
+                .orderByAsc(IssueSyncDownQueue::getSyncVersion)
+                .last("LIMIT " + POLL_BATCH_SIZE));
+
+        if (queueList.isEmpty()) {
+            return null;
+        }
+
+        Long maxVersion = queueList.stream()
+                .mapToLong(IssueSyncDownQueue::getSyncVersion)
+                .max()
+                .orElse(lastVersion);
+
+        return SyncPacket.builder()
+                .tenantId(tenantId)
+                .tableName(tableName)
+                .direction(SyncDirection.DOWN)
+                .dataList(Collections.unmodifiableList(queueList))
+                .batchVersion(maxVersion)
+                .hasMore(queueList.size() >= POLL_BATCH_SIZE)
+                .build();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void ackDownQueue(List<Long> queueIds) {
+        if (queueIds == null || queueIds.isEmpty()) {
+            return;
+        }
+        downQueueMapper.update(null, new LambdaUpdateWrapper<IssueSyncDownQueue>()
+                .in(IssueSyncDownQueue::getId, queueIds)
+                .set(IssueSyncDownQueue::getStatus, 1));
+    }
+
+    @Override
+    public int countPendingDownQueue(Integer tenantId, String tableName) {
+        Integer count = downQueueMapper.selectCount(new LambdaQueryWrapper<IssueSyncDownQueue>()
+                .eq(IssueSyncDownQueue::getTenantId, tenantId)
+                .eq(IssueSyncDownQueue::getTableName, tableName)
+                .eq(IssueSyncDownQueue::getStatus, 0)
+                .eq(IssueSyncDownQueue::getIsDeleted, 0));
+        return count == null ? 0 : count;
+    }
+
+    @SuppressWarnings("unchecked")
+    private IssueSyncData convertToSyncData(Object item, String tableName) throws Exception {
+        if (item instanceof IssueSyncData) {
+            return (IssueSyncData) item;
+        }
+        Map<String, Object> map = OBJECT_MAPPER.convertValue(item, Map.class);
+        IssueSyncData data = new IssueSyncData();
+        data.setTenantId(Integer.valueOf(String.valueOf(map.get("tenantId"))));
+        data.setTableName(tableName);
+        data.setDataKey(String.valueOf(map.get("dataKey")));
+        data.setPayload(map.containsKey("payload") ? String.valueOf(map.get("payload")) : OBJECT_MAPPER.writeValueAsString(map));
+        Object version = map.get("syncVersion");
+        data.setSyncVersion(version == null ? System.currentTimeMillis() : Long.valueOf(String.valueOf(version)));
+        return data;
+    }
+}

+ 190 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncServiceImpl.java

@@ -0,0 +1,190 @@
+package com.usky.issue.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.common.redis.core.RedisHelper;
+import com.usky.issue.service.constant.CloudIntegrationConstants;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.domain.IssueSyncStatus;
+import com.usky.issue.domain.IssueSyncTask;
+import com.usky.issue.domain.IssueTriggerEvent;
+import com.usky.issue.service.vo.SyncStatusResponse;
+import com.usky.issue.service.vo.SyncTaskResponse;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import com.usky.issue.service.enums.TriggerEventCodeEnum;
+import com.usky.issue.mapper.IssueSyncStatusMapper;
+import com.usky.issue.mapper.IssueSyncTaskMapper;
+import com.usky.issue.mapper.IssueTriggerEventMapper;
+import com.usky.issue.service.CloudConfigService;
+import com.usky.issue.service.CloudOperationLogService;
+import com.usky.issue.service.CloudSyncService;
+import com.usky.issue.service.CloudSyncTaskRunner;
+import com.usky.issue.service.support.CloudEntitySupport;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * 云平台数据同步服务实现
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Slf4j
+@Service
+public class CloudSyncServiceImpl implements CloudSyncService {
+
+    @Autowired
+    private IssueSyncStatusMapper syncStatusMapper;
+    @Autowired
+    private IssueSyncTaskMapper syncTaskMapper;
+    @Autowired
+    private IssueTriggerEventMapper triggerEventMapper;
+    @Autowired
+    private CloudConfigService cloudConfigService;
+    @Autowired
+    private CloudOperationLogService operationLogService;
+    @Autowired
+    private CloudSyncTaskRunner cloudSyncTaskRunner;
+    @Autowired
+    private RedisHelper redisHelper;
+    @Autowired
+    @Qualifier("cloudSyncExecutor")
+    private Executor cloudSyncExecutor;
+
+    @Override
+    public List<SyncStatusResponse> listSyncStatus() {
+        IssueCloudConfig config = cloudConfigService.requireActiveConfig();
+        String cacheKey = CloudIntegrationConstants.CACHE_STATUS_PREFIX + config.getId();
+        Object cached = redisHelper.get(cacheKey);
+        if (cached != null) {
+            return JSON.parseArray(cached.toString(), SyncStatusResponse.class);
+        }
+        List<SyncStatusResponse> list = loadStatusFromDb(config.getId());
+        redisHelper.set(cacheKey, JSON.toJSONString(list),
+                CloudIntegrationConstants.CACHE_STATUS_TTL_SECONDS, TimeUnit.SECONDS);
+        return list;
+    }
+
+    @Override
+    public SyncTaskResponse triggerManualSync(String syncType) {
+        String operator = CloudEntitySupport.resolveOperator();
+        String requestIp = CloudEntitySupport.resolveRequestIp();
+        SyncTypeEnum typeEnum;
+        try {
+            typeEnum = SyncTypeEnum.fromCode(syncType);
+        } catch (IllegalArgumentException ex) {
+            throw new BusinessException("无效的同步类型: " + syncType);
+        }
+        IssueCloudConfig config = cloudConfigService.requireActiveConfig();
+        String lockKey = CloudIntegrationConstants.LOCK_SYNC_PREFIX + typeEnum.getCode();
+        String lockValue = UUID.randomUUID().toString();
+        boolean locked = redisHelper.lock(lockKey, lockValue, CloudIntegrationConstants.LOCK_SYNC_SECONDS);
+        if (!locked) {
+            throw new BusinessException("该类型同步正在进行中,请稍后再试");
+        }
+        try {
+            IssueSyncTask task = cloudSyncTaskRunner.createTask(config.getId(), typeEnum, "MANUAL");
+            operationLogService.log(config.getId(), CloudIntegrationConstants.OPERATION_MANUAL_SYNC,
+                    "手动同步 type=" + typeEnum.getCode(), operator, requestIp);
+            redisHelper.delete(CloudIntegrationConstants.CACHE_STATUS_PREFIX + config.getId());
+            if (typeEnum.isAsync()) {
+                cloudSyncExecutor.execute(() -> cloudSyncTaskRunner.executeSyncTask(task.getId(), typeEnum));
+                return toTaskResponse(syncTaskMapper.selectById(task.getId()));
+            }
+            cloudSyncTaskRunner.executeSyncTask(task.getId(), typeEnum);
+            return toTaskResponse(syncTaskMapper.selectById(task.getId()));
+        } finally {
+            redisHelper.unlockLua(lockKey, lockValue);
+        }
+    }
+
+    @Override
+    public SyncTaskResponse getTaskProgress(Long taskId) {
+        IssueSyncTask task = syncTaskMapper.selectById(taskId);
+        if (task == null || (task.getIsDeleted() != null && task.getIsDeleted() == 1)) {
+            throw new BusinessException("同步任务不存在");
+        }
+        return toTaskResponse(task);
+    }
+
+    @Override
+    public void onOrgChanged() {
+        fireEventIfEnabled(TriggerEventCodeEnum.SYNC_ORG, SyncTypeEnum.TENANT);
+    }
+
+    @Override
+    public void onUserChanged() {
+        fireEventIfEnabled(TriggerEventCodeEnum.SYNC_USER, SyncTypeEnum.USER);
+    }
+
+    private void fireEventIfEnabled(TriggerEventCodeEnum eventCode, SyncTypeEnum syncType) {
+        IssueCloudConfig config;
+        try {
+            config = cloudConfigService.requireActiveConfig();
+        } catch (BusinessException ex) {
+            log.debug("跳过事件同步: {}", ex.getMessage());
+            return;
+        }
+        IssueTriggerEvent event = triggerEventMapper.selectOne(new LambdaQueryWrapper<IssueTriggerEvent>()
+                .eq(IssueTriggerEvent::getConfigId, config.getId())
+                .eq(IssueTriggerEvent::getEventCode, eventCode.getCode())
+                .eq(IssueTriggerEvent::getIsDeleted, 0));
+        if (event == null || event.getEnabled() == null || event.getEnabled() == 0) {
+            return;
+        }
+        IssueSyncTask task = cloudSyncTaskRunner.createTask(config.getId(), syncType, "EVENT");
+        cloudSyncExecutor.execute(() -> cloudSyncTaskRunner.executeSyncTask(task.getId(), syncType));
+    }
+
+    private List<SyncStatusResponse> loadStatusFromDb(Long configId) {
+        return syncStatusMapper.selectList(new LambdaQueryWrapper<IssueSyncStatus>()
+                        .eq(IssueSyncStatus::getConfigId, configId)
+                        .eq(IssueSyncStatus::getIsDeleted, 0)
+                        .orderByAsc(IssueSyncStatus::getId))
+                .stream()
+                .map(this::toStatusResponse)
+                .collect(Collectors.toList());
+    }
+
+    private SyncStatusResponse toStatusResponse(IssueSyncStatus status) {
+        SyncStatusResponse resp = new SyncStatusResponse();
+        resp.setSyncType(status.getSyncType());
+        resp.setTypeName(status.getTypeName());
+        resp.setTotalCount(status.getTotalCount());
+        resp.setSuccessCount(status.getSuccessCount());
+        resp.setFailureCount(status.getFailureCount());
+        resp.setUnsyncedCount(status.getUnsyncedCount());
+        resp.setLastSyncTime(status.getLastSyncTime());
+        return resp;
+    }
+
+    private SyncTaskResponse toTaskResponse(IssueSyncTask task) {
+        SyncTaskResponse resp = new SyncTaskResponse();
+        resp.setTaskId(task.getId());
+        resp.setSyncType(task.getSyncType());
+        resp.setTaskStatus(task.getTaskStatus());
+        resp.setTriggerMode(task.getTriggerMode());
+        resp.setTotalCount(task.getTotalCount());
+        resp.setProcessedCount(task.getProcessedCount());
+        resp.setSuccessCount(task.getSuccessCount());
+        resp.setFailureCount(task.getFailureCount());
+        resp.setErrorSummary(task.getErrorSummary());
+        resp.setStartTime(task.getStartTime());
+        resp.setFinishTime(task.getFinishTime());
+        if (task.getTotalCount() != null && task.getTotalCount() > 0 && task.getProcessedCount() != null) {
+            resp.setProgressPercent(task.getProcessedCount() * 100 / task.getTotalCount());
+        } else {
+            resp.setProgressPercent(0);
+        }
+        return resp;
+    }
+}

+ 148 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncTaskRunnerImpl.java

@@ -0,0 +1,148 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.issue.domain.IssueSyncDetail;
+import com.usky.issue.domain.IssueSyncStatus;
+import com.usky.issue.domain.IssueSyncTask;
+import com.usky.issue.mapper.IssueSyncDetailMapper;
+import com.usky.issue.mapper.IssueSyncStatusMapper;
+import com.usky.issue.mapper.IssueSyncTaskMapper;
+import com.usky.issue.service.CloudSyncTaskRunner;
+import com.usky.issue.service.LocalSyncAgent;
+import com.usky.issue.service.constant.CloudIntegrationConstants;
+import com.usky.issue.service.enums.SyncDirection;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import com.usky.issue.service.enums.TaskStatusEnum;
+import com.usky.issue.service.support.CloudEntitySupport;
+import com.usky.common.redis.core.RedisHelper;
+import com.usky.issue.service.vo.SyncResult;
+import org.springframework.beans.factory.annotation.Autowired;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.mapper.IssueCloudConfigMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+
+/**
+ * 同步任务执行器实现
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Service
+public class CloudSyncTaskRunnerImpl implements CloudSyncTaskRunner {
+
+    @Autowired
+    private IssueSyncTaskMapper syncTaskMapper;
+    @Autowired
+    private IssueSyncDetailMapper syncDetailMapper;
+    @Autowired
+    private IssueSyncStatusMapper syncStatusMapper;
+    @Autowired
+    private IssueCloudConfigMapper configMapper;
+    @Autowired
+    private LocalSyncAgent localSyncAgent;
+    @Autowired
+    private RedisHelper redisHelper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public IssueSyncTask createTask(Long configId, SyncTypeEnum type, String triggerMode) {
+        IssueCloudConfig config = configMapper.selectById(configId);
+        int pending = localSyncAgent.countPending(type, config);
+        IssueSyncTask task = new IssueSyncTask();
+        task.setConfigId(configId);
+        if (config != null) {
+            task.setTenantId(config.getTenantId());
+        }
+        task.setSyncType(type.getCode());
+        task.setTaskStatus(TaskStatusEnum.PENDING.getCode());
+        task.setTriggerMode(triggerMode);
+        task.setTotalCount(pending);
+        task.setProcessedCount(0);
+        task.setSuccessCount(0);
+        task.setFailureCount(0);
+        CloudEntitySupport.fillOnInsert(task);
+        syncTaskMapper.insert(task);
+        return task;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void executeSyncTask(Long taskId, SyncTypeEnum type) {
+        IssueSyncTask task = syncTaskMapper.selectById(taskId);
+        if (task == null) {
+            return;
+        }
+        if (TaskStatusEnum.CANCELLED.getCode().equals(task.getTaskStatus())) {
+            return;
+        }
+        task.setTaskStatus(TaskStatusEnum.RUNNING.getCode());
+        task.setStartTime(LocalDateTime.now());
+        CloudEntitySupport.fillOnUpdate(task);
+        syncTaskMapper.updateById(task);
+
+        IssueCloudConfig config = configMapper.selectById(task.getConfigId());
+        SyncResult syncResult = localSyncAgent.executeSync(type, config, SyncDirection.BOTH, false);
+
+        IssueSyncTask current = syncTaskMapper.selectById(taskId);
+        if (current != null && TaskStatusEnum.CANCELLED.getCode().equals(current.getTaskStatus())) {
+            return;
+        }
+
+        task.setTotalCount(Math.max(task.getTotalCount(), syncResult.getTotalCount()));
+        task.setProcessedCount(syncResult.getTotalCount());
+        task.setSuccessCount(syncResult.getSuccessCount());
+        task.setFailureCount(syncResult.getFailureCount());
+        task.setTaskStatus(syncResult.getFailureCount() > 0 && syncResult.getSuccessCount() == 0
+                ? TaskStatusEnum.FAILED.getCode() : TaskStatusEnum.SUCCESS.getCode());
+        if (syncResult.getFailureCount() > 0) {
+            task.setErrorSummary(syncResult.getErrorMessage() != null
+                    ? syncResult.getErrorMessage()
+                    : "部分失败: " + syncResult.getFailureCount() + " 条");
+        }
+        task.setFinishTime(LocalDateTime.now());
+        CloudEntitySupport.fillOnUpdate(task);
+        syncTaskMapper.updateById(task);
+
+        recordSyncDetails(taskId, task.getTenantId(), syncResult);
+        refreshSyncStatus(task.getConfigId(), type, task);
+        redisHelper.delete(CloudIntegrationConstants.CACHE_STATUS_PREFIX + task.getConfigId());
+    }
+
+    private void recordSyncDetails(Long taskId, Integer tenantId, SyncResult syncResult) {
+        if (syncResult.getTotalCount() <= 0) {
+            return;
+        }
+        IssueSyncDetail detail = new IssueSyncDetail();
+        detail.setTaskId(taskId);
+        detail.setTenantId(tenantId);
+        detail.setDataId("batch-" + taskId);
+        detail.setRetryCount(0);
+        detail.setDetailStatus(syncResult.getFailureCount() > 0
+                ? TaskStatusEnum.FAILED.getCode() : TaskStatusEnum.SUCCESS.getCode());
+        if (syncResult.getFailureCount() > 0) {
+            detail.setErrorMessage(syncResult.getErrorMessage());
+        }
+        CloudEntitySupport.fillOnInsert(detail);
+        syncDetailMapper.insert(detail);
+    }
+
+    private void refreshSyncStatus(Long configId, SyncTypeEnum type, IssueSyncTask task) {
+        IssueSyncStatus status = syncStatusMapper.selectOne(new LambdaQueryWrapper<IssueSyncStatus>()
+                .eq(IssueSyncStatus::getConfigId, configId)
+                .eq(IssueSyncStatus::getSyncType, type.getCode())
+                .eq(IssueSyncStatus::getIsDeleted, 0));
+        if (status == null) {
+            return;
+        }
+        status.setTotalCount(task.getTotalCount());
+        status.setSuccessCount(task.getSuccessCount());
+        status.setFailureCount(task.getFailureCount());
+        status.setUnsyncedCount(Math.max(0, task.getTotalCount() - task.getProcessedCount()));
+        status.setLastSyncTime(LocalDateTime.now());
+        CloudEntitySupport.fillOnUpdate(status);
+        syncStatusMapper.updateById(status);
+    }
+}

+ 93 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudTriggerEventServiceImpl.java

@@ -0,0 +1,93 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.domain.IssueTriggerEvent;
+import com.usky.issue.mapper.IssueTriggerEventMapper;
+import com.usky.issue.service.CloudConfigService;
+import com.usky.issue.service.CloudOperationLogService;
+import com.usky.issue.service.CloudTriggerEventService;
+import com.usky.issue.service.constant.CloudIntegrationConstants;
+import com.usky.issue.service.enums.TriggerEventCodeEnum;
+import com.usky.issue.service.support.CloudEntitySupport;
+import com.usky.issue.service.vo.TriggerEventItemRequest;
+import com.usky.issue.service.vo.TriggerEventResponse;
+import com.usky.issue.service.vo.TriggerEventSaveRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 触发事件配置服务实现
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Service
+public class CloudTriggerEventServiceImpl implements CloudTriggerEventService {
+
+    @Autowired
+    private IssueTriggerEventMapper triggerEventMapper;
+    @Autowired
+    private CloudConfigService cloudConfigService;
+    @Autowired
+    private CloudOperationLogService operationLogService;
+
+    @Override
+    public List<TriggerEventResponse> listEvents() {
+        IssueCloudConfig config = cloudConfigService.requireActiveConfig();
+        return triggerEventMapper.selectList(new LambdaQueryWrapper<IssueTriggerEvent>()
+                        .eq(IssueTriggerEvent::getConfigId, config.getId())
+                        .eq(IssueTriggerEvent::getIsDeleted, 0))
+                .stream()
+                .map(this::toResponse)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void saveEvents(TriggerEventSaveRequest request, String operator, String requestIp) {
+        IssueCloudConfig config = cloudConfigService.requireActiveConfig();
+        triggerEventMapper.delete(new LambdaQueryWrapper<IssueTriggerEvent>()
+                .eq(IssueTriggerEvent::getConfigId, config.getId()));
+        for (TriggerEventItemRequest item : request.getEvents()) {
+            TriggerEventCodeEnum codeEnum = resolveEventCode(item.getEventCode());
+            if (codeEnum == null) {
+                throw new BusinessException("未知事件编码: " + item.getEventCode());
+            }
+            IssueTriggerEvent event = new IssueTriggerEvent();
+            event.setConfigId(config.getId());
+            event.setTenantId(config.getTenantId());
+            event.setEventCode(codeEnum.getCode());
+            event.setEventName(codeEnum.getName());
+            event.setDescription(codeEnum.getDescription());
+            event.setEnabled(Boolean.TRUE.equals(item.getEnabled()) ? 1 : 0);
+            CloudEntitySupport.fillOnInsert(event);
+            triggerEventMapper.insert(event);
+        }
+        operationLogService.log(config.getId(), CloudIntegrationConstants.OPERATION_SAVE_TRIGGER,
+                "保存触发事件 " + request.getEvents().size() + " 项", operator, requestIp);
+    }
+
+    private TriggerEventCodeEnum resolveEventCode(String eventCode) {
+        for (TriggerEventCodeEnum e : TriggerEventCodeEnum.values()) {
+            if (e.getCode().equalsIgnoreCase(eventCode) || e.name().equalsIgnoreCase(eventCode)) {
+                return e;
+            }
+        }
+        return null;
+    }
+
+    private TriggerEventResponse toResponse(IssueTriggerEvent event) {
+        TriggerEventResponse resp = new TriggerEventResponse();
+        resp.setEventCode(event.getEventCode());
+        resp.setEventName(event.getEventName());
+        resp.setDescription(event.getDescription());
+        resp.setEnabled(event.getEnabled() != null && event.getEnabled() == 1);
+        return resp;
+    }
+}

+ 185 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalPullServiceImpl.java

@@ -0,0 +1,185 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.domain.IssueSyncCursor;
+import com.usky.issue.domain.IssueSyncData;
+import com.usky.issue.domain.IssueSyncDownQueue;
+import com.usky.issue.mapper.IssueSyncCursorMapper;
+import com.usky.issue.mapper.IssueSyncDataMapper;
+import com.usky.issue.service.LocalPullService;
+import com.usky.issue.service.client.CloudPlatformClient;
+import com.usky.issue.service.enums.SyncDirection;
+import com.usky.issue.service.support.CloudEntitySupport;
+import com.usky.issue.service.vo.SyncPacket;
+import com.usky.issue.service.vo.SyncResponse;
+import com.usky.issue.service.vo.SyncResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 本地拉取服务实现
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Slf4j
+@Service
+public class LocalPullServiceImpl implements LocalPullService {
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    @Autowired
+    private IssueSyncDataMapper syncDataMapper;
+    @Autowired
+    private IssueSyncCursorMapper cursorMapper;
+    @Autowired
+    private CloudPlatformClient cloudPlatformClient;
+
+    @Override
+    public SyncResult pullAndApply(String tableName, IssueCloudConfig config) {
+        SyncResult result = SyncResult.empty();
+        List<IssueSyncCursor> cursors = cursorMapper.selectList(new LambdaQueryWrapper<IssueSyncCursor>()
+                .eq(IssueSyncCursor::getDirection, SyncDirection.DOWN.getCode())
+                .eq(IssueSyncCursor::getIsDeleted, 0));
+        if (cursors.isEmpty() && config.getTenantId() != null) {
+            result.merge(pullByTenant(config.getTenantId(), tableName, config));
+            return result;
+        }
+        for (IssueSyncCursor cursor : cursors) {
+            try {
+                result.merge(pullByTenant(cursor.getTenantId(), tableName, config));
+            } catch (Exception e) {
+                log.error("拉取租户{}数据失败", cursor.getTenantId(), e);
+                result.setFailureCount(result.getFailureCount() + 1);
+                result.setErrorMessage(e.getMessage());
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public SyncResult pullByTenant(Integer tenantId, String tableName, IssueCloudConfig config) {
+        SyncResult result = SyncResult.empty();
+        IssueSyncCursor cursor = getOrInitCursor(tenantId, tableName, SyncDirection.DOWN.getCode());
+        Long lastVersion = cursor.getLastSyncVersion() == null ? 0L : cursor.getLastSyncVersion();
+
+        while (true) {
+            SyncPacket packet = cloudPlatformClient.pollCloud(tenantId, tableName, lastVersion, config);
+            if (packet == null || packet.getDataList() == null || packet.getDataList().isEmpty()) {
+                break;
+            }
+
+            List<IssueSyncDownQueue> queueList = convertQueueList(packet.getDataList());
+            List<Long> ackIds = new ArrayList<>();
+            int success = 0;
+            int failure = 0;
+
+            for (IssueSyncDownQueue queue : queueList) {
+                try {
+                    applyToLocal(queue);
+                    ackIds.add(queue.getId());
+                    success++;
+                } catch (Exception e) {
+                    log.error("应用云端数据到本地失败, queueId={}", queue.getId(), e);
+                    failure++;
+                }
+            }
+
+            result.setTotalCount(result.getTotalCount() + queueList.size());
+            result.setSuccessCount(result.getSuccessCount() + success);
+            result.setFailureCount(result.getFailureCount() + failure);
+
+            lastVersion = packet.getBatchVersion();
+            updateCursor(tenantId, tableName, SyncDirection.DOWN.getCode(), lastVersion);
+            ackCloud(ackIds, config);
+
+            if (Boolean.FALSE.equals(packet.getHasMore())) {
+                break;
+            }
+        }
+        return result;
+    }
+
+    private void applyToLocal(IssueSyncDownQueue queue) throws Exception {
+        IssueSyncData data = OBJECT_MAPPER.readValue(queue.getPayload(), IssueSyncData.class);
+        if (data.getDataKey() == null) {
+            data.setDataKey(queue.getTableName() + "-" + queue.getId());
+        }
+        data.setTenantId(queue.getTenantId());
+        data.setTableName(queue.getTableName());
+        data.setSyncVersion(queue.getSyncVersion());
+        data.setSyncDirection("CLOUD");
+        data.setSyncStatus(1);
+
+        IssueSyncData exist = syncDataMapper.selectOne(new LambdaQueryWrapper<IssueSyncData>()
+                .eq(IssueSyncData::getTenantId, data.getTenantId())
+                .eq(IssueSyncData::getTableName, data.getTableName())
+                .eq(IssueSyncData::getDataKey, data.getDataKey())
+                .eq(IssueSyncData::getIsDeleted, 0));
+        if (exist == null) {
+            CloudEntitySupport.fillOnInsert(data);
+            syncDataMapper.insert(data);
+        } else if (queue.getSyncVersion() >= exist.getSyncVersion()) {
+            data.setId(exist.getId());
+            CloudEntitySupport.fillOnUpdate(data);
+            syncDataMapper.updateById(data);
+        }
+    }
+
+    private void ackCloud(List<Long> queueIds, IssueCloudConfig config) {
+        if (queueIds == null || queueIds.isEmpty()) {
+            return;
+        }
+        SyncResponse response = cloudPlatformClient.ackPoll(queueIds, config);
+        if (response == null || !response.isSuccess()) {
+            log.warn("ACK云端失败 queueIds={}", queueIds);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private List<IssueSyncDownQueue> convertQueueList(List<?> dataList) {
+        return dataList.stream().map(item -> {
+            if (item instanceof IssueSyncDownQueue) {
+                return (IssueSyncDownQueue) item;
+            }
+            return OBJECT_MAPPER.convertValue(item, IssueSyncDownQueue.class);
+        }).collect(Collectors.toList());
+    }
+
+    private IssueSyncCursor getOrInitCursor(Integer tenantId, String tableName, String direction) {
+        IssueSyncCursor cursor = cursorMapper.selectOne(new LambdaQueryWrapper<IssueSyncCursor>()
+                .eq(IssueSyncCursor::getTenantId, tenantId)
+                .eq(IssueSyncCursor::getTableName, tableName)
+                .eq(IssueSyncCursor::getDirection, direction)
+                .eq(IssueSyncCursor::getIsDeleted, 0));
+        if (cursor == null) {
+            cursor = new IssueSyncCursor();
+            cursor.setTenantId(tenantId);
+            cursor.setTableName(tableName);
+            cursor.setDirection(direction);
+            cursor.setLastSyncVersion(0L);
+            cursor.setLastSyncTime(LocalDateTime.now());
+            CloudEntitySupport.fillOnInsert(cursor);
+            cursorMapper.insert(cursor);
+        }
+        return cursor;
+    }
+
+    private void updateCursor(Integer tenantId, String tableName, String direction, Long version) {
+        cursorMapper.update(null, new LambdaUpdateWrapper<IssueSyncCursor>()
+                .eq(IssueSyncCursor::getTenantId, tenantId)
+                .eq(IssueSyncCursor::getTableName, tableName)
+                .eq(IssueSyncCursor::getDirection, direction)
+                .set(IssueSyncCursor::getLastSyncVersion, version)
+                .set(IssueSyncCursor::getLastSyncTime, LocalDateTime.now()));
+    }
+}

+ 176 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalPushServiceImpl.java

@@ -0,0 +1,176 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.domain.IssueSyncCursor;
+import com.usky.issue.domain.IssueSyncData;
+import com.usky.issue.mapper.IssueSyncCursorMapper;
+import com.usky.issue.mapper.IssueSyncDataMapper;
+import com.usky.issue.service.LocalPushService;
+import com.usky.issue.service.client.CloudPlatformClient;
+import com.usky.issue.service.enums.SyncDirection;
+import com.usky.issue.service.support.CloudEntitySupport;
+import com.usky.issue.service.vo.SyncPacket;
+import com.usky.issue.service.vo.SyncResponse;
+import com.usky.issue.service.vo.SyncResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/**
+ * 本地推送服务实现
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Slf4j
+@Service
+public class LocalPushServiceImpl implements LocalPushService {
+
+    @Value("${sync.push.batch-size:100}")
+    private int batchSize;
+
+    @Autowired
+    private IssueSyncDataMapper syncDataMapper;
+    @Autowired
+    private IssueSyncCursorMapper cursorMapper;
+    @Autowired
+    private CloudPlatformClient cloudPlatformClient;
+
+    @Override
+    public SyncResult pushAllPending(String tableName, IssueCloudConfig config) {
+        if (config == null || config.getTenantId() == null) {
+            return SyncResult.empty();
+        }
+        try {
+            return pushByTenant(config.getTenantId(), tableName, config);
+        } catch (Exception e) {
+            log.error("推送租户{}失败", config.getTenantId(), e);
+            SyncResult result = SyncResult.empty();
+            result.setFailureCount(1);
+            result.setErrorMessage(e.getMessage());
+            return result;
+        }
+    }
+
+    @Override
+    public SyncResult pushByTenant(Integer tenantId, String tableName, IssueCloudConfig config) {
+        SyncResult result = SyncResult.empty();
+        IssueSyncCursor cursor = getOrInitCursor(tenantId, tableName, SyncDirection.UP.getCode());
+        Long lastVersion = cursor.getLastSyncVersion() == null ? 0L : cursor.getLastSyncVersion();
+
+        while (true) {
+            List<IssueSyncData> dataList = syncDataMapper.selectList(new LambdaQueryWrapper<IssueSyncData>()
+                    .eq(IssueSyncData::getTenantId, tenantId)
+                    .eq(IssueSyncData::getTableName, tableName)
+                    .in(IssueSyncData::getSyncStatus, 0, 2)
+                    .gt(IssueSyncData::getSyncVersion, lastVersion)
+                    .eq(IssueSyncData::getIsDeleted, 0)
+                    .orderByAsc(IssueSyncData::getSyncVersion)
+                    .last("LIMIT " + batchSize));
+
+            if (dataList.isEmpty()) {
+                break;
+            }
+
+            result.setTotalCount(result.getTotalCount() + dataList.size());
+            Long batchVersion = dataList.stream()
+                    .mapToLong(IssueSyncData::getSyncVersion)
+                    .max()
+                    .orElse(lastVersion);
+
+            SyncPacket packet = SyncPacket.builder()
+                    .tenantId(String.valueOf(tenantId))
+                    .tableName(tableName)
+                    .direction(SyncDirection.UP)
+                    .dataList(dataList)
+                    .batchVersion(batchVersion)
+                    .hasMore(dataList.size() >= batchSize)
+                    .nonce(UUID.randomUUID().toString())
+                    .build();
+
+            SyncResponse response = cloudPlatformClient.pushToCloud(packet, config);
+            if (response != null && response.isSuccess()) {
+                lastVersion = response.getConfirmedVersion() != null ? response.getConfirmedVersion() : batchVersion;
+                updateCursor(tenantId, tableName, SyncDirection.UP.getCode(), lastVersion);
+                List<Long> ids = dataList.stream().map(IssueSyncData::getId).collect(Collectors.toList());
+                markSynced(ids);
+                result.setSuccessCount(result.getSuccessCount() + dataList.size());
+                if (Boolean.FALSE.equals(packet.getHasMore())) {
+                    break;
+                }
+            } else {
+                String message = response != null ? response.getMessage() : "云端无响应";
+                log.error("云端接收失败: {}", message);
+                result.setFailureCount(result.getFailureCount() + dataList.size());
+                result.setErrorMessage(message);
+                markFailed(dataList.stream().map(IssueSyncData::getId).collect(Collectors.toList()));
+                break;
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public void resetCursor(Integer tenantId, String tableName) {
+        cursorMapper.update(null, new LambdaUpdateWrapper<IssueSyncCursor>()
+                .eq(IssueSyncCursor::getTenantId, tenantId)
+                .eq(IssueSyncCursor::getTableName, tableName)
+                .eq(IssueSyncCursor::getDirection, SyncDirection.UP.getCode())
+                .set(IssueSyncCursor::getLastSyncVersion, 0L)
+                .set(IssueSyncCursor::getLastSyncTime, LocalDateTime.now()));
+    }
+
+    private IssueSyncCursor getOrInitCursor(Integer tenantId, String tableName, String direction) {
+        IssueSyncCursor cursor = cursorMapper.selectOne(new LambdaQueryWrapper<IssueSyncCursor>()
+                .eq(IssueSyncCursor::getTenantId, tenantId)
+                .eq(IssueSyncCursor::getTableName, tableName)
+                .eq(IssueSyncCursor::getDirection, direction)
+                .eq(IssueSyncCursor::getIsDeleted, 0));
+        if (cursor == null) {
+            cursor = new IssueSyncCursor();
+            cursor.setTenantId(tenantId);
+            cursor.setTableName(tableName);
+            cursor.setDirection(direction);
+            cursor.setLastSyncVersion(0L);
+            cursor.setLastSyncTime(LocalDateTime.now());
+            CloudEntitySupport.fillOnInsert(cursor);
+            cursorMapper.insert(cursor);
+        }
+        return cursor;
+    }
+
+    private void updateCursor(Integer tenantId, String tableName, String direction, Long version) {
+        cursorMapper.update(null, new LambdaUpdateWrapper<IssueSyncCursor>()
+                .eq(IssueSyncCursor::getTenantId, tenantId)
+                .eq(IssueSyncCursor::getTableName, tableName)
+                .eq(IssueSyncCursor::getDirection, direction)
+                .set(IssueSyncCursor::getLastSyncVersion, version)
+                .set(IssueSyncCursor::getLastSyncTime, LocalDateTime.now()));
+    }
+
+    private void markSynced(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) {
+            return;
+        }
+        syncDataMapper.update(null, new LambdaUpdateWrapper<IssueSyncData>()
+                .in(IssueSyncData::getId, ids)
+                .set(IssueSyncData::getSyncStatus, 1));
+    }
+
+    private void markFailed(List<Long> ids) {
+        if (ids == null || ids.isEmpty()) {
+            return;
+        }
+        syncDataMapper.update(null, new LambdaUpdateWrapper<IssueSyncData>()
+                .in(IssueSyncData::getId, ids)
+                .set(IssueSyncData::getSyncStatus, 2));
+    }
+}

+ 81 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalSyncAgentImpl.java

@@ -0,0 +1,81 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.domain.IssueSyncData;
+import com.usky.issue.mapper.IssueSyncDataMapper;
+import com.usky.issue.service.CloudConfigService;
+import com.usky.issue.service.CloudSyncReceiveService;
+import com.usky.issue.service.LocalPullService;
+import com.usky.issue.service.LocalPushService;
+import com.usky.issue.service.LocalSyncAgent;
+import com.usky.issue.service.enums.SyncDirection;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import com.usky.issue.service.vo.SyncResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 本地同步 Agent 实现
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Slf4j
+@Component
+public class LocalSyncAgentImpl implements LocalSyncAgent {
+
+    @Autowired
+    private LocalPushService pushService;
+    @Autowired
+    private LocalPullService pullService;
+    @Autowired
+    private IssueSyncDataMapper syncDataMapper;
+    @Autowired
+    private CloudSyncReceiveService cloudSyncReceiveService;
+    @Autowired
+    private CloudConfigService cloudConfigService;
+
+    @Scheduled(fixedDelayString = "${sync.push.fixed-delay-ms:5000}")
+    public void autoPush() {
+        try {
+            IssueCloudConfig config = cloudConfigService.requireActiveConfig();
+            for (SyncTypeEnum type : SyncTypeEnum.values()) {
+                pushService.pushAllPending(type.getCode(), config);
+            }
+        } catch (Exception e) {
+            log.debug("自动推送跳过: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public SyncResult executeSync(SyncTypeEnum syncType, IssueCloudConfig config,
+                                  SyncDirection direction, boolean fullSync) {
+        String tableName = syncType.getCode();
+        SyncResult result = SyncResult.empty();
+
+        if (direction == SyncDirection.UP || direction == SyncDirection.BOTH) {
+            if (fullSync) {
+                pushService.resetCursor(config.getTenantId(), tableName);
+            }
+            result.merge(pushService.pushByTenant(config.getTenantId(), tableName, config));
+        }
+        if (direction == SyncDirection.DOWN || direction == SyncDirection.BOTH) {
+            result.merge(pullService.pullByTenant(config.getTenantId(), tableName, config));
+        }
+        return result;
+    }
+
+    @Override
+    public int countPending(SyncTypeEnum syncType, IssueCloudConfig config) {
+        String tableName = syncType.getCode();
+        Integer upCount = syncDataMapper.selectCount(new LambdaQueryWrapper<IssueSyncData>()
+                .eq(IssueSyncData::getTenantId, config.getTenantId())
+                .eq(IssueSyncData::getTableName, tableName)
+                .in(IssueSyncData::getSyncStatus, 0, 2)
+                .eq(IssueSyncData::getIsDeleted, 0));
+        return cloudSyncReceiveService.countPendingDownQueue(config.getTenantId(), tableName);
+    }
+}

+ 0 - 4
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/SpRtu2017ServiceImpl.java

@@ -5,19 +5,15 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 
-import com.usky.common.core.exception.BusinessException;
 import com.usky.issue.domain.SpOwner;
 import com.usky.issue.domain.SpRtu2017;
 import com.usky.issue.mapper.SpRtu2017Mapper;
 import com.usky.issue.service.SpOwnerService;
 import com.usky.issue.service.SpRtu2017Service;
 import com.usky.common.mybatis.core.AbstractCrudService;
-import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
 import java.util.List;
 import java.util.Map;
 

+ 33 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudDataChangeListener.java

@@ -0,0 +1,33 @@
+package com.usky.issue.service.listener;
+
+import com.usky.issue.service.CloudSyncService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * 组织/用户变更事件监听(应用内事件,可替换为 MQ/CDC)
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Slf4j
+@Component
+public class CloudDataChangeListener {
+
+    @Autowired
+    private CloudSyncService cloudSyncService;
+
+    @EventListener
+    public void onOrgDataChanged(CloudOrgChangedEvent event) {
+        log.info("收到组织变更事件 source={}", event.getSource());
+        cloudSyncService.onOrgChanged();
+    }
+
+    @EventListener
+    public void onUserDataChanged(CloudUserChangedEvent event) {
+        log.info("收到用户变更事件 source={}", event.getSource());
+        cloudSyncService.onUserChanged();
+    }
+}

+ 16 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudOrgChangedEvent.java

@@ -0,0 +1,16 @@
+package com.usky.issue.service.listener;
+
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * 组织数据变更事件
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+public class CloudOrgChangedEvent extends ApplicationEvent {
+
+    public CloudOrgChangedEvent(Object source) {
+        super(source);
+    }
+}

+ 16 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudUserChangedEvent.java

@@ -0,0 +1,16 @@
+package com.usky.issue.service.listener;
+
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * 用户数据变更事件
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+public class CloudUserChangedEvent extends ApplicationEvent {
+
+    public CloudUserChangedEvent(Object source) {
+        super(source);
+    }
+}

+ 52 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/support/CloudEntitySupport.java

@@ -0,0 +1,52 @@
+package com.usky.issue.service.support;
+
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.issue.domain.CloudAuditEntity;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.time.LocalDateTime;
+
+/**
+ * 实体审计字段填充
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+public final class CloudEntitySupport {
+
+    private CloudEntitySupport() {
+    }
+
+    public static String resolveOperator() {
+        String username = SecurityUtils.getUsername();
+        return StringUtils.isBlank(username) ? "system" : username;
+    }
+
+    public static String resolveRequestIp() {
+        ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attrs == null) {
+            return null;
+        }
+        return attrs.getRequest().getRemoteAddr();
+    }
+
+    public static void fillOnInsert(CloudAuditEntity entity) {
+        LocalDateTime now = LocalDateTime.now();
+        if (entity.getTenantId() == null) {
+            entity.setTenantId(SecurityUtils.getTenantId());
+        }
+        entity.setCreatedBy(resolveOperator());
+        entity.setCreatedTime(now);
+        entity.setUpdatedTime(now);
+        if (entity.getIsDeleted() == null) {
+            entity.setIsDeleted(0);
+        }
+    }
+
+    public static void fillOnUpdate(CloudAuditEntity entity) {
+        entity.setUpdatedTime(LocalDateTime.now());
+        entity.setUpdatedBy(resolveOperator());
+    }
+}

+ 85 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/AesGcmCipher.java

@@ -0,0 +1,85 @@
+package com.usky.issue.service.util;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.Base64;
+
+/**
+ * AES-256-GCM 加解密工具
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Component
+public class AesGcmCipher {
+
+    private static final String ALGORITHM = "AES/GCM/NoPadding";
+    private static final String PROVIDER = "BC";
+    private static final int GCM_IV_LENGTH = 12;
+    private static final int GCM_TAG_LENGTH = 128;
+
+    private final SecretKey secretKey;
+
+    static {
+        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
+            Security.addProvider(new BouncyCastleProvider());
+        }
+    }
+
+    public AesGcmCipher(@Value("${cloud.integration.encrypt-key:0123456789abcdef}") String encryptKey) {
+        byte[] keyBytes = normalizeKeyBytes(encryptKey.getBytes(StandardCharsets.UTF_8));
+        this.secretKey = new SecretKeySpec(keyBytes, "AES");
+    }
+
+    /** AES-128-GCM(16 字节密钥);JDK8 默认策略下兼容。生产可配置 32 字符密钥取前 16 字节。 */
+    private static byte[] normalizeKeyBytes(byte[] source) {
+        byte[] key = new byte[16];
+        int len = Math.min(source.length, 16);
+        System.arraycopy(source, 0, key, 0, len);
+        return key;
+    }
+
+    public String encrypt(String plainText) {
+        try {
+            byte[] iv = new byte[GCM_IV_LENGTH];
+            new SecureRandom().nextBytes(iv);
+            Cipher cipher = Cipher.getInstance(ALGORITHM, PROVIDER);
+            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
+            byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
+            ByteBuffer buffer = ByteBuffer.allocate(iv.length + cipherText.length);
+            buffer.put(iv);
+            buffer.put(cipherText);
+            return Base64.getEncoder().encodeToString(buffer.array());
+        } catch (Exception e) {
+            throw new IllegalStateException("凭证加密失败", e);
+        }
+    }
+
+    public String decrypt(String encrypted) {
+        try {
+            byte[] decoded = Base64.getDecoder().decode(encrypted);
+            ByteBuffer buffer = ByteBuffer.wrap(decoded);
+            byte[] iv = new byte[GCM_IV_LENGTH];
+            buffer.get(iv);
+            byte[] cipherText = new byte[buffer.remaining()];
+            buffer.get(cipherText);
+            Cipher cipher = Cipher.getInstance(ALGORITHM, PROVIDER);
+            cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
+            byte[] plain = cipher.doFinal(cipherText);
+            return new String(plain, StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            throw new IllegalStateException("凭证解密失败", e);
+        }
+    }
+}

+ 23 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/CredentialMaskUtil.java

@@ -0,0 +1,23 @@
+package com.usky.issue.service.util;
+
+/**
+ * 凭证脱敏工具
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+public final class CredentialMaskUtil {
+
+    private CredentialMaskUtil() {
+    }
+
+    public static String mask(String value) {
+        if (value == null || value.isEmpty()) {
+            return "****";
+        }
+        if (value.length() <= 4) {
+            return "****";
+        }
+        return value.substring(0, 2) + "****" + value.substring(value.length() - 2);
+    }
+}

+ 31 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/HmacSignUtil.java

@@ -0,0 +1,31 @@
+package com.usky.issue.service.util;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * HMAC 签名工具
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+public final class HmacSignUtil {
+
+    private static final String HMAC_SHA256 = "HmacSHA256";
+
+    private HmacSignUtil() {
+    }
+
+    public static String sign(String content, String secret) {
+        try {
+            Mac mac = Mac.getInstance(HMAC_SHA256);
+            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256));
+            byte[] hash = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
+            return Base64.getEncoder().encodeToString(hash);
+        } catch (Exception e) {
+            throw new IllegalStateException("HMAC签名失败", e);
+        }
+    }
+}

+ 28 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConfigResponse.java

@@ -0,0 +1,28 @@
+package com.usky.issue.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 云平台配置响应
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+public class CloudConfigResponse {
+
+    private Long id;
+    private String tenantId;
+    /** 解密后的凭证密钥 */
+    private String credentialKey;
+    private Integer status;
+    private Integer connectionStatus;
+    private Integer version;
+    private LocalDateTime lastTestTime;
+    private LocalDateTime updatedTime;
+    private String updatedBy;
+    private String createdBy;
+    private LocalDateTime createdTime;
+}

+ 28 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConfigSaveRequest.java

@@ -0,0 +1,28 @@
+package com.usky.issue.service.vo;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+/**
+ * 保存云平台配置请求
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+public class CloudConfigSaveRequest {
+
+    @NotNull(message = "租户ID不能为空")
+    //@Size(max = 64, message = "租户ID长度不能超过64")
+    private Integer tenantId;
+
+    @NotBlank(message = "凭证密钥不能为空")
+    @Size(max = 256, message = "凭证密钥长度不能超过256")
+    private String credentialKey;
+
+    /** 乐观锁版本,更新时必填 */
+    private Integer version;
+}

+ 24 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConnectionTestRequest.java

@@ -0,0 +1,24 @@
+package com.usky.issue.service.vo;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 连接测试请求
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Data
+public class CloudConnectionTestRequest {
+
+    @NotNull(message = "租户ID不能为空")
+    private Integer tenantId;
+
+    @NotNull(message = "云平台地址不能为空")
+    private String cloudAddress;
+
+    @NotNull(message = "凭证密钥不能为空")
+    private String credentialKey;
+}

+ 26 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConnectionTestResponse.java

@@ -0,0 +1,26 @@
+package com.usky.issue.service.vo;
+
+import lombok.Data;
+
+/**
+ * 连接测试响应
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+public class CloudConnectionTestResponse {
+
+    private boolean success;
+    private String message;
+    /** 0未知 1成功 2失败 */
+    private Integer connectionStatus;
+    /** 云平台 HTTP/TCP 层是否可达 */
+    private Boolean networkReachable;
+    /** 认证层是否通过 */
+    private Boolean authValid;
+    /** 租户配置是否存在且启用 */
+    private Boolean tenantExists;
+    /** 耗时(毫秒) */
+    private Long costTime;
+}

+ 81 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConnectionTestResult.java

@@ -0,0 +1,81 @@
+package com.usky.issue.service.vo;
+
+import com.usky.issue.service.constant.CloudIntegrationConstants;
+import lombok.Data;
+
+/**
+ * 连接测试内部结果(HTTP 客户端层)
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Data
+public class CloudConnectionTestResult {
+
+    private boolean success;
+    private String message;
+    /** 是否已到达云平台(HTTP/TCP 层可达) */
+    private boolean networkReachable;
+    /** 认证层是否通过 */
+    private boolean authValid;
+    /** 租户配置是否存在且启用 */
+    private boolean tenantExists;
+    /** 耗时(毫秒) */
+    private Long costTime;
+
+    public static CloudConnectionTestResult success(String message) {
+        CloudConnectionTestResult result = new CloudConnectionTestResult();
+        result.setSuccess(true);
+        result.setMessage(message);
+        result.setNetworkReachable(true);
+        result.setAuthValid(true);
+        result.setTenantExists(true);
+        return result;
+    }
+
+    public static CloudConnectionTestResult fullSuccess(long costTime) {
+        CloudConnectionTestResult result = success(CloudIntegrationConstants.MSG_CONNECTION_SUCCESS);
+        result.setCostTime(costTime);
+        return result;
+    }
+
+    public static CloudConnectionTestResult networkFailure(String message) {
+        CloudConnectionTestResult result = new CloudConnectionTestResult();
+        result.setSuccess(false);
+        result.setMessage(message);
+        result.setNetworkReachable(false);
+        result.setAuthValid(false);
+        result.setTenantExists(false);
+        return result;
+    }
+
+    public static CloudConnectionTestResult authFailure(long costTime, String message) {
+        CloudConnectionTestResult result = new CloudConnectionTestResult();
+        result.setSuccess(false);
+        result.setMessage(message);
+        result.setNetworkReachable(true);
+        result.setAuthValid(false);
+        result.setTenantExists(false);
+        result.setCostTime(costTime);
+        return result;
+    }
+
+    public static CloudConnectionTestResult tenantMissing(long costTime) {
+        CloudConnectionTestResult result = new CloudConnectionTestResult();
+        result.setSuccess(false);
+        result.setMessage(CloudIntegrationConstants.MSG_TENANT_CONFIG_MISSING);
+        result.setNetworkReachable(true);
+        result.setAuthValid(true);
+        result.setTenantExists(false);
+        result.setCostTime(costTime);
+        return result;
+    }
+
+    public static CloudConnectionTestResult failure(boolean networkReachable, String message) {
+        CloudConnectionTestResult result = new CloudConnectionTestResult();
+        result.setSuccess(false);
+        result.setMessage(message);
+        result.setNetworkReachable(networkReachable);
+        return result;
+    }
+}

+ 23 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudDisableResponse.java

@@ -0,0 +1,23 @@
+package com.usky.issue.service.vo;
+
+import lombok.Data;
+
+/**
+ * 禁用集成响应
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+public class CloudDisableResponse {
+
+    /**
+     * 是否禁用
+     */
+    private boolean disabled;
+
+    /**
+     * 禁用后,被禁用的任务数
+     */
+    private int affectedTasks;
+}

+ 30 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncPacket.java

@@ -0,0 +1,30 @@
+package com.usky.issue.service.vo;
+
+import com.usky.issue.service.enums.SyncDirection;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+/**
+ * 同步数据包
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SyncPacket {
+
+    private String tenantId;
+    private String tableName;
+    private SyncDirection direction;
+    private List<?> dataList;
+    private Long batchVersion;
+    private Boolean hasMore;
+    private String nonce;
+}

+ 24 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncResponse.java

@@ -0,0 +1,24 @@
+package com.usky.issue.service.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 同步响应
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SyncResponse {
+
+    private boolean success;
+    private String message;
+    private Long confirmedVersion;
+    private Integer acceptedCount;
+}

+ 34 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncResult.java

@@ -0,0 +1,34 @@
+package com.usky.issue.service.vo;
+
+import lombok.Data;
+
+/**
+ * 同步执行结果
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Data
+public class SyncResult {
+
+    private int totalCount;
+    private int successCount;
+    private int failureCount;
+    private String errorMessage;
+
+    public static SyncResult empty() {
+        return new SyncResult();
+    }
+
+    public void merge(SyncResult other) {
+        if (other == null) {
+            return;
+        }
+        this.totalCount += other.totalCount;
+        this.successCount += other.successCount;
+        this.failureCount += other.failureCount;
+        if (other.errorMessage != null) {
+            this.errorMessage = other.errorMessage;
+        }
+    }
+}

+ 23 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncStatusResponse.java

@@ -0,0 +1,23 @@
+package com.usky.issue.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 同步状态项
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+public class SyncStatusResponse {
+
+    private String syncType;
+    private String typeName;
+    private Integer totalCount;
+    private Integer successCount;
+    private Integer failureCount;
+    private Integer unsyncedCount;
+    private LocalDateTime lastSyncTime;
+}

+ 28 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncTaskResponse.java

@@ -0,0 +1,28 @@
+package com.usky.issue.service.vo;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 同步任务进度响应
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+public class SyncTaskResponse {
+
+    private Long taskId;
+    private String syncType;
+    private String taskStatus;
+    private String triggerMode;
+    private Integer totalCount;
+    private Integer processedCount;
+    private Integer successCount;
+    private Integer failureCount;
+    private String errorSummary;
+    private Integer progressPercent;
+    private LocalDateTime startTime;
+    private LocalDateTime finishTime;
+}

+ 20 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/TriggerEventItemRequest.java

@@ -0,0 +1,20 @@
+package com.usky.issue.service.vo;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 触发事件项
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+public class TriggerEventItemRequest {
+
+    @NotBlank(message = "事件编码不能为空")
+    private String eventCode;
+
+    private Boolean enabled;
+}

+ 18 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/TriggerEventResponse.java

@@ -0,0 +1,18 @@
+package com.usky.issue.service.vo;
+
+import lombok.Data;
+
+/**
+ * 触发事件响应
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+public class TriggerEventResponse {
+
+    private String eventCode;
+    private String eventName;
+    private String description;
+    private Boolean enabled;
+}

+ 21 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/TriggerEventSaveRequest.java

@@ -0,0 +1,21 @@
+package com.usky.issue.service.vo;
+
+import lombok.Data;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import java.util.List;
+
+/**
+ * 保存触发事件请求
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Data
+public class TriggerEventSaveRequest {
+
+    @NotEmpty(message = "触发事件列表不能为空")
+    @Valid
+    private List<TriggerEventItemRequest> events;
+}