Ver código fonte

Merge branch 'master' of http://47.111.81.118:3000/uskycloud/usky-modules into fhs-ems

fanghuisheng 1 semana atrás
pai
commit
fe8a2e9f02
100 arquivos alterados com 6542 adições e 37 exclusões
  1. 1 1
      pom.xml
  2. 17 0
      service-data/pom.xml
  3. 33 0
      service-data/service-data-api/pom.xml
  4. 96 0
      service-data/service-data-biz/pom.xml
  5. 50 0
      service-data/service-data-biz/src/main/java/com/usky/data/DataApplicationRun.java
  6. 106 0
      service-data/service-data-biz/src/main/java/com/usky/data/MybatisGenerator.java
  7. 222 0
      service-data/service-data-biz/src/main/java/com/usky/data/controller/web/DataInterfaceController.java
  8. 21 0
      service-data/service-data-biz/src/main/java/com/usky/data/controller/web/DataInterfaceLogController.java
  9. 229 0
      service-data/service-data-biz/src/main/java/com/usky/data/controller/web/DataLinkController.java
  10. 179 0
      service-data/service-data-biz/src/main/java/com/usky/data/domain/DataInterface.java
  11. 122 0
      service-data/service-data-biz/src/main/java/com/usky/data/domain/DataInterfaceLog.java
  12. 136 0
      service-data/service-data-biz/src/main/java/com/usky/data/domain/DataLink.java
  13. 16 0
      service-data/service-data-biz/src/main/java/com/usky/data/mapper/DataInterfaceLogMapper.java
  14. 16 0
      service-data/service-data-biz/src/main/java/com/usky/data/mapper/DataInterfaceMapper.java
  15. 34 0
      service-data/service-data-biz/src/main/java/com/usky/data/mapper/DataLinkMapper.java
  16. 34 0
      service-data/service-data-biz/src/main/java/com/usky/data/model/DataInterfaceModel.java
  17. 52 0
      service-data/service-data-biz/src/main/java/com/usky/data/model/DbLinkBaseForm.java
  18. 24 0
      service-data/service-data-biz/src/main/java/com/usky/data/model/DbLinkCreUpForm.java
  19. 24 0
      service-data/service-data-biz/src/main/java/com/usky/data/model/FieldModel.java
  20. 14 0
      service-data/service-data-biz/src/main/java/com/usky/data/model/PaginationDbLink.java
  21. 26 0
      service-data/service-data-biz/src/main/java/com/usky/data/model/ParamModel.java
  22. 16 0
      service-data/service-data-biz/src/main/java/com/usky/data/service/DataInterfaceLogService.java
  23. 49 0
      service-data/service-data-biz/src/main/java/com/usky/data/service/DataInterfaceService.java
  24. 47 0
      service-data/service-data-biz/src/main/java/com/usky/data/service/DataLinkService.java
  25. 20 0
      service-data/service-data-biz/src/main/java/com/usky/data/service/impl/DataInterfaceLogServiceImpl.java
  26. 291 0
      service-data/service-data-biz/src/main/java/com/usky/data/service/impl/DataInterfaceServiceImpl.java
  27. 120 0
      service-data/service-data-biz/src/main/java/com/usky/data/service/impl/DataLinkServiceImpl.java
  28. 421 0
      service-data/service-data-biz/src/main/java/com/usky/data/service/util/InterfaceExecutor.java
  29. 49 0
      service-data/service-data-biz/src/main/java/com/usky/data/vo/DbLinkInfoVO.java
  30. 22 0
      service-data/service-data-biz/src/main/java/com/usky/data/vo/DbLinkListVO.java
  31. 14 0
      service-data/service-data-biz/src/main/java/com/usky/data/vo/DbLinkSelectorListVO.java
  32. 17 0
      service-data/service-data-biz/src/main/java/com/usky/data/vo/ListVO.java
  33. 19 0
      service-data/service-data-biz/src/main/java/com/usky/data/vo/PageListVO.java
  34. 14 0
      service-data/service-data-biz/src/main/java/com/usky/data/vo/PaginationVO.java
  35. 25 0
      service-data/service-data-biz/src/main/resources/bootstrap.yml
  36. 108 0
      service-data/service-data-biz/src/main/resources/doc/index.adoc
  37. 74 0
      service-data/service-data-biz/src/main/resources/logback.xml
  38. 28 0
      service-data/service-data-biz/src/main/resources/mapper/data/DataInterfaceLogMapper.xml
  39. 35 0
      service-data/service-data-biz/src/main/resources/mapper/data/DataInterfaceMapper.xml
  40. 56 0
      service-data/service-data-biz/src/main/resources/mapper/data/DataLinkMapper.xml
  41. 15 0
      service-data/service-data-biz/src/main/resources/smart-doc.json
  42. 29 29
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EnergyDataServiceImpl.java
  43. 11 0
      service-issue/service-issue-biz/pom.xml
  44. 8 7
      service-issue/service-issue-biz/src/main/java/com/usky/issue/IssueApplication.java
  45. 194 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/api/CloudIntegrationApiController.java
  46. 68 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudConfigController.java
  47. 43 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudSyncController.java
  48. 49 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudTriggerEventController.java
  49. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/sys_user.sql
  50. 36 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/CloudAuditEntity.java
  51. 44 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueCloudConfig.java
  52. 30 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueOperationLog.java
  53. 33 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncCursor.java
  54. 38 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncData.java
  55. 32 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncDetail.java
  56. 34 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncDownQueue.java
  57. 40 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncStatus.java
  58. 46 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueSyncTask.java
  59. 33 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/IssueTriggerEvent.java
  60. 70 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SysUser.java
  61. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueCloudConfigMapper.java
  62. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueOperationLogMapper.java
  63. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncCursorMapper.java
  64. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncDataMapper.java
  65. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncDetailMapper.java
  66. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncDownQueueMapper.java
  67. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncStatusMapper.java
  68. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueSyncTaskMapper.java
  69. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/IssueTriggerEventMapper.java
  70. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/SysUserMapper.java
  71. 16 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudAuthService.java
  72. 35 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudConfigService.java
  73. 12 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudOperationLogService.java
  74. 23 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncPublishService.java
  75. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncReceiveService.java
  76. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncService.java
  77. 18 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncTaskRunner.java
  78. 20 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudTriggerEventService.java
  79. 23 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalPullService.java
  80. 19 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalPushService.java
  81. 19 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalSyncAgent.java
  82. 321 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/client/CloudPlatformClient.java
  83. 30 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/config/CloudSyncExecutorConfig.java
  84. 55 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/constant/CloudIntegrationConstants.java
  85. 35 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncDirection.java
  86. 44 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncTypeEnum.java
  87. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/TaskStatusEnum.java
  88. 26 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/TriggerEventCodeEnum.java
  89. 21 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/exception/CloudIntegrationErrorCode.java
  90. 109 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudAuthServiceImpl.java
  91. 370 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudConfigServiceImpl.java
  92. 44 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudOperationLogServiceImpl.java
  93. 140 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncPublishServiceImpl.java
  94. 193 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncReceiveServiceImpl.java
  95. 203 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncServiceImpl.java
  96. 165 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncTaskRunnerImpl.java
  97. 93 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudTriggerEventServiceImpl.java
  98. 238 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalPullServiceImpl.java
  99. 176 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalPushServiceImpl.java
  100. 94 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalSyncAgentImpl.java

+ 1 - 1
pom.xml

@@ -89,7 +89,7 @@
 
         <module>service-rule</module>
 
-        <!--    <module>service-data</module>-->
+            <module>service-data</module>
 
         <module>service-sas</module>
 

+ 17 - 0
service-data/pom.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>usky-modules</artifactId>
+        <groupId>com.usky</groupId>
+        <version>0.0.1</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>service-data</artifactId>
+    <packaging>pom</packaging>
+    <modules>
+        <module>service-data-biz</module>
+        <module>service-data-api</module>
+  </modules>
+
+</project>

+ 33 - 0
service-data/service-data-api/pom.xml

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>service-data</artifactId>
+        <groupId>com.usky</groupId>
+        <version>0.0.1</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>service-data-api</artifactId>
+    <!-- SpringCloud Openfeign -->
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>usky-common-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-openfeign-core</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+
+</project>

+ 96 - 0
service-data/service-data-biz/pom.xml

@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>service-data</artifactId>
+        <groupId>com.usky</groupId>
+        <version>0.0.1</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>service-data-biz</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>common-cloud-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <!-- Pagehelper -->
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>ruoyi-common-swagger</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-boot-starter</artifactId>
+        </dependency>
+
+        <!--Redis依赖-->
+<!--        <dependency>-->
+<!--            <groupId>org.springframework.boot</groupId>-->
+<!--            <artifactId>spring-boot-starter-data-redis</artifactId>-->
+<!--        </dependency>-->
+
+<!--        <dependency>-->
+<!--            <groupId>com.usky</groupId>-->
+<!--            <artifactId>usky-common-redis</artifactId>-->
+<!--        </dependency>-->
+
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.2.6.RELEASE</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>com.github.shalousun</groupId>
+                <artifactId>smart-doc-maven-plugin</artifactId>
+                <version>2.1.1</version>
+                <configuration>
+                    <!--指定生成文档的使用的配置文件,配置文件放在自己的项目中-->
+                    <configFile>./src/main/resources/smart-doc.json</configFile>
+                    <!--指定项目名称-->
+                    <projectName>test</projectName>
+                    <!--                    <excludes>-->
+                    <!--                        <exclude>com.bizmatics:product-service-provider</exclude>-->
+                    <!--                        <exclude>cn.afterturn:easypoi-web</exclude>-->
+                    <!--                    </excludes>-->
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 50 - 0
service-data/service-data-biz/src/main/java/com/usky/data/DataApplicationRun.java

@@ -0,0 +1,50 @@
+package com.usky.data;
+
+
+import com.ruoyi.common.swagger.annotation.EnableCustomSwagger2;
+import org.mybatis.spring.annotation.MapperScan;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.core.env.Environment;
+import org.springframework.scheduling.annotation.EnableAsync;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * 应用启动模块
+ *
+ * @author fyc
+ */
+
+
+@EnableCustomSwagger2
+//@EnableSwagger2
+@EnableFeignClients(basePackages = "com.usky")
+@MapperScan(value = "com.usky.data.mapper")
+@ComponentScan(basePackages = {"com.usky"})
+@EnableAsync
+@SpringBootApplication
+public class DataApplicationRun {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(DataApplicationRun.class);
+
+    public static void main(String[] args) throws UnknownHostException {
+        ConfigurableApplicationContext application = SpringApplication.run(DataApplicationRun.class, args);
+        Environment env = application.getEnvironment();
+        String ip = InetAddress.getLocalHost().getHostAddress();
+        String port = env.getProperty("server.port");
+        String path = env.getProperty("server.servlet.context-path");
+        LOGGER.info("\n----------------------------------------------------------\n\t" +
+                "Application is running! Access URLs:\n\t" +
+                "Local: \t\thttp://localhost:" + port + (null == path ? "" : path) + "/\n\t" +
+                "External: \thttp://" + ip + ":" + port + (null == path ? "" : path) + "/\n\t" +
+                "Api: \t\thttp://" + ip + ":" + port + (null == path ? "" : path) + "/swagger-ui/index.html\n\t" +
+                "----------------------------------------------------------");
+    }
+}

+ 106 - 0
service-data/service-data-biz/src/main/java/com/usky/data/MybatisGenerator.java

@@ -0,0 +1,106 @@
+package com.usky.data;
+
+import com.baomidou.mybatisplus.core.toolkit.StringPool;
+import com.baomidou.mybatisplus.generator.AutoGenerator;
+import com.baomidou.mybatisplus.generator.InjectionConfig;
+import com.baomidou.mybatisplus.generator.config.*;
+import com.baomidou.mybatisplus.generator.config.po.TableInfo;
+import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ */
+public class MybatisGenerator {
+    public static void main(String[] args) {
+
+        shell("service-data", "service-data-biz");
+    }
+
+    private static void shell(String parentName, String model) {
+
+        AutoGenerator mpg = new AutoGenerator();
+        // 1、全局配置
+        GlobalConfig gc = new GlobalConfig();
+//        File file = new File(model);
+//        String path = file.getAbsolutePath();
+        String projectPath = System.getProperty("user.dir");
+        projectPath += "/" + parentName;
+        projectPath += "/" + model;
+        gc.setOutputDir(projectPath + "/src/main/java");  // 生成路径(一般都是生成在此项目的src/main/java下面)
+        // 修改为自己的名字
+        gc.setAuthor("fyc"); // 设置作者
+        gc.setOpen(false);
+        gc.setFileOverride(true); // 第二次生成会把第一次生成的覆盖掉
+        gc.setServiceName("%sService"); // 生成的service接口名字首字母是否为I,这样设置就没有
+        gc.setBaseResultMap(true); // 生成resultMap
+        mpg.setGlobalConfig(gc);
+
+        // 2、数据源配置
+        // 修改数据源
+        DataSourceConfig dsc = new DataSourceConfig();
+        dsc.setUrl("jdbc:mysql://192.168.10.165:3306/usky-cloud?useUnicode=true&serverTimezone=GMT&useSSL=false&characterEncoding=utf8");
+        dsc.setDriverName("com.mysql.jdbc.Driver");
+        dsc.setUsername("root");
+        dsc.setPassword("yt123456");
+        mpg.setDataSource(dsc);
+
+        // 3、包配置
+        PackageConfig pc = new PackageConfig();
+        pc.setParent("com.usky.data");
+        pc.setController("controller.web");
+        pc.setEntity("domain");
+        pc.setMapper("mapper");
+        pc.setService("service");
+        pc.setServiceImpl("service.impl");
+//        pc.setXml("mapper.data");
+        // pc.setModuleName("test");
+        mpg.setPackageInfo(pc);
+
+        // 4、策略配置
+        StrategyConfig strategy = new StrategyConfig();
+        strategy.setNaming(NamingStrategy.underline_to_camel);
+        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
+        strategy.setSuperMapperClass("com.usky.common.mybatis.core.CrudMapper");
+        strategy.setSuperServiceClass("com.usky.common.mybatis.core.CrudService");
+        strategy.setSuperServiceImplClass("com.usky.common.mybatis.core.AbstractCrudService");
+        // strategy.setTablePrefix("t_"); // 表名前缀
+        strategy.setEntityLombokModel(true); // 使用lombok
+        // 修改自己想要生成的表
+        String[] tables = {"data_interface","data_interface_log"};
+        strategy.setInclude(tables);  // 逆向工程使用的表   如果要生成多个,这里可以传入String[]
+        mpg.setStrategy(strategy);
+
+        // 关闭默认 xml 生成,调整生成 至 根目录
+        // 修改对应的模块名称
+        TemplateConfig tc = new TemplateConfig();
+        // 自定义配置
+        InjectionConfig cfg = new InjectionConfig() {
+            @Override
+            public void initMap() {
+                // to do nothing
+            }
+        };
+        // 如果模板引擎是 velocity
+        String templatePath = "/templates/mapper.xml.vm";
+        // 自定义输出配置
+        List<FileOutConfig> focList = new ArrayList<>();
+        // 自定义配置会被优先输出
+        String finalProjectPath = projectPath;
+        focList.add(new FileOutConfig(templatePath) {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
+                return finalProjectPath + "/src/main/resources/mapper/data" + "/"
+                        + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
+            }
+        });
+        cfg.setFileOutConfigList(focList);
+        mpg.setCfg(cfg);
+        tc.setXml(null);
+        mpg.setTemplate(tc);
+        // 5、执行
+        mpg.execute();
+    }
+}

+ 222 - 0
service-data/service-data-biz/src/main/java/com/usky/data/controller/web/DataInterfaceController.java

@@ -0,0 +1,222 @@
+package com.usky.data.controller.web;
+
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.data.domain.DataInterface;
+import com.usky.data.service.DataInterfaceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ * 数据中心_数据接口表 前端控制器
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+@RestController
+@RequestMapping("/dataInterface")
+public class DataInterfaceController {
+
+    @Autowired
+    private DataInterfaceService dataInterfaceService;
+
+    /**
+     * 获取数据接口列表
+     *
+     * @param currentPage 当前页
+     * @param pageSize    每页数量
+     * @param keyword     关键字
+     * @param type        接口类型 1:SQL操作, 2:静态数据, 3:API操作
+     * @param enabledMark 启用状态 0:未启用,1:已启用
+     * @param category    接口分类
+     * @return
+     */
+    @RequestMapping
+    @GetMapping
+    public ApiResult<CommonPage<DataInterface>> getDataInterfaceList(@RequestParam(value = "currentPage", defaultValue = "1") Integer currentPage,
+                                                                     @RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize,
+                                                                     @RequestParam(value = "keyword", required = false) String keyword,
+                                                                     @RequestParam(value = "type", required = false) String type,
+                                                                     @RequestParam(value = "enabledMark", required = false) Integer enabledMark,
+                                                                     @RequestParam(value = "category", required = false) String category
+    ) {
+        return ApiResult.success(dataInterfaceService.getDataInterfaceList(currentPage, pageSize, keyword, type, enabledMark, category));
+    }
+
+    /**
+     * 获取接口列表(工作流选择时调用)
+     *
+     * @param currentPage 当前页
+     * @param pageSize    每页数量
+     * @param keyword     关键字
+     * @param type        接口类型 1:静态数据, 2:SQL操作, 3:API操作
+     * @param enabledMark 启用状态 0:未启用,1:已启用
+     * @return
+     */
+    @GetMapping("/getList")
+    public ApiResult<CommonPage<DataInterface>> getWorkflowList(@RequestParam(value = "currentPage", defaultValue = "1") Integer currentPage,
+                                                                @RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize,
+                                                                @RequestParam(value = "keyword", required = false) String keyword,
+                                                                @RequestParam(value = "type", required = false) String type,
+                                                                @RequestParam(value = "enabledMark", required = false) Integer enabledMark
+    ) {
+        return ApiResult.success(dataInterfaceService.getWorkflowList(currentPage, pageSize, keyword, type, enabledMark));
+    }
+
+    /**
+     * 获取接口列表下拉框
+     *
+     * @return
+     */
+    @GetMapping("/Selector")
+    public ApiResult<List<DataInterface>> getSelector() {
+        return ApiResult.success(dataInterfaceService.getSelectorList());
+    }
+
+    /**
+     * 获取接口参数列表下拉框
+     *
+     * @param id 主键
+     * @return
+     */
+    @GetMapping("/getParam/{id}")
+    public ApiResult<String> getInterfaceParams(@PathVariable("id") String id) {
+        return ApiResult.success(dataInterfaceService.getInterfaceParameters(id));
+    }
+
+    /**
+     * 获取接口数据
+     *
+     * @param id 主键
+     * @return
+     */
+    @GetMapping("/{id}")
+    public ApiResult<DataInterface> getInterfaceInfo(@PathVariable("id") String id) {
+        return ApiResult.success(dataInterfaceService.getById(id));
+    }
+
+    /**
+     * 添加接口
+     *
+     * @param dataInterface 接口实体
+     * @return
+     */
+    @PostMapping
+    public ApiResult<Boolean> addInterface(@RequestBody DataInterface dataInterface) {
+        return ApiResult.success(dataInterfaceService.addInterface(dataInterface));
+    }
+
+    /**
+     * 修改接口
+     *
+     * @param dataInterface 接口实体
+     * @return
+     */
+    @PutMapping
+    public ApiResult<Boolean> updateInterface(@RequestBody DataInterface dataInterface) {
+        return ApiResult.success(dataInterfaceService.updateInterface(dataInterface));
+    }
+
+    /**
+     * 删除接口
+     *
+     * @param id 主键
+     * @return
+     */
+    @DeleteMapping("/{id}")
+    public ApiResult<Boolean> deleteInterface(@PathVariable("id") Long id) {
+        return ApiResult.success(dataInterfaceService.deleteInterface(id));
+    }
+
+    /**
+     * 更新接口状态
+     *
+     * @param id 主键
+     * @return
+     */
+    @PutMapping("/{id}/Actions/State")
+    public ApiResult<Boolean> updateInterfaceState(@PathVariable("id") String id) {
+        return ApiResult.success(dataInterfaceService.updateInterfaceStatus(id));
+    }
+
+    /**
+     * 测试接口
+     *
+     * @param id     主键
+     * @param params 参数、参数值对象
+     * @return
+     */
+    @PostMapping("/{id}/actions/preview")
+    public ApiResult<?> testInterface(@PathVariable("id") String id, @RequestBody(required = false) Map<String, Object> params) {
+        return dataInterfaceService.testInterface(id, params);
+    }
+
+    /**
+     * 访问接口GET
+     *
+     * @param id     主键
+     * @param params 参数、参数值对象
+     * @return
+     */
+    @GetMapping("/{id}/actions/response")
+    public ApiResult<?> getInterfaceResponse(@PathVariable("id") String id, @RequestParam(required = false) Map<String, String> params) {
+        return dataInterfaceService.getInterfaceResponse(id, params);
+    }
+
+    /**
+     * 访问接口POST
+     *
+     * @param id     主键
+     * @param params 参数、参数值对象
+     * @return
+     */
+    @PostMapping("/{id}/actions/response")
+    public ApiResult<?> postInterfaceResponse(@PathVariable("id") String id, @RequestBody(required = false) Map<String, String> params) {
+        return dataInterfaceService.postInterfaceResponse(id, params);
+    }
+
+    /**
+     * 获取接口分页数据
+     *
+     * @param id         主键
+     * @param pageParams 分页参数
+     * @return
+     */
+    @PostMapping("/{id}/actions/list")
+    public ApiResult<?> getInterfacePageData(@PathVariable("id") String id, @RequestBody Map<String, Object> pageParams) {
+        return dataInterfaceService.getInterfacePageData(id, pageParams);
+    }
+
+    /**
+     * 获取接口详情数据
+     *
+     * @param id         主键
+     * @param pageParams 分页参数
+     * @return
+     */
+    @PostMapping("/{id}/actions/infoByIds")
+    public ApiResult<List<Map<String, Object>>> getInterfaceDetailData(@PathVariable("id") String id, @RequestBody Map<String, Object> pageParams) {
+        return dataInterfaceService.getInterfaceDetailData(id, pageParams);
+    }
+
+    /**
+     * 获取接口字段
+     *
+     * @param id         主键
+     * @param pageParams 参数、参数值
+     * @return
+     */
+    @PostMapping("/{id}/actions/getFields")
+    public ApiResult<List<String>> getInterfaceFields(@PathVariable("id") String id, @RequestBody(required = false) Map<String, Object> pageParams) {
+        return dataInterfaceService.getInterfaceFields(id, pageParams);
+    }
+}
+

+ 21 - 0
service-data/service-data-biz/src/main/java/com/usky/data/controller/web/DataInterfaceLogController.java

@@ -0,0 +1,21 @@
+package com.usky.data.controller.web;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.stereotype.Controller;
+
+/**
+ * <p>
+ * 数据中心_接口调用日志表 前端控制器
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+@Controller
+@RequestMapping("/dataInterfaceLog")
+public class DataInterfaceLogController {
+
+}
+

+ 229 - 0
service-data/service-data-biz/src/main/java/com/usky/data/controller/web/DataLinkController.java

@@ -0,0 +1,229 @@
+package com.usky.data.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.data.domain.DataLink;
+import com.usky.data.model.DbLinkBaseForm;
+import com.usky.data.model.DbLinkCreUpForm;
+import com.usky.data.model.PaginationDbLink;
+import com.usky.data.service.DataLinkService;
+import com.usky.data.vo.*;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Parameters;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 数据源配置控制器
+ *
+ * @author fyc
+ * @since 2026-01-23
+ */
+@Tag(name = "数据连接", description = "DataSource")
+@RestController
+@RequestMapping("/dataSource")
+public class DataLinkController {
+
+    @Autowired
+    private DataLinkService dataLinkService;
+
+    /**
+     * 获取数据连接下拉框列表
+     */
+    @GetMapping("/selector")
+    @Operation(summary = "获取数据连接下拉框列表")
+    public ApiResult<ListVO<DbLinkSelectorListVO>> selectorList(@RequestParam(value = "dbType") String type) {
+        List<DbLinkListVO> modelAll = new LinkedList<>();
+        List<DataLink> dbLinkList = dataLinkService.getList();
+
+        // 转换为VO列表
+        for (DataLink entity : dbLinkList) {
+            DbLinkListVO model = new DbLinkListVO();
+            BeanUtils.copyProperties(entity, model);
+            modelAll.add(model);
+        }
+
+        // 按数据库类型分组
+        Map<String, List<DbLinkListVO>> groupByType = modelAll.stream()
+                .collect(Collectors.groupingBy(DbLinkListVO::getDbType));
+
+        List<DbLinkSelectorListVO> list = new ArrayList<>();
+        // type为空时返回默认库
+        if (type == null) {
+            DbLinkListVO dbLink = new DbLinkListVO();
+            dbLink.setFullName("默认数据库");
+            dbLink.setId(0);
+            // 默认数据库类型
+            dbLink.setDbType("mysql");
+            DbLinkSelectorListVO defaultDb = new DbLinkSelectorListVO();
+            defaultDb.setFullName("");
+            defaultDb.setChildren(Collections.singletonList(dbLink));
+            list.add(defaultDb);
+        }
+
+        // 添加分组后的数据源
+        for (Map.Entry<String, List<DbLinkListVO>> entry : groupByType.entrySet()) {
+            DbLinkSelectorListVO selector = new DbLinkSelectorListVO();
+            selector.setFullName(entry.getKey());
+            selector.setChildren(entry.getValue());
+            list.add(selector);
+        }
+
+        return ApiResult.success(new ListVO<>(list));
+    }
+
+    /**
+     * 获取数据连接列表
+     */
+    @GetMapping
+    @Operation(summary = "获取数据连接列表")
+    public ApiResult<PageListVO<DbLinkListVO>> getList(@RequestBody PaginationDbLink page) {
+        List<DataLink> data = dataLinkService.getList(
+                page.getCurrentPage(),
+                page.getPageSize(),
+                page.getKeyword()
+        );
+
+        // 转换为VO列表
+        List<DbLinkListVO> jsonToList = new ArrayList<>();
+        for (DataLink entity : data) {
+            DbLinkListVO vo = new DbLinkListVO();
+            BeanUtils.copyProperties(entity, vo);
+            jsonToList.add(vo);
+        }
+
+        // 构建分页信息
+        PaginationVO paginationVO = new PaginationVO();
+        paginationVO.setCurrentPage(page.getCurrentPage());
+        paginationVO.setPageSize(page.getPageSize());
+        paginationVO.setTotalRows(data.size());
+        paginationVO.setTotalPages((int) Math.ceil((double) data.size() / page.getPageSize()));
+
+        return ApiResult.success(new PageListVO<>(jsonToList, paginationVO));
+    }
+
+    /**
+     * 获取数据连接列表分组
+     */
+    @GetMapping("/getGroup")
+    @Operation(summary = "获取数据连接列表分组")
+    public ApiResult<List<Map<String, Object>>> getGroup(@RequestBody PaginationDbLink page) {
+        ApiResult<PageListVO<DbLinkListVO>> result = getList(page);
+        List<DbLinkListVO> voList = result.getData().getList();
+
+        // 按host分组
+        Set<String> hostSet = voList.stream()
+                .sorted(Comparator.comparing(DbLinkListVO::getHost))
+                .map(DbLinkListVO::getHost)
+                .collect(Collectors.toSet());
+
+        List<Map<String, Object>> groupList = new ArrayList<>();
+        for (String host : hostSet) {
+            Map<String, Object> groupMap = new HashMap<>();
+            groupMap.put("host", host);
+            groupMap.put("options", voList.stream()
+                    .filter(vo -> vo.getHost().equals(host))
+                    .toArray());
+            groupList.add(groupMap);
+        }
+
+        return ApiResult.success(groupList);
+    }
+
+    /**
+     * 获取单条数据连接
+     */
+    @GetMapping("/{id}")
+    @Operation(summary = "获取数据连接")
+    @Parameters({
+            @Parameter(name = "id", description = "主键")
+    })
+    public ApiResult<DbLinkInfoVO> get(@PathVariable("id") Integer id) {
+        DataLink entity = dataLinkService.getById(id);
+        if (entity == null) {
+            return ApiResult.error("数据源不存在");
+        }
+        DbLinkInfoVO vo = new DbLinkInfoVO();
+        return ApiResult.success(vo.getDbLinkInfoVO(entity));
+    }
+
+    /**
+     * 新建数据连接
+     */
+    @PostMapping
+    @Operation(summary = "添加数据连接")
+    @Parameters({
+            @Parameter(name = "dbLinkCreUpForm", description = "新建数据连接表单对象", required = true)
+    })
+    public ApiResult<String> create(@RequestBody @Validated DbLinkCreUpForm dbLinkCreUpForm) {
+        DataLink entity = dbLinkCreUpForm.getDbLinkEntity(dbLinkCreUpForm);
+        if (dataLinkService.isExistByFullName(entity.getFullName(), null)) {
+            return ApiResult.error("连接名称已存在");
+        }
+        return ApiResult.success(dataLinkService.save(entity) ? "新增成功" : "新增失败");
+    }
+
+    /**
+     * 更新数据连接
+     */
+    @PutMapping("/{id}")
+    @Operation(summary = "修改数据连接")
+    @Parameters({
+            @Parameter(name = "id", description = "主键", required = true),
+            @Parameter(name = "dbLinkCreUpForm", description = "更新数据连接表单对象", required = true)
+    })
+    public ApiResult<String> update(@PathVariable("id") Integer id, @RequestBody @Validated DbLinkCreUpForm dbLinkCreUpForm) {
+        dbLinkCreUpForm.setId(id);
+        DataLink entity = dbLinkCreUpForm.getDbLinkEntity(dbLinkCreUpForm);
+        if (dataLinkService.isExistByFullName(entity.getFullName(), id)) {
+            throw new BusinessException("连接名称已存在");
+        }
+        if (!dataLinkService.updateById(entity)) {
+            return ApiResult.error("更新失败");
+        }
+        return ApiResult.success("更新成功");
+    }
+
+    /**
+     * 删除数据连接
+     */
+    @DeleteMapping("/{id}")
+    @Operation(summary = "删除数据连接")
+    @Parameters({
+            @Parameter(name = "id", description = "主键", required = true)
+    })
+    public ApiResult<String> delete(@PathVariable("id") Integer id) {
+        DataLink entity = dataLinkService.getById(id);
+        if (entity != null) {
+            dataLinkService.removeById(entity);
+            return ApiResult.success("删除成功");
+        }
+        return ApiResult.error("数据源不存在");
+    }
+
+    /**
+     * 测试连接
+     */
+    @PostMapping("/actions/test")
+    @Operation(summary = "测试连接")
+    @Parameters({
+            @Parameter(name = "dbLinkBaseForm", description = "数据连接参数", required = true)
+    })
+    public ApiResult<String> test(@RequestBody DbLinkBaseForm dbLinkBaseForm) throws Exception {
+        boolean data = dataLinkService.testDbConnection(dbLinkBaseForm.getDbLinkEntity(dbLinkBaseForm));
+        if (data) {
+            return ApiResult.success("连接成功");
+        } else {
+            return ApiResult.error("连接失败");
+        }
+    }
+}
+

+ 179 - 0
service-data/service-data-biz/src/main/java/com/usky/data/domain/DataInterface.java

@@ -0,0 +1,179 @@
+package com.usky.data.domain;
+
+import java.time.LocalDateTime;
+import java.io.Serializable;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+
+/**
+ * <p>
+ * 数据中心_数据接口表
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class DataInterface implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 接口名称
+     */
+    @NotBlank(message = "接口名称不能为空")
+    private String fullName;
+
+    /**
+     * 接口编码
+     */
+    @NotBlank(message = "接口编码不能为空")
+    private String enCode;
+
+    /**
+     * 接口分类
+     */
+    private String category;
+
+    /**
+     * 接口类型(1:静态数据, 2:SQL操作, 3:API操作)
+     */
+    @NotNull(message = "接口类型不能为空")
+    private Integer type;
+
+    /**
+     * 操作类型()
+     */
+    private Integer action;
+
+    /**
+     * 分页(0-禁用,1-启用)
+     */
+    private Integer hasPage;
+
+    /**
+     * 后置接口(0-否 1-是)
+     */
+    private Integer isPostposition;
+
+    /**
+     * 字段JSON
+     */
+    private String fieldJson;
+
+    /**
+     * 数据配置JSON
+     */
+    @TableField("config_json")
+    @NotBlank(message = "数据配置JSON不能为空")
+    private String dataConfigJson;
+
+    /**
+     * 数据统计json
+     */
+    @TableField("count_json")
+    @NotBlank(message = "数据统计json不能为空")
+    private String dataCountJson;
+
+    /**
+     * 数据回显json
+     */
+    @TableField("echo_json")
+    @NotBlank(message = "数据回显json不能为空")
+    private String dataEchoJson;
+
+    /**
+     * 数据处理json
+     */
+    @TableField("js_json")
+    @NotBlank(message = "数据处理json不能为空")
+    private String dataJsJson;
+
+    /**
+     * 参数配置JSON
+     */
+    private String parameterJson;
+
+    /**
+     * 异常验证JSON
+     */
+    private String exceptionJson;
+
+    /**
+     * 描述或说明
+     */
+    private String description;
+
+    /**
+     * 启用状态(0-禁用,1-启用)
+     */
+    private Integer enabledMark;
+
+    /**
+     * 排序
+     */
+    private Long sortCode;
+
+    /**
+     * 创建者
+     */
+    @NotBlank(message = "创建人不能为空")
+    private String createBy;
+
+    /**
+     * 创建时间
+     */
+    @NotNull(message = "创建时间不能为空")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新者
+     */
+    @NotBlank(message = "更新人不能为空")
+    private String updateBy;
+
+    /**
+     * 更新时间
+     */
+    @NotNull(message = "更新时间不能为空")
+    private LocalDateTime updateTime;
+
+    /**
+     * 删除时间
+     */
+    @NotNull(message = "删除时间不能为空")
+    private LocalDateTime deleteTime;
+
+    /**
+     * 删除用户
+     */
+    @NotBlank(message = "删除人不能为空")
+    private String deleteBy;
+
+    /**
+     * 是否删除 0:未删除,1:已删除
+     */
+    private Integer isDelete;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+
+}

+ 122 - 0
service-data/service-data-biz/src/main/java/com/usky/data/domain/DataInterfaceLog.java

@@ -0,0 +1,122 @@
+package com.usky.data.domain;
+
+import java.time.LocalDateTime;
+import java.io.Serializable;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 数据中心_接口调用日志表
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class DataInterfaceLog implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 调用接口ID
+     */
+    private Long interfaceId;
+
+    /**
+     * 执行时间
+     */
+    private LocalDateTime executeTime;
+
+    /**
+     * 操作用户ID
+     */
+    private String userId;
+
+    /**
+     * 请求ip
+     */
+    private String requestIp;
+
+    /**
+     * 请求设备
+     */
+    private String requestDevice;
+
+    /**
+     * 请求类型
+     */
+    private String requestType;
+
+    /**
+     * 请求耗时
+     */
+    private Integer requestWasteTime;
+
+    /**
+     * 接口授权AppId
+     */
+    private String oauthAppId;
+
+    /**
+     * 排序
+     */
+    private Long sortCode;
+
+    /**
+     * 执行结果
+     */
+    private String executeResult;
+
+    /**
+     * 执行状态
+     */
+    private Integer executeState;
+
+    /**
+     * 执行消息
+     */
+    private String executeMsg;
+
+    /**
+     * 创建者
+     */
+    private String createBy;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新者
+     */
+    private String updateBy;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 部门ID
+     */
+    private Long deptId;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+
+}

+ 136 - 0
service-data/service-data-biz/src/main/java/com/usky/data/domain/DataLink.java

@@ -0,0 +1,136 @@
+package com.usky.data.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 数据中心_数据连接表
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-23
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class DataLink implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 连接名称
+     */
+    private String fullName;
+
+    /**
+     * 连接驱动
+     */
+    private String dbType;
+
+    /**
+     * 主机地址
+     */
+    private String host;
+
+    /**
+     * 端口
+     */
+    private Integer port;
+
+    /**
+     * 数据库用户名
+     */
+    private String dbName;
+
+    /**
+     * 密码
+     */
+    private String password;
+
+    /**
+     * 服务名称
+     */
+    private String serviceName;
+
+    /**
+     * 描述或说明
+     */
+    private String description;
+
+    /**
+     * 模式
+     */
+    private String dbSchema;
+
+    /**
+     * 表空间
+     */
+    private String tableSpace;
+
+    /**
+     * oracle连接参数
+     */
+    private String oracleParam;
+
+    /**
+     * Oracle扩展开关 1:开启 0:关闭
+     */
+    private Integer oracleExtend;
+
+    /**
+     * 排序
+     */
+    private Integer sortCode;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 创建用户
+     */
+    private String createBy;
+
+    /**
+     * 修改时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 修改用户
+     */
+    private String updateBy;
+
+    /**
+     * 删除时间
+     */
+    private LocalDateTime deleteTime;
+
+    /**
+     * 删除用户
+     */
+    private String deleteBy;
+
+    /**
+     * 是否删除 0:未删除,1:已删除
+     */
+    private Integer isDelete;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+
+}

+ 16 - 0
service-data/service-data-biz/src/main/java/com/usky/data/mapper/DataInterfaceLogMapper.java

@@ -0,0 +1,16 @@
+package com.usky.data.mapper;
+
+import com.usky.data.domain.DataInterfaceLog;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 数据中心_接口调用日志表 Mapper 接口
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+public interface DataInterfaceLogMapper extends CrudMapper<DataInterfaceLog> {
+
+}

+ 16 - 0
service-data/service-data-biz/src/main/java/com/usky/data/mapper/DataInterfaceMapper.java

@@ -0,0 +1,16 @@
+package com.usky.data.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.data.domain.DataInterface;
+
+/**
+ * <p>
+ * 数据中心_数据接口表 Mapper 接口
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+public interface DataInterfaceMapper extends CrudMapper<DataInterface> {
+
+}

+ 34 - 0
service-data/service-data-biz/src/main/java/com/usky/data/mapper/DataLinkMapper.java

@@ -0,0 +1,34 @@
+package com.usky.data.mapper;
+
+import com.usky.data.domain.DataLink;
+import com.usky.common.mybatis.core.CrudMapper;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 数据中心_数据连接表 Mapper 接口
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-23
+ */
+public interface DataLinkMapper extends CrudMapper<DataLink> {
+
+    /**
+     * 根据连接名称统计数量(排除指定ID)
+     * @param fullName 连接名称
+     * @param id 排除的主键ID
+     * @return 数量
+     */
+    int countByFullName(String fullName, Integer id);
+
+    /**
+     * 分页查询数据源列表
+     * @param startIndex 起始索引
+     * @param pageSize 每页条数
+     * @param keyword 关键字
+     * @return 数据源列表
+     */
+    List<DataLink> selectPageList(int startIndex, int pageSize, String keyword);
+}

+ 34 - 0
service-data/service-data-biz/src/main/java/com/usky/data/model/DataInterfaceModel.java

@@ -0,0 +1,34 @@
+package com.usky.data.model;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class DataInterfaceModel extends ParamModel implements Serializable {
+
+    /**
+     * 是否为空(0允许,1不允许)
+     */
+    @Schema(description = "是否为空(0允许,1不允许)")
+    private Integer required;
+
+    /**
+     * 参数名称
+     */
+    @Schema(description = "参数名称")
+    private String parameter;
+    /**
+     * 参数来源
+     */
+    @Schema(description = "参数来源(1-字段,2-自定义,3-为空,4-系统变量)")
+    private Integer sourceType = 1;
+
+    /**
+     * 表单字段
+     */
+    @Schema(description = "表单字段")
+    private String relationField;
+}

+ 52 - 0
service-data/service-data-biz/src/main/java/com/usky/data/model/DbLinkBaseForm.java

@@ -0,0 +1,52 @@
+package com.usky.data.model;
+
+import com.usky.data.domain.DataLink;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 数据源基础表单
+ */
+@Data
+public class DbLinkBaseForm {
+    @NotBlank(message = "连接名称不能为空")
+    private String fullName;
+    @NotBlank(message = "数据库类型不能为空")
+    private String dbType;
+    @NotBlank(message = "主机地址不能为空")
+    private String host;
+    private Integer port;
+    @NotBlank(message = "数据库用户名不能为空")
+    private String dbName;
+    @NotBlank(message = "密码不能为空")
+    private String password;
+    private String serviceName;
+    private String description;
+    private String dbSchema;
+    private String tableSpace;
+    private String oracleParam;
+    private Integer oracleExtend;
+    private Integer sortCode;
+
+    /**
+     * 转换为实体对象
+     */
+    public DataLink getDbLinkEntity(DbLinkBaseForm form) {
+        DataLink dataLink = new DataLink();
+        dataLink.setFullName(form.getFullName());
+        dataLink.setDbType(form.getDbType());
+        dataLink.setHost(form.getHost());
+        dataLink.setPort(form.getPort());
+        dataLink.setDbName(form.getDbName());
+        dataLink.setPassword(form.getPassword());
+        dataLink.setServiceName(form.getServiceName());
+        dataLink.setDescription(form.getDescription());
+        dataLink.setDbSchema(form.getDbSchema());
+        dataLink.setTableSpace(form.getTableSpace());
+        dataLink.setOracleParam(form.getOracleParam());
+        dataLink.setOracleExtend(form.getOracleExtend());
+        dataLink.setSortCode(form.getSortCode());
+        return dataLink;
+    }
+}

+ 24 - 0
service-data/service-data-biz/src/main/java/com/usky/data/model/DbLinkCreUpForm.java

@@ -0,0 +1,24 @@
+package com.usky.data.model;
+
+import com.usky.data.domain.DataLink;
+import lombok.Data;
+
+/**
+ * 数据源创建/更新表单
+ */
+@Data
+public class DbLinkCreUpForm extends DbLinkBaseForm {
+    private Integer id;
+
+    /**
+     * 转换为实体对象
+     */
+    @Override
+    public DataLink getDbLinkEntity(DbLinkBaseForm form) {
+        DataLink dataLink = super.getDbLinkEntity(form);
+        if (this.getId() != null) {
+            dataLink.setId(this.getId());
+        }
+        return dataLink;
+    }
+}

+ 24 - 0
service-data/service-data-biz/src/main/java/com/usky/data/model/FieldModel.java

@@ -0,0 +1,24 @@
+package com.usky.data.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+@Data
+public class FieldModel implements Serializable {
+    @Schema(description = "主键")
+    private String id;
+
+    @Schema(description = "参数名称")
+    private String field;
+
+    @Schema(description = "默认值")
+    private String defaultValue;
+
+    public void setDefaultValue(Object defaultValue) {
+        if (Objects.isNull(defaultValue) || defaultValue.toString().trim().isEmpty()) return;
+        this.defaultValue = defaultValue.toString();
+    }
+}

+ 14 - 0
service-data/service-data-biz/src/main/java/com/usky/data/model/PaginationDbLink.java

@@ -0,0 +1,14 @@
+package com.usky.data.model;
+
+import lombok.Data;
+
+/**
+ * 数据源分页查询参数
+ */
+@Data
+public class PaginationDbLink {
+    private Integer currentPage = 1;
+    private Integer pageSize = 20;
+    private String keyword;
+    private String type;
+}

+ 26 - 0
service-data/service-data-biz/src/main/java/com/usky/data/model/ParamModel.java

@@ -0,0 +1,26 @@
+package com.usky.data.model;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class ParamModel extends FieldModel implements Serializable {
+
+    @Schema(description = "列说明")
+    private String fieldName;
+
+    /**
+     * 参数类型
+     * 字符串
+     * 整型
+     * 日期时间
+     * 浮点
+     * 长整型
+     * 文本
+     */
+    @Schema(description = "参数类型")
+    private String dataType;
+
+}

+ 16 - 0
service-data/service-data-biz/src/main/java/com/usky/data/service/DataInterfaceLogService.java

@@ -0,0 +1,16 @@
+package com.usky.data.service;
+
+import com.usky.data.domain.DataInterfaceLog;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 数据中心_接口调用日志表 服务类
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+public interface DataInterfaceLogService extends CrudService<DataInterfaceLog> {
+
+}

+ 49 - 0
service-data/service-data-biz/src/main/java/com/usky/data/service/DataInterfaceService.java

@@ -0,0 +1,49 @@
+package com.usky.data.service;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.mybatis.core.CrudService;
+import com.usky.data.domain.DataInterface;
+
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ * 数据中心_数据接口表 服务类
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+public interface DataInterfaceService extends CrudService<DataInterface> {
+
+    CommonPage<DataInterface> getDataInterfaceList(Integer currentPage, Integer pageSize, String keyword, String type, Integer enabledMark, String category);
+
+    CommonPage<DataInterface> getWorkflowList(Integer currentPage, Integer pageSize, String keyword, String type, Integer enabledMark);
+
+    List<DataInterface> getSelectorList();
+
+    String getInterfaceParameters(String id);
+
+    boolean updateInterfaceStatus(String id);
+
+    ApiResult<?> testInterface(String id, Map<String, Object> params);
+
+    ApiResult<?> getInterfaceResponse(String id, Map<String, String> params);
+
+    ApiResult<?> postInterfaceResponse(String id, Map<String, String> params);
+
+    ApiResult<?> getInterfacePageData(String id, Map<String, Object> pageParams);
+
+    ApiResult<List<Map<String, Object>>> getInterfaceDetailData(String id, Map<String, Object> pageParams);
+
+    ApiResult<List<String>> getInterfaceFields(String id, Map<String, Object> pageParams);
+
+    Boolean deleteInterface(Long id);
+
+    Boolean addInterface(DataInterface dataInterface);
+
+    Boolean updateInterface(DataInterface dataInterface);
+}

+ 47 - 0
service-data/service-data-biz/src/main/java/com/usky/data/service/DataLinkService.java

@@ -0,0 +1,47 @@
+package com.usky.data.service;
+
+import com.usky.data.domain.DataLink;
+import com.usky.common.mybatis.core.CrudService;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 数据中心_数据连接表 服务类
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-23
+ */
+public interface DataLinkService extends CrudService<DataLink> {
+
+    /**
+     * 测试数据库连接
+     * @param dataLink 数据源信息
+     * @return 是否连接成功
+     */
+    boolean testDbConnection(DataLink dataLink);
+
+    /**
+     * 根据连接名称检查是否存在
+     * @param fullName 连接名称
+     * @param id 排除的主键ID
+     * @return 是否存在
+     */
+    boolean isExistByFullName(String fullName, Integer id);
+
+    /**
+     * 获取所有数据源列表
+     * @return 数据源列表
+     */
+    List<DataLink> getList();
+
+    /**
+     * 分页获取数据源列表
+     * @param page 页码
+     * @param pageSize 每页条数
+     * @param keyword 关键字
+     * @return 数据源列表
+     */
+    List<DataLink> getList(int page, int pageSize, String keyword);
+}

+ 20 - 0
service-data/service-data-biz/src/main/java/com/usky/data/service/impl/DataInterfaceLogServiceImpl.java

@@ -0,0 +1,20 @@
+package com.usky.data.service.impl;
+
+import com.usky.data.domain.DataInterfaceLog;
+import com.usky.data.mapper.DataInterfaceLogMapper;
+import com.usky.data.service.DataInterfaceLogService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 数据中心_接口调用日志表 服务实现类
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+@Service
+public class DataInterfaceLogServiceImpl extends AbstractCrudService<DataInterfaceLogMapper, DataInterfaceLog> implements DataInterfaceLogService {
+
+}

+ 291 - 0
service-data/service-data-biz/src/main/java/com/usky/data/service/impl/DataInterfaceServiceImpl.java

@@ -0,0 +1,291 @@
+package com.usky.data.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.data.domain.DataInterface;
+import com.usky.data.mapper.DataInterfaceMapper;
+import com.usky.data.service.DataInterfaceService;
+import com.usky.data.service.util.InterfaceExecutor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ * 数据中心_数据接口表 服务实现类
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-24
+ */
+@Service
+@Slf4j
+public class DataInterfaceServiceImpl extends AbstractCrudService<DataInterfaceMapper, DataInterface> implements DataInterfaceService {
+
+    @Override
+    public CommonPage<DataInterface> getDataInterfaceList(Integer currentPage, Integer pageSize, String keyword, String type, Integer enabledMark, String category) {
+
+        // if (StringUtils.isBlank(category)) {
+        //     throw new BusinessException("接口分类不能为空!");
+        // }
+
+        IPage<DataInterface> page = new Page<>(currentPage, pageSize);
+        LambdaQueryWrapper<DataInterface> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(StringUtils.isNotBlank(category), DataInterface::getCategory, category)
+                .eq(DataInterface::getTenantId, SecurityUtils.getTenantId())
+                .eq(DataInterface::getIsDelete, 0)
+                .orderByDesc(DataInterface::getSortCode);
+        queryWrapper.eq(StringUtils.isNotBlank(type), DataInterface::getType, type)
+                .eq(enabledMark != null, DataInterface::getEnabledMark, enabledMark);
+        if (StringUtils.isNotBlank(keyword)) {
+            queryWrapper.like(DataInterface::getFullName, keyword)
+                    .or()
+                    .like(DataInterface::getEnCode, keyword);
+        }
+        page = baseMapper.selectPage(page, queryWrapper);
+        return new CommonPage<>(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize());
+    }
+
+    @Override
+    public CommonPage<DataInterface> getWorkflowList(Integer currentPage, Integer pageSize, String keyword, String type, Integer enabledMark) {
+        IPage<DataInterface> page = new Page<>(currentPage, pageSize);
+        LambdaQueryWrapper<DataInterface> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(DataInterface::getTenantId, SecurityUtils.getTenantId())
+                .like(StringUtils.isNotBlank(keyword), DataInterface::getFullName, keyword)
+                .like(StringUtils.isNotBlank(keyword), DataInterface::getEnCode, keyword)
+                .eq(StringUtils.isNotBlank(type), DataInterface::getType, type)
+                .eq(enabledMark != null, DataInterface::getEnabledMark, enabledMark)
+                .eq(DataInterface::getEnabledMark, 1); // 工作流选择时只显示启用的接口
+        page = baseMapper.selectPage(page, queryWrapper);
+        return new CommonPage<>(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize());
+    }
+
+    @Override
+    public List<DataInterface> getSelectorList() {
+        LambdaQueryWrapper<DataInterface> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(DataInterface::getTenantId, SecurityUtils.getTenantId())
+                .eq(DataInterface::getEnabledMark, 1); // 只返回启用的接口
+        return baseMapper.selectList(queryWrapper);
+    }
+
+    @Override
+    public String getInterfaceParameters(String id) {
+        DataInterface entity = baseMapper.selectById(id);
+        return entity != null ? entity.getParameterJson() : "[]";
+    }
+
+    @Override
+    public boolean updateInterfaceStatus(String id) {
+        DataInterface entity = baseMapper.selectById(id);
+        if (entity == null) {
+            return false;
+        }
+        // 切换启用状态
+        entity.setEnabledMark(entity.getEnabledMark() == 1 ? 0 : 1);
+        return baseMapper.updateById(entity) > 0;
+    }
+
+    @Override
+    public ApiResult<?> testInterface(String id, Map<String, Object> params) {
+        // 根据ID获取接口配置
+        DataInterface interfaceEntity = baseMapper.selectById(id);
+        if (interfaceEntity == null) {
+            return ApiResult.error("接口不存在");
+        }
+
+        try {
+            // 执行接口逻辑
+            Object result = InterfaceExecutor.executeInterface(interfaceEntity, params);
+            return ApiResult.success(result);
+        } catch (Exception e) {
+            return ApiResult.error("接口测试失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public ApiResult<?> getInterfaceResponse(String id, Map<String, String> params) {
+        // 这里是访问接口GET的实现,实际逻辑需要根据接口类型和配置进行处理
+        DataInterface interfaceEntity = baseMapper.selectById(id);
+        if (interfaceEntity == null) {
+            return ApiResult.error("接口不存在");
+        }
+
+        try {
+            // 将String类型的Map转换为Object类型的Map
+            Map<String, Object> objectParams = new HashMap<>();
+            if (params != null) {
+                params.forEach((k, v) -> objectParams.put(k, v));
+            }
+
+            // 执行接口逻辑
+            Object result = InterfaceExecutor.executeInterface(interfaceEntity, objectParams);
+            return ApiResult.success(result);
+        } catch (Exception e) {
+            return ApiResult.error("接口访问失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public ApiResult<?> postInterfaceResponse(String id, Map<String, String> params) {
+        // 这里是访问接口POST的实现,实际逻辑需要根据接口类型和配置进行处理
+        DataInterface interfaceEntity = baseMapper.selectById(id);
+        if (interfaceEntity == null) {
+            return ApiResult.error("接口不存在");
+        }
+
+        try {
+            // 将String类型的Map转换为Object类型的Map
+            Map<String, Object> objectParams = new HashMap<>();
+            if (params != null) {
+                params.forEach((k, v) -> objectParams.put(k, v));
+            }
+
+            // 执行接口逻辑
+            Object result = InterfaceExecutor.executeInterface(interfaceEntity, objectParams);
+            return ApiResult.success(result);
+        } catch (Exception e) {
+            return ApiResult.error("接口访问失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public ApiResult<?> getInterfacePageData(String id, Map<String, Object> pageParams) {
+        // 这里是获取接口分页数据的实现,实际逻辑需要根据接口类型和配置进行处理
+        DataInterface interfaceEntity = baseMapper.selectById(id);
+        if (interfaceEntity == null) {
+            return ApiResult.error("接口不存在");
+        }
+
+        try {
+            // 获取接口分页数据
+            Map<String, Object> result = InterfaceExecutor.getInterfacePageData(interfaceEntity, pageParams);
+            return ApiResult.success(result);
+        } catch (Exception e) {
+            return ApiResult.error("获取接口分页数据失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public ApiResult<List<Map<String, Object>>> getInterfaceDetailData(String id, Map<String, Object> pageParams) {
+        // 这里是获取接口详情数据的实现,实际逻辑需要根据接口类型和配置进行处理
+        DataInterface interfaceEntity = baseMapper.selectById(id);
+        if (interfaceEntity == null) {
+            return ApiResult.error("接口不存在");
+        }
+
+        try {
+            // 执行接口逻辑获取数据
+            Object result = InterfaceExecutor.executeInterface(interfaceEntity, pageParams);
+
+            // 转换结果为List<Map<String, Object>>类型
+            List<Map<String, Object>> resultList = new ArrayList<>();
+            if (result instanceof List) {
+                List<?> listResult = (List<?>) result;
+                for (Object item : listResult) {
+                    if (item instanceof Map) {
+                        @SuppressWarnings("unchecked")
+                        Map<String, Object> mapItem = (Map<String, Object>) item;
+                        resultList.add(mapItem);
+                    }
+                }
+            }
+
+            return ApiResult.success(resultList);
+        } catch (Exception e) {
+            return ApiResult.error("获取接口详情数据失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public ApiResult<List<String>> getInterfaceFields(String id, Map<String, Object> pageParams) {
+        // 这里是获取接口字段的实现,实际逻辑需要根据接口类型和配置进行处理
+        DataInterface interfaceEntity = baseMapper.selectById(id);
+        if (interfaceEntity == null) {
+            return ApiResult.error("接口不存在");
+        }
+
+        try {
+            // 获取接口字段
+            List<String> fields = InterfaceExecutor.getInterfaceFields(interfaceEntity, pageParams);
+            return ApiResult.success(fields);
+        } catch (Exception e) {
+            return ApiResult.error("获取接口字段失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    @Transactional
+    public Boolean deleteInterface(Long id) {
+        LambdaQueryWrapper<DataInterface> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(DataInterface::getId, id);
+        DataInterface entity = baseMapper.selectOne(queryWrapper);
+        String username = SecurityUtils.getUsername();
+
+        if (entity == null) {
+            log.warn("尝试删除不存在的接口,ID: {},操作人{}", id, username);
+            throw new BusinessException("接口不存在!");
+        }
+
+        entity.setIsDelete(1);
+        entity.setDeleteTime(LocalDateTime.now());
+        entity.setDeleteBy(username);
+
+        return baseMapper.update(entity, queryWrapper) > 0;
+    }
+
+
+    @Override
+    @Transactional
+    public Boolean addInterface(DataInterface dataInterface) {
+        Integer tenantId = SecurityUtils.getTenantId();
+        if (!selectNameAndCode(dataInterface.getFullName(), dataInterface.getEnCode(), tenantId).isEmpty()) {
+            throw new BusinessException("接口名称或编码已存在");
+        }
+
+        dataInterface.setCreateBy(SecurityUtils.getUsername());
+        dataInterface.setCreateTime(LocalDateTime.now());
+        dataInterface.setIsDelete(0);
+        dataInterface.setEnabledMark(1);
+        dataInterface.setTenantId(tenantId);
+        return baseMapper.insert(dataInterface) > 0;
+    }
+
+    private List<DataInterface> selectNameAndCode(String name, String code, Integer tenantId) {
+        LambdaQueryWrapper<DataInterface> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(DataInterface::getTenantId, tenantId)
+                .and(w -> w.eq(DataInterface::getFullName, name)
+                        .or()
+                        .eq(DataInterface::getEnCode, code));
+        return baseMapper.selectList(queryWrapper);
+    }
+
+
+    @Override
+    public Boolean updateInterface(DataInterface dataInterface) {
+
+        if (dataInterface == null || dataInterface.getId() <= 0) {
+            throw new BusinessException("接口id为空或不合规");
+        }
+
+        if (!selectNameAndCode(dataInterface.getFullName(), dataInterface.getEnCode(), dataInterface.getTenantId()).isEmpty()) {
+            throw new BusinessException("接口名称或编码已存在");
+        }
+
+        dataInterface.setUpdateBy(SecurityUtils.getUsername());
+        dataInterface.setUpdateTime(LocalDateTime.now());
+        return baseMapper.updateById(dataInterface) > 0;
+    }
+}

+ 120 - 0
service-data/service-data-biz/src/main/java/com/usky/data/service/impl/DataLinkServiceImpl.java

@@ -0,0 +1,120 @@
+package com.usky.data.service.impl;
+
+import com.usky.data.domain.DataLink;
+import com.usky.data.mapper.DataLinkMapper;
+import com.usky.data.service.DataLinkService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.util.List;
+
+/**
+ * <p>
+ * 数据中心_数据连接表 服务实现类
+ * </p>
+ *
+ * @author fyc
+ * @since 2026-01-23
+ */
+@Service
+public class DataLinkServiceImpl extends AbstractCrudService<DataLinkMapper, DataLink> implements DataLinkService {
+
+    @Autowired
+    private DataLinkMapper dataLinkMapper;
+
+    @Override
+    public boolean testDbConnection(DataLink dataLink) {
+        Connection connection = null;
+        try {
+            // 根据数据库类型加载驱动
+            String driverClass = getDriverClass(dataLink.getDbType());
+            Class.forName(driverClass);
+
+            // 构建连接URL
+            String url = buildJdbcUrl(dataLink);
+
+            // 建立连接
+            connection = DriverManager.getConnection(url, dataLink.getDbName(), dataLink.getPassword());
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        } finally {
+            if (connection != null) {
+                try {
+                    connection.close();
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean isExistByFullName(String fullName, Integer id) {
+        int count = dataLinkMapper.countByFullName(fullName, id);
+        return count > 0;
+    }
+
+    @Override
+    public List<DataLink> getList() {
+        return dataLinkMapper.selectList(null);
+    }
+
+    @Override
+    public List<DataLink> getList(int page, int pageSize, String keyword) {
+        // 计算起始索引
+        int startIndex = (page - 1) * pageSize;
+        return dataLinkMapper.selectPageList(startIndex, pageSize, keyword);
+    }
+
+    /**
+     * 根据数据库类型获取驱动类名
+     */
+    private String getDriverClass(String dbType) {
+        switch (dbType.toLowerCase()) {
+            case "mysql":
+                return "com.mysql.cj.jdbc.Driver";
+            case "oracle":
+                return "oracle.jdbc.OracleDriver";
+            case "sqlserver":
+                return "com.microsoft.sqlserver.jdbc.SQLServerDriver";
+            case "postgresql":
+                return "org.postgresql.Driver";
+            default:
+                throw new IllegalArgumentException("不支持的数据库类型: " + dbType);
+        }
+    }
+
+    /**
+     * 构建JDBC连接URL
+     */
+    private String buildJdbcUrl(DataLink dataLink) {
+        String dbType = dataLink.getDbType().toLowerCase();
+        String host = dataLink.getHost();
+        Integer port = dataLink.getPort();
+        String dbName = dataLink.getDbName();
+
+        switch (dbType) {
+            case "mysql":
+                return String.format("jdbc:mysql://%s:%d/%s?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%%2B8",
+                        host, port, dbName);
+            case "oracle":
+                // Oracle支持SID和Service Name两种方式
+                if (dataLink.getServiceName() != null && !dataLink.getServiceName().isEmpty()) {
+                    return String.format("jdbc:oracle:thin:@//%s:%d/%s", host, port, dataLink.getServiceName());
+                } else {
+                    return String.format("jdbc:oracle:thin:@%s:%d:%s", host, port, dbName);
+                }
+            case "sqlserver":
+                return String.format("jdbc:sqlserver://%s:%d;databaseName=%s;encrypt=false", host, port, dbName);
+            case "postgresql":
+                return String.format("jdbc:postgresql://%s:%d/%s", host, port, dbName);
+            default:
+                throw new IllegalArgumentException("不支持的数据库类型: " + dbType);
+        }
+    }
+}

+ 421 - 0
service-data/service-data-biz/src/main/java/com/usky/data/service/util/InterfaceExecutor.java

@@ -0,0 +1,421 @@
+package com.usky.data.service.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.usky.data.domain.DataInterface;
+import org.apache.commons.lang3.StringUtils;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 接口执行器,用于处理不同类型接口的执行逻辑
+ * <p>
+ * 支持三种接口类型:
+ * 1. SQL操作(type=1)
+ * 2. 静态数据(type=2)
+ * 3. API操作(type=3)
+ * </p>
+ */
+public class InterfaceExecutor {
+
+    /**
+     * 执行接口逻辑,参考原始infoToId方法
+     *
+     * @param dataInterface 接口实体
+     * @param params        请求参数
+     * @return 执行结果
+     */
+    public static Object executeInterface(DataInterface dataInterface, Map<String, Object> params) {
+        if (dataInterface == null) {
+            throw new IllegalArgumentException("接口配置不能为空");
+        }
+
+        // 转换params为Map<String, String>用于参数处理
+        Map<String, String> stringParams = new HashMap<>();
+        if (params != null) {
+            params.forEach((k, v) -> stringParams.put(k, v != null ? v.toString() : null));
+        }
+
+        // 替换默认值
+        replaceDefaultValue(dataInterface.getParameterJson(), stringParams);
+
+        // 验证参数必填或类型
+        String checkResult = checkRequestParams(dataInterface.getParameterJson(), stringParams);
+        if (StringUtils.isNotEmpty(checkResult)) {
+            throw new IllegalArgumentException(checkResult);
+        }
+
+        Object result = null;
+        try {
+            // 根据接口类型执行不同逻辑
+            Integer type = dataInterface.getType();
+            switch (type) {
+                case 1:
+                    // 静态数据
+                    result = callStaticData(dataInterface.getDataConfigJson());
+                    break;
+                case 3:
+                    // API调用
+                    result = callApi(dataInterface.getDataConfigJson(), stringParams);
+                    break;
+                case 2:
+                    // SQL操作
+                    result = executeSql(dataInterface, stringParams);
+                    break;
+                default:
+                    throw new IllegalArgumentException("不支持的接口类型: " + type);
+            }
+
+            // 处理结果,执行JS脚本
+            if (StringUtils.isNotEmpty(dataInterface.getDataJsJson())) {
+                // 这里模拟JS脚本执行,实际项目中需要引入JS引擎
+                result = processResult(result, dataInterface.getDataJsJson());
+            }
+
+            return result;
+        } catch (Exception e) {
+            throw new RuntimeException("接口执行失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 替换默认值
+     *
+     * @param parameterJson 参数配置JSON
+     * @param params        请求参数
+     */
+    private static void replaceDefaultValue(String parameterJson, Map<String, String> params) {
+        if (StringUtils.isEmpty(parameterJson)) {
+            return;
+        }
+
+        JSONArray paramArray = JSON.parseArray(parameterJson);
+        if (paramArray == null || paramArray.isEmpty()) {
+            return;
+        }
+
+        for (Object obj : paramArray) {
+            JSONObject paramObj = (JSONObject) obj;
+            String field = paramObj.getString("field");
+            String defaultValue = paramObj.getString("defaultValue");
+
+            // 如果参数中没有该字段或值为空,则设置默认值
+            if (!params.containsKey(field) || StringUtils.isEmpty(params.get(field))) {
+                params.put(field, defaultValue == null ? "" : defaultValue);
+            }
+        }
+    }
+
+    /**
+     * 检查请求参数
+     *
+     * @param parameterJson 参数配置JSON
+     * @param params        请求参数
+     * @return 检查结果,空字符串表示通过
+     */
+    private static String checkRequestParams(String parameterJson, Map<String, String> params) {
+        if (StringUtils.isEmpty(parameterJson)) {
+            return "";
+        }
+
+        JSONArray paramArray = JSON.parseArray(parameterJson);
+        if (paramArray == null || paramArray.isEmpty()) {
+            return "";
+        }
+
+        StringBuilder message = new StringBuilder();
+        for (Object obj : paramArray) {
+            JSONObject paramObj = (JSONObject) obj;
+
+            // 验证必填字段
+            Integer required = paramObj.getInteger("required");
+            if (required != null && required == 1) {
+                String field = paramObj.getString("field");
+                String value = params.get(field);
+                if (StringUtils.isEmpty(value)) {
+                    message.append(field).append("不能为空;");
+                }
+            }
+
+            // 验证数据类型
+            String dataType = paramObj.getString("dataType");
+            if (StringUtils.isNotEmpty(dataType)) {
+                String field = paramObj.getString("field");
+                String value = params.get(field);
+                if (StringUtils.isEmpty(value)) {
+                    continue;
+                }
+                try {
+                    if ("int".equals(dataType)) {
+                        Integer.parseInt(value);
+                    } else if ("decimal".equals(dataType)) {
+                        Double.parseDouble(value);
+                    } else if ("datetime".equals(dataType)) {
+                        try {
+                            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+                            formatter.parse(value);
+                        } catch (Exception e) {
+                            // 尝试解析时间戳
+                            Long.parseLong(value);
+                        }
+                    }
+                } catch (Exception e) {
+                    message.append(field).append("类型必须为").append(dataType).append(";");
+                }
+            }
+        }
+
+        return message.length() > 0 ? message.substring(0, message.length() - 1) : "";
+    }
+
+    /**
+     * 执行静态数据类型接口
+     *
+     * @param dataConfigJson 数据配置JSON
+     * @return 静态数据结果
+     */
+    private static Object callStaticData(String dataConfigJson) {
+        JSONObject configJson = JSON.parseObject(dataConfigJson);
+        String staticData = configJson.getString("staticData");
+
+        Object obj;
+        try {
+            Object parse = JSON.parse(staticData);
+            if (parse instanceof JSONArray) {
+                obj = JSON.parseArray(staticData, Map.class);
+            } else {
+                obj = JSON.parseObject(staticData, Map.class);
+            }
+        } catch (Exception e) {
+            obj = staticData;
+        }
+
+        if (obj == null) {
+            return new ArrayList<>();
+        }
+
+        return obj;
+    }
+
+    /**
+     * 执行API类型接口
+     *
+     * @param dataConfigJson 数据配置JSON
+     * @param params        请求参数
+     * @return API调用结果
+     */
+    private static Object callApi(String dataConfigJson, Map<String, String> params) {
+        JSONObject configJson = JSON.parseObject(dataConfigJson);
+        JSONObject apiData = configJson.getJSONObject("apiData");
+
+        if (apiData == null) {
+            throw new IllegalArgumentException("API配置不能为空");
+        }
+
+        String url = apiData.getString("url");
+        String method = apiData.getString("method");
+        JSONObject headers = apiData.getJSONObject("headers");
+        JSONObject apiParams = apiData.getJSONObject("params");
+
+        // 合并参数
+        if (apiParams == null) {
+            apiParams = new JSONObject();
+        }
+        if (params != null) {
+            JSONObject finalApiParams = apiParams;
+            params.forEach((k, v) -> {
+                if (StringUtils.isNotEmpty(v)) {
+                    finalApiParams.put(k, v);
+                }
+            });
+        }
+
+        // 模拟API调用,实际项目中需要使用HTTP客户端
+        Map<String, Object> result = new HashMap<>();
+        result.put("success", true);
+        result.put("message", "API调用成功");
+        result.put("data", apiParams);
+        result.put("url", url);
+        result.put("method", method);
+
+        return result;
+    }
+
+    /**
+     * 执行SQL类型接口
+     *
+     * @param dataInterface 接口实体
+     * @param params        请求参数
+     * @return SQL执行结果
+     */
+    private static Object executeSql(DataInterface dataInterface, Map<String, String> params) {
+        JSONObject configJson = JSON.parseObject(dataInterface.getDataConfigJson());
+        JSONObject sqlData = configJson.getJSONObject("sqlData");
+
+        if (sqlData == null) {
+            throw new IllegalArgumentException("SQL配置不能为空");
+        }
+
+        String sql = sqlData.getString("sql");
+        if (StringUtils.isEmpty(sql)) {
+            throw new IllegalArgumentException("SQL语句不能为空");
+        }
+
+        // 替换SQL中的参数占位符
+        sql = replaceSqlParams(sql, params);
+
+        // 模拟SQL执行结果
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        for (int i = 0; i < 5; i++) {
+            Map<String, Object> map = new HashMap<>();
+            map.put("id", i + 1);
+            map.put("name", "测试数据" + (i + 1));
+            map.put("value", i * 100);
+            map.put("created_at", "2026-01-24 10:00:00");
+            resultList.add(map);
+        }
+
+        // 如果是分页查询,处理分页信息
+        if (dataInterface.getHasPage() != null && dataInterface.getHasPage() == 1) {
+            Map<String, Object> pageResult = new HashMap<>();
+            pageResult.put("list", resultList);
+            pageResult.put("total", 100);
+            pageResult.put("current", params.getOrDefault("currentPage", "1"));
+            pageResult.put("pageSize", params.getOrDefault("pageSize", "20"));
+            return pageResult;
+        }
+
+        return resultList;
+    }
+
+    /**
+     * 替换SQL中的参数占位符
+     *
+     * @param sql    SQL语句
+     * @param params 请求参数
+     * @return 替换后的SQL语句
+     */
+    private static String replaceSqlParams(String sql, Map<String, String> params) {
+        if (StringUtils.isEmpty(sql) || params == null || params.isEmpty()) {
+            return sql;
+        }
+
+        // 匹配 {参数名} 格式的占位符
+        Pattern pattern = Pattern.compile("\\{(\\w+)\\}");
+        Matcher matcher = pattern.matcher(sql);
+
+        StringBuffer sb = new StringBuffer();
+        while (matcher.find()) {
+            String paramName = matcher.group(1);
+            String paramValue = params.getOrDefault(paramName, "");
+            matcher.appendReplacement(sb, paramValue);
+        }
+        matcher.appendTail(sb);
+
+        return sb.toString();
+    }
+
+    /**
+     * 处理结果,执行JS脚本
+     *
+     * @param result    原始结果
+     * @param jsScript  JS脚本
+     * @return 处理后的结果
+     */
+    private static Object processResult(Object result, String jsScript) {
+        // 这里模拟JS脚本执行,实际项目中需要引入JS引擎
+        // 例如使用javax.script.ScriptEngine
+        return result;
+    }
+
+    /**
+     * 判断是否为内部接口返回格式
+     *
+     * @param jsonObject JSON对象
+     * @return 是否为内部接口格式
+     */
+    private static boolean isInternal(JSONObject jsonObject) {
+        if (jsonObject != null) {
+            return jsonObject.size() == 3 &&
+                    jsonObject.containsKey("code") &&
+                    jsonObject.containsKey("msg") &&
+                    jsonObject.containsKey("data");
+        }
+        return false;
+    }
+
+    /**
+     * 获取接口字段信息
+     *
+     * @param dataInterface 接口实体
+     * @param params        请求参数
+     * @return 接口字段列表
+     */
+    public static List<String> getInterfaceFields(DataInterface dataInterface, Map<String, Object> params) {
+        // 执行接口获取数据
+        Object result = executeInterface(dataInterface, params);
+
+        // 解析结果获取字段列表
+        List<String> fields = new ArrayList<>();
+        if (result instanceof List) {
+            List<?> resultList = (List<?>) result;
+            if (!resultList.isEmpty() && resultList.get(0) instanceof Map) {
+                Map<?, ?> firstItem = (Map<?, ?>) resultList.get(0);
+                for (Object key : firstItem.keySet()) {
+                    fields.add(key.toString());
+                }
+            }
+        } else if (result instanceof Map) {
+            Map<?, ?> resultMap = (Map<?, ?>) result;
+            // 如果是分页结果,获取list中的字段
+            if (resultMap.containsKey("list") && resultMap.get("list") instanceof List) {
+                List<?> list = (List<?>) resultMap.get("list");
+                if (!list.isEmpty() && list.get(0) instanceof Map) {
+                    Map<?, ?> firstItem = (Map<?, ?>) list.get(0);
+                    for (Object key : firstItem.keySet()) {
+                        fields.add(key.toString());
+                    }
+                }
+            } else {
+                // 直接获取map中的字段
+                for (Object key : resultMap.keySet()) {
+                    fields.add(key.toString());
+                }
+            }
+        }
+
+        return fields;
+    }
+
+    /**
+     * 获取接口分页数据
+     *
+     * @param dataInterface 接口实体
+     * @param params        请求参数
+     * @return 分页数据结果
+     */
+    public static Map<String, Object> getInterfacePageData(DataInterface dataInterface, Map<String, Object> params) {
+        // 执行接口获取数据
+        Object result = executeInterface(dataInterface, params);
+
+        // 处理分页
+        Map<String, Object> pageResult = new HashMap<>();
+
+        if (result instanceof Map && ((Map<?, ?>) result).containsKey("list")) {
+            // 如果已经是分页格式,直接返回
+            pageResult.putAll((Map<? extends String, ?>) result);
+        } else {
+            // 否则包装成分页格式
+            pageResult.put("list", result);
+            pageResult.put("total", result instanceof List ? ((List<?>) result).size() : 1);
+            pageResult.put("current", params != null ? params.getOrDefault("currentPage", 1) : 1);
+            pageResult.put("pageSize", params != null ? params.getOrDefault("pageSize", 20) : 20);
+        }
+
+        return pageResult;
+    }
+}

+ 49 - 0
service-data/service-data-biz/src/main/java/com/usky/data/vo/DbLinkInfoVO.java

@@ -0,0 +1,49 @@
+package com.usky.data.vo;
+
+import com.usky.data.domain.DataLink;
+import lombok.Data;
+
+/**
+ * 数据源详细信息VO
+ */
+@Data
+public class DbLinkInfoVO {
+    private Integer id;
+    private String fullName;
+    private String dbType;
+    private String host;
+    private Integer port;
+    private String dbName;
+    private String serviceName;
+    private String description;
+    private String dbSchema;
+    private String tableSpace;
+    private String oracleParam;
+    private Integer oracleExtend;
+    private Integer sortCode;
+    private String createBy;
+    private String updateBy;
+
+    /**
+     * 转换为VO对象
+     */
+    public DbLinkInfoVO getDbLinkInfoVO(DataLink dataLink) {
+        DbLinkInfoVO vo = new DbLinkInfoVO();
+        vo.setId(dataLink.getId());
+        vo.setFullName(dataLink.getFullName());
+        vo.setDbType(dataLink.getDbType());
+        vo.setHost(dataLink.getHost());
+        vo.setPort(dataLink.getPort());
+        vo.setDbName(dataLink.getDbName());
+        vo.setServiceName(dataLink.getServiceName());
+        vo.setDescription(dataLink.getDescription());
+        vo.setDbSchema(dataLink.getDbSchema());
+        vo.setTableSpace(dataLink.getTableSpace());
+        vo.setOracleParam(dataLink.getOracleParam());
+        vo.setOracleExtend(dataLink.getOracleExtend());
+        vo.setSortCode(dataLink.getSortCode());
+        vo.setCreateBy(dataLink.getCreateBy());
+        vo.setUpdateBy(dataLink.getUpdateBy());
+        return vo;
+    }
+}

+ 22 - 0
service-data/service-data-biz/src/main/java/com/usky/data/vo/DbLinkListVO.java

@@ -0,0 +1,22 @@
+package com.usky.data.vo;
+
+import lombok.Data;
+
+/**
+ * 数据源列表VO
+ */
+@Data
+public class DbLinkListVO {
+    private Integer id;
+    private String fullName;
+    private String dbType;
+    private String host;
+    private Integer port;
+    private String dbName;
+    private String serviceName;
+    private String description;
+    private String dbSchema;
+    private String tableSpace;
+    private String createBy;
+    private String updateBy;
+}

+ 14 - 0
service-data/service-data-biz/src/main/java/com/usky/data/vo/DbLinkSelectorListVO.java

@@ -0,0 +1,14 @@
+package com.usky.data.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 数据源选择器列表VO
+ */
+@Data
+public class DbLinkSelectorListVO {
+    private String fullName;
+    private List<DbLinkListVO> children;
+}

+ 17 - 0
service-data/service-data-biz/src/main/java/com/usky/data/vo/ListVO.java

@@ -0,0 +1,17 @@
+package com.usky.data.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 通用列表包装类
+ */
+@Data
+public class ListVO<T> {
+    private List<T> list;
+
+    public ListVO(List<T> list) {
+        this.list = list;
+    }
+}

+ 19 - 0
service-data/service-data-biz/src/main/java/com/usky/data/vo/PageListVO.java

@@ -0,0 +1,19 @@
+package com.usky.data.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 分页列表包装类
+ */
+@Data
+public class PageListVO<T> {
+    private List<T> list;
+    private PaginationVO pagination;
+
+    public PageListVO(List<T> list, PaginationVO pagination) {
+        this.list = list;
+        this.pagination = pagination;
+    }
+}

+ 14 - 0
service-data/service-data-biz/src/main/java/com/usky/data/vo/PaginationVO.java

@@ -0,0 +1,14 @@
+package com.usky.data.vo;
+
+import lombok.Data;
+
+/**
+ * 分页信息包装类
+ */
+@Data
+public class PaginationVO {
+    private int currentPage;
+    private int pageSize;
+    private int totalRows;
+    private int totalPages;
+}

+ 25 - 0
service-data/service-data-biz/src/main/resources/bootstrap.yml

@@ -0,0 +1,25 @@
+# Tomcat
+server:
+  port: 9902
+
+# Spring
+spring: 
+  application:
+    # 应用名称
+    name: service-data
+  profiles:
+    # 环境配置
+    active: dev
+  cloud:
+    nacos:
+      discovery:
+        # 服务注册地址
+        server-addr: usky-cloud-nacos:8848
+      config:
+        # 配置中心地址
+        server-addr: usky-cloud-nacos:8848
+        # 配置文件格式
+        file-extension: yml
+        # 共享配置
+        shared-configs:
+          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

+ 108 - 0
service-data/service-data-biz/src/main/resources/doc/index.adoc

@@ -0,0 +1,108 @@
+= 安防项目
+
+[width="100%",options="header"]
+[stripes=even]
+|====================
+|Version |  Update Time  | Status | Author |  Description
+|v2022-04-21 16:57:08|2022-04-21 16:57:08|auto|@yq|Created by smart-doc
+|====================
+
+
+== &lt;p&gt;参数配置表 前端控制器&lt;/p&gt;
+== &lt;p&gt;部门信息&lt;/p&gt;
+=== 查看部门信息
+*URL:* http:10.23.39.1:8082/sysDept/list
+
+*Type:* POST
+
+*Author:* ya
+
+*Content-Type:* application/json; charset=utf-8
+
+
+
+
+*Body-parameters:*
+
+[width="100%",options="header"]
+[stripes=even]
+|====================
+|Parameter | Type|Description|Required|Since
+|deptId|int64|部门id|false|-
+|parentId|int64|父部门id|false|-
+|ancestors|string|祖级列表|false|-
+|deptName|string|部门名称|false|-
+|orderNum|int32|显示顺序|false|-
+|leader|string|负责人|false|-
+|phone|string|联系电话|false|-
+|email|string|邮箱|false|-
+|status|string|部门状态(0正常 1停用)|false|-
+|delFlag|string|删除标志(0代表存在 2代表删除)|false|-
+|createBy|string|创建者|false|-
+|createTime|string|创建时间|false|-
+|updateBy|string|更新者|false|-
+|updateTime|string|更新时间|false|-
+|bId|int64|建筑id|false|-
+|====================
+
+*Response-fields:*
+
+[width="100%",options="header"]
+[stripes=even]
+|====================
+|Field | Type|Description|Since
+|status|object|No comments found.|-
+|code|string|No comments found.|-
+|msg|string|No comments found.|-
+|data|object|No comments found.|-
+|└─deptId|int64|部门id|-
+|└─parentId|int64|父部门id|-
+|└─ancestors|string|祖级列表|-
+|└─deptName|string|部门名称|-
+|└─orderNum|int32|显示顺序|-
+|└─leader|string|负责人|-
+|└─phone|string|联系电话|-
+|└─email|string|邮箱|-
+|└─status|string|部门状态(0正常 1停用)|-
+|└─delFlag|string|删除标志(0代表存在 2代表删除)|-
+|└─createBy|string|创建者|-
+|└─createTime|string|创建时间|-
+|└─updateBy|string|更新者|-
+|└─updateTime|string|更新时间|-
+|└─bId|int64|建筑id|-
+|exception|string|No comments found.|-
+|====================
+
+*Response-example:*
+----
+{
+	"status": {
+		
+	},
+	"code": "97564",
+	"msg": "wnr5qt",
+	"data": [
+		{
+			"deptId": 540,
+			"parentId": 858,
+			"ancestors": "o5lg60",
+			"deptName": "文.沈",
+			"orderNum": 260,
+			"leader": "ufz93p",
+			"phone": "17852835049",
+			"email": "智渊.徐@yahoo.com",
+			"status": "nu6cnp",
+			"delFlag": "72oiji",
+			"createBy": "5fxr6j",
+			"createTime": "2022-04-21 16:57:10",
+			"updateBy": "4kcs4e",
+			"updateTime": "2022-04-21 16:57:10",
+			"bId": 977
+		}
+	],
+	"exception": "53u6bg"
+}
+----
+
+== &lt;p&gt;用户信息表 前端控制器&lt;/p&gt;
+

+ 74 - 0
service-data/service-data-biz/src/main/resources/logback.xml

@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="/var/log/uskycloud/usky-data" />
+   <!-- 日志输出格式 -->
+	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+
+    <!-- 控制台输出 -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+	</appender>
+
+    <!-- 系统日志输出 -->
+	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+			<fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+	</appender>
+
+    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>60</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+			<!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+			<!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 系统模块日志级别控制  -->
+	<logger name="com.usky" level="info" />
+	<!-- Spring日志级别控制  -->
+	<logger name="org.springframework" level="warn" />
+
+	<root level="info">
+		<appender-ref ref="console" />
+	</root>
+	
+	<!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="file_info" />
+        <appender-ref ref="file_error" />
+    </root>
+</configuration>

+ 28 - 0
service-data/service-data-biz/src/main/resources/mapper/data/DataInterfaceLogMapper.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.usky.data.mapper.DataInterfaceLogMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.usky.data.domain.DataInterfaceLog">
+        <id column="id" property="id" />
+        <result column="interface_id" property="interfaceId" />
+        <result column="execute_time" property="executeTime" />
+        <result column="user_id" property="userId" />
+        <result column="request_ip" property="requestIp" />
+        <result column="request_device" property="requestDevice" />
+        <result column="request_type" property="requestType" />
+        <result column="request_waste_time" property="requestWasteTime" />
+        <result column="oauth_app_id" property="oauthAppId" />
+        <result column="sort_code" property="sortCode" />
+        <result column="execute_result" property="executeResult" />
+        <result column="execute_state" property="executeState" />
+        <result column="execute_msg" property="executeMsg" />
+        <result column="create_by" property="createBy" />
+        <result column="create_time" property="createTime" />
+        <result column="update_by" property="updateBy" />
+        <result column="update_time" property="updateTime" />
+        <result column="dept_id" property="deptId" />
+        <result column="tenant_id" property="tenantId" />
+    </resultMap>
+
+</mapper>

+ 35 - 0
service-data/service-data-biz/src/main/resources/mapper/data/DataInterfaceMapper.xml

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.usky.data.mapper.DataInterfaceMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.usky.data.domain.DataInterface">
+        <id column="id" property="id"/>
+        <result column="full_name" property="fullName"/>
+        <result column="en_code" property="enCode"/>
+        <result column="category" property="category"/>
+        <result column="type" property="type"/>
+        <result column="action" property="action"/>
+        <result column="has_page" property="hasPage"/>
+        <result column="is_postposition" property="isPostposition"/>
+        <result column="config_json" property="dataConfigJson"/>
+        <result column="count_json" property="dataCountJson"/>
+        <result column="echo_json" property="dataEchoJson"/>
+        <result column="js_json" property="dataJsJson"/>
+        <result column="field_json" property="fieldJson"/>
+        <result column="parameter_json" property="parameterJson"/>
+        <result column="exception_json" property="exceptionJson"/>
+        <result column="description" property="description"/>
+        <result column="enabled_mark" property="enabledMark"/>
+        <result column="sort_code" property="sortCode"/>
+        <result column="create_by" property="createBy"/>
+        <result column="create_time" property="createTime"/>
+        <result column="update_by" property="updateBy"/>
+        <result column="update_time" property="updateTime"/>
+        <result column="delete_time" property="deleteTime"/>
+        <result column="delete_by" property="deleteBy"/>
+        <result column="is_delete" property="isDelete"/>
+        <result column="tenant_id" property="tenantId"/>
+    </resultMap>
+
+</mapper>

+ 56 - 0
service-data/service-data-biz/src/main/resources/mapper/data/DataLinkMapper.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.usky.data.mapper.DataLinkMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.usky.data.domain.DataLink">
+        <id column="id" property="id" />
+        <result column="full_name" property="fullName" />
+        <result column="db_type" property="dbType" />
+        <result column="host" property="host" />
+        <result column="port" property="port" />
+        <result column="db_name" property="dbName" />
+        <result column="password" property="password" />
+        <result column="service_name" property="serviceName" />
+        <result column="description" property="description" />
+        <result column="db_schema" property="dbSchema" />
+        <result column="table_space" property="tableSpace" />
+        <result column="oracle_param" property="oracleParam" />
+        <result column="oracle_extend" property="oracleExtend" />
+        <result column="sort_code" property="sortCode" />
+        <result column="create_time" property="createTime" />
+        <result column="create_by" property="createBy" />
+        <result column="update_time" property="updateTime" />
+        <result column="update_by" property="updateBy" />
+        <result column="delete_time" property="deleteTime" />
+        <result column="delete_by" property="deleteBy" />
+        <result column="is_delete" property="isDelete" />
+        <result column="tenant_id" property="tenantId" />
+    </resultMap>
+
+    <!-- 根据连接名称统计数量(排除指定ID) -->
+    <select id="countByFullName" resultType="int">
+        SELECT COUNT(*)
+        FROM data_link
+        WHERE full_name = #{fullName}
+        AND is_delete = 0
+        <if test="id != null">
+            AND id != #{id}
+        </if>
+    </select>
+
+    <!-- 分页查询数据源列表 -->
+    <select id="selectPageList" resultMap="BaseResultMap">
+        SELECT *
+        FROM data_link
+        WHERE is_delete = 0
+        <if test="keyword != null and keyword != ''">
+            AND (full_name LIKE CONCAT('%', #{keyword}, '%')
+            OR host LIKE CONCAT('%', #{keyword}, '%')
+            OR db_name LIKE CONCAT('%', #{keyword}, '%'))
+        </if>
+        ORDER BY create_time DESC
+        LIMIT #{startIndex}, #{pageSize}
+    </select>
+
+</mapper>

+ 15 - 0
service-data/service-data-biz/src/main/resources/smart-doc.json

@@ -0,0 +1,15 @@
+{
+  "outPath":"./src/main/resources/doc",
+  "serverUrl": "http:10.23.39.1:9902/",
+  "isStrict": false,
+  "coverOld": true,
+  "allInOne": true,
+  "packageFilters": "com.usky.data.controller.web",
+  "requestExample":"false",
+  "responseExample":"true",
+  "projectName": "数据中心",
+  "appKey": "20211216921084883495813120",
+  "appToken":"36bde2426ad546a5a50311bb747e7e61",
+  "secret": "N@Pd,KXAHki*BW3=zK.XPNykf!=CM79J",
+  "openUrl": "http://101.133.214.75:7700/api"
+}

+ 29 - 29
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EnergyDataServiceImpl.java

@@ -186,7 +186,7 @@ public class EnergyDataServiceImpl implements EnergyDataService {
             }
 
             Map<String, Object> meter = new HashMap<>();
-            meter.put("id", buildingId + device.getDeviceId().substring(device.getDeviceId().length() - 3));
+            meter.put("id", buildingId + "M" + device.getDeviceId().substring(device.getDeviceId().length() - 3));
             meter.put("name", device.getDeviceName());
 
             List<Map<String, String>> functions = new ArrayList<>();
@@ -196,30 +196,30 @@ public class EnergyDataServiceImpl implements EnergyDataService {
                 functions.add(createFunction("WPP", String.valueOf(data.getTotalActiveEnergyF()), null));
             }
             
-            // 正向有功功率
-            if (data.getTotalActivePower() != null) {
-                functions.add(createFunction("Ps", String.valueOf(data.getTotalActivePower()), null));
-            }
-            
-            // 正向无功功率
-            if (data.getTotalReactiveEnergyF() != null) {
-                functions.add(createFunction("Qs", String.valueOf(data.getTotalReactiveEnergyF()), null));
-            }
-            
-            // A相电压
-            if (data.getVoltageA() != null) {
-                functions.add(createFunction("Ua", String.valueOf(data.getVoltageA()), null));
-            }
-            
-            // B相电压
-            if (data.getVoltageB() != null) {
-                functions.add(createFunction("Ub", String.valueOf(data.getVoltageB()), null));
-            }
-            
-            // C相电压
-            if (data.getVoltageC() != null) {
-                functions.add(createFunction("Uc", String.valueOf(data.getVoltageC()), null));
-            }
+//            // 正向有功功率
+//            if (data.getTotalActivePower() != null) {
+//                functions.add(createFunction("Ps", String.valueOf(data.getTotalActivePower()), null));
+//            }
+//
+//            // 正向无功功率
+//            if (data.getTotalReactiveEnergyF() != null) {
+//                functions.add(createFunction("Qs", String.valueOf(data.getTotalReactiveEnergyF()), null));
+//            }
+//
+//            // A相电压
+//            if (data.getVoltageA() != null) {
+//                functions.add(createFunction("Ua", String.valueOf(data.getVoltageA()), null));
+//            }
+//
+//            // B相电压
+//            if (data.getVoltageB() != null) {
+//                functions.add(createFunction("Ub", String.valueOf(data.getVoltageB()), null));
+//            }
+//
+//            // C相电压
+//            if (data.getVoltageC() != null) {
+//                functions.add(createFunction("Uc", String.valueOf(data.getVoltageC()), null));
+//            }
             
             // A相电流
             if (data.getCurrentA() != null) {
@@ -236,10 +236,10 @@ public class EnergyDataServiceImpl implements EnergyDataService {
                 functions.add(createFunction("Ic", String.valueOf(data.getCurrentC()), null));
             }
             
-            // 功率因数
-            if (data.getTotalPowerFactor() != null) {
-                functions.add(createFunction("PFs", String.valueOf(data.getTotalPowerFactor()), null));
-            }
+//            // 功率因数
+//            if (data.getTotalPowerFactor() != null) {
+//                functions.add(createFunction("PFs", String.valueOf(data.getTotalPowerFactor()), null));
+//            }
 
             meter.put("functions", functions);
             meters.add(meter);

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

@@ -66,6 +66,17 @@
             <artifactId>ruoyi-common-swagger</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+        </dependency>
+
     </dependencies>
 
     <build>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+

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

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

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

+import lombok.Data;

+import lombok.EqualsAndHashCode;

+

+/**

+ * 操作审计日志

+ *

+ * @author fyc

+ * @date 2026-05-21

+ */

+@Data

+@EqualsAndHashCode(callSuper = true)

+@TableName("issue_operation_log")

+public class IssueOperationLog extends CloudAuditEntity {

+

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

+    private Long id;

+

+    private Long configId;

+

+    private String operationType;

+

+    private String operationDetail;

+

+    private String requestIp;

+}


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,17 @@
+package com.usky.issue.mapper;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.issue.domain.SysUser;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 用户信息 Mapper
+ *
+ * @author fyc
+ * @date 2026-06-29
+ */
+@DS("usky-cloud")
+@Repository
+public interface SysUserMapper extends CrudMapper<SysUser> {
+}

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,25 @@
+package com.usky.issue.service;
+
+import com.usky.issue.service.vo.SyncStatusResponse;
+import com.usky.issue.service.vo.SyncTaskResponse;
+
+import java.util.List;
+
+/**
+ * 云平台数据同步服务
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+public interface CloudSyncService {
+
+    List<SyncStatusResponse> listSyncStatus();
+
+    SyncTaskResponse triggerManualSync(String syncType, String direction);
+
+    SyncTaskResponse getTaskProgress(Long taskId);
+
+    void onOrgChanged();
+
+    void onUserChanged();
+}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,55 @@
+package com.usky.issue.service.constant;
+
+/**
+ * 云平台集成常量
+ *
+ * @author fyc
+ * @date 2026-05-21
+ */
+public final class CloudIntegrationConstants {
+
+    private CloudIntegrationConstants() {
+    }
+
+    public static final String LOCK_SYNC_PREFIX = "cloud:sync:";
+    public static final long LOCK_SYNC_SECONDS = 30L;
+
+    public static final String CACHE_STATUS_PREFIX = "cloud:sync:status:";
+    public static final long CACHE_STATUS_TTL_SECONDS = 5L;
+
+    public static final int CONNECTION_TEST_TIMEOUT_MS = 10_000;
+
+    public static final String HEADER_TENANT_ID = "X-Tenant-Id";
+    public static final String HEADER_API_KEY = "X-Api-Key";
+    public static final String HEADER_SIGN = "X-Sign";
+
+    public static final String PATH_TEST_CONNECTION = "/testConnection";
+    public static final String PATH_PING = "/ping";
+    public static final String PATH_AUTH_CHECK = "/auth-check";
+    public static final String PATH_SYNC_RECEIVE = "/sync/receive";
+    public static final String PATH_SYNC_POLL = "/sync/poll";
+    public static final String PATH_SYNC_POLL_ACK = "/sync/poll/ack";
+
+    public static final String AUTH_CHECK_VALID = "VALID";
+    public static final String AUTH_CHECK_TENANT_MISSING = "TENANT_MISSING";
+
+    public static final String MSG_TENANT_CONFIG_MISSING = "租户配置缺失";
+    public static final String MSG_AUTH_INVALID = "认证失败:API Key无效";
+    public static final String MSG_CONNECTION_SUCCESS = "连接成功";
+    public static final String MSG_NETWORK_UNREACHABLE = "云平台网络不可达";
+
+    public static final String OPERATION_SAVE_CONFIG = "SAVE_CONFIG";
+    public static final String OPERATION_TEST_CONNECTION = "TEST_CONNECTION";
+    public static final String OPERATION_DISABLE = "DISABLE_CONFIG";
+    public static final String OPERATION_ENABLE = "ENABLE_CONFIG";
+    public static final String OPERATION_MANUAL_SYNC = "MANUAL_SYNC";
+
+    /** 云平台数据源(sys_user、issue_sync_* 等) */
+    public static final String DS_CLOUD = "usky-cloud";
+
+    public static final String MSG_NO_SYNC_DATA =
+            "未拉取到待同步数据:请确认云端 sys_user 存在该租户数据,且 poll 认证通过(type=USER 对应表 sys_user)";
+    public static final String MSG_POLL_AUTH_FAILED = "云端 poll 认证失败,请检查凭证密钥是否与云端配置一致";
+    public static final String PATH_SYNC_DIAGNOSE = "/sync/diagnose";
+    public static final String OPERATION_SAVE_TRIGGER = "SAVE_TRIGGER";
+}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,238 @@
+package com.usky.issue.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.usky.issue.service.util.SyncJsonMapper;
+import com.usky.issue.domain.IssueCloudConfig;
+import com.usky.issue.domain.IssueSyncCursor;
+import com.usky.issue.domain.IssueSyncData;
+import com.usky.issue.domain.IssueSyncDownQueue;
+import com.usky.issue.mapper.IssueSyncCursorMapper;
+import com.usky.issue.mapper.IssueSyncDataMapper;
+import com.usky.issue.service.LocalPullService;
+import com.usky.issue.service.client.CloudPlatformClient;
+import com.usky.issue.service.enums.SyncDirection;
+import com.usky.issue.service.support.CloudEntitySupport;
+import com.usky.issue.service.sync.SyncDataApplierRegistry;
+import com.usky.issue.service.sync.SyncPayloadSupport;
+import com.usky.issue.service.constant.CloudIntegrationConstants;
+import com.usky.issue.service.vo.CloudPollResult;
+import com.usky.issue.service.vo.SyncPacket;
+import com.usky.issue.service.vo.SyncResponse;
+import com.usky.issue.service.vo.SyncResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 本地拉取服务实现
+ *
+ * @author fyc
+ * @date 2026-06-08
+ */
+@Slf4j
+@Service
+public class LocalPullServiceImpl implements LocalPullService {
+
+    private static final com.fasterxml.jackson.databind.ObjectMapper OBJECT_MAPPER = SyncJsonMapper.INSTANCE;
+
+    @Autowired
+    private IssueSyncDataMapper syncDataMapper;
+    @Autowired
+    private IssueSyncCursorMapper cursorMapper;
+    @Autowired
+    private CloudPlatformClient cloudPlatformClient;
+    @Autowired
+    private SyncDataApplierRegistry syncDataApplierRegistry;
+
+    @Override
+    public SyncResult pullAndApply(String tableName, IssueCloudConfig config) {
+        SyncResult result = SyncResult.empty();
+        List<IssueSyncCursor> cursors = cursorMapper.selectList(new LambdaQueryWrapper<IssueSyncCursor>()
+                .eq(IssueSyncCursor::getDirection, SyncDirection.DOWN.getCode())
+                .eq(IssueSyncCursor::getIsDeleted, 0));
+        if (cursors.isEmpty() && config.getTenantId() != null) {
+            result.merge(pullByTenant(config.getTenantId(), tableName, config));
+            return result;
+        }
+        for (IssueSyncCursor cursor : cursors) {
+            try {
+                result.merge(pullByTenant(cursor.getTenantId(), tableName, config));
+            } catch (Exception e) {
+                log.error("拉取租户{}数据失败", cursor.getTenantId(), e);
+                result.setFailureCount(result.getFailureCount() + 1);
+                result.setErrorMessage(e.getMessage());
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public SyncResult pullByTenant(Integer tenantId, String tableName, IssueCloudConfig config, boolean forceBackfill) {
+        SyncResult result = SyncResult.empty();
+        IssueSyncCursor cursor = getOrInitCursor(tenantId, tableName, SyncDirection.DOWN.getCode());
+        Long lastVersion = cursor.getLastSyncVersion() == null ? 0L : cursor.getLastSyncVersion();
+        if (forceBackfill) {
+            lastVersion = 0L;
+            updateCursor(tenantId, tableName, SyncDirection.DOWN.getCode(), 0L);
+            log.info("手动全量拉取,已重置 DOWN 游标 tenantId={}, tableName={}", tenantId, tableName);
+        }
+
+        while (true) {
+            CloudPollResult pollResult = cloudPlatformClient.pollCloud(
+                    tenantId, tableName, lastVersion, config, forceBackfill);
+            if (pollResult.isAuthFailed()) {
+                result.setFailureCount(1);
+                result.setErrorMessage(CloudIntegrationConstants.MSG_POLL_AUTH_FAILED);
+                log.error("[pull] poll 认证失败 tenantId={}, tableName={}", tenantId, tableName);
+                break;
+            }
+            if (pollResult.isRequestFailed()) {
+                result.setFailureCount(1);
+                result.setErrorMessage(pollResult.getErrorMessage());
+                break;
+            }
+            SyncPacket packet = pollResult.getPacket();
+            if (packet == null || packet.getDataList() == null || packet.getDataList().isEmpty()) {
+                if (result.getTotalCount() == 0) {
+                    log.warn("云端未返回待同步数据 tenantId={}, tableName={}(type=USER 对应 sys_user,数据经 issue_sync_down_queue 下发)",
+                            tenantId, tableName);
+                }
+                break;
+            }
+            forceBackfill = false;
+
+            List<IssueSyncDownQueue> queueList = convertQueueList(packet.getDataList());
+            List<Long> ackIds = new ArrayList<>();
+            int success = 0;
+            int failure = 0;
+
+            for (IssueSyncDownQueue queue : queueList) {
+                try {
+                    applyToLocal(queue);
+                    ackIds.add(queue.getId());
+                    success++;
+                } catch (Exception e) {
+                    log.error("应用云端数据到本地失败, queueId={}", queue.getId(), e);
+                    failure++;
+                }
+            }
+
+            result.setTotalCount(result.getTotalCount() + queueList.size());
+            result.setSuccessCount(result.getSuccessCount() + success);
+            result.setFailureCount(result.getFailureCount() + failure);
+
+            lastVersion = packet.getBatchVersion();
+            updateCursor(tenantId, tableName, SyncDirection.DOWN.getCode(), lastVersion);
+            ackCloud(ackIds, config);
+
+            if (Boolean.FALSE.equals(packet.getHasMore())) {
+                break;
+            }
+        }
+        return result;
+    }
+
+    private void applyToLocal(IssueSyncDownQueue queue) throws Exception {
+        String rawPayload = queue.getPayload();
+        IssueSyncData data;
+        try {
+            data = OBJECT_MAPPER.readValue(rawPayload, IssueSyncData.class);
+        } catch (Exception ex) {
+            data = new IssueSyncData();
+            data.setPayload(rawPayload);
+        }
+        if (!StringUtils.hasText(data.getDataKey())) {
+            data.setDataKey(queue.getTableName() + "-" + queue.getId());
+        }
+        data.setTenantId(queue.getTenantId());
+        data.setTableName(queue.getTableName());
+        data.setSyncVersion(queue.getSyncVersion());
+        data.setSyncDirection("CLOUD");
+        data.setSyncStatus(1);
+
+        IssueSyncData exist = syncDataMapper.selectOne(new LambdaQueryWrapper<IssueSyncData>()
+                .eq(IssueSyncData::getTenantId, data.getTenantId())
+                .eq(IssueSyncData::getTableName, data.getTableName())
+                .eq(IssueSyncData::getDataKey, data.getDataKey())
+                .eq(IssueSyncData::getIsDeleted, 0));
+        if (exist == null) {
+            CloudEntitySupport.fillOnInsert(data);
+            syncDataMapper.insert(data);
+        } else if (queue.getSyncVersion() >= exist.getSyncVersion()) {
+            data.setId(exist.getId());
+            CloudEntitySupport.fillOnUpdate(data);
+            syncDataMapper.updateById(data);
+        }
+        applyToBusinessTable(queue, data);
+    }
+
+    private void applyToBusinessTable(IssueSyncDownQueue queue, IssueSyncData data) throws Exception {
+        String rawPayload = StringUtils.hasText(data.getPayload()) ? data.getPayload() : queue.getPayload();
+        if (!StringUtils.hasText(rawPayload)) {
+            return;
+        }
+        String businessPayload = SyncPayloadSupport.resolveBusinessPayload(rawPayload);
+        String operation = StringUtils.hasText(queue.getOperation()) ? queue.getOperation() : "UPDATE";
+        syncDataApplierRegistry.apply(queue.getTenantId(), queue.getTableName(), operation, businessPayload);
+    }
+
+    private void ackCloud(List<Long> queueIds, IssueCloudConfig config) {
+        if (queueIds == null || queueIds.isEmpty()) {
+            return;
+        }
+        SyncResponse response = cloudPlatformClient.ackPoll(queueIds, config);
+        if (response == null || !response.isSuccess()) {
+            log.warn("ACK云端失败 queueIds={}", queueIds);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private List<IssueSyncDownQueue> convertQueueList(List<?> dataList) {
+        return dataList.stream().map(item -> {
+            if (item instanceof IssueSyncDownQueue) {
+                return (IssueSyncDownQueue) item;
+            }
+            return OBJECT_MAPPER.convertValue(item, IssueSyncDownQueue.class);
+        }).collect(Collectors.toList());
+    }
+
+    private IssueSyncCursor getOrInitCursor(Integer tenantId, String tableName, String direction) {
+        IssueSyncCursor cursor = cursorMapper.selectOne(new LambdaQueryWrapper<IssueSyncCursor>()
+                .eq(IssueSyncCursor::getTenantId, tenantId)
+                .eq(IssueSyncCursor::getTableName, tableName)
+                .eq(IssueSyncCursor::getDirection, direction)
+                .eq(IssueSyncCursor::getIsDeleted, 0));
+        if (cursor == null) {
+            cursor = new IssueSyncCursor();
+            cursor.setTenantId(tenantId);
+            cursor.setTableName(tableName);
+            cursor.setDirection(direction);
+            cursor.setLastSyncVersion(0L);
+            cursor.setLastSyncTime(LocalDateTime.now());
+            CloudEntitySupport.fillOnInsert(cursor);
+            cursorMapper.insert(cursor);
+        }
+        return cursor;
+    }
+
+    private void updateCursor(Integer tenantId, String tableName, String direction, Long version) {
+        cursorMapper.update(null, new LambdaUpdateWrapper<IssueSyncCursor>()
+                .eq(IssueSyncCursor::getTenantId, tenantId)
+                .eq(IssueSyncCursor::getTableName, tableName)
+                .eq(IssueSyncCursor::getDirection, direction)
+                .set(IssueSyncCursor::getLastSyncVersion, version)
+                .set(IssueSyncCursor::getLastSyncTime, LocalDateTime.now()));
+    }
+
+    @Override
+    public void resetDownCursor(Integer tenantId, String tableName) {
+        updateCursor(tenantId, tableName, SyncDirection.DOWN.getCode(), 0L);
+    }
+}

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

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

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

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

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff