From be328e3107a6f840637f78bad31d942d42d33e78 Mon Sep 17 00:00:00 2001
From: tangwei
Date: Thu, 13 Aug 2026 16:13:43 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../yfd/platform/config/SecurityConfig.java | 1 +
.../datasource/DynamicDataSource.java | 8 +-
.../fp/service/impl/FpRunServiceImpl.java | 4 +-
.../system/controller/DataSyncController.java | 93 +++
.../system/domain/DataSyncRequest.java | 49 ++
.../com/yfd/platform/utils/DataSyncUtil.java | 637 ++++++++++++++++++
6 files changed, 789 insertions(+), 3 deletions(-)
create mode 100644 backend/src/main/java/com/yfd/platform/system/controller/DataSyncController.java
create mode 100644 backend/src/main/java/com/yfd/platform/system/domain/DataSyncRequest.java
create mode 100644 backend/src/main/java/com/yfd/platform/utils/DataSyncUtil.java
diff --git a/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java b/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java
index 38674041..04ab7190 100644
--- a/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java
+++ b/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java
@@ -71,6 +71,7 @@ public class SecurityConfig {
// .requestMatchers("/system/**").permitAll()
// .requestMatchers("/qgcExport/**").permitAll()
.requestMatchers("/eng/**").permitAll()
+ .requestMatchers("/system/**").permitAll()
// .requestMatchers("/eq/**").permitAll()
// .requestMatchers("/env/**").permitAll()
// .requestMatchers("/warn/**").permitAll()
diff --git a/backend/src/main/java/com/yfd/platform/datasource/DynamicDataSource.java b/backend/src/main/java/com/yfd/platform/datasource/DynamicDataSource.java
index 8b525217..79dc807c 100644
--- a/backend/src/main/java/com/yfd/platform/datasource/DynamicDataSource.java
+++ b/backend/src/main/java/com/yfd/platform/datasource/DynamicDataSource.java
@@ -36,5 +36,11 @@ public class DynamicDataSource extends AbstractRoutingDataSource {
contextHolder.remove();
}
-
+ /**
+ * 根据数据源 key 获取目标数据源实例(如 dm-master / oracle-master)
+ */
+ public DataSource getDataSourceByKey(String key) {
+ Map
+ *
+ * 支持:源表名与目标表名不同、源字段与目标字段不同(通过 {@link DataSyncConfig#setColumnMapping} 配置映射)。
+ *
+ */
+@Component
+public class DataSyncUtil {
+
+ private static final Logger log = LoggerFactory.getLogger(DataSyncUtil.class);
+
+ @Resource
+ private DynamicDataSource dynamicDataSource;
+
+ // ==================== 项目数据源入口 ====================
+
+ /**
+ * 达梦(DM) → Oracle 数据同步(使用项目配置的 dm-master / oracle-master 数据源)
+ */
+ public DataSyncResult syncDmToOracle(DataSyncConfig config) {
+ return syncTable(DataSourceKeys.DM_MASTER, DataSourceKeys.ORACLE_MASTER, config);
+ }
+
+ /**
+ * Oracle → 达梦(DM) 数据同步(使用项目配置的 oracle-master / dm-master 数据源)
+ */
+ public DataSyncResult syncOracleToDm(DataSyncConfig config) {
+ return syncTable(DataSourceKeys.ORACLE_MASTER, DataSourceKeys.DM_MASTER, config);
+ }
+
+ /**
+ * 通用数据同步:从源数据源读取,MERGE INTO 写入目标数据源(使用项目配置的数据源 key)
+ *
+ * @param sourceDsKey 源数据源 key(如 dm-master / oracle-master)
+ * @param targetDsKey 目标数据源 key
+ * @param config 同步配置
+ */
+ public DataSyncResult syncTable(String sourceDsKey, String targetDsKey, DataSyncConfig config) {
+ DataSource sourceDataSource = dynamicDataSource.getDataSourceByKey(sourceDsKey);
+ DataSource targetDataSource = dynamicDataSource.getDataSourceByKey(targetDsKey);
+ if (sourceDataSource == null) {
+ throw new IllegalArgumentException("未找到数据源: " + sourceDsKey);
+ }
+ if (targetDataSource == null) {
+ throw new IllegalArgumentException("未找到数据源: " + targetDsKey);
+ }
+ return doSync(new JdbcTemplate(sourceDataSource), new JdbcTemplate(targetDataSource), config);
+ }
+
+ // ==================== 自定义连接入口 ====================
+
+ /**
+ * 使用调用方提供的连接信息同步数据(DM → Oracle / Oracle → DM 均适用)
+ *
+ * 典型场景:把 DM 库 {@code SD_FPSS_R} 同步到 Oracle 库 {@code SD_FPSS_R},
+ * 连接信息由调用方在请求中提供。
+ *
+ *
+ * @param config 同步配置
+ * @param sourceConn 源库连接信息(driverClassName 不填时按 URL 自动识别)
+ * @param targetConn 目标库连接信息
+ */
+ public DataSyncResult syncWithConnections(DataSyncConfig config, DbConnectionInfo sourceConn, DbConnectionInfo targetConn) {
+ if (sourceConn == null || StrUtil.isBlank(sourceConn.getUrl())) {
+ throw new IllegalArgumentException("源库连接信息(url)不能为空");
+ }
+ if (targetConn == null || StrUtil.isBlank(targetConn.getUrl())) {
+ throw new IllegalArgumentException("目标库连接信息(url)不能为空");
+ }
+ DruidDataSource sourceDataSource = buildDataSource(sourceConn);
+ DruidDataSource targetDataSource = buildDataSource(targetConn);
+ try {
+ return doSync(new JdbcTemplate(sourceDataSource), new JdbcTemplate(targetDataSource), config);
+ } finally {
+ sourceDataSource.close();
+ targetDataSource.close();
+ }
+ }
+
+ // ==================== 核心同步逻辑 ====================
+
+ /**
+ * 核心同步:从源 JdbcTemplate 分页读取,MERGE INTO 写入目标 JdbcTemplate
+ */
+ private DataSyncResult doSync(JdbcTemplate sourceJdbc, JdbcTemplate targetJdbc, DataSyncConfig config) {
+ long startTime = System.currentTimeMillis();
+ DataSyncResult result = new DataSyncResult();
+ result.setStartTime(startTime);
+ try {
+ validateConfig(config);
+
+ // 1. 源表列(未指定时自动读取源表结构)
+ List sourceColumns = normalizeColumns(config.getColumns());
+ if (CollUtil.isEmpty(sourceColumns)) {
+ sourceColumns = loadTableColumns(sourceJdbc, config.getSourceTable());
+ if (CollUtil.isEmpty(sourceColumns)) {
+ throw new IllegalArgumentException("未从源表 [" + config.getSourceTable() + "] 获取到任何列");
+ }
+ }
+
+ // 2. 目标表列:按 columnMapping 映射,未配置映射的列名保持一致
+ Map columnMapping = normalizeMapping(config.getColumnMapping());
+ List targetColumns = new ArrayList<>(sourceColumns.size());
+ for (String sourceColumn : sourceColumns) {
+ targetColumns.add(columnMapping.getOrDefault(sourceColumn, sourceColumn));
+ }
+
+ // 3. 主键列:keyColumns 配置为目标表列名,反查对应的源表列名用于取值
+ List keyColumns = normalizeColumns(config.getKeyColumns());
+ Map reverseMapping = new HashMap<>();
+ for (Map.Entry entry : columnMapping.entrySet()) {
+ reverseMapping.put(entry.getValue(), entry.getKey());
+ }
+ List keySourceColumns = new ArrayList<>(keyColumns.size());
+ for (String keyColumn : keyColumns) {
+ keySourceColumns.add(reverseMapping.getOrDefault(keyColumn, keyColumn));
+ }
+
+ // 4. 可选:同步前清空目标表(全量覆盖)
+ if (Boolean.TRUE.equals(config.getClearTarget())) {
+ clearTargetTable(targetJdbc, config.getTargetTable());
+ }
+
+ // 5. 分页读取源数据 → 批量 MERGE 写入目标库
+ int batchSize = config.getBatchSize() != null && config.getBatchSize() > 0
+ ? config.getBatchSize() : 500;
+ long totalRead = 0;
+ long totalProcessed = 0;
+ int offset = 0;
+
+ while (true) {
+ List