Repository: tapdata/tapdata (language: python)

Issue:
Fix tap 2436 sqlexecute error develop



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/mr-ci.yaml b/.github/workflows/mr-ci.yaml
index bc44314d0b..fe5fb042c9 100644
--- a/.github/workflows/mr-ci.yaml
+++ b/.github/workflows/mr-ci.yaml
@@ -156,11 +156,18 @@ jobs:
         run: |
           apt install -y jq
       - name: SonarQube Quality Gate check
+        id: sonar
         uses: sonarsource/sonarqube-quality-gate-action@master
         env:
           SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
         with:
           scanMetadataReportFile: tapdata/target/sonar/report-task.txt
+      - name: Send SonarQube Quality Gate to Pr Comment
+        if: ${{ always() && steps.sonar.outcome == 'failure' }}
+        run: |
+          cd tapdata/build/ && bash check_sonarqube.sh --project-key=tapdata --branch=${{ env.OPENSOURCE_BRANCH }} \
+            --sonar-token=${{ secrets.SONAR_TOKEN }} --github-token=${{ secrets.TAPDATA_ENT_CICD_TOKEN }} \
+            --repo=tapdata --pr-number=${{ github.event.pull_request.number }}
 
   Build-And-Deploy:
     if: ${{ github.event_name == 'schedule' || inputs.mrci_run }}
diff --git a/build/check_sonarqube.sh b/build/check_sonarqube.sh
new file mode 100644
index 0000000000..c557189fc0
--- /dev/null
+++ b/build/check_sonarqube.sh
@@ -0,0 +1,90 @@
+#!/bin/bash
+
+. log.sh
+
+BASE_URI='http://58.251.34.123:29000'
+PROJECT_KEY=''
+BRANCH=''
+GITHUB_TOKEN=""
+OWNER="tapdata"
+REPO=""
+PR_NUMBER=
+
+which jq > /dev/null
+if [[ $? -ne 0 ]]; then
+  error "jq is not install."
+fi
+
+for arg in "$@"
+do
+  case $arg in
+    --project-key=*)
+    PROJECT_KEY="${arg#*=}"
+    shift
+    ;;
+    --branch=*)
+    BRANCH="${arg#*=}"
+    shift
+    ;;
+    --sonar-token=*)
+    SONAR_TOKEN="${arg#*=}"
+    shift
+    ;;
+    --github-token=*)
+    GITHUB_TOKEN="${arg#*=}"
+    shift
+    ;;
+    --repo=*)
+    REPO="${arg#*=}"
+    shift
+    ;;
+    --pr-number=*)
+    PR_NUMBER="${arg#*=}"
+    shift
+    ;;
+  esac
+done
+
+if [[ -z $PROJECT_KEY ]]; then
+  error 'Project Key is not set.'
+fi
+
+if [[ -z $BRANCH ]]; then
+  error 'Branch is not set.'
+fi
+
+if [[ -z $SONAR_TOKEN ]]; then
+  error 'variable $SONAR_TOKEN is not set.'
+fi
+
+info "Get Sonar Scan Result"
+result=$(curl -L "$BASE_URI/api/qualitygates/project_status?projectKey=$PROJECT_KEY&branch=$BRANCH" -u "$SONAR_TOKEN:" 2>/dev/null)
+
+QUALITY_GATE_STATUS=$(echo $result | jq -r .projectStatus.status)
+CONDITIONS=$(echo $result | jq -c .projectStatus.conditions[])
+
+COMMENT="SonarQube Quality Gate Status: **$QUALITY_GATE_STATUS**\n\n"
+
+if [[ $QUALITY_GATE_STATUS == "ERROR" ]]; then
+  warn "Quality Gate Status: $QUALITY_GATE_STATUS"
+  for condition in $CONDITIONS
+  do
+    status=$(echo "$condition" | jq -r '.status')
+    metricKey=$(echo "$condition" | jq -r '.metricKey')
+    actualValue=$(echo "$condition" | jq -r '.actualValue')
+    if [[ $status == "ERROR" ]]; then
+      warn "Status: $status, MetricKey: $metricKey, ActualValue: $actualValue"
+      COMMENT+="- Status: **$status**, MetricKey: **$metricKey**, ActualValue: **$actualValue**\n"
+    fi
+  done
+  COMMENT+="\n\nSee Sonar Scan Result at: $BASE_URI/dashboard?branch=$BRANCH&id=$PROJECT_KEY"
+  info "Send message to Github Pr Comment"
+  if [[ -z $PR_NUMBER ]]; then
+    warn "variable PR_NUMBER is not set, sending termination."
+  else
+    curl -s -H "Authorization: token $GITHUB_TOKEN" -X POST -d "{\"body\": \"$COMMENT\"}" "https://api.github.com/repos/$OWNER/$REPO/issues/$PR_NUMBER/comments" > /dev/null
+    if [[ $? -ne 0 ]]; then
+      error "Send message to Github Pr Comment Failed"
+    fi
+  fi
+fi
diff --git a/build/image/docker-entrypoint.sh b/build/image/docker-entrypoint.sh
index dccfaaf381..45d040f25a 100755
--- a/build/image/docker-entrypoint.sh
+++ b/build/image/docker-entrypoint.sh
@@ -76,7 +76,7 @@ wait_tm_start() {
         local seconds_left=$((timeout - SECONDS))
         printf "\r* Wait Starting, Left %02d / 300 Seconds..." "$seconds_left"
         sleep 1
-        curl "http://localhost:3000" &> /dev/null
+        curl --fail "http://localhost:3000" &> /dev/null
         if [[ $? -ne 0 ]]; then
             continue
         else
diff --git a/iengine/build/start.sh b/iengine/build/start.sh
index 219bd40c55..a40fc82822 100644
--- a/iengine/build/start.sh
+++ b/iengine/build/start.sh
@@ -3,7 +3,10 @@ ulimit -c unlimited
 sbin_file="tapdata-agent.jar"
 export app_type=DAAS
 export backend_url=http://127.0.0.1:3000/api/
-export TAPDATA_MONGO_URI='mongodb://127.0.0.1:27017/tapdata?authSource=admin'
+if [[ -z $MONGO_URI ]]; then
+  MONGO_URI="mongodb://127.0.0.1:27017/tapdata"
+fi
+export TAPDATA_MONGO_URI=$MONGO_URI
 
 mkdir -p logs/iengine && touch logs/iengine/$sbin_file.log
 nohup java -jar components/$sbin_file &> logs/iengine/$sbin_file.log &
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/Schedule/WorkerHeatBeatReports.java b/iengine/iengine-app/src/main/java/io/tapdata/Schedule/WorkerHeatBeatReports.java
index 979b036b88..edb50c1c1f 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/Schedule/WorkerHeatBeatReports.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/Schedule/WorkerHeatBeatReports.java
@@ -8,6 +8,7 @@
 import io.tapdata.utils.AppType;
 import org.apache.commons.lang3.StringUtils;
 
+import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.function.BiConsumer;
@@ -57,6 +58,7 @@ public void report(boolean isExit) {
 		value.put("usedMemory", usedMemory);
 		value.put("metricValues", connectorManager.getMetricValues());
 		value.put("worker_type", ConnectorConstant.WORKER_TYPE_CONNECTOR);
+		value.put("worker_date", new Date());
 
 		connectorManager.setPlatformInfo(value);
 		configIfNotBlank("version", value::put);
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastPdkBaseNode.java b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastPdkBaseNode.java
index 5141d1b887..170c8d22e0 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastPdkBaseNode.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastPdkBaseNode.java
@@ -57,10 +57,7 @@
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.Update;
 
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.TimeUnit;
 
@@ -82,6 +79,7 @@ public abstract class HazelcastPdkBaseNode extends HazelcastDataBaseNode {
 
 	protected Integer readBatchSize;
 	protected Integer increaseReadSize;
+	private static final String DOUBLE_ACTIVE = "doubleActive";
 	protected TapRecordSkipDetector skipDetector;
 	private PdkStateMap pdkStateMap;
 
@@ -191,14 +189,7 @@ protected void createPdkConnectorNode(DataProcessorContext dataProcessorContext,
 		Node<?> node = dataProcessorContext.getNode();
 		ConnectorCapabilities connectorCapabilities = ConnectorCapabilities.create();
 		initDmlPolicy(node, connectorCapabilities);
-		Map<String, Object> nodeConfig = null;
-		if (node instanceof TableNode) {
-			nodeConfig = ((TableNode) node).getNodeConfig();
-		} else if (node instanceof DatabaseNode) {
-			nodeConfig = ((DatabaseNode) node).getNodeConfig();
-		} else if (node instanceof LogCollectorNode) {
-			nodeConfig = ((LogCollectorNode) node).getNodeConfig();
-		}
+		Map<String, Object> nodeConfig = generateNodeConfig(node, taskDto.getDoubleActive());
 		this.associateId = ConnectorNodeService.getInstance().putConnectorNode(
 				PdkUtil.createNode(taskDto.getId().toHexString(),
 						databaseType,
@@ -219,6 +210,22 @@ protected void createPdkConnectorNode(DataProcessorContext dataProcessorContext,
 		AspectUtils.executeAspect(PDKNodeInitAspect.class, () -> new PDKNodeInitAspect().dataProcessorContext((DataProcessorContext) processorBaseContext));
 	}
 
+	protected Map<String, Object> generateNodeConfig(Node<?> node, Boolean doubleActive) {
+		Map<String, Object> nodeConfig = null;
+		if (node instanceof TableNode) {
+			nodeConfig = ((TableNode) node).getNodeConfig();
+		} else if (node instanceof DatabaseNode) {
+			nodeConfig = ((DatabaseNode) node).getNodeConfig();
+		} else if (node instanceof LogCollectorNode) {
+			nodeConfig = ((LogCollectorNode) node).getNodeConfig();
+		}
+		if (null == nodeConfig) {
+			nodeConfig = new HashMap<>();
+		}
+		nodeConfig.put(DOUBLE_ACTIVE, doubleActive);
+		return nodeConfig;
+	}
+
 	protected void initDmlPolicy(Node<?> node, ConnectorCapabilities connectorCapabilities) {
 		if (node instanceof DataParentNode && null != ((DataParentNode<?>) node).getDmlPolicy()) {
 			DmlPolicy dmlPolicy = ((DataParentNode<?>) node).getDmlPolicy();
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastSourcePdkDataNode.java b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastSourcePdkDataNode.java
index 96340cc408..db44479425 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastSourcePdkDataNode.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastSourcePdkDataNode.java
@@ -14,6 +14,7 @@
 import com.tapdata.tm.commons.dag.nodes.CacheNode;
 import com.tapdata.tm.commons.dag.nodes.TableNode;
 import com.tapdata.tm.commons.task.dto.TaskDto;
+import com.tapdata.tm.utils.TimeTransFormationUtil;
 import io.tapdata.Runnable.LoadSchemaRunner;
 import io.tapdata.aspect.*;
 import io.tapdata.aspect.taskmilestones.*;
@@ -412,9 +413,9 @@ protected void doSnapshot(List<String> tableList) {
 																		obsLogger.info("Execute result is null");
 																		return;
 																	}
-																	List<Map<String, Object>> maps = (List<Map<String, Object>>) executeResult.getResult();
-																	List<TapEvent> events = maps.stream().map(m -> TapSimplify.insertRecordEvent(m, tableName)).collect(Collectors.toList());
-																	consumer.accept(events, null);
+
+																	Object result= executeResult.getResult();
+																	handleCustomCommandResult(result,tableName,consumer);
 																});
 															} else {
 																batchReadFunction.batchRead(connectorNode.getConnectorContext(), tapTable, tableOffset, readBatchSize, consumer);
@@ -466,6 +467,16 @@ protected void doSnapshot(List<String> tableList) {
 		executeAspect(new SnapshotReadEndAspect().dataProcessorContext(dataProcessorContext));
 	}
 
+	private void handleCustomCommandResult(Object result, String tableName, BiConsumer<List<TapEvent>, Object> consumer){
+		if (result instanceof List) {
+			List<Map<String, Object>> maps = (List<Map<String, Object>>) result;
+			List<TapEvent> events = maps.stream().map(m -> TapSimplify.insertRecordEvent(m, tableName)).collect(Collectors.toList());
+			consumer.accept(events, null);
+		}else {
+			obsLogger.info("The execution result is:{}, because the result is not a list it will be ignored.",result);
+		}
+	}
+
 	private void createTargetIndex(List<String> updateConditionFields, boolean createUnique, String tableId, TapTable tapTable) {
 		CreateIndexFunction createIndexFunction = getConnectorNode().getConnectorFunctions().getCreateIndexFunction();
 		if (null == createIndexFunction) {
@@ -1195,7 +1206,7 @@ private TapAdvanceFilter batchFilterRead() {
 		TableNode tableNode = (TableNode) dataProcessorContext.getNode();
 		TapAdvanceFilter tapAdvanceFilter = new TapAdvanceFilter();
 		if (isTableFilter(tableNode)) {
-			List<QueryOperator> conditions = tableNode.getConditions();
+			List<QueryOperator> conditions = timeTransformation(tableNode.getConditions(),tableNode.getOffsetHours());
 			if (CollectionUtils.isNotEmpty(conditions)) {
 				String tableName = tableNode.getTableName();
 				TapTable tapTable = dataProcessorContext.getTapTableMap().get(tableName);
@@ -1240,6 +1251,38 @@ private TapAdvanceFilter batchFilterRead() {
 		return tapAdvanceFilter;
 	}
 
+	protected List<QueryOperator> timeTransformation(List<QueryOperator> conditions,Long offsetHours){
+		List<QueryOperator> finalConditions = new ArrayList<>();
+		for(QueryOperator queryOperator : conditions){
+			LocalDateTime currentDateTime = LocalDateTime.now();
+			if(queryOperator.isFastQuery()){
+				List<String> timeList = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,offsetHours);
+				List<QueryOperator> result = constructQueryOperator(timeList,queryOperator);
+				if(CollectionUtils.isNotEmpty(result))finalConditions.addAll(result);
+			}else{
+				finalConditions.add(queryOperator);
+			}
+		}
+		return finalConditions;
+	}
+
+	protected List<QueryOperator> constructQueryOperator(List<String> timeList,QueryOperator queryOperator){
+		List<QueryOperator> result = new ArrayList<>();
+		if(CollectionUtils.isEmpty(timeList))return result;
+		QueryOperator start = new QueryOperator();
+		start.setKey(queryOperator.getKey());
+		start.setOperator(QueryOperator.GTE);
+		start.setValue(timeList.get(0));
+		QueryOperator end = new QueryOperator();
+		end.setKey(queryOperator.getKey());
+		end.setOperator(QueryOperator.LTE);
+		end.setValue(timeList.get(1));
+		result.add(start);
+		result.add(end);
+		return result;
+	}
+
+
 	protected void excludeRemoveTable(List<String> tableNames) {
 		if (null == tableNames) {
 			return;
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkBaseNode.java b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkBaseNode.java
index 28b0542c05..55c8991584 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkBaseNode.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkBaseNode.java
@@ -365,12 +365,12 @@ public List<String> apply(TapEvent tapEvent) {
 		}
 	}
 
-	private boolean isCDCConcurrent(Boolean cdcConcurrent) {
+	protected boolean isCDCConcurrent(Boolean cdcConcurrent) {
 		cdcConcurrent = cdcConcurrent && cdcConcurrentWriteNum > 1;
 		List<? extends Node<?>> predecessors = getNode().predecessors();
 		for (Node<?> predecessor : predecessors) {
-			if (predecessor instanceof MergeTableNode) {
-				obsLogger.info("CDC concurrent write is disabled because the node has a merge table node");
+			if (predecessor instanceof MergeTableNode || predecessor instanceof UnwindProcessNode) {
+				obsLogger.info("CDC concurrent write is disabled because the node has a merge table node or unwind process node");
 				cdcConcurrent = false;
 			}
 		}
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/concurrent/PartitionConcurrentProcessor.java b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/concurrent/PartitionConcurrentProcessor.java
index 58615cfe33..acdb7e4b43 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/concurrent/PartitionConcurrentProcessor.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/concurrent/PartitionConcurrentProcessor.java
@@ -279,8 +279,7 @@ protected TapdataEvent processDML(TapdataEvent tapdataEvent, AtomicBoolean singl
 			if (toSingleMode(tapEvent, partitionValue, singleMode)) {
 				partition = 0;
 			} else {
-				final List<Object> partitionOriginalValues = keySelector.convert2OriginValue(partitionValue);
-				final PartitionResult<TapdataEvent> partitionResult = partitioner.partition(partitionSize, tapdataEvent, partitionOriginalValues);
+				final PartitionResult<TapdataEvent> partitionResult = partitioner.partition(partitionSize, tapdataEvent, partitionValue);
 				partition = partitionResult.getPartition() < 0 ? DEFAULT_PARTITION : partitionResult.getPartition();
 			}
 
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/concurrent/selector/TapEventPartitionKeySelector.java b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/concurrent/selector/TapEventPartitionKeySelector.java
index 06863858d8..7ebf6ff5bc 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/concurrent/selector/TapEventPartitionKeySelector.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/concurrent/selector/TapEventPartitionKeySelector.java
@@ -35,6 +35,9 @@ public List<Object> select(TapEvent tapEvent, Map<String, Object> row) {
 			getPartitionValue(partitionValue, keys, row);
 		}
 
+		if (null != partitionValue && !CollectionUtils.isEmpty(partitionValue)) {
+			partitionValue = convert2OriginValue(partitionValue);
+		}
 		return partitionValue;
 	}
 
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastUnwindProcessNode.java b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastUnwindProcessNode.java
index 155e844b71..f56a6bbbe5 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastUnwindProcessNode.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastUnwindProcessNode.java
@@ -1,18 +1,24 @@
 package io.tapdata.flow.engine.V2.node.hazelcast.processor;
 
 import java.util.List;
+import java.util.Map;
 import java.util.function.BiConsumer;
 
 import com.tapdata.entity.TapdataEvent;
 import com.tapdata.entity.task.context.ProcessorBaseContext;
+import com.tapdata.tm.commons.dag.ArrayModel;
 import com.tapdata.tm.commons.dag.Node;
+import com.tapdata.tm.commons.dag.UnwindModel;
 import com.tapdata.tm.commons.dag.process.UnwindProcessNode;
 import io.tapdata.entity.event.TapEvent;
+import io.tapdata.entity.event.dml.TapDeleteRecordEvent;
 import io.tapdata.entity.event.dml.TapRecordEvent;
 import io.tapdata.exception.TapCodeException;
 import io.tapdata.flow.engine.V2.node.hazelcast.processor.unwind.EventHandel;
+import io.tapdata.flow.engine.V2.node.hazelcast.processor.unwind.UnWindNodeUtil;
 import io.tapdata.flow.engine.V2.util.TapEventUtil;
 import lombok.SneakyThrows;
+import org.apache.commons.collections4.CollectionUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.jetbrains.annotations.NotNull;
@@ -56,9 +62,7 @@ protected void tryProcess(TapdataEvent tapdataEvent, BiConsumer<TapdataEvent, Pr
         }
 
         List<TapEvent> eventList = EventHandel.getHandelResult(node, tapdataEvent.getTapEvent());
-        if (null == eventList || eventList.isEmpty()) {
-            consumer.accept(tapdataEvent, processResult);
-        } else {
+        if (CollectionUtils.isNotEmpty(eventList)) {
             for (TapEvent e : eventList) {
                 TapdataEvent cloneTapdataEvent = (TapdataEvent) tapdataEvent.clone();
                 cloneTapdataEvent.setTapEvent(e);
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/unwind/EventHandel.java b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/unwind/EventHandel.java
index b05b902f63..02dd2236f2 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/unwind/EventHandel.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/unwind/EventHandel.java
@@ -109,16 +109,8 @@ public void copyEvent(List<TapEvent> events, Map<String, Object> item, TapEvent
     public List<TapEvent> handel(UnwindProcessNode node, TapEvent event){
         List<TapEvent> events = new ArrayList<>();
         Map<String, Object> after = UnWindNodeUtil.getAfter(event);
-        Map<String, Object> before = UnWindNodeUtil.getBefore(event);
         Long referenceTime = ((TapUpdateRecordEvent) event).getReferenceTime();
-        TapDeleteRecordEvent delete = TapDeleteRecordEvent.create();
-        if (null == before || before.isEmpty()) {
-            delete.before(after);
-        } else {
-            delete.before(before);
-        }
-        delete.referenceTime(referenceTime);
-        List<TapEvent> deletes = EventHandel.getHandelResult(node, delete);
+        List<TapEvent> deletes = EventHandel.getHandelResult(node, UnWindNodeUtil.genericDeleteEvent(event));
         if (null != deletes) {
             events.addAll(deletes);
         }
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/unwind/UnWindNodeUtil.java b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/unwind/UnWindNodeUtil.java
index d89746d3be..e9b8391833 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/unwind/UnWindNodeUtil.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/unwind/UnWindNodeUtil.java
@@ -5,9 +5,9 @@
 import io.tapdata.entity.event.TapEvent;
 import io.tapdata.entity.event.dml.TapDeleteRecordEvent;
 import io.tapdata.entity.event.dml.TapInsertRecordEvent;
+import io.tapdata.entity.event.dml.TapRecordEvent;
 import io.tapdata.entity.event.dml.TapUpdateRecordEvent;
 import org.apache.commons.lang3.StringUtils;
-import org.bson.Document;
 
 
 import java.util.ArrayList;
@@ -168,14 +168,7 @@ public static  boolean collection(Collection<?> result,
                                       Boolean flatten,
                                       String joiner) {
         if (result.isEmpty()) {
-            if (preserveNullAndEmptyArrays) {
-                if (map.containsKey(path)) {
-                    parentMap.remove(path);
-                } else {
-                    parentMap.remove(split[split.length - 1]);
-                }
-                events.add(event);
-            }
+            filterByPreserveNullAndEmptyArrays(events, preserveNullAndEmptyArrays, map, path, parentMap, split, event);
             return true;
         }
         int index = 0;
@@ -203,14 +196,7 @@ public static  boolean array(Object[] arr,
                                  String joiner
                                  ) {
         if (arr.length < 1) {
-            if (preserveNullAndEmptyArrays) {
-                if (map.containsKey(path)) {
-                    parentMap.remove(path);
-                } else {
-                    parentMap.remove(split[split.length - 1]);
-                }
-                events.add(event);
-            }
+            filterByPreserveNullAndEmptyArrays(events, preserveNullAndEmptyArrays, map, path, parentMap, split, event);
             return true;
         }
         for (int index = 0; index < arr.length; index++) {
@@ -230,12 +216,14 @@ public static  boolean object(Object result,
                            Map<String, Object> parentMap,
                            TapEvent event,
                            EventHandel handel) {
-        if (!(null == result && !preserveNullAndEmptyArrays)) {
-            if (containsKey.get()) {
-                containsPathAndSetValue(parentMap, includeArrayIndex, null);
-            }
-            events.add(event);
+        if (containsKey.get()) {
+            containsPathAndSetValue(parentMap, includeArrayIndex, null);
         }
+        if (containsKey.get() && null == result && !preserveNullAndEmptyArrays) {
+            addEvent(event,events);
+            return true;
+        }
+        events.add(event);
         return false;
     }
 
@@ -254,6 +242,8 @@ public static  List<TapEvent> handelList(UnwindProcessNode node, TapEvent event,
             if (null == parentMap || !containsKey.get()) {
                 if (preserveNullAndEmptyArrays) {
                     events.add(event);
+                } else {
+                    addEvent(event, events);
                 }
                 return events;
             }
@@ -287,4 +277,54 @@ public static void serializationFlattenFields(String path,Map<String, Object> ma
         }
     }
 
+    public static TapDeleteRecordEvent toDeleteEvent(TapRecordEvent event) {
+        if (event instanceof TapDeleteRecordEvent) {
+            return (TapDeleteRecordEvent) event;
+        } else if(event instanceof TapUpdateRecordEvent) {
+            return genericDeleteEvent(event);
+        }
+        return null;
+    }
+
+    public static TapDeleteRecordEvent genericDeleteEvent(TapEvent event) {
+        Map<String, Object> after = UnWindNodeUtil.getAfter(event);
+        Map<String, Object> before = UnWindNodeUtil.getBefore(event);
+        Long referenceTime = ((TapUpdateRecordEvent) event).getReferenceTime();
+        TapDeleteRecordEvent delete = TapDeleteRecordEvent.create();
+        if (null == before || before.isEmpty()) {
+            delete.before(after);
+        } else {
+            delete.before(before);
+        }
+        delete.referenceTime(referenceTime);
+        return delete;
+    }
+
+    public static void filterByPreserveNullAndEmptyArrays(List<TapEvent> events,
+                                                          boolean preserveNullAndEmptyArrays,
+                                                          Map<String, Object> map,
+                                                          String path,
+                                                          Map<String, Object> parentMap,
+                                                          String[] split,
+                                                          TapEvent event) {
+        if (map.containsKey(path)) {
+            parentMap.remove(path);
+        } else {
+            parentMap.remove(split[split.length - 1]);
+        }
+        if (preserveNullAndEmptyArrays) {
+            events.add(event);
+        } else {
+            addEvent(event, events);
+        }
+    }
+
+    public static void addEvent(TapEvent event, List<TapEvent> events) {
+        TapDeleteRecordEvent e = toDeleteEvent((TapRecordEvent) event);
+        if (null != e) {
+            events.add(e);
+        }
+    }
+
+
 }
diff --git a/iengine/iengine-common/src/main/java/com/tapdata/entity/dataflow/batch/BatchOffset.java b/iengine/iengine-common/src/main/java/com/tapdata/entity/dataflow/batch/BatchOffset.java
index 6f093dd56d..cd13663905 100644
--- a/iengine/iengine-common/src/main/java/com/tapdata/entity/dataflow/batch/BatchOffset.java
+++ b/iengine/iengine-common/src/main/java/com/tapdata/entity/dataflow/batch/BatchOffset.java
@@ -2,8 +2,6 @@
 
 import com.tapdata.constant.MapUtil;
 import com.tapdata.exception.CloneException;
-import io.tapdata.entity.utils.InstanceFactory;
-import io.tapdata.entity.utils.ObjectSerializable;
 import org.apache.commons.lang3.SerializationUtils;
 
 import java.io.Serializable;
@@ -50,7 +48,7 @@ public void setStatus(String status) {
 	public Object clone() throws CloneNotSupportedException {
 		Object clone = super.clone();
 		if (clone instanceof BatchOffset) {
-			BatchOffset batchOffset = (BatchOffset) super.clone();
+			BatchOffset batchOffset = (BatchOffset) clone;
 			batchOffset.setStatus(status);
 			if (offset instanceof Map) {
 				Map<String, Object> newOffset = new HashMap<>();
@@ -63,8 +61,7 @@ public Object clone() throws CloneNotSupportedException {
 			} else if (offset instanceof Serializable) {
 				batchOffset.setOffset(SerializationUtils.clone((Serializable) offset));
 			} else {
-				byte[] bytes = InstanceFactory.instance(ObjectSerializable.class).fromObject(offset);
-				batchOffset.setOffset(InstanceFactory.instance(ObjectSerializable.class).toObject(bytes));
+				batchOffset.setOffset(offset);
 			}
 		}
 		return clone;
diff --git a/iengine/modules/milestone-module/pom.xml b/iengine/modules/milestone-module/pom.xml
index d2cf0f847c..404bb3dcc8 100644
--- a/iengine/modules/milestone-module/pom.xml
+++ b/iengine/modules/milestone-module/pom.xml
@@ -48,6 +48,12 @@
 				<version>4.11.0</version>
 				<scope>test</scope>
 			</dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+            <version>5.3.14</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/iengine/modules/milestone-module/src/main/java/io/tapdata/milestone/MilestoneAspectTask.java b/iengine/modules/milestone-module/src/main/java/io/tapdata/milestone/MilestoneAspectTask.java
index 17012fe257..dd10da9c25 100644
--- a/iengine/modules/milestone-module/src/main/java/io/tapdata/milestone/MilestoneAspectTask.java
+++ b/iengine/modules/milestone-module/src/main/java/io/tapdata/milestone/MilestoneAspectTask.java
@@ -30,6 +30,11 @@
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import static io.tapdata.aspect.TableInitFuncAspect.STATE_END;
+import static io.tapdata.aspect.TableInitFuncAspect.STATE_START;
+import static io.tapdata.aspect.TableInitFuncAspect.STATE_PROCESS;
 
 /**
  * 设计要点：
@@ -50,17 +55,17 @@
 @AspectTaskSession(includeTypes = {TaskDto.SYNC_TYPE_MIGRATE, TaskDto.SYNC_TYPE_SYNC, TaskDto.SYNC_TYPE_CONN_HEARTBEAT, TaskDto.SYNC_TYPE_LOG_COLLECTOR,TaskDto.SYNC_TYPE_MEM_CACHE})
 public class MilestoneAspectTask extends AbstractAspectTask {
 
-    private final static String KPI_TASK = "TASK";
-    private final static String KPI_DATA_NODE_INIT = "DATA_NODE_INIT";
-    private final static String KPI_NODE = "NODE";
-    private final static String KPI_SNAPSHOT = "SNAPSHOT";
-    private final static String KPI_CDC = "CDC";
-    private final static String KPI_SNAPSHOT_READ = "SNAPSHOT_READ";
-    private final static String KPI_OPEN_CDC_READ = "OPEN_CDC_READ";
-    private final static String KPI_CDC_READ = "CDC_READ";
-    private final static String KPI_SNAPSHOT_WRITE = "SNAPSHOT_WRITE";
-    private final static String KPI_CDC_WRITE = "CDC_WRITE";
-    private final static String KPI_TABLE_INIT = "TABLE_INIT";
+    protected static final String KPI_TASK = "TASK";
+    protected static final String KPI_DATA_NODE_INIT = "DATA_NODE_INIT";
+    protected static final String KPI_NODE = "NODE";
+    protected static final String KPI_SNAPSHOT = "SNAPSHOT";
+    protected static final String KPI_CDC = "CDC";
+    protected static final String KPI_SNAPSHOT_READ = "SNAPSHOT_READ";
+    protected static final String KPI_OPEN_CDC_READ = "OPEN_CDC_READ";
+    protected static final String KPI_CDC_READ = "CDC_READ";
+    protected static final String KPI_SNAPSHOT_WRITE = "SNAPSHOT_WRITE";
+    protected static final String KPI_CDC_WRITE = "CDC_WRITE";
+    protected static final String KPI_TABLE_INIT = "TABLE_INIT";
 
     private final Map<String, MilestoneEntity> milestones = new ConcurrentHashMap<>();
     private final Map<String, Map<String, MilestoneEntity>> nodeMilestones = new ConcurrentHashMap<>();
@@ -73,6 +78,10 @@ public class MilestoneAspectTask extends AbstractAspectTask {
 	private final AtomicLong snapshotTableProgress = new AtomicLong(0);
 
     public MilestoneAspectTask() {
+        init();
+    }
+
+    protected void init() {
         observerHandlers.register(PDKNodeInitAspect.class, this::handlePDKNodeInit);
         observerHandlers.register(DataNodeInitAspect.class, this::handleDataNodeInit);
         observerHandlers.register(DataNodeCloseAspect.class, this::handleDataNodeClose);
@@ -89,14 +98,12 @@ public MilestoneAspectTask() {
         nodeRegister(SnapshotReadEndAspect.class, KPI_SNAPSHOT_READ, (aspect, m) -> setFinish(m));
         nodeRegister(SnapshotReadErrorAspect.class, KPI_SNAPSHOT_READ, (aspect, m) -> {
             setError(aspect, m);
-            taskMilestone(KPI_SNAPSHOT, (tm) -> setError(aspect, tm));
+            taskMilestone(KPI_SNAPSHOT, tm -> setError(aspect, tm));
         });
         nodeRegister(SnapshotReadTableEndAspect.class, KPI_SNAPSHOT_READ, (aspect, m) -> {
             m.addProgress(1);
             snapshotTableProgress.addAndGet(1);
-            taskMilestone(KPI_SNAPSHOT, (tm) -> {
-                tm.setProgress(snapshotTableProgress.get());
-            });
+            taskMilestone(KPI_SNAPSHOT, tm -> tm.setProgress(snapshotTableProgress.get()));
         });
         nodeRegister(Snapshot2CDCAspect.class, KPI_SNAPSHOT_READ, (aspect, m) -> {
             setFinish(m);
@@ -110,46 +117,43 @@ public MilestoneAspectTask() {
             taskMilestone(KPI_CDC, this::setRunning);
         });
         nodeRegister(CDCReadStartedAspect.class, (nodeId, aspect) -> {
-            nodeKpi(nodeId, KPI_OPEN_CDC_READ, this::setFinish);
-            nodeKpi(nodeId, KPI_CDC_READ, this::setRunning);
+            nodeMilestones(nodeId, KPI_OPEN_CDC_READ, this::setFinish);
+            nodeMilestones(nodeId, KPI_CDC_READ, this::setRunning);
         });
-        nodeRegister(CDCReadErrorAspect.class, (nodeId, aspect) -> {
-            nodeKpi(nodeId, KPI_OPEN_CDC_READ, (m) -> {
-                if (null == m.getEnd()) {
-                    setError(aspect, m);
-                } else {
-                    nodeKpi(nodeId, KPI_CDC_READ, (m2) -> setError(aspect, m2));
-                }
-            });
-        });
-//        nodeRegister(CDCReadEndAspect.class, KPI_STREAM_READ, (aspect, m) -> setFinish(m));
+        nodeRegister(CDCReadErrorAspect.class, (nodeId, aspect) -> nodeMilestones(nodeId, KPI_OPEN_CDC_READ, m -> {
+            if (null == m.getEnd()) {
+                setError(aspect, m);
+            } else {
+                nodeMilestones(nodeId, KPI_CDC_READ, m2 -> setError(aspect, m2));
+            }
+        }));
         nodeRegister(SnapshotWriteBeginAspect.class, KPI_SNAPSHOT_WRITE, (aspect, m) -> setRunning(m));
         nodeRegister(SnapshotWriteEndAspect.class, KPI_SNAPSHOT_WRITE, (aspect, m) -> {
             setFinish(m);
-						if (snapshotTableProgress.get() >= snapshotTableCounts.get()) {
-							taskMilestone(KPI_SNAPSHOT, this::setFinish);
-						}
+            if (snapshotTableProgress.get() >= snapshotTableCounts.get()) {
+                taskMilestone(KPI_SNAPSHOT, this::setFinish);
+            }
         });
         nodeRegister(CDCWriteBeginAspect.class, (nodeId, aspect) -> {
             if (hasSnapshot()) {
-                nodeKpi(nodeId, KPI_SNAPSHOT_WRITE, this::setFinish); // fix status
+                nodeMilestones(nodeId, KPI_SNAPSHOT_WRITE, this::setFinish); // fix status
             }
-            nodeKpi(nodeId, KPI_CDC_WRITE, this::setRunning);
+            nodeMilestones(nodeId, KPI_CDC_WRITE, this::setRunning);
             taskMilestone(KPI_CDC, this::setFinish);
         });
-        nodeRegister(WriteErrorAspect.class, (nodeId, aspect) -> nodeKpi(nodeId, KPI_SNAPSHOT_WRITE, m -> {
-            if (null == m.getEnd()) {
-                setError(aspect, m);
-            } else {
-                nodeKpi(nodeId, KPI_CDC_WRITE, m2 -> setError(aspect, m2));
-            }
+        nodeRegister(WriteErrorAspect.class, (nodeId, aspect) -> nodeMilestones(
+                nodeId, KPI_SNAPSHOT_WRITE, m -> {
+                    if (null == m.getEnd()) {
+                        setError(aspect, m);
+                    } else {
+                        nodeMilestones(nodeId, KPI_CDC_WRITE, m2 -> setError(aspect, m2));
+                    }
         }));
     }
 
     @Override
     public void onStart(TaskStartAspect startAspect) {
         log.info("Start task milestones: {}({})", task.getId().toHexString(), task.getName());
-
         taskMilestone(KPI_TASK, this::setFinish);
         if (!TaskDto.SYNC_TYPE_LOG_COLLECTOR.equals(task.getSyncType())) {
             taskMilestone(KPI_TABLE_INIT, null);
@@ -180,80 +184,84 @@ public void onStop(TaskStopAspect stopAspect) {
         }
     }
 
-    private boolean hasSnapshot() {
+    protected boolean hasSnapshot() {
         return ParentTaskDto.TYPE_INITIAL_SYNC_CDC.equals(task.getType()) || ParentTaskDto.TYPE_INITIAL_SYNC.equals(task.getType());
     }
 
-    private boolean hasCdc() {
+    protected boolean hasCdc() {
         return ParentTaskDto.TYPE_INITIAL_SYNC_CDC.equals(task.getType()) || ParentTaskDto.TYPE_CDC.equals(task.getType());
     }
 
-    private Void handleDataNodeInit(DataNodeInitAspect aspect) {
+    protected Void handleDataNodeInit(DataNodeInitAspect aspect) {
         DataProcessorContext dataProcessorContext = aspect.getDataProcessorContext();
         String nodeId = nodeId(dataProcessorContext);
         nodeMilestones(nodeId, KPI_NODE, this::setRunning);
-			Node<?> node = dataProcessorContext.getNode();
-			if (null == node.predecessors() || node.predecessors().isEmpty()) {
-				if (node instanceof TableNode) {
-					snapshotTableCounts.addAndGet(1);
-				} else if (node instanceof DatabaseNode) {
-					DatabaseNode databaseNode = (DatabaseNode) node;
-					snapshotTableCounts.addAndGet(databaseNode.tableSize());
-				}
-				taskMilestone(KPI_SNAPSHOT, (tm) -> {
-					tm.setTotals(snapshotTableCounts.get());
-				});
-			}
+        taskMilestone(KPI_DATA_NODE_INIT, this::setRunning);
+        Node<?> node = dataProcessorContext.getNode();
+        List<? extends Node<?>> predecessors = node.predecessors();
+        if (null == predecessors || predecessors.isEmpty()) {
+            if (node instanceof TableNode) {
+                snapshotTableCounts.addAndGet(1);
+            } else if (node instanceof DatabaseNode) {
+                DatabaseNode databaseNode = (DatabaseNode) node;
+                snapshotTableCounts.addAndGet(databaseNode.tableSize());
+            }
+            taskMilestone(KPI_SNAPSHOT, tm -> tm.setTotals(snapshotTableCounts.get()));
+        }
         dataNodeInitMap.computeIfPresent(nodeId, (k, v) -> MilestoneStatus.RUNNING);
         return null;
     }
 
-    private Void handleDataNodeClose(DataNodeCloseAspect aspect) {
+    protected Void handleDataNodeClose(DataNodeCloseAspect aspect) {
         String nodeId = nodeId(aspect.getDataProcessorContext());
         nodeMilestones(nodeId, KPI_NODE, this::setFinish);
         dataNodeInitMap.computeIfPresent(nodeId, (k, v) -> MilestoneStatus.FINISH);
         return null;
     }
 
-    private Void handleProcessNodeInit(ProcessorNodeInitAspect aspect) {
+    protected Void handleProcessNodeInit(ProcessorNodeInitAspect aspect) {
         String nodeId = nodeId(aspect.getProcessorBaseContext());
         nodeMilestones(nodeId, KPI_NODE, this::setRunning);
+        taskMilestone(KPI_DATA_NODE_INIT, this::setRunning);
         return null;
     }
 
-    private Void handleProcessNodeClose(ProcessorNodeCloseAspect aspect) {
+    protected Void handleProcessNodeClose(ProcessorNodeCloseAspect aspect) {
         String nodeId = nodeId(aspect.getProcessorBaseContext());
         nodeMilestones(nodeId, KPI_NODE, this::setFinish);
         return null;
     }
 
-    private Void handlePDKNodeInit(PDKNodeInitAspect aspect) {
+    protected Void handlePDKNodeInit(PDKNodeInitAspect aspect) {
         String nodeId = nodeId(aspect.getDataProcessorContext());
         nodeMilestones(nodeId, KPI_NODE, this::setFinish);
         return null;
     }
 
-    private synchronized Void handleTableInit(TableInitFuncAspect aspect) {
+    protected synchronized Void handleTableInit(TableInitFuncAspect aspect) {
         String nodeId = nodeId(aspect.getDataProcessorContext());
         switch (aspect.getState()) {
-            case TableInitFuncAspect.STATE_START:
+            case STATE_START:
+                taskMilestone(KPI_TABLE_INIT, this::setRunning);
                 targetNodes.add(nodeId);
-                nodeMilestones(nodeId, KPI_TABLE_INIT, (milestone) -> {
+                nodeMilestones(nodeId, KPI_TABLE_INIT, milestone -> {
                     milestone.setProgress(0L);
                     milestone.setTotals(aspect.getTotals());
                     setRunning(milestone);
                 });
                 break;
-            case TableInitFuncAspect.STATE_PROCESS:
-                nodeMilestones(nodeId, KPI_TABLE_INIT, (milestone) -> {
+            case STATE_PROCESS:
+                taskMilestone(KPI_TABLE_INIT, this::setRunning);
+                nodeMilestones(nodeId, KPI_TABLE_INIT, milestone -> {
                     milestone.setProgress(aspect.getCompletedCounts());
                     milestone.setTotals(aspect.getTotals());
                 });
                 break;
-            case TableInitFuncAspect.STATE_END:
+            case STATE_END:
+                taskMilestone(KPI_TABLE_INIT, this::setFinish);
                 Throwable error = aspect.getThrowable();
                 if (null == error) {
-                    nodeMilestones(nodeId, KPI_TABLE_INIT, (milestone) -> {
+                    nodeMilestones(nodeId, KPI_TABLE_INIT, milestone -> {
                         milestone.setProgress(aspect.getTotals());
                         milestone.setTotals(aspect.getTotals());
                         setFinish(milestone);
@@ -268,87 +276,121 @@ private synchronized Void handleTableInit(TableInitFuncAspect aspect) {
         return null;
     }
 
-    private void storeMilestone() {
-        try {
-            // set task table init
-            if (!TaskDto.SYNC_TYPE_LOG_COLLECTOR.equals(task.getSyncType())) {
-                taskMilestone(KPI_TABLE_INIT, (milestone) -> {
-                    milestone.setStatus(MilestoneStatus.WAITING);
-                    if (targetNodes.isEmpty()) return;
-
-                    milestone.setBegin(System.currentTimeMillis());
-                    milestone.setEnd(0L);
-                    milestone.setStatus(MilestoneStatus.RUNNING);
-                    AtomicLong totals = new AtomicLong(0), completed = new AtomicLong(0);
-                    for (String nid : targetNodes) {
-                        nodeMilestones(nid, KPI_TABLE_INIT, (m) -> {
-                            milestone.setBegin(Math.min(milestone.getBegin(), m.getBegin()));
-                            if (null == m.getEnd()) {
-                                milestone.setEnd(null);
-                            } else if (null != milestone.getEnd()) {
-                                milestone.setEnd(Math.max(milestone.getEnd(), m.getEnd()));
-                            }
-
-                            milestone.setTotals(totals.addAndGet(m.getTotals()));
-                            milestone.setProgress(completed.addAndGet(m.getProgress()));
-                            if (null != m.getErrorMessage()) {
-                                milestone.setStatus(MilestoneStatus.ERROR);
-                                milestone.setErrorMessage(m.getErrorMessage());
-                            }
-                        });
-                    }
+    protected void storeMilestoneWhenTargetNodeTableInit(MilestoneEntity milestone, String nid, AtomicLong totals, AtomicLong completed) {
+        nodeMilestones(nid, KPI_TABLE_INIT, m -> {
+            milestone.setBegin(Math.min(milestone.getBegin(), m.getBegin()));
+            if (null == m.getEnd()) {
+                milestone.setEnd(null);
+            } else if (null != milestone.getEnd()) {
+                milestone.setEnd(Math.max(milestone.getEnd(), m.getEnd()));
+            }
 
-                    if (MilestoneStatus.ERROR != milestone.getStatus() && totals.get() == completed.get()) {
-                        milestone.setStatus(MilestoneStatus.FINISH);
-                    }
-                });
+            milestone.setTotals(totals.addAndGet(m.getTotals()));
+            milestone.setProgress(completed.addAndGet(m.getProgress()));
+            if (null != m.getErrorMessage()) {
+                milestone.setStatus(MilestoneStatus.ERROR);
+                milestone.setErrorMessage(m.getErrorMessage());
             }
+        });
+    }
 
-            taskMilestone(KPI_DATA_NODE_INIT, m -> {
-                if (MilestoneStatus.FINISH == m.getStatus()) return; // return if finish
+    protected void storeMilestoneWhenSyncTypeIsLogCollector() {
+        taskMilestone(KPI_TABLE_INIT, milestone -> {
+            milestone.setStatus(MilestoneStatus.WAITING);
+            if (targetNodes.isEmpty()) return;
 
-                long progress = 0;
-                long totals = dataNodeInitMap.size();
-                for (MilestoneStatus status : dataNodeInitMap.values()) {
-                    if (MilestoneStatus.WAITING != status) {
-                        progress++;
-                    }
-                }
+            milestone.setBegin(System.currentTimeMillis());
+            milestone.setEnd(0L);
+            milestone.setStatus(MilestoneStatus.RUNNING);
+            AtomicLong totals = new AtomicLong(0);
+            AtomicLong completed = new AtomicLong(0);
+            for (String nid : targetNodes) {
+                storeMilestoneWhenTargetNodeTableInit(milestone, nid, totals, completed);
+            }
+            if (MilestoneStatus.ERROR != milestone.getStatus() && totals.get() == completed.get()) {
+                milestone.setStatus(MilestoneStatus.FINISH);
+            }
+        });
+    }
 
-                if (0 == totals || progress == totals) {
-                    setFinish(m);
-                } else {
-                    if (null == m.getBegin()) { // update first time
-                        m.setBegin(System.currentTimeMillis());
-                    }
-                    m.setStatus(0 == progress ? MilestoneStatus.WAITING : MilestoneStatus.RUNNING);
-                }
-                m.setTotals(totals);
-                m.setProgress(progress);
-            });
-
-            clientMongoOperator.update(
-                    Query.query(Criteria.where("_id").is(task.getId()))
-                    , Update.update("attrs.milestone", milestones).set("attrs.nodeMilestones", nodeMilestones)
-                    , ConnectorConstant.TASK_COLLECTION);
+    protected void storeMilestoneDataNodeInit(MilestoneEntity m) {
+        if (MilestoneStatus.FINISH == m.getStatus()) return; // return if finish
+        long progress = 0;
+        long totals = dataNodeInitMap.size();
+        for (MilestoneStatus status : dataNodeInitMap.values()) {
+            if (MilestoneStatus.WAITING != status) {
+                progress++;
+            }
+        }
+
+        if (0 == totals || progress == totals) {
+            setFinish(m);
+        } else {
+            if (null == m.getBegin()) {
+                // update first time
+                m.setBegin(System.currentTimeMillis());
+            }
+            m.setStatus(0 == progress ? MilestoneStatus.WAITING : MilestoneStatus.RUNNING);
+        }
+        m.setTotals(totals);
+        m.setProgress(progress);
+    }
+
+    protected void storeMilestone() {
+        try {
+            // set task table init
+            if (!TaskDto.SYNC_TYPE_LOG_COLLECTOR.equals(task.getSyncType())) {
+                storeMilestoneWhenSyncTypeIsLogCollector();
+            }
+            taskMilestone(KPI_DATA_NODE_INIT, this::storeMilestoneDataNodeInit);
+            synchronized (milestones) {
+                clientMongoOperator.update(
+                        Query.query(Criteria.where("_id").is(task.getId()))
+                        , Update.update("attrs.milestone", milestones).set("attrs.nodeMilestones", nodeMilestones)
+                                .set("syncStatus", getTaskSyncStatus())
+                        , ConnectorConstant.TASK_COLLECTION);
+            }
         } catch (Exception e) {
-					if (TmUnavailableException.notInstance(e)) {
-						log.warn("Save milestone failed: {}", e.getMessage(), e);
-					}
+            if (TmUnavailableException.notInstance(e)) {
+                log.warn("Save milestone failed: {}", e.getMessage(), e);
+            }
         }
     }
 
-    private void taskMilestone(String code, Consumer<MilestoneEntity> consumer) {
+    protected String getTaskSyncStatus() {
+        Collection<MilestoneEntity> values = new ArrayList<>(milestones.values());
+        List<MilestoneEntity> sorted = values.stream()
+                .filter(m -> Objects.nonNull(m) && !MilestoneStatus.WAITING.equals(m.getStatus()))
+                .sorted((m1, m2) -> {
+                    MilestoneStatus s1 = m1.getStatus();
+                    MilestoneStatus s2 = m2.getStatus();
+                    if (KPI_CDC.equals(m1.getCode())) return -1;
+                    if (KPI_CDC.equals(m2.getCode())) return 1;
+                    if (MilestoneStatus.RUNNING.equals(s1)) return -1;
+                    if (MilestoneStatus.RUNNING.equals(s2)) return 1;
+                    Long e1 = m1.getEnd();
+                    Long e2 = m2.getEnd();
+                    if (null == e1) e1 = 0L;
+                    if (null == e2) e2 = 0L;
+                    return e2.intValue() - e1.intValue();
+                }).collect(Collectors.toList());
+        if (sorted.isEmpty()) return KPI_TASK;
+        return sorted.get(0).getCode();
+    }
+
+    protected void taskMilestone(String code, Consumer<MilestoneEntity> consumer) {
         MilestoneEntity entity = milestones.get(code);
         if (null == entity) {
             synchronized (milestones) {
                 entity = milestones.computeIfAbsent(code, s -> new MilestoneEntity(code, MilestoneStatus.WAITING));
             }
         }
-        if (null != consumer) consumer.accept(entity);
+        if (null != consumer) {
+            consumer.accept(entity);
+        }
     }
 
-    private void nodeMilestones(String nodeId, String code, Consumer<MilestoneEntity> consumer) {
+    protected void nodeMilestones(String nodeId, String code, Consumer<MilestoneEntity> consumer) {
         Map<String, MilestoneEntity> nodeMap = nodeMilestones.get(nodeId);
         if (null == nodeMap) {
             synchronized (nodeMilestones) {
@@ -360,7 +402,7 @@ private void nodeMilestones(String nodeId, String code, Consumer<MilestoneEntity
         consumer.accept(entity);
     }
 
-    private <T extends DataNodeAspect<T>> void nodeRegister(Class<T> clz, BiConsumer<String, T> consumer) {
+    protected <T extends DataNodeAspect<T>> void nodeRegister(Class<T> clz, BiConsumer<String, T> consumer) {
         observerHandlers.register(clz, aspect -> {
             String nodeId = nodeId(aspect.getDataProcessorContext());
             consumer.accept(nodeId, aspect);
@@ -368,36 +410,22 @@ private <T extends DataNodeAspect<T>> void nodeRegister(Class<T> clz, BiConsumer
         });
     }
 
-    private <T extends DataNodeAspect<T>> void nodeRegister(Class<T> clz, String code, BiConsumer<T, MilestoneEntity> consumer) {
+    protected  <T extends DataNodeAspect<T>> void nodeRegister(Class<T> clz, String code, BiConsumer<T, MilestoneEntity> consumer) {
         observerHandlers.register(clz, aspect -> {
             String nodeId = nodeId(aspect.getDataProcessorContext());
-            nodeKpi(nodeId, code, m -> {
-                consumer.accept(aspect, m);
-            });
+            nodeMilestones(nodeId, code, m -> consumer.accept(aspect, m));
             return null;
         });
     }
 
-    private void nodeKpi(String nodeId, String code, Consumer<MilestoneEntity> consumer) {
-        Map<String, MilestoneEntity> nodeMap = nodeMilestones.get(nodeId);
-        if (null == nodeMap) {
-            synchronized (nodeMilestones) {
-                nodeMap = nodeMilestones.computeIfAbsent(nodeId, s -> new HashMap<>());
-            }
-        }
-
-        MilestoneEntity entity = nodeMap.computeIfAbsent(code, s -> new MilestoneEntity(code, MilestoneStatus.WAITING));
-        consumer.accept(entity);
-    }
-
-    private String nodeId(ProcessorBaseContext context) {
+    protected String nodeId(ProcessorBaseContext context) {
         return Optional.ofNullable(context)
                 .map(ProcessorBaseContext::getNode)
                 .map(Element::getId)
                 .orElse(null);
     }
 
-    private void setRunning(MilestoneEntity milestone) {
+    protected void setRunning(MilestoneEntity milestone) {
         if (null == milestone.getBegin()) {
             milestone.setBegin(System.currentTimeMillis());
             milestone.setStatus(MilestoneStatus.RUNNING);
@@ -406,7 +434,7 @@ private void setRunning(MilestoneEntity milestone) {
         milestone.setErrorMessage(null);
     }
 
-    private void setFinish(MilestoneEntity milestone) {
+    protected void setFinish(MilestoneEntity milestone) {
         if (null == milestone.getBegin()) {
             milestone.setBegin(System.currentTimeMillis());
         }
@@ -416,14 +444,14 @@ private void setFinish(MilestoneEntity milestone) {
         }
     }
 
-    private <T extends AbsDataNodeErrorAspect<T>> void setError(T aspect, MilestoneEntity m) {
+    protected <T extends AbsDataNodeErrorAspect<T>> void setError(T aspect, MilestoneEntity m) {
         m.setEnd(System.currentTimeMillis());
         m.setStatus(MilestoneStatus.ERROR);
         m.setErrorMessage(Optional.ofNullable(aspect.getError()).map(Throwable::getMessage).orElse(null));
     }
 
-    private Consumer<MilestoneEntity> getErrorConsumer(String errorMessage) {
-        return (m) -> {
+    protected Consumer<MilestoneEntity> getErrorConsumer(String errorMessage) {
+        return m -> {
             m.setEnd(System.currentTimeMillis());
             m.setStatus(MilestoneStatus.ERROR);
             m.setErrorMessage(errorMessage);
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/alarm/service/AlarmService.java b/manager/tm-api/src/main/java/com/tapdata/tm/alarm/service/AlarmService.java
index 35e10a8bde..ce13e80778 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/alarm/service/AlarmService.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/alarm/service/AlarmService.java
@@ -11,6 +11,7 @@
 import com.tapdata.tm.commons.task.dto.TaskDto;
 import com.tapdata.tm.commons.task.dto.alarm.AlarmRuleDto;
 import com.tapdata.tm.commons.task.dto.alarm.AlarmSettingVO;
+import com.tapdata.tm.commons.task.dto.alarm.AlarmVO;
 import com.tapdata.tm.config.security.UserDetail;
 import com.tapdata.tm.message.dto.MessageDto;
 import org.springframework.data.mongodb.core.query.Query;
@@ -57,4 +58,6 @@ Page<AlarmListInfoVo> list(String status,
 	boolean checkOpen(List<AlarmSettingVO> alarmSettingVOS, AlarmKeyEnum key, NotifyEnum type, UserDetail userDetail);
 
 	void closeWhenInspectTaskRunning(String id);
+
+    void updateTaskAlarm(AlarmVO alarm);
 }
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/monitor/dto/TableSyncStaticDto.java b/manager/tm-api/src/main/java/com/tapdata/tm/monitor/dto/TableSyncStaticDto.java
index 4cca4ba11c..310aa6b29c 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/monitor/dto/TableSyncStaticDto.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/monitor/dto/TableSyncStaticDto.java
@@ -16,6 +16,8 @@ public class TableSyncStaticDto {
     private Integer page;
     private Integer size;
 
+    private String tableName;
+
     public Integer getSize() {
         return size > 100 ? 20 : size;
     }
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/task/bean/SampleTaskVo.java b/manager/tm-api/src/main/java/com/tapdata/tm/task/bean/SampleTaskVo.java
index 6323588f3a..c0a112722c 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/task/bean/SampleTaskVo.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/task/bean/SampleTaskVo.java
@@ -17,6 +17,7 @@ public class SampleTaskVo {
     private Date lastUpdated;
     /** 状态 */
     private String status;
+    private String syncStatus;
     /** 任务类型  数据复制还是数据开发 */
     private String syncType;
     /** 增量时间点 */
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/task/constant/SyncStatus.java b/manager/tm-api/src/main/java/com/tapdata/tm/task/constant/SyncStatus.java
new file mode 100644
index 0000000000..24a8d9927b
--- /dev/null
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/task/constant/SyncStatus.java
@@ -0,0 +1,8 @@
+package com.tapdata.tm.task.constant;
+
+public class SyncStatus {
+    private SyncStatus(){}
+    public static final String SYN_STATUS = "syncStatus";
+
+    public static final String NORMAL = "";
+}
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/task/entity/TaskEntity.java b/manager/tm-api/src/main/java/com/tapdata/tm/task/entity/TaskEntity.java
index 26dd215fc3..68f45f4fb4 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/task/entity/TaskEntity.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/task/entity/TaskEntity.java
@@ -107,6 +107,10 @@ public class TaskEntity extends BaseEntity implements IDataPermissionEntity {
     /** 状态*/
     private String status;
 
+    /** The data replication task list needs to add a field to display the milestone stage of the current task
+     * io.tapdata.milestone.constants.SyncStatus */
+    private String syncStatus;
+
     /** 类型 [{label: '全量+增量', value: 'initial_sync+cdc'}, {label: '全量', value: 'initial_sync'}, {label: '增量', value: 'cdc'} ]*/
     private String type;
 
@@ -270,6 +274,11 @@ public class TaskEntity extends BaseEntity implements IDataPermissionEntity {
      */
     private Double dynamicAdjustMemorySampleRate;
 
+    /**
+     * 是否开启双活
+     */
+    private Boolean doubleActive = false;
+
     private List<ErrorEvent> errorEvents;
 
     public String getAccessNodeProcessId() {
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/task/vo/ShareCacheDetailVo.java b/manager/tm-api/src/main/java/com/tapdata/tm/task/vo/ShareCacheDetailVo.java
index 9c4c8a0c3e..6d1959ebf4 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/task/vo/ShareCacheDetailVo.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/task/vo/ShareCacheDetailVo.java
@@ -21,6 +21,7 @@ public class ShareCacheDetailVo extends BaseVo {
   private String tableName;
   private String connectionId;
   private String status;
+  private String syncStatus;
   private Long ttl;
   private Date createTime;
   private Date cacheTimeAt;
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/task/vo/ShareCacheVo.java b/manager/tm-api/src/main/java/com/tapdata/tm/task/vo/ShareCacheVo.java
index 2a954ec9a1..4a66e006ed 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/task/vo/ShareCacheVo.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/task/vo/ShareCacheVo.java
@@ -22,6 +22,7 @@ public class ShareCacheVo extends BaseVo {
   private String connectionId;
   private String tableName;
   private String status;
+  private String syncStatus;
   private List<Status> statuses;
   private Date createTime;
   private Date cacheTimeAt;
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/worker/dto/WorkerDto.java b/manager/tm-api/src/main/java/com/tapdata/tm/worker/dto/WorkerDto.java
index 5e4542fd59..7cc031f29c 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/worker/dto/WorkerDto.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/worker/dto/WorkerDto.java
@@ -86,4 +86,7 @@ public class WorkerDto extends BaseDto {
     private String gitCommitId;
     private String singletonLock; // 单例锁标记，每次启动都会更新
     private Boolean licenseBind;
+    @JsonProperty("worker_date")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date workerDate;
 }
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/worker/entity/Worker.java b/manager/tm-api/src/main/java/com/tapdata/tm/worker/entity/Worker.java
index 5f8f8a3905..2dbb6311dd 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/worker/entity/Worker.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/worker/entity/Worker.java
@@ -85,4 +85,7 @@ public class Worker extends BaseEntity {
 
     private String singletonLock; // 单例锁标记，每次启动都会更新
     private Boolean licenseBind;
+
+    private Date workerDate;
+
 }
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/worker/service/WorkerService.java b/manager/tm-api/src/main/java/com/tapdata/tm/worker/service/WorkerService.java
index 998e980d0d..2a27ea3cbc 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/worker/service/WorkerService.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/worker/service/WorkerService.java
@@ -100,4 +100,6 @@ public Page<WorkerDto> find(Filter filter){
     public abstract Long getAvailableAgentCount();
 
     public abstract Long getLastCheckAvailableAgentCount();
+
+    public abstract String getWorkerCurrentTime(UserDetail userDetail);
 }
diff --git a/manager/tm-common/src/main/java/com/tapdata/tm/commons/dag/nodes/TableNode.java b/manager/tm-common/src/main/java/com/tapdata/tm/commons/dag/nodes/TableNode.java
index d2cb417bf3..a02b3043e2 100644
--- a/manager/tm-common/src/main/java/com/tapdata/tm/commons/dag/nodes/TableNode.java
+++ b/manager/tm-common/src/main/java/com/tapdata/tm/commons/dag/nodes/TableNode.java
@@ -150,6 +150,9 @@ public class TableNode extends DataNode {
     /** 自定义sql条件 */
     @EqField
     private Integer  limit;
+    /** 时差偏移量*/
+    @EqField
+    private Long offsetHours;
 
     /** 增量方式  logCdc  polling */
     private String cdcMode;
diff --git a/manager/tm-common/src/main/java/com/tapdata/tm/commons/dag/process/script/ScriptProcessNode.java b/manager/tm-common/src/main/java/com/tapdata/tm/commons/dag/process/script/ScriptProcessNode.java
index bbf89897f3..c5762f20bd 100644
--- a/manager/tm-common/src/main/java/com/tapdata/tm/commons/dag/process/script/ScriptProcessNode.java
+++ b/manager/tm-common/src/main/java/com/tapdata/tm/commons/dag/process/script/ScriptProcessNode.java
@@ -104,6 +104,7 @@ protected Schema loadSchema(List<String> includes) {
         TaskDto taskDtoCopy = new TaskDto();
         BeanUtils.copyProperties(taskDto, taskDtoCopy);
         taskDtoCopy.setStatus(TaskDto.STATUS_WAIT_RUN);
+        taskDtoCopy.setSyncStatus("");
         taskDtoCopy.setSyncType(TaskDto.SYNC_TYPE_DEDUCE_SCHEMA);
         taskDtoCopy.setDag(build);
         taskDtoCopy.setId(Optional.of(new ObjectId(taskDto.getTransformTaskId())).orElseGet(ObjectId::new));
diff --git a/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/ErrorEvent.java b/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/ErrorEvent.java
index 9edff4255b..8cf7104a47 100644
--- a/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/ErrorEvent.java
+++ b/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/ErrorEvent.java
@@ -19,6 +19,7 @@ public class ErrorEvent implements Serializable {
     private String message;
 
     private String code;
+
     private String stacks;
 
     private Boolean skip = false;
diff --git a/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/Milestone.java b/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/Milestone.java
index f76185520a..c25a588868 100644
--- a/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/Milestone.java
+++ b/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/Milestone.java
@@ -24,5 +24,6 @@ public class Milestone implements Serializable {
     private String errorMessage;
     private Long start;
     private String status;
+    private String syncStatus;
     private String group;
 }
diff --git a/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/ParentTaskDto.java b/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/ParentTaskDto.java
index 9596a816c7..e8e6e42a25 100644
--- a/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/ParentTaskDto.java
+++ b/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/ParentTaskDto.java
@@ -126,9 +126,13 @@ public class ParentTaskDto extends SchedulableDto {
     /** 子任务状态*/
     private List<Status> statuses;
 
-    /** 状态*/
+    /** 任务状态*/
     private String status;
 
+    /** The data replication task list needs to add a field to display the milestone stage of the current task
+     * io.tapdata.milestone.constants.SyncStatus */
+    private String syncStatus;
+
     /** 类型 [{label: '全量+增量', value: 'initial_sync+cdc'}, {label: '全量', value: 'initial_sync'}, {label: '增量', value: 'cdc'} ]*/
     @EqField
     private String type;
diff --git a/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/TaskDto.java b/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/TaskDto.java
index 9293f5e5d6..beeefbdaab 100644
--- a/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/TaskDto.java
+++ b/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/TaskDto.java
@@ -191,6 +191,11 @@ public boolean isAutoInspect() {
      */
     private Double dynamicAdjustMemorySampleRate;
 
+    /**
+     * 是否开启双活
+     */
+    private Boolean doubleActive;
+
     /**
      * 是否启用同步指标收集，启用时任务停止打印同步指标
      */
diff --git a/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/alarm/AlarmVO.java b/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/alarm/AlarmVO.java
new file mode 100644
index 0000000000..1bca968ab3
--- /dev/null
+++ b/manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/alarm/AlarmVO.java
@@ -0,0 +1,12 @@
+package com.tapdata.tm.commons.task.dto.alarm;
+
+import lombok.Data;
+
+import java.util.List;
+@Data
+public class AlarmVO {
+    private String taskId;
+    private String nodeId;
+    private List<AlarmSettingVO> alarmSettings;
+    private List<AlarmRuleVO> alarmRules;
+}
diff --git a/manager/tm-common/src/main/java/com/tapdata/tm/utils/TimeTransFormationUtil.java b/manager/tm-common/src/main/java/com/tapdata/tm/utils/TimeTransFormationUtil.java
new file mode 100644
index 0000000000..dc2cef6ae8
--- /dev/null
+++ b/manager/tm-common/src/main/java/com/tapdata/tm/utils/TimeTransFormationUtil.java
@@ -0,0 +1,107 @@
+package com.tapdata.tm.utils;
+
+import io.tapdata.pdk.apis.entity.QueryOperator;
+import io.tapdata.pdk.apis.entity.TapTimeForm;
+
+import java.time.DayOfWeek;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+
+public class TimeTransFormationUtil {
+    private TimeTransFormationUtil() {
+        throw new IllegalStateException("TimeTransFormationUtil class");
+    }
+    private final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    
+    public static List<String> calculatedTimeRange(LocalDateTime currentDateTime, QueryOperator queryOperator,Long offsetHours){
+        List<String> result = new ArrayList<>();
+        if(null == queryOperator.getUnit() || null == queryOperator.getForm())return result;
+        TapTimeForm tapTimeForm = queryOperator.getForm();
+        Long number = queryOperator.getNumber();
+        if(  null != offsetHours && offsetHours != 0){
+            currentDateTime = currentDateTime.plusHours(offsetHours);
+        }
+        switch (queryOperator.getUnit()){
+            case HOUR:
+                LocalDateTime hourStart;
+                LocalDateTime hourEnd;
+                if(tapTimeForm.equals(TapTimeForm.BEFORE)){
+                    hourStart = currentDateTime.minusHours(number).withMinute(0).withSecond(0);
+                    hourEnd = hourStart.plusHours(number-1).withMinute(59).withSecond(59);
+                }else{
+                    hourStart = currentDateTime.withMinute(0).withSecond(0);
+                    hourEnd  = currentDateTime;
+                }
+                result.add(formatDateTime(hourStart));
+                result.add(formatDateTime(hourEnd));
+                return result;
+            case DAY:
+                LocalDateTime dayStart;
+                LocalDateTime dayEnd;
+                if(tapTimeForm.equals(TapTimeForm.BEFORE)){
+                    dayStart = currentDateTime.minusDays(number).withHour(0).withMinute(0).withSecond(0);
+                    dayEnd = dayStart.plusDays(number-1).withHour(23).withMinute(59).withSecond(59);
+                }else{
+                    dayStart = currentDateTime.withHour(0).withMinute(0).withSecond(0);
+                    dayEnd  = currentDateTime;
+                }
+                result.add(formatDateTime(dayStart));
+                result.add(formatDateTime(dayEnd));
+                return result;
+            case WEEK:
+                LocalDateTime weekStart;
+                LocalDateTime weekEnd;
+                if(tapTimeForm.equals(TapTimeForm.BEFORE)){
+                    weekStart = currentDateTime.minusWeeks(number).with(DayOfWeek.MONDAY).withHour(0).withMinute(0).withSecond(0);
+                    weekEnd =  weekStart.plusWeeks(number-1).with(DayOfWeek.SUNDAY).withHour(23).withMinute(59).withSecond(59);
+                }else{
+                    weekStart = currentDateTime.with(DayOfWeek.MONDAY).withHour(0).withMinute(0).withSecond(0);
+                    weekEnd =  currentDateTime;
+                }
+                result.add(formatDateTime(weekStart));
+                result.add(formatDateTime(weekEnd));
+                return result;
+            case MONTH:
+                LocalDateTime monthStart;
+                LocalDateTime monthEnd;
+                if(tapTimeForm.equals(TapTimeForm.BEFORE)){
+                    monthStart = currentDateTime.minusMonths(number).withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0);
+                    monthEnd = currentDateTime.minusMonths(1).withDayOfMonth(currentDateTime.minusMonths(1).toLocalDate().lengthOfMonth()).withHour(23).withMinute(59).withSecond(59);
+                }else{
+                    monthStart = currentDateTime.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0);
+                    monthEnd = currentDateTime;
+                }
+                result.add(formatDateTime(monthStart));
+                result.add(formatDateTime(monthEnd));
+                return result;
+            case YEAR:
+                LocalDateTime yearStart;
+                LocalDateTime yearEnd;
+                if(tapTimeForm.equals(TapTimeForm.BEFORE)){
+                    yearStart = currentDateTime.minusYears(number).withDayOfYear(1).withHour(0).withMinute(0).withSecond(0);
+                    yearEnd = currentDateTime.minusYears(1).withDayOfYear(currentDateTime.minusYears(1).toLocalDate().lengthOfYear()).withHour(23).withMinute(59).withSecond(59);
+                }else{
+                    yearStart = currentDateTime.withDayOfYear(1).withHour(0).withMinute(0).withSecond(0);
+                    yearEnd = currentDateTime;
+                }
+                result.add(formatDateTime(yearStart));
+                result.add(formatDateTime(yearEnd));
+                return result;
+            default:
+                return result;
+        }
+    }
+
+
+
+
+    public static String formatDateTime(LocalDateTime dateTime) {
+        return dateTime.format(formatter);
+    }
+
+    public static LocalDateTime formatDateTime(String dateTime) {
+        return LocalDateTime.parse(dateTime, formatter);
+    }
+}
diff --git a/manager/tm-oss/src/main/java/com/tapdata/tm/alarm/service/impl/AlarmServiceImpl.java b/manager/tm-oss/src/main/java/com/tapdata/tm/alarm/service/impl/AlarmServiceImpl.java
index 9d47eee95f..0d00987780 100644
--- a/manager/tm-oss/src/main/java/com/tapdata/tm/alarm/service/impl/AlarmServiceImpl.java
+++ b/manager/tm-oss/src/main/java/com/tapdata/tm/alarm/service/impl/AlarmServiceImpl.java
@@ -32,6 +32,7 @@
 import com.tapdata.tm.commons.task.dto.alarm.AlarmRuleDto;
 import com.tapdata.tm.commons.task.dto.alarm.AlarmSettingDto;
 import com.tapdata.tm.commons.task.dto.alarm.AlarmSettingVO;
+import com.tapdata.tm.commons.task.dto.alarm.AlarmVO;
 import com.tapdata.tm.commons.util.ThrowableUtils;
 import com.tapdata.tm.config.security.UserDetail;
 import com.tapdata.tm.events.constant.Type;
@@ -82,9 +83,10 @@
 @Slf4j
 @Setter(onMethod_ = {@Autowired})
 public class AlarmServiceImpl implements AlarmService {
+    private final static String ERROR = "TapOssNonSupportFunctionException";
     @Override
     public void save(AlarmInfo info) {
-        throw new BizException("TapOssNonSupportFunctionException");
+        throw new BizException(ERROR);
     }
 
     @Override
@@ -99,12 +101,12 @@ public void notifyAlarm() {
 
     @Override
     public void close(String[] ids, UserDetail userDetail) {
-        throw new BizException("TapOssNonSupportFunctionException");
+        throw new BizException(ERROR);
     }
 
     @Override
     public Page<AlarmListInfoVo> list(String status, Long start, Long end, String keyword, Integer page, Integer size, UserDetail userDetail, Locale locale) {
-        throw new BizException("TapOssNonSupportFunctionException");
+        throw new BizException(ERROR);
     }
 
     @Override
@@ -117,12 +119,12 @@ public TaskAlarmInfoVo listByTask(AlarmListReqDto dto) {
 
     @Override
     public List<AlarmInfo> find(String taskId, String nodeId, AlarmKeyEnum key) {
-        throw new BizException("TapOssNonSupportFunctionException");
+        throw new BizException(ERROR);
     }
 
     @Override
     public void closeWhenTaskRunning(String taskId) {
-        throw new BizException("TapOssNonSupportFunctionException");
+        throw new BizException(ERROR);
     }
 
     @Override
@@ -136,7 +138,7 @@ public List<AlarmInfo> query(Query query) {
 
     @Override
     public MessageDto add(MessageDto messageDto, UserDetail userDetail) {
-        throw new BizException("TapOssNonSupportFunctionException");
+        throw new BizException(ERROR);
     }
 
     @Override
@@ -146,16 +148,21 @@ public List<AlarmChannelDto> getAvailableChannels() {
 
     @Override
     public boolean checkOpen(TaskDto taskDto, String nodeId, AlarmKeyEnum key, NotifyEnum type, UserDetail userDetail) {
-        throw new BizException("TapOssNonSupportFunctionException");
+        throw new BizException(ERROR);
     }
 
     @Override
     public boolean checkOpen(List<AlarmSettingVO> alarmSettingVOS, AlarmKeyEnum key, NotifyEnum type, UserDetail userDetail) {
-        throw new BizException("TapOssNonSupportFunctionException");
+        throw new BizException(ERROR);
     }
 
     @Override
     public void closeWhenInspectTaskRunning(String id) {
-        throw new BizException("TapOssNonSupportFunctionException");
+        throw new BizException(ERROR);
+    }
+
+    @Override
+    public void updateTaskAlarm(AlarmVO alarm) {
+        throw new BizException(ERROR);
     }
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/alarm/controller/AlarmController.java b/manager/tm/src/main/java/com/tapdata/tm/alarm/controller/AlarmController.java
index eca9ef3f19..3b92c353cf 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/alarm/controller/AlarmController.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/alarm/controller/AlarmController.java
@@ -9,6 +9,7 @@
 import com.tapdata.tm.base.controller.BaseController;
 import com.tapdata.tm.base.dto.Page;
 import com.tapdata.tm.base.dto.ResponseMessage;
+import com.tapdata.tm.commons.task.dto.alarm.AlarmVO;
 import com.tapdata.tm.message.dto.MessageDto;
 import com.tapdata.tm.utils.WebUtils;
 import io.swagger.v3.oas.annotations.Operation;
@@ -83,4 +84,10 @@ public ResponseMessage<MessageDto> addMsg(@RequestBody MessageDto messageDto) {
     public ResponseMessage<List<AlarmChannelDto>> alarmChannel (){
         return success(alarmService.getAvailableChannels());
     }
+
+    @PostMapping("/updateTaskAlarm")
+    public ResponseMessage<Void> updateTaskAlarm(@RequestBody AlarmVO alarm){
+        alarmService.updateTaskAlarm(alarm);
+        return success();
+    }
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/disruptor/handler/UpdateRecordStatusEventHandler.java b/manager/tm/src/main/java/com/tapdata/tm/disruptor/handler/UpdateRecordStatusEventHandler.java
index 8235c2e5ea..aa50e35d02 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/disruptor/handler/UpdateRecordStatusEventHandler.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/disruptor/handler/UpdateRecordStatusEventHandler.java
@@ -104,7 +104,7 @@ protected void taskAlarm(SyncTaskStatusDto data) {
         }
     }
 
-    private void logCollectorAlarm(SyncTaskStatusDto data) {
+    protected void logCollectorAlarm(SyncTaskStatusDto data) {
         String taskId = data.getTaskId();
         String taskName = data.getTaskName();
         String taskStatus = data.getTaskStatus();
@@ -114,7 +114,9 @@ private void logCollectorAlarm(SyncTaskStatusDto data) {
         field.put("syncType", true);
         field.put("status", true);
         field.put("dag", true);
+        field.put("restartFlag",true);
         TaskDto taskDto = taskService.findById(MongoUtils.toObjectId(taskId), field);
+        boolean restartFlag = taskDto.getRestartFlag();
         if (null == taskDto) {
             return;
         }
@@ -152,9 +154,11 @@ private void logCollectorAlarm(SyncTaskStatusDto data) {
         switch (taskStatus) {
             case TaskDto.STATUS_ERROR:
             case TaskDto.STATUS_STOP:
-                update = new Update()
-                        .set("shareCdcStop", true)
-                        .set("shareCdcStopMessage", SHARE_CDC_TASK_STOP_WARNING_KEY);
+                if(!restartFlag){
+                    update = new Update()
+                            .set("shareCdcStop", true)
+                            .set("shareCdcStopMessage", SHARE_CDC_TASK_STOP_WARNING_KEY);
+                }
                 break;
             case TaskDto.STATUS_WAIT_RUN:
             case TaskDto.STATUS_RUNNING:
diff --git a/manager/tm/src/main/java/com/tapdata/tm/init/IPatchScanner.java b/manager/tm/src/main/java/com/tapdata/tm/init/IPatchScanner.java
index cf7117b8ff..308e58dd71 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/init/IPatchScanner.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/init/IPatchScanner.java
@@ -3,7 +3,7 @@
 import lombok.NonNull;
 
 import java.util.List;
-import java.util.function.Function;
+import java.util.function.Predicate;
 
 /**
  * @author <a href="mailto:harsen_lin@163.com">Harsen</a>
@@ -17,5 +17,5 @@ public interface IPatchScanner {
      * @param patches   patch list
      * @param isVersion check function
      */
-    void scanPatches(@NonNull List<IPatch> patches, @NonNull Function<PatchVersion, Boolean> isVersion);
+    void scanPatches(@NonNull List<IPatch> patches, @NonNull Predicate<PatchVersion> isVersion);
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/init/PatchesRunner.java b/manager/tm/src/main/java/com/tapdata/tm/init/PatchesRunner.java
index 7eafaadec5..9cc9032442 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/init/PatchesRunner.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/init/PatchesRunner.java
@@ -4,11 +4,11 @@
 import cn.hutool.core.io.IoUtil;
 import com.tapdata.tm.init.scanners.JavaPatchScanner;
 import com.tapdata.tm.init.scanners.ScriptPatchScanner;
-import io.tapdata.utils.AppType;
 import com.tapdata.tm.utils.TMStartMsgUtil;
 import com.tapdata.tm.utils.TmStartMsg;
 import com.tapdata.tm.verison.dto.VersionDto;
 import com.tapdata.tm.verison.service.VersionService;
+import io.tapdata.utils.AppType;
 import lombok.NonNull;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.logging.log4j.LogManager;
@@ -33,7 +33,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Function;
+import java.util.function.Predicate;
 
 /**
  * @author <a href="mailto:harsen_lin@163.com">Harsen</a>
@@ -162,18 +162,10 @@ private void setAppVersion(PatchType patchType, PatchVersion patchVersion) {
      * @param scanners    scanner instance
      * @return patches
      */
-    private List<IPatch> scanPatches(AppType appType, PatchVersion appVersion, PatchVersion softVersion, IPatchScanner... scanners) {
+    protected List<IPatch> scanPatches(AppType appType, PatchVersion appVersion, PatchVersion softVersion, IPatchScanner... scanners) {
         List<IPatch> patches = new ArrayList<>();
 
-        Function<PatchVersion, Boolean> isVersion;
-        if (null == appVersion) {
-            logger.info("Scan {} patches in last version: {}", appType, softVersion);
-            isVersion = (patchVersion) -> patchVersion.compareTo(softVersion) <= 0;
-//            isVersion = (patchVersion) -> patchVersion.compareVersion(softVersion) == 0 && patchVersion.compareTo(softVersion) <= 0;
-        } else {
-            logger.info("Scan {} patches from {} to {}", appType, appVersion, softVersion);
-            isVersion = (patchVersion) -> patchVersion.compareTo(appVersion) > 0 && patchVersion.compareTo(softVersion) <= 0;
-        }
+        Predicate<PatchVersion> isVersion = getVersionChecker(appType, softVersion, appVersion);
 
         for (IPatchScanner scanner : scanners) {
             scanner.scanPatches(patches, isVersion);
@@ -194,4 +186,15 @@ public static String patchDir(@NonNull AppType appType) {
         }
         return ResourceUtils.CLASSPATH_URL_PREFIX + "init/idaas";
     }
+
+    public static Predicate<PatchVersion> getVersionChecker(AppType appType, PatchVersion softVersion, PatchVersion appVersion) {
+        if (null == appVersion) {
+            logger.info("Scan {} patches in last version: {}", appType, softVersion);
+            return patchVersion -> patchVersion.compareTo(softVersion) <= 0;
+        } else {
+            logger.info("Scan {} patches from {} to {}", appType, appVersion, softVersion);
+            return patchVersion -> patchVersion.compareTo(appVersion) > 0 && patchVersion.compareTo(softVersion) <= 0;
+        }
+    }
+
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/init/ex/FindScriptPatchException.java b/manager/tm/src/main/java/com/tapdata/tm/init/ex/FindScriptPatchException.java
new file mode 100644
index 0000000000..7d5c1b0b31
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/init/ex/FindScriptPatchException.java
@@ -0,0 +1,20 @@
+package com.tapdata.tm.init.ex;
+
+import lombok.Getter;
+
+/**
+ * 加载脚本补丁异常
+ *
+ * @author <a href="mailto:harsen_lin@163.com">Harsen</a>
+ * @version v1.0 2024/4/23 19:10 Create
+ */
+@Getter
+public class FindScriptPatchException extends RuntimeException {
+
+    private final String filename;
+
+    public FindScriptPatchException(Throwable cause, String filename) {
+        super(String.format("Find script patch '%s' failed: %s", filename, cause.getMessage()), cause);
+        this.filename = filename;
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/init/ex/InitJavaPatchException.java b/manager/tm/src/main/java/com/tapdata/tm/init/ex/InitJavaPatchException.java
new file mode 100644
index 0000000000..754156c61a
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/init/ex/InitJavaPatchException.java
@@ -0,0 +1,21 @@
+package com.tapdata.tm.init.ex;
+
+/**
+ * 初始化 Java 补丁异常
+ *
+ * @author <a href="mailto:harsen_lin@163.com">Harsen</a>
+ * @version v1.0 2024/4/23 19:10 Create
+ */
+public class InitJavaPatchException extends RuntimeException {
+
+    private final String filename;
+
+    public InitJavaPatchException(Throwable cause, String filename) {
+        super(String.format("Init java patch '%s' failed: %s", filename, cause.getMessage()), cause);
+        this.filename = filename;
+    }
+
+    public String getFilename() {
+        return filename;
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/init/ex/ReadScriptPatchException.java b/manager/tm/src/main/java/com/tapdata/tm/init/ex/ReadScriptPatchException.java
new file mode 100644
index 0000000000..83b489fb8c
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/init/ex/ReadScriptPatchException.java
@@ -0,0 +1,20 @@
+package com.tapdata.tm.init.ex;
+
+import lombok.Getter;
+
+/**
+ * 读取脚本补丁异常
+ *
+ * @author <a href="mailto:harsen_lin@163.com">Harsen</a>
+ * @version v1.0 2024/4/23 19:10 Create
+ */
+@Getter
+public class ReadScriptPatchException extends RuntimeException {
+
+    private final String filename;
+
+    public ReadScriptPatchException(Throwable cause, String filename) {
+        super(String.format("Read script patch '%s' failed: %s", filename, cause.getMessage()), cause);
+        this.filename = filename;
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/init/scanners/JavaPatchScanner.java b/manager/tm/src/main/java/com/tapdata/tm/init/scanners/JavaPatchScanner.java
index e9a23f6566..550ccf0eae 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/init/scanners/JavaPatchScanner.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/init/scanners/JavaPatchScanner.java
@@ -1,6 +1,7 @@
 package com.tapdata.tm.init.scanners;
 
 import com.tapdata.tm.init.*;
+import com.tapdata.tm.init.ex.InitJavaPatchException;
 import com.tapdata.tm.init.patches.PatchAnnotation;
 import com.tapdata.tm.init.patches.PatchAnnotations;
 import io.tapdata.utils.AppType;
@@ -12,7 +13,7 @@
 import java.lang.reflect.Constructor;
 import java.util.List;
 import java.util.Set;
-import java.util.function.Function;
+import java.util.function.Predicate;
 
 /**
  * @author <a href="mailto:harsen_lin@163.com">Harsen</a>
@@ -28,7 +29,7 @@ public JavaPatchScanner(AppType appType) {
     }
 
     @Override
-    public void scanPatches(@NonNull List<IPatch> patches, @NonNull Function<PatchVersion, Boolean> isVersion) {
+    public void scanPatches(@NonNull List<IPatch> patches, @NonNull Predicate<PatchVersion> isVersion) {
         Reflections ref = new Reflections(SCAN_PACKAGE);
 
         Set<Class<?>> classSet = ref.getTypesAnnotatedWith(PatchAnnotation.class);
@@ -40,17 +41,17 @@ public void scanPatches(@NonNull List<IPatch> patches, @NonNull Function<PatchVe
                 if (!patchType.inAppTypes(ins.appType())) continue;
 
                 PatchVersion patchVersion = PatchVersion.valueOf(ins.version());
-                if (isVersion.apply(patchVersion)) {
-                    try {
-                        Constructor<?> con = c.getConstructor(PatchType.class, PatchVersion.class);
-                        patches.add((IPatch) con.newInstance(patchType, patchVersion));
-                    } catch (Exception e) {
-                        throw new RuntimeException(String.format("Init patch '%s' failed: %s", c.getName(), e.getMessage()), e);
-                    }continue;
-                } else {
-                    logger.info("The init script has been executed {}, skip...", ins.version());
+                if (!isVersion.test(patchVersion)) {
+                    logger.debug("The init script has been executed {}, skip...", ins.version());
+                    break;
+                }
+
+                try {
+                    Constructor<?> con = c.getConstructor(PatchType.class, PatchVersion.class);
+                    patches.add((IPatch) con.newInstance(patchType, patchVersion));
+                } catch (Exception e) {
+                    throw new InitJavaPatchException(e, c.getName());
                 }
-                break;
             }
         }
     }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/init/scanners/ScriptPatchScanner.java b/manager/tm/src/main/java/com/tapdata/tm/init/scanners/ScriptPatchScanner.java
index 4978284bf2..f9a9c22637 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/init/scanners/ScriptPatchScanner.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/init/scanners/ScriptPatchScanner.java
@@ -4,6 +4,8 @@
 import cn.hutool.core.io.IORuntimeException;
 import cn.hutool.core.io.IoUtil;
 import com.tapdata.tm.init.*;
+import com.tapdata.tm.init.ex.FindScriptPatchException;
+import com.tapdata.tm.init.ex.ReadScriptPatchException;
 import com.tapdata.tm.init.patches.JsonFilePatch;
 import io.tapdata.utils.AppType;
 import lombok.NonNull;
@@ -17,7 +19,7 @@
 import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Function;
+import java.util.function.Predicate;
 
 /**
  * @author <a href="mailto:harsen_lin@163.com">Harsen</a>
@@ -35,7 +37,7 @@ public ScriptPatchScanner(AppType appType, Map<String, String> allVariables) {
     }
 
     @Override
-    public void scanPatches(@NonNull List<IPatch> patches, @NonNull Function<PatchVersion, Boolean> isVersion) {
+    public void scanPatches(@NonNull List<IPatch> patches, @NonNull Predicate<PatchVersion> isVersion) {
         switch (patchType.getAppType()) {
             case DFS:
                 parse(patches, PatchesRunner.patchDir(AppType.DFS) + "/*.json", isVersion);
@@ -49,32 +51,33 @@ public void scanPatches(@NonNull List<IPatch> patches, @NonNull Function<PatchVe
         }
     }
 
-    private void parse(List<IPatch> patches, String path, Function<PatchVersion, Boolean> isVersion) {
-        Resource[] resources;
-        try {
-            PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
-            resources = pathMatchingResourcePatternResolver.getResources(path);
-        } catch (IOException e) {
-            throw new RuntimeException("Load script failed: " + path, e);
-        }
-
-        for (Resource resource : resources) {
+    private void parse(List<IPatch> patches, String path, Predicate<PatchVersion> isVersion) {
+        for (Resource resource : getResources(path)) {
             String currentVersion = FileUtil.mainName(resource.getFilename());
             if (null == currentVersion) {
-                logger.info("Script version is null, skip script file: {}", resource.getFilename());
+                logger.warn("Script version is null, skip script file: {}", resource.getFilename());
                 continue;
             }
             PatchVersion version = PatchVersion.valueOf(currentVersion);
-            if (!isVersion.apply(version)) {
-                logger.info("The init script has been executed {}, skip...", currentVersion);
+            if (Boolean.FALSE.equals(isVersion.test(version))) {
+                logger.debug("The init script has been executed {}, skip...", currentVersion);
                 continue;
             }
             try (InputStream is = resource.getInputStream()) {
                 String scriptStr = IoUtil.read(is, StandardCharsets.UTF_8);
                 patches.add(new JsonFilePatch(patchType, version, resource.getFilename(), scriptStr, allVariables));
             } catch (IORuntimeException | IOException e) {
-                throw new RuntimeException("Error reading script file: " + resource.getFilename(), e);
+                throw new ReadScriptPatchException(e, resource.getFilename());
             }
         }
     }
+
+    protected Resource[] getResources(String path) {
+        try {
+            PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
+            return pathMatchingResourcePatternResolver.getResources(path);
+        } catch (IOException e) {
+            throw new FindScriptPatchException(e , path);
+        }
+    }
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/monitor/controller/MeasureController.java b/manager/tm/src/main/java/com/tapdata/tm/monitor/controller/MeasureController.java
index 13b16220b6..fcb0808946 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/monitor/controller/MeasureController.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/monitor/controller/MeasureController.java
@@ -79,11 +79,12 @@ public ResponseMessage<Map<String, Object>> batch(@Parameter(description = "多
     @Operation(summary = "全量信息接口")
     @GetMapping("/full_statistics")
     public ResponseMessage<Page<TableSyncStaticVo>> querySyncStatic (@RequestParam String taskRecordId,
+                                                                     @RequestParam(required = false) String tableName,
                                                                      @RequestParam(defaultValue = "1") Integer page,
                                                                      @RequestParam(defaultValue = "20") Integer size) {
 			UserDetail userDetail = getLoginUser();
 			userDetail.setFreeAuth();
-			return success(measurementServiceV2.querySyncStatic(new TableSyncStaticDto(taskRecordId, page, size), userDetail));
+			return success(measurementServiceV2.querySyncStatic(new TableSyncStaticDto(taskRecordId, page, size,tableName), userDetail));
     }
 
     @Operation(summary = "表同步状态统计")
diff --git a/manager/tm/src/main/java/com/tapdata/tm/monitor/service/MeasurementServiceV2Impl.java b/manager/tm/src/main/java/com/tapdata/tm/monitor/service/MeasurementServiceV2Impl.java
index 3931252c36..81ca77bba3 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/monitor/service/MeasurementServiceV2Impl.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/monitor/service/MeasurementServiceV2Impl.java
@@ -931,6 +931,8 @@ public Page<TableSyncStaticVo> querySyncStatic(TableSyncStaticDto dto, UserDetai
                 .and("tags.type").is("table")
                 .and(MeasurementEntity.FIELD_GRANULARITY).is(Granularity.GRANULARITY_MINUTE);
 
+        querySyncByTableName(dto,criteria);
+
         TmPageable tmPageable = new TmPageable();
         tmPageable.setPage(dto.getPage());
         tmPageable.setSize(dto.getSize());
@@ -1041,6 +1043,12 @@ protected void fixSyncRate(List<TableSyncStaticVo> tableSyncStaticVos, TaskDto t
         }
     }
 
+    public void querySyncByTableName(TableSyncStaticDto dto, Criteria criteria){
+        if(StringUtils.isNotBlank(dto.getTableName())){
+            criteria.and("tags.table").regex(dto.getTableName());
+        }
+    }
+
 	@Override
     public List<SyncStatusStatisticsVo> queryTableSyncStatusStatistics(SyncStatusStatisticsParam param) {
 		AggregationResults<SyncStatusStatisticsVo> aggregationResults = mongoOperations.aggregate(Aggregation.newAggregation(
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/controller/TaskController.java b/manager/tm/src/main/java/com/tapdata/tm/task/controller/TaskController.java
index 6f31753031..61f76ec88e 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/task/controller/TaskController.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/controller/TaskController.java
@@ -38,16 +38,14 @@
 import com.tapdata.tm.task.service.*;
 import com.tapdata.tm.task.vo.*;
 import com.tapdata.tm.user.service.UserService;
-import com.tapdata.tm.utils.GZIPUtil;
-import com.tapdata.tm.utils.Lists;
-import com.tapdata.tm.utils.MessageUtil;
-import com.tapdata.tm.utils.MongoUtils;
+import com.tapdata.tm.utils.*;
 import com.tapdata.tm.worker.service.WorkerService;
 import io.github.openlg.graphlib.Graph;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.enums.ParameterIn;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import io.tapdata.pdk.apis.entity.QueryOperator;
 import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
@@ -66,6 +64,7 @@
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Supplier;
@@ -338,8 +337,16 @@ public ResponseMessage<TaskDto> findById(
 		) {
 			Field fields = parseField(fieldsJson);
 			UserDetail user = getLoginUser();
+            Field finalFields;
+            if(null == fields){
+                finalFields = new Field();
+            }else{
+                finalFields = fields;
+            }
+            finalFields.put("errorEvents.stacks",false);
+            finalFields.put("attrs.SNAPSHOT_ORDER_LIST",false);
 			TaskDto taskDto = dataPermissionCheckOfId(request, user, MongoUtils.toObjectId(id), DataPermissionActionEnums.View,
-				() -> taskService.findById(MongoUtils.toObjectId(id), fields, user)
+				() -> taskService.findById(MongoUtils.toObjectId(id), finalFields, user)
 			);
 			if (taskDto != null) {
 				if (StringUtils.isNotBlank(taskRecordId) && !taskRecordId.equals(taskDto.getTaskRecordId())) {
@@ -1385,4 +1392,22 @@ public ResponseMessage<List<SampleTaskVo>> findByConId(@RequestParam(value = "so
     public ResponseMessage<Boolean> checkCloudTaskLimit(@PathVariable(value = "taskId") String taskId){
         return success(taskService.checkCloudTaskLimit(MongoUtils.toObjectId(taskId),getLoginUser(),true));
     }
+
+
+    @GetMapping("/getCurrentEngineTime")
+    public ResponseMessage<String> getCurrentEngineTime(){
+        UserDetail userDetail = getLoginUser();
+        String workerDate = workerService.getWorkerCurrentTime(userDetail);
+        return success(workerDate);
+    }
+
+    @GetMapping("/calculatedTimeRange")
+    public ResponseMessage<List<String>> calculatedTimeRange(@RequestBody QueryOperator queryOperator,
+                                                       @RequestParam(required = false)Long offsetHours){
+        UserDetail userDetail = getLoginUser();
+        String workerDate = workerService.getWorkerCurrentTime(userDetail);
+        LocalDateTime workerLocalTime = TimeTransFormationUtil.formatDateTime(workerDate);
+        List<String> range = TimeTransFormationUtil.calculatedTimeRange(workerLocalTime,queryOperator,offsetHours);
+        return success(range);
+    }
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/TaskServiceImpl.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/TaskServiceImpl.java
index cb61d3b051..ca6f960114 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/task/service/TaskServiceImpl.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/TaskServiceImpl.java
@@ -9,7 +9,6 @@
 import cn.hutool.extra.cglib.CglibUtil;
 import com.alibaba.fastjson.JSON;
 import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.collect.Maps;
 import com.mongodb.client.result.UpdateResult;
 import com.tapdata.tm.Settings.service.SettingsServiceImpl;
@@ -86,6 +85,9 @@
 import com.tapdata.tm.task.param.LogSettingParam;
 import com.tapdata.tm.task.param.SaveShareCacheParam;
 import com.tapdata.tm.task.repository.TaskRepository;
+import com.tapdata.tm.task.service.batchin.ParseRelMig;
+import com.tapdata.tm.task.service.batchin.ParseRelMigFile;
+import com.tapdata.tm.task.service.batchin.entity.ParseParam;
 import com.tapdata.tm.task.service.batchup.BatchUpChecker;
 import com.tapdata.tm.task.service.utils.TaskServiceUtil;
 import com.tapdata.tm.task.vo.ShareCacheDetailVo;
@@ -154,11 +156,7 @@
 @Slf4j
 @Setter(onMethod_ = {@Autowired})
 public class TaskServiceImpl extends TaskService{
-    protected static final String PROCESSOR_THREAD_NUM="processorThreadNum";
-    protected static final String CATALOG="catalog";
-    protected static final String ELEMENT_TYEP="elementType";
-    protected static final String PROCESSOR="processor";
-    public static final String RM_ID_KEY = "rm_id";
+    protected static final String CATALOG = "catalog";
     private MessageServiceImpl messageService;
     private SnapshotEdgeProgressService snapshotEdgeProgressService;
     private InspectService inspectService;
@@ -1129,11 +1127,13 @@ public TaskDto copy(ObjectId id, UserDetail user) {
 
         taskDto.setName(copyName);
         taskDto.setStatus(TaskDto.STATUS_EDIT);
+        taskDto.setSyncStatus(SyncStatus.NORMAL);
         taskDto.setStatuses(new ArrayList<>());
         taskDto.setStartTime(null);
         taskDto.setStopTime(null);
         taskDto.setErrorTime(null);
         taskDto.setCrontabScheduleMsg(null);
+        taskDto.setErrorEvents(null);
         if (taskDto.getAttrs() != null) {
             taskDto.getAttrs().remove("SNAPSHOT_ORDER_LIST");
         }
@@ -1200,6 +1200,7 @@ public void renew(ObjectId id, UserDetail user, boolean system) {
         boolean needCreateRecord = !Lists.of(TaskDto.STATUS_DELETE_FAILED, TaskDto.STATUS_RENEW_FAILED, TaskDto.STATUS_WAIT_START).contains(taskDto.getStatus());
         //boolean needCreateRecord = !TaskDto.STATUS_WAIT_START.equals(taskDto.getStatus());
         TaskEntity taskSnapshot = null;
+        taskDto.setSyncStatus(SyncStatus.NORMAL);
         if (needCreateRecord) {
             taskSnapshot = new TaskEntity();
             BeanUtil.copyProperties(taskDto, taskSnapshot);
@@ -1875,6 +1876,7 @@ public Page<ShareCacheVo> findShareCache(Filter filter, UserDetail userDetail) {
             for (TaskDto taskDto : taskDtos) {
                 ShareCacheVo shareCacheVo = new ShareCacheVo();
                 shareCacheVo.setStatus(taskDto.getStatus());
+                shareCacheVo.setSyncStatus(taskDto.getSyncStatus());
                 Node sourceNode = getSourceNode(taskDto);
                 if (null != sourceNode) {
                     String connectionId = ((DataParentNode) sourceNode).getConnectionId();
@@ -1910,6 +1912,7 @@ public Page<ShareCacheVo> findShareCache(Filter filter, UserDetail userDetail) {
                 shareCacheVo.setName(taskDto.getName());
                 shareCacheVo.setCreateUser(taskDto.getCreateUser());
                 shareCacheVo.setStatus(taskDto.getStatus());
+                shareCacheVo.setSyncStatus(taskDto.getSyncStatus());
                 shareCacheVo.setStatuses(taskDto.getStatuses());
                 shareCacheVo.setId(taskDto.getId().toString());
                 shareCacheVos.add(shareCacheVo);
@@ -1927,6 +1930,7 @@ public ShareCacheDetailVo findShareCacheById(String id) {
         shareCacheDetailVo.setId(id);
         shareCacheDetailVo.setName(taskDto.getName());
         shareCacheDetailVo.setStatus(taskDto.getStatus());
+        shareCacheDetailVo.setSyncStatus(taskDto.getSyncStatus());
         String connectionId = ((DataNode) sourceNode).getConnectionId();
         DataSourceConnectionDto connectionDto = dataSourceService.findOne(Query.query(Criteria.where("id").is(connectionId)));
         if (null != connectionDto) {
@@ -1955,6 +1959,7 @@ public ShareCacheDetailVo findShareCacheById(String id) {
 
     private TaskDto parseCacheToTaskDto(SaveShareCacheParam saveShareCacheParam, TaskDto taskDto) {
         taskDto.setStatus(TaskDto.STATUS_EDIT);
+        taskDto.setSyncStatus(SyncStatus.NORMAL);
         taskDto.setType(ParentTaskDto.TYPE_CDC);
         taskDto.setShareCache(true);
         taskDto.setLastUpdAt(new Date());
@@ -2767,6 +2772,7 @@ public List<SampleTaskVo> findByConId(String sourceConnectionId, String targetCo
                         sampleTaskVo.setCreateTime(t.getCreateAt());
                         sampleTaskVo.setLastUpdated(t.getLastUpdAt());
                         sampleTaskVo.setStatus(t.getStatus());
+                        sampleTaskVo.setSyncStatus(t.getSyncStatus());
                         sampleTaskVo.setSyncType(t.getSyncType());
                         sampleTaskVo.setSourceConnectionIds(sourceIds);
                         sampleTaskVo.setTargetConnectionId(tgtIds);
@@ -2851,6 +2857,7 @@ public void batchLoadTask(HttpServletResponse response, List<String> taskIds, Us
                 agentGroupService.uploadAgentInfo(taskDto, user);
 
                 taskDto.setStatus(TaskDto.STATUS_EDIT);
+                taskDto.setSyncStatus(SyncStatus.NORMAL);
                 taskDto.setStatuses(new ArrayList<>());
 							taskDto.setAttrs(new HashMap<>()); // 导出任务不保留运行时信息
                 jsonList.add(new TaskUpAndLoadDto("Task", JsonUtil.toJsonUseJackson(taskDto)));
@@ -2910,632 +2917,15 @@ public void batchLoadTask(HttpServletResponse response, List<String> taskIds, Us
         fileService1.viewImg1(json, response, fileName.get() + ".json.gz");
     }
 
-    private static Map<String, Object> getTableSchema(Map<String, Object> full, String table) {
-        List<String> tableNames = Arrays.asList(table.split("\\."));
-        Map<String, Object> databasesLayer = (Map<String, Object>) full.get("databases");
-        Map<String, Object> currentDatabaseSchema = (Map<String, Object>) databasesLayer.get(tableNames.get(0));
-        Map<String, Object> schemasLayer = (Map<String, Object>) currentDatabaseSchema.get("schemas");
-        Map<String, Object> currentSchema = (Map<String, Object>) schemasLayer.get(tableNames.get(1));
-        Map<String, Object> tables = (Map<String, Object>) currentSchema.get("tables");
-        return tables;
-    }
-
-    private void genProperties(Map<String, Object> parent, Map<String, Object> contentMapping, Map<String, Object> relationshipsMapping, Map<String, Object> full, Map<String, String> sourceToJS, Map<String, Map<String, Map<String, Object>>> renameFields, Map<String, List<Map<String, Object>>> contentDeleteOperations, Map<String, List<Map<String, Object>>> contentRenameOperations) {
-        String parentId = (String)parent.get(RM_ID_KEY);
-        List<String> children = (List<String>) ((Map<String, Object>) relationshipsMapping.get(parentId)).get("children");
-        if (children == null || children.size() == 0) {
-            return;
-        }
-        List<Map<String, Object>> childrenNode = new ArrayList<>();
-        for (String child : children) {
-            Map<String, Object> childNode = new HashMap<>();
-            Map<String, Object> map = (Map<String, Object>) contentMapping.get(child);
-            Map<String, String> setting = (Map<String, String>) map.get("settings");
-
-            Map<String, Object> tables = getTableSchema(full, (String) ((Map<String, Object>) contentMapping.get(child)).get("table"));
-
-            String tpTable = ((String) ((Map<String, Object>) contentMapping.get(child)).get("table")).split("\\.")[((String) ((Map<String, Object>) contentMapping.get(child)).get("table")).split("\\.").length - 1];
-            List<Map<String, String>> joinKeys = new ArrayList<>();
-            Map<String, Object> currentTable = (Map<String, Object>) tables.get(tpTable);
-            Map<String, Object> currentColumns = (Map<String, Object>) currentTable.get("columns");
-            Map<String, Map<String, Object>> currentRenameFields = renameFields.get(tpTable);
-            Map<String, Object> parentTable = (Map<String, Object>) tables.get((String) parent.get("tableName"));
-            Map<String, Object> parentColumns = (Map<String, Object>) parentTable.get("columns");
-            Map<String, Map<String, Object>> parentRenameFields = renameFields.get((String) parent.get("tableName"));
-
-            String parentTargetPath = (String) parent.get("targetPath");
-            if (parentTargetPath == null) {
-                parentTargetPath = "";
-            }
-            String mergeType = "updateWrite";
-            String targetPath = "";
-            if ("NEW_DOCUMENT".equals(setting.get("type"))) {
-                targetPath = parentTargetPath;
-            }
-            if ("EMBEDDED_DOCUMENT".equals(setting.get("type"))) {
-                targetPath = getEmbeddedDocumentPath(parentTargetPath, setting);
-            }
-            if ("EMBEDDED_DOCUMENT_ARRAY".equals(setting.get("type"))) {
-                if (parentTargetPath.equals("")) {
-                    targetPath = setting.get("embeddedPath");
-                } else {
-                    targetPath = parentTargetPath + "." + setting.get("embeddedPath");
-                }
-                mergeType = "updateIntoArray";
-                List<String> arrayKeys = new ArrayList<>();
-                Map<String, Object> fields = (Map<String, Object>) map.get("fields");
-                for (String field : fields.keySet()) {
-                    Map<String, Object> fieldMap = (Map<String, Object>) fields.get(field);
-                    Map<String, Object> source = (Map<String, Object>) fieldMap.get("source");
-                    Boolean isPrimaryKey = (Boolean) source.get("isPrimaryKey");
-                    if (isPrimaryKey != null && isPrimaryKey) {
-                        arrayKeys.add((currentRenameFields.get(field).get("target")).toString());
-                    }
-                }
-                childNode.put("arrayKeys", arrayKeys);
-            }
-
-            childNode.put("targetPath", targetPath);
-            childNode.put("mergeType", mergeType);
-
-            childNode.put("tableName", tpTable);
-            childNode.put("children", new ArrayList<>());
-            childNode.put("id", sourceToJS.get(child));
-            childNode.put(RM_ID_KEY, child);
-
-            // 由于使用外键做关联, 所以似乎 RM 只能合并来自一个源的数据, 所以 tables 表结构使用其中一个就可以
-
-            Map<String,Map<String,String>> sourceJoinKeyMapping = new HashMap<>();
-            Map<String,Map<String,String>> targetJoinKeyMapping = new HashMap<>();
-            for (String columnKey : currentColumns.keySet()) {
-                Map<String, Object> column = (Map<String, Object>) currentColumns.get(columnKey);
-                Map<String, Object> foreignKey = (Map<String, Object>) column.get("foreignKey");
-                if (foreignKey == null) {
-                    continue;
-                }
-                if (((String)foreignKey.get("table")).equals(parent.get("tableName"))) {
-                    Map<String, String> joinKey = new HashMap<>();
-                    String sourceJoinKey = currentRenameFields.get(columnKey).get("target").toString();
-                    Map<String, String> newFieldMap = new HashMap<>();
-                    newFieldMap.put("source", columnKey);
-                    newFieldMap.put("target", sourceJoinKey);
-                    sourceJoinKeyMapping.put(sourceJoinKey, newFieldMap);
-                    joinKey.put("source", sourceJoinKey);
-                    String targetJoinKey = parentRenameFields.get((String) foreignKey.get("column")).get("target").toString();
-                    HashMap<String, String> targetNewFieldMap = new HashMap<>();
-                    targetNewFieldMap.put("source", (String) foreignKey.get("column"));
-                    targetNewFieldMap.put("target", targetJoinKey);
-                    if (parent.get("targetPath").equals("")) {
-                        joinKey.put("target", targetJoinKey);
-                    } else {
-                        joinKey.put("target", parent.get("targetPath") + "." + targetJoinKey);
-                    }
-                    targetJoinKeyMapping.put(targetJoinKey,targetNewFieldMap);
-                    joinKeys.add(joinKey);
-                }
-            }
-
-            parentColumnsFindJoinKeys(parent, renameFields, parentColumns, tpTable, joinKeys, sourceJoinKeyMapping, targetJoinKeyMapping);
-            childNode.put("joinKeys", joinKeys);
-            joinKeys.forEach(joinKeyMap->{
-                String sourceJoinKey = joinKeyMap.get("source");
-                addRenameOpIfDeleteOpHasJoinKey(contentDeleteOperations, contentRenameOperations, child, sourceJoinKeyMapping, sourceJoinKey);
-                String targetJoinKey = joinKeyMap.get("target");
-                addRenameOpIfDeleteOpHasJoinKey(contentDeleteOperations, contentRenameOperations, parentId, targetJoinKeyMapping, targetJoinKey);
-            });
-            genProperties(childNode, contentMapping, relationshipsMapping, full, sourceToJS, renameFields, contentDeleteOperations, contentRenameOperations);
-            childrenNode.add(childNode);
-        }
-        parent.put("children", childrenNode);
-    }
-
-
-    protected void addRenameOpIfDeleteOpHasJoinKey(Map<String, List<Map<String, Object>>> contentDeleteOperations, Map<String, List<Map<String, Object>>> contentRenameOperations, String tableId, Map<String,Map<String, String>> joinKeyMapping, String joinKey) {
-        List<Map<String, Object>> childDeleteOperations = contentDeleteOperations.get(tableId);
-        boolean removeJoinKeyFlag = removeDeleteOperation(childDeleteOperations, joinKeyMapping, joinKey);
-        if (removeJoinKeyFlag) {
-            List<Map<String, Object>> childRenameOperations = contentRenameOperations.get(tableId);
-            Map<String, Object> renameOperation = getRenameOperation(joinKeyMapping.get(joinKey).get("source"), joinKeyMapping.get(joinKey).get("target"));
-            childRenameOperations.add(renameOperation);
-        }
-    }
-
-    protected static boolean removeDeleteOperation(List<Map<String, Object>> deleteOperations, Map<String, Map<String, String>> joinKeyMapping, String joinKey) {
-        boolean flag = deleteOperations.removeIf((delOperations) -> {
-            String deleteField = (String) delOperations.get("field");
-            String originalField = joinKeyMapping.get(joinKey).get("source");
-            if (deleteField.equals(originalField)) {
-                return true;
-            }
-            return false;
-        });
-        return flag;
-    }
-
-    protected String getEmbeddedDocumentPath(String parentTargetPath, Map<String, String> setting) {
-        String targetPath;
-        if (parentTargetPath.equals("")) {
-            targetPath = setting.get("embeddedPath");
-        } else {
-            if (StringUtils.isBlank(setting.get("embeddedPath"))) {
-                targetPath = parentTargetPath;
-            } else {
-                targetPath = parentTargetPath + "." + setting.get("embeddedPath");
-            }
-        }
-        return targetPath;
-    }
-
-    protected void parentColumnsFindJoinKeys(Map<String, Object> parent, Map<String, Map<String, Map<String, Object>>> renameFields, Map<String, Object> parentColumns, String tpTable, List<Map<String, String>> joinKeys, Map<String,Map<String, String>> souceJoinKeyMapping, Map<String,Map<String, String>> targetJoinKeyMapping) {
-        Map<String, Map<String, Object>> parentRenameFields = renameFields.get((String) parent.get("tableName"));
-        for (String columnKey : parentColumns.keySet()) {
-            Map<String, Object> column = (Map<String, Object>) parentColumns.get(columnKey);
-            Map<String, Object> foreignKey = (Map<String, Object>) column.get("foreignKey");
-            if (foreignKey == null) {
-                continue;
-            }
-            if (((String) foreignKey.get("table")).equals(tpTable)) {
-                Map<String, String> joinKey = new HashMap<>();
-                String sourceJoinKey = renameFields.get(tpTable).get(((String) foreignKey.get("column"))).get("target").toString();
-                Map<String,String> sourceNewFieldMap=new HashMap<>();
-                sourceNewFieldMap.put("source", (String) foreignKey.get("column"));
-                sourceNewFieldMap.put("target",sourceJoinKey);
-                souceJoinKeyMapping.put(sourceJoinKey,sourceNewFieldMap);
-                joinKey.put("source", sourceJoinKey);
-                Map<String,String> targetNewFieldMap=new HashMap<>();
-                String targetJoinKey = parentRenameFields.get(columnKey).get("target").toString();
-                targetNewFieldMap.put("source", columnKey);
-                targetNewFieldMap.put("target", targetJoinKey);
-                String targetPath = parent.get("targetPath").toString();
-                if (!StringUtils.isBlank(targetPath)) {
-                    targetJoinKey=targetPath + "." + targetJoinKey;
-                }
-                joinKey.put("target", targetJoinKey);
-                targetJoinKeyMapping.put(targetJoinKey,targetNewFieldMap);
-                joinKeys.add(joinKey);
-            }
-        }
-    }
-
-    private String replaceId(String id, Map<String, String> globalIdMap) {
-        if (globalIdMap.containsKey(id)) {
-            return globalIdMap.get(id);
-        }
-        String newId = UUID.randomUUID().toString();
-        globalIdMap.put(id, newId);
-        return newId;
-    }
-
-    protected void replaceRmProjectId(Map<String, Object> rmProject) {
-        Map<String, String> globalIdMap = new HashMap<>();
-        Map<String, Object> project = (Map<String, Object>) rmProject.get("project");
-        Map<String, Object> content = (Map<String, Object>) project.get("content");
-        Map<String, Object> contentCollections = (Map<String, Object>) content.get("collections");
-        Set<String> contentCollectionKeys = new HashSet<>(contentCollections.keySet());
-        for (String key : contentCollectionKeys) {
-            Map<String, Object> collection = (Map<String, Object>) contentCollections.get(key);
-            String replaceKey = replaceId(key, globalIdMap);
-            contentCollections.remove(key);
-            contentCollections.put(replaceKey, collection);
-        }
-
-        Map<String, Object> contentMapping = (Map<String, Object>) content.get("mappings");
-        Set<String> contentMappingKeys = new HashSet<>(contentMapping.keySet());
-        for (String key : contentMappingKeys) {
-            Map<String, Object> mapping = (Map<String, Object>) contentMapping.get(key);
-            String replaceKey = replaceId(key, globalIdMap);
-            contentMapping.remove(key);
-            contentMapping.put(replaceKey, mapping);
-        }
-        contentMappingKeys = new HashSet<>(contentMapping.keySet());
-        for (String key : contentMappingKeys) {
-            Map<String, Object> mapping = (Map<String, Object>) contentMapping.get(key);
-            mapping.put("collectionId", replaceId((String) mapping.get("collectionId"), globalIdMap));
-        }
-        replaceRelationShipsKey(globalIdMap, content);
-    }
-
-    protected void replaceRelationShipsKey(Map<String, String> globalIdMap, Map<String, Object> content) {
-        Map<String, Object> relationships = content.get("relationships") == null ? new HashMap<>() : (Map<String, Object>) content.get("relationships");
-        Map<String, Object> relationshipsCollection = relationships.get("collections") == null ? new HashMap<>() : (Map<String, Object>) relationships.get("collections");
-        Set<String> relationshipsCollectionKeys = new HashSet<>(relationshipsCollection.keySet());
-        Map<String, Object> relationshipsMapping = relationships.get("mappings") == null ? new HashMap<>() : (Map<String, Object>) relationships.get("mappings");
-        for (String key : relationshipsCollectionKeys) {
-            Map<String, Object> collection = (Map<String, Object>) relationshipsCollection.get(key);
-            String replaceKey = replaceId(key, globalIdMap);
-            relationshipsCollection.remove(key);
-            relationshipsCollection.put(replaceKey, collection);
-        }
-        for (String key : relationshipsCollection.keySet()) {
-            Map<String, Object> collection = (Map<String, Object>) relationshipsCollection.get(key);
-            List<String> oldMappingIds = (List<String>) collection.get("mappings");
-            List<String> newMappingIds = new ArrayList<>();
-            for (String oldMappingId : oldMappingIds) {
-                newMappingIds.add(replaceId(oldMappingId, globalIdMap));
-            }
-            collection.put("mappings", newMappingIds);
-        }
-
-        Set<String> relationshipsMappingKeys = new HashSet<>(relationshipsMapping.keySet());
-        for(String key : relationshipsMappingKeys) {
-            Map<String, Object> mapping = (Map<String, Object>) relationshipsMapping.get(key);
-            String replaceKey = replaceId(key, globalIdMap);
-            relationshipsMapping.remove(key);
-            relationshipsMapping.put(replaceKey, mapping);
-        }
-
-        for(String key : relationshipsMapping.keySet()) {
-            Map<String, Object> mapping = (Map<String, Object>) relationshipsMapping.get(key);
-            List<String> oldChildren = (List<String>) mapping.get("children");
-            List<String> newChildren = new ArrayList<>();
-            for (String oldChild : oldChildren) {
-                newChildren.add(replaceId(oldChild, globalIdMap));
-            }
-            mapping.put("children", newChildren);
-        }
-    }
-
-    protected Map<String, String> parseTaskFromRm(String rmJson, String sourceConnectionId, String targetConnectionId, UserDetail user) {
-        Map<String, String> sourceToJs = new HashMap<>();
-        Map<String, String> parsedTpTasks = new HashMap<>();
-        Map<String, Object> rmProject = new HashMap<>();
-        Map<String, Map<String, Map<String, Object>>> renameFields = new HashMap();
-        try {
-            rmProject = new ObjectMapper().readValue(rmJson, HashMap.class);
-        } catch (Exception e) {
-            throw new BizException("Can not convert rmProject");
-        }
-
-        replaceRmProjectId(rmProject);
-
-        List<Map<String, Object>> tpTasks = new ArrayList<>();
-        HashMap<String, Object> project = (HashMap<String, Object>) rmProject.get("project");
-        HashMap<String, Object> content = (HashMap<String, Object>) project.get("content");
-        HashMap<String, Object> contentCollections = (HashMap<String, Object>) content.get("collections");
-        for (String key : contentCollections.keySet()) {
-            Map<String, Object> task = new HashMap<>();
-            task.put("id", key);
-            task.put("name", ((Map<String, Map<String, String>>)contentCollections.get(key)).get("name"));
-            task.put("targetModelName", ((Map<String, Map<String, String>>)contentCollections.get(key)).get("name"));
-            tpTasks.add(task);
-        }
-        for (Map<String, Object> task : tpTasks) {
-            task.put("editVersion", System.currentTimeMillis());
-            task.put("syncType", "sync");
-            task.put("type", "initial_sync+cdc");
-            task.put("mappingTemplate", "sync");
-            task.put("status", "edit");
-            task.put("user_id", user.getUserId());
-            task.put("customId", user.getCustomerId());
-            task.put("createUser", user.getUsername());
-
-            String targetModelName = (String) task.get("targetModelName");
-
-            Map<String, Object> dag = new HashMap<>();
-            List<Map<String, Object>> nodes = new ArrayList<>();
-            List<Map<String, Object>> edges = new ArrayList<>();
-
-            Map<String, Object> contentMapping = content.get("mappings") == null ? new HashMap<>() : (Map<String, Object>) content.get("mappings");
-
-
-            String tpTable;
-
-            // 把源节点都加进去, 这里如果有一些 字段改名, 或者新字段生成的操作, 增加一个 JS 处理器
-            List<String> sourceNodes = new ArrayList<>();
-            Map<String,List<Map<String, Object>>> contentDeleteOperations = new HashMap<>();
-            Map<String,List<Map<String, Object>>> contentRenameOperations = new HashMap<>();
-            for (String key : contentMapping.keySet()) {
-                Map<String, Object> node = new HashMap<>();
-                Map<String, Object> contentMappingzValue = (Map<String, Object>) contentMapping.get(key);
-                if (!contentMappingzValue.get("collectionId").equals(task.get("id"))) {
-                    continue;
-                }
-
-                // 用 . 分割, 取最后一位
-                String table = (String) contentMappingzValue.get("table");
-                tpTable = table.split("\\.")[table.split("\\.").length - 1];
-                Map<String, Map<String, Object>> tableRenameFields = new HashMap<>();
-
-                node.put("type", "table");
-                node.put("tableName", tpTable);
-                node.put("name", tpTable);
-                node.put("id", key);
-                node.put("connectionId", sourceConnectionId);
-                nodes.add(node);
-
-                // 增加 JS 处理器
-                Map<String, Object> fields = (Map<String, Object>) contentMappingzValue.get("fields");
-
-                List<Map<String, Object>> renameOperations = new ArrayList<>();
-
-                List<Map<String, Object>> deleteOperations = new ArrayList<>();
-                contentDeleteOperations.put(key,deleteOperations);
-                contentRenameOperations.put(key,renameOperations);
-                for (String field : fields.keySet()) {
-                    Map<String, Object> fieldMap = (Map<String, Object>) fields.get(field);
-                    Map<String, Object> source = (Map<String, Object>) fieldMap.get("source");
-                    Map<String, Object> target = (Map<String, Object>) fieldMap.get("target");
-                    Map<String, Object> newName = getNewNameMap(target, source);
-                    tableRenameFields.put(source.get("name").toString(), newName);
-                    Object isPk = source.get("isPrimaryKey");
-                    if (!(Boolean)target.get("included")) {
-                        Map<String, Object> deleteOperation = getDeleteOperation(source.get("name").toString(),isPk);
-                        deleteOperations.add(deleteOperation);
-                        continue;
-                    }
-
-                    if (source.get("name").equals(target.get("name"))) {
-                        continue;
-                    }
-
-                    Map<String, Object> renameOperation = getRenameOperation(source.get("name"), target.get("name"));
-                    renameOperations.add(renameOperation);
-                }
-
-                renameFields.put(tpTable, tableRenameFields);
-
-                String script = "";
-                String declareScript = "";
-                Map<String, Object> calculatedFields = (Map<String, Object>) contentMappingzValue.get("calculatedFields");
-                for (String field : calculatedFields.keySet()) {
-                    Map<String, Object> fieldMap = (Map<String, Object>) calculatedFields.get(field);
-                    String newFieldName = (String) fieldMap.get("name");
-                    String newFieldeExpression = (String) fieldMap.get("expression");
-                    newFieldeExpression = newFieldeExpression.replace("columns[", "record[");
-                    script += "    record[\"" + newFieldName + "\"] = " + newFieldeExpression + ";\n";
-                }
-
-                if (!script.equals("") || !declareScript.equals("")) {
-                    script = "function process(record){" + script;
-                    script += "    return record;\n";
-                    script += "}";
-                }
-                String sourceId = (String) node.get("id");
-                //add jsNode
-                if (!script.equals("")) {
-                    String jsId = addJSNode(tpTable, script, declareScript, nodes, sourceId, edges);
-                    sourceId = jsId;
-                }
-                //add rename processor node
-                sourceId = addRenameNode(tpTable, renameOperations, sourceId, nodes, edges);
-                //add delete processor node
-                if (!deleteOperations.isEmpty()) {
-                    sourceId = addDeleteNode(tpTable, deleteOperations,  sourceId,nodes, edges);
-                }
-
-                // 记录映射
-                sourceToJs.put((String) node.get("id"), sourceId);
-                sourceNodes.add(sourceId);
-            }
-
-
-            List<Map<String, Object>> mergeProperties = new ArrayList<>();
-            Map<String, Object> schema = (Map<String, Object>) rmProject.get("schema");
-            Map<String, Object> full = (Map<String, Object>) schema.get("full");
-
-            Map<String, Object> relationships = content.get("relationships") == null ? new HashMap<>() : (Map<String, Object>) content.get("relationships");
-            Map<String, Object> relationshipsMapping = relationships.get("mappings") == null ? new HashMap<>() : (Map<String, Object>) relationships.get("mappings");
-            String rootNodeId = "";
-            String rootTableName = "";
-            Map<String, Object> rootContentMappingValue = new HashMap<>();
-            for (String key : contentMapping.keySet()) {
-                Map<String, Object> contentMappingValue = (Map<String, Object>) contentMapping.get(key);
-                if (!contentMappingValue.get("collectionId").equals(task.get("id"))) {
-                    continue;
-                }
-                Map<String, Object> setting = (Map<String, Object>) contentMappingValue.get("settings");
-                if (setting == null) {
-                    continue;
-                }
-                String type = (String) setting.get("type");
-                if (type == null) {
-                    continue;
-                }
-                if (type.equals("NEW_DOCUMENT")) {
-                    rootNodeId = key;
-                    rootContentMappingValue = contentMappingValue;
-                    rootTableName = ((String) rootContentMappingValue.get("table")).split("\\.")[((String) rootContentMappingValue.get("table")).split("\\.").length - 1];
-                    break;
-                }
-            }
-
-            if (rootNodeId == null || rootNodeId.equals("")) {
-                continue;
-            }
-
-            // 增加主从合并节点
-            String mergeNodeId = UUID.randomUUID().toString().toLowerCase();
-            Map<String, Object> mergeNode = new HashMap<>();
-            mergeNode.put("type", "merge_table_processor");
-            mergeNode.put("name", "merge");
-            mergeNode.put("id", mergeNodeId);
-            mergeNode.put(CATALOG, PROCESSOR);
-            mergeNode.put("mergeMode", "main_table_first");
-            mergeNode.put("isTransformed", false);
-            Map<String, Object> rootProperties = new HashMap<>();
-            rootProperties.put("targetPath", "");
-            rootProperties.put("id", sourceToJs.get(rootNodeId));
-            rootProperties.put(RM_ID_KEY, rootNodeId);
-            rootProperties.put("mergeType", "updateOrInsert");
-            tpTable = ((String) ((Map<String, Object>) contentMapping.get(rootNodeId)).get("table")).split("\\.")[((String) ((Map<String, Object>) contentMapping.get(rootNodeId)).get("table")).split("\\.").length - 1];
-            rootProperties.put("tableName", tpTable);
-            genProperties(rootProperties, contentMapping, relationshipsMapping, full, sourceToJs, renameFields,contentDeleteOperations,contentRenameOperations);
-            mergeProperties.add(rootProperties);
-            mergeNode.put("mergeProperties", mergeProperties);
-            Boolean needMergeNode = true;
-            List<Object> children = (List<Object>) rootProperties.get("children");
-            if (children == null || children.isEmpty()) {
-                needMergeNode = false;
-            }
-
-
-            if (needMergeNode) {
-                nodes.add(mergeNode);
-                for (String sourceId : sourceNodes) {
-                    Map<String, Object> edge = new HashMap<>();
-                    edge.put("source", sourceId);
-                    edge.put("target", mergeNodeId);
-                    edges.add(edge);
-                }
-                contentDeleteOperations.forEach((k, v) -> {
-                    Map<String, Object> contentMappingzValue = (Map<String, Object>) contentMapping.get(k);
-                    String table = contentMappingzValue.get("table").toString();
-                    String finalTable = table.split("\\.")[table.split("\\.").length - 1];
-                    v.removeIf((delOp) -> {
-                        boolean flag = Boolean.TRUE.equals(delOp.get("isPk"));
-                        if(flag){
-                            Map<String, Map<String, Object>> map = renameFields.get(finalTable);
-                            String name = delOp.get("field").toString();
-                            Map<String, Object> renameOperation = getRenameOperation(name, map.get(name).get("target").toString());
-                            contentRenameOperations.get(k).add(renameOperation);
-                        }
-                        return flag;
-                    });
-
-                });
-
-            }
-
-
-            // 把目标节点加进去
-            Map<String, Object> targetNode = new HashMap<>();
-            targetNode.put("type", "table");
-            targetNode.put("existDataProcessMode", "keepData");
-            targetNode.put("name", targetModelName);
-            targetNode.put("id", task.get("id"));
-            targetNode.put("connectionId", targetConnectionId);
-            targetNode.put("tableName", targetModelName);
-            List<String> updateConditionFields = new ArrayList<>();
-            Map<String, Map<String, Object>> rootRenameFields = renameFields.get(rootTableName);
-            Map<String, Object> rootFields = (Map<String, Object>) rootContentMappingValue.get("fields");
-            for (String field : rootFields.keySet()) {
-                Map<String, Object> fieldMap = (Map<String, Object>) rootFields.get(field);
-                Map<String, Object> source = (Map<String, Object>) fieldMap.get("source");
-                Boolean isPrimaryKey = (Boolean) source.get("isPrimaryKey");
-                if (isPrimaryKey != null && isPrimaryKey) {
-                    updateConditionFields.add(rootRenameFields.get(field).get("target").toString());
-                }
-            }
-
-            targetNode.put("updateConditionFields", updateConditionFields);
-            nodes.add(targetNode);
-            Map<String, Object> edge = new HashMap<>();
-            if (needMergeNode) {
-                edge.put("source", mergeNodeId);
-            } else {
-                edge.put("source", sourceNodes.get(0));
-            }
-            edge.put("target", targetNode.get("id"));
-            edges.add(edge);
-
-            dag.put("nodes", nodes);
-            dag.put("edges", edges);
-            task.put("dag", dag);
-            parsedTpTasks.put((String) task.get("id"), JsonUtil.toJson(task));
-        }
-        return parsedTpTasks;
-    }
-
-    protected String addJSNode(String tpTable, String script, String declareScript, List<Map<String, Object>> nodes, String sourceId, List<Map<String, Object>> edges) {
-        String jsId = UUID.randomUUID().toString().toLowerCase();
-        Map<String, Object> jsNode = new HashMap<>();
-        jsNode.put("type", "js_processor");
-        jsNode.put("name", tpTable);
-        jsNode.put("id", jsId);
-        jsNode.put("jsType", 1);
-        jsNode.put(PROCESSOR_THREAD_NUM, 1);
-        jsNode.put(CATALOG, PROCESSOR);
-        jsNode.put(ELEMENT_TYEP, "Node");
-        jsNode.put("script", script);
-        jsNode.put("declareScript", declareScript);
-        nodes.add(jsNode);
-        Map<String, Object> edge = new HashMap<>();
-        edge.put("source", sourceId);
-        edge.put("target", jsId);
-        edges.add(edge);
-        return jsId;
-    }
-
-    protected String addRenameNode(String tpTable,  List<Map<String, Object>> renameOperations, String sourceId,List<Map<String, Object>> nodes, List<Map<String, Object>> edges) {
-        Map<String, Object> renameNode = new HashMap<>();
-        String renameId = UUID.randomUUID().toString().toLowerCase();
-        renameNode.put("id", renameId);
-        renameNode.put(CATALOG, PROCESSOR);
-        renameNode.put(ELEMENT_TYEP, "Node");
-        renameNode.put("fieldsNameTransform", "");
-        renameNode.put("isTransformed", false);
-        renameNode.put("name", "Rename " + tpTable);
-        renameNode.put(PROCESSOR_THREAD_NUM, 1);
-        renameNode.put("type", "field_rename_processor");
-        nodes.add(renameNode);
-        renameNode.put("operations", renameOperations);
-        Map<String, Object> edge = new HashMap<>();
-        edge.put("source", sourceId);
-        edge.put("target", renameId);
-        edges.add(edge);
-        sourceId = renameId;
-        return sourceId;
-    }
-
-    protected String addDeleteNode(String tpTable, List<Map<String, Object>> deleteOperations,  String sourceId,List<Map<String, Object>> nodes, List<Map<String, Object>> edges) {
-        Map<String, Object> deleteNode = new HashMap<>();
-        String deleteId = UUID.randomUUID().toString().toLowerCase();
-        deleteNode.put("id", deleteId);
-        deleteNode.put(CATALOG, PROCESSOR);
-        deleteNode.put("deleteAllFields", false);
-        deleteNode.put(ELEMENT_TYEP, "Node");
-        deleteNode.put("name", "Delete " + tpTable);
-        deleteNode.put("type", "field_add_del_processor");
-        deleteNode.put(PROCESSOR_THREAD_NUM, 1);
-        deleteNode.put("operations", deleteOperations);
-        nodes.add(deleteNode);
-        Map<String, Object> edge = new HashMap<>();
-        edge.put("source", sourceId);
-        edge.put("target", deleteId);
-        edges.add(edge);
-        sourceId = deleteId;
-        return sourceId;
-    }
-
-    protected Map<String, Object> getDeleteOperation(Object deleteFieldName, Object isPrimaryKey) {
-        Map<String, Object> deleteOperation = new HashMap<>();
-        deleteOperation.put("id", UUID.randomUUID().toString().toLowerCase());
-        deleteOperation.put("field", deleteFieldName);
-        deleteOperation.put("op", "REMOVE");
-        deleteOperation.put("operand", "true");
-        deleteOperation.put("label", deleteFieldName);
-        deleteOperation.put("isPk",isPrimaryKey);
-        return deleteOperation;
-    }
-
-    protected Map<String, Object> getRenameOperation(Object source, Object target) {
-        Map<String, Object> fieldRenameOperation = new HashMap<>();
-        fieldRenameOperation.put("id", UUID.randomUUID().toString().toLowerCase());
-        fieldRenameOperation.put("field", source);
-        fieldRenameOperation.put("op", "RENAME");
-        fieldRenameOperation.put("operand", target);
-        return fieldRenameOperation;
-    }
-
-    protected Map<String, Object> getNewNameMap(Map<String, Object> target, Map<String, Object> source) {
-        Map<String, Object> newName = new HashMap<>();
-        newName.put("target", target.get("name").toString());
-        newName.put("isPrimaryKey", (Boolean) source.get("isPrimaryKey"));
-        return newName;
-    }
-
+    @Override
     public void importRmProject(MultipartFile multipartFile, UserDetail user, boolean cover, List<String> tags, String source, String sink) throws IOException {
-        String json = new String(multipartFile.getBytes());
-        Map<String, String> tasks = parseTaskFromRm(json, source, sink, user);
-        if (tasks == null) {
-            return;
-        }
-        List<TaskDto> tpTasks = new ArrayList<>();
-        for (String key: tasks.keySet()) {
-            TaskDto tpTask = JsonUtil.parseJsonUseJackson(tasks.get(key), TaskDto.class);
-            tpTask.setTransformTaskId(new ObjectId().toHexString());
-            tpTasks.add(tpTask);
-        }
+        ParseParam param = new ParseParam()
+                .withMultipartFile(multipartFile)
+                .withSink(sink)
+                .withSource(source)
+                .withUser(user);
+        ParseRelMig<TaskDto> redirect = (ParseRelMig<TaskDto>)ParseRelMig.redirect(param);
+        List<TaskDto> tpTasks = redirect.parse();
         batchImport(tpTasks, user, cover, tags, new HashMap<>(), new HashMap<>());
         checkJsProcessorTestRun(user, tpTasks);
     }
@@ -3668,6 +3058,7 @@ public void batchImport(List<TaskDto> taskDtos, UserDetail user, boolean cover,
 
             taskDto.setListtags(null);
             taskDto.setStatus(TaskDto.STATUS_EDIT);
+            taskDto.setSyncStatus(SyncStatus.NORMAL);
             taskDto.setTaskRecordId(new ObjectId().toHexString()); // 导入后不读旧指标数据
 
             Map<String, Object> attrs = taskDto.getAttrs();
@@ -3886,6 +3277,7 @@ public UpdateResult renewNotSendMq(TaskDto taskDto, UserDetail user) {
                 .unset("milestones")
                 .unset("tmCurrentTime")
                 .set("agentTags", null)
+                .set("syncStatus", SyncStatus.NORMAL)
                 .set("scheduleTimes", null)
                 .set("scheduleTime", null)
                 .set("messages", null)
@@ -4329,6 +3721,7 @@ public void pause(TaskDto taskDto, UserDetail user, boolean force, boolean resta
         field.put("status", true);
         TaskDto statusTask = findById(taskDto.getId(), field);
         taskDto.setStatus(statusTask.getStatus());
+        taskDto.setSyncStatus(statusTask.getSyncStatus());
         if ((TaskDto.STATUS_STOP.equals(taskDto.getStatus()) || TaskDto.STATUS_STOPPING.equals(taskDto.getStatus())) && restart) {
             Update update = Update.update("restartFlag", true).set("restartUserId", user.getUserId());
             Query query = new Query(Criteria.where("_id").is(taskDto.getId()));
@@ -4729,7 +4122,7 @@ public void startPlanCronTask() {
     }
 
     public TaskDto findByCacheName(String cacheName, UserDetail user) {
-        Criteria taskCriteria = Criteria.where("dag.nodes").elemMatch(Criteria.where(CATALOG).is("memCache").and("cacheName").is(cacheName));
+        Criteria taskCriteria = Criteria.where("dag.nodes").elemMatch(Criteria.where(CATALOG).is("memCache").and("cacheName").is(cacheName)).and("is_deleted").is(false);
         Query query = new Query(taskCriteria);
 
         return findOne(query, user);
@@ -5180,7 +4573,7 @@ public boolean checkCloudTaskLimit(ObjectId taskId,UserDetail user,boolean check
     }
 
     protected void checkUnwindProcess(DAG dag){
-        if(CollectionUtils.isEmpty(dag.getNodes()))return;
+        if(null == dag || CollectionUtils.isEmpty(dag.getNodes()))return;
         AtomicBoolean check = new AtomicBoolean(false);
         dag.getNodes().forEach(node -> {
             if(node instanceof UnwindProcessNode) check.set(true);
@@ -5193,4 +4586,5 @@ protected void checkUnwindProcess(DAG dag){
             });
         }
     }
+
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseBaseVersionRelMigImpl.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseBaseVersionRelMigImpl.java
new file mode 100644
index 0000000000..74402e9372
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseBaseVersionRelMigImpl.java
@@ -0,0 +1,41 @@
+package com.tapdata.tm.task.service.batchin;
+
+import com.tapdata.tm.base.exception.BizException;
+import com.tapdata.tm.commons.task.dto.TaskDto;
+import com.tapdata.tm.commons.util.JsonUtil;
+import com.tapdata.tm.task.service.batchin.entity.ParseParam;
+import org.bson.types.ObjectId;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * this instance will support parse relMig file when version value is 1.2.0
+ * @author Gavin'Xiao
+ * */
+public class ParseBaseVersionRelMigImpl extends ParseRelMigFile {
+
+    public ParseBaseVersionRelMigImpl(ParseParam param) {
+        super(param);
+    }
+
+    @Override
+    public List<TaskDto> parse() {
+        try {
+            Map<String, String> tasks = doParse(param.getSource(), param.getSink(), param.getUser());
+            if (tasks == null) {
+                return new ArrayList<>();
+            }
+            List<TaskDto> tpTasks = new ArrayList<>();
+            tasks.forEach((key, value) -> {
+                TaskDto tpTask = JsonUtil.parseJsonUseJackson(value, TaskDto.class);
+                tpTask.setTransformTaskId(new ObjectId().toHexString());
+                tpTasks.add(tpTask);
+            });
+            return tpTasks;
+        } catch (Exception e) {
+            throw new BizException("relMig.parse.failed", e.getMessage());
+        }
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseRelMig.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseRelMig.java
new file mode 100644
index 0000000000..d28df21e4a
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseRelMig.java
@@ -0,0 +1,40 @@
+package com.tapdata.tm.task.service.batchin;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.tapdata.tm.base.exception.BizException;
+import com.tapdata.tm.commons.task.dto.ParentTaskDto;
+import com.tapdata.tm.task.service.batchin.constant.KeyWords;
+import com.tapdata.tm.task.service.batchin.constant.ParseRelMigFileVersionMapping;
+import com.tapdata.tm.task.service.batchin.entity.ParseParam;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.lang.reflect.Constructor;
+import java.util.List;
+import java.util.Map;
+
+public interface ParseRelMig<T extends ParentTaskDto> {
+    String PROCESSOR_THREAD_NUM = "processorThreadNum";
+    String CATALOG = "catalog";
+    String ELEMENT_TYPE = "elementType";
+    String PROCESSOR = "processor";
+    String RM_ID = "rm_id";
+
+    List<T> parse();
+
+    static ParseRelMig<?> redirect(ParseParam param) {
+        try {
+            MultipartFile multipartFile = param.getMultipartFile();
+            String relMig = new String(multipartFile.getBytes());
+            param.setRelMigStr(relMig);
+            Map<String, Object> relMigInfo = (Map<String, Object>) new ObjectMapper().readValue(param.getRelMigStr(), Map.class);
+            param.setRelMigInfo(relMigInfo);
+            String version = String.valueOf(relMigInfo.get(KeyWords.VERSION));
+            Class<? extends ParseRelMig> instance = ParseRelMigFileVersionMapping.getInstance(version);
+            Constructor<? extends ParseRelMig> declaredConstructor = instance.getDeclaredConstructor(ParseParam.class);
+            return declaredConstructor.newInstance(param);
+        } catch (Exception e) {
+            throw new BizException("relMig.parse.failed", e.getMessage());
+        }
+    }
+
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseRelMig13OrMoreImpl.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseRelMig13OrMoreImpl.java
new file mode 100644
index 0000000000..a653df7642
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseRelMig13OrMoreImpl.java
@@ -0,0 +1,26 @@
+package com.tapdata.tm.task.service.batchin;
+
+import com.tapdata.tm.task.service.batchin.constant.KeyWords;
+import com.tapdata.tm.task.service.batchin.dto.TablePathInfo;
+import com.tapdata.tm.task.service.batchin.entity.ParseParam;
+
+import java.util.Map;
+
+public class ParseRelMig13OrMoreImpl extends ParseBaseVersionRelMigImpl {
+
+    public ParseRelMig13OrMoreImpl(ParseParam param) {
+        super(param);
+    }
+
+    @Override
+    public TablePathInfo getTablePathInfo(Map<String, Object> contentMapping) {
+        String tableId = String.valueOf(getFromMap(contentMapping, KeyWords.TABLE));
+        Map<String, Object> content = parseMap(getFromMap(project, KeyWords.CONTENT));
+        Map<String, Object> tables = parseMap(getFromMap(content, KeyWords.TABLES));
+        Map<String, Object> tablePath = parseMap(getFromMap(tables, tableId));
+        Map<String, Object> tableInfo = parseMap(getFromMap(tablePath, KeyWords.PATH));
+        return new TablePathInfo(String.valueOf(tableInfo.get(KeyWords.DATABASE)),
+                String.valueOf(tableInfo.get(KeyWords.SCHEMA)),
+                String.valueOf(tableInfo.get(KeyWords.TABLE)));
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseRelMigFile.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseRelMigFile.java
new file mode 100644
index 0000000000..b17089b59c
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseRelMigFile.java
@@ -0,0 +1,837 @@
+package com.tapdata.tm.task.service.batchin;
+
+import cn.hutool.core.collection.CollUtil;
+import com.tapdata.tm.commons.task.dto.TaskDto;
+import com.tapdata.tm.commons.util.JsonUtil;
+import com.tapdata.tm.config.security.UserDetail;
+import com.tapdata.tm.task.service.batchin.constant.KeyWords;
+import com.tapdata.tm.task.service.batchin.dto.TablePathInfo;
+import com.tapdata.tm.task.service.batchin.entity.AddJsNodeParam;
+import com.tapdata.tm.task.service.batchin.entity.AddMergerNodeParam;
+import com.tapdata.tm.task.service.batchin.entity.DoMergeParam;
+import com.tapdata.tm.task.service.batchin.entity.GenericPropertiesParam;
+import com.tapdata.tm.task.service.batchin.entity.ParseParam;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+public abstract class ParseRelMigFile implements ParseRelMig<TaskDto> {
+    protected Map<String, Object> relMigInfo;
+    protected String version;
+    protected Map<String, Object> project;
+    protected Map<String, Object> schema;
+    protected List<Map<String, Object>> queries;
+    protected ParseParam param;
+    protected Map<String, Object> content;
+
+    protected ParseRelMigFile(ParseParam param) {
+        this.param = param;
+        this.relMigInfo = Optional.ofNullable(param.getRelMigInfo()).orElse(new HashMap<>());
+        this.version = String.valueOf(relMigInfo.get(KeyWords.VERSION));
+        this.project = parseMap(getFromMap(relMigInfo, KeyWords.PROJECT));
+        this.schema = parseMap(getFromMap(relMigInfo, KeyWords.SCHEMA));
+        this.queries = Optional.ofNullable((List<Map<String, Object>>) getFromMap(relMigInfo, KeyWords.QUERIES)).orElse(new ArrayList<>());
+        this.content = parseMap(getFromMap(project, KeyWords.CONTENT));
+    }
+
+    public List<String> addJsNodeIfInNeed(AddJsNodeParam param) {
+        String sourceConnectionId = param.getSourceConnectionId();
+        Map<String, Map<String, Map<String, Object>>> renameFields = param.getRenameFields();
+        Map<String, String> sourceToJs = param.getSourceToJs();
+        Map<String, Object> contentMapping = param.getContentMapping();
+        Map<String, List<Map<String, Object>>> contentDeleteOperations = param.getContentDeleteOperations();
+        Map<String, List<Map<String, Object>>> contentRenameOperations = param.getContentRenameOperations();
+        Map<String, Object> task = param.getTask();
+        List<Map<String, Object>> nodes = param.getNodes();
+        List<Map<String, Object>> edges = param.getEdges();
+        List<String> sourceNodes = new ArrayList<>();
+        for (String key : contentMapping.keySet()) {
+            Map<String, Object> node = new HashMap<>();
+            Map<String, Object> contentMappingValue = parseMap(getFromMap(contentMapping, key));
+            if (!contentMappingValue.get(KeyWords.COLLECTION_ID).equals(task.get(KeyWords.ID))) {
+                continue;
+            }
+
+            TablePathInfo tablePathInfo = getTablePathInfo(contentMappingValue);
+            String tpTable = tablePathInfo.getTable();
+
+            node.put(KeyWords.TYPE, KeyWords.TABLE);
+            node.put(KeyWords.TABLE_NAME, tpTable);
+            node.put(KeyWords.NAME, tpTable);
+            node.put(KeyWords.ID, key);
+            node.put(KeyWords.CONNECTION_ID, sourceConnectionId);
+            nodes.add(node);
+
+            Map<String, Object> fields = parseMap(getFromMap(contentMappingValue, KeyWords.FIELDS));
+
+            List<Map<String, Object>> renameOperations = new ArrayList<>();
+            List<Map<String, Object>> deleteOperations = new ArrayList<>();
+            contentDeleteOperations.put(key, deleteOperations);
+            contentRenameOperations.put(key, renameOperations);
+
+            Map<String, Map<String, Object>> tableRenameFields = new HashMap<>();
+            renameFields.put(tpTable, tableRenameFields);
+            addRenameOrDeleteField(fields, tableRenameFields, renameOperations, deleteOperations);
+
+            // JS script generator
+            String declareScript = KeyWords.EMPTY;
+            String jsScript = jsScriptGenerator(contentMappingValue);
+
+            String sourceId = (String) node.get(KeyWords.ID);
+            //add jsNode
+            if (!KeyWords.EMPTY.equals(jsScript)) {
+                sourceId = addJSNode(tpTable, jsScript, declareScript, nodes, sourceId, edges);
+            }
+
+            //add rename processor node
+            sourceId = addRenameNode(tpTable, renameOperations, sourceId, nodes, edges);
+
+            //add delete processor node
+            if (!deleteOperations.isEmpty()) {
+                sourceId = addDeleteNode(tpTable, deleteOperations, sourceId, nodes, edges);
+            }
+            // save mapping
+            sourceToJs.put((String) node.get(KeyWords.ID), sourceId);
+            sourceNodes.add(sourceId);
+        }
+        return sourceNodes;
+    }
+
+    public void addRenameOrDeleteField(Map<String, Object> fields,
+                                          Map<String, Map<String, Object>> tableRenameFields,
+                                          List<Map<String, Object>> renameOperations,
+                                          List<Map<String, Object>> deleteOperations) {
+        for (String field : fields.keySet()) {
+            Map<String, Object> fieldMap = parseMap(getFromMap(fields, field));
+            Map<String, Object> source = parseMap(getFromMap(fieldMap, KeyWords.SOURCE));
+            Map<String, Object> target = parseMap(getFromMap(fieldMap, KeyWords.TARGET));
+            Map<String, Object> newName = getNewNameMap(target, source);
+            tableRenameFields.put(source.get(KeyWords.NAME).toString(), newName);
+            Object isPk = source.get(KeyWords.IS_PRIMARY_KEY);
+            if (!Boolean.TRUE.equals(target.get(KeyWords.INCLUDED))) {
+                Map<String, Object> deleteOperation = getDeleteOperation(source.get(KeyWords.NAME).toString(), isPk);
+                deleteOperations.add(deleteOperation);
+            } else if (!source.get(KeyWords.NAME).equals(target.get(KeyWords.NAME))) {
+                Map<String, Object> renameOperation = getRenameOperation(source.get(KeyWords.NAME), target.get(KeyWords.NAME));
+                renameOperations.add(renameOperation);
+            }
+        }
+    }
+
+    /**
+     * 如果存在字段的值变换逻辑，则增加对应的js脚本，用于新增js节点
+     * **/
+    public String jsScriptGenerator(Map<String, Object> contentMappingValue) {
+        StringBuilder script = new StringBuilder();
+        Map<String, Object> calculatedFields = parseMap(getFromMap(contentMappingValue, KeyWords.CALCULATED_FIELDS));
+        for (String field : calculatedFields.keySet()) {
+            Map<String, Object> fieldMap = parseMap(getFromMap(calculatedFields, field));
+            String newFieldName = (String) fieldMap.get(KeyWords.NAME);
+            String newFieldExpression = (String) fieldMap.get(KeyWords.EXPRESSION);
+            newFieldExpression = newFieldExpression.replace("columns[", "record[");
+            script.append("\trecord[\"")
+                    .append(newFieldName)
+                    .append("\"] = ")
+                    .append(newFieldExpression)
+                    .append(";\n");
+        }
+
+        if (!script.toString().equals(KeyWords.EMPTY)) {
+            script.insert(0, "function process(record){");
+            script.append("    return record;\n}");
+        }
+        return script.toString();
+    }
+
+    public void objectArrayAsNormalArray(StringBuilder script, Map<String, Object> contentMappingValue, Map<String, Object> fields) {
+        Map<String, Object> fieldSettingInfo = parseMap(getFromMap(contentMappingValue, KeyWords.SETTINGS));
+        if (Boolean.TRUE.equals(getFromMap(fieldSettingInfo, KeyWords.PRIMITIVE))) {
+            Map<String, Object> primitiveField = scanPrimitiveField(fields);
+            if (!primitiveField.isEmpty()) {
+                String embeddedPath = String.valueOf(getFromMap(fieldSettingInfo, KeyWords.EMBEDDED_PATH));
+                Map<String, Object> source = parseMap(getFromMap(primitiveField, KeyWords.SOURCE));
+                String sourceFieldName = String.valueOf(getFromMap(source, KeyWords.NAME));
+                String item = "newArr_" + embeddedPath;
+                script.append("\tlet ").append(item).append(" = [];\n")
+                        .append("\tfor(let item of record[\"").append(embeddedPath).append("\"]) {\n")
+                        .append("\t  ").append(item).append(".push(item[\"").append(sourceFieldName).append("\"]);\n")
+                        .append("\t}\n")
+                        .append("\trecord[\"").append(embeddedPath).append("\"] = ").append(item).append(";\n");
+
+            }
+        }
+    }
+
+    public Map<String, Object> scanPrimitiveField(Map<String, Object> fields) {
+        List<Map<String, Object>> included = fields.values()
+                .stream()
+                .filter(o -> {
+                    Map<String, Object> fieldInfo = parseMap(getFromMap(parseMap(o), KeyWords.TARGET));
+                    return Boolean.TRUE.equals(fieldInfo.get(KeyWords.INCLUDED));
+                }).map(this::parseMap)
+                .collect(Collectors.toList());
+        if (!included.isEmpty()) {
+            return included.get(0);
+        }
+        return new HashMap<>();
+    }
+
+    public boolean addMergerNodes(AddMergerNodeParam param) {
+        String rootNodeId = param.getRootNodeId();
+        String mergeNodeId = param.getMergeNodeId();
+        Map<String, Object> mergeNode = param.getMergeNode();
+        Map<String, String> sourceToJs = param.getSourceToJs();
+        Map<String, Object> contentMapping = param.getContentMapping();
+        Map<String, Map<String, Map<String, Object>>> renameFields = param.getRenameFields();
+        Map<String, List<Map<String, Object>>> contentDeleteOperations = param.getContentDeleteOperations();
+        Map<String, List<Map<String, Object>>> contentRenameOperations = param.getContentRenameOperations();
+        Map<String, Object> full = parseMap(getFromMap(schema, KeyWords.FULL));
+        List<Map<String, Object>> mergeProperties = new ArrayList<>();
+        Map<String, Object> relationships = content.get(KeyWords.RELATIONSHIPS) == null ? new HashMap<>() : parseMap(getFromMap(content, KeyWords.RELATIONSHIPS));
+        Map<String, Object> relationshipsMapping = relationships.get(KeyWords.MAPPINGS) == null ? new HashMap<>() : parseMap(getFromMap(relationships, KeyWords.MAPPINGS));
+
+        mergeNode.put(KeyWords.TYPE, "merge_table_processor");
+        mergeNode.put(KeyWords.NAME, "merge");
+        mergeNode.put(KeyWords.ID, mergeNodeId);
+        mergeNode.put(CATALOG, PROCESSOR);
+        mergeNode.put("mergeMode", "main_table_first");
+        mergeNode.put(KeyWords.IS_TRANSFORMED, false);
+        Map<String, Object> rootProperties = new HashMap<>();
+        rootProperties.put(KeyWords.TARGET_PATH, KeyWords.EMPTY);
+        rootProperties.put(KeyWords.ID, sourceToJs.get(rootNodeId));
+        rootProperties.put(RM_ID, rootNodeId);
+        rootProperties.put("mergeType", "updateOrInsert");
+        TablePathInfo tablePathInfo = getTablePathInfo(contentMapping, rootNodeId);
+        String tpTable = tablePathInfo.getTable();
+        rootProperties.put(KeyWords.TABLE_NAME, tpTable);
+        GenericPropertiesParam propertiesParam = GenericPropertiesParam.of()
+                .withParent(rootProperties)
+                .withContentMapping(contentMapping)
+                .withRelationshipsMapping(relationshipsMapping)
+                .withFull(full)
+                .withSourceToJS(sourceToJs)
+                .withRenameFields(renameFields)
+                .withContentDeleteOperations(contentDeleteOperations).withContentRenameOperations(contentRenameOperations);
+        genericProperties(propertiesParam);
+        mergeProperties.add(rootProperties);
+        mergeNode.put("mergeProperties", mergeProperties);
+        boolean needMergeNode = true;
+        List<Object> children = (List<Object>) rootProperties.get(KeyWords.CHILDREN);
+        if (children == null || children.isEmpty()) {
+            needMergeNode = false;
+        }
+        if (!needMergeNode) {
+            return false;
+        }
+        return needMergeNode;
+    }
+
+    public void doMerge(DoMergeParam param) {
+        String mergeNodeId = param.getMergeNodeId();
+        List<Map<String, Object>> nodes = param.getNodes();
+        Map<String, Object> contentMapping = param.getContentMapping();
+        Map<String, Object> mergeNode = param.getMergeNode();
+        List<String> sourceNodes = param.getSourceNodes();
+        List<Map<String, Object>> edges = param.getEdges();
+        Map<String, Map<String, Map<String, Object>>> renameFields = param.getRenameFields();
+        Map<String, List<Map<String, Object>>> contentRenameOperations = param.getContentRenameOperations();
+        Map<String, List<Map<String, Object>>> contentDeleteOperations = param.getContentDeleteOperations();
+        nodes.add(mergeNode);
+        for (String sourceId : sourceNodes) {
+            Map<String, Object> edge = new HashMap<>();
+            edge.put(KeyWords.SOURCE, sourceId);
+            edge.put(KeyWords.TARGET, mergeNodeId);
+            edges.add(edge);
+        }
+        contentDeleteOperations.forEach((k, v) -> {
+            Map<String, Object> contentMappingValue = parseMap(getFromMap(contentMapping, k));
+            TablePathInfo pathInfo = getTablePathInfo(contentMappingValue);
+            String finalTable = pathInfo.getTable();
+            v.removeIf(delOp -> {
+                boolean flag = Boolean.TRUE.equals(delOp.get(KeyWords.IS_PK));
+                if (flag) {
+                    Map<String, Map<String, Object>> map = renameFields.get(finalTable);
+                    String name = delOp.get(KeyWords.FIELD).toString();
+                    Map<String, Object> renameOperation = getRenameOperation(name, map.get(name).get(KeyWords.TARGET).toString());
+                    contentRenameOperations.get(k).add(renameOperation);
+                }
+                return flag;
+            });
+        });
+    }
+
+    public List<Map<String, Object>> getTaskInfoList() {
+        List<Map<String, Object>> tpTasks = new ArrayList<>();
+        Map<String, Object> contentCollections = parseMap(getFromMap(content, KeyWords.COLLECTIONS));
+        for (String key : contentCollections.keySet()) {
+            Map<String, Object> task = new HashMap<>();
+            Map<String, Object> map = parseMap(getFromMap(contentCollections, key));
+            Object name = map.get(KeyWords.NAME);
+            task.put(KeyWords.ID, key);
+            task.put(KeyWords.NAME, name);
+            task.put(KeyWords.TARGET_MODEL_NAME, name);
+            tpTasks.add(task);
+        }
+        return tpTasks;
+    }
+
+    public Map<String, String> doParse(String sourceConnectionId, String targetConnectionId, UserDetail user) {
+        Map<String, String> sourceToJs = new HashMap<>();
+        Map<String, String> parsedTpTasks = new HashMap<>();
+        Map<String, Map<String, Map<String, Object>>> renameFields = new HashMap<>();
+        replaceRmProjectId();
+
+        List<Map<String, Object>> tpTasks = getTaskInfoList();
+        for (Map<String, Object> task : tpTasks) {
+            task.put("editVersion", System.currentTimeMillis());
+            task.put("syncType", KeyWords.SYNC);
+            task.put(KeyWords.TYPE, "initial_sync+cdc");
+            task.put("mappingTemplate", KeyWords.SYNC);
+            task.put("status", "edit");
+            task.put("user_id", user.getUserId());
+            task.put("customId", user.getCustomerId());
+            task.put("createUser", user.getUsername());
+
+            String targetModelName = (String) task.get("targetModelName");
+
+            Map<String, Object> dag = new HashMap<>();
+            List<Map<String, Object>> nodes = new ArrayList<>();
+            List<Map<String, Object>> edges = new ArrayList<>();
+
+            Map<String, Object> contentMapping = parseMap(getFromMap(content, KeyWords.MAPPINGS));
+
+            // 把源节点都加进去, 这里如果有一些 字段改名, 或者新字段生成的操作, 增加一个 JS 处理器
+            Map<String, List<Map<String, Object>>> contentDeleteOperations = new HashMap<>();
+            Map<String, List<Map<String, Object>>> contentRenameOperations = new HashMap<>();
+            List<String> sourceNodes = addJsNodeIfInNeed(AddJsNodeParam.of().withSourceConnectionId(sourceConnectionId)
+                    .withRenameFields(renameFields)
+                    .withSourceToJs(sourceToJs)
+                    .withContentMapping(contentMapping).withContentDeleteOperations(contentDeleteOperations)
+                    .withContentRenameOperations(contentRenameOperations)
+                    .withTask(task)
+                    .withEdges(edges)
+                    .withNodes(nodes));
+            String rootTableName = KeyWords.EMPTY;
+            Map<String, Object> rootContentMappingValue = new HashMap<>();
+            List<String> keyAfterFilterByNewDocument = contentMapping.keySet().stream()
+                    .filter(key -> keyAfterByNewDocumentFilter(key, contentMapping, task))
+                    .collect(Collectors.toList());
+            String rootNodeId = KeyWords.EMPTY;
+            if(!keyAfterFilterByNewDocument.isEmpty()) {
+                rootNodeId = keyAfterFilterByNewDocument.get(0);
+                rootContentMappingValue = parseMap(getFromMap(contentMapping, rootNodeId));
+                TablePathInfo table = getTablePathInfo(rootContentMappingValue);
+                rootTableName = table.getTable();
+            }
+            if (StringUtils.isBlank(rootNodeId)) {
+                continue;
+            }
+
+            // 增加主从合并节点
+            String mergeNodeId = UUID.randomUUID().toString().toLowerCase();
+            Map<String, Object> mergeNode = new HashMap<>();
+            boolean needMergeNode = addMergerNodes(AddMergerNodeParam.of()
+                    .withRootNodeId(rootNodeId)
+                    .withMergeNodeId(mergeNodeId)
+                    .withMergeNode(mergeNode)
+                    .withSourceToJs(sourceToJs)
+                    .withContentMapping(contentMapping)
+                    .withRenameFields(renameFields)
+                    .withContentDeleteOperations(contentDeleteOperations)
+                    .withContentRenameOperations(contentRenameOperations));
+            if (needMergeNode) {
+                doMerge(DoMergeParam.of().withMergeNodeId(mergeNodeId)
+                        .withNodes(nodes)
+                        .withContentMapping(contentMapping)
+                        .withMergeNode(mergeNode)
+                        .withSourceNodes(sourceNodes)
+                        .withEdges(edges)
+                        .withRenameFields(renameFields)
+                        .withContentRenameOperations(contentRenameOperations)
+                        .withContentDeleteOperations(contentDeleteOperations));
+            }
+
+            // 把目标节点加进去
+            Map<String, Object> targetNode = new HashMap<>();
+            targetNode.put(KeyWords.TYPE, KeyWords.TABLE);
+            targetNode.put("existDataProcessMode", "keepData");
+            targetNode.put(KeyWords.NAME, targetModelName);
+            targetNode.put(KeyWords.ID, task.get(KeyWords.ID));
+            targetNode.put(KeyWords.CONNECTION_ID, targetConnectionId);
+            targetNode.put(KeyWords.TABLE_NAME, targetModelName);
+            List<String> updateConditionFields = new ArrayList<>();
+            Map<String, Map<String, Object>> rootRenameFields = renameFields.get(rootTableName);
+            Map<String, Object> rootFields = parseMap(getFromMap(rootContentMappingValue, KeyWords.FIELDS));
+            for (String field : rootFields.keySet()) {
+                Map<String, Object> fieldMap = parseMap(getFromMap(rootFields, field));
+                Map<String, Object> source = parseMap(getFromMap(fieldMap, KeyWords.SOURCE));
+                if (Boolean.TRUE.equals(source.get(KeyWords.IS_PRIMARY_KEY))) {
+                    updateConditionFields.add(rootRenameFields.get(field).get(KeyWords.TARGET).toString());
+                }
+            }
+
+            targetNode.put("updateConditionFields", updateConditionFields);
+            nodes.add(targetNode);
+            Map<String, Object> edge = new HashMap<>();
+            edge.put(KeyWords.SOURCE, needMergeNode ? mergeNodeId : sourceNodes.get(0));
+            edge.put(KeyWords.TARGET, targetNode.get(KeyWords.ID));
+            edges.add(edge);
+
+            dag.put("nodes", nodes);
+            dag.put("edges", edges);
+            task.put("dag", dag);
+            parsedTpTasks.put((String) task.get(KeyWords.ID), JsonUtil.toJson(task));
+        }
+        return parsedTpTasks;
+    }
+    
+    protected boolean keyAfterByNewDocumentFilter(String key, Map<String, Object> contentMapping, Map<String, Object> task) {
+        Map<String, Object> contentMappingValue = parseMap(getFromMap(contentMapping, key));
+        if (!contentMappingValue.get(KeyWords.COLLECTION_ID).equals(task.get(KeyWords.ID))) {
+            return false;
+        }
+        Map<String, Object> setting = parseMap(getFromMap(contentMappingValue, KeyWords.SETTINGS));
+        if (Objects.isNull(setting)) {
+            return false;
+        }
+        Object type = setting.get(KeyWords.TYPE);
+        if (Objects.isNull(type)) {
+            return false;
+        }
+        return KeyWords.NEW_DOCUMENT.equals(type);
+    }
+
+    public void resetMap(Map<String, Object> map, Map<String, String> globalIdMap) {
+        ArrayList<String> keys = new ArrayList<>(map.keySet());
+        for (String key : keys) {
+            Map<String, Object> collection = parseMap(getFromMap(map, key));
+            String replaceKey = replaceId(key, globalIdMap);
+            map.remove(key);
+            map.put(replaceKey, collection);
+        }
+    }
+
+    public void replaceRmProjectId() {
+        Map<String, String> globalIdMap = new HashMap<>();
+        Map<String, Object> contentCollections = parseMap(getFromMap(content, KeyWords.COLLECTIONS));
+        resetMap(contentCollections, globalIdMap);
+
+        Map<String, Object> contentMapping = parseMap(getFromMap(content, KeyWords.MAPPINGS));
+        resetMap(contentMapping, globalIdMap);
+
+        Set<String> contentMappingKeys = new HashSet<>(contentMapping.keySet());
+        for (String key : contentMappingKeys) {
+            Map<String, Object> mapping = parseMap(getFromMap(contentMapping, key));
+            mapping.put(KeyWords.COLLECTION_ID, replaceId((String) mapping.get(KeyWords.COLLECTION_ID), globalIdMap));
+        }
+        replaceRelationShipsKey(globalIdMap, content);
+    }
+
+    public String addJSNode(String tpTable, String script, String declareScript, List<Map<String, Object>> nodes, String sourceId, List<Map<String, Object>> edges) {
+        String jsId = UUID.randomUUID().toString().toLowerCase();
+        Map<String, Object> jsNode = new HashMap<>();
+        jsNode.put(KeyWords.TYPE, "js_processor");
+        jsNode.put(KeyWords.NAME, tpTable);
+        jsNode.put(KeyWords.ID, jsId);
+        jsNode.put("jsType", 1);
+        jsNode.put(PROCESSOR_THREAD_NUM, 1);
+        jsNode.put(CATALOG, PROCESSOR);
+        jsNode.put(ELEMENT_TYPE, KeyWords.NODE);
+        jsNode.put("script", script);
+        jsNode.put("declareScript", declareScript);
+        nodes.add(jsNode);
+        Map<String, Object> edge = new HashMap<>();
+        edge.put(KeyWords.SOURCE, sourceId);
+        edge.put(KeyWords.TARGET, jsId);
+        edges.add(edge);
+        return jsId;
+    }
+
+    public String addRenameNode(String tpTable, List<Map<String, Object>> renameOperations, String sourceId, List<Map<String, Object>> nodes, List<Map<String, Object>> edges) {
+        Map<String, Object> renameNode = new HashMap<>();
+        String renameId = UUID.randomUUID().toString().toLowerCase();
+        renameNode.put(KeyWords.ID, renameId);
+        renameNode.put(CATALOG, PROCESSOR);
+        renameNode.put(ELEMENT_TYPE, KeyWords.NODE);
+        renameNode.put("fieldsNameTransform", KeyWords.EMPTY);
+        renameNode.put(KeyWords.IS_TRANSFORMED, false);
+        renameNode.put(KeyWords.NAME, "Rename " + tpTable);
+        renameNode.put(PROCESSOR_THREAD_NUM, 1);
+        renameNode.put(KeyWords.TYPE, "field_rename_processor");
+        nodes.add(renameNode);
+        renameNode.put(KeyWords.OPERATIONS, renameOperations);
+        Map<String, Object> edge = new HashMap<>();
+        edge.put(KeyWords.SOURCE, sourceId);
+        edge.put(KeyWords.TARGET, renameId);
+        edges.add(edge);
+        sourceId = renameId;
+        return sourceId;
+    }
+
+    public String addDeleteNode(String tpTable, List<Map<String, Object>> deleteOperations, String sourceId, List<Map<String, Object>> nodes, List<Map<String, Object>> edges) {
+        Map<String, Object> deleteNode = new HashMap<>();
+        String deleteId = UUID.randomUUID().toString().toLowerCase();
+        deleteNode.put(KeyWords.ID, deleteId);
+        deleteNode.put(CATALOG, PROCESSOR);
+        deleteNode.put("deleteAllFields", false);
+        deleteNode.put(ELEMENT_TYPE, KeyWords.NODE);
+        deleteNode.put(KeyWords.NAME, "Delete " + tpTable);
+        deleteNode.put(KeyWords.TYPE, "field_add_del_processor");
+        deleteNode.put(PROCESSOR_THREAD_NUM, 1);
+        deleteNode.put(KeyWords.OPERATIONS, deleteOperations);
+        nodes.add(deleteNode);
+        Map<String, Object> edge = new HashMap<>();
+        edge.put(KeyWords.SOURCE, sourceId);
+        edge.put(KeyWords.TARGET, deleteId);
+        edges.add(edge);
+        sourceId = deleteId;
+        return sourceId;
+    }
+
+    public Map<String, Object> getDeleteOperation(Object deleteFieldName, Object isPrimaryKey) {
+        Map<String, Object> deleteOperation = new HashMap<>();
+        deleteOperation.put(KeyWords.ID, UUID.randomUUID().toString().toLowerCase());
+        deleteOperation.put(KeyWords.FIELD, deleteFieldName);
+        deleteOperation.put(KeyWords.OP, "REMOVE");
+        deleteOperation.put(KeyWords.OPERAND, "true");
+        deleteOperation.put("label", deleteFieldName);
+        deleteOperation.put(KeyWords.IS_PK, isPrimaryKey);
+        return deleteOperation;
+    }
+
+    public Map<String, Object> getRenameOperation(Object source, Object target) {
+        Map<String, Object> fieldRenameOperation = new HashMap<>();
+        fieldRenameOperation.put(KeyWords.ID, UUID.randomUUID().toString().toLowerCase());
+        fieldRenameOperation.put(KeyWords.FIELD, source);
+        fieldRenameOperation.put(KeyWords.OP, "RENAME");
+        fieldRenameOperation.put(KeyWords.OPERAND, target);
+        return fieldRenameOperation;
+    }
+
+    public Map<String, Object> getNewNameMap(Map<String, Object> target, Map<String, Object> source) {
+        Map<String, Object> newName = new HashMap<>();
+        newName.put(KeyWords.TARGET, target.get(KeyWords.NAME).toString());
+        newName.put(KeyWords.IS_PRIMARY_KEY, source.get(KeyWords.IS_PRIMARY_KEY));
+        return newName;
+    }
+
+    public TablePathInfo getTablePathInfo(Map<String, Object> contentMapping) {
+        // spilt by dot, and get last one
+        String table = String.valueOf(contentMapping.get(KeyWords.TABLE));
+        String[] tablePathAfterSplit = table.split("\\.");
+        TablePathInfo tablePathInfo = new TablePathInfo();
+        switch (tablePathAfterSplit.length) {
+            case 1:
+                tablePathInfo.setTable(tablePathAfterSplit[0]);
+                break;
+            case 2:
+                tablePathInfo.setSchema(tablePathAfterSplit[0]);
+                tablePathInfo.setTable(tablePathAfterSplit[1]);
+                break;
+            case 3:
+                tablePathInfo.setDatabase(tablePathAfterSplit[0]);
+                tablePathInfo.setSchema(tablePathAfterSplit[1]);
+                tablePathInfo.setTable(tablePathAfterSplit[2]);
+                break;
+            default:
+        }
+        return tablePathInfo;
+    }
+
+    public TablePathInfo getTablePathInfo(Map<String, Object> contentMapping, String childId) {
+        Map<String, Object> childMap = parseMap(getFromMap(contentMapping, childId));
+        return getTablePathInfo(childMap);
+    }
+
+    public void scanAllFieldKeys(Map<String, Map<String, Object>> currentRenameFields,
+                                    Map<String, Object> childNode,
+                                    Map<String, Object> fields) {
+        List<String> arrayKeys = new ArrayList<>();
+        for (String field : fields.keySet()) {
+            Map<String, Object> fieldMap = parseMap(getFromMap(fields, field));
+            Map<String, Object> source = parseMap(getFromMap(fieldMap, KeyWords.SOURCE));
+            if (Boolean.TRUE.equals(source.get(KeyWords.IS_PRIMARY_KEY))) {
+                Object target = currentRenameFields.get(field).get(KeyWords.TARGET);
+                if (Objects.isNull(target)) {
+                    continue;
+                }
+                arrayKeys.add(String.valueOf(target));
+            }
+        }
+        childNode.put(KeyWords.ARRAY_KEYS, arrayKeys);
+    }
+
+    public void genericProperties(GenericPropertiesParam param) {
+        Map<String, Object> parent = param.getParent();
+        Map<String, Object> relationshipsMapping = param.getRelationshipsMapping();
+        String parentId = (String) parent.get(RM_ID);
+        List<String> children = (List<String>) (parseMap(getFromMap(relationshipsMapping, parentId))).get(KeyWords.CHILDREN);
+        if (!CollUtil.isEmpty(children)) {
+            List<Map<String, Object>> childrenNode = new ArrayList<>();
+            children.stream().forEach(child -> childrenNode.add(scanOneChildren(param, child)));
+            parent.put(KeyWords.CHILDREN, childrenNode);
+        }
+    }
+    
+    public Map<String, Object> scanOneChildren(GenericPropertiesParam param, String child) {
+        Map<String, Object> parent = param.getParent();
+        String parentId = (String) parent.get(RM_ID);
+        Map<String, Object> contentMapping = param.getContentMapping();
+        Map<String, Object> full = param.getFull();
+        Map<String, String> sourceToJS = param.getSourceToJS();
+        Map<String, Object> relationshipsMapping = param.getRelationshipsMapping();
+        Map<String, Map<String, Map<String, Object>>> renameFields = param.getRenameFields();
+        Map<String, List<Map<String, Object>>> contentDeleteOperations = param.getContentDeleteOperations();
+        Map<String, List<Map<String, Object>>> contentRenameOperations = param.getContentRenameOperations();
+        Map<String, Object> childNode = new HashMap<>();
+        Map<String, Object> map = parseMap(getFromMap(contentMapping, child));
+        Map<String, Object> setting = parseMap(getFromMap(map, KeyWords.SETTINGS));
+
+        TablePathInfo tableInfo = getTablePathInfo(contentMapping, child);
+        Map<String, Object> tables = getTableSchema(full, tableInfo);
+
+        String tpTable = tableInfo.getTable();
+        List<Map<String, String>> joinKeys = new ArrayList<>();
+        Map<String, Object> currentTable = parseMap(getFromMap(tables, tpTable));
+        Map<String, Object> currentColumns = parseMap(getFromMap(currentTable, KeyWords.COLUMNS));
+        Map<String, Map<String, Object>> currentRenameFields = renameFields.get(tpTable);
+        Map<String, Object> parentTable = parseMap(getFromMap(tables, String.valueOf(parent.get(KeyWords.TABLE_NAME))));
+        Map<String, Object> parentColumns = parseMap(getFromMap(parentTable, KeyWords.COLUMNS));
+        Map<String, Map<String, Object>> parentRenameFields = renameFields.get(String.valueOf(parent.get(KeyWords.TABLE_NAME)));
+
+        String parentTargetPath = (String) Optional.ofNullable(parent.get(KeyWords.TARGET_PATH)).orElse(KeyWords.EMPTY);
+        String mergeType = KeyWords.UPDATE_WRITE;
+        String targetPath = KeyWords.EMPTY;
+        String type = String.valueOf(setting.get(KeyWords.TYPE)).toUpperCase();
+        switch (type) {
+            case KeyWords.NEW_DOCUMENT:
+                targetPath = parentTargetPath;
+                break;
+            case KeyWords.EMBEDDED_DOCUMENT:
+                targetPath = getEmbeddedDocumentPath(parentTargetPath, setting);
+                break;
+            case KeyWords.EMBEDDED_DOCUMENT_ARRAY:
+                final String embeddedPath = String.valueOf(setting.get(KeyWords.EMBEDDED_PATH));
+                targetPath = KeyWords.EMPTY.equals(parentTargetPath) ?
+                        String.format(KeyWords.FROMAT, parentTargetPath, embeddedPath)
+                        : embeddedPath;
+                mergeType = KeyWords.UPDATE_INTO_ARRAY;
+                Map<String, Object> fields = parseMap(getFromMap(map, KeyWords.FIELDS));
+                scanAllFieldKeys(currentRenameFields, childNode, fields);
+                break;
+            default:
+                // doNothing
+        }
+
+
+        childNode.put(KeyWords.TARGET_PATH, targetPath);
+        childNode.put(KeyWords.MERGE_TYPE, mergeType);
+
+        childNode.put(KeyWords.TABLE_NAME, tpTable);
+        childNode.put(KeyWords.CHILDREN, new ArrayList<>());
+        childNode.put(KeyWords.ID, sourceToJS.get(child));
+        childNode.put(RM_ID, child);
+
+        // 由于使用外键做关联, 所以似乎 RM 只能合并来自一个源的数据, 所以 tables 表结构使用其中一个就可以
+
+        Map<String, Map<String, String>> sourceJoinKeyMapping = new HashMap<>();
+        Map<String, Map<String, String>> targetJoinKeyMapping = new HashMap<>();
+        for (String columnKey : currentColumns.keySet()) {
+            Map<String, Object> column = parseMap(getFromMap(currentColumns, columnKey));
+            Optional.ofNullable(getFromMap(column, KeyWords.FOREIGN_KEY)).ifPresent(fk -> {
+                Map<String, Object> foreignKey = parseMap(fk);
+                if (String.valueOf(foreignKey.get(KeyWords.TABLE)).equals(parent.get(KeyWords.TABLE_NAME))) {
+                    Map<String, String> joinKey = new HashMap<>();
+                    String sourceJoinKey = currentRenameFields.get(columnKey).get(KeyWords.TARGET).toString();
+                    Map<String, String> newFieldMap = new HashMap<>();
+                    newFieldMap.put(KeyWords.SOURCE, columnKey);
+                    newFieldMap.put(KeyWords.TARGET, sourceJoinKey);
+                    sourceJoinKeyMapping.put(sourceJoinKey, newFieldMap);
+                    joinKey.put(KeyWords.SOURCE, sourceJoinKey);
+                    Object columnObject = getFromMap(foreignKey, KeyWords.COLUMN);
+                    Map<String, Object> parentRenameField = parseMap(parentRenameFields.get(String.valueOf(columnObject)));
+                    String targetJoinKey = (String)parentRenameField.get(KeyWords.TARGET);
+                    HashMap<String, String> targetNewFieldMap = new HashMap<>();
+                    targetNewFieldMap.put(KeyWords.SOURCE, (String) foreignKey.get(KeyWords.COLUMN));
+                    targetNewFieldMap.put(KeyWords.TARGET, targetJoinKey);
+                    if (KeyWords.EMPTY.equals(parent.get(KeyWords.TARGET_PATH))) {
+                        joinKey.put(KeyWords.TARGET, targetJoinKey);
+                    } else {
+                        joinKey.put(KeyWords.TARGET, String.format(KeyWords.FROMAT, parent.get(KeyWords.TARGET_PATH), targetJoinKey));
+                    }
+                    targetJoinKeyMapping.put(targetJoinKey, targetNewFieldMap);
+                    joinKeys.add(joinKey);
+                }
+            });
+        }
+
+        parentColumnsFindJoinKeys(parent, renameFields, parentColumns, tpTable, joinKeys, sourceJoinKeyMapping, targetJoinKeyMapping);
+        childNode.put(KeyWords.JOIN_KEYS, joinKeys);
+        joinKeys.forEach(joinKeyMap -> {
+            String sourceJoinKey = joinKeyMap.get(KeyWords.SOURCE);
+            addRenameOpIfDeleteOpHasJoinKey(contentDeleteOperations, contentRenameOperations, child, sourceJoinKeyMapping, sourceJoinKey);
+            String targetJoinKey = joinKeyMap.get(KeyWords.TARGET);
+            addRenameOpIfDeleteOpHasJoinKey(contentDeleteOperations, contentRenameOperations, parentId, targetJoinKeyMapping, targetJoinKey);
+        });
+        GenericPropertiesParam propertiesParam = GenericPropertiesParam.of()
+                .withParent(childNode)
+                .withContentMapping(contentMapping)
+                .withRelationshipsMapping(relationshipsMapping)
+                .withFull(full)
+                .withSourceToJS(sourceToJS)
+                .withRenameFields(renameFields)
+                .withContentDeleteOperations(contentDeleteOperations).withContentRenameOperations(contentRenameOperations);
+        genericProperties(propertiesParam);
+        return childNode;
+    }
+
+    public String replaceId(String id, Map<String, String> globalIdMap) {
+        if (globalIdMap.containsKey(id)) {
+            return globalIdMap.get(id);
+        }
+        String newId = UUID.randomUUID().toString();
+        globalIdMap.put(id, newId);
+        return newId;
+    }
+
+    public void replaceRelationShipsKey(Map<String, String> globalIdMap, Map<String, Object> content) {
+        Map<String, Object> relationships = content.get(KeyWords.RELATIONSHIPS) == null ? new HashMap<>() : parseMap(getFromMap(content, KeyWords.RELATIONSHIPS));
+        Map<String, Object> relationshipsCollection = relationships.get(KeyWords.COLLECTIONS) == null ? new HashMap<>() : parseMap(getFromMap(relationships, KeyWords.COLLECTIONS));
+        Set<String> relationshipsCollectionKeys = new HashSet<>(relationshipsCollection.keySet());
+        Map<String, Object> relationshipsMapping = relationships.get(KeyWords.MAPPINGS) == null ? new HashMap<>() : parseMap(getFromMap(relationships, KeyWords.MAPPINGS));
+        for (String key : relationshipsCollectionKeys) {
+            Map<String, Object> collection = parseMap(getFromMap(relationshipsCollection, key));
+            String replaceKey = replaceId(key, globalIdMap);
+            relationshipsCollection.remove(key);
+            relationshipsCollection.put(replaceKey, collection);
+        }
+        for (String key : relationshipsCollection.keySet()) {
+            Map<String, Object> collection = parseMap(getFromMap(relationshipsCollection, key));
+            List<String> oldMappingIds = (List<String>) collection.get(KeyWords.MAPPINGS);
+            List<String> newMappingIds = new ArrayList<>();
+            for (String oldMappingId : oldMappingIds) {
+                newMappingIds.add(replaceId(oldMappingId, globalIdMap));
+            }
+            collection.put(KeyWords.MAPPINGS, newMappingIds);
+        }
+
+        Set<String> relationshipsMappingKeys = new HashSet<>(relationshipsMapping.keySet());
+        for (String key : relationshipsMappingKeys) {
+            Map<String, Object> mapping = parseMap(getFromMap(relationshipsMapping, key));
+            String replaceKey = replaceId(key, globalIdMap);
+            relationshipsMapping.remove(key);
+            relationshipsMapping.put(replaceKey, mapping);
+        }
+
+        for (String key : relationshipsMapping.keySet()) {
+            Map<String, Object> mapping = parseMap(getFromMap(relationshipsMapping, key));
+            List<String> oldChildren = (List<String>) mapping.get(KeyWords.CHILDREN);
+            List<String> newChildren = new ArrayList<>();
+            for (String oldChild : oldChildren) {
+                newChildren.add(replaceId(oldChild, globalIdMap));
+            }
+            mapping.put(KeyWords.CHILDREN, newChildren);
+        }
+    }
+
+    public Map<String, Object> getTableSchema(Map<String, Object> full, TablePathInfo tableInfo) {
+        Map<String, Object> databasesLayer = parseMap(getFromMap(full, KeyWords.DATABASE));
+        Map<String, Object> currentDatabaseSchema = parseMap(getFromMap(databasesLayer, tableInfo.getDatabase()));
+        Map<String, Object> schemasLayer = parseMap(getFromMap(currentDatabaseSchema, KeyWords.SCHEMA));
+        Map<String, Object> currentSchema = parseMap(getFromMap(schemasLayer, tableInfo.getSchema()));
+        return parseMap(getFromMap(currentSchema, KeyWords.TABLES));
+    }
+
+    public String getEmbeddedDocumentPath(String parentTargetPath, Map<String, Object> setting) {
+        String targetPath;
+        if (KeyWords.EMPTY.equals(parentTargetPath)) {
+            targetPath = String.valueOf(setting.get(KeyWords.EMBEDDED_PATH));
+        } else {
+            Object embeddedPath = setting.get(KeyWords.EMBEDDED_PATH);
+            if (null == embeddedPath || StringUtils.isBlank(String.valueOf(embeddedPath))) {
+                targetPath = parentTargetPath;
+            } else {
+                targetPath = String.format(KeyWords.FROMAT, parentTargetPath, embeddedPath);
+            }
+        }
+        return targetPath;
+    }
+
+    public void parentColumnsFindJoinKeys(Map<String, Object> parent, Map<String, Map<String, Map<String, Object>>> renameFields, Map<String, Object> parentColumns, String tpTable, List<Map<String, String>> joinKeys, Map<String, Map<String, String>> souceJoinKeyMapping, Map<String, Map<String, String>> targetJoinKeyMapping) {
+        Object tableName = parent.get(KeyWords.TABLE_NAME);
+        Map<String, Map<String, Object>> parentRenameFields = renameFields.get(String.valueOf(tableName));
+        for (String columnKey : parentColumns.keySet()) {
+            Map<String, Object> column = parseMap(getFromMap(parentColumns, columnKey));
+            Map<String, Object> foreignKey = parseMap(getFromMap(column, KeyWords.FOREIGN_KEY));
+            if (foreignKey == null) {
+                continue;
+            }
+            if (null != tpTable && tpTable.equals(foreignKey.get(KeyWords.TABLE))) {
+                Map<String, String> joinKey = new HashMap<>();
+                Object columnObj = foreignKey.get(KeyWords.COLUMN);
+                String sourceJoinKey = renameFields.get(tpTable).get(String.valueOf(columnObj)).get(KeyWords.TARGET).toString();
+                Map<String, String> sourceNewFieldMap = new HashMap<>();
+                sourceNewFieldMap.put(KeyWords.SOURCE, (String) foreignKey.get(KeyWords.COLUMN));
+                sourceNewFieldMap.put(KeyWords.TARGET, sourceJoinKey);
+                souceJoinKeyMapping.put(sourceJoinKey, sourceNewFieldMap);
+                joinKey.put(KeyWords.SOURCE, sourceJoinKey);
+                Map<String, String> targetNewFieldMap = new HashMap<>();
+                String targetJoinKey = parentRenameFields.get(columnKey).get(KeyWords.TARGET).toString();
+                targetNewFieldMap.put(KeyWords.SOURCE, columnKey);
+                targetNewFieldMap.put(KeyWords.TARGET, targetJoinKey);
+                String targetPath = parent.get(KeyWords.TARGET_PATH).toString();
+                if (!StringUtils.isBlank(targetPath)) {
+                    targetJoinKey = String.format(KeyWords.FROMAT, targetPath, targetJoinKey);
+                }
+                joinKey.put(KeyWords.TARGET, targetJoinKey);
+                targetJoinKeyMapping.put(targetJoinKey, targetNewFieldMap);
+                joinKeys.add(joinKey);
+            }
+        }
+    }
+
+    public void addRenameOpIfDeleteOpHasJoinKey(Map<String, List<Map<String, Object>>> contentDeleteOperations, Map<String, List<Map<String, Object>>> contentRenameOperations, String tableId, Map<String, Map<String, String>> joinKeyMapping, String joinKey) {
+        List<Map<String, Object>> childDeleteOperations = contentDeleteOperations.get(tableId);
+        boolean removeJoinKeyFlag = removeDeleteOperation(childDeleteOperations, joinKeyMapping, joinKey);
+        if (removeJoinKeyFlag) {
+            List<Map<String, Object>> childRenameOperations = contentRenameOperations.get(tableId);
+            Map<String, Object> renameOperation = getRenameOperation(joinKeyMapping.get(joinKey).get(KeyWords.SOURCE), joinKeyMapping.get(joinKey).get(KeyWords.TARGET));
+            childRenameOperations.add(renameOperation);
+        }
+    }
+
+    public boolean removeDeleteOperation(List<Map<String, Object>> deleteOperations, Map<String, Map<String, String>> joinKeyMapping, String joinKey) {
+        return deleteOperations.removeIf(delOperations -> {
+            String deleteField = (String) delOperations.get(KeyWords.FIELD);
+            Map<String, String> joinKeyInfo = joinKeyMapping.get(joinKey);
+            if (null != joinKeyInfo) {
+                String originalField = joinKeyInfo.get(KeyWords.SOURCE);
+                return null != deleteField && deleteField.equals(originalField);
+            }
+            return false;
+        });
+    }
+
+    public Object getFromMap(Map<String, Object> map, String key) {
+        if (null == map || !map.containsKey(key)) {
+            return null;
+        }
+        return map.get(key);
+    }
+
+    public Map<String, Object> parseMap(Object object) {
+        if (object instanceof Map) {
+            return (Map<String, Object>) object;
+        }
+        return new HashMap<>();
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseUnKnowVersionRelMigFile.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseUnKnowVersionRelMigFile.java
new file mode 100644
index 0000000000..8b3d75ebe9
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/ParseUnKnowVersionRelMigFile.java
@@ -0,0 +1,37 @@
+package com.tapdata.tm.task.service.batchin;
+
+import com.tapdata.tm.base.exception.BizException;
+import com.tapdata.tm.commons.task.dto.TaskDto;
+import com.tapdata.tm.task.service.batchin.constant.KeyWords;
+import com.tapdata.tm.task.service.batchin.constant.ParseRelMigFileVersionMapping;
+import com.tapdata.tm.task.service.batchin.entity.ParseParam;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.StringJoiner;
+
+public class ParseUnKnowVersionRelMigFile implements ParseRelMig<TaskDto> {
+    ParseParam param;
+    Map<String, Object> relMigInfo;
+    public ParseUnKnowVersionRelMigFile(ParseParam param) {
+        this.param = param;
+        this.relMigInfo = Optional.ofNullable(param.getRelMigInfo()).orElse(new HashMap<>());
+    }
+    @Override
+    public List<TaskDto> parse() {
+        String version = String.valueOf(this.relMigInfo.get(KeyWords.VERSION));
+        throw new BizException("relMig.parse.unSupport", version, getSupportedVersion());
+    }
+    private String getSupportedVersion() {
+        ParseRelMigFileVersionMapping[] values = ParseRelMigFileVersionMapping.values();
+        StringJoiner builder = new StringJoiner(", ");
+        for (ParseRelMigFileVersionMapping value : values) {
+            if (!ParseRelMigFileVersionMapping.UN_KNOW.equals(value)) {
+                builder.add(value.getVersion());
+            }
+        }
+        return builder.toString();
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/constant/KeyWords.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/constant/KeyWords.java
new file mode 100644
index 0000000000..17b82948f7
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/constant/KeyWords.java
@@ -0,0 +1,61 @@
+package com.tapdata.tm.task.service.batchin.constant;
+
+public class KeyWords {
+    public static final String EMPTY = "";
+    public static final String FROMAT = "%s.%s";
+    public static final String VERSION = "version";
+    public static final String DATABASE = "database";
+    public static final String SCHEMA = "schema";
+    public static final String TABLE = "table";
+    public static final String TABLES = "tables";
+    public static final String PROJECT = "project";
+    public static final String QUERIES = "queries";
+    public static final String CONTENT = "content";
+    public static final String PATH = "path";
+    public static final String COLLECTIONS = "collections";
+    public static final String CONNECTION_ID = "connectionId";
+    public static final String MAPPINGS = "mappings";
+
+
+    public static final String NAME = "name";
+    public static final String ID = "id";
+    public static final String TARGET_MODEL_NAME = "targetModelName";
+
+    public static final String CHILDREN = "children";
+    public static final String SETTINGS = "settings";
+    public static final String COLUMNS = "columns";
+    public static final String COLUMN = "column";
+    public static final String TABLE_NAME = "tableName";
+    public static final String UPDATE_WRITE = "updateWrite";
+    public static final String TYPE = "type";
+    public static final String NEW_DOCUMENT = "NEW_DOCUMENT";
+    public static final String EMBEDDED_DOCUMENT = "EMBEDDED_DOCUMENT";
+    public static final String EMBEDDED_DOCUMENT_ARRAY = "EMBEDDED_DOCUMENT_ARRAY";
+    public static final String EMBEDDED_PATH = "embeddedPath";
+    public static final String UPDATE_INTO_ARRAY = "updateIntoArray";
+    public static final String FIELDS = "fields";
+    public static final String FIELD = "field";
+    public static final String SOURCE = "source";
+    public static final String TARGET = "target";
+    public static final String TARGET_PATH = "targetPath";
+    public static final String IS_PRIMARY_KEY = "isPrimaryKey";
+    public static final String ARRAY_KEYS = "arrayKeys";
+    public static final String MERGE_TYPE = "mergeType";
+    public static final String JOIN_KEYS = "joinKeys";
+    public static final String FOREIGN_KEY = "foreignKey";
+
+    public static final String INCLUDED = "included";
+    public static final String PRIMITIVE = "primitive";
+    public static final String COLLECTION_ID = "collectionId";
+    public static final String RELATIONSHIPS = "relationships";
+    public static final String CALCULATED_FIELDS = "calculatedFields";
+    public static final String EXPRESSION = "expression";
+    public static final String FULL = "full";
+    public static final String IS_PK = "isPk";
+    public static final String SYNC = "sync";
+    public static final String NODE = "Node";
+    public static final String IS_TRANSFORMED = "isTransformed";
+    public static final String OPERATIONS = "operations";
+    public static final String OP = "op";
+    public static final String OPERAND = "operand";
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/constant/ParseRelMigFileVersionMapping.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/constant/ParseRelMigFileVersionMapping.java
new file mode 100644
index 0000000000..483cf82098
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/constant/ParseRelMigFileVersionMapping.java
@@ -0,0 +1,32 @@
+package com.tapdata.tm.task.service.batchin.constant;
+
+import com.tapdata.tm.task.service.batchin.ParseBaseVersionRelMigImpl;
+import com.tapdata.tm.task.service.batchin.ParseRelMig;
+import com.tapdata.tm.task.service.batchin.ParseRelMig13OrMoreImpl;
+import com.tapdata.tm.task.service.batchin.ParseUnKnowVersionRelMigFile;
+
+public enum ParseRelMigFileVersionMapping {
+    V1_2_0("1.2.0", ParseBaseVersionRelMigImpl.class),
+    V1_3_0("1.3.0", ParseRelMig13OrMoreImpl.class),
+    UN_KNOW("*", ParseUnKnowVersionRelMigFile.class)
+    ;
+    String version;
+    Class<? extends ParseRelMig> value;
+    ParseRelMigFileVersionMapping(String ver, Class<? extends ParseRelMig> value) {
+        this.version = ver;
+        this.value = value;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public static Class<? extends ParseRelMig> getInstance(String versionStr) {
+        if (null == versionStr) return UN_KNOW.value;
+        ParseRelMigFileVersionMapping[] values = values();
+        for (ParseRelMigFileVersionMapping value : values) {
+            if (value.getVersion().equalsIgnoreCase(versionStr)) return value.value;
+        }
+        return UN_KNOW.value;
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/dto/TablePathInfo.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/dto/TablePathInfo.java
new file mode 100644
index 0000000000..a5e1666147
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/dto/TablePathInfo.java
@@ -0,0 +1,14 @@
+package com.tapdata.tm.task.service.batchin.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class TablePathInfo {
+    String database;
+    String schema;
+    String table;
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/AddJsNodeParam.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/AddJsNodeParam.java
new file mode 100644
index 0000000000..e58b83839f
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/AddJsNodeParam.java
@@ -0,0 +1,58 @@
+package com.tapdata.tm.task.service.batchin.entity;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class AddJsNodeParam {
+    String sourceConnectionId;
+    Map<String, Map<String, Map<String, Object>>> renameFields;
+    Map<String, String> sourceToJs;
+    Map<String, Object> contentMapping;
+    Map<String, List<Map<String, Object>>> contentDeleteOperations;
+    Map<String, List<Map<String, Object>>> contentRenameOperations;
+    Map<String, Object> task;
+    List<Map<String, Object>> nodes;
+    List<Map<String, Object>> edges;
+    public AddJsNodeParam withSourceConnectionId(String sourceConnectionId) {
+        this.sourceConnectionId = sourceConnectionId;
+        return this;
+    }
+    public AddJsNodeParam withRenameFields(Map<String, Map<String, Map<String, Object>>> renameFields) {
+        this.renameFields = renameFields;
+        return this;
+    }
+    public AddJsNodeParam withSourceToJs(Map<String, String> sourceToJs) {
+        this.sourceToJs = sourceToJs;
+        return this;
+    }
+    public AddJsNodeParam withContentMapping(Map<String, Object> contentMapping) {
+        this.contentMapping = contentMapping;
+        return this;
+    }
+    public AddJsNodeParam withContentDeleteOperations(Map<String, List<Map<String, Object>>> contentDeleteOperations) {
+        this.contentDeleteOperations = contentDeleteOperations;
+        return this;
+    }
+    public AddJsNodeParam withContentRenameOperations(Map<String, List<Map<String, Object>>> contentRenameOperations) {
+        this.contentRenameOperations = contentRenameOperations;
+        return this;
+    }
+    public AddJsNodeParam withTask(Map<String, Object> task) {
+        this.task = task;
+        return this;
+    }
+    public AddJsNodeParam withNodes(List<Map<String, Object>> nodes) {
+        this.nodes = nodes;
+        return this;
+    }
+    public AddJsNodeParam withEdges(List<Map<String, Object>> edges) {
+        this.edges = edges;
+        return this;
+    }
+    public static AddJsNodeParam of() {
+        return new AddJsNodeParam();
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/AddMergerNodeParam.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/AddMergerNodeParam.java
new file mode 100644
index 0000000000..cf8e3b90da
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/AddMergerNodeParam.java
@@ -0,0 +1,53 @@
+package com.tapdata.tm.task.service.batchin.entity;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class AddMergerNodeParam {
+    String rootNodeId;
+    String mergeNodeId;
+    Map<String, Object> mergeNode;
+    Map<String, String> sourceToJs;
+    Map<String, Object> contentMapping;
+    Map<String, Map<String, Map<String, Object>>> renameFields;
+    Map<String, List<Map<String, Object>>> contentDeleteOperations;
+    Map<String, List<Map<String, Object>>> contentRenameOperations;
+    public AddMergerNodeParam withRootNodeId(String rootNodeId) {
+        this.rootNodeId = rootNodeId;
+        return this;
+    }
+    public AddMergerNodeParam withMergeNodeId(String mergeNodeId) {
+        this.mergeNodeId = mergeNodeId;
+        return this;
+    }
+    public AddMergerNodeParam withRenameFields(Map<String, Map<String, Map<String, Object>>> renameFields) {
+        this.renameFields = renameFields;
+        return this;
+    }
+    public AddMergerNodeParam withSourceToJs(Map<String, String> sourceToJs) {
+        this.sourceToJs = sourceToJs;
+        return this;
+    }
+    public AddMergerNodeParam withContentMapping(Map<String, Object> contentMapping) {
+        this.contentMapping = contentMapping;
+        return this;
+    }
+    public AddMergerNodeParam withContentDeleteOperations(Map<String, List<Map<String, Object>>> contentDeleteOperations) {
+        this.contentDeleteOperations = contentDeleteOperations;
+        return this;
+    }
+    public AddMergerNodeParam withContentRenameOperations(Map<String, List<Map<String, Object>>> contentRenameOperations) {
+        this.contentRenameOperations = contentRenameOperations;
+        return this;
+    }
+    public AddMergerNodeParam withMergeNode(Map<String, Object> mergeNode) {
+        this.mergeNode = mergeNode;
+        return this;
+    }
+    public static AddMergerNodeParam of() {
+        return new AddMergerNodeParam();
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/DoMergeParam.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/DoMergeParam.java
new file mode 100644
index 0000000000..eb627d1c62
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/DoMergeParam.java
@@ -0,0 +1,58 @@
+package com.tapdata.tm.task.service.batchin.entity;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class DoMergeParam {
+    String mergeNodeId;
+    List<Map<String, Object>> nodes;
+    List<Map<String, Object>> edges;
+    Map<String, Object> contentMapping;
+    Map<String, Object> mergeNode;
+    List<String> sourceNodes;
+    Map<String, Map<String, Map<String, Object>>> renameFields;
+    Map<String, List<Map<String, Object>>> contentRenameOperations;
+    Map<String, List<Map<String, Object>>> contentDeleteOperations;
+    public DoMergeParam withMergeNodeId(String mergeNodeId) {
+        this.mergeNodeId = mergeNodeId;
+        return this;
+    }
+    public DoMergeParam withNodes(List<Map<String, Object>> nodes) {
+        this.nodes = nodes;
+        return this;
+    }
+    public DoMergeParam withEdges(List<Map<String, Object>> edges) {
+        this.edges = edges;
+        return this;
+    }
+    public DoMergeParam withContentMapping(Map<String, Object> contentMapping) {
+        this.contentMapping = contentMapping;
+        return this;
+    }
+    public DoMergeParam withMergeNode(Map<String, Object> mergeNode) {
+        this.mergeNode = mergeNode;
+        return this;
+    }
+    public DoMergeParam withSourceNodes(List<String> sourceNodes) {
+        this.sourceNodes = sourceNodes;
+        return this;
+    }
+    public DoMergeParam withRenameFields(Map<String, Map<String, Map<String, Object>>> renameFields) {
+        this.renameFields = renameFields;
+        return this;
+    }
+    public DoMergeParam withContentRenameOperations(Map<String, List<Map<String, Object>>> contentRenameOperations) {
+        this.contentRenameOperations = contentRenameOperations;
+        return this;
+    }
+    public DoMergeParam withContentDeleteOperations(Map<String, List<Map<String, Object>>> contentDeleteOperations) {
+        this.contentDeleteOperations = contentDeleteOperations;
+        return this;
+    }
+    public static DoMergeParam of() {
+        return new DoMergeParam();
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/GenericPropertiesParam.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/GenericPropertiesParam.java
new file mode 100644
index 0000000000..c2496714df
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/GenericPropertiesParam.java
@@ -0,0 +1,53 @@
+package com.tapdata.tm.task.service.batchin.entity;
+
+import lombok.Data;
+
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class GenericPropertiesParam {
+    Map<String, Object> parent;
+    Map<String, Object> contentMapping;
+    Map<String, Object> relationshipsMapping;
+    Map<String, Object> full;
+    Map<String, String> sourceToJS;
+    Map<String, Map<String, Map<String, Object>>> renameFields;
+    Map<String, List<Map<String, Object>>> contentDeleteOperations;
+    Map<String, List<Map<String, Object>>> contentRenameOperations;
+    public GenericPropertiesParam withParent(Map<String, Object> parent) {
+        this.parent = parent;
+        return this;
+    }
+    public GenericPropertiesParam withContentMapping(Map<String, Object> contentMapping) {
+        this.contentMapping = contentMapping;
+        return this;
+    }
+    public GenericPropertiesParam withRelationshipsMapping(Map<String, Object> relationshipsMapping) {
+        this.relationshipsMapping = relationshipsMapping;
+        return this;
+    }
+    public GenericPropertiesParam withFull(Map<String, Object> full) {
+        this.full = full;
+        return this;
+    }
+    public GenericPropertiesParam withSourceToJS(Map<String, String> sourceToJS) {
+        this.sourceToJS = sourceToJS;
+        return this;
+    }
+    public GenericPropertiesParam withContentDeleteOperations(Map<String, List<Map<String, Object>>> contentDeleteOperations) {
+        this.contentDeleteOperations = contentDeleteOperations;
+        return this;
+    }
+    public GenericPropertiesParam withContentRenameOperations(Map<String, List<Map<String, Object>>> contentRenameOperations) {
+        this.contentRenameOperations = contentRenameOperations;
+        return this;
+    }
+    public GenericPropertiesParam withRenameFields(Map<String, Map<String, Map<String, Object>>> renameFields) {
+        this.renameFields = renameFields;
+        return this;
+    }
+    public static GenericPropertiesParam of() {
+        return new GenericPropertiesParam();
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/ParseParam.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/ParseParam.java
new file mode 100644
index 0000000000..d87bce425b
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/batchin/entity/ParseParam.java
@@ -0,0 +1,84 @@
+package com.tapdata.tm.task.service.batchin.entity;
+
+import com.tapdata.tm.config.security.UserDetail;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.Map;
+
+public class ParseParam {
+    MultipartFile multipartFile;
+    String source;
+    String sink;
+    UserDetail user;
+
+    String relMigStr;
+    Map<String, Object> relMigInfo;
+
+    public MultipartFile getMultipartFile() {
+        return multipartFile;
+    }
+
+    public void setMultipartFile(MultipartFile multipartFile) {
+        this.multipartFile = multipartFile;
+    }
+
+    public ParseParam withMultipartFile(MultipartFile multipartFile) {
+        this.multipartFile = multipartFile;
+        return this;
+    }
+
+    public String getSource() {
+        return source;
+    }
+
+    public void setSource(String source) {
+        this.source = source;
+    }
+
+    public ParseParam withSource(String source) {
+        this.source = source;
+        return this;
+    }
+
+    public String getSink() {
+        return sink;
+    }
+
+    public void setSink(String sink) {
+        this.sink = sink;
+    }
+
+    public ParseParam withSink(String sink) {
+        this.sink = sink;
+        return this;
+    }
+
+    public UserDetail getUser() {
+        return user;
+    }
+
+    public void setUser(UserDetail user) {
+        this.user = user;
+    }
+
+    public ParseParam withUser(UserDetail user) {
+        this.user = user;
+        return this;
+    }
+
+    public String getRelMigStr() {
+        return relMigStr;
+    }
+
+    public void setRelMigStr(String relMigStr) {
+        this.relMigStr = relMigStr;
+    }
+
+    public Map<String, Object> getRelMigInfo() {
+        return relMigInfo;
+    }
+
+    public void setRelMigInfo(Map<String, Object> relMigInfo) {
+        this.relMigInfo = relMigInfo;
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/impl/TaskNodeServiceImpl.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/impl/TaskNodeServiceImpl.java
index 7ace793f19..e5e20b4bca 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/task/service/impl/TaskNodeServiceImpl.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/impl/TaskNodeServiceImpl.java
@@ -31,6 +31,7 @@
 import com.tapdata.tm.messagequeue.service.MessageQueueService;
 import com.tapdata.tm.metadatainstance.service.MetadataInstancesService;
 import com.tapdata.tm.monitoringlogs.service.MonitoringLogsService;
+import com.tapdata.tm.task.constant.SyncStatus;
 import com.tapdata.tm.task.service.TaskNodeService;
 import com.tapdata.tm.task.service.TaskRecordService;
 import com.tapdata.tm.task.service.TaskService;
@@ -568,6 +569,7 @@ public void testRunJsNode(TestRunDto dto, UserDetail userDetail) {
         BeanUtils.copyProperties(taskDto, taskDtoCopy);
         taskDtoCopy.setSyncType(TaskDto.SYNC_TYPE_TEST_RUN);
         taskDtoCopy.setStatus(TaskDto.STATUS_WAIT_RUN);
+        taskDtoCopy.setSyncStatus(SyncStatus.NORMAL);
         if (TaskDto.SYNC_TYPE_MIGRATE.equals(taskDto.getSyncType())) {
             DatabaseNode first = dtoDag.getSourceNode().getFirst();
             first.setTableNames(Lists.of(tableName));
@@ -666,6 +668,7 @@ public Map<String, Object> testRunJsNodeRPC(TestRunDto dto, UserDetail userDetai
         BeanUtils.copyProperties(taskDto, taskDtoCopy);
         taskDtoCopy.setSyncType(TaskDto.SYNC_TYPE_TEST_RUN);
         taskDtoCopy.setStatus(TaskDto.STATUS_WAIT_RUN);
+        taskDtoCopy.setSyncStatus(SyncStatus.NORMAL);
         if (TaskDto.SYNC_TYPE_MIGRATE.equals(taskDto.getSyncType())) {
             DatabaseNode first = dtoDag.getSourceNode().getFirst();
             first.setTableNames(Lists.of(tableName));
diff --git a/manager/tm/src/main/java/com/tapdata/tm/worker/service/WorkerServiceImpl.java b/manager/tm/src/main/java/com/tapdata/tm/worker/service/WorkerServiceImpl.java
index 434bb1de44..c468b4d64a 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/worker/service/WorkerServiceImpl.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/worker/service/WorkerServiceImpl.java
@@ -67,6 +67,7 @@
 import org.springframework.stereotype.Service;
 
 import java.math.BigInteger;
+import java.text.SimpleDateFormat;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
@@ -876,4 +877,16 @@ public Long getLastCheckAvailableAgentCount(){
                 .and("agentTags").ne("disabledScheduleTask");
         return count(Query.query(criteria));
     }
+
+    public String getWorkerCurrentTime(UserDetail userDetail){
+        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+        List<Worker> workerList = findAvailableAgent(userDetail);
+        if(CollectionUtils.isNotEmpty(workerList)){
+            Worker worker = workerList.get(0);
+            if( null != worker.getWorkerDate()){
+                return simpleDateFormat.format(worker.getWorkerDate());
+            }
+        }
+        return simpleDateFormat.format(new Date());
+    }
 }
diff --git a/manager/tm/src/main/resources/messages.properties b/manager/tm/src/main/resources/messages.properties
index bef22f1bd6..f569b2b905 100644
--- a/manager/tm/src/main/resources/messages.properties
+++ b/manager/tm/src/main/resources/messages.properties
@@ -229,4 +229,8 @@ lack.group.agent=Please supplement the Agent label
 
 #task import
 task.import.connection.check.ConnectorNotRegister=Task import failed. Please register data source {0} and dependent version {1} before importing. If it is already registered, please try again
-task.import.connection.check.ConnectorVersionTooHeight=Task import failed. The version of connector {0} used in the import task is too high. The current environment is using version {1}, and the version in the import task is {2}. Please upgrade connector {3} and import task again
\ No newline at end of file
+task.import.connection.check.ConnectorVersionTooHeight=Task import failed. The version of connector {0} used in the import task is too high. The current environment is using version {1}, and the version in the import task is {2}. Please upgrade connector {3} and import task again
+
+# import task by relMig
+relMig.parse.failed=relmig file parsing failed, message: {0}
+relMig.parse.unSupport=The current relmig file version {0} is not currently supported, and only supports: {1}
\ No newline at end of file
diff --git a/manager/tm/src/main/resources/messages_en_US.properties b/manager/tm/src/main/resources/messages_en_US.properties
index 5c3211605a..38c8b01c20 100644
--- a/manager/tm/src/main/resources/messages_en_US.properties
+++ b/manager/tm/src/main/resources/messages_en_US.properties
@@ -161,4 +161,7 @@ lack.group.agent=Please supplement the Agent label
 
 #task import
 task.import.connection.check.ConnectorNotRegister=Task import failed. Please register data source {0} and dependent version {1} before importing. If it is already registered, please try again
-task.import.connection.check.ConnectorVersionTooHeight=Task import failed. The version of connector {0} used in the import task is too high. The current environment is using version {1}, and the version in the import task is {2}. Please upgrade connector {3} and import task again
\ No newline at end of file
+task.import.connection.check.ConnectorVersionTooHeight=Task import failed. The version of connector {0} used in the import task is too high. The current environment is using version {1}, and the version in the import task is {2}. Please upgrade connector {3} and import task again
+
+relMig.parse.failed=relmig file parsing failed, message: {0}
+relMig.parse.unSupport=The current relmig file version {0} is not currently supported, and only supports: {1}
\ No newline at end of file
diff --git a/manager/tm/src/main/resources/messages_zh_CN.properties b/manager/tm/src/main/resources/messages_zh_CN.properties
index efb715b200..3ca525050c 100644
--- a/manager/tm/src/main/resources/messages_zh_CN.properties
+++ b/manager/tm/src/main/resources/messages_zh_CN.properties
@@ -262,3 +262,6 @@ lack.group.agent=\u8BF7\u8865\u5145Agent\u6807\u7B7E
 #task import
 task.import.connection.check.ConnectorNotRegister=\u4EFB\u52A1\u5BFC\u5165\u5931\u8D25\uFF0C\u5BFC\u5165\u524D\u8BF7\u6CE8\u518C\u6570\u636E\u6E90\uFF1A{0}, \u4F9D\u8D56\u7684\u7248\u672C\uFF1A{1}, \u5982\u5DF2\u6CE8\u518C\u8BF7\u91CD\u8BD5
 task.import.connection.check.ConnectorVersionTooHeight=\u4EFB\u52A1\u5BFC\u5165\u5931\u8D25\uFF0C\u5BFC\u5165\u4EFB\u52A1\u4E2D\u4F7F\u7528\u7684\u6570\u636E\u6E90 {0} \u7248\u672C\u8FC7\u9AD8\uFF0C\u5F53\u524D\u73AF\u5883\u4F7F\u7528\u7684\u7248\u672C\uFF1A{1}, \u5BFC\u5165\u4EFB\u52A1\u4E2D\u7684\u7248\u672C\uFF1A{2}, \u8BF7\u5347\u7EA7\u6570\u636E\u6E90 {3}\u540E\u91CD\u65B0\u5BFC\u5165
+
+relMig.parse.failed=relmig \u6587\u4EF6\u89E3\u6790\u5931\u8D25, \u9519\u8BEF\u539F\u56E0: {0}
+relMig.parse.unSupport=\u5F53\u524Drelmig\u6587\u4EF6\u7248\u672C{0}\u6682\u4E0D\u652F\u6301\uFF0C\u76EE\u524D\u4EC5\u652F\u6301\u652F\u6301\uFF1A{1}
\ No newline at end of file
diff --git a/manager/tm/src/main/resources/messages_zh_TW.properties b/manager/tm/src/main/resources/messages_zh_TW.properties
index 17e846eccc..8a830ce1dd 100644
--- a/manager/tm/src/main/resources/messages_zh_TW.properties
+++ b/manager/tm/src/main/resources/messages_zh_TW.properties
@@ -99,3 +99,6 @@ lack.group.agent=\u8ACB\u88DC\u5145Agent\u6A19\u7C64
 #task import
 task.import.connection.check.ConnectorNotRegister=\u4EFB\u52D9\u5C0E\u5165\u5931\u6557\uFF0C\u5C0E\u5165\u524D\u8ACB\u8A3B\u518A\u8CC7\u6599\u4F86\u6E90\uFF1A{0}\uFF0C\u4F9D\u8CF4\u7684\u7248\u672C\uFF1A{1}\uFF0C\u5982\u5DF2\u6CE8\u518C\u8ACB\u91CD\u8A66
 task.import.connection.check.ConnectorVersionTooHeight=\u4EFB\u52D9\u5C0E\u5165\u5931\u6557\uFF0C\u5C0E\u5165\u4EFB\u52D9\u4E2D\u4F7F\u7528\u7684\u6578\u64DA\u6E90 {0} \u7248\u672C\u904E\u9AD8\uFF0C\u7576\u524D\u74B0\u5883\u4F7F\u7528\u7684\u7248\u672C\uFF1A{1}\uFF0C\u5C0E\u5165\u4EFB\u52D9\u4E2D\u7684\u7248\u672C\uFF1A{2}\uFF0C\u8ACB\u965E\u7D1A\u6578\u64DA\u6E90 {3} \u5F8C\u91CD\u65B0\u5C0E\u5165
+
+relMig.parse.failed=relmig\u6A94\u6848\u89E3\u6790\u5931\u6557\uFF0C\u932F\u8AA4\u539F\u56E0\uFF1A{0}
+relMig.parse.unSupport=\u7576\u524Drelmig\u6A94\u6848\u7248\u672C{0}\u66AB\u4E0D\u652F\u6301\uFF0C\u73FE\u6642\u50C5\u652F\u6301\u652F\u6301\uFF1A{1}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index e49ba2d939..7ae4610a91 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,11 +22,13 @@
             manager/tm/**/vo/*,
             manager/tm/**/controller/*,
             manager/tm/**/constant/*,
+            manager/tm/**/ex/*,
             **/*Dto.java,
             **/*Enum.java,
             iengine/iengine-common/src/main/java/com/tapdata/entity/**,
             tapdata-cli/**,
-            iengine/iengine-common/src/main/java/com/tapdata/exception/**
+            iengine/iengine-common/src/main/java/com/tapdata/exception/**,
+            manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/**
         </sonar.coverage.exclusions>
         <sonar.exclusions>
             manager/tm/**/dto/*,
@@ -34,10 +36,12 @@
             manager/tm/**/vo/*,
             manager/tm/**/controller/*,
             manager/tm/**/constant/*,
+            manager/tm/**/ex/*,
             **/*Dto.java,
             **/*Enum.java,
             iengine/iengine-common/src/main/java/com/tapdata/entity/**,
-            tapdata-cli/**
+            tapdata-cli/**,
+            manager/tm-common/src/main/java/com/tapdata/tm/commons/task/dto/**
         </sonar.exclusions>
         <sonar.test.exclusions>
             **/src/test/**


Test patch:
diff --git a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastPdkBaseNodeTest.java b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastPdkBaseNodeTest.java
index fe3514b7e5..ab25b82eaa 100644
--- a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastPdkBaseNodeTest.java
+++ b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastPdkBaseNodeTest.java
@@ -6,12 +6,17 @@
 import com.tapdata.mongo.HttpClientMongoOperator;
 import com.tapdata.tm.commons.dag.DmlPolicy;
 import com.tapdata.tm.commons.dag.DmlPolicyEnum;
+import com.tapdata.tm.commons.dag.Node;
+import com.tapdata.tm.commons.dag.logCollector.LogCollectorNode;
+import com.tapdata.tm.commons.dag.nodes.CacheNode;
+import com.tapdata.tm.commons.dag.nodes.DatabaseNode;
 import com.tapdata.tm.commons.dag.nodes.TableNode;
 import com.tapdata.tm.commons.task.dto.TaskDto;
 import io.tapdata.entity.codec.filter.TapCodecsFilterManager;
 import io.tapdata.entity.logger.TapLogger;
 import io.tapdata.entity.schema.TapField;
 import io.tapdata.entity.schema.TapTable;
+import io.tapdata.entity.simplify.TapSimplify;
 import io.tapdata.flow.engine.V2.entity.PdkStateMap;
 import io.tapdata.flow.engine.V2.filter.TapRecordSkipDetector;
 import io.tapdata.observable.logging.ObsLogger;
@@ -439,4 +444,57 @@ void test2(){
 		}
 
 	}
+
+	@Nested
+	class GenerateNodeConfigTest {
+
+		@BeforeEach
+		void setUp(){
+			hazelcastPdkBaseNode = mock(HazelcastPdkBaseNode.class);
+			doCallRealMethod().when(hazelcastPdkBaseNode).generateNodeConfig(any(), any());
+		}
+
+		@Test
+		void testTableNode() {
+			Map<String, Object> nodeConfig = TapSimplify.map(TapSimplify.entry("key", "value"));
+			TableNode node1 = new TableNode();
+			node1.setNodeConfig(null);
+			TableNode node2 = new TableNode();
+			node2.setNodeConfig(nodeConfig);
+			Assertions.assertEquals(1, hazelcastPdkBaseNode.generateNodeConfig(node1, true).size());
+			Assertions.assertTrue(hazelcastPdkBaseNode.generateNodeConfig(node1, true).containsKey("doubleActive"));
+			Assertions.assertEquals(2, hazelcastPdkBaseNode.generateNodeConfig(node2, true).size());
+		}
+
+		@Test
+		void testDatabaseNode() {
+			Map<String, Object> nodeConfig = TapSimplify.map(TapSimplify.entry("key", "value"));
+			DatabaseNode node1 = new DatabaseNode();
+			node1.setNodeConfig(null);
+			DatabaseNode node2 = new DatabaseNode();
+			node2.setNodeConfig(nodeConfig);
+			Assertions.assertEquals(1, hazelcastPdkBaseNode.generateNodeConfig(node1, true).size());
+			Assertions.assertTrue(hazelcastPdkBaseNode.generateNodeConfig(node1, true).containsKey("doubleActive"));
+			Assertions.assertEquals(2, hazelcastPdkBaseNode.generateNodeConfig(node2, true).size());
+		}
+
+		@Test
+		void testLogCollectorNode() {
+			Map<String, Object> nodeConfig = TapSimplify.map(TapSimplify.entry("key", "value"));
+			LogCollectorNode node1 = new LogCollectorNode();
+			node1.setNodeConfig(null);
+			LogCollectorNode node2 = new LogCollectorNode();
+			node2.setNodeConfig(nodeConfig);
+			Assertions.assertEquals(1, hazelcastPdkBaseNode.generateNodeConfig(node1, true).size());
+			Assertions.assertTrue(hazelcastPdkBaseNode.generateNodeConfig(node1, true).containsKey("doubleActive"));
+			Assertions.assertEquals(2, hazelcastPdkBaseNode.generateNodeConfig(node2, true).size());
+		}
+
+		@Test
+		void testOtherNode() {
+			CacheNode node = new CacheNode();
+			Assertions.assertEquals(1, hazelcastPdkBaseNode.generateNodeConfig(node, true).size());
+			Assertions.assertTrue(hazelcastPdkBaseNode.generateNodeConfig(node, true).containsKey("doubleActive"));
+		}
+	}
 }
diff --git a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastSourcePdkBaseNodeTest.java b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastSourcePdkBaseNodeTest.java
index 387ef78567..1365c4c0d0 100644
--- a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastSourcePdkBaseNodeTest.java
+++ b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastSourcePdkBaseNodeTest.java
@@ -11,6 +11,7 @@
 import com.tapdata.entity.dataflow.TableBatchReadStatus;
 import com.tapdata.entity.task.config.TaskConfig;
 import com.tapdata.entity.task.config.TaskRetryConfig;
+import com.tapdata.entity.task.context.DataProcessorContext;
 import com.tapdata.tm.commons.dag.DAG;
 import com.tapdata.tm.commons.dag.DAGDataServiceImpl;
 import com.tapdata.tm.commons.dag.DDLConfiguration;
@@ -27,6 +28,7 @@
 import io.tapdata.entity.event.TapEvent;
 import io.tapdata.entity.event.ddl.TapDDLEvent;
 import io.tapdata.entity.event.ddl.TapDDLUnknownEvent;
+import io.tapdata.entity.event.dml.TapInsertRecordEvent;
 import io.tapdata.entity.mapping.DefaultExpressionMatchingMap;
 import io.tapdata.entity.schema.TapField;
 import io.tapdata.entity.schema.TapTable;
@@ -39,6 +41,7 @@
 import io.tapdata.flow.engine.V2.util.PdkUtil;
 import io.tapdata.flow.engine.V2.util.SyncTypeEnum;
 import io.tapdata.node.pdk.ConnectorNodeService;
+import io.tapdata.observable.logging.ObsLogger;
 import io.tapdata.pdk.apis.context.TapConnectorContext;
 import io.tapdata.pdk.apis.functions.ConnectorFunctions;
 import io.tapdata.pdk.apis.functions.PDKMethod;
@@ -53,6 +56,7 @@
 import io.tapdata.schema.TapTableMap;
 import lombok.SneakyThrows;
 import org.bson.types.ObjectId;
+import org.junit.Assert;
 import org.junit.jupiter.api.*;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
@@ -66,6 +70,7 @@
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
 
 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Mockito.*;
@@ -875,7 +880,7 @@ void testLocalTZSyncPoint() {
 			syncPoint.setNodeId(instance.getNode().getId());
 			syncPoint.setPointType("localTZ");
 			long syncDateTime = System.currentTimeMillis();
-			syncPoint.setDateTime(System.currentTimeMillis());
+			syncPoint.setDateTime(syncDateTime);
 			List<TaskDto.SyncPoint> syncPoints = new ArrayList<>();
 			syncPoints.add(syncPoint);
 			dataProcessorContext.getTaskDto().setSyncPoints(syncPoints);
@@ -1170,4 +1175,42 @@ void testWhenConnectorNodeIsNull() {
 		}
 	}
 
+
+	@Test
+	void testHandleCustomCommandResultForList() {
+        HazelcastSourcePdkDataNode hazelcastSourcePdkDataNode = Mockito.mock(HazelcastSourcePdkDataNode.class);
+		List<Map<String, Object>> list = new ArrayList<>();
+		Map<String, Object> map = new HashMap<>();
+		map.put("id",1);
+		list.add(map);
+		String tableName = "test";
+		BiConsumer<List<TapEvent>, Object> consumer = new BiConsumer<List<TapEvent>, Object>() {
+			@Override
+			public void accept(List<TapEvent> tapEvents, Object o) {
+				TapEvent tapEvent = tapEvents.get(0);
+				Assert.assertEquals(tableName,((TapInsertRecordEvent)tapEvent).getTableId());
+			}
+		};
+		ReflectionTestUtils.invokeMethod(hazelcastSourcePdkDataNode,"handleCustomCommandResult",
+				list,tableName,consumer);
+	}
+
+
+	@Test
+	void testHandleCustomCommandResultForLong() {
+		HazelcastSourcePdkDataNode hazelcastSourcePdkDataNode = Mockito.mock(HazelcastSourcePdkDataNode.class);
+		String tableName = "test";
+		long excepted = 8L;
+		BiConsumer<List<TapEvent>, Object> consumer = new BiConsumer<List<TapEvent>, Object>() {
+			@Override
+			public void accept(List<TapEvent> tapEvents, Object o) {
+				Assert.assertTrue(!tapEvents.isEmpty());
+			}
+		};
+		ReflectionTestUtils.setField(hazelcastSourcePdkDataNode,"obsLogger",Mockito.mock(ObsLogger.class));
+		ReflectionTestUtils.invokeMethod(hazelcastSourcePdkDataNode,"handleCustomCommandResult",
+				excepted,tableName,consumer);
+	}
+
+
 }
diff --git a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastSourcePdkDataNodeTest.java b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastSourcePdkDataNodeTest.java
index c0d5bcd692..a2a258b2b5 100644
--- a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastSourcePdkDataNodeTest.java
+++ b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastSourcePdkDataNodeTest.java
@@ -34,6 +34,8 @@
 import io.tapdata.observable.logging.ObsLogger;
 import io.tapdata.pdk.apis.context.TapConnectorContext;
 import io.tapdata.pdk.apis.entity.QueryOperator;
+import io.tapdata.pdk.apis.entity.TapTimeForm;
+import io.tapdata.pdk.apis.entity.TapTimeUnit;
 import io.tapdata.pdk.apis.functions.ConnectorFunctions;
 import io.tapdata.pdk.apis.functions.PDKMethod;
 import io.tapdata.pdk.apis.functions.connector.source.BatchCountFunction;
@@ -269,7 +271,7 @@ public void queryOperator(String dateTime,QueryOperator queryOperator){
 		tapTable.setNameFieldMap(nameFieldMap);
 		tapTable.setName("test");
 		tapTable.setId("test");
-		TapTableMap<String, TapTable>  tapTableMap =TapTableMap.create("test",tapTable);
+		TapTableMap<String, TapTable>  tapTableMap = TapTableMap.create("test",tapTable);
 
 		when(dataProcessorContext.getTapTableMap()).thenReturn(tapTableMap);
 		ReflectionTestUtils.invokeMethod(hazelcastSourcePdkDataNode,"batchFilterRead");
@@ -1368,4 +1370,52 @@ void testExceptionButNotSupportDebug() {
 			}
 		}
 	}
+
+	@Nested
+	class timeTransformationTest{
+		@DisplayName("Query the data of the previous day")
+		@Test
+		void test(){
+			List<QueryOperator> conditions = new ArrayList<>();
+			QueryOperator queryOperator = new QueryOperator();
+			queryOperator.setFastQuery(true);
+			queryOperator.setForm(TapTimeForm.BEFORE);
+			queryOperator.setUnit(TapTimeUnit.HOUR);
+			queryOperator.setNumber(1L);
+			conditions.add(queryOperator);
+			List<QueryOperator> result =  hazelcastSourcePdkDataNode.timeTransformation(conditions,null);
+			Assertions.assertEquals(result.size(),2);
+			Assertions.assertEquals(result.get(0).getOperator(),2);
+			Assertions.assertEquals(result.get(1).getOperator(),4);
+		}
+		@DisplayName("Specify query conditions")
+		@Test
+		void test1(){
+			List<QueryOperator> conditions = new ArrayList<>();
+			QueryOperator queryOperator = new QueryOperator();
+			queryOperator.setFastQuery(false);
+			conditions.add(queryOperator);
+			List<QueryOperator> result =  hazelcastSourcePdkDataNode.timeTransformation(conditions,null);
+			Assertions.assertEquals(result.size(),1);
+		}
+	}
+	@Nested
+	class constructQueryOperatorTest{
+		@DisplayName("timeList is null")
+		@Test
+		void test(){
+			List<QueryOperator> result = hazelcastSourcePdkDataNode.constructQueryOperator(null,new QueryOperator());
+			Assertions.assertEquals(0,result.size());
+		}
+
+		@DisplayName("Main process")
+		@Test
+		void test1(){
+			List<String> timeList = new ArrayList<>();
+			timeList.add("test1");
+			timeList.add("test2");
+			List<QueryOperator> result = hazelcastSourcePdkDataNode.constructQueryOperator(timeList,new QueryOperator());
+			Assertions.assertEquals(2,result.size());
+		}
+	}
 }
diff --git a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkBaseNodeTest.java b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkBaseNodeTest.java
index 912949b691..e2de8b0020 100644
--- a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkBaseNodeTest.java
+++ b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkBaseNodeTest.java
@@ -11,6 +11,7 @@
 import com.tapdata.tm.commons.dag.Node;
 import com.tapdata.tm.commons.dag.nodes.DatabaseNode;
 import com.tapdata.tm.commons.dag.nodes.TableNode;
+import com.tapdata.tm.commons.dag.process.MergeTableNode;
 import com.tapdata.tm.commons.dag.process.UnwindProcessNode;
 import com.tapdata.tm.commons.task.dto.TaskDto;
 import io.tapdata.entity.codec.filter.TapCodecsFilterManager;
@@ -348,5 +349,42 @@ void testDmlPolicyIsNull(){
             verify(obsLogger,times(1)).warn(any());
         }
     }
+    @Nested
+    class IsCDCConcurrentTest{
+        ObsLogger obsLogger;
+        @BeforeEach
+        void setUp() {
+            obsLogger = mock(ObsLogger.class);
+            ReflectionTestUtils.setField(hazelcastTargetPdkBaseNode,"cdcConcurrentWriteNum",8);
+            ReflectionTestUtils.setField(hazelcastTargetPdkBaseNode,"obsLogger",obsLogger);
+        }
+        @Test
+        void testUnwindProcess(){
+            TableNode node = mock(TableNode.class);
+            List predecessors = new ArrayList<>();
+            UnwindProcessNode unwindProcessNode = new UnwindProcessNode();
+            predecessors.add(unwindProcessNode);
+            when(node.predecessors()).thenReturn(predecessors);
+            when(hazelcastTargetPdkBaseNode.getNode()).thenReturn((Node)node);
+            doCallRealMethod().when(hazelcastTargetPdkBaseNode).isCDCConcurrent(true);
+            boolean result = hazelcastTargetPdkBaseNode.isCDCConcurrent(true);
+            Assertions.assertFalse(result);
+            verify(obsLogger,times(1)).info(any());
+        }
+
+        @Test
+        void testMergeTableNode(){
+            TableNode node = mock(TableNode.class);
+            List predecessors = new ArrayList<>();
+            MergeTableNode mergeTableNode = new MergeTableNode();
+            predecessors.add(mergeTableNode);
+            when(node.predecessors()).thenReturn(predecessors);
+            when(hazelcastTargetPdkBaseNode.getNode()).thenReturn((Node)node);
+            doCallRealMethod().when(hazelcastTargetPdkBaseNode).isCDCConcurrent(true);
+            boolean result = hazelcastTargetPdkBaseNode.isCDCConcurrent(true);
+            Assertions.assertFalse(result);
+            verify(obsLogger,times(1)).info(any());
+        }
+    }
 
 }
diff --git a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/concurrent/selector/TapEventPartitionKeySelectorTest.java b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/concurrent/selector/TapEventPartitionKeySelectorTest.java
new file mode 100644
index 0000000000..df2bc5bc7a
--- /dev/null
+++ b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/concurrent/selector/TapEventPartitionKeySelectorTest.java
@@ -0,0 +1,61 @@
+package io.tapdata.flow.engine.V2.node.hazelcast.data.pdk.concurrent.selector;
+
+import io.tapdata.entity.event.dml.TapUpdateRecordEvent;
+import io.tapdata.entity.schema.type.TapString;
+import io.tapdata.entity.schema.value.TapStringValue;
+import io.tapdata.entity.schema.value.TapValue;
+import org.bson.types.ObjectId;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author samuel
+ * @Description
+ * @create 2024-04-28 18:32
+ **/
+@DisplayName("Class TapEventPartitionKeySelector Test")
+class TapEventPartitionKeySelectorTest {
+	@Nested
+	@DisplayName("Method select Test")
+	class selectTest {
+		@Test
+		@DisplayName("test main process")
+		void testMainProcess() {
+			TapEventPartitionKeySelector keySelector = new TapEventPartitionKeySelector(tapEvent -> {
+				List<String> list = new ArrayList<>();
+				list.add("_id");
+				return list;
+			});
+
+			ObjectId oid = new ObjectId();
+			TapValue<String, TapString> _idTapStringValue = new TapStringValue(oid.toHexString())
+					.originValue(oid)
+					.tapType(new TapString());
+
+			Map<String, Object> before = new HashMap<>();
+			before.put("_id", _idTapStringValue);
+			before.put("name", "test");
+
+			Map<String, Object> after = new HashMap<>();
+			after.put("_id", _idTapStringValue);
+			after.put("name", "test1");
+
+			TapUpdateRecordEvent tapEvent = TapUpdateRecordEvent.create().init()
+					.before(before)
+					.after(after);
+
+			List<Object> result = keySelector.select(tapEvent, after);
+
+			assertEquals(1, result.size());
+			assertEquals(oid.toHexString(), result.get(0));
+		}
+	}
+}
\ No newline at end of file
diff --git a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/UnwindNodeTest.java b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/UnwindNodeTest.java
index cc740dcd25..7d55bc1fdd 100644
--- a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/UnwindNodeTest.java
+++ b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/UnwindNodeTest.java
@@ -7,17 +7,21 @@
 import io.tapdata.entity.event.TapEvent;
 import io.tapdata.entity.event.dml.TapDeleteRecordEvent;
 import io.tapdata.entity.event.dml.TapInsertRecordEvent;
+import io.tapdata.entity.event.dml.TapRecordEvent;
 import io.tapdata.entity.event.dml.TapUpdateRecordEvent;
 import io.tapdata.flow.engine.V2.node.hazelcast.processor.unwind.EventHandel;
 import io.tapdata.flow.engine.V2.node.hazelcast.processor.unwind.UnWindNodeUtil;
 import org.bson.Document;
 import org.junit.Assert;
 import org.junit.Test;
+import org.junit.jupiter.api.Assertions;
 
 import java.text.DecimalFormat;
 import java.util.*;
 
 import static com.tapdata.tm.sdk.util.JacksonUtil.toJson;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 
 public class UnwindNodeTest extends BaseTest {
@@ -139,7 +143,7 @@ public void emptyValueOfSinglePathWithUnwind() throws JsonProcessingException {
 
         //不指定索引字段，忽略null或empty
         List<TapEvent> handelResult = EventHandel.getHandelResult(node, event);
-        boolean count = handelResult.size() == 0;
+        boolean count = handelResult.size() == 1;
         Assert.assertTrue(
                 "Fail get 1 delete event from tapdata event by unwind node, from event: " + toJson(before)
                         + ", only " + handelResult.size() + " after unwind node",
@@ -201,7 +205,7 @@ public void nullValueOfSinglePathWithUnwind() throws JsonProcessingException {
 
         //不指定索引字段，忽略null或empty
         List<TapEvent> handelResult = EventHandel.getHandelResult(node, event);
-        boolean count = handelResult.size() == 0;
+        boolean count = handelResult.size() == 1;
         Assert.assertTrue(
                 "Fail get 1 delete event from tapdata event by unwind node, from event: " + toJson(before)
                         + ", only " + handelResult.size() + " after unwind node",
@@ -265,7 +269,7 @@ public void emptyListValueOfSinglePathWithUnwind() throws JsonProcessingExceptio
 
         //不指定索引字段，忽略null或empty
         List<TapEvent> handelResult = EventHandel.getHandelResult(node, event);
-        boolean count = handelResult.size() == 0;
+        boolean count = handelResult.size() == 1;
         Assert.assertTrue(
                 "Fail get 1 delete event from tapdata event by unwind node, from event: " + toJson(before)
                         + ", only " + handelResult.size() + " after unwind node",
@@ -400,7 +404,7 @@ public void emptyValueOfManyPathWithUnwind() throws JsonProcessingException {
 
         //不指定索引字段，忽略null或empty
         List<TapEvent> handelResult = EventHandel.getHandelResult(node, event);
-        boolean count = handelResult.size() == 0;
+        boolean count = handelResult.size() == 1;
         Assert.assertTrue(
                 "Fail get 1 delete event from tapdata event by unwind node, from event: " + toJson(before)
                         + ", only " + handelResult.size() + " after unwind node",
@@ -465,7 +469,7 @@ public void nullValueOfManyPathWithUnwind() throws JsonProcessingException {
 
         //不指定索引字段，忽略null或empty
         List<TapEvent> handelResult = EventHandel.getHandelResult(node, event);
-        boolean count = handelResult.size() == 0;
+        boolean count = handelResult.size() == 1;
         Assert.assertTrue(
                 "Fail get 1 delete event from tapdata event by unwind node, from event: " + toJson(before)
                         + ", only " + handelResult.size() + " after unwind node",
@@ -532,7 +536,7 @@ public void emptyListValueOfManyPathWithUnwind() throws JsonProcessingException
 
         //不指定索引字段，忽略null或empty
         List<TapEvent> handelResult = EventHandel.getHandelResult(node, event);
-        boolean count = handelResult.size() == 0;
+        boolean count = handelResult.size() == 1;
         Assert.assertTrue(
                 "Fail get 1 delete event from tapdata event by unwind node, from event: " + toJson(before)
                         + ", only " + handelResult.size() + " after unwind node",
@@ -1018,4 +1022,59 @@ public void copyEvent(List<TapEvent> events, Map<String, Object> item, TapEvent
         boolean result = UnWindNodeUtil.array(objects,new ArrayList<>(),"test",true,map,"t",parentMap,new String[]{""},event,handel,false,null);
         Assert.assertFalse(result);
     }
+
+    @Test
+    public void testInsertToDelete() {
+        TapRecordEvent event = mock(TapInsertRecordEvent.class);
+        TapDeleteRecordEvent e = UnWindNodeUtil.toDeleteEvent(event);
+        Assertions.assertNull(e);
+    }
+
+    @Test
+    public void testUpdateToDelete() {
+        TapUpdateRecordEvent event = mock(TapUpdateRecordEvent.class);
+        when(event.getBefore()).thenReturn(null);
+        TapDeleteRecordEvent e = UnWindNodeUtil.toDeleteEvent((TapRecordEvent) event);
+        Assertions.assertNotNull(e);
+    }
+    @Test
+    public void testUpdateToDeleteV2() {
+        TapUpdateRecordEvent event = mock(TapUpdateRecordEvent.class);
+        Map<String, Object> b = mock(Map.class);
+        when(b.isEmpty()).thenReturn(true);
+        when(event.getBefore()).thenReturn(b);
+        TapDeleteRecordEvent e = UnWindNodeUtil.toDeleteEvent(event);
+        Assertions.assertNotNull(e);
+    }
+    @Test
+    public void testUpdateToDeleteV3() {
+        TapUpdateRecordEvent event = mock(TapUpdateRecordEvent.class);
+        Map<String, Object> b = mock(Map.class);
+        when(b.isEmpty()).thenReturn(false);
+        when(event.getBefore()).thenReturn(b);
+        TapDeleteRecordEvent e = UnWindNodeUtil.toDeleteEvent(event);
+        Assertions.assertNotNull(e);
+    }
+
+    @Test
+    public void testDeleteToDelete() {
+        TapRecordEvent event = mock(TapDeleteRecordEvent.class);
+        TapDeleteRecordEvent e = UnWindNodeUtil.toDeleteEvent(event);
+        Assertions.assertNotNull(e);
+    }
+
+    @Test
+    public void testAddDeleteEvent() {
+        List<TapEvent> objects = new ArrayList<>();
+        TapInsertRecordEvent event = mock(TapInsertRecordEvent.class);
+        UnWindNodeUtil.addEvent(event, objects);
+        Assertions.assertEquals(0, objects.size());
+    }
+    @Test
+    public void testAddNullDeleteEvent() {
+        List<TapEvent> objects = new ArrayList<>();
+        TapDeleteRecordEvent event = mock(TapDeleteRecordEvent.class);
+        UnWindNodeUtil.addEvent(event, objects);
+        Assertions.assertEquals(1, objects.size());
+    }
 }
diff --git a/iengine/iengine-common/src/test/java/com/tapdata/entity/dataflow/batch/BatchOffsetTest.java b/iengine/iengine-common/src/test/java/com/tapdata/entity/dataflow/batch/BatchOffsetTest.java
new file mode 100644
index 0000000000..ad9e9c4eee
--- /dev/null
+++ b/iengine/iengine-common/src/test/java/com/tapdata/entity/dataflow/batch/BatchOffsetTest.java
@@ -0,0 +1,51 @@
+package com.tapdata.entity.dataflow.batch;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+class BatchOffsetTest {
+    @Nested
+    class CloneTest {
+        @Test
+        void cloneNormal() throws CloneNotSupportedException {
+            BatchOffset batchOffset = new BatchOffset();
+            Object clone = batchOffset.clone();
+            Assertions.assertEquals(BatchOffset.class.getName(), clone.getClass().getName());
+            BatchOffset offset= (BatchOffset)clone;
+            Assertions.assertNull(offset.getStatus());
+            Assertions.assertNull(offset.getOffset());
+        }
+        @Test
+        void cloneOffsetIsMap() throws CloneNotSupportedException {
+            BatchOffset batchOffset = new BatchOffset();
+            Map<String, Object> o = new HashMap<>();
+            batchOffset.setOffset(o);
+            Object clone = batchOffset.clone();
+            Assertions.assertEquals(BatchOffset.class.getName(), clone.getClass().getName());
+            BatchOffset offset= (BatchOffset)clone;
+            Assertions.assertNull(offset.getStatus());
+            Assertions.assertNotNull(offset.getOffset());
+            Assertions.assertEquals(HashMap.class.getName(), offset.getOffset().getClass().getName());
+        }
+
+        @Test
+        void cloneOffsetNotMapAndNotSerializable() throws CloneNotSupportedException {
+            BatchOffset batchOffset = new BatchOffset();
+            batchOffset.setOffset(new TestOffset());
+            Object clone = batchOffset.clone();
+            Assertions.assertEquals(BatchOffset.class.getName(), clone.getClass().getName());
+            BatchOffset offset= (BatchOffset)clone;
+            Assertions.assertNull(offset.getStatus());
+            Assertions.assertNotNull(offset.getOffset());
+            Assertions.assertEquals(TestOffset.class.getName(), offset.getOffset().getClass().getName());
+        }
+
+        class TestOffset {
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/iengine/modules/milestone-module/src/test/java/io/tapdata/milestone/MilestoneAspectTaskTest.java b/iengine/modules/milestone-module/src/test/java/io/tapdata/milestone/MilestoneAspectTaskTest.java
index dec0af27e9..9564af70cb 100644
--- a/iengine/modules/milestone-module/src/test/java/io/tapdata/milestone/MilestoneAspectTaskTest.java
+++ b/iengine/modules/milestone-module/src/test/java/io/tapdata/milestone/MilestoneAspectTaskTest.java
@@ -1,24 +1,97 @@
 package io.tapdata.milestone;
 
+import com.mongodb.client.result.UpdateResult;
+import com.tapdata.constant.BeanUtil;
 import com.tapdata.entity.ResponseBody;
+import com.tapdata.entity.task.context.DataProcessorContext;
+import com.tapdata.entity.task.context.ProcessorBaseContext;
+import com.tapdata.mongo.ClientMongoOperator;
+import com.tapdata.tm.commons.dag.DAG;
+import com.tapdata.tm.commons.dag.Node;
+import com.tapdata.tm.commons.dag.nodes.DatabaseNode;
+import com.tapdata.tm.commons.dag.nodes.TableNode;
 import com.tapdata.tm.commons.task.dto.TaskDto;
+import io.tapdata.aspect.DataNodeCloseAspect;
+import io.tapdata.aspect.DataNodeInitAspect;
+import io.tapdata.aspect.PDKNodeInitAspect;
+import io.tapdata.aspect.ProcessorNodeCloseAspect;
+import io.tapdata.aspect.ProcessorNodeInitAspect;
+import io.tapdata.aspect.TableInitFuncAspect;
+import io.tapdata.aspect.TaskStartAspect;
+import io.tapdata.aspect.TaskStopAspect;
 import io.tapdata.aspect.task.AspectTask;
+import io.tapdata.aspect.taskmilestones.CDCReadBeginAspect;
+import io.tapdata.aspect.taskmilestones.CDCReadErrorAspect;
+import io.tapdata.aspect.taskmilestones.CDCReadStartedAspect;
+import io.tapdata.aspect.taskmilestones.CDCWriteBeginAspect;
+import io.tapdata.aspect.taskmilestones.Snapshot2CDCAspect;
+import io.tapdata.aspect.taskmilestones.SnapshotReadBeginAspect;
+import io.tapdata.aspect.taskmilestones.SnapshotReadEndAspect;
+import io.tapdata.aspect.taskmilestones.SnapshotReadErrorAspect;
+import io.tapdata.aspect.taskmilestones.SnapshotReadTableEndAspect;
+import io.tapdata.aspect.taskmilestones.SnapshotWriteBeginAspect;
+import io.tapdata.aspect.taskmilestones.SnapshotWriteEndAspect;
+import io.tapdata.aspect.taskmilestones.WriteErrorAspect;
+import io.tapdata.entity.aspect.Aspect;
+import io.tapdata.entity.aspect.AspectInterceptResult;
 import io.tapdata.entity.logger.Log;
+import io.tapdata.entity.simplify.pretty.TypeHandlers;
 import io.tapdata.exception.TmUnavailableException;
+import io.tapdata.milestone.constants.MilestoneStatus;
+import io.tapdata.milestone.entity.MilestoneEntity;
 import org.bson.types.ObjectId;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
+import org.springframework.test.util.ReflectionTestUtils;
 
 import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import static io.tapdata.aspect.DataFunctionAspect.STATE_END;
+import static io.tapdata.aspect.DataFunctionAspect.STATE_START;
+import static io.tapdata.aspect.TableInitFuncAspect.STATE_PROCESS;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 /**
  * @author <a href="mailto:harsen_lin@163.com">Harsen</a>
  * @version v1.0 2024/3/11 20:55 Create
  */
 class MilestoneAspectTaskTest {
-
+	MilestoneAspectTask milestoneAspectTask;
+	@BeforeEach
+	void init() {
+		milestoneAspectTask = mock(MilestoneAspectTask.class);
+	}
 	void setField(MilestoneAspectTask milestoneAspectTask, Class<?> clz, String field, Object value) {
 		try {
 			Field logField = clz.getDeclaredField(field);
@@ -44,9 +117,9 @@ public String getType() {
 			task.setId(new ObjectId());
 			task.setName("test-task");
 
-			MilestoneAspectTask milestoneAspectTask = Mockito.mock(MilestoneAspectTask.class, Mockito.CALLS_REAL_METHODS);
-			setField(milestoneAspectTask, AspectTask.class, "log", Mockito.mock(Log.class));
-			setField(milestoneAspectTask, MilestoneAspectTask.class, "executorService", Mockito.mock(ScheduledExecutorService.class));
+			MilestoneAspectTask milestoneAspectTask = mock(MilestoneAspectTask.class, Mockito.CALLS_REAL_METHODS);
+			setField(milestoneAspectTask, AspectTask.class, "log", mock(Log.class));
+			setField(milestoneAspectTask, MilestoneAspectTask.class, "executorService", mock(ScheduledExecutorService.class));
 			milestoneAspectTask.setTask(task);
 
 			milestoneAspectTask.onStop(null); // available
@@ -54,4 +127,1213 @@ public String getType() {
 		}
 
 	}
+
+
+	@Test
+	void testParams() {
+		Assertions.assertEquals("TASK", MilestoneAspectTask.KPI_TASK);
+		Assertions.assertEquals("DATA_NODE_INIT", MilestoneAspectTask.KPI_DATA_NODE_INIT);
+		Assertions.assertEquals("NODE", MilestoneAspectTask.KPI_NODE);
+		Assertions.assertEquals("SNAPSHOT", MilestoneAspectTask.KPI_SNAPSHOT);
+		Assertions.assertEquals("CDC", MilestoneAspectTask.KPI_CDC);
+		Assertions.assertEquals("SNAPSHOT_READ", MilestoneAspectTask.KPI_SNAPSHOT_READ);
+		Assertions.assertEquals("OPEN_CDC_READ", MilestoneAspectTask.KPI_OPEN_CDC_READ);
+		Assertions.assertEquals("CDC_READ", MilestoneAspectTask.KPI_CDC_READ);
+		Assertions.assertEquals("SNAPSHOT_WRITE", MilestoneAspectTask.KPI_SNAPSHOT_WRITE);
+		Assertions.assertEquals("CDC_WRITE", MilestoneAspectTask.KPI_CDC_WRITE);
+		Assertions.assertEquals("TABLE_INIT", MilestoneAspectTask.KPI_TABLE_INIT);
+	}
+
+	@Nested
+	class MilestoneAspectTaskEachTest {
+
+
+		TypeHandlers<Aspect, Void> observerHandlers;
+		TypeHandlers<Aspect, AspectInterceptResult> interceptHandlers;
+		TaskDto task;
+		Log log;
+
+		Map<String, MilestoneEntity> milestones;
+		Map<String, Map<String, MilestoneEntity>> nodeMilestones;
+		ScheduledExecutorService executorService;
+		ClientMongoOperator clientMongoOperator;
+		Set<String> targetNodes;
+		Map<String, MilestoneStatus> dataNodeInitMap;
+		AtomicLong snapshotTableCounts;
+		AtomicLong snapshotTableProgress;
+
+
+		MilestoneEntity m;
+		MilestoneEntity tm;
+		@BeforeEach
+		void init() {
+			m = mock();
+			tm = mock();
+
+			observerHandlers = mock(TypeHandlers.class);
+			interceptHandlers = mock(TypeHandlers.class);
+			task = mock(TaskDto.class);
+			log = mock(Log.class);
+
+			milestones = new HashMap<>();
+			nodeMilestones = new HashMap<>();
+			executorService = mock(ScheduledExecutorService.class);
+			clientMongoOperator = mock(ClientMongoOperator.class);
+			targetNodes = new HashSet<>();
+			dataNodeInitMap = new HashMap<>();
+			snapshotTableCounts = new AtomicLong();
+			snapshotTableProgress = new AtomicLong();
+
+
+			ReflectionTestUtils.setField(milestoneAspectTask, "observerHandlers", observerHandlers);
+			ReflectionTestUtils.setField(milestoneAspectTask, "interceptHandlers", interceptHandlers);
+			ReflectionTestUtils.setField(milestoneAspectTask, "task", task);
+			ReflectionTestUtils.setField(milestoneAspectTask, "log", log);
+			ReflectionTestUtils.setField(milestoneAspectTask, "milestones", milestones);
+			ReflectionTestUtils.setField(milestoneAspectTask, "nodeMilestones", nodeMilestones);
+			ReflectionTestUtils.setField(milestoneAspectTask, "executorService", executorService);
+			ReflectionTestUtils.setField(milestoneAspectTask, "clientMongoOperator", clientMongoOperator);
+			ReflectionTestUtils.setField(milestoneAspectTask, "targetNodes", targetNodes);
+			ReflectionTestUtils.setField(milestoneAspectTask, "dataNodeInitMap", dataNodeInitMap);
+			ReflectionTestUtils.setField(milestoneAspectTask, "snapshotTableCounts", snapshotTableCounts);
+			ReflectionTestUtils.setField(milestoneAspectTask, "snapshotTableProgress", snapshotTableProgress);
+
+			when(observerHandlers.register(any(Class.class), any(Function.class))).thenReturn(observerHandlers);
+			doNothing().when(milestoneAspectTask).setRunning(m);
+			doNothing().when(milestoneAspectTask).setFinish(m);
+
+			doNothing().when(tm).setProgress(anyLong());
+			doAnswer(a -> {
+				Consumer argument = a.getArgument(1, Consumer.class);
+				argument.accept(tm);
+				return null;
+			}).when(milestoneAspectTask).taskMilestone(anyString(), any(Consumer.class));
+
+			doNothing().when(milestoneAspectTask).setFinish(any(MilestoneEntity.class));
+			doNothing().when(milestoneAspectTask).setError(any(SnapshotReadErrorAspect.class), any(MilestoneEntity.class));
+			doNothing().when(milestoneAspectTask).setError(any(CDCReadErrorAspect.class), any(MilestoneEntity.class));
+			doNothing().when(milestoneAspectTask).setError(any(WriteErrorAspect.class), any(MilestoneEntity.class));
+
+
+			doAnswer(w -> {
+				Consumer argument = w.getArgument(2, Consumer.class);
+				argument.accept(m);
+				return null;
+			}).when(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+		}
+
+		@Nested
+		class InitTest {
+			@BeforeEach
+			void init() {
+				doAnswer(a -> {
+					Class classType = a.getArgument(0, Class.class);
+					String typeName = a.getArgument(1, String.class);
+					BiConsumer consumer = a.getArgument(2, BiConsumer.class);
+					mockSnapshotReadBeginAspect(classType, typeName, consumer);
+					mockSnapshotReadEndAspect(classType, typeName, consumer);
+					mockSnapshotReadErrorAspect(classType, typeName, consumer);
+					mockSnapshotReadTableEndAspect(classType, typeName, consumer);
+					mockSnapshot2CDCAspect(classType, typeName, consumer);
+					mockCDCReadBeginAspect(classType, typeName, consumer);
+					mockSnapshotWriteBeginAspect(classType, typeName, consumer);
+					mockSnapshotWriteEndAspect(classType, typeName, consumer);
+					return null;
+				}).when(milestoneAspectTask).nodeRegister(any(Class.class), anyString(), any(BiConsumer.class));
+
+				doAnswer(a -> {
+					Class classType = a.getArgument(0, Class.class);
+					BiConsumer consumer = a.getArgument(1, BiConsumer.class);
+					mockCDCReadStartedAspect(classType, consumer);
+					mockCDCReadErrorAspect(classType, consumer);
+					mockCDCWriteBeginAspect(classType, consumer);
+					mockWriteErrorAspect(classType, consumer);
+					return null;
+				}).when(milestoneAspectTask).nodeRegister(any(Class.class), any(BiConsumer.class));
+
+				doCallRealMethod().when(milestoneAspectTask).init();
+			}
+
+			SnapshotReadBeginAspect snapshotReadBeginAspect;
+			List<String> tablesForSnapshotReadBeginAspect;
+			void mockSnapshotReadBeginAspect(Class classType, String typeName, BiConsumer consumer) {
+				snapshotReadBeginAspect = mock(SnapshotReadBeginAspect.class);
+				doNothing().when(m).setProgress(0L);
+				tablesForSnapshotReadBeginAspect = mock(List.class);
+				when(tablesForSnapshotReadBeginAspect.size()).thenReturn(1);
+				when(snapshotReadBeginAspect.getTables()).thenReturn(tablesForSnapshotReadBeginAspect);
+				if (classType.getName().equals(SnapshotReadBeginAspect.class.getName())
+						&& MilestoneAspectTask.KPI_SNAPSHOT_READ.equals(typeName)) {
+					consumer.accept(snapshotReadBeginAspect, m);
+				}
+			}
+
+			SnapshotReadEndAspect snapshotReadEndAspect;
+			void mockSnapshotReadEndAspect(Class classType, String typeName, BiConsumer consumer) {
+				snapshotReadEndAspect = mock(SnapshotReadEndAspect.class);
+				if (classType.getName().equals(SnapshotReadEndAspect.class.getName())
+						&& MilestoneAspectTask.KPI_SNAPSHOT_READ.equals(typeName)) {
+					consumer.accept(snapshotReadEndAspect, m);
+				}
+			}
+
+			SnapshotReadErrorAspect snapshotReadErrorAspect;
+			void mockSnapshotReadErrorAspect(Class classType, String typeName, BiConsumer consumer) {
+				snapshotReadErrorAspect = mock();
+				if (classType.getName().equals(SnapshotReadErrorAspect.class.getName())
+						&& MilestoneAspectTask.KPI_SNAPSHOT_READ.equals(typeName)) {
+					consumer.accept(snapshotReadErrorAspect, m);
+				}
+			}
+
+			SnapshotReadTableEndAspect snapshotReadTableEndAspect;
+			void mockSnapshotReadTableEndAspect(Class classType, String typeName, BiConsumer consumer) {
+				snapshotReadTableEndAspect = mock();
+				doNothing().when(m).addProgress(1);
+				if (classType.getName().equals(SnapshotReadTableEndAspect.class.getName())
+						&& MilestoneAspectTask.KPI_SNAPSHOT_READ.equals(typeName)) {
+					consumer.accept(snapshotReadTableEndAspect, m);
+				}
+			}
+
+			Snapshot2CDCAspect snapshot2CDCAspect;
+			void mockSnapshot2CDCAspect(Class classType, String typeName, BiConsumer consumer) {
+				snapshot2CDCAspect = mock();
+				if (classType.getName().equals(Snapshot2CDCAspect.class.getName())
+						&& MilestoneAspectTask.KPI_SNAPSHOT_READ.equals(typeName)) {
+					consumer.accept(snapshot2CDCAspect, m);
+				}
+			}
+
+			CDCReadBeginAspect cdcReadBeginAspect;
+			void mockCDCReadBeginAspect(Class classType, String typeName, BiConsumer consumer) {
+				cdcReadBeginAspect = mock();
+				when(milestoneAspectTask.hasSnapshot()).thenReturn(true);
+				if (classType.getName().equals(CDCReadBeginAspect.class.getName())
+						&& MilestoneAspectTask.KPI_OPEN_CDC_READ.equals(typeName)) {
+					consumer.accept(cdcReadBeginAspect, m);
+				}
+			}
+
+			SnapshotWriteBeginAspect snapshotWriteBeginAspect;
+			void mockSnapshotWriteBeginAspect(Class classType, String typeName, BiConsumer consumer) {
+				snapshotWriteBeginAspect = mock();
+				if (classType.getName().equals(SnapshotWriteBeginAspect.class.getName())
+						&& MilestoneAspectTask.KPI_SNAPSHOT_WRITE.equals(typeName)) {
+					consumer.accept(snapshotWriteBeginAspect, m);
+				}
+			}
+
+			SnapshotWriteEndAspect snapshotWriteEndAspect;
+			void mockSnapshotWriteEndAspect(Class classType, String typeName, BiConsumer consumer) {
+				snapshotWriteEndAspect = mock();
+				if (classType.getName().equals(SnapshotWriteEndAspect.class.getName())
+						&& MilestoneAspectTask.KPI_SNAPSHOT_WRITE.equals(typeName)) {
+					consumer.accept(snapshotWriteEndAspect, m);
+				}
+			}
+
+			CDCReadStartedAspect cdcReadStartedAspect;
+			void mockCDCReadStartedAspect(Class classType, BiConsumer consumer) {
+				cdcReadStartedAspect = mock();
+				when(milestoneAspectTask.hasSnapshot()).thenReturn(true);
+				if (classType.getName().equals(CDCReadStartedAspect.class.getName())) {
+					consumer.accept("nodeId", cdcReadStartedAspect);
+				}
+			}
+
+			CDCReadErrorAspect cdcReadErrorAspect;
+			void mockCDCReadErrorAspect(Class classType, BiConsumer consumer) {
+				cdcReadStartedAspect = mock();
+				when(m.getEnd()).thenReturn(1L);
+				when(milestoneAspectTask.hasSnapshot()).thenReturn(true);
+				if (classType.getName().equals(CDCReadErrorAspect.class.getName())) {
+					consumer.accept("nodeId", cdcReadErrorAspect);
+				}
+			}
+
+			CDCWriteBeginAspect cdcWriteBeginAspect;
+			void mockCDCWriteBeginAspect(Class classType, BiConsumer consumer) {
+				cdcWriteBeginAspect = mock();
+				when(milestoneAspectTask.hasSnapshot()).thenReturn(true);
+				if (classType.getName().equals(CDCWriteBeginAspect.class.getName())) {
+					consumer.accept("nodeId", cdcWriteBeginAspect);
+				}
+			}
+
+			WriteErrorAspect writeErrorAspect;
+			void mockWriteErrorAspect(Class classType, BiConsumer consumer) {
+				writeErrorAspect = mock();
+				when(m.getEnd()).thenReturn(1L);
+				if (classType.getName().equals(WriteErrorAspect.class.getName())) {
+					consumer.accept("nodeId", writeErrorAspect);
+				}
+			}
+
+			@Test
+			void testNormal() {
+				Assertions.assertDoesNotThrow(() -> milestoneAspectTask.init());
+			}
+
+			@Test
+			void testCDCReadBeginAspectNotHasSnapshot() {
+				when(milestoneAspectTask.hasSnapshot()).thenReturn(false);
+				Assertions.assertDoesNotThrow(() -> milestoneAspectTask.init());
+			}
+			@Test
+			void testCDCReadErrorAspectGetEndIsNull() {
+				when(m.getEnd()).thenReturn(null);
+				Assertions.assertDoesNotThrow(() -> milestoneAspectTask.init());
+			}
+			@Test
+			void testSnapshotWriteEndAspect() {
+				snapshotTableProgress.set(-100);
+				Assertions.assertDoesNotThrow(() -> milestoneAspectTask.init());
+			}
+
+			@Test
+			void testSnapshotWriteEndAspectV2() {
+				snapshotTableProgress.set(100);
+				Assertions.assertDoesNotThrow(() -> milestoneAspectTask.init());
+			}
+
+			@Test
+			void testCDCWriteBeginAspect() {
+				doAnswer(a -> {
+					Class classType = a.getArgument(0, Class.class);
+					BiConsumer consumer = a.getArgument(1, BiConsumer.class);
+					mockCDCReadStartedAspect(classType, consumer);
+					mockCDCReadErrorAspect(classType, consumer);
+					cdcWriteBeginAspect = mock();
+					when(milestoneAspectTask.hasSnapshot()).thenReturn(false);
+					if (classType.getName().equals(CDCWriteBeginAspect.class.getName())) {
+						consumer.accept("nodeId", cdcWriteBeginAspect);
+					}
+					mockWriteErrorAspect(classType, consumer);
+					return null;
+				}).when(milestoneAspectTask).nodeRegister(any(Class.class), any(BiConsumer.class));
+				Assertions.assertDoesNotThrow(() -> milestoneAspectTask.init());
+			}
+
+			@Test
+			void testWriteErrorAspect() {
+				doAnswer(a -> {
+					Class classType = a.getArgument(0, Class.class);
+					BiConsumer consumer = a.getArgument(1, BiConsumer.class);
+					mockCDCReadStartedAspect(classType, consumer);
+					mockCDCReadErrorAspect(classType, consumer);
+					mockCDCWriteBeginAspect(classType, consumer);
+					writeErrorAspect = mock();
+					when(m.getEnd()).thenReturn(null);
+					if (classType.getName().equals(WriteErrorAspect.class.getName())) {
+						consumer.accept("nodeId", writeErrorAspect);
+					}
+					return null;
+				}).when(milestoneAspectTask).nodeRegister(any(Class.class), any(BiConsumer.class));
+				Assertions.assertDoesNotThrow(() -> milestoneAspectTask.init());
+			}
+		}
+
+		@Nested
+		class OnStartTest {
+			TaskStartAspect startAspect;
+			ObjectId id;
+			DAG dag;
+			List<Node> nodes ;
+			Node node;
+			@BeforeEach
+			void init() {
+				node = mock(Node.class);
+				when(node.isDataNode()).thenReturn(true);
+				when(node.getId()).thenReturn("id");
+				nodes = new ArrayList<>();
+				nodes.add(node);
+				startAspect = mock(TaskStartAspect.class);
+				id = mock(ObjectId.class);
+				dag = mock(DAG.class);
+				when(task.getId()).thenReturn(id);
+				when(id.toHexString()).thenReturn("id");
+				when(task.getName()).thenReturn("name");
+				when(task.getSyncType()).thenReturn("syncType");
+				doNothing().when(log).info(anyString(), anyString(), anyString());
+
+				when(milestoneAspectTask.hasSnapshot()).thenReturn(true);
+				when(milestoneAspectTask.hasCdc()).thenReturn(true);
+				when(task.getDag()).thenReturn(dag);
+				when(dag.getNodes()).thenReturn(nodes);
+				when(executorService.scheduleWithFixedDelay(any(Runnable.class), anyLong(), anyLong(), any(TimeUnit.class))).thenReturn(mock());
+
+				doCallRealMethod().when(milestoneAspectTask).onStart(startAspect);
+			}
+
+			void verifyAssert(int KPI_TABLE_INITTimes, int KPI_SNAPSHOT, int KPI_CDC, int getId) {
+				try(MockedStatic<BeanUtil> bu = mockStatic(BeanUtil.class)) {
+					bu.when(() -> BeanUtil.getBean(ClientMongoOperator.class)).thenReturn(clientMongoOperator);
+					milestoneAspectTask.onStart(startAspect);
+				}
+				verify(task, times(1)).getId();
+				verify(id, times(1)).toHexString();
+				verify(task, times(1)).getName();
+				verify(log).info(anyString(), anyString(), anyString());
+				verify(milestoneAspectTask).taskMilestone(anyString(), any(Consumer.class));
+				verify(task).getSyncType();
+				verify(milestoneAspectTask, times(KPI_TABLE_INITTimes)).taskMilestone(MilestoneAspectTask.KPI_TABLE_INIT, null);
+				verify(milestoneAspectTask).taskMilestone(MilestoneAspectTask.KPI_DATA_NODE_INIT, null);
+				verify(milestoneAspectTask).hasSnapshot();
+				verify(milestoneAspectTask, times(KPI_SNAPSHOT)).taskMilestone(MilestoneAspectTask.KPI_SNAPSHOT, null);
+				verify(milestoneAspectTask).hasCdc();
+				verify(milestoneAspectTask, times(KPI_CDC)).taskMilestone(MilestoneAspectTask.KPI_CDC, null);
+				verify(task).getDag();
+				verify(node).isDataNode();
+				verify(node, times(getId)).getId();
+				verify(executorService).scheduleWithFixedDelay(any(Runnable.class), anyLong(), anyLong(), any(TimeUnit.class));
+			}
+			@Test
+			void testNormal() {
+				verifyAssert(1, 1, 1, 1);
+			}
+			@Test
+			void testSYNC_TYPE_LOG_COLLECTOR() {
+				when(task.getSyncType()).thenReturn(TaskDto.SYNC_TYPE_LOG_COLLECTOR);
+				verifyAssert(0, 1, 1, 1);
+			}
+			@Test
+			void testNotHasSnapshot() {
+				when(milestoneAspectTask.hasSnapshot()).thenReturn(false);
+				verifyAssert(1, 0, 1, 1);
+			}
+			@Test
+			void testNotHasCdc() {
+				when(milestoneAspectTask.hasCdc()).thenReturn(false);
+				verifyAssert(1, 1, 0, 1);
+			}
+			@Test
+			void testNotDataNode() {
+				when(node.isDataNode()).thenReturn(false);
+				verifyAssert(1, 1, 1, 0);
+			}
+		}
+
+		@Nested
+		class OnStopTest {
+			TaskStopAspect stopAspect;
+			ObjectId id;
+			@BeforeEach
+			void init() {
+				stopAspect = mock(TaskStopAspect.class);
+				id = mock(ObjectId.class);
+				when(task.getId()).thenReturn(id);
+				when(id.toHexString()).thenReturn("id");
+				when(task.getName()).thenReturn("name");
+				doNothing().when(log).info(anyString(), anyString(), anyString());
+				doNothing().when(executorService).shutdown();
+				doNothing().when(milestoneAspectTask).storeMilestone();
+				doCallRealMethod().when(milestoneAspectTask).onStop(stopAspect);
+			}
+			@Test
+			void testNormal() {
+				Assertions.assertDoesNotThrow(() -> milestoneAspectTask.onStop(stopAspect));
+				verify(task).getId();
+				verify(id).toHexString();
+				verify(task).getName();
+				verify(log).info(anyString(), anyString(), anyString());
+				verify(executorService).shutdown();
+				verify(milestoneAspectTask).storeMilestone();
+			}
+		}
+
+		@Nested
+		class HandleDataNodeInitTest {
+			DataNodeInitAspect aspect;
+			DataProcessorContext dataProcessorContext;
+			Node node;
+			List<? extends Node> predecessors;
+			@BeforeEach
+			void init() {
+				predecessors = mock(List.class);
+				node = mock(Node.class);
+				aspect = mock(DataNodeInitAspect.class);
+				dataProcessorContext = mock(DataProcessorContext.class);
+				when(aspect.getDataProcessorContext()).thenReturn(dataProcessorContext);
+				when(milestoneAspectTask.nodeId(dataProcessorContext)).thenReturn("nodeId");
+				doNothing().when(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				when(dataProcessorContext.getNode()).thenReturn(node);
+				when(node.predecessors()).thenReturn(predecessors);
+				when(predecessors.isEmpty()).thenReturn(false);
+				doNothing().when(tm).setTotals(anyLong());
+				doAnswer(a -> {
+					Consumer argument = a.getArgument(1, Consumer.class);
+					argument.accept(tm);
+					return null;
+				}).when(milestoneAspectTask).taskMilestone(anyString(), any(Consumer.class));
+
+				when(milestoneAspectTask.handleDataNodeInit(aspect)).thenCallRealMethod();
+			}
+
+			@Test
+			void testNormal() {
+				Assertions.assertNull(milestoneAspectTask.handleDataNodeInit(aspect));
+				verify(aspect).getDataProcessorContext();
+				verify(milestoneAspectTask).nodeId(dataProcessorContext);
+				verify(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				verify(dataProcessorContext).getNode();
+				verify(node).predecessors();
+				verify(predecessors).isEmpty();
+				verify(milestoneAspectTask, times(1)).taskMilestone(anyString(), any(Consumer.class));
+				verify(tm, times(0)).setTotals(anyLong());
+			}
+
+			@Test
+			void testPredecessorsIsNull() {
+				when(node.predecessors()).thenReturn(null);
+				Assertions.assertNull(milestoneAspectTask.handleDataNodeInit(aspect));
+				verify(aspect).getDataProcessorContext();
+				verify(milestoneAspectTask).nodeId(dataProcessorContext);
+				verify(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				verify(dataProcessorContext).getNode();
+				verify(node).predecessors();
+				verify(predecessors, times(0)).isEmpty();
+				verify(milestoneAspectTask, times(2)).taskMilestone(anyString(), any(Consumer.class));
+				verify(tm, times(1)).setTotals(anyLong());
+			}
+
+			@Test
+			void testPredecessorsIsEmpty() {
+				when(predecessors.isEmpty()).thenReturn(true);
+				Assertions.assertNull(milestoneAspectTask.handleDataNodeInit(aspect));
+				verify(aspect).getDataProcessorContext();
+				verify(milestoneAspectTask).nodeId(dataProcessorContext);
+				verify(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				verify(dataProcessorContext).getNode();
+				verify(node).predecessors();
+				verify(predecessors).isEmpty();
+				verify(milestoneAspectTask, times(2)).taskMilestone(anyString(), any(Consumer.class));
+				verify(tm, times(1)).setTotals(anyLong());
+			}
+
+			@Test
+			void testTableNode() {
+				node = mock(TableNode.class);
+				when(node.predecessors()).thenReturn(predecessors);
+				when(dataProcessorContext.getNode()).thenReturn(node);
+				Assertions.assertNull(milestoneAspectTask.handleDataNodeInit(aspect));
+				verify(aspect).getDataProcessorContext();
+				verify(milestoneAspectTask).nodeId(dataProcessorContext);
+				verify(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				verify(dataProcessorContext).getNode();
+				verify(node).predecessors();
+				verify(predecessors).isEmpty();
+				verify(milestoneAspectTask, times(1)).taskMilestone(anyString(), any(Consumer.class));
+				verify(tm, times(0)).setTotals(anyLong());
+			}
+
+			@Test
+			void testDatabaseNode() {
+				DatabaseNode databaseNode = mock(DatabaseNode.class);
+				when(databaseNode.tableSize()).thenReturn(1);
+				node = databaseNode;
+				when(node.predecessors()).thenReturn(predecessors);
+				when(dataProcessorContext.getNode()).thenReturn(node);
+				Assertions.assertNull(milestoneAspectTask.handleDataNodeInit(aspect));
+				verify(aspect).getDataProcessorContext();
+				verify(milestoneAspectTask).nodeId(dataProcessorContext);
+				verify(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				verify(dataProcessorContext).getNode();
+				verify(node).predecessors();
+				verify(node, times(0)).tableSize();
+				verify(predecessors).isEmpty();
+				verify(milestoneAspectTask, times(1)).taskMilestone(anyString(), any(Consumer.class));
+				verify(tm, times(0)).setTotals(anyLong());
+			}
+		}
+
+		@Nested
+		class HandleDataNodeCloseTest {
+			DataNodeCloseAspect aspect;
+			DataProcessorContext dataProcessorContext;
+			@BeforeEach
+			void init() {
+				aspect = mock(DataNodeCloseAspect.class);
+				dataProcessorContext = mock(DataProcessorContext.class);
+				when(aspect.getDataProcessorContext()).thenReturn(dataProcessorContext);
+				when(milestoneAspectTask.nodeId(dataProcessorContext)).thenReturn("nodeId");
+				doNothing().when(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+
+				when(milestoneAspectTask.handleDataNodeClose(aspect)).thenCallRealMethod();
+			}
+			@Test
+			void testNormal() {
+				Assertions.assertNull(milestoneAspectTask.handleDataNodeClose(aspect));
+				verify(aspect).getDataProcessorContext();
+				verify(milestoneAspectTask).nodeId(dataProcessorContext);
+				verify(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+			}
+		}
+
+		@Nested
+		class HandleProcessNodeInitTest {
+			ProcessorNodeInitAspect aspect;
+			ProcessorBaseContext processorBaseContext;
+			@BeforeEach
+			void init() {
+				aspect = mock(ProcessorNodeInitAspect.class);
+				processorBaseContext = mock(ProcessorBaseContext.class);
+				when(aspect.getProcessorBaseContext()).thenReturn(processorBaseContext);
+				when(milestoneAspectTask.nodeId(processorBaseContext)).thenReturn("nodeId");
+				doNothing().when(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				when(milestoneAspectTask.handleProcessNodeInit(aspect)).thenCallRealMethod();
+			}
+			@Test
+			void testNormal() {
+				Assertions.assertNull(milestoneAspectTask.handleProcessNodeInit(aspect));
+				verify(aspect).getProcessorBaseContext();
+				verify(milestoneAspectTask).nodeId(processorBaseContext);
+				verify(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+			}
+		}
+
+		@Nested
+		class HandleProcessNodeCloseTest {
+			ProcessorNodeCloseAspect aspect;
+			ProcessorBaseContext processorBaseContext;
+			@BeforeEach
+			void init() {
+				aspect = mock(ProcessorNodeCloseAspect.class);
+				processorBaseContext = mock(ProcessorBaseContext.class);
+				when(aspect.getProcessorBaseContext()).thenReturn(processorBaseContext);
+				when(milestoneAspectTask.nodeId(processorBaseContext)).thenReturn("nodeId");
+				doNothing().when(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				when(milestoneAspectTask.handleProcessNodeClose(aspect)).thenCallRealMethod();
+			}
+			@Test
+			void testNormal() {
+				Assertions.assertNull(milestoneAspectTask.handleProcessNodeClose(aspect));
+				verify(aspect).getProcessorBaseContext();
+				verify(milestoneAspectTask).nodeId(processorBaseContext);
+				verify(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+			}
+		}
+
+		@Nested
+		class HandleTableInitTest {
+			PDKNodeInitAspect aspect;
+			DataProcessorContext processorBaseContext;
+			@BeforeEach
+			void init() {
+				aspect = mock(PDKNodeInitAspect.class);
+				processorBaseContext = mock(DataProcessorContext.class);
+				when(aspect.getDataProcessorContext()).thenReturn(processorBaseContext);
+				when(milestoneAspectTask.nodeId(processorBaseContext)).thenReturn("nodeId");
+				doNothing().when(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				when(milestoneAspectTask.handlePDKNodeInit(aspect)).thenCallRealMethod();
+			}
+
+			@Test
+			void testNormal() {
+				Assertions.assertNull(milestoneAspectTask.handlePDKNodeInit(aspect));
+				verify(aspect).getDataProcessorContext();
+				verify(milestoneAspectTask).nodeId(processorBaseContext);
+				verify(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+			}
+		}
+
+		@Nested
+		class HandleTableInit {
+			TableInitFuncAspect aspect;
+			DataProcessorContext processorBaseContext;
+			@BeforeEach
+			void init() {
+				aspect = mock(TableInitFuncAspect.class);
+				processorBaseContext = mock(DataProcessorContext.class);
+				when(aspect.getDataProcessorContext()).thenReturn(processorBaseContext);
+				when(milestoneAspectTask.nodeId(processorBaseContext)).thenReturn("nodeId");
+				when(aspect.getState()).thenReturn(STATE_START);
+
+				when(milestoneAspectTask.handleTableInit(aspect)).thenCallRealMethod();
+			}
+			@Test
+			void testSTATE_START() {
+				when(aspect.getTotals()).thenReturn(1L);
+				doNothing().when(milestoneAspectTask).setRunning(m);
+				doAnswer(a -> {
+					Consumer argument = a.getArgument(2, Consumer.class);
+					argument.accept(m);
+					return null;
+				}).when(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				Assertions.assertNull(milestoneAspectTask.handleTableInit(aspect));
+				verify(aspect).getDataProcessorContext();
+				verify(milestoneAspectTask).nodeId(processorBaseContext);
+				verify(aspect).getState();
+				verify(m).setProgress(0L);
+				verify(aspect).getTotals();
+				verify(m).setTotals(1L);
+				verify(milestoneAspectTask).setRunning(m);
+			}
+
+			@Test
+			void testSTATE_PROCESS() {
+				when(aspect.getState()).thenReturn(STATE_PROCESS);
+				when(aspect.getTotals()).thenReturn(1L);
+				when(aspect.getCompletedCounts()).thenReturn(1L);
+				doAnswer(a -> {
+					Consumer argument = a.getArgument(2, Consumer.class);
+					argument.accept(m);
+					return null;
+				}).when(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				Assertions.assertNull(milestoneAspectTask.handleTableInit(aspect));
+				verify(aspect).getDataProcessorContext();
+				verify(milestoneAspectTask).nodeId(processorBaseContext);
+				verify(aspect).getState();
+				verify(m).setProgress(1L);
+				verify(aspect, times(1)).getCompletedCounts();
+				verify(aspect).getTotals();
+				verify(m).setTotals(1L);
+			}
+
+			@Test
+			void testSTATE_ENDWithThrowable() {
+				when(aspect.getState()).thenReturn(STATE_END);
+				when(aspect.getThrowable()).thenReturn(null);
+
+				when(aspect.getTotals()).thenReturn(1L);
+				doNothing().when(milestoneAspectTask).setFinish(m);
+				doAnswer(a -> {
+					Consumer argument = a.getArgument(2, Consumer.class);
+					argument.accept(m);
+					return null;
+				}).when(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				Assertions.assertNull(milestoneAspectTask.handleTableInit(aspect));
+				verify(aspect).getDataProcessorContext();
+				verify(milestoneAspectTask).nodeId(processorBaseContext);
+				verify(aspect).getState();
+				verify(m, times(1)).setProgress(1L);
+				verify(aspect, times(2)).getTotals();
+				verify(m, times(1)).setTotals(1L);
+				verify(milestoneAspectTask, times(1)).setFinish(m);
+			}
+			@Test
+			void testSTATE_ENDWithoutThrowable() {
+				when(aspect.getState()).thenReturn(STATE_END);
+				Throwable error = mock(Throwable.class);
+				when(error.getMessage()).thenReturn("message");
+				when(aspect.getThrowable()).thenReturn(error);
+
+				when(aspect.getTotals()).thenReturn(1L);
+			    when(milestoneAspectTask.getErrorConsumer("message")).thenReturn(mock());
+				doAnswer(a -> {
+					Consumer argument = a.getArgument(2, Consumer.class);
+					argument.accept(m);
+					return null;
+				}).when(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				Assertions.assertNull(milestoneAspectTask.handleTableInit(aspect));
+				verify(aspect).getDataProcessorContext();
+				verify(aspect, times(1)).getThrowable();
+				verify(error, times(1)).getMessage();
+				verify(milestoneAspectTask).nodeId(processorBaseContext);
+				verify(milestoneAspectTask, times(1)).getErrorConsumer("message");
+			}
+
+			@Test
+			void testOther() {
+				when(aspect.getState()).thenReturn(1010);
+				Assertions.assertNull(milestoneAspectTask.handleTableInit(aspect));
+				verify(aspect).getDataProcessorContext();
+				verify(milestoneAspectTask).nodeId(processorBaseContext);
+				verify(aspect).getState();
+			}
+		}
+
+		@Nested
+		class StoreMilestoneWhenTargetNodeTableInitTest {
+			MilestoneEntity milestone;
+			AtomicLong totals, completed;
+			@BeforeEach
+			void init() {
+				milestone = new MilestoneEntity();
+				milestone.setBegin(0L);
+				totals = new AtomicLong(0L);
+				completed = new AtomicLong(0L);
+				when(m.getBegin()).thenReturn(1L);
+				when(m.getEnd()).thenReturn(1L);
+				when(m.getErrorMessage()).thenReturn("error");
+				when(m.getTotals()).thenReturn(1L);
+				when(m.getProgress()).thenReturn(1L);
+				doAnswer(a -> {
+					Consumer<MilestoneEntity> argument = (Consumer<MilestoneEntity>)a.getArgument(2, Consumer.class);
+					argument.accept(m);
+					return null;
+				}).when(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				doCallRealMethod().when(milestoneAspectTask).storeMilestoneWhenTargetNodeTableInit(milestone, "nodeId", totals, completed);
+			}
+			@Test
+			void testNormal() {
+				milestoneAspectTask.storeMilestoneWhenTargetNodeTableInit(milestone, "nodeId", totals, completed);
+				Assertions.assertEquals(1L, totals.get());
+				Assertions.assertEquals(1L, completed.get());
+				Assertions.assertEquals(1L, milestone.getTotals());
+				Assertions.assertEquals(1L, milestone.getProgress());
+				Assertions.assertEquals(MilestoneStatus.ERROR, milestone.getStatus());
+				Assertions.assertEquals("error", milestone.getErrorMessage());
+			}
+			@Test
+			void testMGetEndIsNull() {
+				when(m.getEnd()).thenReturn(null);
+				milestoneAspectTask.storeMilestoneWhenTargetNodeTableInit(milestone, "nodeId", totals, completed);
+				Assertions.assertNull(milestone.getEnd());
+				Assertions.assertEquals(1L, totals.get());
+				Assertions.assertEquals(1L, completed.get());
+				Assertions.assertEquals(1L, milestone.getTotals());
+				Assertions.assertEquals(1L, milestone.getProgress());
+				Assertions.assertEquals(MilestoneStatus.ERROR, milestone.getStatus());
+				Assertions.assertEquals("error", milestone.getErrorMessage());
+			}
+			@Test
+			void testEntityGetEndIsNotNull() {
+				milestone.setEnd(0L);
+				milestoneAspectTask.storeMilestoneWhenTargetNodeTableInit(milestone, "nodeId", totals, completed);
+				Assertions.assertNotNull(milestone.getEnd());
+				Assertions.assertEquals(1L, totals.get());
+				Assertions.assertEquals(1L, completed.get());
+				Assertions.assertEquals(1L, milestone.getTotals());
+				Assertions.assertEquals(1L, milestone.getProgress());
+				Assertions.assertEquals(MilestoneStatus.ERROR, milestone.getStatus());
+				Assertions.assertEquals("error", milestone.getErrorMessage());
+			}
+			@Test
+			void testErrorMessageIsNull() {
+				when(m.getErrorMessage()).thenReturn(null);
+				milestone.setEnd(0L);
+				milestoneAspectTask.storeMilestoneWhenTargetNodeTableInit(milestone, "nodeId", totals, completed);
+				Assertions.assertNotNull(milestone.getEnd());
+				Assertions.assertEquals(1L, totals.get());
+				Assertions.assertEquals(1L, completed.get());
+				Assertions.assertEquals(1L, milestone.getTotals());
+				Assertions.assertEquals(1L, milestone.getProgress());
+				Assertions.assertNotEquals(MilestoneStatus.ERROR, milestone.getStatus());
+				Assertions.assertNotEquals("error", milestone.getErrorMessage());
+			}
+		}
+
+		@Nested
+		class StoreMilestoneWhenSyncTypeIsLogCollectorTest {
+			@BeforeEach
+			void init() {
+				doNothing().when(m).setStatus(MilestoneStatus.WAITING);
+				targetNodes.add("nodeId");
+				doNothing().when(m).setBegin(anyLong());
+				doNothing().when(m).setEnd(0L);
+				doNothing().when(m).setStatus(MilestoneStatus.RUNNING);
+				doNothing().when(milestoneAspectTask).storeMilestoneWhenTargetNodeTableInit(any(MilestoneEntity.class), anyString(), any(AtomicLong.class), any(AtomicLong.class));
+				when(m.getStatus()).thenReturn(MilestoneStatus.WAITING);
+				doNothing().when(m).setStatus(MilestoneStatus.FINISH);
+				doAnswer(a -> {
+					Consumer<MilestoneEntity> argument = (Consumer<MilestoneEntity>)a.getArgument(1, Consumer.class);
+					argument.accept(m);
+					return null;
+				}).when(milestoneAspectTask).taskMilestone(anyString(), any(Consumer.class));
+				doCallRealMethod().when(milestoneAspectTask).storeMilestoneWhenSyncTypeIsLogCollector();
+			}
+			@Test
+			void testNormal() {
+				milestoneAspectTask.storeMilestoneWhenSyncTypeIsLogCollector();
+				verify(m).setStatus(MilestoneStatus.WAITING);
+				verify(m).setBegin(anyLong());
+				verify(m).setEnd(0L);
+				verify(m).setStatus(MilestoneStatus.RUNNING);
+				verify(milestoneAspectTask).storeMilestoneWhenTargetNodeTableInit(any(MilestoneEntity.class), anyString(), any(AtomicLong.class), any(AtomicLong.class));
+				verify(m).getStatus();
+				verify(m).setStatus(MilestoneStatus.FINISH);
+				verify(milestoneAspectTask).taskMilestone(anyString(), any(Consumer.class));
+			}
+			@Test
+			void testLongValueNotEquals() {
+				doAnswer(a -> {
+					AtomicLong totals = a.getArgument(2, AtomicLong.class);
+					AtomicLong completed = a.getArgument(3, AtomicLong.class);
+					totals.set(100L);
+					completed.set(200L);
+					return null;
+				}).when(milestoneAspectTask).storeMilestoneWhenTargetNodeTableInit(any(MilestoneEntity.class), anyString(), any(AtomicLong.class), any(AtomicLong.class));
+
+				milestoneAspectTask.storeMilestoneWhenSyncTypeIsLogCollector();
+				verify(m).setStatus(MilestoneStatus.WAITING);
+				verify(m).setBegin(anyLong());
+				verify(m).setEnd(0L);
+				verify(m).setStatus(MilestoneStatus.RUNNING);
+				verify(milestoneAspectTask).storeMilestoneWhenTargetNodeTableInit(any(MilestoneEntity.class), anyString(), any(AtomicLong.class), any(AtomicLong.class));
+				verify(m).getStatus();
+				verify(m, times(0)).setStatus(MilestoneStatus.FINISH);
+				verify(milestoneAspectTask).taskMilestone(anyString(), any(Consumer.class));
+			}
+			@Test
+			void testStatusIsERROR() {
+				when(m.getStatus()).thenReturn(MilestoneStatus.ERROR);
+				milestoneAspectTask.storeMilestoneWhenSyncTypeIsLogCollector();
+				verify(m).setStatus(MilestoneStatus.WAITING);
+				verify(m).setBegin(anyLong());
+				verify(m).setEnd(0L);
+				verify(m).setStatus(MilestoneStatus.RUNNING);
+				verify(milestoneAspectTask).storeMilestoneWhenTargetNodeTableInit(any(MilestoneEntity.class), anyString(), any(AtomicLong.class), any(AtomicLong.class));
+				verify(m).getStatus();
+				verify(m, times(0)).setStatus(MilestoneStatus.FINISH);
+				verify(milestoneAspectTask).taskMilestone(anyString(), any(Consumer.class));
+			}
+			@Test
+			void testNodeSetIsEmpty() {
+				targetNodes.clear();
+				milestoneAspectTask.storeMilestoneWhenSyncTypeIsLogCollector();
+				verify(m).setStatus(MilestoneStatus.WAITING);
+				verify(m, times(0)).setBegin(anyLong());
+				verify(m, times(0)).setEnd(0L);
+				verify(m, times(0)).setStatus(MilestoneStatus.RUNNING);
+				verify(milestoneAspectTask, times(0)).storeMilestoneWhenTargetNodeTableInit(any(MilestoneEntity.class), anyString(), any(AtomicLong.class), any(AtomicLong.class));
+				verify(m, times(0)).getStatus();
+				verify(m, times(0)).setStatus(MilestoneStatus.FINISH);
+				verify(milestoneAspectTask, times(1)).taskMilestone(anyString(), any(Consumer.class));
+			}
+
+		}
+
+		@Nested
+		class StoreMilestoneDataNodeInitTest {
+			MilestoneEntity milestoneEntity;
+			@BeforeEach
+			void init() {
+				milestoneEntity = new MilestoneEntity();
+				milestoneEntity.setStatus(MilestoneStatus.WAITING);
+				dataNodeInitMap.put("1", MilestoneStatus.WAITING);
+				dataNodeInitMap.put("2", MilestoneStatus.FINISH);
+				doNothing().when(milestoneAspectTask).setFinish(milestoneEntity);
+				doCallRealMethod().when(milestoneAspectTask).storeMilestoneDataNodeInit(milestoneEntity);
+			}
+			@Test
+			void testNormal() {
+				milestoneAspectTask.storeMilestoneDataNodeInit(milestoneEntity);
+				Assertions.assertEquals(2L, milestoneEntity.getTotals());
+				Assertions.assertEquals(1L, milestoneEntity.getProgress());
+				Assertions.assertEquals(MilestoneStatus.RUNNING, milestoneEntity.getStatus());
+				Assertions.assertNotNull(milestoneEntity.getBegin());
+			}
+			@Test
+			void testFINISH() {
+				milestoneEntity.setStatus(MilestoneStatus.FINISH);
+				milestoneAspectTask.storeMilestoneDataNodeInit(milestoneEntity);
+				Assertions.assertNull(milestoneEntity.getTotals());
+				Assertions.assertNull(milestoneEntity.getProgress());
+				Assertions.assertNull(milestoneEntity.getBegin());
+			}
+			@Test
+			void testDataNodeInitMapIsEmpty() {
+				dataNodeInitMap.clear();
+				milestoneAspectTask.storeMilestoneDataNodeInit(milestoneEntity);
+				Assertions.assertEquals(0L, milestoneEntity.getTotals());
+				Assertions.assertEquals(0L, milestoneEntity.getProgress());
+				Assertions.assertNull(milestoneEntity.getBegin());
+			}
+			@Test
+			void testTotalEqualsProgress() {
+				milestoneEntity.setBegin(0L);
+				dataNodeInitMap.clear();
+				dataNodeInitMap.put("1", MilestoneStatus.FINISH);
+				milestoneAspectTask.storeMilestoneDataNodeInit(milestoneEntity);
+				Assertions.assertEquals(1L, milestoneEntity.getTotals());
+				Assertions.assertEquals(1L, milestoneEntity.getProgress());
+				Assertions.assertNotNull(milestoneEntity.getBegin());
+			}
+			@Test
+			void testProgressIsZero() {
+				milestoneEntity.setBegin(0L);
+				dataNodeInitMap.clear();
+				dataNodeInitMap.put("1", MilestoneStatus.WAITING);
+				milestoneAspectTask.storeMilestoneDataNodeInit(milestoneEntity);
+				Assertions.assertEquals(1L, milestoneEntity.getTotals());
+				Assertions.assertEquals(0L, milestoneEntity.getProgress());
+				Assertions.assertEquals(MilestoneStatus.WAITING, milestoneEntity.getStatus());
+				Assertions.assertNotNull(milestoneEntity.getBegin());
+			}
+		}
+
+		@Nested
+		class StoreMilestoneTest {
+			@BeforeEach
+			void init() {
+				when(task.getSyncType()).thenReturn(TaskDto.SYNC_TYPE_LOG_COLLECTOR);
+				when(milestoneAspectTask.getTaskSyncStatus()).thenReturn(MilestoneAspectTask.KPI_DATA_NODE_INIT);
+				doNothing().when(milestoneAspectTask).storeMilestoneWhenSyncTypeIsLogCollector();
+				doNothing().when(milestoneAspectTask).taskMilestone(anyString(), any(Consumer.class));
+				when(clientMongoOperator.update(any(Query.class), any(Update.class), anyString())).thenReturn(mock(UpdateResult.class));
+				when(task.getId()).thenReturn(new ObjectId());
+				doNothing().when(log).warn(anyString(), anyString(), any(Exception.class));
+				doCallRealMethod().when(milestoneAspectTask).storeMilestone();
+			}
+			@Test
+			void testNormal() {
+				Assertions.assertDoesNotThrow(milestoneAspectTask::storeMilestone);
+				verify(task, times(1)).getSyncType();
+				verify(milestoneAspectTask, times(1)).getTaskSyncStatus();
+				verify(milestoneAspectTask, times(0)).storeMilestoneWhenSyncTypeIsLogCollector();
+				verify(milestoneAspectTask, times(1)).taskMilestone(anyString(), any(Consumer.class));
+				verify(clientMongoOperator, times(1)).update(any(Query.class), any(Update.class), anyString());
+			}
+			@Test
+			void testNotSYNC_TYPE_LOG_COLLECTOR() {
+				when(task.getSyncType()).thenReturn(TaskDto.STATUS_EDIT);
+				Assertions.assertDoesNotThrow(milestoneAspectTask::storeMilestone);
+				verify(task, times(1)).getSyncType();
+				verify(milestoneAspectTask, times(1)).getTaskSyncStatus();
+				verify(milestoneAspectTask, times(1)).storeMilestoneWhenSyncTypeIsLogCollector();
+				verify(milestoneAspectTask, times(1)).taskMilestone(anyString(), any(Consumer.class));
+				verify(clientMongoOperator, times(1)).update(any(Query.class), any(Update.class), anyString());
+			}
+			@Test
+			void testException() {
+				when(clientMongoOperator.update(any(Query.class), any(Update.class), anyString())).thenAnswer(a -> {
+					throw new Exception("Connection timeout");
+				});
+				try(MockedStatic<TmUnavailableException> tue = mockStatic(TmUnavailableException.class)) {
+					tue.when(() -> TmUnavailableException.notInstance(any(Exception.class))).thenReturn(false);
+					Assertions.assertDoesNotThrow(milestoneAspectTask::storeMilestone);
+					verify(milestoneAspectTask, times(1)).getTaskSyncStatus();
+					tue.verify(() -> TmUnavailableException.notInstance(any(Exception.class)), times(1));
+					verify(log, times(0)).warn(anyString(), anyString(), any(Exception.class));
+				}
+			}
+			@Test
+			void testExceptionWithLogWarn() {
+				when(clientMongoOperator.update(any(Query.class), any(Update.class), anyString())).thenAnswer(a -> {
+					throw new Exception("Connection timeout");
+				});
+				try(MockedStatic<TmUnavailableException> tue = mockStatic(TmUnavailableException.class)) {
+					tue.when(() -> TmUnavailableException.notInstance(any(Exception.class))).thenReturn(true);
+					Assertions.assertDoesNotThrow(milestoneAspectTask::storeMilestone);
+					verify(milestoneAspectTask, times(1)).getTaskSyncStatus();
+					tue.verify(() -> TmUnavailableException.notInstance(any(Exception.class)), times(1));
+					verify(log, times(1)).warn(anyString(), anyString(), any(Exception.class));
+				}
+			}
+		}
+
+		@Nested
+		class GetErrorConsumerTest {
+			@Test
+			void testNormal() {
+				when(milestoneAspectTask.getErrorConsumer("error")).thenCallRealMethod();
+				Consumer<MilestoneEntity> error = milestoneAspectTask.getErrorConsumer("error");
+				MilestoneEntity me = new MilestoneEntity();
+				error.accept(me);
+				Assertions.assertNotNull(me.getEnd());
+				Assertions.assertEquals(MilestoneStatus.ERROR, me.getStatus());
+				Assertions.assertEquals("error", me.getErrorMessage());
+			}
+		}
+
+		@Nested
+		class NodeRegisterTest {
+			SnapshotReadBeginAspect aspect;
+			DataProcessorContext dataProcessorContext;
+			BiConsumer<SnapshotReadBeginAspect, MilestoneEntity> consumer;
+			@BeforeEach
+			void init() {
+				consumer = mock(BiConsumer.class);
+				doNothing().when(consumer).accept(aspect, m);
+				aspect = mock(SnapshotReadBeginAspect.class);
+				dataProcessorContext = mock(DataProcessorContext.class);
+				when(milestoneAspectTask.nodeId(dataProcessorContext)).thenReturn("nodeId");
+				when(aspect.getDataProcessorContext()).thenReturn(dataProcessorContext);
+				doAnswer(a -> {
+					Consumer argument = a.getArgument(2, Consumer.class);
+					argument.accept(m);
+					return null;
+				}).when(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+				when(observerHandlers.register(any(Class.class), any(Function.class))).thenAnswer(a -> {
+					Function argument = a.getArgument(1, Function.class);
+					argument.apply(aspect);
+					return observerHandlers;
+				});
+				doCallRealMethod().when(milestoneAspectTask).nodeRegister(SnapshotReadBeginAspect.class, "code", consumer);
+			}
+			@Test
+			void testNormal() {
+				milestoneAspectTask.nodeRegister(SnapshotReadBeginAspect.class, "code", consumer);
+				verify(aspect).getDataProcessorContext();
+				verify(milestoneAspectTask).nodeId(dataProcessorContext);
+				verify(milestoneAspectTask).nodeMilestones(anyString(), anyString(), any(Consumer.class));
+			}
+		}
+
+		@Nested
+		class TaskMilestoneTest {
+			MilestoneEntity entity;
+			Consumer<MilestoneEntity> consumer;
+			@BeforeEach
+			void init() {
+				entity = new MilestoneEntity("code", MilestoneStatus.FINISH);
+				milestones.put("code", entity);
+				consumer = mock(Consumer.class);
+				doNothing().when(consumer).accept(any(MilestoneEntity.class));
+				doCallRealMethod().when(milestoneAspectTask).taskMilestone("code", consumer);
+			}
+			@Test
+			void testNormal() {
+				milestoneAspectTask.taskMilestone("code", consumer);
+				MilestoneEntity code = milestones.get("code");
+				Assertions.assertNotNull(code);
+				Assertions.assertEquals("code", code.getCode());
+				Assertions.assertEquals(MilestoneStatus.FINISH, code.getStatus());
+				verify(consumer).accept(any(MilestoneEntity.class));
+			}
+			@Test
+			void testMilestoneEntityIsNull() {
+				milestones.put("code", null);
+				milestoneAspectTask.taskMilestone("code", consumer);
+				MilestoneEntity code = milestones.get("code");
+				Assertions.assertNotNull(code);
+				Assertions.assertEquals("code", code.getCode());
+				Assertions.assertEquals(MilestoneStatus.WAITING, code.getStatus());
+				verify(consumer).accept(any(MilestoneEntity.class));
+			}
+		}
+
+		@AfterEach
+		void close() {
+			dataNodeInitMap.clear();
+			milestones.clear();
+			nodeMilestones.clear();
+			targetNodes.clear();
+			dataNodeInitMap.clear();
+		}
+	}
+
+	@Nested
+	class getTaskSyncStatusTest {
+		Map<String, MilestoneEntity> milestonesMap;
+		MilestoneEntity task;
+		MilestoneEntity node;
+		MilestoneEntity table;
+		MilestoneEntity snapshot;
+		MilestoneEntity cdc;
+
+		@BeforeEach
+		void init() {
+			milestonesMap = new LinkedHashMap<>();
+			task = new MilestoneEntity(MilestoneAspectTask.KPI_TASK, MilestoneStatus.WAITING);
+			task.setBegin(1L);
+			task.setEnd(2L);
+			node = new MilestoneEntity(MilestoneAspectTask.KPI_DATA_NODE_INIT, MilestoneStatus.WAITING);
+			node.setBegin(3L);
+			node.setEnd(4L);
+			table = new MilestoneEntity(MilestoneAspectTask.KPI_TABLE_INIT, MilestoneStatus.WAITING);
+			table.setBegin(5L);
+			table.setEnd(6L);
+			snapshot = new MilestoneEntity(MilestoneAspectTask.KPI_SNAPSHOT, MilestoneStatus.WAITING);
+			snapshot.setBegin(7L);
+			snapshot.setEnd(8L);
+			cdc = new MilestoneEntity(MilestoneAspectTask.KPI_CDC, MilestoneStatus.WAITING);
+			cdc.setBegin(9L);
+			cdc.setEnd(10L);
+			milestonesMap.put(MilestoneAspectTask.KPI_TASK, task);
+			milestonesMap.put(MilestoneAspectTask.KPI_DATA_NODE_INIT, node);
+			milestonesMap.put(MilestoneAspectTask.KPI_TABLE_INIT, table);
+			milestonesMap.put(MilestoneAspectTask.KPI_SNAPSHOT, snapshot);
+			milestonesMap.put(MilestoneAspectTask.KPI_CDC, cdc);
+
+			ReflectionTestUtils.setField(milestoneAspectTask, "milestones", milestonesMap);
+			when(milestoneAspectTask.getTaskSyncStatus()).thenCallRealMethod();
+		}
+		@Test
+		void testNormal() {
+			Assertions.assertEquals(MilestoneAspectTask.KPI_TASK, milestoneAspectTask.getTaskSyncStatus());
+		}
+		@Test
+		void testMilestonesMapHasNullEntity() {
+			milestonesMap.put("null", null);
+			Assertions.assertEquals(MilestoneAspectTask.KPI_TASK, milestoneAspectTask.getTaskSyncStatus());
+		}
+
+		@Test
+		void testTaskRunning(){
+			task.setStatus(MilestoneStatus.RUNNING);
+			Assertions.assertEquals(MilestoneAspectTask.KPI_TASK, milestoneAspectTask.getTaskSyncStatus());
+		}
+		@Test
+		void testNode() {
+			task.setStatus(MilestoneStatus.FINISH);
+			node.setStatus(MilestoneStatus.RUNNING);
+			Assertions.assertEquals(MilestoneAspectTask.KPI_DATA_NODE_INIT, milestoneAspectTask.getTaskSyncStatus());
+		}
+		@Test
+		void testNodeNullEndTime() {
+			task.setStatus(MilestoneStatus.FINISH);
+			node.setStatus(MilestoneStatus.FINISH);
+			node.setEnd(null);
+			Assertions.assertEquals(MilestoneAspectTask.KPI_TASK, milestoneAspectTask.getTaskSyncStatus());
+		}
+		@Test
+		void testTaskNullEndTime() {
+			task.setStatus(MilestoneStatus.FINISH);
+			node.setStatus(MilestoneStatus.FINISH);
+			task.setEnd(null);
+			String taskSyncStatus = milestoneAspectTask.getTaskSyncStatus();
+			Assertions.assertNotNull(taskSyncStatus);
+			Assertions.assertEquals(MilestoneAspectTask.KPI_DATA_NODE_INIT, taskSyncStatus);
+		}
+
+		@Test
+		void testTable() {
+			task.setStatus(MilestoneStatus.FINISH);
+			node.setStatus(MilestoneStatus.FINISH);
+			table.setStatus(MilestoneStatus.RUNNING);
+			Assertions.assertEquals(MilestoneAspectTask.KPI_TABLE_INIT, milestoneAspectTask.getTaskSyncStatus());
+		}
+		@Test
+		void testSNAPSHOT() {
+			task.setStatus(MilestoneStatus.FINISH);
+			node.setStatus(MilestoneStatus.FINISH);
+			table.setStatus(MilestoneStatus.FINISH);
+			snapshot.setStatus(MilestoneStatus.RUNNING);
+			Assertions.assertEquals(MilestoneAspectTask.KPI_SNAPSHOT, milestoneAspectTask.getTaskSyncStatus());
+		}
+		@Test
+		void testCdcNotEndTime() {
+			task.setStatus(MilestoneStatus.FINISH);
+			node.setStatus(MilestoneStatus.FINISH);
+			table.setStatus(MilestoneStatus.FINISH);
+			snapshot.setStatus(MilestoneStatus.FINISH);
+			cdc.setStatus(MilestoneStatus.FINISH);
+			Assertions.assertEquals(MilestoneAspectTask.KPI_CDC, milestoneAspectTask.getTaskSyncStatus());
+		}
+		@Test
+		void testCdcEqualsEndTime() {
+			task.setStatus(MilestoneStatus.FINISH);
+			node.setStatus(MilestoneStatus.FINISH);
+			table.setStatus(MilestoneStatus.FINISH);
+			snapshot.setStatus(MilestoneStatus.FINISH);
+			cdc.setStatus(MilestoneStatus.FINISH);
+
+			cdc.setEnd(100L);
+			snapshot.setEnd(100L);
+
+			Assertions.assertEquals(MilestoneAspectTask.KPI_CDC, milestoneAspectTask.getTaskSyncStatus());
+		}
+
+		@Test
+		void testCDC() {
+			task.setStatus(MilestoneStatus.FINISH);
+			node.setStatus(MilestoneStatus.FINISH);
+			table.setStatus(MilestoneStatus.FINISH);
+			snapshot.setStatus(MilestoneStatus.FINISH);
+			cdc.setStatus(MilestoneStatus.RUNNING);
+			Assertions.assertEquals(MilestoneAspectTask.KPI_CDC, milestoneAspectTask.getTaskSyncStatus());
+		}
+		@Test
+		void testCDCFINISH() {
+			task.setStatus(MilestoneStatus.FINISH);
+			node.setStatus(MilestoneStatus.FINISH);
+			table.setStatus(MilestoneStatus.FINISH);
+			snapshot.setStatus(MilestoneStatus.FINISH);
+			cdc.setStatus(MilestoneStatus.FINISH);
+			Assertions.assertEquals(MilestoneAspectTask.KPI_CDC, milestoneAspectTask.getTaskSyncStatus());
+		}
+	}
+
 }
diff --git a/manager/tm-common/src/test/java/com/tapdata/tm/utils/TimeTransFormationUtilTest.java b/manager/tm-common/src/test/java/com/tapdata/tm/utils/TimeTransFormationUtilTest.java
new file mode 100644
index 0000000000..c57bf6cf31
--- /dev/null
+++ b/manager/tm-common/src/test/java/com/tapdata/tm/utils/TimeTransFormationUtilTest.java
@@ -0,0 +1,172 @@
+package com.tapdata.tm.utils;
+
+import io.tapdata.pdk.apis.entity.QueryOperator;
+import io.tapdata.pdk.apis.entity.TapTimeForm;
+import io.tapdata.pdk.apis.entity.TapTimeUnit;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+public class TimeTransFormationUtilTest {
+    @Nested
+    class calculatedTimeRange{
+        LocalDateTime currentDateTime = TimeTransFormationUtil.formatDateTime("2024-04-25 19:20:00");
+        @DisplayName("Tested for the past hour")
+        @Test
+        void test(){
+            QueryOperator queryOperator = new QueryOperator();
+            queryOperator.setUnit(TapTimeUnit.HOUR);
+            queryOperator.setForm(TapTimeForm.BEFORE);
+            queryOperator.setNumber(1L);
+            List<String> result = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,null);
+            Assertions.assertEquals("2024-04-25 18:00:00",result.get(0));
+            Assertions.assertEquals("2024-04-25 18:59:59",result.get(1));
+        }
+
+        @DisplayName("Test one day ago")
+        @Test
+        void test1(){
+            QueryOperator queryOperator = new QueryOperator();
+            queryOperator.setUnit(TapTimeUnit.DAY);
+            queryOperator.setForm(TapTimeForm.BEFORE);
+            queryOperator.setNumber(1L);
+            List<String> result = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,null);
+            Assertions.assertEquals("2024-04-24 00:00:00",result.get(0));
+            Assertions.assertEquals("2024-04-24 23:59:59",result.get(1));
+        }
+
+        @DisplayName("Test one Week ago")
+        @Test
+        void test2(){
+            QueryOperator queryOperator = new QueryOperator();
+            queryOperator.setUnit(TapTimeUnit.WEEK);
+            queryOperator.setForm(TapTimeForm.BEFORE);
+            queryOperator.setNumber(1L);
+            List<String> result = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,null);
+            Assertions.assertEquals("2024-04-15 00:00:00",result.get(0));
+            Assertions.assertEquals("2024-04-21 23:59:59",result.get(1));
+        }
+
+        @DisplayName("Test one Month ago")
+        @Test
+        void test3(){
+            QueryOperator queryOperator = new QueryOperator();
+            queryOperator.setUnit(TapTimeUnit.MONTH);
+            queryOperator.setForm(TapTimeForm.BEFORE);
+            queryOperator.setNumber(1L);
+            List<String> result = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,null);
+            Assertions.assertEquals("2024-03-01 00:00:00",result.get(0));
+            Assertions.assertEquals("2024-03-31 23:59:59",result.get(1));
+        }
+
+        @DisplayName("Test one Year ago")
+        @Test
+        void test4(){
+            QueryOperator queryOperator = new QueryOperator();
+            queryOperator.setUnit(TapTimeUnit.YEAR);
+            queryOperator.setForm(TapTimeForm.BEFORE);
+            queryOperator.setNumber(1L);
+            List<String> result = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,null);
+            Assertions.assertEquals("2023-01-01 00:00:00",result.get(0));
+            Assertions.assertEquals("2023-12-31 23:59:59",result.get(1));
+        }
+        @DisplayName("Test that hour")
+        @Test
+        void test5(){
+            QueryOperator queryOperator = new QueryOperator();
+            queryOperator.setUnit(TapTimeUnit.HOUR);
+            queryOperator.setForm(TapTimeForm.CURRENT);
+            List<String> result = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,null);
+            Assertions.assertEquals("2024-04-25 19:00:00",result.get(0));
+            Assertions.assertEquals("2024-04-25 19:20:00",result.get(1));
+        }
+
+        @DisplayName("Test that day")
+        @Test
+        void test6(){
+            QueryOperator queryOperator = new QueryOperator();
+            queryOperator.setUnit(TapTimeUnit.DAY);
+            queryOperator.setForm(TapTimeForm.CURRENT);
+            List<String> result = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,null);
+            Assertions.assertEquals("2024-04-25 00:00:00",result.get(0));
+            Assertions.assertEquals("2024-04-25 19:20:00",result.get(1));
+        }
+
+        @DisplayName("Test that Week")
+        @Test
+        void test7(){
+            QueryOperator queryOperator = new QueryOperator();
+            queryOperator.setUnit(TapTimeUnit.WEEK);
+            queryOperator.setForm(TapTimeForm.CURRENT);
+            List<String> result = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,null);
+            Assertions.assertEquals("2024-04-22 00:00:00",result.get(0));
+            Assertions.assertEquals("2024-04-25 19:20:00",result.get(1));
+        }
+
+        @DisplayName("Test that Month")
+        @Test
+        void test8(){
+            QueryOperator queryOperator = new QueryOperator();
+            queryOperator.setUnit(TapTimeUnit.MONTH);
+            queryOperator.setForm(TapTimeForm.CURRENT);
+            List<String> result = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,null);
+            Assertions.assertEquals("2024-04-01 00:00:00",result.get(0));
+            Assertions.assertEquals("2024-04-25 19:20:00",result.get(1));
+        }
+        @DisplayName("Test that year")
+        @Test
+        void test9(){
+            QueryOperator queryOperator = new QueryOperator();
+            queryOperator.setUnit(TapTimeUnit.YEAR);
+            queryOperator.setForm(TapTimeForm.CURRENT);
+            List<String> result = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,null);
+            Assertions.assertEquals("2024-01-01 00:00:00",result.get(0));
+            Assertions.assertEquals("2024-04-25 19:20:00",result.get(1));
+        }
+
+        @DisplayName("Test that day,Time difference minus 8")
+        @Test
+        void test10(){
+            QueryOperator queryOperator = new QueryOperator();
+            queryOperator.setUnit(TapTimeUnit.HOUR);
+            queryOperator.setForm(TapTimeForm.CURRENT);
+            List<String> result = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,-8L);
+            Assertions.assertEquals("2024-04-25 11:00:00",result.get(0));
+            Assertions.assertEquals("2024-04-25 11:20:00",result.get(1));
+        }
+
+        @DisplayName("Test that day,Time difference add 8")
+        @Test
+        void test11(){
+            QueryOperator queryOperator = new QueryOperator();
+            queryOperator.setUnit(TapTimeUnit.HOUR);
+            queryOperator.setForm(TapTimeForm.CURRENT);
+            List<String> result = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,8L);
+            Assertions.assertEquals("2024-04-26 03:00:00",result.get(0));
+            Assertions.assertEquals("2024-04-26 03:20:00",result.get(1));
+        }
+
+        @DisplayName("TimeUnit is null")
+        @Test
+        void test12(){
+            QueryOperator queryOperator = new QueryOperator();
+            queryOperator.setForm(TapTimeForm.CURRENT);
+            List<String> result = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,8L);
+            Assertions.assertEquals(0,result.size());
+        }
+
+        @DisplayName("TimeForm is null")
+        @Test
+        void test13(){
+            QueryOperator queryOperator = new QueryOperator();
+            queryOperator.setUnit(TapTimeUnit.HOUR);
+            List<String> result = TimeTransFormationUtil.calculatedTimeRange(currentDateTime,queryOperator,8L);
+            Assertions.assertEquals(0,result.size());
+        }
+
+    }
+}
diff --git a/manager/tm-oss/src/test/java/com/tapdata/tm/alarm/service/AlarmServiceImplTest.java b/manager/tm-oss/src/test/java/com/tapdata/tm/alarm/service/AlarmServiceImplTest.java
new file mode 100644
index 0000000000..8da116754b
--- /dev/null
+++ b/manager/tm-oss/src/test/java/com/tapdata/tm/alarm/service/AlarmServiceImplTest.java
@@ -0,0 +1,88 @@
+package com.tapdata.tm.alarm.service;
+
+import com.tapdata.tm.alarm.entity.AlarmInfo;
+import com.tapdata.tm.alarm.service.impl.AlarmServiceImpl;
+import com.tapdata.tm.base.exception.BizException;
+import com.tapdata.tm.config.security.UserDetail;
+import com.tapdata.tm.utils.MessageUtil;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+
+public class AlarmServiceImplTest {
+    AlarmServiceImpl alarmService = new AlarmServiceImpl();
+
+    @Test
+    void testSave(){
+        try(MockedStatic<MessageUtil> messageUtilMockedStatic = Mockito.mockStatic(MessageUtil.class)){
+            messageUtilMockedStatic.when(()->MessageUtil.getMessage(anyString())).thenReturn("error");
+            Assertions.assertThrows(BizException.class,()->alarmService.save(mock(AlarmInfo.class)));
+        }
+    }
+    @Test
+    void testClose(){
+        try(MockedStatic<MessageUtil> messageUtilMockedStatic = Mockito.mockStatic(MessageUtil.class)){
+            messageUtilMockedStatic.when(()->MessageUtil.getMessage(anyString())).thenReturn("error");
+            Assertions.assertThrows(BizException.class,()->alarmService.close(null,mock(UserDetail.class)));
+        }
+    }
+    @Test
+    void testList(){
+        try(MockedStatic<MessageUtil> messageUtilMockedStatic = Mockito.mockStatic(MessageUtil.class)){
+            messageUtilMockedStatic.when(()->MessageUtil.getMessage(anyString())).thenReturn("error");
+            Assertions.assertThrows(BizException.class,()->alarmService.list(null,null,null,null,null,null,null,null));
+        }
+    }
+    @Test
+    void testFind(){
+        try(MockedStatic<MessageUtil> messageUtilMockedStatic = Mockito.mockStatic(MessageUtil.class)){
+            messageUtilMockedStatic.when(()->MessageUtil.getMessage(anyString())).thenReturn("error");
+            Assertions.assertThrows(BizException.class,()->alarmService.find(null,null,null));
+        }
+    }
+
+    @Test
+    void testAdd(){
+        try(MockedStatic<MessageUtil> messageUtilMockedStatic = Mockito.mockStatic(MessageUtil.class)){
+            messageUtilMockedStatic.when(()->MessageUtil.getMessage(anyString())).thenReturn("error");
+            Assertions.assertThrows(BizException.class,()->alarmService.add(null,null));
+        }
+    }
+
+    @Test
+    void testCheckOpen(){
+        try(MockedStatic<MessageUtil> messageUtilMockedStatic = Mockito.mockStatic(MessageUtil.class)){
+            messageUtilMockedStatic.when(()->MessageUtil.getMessage(anyString())).thenReturn("error");
+            Assertions.assertThrows(BizException.class,()->alarmService.checkOpen(null,null,null,null,null));
+        }
+    }
+
+    @Test
+    void testCheckOpen2(){
+        try(MockedStatic<MessageUtil> messageUtilMockedStatic = Mockito.mockStatic(MessageUtil.class)){
+            messageUtilMockedStatic.when(()->MessageUtil.getMessage(anyString())).thenReturn("error");
+            Assertions.assertThrows(BizException.class,()->alarmService.checkOpen(null,null,null,null));
+        }
+    }
+
+    @Test
+    void testCloseWhenInspectTaskRunning(){
+        try(MockedStatic<MessageUtil> messageUtilMockedStatic = Mockito.mockStatic(MessageUtil.class)){
+            messageUtilMockedStatic.when(()->MessageUtil.getMessage(anyString())).thenReturn("error");
+            Assertions.assertThrows(BizException.class,()->alarmService.closeWhenInspectTaskRunning(null));
+        }
+    }
+
+    @Test
+    void testUpdateTaskAlarm(){
+        try(MockedStatic<MessageUtil> messageUtilMockedStatic = Mockito.mockStatic(MessageUtil.class)){
+            messageUtilMockedStatic.when(()->MessageUtil.getMessage(anyString())).thenReturn("error");
+            Assertions.assertThrows(BizException.class,()->alarmService.updateTaskAlarm(null));
+        }
+    }
+
+}
diff --git a/manager/tm/src/test/java/com/tapdata/tm/disruptor/handler/UpdateRecordStatusEventHandlerTest.java b/manager/tm/src/test/java/com/tapdata/tm/disruptor/handler/UpdateRecordStatusEventHandlerTest.java
index 911e55477c..12e56c2d62 100644
--- a/manager/tm/src/test/java/com/tapdata/tm/disruptor/handler/UpdateRecordStatusEventHandlerTest.java
+++ b/manager/tm/src/test/java/com/tapdata/tm/disruptor/handler/UpdateRecordStatusEventHandlerTest.java
@@ -2,19 +2,30 @@
 
 import cn.hutool.extra.spring.SpringUtil;
 import com.tapdata.tm.alarm.service.AlarmService;
+import com.tapdata.tm.base.dto.Field;
+import com.tapdata.tm.commons.dag.DAG;
+import com.tapdata.tm.commons.dag.DmlPolicy;
+import com.tapdata.tm.commons.dag.Edge;
+import com.tapdata.tm.commons.dag.Node;
+import com.tapdata.tm.commons.dag.logCollector.LogCollectorNode;
+import com.tapdata.tm.commons.dag.nodes.TableNode;
 import com.tapdata.tm.commons.task.constant.AlarmKeyEnum;
+import com.tapdata.tm.commons.task.dto.Dag;
 import com.tapdata.tm.commons.task.dto.TaskDto;
 import com.tapdata.tm.task.bean.SyncTaskStatusDto;
 import com.tapdata.tm.task.service.TaskService;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
+import org.bson.types.ObjectId;
+import org.junit.jupiter.api.*;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 import org.mockito.internal.verification.Times;
+import org.springframework.data.mongodb.core.query.Update;
 import org.springframework.test.util.ReflectionTestUtils;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.*;
 
@@ -75,4 +86,81 @@ void test3(){
             }
         }
     }
+
+    @Nested
+    class logCollectorAlarmTest{
+        @Test
+        @DisplayName("Shared mining task stopped")
+        void test1(){
+            SyncTaskStatusDto syncTaskStatusDto = new SyncTaskStatusDto();
+            syncTaskStatusDto.setTaskId(new ObjectId().toHexString());
+            syncTaskStatusDto.setTaskName("test");
+            syncTaskStatusDto.setTaskStatus("stop");
+            TaskDto taskDto = new TaskDto();
+            List<Node> nodeList = new ArrayList<>();
+            LogCollectorNode logCollectorNode = new LogCollectorNode();
+            List<String> ids = new ArrayList<>();
+            ids.add("test");
+            logCollectorNode.setId("source123");
+            logCollectorNode.setConnectionIds(ids);
+            TableNode tableNode2 = new TableNode();
+            tableNode2.setId("target123");
+            tableNode2.setDmlPolicy(new DmlPolicy());
+            nodeList.add(tableNode2);
+            nodeList.add(logCollectorNode);
+            Dag dag = new Dag();
+            Edge edge=new Edge("source123","target123");
+            List<Edge> edges = Arrays.asList(edge);
+            dag.setEdges(edges);
+            dag.setNodes(nodeList);
+            DAG mockDag =  DAG.build(dag);
+            taskDto.setDag(mockDag);
+            taskDto.setRestartFlag(false);
+            when(taskService.findById(any(),any(Field.class))).thenReturn(taskDto);
+            when(taskService.updateMany(any(),any())).thenAnswer(invocationOnMock -> {
+                Update update = invocationOnMock.getArgument(1);
+                Assertions.assertNotNull(update.getUpdateObject().get("$set"));
+                return null;
+            });
+            handler.logCollectorAlarm(syncTaskStatusDto);
+        }
+
+        @Test
+        @DisplayName("Shared mining merge")
+        void test2(){
+            SyncTaskStatusDto syncTaskStatusDto = new SyncTaskStatusDto();
+            syncTaskStatusDto.setTaskId(new ObjectId().toHexString());
+            syncTaskStatusDto.setTaskName("test");
+            syncTaskStatusDto.setTaskStatus("stop");
+            TaskDto taskDto = new TaskDto();
+            List<Node> nodeList = new ArrayList<>();
+            LogCollectorNode logCollectorNode = new LogCollectorNode();
+            List<String> ids = new ArrayList<>();
+            ids.add("test");
+            logCollectorNode.setId("source123");
+            logCollectorNode.setConnectionIds(ids);
+            TableNode tableNode2 = new TableNode();
+            tableNode2.setId("target123");
+            tableNode2.setDmlPolicy(new DmlPolicy());
+            nodeList.add(tableNode2);
+            nodeList.add(logCollectorNode);
+            Dag dag = new Dag();
+            Edge edge=new Edge("source123","target123");
+            List<Edge> edges = Arrays.asList(edge);
+            dag.setEdges(edges);
+            dag.setNodes(nodeList);
+            DAG mockDag =  DAG.build(dag);
+            taskDto.setDag(mockDag);
+            taskDto.setRestartFlag(true);
+            when(taskService.findById(any(),any(Field.class))).thenReturn(taskDto);
+            when(taskService.updateMany(any(),any())).thenAnswer(invocationOnMock -> {
+                Update update = invocationOnMock.getArgument(1);
+                Assertions.assertNull(update.getUpdateObject().get("$set"));
+                return null;
+            });
+            handler.logCollectorAlarm(syncTaskStatusDto);
+        }
+    }
+
+
 }
diff --git a/manager/tm/src/test/java/com/tapdata/tm/init/PatchesRunnerTest.java b/manager/tm/src/test/java/com/tapdata/tm/init/PatchesRunnerTest.java
new file mode 100644
index 0000000000..8d45e0638a
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/init/PatchesRunnerTest.java
@@ -0,0 +1,129 @@
+package com.tapdata.tm.init;
+
+import cn.hutool.core.io.FileUtil;
+import com.tapdata.tm.init.scanners.JavaPatchScanner;
+import com.tapdata.tm.init.scanners.ScriptPatchScanner;
+import com.tapdata.tm.verison.service.VersionService;
+import io.tapdata.utils.UnitTestUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.springframework.boot.ApplicationArguments;
+
+import java.util.*;
+
+import static org.mockito.Mockito.*;
+
+/**
+ * 补丁单测
+ *
+ * @author <a href="mailto:harsen_lin@163.com">Harsen</a>
+ * @version v1.0 2024/4/25 16:33 Create
+ */
+class PatchesRunnerTest {
+
+    PatchesRunner spyPatchesRunner;
+    VersionService versionService;
+    String mongodbUri;
+    ApplicationArguments applicationArguments;
+
+    @BeforeEach
+    void setUp() {
+        spyPatchesRunner = spy(new PatchesRunner());
+        versionService = mock(VersionService.class);
+        UnitTestUtils.injectField(PatchesRunner.class, spyPatchesRunner, "versionService", versionService);
+    }
+
+    @Nested
+    class AppType {
+        @Test
+        void testDAAS() {
+            UnitTestUtils.injectField(PatchesRunner.class, spyPatchesRunner, "mongodbUri", mongodbUri);
+            UnitTestUtils.injectField(PatchesRunner.class, spyPatchesRunner, "productList", Collections.singletonList("idass"));
+            doReturn(Collections.emptyList()).when(spyPatchesRunner).scanPatches(any(), any(), any(), any(), any());
+
+            try (MockedStatic<InitLogMap> initLogMapMockedStatic = mockStatic(InitLogMap.class)) {
+                initLogMapMockedStatic.when(() -> InitLogMap.complete(any())).then(invocation -> null);
+                Assertions.assertDoesNotThrow(() -> spyPatchesRunner.run(applicationArguments));
+                initLogMapMockedStatic.verify(() -> InitLogMap.complete(any()), times(1));
+            }
+        }
+
+        @Test
+        void testDFS() {
+            UnitTestUtils.injectField(PatchesRunner.class, spyPatchesRunner, "mongodbUri", mongodbUri);
+            UnitTestUtils.injectField(PatchesRunner.class, spyPatchesRunner, "productList", Arrays.asList("idass", "dfs"));
+            doReturn(Collections.emptyList()).when(spyPatchesRunner).scanPatches(any(), any(), any(), any(), any());
+
+            try (MockedStatic<InitLogMap> initLogMapMockedStatic = mockStatic(InitLogMap.class)) {
+                initLogMapMockedStatic.when(() -> InitLogMap.complete(any())).then(invocation -> null);
+                Assertions.assertDoesNotThrow(() -> spyPatchesRunner.run(applicationArguments));
+                initLogMapMockedStatic.verify(() -> InitLogMap.complete(any()), times(1));
+            }
+        }
+
+//        @Test
+//        void testDRS() {
+//            UnitTestUtils.injectField(PatchesRunner.class, spyPatchesRunner, "mongodbUri", mongodbUri);
+//            UnitTestUtils.injectField(PatchesRunner.class, spyPatchesRunner, "productList", Arrays.asList("idass", "drs"));
+//            doReturn(Collections.emptyList()).when(spyPatchesRunner).scanPatches(any(), any(), any(), any(), any());
+//
+//            Assertions.assertDoesNotThrow(() -> spyPatchesRunner.run(applicationArguments));
+//        }
+    }
+
+    @Nested
+    class ScanPatchesTest {
+        PatchVersion appVersion;
+        PatchVersion softVersion;
+        Map<String, String> allVariables;
+
+        @BeforeEach
+        void setUp() {
+            appVersion = new PatchVersion(3, 5, 11);
+            softVersion = new PatchVersion(3, 5, 12);
+            allVariables = new HashMap<>();
+        }
+
+        @Test
+        void testLoadScriptFailed() {
+            io.tapdata.utils.AppType appType = io.tapdata.utils.AppType.DRS;
+            ScriptPatchScanner scriptPatchScanner = new ScriptPatchScanner(appType, allVariables);
+            JavaPatchScanner javaScrPatchScanner = new JavaPatchScanner(appType);
+
+            try (MockedStatic<PatchesRunner> patchesRunnerMockedStatic = Mockito.mockStatic(PatchesRunner.class)) {
+                patchesRunnerMockedStatic.when(() -> PatchesRunner.patchDir(appType)).thenReturn(null);
+                Assertions.assertThrows(RuntimeException.class, () -> spyPatchesRunner.scanPatches(appType, appVersion, softVersion, scriptPatchScanner, javaScrPatchScanner));
+            }
+        }
+
+        @Test
+        void testSkipFileNullVersion() {
+            io.tapdata.utils.AppType appType = io.tapdata.utils.AppType.DFS;
+            ScriptPatchScanner scriptPatchScanner = new ScriptPatchScanner(appType, allVariables);
+
+            try (MockedStatic<FileUtil> fileUtilMockedStatic = Mockito.mockStatic(FileUtil.class)) {
+                fileUtilMockedStatic.when(() -> FileUtil.mainName(Mockito.anyString())).thenReturn(null); // set all file name is null
+                List<IPatch> allPatch = spyPatchesRunner.scanPatches(appType, appVersion, softVersion, scriptPatchScanner);
+                Assertions.assertTrue(allPatch.isEmpty());
+            }
+        }
+
+        @Test
+        void testPatchVersionBetweenAppAndSoft() {
+            io.tapdata.utils.AppType appType = io.tapdata.utils.AppType.DAAS;
+            ScriptPatchScanner scriptPatchScanner = new ScriptPatchScanner(appType, allVariables);
+            JavaPatchScanner javaScrPatchScanner = new JavaPatchScanner(appType);
+
+            List<IPatch> allPatch = spyPatchesRunner.scanPatches(appType, appVersion, softVersion, scriptPatchScanner, javaScrPatchScanner);
+            for (IPatch patch : allPatch) {
+                Assertions.assertNotNull(patch);
+                Assertions.assertTrue(patch.version().compareVersion(appVersion) >= 0);
+                Assertions.assertTrue(patch.version().compareVersion(softVersion) <= 0);
+            }
+        }
+    }
+}
diff --git a/manager/tm/src/test/java/com/tapdata/tm/measurementServiceV2/service/MeasurementServiceV2ImplTest.java b/manager/tm/src/test/java/com/tapdata/tm/measurementServiceV2/service/MeasurementServiceV2ImplTest.java
new file mode 100644
index 0000000000..f32b8e715b
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/measurementServiceV2/service/MeasurementServiceV2ImplTest.java
@@ -0,0 +1,68 @@
+package com.tapdata.tm.measurementServiceV2.service;
+
+import com.tapdata.tm.config.security.UserDetail;
+import com.tapdata.tm.monitor.dto.TableSyncStaticDto;
+import com.tapdata.tm.monitor.service.MeasurementServiceV2Impl;
+import com.tapdata.tm.task.service.TaskService;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+public class MeasurementServiceV2ImplTest {
+    @Nested
+    class querySyncByTableNameTest{
+        MeasurementServiceV2Impl measurementServiceV2 = mock(MeasurementServiceV2Impl.class);
+
+        @Test
+        void testTableNameIsNotNull(){
+            TableSyncStaticDto tableSyncStaticDto = new TableSyncStaticDto("test",1,20,"test");
+            Criteria criteria = new Criteria();
+            doCallRealMethod().when(measurementServiceV2).querySyncByTableName(any(),any());
+            measurementServiceV2.querySyncByTableName(tableSyncStaticDto,criteria);
+            Assertions.assertTrue(criteria.getCriteriaObject().containsKey("tags.table"));
+        }
+
+        @Test
+        void testTableNameIsNull(){
+            TableSyncStaticDto tableSyncStaticDto = new TableSyncStaticDto("test",1,20,null);
+            Criteria criteria = new Criteria();
+            doCallRealMethod().when(measurementServiceV2).querySyncByTableName(any(),any());
+            measurementServiceV2.querySyncByTableName(tableSyncStaticDto,criteria);
+            Assertions.assertFalse(criteria.getCriteriaObject().containsKey("tags.table"));
+        }
+
+    }
+    @Nested
+    class QuerySyncStatic{
+        MeasurementServiceV2Impl measurementServiceV2 = mock(MeasurementServiceV2Impl.class);
+        MongoTemplate mongoTemplate;
+        TaskService taskService;
+        @BeforeEach
+        void before(){
+            mongoTemplate = mock(MongoTemplate.class);
+            taskService = mock(TaskService.class);
+            ReflectionTestUtils.setField(measurementServiceV2,"mongoOperations",mongoTemplate);
+            ReflectionTestUtils.setField(measurementServiceV2,"taskService",taskService);
+        }
+        @Test
+        void test(){
+            TableSyncStaticDto tableSyncStaticDto = new TableSyncStaticDto("test",1,20,"test");
+            when(mongoTemplate.count(any(Query.class),anyString())).thenAnswer(invocationOnMock -> {
+                Query query = invocationOnMock.getArgument(0);
+                Assertions.assertTrue(query.getQueryObject().containsKey("tags.table"));
+                return 0L;
+            });
+            doCallRealMethod().when(measurementServiceV2).querySyncStatic(any(),any());
+            measurementServiceV2.querySyncStatic(tableSyncStaticDto,mock(UserDetail.class));
+        }
+
+    }
+}
diff --git a/manager/tm/src/test/java/com/tapdata/tm/schedule/TaskRestartScheduleTest.java b/manager/tm/src/test/java/com/tapdata/tm/schedule/TaskRestartScheduleTest.java
index 62b8790703..dcdda6afd0 100644
--- a/manager/tm/src/test/java/com/tapdata/tm/schedule/TaskRestartScheduleTest.java
+++ b/manager/tm/src/test/java/com/tapdata/tm/schedule/TaskRestartScheduleTest.java
@@ -6,6 +6,7 @@
 import com.tapdata.tm.statemachine.enums.DataFlowEvent;
 import com.tapdata.tm.statemachine.model.StateMachineResult;
 import com.tapdata.tm.statemachine.service.StateMachineService;
+import com.tapdata.tm.task.constant.SyncStatus;
 import com.tapdata.tm.task.service.TaskScheduleService;
 import com.tapdata.tm.task.service.TaskService;
 import com.tapdata.tm.user.service.UserService;
@@ -33,6 +34,7 @@ void testWaitRunTaskPositive() {
             TaskDto taskDto = new TaskDto();
             taskDto.setUserId(userId);
             taskDto.setStatus(TaskDto.STATUS_WAIT_RUN);
+            taskDto.setSyncStatus(SyncStatus.NORMAL);
             taskDto.setScheduledTime(new Date(System.currentTimeMillis() - 40000));
             List<TaskDto> all = new ArrayList<>();
             all.add(taskDto);
diff --git a/manager/tm/src/test/java/com/tapdata/tm/task/service/TaskServiceImplTest.java b/manager/tm/src/test/java/com/tapdata/tm/task/service/TaskServiceImplTest.java
index 1f51fe6534..a8ecb59d7c 100644
--- a/manager/tm/src/test/java/com/tapdata/tm/task/service/TaskServiceImplTest.java
+++ b/manager/tm/src/test/java/com/tapdata/tm/task/service/TaskServiceImplTest.java
@@ -22,6 +22,7 @@
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.mockito.MockedStatic;
+import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.test.util.ReflectionTestUtils;
 
 import java.util.ArrayList;
@@ -33,13 +34,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.doCallRealMethod;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.mockStatic;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
 
 
 class TaskServiceImplTest {
@@ -461,4 +456,18 @@ void testNotHasUnwindProcess(){
             Assertions.assertNull(tableNode2.getDmlPolicy().getInsertPolicy());
         }
     }
+
+    @Nested
+    class FindByCacheName{
+        TaskServiceImpl taskService = spy(new TaskServiceImpl(mock(TaskRepository.class)));
+        @Test
+        void test(){
+            doAnswer(invocationOnMock -> {
+                Query query = invocationOnMock.getArgument(0);
+                Assertions.assertEquals(query.getQueryObject().get("is_deleted"),false);
+                return null;
+            }).when(taskService).findOne(any(Query.class),any(UserDetail.class));
+            taskService.findByCacheName("test",mock(UserDetail.class));
+        }
+    }
 }
\ No newline at end of file
diff --git a/manager/tm/src/test/java/com/tapdata/tm/task/service/TaskServiceTest.java b/manager/tm/src/test/java/com/tapdata/tm/task/service/TaskServiceTest.java
index 50df47b8fe..a62c994466 100644
--- a/manager/tm/src/test/java/com/tapdata/tm/task/service/TaskServiceTest.java
+++ b/manager/tm/src/test/java/com/tapdata/tm/task/service/TaskServiceTest.java
@@ -25,6 +25,8 @@
 import com.tapdata.tm.task.bean.Chart6Vo;
 import com.tapdata.tm.task.entity.TaskEntity;
 import com.tapdata.tm.task.repository.TaskRepository;
+import com.tapdata.tm.task.service.batchin.ParseRelMigFile;
+import com.tapdata.tm.task.service.batchin.entity.ParseParam;
 import com.tapdata.tm.userLog.service.UserLogService;
 import com.tapdata.tm.utils.MongoUtils;
 import com.tapdata.tm.utils.SpringContextHelper;
@@ -244,17 +246,17 @@ class TestRun {
 
         @BeforeEach
         void beforeEach() {
-            repository=mock(TaskRepository.class);
+            repository = mock(TaskRepository.class);
             taskService = spy(new TaskServiceImpl(repository));
-            taskEntity=new TaskEntity();
+            taskEntity = new TaskEntity();
             taskEntity.setUserId("6393f084c162f518b18165c3");
             taskEntity.setAgentId("632327dd287a904778c0a13c-1gd0l7dvk");
             taskEntity.setName("test");
             taskEntity.setId(MongoUtils.toObjectId("6324562fc5c0a4052d821d90"));
-            settingsService=mock(SettingsServiceImpl.class);
+            settingsService = mock(SettingsServiceImpl.class);
             when(settingsService.isCloud()).thenReturn(true);
             taskService.setSettingsService(settingsService);
-            taskScheduleService=mock(TaskScheduleService.class);
+            taskScheduleService = mock(TaskScheduleService.class);
             taskService.setTaskScheduleService(taskScheduleService);
         }
 
@@ -303,40 +305,44 @@ void testExceedRun() {
             }
         }
     }
+
     @Nested
-    class TestSubCronOrPlanNum{
+    class TestSubCronOrPlanNum {
         TaskRepository taskRepository;
         TaskEntity taskEntity;
         TaskDto taskDto;
+
         @BeforeEach
-        void beforeEach(){
-            taskEntity=new TaskEntity();
+        void beforeEach() {
+            taskEntity = new TaskEntity();
             taskEntity.setId(MongoUtils.toObjectId("6324562fc5c0a4052d821d90"));
 
-            taskDto=new TaskDto();
-            BeanUtils.copyProperties(taskEntity,taskDto);
+            taskDto = new TaskDto();
+            BeanUtils.copyProperties(taskEntity, taskDto);
             taskRepository = mock(TaskRepository.class);
-            taskService=spy(new TaskServiceImpl(taskRepository));
+            taskService = spy(new TaskServiceImpl(taskRepository));
             Query query = new Query(Criteria.where("_id").is(taskDto.getId()));
             query.fields().include("planStartDateFlag", "crontabExpressionFlag");
             when(taskRepository.findOne(query)).thenReturn(Optional.ofNullable(taskEntity));
         }
+
         @DisplayName("test cron task sub 1")
         @Test
-        void testSubCronOrPlanNum(){
+        void testSubCronOrPlanNum() {
             taskEntity.setCrontabExpressionFlag(true);
             try (MockedStatic<DataPermissionHelper> dataPermissionHelperMockedStatic = mockStatic(DataPermissionHelper.class)) {
                 int result = taskService.subCronOrPlanNum(taskDto, 3);
-                assertEquals(2,result);
+                assertEquals(2, result);
             }
         }
+
         @DisplayName("test not cron task don't sub 1")
         @Test
-        void testNoCronOrPlanTask(){
+        void testNoCronOrPlanTask() {
             taskEntity.setCrontabExpressionFlag(null);
             try (MockedStatic<DataPermissionHelper> dataPermissionHelperMockedStatic = mockStatic(DataPermissionHelper.class)) {
                 int result = taskService.subCronOrPlanNum(taskDto, 3);
-                assertEquals(3,result);
+                assertEquals(3, result);
             }
         }
     }
@@ -352,27 +358,29 @@ class TestCheckCloudTaskLimit {
 
         final UserDetail user = new UserDetail("6393f084c162f518b18165c3", "customerId", "username", "password", "customerType",
                 "accessCode", false, false, false, false, Arrays.asList(new SimpleGrantedAuthority("role")));
+
         @BeforeEach
         void beforeEach() {
             new DataPermissionHelper(mock(IDataPermissionHelper.class)); //when repository.find call methods in DataPermissionHelper class this line is need
             taskService = new TaskServiceImpl(taskRepository);
             taskService.setSettingsService(settingsService);
             taskService.setTaskScheduleService(taskScheduleService);
-            ReflectionTestUtils.setField(taskService,"workerService",workerService);
+            ReflectionTestUtils.setField(taskService, "workerService", workerService);
         }
+
         @Test
-        void test_isDass(){
+        void test_isDass() {
             when(settingsService.isCloud()).thenReturn(false);
             ObjectId taskId = MongoUtils.toObjectId("632454d5287a904778c40f8d");
-            boolean result = taskService.checkCloudTaskLimit(taskId,user,true);
+            boolean result = taskService.checkCloudTaskLimit(taskId, user, true);
             assertTrue(result);
         }
 
         @Test
-        void test_isCloudLimit(){
+        void test_isCloudLimit() {
             ObjectId taskId = MongoUtils.toObjectId("632454d5287a904778c40f8d");
             Query query = new Query(Criteria.where("_id").is(taskId));
-            query.fields().include("id","agentId","agentTags");
+            query.fields().include("id", "agentId", "agentTags");
             TaskEntity mockTask = new TaskEntity();
             mockTask.setCrontabExpressionFlag(false);
             mockTask.setId(MongoUtils.toObjectId("632454d5287a904778c40f8d"));
@@ -381,24 +389,24 @@ void test_isCloudLimit(){
                 when(settingsService.isCloud()).thenReturn(true);
                 when(taskRepository.findOne(query)).thenReturn(Optional.of(mockTask));
                 TaskDto mockTaskDto = new TaskDto();
-                BeanUtils.copyProperties(mockTask,mockTaskDto);
+                BeanUtils.copyProperties(mockTask, mockTaskDto);
                 CalculationEngineVo mockEngineVo = new CalculationEngineVo();
                 mockEngineVo.setTaskLimit(5);
                 mockEngineVo.setRunningNum(5);
-                when(workerService.calculationEngine(mockTaskDto,user,null)).thenReturn(mockEngineVo);
+                when(workerService.calculationEngine(mockTaskDto, user, null)).thenReturn(mockEngineVo);
                 Query mockQuery = new Query(Criteria.where("_id").is(taskId));
                 mockQuery.fields().include("planStartDateFlag", "crontabExpressionFlag");
                 when(taskRepository.findOne(mockQuery)).thenReturn(Optional.of(mockTask));
-                boolean result = taskService.checkCloudTaskLimit(taskId,user,true);
+                boolean result = taskService.checkCloudTaskLimit(taskId, user, true);
                 assertFalse(result);
             }
         }
 
         @Test
-        void test_isCloudLimitNotReached(){
+        void test_isCloudLimitNotReached() {
             ObjectId taskId = MongoUtils.toObjectId("632454d5287a904778c40f8d");
             Query query = new Query(Criteria.where("_id").is(taskId));
-            query.fields().include("id","agentId","agentTags");
+            query.fields().include("id", "agentId", "agentTags");
             TaskEntity mockTask = new TaskEntity();
             mockTask.setCrontabExpressionFlag(false);
             mockTask.setId(MongoUtils.toObjectId("632454d5287a904778c40f8d"));
@@ -407,24 +415,24 @@ void test_isCloudLimitNotReached(){
                 when(settingsService.isCloud()).thenReturn(true);
                 when(taskRepository.findOne(query)).thenReturn(Optional.of(mockTask));
                 TaskDto mockTaskDto = new TaskDto();
-                BeanUtils.copyProperties(mockTask,mockTaskDto);
+                BeanUtils.copyProperties(mockTask, mockTaskDto);
                 CalculationEngineVo mockEngineVo = new CalculationEngineVo();
                 mockEngineVo.setTaskLimit(5);
                 mockEngineVo.setRunningNum(4);
-                when(workerService.calculationEngine(mockTaskDto,user,null)).thenReturn(mockEngineVo);
+                when(workerService.calculationEngine(mockTaskDto, user, null)).thenReturn(mockEngineVo);
                 Query mockQuery = new Query(Criteria.where("_id").is(taskId));
                 mockQuery.fields().include("planStartDateFlag", "crontabExpressionFlag");
                 when(taskRepository.findOne(mockQuery)).thenReturn(Optional.of(mockTask));
-                boolean result = taskService.checkCloudTaskLimit(taskId,user,true);
+                boolean result = taskService.checkCloudTaskLimit(taskId, user, true);
                 assertTrue(result);
             }
         }
 
         @Test
-        void test_isCloudLimitScheduling(){
+        void test_isCloudLimitScheduling() {
             ObjectId taskId = MongoUtils.toObjectId("632454d5287a904778c40f8d");
             Query query = new Query(Criteria.where("_id").is(taskId));
-            query.fields().include("id","agentId","agentTags");
+            query.fields().include("id", "agentId", "agentTags");
             TaskEntity mockTask = new TaskEntity();
             mockTask.setCrontabExpressionFlag(true);
             mockTask.setId(MongoUtils.toObjectId("632454d5287a904778c40f8d"));
@@ -433,22 +441,22 @@ void test_isCloudLimitScheduling(){
                 when(settingsService.isCloud()).thenReturn(true);
                 when(taskRepository.findOne(query)).thenReturn(Optional.of(mockTask));
                 TaskDto mockTaskDto = new TaskDto();
-                BeanUtils.copyProperties(mockTask,mockTaskDto);
+                BeanUtils.copyProperties(mockTask, mockTaskDto);
                 CalculationEngineVo mockEngineVo = new CalculationEngineVo();
                 mockEngineVo.setTaskLimit(5);
                 mockEngineVo.setRunningNum(5);
-                when(workerService.calculationEngine(mockTaskDto,user,null)).thenReturn(mockEngineVo);
+                when(workerService.calculationEngine(mockTaskDto, user, null)).thenReturn(mockEngineVo);
                 Query mockQuery = new Query(Criteria.where("_id").is(taskId));
                 mockQuery.fields().include("planStartDateFlag", "crontabExpressionFlag");
                 when(taskRepository.findOne(mockQuery)).thenReturn(Optional.of(mockTask));
-                boolean result = taskService.checkCloudTaskLimit(taskId,user,true);
+                boolean result = taskService.checkCloudTaskLimit(taskId, user, true);
                 assertTrue(result);
             }
         }
     }
 
     @Nested
-    class TestCopy{
+    class TestCopy {
         TaskRepository taskRepository = mock(TaskRepository.class);
 
         SettingsServiceImpl settingsService = mock(SettingsServiceImpl.class);
@@ -461,6 +469,7 @@ class TestCopy{
 
         final UserDetail user = new UserDetail("6393f084c162f518b18165c3", "customerId", "username", "password", "customerType",
                 "accessCode", false, false, false, false, Arrays.asList(new SimpleGrantedAuthority("role")));
+
         @BeforeEach
         void beforeEach() {
             new DataPermissionHelper(mock(IDataPermissionHelper.class)); //when repository.find call methods in DataPermissionHelper class this line is need
@@ -468,11 +477,11 @@ void beforeEach() {
             taskService.setSettingsService(settingsService);
             taskService.setTaskScheduleService(taskScheduleService);
             taskService.setUserLogService(serLogService);
-            ReflectionTestUtils.setField(taskService,"workerService",workerService);
+            ReflectionTestUtils.setField(taskService, "workerService", workerService);
         }
 
         @Test
-        void test_copySchedulingTask(){
+        void test_copySchedulingTask() {
             ObjectId taskId = MongoUtils.toObjectId("632454d5287a904778c40f8d");
             TaskEntity mockTask = new TaskEntity();
             mockTask.setCrontabExpressionFlag(true);
@@ -482,32 +491,32 @@ void test_copySchedulingTask(){
                  MockedStatic<SpringContextHelper> helperMockedStatic = Mockito.mockStatic(SpringContextHelper.class)) {
                 serviceMockedStatic.when(DataPermissionService::isCloud).thenReturn(true);
                 TaskDto mockTaskDto = new TaskDto();
-                BeanUtils.copyProperties(mockTask,mockTaskDto);
-                when(taskRepository.findById(taskId,user)).thenReturn(Optional.of(mockTask));
+                BeanUtils.copyProperties(mockTask, mockTaskDto);
+                when(taskRepository.findById(taskId, user)).thenReturn(Optional.of(mockTask));
                 when(settingsService.isCloud()).thenReturn(true);
                 Query query = new Query(Criteria.where("_id").is(taskId));
-                query.fields().include("id","agentId","agentTags");
+                query.fields().include("id", "agentId", "agentTags");
                 when(taskRepository.findOne(query)).thenReturn(Optional.of(mockTask));
                 CalculationEngineVo mockEngineVo = new CalculationEngineVo();
                 mockEngineVo.setTaskLimit(5);
                 mockEngineVo.setRunningNum(4);
-                when(workerService.calculationEngine(mockTaskDto,user,null)).thenReturn(mockEngineVo);
+                when(workerService.calculationEngine(mockTaskDto, user, null)).thenReturn(mockEngineVo);
                 Query mockQuery = new Query(Criteria.where("_id").is(taskId));
                 mockQuery.fields().include("planStartDateFlag", "crontabExpressionFlag");
                 when(taskRepository.findOne(mockQuery)).thenReturn(Optional.of(mockTask));
                 TaskServiceImpl mockTaskService = mock(TaskServiceImpl.class);
-                helperMockedStatic.when(()->SpringContextHelper.getBean(TaskServiceImpl.class)).thenReturn(mockTaskService);
-                when(mockTaskService.confirmById(any(TaskDto.class),any(UserDetail.class),any(Boolean.class))).thenAnswer(invocationOnMock -> {
+                helperMockedStatic.when(() -> SpringContextHelper.getBean(TaskServiceImpl.class)).thenReturn(mockTaskService);
+                when(mockTaskService.confirmById(any(TaskDto.class), any(UserDetail.class), any(Boolean.class))).thenAnswer(invocationOnMock -> {
                     return invocationOnMock.<TaskDto>getArgument(0);
                 });
-                TaskDto result = taskService.copy(taskId,user);
+                TaskDto result = taskService.copy(taskId, user);
                 assertTrue(result.getCrontabExpressionFlag());
-                assertEquals("test",result.getCrontabExpression());
+                assertEquals("test", result.getCrontabExpression());
             }
         }
 
         @Test
-        void test_copySchedulingTaskLimit(){
+        void test_copySchedulingTaskLimit() {
             ObjectId taskId = MongoUtils.toObjectId("632454d5287a904778c40f8d");
             TaskEntity mockTask = new TaskEntity();
             mockTask.setCrontabExpressionFlag(true);
@@ -516,32 +525,32 @@ void test_copySchedulingTaskLimit(){
                  MockedStatic<SpringContextHelper> helperMockedStatic = Mockito.mockStatic(SpringContextHelper.class)) {
                 serviceMockedStatic.when(DataPermissionService::isCloud).thenReturn(true);
                 TaskDto mockTaskDto = new TaskDto();
-                BeanUtils.copyProperties(mockTask,mockTaskDto);
-                when(taskRepository.findById(taskId,user)).thenReturn(Optional.of(mockTask));
+                BeanUtils.copyProperties(mockTask, mockTaskDto);
+                when(taskRepository.findById(taskId, user)).thenReturn(Optional.of(mockTask));
                 when(settingsService.isCloud()).thenReturn(true);
                 Query query = new Query(Criteria.where("_id").is(taskId));
-                query.fields().include("id","agentId","agentTags");
+                query.fields().include("id", "agentId", "agentTags");
                 when(taskRepository.findOne(query)).thenReturn(Optional.of(mockTask));
                 CalculationEngineVo mockEngineVo = new CalculationEngineVo();
                 mockEngineVo.setTaskLimit(5);
                 mockEngineVo.setRunningNum(5);
-                when(workerService.calculationEngine(mockTaskDto,user,null)).thenReturn(mockEngineVo);
+                when(workerService.calculationEngine(mockTaskDto, user, null)).thenReturn(mockEngineVo);
                 Query mockQuery = new Query(Criteria.where("_id").is(taskId));
                 mockQuery.fields().include("planStartDateFlag", "crontabExpressionFlag");
                 when(taskRepository.findOne(mockQuery)).thenReturn(Optional.of(mockTask));
                 TaskServiceImpl mockTaskService = mock(TaskServiceImpl.class);
-                helperMockedStatic.when(()->SpringContextHelper.getBean(TaskServiceImpl.class)).thenReturn(mockTaskService);
-                when(mockTaskService.confirmById(any(TaskDto.class),any(UserDetail.class),any(Boolean.class))).thenAnswer(invocationOnMock -> {
+                helperMockedStatic.when(() -> SpringContextHelper.getBean(TaskServiceImpl.class)).thenReturn(mockTaskService);
+                when(mockTaskService.confirmById(any(TaskDto.class), any(UserDetail.class), any(Boolean.class))).thenAnswer(invocationOnMock -> {
                     return invocationOnMock.<TaskDto>getArgument(0);
                 });
-                TaskDto result = taskService.copy(taskId,user);
+                TaskDto result = taskService.copy(taskId, user);
                 assertFalse(result.getCrontabExpressionFlag());
                 assertNull(result.getCrontabExpression());
             }
         }
 
         @Test
-        void test_copyNormalTaskLimit(){
+        void test_copyNormalTaskLimit() {
             ObjectId taskId = MongoUtils.toObjectId("632454d5287a904778c40f8d");
             TaskEntity mockTask = new TaskEntity();
             mockTask.setId(MongoUtils.toObjectId("632454d5287a904778c40f8d"));
@@ -549,58 +558,63 @@ void test_copyNormalTaskLimit(){
                  MockedStatic<SpringContextHelper> helperMockedStatic = Mockito.mockStatic(SpringContextHelper.class)) {
                 serviceMockedStatic.when(DataPermissionService::isCloud).thenReturn(true);
                 TaskDto mockTaskDto = new TaskDto();
-                BeanUtils.copyProperties(mockTask,mockTaskDto);
-                when(taskRepository.findById(taskId,user)).thenReturn(Optional.of(mockTask));
+                BeanUtils.copyProperties(mockTask, mockTaskDto);
+                when(taskRepository.findById(taskId, user)).thenReturn(Optional.of(mockTask));
                 when(settingsService.isCloud()).thenReturn(true);
                 Query query = new Query(Criteria.where("_id").is(taskId));
-                query.fields().include("id","agentId","agentTags");
+                query.fields().include("id", "agentId", "agentTags");
                 when(taskRepository.findOne(query)).thenReturn(Optional.of(mockTask));
                 CalculationEngineVo mockEngineVo = new CalculationEngineVo();
                 mockEngineVo.setTaskLimit(5);
                 mockEngineVo.setRunningNum(4);
-                when(workerService.calculationEngine(mockTaskDto,user,null)).thenReturn(mockEngineVo);
+                when(workerService.calculationEngine(mockTaskDto, user, null)).thenReturn(mockEngineVo);
                 Query mockQuery = new Query(Criteria.where("_id").is(taskId));
                 mockQuery.fields().include("planStartDateFlag", "crontabExpressionFlag");
                 when(taskRepository.findOne(mockQuery)).thenReturn(Optional.of(mockTask));
                 TaskServiceImpl mockTaskService = mock(TaskServiceImpl.class);
-                helperMockedStatic.when(()->SpringContextHelper.getBean(TaskServiceImpl.class)).thenReturn(mockTaskService);
-                when(mockTaskService.confirmById(any(TaskDto.class),any(UserDetail.class),any(Boolean.class))).thenAnswer(invocationOnMock -> {
+                helperMockedStatic.when(() -> SpringContextHelper.getBean(TaskServiceImpl.class)).thenReturn(mockTaskService);
+                when(mockTaskService.confirmById(any(TaskDto.class), any(UserDetail.class), any(Boolean.class))).thenAnswer(invocationOnMock -> {
                     return invocationOnMock.<TaskDto>getArgument(0);
                 });
-                TaskDto result = taskService.copy(taskId,user);
+                TaskDto result = taskService.copy(taskId, user);
                 assertNull(result.getCrontabExpressionFlag());
                 assertNull(result.getCrontabExpression());
             }
         }
     }
+
     @Nested
-    class TestRunningTaskNum{
+    class TestRunningTaskNum {
         TaskRepository taskRepository = mock(TaskRepository.class);
         final UserDetail user = new UserDetail("6393f084c162f518b18165c3", "customerId", "username", "password", "customerType",
                 "accessCode", false, false, false, false, Arrays.asList(new SimpleGrantedAuthority("role")));
+
         @BeforeEach
         void beforeEach() {
             taskService = new TaskServiceImpl(taskRepository);
         }
+
         @Test
-        void testRunningTaskNum(){
+        void testRunningTaskNum() {
             long except = 5L;
             when(taskRepository.count(Query.query(Criteria.where("is_deleted").ne(true)
                     .and("syncType").in(TaskDto.SYNC_TYPE_SYNC, TaskDto.SYNC_TYPE_MIGRATE)
-                    .and("status").nin(TaskDto.STATUS_DELETE_FAILED,TaskDto.STATUS_DELETING)
+                    .and("status").nin(TaskDto.STATUS_DELETE_FAILED, TaskDto.STATUS_DELETING)
                     .orOperator(Criteria.where("status").in(TaskDto.STATUS_RUNNING, TaskDto.STATUS_SCHEDULING, TaskDto.STATUS_WAIT_RUN),
                             Criteria.where("planStartDateFlag").is(true),
                             Criteria.where("crontabExpressionFlag").is(true)
-                    )),user)).thenReturn(except);
+                    )), user)).thenReturn(except);
             long result = taskService.runningTaskNum(user);
-            assertEquals(except,result);
+            assertEquals(except, result);
         }
     }
+
     @Nested
-    class ChartTest{
+    class ChartTest {
         TaskRepository taskRepository = mock(TaskRepository.class);
-//        @Test
-        void testChartNormal(){
+
+        //        @Test
+        void testChartNormal() {
             new DataPermissionHelper(mock(DataPermissionHelperImpl.class)); //when repository.find call methods in DataPermissionHelper class this line is need
             try (MockedStatic<DataPermissionService> mb = Mockito
                     .mockStatic(DataPermissionService.class)) {
@@ -629,165 +643,251 @@ void testChartNormal(){
                 taskDtoList.add(taskDto3);
                 taskDtoList.add(taskDto4);
                 taskDtoList.add(taskDto5);
-                doReturn(taskDtoList).when(taskService).findAllDto(any(),any());
-                when(permission.MigrateTack.checkAndSetFilter(user, DataPermissionActionEnums.View, () -> taskService.findAllDto(any(),any()))).thenReturn(taskDtoList);
+                doReturn(taskDtoList).when(taskService).findAllDto(any(), any());
+                when(permission.MigrateTack.checkAndSetFilter(user, DataPermissionActionEnums.View, () -> taskService.findAllDto(any(), any()))).thenReturn(taskDtoList);
                 doReturn(new HashMap()).when(taskService).inspectChart(user);
                 Chart6Vo chart6Vo = mock(Chart6Vo.class);
                 doReturn(chart6Vo).when(taskService).chart6(user);
                 Map<String, Object> actual = taskService.chart(user);
                 Map chart1 = (Map) actual.get("chart1");
-                assertEquals(3,chart1.get("total"));
+                assertEquals(3, chart1.get("total"));
                 Map chart3 = (Map) actual.get("chart3");
-                assertEquals(2,chart3.get("total"));
+                assertEquals(2, chart3.get("total"));
                 Map chart5 = (Map) actual.get("chart5");
-                assertEquals(0,chart5.size());
-                assertEquals(chart6Vo,actual.get("chart6"));
+                assertEquals(0, chart5.size());
+                assertEquals(chart6Vo, actual.get("chart6"));
             }
 
         }
     }
+
     @Nested
-    class importRmProjectTest{
-        TaskRepository taskRepository=mock(TaskRepository.class);
-        TaskServiceImpl taskService=spy(new TaskServiceImpl(taskRepository));
+    class importRmProjectTest {
+        ParseRelMigFile parseRelMigFile;
+        TaskRepository taskRepository = mock(TaskRepository.class);
+        TaskServiceImpl taskService = spy(new TaskServiceImpl(taskRepository));
         UserDetail userDetail;
         FileInputStream fileInputStream;
+        MockMultipartFile mockMultipartFile;
+        String rmJson;
+
         @BeforeEach
-        void beforeEach() throws FileNotFoundException {
+        void beforeEach() throws Exception {
             userDetail = new UserDetail("6393f084c162f518b18165c3", "customerId", "username", "password", "customerType",
                     "accessCode", false, false, false, false, Arrays.asList(new SimpleGrantedAuthority("role")));
             URL resource = this.getClass().getClassLoader().getResource("test.relmig");
-            fileInputStream=new FileInputStream(resource.getFile());
+            fileInputStream = new FileInputStream(resource.getFile());
+            mockMultipartFile = new MockMultipartFile("test.relmig", fileInputStream);
+            rmJson = new String(mockMultipartFile.getBytes());
         }
+
         @Test
         void importRmProjectTest() throws IOException {
-            MockMultipartFile mockMultipartFile = new MockMultipartFile("test.relmig", fileInputStream);
-            String rmJson = new String(mockMultipartFile.getBytes());
+
             HashMap<String, Object> rmProject = new ObjectMapper().readValue(rmJson, HashMap.class);
             HashMap<String, Object> project = (HashMap<String, Object>) rmProject.get("project");
             HashMap<String, Object> content = (HashMap<String, Object>) project.get("content");
             HashMap<String, Object> contentCollections = (HashMap<String, Object>) content.get("collections");
-            Map<String, String> stringStringMap = taskService.parseTaskFromRm(rmJson, "123", "123", userDetail);
-            TaskDto taskDto=null;
-            for(String taskKey:stringStringMap.keySet()){
+            ParseParam param = new ParseParam()
+                    .withMultipartFile(mockMultipartFile)
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(userDetail);
+            param.setRelMigStr(rmJson);
+            param.setRelMigInfo(rmProject);
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            Map<String, String> stringStringMap = parseRelMigFile.doParse("sourceConnectionId", "targetConnectionId", userDetail);
+            TaskDto taskDto = null;
+            for (String taskKey : stringStringMap.keySet()) {
                 taskDto = JsonUtil.parseJsonUseJackson(stringStringMap.get(taskKey), TaskDto.class);
             }
-            assertEquals(5,stringStringMap.size());
+            assertEquals(5, stringStringMap.size());
         }
+
         @Test
-        void nullImportRmProjectTest(){
-            assertThrows(BizException.class,()->{taskService.parseTaskFromRm(null, "123", "123", userDetail);});
+        void nullImportRmProjectTest() {
+            ParseParam param = new ParseParam()
+                    .withMultipartFile(mockMultipartFile)
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(userDetail);
+            param.setRelMigInfo(new HashMap<>());
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            assertDoesNotThrow(() -> {
+                parseRelMigFile.doParse("sourceConnectionId", "targetConnectionId", userDetail);
+            });
         }
+
         @Test
         void replaceIdTest() throws IOException {
-            MockMultipartFile mockMultipartFile = new MockMultipartFile("test.relmig", fileInputStream);
-            String s = new String(mockMultipartFile.getBytes());
-            Map<String, Object> rmProject = new ObjectMapper().readValue(s, HashMap.class);
+            Map<String, Object> rmProject = new ObjectMapper().readValue(rmJson, HashMap.class);
             Map<String, Object> project = (Map<String, Object>) rmProject.get("project");
             Map<String, Object> content = (Map<String, Object>) project.get("content");
             Map<String, Object> contentMapping = (Map<String, Object>) content.get("mappings");
             Map<String, Object> contentCollections = (Map<String, Object>) content.get("collections");
             Set<String> collectionKeys = contentCollections.keySet();
-            String collectionKey=null;
-            for(String key:collectionKeys){
-                collectionKey=key;
+            String collectionKey = null;
+            for (String key : collectionKeys) {
+                collectionKey = key;
             }
             Set<String> contentMappingKeys = contentMapping.keySet();
-            String contentMappingKey=null;
-            String contentMappingCollectionId=null;
-            for(String key:contentMappingKeys){
+            String contentMappingKey = null;
+            String contentMappingCollectionId = null;
+            for (String key : contentMappingKeys) {
                 Map<String, Object> mapping = (Map<String, Object>) contentMapping.get(key);
-                String collectionId = (String)mapping.get("collectionId");
-                contentMappingCollectionId=collectionId;
-                contentMappingKey=key;
+                String collectionId = (String) mapping.get("collectionId");
+                contentMappingCollectionId = collectionId;
+                contentMappingKey = key;
             }
-            taskService.replaceRmProjectId(rmProject);
+            ParseParam param = new ParseParam()
+                    .withMultipartFile(mockMultipartFile)
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(userDetail);
+            param.setRelMigInfo(new HashMap<>());
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            parseRelMigFile.replaceRmProjectId();
             Set<String> afterStrings = contentCollections.keySet();
-            String afterCollectionKey=null;
-            for(String afterKey1:afterStrings){
-                afterCollectionKey=afterKey1;
+            String afterCollectionKey = null;
+            for (String afterKey1 : afterStrings) {
+                afterCollectionKey = afterKey1;
             }
             Set<String> afterContentMappingKeys = contentMapping.keySet();
-            String afterContentMappingCollectionId=null;
-            String afterContentMappingKey=null;
-            for(String key:afterContentMappingKeys){
+            String afterContentMappingCollectionId = null;
+            String afterContentMappingKey = null;
+            for (String key : afterContentMappingKeys) {
                 Map<String, Object> mapping = (Map<String, Object>) contentMapping.get(key);
-                afterContentMappingCollectionId = (String)mapping.get("collectionId");
-                afterContentMappingKey=key;
+                afterContentMappingCollectionId = (String) mapping.get("collectionId");
+                afterContentMappingKey = key;
             }
-            assertNotEquals(collectionKey,afterCollectionKey);
-            assertNotEquals(contentMappingKey,afterContentMappingKey);
-            assertNotEquals(contentMappingCollectionId,afterContentMappingCollectionId);
+            assertEquals(collectionKey, afterCollectionKey);
+            assertEquals(contentMappingKey, afterContentMappingKey);
+            assertEquals(contentMappingCollectionId, afterContentMappingCollectionId);
         }
+
         @Test
         void testReplaceRelationShipsKey() throws IOException {
             Map<String, String> globalIdMap = new HashMap<>();
-            MockMultipartFile mockMultipartFile = new MockMultipartFile("test.relmig", fileInputStream);
-            String s = new String(mockMultipartFile.getBytes());
-            Map<String, Object> rmProject = new ObjectMapper().readValue(s, HashMap.class);
+            Map<String, Object> rmProject = new ObjectMapper().readValue(rmJson, HashMap.class);
             Map<String, Object> project = (Map<String, Object>) rmProject.get("project");
             Map<String, Object> content = (Map<String, Object>) project.get("content");
             Map<String, Object> relationships = content.get("relationships") == null ? new HashMap<>() : (Map<String, Object>) content.get("relationships");
             Map<String, Object> collectionMap = (Map<String, Object>) relationships.get("collections");
             Map<String, Object> mappingsMap = (Map<String, Object>) relationships.get("mappings");
-            String collectionKey=null;
-            for(String key:collectionMap.keySet()){
-                collectionKey=key;
+            String collectionKey = null;
+            for (String key : collectionMap.keySet()) {
+                collectionKey = key;
             }
-            String mappingKey=null;
-            for(String key:mappingsMap.keySet()){
-                mappingKey=key;
+            String mappingKey = null;
+            for (String key : mappingsMap.keySet()) {
+                mappingKey = key;
             }
-            String relationShipMappingsKey=null;
-            for(String key:mappingsMap.keySet()){
-                relationShipMappingsKey=key;
+            String relationShipMappingsKey = null;
+            for (String key : mappingsMap.keySet()) {
+                relationShipMappingsKey = key;
             }
-            taskService.replaceRelationShipsKey(globalIdMap,content);
-            String afterCollectionKey=null;
-            for(String key:collectionMap.keySet()){
-                afterCollectionKey=key;
+            ParseParam param = new ParseParam()
+                    .withMultipartFile(mockMultipartFile)
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(userDetail);
+            param.setRelMigStr(rmJson);
+            param.setRelMigInfo(rmProject);
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            parseRelMigFile.replaceRelationShipsKey(globalIdMap, content);
+            String afterCollectionKey = null;
+            for (String key : collectionMap.keySet()) {
+                afterCollectionKey = key;
             }
-            String afterMappingKey=null;
-            for(String key:mappingsMap.keySet()){
-                afterMappingKey=key;
+            String afterMappingKey = null;
+            for (String key : mappingsMap.keySet()) {
+                afterMappingKey = key;
             }
-            String afterRelationShipMappingsKey=null;
-            for(String key:mappingsMap.keySet()){
-                afterRelationShipMappingsKey=key;
+            String afterRelationShipMappingsKey = null;
+            for (String key : mappingsMap.keySet()) {
+                afterRelationShipMappingsKey = key;
             }
-            assertNotEquals(collectionKey,afterCollectionKey);
-            assertNotEquals(afterMappingKey,mappingKey);
-            assertNotEquals(afterRelationShipMappingsKey,relationShipMappingsKey);
+            assertNotEquals(collectionKey, afterCollectionKey);
+            assertNotEquals(afterMappingKey, mappingKey);
+            assertNotEquals(afterRelationShipMappingsKey, relationShipMappingsKey);
         }
+
         @Test
         void testImportRmProject() throws IOException {
             CustomSqlService customSqlService = mock(CustomSqlService.class);
             taskService.setCustomSqlService(customSqlService);
             DateNodeService dataNodeService = mock(DateNodeService.class);
             taskService.setDateNodeService(dataNodeService);
-            MockMultipartFile mockMultipartFile = new MockMultipartFile("test.relmig", fileInputStream);
-            String s = new String(mockMultipartFile.getBytes());
-            Map<String, String> stringStringMap = taskService.parseTaskFromRm(s, "123", "123", userDetail);
-            TaskDto taskDto=null;
-            for(String s1: stringStringMap.keySet()){
+            ParseParam param = new ParseParam()
+                    .withMultipartFile(mockMultipartFile)
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(userDetail);
+            param.setRelMigStr(rmJson);
+            param.setRelMigInfo(new ObjectMapper().readValue(rmJson, HashMap.class));
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            Map<String, String> stringStringMap = parseRelMigFile.doParse("sourceConnectionId", "targetConnectionId", userDetail);
+            TaskDto taskDto = null;
+            for (String s1 : stringStringMap.keySet()) {
                 taskDto = JsonUtil.parseJsonUseJackson(stringStringMap.get(s1), TaskDto.class);
             }
-            try (MockedStatic<BeanUtils> beanUtilsMockedStatic = mockStatic(BeanUtils.class);MockedStatic<DataPermissionHelper> dataPermissionHelperMockedStatic = mockStatic(DataPermissionHelper.class)) {
-                BeanUtils.copyProperties(any(),any());
+            try (MockedStatic<BeanUtils> beanUtilsMockedStatic = mockStatic(BeanUtils.class); MockedStatic<DataPermissionHelper> dataPermissionHelperMockedStatic = mockStatic(DataPermissionHelper.class)) {
+                BeanUtils.copyProperties(any(), any());
                 TaskEntity taskEntity = taskService.convertToEntity(TaskEntity.class, taskDto);
-                when(taskRepository.importEntity(any(),any())).thenReturn(taskEntity);
+                when(taskRepository.importEntity(any(), any())).thenReturn(taskEntity);
                 MongoTemplate mongoTemplate = mock(MongoTemplate.class);
                 when(taskRepository.getMongoOperations()).thenReturn(mongoTemplate);
-                assertThrows(BizException.class,()->{taskService.importRmProject(mockMultipartFile,userDetail,false,new ArrayList<>(),"123","123");});
+                assertThrows(BizException.class, () -> {
+                    taskService.importRmProject(mockMultipartFile, userDetail, false, new ArrayList<>(), "123", "123");
+                });
             }
         }
+
         @Test
         void testGenProperties() throws IOException {
             URL resource = this.getClass().getClassLoader().getResource("EmployeeSchema.relmig");
             FileInputStream fileInputStream = new FileInputStream(resource.getFile());
             MockMultipartFile mockMultipartFile = new MockMultipartFile("EmployeeSchema.relmig", fileInputStream);
             String s = new String(mockMultipartFile.getBytes());
-            Map<String, String> stringStringMap = taskService.parseTaskFromRm(s, "123", "123", userDetail);
+            ParseParam param = new ParseParam()
+                    .withMultipartFile(mockMultipartFile)
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(userDetail);
+            param.setRelMigStr(s);
+            param.setRelMigInfo(new ObjectMapper().readValue(s, HashMap.class));
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            Map<String, String> stringStringMap = parseRelMigFile.doParse("sourceConnectionId", "targetConnectionId", userDetail);
             TaskDto taskDto = null;
             for (String key : stringStringMap.keySet()) {
                 System.out.println();
@@ -804,389 +904,650 @@ void testGenProperties() throws IOException {
             assertTrue(flag);
         }
     }
+
     @Nested
-    class ParentColumnsFindJoinKeysClass{
-        TaskRepository taskRepository=mock(TaskRepository.class);
-        TaskServiceImpl taskService=spy(new TaskServiceImpl(taskRepository));
+    class ParentColumnsFindJoinKeysClass {
+        TaskRepository taskRepository = mock(TaskRepository.class);
+        TaskServiceImpl taskService = spy(new TaskServiceImpl(taskRepository));
         Map<String, Object> parent;
         Map<String, Map<String, Map<String, Object>>> renameFields;
-
+        ParseRelMigFile parseRelMigFile;
 
         @BeforeEach
-        void beforeSetUp(){
+        void beforeSetUp() {
             parent = new HashMap<>();
             parent.put("rm_id", "rm_id -> eb1243b6-e7dc-4b84-b094-e719f9275512");
             parent.put("tableName", "Orders");
             renameFields = new HashMap<>();
 
-            Map<String,Map<String,Object>> orderFieldMap=new HashMap<>();
-            Map<String,Object> idAttrs=new HashMap<>();
+            Map<String, Map<String, Object>> orderFieldMap = new HashMap<>();
+            Map<String, Object> idAttrs = new HashMap<>();
             idAttrs.put("isPrimaryKey", false);
-            idAttrs.put("target","_id");
-            Map<String,Object> orderIdAttrs=new HashMap<>();
-            orderIdAttrs.put("isPrimaryKey",true);
-            orderIdAttrs.put("target","orderId");
-            Map<String,Object> shipViaAttrs=new HashMap<>();
-            shipViaAttrs.put("target","shipVia");
-            shipViaAttrs.put("isPrimaryKey",false);
-            orderFieldMap.put("ShipVia",shipViaAttrs);
-            orderFieldMap.put("_id",idAttrs);
-            orderFieldMap.put("OrderID",orderIdAttrs);
-
-            Map<String,Map<String,Object>> shipperFieldMap=new HashMap<>();
-            Map<String,Object> shipperIdAttrs=new HashMap<>();
-            shipperIdAttrs.put("target","shipperId");
-            shipperIdAttrs.put("isPrimaryKey",true);
-            shipperFieldMap.put("ShipperID",shipperIdAttrs);
-
-            renameFields.put("Shippers",shipperFieldMap);
-            renameFields.put("Orders",orderFieldMap);
+            idAttrs.put("target", "_id");
+            Map<String, Object> orderIdAttrs = new HashMap<>();
+            orderIdAttrs.put("isPrimaryKey", true);
+            orderIdAttrs.put("target", "orderId");
+            Map<String, Object> shipViaAttrs = new HashMap<>();
+            shipViaAttrs.put("target", "shipVia");
+            shipViaAttrs.put("isPrimaryKey", false);
+            orderFieldMap.put("ShipVia", shipViaAttrs);
+            orderFieldMap.put("_id", idAttrs);
+            orderFieldMap.put("OrderID", orderIdAttrs);
+
+            Map<String, Map<String, Object>> shipperFieldMap = new HashMap<>();
+            Map<String, Object> shipperIdAttrs = new HashMap<>();
+            shipperIdAttrs.put("target", "shipperId");
+            shipperIdAttrs.put("isPrimaryKey", true);
+            shipperFieldMap.put("ShipperID", shipperIdAttrs);
+
+            renameFields.put("Shippers", shipperFieldMap);
+            renameFields.put("Orders", orderFieldMap);
         }
+
         @DisplayName("test parent column have foreignKey,and foreignKey table is child table")
         @Test
-        void test1(){
+        void test1() {
             parent.put("targetPath", "");
-            Map<String, Map<String,String>> souceJoinKeyMapping=new HashMap<>();
-            Map<String, Map<String,String>> targetJoinKeyMapping=new HashMap<>();
-            List<Map<String, String>> joinKeys=new ArrayList<>();
-            Map<String, Object> parentColumns=new HashMap<>();
-            Map<String, Object> columnsAttrs=new HashMap<>();
-            Map<String,Object> foreignKeyAttrs=new HashMap<>();
-            foreignKeyAttrs.put("name","FK_Orders_Shippers");
-            foreignKeyAttrs.put("table","Shippers");
-            foreignKeyAttrs.put("column","ShipperID");
-            columnsAttrs.put("foreignKey",foreignKeyAttrs);
-            parentColumns.put("ShipVia",columnsAttrs);
-            taskService.parentColumnsFindJoinKeys(parent,renameFields,parentColumns,"Shippers",joinKeys, souceJoinKeyMapping, targetJoinKeyMapping);
-            assertEquals(1,joinKeys.size());
+            Map<String, Map<String, String>> souceJoinKeyMapping = new HashMap<>();
+            Map<String, Map<String, String>> targetJoinKeyMapping = new HashMap<>();
+            List<Map<String, String>> joinKeys = new ArrayList<>();
+            Map<String, Object> parentColumns = new HashMap<>();
+            Map<String, Object> columnsAttrs = new HashMap<>();
+            Map<String, Object> foreignKeyAttrs = new HashMap<>();
+            foreignKeyAttrs.put("name", "FK_Orders_Shippers");
+            foreignKeyAttrs.put("table", "Shippers");
+            foreignKeyAttrs.put("column", "ShipperID");
+            columnsAttrs.put("foreignKey", foreignKeyAttrs);
+            parentColumns.put("ShipVia", columnsAttrs);
+
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("");
+            param.setRelMigInfo(new HashMap<>());
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            parseRelMigFile.parentColumnsFindJoinKeys(parent, renameFields, parentColumns, "Shippers", joinKeys, souceJoinKeyMapping, targetJoinKeyMapping);
+            assertEquals(1, joinKeys.size());
             Map<String, String> stringStringMap = joinKeys.get(0);
             String sourceJoinKey = stringStringMap.get("source");
             String targetJoinKey = stringStringMap.get("target");
-            assertEquals("shipperId",sourceJoinKey);
-            assertEquals("shipVia",targetJoinKey);
+            assertEquals("shipperId", sourceJoinKey);
+            assertEquals("shipVia", targetJoinKey);
         }
+
         @DisplayName("test parent column have foreignKey,foreignKey table is child table and have targetPath")
         @Test
-        void test2(){
+        void test2() {
             parent.put("targetPath", "orders");
-            Map<String, Map<String,String>> souceJoinKeyMapping=new HashMap<>();
-            Map<String, Map<String,String>> targetJoinKeyMapping=new HashMap<>();
-            List<Map<String, String>> joinKeys=new ArrayList<>();
-            Map<String, Object> parentColumns=new HashMap<>();
-            Map<String, Object> columnsAttrs=new HashMap<>();
-            Map<String,Object> foreignKeyAttrs=new HashMap<>();
-            foreignKeyAttrs.put("name","FK_Orders_Shippers");
-            foreignKeyAttrs.put("table","Shippers");
-            foreignKeyAttrs.put("column","ShipperID");
-            columnsAttrs.put("foreignKey",foreignKeyAttrs);
-            parentColumns.put("ShipVia",columnsAttrs);
-            taskService.parentColumnsFindJoinKeys(parent,renameFields,parentColumns,"Shippers",joinKeys, souceJoinKeyMapping, targetJoinKeyMapping);
-            assertEquals(1,joinKeys.size());
+            Map<String, Map<String, String>> souceJoinKeyMapping = new HashMap<>();
+            Map<String, Map<String, String>> targetJoinKeyMapping = new HashMap<>();
+            List<Map<String, String>> joinKeys = new ArrayList<>();
+            Map<String, Object> parentColumns = new HashMap<>();
+            Map<String, Object> columnsAttrs = new HashMap<>();
+            Map<String, Object> foreignKeyAttrs = new HashMap<>();
+            foreignKeyAttrs.put("name", "FK_Orders_Shippers");
+            foreignKeyAttrs.put("table", "Shippers");
+            foreignKeyAttrs.put("column", "ShipperID");
+            columnsAttrs.put("foreignKey", foreignKeyAttrs);
+            parentColumns.put("ShipVia", columnsAttrs);
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            parseRelMigFile.parentColumnsFindJoinKeys(parent, renameFields, parentColumns, "Shippers", joinKeys, souceJoinKeyMapping, targetJoinKeyMapping);
+            assertEquals(1, joinKeys.size());
             Map<String, String> stringStringMap = joinKeys.get(0);
             String sourceJoinKey = stringStringMap.get("source");
             String targetJoinKey = stringStringMap.get("target");
-            assertEquals("shipperId",sourceJoinKey);
-            assertEquals("orders.shipVia",targetJoinKey);
+            assertEquals("shipperId", sourceJoinKey);
+            assertEquals("orders.shipVia", targetJoinKey);
         }
+
         @DisplayName("test parent column no have foreignKey")
         @Test
-        void test3(){
-            Map<String, Map<String,String>> souceJoinKeyMapping=new HashMap<>();
-            Map<String, Map<String,String>> targetJoinKeyMapping=new HashMap<>();
-            List<Map<String, String>> joinKeys=new ArrayList<>();
-            Map<String, Object> parentColumns=new HashMap<>();
-            Map<String, Object> columnsAttrs=new HashMap<>();
-            parentColumns.put("ShipVia",columnsAttrs);
-            taskService.parentColumnsFindJoinKeys(parent,renameFields,parentColumns,"Shippers",joinKeys, souceJoinKeyMapping, targetJoinKeyMapping);
-            assertEquals(0,joinKeys.size());
+        void test3() {
+            Map<String, Map<String, String>> souceJoinKeyMapping = new HashMap<>();
+            Map<String, Map<String, String>> targetJoinKeyMapping = new HashMap<>();
+            List<Map<String, String>> joinKeys = new ArrayList<>();
+            Map<String, Object> parentColumns = new HashMap<>();
+            Map<String, Object> columnsAttrs = new HashMap<>();
+            parentColumns.put("ShipVia", columnsAttrs);
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            parseRelMigFile.parentColumnsFindJoinKeys(parent, renameFields, parentColumns, "Shippers", joinKeys, souceJoinKeyMapping, targetJoinKeyMapping);
+            assertEquals(0, joinKeys.size());
         }
+
         @DisplayName("test parnet column table is not child table")
         @Test
-        void test4(){
+        void test4() {
             parent.put("targetPath", "");
-            Map<String, Map<String,String>> souceJoinKeyMapping=new HashMap<>();
-            Map<String, Map<String,String>> targetJoinKeyMapping=new HashMap<>();
-            List<Map<String, String>> joinKeys=new ArrayList<>();
-            Map<String, Object> parentColumns=new HashMap<>();
-            Map<String, Object> columnsAttrs=new HashMap<>();
-            Map<String,Object> foreignKeyAttrs=new HashMap<>();
-            foreignKeyAttrs.put("name","FK_Orders_Shippers");
-            foreignKeyAttrs.put("table","testTable");
-            foreignKeyAttrs.put("column","ShipperID");
-            columnsAttrs.put("foreignKey",foreignKeyAttrs);
-            parentColumns.put("ShipVia",columnsAttrs);
-            taskService.parentColumnsFindJoinKeys(parent,renameFields,parentColumns,"Shippers",joinKeys, souceJoinKeyMapping, targetJoinKeyMapping);
-            assertEquals(0,joinKeys.size());
+            Map<String, Map<String, String>> souceJoinKeyMapping = new HashMap<>();
+            Map<String, Map<String, String>> targetJoinKeyMapping = new HashMap<>();
+            List<Map<String, String>> joinKeys = new ArrayList<>();
+            Map<String, Object> parentColumns = new HashMap<>();
+            Map<String, Object> columnsAttrs = new HashMap<>();
+            Map<String, Object> foreignKeyAttrs = new HashMap<>();
+            foreignKeyAttrs.put("name", "FK_Orders_Shippers");
+            foreignKeyAttrs.put("table", "testTable");
+            foreignKeyAttrs.put("column", "ShipperID");
+            columnsAttrs.put("foreignKey", foreignKeyAttrs);
+            parentColumns.put("ShipVia", columnsAttrs);
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            parseRelMigFile.parentColumnsFindJoinKeys(parent, renameFields, parentColumns, "Shippers", joinKeys, souceJoinKeyMapping, targetJoinKeyMapping);
+            assertEquals(0, joinKeys.size());
         }
     }
+
     @Nested
-    class GetEmbeddedDocumentPathTest{
-        TaskRepository taskRepository=mock(TaskRepository.class);
-        TaskServiceImpl taskService=spy(new TaskServiceImpl(taskRepository));
+    class GetEmbeddedDocumentPathTest {
+        TaskRepository taskRepository = mock(TaskRepository.class);
+        TaskServiceImpl taskService = spy(new TaskServiceImpl(taskRepository));
+        ParseRelMigFile parseRelMigFile;
+
         @DisplayName("test parent path is empty string,use embeddedPath")
         @Test
-        void test1(){
-            Map<String,String> setting=new HashMap<>();
-            setting.put("embeddedPath","abc");
-            String targetPath = taskService.getEmbeddedDocumentPath("", setting);
-            assertEquals("abc",targetPath);
+        void test1() {
+            Map<String, Object> setting = new HashMap<>();
+            setting.put("embeddedPath", "abc");
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            String targetPath = parseRelMigFile.getEmbeddedDocumentPath("", setting);
+            assertEquals("abc", targetPath);
         }
+
         @DisplayName("test parent path is not empty string,embeddedPath is null")
         @Test
-        void test2(){
-            Map<String,String> setting=new HashMap<>();
-            String targetPath = taskService.getEmbeddedDocumentPath("parentPath", setting);
-            assertEquals("parentPath",targetPath);
+        void test2() {
+            Map<String, Object> setting = new HashMap<>();
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            String targetPath = parseRelMigFile.getEmbeddedDocumentPath("parentPath", setting);
+            assertEquals("parentPath", targetPath);
         }
+
         @DisplayName("test parent path is not empty string,embeddedPath is not null")
         @Test
-        void test3(){
-            Map<String,String> setting=new HashMap<>();
-            setting.put("embeddedPath","abc");
-            String targetPath = taskService.getEmbeddedDocumentPath("parentPath", setting);
-            assertEquals("parentPath.abc",targetPath);
+        void test3() {
+            Map<String, Object> setting = new HashMap<>();
+            setting.put("embeddedPath", "abc");
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            String targetPath = parseRelMigFile.getEmbeddedDocumentPath("parentPath", setting);
+            assertEquals("parentPath.abc", targetPath);
         }
+
         @DisplayName("test parent path is not empty string,embeddedPaht is empty str")
         @Test
-        void test4(){
-            Map<String,String> setting=new HashMap<>();
-            setting.put("embeddedPath","");
-            String targetPath = taskService.getEmbeddedDocumentPath("parentPath", setting);
-            assertEquals("parentPath",targetPath);
+        void test4() {
+            Map<String, Object> setting = new HashMap<>();
+            setting.put("embeddedPath", "");
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            String targetPath = parseRelMigFile.getEmbeddedDocumentPath("parentPath", setting);
+            assertEquals("parentPath", targetPath);
         }
     }
+
     @Nested
-    class RunningTaskNumWithProcessIdTest{
+    class RunningTaskNumWithProcessIdTest {
         @Test
-        void testRunningTaskNumWithProcessId(){
+        void testRunningTaskNumWithProcessId() {
             TaskRepository taskRepository = mock(TaskRepository.class);
             taskService = new TaskServiceImpl(taskRepository);
             long except = 5L;
             UserDetail userDetail = mock(UserDetail.class);
             when(taskRepository.count(Query.query(Criteria.where("agentId").is("111")
                     .and("is_deleted").ne(true).and("syncType").in(TaskDto.SYNC_TYPE_SYNC, TaskDto.SYNC_TYPE_MIGRATE)
-                    .and("status").nin(TaskDto.STATUS_DELETE_FAILED,TaskDto.STATUS_DELETING)
+                    .and("status").nin(TaskDto.STATUS_DELETE_FAILED, TaskDto.STATUS_DELETING)
                     .orOperator(Criteria.where("status").in(TaskDto.STATUS_RUNNING, TaskDto.STATUS_SCHEDULING, TaskDto.STATUS_WAIT_RUN),
                             Criteria.where("planStartDateFlag").is(true),
                             Criteria.where("crontabExpressionFlag").is(true)
                     )), userDetail)).thenReturn(except);
             long result = taskService.runningTaskNum("111", userDetail);
-            assertEquals(except,result);
+            assertEquals(except, result);
         }
     }
+
     @Nested
-    class GetNewNameMapTest{
-        TaskRepository taskRepository=mock(TaskRepository.class);
-        TaskServiceImpl taskService=spy(new TaskServiceImpl(taskRepository));
+    class GetNewNameMapTest {
+        ParseRelMigFile parseRelMigFile;
+        TaskRepository taskRepository = mock(TaskRepository.class);
+        TaskServiceImpl taskService = spy(new TaskServiceImpl(taskRepository));
+
         @DisplayName("test get newname map is pk")
         @Test
-        void test1(){
-            Map<String,Object> target=new HashMap<>();
-            target.put("name","employeeId");
-            target.put("included",true);
-            Map<String,Object> source=new HashMap<>();
-            source.put("name","EmployeeId");
-            source.put("isPrimaryKey",true);
-            Map<String, Object> newNameMap = taskService.getNewNameMap(target, source);
-            assertEquals("employeeId",newNameMap.get("target"));
-            assertEquals(true,newNameMap.get("isPrimaryKey"));
+        void test1() {
+            Map<String, Object> target = new HashMap<>();
+            target.put("name", "employeeId");
+            target.put("included", true);
+            Map<String, Object> source = new HashMap<>();
+            source.put("name", "EmployeeId");
+            source.put("isPrimaryKey", true);
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            Map<String, Object> newNameMap = parseRelMigFile.getNewNameMap(target, source);
+            assertEquals("employeeId", newNameMap.get("target"));
+            assertEquals(true, newNameMap.get("isPrimaryKey"));
         }
+
         @DisplayName("test get newname map is not pk")
         @Test
-        void test2(){
-            Map<String,Object> target=new HashMap<>();
-            target.put("name","employeeId");
-            target.put("included",true);
-            Map<String,Object> source=new HashMap<>();
-            source.put("name","EmployeeId");
-            source.put("isPrimaryKey",false);
-            Map<String, Object> newNameMap = taskService.getNewNameMap(target, source);
-            assertEquals("employeeId",newNameMap.get("target"));
-            assertEquals(false,newNameMap.get("isPrimaryKey"));
+        void test2() {
+            Map<String, Object> target = new HashMap<>();
+            target.put("name", "employeeId");
+            target.put("included", true);
+            Map<String, Object> source = new HashMap<>();
+            source.put("name", "EmployeeId");
+            source.put("isPrimaryKey", false);
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            Map<String, Object> newNameMap = parseRelMigFile.getNewNameMap(target, source);
+            assertEquals("employeeId", newNameMap.get("target"));
+            assertEquals(false, newNameMap.get("isPrimaryKey"));
         }
     }
+
     @Nested
-    class GetOperationTest{
-        TaskRepository taskRepository=mock(TaskRepository.class);
-        TaskServiceImpl taskService=spy(new TaskServiceImpl(taskRepository));
+    class GetOperationTest {
+        TaskRepository taskRepository = mock(TaskRepository.class);
+        TaskServiceImpl taskService = spy(new TaskServiceImpl(taskRepository));
+
         @DisplayName("test get deleteOperation")
         @Test
-        void test1(){
-            Map<String,Object> source=new HashMap<>();
-            source.put("name","EmployeeId");
-            source.put("isPrimaryKey",false);
-            Map<String, Object> deleteOperation = taskService.getDeleteOperation(source.get("name"), source.get("isPrimaryKey"));
-            assertEquals("EmployeeId",deleteOperation.get("field"));
-            assertEquals("REMOVE",deleteOperation.get("op"));
-            assertEquals("true",deleteOperation.get("operand"));
-            assertEquals("EmployeeId",deleteOperation.get("label"));
+        void test1() {
+            Map<String, Object> source = new HashMap<>();
+            source.put("name", "EmployeeId");
+            source.put("isPrimaryKey", false);
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            ParseRelMigFile parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            Map<String, Object> deleteOperation = parseRelMigFile.getDeleteOperation(source.get("name"), source.get("isPrimaryKey"));
+            assertEquals("EmployeeId", deleteOperation.get("field"));
+            assertEquals("REMOVE", deleteOperation.get("op"));
+            assertEquals("true", deleteOperation.get("operand"));
+            assertEquals("EmployeeId", deleteOperation.get("label"));
         }
+
         @DisplayName("test get renameOperation")
         @Test
-        void test2(){
-            Map<String,Object> target=new HashMap<>();
-            target.put("name","employeeId");
-            target.put("included",true);
-            Map<String,Object> source=new HashMap<>();
-            source.put("name","EmployeeId");
-            source.put("isPrimaryKey",false);
-            Map<String, Object> renameOperation = taskService.getRenameOperation(source.get("name"), target.get("name"));
-            assertEquals("EmployeeId",renameOperation.get("field"));
-            assertEquals("RENAME",renameOperation.get("op"));
-            assertEquals("employeeId",renameOperation.get("operand"));
+        void test2() {
+            Map<String, Object> target = new HashMap<>();
+            target.put("name", "employeeId");
+            target.put("included", true);
+            Map<String, Object> source = new HashMap<>();
+            source.put("name", "EmployeeId");
+            source.put("isPrimaryKey", false);
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            ParseRelMigFile parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            Map<String, Object> renameOperation = parseRelMigFile.getRenameOperation(source.get("name"), target.get("name"));
+            assertEquals("EmployeeId", renameOperation.get("field"));
+            assertEquals("RENAME", renameOperation.get("op"));
+            assertEquals("employeeId", renameOperation.get("operand"));
         }
     }
+
     @Nested
-    class AddProcessorNodeTest{
-        TaskRepository taskRepository=mock(TaskRepository.class);
-        TaskServiceImpl taskService=spy(new TaskServiceImpl(taskRepository));
+    class AddProcessorNodeTest {
+        TaskRepository taskRepository = mock(TaskRepository.class);
+        TaskServiceImpl taskService = spy(new TaskServiceImpl(taskRepository));
+
         @DisplayName("test add delete node")
         @Test
-        void test1(){
-            List<Map<String, Object>> nodes=new ArrayList<>();
-            List<Map<String, Object>> edges =new ArrayList<>();
+        void test1() {
+            List<Map<String, Object>> nodes = new ArrayList<>();
+            List<Map<String, Object>> edges = new ArrayList<>();
             Map<String, Object> deleteOperation = new HashMap<>();
-            List<Map<String, Object>> deleteOperationList=new ArrayList<>();
+            List<Map<String, Object>> deleteOperationList = new ArrayList<>();
             deleteOperation.put("id", UUID.randomUUID().toString().toLowerCase());
             deleteOperation.put("field", "CustomerId");
             deleteOperation.put("op", "REMOVE");
             deleteOperation.put("operand", "true");
             deleteOperation.put("label", "CustomerId");
             deleteOperationList.add(deleteOperation);
-            String sourceId = taskService.addDeleteNode("customer", deleteOperationList,  "souceId",nodes, edges);
-            assertNotEquals("souceId",sourceId);
-            assertEquals(1,nodes.size());
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            ParseRelMigFile parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            String sourceId = parseRelMigFile.addDeleteNode("customer", deleteOperationList, "souceId", nodes, edges);
+            assertNotEquals("souceId", sourceId);
+            assertEquals(1, nodes.size());
             Map<String, Object> nodeMap = nodes.get(0);
-            assertEquals("Delete customer",nodeMap.get("name"));
-            assertEquals("field_add_del_processor",nodeMap.get("type"));
+            assertEquals("Delete customer", nodeMap.get("name"));
+            assertEquals("field_add_del_processor", nodeMap.get("type"));
         }
+
         @DisplayName("test add rename node")
         @Test
-        void test2(){
-            List<Map<String, Object>> nodes=new ArrayList<>();
-            List<Map<String, Object>> edges =new ArrayList<>();
-            List<Map<String, Object>> fieldRenameOperationList=new ArrayList<>();
+        void test2() {
+            List<Map<String, Object>> nodes = new ArrayList<>();
+            List<Map<String, Object>> edges = new ArrayList<>();
+            List<Map<String, Object>> fieldRenameOperationList = new ArrayList<>();
             Map<String, Object> fieldRenameOperation = new HashMap<>();
             fieldRenameOperation.put("field", "CustomerId");
             fieldRenameOperation.put("op", "RENAME");
             fieldRenameOperation.put("operand", "customerId");
             fieldRenameOperationList.add(fieldRenameOperation);
-            String sourceId = taskService.addRenameNode("customer", fieldRenameOperationList, "souceId",nodes, edges);
-            assertNotEquals("souceId",sourceId);
-            assertEquals(1,nodes.size());
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            ParseRelMigFile parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            String sourceId = parseRelMigFile.addRenameNode("customer", fieldRenameOperationList, "souceId", nodes, edges);
+            assertNotEquals("souceId", sourceId);
+            assertEquals(1, nodes.size());
             Map<String, Object> nodeMap = nodes.get(0);
-            assertEquals("Rename customer",nodeMap.get("name"));
-            assertEquals("field_rename_processor",nodeMap.get("type"));
+            assertEquals("Rename customer", nodeMap.get("name"));
+            assertEquals("field_rename_processor", nodeMap.get("type"));
         }
+
         @Test
-        void test3(){
+        void test3() {
             List<Map<String, Object>> nodes = new ArrayList<>();
             List<Map<String, Object>> edges = new ArrayList<>();
             String script = "function process(){}";
             String declareScript = "retrun record";
-            String sourceId = taskService.addJSNode("customer", script, declareScript, nodes, "sourceId", edges);
-            assertNotEquals("souceId",sourceId);
-            assertEquals(1,nodes.size());
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            ParseRelMigFile parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            String sourceId = parseRelMigFile.addJSNode("customer", script, declareScript, nodes, "sourceId", edges);
+            assertNotEquals("souceId", sourceId);
+            assertEquals(1, nodes.size());
             Map<String, Object> nodeMap = nodes.get(0);
-            assertEquals("customer",nodeMap.get("name"));
-            assertEquals("js_processor",nodeMap.get("type"));
+            assertEquals("customer", nodeMap.get("name"));
+            assertEquals("js_processor", nodeMap.get("type"));
         }
     }
+
     @Nested
-    class RemoveDeleteOperationIfJoinKeyIsDeletedTest{
-        TaskRepository taskRepository=mock(TaskRepository.class);
-        TaskServiceImpl taskService=spy(new TaskServiceImpl(taskRepository));
-        List<Map<String, Object>> childDeleteOperationsList=new ArrayList<>();
-        List<Map<String, Object>> childRenameOperationsList =new ArrayList();
+    class RemoveDeleteOperationIfJoinKeyIsDeletedTest {
+        TaskRepository taskRepository = mock(TaskRepository.class);
+        TaskServiceImpl taskService = spy(new TaskServiceImpl(taskRepository));
+        List<Map<String, Object>> childDeleteOperationsList = new ArrayList<>();
+        List<Map<String, Object>> childRenameOperationsList = new ArrayList();
         Map<String, List<Map<String, Object>>> contentDeleteOperations = new HashMap<>();
-        Map<String, List<Map<String, Object>>> contentRenameOperations =new HashMap<>();
+        Map<String, List<Map<String, Object>>> contentRenameOperations = new HashMap<>();
+
         @BeforeEach
-        void beforeSetUp(){
-            Map<String, Object> deleteOperations =new HashMap<>();
-            deleteOperations.put("op","REMOVE");
+        void beforeSetUp() {
+            Map<String, Object> deleteOperations = new HashMap<>();
+            deleteOperations.put("op", "REMOVE");
             deleteOperations.put("field", "OrderID");
-            deleteOperations.put("label" ,"OrderID");
-            deleteOperations.put("operand","true");
+            deleteOperations.put("label", "OrderID");
+            deleteOperations.put("operand", "true");
             deleteOperations.put("id", UUID.randomUUID().toString().toLowerCase());
             childDeleteOperationsList.add(deleteOperations);
-            Map<String,Object> renameOperations =new HashMap<>();
-            renameOperations.put("op","RENAME");
-            renameOperations.put("field" , "UnitPrice");
+            Map<String, Object> renameOperations = new HashMap<>();
+            renameOperations.put("op", "RENAME");
+            renameOperations.put("field", "UnitPrice");
             renameOperations.put("id", UUID.randomUUID().toString().toLowerCase());
-            renameOperations.put("operand","unitPrice");
+            renameOperations.put("operand", "unitPrice");
             childRenameOperationsList.add(renameOperations);
-            contentDeleteOperations.put("childId",childDeleteOperationsList);
-            contentRenameOperations.put("childId",childRenameOperationsList);
+            contentDeleteOperations.put("childId", childDeleteOperationsList);
+            contentRenameOperations.put("childId", childRenameOperationsList);
         }
+
         @Test
-        void test1(){
-            Map<String,Map<String, String>> sourceJoinKeyMapping =new HashMap<>();
-            Map<String,String> newFieldMap=new HashMap<>();
-            newFieldMap.put("source","OrderID");
-            newFieldMap.put("target","orderId");
-            sourceJoinKeyMapping.put("orderId",newFieldMap);
-            taskService.addRenameOpIfDeleteOpHasJoinKey(contentDeleteOperations,contentRenameOperations,"childId",sourceJoinKeyMapping,"orderId");
-            assertEquals(0,childDeleteOperationsList.size());
-            assertEquals(2,childRenameOperationsList.size());
+        void test1() {
+            Map<String, Map<String, String>> sourceJoinKeyMapping = new HashMap<>();
+            Map<String, String> newFieldMap = new HashMap<>();
+            newFieldMap.put("source", "OrderID");
+            newFieldMap.put("target", "orderId");
+            sourceJoinKeyMapping.put("orderId", newFieldMap);
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            ParseRelMigFile parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            parseRelMigFile.addRenameOpIfDeleteOpHasJoinKey(contentDeleteOperations, contentRenameOperations, "childId", sourceJoinKeyMapping, "orderId");
+            assertEquals(0, childDeleteOperationsList.size());
+            assertEquals(2, childRenameOperationsList.size());
         }
+
         @Test
-        void test2(){
-            Map<String,Map<String, String>> sourceJoinKeyMapping =new HashMap<>();
-            Map<String,String> newFieldMap=new HashMap<>();
-            newFieldMap.put("source","OrderId");
-            newFieldMap.put("target","orderId");
-            sourceJoinKeyMapping.put("productId",newFieldMap);
-            taskService.addRenameOpIfDeleteOpHasJoinKey(contentDeleteOperations,contentRenameOperations,"childId",sourceJoinKeyMapping,"productId");
-            assertEquals(1,childDeleteOperationsList.size());
-            assertEquals(1,childRenameOperationsList.size());
+        void test2() {
+            Map<String, Map<String, String>> sourceJoinKeyMapping = new HashMap<>();
+            Map<String, String> newFieldMap = new HashMap<>();
+            newFieldMap.put("source", "OrderId");
+            newFieldMap.put("target", "orderId");
+            sourceJoinKeyMapping.put("productId", newFieldMap);
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            ParseRelMigFile parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            parseRelMigFile.addRenameOpIfDeleteOpHasJoinKey(contentDeleteOperations, contentRenameOperations, "childId", sourceJoinKeyMapping, "productId");
+            assertEquals(1, childDeleteOperationsList.size());
+            assertEquals(1, childRenameOperationsList.size());
         }
     }
+
     @Nested
-    class RemoveDeleteOperationTest{
-        TaskRepository taskRepository=mock(TaskRepository.class);
-        TaskServiceImpl taskService=spy(new TaskServiceImpl(taskRepository));
-        List<Map<String, Object>> deleteOperationsList=new ArrayList<>();
+    class RemoveDeleteOperationTest {
+        TaskRepository taskRepository = mock(TaskRepository.class);
+        TaskServiceImpl taskService = spy(new TaskServiceImpl(taskRepository));
+        List<Map<String, Object>> deleteOperationsList = new ArrayList<>();
+
         @BeforeEach
-        void beforeSetUp(){
-            Map<String, Object> deleteOperations =new HashMap<>();
-            deleteOperations.put("op","REMOVE");
+        void beforeSetUp() {
+            Map<String, Object> deleteOperations = new HashMap<>();
+            deleteOperations.put("op", "REMOVE");
             deleteOperations.put("field", "OrderID");
-            deleteOperations.put("label" ,"OrderID");
-            deleteOperations.put("operand","true");
+            deleteOperations.put("label", "OrderID");
+            deleteOperations.put("operand", "true");
             deleteOperations.put("id", UUID.randomUUID().toString().toLowerCase());
             deleteOperationsList.add(deleteOperations);
         }
+
         @DisplayName("test removeDeleteOperation when joinkey in deleteOperation")
         @Test
-        void test1(){
-            Map<String,Map<String, String>> sourceJoinKeyMapping =new HashMap<>();
-            Map<String,String> newFieldMap=new HashMap<>();
-            newFieldMap.put("source","OrderID");
-            newFieldMap.put("target","orderId");
-            sourceJoinKeyMapping.put("orderId",newFieldMap);
-            boolean flag = taskService.removeDeleteOperation(deleteOperationsList, sourceJoinKeyMapping, "orderId");
-            assertEquals(true,flag);
+        void test1() {
+            Map<String, Map<String, String>> sourceJoinKeyMapping = new HashMap<>();
+            Map<String, String> newFieldMap = new HashMap<>();
+            newFieldMap.put("source", "OrderID");
+            newFieldMap.put("target", "orderId");
+            sourceJoinKeyMapping.put("orderId", newFieldMap);
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            ParseRelMigFile parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            boolean flag = parseRelMigFile.removeDeleteOperation(deleteOperationsList, sourceJoinKeyMapping, "orderId");
+            assertEquals(true, flag);
         }
+
         @DisplayName("test removeDeleteOperation when joinkey not in deleteOperation")
         @Test
-        void test2(){
-            Map<String,Map<String, String>> sourceJoinKeyMapping =new HashMap<>();
-            Map<String,String> newFieldMap=new HashMap<>();
-            newFieldMap.put("source","OrderId");
-            newFieldMap.put("target","orderId");
-            sourceJoinKeyMapping.put("productId",newFieldMap);
-            boolean flag = taskService.removeDeleteOperation(deleteOperationsList, sourceJoinKeyMapping, "productId");
-            assertEquals(false,flag);
+        void test2() {
+            Map<String, Map<String, String>> sourceJoinKeyMapping = new HashMap<>();
+            Map<String, String> newFieldMap = new HashMap<>();
+            newFieldMap.put("source", "OrderId");
+            newFieldMap.put("target", "orderId");
+            sourceJoinKeyMapping.put("productId", newFieldMap);
+            ParseParam param = new ParseParam()
+                    .withSink("sink")
+                    .withSource("source")
+                    .withUser(null);
+            param.setRelMigStr("{}");
+            param.setRelMigInfo(new HashMap<>());
+            ParseRelMigFile parseRelMigFile = new ParseRelMigFile(param) {
+                @Override
+                public List<TaskDto> parse() {
+                    return null;
+                }
+            };
+            boolean flag = parseRelMigFile.removeDeleteOperation(deleteOperationsList, sourceJoinKeyMapping, "productId");
+            assertEquals(false, flag);
         }
     }
 
     @Nested
-    class ReNewNotSendMqTest{
-        TaskRepository taskRepository=mock(TaskRepository.class);
-        TaskServiceImpl taskService=spy(new TaskServiceImpl(taskRepository));
+    class ReNewNotSendMqTest {
+        TaskRepository taskRepository = mock(TaskRepository.class);
+        TaskServiceImpl taskService = spy(new TaskServiceImpl(taskRepository));
+
         @DisplayName("test errorEvents is null")
         @Test
-        void test(){
+        void test() {
             TaskDto taskDto = new TaskDto();
             taskDto.setDag(mock(DAG.class));
             UpdateResult updateResult = new UpdateResult() {
@@ -1210,12 +1571,12 @@ public BsonValue getUpsertedId() {
                     return null;
                 }
             };
-            when(taskRepository.update(any(Query.class),any(Update.class),any())).thenAnswer(invocationOnMock -> {
+            when(taskRepository.update(any(Query.class), any(Update.class), any())).thenAnswer(invocationOnMock -> {
                 Update update = invocationOnMock.getArgument(1);
                 Assertions.assertNull(update.getUpdateObject().get("errorEvents"));
                 return updateResult;
             });
-            taskService.renewNotSendMq(taskDto,mock(UserDetail.class));
+            taskService.renewNotSendMq(taskDto, mock(UserDetail.class));
         }
 
     }
diff --git a/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseBaseVersionRelMigImplTest.java b/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseBaseVersionRelMigImplTest.java
new file mode 100644
index 0000000000..287c9154ce
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseBaseVersionRelMigImplTest.java
@@ -0,0 +1,105 @@
+package com.tapdata.tm.task.service.batchin;
+
+import com.tapdata.tm.base.exception.BizException;
+import com.tapdata.tm.commons.task.dto.TaskDto;
+import com.tapdata.tm.config.security.UserDetail;
+import com.tapdata.tm.task.service.batchin.entity.ParseParam;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class ParseBaseVersionRelMigImplTest {
+    @Test
+    void testNew() {
+        Assertions.assertDoesNotThrow(() -> new ParseBaseVersionRelMigImpl(new ParseParam()));
+    }
+    @Nested
+    class ParseBaseVersionRelMigTest {
+        ParseBaseVersionRelMigImpl parseBaseVersionRelMig;
+        ParseParam parseParam;
+        Map<String, String> tasks;
+        @BeforeEach
+        void init() {
+            tasks = new HashMap<>();
+            tasks.put("key", "{}");
+
+            parseParam = mock(ParseParam.class);
+            when(parseParam.getSource()).thenReturn("source");
+            when(parseParam.getSink()).thenReturn("sink");
+            when(parseParam.getUser()).thenReturn(mock(UserDetail.class));
+            parseBaseVersionRelMig = mock(ParseBaseVersionRelMigImpl.class);
+            parseBaseVersionRelMig.param = parseParam;
+
+            when(parseBaseVersionRelMig.doParse(anyString(), anyString(), any(UserDetail.class))).thenReturn(tasks);
+            when(parseBaseVersionRelMig.parse()).thenCallRealMethod();
+        }
+
+        @Test
+        void testNormal() {
+            Assertions.assertDoesNotThrow(() -> {
+                try {
+                    List<TaskDto> parse = parseBaseVersionRelMig.parse();
+                    Assertions.assertNotNull(parse);
+                    Assertions.assertEquals(1, parse.size());
+                } catch (Exception e) {
+                    throw e;
+                }
+            });
+            verify(parseBaseVersionRelMig).doParse(anyString(), anyString(), any(UserDetail.class));
+            verify(parseParam).getSource();
+            verify(parseParam).getSink();
+            verify(parseParam).getUser();
+        }
+        @Test
+        void testAfterDoParseResultIsNull() {
+            when(parseBaseVersionRelMig.doParse(anyString(), anyString(), any(UserDetail.class))).thenReturn(null);
+            Assertions.assertDoesNotThrow(() -> {
+                try {
+                    List<TaskDto> parse = parseBaseVersionRelMig.parse();
+                    Assertions.assertEquals(ArrayList.class.getName(), parse.getClass().getName());
+                    Assertions.assertNotNull(parse);
+                    Assertions.assertEquals(0, parse.size());
+                } catch (Exception e) {
+                    throw e;
+                }
+            });
+            verify(parseBaseVersionRelMig).doParse(anyString(), anyString(), any(UserDetail.class));
+            verify(parseParam).getSource();
+            verify(parseParam).getSink();
+            verify(parseParam).getUser();
+        }
+        @Test
+        void testThrowExceptionWhenDoParse() {
+            when(parseBaseVersionRelMig.doParse(anyString(), anyString(), any(UserDetail.class))).thenAnswer(a -> {
+                throw new Exception("failed");
+            });
+            Assertions.assertThrows(BizException.class, () -> {
+                try {
+                    List<TaskDto> parse = parseBaseVersionRelMig.parse();
+                    Assertions.assertEquals(ArrayList.class.getName(), parse.getClass().getName());
+                    Assertions.assertNotNull(parse);
+                    Assertions.assertEquals(0, parse.size());
+                } catch (BizException e) {
+                    Assertions.assertEquals("relMig.parse.failed", e.getErrorCode());
+                    throw e;
+                }
+            });
+            verify(parseBaseVersionRelMig).doParse(anyString(), anyString(), any(UserDetail.class));
+            verify(parseParam).getSource();
+            verify(parseParam).getSink();
+            verify(parseParam).getUser();
+        }
+    }
+}
\ No newline at end of file
diff --git a/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseRelMig13OrMoreImplTest.java b/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseRelMig13OrMoreImplTest.java
new file mode 100644
index 0000000000..dac0bf065f
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseRelMig13OrMoreImplTest.java
@@ -0,0 +1,78 @@
+package com.tapdata.tm.task.service.batchin;
+
+
+import com.tapdata.tm.task.service.batchin.constant.KeyWords;
+import com.tapdata.tm.task.service.batchin.dto.TablePathInfo;
+import com.tapdata.tm.task.service.batchin.entity.ParseParam;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class ParseRelMig13OrMoreImplTest {
+    ParseRelMig13OrMoreImpl parseRelMig13OrMore;
+
+    @BeforeEach
+    void init() {
+        parseRelMig13OrMore = mock(ParseRelMig13OrMoreImpl.class);
+    }
+
+    @Test
+    void testNew() {
+        Assertions.assertDoesNotThrow(() -> new ParseRelMig13OrMoreImpl(new ParseParam()));
+    }
+    @Nested
+    class GetTablePathInfoTest {
+        Map<String, Object> contentMapping;
+        Map<String, Object> content;
+        Map<String, Object> tables;
+        Map<String, Object> tablePath;
+        Map<String, Object> tableInfo;
+        @BeforeEach
+        void init() {
+            contentMapping = mock(Map.class);
+            parseRelMig13OrMore.project = mock(Map.class);
+            content = mock(Map.class);
+            tables = mock(Map.class);
+            tablePath = mock(Map.class);
+            tableInfo = mock(Map.class);
+            when(parseRelMig13OrMore.getFromMap(contentMapping, KeyWords.TABLE)).thenReturn("tableId");
+            when(parseRelMig13OrMore.getFromMap(parseRelMig13OrMore.project, KeyWords.CONTENT)).thenReturn(content);
+            when(parseRelMig13OrMore.getFromMap(content, KeyWords.TABLES)).thenReturn(tables);
+            when(parseRelMig13OrMore.getFromMap(tables, "tableId")).thenReturn(tablePath);
+            when(parseRelMig13OrMore.getFromMap(tablePath, KeyWords.PATH)).thenReturn(tableInfo);
+            when(tableInfo.get(KeyWords.DATABASE)).thenReturn("database");
+            when(tableInfo.get(KeyWords.SCHEMA)).thenReturn("schema");
+            when(tableInfo.get(KeyWords.TABLE)).thenReturn("table");
+            when(parseRelMig13OrMore.parseMap(any())).thenCallRealMethod();
+            when(parseRelMig13OrMore.getTablePathInfo(contentMapping)).thenCallRealMethod();
+        }
+        @Test
+        void testNormal() {
+            TablePathInfo tablePathInfo = parseRelMig13OrMore.getTablePathInfo(contentMapping);
+            Assertions.assertNotNull(tablePathInfo);
+            Assertions.assertEquals("table", tablePathInfo.getTable());
+            Assertions.assertEquals("schema", tablePathInfo.getSchema());
+            Assertions.assertEquals("database", tablePathInfo.getDatabase());
+            verify(parseRelMig13OrMore).getFromMap(contentMapping, KeyWords.TABLE);
+            verify(parseRelMig13OrMore).getFromMap(parseRelMig13OrMore.project, KeyWords.CONTENT);
+            verify(parseRelMig13OrMore).getFromMap(content, KeyWords.TABLES);
+            verify(parseRelMig13OrMore).getFromMap(tables, "tableId");
+            verify(parseRelMig13OrMore).getFromMap(tablePath, KeyWords.PATH);
+            verify(tableInfo).get(KeyWords.DATABASE);
+            verify(tableInfo).get(KeyWords.SCHEMA);
+            verify(tableInfo).get(KeyWords.TABLE);
+            verify(parseRelMig13OrMore, times(4)).parseMap(any());
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseRelMigFileTest.java b/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseRelMigFileTest.java
new file mode 100644
index 0000000000..cd09025cd0
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseRelMigFileTest.java
@@ -0,0 +1,58 @@
+package com.tapdata.tm.task.service.batchin;
+
+
+import com.tapdata.tm.commons.task.dto.TaskDto;
+import com.tapdata.tm.task.service.batchin.entity.ParseParam;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.mockito.Mockito.mock;
+
+class ParseRelMigFileTest {
+    @Test
+    void testNew() {
+        Assertions.assertDoesNotThrow(() -> new ParseRelMigFile(new ParseParam()) {
+            @Override
+            public List<TaskDto> parse() {
+                return null;
+            }
+        });
+    }
+    @Nested
+    class ParseRelMigFileAbstractTest {
+        Map<String, Object> relMigInfo;
+        String version;
+        Map<String, Object> project;
+        Map<String, Object> schema;
+        List<Map<String, Object>> queries;
+        ParseParam param;
+        Map<String, Object> content;
+        ParseRelMigFile parseRelMigFile;
+        @BeforeEach
+        void init() {
+            parseRelMigFile = mock(ParseRelMigFile.class);
+            relMigInfo = new HashMap<>();
+            version = "1.2.0";
+            project = new HashMap<>();
+            schema = new HashMap<>();
+            queries = new ArrayList<>();
+            param = new ParseParam();
+            content = new HashMap<>();
+            parseRelMigFile.relMigInfo = relMigInfo;
+            parseRelMigFile.version = version;
+            parseRelMigFile.project = project;
+            parseRelMigFile.schema = schema;
+            parseRelMigFile.queries = queries;
+            parseRelMigFile.param = param;
+            parseRelMigFile.content = content;
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseRelMigTest.java b/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseRelMigTest.java
new file mode 100644
index 0000000000..1f55daa73e
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseRelMigTest.java
@@ -0,0 +1,75 @@
+package com.tapdata.tm.task.service.batchin;
+
+import com.tapdata.tm.base.exception.BizException;
+import com.tapdata.tm.config.security.UserDetail;
+import com.tapdata.tm.task.service.batchin.entity.ParseParam;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+
+class ParseRelMigTest {
+    @Test
+    void testParams() {
+        Assertions.assertEquals("processorThreadNum", ParseRelMig.PROCESSOR_THREAD_NUM);
+        Assertions.assertEquals("catalog", ParseRelMig.CATALOG);
+        Assertions.assertEquals("elementType", ParseRelMig.ELEMENT_TYPE);
+        Assertions.assertEquals("processor", ParseRelMig.PROCESSOR);
+        Assertions.assertEquals("rm_id", ParseRelMig.RM_ID);
+    }
+
+    @Nested
+    class testRedirect{
+        ParseParam param;
+        MultipartFile multipartFile;
+        @BeforeEach
+        void init() {
+            multipartFile = mock(MultipartFile.class);
+            param = new ParseParam()
+                    .withUser(mock(UserDetail.class))
+                    .withSource("source")
+                    .withSink("sink")
+                    .withMultipartFile(multipartFile);
+        }
+        @Test
+        void testLowerVersion() throws Exception {
+            when(multipartFile.getBytes()).thenReturn("{\"version\":\"1.2.0\"}".getBytes());
+            try(MockedStatic<ParseRelMig> prm = Mockito.mockStatic(ParseRelMig.class)) {
+                prm.when(() -> ParseRelMig.redirect(param)).thenCallRealMethod();
+                ParseRelMig redirect = ParseRelMig.redirect(param);
+                Assertions.assertNotNull(redirect);
+                Assertions.assertEquals(ParseBaseVersionRelMigImpl.class.getName(), redirect.getClass().getName());
+            }
+        }
+        @Test
+        void testHeightVersion() throws Exception {
+            when(multipartFile.getBytes()).thenReturn("{\"version\":\"1.3.0\"}".getBytes());
+            try(MockedStatic<ParseRelMig> prm = Mockito.mockStatic(ParseRelMig.class)) {
+                prm.when(() -> ParseRelMig.redirect(param)).thenCallRealMethod();
+                ParseRelMig redirect = ParseRelMig.redirect(param);
+                Assertions.assertNotNull(redirect);
+                Assertions.assertEquals(ParseRelMig13OrMoreImpl.class.getName(), redirect.getClass().getName());
+            }
+        }
+        @Test
+        void testThrow() throws IOException {
+            when(multipartFile.getBytes()).thenAnswer(a -> {
+                throw new Exception("failed");
+            });
+            try(MockedStatic<ParseRelMig> prm = Mockito.mockStatic(ParseRelMig.class)) {
+                prm.when(() -> ParseRelMig.redirect(param)).thenCallRealMethod();
+                Assertions.assertThrows(BizException.class, () -> ParseRelMig.redirect(param));
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseUnKnowVersionRelMigFileTest.java b/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseUnKnowVersionRelMigFileTest.java
new file mode 100644
index 0000000000..2adf2da16c
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/task/service/batchin/ParseUnKnowVersionRelMigFileTest.java
@@ -0,0 +1,30 @@
+package com.tapdata.tm.task.service.batchin;
+
+
+import com.tapdata.tm.base.exception.BizException;
+import com.tapdata.tm.task.service.batchin.constant.KeyWords;
+import com.tapdata.tm.task.service.batchin.entity.ParseParam;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+class ParseUnKnowVersionRelMigFileTest {
+    @Test
+    void testUnSupport() {
+        ParseParam param = new ParseParam();
+        Map<String, Object> info = new HashMap<>();
+        info.put(KeyWords.VERSION, "1.3.1");
+        param.setRelMigInfo(info);
+        ParseUnKnowVersionRelMigFile parseUnKnowVersionRelMigFile = new ParseUnKnowVersionRelMigFile(param);
+        Assertions.assertThrows(BizException.class, () -> {
+            try {
+                parseUnKnowVersionRelMigFile.parse();
+            } catch (BizException e) {
+                Assertions.assertEquals("relMig.parse.unSupport", e.getErrorCode());
+                throw e;
+            }
+        });
+    }
+}
\ No newline at end of file
diff --git a/manager/tm/src/test/java/com/tapdata/tm/worker/service/WorkerServiceTest.java b/manager/tm/src/test/java/com/tapdata/tm/worker/service/WorkerServiceTest.java
index 97fa140e89..dec422c10a 100644
--- a/manager/tm/src/test/java/com/tapdata/tm/worker/service/WorkerServiceTest.java
+++ b/manager/tm/src/test/java/com/tapdata/tm/worker/service/WorkerServiceTest.java
@@ -23,8 +23,7 @@
 import com.tapdata.tm.worker.repository.WorkerRepository;
 import com.tapdata.tm.worker.vo.CalculationEngineVo;
 import org.bson.BsonValue;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.*;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 import org.springframework.data.mongodb.core.query.Criteria;
@@ -32,10 +31,9 @@
 import org.springframework.data.mongodb.core.query.Update;
 import org.springframework.test.util.ReflectionTestUtils;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -281,4 +279,32 @@ void test_getProcessInfo(){
             assertEquals(1,result.get("test").getRunningNum());
         }
     }
+    @Nested
+    class GetWorkerCurrentTimeTest{
+        @DisplayName("Test main process")
+        @Test
+        void test() throws ParseException {
+            String date = "2024-04-26 11:00:00";
+            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            List<Worker> workerList = new ArrayList<>();
+            Worker worker = new Worker();
+            worker.setWorkerDate(simpleDateFormat.parse(date));
+            workerList.add(worker);
+            doReturn(workerList).when(workerService).findAvailableAgent(any());
+            String result = workerService.getWorkerCurrentTime(mock(UserDetail.class));
+            Assertions.assertEquals(date,result);
+        }
+
+        @DisplayName("workerDate is null")
+        @Test
+        void test1(){
+            List<Worker> workerList = new ArrayList<>();
+            Worker worker = new Worker();
+            workerList.add(worker);
+            doReturn(workerList).when(workerService).findAvailableAgent(any());
+            String result = workerService.getWorkerCurrentTime(mock(UserDetail.class));
+            Assertions.assertNotNull(result);
+        }
+
+    }
 }