瀏覽代碼

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

hanzhengyi 3 周之前
父節點
當前提交
609f749f02
共有 91 個文件被更改,包括 4743 次插入11 次删除
  1. 11 0
      service-issue/service-issue-biz/pom.xml
  2. 8 7
      service-issue/service-issue-biz/src/main/java/com/usky/issue/IssueApplication.java
  3. 194 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/api/CloudIntegrationApiController.java
  4. 68 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudConfigController.java
  5. 43 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudSyncController.java
  6. 49 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudTriggerEventController.java
  7. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/sys_user.sql
  8. 36 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/CloudAuditEntity.java
  9. 44 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueCloudConfig.java
  10. 30 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueOperationLog.java
  11. 33 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncCursor.java
  12. 38 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncData.java
  13. 32 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncDetail.java
  14. 34 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncDownQueue.java
  15. 40 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncStatus.java
  16. 46 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncTask.java
  17. 33 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueTriggerEvent.java
  18. 70 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SysUser.java
  19. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueCloudConfigMapper.java
  20. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueOperationLogMapper.java
  21. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncCursorMapper.java
  22. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncDataMapper.java
  23. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncDetailMapper.java
  24. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncDownQueueMapper.java
  25. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncStatusMapper.java
  26. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncTaskMapper.java
  27. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueTriggerEventMapper.java
  28. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/SysUserMapper.java
  29. 16 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudAuthService.java
  30. 35 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudConfigService.java
  31. 12 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudOperationLogService.java
  32. 23 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncPublishService.java
  33. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncReceiveService.java
  34. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncService.java
  35. 18 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncTaskRunner.java
  36. 20 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudTriggerEventService.java
  37. 23 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalPullService.java
  38. 19 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalPushService.java
  39. 19 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalSyncAgent.java
  40. 321 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/client/CloudPlatformClient.java
  41. 30 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/config/CloudSyncExecutorConfig.java
  42. 55 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/constant/CloudIntegrationConstants.java
  43. 35 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncDirection.java
  44. 44 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncTypeEnum.java
  45. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/TaskStatusEnum.java
  46. 26 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/TriggerEventCodeEnum.java
  47. 21 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/exception/CloudIntegrationErrorCode.java
  48. 109 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudAuthServiceImpl.java
  49. 370 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudConfigServiceImpl.java
  50. 44 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudOperationLogServiceImpl.java
  51. 140 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncPublishServiceImpl.java
  52. 193 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncReceiveServiceImpl.java
  53. 203 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncServiceImpl.java
  54. 165 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. 238 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. 94 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. 28 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudUserChangedEvent.java
  63. 28 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudUserSyncPublishListener.java
  64. 98 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/support/CloudEntitySupport.java
  65. 32 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/support/CloudSyncEventPublisher.java
  66. 24 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncDataApplier.java
  67. 28 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncDataApplierRegistry.java
  68. 51 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncPayloadSupport.java
  69. 78 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/impl/UserSyncDataApplier.java
  70. 85 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/AesGcmCipher.java
  71. 59 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/ApiResultParser.java
  72. 23 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/CredentialMaskUtil.java
  73. 31 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/HmacSignUtil.java
  74. 26 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/SyncJsonMapper.java
  75. 29 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConfigResponse.java
  76. 28 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConfigSaveRequest.java
  77. 24 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConnectionTestRequest.java
  78. 26 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConnectionTestResponse.java
  79. 81 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConnectionTestResult.java
  80. 23 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudDisableResponse.java
  81. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudPollResult.java
  82. 24 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/ManualSyncRequest.java
  83. 33 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncPacket.java
  84. 21 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncPollRequest.java
  85. 24 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncResponse.java
  86. 34 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncResult.java
  87. 23 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncStatusResponse.java
  88. 28 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncTaskResponse.java
  89. 20 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/TriggerEventItemRequest.java
  90. 18 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/TriggerEventResponse.java
  91. 21 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/TriggerEventSaveRequest.java

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

@@ -66,6 +66,17 @@
             <artifactId>ruoyi-common-swagger</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+        </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" +
                 "----------------------------------------------------------");
     }
 }

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

@@ -0,0 +1,194 @@
+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.SyncPollRequest;
+import com.usky.issue.service.vo.SyncResponse;
+import com.usky.issue.service.util.CredentialMaskUtil;
+import lombok.extern.slf4j.Slf4j;
+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
+ */
+@Slf4j
+@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);
+        log.info("[连接测试-云端] 收到请求 tenantIdHeader={}, tenantId={}, apiKey={}",
+                tenantIdHeader, tenantId, CredentialMaskUtil.mask(apiKey));
+
+        if (tenantId != null && !cloudAuthService.validateToken(apiKey, tenantId)) {
+            log.warn("[连接测试-云端] 认证失败 tenantId={}, apiKey={}", tenantId, CredentialMaskUtil.mask(apiKey));
+            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);
+        log.info("[连接测试-云端] 租户校验 success={}, message={}", response.isSuccess(), response.getMessage());
+        response.setNetworkReachable(true);
+        response.setAuthValid(tenantId == null || cloudAuthService.validateToken(apiKey, tenantId));
+        response.setTenantExists(response.isSuccess());
+        log.info("[连接测试-云端] 返回 success={}, authValid={}, tenantExists={}, message={}",
+                response.isSuccess(), response.getAuthValid(), response.getTenantExists(), response.getMessage());
+        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();
+        }
+    }
+
+    /**
+     * 下发数据轮询(云端→本地),POST 供网关/offline-api 路由(与 testConnection 一致)
+     */
+    @PostMapping("/sync/poll")
+    public ApiResult<SyncPacket> pollPost(
+            @RequestHeader(value = CloudIntegrationConstants.HEADER_API_KEY, required = false) String apiKey,
+            @RequestBody SyncPollRequest request) {
+        return doPoll(apiKey, request.getTenantId(), request.getTableName(),
+                request.getLastVersion(), request.isForceBackfill());
+    }
+
+    /**
+     * 下发数据轮询(GET 兼容旧调用)
+     */
+    @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,
+            @RequestParam(required = false, defaultValue = "false") boolean forceBackfill) {
+        return doPoll(apiKey, tenantId, tableName, lastVersion, forceBackfill);
+    }
+
+    private ApiResult<SyncPacket> doPoll(String apiKey, String tenantId, String tableName,
+                                         Long lastVersion, boolean forceBackfill) {
+        if (!cloudAuthService.validateToken(apiKey, Integer.valueOf(tenantId))) {
+            log.warn("[poll] 认证失败 tenantId={}, tableName={}", tenantId, tableName);
+            return ApiResult.error(CloudIntegrationConstants.MSG_POLL_AUTH_FAILED);
+        }
+        log.info("[poll] tenantId={}, tableName={}, lastVersion={}, forceBackfill={}",
+                tenantId, tableName, lastVersion, forceBackfill);
+        return ApiResult.success(cloudSyncReceiveService.pollDownQueue(
+                tenantId, tableName, lastVersion, forceBackfill));
+    }
+
+    /**
+     * 确认下发数据已消费
+     */
+    @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));
+    }
+
+}

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

@@ -0,0 +1,43 @@
+package com.usky.issue.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.issue.service.vo.ManualSyncRequest;
+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.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * 云平台数据同步接口
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@RestController
+@Validated
+@RequestMapping("/cloud")
+public class CloudSyncController {
+
+    @Autowired
+    private CloudSyncService cloudSyncService;
+
+    @GetMapping("/sync/status")
+    public ApiResult<List<SyncStatusResponse>> listStatus() {
+        return ApiResult.success(cloudSyncService.listSyncStatus());
+    }
+
+    @PostMapping("/sync")
+    public ApiResult<SyncTaskResponse> manualSync(@RequestBody ManualSyncRequest request) {
+        return ApiResult.success(cloudSyncService.triggerManualSync(request.getType(), request.getDirection()));
+    }
+
+    @GetMapping("/sync/task/{taskId}")
+    public ApiResult<SyncTaskResponse> taskProgress(@PathVariable Long taskId) {
+        return ApiResult.success(cloudSyncService.getTaskProgress(taskId));
+    }
+}

+ 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";
+    }
+}

+ 25 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/sys_user.sql

@@ -0,0 +1,25 @@
+CREATE TABLE `sys_user` (
+  `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
+  `dept_id` bigint(20) DEFAULT NULL COMMENT '部门ID',
+  `user_name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户账号',
+  `nick_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户昵称',
+  `user_type` varchar(2) COLLATE utf8mb4_unicode_ci DEFAULT '00' COMMENT '用户类型(00系统用户, 01 租户管理员)',
+  `email` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '用户邮箱',
+  `phonenumber` varchar(11) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '手机号码',
+  `sex` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
+  `full_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '姓名',
+  `avatar` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '头像地址',
+  `password` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '密码',
+  `status` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
+  `del_flag` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
+  `login_ip` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '最后登录IP',
+  `login_date` datetime DEFAULT NULL COMMENT '最后登录时间',
+  `create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  `remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
+  `tenant_id` int(11) DEFAULT NULL COMMENT '租户ID',
+  `address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地址',
+  PRIMARY KEY (`user_id`) USING BTREE
+) ENGINE=InnoDB AUTO_INCREMENT=1825 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='用户信息表';

+ 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;
+}

+ 70 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SysUser.java

@@ -0,0 +1,70 @@
+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 java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 用户信息表
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Data
+@TableName("sys_user")
+public class SysUser implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "user_id", type = IdType.AUTO)
+    private Long userId;
+
+    private Long deptId;
+
+    private String userName;
+
+    private String nickName;
+
+    /** 用户类型(00系统用户,01租户管理员) */
+    private String userType;
+
+    private String email;
+
+    private String phonenumber;
+
+    private String sex;
+
+    private String fullName;
+
+    private String avatar;
+
+    private String password;
+
+    /** 帐号状态(0正常 1停用) */
+    private String status;
+
+    /** 删除标志(0代表存在 2代表删除) */
+    private String delFlag;
+
+    private String loginIp;
+
+    private LocalDateTime loginDate;
+
+    private String createBy;
+
+    private LocalDateTime createTime;
+
+    private String updateBy;
+
+    private LocalDateTime updateTime;
+
+    private String remark;
+
+    private Integer tenantId;
+
+    private String address;
+}

+ 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> {
+}

+ 17 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/SysUserMapper.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.SysUser;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 用户信息 Mapper
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@DS("usky-cloud")
+@Repository
+public interface SysUserMapper extends CrudMapper<SysUser> {
+}

+ 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);
+}

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

@@ -0,0 +1,23 @@
+package com.usky.issue.service;
+
+import com.usky.issue.domain.SysUser;
+
+/**
+ * 云端下发队列发布(云→本地)
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+public interface CloudSyncPublishService {
+
+    /**
+     * 将单条用户变更加入下发队列
+     */
+    void enqueueUser(SysUser user, String operation);
+
+    /** 首次拉取时从 sys_user 回填下发队列(仅当前租户) */
+    int backfillUsersIfNeeded(Integer tenantId, Long lastVersion, boolean force);
+
+    /** 统计租户在 sys_user 中的有效用户数 */
+    int countSysUsers(Integer tenantId);
+}

+ 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, boolean forceBackfill);
+
+    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, String direction);
+
+    SyncTaskResponse getTaskProgress(Long taskId);
+
+    void onOrgChanged();
+
+    void onUserChanged();
+}

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

@@ -0,0 +1,18 @@
+package com.usky.issue.service;
+
+import com.usky.issue.service.enums.SyncDirection;
+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,
+                                                   SyncDirection direction);
+
+    void executeSyncTask(Long taskId, SyncTypeEnum type, SyncDirection direction);
+}

+ 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);
+}

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

@@ -0,0 +1,23 @@
+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);
+
+    default SyncResult pullByTenant(Integer tenantId, String tableName, IssueCloudConfig config) {
+        return pullByTenant(tenantId, tableName, config, false);
+    }
+
+    SyncResult pullByTenant(Integer tenantId, String tableName, IssueCloudConfig config, boolean forceBackfill);
+
+    void resetDownCursor(Integer tenantId, String tableName);
+}

+ 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, SyncDirection direction);
+}

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

@@ -0,0 +1,321 @@
+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.support.CloudEntitySupport;
+import com.usky.issue.service.util.AesGcmCipher;
+import com.usky.issue.service.util.CredentialMaskUtil;
+import com.usky.issue.service.util.HmacSignUtil;
+import com.usky.issue.service.vo.CloudConnectionTestResult;
+import com.usky.issue.service.vo.CloudPollResult;
+import com.usky.issue.service.vo.SyncPacket;
+import com.usky.issue.service.vo.SyncPollRequest;
+import com.usky.issue.service.vo.SyncResponse;
+import com.usky.issue.service.util.ApiResultParser;
+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.HttpStatusCodeException;
+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();
+        log.info("[连接测试-HTTP] 准备请求 url={}, tenantId={}, credentialKey={}, hasToken={}",
+                testUrl, tenantId, CredentialMaskUtil.mask(credentialKey), StringUtils.hasText(token));
+
+        try {
+            HttpHeaders headers = buildAuthHeaders(tenantId, credentialKey, token);
+            ResponseEntity<String> response = restTemplate.exchange(
+                    testUrl, HttpMethod.POST, new HttpEntity<>(headers), String.class);
+            log.info("[连接测试-HTTP] 响应 status={}, body={}", response.getStatusCodeValue(), response.getBody());
+            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());
+            String token = CloudEntitySupport.resolveAccessToken(config);
+            return testConnection(config.getCloudAddress(), config.getTenantId(), credential, token);
+        } 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 CloudPollResult pollCloud(Integer tenantId, String tableName, Long lastVersion,
+                                     IssueCloudConfig config, boolean forceBackfill) {
+        try {
+            String credential = aesGcmCipher.decrypt(config.getCredentialKey());
+            HttpHeaders headers = buildAuthHeaders(config, credential);
+            headers.setContentType(MediaType.APPLICATION_JSON);
+
+            SyncPollRequest pollRequest = new SyncPollRequest();
+            pollRequest.setTenantId(String.valueOf(tenantId));
+            pollRequest.setTableName(tableName);
+            pollRequest.setLastVersion(lastVersion == null ? 0L : lastVersion);
+            pollRequest.setForceBackfill(forceBackfill);
+            String requestBody = OBJECT_MAPPER.writeValueAsString(pollRequest);
+
+            String url = resolveIntegrationBaseUrl(config) + CloudIntegrationConstants.PATH_SYNC_POLL;
+            log.info("[pollCloud] POST url={}, tenantId={}, tableName={}, lastVersion={}, forceBackfill={}, hasToken={}",
+                    url, tenantId, tableName, lastVersion, forceBackfill,
+                    StringUtils.hasText(CloudEntitySupport.resolveAccessToken(config)));
+
+            ResponseEntity<String> response = restTemplate.exchange(
+                    url, HttpMethod.POST, new HttpEntity<>(requestBody, headers), String.class);
+            return parsePollResponse(response.getBody());
+        } catch (HttpStatusCodeException ex) {
+            log.warn("[pollCloud] HTTP {} tenantId={}, body={}",
+                    ex.getStatusCode(), tenantId, ex.getResponseBodyAsString());
+            return parsePollResponse(ex.getResponseBodyAsString());
+        } catch (Exception e) {
+            log.error("[pollCloud] 轮询云端失败 tenantId={}, tableName={}", tenantId, tableName, e);
+            return CloudPollResult.builder().requestFailed(true).errorMessage(e.getMessage()).build();
+        }
+    }
+
+    private CloudPollResult parsePollResponse(String body) {
+        if (!StringUtils.hasText(body)) {
+            return CloudPollResult.builder().requestFailed(true).errorMessage("云端返回空响应").build();
+        }
+        log.info("[pollCloud] 响应 body={}", body);
+        try {
+            JsonNode json = OBJECT_MAPPER.readTree(body);
+            if (!ApiResultParser.isSuccess(json)) {
+                String msg = ApiResultParser.resolveMessage(json);
+                log.warn("[pollCloud] 业务失败 msg={}", msg);
+                boolean authFailed = msg != null && (msg.contains("认证") || msg.contains("令牌"));
+                return CloudPollResult.builder()
+                        .authFailed(authFailed)
+                        .requestFailed(true)
+                        .errorMessage(StringUtils.hasText(msg) ? msg : "poll 调用失败")
+                        .build();
+            }
+            JsonNode dataNode = json.has("data") ? json.get("data") : null;
+            if (dataNode == null || dataNode.isNull()) {
+                return CloudPollResult.builder().packet(null).build();
+            }
+            SyncPacket packet = OBJECT_MAPPER.treeToValue(dataNode, SyncPacket.class);
+            return CloudPollResult.builder().packet(packet).build();
+        } catch (Exception e) {
+            log.error("[pollCloud] 响应解析失败 body={}", body, e);
+            return CloudPollResult.builder().requestFailed(true).errorMessage("响应解析失败: " + e.getMessage()).build();
+        }
+    }
+
+    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, CloudEntitySupport.resolveAccessToken(config));
+    }
+
+    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)) {
+            log.warn("[连接测试-HTTP] 云端返回空响应");
+            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()) {
+                log.warn("[连接测试-HTTP] 云端返回空 data, rawBody={}", body);
+                return CloudConnectionTestResult.failure(true, "云端返回空数据");
+            }
+            CloudConnectionTestResult result = new CloudConnectionTestResult();
+            result.setSuccess(dataNode.path("success").asBoolean(false));
+            result.setMessage(resolveResponseMessage(json, dataNode));
+            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
+                        : "连接测试未通过");
+                log.warn("[连接测试-HTTP] 响应缺少 message,已使用默认文案, parsedSuccess={}, rawBody={}",
+                        result.isSuccess(), body);
+            }
+            log.info("[连接测试-HTTP] 解析结果 success={}, authValid={}, tenantExists={}, message={}",
+                    result.isSuccess(), result.isAuthValid(), result.isTenantExists(), result.getMessage());
+            return result;
+        } catch (Exception e) {
+            log.error("[连接测试-HTTP] 响应解析失败, rawBody={}", body, e);
+            return CloudConnectionTestResult.failure(true, "响应解析失败");
+        }
+    }
+
+    private String resolveResponseMessage(JsonNode root, JsonNode dataNode) {
+        String message = dataNode.path("message").asText(null);
+        if (StringUtils.hasText(message)) {
+            return message;
+        }
+        message = root.path("msg").asText(null);
+        if (StringUtils.hasText(message)) {
+            return message;
+        }
+        return dataNode.path("errorMessage").asText(null);
+    }
+
+    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;
+    }
+}

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

@@ -0,0 +1,55 @@
+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";
+
+    /** 云平台数据源(sys_user、issue_sync_* 等) */
+    public static final String DS_CLOUD = "usky-cloud";
+
+    public static final String MSG_NO_SYNC_DATA =
+            "未拉取到待同步数据:请确认云端 sys_user 存在该租户数据,且 poll 认证通过(type=USER 对应表 sys_user)";
+    public static final String MSG_POLL_AUTH_FAILED = "云端 poll 认证失败,请检查凭证密钥是否与云端配置一致";
+    public static final String PATH_SYNC_DIAGNOSE = "/sync/diagnose";
+    public static final String OPERATION_SAVE_TRIGGER = "SAVE_TRIGGER";
+}

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

@@ -0,0 +1,35 @@
+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) {
+        if (code == null) {
+            throw new IllegalArgumentException("未知同步方向: null");
+        }
+        String normalized = code.trim();
+        for (SyncDirection direction : values()) {
+            if (direction.code.equalsIgnoreCase(normalized)) {
+                return direction;
+            }
+        }
+        throw new IllegalArgumentException("未知同步方向: " + code);
+    }
+}

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

@@ -0,0 +1,44 @@
+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) {
+        if (code == null) {
+            throw new IllegalArgumentException("未知同步类型: null");
+        }
+        String normalized = code.trim();
+        for (SyncTypeEnum type : values()) {
+            if (type.code.equalsIgnoreCase(normalized)) {
+                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;
+}

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

@@ -0,0 +1,109 @@
+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.CredentialMaskUtil;
+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)) {
+            log.warn("[连接测试-认证] apiKey 为空 tenantId={}", tenantId);
+            return false;
+        }
+        if (tenantId == null) {
+            log.warn("[连接测试-认证] tenantId 为空");
+            return false;
+        }
+        IssueCloudConfig config = findActiveConfig(tenantId);
+        if (config == null) {
+            log.warn("[连接测试-认证] 云端无启用配置 tenantId={}", tenantId);
+            return false;
+        }
+        boolean matched = matchCredential(apiKey, config);
+        if (!matched) {
+            log.warn("[连接测试-认证] apiKey 与云端配置不匹配 tenantId={}, configId={}, apiKey={}",
+                    tenantId, config.getId(), maskApiKey(apiKey));
+        } else {
+            log.info("[连接测试-认证] 通过 tenantId={}, configId={}", tenantId, config.getId());
+        }
+        return matched;
+    }
+
+    @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;
+        }
+    }
+
+    private String maskApiKey(String apiKey) {
+        return CredentialMaskUtil.mask(apiKey);
+    }
+}

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

@@ -0,0 +1,370 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+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 lombok.extern.slf4j.Slf4j;
+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
+ */
+@DS("usky-cloud")
+@Slf4j
+@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();
+        log.info("[连接测试] 开始 tenantId={}, cloudAddress={}, credentialKey={}",
+                tenantId, request.getCloudAddress(), CredentialMaskUtil.mask(request.getCredentialKey()));
+
+        Integer loginTenantId = SecurityUtils.getTenantId();
+        if (loginTenantId != null && !tenantId.equals(loginTenantId)) {
+            log.warn("[连接测试] 租户不一致 requestTenantId={}, loginTenantId={}", tenantId, loginTenantId);
+            throw new BusinessException("输入租户ID与登录租户不一致,请确认后重试!");
+        }
+
+        IssueCloudConfig config = findConfigByTenantId(tenantId);
+        if (config == null) {
+            log.warn("[连接测试] 配置不存在 tenantId={}", tenantId);
+            throw new BusinessException("云平台配置不存在,请先保存配置");
+        }
+        if (config.getStatus() != null && config.getStatus() == 0) {
+            log.warn("[连接测试] 集成已禁用 tenantId={}, configId={}", tenantId, config.getId());
+            throw new BusinessException("云平台集成已禁用");
+        }
+        validateCredentialKey(config, request.getCredentialKey());
+        log.info("[连接测试] 本地密钥校验通过 configId={}", config.getId());
+
+        CloudConnectionTestResult result = cloudPlatformClient.testConnection(
+                request.getCloudAddress(), request.getTenantId(), request.getCredentialKey(),
+                CloudEntitySupport.resolveAccessToken());
+
+        log.info("[连接测试] 远程结果 success={}, networkReachable={}, authValid={}, tenantExists={}, message={}, costTime={}ms",
+                result.isSuccess(), result.isNetworkReachable(), result.isAuthValid(),
+                result.isTenantExists(), result.getMessage(), result.getCostTime());
+
+        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)) {
+                log.warn("[连接测试] 密钥不一致 tenantId={}, configId={}, requestKey={}, storedKey={}",
+                        config.getTenantId(), config.getId(),
+                        CredentialMaskUtil.mask(requestKey), CredentialMaskUtil.mask(storedKey));
+                throw new BusinessException("凭证密钥与已保存配置不一致");
+            }
+        } catch (BusinessException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            log.error("[连接测试] 密钥解密失败 tenantId={}, configId={}", config.getTenantId(), config.getId(), ex);
+            throw new BusinessException("凭证密钥校验失败");
+        }
+    }
+
+    @Override
+    public CloudConnectionTestResponse validateTenantConnection(Integer tenantId) {
+        CloudConnectionTestResponse response = new CloudConnectionTestResponse();
+        if (tenantId == null) {
+            log.warn("[连接测试-租户校验] tenantId 为空");
+            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) {
+            log.warn("[连接测试-租户校验] 配置不存在 tenantId={}", tenantId);
+            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) {
+            log.warn("[连接测试-租户校验] 配置已禁用 tenantId={}, configId={}", tenantId, config.getId());
+            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);
+        log.info("[连接测试-租户校验] 通过 tenantId={}, configId={}", tenantId, config.getId());
+        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());
+        resp.setCloudAddress(config.getCloudAddress());
+        return resp;
+    }
+}

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

@@ -0,0 +1,44 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+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;
+
+    @DS("usky-cloud")
+    @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);
+    }
+}

+ 140 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncPublishServiceImpl.java

@@ -0,0 +1,140 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.issue.domain.IssueSyncDownQueue;
+import com.usky.issue.domain.SysUser;
+import com.usky.issue.mapper.IssueSyncDownQueueMapper;
+import com.usky.issue.mapper.SysUserMapper;
+import com.usky.issue.service.CloudSyncPublishService;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import com.usky.issue.service.support.CloudEntitySupport;
+import com.usky.issue.service.sync.SyncPayloadSupport;
+import com.usky.issue.service.util.SyncJsonMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+
+/**
+ * 云端下发队列发布实现
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Slf4j
+@DS("usky-cloud")
+@Service
+public class CloudSyncPublishServiceImpl implements CloudSyncPublishService {
+
+    private static final com.fasterxml.jackson.databind.ObjectMapper OBJECT_MAPPER = SyncJsonMapper.INSTANCE;
+
+    @Autowired
+    private IssueSyncDownQueueMapper downQueueMapper;
+    @Autowired
+    private SysUserMapper sysUserMapper;
+    @Lazy
+    @Autowired
+    private CloudSyncPublishService self;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void enqueueUser(SysUser user, String operation) {
+        if (user == null || user.getUserId() == null) {
+            throw new BusinessException("用户信息不完整,无法入队");
+        }
+        if (user.getTenantId() == null) {
+            throw new BusinessException("用户缺少租户ID,无法入队 userId=" + user.getUserId());
+        }
+        String op = StringUtils.hasText(operation) ? operation : "UPDATE";
+        String tableName = SyncTypeEnum.USER.getCode();
+        String dataKey = SyncPayloadSupport.buildUserDataKey(user.getUserId());
+        long syncVersion = System.currentTimeMillis();
+
+        Integer pending = downQueueMapper.selectCount(new LambdaQueryWrapper<IssueSyncDownQueue>()
+                .eq(IssueSyncDownQueue::getTenantId, user.getTenantId())
+                .eq(IssueSyncDownQueue::getTableName, tableName)
+                .eq(IssueSyncDownQueue::getStatus, 0)
+                .eq(IssueSyncDownQueue::getIsDeleted, 0)
+                .like(IssueSyncDownQueue::getPayload, "\"dataKey\":\"" + dataKey + "\""));
+        if (pending != null && pending > 0) {
+            log.debug("用户下发队列已存在待同步记录 dataKey={}", dataKey);
+            return;
+        }
+
+        try {
+            String businessPayload = OBJECT_MAPPER.writeValueAsString(user);
+            String wrappedPayload = SyncPayloadSupport.wrapBusinessPayload(
+                    user.getTenantId(), tableName, dataKey, businessPayload, syncVersion);
+
+            IssueSyncDownQueue queue = new IssueSyncDownQueue();
+            queue.setTenantId(user.getTenantId());
+            queue.setTableName(tableName);
+            queue.setOperation(op);
+            queue.setPayload(wrappedPayload);
+            queue.setSyncVersion(syncVersion);
+            queue.setStatus(0);
+            CloudEntitySupport.fillOnInsert(queue);
+            downQueueMapper.insert(queue);
+            log.info("用户已从 sys_user 入下发队列 userId={}, tenantId={}, operation={}",
+                    user.getUserId(), user.getTenantId(), op);
+        } catch (Exception e) {
+            log.error("用户入下发队列失败 userId={}, tenantId={}", user.getUserId(), user.getTenantId(), e);
+            throw new BusinessException("用户入下发队列失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int backfillUsersIfNeeded(Integer tenantId, Long lastVersion, boolean force) {
+        if (tenantId == null) {
+            return 0;
+        }
+        if (!force && lastVersion != null && lastVersion > 0) {
+            return 0;
+        }
+        String tableName = SyncTypeEnum.USER.getCode();
+        if (!force) {
+            Integer pending = downQueueMapper.selectCount(new LambdaQueryWrapper<IssueSyncDownQueue>()
+                    .eq(IssueSyncDownQueue::getTenantId, tenantId)
+                    .eq(IssueSyncDownQueue::getTableName, tableName)
+                    .eq(IssueSyncDownQueue::getStatus, 0)
+                    .eq(IssueSyncDownQueue::getIsDeleted, 0));
+            if (pending != null && pending > 0) {
+                log.info("租户{}下发队列已有{}条待同步,跳过 sys_user 回填", tenantId, pending);
+                return 0;
+            }
+        }
+
+        List<SysUser> users = sysUserMapper.selectList(new LambdaQueryWrapper<SysUser>()
+                .eq(SysUser::getTenantId, tenantId)
+                .and(w -> w.eq(SysUser::getDelFlag, "0").or().isNull(SysUser::getDelFlag)));
+        if (users.isEmpty()) {
+            log.warn("租户{}在 sys_user 无有效用户(tenant_id={} 且 del_flag=0)", tenantId, tenantId);
+            return 0;
+        }
+        log.info("从 sys_user 回填下发队列 tenantId={}, userCount={}, force={}", tenantId, users.size(), force);
+        int enqueued = 0;
+        for (SysUser user : users) {
+            self.enqueueUser(user, "INSERT");
+            enqueued++;
+        }
+        return enqueued;
+    }
+
+    @Override
+    public int countSysUsers(Integer tenantId) {
+        if (tenantId == null) {
+            return 0;
+        }
+        Integer count = sysUserMapper.selectCount(new LambdaQueryWrapper<SysUser>()
+                .eq(SysUser::getTenantId, tenantId)
+                .and(w -> w.eq(SysUser::getDelFlag, "0").or().isNull(SysUser::getDelFlag)));
+        return count == null ? 0 : count;
+    }
+}

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

@@ -0,0 +1,193 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.usky.issue.service.util.SyncJsonMapper;
+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.CloudSyncPublishService;
+import com.usky.issue.service.CloudSyncReceiveService;
+import com.usky.issue.service.enums.SyncDirection;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import com.usky.issue.service.support.CloudEntitySupport;
+import com.usky.issue.service.sync.SyncDataApplierRegistry;
+import com.usky.issue.service.sync.SyncPayloadSupport;
+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 com.fasterxml.jackson.databind.ObjectMapper OBJECT_MAPPER = SyncJsonMapper.INSTANCE;
+
+    @Autowired
+    private IssueSyncDataMapper syncDataMapper;
+    @Autowired
+    private IssueSyncDownQueueMapper downQueueMapper;
+    @Autowired
+    private CloudConfigService cloudConfigService;
+    @Autowired
+    private CloudSyncPublishService cloudSyncPublishService;
+    @Autowired
+    private SyncDataApplierRegistry syncDataApplierRegistry;
+
+    @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);
+                    applyToBusinessTable(incoming, "UPDATE");
+                    count++;
+                } else if (incoming.getSyncVersion() >= exist.getSyncVersion()) {
+                    incoming.setId(exist.getId());
+                    incoming.setSyncStatus(1);
+                    incoming.setSyncDirection("LOCAL");
+                    CloudEntitySupport.fillOnUpdate(incoming);
+                    syncDataMapper.updateById(incoming);
+                    applyToBusinessTable(incoming, "UPDATE");
+                    count++;
+                }
+            } catch (Exception e) {
+                log.error("云端写入同步数据失败", e);
+            }
+        }
+        return count;
+    }
+
+    @DS("usky-cloud")
+    @Override
+    public SyncPacket pollDownQueue(String tenantId, String tableName, Long lastVersion, boolean forceBackfill) {
+        if (lastVersion == null) {
+            lastVersion = 0L;
+        }
+        Integer tenant = Integer.valueOf(tenantId);
+        if (SyncTypeEnum.USER.getCode().equalsIgnoreCase(tableName)) {
+            int backfilled = cloudSyncPublishService.backfillUsersIfNeeded(tenant, lastVersion, forceBackfill);
+            log.info("[poll] USER 类型从 sys_user 回填 {} 条, tenantId={}, lastVersion={}, force={}",
+                    backfilled, tenantId, lastVersion, forceBackfill);
+        }
+        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()) {
+            log.info("[poll] 下发队列为空 tenantId={}, tableName={}, lastVersion={}", tenantId, tableName, lastVersion);
+            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;
+    }
+
+    private void applyToBusinessTable(IssueSyncData incoming, String operation) {
+        if (!StringUtils.hasText(incoming.getPayload()) || incoming.getTenantId() == null) {
+            return;
+        }
+        try {
+            String businessPayload = SyncPayloadSupport.resolveBusinessPayload(incoming.getPayload());
+            syncDataApplierRegistry.apply(incoming.getTenantId(), incoming.getTableName(), operation, businessPayload);
+        } catch (Exception e) {
+            log.error("云端应用同步数据到业务表失败 tableName={}, tenantId={}",
+                    incoming.getTableName(), incoming.getTenantId(), e);
+        }
+    }
+}

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

@@ -0,0 +1,203 @@
+package com.usky.issue.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.dynamic.datasource.annotation.DS;
+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.SyncDirection;
+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
+ */
+@DS("usky-cloud")
+@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 direction) {
+        String operator = CloudEntitySupport.resolveOperator();
+        String requestIp = CloudEntitySupport.resolveRequestIp();
+        SyncTypeEnum typeEnum;
+        try {
+            typeEnum = SyncTypeEnum.fromCode(syncType);
+        } catch (IllegalArgumentException ex) {
+            throw new BusinessException("无效的同步类型: " + syncType);
+        }
+        SyncDirection directionEnum;
+        try {
+            directionEnum = SyncDirection.fromCode(direction);
+        } catch (IllegalArgumentException ex) {
+            throw new BusinessException("无效的同步方向: " + direction);
+        }
+        if (directionEnum == SyncDirection.BOTH) {
+            throw new BusinessException("手动同步请指定 UP 或 DOWN 方向");
+        }
+        IssueCloudConfig config = cloudConfigService.requireActiveConfig();
+        String lockKey = CloudIntegrationConstants.LOCK_SYNC_PREFIX + typeEnum.getCode() + ":" + directionEnum.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", directionEnum);
+            operationLogService.log(config.getId(), CloudIntegrationConstants.OPERATION_MANUAL_SYNC,
+                    "手动同步 type=" + typeEnum.getCode() + " direction=" + directionEnum.getCode(),
+                    operator, requestIp);
+            redisHelper.delete(CloudIntegrationConstants.CACHE_STATUS_PREFIX + config.getId());
+            if (typeEnum.isAsync()) {
+                cloudSyncExecutor.execute(() -> cloudSyncTaskRunner.executeSyncTask(task.getId(), typeEnum, directionEnum));
+                return toTaskResponse(syncTaskMapper.selectById(task.getId()));
+            }
+            cloudSyncTaskRunner.executeSyncTask(task.getId(), typeEnum, directionEnum);
+            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", SyncDirection.BOTH);
+        cloudSyncExecutor.execute(() -> cloudSyncTaskRunner.executeSyncTask(task.getId(), syncType, SyncDirection.BOTH));
+    }
+
+    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;
+    }
+}

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

@@ -0,0 +1,165 @@
+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, SyncDirection direction) {
+        IssueCloudConfig config = configMapper.selectById(configId);
+        int pending = localSyncAgent.countPending(type, config, direction);
+        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, SyncDirection direction) {
+        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());
+        boolean fullSync = "MANUAL".equals(task.getTriggerMode()) && direction == SyncDirection.DOWN;
+        SyncResult syncResult = localSyncAgent.executeSync(type, config, direction, fullSync);
+
+        IssueSyncTask current = syncTaskMapper.selectById(taskId);
+        if (current != null && TaskStatusEnum.CANCELLED.getCode().equals(current.getTaskStatus())) {
+            return;
+        }
+
+        int total = Math.max(task.getTotalCount() == null ? 0 : task.getTotalCount(), syncResult.getTotalCount());
+        task.setTotalCount(total);
+        task.setProcessedCount(syncResult.getTotalCount());
+        task.setSuccessCount(syncResult.getSuccessCount());
+        task.setFailureCount(syncResult.getFailureCount());
+
+        if (syncResult.getFailureCount() > 0 && syncResult.getSuccessCount() == 0) {
+            task.setTaskStatus(TaskStatusEnum.FAILED.getCode());
+            task.setErrorSummary(syncResult.getErrorMessage() != null
+                    ? syncResult.getErrorMessage()
+                    : "同步失败: " + syncResult.getFailureCount() + " 条");
+        } else if (syncResult.getTotalCount() == 0) {
+            task.setTaskStatus(TaskStatusEnum.SUCCESS.getCode());
+            task.setErrorSummary(syncResult.getErrorMessage() != null
+                    ? syncResult.getErrorMessage()
+                    : CloudIntegrationConstants.MSG_NO_SYNC_DATA);
+        } else {
+            task.setTaskStatus(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() == null ? 0 : task.getTotalCount())
+                        - (task.getProcessedCount() == null ? 0 : 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;
+    }
+}

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

@@ -0,0 +1,238 @@
+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.service.util.SyncJsonMapper;
+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.sync.SyncDataApplierRegistry;
+import com.usky.issue.service.sync.SyncPayloadSupport;
+import com.usky.issue.service.constant.CloudIntegrationConstants;
+import com.usky.issue.service.vo.CloudPollResult;
+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 org.springframework.util.StringUtils;
+
+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 com.fasterxml.jackson.databind.ObjectMapper OBJECT_MAPPER = SyncJsonMapper.INSTANCE;
+
+    @Autowired
+    private IssueSyncDataMapper syncDataMapper;
+    @Autowired
+    private IssueSyncCursorMapper cursorMapper;
+    @Autowired
+    private CloudPlatformClient cloudPlatformClient;
+    @Autowired
+    private SyncDataApplierRegistry syncDataApplierRegistry;
+
+    @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, boolean forceBackfill) {
+        SyncResult result = SyncResult.empty();
+        IssueSyncCursor cursor = getOrInitCursor(tenantId, tableName, SyncDirection.DOWN.getCode());
+        Long lastVersion = cursor.getLastSyncVersion() == null ? 0L : cursor.getLastSyncVersion();
+        if (forceBackfill) {
+            lastVersion = 0L;
+            updateCursor(tenantId, tableName, SyncDirection.DOWN.getCode(), 0L);
+            log.info("手动全量拉取,已重置 DOWN 游标 tenantId={}, tableName={}", tenantId, tableName);
+        }
+
+        while (true) {
+            CloudPollResult pollResult = cloudPlatformClient.pollCloud(
+                    tenantId, tableName, lastVersion, config, forceBackfill);
+            if (pollResult.isAuthFailed()) {
+                result.setFailureCount(1);
+                result.setErrorMessage(CloudIntegrationConstants.MSG_POLL_AUTH_FAILED);
+                log.error("[pull] poll 认证失败 tenantId={}, tableName={}", tenantId, tableName);
+                break;
+            }
+            if (pollResult.isRequestFailed()) {
+                result.setFailureCount(1);
+                result.setErrorMessage(pollResult.getErrorMessage());
+                break;
+            }
+            SyncPacket packet = pollResult.getPacket();
+            if (packet == null || packet.getDataList() == null || packet.getDataList().isEmpty()) {
+                if (result.getTotalCount() == 0) {
+                    log.warn("云端未返回待同步数据 tenantId={}, tableName={}(type=USER 对应 sys_user,数据经 issue_sync_down_queue 下发)",
+                            tenantId, tableName);
+                }
+                break;
+            }
+            forceBackfill = false;
+
+            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 {
+        String rawPayload = queue.getPayload();
+        IssueSyncData data;
+        try {
+            data = OBJECT_MAPPER.readValue(rawPayload, IssueSyncData.class);
+        } catch (Exception ex) {
+            data = new IssueSyncData();
+            data.setPayload(rawPayload);
+        }
+        if (!StringUtils.hasText(data.getDataKey())) {
+            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);
+        }
+        applyToBusinessTable(queue, data);
+    }
+
+    private void applyToBusinessTable(IssueSyncDownQueue queue, IssueSyncData data) throws Exception {
+        String rawPayload = StringUtils.hasText(data.getPayload()) ? data.getPayload() : queue.getPayload();
+        if (!StringUtils.hasText(rawPayload)) {
+            return;
+        }
+        String businessPayload = SyncPayloadSupport.resolveBusinessPayload(rawPayload);
+        String operation = StringUtils.hasText(queue.getOperation()) ? queue.getOperation() : "UPDATE";
+        syncDataApplierRegistry.apply(queue.getTenantId(), queue.getTableName(), operation, businessPayload);
+    }
+
+    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()));
+    }
+
+    @Override
+    public void resetDownCursor(Integer tenantId, String tableName) {
+        updateCursor(tenantId, tableName, SyncDirection.DOWN.getCode(), 0L);
+    }
+}

+ 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));
+    }
+}

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

@@ -0,0 +1,94 @@
+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) {
+            if (fullSync) {
+                pullService.resetDownCursor(config.getTenantId(), tableName);
+            }
+            result.merge(pullService.pullByTenant(config.getTenantId(), tableName, config, fullSync));
+        }
+        return result;
+    }
+
+    @Override
+    public int countPending(SyncTypeEnum syncType, IssueCloudConfig config, SyncDirection direction) {
+        if (config == null || config.getTenantId() == null) {
+            return 0;
+        }
+        String tableName = syncType.getCode();
+        int count = 0;
+        if (direction == SyncDirection.UP || direction == SyncDirection.BOTH) {
+            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));
+            count += upCount == null ? 0 : upCount;
+        }
+        if (direction == SyncDirection.DOWN || direction == SyncDirection.BOTH) {
+            count += cloudSyncReceiveService.countPendingDownQueue(config.getTenantId(), tableName);
+        }
+        return count;
+    }
+}

+ 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);
+    }
+}

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

@@ -0,0 +1,28 @@
+package com.usky.issue.service.listener;
+
+import com.usky.issue.domain.SysUser;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * 用户数据变更事件
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+public class CloudUserChangedEvent extends ApplicationEvent {
+
+    private final String operation;
+
+    public CloudUserChangedEvent(SysUser user, String operation) {
+        super(user);
+        this.operation = operation != null ? operation : "UPDATE";
+    }
+
+    public SysUser getUser() {
+        return (SysUser) getSource();
+    }
+
+    public String getOperation() {
+        return operation;
+    }
+}

+ 28 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudUserSyncPublishListener.java

@@ -0,0 +1,28 @@
+package com.usky.issue.service.listener;
+
+import com.usky.issue.service.CloudSyncPublishService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * 用户变更时写入云端下发队列
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Slf4j
+@Component
+public class CloudUserSyncPublishListener {
+
+    @Autowired
+    private CloudSyncPublishService cloudSyncPublishService;
+
+    @EventListener
+    public void onUserChanged(CloudUserChangedEvent event) {
+        log.info("收到用户变更事件 userId={}, operation={}",
+                event.getUser().getUserId(), event.getOperation());
+        cloudSyncPublishService.enqueueUser(event.getUser(), event.getOperation());
+    }
+}

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

@@ -0,0 +1,98 @@
+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 com.usky.issue.domain.IssueCloudConfig;
+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();
+    }
+
+    /**
+     * 解析当前请求的访问令牌,用于转发至云端 HTTP 调用(Authorization: Bearer)
+     */
+    public static String resolveAccessToken() {
+        String fromHeader = resolveAuthorizationHeader();
+        if (StringUtils.isNotBlank(fromHeader)) {
+            return stripBearerPrefix(fromHeader);
+        }
+        try {
+            String fromSecurity = SecurityUtils.getToken();
+            if (StringUtils.isNotBlank(fromSecurity)) {
+                return stripBearerPrefix(fromSecurity);
+            }
+        } catch (Exception ignored) {
+            // 非 Web 上下文或无登录态
+        }
+        return null;
+    }
+
+    public static String resolveAccessToken(IssueCloudConfig config) {
+        if (config != null && StringUtils.isNotBlank(config.getToken())) {
+            return stripBearerPrefix(config.getToken());
+        }
+        return resolveAccessToken();
+    }
+
+    private static String resolveAuthorizationHeader() {
+        ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attrs == null) {
+            return null;
+        }
+        return attrs.getRequest().getHeader("Authorization");
+    }
+
+    private static String stripBearerPrefix(String token) {
+        if (token == null) {
+            return null;
+        }
+        String trimmed = token.trim();
+        if (trimmed.regionMatches(true, 0, "Bearer ", 0, 7)) {
+            return trimmed.substring(7).trim();
+        }
+        return trimmed;
+    }
+
+    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());
+    }
+}

+ 32 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/support/CloudSyncEventPublisher.java

@@ -0,0 +1,32 @@
+package com.usky.issue.service.support;
+
+import com.usky.issue.domain.SysUser;
+import com.usky.issue.service.listener.CloudUserChangedEvent;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+/**
+ * 云平台同步事件发布(供用户/组织模块在数据变更后调用)
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Component
+public class CloudSyncEventPublisher {
+
+    @Autowired
+    private ApplicationEventPublisher eventPublisher;
+
+    public void notifyUserCreated(SysUser user) {
+        eventPublisher.publishEvent(new CloudUserChangedEvent(user, "INSERT"));
+    }
+
+    public void notifyUserUpdated(SysUser user) {
+        eventPublisher.publishEvent(new CloudUserChangedEvent(user, "UPDATE"));
+    }
+
+    public void notifyUserDeleted(SysUser user) {
+        eventPublisher.publishEvent(new CloudUserChangedEvent(user, "DELETE"));
+    }
+}

+ 24 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncDataApplier.java

@@ -0,0 +1,24 @@
+package com.usky.issue.service.sync;
+
+/**
+ * 同步数据应用到本地业务表
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+public interface SyncDataApplier {
+
+    /**
+     * 是否支持该同步类型(如 USER)
+     */
+    boolean supports(String tableName);
+
+    /**
+     * 将 payload 应用到业务表
+     *
+     * @param tenantId        期望租户(来自同步队列/配置)
+     * @param operation       INSERT / UPDATE / DELETE
+     * @param businessPayload 业务 JSON(如 SysUser)
+     */
+    void apply(Integer tenantId, String operation, String businessPayload) throws Exception;
+}

+ 28 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncDataApplierRegistry.java

@@ -0,0 +1,28 @@
+package com.usky.issue.service.sync;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * 同步数据应用器路由
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Component
+public class SyncDataApplierRegistry {
+
+    @Autowired
+    private List<SyncDataApplier> appliers;
+
+    public void apply(Integer tenantId, String tableName, String operation, String businessPayload) throws Exception {
+        for (SyncDataApplier applier : appliers) {
+            if (applier.supports(tableName)) {
+                applier.apply(tenantId, operation, businessPayload);
+                return;
+            }
+        }
+    }
+}

+ 51 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncPayloadSupport.java

@@ -0,0 +1,51 @@
+package com.usky.issue.service.sync;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.issue.domain.IssueSyncData;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import com.usky.issue.service.util.SyncJsonMapper;
+import org.springframework.util.StringUtils;
+
+/**
+ * 同步 payload 解析
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+public final class SyncPayloadSupport {
+
+    private static final ObjectMapper OBJECT_MAPPER = SyncJsonMapper.INSTANCE;
+
+    private SyncPayloadSupport() {
+    }
+
+    public static String buildUserDataKey(Long userId) {
+        return SyncTypeEnum.USER.getCode() + "-" + userId;
+    }
+
+    public static String resolveBusinessPayload(String rawPayload) throws Exception {
+        if (!StringUtils.hasText(rawPayload)) {
+            return rawPayload;
+        }
+        try {
+            IssueSyncData wrapper = OBJECT_MAPPER.readValue(rawPayload, IssueSyncData.class);
+            if (StringUtils.hasText(wrapper.getPayload())) {
+                return wrapper.getPayload();
+            }
+        } catch (Exception ignored) {
+            // 非 IssueSyncData 包装,按业务 JSON 原样处理
+        }
+        return rawPayload;
+    }
+
+    public static String wrapBusinessPayload(Integer tenantId, String tableName, String dataKey,
+                                             String businessPayload, Long syncVersion) throws Exception {
+        IssueSyncData syncData = new IssueSyncData();
+        syncData.setTenantId(tenantId);
+        syncData.setTableName(tableName);
+        syncData.setDataKey(dataKey);
+        syncData.setPayload(businessPayload);
+        syncData.setSyncVersion(syncVersion);
+        return OBJECT_MAPPER.writeValueAsString(syncData);
+    }
+}

+ 78 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/impl/UserSyncDataApplier.java

@@ -0,0 +1,78 @@
+package com.usky.issue.service.sync.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.issue.service.util.SyncJsonMapper;
+import com.usky.issue.domain.SysUser;
+import com.usky.issue.mapper.SysUserMapper;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import com.usky.issue.service.sync.SyncDataApplier;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 用户同步应用到 sys_user
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Slf4j
+@Component
+public class UserSyncDataApplier implements SyncDataApplier {
+
+    private static final com.fasterxml.jackson.databind.ObjectMapper OBJECT_MAPPER = SyncJsonMapper.INSTANCE;
+
+    @Autowired
+    private SysUserMapper sysUserMapper;
+
+    @Override
+    public boolean supports(String tableName) {
+        return SyncTypeEnum.USER.getCode().equalsIgnoreCase(tableName);
+    }
+
+    @Override
+    public void apply(Integer tenantId, String operation, String businessPayload) throws Exception {
+        if (tenantId == null) {
+            throw new BusinessException("用户同步缺少租户ID");
+        }
+        SysUser user = OBJECT_MAPPER.readValue(businessPayload, SysUser.class);
+        if (user.getUserId() == null) {
+            throw new BusinessException("用户同步缺少 userId");
+        }
+        if (user.getTenantId() != null && !tenantId.equals(user.getTenantId())) {
+            throw new BusinessException("用户租户不匹配,拒绝同步 userId=" + user.getUserId());
+        }
+        user.setTenantId(tenantId);
+
+        if ("DELETE".equalsIgnoreCase(operation)) {
+            softDeleteUser(user.getUserId(), tenantId);
+            return;
+        }
+
+        if ("2".equals(user.getDelFlag())) {
+            softDeleteUser(user.getUserId(), tenantId);
+            return;
+        }
+
+        SysUser exist = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>()
+                .eq(SysUser::getUserId, user.getUserId())
+                .eq(SysUser::getTenantId, tenantId));
+        if (exist == null) {
+            sysUserMapper.insert(user);
+            log.debug("用户同步插入 userId={}, tenantId={}", user.getUserId(), tenantId);
+        } else {
+            sysUserMapper.updateById(user);
+            log.debug("用户同步更新 userId={}, tenantId={}", user.getUserId(), tenantId);
+        }
+    }
+
+    private void softDeleteUser(Long userId, Integer tenantId) {
+        sysUserMapper.update(null, new LambdaUpdateWrapper<SysUser>()
+                .eq(SysUser::getUserId, userId)
+                .eq(SysUser::getTenantId, tenantId)
+                .set(SysUser::getDelFlag, "2"));
+        log.debug("用户同步软删除 userId={}, tenantId={}", userId, tenantId);
+    }
+}

+ 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);
+        }
+    }
+}

+ 59 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/ApiResultParser.java

@@ -0,0 +1,59 @@
+package com.usky.issue.service.util;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.springframework.util.StringUtils;
+
+/**
+ * ApiResult 响应解析
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+public final class ApiResultParser {
+
+    private ApiResultParser() {
+    }
+
+    /** 本项目 ApiResult 成功:status=SUCCESS 或 code=0/"0"/200 */
+    public static boolean isSuccess(JsonNode json) {
+        if (json == null || json.isNull()) {
+            return false;
+        }
+        if (json.has("status")) {
+            String status = json.get("status").asText("");
+            if ("SUCCESS".equalsIgnoreCase(status)) {
+                return true;
+            }
+            if ("FAIL".equalsIgnoreCase(status) || "ERROR".equalsIgnoreCase(status)) {
+                return false;
+            }
+        }
+        if (json.has("code")) {
+            JsonNode codeNode = json.get("code");
+            if (codeNode.isTextual()) {
+                String code = codeNode.asText();
+                return "0".equals(code) || "200".equals(code);
+            }
+            if (codeNode.isNumber()) {
+                int code = codeNode.asInt();
+                return code == 0 || code == 200;
+            }
+        }
+        return true;
+    }
+
+    public static String resolveMessage(JsonNode json) {
+        if (json == null) {
+            return null;
+        }
+        String msg = json.path("msg").asText(null);
+        if (StringUtils.hasText(msg)) {
+            return msg;
+        }
+        msg = json.path("message").asText(null);
+        if (StringUtils.hasText(msg)) {
+            return msg;
+        }
+        return json.path("errorSummary").asText(null);
+    }
+}

+ 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);
+        }
+    }
+}

+ 26 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/SyncJsonMapper.java

@@ -0,0 +1,26 @@
+package com.usky.issue.service.util;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+/**
+ * 同步模块 JSON 序列化(支持 LocalDateTime 等 Java 8 时间类型)
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+public final class SyncJsonMapper {
+
+    public static final ObjectMapper INSTANCE = create();
+
+    private SyncJsonMapper() {
+    }
+
+    private static ObjectMapper create() {
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.registerModule(new JavaTimeModule());
+        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+        return mapper;
+    }
+}

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

@@ -0,0 +1,29 @@
+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;
+    private String cloudAddress;
+}

+ 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;
+}

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

@@ -0,0 +1,25 @@
+package com.usky.issue.service.vo;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 云端 poll 调用结果
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Data
+@Builder
+public class CloudPollResult {
+
+    private SyncPacket packet;
+
+    /** 认证失败 */
+    private boolean authFailed;
+
+    /** 网络或解析异常 */
+    private boolean requestFailed;
+
+    private String errorMessage;
+}

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

@@ -0,0 +1,24 @@
+package com.usky.issue.service.vo;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Pattern;
+
+/**
+ * 手动同步请求
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Data
+public class ManualSyncRequest {
+
+    @NotBlank(message = "同步类型不能为空")
+    private String type;
+
+    /** 同步方向:UP=本地→云端,DOWN=云端→本地 */
+    @NotBlank(message = "同步方向不能为空")
+    @Pattern(regexp = "UP|DOWN", flags = Pattern.Flag.CASE_INSENSITIVE, message = "同步方向仅支持 UP 或 DOWN")
+    private String direction;
+}

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

@@ -0,0 +1,33 @@
+package com.usky.issue.service.vo;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+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
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class SyncPacket {
+
+    private String tenantId;
+    private String tableName;
+    private SyncDirection direction;
+    private List<?> dataList;
+    private Long batchVersion;
+    private Boolean hasMore;
+    private String nonce;
+
+}

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

@@ -0,0 +1,21 @@
+package com.usky.issue.service.vo;
+
+import lombok.Data;
+
+/**
+ * 云端下发轮询请求
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@Data
+public class SyncPollRequest {
+
+    private String tenantId;
+
+    private String tableName;
+
+    private Long lastVersion;
+
+    private boolean forceBackfill;
+}

+ 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;
+}