diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeBusinessViewRowDto.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeBusinessViewRowDto.java index 0fb100a6..ea48a9c4 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeBusinessViewRowDto.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/dto/digital/DigitalEmployeeBusinessViewRowDto.java @@ -58,4 +58,5 @@ public class DigitalEmployeeBusinessViewRowDto { private String latestEventAt; @JsonProperty("last_event_message") private String lastEventMessage; + private Boolean materialized; } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/DigitalEmployeeArtifactLifecycleRepository.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/DigitalEmployeeArtifactLifecycleRepository.java new file mode 100644 index 00000000..e81fdb89 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/DigitalEmployeeArtifactLifecycleRepository.java @@ -0,0 +1,8 @@ +package com.xspaceagi.agent.core.adapter.repository; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeArtifactLifecycle; + +public interface DigitalEmployeeArtifactLifecycleRepository extends IService { + void upsertBySyncRecord(DigitalEmployeeArtifactLifecycle lifecycle); +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/DigitalEmployeeBusinessFactRepository.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/DigitalEmployeeBusinessFactRepository.java new file mode 100644 index 00000000..0536d779 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/DigitalEmployeeBusinessFactRepository.java @@ -0,0 +1,24 @@ +package com.xspaceagi.agent.core.adapter.repository; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.service.IService; +import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeBusinessFact; + +import java.util.Date; + +public interface DigitalEmployeeBusinessFactRepository extends IService { + void upsertBySyncRecord(DigitalEmployeeBusinessFact fact); + + IPage queryFacts( + Long tenantId, + String clientKey, + String deviceId, + String entityType, + String entityId, + Date updatedFrom, + Date updatedTo, + String syncStatus, + long pageNo, + long pageSize + ); +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/DigitalEmployeeDailyReportFactRepository.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/DigitalEmployeeDailyReportFactRepository.java new file mode 100644 index 00000000..3d159f00 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/DigitalEmployeeDailyReportFactRepository.java @@ -0,0 +1,8 @@ +package com.xspaceagi.agent.core.adapter.repository; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeDailyReportFact; + +public interface DigitalEmployeeDailyReportFactRepository extends IService { + void upsertBySyncRecord(DigitalEmployeeDailyReportFact fact); +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/DigitalEmployeeArtifactLifecycle.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/DigitalEmployeeArtifactLifecycle.java new file mode 100644 index 00000000..465edd93 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/DigitalEmployeeArtifactLifecycle.java @@ -0,0 +1,41 @@ +package com.xspaceagi.agent.core.adapter.repository.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@TableName("digital_employee_artifact_lifecycle") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class DigitalEmployeeArtifactLifecycle { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + @TableField(value = "_tenant_id") + private Long tenantId; + private Long syncRecordId; + private String clientKey; + private String deviceId; + private String artifactId; + private String planId; + private String taskId; + private String runId; + private String status; + private String retentionStatus; + private String cleanupStatus; + private String deletedAt; + private String versionKey; + private String source; + private String businessView; + private Date syncedAt; + private Date modified; + private Date created; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/DigitalEmployeeBusinessFact.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/DigitalEmployeeBusinessFact.java new file mode 100644 index 00000000..f445a6dd --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/DigitalEmployeeBusinessFact.java @@ -0,0 +1,50 @@ +package com.xspaceagi.agent.core.adapter.repository.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@TableName("digital_employee_business_fact") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class DigitalEmployeeBusinessFact { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + @TableField(value = "_tenant_id") + private Long tenantId; + private Long userId; + private Long syncRecordId; + private String clientKey; + private String deviceId; + private String outboxId; + private String entityType; + private String entityId; + private String operation; + private String title; + private String status; + private String summary; + private String planId; + private String taskId; + private String runId; + private String kind; + private String category; + private String occurredAt; + private String startedAt; + private String finishedAt; + private Long durationMs; + private String syncStatus; + private String businessView; + private String payload; + private Date syncedAt; + private Date modified; + private Date created; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/DigitalEmployeeDailyReportFact.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/DigitalEmployeeDailyReportFact.java new file mode 100644 index 00000000..670dd700 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-adapter/src/main/java/com/xspaceagi/agent/core/adapter/repository/entity/DigitalEmployeeDailyReportFact.java @@ -0,0 +1,38 @@ +package com.xspaceagi.agent.core.adapter.repository.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@TableName("digital_employee_daily_report_fact") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class DigitalEmployeeDailyReportFact { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + @TableField(value = "_tenant_id") + private Long tenantId; + private Long syncRecordId; + private String clientKey; + private String deviceId; + private String reportId; + private String planId; + private String status; + private String deliveryStatus; + private String approvalStatus; + private String confirmedAt; + private String reportDate; + private String businessView; + private Date syncedAt; + private Date modified; + private Date created; +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImpl.java index 8a9ca52e..d97690b1 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImpl.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/main/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImpl.java @@ -25,8 +25,14 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordPag import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncResponseDto; import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeAdminActionRepository; +import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeArtifactLifecycleRepository; +import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeBusinessFactRepository; +import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeDailyReportFactRepository; import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeSyncRecordRepository; import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeAdminAction; +import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeArtifactLifecycle; +import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeBusinessFact; +import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeDailyReportFact; import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRecord; import com.xspaceagi.system.spec.common.RequestContext; import jakarta.annotation.Resource; @@ -70,6 +76,15 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye @Resource private DigitalEmployeeAdminActionRepository digitalEmployeeAdminActionRepository; + @Resource + private DigitalEmployeeBusinessFactRepository digitalEmployeeBusinessFactRepository; + + @Resource + private DigitalEmployeeArtifactLifecycleRepository digitalEmployeeArtifactLifecycleRepository; + + @Resource + private DigitalEmployeeDailyReportFactRepository digitalEmployeeDailyReportFactRepository; + @Resource private ObjectMapper objectMapper; @@ -116,6 +131,7 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye } else { digitalEmployeeSyncRecordRepository.updateById(record); } + materializeSyncRecord(record, now); acked.add(DigitalEmployeeSyncAckDto.builder() .outboxId(item.getOutboxId()) @@ -190,6 +206,94 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye .build(); } + private void materializeSyncRecord(DigitalEmployeeSyncRecord record, Date now) { + if (record == null || record.getId() == null || digitalEmployeeBusinessFactRepository == null) return; + DigitalEmployeeBusinessViewRowDto row = toBusinessRow(record); + digitalEmployeeBusinessFactRepository.upsertBySyncRecord(DigitalEmployeeBusinessFact.builder() + .tenantId(record.getTenantId()) + .userId(record.getUserId()) + .syncRecordId(record.getId()) + .clientKey(record.getClientKey()) + .deviceId(record.getDeviceId()) + .outboxId(record.getOutboxId()) + .entityType(row.getEntityType()) + .entityId(row.getEntityId()) + .operation(row.getOperation()) + .title(row.getTitle()) + .status(row.getStatus()) + .summary(row.getSummary()) + .planId(row.getPlanId()) + .taskId(row.getTaskId()) + .runId(row.getRunId()) + .kind(row.getKind()) + .category(firstString(row.getBusinessView().get("category"), row.getEntityType())) + .occurredAt(row.getOccurredAt()) + .startedAt(row.getStartedAt()) + .finishedAt(row.getFinishedAt()) + .durationMs(row.getDurationMs()) + .syncStatus(row.getSyncStatus()) + .businessView(record.getBusinessView()) + .payload(record.getPayload()) + .syncedAt(record.getSyncedAt()) + .modified(now) + .created(now) + .build()); + materializeArtifactLifecycle(record, row, now); + materializeDailyReportFact(record, row, now); + } + + private void materializeArtifactLifecycle(DigitalEmployeeSyncRecord record, DigitalEmployeeBusinessViewRowDto row, Date now) { + if (digitalEmployeeArtifactLifecycleRepository == null || !StringUtils.equals(row.getEntityType(), "artifact")) return; + Map view = row.getBusinessView(); + String artifactId = firstString(view.get("artifact_id"), view.get("artifactId"), row.getEntityId()); + digitalEmployeeArtifactLifecycleRepository.upsertBySyncRecord(DigitalEmployeeArtifactLifecycle.builder() + .tenantId(record.getTenantId()) + .syncRecordId(record.getId()) + .clientKey(record.getClientKey()) + .deviceId(record.getDeviceId()) + .artifactId(artifactId) + .planId(row.getPlanId()) + .taskId(row.getTaskId()) + .runId(row.getRunId()) + .status(row.getStatus()) + .retentionStatus(firstString(view.get("retention_status"), view.get("retentionStatus"))) + .cleanupStatus(firstString(view.get("cleanup_status"), view.get("cleanupStatus"))) + .deletedAt(firstString(view.get("deleted_at"), view.get("deletedAt"))) + .versionKey(firstString(view.get("version_key"), view.get("versionKey"))) + .source(firstString(view.get("source"), view.get("artifact_source"), view.get("artifactSource"))) + .businessView(record.getBusinessView()) + .syncedAt(record.getSyncedAt()) + .modified(now) + .created(now) + .build()); + } + + private void materializeDailyReportFact(DigitalEmployeeSyncRecord record, DigitalEmployeeBusinessViewRowDto row, Date now) { + Map view = row.getBusinessView(); + if (digitalEmployeeDailyReportFactRepository == null + || !(StringUtils.equals(row.getEntityType(), "daily_report") || StringUtils.equals(firstString(view.get("category")), "daily_report"))) { + return; + } + String reportId = firstString(view.get("report_id"), view.get("reportId"), row.getEntityId()); + digitalEmployeeDailyReportFactRepository.upsertBySyncRecord(DigitalEmployeeDailyReportFact.builder() + .tenantId(record.getTenantId()) + .syncRecordId(record.getId()) + .clientKey(record.getClientKey()) + .deviceId(record.getDeviceId()) + .reportId(reportId) + .planId(row.getPlanId()) + .status(row.getStatus()) + .deliveryStatus(firstString(view.get("delivery_status"), view.get("deliveryStatus"))) + .approvalStatus(firstString(view.get("approval_status"), view.get("approvalStatus"))) + .confirmedAt(firstString(view.get("confirmed_at"), view.get("confirmedAt"))) + .reportDate(firstString(view.get("report_date"), view.get("reportDate"))) + .businessView(record.getBusinessView()) + .syncedAt(record.getSyncedAt()) + .modified(now) + .created(now) + .build()); + } + @Override public DigitalEmployeeBusinessViewPageDto queryBusinessViews( String entityType, @@ -214,6 +318,28 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye String normalizedEntityType = normalizeBusinessEntityType(entityType); String repositoryEntityType = "plan".equals(normalizedEntityType) ? null : normalizedEntityType; + List materializedRows = queryMaterializedBusinessRows( + tenantId, + clientKey, + deviceId, + repositoryEntityType, + entityId, + updatedFrom, + updatedTo, + syncStatus, + currentPage, + currentPageSize + ); + if (materializedRows != null) { + List rows = "plan".equals(normalizedEntityType) + ? enrichPlanRows(materializedRows) + : materializedRows.stream().filter(row -> StringUtils.equals(row.getEntityType(), normalizedEntityType)).toList(); + rows = rows.stream() + .filter(row -> matchesText(status, row.getStatus())) + .filter(row -> matchesText(syncStatus, row.getSyncStatus())) + .toList(); + return businessPage(rows, currentPage, currentPageSize); + } IPage page = digitalEmployeeSyncRecordRepository.queryBusinessRecords( tenantId, clientKey, @@ -239,6 +365,35 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye return businessPage(rows, currentPage, currentPageSize); } + private List queryMaterializedBusinessRows( + Long tenantId, + String clientKey, + String deviceId, + String entityType, + String entityId, + Date updatedFrom, + Date updatedTo, + String syncStatus, + long pageNo, + long pageSize + ) { + if (digitalEmployeeBusinessFactRepository == null) return null; + IPage factPage = digitalEmployeeBusinessFactRepository.queryFacts( + tenantId, + clientKey, + deviceId, + entityType, + entityId, + updatedFrom, + updatedTo, + syncStatus, + pageNo, + pageSize + ); + if (factPage == null || factPage.getTotal() <= 0) return null; + return factPage.getRecords().stream().map(this::toBusinessRow).toList(); + } + @Override public DigitalEmployeePlanBusinessViewDetailDto queryPlanBusinessViewDetail( String planId, @@ -814,6 +969,38 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye .businessView(businessView) .payload(payload) .lastEventMessage(lastEventMessage(entityType, businessView, payload)) + .materialized(false) + .build(); + } + + private DigitalEmployeeBusinessViewRowDto toBusinessRow(DigitalEmployeeBusinessFact fact) { + return DigitalEmployeeBusinessViewRowDto.builder() + .syncRecordId(fact.getSyncRecordId()) + .syncRecordUrl(syncRecordUrl(fact.getOutboxId())) + .entityType(fact.getEntityType()) + .entityId(fact.getEntityId()) + .operation(fact.getOperation()) + .title(fact.getTitle()) + .status(fact.getStatus()) + .summary(fact.getSummary()) + .planId(fact.getPlanId()) + .taskId(fact.getTaskId()) + .runId(fact.getRunId()) + .kind(fact.getKind()) + .occurredAt(fact.getOccurredAt()) + .startedAt(fact.getStartedAt()) + .finishedAt(fact.getFinishedAt()) + .durationMs(fact.getDurationMs()) + .syncStatus(fact.getSyncStatus()) + .clientKeyMasked(maskClientKey(fact.getClientKey())) + .deviceId(fact.getDeviceId()) + .syncedAt(fact.getSyncedAt()) + .modified(fact.getModified()) + .created(fact.getCreated()) + .businessView(readJsonObject(fact.getBusinessView())) + .payload(readJsonObject(fact.getPayload())) + .lastEventMessage(lastEventMessage(fact.getEntityType(), readJsonObject(fact.getBusinessView()), readJsonObject(fact.getPayload()))) + .materialized(true) .build(); } diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/test/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImplTest.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/test/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImplTest.java index 91a9b88e..4cd6c7ad 100644 --- a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/test/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImplTest.java +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-application/src/test/java/com/xspaceagi/agent/core/application/service/DigitalEmployeeSyncApplicationServiceImplTest.java @@ -18,8 +18,12 @@ import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncItemDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRecordPageDto; import com.xspaceagi.agent.core.adapter.dto.digital.DigitalEmployeeSyncRequestDto; import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeAdminActionRepository; +import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeArtifactLifecycleRepository; +import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeBusinessFactRepository; +import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeDailyReportFactRepository; import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeSyncRecordRepository; import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeAdminAction; +import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeBusinessFact; import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeSyncRecord; import com.xspaceagi.system.spec.common.RequestContext; import org.junit.jupiter.api.AfterEach; @@ -35,6 +39,7 @@ import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -44,6 +49,9 @@ class DigitalEmployeeSyncApplicationServiceImplTest { private DigitalEmployeeSyncRecordRepository repository; private DigitalEmployeeAdminActionRepository adminActionRepository; + private DigitalEmployeeBusinessFactRepository businessFactRepository; + private DigitalEmployeeArtifactLifecycleRepository artifactLifecycleRepository; + private DigitalEmployeeDailyReportFactRepository dailyReportFactRepository; private DigitalEmployeeSyncApplicationServiceImpl service; @BeforeEach @@ -55,9 +63,15 @@ class DigitalEmployeeSyncApplicationServiceImplTest { repository = mock(DigitalEmployeeSyncRecordRepository.class); adminActionRepository = mock(DigitalEmployeeAdminActionRepository.class); + businessFactRepository = mock(DigitalEmployeeBusinessFactRepository.class); + artifactLifecycleRepository = mock(DigitalEmployeeArtifactLifecycleRepository.class); + dailyReportFactRepository = mock(DigitalEmployeeDailyReportFactRepository.class); service = new DigitalEmployeeSyncApplicationServiceImpl(); ReflectionTestUtils.setField(service, "digitalEmployeeSyncRecordRepository", repository); ReflectionTestUtils.setField(service, "digitalEmployeeAdminActionRepository", adminActionRepository); + ReflectionTestUtils.setField(service, "digitalEmployeeBusinessFactRepository", businessFactRepository); + ReflectionTestUtils.setField(service, "digitalEmployeeArtifactLifecycleRepository", artifactLifecycleRepository); + ReflectionTestUtils.setField(service, "digitalEmployeeDailyReportFactRepository", dailyReportFactRepository); ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper()); } @@ -82,6 +96,11 @@ class DigitalEmployeeSyncApplicationServiceImplTest { request.setClientKey("client-key-001"); request.setDeviceId("device-001"); request.setItems(List.of(item)); + doAnswer(invocation -> { + DigitalEmployeeSyncRecord saved = invocation.getArgument(0); + saved.setId(1001L); + return true; + }).when(repository).save(any(DigitalEmployeeSyncRecord.class)); service.syncOutbox(request); @@ -94,6 +113,11 @@ class DigitalEmployeeSyncApplicationServiceImplTest { assertThat(record.getPayload()).contains("plan-1"); assertThat(record.getTenantId()).isEqualTo(101L); assertThat(record.getUserId()).isEqualTo(202L); + ArgumentCaptor factCaptor = ArgumentCaptor.forClass(DigitalEmployeeBusinessFact.class); + verify(businessFactRepository).upsertBySyncRecord(factCaptor.capture()); + assertThat(factCaptor.getValue().getSyncRecordId()).isEqualTo(1001L); + assertThat(factCaptor.getValue().getTitle()).isEqualTo("客户回访"); + assertThat(factCaptor.getValue().getStatus()).isEqualTo("active"); } @Test @@ -140,6 +164,22 @@ class DigitalEmployeeSyncApplicationServiceImplTest { assertThat(plan.getClientKeyMasked()).isEqualTo("clie*********001"); } + @Test + void queryBusinessViewsPrefersMaterializedFactsWhenAvailable() { + DigitalEmployeeBusinessFact fact = businessFact("plan-1", "客户回访", "active", new Date(10_000)); + Page page = new Page<>(1, 20, 1); + page.setRecords(List.of(fact)); + when(businessFactRepository.queryFacts(eq(101L), eq("client-key-001"), eq(null), eq(null), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(20L))) + .thenReturn(page); + + DigitalEmployeeBusinessViewPageDto result = service.queryBusinessViews("plan", "client-key-001", null, null, null, null, null, null, 1L, 20L); + + assertThat(result.getRecords()).hasSize(1); + assertThat(result.getRecords().get(0).getTitle()).isEqualTo("客户回访"); + assertThat(result.getRecords().get(0).getMaterialized()).isTrue(); + verify(repository, never()).queryBusinessRecords(any(), any(), any(), any(), any(), any(), any(), any(), any(Long.class), any(Long.class)); + } + @Test void queryEventsFiltersStatusEntityAndTimeRangeFromBusinessView() { Date from = new Date(10_000); @@ -416,6 +456,30 @@ class DigitalEmployeeSyncApplicationServiceImplTest { return record; } + private DigitalEmployeeBusinessFact businessFact(String planId, String title, String status, Date syncedAt) { + DigitalEmployeeBusinessFact fact = new DigitalEmployeeBusinessFact(); + fact.setId(7001L); + fact.setTenantId(101L); + fact.setUserId(202L); + fact.setSyncRecordId(1001L); + fact.setClientKey("client-key-001"); + fact.setDeviceId("device-001"); + fact.setOutboxId("outbox-" + planId); + fact.setEntityType("plan"); + fact.setEntityId(planId); + fact.setOperation("upsert"); + fact.setTitle(title); + fact.setStatus(status); + fact.setSummary("计划摘要"); + fact.setPlanId(planId); + fact.setBusinessView(toJson(Map.of("entity_type", "plan", "local_id", planId, "title", title, "status", status))); + fact.setPayload(toJson(Map.of("id", planId, "title", title, "status", status))); + fact.setSyncedAt(syncedAt); + fact.setCreated(syncedAt); + fact.setModified(syncedAt); + return fact; + } + private DigitalEmployeeSyncRecord planRecord(String planId, String title, String status, Long tenantId, Date syncedAt) { return record("outbox-" + planId, "plan", planId, tenantId, syncedAt, Map.of("entity_type", "plan", "local_id", planId, "title", title, "status", status, "summary", "计划摘要"), diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/DigitalEmployeeArtifactLifecycleMapper.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/DigitalEmployeeArtifactLifecycleMapper.java new file mode 100644 index 00000000..61ba5b5d --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/DigitalEmployeeArtifactLifecycleMapper.java @@ -0,0 +1,9 @@ +package com.xspaceagi.agent.core.infra.dao.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeArtifactLifecycle; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DigitalEmployeeArtifactLifecycleMapper extends BaseMapper { +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/DigitalEmployeeBusinessFactMapper.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/DigitalEmployeeBusinessFactMapper.java new file mode 100644 index 00000000..47b29f8a --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/DigitalEmployeeBusinessFactMapper.java @@ -0,0 +1,9 @@ +package com.xspaceagi.agent.core.infra.dao.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeBusinessFact; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DigitalEmployeeBusinessFactMapper extends BaseMapper { +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/DigitalEmployeeDailyReportFactMapper.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/DigitalEmployeeDailyReportFactMapper.java new file mode 100644 index 00000000..7c8f9cb0 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/dao/mapper/DigitalEmployeeDailyReportFactMapper.java @@ -0,0 +1,9 @@ +package com.xspaceagi.agent.core.infra.dao.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeDailyReportFact; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DigitalEmployeeDailyReportFactMapper extends BaseMapper { +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/DigitalEmployeeArtifactLifecycleRepositoryImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/DigitalEmployeeArtifactLifecycleRepositoryImpl.java new file mode 100644 index 00000000..9331de5f --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/DigitalEmployeeArtifactLifecycleRepositoryImpl.java @@ -0,0 +1,29 @@ +package com.xspaceagi.agent.core.infra.repository; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeArtifactLifecycleRepository; +import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeArtifactLifecycle; +import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeArtifactLifecycleMapper; +import org.springframework.stereotype.Service; + +@Service +public class DigitalEmployeeArtifactLifecycleRepositoryImpl + extends ServiceImpl + implements DigitalEmployeeArtifactLifecycleRepository { + + @Override + public void upsertBySyncRecord(DigitalEmployeeArtifactLifecycle lifecycle) { + if (lifecycle == null || lifecycle.getTenantId() == null || lifecycle.getSyncRecordId() == null) return; + DigitalEmployeeArtifactLifecycle existing = getOne(new LambdaQueryWrapper() + .eq(DigitalEmployeeArtifactLifecycle::getTenantId, lifecycle.getTenantId()) + .eq(DigitalEmployeeArtifactLifecycle::getSyncRecordId, lifecycle.getSyncRecordId()), false); + if (existing == null) { + save(lifecycle); + } else { + lifecycle.setId(existing.getId()); + lifecycle.setCreated(existing.getCreated()); + updateById(lifecycle); + } + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/DigitalEmployeeBusinessFactRepositoryImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/DigitalEmployeeBusinessFactRepositoryImpl.java new file mode 100644 index 00000000..061dd144 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/DigitalEmployeeBusinessFactRepositoryImpl.java @@ -0,0 +1,61 @@ +package com.xspaceagi.agent.core.infra.repository; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeBusinessFactRepository; +import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeBusinessFact; +import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeBusinessFactMapper; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.Date; + +@Service +public class DigitalEmployeeBusinessFactRepositoryImpl + extends ServiceImpl + implements DigitalEmployeeBusinessFactRepository { + + @Override + public void upsertBySyncRecord(DigitalEmployeeBusinessFact fact) { + if (fact == null || fact.getTenantId() == null || fact.getSyncRecordId() == null) return; + DigitalEmployeeBusinessFact existing = getOne(new LambdaQueryWrapper() + .eq(DigitalEmployeeBusinessFact::getTenantId, fact.getTenantId()) + .eq(DigitalEmployeeBusinessFact::getSyncRecordId, fact.getSyncRecordId()), false); + if (existing == null) { + save(fact); + } else { + fact.setId(existing.getId()); + fact.setCreated(existing.getCreated()); + updateById(fact); + } + } + + @Override + public IPage queryFacts( + Long tenantId, + String clientKey, + String deviceId, + String entityType, + String entityId, + Date updatedFrom, + Date updatedTo, + String syncStatus, + long pageNo, + long pageSize + ) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(tenantId != null, DigitalEmployeeBusinessFact::getTenantId, tenantId) + .eq(StringUtils.isNotBlank(clientKey), DigitalEmployeeBusinessFact::getClientKey, clientKey) + .eq(StringUtils.isNotBlank(deviceId), DigitalEmployeeBusinessFact::getDeviceId, deviceId) + .eq(StringUtils.isNotBlank(entityType), DigitalEmployeeBusinessFact::getEntityType, entityType) + .eq(StringUtils.isNotBlank(entityId), DigitalEmployeeBusinessFact::getEntityId, entityId) + .eq(StringUtils.isNotBlank(syncStatus), DigitalEmployeeBusinessFact::getSyncStatus, syncStatus) + .ge(updatedFrom != null, DigitalEmployeeBusinessFact::getSyncedAt, updatedFrom) + .le(updatedTo != null, DigitalEmployeeBusinessFact::getSyncedAt, updatedTo) + .orderByDesc(DigitalEmployeeBusinessFact::getSyncedAt) + .orderByDesc(DigitalEmployeeBusinessFact::getId); + return page(new Page<>(pageNo, pageSize), queryWrapper); + } +} diff --git a/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/DigitalEmployeeDailyReportFactRepositoryImpl.java b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/DigitalEmployeeDailyReportFactRepositoryImpl.java new file mode 100644 index 00000000..a3cf6839 --- /dev/null +++ b/qiming-backend/app-platform-modules/app-platform-agent/app-platform-agent-core-infra/src/main/java/com/xspaceagi/agent/core/infra/repository/DigitalEmployeeDailyReportFactRepositoryImpl.java @@ -0,0 +1,29 @@ +package com.xspaceagi.agent.core.infra.repository; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.xspaceagi.agent.core.adapter.repository.DigitalEmployeeDailyReportFactRepository; +import com.xspaceagi.agent.core.adapter.repository.entity.DigitalEmployeeDailyReportFact; +import com.xspaceagi.agent.core.infra.dao.mapper.DigitalEmployeeDailyReportFactMapper; +import org.springframework.stereotype.Service; + +@Service +public class DigitalEmployeeDailyReportFactRepositoryImpl + extends ServiceImpl + implements DigitalEmployeeDailyReportFactRepository { + + @Override + public void upsertBySyncRecord(DigitalEmployeeDailyReportFact fact) { + if (fact == null || fact.getTenantId() == null || fact.getSyncRecordId() == null) return; + DigitalEmployeeDailyReportFact existing = getOne(new LambdaQueryWrapper() + .eq(DigitalEmployeeDailyReportFact::getTenantId, fact.getTenantId()) + .eq(DigitalEmployeeDailyReportFact::getSyncRecordId, fact.getSyncRecordId()), false); + if (existing == null) { + save(fact); + } else { + fact.setId(existing.getId()); + fact.setCreated(existing.getCreated()); + updateById(fact); + } + } +} diff --git a/qiming-backend/sql/update-20260612.sql b/qiming-backend/sql/update-20260612.sql index 84b3b311..6d17da77 100644 --- a/qiming-backend/sql/update-20260612.sql +++ b/qiming-backend/sql/update-20260612.sql @@ -23,6 +23,91 @@ CREATE TABLE IF NOT EXISTS `digital_employee_admin_action` ( KEY `idx_de_admin_action_entity` (`_tenant_id`, `entity_type`, `entity_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字员工管理端远程动作意图'; +CREATE TABLE IF NOT EXISTS `digital_employee_business_fact` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `_tenant_id` BIGINT NOT NULL COMMENT '租户ID', + `user_id` BIGINT DEFAULT NULL COMMENT '用户ID', + `sync_record_id` BIGINT NOT NULL COMMENT '同步记录ID', + `client_key` VARCHAR(255) DEFAULT NULL COMMENT '客户端Key', + `device_id` VARCHAR(255) DEFAULT NULL COMMENT '设备ID', + `outbox_id` VARCHAR(255) DEFAULT NULL COMMENT '客户端outbox ID', + `entity_type` VARCHAR(64) NOT NULL COMMENT '实体类型', + `entity_id` VARCHAR(255) NOT NULL COMMENT '实体ID', + `operation` VARCHAR(64) DEFAULT NULL COMMENT '操作', + `title` VARCHAR(512) DEFAULT NULL COMMENT '标题', + `status` VARCHAR(128) DEFAULT NULL COMMENT '业务状态', + `summary` VARCHAR(1024) DEFAULT NULL COMMENT '摘要', + `plan_id` VARCHAR(255) DEFAULT NULL COMMENT '计划ID', + `task_id` VARCHAR(255) DEFAULT NULL COMMENT '任务ID', + `run_id` VARCHAR(255) DEFAULT NULL COMMENT '运行ID', + `kind` VARCHAR(255) DEFAULT NULL COMMENT '类型', + `category` VARCHAR(128) DEFAULT NULL COMMENT '分类', + `occurred_at` VARCHAR(64) DEFAULT NULL COMMENT '发生时间', + `started_at` VARCHAR(64) DEFAULT NULL COMMENT '开始时间', + `finished_at` VARCHAR(64) DEFAULT NULL COMMENT '结束时间', + `duration_ms` BIGINT DEFAULT NULL COMMENT '耗时毫秒', + `sync_status` VARCHAR(128) DEFAULT NULL COMMENT '同步状态', + `business_view` JSON DEFAULT NULL COMMENT '业务视图快照', + `payload` JSON DEFAULT NULL COMMENT '安全payload快照', + `synced_at` DATETIME DEFAULT NULL COMMENT '同步时间', + `modified` DATETIME NOT NULL COMMENT '修改时间', + `created` DATETIME NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_de_business_fact_record` (`_tenant_id`, `sync_record_id`), + KEY `idx_de_business_fact_entity` (`_tenant_id`, `entity_type`, `entity_id`), + KEY `idx_de_business_fact_plan` (`_tenant_id`, `plan_id`, `entity_type`), + KEY `idx_de_business_fact_client` (`_tenant_id`, `client_key`, `device_id`, `synced_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字员工业务事实物化表'; + +CREATE TABLE IF NOT EXISTS `digital_employee_artifact_lifecycle` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `_tenant_id` BIGINT NOT NULL COMMENT '租户ID', + `sync_record_id` BIGINT NOT NULL COMMENT '同步记录ID', + `client_key` VARCHAR(255) DEFAULT NULL COMMENT '客户端Key', + `device_id` VARCHAR(255) DEFAULT NULL COMMENT '设备ID', + `artifact_id` VARCHAR(255) NOT NULL COMMENT '产物ID', + `plan_id` VARCHAR(255) DEFAULT NULL COMMENT '计划ID', + `task_id` VARCHAR(255) DEFAULT NULL COMMENT '任务ID', + `run_id` VARCHAR(255) DEFAULT NULL COMMENT '运行ID', + `status` VARCHAR(128) DEFAULT NULL COMMENT '状态', + `retention_status` VARCHAR(128) DEFAULT NULL COMMENT '保留状态', + `cleanup_status` VARCHAR(128) DEFAULT NULL COMMENT '清理状态', + `deleted_at` VARCHAR(64) DEFAULT NULL COMMENT '删除时间', + `version_key` VARCHAR(255) DEFAULT NULL COMMENT '版本Key', + `source` VARCHAR(255) DEFAULT NULL COMMENT '来源', + `business_view` JSON DEFAULT NULL COMMENT '业务视图快照', + `synced_at` DATETIME DEFAULT NULL COMMENT '同步时间', + `modified` DATETIME NOT NULL COMMENT '修改时间', + `created` DATETIME NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_de_artifact_lifecycle_record` (`_tenant_id`, `sync_record_id`), + KEY `idx_de_artifact_lifecycle_artifact` (`_tenant_id`, `artifact_id`), + KEY `idx_de_artifact_lifecycle_plan` (`_tenant_id`, `plan_id`, `task_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字员工产物生命周期物化表'; + +CREATE TABLE IF NOT EXISTS `digital_employee_daily_report_fact` ( + `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `_tenant_id` BIGINT NOT NULL COMMENT '租户ID', + `sync_record_id` BIGINT NOT NULL COMMENT '同步记录ID', + `client_key` VARCHAR(255) DEFAULT NULL COMMENT '客户端Key', + `device_id` VARCHAR(255) DEFAULT NULL COMMENT '设备ID', + `report_id` VARCHAR(255) NOT NULL COMMENT '日报ID', + `plan_id` VARCHAR(255) DEFAULT NULL COMMENT '计划ID', + `status` VARCHAR(128) DEFAULT NULL COMMENT '状态', + `delivery_status` VARCHAR(128) DEFAULT NULL COMMENT '交付状态', + `approval_status` VARCHAR(128) DEFAULT NULL COMMENT '审批状态', + `confirmed_at` VARCHAR(64) DEFAULT NULL COMMENT '确认时间', + `report_date` VARCHAR(64) DEFAULT NULL COMMENT '日报日期', + `business_view` JSON DEFAULT NULL COMMENT '业务视图快照', + `synced_at` DATETIME DEFAULT NULL COMMENT '同步时间', + `modified` DATETIME NOT NULL COMMENT '修改时间', + `created` DATETIME NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_de_daily_report_fact_record` (`_tenant_id`, `sync_record_id`), + KEY `idx_de_daily_report_fact_report` (`_tenant_id`, `report_id`), + KEY `idx_de_daily_report_fact_client` (`_tenant_id`, `client_key`, `device_id`, `synced_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数字员工日报事实物化表'; + -- Add digital employee admin operations workbench menu and resource. -- Idempotent for existing tenants: creates menu/resource rows, binds menu to resource, -- and grants super_admin default access. Other roles can be granted this menu diff --git a/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx index 268b463e..ee68d413 100644 --- a/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx +++ b/qiming/src/pages/SystemManagement/Content/DigitalEmployeeOps/index.tsx @@ -99,6 +99,7 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) { {title} {record.status && {record.status}} + {record.materialized && materialized} ; diff --git a/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js b/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js index d3640b9c..84a3b028 100644 --- a/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js +++ b/qimingclaw/crates/agent-electron-client/scripts/check-digital-employee.js @@ -1241,6 +1241,71 @@ function checkDigitalAdminPolicySnapshots() { console.log("[DigitalEmployeeCheck] admin policy snapshot conflict view hooks OK"); } +function checkDigitalBackendMaterializedFacts() { + console.log("\n[DigitalEmployeeCheck] Verify backend materialized business fact hooks"); + const backendRoot = path.resolve(packageRoot, "..", "..", "..", "qiming-backend"); + const qimingRoot = path.resolve(packageRoot, "..", "..", "..", "qiming"); + const servicePath = path.join( + backendRoot, + "app-platform-modules", + "app-platform-agent", + "app-platform-agent-core-application", + "src", + "main", + "java", + "com", + "xspaceagi", + "agent", + "core", + "application", + "service", + "DigitalEmployeeSyncApplicationServiceImpl.java", + ); + const sqlPath = path.join(backendRoot, "sql", "update-20260612.sql"); + const dtoPath = path.join( + backendRoot, + "app-platform-modules", + "app-platform-agent", + "app-platform-agent-core-adapter", + "src", + "main", + "java", + "com", + "xspaceagi", + "agent", + "core", + "adapter", + "dto", + "digital", + "DigitalEmployeeBusinessViewRowDto.java", + ); + const qimingTypesPath = path.join(qimingRoot, "src", "types", "interfaces", "systemManage.ts"); + const docsPath = path.resolve(packageRoot, "..", "..", "docs", "digital-employee-frontend-integration-plan.md"); + const service = fs.readFileSync(servicePath, "utf8"); + const sql = fs.readFileSync(sqlPath, "utf8"); + const dto = fs.readFileSync(dtoPath, "utf8"); + const qimingTypes = fs.readFileSync(qimingTypesPath, "utf8"); + const docs = fs.readFileSync(docsPath, "utf8"); + const requiredSnippets = [ + [sql, "digital_employee_business_fact", "business fact table migration"], + [sql, "digital_employee_artifact_lifecycle", "artifact lifecycle table migration"], + [sql, "digital_employee_daily_report_fact", "daily report fact table migration"], + [service, "materializeSyncRecord", "sync materialization hook"], + [service, "upsertBySyncRecord", "materialized upsert call"], + [service, "queryMaterializedBusinessRows", "materialized query preference"], + [dto, "materialized", "business row materialized flag"], + [qimingTypes, "materialized", "qiming materialized type field"], + [docs, "业务事实物化查询", "docs materialized fact absorption"], + ]; + const missing = requiredSnippets + .filter(([content, snippet]) => !content.includes(snippet)) + .map(([, , label]) => label); + if (missing.length > 0) { + throw new Error(`Missing backend materialized fact hooks: ${missing.join(", ")}`); + } + console.log("[DigitalEmployeeCheck] backend materialized business fact hooks OK"); +} + function checkLocalDatabaseSchema() { console.log("\n[DigitalEmployeeCheck] Verify local SQLite digital schema"); const dbPath = path.join(os.homedir(), ".qimingclaw", "qimingclaw.db"); @@ -1301,6 +1366,7 @@ function main() { checkDigitalPolicySchedulerWorkbenches(); checkDigitalAdminActionLoop(); checkDigitalAdminPolicySnapshots(); + checkDigitalBackendMaterializedFacts(); checkLocalDatabaseSchema(); console.log("\n[DigitalEmployeeCheck] All checks passed"); } catch (error) { diff --git a/qimingclaw/docs/digital-employee-frontend-integration-plan.md b/qimingclaw/docs/digital-employee-frontend-integration-plan.md index 4127610b..e04b5dcf 100644 --- a/qimingclaw/docs/digital-employee-frontend-integration-plan.md +++ b/qimingclaw/docs/digital-employee-frontend-integration-plan.md @@ -609,7 +609,7 @@ GET /api/digital-employee/approvals - 建议:下一轮围绕管理端正式审计视图和诊断修复动作,把本地可追溯审计继续推进到可运营处置。 12. **管理端业务查询模型** - - 已吸收:管理端已有通用 sync record 接收、查询、payload 摘要和深链;后端 sync record 已开始保存客户端顶层 `contract_version`、`entity_summary` 和 `business_view`,并基于现有 `digital_employee_sync_record` 拆出 `/api/digital-employee/plans`、`/plans/{plan_id}`、`/tasks`、`/runs`、`/events`、`/artifacts`、`/approvals` 管理端业务查询模型;列表支持客户端 Key、设备、实体 ID、状态、同步状态和同步时间范围过滤,业务 row 会提炼 title/status/summary、Plan/Task/Run 关联、发生/开始/结束时间、sync record 深链和脱敏客户端 Key;Plan 视图会聚合 task/run 计数与最近事件,Plan 详情会返回关联 tasks/runs/events/artifacts/approvals 摘要。embedded adapter 已拆出同名只读业务查询接口,从 qimingclaw runtime、artifact、approval 和 canonical event 投影生成统一分页视图;embedded 数字员工页已新增“业务视图”入口,支持计划/任务/运行/事件/产物/审批分段、实体/状态/同步状态/时间筛选、分页和详情 JSON 查看;客户端 outbox 的 event `business_view` 已开始按 route decision、governance command、approval action、artifact lifecycle/access、skill call audit 和 ready PlanStep dispatch 细分常用字段,本地与管理端业务视图投影已开始闭环。正式外部管理端台面已开始吸收:qiming-backend 新增 `/api/digital-employee/admin/workbench/{view}`,qiming 管理端新增“数字员工运营台”,统一展示业务视图、介入台、产物治理、通知运营、审计诊断、日报运营和路由治理,只读消费 sync record `business_view` 并提供同步记录深链。管理端低风险动作闭环已开始吸收:后端新增 `/api/digital-employee/admin/actions` 记录动作意图和 `/pending` 拉取队列,qimingclaw 只应用保留本地、标记复核、通知已读/忽略、产物保留/到期、日报交付/确认/审批请求等低风险动作并写入 `admin_action_applied` / `admin_action_rejected` / `admin_action_failed`,qiming 运营台抽屉只提示“已记录,等待客户端拉取”。跨设备策略冲突视图已开始吸收:qimingclaw 将派发、风险审批和入口路由策略同步事件投影为 `policy_snapshot` business view,后端新增 `/api/digital-employee/admin/policies/snapshots` 聚合 client/device 维度差异并输出 `conflict_keys`,qiming 运营台新增“策略冲突” tab 展示 policy drift 与 sync record 深链。 + - 已吸收:管理端已有通用 sync record 接收、查询、payload 摘要和深链;后端 sync record 已开始保存客户端顶层 `contract_version`、`entity_summary` 和 `business_view`,并基于现有 `digital_employee_sync_record` 拆出 `/api/digital-employee/plans`、`/plans/{plan_id}`、`/tasks`、`/runs`、`/events`、`/artifacts`、`/approvals` 管理端业务查询模型;列表支持客户端 Key、设备、实体 ID、状态、同步状态和同步时间范围过滤,业务 row 会提炼 title/status/summary、Plan/Task/Run 关联、发生/开始/结束时间、sync record 深链和脱敏客户端 Key;Plan 视图会聚合 task/run 计数与最近事件,Plan 详情会返回关联 tasks/runs/events/artifacts/approvals 摘要。embedded adapter 已拆出同名只读业务查询接口,从 qimingclaw runtime、artifact、approval 和 canonical event 投影生成统一分页视图;embedded 数字员工页已新增“业务视图”入口,支持计划/任务/运行/事件/产物/审批分段、实体/状态/同步状态/时间筛选、分页和详情 JSON 查看;客户端 outbox 的 event `business_view` 已开始按 route decision、governance command、approval action、artifact lifecycle/access、skill call audit 和 ready PlanStep dispatch 细分常用字段,本地与管理端业务视图投影已开始闭环。正式外部管理端台面已开始吸收:qiming-backend 新增 `/api/digital-employee/admin/workbench/{view}`,qiming 管理端新增“数字员工运营台”,统一展示业务视图、介入台、产物治理、通知运营、审计诊断、日报运营和路由治理,只读消费 sync record `business_view` 并提供同步记录深链。管理端低风险动作闭环已开始吸收:后端新增 `/api/digital-employee/admin/actions` 记录动作意图和 `/pending` 拉取队列,qimingclaw 只应用保留本地、标记复核、通知已读/忽略、产物保留/到期、日报交付/确认/审批请求等低风险动作并写入 `admin_action_applied` / `admin_action_rejected` / `admin_action_failed`,qiming 运营台抽屉只提示“已记录,等待客户端拉取”。跨设备策略冲突视图已开始吸收:qimingclaw 将派发、风险审批和入口路由策略同步事件投影为 `policy_snapshot` business view,后端新增 `/api/digital-employee/admin/policies/snapshots` 聚合 client/device 维度差异并输出 `conflict_keys`,qiming 运营台新增“策略冲突” tab 展示 policy drift 与 sync record 深链。业务事实物化查询已开始吸收:后端新增 `digital_employee_business_fact`、`digital_employee_artifact_lifecycle`、`digital_employee_daily_report_fact` 三张增量物化表,同步入库时 upsert 安全业务摘要,业务查询优先返回 `materialized: true` 行,旧记录继续回退 sync record。 - 缺口:复杂组合检索、跨设备聚合图、远程裁决/策略编辑动作和物化业务表仍待吸收。 - 建议:先使用 sync record 派生业务查询模型和 embedded 业务视图预览承接联调,再按容量和查询压力决定是否拆 Plan / Task / Run / Event 物理业务表。