diff --git a/RuoYi-Vue/docs/superpowers/plans/2026-06-28-business-loop-generation-engine.md b/RuoYi-Vue/docs/superpowers/plans/2026-06-28-business-loop-generation-engine.md new file mode 100644 index 0000000..c54f8e0 --- /dev/null +++ b/RuoYi-Vue/docs/superpowers/plans/2026-06-28-business-loop-generation-engine.md @@ -0,0 +1,1980 @@ +# 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.