# 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`: add `businessLoopPlan`. - Modify `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateBusinessBlueprintRequest.java`: add `businessLoopPlan`. - 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 persisted `businessLoopPlan`. - Modify `ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml`: map `business_loop_plan`. - Modify `sql/db.sql` and `sql/front_project_upgrade.sql`: add `front_project.business_loop_plan`. - Modify `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IAiGenerateService.java`: add `generateBusinessLoopPlan`. - 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. ```java package com.ruoyi.generator.domain.front.dto; import java.util.ArrayList; import java.util.List; public class BusinessLoopPlan { private String domainName; private List coreObjects = new ArrayList(); private List stateMachines = new ArrayList(); private List actions = new ArrayList(); private List quantityRules = new ArrayList(); private List recordRules = new ArrayList(); private List pageBindings = new ArrayList(); private List acceptanceScenarios = new ArrayList(); public String getDomainName() { return domainName; } public void setDomainName(String domainName) { this.domainName = domainName; } public List getCoreObjects() { return coreObjects; } public void setCoreObjects(List coreObjects) { this.coreObjects = coreObjects; } public List getStateMachines() { return stateMachines; } public void setStateMachines(List stateMachines) { this.stateMachines = stateMachines; } public List getActions() { return actions; } public void setActions(List actions) { this.actions = actions; } public List getQuantityRules() { return quantityRules; } public void setQuantityRules(List quantityRules) { this.quantityRules = quantityRules; } public List getRecordRules() { return recordRules; } public void setRecordRules(List recordRules) { this.recordRules = recordRules; } public List getPageBindings() { return pageBindings; } public void setPageBindings(List pageBindings) { this.pageBindings = pageBindings; } public List getAcceptanceScenarios() { return acceptanceScenarios; } public void setAcceptanceScenarios(List 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 states = new ArrayList(); private List transitions = new ArrayList(); 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 getStates() { return states; } public void setStates(List states) { this.states = states; } public List getTransitions() { return transitions; } public void setTransitions(List 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 preconditions = new ArrayList(); private List stateTransitions = new ArrayList(); private List quantityEffects = new ArrayList(); private List recordEffects = new ArrayList(); 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 getPreconditions() { return preconditions; } public void setPreconditions(List preconditions) { this.preconditions = preconditions; } public List getStateTransitions() { return stateTransitions; } public void setStateTransitions(List stateTransitions) { this.stateTransitions = stateTransitions; } public List getQuantityEffects() { return quantityEffects; } public void setQuantityEffects(List quantityEffects) { this.quantityEffects = quantityEffects; } public List getRecordEffects() { return recordEffects; } public void setRecordEffects(List 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 steps = new ArrayList(); private List expectedResults = new ArrayList(); public String getName() { return name; } public void setName(String name) { this.name = name; } public List getSteps() { return steps; } public void setSteps(List steps) { this.steps = steps; } public List getExpectedResults() { return expectedResults; } public void setExpectedResults(List expectedResults) { this.expectedResults = expectedResults; } } } ``` - [ ] **Step 2: Create the audit result DTO** ```java 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 missingRequiredActions = new ArrayList(); private List errors = new ArrayList(); 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 getMissingRequiredActions() { return missingRequiredActions; } public void setMissingRequiredActions(List missingRequiredActions) { this.missingRequiredActions = missingRequiredActions; } public List getErrors() { return errors; } public void setErrors(List errors) { this.errors = errors; } } ``` - [ ] **Step 3: Create the loop-plan request DTO** ```java 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: ```java 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: ```java private boolean businessLoopComplete; private int loopActions; private int stateTransitions; private int quantityRules; private int recordRules; private List missingRequiredActions = new ArrayList(); 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 getMissingRequiredActions() { return missingRequiredActions; } public void setMissingRequiredActions(List missingRequiredActions) { this.missingRequiredActions = missingRequiredActions; } ``` In `OneClickProjectGenerationResult.java`, add: ```java 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: ```java private String businessLoopPlan; public String getBusinessLoopPlan() { return businessLoopPlan; } public void setBusinessLoopPlan(String businessLoopPlan) { this.businessLoopPlan = businessLoopPlan; } ``` In `FrontProjectMapper.xml`, add to `FrontProjectResult`: ```xml ``` Add `business_loop_plan` to the `selectFrontProjectVo` column list immediately after `app_blueprint`. Add insert mappings: ```xml business_loop_plan, ``` and: ```xml #{businessLoopPlan}, ``` Add update mapping: ```xml business_loop_plan = #{businessLoopPlan}, ``` - [ ] **Step 7: Add SQL migrations** In `sql/front_project_upgrade.sql`, append: ```sql 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: ```sql business_loop_plan longtext NULL COMMENT '业务闭环计划', ``` Place it after `app_blueprint`. - [ ] **Step 8: Run focused compile check** Run: ```powershell 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** ```powershell 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`: ```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.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.emptyList()); plan.setRecordRules(Collections.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`: ```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 actions) { Set codes = new HashSet(); 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: ```java 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.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() : 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: ```java 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> 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 actions) { BusinessLoopAuditResult result = baseResult(plan); Map 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> 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> columnsByTable(DatabaseDesignResponse database) { Map> result = new HashMap>(); if (database == null || database.getTables() == null) { return result; } for (DatabaseTableDesign table : database.getTables()) { if (table == null || StringUtils.isBlank(table.getTableName())) { continue; } Set columns = new HashSet(); 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 actionsByCode(List actions) { Map result = new HashMap(); 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 safeActions(BusinessLoopPlan plan) { return plan == null || plan.getActions() == null ? java.util.Collections.emptyList() : plan.getActions(); } private List safeStateMachines(BusinessLoopPlan plan) { return plan == null || plan.getStateMachines() == null ? java.util.Collections.emptyList() : plan.getStateMachines(); } private List safeQuantityRules(BusinessLoopPlan plan) { return plan == null || plan.getQuantityRules() == null ? java.util.Collections.emptyList() : plan.getQuantityRules(); } private List safeRecordRules(BusinessLoopPlan plan) { return plan == null || plan.getRecordRules() == null ? java.util.Collections.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: ```powershell mvn -q -pl ruoyi-generator -Dtest=BusinessLoopPlanValidatorTest,BusinessLoopCoverageValidatorTest test ``` Expected: both test classes pass. - [ ] **Step 6: Commit** ```powershell 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`: ```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 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 projectCaptor = ArgumentCaptor.forClass(FrontProject.class); verify(frontProjectMapper).updateFrontProject(projectCaptor.capture()); assertTrue(projectCaptor.getValue().getBusinessLoopPlan().contains("confirm_borrow")); ArgumentCaptor 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.emptyList()); when(deepSeekClient.chat(anyString())).thenReturn(mockResponse()); GenerateDatabaseRequest request = request(); request.setBusinessLoopPlan(loopPlanJson()); service.generateDatabase(7L, 10L, request); ArgumentCaptor 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 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: ```java import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan; import com.ruoyi.generator.domain.front.dto.GenerateBusinessLoopPlanRequest; ``` - [ ] **Step 2: Extend IAiGenerateService** ```java BusinessLoopPlan generateBusinessLoopPlan(Long userId, Long projectId, GenerateBusinessLoopPlanRequest request); ``` Add imports for `BusinessLoopPlan` and `GenerateBusinessLoopPlanRequest`. - [ ] **Step 3: Add service dependencies** In `AiGenerateServiceImpl`, add: ```java @Autowired private BusinessLoopPlanValidator businessLoopPlanValidator; ``` In `AiGenerateServiceImplTest.setUp`, add: ```java setField("businessLoopPlanValidator", new BusinessLoopPlanValidator()); ``` - [ ] **Step 4: Implement generateBusinessLoopPlan** Add this method to `AiGenerateServiceImpl`: ```java @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`: ```java 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: ```java appendBusinessLoopDatabaseConstraints(prompt, project, request); ``` Add: ```java 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: ```java appendBusinessLoopActionConstraints(prompt, project, request); ``` Add: ```java 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: ```powershell mvn -q -pl ruoyi-generator -Dtest=AiGenerateServiceImplTest test ``` Expected: existing tests and new loop-plan tests pass. - [ ] **Step 8: Commit** ```powershell 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: ```java @Mock private BusinessLoopCoverageValidator businessLoopCoverageValidator; ``` In `setUp`, add: ```java setField("businessLoopCoverageValidator", businessLoopCoverageValidator); ``` Add this test: ```java @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: ```java 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: ```java 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: ```java @Autowired private BusinessLoopCoverageValidator businessLoopCoverageValidator; ``` After app-blueprint generation, add: ```java 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: ```java DatabaseDesignResponse database = aiGenerateService.generateDatabase(task.getUserId(), task.getProjectId(), toDatabaseRequest(request, appBlueprintJson, businessLoopPlanJson)); ``` After table count: ```java 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: ```java List businessActions = aiGenerateService.generateBusinessBlueprint(task.getUserId(), task.getProjectId(), toBusinessBlueprintRequest(request, appBlueprintJson, businessLoopPlanJson)); ``` After business action count: ```java 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: ```java 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: ```java 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: ```powershell mvn -q -pl ruoyi-generator -Dtest=OneClickProjectGenerationServiceImplTest test ``` Expected: one-click tests pass and verify the new business loop stages. - [ ] **Step 6: Commit** ```powershell 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: ```java @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 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: ```java import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan; ``` - [ ] **Step 2: Implement applyBusinessLoopBindings** In `FrontendPageDesignService`, add public method: ```java @Transactional public void applyBusinessLoopBindings(Long userId, Long projectId, BusinessLoopPlan plan, String scope) { frontProjectService.getProject(userId, projectId); String normalizedScope = normalizeScope(scope); List designs = safeDesigns(pageDesignMapper.selectDesignsByProjectIdAndScope(projectId, normalizedScope)); Map designsByPageCode = new LinkedHashMap(); for (FrontProjectPageDesign design : designs) { if (design != null && StringUtils.isNotEmpty(design.getPageCode())) { designsByPageCode.put(design.getPageCode(), design); } } Map 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: ```java private List safeLoopBindings(BusinessLoopPlan plan) { return plan == null || plan.getPageBindings() == null ? new ArrayList() : 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 action = StringUtils.isEmpty(actionJson) ? new LinkedHashMap() : readMap(actionJson, "页面按钮JSON格式错误"); String key = "list.toolbarActions".equals(binding.getSlot()) ? "toolbarActions" : "rowActions"; Object current = action.get(key); List actions = current instanceof List ? new ArrayList((List) current) : new ArrayList(); String actionCode = "business:" + binding.getActionCode(); for (Object item : actions) { if (item instanceof Map && actionCode.equals(stringValue(((Map) item).get("code")))) { return writeJson(action); } if (actionCode.equals(stringValue(item))) { return writeJson(action); } } Map item = new LinkedHashMap(); item.put("code", actionCode); actions.add(item); action.put(key, actions); if (!action.containsKey("toolbarActions")) { action.put("toolbarActions", new ArrayList()); } if (!action.containsKey("rowActions")) { action.put("rowActions", new ArrayList()); } return writeJson(action); } ``` Add import: ```java import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan; ``` - [ ] **Step 3: Wire one-click page binding** In `OneClickProjectGenerationServiceImpl`, after each `initializePageDesigns` call, add: ```java frontendPageDesignService.applyBusinessLoopBindings(task.getUserId(), task.getProjectId(), businessLoopPlan, "frontend"); ``` for frontend, and: ```java frontendPageDesignService.applyBusinessLoopBindings(task.getUserId(), task.getProjectId(), businessLoopPlan, "admin"); ``` for admin. Update `OneClickProjectGenerationServiceImplTest.generateRunsBusinessLoopPlanBeforeDatabaseAndAuditsCoverage` to verify: ```java verify(frontendPageDesignService).applyBusinessLoopBindings(10L, 20L, loopPlan, "frontend"); verify(frontendPageDesignService).applyBusinessLoopBindings(10L, 20L, loopPlan, "admin"); ``` - [ ] **Step 4: Run page binding tests** Run: ```powershell mvn -q -pl ruoyi-generator -Dtest=FrontendPageDesignServiceTest,OneClickProjectGenerationServiceImplTest test ``` Expected: page design and one-click tests pass. - [ ] **Step 5: Commit** ```powershell 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: ```javascript 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: ```vue {{ oneClickResult.report.businessLoopComplete ? '已完成' : '未完成' }} {{ oneClickResult.report.loopActions || 0 }} {{ oneClickResult.report.stateTransitions || 0 }} {{ oneClickResult.report.quantityRules || 0 }} ``` Add style: ```scss .loop-report { margin-top: 14px; } ``` - [ ] **Step 3: Run frontend test** Run: ```powershell cd easycode-web node --test src/views/generateView.test.mjs ``` Expected: `generateView.test.mjs` passes. - [ ] **Step 4: Commit** ```powershell 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** ```powershell mvn -q -pl ruoyi-generator -Dtest=BusinessLoopPlanValidatorTest,BusinessLoopCoverageValidatorTest,AiGenerateServiceImplTest,OneClickProjectGenerationServiceImplTest,FrontendPageDesignServiceTest test ``` Expected: all focused backend tests pass. - [ ] **Step 2: Run generator module tests** ```powershell mvn -q -pl ruoyi-generator test ``` Expected: generator module tests pass. - [ ] **Step 3: Run frontend source-level tests** ```powershell 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** ```powershell cd easycode-web npm run build ``` Expected: Vite build completes successfully. - [ ] **Step 5: Run diff checks** ```powershell 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** 1. Start backend and frontend using the repo's normal local workflow. 2. Log in to EasyCode frontend. 3. Open the generation page. 4. Enter `图书借阅系统` as project name and leave description empty. 5. Click `生成完整项目`. 6. Confirm task progress includes business loop planning and loop audit stages. 7. Confirm success report shows business loop complete. 8. Open generated user frontend and confirm a book list or detail page has a borrow action. 9. Open generated admin frontend and confirm the borrow record page has a confirmation or status action. 10. 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: ```powershell git add -- git commit -m "Stabilize business loop generation" ``` --- ## Acceptance Criteria - A one-click task generates a structured `BusinessLoopPlan` after 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.