吸收数字员工跨设备路由干预冲突预览
This commit is contained in:
@@ -27,6 +27,18 @@ public class DigitalEmployeePolicySnapshotRowDto {
|
||||
private Date latestSyncedAt;
|
||||
@JsonProperty("recommended_resolution")
|
||||
private String recommendedResolution;
|
||||
@JsonProperty("route_decision_count")
|
||||
private Integer routeDecisionCount;
|
||||
@JsonProperty("blocked_count")
|
||||
private Integer blockedCount;
|
||||
@JsonProperty("approval_required_count")
|
||||
private Integer approvalRequiredCount;
|
||||
@JsonProperty("missing_context_count")
|
||||
private Integer missingContextCount;
|
||||
@JsonProperty("not_mapped_count")
|
||||
private Integer notMappedCount;
|
||||
@JsonProperty("intervention_conflict_status")
|
||||
private String interventionConflictStatus;
|
||||
@JsonProperty("sync_record_url")
|
||||
private String syncRecordUrl;
|
||||
@JsonProperty("client_key_masked")
|
||||
|
||||
@@ -546,6 +546,9 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
for (DigitalEmployeeSyncRecord record : page.getRecords()) {
|
||||
Map<String, Object> view = readJsonObject(record.getBusinessView());
|
||||
String kind = firstString(view.get("policy_kind"));
|
||||
if (StringUtils.isBlank(kind) && isRouteInterventionPolicySnapshotView(view)) {
|
||||
kind = "route_decision";
|
||||
}
|
||||
if (StringUtils.isBlank(kind)) continue;
|
||||
if (StringUtils.isNotBlank(policyKind) && !StringUtils.equals(kind, policyKind)) continue;
|
||||
grouped.computeIfAbsent(kind, ignored -> new ArrayList<>()).add(record);
|
||||
@@ -570,22 +573,40 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
|
||||
private DigitalEmployeePolicySnapshotRowDto toPolicySnapshotRow(String policyKind, List<DigitalEmployeeSyncRecord> records) {
|
||||
List<Map<String, Object>> views = records.stream().map(record -> readJsonObject(record.getBusinessView())).toList();
|
||||
List<String> signatures = views.stream().map(this::policySignature).distinct().toList();
|
||||
List<Map<String, Object>> policyViews = views.stream()
|
||||
.filter(view -> StringUtils.isNotBlank(firstString(view.get("policy_kind"))))
|
||||
.toList();
|
||||
List<Map<String, Object>> routeInterventionViews = views.stream()
|
||||
.filter(this::isRouteInterventionPolicySnapshotView)
|
||||
.toList();
|
||||
List<String> signatures = policyViews.stream().map(this::policySignature).distinct().toList();
|
||||
DigitalEmployeeSyncRecord latest = records.stream()
|
||||
.max(Comparator.comparing(DigitalEmployeeSyncRecord::getSyncedAt, Comparator.nullsLast(Comparator.naturalOrder())))
|
||||
.orElse(records.get(0));
|
||||
Map<String, Object> latestView = readJsonObject(latest.getBusinessView());
|
||||
String status = signatures.size() > 1 ? "conflict" : "consistent";
|
||||
String interventionConflictStatus = routeInterventionConflictStatus(routeInterventionViews);
|
||||
boolean routeConflict = StringUtils.equals(interventionConflictStatus, "conflict");
|
||||
String status = signatures.size() > 1 || routeConflict ? "conflict" : "consistent";
|
||||
List<String> conflictKeys = new ArrayList<>("conflict".equals(status) ? policyConflictKeys(policyViews) : List.of());
|
||||
if (routeConflict && !conflictKeys.contains("route_intervention_state")) {
|
||||
conflictKeys.add("route_intervention_state");
|
||||
}
|
||||
return DigitalEmployeePolicySnapshotRowDto.builder()
|
||||
.policyKind(policyKind)
|
||||
.policyKey(firstString(latestView.get("policy_key"), policyKind))
|
||||
.status(status)
|
||||
.summary(firstString(latestView.get("summary"), latestView.get("title"), policyKind))
|
||||
.summary(routeConflict ? "跨设备路由干预冲突" : firstString(latestView.get("summary"), latestView.get("title"), policyKind))
|
||||
.clientCount((int) records.stream().map(DigitalEmployeeSyncRecord::getClientKey).filter(StringUtils::isNotBlank).distinct().count())
|
||||
.deviceCount((int) records.stream().map(DigitalEmployeeSyncRecord::getDeviceId).filter(StringUtils::isNotBlank).distinct().count())
|
||||
.conflictKeys("conflict".equals(status) ? policyConflictKeys(views) : List.of())
|
||||
.conflictKeys(conflictKeys)
|
||||
.latestSyncedAt(latest.getSyncedAt())
|
||||
.recommendedResolution("conflict".equals(status) ? "review_latest_policy_snapshot" : "keep_current")
|
||||
.routeDecisionCount(routeInterventionViews.size())
|
||||
.blockedCount(routeInterventionCount(routeInterventionViews, "blocked"))
|
||||
.approvalRequiredCount(routeInterventionCount(routeInterventionViews, "approval_required"))
|
||||
.missingContextCount(routeInterventionCount(routeInterventionViews, "missing_context"))
|
||||
.notMappedCount(routeInterventionCount(routeInterventionViews, "not_mapped"))
|
||||
.interventionConflictStatus(interventionConflictStatus)
|
||||
.syncRecordUrl(syncRecordUrl(latest.getOutboxId()))
|
||||
.clientKeyMasked(maskClientKey(latest.getClientKey()))
|
||||
.deviceId(latest.getDeviceId())
|
||||
@@ -593,6 +614,47 @@ public class DigitalEmployeeSyncApplicationServiceImpl implements DigitalEmploye
|
||||
.build();
|
||||
}
|
||||
|
||||
private boolean isRouteInterventionPolicySnapshotView(Map<String, Object> view) {
|
||||
String category = firstString(view.get("category"));
|
||||
String kind = firstString(view.get("kind"));
|
||||
return StringUtils.equals(category, "route_intervention")
|
||||
|| StringUtils.equals(category, "route_governance")
|
||||
|| contains(kind, "route_decision_blocked")
|
||||
|| contains(kind, "route_decision_approval_required")
|
||||
|| contains(kind, "route_action_policy_blocked")
|
||||
|| contains(kind, "route_action_missing_context")
|
||||
|| contains(kind, "route_action_not_mapped")
|
||||
|| StringUtils.isNotBlank(firstString(view.get("route_decision_id"), view.get("routeDecisionId")));
|
||||
}
|
||||
|
||||
private String routeInterventionConflictStatus(List<Map<String, Object>> views) {
|
||||
if (views.isEmpty()) return "none";
|
||||
return views.stream().map(this::routeInterventionState).distinct().count() > 1 ? "conflict" : "consistent";
|
||||
}
|
||||
|
||||
private int routeInterventionCount(List<Map<String, Object>> views, String state) {
|
||||
return (int) views.stream().filter(view -> StringUtils.equals(routeInterventionState(view), state)).count();
|
||||
}
|
||||
|
||||
private String routeInterventionState(Map<String, Object> view) {
|
||||
String kind = firstString(view.get("kind"));
|
||||
String policyResult = firstString(view.get("policy_result"), view.get("policyResult"));
|
||||
String status = firstString(view.get("status"));
|
||||
if (contains(kind, "route_decision_approval_required") || contains(policyResult, "approval_required") || contains(status, "approval_required")) {
|
||||
return "approval_required";
|
||||
}
|
||||
if (contains(kind, "route_action_missing_context") || contains(status, "missing_context")) {
|
||||
return "missing_context";
|
||||
}
|
||||
if (contains(kind, "route_action_not_mapped") || contains(status, "not_mapped")) {
|
||||
return "not_mapped";
|
||||
}
|
||||
if (contains(kind, "route_decision_blocked") || contains(kind, "route_action_policy_blocked") || contains(policyResult, "blocked") || contains(status, "blocked")) {
|
||||
return "blocked";
|
||||
}
|
||||
return firstString(policyResult, status, "observed");
|
||||
}
|
||||
|
||||
private String policySignature(Map<String, Object> view) {
|
||||
Map<String, Object> signature = new LinkedHashMap<>();
|
||||
for (String key : policyConflictCandidateKeys()) {
|
||||
|
||||
@@ -1460,6 +1460,86 @@ class DigitalEmployeeSyncApplicationServiceImplTest {
|
||||
assertThat(row.getRecommendedResolution()).isEqualTo("review_latest_policy_snapshot");
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryAdminPolicySnapshotsSummarizesRouteInterventionConflicts() {
|
||||
DigitalEmployeeSyncRecord routeBlocked = record("outbox-route-blocked", "event", "event-route-1", 101L, new Date(10_000),
|
||||
Map.of(
|
||||
"entity_type", "event",
|
||||
"category", "route_intervention",
|
||||
"kind", "route_decision_blocked",
|
||||
"status", "blocked",
|
||||
"title", "入口路由被阻断",
|
||||
"route_decision_id", "route-1",
|
||||
"policy_result", "blocked",
|
||||
"blocked_reason", "sensitive_action"
|
||||
),
|
||||
Map.of("id", "event-route-1", "kind", "route_decision_blocked"));
|
||||
routeBlocked.setClientKey("client-key-a");
|
||||
routeBlocked.setDeviceId("device-a");
|
||||
DigitalEmployeeSyncRecord routeApproval = record("outbox-route-approval", "event", "event-route-2", 101L, new Date(11_000),
|
||||
Map.of(
|
||||
"entity_type", "event",
|
||||
"category", "route_intervention",
|
||||
"kind", "route_decision_approval_required",
|
||||
"status", "approval_required",
|
||||
"title", "入口路由待审批",
|
||||
"route_decision_id", "route-2",
|
||||
"policy_result", "approval_required",
|
||||
"approval_id", "approval-1"
|
||||
),
|
||||
Map.of("id", "event-route-2", "kind", "route_decision_approval_required"));
|
||||
routeApproval.setClientKey("client-key-b");
|
||||
routeApproval.setDeviceId("device-b");
|
||||
DigitalEmployeeSyncRecord routeMissingContext = record("outbox-route-missing-context", "event", "event-route-3", 101L, new Date(12_000),
|
||||
Map.of(
|
||||
"entity_type", "event",
|
||||
"category", "route_governance",
|
||||
"kind", "route_action_missing_context",
|
||||
"status", "missing_context",
|
||||
"title", "路由动作缺少上下文",
|
||||
"route_decision_id", "route-3"
|
||||
),
|
||||
Map.of("id", "event-route-3", "kind", "route_action_missing_context"));
|
||||
routeMissingContext.setClientKey("client-key-c");
|
||||
routeMissingContext.setDeviceId("device-c");
|
||||
|
||||
when(repository.queryBusinessRecords(eq(101L), eq(null), eq(null), eq("event"), eq(null), eq(null), eq(null), eq(null), eq(1L), eq(100L)))
|
||||
.thenReturn(page(List.of(
|
||||
routeBlocked,
|
||||
routeApproval,
|
||||
routeMissingContext,
|
||||
record("outbox-policy", "event", "event-policy-1", 101L, new Date(13_000),
|
||||
Map.of(
|
||||
"entity_type", "event",
|
||||
"category", "policy_snapshot",
|
||||
"kind", "digital_workday_pull_route_decision_policy",
|
||||
"policy_kind", "route_decision",
|
||||
"policy_key", "route_decision",
|
||||
"enabled", true,
|
||||
"auto_create_governance_commands", true,
|
||||
"title", "入口路由策略"
|
||||
),
|
||||
Map.of("kind", "digital_workday_pull_route_decision_policy")),
|
||||
record("outbox-skill", "event", "event-skill-1", 101L, new Date(14_000),
|
||||
Map.of("entity_type", "event", "category", "skill_call_audit", "title", "技能审计", "status", "allowed"),
|
||||
Map.of("id", "event-skill-1", "status", "allowed"))
|
||||
)));
|
||||
|
||||
DigitalEmployeePolicySnapshotPageDto result = service.queryAdminPolicySnapshots(null, null, "conflict", "route_decision", null, null, 1L, 20L);
|
||||
|
||||
assertThat(result.getTotal()).isEqualTo(1);
|
||||
DigitalEmployeePolicySnapshotRowDto row = result.getRecords().get(0);
|
||||
assertThat(row.getPolicyKind()).isEqualTo("route_decision");
|
||||
assertThat(row.getStatus()).isEqualTo("conflict");
|
||||
assertThat(row.getRouteDecisionCount()).isEqualTo(3);
|
||||
assertThat(row.getBlockedCount()).isEqualTo(1);
|
||||
assertThat(row.getApprovalRequiredCount()).isEqualTo(1);
|
||||
assertThat(row.getMissingContextCount()).isEqualTo(1);
|
||||
assertThat(row.getNotMappedCount()).isZero();
|
||||
assertThat(row.getInterventionConflictStatus()).isEqualTo("conflict");
|
||||
assertThat(row.getConflictKeys()).contains("route_intervention_state");
|
||||
}
|
||||
|
||||
@Test
|
||||
void recordDailyReportSendResultStoresRecordedObservation() {
|
||||
DigitalEmployeeAdminRecordRequestDto request = adminRecordRequest("daily_report", "report-1");
|
||||
|
||||
Reference in New Issue
Block a user