Ver Fonte

Merge branch 'fhs-ems' of uskycloud/usky-modules into server-165

fanghuisheng há 1 semana atrás
pai
commit
f09cded730
85 ficheiros alterados com 4099 adições e 141 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. 9 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsProjectController.java
  43. 6 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsProjectService.java
  44. 34 0
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsProjectServiceImpl.java
  45. 29 29
      service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EnergyDataServiceImpl.java
  46. 5 0
      service-issue/service-issue-biz/pom.xml
  47. 35 4
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/api/CloudIntegrationApiController.java
  48. 8 15
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudSyncController.java
  49. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/sys_user.sql
  50. 70 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/domain/SysUser.java
  51. 17 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/mapper/SysUserMapper.java
  52. 23 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncPublishService.java
  53. 1 1
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncReceiveService.java
  54. 25 25
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncService.java
  55. 4 2
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/CloudSyncTaskRunner.java
  56. 7 1
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalPullService.java
  57. 1 1
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalSyncAgent.java
  58. 81 20
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/client/CloudPlatformClient.java
  59. 8 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/constant/CloudIntegrationConstants.java
  60. 5 1
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncDirection.java
  61. 5 1
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncTypeEnum.java
  62. 20 2
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudAuthServiceImpl.java
  63. 25 1
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudConfigServiceImpl.java
  64. 2 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudOperationLogServiceImpl.java
  65. 140 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncPublishServiceImpl.java
  66. 34 3
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncReceiveServiceImpl.java
  67. 21 8
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncServiceImpl.java
  68. 27 10
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncTaskRunnerImpl.java
  69. 59 6
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalPullServiceImpl.java
  70. 21 8
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/LocalSyncAgentImpl.java
  71. 14 2
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudUserChangedEvent.java
  72. 28 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudUserSyncPublishListener.java
  73. 46 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/support/CloudEntitySupport.java
  74. 32 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/support/CloudSyncEventPublisher.java
  75. 24 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncDataApplier.java
  76. 28 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncDataApplierRegistry.java
  77. 51 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/SyncPayloadSupport.java
  78. 78 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/sync/impl/UserSyncDataApplier.java
  79. 59 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/ApiResultParser.java
  80. 26 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/util/SyncJsonMapper.java
  81. 1 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudConfigResponse.java
  82. 25 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/CloudPollResult.java
  83. 24 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/ManualSyncRequest.java
  84. 3 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncPacket.java
  85. 21 0
      service-issue/service-issue-biz/src/main/java/com/usky/issue/service/vo/SyncPollRequest.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"
+}

+ 9 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/controller/web/EmsProjectController.java

@@ -2,6 +2,7 @@ package com.usky.ems.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
 import com.usky.ems.domain.DmpDevice;
+import com.usky.ems.domain.DmpProduct;
 import com.usky.ems.domain.EmsEnergyItemCode;
 import com.usky.ems.service.EmsEnergyItemCodeService;
 import com.usky.ems.service.EmsProjectService;
@@ -60,6 +61,14 @@ public class EmsProjectController {
         return ApiResult.success(emsProjectService.listAreaDeviceItemCodes(spaceId));
     }
 
+    /**
+     * 按能耗类型查询已绑定产品列表(ems_product_energy_type)
+     */
+    @GetMapping("/product-energy-type/list")
+    public ApiResult<List<DmpProduct>> listProductsByEnergyType(@RequestParam Integer energyType) {
+        return ApiResult.success(emsProjectService.listProductsByEnergyType(energyType));
+    }
+
     /**
      * 产品关联的能源类型(ems_product_energy_type)
      */

+ 6 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/EmsProjectService.java

@@ -1,6 +1,7 @@
 package com.usky.ems.service;
 
 import com.usky.ems.domain.DmpDevice;
+import com.usky.ems.domain.DmpProduct;
 import com.usky.ems.domain.EmsEnergyItemCode;
 import com.usky.ems.service.vo.EmsDeviceItemCodeSaveRequest;
 import com.usky.ems.service.vo.EmsProductEnergyTypeSaveRequest;
@@ -25,6 +26,11 @@ public interface EmsProjectService {
      */
     void saveProductEnergyTypes(EmsProductEnergyTypeSaveRequest request);
 
+    /**
+     * 按能耗类型查询已绑定产品列表(ems_product_energy_type)
+     */
+    List<DmpProduct> listProductsByEnergyType(Integer energyType);
+
     /**
      * 保存设备关联的能源分项:先删除当前租户下该分项编码的记录,再按设备列表插入
      */

+ 34 - 0
service-ems/service-ems-biz/src/main/java/com/usky/ems/service/impl/EmsProjectServiceImpl.java

@@ -7,6 +7,7 @@ import com.usky.common.security.utils.SecurityUtils;
 import com.usky.ems.domain.BaseSpace;
 import com.usky.ems.domain.BaseSpaceGateway;
 import com.usky.ems.domain.DmpDevice;
+import com.usky.ems.domain.DmpProduct;
 import com.usky.ems.domain.EmsDeviceItemCode;
 import com.usky.ems.domain.EmsEnergyItemCode;
 import com.usky.ems.domain.EmsProductEnergyType;
@@ -14,6 +15,7 @@ import com.usky.ems.domain.EmsProject;
 import com.usky.ems.domain.EmsProjectDeviceSystem;
 import com.usky.ems.mapper.BaseSpaceGatewayMapper;
 import com.usky.ems.mapper.DmpDeviceMapper;
+import com.usky.ems.mapper.DmpProductMapper;
 import com.usky.ems.mapper.EmsDeviceItemCodeMapper;
 import com.usky.ems.mapper.EmsEnergyItemCodeMapper;
 import com.usky.ems.mapper.EmsProductEnergyTypeMapper;
@@ -64,6 +66,8 @@ public class EmsProjectServiceImpl implements EmsProjectService {
     @Autowired
     private DmpDeviceMapper dmpDeviceMapper;
     @Autowired
+    private DmpProductMapper dmpProductMapper;
+    @Autowired
     private EmsEnergyItemCodeMapper emsEnergyItemCodeMapper;
 
     @Override
@@ -160,6 +164,36 @@ public class EmsProjectServiceImpl implements EmsProjectService {
         }
     }
 
+    @Override
+    public List<DmpProduct> listProductsByEnergyType(Integer energyType) {
+        if (energyType == null) {
+            throw new BusinessException("能耗类型不能为空");
+        }
+        Integer tenantId = SecurityUtils.getTenantId();
+        List<EmsProductEnergyType> bindings = emsProductEnergyTypeMapper.selectList(
+                Wrappers.<EmsProductEnergyType>lambdaQuery()
+                        .eq(EmsProductEnergyType::getTenantId, tenantId)
+                        .eq(EmsProductEnergyType::getEnergyType, energyType));
+        if (bindings.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<Integer> productIds = bindings.stream()
+                .map(EmsProductEnergyType::getProductId)
+                .filter(Objects::nonNull)
+                .distinct()
+                .map(Long::intValue)
+                .collect(Collectors.toList());
+        if (productIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return dmpProductMapper.selectList(
+                Wrappers.<DmpProduct>lambdaQuery()
+                        .in(DmpProduct::getId, productIds)
+                        .eq(DmpProduct::getTenantId, tenantId)
+                        .eq(DmpProduct::getDeleteFlag, 0)
+                        .orderByAsc(DmpProduct::getId));
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void saveDeviceItemCodes(EmsDeviceItemCodeSaveRequest request) {

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

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

@@ -72,6 +72,11 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 35 - 4
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/api/CloudIntegrationApiController.java

@@ -7,7 +7,10 @@ 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;
@@ -28,6 +31,7 @@ import java.util.List;
  * @author fyc
  * @date 2026-06-08
  */
+@Slf4j
 @RestController
 @RequestMapping("/cloud/integration")
 public class CloudIntegrationApiController {
@@ -72,7 +76,11 @@ public class CloudIntegrationApiController {
             @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);
@@ -83,9 +91,12 @@ public class CloudIntegrationApiController {
             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);
     }
 
@@ -118,19 +129,39 @@ public class CloudIntegrationApiController {
     }
 
     /**
-     * 下发数据轮询(云端→本地)
+     * 下发数据轮询(云端→本地),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) 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))) {
-            return ApiResult.success(null);
+            log.warn("[poll] 认证失败 tenantId={}, tableName={}", tenantId, tableName);
+            return ApiResult.error(CloudIntegrationConstants.MSG_POLL_AUTH_FAILED);
         }
-        return ApiResult.success(cloudSyncReceiveService.pollDownQueue(tenantId, tableName, lastVersion));
+        log.info("[poll] tenantId={}, tableName={}, lastVersion={}, forceBackfill={}",
+                tenantId, tableName, lastVersion, forceBackfill);
+        return ApiResult.success(cloudSyncReceiveService.pollDownQueue(
+                tenantId, tableName, lastVersion, forceBackfill));
     }
 
     /**

+ 8 - 15
service-issue/service-issue-biz/src/main/java/com/usky/issue/controller/web/CloudSyncController.java

@@ -1,18 +1,15 @@
 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.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.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
 
-import javax.servlet.http.HttpServletRequest;
+import javax.validation.Valid;
 import java.util.List;
 
 /**
@@ -22,6 +19,7 @@ import java.util.List;
  * @date 2026-05-21
  */
 @RestController
+@Validated
 @RequestMapping("/cloud")
 public class CloudSyncController {
 
@@ -33,18 +31,13 @@ public class CloudSyncController {
         return ApiResult.success(cloudSyncService.listSyncStatus());
     }
 
-    @PostMapping("/sync/{type}")
-    public ApiResult<SyncTaskResponse> manualSync(@PathVariable String type, HttpServletRequest request) {
-        return ApiResult.success(cloudSyncService.triggerManualSync(type));
+    @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));
     }
-
-    // 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='用户信息表';

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

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

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

@@ -17,7 +17,7 @@ public interface CloudSyncReceiveService {
 
     int batchUpsert(SyncPacket packet);
 
-    SyncPacket pollDownQueue(String tenantId, String tableName, Long lastVersion);
+    SyncPacket pollDownQueue(String tenantId, String tableName, Long lastVersion, boolean forceBackfill);
 
     void ackDownQueue(List<Long> queueIds);
 

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

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

-

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

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

-

-import java.util.List;

-

-/**

- * 云平台数据同步服务

- *

- * @author fyc

- * @date 2026-05-21

- */

-public interface CloudSyncService {

-

-    List<SyncStatusResponse> listSyncStatus();

-

-    SyncTaskResponse triggerManualSync(String syncType);

-

-    SyncTaskResponse getTaskProgress(Long taskId);

-

-    void onOrgChanged();

-

-    void onUserChanged();

-}

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

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

@@ -1,5 +1,6 @@
 package com.usky.issue.service;
 
+import com.usky.issue.service.enums.SyncDirection;
 import com.usky.issue.service.enums.SyncTypeEnum;
 
 /**
@@ -10,7 +11,8 @@ import com.usky.issue.service.enums.SyncTypeEnum;
  */
 public interface CloudSyncTaskRunner {
 
-    com.usky.issue.domain.IssueSyncTask createTask(Long configId, SyncTypeEnum type, String triggerMode);
+    com.usky.issue.domain.IssueSyncTask createTask(Long configId, SyncTypeEnum type, String triggerMode,
+                                                   SyncDirection direction);
 
-    void executeSyncTask(Long taskId, SyncTypeEnum type);
+    void executeSyncTask(Long taskId, SyncTypeEnum type, SyncDirection direction);
 }

+ 7 - 1
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/LocalPullService.java

@@ -13,5 +13,11 @@ public interface LocalPullService {
 
     SyncResult pullAndApply(String tableName, IssueCloudConfig config);
 
-    SyncResult pullByTenant(Integer tenantId, 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);
 }

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

@@ -15,5 +15,5 @@ public interface LocalSyncAgent {
 
     SyncResult executeSync(SyncTypeEnum syncType, IssueCloudConfig config, SyncDirection direction, boolean fullSync);
 
-    int countPending(SyncTypeEnum syncType, IssueCloudConfig config);
+    int countPending(SyncTypeEnum syncType, IssueCloudConfig config, SyncDirection direction);
 }

+ 81 - 20
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/client/CloudPlatformClient.java

@@ -4,11 +4,16 @@ 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;
@@ -20,6 +25,7 @@ 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;
@@ -64,11 +70,14 @@ public class CloudPlatformClient {
 
         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());
@@ -97,7 +106,8 @@ public class CloudPlatformClient {
         }
         try {
             String credential = aesGcmCipher.decrypt(config.getCredentialKey());
-            return testConnection(config.getCloudAddress(), config.getTenantId(), credential, config.getToken());
+            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, "凭证解密失败");
@@ -124,31 +134,64 @@ public class CloudPlatformClient {
         }
     }
 
-    public SyncPacket pollCloud(Integer tenantId, String tableName, Long lastVersion, IssueCloudConfig config) {
+    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);
-            String url = UriComponentsBuilder
-                    .fromHttpUrl(resolveIntegrationBaseUrl(config) + CloudIntegrationConstants.PATH_SYNC_POLL)
-                    .queryParam("tenantId", tenantId)
-                    .queryParam("tableName", tableName)
-                    .queryParam("lastVersion", lastVersion == null ? 0L : lastVersion)
-                    .toUriString();
+            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.GET, new HttpEntity<>(headers), String.class);
-            if (!StringUtils.hasText(response.getBody())) {
-                return null;
+                    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 json = OBJECT_MAPPER.readTree(response.getBody());
-            JsonNode dataNode = json.has("data") ? json.get("data") : json;
+            JsonNode dataNode = json.has("data") ? json.get("data") : null;
             if (dataNode == null || dataNode.isNull()) {
-                return null;
+                return CloudPollResult.builder().packet(null).build();
             }
-            return OBJECT_MAPPER.treeToValue(dataNode, SyncPacket.class);
+            SyncPacket packet = OBJECT_MAPPER.treeToValue(dataNode, SyncPacket.class);
+            return CloudPollResult.builder().packet(packet).build();
         } catch (Exception e) {
-            log.error("轮询云端失败", e);
-            return null;
+            log.error("[pollCloud] 响应解析失败 body={}", body, e);
+            return CloudPollResult.builder().requestFailed(true).errorMessage("响应解析失败: " + e.getMessage()).build();
         }
     }
 
@@ -172,7 +215,7 @@ public class CloudPlatformClient {
     }
 
     private HttpHeaders buildAuthHeaders(IssueCloudConfig config, String credential) {
-        return buildAuthHeaders(config.getTenantId(), credential, config.getToken());
+        return buildAuthHeaders(config.getTenantId(), credential, CloudEntitySupport.resolveAccessToken(config));
     }
 
     private HttpHeaders buildAuthHeaders(Integer tenantId, String credential, String token) {
@@ -187,17 +230,19 @@ public class CloudPlatformClient {
 
     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(dataNode.path("message").asText(null));
+            result.setMessage(resolveResponseMessage(json, dataNode));
             result.setNetworkReachable(dataNode.has("networkReachable")
                     ? dataNode.get("networkReachable").asBoolean() : true);
             result.setAuthValid(dataNode.has("authValid")
@@ -209,14 +254,30 @@ public class CloudPlatformClient {
                 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("连接测试响应解析失败: {}", body, 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;
     }

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

@@ -43,5 +43,13 @@ public final class CloudIntegrationConstants {
     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";
 }

+ 5 - 1
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncDirection.java

@@ -21,8 +21,12 @@ public enum SyncDirection {
     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(code)) {
+            if (direction.code.equalsIgnoreCase(normalized)) {
                 return direction;
             }
         }

+ 5 - 1
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/enums/SyncTypeEnum.java

@@ -30,8 +30,12 @@ public enum SyncTypeEnum {
     }
 
     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(code)) {
+            if (type.code.equalsIgnoreCase(normalized)) {
                 return type;
             }
         }

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

@@ -6,6 +6,7 @@ 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;
@@ -32,14 +33,27 @@ public class CloudAuthServiceImpl implements CloudAuthService {
 
     @Override
     public boolean validateToken(String apiKey, Integer tenantId) {
-        if (!StringUtils.hasText(apiKey) || tenantId == null) {
+        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;
         }
-        return matchCredential(apiKey, config);
+        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
@@ -88,4 +102,8 @@ public class CloudAuthServiceImpl implements CloudAuthService {
             return false;
         }
     }
+
+    private String maskApiKey(String apiKey) {
+        return CredentialMaskUtil.mask(apiKey);
+    }
 }

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

@@ -1,5 +1,6 @@
 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;
@@ -27,6 +28,7 @@ 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;
@@ -41,6 +43,8 @@ import java.util.List;
  * @author fyc
  * @date 2026-05-21
  */
+@DS("usky-cloud")
+@Slf4j
 @Service
 public class CloudConfigServiceImpl implements CloudConfigService {
 
@@ -119,23 +123,34 @@ public class CloudConfigServiceImpl implements CloudConfigService {
         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(),
-                SecurityUtils.getToken());
+                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());
@@ -150,11 +165,15 @@ public class CloudConfigServiceImpl implements CloudConfigService {
         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("凭证密钥校验失败");
         }
     }
@@ -163,6 +182,7 @@ public class CloudConfigServiceImpl implements CloudConfigService {
     public CloudConnectionTestResponse validateTenantConnection(Integer tenantId) {
         CloudConnectionTestResponse response = new CloudConnectionTestResponse();
         if (tenantId == null) {
+            log.warn("[连接测试-租户校验] tenantId 为空");
             response.setSuccess(false);
             response.setConnectionStatus(2);
             response.setMessage("租户ID不能为空");
@@ -174,6 +194,7 @@ public class CloudConfigServiceImpl implements CloudConfigService {
 
         IssueCloudConfig config = findConfigByTenantId(tenantId);
         if (config == null) {
+            log.warn("[连接测试-租户校验] 配置不存在 tenantId={}", tenantId);
             response.setSuccess(false);
             response.setConnectionStatus(2);
             response.setMessage(CloudIntegrationConstants.MSG_TENANT_CONFIG_MISSING);
@@ -183,6 +204,7 @@ public class CloudConfigServiceImpl implements CloudConfigService {
             return response;
         }
         if (config.getStatus() != null && config.getStatus() == 0) {
+            log.warn("[连接测试-租户校验] 配置已禁用 tenantId={}, configId={}", tenantId, config.getId());
             response.setSuccess(false);
             response.setConnectionStatus(2);
             response.setMessage("租户配置已禁用");
@@ -198,6 +220,7 @@ public class CloudConfigServiceImpl implements CloudConfigService {
         response.setNetworkReachable(true);
         response.setAuthValid(true);
         response.setTenantExists(true);
+        log.info("[连接测试-租户校验] 通过 tenantId={}, configId={}", tenantId, config.getId());
         return response;
     }
 
@@ -341,6 +364,7 @@ public class CloudConfigServiceImpl implements CloudConfigService {
         resp.setUpdatedBy(config.getUpdatedBy());
         resp.setCreatedTime(config.getCreatedTime());
         resp.setCreatedBy(config.getCreatedBy());
+        resp.setCloudAddress(config.getCloudAddress());
         return resp;
     }
 }

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

@@ -1,5 +1,6 @@
 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;
@@ -24,6 +25,7 @@ public class CloudOperationLogServiceImpl implements CloudOperationLogService {
     @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();

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

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

@@ -1,16 +1,21 @@
 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.fasterxml.jackson.databind.ObjectMapper;
+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;
@@ -33,7 +38,7 @@ import java.util.Map;
 public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
 
     private static final int POLL_BATCH_SIZE = 100;
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final com.fasterxml.jackson.databind.ObjectMapper OBJECT_MAPPER = SyncJsonMapper.INSTANCE;
 
     @Autowired
     private IssueSyncDataMapper syncDataMapper;
@@ -41,6 +46,10 @@ public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
     private IssueSyncDownQueueMapper downQueueMapper;
     @Autowired
     private CloudConfigService cloudConfigService;
+    @Autowired
+    private CloudSyncPublishService cloudSyncPublishService;
+    @Autowired
+    private SyncDataApplierRegistry syncDataApplierRegistry;
 
     @Override
     public boolean validateTenant(String tenantId) {
@@ -73,6 +82,7 @@ public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
                     incoming.setSyncDirection("LOCAL");
                     CloudEntitySupport.fillOnInsert(incoming);
                     syncDataMapper.insert(incoming);
+                    applyToBusinessTable(incoming, "UPDATE");
                     count++;
                 } else if (incoming.getSyncVersion() >= exist.getSyncVersion()) {
                     incoming.setId(exist.getId());
@@ -80,6 +90,7 @@ public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
                     incoming.setSyncDirection("LOCAL");
                     CloudEntitySupport.fillOnUpdate(incoming);
                     syncDataMapper.updateById(incoming);
+                    applyToBusinessTable(incoming, "UPDATE");
                     count++;
                 }
             } catch (Exception e) {
@@ -89,12 +100,18 @@ public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
         return count;
     }
 
+    @DS("usky-cloud")
     @Override
-    public SyncPacket pollDownQueue(String tenantId, String tableName, Long lastVersion) {
+    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)
@@ -105,6 +122,7 @@ public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
                 .last("LIMIT " + POLL_BATCH_SIZE));
 
         if (queueList.isEmpty()) {
+            log.info("[poll] 下发队列为空 tenantId={}, tableName={}, lastVersion={}", tenantId, tableName, lastVersion);
             return null;
         }
 
@@ -159,4 +177,17 @@ public class CloudSyncReceiveServiceImpl implements CloudSyncReceiveService {
         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);
+        }
+    }
 }

+ 21 - 8
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncServiceImpl.java

@@ -1,6 +1,7 @@
 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;
@@ -11,6 +12,7 @@ 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;
@@ -38,6 +40,7 @@ import java.util.stream.Collectors;
  * @author fyc
  * @date 2026-05-21
  */
+@DS("usky-cloud")
 @Slf4j
 @Service
 public class CloudSyncServiceImpl implements CloudSyncService {
@@ -75,7 +78,7 @@ public class CloudSyncServiceImpl implements CloudSyncService {
     }
 
     @Override
-    public SyncTaskResponse triggerManualSync(String syncType) {
+    public SyncTaskResponse triggerManualSync(String syncType, String direction) {
         String operator = CloudEntitySupport.resolveOperator();
         String requestIp = CloudEntitySupport.resolveRequestIp();
         SyncTypeEnum typeEnum;
@@ -84,23 +87,33 @@ public class CloudSyncServiceImpl implements CloudSyncService {
         } 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();
+        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");
+            IssueSyncTask task = cloudSyncTaskRunner.createTask(config.getId(), typeEnum, "MANUAL", directionEnum);
             operationLogService.log(config.getId(), CloudIntegrationConstants.OPERATION_MANUAL_SYNC,
-                    "手动同步 type=" + typeEnum.getCode(), operator, requestIp);
+                    "手动同步 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));
+                cloudSyncExecutor.execute(() -> cloudSyncTaskRunner.executeSyncTask(task.getId(), typeEnum, directionEnum));
                 return toTaskResponse(syncTaskMapper.selectById(task.getId()));
             }
-            cloudSyncTaskRunner.executeSyncTask(task.getId(), typeEnum);
+            cloudSyncTaskRunner.executeSyncTask(task.getId(), typeEnum, directionEnum);
             return toTaskResponse(syncTaskMapper.selectById(task.getId()));
         } finally {
             redisHelper.unlockLua(lockKey, lockValue);
@@ -141,8 +154,8 @@ public class CloudSyncServiceImpl implements CloudSyncService {
         if (event == null || event.getEnabled() == null || event.getEnabled() == 0) {
             return;
         }
-        IssueSyncTask task = cloudSyncTaskRunner.createTask(config.getId(), syncType, "EVENT");
-        cloudSyncExecutor.execute(() -> cloudSyncTaskRunner.executeSyncTask(task.getId(), syncType));
+        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) {

+ 27 - 10
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/impl/CloudSyncTaskRunnerImpl.java

@@ -48,9 +48,9 @@ public class CloudSyncTaskRunnerImpl implements CloudSyncTaskRunner {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public IssueSyncTask createTask(Long configId, SyncTypeEnum type, String triggerMode) {
+    public IssueSyncTask createTask(Long configId, SyncTypeEnum type, String triggerMode, SyncDirection direction) {
         IssueCloudConfig config = configMapper.selectById(configId);
-        int pending = localSyncAgent.countPending(type, config);
+        int pending = localSyncAgent.countPending(type, config, direction);
         IssueSyncTask task = new IssueSyncTask();
         task.setConfigId(configId);
         if (config != null) {
@@ -70,7 +70,7 @@ public class CloudSyncTaskRunnerImpl implements CloudSyncTaskRunner {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void executeSyncTask(Long taskId, SyncTypeEnum type) {
+    public void executeSyncTask(Long taskId, SyncTypeEnum type, SyncDirection direction) {
         IssueSyncTask task = syncTaskMapper.selectById(taskId);
         if (task == null) {
             return;
@@ -84,24 +84,39 @@ public class CloudSyncTaskRunnerImpl implements CloudSyncTaskRunner {
         syncTaskMapper.updateById(task);
 
         IssueCloudConfig config = configMapper.selectById(task.getConfigId());
-        SyncResult syncResult = localSyncAgent.executeSync(type, config, SyncDirection.BOTH, false);
+        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;
         }
 
-        task.setTotalCount(Math.max(task.getTotalCount(), syncResult.getTotalCount()));
+        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());
-        task.setTaskStatus(syncResult.getFailureCount() > 0 && syncResult.getSuccessCount() == 0
-                ? TaskStatusEnum.FAILED.getCode() : TaskStatusEnum.SUCCESS.getCode());
-        if (syncResult.getFailureCount() > 0) {
+
+        if (syncResult.getFailureCount() > 0 && syncResult.getSuccessCount() == 0) {
+            task.setTaskStatus(TaskStatusEnum.FAILED.getCode());
             task.setErrorSummary(syncResult.getErrorMessage() != null
                     ? syncResult.getErrorMessage()
-                    : "部分失败: " + syncResult.getFailureCount() + " 条");
+                    : "同步失败: " + 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);
@@ -140,7 +155,9 @@ public class CloudSyncTaskRunnerImpl implements CloudSyncTaskRunner {
         status.setTotalCount(task.getTotalCount());
         status.setSuccessCount(task.getSuccessCount());
         status.setFailureCount(task.getFailureCount());
-        status.setUnsyncedCount(Math.max(0, task.getTotalCount() - task.getProcessedCount()));
+        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);

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

@@ -2,7 +2,7 @@ package com.usky.issue.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import com.usky.issue.service.util.SyncJsonMapper;
 import com.usky.issue.domain.IssueCloudConfig;
 import com.usky.issue.domain.IssueSyncCursor;
 import com.usky.issue.domain.IssueSyncData;
@@ -13,12 +13,17 @@ 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;
@@ -35,7 +40,7 @@ import java.util.stream.Collectors;
 @Service
 public class LocalPullServiceImpl implements LocalPullService {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final com.fasterxml.jackson.databind.ObjectMapper OBJECT_MAPPER = SyncJsonMapper.INSTANCE;
 
     @Autowired
     private IssueSyncDataMapper syncDataMapper;
@@ -43,6 +48,8 @@ public class LocalPullServiceImpl implements LocalPullService {
     private IssueSyncCursorMapper cursorMapper;
     @Autowired
     private CloudPlatformClient cloudPlatformClient;
+    @Autowired
+    private SyncDataApplierRegistry syncDataApplierRegistry;
 
     @Override
     public SyncResult pullAndApply(String tableName, IssueCloudConfig config) {
@@ -67,16 +74,39 @@ public class LocalPullServiceImpl implements LocalPullService {
     }
 
     @Override
-    public SyncResult pullByTenant(Integer tenantId, String tableName, IssueCloudConfig config) {
+    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) {
-            SyncPacket packet = cloudPlatformClient.pollCloud(tenantId, tableName, lastVersion, config);
+            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<>();
@@ -110,8 +140,15 @@ public class LocalPullServiceImpl implements LocalPullService {
     }
 
     private void applyToLocal(IssueSyncDownQueue queue) throws Exception {
-        IssueSyncData data = OBJECT_MAPPER.readValue(queue.getPayload(), IssueSyncData.class);
-        if (data.getDataKey() == null) {
+        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());
@@ -133,6 +170,17 @@ public class LocalPullServiceImpl implements LocalPullService {
             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) {
@@ -182,4 +230,9 @@ public class LocalPullServiceImpl implements LocalPullService {
                 .set(IssueSyncCursor::getLastSyncVersion, version)
                 .set(IssueSyncCursor::getLastSyncTime, LocalDateTime.now()));
     }
+
+    @Override
+    public void resetDownCursor(Integer tenantId, String tableName) {
+        updateCursor(tenantId, tableName, SyncDirection.DOWN.getCode(), 0L);
+    }
 }

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

@@ -63,19 +63,32 @@ public class LocalSyncAgentImpl implements LocalSyncAgent {
             result.merge(pushService.pushByTenant(config.getTenantId(), tableName, config));
         }
         if (direction == SyncDirection.DOWN || direction == SyncDirection.BOTH) {
-            result.merge(pullService.pullByTenant(config.getTenantId(), tableName, config));
+            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) {
+    public int countPending(SyncTypeEnum syncType, IssueCloudConfig config, SyncDirection direction) {
+        if (config == null || config.getTenantId() == null) {
+            return 0;
+        }
         String tableName = syncType.getCode();
-        Integer upCount = syncDataMapper.selectCount(new LambdaQueryWrapper<IssueSyncData>()
-                .eq(IssueSyncData::getTenantId, config.getTenantId())
-                .eq(IssueSyncData::getTableName, tableName)
-                .in(IssueSyncData::getSyncStatus, 0, 2)
-                .eq(IssueSyncData::getIsDeleted, 0));
-        return cloudSyncReceiveService.countPendingDownQueue(config.getTenantId(), tableName);
+        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;
     }
 }

+ 14 - 2
service-issue/service-issue-biz/src/main/java/com/usky/issue/service/listener/CloudUserChangedEvent.java

@@ -1,5 +1,6 @@
 package com.usky.issue.service.listener;
 
+import com.usky.issue.domain.SysUser;
 import org.springframework.context.ApplicationEvent;
 
 /**
@@ -10,7 +11,18 @@ import org.springframework.context.ApplicationEvent;
  */
 public class CloudUserChangedEvent extends ApplicationEvent {
 
-    public CloudUserChangedEvent(Object source) {
-        super(source);
+    private final String operation;
+
+    public CloudUserChangedEvent(SysUser user, String operation) {
+        super(user);
+        this.operation = operation != null ? operation : "UPDATE";
+    }
+
+    public SysUser getUser() {
+        return (SysUser) getSource();
+    }
+
+    public String getOperation() {
+        return operation;
     }
 }

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

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

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

@@ -3,6 +3,7 @@ package com.usky.issue.service.support;
 import com.baomidou.mybatisplus.core.toolkit.StringUtils;
 import com.usky.common.security.utils.SecurityUtils;
 import com.usky.issue.domain.CloudAuditEntity;
+import com.usky.issue.domain.IssueCloudConfig;
 import org.springframework.web.context.request.RequestContextHolder;
 import org.springframework.web.context.request.ServletRequestAttributes;
 
@@ -32,6 +33,51 @@ public final class CloudEntitySupport {
         return attrs.getRequest().getRemoteAddr();
     }
 
+    /**
+     * 解析当前请求的访问令牌,用于转发至云端 HTTP 调用(Authorization: Bearer)
+     */
+    public static String resolveAccessToken() {
+        String fromHeader = resolveAuthorizationHeader();
+        if (StringUtils.isNotBlank(fromHeader)) {
+            return stripBearerPrefix(fromHeader);
+        }
+        try {
+            String fromSecurity = SecurityUtils.getToken();
+            if (StringUtils.isNotBlank(fromSecurity)) {
+                return stripBearerPrefix(fromSecurity);
+            }
+        } catch (Exception ignored) {
+            // 非 Web 上下文或无登录态
+        }
+        return null;
+    }
+
+    public static String resolveAccessToken(IssueCloudConfig config) {
+        if (config != null && StringUtils.isNotBlank(config.getToken())) {
+            return stripBearerPrefix(config.getToken());
+        }
+        return resolveAccessToken();
+    }
+
+    private static String resolveAuthorizationHeader() {
+        ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attrs == null) {
+            return null;
+        }
+        return attrs.getRequest().getHeader("Authorization");
+    }
+
+    private static String stripBearerPrefix(String token) {
+        if (token == null) {
+            return null;
+        }
+        String trimmed = token.trim();
+        if (trimmed.regionMatches(true, 0, "Bearer ", 0, 7)) {
+            return trimmed.substring(7).trim();
+        }
+        return trimmed;
+    }
+
     public static void fillOnInsert(CloudAuditEntity entity) {
         LocalDateTime now = LocalDateTime.now();
         if (entity.getTenantId() == null) {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -25,4 +25,5 @@ public class CloudConfigResponse {
     private String updatedBy;
     private String createdBy;
     private LocalDateTime createdTime;
+    private String cloudAddress;
 }

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

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

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

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

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

@@ -1,5 +1,6 @@
 package com.usky.issue.service.vo;
 
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.usky.issue.service.enums.SyncDirection;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -18,6 +19,7 @@ import java.util.List;
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
+@JsonIgnoreProperties(ignoreUnknown = true)
 public class SyncPacket {
 
     private String tenantId;
@@ -27,4 +29,5 @@ public class SyncPacket {
     private Long batchVersion;
     private Boolean hasMore;
     private String nonce;
+
 }

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

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