diff --git a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphSyncService.java b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphSyncService.java index ef7693b..db6a33e 100644 --- a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphSyncService.java +++ b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphSyncService.java @@ -200,6 +200,108 @@ public class GraphSyncService { } } + // ----------------------------------------------------------------------- + // 增量同步 + // ----------------------------------------------------------------------- + + /** + * 增量同步:仅拉取指定时间窗口内变更的数据并同步到 Neo4j。 + *
+ * 与全量同步的区别: + *
- * TODO: 字段影响关系来源于 LLM 抽取或规则引擎,而非简单外键关联。 - * 当前 MVP 阶段为占位实现,后续由抽取模块填充。 + * 通过两种途径推导字段间的影响关系: + *
@@ -925,6 +1195,7 @@ public class GraphSyncStepService {
"MERGE (s)-[r:" + REL_TYPE + " {graph_id: $graphId, relation_type: $relationType}]->(t) " +
"ON CREATE SET r.id = $newId, r.weight = 1.0, r.confidence = 1.0, " +
" r.source_id = '', r.properties_json = $propertiesJson, r.created_at = datetime() " +
+ "ON MATCH SET r.properties_json = CASE WHEN $propertiesJson <> '{}' THEN $propertiesJson ELSE r.properties_json END " +
"RETURN r.id AS relId"
)
.bindAll(Map.of(
diff --git a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/domain/model/SyncMetadata.java b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/domain/model/SyncMetadata.java
index 69ce1c9..3ab7e50 100644
--- a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/domain/model/SyncMetadata.java
+++ b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/domain/model/SyncMetadata.java
@@ -33,6 +33,7 @@ public class SyncMetadata {
public static final String STATUS_PARTIAL = "PARTIAL";
public static final String TYPE_FULL = "FULL";
+ public static final String TYPE_INCREMENTAL = "INCREMENTAL";
public static final String TYPE_DATASETS = "DATASETS";
public static final String TYPE_FIELDS = "FIELDS";
public static final String TYPE_USERS = "USERS";
diff --git a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/domain/repository/GraphRelationRepository.java b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/domain/repository/GraphRelationRepository.java
index 8b9578b..84606cc 100644
--- a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/domain/repository/GraphRelationRepository.java
+++ b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/domain/repository/GraphRelationRepository.java
@@ -345,16 +345,13 @@ public class GraphRelationRepository {
.query(
"MATCH (s:Entity {graph_id: $graphId, id: $sourceEntityId}) " +
"MATCH (t:Entity {graph_id: $graphId, id: $targetEntityId}) " +
- "CREATE (s)-[r:" + REL_TYPE + " {" +
- " id: $id," +
- " relation_type: $relationType," +
- " weight: $weight," +
- " confidence: $confidence," +
- " source_id: $sourceId," +
- " graph_id: $graphId," +
- " properties_json: $propertiesJson," +
- " created_at: $createdAt" +
- "}]->(t) " +
+ "MERGE (s)-[r:" + REL_TYPE + " {graph_id: $graphId, relation_type: $relationType}]->(t) " +
+ "ON CREATE SET r.id = $id, r.weight = $weight, r.confidence = $confidence, " +
+ " r.source_id = $sourceId, r.properties_json = $propertiesJson, r.created_at = $createdAt " +
+ "ON MATCH SET r.weight = CASE WHEN $weight IS NOT NULL THEN $weight ELSE r.weight END, " +
+ " r.confidence = CASE WHEN $confidence IS NOT NULL THEN $confidence ELSE r.confidence END, " +
+ " r.source_id = CASE WHEN $sourceId <> '' THEN $sourceId ELSE r.source_id END, " +
+ " r.properties_json = CASE WHEN $propertiesJson <> '{}' THEN $propertiesJson ELSE r.properties_json END " +
RETURN_COLUMNS
)
.bindAll(params)
diff --git a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/interfaces/rest/GraphSyncController.java b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/interfaces/rest/GraphSyncController.java
index 62516ab..0d19fd0 100644
--- a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/interfaces/rest/GraphSyncController.java
+++ b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/interfaces/rest/GraphSyncController.java
@@ -49,6 +49,18 @@ public class GraphSyncController {
return SyncMetadataVO.from(metadata);
}
+ /**
+ * 增量同步:仅拉取指定时间窗口内变更的数据并同步。
+ */
+ @PostMapping("/incremental")
+ public SyncMetadataVO syncIncremental(
+ @PathVariable @Pattern(regexp = UUID_REGEX, message = "graphId 格式无效") String graphId,
+ @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime updatedFrom,
+ @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime updatedTo) {
+ SyncMetadata metadata = syncService.syncIncremental(graphId, updatedFrom, updatedTo);
+ return SyncMetadataVO.from(metadata);
+ }
+
/**
* 同步数据集实体。
*/
diff --git a/backend/services/knowledge-graph-service/src/test/java/com/datamate/knowledgegraph/application/GraphSyncServiceTest.java b/backend/services/knowledge-graph-service/src/test/java/com/datamate/knowledgegraph/application/GraphSyncServiceTest.java
index 7b6c490..2ae4941 100644
--- a/backend/services/knowledge-graph-service/src/test/java/com/datamate/knowledgegraph/application/GraphSyncServiceTest.java
+++ b/backend/services/knowledge-graph-service/src/test/java/com/datamate/knowledgegraph/application/GraphSyncServiceTest.java
@@ -566,6 +566,163 @@ class GraphSyncServiceTest {
}
}
+ // -----------------------------------------------------------------------
+ // 增量同步
+ // -----------------------------------------------------------------------
+
+ @Nested
+ class IncrementalSyncTest {
+
+ private final LocalDateTime UPDATED_FROM = LocalDateTime.of(2025, 6, 1, 0, 0);
+ private final LocalDateTime UPDATED_TO = LocalDateTime.of(2025, 6, 30, 23, 59);
+
+ @Test
+ void syncIncremental_invalidGraphId_throwsBusinessException() {
+ assertThatThrownBy(() -> syncService.syncIncremental(INVALID_GRAPH_ID, UPDATED_FROM, UPDATED_TO))
+ .isInstanceOf(BusinessException.class);
+ }
+
+ @Test
+ void syncIncremental_nullUpdatedFrom_throwsBusinessException() {
+ assertThatThrownBy(() -> syncService.syncIncremental(GRAPH_ID, null, UPDATED_TO))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining("updatedFrom");
+ }
+
+ @Test
+ void syncIncremental_nullUpdatedTo_throwsBusinessException() {
+ assertThatThrownBy(() -> syncService.syncIncremental(GRAPH_ID, UPDATED_FROM, null))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining("updatedTo");
+ }
+
+ @Test
+ void syncIncremental_fromAfterTo_throwsBusinessException() {
+ assertThatThrownBy(() -> syncService.syncIncremental(GRAPH_ID, UPDATED_TO, UPDATED_FROM))
+ .isInstanceOf(BusinessException.class)
+ .hasMessageContaining("updatedFrom");
+ }
+
+ @Test
+ void syncIncremental_success_passesTimeWindowToClient() {
+ when(properties.getSync()).thenReturn(syncConfig);
+
+ DatasetDTO dto = new DatasetDTO();
+ dto.setId("ds-001");
+ dto.setName("Test");
+ dto.setCreatedBy("admin");
+ when(dataManagementClient.listAllDatasets(UPDATED_FROM, UPDATED_TO)).thenReturn(List.of(dto));
+ when(dataManagementClient.listAllWorkflows(UPDATED_FROM, UPDATED_TO)).thenReturn(List.of());
+ when(dataManagementClient.listAllJobs(UPDATED_FROM, UPDATED_TO)).thenReturn(List.of());
+ when(dataManagementClient.listAllLabelTasks(UPDATED_FROM, UPDATED_TO)).thenReturn(List.of());
+ when(dataManagementClient.listAllKnowledgeSets(UPDATED_FROM, UPDATED_TO)).thenReturn(List.of());
+
+ stubAllEntityUpserts();
+ stubAllRelationMerges();
+
+ SyncMetadata metadata = syncService.syncIncremental(GRAPH_ID, UPDATED_FROM, UPDATED_TO);
+
+ assertThat(metadata.getSyncType()).isEqualTo(SyncMetadata.TYPE_INCREMENTAL);
+ assertThat(metadata.getStatus()).isEqualTo(SyncMetadata.STATUS_SUCCESS);
+ assertThat(metadata.getUpdatedFrom()).isEqualTo(UPDATED_FROM);
+ assertThat(metadata.getUpdatedTo()).isEqualTo(UPDATED_TO);
+ assertThat(metadata.getResults()).hasSize(18);
+
+ // 验证使用了带时间窗口的 client 方法
+ verify(dataManagementClient).listAllDatasets(UPDATED_FROM, UPDATED_TO);
+ verify(dataManagementClient).listAllWorkflows(UPDATED_FROM, UPDATED_TO);
+ verify(dataManagementClient).listAllJobs(UPDATED_FROM, UPDATED_TO);
+ verify(dataManagementClient).listAllLabelTasks(UPDATED_FROM, UPDATED_TO);
+ verify(dataManagementClient).listAllKnowledgeSets(UPDATED_FROM, UPDATED_TO);
+
+ // 验证不执行 purge
+ verify(stepService, never()).purgeStaleEntities(anyString(), anyString(), anySet(), anyString());
+ }
+
+ @Test
+ void syncIncremental_failure_recordsMetadataWithTimeWindow() {
+ when(properties.getSync()).thenReturn(syncConfig);
+ when(dataManagementClient.listAllDatasets(UPDATED_FROM, UPDATED_TO))
+ .thenThrow(new RuntimeException("connection refused"));
+
+ assertThatThrownBy(() -> syncService.syncIncremental(GRAPH_ID, UPDATED_FROM, UPDATED_TO))
+ .isInstanceOf(BusinessException.class);
+
+ ArgumentCaptor