From a9798623791c6992c9de84900b9a517917f67159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Mon, 29 Jun 2026 10:49:55 +0800 Subject: [PATCH] Fix business loop coverage audit matching --- .../front/BusinessLoopCoverageValidator.java | 259 +++++++++++++++++- .../BusinessLoopCoverageValidatorTest.java | 66 ++++- 2 files changed, 312 insertions(+), 13 deletions(-) diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidator.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidator.java index 1c475ec..aa98012 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidator.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidator.java @@ -1,5 +1,6 @@ package com.ruoyi.generator.service.front; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -83,6 +84,9 @@ public class BusinessLoopCoverageValidator finish(result); return result; } + Map> stateTargetsByAction = collectStateTargetsByAction(plan); + Map> quantityTargetsByAction = collectQuantityTargetsByAction(plan); + Map> recordTargetsByAction = collectRecordTargetsByAction(plan); for (BusinessLoopAction planAction : safe(plan.getActions())) { if (planAction == null || !planAction.isRequired()) @@ -95,40 +99,64 @@ public class BusinessLoopCoverageValidator result.getMissingRequiredActions().add(planAction.getCode()); continue; } - validateRequiredEffects(result, planAction, action); + validateRequiredEffects(result, planAction, action, stateTargetsByAction, quantityTargetsByAction, + recordTargetsByAction); } finish(result); return result; } private void validateRequiredEffects(BusinessLoopAuditResult result, BusinessLoopAction planAction, - BusinessActionDesign action) + BusinessActionDesign action, Map> stateTargetsByAction, + Map> quantityTargetsByAction, + Map> recordTargetsByAction) { - if (size(planAction.getStateTransitions()) > 0 && !hasAnyEffect(action, "SET_STATUS", "UPDATE_FIELDS", "UPDATE_FIELD")) + List stateTargets = safe(stateTargetsByAction.get(planAction.getCode())); + List quantityTargets = safe(quantityTargetsByAction.get(planAction.getCode())); + List recordTargets = safe(recordTargetsByAction.get(planAction.getCode())); + if ((size(planAction.getStateTransitions()) > 0 || !stateTargets.isEmpty()) + && !hasMatchingEffect(action, stateTargets, "SET_STATUS", "UPDATE_FIELDS", "UPDATE_FIELD")) { - result.getErrors().add("Missing status effect for required action: " + planAction.getCode()); + result.getErrors().add("Missing status effect for required action " + planAction.getCode() + + formatTargets(stateTargets)); } - if (size(planAction.getQuantityEffects()) > 0 && !hasAnyEffect(action, "INCREASE_NUMBER", "DECREASE_NUMBER")) + if (!quantityTargets.isEmpty() + && !hasMatchingEffect(action, quantityTargets, "INCREASE_NUMBER", "DECREASE_NUMBER")) { - result.getErrors().add("Missing quantity effect for required action: " + planAction.getCode()); + result.getErrors().add("Missing quantity effect for required action " + planAction.getCode() + + formatTargets(quantityTargets)); } - if (size(planAction.getRecordEffects()) > 0 && !hasAnyEffect(action, "INSERT_ROW", "UPDATE_FIELDS")) + if (!recordTargets.isEmpty() && !hasMatchingEffect(action, recordTargets, "INSERT_ROW", "UPDATE_FIELDS")) { - result.getErrors().add("Missing record effect for required action: " + planAction.getCode()); + result.getErrors().add("Missing record effect for required action " + planAction.getCode() + + formatTargets(recordTargets)); } } - private boolean hasAnyEffect(BusinessActionDesign action, String... types) + private boolean hasMatchingEffect(BusinessActionDesign action, List expectedTargets, String... types) { if (action == null || action.getEffects() == null) { return false; } - Set expectedTypes = new HashSet(); - for (String type : types) + if (expectedTargets == null || expectedTargets.isEmpty()) { - expectedTypes.add(type); + return hasAnyEffectType(action, types); } + Set expectedTypes = collectExpectedTypes(types); + for (ExpectedTarget expectedTarget : expectedTargets) + { + if (!hasMatchingEffect(action, expectedTarget, expectedTypes)) + { + return false; + } + } + return true; + } + + private boolean hasAnyEffectType(BusinessActionDesign action, String... types) + { + Set expectedTypes = collectExpectedTypes(types); for (BusinessActionEffectDesign effect : action.getEffects()) { if (effect == null) @@ -144,6 +172,56 @@ public class BusinessLoopCoverageValidator return false; } + private boolean hasMatchingEffect(BusinessActionDesign action, ExpectedTarget expectedTarget, + Set expectedTypes) + { + for (BusinessActionEffectDesign effect : action.getEffects()) + { + if (effect == null) + { + continue; + } + String type = StringUtils.defaultString(effect.getType()).toUpperCase(Locale.ENGLISH); + if (expectedTypes.contains(type) && matchesTarget(effect, expectedTarget)) + { + return true; + } + } + return false; + } + + private boolean matchesTarget(BusinessActionEffectDesign effect, ExpectedTarget expectedTarget) + { + if (expectedTarget == null) + { + return true; + } + if (StringUtils.isNotBlank(expectedTarget.getTableName()) + && !normalize(expectedTarget.getTableName()).equals(normalize(effect.getTargetTable()))) + { + return false; + } + if (StringUtils.isBlank(expectedTarget.getFieldName())) + { + return true; + } + if (normalize(expectedTarget.getFieldName()).equals(normalize(effect.getTargetField()))) + { + return true; + } + return effect.getValues() != null && effect.getValues().containsKey(expectedTarget.getFieldName()); + } + + private Set collectExpectedTypes(String... types) + { + Set expectedTypes = new HashSet(); + for (String type : types) + { + expectedTypes.add(type); + } + return expectedTypes; + } + private Map collectActionsByCode(List actions) { Map actionsByCode = new HashMap(); @@ -161,6 +239,114 @@ public class BusinessLoopCoverageValidator return actionsByCode; } + private Map> collectStateTargetsByAction(BusinessLoopPlan plan) + { + Map> targetsByAction = new HashMap>(); + for (BusinessLoopStateMachine stateMachine : safe(plan.getStateMachines())) + { + if (stateMachine == null) + { + continue; + } + for (BusinessLoopPlan.BusinessLoopStateTransition transition : safe(stateMachine.getTransitions())) + { + if (transition != null && StringUtils.isNotBlank(transition.getActionCode())) + { + addTarget(targetsByAction, transition.getActionCode(), + new ExpectedTarget(stateMachine.getTableName(), stateMachine.getStatusField())); + } + } + } + return targetsByAction; + } + + private Map> collectQuantityTargetsByAction(BusinessLoopPlan plan) + { + Map> targetsByAction = new HashMap>(); + for (BusinessLoopQuantityEffect quantityRule : safe(plan.getQuantityRules())) + { + addQuantityTarget(targetsByAction, quantityRule, null); + } + for (BusinessLoopAction action : safe(plan.getActions())) + { + if (action == null) + { + continue; + } + for (BusinessLoopQuantityEffect quantityEffect : safe(action.getQuantityEffects())) + { + addQuantityTarget(targetsByAction, quantityEffect, action.getCode()); + } + } + return targetsByAction; + } + + private void addQuantityTarget(Map> targetsByAction, + BusinessLoopQuantityEffect quantityEffect, String fallbackActionCode) + { + if (quantityEffect == null) + { + return; + } + String actionCode = StringUtils.isNotBlank(quantityEffect.getActionCode()) + ? quantityEffect.getActionCode() : fallbackActionCode; + if (StringUtils.isBlank(actionCode)) + { + return; + } + addTarget(targetsByAction, actionCode, + new ExpectedTarget(quantityEffect.getTableName(), quantityEffect.getQuantityField())); + } + + private Map> collectRecordTargetsByAction(BusinessLoopPlan plan) + { + Map> targetsByAction = new HashMap>(); + for (BusinessLoopRecordEffect recordRule : safe(plan.getRecordRules())) + { + addRecordTarget(targetsByAction, recordRule, null); + } + for (BusinessLoopAction action : safe(plan.getActions())) + { + if (action == null) + { + continue; + } + for (BusinessLoopRecordEffect recordEffect : safe(action.getRecordEffects())) + { + addRecordTarget(targetsByAction, recordEffect, action.getCode()); + } + } + return targetsByAction; + } + + private void addRecordTarget(Map> targetsByAction, + BusinessLoopRecordEffect recordEffect, String fallbackActionCode) + { + if (recordEffect == null) + { + return; + } + String actionCode = StringUtils.isNotBlank(recordEffect.getActionCode()) + ? recordEffect.getActionCode() : fallbackActionCode; + if (StringUtils.isBlank(actionCode)) + { + return; + } + addTarget(targetsByAction, actionCode, new ExpectedTarget(recordEffect.getTableName(), null)); + } + + private void addTarget(Map> targetsByAction, String actionCode, + ExpectedTarget target) + { + List targets = targetsByAction.get(actionCode); + if (targets == null) + { + targets = new ArrayList(); + targetsByAction.put(actionCode, targets); + } + targets.add(target); + } + private Map> collectColumnsByTable(DatabaseDesignResponse database) { Map> columnsByTable = new HashMap>(); @@ -245,6 +431,24 @@ public class BusinessLoopCoverageValidator return StringUtils.defaultString(value).toLowerCase(Locale.ENGLISH); } + private String formatTargets(List targets) + { + if (targets == null || targets.isEmpty()) + { + return ""; + } + StringBuilder builder = new StringBuilder(": "); + for (int i = 0; i < targets.size(); i++) + { + if (i > 0) + { + builder.append(", "); + } + builder.append(targets.get(i).format()); + } + return builder.toString(); + } + private List safe(List items) { return items == null ? java.util.Collections.emptyList() : items; @@ -254,4 +458,35 @@ public class BusinessLoopCoverageValidator { return items == null ? 0 : items.size(); } + + private static class ExpectedTarget + { + private final String tableName; + private final String fieldName; + + ExpectedTarget(String tableName, String fieldName) + { + this.tableName = tableName; + this.fieldName = fieldName; + } + + String getTableName() + { + return tableName; + } + + String getFieldName() + { + return fieldName; + } + + String format() + { + if (StringUtils.isBlank(fieldName)) + { + return StringUtils.defaultString(tableName); + } + return StringUtils.defaultString(tableName) + "." + fieldName; + } + } } diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidatorTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidatorTest.java index 0fe8311..134b298 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidatorTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidatorTest.java @@ -65,7 +65,10 @@ public class BusinessLoopCoverageValidatorTest { BusinessActionDesign action = new BusinessActionDesign(); action.setCode("confirm_borrow"); - action.setEffects(Arrays.asList(effect("SET_STATUS"), effect("DECREASE_NUMBER"))); + action.setEffects(Arrays.asList( + effect("SET_STATUS", "borrow_order", "borrow_status"), + effect("DECREASE_NUMBER", "book_stock", "available_count"), + effect("INSERT_ROW", "borrow_record", null))); BusinessLoopAuditResult result = validator.auditBusinessActions(completePlan(), Arrays.asList(action)); @@ -74,6 +77,60 @@ public class BusinessLoopCoverageValidatorTest assertTrue(result.getErrors().isEmpty()); } + @Test + public void businessActionAuditFailsWhenTopLevelRecordRuleMissingEffect() + { + BusinessActionDesign action = new BusinessActionDesign(); + action.setCode("confirm_borrow"); + action.setEffects(Arrays.asList( + effect("SET_STATUS", "borrow_order", "borrow_status"), + effect("DECREASE_NUMBER", "book_stock", "available_count"))); + + BusinessLoopAuditResult result = validator.auditBusinessActions(completePlan(), Arrays.asList(action)); + + assertFalse(result.isComplete()); + assertTrue(contains(result.getErrors().toString(), "record effect") + || contains(result.getErrors().toString(), "confirm_borrow")); + } + + @Test + public void businessActionAuditFailsWhenTopLevelQuantityRuleMatchesWrongField() + { + BusinessLoopPlan plan = completePlan(); + plan.getActions().get(0).setQuantityEffects(Collections.emptyList()); + BusinessActionDesign action = new BusinessActionDesign(); + action.setCode("confirm_borrow"); + action.setEffects(Arrays.asList( + effect("SET_STATUS", "borrow_order", "borrow_status"), + effect("DECREASE_NUMBER", "book_stock", "borrowed_count"), + effect("INSERT_ROW", "borrow_record", null))); + + BusinessLoopAuditResult result = validator.auditBusinessActions(plan, Arrays.asList(action)); + + assertFalse(result.isComplete()); + assertTrue(contains(result.getErrors().toString(), "available_count") + || contains(result.getErrors().toString(), "quantity effect")); + } + + @Test + public void businessActionAuditFailsWhenStateStatusEffectMatchesWrongField() + { + BusinessLoopPlan plan = completePlan(); + plan.getActions().get(0).setStateTransitions(Collections.emptyList()); + BusinessActionDesign action = new BusinessActionDesign(); + action.setCode("confirm_borrow"); + action.setEffects(Arrays.asList( + effect("SET_STATUS", "borrow_order", "state"), + effect("DECREASE_NUMBER", "book_stock", "available_count"), + effect("INSERT_ROW", "borrow_record", null))); + + BusinessLoopAuditResult result = validator.auditBusinessActions(plan, Arrays.asList(action)); + + assertFalse(result.isComplete()); + assertTrue(contains(result.getErrors().toString(), "borrow_status") + || contains(result.getErrors().toString(), "status effect")); + } + private BusinessLoopPlan completePlan() { BusinessLoopPlan plan = new BusinessLoopPlan(); @@ -180,9 +237,16 @@ public class BusinessLoopCoverageValidatorTest } private BusinessActionEffectDesign effect(String type) + { + return effect(type, null, null); + } + + private BusinessActionEffectDesign effect(String type, String targetTable, String targetField) { BusinessActionEffectDesign effect = new BusinessActionEffectDesign(); effect.setType(type); + effect.setTargetTable(targetTable); + effect.setTargetField(targetField); return effect; }