吸收数字员工业务事实物化查询
This commit is contained in:
@@ -58,4 +58,5 @@ public class DigitalEmployeeBusinessViewRowDto {
|
||||
private String latestEventAt;
|
||||
@JsonProperty("last_event_message")
|
||||
private String lastEventMessage;
|
||||
private Boolean materialized;
|
||||
}
|
||||
|
||||
@@ -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<DigitalEmployeeArtifactLifecycle> {
|
||||
void upsertBySyncRecord(DigitalEmployeeArtifactLifecycle lifecycle);
|
||||
}
|
||||
@@ -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<DigitalEmployeeBusinessFact> {
|
||||
void upsertBySyncRecord(DigitalEmployeeBusinessFact fact);
|
||||
|
||||
IPage<DigitalEmployeeBusinessFact> queryFacts(
|
||||
Long tenantId,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String entityType,
|
||||
String entityId,
|
||||
Date updatedFrom,
|
||||
Date updatedTo,
|
||||
String syncStatus,
|
||||
long pageNo,
|
||||
long pageSize
|
||||
);
|
||||
}
|
||||
@@ -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<DigitalEmployeeDailyReportFact> {
|
||||
void upsertBySyncRecord(DigitalEmployeeDailyReportFact fact);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<String, Object> 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<String, Object> 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<DigitalEmployeeBusinessViewRowDto> materializedRows = queryMaterializedBusinessRows(
|
||||
tenantId,
|
||||
clientKey,
|
||||
deviceId,
|
||||
repositoryEntityType,
|
||||
entityId,
|
||||
updatedFrom,
|
||||
updatedTo,
|
||||
syncStatus,
|
||||
currentPage,
|
||||
currentPageSize
|
||||
);
|
||||
if (materializedRows != null) {
|
||||
List<DigitalEmployeeBusinessViewRowDto> 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<DigitalEmployeeSyncRecord> page = digitalEmployeeSyncRecordRepository.queryBusinessRecords(
|
||||
tenantId,
|
||||
clientKey,
|
||||
@@ -239,6 +365,35 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
return businessPage(rows, currentPage, currentPageSize);
|
||||
}
|
||||
|
||||
private List<DigitalEmployeeBusinessViewRowDto> 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<DigitalEmployeeBusinessFact> 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<DigitalEmployeeBusinessFact> 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<DigitalEmployeeBusinessFact> 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", "计划摘要"),
|
||||
|
||||
@@ -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<DigitalEmployeeArtifactLifecycle> {
|
||||
}
|
||||
@@ -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<DigitalEmployeeBusinessFact> {
|
||||
}
|
||||
@@ -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<DigitalEmployeeDailyReportFact> {
|
||||
}
|
||||
@@ -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<DigitalEmployeeArtifactLifecycleMapper, DigitalEmployeeArtifactLifecycle>
|
||||
implements DigitalEmployeeArtifactLifecycleRepository {
|
||||
|
||||
@Override
|
||||
public void upsertBySyncRecord(DigitalEmployeeArtifactLifecycle lifecycle) {
|
||||
if (lifecycle == null || lifecycle.getTenantId() == null || lifecycle.getSyncRecordId() == null) return;
|
||||
DigitalEmployeeArtifactLifecycle existing = getOne(new LambdaQueryWrapper<DigitalEmployeeArtifactLifecycle>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<DigitalEmployeeBusinessFactMapper, DigitalEmployeeBusinessFact>
|
||||
implements DigitalEmployeeBusinessFactRepository {
|
||||
|
||||
@Override
|
||||
public void upsertBySyncRecord(DigitalEmployeeBusinessFact fact) {
|
||||
if (fact == null || fact.getTenantId() == null || fact.getSyncRecordId() == null) return;
|
||||
DigitalEmployeeBusinessFact existing = getOne(new LambdaQueryWrapper<DigitalEmployeeBusinessFact>()
|
||||
.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<DigitalEmployeeBusinessFact> queryFacts(
|
||||
Long tenantId,
|
||||
String clientKey,
|
||||
String deviceId,
|
||||
String entityType,
|
||||
String entityId,
|
||||
Date updatedFrom,
|
||||
Date updatedTo,
|
||||
String syncStatus,
|
||||
long pageNo,
|
||||
long pageSize
|
||||
) {
|
||||
LambdaQueryWrapper<DigitalEmployeeBusinessFact> queryWrapper = new LambdaQueryWrapper<DigitalEmployeeBusinessFact>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -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<DigitalEmployeeDailyReportFactMapper, DigitalEmployeeDailyReportFact>
|
||||
implements DigitalEmployeeDailyReportFactRepository {
|
||||
|
||||
@Override
|
||||
public void upsertBySyncRecord(DigitalEmployeeDailyReportFact fact) {
|
||||
if (fact == null || fact.getTenantId() == null || fact.getSyncRecordId() == null) return;
|
||||
DigitalEmployeeDailyReportFact existing = getOne(new LambdaQueryWrapper<DigitalEmployeeDailyReportFact>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -99,6 +99,7 @@ function renderSummary(record: DigitalEmployeeAdminWorkbenchRow) {
|
||||
{title}
|
||||
</Typography.Text>
|
||||
{record.status && <Tag color={tone(record.status)}>{record.status}</Tag>}
|
||||
{record.materialized && <Tag color="green">materialized</Tag>}
|
||||
</Space>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
|
||||
@@ -595,6 +595,7 @@ export interface DigitalEmployeeAdminWorkbenchRow {
|
||||
occurred_at?: string;
|
||||
synced_at?: string;
|
||||
latest_synced_at?: string;
|
||||
materialized?: boolean;
|
||||
client_key_masked?: string;
|
||||
device_id?: string;
|
||||
business_view?: Record<string, unknown>;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 物理业务表。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user