82 KiB
Business Loop Generation Engine Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a generic business-loop generation layer so one-click generation produces runnable workflows with state transitions, quantity changes, record creation, page buttons, and auditable coverage instead of CRUD-only projects.
Architecture: Add a structured BusinessLoopPlan between app-blueprint generation and database generation. The plan is persisted on front_project, passed into database and business-action prompts, audited against generated schema and DSL actions, then used to place business action buttons into page designs.
Tech Stack: Java 8, Spring Boot, MyBatis XML, Fastjson2, Jackson ObjectMapper, JUnit4, Mockito, Vue 3, Element Plus, Node source-level tests.
File Structure
- Create
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessLoopPlan.java: DTO contract for the generated loop plan and its nested objects. - Create
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessLoopAuditResult.java: DTO for database/action/page coverage results. - Create
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateBusinessLoopPlanRequest.java: AI request payload for loop-plan generation. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateDatabaseRequest.java: addbusinessLoopPlan. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateBusinessBlueprintRequest.java: addbusinessLoopPlan. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/OneClickProjectGenerationReport.java: add loop report fields. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/OneClickProjectGenerationResult.java: add loop stage constants. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java: add persistedbusinessLoopPlan. - Modify
ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml: mapbusiness_loop_plan. - Modify
sql/db.sqlandsql/front_project_upgrade.sql: addfront_project.business_loop_plan. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IAiGenerateService.java: addgenerateBusinessLoopPlan. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java: generate, parse, normalize, validate, persist, and propagate loop plans. - Create
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidator.java: validates the plan itself. - Create
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidator.java: audits database and action coverage. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImpl.java: add loop-plan and audit stages. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontendPageDesignService.java: apply plan-derived business button bindings. - Modify
easycode-web/src/views/GenerateView.vue: display business-loop result fields. - Add or modify the focused tests listed in each task.
Task 1: Business Loop DTO and Persistence Contract
Files:
-
Create:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessLoopPlan.java -
Create:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessLoopAuditResult.java -
Create:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateBusinessLoopPlanRequest.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateDatabaseRequest.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateBusinessBlueprintRequest.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/OneClickProjectGenerationReport.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/OneClickProjectGenerationResult.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java -
Modify:
ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml -
Modify:
sql/db.sql -
Modify:
sql/front_project_upgrade.sql -
Step 1: Create the loop-plan DTO
Create BusinessLoopPlan.java with this complete contract. Use nested static classes so JSON serialization keeps the contract together.
package com.ruoyi.generator.domain.front.dto;
import java.util.ArrayList;
import java.util.List;
public class BusinessLoopPlan
{
private String domainName;
private List<BusinessLoopObject> coreObjects = new ArrayList<BusinessLoopObject>();
private List<BusinessLoopStateMachine> stateMachines = new ArrayList<BusinessLoopStateMachine>();
private List<BusinessLoopAction> actions = new ArrayList<BusinessLoopAction>();
private List<BusinessLoopQuantityEffect> quantityRules = new ArrayList<BusinessLoopQuantityEffect>();
private List<BusinessLoopRecordEffect> recordRules = new ArrayList<BusinessLoopRecordEffect>();
private List<BusinessLoopPageBinding> pageBindings = new ArrayList<BusinessLoopPageBinding>();
private List<BusinessLoopAcceptanceScenario> acceptanceScenarios = new ArrayList<BusinessLoopAcceptanceScenario>();
public String getDomainName() { return domainName; }
public void setDomainName(String domainName) { this.domainName = domainName; }
public List<BusinessLoopObject> getCoreObjects() { return coreObjects; }
public void setCoreObjects(List<BusinessLoopObject> coreObjects) { this.coreObjects = coreObjects; }
public List<BusinessLoopStateMachine> getStateMachines() { return stateMachines; }
public void setStateMachines(List<BusinessLoopStateMachine> stateMachines) { this.stateMachines = stateMachines; }
public List<BusinessLoopAction> getActions() { return actions; }
public void setActions(List<BusinessLoopAction> actions) { this.actions = actions; }
public List<BusinessLoopQuantityEffect> getQuantityRules() { return quantityRules; }
public void setQuantityRules(List<BusinessLoopQuantityEffect> quantityRules) { this.quantityRules = quantityRules; }
public List<BusinessLoopRecordEffect> getRecordRules() { return recordRules; }
public void setRecordRules(List<BusinessLoopRecordEffect> recordRules) { this.recordRules = recordRules; }
public List<BusinessLoopPageBinding> getPageBindings() { return pageBindings; }
public void setPageBindings(List<BusinessLoopPageBinding> pageBindings) { this.pageBindings = pageBindings; }
public List<BusinessLoopAcceptanceScenario> getAcceptanceScenarios() { return acceptanceScenarios; }
public void setAcceptanceScenarios(List<BusinessLoopAcceptanceScenario> acceptanceScenarios) { this.acceptanceScenarios = acceptanceScenarios; }
public static class BusinessLoopObject
{
private String code;
private String name;
private String tableName;
private String objectType;
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getTableName() { return tableName; }
public void setTableName(String tableName) { this.tableName = tableName; }
public String getObjectType() { return objectType; }
public void setObjectType(String objectType) { this.objectType = objectType; }
}
public static class BusinessLoopStateMachine
{
private String objectCode;
private String tableName;
private String statusField;
private List<String> states = new ArrayList<String>();
private List<BusinessLoopStateTransition> transitions = new ArrayList<BusinessLoopStateTransition>();
public String getObjectCode() { return objectCode; }
public void setObjectCode(String objectCode) { this.objectCode = objectCode; }
public String getTableName() { return tableName; }
public void setTableName(String tableName) { this.tableName = tableName; }
public String getStatusField() { return statusField; }
public void setStatusField(String statusField) { this.statusField = statusField; }
public List<String> getStates() { return states; }
public void setStates(List<String> states) { this.states = states; }
public List<BusinessLoopStateTransition> getTransitions() { return transitions; }
public void setTransitions(List<BusinessLoopStateTransition> transitions) { this.transitions = transitions; }
}
public static class BusinessLoopStateTransition
{
private String actionCode;
private String fromState;
private String toState;
public String getActionCode() { return actionCode; }
public void setActionCode(String actionCode) { this.actionCode = actionCode; }
public String getFromState() { return fromState; }
public void setFromState(String fromState) { this.fromState = fromState; }
public String getToState() { return toState; }
public void setToState(String toState) { this.toState = toState; }
}
public static class BusinessLoopAction
{
private String code;
private String name;
private String actor;
private String ownerObject;
private String ownerTable;
private String triggerPage;
private boolean required;
private List<String> preconditions = new ArrayList<String>();
private List<BusinessLoopStateTransition> stateTransitions = new ArrayList<BusinessLoopStateTransition>();
private List<BusinessLoopQuantityEffect> quantityEffects = new ArrayList<BusinessLoopQuantityEffect>();
private List<BusinessLoopRecordEffect> recordEffects = new ArrayList<BusinessLoopRecordEffect>();
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getActor() { return actor; }
public void setActor(String actor) { this.actor = actor; }
public String getOwnerObject() { return ownerObject; }
public void setOwnerObject(String ownerObject) { this.ownerObject = ownerObject; }
public String getOwnerTable() { return ownerTable; }
public void setOwnerTable(String ownerTable) { this.ownerTable = ownerTable; }
public String getTriggerPage() { return triggerPage; }
public void setTriggerPage(String triggerPage) { this.triggerPage = triggerPage; }
public boolean isRequired() { return required; }
public void setRequired(boolean required) { this.required = required; }
public List<String> getPreconditions() { return preconditions; }
public void setPreconditions(List<String> preconditions) { this.preconditions = preconditions; }
public List<BusinessLoopStateTransition> getStateTransitions() { return stateTransitions; }
public void setStateTransitions(List<BusinessLoopStateTransition> stateTransitions) { this.stateTransitions = stateTransitions; }
public List<BusinessLoopQuantityEffect> getQuantityEffects() { return quantityEffects; }
public void setQuantityEffects(List<BusinessLoopQuantityEffect> quantityEffects) { this.quantityEffects = quantityEffects; }
public List<BusinessLoopRecordEffect> getRecordEffects() { return recordEffects; }
public void setRecordEffects(List<BusinessLoopRecordEffect> recordEffects) { this.recordEffects = recordEffects; }
}
public static class BusinessLoopQuantityEffect
{
private String actionCode;
private String objectCode;
private String tableName;
private String quantityField;
private String effectType;
private String amount;
public String getActionCode() { return actionCode; }
public void setActionCode(String actionCode) { this.actionCode = actionCode; }
public String getObjectCode() { return objectCode; }
public void setObjectCode(String objectCode) { this.objectCode = objectCode; }
public String getTableName() { return tableName; }
public void setTableName(String tableName) { this.tableName = tableName; }
public String getQuantityField() { return quantityField; }
public void setQuantityField(String quantityField) { this.quantityField = quantityField; }
public String getEffectType() { return effectType; }
public void setEffectType(String effectType) { this.effectType = effectType; }
public String getAmount() { return amount; }
public void setAmount(String amount) { this.amount = amount; }
}
public static class BusinessLoopRecordEffect
{
private String actionCode;
private String objectCode;
private String tableName;
private String operation;
public String getActionCode() { return actionCode; }
public void setActionCode(String actionCode) { this.actionCode = actionCode; }
public String getObjectCode() { return objectCode; }
public void setObjectCode(String objectCode) { this.objectCode = objectCode; }
public String getTableName() { return tableName; }
public void setTableName(String tableName) { this.tableName = tableName; }
public String getOperation() { return operation; }
public void setOperation(String operation) { this.operation = operation; }
}
public static class BusinessLoopPageBinding
{
private String actionCode;
private String target;
private String pageCode;
private String tableName;
private String slot;
public String getActionCode() { return actionCode; }
public void setActionCode(String actionCode) { this.actionCode = actionCode; }
public String getTarget() { return target; }
public void setTarget(String target) { this.target = target; }
public String getPageCode() { return pageCode; }
public void setPageCode(String pageCode) { this.pageCode = pageCode; }
public String getTableName() { return tableName; }
public void setTableName(String tableName) { this.tableName = tableName; }
public String getSlot() { return slot; }
public void setSlot(String slot) { this.slot = slot; }
}
public static class BusinessLoopAcceptanceScenario
{
private String name;
private List<String> steps = new ArrayList<String>();
private List<String> expectedResults = new ArrayList<String>();
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<String> getSteps() { return steps; }
public void setSteps(List<String> steps) { this.steps = steps; }
public List<String> getExpectedResults() { return expectedResults; }
public void setExpectedResults(List<String> expectedResults) { this.expectedResults = expectedResults; }
}
}
- Step 2: Create the audit result DTO
package com.ruoyi.generator.domain.front.dto;
import java.util.ArrayList;
import java.util.List;
public class BusinessLoopAuditResult
{
private boolean complete;
private int loopActions;
private int stateTransitions;
private int quantityRules;
private int recordRules;
private List<String> missingRequiredActions = new ArrayList<String>();
private List<String> errors = new ArrayList<String>();
public boolean isComplete() { return complete; }
public void setComplete(boolean complete) { this.complete = complete; }
public int getLoopActions() { return loopActions; }
public void setLoopActions(int loopActions) { this.loopActions = loopActions; }
public int getStateTransitions() { return stateTransitions; }
public void setStateTransitions(int stateTransitions) { this.stateTransitions = stateTransitions; }
public int getQuantityRules() { return quantityRules; }
public void setQuantityRules(int quantityRules) { this.quantityRules = quantityRules; }
public int getRecordRules() { return recordRules; }
public void setRecordRules(int recordRules) { this.recordRules = recordRules; }
public List<String> getMissingRequiredActions() { return missingRequiredActions; }
public void setMissingRequiredActions(List<String> missingRequiredActions) { this.missingRequiredActions = missingRequiredActions; }
public List<String> getErrors() { return errors; }
public void setErrors(List<String> errors) { this.errors = errors; }
}
- Step 3: Create the loop-plan request DTO
package com.ruoyi.generator.domain.front.dto;
public class GenerateBusinessLoopPlanRequest
{
private String projectName;
private String projectDesc;
private String industryTemplate;
private String extraRequirements;
private String appBlueprint;
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public String getProjectDesc() { return projectDesc; }
public void setProjectDesc(String projectDesc) { this.projectDesc = projectDesc; }
public String getIndustryTemplate() { return industryTemplate; }
public void setIndustryTemplate(String industryTemplate) { this.industryTemplate = industryTemplate; }
public String getExtraRequirements() { return extraRequirements; }
public void setExtraRequirements(String extraRequirements) { this.extraRequirements = extraRequirements; }
public String getAppBlueprint() { return appBlueprint; }
public void setAppBlueprint(String appBlueprint) { this.appBlueprint = appBlueprint; }
}
- Step 4: Extend generation request DTOs
In GenerateDatabaseRequest.java and GenerateBusinessBlueprintRequest.java, add:
private String businessLoopPlan;
public String getBusinessLoopPlan() { return businessLoopPlan; }
public void setBusinessLoopPlan(String businessLoopPlan) { this.businessLoopPlan = businessLoopPlan; }
- Step 5: Extend one-click result contracts
In OneClickProjectGenerationReport.java, add import java.util.ArrayList; and import java.util.List;, then add:
private boolean businessLoopComplete;
private int loopActions;
private int stateTransitions;
private int quantityRules;
private int recordRules;
private List<String> missingRequiredActions = new ArrayList<String>();
public boolean isBusinessLoopComplete() { return businessLoopComplete; }
public void setBusinessLoopComplete(boolean businessLoopComplete) { this.businessLoopComplete = businessLoopComplete; }
public int getLoopActions() { return loopActions; }
public void setLoopActions(int loopActions) { this.loopActions = loopActions; }
public int getStateTransitions() { return stateTransitions; }
public void setStateTransitions(int stateTransitions) { this.stateTransitions = stateTransitions; }
public int getQuantityRules() { return quantityRules; }
public void setQuantityRules(int quantityRules) { this.quantityRules = quantityRules; }
public int getRecordRules() { return recordRules; }
public void setRecordRules(int recordRules) { this.recordRules = recordRules; }
public List<String> getMissingRequiredActions() { return missingRequiredActions; }
public void setMissingRequiredActions(List<String> missingRequiredActions) { this.missingRequiredActions = missingRequiredActions; }
In OneClickProjectGenerationResult.java, add:
public static final String STAGE_BUSINESS_LOOP_PLAN = "BUSINESS_LOOP_PLAN";
public static final String STAGE_DATABASE_LOOP_AUDIT = "DATABASE_LOOP_AUDIT";
public static final String STAGE_BUSINESS_LOOP_AUDIT = "BUSINESS_LOOP_AUDIT";
- Step 6: Persist the loop plan on FrontProject
In FrontProject.java, add:
private String businessLoopPlan;
public String getBusinessLoopPlan() { return businessLoopPlan; }
public void setBusinessLoopPlan(String businessLoopPlan) { this.businessLoopPlan = businessLoopPlan; }
In FrontProjectMapper.xml, add to FrontProjectResult:
<result property="businessLoopPlan" column="business_loop_plan" />
Add business_loop_plan to the selectFrontProjectVo column list immediately after app_blueprint.
Add insert mappings:
<if test="businessLoopPlan != null">business_loop_plan,</if>
and:
<if test="businessLoopPlan != null">#{businessLoopPlan},</if>
Add update mapping:
<if test="businessLoopPlan != null">business_loop_plan = #{businessLoopPlan},</if>
- Step 7: Add SQL migrations
In sql/front_project_upgrade.sql, append:
ALTER TABLE front_project
ADD COLUMN business_loop_plan longtext NULL COMMENT '业务闭环计划' AFTER app_blueprint;
In sql/db.sql, update the front_project DDL to include:
business_loop_plan longtext NULL COMMENT '业务闭环计划',
Place it after app_blueprint.
- Step 8: Run focused compile check
Run:
mvn -q -pl ruoyi-generator -DskipTests compile
Expected: compile succeeds. If it fails, fix only missing imports, duplicate methods, or mapper column spelling in files touched by this task.
- Step 9: Commit
git add -- ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessLoopPlan.java ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessLoopAuditResult.java ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateBusinessLoopPlanRequest.java ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateDatabaseRequest.java ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateBusinessBlueprintRequest.java ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/OneClickProjectGenerationReport.java ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/OneClickProjectGenerationResult.java ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml sql/db.sql sql/front_project_upgrade.sql
git commit -m "Add business loop generation contracts"
Task 2: Plan Validator and Coverage Auditor
Files:
-
Create:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidator.java -
Create:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidator.java -
Test:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidatorTest.java -
Test:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidatorTest.java -
Step 1: Write plan-validator failing tests
Create BusinessLoopPlanValidatorTest.java:
package com.ruoyi.generator.service.front;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Test;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
public class BusinessLoopPlanValidatorTest
{
private final BusinessLoopPlanValidator validator = new BusinessLoopPlanValidator();
@Test
public void validateAcceptsCompleteLoopPlan()
{
validator.validate(plan());
}
@Test
public void validateRejectsPlanWithoutStateTransition()
{
BusinessLoopPlan plan = plan();
plan.getStateMachines().get(0).setTransitions(Collections.<BusinessLoopPlan.BusinessLoopStateTransition>emptyList());
ServiceException exception = expectServiceException(new ThrowingRunnable()
{
@Override
public void run()
{
validator.validate(plan);
}
});
assertTrue(exception.getMessage().contains("state transition"));
}
@Test
public void validateRejectsPlanWithoutQuantityOrRecordRule()
{
BusinessLoopPlan plan = plan();
plan.setQuantityRules(Collections.<BusinessLoopPlan.BusinessLoopQuantityEffect>emptyList());
plan.setRecordRules(Collections.<BusinessLoopPlan.BusinessLoopRecordEffect>emptyList());
ServiceException exception = expectServiceException(new ThrowingRunnable()
{
@Override
public void run()
{
validator.validate(plan);
}
});
assertTrue(exception.getMessage().contains("quantity or record"));
}
private BusinessLoopPlan plan()
{
BusinessLoopPlan plan = new BusinessLoopPlan();
plan.setDomainName("library_borrow");
BusinessLoopPlan.BusinessLoopObject book = new BusinessLoopPlan.BusinessLoopObject();
book.setCode("book");
book.setName("Book");
book.setTableName("book_info");
plan.getCoreObjects().add(book);
BusinessLoopPlan.BusinessLoopStateTransition transition = new BusinessLoopPlan.BusinessLoopStateTransition();
transition.setActionCode("confirm_borrow");
transition.setFromState("pending");
transition.setToState("borrowing");
BusinessLoopPlan.BusinessLoopStateMachine stateMachine = new BusinessLoopPlan.BusinessLoopStateMachine();
stateMachine.setObjectCode("borrow_record");
stateMachine.setTableName("borrow_record");
stateMachine.setStatusField("borrow_status");
stateMachine.getStates().add("pending");
stateMachine.getStates().add("borrowing");
stateMachine.getTransitions().add(transition);
plan.getStateMachines().add(stateMachine);
BusinessLoopPlan.BusinessLoopAction action = new BusinessLoopPlan.BusinessLoopAction();
action.setCode("confirm_borrow");
action.setName("Confirm Borrow");
action.setOwnerObject("borrow_record");
action.setOwnerTable("borrow_record");
action.setRequired(true);
action.getStateTransitions().add(transition);
plan.getActions().add(action);
BusinessLoopPlan.BusinessLoopQuantityEffect quantity = new BusinessLoopPlan.BusinessLoopQuantityEffect();
quantity.setActionCode("confirm_borrow");
quantity.setTableName("book_info");
quantity.setQuantityField("available_count");
quantity.setEffectType("DECREASE_NUMBER");
quantity.setAmount("1");
plan.getQuantityRules().add(quantity);
return plan;
}
private ServiceException expectServiceException(ThrowingRunnable runnable)
{
try
{
runnable.run();
}
catch (ServiceException e)
{
return e;
}
throw new AssertionError("Expected ServiceException");
}
private interface ThrowingRunnable
{
void run();
}
}
- Step 2: Implement BusinessLoopPlanValidator
Create BusinessLoopPlanValidator.java:
package com.ruoyi.generator.service.front;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.springframework.stereotype.Component;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
@Component
public class BusinessLoopPlanValidator
{
private static final int MAX_OBJECTS = 20;
private static final int MAX_ACTIONS = 30;
private static final Pattern CODE = Pattern.compile("^[a-z][a-z0-9_]{1,63}$");
public void validate(BusinessLoopPlan plan)
{
if (plan == null)
{
throw new ServiceException("Business loop plan is required");
}
if (StringUtils.isEmpty(plan.getCoreObjects()))
{
throw new ServiceException("Business loop plan must contain core objects");
}
if (plan.getCoreObjects().size() > MAX_OBJECTS)
{
throw new ServiceException("Business loop object count exceeds limit");
}
if (StringUtils.isEmpty(plan.getActions()))
{
throw new ServiceException("Business loop plan must contain actions");
}
if (plan.getActions().size() > MAX_ACTIONS)
{
throw new ServiceException("Business loop action count exceeds limit");
}
validateActions(plan.getActions());
if (countTransitions(plan) == 0)
{
throw new ServiceException("Business loop plan must contain at least one state transition");
}
if (StringUtils.isEmpty(plan.getQuantityRules()) && StringUtils.isEmpty(plan.getRecordRules()))
{
throw new ServiceException("Business loop plan must contain at least one quantity or record rule");
}
}
private void validateActions(List<BusinessLoopPlan.BusinessLoopAction> actions)
{
Set<String> codes = new HashSet<String>();
boolean hasRequired = false;
for (BusinessLoopPlan.BusinessLoopAction action : actions)
{
if (action == null || StringUtils.isBlank(action.getCode()) || !CODE.matcher(action.getCode()).matches())
{
throw new ServiceException("Business loop action code is invalid");
}
if (!codes.add(action.getCode()))
{
throw new ServiceException("Business loop action code duplicated: " + action.getCode());
}
if (action.isRequired())
{
hasRequired = true;
}
}
if (!hasRequired)
{
throw new ServiceException("Business loop plan must contain at least one required action");
}
}
private int countTransitions(BusinessLoopPlan plan)
{
int count = 0;
if (plan.getStateMachines() != null)
{
for (BusinessLoopPlan.BusinessLoopStateMachine machine : plan.getStateMachines())
{
if (machine != null && machine.getTransitions() != null)
{
count += machine.getTransitions().size();
}
}
}
for (BusinessLoopPlan.BusinessLoopAction action : plan.getActions())
{
if (action != null && action.getStateTransitions() != null)
{
count += action.getStateTransitions().size();
}
}
return count;
}
}
- Step 3: Write coverage-validator failing tests
Create BusinessLoopCoverageValidatorTest.java with tests for database coverage and action coverage:
package com.ruoyi.generator.service.front;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.Collections;
import org.junit.Test;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign;
import com.ruoyi.generator.domain.front.dto.BusinessLoopAuditResult;
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
public class BusinessLoopCoverageValidatorTest
{
private final BusinessLoopCoverageValidator validator = new BusinessLoopCoverageValidator();
@Test
public void auditDatabasePassesWhenTablesAndFieldsCoverPlan()
{
BusinessLoopAuditResult result = validator.auditDatabase(plan(), database(true));
assertTrue(result.isComplete());
assertTrue(result.getErrors().isEmpty());
}
@Test
public void auditDatabaseFailsWhenQuantityFieldIsMissing()
{
BusinessLoopAuditResult result = validator.auditDatabase(plan(), database(false));
assertFalse(result.isComplete());
assertTrue(result.getErrors().get(0).contains("available_count"));
}
@Test
public void auditBusinessActionsFailsWhenRequiredActionIsMissing()
{
BusinessLoopAuditResult result = validator.auditBusinessActions(plan(), Collections.<BusinessActionDesign>emptyList());
assertFalse(result.isComplete());
assertTrue(result.getMissingRequiredActions().contains("confirm_borrow"));
}
@Test
public void auditBusinessActionsPassesWhenRequiredEffectsExist()
{
BusinessLoopAuditResult result = validator.auditBusinessActions(plan(), Collections.singletonList(action()));
assertTrue(result.isComplete());
assertTrue(result.getMissingRequiredActions().isEmpty());
}
private BusinessLoopPlan plan()
{
BusinessLoopPlan plan = new BusinessLoopPlan();
BusinessLoopPlan.BusinessLoopAction action = new BusinessLoopPlan.BusinessLoopAction();
action.setCode("confirm_borrow");
action.setRequired(true);
action.setOwnerTable("borrow_record");
plan.getActions().add(action);
BusinessLoopPlan.BusinessLoopStateMachine stateMachine = new BusinessLoopPlan.BusinessLoopStateMachine();
stateMachine.setTableName("borrow_record");
stateMachine.setStatusField("borrow_status");
BusinessLoopPlan.BusinessLoopStateTransition transition = new BusinessLoopPlan.BusinessLoopStateTransition();
transition.setActionCode("confirm_borrow");
transition.setFromState("pending");
transition.setToState("borrowing");
stateMachine.getTransitions().add(transition);
plan.getStateMachines().add(stateMachine);
BusinessLoopPlan.BusinessLoopQuantityEffect quantity = new BusinessLoopPlan.BusinessLoopQuantityEffect();
quantity.setActionCode("confirm_borrow");
quantity.setTableName("book_info");
quantity.setQuantityField("available_count");
quantity.setEffectType("DECREASE_NUMBER");
plan.getQuantityRules().add(quantity);
return plan;
}
private DatabaseDesignResponse database(boolean includeQuantityField)
{
DatabaseDesignResponse response = new DatabaseDesignResponse();
response.setTables(Arrays.asList(
table("borrow_record", "id", "borrow_status"),
includeQuantityField ? table("book_info", "id", "available_count") : table("book_info", "id")));
return response;
}
private DatabaseTableDesign table(String tableName, String... columns)
{
DatabaseTableDesign table = new DatabaseTableDesign();
table.setTableName(tableName);
for (String columnName : columns)
{
DatabaseColumnDesign column = new DatabaseColumnDesign();
column.setColumnName(columnName);
table.setColumns(table.getColumns() == null ? new java.util.ArrayList<DatabaseColumnDesign>() : table.getColumns());
table.getColumns().add(column);
}
return table;
}
private BusinessActionDesign action()
{
BusinessActionDesign action = new BusinessActionDesign();
action.setCode("confirm_borrow");
BusinessActionEffectDesign status = new BusinessActionEffectDesign();
status.setType("SET_STATUS");
status.setTargetTable("borrow_record");
status.setTargetField("borrow_status");
BusinessActionEffectDesign quantity = new BusinessActionEffectDesign();
quantity.setType("DECREASE_NUMBER");
quantity.setTargetTable("book_info");
quantity.setTargetField("available_count");
action.setEffects(Arrays.asList(status, quantity));
return action;
}
}
- Step 4: Implement BusinessLoopCoverageValidator
Create a validator that returns BusinessLoopAuditResult instead of throwing, so one-click orchestration can put audit errors into task result payload:
package com.ruoyi.generator.service.front;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.stereotype.Component;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign;
import com.ruoyi.generator.domain.front.dto.BusinessLoopAuditResult;
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
@Component
public class BusinessLoopCoverageValidator
{
public BusinessLoopAuditResult auditDatabase(BusinessLoopPlan plan, DatabaseDesignResponse database)
{
BusinessLoopAuditResult result = baseResult(plan);
Map<String, Set<String>> columnsByTable = columnsByTable(database);
for (BusinessLoopPlan.BusinessLoopStateMachine machine : safeStateMachines(plan))
{
requireColumn(result, columnsByTable, machine.getTableName(), machine.getStatusField());
}
for (BusinessLoopPlan.BusinessLoopQuantityEffect quantity : safeQuantityRules(plan))
{
requireColumn(result, columnsByTable, quantity.getTableName(), quantity.getQuantityField());
}
for (BusinessLoopPlan.BusinessLoopRecordEffect record : safeRecordRules(plan))
{
if (StringUtils.isBlank(record.getTableName()) || !columnsByTable.containsKey(record.getTableName()))
{
result.getErrors().add("Missing record table: " + StringUtils.defaultString(record.getTableName()));
}
}
result.setComplete(result.getErrors().isEmpty());
return result;
}
public BusinessLoopAuditResult auditBusinessActions(BusinessLoopPlan plan, List<BusinessActionDesign> actions)
{
BusinessLoopAuditResult result = baseResult(plan);
Map<String, BusinessActionDesign> actionsByCode = actionsByCode(actions);
for (BusinessLoopPlan.BusinessLoopAction required : safeActions(plan))
{
if (!required.isRequired())
{
continue;
}
BusinessActionDesign action = actionsByCode.get(required.getCode());
if (action == null)
{
result.getMissingRequiredActions().add(required.getCode());
continue;
}
if (!hasStatusEffect(action) && !required.getStateTransitions().isEmpty())
{
result.getErrors().add("Missing status effect for action: " + required.getCode());
}
if (!hasQuantityEffect(action) && !required.getQuantityEffects().isEmpty())
{
result.getErrors().add("Missing quantity effect for action: " + required.getCode());
}
if (!hasRecordEffect(action) && !required.getRecordEffects().isEmpty())
{
result.getErrors().add("Missing record effect for action: " + required.getCode());
}
}
result.setComplete(result.getMissingRequiredActions().isEmpty() && result.getErrors().isEmpty());
return result;
}
private BusinessLoopAuditResult baseResult(BusinessLoopPlan plan)
{
BusinessLoopAuditResult result = new BusinessLoopAuditResult();
result.setLoopActions(safeActions(plan).size());
result.setStateTransitions(countTransitions(plan));
result.setQuantityRules(safeQuantityRules(plan).size());
result.setRecordRules(safeRecordRules(plan).size());
return result;
}
private void requireColumn(BusinessLoopAuditResult result, Map<String, Set<String>> columnsByTable, String tableName, String columnName)
{
if (StringUtils.isBlank(tableName) || !columnsByTable.containsKey(tableName))
{
result.getErrors().add("Missing table: " + StringUtils.defaultString(tableName));
return;
}
if (StringUtils.isBlank(columnName) || !columnsByTable.get(tableName).contains(columnName))
{
result.getErrors().add("Missing field " + StringUtils.defaultString(columnName) + " on table " + tableName);
}
}
private Map<String, Set<String>> columnsByTable(DatabaseDesignResponse database)
{
Map<String, Set<String>> result = new HashMap<String, Set<String>>();
if (database == null || database.getTables() == null)
{
return result;
}
for (DatabaseTableDesign table : database.getTables())
{
if (table == null || StringUtils.isBlank(table.getTableName()))
{
continue;
}
Set<String> columns = new HashSet<String>();
if (table.getColumns() != null)
{
for (DatabaseColumnDesign column : table.getColumns())
{
if (column != null && StringUtils.isNotBlank(column.getColumnName()))
{
columns.add(column.getColumnName());
}
}
}
result.put(table.getTableName(), columns);
}
return result;
}
private Map<String, BusinessActionDesign> actionsByCode(List<BusinessActionDesign> actions)
{
Map<String, BusinessActionDesign> result = new HashMap<String, BusinessActionDesign>();
if (actions == null)
{
return result;
}
for (BusinessActionDesign action : actions)
{
if (action != null && StringUtils.isNotBlank(action.getCode()))
{
result.put(action.getCode(), action);
}
}
return result;
}
private boolean hasStatusEffect(BusinessActionDesign action)
{
return hasEffect(action, "SET_STATUS") || hasEffect(action, "UPDATE_FIELDS") || hasEffect(action, "UPDATE_FIELD");
}
private boolean hasQuantityEffect(BusinessActionDesign action)
{
return hasEffect(action, "INCREASE_NUMBER") || hasEffect(action, "DECREASE_NUMBER");
}
private boolean hasRecordEffect(BusinessActionDesign action)
{
return hasEffect(action, "INSERT_ROW") || hasEffect(action, "UPDATE_FIELDS");
}
private boolean hasEffect(BusinessActionDesign action, String type)
{
if (action == null || action.getEffects() == null)
{
return false;
}
for (BusinessActionEffectDesign effect : action.getEffects())
{
if (effect != null && type.equalsIgnoreCase(effect.getType()))
{
return true;
}
}
return false;
}
private List<BusinessLoopPlan.BusinessLoopAction> safeActions(BusinessLoopPlan plan)
{
return plan == null || plan.getActions() == null
? java.util.Collections.<BusinessLoopPlan.BusinessLoopAction>emptyList() : plan.getActions();
}
private List<BusinessLoopPlan.BusinessLoopStateMachine> safeStateMachines(BusinessLoopPlan plan)
{
return plan == null || plan.getStateMachines() == null
? java.util.Collections.<BusinessLoopPlan.BusinessLoopStateMachine>emptyList() : plan.getStateMachines();
}
private List<BusinessLoopPlan.BusinessLoopQuantityEffect> safeQuantityRules(BusinessLoopPlan plan)
{
return plan == null || plan.getQuantityRules() == null
? java.util.Collections.<BusinessLoopPlan.BusinessLoopQuantityEffect>emptyList() : plan.getQuantityRules();
}
private List<BusinessLoopPlan.BusinessLoopRecordEffect> safeRecordRules(BusinessLoopPlan plan)
{
return plan == null || plan.getRecordRules() == null
? java.util.Collections.<BusinessLoopPlan.BusinessLoopRecordEffect>emptyList() : plan.getRecordRules();
}
private int countTransitions(BusinessLoopPlan plan)
{
int count = 0;
for (BusinessLoopPlan.BusinessLoopStateMachine machine : safeStateMachines(plan))
{
if (machine != null && machine.getTransitions() != null)
{
count += machine.getTransitions().size();
}
}
return count;
}
}
- Step 5: Run validator tests
Run:
mvn -q -pl ruoyi-generator -Dtest=BusinessLoopPlanValidatorTest,BusinessLoopCoverageValidatorTest test
Expected: both test classes pass.
- Step 6: Commit
git add -- ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidator.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidator.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidatorTest.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidatorTest.java
git commit -m "Add business loop validation and coverage audit"
Task 3: AI Loop Plan Generation and Prompt Propagation
Files:
-
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IAiGenerateService.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java -
Test:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java -
Step 1: Write failing AI service tests
Add tests to AiGenerateServiceImplTest.java:
@Test
public void generateBusinessLoopPlanPersistsPlanAndGenerationRecord()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
when(deepSeekClient.chat(anyString())).thenReturn(loopPlanResponse());
BusinessLoopPlan plan = service.generateBusinessLoopPlan(7L, 10L, loopPlanRequest());
assertEquals("library_borrow", plan.getDomainName());
assertEquals(1, plan.getCoreObjects().size());
assertEquals("confirm_borrow", plan.getActions().get(0).getCode());
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
verify(deepSeekClient).chat(promptCaptor.capture());
assertTrue(promptCaptor.getValue().contains("Do not generate CRUD"));
assertTrue(promptCaptor.getValue().contains("state transition"));
assertTrue(promptCaptor.getValue().contains("quantity"));
assertTrue(promptCaptor.getValue().contains("record"));
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
assertTrue(projectCaptor.getValue().getBusinessLoopPlan().contains("confirm_borrow"));
ArgumentCaptor<FrontProjectGeneration> generationCaptor = ArgumentCaptor.forClass(FrontProjectGeneration.class);
verify(frontProjectGenerationMapper).insertFrontProjectGeneration(generationCaptor.capture());
assertEquals("business_loop_plan", generationCaptor.getValue().getGenerateType());
assertEquals("1", generationCaptor.getValue().getSuccess());
}
@Test
public void generateDatabasePromptIncludesBusinessLoopPlan()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.<FrontProjectTable>emptyList());
when(deepSeekClient.chat(anyString())).thenReturn(mockResponse());
GenerateDatabaseRequest request = request();
request.setBusinessLoopPlan(loopPlanJson());
service.generateDatabase(7L, 10L, request);
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
verify(deepSeekClient).chat(promptCaptor.capture());
assertTrue(promptCaptor.getValue().contains("Business loop plan JSON"));
assertTrue(promptCaptor.getValue().contains("confirm_borrow"));
assertTrue(promptCaptor.getValue().contains("status field"));
}
@Test
public void generateBusinessBlueprintPromptIncludesBusinessLoopPlan()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
mockSavedLibraryTables();
when(deepSeekClient.chat(anyString())).thenReturn(executableBusinessBlueprintResponse());
GenerateBusinessBlueprintRequest request = businessBlueprintRequest();
request.setBusinessLoopPlan(loopPlanJson());
service.generateBusinessBlueprint(7L, 10L, request);
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
verify(deepSeekClient).chat(promptCaptor.capture());
assertTrue(promptCaptor.getValue().contains("Business loop plan JSON"));
assertTrue(promptCaptor.getValue().contains("Every required action"));
assertTrue(promptCaptor.getValue().contains("confirm_borrow"));
}
private GenerateBusinessLoopPlanRequest loopPlanRequest()
{
GenerateBusinessLoopPlanRequest request = new GenerateBusinessLoopPlanRequest();
request.setProjectName("Library Borrow System");
request.setProjectDesc("");
request.setAppBlueprint(frontendPagesBlueprint());
return request;
}
private String loopPlanJson()
{
return "{\"domainName\":\"library_borrow\",\"coreObjects\":[{\"code\":\"book\",\"name\":\"Book\",\"tableName\":\"book_info\"}],"
+ "\"stateMachines\":[{\"objectCode\":\"borrow_record\",\"tableName\":\"borrow_record\",\"statusField\":\"borrow_status\","
+ "\"states\":[\"pending\",\"borrowing\"],\"transitions\":[{\"actionCode\":\"confirm_borrow\",\"fromState\":\"pending\",\"toState\":\"borrowing\"}]}],"
+ "\"actions\":[{\"code\":\"confirm_borrow\",\"name\":\"Confirm Borrow\",\"ownerObject\":\"borrow_record\",\"ownerTable\":\"borrow_record\","
+ "\"required\":true,\"stateTransitions\":[{\"actionCode\":\"confirm_borrow\",\"fromState\":\"pending\",\"toState\":\"borrowing\"}],"
+ "\"quantityEffects\":[{\"actionCode\":\"confirm_borrow\",\"tableName\":\"book_info\",\"quantityField\":\"available_count\",\"effectType\":\"DECREASE_NUMBER\",\"amount\":\"1\"}]}],"
+ "\"quantityRules\":[{\"actionCode\":\"confirm_borrow\",\"tableName\":\"book_info\",\"quantityField\":\"available_count\",\"effectType\":\"DECREASE_NUMBER\",\"amount\":\"1\"}],"
+ "\"recordRules\":[],\"pageBindings\":[],\"acceptanceScenarios\":[{\"name\":\"Borrow\",\"steps\":[\"confirm\"],\"expectedResults\":[\"stock decreases\"]}]}";
}
private String loopPlanResponse()
{
return loopPlanJson();
}
Add imports in the test:
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
import com.ruoyi.generator.domain.front.dto.GenerateBusinessLoopPlanRequest;
- Step 2: Extend IAiGenerateService
BusinessLoopPlan generateBusinessLoopPlan(Long userId, Long projectId, GenerateBusinessLoopPlanRequest request);
Add imports for BusinessLoopPlan and GenerateBusinessLoopPlanRequest.
- Step 3: Add service dependencies
In AiGenerateServiceImpl, add:
@Autowired
private BusinessLoopPlanValidator businessLoopPlanValidator;
In AiGenerateServiceImplTest.setUp, add:
setField("businessLoopPlanValidator", new BusinessLoopPlanValidator());
- Step 4: Implement generateBusinessLoopPlan
Add this method to AiGenerateServiceImpl:
@Override
@Transactional
public BusinessLoopPlan generateBusinessLoopPlan(Long userId, Long projectId, GenerateBusinessLoopPlanRequest request)
{
assertFrontUserLoggedIn(userId);
FrontProject project = getProject(userId, projectId);
String prompt = buildBusinessLoopPlanPrompt(project, request);
long start = System.currentTimeMillis();
FrontProjectGeneration generation = newGeneration(projectId, userId, prompt, "business_loop_plan");
try
{
String aiContent = deepSeekClient.chat(prompt);
BusinessLoopPlan plan = parseBusinessLoopPlanResponse(aiContent);
businessLoopPlanValidator.validate(plan);
markBusinessLoopPlanGenerated(projectId, plan, request);
generation.setResponsePayload(aiContent);
generation.setSuccess("1");
generation.setElapsedMs(System.currentTimeMillis() - start);
generationRecordService.insert(generation);
return plan;
}
catch (RuntimeException e)
{
generation.setSuccess("0");
generation.setErrorMessage(sanitizeErrorMessage(e.getMessage()));
generation.setElapsedMs(System.currentTimeMillis() - start);
generationRecordService.insert(generation);
throw e;
}
}
- Step 5: Add loop-plan prompt and parsing helpers
Add these helpers to AiGenerateServiceImpl:
private String buildBusinessLoopPlanPrompt(FrontProject project, GenerateBusinessLoopPlanRequest request)
{
StringBuilder prompt = new StringBuilder();
prompt.append("Generate a generic business loop plan for the target generated project. Return JSON only matching the BusinessLoopPlan shape.\n");
prompt.append("Do not generate CRUD. Do not generate code, SQL, database tables, or menu rows.\n");
prompt.append("The plan must contain at least one required end-to-end workflow with a state transition.\n");
prompt.append("The plan must contain quantity change or record creation rules, and should contain both when the domain supports them.\n");
prompt.append("Use action codes in lowercase snake_case. Required actions must be executable later with safe DSL effects: INSERT_ROW, UPDATE_FIELDS, SET_STATUS, INCREASE_NUMBER, DECREASE_NUMBER.\n");
prompt.append("If the project name is clear, infer the loop without asking for more text. Library borrow, order, approval, inventory, appointment, repair, and leave systems must not fall back to CRUD.\n");
prompt.append("Project name: ").append(firstNonBlank(request == null ? null : request.getProjectName(), project.getProjectName())).append("\n");
prompt.append("Project description: ").append(firstNonBlank(request == null ? null : request.getProjectDesc(), project.getProjectDesc())).append("\n");
prompt.append("Industry template: ").append(firstNonBlank(request == null ? null : request.getIndustryTemplate(), project.getIndustryTemplate())).append("\n");
prompt.append("Extra requirements: ").append(request == null ? "" : StringUtils.defaultString(request.getExtraRequirements())).append("\n");
prompt.append("Confirmed app blueprint JSON: ").append(request == null ? "" : StringUtils.defaultString(request.getAppBlueprint())).append("\n");
return prompt.toString();
}
private BusinessLoopPlan parseBusinessLoopPlanResponse(String aiContent)
{
String jsonText = extractJson(aiContent);
BusinessLoopPlan plan = JSON.parseObject(jsonText, BusinessLoopPlan.class);
if (plan == null)
{
throw new ServiceException("AI did not return a business loop plan");
}
return plan;
}
private void markBusinessLoopPlanGenerated(Long projectId, BusinessLoopPlan plan, GenerateBusinessLoopPlanRequest request)
{
FrontProject project = new FrontProject();
project.setProjectId(projectId);
project.setPreviewStatus("0");
project.setBusinessLoopPlan(JSON.toJSONString(plan));
project.setRequirementKeyword(request == null ? null : request.getExtraRequirements());
project.setProjectDesc(request == null ? null : request.getProjectDesc());
project.setIndustryTemplate(request == null ? null : request.getIndustryTemplate());
frontProjectMapper.updateFrontProject(project);
}
- Step 6: Add business loop prompt propagation
In buildPrompt, after appendAppBlueprintConstraints(prompt, project, request);, add:
appendBusinessLoopDatabaseConstraints(prompt, project, request);
Add:
private void appendBusinessLoopDatabaseConstraints(StringBuilder prompt, FrontProject project, GenerateDatabaseRequest request)
{
String businessLoopPlan = firstNonBlank(request == null ? null : request.getBusinessLoopPlan(), project.getBusinessLoopPlan());
if (StringUtils.isBlank(businessLoopPlan))
{
return;
}
prompt.append("Business loop plan JSON: ").append(businessLoopPlan).append("\n");
prompt.append("Database generation constraints from the business loop plan:\n");
prompt.append("- Core objects must map to generated business tables.\n");
prompt.append("- State machines must have the requested status field on the requested table.\n");
prompt.append("- Quantity rules must have numeric fields such as stock, available_count, balance, or quantity.\n");
prompt.append("- Record rules must have record tables and reference fields needed by required actions.\n");
}
In buildBusinessBlueprintPrompt, after appendBusinessAppBlueprintConstraints(prompt, project, request);, add:
appendBusinessLoopActionConstraints(prompt, project, request);
Add:
private void appendBusinessLoopActionConstraints(StringBuilder prompt, FrontProject project, GenerateBusinessBlueprintRequest request)
{
String businessLoopPlan = firstNonBlank(request == null ? null : request.getBusinessLoopPlan(), project.getBusinessLoopPlan());
if (StringUtils.isBlank(businessLoopPlan))
{
return;
}
prompt.append("Business loop plan JSON: ").append(businessLoopPlan).append("\n");
prompt.append("Every required action in the business loop plan must have one matching businessActions entry.\n");
prompt.append("Actions with stateTransitions must emit SET_STATUS or UPDATE_FIELDS effects.\n");
prompt.append("Actions with quantityEffects must emit INCREASE_NUMBER or DECREASE_NUMBER effects.\n");
prompt.append("Actions with recordEffects must emit INSERT_ROW or UPDATE_FIELDS effects against the record table.\n");
}
- Step 7: Run AI service tests
Run:
mvn -q -pl ruoyi-generator -Dtest=AiGenerateServiceImplTest test
Expected: existing tests and new loop-plan tests pass.
- Step 8: Commit
git add -- ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IAiGenerateService.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java
git commit -m "Generate and propagate business loop plans"
Task 4: One-Click Orchestration and Audit Enforcement
Files:
-
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImpl.java -
Test:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImplTest.java -
Step 1: Add failing orchestration tests
In OneClickProjectGenerationServiceImplTest, add a mock field:
@Mock
private BusinessLoopCoverageValidator businessLoopCoverageValidator;
In setUp, add:
setField("businessLoopCoverageValidator", businessLoopCoverageValidator);
Add this test:
@Test
public void generateRunsBusinessLoopPlanBeforeDatabaseAndAuditsCoverage()
{
AiGenerationTask task = oneClickTask();
AiGenerationTaskCreateRequest request = request();
FrontProject project = project();
AppBlueprintDesign appBlueprint = new AppBlueprintDesign();
BusinessLoopPlan loopPlan = loopPlan();
DatabaseDesignResponse database = database();
BusinessActionDesign action = businessAction("confirm_borrow");
BusinessLoopAuditResult databaseAudit = completeAudit();
BusinessLoopAuditResult actionAudit = completeAudit();
when(frontProjectService.getProject(10L, 20L)).thenReturn(project);
when(aiGenerateService.generateAppBlueprint(eq(10L), eq(20L), any(GenerateAppBlueprintRequest.class))).thenReturn(appBlueprint);
when(aiGenerateService.generateBusinessLoopPlan(eq(10L), eq(20L), any(GenerateBusinessLoopPlanRequest.class))).thenReturn(loopPlan);
when(aiGenerateService.generateDatabase(eq(10L), eq(20L), any(GenerateDatabaseRequest.class))).thenReturn(database);
when(businessLoopCoverageValidator.auditDatabase(eq(loopPlan), eq(database))).thenReturn(databaseAudit);
when(aiGenerateService.generateBusinessBlueprint(eq(10L), eq(20L), any(GenerateBusinessBlueprintRequest.class))).thenReturn(Collections.singletonList(action));
when(businessLoopCoverageValidator.auditBusinessActions(eq(loopPlan), any())).thenReturn(actionAudit);
when(frontendPageDesignService.initializePageDesigns(10L, 20L, "frontend")).thenReturn(initResponse());
when(frontendPageDesignService.initializePageDesigns(10L, 20L, "admin")).thenReturn(initResponse());
when(frontProjectRunPreviewService.start(10L, 20L)).thenReturn(runningPreview());
OneClickProjectGenerationResult result = service.generate(task, request);
assertTrue(result.getReport().isBusinessLoopComplete());
assertEquals(1, result.getReport().getLoopActions());
InOrder inOrder = inOrder(aiGenerateService, businessLoopCoverageValidator, frontendPageDesignService);
inOrder.verify(aiGenerateService).generateAppBlueprint(eq(10L), eq(20L), any(GenerateAppBlueprintRequest.class));
inOrder.verify(aiGenerateService).generateBusinessLoopPlan(eq(10L), eq(20L), any(GenerateBusinessLoopPlanRequest.class));
inOrder.verify(aiGenerateService).generateDatabase(eq(10L), eq(20L), any(GenerateDatabaseRequest.class));
inOrder.verify(businessLoopCoverageValidator).auditDatabase(eq(loopPlan), eq(database));
inOrder.verify(aiGenerateService).generateBusinessBlueprint(eq(10L), eq(20L), any(GenerateBusinessBlueprintRequest.class));
inOrder.verify(businessLoopCoverageValidator).auditBusinessActions(eq(loopPlan), any());
inOrder.verify(frontendPageDesignService).initializePageDesigns(10L, 20L, "frontend");
}
@Test
public void generateFailsWhenBusinessLoopAuditIsIncomplete()
{
stubSuccessfulGeneration(project());
BusinessLoopPlan loopPlan = loopPlan();
when(aiGenerateService.generateBusinessLoopPlan(eq(10L), eq(20L), any(GenerateBusinessLoopPlanRequest.class))).thenReturn(loopPlan);
BusinessLoopAuditResult failedAudit = new BusinessLoopAuditResult();
failedAudit.setComplete(false);
failedAudit.getErrors().add("Missing field available_count on table book_info");
when(businessLoopCoverageValidator.auditDatabase(eq(loopPlan), any(DatabaseDesignResponse.class))).thenReturn(failedAudit);
try
{
service.generate(oneClickTask(), request());
fail("Expected OneClickProjectGenerationException");
}
catch (OneClickProjectGenerationException e)
{
assertEquals(OneClickProjectGenerationResult.STAGE_DATABASE_LOOP_AUDIT, e.getResult().getFailedStage());
assertTrue(e.getResult().getErrorMessage().contains("available_count"));
}
}
Add helper methods:
private BusinessLoopPlan loopPlan()
{
BusinessLoopPlan plan = new BusinessLoopPlan();
BusinessLoopPlan.BusinessLoopAction action = new BusinessLoopPlan.BusinessLoopAction();
action.setCode("confirm_borrow");
action.setRequired(true);
action.setOwnerTable("borrow_record");
plan.getActions().add(action);
BusinessLoopPlan.BusinessLoopStateMachine stateMachine = new BusinessLoopPlan.BusinessLoopStateMachine();
stateMachine.setTableName("borrow_record");
stateMachine.setStatusField("borrow_status");
BusinessLoopPlan.BusinessLoopStateTransition transition = new BusinessLoopPlan.BusinessLoopStateTransition();
transition.setActionCode("confirm_borrow");
transition.setFromState("pending");
transition.setToState("borrowing");
stateMachine.getTransitions().add(transition);
plan.getStateMachines().add(stateMachine);
BusinessLoopPlan.BusinessLoopQuantityEffect quantity = new BusinessLoopPlan.BusinessLoopQuantityEffect();
quantity.setActionCode("confirm_borrow");
quantity.setTableName("book_info");
quantity.setQuantityField("available_count");
plan.getQuantityRules().add(quantity);
return plan;
}
private DatabaseDesignResponse database()
{
DatabaseDesignResponse database = new DatabaseDesignResponse();
database.setProjectId(20L);
database.setTables(Collections.singletonList(new DatabaseTableDesign()));
return database;
}
private BusinessActionDesign businessAction(String code)
{
BusinessActionDesign action = new BusinessActionDesign();
action.setCode(code);
return action;
}
private BusinessLoopAuditResult completeAudit()
{
BusinessLoopAuditResult result = new BusinessLoopAuditResult();
result.setComplete(true);
result.setLoopActions(1);
result.setStateTransitions(1);
result.setQuantityRules(1);
result.setRecordRules(0);
return result;
}
Add imports:
import com.ruoyi.generator.domain.front.dto.BusinessLoopAuditResult;
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
import com.ruoyi.generator.domain.front.dto.GenerateBusinessLoopPlanRequest;
- Step 2: Wire dependencies and stage calls
In OneClickProjectGenerationServiceImpl, add:
@Autowired
private BusinessLoopCoverageValidator businessLoopCoverageValidator;
After app-blueprint generation, add:
updateStage(task, result, OneClickProjectGenerationResult.STAGE_BUSINESS_LOOP_PLAN, 25, "正在规划业务闭环");
BusinessLoopPlan businessLoopPlan = aiGenerateService.generateBusinessLoopPlan(task.getUserId(), task.getProjectId(),
toBusinessLoopPlanRequest(request, appBlueprintJson));
String businessLoopPlanJson = JSON.toJSONString(businessLoopPlan);
Change database progress from 35 to 42 and pass the loop plan:
DatabaseDesignResponse database = aiGenerateService.generateDatabase(task.getUserId(), task.getProjectId(),
toDatabaseRequest(request, appBlueprintJson, businessLoopPlanJson));
After table count:
updateStage(task, result, OneClickProjectGenerationResult.STAGE_DATABASE_LOOP_AUDIT, 48, "正在校验数据库是否覆盖业务闭环");
BusinessLoopAuditResult databaseAudit = businessLoopCoverageValidator.auditDatabase(businessLoopPlan, database);
applyLoopAudit(result, databaseAudit);
if (!databaseAudit.isComplete())
{
throw auditFailure(result, OneClickProjectGenerationResult.STAGE_DATABASE_LOOP_AUDIT, databaseAudit);
}
Change business generation call:
List<BusinessActionDesign> businessActions = aiGenerateService.generateBusinessBlueprint(task.getUserId(),
task.getProjectId(), toBusinessBlueprintRequest(request, appBlueprintJson, businessLoopPlanJson));
After business action count:
updateStage(task, result, OneClickProjectGenerationResult.STAGE_BUSINESS_LOOP_AUDIT, 58, "正在校验业务动作是否形成闭环");
BusinessLoopAuditResult actionAudit = businessLoopCoverageValidator.auditBusinessActions(businessLoopPlan, businessActions);
applyLoopAudit(result, actionAudit);
if (!actionAudit.isComplete())
{
throw auditFailure(result, OneClickProjectGenerationResult.STAGE_BUSINESS_LOOP_AUDIT, actionAudit);
}
- Step 3: Add request and audit helpers
Add helpers:
private GenerateBusinessLoopPlanRequest toBusinessLoopPlanRequest(AiGenerationTaskCreateRequest source, String appBlueprintJson)
{
GenerateBusinessLoopPlanRequest request = new GenerateBusinessLoopPlanRequest();
request.setProjectName(source.getProjectName());
request.setProjectDesc(source.getProjectDesc());
request.setIndustryTemplate(source.getIndustryTemplate());
request.setExtraRequirements(source.getExtraRequirements());
request.setAppBlueprint(appBlueprintJson);
return request;
}
private GenerateDatabaseRequest toDatabaseRequest(AiGenerationTaskCreateRequest source, String appBlueprintJson,
String businessLoopPlanJson)
{
GenerateDatabaseRequest request = new GenerateDatabaseRequest();
request.setProjectName(source.getProjectName());
request.setProjectDesc(source.getProjectDesc());
request.setIndustryTemplate(source.getIndustryTemplate());
request.setExtraRequirements(source.getExtraRequirements());
request.setAppBlueprint(appBlueprintJson);
request.setBusinessLoopPlan(businessLoopPlanJson);
return request;
}
private GenerateBusinessBlueprintRequest toBusinessBlueprintRequest(AiGenerationTaskCreateRequest source,
String appBlueprintJson, String businessLoopPlanJson)
{
GenerateBusinessBlueprintRequest request = new GenerateBusinessBlueprintRequest();
request.setProjectName(source.getProjectName());
request.setProjectDesc(source.getProjectDesc());
request.setIndustryTemplate(source.getIndustryTemplate());
request.setExtraRequirements(source.getExtraRequirements());
request.setAppBlueprint(appBlueprintJson);
request.setBusinessLoopPlan(businessLoopPlanJson);
return request;
}
private void applyLoopAudit(OneClickProjectGenerationResult result, BusinessLoopAuditResult audit)
{
if (audit == null || result.getReport() == null)
{
return;
}
result.getReport().setBusinessLoopComplete(audit.isComplete());
result.getReport().setLoopActions(audit.getLoopActions());
result.getReport().setStateTransitions(audit.getStateTransitions());
result.getReport().setQuantityRules(audit.getQuantityRules());
result.getReport().setRecordRules(audit.getRecordRules());
result.getReport().setMissingRequiredActions(audit.getMissingRequiredActions());
}
private OneClickProjectGenerationException auditFailure(OneClickProjectGenerationResult result, String stage,
BusinessLoopAuditResult audit)
{
result.setFailedStage(stage);
String message = audit == null || audit.getErrors() == null || audit.getErrors().isEmpty()
? "业务闭环不完整"
: audit.getErrors().get(0);
result.setErrorMessage(message);
result.getSuggestedActions().add("进入专家模式查看业务闭环计划和缺失项后重新生成");
return new OneClickProjectGenerationException(message, result, null);
}
Remove or replace the old two-argument toDatabaseRequest and toBusinessBlueprintRequest methods so there is no overload ambiguity.
- Step 4: Update successful generation stubs
In stubSuccessfulGeneration, add:
BusinessLoopPlan loopPlan = loopPlan();
when(aiGenerateService.generateBusinessLoopPlan(eq(10L), eq(20L), any(GenerateBusinessLoopPlanRequest.class)))
.thenReturn(loopPlan);
when(businessLoopCoverageValidator.auditDatabase(eq(loopPlan), any(DatabaseDesignResponse.class)))
.thenReturn(completeAudit());
when(businessLoopCoverageValidator.auditBusinessActions(eq(loopPlan), any()))
.thenReturn(completeAudit());
- Step 5: Run one-click tests
Run:
mvn -q -pl ruoyi-generator -Dtest=OneClickProjectGenerationServiceImplTest test
Expected: one-click tests pass and verify the new business loop stages.
- Step 6: Commit
git add -- ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImpl.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImplTest.java
git commit -m "Enforce business loop audit in one-click generation"
Task 5: Page Action Binding From Loop Plan
Files:
-
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontendPageDesignService.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImpl.java -
Test:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontendPageDesignServiceTest.java -
Test:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImplTest.java -
Step 1: Add failing page binding test
In FrontendPageDesignServiceTest, add:
@Test
public void applyBusinessLoopBindingsAddsBusinessRowActions()
{
FrontProjectPageDesign current = currentDesign();
current.setActionJson("{\"toolbarActions\":[],\"rowActions\":[]}");
when(frontProjectService.getProject(7L, 10L)).thenReturn(project());
when(pageDesignMapper.selectDesignsByProjectIdAndScope(10L, "frontend"))
.thenReturn(Collections.singletonList(current));
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(bookTable()));
when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Collections.singletonList(column("book_title", "bookTitle", "1", "1", "1")));
service.applyBusinessLoopBindings(7L, 10L, bindingPlan(), "frontend");
ArgumentCaptor<FrontProjectPageDesign> captor = ArgumentCaptor.forClass(FrontProjectPageDesign.class);
verify(pageDesignMapper).updateFrontProjectPageDesign(captor.capture());
assertTrue(captor.getValue().getActionJson().contains("business:borrow_book"));
}
private BusinessLoopPlan bindingPlan()
{
BusinessLoopPlan plan = new BusinessLoopPlan();
BusinessLoopPlan.BusinessLoopPageBinding binding = new BusinessLoopPlan.BusinessLoopPageBinding();
binding.setActionCode("borrow_book");
binding.setTarget("frontend");
binding.setPageCode("book_catalog_page");
binding.setTableName("book_info");
binding.setSlot("list.rowActions");
plan.getPageBindings().add(binding);
return plan;
}
Add import:
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
- Step 2: Implement applyBusinessLoopBindings
In FrontendPageDesignService, add public method:
@Transactional
public void applyBusinessLoopBindings(Long userId, Long projectId, BusinessLoopPlan plan, String scope)
{
frontProjectService.getProject(userId, projectId);
String normalizedScope = normalizeScope(scope);
List<FrontProjectPageDesign> designs = safeDesigns(pageDesignMapper.selectDesignsByProjectIdAndScope(projectId, normalizedScope));
Map<String, FrontProjectPageDesign> designsByPageCode = new LinkedHashMap<String, FrontProjectPageDesign>();
for (FrontProjectPageDesign design : designs)
{
if (design != null && StringUtils.isNotEmpty(design.getPageCode()))
{
designsByPageCode.put(design.getPageCode(), design);
}
}
Map<String, TableBundle> tablesByName = loadTables(projectId);
for (BusinessLoopPlan.BusinessLoopPageBinding binding : safeLoopBindings(plan))
{
if (binding == null || !bindingTargetMatches(binding, normalizedScope))
{
continue;
}
FrontProjectPageDesign design = designsByPageCode.get(binding.getPageCode());
if (design == null)
{
continue;
}
TableBundle table = StringUtils.isEmpty(design.getTableName()) ? null : tablesByName.get(design.getTableName());
String actionJson = mergeBusinessActionBinding(design.getActionJson(), binding);
design.setActionJson(normalizeAction(actionJson, table, normalizedScope));
design.setVersion(design.getVersion() == null ? 2 : design.getVersion() + 1);
pageDesignMapper.updateFrontProjectPageDesign(design);
}
}
Add helpers:
private List<BusinessLoopPlan.BusinessLoopPageBinding> safeLoopBindings(BusinessLoopPlan plan)
{
return plan == null || plan.getPageBindings() == null
? new ArrayList<BusinessLoopPlan.BusinessLoopPageBinding>() : plan.getPageBindings();
}
private boolean bindingTargetMatches(BusinessLoopPlan.BusinessLoopPageBinding binding, String scope)
{
String target = StringUtils.isEmpty(binding.getTarget()) ? FRONTEND_SCOPE : binding.getTarget();
return normalizeScope(scope).equals(normalizeScope("admin_frontend".equals(target) ? ADMIN_SCOPE : target));
}
@SuppressWarnings("unchecked")
private String mergeBusinessActionBinding(String actionJson, BusinessLoopPlan.BusinessLoopPageBinding binding)
{
Map<String, Object> action = StringUtils.isEmpty(actionJson)
? new LinkedHashMap<String, Object>()
: readMap(actionJson, "页面按钮JSON格式错误");
String key = "list.toolbarActions".equals(binding.getSlot()) ? "toolbarActions" : "rowActions";
Object current = action.get(key);
List<Object> actions = current instanceof List ? new ArrayList<Object>((List<Object>) current) : new ArrayList<Object>();
String actionCode = "business:" + binding.getActionCode();
for (Object item : actions)
{
if (item instanceof Map && actionCode.equals(stringValue(((Map<String, Object>) item).get("code"))))
{
return writeJson(action);
}
if (actionCode.equals(stringValue(item)))
{
return writeJson(action);
}
}
Map<String, Object> item = new LinkedHashMap<String, Object>();
item.put("code", actionCode);
actions.add(item);
action.put(key, actions);
if (!action.containsKey("toolbarActions"))
{
action.put("toolbarActions", new ArrayList<Object>());
}
if (!action.containsKey("rowActions"))
{
action.put("rowActions", new ArrayList<Object>());
}
return writeJson(action);
}
Add import:
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
- Step 3: Wire one-click page binding
In OneClickProjectGenerationServiceImpl, after each initializePageDesigns call, add:
frontendPageDesignService.applyBusinessLoopBindings(task.getUserId(), task.getProjectId(), businessLoopPlan, "frontend");
for frontend, and:
frontendPageDesignService.applyBusinessLoopBindings(task.getUserId(), task.getProjectId(), businessLoopPlan, "admin");
for admin.
Update OneClickProjectGenerationServiceImplTest.generateRunsBusinessLoopPlanBeforeDatabaseAndAuditsCoverage to verify:
verify(frontendPageDesignService).applyBusinessLoopBindings(10L, 20L, loopPlan, "frontend");
verify(frontendPageDesignService).applyBusinessLoopBindings(10L, 20L, loopPlan, "admin");
- Step 4: Run page binding tests
Run:
mvn -q -pl ruoyi-generator -Dtest=FrontendPageDesignServiceTest,OneClickProjectGenerationServiceImplTest test
Expected: page design and one-click tests pass.
- Step 5: Commit
git add -- ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontendPageDesignService.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImpl.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontendPageDesignServiceTest.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImplTest.java
git commit -m "Bind business loop actions to generated pages"
Task 6: Frontend Result Reporting
Files:
-
Modify:
easycode-web/src/views/GenerateView.vue -
Test:
easycode-web/src/views/generateView.test.mjs -
Step 1: Add failing source-level test
In generateView.test.mjs, add:
test('GenerateView shows business loop completion in one-click result', () => {
assert.match(source, /businessLoopComplete/)
assert.match(source, /loopActions/)
assert.match(source, /missingRequiredActions/)
assert.match(source, /业务闭环/)
})
- Step 2: Render loop report fields
In the one-click result card inside GenerateView.vue, after the existing el-descriptions block, add:
<el-descriptions v-if="oneClickResult.report" class="loop-report" :column="2" border>
<el-descriptions-item label="业务闭环">
{{ oneClickResult.report.businessLoopComplete ? '已完成' : '未完成' }}
</el-descriptions-item>
<el-descriptions-item label="闭环动作">
{{ oneClickResult.report.loopActions || 0 }}
</el-descriptions-item>
<el-descriptions-item label="状态流转">
{{ oneClickResult.report.stateTransitions || 0 }}
</el-descriptions-item>
<el-descriptions-item label="数量规则">
{{ oneClickResult.report.quantityRules || 0 }}
</el-descriptions-item>
</el-descriptions>
<el-alert
v-if="oneClickResult.report?.missingRequiredActions?.length"
type="warning"
:title="`缺少业务动作:${oneClickResult.report.missingRequiredActions.join('、')}`"
show-icon
:closable="false"
/>
Add style:
.loop-report {
margin-top: 14px;
}
- Step 3: Run frontend test
Run:
cd easycode-web
node --test src/views/generateView.test.mjs
Expected: generateView.test.mjs passes.
- Step 4: Commit
git add -- easycode-web/src/views/GenerateView.vue easycode-web/src/views/generateView.test.mjs
git commit -m "Show business loop report in one-click UI"
Task 7: Verification and Regression Sweep
Files:
-
All files modified by Tasks 1-6.
-
Step 1: Run focused backend tests
mvn -q -pl ruoyi-generator -Dtest=BusinessLoopPlanValidatorTest,BusinessLoopCoverageValidatorTest,AiGenerateServiceImplTest,OneClickProjectGenerationServiceImplTest,FrontendPageDesignServiceTest test
Expected: all focused backend tests pass.
- Step 2: Run generator module tests
mvn -q -pl ruoyi-generator test
Expected: generator module tests pass.
- Step 3: Run frontend source-level tests
cd easycode-web
node --test src/views/generateView.test.mjs src/views/previewView.test.mjs src/components/frontendPageDesigner.test.mjs
Expected: listed frontend tests pass.
- Step 4: Run frontend build
cd easycode-web
npm run build
Expected: Vite build completes successfully.
- Step 5: Run diff checks
git diff --check
git status --short
Expected: git diff --check prints no errors. git status --short shows only intentional files from this implementation plus pre-existing unrelated workspace changes.
- Step 6: Manual smoke test
- Start backend and frontend using the repo's normal local workflow.
- Log in to EasyCode frontend.
- Open the generation page.
- Enter
图书借阅系统as project name and leave description empty. - Click
生成完整项目. - Confirm task progress includes business loop planning and loop audit stages.
- Confirm success report shows business loop complete.
- Open generated user frontend and confirm a book list or detail page has a borrow action.
- Open generated admin frontend and confirm the borrow record page has a confirmation or status action.
- Download source and confirm generated backend contains business action controller/service methods.
- Step 7: Final commit if verification required fixes
If verification fixes were made after Task 6, commit only those fixes:
git add -- <fixed-files>
git commit -m "Stabilize business loop generation"
Acceptance Criteria
- A one-click task generates a structured
BusinessLoopPlanafter app-blueprint generation. - Database and business-action prompts receive the same loop-plan JSON.
- Database generation is audited for required status fields, quantity fields, and record tables.
- Business-action generation is audited for required actions and required effects.
- A one-click task fails clearly when the loop is incomplete instead of silently succeeding as CRUD.
- Generated page designs receive business action buttons from loop page bindings.
- The one-click UI shows loop completion, action count, transition count, quantity rule count, and missing required actions.
- Focused backend tests, generator module tests, frontend source-level tests, and frontend build pass.