Repository: tapdata/tapdata (language: python)

Issue:
TAP-4706:fix IllegalDateField rename error



Discussion / hints:

Fix patch:
diff --git a/RELEASE b/RELEASE
index 42e0c35cb4..8139c6f18e 100644
--- a/RELEASE
+++ b/RELEASE
@@ -1,2 +1,2 @@
-LST: v3.11.0
-DEV: v3.12.0-dev
\ No newline at end of file
+LST: v3.12.0
+DEV: v3.13.0-dev
\ No newline at end of file
diff --git a/build/etc/logback.xml b/build/etc/logback.xml
index ca69fdcd8a..8d3234f28b 100644
--- a/build/etc/logback.xml
+++ b/build/etc/logback.xml
@@ -24,9 +24,9 @@
 			</timeBasedFileNamingAndTriggeringPolicy>
 			<maxHistory>7</maxHistory>
 		</rollingPolicy>
-		<encoder>
+		<layout class="com.tapdata.tm.utils.CustomPatternLayout">
 			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%contextName] [%thread] %-5level %logger{36} - %msg%n</pattern>
-		</encoder>
+		</layout>
 	</appender>
 
 	<!-- 异步输出 -->
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/Application.java b/iengine/iengine-app/src/main/java/io/tapdata/Application.java
index 5edf54a14d..1da596847d 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/Application.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/Application.java
@@ -17,9 +17,11 @@
 import io.tapdata.entity.logger.TapLogger;
 import io.tapdata.entity.utils.InstanceFactory;
 import io.tapdata.flow.engine.V2.schedule.TapdataTaskScheduler;
+import io.tapdata.log.CustomPatternLayout;
 import io.tapdata.observable.logging.util.LogUtil;
 import io.tapdata.pdk.core.runtime.TapRuntime;
 import io.tapdata.pdk.core.utils.CommonUtils;
+import org.apache.commons.collections4.MapUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.LogManager;
@@ -51,7 +53,6 @@
 import org.springframework.stereotype.Component;
 import org.yaml.snakeyaml.Yaml;
 
-import javax.script.ScriptEngine;
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
@@ -250,7 +251,8 @@ public TaskScheduler taskScheduler() {
 	}
 
 	protected static void addRollingFileAppender(String tapdataWorkDir) {
-		logger.info("Get oem type from evn: {}", OEMReplaceUtil.oemType());
+		String oemType = OEMReplaceUtil.oemType();
+		logger.info("Get oem type from evn: {}", oemType);
 		Level defaultLogLevel = Level.INFO;
 		String debug = System.getenv("DEBUG");
 		if ("true".equalsIgnoreCase(debug)) {
@@ -277,9 +279,6 @@ protected static void addRollingFileAppender(String tapdataWorkDir) {
 
 		LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
 		org.apache.logging.log4j.core.config.Configuration config = ctx.getConfiguration();
-		PatternLayout patternLayout = PatternLayout.newBuilder()
-				.withPattern("[%-5level] %date{yyyy-MM-dd HH:mm:ss.SSS} %X{taskId} [%t] %c{1} - %msg%n")
-				.build();
 
 		CompositeTriggeringPolicy compositeTriggeringPolicy = LogUtil.getCompositeTriggeringPolicy(DEFAULT_LOG_SAVE_SIZE);
 		String glob=getFileNameAfterOem("tapdata-agent-*.log.*.gz");
@@ -292,14 +291,23 @@ protected static void addRollingFileAppender(String tapdataWorkDir) {
 				.withConfig(config).build();
 
 		JetExceptionFilter jetExceptionFilter = new JetExceptionFilter.TapLogBuilder().build();
-		RollingFileAppender rollingFileAppender = RollingFileAppender.newBuilder()
+
+		PatternLayout patternLayout = PatternLayout.newBuilder().withPattern("[%-5level] %date{yyyy-MM-dd HH:mm:ss.SSS} %X{taskId} [%t] %c{1} - %msg%n").build();
+		RollingFileAppender.Builder<?> rollingFileAppenderBuilder = RollingFileAppender.newBuilder()
 				.setName(ROLLING_FILE_APPENDER)
 				.withFileName(logsPath + getFileNameAfterOem("/tapdata-agent.log"))
 				.withFilePattern(logsPath + getFileNameAfterOem("/tapdata-agent-%i.log.%d{yyyyMMdd}.gz"))
-				.setLayout(patternLayout)
 				.withPolicy(compositeTriggeringPolicy)
-				.withStrategy(strategy)
-				.build();
+				.withStrategy(strategy);
+		Map<String, Object> oemConfigMap = OEMReplaceUtil.getOEMConfigMap("log/replace.json");
+		if (MapUtils.isEmpty(oemConfigMap)) {
+			rollingFileAppenderBuilder.setLayout(patternLayout);
+		} else {
+			CustomPatternLayout customPatternLayout = CustomPatternLayout.newBuilder()
+					.withPattern("[%-5level] %date{yyyy-MM-dd HH:mm:ss.SSS} %X{taskId} [%t] %c{1} - %msg%n").build();
+			rollingFileAppenderBuilder.setLayout(customPatternLayout);
+		}
+		RollingFileAppender rollingFileAppender = rollingFileAppenderBuilder.build();
 		rollingFileAppender.addFilter(jetExceptionFilter);
 		config.addAppender(rollingFileAppender);
 		LoggerConfig rootLoggerConfig = config.getRootLogger();
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/error/TaskMergeProcessorExCode_16.java b/iengine/iengine-app/src/main/java/io/tapdata/error/TaskMergeProcessorExCode_16.java
index aa464bb919..f06415c1d8 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/error/TaskMergeProcessorExCode_16.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/error/TaskMergeProcessorExCode_16.java
@@ -343,4 +343,11 @@ public interface TaskMergeProcessorExCode_16 {
 			describeCN = "反查数据的CompletableFuture列表不能为空"
 	)
 	String LOOKUP_COMPLETABLE_FUTURE_LIST_IS_NULL = "16061";
+
+
+	@TapExCode(
+			describe = "Init Cache failed",
+			describeCN = "初始化缓存失败"
+	)
+	String INIT_CACHE_FAILED = "16062";
 }
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 48672266e1..bbc16d7339 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
@@ -41,6 +41,9 @@
 import io.tapdata.entity.event.dml.TapRecordEvent;
 import io.tapdata.entity.event.dml.TapUpdateRecordEvent;
 import io.tapdata.entity.schema.TapTable;
+import io.tapdata.entity.schema.value.DateTime;
+import io.tapdata.entity.schema.value.TapDateTimeValue;
+import io.tapdata.entity.schema.value.TapDateValue;
 import io.tapdata.entity.schema.value.TapMapValue;
 import io.tapdata.entity.utils.InstanceFactory;
 import io.tapdata.error.TapEventException;
@@ -1069,24 +1072,25 @@ protected void replaceIllegalDateWithNullIfNeed(TapRecordEvent event){
 		boolean containsIllegalDate = event.getContainsIllegalDate();
 		if (containsIllegalDate && !illegalDateAcceptable){
 			Map<String, Object> before = Optional.ofNullable(TapEventUtil.getBefore(event)).orElse(new HashMap<>());
-			Map<String, List<String>> illegalFieldMap = Optional.ofNullable(TapEventUtil.getIllegalField(event)).orElse(new HashMap<>());
-			List<String> beforeIllegal = Optional.ofNullable(illegalFieldMap.get("before")).orElse(new ArrayList<>());
-			List<String> afterIllegal = Optional.ofNullable(illegalFieldMap.get("after")).orElse(new ArrayList<>());
-			for (String filedName : beforeIllegal) {
-				Object value = before.get(filedName);
-				if (null != value){
-					before.put(filedName, null);
-				}
-			}
-			Map<String, Object> after = TapEventUtil.getAfter(event);
-			for (String filedName : afterIllegal) {
-				Object value = after.get(filedName);
-				if (null != value){
-					after.put(filedName, null);
+			replaceIllegalDate(before);
+			Map<String, Object> after = Optional.ofNullable(TapEventUtil.getAfter(event)).orElse(new HashMap<>());
+			replaceIllegalDate(after);
+		}
+	}
+
+	protected void replaceIllegalDate(Map<String, Object> data){
+		for(Map.Entry<String, Object> entry : data.entrySet()){
+			if(entry.getValue() instanceof TapDateTimeValue){
+				TapDateTimeValue tapDateTimeValue = (TapDateTimeValue) entry.getValue();
+				DateTime dateTime = tapDateTimeValue.getValue();
+				if (dateTime.isContainsIllegal()){
+					entry.setValue(null);
 				}
 			}
 		}
 	}
+
+
 	private boolean handleExactlyOnceWriteCacheIfNeed(TapdataEvent tapdataEvent, List<TapRecordEvent> exactlyOnceWriteCache) {
 		if (!tableEnableExactlyOnceWrite(tapdataEvent.getSyncStage(), getTgtTableNameFromTapEvent(tapdataEvent.getTapEvent()))) {
 			return false;
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkCacheNode.java b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkCacheNode.java
index 40a39f7480..0f39365cae 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkCacheNode.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkCacheNode.java
@@ -1,6 +1,7 @@
 package io.tapdata.flow.engine.V2.node.hazelcast.data.pdk;
 
 import com.hazelcast.core.HazelcastInstance;
+import com.hazelcast.persistence.ConstructType;
 import com.hazelcast.persistence.PersistenceStorage;
 import com.tapdata.cache.CacheUtil;
 import com.tapdata.cache.ICacheService;
@@ -114,7 +115,7 @@ private String getCacheKey(Map<String, Object> row) {
 	@Override
 	public void doClose() throws TapCodeException {
 		if (dataMap != null) {
-			PersistenceStorage.getInstance().destroy(dataMap.getName());
+			PersistenceStorage.getInstance().destroy(this.referenceId, ConstructType.IMAP, dataMap.getName());
 		}
 		super.doClose();
 	}
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkDataNode.java b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkDataNode.java
index 862f143758..df00e884b3 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkDataNode.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/data/pdk/HazelcastTargetPdkDataNode.java
@@ -350,7 +350,6 @@ protected void syncIndex(String tableId, TapTable tapTable, boolean autoCreateTa
 					obsLogger.info("Table: {} already exists Index: {} and will no longer create index", tableId, i.getName());
 					indexList.remove(i);
 				});
-				return;
 			}
 			if (CollectionUtils.isEmpty(indexList)) {
 				obsLogger.info("Table: {} already exists Index list: {}", tableId, indices);
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastJavaScriptProcessorNode.java b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastJavaScriptProcessorNode.java
index 84d5125b1f..0151de81bc 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastJavaScriptProcessorNode.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastJavaScriptProcessorNode.java
@@ -326,10 +326,14 @@ protected void flushBeforeIfNeed (Object processedBefore, TapEvent returnTapEven
 			if (index >= ((List<?>) processedBefore).size()) return;
 			Object beforeMap = ((List<?>) processedBefore).get(index);
 			if (beforeMap instanceof Map) {
-				TapEventUtil.setBefore(returnTapEvent, (Map<String, Object>) beforeMap);
+				Map<String, Object> recordMap = new HashMap<>();
+				MapUtil.copyToNewMap((Map<String, Object>) beforeMap, recordMap);
+				TapEventUtil.setBefore(returnTapEvent, recordMap);
 			}
 		} else if (processedBefore instanceof Map) {
-			TapEventUtil.setBefore(returnTapEvent, (Map<String, Object>) processedBefore);
+			Map<String, Object> recordMap = new HashMap<>();
+			MapUtil.copyToNewMap((Map<String, Object>) processedBefore, recordMap);
+			TapEventUtil.setBefore(returnTapEvent, recordMap);
 		}
 	}
 
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastMergeNode.java b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastMergeNode.java
index cb21e77e5d..8c6c48e6be 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastMergeNode.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastMergeNode.java
@@ -494,9 +494,9 @@ protected void initMergeCache() {
 				}
 				int mergeCacheInMemSize = CommonUtils.getPropertyInt(MERGE_CACHE_IN_MEM_SIZE_PROP_KEY, DEFAULT_MERGE_CACHE_IN_MEM_SIZE);
 				ExternalStorageDto externalStorageDtoCopy = copyExternalStorage(mergeCacheInMemSize);
-				ConstructIMap<Document> hazelcastConstruct = buildConstructIMap(jetContext.hazelcastInstance(), TAG, cacheName, externalStorageDtoCopy);
+				ConstructIMap<Document> hazelcastConstruct = checkBuildConstructIMap(jetContext.hazelcastInstance(), TAG, cacheName, externalStorageDtoCopy);
 				this.mergeCacheMap.put(mergeProperty.getId(), hazelcastConstruct);
-				obsLogger.info("Create merge cache imap name: {}, external storage: {}", cacheName, externalStorageDtoCopy);
+				obsLogger.info("Create merge cache imap name: {}, external storage: {}", String.valueOf(cacheName.hashCode()), externalStorageDtoCopy);
 			}
 		}
 	}
@@ -770,7 +770,43 @@ protected ExternalStorageDto copyExternalStorage(int inMemSize) {
 		return externalStorageDtoCopy;
 	}
 
-	protected ConstructIMap<Document> buildConstructIMap(HazelcastInstance hazelcastInstance, String referenceId, String cacheName, ExternalStorageDto externalStorageDtoCopy) {
+	protected static ConstructIMap<Document> checkBuildConstructIMap(HazelcastInstance hazelcastInstance, String referenceId, String cacheName, ExternalStorageDto externalStorageDtoCopy){
+		ConstructIMap<Document> constructIMapV2;
+		try{
+			constructIMapV2 = buildConstructIMap(hazelcastInstance, referenceId, String.valueOf(cacheName.hashCode()), externalStorageDtoCopy);
+		}catch (Exception e) {
+			throw new TapCodeException(TaskMergeProcessorExCode_16.INIT_CACHE_FAILED, String.format("Cache name: %s", cacheName), e);
+		}
+		if(constructIMapV2.isEmpty()){
+			ConstructIMap<Document> constructIMapV1;
+			try {
+				constructIMapV1 = buildConstructIMap(hazelcastInstance, referenceId, cacheName, externalStorageDtoCopy);
+			} catch (Exception e) {
+				return constructIMapV2;
+			}
+			if (!constructIMapV1.isEmpty()) {
+				try {
+					constructIMapV2.clear();
+					constructIMapV2.destroy();
+				} catch (Exception e) {
+				}
+				return constructIMapV1;
+			} else {
+				try {
+					constructIMapV1.clear();
+					constructIMapV1.destroy();
+				} catch (Exception e) {
+				}
+				return constructIMapV2;
+			}
+
+		}else {
+			return constructIMapV2;
+		}
+
+	}
+
+	protected static ConstructIMap<Document> buildConstructIMap(HazelcastInstance hazelcastInstance, String referenceId, String cacheName, ExternalStorageDto externalStorageDtoCopy) {
 		return new ConstructIMap<>(hazelcastInstance, referenceId, cacheName, externalStorageDtoCopy);
 	}
 
@@ -962,7 +998,7 @@ private static String getMergeCacheName(String nodeId, String tableName) {
 		return name;
 	}
 
-	private static String getCheckUpdateJoinKeyValueCacheName(String nodeId) {
+	protected static String getCheckUpdateJoinKeyValueCacheName(String nodeId) {
 		if (StringUtils.isBlank(nodeId)) {
 			throw new TapCodeException(TaskMergeProcessorExCode_16.GET_CHECK_UPDATE_JOIN_KEY_VALUE_CACHE_NAME_FAILED_NODE_ID_CANNOT_NULL);
 		}
@@ -1646,7 +1682,7 @@ private static void recursiveClearCache(ExternalStorageDto externalStorageDto, L
 		CommonUtils.handleAnyErrors((Consumer<Throwable> consumer) -> {
 			for (MergeTableProperties mergeTableProperty : mergeTableProperties) {
 				try {
-					ConstructIMap<Document> mergeCache = new ConstructIMap<>(hazelcastInstance, HazelcastMergeNode.class.getSimpleName(),
+					ConstructIMap<Document> mergeCache = checkBuildConstructIMap(hazelcastInstance, HazelcastMergeNode.class.getSimpleName(),
 							getMergeCacheName(mergeTableProperty.getId(), mergeTableProperty.getTableName()), externalStorageDto);
 					try {
 						mergeCache.clear();
diff --git a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastProcessorNode.java b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastProcessorNode.java
index 1dfde23000..98ae033c91 100644
--- a/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastProcessorNode.java
+++ b/iengine/iengine-app/src/main/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastProcessorNode.java
@@ -413,12 +413,16 @@ protected void handleTransformToTapValueResult(TapdataEvent tapdataEvent) {
 							break;
 						case "RENAME":
 							Optional.ofNullable(transformToTapValueResult.getBeforeTransformedToTapValueFieldNames()).ifPresent(ttf -> {
-								ttf.remove(operation.getField());
-								ttf.add(operation.getOperand());
+								boolean remove = ttf.remove(operation.getField());
+								if (remove) {
+									ttf.add(operation.getOperand());
+								}
 							});
 							Optional.ofNullable(transformToTapValueResult.getAfterTransformedToTapValueFieldNames()).ifPresent(ttf -> {
-								ttf.remove(operation.getField());
-								ttf.add(operation.getOperand());
+								boolean remove = ttf.remove(operation.getField());
+								if (remove) {
+									ttf.add(operation.getOperand());
+								}
 							});
 							break;
 						case "REMOVE":
diff --git a/iengine/iengine-app/src/main/resources/oem/datapp/log/replace.json b/iengine/iengine-app/src/main/resources/oem/datapp/log/replace.json
index 9aeb133c61..1017db9e5a 100644
--- a/iengine/iengine-app/src/main/resources/oem/datapp/log/replace.json
+++ b/iengine/iengine-app/src/main/resources/oem/datapp/log/replace.json
@@ -1,3 +1,6 @@
 {
-  "tapdata": "DGG"
+  "tapdata": "DGG",
+  "Tapdata": "DGG",
+  "TapData": "DGG",
+  "TAPDATA": "DGG"
 }
\ No newline at end of file
diff --git a/iengine/iengine-app/src/main/resources/pluginKit.properties b/iengine/iengine-app/src/main/resources/pluginKit.properties
index eebe210aaf..a678dd401f 100644
--- a/iengine/iengine-app/src/main/resources/pluginKit.properties
+++ b/iengine/iengine-app/src/main/resources/pluginKit.properties
@@ -12,7 +12,7 @@ maven.compiler.target=1.8
 project.build.sourceEncoding=UTF-8
 project.reporting.outputEncoding=UTF-8
 proxy.file.name=plugin-kit-v1.0-SNAPSHOT
-tapdata.api.verison=1.3.8-SNAPSHOT
-tapdata.pdk.api.verison=1.3.8-SNAPSHOT
+tapdata.api.verison=1.3.9-SNAPSHOT
+tapdata.pdk.api.verison=1.3.9-SNAPSHOT
 tapdata.pdk.connector.core.version=1.0-SNAPSHOT
-tapdata.pdk.runner.verison=1.8-SNAPSHOT
+tapdata.pdk.runner.verison=1.9-SNAPSHOT
diff --git a/iengine/iengine-common/src/main/java/io/tapdata/log/CustomPatternLayout.java b/iengine/iengine-common/src/main/java/io/tapdata/log/CustomPatternLayout.java
new file mode 100644
index 0000000000..a83fd2f05d
--- /dev/null
+++ b/iengine/iengine-common/src/main/java/io/tapdata/log/CustomPatternLayout.java
@@ -0,0 +1,648 @@
+package io.tapdata.log;
+
+import com.tapdata.tm.utils.OEMReplaceUtil;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
+import org.apache.logging.log4j.core.config.Node;
+import org.apache.logging.log4j.core.config.plugins.*;
+import org.apache.logging.log4j.core.layout.AbstractStringLayout;
+import org.apache.logging.log4j.core.layout.ByteBufferDestination;
+import org.apache.logging.log4j.core.layout.Encoder;
+import org.apache.logging.log4j.core.layout.PatternSelector;
+import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
+import org.apache.logging.log4j.core.pattern.PatternFormatter;
+import org.apache.logging.log4j.core.pattern.PatternParser;
+import org.apache.logging.log4j.core.pattern.RegexReplacement;
+import org.apache.logging.log4j.util.Strings;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Plugin(name = "CustomPatternLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)
+public final class CustomPatternLayout extends AbstractStringLayout {
+
+
+    /**
+     * Default pattern string for log output. Currently set to the string <b>"%m%n"</b> which just prints the
+     * application supplied message.
+     */
+    public static final String DEFAULT_CONVERSION_PATTERN = "%m%n";
+
+    /**
+     * A conversion pattern equivalent to the TTCCLayout. Current value is <b>%r [%t] %p %c %notEmpty{%x }- %m%n</b>.
+     */
+    public static final String TTCC_CONVERSION_PATTERN = "%r [%t] %p %c %notEmpty{%x }- %m%n";
+
+    /**
+     * A simple pattern. Current value is <b>%d [%t] %p %c - %m%n</b>.
+     */
+    public static final String SIMPLE_CONVERSION_PATTERN = "%d [%t] %p %c - %m%n";
+
+    /** Key to identify pattern converters. */
+    public static final String KEY = "Converter";
+
+    /**
+     * Conversion pattern.
+     */
+    private String conversionPattern;
+    private PatternSelector patternSelector;
+    private Serializer eventSerializer;
+
+    /**
+     * Constructs a CustomPatternLayout using the supplied conversion pattern.
+     *
+     * @param config The Configuration.
+     * @param replace The regular expression to match.
+     * @param eventPattern conversion pattern.
+     * @param patternSelector The PatternSelector.
+     * @param charset The character set.
+     * @param alwaysWriteExceptions Whether or not exceptions should always be handled in this pattern (if {@code true},
+     *                         exceptions will be written even if the pattern does not specify so).
+     * @param disableAnsi
+     *            If {@code "true"}, do not output ANSI escape codes
+     * @param noConsoleNoAnsi
+     *            If {@code "true"} (default) and {@link System#console()} is null, do not output ANSI escape codes
+     * @param headerPattern header conversion pattern.
+     * @param footerPattern footer conversion pattern.
+     */
+    private CustomPatternLayout(final Configuration config, final RegexReplacement replace, final String eventPattern,
+                                final PatternSelector patternSelector, final Charset charset, final boolean alwaysWriteExceptions,
+                                final boolean disableAnsi, final boolean noConsoleNoAnsi, final String headerPattern,
+                                final String footerPattern) {
+        super(config, charset,
+                newSerializerBuilder()
+                        .setConfiguration(config)
+                        .setReplace(replace)
+                        .setPatternSelector(patternSelector)
+                        .setAlwaysWriteExceptions(alwaysWriteExceptions)
+                        .setDisableAnsi(disableAnsi)
+                        .setNoConsoleNoAnsi(noConsoleNoAnsi)
+                        .setPattern(headerPattern)
+                        .build(),
+                newSerializerBuilder()
+                        .setConfiguration(config)
+                        .setReplace(replace)
+                        .setPatternSelector(patternSelector)
+                        .setAlwaysWriteExceptions(alwaysWriteExceptions)
+                        .setDisableAnsi(disableAnsi)
+                        .setNoConsoleNoAnsi(noConsoleNoAnsi)
+                        .setPattern(footerPattern)
+                        .build());
+        this.conversionPattern = eventPattern;
+        this.patternSelector = patternSelector;
+        this.eventSerializer = newSerializerBuilder()
+                .setConfiguration(config)
+                .setReplace(replace)
+                .setPatternSelector(patternSelector)
+                .setAlwaysWriteExceptions(alwaysWriteExceptions)
+                .setDisableAnsi(disableAnsi)
+                .setNoConsoleNoAnsi(noConsoleNoAnsi)
+                .setPattern(eventPattern)
+                .setDefaultPattern(DEFAULT_CONVERSION_PATTERN)
+                .build();
+    }
+
+    protected CustomPatternLayout(Charset charset) {
+        super(charset);
+    }
+
+    public static SerializerBuilder newSerializerBuilder() {
+        return new SerializerBuilder();
+    }
+
+    /**
+     * Deprecated, use {@link #newSerializerBuilder()} instead.
+     *
+     * @param configuration
+     * @param replace
+     * @param pattern
+     * @param defaultPattern
+     * @param patternSelector
+     * @param alwaysWriteExceptions
+     * @param noConsoleNoAnsi
+     * @return a new Serializer.
+     * @deprecated Use {@link #newSerializerBuilder()} instead.
+     */
+    @Deprecated
+    public static Serializer createSerializer(final Configuration configuration, final RegexReplacement replace,
+                                              final String pattern, final String defaultPattern, final PatternSelector patternSelector,
+                                              final boolean alwaysWriteExceptions, final boolean noConsoleNoAnsi) {
+        final SerializerBuilder builder = newSerializerBuilder();
+        builder.setAlwaysWriteExceptions(alwaysWriteExceptions);
+        builder.setConfiguration(configuration);
+        builder.setDefaultPattern(defaultPattern);
+        builder.setNoConsoleNoAnsi(noConsoleNoAnsi);
+        builder.setPattern(pattern);
+        builder.setPatternSelector(patternSelector);
+        builder.setReplace(replace);
+        return builder.build();
+    }
+
+    /**
+     * Gets the conversion pattern.
+     *
+     * @return the conversion pattern.
+     */
+    public String getConversionPattern() {
+        return conversionPattern;
+    }
+
+
+    /**
+     * Gets this CustomPatternLayout's content format. Specified by:
+     * <ul>
+     * <li>Key: "structured" Value: "false"</li>
+     * <li>Key: "formatType" Value: "conversion" (format uses the keywords supported by OptionConverter)</li>
+     * <li>Key: "format" Value: provided "conversionPattern" param</li>
+     * </ul>
+     *
+     * @return Map of content format keys supporting CustomPatternLayout
+     */
+    @Override
+    public Map<String, String> getContentFormat() {
+        final Map<String, String> result = new HashMap<>();
+        result.put("structured", "false");
+        result.put("formatType", "conversion");
+        result.put("format", conversionPattern);
+        return result;
+    }
+
+    /**
+     * Formats a logging event to a writer.
+     *
+     * @param event logging event to be formatted.
+     * @return The event formatted as a String.
+     */
+    @Override
+    public String toSerializable(final LogEvent event) {
+        return eventSerializer.toSerializable(event);
+    }
+
+    @Override
+    public void encode(final LogEvent event, final ByteBufferDestination destination) {
+        if (!(eventSerializer instanceof Serializer2)) {
+            super.encode(event, destination);
+            return;
+        }
+        final StringBuilder text = toText((Serializer2) eventSerializer, event, getStringBuilder());
+        final Encoder<StringBuilder> encoder = getStringBuilderEncoder();
+        encoder.encode(text, destination);
+        trimToMaxSize(text);
+    }
+
+    /**
+     * Creates a text representation of the specified log event
+     * and writes it into the specified StringBuilder.
+     * <p>
+     * Implementations are free to return a new StringBuilder if they can
+     * detect in advance that the specified StringBuilder is too small.
+     */
+    private StringBuilder toText(final Serializer2 serializer, final LogEvent event,
+                                 final StringBuilder destination) {
+        return serializer.toSerializable(event, destination);
+    }
+
+    /**
+     * Creates a PatternParser.
+     * @param config The Configuration.
+     * @return The PatternParser.
+     */
+    public static PatternParser createPatternParser(final Configuration config) {
+        if (config == null) {
+            return new PatternParser(config, KEY, LogEventPatternConverter.class);
+        }
+        PatternParser parser = config.getComponent(KEY);
+        if (parser == null) {
+            parser = new PatternParser(config, KEY, LogEventPatternConverter.class);
+            config.addComponent(KEY, parser);
+            parser = config.getComponent(KEY);
+        }
+        return parser;
+    }
+
+    @Override
+    public String toString() {
+        return patternSelector == null ? conversionPattern : patternSelector.toString();
+    }
+
+    /**
+     * Creates a pattern layout.
+     *
+     * @param pattern
+     *        The pattern. If not specified, defaults to DEFAULT_CONVERSION_PATTERN.
+     * @param patternSelector
+     *        Allows different patterns to be used based on some selection criteria.
+     * @param config
+     *        The Configuration. Some Converters require access to the Interpolator.
+     * @param replace
+     *        A Regex replacement String.
+     * @param charset
+     *        The character set. The platform default is used if not specified.
+     * @param alwaysWriteExceptions
+     *        If {@code "true"} (default) exceptions are always written even if the pattern contains no exception tokens.
+     * @param noConsoleNoAnsi
+     *        If {@code "true"} (default is false) and {@link System#console()} is null, do not output ANSI escape codes
+     * @param headerPattern
+     *        The footer to place at the top of the document, once.
+     * @param footerPattern
+     *        The footer to place at the bottom of the document, once.
+     * @return The
+     * @deprecated Use {@link #newBuilder()} instead. This will be private in a future version.
+     */
+    @PluginFactory
+    @Deprecated
+    public static CustomPatternLayout createLayout(
+            @PluginAttribute(value = "pattern", defaultString = DEFAULT_CONVERSION_PATTERN) final String pattern,
+            @PluginElement("PatternSelector") final PatternSelector patternSelector,
+            @PluginConfiguration final Configuration config,
+            @PluginElement("Replace") final RegexReplacement replace,
+            // LOG4J2-783 use platform default by default, so do not specify defaultString for charset
+            @PluginAttribute(value = "charset") final Charset charset,
+            @PluginAttribute(value = "alwaysWriteExceptions", defaultBoolean = true) final boolean alwaysWriteExceptions,
+            @PluginAttribute(value = "noConsoleNoAnsi") final boolean noConsoleNoAnsi,
+            @PluginAttribute("header") final String headerPattern,
+            @PluginAttribute("footer") final String footerPattern) {
+        return newBuilder()
+                .withPattern(pattern)
+                .withPatternSelector(patternSelector)
+                .withConfiguration(config)
+                .withRegexReplacement(replace)
+                .withCharset(charset)
+                .withAlwaysWriteExceptions(alwaysWriteExceptions)
+                .withNoConsoleNoAnsi(noConsoleNoAnsi)
+                .withHeader(headerPattern)
+                .withFooter(footerPattern)
+                .build();
+    }
+
+    private static class PatternSerializer implements Serializer, Serializer2 {
+
+        private final PatternFormatter[] formatters;
+        private final RegexReplacement replace;
+        Map<String, Object> oemConfigMap = OEMReplaceUtil.getOEMConfigMap("log/replace.json");
+
+        private PatternSerializer(final PatternFormatter[] formatters, final RegexReplacement replace) {
+            super();
+            this.formatters = formatters;
+            this.replace = replace;
+        }
+
+        @Override
+        public String toSerializable(final LogEvent event) {
+            final StringBuilder sb = getStringBuilder();
+            try {
+                return toSerializable(event, sb).toString();
+            } finally {
+                trimToMaxSize(sb);
+            }
+        }
+
+        @Override
+        public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
+            final int len = formatters.length;
+            for (int i = 0; i < len; i++) {
+                formatters[i].format(event, buffer);
+            }
+
+
+            if (MapUtils.isNotEmpty(oemConfigMap)) {
+                String oemReplaceString = OEMReplaceUtil.replace(buffer.toString(), oemConfigMap);
+                buffer.setLength(0);
+                buffer.append(oemReplaceString);
+            }
+
+            if (replace != null) { // creates temporary objects
+                String str = buffer.toString();
+                str = replace.format(str);
+                buffer.setLength(0);
+                buffer.append(str);
+            }
+            return buffer;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder builder = new StringBuilder();
+            builder.append(super.toString());
+            builder.append("[formatters=");
+            builder.append(Arrays.toString(formatters));
+            builder.append(", replace=");
+            builder.append(replace);
+            builder.append("]");
+            return builder.toString();
+        }
+    }
+
+    public static class SerializerBuilder implements org.apache.logging.log4j.core.util.Builder<Serializer> {
+
+        private Configuration configuration;
+        private RegexReplacement replace;
+        private String pattern;
+        private String defaultPattern;
+        private PatternSelector patternSelector;
+        private boolean alwaysWriteExceptions;
+        private boolean disableAnsi;
+        private boolean noConsoleNoAnsi;
+
+        @Override
+        public Serializer build() {
+            if (Strings.isEmpty(pattern) && Strings.isEmpty(defaultPattern)) {
+                return null;
+            }
+            if (patternSelector == null) {
+                try {
+                    final PatternParser parser = createPatternParser(configuration);
+                    final List<PatternFormatter> list = parser.parse(pattern == null ? defaultPattern : pattern,
+                            alwaysWriteExceptions, disableAnsi, noConsoleNoAnsi);
+                    final PatternFormatter[] formatters = list.toArray(new PatternFormatter[0]);
+                    return new PatternSerializer(formatters, replace);
+                } catch (final RuntimeException ex) {
+                    throw new IllegalArgumentException("Cannot parse pattern '" + pattern + "'", ex);
+                }
+            }
+            return new PatternSelectorSerializer(patternSelector, replace);
+        }
+
+        public SerializerBuilder setConfiguration(final Configuration configuration) {
+            this.configuration = configuration;
+            return this;
+        }
+
+        public SerializerBuilder setReplace(final RegexReplacement replace) {
+            this.replace = replace;
+            return this;
+        }
+
+        public SerializerBuilder setPattern(final String pattern) {
+            this.pattern = pattern;
+            return this;
+        }
+
+        public SerializerBuilder setDefaultPattern(final String defaultPattern) {
+            this.defaultPattern = defaultPattern;
+            return this;
+        }
+
+        public SerializerBuilder setPatternSelector(final PatternSelector patternSelector) {
+            this.patternSelector = patternSelector;
+            return this;
+        }
+
+        public SerializerBuilder setAlwaysWriteExceptions(final boolean alwaysWriteExceptions) {
+            this.alwaysWriteExceptions = alwaysWriteExceptions;
+            return this;
+        }
+
+        public SerializerBuilder setDisableAnsi(final boolean disableAnsi) {
+            this.disableAnsi = disableAnsi;
+            return this;
+        }
+
+        public SerializerBuilder setNoConsoleNoAnsi(final boolean noConsoleNoAnsi) {
+            this.noConsoleNoAnsi = noConsoleNoAnsi;
+            return this;
+        }
+
+    }
+
+    private static class PatternSelectorSerializer implements Serializer, Serializer2 {
+
+        private final PatternSelector patternSelector;
+        private final RegexReplacement replace;
+        Map<String, Object> oemConfigMap = OEMReplaceUtil.getOEMConfigMap("log/replace.json");
+
+        private PatternSelectorSerializer(final PatternSelector patternSelector, final RegexReplacement replace) {
+            super();
+            this.patternSelector = patternSelector;
+            this.replace = replace;
+        }
+
+        @Override
+        public String toSerializable(final LogEvent event) {
+            final StringBuilder sb = getStringBuilder();
+            try {
+                return toSerializable(event, sb).toString();
+            } finally {
+                trimToMaxSize(sb);
+            }
+        }
+
+        @Override
+        public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
+            final PatternFormatter[] formatters = patternSelector.getFormatters(event);
+            final int len = formatters.length;
+            for (int i = 0; i < len; i++) {
+                formatters[i].format(event, buffer);
+            }
+            if (MapUtils.isNotEmpty(oemConfigMap)) {
+                String oemReplaceString = OEMReplaceUtil.replace(buffer.toString(), oemConfigMap);
+                buffer.setLength(0);
+                buffer.append(oemReplaceString);
+            }
+            if (replace != null) { // creates temporary objects
+                String str = buffer.toString();
+                str = replace.format(str);
+                buffer.setLength(0);
+                buffer.append(str);
+            }
+            return buffer;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder builder = new StringBuilder();
+            builder.append(super.toString());
+            builder.append("[patternSelector=");
+            builder.append(patternSelector);
+            builder.append(", replace=");
+            builder.append(replace);
+            builder.append("]");
+            return builder.toString();
+        }
+    }
+
+    /**
+     * Creates a CustomPatternLayout using the default options. These options include using UTF-8, the default conversion
+     * pattern, exceptions being written, and with ANSI escape codes.
+     *
+     * @return the
+     * @see #DEFAULT_CONVERSION_PATTERN Default conversion pattern
+     */
+    public static CustomPatternLayout createDefaultLayout() {
+        return newBuilder().build();
+    }
+
+    /**
+     * Creates a CustomPatternLayout using the default options and the given configuration. These options include using UTF-8,
+     * the default conversion pattern, exceptions being written, and with ANSI escape codes.
+     *
+     * @param configuration The Configuration.
+     *
+     * @return the
+     * @see #DEFAULT_CONVERSION_PATTERN Default conversion pattern
+     */
+    public static CustomPatternLayout createDefaultLayout(final Configuration configuration) {
+        return newBuilder().withConfiguration(configuration).build();
+    }
+
+    /**
+     * Creates a builder for a custom
+     *
+     * @return a CustomPatternLayout builder.
+     */
+    @PluginBuilderFactory
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    /**
+     * Custom CustomPatternLayout builder. Use the {@link CustomPatternLayout#newBuilder() builder factory method} to create this.
+     */
+    public static class Builder implements org.apache.logging.log4j.core.util.Builder<CustomPatternLayout> {
+
+        @PluginBuilderAttribute
+        private String pattern = DEFAULT_CONVERSION_PATTERN;
+
+        @PluginElement("PatternSelector")
+        private PatternSelector patternSelector;
+
+        @PluginConfiguration
+        private Configuration configuration;
+
+        @PluginElement("Replace")
+        private RegexReplacement regexReplacement;
+
+        // LOG4J2-783 use platform default by default
+        @PluginBuilderAttribute
+        private Charset charset = Charset.defaultCharset();
+
+        @PluginBuilderAttribute
+        private boolean alwaysWriteExceptions = true;
+
+        @PluginBuilderAttribute
+        private boolean disableAnsi;
+
+        @PluginBuilderAttribute
+        private boolean noConsoleNoAnsi;
+
+        @PluginBuilderAttribute
+        private String header;
+
+        @PluginBuilderAttribute
+        private String footer;
+
+        private Builder() {
+        }
+
+
+        /**
+         * @param pattern
+         *        The pattern. If not specified, defaults to DEFAULT_CONVERSION_PATTERN.
+         */
+        public Builder withPattern(final String pattern) {
+            this.pattern = pattern;
+            return this;
+        }
+
+        /**
+         * @param patternSelector
+         *        Allows different patterns to be used based on some selection criteria.
+         */
+        public Builder withPatternSelector(final PatternSelector patternSelector) {
+            this.patternSelector = patternSelector;
+            return this;
+        }
+
+        /**
+         * @param configuration
+         *        The Configuration. Some Converters require access to the Interpolator.
+         */
+        public Builder withConfiguration(final Configuration configuration) {
+            this.configuration = configuration;
+            return this;
+        }
+
+        /**
+         * @param regexReplacement
+         *        A Regex replacement
+         */
+        public Builder withRegexReplacement(final RegexReplacement regexReplacement) {
+            this.regexReplacement = regexReplacement;
+            return this;
+        }
+
+        /**
+         * @param charset
+         *        The character set. The platform default is used if not specified.
+         */
+        public Builder withCharset(final Charset charset) {
+            // LOG4J2-783 if null, use platform default by default
+            if (charset != null) {
+                this.charset = charset;
+            }
+            return this;
+        }
+
+        /**
+         * @param alwaysWriteExceptions
+         *        If {@code "true"} (default) exceptions are always written even if the pattern contains no exception tokens.
+         */
+        public Builder withAlwaysWriteExceptions(final boolean alwaysWriteExceptions) {
+            this.alwaysWriteExceptions = alwaysWriteExceptions;
+            return this;
+        }
+
+        /**
+         * @param disableAnsi
+         *        If {@code "true"} (default is false), do not output ANSI escape codes
+         */
+        public Builder withDisableAnsi(final boolean disableAnsi) {
+            this.disableAnsi = disableAnsi;
+            return this;
+        }
+
+        /**
+         * @param noConsoleNoAnsi
+         *        If {@code "true"} (default is false) and {@link System#console()} is null, do not output ANSI escape codes
+         */
+        public Builder withNoConsoleNoAnsi(final boolean noConsoleNoAnsi) {
+            this.noConsoleNoAnsi = noConsoleNoAnsi;
+            return this;
+        }
+
+        /**
+         * @param header
+         *        The footer to place at the top of the document, once.
+         */
+        public Builder withHeader(final String header) {
+            this.header = header;
+            return this;
+        }
+
+        /**
+         * @param footer
+         *        The footer to place at the bottom of the document, once.
+         */
+        public Builder withFooter(final String footer) {
+            this.footer = footer;
+            return this;
+        }
+
+        @Override
+        public CustomPatternLayout build() {
+            // fall back to DefaultConfiguration
+            if (configuration == null) {
+                configuration = new DefaultConfiguration();
+            }
+            return new CustomPatternLayout(configuration, regexReplacement, pattern, patternSelector, charset,
+                    alwaysWriteExceptions, disableAnsi, noConsoleNoAnsi, header, footer);
+        }
+    }
+}
+
diff --git a/iengine/modules/observable-module/src/main/java/io/tapdata/observable/logging/appender/FileAppender.java b/iengine/modules/observable-module/src/main/java/io/tapdata/observable/logging/appender/FileAppender.java
index 999835c06f..e7c9026fba 100644
--- a/iengine/modules/observable-module/src/main/java/io/tapdata/observable/logging/appender/FileAppender.java
+++ b/iengine/modules/observable-module/src/main/java/io/tapdata/observable/logging/appender/FileAppender.java
@@ -1,6 +1,7 @@
 package io.tapdata.observable.logging.appender;
 
 import com.tapdata.tm.commons.schema.MonitoringLogsDto;
+import io.tapdata.log.CustomPatternLayout;
 import io.tapdata.observable.logging.ObsLoggerFactory;
 import io.tapdata.observable.logging.util.Conf.LogConfiguration;
 import io.tapdata.observable.logging.util.LogUtil;
@@ -98,7 +99,7 @@ public void start() {
 
 		final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
 		final Configuration config = ctx.getConfiguration();
-		PatternLayout patternLayout = PatternLayout.newBuilder()
+		CustomPatternLayout patternLayout = CustomPatternLayout.newBuilder()
 				.withPattern("[%-5level] %date{yyyy-MM-dd HH:mm:ss.SSS} - %msg%n")
 				.withConfiguration(config)
 				.build();
diff --git a/iengine/pom.xml b/iengine/pom.xml
index 14d0eedabe..572c8384ca 100644
--- a/iengine/pom.xml
+++ b/iengine/pom.xml
@@ -799,7 +799,7 @@
             <dependency>
                 <groupId>com.tapdata</groupId>
                 <artifactId>tapdata-api</artifactId>
-                <version>1.3.8-SNAPSHOT</version>
+                <version>1.3.9-SNAPSHOT</version>
             </dependency>
             <dependency>
                 <groupId>com.tapdata</groupId>
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/dto/SendMailResponseDto.java b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/dto/SendMailResponseDto.java
new file mode 100644
index 0000000000..c17d974c86
--- /dev/null
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/dto/SendMailResponseDto.java
@@ -0,0 +1,21 @@
+package com.tapdata.tm.Settings.dto;
+
+import java.io.Serializable;
+
+public class SendMailResponseDto implements Serializable {
+    private boolean result;
+    private String stack;
+
+    public boolean isResult() {
+        return result;
+    }
+
+    public String getStack() {
+        return stack;
+    }
+
+    public SendMailResponseDto(boolean result, String stack) {
+        this.result = result;
+        this.stack = stack;
+    }
+}
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/service/SettingsService.java b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/service/SettingsService.java
index e801a39908..96796bd590 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/Settings/service/SettingsService.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/Settings/service/SettingsService.java
@@ -4,6 +4,7 @@
 import com.tapdata.tm.Settings.constant.KeyEnum;
 import com.tapdata.tm.Settings.constant.SettingsEnum;
 import com.tapdata.tm.Settings.dto.MailAccountDto;
+import com.tapdata.tm.Settings.dto.SendMailResponseDto;
 import com.tapdata.tm.Settings.dto.SettingsDto;
 import com.tapdata.tm.Settings.dto.TestMailDto;
 import com.tapdata.tm.Settings.entity.Settings;
@@ -50,7 +51,7 @@ public interface SettingsService {
 
     List<Settings> findAll(Query query);
 
-    void testSendMail(TestMailDto testMailDto);
+    SendMailResponseDto testSendMail(TestMailDto testMailDto);
 
     void setSettingsRepository(com.tapdata.tm.Settings.repository.SettingsRepository settingsRepository);
 
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/base/handler/ExceptionHandler.java b/manager/tm-api/src/main/java/com/tapdata/tm/base/handler/ExceptionHandler.java
index cc40a6bacd..2c5ada46b3 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/base/handler/ExceptionHandler.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/base/handler/ExceptionHandler.java
@@ -41,7 +41,7 @@ public class ExceptionHandler extends BaseController {
 
 	@org.springframework.web.bind.annotation.ExceptionHandler(Throwable.class)
 	public ResponseMessage<?> handlerException(Throwable e, HttpServletRequest request, HttpServletResponse response) throws Throwable {
-		log.error("System error:{}", ThrowableUtils.getStackTraceByPn(e));
+		log.error("System error", e);
 
 		Locale locale = WebUtils.getLocale(request);
 
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/base/service/BaseService.java b/manager/tm-api/src/main/java/com/tapdata/tm/base/service/BaseService.java
index 1e4bb89aff..aa8cd990b9 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/base/service/BaseService.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/base/service/BaseService.java
@@ -3,11 +3,8 @@
 import cn.hutool.json.JSONObject;
 import cn.hutool.json.JSONUtil;
 import com.mongodb.client.result.UpdateResult;
+import com.tapdata.tm.base.dto.*;
 import com.tapdata.tm.permissions.DataPermissionHelper;
-import com.tapdata.tm.base.dto.Field;
-import com.tapdata.tm.base.dto.Filter;
-import com.tapdata.tm.base.dto.Page;
-import com.tapdata.tm.base.dto.Where;
 import com.tapdata.tm.base.entity.BaseEntity;
 import com.tapdata.tm.base.reporitory.BaseRepository;
 import com.tapdata.tm.commons.base.dto.BaseDto;
@@ -577,4 +574,12 @@ public Dto replaceOrInsert(Dto dto, UserDetail userDetail) {
         Entity entity = repository.replaceOrInsert(new Query(Criteria.where("_id").is(dto.getId())), convertToEntity(entityClass, dto), userDetail);
         return convertToDto(entity, dtoClass);
     }
+
+    protected TmPageable filterToTmPageable(Filter filter) {
+        TmPageable tmPageable = new TmPageable();
+        Integer page = (filter.getSkip() / filter.getLimit()) + 1;
+        tmPageable.setPage(page);
+        tmPageable.setSize(filter.getLimit());
+        return tmPageable;
+    }
 }
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/ds/entity/DataSourceEntity.java b/manager/tm-api/src/main/java/com/tapdata/tm/ds/entity/DataSourceEntity.java
index b4722c35b6..43fa87e68f 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/ds/entity/DataSourceEntity.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/ds/entity/DataSourceEntity.java
@@ -335,6 +335,7 @@ public class DataSourceEntity extends BaseEntity implements IDataPermissionEntit
     private String multiConnectionInstanceId;
 	private List<String> namespace;
     private Long timeDifference;
+    private String datasourceInstanceTag;
 
     /**
      * 后续 开放可以多选 flow engine 的话，这里一定要删除
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/lock/annotation/Lock.java b/manager/tm-api/src/main/java/com/tapdata/tm/lock/annotation/Lock.java
index 37db29b048..4b395e8fcb 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/lock/annotation/Lock.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/lock/annotation/Lock.java
@@ -18,6 +18,10 @@
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 public @interface Lock {
+
+    String PARAMETER = "PARAMETER";
+    String CUSTOM = "CUSTOM";
+
     /** 锁的key 方法参数中的值，可以用.去属性， 如@Lock(task.id) 则是取参数task对象的id作为key， 如果参数的属性为对象，默认取对象的tostring方法作为key*/
     String value();
     /** 锁的类型，不同类型的竞争可以用不同的锁  例如，任务的模型推演，跟任务的校验，是不同的类型，分别加锁不会冲突*/
@@ -26,4 +30,7 @@
     int expireSeconds() default 10;
     /** 循环等待睡眠时间 单位毫秒*/
     int sleepMillis() default 50;
+
+    /** 锁的key 可以不是方法参数中的值， 如@Lock("task-lock")*/
+    String valueType() default PARAMETER;
 }
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/lock/constant/LockType.java b/manager/tm-api/src/main/java/com/tapdata/tm/lock/constant/LockType.java
index 813e888197..fd4b8a66e3 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/lock/constant/LockType.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/lock/constant/LockType.java
@@ -18,5 +18,7 @@ public enum LockType {
     START_LDP_FDM,
     /** 启动CHECK_ENGINE_VALID任务 */
     CHECK_ENGINE_VALID,
+    /** 添加通道授权*/
+    PIPELINE_LIMIT
     ;
 }
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/task/service/ILicenseService.java b/manager/tm-api/src/main/java/com/tapdata/tm/task/service/ILicenseService.java
new file mode 100644
index 0000000000..0768f54f13
--- /dev/null
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/task/service/ILicenseService.java
@@ -0,0 +1,8 @@
+package com.tapdata.tm.task.service;
+
+import com.tapdata.tm.commons.task.dto.TaskDto;
+import com.tapdata.tm.config.security.UserDetail;
+
+public interface ILicenseService {
+    public abstract boolean checkTaskPipelineLimit(TaskDto taskDto, UserDetail user);
+}
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/task/service/LogCollectorService.java b/manager/tm-api/src/main/java/com/tapdata/tm/task/service/LogCollectorService.java
index be79f2c3e4..90739c484e 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/task/service/LogCollectorService.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/task/service/LogCollectorService.java
@@ -120,9 +120,9 @@ static void main(String[] args) {
 
     void cancelMerge(String taskId, String connectionId, UserDetail user);
 
-    Page<ShareCdcTableInfo> tableInfos(String taskId, String connectionId, String keyword, Integer page, Integer size, UserDetail user);
+    Page<ShareCdcTableInfo> tableInfos(String taskId, String connectionId, String keyword, Integer page, Integer size, String order, UserDetail user);
 
-    Page<ShareCdcTableInfo> excludeTableInfos(String taskId, String connectionId, String keyword, Integer page, Integer size, UserDetail user);
+    Page<ShareCdcTableInfo> excludeTableInfos(String taskId, String connectionId, String keyword, Integer page, Integer size, String order, UserDetail user);
 
     void configTables(String taskId, List<TableLogCollectorParam> params, String type, UserDetail user);
 
diff --git a/manager/tm-api/src/main/java/com/tapdata/tm/utils/MailUtils.java b/manager/tm-api/src/main/java/com/tapdata/tm/utils/MailUtils.java
index 4d9dc1848d..dacd0f3d9d 100644
--- a/manager/tm-api/src/main/java/com/tapdata/tm/utils/MailUtils.java
+++ b/manager/tm-api/src/main/java/com/tapdata/tm/utils/MailUtils.java
@@ -6,10 +6,13 @@
 import com.tapdata.tm.Settings.constant.CategoryEnum;
 import com.tapdata.tm.Settings.constant.KeyEnum;
 import com.tapdata.tm.Settings.dto.MailAccountDto;
+import com.tapdata.tm.Settings.dto.SendMailResponseDto;
 import com.tapdata.tm.Settings.service.SettingsService;
+import com.tapdata.tm.base.exception.BizException;
 import com.tapdata.tm.message.constant.MsgTypeEnum;
 import com.tapdata.tm.message.constant.SystemEnum;
 import com.tapdata.tm.message.service.BlacklistService;
+import io.tapdata.entity.simplify.TapSimplify;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -451,18 +454,17 @@ public static String getMailTitle(SystemEnum systemEnum, MsgTypeEnum msgTypeEnum
     /**
      * 发送HTML邮件
      */
-    public static void sendHtmlEmail(MailAccountDto parms, List<String> adressees, String title, String content) {
+    public static SendMailResponseDto sendHtmlEmail(MailAccountDto parms, List<String> adressees, String title, String content) {
         adressees = filterBlackList(adressees);
-        if (adressees == null) return;
+        if (adressees == null) return new SendMailResponseDto(false,"Please check your configuration, receivers cannot be empty.");
 
         boolean flag = true;
         if (StringUtils.isAnyBlank(parms.getHost(), parms.getFrom(),parms.getUser(), parms.getPass()) || CollectionUtils.isEmpty(adressees)) {
             log.error("mail account info empty, params:{}", JSON.toJSONString(parms));
-            flag = false;
+            return new SendMailResponseDto(false,"Please check your configuration, mail account information cannot be empty.");
         } else {
-            if (StringUtils.isNotBlank(parms.getProxyHost()) && null != parms.getProxyPort()) {
-                sendEmailForProxy(parms, adressees, title, content, flag);
-                return;
+            if (StringUtils.isNotBlank(parms.getProxyHost()) && 0 != parms.getProxyPort()) {
+                return sendEmailForProxy(parms, adressees, title, content, flag);
             }
             try {
                 MailAccount account = new MailAccount();
@@ -496,9 +498,11 @@ public static void sendHtmlEmail(MailAccountDto parms, List<String> adressees, S
             } catch (Exception e) {
                 log.error("mail send error：{}", e.getMessage(), e);
                 flag = false;
+                return new SendMailResponseDto(flag, TapSimplify.getStackTrace(e));
             }
         }
         log.debug("mail send status：{}", flag ? "suc" : "error");
+        return new SendMailResponseDto(flag, null);
     }
 
     @Nullable
@@ -522,17 +526,20 @@ protected static List<String> filterBlackList(List<String> adressees) {
         return adressees;
     }
 
-    protected static void sendEmailForProxy(MailAccountDto parms, List<String> adressees, String title, String content, boolean flag) {
+    protected static SendMailResponseDto sendEmailForProxy(MailAccountDto parms, List<String> adressees, String title, String content, boolean flag) {
         final String username = parms.getUser();
         final String password = parms.getPass();
 
-        Properties properties = System.getProperties();
+        Properties properties = new Properties();
         properties.put("mail.smtp.host", parms.getHost());
         properties.put("mail.smtp.port", parms.getPort());
         properties.put("mail.smtp.auth", "true");
-        if ("SSL".equals(parms.getProtocol()) || "TLS".equals(parms.getProtocol())) {
+        if ("SSL".equals(parms.getProtocol())){
+            properties.put("mail.smtp.ssl.enable", "true");
+        } else if ("TLS".equals(parms.getProtocol())) {
             properties.put("mail.smtp.starttls.enable", "true");
         } else {
+            properties.put("mail.smtp.ssl.enable", "false");
             properties.put("mail.smtp.starttls.enable", "false");
         }
         //set proxy server
@@ -568,10 +575,12 @@ protected PasswordAuthentication getPasswordAuthentication() {
 
             Transport.send(message);
         } catch (Exception e) {
-            log.error("mail send error：{}", e.getMessage(), e);
             flag = false;
+            log.error("mail send error：{}", e.getMessage(), e);
+            return new SendMailResponseDto(flag, TapSimplify.getStackTrace(e));
         }
         log.debug("mail send status：{}", flag ? "suc" : "error");
+        return new SendMailResponseDto(flag,null);
     }
 
     protected static String assemblyMessageBody(String message) {
diff --git a/manager/tm-common/src/main/java/com/tapdata/tm/commons/schema/DataSourceConnectionDto.java b/manager/tm-common/src/main/java/com/tapdata/tm/commons/schema/DataSourceConnectionDto.java
index 09038b6c1d..8e655eeecd 100644
--- a/manager/tm-common/src/main/java/com/tapdata/tm/commons/schema/DataSourceConnectionDto.java
+++ b/manager/tm-common/src/main/java/com/tapdata/tm/commons/schema/DataSourceConnectionDto.java
@@ -306,9 +306,10 @@ public class DataSourceConnectionDto extends BaseDto implements IDataPermissionD
 
         private String multiConnectionInstanceId;
         private List<String> namespace;
+        private String datasourceInstanceTag;
 
 
-        /**
+    /**
          * 后续 开放可以多选 flow engine 的话，这里一定要删除
          *
          */
diff --git a/manager/tm-common/src/main/java/com/tapdata/tm/utils/DocumentUtils.java b/manager/tm-common/src/main/java/com/tapdata/tm/utils/DocumentUtils.java
new file mode 100644
index 0000000000..80cf21f214
--- /dev/null
+++ b/manager/tm-common/src/main/java/com/tapdata/tm/utils/DocumentUtils.java
@@ -0,0 +1,31 @@
+package com.tapdata.tm.utils;
+
+import org.bson.Document;
+
+/**
+ * @author samuel
+ * @Description
+ * @create 2024-08-29 12:31
+ **/
+public class DocumentUtils {
+
+	private DocumentUtils() {
+	}
+
+	public static Long getLong(Document document, String key) {
+		if (null == document || null == key) {
+			return 0L;
+		}
+		Object value = document.get(key);
+		if (null == value) {
+			return 0L;
+		}
+		if (value instanceof Long) {
+			return (Long) value;
+		} else if (value instanceof Integer) {
+			return Long.parseLong(value.toString());
+		} else {
+			return 0L;
+		}
+	}
+}
diff --git a/manager/tm-common/src/main/java/com/tapdata/tm/utils/EntityUtils.java b/manager/tm-common/src/main/java/com/tapdata/tm/utils/EntityUtils.java
new file mode 100644
index 0000000000..30ec93d96d
--- /dev/null
+++ b/manager/tm-common/src/main/java/com/tapdata/tm/utils/EntityUtils.java
@@ -0,0 +1,24 @@
+package com.tapdata.tm.utils;
+
+import org.springframework.data.mongodb.core.mapping.Document;
+
+/**
+ * @author samuel
+ * @Description
+ * @create 2024-08-29 12:34
+ **/
+public class EntityUtils {
+	private EntityUtils() {
+	}
+
+	public static String documentAnnotationValue(Class<?> clazz) {
+		if (null == clazz) {
+			throw new IllegalArgumentException("Input class is null");
+		}
+		Document annotation = clazz.getAnnotation(Document.class);
+		if (null == annotation) {
+			throw new IllegalArgumentException("Class %s not have a org.springframework.data.mongodb.core.mapping.Document annotation");
+		}
+		return annotation.value();
+	}
+}
diff --git a/manager/tm-common/src/main/java/com/tapdata/tm/utils/OEMReplaceUtil.java b/manager/tm-common/src/main/java/com/tapdata/tm/utils/OEMReplaceUtil.java
index 64e4f26eab..d451228660 100644
--- a/manager/tm-common/src/main/java/com/tapdata/tm/utils/OEMReplaceUtil.java
+++ b/manager/tm-common/src/main/java/com/tapdata/tm/utils/OEMReplaceUtil.java
@@ -35,7 +35,6 @@ public class OEMReplaceUtil {
      * */
     public static String getOEMConfigPath(String fileName){
         String oemType = oemType();
-        TapLogger.info(OEMReplaceUtil.class.getSimpleName(), "Get oem type from evn: {}", String.valueOf(oemType));
         if (null == fileName || "".equals(fileName.trim()) || null == oemType) return null;
         return Optional.ofNullable(System.getenv("oem_file_path")).orElse(path) + oemType + (fileName.startsWith("/") ? "" : "/") + fileName;
     }
@@ -60,7 +59,7 @@ public static InputStream getOEMConfigInputStream(String fileName) throws FileNo
             //return org.springframework.util.ResourceUtils.getFile(configPath);//new File(configPath);
         }catch (Exception e){
             //TapLogger.info("FileNotFound in path {}", configPath);
-            TapLogger.warn("OEM-REPLACE", "OEM name is {} in evn, but file not found in path {}", oemType(), configPath);
+            TapLogger.warn("APPLICATION-REPLACE", "APPLICATION name is {} in evn, but file not found in path {}", oemType(), configPath);
             //throw new CoreException("OEM name is {} in evn, but file not found in path {}", oemType(), configPath);
         }
         return null;
diff --git a/manager/tm-oss/src/main/java/com/tapdata/tm/task/service/LogCollectorServiceImpl.java b/manager/tm-oss/src/main/java/com/tapdata/tm/task/service/LogCollectorServiceImpl.java
index 602614969b..2fbb632aab 100644
--- a/manager/tm-oss/src/main/java/com/tapdata/tm/task/service/LogCollectorServiceImpl.java
+++ b/manager/tm-oss/src/main/java/com/tapdata/tm/task/service/LogCollectorServiceImpl.java
@@ -178,13 +178,13 @@ public void cancelMerge(String taskId, String connectionId, UserDetail user) {
     }
 
     @Override
-    public Page<ShareCdcTableInfo> tableInfos(String taskId, String connectionId, String keyword, Integer page, Integer size, UserDetail user) {
+    public Page<ShareCdcTableInfo> tableInfos(String taskId, String connectionId, String keyword, Integer page, Integer size, String order, UserDetail user) {
         Page<ShareCdcTableInfo> shareCdcTableInfoPage = new Page<>();
         return shareCdcTableInfoPage;
     }
 
     @Override
-    public Page<ShareCdcTableInfo> excludeTableInfos(String taskId, String connectionId, String keyword, Integer page, Integer size, UserDetail user) {
+    public Page<ShareCdcTableInfo> excludeTableInfos(String taskId, String connectionId, String keyword, Integer page, Integer size, String order, UserDetail user) {
         throw new BizException("TapOssNonSupportFunctionException");
     }
 
diff --git a/manager/tm-sdk/pom.xml b/manager/tm-sdk/pom.xml
index 66625d45d3..8b8bce63a6 100644
--- a/manager/tm-sdk/pom.xml
+++ b/manager/tm-sdk/pom.xml
@@ -22,7 +22,7 @@
 		<dependency>
 			<groupId>io.tapdata</groupId>
 			<artifactId>tapdata-api</artifactId>
-			<version>1.3.8-SNAPSHOT</version>
+			<version>1.3.9-SNAPSHOT</version>
 		</dependency>
 
     <dependency>
diff --git a/manager/tm/src/main/java/com/tapdata/tm/Settings/controller/SettingsController.java b/manager/tm/src/main/java/com/tapdata/tm/Settings/controller/SettingsController.java
index 6995dfa2ec..10472ba0b5 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/Settings/controller/SettingsController.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/Settings/controller/SettingsController.java
@@ -1,5 +1,6 @@
 package com.tapdata.tm.Settings.controller;
 
+import com.tapdata.tm.Settings.dto.SendMailResponseDto;
 import com.tapdata.tm.Settings.dto.SettingsDto;
 import com.tapdata.tm.Settings.dto.TestMailDto;
 import com.tapdata.tm.Settings.entity.Settings;
@@ -116,8 +117,8 @@ public ResponseMessage<Void> updateAlarm(@RequestBody UpdateRuleDto ruleDto) {
 
     @Operation(summary = "test send mail")
     @PostMapping("testEmail")
-    public ResponseMessage<Void> testSendMail(@RequestBody TestMailDto testMailDto) {
-        settingsService.testSendMail(testMailDto);
-        return success();
+    public ResponseMessage<SendMailResponseDto> testSendMail(@RequestBody TestMailDto testMailDto) {
+        SendMailResponseDto res = settingsService.testSendMail(testMailDto);
+        return success(res);
     }
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/Settings/service/SettingsServiceImpl.java b/manager/tm/src/main/java/com/tapdata/tm/Settings/service/SettingsServiceImpl.java
index dfcb34cf6f..e2672b6d8e 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/Settings/service/SettingsServiceImpl.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/Settings/service/SettingsServiceImpl.java
@@ -6,6 +6,7 @@
 import com.tapdata.tm.Settings.constant.KeyEnum;
 import com.tapdata.tm.Settings.constant.SettingsEnum;
 import com.tapdata.tm.Settings.dto.MailAccountDto;
+import com.tapdata.tm.Settings.dto.SendMailResponseDto;
 import com.tapdata.tm.Settings.dto.SettingsDto;
 import com.tapdata.tm.Settings.dto.TestMailDto;
 import com.tapdata.tm.Settings.entity.Settings;
@@ -108,7 +109,8 @@ public MailAccountDto getMailAccount(String userId) {
         String password = Objects.nonNull(pwd) ? pwd.toString() : null;
         String protocol = (String) collect.get("email.server.tls");
         String proxyHost = (String) collect.get("smtp.proxy.host");
-        String proxyPort = (String) collect.getOrDefault("smtp.proxy.port", "0");
+        String proxyPort = (String) collect.get("smtp.proxy.port");
+        proxyPort = StringUtils.isNotBlank(proxyPort) ? proxyPort : "0";
 
         AtomicReference<List<String>> receiverList = new AtomicReference<>(new ArrayList<>());
 
@@ -307,7 +309,7 @@ public List<Settings> findAll(Query query) {
         return mongoTemplate.find(query, Settings.class);
     }
 
-    public void testSendMail(TestMailDto testMailDto) {
+    public SendMailResponseDto testSendMail(TestMailDto testMailDto) {
         MailAccountDto mailAccount = getMailAccount(testMailDto);
 
         if ("*****".equals(mailAccount.getPass())) {
@@ -315,7 +317,7 @@ public void testSendMail(TestMailDto testMailDto) {
             mailAccount.setPass(value);
         }
 
-        MailUtils.sendHtmlEmail(mailAccount, mailAccount.getReceivers(), testMailDto.getTitle(), testMailDto.getText());
+        return MailUtils.sendHtmlEmail(mailAccount, mailAccount.getReceivers(), testMailDto.getTitle(), testMailDto.getText());
 
     }
 
diff --git a/manager/tm/src/main/java/com/tapdata/tm/apiCalls/service/ApiCallService.java b/manager/tm/src/main/java/com/tapdata/tm/apiCalls/service/ApiCallService.java
index 7635ad0428..7557ed1fd1 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/apiCalls/service/ApiCallService.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/apiCalls/service/ApiCallService.java
@@ -1,21 +1,30 @@
 package com.tapdata.tm.apiCalls.service;
 
 import cn.hutool.core.bean.BeanUtil;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoCursor;
 import com.tapdata.tm.apiCalls.dto.ApiCallDto;
 import com.tapdata.tm.apiCalls.entity.ApiCallEntity;
 import com.tapdata.tm.apiCalls.vo.ApiCallDetailVo;
+import com.tapdata.tm.apicallminutestats.dto.ApiCallMinuteStatsDto;
+import com.tapdata.tm.apicallminutestats.service.ApiCallMinuteStatsService;
+import com.tapdata.tm.apicallstats.dto.ApiCallStatsDto;
+import com.tapdata.tm.apicallstats.service.ApiCallStatsService;
 import com.tapdata.tm.application.dto.ApplicationDto;
 import com.tapdata.tm.application.service.ApplicationService;
 import com.tapdata.tm.base.dto.*;
+import com.tapdata.tm.base.exception.BizException;
 import com.tapdata.tm.config.security.UserDetail;
 import com.tapdata.tm.modules.dto.ModulesDto;
 import com.tapdata.tm.modules.entity.ModulesEntity;
-import com.tapdata.tm.modules.param.ApiDetailParam;
 import com.tapdata.tm.modules.service.ModulesService;
+import com.tapdata.tm.utils.EntityUtils;
 import com.tapdata.tm.utils.MongoUtils;
+import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.bson.Document;
 import org.bson.types.ObjectId;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Sort;
@@ -24,9 +33,14 @@
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.stereotype.Service;
 
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
 import java.util.*;
 import java.util.stream.Collectors;
 
+import static com.tapdata.tm.utils.DocumentUtils.getLong;
+
 /**
  * @Author:
  * @Date: 2021/10/15
@@ -34,18 +48,19 @@
  */
 @Service
 @Slf4j
+@Setter(onMethod_ = {@Autowired})
 public class ApiCallService {
-    @Autowired
+    ApiCallMinuteStatsService apiCallMinuteStatsService;
     MongoTemplate mongoOperations;
-
-    @Autowired
     ModulesService modulesService;
-
-    @Autowired
     ApplicationService applicationService;
+    ApiCallStatsService apiCallStatsService;
 
-    public ApiCallDto findOne(Query query) {
-        return null;
+    public ApiCallService() {
+    }
+
+    public ApiCallEntity findOne(Query query) {
+        return mongoOperations.findOne(query, ApiCallEntity.class);
     }
 
     public ApiCallDto upsertByWhere(Where where, ApiCallDto metadataDefinition, UserDetail loginUser) {
@@ -286,27 +301,6 @@ public ApiCallDto findOne(Filter filter, UserDetail loginUser) {
         return null;
     }
 
-    public Map<String, List<ApiCallEntity>> getVisitTotalCount(List<ApiCallEntity> apiCallEntityList) {
-        return apiCallEntityList.stream().collect(Collectors.groupingBy(item -> {
-            if ("200".equals(item.getCode())) {
-                return "normal";
-            } else {
-                return "warning";
-            }
-        }));
-    }
-
-
-    /**
-     * 获取传输的数据量
-     *
-     * @return
-     */
-    public Long getTransmitTotal(List<ApiCallEntity> apiCallEntityList) {
-        Long totalScore = apiCallEntityList.stream().mapToLong(ApiCallEntity::getReqBytes).sum();
-        return totalScore;
-    }
-
     public List<ApiCallEntity> findByModuleIds(List<String> moduleIdList) {
         Query query = Query.query(Criteria.where("allPathId").in(moduleIdList));
         query.with(Sort.by("createTime").descending());
@@ -331,52 +325,23 @@ public List<ApiCallEntity> findByUser(List<ModulesDto> modulesDtoList) {
         return apiCallEntityList;
     }
 
-
-    public Long getVisitTotalLine(List<ApiCallEntity> apiCallEntityList) {
-        Long totalLine = apiCallEntityList.stream().filter(apiCallEntity -> null != apiCallEntity.getResRows()).mapToLong(ApiCallEntity::getResRows).sum();
-        return totalLine;
-    }
-
-    /**
-     * 处理 where 第一层过滤条件
-     *
-     * @param where
-     */
-    private void parseWhereCondition(Where where, Query query) {
-        where.forEach((prop, value) -> {
-            if (!query.getQueryObject().containsKey(prop)) {
-                query.addCriteria(Criteria.where(prop).is(value));
-            }
-        });
-    }
-
-    private void parseFieldCondition(Field fields, Query query) {
-        if (null != fields) {
-            fields.forEach((filedName, get) -> {
-                if ((Boolean) get) {
-                    query.fields().include(filedName);
-                }
-            });
-        }
-    }
-
     public List<Map<String, String>> findClients(List<String> moduleIdList) {
         List<Map<String, String>> result = new ArrayList<>();
-        Query query = Query.query(Criteria.where("is_deleted").ne(true));
+        Query query = new Query();
         if (CollectionUtils.isNotEmpty(moduleIdList)) {
-            query.addCriteria(Criteria.where("allPathId").in(moduleIdList));
+            query.addCriteria(Criteria.where("moduleId").in(moduleIdList));
         }
-        query.fields().include("user_info.clientId");
-        List<ApiCallEntity> modulesDtoList = mongoOperations.find(query, ApiCallEntity.class);
-        List<Map> userInfoMap = modulesDtoList.stream().map(ApiCallEntity::getUserInfo).collect(Collectors.toList());
-
-        List<String> clientIdList = new ArrayList<>();
-        userInfoMap.forEach(userInfo -> {
-            String clientId = (String) userInfo.get("clientId");
-            clientIdList.add(clientId);
+        query.fields().include("clientIds");
+        List<ApiCallStatsDto> apiCallStatsDtoList = apiCallStatsService.findAll(query);
+        Set<String> clientIdSet = new HashSet<>();
+        apiCallStatsDtoList.forEach(apiCallStatsDto -> {
+            Set<String> clientIds = apiCallStatsDto.getClientIds();
+            if (CollectionUtils.isNotEmpty(clientIds)) {
+                clientIdSet.addAll(clientIds);
+            }
         });
 
-        List<ApplicationDto> applicationDtoList = applicationService.findByIds(clientIdList);
+        List<ApplicationDto> applicationDtoList = applicationService.findByIds(new ArrayList<>(clientIdSet));
         applicationDtoList.forEach(applicationDto -> {
             Map<String, String> map = new HashMap<>();
             map.put("id", applicationDto.getId().toString());
@@ -387,51 +352,6 @@ public List<Map<String, String>> findClients(List<String> moduleIdList) {
         return result;
     }
 
-    public List<ApiCallEntity> findByClientId(String clientId) {
-        Query query = Query.query(Criteria.where("user_info.clientId").is(clientId));
-        List<ApiCallEntity> apiCallEntityList = mongoOperations.find(query, ApiCallEntity.class);
-        return apiCallEntityList;
-    }
-
-    public List<ApiCallEntity> findByModuleIdAndTimePeriod(List<String> moduleIds, Date startTime, Date endTime) {
-        Criteria startTimeCri = Criteria.where("createTime").gte(startTime);
-        Criteria endTimeCri = Criteria.where("createTime").lte(endTime);
-        Query query = Query.query(Criteria.where("allPathId").in(moduleIds).andOperator(startTimeCri, endTimeCri));
-        query.with(Sort.by("createTime").descending());
-        List<ApiCallEntity> apiCallEntityList = new ArrayList<>();
-        apiCallEntityList = mongoOperations.find(query, ApiCallEntity.class);
-        return apiCallEntityList;
-    }
-
-    /**
-     * 处理filter里面的or 请求，传话成Criteria
-     *
-     * @param where
-     * @return
-     */
-    private Criteria parseOrToCriteria(Where where, String queryField) {
-        //处理关键字搜索
-        Criteria nameCriteria = new Criteria();
-        if (null != where.get("or")) {
-            List<Criteria> criteriaList = new ArrayList<>();
-            List<Map<String, Map<String, String>>> orList = (List) where.remove("or");
-            for (Map<String, Map<String, String>> orMap : orList) {
-                orMap.forEach((key, value) -> {
-                    if (value.containsKey("$regex")) {
-                        String queryStr = value.get("$regex");
-                        if (queryField.equals(key)) {
-                            Criteria orCriteria = Criteria.where(key).regex(queryStr);
-                            criteriaList.add(orCriteria);
-                        }
-                    }
-                });
-            }
-            nameCriteria = new Criteria().orOperator(criteriaList);
-        }
-        return nameCriteria;
-    }
-
-
     private String getValueFromOrList(List<Map<String, Map<String, String>>> orList, String fieldName) {
         String fieldValue = "";
         for (Map<String, Map<String, String>> orMap : orList) {
@@ -444,22 +364,206 @@ private String getValueFromOrList(List<Map<String, Map<String, String>>> orList,
         return fieldValue;
     }
 
-    public List<ApiCallEntity> findMonitorApiCall(ApiDetailParam apiDetailParam) {
-        List<ApiCallEntity> apiCallEntityList = new ArrayList<>();
+    public ApiCallStatsDto aggregateByAllPathId(String allPathId, String lastApiCallId) {
+        if (StringUtils.isBlank(allPathId)) {
+            return null;
+        }
+        ApiCallStatsDto apiCallStatsDto = new ApiCallStatsDto();
+        apiCallStatsDto.setModuleId(allPathId);
+        String apiCallCollectionName;
+        try {
+            apiCallCollectionName = EntityUtils.documentAnnotationValue(ApiCallEntity.class);
+        } catch (Exception e) {
+            throw new BizException("Get ApiCallEntity's collection name failed", e);
+        }
+        MongoCollection<Document> apiCallCollection = mongoOperations.getCollection(apiCallCollectionName);
 
-        List<String> clientIdList = apiDetailParam.getClientId();
+        // Build aggregation pipeline
+        Document match = new Document("allPathId", allPathId);
+        if (StringUtils.isNotBlank(lastApiCallId)) {
+            match.append("_id", new Document("$gt", new ObjectId(lastApiCallId)));
+        }
+        List<Document> pipeline = Arrays.asList(new Document("$match", match),
+                new Document("$facet",
+                        new Document("callTotalCount", Arrays.asList(new Document("$group", new Document("_id", "$allPathId").append("data", new Document("$sum", 1L)))))
+                                .append("transferDataTotalBytes", Arrays.asList(new Document("$group", new Document("_id", "$allPathId").append("data", new Document("$sum", "$req_bytes")))))
+                                .append("callAlarmTotalCount", Arrays.asList(new Document("$match", new Document("code", new Document("$ne", "200"))), new Document("$group", new Document("_id", "$allPathId").append("data", new Document("$sum", 1L)))))
+                                .append("responseDataRowTotalCount", Arrays.asList(new Document("$group", new Document("_id", "$allPathId").append("data", new Document("$sum", "$res_rows")))))
+                                .append("totalResponseTime", Arrays.asList(new Document("$group", new Document("_id", "$allPathId").append("data", new Document("$sum", "$latency")))))
+                                .append("lastApiCallId", Arrays.asList(new Document("$project", new Document("_id", 1L)), new Document("$sort", new Document("_id", -1L)), new Document("$limit", 1L)))
+                                .append("maxResponseTime", Arrays.asList(new Document("$group", new Document("_id", "$allPathId").append("data", new Document("$max", "$latency")))))
+                                .append("clientIds", Arrays.asList(new Document("$group", new Document("_id", "$user_info.clientId"))))
+                )
+        );
+        if (log.isDebugEnabled()) {
+            StringBuilder pipelineString = new StringBuilder();
+            pipeline.forEach(document -> pipelineString.append(document.toJson()).append(System.lineSeparator()));
+            log.debug("ApiCallStatsService.aggregateApiCallStats pipeline: {}{}", System.lineSeparator(), pipelineString);
+        }
 
-        List<String> moduleIds = new ArrayList<>();
-        moduleIds.add(apiDetailParam.getId());
-        Criteria criteria = Criteria.where("allPathId").in(moduleIds);
+        // Execute aggregation
+        long startMs = System.currentTimeMillis();
+        try (
+                MongoCursor<Document> iterator = apiCallCollection.aggregate(pipeline, Document.class).allowDiskUse(true).iterator()
+        ) {
+            if (iterator.hasNext()) {
+                // Parse the result of the aggregation, and set value in ApiCallStatsDto
+                Document doc = iterator.next();
+                long costMs = System.currentTimeMillis() - startMs;
+                if (log.isDebugEnabled()) {
+                    log.debug("Execute aggregate by module id [{}], get result document: {}, cost: {} ms", allPathId, doc.toJson(), costMs);
+                }
+                List<?> tempList;
+                // callTotalCount
+                Object callTotalCount = doc.get("callTotalCount");
+                if (callTotalCount instanceof List) {
+                    tempList = (List<?>) callTotalCount;
+                    if (!tempList.isEmpty()) {
+                        apiCallStatsDto.setCallTotalCount(getLong((Document) tempList.get(0), "data"));
+                    }
+                }
+                // transferDataTotalBytes
+                Object transferDataTotalBytes = doc.get("transferDataTotalBytes");
+                if (transferDataTotalBytes instanceof List) {
+                    tempList = (List<?>) transferDataTotalBytes;
+                    if (!tempList.isEmpty()) {
+                        apiCallStatsDto.setTransferDataTotalBytes(getLong((Document) tempList.get(0), "data"));
+                    }
+                }
+                // callAlarmTotalCount
+                Object callAlarmTotalCount = doc.get("callAlarmTotalCount");
+                if (callAlarmTotalCount instanceof List) {
+                    tempList = (List<?>) callAlarmTotalCount;
+                    if (!tempList.isEmpty()) {
+                        apiCallStatsDto.setCallAlarmTotalCount(getLong((Document) tempList.get(0), "data"));
+                    }
+                }
+                // responseDataRowTotalCount
+                Object responseDataRowTotalCount = doc.get("responseDataRowTotalCount");
+                if (responseDataRowTotalCount instanceof List) {
+                    tempList = (List<?>) responseDataRowTotalCount;
+                    if (!tempList.isEmpty()) {
+                        apiCallStatsDto.setResponseDataRowTotalCount(getLong((Document) tempList.get(0), "data"));
+                    }
+                }
+                // totalResponseTime
+                Object totalResponseTime = doc.get("totalResponseTime");
+                if (totalResponseTime instanceof List) {
+                    tempList = (List<?>) totalResponseTime;
+                    if (!tempList.isEmpty()) {
+                        apiCallStatsDto.setTotalResponseTime(getLong((Document) tempList.get(0), "data"));
+                    }
+                }
+                // maxResponseTime
+                Object maxResponseTime = doc.get("maxResponseTime");
+                if (maxResponseTime instanceof List) {
+                    tempList = (List<?>) maxResponseTime;
+                    if (!tempList.isEmpty()) {
+                        apiCallStatsDto.setMaxResponseTime(getLong((Document) tempList.get(0), "data"));
+                    }
+                }
+                // lastApiCallId
+                Object lastApiCallIdObj = doc.get("lastApiCallId");
+                if (lastApiCallIdObj instanceof List) {
+                    tempList = (List<?>) lastApiCallIdObj;
+                    if (!tempList.isEmpty()) {
+                        apiCallStatsDto.setLastApiCallId(((Document) tempList.get(0)).getObjectId("_id").toString());
+                    }
+                }
+                // clientIds
+                Object clientIds = doc.get("clientIds");
+                if (clientIds instanceof List) {
+                    tempList = (List<?>) clientIds;
+                    if (!tempList.isEmpty()) {
+                        tempList.stream().map(t -> ((Document) t).getString("_id")).filter(Objects::nonNull).forEach(apiCallStatsDto.getClientIds()::add);
+                    }
+                }
+            }
+        }
+
+        return apiCallStatsDto;
+    }
 
-        criteria.and("user_info.clientId").in(clientIdList);
+    public List<ApiCallMinuteStatsDto> aggregateMinuteByAllPathId(String allPathId, String lastApiCallId, Date startTime) {
+        List<ApiCallMinuteStatsDto> apiCallMinuteStatsDtoList = new ArrayList<>();
+        String apiCallCollectionName;
+        try {
+            apiCallCollectionName = EntityUtils.documentAnnotationValue(ApiCallEntity.class);
+        } catch (Exception e) {
+            throw new BizException("Get ApiCallEntity's collection name failed", e);
+        }
+        MongoCollection<Document> apiCallCollection = mongoOperations.getCollection(apiCallCollectionName);
 
-        Query query = new Query().addCriteria(criteria);
-        apiCallEntityList = mongoOperations.find(query, ApiCallEntity.class);
+        // Build aggregation pipeline
+        Document match = new Document("allPathId", allPathId);
+        if (StringUtils.isNotBlank(lastApiCallId)) {
+            match.append("_id", new Document("$gt", new ObjectId(lastApiCallId)));
+        }
+        if (null != startTime) {
+            match.append("createTime", new Document("$gte", startTime));
+        }
+        List<Document> pipeline = new ArrayList<>();
+        pipeline.add(new Document("$match", match));
+        pipeline.add(new Document("$project", new Document("year", new Document("$year", "$createTime"))
+                .append("month", new Document("$month", "$createTime"))
+                .append("day", new Document("$dayOfMonth", "$createTime"))
+                .append("hour", new Document("$hour", "$createTime"))
+                .append("minute", new Document("$minute", "$createTime"))
+                .append("res_rows", 1)
+                .append("latency", 1)
+                .append("req_bytes", 1)
+        ));
+        Document group = new Document("_id", groupByMinute())
+                .append("responseDataRowTotalCount", new Document("$sum", "$res_rows"))
+                .append("totalResponseTime", new Document("$sum", "$latency"))
+                .append("transferDataTotalBytes", new Document("$sum", "$req_bytes"))
+                .append("lastApiCallId", new Document("$last", "$_id"));
+        pipeline.add(new Document("$group", group));
+        if (log.isDebugEnabled()) {
+            StringBuilder pipelineString = new StringBuilder();
+            pipeline.forEach(document -> pipelineString.append(document.toJson()).append(System.lineSeparator()));
+            log.debug("ApiCallStatsService.aggregateMinuteByAllPathId pipeline: {}{}", System.lineSeparator(), pipelineString);
+        }
+        // Execute aggregation
+        try (
+                MongoCursor<Document> iterator = apiCallCollection.aggregate(pipeline, Document.class).allowDiskUse(true).iterator()
+        ) {
+            while (iterator.hasNext()) {
+                Document document = iterator.next();
+                ApiCallMinuteStatsDto apiCallMinuteStatsDto = new ApiCallMinuteStatsDto();
+                apiCallMinuteStatsDto.setModuleId(allPathId);
+
+                apiCallMinuteStatsDto.setResponseDataRowTotalCount(getLong(document, "responseDataRowTotalCount"));
+                apiCallMinuteStatsDto.setTotalResponseTime(getLong(document, "totalResponseTime"));
+                apiCallMinuteStatsDto.setTransferDataTotalBytes(getLong(document, "transferDataTotalBytes"));
+                // responseTimePerRow, rowPerSecond
+                apiCallMinuteStatsService.calculate(apiCallMinuteStatsDto);
+				if (null != document.get("lastApiCallId")) {
+					apiCallMinuteStatsDto.setLastApiCallId(document.getObjectId("lastApiCallId").toString());
+				}
+                // apiCallTime: year, month, day, hour, minute
+                Document id = document.get("_id", Document.class);
+                Instant apiCallTime = LocalDateTime.of(
+						id.getInteger("year"),
+						id.getInteger("month"),
+						id.getInteger("day"),
+						id.getInteger("hour"),
+						id.getInteger("minute")
+				).toInstant(ZoneOffset.UTC);
+                apiCallMinuteStatsDto.setApiCallTime(Date.from(apiCallTime));
+
+                apiCallMinuteStatsDtoList.add(apiCallMinuteStatsDto);
+            }
+        }
 
-        return apiCallEntityList;
+        return apiCallMinuteStatsDtoList;
     }
 
-
+    public Document groupByMinute() {
+        return new Document("year", "$year")
+                .append("month", "$month")
+                .append("day", "$day")
+                .append("hour", "$hour")
+                .append("minute", "$minute");
+    }
 }
\ No newline at end of file
diff --git a/manager/tm/src/main/java/com/tapdata/tm/apicallstats/controller/ApiCallStatsController.java b/manager/tm/src/main/java/com/tapdata/tm/apicallstats/controller/ApiCallStatsController.java
new file mode 100644
index 0000000000..3f5b2d4b53
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/apicallstats/controller/ApiCallStatsController.java
@@ -0,0 +1,239 @@
+package com.tapdata.tm.apicallstats.controller;
+
+import com.tapdata.tm.base.controller.BaseController;
+import com.tapdata.tm.base.dto.*;
+import com.tapdata.tm.apicallstats.dto.ApiCallStatsDto;
+import com.tapdata.tm.apicallstats.service.ApiCallStatsService;
+import com.tapdata.tm.utils.MongoUtils;
+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 org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * @Date: 2024/08/26
+ * @Description:
+ */
+@Tag(name = "ApiCallStats", description = "ApiCallStats相关接口")
+@RestController
+@RequestMapping("/api/ApiCallStats")
+public class ApiCallStatsController extends BaseController {
+
+    @Autowired
+    private ApiCallStatsService apiCallStatsService;
+
+    /**
+     * Create a new instance of the model and persist it into the data source
+     * @param apiCallStats
+     * @return
+     */
+    @Operation(summary = "Create a new instance of the model and persist it into the data source")
+    @PostMapping
+    public ResponseMessage<ApiCallStatsDto> save(@RequestBody ApiCallStatsDto apiCallStats) {
+        apiCallStats.setId(null);
+        return success(apiCallStatsService.save(apiCallStats, getLoginUser()));
+    }
+
+    /**
+     *  Patch an existing model instance or insert a new one into the data source
+     * @param apiCallStats
+     * @return
+     */
+    @Operation(summary = "Patch an existing model instance or insert a new one into the data source")
+    @PatchMapping()
+    public ResponseMessage<ApiCallStatsDto> update(@RequestBody ApiCallStatsDto apiCallStats) {
+        return success(apiCallStatsService.save(apiCallStats, getLoginUser()));
+    }
+
+
+    /**
+     * Find all instances of the model matched by filter from the data source
+     * @param filterJson
+     * @return
+     */
+    @Operation(summary = "Find all instances of the model matched by filter from the data source")
+    @GetMapping
+    public ResponseMessage<Page<ApiCallStatsDto>> find(
+            @Parameter(in = ParameterIn.QUERY,
+                    description = "Filter defining fields, where, sort, skip, and limit - must be a JSON-encoded string (`{\"where\":{\"something\":\"value\"},\"fields\":{\"something\":true|false},\"sort\": [\"name desc\"],\"page\":1,\"size\":20}`)."
+            )
+            @RequestParam(value = "filter", required = false) String filterJson) {
+        Filter filter = parseFilter(filterJson);
+        if (filter == null) {
+            filter = new Filter();
+        }
+        return success(apiCallStatsService.find(filter, getLoginUser()));
+    }
+
+    /**
+     *  Replace an existing model instance or insert a new one into the data source
+     * @param apiCallStats
+     * @return
+     */
+    @Operation(summary = "Replace an existing model instance or insert a new one into the data source")
+    @PutMapping
+    public ResponseMessage<ApiCallStatsDto> put(@RequestBody ApiCallStatsDto apiCallStats) {
+        return success(apiCallStatsService.replaceOrInsert(apiCallStats, getLoginUser()));
+    }
+
+
+    /**
+     * Check whether a model instance exists in the data source
+     * @return
+     */
+    @Operation(summary = "Check whether a model instance exists in the data source")
+    @RequestMapping(value = "{id}", method = RequestMethod.HEAD)
+    public ResponseMessage<HashMap<String, Boolean>> checkById(@PathVariable("id") String id) {
+        long count = apiCallStatsService.count(Where.where("_id", MongoUtils.toObjectId(id)), getLoginUser());
+        HashMap<String, Boolean> existsValue = new HashMap<>();
+        existsValue.put("exists", count > 0);
+        return success(existsValue);
+    }
+
+    /**
+     *  Patch attributes for a model instance and persist it into the data source
+     * @param apiCallStats
+     * @return
+     */
+    @Operation(summary = "Patch attributes for a model instance and persist it into the data source")
+    @PatchMapping("{id}")
+    public ResponseMessage<ApiCallStatsDto> updateById(@PathVariable("id") String id, @RequestBody ApiCallStatsDto apiCallStats) {
+        apiCallStats.setId(MongoUtils.toObjectId(id));
+        return success(apiCallStatsService.save(apiCallStats, getLoginUser()));
+    }
+
+
+    /**
+     * Find a model instance by {{id}} from the data source
+     * @param fieldsJson
+     * @return
+     */
+    @Operation(summary = "Find a model instance by {{id}} from the data source")
+    @GetMapping("{id}")
+    public ResponseMessage<ApiCallStatsDto> findById(@PathVariable("id") String id,
+            @RequestParam(value = "fields", required = false) String fieldsJson) {
+        Field fields = parseField(fieldsJson);
+        return success(apiCallStatsService.findById(MongoUtils.toObjectId(id),  fields, getLoginUser()));
+    }
+
+    /**
+     *  Replace attributes for a model instance and persist it into the data source.
+     * @param apiCallStats
+     * @return
+     */
+    @Operation(summary = "Replace attributes for a model instance and persist it into the data source.")
+    @PutMapping("{id}")
+    public ResponseMessage<ApiCallStatsDto> replceById(@PathVariable("id") String id, @RequestBody ApiCallStatsDto apiCallStats) {
+        return success(apiCallStatsService.replaceById(MongoUtils.toObjectId(id), apiCallStats, getLoginUser()));
+    }
+
+    /**
+     *  Replace attributes for a model instance and persist it into the data source.
+     * @param apiCallStats
+     * @return
+     */
+    @Operation(summary = "Replace attributes for a model instance and persist it into the data source.")
+    @PostMapping("{id}/replace")
+    public ResponseMessage<ApiCallStatsDto> replaceById2(@PathVariable("id") String id, @RequestBody ApiCallStatsDto apiCallStats) {
+        return success(apiCallStatsService.replaceById(MongoUtils.toObjectId(id), apiCallStats, getLoginUser()));
+    }
+
+
+
+    /**
+     * Delete a model instance by {{id}} from the data source
+     * @param id
+     * @return
+     */
+    @Operation(summary = "Delete a model instance by {{id}} from the data source")
+    @DeleteMapping("{id}")
+    public ResponseMessage<Void> delete(@PathVariable("id") String id) {
+        apiCallStatsService.deleteById(MongoUtils.toObjectId(id), getLoginUser());
+        return success();
+    }
+
+    /**
+     *  Check whether a model instance exists in the data source
+     * @param id
+     * @return
+     */
+    @Operation(summary = "Check whether a model instance exists in the data source")
+    @GetMapping("{id}/exists")
+    public ResponseMessage<HashMap<String, Boolean>> checkById1(@PathVariable("id") String id) {
+        long count = apiCallStatsService.count(Where.where("_id", MongoUtils.toObjectId(id)), getLoginUser());
+        HashMap<String, Boolean> existsValue = new HashMap<>();
+        existsValue.put("exists", count > 0);
+        return success(existsValue);
+    }
+
+    /**
+     *  Count instances of the model matched by where from the data source
+     * @param whereJson
+     * @return
+     */
+    @Operation(summary = "Count instances of the model matched by where from the data source")
+    @GetMapping("count")
+    public ResponseMessage<HashMap<String, Long>> count(@RequestParam("where") String whereJson) {
+        Where where = parseWhere(whereJson);
+        if (where == null) {
+            where = new Where();
+        }
+        long count = apiCallStatsService.count(where, getLoginUser());
+        HashMap<String, Long> countValue = new HashMap<>();
+        countValue.put("count", count);
+        return success(countValue);
+    }
+
+    /**
+     *  Find first instance of the model matched by filter from the data source.
+     * @param filterJson
+     * @return
+     */
+    @Operation(summary = "Find first instance of the model matched by filter from the data source.")
+    @GetMapping("findOne")
+    public ResponseMessage<ApiCallStatsDto> findOne(
+            @Parameter(in = ParameterIn.QUERY,
+                    description = "Filter defining fields, where, sort, skip, and limit - must be a JSON-encoded string (`{\"where\":{\"something\":\"value\"},\"field\":{\"something\":true|false},\"sort\": [\"name desc\"],\"page\":1,\"size\":20}`)."
+            )
+            @RequestParam(value = "filter", required = false) String filterJson) {
+        Filter filter = parseFilter(filterJson);
+        if (filter == null) {
+            filter = new Filter();
+        }
+        return success(apiCallStatsService.findOne(filter, getLoginUser()));
+    }
+
+    /**
+     *  Update instances of the model matched by {{where}} from the data source.
+     * @param whereJson
+     * @return
+     */
+    @Operation(summary = "Update instances of the model matched by {{where}} from the data source")
+    @PostMapping("update")
+    public ResponseMessage<Map<String, Long>> updateByWhere(@RequestParam("where") String whereJson, @RequestBody ApiCallStatsDto apiCallStats) {
+        Where where = parseWhere(whereJson);
+        long count = apiCallStatsService.updateByWhere(where, apiCallStats, getLoginUser());
+        HashMap<String, Long> countValue = new HashMap<>();
+        countValue.put("count", count);
+        return success(countValue);
+    }
+
+    /**
+     *  Update an existing model instance or insert a new one into the data source based on the where criteria.
+     * @param whereJson
+     * @return
+     */
+    @Operation(summary = "Update an existing model instance or insert a new one into the data source based on the where criteria.")
+    @PostMapping("upsertWithWhere")
+    public ResponseMessage<ApiCallStatsDto> upsertByWhere(@RequestParam("where") String whereJson, @RequestBody ApiCallStatsDto apiCallStats) {
+        Where where = parseWhere(whereJson);
+        return success(apiCallStatsService.upsertByWhere(where, apiCallStats, getLoginUser()));
+    }
+
+}
\ No newline at end of file
diff --git a/manager/tm/src/main/java/com/tapdata/tm/apicallstats/dto/ApiCallStatsDto.java b/manager/tm/src/main/java/com/tapdata/tm/apicallstats/dto/ApiCallStatsDto.java
new file mode 100644
index 0000000000..12b1965cb7
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/apicallstats/dto/ApiCallStatsDto.java
@@ -0,0 +1,44 @@
+package com.tapdata.tm.apicallstats.dto;
+
+import com.tapdata.tm.commons.base.dto.BaseDto;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.HashSet;
+import java.util.Set;
+
+
+/**
+ * ApiCallStats
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class ApiCallStatsDto extends BaseDto {
+	private String moduleId;
+	// Total number of API calls
+	private Long callTotalCount = 0L;
+	// Total size of data transferred by Api (Byte)
+	private Long transferDataTotalBytes = 0L;
+	// Total number of API access alarms
+	private Long callAlarmTotalCount = 0L;
+	// Total number of response data rows accessed by API
+	private Long responseDataRowTotalCount = 0L;
+	// Access failure rate
+	private Double accessFailureRate = 0D;
+	// Total response time(milliseconds)
+	private Long totalResponseTime = 0L;
+	// Max response time(milliseconds)
+	private Long maxResponseTime;
+	// Number of APIs with warning calls(As long as the alarm is called once)
+	private Long alarmApiTotalCount = 0L;
+	/**
+	 * Client id list(associated with {@link com.tapdata.tm.apiCalls.entity.ApiCallEntity}#user_info#clientId)
+	 */
+	private Set<String> clientIds = new HashSet<>();
+	/**
+	 * Associated with {@link com.tapdata.tm.apiCalls.entity.ApiCallEntity}#id
+	 * The last API call ID
+	 * Used to query the last API call record as an offset
+	 */
+	private String lastApiCallId;
+}
\ No newline at end of file
diff --git a/manager/tm/src/main/java/com/tapdata/tm/apicallstats/entity/ApiCallStatsEntity.java b/manager/tm/src/main/java/com/tapdata/tm/apicallstats/entity/ApiCallStatsEntity.java
new file mode 100644
index 0000000000..a539ed0fcc
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/apicallstats/entity/ApiCallStatsEntity.java
@@ -0,0 +1,46 @@
+package com.tapdata.tm.apicallstats.entity;
+
+import com.tapdata.tm.base.entity.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import java.util.Set;
+
+
+/**
+ * ApiCallStats
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Document("ApiCallStats")
+public class ApiCallStatsEntity extends BaseEntity {
+	/**
+	 * Associated with {@link com.tapdata.tm.modules.entity.ModulesEntity}#id
+	 */
+	private String moduleId;
+	// Total number of API calls
+	private Long callTotalCount;
+	// Total size of data transferred by Api (Byte)
+	private Long transferDataTotalBytes;
+	// Total number of API access alarms
+	private Long callAlarmTotalCount;
+	// Total number of response data rows accessed by API
+	private Long responseDataRowTotalCount;
+	// Access failure rate
+	private Double accessFailureRate;
+	// Total response time(milliseconds)
+	private Long totalResponseTime;
+	// Max response time(milliseconds)
+	private Long maxResponseTime;
+	/**
+	 * Client id list(associated with {@link com.tapdata.tm.apiCalls.entity.ApiCallEntity}#user_info#clientId)
+	 */
+	private Set<String> clientIds;
+	/**
+	 * Associated with {@link com.tapdata.tm.apiCalls.entity.ApiCallEntity}#id
+	 * The last API call ID
+	 * Used to query the last API call record as an offset
+	 */
+	private String lastApiCallId;
+}
\ No newline at end of file
diff --git a/manager/tm/src/main/java/com/tapdata/tm/apicallstats/repository/ApiCallStatsRepository.java b/manager/tm/src/main/java/com/tapdata/tm/apicallstats/repository/ApiCallStatsRepository.java
new file mode 100644
index 0000000000..1a0d0fd354
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/apicallstats/repository/ApiCallStatsRepository.java
@@ -0,0 +1,24 @@
+package com.tapdata.tm.apicallstats.repository;
+
+import com.tapdata.tm.base.reporitory.BaseRepository;
+import com.tapdata.tm.apicallstats.entity.ApiCallStatsEntity;
+import org.bson.types.ObjectId;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @Author:
+ * @Date: 2024/08/26
+ * @Description:
+ */
+@Repository
+public class ApiCallStatsRepository extends BaseRepository<ApiCallStatsEntity, ObjectId> {
+    public ApiCallStatsRepository(MongoTemplate mongoOperations) {
+        super(ApiCallStatsEntity.class, mongoOperations);
+    }
+
+    @Override
+    protected void init() {
+        super.init();
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/apicallstats/service/ApiCallStatsService.java b/manager/tm/src/main/java/com/tapdata/tm/apicallstats/service/ApiCallStatsService.java
new file mode 100644
index 0000000000..1405bb0d54
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/apicallstats/service/ApiCallStatsService.java
@@ -0,0 +1,180 @@
+package com.tapdata.tm.apicallstats.service;
+
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoCursor;
+import com.tapdata.tm.apicallstats.dto.ApiCallStatsDto;
+import com.tapdata.tm.apicallstats.entity.ApiCallStatsEntity;
+import com.tapdata.tm.apicallstats.repository.ApiCallStatsRepository;
+import com.tapdata.tm.base.exception.BizException;
+import com.tapdata.tm.base.service.BaseService;
+import com.tapdata.tm.config.security.UserDetail;
+import com.tapdata.tm.utils.DocumentUtils;
+import com.tapdata.tm.utils.EntityUtils;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.bson.Document;
+import org.bson.types.ObjectId;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @Author: sam
+ * @Date: 2024/08/26
+ * @Description: ApiCallStatsService
+ */
+@Service
+@Slf4j
+public class ApiCallStatsService extends BaseService<ApiCallStatsDto, ApiCallStatsEntity, ObjectId, ApiCallStatsRepository> {
+
+	public ApiCallStatsService(@NonNull ApiCallStatsRepository repository) {
+		super(repository, ApiCallStatsDto.class, ApiCallStatsEntity.class);
+	}
+
+	protected void beforeSave(ApiCallStatsDto apiCallStats, UserDetail user) {
+
+	}
+
+	public void merge(ApiCallStatsDto oldStats, ApiCallStatsDto newStats) {
+		if (null == oldStats && null == newStats) {
+			return;
+		}
+		if (null != oldStats) {
+			newStats.setId(oldStats.getId());
+			newStats.setCallTotalCount(newStats.getCallTotalCount() + oldStats.getCallTotalCount());
+			newStats.setTransferDataTotalBytes(newStats.getTransferDataTotalBytes() + oldStats.getTransferDataTotalBytes());
+			newStats.setCallAlarmTotalCount(newStats.getCallAlarmTotalCount() + oldStats.getCallAlarmTotalCount());
+			newStats.setResponseDataRowTotalCount(newStats.getResponseDataRowTotalCount() + oldStats.getResponseDataRowTotalCount());
+			newStats.setTotalResponseTime(newStats.getTotalResponseTime() + oldStats.getTotalResponseTime());
+			if (CollectionUtils.isNotEmpty(oldStats.getClientIds())) {
+				newStats.getClientIds().addAll(oldStats.getClientIds());
+			}
+			newStats.setCreateAt(oldStats.getCreateAt());
+			if (log.isDebugEnabled()) {
+				log.debug("ApiCallStatsService.merge oldStats: {}, newStats: {}", oldStats, newStats);
+			}
+		} else {
+			newStats.setId(new ObjectId());
+			newStats.setCreateAt(new Date());
+		}
+	}
+
+	public ApiCallStatsDto aggregateByUserId(String userId) {
+		ApiCallStatsDto apiCallStatsDto = new ApiCallStatsDto();
+		String collectionName;
+		try {
+			collectionName = EntityUtils.documentAnnotationValue(ApiCallStatsEntity.class);
+		} catch (Exception e) {
+			throw new BizException("Get ApiCallStatsEntity's collection name failed", e);
+		}
+		MongoCollection<org.bson.Document> collection = repository.getMongoOperations().getCollection(collectionName);
+		List<Document> pipeline = new ArrayList<>();
+		if (StringUtils.isNotBlank(userId)) {
+			pipeline.add(new Document("$match", new Document("user_id", userId)));
+		}
+		pipeline.addAll(Arrays.asList(new Document("$facet",
+				new Document("callTotalCount", Arrays.asList(new Document("$group", new Document("_id", null).append("data", new Document("$sum", "$callTotalCount")))))
+						.append("transferDataTotalBytes", Arrays.asList(new Document("$group", new Document("_id", null).append("data", new Document("$sum", "$transferDataTotalBytes")))))
+						.append("callAlarmTotalCount", Arrays.asList(new Document("$group", new Document("_id", null).append("data", new Document("$sum", "$callAlarmTotalCount")))))
+						.append("responseDataRowTotalCount", Arrays.asList(new Document("$group", new Document("_id", "$allPathId").append("data", new Document("$sum", "$responseDataRowTotalCount")))))
+						.append("totalResponseTime", Arrays.asList(new Document("$group", new Document("_id", "$allPathId").append("data", new Document("$sum", "$totalResponseTime")))))
+						.append("alarmApiTotalCount", Arrays.asList(new Document("$match", new Document("accessFailureRate", new Document("$gt", 0))),
+								new Document("$group", new Document("_id", null).append("data", new Document("$sum", 1L)))))
+						.append("lastUpdAt", Arrays.asList(new Document("$group", new Document("_id", null).append("data", new Document("$max", "$last_updated")))))
+		)));
+		if (log.isDebugEnabled()) {
+			StringBuilder pipelineString = new StringBuilder();
+			pipeline.forEach(document -> pipelineString.append(document.toJson()).append(System.lineSeparator()));
+			log.debug("ApiCallStatsService.aggregateByUserId pipeline: {}{}", System.lineSeparator(), pipelineString);
+		}
+		try (
+				MongoCursor<Document> iterator = collection.aggregate(pipeline).allowDiskUse(true).iterator()
+		) {
+			if (iterator.hasNext()) {
+				Document doc = iterator.next();
+				if (log.isDebugEnabled()) {
+					log.debug("ApiCallStatsService.aggregateByUserId doc: {}", doc.toJson());
+				}
+				List<?> tempList;
+				// callTotalCount
+				Object callTotalCount = doc.get("callTotalCount");
+				if (callTotalCount instanceof List) {
+					tempList = (List<?>) callTotalCount;
+					if (!tempList.isEmpty()) {
+						apiCallStatsDto.setCallTotalCount(DocumentUtils.getLong((Document) tempList.get(0), "data"));
+					}
+				}
+				// transferDataTotalBytes
+				Object transferDataTotalBytes = doc.get("transferDataTotalBytes");
+				if (transferDataTotalBytes instanceof List) {
+					tempList = (List<?>) transferDataTotalBytes;
+					if (!tempList.isEmpty()) {
+						apiCallStatsDto.setTransferDataTotalBytes(DocumentUtils.getLong((Document) tempList.get(0), "data"));
+					}
+				}
+				// callAlarmTotalCount
+				Object callAlarmTotalCount = doc.get("callAlarmTotalCount");
+				if (callAlarmTotalCount instanceof List) {
+					tempList = (List<?>) callAlarmTotalCount;
+					if (!tempList.isEmpty()) {
+						apiCallStatsDto.setCallAlarmTotalCount(DocumentUtils.getLong((Document) tempList.get(0), "data"));
+					}
+				}
+				// responseDataRowTotalCount
+				Object responseDataRowTotalCount = doc.get("responseDataRowTotalCount");
+				if (responseDataRowTotalCount instanceof List) {
+					tempList = (List<?>) responseDataRowTotalCount;
+					if (!tempList.isEmpty()) {
+						apiCallStatsDto.setResponseDataRowTotalCount(DocumentUtils.getLong((Document) tempList.get(0), "data"));
+					}
+				}
+				// totalResponseTime
+				Object totalResponseTime = doc.get("totalResponseTime");
+				if (totalResponseTime instanceof List) {
+					tempList = (List<?>) totalResponseTime;
+					if (!tempList.isEmpty()) {
+						apiCallStatsDto.setTotalResponseTime(DocumentUtils.getLong((Document) tempList.get(0), "data"));
+					}
+				}
+				// alarmApiTotalCount
+				Object alarmApiTotalCount = doc.get("alarmApiTotalCount");
+				if (alarmApiTotalCount instanceof List) {
+					tempList = (List<?>) alarmApiTotalCount;
+					if (!tempList.isEmpty()) {
+						apiCallStatsDto.setAlarmApiTotalCount(DocumentUtils.getLong((Document) tempList.get(0), "data"));
+					}
+				}
+				// max last update time
+				Object lastUpdAt = doc.get("lastUpdAt");
+				if (lastUpdAt instanceof List) {
+					tempList = (List<?>) lastUpdAt;
+					if (!tempList.isEmpty()) {
+						Object data = ((Document) tempList.get(0)).get("data");
+						if (data instanceof Date) {
+							apiCallStatsDto.setLastUpdAt((Date) data);
+						}
+					}
+				}
+			}
+		}
+		return apiCallStatsDto;
+	}
+
+	public boolean isEmpty() {
+		Query query = new Query().limit(1);
+		query.fields().include("_id");
+		return !repository.findOne(query).isPresent();
+	}
+
+	public void deleteAllByModuleId(String moduleId) {
+		Query deleteQuery = Query.query(Criteria.where("moduleId").is(moduleId));
+		deleteAll(deleteQuery);
+	}
+}
\ No newline at end of file
diff --git a/manager/tm/src/main/java/com/tapdata/tm/ds/service/impl/DataSourceServiceImpl.java b/manager/tm/src/main/java/com/tapdata/tm/ds/service/impl/DataSourceServiceImpl.java
index 5719093770..ad17251cf9 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/ds/service/impl/DataSourceServiceImpl.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/ds/service/impl/DataSourceServiceImpl.java
@@ -1892,7 +1892,7 @@ public void updateConnectionOptions(ObjectId id, ConnectionOptions options, User
 
         Criteria criteria = Criteria.where("_id").is(id);
         Query query = new Query(criteria);
-        query.fields().include("_id", "database_type");
+        query.fields().include("_id", "database_type", "encryptConfig");
         DataSourceConnectionDto connectionDto = findOne(query, user);
         if (connectionDto == null) {
             return;
@@ -1938,6 +1938,18 @@ public void updateConnectionOptions(ObjectId id, ConnectionOptions options, User
         if (null != options.getTimeDifference()){
             update.set("timeDifference",options.getTimeDifference());
         }
+        if (null != options.getDatasourceInstanceInfo()){
+            if (StringUtils.isNotBlank(options.getDatasourceInstanceInfo().get("tag"))) {
+                update.set("datasourceInstanceTag",options.getDatasourceInstanceInfo().get("tag"));
+            }
+            if (StringUtils.isNotBlank(options.getDatasourceInstanceInfo().get("id"))) {
+                Map<String, Object> config = connectionDto.getConfig();
+                config.put("datasourceInstanceId", options.getDatasourceInstanceInfo().get("id"));
+                DataSourceEntity dataSourceEntity = convertToEntity(DataSourceEntity.class, connectionDto);
+                repository.encryptConfig(dataSourceEntity);
+                update.set("encryptConfig", dataSourceEntity.getEncryptConfig());
+            }
+        }
 
         updateByIdNotChangeLast(id, update, user);
 
diff --git a/manager/tm/src/main/java/com/tapdata/tm/init/InitLogMap.java b/manager/tm/src/main/java/com/tapdata/tm/init/InitLogMap.java
index eda5d56e29..26562aac27 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/init/InitLogMap.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/init/InitLogMap.java
@@ -1,6 +1,7 @@
 package com.tapdata.tm.init;
 
 
+import com.tapdata.tm.utils.OEMReplaceUtil;
 import com.tapdata.tm.utils.TMStartMsgUtil;
 import com.tapdata.tm.utils.TmStartMsg;
 import lombok.extern.slf4j.Slf4j;
@@ -28,14 +29,27 @@ public static synchronized void complete(Class<? extends ApplicationRunner> claz
         }
         TmStartMsg tmStartMsg = new TmStartMsg("ok","TM启动成功");
         TMStartMsgUtil.writeTMStartMsg(tmStartMsg);
-        log.info("\n" +
-                "  _______              _       _           _                    _             _                                _      _           _ \n" +
-                " |__   __|            | |     | |         | |                  | |           | |                              | |    | |         | |\n" +
-                "    | | __ _ _ __   __| | __ _| |_ __ _   | |_ _ __ ___     ___| |_ __ _ _ __| |_     ___ ___  _ __ ___  _ __ | | ___| |_ ___  __| |\n" +
-                "    | |/ _` | '_ \\ / _` |/ _` | __/ _` |  | __| '_ ` _ \\   / __| __/ _` | '__| __|   / __/ _ \\| '_ ` _ \\| '_ \\| |/ _ \\ __/ _ \\/ _` |\n" +
-                "    | | (_| | |_) | (_| | (_| | || (_| |  | |_| | | | | |  \\__ \\ || (_| | |  | |_   | (_| (_) | | | | | | |_) | |  __/ ||  __/ (_| |\n" +
-                "    |_|\\__,_| .__/ \\__,_|\\__,_|\\__\\__,_|   \\__|_| |_| |_|  |___/\\__\\__,_|_|   \\__|   \\___\\___/|_| |_| |_| .__/|_|\\___|\\__\\___|\\__,_|\n" +
-                "            | |                                                                                         | |                         \n" +
-                "            |_|                                                                                         |_|                         ");
+        String oemType = OEMReplaceUtil.oemType();
+        if (null == oemType) {
+            log.info("\n" +
+                    "  _______              _       _           _                    _             _                                _      _           _ \n" +
+                    " |__   __|            | |     | |         | |                  | |           | |                              | |    | |         | |\n" +
+                    "    | | __ _ _ __   __| | __ _| |_ __ _   | |_ _ __ ___     ___| |_ __ _ _ __| |_     ___ ___  _ __ ___  _ __ | | ___| |_ ___  __| |\n" +
+                    "    | |/ _` | '_ \\ / _` |/ _` | __/ _` |  | __| '_ ` _ \\   / __| __/ _` | '__| __|   / __/ _ \\| '_ ` _ \\| '_ \\| |/ _ \\ __/ _ \\/ _` |\n" +
+                    "    | | (_| | |_) | (_| | (_| | || (_| |  | |_| | | | | |  \\__ \\ || (_| | |  | |_   | (_| (_) | | | | | | |_) | |  __/ ||  __/ (_| |\n" +
+                    "    |_|\\__,_| .__/ \\__,_|\\__,_|\\__\\__,_|   \\__|_| |_| |_|  |___/\\__\\__,_|_|   \\__|   \\___\\___/|_| |_| |_| .__/|_|\\___|\\__\\___|\\__,_|\n" +
+                    "            | |                                                                                         | |                         \n" +
+                    "            |_|                                                                                         |_|                         ");
+        } else {
+            log.info("\n"+
+                    " _                    _             _                                _      _           _ \n" +
+                    "| |                  | |           | |                              | |    | |         | |\n" +
+                    "| |_ _ __ ___     ___| |_ __ _ _ __| |_     ___ ___  _ __ ___  _ __ | | ___| |_ ___  __| |\n" +
+                    "| __| '_ ` _ \\   / __| __/ _` | '__| __|   / __/ _ \\| '_ ` _ \\| '_ \\| |/ _ \\ __/ _ \\/ _` |\n" +
+                    "| |_| | | | | |  \\__ \\ || (_| | |  | |_   | (_| (_) | | | | | | |_) | |  __/ ||  __/ (_| |\n" +
+                    " \\__|_| |_| |_|  |___/\\__\\__,_|_|   \\__|   \\___\\___/|_| |_| |_| .__/|_|\\___|\\__\\___|\\__,_|\n" +
+                    "                                                              | |                         \n" +
+                    "                                                              |_|                         ");
+        }
     }
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/init/patches/daas/V3_13_1_TaskPipelineLimit_PipelineId_Index.java b/manager/tm/src/main/java/com/tapdata/tm/init/patches/daas/V3_13_1_TaskPipelineLimit_PipelineId_Index.java
new file mode 100644
index 0000000000..028652a5bf
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/init/patches/daas/V3_13_1_TaskPipelineLimit_PipelineId_Index.java
@@ -0,0 +1,32 @@
+package com.tapdata.tm.init.patches.daas;
+
+import com.tapdata.tm.init.PatchType;
+import com.tapdata.tm.init.PatchVersion;
+import com.tapdata.tm.init.patches.AbsPatch;
+import com.tapdata.tm.init.patches.PatchAnnotation;
+import com.tapdata.tm.utils.SpringContextHelper;
+import io.tapdata.utils.AppType;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.index.Index;
+
+@PatchAnnotation(appType = AppType.DAAS, version = "3.13-1")
+public class V3_13_1_TaskPipelineLimit_PipelineId_Index extends AbsPatch {
+    private static final Logger logger = LogManager.getLogger(V3_13_1_TaskPipelineLimit_PipelineId_Index.class);
+
+    public V3_13_1_TaskPipelineLimit_PipelineId_Index(PatchType type, PatchVersion version) {
+        super(type, version);
+    }
+
+    @Override
+    public void run() {
+        logger.info("Execute java patch: {}...", getClass().getName());
+        String collectionName = "TaskPipelineLimit";
+        MongoTemplate mongoTemplate = SpringContextHelper.getBean(MongoTemplate.class);
+
+        Index index = new Index().named("pipelineId_unique").on("pipelineId", Sort.Direction.DESC).unique().background();
+        mongoTemplate.indexOps(collectionName).ensureIndex(index);
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/lock/aop/LockAop.java b/manager/tm/src/main/java/com/tapdata/tm/lock/aop/LockAop.java
index f831200554..9ee895e09e 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/lock/aop/LockAop.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/lock/aop/LockAop.java
@@ -37,11 +37,12 @@ public class LockAop {
 	 * @throws Throwable
 	 */
 	@Around("(@annotation(com.tapdata.tm.lock.annotation.Lock)) && execution(* com.tapdata.tm.*.service..*.*(..))")
-	public Object lock(ProceedingJoinPoint pjp) throws Throwable {
+	public Object lock(ProceedingJoinPoint pjp) throws Throwable {/**/
 		MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
 		Lock annotation = methodSignature.getMethod().getAnnotation(Lock.class);
 		//锁的key 一般为taskId等
 		String value = annotation.value();
+		String valueType = annotation.valueType();
 		String type = annotation.type().name();
 		//锁设置的过期时间
 		int expireSeconds = annotation.expireSeconds();
@@ -51,27 +52,31 @@ public Object lock(ProceedingJoinPoint pjp) throws Throwable {
 		if (StringUtils.isBlank(value)) {
 			throw new BizException("SystemError");
 		}
-
-		List<String> values = Arrays.asList(value.split("\\."));
-		String param = values.get(0);
-		String [] parameters = methodSignature.getParameterNames();
-		int paramIndex = -1;
-		for (int i = 0; i < parameters.length; i++) {
-			if (param.equals(parameters[i])) {
-				paramIndex = i;
-				break;
+		Object obj;
+		if (Lock.CUSTOM.equals(valueType)) {
+			obj = value;
+		} else {
+			List<String> values = Arrays.asList(value.split("\\."));
+			String param = values.get(0);
+			String [] parameters = methodSignature.getParameterNames();
+			int paramIndex = -1;
+			for (int i = 0; i < parameters.length; i++) {
+				if (param.equals(parameters[i])) {
+					paramIndex = i;
+					break;
+				}
 			}
-		}
-		if (paramIndex == -1) {
-			throw new BizException("SystemError");
-		}
-		Object[] args = pjp.getArgs();
-		Object obj = args[paramIndex];
+			if (paramIndex == -1) {
+				throw new BizException("SystemError");
+			}
+			Object[] args = pjp.getArgs();
+			obj = args[paramIndex];
 
-		//如果是类似record.id的主机value值，需要一步一步的获取属性
-		for (int i = 1; i < values.size(); i++) {
-			Map<String, Object> objectMap = bean2Map(obj);
-			obj = objectMap.get(values.get(i));
+			//如果是类似record.id的主机value值，需要一步一步的获取属性
+			for (int i = 1; i < values.size(); i++) {
+				Map<String, Object> objectMap = bean2Map(obj);
+				obj = objectMap.get(values.get(i));
+			}
 		}
 
 		String key = obj instanceof String ? (String) obj : obj.toString();
diff --git a/manager/tm/src/main/java/com/tapdata/tm/metadatainstance/service/MetadataInstancesServiceImpl.java b/manager/tm/src/main/java/com/tapdata/tm/metadatainstance/service/MetadataInstancesServiceImpl.java
index e307262f3d..b82fdc4f49 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/metadatainstance/service/MetadataInstancesServiceImpl.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/metadatainstance/service/MetadataInstancesServiceImpl.java
@@ -2236,7 +2236,7 @@ public Map<String, TapType> dataType2TapType(DataType2TapTypeDto dto, UserDetail
     public boolean checkTableExist(String connectionId, String tableName, UserDetail user) {
         Criteria criteria = Criteria.where(ORIGINAL_NAME).is(tableName)
                 .and(IS_DELETED).ne(true)
-                .and("SOURCE_ID").is(connectionId)
+                .and(SOURCE_ID).is(connectionId)
                 .and(SOURCE_TYPE).is(SourceTypeEnum.SOURCE.name())
                 .and(TASK_ID).exists(false);
         Query query = new Query(criteria);
diff --git a/manager/tm/src/main/java/com/tapdata/tm/modules/controller/ModulesController.java b/manager/tm/src/main/java/com/tapdata/tm/modules/controller/ModulesController.java
index 43b692984a..3f12ef7498 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/modules/controller/ModulesController.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/modules/controller/ModulesController.java
@@ -236,6 +236,7 @@ public ResponseMessage getApiDocument(@PathVariable("id") String id){
 
 
   @GetMapping("preview")
+  @Deprecated
   public ResponseMessage preview(){
     return success(modulesService.preview(getLoginUser()));
   }
@@ -258,7 +259,7 @@ public ResponseMessage apiList(@RequestParam(value = "filter", required = false)
 
   @PostMapping("apiDetail")
   public ResponseMessage apiDetail(@RequestBody ApiDetailParam apiDetailParam){
-    return success(modulesService.apiDetail(apiDetailParam,getLoginUser()));
+    return success(modulesService.apiDetail(apiDetailParam));
   }
 
 	@Operation(summary = "api导出")
diff --git a/manager/tm/src/main/java/com/tapdata/tm/modules/service/ModulesService.java b/manager/tm/src/main/java/com/tapdata/tm/modules/service/ModulesService.java
index ac2127ec8b..27d1c587c3 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/modules/service/ModulesService.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/modules/service/ModulesService.java
@@ -11,10 +11,19 @@
 import com.deepoove.poi.plugin.toc.TOCRenderPolicy;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.mongodb.ConnectionString;
-import com.mongodb.client.result.UpdateResult;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoCursor;
 import com.tapdata.manager.common.utils.StringUtils;
+import com.tapdata.tm.apiCalls.dto.ApiCallDto;
 import com.tapdata.tm.apiCalls.entity.ApiCallEntity;
 import com.tapdata.tm.apiCalls.service.ApiCallService;
+import com.tapdata.tm.apicallminutestats.dto.ApiCallMinuteStatsDto;
+import com.tapdata.tm.apicallminutestats.service.ApiCallMinuteStatsService;
+import com.tapdata.tm.apicallstats.dto.ApiCallStatsDto;
+import com.tapdata.tm.apicallstats.entity.ApiCallStatsEntity;
+import com.tapdata.tm.apicallstats.service.ApiCallStatsService;
+import com.tapdata.tm.application.dto.ApplicationDto;
+import com.tapdata.tm.application.service.ApplicationService;
 import com.tapdata.tm.base.dto.Filter;
 import com.tapdata.tm.base.dto.Page;
 import com.tapdata.tm.base.dto.TmPageable;
@@ -37,17 +46,15 @@
 import com.tapdata.tm.modules.entity.ModulesEntity;
 import com.tapdata.tm.modules.entity.Path;
 import com.tapdata.tm.modules.param.ApiDetailParam;
-import com.tapdata.tm.modules.param.AttrsParam;
 import com.tapdata.tm.modules.repository.ModulesRepository;
 import com.tapdata.tm.modules.vo.*;
 import com.tapdata.tm.task.bean.TaskUpAndLoadDto;
-import com.tapdata.tm.utils.AES256Util;
-import com.tapdata.tm.utils.FunctionUtils;
-import com.tapdata.tm.utils.GZIPUtil;
-import com.tapdata.tm.utils.MongoUtils;
+import com.tapdata.tm.utils.*;
 import lombok.NonNull;
+import lombok.Setter;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
+import org.bson.Document;
 import org.bson.types.ObjectId;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Sort;
@@ -55,6 +62,7 @@
 import org.springframework.data.mongodb.core.query.Criteria;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.Update;
+import org.springframework.security.core.GrantedAuthority;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
@@ -64,10 +72,15 @@
 import java.math.RoundingMode;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
+import java.time.*;
+import java.time.temporal.ChronoUnit;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
 import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static com.tapdata.tm.utils.DocumentUtils.getLong;
 
 /**
  * @Author:
@@ -76,101 +89,96 @@
  */
 @Service
 @Slf4j
+@Setter(onMethod_ = {@Autowired})
 public class ModulesService extends BaseService<ModulesDto, ModulesEntity, ObjectId, ModulesRepository> {
+	private ApplicationService applicationService;
+	private DataSourceService dataSourceService;
+	private MetadataInstancesService metadataInstancesService;
+	private FileService fileService;
+	private ApiCallService apiCallService;
+	private DataSourceDefinitionService dataSourceDefinitionService;
+	private ApiCallStatsService apiCallStatsService;
+	private ApiCallMinuteStatsService apiCallMinuteStatsService;
+
+	public ModulesService(@NonNull ModulesRepository repository) {
+		super(repository, ModulesDto.class, ModulesEntity.class);
+	}
 
-    @Autowired
-    DataSourceService dataSourceService;
-
-		@Autowired
-		private MetadataInstancesService metadataInstancesService;
-
-		@Autowired
-		private FileService fileService;
-
-    @Autowired
-    ApiCallService apiCallService;
-
-    @Autowired
-    private DataSourceDefinitionService dataSourceDefinitionService;
-
-    private final Long oneMinuteMillSeconds = 1000L * 60;
-
-    public ModulesService(@NonNull ModulesRepository repository) {
-        super(repository, ModulesDto.class, ModulesEntity.class);
-    }
-    @Override
-    protected void beforeSave(ModulesDto modules, UserDetail user) {
-        if(CollectionUtils.isNotEmpty(modules.getPaths())){
-            modules.getPaths().forEach(path -> {
-                if(CollectionUtils.isNotEmpty(path.getFields())){
-                    path.setFields(path.getFields().stream().filter(Objects::nonNull).collect(Collectors.toList()));
-                }
-            });
-        }
-    }
-
-
-    public ModulesDetailVo findById(String id) {
-        ModulesDto modulesDto = findById(MongoUtils.toObjectId(id));
-        ModulesDetailVo modulesDetailVo = BeanUtil.copyProperties(modulesDto, ModulesDetailVo.class);
-
-        String connectionId = modulesDto.getConnection().toString();
-        DataSourceConnectionDto dataSourceConnectionDto = dataSourceService.findById(MongoUtils.toObjectId(connectionId));
-        if (null != dataSourceConnectionDto) {
-            dataSourceConnectionDto.setDatabase_password(null);
-            dataSourceConnectionDto.setPlain_password(null);
-            modulesDetailVo.setSource(dataSourceConnectionDto);
-        }
-        modulesDetailVo.setConnection(connectionId);
-        return modulesDetailVo;
-    }
-
-    public Page findModules(Filter filter, UserDetail userDetail) {
-        //不需要设定fields
-        filter.setFields(null);
-        Map where = filter.getWhere();
-        Map notDeleteMap = new HashMap();
-        notDeleteMap.put("$ne", true);
-        where.put("is_deleted", notDeleteMap);
-
-
-        String status = (String) where.getOrDefault("status", "");
-        if ("all".equals(status)) {
-            where.remove("status");
-        }
-
-        Page page = find(filter, userDetail);
-
-        String createUser = "";
-        List<ModulesListVo> modulesListVoList = com.tapdata.tm.utils.BeanUtil.deepCloneList(page.getItems(), ModulesListVo.class);
-        if (CollectionUtils.isNotEmpty(modulesListVoList)) {
-            for (ModulesListVo modulesListVo : modulesListVoList) {
-                String connectionId = modulesListVo.getConnection();
-                if (null != connectionId) {
-                    DataSourceConnectionDto dataSourceConnectionDto = dataSourceService.findById(MongoUtils.toObjectId(connectionId));
-                    if (null != dataSourceConnectionDto) {
-                        Source source = new Source(dataSourceConnectionDto.getDatabase_type(), dataSourceConnectionDto.getId().toString(), dataSourceConnectionDto.getName(), dataSourceConnectionDto.getStatus());
-                        modulesListVo.setSource(source);
-                    }
-                }
-                createUser = modulesListVo.getCreateUser() == null ? userDetail.getEmail() : modulesListVo.getCreateUser();
-                modulesListVo.setUser(createUser);
-            }
-        }
-        page.setItems(modulesListVoList);
-        return page;
-    }
+	private final Long oneMinuteMillSeconds = 1000L * 60;
 
+	@Override
+	protected void beforeSave(ModulesDto modules, UserDetail user) {
+		if (CollectionUtils.isNotEmpty(modules.getPaths())) {
+			modules.getPaths().forEach(path -> {
+				if (CollectionUtils.isNotEmpty(path.getFields())) {
+					path.setFields(path.getFields().stream().filter(Objects::nonNull).collect(Collectors.toList()));
+				}
+			});
+		}
+	}
 
-    /**
-     * base+version 不能重复
-     *
-     * @param modulesDto
-     * @param userDetail
-     * @return
-     */
-    @Override
-    public ModulesDto save(ModulesDto modulesDto, UserDetail userDetail) {
+
+	public ModulesDetailVo findById(String id) {
+		ModulesDto modulesDto = findById(MongoUtils.toObjectId(id));
+		ModulesDetailVo modulesDetailVo = BeanUtil.copyProperties(modulesDto, ModulesDetailVo.class);
+
+		String connectionId = modulesDto.getConnection().toString();
+		DataSourceConnectionDto dataSourceConnectionDto = dataSourceService.findById(MongoUtils.toObjectId(connectionId));
+		if (null != dataSourceConnectionDto) {
+			dataSourceConnectionDto.setDatabase_password(null);
+			dataSourceConnectionDto.setPlain_password(null);
+			modulesDetailVo.setSource(dataSourceConnectionDto);
+		}
+		modulesDetailVo.setConnection(connectionId);
+		return modulesDetailVo;
+	}
+
+	public Page findModules(Filter filter, UserDetail userDetail) {
+		//不需要设定fields
+		filter.setFields(null);
+		Map where = filter.getWhere();
+		Map notDeleteMap = new HashMap();
+		notDeleteMap.put("$ne", true);
+		where.put("is_deleted", notDeleteMap);
+
+
+		String status = (String) where.getOrDefault("status", "");
+		if ("all".equals(status)) {
+			where.remove("status");
+		}
+
+		Page page = find(filter, userDetail);
+
+		String createUser = "";
+		List<ModulesListVo> modulesListVoList = com.tapdata.tm.utils.BeanUtil.deepCloneList(page.getItems(), ModulesListVo.class);
+		if (CollectionUtils.isNotEmpty(modulesListVoList)) {
+			for (ModulesListVo modulesListVo : modulesListVoList) {
+				String connectionId = modulesListVo.getConnection();
+				if (null != connectionId) {
+					DataSourceConnectionDto dataSourceConnectionDto = dataSourceService.findById(MongoUtils.toObjectId(connectionId));
+					if (null != dataSourceConnectionDto) {
+						Source source = new Source(dataSourceConnectionDto.getDatabase_type(), dataSourceConnectionDto.getId().toString(), dataSourceConnectionDto.getName(), dataSourceConnectionDto.getStatus());
+						modulesListVo.setSource(source);
+					}
+				}
+				createUser = modulesListVo.getCreateUser() == null ? userDetail.getEmail() : modulesListVo.getCreateUser();
+				modulesListVo.setUser(createUser);
+			}
+		}
+		page.setItems(modulesListVoList);
+		return page;
+	}
+
+
+	/**
+	 * base+version 不能重复
+	 *
+	 * @param modulesDto
+	 * @param userDetail
+	 * @return
+	 */
+	@Override
+	public ModulesDto save(ModulesDto modulesDto, UserDetail userDetail) {
 //        if (findByName(modulesDto.getName()).size() > 0) {
 //            throw new BizException("Modules.Name.Existed");
 //        }
@@ -180,693 +188,656 @@ public ModulesDto save(ModulesDto modulesDto, UserDetail userDetail) {
 //        if (null == modulesDto.getDataSource()) {
 //            throw new BizException("Modules.Connection.Null");
 //        }
-        if (findByName(modulesDto.getName()).size() > 1)
-            throw new BizException("Modules.Name.Existed");
-        modulesDto.setConnection(MongoUtils.toObjectId(modulesDto.getDataSource()));
-        modulesDto.setLastUpdAt(new Date());
-        modulesDto.setCreateAt(new Date());
-        //user表  admin@admin.com  的username 没有这个字段?
-        modulesDto.setCreateUser(userDetail.getUsername());
-        modulesDto.setLastUpdBy(userDetail.getUsername());
-        if (StringUtils.isBlank(modulesDto.getStatus())) {
-            modulesDto.setStatus(ModuleStatusEnum.GENERATING.getValue());
-        }
-        return super.save(modulesDto, userDetail);
-
-    }
-
-
-    public ModulesDto updateModuleById(ModulesDto modulesDto, UserDetail userDetail) {
-        Where where = new Where();
-        where.put("id", modulesDto.getId().toString());
-
-        Query query = new Query();
-        ObjectId id = modulesDto.getId();
-        if (id != null) {
-            Criteria criteria = Criteria.where("_id").is(id);
-            query.addCriteria(criteria);
-        }
-        //没有生成的接口 不能发布
-        ModulesDto dto = findOne(query, userDetail);
-        if (dto == null)
-            throw new BizException("current module not exist");
-        if (ModuleStatusEnum.ACTIVE.getValue().equals(modulesDto.getStatus()) && ModuleStatusEnum.GENERATING.getValue().equals(dto.getStatus()))
-            throw new BizException("generating status can't release");
-        //点击生成按钮 才校验(撤销发布等不校验)
-        if (ModuleStatusEnum.PENDING.getValue().equals(modulesDto.getStatus()) && !ModuleStatusEnum.ACTIVE.getValue().equals(dto.getStatus())) {
-            if (findByName(modulesDto.getName()).size() > 1)
-                throw new BizException("Modules.Name.Existed");
-            if (isBasePathAndVersionRepeat(modulesDto.getBasePath(), modulesDto.getApiVersion()).size() > 1)
-                throw new BizException("Modules.BasePathAndVersion.Existed");
-            checkoutInputParamIsValid(modulesDto);
-        }
-        return super.upsertByWhere(where, modulesDto, userDetail);
-    }
-
-    public List<ModulesDto> batchUpdateModuleByList(List<ModulesDto> modulesDtos, UserDetail userDetail) {
-        List<ModulesDto> modulesDtoList = new ArrayList<>();
-        modulesDtos.forEach((modulesDto -> modulesDtoList.add(updateModuleById(modulesDto, userDetail))));
-        return modulesDtoList;
-    }
+		if (findByName(modulesDto.getName()).size() > 1)
+			throw new BizException("Modules.Name.Existed");
+		modulesDto.setConnection(MongoUtils.toObjectId(modulesDto.getDataSource()));
+		modulesDto.setLastUpdAt(new Date());
+		modulesDto.setCreateAt(new Date());
+		//user表  admin@admin.com  的username 没有这个字段?
+		modulesDto.setCreateUser(userDetail.getUsername());
+		modulesDto.setLastUpdBy(userDetail.getUsername());
+		if (StringUtils.isBlank(modulesDto.getStatus())) {
+			modulesDto.setStatus(ModuleStatusEnum.GENERATING.getValue());
+		}
+		return super.save(modulesDto, userDetail);
 
+	}
 
-    /**
-     * 复制api
-     *
-     * @param id
-     * @param userDetail
-     * @return
-     */
-    public ModulesDto copy(String id, UserDetail userDetail) {
-        ModulesDto existedModulesDto = findById(MongoUtils.toObjectId(id));
-        if (null == existedModulesDto) {
-            throw new BizException("Modules.Not.Existed");
-        }
-        String copyName = existedModulesDto.getName() + "_Copy";
-        String copyBasePath = existedModulesDto.getBasePath() + "_Copy";
-        existedModulesDto.setId(null);
-        existedModulesDto.setBasePath(copyBasePath);
-
-        existedModulesDto.setName(copyName);
-        existedModulesDto.setStatus(ModuleStatusEnum.PENDING.getValue());
-        save(existedModulesDto, userDetail);
-        return existedModulesDto;
-    }
-
-
-    public Map getSchema(String id, UserDetail userDetail) {
-        ModulesDto modulesDto = findById(MongoUtils.toObjectId(id), userDetail);
-        if (null == modulesDto) {
-            throw new BizException("Modules.Not.Existed");
-        }
-        Map retMap = new HashMap();
-        retMap.put("fields", modulesDto.getFields());
-        return retMap;
-    }
-
-
-    public List<ModulesDto> findByName(String name) {
-        Query query = Query.query(Criteria.where("name").is(name).and("is_deleted").ne(true));
-        List<ModulesDto> modulesDtoList = findAll(query);
-        return modulesDtoList;
-    }
 
-    /**
-     * * upsert  0跳过已有数据  1 覆盖已有数据
-     *
-     * @param json
-     * @param upsert
-     * @param userDetail
-     * @param listTag
-     */
-    public void importData(String json, String upsert, List<Tag> listTag, UserDetail userDetail) {
-        Map map = JsonUtil.parseJson(json, Map.class);
-        List<Map<String, Object>> data = (List) map.get("data");
-
-        if (CollectionUtils.isNotEmpty(data)) {
-            for (Map<String, Object> singleDataMap : data) {
-                Query query = new Query();
-                String id = singleDataMap.get("id").toString();
-                singleDataMap.remove("id");
-                if (StringUtils.isNotBlank(id)) {
-                    ModulesDto existedDto = findById(MongoUtils.toObjectId(id));
-                    Map existedPoperty = BeanUtil.beanToMap(existedDto);
-                    if ("0".equals(upsert)) {
+	public ModulesDto updateModuleById(ModulesDto modulesDto, UserDetail userDetail) {
+		Where where = new Where();
+		where.put("id", modulesDto.getId().toString());
+
+		Query query = new Query();
+		ObjectId id = modulesDto.getId();
+		if (id != null) {
+			Criteria criteria = Criteria.where("_id").is(id);
+			query.addCriteria(criteria);
+		}
+		//没有生成的接口 不能发布
+		ModulesDto dto = findOne(query, userDetail);
+		if (dto == null)
+			throw new BizException("current module not exist");
+		if (ModuleStatusEnum.ACTIVE.getValue().equals(modulesDto.getStatus()) && ModuleStatusEnum.GENERATING.getValue().equals(dto.getStatus()))
+			throw new BizException("generating status can't release");
+		//点击生成按钮 才校验(撤销发布等不校验)
+		if (ModuleStatusEnum.PENDING.getValue().equals(modulesDto.getStatus()) && !ModuleStatusEnum.ACTIVE.getValue().equals(dto.getStatus())) {
+			if (findByName(modulesDto.getName()).size() > 1)
+				throw new BizException("Modules.Name.Existed");
+			if (isBasePathAndVersionRepeat(modulesDto.getBasePath(), modulesDto.getApiVersion()).size() > 1)
+				throw new BizException("Modules.BasePathAndVersion.Existed");
+			checkoutInputParamIsValid(modulesDto);
+		}
+		return super.upsertByWhere(where, modulesDto, userDetail);
+	}
+
+	public List<ModulesDto> batchUpdateModuleByList(List<ModulesDto> modulesDtos, UserDetail userDetail) {
+		List<ModulesDto> modulesDtoList = new ArrayList<>();
+		modulesDtos.forEach((modulesDto -> modulesDtoList.add(updateModuleById(modulesDto, userDetail))));
+		return modulesDtoList;
+	}
+
+
+	/**
+	 * 复制api
+	 *
+	 * @param id
+	 * @param userDetail
+	 * @return
+	 */
+	public ModulesDto copy(String id, UserDetail userDetail) {
+		ModulesDto existedModulesDto = findById(MongoUtils.toObjectId(id));
+		if (null == existedModulesDto) {
+			throw new BizException("Modules.Not.Existed");
+		}
+		String copyName = existedModulesDto.getName() + "_Copy";
+		String copyBasePath = existedModulesDto.getBasePath() + "_Copy";
+		existedModulesDto.setId(null);
+		existedModulesDto.setBasePath(copyBasePath);
+
+		existedModulesDto.setName(copyName);
+		existedModulesDto.setStatus(ModuleStatusEnum.PENDING.getValue());
+		save(existedModulesDto, userDetail);
+		return existedModulesDto;
+	}
+
+
+	public Map getSchema(String id, UserDetail userDetail) {
+		ModulesDto modulesDto = findById(MongoUtils.toObjectId(id), userDetail);
+		if (null == modulesDto) {
+			throw new BizException("Modules.Not.Existed");
+		}
+		Map retMap = new HashMap();
+		retMap.put("fields", modulesDto.getFields());
+		return retMap;
+	}
+
+
+	public List<ModulesDto> findByName(String name) {
+		Query query = Query.query(Criteria.where("name").is(name).and("is_deleted").ne(true));
+		List<ModulesDto> modulesDtoList = findAll(query);
+		return modulesDtoList;
+	}
+
+	/**
+	 * * upsert  0跳过已有数据  1 覆盖已有数据
+	 *
+	 * @param json
+	 * @param upsert
+	 * @param userDetail
+	 * @param listTag
+	 */
+	public void importData(String json, String upsert, List<Tag> listTag, UserDetail userDetail) {
+		Map map = JsonUtil.parseJson(json, Map.class);
+		List<Map<String, Object>> data = (List) map.get("data");
+
+		if (CollectionUtils.isNotEmpty(data)) {
+			for (Map<String, Object> singleDataMap : data) {
+				Query query = new Query();
+				String id = singleDataMap.get("id").toString();
+				singleDataMap.remove("id");
+				if (StringUtils.isNotBlank(id)) {
+					ModulesDto existedDto = findById(MongoUtils.toObjectId(id));
+					Map existedPoperty = BeanUtil.beanToMap(existedDto);
+					if ("0".equals(upsert)) {
 //                        跳过已有数据,
-                        singleDataMap.forEach((key, value) ->
-                        {
-                            if (existedPoperty.containsKey(key)) {
-                                singleDataMap.put(key, existedPoperty.get(key));
-                            }
-                        });
-
-                    }
-                    query.addCriteria(Criteria.where("id").is(id));
-                }
-                ModulesDto newDto = new ModulesDto();
-                for (String key : singleDataMap.keySet()) {
-                    if ("connection".equals(key)) {
-                        BeanUtil.setFieldValue(newDto, key, MongoUtils.toObjectId((String) singleDataMap.get(key)));
-                    } else if ("user_id".equals(key)) {
-                        BeanUtil.setFieldValue(newDto, key, MongoUtils.toObjectId((String) singleDataMap.get(key)));
-                    } else {
-                        BeanUtil.setFieldValue(newDto, key, singleDataMap.get(key));
-                    }
-
-                }
-
-                if (CollectionUtils.isNotEmpty(listTag)) {
-                    newDto.setListtags(listTag);
-                }
-                newDto.setIsDeleted(false);
-                super.upsert(query, newDto, userDetail);
-            }
-        }
-    }
+						singleDataMap.forEach((key, value) ->
+						{
+							if (existedPoperty.containsKey(key)) {
+								singleDataMap.put(key, existedPoperty.get(key));
+							}
+						});
 
+					}
+					query.addCriteria(Criteria.where("id").is(id));
+				}
+				ModulesDto newDto = new ModulesDto();
+				for (String key : singleDataMap.keySet()) {
+					if ("connection".equals(key)) {
+						BeanUtil.setFieldValue(newDto, key, MongoUtils.toObjectId((String) singleDataMap.get(key)));
+					} else if ("user_id".equals(key)) {
+						BeanUtil.setFieldValue(newDto, key, MongoUtils.toObjectId((String) singleDataMap.get(key)));
+					} else {
+						BeanUtil.setFieldValue(newDto, key, singleDataMap.get(key));
+					}
 
-    /**
-     * 查找已经发布的api
-     *
-     * @param userDetail
-     * @return
-     */
-    public ApiDefinitionVo apiDefinition(UserDetail userDetail) {
-        List<ConnectionVo> connectionVos = new ArrayList<>();
-        ApiDefinitionVo apiDefinitionVo = new ApiDefinitionVo();
-        //查找已发布的api
-        List<ModulesDto> apis = findAllActiveApi(ModuleStatusEnum.ACTIVE);
-        if (CollectionUtils.isNotEmpty(apis)) {
-            List<ObjectId> connections = apis.stream().map(ModulesDto::getConnection).collect(Collectors.toList());
-            Query query = Query.query(Criteria.where("id").in(connections));
-            List<DataSourceConnectionDto> dataSourceConnectionDtoList = dataSourceService.findAll(query);
-
-            //设置上pdk的APIserverkey属性
-            for (DataSourceConnectionDto dataSourceConnectionDto : dataSourceConnectionDtoList) {
-
-                Map<String, Object> config = dataSourceConnectionDto.getConfig();
-                if (dataSourceConnectionDto.getDatabase_type().toLowerCase(Locale.ROOT).contains("mongo")) {
-                    if (config.get("uri") == null) {
-                        StringBuilder sb = new StringBuilder("mongodb://");
-                        if (config.get("user") != null) {
-                            String user = (String) config.get("user");
-                            String password = (String) config.get("password");
-                            try {
-                                user = URLEncoder.encode(user, "UTF-8");
-                                password = URLEncoder.encode(password, "UTF-8");
-                            } catch (Exception e) {
-                                throw new BizException("SystemError");
-                            }
-                            sb.append(user).append(":")
-                                    .append(password).append("@");
-                        }
-                        sb.append(config.get("host")).append("/").append(config.get("database"));
-                        if (config.get("additionalString") != null) {
-                            sb.append("?").append(config.get("additionalString"));
-                        }
-
-                        config.put("uri", sb.toString());
-                    } else {
-                        String uri = (String) config.get("uri");
-                        ConnectionString connectionString = new ConnectionString(uri);
-                        String username = connectionString.getUsername();
-                        char[] passwordChar = connectionString.getPassword();
-                        String password = null;
-                        if (passwordChar != null) {
-                            password = new String(passwordChar);
-                        }
-
-                        try {
-                            if (StringUtils.isNotBlank(username)) {
-                                String newUsername = URLEncoder.encode(username, "UTF-8");
-                                uri = uri.replace(username, newUsername);
-                            }
-
-                            if (StringUtils.isNotBlank(password)) {
-                                String newPassword = URLEncoder.encode(password, "UTF-8");
-                                uri = uri.replace(password, newPassword);
-                            }
-                        } catch (Exception e) {
-                            throw new BizException("SystemError");
-                        }
-                        config.put("uri", uri);
-
-                    }
-                }
-                DataSourceDefinitionDto definitionDto = dataSourceDefinitionService.getByDataSourceType(dataSourceConnectionDto.getDatabase_type(), userDetail);
-                Map<String, Object> properties = definitionDto.getProperties();
-                LinkedHashMap connection = (LinkedHashMap) properties.get("connection");
-                analyzeApiServerKey(dataSourceConnectionDto, connection, null);
-
-                ConnectionVo connectionVo = cn.hutool.core.bean.BeanUtil.copyProperties(dataSourceConnectionDto, ConnectionVo.class);
-                if (null != connectionVo) {
-                    String plainPassword = AES256Util.Aes256Decode(connectionVo.getDatabase_password());
-                    connectionVo.setDatabase_password(plainPassword);
-
-                    switch (dataSourceConnectionDto.getDatabase_type().toLowerCase(Locale.ROOT)) {
-                        case "oracle":
-                            if ("SID".equals(config.get("thinType"))) {
-                                Optional.ofNullable(config.get("sid")).map(o -> {
-                                    connectionVo.setDatabase_name(o.toString());
-                                    return o;
-                                });
-                            }
-                            break;
-                        default:
-                            break;
-                    }
-                }
-                connectionVos.add(connectionVo);
-            }
-            apiDefinitionVo.setConnections(connectionVos);
-            apiDefinitionVo.setApis(apis);
-        }
-        return apiDefinitionVo;
-    }
-
-    private void analyzeApiServerKey(DataSourceConnectionDto dataSourceConnectionDto, LinkedHashMap connection, String parent) {
-        LinkedHashMap properties1 = (LinkedHashMap) connection.get("properties");
-        properties1.forEach((k, v) -> {
-            LinkedHashMap v1 = (LinkedHashMap) v;
-            String key = StringUtils.isBlank(parent) ? (String) k : parent + "." + k;
-            if ("object".equals(((LinkedHashMap<?, ?>) v).get("type"))) {
-                analyzeApiServerKey(dataSourceConnectionDto, v1, key);
-            }
-            String apiServerKey = (String) v1.get("apiServerKey");
-            if (StringUtils.isNotBlank(apiServerKey)) {
-                setApiServerKey(dataSourceConnectionDto, key, apiServerKey);
-            }
-
-        });
-    }
-
-    private void setApiServerKey(DataSourceConnectionDto dataSourceConnectionDto, String k, String apiServerKey) {
-        Map<String, Object> config = dataSourceConnectionDto.getConfig();
-        Object value = getValue(k, config);
-        Class<? extends DataSourceConnectionDto> aClass = dataSourceConnectionDto.getClass();
-        for (java.lang.reflect.Field declaredField : aClass.getDeclaredFields()) {
-            declaredField.setAccessible(true);
-            if (declaredField.getName().equals(apiServerKey)) {
-                try {
-                    if (value == null) {
-                        continue;
-                    }
-                    if (declaredField.getType().equals(value.getClass())) {
-                        declaredField.set(dataSourceConnectionDto, value);
-                    } else {
-                        if (declaredField.getType().getName().contains("Integer") || value.getClass().getName().equals("String")) {
-                            declaredField.set(dataSourceConnectionDto, Integer.parseInt((String) value));
-                        } else if (declaredField.getType().getName().contains("String")) {
-                            declaredField.set(dataSourceConnectionDto, String.valueOf(value));
-                        }
-                    }
-                } catch (IllegalAccessException e) {
-                    throw new BizException("System error");
-                }
-            }
-        }
-    }
-
-    private Object getValue(String k, Map<String, Object> config) {
-        if (k.contains(".")) {
-            int i = k.indexOf(".");
-            String key1 = k.substring(0, i);
-            Map config1 = (Map) config.get(key1);
-            String key2 = k.substring(i + 1);
-            return getValue(key2, config1);
-
-        } else {
-            return config.get(k);
-        }
-    }
-
-    public List<ModulesDto> getByCollectionName(String connection_id, String collectionName, UserDetail user) {
-        if (connection_id != null) {
-            Criteria criteria1 = Criteria.where("datasource").is(connection_id).and("tablename")
-                    .is(collectionName).and("status").is("active");
-            List<ModulesDto> allDto = findAllDto(Query.query(criteria1), user);
-            return allDto;
-        }
-        Criteria criteria = Criteria.where("tablename").is(collectionName).and("status").is("active");
-        return findAllDto(Query.query(criteria), user);
-    }
-
-    //todo  公共库
+				}
+
+				if (CollectionUtils.isNotEmpty(listTag)) {
+					newDto.setListtags(listTag);
+				}
+				newDto.setIsDeleted(false);
+				super.upsert(query, newDto, userDetail);
+			}
+		}
+	}
+
+
+	/**
+	 * 查找已经发布的api
+	 *
+	 * @param userDetail
+	 * @return
+	 */
+	public ApiDefinitionVo apiDefinition(UserDetail userDetail) {
+		List<ConnectionVo> connectionVos = new ArrayList<>();
+		ApiDefinitionVo apiDefinitionVo = new ApiDefinitionVo();
+		//查找已发布的api
+		List<ModulesDto> apis = findAllActiveApi(ModuleStatusEnum.ACTIVE);
+		if (CollectionUtils.isNotEmpty(apis)) {
+			List<ObjectId> connections = apis.stream().map(ModulesDto::getConnection).collect(Collectors.toList());
+			Query query = Query.query(Criteria.where("id").in(connections));
+			List<DataSourceConnectionDto> dataSourceConnectionDtoList = dataSourceService.findAll(query);
+
+			//设置上pdk的APIserverkey属性
+			for (DataSourceConnectionDto dataSourceConnectionDto : dataSourceConnectionDtoList) {
+
+				Map<String, Object> config = dataSourceConnectionDto.getConfig();
+				if (dataSourceConnectionDto.getDatabase_type().toLowerCase(Locale.ROOT).contains("mongo")) {
+					if (config.get("uri") == null) {
+						StringBuilder sb = new StringBuilder("mongodb://");
+						if (config.get("user") != null) {
+							String user = (String) config.get("user");
+							String password = (String) config.get("password");
+							try {
+								user = URLEncoder.encode(user, "UTF-8");
+								password = URLEncoder.encode(password, "UTF-8");
+							} catch (Exception e) {
+								throw new BizException("SystemError");
+							}
+							sb.append(user).append(":")
+									.append(password).append("@");
+						}
+						sb.append(config.get("host")).append("/").append(config.get("database"));
+						if (config.get("additionalString") != null) {
+							sb.append("?").append(config.get("additionalString"));
+						}
+
+						config.put("uri", sb.toString());
+					} else {
+						String uri = (String) config.get("uri");
+						ConnectionString connectionString = new ConnectionString(uri);
+						String username = connectionString.getUsername();
+						char[] passwordChar = connectionString.getPassword();
+						String password = null;
+						if (passwordChar != null) {
+							password = new String(passwordChar);
+						}
+
+						try {
+							if (StringUtils.isNotBlank(username)) {
+								String newUsername = URLEncoder.encode(username, "UTF-8");
+								uri = uri.replace(username, newUsername);
+							}
+
+							if (StringUtils.isNotBlank(password)) {
+								String newPassword = URLEncoder.encode(password, "UTF-8");
+								uri = uri.replace(password, newPassword);
+							}
+						} catch (Exception e) {
+							throw new BizException("SystemError");
+						}
+						config.put("uri", uri);
+
+					}
+				}
+				DataSourceDefinitionDto definitionDto = dataSourceDefinitionService.getByDataSourceType(dataSourceConnectionDto.getDatabase_type(), userDetail);
+				Map<String, Object> properties = definitionDto.getProperties();
+				LinkedHashMap connection = (LinkedHashMap) properties.get("connection");
+				analyzeApiServerKey(dataSourceConnectionDto, connection, null);
+
+				ConnectionVo connectionVo = cn.hutool.core.bean.BeanUtil.copyProperties(dataSourceConnectionDto, ConnectionVo.class);
+				if (null != connectionVo) {
+					String plainPassword = AES256Util.Aes256Decode(connectionVo.getDatabase_password());
+					connectionVo.setDatabase_password(plainPassword);
+
+					switch (dataSourceConnectionDto.getDatabase_type().toLowerCase(Locale.ROOT)) {
+						case "oracle":
+							if ("SID".equals(config.get("thinType"))) {
+								Optional.ofNullable(config.get("sid")).map(o -> {
+									connectionVo.setDatabase_name(o.toString());
+									return o;
+								});
+							}
+							break;
+						default:
+							break;
+					}
+				}
+				connectionVos.add(connectionVo);
+			}
+			apiDefinitionVo.setConnections(connectionVos);
+			apiDefinitionVo.setApis(apis);
+		}
+		return apiDefinitionVo;
+	}
+
+	private void analyzeApiServerKey(DataSourceConnectionDto dataSourceConnectionDto, LinkedHashMap connection, String parent) {
+		LinkedHashMap properties1 = (LinkedHashMap) connection.get("properties");
+		properties1.forEach((k, v) -> {
+			LinkedHashMap v1 = (LinkedHashMap) v;
+			String key = StringUtils.isBlank(parent) ? (String) k : parent + "." + k;
+			if ("object".equals(((LinkedHashMap<?, ?>) v).get("type"))) {
+				analyzeApiServerKey(dataSourceConnectionDto, v1, key);
+			}
+			String apiServerKey = (String) v1.get("apiServerKey");
+			if (StringUtils.isNotBlank(apiServerKey)) {
+				setApiServerKey(dataSourceConnectionDto, key, apiServerKey);
+			}
+
+		});
+	}
+
+	private void setApiServerKey(DataSourceConnectionDto dataSourceConnectionDto, String k, String apiServerKey) {
+		Map<String, Object> config = dataSourceConnectionDto.getConfig();
+		Object value = getValue(k, config);
+		Class<? extends DataSourceConnectionDto> aClass = dataSourceConnectionDto.getClass();
+		for (java.lang.reflect.Field declaredField : aClass.getDeclaredFields()) {
+			declaredField.setAccessible(true);
+			if (declaredField.getName().equals(apiServerKey)) {
+				try {
+					if (value == null) {
+						continue;
+					}
+					if (declaredField.getType().equals(value.getClass())) {
+						declaredField.set(dataSourceConnectionDto, value);
+					} else {
+						if (declaredField.getType().getName().contains("Integer") || value.getClass().getName().equals("String")) {
+							declaredField.set(dataSourceConnectionDto, Integer.parseInt((String) value));
+						} else if (declaredField.getType().getName().contains("String")) {
+							declaredField.set(dataSourceConnectionDto, String.valueOf(value));
+						}
+					}
+				} catch (IllegalAccessException e) {
+					throw new BizException("System error");
+				}
+			}
+		}
+	}
+
+	private Object getValue(String k, Map<String, Object> config) {
+		if (k.contains(".")) {
+			int i = k.indexOf(".");
+			String key1 = k.substring(0, i);
+			Map config1 = (Map) config.get(key1);
+			String key2 = k.substring(i + 1);
+			return getValue(key2, config1);
+
+		} else {
+			return config.get(k);
+		}
+	}
+
+	public List<ModulesDto> getByCollectionName(String connection_id, String collectionName, UserDetail user) {
+		if (connection_id != null) {
+			Criteria criteria1 = Criteria.where("datasource").is(connection_id).and("tablename")
+					.is(collectionName).and("status").is("active");
+			List<ModulesDto> allDto = findAllDto(Query.query(criteria1), user);
+			return allDto;
+		}
+		Criteria criteria = Criteria.where("tablename").is(collectionName).and("status").is("active");
+		return findAllDto(Query.query(criteria), user);
+	}
+
+	//todo  公共库
      /* public ModulesDto apiPermission(UserDetail loginUser,ModulesDto modulesDto){
       if (modulesDto.getAccess_token()!=null){
 
       }
       }*/
-    public Map<String, Object> getApiDocument(ObjectId id, UserDetail userDetail) {
-        Query query = new Query();
-        if (id != null) {
-            Criteria criteria = Criteria.where("_id").is(id);
-            query.addCriteria(criteria);
-        }
-        ModulesDto allDto = findOne(query, userDetail);
-        Map<String, Object> map = new HashMap<>();
-        map.put("name", allDto.getName());//getName
-        map.put("status", allDto.getStatus());//getStatus
-        map.put("format", "JSON");
-        map.put("user", allDto.getUser());//getUser
-        map.put("createUser", userDetail.getEmail());
-        map.put("listtags", allDto.getListtags());//getListTags
-        map.put("last_updated", allDto.getLast_updated());//getLast_updated
-        map.put("apiVersion", allDto.getApiVersion());//getApiVersion
-        map.put("basePath", allDto.getBasePath());
-        map.put("paths", new ArrayList<>());//getBasePath
-        //getPaths
-        List<Field> fields = getFileds(allDto.getFields(), null, null);//getFields
-        for (int i = 0; i < allDto.getPaths().size(); i++) {//getPaths
-            Path pathObj = allDto.getPaths().get(i);//getPaths
-            if ("defaultApi".equals(allDto.getApiType())) {//getApiType
-                if ("create".equals(pathObj.getName())) {
-                    List<Map<String, Object>> paths = createApiDoc(pathObj, fields);
-                    for (Map<String, Object> path : paths) {
-                        map.put("paths", path);
-                    }
-                } else if (Objects.equals(pathObj.getName(), "findById")) {
-                    List<Map<String, Object>> paths = findByIdDoc(pathObj, fields);
-                    for (Map<String, Object> path : paths) {
-                        map.put("paths", path);
-                    }
-                } else if (Objects.equals(pathObj.getName(), "updateById")) {
-                    List<Map<String, Object>> paths = updateByIdDoc(pathObj, fields);
-                    for (Map<String, Object> path : paths) {
-                        map.put("paths", path);
-                    }
-                } else if (Objects.equals(pathObj.getName(), "deleteById")) {
-                    List<Map<String, Object>> paths = deleteByIdDoc(pathObj, fields);
-                    for (Map<String, Object> path : paths) {
-                        map.put("paths", path);
-                    }
-                } else if (Objects.equals(pathObj.getName(), "findPage")) {
-                    List<Map<String, Object>> paths = findByIdDoc(pathObj, fields);
-                    for (Map<String, Object> path : paths) {
-                        map.put("paths", path);
-                    }
-                }
-            } else if (Objects.equals(pathObj.getName(), "GET")) {
-                List<Map<String, Object>> paths = custGetDoc(pathObj, fields);
-                for (Map<String, Object> path : paths) {
-                    map.put("paths", path);
-                }
-            }
-        }
-        return map;
-
-    }
-
-    public List<Map<String, Object>> findByIdDoc(Path pathObj, List<Field> fields) {
-        List<Map<String, Object>> list = new ArrayList<>();
-        Map<String, Object> map = new HashMap<>();
-        map.put("method", "GET");
-        map.put("path", pathObj.getPath());
-        map.put("description", "Get record by ID");
-        Map responseFields = JsonUtil.parseJson("{'field_name':'_id','field_type':'String','required':'否','example':''}", Map.class);
-        map.put("requestFields", "{'field_name':'id','field_type':'String','required':'是','example':'5edf26e662a932388458f153'}");
-        map.put("responseFields", responseFields);
-        map.put("requestExample", "http://127.0.0.1:3080" + pathObj.getPath());
-        map.put("responseExample", getExample((List<Field>) map.get("responseFields")));
-        list.add(map);
-        return list;
-    }
-
-    public List<Map<String, Object>> custGetDoc(Path pathObj, List<Field> fields) {
-        List<Map<String, Object>> list = new ArrayList<>();
-        Map<String, Object> map = new HashMap<>();
-        map.put("method", "GET");
-        map.put("path", pathObj.getPath());
-        map.put("description", "Paging records");
-        map.put("requestFields", getFileds(fields, (Path) pathObj.getAvailableQueryField(), (Path) pathObj.getRequiredQueryField()));
-        map.put("responseFields", getFileds(pathObj.getFields(), (Path) pathObj.getAvailableQueryField(), (Path) pathObj.getRequiredQueryField()));
-        map.put("requestExample", "URL:http://127.0.0.1:3080" + pathObj.getPath() + "?filter={\"limit\":10,\"skip\"=50，\"where\":{\"property\":{\"operator\":value}}}");
-        map.put("responseExample", "[\\n\" +\n" +
-                "\t\t\tgetExample(obj.responseFields)+\n" +
-                "\t\t\t\"]");
-        list.add(map);
-        Map<String, Object> map1 = new HashMap<>();
-        map1.put("method", "POST");
-        map1.put("path", pathObj.getPath());
-        map1.put("description", "Paging records");
-        map1.put("requestFields", getFileds(fields, (Path) pathObj.getAvailableQueryField(), (Path) pathObj.getRequiredQueryField()));
-        map1.put("requestExample", "URL:http://127.0.0.1:3080" + pathObj.getPath() + "\\nbody:\\n{filter:{\"limit\":10,\"skip\"=50，\"where\":{\"property\":{\"operator\":value}}}}");
-        map1.put("responseExample", "[\\n\" +\n" +
-                "\t\t\tgetExample(obj1.responseFields)+\n" +
-                "\t\t\t\"]");
-        list.add(map1);
-        return list;
-    }
-
-    public List<Map<String, Object>> findPageDoc(Path pathObj, List<Field> fields) {
-        List<Map<String, Object>> list = new ArrayList<>();
-        Map<String, Object> map = new HashMap<>();
-        map.put("method", "GET");
-        map.put("path", pathObj.getPath());
-        map.put("description", "Paging records");
-        map.put("requestFields", "{'field_name':'limit','field_type':'int','required':'是','example':'10'},{'field_name':'skip','field_type':'int','required':'是','example':'50'},\n" +
-                "\t\t\t{'field_name':'where','field_type':'Object','required':'否','example':'{\"where\":{\"property\":value}}'");
-        Map responseFields = JsonUtil.parseJson("{'field_name':'_id','field_type':'String','required':'否','example':''}", Map.class);
-        map.put("responseFields", responseFields);
-        map.put("requestExample", "URL:http://127.0.0.1:3080" + pathObj.getPath() + "/find?filter={\"limit\":10,\"skip\"=50，\"where\":{\"property\":{\"operator\":value}}}");
-        map.put("responseExample", "[\\n\" +\n" +
-                "\t\t\tgetExample(obj.responseFields)+\n" +
-                "\t\t\t\"]\"");
-        list.add(map);
-        Map<String, Object> map1 = new HashMap<>();
-        map1.put("method", "POST");
-        map1.put("path", pathObj.getPath() + "/find");
-        map1.put("description", "Paging records");
-        map1.put("requestFields", "{'field_name':'limit','field_type':'int','required':'是','example':'10'},\n" +
-                "\t\t\t{'field_name':'skip','field_type':'int','required':'是','example':'50'},\n" +
-                "\t\t\t{'field_name':'where','field_type':'Object','required':'否','example':'{\"where\":{\"property\":value}}'}");
-        map1.put("responseFields", responseFields);
-        map1.put("requestExample", "URL:http://127.0.0.1:3080" + pathObj.getPath() + "\\nbody:\\n{filter:{\"limit\":10,\"skip\"=50，\"where\":{\"property\":{\"operator\":value}}}}");
-        map1.put("responseExample", "[\\n\" +\n" +
-                "\t\t\tgetExample(obj1.responseFields)+\n" +
-                "\t\t\t\"]");
-        list.add(map1);
-        return list;
-    }
-
-    public List<Map<String, Object>> updateByIdDoc(Path pathObj, List<Field> fields) {
-        List<Map<String, Object>> list = new ArrayList<>();
-        Map<String, Object> map = new HashMap<>();
-        map.put("method", "POST");
-        map.put("path", pathObj.getPath());
-        map.put("description", "Update by ID");
-        map.put("requestFields", getFileds(fields, null, null));
-        map.put("responseFields", new ArrayList<>());
-        map.put("requestExample", "URL:http://127.0.0.1:3080" + pathObj.getPath() + "\n" + "Body:\n" + getExample((List<Field>) map.get("requestFields")));
-        map.put("responseExample", "{\n" + "'204':{\n" + "description: 'CAR_CUSTOMER PATCH success',\n" + "},\n" + "}");
-        list.add(map);
-        Map<String, Object> map1 = new HashMap<>();
-        map1.put("method", "PATCH");
-        map1.put("path", pathObj.getPath());
-        map1.put("description", "Update by ID");
-        map1.put("requestFields", getFileds(fields, null, null));
-        map1.put("responseFields", new ArrayList<>());
-        map1.put("requestExample", "URL:http://127.0.0.1:3080" + pathObj.getPath() + "\n" + "Body:\n" + getExample((List<Field>) map1.get("requestFields")));
-        map1.put("responseExample", "{\n" + "'204':{\n" + "description: 'CAR_CUSTOMER PATCH success',\n" + "},\n" + "}");
-        list.add(map1);
-        Map<String, Object> map2 = new HashMap<>();
-        map2.put("method", "PATCH");
-        map2.put("path", pathObj.getPath().split("\\{", 0));
-        map2.put("description", "update all match document with where");
-        map2.put("requestFields", getFileds(fields, null, null));
-        map2.put("responseFields", new ArrayList<>());
-        map2.put("requestExample", "http://127.0.0.1:3080" + pathObj.getPath().split("\\{", 0) + "?filter={\"where\":{\"property\":value\")\n" + "Body:\n" + getExample((List<Field>) map1.get("requestFields")));
-        map2.put("responseExample", "{\n" +
-                "            '204': {\n" +
-                "              description: 'CAR_CUSTOMER PATCH success',\n" +
-                "            },\n" +
-                "}");
-        list.add(map2);
-        return list;
-    }
-
-    public List<Map<String, Object>> deleteByIdDoc(Path pathObj, List<Field> fields) {
-        List<Map<String, Object>> list = new ArrayList<>();
-        Map<String, Object> map = new HashMap<>();
-        map.put("method", "GET");
-        map.put("path", pathObj.getPath() + "/delete");
-        map.put("description", "Delete by ID");
-        map.put("requestFields", "{'field_name':'id','field_type':'String','required':'是','example':'5edf26e662a932388458f153'}");
-        map.put("responseFields", new ArrayList<>());
-        map.put("requestExample", "http://127.0.0.1:3080" + pathObj.getPath());
-        map.put("responseExample", "{\n" +
-                "            '204': {\n" +
-                "              description: 'CAR_CUSTOMER DELETE success',\n" +
-                "            },\n" +
-                "}");
-        list.add(map);
-        Map<String, Object> map1 = new HashMap<>();
-        map1.put("method", "DELETE");
-        map1.put("path", pathObj.getPath());
-        map1.put("description", "Delete all match document with where");
-        map1.put("requestFields", "{'field_name':'id','field_type':'String','required':'是','example':'5edf26e662a932388458f153'}");
-        map1.put("responseFields", new ArrayList<>());
-        map1.put("requestExample", "http://127.0.0.1:3080" + pathObj.getPath());
-        map1.put("responseExample", "{\n" +
-                "            '204': {\n" +
-                "              description: 'CAR_CUSTOMER DELETE success',\n" +
-                "            },\n" +
-                "}");
-        list.add(map1);
-        Map<String, Object> map2 = new HashMap<>();
-        map2.put("method", "DELETE");
-        map2.put("path", pathObj.getPath().split("\\{", 0));
-        map2.put("description", "Delete all match document with where");
-        map2.put("requestFields", "{'field_name':'where','field_type':'object','required':'是','example':''}");
-        map2.put("responseFields", new ArrayList<>());
-        map2.put("requestExample", "http://127.0.0.1:3080" + pathObj.getPath().split("\\{", 0) + "?filter={\"where\":{\"property\":value\")");
-        map2.put("responseExample", "{\n" +
-                "            '204': {\n" +
-                "              description: 'CAR_CUSTOMER DELETE success',\n" +
-                "            },\n" +
-                "}");
-        list.add(map2);
-        return list;
-    }
-
-
-    public List<Field> getFileds(List<Field> fields, Path availableQueryField, Path requiredQueryField) {
-        List obj = availableQueryField != null ? (List) availableQueryField : new ArrayList<>();
-        List obj1 = requiredQueryField != null ? (List) requiredQueryField : new ArrayList<>();
-        List<Field> newField = new ArrayList<>();
-        for (Field field : fields) {
-            if (!Objects.equals(field.getFieldName(), "id") && (field.getVisible() == null || field.getVisible()) && obj.size() == 0 || obj.contains(field.getFieldName())) {
-                String required = obj1.contains(field.getFieldName()) ? "是" : "否";
-                Field field1 = new Field();
-        /*map.put("field_name",field.getFieldName());
-        map.put("field_type",StringUtils.isEmpty(field.getJavaType())||StringUtils.isEmpty(field.getField_type()));
-        map.put("required",required);
-        map.put("example","");*/
-                field1.setFieldName(field.getFieldName());
-                field1.setField_type(StringUtils.isNotBlank(field.getJavaType()) ? field.getJavaType() : field.getField_type());
-                field1.setRequired(required);
-                field1.setExample("");
-                newField.add(field1);
-            }
-        }
-        return newField;
-    }
-
-
-    public List<Map<String, Object>> createApiDoc(Path pathObj, List<Field> fields) {
-        List<Map<String, Object>> list = new ArrayList<>();
-        Map<String, Object> map = new HashMap<>();
-        map.put("method", "POST");
-        map.put("path", pathObj.getPath());
-        map.put("description", "Use this interface to create new data");
-        map.put("requestFields", fields);
-        Map responseFields = JsonUtil.parseJson("{'field_name':'_id','field_type':'String','required':'否','example':''}", Map.class);
-        map.put("responseFields", responseFields);
-        map.put("requestExample", getExample(fields));
-        map.put("responseExample", getExample(fields));
-        list.add(map);
-        Map<String, Object> map1 = new HashMap<>();
-        map1.put("method", "POST");
-        map1.put("path", pathObj.getPath() + "/batch");
-        map1.put("description", "Batch import excel records");
-        Map<String, Object> objectMap = new HashMap<>();
-        objectMap.put("field_name", "total");
-        objectMap.put("field_type", "int");
-        objectMap.put("required", "否");
-        objectMap.put("example", "0");
-        List<Map<String, Object>> object = new ArrayList<>();
-        map.put("responseFields", object.add(objectMap));
-        map1.put("requestExample", "use multipart/form-data upload xlsx file.");
-        map1.put("responseExample", "{\n' + '  'total': 0,\n' + '}");
-        list.add(map1);
-        Map<String, Object> map2 = new HashMap<>();
-        map2.put("method", "GET");
-        map2.put("path", pathObj.getPath() + "/batch");
-        map2.put("description", "Download batch import excel template");
-        map2.put("requestFields", new ArrayList<>());
-        map2.put("responseFields", new ArrayList<>());
-        map2.put("requestExample", "Parameters\nNo parameters");
-        map2.put("responseExample", "content: {'application/octet-stream': {}}");
-        list.add(map2);
-        return list;
-    }
-
-    public String getExample(List<Field> fields) {
-        Map<String, Object> map = new HashMap<>();
-        String str = "{";
-        for (Field field : fields) {
-            map.put(field.getFieldName(), "");
-            str = str + "\n " + field.getFieldName() + " : " + field.getField_type() + ",";
-        }
-        str = str.substring(0, str.length() - 1);
-        str = str + "}";
-        return str;
-    }
-
-
-    public List findAllActiveApi(ModuleStatusEnum moduleStatusEnum) {
-        Query query = Query.query(Criteria.where("status").is(moduleStatusEnum.getValue()).and("is_deleted").ne(true));
-        List<ModulesDto> modulesDtoList = findAll(query);
-        return modulesDtoList;
-    }
+	public Map<String, Object> getApiDocument(ObjectId id, UserDetail userDetail) {
+		Query query = new Query();
+		if (id != null) {
+			Criteria criteria = Criteria.where("_id").is(id);
+			query.addCriteria(criteria);
+		}
+		ModulesDto allDto = findOne(query, userDetail);
+		Map<String, Object> map = new HashMap<>();
+		map.put("name", allDto.getName());//getName
+		map.put("status", allDto.getStatus());//getStatus
+		map.put("format", "JSON");
+		map.put("user", allDto.getUser());//getUser
+		map.put("createUser", userDetail.getEmail());
+		map.put("listtags", allDto.getListtags());//getListTags
+		map.put("last_updated", allDto.getLast_updated());//getLast_updated
+		map.put("apiVersion", allDto.getApiVersion());//getApiVersion
+		map.put("basePath", allDto.getBasePath());
+		map.put("paths", new ArrayList<>());//getBasePath
+		//getPaths
+		List<Field> fields = getFileds(allDto.getFields(), null, null);//getFields
+		for (int i = 0; i < allDto.getPaths().size(); i++) {//getPaths
+			Path pathObj = allDto.getPaths().get(i);//getPaths
+			if ("defaultApi".equals(allDto.getApiType())) {//getApiType
+				if ("create".equals(pathObj.getName())) {
+					List<Map<String, Object>> paths = createApiDoc(pathObj, fields);
+					for (Map<String, Object> path : paths) {
+						map.put("paths", path);
+					}
+				} else if (Objects.equals(pathObj.getName(), "findById")) {
+					List<Map<String, Object>> paths = findByIdDoc(pathObj, fields);
+					for (Map<String, Object> path : paths) {
+						map.put("paths", path);
+					}
+				} else if (Objects.equals(pathObj.getName(), "updateById")) {
+					List<Map<String, Object>> paths = updateByIdDoc(pathObj, fields);
+					for (Map<String, Object> path : paths) {
+						map.put("paths", path);
+					}
+				} else if (Objects.equals(pathObj.getName(), "deleteById")) {
+					List<Map<String, Object>> paths = deleteByIdDoc(pathObj, fields);
+					for (Map<String, Object> path : paths) {
+						map.put("paths", path);
+					}
+				} else if (Objects.equals(pathObj.getName(), "findPage")) {
+					List<Map<String, Object>> paths = findByIdDoc(pathObj, fields);
+					for (Map<String, Object> path : paths) {
+						map.put("paths", path);
+					}
+				}
+			} else if (Objects.equals(pathObj.getName(), "GET")) {
+				List<Map<String, Object>> paths = custGetDoc(pathObj, fields);
+				for (Map<String, Object> path : paths) {
+					map.put("paths", path);
+				}
+			}
+		}
+		return map;
 
-    /**
-     * basePath+version 不能重复
-     */
-    private List<ModulesDto> isBasePathAndVersionRepeat(String basepath, String apiVersion) {
-        Query query = Query.query(Criteria.where("is_deleted").ne(true));
-        query.addCriteria(Criteria.where("basePath").is(basepath));
-        query.addCriteria(Criteria.where("apiVersion").is(apiVersion));
-        List<ModulesDto> modulesDto = findAll(query);
-        return modulesDto;
-    }
+	}
 
-    /**
-     * api情况预览
-     *
-     * @return
-     */
-    public PreviewVo preview(UserDetail userDetail) {
-        PreviewVo previewVo = new PreviewVo();
+	public List<Map<String, Object>> findByIdDoc(Path pathObj, List<Field> fields) {
+		List<Map<String, Object>> list = new ArrayList<>();
+		Map<String, Object> map = new HashMap<>();
+		map.put("method", "GET");
+		map.put("path", pathObj.getPath());
+		map.put("description", "Get record by ID");
+		Map responseFields = JsonUtil.parseJson("{'field_name':'_id','field_type':'String','required':'否','example':''}", Map.class);
+		map.put("requestFields", "{'field_name':'id','field_type':'String','required':'是','example':'5edf26e662a932388458f153'}");
+		map.put("responseFields", responseFields);
+		map.put("requestExample", "http://127.0.0.1:3080" + pathObj.getPath());
+		map.put("responseExample", getExample((List<Field>) map.get("responseFields")));
+		list.add(map);
+		return list;
+	}
 
-        List<ModulesDto> modulesDtoList = getByUserId(userDetail.getUserId());
-        List<String> ids = modulesDtoList.stream().map(ModulesDto::getId).map(ObjectId::toString).collect(Collectors.toList());
+	public List<Map<String, Object>> custGetDoc(Path pathObj, List<Field> fields) {
+		List<Map<String, Object>> list = new ArrayList<>();
+		Map<String, Object> map = new HashMap<>();
+		map.put("method", "GET");
+		map.put("path", pathObj.getPath());
+		map.put("description", "Paging records");
+		map.put("requestFields", getFileds(fields, (Path) pathObj.getAvailableQueryField(), (Path) pathObj.getRequiredQueryField()));
+		map.put("responseFields", getFileds(pathObj.getFields(), (Path) pathObj.getAvailableQueryField(), (Path) pathObj.getRequiredQueryField()));
+		map.put("requestExample", "URL:http://127.0.0.1:3080" + pathObj.getPath() + "?filter={\"limit\":10,\"skip\"=50，\"where\":{\"property\":{\"operator\":value}}}");
+		map.put("responseExample", "[\\n\" +\n" +
+				"\t\t\tgetExample(obj.responseFields)+\n" +
+				"\t\t\t\"]");
+		list.add(map);
+		Map<String, Object> map1 = new HashMap<>();
+		map1.put("method", "POST");
+		map1.put("path", pathObj.getPath());
+		map1.put("description", "Paging records");
+		map1.put("requestFields", getFileds(fields, (Path) pathObj.getAvailableQueryField(), (Path) pathObj.getRequiredQueryField()));
+		map1.put("requestExample", "URL:http://127.0.0.1:3080" + pathObj.getPath() + "\\nbody:\\n{filter:{\"limit\":10,\"skip\"=50，\"where\":{\"property\":{\"operator\":value}}}}");
+		map1.put("responseExample", "[\\n\" +\n" +
+				"\t\t\tgetExample(obj1.responseFields)+\n" +
+				"\t\t\t\"]");
+		list.add(map1);
+		return list;
+	}
 
-        List<ApiCallEntity> apiCallEntityList = apiCallService.findByModuleIds(ids);
+	public List<Map<String, Object>> findPageDoc(Path pathObj, List<Field> fields) {
+		List<Map<String, Object>> list = new ArrayList<>();
+		Map<String, Object> map = new HashMap<>();
+		map.put("method", "GET");
+		map.put("path", pathObj.getPath());
+		map.put("description", "Paging records");
+		map.put("requestFields", "{'field_name':'limit','field_type':'int','required':'是','example':'10'},{'field_name':'skip','field_type':'int','required':'是','example':'50'},\n" +
+				"\t\t\t{'field_name':'where','field_type':'Object','required':'否','example':'{\"where\":{\"property\":value}}'");
+		Map responseFields = JsonUtil.parseJson("{'field_name':'_id','field_type':'String','required':'否','example':''}", Map.class);
+		map.put("responseFields", responseFields);
+		map.put("requestExample", "URL:http://127.0.0.1:3080" + pathObj.getPath() + "/find?filter={\"limit\":10,\"skip\"=50，\"where\":{\"property\":{\"operator\":value}}}");
+		map.put("responseExample", "[\\n\" +\n" +
+				"\t\t\tgetExample(obj.responseFields)+\n" +
+				"\t\t\t\"]\"");
+		list.add(map);
+		Map<String, Object> map1 = new HashMap<>();
+		map1.put("method", "POST");
+		map1.put("path", pathObj.getPath() + "/find");
+		map1.put("description", "Paging records");
+		map1.put("requestFields", "{'field_name':'limit','field_type':'int','required':'是','example':'10'},\n" +
+				"\t\t\t{'field_name':'skip','field_type':'int','required':'是','example':'50'},\n" +
+				"\t\t\t{'field_name':'where','field_type':'Object','required':'否','example':'{\"where\":{\"property\":value}}'}");
+		map1.put("responseFields", responseFields);
+		map1.put("requestExample", "URL:http://127.0.0.1:3080" + pathObj.getPath() + "\\nbody:\\n{filter:{\"limit\":10,\"skip\"=50，\"where\":{\"property\":{\"operator\":value}}}}");
+		map1.put("responseExample", "[\\n\" +\n" +
+				"\t\t\tgetExample(obj1.responseFields)+\n" +
+				"\t\t\t\"]");
+		list.add(map1);
+		return list;
+	}
 
-        Map<String, List<ApiCallEntity>> codeToApiCallMap = apiCallService.getVisitTotalCount(apiCallEntityList);
+	public List<Map<String, Object>> updateByIdDoc(Path pathObj, List<Field> fields) {
+		List<Map<String, Object>> list = new ArrayList<>();
+		Map<String, Object> map = new HashMap<>();
+		map.put("method", "POST");
+		map.put("path", pathObj.getPath());
+		map.put("description", "Update by ID");
+		map.put("requestFields", getFileds(fields, null, null));
+		map.put("responseFields", new ArrayList<>());
+		map.put("requestExample", "URL:http://127.0.0.1:3080" + pathObj.getPath() + "\n" + "Body:\n" + getExample((List<Field>) map.get("requestFields")));
+		map.put("responseExample", "{\n" + "'204':{\n" + "description: 'CAR_CUSTOMER PATCH success',\n" + "},\n" + "}");
+		list.add(map);
+		Map<String, Object> map1 = new HashMap<>();
+		map1.put("method", "PATCH");
+		map1.put("path", pathObj.getPath());
+		map1.put("description", "Update by ID");
+		map1.put("requestFields", getFileds(fields, null, null));
+		map1.put("responseFields", new ArrayList<>());
+		map1.put("requestExample", "URL:http://127.0.0.1:3080" + pathObj.getPath() + "\n" + "Body:\n" + getExample((List<Field>) map1.get("requestFields")));
+		map1.put("responseExample", "{\n" + "'204':{\n" + "description: 'CAR_CUSTOMER PATCH success',\n" + "},\n" + "}");
+		list.add(map1);
+		Map<String, Object> map2 = new HashMap<>();
+		map2.put("method", "PATCH");
+		map2.put("path", pathObj.getPath().split("\\{", 0));
+		map2.put("description", "update all match document with where");
+		map2.put("requestFields", getFileds(fields, null, null));
+		map2.put("responseFields", new ArrayList<>());
+		map2.put("requestExample", "http://127.0.0.1:3080" + pathObj.getPath().split("\\{", 0) + "?filter={\"where\":{\"property\":value\")\n" + "Body:\n" + getExample((List<Field>) map1.get("requestFields")));
+		map2.put("responseExample", "{\n" +
+				"            '204': {\n" +
+				"              description: 'CAR_CUSTOMER PATCH success',\n" +
+				"            },\n" +
+				"}");
+		list.add(map2);
+		return list;
+	}
 
-        List<ApiCallEntity> normalApiCallList = codeToApiCallMap.getOrDefault("normal", new ArrayList<>());
-        List<ApiCallEntity> warningApiCallList = codeToApiCallMap.getOrDefault("warning", new ArrayList<>());
+	public List<Map<String, Object>> deleteByIdDoc(Path pathObj, List<Field> fields) {
+		List<Map<String, Object>> list = new ArrayList<>();
+		Map<String, Object> map = new HashMap<>();
+		map.put("method", "GET");
+		map.put("path", pathObj.getPath() + "/delete");
+		map.put("description", "Delete by ID");
+		map.put("requestFields", "{'field_name':'id','field_type':'String','required':'是','example':'5edf26e662a932388458f153'}");
+		map.put("responseFields", new ArrayList<>());
+		map.put("requestExample", "http://127.0.0.1:3080" + pathObj.getPath());
+		map.put("responseExample", "{\n" +
+				"            '204': {\n" +
+				"              description: 'CAR_CUSTOMER DELETE success',\n" +
+				"            },\n" +
+				"}");
+		list.add(map);
+		Map<String, Object> map1 = new HashMap<>();
+		map1.put("method", "DELETE");
+		map1.put("path", pathObj.getPath());
+		map1.put("description", "Delete all match document with where");
+		map1.put("requestFields", "{'field_name':'id','field_type':'String','required':'是','example':'5edf26e662a932388458f153'}");
+		map1.put("responseFields", new ArrayList<>());
+		map1.put("requestExample", "http://127.0.0.1:3080" + pathObj.getPath());
+		map1.put("responseExample", "{\n" +
+				"            '204': {\n" +
+				"              description: 'CAR_CUSTOMER DELETE success',\n" +
+				"            },\n" +
+				"}");
+		list.add(map1);
+		Map<String, Object> map2 = new HashMap<>();
+		map2.put("method", "DELETE");
+		map2.put("path", pathObj.getPath().split("\\{", 0));
+		map2.put("description", "Delete all match document with where");
+		map2.put("requestFields", "{'field_name':'where','field_type':'object','required':'是','example':''}");
+		map2.put("responseFields", new ArrayList<>());
+		map2.put("requestExample", "http://127.0.0.1:3080" + pathObj.getPath().split("\\{", 0) + "?filter={\"where\":{\"property\":value\")");
+		map2.put("responseExample", "{\n" +
+				"            '204': {\n" +
+				"              description: 'CAR_CUSTOMER DELETE success',\n" +
+				"            },\n" +
+				"}");
+		list.add(map2);
+		return list;
+	}
 
-        //访问总次数  和警告访问总次数
-        previewVo.setVisitTotalCount((long) (warningApiCallList.size() + normalApiCallList.size()));
-        previewVo.setWarningVisitTotalCount((long) warningApiCallList.size());
 
+	public List<Field> getFileds(List<Field> fields, Path availableQueryField, Path requiredQueryField) {
+		List obj = availableQueryField != null ? (List) availableQueryField : new ArrayList<>();
+		List obj1 = requiredQueryField != null ? (List) requiredQueryField : new ArrayList<>();
+		List<Field> newField = new ArrayList<>();
+		for (Field field : fields) {
+			if (!Objects.equals(field.getFieldName(), "id") && (field.getVisible() == null || field.getVisible()) && obj.size() == 0 || obj.contains(field.getFieldName())) {
+				String required = obj1.contains(field.getFieldName()) ? "是" : "否";
+				Field field1 = new Field();
+        /*map.put("field_name",field.getFieldName());
+        map.put("field_type",StringUtils.isEmpty(field.getJavaType())||StringUtils.isEmpty(field.getField_type()));
+        map.put("required",required);
+        map.put("example","");*/
+				field1.setFieldName(field.getFieldName());
+				field1.setField_type(StringUtils.isNotBlank(field.getJavaType()) ? field.getJavaType() : field.getField_type());
+				field1.setRequired(required);
+				field1.setExample("");
+				newField.add(field1);
+			}
+		}
+		return newField;
+	}
 
-        //只要有过告警的，就算一个告警api
-        List<String> warnModuleIds = warningApiCallList.stream().map(ApiCallEntity::getAllPathId).collect(Collectors.toList()).stream().distinct().collect(Collectors.toList());
-        previewVo.setWarningApiCount((long) warnModuleIds.size());
 
-        previewVo.setTransmitTotal(apiCallService.getTransmitTotal(apiCallEntityList));
-        previewVo.setTotalCount((long) modulesDtoList.size());
-        previewVo.setVisitTotalLine(apiCallService.getVisitTotalLine(apiCallEntityList));
+	public List<Map<String, Object>> createApiDoc(Path pathObj, List<Field> fields) {
+		List<Map<String, Object>> list = new ArrayList<>();
+		Map<String, Object> map = new HashMap<>();
+		map.put("method", "POST");
+		map.put("path", pathObj.getPath());
+		map.put("description", "Use this interface to create new data");
+		map.put("requestFields", fields);
+		Map responseFields = JsonUtil.parseJson("{'field_name':'_id','field_type':'String','required':'否','example':''}", Map.class);
+		map.put("responseFields", responseFields);
+		map.put("requestExample", getExample(fields));
+		map.put("responseExample", getExample(fields));
+		list.add(map);
+		Map<String, Object> map1 = new HashMap<>();
+		map1.put("method", "POST");
+		map1.put("path", pathObj.getPath() + "/batch");
+		map1.put("description", "Batch import excel records");
+		Map<String, Object> objectMap = new HashMap<>();
+		objectMap.put("field_name", "total");
+		objectMap.put("field_type", "int");
+		objectMap.put("required", "否");
+		objectMap.put("example", "0");
+		List<Map<String, Object>> object = new ArrayList<>();
+		map.put("responseFields", object.add(objectMap));
+		map1.put("requestExample", "use multipart/form-data upload xlsx file.");
+		map1.put("responseExample", "{\n' + '  'total': 0,\n' + '}");
+		list.add(map1);
+		Map<String, Object> map2 = new HashMap<>();
+		map2.put("method", "GET");
+		map2.put("path", pathObj.getPath() + "/batch");
+		map2.put("description", "Download batch import excel template");
+		map2.put("requestFields", new ArrayList<>());
+		map2.put("responseFields", new ArrayList<>());
+		map2.put("requestExample", "Parameters\nNo parameters");
+		map2.put("responseExample", "content: {'application/octet-stream': {}}");
+		list.add(map2);
+		return list;
+	}
+
+	public String getExample(List<Field> fields) {
+		Map<String, Object> map = new HashMap<>();
+		String str = "{";
+		for (Field field : fields) {
+			map.put(field.getFieldName(), "");
+			str = str + "\n " + field.getFieldName() + " : " + field.getField_type() + ",";
+		}
+		str = str.substring(0, str.length() - 1);
+		str = str + "}";
+		return str;
+	}
 
-        executeRankList(userDetail, modulesDtoList, apiCallEntityList);
 
-        return previewVo;
-    }
+	public List findAllActiveApi(ModuleStatusEnum moduleStatusEnum) {
+		Query query = Query.query(Criteria.where("status").is(moduleStatusEnum.getValue()).and("is_deleted").ne(true));
+		List<ModulesDto> modulesDtoList = findAll(query);
+		return modulesDtoList;
+	}
 
+	/**
+	 * basePath+version 不能重复
+	 */
+	private List<ModulesDto> isBasePathAndVersionRepeat(String basepath, String apiVersion) {
+		Query query = Query.query(Criteria.where("is_deleted").ne(true));
+		query.addCriteria(Criteria.where("basePath").is(basepath));
+		query.addCriteria(Criteria.where("apiVersion").is(apiVersion));
+		List<ModulesDto> modulesDto = findAll(query);
+		return modulesDto;
+	}
 
-    /**
-     * 发生过告警的api的数量
-     *
-     * @param
-     * @return
-     */
-   /* private Long getWarningApis(Map<String, List<ApiCallEntity>> codeToApiCallMap, List<String> moduleIdsList) {
-        List<String> intersection = new ArrayList<>();
-        try {
-            List<ApiCallEntity> apiCallEntityList = codeToApiCallMap.get("warning");
-            List<String> allPathId = apiCallEntityList.stream().map(ApiCallEntity::getAllPathId).collect(Collectors.toList());
-            moduleIdsList.stream().filter(allPathId::contains).collect(Collectors.toList());
-        } catch (Exception e) {
-            log.error("发生过告警的api的数量 异常", e);
-        }
-        return Long.valueOf(intersection.size());
-    }*/
-
-
-  /*  private Long getTotalApiCount(UserDetail userDetail) {
-        Query query = Query.query(Criteria.where("is_deleted").ne(true).and("user_id").is(userDetail.getUserId()));
-        Long count = repository.count(query);
-        return count;
-    }*/
-    public List<ModulesDto> getByUserId(String userId) {
-        Query query = Query.query(Criteria.where("is_deleted").ne(true).and("user_id").is(userId));
-        List<ModulesDto> modulesDtoList = findAll(query);
-        return modulesDtoList;
-    }
+	public PreviewVo preview(UserDetail userDetail) {
+		PreviewVo previewVo = new PreviewVo();
+		if (null == userDetail) {
+			return previewVo;
+		}
+		String userId = userDetail.getUserId();
+		if (StringUtils.isBlank(userId)) {
+			return previewVo;
+		}
+		ApiCallStatsDto apiCallStatsDto = apiCallStatsService.aggregateByUserId(userDetail.isRoot() ? "" : userId);
+		Query query = Query.query(Criteria.where("is_deleted").ne(true));
+		if (!userDetail.isRoot()) {
+			query.addCriteria(Criteria.where("user_id").is(userId));
+		}
+		previewVo.setTotalCount(count(query));
+		previewVo.setVisitTotalCount(apiCallStatsDto.getCallTotalCount());
+		previewVo.setTransmitTotal(apiCallStatsDto.getTransferDataTotalBytes());
+		previewVo.setWarningApiCount(apiCallStatsDto.getAlarmApiTotalCount());
+		previewVo.setWarningVisitTotalCount(apiCallStatsDto.getCallAlarmTotalCount());
+		previewVo.setVisitTotalLine(apiCallStatsDto.getResponseDataRowTotalCount());
+		previewVo.setLastUpdAt(null != apiCallStatsDto.getLastUpdAt() ? apiCallStatsDto.getLastUpdAt().getTime() : 0L);
+		return previewVo;
+	}
+
+	public List<ModulesDto> getByUserId(String userId) {
+		Query query = Query.query(Criteria.where("is_deleted").ne(true).and("user_id").is(userId));
+		List<ModulesDto> modulesDtoList = findAll(query);
+		return modulesDtoList;
+	}
 
 
     /**
@@ -880,362 +851,320 @@ public List<ModulesDto> getByUserId(String userId) {
     public RankListsVo rankLists(Filter filter, UserDetail userDetail) {
         String order = (String) (filter.getOrder() == null ? "DESC" : filter.getOrder());
         String type = (String) filter.getWhere().getOrDefault("type", "failRate");
+		switch (type) {
+			case "responseTime":
+				type = "maxResponseTime";
+				break;
+			case "failRate":
+				type = "accessFailureRate";
+				break;
+			default:
+				break;
+		}
         RankListsVo rankListsVo = new RankListsVo();
 
+		Query query = new Query();
+		if (!userDetail.isRoot()) {
+			query.addCriteria(Criteria.where("user_id").is(userDetail.getUserId()));
+		}
+		Long total = apiCallStatsService.count(query);
+		query.fields().include("moduleId", type);
+		if ("DESC".equals(order)) {
+			query.with(Sort.by(type).descending());
 
-        Query query = Query.query(Criteria.where("is_deleted").ne(true).and("user_id").is(userDetail.getUserId()));
-        List<ModulesEntity> modulesEntityList = new ArrayList<>();
-        if ("DESC".equals(order)) {
-            query.with(Sort.by(type).descending());
-
-        } else if ("ASC".equals(order)) {
-            query.with(Sort.by(type).ascending());
-        }
-        TmPageable tmPageable = new TmPageable();
-        Integer page = (filter.getSkip() / filter.getLimit()) + 1;
-        tmPageable.setPage(page);
-        tmPageable.setSize(filter.getLimit());
-
-        Long total = repository.getMongoOperations().count(query, ModulesEntity.class);
-        modulesEntityList = repository.getMongoOperations().find(query.with(tmPageable), ModulesEntity.class);
-
-        List<Map> list = new ArrayList<>();
-        modulesEntityList.forEach(modulesEntity -> {
-            Map map = new HashMap();
-            Object value = BeanUtil.getProperty(modulesEntity, type) == null ? 0L : BeanUtil.getProperty(modulesEntity, type);
-            map.put(modulesEntity.getName(), value);
-            list.add(map);
-        });
-
-
-        rankListsVo.setItems(list);
-        rankListsVo.setTotal(total);
-        return rankListsVo;
-    }
-
-
-    public Page<ApiListVo> apiList(Filter filter, UserDetail userDetail) {
-        Where where = filter.getWhere();
-        if (null != where.get("clientId")) {
-            String clientId = (String) where.remove("clientId");
-            List<ApiCallEntity> apiCallEntityList = apiCallService.findByClientId(clientId);
-            List<String> moduleIdList = apiCallEntityList.stream().map(ApiCallEntity::getAllPathId).collect(Collectors.toList()).stream().distinct().collect(Collectors.toList());
-            Map notDeleteMap = new HashMap();
-            notDeleteMap.put("$in", moduleIdList);
-            where.put("id", notDeleteMap);
-        }
-
-        Map notDeleteMap = new HashMap();
-        notDeleteMap.put("$ne", true);
-        where.put("is_deleted", notDeleteMap);
-        Page page = find(filter, userDetail);
-        List<ApiListVo> apiListVoList = new ArrayList<>();
-        List<ModulesDto> items = page.getItems();
-
-        List<String> moduleIdList = items.stream().map(ModulesDto::getId).map(ObjectId::toString).collect(Collectors.toList());
-        List<ApiCallEntity> apiCallEntityList = apiCallService.findByModuleIds(moduleIdList);
-        Map<String, List<ApiCallEntity>> moduleIdToApicallList = apiCallEntityList.stream().collect(Collectors.groupingBy(p -> p.getAllPathId(), Collectors.toList()));
-
-        for (ModulesDto modulesDto : items) {
-            String moduleId = modulesDto.getId().toString();
-            ApiListVo apiListVo = BeanUtil.copyProperties(modulesDto, ApiListVo.class);
-
-            List<ApiCallEntity> singleApiCallList = moduleIdToApicallList.getOrDefault(moduleId, new ArrayList<ApiCallEntity>());
-
-            Double sumVisitLine = singleApiCallList.stream().filter(apiCallEntity -> null != apiCallEntity.getResRows()).mapToDouble(ApiCallEntity::getResRows).sum();
-            apiListVo.setVisitLine(sumVisitLine.longValue());
-
-
-            Integer sumVisitCount = singleApiCallList.size();
-            apiListVo.setVisitCount(sumVisitCount.longValue());
-
-            Double transmitQuantity = singleApiCallList.stream().mapToDouble(ApiCallEntity::getReqBytes).sum();
-            apiListVo.setTransitQuantity(transmitQuantity.longValue());
-            apiListVoList.add(apiListVo);
-        }
-        page.setItems(apiListVoList);
-        return page;
-    }
+		} else if ("ASC".equals(order)) {
+			query.with(Sort.by(type).ascending());
+		}
+		TmPageable tmPageable = filterToTmPageable(filter);
+		List<ApiCallStatsDto> apiCallStatsDtoList = apiCallStatsService.findAll(query.with(tmPageable));
+		List<ObjectId> moduleIds = apiCallStatsDtoList.stream().map(acs -> new ObjectId(acs.getModuleId())).collect(Collectors.toList());
+		List<ModulesDto> modulesDtoList = findAll(Query.query(Criteria.where("id").in(moduleIds)));
+
+		List<Map<String, Object>> list = new ArrayList<>();
+		String finalType = type;
+		apiCallStatsDtoList.forEach(apiCallStatsDto -> {
+			Map<String, Object> map = new HashMap<>();
+			Object value = BeanUtil.getProperty(apiCallStatsDto, finalType) == null ? 0L : BeanUtil.getProperty(apiCallStatsDto, finalType);
+			AtomicReference<String> name = new AtomicReference<>("");
+			modulesDtoList.stream().filter(modulesDto -> apiCallStatsDto.getModuleId().equals(modulesDto.getId().toString())).findFirst().ifPresent(modulesDto -> name.set(modulesDto.getName()));
+			map.put(name.get(), value);
+			list.add(map);
+		});
+
+		rankListsVo.setItems(list);
+		rankListsVo.setTotal(total);
+		return rankListsVo;
+	}
 
-    /**
-     * 时间间隔  （可选5分钟、10分钟、30分钟、60分钟））
-     * "type":    "visitTotalLine",       可选值：visitTotalLine (访问行数),
-     * timeConsuming（耗时），
-     * speed（传输速率）
-     * latency（响应时间）
-     *
-     * @param apiDetailParam
-     * @return
-     */
-    public ApiDetailVo apiDetail(ApiDetailParam apiDetailParam, UserDetail userDetail) {
-        ApiDetailVo apiDetailVo = new ApiDetailVo();
-        List<String> moduleIds = new ArrayList<>();
-        moduleIds.add(apiDetailParam.getId());
-        List<ApiCallEntity> apiCallEntityList = apiCallService.findByModuleIds(moduleIds);
-
-        if (CollectionUtils.isNotEmpty(apiCallEntityList)) {
-            apiDetailVo.setVisitTotalCount(Long.valueOf(apiCallEntityList.size()));
-            //传输的数据量
-            Double sumReqBytes = apiCallEntityList.stream().mapToDouble(ApiCallEntity::getReqBytes).sum();
-            apiDetailVo.setVisitQuantity(sumReqBytes.longValue());
-            // 总的响应时间
-            Double totalLatency = apiCallEntityList.stream().mapToDouble(ApiCallEntity::getLatency).sum();
-
-            // 传输速率
-            apiDetailVo.setSpeed((sumReqBytes.longValue() / totalLatency.longValue()) * 1000);
-
-            //最新一次的响应时间
-            ApiCallEntity latestApiCall = apiCallEntityList.get(0);
-            if (null != latestApiCall.getResRows() && latestApiCall.getResRows() > 0) {
-                apiDetailVo.setResponseTime(latestApiCall.getLatency() / latestApiCall.getResRows());
-            }
-            //最新一次的的耗时
-            apiDetailVo.setTimeConsuming(latestApiCall.getLatency());
-
-            // 总行数
-            Double totalLine = apiCallEntityList.stream().filter(item -> null != item.getResRows()).mapToDouble(ApiCallEntity::getResRows).sum();
-            apiDetailVo.setVisitTotalLine(totalLine.longValue());
-
-
-            //查询客户端
-            List<Map<String, String>> clientNameList = new ArrayList<>();
-            List<Map<String, String>> idToClientName = apiCallService.findClients(moduleIds);
-            for (Map<String, String> map : idToClientName) {
-                clientNameList.add(map);
-            }
-            apiDetailVo.setClientNameList(clientNameList);
-        }
-
-        Integer guanluary = apiDetailParam.getGuanluary();
-        Long startParam = apiDetailParam.getStart();
-
-        Date startDate = DateUtil.offsetMinute(new Date(startParam), 0 - guanluary).toJdkDate();
-        Date endDate = new Date(startParam);
-
-        List<Long> dateList = getDateList(startDate.getTime(), endDate.getTime(), guanluary).stream().map(Date::getTime).collect(Collectors.toList());
-
-        List<ApiCallEntity> timePeriodApiCallList = apiCallService.findByModuleIdAndTimePeriod(moduleIds, startDate, endDate);
-        List<Object> valueList = formDateAndValueList(dateList, timePeriodApiCallList, apiDetailParam.getType());
-
-        apiDetailVo.setTime(dateList);
-        apiDetailVo.setValue(valueList);
-        return apiDetailVo;
-    }
-
-    private List<Object> formDateAndValueList(List<Long> dateList, List<ApiCallEntity> timePeriodApiCallList, String type) {
-        List<Object> valueList = new ArrayList<>();
-        Map<Date, Object> dateToApiCall = new HashMap<>();
-        if ("latency".equals(type)) {
-            dateToApiCall = timePeriodApiCallList.stream().collect(Collectors.toMap(ApiCallEntity::getCreateAt, ApiCallEntity::getLatency, (v1, v2) -> v1));
-        } else if ("visitTotalLine".equals(type)) {
-            dateToApiCall = timePeriodApiCallList.stream().collect(Collectors.toMap(ApiCallEntity::getCreateAt, ApiCallEntity::getResRows, (v1, v2) -> v1));
-        } else if ("responseTime".equals(type)) {
-            //速率和响应时间需要另外处理
-            for (ApiCallEntity apiCallEntity : timePeriodApiCallList) {
-                Date date = apiCallEntity.getCreateAt();
-                Date endDateOfTimeGap = new Date(date.getTime() + oneMinuteMillSeconds);
-                List<ApiCallEntity> timeGaApiCallList = timePeriodApiCallList.stream().filter(s -> s.getCreateAt().getTime() >= date.getTime() && s.getCreateAt().getTime() <= endDateOfTimeGap.getTime()).collect(Collectors.toList());
-                Double sumLatency = timeGaApiCallList.stream().mapToDouble(ApiCallEntity::getLatency).sum();
-                Double sumChildAmountResult = timeGaApiCallList.stream().mapToDouble(ApiCallEntity::getResRows).sum();
-                Double responseTime = 0d;
-                if (sumLatency > 0) {
-                    responseTime = sumLatency / sumChildAmountResult;
-                }
-                dateToApiCall.put(date, responseTime);
-
-            }
-        } else if ("speed".equals(type)) {
-            //速率和响应时间需要另外处理
-            for (ApiCallEntity apiCallEntity : timePeriodApiCallList) {
-                Date date = apiCallEntity.getCreateAt();
-                Date endDateOfTimeGap = new Date(date.getTime() + oneMinuteMillSeconds);
-                List<ApiCallEntity> timeGaApiCallList = timePeriodApiCallList.stream().filter(s -> s.getCreateAt().getTime() >= date.getTime() && s.getCreateAt().getTime() <= endDateOfTimeGap.getTime()).collect(Collectors.toList());
-                Double sumLatency = timeGaApiCallList.stream().mapToDouble(ApiCallEntity::getLatency).sum();
-                Double sumChildAmountResult = timeGaApiCallList.stream().mapToDouble(ApiCallEntity::getReqBytes).sum();
-                Double responseTime = 0d;
-                if (sumLatency > 0) {
-                    //毫秒为单位，要转换成秒
-                    responseTime = sumChildAmountResult / (sumLatency / 1000);
-                }
-                dateToApiCall.put(date, responseTime);
-
-            }
-        }
-
-        List<Date> sampleDateList = getAllKeysAsList(dateToApiCall);
-        Collections.sort(sampleDateList, (o1, o2) -> {
-            return o1.compareTo(o2);//升序，前边加负号变为降序
-        });
-
-        int sampleIndex = 0;
-        for (int i = 0; i < dateList.size(); i++) {
-            Date date = new Date(dateList.get(i));
-            Object value = 0l;
-            for (int j = sampleIndex; j < sampleDateList.size(); j++) {
-                Date sampleDate = sampleDateList.get(j);
-                if (DateUtil.between(date, sampleDate, DateUnit.MS) <= oneMinuteMillSeconds) {
-                    value = dateToApiCall.get(sampleDate);
-                    sampleIndex = j + 1;
-                    break;
-                }
-            }
-            valueList.add(value);
-        }
-        return valueList;
-    }
-
-
-    private List<Date> getAllKeysAsList(Map<Date, Object> map) {
-        List<Date> keyList = new ArrayList<>();
-        Set<Date> sampleDateKeySet = map.keySet();
-        sampleDateKeySet.forEach(set -> {
-            keyList.add(set);
-        });
-        return keyList;
-    }
 
-    /**
-     * 固定返回五个点
-     *
-     * @param startMillSeconds
-     * @param nowMillSeconds
-     * @return
-     */
-    private List<Date> getDateList(Long startMillSeconds, Long nowMillSeconds, Integer guanluary) {
-        List<Date> dateList = new ArrayList();
-        Integer pointCount = 1;
-        while (startMillSeconds <= nowMillSeconds && pointCount <= guanluary) {
-            dateList.add(new Date(startMillSeconds.longValue()));
-            startMillSeconds = startMillSeconds + oneMinuteMillSeconds;
-            pointCount++;
-        }
-        return dateList;
-    }
-
-
-    private void executeRankList(UserDetail userDetail, List<ModulesDto> modulesDtoList, List<ApiCallEntity> apiCallEntityList) {
-        log.info("计算失败率和响应时间");
-        if (CollectionUtils.isNotEmpty(modulesDtoList)) {
-            modulesDtoList = findAll(Query.query(Criteria.where("is_deleted").ne(true).and("user_id").is(userDetail.getUserId())));
-        }
-
-        List<String> moduleIdList = modulesDtoList.stream().map(ModulesDto::getId).map(ObjectId::toString).collect(Collectors.toList());
-
-        if (CollectionUtils.isNotEmpty(apiCallEntityList)) {
-            apiCallEntityList = apiCallService.findByModuleIds(moduleIdList);
-        }
-        Map<String, List<ApiCallEntity>> moduleIdToApiCallList = apiCallEntityList.stream().collect(Collectors.groupingBy(ApiCallEntity::getAllPathId));
-
-        Date oneHourAgo = DateUtil.offsetHour(new Date(), -24);
-        List<ApiCallEntity> oneHourApiCall = apiCallEntityList.stream().filter(item -> (item.getCreateAt().after(oneHourAgo))).collect(Collectors.toList());
-        Map<String, List<ApiCallEntity>> oneHourModuleIdToApiCall = oneHourApiCall.stream().collect(Collectors.groupingBy(ApiCallEntity::getAllPathId));
-
-
-        BulkOperations bulkOperations = repository.bulkOperations(BulkOperations.BulkMode.UNORDERED);
-        int exeNum = 0;
-        for (ModulesDto modulesDto : modulesDtoList) {
-            String moduleId = modulesDto.getId().toString();
-            List<ApiCallEntity> apiCallList = moduleIdToApiCallList.getOrDefault(moduleId, Collections.emptyList());
-            Map<String, List<ApiCallEntity>> prodMap = apiCallList.stream().collect(Collectors.groupingBy(item -> {
-                if ("200".equals(item.getCode())) {
-                    return "normal";
-                } else {
-                    return "warning";
-                }
-            }));
-            List<ApiCallEntity> warningApiCall = prodMap.getOrDefault("warning", Collections.emptyList());
-            double failRate = 0d;
-            if (CollectionUtils.isNotEmpty(warningApiCall) && CollectionUtils.isNotEmpty(apiCallList)) {
-                failRate = (double) warningApiCall.size() / (double) apiCallList.size();
-                failRate = new BigDecimal(failRate).setScale(2, RoundingMode.HALF_UP).doubleValue();
-            }
-
-            Update update = new Update().set("failRate", failRate);
-
-            if (CollectionUtils.isNotEmpty(oneHourModuleIdToApiCall.get(moduleId))) {
-                Number sumResRows = oneHourModuleIdToApiCall.get(moduleId).stream().filter(item -> null != item.getResRows()).collect(Collectors.toList()).stream().mapToDouble(ApiCallEntity::getResRows).sum();
-                Number sumLatency = oneHourModuleIdToApiCall.get(moduleId).stream().filter(item -> null != item.getReqBytes()).collect(Collectors.toList()).stream().mapToDouble(ApiCallEntity::getReqBytes).sum();
-                if (sumResRows.longValue() > 0) {
-                    update.set("responseTime", (sumLatency.longValue() / sumResRows.longValue()));
-                }
-
-            }
-            Query query = new Query(Criteria.where("_id").is(modulesDto.getId()));
-            bulkOperations.updateOne(query, update);
-            if (++exeNum > 1000) {
-                bulkOperations.execute();
-                exeNum = 0;
-            }
-        }
-        if (exeNum > 0) {
-            bulkOperations.execute();
-        }
-    }
-
-    public List<ModulesDto> findByConnectionId(String connectionId) {
-        List<ModulesDto> modulesDtoList = findAll(Query.query(Criteria.where("connection").is(MongoUtils.toObjectId(connectionId)).and("is_deleted").ne(true)));
-        return modulesDtoList;
-    }
+	public Page<ApiListVo> apiList(Filter filter, UserDetail userDetail) {
+		Page<ApiListVo> page = new Page<>(0L, new ArrayList<>());
+		String apiCallStatsCollectionName;
+		try {
+			apiCallStatsCollectionName = EntityUtils.documentAnnotationValue(ApiCallStatsEntity.class);
+		} catch (Exception e) {
+			throw new BizException("Get ApiCallStatsEntity's collection name failed", e);
+		}
+		MongoCollection<Document> apiCallStatsCollection = repository.getMongoOperations().getCollection(apiCallStatsCollectionName);
+		List<Document> pipeline = new ArrayList<>();
+		pipeline.add(new Document("$project", new Document("moduleId", new Document("$toObjectId", "$moduleId"))
+				.append("responseDataRowTotalCount", 1)
+				.append("callTotalCount", 1)
+				.append("transferDataTotalBytes", 1)
+				.append("clientIds", 1)
+		));
+		pipeline.add(new Document("$lookup", new Document("from", "Modules")
+				.append("let", new Document("moduleId", "$moduleId"))
+				.append("pipeline", Arrays.asList(
+						new Document("$project", new Document("_id", 1).append("name", 1).append("status", 1).append("user_id", 1).append("createTime", 1)),
+						new Document("$match", new Document("$expr", new Document("$eq", Arrays.asList("$_id", "$$moduleId"))))
+				))
+				.append("as", "modules")
+		));
+		pipeline.add(new Document("$unwind", new Document("path", "$modules")));
+		Where where = filter.getWhere();
+		Document match = new Document();
+		if (!userDetail.isRoot()) {
+			match.append("modules.user_id", userDetail.getUserId());
+		}
+		where.forEach((k, v) -> {
+			if ("clientId".equals(k)) {
+				match.append("clientIds", v);
+			} else {
+				match.append("modules." + k, v);
+			}
+		});
+		if (!match.isEmpty()) {
+			pipeline.add(new Document("$match", match));
+		}
+		List<Document> itemPipeline = new ArrayList<>();
+		Object order = filter.getOrder();
+		Document sortDoc = new Document();
+		if (order instanceof String) {
+			String[] split1 = ((String) order).split(",");
+			for (String s : split1) {
+				String[] split2 = s.trim().split(" ");
+				if (split2.length != 2) {
+					continue;
+				}
+				sortDoc.put("modules." + split2[0], "ASC".equals(split2[1]) ? 1 : -1);
+			}
+		}
+		if (!sortDoc.isEmpty()) {
+			itemPipeline.add(new Document("$sort", sortDoc));
+		}
+		int skip = filter.getSkip();
+		if (skip > 0) {
+			itemPipeline.add(new Document("$skip", skip));
+		}
+		int limit = filter.getLimit();
+		if (limit > 0) {
+			itemPipeline.add(new Document("$limit", limit));
+		}
+		Document facet = new Document("total", Arrays.asList(new Document("$group", new Document("_id", null).append("total", new Document("$sum", 1)))))
+				.append("items", itemPipeline);
+		pipeline.add(new Document("$facet", facet));
+		long startMs = System.currentTimeMillis();
+		try (
+				MongoCursor<Document> iterator = apiCallStatsCollection.aggregate(pipeline).allowDiskUse(true).iterator()
+		) {
+			if (iterator.hasNext()) {
+				Document doc = iterator.next();
+				long cost = System.currentTimeMillis() - startMs;
+				if (log.isDebugEnabled()) {
+					StringBuilder pipelineStr = new StringBuilder();
+					pipeline.forEach(p -> pipelineStr.append(p.toJson()).append(System.lineSeparator()));
+					log.debug("Execute aggregate on ApiCallStats collection, pipeline: {}, cost: {} ms, result: {}", pipelineStr, cost, doc.toJson());
+				}
+				List<?> tempList;
+				Object total = doc.get("total");
+				if (total instanceof List) {
+					tempList = (List<?>) total;
+					if (!tempList.isEmpty()) {
+						page.setTotal(getLong((Document) tempList.get(0), "total"));
+					}
+				}
+				Object items = doc.get("items");
+				if (items instanceof List) {
+					tempList = (List<?>) items;
+					if (!tempList.isEmpty()) {
+						for (Object o : tempList) {
+							if (!(o instanceof Document)) {
+								continue;
+							}
+							Document item = (Document) o;
+							Object modules = item.get("modules");
+							if (!(modules instanceof Document)) {
+								continue;
+							}
+							ApiListVo apiListVo = new ApiListVo();
+							apiListVo.setId(((Document) modules).getObjectId("_id").toString());
+							apiListVo.setName(((Document)modules).getString("name"));
+							apiListVo.setStatus(((Document) modules).getString("status"));
+							apiListVo.setVisitLine(getLong(item, "responseDataRowTotalCount"));
+							apiListVo.setVisitCount(getLong(item, "callTotalCount"));
+							apiListVo.setTransitQuantity(getLong(item, "transferDataTotalBytes"));
+							page.getItems().add(apiListVo);
+						}
+					}
+				}
+			}
+		}
+		return page;
+	}
 
+	/**
+	 * 时间间隔  （可选5分钟、10分钟、30分钟、60分钟））
+	 * "type":    "visitTotalLine",       可选值：visitTotalLine (访问行数),
+	 * timeConsuming（耗时），
+	 * speed（传输速率）
+	 * latency（响应时间）
+	 *
+	 * @param apiDetailParam
+	 * @return
+	 */
+	public ApiDetailVo apiDetail(ApiDetailParam apiDetailParam) {
+		ApiDetailVo apiDetailVo = new ApiDetailVo();
+		ApiCallStatsDto apiCallStatsDto = apiCallStatsService.findOne(Query.query(Criteria.where("moduleId").is(apiDetailParam.getId())));
+		if (null != apiCallStatsDto) {
+			apiDetailVo.setVisitTotalCount(apiCallStatsDto.getCallTotalCount());
+			//传输的数据量
+			apiDetailVo.setVisitQuantity(apiCallStatsDto.getTransferDataTotalBytes());
+			// 总的响应时间
+			Long totalLatency = apiCallStatsDto.getTotalResponseTime();
+			// 传输速率
+			if (totalLatency > 0L) {
+				apiDetailVo.setSpeed(BigDecimal.valueOf(apiCallStatsDto.getTransferDataTotalBytes()).divide(BigDecimal.valueOf(totalLatency), 2, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(1000)).doubleValue());
+			}
+			Query lastApiCallQuery = Query.query(Criteria.where("allPathId").is(apiDetailParam.getId())).with(Sort.by("_id").descending()).limit(1);
+			lastApiCallQuery.fields().include("res_rows", "latency");
+			ApiCallEntity lastApiCall = apiCallService.findOne(lastApiCallQuery);
+			if (null != lastApiCall && null != lastApiCall.getResRows() && lastApiCall.getResRows() > 0) {
+				//最新一次的响应时间
+				apiDetailVo.setResponseTime(BigDecimal.valueOf(lastApiCall.getLatency()).divide(BigDecimal.valueOf(lastApiCall.getResRows()), 2, RoundingMode.HALF_UP).doubleValue());
+				//最新一次的的耗时
+				apiDetailVo.setTimeConsuming(lastApiCall.getLatency());
+			}
+			// 总行数
+			apiDetailVo.setVisitTotalLine(apiCallStatsDto.getResponseDataRowTotalCount());
+			//查询客户端
+			Set<String> clientIds = apiCallStatsDto.getClientIds();
+			Query applicationQuery = Query.query(Criteria.where("id").in(clientIds));
+			applicationQuery.fields().include("id", "name");
+			List<ApplicationDto> applicationDtoList = applicationService.findAll(applicationQuery);
+			applicationDtoList.forEach(applicationDto -> {
+				Map<String, String> clientNameMap = new HashMap<>();
+				clientNameMap.put("id", applicationDto.getId().toString());
+				clientNameMap.put("name", applicationDto.getName());
+				apiDetailVo.getClientNameList().add(clientNameMap);
+			});
+		}
 
-    /**
-     * 生成,compare update function，just add check function
-     * @param modulesDto
-     * @param userDetail
-     * @return
-     */
-    public ModulesDto generate(ModulesDto modulesDto, UserDetail userDetail) {
-        if (findByName(modulesDto.getName()).size() > 1) {
-            throw new BizException("Modules.Name.Existed");
-        }
-        if (isBasePathAndVersionRepeat(modulesDto.getBasePath(), modulesDto.getApiVersion()).size() > 1) {
-            throw new BizException("Modules.BasePathAndVersion.Existed");
-        }
-        checkoutInputParamIsValid(modulesDto);
-        modulesDto.setStatus(ModuleStatusEnum.PENDING.getValue());
-        Where where = new Where();
-        where.put("id", modulesDto.getId().toString());
-        return super.upsertByWhere(where, modulesDto, userDetail);
-    }
-
-    private void checkoutInputParamIsValid(ModulesDto modulesDto) {
-        String apiType = modulesDto.getApiType();
-        List<Path> paths = modulesDto.getPaths();
-        if(StringUtils.isBlank(modulesDto.getDataSource())) throw new BizException("datasource can't be null");
-        if(StringUtils.isBlank(modulesDto.getTableName())) throw new BizException("tableName can't be null");
-        if(StringUtils.isBlank(modulesDto.getApiType())) throw new BizException("apiType can't be null");
-        if(StringUtils.isBlank(modulesDto.getConnectionId())) throw new BizException("connectionId can't be null");
-        if(StringUtils.isBlank(modulesDto.getOperationType())) throw new BizException("operationType can't be null");
-        if(StringUtils.isBlank(modulesDto.getConnectionType())) throw new BizException("connectionType can't be null");
-        if(StringUtils.isBlank(modulesDto.getConnectionName())) throw new BizException("connectionName can't be null");
-        if (CollectionUtils.isNotEmpty(paths)) {
-            for (Path path : paths) {
-                //base param can't be null
-                List<Param> params = path.getParams();
-                if(CollectionUtils.isEmpty(params))
-                    throw new BizException("params can't be null");
-                Map<String, List<Param>> paramMap = params.stream().collect(Collectors.groupingBy(Param::getName));
-                List<Param> pages = paramMap.get("page");
-                List<Param> limits = paramMap.get("limit");
-                List<Param> filters = paramMap.get("filter");
-                if (ApiTypeEnum.DEFAULT_API.getValue().equals(apiType) || ApiTypeEnum.CUSTOMER_QUERY.getValue().equals(apiType)) {
-                    if (CollectionUtils.isEmpty(pages))
-                        throw new BizException("paths's page can't be null");
-                    if (CollectionUtils.isEmpty(limits))
-                        throw new BizException("paths's limit can't be null");
-                    if (ApiTypeEnum.DEFAULT_API.getValue().equals(apiType) && CollectionUtils.isEmpty(filters))
-                        throw new BizException("paths's filter can't be null");
-                }
-                //input params type checkout
-                for (Param param : params) {
-                    if(ApiTypeEnum.DEFAULT_API.getValue().equals(apiType) && StringUtils.isNotBlank(param.getType()) && "object".equalsIgnoreCase(param.getType().trim()))
-                        continue;
-                    if (!ParamTypeEnum.isValid(param.getType(), param.getDefaultvalue()))
-                        throw new BizException(param.getName() + " is invalid");
-                }
-            }
-        }
-    }
+		Integer guanluary = apiDetailParam.getGuanluary();
+
+		ZonedDateTime end = ZonedDateTime.now().withSecond(0).withNano(0);
+		ZonedDateTime start = end.minusMinutes(guanluary).withSecond(0).withNano(0);
+		Date startDate = Date.from(start.toInstant());
+		Date endDate = Date.from(end.toInstant());
+		Query query = Query.query(Criteria.where("moduleId").is(apiDetailParam.getId())
+						.andOperator(
+								Criteria.where("apiCallTime").gte(startDate),
+								Criteria.where("apiCallTime").lte(endDate)
+						))
+				.with(Sort.by("apiCallTime").ascending());
+		List<ApiCallMinuteStatsDto> apiCallMinuteStatsDtoList = apiCallMinuteStatsService.findAll(query);
+		IntStream.range(0, guanluary).forEach(i -> {
+			long pointTime = startDate.getTime() + i * oneMinuteMillSeconds;
+			apiDetailVo.getTime().add(pointTime);
+			ApiCallMinuteStatsDto pointStatsDto = apiCallMinuteStatsDtoList.stream().filter(apiCallMinuteStatsDto -> apiCallMinuteStatsDto.getApiCallTime().getTime() == pointTime).findFirst().orElse(null);
+			if (null != pointStatsDto) {
+				apiDetailVo.getValue().add(pickValue(apiDetailParam, pointStatsDto));
+			} else {
+				apiDetailVo.getValue().add(0);
+			}
+		});
+		return apiDetailVo;
+	}
+
+	public Object pickValue(ApiDetailParam apiDetailParam, ApiCallMinuteStatsDto apiCallMinuteStatsDto) {
+		Object value = 0;
+		if ("latency".equals(apiDetailParam.getType())) {
+			value = apiCallMinuteStatsDto.getTotalResponseTime();
+		} else if ("visitTotalLine".equals(apiDetailParam.getType())) {
+			value = apiCallMinuteStatsDto.getResponseDataRowTotalCount();
+		} else if ("responseTime".equals(apiDetailParam.getType())) {
+			value = apiCallMinuteStatsDto.getResponseTimePerRow();
+		} else if ("speed".equals(apiDetailParam.getType())) {
+			value = apiCallMinuteStatsDto.getTransferBytePerSecond();
+		}
+		return value;
+	}
+
+	public List<ModulesDto> findByConnectionId(String connectionId) {
+		List<ModulesDto> modulesDtoList = findAll(Query.query(Criteria.where("connection").is(MongoUtils.toObjectId(connectionId)).and("is_deleted").ne(true)));
+		return modulesDtoList;
+	}
+
+
+	/**
+	 * 生成,compare update function，just add check function
+	 *
+	 * @param modulesDto
+	 * @param userDetail
+	 * @return
+	 */
+	public ModulesDto generate(ModulesDto modulesDto, UserDetail userDetail) {
+		if (findByName(modulesDto.getName()).size() > 1) {
+			throw new BizException("Modules.Name.Existed");
+		}
+		if (isBasePathAndVersionRepeat(modulesDto.getBasePath(), modulesDto.getApiVersion()).size() > 1) {
+			throw new BizException("Modules.BasePathAndVersion.Existed");
+		}
+		checkoutInputParamIsValid(modulesDto);
+		modulesDto.setStatus(ModuleStatusEnum.PENDING.getValue());
+		Where where = new Where();
+		where.put("id", modulesDto.getId().toString());
+		return super.upsertByWhere(where, modulesDto, userDetail);
+	}
+
+	private void checkoutInputParamIsValid(ModulesDto modulesDto) {
+		String apiType = modulesDto.getApiType();
+		List<Path> paths = modulesDto.getPaths();
+		if (StringUtils.isBlank(modulesDto.getDataSource())) throw new BizException("datasource can't be null");
+		if (StringUtils.isBlank(modulesDto.getTableName())) throw new BizException("tableName can't be null");
+		if (StringUtils.isBlank(modulesDto.getApiType())) throw new BizException("apiType can't be null");
+		if (StringUtils.isBlank(modulesDto.getConnectionId())) throw new BizException("connectionId can't be null");
+		if (StringUtils.isBlank(modulesDto.getOperationType())) throw new BizException("operationType can't be null");
+		if (StringUtils.isBlank(modulesDto.getConnectionType())) throw new BizException("connectionType can't be null");
+		if (StringUtils.isBlank(modulesDto.getConnectionName())) throw new BizException("connectionName can't be null");
+		if (CollectionUtils.isNotEmpty(paths)) {
+			for (Path path : paths) {
+				//base param can't be null
+				List<Param> params = path.getParams();
+				if (CollectionUtils.isEmpty(params))
+					throw new BizException("params can't be null");
+				Map<String, List<Param>> paramMap = params.stream().collect(Collectors.groupingBy(Param::getName));
+				List<Param> pages = paramMap.get("page");
+				List<Param> limits = paramMap.get("limit");
+				List<Param> filters = paramMap.get("filter");
+				if (ApiTypeEnum.DEFAULT_API.getValue().equals(apiType) || ApiTypeEnum.CUSTOMER_QUERY.getValue().equals(apiType)) {
+					if (CollectionUtils.isEmpty(pages))
+						throw new BizException("paths's page can't be null");
+					if (CollectionUtils.isEmpty(limits))
+						throw new BizException("paths's limit can't be null");
+					if (ApiTypeEnum.DEFAULT_API.getValue().equals(apiType) && CollectionUtils.isEmpty(filters))
+						throw new BizException("paths's filter can't be null");
+				}
+				//input params type checkout
+				for (Param param : params) {
+					if (ApiTypeEnum.DEFAULT_API.getValue().equals(apiType) && StringUtils.isNotBlank(param.getType()) && "object".equalsIgnoreCase(param.getType().trim()))
+						continue;
+					if (!ParamTypeEnum.isValid(param.getType(), param.getDefaultvalue()))
+						throw new BizException(param.getName() + " is invalid");
+				}
+			}
+		}
+	}
 
 	public void batchLoadTask(HttpServletResponse response, List<String> ids, UserDetail user) {
 
@@ -1260,7 +1189,7 @@ public void batchLoadTask(HttpServletResponse response, List<String> ids, UserDe
 				dataSourceConnectionDto.setListtags(null);
 				String databaseQualifiedName = MetaDataBuilderUtils.generateQualifiedName("database", dataSourceConnectionDto, null);
 				MetadataInstancesDto dataSourceMetadataInstance = metadataInstancesService.findOne(
-								Query.query(Criteria.where("qualified_name").is(databaseQualifiedName).and("is_deleted").ne(true)), user);
+						Query.query(Criteria.where("qualified_name").is(databaseQualifiedName).and("is_deleted").ne(true)), user);
 				jsonList.add(new TaskUpAndLoadDto("MetadataInstances", JsonUtil.toJsonUseJackson(dataSourceMetadataInstance)));
 				jsonList.add(new TaskUpAndLoadDto("Connections", JsonUtil.toJsonUseJackson(dataSourceConnectionDto)));
 			}
@@ -1270,8 +1199,8 @@ public void batchLoadTask(HttpServletResponse response, List<String> ids, UserDe
 		AtomicReference<String> fileName = new AtomicReference<>("");
 		String yyyymmdd = DateUtil.today().replaceAll("-", "");
 		FunctionUtils.isTureOrFalse(ids.size() > 1).trueOrFalseHandle(
-						() -> fileName.set("module_batch" + "-" + yyyymmdd),
-						() -> fileName.set(modulesDtoMap.get(ids.get(0)).getName() + "-" + yyyymmdd)
+				() -> fileName.set("module_batch" + "-" + yyyymmdd),
+				() -> fileName.set(modulesDtoMap.get(ids.get(0)).getName() + "-" + yyyymmdd)
 		);
 		fileService.viewImg1(json, response, fileName.get() + ".json.gz");
 	}
@@ -1307,10 +1236,10 @@ public void batchUpTask(MultipartFile multipartFile, UserDetail user, boolean co
 					if ("MetadataInstances".equals(modulesUpAndLoadDto.getCollectionName())) {
 						metadataInstancess.add(JsonUtil.parseJsonUseJackson(dtoJson, MetadataInstancesDto.class));
 					} else if ("Modules".equals(modulesUpAndLoadDto.getCollectionName())) {
-              ModulesDto modulesDto = JsonUtil.parseJsonUseJackson(dtoJson, ModulesDto.class);
-              checkoutInputParamIsValid(modulesDto);
-              modulesDto.setConnection(MongoUtils.toObjectId(modulesDto.getConnectionId()));
-              modulesDtos.add(modulesDto);
+						ModulesDto modulesDto = JsonUtil.parseJsonUseJackson(dtoJson, ModulesDto.class);
+						checkoutInputParamIsValid(modulesDto);
+						modulesDto.setConnection(MongoUtils.toObjectId(modulesDto.getConnectionId()));
+						modulesDtos.add(modulesDto);
 					} else if ("Connections".equals(modulesUpAndLoadDto.getCollectionName())) {
 						connections.add(JsonUtil.parseJsonUseJackson(dtoJson, DataSourceConnectionDto.class));
 					}
@@ -1350,7 +1279,7 @@ private void batchImport(List<ModulesDto> modulesDtos, UserDetail user, boolean
 			ModulesDto one = findOne(query, user);
 
 			modulesDto.setIsDeleted(false);
-      modulesDto.setStatus(ModuleStatusEnum.PENDING.getValue());
+			modulesDto.setStatus(ModuleStatusEnum.PENDING.getValue());
 
 			if (one == null) {
 				ModulesDto one1 = findOne(new Query(Criteria.where("_id").is(modulesDto.getId()).and("is_deleted").ne(true)));
@@ -1399,77 +1328,77 @@ public List<ModulesDto> findAllModulesByIds(List<String> list) {
 		return CglibUtil.copyList(entityList, ModulesDto::new);
 	}
 
-    public void updateOutParameter(String id, DiscoveryFieldDto discoveryFieldDto, UserDetail userDetail){
-        if(org.springframework.util.StringUtils.isEmpty(id)){
-            throw new BizException("IllegalArgument", "Id");
-        }
-        Criteria criteria = Criteria.where("_id").is(MongoUtils.toObjectId(id));
-        Query query = new Query(criteria);
-        Update update = new Update().set("paths.$[].fields.$[id1].description",discoveryFieldDto.getBusinessDesc())
-                .filterArray("id1.id",discoveryFieldDto.getId());
-        update(query,update, userDetail);
-    }
-
-    public void updateIntParameter(String id, Param param, UserDetail userDetail){
-        if(org.springframework.util.StringUtils.isEmpty(id)){
-            throw new BizException("IllegalArgument", "Id");
-        }
-        Criteria criteria = Criteria.where("_id").is(MongoUtils.toObjectId(id));
-        Query query = new Query(criteria);
-        Update update = new Update().set("paths.$[].params.$[id1].description",param.getDescription())
-                .filterArray("id1.name",param.getName());
-        update(query,update, userDetail);
-    }
-
-    public void saveWord(HttpServletResponse response, List<String> ids,String ip,UserDetail user){
-        if (CollectionUtils.isEmpty(ids)||StringUtils.isBlank(ip)){
-            return;
-        }
-        List<ModulesDto> allModules = findAllModulesByIds(ids);
-       Map<String,List<ModulesDto>> modules = allModules.stream().collect(Collectors.groupingBy(modulesDto -> {
-            return modulesDto.getListtags().get(0).getValue();
-        }));
-       ApiView apiView =ApiViewUtil.convert(modules,ip);
-       apiView.getApiTypeList();
-        InputStream is = this.getClass().getClassLoader().getResourceAsStream("template/testTemplate.docx");
-        if(is==null){
-            log.error("未找到模板文件");
-        }
-        LoopRowTableRenderPolicy hackLoopTableRenderPolicy = new LoopRowTableRenderPolicy();
-        Configure config=Configure.builder()
-                .bind("params",hackLoopTableRenderPolicy)
-                .bind("fields",hackLoopTableRenderPolicy)
-                .bind("TOC",new TOCRenderPolicy())
-                .bind("code",new HighlightRenderPolicy())
-                .useSpringEL()
-                .build();
-        XWPFTemplate template =null;
-        try{
-             template=XWPFTemplate.compile(is,config)
-                    .render(apiView);
-        }catch (Exception e){
-            log.error("render template error{}",e);
-        }
-        AtomicReference<String> fileName = new AtomicReference<>("");
-        String yyyymmdd = DateUtil.today().replaceAll("-", "");
-        FunctionUtils.isTureOrFalse(ids.size() > 1).trueOrFalseHandle(
-                () -> fileName.set("api_word" + "-" + yyyymmdd),
-                () -> fileName.set(allModules.get(0).getName() + "-" + yyyymmdd)
-        );
-        fileService.viewWord(template,response,fileName.get());
-    }
-
-    public void updatePermissions(ModulesPermissionsDto permissions,UserDetail userDetail){
-        if(CollectionUtils.isEmpty(permissions.getAcl()))throw new BizException("Modules.Permission.Scope.Null");
-        Update update =  new Update();
-        update.set("paths.$[].acl",permissions.getAcl());
-        updateById(permissions.getModuleId(), update, userDetail);
-    }
-
-    public void updateTags(ModulesTagsDto modulesTagsDto,UserDetail userDetail){
-        if(CollectionUtils.isEmpty(modulesTagsDto.getListtags()))throw new BizException("Modules.Tags.Null");
-        Update update =  new Update();
-        update.set("listtags",modulesTagsDto.getListtags());
-        updateById(modulesTagsDto.getModuleId(), update, userDetail);
-    }
+	public void updateOutParameter(String id, DiscoveryFieldDto discoveryFieldDto, UserDetail userDetail) {
+		if (org.springframework.util.StringUtils.isEmpty(id)) {
+			throw new BizException("IllegalArgument", "Id");
+		}
+		Criteria criteria = Criteria.where("_id").is(MongoUtils.toObjectId(id));
+		Query query = new Query(criteria);
+		Update update = new Update().set("paths.$[].fields.$[id1].description", discoveryFieldDto.getBusinessDesc())
+				.filterArray("id1.id", discoveryFieldDto.getId());
+		update(query, update, userDetail);
+	}
+
+	public void updateIntParameter(String id, Param param, UserDetail userDetail) {
+		if (org.springframework.util.StringUtils.isEmpty(id)) {
+			throw new BizException("IllegalArgument", "Id");
+		}
+		Criteria criteria = Criteria.where("_id").is(MongoUtils.toObjectId(id));
+		Query query = new Query(criteria);
+		Update update = new Update().set("paths.$[].params.$[id1].description", param.getDescription())
+				.filterArray("id1.name", param.getName());
+		update(query, update, userDetail);
+	}
+
+	public void saveWord(HttpServletResponse response, List<String> ids, String ip, UserDetail user) {
+		if (CollectionUtils.isEmpty(ids) || StringUtils.isBlank(ip)) {
+			return;
+		}
+		List<ModulesDto> allModules = findAllModulesByIds(ids);
+		Map<String, List<ModulesDto>> modules = allModules.stream().collect(Collectors.groupingBy(modulesDto -> {
+			return modulesDto.getListtags().get(0).getValue();
+		}));
+		ApiView apiView = ApiViewUtil.convert(modules, ip);
+		apiView.getApiTypeList();
+		InputStream is = this.getClass().getClassLoader().getResourceAsStream("template/testTemplate.docx");
+		if (is == null) {
+			log.error("未找到模板文件");
+		}
+		LoopRowTableRenderPolicy hackLoopTableRenderPolicy = new LoopRowTableRenderPolicy();
+		Configure config = Configure.builder()
+				.bind("params", hackLoopTableRenderPolicy)
+				.bind("fields", hackLoopTableRenderPolicy)
+				.bind("TOC", new TOCRenderPolicy())
+				.bind("code", new HighlightRenderPolicy())
+				.useSpringEL()
+				.build();
+		XWPFTemplate template = null;
+		try {
+			template = XWPFTemplate.compile(is, config)
+					.render(apiView);
+		} catch (Exception e) {
+			log.error("render template error{}", e);
+		}
+		AtomicReference<String> fileName = new AtomicReference<>("");
+		String yyyymmdd = DateUtil.today().replaceAll("-", "");
+		FunctionUtils.isTureOrFalse(ids.size() > 1).trueOrFalseHandle(
+				() -> fileName.set("api_word" + "-" + yyyymmdd),
+				() -> fileName.set(allModules.get(0).getName() + "-" + yyyymmdd)
+		);
+		fileService.viewWord(template, response, fileName.get());
+	}
+
+	public void updatePermissions(ModulesPermissionsDto permissions, UserDetail userDetail) {
+		if (CollectionUtils.isEmpty(permissions.getAcl())) throw new BizException("Modules.Permission.Scope.Null");
+		Update update = new Update();
+		update.set("paths.$[].acl", permissions.getAcl());
+		updateById(permissions.getModuleId(), update, userDetail);
+	}
+
+	public void updateTags(ModulesTagsDto modulesTagsDto, UserDetail userDetail) {
+		if (CollectionUtils.isEmpty(modulesTagsDto.getListtags())) throw new BizException("Modules.Tags.Null");
+		Update update = new Update();
+		update.set("listtags", modulesTagsDto.getListtags());
+		updateById(modulesTagsDto.getModuleId(), update, userDetail);
+	}
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/modules/vo/ApiDetailVo.java b/manager/tm/src/main/java/com/tapdata/tm/modules/vo/ApiDetailVo.java
index dcf4d6290a..af98fa4db2 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/modules/vo/ApiDetailVo.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/modules/vo/ApiDetailVo.java
@@ -2,22 +2,23 @@
 
 import lombok.Data;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
 @Data
 public class ApiDetailVo {
-    private Long visitTotalCount;
-    private Long errorVisitTotalCount;
-    private Long visitQuantity;
-    private Long timeConsuming;
-    private Long visitTotalLine;
+    private Long visitTotalCount = 0L;
+    private Long errorVisitTotalCount = 0L;
+    private Long visitQuantity = 0L;
+    private Long timeConsuming = 0L;
+    private Long visitTotalLine = 0L;
 //    api  最新一次调用耗费了多少时间   /  传输了多少条数据
-    private Long responseTime;
-    private Long speed;
+    private Double responseTime = 0D;
+    private Double speed = 0D;
 
-    private List<Long> time;
-    private List<Object> value;
+    private List<Long> time = new ArrayList<>();
+    private List<Object> value = new ArrayList<>();
 
-    private List<Map<String,String>> clientNameList;
+    private List<Map<String,String>> clientNameList = new ArrayList<>();
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/modules/vo/PreviewVo.java b/manager/tm/src/main/java/com/tapdata/tm/modules/vo/PreviewVo.java
index 703b42139a..320294855d 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/modules/vo/PreviewVo.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/modules/vo/PreviewVo.java
@@ -4,15 +4,15 @@
 
 @Data
 public class PreviewVo {
-    private Long totalCount;
+    private Long totalCount = 0L;
     //总共访问次数
-    private Long visitTotalCount;
+    private Long visitTotalCount = 0L;
 
     //告警的访问次数
-    private Long warningVisitTotalCount;
-
-    private Long visitTotalLine;
-    private Long transmitTotal;
-    private Long warningApiCount;
+    private Long warningVisitTotalCount = 0L;
 
+    private Long visitTotalLine = 0L;
+    private Long transmitTotal = 0L;
+    private Long warningApiCount = 0L;
+    private Long lastUpdAt = 0L;
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/modules/vo/RankListsVo.java b/manager/tm/src/main/java/com/tapdata/tm/modules/vo/RankListsVo.java
index e79912f571..71cbec5bd4 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/modules/vo/RankListsVo.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/modules/vo/RankListsVo.java
@@ -8,7 +8,7 @@
 @Data
 public class RankListsVo {
     //失败率排行
-    List<Map> items;
+    List<Map<String, Object>> items;
     Long total;
     Number totalPage;
 }
diff --git a/manager/tm/src/main/java/com/tapdata/tm/schedule/ApiCallMinuteStatsScheduler.java b/manager/tm/src/main/java/com/tapdata/tm/schedule/ApiCallMinuteStatsScheduler.java
new file mode 100644
index 0000000000..c0dbd14a7f
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/schedule/ApiCallMinuteStatsScheduler.java
@@ -0,0 +1,127 @@
+package com.tapdata.tm.schedule;
+
+import com.tapdata.tm.apiCalls.service.ApiCallService;
+import com.tapdata.tm.apicallminutestats.dto.ApiCallMinuteStatsDto;
+import com.tapdata.tm.apicallminutestats.service.ApiCallMinuteStatsService;
+import com.tapdata.tm.modules.dto.ModulesDto;
+import com.tapdata.tm.modules.service.ModulesService;
+import lombok.extern.slf4j.Slf4j;
+import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
+import org.apache.commons.lang3.StringUtils;
+import org.bson.types.ObjectId;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author samuel
+ * @Description
+ * @create 2024-08-27 10:30
+ **/
+@Component
+@Slf4j
+public class ApiCallMinuteStatsScheduler {
+
+	private final ModulesService modulesService;
+	private final ApiCallMinuteStatsService apiCallMinuteStatsService;
+	private final ApiCallService apiCallService;
+
+	@Autowired
+	public ApiCallMinuteStatsScheduler(ModulesService modulesService, ApiCallMinuteStatsService apiCallMinuteStatsService, ApiCallService apiCallService) {
+		this.modulesService = modulesService;
+		this.apiCallMinuteStatsService = apiCallMinuteStatsService;
+		this.apiCallService = apiCallService;
+	}
+
+	/**
+	 * Scheduled task to aggregate the API call data of each module and save it to ApiCallMinuteStats
+	 */
+	@Scheduled(cron = "0 0/1 * * * ?")
+	@SchedulerLock(name = "api_call_minute_stats_scheduler", lockAtMostFor = "30m", lockAtLeastFor = "5s")
+	public void schedule() {
+		Thread.currentThread().setName(getClass().getSimpleName() + "-scheduler");
+		if (log.isDebugEnabled()) {
+			log.debug("Start to aggregate ApiCallMinuteStats...");
+		}
+
+		// Get all Modules, excluding deleted ones
+		Query modulesQuery = new Query(Criteria.where("is_deleted").ne(true));
+		modulesQuery.fields().include("id", "user_id");
+		List<ModulesDto> modulesList = modulesService.findAll(modulesQuery);
+		if (log.isDebugEnabled()) {
+			log.debug("Found all modules size: {}, include fields: {}", modulesList.size(), modulesQuery.getFieldsObject().toJson());
+		}
+
+		// Traverse all Modules, perform pre-aggregation, and save to ApiCallMinuteStats
+		int traverseStep = 0;
+		// Because the expiration time defined by the ttl index is 2 hours, only the data for the last 2 hours can be counted.
+		Instant startInstant = Instant.now().minus(2, ChronoUnit.HOURS);
+		for (ModulesDto modulesDto : modulesList) {
+			long loopStartMs = System.currentTimeMillis();
+			ObjectId moduleOid = modulesDto.getId();
+			if (null == moduleOid) {
+				continue;
+			}
+			String moduleId = moduleOid.toString();
+			traverseStep++;
+			// Get the historical ApiCallMinuteStats record based on moduleId, and get the lastApiCallId from it as this offset
+			Query apiCallStatsQuery = Query.query(Criteria.where("moduleId").is(moduleId)).with(Sort.by("_id").descending()).limit(1);
+			ApiCallMinuteStatsDto lastApiCallMinuteStatsDto = apiCallMinuteStatsService.findOne(apiCallStatsQuery);
+			String lastApiCallId = null;
+			if (null != lastApiCallMinuteStatsDto) {
+				lastApiCallId = StringUtils.isBlank(lastApiCallMinuteStatsDto.getLastApiCallId()) ? null : lastApiCallMinuteStatsDto.getLastApiCallId();
+			}
+			if (log.isDebugEnabled()) {
+				log.debug(" {} - Found exists ApiCallStatsDto based on filter: {}, lastApiCallId: {}, exists ApiCallStatsDto: {}", traverseStep, apiCallStatsQuery.getQueryObject().toJson(), lastApiCallId, lastApiCallMinuteStatsDto);
+			}
+
+			// Aggregate the ApiCall data of the current module, and wrap new ApiCallMinuteStats
+			List<ApiCallMinuteStatsDto> apiCallMinuteStatsDtoList;
+			try {
+				apiCallMinuteStatsDtoList = apiCallService.aggregateMinuteByAllPathId(moduleId, lastApiCallId, Date.from(startInstant));
+			} catch (Exception e) {
+				log.error("Aggregate ApiCallStatsDto failed, moduleId: {}, will skip it, error: {}", moduleId, e.getMessage(), e);
+				continue;
+			}
+
+			// Merge the new ApiCallMinuteStats with the historical ApiCallMinuteStats
+			try {
+				apiCallMinuteStatsService.merge(apiCallMinuteStatsDtoList);
+			} catch (Exception e) {
+				log.error("Merge ApiCallStatsDto failed, will skip it, error: {}", e.getMessage(), e);
+				continue;
+			}
+
+			// Save the merged ApiCallMinuteStats
+			apiCallMinuteStatsDtoList.forEach(dto -> {
+				dto.setUserId(modulesDto.getUserId());
+				dto.setLastUpdAt(new Date());
+				if (null == dto.getId()) {
+					dto.setId(new ObjectId());
+					dto.setCreateAt(new Date());
+				}
+			});
+			try {
+				apiCallMinuteStatsService.bulkWrite(apiCallMinuteStatsDtoList);
+				long loopCost = System.currentTimeMillis() - loopStartMs;
+				if (log.isDebugEnabled()) {
+					StringBuilder sb = new StringBuilder();
+					for (ApiCallMinuteStatsDto callMinuteStatsDto : apiCallMinuteStatsDtoList) {
+						sb.append(callMinuteStatsDto.toString()).append(System.lineSeparator());
+					}
+					log.info("Bulk write Api Call Minute Stats data completed, moduleId: {}, cost: {} ms, progress: {}/{}, data: {}", moduleId, loopCost, traverseStep, modulesList.size(), sb);
+				}
+			} catch (Exception e) {
+				log.error("BulkWrite ApiCallMinuteStatsDto failed, will skip it, error: {}", e.getMessage(), e);
+			}
+		}
+	}
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/schedule/ApiCallStatsScheduler.java b/manager/tm/src/main/java/com/tapdata/tm/schedule/ApiCallStatsScheduler.java
new file mode 100644
index 0000000000..49eb78ee7c
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/schedule/ApiCallStatsScheduler.java
@@ -0,0 +1,151 @@
+package com.tapdata.tm.schedule;
+
+import com.tapdata.tm.apiCalls.service.ApiCallService;
+import com.tapdata.tm.apicallstats.dto.ApiCallStatsDto;
+import com.tapdata.tm.apicallstats.service.ApiCallStatsService;
+import com.tapdata.tm.modules.dto.ModulesDto;
+import com.tapdata.tm.modules.service.ModulesService;
+import lombok.extern.slf4j.Slf4j;
+import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.bson.types.ObjectId;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @author samuel
+ * @Description
+ * @create 2024-08-27 10:30
+ **/
+@Component
+@Slf4j
+public class ApiCallStatsScheduler {
+
+	private final ModulesService modulesService;
+	private final ApiCallStatsService apiCallStatsService;
+	private final ApiCallService apiCallService;
+
+	@Autowired
+	public ApiCallStatsScheduler(ModulesService modulesService, ApiCallStatsService apiCallStatsService, ApiCallService apiCallService) {
+		this.modulesService = modulesService;
+		this.apiCallStatsService = apiCallStatsService;
+		this.apiCallService = apiCallService;
+	}
+
+	/**
+	 * Scheduled task to aggregate the API call data of each module and save it to ApiCallStats
+	 */
+	@Scheduled(cron = "0 0/5 * * * ?")
+	@SchedulerLock(name = "api_call_stats_scheduler", lockAtMostFor = "30m", lockAtLeastFor = "5s")
+	public void schedule() {
+		Thread.currentThread().setName(getClass().getSimpleName() + "-scheduler");
+		if (log.isDebugEnabled()) {
+			log.debug("Start to aggregate ApiCallStats...");
+		}
+		long startMs = System.currentTimeMillis();
+
+		// Get all Modules, excluding deleted ones
+		Query modulesQuery = new Query();
+		modulesQuery.fields().include("id", "user_id", "is_deleted");
+		List<ModulesDto> modulesList = modulesService.findAll(modulesQuery);
+		if (log.isDebugEnabled()) {
+			log.debug("Found all modules size: {}, include fields: {}", modulesList.size(), modulesQuery.getFieldsObject().toJson());
+		}
+		boolean apiCallStatsServiceEmpty = apiCallStatsService.isEmpty();
+		if (apiCallStatsServiceEmpty && !modulesList.isEmpty()) {
+			log.info("Initializing Api Call Stats data for the first time, discover the number of apis: {}, please wait...", modulesList.size());
+		}
+
+		if (CollectionUtils.isEmpty(modulesList)) {
+			return;
+		}
+
+		// Traverse all Modules, perform pre-aggregation, and save to ApiCallMinuteStats
+		int traverseStep = 0;
+		for (ModulesDto modulesDto : modulesList) {
+			long loopStartMs = System.currentTimeMillis();
+			ObjectId moduleOid = modulesDto.getId();
+			if (null == moduleOid) {
+				continue;
+			}
+			String moduleId = moduleOid.toString();
+			traverseStep++;
+			Boolean isDeleted = modulesDto.getIsDeleted();
+
+			if (Boolean.TRUE.equals(isDeleted)) {
+				// Delete ApiCallStats which Api is deleted
+				apiCallStatsService.deleteAllByModuleId(moduleId);
+				long loopCost = System.currentTimeMillis() - loopStartMs;
+				if (apiCallStatsServiceEmpty) {
+					log.info("Delete Api Call Stats by module id completed, module id: {}, cost: {} ms, progress: {}/{}", modulesDto, loopCost, traverseStep, modulesList.size());
+				}
+			} else {
+				// Get the historical ApiCallStats record based on moduleId, and get the lastApiCallId from it as this offset
+				Query apiCallStatsQuery = Query.query(Criteria.where("moduleId").is(moduleId)).limit(1);
+				ApiCallStatsDto apiCallStatsDto = apiCallStatsService.findOne(apiCallStatsQuery);
+				String lastApiCallId = null;
+				if (null != apiCallStatsDto) {
+					lastApiCallId = StringUtils.isBlank(apiCallStatsDto.getLastApiCallId()) ? null : apiCallStatsDto.getLastApiCallId();
+				}
+				if (log.isDebugEnabled()) {
+					log.debug(" {} - Found exists ApiCallStatsDto based on filter: {}, lastApiCallId: {}, exists ApiCallStatsDto: {}", traverseStep, apiCallStatsQuery.getQueryObject().toJson(), lastApiCallId, apiCallStatsDto);
+				}
+
+				// Aggregate the ApiCall data of the current module, and wrap new ApiCallStats
+				ApiCallStatsDto newApiCallStatsDto;
+				try {
+					newApiCallStatsDto = apiCallService.aggregateByAllPathId(moduleId, lastApiCallId);
+				} catch (Exception e) {
+					log.error("Aggregate ApiCallStatsDto failed, moduleId: {}, will skip it, error: {}", moduleId, e.getMessage(), e);
+					continue;
+				}
+
+				// Merge the new ApiCallStats with the historical ApiCallStats
+				try {
+					apiCallStatsService.merge(apiCallStatsDto, newApiCallStatsDto);
+				} catch (Exception e) {
+					log.error("Merge ApiCallStatsDto failed, old ApiCallStatsDto: {}, new ApiCallStatsDto: {}, will skip it, error: {}", apiCallStatsDto, newApiCallStatsDto, e.getMessage(), e);
+					continue;
+				}
+
+				// Calculate accessFailureRate
+				if (newApiCallStatsDto.getCallTotalCount() > 0) {
+					double rate = new BigDecimal(newApiCallStatsDto.getCallAlarmTotalCount()).divide(new BigDecimal(newApiCallStatsDto.getCallTotalCount()), 4, RoundingMode.HALF_UP).doubleValue();
+					newApiCallStatsDto.setAccessFailureRate(rate);
+				}
+
+				// Save the merged ApiCallStats
+				Query upsertQuery = null;
+				try {
+					newApiCallStatsDto.setLastUpdAt(new Date());
+					newApiCallStatsDto.setUserId(modulesDto.getUserId());
+					upsertQuery = Query.query(Criteria.where("moduleId").is(moduleId));
+					apiCallStatsService.upsert(upsertQuery, newApiCallStatsDto);
+					long loopCost = System.currentTimeMillis() - loopStartMs;
+					if (apiCallStatsServiceEmpty) {
+						log.info("Upsert one Api Call Stats completed, filter: {}, cost: {} ms, progress: {}/{}, stats data: {}", upsertQuery.getQueryObject().toJson(), loopCost, traverseStep, modulesList.size(), newApiCallStatsDto);
+					}
+					if (log.isDebugEnabled()) {
+						log.debug("Upsert one Api Call Stats completed, filter: {}, cost: {} ms, progress: {}/{}, stats data: {}", upsertQuery.getQueryObject().toJson(), loopCost, traverseStep, modulesList.size(), newApiCallStatsDto);
+					}
+				} catch (Exception e) {
+					log.error("Upsert one Api Call Stats failed, query: {}, data: {}, will skip it, error: {}",
+							null != upsertQuery ? upsertQuery.getQueryObject().toJson() : "null", newApiCallStatsDto, e.getMessage(), e);
+				}
+			}
+		}
+		long cost = System.currentTimeMillis() - startMs;
+		if (apiCallStatsServiceEmpty && !modulesList.isEmpty()) {
+			log.info("Initialize Api Call Stats data for the first time completed, cost: {} ms", cost);
+		}
+	}
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/controller/LogCollectorController.java b/manager/tm/src/main/java/com/tapdata/tm/task/controller/LogCollectorController.java
index e7fdb888b5..f8441546b3 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/task/controller/LogCollectorController.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/controller/LogCollectorController.java
@@ -169,8 +169,9 @@ public ResponseMessage<Page<LogCollectorRelateTaskVo>> getRelateTasks(@RequestPa
     public ResponseMessage<Page<ShareCdcTableInfo>> tableInfos(@RequestParam("taskId") String taskId, @RequestParam(value = "connectionId", required = false) String connectionId,
                                                                   @RequestParam(value = "keyword", required = false) String keyword,
                                                                   @RequestParam(value = "page", defaultValue = "1") Integer page,
-                                                                  @RequestParam(value = "size", defaultValue = "20") Integer size) {
-        Page<ShareCdcTableInfo> connectionInfos = logCollectorService.tableInfos(taskId, connectionId, keyword, page, size, getLoginUser());
+                                                                  @RequestParam(value = "size", defaultValue = "20") Integer size,
+                                                                  @RequestParam(value = "order", defaultValue = "") String order) {
+        Page<ShareCdcTableInfo> connectionInfos = logCollectorService.tableInfos(taskId, connectionId, keyword, page, size, order, getLoginUser());
         return success(connectionInfos);
     }
 
@@ -179,9 +180,10 @@ public ResponseMessage<Page<ShareCdcTableInfo>> tableInfos(@RequestParam("taskId
     @Operation(summary = "已停止挖掘的表")
     public ResponseMessage<Page<ShareCdcTableInfo>> excludeTableInfos(@RequestParam("taskId") String taskId, @RequestParam(value = "connectionId", required = false) String connectionId,
                                                                       @RequestParam(value = "keyword", required = false) String keyword,
-                                                                   @RequestParam(value = "page", defaultValue = "1") Integer page,
-                                                                   @RequestParam(value = "size", defaultValue = "20") Integer size) {
-        Page<ShareCdcTableInfo> connectionInfos = logCollectorService.excludeTableInfos(taskId, connectionId, keyword, page, size, getLoginUser());
+                                                                      @RequestParam(value = "page", defaultValue = "1") Integer page,
+                                                                      @RequestParam(value = "size", defaultValue = "20") Integer size,
+                                                                      @RequestParam(value = "order", defaultValue = "") String order) {
+        Page<ShareCdcTableInfo> connectionInfos = logCollectorService.excludeTableInfos(taskId, connectionId, keyword, page, size, order, getLoginUser());
         return success(connectionInfos);
     }
 
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 615dc54b95..55d8667f37 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
@@ -188,7 +188,9 @@ public ResponseMessage<Page<TaskDto>> find(@RequestParam(value = "filter", requi
 
         UserDetail userDetail;
         Where where = filter.getWhere();
-        if (where.containsKey("_id") || where.containsKey("id")) {
+        if (where.get("id") instanceof Map && ((Map) where.get("id")).containsKey("$in")) {
+            userDetail = getLoginUser();
+        } else if (where.containsKey("_id") || where.containsKey("id")) {
             Object objectId = where.get("_id");
             String taskId;
             if (objectId != null)
diff --git a/manager/tm/src/main/java/com/tapdata/tm/task/service/DefaultLicenseService.java b/manager/tm/src/main/java/com/tapdata/tm/task/service/DefaultLicenseService.java
new file mode 100644
index 0000000000..1c4d1e1e9d
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/task/service/DefaultLicenseService.java
@@ -0,0 +1,13 @@
+package com.tapdata.tm.task.service;
+
+import com.tapdata.tm.commons.task.dto.TaskDto;
+import com.tapdata.tm.config.security.UserDetail;
+import org.springframework.stereotype.Service;
+
+@Service
+public class DefaultLicenseService implements ILicenseService{
+    @Override
+    public boolean checkTaskPipelineLimit(TaskDto taskDto, UserDetail user) {
+        return true;
+    }
+}
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 1fe9eb25b7..4b64298285 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
@@ -27,8 +27,6 @@
 import com.tapdata.tm.base.dto.Where;
 import com.tapdata.tm.commons.dag.logCollector.LogCollecotrConnConfig;
 import com.tapdata.tm.commons.dag.logCollector.LogCollectorNode;
-import com.tapdata.tm.commons.schema.bean.ResponseBody;
-import com.tapdata.tm.commons.schema.bean.ValidateDetail;
 import com.tapdata.tm.ds.entity.DataSourceEntity;
 import com.tapdata.tm.monitor.service.BatchService;
 import com.tapdata.tm.shareCdcTableMapping.service.ShareCdcTableMappingService;
@@ -375,6 +373,7 @@ private static String getTableName() {
     private UserDataReportService userDataReportService;
     private BatchService batchService;
     private ShareCdcTableMappingService shareCdcTableMappingService;
+    private ILicenseService iLicenseService;
 
     public TaskServiceImpl(@NonNull TaskRepository repository) {
         super(repository);
@@ -1429,6 +1428,12 @@ public List<MutiResponseMessage> batchStart(List<ObjectId> taskIds, UserDetail u
                 }
                 start(task, user, "11");
             } catch (Exception e) {
+                if (e instanceof BizException) {
+                    if (("License.NodeInstanceIdInvalid".equals(((BizException)e).getErrorCode()))
+                            || "Task.LicenseScheduleLimit".equals(((BizException)e).getErrorCode())) {
+                        throw e;
+                    }
+                }
                 log.warn("start task exception, task id = {}, e = {}", task.getId(), ThrowableUtils.getStackTraceByPn(e));
                 monitoringLogsService.startTaskErrorLog(task, user, e, Level.ERROR);
                 if (e instanceof BizException) {
@@ -3968,6 +3973,8 @@ public void start(TaskDto taskDto, UserDetail user, String startFlag, boolean sy
      *                  第二位 是否开启打点任务      1 是   0 否
      */
     public void start(TaskDto taskDto, UserDetail user, String startFlag) {
+        boolean canStart = iLicenseService.checkTaskPipelineLimit(taskDto, user);
+        if (!canStart) throw new BizException("Task.LicenseScheduleLimit");
         if (TaskDto.TYPE_INITIAL_SYNC.equals(taskDto.getType()) && TaskDto.STATUS_COMPLETE.equals(taskDto.getStatus()) && !taskDto.getCrontabExpressionFlag()) {
             scheduleService.createTaskRecordForInitial(taskDto);
             update(new Query(Criteria.where("_id").is(taskDto.getId())), taskDto);
diff --git a/manager/tm/src/main/java/com/tapdata/tm/user/service/UserServiceImpl.java b/manager/tm/src/main/java/com/tapdata/tm/user/service/UserServiceImpl.java
index 91b43638d3..0d41bf5615 100644
--- a/manager/tm/src/main/java/com/tapdata/tm/user/service/UserServiceImpl.java
+++ b/manager/tm/src/main/java/com/tapdata/tm/user/service/UserServiceImpl.java
@@ -397,12 +397,12 @@ public <T extends BaseDto> UserDto save(CreateUserRequest request, UserDetail us
 
         return result;
     }
-    private List<RoleMappingDto> updateRoleMapping(String userId, List<Object> roleusers, UserDetail userDetail) {
+    protected List<RoleMappingDto> updateRoleMapping(String userId, List<Object> roleusers, UserDetail userDetail) {
         // delete old role mapping
+        if (CollectionUtils.isNotEmpty(roleusers)) {
             long deleted = roleMappingService.deleteAll(Query.query(Criteria.where("principalId").is(userId).and("principalType").is("USER")));
             log.info("delete old role mapping for userId {}, deleted: {}", userId, deleted);
         // add new role mapping
-        if (CollectionUtils.isNotEmpty(roleusers)) {
             List<RoleMappingDto> roleMappingDtos = roleusers.stream().map(r -> (String) r).map(roleId -> {
                 RoleMappingDto roleMappingDto = new RoleMappingDto();
                 roleMappingDto.setPrincipalType("USER");
diff --git a/manager/tm/src/main/java/com/tapdata/tm/utils/CustomPatternLayout.java b/manager/tm/src/main/java/com/tapdata/tm/utils/CustomPatternLayout.java
new file mode 100644
index 0000000000..b2b75d5940
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/utils/CustomPatternLayout.java
@@ -0,0 +1,27 @@
+package com.tapdata.tm.utils;
+
+import ch.qos.logback.classic.PatternLayout;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+
+import java.util.Map;
+
+public class CustomPatternLayout extends PatternLayout {
+    private Map<String, Object> oemConfigmap = OEMReplaceUtil.getOEMConfigMap("log/replace.json");
+
+    @Override
+    public String doLayout(ILoggingEvent event) {
+        String formatMessage = super.doLayout(event);
+        if (MapUtils.isNotEmpty(oemConfigmap)) {
+            return OEMReplaceUtil.replace(formatMessage, oemConfigmap);
+        }
+        return formatMessage;
+    }
+
+    public Map<String, Object> getOemConfigmap() {
+        return oemConfigmap;
+    }
+
+    public void setOemConfigmap(Map<String, Object> oemConfigmap) {
+        this.oemConfigmap = oemConfigmap;
+    }
+}
diff --git a/manager/tm/src/main/resources/banner.txt b/manager/tm/src/main/resources/banner.txt
index 5afc196d4a..347e8eb43a 100644
--- a/manager/tm/src/main/resources/banner.txt
+++ b/manager/tm/src/main/resources/banner.txt
@@ -1,10 +1,7 @@
-.-----.              .-.       .-.
-`-. .-'              : :      .' `.
-  : : .--.  .---.  .-' : .--. `. .'.--.
-  : :' .; ; : .; `' .; :' .; ; : :' .; ;
-  :_;`.__,_;: ._.'`.__.'`.__,_;:_;`.__,_;
-            : :
-            :_;
+.  .
+|\/|.-. .-..-. .-..-,.-.
+'  '`-`-' '`-`-`-|`'-'
+               `-'
 ${AnsiColor.BRIGHT_YELLOW}
 :: Tapdata Manager ::    (v${application.version})
 :: Build At ::           (${application.build})
diff --git a/manager/tm/src/main/resources/dev/logback.xml b/manager/tm/src/main/resources/dev/logback.xml
index 6ef16284d9..a25ce76536 100644
--- a/manager/tm/src/main/resources/dev/logback.xml
+++ b/manager/tm/src/main/resources/dev/logback.xml
@@ -24,9 +24,9 @@
 			</timeBasedFileNamingAndTriggeringPolicy>
 			<maxHistory>7</maxHistory>
 		</rollingPolicy>
-		<encoder>
+		<layout class="com.tapdata.tm.utils.CustomPatternLayout">
 			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%contextName] [%thread] %-5level %logger{36} - %msg%n</pattern>
-		</encoder>
+		</layout>
 	</appender>
 
 	<jmxConfigurator />
diff --git a/manager/tm/src/main/resources/idaas/logback.xml b/manager/tm/src/main/resources/idaas/logback.xml
index ca69fdcd8a..8d3234f28b 100644
--- a/manager/tm/src/main/resources/idaas/logback.xml
+++ b/manager/tm/src/main/resources/idaas/logback.xml
@@ -24,9 +24,9 @@
 			</timeBasedFileNamingAndTriggeringPolicy>
 			<maxHistory>7</maxHistory>
 		</rollingPolicy>
-		<encoder>
+		<layout class="com.tapdata.tm.utils.CustomPatternLayout">
 			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%contextName] [%thread] %-5level %logger{36} - %msg%n</pattern>
-		</encoder>
+		</layout>
 	</appender>
 
 	<!-- 异步输出 -->
diff --git a/manager/tm/src/main/resources/init/idaas/3.13-2.json b/manager/tm/src/main/resources/init/idaas/3.13-2.json
new file mode 100644
index 0000000000..c022188d65
--- /dev/null
+++ b/manager/tm/src/main/resources/init/idaas/3.13-2.json
@@ -0,0 +1,89 @@
+[
+  {
+    "createIndexes": "ApiCall",
+    "indexes": [
+      {
+        "name": "allPathId_1__id_1",
+        "key": {
+          "allPathId": 1,
+          "_id": 1
+        },
+        "background": true
+      }
+    ]
+  },
+  {
+    "createIndexes": "ApiCall",
+    "indexes": [
+      {
+        "name": "createTime_1",
+        "key": {
+          "createTime": 1
+        },
+        "background": true
+      }
+    ]
+  },
+  {
+    "createIndexes": "ApiCallStats",
+    "indexes": [
+      {
+        "name": "moduleId_1",
+        "key": {
+          "moduleId": 1
+        },
+        "unique": true,
+        "background": true
+      }
+    ]
+  },
+  {
+    "createIndexes": "ApiCallStats",
+    "indexes": [
+      {
+        "name": "user_id_1",
+        "key": {
+          "user_id": 1
+        },
+        "background": true
+      }
+    ]
+  },
+  {
+    "createIndexes": "ApiCallStats",
+    "indexes": [
+      {
+        "name": "clientIds_1",
+        "key": {
+          "clientIds": 1
+        },
+        "background": true
+      }
+    ]
+  },
+  {
+    "createIndexes": "ApiCallMinuteStats",
+    "indexes": [
+      {
+        "name": "moduleId_1",
+        "key": {
+          "moduleId": 1
+        },
+        "background": true
+      }
+    ]
+  },
+  {
+    "createIndexes": "ApiCallMinuteStats",
+    "indexes": [
+      {
+        "name": "apiCallTime_1",
+        "key": {
+          "apiCallTime": 1
+        },
+        "expireAfterSeconds": 7200,
+        "background": true
+      }
+    ]
+  }
+]
\ No newline at end of file
diff --git a/manager/tm/src/main/resources/init/idaas/version b/manager/tm/src/main/resources/init/idaas/version
index d269aed263..8dddc5581f 100644
--- a/manager/tm/src/main/resources/init/idaas/version
+++ b/manager/tm/src/main/resources/init/idaas/version
@@ -1,1 +1,1 @@
-3.12-2
\ No newline at end of file
+3.13-2
\ No newline at end of file
diff --git a/manager/tm/src/main/resources/messages.properties b/manager/tm/src/main/resources/messages.properties
index c30441eb5c..7f4d343703 100644
--- a/manager/tm/src/main/resources/messages.properties
+++ b/manager/tm/src/main/resources/messages.properties
@@ -48,6 +48,8 @@ Task.nodeRefresh=The task node has been refreshed
 Task.DateProcessConfigInvalid=Time processing node selection time type exception
 Task.ScheduleLimit=The number of tasks that the engine can be called exceeds the limit. Please upgrade your subscription to obtain more tasks
 Task.ManuallyScheduleLimit=The number of tasks that the engine is manually specified by the user can be called exceeds the limit. Please specify another engine {0}
+Task.LicenseScheduleLimit=The number of pipelines that can be scheduled exceeds the quota. Please update the license quota or stop related tasks.
+License.NodeInstanceIdInvalid=Please make sure the node [{0}] is datasource node
 Task.ResetAgentNotFound=The reset task did not find an available engine, please start/restart the engine and try again
 Task.ResetStatusInvalid=The current status of the task that needs to be reset has been updated, please refresh the page and try again
 Task.NotfoundDatasource=Not found datasource '{0}'
diff --git a/manager/tm/src/main/resources/messages_zh_CN.properties b/manager/tm/src/main/resources/messages_zh_CN.properties
index 5f3819aa17..14ada1cf08 100644
--- a/manager/tm/src/main/resources/messages_zh_CN.properties
+++ b/manager/tm/src/main/resources/messages_zh_CN.properties
@@ -72,6 +72,8 @@ Task.nodeRefresh=\u4EFB\u52A1\u8282\u70B9\u5DF2\u7ECF\u88AB\u5237\u65B0
 Task.DateProcessConfigInvalid=\u65F6\u95F4\u5904\u7406\u8282\u70B9\u9009\u62E9\u65F6\u95F4\u7C7B\u578B\u5F02\u5E38
 Task.ScheduleLimit=\u5F15\u64CE\u53EF\u4EE5\u88AB\u8C03\u7528\u7684\u4EFB\u52A1\u8D85\u8FC7\u4E86\u9650\u5236\u6570\uFF0C\u8BF7\u5347\u7EA7\u8BA2\u9605\u4EE5\u83B7\u53D6\u66F4\u591A\u4EFB\u52A1\u6570\u91CF
 Task.ManuallyScheduleLimit=\u7528\u6237\u6307\u5B9A\u5F15\u64CE\u53EF\u4EE5\u88AB\u8C03\u7528\u7684\u4EFB\u52A1\u8D85\u8FC7\u4E86\u9650\u5236\u6570\uFF0C\u8BF7\u66FF\u6362\u5176\u4ED6\u5F15\u64CE {0}
+Task.LicenseScheduleLimit=\u53ef\u4ee5\u8c03\u5ea6\u7684\u901a\u9053\u6570\u8d85\u8fc7\u4e86\u914d\u989d\uff0c\u8bf7\u66f4\u65b0\u006c\u0069\u0063\u0065\u006e\u0073\u0065\u914d\u989d\u6216\u505c\u6b62\u76f8\u5173\u4efb\u52a1
+License.NodeInstanceIdInvalid=\u8bf7\u786e\u4fdd\u8282\u70b9 [{0}] \u4e3a\u6570\u636e\u6e90\u8282\u70b9
 Task.ResetAgentNotFound=\u91CD\u7F6E\u4EFB\u52A1\u6CA1\u6709\u627E\u5230\u53EF\u7528\u7684\u5F15\u64CE\uFF0C\u8BF7\u542F\u52A8/\u91CD\u542F\u5F15\u64CE\u540E\u518D\u8BD5
 Task.ResetStatusInvalid=\u5F53\u524D\u9700\u8981\u91CD\u7F6E\u7684\u4EFB\u52A1\u72B6\u6001\u88AB\u66F4\u65B0\uFF0C\u8BF7\u5237\u65B0\u9875\u9762\u518D\u8BD5
 Task.NotfoundDatasource=\u627E\u4E0D\u5230\u6570\u636E\u6E90 '{0}'
diff --git a/manager/tm/src/main/resources/pdkTestItem_en.properties b/manager/tm/src/main/resources/pdkTestItem_en.properties
index 7ef73e3b05..737e0841b3 100644
--- a/manager/tm/src/main/resources/pdkTestItem_en.properties
+++ b/manager/tm/src/main/resources/pdkTestItem_en.properties
@@ -27,7 +27,7 @@ check.cdc.privilege.solution=Please check cdc configuration.
 create.table.privilege.fail=Check create table privileges failed.
 create.table.privilege.reason=When creating table, the corresponding permission is missing.
 create.table.privilege.solution=Check whether the username in the data connection used by the target node lacks create table permission.
-time.consistent.file=Failed to obtain current database time.
+time.consistent.fail=Failed to obtain current database time.
 time.consistent.reason=1. The server is down;\n2. There is no privilege when obtain database time.
 time.consistent.solution=1. Ensure database server is available;\n2. Ensure current user has enough privilege to obtain database time.
 
diff --git a/manager/tm/src/main/resources/pdkTestItem_zh_CN.properties b/manager/tm/src/main/resources/pdkTestItem_zh_CN.properties
index 1068eeb1dc..ff0420cb86 100644
--- a/manager/tm/src/main/resources/pdkTestItem_zh_CN.properties
+++ b/manager/tm/src/main/resources/pdkTestItem_zh_CN.properties
@@ -27,7 +27,7 @@ check.cdc.privilege.solution=\u8bf7\u68c0\u67e5\u0063\u0064\u0063\u914d\u7f6e\u3
 create.table.privilege.fail=\u68c0\u67e5\u521b\u5efa\u8868\u6743\u9650\u5931\u8d25\u3002
 create.table.privilege.reason=\u521b\u5efa\u8868\u65f6\uff0c\u7f3a\u5c11\u76f8\u5e94\u7684\u6743\u9650\u3002
 create.table.privilege.solution=\u68c0\u67e5\u76ee\u6807\u8282\u70b9\u4f7f\u7528\u7684\u6570\u636e\u8fde\u63a5\u4e2d\u7684\u7528\u6237\u540d\u662f\u5426\u7f3a\u5c11\u521b\u5efa\u8868\u6743\u9650\u3002
-time.consistent.file=\u65e0\u6cd5\u83b7\u53d6\u5f53\u524d\u6570\u636e\u5e93\u65f6\u95f4\u3002
+time.consistent.fail=\u65e0\u6cd5\u83b7\u53d6\u5f53\u524d\u6570\u636e\u5e93\u65f6\u95f4\u3002
 time.consistent.reason=\u0031\u002e\u670d\u52a1\u5668\u5b95\u673a\uff1b\u000A\u0032\u002e\u83b7\u53d6\u6570\u636e\u5e93\u65f6\u95f4\u65f6\u6ca1\u6709\u6743\u9650\u3002
 time.consistent.solution=\u0031\u002e\u786e\u4fdd\u6570\u636e\u5e93\u670d\u52a1\u5668\u53ef\u7528\uff1b\u000A\u0032\u002e\u786e\u4fdd\u5f53\u524d\u7528\u6237\u6709\u8db3\u591f\u7684\u6743\u9650\u83b7\u53d6\u6570\u636e\u5e93\u65f6\u95f4\u3002
 
diff --git a/pom.xml b/pom.xml
index a84550e275..cdcd5d4ff6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,7 +37,8 @@
             iengine/api/src/main/java/io/tapdata/aspect/DropTableFuncAspect.java,
             **/*Context.java,
             **/*Exception.java,
-            **/*Controller.java
+            **/*Controller.java,
+            iengine/iengine-common/src/main/java/io/tapdata/log/CustomPatternLayout.java
         </sonar.coverage.exclusions>
         <sonar.exclusions>
             manager/**/dto/*,
@@ -57,7 +58,8 @@
             manager/tm/src/main/java/com/tapdata/tm/init/patches/dfs/*,
             **/*Context.java,
             **/*Exception.java,
-            **/*Controller.java
+            **/*Controller.java,
+            iengine/iengine-common/src/main/java/io/tapdata/log/CustomPatternLayout.java
         </sonar.exclusions>
         <sonar.test.exclusions>
             **/src/test/**
diff --git a/tapdata-cli/src/main/java/io/tapdata/pdk/cli/services/UploadFileService.java b/tapdata-cli/src/main/java/io/tapdata/pdk/cli/services/UploadFileService.java
index 97289a7dd2..9d51a12b69 100644
--- a/tapdata-cli/src/main/java/io/tapdata/pdk/cli/services/UploadFileService.java
+++ b/tapdata-cli/src/main/java/io/tapdata/pdk/cli/services/UploadFileService.java
@@ -214,6 +214,9 @@ public static void upload(Map<String, InputStream> inputStreamMap, File file, Li
     if (!"ok".equals(map.get("code"))) {
         msg = map.get("reqId") != null ? (String) map.get("message") : (String) map.get("msg");
         result = "fail";
+      printUtil.print(PrintUtil.TYPE.ERROR, String.format("* Register Connector: %s Failed, message: %s", file.getName(), msg));
+    } else {
+      printUtil.print(PrintUtil.TYPE.INFO, String.format("* Register Connector: %s Completed", file.getName()));
     }
     printUtil.print(PrintUtil.TYPE.DEBUG, "result:" + result + ", name:" + file.getName() + ", msg:" + msg + ", response:" + response);
   }


Test patch:
diff --git a/build/run_smoke_tests.sh b/build/run_smoke_tests.sh
index f932f4bc93..5fcd27fe05 100644
--- a/build/run_smoke_tests.sh
+++ b/build/run_smoke_tests.sh
@@ -209,7 +209,7 @@ function convertsecs(){
 }
  
 function setExitCode(){
-  RESULT=$(echo $RUN_BODY | getJsonValue result)
+  RESULT=$(echo $RUN_BODY | getJsonValue consolidatedResult)
   APPURL=$(echo $RUN_BODY | getJsonValue result)
   echo $RESULT
   echo $([[ $RESULT =~ "SUCCESS" ]])
diff --git a/iengine/iengine-app/src/test/java/io/tapdata/ApplicationTest.java b/iengine/iengine-app/src/test/java/io/tapdata/ApplicationTest.java
index 280e9eef63..9330a5eb79 100644
--- a/iengine/iengine-app/src/test/java/io/tapdata/ApplicationTest.java
+++ b/iengine/iengine-app/src/test/java/io/tapdata/ApplicationTest.java
@@ -8,8 +8,12 @@
 import org.junit.jupiter.api.Test;
 import org.mockito.MockedStatic;
 
+import java.util.HashMap;
+import java.util.Map;
+
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mockStatic;
 
 public class ApplicationTest {
     @Nested
@@ -22,13 +26,23 @@ void test1() {
             LoggerContext context = (LoggerContext) LogManager.getContext(false);
             assertEquals(2, context.getRootLogger().getAppenders().size());
         }
+
+        @Test
+        void test2() {
+            try (MockedStatic<OEMReplaceUtil> oemReplaceUtilMockedStatic = mockStatic(OEMReplaceUtil.class)) {
+                Map<String, Object> oemConfig = new HashMap<>();
+                oemConfig.put("oem", "replace");
+                oemReplaceUtilMockedStatic.when(() -> OEMReplaceUtil.getOEMConfigMap("log/replace.json")).thenReturn(oemConfig);
+                Application.addRollingFileAppender("./workDir");
+            }
+        }
     }
 
     @Nested
     class getFileNameAfterOem {
         @Test
         void testNormal() {
-            try (MockedStatic<OEMReplaceUtil> oem = org.mockito.Mockito.mockStatic(OEMReplaceUtil.class)) {
+            try (MockedStatic<OEMReplaceUtil> oem = mockStatic(OEMReplaceUtil.class)) {
                 oem.when(() -> OEMReplaceUtil.replace("fileName", "log/replace.json")).thenReturn("app");
                 String fileName = Application.getFileNameAfterOem("fileName");
                 Assertions.assertEquals("app", fileName);
@@ -37,7 +51,7 @@ void testNormal() {
 
         @Test
         void testNull() {
-            try (MockedStatic<OEMReplaceUtil> oem = org.mockito.Mockito.mockStatic(OEMReplaceUtil.class)) {
+            try (MockedStatic<OEMReplaceUtil> oem = mockStatic(OEMReplaceUtil.class)) {
                 oem.when(() -> OEMReplaceUtil.replace("fileName", "log/replace.json")).thenReturn(null);
                 String fileName = Application.getFileNameAfterOem("fileName");
                 Assertions.assertEquals("fileName", fileName);
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 6d11f02fce..86cce4bd7f 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
@@ -709,18 +709,17 @@ void test2() {
 			event = new TapInsertRecordEvent();
 			event.setContainsIllegalDate(true);
 			Map<String, Object> after = new HashMap<>();
+			TapDateTimeValue tapDateTimeValue = new TapDateTimeValue();
+			DateTime dateTime = new DateTime("0000-00-00 00:00:00", DateTime.DATETIME_TYPE);
+			tapDateTimeValue.setValue(dateTime);
 			after.put("id", "1");
 			after.put("name", "test");
-			after.put("date", new Date());
-			after.put("last_date", new Date());
+			after.put("date", tapDateTimeValue);
+			after.put("last_date", tapDateTimeValue);
 			((TapInsertRecordEvent) event).setAfter(after);
 			event.setContainsIllegalDate(true);
-			List<String> illegalDateFiledName = new ArrayList<>();
-			illegalDateFiledName.add("date");
-			illegalDateFiledName.add("last_date");
-			illegalDateFiledName.add("test_date");
-			((TapInsertRecordEvent) event).setAfterIllegalDateFieldName(illegalDateFiledName);
 			doCallRealMethod().when(hazelcastTargetPdkBaseNode).replaceIllegalDateWithNullIfNeed(event);
+			doCallRealMethod().when(hazelcastTargetPdkBaseNode).replaceIllegalDate(any());
 			hazelcastTargetPdkBaseNode.replaceIllegalDateWithNullIfNeed(event);
 			assertNull(((TapInsertRecordEvent) event).getAfter().get("date"));
 			assertNull(((TapInsertRecordEvent) event).getAfter().get("last_date"));
@@ -732,18 +731,17 @@ void test3() {
 			event = new TapUpdateRecordEvent();
 			event.setContainsIllegalDate(true);
 			Map<String, Object> after = new HashMap<>();
+			TapDateTimeValue tapDateTimeValue = new TapDateTimeValue();
+			DateTime dateTime = new DateTime("0000-00-00 00:00:00", DateTime.DATETIME_TYPE);
+			tapDateTimeValue.setValue(dateTime);
 			after.put("id", "1");
 			after.put("name", "test");
-			after.put("date", new Date());
+			after.put("date", tapDateTimeValue);
 			((TapUpdateRecordEvent) event).setBefore(after);
 			((TapUpdateRecordEvent) event).setAfter(after);
 			event.setContainsIllegalDate(true);
-			List<String> illegalDateFiledName = new ArrayList<>();
-			illegalDateFiledName.add("date");
-			illegalDateFiledName.add("last_date");
-			((TapUpdateRecordEvent) event).setBeforeIllegalDateFieldName(illegalDateFiledName);
-			((TapUpdateRecordEvent) event).setAfterIllegalDateFieldName(illegalDateFiledName);
 			doCallRealMethod().when(hazelcastTargetPdkBaseNode).replaceIllegalDateWithNullIfNeed(event);
+			doCallRealMethod().when(hazelcastTargetPdkBaseNode).replaceIllegalDate(any());
 			hazelcastTargetPdkBaseNode.replaceIllegalDateWithNullIfNeed(event);
 			assertNull(((TapUpdateRecordEvent) event).getBefore().get("date"));
 			assertNull(((TapUpdateRecordEvent) event).getAfter().get("date"));
diff --git a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastMergeNodeTest.java b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastMergeNodeTest.java
index e6cb7bbca5..dde88c8153 100644
--- a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastMergeNodeTest.java
+++ b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastMergeNodeTest.java
@@ -4,6 +4,9 @@
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.hazelcast.core.HazelcastInstance;
 import com.hazelcast.jet.core.Processor;
+import com.hazelcast.map.IMap;
+import com.hazelcast.persistence.ConstructType;
+import com.hazelcast.persistence.PersistenceStorage;
 import com.tapdata.entity.TapdataHeartbeatEvent;
 import com.tapdata.entity.task.context.ProcessorBaseContext;
 import io.tapdata.entity.schema.value.TapArrayValue;
@@ -709,7 +712,6 @@ void setUp() {
 			HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
 			when(jetContext.hazelcastInstance()).thenReturn(hazelcastInstance);
 			ReflectionTestUtils.setField(mockHazelcastMergeNode, "jetContext", jetContext);
-			doReturn(constructIMap).when(mockHazelcastMergeNode).buildConstructIMap(eq(hazelcastInstance), anyString(), anyString(), any(ExternalStorageDto.class));
 			ReflectionTestUtils.setField(mockHazelcastMergeNode, "obsLogger", mockObsLogger);
 
 			mockHazelcastMergeNode.initFirstLevelIds();
@@ -721,26 +723,33 @@ void setUp() {
 		@SneakyThrows
 		@DisplayName("main process test")
 		void testMainProcess() {
-			mockHazelcastMergeNode.initCheckJoinKeyUpdateCacheMap();
-
-			Object checkJoinKeyUpdateCacheMapObj = ReflectionTestUtils.getField(mockHazelcastMergeNode, "checkJoinKeyUpdateCacheMap");
-			assertInstanceOf(HashMap.class, checkJoinKeyUpdateCacheMapObj);
-			HashMap checkJoinKeyUpdateCacheMap = (HashMap) checkJoinKeyUpdateCacheMapObj;
-			assertEquals(1, checkJoinKeyUpdateCacheMap.size());
-			assertTrue(checkJoinKeyUpdateCacheMap.containsKey("2"));
-			Object constructIMapObj = checkJoinKeyUpdateCacheMap.get("2");
-			assertInstanceOf(ConstructIMap.class, constructIMapObj);
-			assertEquals(constructIMap, constructIMapObj);
+			try(MockedStatic<HazelcastMergeNode> hazelcastMergeNodeMockedStatic = mockStatic(HazelcastMergeNode.class)){
+				hazelcastMergeNodeMockedStatic.when(() -> HazelcastMergeNode.buildConstructIMap(any(), anyString(),anyString(),any())).thenReturn(constructIMap);
+				hazelcastMergeNodeMockedStatic.when(() -> HazelcastMergeNode.getCheckUpdateJoinKeyValueCacheName(any())).thenReturn("Merge_Test");
+				mockHazelcastMergeNode.initCheckJoinKeyUpdateCacheMap();
+				Object checkJoinKeyUpdateCacheMapObj = ReflectionTestUtils.getField(mockHazelcastMergeNode, "checkJoinKeyUpdateCacheMap");
+				assertInstanceOf(HashMap.class, checkJoinKeyUpdateCacheMapObj);
+				HashMap checkJoinKeyUpdateCacheMap = (HashMap) checkJoinKeyUpdateCacheMapObj;
+				assertEquals(1, checkJoinKeyUpdateCacheMap.size());
+				assertTrue(checkJoinKeyUpdateCacheMap.containsKey("2"));
+				Object constructIMapObj = checkJoinKeyUpdateCacheMap.get("2");
+				assertInstanceOf(ConstructIMap.class, constructIMapObj);
+				assertEquals(constructIMap, constructIMapObj);
+			}
 		}
 
 		@Test
 		@DisplayName("when join key include pk")
 		void whenJoinKeyIncludePK() {
-			TapTable b = tapTableMap.get("b");
-			b.getNameFieldMap().get("b_id").primaryKeyPos(1);
-			TapCodeException tapCodeException = assertThrows(TapCodeException.class, () -> mockHazelcastMergeNode.initCheckJoinKeyUpdateCacheMap());
-			assertEquals(TaskMergeProcessorExCode_16.BUILD_CHECK_UPDATE_JOIN_KEY_CACHE_FAILED_JOIN_KEY_INCLUDE_PK, tapCodeException.getCode());
-			assertNotNull(tapCodeException.getMessage());
+			try(MockedStatic<HazelcastMergeNode> hazelcastMergeNodeMockedStatic = mockStatic(HazelcastMergeNode.class)){
+				hazelcastMergeNodeMockedStatic.when(() -> HazelcastMergeNode.buildConstructIMap(any(), anyString(),anyString(),any())).thenReturn(constructIMap);
+				hazelcastMergeNodeMockedStatic.when(() -> HazelcastMergeNode.getCheckUpdateJoinKeyValueCacheName(any())).thenReturn("Merge_Test");
+				TapTable b = tapTableMap.get("b");
+				b.getNameFieldMap().get("b_id").primaryKeyPos(1);
+				TapCodeException tapCodeException = assertThrows(TapCodeException.class, () -> mockHazelcastMergeNode.initCheckJoinKeyUpdateCacheMap());
+				assertEquals(TaskMergeProcessorExCode_16.BUILD_CHECK_UPDATE_JOIN_KEY_CACHE_FAILED_JOIN_KEY_INCLUDE_PK, tapCodeException.getCode());
+				assertNotNull(tapCodeException.getMessage());
+			}
 		}
 	}
 
@@ -2319,4 +2328,98 @@ void testNodeIsNotDisabled(){
 		}
 
 	}
+
+	@Nested
+	@DisplayName("Method checkBuildConstructIMap test")
+	class checkBuildConstructIMapTest{
+		@Test
+		void test_createNewHashConstructIMap(){
+			try(MockedStatic<ExternalStorageUtil> utilMockedStatic = mockStatic(ExternalStorageUtil.class);
+				MockedStatic<PersistenceStorage> persistenceStorageMockedStatic = mockStatic(PersistenceStorage.class)){
+				PersistenceStorage persistenceStorage = mock(PersistenceStorage.class);
+				persistenceStorageMockedStatic.when(PersistenceStorage::getInstance).thenReturn(persistenceStorage);
+				when(persistenceStorage.isEmpty(any(),any())).thenReturn(false);
+				HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
+				IMap map = mock(IMap.class);
+				String cacheNameHash = String.valueOf("test".hashCode());
+				when(map.getName()).thenReturn(cacheNameHash);
+				when(hazelcastInstance.getMap(cacheNameHash)).thenReturn(map);
+				utilMockedStatic.when(()->ExternalStorageUtil.initHZMapStorage(any(),any(),any(),any())).thenAnswer(invocation -> null);
+				ConstructIMap<Document>  result = HazelcastMergeNode.checkBuildConstructIMap(hazelcastInstance,"test","test",mock(ExternalStorageDto.class));
+				Assertions.assertEquals(cacheNameHash,result.getName());
+			}
+		}
+
+		@Test
+		void test_createNewHashConstructIMapError(){
+			try(MockedStatic<ExternalStorageUtil> utilMockedStatic = mockStatic(ExternalStorageUtil.class)){
+				HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
+				utilMockedStatic.when(()->ExternalStorageUtil.initHZMapStorage(any(),any(),any(),any())).thenThrow(RuntimeException.class);
+				Assertions.assertThrows(TapCodeException.class,()-> HazelcastMergeNode.checkBuildConstructIMap(hazelcastInstance,"test","test",mock(ExternalStorageDto.class)));
+			}
+		}
+
+		@Test
+		void test_createOldConstructIMapError(){
+			try(MockedStatic<ExternalStorageUtil> utilMockedStatic = mockStatic(ExternalStorageUtil.class);
+				MockedStatic<PersistenceStorage> persistenceStorageMockedStatic = mockStatic(PersistenceStorage.class)){
+				PersistenceStorage persistenceStorage = mock(PersistenceStorage.class);
+				persistenceStorageMockedStatic.when(PersistenceStorage::getInstance).thenReturn(persistenceStorage);
+				HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
+				IMap map = mock(IMap.class);
+				String cacheNameHash = String.valueOf("test".hashCode());
+				when(persistenceStorage.isEmpty(ConstructType.IMAP,cacheNameHash)).thenReturn(true);
+				when(map.getName()).thenReturn(cacheNameHash);
+				when(hazelcastInstance.getMap(cacheNameHash)).thenReturn(map);
+				utilMockedStatic.when(()->ExternalStorageUtil.initHZMapStorage(any(),any(),any(),any())).thenAnswer(invocation -> null).thenThrow(RuntimeException.class);
+				ConstructIMap<Document>  result = HazelcastMergeNode.checkBuildConstructIMap(hazelcastInstance,"test","test",mock(ExternalStorageDto.class));
+				Assertions.assertEquals(cacheNameHash,result.getName());
+			}
+		}
+
+		@Test
+		void test_oldConstructIMapIsNotEmpty(){
+			try(MockedStatic<ExternalStorageUtil> utilMockedStatic = mockStatic(ExternalStorageUtil.class);
+				MockedStatic<PersistenceStorage> persistenceStorageMockedStatic = mockStatic(PersistenceStorage.class)){
+				PersistenceStorage persistenceStorage = mock(PersistenceStorage.class);
+				persistenceStorageMockedStatic.when(PersistenceStorage::getInstance).thenReturn(persistenceStorage);
+				HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
+				String cacheNameHash = String.valueOf("test".hashCode());
+				when(persistenceStorage.isEmpty(ConstructType.IMAP,cacheNameHash)).thenReturn(true);
+				when(persistenceStorage.isEmpty(ConstructType.IMAP,"test")).thenReturn(false);
+				IMap map = mock(IMap.class);
+				when(map.getName()).thenReturn(cacheNameHash);
+				when(hazelcastInstance.getMap(cacheNameHash)).thenReturn(map);
+				IMap map2 = mock(IMap.class);
+				when(map2.getName()).thenReturn("test");
+				when(hazelcastInstance.getMap("test")).thenReturn(map2);
+				utilMockedStatic.when(()->ExternalStorageUtil.initHZMapStorage(any(),any(),any(),any())).thenAnswer(invocation -> null);
+				ConstructIMap<Document>  result = HazelcastMergeNode.checkBuildConstructIMap(hazelcastInstance,"test","test",mock(ExternalStorageDto.class));
+				Assertions.assertEquals("test",result.getName());
+			}
+		}
+
+		@Test
+		void test_oldConstructIMapIsEmpty(){
+			try(MockedStatic<ExternalStorageUtil> utilMockedStatic = mockStatic(ExternalStorageUtil.class);
+				MockedStatic<PersistenceStorage> persistenceStorageMockedStatic = mockStatic(PersistenceStorage.class)){
+				PersistenceStorage persistenceStorage = mock(PersistenceStorage.class);
+				persistenceStorageMockedStatic.when(PersistenceStorage::getInstance).thenReturn(persistenceStorage);
+				HazelcastInstance hazelcastInstance = mock(HazelcastInstance.class);
+				String cacheNameHash = String.valueOf("test".hashCode());
+				when(persistenceStorage.isEmpty(ConstructType.IMAP,cacheNameHash)).thenReturn(true);
+				when(persistenceStorage.isEmpty(ConstructType.IMAP,"test")).thenReturn(true);
+				IMap map = mock(IMap.class);
+				when(map.getName()).thenReturn(cacheNameHash);
+				when(hazelcastInstance.getMap(cacheNameHash)).thenReturn(map);
+				IMap map2 = mock(IMap.class);
+				when(map2.getName()).thenReturn("test");
+				when(hazelcastInstance.getMap("test")).thenReturn(map2);
+				utilMockedStatic.when(()->ExternalStorageUtil.initHZMapStorage(any(),any(),any(),any())).thenAnswer(invocation -> null);
+				ConstructIMap<Document>  result = HazelcastMergeNode.checkBuildConstructIMap(hazelcastInstance,"test","test",mock(ExternalStorageDto.class));
+				Assertions.assertEquals(cacheNameHash,result.getName());
+			}
+		}
+
+	}
 }
diff --git a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastProcessorBaseNodeTest.java b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastProcessorBaseNodeTest.java
index ddd09ccc3b..10aed48e1a 100644
--- a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastProcessorBaseNodeTest.java
+++ b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastProcessorBaseNodeTest.java
@@ -41,6 +41,7 @@
  * @Description
  * @create 2024-05-18 17:22
  **/
+@Disabled
 @DisplayName("Class HazelcastProcessorBaseNode Test")
 class HazelcastProcessorBaseNodeTest extends BaseHazelcastNodeTest {
 
diff --git a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastProcessorNodeTest.java b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastProcessorNodeTest.java
index 1ffe2b154c..4e9bb443ba 100644
--- a/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastProcessorNodeTest.java
+++ b/iengine/iengine-app/src/test/java/io/tapdata/flow/engine/V2/node/hazelcast/processor/HazelcastProcessorNodeTest.java
@@ -184,5 +184,38 @@ void test4() {
 			hazelcastProcessorNode.handleTransformToTapValueResult(tapdataEvent);
 			assertTrue(tapdataEvent.getTransformToTapValueResult().isEmpty());
 		}
+
+		@Test
+		@DisplayName("test rename when transform result not contains this field")
+		void test5() {
+			FieldProcessorNode.Operation rename = new FieldProcessorNode.Operation();
+			rename.setOp("RENAME");
+			rename.setField("field_old");
+			rename.setOperand("field_new");
+			List<FieldProcessorNode.Operation> operations = new ArrayList<>();
+			operations.add(rename);
+
+			FieldProcessorNode fieldProcessorNode = new FieldProcessorNode();
+			fieldProcessorNode.setOperations(operations);
+			when(dataProcessorContext.getNode()).thenReturn((Node) fieldProcessorNode);
+
+			TapdataEvent tapdataEvent = new TapdataEvent();
+			TransformToTapValueResult transformToTapValueResult = TransformToTapValueResult.create()
+					.beforeTransformedToTapValueFieldNames(new HashSet<String>() {{
+						add("field_remove");
+					}}).afterTransformedToTapValueFieldNames(new HashSet<String>() {{
+						add("field_remove");
+					}});
+			tapdataEvent.setTransformToTapValueResult(transformToTapValueResult);
+
+			hazelcastProcessorNode.handleTransformToTapValueResult(tapdataEvent);
+
+			Set<String> expect = new HashSet<String>() {{
+				add("field_remove");
+			}};
+
+			assertEquals(expect, tapdataEvent.getTransformToTapValueResult().getBeforeTransformedToTapValueFieldNames());
+			assertEquals(expect, tapdataEvent.getTransformToTapValueResult().getAfterTransformedToTapValueFieldNames());
+		}
 	}
 }
\ No newline at end of file
diff --git a/iengine/iengine-common/src/test/java/io/tapdata/log/CustomPatternLayoutTest.java b/iengine/iengine-common/src/test/java/io/tapdata/log/CustomPatternLayoutTest.java
new file mode 100644
index 0000000000..c666f78519
--- /dev/null
+++ b/iengine/iengine-common/src/test/java/io/tapdata/log/CustomPatternLayoutTest.java
@@ -0,0 +1,141 @@
+package io.tapdata.log;
+
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.core.layout.AbstractStringLayout;
+import org.apache.logging.log4j.core.layout.LevelPatternSelector;
+import org.apache.logging.log4j.core.layout.PatternMatch;
+import org.apache.logging.log4j.core.pattern.RegexReplacement;
+import org.apache.logging.log4j.message.SimpleMessage;
+import org.junit.jupiter.api.*;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+public class CustomPatternLayoutTest {
+
+    Log4jLogEvent log4jLogEvent;
+    String pattern = "[%-5level] %date{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n";
+    String message = "at org.tapdata.tapdata.tapdata.AbstractBufferingClientHttpRequest.tapdata(AbstractBufferingClientHttpRequest.tapdata:48)";
+    @BeforeEach
+    void before(){
+        Log4jLogEvent.Builder loggingEventBuilder = new Log4jLogEvent.Builder();
+        SimpleMessage simpleMessage = new SimpleMessage(message);
+        loggingEventBuilder.setMessage(simpleMessage);
+        loggingEventBuilder.setLevel(Level.INFO);
+        loggingEventBuilder.setLoggerName("testLoggerName");
+        log4jLogEvent = loggingEventBuilder.build();
+    }
+
+    @Nested
+    class NormalPatternTest {
+        AbstractStringLayout.Serializer customPatternLayout;
+        @BeforeEach
+        void beforeAll() {
+            CustomPatternLayout.SerializerBuilder serializerBuilder = new CustomPatternLayout.SerializerBuilder();
+            serializerBuilder.setPattern(pattern);
+            customPatternLayout = serializerBuilder.build();
+        }
+
+        @DisplayName("test normal pattern not oem Serializable")
+        @Test
+        void test1() {
+            StringBuilder stringBuilder = new StringBuilder();
+            StringBuilder convertString = customPatternLayout.toSerializable(log4jLogEvent, stringBuilder);
+            Assertions.assertEquals(true, convertString.toString().contains(message));
+        }
+
+        @DisplayName("test normal pattern oem Serializable")
+        @Test
+        void test2() {
+            Map<String, Object> oemMap = new HashMap<>();
+            oemMap.put("tapdata", "oem");
+            StringBuilder stringBuilder = new StringBuilder();
+            ReflectionTestUtils.setField(customPatternLayout, "oemConfigMap", oemMap);
+            StringBuilder convertString = customPatternLayout.toSerializable(log4jLogEvent, stringBuilder);
+            Assertions.assertEquals(false, convertString.toString().contains(message));
+        }
+
+        @DisplayName("test normal pattern replace Serializable")
+        @Test
+        void test3() {
+            RegexReplacement regexReplacement = RegexReplacement.createRegexReplacement(Pattern.compile("org"), "io");
+            CustomPatternLayout.SerializerBuilder serializerBuilder = new CustomPatternLayout.SerializerBuilder();
+            serializerBuilder.setPattern(pattern);
+            serializerBuilder.setReplace(regexReplacement);
+            AbstractStringLayout.Serializer customPatternLayout = serializerBuilder.build();
+            StringBuilder stringBuilder = new StringBuilder();
+            StringBuilder convertString = customPatternLayout.toSerializable(log4jLogEvent, stringBuilder);
+            Assertions.assertEquals(false, convertString.toString().contains(message));
+        }
+
+    }
+    @Nested
+    class PatternSelectTest{
+        LevelPatternSelector patternSelector;
+
+        @BeforeEach
+        void beforeAll() {
+            String infoPattern="[%-5level] %date{yyyy-MM-dd HH:mm:ss.SSS} %msg%n";
+            PatternMatch level = PatternMatch.newBuilder()
+                    .setPattern(infoPattern)
+                    .setKey("INFO").build();
+            PatternMatch[] patternMatchList = new PatternMatch[1];
+            patternMatchList[0] = level;
+            LevelPatternSelector.Builder patternSelectBuilder = LevelPatternSelector.newBuilder();
+            patternSelectBuilder.setProperties(patternMatchList);
+            patternSelectBuilder.setDefaultPattern(pattern);
+            patternSelector = patternSelectBuilder.build();
+        }
+        @DisplayName("test patternSelector not oem Serializable")
+        @Test
+        void test4() {
+            CustomPatternLayout.SerializerBuilder serializerBuilder = new CustomPatternLayout.SerializerBuilder();
+            serializerBuilder.setPatternSelector(patternSelector);
+            serializerBuilder.setDefaultPattern(pattern);
+
+            AbstractStringLayout.Serializer patternSelectLayout = serializerBuilder.build();
+            StringBuilder stringBuilder = new StringBuilder();
+            StringBuilder covertString = patternSelectLayout.toSerializable(log4jLogEvent, stringBuilder);
+
+            Assertions.assertEquals(true, covertString.toString().contains(message));
+        }
+        @DisplayName("test patternSelector oem Serializable")
+        @Test
+        void test5(){
+            CustomPatternLayout.SerializerBuilder serializerBuilder = new CustomPatternLayout.SerializerBuilder();
+            serializerBuilder.setPatternSelector(patternSelector);
+            serializerBuilder.setDefaultPattern(pattern);
+            AbstractStringLayout.Serializer patternSelectLayout = serializerBuilder.build();
+            Map<String, Object> oemMap = new HashMap<>();
+            oemMap.put("tapdata", "oem");
+            StringBuilder stringBuilder = new StringBuilder();
+
+            ReflectionTestUtils.setField(patternSelectLayout, "oemConfigMap", oemMap);
+            StringBuilder covertString = patternSelectLayout.toSerializable(log4jLogEvent, stringBuilder);
+            Assertions.assertEquals(false,covertString.toString().contains(message));
+        }
+        @DisplayName("test patternSelector oem Serializable and have replace")
+        @Test
+        void test6(){
+            RegexReplacement regexReplacement = RegexReplacement.createRegexReplacement(Pattern.compile("org"), "io");
+            CustomPatternLayout.SerializerBuilder serializerBuilder = new CustomPatternLayout.SerializerBuilder();
+            serializerBuilder.setPatternSelector(patternSelector);
+            serializerBuilder.setDefaultPattern(pattern);
+            serializerBuilder.setReplace(regexReplacement);
+            AbstractStringLayout.Serializer patternSelectLayout = serializerBuilder.build();
+            Map<String, Object> oemMap = new HashMap<>();
+            oemMap.put("tapdata", "oem");
+            StringBuilder stringBuilder = new StringBuilder();
+
+            ReflectionTestUtils.setField(patternSelectLayout, "oemConfigMap", oemMap);
+            StringBuilder covertString = patternSelectLayout.toSerializable(log4jLogEvent, stringBuilder);
+            Assertions.assertEquals(false,covertString.toString().contains(message));
+        }
+    }
+
+
+}
diff --git a/manager/tm-api/src/test/java/com/tapdata/tm/base/service/BaseServiceTest.java b/manager/tm-api/src/test/java/com/tapdata/tm/base/service/BaseServiceTest.java
new file mode 100644
index 0000000000..4b9ddac31f
--- /dev/null
+++ b/manager/tm-api/src/test/java/com/tapdata/tm/base/service/BaseServiceTest.java
@@ -0,0 +1,45 @@
+package com.tapdata.tm.base.service;
+
+import com.tapdata.tm.base.dto.Filter;
+import com.tapdata.tm.base.dto.TmPageable;
+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.mockito.Mockito;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author samuel
+ * @Description
+ * @create 2024-09-04 16:28
+ **/
+@DisplayName("Class BaseService Test")
+class BaseServiceTest {
+
+	private BaseService<?, ?, ?, ?> baseService;
+
+	@BeforeEach
+	void setUp() {
+		baseService = Mockito.mock(BaseService.class);
+	}
+
+	@Nested
+	@DisplayName("Method filterToTmPageable test")
+	class filterToTmPageableTest {
+		@Test
+		@DisplayName("test main process")
+		void test1() {
+			Filter filter = new Filter();
+			filter.setSkip(21);
+			filter.setLimit(10);
+			when(baseService.filterToTmPageable(any())).thenCallRealMethod();
+			TmPageable tmPageable = baseService.filterToTmPageable(filter);
+			assertEquals(3, tmPageable.getPage());
+			assertEquals(10, tmPageable.getSize());
+		}
+	}
+}
\ No newline at end of file
diff --git a/manager/tm-common/src/test/java/com/tapdata/tm/utils/DocumentUtilsTest.java b/manager/tm-common/src/test/java/com/tapdata/tm/utils/DocumentUtilsTest.java
new file mode 100644
index 0000000000..2157f8bbd2
--- /dev/null
+++ b/manager/tm-common/src/test/java/com/tapdata/tm/utils/DocumentUtilsTest.java
@@ -0,0 +1,61 @@
+package com.tapdata.tm.utils;
+
+import org.bson.Document;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author samuel
+ * @Description
+ * @create 2024-09-03 18:55
+ **/
+@DisplayName("Class DocumentUtils Test")
+class DocumentUtilsTest {
+	@Nested
+	@DisplayName("Method getLong test")
+	class getLongTest {
+		@Test
+		@DisplayName("test when value is long type")
+		void test1() {
+			Document document = new Document("test", 1L);
+			assertEquals(1L, DocumentUtils.getLong(document, "test"));
+		}
+
+		@Test
+		@DisplayName("test when value is integer type")
+		void test2() {
+			Document document = new Document("test", 1);
+			assertEquals(1L, DocumentUtils.getLong(document, "test"));
+		}
+
+		@Test
+		@DisplayName("test when value is null")
+		void test3() {
+			Document document = new Document("test", null);
+			assertEquals(0L, DocumentUtils.getLong(document, "test"));
+		}
+
+		@Test
+		@DisplayName("test when document is null")
+		void test4() {
+			assertEquals(0L, DocumentUtils.getLong(null, "test"));
+		}
+
+		@Test
+		@DisplayName("test when key is null")
+		void test5() {
+			Document document = new Document("test", 1L);
+			assertEquals(0L, DocumentUtils.getLong(document, null));
+		}
+
+		@Test
+		@DisplayName("test when value is string type")
+		void test6() {
+			Document document = new Document("test", "1");
+			assertEquals(0L, DocumentUtils.getLong(document, "test"));
+		}
+	}
+}
\ No newline at end of file
diff --git a/manager/tm-common/src/test/java/com/tapdata/tm/utils/EntityUtilsTest.java b/manager/tm-common/src/test/java/com/tapdata/tm/utils/EntityUtilsTest.java
new file mode 100644
index 0000000000..edfcd62546
--- /dev/null
+++ b/manager/tm-common/src/test/java/com/tapdata/tm/utils/EntityUtilsTest.java
@@ -0,0 +1,42 @@
+package com.tapdata.tm.utils;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * @author samuel
+ * @Description
+ * @create 2024-09-04 16:40
+ **/
+@DisplayName("Class EntityUtils Test")
+class EntityUtilsTest {
+	@Nested
+	@DisplayName("Method documentAnnotationValue test")
+	@Document("documentAnnotationValueTest")
+	class documentAnnotationValueTest {
+		@Test
+		@DisplayName("test main process")
+		void test1() {
+			assertEquals("documentAnnotationValueTest", EntityUtils.documentAnnotationValue(documentAnnotationValueTest.class));
+		}
+
+		@Test
+		@DisplayName("test input class is null")
+		void test2() {
+			IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> EntityUtils.documentAnnotationValue(null));
+			assertEquals("Input class is null", exception.getMessage());
+		}
+
+		@Test
+		@DisplayName("test class not have a org.springframework.data.mongodb.core.mapping.Document annotation")
+		void test3() {
+			IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> EntityUtils.documentAnnotationValue(EntityUtilsTest.class));
+			assertEquals("Class %s not have a org.springframework.data.mongodb.core.mapping.Document annotation", exception.getMessage());
+		}
+	}
+}
\ No newline at end of file
diff --git a/manager/tm-common/src/test/java/com/tapdata/tm/utils/OEMReplaceUtilTest.java b/manager/tm-common/src/test/java/com/tapdata/tm/utils/OEMReplaceUtilTest.java
new file mode 100644
index 0000000000..a30b2467f7
--- /dev/null
+++ b/manager/tm-common/src/test/java/com/tapdata/tm/utils/OEMReplaceUtilTest.java
@@ -0,0 +1,39 @@
+package com.tapdata.tm.utils;
+
+import lombok.SneakyThrows;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import java.io.InputStream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class OEMReplaceUtilTest {
+    @Nested
+    class getOEMConfigInputStreamTest {
+        @Test
+        @SneakyThrows
+        @DisplayName("test getOEMConfigInputStream method when configPath is null")
+        void test1(){
+            String fileName = "test";
+            try (MockedStatic<OEMReplaceUtil> mb = Mockito
+                    .mockStatic(OEMReplaceUtil.class)) {
+                mb.when(()->OEMReplaceUtil.getOEMConfigPath(fileName)).thenReturn(null);
+                InputStream actual = OEMReplaceUtil.getOEMConfigInputStream(fileName);
+                assertNull(actual);
+            }
+        }
+    }
+    @Nested
+    class getOEMConfigPathTest{
+        @Test
+        @DisplayName("test getOEMConfigPath method when oemType is null")
+        void test1(){
+            String fileName = "test";
+            String actual = OEMReplaceUtil.getOEMConfigPath(fileName);
+            assertNull(actual);
+        }
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/controller/ApiCallMinuteStatsController.java b/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/controller/ApiCallMinuteStatsController.java
new file mode 100644
index 0000000000..dab6abf54e
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/controller/ApiCallMinuteStatsController.java
@@ -0,0 +1,239 @@
+package com.tapdata.tm.apicallminutestats.controller;
+
+import com.tapdata.tm.base.controller.BaseController;
+import com.tapdata.tm.base.dto.*;
+import com.tapdata.tm.apicallminutestats.dto.ApiCallMinuteStatsDto;
+import com.tapdata.tm.apicallminutestats.service.ApiCallMinuteStatsService;
+import com.tapdata.tm.utils.MongoUtils;
+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 org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * @Date: 2024/08/29
+ * @Description:
+ */
+@Tag(name = "ApiCallMinuteStats", description = "ApiCallMinuteStats相关接口")
+@RestController
+@RequestMapping("/api/ApiCallMinuteStats")
+public class ApiCallMinuteStatsController extends BaseController {
+
+    @Autowired
+    private ApiCallMinuteStatsService apiCallMinuteStatsService;
+
+    /**
+     * Create a new instance of the model and persist it into the data source
+     * @param apiCallMinuteStats
+     * @return
+     */
+    @Operation(summary = "Create a new instance of the model and persist it into the data source")
+    @PostMapping
+    public ResponseMessage<ApiCallMinuteStatsDto> save(@RequestBody ApiCallMinuteStatsDto apiCallMinuteStats) {
+        apiCallMinuteStats.setId(null);
+        return success(apiCallMinuteStatsService.save(apiCallMinuteStats, getLoginUser()));
+    }
+
+    /**
+     *  Patch an existing model instance or insert a new one into the data source
+     * @param apiCallMinuteStats
+     * @return
+     */
+    @Operation(summary = "Patch an existing model instance or insert a new one into the data source")
+    @PatchMapping()
+    public ResponseMessage<ApiCallMinuteStatsDto> update(@RequestBody ApiCallMinuteStatsDto apiCallMinuteStats) {
+        return success(apiCallMinuteStatsService.save(apiCallMinuteStats, getLoginUser()));
+    }
+
+
+    /**
+     * Find all instances of the model matched by filter from the data source
+     * @param filterJson
+     * @return
+     */
+    @Operation(summary = "Find all instances of the model matched by filter from the data source")
+    @GetMapping
+    public ResponseMessage<Page<ApiCallMinuteStatsDto>> find(
+            @Parameter(in = ParameterIn.QUERY,
+                    description = "Filter defining fields, where, sort, skip, and limit - must be a JSON-encoded string (`{\"where\":{\"something\":\"value\"},\"fields\":{\"something\":true|false},\"sort\": [\"name desc\"],\"page\":1,\"size\":20}`)."
+            )
+            @RequestParam(value = "filter", required = false) String filterJson) {
+        Filter filter = parseFilter(filterJson);
+        if (filter == null) {
+            filter = new Filter();
+        }
+        return success(apiCallMinuteStatsService.find(filter, getLoginUser()));
+    }
+
+    /**
+     *  Replace an existing model instance or insert a new one into the data source
+     * @param apiCallMinuteStats
+     * @return
+     */
+    @Operation(summary = "Replace an existing model instance or insert a new one into the data source")
+    @PutMapping
+    public ResponseMessage<ApiCallMinuteStatsDto> put(@RequestBody ApiCallMinuteStatsDto apiCallMinuteStats) {
+        return success(apiCallMinuteStatsService.replaceOrInsert(apiCallMinuteStats, getLoginUser()));
+    }
+
+
+    /**
+     * Check whether a model instance exists in the data source
+     * @return
+     */
+    @Operation(summary = "Check whether a model instance exists in the data source")
+    @RequestMapping(value = "{id}", method = RequestMethod.HEAD)
+    public ResponseMessage<HashMap<String, Boolean>> checkById(@PathVariable("id") String id) {
+        long count = apiCallMinuteStatsService.count(Where.where("_id", MongoUtils.toObjectId(id)), getLoginUser());
+        HashMap<String, Boolean> existsValue = new HashMap<>();
+        existsValue.put("exists", count > 0);
+        return success(existsValue);
+    }
+
+    /**
+     *  Patch attributes for a model instance and persist it into the data source
+     * @param apiCallMinuteStats
+     * @return
+     */
+    @Operation(summary = "Patch attributes for a model instance and persist it into the data source")
+    @PatchMapping("{id}")
+    public ResponseMessage<ApiCallMinuteStatsDto> updateById(@PathVariable("id") String id, @RequestBody ApiCallMinuteStatsDto apiCallMinuteStats) {
+        apiCallMinuteStats.setId(MongoUtils.toObjectId(id));
+        return success(apiCallMinuteStatsService.save(apiCallMinuteStats, getLoginUser()));
+    }
+
+
+    /**
+     * Find a model instance by {{id}} from the data source
+     * @param fieldsJson
+     * @return
+     */
+    @Operation(summary = "Find a model instance by {{id}} from the data source")
+    @GetMapping("{id}")
+    public ResponseMessage<ApiCallMinuteStatsDto> findById(@PathVariable("id") String id,
+            @RequestParam(value = "fields", required = false) String fieldsJson) {
+        Field fields = parseField(fieldsJson);
+        return success(apiCallMinuteStatsService.findById(MongoUtils.toObjectId(id),  fields, getLoginUser()));
+    }
+
+    /**
+     *  Replace attributes for a model instance and persist it into the data source.
+     * @param apiCallMinuteStats
+     * @return
+     */
+    @Operation(summary = "Replace attributes for a model instance and persist it into the data source.")
+    @PutMapping("{id}")
+    public ResponseMessage<ApiCallMinuteStatsDto> replceById(@PathVariable("id") String id, @RequestBody ApiCallMinuteStatsDto apiCallMinuteStats) {
+        return success(apiCallMinuteStatsService.replaceById(MongoUtils.toObjectId(id), apiCallMinuteStats, getLoginUser()));
+    }
+
+    /**
+     *  Replace attributes for a model instance and persist it into the data source.
+     * @param apiCallMinuteStats
+     * @return
+     */
+    @Operation(summary = "Replace attributes for a model instance and persist it into the data source.")
+    @PostMapping("{id}/replace")
+    public ResponseMessage<ApiCallMinuteStatsDto> replaceById2(@PathVariable("id") String id, @RequestBody ApiCallMinuteStatsDto apiCallMinuteStats) {
+        return success(apiCallMinuteStatsService.replaceById(MongoUtils.toObjectId(id), apiCallMinuteStats, getLoginUser()));
+    }
+
+
+
+    /**
+     * Delete a model instance by {{id}} from the data source
+     * @param id
+     * @return
+     */
+    @Operation(summary = "Delete a model instance by {{id}} from the data source")
+    @DeleteMapping("{id}")
+    public ResponseMessage<Void> delete(@PathVariable("id") String id) {
+        apiCallMinuteStatsService.deleteById(MongoUtils.toObjectId(id), getLoginUser());
+        return success();
+    }
+
+    /**
+     *  Check whether a model instance exists in the data source
+     * @param id
+     * @return
+     */
+    @Operation(summary = "Check whether a model instance exists in the data source")
+    @GetMapping("{id}/exists")
+    public ResponseMessage<HashMap<String, Boolean>> checkById1(@PathVariable("id") String id) {
+        long count = apiCallMinuteStatsService.count(Where.where("_id", MongoUtils.toObjectId(id)), getLoginUser());
+        HashMap<String, Boolean> existsValue = new HashMap<>();
+        existsValue.put("exists", count > 0);
+        return success(existsValue);
+    }
+
+    /**
+     *  Count instances of the model matched by where from the data source
+     * @param whereJson
+     * @return
+     */
+    @Operation(summary = "Count instances of the model matched by where from the data source")
+    @GetMapping("count")
+    public ResponseMessage<HashMap<String, Long>> count(@RequestParam("where") String whereJson) {
+        Where where = parseWhere(whereJson);
+        if (where == null) {
+            where = new Where();
+        }
+        long count = apiCallMinuteStatsService.count(where, getLoginUser());
+        HashMap<String, Long> countValue = new HashMap<>();
+        countValue.put("count", count);
+        return success(countValue);
+    }
+
+    /**
+     *  Find first instance of the model matched by filter from the data source.
+     * @param filterJson
+     * @return
+     */
+    @Operation(summary = "Find first instance of the model matched by filter from the data source.")
+    @GetMapping("findOne")
+    public ResponseMessage<ApiCallMinuteStatsDto> findOne(
+            @Parameter(in = ParameterIn.QUERY,
+                    description = "Filter defining fields, where, sort, skip, and limit - must be a JSON-encoded string (`{\"where\":{\"something\":\"value\"},\"field\":{\"something\":true|false},\"sort\": [\"name desc\"],\"page\":1,\"size\":20}`)."
+            )
+            @RequestParam(value = "filter", required = false) String filterJson) {
+        Filter filter = parseFilter(filterJson);
+        if (filter == null) {
+            filter = new Filter();
+        }
+        return success(apiCallMinuteStatsService.findOne(filter, getLoginUser()));
+    }
+
+    /**
+     *  Update instances of the model matched by {{where}} from the data source.
+     * @param whereJson
+     * @return
+     */
+    @Operation(summary = "Update instances of the model matched by {{where}} from the data source")
+    @PostMapping("update")
+    public ResponseMessage<Map<String, Long>> updateByWhere(@RequestParam("where") String whereJson, @RequestBody ApiCallMinuteStatsDto apiCallMinuteStats) {
+        Where where = parseWhere(whereJson);
+        long count = apiCallMinuteStatsService.updateByWhere(where, apiCallMinuteStats, getLoginUser());
+        HashMap<String, Long> countValue = new HashMap<>();
+        countValue.put("count", count);
+        return success(countValue);
+    }
+
+    /**
+     *  Update an existing model instance or insert a new one into the data source based on the where criteria.
+     * @param whereJson
+     * @return
+     */
+    @Operation(summary = "Update an existing model instance or insert a new one into the data source based on the where criteria.")
+    @PostMapping("upsertWithWhere")
+    public ResponseMessage<ApiCallMinuteStatsDto> upsertByWhere(@RequestParam("where") String whereJson, @RequestBody ApiCallMinuteStatsDto apiCallMinuteStats) {
+        Where where = parseWhere(whereJson);
+        return success(apiCallMinuteStatsService.upsertByWhere(where, apiCallMinuteStats, getLoginUser()));
+    }
+
+}
\ No newline at end of file
diff --git a/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/dto/ApiCallMinuteStatsDto.java b/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/dto/ApiCallMinuteStatsDto.java
new file mode 100644
index 0000000000..df83907db9
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/dto/ApiCallMinuteStatsDto.java
@@ -0,0 +1,40 @@
+package com.tapdata.tm.apicallminutestats.dto;
+
+import com.tapdata.tm.commons.base.dto.BaseDto;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+
+/**
+ * ApiCallMinuteStats
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class ApiCallMinuteStatsDto extends BaseDto {
+	/**
+	 * Associated with {@link com.tapdata.tm.modules.entity.ModulesEntity}#id
+	 */
+	private String moduleId;
+	/**
+	 * API call time
+	 * Only keep year, month, day, hour, minute
+	 * Example: 2021-08-29T10:01:00.000Z
+	 */
+	private Date apiCallTime;
+	// Total number of response data rows accessed by API
+	private Long responseDataRowTotalCount = 0L;
+	// Total response time(milliseconds)
+	private Long totalResponseTime = 0L;
+	// Total size of data transferred by Api (Byte)
+	private Long transferDataTotalBytes = 0L;
+	// Response time per row: totalResponseTime / responseDataRowTotalCount
+	private Double responseTimePerRow = 0D;
+	// Number of rows per second: (transferDataTotalBytes / totalResponseTime) * 1000
+	private Double transferBytePerSecond = 0D;
+	/**
+	 * Associated with {@link com.tapdata.tm.apiCalls.entity.ApiCallEntity}#id
+	 */
+	private String lastApiCallId;
+}
\ No newline at end of file
diff --git a/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/entity/ApiCallMinuteStatsEntity.java b/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/entity/ApiCallMinuteStatsEntity.java
new file mode 100644
index 0000000000..86e3638790
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/entity/ApiCallMinuteStatsEntity.java
@@ -0,0 +1,42 @@
+package com.tapdata.tm.apicallminutestats.entity;
+
+import com.tapdata.tm.base.entity.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springframework.data.mongodb.core.mapping.Document;
+
+import java.util.Date;
+
+
+/**
+ * ApiCallMinuteStats
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+@Document("ApiCallMinuteStats")
+public class ApiCallMinuteStatsEntity extends BaseEntity {
+	/**
+	 * Associated with {@link com.tapdata.tm.modules.entity.ModulesEntity}#id
+	 */
+	private String moduleId;
+	/**
+	 * API call time
+	 * Only keep year, month, day, hour, minute
+	 * Example: 2021-08-29T10:01:00.000Z
+	 */
+	private Date apiCallTime;
+	// Total number of response data rows accessed by API
+	private Long responseDataRowTotalCount;
+	// Total response time(milliseconds)
+	private Long totalResponseTime;
+	// Total size of data transferred by Api (Byte)
+	private Long transferDataTotalBytes;
+	// Response time per row: totalResponseTime / responseDataRowTotalCount
+	private Double responseTimePerRow;
+	// Number of rows per second: (transferDataTotalBytes / totalResponseTime) * 1000
+	private Double transferBytePerSecond;
+	/**
+	 * Associated with {@link com.tapdata.tm.apiCalls.entity.ApiCallEntity}#id
+	 */
+	private String lastApiCallId;
+}
\ No newline at end of file
diff --git a/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/repository/ApiCallMinuteStatsRepository.java b/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/repository/ApiCallMinuteStatsRepository.java
new file mode 100644
index 0000000000..00281880f1
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/repository/ApiCallMinuteStatsRepository.java
@@ -0,0 +1,24 @@
+package com.tapdata.tm.apicallminutestats.repository;
+
+import com.tapdata.tm.base.reporitory.BaseRepository;
+import com.tapdata.tm.apicallminutestats.entity.ApiCallMinuteStatsEntity;
+import org.bson.types.ObjectId;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.stereotype.Repository;
+
+/**
+ * @Author:
+ * @Date: 2024/08/29
+ * @Description:
+ */
+@Repository
+public class ApiCallMinuteStatsRepository extends BaseRepository<ApiCallMinuteStatsEntity, ObjectId> {
+    public ApiCallMinuteStatsRepository(MongoTemplate mongoOperations) {
+        super(ApiCallMinuteStatsEntity.class, mongoOperations);
+    }
+
+    @Override
+    protected void init() {
+        super.init();
+    }
+}
diff --git a/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/service/ApiCallMinuteStatsService.java b/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/service/ApiCallMinuteStatsService.java
new file mode 100644
index 0000000000..6991067485
--- /dev/null
+++ b/manager/tm/src/main/java/com/tapdata/tm/apicallminutestats/service/ApiCallMinuteStatsService.java
@@ -0,0 +1,90 @@
+package com.tapdata.tm.apicallminutestats.service;
+
+import com.tapdata.tm.apicallminutestats.dto.ApiCallMinuteStatsDto;
+import com.tapdata.tm.apicallminutestats.entity.ApiCallMinuteStatsEntity;
+import com.tapdata.tm.apicallminutestats.repository.ApiCallMinuteStatsRepository;
+import com.tapdata.tm.base.service.BaseService;
+import com.tapdata.tm.config.security.UserDetail;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.bson.types.ObjectId;
+import org.springframework.data.mongodb.core.BulkOperations;
+import org.springframework.data.mongodb.core.query.Criteria;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * @Author:
+ * @Date: 2024/08/29
+ * @Description:
+ */
+@Service
+@Slf4j
+public class ApiCallMinuteStatsService extends BaseService<ApiCallMinuteStatsDto, ApiCallMinuteStatsEntity, ObjectId, ApiCallMinuteStatsRepository> {
+	public ApiCallMinuteStatsService(@NonNull ApiCallMinuteStatsRepository repository) {
+		super(repository, ApiCallMinuteStatsDto.class, ApiCallMinuteStatsEntity.class);
+	}
+
+	protected void beforeSave(ApiCallMinuteStatsDto apiCallMinuteStats, UserDetail user) {
+
+	}
+
+	public void merge(List<ApiCallMinuteStatsDto> apiCallMinuteStatsDtoList) {
+		// Just check and merge first record
+		if (apiCallMinuteStatsDtoList == null || apiCallMinuteStatsDtoList.isEmpty()) {
+			return;
+		}
+		ApiCallMinuteStatsDto apiCallMinuteStatsDto = apiCallMinuteStatsDtoList.get(0);
+		Query query = Query.query(Criteria.where("moduleId").is(apiCallMinuteStatsDto.getModuleId())
+				.and("apiCallTime").is(apiCallMinuteStatsDto.getApiCallTime()));
+		ApiCallMinuteStatsDto existsApiCallMinuteStats = findOne(query);
+		if (null != existsApiCallMinuteStats) {
+			apiCallMinuteStatsDto.setTotalResponseTime(apiCallMinuteStatsDto.getTotalResponseTime() + existsApiCallMinuteStats.getTotalResponseTime());
+			apiCallMinuteStatsDto.setResponseDataRowTotalCount(apiCallMinuteStatsDto.getResponseDataRowTotalCount() + existsApiCallMinuteStats.getResponseDataRowTotalCount());
+			apiCallMinuteStatsDto.setTransferDataTotalBytes(apiCallMinuteStatsDto.getTransferDataTotalBytes() + existsApiCallMinuteStats.getTransferDataTotalBytes());
+			// Recalculate responseTimePerRow and rowPerSecond
+			calculate(apiCallMinuteStatsDto);
+			apiCallMinuteStatsDto.setId(existsApiCallMinuteStats.getId());
+			apiCallMinuteStatsDto.setCreateAt(existsApiCallMinuteStats.getCreateAt());
+		}
+	}
+
+	public void calculate(ApiCallMinuteStatsDto apiCallMinuteStatsDto) {
+		if (null == apiCallMinuteStatsDto) {
+			return;
+		}
+		if (apiCallMinuteStatsDto.getResponseDataRowTotalCount() <= 0 || apiCallMinuteStatsDto.getTotalResponseTime() <= 0) {
+			return;
+		}
+		BigDecimal responseTimePerRow = BigDecimal.valueOf(apiCallMinuteStatsDto.getTotalResponseTime()).divide(BigDecimal.valueOf(apiCallMinuteStatsDto.getResponseDataRowTotalCount()), 2, RoundingMode.HALF_UP);
+		BigDecimal rowPerSecond = BigDecimal.valueOf(apiCallMinuteStatsDto.getTransferDataTotalBytes()).divide(BigDecimal.valueOf(apiCallMinuteStatsDto.getTotalResponseTime()), 2, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(1000));
+		apiCallMinuteStatsDto.setResponseTimePerRow(responseTimePerRow.doubleValue());
+		apiCallMinuteStatsDto.setTransferBytePerSecond(rowPerSecond.doubleValue());
+	}
+
+	public void bulkWrite(List<ApiCallMinuteStatsDto> apiCallMinuteStatsDtoList) {
+		if (null == apiCallMinuteStatsDtoList || apiCallMinuteStatsDtoList.isEmpty()) {
+			return;
+		}
+		BulkOperations bulkOperations = repository.bulkOperations(BulkOperations.BulkMode.UNORDERED);
+		List<ApiCallMinuteStatsEntity> apiCallMinuteStatsEntities = convertToEntity(ApiCallMinuteStatsEntity.class, apiCallMinuteStatsDtoList);
+		for (ApiCallMinuteStatsEntity apiCallMinuteStatsEntity : apiCallMinuteStatsEntities) {
+			Update update = repository.buildUpdateSet(apiCallMinuteStatsEntity);
+			Query query = Query.query(Criteria.where("id").is(apiCallMinuteStatsEntity.getId()));
+			bulkOperations.upsert(query, update);
+		}
+		bulkOperations.execute();
+	}
+
+	public boolean isEmpty() {
+		Query query = new Query().limit(1);
+		query.fields().include("_id");
+		return !repository.findOne(query).isPresent();
+	}
+}
\ No newline at end of file
diff --git a/manager/tm/src/test/java/com/tapdata/tm/Settings/service/SettingsServiceTest.java b/manager/tm/src/test/java/com/tapdata/tm/Settings/service/SettingsServiceTest.java
index f92f91cc17..3998ddfa4c 100644
--- a/manager/tm/src/test/java/com/tapdata/tm/Settings/service/SettingsServiceTest.java
+++ b/manager/tm/src/test/java/com/tapdata/tm/Settings/service/SettingsServiceTest.java
@@ -1,6 +1,9 @@
 package com.tapdata.tm.Settings.service;
 
+import com.tapdata.tm.Settings.constant.SettingUtil;
+import com.tapdata.tm.Settings.constant.SettingsEnum;
 import com.tapdata.tm.Settings.dto.MailAccountDto;
+import com.tapdata.tm.Settings.dto.SendMailResponseDto;
 import com.tapdata.tm.Settings.dto.SettingsDto;
 import com.tapdata.tm.Settings.dto.TestMailDto;
 import com.tapdata.tm.Settings.entity.Settings;
@@ -213,4 +216,22 @@ void testGetMailAccountWithoutProxy(){
             assertEquals(0, actual.getProxyPort());
         }
     }
+    @Nested
+    class testSendMailTest{
+        @Test
+        void testSendMailNormal(){
+            settingsService = mock(SettingsServiceImpl.class);
+            try (MockedStatic<SettingUtil> mb = Mockito
+                    .mockStatic(SettingUtil.class)) {
+                mb.when(()->SettingUtil.getValue(anyString(),anyString())).thenReturn("123456");
+                TestMailDto testMailDto = mock(TestMailDto.class);
+                MailAccountDto mailAccountDto = mock(MailAccountDto.class);
+                when(mailAccountDto.getPass()).thenReturn("*****");
+                when(settingsService.getMailAccount(testMailDto)).thenReturn(mailAccountDto);
+                doCallRealMethod().when(settingsService).testSendMail(testMailDto);
+                SendMailResponseDto actual = settingsService.testSendMail(testMailDto);
+                assertEquals(false,actual.isResult());
+            }
+        }
+    }
 }
diff --git a/manager/tm/src/test/java/com/tapdata/tm/apiCalls/service/ApiCallServiceTest.java b/manager/tm/src/test/java/com/tapdata/tm/apiCalls/service/ApiCallServiceTest.java
new file mode 100644
index 0000000000..4bf5f8ec47
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/apiCalls/service/ApiCallServiceTest.java
@@ -0,0 +1,324 @@
+package com.tapdata.tm.apiCalls.service;
+
+import com.mongodb.client.AggregateIterable;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoCursor;
+import com.tapdata.tm.apiCalls.entity.ApiCallEntity;
+import com.tapdata.tm.apicallminutestats.dto.ApiCallMinuteStatsDto;
+import com.tapdata.tm.apicallminutestats.service.ApiCallMinuteStatsService;
+import com.tapdata.tm.apicallstats.dto.ApiCallStatsDto;
+import com.tapdata.tm.apicallstats.service.ApiCallStatsService;
+import com.tapdata.tm.application.dto.ApplicationDto;
+import com.tapdata.tm.application.service.ApplicationService;
+import com.tapdata.tm.modules.service.ModulesService;
+import org.bson.Document;
+import org.bson.types.ObjectId;
+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.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 java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author samuel
+ * @Description
+ * @create 2024-09-03 14:36
+ **/
+@DisplayName("Class ApiCallService Test")
+class ApiCallServiceTest {
+
+	private ApiCallService apiCallService;
+	private ApiCallMinuteStatsService apiCallMinuteStatsService;
+	private MongoTemplate mongoTemplate;
+	private ModulesService modulesService;
+	private ApplicationService applicationService;
+	private ApiCallStatsService apiCallStatsService;
+
+	@BeforeEach
+	void setUp() {
+		apiCallService = new ApiCallService();
+		apiCallMinuteStatsService = mock(ApiCallMinuteStatsService.class);
+		ReflectionTestUtils.setField(apiCallService, "apiCallMinuteStatsService", apiCallMinuteStatsService);
+		mongoTemplate = mock(MongoTemplate.class);
+		ReflectionTestUtils.setField(apiCallService, "mongoOperations", mongoTemplate);
+		modulesService = mock(ModulesService.class);
+		ReflectionTestUtils.setField(apiCallService, "modulesService", modulesService);
+		applicationService = mock(ApplicationService.class);
+		ReflectionTestUtils.setField(apiCallService, "applicationService", applicationService);
+		apiCallStatsService = mock(ApiCallStatsService.class);
+		ReflectionTestUtils.setField(apiCallService, "apiCallStatsService", apiCallStatsService);
+	}
+
+	@Nested
+	@DisplayName("Method aggregateByAllPathId test")
+	class aggregateByAllPathIdTest {
+
+		private String allPathId;
+		private String lastApiCallId;
+
+		@BeforeEach
+		void setUp() {
+			allPathId = new ObjectId().toString();
+			lastApiCallId = new ObjectId().toString();
+		}
+
+		@Test
+		@DisplayName("test main process")
+		void test1() {
+			MongoCollection<Document> mongoCollection = mock(MongoCollection.class);
+			when(mongoTemplate.getCollection(anyString())).thenAnswer(invocationOnMock -> {
+				assertEquals(ApiCallEntity.class.getAnnotation(org.springframework.data.mongodb.core.mapping.Document.class).value(), invocationOnMock.getArgument(0));
+				return mongoCollection;
+			});
+			AggregateIterable<Document> aggregateIterable = mock(AggregateIterable.class);
+			when(mongoCollection.aggregate(anyList(), any(Class.class))).thenAnswer(invocationOnMock -> {
+				List<Document> pipeline = invocationOnMock.getArgument(0);
+				assertEquals("{\"$match\": {\"allPathId\": \"" + allPathId + "\", \"_id\": {\"$gt\": {\"$oid\": \"" + lastApiCallId + "\"}}}}", pipeline.get(0).toJson());
+				assertEquals("{\"$facet\": {\"callTotalCount\": [{\"$group\": {\"_id\": \"$allPathId\", \"data\": {\"$sum\": 1}}}], \"transferDataTotalBytes\": [{\"$group\": {\"_id\": \"$allPathId\", \"data\": {\"$sum\": \"$req_bytes\"}}}], \"callAlarmTotalCount\": [{\"$match\": {\"code\": {\"$ne\": \"200\"}}}, {\"$group\": {\"_id\": \"$allPathId\", \"data\": {\"$sum\": 1}}}], \"responseDataRowTotalCount\": [{\"$group\": {\"_id\": \"$allPathId\", \"data\": {\"$sum\": \"$res_rows\"}}}], \"totalResponseTime\": [{\"$group\": {\"_id\": \"$allPathId\", \"data\": {\"$sum\": \"$latency\"}}}], \"lastApiCallId\": [{\"$project\": {\"_id\": 1}}, {\"$sort\": {\"_id\": -1}}, {\"$limit\": 1}], \"maxResponseTime\": [{\"$group\": {\"_id\": \"$allPathId\", \"data\": {\"$max\": \"$latency\"}}}], \"clientIds\": [{\"$group\": {\"_id\": \"$user_info.clientId\"}}]}}", pipeline.get(1).toJson());
+				assertEquals(Document.class, invocationOnMock.getArgument(1));
+				return aggregateIterable;
+			});
+			when(aggregateIterable.allowDiskUse(anyBoolean())).thenAnswer(invocationOnMock -> {
+				assertEquals(true, invocationOnMock.getArgument(0));
+				return aggregateIterable;
+			});
+			MongoCursor<Document> mongoCursor = mock(MongoCursor.class);
+			when(aggregateIterable.iterator()).thenReturn(mongoCursor);
+			when(mongoCursor.hasNext()).thenReturn(true);
+			Document aggregateResult = new Document()
+					.append("callTotalCount", Arrays.asList(new Document("_id", allPathId).append("data", 132968)))
+					.append("transferDataTotalBytes", Arrays.asList(new Document("_id", allPathId).append("data", 666876690L)))
+					.append("callAlarmTotalCount", Arrays.asList(new Document("_id", allPathId).append("data", 61178)))
+					.append("responseDataRowTotalCount", Arrays.asList(new Document("_id", allPathId).append("data", 56052629L)))
+					.append("totalResponseTime", Arrays.asList(new Document("_id", allPathId).append("data", 53680329L)))
+					.append("lastApiCallId", Arrays.asList(new Document("_id", new ObjectId("66d6b3b69b37425548a8017a"))))
+					.append("maxResponseTime", Arrays.asList(new Document("_id", allPathId).append("data", 5119)))
+					.append("clientIds", Arrays.asList(new Document("_id", "client1")));
+			when(mongoCursor.next()).thenReturn(aggregateResult);
+
+			ApiCallStatsDto apiCallStatsDto = apiCallService.aggregateByAllPathId(allPathId, lastApiCallId);
+			assertEquals(allPathId, apiCallStatsDto.getModuleId());
+			assertEquals(132968L, apiCallStatsDto.getCallTotalCount());
+			assertEquals(666876690L, apiCallStatsDto.getTransferDataTotalBytes());
+			assertEquals(61178L, apiCallStatsDto.getCallAlarmTotalCount());
+			assertEquals(56052629L, apiCallStatsDto.getResponseDataRowTotalCount());
+			assertEquals(53680329L, apiCallStatsDto.getTotalResponseTime());
+			assertEquals(1, apiCallStatsDto.getClientIds().size());
+			assertEquals("client1", apiCallStatsDto.getClientIds().iterator().next());
+			assertEquals("66d6b3b69b37425548a8017a", apiCallStatsDto.getLastApiCallId());
+			assertEquals(5119L, apiCallStatsDto.getMaxResponseTime());
+		}
+	}
+
+	@Nested
+	@DisplayName("Method aggregateMinuteByAllPathId test")
+	class aggregateMinuteByAllPathIdTest {
+		private String allPathId;
+		private String lastApiCallId;
+		private Date startTime;
+
+		@BeforeEach
+		void setUp() {
+			allPathId = new ObjectId().toString();
+			lastApiCallId = new ObjectId().toString();
+			startTime = Date.from(ZonedDateTime.of(LocalDateTime.now().withNano(0), ZoneId.systemDefault()).toInstant());
+		}
+
+		@Test
+		@DisplayName("test main process")
+		void test1() {
+			MongoCollection<Document> mongoCollection = mock(MongoCollection.class);
+			when(mongoTemplate.getCollection(anyString())).thenAnswer(invocationOnMock -> {
+				assertEquals(ApiCallEntity.class.getAnnotation(org.springframework.data.mongodb.core.mapping.Document.class).value(), invocationOnMock.getArgument(0));
+				return mongoCollection;
+			});
+			AggregateIterable<Document> aggregateIterable = mock(AggregateIterable.class);
+			when(mongoCollection.aggregate(anyList(), any(Class.class))).thenAnswer(invocationOnMock -> {
+				List<Document> pipeline = invocationOnMock.getArgument(0);
+				System.out.println(pipeline.get(1).toJson());
+				assertEquals("{\"$match\": {\"allPathId\": \"" + allPathId + "\", \"_id\": {\"$gt\": {\"$oid\": \"" + lastApiCallId + "\"}}, \"createTime\": {\"$gte\": {\"$date\": \"" + startTime.toInstant().toString() + "\"}}}}", pipeline.get(0).toJson());
+				assertEquals("{\"$project\": {\"year\": {\"$year\": \"$createTime\"}, \"month\": {\"$month\": \"$createTime\"}, \"day\": {\"$dayOfMonth\": \"$createTime\"}, \"hour\": {\"$hour\": \"$createTime\"}, \"minute\": {\"$minute\": \"$createTime\"}, \"res_rows\": 1, \"latency\": 1, \"req_bytes\": 1}}", pipeline.get(1).toJson());
+				assertEquals("{\"$group\": {\"_id\": {\"year\": \"$year\", \"month\": \"$month\", \"day\": \"$day\", \"hour\": \"$hour\", \"minute\": \"$minute\"}, \"responseDataRowTotalCount\": {\"$sum\": \"$res_rows\"}, \"totalResponseTime\": {\"$sum\": \"$latency\"}, \"transferDataTotalBytes\": {\"$sum\": \"$req_bytes\"}, \"lastApiCallId\": {\"$last\": \"$_id\"}}}", pipeline.get(2).toJson());
+				assertEquals(Document.class, invocationOnMock.getArgument(1));
+				return aggregateIterable;
+			});
+			when(aggregateIterable.allowDiskUse(anyBoolean())).thenAnswer(invocationOnMock -> {
+				assertEquals(true, invocationOnMock.getArgument(0));
+				return aggregateIterable;
+			});
+			MongoCursor<Document> mongoCursor = mock(MongoCursor.class);
+			when(aggregateIterable.iterator()).thenReturn(mongoCursor);
+			List<Document> documents = new ArrayList<>();
+			documents.add(new Document("_id", new Document("year", 2024).append("month", 9).append("day", 3).append("hour", 6).append("minute", 58))
+					.append("responseDataRowTotalCount", 111078)
+					.append("totalResponseTime", 828882)
+					.append("transferDataTotalBytes", 159714)
+					.append("lastApiCallId", new ObjectId("66d6b3b39b37425548a7fb55")));
+			documents.add(new Document("_id", new Document("year", 2024).append("month", 9).append("day", 3).append("hour", 6).append("minute", 59))
+					.append("responseDataRowTotalCount", 127664)
+					.append("totalResponseTime", 636568)
+					.append("transferDataTotalBytes", 225802)
+					.append("lastApiCallId", new ObjectId("66d6b3b69b37425548a8017a")));
+			AtomicInteger countDown = new AtomicInteger();
+			AtomicInteger countUp = new AtomicInteger();
+			when(mongoCursor.hasNext()).thenAnswer(invocationOnMock -> countDown.getAndIncrement() <= 1);
+			when(mongoCursor.next()).thenAnswer(invocationOnMock -> documents.get(countUp.getAndIncrement()));
+
+			List<ApiCallMinuteStatsDto> apiCallMinuteStatsDtoList = apiCallService.aggregateMinuteByAllPathId(allPathId, lastApiCallId, startTime);
+
+			assertEquals(allPathId, apiCallMinuteStatsDtoList.get(0).getModuleId());
+			assertEquals(111078L, apiCallMinuteStatsDtoList.get(0).getResponseDataRowTotalCount());
+			assertEquals(828882L, apiCallMinuteStatsDtoList.get(0).getTotalResponseTime());
+			assertEquals(159714L, apiCallMinuteStatsDtoList.get(0).getTransferDataTotalBytes());
+			assertEquals("66d6b3b39b37425548a7fb55", apiCallMinuteStatsDtoList.get(0).getLastApiCallId());
+			Date apiCallTime = apiCallMinuteStatsDtoList.get(0).getApiCallTime();
+			LocalDateTime localDateTime = LocalDateTime.ofInstant(apiCallTime.toInstant(), ZoneId.of("UTC"));
+			assertEquals(2024, localDateTime.getYear());
+			assertEquals(9, localDateTime.getMonthValue());
+			assertEquals(3, localDateTime.getDayOfMonth());
+			assertEquals(6, localDateTime.getHour());
+			assertEquals(58, localDateTime.getMinute());
+			assertEquals(allPathId, apiCallMinuteStatsDtoList.get(1).getModuleId());
+			assertEquals(127664L, apiCallMinuteStatsDtoList.get(1).getResponseDataRowTotalCount());
+			assertEquals(636568L, apiCallMinuteStatsDtoList.get(1).getTotalResponseTime());
+			assertEquals(225802L, apiCallMinuteStatsDtoList.get(1).getTransferDataTotalBytes());
+			assertEquals("66d6b3b69b37425548a8017a", apiCallMinuteStatsDtoList.get(1).getLastApiCallId());
+			apiCallTime = apiCallMinuteStatsDtoList.get(1).getApiCallTime();
+			localDateTime = LocalDateTime.ofInstant(apiCallTime.toInstant(), ZoneId.of("UTC"));
+			assertEquals(2024, localDateTime.getYear());
+			assertEquals(9, localDateTime.getMonthValue());
+			assertEquals(3, localDateTime.getDayOfMonth());
+			assertEquals(6, localDateTime.getHour());
+			assertEquals(59, localDateTime.getMinute());
+		}
+	}
+
+	@Nested
+	@DisplayName("Method findClients test")
+	class findClientsTest {
+		@Test
+		@DisplayName("test main process")
+		void test1() {
+			List<String> moduleIdList = new ArrayList<String>() {{
+				add(new ObjectId().toString());
+				add(new ObjectId().toString());
+			}};
+			List<ApiCallStatsDto> apiCallStatsDtoList = new ArrayList<>();
+			apiCallStatsDtoList.add(new ApiCallStatsDto() {{
+				setClientIds(new HashSet<String>() {{
+					add(new ObjectId().toString());
+				}});
+			}});
+			apiCallStatsDtoList.add(new ApiCallStatsDto() {{
+				setClientIds(new HashSet<String>() {{
+					add(new ObjectId().toString());
+				}});
+			}});
+			when(apiCallStatsService.findAll(any(Query.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				assertEquals("{\"moduleId\": {\"$in\": [\"" + moduleIdList.get(0) + "\", \"" + moduleIdList.get(1) + "\"]}}", query.getQueryObject().toJson());
+				assertEquals("{\"clientIds\": 1}", query.getFieldsObject().toJson());
+				return apiCallStatsDtoList;
+			});
+			List<ApplicationDto> applicationDtoList = new ArrayList<>();
+			applicationDtoList.add(new ApplicationDto() {{
+				setId(new ObjectId(apiCallStatsDtoList.get(0).getClientIds().iterator().next()));
+				setName("app1");
+			}});
+			applicationDtoList.add(new ApplicationDto() {{
+				setId(new ObjectId(apiCallStatsDtoList.get(1).getClientIds().iterator().next()));
+				setName("app2");
+			}});
+			when(applicationService.findByIds(anyList())).thenAnswer(invocationOnMock -> {
+				List<String> ids = invocationOnMock.getArgument(0);
+				assertEquals(2, ids.size());
+				assertTrue(ids.contains(apiCallStatsDtoList.get(0).getClientIds().iterator().next()));
+				assertTrue(ids.contains(apiCallStatsDtoList.get(1).getClientIds().iterator().next()));
+				return applicationDtoList;
+			});
+
+			List<Map<String, String>> clients = apiCallService.findClients(moduleIdList);
+			Map<String, String> client = clients.get(0);
+			assertEquals("app1", client.get("name"));
+			assertEquals(applicationDtoList.get(0).getId().toString(), client.get("id"));
+			client = clients.get(1);
+			assertEquals("app2", client.get("name"));
+			assertEquals(applicationDtoList.get(1).getId().toString(), client.get("id"));
+		}
+
+		@Test
+		@DisplayName("test input module id list is empty")
+		void test2() {
+			List<String> moduleIdList = new ArrayList<>();
+			List<ApiCallStatsDto> apiCallStatsDtoList = new ArrayList<>();
+			apiCallStatsDtoList.add(new ApiCallStatsDto() {{
+				setClientIds(new HashSet<String>() {{
+					add(new ObjectId().toString());
+				}});
+			}});
+			apiCallStatsDtoList.add(new ApiCallStatsDto() {{
+				setClientIds(new HashSet<String>() {{
+					add(new ObjectId().toString());
+				}});
+			}});
+			when(apiCallStatsService.findAll(any(Query.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				assertTrue(query.getQueryObject().isEmpty());
+				assertEquals("{\"clientIds\": 1}", query.getFieldsObject().toJson());
+				return apiCallStatsDtoList;
+			});
+			List<ApplicationDto> applicationDtoList = new ArrayList<>();
+			applicationDtoList.add(new ApplicationDto() {{
+				setId(new ObjectId(apiCallStatsDtoList.get(0).getClientIds().iterator().next()));
+				setName("app1");
+			}});
+			applicationDtoList.add(new ApplicationDto() {{
+				setId(new ObjectId(apiCallStatsDtoList.get(1).getClientIds().iterator().next()));
+				setName("app2");
+			}});
+			when(applicationService.findByIds(anyList())).thenAnswer(invocationOnMock -> {
+				List<String> ids = invocationOnMock.getArgument(0);
+				assertEquals(2, ids.size());
+				assertTrue(ids.contains(apiCallStatsDtoList.get(0).getClientIds().iterator().next()));
+				assertTrue(ids.contains(apiCallStatsDtoList.get(1).getClientIds().iterator().next()));
+				return applicationDtoList;
+			});
+
+			List<Map<String, String>> clients = apiCallService.findClients(moduleIdList);
+			Map<String, String> client = clients.get(0);
+			assertEquals("app1", client.get("name"));
+			assertEquals(applicationDtoList.get(0).getId().toString(), client.get("id"));
+			client = clients.get(1);
+			assertEquals("app2", client.get("name"));
+			assertEquals(applicationDtoList.get(1).getId().toString(), client.get("id"));
+		}
+	}
+
+	@Test
+	@DisplayName("Method findOne(Query) test")
+	void testFindOne() {
+		Query inputQuery = Query.query(Criteria.where("_id").is(1));
+		ApiCallEntity apiCallEntity = new ApiCallEntity();
+		when(mongoTemplate.findOne(any(Query.class), any(Class.class))).thenAnswer(invocationOnMock -> {
+			Query query = invocationOnMock.getArgument(0);
+			assertSame(query, inputQuery);
+			assertEquals(ApiCallEntity.class, invocationOnMock.getArgument(1));
+			return apiCallEntity;
+		});
+
+		assertSame(apiCallEntity, apiCallService.findOne(inputQuery));
+	}
+}
\ No newline at end of file
diff --git a/manager/tm/src/test/java/com/tapdata/tm/apicallminutestats/service/ApiCallMinuteStatsServiceTest.java b/manager/tm/src/test/java/com/tapdata/tm/apicallminutestats/service/ApiCallMinuteStatsServiceTest.java
new file mode 100644
index 0000000000..22f99ca0fc
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/apicallminutestats/service/ApiCallMinuteStatsServiceTest.java
@@ -0,0 +1,176 @@
+package com.tapdata.tm.apicallminutestats.service;
+
+import com.tapdata.tm.apicallminutestats.dto.ApiCallMinuteStatsDto;
+import com.tapdata.tm.apicallminutestats.entity.ApiCallMinuteStatsEntity;
+import com.tapdata.tm.apicallminutestats.repository.ApiCallMinuteStatsRepository;
+import org.bson.types.ObjectId;
+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.springframework.data.mongodb.core.BulkOperations;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
+
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author samuel
+ * @Description
+ * @create 2024-09-03 18:59
+ **/
+@DisplayName("Class ApiCallMinuteStatsService Test")
+class ApiCallMinuteStatsServiceTest {
+
+	private ApiCallMinuteStatsRepository repository;
+	private ApiCallMinuteStatsService apiCallMinuteStatsService;
+
+	@BeforeEach
+	void setUp() {
+		repository = mock(ApiCallMinuteStatsRepository.class);
+		apiCallMinuteStatsService = new ApiCallMinuteStatsService(repository);
+		apiCallMinuteStatsService = spy(apiCallMinuteStatsService);
+	}
+
+	@Nested
+	@DisplayName("Method merge test")
+	class mergeTest {
+		@Test
+		@DisplayName("test main process")
+		void test1() {
+			Instant apiCallTime = ZonedDateTime.now().withSecond(0).withNano(0).toInstant();
+			List<ApiCallMinuteStatsDto> apiCallMinuteStatsDtoList = new ArrayList<>();
+			apiCallMinuteStatsDtoList.add(new ApiCallMinuteStatsDto() {{
+				setModuleId("1");
+				setApiCallTime(Date.from(apiCallTime));
+				setTotalResponseTime(1800L);
+				setResponseDataRowTotalCount(11L);
+				setTransferDataTotalBytes(134L);
+			}});
+			apiCallMinuteStatsDtoList.add(new ApiCallMinuteStatsDto() {{
+				setModuleId("2");
+				setApiCallTime(Date.from(apiCallTime.plusSeconds(60L)));
+				setTotalResponseTime(1000L);
+				setResponseDataRowTotalCount(10L);
+				setTransferDataTotalBytes(100L);
+			}});
+			ApiCallMinuteStatsDto existsApiCallMinuteStats = new ApiCallMinuteStatsDto() {{
+				setId(new ObjectId());
+				setCreateAt(new Date());
+				setModuleId("1");
+				setApiCallTime(Date.from(apiCallTime));
+				setTotalResponseTime(1500L);
+				setResponseDataRowTotalCount(16L);
+				setTransferDataTotalBytes(1234L);
+			}};
+			doAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				assertEquals("{\"moduleId\": \"1\", \"apiCallTime\": {\"$date\": \"" + apiCallTime + "\"}}", query.getQueryObject().toJson());
+				return existsApiCallMinuteStats;
+			}).when(apiCallMinuteStatsService).findOne(any(Query.class));
+
+			apiCallMinuteStatsService.merge(apiCallMinuteStatsDtoList);
+
+			assertEquals("1", apiCallMinuteStatsDtoList.get(0).getModuleId());
+			assertEquals(27L, apiCallMinuteStatsDtoList.get(0).getResponseDataRowTotalCount());
+			assertEquals(3300L, apiCallMinuteStatsDtoList.get(0).getTotalResponseTime());
+			assertEquals(1368L, apiCallMinuteStatsDtoList.get(0).getTransferDataTotalBytes());
+			assertEquals(122.22D, apiCallMinuteStatsDtoList.get(0).getResponseTimePerRow());
+			assertEquals(410D, apiCallMinuteStatsDtoList.get(0).getTransferBytePerSecond());
+			assertEquals(existsApiCallMinuteStats.getId(), apiCallMinuteStatsDtoList.get(0).getId());
+			assertEquals(existsApiCallMinuteStats.getCreateAt(), apiCallMinuteStatsDtoList.get(0).getCreateAt());
+			assertEquals("2", apiCallMinuteStatsDtoList.get(1).getModuleId());
+			assertEquals(10L, apiCallMinuteStatsDtoList.get(1).getResponseDataRowTotalCount());
+			assertEquals(1000L, apiCallMinuteStatsDtoList.get(1).getTotalResponseTime());
+			assertEquals(100L, apiCallMinuteStatsDtoList.get(1).getTransferDataTotalBytes());
+			assertEquals(0D, apiCallMinuteStatsDtoList.get(1).getResponseTimePerRow());
+			assertEquals(0D, apiCallMinuteStatsDtoList.get(1).getTransferBytePerSecond());
+		}
+
+		@Test
+		@DisplayName("test when apiCallMinuteStatsDtoList is null or empty list")
+		void test2() {
+			assertDoesNotThrow(() -> apiCallMinuteStatsService.merge(null));
+			assertDoesNotThrow(() -> apiCallMinuteStatsService.merge(new ArrayList<>()));
+		}
+	}
+
+	@Nested
+	@DisplayName("Method bulkWrite test")
+	class bulkWriteTest {
+		@Test
+		@DisplayName("test main process")
+		void test1() {
+			List<ApiCallMinuteStatsDto> apiCallMinuteStatsDtoList = new ArrayList<>();
+			apiCallMinuteStatsDtoList.add(new ApiCallMinuteStatsDto() {{
+				setId(new ObjectId());
+			}});
+			BulkOperations bulkOperations = mock(BulkOperations.class);
+			when(repository.bulkOperations(any(BulkOperations.BulkMode.class))).thenAnswer(invocationOnMock -> {
+				BulkOperations.BulkMode bulkMode = invocationOnMock.getArgument(0);
+				assertEquals(BulkOperations.BulkMode.UNORDERED, bulkMode);
+				return bulkOperations;
+			});
+			when(bulkOperations.upsert(any(Query.class), any(Update.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				Update update = invocationOnMock.getArgument(1);
+				assertEquals("{\"id\": {\"$oid\": \"" + apiCallMinuteStatsDtoList.get(0).getId() + "\"}}", query.getQueryObject().toJson());
+				assertEquals("{\"$set\": {\"test\": \"test\"}}", update.getUpdateObject().toJson());
+				return null;
+			});
+			when(repository.buildUpdateSet(any(ApiCallMinuteStatsEntity.class))).thenReturn(Update.update("test", "test"));
+
+			apiCallMinuteStatsService.bulkWrite(apiCallMinuteStatsDtoList);
+
+			verify(bulkOperations).execute();
+		}
+
+		@Test
+		@DisplayName("test when apiCallMinuteStatsDtoList is null or empty list")
+		void test2() {
+			BulkOperations bulkOperations = mock(BulkOperations.class);
+			when(repository.bulkOperations(any())).thenReturn(bulkOperations);
+			assertDoesNotThrow(() -> apiCallMinuteStatsService.bulkWrite(null));
+			assertDoesNotThrow(() -> apiCallMinuteStatsService.bulkWrite(new ArrayList<>()));
+			verify(bulkOperations, never()).upsert(any(Query.class), any(Update.class));
+			verify(bulkOperations, never()).execute();
+		}
+	}
+
+	@Nested
+	@DisplayName("Method isEmpty test")
+	class isEmptyTest {
+		@Test
+		@DisplayName("test main process")
+		void test1() {
+			when(repository.findOne(any(Query.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				assertEquals("{\"_id\": 1}", query.getFieldsObject().toJson());
+				assertEquals(1, query.getLimit());
+				return Optional.of(new ApiCallMinuteStatsEntity());
+			});
+			assertFalse(apiCallMinuteStatsService.isEmpty());
+		}
+
+		@Test
+		@DisplayName("test when findOne return null")
+		void test2() {
+			when(repository.findOne(any(Query.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				assertEquals("{\"_id\": 1}", query.getFieldsObject().toJson());
+				assertEquals(1, query.getLimit());
+				return Optional.empty();
+			});
+			assertTrue(apiCallMinuteStatsService.isEmpty());
+		}
+	}
+}
\ No newline at end of file
diff --git a/manager/tm/src/test/java/com/tapdata/tm/apicallstats/service/ApiCallStatsServiceTest.java b/manager/tm/src/test/java/com/tapdata/tm/apicallstats/service/ApiCallStatsServiceTest.java
new file mode 100644
index 0000000000..737a21d69d
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/apicallstats/service/ApiCallStatsServiceTest.java
@@ -0,0 +1,217 @@
+package com.tapdata.tm.apicallstats.service;
+
+import com.mongodb.client.AggregateIterable;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoCursor;
+import com.tapdata.tm.apicallminutestats.entity.ApiCallMinuteStatsEntity;
+import com.tapdata.tm.apicallstats.dto.ApiCallStatsDto;
+import com.tapdata.tm.apicallstats.entity.ApiCallStatsEntity;
+import com.tapdata.tm.apicallstats.repository.ApiCallStatsRepository;
+import org.bson.Document;
+import org.bson.types.ObjectId;
+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.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.query.Query;
+
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author samuel
+ * @Description
+ * @create 2024-09-04 10:44
+ **/
+@DisplayName("Class ApiCallStatsService Test")
+class ApiCallStatsServiceTest {
+
+	private ApiCallStatsRepository apiCallStatsRepository;
+	private ApiCallStatsService apiCallStatsService;
+
+	@BeforeEach
+	void setUp() {
+		apiCallStatsRepository = mock(ApiCallStatsRepository.class);
+		apiCallStatsService = new ApiCallStatsService(apiCallStatsRepository);
+	}
+
+	@Nested
+	@DisplayName("Method merge test")
+	class mergeTest {
+
+		private ApiCallStatsDto oldStats;
+		private ApiCallStatsDto newStats;
+		private ObjectId oldId;
+
+		@BeforeEach
+		void setUp() {
+			oldId = new ObjectId();
+			oldStats = new ApiCallStatsDto() {{
+				setId(oldId);
+				setCallTotalCount(100L);
+				setTransferDataTotalBytes(100L);
+				setCallAlarmTotalCount(10L);
+				setResponseDataRowTotalCount(10L);
+				setTotalResponseTime(1000L);
+				setClientIds(new HashSet<String>() {{
+					add("client1");
+				}});
+				setCreateAt(new Date());
+			}};
+			newStats = new ApiCallStatsDto() {{
+				setCallTotalCount(100L);
+				setTransferDataTotalBytes(100L);
+				setCallAlarmTotalCount(10L);
+				setResponseDataRowTotalCount(10L);
+				setTotalResponseTime(1000L);
+				setClientIds(new HashSet<String>() {{
+					add("client1");
+					add("client2");
+				}});
+				setCreateAt(new Date());
+			}};
+		}
+
+		@Test
+		@DisplayName("test new stats merge old stats")
+		void test1() {
+			apiCallStatsService.merge(oldStats, newStats);
+
+			assertEquals(oldId, newStats.getId());
+			assertEquals(200L, newStats.getCallTotalCount());
+			assertEquals(200L, newStats.getTransferDataTotalBytes());
+			assertEquals(20L, newStats.getCallAlarmTotalCount());
+			assertEquals(20L, newStats.getResponseDataRowTotalCount());
+			assertEquals(2000L, newStats.getTotalResponseTime());
+			assertEquals(2, newStats.getClientIds().size());
+			assertTrue(newStats.getClientIds().contains("client1"));
+			assertTrue(newStats.getClientIds().contains("client2"));
+			assertEquals(oldStats.getCreateAt(), newStats.getCreateAt());
+		}
+
+		@Test
+		@DisplayName("test old stats is null")
+		void test2() {
+			apiCallStatsService.merge(null, newStats);
+
+			assertNotNull(newStats.getId());
+			assertEquals(100L, newStats.getCallTotalCount());
+			assertEquals(100L, newStats.getTransferDataTotalBytes());
+			assertEquals(10L, newStats.getCallAlarmTotalCount());
+			assertEquals(10L, newStats.getResponseDataRowTotalCount());
+			assertEquals(1000L, newStats.getTotalResponseTime());
+			assertEquals(2, newStats.getClientIds().size());
+			assertTrue(newStats.getClientIds().contains("client1"));
+			assertTrue(newStats.getClientIds().contains("client2"));
+			assertNotNull(newStats.getCreateAt());
+		}
+
+		@Test
+		@DisplayName("test input two parameters all null")
+		void test3() {
+			assertDoesNotThrow(() -> apiCallStatsService.merge(null, null));
+		}
+	}
+
+	@Nested
+	@DisplayName("Method aggregateByUserId test")
+	class aggregateByUserIdTest {
+		@BeforeEach
+		void setUp() {
+
+		}
+
+		@Test
+		@DisplayName("test main process")
+		void test1() {
+			String userId = new ObjectId().toString();
+			MongoCollection<Document> mongoCollection = mock(MongoCollection.class);
+			MongoTemplate mongoTemplate = mock(MongoTemplate.class);
+			when(mongoTemplate.getCollection(anyString())).thenAnswer(invocationOnMock -> {
+				assertEquals(ApiCallStatsEntity.class.getAnnotation(org.springframework.data.mongodb.core.mapping.Document.class).value(), invocationOnMock.getArgument(0));
+				return mongoCollection;
+			});
+			when(apiCallStatsRepository.getMongoOperations()).thenReturn(mongoTemplate);
+			MongoCursor<Document> mongoCursor = mock(MongoCursor.class);
+			AggregateIterable<Document> aggregateIterable = mock(AggregateIterable.class);
+			when(aggregateIterable.iterator()).thenReturn(mongoCursor);
+			when(mongoCollection.aggregate(anyList())).thenAnswer(invocationOnMock -> {
+				List<Document> pipeline = invocationOnMock.getArgument(0);
+				assertEquals("{\"$match\": {\"user_id\": \"" + userId + "\"}}", pipeline.get(0).toJson());
+				assertEquals("{\"$facet\": {\"callTotalCount\": [{\"$group\": {\"_id\": null, \"data\": {\"$sum\": \"$callTotalCount\"}}}], \"transferDataTotalBytes\": [{\"$group\": {\"_id\": null, \"data\": {\"$sum\": \"$transferDataTotalBytes\"}}}], \"callAlarmTotalCount\": [{\"$group\": {\"_id\": null, \"data\": {\"$sum\": \"$callAlarmTotalCount\"}}}], \"responseDataRowTotalCount\": [{\"$group\": {\"_id\": \"$allPathId\", \"data\": {\"$sum\": \"$responseDataRowTotalCount\"}}}], \"totalResponseTime\": [{\"$group\": {\"_id\": \"$allPathId\", \"data\": {\"$sum\": \"$totalResponseTime\"}}}], \"alarmApiTotalCount\": [{\"$match\": {\"accessFailureRate\": {\"$gt\": 0}}}, {\"$group\": {\"_id\": null, \"data\": {\"$sum\": 1}}}], \"lastUpdAt\": [{\"$group\": {\"_id\": null, \"data\": {\"$max\": \"$last_updated\"}}}]}}", pipeline.get(1).toJson());
+				return aggregateIterable;
+			});
+			when(aggregateIterable.allowDiskUse(anyBoolean())).thenAnswer(invocationOnMock -> {
+				assertEquals(true, invocationOnMock.getArgument(0));
+				return aggregateIterable;
+			});
+			when(mongoCursor.hasNext()).thenReturn(true);
+			Document mockResult = new Document("callTotalCount", Arrays.asList(new Document("_id", null).append("data", 4226845L)))
+					.append("transferDataTotalBytes", Arrays.asList(new Document("_id", null).append("data", 309468035240L)))
+					.append("callAlarmTotalCount", Arrays.asList(new Document("_id", null).append("data", 1926396L)))
+					.append("responseDataRowTotalCount", Arrays.asList(new Document("_id", null).append("data", 1887940806L)))
+					.append("totalResponseTime", Arrays.asList(new Document("_id", null).append("data", 12986886132L)))
+					.append("alarmApiTotalCount", Arrays.asList(new Document("_id", null).append("data", 32)));
+			when(mongoCursor.next()).thenReturn(mockResult);
+
+			ApiCallStatsDto apiCallStatsDto = apiCallStatsService.aggregateByUserId(userId);
+
+			assertEquals(4226845L, apiCallStatsDto.getCallTotalCount());
+			assertEquals(309468035240L, apiCallStatsDto.getTransferDataTotalBytes());
+			assertEquals(1926396L, apiCallStatsDto.getCallAlarmTotalCount());
+			assertEquals(1887940806L, apiCallStatsDto.getResponseDataRowTotalCount());
+			assertEquals(12986886132L, apiCallStatsDto.getTotalResponseTime());
+			assertEquals(32L, apiCallStatsDto.getAlarmApiTotalCount());
+		}
+	}
+
+	@Nested
+	@DisplayName("Method isEmpty test")
+	class isEmptyTest {
+		@Test
+		@DisplayName("test main process")
+		void test1() {
+			when(apiCallStatsRepository.findOne(any(Query.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				assertEquals("{\"_id\": 1}", query.getFieldsObject().toJson());
+				assertEquals(1, query.getLimit());
+				return Optional.of(new ApiCallMinuteStatsEntity());
+			});
+			assertFalse(apiCallStatsService.isEmpty());
+		}
+
+		@Test
+		@DisplayName("test when findOne return null")
+		void test2() {
+			when(apiCallStatsRepository.findOne(any(Query.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				assertEquals("{\"_id\": 1}", query.getFieldsObject().toJson());
+				assertEquals(1, query.getLimit());
+				return Optional.empty();
+			});
+			assertTrue(apiCallStatsService.isEmpty());
+		}
+	}
+
+	@Nested
+	@DisplayName("Method deleteAllByModuleId test")
+	class deleteAllByModuleIdTest {
+		@Test
+		@DisplayName("test main process")
+		void test1() {
+			String moduleId = new ObjectId().toString();
+			apiCallStatsService = spy(apiCallStatsService);
+			doAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				System.out.println(query.getQueryObject().toJson());
+				return 1L;
+			}).when(apiCallStatsService).deleteAll(any(Query.class));
+
+			apiCallStatsService.deleteAllByModuleId(moduleId);
+		}
+	}
+}
\ No newline at end of file
diff --git a/manager/tm/src/test/java/com/tapdata/tm/ds/service/impl/DataSourceServiceTest.java b/manager/tm/src/test/java/com/tapdata/tm/ds/service/impl/DataSourceServiceTest.java
index 1c9b9d266a..9f7c838099 100644
--- a/manager/tm/src/test/java/com/tapdata/tm/ds/service/impl/DataSourceServiceTest.java
+++ b/manager/tm/src/test/java/com/tapdata/tm/ds/service/impl/DataSourceServiceTest.java
@@ -11,9 +11,11 @@
 import com.tapdata.tm.permissions.service.DataPermissionService;
 import io.tapdata.pdk.apis.entity.Capability;
 import io.tapdata.pdk.apis.entity.ConnectionOptions;
+import org.bson.Document;
 import org.bson.types.ObjectId;
 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.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
@@ -28,8 +30,8 @@
 import java.util.*;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
+
 @ExtendWith(MockitoExtension.class)
 class DataSourceServiceTest {
     private DataSourceService dataSourceServiceUnderTest;
@@ -44,21 +46,30 @@ void setUp() {
         dataSourceServiceUnderTest = new DataSourceServiceImpl(dataSourceRepository);
         ReflectionTestUtils.setField(dataSourceServiceUnderTest,"dataSourceDefinitionService",dataSourceDefinitionService);
     }
-
+    @Nested
+    class updateConnectionOptionsTest{
+        private ObjectId id;
+        private DataSourceEntity dataSourceEntity;
+        private UserDetail user;
+        private Query query;
+        private ConnectionOptions options;
+        @BeforeEach
+        void beforeEach(){
+            id = new ObjectId(new GregorianCalendar(2020, Calendar.JANUARY, 1).getTime(), 0);
+            options = new ConnectionOptions();
+            options.setDbVersion("dbVersion");
+            options.setTimeDifference(1000L);
+            user = new UserDetail("userId", "customerId", "username", "password", "customerType",
+                    "accessCode", false, false, false, false, Arrays.asList(new SimpleGrantedAuthority("role")));
+            Criteria criteria = Criteria.where("_id").is(id);
+            query = new Query(criteria);
+            query.fields().include("_id", "database_type", "encryptConfig");
+            dataSourceEntity = new DataSourceEntity();
+            dataSourceEntity.setDb_version("test");
+            dataSourceEntity.setDatabase_type("mongo");
+        }
     @Test
     void testUpdateConnectionOptions() {
-        final ObjectId id = new ObjectId(new GregorianCalendar(2020, Calendar.JANUARY, 1).getTime(), 0);
-        final ConnectionOptions options = new ConnectionOptions();
-        options.setDbVersion("dbVersion");
-        options.setTimeDifference(1000L);
-        final UserDetail user = new UserDetail("userId", "customerId", "username", "password", "customerType",
-                "accessCode", false, false, false, false, Arrays.asList(new SimpleGrantedAuthority("role")));
-        Criteria criteria = Criteria.where("_id").is(id);
-        Query query = new Query(criteria);
-        query.fields().include("_id", "database_type");
-        DataSourceEntity dataSourceEntity = new DataSourceEntity();
-        dataSourceEntity.setDb_version("test");
-        dataSourceEntity.setDatabase_type("mongo");
         DataSourceDefinitionDto definitionDto = new DataSourceDefinitionDto();
         definitionDto.setCapabilities(Arrays.asList(new Capability("id")));
         Update expect = new Update();
@@ -77,6 +88,34 @@ void testUpdateConnectionOptions() {
             dataSourceServiceUnderTest.updateConnectionOptions(id, options, user);
         }
     }
+    @Test
+    void testUpdateConnectionOptionsWithInstanceInfo() {
+        Map<String, String> datasourceInstanceInfo = new HashMap<>();
+        datasourceInstanceInfo.put("id","7e62e76bb5701454987485b28a70eecc");
+        datasourceInstanceInfo.put("tag","127.0.0.1:3306");
+        options.setDatasourceInstanceInfo(datasourceInstanceInfo);
+        Map<String, Object> config = new HashMap<>();
+        config.put("datasourceInstanceId","7e62e76bb5701454987485b28a70eecc");
+        dataSourceEntity.setConfig(config);
+        DataSourceDefinitionDto definitionDto = new DataSourceDefinitionDto();
+        definitionDto.setCapabilities(Arrays.asList(new Capability("id")));
+       try (MockedStatic<DataPermissionService> serviceMockedStatic = Mockito.mockStatic(DataPermissionService.class)){
+            serviceMockedStatic.when(DataPermissionService::isCloud).thenReturn(true);
+            when(dataSourceRepository.findOne(query,user)).thenReturn(Optional.of(dataSourceEntity));
+            when(dataSourceDefinitionService.getByDataSourceType(dataSourceEntity.getDatabase_type(),user)).thenReturn(definitionDto);
+            when(dataSourceRepository.updateFirstNotChangeLast(any(),any(),any())).thenAnswer(invocationOnMock -> {
+                Update result = invocationOnMock.getArgument(1, Update.class);
+                Document updateObject = result.getUpdateObject();
+                Document set = (Document) updateObject.get("$set");
+                Assertions.assertNotNull(set.get("datasourceInstanceTag"));
+                Assertions.assertNotNull(set.get("encryptConfig"));
+                return null;
+            });
+            doCallRealMethod().when(dataSourceRepository).encryptConfig(any(DataSourceEntity.class));
+            dataSourceServiceUnderTest.updateConnectionOptions(id, options, user);
+        }
+    }
+    }
 
     @Test
     void testUpdateConnectionOptions_dbVersoinNull() {
@@ -86,7 +125,7 @@ void testUpdateConnectionOptions_dbVersoinNull() {
                 "accessCode", false, false, false, false, Arrays.asList(new SimpleGrantedAuthority("role")));
         Criteria criteria = Criteria.where("_id").is(id);
         Query query = new Query(criteria);
-        query.fields().include("_id", "database_type");
+        query.fields().include("_id", "database_type", "encryptConfig");
         DataSourceEntity dataSourceEntity = new DataSourceEntity();
         dataSourceEntity.setDb_version("test");
         dataSourceEntity.setDatabase_type("mongo");
diff --git a/manager/tm/src/test/java/com/tapdata/tm/init/InitLogMapTest.java b/manager/tm/src/test/java/com/tapdata/tm/init/InitLogMapTest.java
new file mode 100644
index 0000000000..80cdbe9b11
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/init/InitLogMapTest.java
@@ -0,0 +1,24 @@
+package com.tapdata.tm.init;
+
+import com.tapdata.tm.utils.OEMReplaceUtil;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+public class InitLogMapTest {
+    @DisplayName("test log not oem start banner")
+    @Test
+    void test1(){
+        assertDoesNotThrow(() -> {InitLogMap.complete(PatchesRunner.class);});
+    }
+    @DisplayName("test log oem start banner")
+    @Test
+    void test2(){
+        try (MockedStatic<OEMReplaceUtil> oem = org.mockito.Mockito.mockStatic(OEMReplaceUtil.class)) {
+            oem.when(()->OEMReplaceUtil.oemType()).thenReturn("data");
+            assertDoesNotThrow(() -> {InitLogMap.complete(PatchesRunner.class);});
+        }
+    }
+}
diff --git a/manager/tm/src/test/java/com/tapdata/tm/lock/aop/LockAopTest.java b/manager/tm/src/test/java/com/tapdata/tm/lock/aop/LockAopTest.java
new file mode 100644
index 0000000000..569f356326
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/lock/aop/LockAopTest.java
@@ -0,0 +1,102 @@
+package com.tapdata.tm.lock.aop;
+
+import com.tapdata.tm.base.exception.BizException;
+import com.tapdata.tm.lock.annotation.Lock;
+import com.tapdata.tm.lock.constant.LockType;
+import com.tapdata.tm.lock.service.LockService;
+import lombok.SneakyThrows;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.reflect.MethodSignature;
+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.mockito.internal.verification.Times;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.*;
+
+public class LockAopTest {
+    private LockAop lockAop;
+    private LockService lockService;
+
+    @BeforeEach
+    void beforeEach() {
+        lockAop = mock(LockAop.class);
+        lockService = mock(LockService.class);
+        ReflectionTestUtils.setField(lockAop, "lockService", lockService);
+    }
+
+    @Nested
+    class lockTest {
+        private ProceedingJoinPoint pjp;
+        private MethodSignature methodSignature;
+
+        @BeforeEach
+        @SneakyThrows
+        void beforeEach() {
+            pjp = mock(ProceedingJoinPoint.class);
+            methodSignature = mock(MethodSignature.class);
+            String[] parameters = {"taskId"};
+            when(methodSignature.getParameterNames()).thenReturn(parameters);
+            when((MethodSignature) pjp.getSignature()).thenReturn(methodSignature);
+            when(pjp.getArgs()).thenReturn(parameters);
+            doCallRealMethod().when(lockAop).lock(pjp);
+        }
+
+        @Lock(value = "taskId", type = LockType.PIPELINE_LIMIT)
+        void test1Method() {
+        }
+
+        @Test
+        @SneakyThrows
+        @DisplayName("test lock method when value is not empty")
+        void test1() {
+            when(methodSignature.getMethod()).thenReturn(getClass().getDeclaredMethod("test1Method"));
+            when(lockService.lock("PIPELINE_LIMIT_taskId", 10, 50)).thenReturn(true);
+            lockAop.lock(pjp);
+            verify(lockService, new Times(1)).lock("PIPELINE_LIMIT_taskId", 10, 50);
+        }
+
+        @Lock(value = "test", type = LockType.PIPELINE_LIMIT, valueType = Lock.CUSTOM)
+        void test2Method() {
+        }
+
+        @Test
+        @SneakyThrows
+        @DisplayName("test lock method when custom is not empty")
+        void test2() {
+            when(methodSignature.getMethod()).thenReturn(getClass().getDeclaredMethod("test2Method"));
+            when(lockService.lock("PIPELINE_LIMIT_test", 10, 50)).thenReturn(true);
+            lockAop.lock(pjp);
+            verify(lockService, new Times(1)).lock("PIPELINE_LIMIT_test", 10, 50);
+        }
+
+        @Lock(value = "", type = LockType.PIPELINE_LIMIT, valueType = Lock.CUSTOM)
+        void test3Method() {
+        }
+        @Test
+        @SneakyThrows
+        @DisplayName("test lock method when custom and value is empty")
+        void test3() {
+            when(methodSignature.getMethod()).thenReturn(getClass().getDeclaredMethod("test3Method"));
+            assertThrows(BizException.class, () -> lockAop.lock(pjp));
+            verify(lockService, new Times(0)).lock(anyString(), anyInt(), anyInt());
+        }
+
+        @Lock(value = "taskId", type = LockType.PIPELINE_LIMIT, valueType = Lock.CUSTOM)
+        void test4Method() {
+        }
+        @Test
+        @SneakyThrows
+        @DisplayName("test lock method when paramIndex is -1")
+        void test4() {
+            when(methodSignature.getMethod()).thenReturn(getClass().getDeclaredMethod("test4Method"));
+            String[] parameters = {};
+            when(methodSignature.getParameterNames()).thenReturn(parameters);
+            assertThrows(BizException.class, () -> lockAop.lock(pjp));
+            verify(lockService, new Times(0)).lock(anyString(), anyInt(), anyInt());
+        }
+    }
+}
diff --git a/manager/tm/src/test/java/com/tapdata/tm/modules/service/ModulesServiceTest.java b/manager/tm/src/test/java/com/tapdata/tm/modules/service/ModulesServiceTest.java
index de16b2230c..81d678385f 100644
--- a/manager/tm/src/test/java/com/tapdata/tm/modules/service/ModulesServiceTest.java
+++ b/manager/tm/src/test/java/com/tapdata/tm/modules/service/ModulesServiceTest.java
@@ -1,5 +1,18 @@
 package com.tapdata.tm.modules.service;
 
+import com.mongodb.client.AggregateIterable;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoCursor;
+import com.tapdata.tm.apiCalls.entity.ApiCallEntity;
+import com.tapdata.tm.apiCalls.service.ApiCallService;
+import com.tapdata.tm.apicallminutestats.dto.ApiCallMinuteStatsDto;
+import com.tapdata.tm.apicallminutestats.service.ApiCallMinuteStatsService;
+import com.tapdata.tm.apicallstats.dto.ApiCallStatsDto;
+import com.tapdata.tm.apicallstats.service.ApiCallStatsService;
+import com.tapdata.tm.application.dto.ApplicationDto;
+import com.tapdata.tm.application.service.ApplicationService;
+import com.tapdata.tm.base.dto.Filter;
+import com.tapdata.tm.base.dto.Page;
 import com.tapdata.tm.base.exception.BizException;
 import com.tapdata.tm.commons.schema.Field;
 import com.tapdata.tm.commons.schema.Tag;
@@ -9,25 +22,38 @@
 import com.tapdata.tm.modules.dto.ModulesTagsDto;
 import com.tapdata.tm.modules.entity.ModulesEntity;
 import com.tapdata.tm.modules.entity.Path;
+import com.tapdata.tm.modules.param.ApiDetailParam;
 import com.tapdata.tm.modules.repository.ModulesRepository;
+import com.tapdata.tm.modules.vo.ApiDetailVo;
+import com.tapdata.tm.modules.vo.ApiListVo;
+import com.tapdata.tm.modules.vo.PreviewVo;
+import com.tapdata.tm.modules.vo.RankListsVo;
+import org.bson.Document;
+import org.bson.types.ObjectId;
 import org.junit.jupiter.api.*;
 import org.mockito.internal.verification.Times;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.test.util.ReflectionTestUtils;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.*;
 
-import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Mockito.*;
 
-public class ModulesServiceTest {
+@DisplayName("Class ModulesService Test")
+class ModulesServiceTest {
     ModulesService modulesService;
     ModulesRepository modulesRepository;
+
     @BeforeEach
     void init(){
         modulesRepository = mock(ModulesRepository.class);
         modulesService = new ModulesService(modulesRepository);
     }
+
     @Nested
     class UpdatePermissionsTest{
         @Test
@@ -67,6 +93,7 @@ void test_tagsIsNull(){
         }
 
     }
+
     @Nested
     class BeforeSaveTest{
         @Test
@@ -84,6 +111,7 @@ void test_main(){
             modulesService.beforeSave(modules,mock(UserDetail.class));
             Assertions.assertEquals(1,modules.getPaths().get(0).getFields().size());
         }
+
         @Test
         void test_paths_isNull(){
             ModulesDto modules = new ModulesDto();
@@ -92,6 +120,7 @@ void test_paths_isNull(){
             modulesService.beforeSave(modules,mock(UserDetail.class));
             Assertions.assertEquals(0,modules.getPaths().size());
         }
+
         @Test
         void test_paths_fields_isNull(){
             ModulesDto modules = new ModulesDto();
@@ -105,16 +134,19 @@ void test_paths_fields_isNull(){
             Assertions.assertEquals(0,modules.getPaths().get(0).getFields().size());
         }
     }
+
     @Nested
     class saveTest{
         private ModulesDto modulesDto;
         private UserDetail userDetail;
+
         @BeforeEach
         void beforeEach(){
             modulesService = spy(modulesService);
             modulesDto = mock(ModulesDto.class);
             userDetail = mock(UserDetail.class);
         }
+
         @Test
         @DisplayName("test save method when name existed")
         void test1(){
@@ -126,6 +158,7 @@ void test1(){
             doReturn(modules).when(modulesService).findByName(name);
             assertThrows(BizException.class, ()->modulesService.save(modulesDto, userDetail));
         }
+
         @Test
         @DisplayName("test save method normal")
         void test2(){
@@ -139,10 +172,12 @@ void test2(){
             verify(modulesRepository).save(any(),any());
         }
     }
+
     @Nested
     class batchUpdateModuleByListTest{
         private List<ModulesDto> modulesDtos;
         private UserDetail userDetail;
+
         @Test
         void testBatchUpdateModuleByListNormal(){
             modulesService = spy(modulesService);
@@ -155,4 +190,381 @@ void testBatchUpdateModuleByListNormal(){
             verify(modulesService,new Times(1)).updateModuleById(modulesDto, userDetail);
         }
     }
+
+    @Nested
+    @DisplayName("Method preview test")
+    class previewTest {
+        @Test
+        @DisplayName("test main process")
+        void test1() {
+            ApiCallStatsDto apiCallStatsDto = new ApiCallStatsDto() {{
+                setCallTotalCount(200L);
+                setTransferDataTotalBytes(200L);
+                setCallAlarmTotalCount(8L);
+                setResponseDataRowTotalCount(250L);
+                setTotalResponseTime(60000L);
+                setMaxResponseTime(2000L);
+                setAlarmApiTotalCount(2L);
+            }};
+            ApiCallStatsService apiCallStatsService = mock(ApiCallStatsService.class);
+            ReflectionTestUtils.setField(modulesService, "apiCallStatsService", apiCallStatsService);
+            when(apiCallStatsService.aggregateByUserId(anyString())).thenAnswer(invocationOnMock -> {
+                assertEquals("user1", invocationOnMock.getArgument(0));
+                return apiCallStatsDto;
+            });
+            UserDetail userDetail = mock(UserDetail.class);
+            when(userDetail.getUserId()).thenReturn("user1");
+            when(userDetail.isRoot()).thenReturn(false);
+            when(modulesService.count(any(Query.class))).thenAnswer(invocationOnMock -> {
+                Query query = invocationOnMock.getArgument(0);
+                assertEquals("{\"is_deleted\": {\"$ne\": true}, \"user_id\": \"user1\"}", query.getQueryObject().toJson());
+                return 2L;
+            });
+
+            PreviewVo preview = modulesService.preview(userDetail);
+
+            assertEquals(2L, preview.getTotalCount());
+            assertEquals(200L, preview.getVisitTotalCount());
+            assertEquals(8L, preview.getWarningVisitTotalCount());
+            assertEquals(250L, preview.getVisitTotalLine());
+            assertEquals(200L, preview.getTransmitTotal());
+            assertEquals(2L, preview.getWarningApiCount());
+        }
+
+        @Test
+        @DisplayName("test user detail is null")
+        void test2() {
+            PreviewVo preview = assertDoesNotThrow(() -> modulesService.preview(null));
+
+            assertEquals(0L, preview.getTotalCount());
+            assertEquals(0L, preview.getVisitTotalCount());
+            assertEquals(0L, preview.getWarningVisitTotalCount());
+            assertEquals(0L, preview.getVisitTotalLine());
+            assertEquals(0L, preview.getTransmitTotal());
+            assertEquals(0L, preview.getWarningApiCount());
+        }
+
+        @Test
+        @DisplayName("test user id is null")
+        void test3() {
+            UserDetail userDetail = mock(UserDetail.class);
+            PreviewVo preview = assertDoesNotThrow(() -> modulesService.preview(userDetail));
+
+            assertEquals(0L, preview.getTotalCount());
+            assertEquals(0L, preview.getVisitTotalCount());
+            assertEquals(0L, preview.getWarningVisitTotalCount());
+            assertEquals(0L, preview.getVisitTotalLine());
+            assertEquals(0L, preview.getTransmitTotal());
+            assertEquals(0L, preview.getWarningApiCount());
+        }
+    }
+
+    @Nested
+    @DisplayName("Method rankLists test")
+    class rankListsTest {
+
+        private ApiCallStatsService apiCallStatsService;
+
+        @BeforeEach
+        void setUp() {
+            apiCallStatsService = mock(ApiCallStatsService.class);
+            ReflectionTestUtils.setField(modulesService, "apiCallStatsService", apiCallStatsService);
+            modulesService = spy(modulesService);
+        }
+
+        @Test
+        @DisplayName("test main process with default where filter")
+        void test1() {
+            Filter filter = new Filter();
+            filter.setLimit(2);
+            filter.setSkip(5);
+            UserDetail userDetail = mock(UserDetail.class);
+            when(userDetail.getUserId()).thenReturn("user1");
+            when(apiCallStatsService.count(any(Query.class))).thenAnswer(invocationOnMock -> {
+                Query query = invocationOnMock.getArgument(0);
+                assertEquals("{\"user_id\": \"user1\"}", query.getQueryObject().toJson());
+                return 19L;
+            });
+            List<ModulesDto> modulesDtoList = new ArrayList<ModulesDto>(){{
+                add(new ModulesDto() {{
+                    setId(new ObjectId());
+                    setName("module 1");
+                }});
+                add(new ModulesDto() {{
+                    setId(new ObjectId());
+                    setName("module 2");
+                }});
+            }};
+            List<ApiCallStatsDto> apiCallStatsDtoList = new ArrayList<ApiCallStatsDto>(){{
+                add(new ApiCallStatsDto() {{
+                    setModuleId(modulesDtoList.get(0).getId().toString());
+                    setAccessFailureRate(0.01D);
+                }});
+                add(new ApiCallStatsDto() {{
+                    setModuleId(modulesDtoList.get(1).getId().toString());
+                    setAccessFailureRate(0D);
+                }});
+            }};
+            when(apiCallStatsService.findAll(any(Query.class))).thenAnswer(invocationOnMock -> {
+                Query query = invocationOnMock.getArgument(0);
+                assertEquals("{\"user_id\": \"user1\"}", query.getQueryObject().toJson());
+                assertEquals("{\"accessFailureRate\": -1, \"createTime\": -1}", query.getSortObject().toJson());
+                assertEquals("{\"accessFailureRate\": 1, \"moduleId\": 1}", query.getFieldsObject().toJson());
+                assertEquals(2, query.getLimit());
+                assertEquals(4, query.getSkip());
+                return apiCallStatsDtoList;
+            });
+            doAnswer(invocationOnMock -> {
+                Query query = invocationOnMock.getArgument(0);
+                assertEquals("{\"id\": {\"$in\": [{\"$oid\": \"" + apiCallStatsDtoList.get(0).getModuleId() + "\"}, {\"$oid\": \"" + apiCallStatsDtoList.get(1).getModuleId() + "\"}]}}", query.getQueryObject().toJson());
+                return modulesDtoList;
+            }).when(modulesService).findAll(any(Query.class));
+
+            RankListsVo rankListsVo = modulesService.rankLists(filter, userDetail);
+
+            assertEquals(19L, rankListsVo.getTotal());
+            assertEquals(2, rankListsVo.getItems().size());
+            List<Map<String, Object>> items = rankListsVo.getItems();
+            assertEquals(0.01D, items.get(0).get("module 1"));
+            assertEquals(0D, items.get(1).get("module 2"));
+        }
+
+        @Test
+        @DisplayName("test type is responseTime")
+        void test2() {
+            Filter filter = new Filter();
+            filter.setLimit(2);
+            filter.setSkip(5);
+            filter.where("type", "responseTime");
+            filter.setOrder("ASC");
+            UserDetail userDetail = mock(UserDetail.class);
+            when(userDetail.getUserId()).thenReturn("user1");
+            when(apiCallStatsService.count(any(Query.class))).thenAnswer(invocationOnMock -> {
+                Query query = invocationOnMock.getArgument(0);
+                assertEquals("{\"user_id\": \"user1\"}", query.getQueryObject().toJson());
+                return 19L;
+            });
+            List<ModulesDto> modulesDtoList = new ArrayList<ModulesDto>(){{
+                add(new ModulesDto() {{
+                    setId(new ObjectId());
+                    setName("module 1");
+                }});
+                add(new ModulesDto() {{
+                    setId(new ObjectId());
+                    setName("module 2");
+                }});
+            }};
+            List<ApiCallStatsDto> apiCallStatsDtoList = new ArrayList<ApiCallStatsDto>(){{
+                add(new ApiCallStatsDto() {{
+                    setModuleId(modulesDtoList.get(0).getId().toString());
+                    setMaxResponseTime(2000L);
+                }});
+                add(new ApiCallStatsDto() {{
+                    setModuleId(modulesDtoList.get(1).getId().toString());
+                    setMaxResponseTime(1000L);
+                }});
+            }};
+            when(apiCallStatsService.findAll(any(Query.class))).thenAnswer(invocationOnMock -> {
+                Query query = invocationOnMock.getArgument(0);
+                assertEquals("{\"user_id\": \"user1\"}", query.getQueryObject().toJson());
+                assertEquals("{\"maxResponseTime\": 1, \"createTime\": -1}", query.getSortObject().toJson());
+                assertEquals("{\"maxResponseTime\": 1, \"moduleId\": 1}", query.getFieldsObject().toJson());
+                assertEquals(2, query.getLimit());
+                assertEquals(4, query.getSkip());
+                return apiCallStatsDtoList;
+            });
+            doAnswer(invocationOnMock -> {
+                Query query = invocationOnMock.getArgument(0);
+                assertEquals("{\"id\": {\"$in\": [{\"$oid\": \"" + apiCallStatsDtoList.get(0).getModuleId() + "\"}, {\"$oid\": \"" + apiCallStatsDtoList.get(1).getModuleId() + "\"}]}}", query.getQueryObject().toJson());
+                return modulesDtoList;
+            }).when(modulesService).findAll(any(Query.class));
+
+            RankListsVo rankListsVo = modulesService.rankLists(filter, userDetail);
+
+            assertEquals(19L, rankListsVo.getTotal());
+            assertEquals(2, rankListsVo.getItems().size());
+            List<Map<String, Object>> items = rankListsVo.getItems();
+            assertEquals(2000L, items.get(0).get("module 1"));
+            assertEquals(1000L, items.get(1).get("module 2"));
+        }
+    }
+
+    @Nested
+    @DisplayName("Method apiList test")
+    class apiListTest {
+        @Test
+        @DisplayName("test main process")
+        void test1() {
+            Filter filter = new Filter();
+            filter.setOrder("createTime DESC");
+            UserDetail userDetail = mock(UserDetail.class);
+            when(userDetail.getUserId()).thenReturn("user1");
+            MongoCollection<Document> mongoCollection = mock(MongoCollection.class);
+            MongoTemplate mongoTemplate = mock(MongoTemplate.class);
+            when(mongoTemplate.getCollection("ApiCallStats")).thenReturn(mongoCollection);
+            ModulesRepository repository = mock(ModulesRepository.class);
+            when(repository.getMongoOperations()).thenReturn(mongoTemplate);
+            ReflectionTestUtils.setField(modulesService, "repository", repository);
+            MongoCursor<Document> mongoCursor = mock(MongoCursor.class);
+            AggregateIterable<Document> aggregateIterable = mock(AggregateIterable.class);
+            when(aggregateIterable.allowDiskUse(any(Boolean.class))).thenReturn(aggregateIterable);
+            when(aggregateIterable.iterator()).thenReturn(mongoCursor);
+            when(mongoCollection.aggregate(any(List.class))).thenAnswer(invocationOnMock -> {
+                List<Document> pipeline = invocationOnMock.getArgument(0);
+                assertEquals(5, pipeline.size());
+                assertEquals("{\"$project\": {\"moduleId\": {\"$toObjectId\": \"$moduleId\"}, \"responseDataRowTotalCount\": 1, \"callTotalCount\": 1, \"transferDataTotalBytes\": 1, \"clientIds\": 1}}", pipeline.get(0).toJson());
+                assertEquals("{\"$lookup\": {\"from\": \"Modules\", \"let\": {\"moduleId\": \"$moduleId\"}, \"pipeline\": [{\"$project\": {\"_id\": 1, \"name\": 1, \"status\": 1, \"user_id\": 1, \"createTime\": 1}}, {\"$match\": {\"$expr\": {\"$eq\": [\"$_id\", \"$$moduleId\"]}}}], \"as\": \"modules\"}}", pipeline.get(1).toJson());
+                assertEquals("{\"$unwind\": {\"path\": \"$modules\"}}", pipeline.get(2).toJson());
+                assertEquals("{\"$match\": {\"modules.user_id\": \"user1\"}}", pipeline.get(3).toJson());
+                assertEquals("{\"$facet\": {\"total\": [{\"$group\": {\"_id\": null, \"total\": {\"$sum\": 1}}}], \"items\": [{\"$sort\": {\"modules.createTime\": -1}}, {\"$limit\": 20}]}}", pipeline.get(4).toJson());
+                return aggregateIterable;
+            });
+            String json = "{\"total\": [{\"_id\": null, \"total\": 54}], \"items\": [{\"_id\": {\"$oid\": \"66d16f366d155a6f5d5b54a2\"}, \"callTotalCount\": 29008, \"clientIds\": [\"5c0e750b7a5cd42464a5099d\"], \"responseDataRowTotalCount\": 7617120, \"transferDataTotalBytes\": 161297942, \"moduleId\": {\"$oid\": \"65fcf4a7edf441365e98dff2\"}, \"modules\": {\"_id\": {\"$oid\": \"65fcf4a7edf441365e98dff2\"}, \"name\": \"POSS_IM_COMPANY\", \"status\": \"pending\", \"createTime\": {\"$date\": \"2024-03-22T03:01:59.379Z\"}, \"user_id\": \"62bc5008d4958d013d97c7a6\"}}, {\"_id\": {\"$oid\": \"66d16f366d155a6f5d5b54a1\"}, \"callTotalCount\": 20002, \"clientIds\": [\"5c0e750b7a5cd42464a5099d\"], \"responseDataRowTotalCount\": 2429040, \"transferDataTotalBytes\": 104957808, \"moduleId\": {\"$oid\": \"65fced81edf441365e98d9ad\"}, \"modules\": {\"_id\": {\"$oid\": \"65fced81edf441365e98d9ad\"}, \"name\": \"POSS_IV_STYLE_CATG\", \"status\": \"active\", \"createTime\": {\"$date\": \"2024-03-22T02:31:29.986Z\"}, \"user_id\": \"62bc5008d4958d013d97c7a6\"}}, {\"_id\": {\"$oid\": \"66d16f356d155a6f5d5b54a0\"}, \"callTotalCount\": 28001, \"clientIds\": [\"5c0e750b7a5cd42464a5099d\"], \"responseDataRowTotalCount\": 3604020, \"transferDataTotalBytes\": 124127362, \"moduleId\": {\"$oid\": \"65fc02faedf441365e97a697\"}, \"modules\": {\"_id\": {\"$oid\": \"65fc02faedf441365e97a697\"}, \"name\": \"MDM_catalogItem_changelog\", \"status\": \"active\", \"createTime\": {\"$date\": \"2024-03-21T09:50:50.036Z\"}, \"user_id\": \"62bc5008d4958d013d97c7a6\"}}, {\"_id\": {\"$oid\": \"66d16f356d155a6f5d5b549f\"}, \"callTotalCount\": 0, \"clientIds\": [], \"responseDataRowTotalCount\": 0, \"transferDataTotalBytes\": 0, \"moduleId\": {\"$oid\": \"65fbfa61edf441365e979cfd\"}, \"modules\": {\"_id\": {\"$oid\": \"65fbfa61edf441365e979cfd\"}, \"name\": \"MDM_model_changelog\", \"status\": \"active\", \"createTime\": {\"$date\": \"2024-03-21T09:14:09.967Z\"}, \"user_id\": \"62bc5008d4958d013d97c7a6\"}}, {\"_id\": {\"$oid\": \"66d16f356d155a6f5d5b549e\"}, \"callTotalCount\": 37001, \"clientIds\": [\"5c0e750b7a5cd42464a5099d\"], \"responseDataRowTotalCount\": 4973000, \"transferDataTotalBytes\": 194749360, \"moduleId\": {\"$oid\": \"65fbf59dedf441365e9790f6\"}, \"modules\": {\"_id\": {\"$oid\": \"65fbf59dedf441365e9790f6\"}, \"name\": \"NonStockModel_changelog\", \"status\": \"active\", \"createTime\": {\"$date\": \"2024-03-21T08:53:49.59Z\"}, \"user_id\": \"62bc5008d4958d013d97c7a6\"}}]}";
+            Document aggregateReturn = Document.parse(json);
+            when(mongoCursor.hasNext()).thenReturn(true);
+            when(mongoCursor.next()).thenReturn(aggregateReturn);
+
+            Page<ApiListVo> apiListVoPage = modulesService.apiList(filter, userDetail);
+
+            assertEquals(54, apiListVoPage.getTotal());
+            assertEquals(5, apiListVoPage.getItems().size());
+        }
+    }
+
+    @Nested
+    @DisplayName("Method apiDetail test")
+    class apiDetailTest {
+
+        private ApiCallStatsService apiCallStatsService;
+        private ApiCallService apiCallService;
+        private ApplicationService applicationService;
+        private ApiCallMinuteStatsService apiCallMinuteStatsService;
+
+        @BeforeEach
+        void setUp() {
+            apiCallStatsService = mock(ApiCallStatsService.class);
+            ReflectionTestUtils.setField(modulesService, "apiCallStatsService", apiCallStatsService);
+            apiCallService = mock(ApiCallService.class);
+            ReflectionTestUtils.setField(modulesService, "apiCallService", apiCallService);
+            applicationService = mock(ApplicationService.class);
+            ReflectionTestUtils.setField(modulesService, "applicationService", applicationService);
+            apiCallMinuteStatsService = mock(ApiCallMinuteStatsService.class);
+            ReflectionTestUtils.setField(modulesService, "apiCallMinuteStatsService", apiCallMinuteStatsService);
+        }
+
+        @Test
+        @DisplayName("test main process")
+        void test1() {
+            ApiDetailParam apiDetailParam = new ApiDetailParam();
+            apiDetailParam.setId("1");
+            apiDetailParam.setGuanluary(5);
+            apiDetailParam.setType("latency");
+            ApiCallStatsDto apiCallStatsDto = new ApiCallStatsDto() {{
+                setClientIds(new HashSet<String>() {{
+                    add(new ObjectId().toString());
+                }});
+                setTransferDataTotalBytes(1000L);
+                setTotalResponseTime(10L);
+            }};
+            when(apiCallStatsService.findOne(any(Query.class))).thenAnswer(invocationOnMock -> {
+                Query query = invocationOnMock.getArgument(0);
+                assertEquals("{\"moduleId\": \"1\"}", query.getQueryObject().toJson());
+                return apiCallStatsDto;
+            });
+            ApiCallEntity apiCallEntity = new ApiCallEntity(){{
+                setResRows(100L);
+                setLatency(1000L);
+            }};
+            when(apiCallService.findOne(any(Query.class))).thenAnswer(invocationOnMock -> {
+                Query query = invocationOnMock.getArgument(0);
+                assertEquals("{\"allPathId\": \"1\"}", query.getQueryObject().toJson());
+                assertEquals("{\"res_rows\": 1, \"latency\": 1}", query.getFieldsObject().toJson());
+                assertEquals("{\"_id\": -1}", query.getSortObject().toJson());
+                assertEquals(1, query.getLimit());
+                return apiCallEntity;
+            });
+            List<ApplicationDto> applicationDtoList = new ArrayList<>();
+            applicationDtoList.add(new ApplicationDto(){{
+                setId(new ObjectId(apiCallStatsDto.getClientIds().iterator().next()));
+                setName("app");
+            }});
+            when(applicationService.findAll(any(Query.class))).thenAnswer(invocationOnMock -> {
+                Query query = invocationOnMock.getArgument(0);
+                assertEquals("{\"id\": {\"$in\": [\"" + apiCallStatsDto.getClientIds().iterator().next() + "\"]}}", query.getQueryObject().toJson());
+                assertEquals("{\"name\": 1, \"id\": 1}", query.getFieldsObject().toJson());
+                return applicationDtoList;
+            });
+            List<ApiCallMinuteStatsDto> apiCallMinuteStatsDtoList = new ArrayList<>();
+            apiCallMinuteStatsDtoList.add(new ApiCallMinuteStatsDto(){{
+                setApiCallTime(Date.from(ZonedDateTime.now(ZoneId.systemDefault()).minusMinutes(1).withSecond(0).withNano(0).toInstant()));
+                setTotalResponseTime(1000L);
+            }});
+            when(apiCallMinuteStatsService.findAll(any(Query.class))).thenReturn(apiCallMinuteStatsDtoList);
+
+            ApiDetailVo apiDetailVo = modulesService.apiDetail(apiDetailParam);
+            assertEquals(5, apiDetailVo.getTime().size());
+            assertEquals(5, apiDetailVo.getValue().size());
+            assertEquals(1000L, apiDetailVo.getValue().get(4));
+            assertEquals(10D, apiDetailVo.getResponseTime());
+            assertEquals(1000L, apiDetailVo.getTimeConsuming());
+            assertEquals(100000D, apiDetailVo.getSpeed());
+        }
+    }
+
+    @Nested
+    @DisplayName("Method pickValue test")
+    class pickValueTest {
+
+        private ApiCallMinuteStatsDto apiCallMinuteStatsDto;
+
+        @BeforeEach
+        void setUp() {
+            apiCallMinuteStatsDto = new ApiCallMinuteStatsDto(){{
+                setTotalResponseTime(1L);
+                setResponseDataRowTotalCount(2L);
+                setResponseTimePerRow(3D);
+                setTransferBytePerSecond(4D);
+            }};
+        }
+
+        @Test
+        @DisplayName("type is latency")
+        void test1() {
+            ApiDetailParam apiDetailParam = new ApiDetailParam();
+            apiDetailParam.setType("latency");
+            assertEquals(1L, modulesService.pickValue(apiDetailParam, apiCallMinuteStatsDto));
+        }
+
+        @Test
+        @DisplayName("type is visitTotalLine")
+        void test2() {
+            ApiDetailParam apiDetailParam = new ApiDetailParam();
+            apiDetailParam.setType("visitTotalLine");
+            assertEquals(2L, modulesService.pickValue(apiDetailParam, apiCallMinuteStatsDto));
+        }
+
+        @Test
+        @DisplayName("type is responseTime")
+        void test3() {
+            ApiDetailParam apiDetailParam = new ApiDetailParam();
+            apiDetailParam.setType("responseTime");
+            assertEquals(3D, modulesService.pickValue(apiDetailParam, apiCallMinuteStatsDto));
+        }
+
+        @Test
+        @DisplayName("type is speed")
+        void test4() {
+            ApiDetailParam apiDetailParam = new ApiDetailParam();
+            apiDetailParam.setType("speed");
+            assertEquals(4D, modulesService.pickValue(apiDetailParam, apiCallMinuteStatsDto));
+        }
+
+        @Test
+        @DisplayName("type is other")
+        void test5() {
+            ApiDetailParam apiDetailParam = new ApiDetailParam();
+            apiDetailParam.setType("other");
+            assertEquals(0, modulesService.pickValue(apiDetailParam, apiCallMinuteStatsDto));
+        }
+    }
 }
diff --git a/manager/tm/src/test/java/com/tapdata/tm/schedule/ApiCallMinuteStatsSchedulerTest.java b/manager/tm/src/test/java/com/tapdata/tm/schedule/ApiCallMinuteStatsSchedulerTest.java
new file mode 100644
index 0000000000..05a2b8fe04
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/schedule/ApiCallMinuteStatsSchedulerTest.java
@@ -0,0 +1,175 @@
+package com.tapdata.tm.schedule;
+
+import com.tapdata.tm.apiCalls.service.ApiCallService;
+import com.tapdata.tm.apicallminutestats.dto.ApiCallMinuteStatsDto;
+import com.tapdata.tm.apicallminutestats.service.ApiCallMinuteStatsService;
+import com.tapdata.tm.modules.dto.ModulesDto;
+import com.tapdata.tm.modules.service.ModulesService;
+import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
+import org.bson.types.ObjectId;
+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.springframework.data.mongodb.core.query.Query;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author samuel
+ * @Description
+ * @create 2024-09-02 19:25
+ **/
+@DisplayName("Class ApiCallMinuteStatsScheduler Test")
+class ApiCallMinuteStatsSchedulerTest {
+
+	private ModulesService modulesService;
+	private ApiCallMinuteStatsService apiCallMinuteStatsService;
+	private ApiCallService apiCallService;
+	private ApiCallMinuteStatsScheduler apiCallMinuteStatsScheduler;
+
+	@BeforeEach
+	void setUp() {
+		modulesService = mock(ModulesService.class);
+		apiCallMinuteStatsService = mock(ApiCallMinuteStatsService.class);
+		apiCallService = mock(ApiCallService.class);
+		apiCallMinuteStatsScheduler = new ApiCallMinuteStatsScheduler(modulesService, apiCallMinuteStatsService, apiCallService);
+	}
+
+	@Test
+	@DisplayName("test scheduler config")
+	void test1() {
+		apiCallMinuteStatsScheduler.schedule();
+		assertEquals("ApiCallMinuteStatsScheduler-scheduler", Thread.currentThread().getName());
+		Method schedule = assertDoesNotThrow(() -> apiCallMinuteStatsScheduler.getClass().getDeclaredMethod("schedule"));
+		Scheduled scheduleAnnotation = schedule.getAnnotation(Scheduled.class);
+		assertEquals("0 0/1 * * * ?", scheduleAnnotation.cron());
+		SchedulerLock schedulerLockAnnotation = schedule.getAnnotation(SchedulerLock.class);
+		assertEquals("api_call_minute_stats_scheduler", schedulerLockAnnotation.name());
+		assertEquals("30m", schedulerLockAnnotation.lockAtMostFor());
+		assertEquals("5s", schedulerLockAnnotation.lockAtLeastFor());
+	}
+
+	@Nested
+	@DisplayName("test schedule method")
+	class TestScheduleMethod {
+
+		private List<ModulesDto> modulesList;
+		private ApiCallMinuteStatsDto lastApiCallMinuteStatsDto;
+		private List<ApiCallMinuteStatsDto> apiCallMinuteStatsDtoList;
+
+		@BeforeEach
+		void setUp() {
+			modulesList = new ArrayList<ModulesDto>() {{
+				add(new ModulesDto() {{
+					setId(new ObjectId());
+					setUserId("test");
+				}});
+			}};
+			lastApiCallMinuteStatsDto = new ApiCallMinuteStatsDto() {{
+				setLastApiCallId(new ObjectId().toString());
+			}};
+			apiCallMinuteStatsDtoList = new ArrayList<ApiCallMinuteStatsDto>() {{
+				add(new ApiCallMinuteStatsDto());
+			}};
+		}
+
+		@Test
+		@DisplayName("test schedule method")
+		void test1() {
+			when(modulesService.findAll(any(Query.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				assertEquals("{\"is_deleted\": {\"$ne\": true}}", query.getQueryObject().toJson());
+				assertEquals("{\"user_id\": 1, \"id\": 1}", query.getFieldsObject().toJson());
+				return modulesList;
+			});
+			when(apiCallMinuteStatsService.findOne(any(Query.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				assertEquals("{\"moduleId\": \"" + modulesList.get(0).getId().toString() + "\"}", query.getQueryObject().toJson());
+				assertEquals("{\"_id\": -1}", query.getSortObject().toJson());
+				assertEquals(1, query.getLimit());
+				return lastApiCallMinuteStatsDto;
+			});
+			when(apiCallService.aggregateMinuteByAllPathId(anyString(), anyString(), any(Date.class))).thenAnswer(invocationOnMock -> {
+				assertEquals(modulesList.get(0).getId().toString(), invocationOnMock.getArgument(0));
+				assertEquals(lastApiCallMinuteStatsDto.getLastApiCallId(), invocationOnMock.getArgument(1));
+				assertNotNull(invocationOnMock.getArgument(2));
+				assertTrue(new Date().getTime() > ((Date) invocationOnMock.getArgument(2)).getTime());
+				return apiCallMinuteStatsDtoList;
+			});
+			doAnswer(invocationOnMock -> {
+				List<ApiCallMinuteStatsDto> list = invocationOnMock.getArgument(0);
+				assertEquals(1, list.size());
+				assertSame(apiCallMinuteStatsDtoList.get(0), list.get(0));
+				assertEquals(modulesList.get(0).getUserId(), list.get(0).getUserId());
+				assertNotNull(list.get(0).getCreateAt());
+				return null;
+			}).when(apiCallMinuteStatsService).bulkWrite(any(List.class));
+
+			apiCallMinuteStatsScheduler.schedule();
+		}
+
+		@Test
+		@DisplayName("test modules oid is null")
+		void test2() {
+			modulesList = new ArrayList<ModulesDto>() {{
+				add(new ModulesDto());
+			}};
+			when(modulesService.findAll(any(Query.class))).thenReturn(modulesList);
+
+			apiCallMinuteStatsScheduler.schedule();
+
+			verify(apiCallService, never()).aggregateByAllPathId(any(), any());
+		}
+
+		@Test
+		@DisplayName("test last api call id is blank")
+		void test3() {
+			when(modulesService.findAll(any(Query.class))).thenReturn(modulesList);
+			lastApiCallMinuteStatsDto.setLastApiCallId("");
+			when(apiCallMinuteStatsService.findOne(any(Query.class))).thenReturn(lastApiCallMinuteStatsDto);
+			when(apiCallService.aggregateMinuteByAllPathId(anyString(), any(), any(Date.class))).thenAnswer(invocationOnMock -> {
+				assertNull(invocationOnMock.getArgument(1));
+				return apiCallMinuteStatsDtoList;
+			});
+			apiCallMinuteStatsScheduler.schedule();
+
+			when(modulesService.findAll(any(Query.class))).thenReturn(modulesList);
+			lastApiCallMinuteStatsDto.setLastApiCallId("    ");
+			when(apiCallService.aggregateMinuteByAllPathId(anyString(), any(), any(Date.class))).thenAnswer(invocationOnMock -> {
+				assertNull(invocationOnMock.getArgument(1));
+				return apiCallMinuteStatsDtoList;
+			});
+			apiCallMinuteStatsScheduler.schedule();
+
+			when(modulesService.findAll(any(Query.class))).thenReturn(modulesList);
+			lastApiCallMinuteStatsDto.setLastApiCallId(null);
+			when(apiCallService.aggregateMinuteByAllPathId(anyString(), any(), any(Date.class))).thenAnswer(invocationOnMock -> {
+				assertNull(invocationOnMock.getArgument(1));
+				return apiCallMinuteStatsDtoList;
+			});
+			apiCallMinuteStatsScheduler.schedule();
+		}
+
+		@Test
+		@DisplayName("test last api call minute stats not exists")
+		void test4() {
+			when(modulesService.findAll(any(Query.class))).thenReturn(modulesList);
+			when(apiCallMinuteStatsService.findOne(any(Query.class))).thenReturn(null);
+			when(apiCallService.aggregateMinuteByAllPathId(anyString(), any(), any(Date.class))).thenAnswer(invocationOnMock -> {
+				assertNull(invocationOnMock.getArgument(1));
+				return apiCallMinuteStatsDtoList;
+			});
+			apiCallMinuteStatsScheduler.schedule();
+		}
+	}
+}
\ No newline at end of file
diff --git a/manager/tm/src/test/java/com/tapdata/tm/schedule/ApiCallStatsSchedulerTest.java b/manager/tm/src/test/java/com/tapdata/tm/schedule/ApiCallStatsSchedulerTest.java
new file mode 100644
index 0000000000..ffaad6b239
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/schedule/ApiCallStatsSchedulerTest.java
@@ -0,0 +1,275 @@
+package com.tapdata.tm.schedule;
+
+import ch.qos.logback.classic.Logger;
+import com.tapdata.tm.apiCalls.service.ApiCallService;
+import com.tapdata.tm.apicallstats.dto.ApiCallStatsDto;
+import com.tapdata.tm.apicallstats.service.ApiCallStatsService;
+import com.tapdata.tm.modules.dto.ModulesDto;
+import com.tapdata.tm.modules.service.ModulesService;
+import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
+import org.bson.Document;
+import org.bson.types.ObjectId;
+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.springframework.data.mongodb.core.query.Query;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashSet;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author samuel
+ * @Description
+ * @create 2024-08-30 18:28
+ **/
+@DisplayName("Class ApiCallStatsScheduler Test")
+class ApiCallStatsSchedulerTest {
+
+	private ApiCallStatsScheduler apiCallStatsScheduler;
+	private ModulesService modulesService;
+	private ApiCallService apiCallService;
+	private ApiCallStatsService apiCallStatsService;
+
+	@BeforeEach
+	void setUp() {
+		modulesService = mock(ModulesService.class);
+		apiCallService = mock(ApiCallService.class);
+		apiCallStatsService = mock(ApiCallStatsService.class);
+		apiCallStatsScheduler = new ApiCallStatsScheduler(modulesService, apiCallStatsService, apiCallService);
+	}
+
+	@Test
+	@DisplayName("test scheduler config")
+	void test1() {
+		apiCallStatsScheduler.schedule();
+		assertEquals("ApiCallStatsScheduler-scheduler", Thread.currentThread().getName());
+		Method schedule = assertDoesNotThrow(() -> apiCallStatsScheduler.getClass().getDeclaredMethod("schedule"));
+		Scheduled scheduleAnnotation = schedule.getAnnotation(Scheduled.class);
+		assertEquals("0 0/5 * * * ?", scheduleAnnotation.cron());
+		SchedulerLock schedulerLockAnnotation = schedule.getAnnotation(SchedulerLock.class);
+		assertEquals("api_call_stats_scheduler", schedulerLockAnnotation.name());
+		assertEquals("30m", schedulerLockAnnotation.lockAtMostFor());
+		assertEquals("5s", schedulerLockAnnotation.lockAtLeastFor());
+	}
+
+	@Nested
+	@DisplayName("Method schedule test")
+	class ScheduleTest {
+
+		private ArrayList<ModulesDto> modulesList;
+		private ApiCallStatsDto existsApiCallStatsDto;
+		private ApiCallStatsDto newApiCallStatsDto;
+		private Logger log;
+
+		@BeforeEach
+		void setUp() {
+			modulesList = new ArrayList<ModulesDto>() {{
+				add(new ModulesDto() {{
+					setId(new ObjectId());
+					setUserId("test");
+					setIsDeleted(false);
+				}});
+				add(new ModulesDto() {{
+					setId(new ObjectId());
+					setUserId("test");
+					setIsDeleted(true);
+				}});
+			}};
+			existsApiCallStatsDto = new ApiCallStatsDto() {{
+				setId(new ObjectId());
+				setCallTotalCount(100L);
+				setTransferDataTotalBytes(100L);
+				setCallAlarmTotalCount(5L);
+				setResponseDataRowTotalCount(150L);
+				setTotalResponseTime(30000L);
+				setMaxResponseTime(2000L);
+				setClientIds(new HashSet<String>() {{
+					add("client1");
+				}});
+				setLastApiCallId(new ObjectId().toString());
+				setModuleId(modulesList.get(0).getId().toString());
+				setCreateAt(new Date());
+			}};
+			newApiCallStatsDto = new ApiCallStatsDto() {{
+				setCallTotalCount(200L);
+				setTransferDataTotalBytes(200L);
+				setCallAlarmTotalCount(8L);
+				setResponseDataRowTotalCount(250L);
+				setTotalResponseTime(60000L);
+				setMaxResponseTime(2000L);
+				setClientIds(new HashSet<String>() {{
+					add("client2");
+					add("client3");
+				}});
+				setLastApiCallId(new ObjectId().toString());
+				setModuleId(modulesList.get(0).getId().toString());
+			}};
+			log = (Logger) ReflectionTestUtils.getField(apiCallStatsScheduler, "log");
+			log = spy(log);
+			doReturn(true).when(log).isDebugEnabled();
+		}
+
+		@Test
+		@DisplayName("test schedule main process")
+		void test1() {
+			when(modulesService.findAll(any(Query.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				Document fieldsObject = query.getFieldsObject();
+				assertEquals("{\"is_deleted\": 1, \"user_id\": 1, \"id\": 1}", fieldsObject.toJson());
+				assertTrue(query.getQueryObject().isEmpty());
+				return modulesList;
+			});
+			when(apiCallStatsService.findOne(any(Query.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				assertEquals("{\"moduleId\": \"" + modulesList.get(0).getId().toString() + "\"}", query.getQueryObject().toJson());
+				assertEquals(1, query.getLimit());
+				assertTrue(query.getFieldsObject().isEmpty());
+				return existsApiCallStatsDto;
+			});
+			when(apiCallService.aggregateByAllPathId(anyString(), anyString())).thenAnswer(invocationOnMock -> {
+				assertEquals(modulesList.get(0).getId().toString(), invocationOnMock.getArgument(0));
+				assertEquals(existsApiCallStatsDto.getLastApiCallId(), invocationOnMock.getArgument(1));
+				return newApiCallStatsDto;
+			});
+			doCallRealMethod().when(apiCallStatsService).merge(any(ApiCallStatsDto.class), any(ApiCallStatsDto.class));
+			when(apiCallStatsService.upsert(any(Query.class), any(ApiCallStatsDto.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				assertEquals("{\"moduleId\": \"" + modulesList.get(0).getId().toString() + "\"}", query.getQueryObject().toJson());
+				ApiCallStatsDto actual = invocationOnMock.getArgument(1);
+				assertEquals(modulesList.get(0).getId().toString(), actual.getModuleId());
+				assertEquals(300L, actual.getCallTotalCount());
+				assertEquals(300L, actual.getTransferDataTotalBytes());
+				assertEquals(13L, actual.getCallAlarmTotalCount());
+				assertEquals(400L, actual.getResponseDataRowTotalCount());
+				assertEquals(90000L, actual.getTotalResponseTime());
+				assertEquals(3, actual.getClientIds().size());
+				assertTrue(actual.getClientIds().contains("client1"));
+				assertTrue(actual.getClientIds().contains("client2"));
+				assertTrue(actual.getClientIds().contains("client3"));
+				assertEquals(0.0433D, actual.getAccessFailureRate());
+				assertNotNull(actual.getLastApiCallId());
+				assertEquals(existsApiCallStatsDto.getCreateAt(), actual.getCreateAt());
+				assertEquals(modulesList.get(0).getUserId(), actual.getUserId());
+				return null;
+			});
+
+			apiCallStatsScheduler.schedule();
+
+			verify(apiCallStatsService).deleteAllByModuleId(modulesList.get(1).getId().toString());
+		}
+
+		@Test
+		@DisplayName("test no exists api call stats")
+		void test2() {
+			when(modulesService.findAll(any(Query.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				Document fieldsObject = query.getFieldsObject();
+				assertEquals("{\"is_deleted\": 1, \"user_id\": 1, \"id\": 1}", fieldsObject.toJson());
+				assertTrue(query.getQueryObject().isEmpty());
+				return modulesList;
+			});
+			when(apiCallStatsService.findOne(any(Query.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				assertEquals("{\"moduleId\": \"" + modulesList.get(0).getId().toString() + "\"}", query.getQueryObject().toJson());
+				assertEquals(1, query.getLimit());
+				assertTrue(query.getFieldsObject().isEmpty());
+				return null;
+			});
+			when(apiCallService.aggregateByAllPathId(anyString(), any())).thenAnswer(invocationOnMock -> {
+				assertEquals(modulesList.get(0).getId().toString(), invocationOnMock.getArgument(0));
+				assertNull(invocationOnMock.getArgument(1));
+				return newApiCallStatsDto;
+			});
+			doCallRealMethod().when(apiCallStatsService).merge(any(), any(ApiCallStatsDto.class));
+			when(apiCallStatsService.upsert(any(Query.class), any(ApiCallStatsDto.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				assertEquals("{\"moduleId\": \"" + modulesList.get(0).getId().toString() + "\"}", query.getQueryObject().toJson());
+				ApiCallStatsDto actual = invocationOnMock.getArgument(1);
+				assertEquals(modulesList.get(0).getId().toString(), actual.getModuleId());
+				assertEquals(200L, actual.getCallTotalCount());
+				assertEquals(200L, actual.getTransferDataTotalBytes());
+				assertEquals(8L, actual.getCallAlarmTotalCount());
+				assertEquals(250L, actual.getResponseDataRowTotalCount());
+				assertEquals(60000L, actual.getTotalResponseTime());
+				assertEquals(2, actual.getClientIds().size());
+				assertTrue(actual.getClientIds().contains("client2"));
+				assertTrue(actual.getClientIds().contains("client3"));
+				assertEquals(0.04D, actual.getAccessFailureRate());
+				assertNotNull(actual.getLastApiCallId());
+				assertNotNull(actual.getCreateAt());
+				assertEquals(modulesList.get(0).getUserId(), actual.getUserId());
+				return null;
+			});
+			when(apiCallStatsService.isEmpty()).thenReturn(true);
+
+			apiCallStatsScheduler.schedule();
+
+			verify(apiCallStatsService).deleteAllByModuleId(modulesList.get(1).getId().toString());
+		}
+
+		@Test
+		@DisplayName("test no modules")
+		void test3() {
+			when(modulesService.findAll(any(Query.class))).thenAnswer(invocationOnMock -> {
+				Query query = invocationOnMock.getArgument(0);
+				Document fieldsObject = query.getFieldsObject();
+				assertEquals("{\"is_deleted\": 1, \"user_id\": 1, \"id\": 1}", fieldsObject.toJson());
+				assertTrue(query.getQueryObject().isEmpty());
+				return null;
+			});
+
+			apiCallStatsScheduler.schedule();
+
+			verify(apiCallStatsService, never()).findOne(any(Query.class));
+		}
+
+		@Test
+		@DisplayName("test aggregateByAllPathId error")
+		void test4() {
+			when(modulesService.findAll(any(Query.class))).thenReturn(modulesList);
+			when(apiCallStatsService.findOne(any(Query.class))).thenReturn(existsApiCallStatsDto);
+			RuntimeException runtimeException = new RuntimeException("test");
+			when(apiCallService.aggregateByAllPathId(anyString(), anyString())).thenThrow(runtimeException);
+
+			assertDoesNotThrow(() -> apiCallStatsScheduler.schedule());
+
+			verify(apiCallStatsService, never()).merge(any(), any(ApiCallStatsDto.class));
+		}
+
+		@Test
+		@DisplayName("test merge error")
+		void test5() {
+			when(modulesService.findAll(any(Query.class))).thenReturn(modulesList);
+			when(apiCallStatsService.findOne(any(Query.class))).thenReturn(existsApiCallStatsDto);
+			when(apiCallService.aggregateByAllPathId(anyString(), anyString())).thenReturn(newApiCallStatsDto);
+			RuntimeException runtimeException = new RuntimeException("test");
+			doThrow(runtimeException).when(apiCallStatsService).merge(any(), any(ApiCallStatsDto.class));
+
+			assertDoesNotThrow(() -> apiCallStatsScheduler.schedule());
+
+			verify(apiCallStatsService, never()).upsert(any(Query.class), any(ApiCallStatsDto.class));
+		}
+
+		@Test
+		@DisplayName("test upsert error")
+		void test6() {
+			when(modulesService.findAll(any(Query.class))).thenReturn(modulesList);
+			when(apiCallStatsService.findOne(any(Query.class))).thenReturn(existsApiCallStatsDto);
+			when(apiCallService.aggregateByAllPathId(anyString(), anyString())).thenReturn(newApiCallStatsDto);
+			doCallRealMethod().when(apiCallStatsService).merge(any(ApiCallStatsDto.class), any(ApiCallStatsDto.class));
+			RuntimeException runtimeException = new RuntimeException("test");
+			when(apiCallStatsService.upsert(any(Query.class), any(ApiCallStatsDto.class))).thenThrow(runtimeException);
+
+			assertDoesNotThrow(() -> apiCallStatsScheduler.schedule());
+		}
+	}
+}
\ No newline at end of file
diff --git a/manager/tm/src/test/java/com/tapdata/tm/task/service/DefaultLicenseServiceTest.java b/manager/tm/src/test/java/com/tapdata/tm/task/service/DefaultLicenseServiceTest.java
new file mode 100644
index 0000000000..bb599e38c7
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/task/service/DefaultLicenseServiceTest.java
@@ -0,0 +1,26 @@
+package com.tapdata.tm.task.service;
+
+import com.tapdata.tm.commons.task.dto.TaskDto;
+import com.tapdata.tm.config.security.UserDetail;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.mock;
+
+public class DefaultLicenseServiceTest {
+    private DefaultLicenseService defaultLicenseService;
+    @BeforeEach
+    void beforeEach(){
+        defaultLicenseService = mock(DefaultLicenseService.class);
+    }
+    @Test
+    void testCheckTaskPipelineLimit(){
+        TaskDto taskDto = mock(TaskDto.class);
+        UserDetail user = mock(UserDetail.class);
+        doCallRealMethod().when(defaultLicenseService).checkTaskPipelineLimit(taskDto,user);
+        boolean actual = defaultLicenseService.checkTaskPipelineLimit(taskDto, user);
+        assertTrue(actual);
+    }
+}
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 ec06526781..76f6bacb7c 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
@@ -2509,6 +2509,7 @@ class StartWithFlagTest{
         private DisruptorService disruptorService;
         private LogCollectorService logCollectorService;
         private ScheduleService scheduleService;
+        private ILicenseService iLicenseService;
         @BeforeEach
         void beforeEach(){
             startFlag = "11";
@@ -2516,18 +2517,21 @@ void beforeEach(){
             disruptorService = mock(DisruptorService.class);
             logCollectorService = mock(LogCollectorService.class);
             scheduleService = mock(ScheduleService.class);
+            iLicenseService = mock(ILicenseService.class);
             UserDataReportService userDataReportService = mock(UserDataReportService.class);
             ReflectionTestUtils.setField(taskService,"userDataReportService",userDataReportService);
             ReflectionTestUtils.setField(taskService,"lockControlService",lockControlService);
             ReflectionTestUtils.setField(taskService,"disruptorService",disruptorService);
             ReflectionTestUtils.setField(taskService,"logCollectorService",logCollectorService);
             ReflectionTestUtils.setField(taskService,"scheduleService",scheduleService);
+            ReflectionTestUtils.setField(taskService,"iLicenseService",iLicenseService);
             when(taskDto.getShareCdcEnable()).thenReturn(true);
             when(taskDto.getSyncType()).thenReturn("sync");
             when(taskDto.getTaskRecordId()).thenReturn("111");
             when(taskDto.getStartTime()).thenReturn(null);
             when(taskDto.getShareCache()).thenReturn(false);
             when(taskDto.getId()).thenReturn(mock(ObjectId.class));
+            when(iLicenseService.checkTaskPipelineLimit(taskDto, user)).thenReturn(true);
         }
         @Test
         @DisplayName("test start method when dag is invalid")
@@ -2611,6 +2615,13 @@ void test7(){
             verify(scheduleService,new Times(1)).createTaskRecordForInitial(taskDto);
             verify(taskService,new Times(1)).update(any(Query.class), any(TaskDto.class));
         }
+        @Test
+        @DisplayName("test start method when exceed pipeline limit")
+        void test8(){
+            when(iLicenseService.checkTaskPipelineLimit(taskDto, user)).thenReturn(false);
+            doCallRealMethod().when(taskService).start(taskDto, user, startFlag);
+            assertThrows(BizException.class, ()->taskService.start(taskDto, user, startFlag));
+        }
     }
     @Nested
     class UpdateTaskRecordsStatusTest{
@@ -3161,6 +3172,27 @@ void testBatchStart() {
                 verify(taskService, times(1)).start(taskDtos.get(0), user, "11");
             }
         }
+        @Test
+        void testBatchStartWithNodeInstanceIdInvalidEx() {
+            try (MockedStatic<DataPermissionHelper> dataPermissionHelperMockedStatic = mockStatic(DataPermissionHelper.class)) {
+                taskEntity.setCrontabExpressionFlag(true);
+                Query query = new Query(Criteria.where("_id").is(taskEntity.getId()));
+                query.fields().include("planStartDateFlag", "crontabExpressionFlag");
+                when(repository.findOne(query)).thenReturn(Optional.ofNullable(taskEntity));
+                List<TaskDto> taskDtos = CglibUtil.copyList(taskEntities, TaskDto::new);
+                CalculationEngineVo calculationEngineVo = new CalculationEngineVo();
+                calculationEngineVo.setTaskLimit(2);
+                calculationEngineVo.setRunningNum(2);
+                calculationEngineVo.setTaskLimit(2);
+                calculationEngineVo.setTotalLimit(2);
+                when(taskScheduleService.cloudTaskLimitNum(taskDtos.get(0), user, true)).thenReturn(calculationEngineVo);
+                MonitoringLogsService monitoringLogsService = mock(MonitoringLogsService.class);
+                taskService.setMonitoringLogsService(monitoringLogsService);
+                doThrow(new BizException("License.NodeInstanceIdInvalid","test exception")).when(taskService).start(taskDtos.get(0), user, "11");
+                assertThrows(BizException.class, ()->taskService.batchStart(ids, user, null, null));
+                verify(taskService, times(1)).start(taskDtos.get(0), user, "11");
+            }
+        }
     }
 
     @Nested
diff --git a/manager/tm/src/test/java/com/tapdata/tm/user/service/UserServiceImplTest.java b/manager/tm/src/test/java/com/tapdata/tm/user/service/UserServiceImplTest.java
new file mode 100644
index 0000000000..ab155d18dd
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/user/service/UserServiceImplTest.java
@@ -0,0 +1,59 @@
+package com.tapdata.tm.user.service;
+
+import com.tapdata.tm.config.security.UserDetail;
+import com.tapdata.tm.roleMapping.dto.RoleMappingDto;
+import com.tapdata.tm.roleMapping.service.RoleMappingService;
+import com.tapdata.tm.user.service.UserServiceImpl;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.internal.verification.Times;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.*;
+
+public class UserServiceImplTest {
+    private UserServiceImpl userService;
+    private RoleMappingService roleMappingService;
+    @BeforeEach
+    void beforeEach(){
+        userService = mock(UserServiceImpl.class);
+        roleMappingService = mock(RoleMappingService.class);
+        ReflectionTestUtils.setField(userService,"roleMappingService",roleMappingService);
+    }
+    @Nested
+    class updateRoleMapping{
+        private String userId;
+        private List<Object> roleusers;
+        private UserDetail userDetail;
+        @BeforeEach
+        void beforeEach(){
+            userId = "66c84372a5921a16459c2cef";
+            roleusers = new ArrayList<>();
+            userDetail = mock(UserDetail.class);
+        }
+        @Test
+        void testForAdmin(){
+            doCallRealMethod().when(userService).updateRoleMapping(userId,roleusers,userDetail);
+            List<RoleMappingDto> actual = userService.updateRoleMapping(userId, roleusers, userDetail);
+            verify(roleMappingService,new Times(0)).deleteAll(any(Query.class));
+            assertNull(actual);
+        }
+        @Test
+        void testForUser(){
+            when(userDetail.getEmail()).thenReturn("test@tapdata.com");
+            roleusers.add("5d31ae1ab953565ded04badd");
+            doCallRealMethod().when(userService).updateRoleMapping(userId,roleusers,userDetail);
+            List<RoleMappingDto> actual = userService.updateRoleMapping(userId, roleusers, userDetail);
+            verify(roleMappingService,new Times(1)).deleteAll(any(Query.class));
+            verify(roleMappingService,new Times(1)).updateUserRoleMapping(anyList(), any(UserDetail.class));
+        }
+    }
+}
diff --git a/manager/tm/src/test/java/com/tapdata/tm/utils/CustomPatternLayoutTest.java b/manager/tm/src/test/java/com/tapdata/tm/utils/CustomPatternLayoutTest.java
new file mode 100644
index 0000000000..325373cbfd
--- /dev/null
+++ b/manager/tm/src/test/java/com/tapdata/tm/utils/CustomPatternLayoutTest.java
@@ -0,0 +1,49 @@
+package com.tapdata.tm.utils;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.LoggingEvent;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import java.util.HashMap;
+import java.util.Map;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+public class CustomPatternLayoutTest {
+    LoggerContext loggerContext;
+    LoggingEvent iLoggingEvent=new LoggingEvent();
+    String message;
+    CustomPatternLayout customPatternLayout = new CustomPatternLayout();
+
+    @BeforeEach
+    void beforeEach(){
+        loggerContext= new LoggerContext();
+        loggerContext.setName("managerContext");
+        message = "at org.tapdata.tapdata.tapdata.AbstractBufferingClientHttpRequest.tapdata(AbstractBufferingClientHttpRequest.tapdata:48)";
+        iLoggingEvent.setLevel(Level.INFO);
+        iLoggingEvent.setMessage(message);
+        customPatternLayout.setPattern("%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level  - %msg%n");
+        customPatternLayout.setContext(loggerContext);
+        customPatternLayout.start();
+    }
+    @Test
+    void test1(){
+        customPatternLayout.setContext(loggerContext);
+        customPatternLayout.start();
+        String formatLayout = customPatternLayout.doLayout(iLoggingEvent);
+        assertEquals(true, formatLayout.contains(message));
+    }
+    @Test
+    void test2(){
+        Map<String,Object> oemConfigMap=new HashMap<>();
+        oemConfigMap.put("tapdata","oem");
+        customPatternLayout.setOemConfigmap(oemConfigMap);
+        customPatternLayout.setContext(loggerContext);
+        customPatternLayout.start();
+        String formatLayout = customPatternLayout.doLayout(iLoggingEvent);
+        assertEquals(false,formatLayout.contains(message));
+    }
+
+
+}