Fix business loop coverage audit matching

This commit is contained in:
王鹏
2026-06-29 10:49:55 +08:00
parent 39c363069d
commit a979862379
2 changed files with 312 additions and 13 deletions

View File

@@ -1,5 +1,6 @@
package com.ruoyi.generator.service.front; package com.ruoyi.generator.service.front;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@@ -83,6 +84,9 @@ public class BusinessLoopCoverageValidator
finish(result); finish(result);
return result; return result;
} }
Map<String, List<ExpectedTarget>> stateTargetsByAction = collectStateTargetsByAction(plan);
Map<String, List<ExpectedTarget>> quantityTargetsByAction = collectQuantityTargetsByAction(plan);
Map<String, List<ExpectedTarget>> recordTargetsByAction = collectRecordTargetsByAction(plan);
for (BusinessLoopAction planAction : safe(plan.getActions())) for (BusinessLoopAction planAction : safe(plan.getActions()))
{ {
if (planAction == null || !planAction.isRequired()) if (planAction == null || !planAction.isRequired())
@@ -95,40 +99,64 @@ public class BusinessLoopCoverageValidator
result.getMissingRequiredActions().add(planAction.getCode()); result.getMissingRequiredActions().add(planAction.getCode());
continue; continue;
} }
validateRequiredEffects(result, planAction, action); validateRequiredEffects(result, planAction, action, stateTargetsByAction, quantityTargetsByAction,
recordTargetsByAction);
} }
finish(result); finish(result);
return result; return result;
} }
private void validateRequiredEffects(BusinessLoopAuditResult result, BusinessLoopAction planAction, private void validateRequiredEffects(BusinessLoopAuditResult result, BusinessLoopAction planAction,
BusinessActionDesign action) BusinessActionDesign action, Map<String, List<ExpectedTarget>> stateTargetsByAction,
Map<String, List<ExpectedTarget>> quantityTargetsByAction,
Map<String, List<ExpectedTarget>> recordTargetsByAction)
{ {
if (size(planAction.getStateTransitions()) > 0 && !hasAnyEffect(action, "SET_STATUS", "UPDATE_FIELDS", "UPDATE_FIELD")) List<ExpectedTarget> stateTargets = safe(stateTargetsByAction.get(planAction.getCode()));
List<ExpectedTarget> quantityTargets = safe(quantityTargetsByAction.get(planAction.getCode()));
List<ExpectedTarget> 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<ExpectedTarget> expectedTargets, String... types)
{ {
if (action == null || action.getEffects() == null) if (action == null || action.getEffects() == null)
{ {
return false; return false;
} }
Set<String> expectedTypes = new HashSet<String>(); if (expectedTargets == null || expectedTargets.isEmpty())
for (String type : types)
{ {
expectedTypes.add(type); return hasAnyEffectType(action, types);
} }
Set<String> expectedTypes = collectExpectedTypes(types);
for (ExpectedTarget expectedTarget : expectedTargets)
{
if (!hasMatchingEffect(action, expectedTarget, expectedTypes))
{
return false;
}
}
return true;
}
private boolean hasAnyEffectType(BusinessActionDesign action, String... types)
{
Set<String> expectedTypes = collectExpectedTypes(types);
for (BusinessActionEffectDesign effect : action.getEffects()) for (BusinessActionEffectDesign effect : action.getEffects())
{ {
if (effect == null) if (effect == null)
@@ -144,6 +172,56 @@ public class BusinessLoopCoverageValidator
return false; return false;
} }
private boolean hasMatchingEffect(BusinessActionDesign action, ExpectedTarget expectedTarget,
Set<String> 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<String> collectExpectedTypes(String... types)
{
Set<String> expectedTypes = new HashSet<String>();
for (String type : types)
{
expectedTypes.add(type);
}
return expectedTypes;
}
private Map<String, BusinessActionDesign> collectActionsByCode(List<BusinessActionDesign> actions) private Map<String, BusinessActionDesign> collectActionsByCode(List<BusinessActionDesign> actions)
{ {
Map<String, BusinessActionDesign> actionsByCode = new HashMap<String, BusinessActionDesign>(); Map<String, BusinessActionDesign> actionsByCode = new HashMap<String, BusinessActionDesign>();
@@ -161,6 +239,114 @@ public class BusinessLoopCoverageValidator
return actionsByCode; return actionsByCode;
} }
private Map<String, List<ExpectedTarget>> collectStateTargetsByAction(BusinessLoopPlan plan)
{
Map<String, List<ExpectedTarget>> targetsByAction = new HashMap<String, List<ExpectedTarget>>();
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<String, List<ExpectedTarget>> collectQuantityTargetsByAction(BusinessLoopPlan plan)
{
Map<String, List<ExpectedTarget>> targetsByAction = new HashMap<String, List<ExpectedTarget>>();
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<String, List<ExpectedTarget>> 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<String, List<ExpectedTarget>> collectRecordTargetsByAction(BusinessLoopPlan plan)
{
Map<String, List<ExpectedTarget>> targetsByAction = new HashMap<String, List<ExpectedTarget>>();
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<String, List<ExpectedTarget>> 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<String, List<ExpectedTarget>> targetsByAction, String actionCode,
ExpectedTarget target)
{
List<ExpectedTarget> targets = targetsByAction.get(actionCode);
if (targets == null)
{
targets = new ArrayList<ExpectedTarget>();
targetsByAction.put(actionCode, targets);
}
targets.add(target);
}
private Map<String, Set<String>> collectColumnsByTable(DatabaseDesignResponse database) private Map<String, Set<String>> collectColumnsByTable(DatabaseDesignResponse database)
{ {
Map<String, Set<String>> columnsByTable = new HashMap<String, Set<String>>(); Map<String, Set<String>> columnsByTable = new HashMap<String, Set<String>>();
@@ -245,6 +431,24 @@ public class BusinessLoopCoverageValidator
return StringUtils.defaultString(value).toLowerCase(Locale.ENGLISH); return StringUtils.defaultString(value).toLowerCase(Locale.ENGLISH);
} }
private String formatTargets(List<ExpectedTarget> 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 <T> List<T> safe(List<T> items) private <T> List<T> safe(List<T> items)
{ {
return items == null ? java.util.Collections.<T>emptyList() : items; return items == null ? java.util.Collections.<T>emptyList() : items;
@@ -254,4 +458,35 @@ public class BusinessLoopCoverageValidator
{ {
return items == null ? 0 : items.size(); 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;
}
}
} }

View File

@@ -65,7 +65,10 @@ public class BusinessLoopCoverageValidatorTest
{ {
BusinessActionDesign action = new BusinessActionDesign(); BusinessActionDesign action = new BusinessActionDesign();
action.setCode("confirm_borrow"); 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)); BusinessLoopAuditResult result = validator.auditBusinessActions(completePlan(), Arrays.asList(action));
@@ -74,6 +77,60 @@ public class BusinessLoopCoverageValidatorTest
assertTrue(result.getErrors().isEmpty()); 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.<BusinessLoopQuantityEffect>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.<BusinessLoopStateTransition>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() private BusinessLoopPlan completePlan()
{ {
BusinessLoopPlan plan = new BusinessLoopPlan(); BusinessLoopPlan plan = new BusinessLoopPlan();
@@ -180,9 +237,16 @@ public class BusinessLoopCoverageValidatorTest
} }
private BusinessActionEffectDesign effect(String type) private BusinessActionEffectDesign effect(String type)
{
return effect(type, null, null);
}
private BusinessActionEffectDesign effect(String type, String targetTable, String targetField)
{ {
BusinessActionEffectDesign effect = new BusinessActionEffectDesign(); BusinessActionEffectDesign effect = new BusinessActionEffectDesign();
effect.setType(type); effect.setType(type);
effect.setTargetTable(targetTable);
effect.setTargetField(targetField);
return effect; return effect;
} }