fuyuchuan 4 дней назад
Родитель
Сommit
47cd30308a
39 измененных файлов с 342 добавлено и 1366 удалено
  1. 10 25
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudConfigController.java
  2. 1 2
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudSyncController.java
  3. 8 5
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/CloudAuditEntity.java
  4. 2 2
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueCloudConfig.java
  5. 30 32
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueOperationLog.java
  6. 0 2
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncCursor.java
  7. 0 2
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncData.java
  8. 0 2
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncDownQueue.java
  9. 3 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SpHj2017.java
  10. 3 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SpOwner.java
  11. 3 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SpProjectConfig.java
  12. 3 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SpRtu2017.java
  13. 3 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SpSj2017.java
  14. 3 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/YtDeviceStatus.java
  15. 4 4
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudConfigService.java
  16. 25 25
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncService.java
  17. 100 60
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/client/CloudPlatformClient.java
  18. 1 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/constant/CloudIntegrationConstants.java
  19. 1 1
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudAuthServiceImpl.java
  20. 66 26
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudConfigServiceImpl.java
  21. 10 1
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudOperationLogServiceImpl.java
  22. 3 3
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncReceiveServiceImpl.java
  23. 7 4
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncServiceImpl.java
  24. 7 3
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncTaskRunnerImpl.java
  25. 2 1
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudTriggerEventServiceImpl.java
  26. 3 3
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalPullServiceImpl.java
  27. 2 2
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalPushServiceImpl.java
  28. 1 1
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalSyncAgentImpl.java
  29. 24 18
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/support/CloudEntitySupport.java
  30. 4 4
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConfigResponse.java
  31. 6 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConnectionTestRequest.java
  32. 7 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudDisableResponse.java
  33. 0 51
      service-issue/service-issue-biz/src/main/resources/sql/issue_cloud_sync_tables.sql
  34. 0 796
      service-issue/service-issue-biz/src/main/resources/本地云端双向数据同步与连通性测试方案.md
  35. 0 70
      service-issue/service-issue-biz/src/test/java/com/usky/issue/cloud/controller/CloudIntegrationControllerTest.java
  36. 0 76
      service-issue/service-issue-biz/src/test/java/com/usky/issue/cloud/service/CloudConfigServiceImplTest.java
  37. 0 98
      service-issue/service-issue-biz/src/test/java/com/usky/issue/cloud/service/CloudConnectionTestServiceTest.java
  38. 0 23
      service-issue/service-issue-biz/src/test/java/com/usky/issue/cloud/util/AesGcmCipherTest.java
  39. 0 24
      service-issue/service-issue-biz/src/test/java/com/usky/issue/cloud/util/CredentialMaskUtilTest.java

+ 10 - 25
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudConfigController.java

@@ -10,13 +10,7 @@ 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 org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.validation.Valid;
@@ -48,9 +42,8 @@ public class CloudConfigController {
      */
     @PutMapping("/config")
     public ApiResult<CloudConfigResponse> saveConfig(
-            @Valid @RequestBody CloudConfigSaveRequest request, HttpServletRequest httpRequest) {
-        return ApiResult.success(cloudConfigService.saveConfig(
-                request, SecurityUtils.getUsername(), httpRequest.getRemoteAddr()));
+            @Valid @RequestBody CloudConfigSaveRequest request) {
+        return ApiResult.success(cloudConfigService.saveConfig(request));
     }
 
 
@@ -59,25 +52,17 @@ public class CloudConfigController {
      */
     @PostMapping("/config/testConnection")
     public ApiResult<CloudConnectionTestResponse> testConnection(
-            @Valid @RequestBody(required = false) CloudConnectionTestRequest request,
-            HttpServletRequest httpRequest) {
-        if (request == null) {
-            request = new CloudConnectionTestRequest();
-        }
-        if (request.getTenantId() == null) {
-            request.setTenantId(SecurityUtils.getTenantId());
-        }
-        return ApiResult.success(cloudConfigService.testConnection(
-                request, SecurityUtils.getUsername(), httpRequest.getRemoteAddr()));
+            @Valid @RequestBody CloudConnectionTestRequest request
+    ) {
+        return ApiResult.success(cloudConfigService.testConnection(request));
     }
 
     /**
-     * 禁用云平台
+     * 启用/禁用云平台(status: 0-禁用, 1-启用)
      */
-    @PostMapping("/config/disable")
-    public ApiResult<CloudDisableResponse> disable(HttpServletRequest httpRequest) {
-        return ApiResult.success(cloudConfigService.disable(
-                SecurityUtils.getUsername(), httpRequest.getRemoteAddr()));
+    @PutMapping("/config/disable")
+    public ApiResult<CloudDisableResponse> disable(@RequestParam Integer status) {
+        return ApiResult.success(cloudConfigService.disable(status));
     }
 
 }

+ 1 - 2
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudSyncController.java

@@ -35,8 +35,7 @@ public class CloudSyncController {
 
     @PostMapping("/sync/{type}")
     public ApiResult<SyncTaskResponse> manualSync(@PathVariable String type, HttpServletRequest request) {
-        return ApiResult.success(cloudSyncService.triggerManualSync(
-                type, SecurityUtils.getUsername(), request.getRemoteAddr()));
+        return ApiResult.success(cloudSyncService.triggerManualSync(type));
     }
 
     @GetMapping("/sync/task/{taskId}")

+ 8 - 5
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/CloudAuditEntity.java

@@ -18,16 +18,19 @@ public abstract class CloudAuditEntity implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
-    private String createBy;
+    /** 租户ID */
+    private Integer tenantId;
+
+    private String createdBy;
 
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
-    private LocalDateTime createTime;
+    private LocalDateTime createdTime;
 
-    private String updateBy;
+    private String updatedBy;
 
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
-    private LocalDateTime updateTime;
+    private LocalDateTime updatedTime;
 
     @TableLogic(value = "0", delval = "1")
-    private Integer deleted;
+    private Integer isDeleted;
 }

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

@@ -21,8 +21,8 @@ public class IssueCloudConfig extends CloudAuditEntity {
     @TableId(value = "id", type = IdType.AUTO)
     private Long id;
 
-    /** 租户ID */
-    private Integer tenantId;
+    /** 云平台地址 */
+    private String cloudAddress;
 
     /** AES-256-GCM加密后的凭证密钥 */
     private String credentialKey;

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

@@ -1,32 +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 createBy;
-
-    private String requestIp;
-}
+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;

+}


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

@@ -22,8 +22,6 @@ public class IssueSyncCursor extends CloudAuditEntity {
     @TableId(value = "id", type = IdType.AUTO)
     private Long id;
 
-    private Integer tenantId;
-
     private String tableName;
 
     /** UP/DOWN */

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

@@ -22,8 +22,6 @@ public class IssueSyncData extends CloudAuditEntity {
     @TableId(value = "id", type = IdType.AUTO)
     private Long id;
 
-    private Integer tenantId;
-
     private String tableName;
 
     private String dataKey;

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

@@ -20,8 +20,6 @@ public class IssueSyncDownQueue extends CloudAuditEntity {
     @TableId(value = "id", type = IdType.AUTO)
     private Long id;
 
-    private Integer tenantId;
-
     private String tableName;
 
     /** INSERT/UPDATE/DELETE */

+ 3 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SpHj2017.java

@@ -28,6 +28,9 @@ public class SpHj2017 implements Serializable {
     @TableId(value = "id", type = IdType.AUTO)
     private Long id;
 
+    /** 租户ID */
+    private Integer tenantId;
+
     private String port;
 
     /**

+ 3 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SpOwner.java

@@ -28,6 +28,9 @@ public class SpOwner implements Serializable {
     @TableId(value = "id", type = IdType.AUTO)
     private Integer id;
 
+    /** 租户ID */
+    private Integer tenantId;
+
     /**
      * 单位编号
      */

+ 3 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SpProjectConfig.java

@@ -23,6 +23,9 @@ public class SpProjectConfig implements Serializable {
     @TableId(value = "id", type = IdType.AUTO)
     private Integer id;
 
+    /** 租户ID */
+    private Integer tenantId;
+
     /**
      * 项目名称
      */

+ 3 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SpRtu2017.java

@@ -26,6 +26,9 @@ public class SpRtu2017 implements Serializable {
     @TableId(value = "id", type = IdType.AUTO)
     private Long id;
 
+    /** 租户ID */
+    private Integer tenantId;
+
     private String port;
 
     /**

+ 3 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SpSj2017.java

@@ -26,6 +26,9 @@ public class SpSj2017 implements Serializable {
     @TableId(value = "id", type = IdType.AUTO)
     private Long id;
 
+    /** 租户ID */
+    private Integer tenantId;
+
     private String port;
 
     /**

+ 3 - 0
service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/YtDeviceStatus.java

@@ -26,6 +26,9 @@ public class YtDeviceStatus implements Serializable {
      */
     private Integer id;
 
+    /** 租户ID */
+    private Integer tenantId;
+
     /**
      * 单位编号
      */

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

@@ -1,12 +1,12 @@
 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;
-import com.usky.issue.domain.IssueCloudConfig;
 
 /**
  * 云平台配置服务
@@ -19,16 +19,16 @@ public interface CloudConfigService {
 
     CloudConfigResponse getConfig();
 
-    CloudConfigResponse saveConfig(CloudConfigSaveRequest request, String operator, String requestIp);
+    CloudConfigResponse saveConfig(CloudConfigSaveRequest request);
 
-    CloudConnectionTestResponse testConnection(CloudConnectionTestRequest request, String operator, String requestIp);
+    CloudConnectionTestResponse testConnection(CloudConnectionTestRequest request);
 
     /**
      * 云平台侧租户配置校验(供集成 API 调用,查询云库后返回结果,不再向外发起 HTTP)
      */
     CloudConnectionTestResponse validateTenantConnection(Integer tenantId);
 
-    CloudDisableResponse disable(String operator, String requestIp);
+    CloudDisableResponse disable(Integer status);
 
     /** 获取当前生效配置,不存在则抛业务异常 */
     IssueCloudConfig requireActiveConfig();

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

@@ -1,25 +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();
-}
+package com.usky.issue.service;

+

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

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

+

+import java.util.List;

+

+/**

+ * 云平台数据同步服务

+ *

+ * @author fyc

+ * @date 2026-05-21

+ */

+public interface CloudSyncService {

+

+    List<SyncStatusResponse> listSyncStatus();

+

+    SyncTaskResponse triggerManualSync(String syncType);

+

+    SyncTaskResponse getTaskProgress(Long taskId);

+

+    void onOrgChanged();

+

+    void onUserChanged();

+}


+ 100 - 60
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/client/CloudPlatformClient.java

@@ -10,7 +10,6 @@ import com.usky.issue.service.vo.CloudConnectionTestResult;
 import com.usky.issue.service.vo.SyncPacket;
 import com.usky.issue.service.vo.SyncResponse;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.web.client.RestTemplateBuilder;
 import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpHeaders;
@@ -43,8 +42,8 @@ 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/service-issue/cloud/integration/testConnection}")
-    private String apiBaseUrl;
+    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
@@ -55,59 +54,53 @@ public class CloudPlatformClient {
     }
 
     /**
-     * 分层连通性测试:网络层 → 认证层 → 租户层
+     * 使用请求参数发起连通性测试:cloudAddress + URL_END
      */
-    public CloudConnectionTestResult testConnection(IssueCloudConfig config) {
-        if (config == null || config.getTenantId() == null || config.getCredentialKey() == null) {
-            log.warn("云平台配置不完整");
-            return CloudConnectionTestResult.failure(false, "云平台配置不完整");
+    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();
-        String integrationBase = resolveIntegrationBaseUrl();
 
         try {
-            String pingUrl = integrationBase + CloudIntegrationConstants.PATH_PING;
-            restTemplate.getForObject(pingUrl, String.class);
+            HttpHeaders headers = buildAuthHeaders(tenantId, credentialKey, token);
+            ResponseEntity<String> response = restTemplate.exchange(
+                    testUrl, HttpMethod.POST, new HttpEntity<>(headers), String.class);
+            return parseConnectionTestResult(response.getBody(), System.currentTimeMillis() - start);
         } catch (ResourceAccessException ex) {
-            log.error("云平台网络不可达: {}", ex.getMessage());
+            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("云平台 Ping 失败: {}", ex.getMessage());
-            return CloudConnectionTestResult.networkFailure("网络层检测失败:" + ex.getMessage());
+            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());
-            HttpHeaders headers = buildAuthHeaders(config, credential);
-            String authCheckUrl = UriComponentsBuilder
-                    .fromHttpUrl(integrationBase + CloudIntegrationConstants.PATH_AUTH_CHECK)
-                    .queryParam("tenantId", config.getTenantId())
-                    .toUriString();
-
-            ResponseEntity<String> authResp = restTemplate.exchange(
-                    authCheckUrl, HttpMethod.GET, new HttpEntity<>(headers), String.class);
-
-            long costTime = System.currentTimeMillis() - start;
-            String body = authResp.getBody();
-            if (CloudIntegrationConstants.AUTH_CHECK_VALID.equals(body)) {
-                return CloudConnectionTestResult.fullSuccess(costTime);
-            }
-            if (CloudIntegrationConstants.AUTH_CHECK_TENANT_MISSING.equals(body)) {
-                return CloudConnectionTestResult.tenantMissing(costTime);
-            }
-            return CloudConnectionTestResult.authFailure(costTime, "认证层检测未通过");
-        } catch (HttpClientErrorException.Unauthorized ex) {
-            return CloudConnectionTestResult.authFailure(System.currentTimeMillis() - start, "认证失败:Token无效");
-        } catch (HttpClientErrorException | HttpServerErrorException ex) {
-            log.error("云平台认证检测失败: {} {}", ex.getStatusCode(), ex.getResponseBodyAsString());
-            return CloudConnectionTestResult.authFailure(System.currentTimeMillis() - start,
-                    "云平台返回异常:" + ex.getStatusCode());
-        } catch (ResourceAccessException ex) {
-            return CloudConnectionTestResult.networkFailure(CloudIntegrationConstants.MSG_NETWORK_UNREACHABLE);
+            return testConnection(config.getCloudAddress(), config.getTenantId(), credential, config.getToken());
         } catch (Exception ex) {
-            log.error("云平台连接测试失败: {}", ex.getMessage(), ex);
-            return CloudConnectionTestResult.failure(true, ex.getMessage());
+            log.error("凭证解密失败 tenantId={}", config.getTenantId(), ex);
+            return CloudConnectionTestResult.failure(false, "凭证解密失败");
         }
     }
 
@@ -120,7 +113,7 @@ public class CloudPlatformClient {
             headers.add(CloudIntegrationConstants.HEADER_SIGN, HmacSignUtil.sign(body, credential));
 
             ResponseEntity<String> response = restTemplate.exchange(
-                    resolveIntegrationBaseUrl() + CloudIntegrationConstants.PATH_SYNC_RECEIVE,
+                    resolveIntegrationBaseUrl(config) + CloudIntegrationConstants.PATH_SYNC_RECEIVE,
                     HttpMethod.POST,
                     new HttpEntity<>(body, headers),
                     String.class);
@@ -136,7 +129,7 @@ public class CloudPlatformClient {
             String credential = aesGcmCipher.decrypt(config.getCredentialKey());
             HttpHeaders headers = buildAuthHeaders(config, credential);
             String url = UriComponentsBuilder
-                    .fromHttpUrl(resolveIntegrationBaseUrl() + CloudIntegrationConstants.PATH_SYNC_POLL)
+                    .fromHttpUrl(resolveIntegrationBaseUrl(config) + CloudIntegrationConstants.PATH_SYNC_POLL)
                     .queryParam("tenantId", tenantId)
                     .queryParam("tableName", tableName)
                     .queryParam("lastVersion", lastVersion == null ? 0L : lastVersion)
@@ -167,7 +160,7 @@ public class CloudPlatformClient {
             String body = OBJECT_MAPPER.writeValueAsString(queueIds);
 
             ResponseEntity<String> response = restTemplate.exchange(
-                    resolveIntegrationBaseUrl() + CloudIntegrationConstants.PATH_SYNC_POLL_ACK,
+                    resolveIntegrationBaseUrl(config) + CloudIntegrationConstants.PATH_SYNC_POLL_ACK,
                     HttpMethod.POST,
                     new HttpEntity<>(body, headers),
                     String.class);
@@ -179,42 +172,89 @@ public class CloudPlatformClient {
     }
 
     private HttpHeaders buildAuthHeaders(IssueCloudConfig config, String credential) {
+        return buildAuthHeaders(config.getTenantId(), credential, config.getToken());
+    }
+
+    private HttpHeaders buildAuthHeaders(Integer tenantId, String credential, String token) {
         HttpHeaders headers = new HttpHeaders();
-        headers.add(CloudIntegrationConstants.HEADER_TENANT_ID, String.valueOf(config.getTenantId()));
+        headers.add(CloudIntegrationConstants.HEADER_TENANT_ID, String.valueOf(tenantId));
         headers.add(CloudIntegrationConstants.HEADER_API_KEY, credential);
-        if (StringUtils.hasText(config.getToken())) {
-            headers.add("Authorization", "Bearer " + config.getToken());
+        if (StringUtils.hasText(token)) {
+            headers.add("Authorization", "Bearer " + token);
         }
         return headers;
     }
 
-    private SyncResponse parseSyncResponse(String body) {
+    private CloudConnectionTestResult parseConnectionTestResult(String body, long costTime) {
         if (!StringUtils.hasText(body)) {
-            return SyncResponse.builder().success(false).message("云端返回空响应").build();
+            return CloudConnectionTestResult.failure(true, "云端返回空响应");
         }
         try {
             JsonNode json = OBJECT_MAPPER.readTree(body);
             JsonNode dataNode = json.has("data") ? json.get("data") : json;
-            return OBJECT_MAPPER.treeToValue(dataNode, SyncResponse.class);
+            if (dataNode == null || dataNode.isNull()) {
+                return CloudConnectionTestResult.failure(true, "云端返回空数据");
+            }
+            CloudConnectionTestResult result = new CloudConnectionTestResult();
+            result.setSuccess(dataNode.path("success").asBoolean(false));
+            result.setMessage(dataNode.path("message").asText(null));
+            result.setNetworkReachable(dataNode.has("networkReachable")
+                    ? dataNode.get("networkReachable").asBoolean() : true);
+            result.setAuthValid(dataNode.has("authValid")
+                    ? dataNode.get("authValid").asBoolean() : result.isSuccess());
+            result.setTenantExists(dataNode.has("tenantExists")
+                    ? dataNode.get("tenantExists").asBoolean() : result.isSuccess());
+            result.setCostTime(dataNode.has("costTime") ? dataNode.get("costTime").asLong() : costTime);
+            if (!StringUtils.hasText(result.getMessage())) {
+                result.setMessage(result.isSuccess()
+                        ? CloudIntegrationConstants.MSG_CONNECTION_SUCCESS
+                        : "连接测试未通过");
+            }
+            return result;
         } catch (Exception e) {
-            return SyncResponse.builder().success(false).message("响应解析失败").build();
+            log.error("连接测试响应解析失败: {}", body, e);
+            return CloudConnectionTestResult.failure(true, "响应解析失败");
         }
     }
 
-    String resolveIntegrationBaseUrl() {
-        if (!StringUtils.hasText(apiBaseUrl)) {
+    private String buildTestConnectionUrl(String cloudAddress) {
+        return normalizeCloudBaseUrl(cloudAddress) + URL_END;
+    }
+
+    private String resolveIntegrationBaseUrl(IssueCloudConfig config) {
+        if (config == null || !StringUtils.hasText(config.getCloudAddress())) {
             return "";
         }
-        String url = apiBaseUrl.trim();
+        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());
         }
-        if (url.endsWith("/cloud/integration")) {
-            return url;
+        return url;
+    }
+
+    private SyncResponse parseSyncResponse(String body) {
+        if (!StringUtils.hasText(body)) {
+            return SyncResponse.builder().success(false).message("云端返回空响应").build();
         }
-        if (url.endsWith("/")) {
-            return url.substring(0, url.length() - 1);
+        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();
         }
-        return url;
     }
 }

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

@@ -41,6 +41,7 @@ public final class CloudIntegrationConstants {
     public static final String OPERATION_SAVE_CONFIG = "SAVE_CONFIG";
     public static final String OPERATION_TEST_CONNECTION = "TEST_CONNECTION";
     public static final String OPERATION_DISABLE = "DISABLE_CONFIG";
+    public static final String OPERATION_ENABLE = "ENABLE_CONFIG";
     public static final String OPERATION_MANUAL_SYNC = "MANUAL_SYNC";
     public static final String OPERATION_SAVE_TRIGGER = "SAVE_TRIGGER";
 }

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

@@ -72,7 +72,7 @@ public class CloudAuthServiceImpl implements CloudAuthService {
 
     private IssueCloudConfig findActiveConfig(Integer tenantId) {
         return configMapper.selectOne(new QueryWrapper<IssueCloudConfig>()
-                .eq("deleted", 0)
+                .eq("is_deleted", 0)
                 .eq("tenant_id", tenantId)
                 .eq("status", 1)
                 .orderByDesc("id")

+ 66 - 26
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudConfigServiceImpl.java

@@ -70,15 +70,15 @@ public class CloudConfigServiceImpl implements CloudConfigService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public CloudConfigResponse saveConfig(CloudConfigSaveRequest request, String operator, String requestIp) {
+    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与登录租户不一致,请确认后重试!");
         }
 
-        String userName = SecurityUtils.getUsername();
-
         IssueCloudConfig existing = findActiveConfig();
         if (existing == null) {
             IssueCloudConfig created = new IssueCloudConfig();
@@ -87,11 +87,10 @@ public class CloudConfigServiceImpl implements CloudConfigService {
             created.setStatus(1);
             created.setConnectionStatus(0);
             created.setVersion(1);
-            created.setCreateBy(userName);
             CloudEntitySupport.fillOnInsert(created);
             configMapper.insert(created);
-            initSyncStatusRows(created.getId());
-            initTriggerEvents(created.getId());
+            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);
@@ -116,7 +115,9 @@ public class CloudConfigServiceImpl implements CloudConfigService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public CloudConnectionTestResponse testConnection(CloudConnectionTestRequest request, String operator, String requestIp) {
+    public CloudConnectionTestResponse testConnection(CloudConnectionTestRequest request) {
+        String operator = CloudEntitySupport.resolveOperator();
+        String requestIp = CloudEntitySupport.resolveRequestIp();
         Integer tenantId = request.getTenantId();
         Integer loginTenantId = SecurityUtils.getTenantId();
         if (loginTenantId != null && !tenantId.equals(loginTenantId)) {
@@ -130,9 +131,11 @@ public class CloudConfigServiceImpl implements CloudConfigService {
         if (config.getStatus() != null && config.getStatus() == 0) {
             throw new BusinessException("云平台集成已禁用");
         }
+        validateCredentialKey(config, request.getCredentialKey());
 
-        config.setToken(SecurityUtils.getToken());
-        CloudConnectionTestResult result = cloudPlatformClient.testConnection(config);
+        CloudConnectionTestResult result = cloudPlatformClient.testConnection(
+                request.getCloudAddress(), request.getTenantId(), request.getCredentialKey(),
+                SecurityUtils.getToken());
 
         config.setConnectionStatus(result.isSuccess() ? 1 : 2);
         config.setLastTestTime(LocalDateTime.now());
@@ -143,6 +146,19 @@ public class CloudConfigServiceImpl implements CloudConfigService {
         return toConnectionTestResponse(result);
     }
 
+    private void validateCredentialKey(IssueCloudConfig config, String requestKey) {
+        try {
+            String storedKey = aesGcmCipher.decrypt(config.getCredentialKey());
+            if (!storedKey.equals(requestKey)) {
+                throw new BusinessException("凭证密钥与已保存配置不一致");
+            }
+        } catch (BusinessException ex) {
+            throw ex;
+        } catch (Exception ex) {
+            throw new BusinessException("凭证密钥校验失败");
+        }
+    }
+
     @Override
     public CloudConnectionTestResponse validateTenantConnection(Integer tenantId) {
         CloudConnectionTestResponse response = new CloudConnectionTestResponse();
@@ -187,20 +203,42 @@ public class CloudConfigServiceImpl implements CloudConfigService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public CloudDisableResponse disable(String operator, String requestIp) {
-        IssueCloudConfig config = requireActiveConfig();
-        config.setStatus(0);
+    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);
-        int affected = cancelRunningTasks(config.getId());
-        operationLogService.log(config.getId(), CloudIntegrationConstants.OPERATION_DISABLE,
-                "禁用集成,终止任务数=" + affected, operator, requestIp);
+
+        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(true);
+        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();
@@ -222,7 +260,7 @@ public class CloudConfigServiceImpl implements CloudConfigService {
             return null;
         }
         return configMapper.selectOne(new QueryWrapper<IssueCloudConfig>()
-                .eq("deleted", 0)
+                .eq("is_deleted", 0)
                 .eq("tenant_id", tenantId)
                 .orderByDesc("id")
                 .last("LIMIT 1"));
@@ -243,7 +281,7 @@ public class CloudConfigServiceImpl implements CloudConfigService {
     private int cancelRunningTasks(Long configId) {
         List<IssueSyncTask> running = syncTaskMapper.selectList(new QueryWrapper<IssueSyncTask>()
                 .eq("config_id", configId)
-                .eq("deleted", 0)
+                .eq("is_deleted", 0)
                 .in("task_status",
                         TaskStatusEnum.PENDING.getCode(), TaskStatusEnum.RUNNING.getCode()));
         for (IssueSyncTask task : running) {
@@ -256,10 +294,11 @@ public class CloudConfigServiceImpl implements CloudConfigService {
         return running.size();
     }
 
-    private void initSyncStatusRows(Long configId) {
+    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);
@@ -271,10 +310,11 @@ public class CloudConfigServiceImpl implements CloudConfigService {
         }
     }
 
-    private void initTriggerEvents(Long configId) {
+    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());
@@ -289,18 +329,18 @@ public class CloudConfigServiceImpl implements CloudConfigService {
         resp.setId(config.getId());
         resp.setTenantId(String.valueOf(config.getTenantId()));
         try {
-            resp.setCredentialKeyMasked(CredentialMaskUtil.mask(aesGcmCipher.decrypt(config.getCredentialKey())));
+            resp.setCredentialKey(aesGcmCipher.decrypt(config.getCredentialKey()));
         } catch (Exception e) {
-            resp.setCredentialKeyMasked("****");
+            resp.setCredentialKey(null);
         }
         resp.setStatus(config.getStatus());
         resp.setConnectionStatus(config.getConnectionStatus());
         resp.setVersion(config.getVersion());
         resp.setLastTestTime(config.getLastTestTime());
-        resp.setUpdatedTime(config.getUpdateTime());
-        resp.setUpdatedBy(config.getUpdateBy());
-        resp.setCreateTime(config.getCreateTime());
-        resp.setCreateBy(config.getCreateBy());
+        resp.setUpdatedTime(config.getUpdatedTime());
+        resp.setUpdatedBy(config.getUpdatedBy());
+        resp.setCreatedTime(config.getCreatedTime());
+        resp.setCreatedBy(config.getCreatedBy());
         return resp;
     }
 }

+ 10 - 1
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudOperationLogServiceImpl.java

@@ -1,6 +1,8 @@
 package com.usky.issue.service.impl;
 
+import com.usky.issue.domain.IssueCloudConfig;
 import com.usky.issue.domain.IssueOperationLog;
+import com.usky.issue.mapper.IssueCloudConfigMapper;
 import com.usky.issue.mapper.IssueOperationLogMapper;
 import com.usky.issue.service.CloudOperationLogService;
 import com.usky.issue.service.support.CloudEntitySupport;
@@ -19,15 +21,22 @@ public class CloudOperationLogServiceImpl implements CloudOperationLogService {
     @Autowired
     private IssueOperationLogMapper operationLogMapper;
 
+    @Autowired
+    private IssueCloudConfigMapper configMapper;
+
     @Override
     public void log(Long configId, String operationType, String detail, String operator, String requestIp) {
         IssueOperationLog log = new IssueOperationLog();
         log.setConfigId(configId);
+        IssueCloudConfig config = configMapper.selectById(configId);
+        if (config != null) {
+            log.setTenantId(config.getTenantId());
+        }
         log.setOperationType(operationType);
         log.setOperationDetail(detail);
-        log.setCreateBy(operator);
         log.setRequestIp(requestIp);
         CloudEntitySupport.fillOnInsert(log);
+        log.setCreatedBy(operator);
         operationLogMapper.insert(log);
     }
 }

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

@@ -67,7 +67,7 @@ public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
                         .eq(IssueSyncData::getTenantId, incoming.getTenantId())
                         .eq(IssueSyncData::getTableName, incoming.getTableName())
                         .eq(IssueSyncData::getDataKey, incoming.getDataKey())
-                        .eq(IssueSyncData::getDeleted, 0));
+                        .eq(IssueSyncData::getIsDeleted, 0));
                 if (exist == null) {
                     incoming.setSyncStatus(1);
                     incoming.setSyncDirection("LOCAL");
@@ -100,7 +100,7 @@ public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
                 .eq(IssueSyncDownQueue::getTableName, tableName)
                 .eq(IssueSyncDownQueue::getStatus, 0)
                 .gt(IssueSyncDownQueue::getSyncVersion, lastVersion)
-                .eq(IssueSyncDownQueue::getDeleted, 0)
+                .eq(IssueSyncDownQueue::getIsDeleted, 0)
                 .orderByAsc(IssueSyncDownQueue::getSyncVersion)
                 .last("LIMIT " + POLL_BATCH_SIZE));
 
@@ -140,7 +140,7 @@ public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
                 .eq(IssueSyncDownQueue::getTenantId, tenantId)
                 .eq(IssueSyncDownQueue::getTableName, tableName)
                 .eq(IssueSyncDownQueue::getStatus, 0)
-                .eq(IssueSyncDownQueue::getDeleted, 0));
+                .eq(IssueSyncDownQueue::getIsDeleted, 0));
         return count == null ? 0 : count;
     }
 

+ 7 - 4
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncServiceImpl.java

@@ -20,6 +20,7 @@ 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;
@@ -74,7 +75,9 @@ public class CloudSyncServiceImpl implements CloudSyncService {
     }
 
     @Override
-    public SyncTaskResponse triggerManualSync(String syncType, String operator, String requestIp) {
+    public SyncTaskResponse triggerManualSync(String syncType) {
+        String operator = CloudEntitySupport.resolveOperator();
+        String requestIp = CloudEntitySupport.resolveRequestIp();
         SyncTypeEnum typeEnum;
         try {
             typeEnum = SyncTypeEnum.fromCode(syncType);
@@ -107,7 +110,7 @@ public class CloudSyncServiceImpl implements CloudSyncService {
     @Override
     public SyncTaskResponse getTaskProgress(Long taskId) {
         IssueSyncTask task = syncTaskMapper.selectById(taskId);
-        if (task == null || (task.getDeleted() != null && task.getDeleted() == 1)) {
+        if (task == null || (task.getIsDeleted() != null && task.getIsDeleted() == 1)) {
             throw new BusinessException("同步任务不存在");
         }
         return toTaskResponse(task);
@@ -134,7 +137,7 @@ public class CloudSyncServiceImpl implements CloudSyncService {
         IssueTriggerEvent event = triggerEventMapper.selectOne(new LambdaQueryWrapper<IssueTriggerEvent>()
                 .eq(IssueTriggerEvent::getConfigId, config.getId())
                 .eq(IssueTriggerEvent::getEventCode, eventCode.getCode())
-                .eq(IssueTriggerEvent::getDeleted, 0));
+                .eq(IssueTriggerEvent::getIsDeleted, 0));
         if (event == null || event.getEnabled() == null || event.getEnabled() == 0) {
             return;
         }
@@ -145,7 +148,7 @@ public class CloudSyncServiceImpl implements CloudSyncService {
     private List<SyncStatusResponse> loadStatusFromDb(Long configId) {
         return syncStatusMapper.selectList(new LambdaQueryWrapper<IssueSyncStatus>()
                         .eq(IssueSyncStatus::getConfigId, configId)
-                        .eq(IssueSyncStatus::getDeleted, 0)
+                        .eq(IssueSyncStatus::getIsDeleted, 0)
                         .orderByAsc(IssueSyncStatus::getId))
                 .stream()
                 .map(this::toStatusResponse)

+ 7 - 3
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncTaskRunnerImpl.java

@@ -53,6 +53,9 @@ public class CloudSyncTaskRunnerImpl implements CloudSyncTaskRunner {
         int pending = localSyncAgent.countPending(type, config);
         IssueSyncTask task = new IssueSyncTask();
         task.setConfigId(configId);
+        if (config != null) {
+            task.setTenantId(config.getTenantId());
+        }
         task.setSyncType(type.getCode());
         task.setTaskStatus(TaskStatusEnum.PENDING.getCode());
         task.setTriggerMode(triggerMode);
@@ -103,17 +106,18 @@ public class CloudSyncTaskRunnerImpl implements CloudSyncTaskRunner {
         CloudEntitySupport.fillOnUpdate(task);
         syncTaskMapper.updateById(task);
 
-        recordSyncDetails(taskId, syncResult);
+        recordSyncDetails(taskId, task.getTenantId(), syncResult);
         refreshSyncStatus(task.getConfigId(), type, task);
         redisHelper.delete(CloudIntegrationConstants.CACHE_STATUS_PREFIX + task.getConfigId());
     }
 
-    private void recordSyncDetails(Long taskId, SyncResult syncResult) {
+    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
@@ -129,7 +133,7 @@ public class CloudSyncTaskRunnerImpl implements CloudSyncTaskRunner {
         IssueSyncStatus status = syncStatusMapper.selectOne(new LambdaQueryWrapper<IssueSyncStatus>()
                 .eq(IssueSyncStatus::getConfigId, configId)
                 .eq(IssueSyncStatus::getSyncType, type.getCode())
-                .eq(IssueSyncStatus::getDeleted, 0));
+                .eq(IssueSyncStatus::getIsDeleted, 0));
         if (status == null) {
             return;
         }

+ 2 - 1
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudTriggerEventServiceImpl.java

@@ -42,7 +42,7 @@ public class CloudTriggerEventServiceImpl implements CloudTriggerEventService {
         IssueCloudConfig config = cloudConfigService.requireActiveConfig();
         return triggerEventMapper.selectList(new LambdaQueryWrapper<IssueTriggerEvent>()
                         .eq(IssueTriggerEvent::getConfigId, config.getId())
-                        .eq(IssueTriggerEvent::getDeleted, 0))
+                        .eq(IssueTriggerEvent::getIsDeleted, 0))
                 .stream()
                 .map(this::toResponse)
                 .collect(Collectors.toList());
@@ -61,6 +61,7 @@ public class CloudTriggerEventServiceImpl implements CloudTriggerEventService {
             }
             IssueTriggerEvent event = new IssueTriggerEvent();
             event.setConfigId(config.getId());
+            event.setTenantId(config.getTenantId());
             event.setEventCode(codeEnum.getCode());
             event.setEventName(codeEnum.getName());
             event.setDescription(codeEnum.getDescription());

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

@@ -49,7 +49,7 @@ public class LocalPullServiceImpl implements LocalPullService {
         SyncResult result = SyncResult.empty();
         List<IssueSyncCursor> cursors = cursorMapper.selectList(new LambdaQueryWrapper<IssueSyncCursor>()
                 .eq(IssueSyncCursor::getDirection, SyncDirection.DOWN.getCode())
-                .eq(IssueSyncCursor::getDeleted, 0));
+                .eq(IssueSyncCursor::getIsDeleted, 0));
         if (cursors.isEmpty() && config.getTenantId() != null) {
             result.merge(pullByTenant(config.getTenantId(), tableName, config));
             return result;
@@ -124,7 +124,7 @@ public class LocalPullServiceImpl implements LocalPullService {
                 .eq(IssueSyncData::getTenantId, data.getTenantId())
                 .eq(IssueSyncData::getTableName, data.getTableName())
                 .eq(IssueSyncData::getDataKey, data.getDataKey())
-                .eq(IssueSyncData::getDeleted, 0));
+                .eq(IssueSyncData::getIsDeleted, 0));
         if (exist == null) {
             CloudEntitySupport.fillOnInsert(data);
             syncDataMapper.insert(data);
@@ -160,7 +160,7 @@ public class LocalPullServiceImpl implements LocalPullService {
                 .eq(IssueSyncCursor::getTenantId, tenantId)
                 .eq(IssueSyncCursor::getTableName, tableName)
                 .eq(IssueSyncCursor::getDirection, direction)
-                .eq(IssueSyncCursor::getDeleted, 0));
+                .eq(IssueSyncCursor::getIsDeleted, 0));
         if (cursor == null) {
             cursor = new IssueSyncCursor();
             cursor.setTenantId(tenantId);

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

@@ -72,7 +72,7 @@ public class LocalPushServiceImpl implements LocalPushService {
                     .eq(IssueSyncData::getTableName, tableName)
                     .in(IssueSyncData::getSyncStatus, 0, 2)
                     .gt(IssueSyncData::getSyncVersion, lastVersion)
-                    .eq(IssueSyncData::getDeleted, 0)
+                    .eq(IssueSyncData::getIsDeleted, 0)
                     .orderByAsc(IssueSyncData::getSyncVersion)
                     .last("LIMIT " + batchSize));
 
@@ -133,7 +133,7 @@ public class LocalPushServiceImpl implements LocalPushService {
                 .eq(IssueSyncCursor::getTenantId, tenantId)
                 .eq(IssueSyncCursor::getTableName, tableName)
                 .eq(IssueSyncCursor::getDirection, direction)
-                .eq(IssueSyncCursor::getDeleted, 0));
+                .eq(IssueSyncCursor::getIsDeleted, 0));
         if (cursor == null) {
             cursor = new IssueSyncCursor();
             cursor.setTenantId(tenantId);

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

@@ -75,7 +75,7 @@ public class LocalSyncAgentImpl implements LocalSyncAgent {
                 .eq(IssueSyncData::getTenantId, config.getTenantId())
                 .eq(IssueSyncData::getTableName, tableName)
                 .in(IssueSyncData::getSyncStatus, 0, 2)
-                .eq(IssueSyncData::getDeleted, 0));
+                .eq(IssueSyncData::getIsDeleted, 0));
         return cloudSyncReceiveService.countPendingDownQueue(config.getTenantId(), tableName);
     }
 }

+ 24 - 18
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/support/CloudEntitySupport.java

@@ -1,9 +1,10 @@
 package com.usky.issue.service.support;
 
-import com.alibaba.nacos.shaded.io.grpc.netty.shaded.io.netty.util.internal.StringUtil;
 import com.baomidou.mybatisplus.core.toolkit.StringUtils;
 import com.usky.common.security.utils.SecurityUtils;
 import com.usky.issue.domain.CloudAuditEntity;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
 
 import java.time.LocalDateTime;
 
@@ -18,29 +19,34 @@ public final class CloudEntitySupport {
     private CloudEntitySupport() {
     }
 
-    public static void fillOnInsert(CloudAuditEntity entity) {
-        LocalDateTime now = LocalDateTime.now();
+    public static String resolveOperator() {
         String username = SecurityUtils.getUsername();
-        if (StringUtils.isBlank(username)) {
-            entity.setCreateBy(username);
-        } else {
-            entity.setCreateBy("system");
+        return StringUtils.isBlank(username) ? "system" : username;
+    }
+
+    public static String resolveRequestIp() {
+        ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attrs == null) {
+            return null;
         }
+        return attrs.getRequest().getRemoteAddr();
+    }
 
-        entity.setCreateTime(now);
-        entity.setUpdateTime(now);
-        if (entity.getDeleted() == null) {
-            entity.setDeleted(0);
+    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.setUpdateTime(LocalDateTime.now());
-        String username = SecurityUtils.getUsername();
-        if (StringUtils.isBlank(username)) {
-            entity.setUpdateBy(username);
-        } else {
-            entity.setUpdateBy("system");
-        }
+        entity.setUpdatedTime(LocalDateTime.now());
+        entity.setUpdatedBy(resolveOperator());
     }
 }

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

@@ -15,14 +15,14 @@ public class CloudConfigResponse {
 
     private Long id;
     private String tenantId;
-    /** 脱敏后的凭证展示 */
-    private String credentialKeyMasked;
+    /** 解密后的凭证密钥 */
+    private String credentialKey;
     private Integer status;
     private Integer connectionStatus;
     private Integer version;
     private LocalDateTime lastTestTime;
     private LocalDateTime updatedTime;
     private String updatedBy;
-    private String createBy;
-    private LocalDateTime createTime;
+    private String createdBy;
+    private LocalDateTime createdTime;
 }

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

@@ -15,4 +15,10 @@ public class CloudConnectionTestRequest {
 
     @NotNull(message = "租户ID不能为空")
     private Integer tenantId;
+
+    @NotNull(message = "云平台地址不能为空")
+    private String cloudAddress;
+
+    @NotNull(message = "凭证密钥不能为空")
+    private String credentialKey;
 }

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

@@ -11,6 +11,13 @@ import lombok.Data;
 @Data
 public class CloudDisableResponse {
 
+    /**
+     * 是否禁用
+     */
     private boolean disabled;
+
+    /**
+     * 禁用后,被禁用的任务数
+     */
     private int affectedTasks;
 }

+ 0 - 51
service-issue/service-issue-biz/src/main/resources/sql/issue_cloud_sync_tables.sql

@@ -1,51 +0,0 @@
--- 本地同步游标表
-CREATE TABLE IF NOT EXISTS issue_sync_cursor (
-    id BIGINT PRIMARY KEY AUTO_INCREMENT,
-    tenant_id INT NOT NULL,
-    table_name VARCHAR(50) NOT NULL COMMENT '同步类型/表名,如 TENANT、USER',
-    direction VARCHAR(10) NOT NULL COMMENT 'UP/DOWN',
-    last_sync_version BIGINT DEFAULT 0 COMMENT '最后同步的版本号',
-    last_sync_time DATETIME,
-    create_by VARCHAR(64),
-    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(64),
-    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    deleted TINYINT DEFAULT 0,
-    UNIQUE KEY uk_tenant_table_dir (tenant_id, table_name, direction)
-) COMMENT '同步游标表';
-
--- 云端下发队列(云端→本地)
-CREATE TABLE IF NOT EXISTS issue_sync_down_queue (
-    id BIGINT PRIMARY KEY AUTO_INCREMENT,
-    tenant_id INT NOT NULL,
-    table_name VARCHAR(50) NOT NULL,
-    operation VARCHAR(20) COMMENT 'INSERT/UPDATE/DELETE',
-    payload JSON NOT NULL COMMENT '完整数据JSON',
-    sync_version BIGINT NOT NULL,
-    status TINYINT DEFAULT 0 COMMENT '0:待拉取 1:已确认 2:失败',
-    create_by VARCHAR(64),
-    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(64),
-    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    deleted TINYINT DEFAULT 0,
-    INDEX idx_tenant_status (tenant_id, status)
-) COMMENT '云端下发队列';
-
--- 本地同步业务数据表
-CREATE TABLE IF NOT EXISTS issue_sync_data (
-    id BIGINT PRIMARY KEY AUTO_INCREMENT,
-    tenant_id INT NOT NULL,
-    table_name VARCHAR(50) NOT NULL COMMENT '同步类型,如 TENANT、USER',
-    data_key VARCHAR(100) NOT NULL COMMENT '业务唯一键',
-    payload JSON NOT NULL COMMENT '业务数据JSON',
-    sync_version BIGINT NOT NULL COMMENT '数据版本时间戳(毫秒)',
-    sync_status TINYINT DEFAULT 0 COMMENT '0:未同步 1:已推送 2:推送失败',
-    sync_direction VARCHAR(10) DEFAULT 'LOCAL' COMMENT 'LOCAL/CLOUD 数据来源',
-    create_by VARCHAR(64),
-    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
-    update_by VARCHAR(64),
-    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    deleted TINYINT DEFAULT 0,
-    UNIQUE KEY uk_tenant_table_key (tenant_id, table_name, data_key),
-    INDEX idx_tenant_table_status (tenant_id, table_name, sync_status)
-) COMMENT '同步业务数据表';

+ 0 - 796
service-issue/service-issue-biz/src/main/resources/本地云端双向数据同步与连通性测试方案.md

@@ -1,796 +0,0 @@
-# 本地-云端双向数据同步与连通性测试方案
-
-## 一、方案概述
-
-### 1.1 背景与约束
-- **本地服务器A**:不可信区,不受管控,不能暴露云端数据库密码
-- **云服务器**:可信区,持有数据库连接凭证
-- **同步需求**:双向同步(本地↔云端),支持手动触发与自动同步
-- **连通性测试**:验证本地Agent与云端API通道的可用性
-
-### 1.2 核心设计原则
-| 原则 | 说明 |
-|------|------|
-| **单向出站** | 所有连接由本地主动发起(HTTPS出站),云端零暴露 |
-| **密码隔离** | 云端DB密码仅存在于云端服务,本地仅持有API Token |
-| **游标断点续传** | 基于`sync_version`游标,支持断网恢复后增量同步 |
-| **冲突解决** | 默认`last-write-wins`,云端优先;可配置为本地优先 |
-| **分层检测** | 连通性测试覆盖网络层、认证层、租户层 |
-
----
-
-## 二、架构设计
-
-```
-┌─────────────────────────────────────────────────────────────────┐
-│                         云服务器(可信区)                          │
-│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐   │
-│  │  SyncReceive   │  │ SyncCommand    │  │   Cloud Data Service   │   │
-│  │  Controller    │  │  Controller    │  │   (直连云端DB)          │   │
-│  │  (接收推送)     │  │  (下发指令/数据) │  │                        │   │
-│  └──────┬─────────┘  └──────┬───────┘  └──────────────────────┘   │
-│         ▲                   │                                      │
-│         │                   │                                      │
-│    HTTPS│出站上推            │HTTPS出站长轮询拉取                    │
-│         │                   │                                      │
-│  ┌──────┴───────────────────┴──────────────────────────────────┐   │
-│  │                    本地服务器A(不可信区)                      │   │
-│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐       │   │
-│  │  │  LocalPush     │  │  LocalPull     │  │  Local Data    │       │   │
-│  │  │  Service       │  │  Service       │  │  Service       │       │   │
-│  │  │  (读取本地变更)  │  │  (拉云端指令)   │  │  (操作本地DB)   │       │   │
-│  │  └──────┬─────────┘  └──────┬───────┘  └──────┬───────┘       │   │
-│  │         │                   │                  │               │   │
-│  │  ┌──────┴───────────────────┴──────────────────┘               │   │
-│  │  │              Local Sync Agent (调度核心)                       │   │
-│  │  │   • Push线程:定时/实时扫描本地变更 → 推云端                    │   │
-│  │  │   • Pull线程:长轮询云端指令队列 → 写入本地                     │   │
-│  │  │   • 手动触发:REST接口接收任务 → 提交线程池                     │   │
-│  │  └────────────────────────────────────────────────────────────┘   │
-│  └─────────────────────────────────────────────────────────────────┘
-```
-
----
-
-## 三、数据库表设计
-
-### 3.1 本地业务表(示例:device_data)
-```sql
-CREATE TABLE local_device_data (
-    id BIGINT PRIMARY KEY AUTO_INCREMENT,
-    tenant_id VARCHAR(50) NOT NULL,
-    device_code VARCHAR(50),
-    sensor_value DECIMAL(10,2),
-    -- 同步控制字段
-    sync_version BIGINT COMMENT '数据版本时间戳(毫秒)',
-    sync_status TINYINT DEFAULT 0 COMMENT '0:未同步 1:已推送 2:推送失败',
-    sync_direction VARCHAR(10) DEFAULT 'LOCAL' COMMENT 'LOCAL/CLOUD 数据来源',
-    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    create_time DATETIME DEFAULT CURRENT_TIMESTAMP
-);
-```
-
-### 3.2 本地同步游标表
-```sql
-CREATE TABLE local_sync_cursor (
-    id BIGINT PRIMARY KEY AUTO_INCREMENT,
-    tenant_id VARCHAR(50) NOT NULL,
-    table_name VARCHAR(50) NOT NULL,
-    direction VARCHAR(10) NOT NULL COMMENT 'UP/DOWN',
-    last_sync_version BIGINT DEFAULT 0 COMMENT '最后同步的版本号',
-    last_sync_time DATETIME,
-    UNIQUE KEY uk_tenant_table_dir (tenant_id, table_name, direction)
-);
-```
-
-### 3.3 云端下发队列(云端→本地)
-```sql
-CREATE TABLE cloud_sync_down_queue (
-    id BIGINT PRIMARY KEY AUTO_INCREMENT,
-    tenant_id VARCHAR(50) NOT NULL,
-    table_name VARCHAR(50) NOT NULL,
-    operation VARCHAR(20) COMMENT 'INSERT/UPDATE/DELETE',
-    payload JSON NOT NULL COMMENT '完整数据JSON',
-    sync_version BIGINT NOT NULL,
-    status TINYINT DEFAULT 0 COMMENT '0:待拉取 1:已确认 2:失败',
-    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
-    INDEX idx_tenant_status (tenant_id, status)
-);
-```
-
----
-
-## 四、公共实体与枚举
-
-### 4.1 同步方向枚举
-```java
-@Getter
-@AllArgsConstructor
-public enum SyncDirection {
-    UP("本地→云端"),
-    DOWN("云端→本地"),
-    BOTH("双向");
-    private final String desc;
-}
-```
-
-### 4.2 数据包
-```java
-@Data
-@Builder
-public class SyncPacket<T> {
-    private String tenantId;
-    private String tableName;
-    private SyncDirection direction;
-    private List<T> dataList;
-    private Long batchVersion;
-    private Boolean hasMore;
-    private String nonce;
-}
-```
-
-### 4.3 同步响应
-```java
-@Data
-@Builder
-public class SyncResponse {
-    private boolean success;
-    private String message;
-    private Long confirmedVersion;
-    private Integer acceptedCount;
-}
-```
-
-### 4.4 手动触发请求
-```java
-@Data
-public class SyncTriggerRequest {
-    private String tenantId;
-    private String tableName;
-    private SyncDirection direction;
-    private Boolean fullSync; // 是否全量同步(重置游标)
-}
-```
-
----
-
-## 五、云端服务实现
-
-### 5.1 云端接收控制器(本地→云端)
-```java
-@Slf4j
-@RestController
-@RequestMapping("/api/cloud/sync")
-@RequiredArgsConstructor
-public class CloudSyncReceiveController {
-
-    private final CloudSyncService cloudSyncService;
-    private final CloudAuthService cloudAuthService;
-
-    @PostMapping("/receive")
-    public SyncResponse receive(
-            @RequestHeader("X-App-Token") String token,
-            @RequestHeader("X-Sign") String sign,
-            @RequestBody @Valid SyncPacket packet) {
-
-        if (!cloudAuthService.validate(token, sign, packet)) {
-            return SyncResponse.builder().success(false).message("认证失败或请求已过期").build();
-        }
-
-        if (!cloudSyncService.validateTenant(packet.getTenantId())) {
-            return SyncResponse.builder().success(false).message("租户不存在或已停用").build();
-        }
-
-        try {
-            int count = cloudSyncService.batchUpsert(packet);
-            return SyncResponse.builder()
-                    .success(true)
-                    .message("接收成功")
-                    .acceptedCount(count)
-                    .confirmedVersion(packet.getBatchVersion())
-                    .build();
-        } catch (Exception e) {
-            log.error("云端接收数据失败, tenantId={}", packet.getTenantId(), e);
-            return SyncResponse.builder().success(false).message("写入失败:" + e.getMessage()).build();
-        }
-    }
-}
-```
-
-### 5.2 云端下发控制器(云端→本地)
-```java
-@Slf4j
-@RestController
-@RequestMapping("/api/cloud/sync")
-@RequiredArgsConstructor
-public class CloudSyncCommandController {
-
-    private final CloudSyncService cloudSyncService;
-    private final CloudAuthService cloudAuthService;
-
-    @GetMapping("/poll")
-    public SyncPacket poll(
-            @RequestHeader("X-App-Token") String token,
-            @RequestParam String tenantId,
-            @RequestParam String tableName,
-            @RequestParam(required = false) Long lastVersion) {
-
-        if (!cloudAuthService.validateToken(token)) {
-            return null;
-        }
-        return cloudSyncService.pollDownQueue(tenantId, tableName, lastVersion);
-    }
-
-    @PostMapping("/poll/ack")
-    public SyncResponse ackPoll(
-            @RequestHeader("X-App-Token") String token,
-            @RequestBody List<Long> queueIds) {
-
-        if (!cloudAuthService.validateToken(token)) {
-            return SyncResponse.builder().success(false).message("认证失败").build();
-        }
-        cloudSyncService.ackDownQueue(queueIds);
-        return SyncResponse.builder().success(true).build();
-    }
-}
-```
-
-### 5.3 云端同步服务(冲突解决:云端优先)
-```java
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class CloudSyncService {
-
-    private final CloudDeviceDataMapper deviceDataMapper;
-    private final CloudSyncDownQueueMapper downQueueMapper;
-    private final TenantConfigMapper tenantConfigMapper;
-
-    public boolean validateTenant(String tenantId) {
-        TenantConfig config = tenantConfigMapper.selectByTenantId(tenantId);
-        return config != null && config.getStatus() == 1;
-    }
-
-    @Transactional(rollbackFor = Exception.class)
-    public int batchUpsert(SyncPacket packet) {
-        List<CloudDeviceData> list = convertToCloudEntity(packet);
-        int count = 0;
-        for (CloudDeviceData data : list) {
-            CloudDeviceData exist = deviceDataMapper.selectByUk(data.getTenantId(), data.getDeviceCode());
-            if (exist == null) {
-                deviceDataMapper.insert(data);
-                count++;
-            } else if (data.getSyncVersion() >= exist.getSyncVersion()) {
-                data.setId(exist.getId());
-                deviceDataMapper.updateById(data);
-                count++;
-            }
-        }
-        return count;
-    }
-
-    public SyncPacket pollDownQueue(String tenantId, String tableName, Long lastVersion) {
-        if (lastVersion == null) lastVersion = 0L;
-        List<CloudSyncDownQueue> queueList = downQueueMapper.selectPending(
-                tenantId, tableName, lastVersion, 100);
-
-        if (queueList.isEmpty()) return null;
-
-        Long maxVersion = queueList.stream()
-                .mapToLong(CloudSyncDownQueue::getSyncVersion)
-                .max().orElse(lastVersion);
-
-        return SyncPacket.builder()
-                .tenantId(tenantId)
-                .tableName(tableName)
-                .direction(SyncDirection.DOWN)
-                .dataList(queueList)
-                .batchVersion(maxVersion)
-                .hasMore(queueList.size() >= 100)
-                .build();
-    }
-
-    public void ackDownQueue(List<Long> queueIds) {
-        if (queueIds == null || queueIds.isEmpty()) return;
-        downQueueMapper.updateStatus(queueIds, 1);
-    }
-}
-```
-
----
-
-## 六、本地服务实现
-
-### 6.1 手动触发接口
-```java
-@RestController
-@RequestMapping("/api/local/sync")
-@RequiredArgsConstructor
-public class LocalSyncTriggerController {
-
-    private final LocalSyncAgent syncAgent;
-
-    @PostMapping("/trigger")
-    public String trigger(@RequestBody @Valid SyncTriggerRequest request) {
-        syncAgent.submitTask(request);
-        return "同步任务已提交: " + request.getDirection() + 
-               ", tenant=" + request.getTenantId() + 
-               ", table=" + request.getTableName();
-    }
-
-    @GetMapping("/status")
-    public String status() {
-        return syncAgent.getStatus();
-    }
-}
-```
-
-### 6.2 本地Agent核心调度器
-```java
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class LocalSyncAgent {
-
-    private final LocalPushService pushService;
-    private final LocalPullService pullService;
-    private final ExecutorService executor = new ThreadPoolExecutor(
-            4, 8, 60, TimeUnit.SECONDS,
-            new LinkedBlockingQueue<>(100),
-            new ThreadFactoryBuilder().setNameFormat("sync-agent-%d").build(),
-            new ThreadPoolExecutor.CallerRunsPolicy()
-    );
-
-    @PostConstruct
-    public void init() {
-        log.info("本地同步Agent启动");
-        executor.submit(this::pullLoop);
-    }
-
-    @PreDestroy
-    public void destroy() {
-        executor.shutdown();
-    }
-
-    @Scheduled(fixedDelay = 5000)
-    public void autoPush() {
-        try {
-            pushService.pushAllPending("device_data");
-        } catch (Exception e) {
-            log.error("自动推送失败", e);
-        }
-    }
-
-    private void pullLoop() {
-        while (!Thread.currentThread().isInterrupted()) {
-            try {
-                pullService.pullAndApply("device_data");
-                Thread.sleep(3000);
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                break;
-            } catch (Exception e) {
-                log.error("拉取云端数据失败", e);
-                try {
-                    Thread.sleep(10000);
-                } catch (InterruptedException ie) {
-                    Thread.currentThread().interrupt();
-                    break;
-                }
-            }
-        }
-    }
-
-    public void submitTask(SyncTriggerRequest request) {
-        executor.submit(() -> {
-            try {
-                if (request.getDirection() == SyncDirection.UP || request.getDirection() == SyncDirection.BOTH) {
-                    if (Boolean.TRUE.equals(request.getFullSync())) {
-                        pushService.resetCursor(request.getTenantId(), "device_data");
-                    }
-                    pushService.pushByTenant(request.getTenantId(), "device_data");
-                }
-                if (request.getDirection() == SyncDirection.DOWN || request.getDirection() == SyncDirection.BOTH) {
-                    pullService.pullByTenant(request.getTenantId(), "device_data");
-                }
-            } catch (Exception e) {
-                log.error("手动同步任务失败", e);
-            }
-        });
-    }
-
-    public String getStatus() {
-        return "运行中, 活跃线程:" + ((ThreadPoolExecutor) executor).getActiveCount();
-    }
-}
-```
-
-### 6.3 本地推送服务(本地→云端)
-```java
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class LocalPushService {
-
-    private final LocalDeviceDataMapper deviceDataMapper;
-    private final LocalSyncCursorMapper cursorMapper;
-    private final RestTemplate restTemplate;
-    private final ObjectMapper objectMapper;
-
-    @Value("${cloud.api.endpoint}")
-    private String cloudEndpoint;
-    @Value("${cloud.api.token}")
-    private String apiToken;
-
-    public void pushAllPending(String tableName) {
-        List<String> tenantIds = deviceDataMapper.selectPendingTenantIds(tableName);
-        for (String tenantId : tenantIds) {
-            try {
-                pushByTenant(tenantId, tableName);
-            } catch (Exception e) {
-                log.error("推送租户{}失败", tenantId, e);
-            }
-        }
-    }
-
-    public void pushByTenant(String tenantId, String tableName) {
-        LocalSyncCursor cursor = getOrInitCursor(tenantId, tableName, "UP");
-        Long lastVersion = cursor.getLastSyncVersion();
-
-        while (true) {
-            List<LocalDeviceData> dataList = deviceDataMapper.selectByVersionGt(
-                    tenantId, lastVersion, 100);
-            if (dataList.isEmpty()) break;
-
-            Long batchVersion = dataList.stream()
-                    .mapToLong(LocalDeviceData::getSyncVersion)
-                    .max().orElse(lastVersion);
-
-            SyncPacket packet = SyncPacket.builder()
-                    .tenantId(tenantId)
-                    .tableName(tableName)
-                    .direction(SyncDirection.UP)
-                    .dataList(dataList)
-                    .batchVersion(batchVersion)
-                    .hasMore(dataList.size() >= 100)
-                    .nonce(UUID.randomUUID().toString())
-                    .build();
-
-            SyncResponse response = pushToCloud(packet);
-            if (response != null && response.isSuccess()) {
-                lastVersion = response.getConfirmedVersion();
-                updateCursor(tenantId, tableName, "UP", lastVersion);
-                List<Long> ids = dataList.stream().map(LocalDeviceData::getId).toList();
-                deviceDataMapper.markSynced(ids);
-                if (Boolean.FALSE.equals(packet.getHasMore())) break;
-            } else {
-                log.error("云端接收失败: {}", response != null ? response.getMessage() : "无响应");
-                break;
-            }
-        }
-    }
-
-    public void resetCursor(String tenantId, String tableName) {
-        cursorMapper.updateLastVersion(tenantId, tableName, "UP", 0L);
-    }
-
-    private SyncResponse pushToCloud(SyncPacket packet) {
-        try {
-            HttpHeaders headers = new HttpHeaders();
-            headers.setContentType(MediaType.APPLICATION_JSON);
-            headers.set("X-App-Token", apiToken);
-            headers.set("X-Sign", HmacUtils.sign(objectMapper.writeValueAsString(packet), apiToken));
-
-            ResponseEntity<SyncResponse> response = restTemplate.exchange(
-                    cloudEndpoint + "/api/cloud/sync/receive",
-                    HttpMethod.POST,
-                    new HttpEntity<>(packet, headers),
-                    SyncResponse.class
-            );
-            return response.getBody();
-        } catch (Exception e) {
-            log.error("推送云端网络异常", e);
-            return null;
-        }
-    }
-
-    private LocalSyncCursor getOrInitCursor(String tenantId, String tableName, String direction) {
-        LocalSyncCursor cursor = cursorMapper.selectByUk(tenantId, tableName, direction);
-        if (cursor == null) {
-            cursor = new LocalSyncCursor();
-            cursor.setTenantId(tenantId);
-            cursor.setTableName(tableName);
-            cursor.setDirection(direction);
-            cursor.setLastSyncVersion(0L);
-            cursorMapper.insert(cursor);
-        }
-        return cursor;
-    }
-
-    private void updateCursor(String tenantId, String tableName, String direction, Long version) {
-        cursorMapper.updateLastVersion(tenantId, tableName, direction, version);
-    }
-}
-```
-
-### 6.4 本地拉取服务(云端→本地)
-```java
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class LocalPullService {
-
-    private final LocalDeviceDataMapper deviceDataMapper;
-    private final LocalSyncCursorMapper cursorMapper;
-    private final RestTemplate restTemplate;
-    private final ObjectMapper objectMapper;
-
-    @Value("${cloud.api.endpoint}")
-    private String cloudEndpoint;
-    @Value("${cloud.api.token}")
-    private String apiToken;
-
-    public void pullAndApply(String tableName) {
-        List<String> tenantIds = cursorMapper.selectAllTenantIds();
-        for (String tenantId : tenantIds) {
-            try {
-                pullByTenant(tenantId, tableName);
-            } catch (Exception e) {
-                log.error("拉取租户{}数据失败", tenantId, e);
-            }
-        }
-    }
-
-    public void pullByTenant(String tenantId, String tableName) {
-        LocalSyncCursor cursor = getOrInitCursor(tenantId, tableName, "DOWN");
-        Long lastVersion = cursor.getLastSyncVersion();
-
-        while (true) {
-            SyncPacket packet = pollCloud(tenantId, tableName, lastVersion);
-            if (packet == null || packet.getDataList() == null || packet.getDataList().isEmpty()) break;
-
-            List<CloudSyncDownQueue> queueList = (List<CloudSyncDownQueue>) packet.getDataList();
-            List<Long> ackIds = queueList.stream().map(CloudSyncDownQueue::getId).collect(Collectors.toList());
-
-            for (CloudSyncDownQueue queue : queueList) {
-                applyToLocal(queue);
-            }
-
-            lastVersion = packet.getBatchVersion();
-            updateCursor(tenantId, tableName, "DOWN", lastVersion);
-            ackCloud(ackIds);
-
-            if (Boolean.FALSE.equals(packet.getHasMore())) break;
-        }
-    }
-
-    private void applyToLocal(CloudSyncDownQueue queue) {
-        try {
-            LocalDeviceData data = objectMapper.readValue(queue.getPayload().toString(), LocalDeviceData.class);
-            data.setSyncVersion(queue.getSyncVersion());
-            data.setSyncDirection("CLOUD");
-            data.setSyncStatus(1);
-
-            LocalDeviceData exist = deviceDataMapper.selectByUk(data.getTenantId(), data.getDeviceCode());
-            if (exist == null) {
-                deviceDataMapper.insert(data);
-            } else if (queue.getSyncVersion() >= exist.getSyncVersion()) {
-                data.setId(exist.getId());
-                deviceDataMapper.updateById(data);
-            }
-        } catch (Exception e) {
-            log.error("应用云端数据到本地失败, queueId={}", queue.getId(), e);
-        }
-    }
-
-    private SyncPacket pollCloud(String tenantId, String tableName, Long lastVersion) {
-        try {
-            String url = UriComponentsBuilder.fromHttpUrl(cloudEndpoint + "/api/cloud/sync/poll")
-                    .queryParam("tenantId", tenantId)
-                    .queryParam("tableName", tableName)
-                    .queryParam("lastVersion", lastVersion)
-                    .toUriString();
-            return restTemplate.getForObject(url, SyncPacket.class);
-        } catch (Exception e) {
-            log.error("轮询云端失败", e);
-            return null;
-        }
-    }
-
-    private void ackCloud(List<Long> queueIds) {
-        try {
-            restTemplate.postForObject(
-                    cloudEndpoint + "/api/cloud/sync/poll/ack",
-                    queueIds,
-                    SyncResponse.class
-            );
-        } catch (Exception e) {
-            log.error("ACK云端失败", e);
-        }
-    }
-
-    private LocalSyncCursor getOrInitCursor(String tenantId, String tableName, String direction) {
-        LocalSyncCursor cursor = cursorMapper.selectByUk(tenantId, tableName, direction);
-        if (cursor == null) {
-            cursor = new LocalSyncCursor();
-            cursor.setTenantId(tenantId);
-            cursor.setTableName(tableName);
-            cursor.setDirection(direction);
-            cursor.setLastSyncVersion(0L);
-            cursorMapper.insert(cursor);
-        }
-        return cursor;
-    }
-
-    private void updateCursor(String tenantId, String tableName, String direction, Long version) {
-        cursorMapper.updateLastVersion(tenantId, tableName, direction, version);
-    }
-}
-```
-
----
-
-## 七、连通性测试接口
-
-### 7.1 测试目标转变
-| 原目标 | 新目标 |
-|--------|--------|
-| 本地能否连云端DB | 本地Agent能否正常推送/拉取 |
-| 云端租户配置是否存在 | 云端API返回的认证结果 |
-
-### 7.2 本地连通性测试接口
-```java
-@RestController
-@RequestMapping("/api/local/health")
-@RequiredArgsConstructor
-public class LocalHealthCheckController {
-
-    private final RestTemplate restTemplate;
-    @Value("${cloud.api.endpoint}")
-    private String cloudEndpoint;
-    @Value("${cloud.api.token}")
-    private String apiToken;
-
-    @GetMapping("/check")
-    public HealthCheckResponse check(@RequestParam String tenantId) {
-        long start = System.currentTimeMillis();
-
-        try {
-            String pingUrl = cloudEndpoint + "/api/cloud/sync/ping";
-            restTemplate.getForObject(pingUrl, String.class);
-
-            HttpHeaders headers = new HttpHeaders();
-            headers.set("X-App-Token", apiToken);
-            HttpEntity entity = new HttpEntity<>(headers);
-            ResponseEntity<String> authResp = restTemplate.exchange(
-                    cloudEndpoint + "/api/cloud/sync/auth-check?tenantId=" + tenantId,
-                    HttpMethod.GET, entity, String.class);
-
-            boolean tenantExists = "VALID".equals(authResp.getBody());
-
-            return HealthCheckResponse.builder()
-                    .success(true)
-                    .networkReachable(true)
-                    .authValid(true)
-                    .tenantExists(tenantExists)
-                    .costTime(System.currentTimeMillis() - start)
-                    .build();
-
-        } catch (ResourceAccessException e) {
-            return HealthCheckResponse.builder()
-                    .success(false)
-                    .message("网络不可达:" + e.getMessage())
-                    .build();
-        } catch (HttpClientErrorException.Unauthorized e) {
-            return HealthCheckResponse.builder()
-                    .success(false)
-                    .message("认证失败:Token无效")
-                    .build();
-        }
-    }
-}
-```
-
-### 7.3 响应示例
-```json
-{
-  "success": true,
-  "networkReachable": true,
-  "authValid": true,
-  "tenantExists": true,
-  "costTime": 245
-}
-```
-
----
-
-## 八、配置说明
-
-### 8.1 本地配置(application.yml)
-```yaml
-server:
-  port: 9887
-
-cloud:
-  api:
-    endpoint: https://cloud.example.com
-    token: ${CLOUD_API_TOKEN}  # 环境变量注入,非DB密码
-
-sync:
-  push:
-    batch-size: 100
-    fixed-delay-ms: 5000       # 准实时:每5秒扫描
-  pull:
-    interval-ms: 3000          # 长轮询间隔
-    fail-backoff-ms: 10000     # 失败退避
-```
-
-### 8.2 云端配置
-```yaml
-server:
-  port: 8080
-
-spring:
-  datasource:
-    url: jdbc:mysql://localhost:3306/cloud_db
-    username: cloud_root
-    password: ${DB_PASSWORD}   # 仅云端持有
-```
-
----
-
-## 九、API调用示例
-
-### 9.1 手动触发本地→云端
-```bash
-curl -X POST http://localhost:9887/api/local/sync/trigger   -H "Content-Type: application/json"   -d '{
-    "tenantId": "TENANT_001",
-    "tableName": "device_data",
-    "direction": "UP"
-  }'
-```
-
-### 9.2 手动触发双向全量同步
-```bash
-curl -X POST http://localhost:9887/api/local/sync/trigger   -H "Content-Type: application/json"   -d '{
-    "tenantId": "TENANT_001",
-    "tableName": "device_data",
-    "direction": "BOTH",
-    "fullSync": true
-  }'
-```
-
-### 9.3 查询Agent状态
-```bash
-curl http://localhost:9887/api/local/sync/status
-```
-
-### 9.4 连通性测试
-```bash
-curl "http://localhost:9887/api/local/health/check?tenantId=TENANT_001"
-```
-
----
-
-## 十、方案特性总结
-
-| 能力 | 实现方式 | 说明 |
-|------|---------|------|
-| **本地→云端** | PushService 定时扫描 + 批量推送 | 准实时(5秒级) |
-| **云端→本地** | PullService 长轮询 + 写入本地 | 准实时(3秒级) |
-| **手动触发** | REST接口提交任务到线程池 | 异步执行,不阻塞HTTP |
-| **自动实时** | 可接入Canal/Debezium替换轮询 | 当前用定时轮询兜底 |
-| **冲突解决** | sync_version 时间戳,云端优先 | 可配置为本地优先 |
-| **密码安全** | 本地只有API Token,无DB密码 | 云端Token可吊销、可限流 |
-| **断点续传** | sync_cursor 游标表 | 重启后从断点继续 |
-| **单向出站** | 所有连接由本地发起 | 云端零端口暴露 |
-
----
-
-## 十一、扩展建议
-
-1. **CDC实时接入**:使用Canal或Debezium监听本地Binlog,替换轮询扫描,实现毫秒级同步
-2. **消息队列缓冲**:云端引入Kafka/RabbitMQ,削峰填谷,避免突发流量压垮云端DB
-3. **数据压缩**:大数据量时启用Gzip压缩,减少带宽占用
-4. **监控告警**:对同步延迟、失败率、队列堆积进行监控,超过阈值触发告警
-

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

@@ -1,70 +0,0 @@
-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());
-    }
-}

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

@@ -1,76 +0,0 @@
-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(1001);
-        request.setCredentialKey("secret");
-
-        CloudConfigResponse response = cloudConfigService.saveConfig(request, "admin", "127.0.0.1");
-        Assertions.assertNotNull(response);
-        Assertions.assertEquals("tenant-001", response.getTenantId());
-    }
-}

+ 0 - 98
service-issue/service-issue-biz/src/test/java/com/usky/issue/cloud/service/CloudConnectionTestServiceTest.java

@@ -1,98 +0,0 @@
-package com.usky.issue.cloud.service;
-
-import com.usky.issue.domain.IssueCloudConfig;
-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.CloudOperationLogService;
-import com.usky.issue.service.client.CloudPlatformClient;
-import com.usky.issue.service.constant.CloudIntegrationConstants;
-import com.usky.issue.service.impl.CloudConfigServiceImpl;
-import com.usky.issue.service.util.AesGcmCipher;
-import com.usky.issue.service.vo.CloudConnectionTestRequest;
-import com.usky.issue.service.vo.CloudConnectionTestResponse;
-import com.usky.issue.service.vo.CloudConnectionTestResult;
-import org.junit.jupiter.api.Assertions;
-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.verify;
-import static org.mockito.Mockito.when;
-
-/**
- * 连接测试服务单元测试
- *
- * @author fyc
- * @date 2026-06-08
- */
-@ExtendWith(MockitoExtension.class)
-class CloudConnectionTestServiceTest {
-
-    @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;
-
-    @Test
-    void validateTenantConnectionReturnsMissingWhenConfigNotFound() {
-        when(configMapper.selectOne(any())).thenReturn(null);
-
-        CloudConnectionTestResponse response = cloudConfigService.validateTenantConnection(1001);
-
-        Assertions.assertFalse(response.isSuccess());
-        Assertions.assertEquals(CloudIntegrationConstants.MSG_TENANT_CONFIG_MISSING, response.getMessage());
-        Assertions.assertTrue(response.getNetworkReachable());
-    }
-
-    @Test
-    void validateTenantConnectionReturnsSuccessWhenConfigExists() {
-        IssueCloudConfig config = new IssueCloudConfig();
-        config.setTenantId(1001);
-        config.setStatus(1);
-        when(configMapper.selectOne(any())).thenReturn(config);
-
-        CloudConnectionTestResponse response = cloudConfigService.validateTenantConnection(1001);
-
-        Assertions.assertTrue(response.isSuccess());
-        Assertions.assertEquals(CloudIntegrationConstants.MSG_CONNECTION_SUCCESS, response.getMessage());
-    }
-
-    @Test
-    void testConnectionPropagatesTenantMissingFromCloud() {
-        IssueCloudConfig config = new IssueCloudConfig();
-        config.setId(1L);
-        config.setTenantId(1001);
-        config.setStatus(1);
-        when(configMapper.selectOne(any())).thenReturn(config);
-        when(cloudPlatformClient.testConnection(config)).thenReturn(
-                CloudConnectionTestResult.failure(true, CloudIntegrationConstants.MSG_TENANT_CONFIG_MISSING));
-
-        CloudConnectionTestRequest request = new CloudConnectionTestRequest();
-        request.setTenantId(1001);
-
-        CloudConnectionTestResponse response = cloudConfigService.testConnection(request, "admin", "127.0.0.1");
-
-        Assertions.assertFalse(response.isSuccess());
-        Assertions.assertEquals(CloudIntegrationConstants.MSG_TENANT_CONFIG_MISSING, response.getMessage());
-        Assertions.assertTrue(response.getNetworkReachable());
-        verify(configMapper).updateById(any(IssueCloudConfig.class));
-    }
-}

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

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

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

@@ -1,24 +0,0 @@
-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"));
-    }
-}