Explorar el Código

集成中心代码提交

fuyuchuan hace 11 horas
padre
commit
3a530e6955
Se han modificado 54 ficheros con 2217 adiciones y 7 borrados
  1. 6 0
      service-issue/service-issue-biz/pom.xml
  2. 6 7
      service-issue/service-issue-biz/src/main/java/com/usky/issue/IssueApplication.java
  3. 69 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudConfigController.java
  4. 50 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudSyncController.java
  5. 49 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudTriggerEventController.java
  6. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/CloudAuditEntity.java
  7. 40 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueCloudConfig.java
  8. 32 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueOperationLog.java
  9. 32 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncDetail.java
  10. 40 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncStatus.java
  11. 46 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncTask.java
  12. 33 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueTriggerEvent.java
  13. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueCloudConfigMapper.java
  14. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueOperationLogMapper.java
  15. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncDetailMapper.java
  16. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncStatusMapper.java
  17. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncTaskMapper.java
  18. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueTriggerEventMapper.java
  19. 29 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudConfigService.java
  20. 12 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudOperationLogService.java
  21. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncService.java
  22. 16 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncTaskRunner.java
  23. 20 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudTriggerEventService.java
  24. 148 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/client/CloudPlatformClient.java
  25. 30 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/config/CloudSyncExecutorConfig.java
  26. 27 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/constant/CloudIntegrationConstants.java
  27. 40 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncTypeEnum.java
  28. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/TaskStatusEnum.java
  29. 26 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/TriggerEventCodeEnum.java
  30. 21 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/exception/CloudIntegrationErrorCode.java
  31. 218 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudConfigServiceImpl.java
  32. 33 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudOperationLogServiceImpl.java
  33. 187 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncServiceImpl.java
  34. 147 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncTaskRunnerImpl.java
  35. 92 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudTriggerEventServiceImpl.java
  36. 33 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudDataChangeListener.java
  37. 16 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudOrgChangedEvent.java
  38. 16 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudUserChangedEvent.java
  39. 30 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/support/CloudEntitySupport.java
  40. 85 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/AesGcmCipher.java
  41. 23 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/CredentialMaskUtil.java
  42. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConfigResponse.java
  43. 27 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConfigSaveRequest.java
  44. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConnectionTestResponse.java
  45. 16 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudDisableResponse.java
  46. 23 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncStatusResponse.java
  47. 28 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncTaskResponse.java
  48. 20 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/TriggerEventItemRequest.java
  49. 18 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/TriggerEventResponse.java
  50. 21 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/TriggerEventSaveRequest.java
  51. 70 0
      service-issue/service-issue-biz/src/test/java/com/usky/issue/cloud/controller/CloudIntegrationControllerTest.java
  52. 76 0
      service-issue/service-issue-biz/src/test/java/com/usky/issue/cloud/service/CloudConfigServiceImplTest.java
  53. 23 0
      service-issue/service-issue-biz/src/test/java/com/usky/issue/cloud/util/AesGcmCipherTest.java
  54. 24 0
      service-issue/service-issue-biz/src/test/java/com/usky/issue/cloud/util/CredentialMaskUtilTest.java

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

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

+ 6 - 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

@@ -28,21 +28,20 @@ import java.net.UnknownHostException;
 @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" +
                 "----------------------------------------------------------");
     }
 }

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

@@ -0,0 +1,69 @@
+package com.usky.issue.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.issue.service.vo.CloudConfigResponse;
+import com.usky.issue.service.vo.CloudConfigSaveRequest;
+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.GetMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+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.RestController;
+
+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,
+            @RequestHeader(value = "If-Match", required = false) Integer ifMatch,
+            HttpServletRequest httpRequest) {
+        if (request.getVersion() == null && ifMatch != null) {
+            request.setVersion(ifMatch);
+        }
+        return ApiResult.success(cloudConfigService.saveConfig(
+                request, resolveOperator(httpRequest), httpRequest.getRemoteAddr()));
+    }
+
+    @PostMapping("/config/testConnection")
+    public ApiResult<CloudConnectionTestResponse> testConnection(HttpServletRequest httpRequest) {
+        return ApiResult.success(cloudConfigService.testConnection(
+                resolveOperator(httpRequest), httpRequest.getRemoteAddr()));
+    }
+
+    @PostMapping("/config/disable")
+    public ApiResult<CloudDisableResponse> disable(HttpServletRequest httpRequest) {
+        return ApiResult.success(cloudConfigService.disable(
+                resolveOperator(httpRequest), httpRequest.getRemoteAddr()));
+    }
+
+    private String resolveOperator(HttpServletRequest request) {
+        String user = request.getHeader("X-User-Name");
+        return user != null ? user : "system";
+    }
+}

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

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

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

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

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

@@ -0,0 +1,25 @@
+package com.usky.issue.domain;
+
+import com.baomidou.mybatisplus.annotation.TableLogic;
+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;
+
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+
+    @TableLogic
+    private Integer deleted;
+}

+ 40 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueCloudConfig.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 com.baomidou.mybatisplus.annotation.Version;
+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 Integer tenantId;
+
+    private String credentialKey;
+
+    /** 0禁用 1启用 */
+    private Integer status;
+
+    /** 0未知 1成功 2失败 */
+    private Integer connectionStatus;
+
+    @Version
+    private Integer version;
+
+    private LocalDateTime lastTestAt;
+}

+ 32 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueOperationLog.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_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 operator;
+
+    private String requestIp;
+}

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

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

+ 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 startedAt;
+
+    private LocalDateTime finishedAt;
+}

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

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

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

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

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

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

+ 17 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/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/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> {
+}

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

@@ -0,0 +1,29 @@
+package com.usky.issue.service;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.usky.issue.service.vo.CloudConfigResponse;
+import com.usky.issue.service.vo.CloudConfigSaveRequest;
+import com.usky.issue.service.vo.CloudConnectionTestResponse;
+import com.usky.issue.service.vo.CloudDisableResponse;
+import com.usky.issue.domain.IssueCloudConfig;
+
+/**
+ * 云平台配置服务
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@DS("usky-cloud")
+public interface CloudConfigService {
+
+    CloudConfigResponse getConfig();
+
+    CloudConfigResponse saveConfig(CloudConfigSaveRequest request, String operator, String requestIp);
+
+    CloudConnectionTestResponse testConnection(String operator, String requestIp);
+
+    CloudDisableResponse disable(String operator, String requestIp);
+
+    /** 获取当前生效配置,不存在则抛业务异常 */
+    IssueCloudConfig requireActiveConfig();
+}

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

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

+ 25 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/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 operator, String requestIp);
+
+    SyncTaskResponse getTaskProgress(Long taskId);
+
+    void onOrgChanged();
+
+    void onUserChanged();
+}

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

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

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

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

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

@@ -0,0 +1,148 @@
+package com.usky.issue.service.client;
+
+import com.usky.issue.service.constant.CloudIntegrationConstants;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.service.util.AesGcmCipher;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+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.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import java.time.Duration;
+
+/**
+ * 云平台 HTTP 客户端(测试连接与数据拉取模拟)
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Slf4j
+@Component
+public class CloudPlatformClient {
+
+    private final RestTemplate restTemplate;
+    private final AesGcmCipher aesGcmCipher;
+
+    @Value("${cloud.integration.api-base-url:http://192.168.10.165:13200/offline-api/issue/cloud/config/testConnection}")
+    private String apiBaseUrl;
+
+    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;
+    }
+
+    /**
+     * 测试云平台连接,当前为模拟实现:配置完整即视为可达。
+     */
+    public boolean testConnection(IssueCloudConfig config) {
+        if (config == null || config.getTenantId() == null || config.getCredentialKey() == null) {
+            log.warn("云平台配置不完整");
+            return false;
+        }
+        
+        try {
+            String credential = aesGcmCipher.decrypt(config.getCredentialKey());
+            HttpHeaders headers = new HttpHeaders();
+            headers.add("X-Tenant-Id", String.valueOf(config.getTenantId()));
+            headers.add("X-Api-Key", credential);
+            String url = apiBaseUrl;
+            log.info("测试云平台连接 tenantId={}, url={}", config.getTenantId(), url);
+            
+            ResponseEntity<String> response = restTemplate.exchange(
+                    url, HttpMethod.POST, new HttpEntity<>(headers), String.class);
+            
+            boolean is2xx = response.getStatusCode().is2xxSuccessful();
+            String body = response.getBody();
+            
+            log.info("云平台连接测试响应: status={}, body={}", response.getStatusCode(), body);
+            
+            if (!is2xx) {
+                log.warn("云平台返回非2xx状态码: {}", response.getStatusCode());
+                return false;
+            }
+            
+            if (body == null || body.isEmpty()) {
+                log.warn("云平台返回空响应体");
+                return false;
+            }
+            
+            try {
+                com.fasterxml.jackson.databind.JsonNode json = 
+                    new com.fasterxml.jackson.databind.ObjectMapper().readTree(body);
+                
+                if (json.has("code")) {
+                    int code = json.get("code").asInt(-1);
+                    if (code != 200 && code != 0) {
+                        log.warn("云平台业务层返回错误: code={}, message={}", 
+                                code, json.has("message") ? json.get("message").asText() : "unknown");
+                        return false;
+                    }
+                }
+                
+                if (json.has("success")) {
+                    boolean success = json.get("success").asBoolean(false);
+                    if (!success) {
+                        log.warn("云平台业务层返回失败: {}", body);
+                        return false;
+                    }
+                }
+            } catch (Exception e) {
+                log.warn("解析云平台响应失败,按原始HTTP状态码判断: {}", e.getMessage());
+            }
+            
+            log.info("云平台连接测试成功");
+            return true;
+            
+        } catch (org.springframework.web.client.HttpClientErrorException ex) {
+            log.error("云平台连接失败 - HTTP客户端错误: {} {}", ex.getStatusCode(), ex.getResponseBodyAsString());
+            return false;
+        } catch (org.springframework.web.client.HttpServerErrorException ex) {
+            log.error("云平台连接失败 - HTTP服务端错误: {} {}", ex.getStatusCode(), ex.getResponseBodyAsString());
+            return false;
+        } catch (org.springframework.web.client.ResourceAccessException ex) {
+            log.error("云平台连接失败 - 网络异常: {}", ex.getMessage());
+            return false;
+        } catch (Exception ex) {
+            log.error("云平台连接测试失败: {}", ex.getMessage(), ex);
+            return false;
+        }
+    }
+
+    /**
+     * 模拟拉取待同步数据条数
+     */
+    public int fetchPendingCount(String syncType) {
+        switch (syncType) {
+            case "IOT_FACILITY":
+            case "IOT_MONITOR":
+                return 520;
+            case "BUILDING":
+                return 120;
+            case "ALARM":
+                return 15;
+            case "USER":
+                return 3;
+            case "TENANT":
+                return 1;
+            default:
+                return 10;
+        }
+    }
+
+    public java.util.List<String> fetchDataIds(String syncType, int batchSize) {
+        int count = Math.min(fetchPendingCount(syncType), batchSize);
+        java.util.List<String> ids = new java.util.ArrayList<>(count);
+        for (int i = 0; i < count; i++) {
+            ids.add(syncType + "-" + System.currentTimeMillis() + "-" + i);
+        }
+        return ids;
+    }
+}

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

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

@@ -0,0 +1,27 @@
+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 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_MANUAL_SYNC = "MANUAL_SYNC";
+    public static final String OPERATION_SAVE_TRIGGER = "SAVE_TRIGGER";
+}

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,218 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.domain.IssueSyncStatus;
+import com.usky.issue.domain.IssueSyncTask;
+import com.usky.issue.domain.IssueTriggerEvent;
+import com.usky.issue.mapper.IssueCloudConfigMapper;
+import com.usky.issue.mapper.IssueSyncStatusMapper;
+import com.usky.issue.mapper.IssueSyncTaskMapper;
+import com.usky.issue.mapper.IssueTriggerEventMapper;
+import com.usky.issue.service.CloudConfigService;
+import com.usky.issue.service.CloudOperationLogService;
+import com.usky.issue.service.client.CloudPlatformClient;
+import com.usky.issue.service.constant.CloudIntegrationConstants;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import com.usky.issue.service.enums.TaskStatusEnum;
+import com.usky.issue.service.enums.TriggerEventCodeEnum;
+import com.usky.issue.service.support.CloudEntitySupport;
+import com.usky.issue.service.util.AesGcmCipher;
+import com.usky.issue.service.util.CredentialMaskUtil;
+import com.usky.issue.service.vo.CloudConfigResponse;
+import com.usky.issue.service.vo.CloudConfigSaveRequest;
+import com.usky.issue.service.vo.CloudConnectionTestResponse;
+import com.usky.issue.service.vo.CloudDisableResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 云平台配置服务实现
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Service
+public class CloudConfigServiceImpl implements CloudConfigService {
+
+    @Autowired
+    private IssueCloudConfigMapper configMapper;
+    @Autowired
+    private IssueSyncStatusMapper syncStatusMapper;
+    @Autowired
+    private IssueTriggerEventMapper triggerEventMapper;
+    @Autowired
+    private IssueSyncTaskMapper syncTaskMapper;
+    @Autowired
+    private AesGcmCipher aesGcmCipher;
+    @Autowired
+    private CloudPlatformClient cloudPlatformClient;
+    @Autowired
+    private CloudOperationLogService operationLogService;
+
+    @Override
+    public CloudConfigResponse getConfig() {
+        IssueCloudConfig config = findActiveConfig();
+        if (config == null) {
+            return null;
+        }
+        return toResponse(config);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public CloudConfigResponse saveConfig(CloudConfigSaveRequest request, String operator, String requestIp) {
+        IssueCloudConfig existing = findActiveConfig();
+        if (existing == null) {
+            IssueCloudConfig created = new IssueCloudConfig();
+            created.setTenantId(Integer.valueOf(request.getTenantId()));
+            created.setCredentialKey(aesGcmCipher.encrypt(request.getCredentialKey()));
+            created.setStatus(1);
+            created.setConnectionStatus(0);
+            created.setVersion(0);
+            CloudEntitySupport.fillOnInsert(created);
+            configMapper.insert(created);
+            initSyncStatusRows(created.getId());
+            initTriggerEvents(created.getId());
+            operationLogService.log(created.getId(), CloudIntegrationConstants.OPERATION_SAVE_CONFIG,
+                    "新建配置 tenantId=" + request.getTenantId(), operator, requestIp);
+            return toResponse(created);
+        }
+        if (request.getVersion() != null && !request.getVersion().equals(existing.getVersion())) {
+            throw new BusinessException("配置版本冲突,请刷新后重试");
+        }
+        existing.setTenantId(Integer.valueOf(request.getTenantId()));
+        existing.setCredentialKey(aesGcmCipher.encrypt(request.getCredentialKey()));
+        existing.setStatus(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(request.getTenantId()), operator, requestIp);
+        return toResponse(configMapper.selectById(existing.getId()));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public CloudConnectionTestResponse testConnection(String operator, String requestIp) {
+        IssueCloudConfig config = requireActiveConfig();
+        boolean ok = cloudPlatformClient.testConnection(config);
+        config.setConnectionStatus(ok ? 1 : 2);
+        config.setLastTestAt(LocalDateTime.now());
+        CloudEntitySupport.fillOnUpdate(config);
+        configMapper.updateById(config);
+        operationLogService.log(config.getId(), CloudIntegrationConstants.OPERATION_TEST_CONNECTION,
+                "连接测试" + (ok ? "成功" : "失败"), operator, requestIp);
+        CloudConnectionTestResponse response = new CloudConnectionTestResponse();
+        response.setSuccess(ok);
+        response.setConnectionStatus(config.getConnectionStatus());
+        response.setMessage(ok ? "连接成功" : "连接失败");
+        return response;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public CloudDisableResponse disable(String operator, String requestIp) {
+        IssueCloudConfig config = requireActiveConfig();
+        config.setStatus(0);
+        CloudEntitySupport.fillOnUpdate(config);
+        configMapper.updateById(config);
+        int affected = cancelRunningTasks(config.getId());
+        operationLogService.log(config.getId(), CloudIntegrationConstants.OPERATION_DISABLE,
+                "禁用集成,终止任务数=" + affected, operator, requestIp);
+        CloudDisableResponse response = new CloudDisableResponse();
+        response.setDisabled(true);
+        response.setAffectedTasks(affected);
+        return response;
+    }
+
+    @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 configMapper.selectOne(new QueryWrapper<IssueCloudConfig>()
+                .eq("deleted", 0)
+                .eq("tenant_id", SecurityUtils.getTenantId())
+                .orderByDesc("id")
+                .last("LIMIT 1"));
+    }
+
+    private int cancelRunningTasks(Long configId) {
+        List<IssueSyncTask> running = syncTaskMapper.selectList(new QueryWrapper<IssueSyncTask>()
+                .eq("config_id", configId)
+                .eq("deleted", 0)
+                .in("task_status",
+                        TaskStatusEnum.PENDING.getCode(), TaskStatusEnum.RUNNING.getCode()));
+        for (IssueSyncTask task : running) {
+            task.setTaskStatus(TaskStatusEnum.CANCELLED.getCode());
+            task.setFinishedAt(LocalDateTime.now());
+            task.setErrorSummary("集成已禁用,任务终止");
+            CloudEntitySupport.fillOnUpdate(task);
+            syncTaskMapper.updateById(task);
+        }
+        return running.size();
+    }
+
+    private void initSyncStatusRows(Long configId) {
+        for (SyncTypeEnum type : SyncTypeEnum.values()) {
+            IssueSyncStatus row = new IssueSyncStatus();
+            row.setConfigId(configId);
+            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) {
+        Arrays.stream(TriggerEventCodeEnum.values()).forEach(code -> {
+            IssueTriggerEvent event = new IssueTriggerEvent();
+            event.setConfigId(configId);
+            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.setCredentialKeyMasked(CredentialMaskUtil.mask(aesGcmCipher.decrypt(config.getCredentialKey())));
+        } catch (Exception e) {
+            resp.setCredentialKeyMasked("****");
+        }
+        resp.setStatus(config.getStatus());
+        resp.setConnectionStatus(config.getConnectionStatus());
+        resp.setVersion(config.getVersion());
+        resp.setLastTestAt(config.getLastTestAt());
+        resp.setUpdatedAt(config.getUpdatedAt());
+        return resp;
+    }
+}

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

@@ -0,0 +1,33 @@
+package com.usky.issue.service.impl;
+
+import com.usky.issue.domain.IssueOperationLog;
+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;
+
+    @Override
+    public void log(Long configId, String operationType, String detail, String operator, String requestIp) {
+        IssueOperationLog log = new IssueOperationLog();
+        log.setConfigId(configId);
+        log.setOperationType(operationType);
+        log.setOperationDetail(detail);
+        log.setOperator(operator);
+        log.setRequestIp(requestIp);
+        CloudEntitySupport.fillOnInsert(log);
+        operationLogMapper.insert(log);
+    }
+}

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

@@ -0,0 +1,187 @@
+package com.usky.issue.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.common.redis.core.RedisHelper;
+import com.usky.issue.service.constant.CloudIntegrationConstants;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.domain.IssueSyncStatus;
+import com.usky.issue.domain.IssueSyncTask;
+import com.usky.issue.domain.IssueTriggerEvent;
+import com.usky.issue.service.vo.SyncStatusResponse;
+import com.usky.issue.service.vo.SyncTaskResponse;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import com.usky.issue.service.enums.TriggerEventCodeEnum;
+import com.usky.issue.mapper.IssueSyncStatusMapper;
+import com.usky.issue.mapper.IssueSyncTaskMapper;
+import com.usky.issue.mapper.IssueTriggerEventMapper;
+import com.usky.issue.service.CloudConfigService;
+import com.usky.issue.service.CloudOperationLogService;
+import com.usky.issue.service.CloudSyncService;
+import com.usky.issue.service.CloudSyncTaskRunner;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+/**
+ * 云平台数据同步服务实现
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@Slf4j
+@Service
+public class CloudSyncServiceImpl implements CloudSyncService {
+
+    @Autowired
+    private IssueSyncStatusMapper syncStatusMapper;
+    @Autowired
+    private IssueSyncTaskMapper syncTaskMapper;
+    @Autowired
+    private IssueTriggerEventMapper triggerEventMapper;
+    @Autowired
+    private CloudConfigService cloudConfigService;
+    @Autowired
+    private CloudOperationLogService operationLogService;
+    @Autowired
+    private CloudSyncTaskRunner cloudSyncTaskRunner;
+    @Autowired
+    private RedisHelper redisHelper;
+    @Autowired
+    @Qualifier("cloudSyncExecutor")
+    private Executor cloudSyncExecutor;
+
+    @Override
+    public List<SyncStatusResponse> listSyncStatus() {
+        IssueCloudConfig config = cloudConfigService.requireActiveConfig();
+        String cacheKey = CloudIntegrationConstants.CACHE_STATUS_PREFIX + config.getId();
+        Object cached = redisHelper.get(cacheKey);
+        if (cached != null) {
+            return JSON.parseArray(cached.toString(), SyncStatusResponse.class);
+        }
+        List<SyncStatusResponse> list = loadStatusFromDb(config.getId());
+        redisHelper.set(cacheKey, JSON.toJSONString(list),
+                CloudIntegrationConstants.CACHE_STATUS_TTL_SECONDS, TimeUnit.SECONDS);
+        return list;
+    }
+
+    @Override
+    public SyncTaskResponse triggerManualSync(String syncType, String operator, String requestIp) {
+        SyncTypeEnum typeEnum;
+        try {
+            typeEnum = SyncTypeEnum.fromCode(syncType);
+        } catch (IllegalArgumentException ex) {
+            throw new BusinessException("无效的同步类型: " + syncType);
+        }
+        IssueCloudConfig config = cloudConfigService.requireActiveConfig();
+        String lockKey = CloudIntegrationConstants.LOCK_SYNC_PREFIX + typeEnum.getCode();
+        String lockValue = UUID.randomUUID().toString();
+        boolean locked = redisHelper.lock(lockKey, lockValue, CloudIntegrationConstants.LOCK_SYNC_SECONDS);
+        if (!locked) {
+            throw new BusinessException("该类型同步正在进行中,请稍后再试");
+        }
+        try {
+            IssueSyncTask task = cloudSyncTaskRunner.createTask(config.getId(), typeEnum, "MANUAL");
+            operationLogService.log(config.getId(), CloudIntegrationConstants.OPERATION_MANUAL_SYNC,
+                    "手动同步 type=" + typeEnum.getCode(), operator, requestIp);
+            redisHelper.delete(CloudIntegrationConstants.CACHE_STATUS_PREFIX + config.getId());
+            if (typeEnum.isAsync()) {
+                cloudSyncExecutor.execute(() -> cloudSyncTaskRunner.executeSyncTask(task.getId(), typeEnum));
+                return toTaskResponse(syncTaskMapper.selectById(task.getId()));
+            }
+            cloudSyncTaskRunner.executeSyncTask(task.getId(), typeEnum);
+            return toTaskResponse(syncTaskMapper.selectById(task.getId()));
+        } finally {
+            redisHelper.unlockLua(lockKey, lockValue);
+        }
+    }
+
+    @Override
+    public SyncTaskResponse getTaskProgress(Long taskId) {
+        IssueSyncTask task = syncTaskMapper.selectById(taskId);
+        if (task == null || (task.getDeleted() != null && task.getDeleted() == 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::getDeleted, 0));
+        if (event == null || event.getEnabled() == null || event.getEnabled() == 0) {
+            return;
+        }
+        IssueSyncTask task = cloudSyncTaskRunner.createTask(config.getId(), syncType, "EVENT");
+        cloudSyncExecutor.execute(() -> cloudSyncTaskRunner.executeSyncTask(task.getId(), syncType));
+    }
+
+    private List<SyncStatusResponse> loadStatusFromDb(Long configId) {
+        return syncStatusMapper.selectList(new LambdaQueryWrapper<IssueSyncStatus>()
+                        .eq(IssueSyncStatus::getConfigId, configId)
+                        .eq(IssueSyncStatus::getDeleted, 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.setLastSyncAt(status.getLastSyncAt());
+        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.setStartedAt(task.getStartedAt());
+        resp.setFinishedAt(task.getFinishedAt());
+        if (task.getTotalCount() != null && task.getTotalCount() > 0 && task.getProcessedCount() != null) {
+            resp.setProgressPercent(task.getProcessedCount() * 100 / task.getTotalCount());
+        } else {
+            resp.setProgressPercent(0);
+        }
+        return resp;
+    }
+}

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

@@ -0,0 +1,147 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.usky.issue.service.client.CloudPlatformClient;
+import com.usky.issue.service.constant.CloudIntegrationConstants;
+import com.usky.issue.domain.IssueSyncDetail;
+import com.usky.issue.domain.IssueSyncStatus;
+import com.usky.issue.domain.IssueSyncTask;
+import com.usky.issue.service.enums.SyncTypeEnum;
+import com.usky.issue.service.enums.TaskStatusEnum;
+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.support.CloudEntitySupport;
+import com.usky.common.redis.core.RedisHelper;
+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.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 同步任务执行器实现
+ *
+ * @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 CloudPlatformClient cloudPlatformClient;
+    @Autowired
+    private RedisHelper redisHelper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public IssueSyncTask createTask(Long configId, SyncTypeEnum type, String triggerMode) {
+        int pending = cloudPlatformClient.fetchPendingCount(type.getCode());
+        IssueSyncTask task = new IssueSyncTask();
+        task.setConfigId(configId);
+        task.setSyncType(type.getCode());
+        task.setTaskStatus(TaskStatusEnum.PENDING.getCode());
+        task.setTriggerMode(triggerMode);
+        task.setTotalCount(pending);
+        task.setProcessedCount(0);
+        task.setSuccessCount(0);
+        task.setFailureCount(0);
+        CloudEntitySupport.fillOnInsert(task);
+        syncTaskMapper.insert(task);
+        return task;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void executeSyncTask(Long taskId, SyncTypeEnum type) {
+        IssueSyncTask task = syncTaskMapper.selectById(taskId);
+        if (task == null) {
+            return;
+        }
+        if (TaskStatusEnum.CANCELLED.getCode().equals(task.getTaskStatus())) {
+            return;
+        }
+        task.setTaskStatus(TaskStatusEnum.RUNNING.getCode());
+        task.setStartedAt(LocalDateTime.now());
+        CloudEntitySupport.fillOnUpdate(task);
+        syncTaskMapper.updateById(task);
+
+        int batchSize = type.isAsync() ? 100 : task.getTotalCount();
+        List<String> dataIds = cloudPlatformClient.fetchDataIds(type.getCode(), batchSize);
+        int success = 0;
+        int failure = 0;
+        for (String dataId : dataIds) {
+            IssueSyncTask current = syncTaskMapper.selectById(taskId);
+            if (current != null && TaskStatusEnum.CANCELLED.getCode().equals(current.getTaskStatus())) {
+                break;
+            }
+            IssueSyncDetail existing = syncDetailMapper.selectOne(new LambdaQueryWrapper<IssueSyncDetail>()
+                    .eq(IssueSyncDetail::getTaskId, taskId)
+                    .eq(IssueSyncDetail::getDataId, dataId)
+                    .eq(IssueSyncDetail::getDeleted, 0));
+            if (existing != null) {
+                continue;
+            }
+            boolean itemOk = !dataId.contains("-fail-");
+            IssueSyncDetail detail = new IssueSyncDetail();
+            detail.setTaskId(taskId);
+            detail.setDataId(dataId);
+            detail.setRetryCount(0);
+            detail.setDetailStatus(itemOk ? TaskStatusEnum.SUCCESS.getCode() : TaskStatusEnum.FAILED.getCode());
+            if (!itemOk) {
+                detail.setErrorMessage("模拟同步失败");
+                failure++;
+            } else {
+                success++;
+            }
+            CloudEntitySupport.fillOnInsert(detail);
+            syncDetailMapper.insert(detail);
+            task.setProcessedCount(task.getProcessedCount() + 1);
+            task.setSuccessCount(success);
+            task.setFailureCount(failure);
+            CloudEntitySupport.fillOnUpdate(task);
+            syncTaskMapper.updateById(task);
+        }
+
+        task = syncTaskMapper.selectById(taskId);
+        if (TaskStatusEnum.CANCELLED.getCode().equals(task.getTaskStatus())) {
+            return;
+        }
+        task.setTaskStatus(failure > 0 && success == 0
+                ? TaskStatusEnum.FAILED.getCode() : TaskStatusEnum.SUCCESS.getCode());
+        if (failure > 0) {
+            task.setErrorSummary("部分失败: " + failure + " 条");
+        }
+        task.setFinishedAt(LocalDateTime.now());
+        CloudEntitySupport.fillOnUpdate(task);
+        syncTaskMapper.updateById(task);
+        refreshSyncStatus(task.getConfigId(), type, task);
+        redisHelper.delete(CloudIntegrationConstants.CACHE_STATUS_PREFIX + task.getConfigId());
+    }
+
+    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::getDeleted, 0));
+        if (status == null) {
+            return;
+        }
+        status.setTotalCount(task.getTotalCount());
+        status.setSuccessCount(task.getSuccessCount());
+        status.setFailureCount(task.getFailureCount());
+        status.setUnsyncedCount(Math.max(0, task.getTotalCount() - task.getProcessedCount()));
+        status.setLastSyncAt(LocalDateTime.now());
+        CloudEntitySupport.fillOnUpdate(status);
+        syncStatusMapper.updateById(status);
+    }
+}

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

@@ -0,0 +1,92 @@
+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::getDeleted, 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.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;
+    }
+}

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

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

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

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

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

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

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

@@ -0,0 +1,30 @@
+package com.usky.issue.service.support;
+
+import com.usky.issue.domain.CloudAuditEntity;
+
+import java.time.LocalDateTime;
+
+/**
+ * 实体审计字段填充
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+public final class CloudEntitySupport {
+
+    private CloudEntitySupport() {
+    }
+
+    public static void fillOnInsert(CloudAuditEntity entity) {
+        LocalDateTime now = LocalDateTime.now();
+        entity.setCreatedAt(now);
+        entity.setUpdatedAt(now);
+        if (entity.getDeleted() == null) {
+            entity.setDeleted(0);
+        }
+    }
+
+    public static void fillOnUpdate(CloudAuditEntity entity) {
+        entity.setUpdatedAt(LocalDateTime.now());
+    }
+}

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

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

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

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

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

@@ -0,0 +1,25 @@
+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 credentialKeyMasked;
+    private Integer status;
+    private Integer connectionStatus;
+    private Integer version;
+    private LocalDateTime lastTestAt;
+    private LocalDateTime updatedAt;
+}

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

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

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

@@ -0,0 +1,17 @@
+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;
+    private Integer connectionStatus;
+}

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

@@ -0,0 +1,16 @@
+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;
+}

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

+ 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 startedAt;
+    private LocalDateTime finishedAt;
+}

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

+ 70 - 0
service-issue/service-issue-biz/src/test/java/com/usky/issue/cloud/controller/CloudIntegrationControllerTest.java

@@ -0,0 +1,70 @@
+package com.usky.issue.cloud.controller;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.issue.service.vo.CloudConfigResponse;
+import com.usky.issue.service.vo.SyncStatusResponse;
+import com.usky.issue.service.CloudConfigService;
+import com.usky.issue.service.CloudSyncService;
+import com.usky.issue.controller.web.CloudConfigController;
+import com.usky.issue.controller.web.CloudSyncController;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Collections;
+
+import static org.mockito.Mockito.when;
+
+/**
+ * 控制器层集成测试(Mockito 模拟依赖)
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@ExtendWith(MockitoExtension.class)
+class CloudIntegrationControllerTest {
+
+    @InjectMocks
+    private CloudConfigController cloudConfigController;
+
+    @InjectMocks
+    private CloudSyncController cloudSyncController;
+
+    @Mock
+    private CloudConfigService cloudConfigService;
+
+    @Mock
+    private CloudSyncService cloudSyncService;
+
+    @BeforeEach
+    void injectSyncController() {
+        cloudSyncController = new CloudSyncController();
+        org.springframework.test.util.ReflectionTestUtils.setField(
+                cloudSyncController, "cloudSyncService", cloudSyncService);
+    }
+
+    @Test
+    void getConfigReturnsSuccess() {
+        CloudConfigResponse config = new CloudConfigResponse();
+        config.setTenantId("tenant-001");
+        when(cloudConfigService.getConfig()).thenReturn(config);
+
+        ApiResult<CloudConfigResponse> result = cloudConfigController.getConfig();
+        Assertions.assertEquals("tenant-001", result.getData().getTenantId());
+    }
+
+    @Test
+    void listSyncStatusReturnsSuccess() {
+        SyncStatusResponse item = new SyncStatusResponse();
+        item.setSyncType("TENANT");
+        item.setTypeName("租户");
+        when(cloudSyncService.listSyncStatus()).thenReturn(Collections.singletonList(item));
+
+        ApiResult<java.util.List<SyncStatusResponse>> result = cloudSyncController.listStatus();
+        Assertions.assertEquals("TENANT", result.getData().get(0).getSyncType());
+    }
+}

+ 76 - 0
service-issue/service-issue-biz/src/test/java/com/usky/issue/cloud/service/CloudConfigServiceImplTest.java

@@ -0,0 +1,76 @@
+package com.usky.issue.cloud.service;
+
+import com.usky.issue.service.client.CloudPlatformClient;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.service.CloudOperationLogService;
+import com.usky.issue.service.vo.CloudConfigSaveRequest;
+import com.usky.issue.service.vo.CloudConfigResponse;
+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.impl.CloudConfigServiceImpl;
+import com.usky.issue.service.util.AesGcmCipher;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+/**
+ * 配置服务单元测试
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+@ExtendWith(MockitoExtension.class)
+class CloudConfigServiceImplTest {
+
+    @InjectMocks
+    private CloudConfigServiceImpl cloudConfigService;
+
+    @Mock
+    private IssueCloudConfigMapper configMapper;
+    @Mock
+    private IssueSyncStatusMapper syncStatusMapper;
+    @Mock
+    private IssueTriggerEventMapper triggerEventMapper;
+    @Mock
+    private IssueSyncTaskMapper syncTaskMapper;
+    @Mock
+    private AesGcmCipher aesGcmCipher;
+    @Mock
+    private CloudPlatformClient cloudPlatformClient;
+    @Mock
+    private CloudOperationLogService operationLogService;
+
+    @BeforeEach
+    void setUp() {
+        when(aesGcmCipher.encrypt(any())).thenReturn("encrypted-value");
+    }
+
+    @Test
+    void saveConfigCreatesSingleton() {
+        when(configMapper.selectOne(any())).thenReturn(null);
+        when(configMapper.insert(any())).thenAnswer(invocation -> {
+            IssueCloudConfig config = invocation.getArgument(0);
+            config.setId(1L);
+            config.setVersion(0);
+            return 1;
+        });
+        when(aesGcmCipher.decrypt("encrypted-value")).thenReturn("secret");
+
+        CloudConfigSaveRequest request = new CloudConfigSaveRequest();
+        request.setTenantId("tenant-001");
+        request.setCredentialKey("secret");
+
+        CloudConfigResponse response = cloudConfigService.saveConfig(request, "admin", "127.0.0.1");
+        Assertions.assertNotNull(response);
+        Assertions.assertEquals("tenant-001", response.getTenantId());
+    }
+}

+ 23 - 0
service-issue/service-issue-biz/src/test/java/com/usky/issue/cloud/util/AesGcmCipherTest.java

@@ -0,0 +1,23 @@
+package com.usky.issue.cloud.util;
+
+import com.usky.issue.service.util.AesGcmCipher;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * AES-GCM 加解密单元测试
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+class AesGcmCipherTest {
+
+    @Test
+    void encryptDecryptRoundTrip() {
+        AesGcmCipher cipher = new AesGcmCipher("0123456789abcdef");
+        String plain = "my-secret-credential-key";
+        String encrypted = cipher.encrypt(plain);
+        Assertions.assertNotEquals(plain, encrypted);
+        Assertions.assertEquals(plain, cipher.decrypt(encrypted));
+    }
+}

+ 24 - 0
service-issue/service-issue-biz/src/test/java/com/usky/issue/cloud/util/CredentialMaskUtilTest.java

@@ -0,0 +1,24 @@
+package com.usky.issue.cloud.util;
+
+import com.usky.issue.service.util.CredentialMaskUtil;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * 凭证脱敏单元测试
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+class CredentialMaskUtilTest {
+
+    @Test
+    void maskLongValue() {
+        Assertions.assertEquals("ab****yz", CredentialMaskUtil.mask("abcdefghyz"));
+    }
+
+    @Test
+    void maskShortValue() {
+        Assertions.assertEquals("****", CredentialMaskUtil.mask("ab"));
+    }
+}