Files
yidaima/RuoYi-Vue/docs/superpowers/plans/2026-06-28-business-loop-generation-engine.md

82 KiB

Business Loop Generation Engine Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build a generic business-loop generation layer so one-click generation produces runnable workflows with state transitions, quantity changes, record creation, page buttons, and auditable coverage instead of CRUD-only projects.

Architecture: Add a structured BusinessLoopPlan between app-blueprint generation and database generation. The plan is persisted on front_project, passed into database and business-action prompts, audited against generated schema and DSL actions, then used to place business action buttons into page designs.

Tech Stack: Java 8, Spring Boot, MyBatis XML, Fastjson2, Jackson ObjectMapper, JUnit4, Mockito, Vue 3, Element Plus, Node source-level tests.


File Structure

  • Create ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessLoopPlan.java: DTO contract for the generated loop plan and its nested objects.
  • Create ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessLoopAuditResult.java: DTO for database/action/page coverage results.
  • Create ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateBusinessLoopPlanRequest.java: AI request payload for loop-plan generation.
  • Modify ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateDatabaseRequest.java: 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.

package com.ruoyi.generator.domain.front.dto;

import java.util.ArrayList;
import java.util.List;

public class BusinessLoopPlan
{
    private String domainName;
    private List<BusinessLoopObject> coreObjects = new ArrayList<BusinessLoopObject>();
    private List<BusinessLoopStateMachine> stateMachines = new ArrayList<BusinessLoopStateMachine>();
    private List<BusinessLoopAction> actions = new ArrayList<BusinessLoopAction>();
    private List<BusinessLoopQuantityEffect> quantityRules = new ArrayList<BusinessLoopQuantityEffect>();
    private List<BusinessLoopRecordEffect> recordRules = new ArrayList<BusinessLoopRecordEffect>();
    private List<BusinessLoopPageBinding> pageBindings = new ArrayList<BusinessLoopPageBinding>();
    private List<BusinessLoopAcceptanceScenario> acceptanceScenarios = new ArrayList<BusinessLoopAcceptanceScenario>();

    public String getDomainName() { return domainName; }
    public void setDomainName(String domainName) { this.domainName = domainName; }
    public List<BusinessLoopObject> getCoreObjects() { return coreObjects; }
    public void setCoreObjects(List<BusinessLoopObject> coreObjects) { this.coreObjects = coreObjects; }
    public List<BusinessLoopStateMachine> getStateMachines() { return stateMachines; }
    public void setStateMachines(List<BusinessLoopStateMachine> stateMachines) { this.stateMachines = stateMachines; }
    public List<BusinessLoopAction> getActions() { return actions; }
    public void setActions(List<BusinessLoopAction> actions) { this.actions = actions; }
    public List<BusinessLoopQuantityEffect> getQuantityRules() { return quantityRules; }
    public void setQuantityRules(List<BusinessLoopQuantityEffect> quantityRules) { this.quantityRules = quantityRules; }
    public List<BusinessLoopRecordEffect> getRecordRules() { return recordRules; }
    public void setRecordRules(List<BusinessLoopRecordEffect> recordRules) { this.recordRules = recordRules; }
    public List<BusinessLoopPageBinding> getPageBindings() { return pageBindings; }
    public void setPageBindings(List<BusinessLoopPageBinding> pageBindings) { this.pageBindings = pageBindings; }
    public List<BusinessLoopAcceptanceScenario> getAcceptanceScenarios() { return acceptanceScenarios; }
    public void setAcceptanceScenarios(List<BusinessLoopAcceptanceScenario> acceptanceScenarios) { this.acceptanceScenarios = acceptanceScenarios; }

    public static class BusinessLoopObject
    {
        private String code;
        private String name;
        private String tableName;
        private String objectType;

        public String getCode() { return code; }
        public void setCode(String code) { this.code = code; }
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public String getTableName() { return tableName; }
        public void setTableName(String tableName) { this.tableName = tableName; }
        public String getObjectType() { return objectType; }
        public void setObjectType(String objectType) { this.objectType = objectType; }
    }

    public static class BusinessLoopStateMachine
    {
        private String objectCode;
        private String tableName;
        private String statusField;
        private List<String> states = new ArrayList<String>();
        private List<BusinessLoopStateTransition> transitions = new ArrayList<BusinessLoopStateTransition>();

        public String getObjectCode() { return objectCode; }
        public void setObjectCode(String objectCode) { this.objectCode = objectCode; }
        public String getTableName() { return tableName; }
        public void setTableName(String tableName) { this.tableName = tableName; }
        public String getStatusField() { return statusField; }
        public void setStatusField(String statusField) { this.statusField = statusField; }
        public List<String> getStates() { return states; }
        public void setStates(List<String> states) { this.states = states; }
        public List<BusinessLoopStateTransition> getTransitions() { return transitions; }
        public void setTransitions(List<BusinessLoopStateTransition> transitions) { this.transitions = transitions; }
    }

    public static class BusinessLoopStateTransition
    {
        private String actionCode;
        private String fromState;
        private String toState;

        public String getActionCode() { return actionCode; }
        public void setActionCode(String actionCode) { this.actionCode = actionCode; }
        public String getFromState() { return fromState; }
        public void setFromState(String fromState) { this.fromState = fromState; }
        public String getToState() { return toState; }
        public void setToState(String toState) { this.toState = toState; }
    }

    public static class BusinessLoopAction
    {
        private String code;
        private String name;
        private String actor;
        private String ownerObject;
        private String ownerTable;
        private String triggerPage;
        private boolean required;
        private List<String> preconditions = new ArrayList<String>();
        private List<BusinessLoopStateTransition> stateTransitions = new ArrayList<BusinessLoopStateTransition>();
        private List<BusinessLoopQuantityEffect> quantityEffects = new ArrayList<BusinessLoopQuantityEffect>();
        private List<BusinessLoopRecordEffect> recordEffects = new ArrayList<BusinessLoopRecordEffect>();

        public String getCode() { return code; }
        public void setCode(String code) { this.code = code; }
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public String getActor() { return actor; }
        public void setActor(String actor) { this.actor = actor; }
        public String getOwnerObject() { return ownerObject; }
        public void setOwnerObject(String ownerObject) { this.ownerObject = ownerObject; }
        public String getOwnerTable() { return ownerTable; }
        public void setOwnerTable(String ownerTable) { this.ownerTable = ownerTable; }
        public String getTriggerPage() { return triggerPage; }
        public void setTriggerPage(String triggerPage) { this.triggerPage = triggerPage; }
        public boolean isRequired() { return required; }
        public void setRequired(boolean required) { this.required = required; }
        public List<String> getPreconditions() { return preconditions; }
        public void setPreconditions(List<String> preconditions) { this.preconditions = preconditions; }
        public List<BusinessLoopStateTransition> getStateTransitions() { return stateTransitions; }
        public void setStateTransitions(List<BusinessLoopStateTransition> stateTransitions) { this.stateTransitions = stateTransitions; }
        public List<BusinessLoopQuantityEffect> getQuantityEffects() { return quantityEffects; }
        public void setQuantityEffects(List<BusinessLoopQuantityEffect> quantityEffects) { this.quantityEffects = quantityEffects; }
        public List<BusinessLoopRecordEffect> getRecordEffects() { return recordEffects; }
        public void setRecordEffects(List<BusinessLoopRecordEffect> recordEffects) { this.recordEffects = recordEffects; }
    }

    public static class BusinessLoopQuantityEffect
    {
        private String actionCode;
        private String objectCode;
        private String tableName;
        private String quantityField;
        private String effectType;
        private String amount;

        public String getActionCode() { return actionCode; }
        public void setActionCode(String actionCode) { this.actionCode = actionCode; }
        public String getObjectCode() { return objectCode; }
        public void setObjectCode(String objectCode) { this.objectCode = objectCode; }
        public String getTableName() { return tableName; }
        public void setTableName(String tableName) { this.tableName = tableName; }
        public String getQuantityField() { return quantityField; }
        public void setQuantityField(String quantityField) { this.quantityField = quantityField; }
        public String getEffectType() { return effectType; }
        public void setEffectType(String effectType) { this.effectType = effectType; }
        public String getAmount() { return amount; }
        public void setAmount(String amount) { this.amount = amount; }
    }

    public static class BusinessLoopRecordEffect
    {
        private String actionCode;
        private String objectCode;
        private String tableName;
        private String operation;

        public String getActionCode() { return actionCode; }
        public void setActionCode(String actionCode) { this.actionCode = actionCode; }
        public String getObjectCode() { return objectCode; }
        public void setObjectCode(String objectCode) { this.objectCode = objectCode; }
        public String getTableName() { return tableName; }
        public void setTableName(String tableName) { this.tableName = tableName; }
        public String getOperation() { return operation; }
        public void setOperation(String operation) { this.operation = operation; }
    }

    public static class BusinessLoopPageBinding
    {
        private String actionCode;
        private String target;
        private String pageCode;
        private String tableName;
        private String slot;

        public String getActionCode() { return actionCode; }
        public void setActionCode(String actionCode) { this.actionCode = actionCode; }
        public String getTarget() { return target; }
        public void setTarget(String target) { this.target = target; }
        public String getPageCode() { return pageCode; }
        public void setPageCode(String pageCode) { this.pageCode = pageCode; }
        public String getTableName() { return tableName; }
        public void setTableName(String tableName) { this.tableName = tableName; }
        public String getSlot() { return slot; }
        public void setSlot(String slot) { this.slot = slot; }
    }

    public static class BusinessLoopAcceptanceScenario
    {
        private String name;
        private List<String> steps = new ArrayList<String>();
        private List<String> expectedResults = new ArrayList<String>();

        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public List<String> getSteps() { return steps; }
        public void setSteps(List<String> steps) { this.steps = steps; }
        public List<String> getExpectedResults() { return expectedResults; }
        public void setExpectedResults(List<String> expectedResults) { this.expectedResults = expectedResults; }
    }
}
  • Step 2: Create the audit result DTO
package com.ruoyi.generator.domain.front.dto;

import java.util.ArrayList;
import java.util.List;

public class BusinessLoopAuditResult
{
    private boolean complete;
    private int loopActions;
    private int stateTransitions;
    private int quantityRules;
    private int recordRules;
    private List<String> missingRequiredActions = new ArrayList<String>();
    private List<String> errors = new ArrayList<String>();

    public boolean isComplete() { return complete; }
    public void setComplete(boolean complete) { this.complete = complete; }
    public int getLoopActions() { return loopActions; }
    public void setLoopActions(int loopActions) { this.loopActions = loopActions; }
    public int getStateTransitions() { return stateTransitions; }
    public void setStateTransitions(int stateTransitions) { this.stateTransitions = stateTransitions; }
    public int getQuantityRules() { return quantityRules; }
    public void setQuantityRules(int quantityRules) { this.quantityRules = quantityRules; }
    public int getRecordRules() { return recordRules; }
    public void setRecordRules(int recordRules) { this.recordRules = recordRules; }
    public List<String> getMissingRequiredActions() { return missingRequiredActions; }
    public void setMissingRequiredActions(List<String> missingRequiredActions) { this.missingRequiredActions = missingRequiredActions; }
    public List<String> getErrors() { return errors; }
    public void setErrors(List<String> errors) { this.errors = errors; }
}
  • Step 3: Create the loop-plan request DTO
package com.ruoyi.generator.domain.front.dto;

public class GenerateBusinessLoopPlanRequest
{
    private String projectName;
    private String projectDesc;
    private String industryTemplate;
    private String extraRequirements;
    private String appBlueprint;

    public String getProjectName() { return projectName; }
    public void setProjectName(String projectName) { this.projectName = projectName; }
    public String getProjectDesc() { return projectDesc; }
    public void setProjectDesc(String projectDesc) { this.projectDesc = projectDesc; }
    public String getIndustryTemplate() { return industryTemplate; }
    public void setIndustryTemplate(String industryTemplate) { this.industryTemplate = industryTemplate; }
    public String getExtraRequirements() { return extraRequirements; }
    public void setExtraRequirements(String extraRequirements) { this.extraRequirements = extraRequirements; }
    public String getAppBlueprint() { return appBlueprint; }
    public void setAppBlueprint(String appBlueprint) { this.appBlueprint = appBlueprint; }
}
  • Step 4: Extend generation request DTOs

In GenerateDatabaseRequest.java and GenerateBusinessBlueprintRequest.java, add:

private String businessLoopPlan;

public String getBusinessLoopPlan() { return businessLoopPlan; }
public void setBusinessLoopPlan(String businessLoopPlan) { this.businessLoopPlan = businessLoopPlan; }
  • Step 5: Extend one-click result contracts

In OneClickProjectGenerationReport.java, add import java.util.ArrayList; and import java.util.List;, then add:

private boolean businessLoopComplete;
private int loopActions;
private int stateTransitions;
private int quantityRules;
private int recordRules;
private List<String> missingRequiredActions = new ArrayList<String>();

public boolean isBusinessLoopComplete() { return businessLoopComplete; }
public void setBusinessLoopComplete(boolean businessLoopComplete) { this.businessLoopComplete = businessLoopComplete; }
public int getLoopActions() { return loopActions; }
public void setLoopActions(int loopActions) { this.loopActions = loopActions; }
public int getStateTransitions() { return stateTransitions; }
public void setStateTransitions(int stateTransitions) { this.stateTransitions = stateTransitions; }
public int getQuantityRules() { return quantityRules; }
public void setQuantityRules(int quantityRules) { this.quantityRules = quantityRules; }
public int getRecordRules() { return recordRules; }
public void setRecordRules(int recordRules) { this.recordRules = recordRules; }
public List<String> getMissingRequiredActions() { return missingRequiredActions; }
public void setMissingRequiredActions(List<String> missingRequiredActions) { this.missingRequiredActions = missingRequiredActions; }

In OneClickProjectGenerationResult.java, add:

public static final String STAGE_BUSINESS_LOOP_PLAN = "BUSINESS_LOOP_PLAN";
public static final String STAGE_DATABASE_LOOP_AUDIT = "DATABASE_LOOP_AUDIT";
public static final String STAGE_BUSINESS_LOOP_AUDIT = "BUSINESS_LOOP_AUDIT";
  • Step 6: Persist the loop plan on FrontProject

In FrontProject.java, add:

private String businessLoopPlan;

public String getBusinessLoopPlan() { return businessLoopPlan; }
public void setBusinessLoopPlan(String businessLoopPlan) { this.businessLoopPlan = businessLoopPlan; }

In FrontProjectMapper.xml, add to FrontProjectResult:

<result property="businessLoopPlan" column="business_loop_plan" />

Add business_loop_plan to the selectFrontProjectVo column list immediately after app_blueprint.

Add insert mappings:

<if test="businessLoopPlan != null">business_loop_plan,</if>

and:

<if test="businessLoopPlan != null">#{businessLoopPlan},</if>

Add update mapping:

<if test="businessLoopPlan != null">business_loop_plan = #{businessLoopPlan},</if>
  • Step 7: Add SQL migrations

In sql/front_project_upgrade.sql, append:

ALTER TABLE front_project
    ADD COLUMN business_loop_plan longtext NULL COMMENT '业务闭环计划' AFTER app_blueprint;

In sql/db.sql, update the front_project DDL to include:

business_loop_plan longtext NULL COMMENT '业务闭环计划',

Place it after app_blueprint.

  • Step 8: Run focused compile check

Run:

mvn -q -pl ruoyi-generator -DskipTests compile

Expected: compile succeeds. If it fails, fix only missing imports, duplicate methods, or mapper column spelling in files touched by this task.

  • Step 9: Commit
git add -- ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessLoopPlan.java ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessLoopAuditResult.java ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateBusinessLoopPlanRequest.java ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateDatabaseRequest.java ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/GenerateBusinessBlueprintRequest.java ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/OneClickProjectGenerationReport.java ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/OneClickProjectGenerationResult.java ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml sql/db.sql sql/front_project_upgrade.sql
git commit -m "Add business loop generation contracts"

Task 2: Plan Validator and Coverage Auditor

Files:

  • Create: ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidator.java

  • Create: ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidator.java

  • Test: ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidatorTest.java

  • Test: ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidatorTest.java

  • Step 1: Write plan-validator failing tests

Create BusinessLoopPlanValidatorTest.java:

package com.ruoyi.generator.service.front;

import static org.junit.Assert.assertTrue;

import java.util.Arrays;
import java.util.Collections;
import org.junit.Test;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;

public class BusinessLoopPlanValidatorTest
{
    private final BusinessLoopPlanValidator validator = new BusinessLoopPlanValidator();

    @Test
    public void validateAcceptsCompleteLoopPlan()
    {
        validator.validate(plan());
    }

    @Test
    public void validateRejectsPlanWithoutStateTransition()
    {
        BusinessLoopPlan plan = plan();
        plan.getStateMachines().get(0).setTransitions(Collections.<BusinessLoopPlan.BusinessLoopStateTransition>emptyList());

        ServiceException exception = expectServiceException(new ThrowingRunnable()
        {
            @Override
            public void run()
            {
                validator.validate(plan);
            }
        });

        assertTrue(exception.getMessage().contains("state transition"));
    }

    @Test
    public void validateRejectsPlanWithoutQuantityOrRecordRule()
    {
        BusinessLoopPlan plan = plan();
        plan.setQuantityRules(Collections.<BusinessLoopPlan.BusinessLoopQuantityEffect>emptyList());
        plan.setRecordRules(Collections.<BusinessLoopPlan.BusinessLoopRecordEffect>emptyList());

        ServiceException exception = expectServiceException(new ThrowingRunnable()
        {
            @Override
            public void run()
            {
                validator.validate(plan);
            }
        });

        assertTrue(exception.getMessage().contains("quantity or record"));
    }

    private BusinessLoopPlan plan()
    {
        BusinessLoopPlan plan = new BusinessLoopPlan();
        plan.setDomainName("library_borrow");
        BusinessLoopPlan.BusinessLoopObject book = new BusinessLoopPlan.BusinessLoopObject();
        book.setCode("book");
        book.setName("Book");
        book.setTableName("book_info");
        plan.getCoreObjects().add(book);

        BusinessLoopPlan.BusinessLoopStateTransition transition = new BusinessLoopPlan.BusinessLoopStateTransition();
        transition.setActionCode("confirm_borrow");
        transition.setFromState("pending");
        transition.setToState("borrowing");
        BusinessLoopPlan.BusinessLoopStateMachine stateMachine = new BusinessLoopPlan.BusinessLoopStateMachine();
        stateMachine.setObjectCode("borrow_record");
        stateMachine.setTableName("borrow_record");
        stateMachine.setStatusField("borrow_status");
        stateMachine.getStates().add("pending");
        stateMachine.getStates().add("borrowing");
        stateMachine.getTransitions().add(transition);
        plan.getStateMachines().add(stateMachine);

        BusinessLoopPlan.BusinessLoopAction action = new BusinessLoopPlan.BusinessLoopAction();
        action.setCode("confirm_borrow");
        action.setName("Confirm Borrow");
        action.setOwnerObject("borrow_record");
        action.setOwnerTable("borrow_record");
        action.setRequired(true);
        action.getStateTransitions().add(transition);
        plan.getActions().add(action);

        BusinessLoopPlan.BusinessLoopQuantityEffect quantity = new BusinessLoopPlan.BusinessLoopQuantityEffect();
        quantity.setActionCode("confirm_borrow");
        quantity.setTableName("book_info");
        quantity.setQuantityField("available_count");
        quantity.setEffectType("DECREASE_NUMBER");
        quantity.setAmount("1");
        plan.getQuantityRules().add(quantity);
        return plan;
    }

    private ServiceException expectServiceException(ThrowingRunnable runnable)
    {
        try
        {
            runnable.run();
        }
        catch (ServiceException e)
        {
            return e;
        }
        throw new AssertionError("Expected ServiceException");
    }

    private interface ThrowingRunnable
    {
        void run();
    }
}
  • Step 2: Implement BusinessLoopPlanValidator

Create BusinessLoopPlanValidator.java:

package com.ruoyi.generator.service.front;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.springframework.stereotype.Component;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;

@Component
public class BusinessLoopPlanValidator
{
    private static final int MAX_OBJECTS = 20;
    private static final int MAX_ACTIONS = 30;
    private static final Pattern CODE = Pattern.compile("^[a-z][a-z0-9_]{1,63}$");

    public void validate(BusinessLoopPlan plan)
    {
        if (plan == null)
        {
            throw new ServiceException("Business loop plan is required");
        }
        if (StringUtils.isEmpty(plan.getCoreObjects()))
        {
            throw new ServiceException("Business loop plan must contain core objects");
        }
        if (plan.getCoreObjects().size() > MAX_OBJECTS)
        {
            throw new ServiceException("Business loop object count exceeds limit");
        }
        if (StringUtils.isEmpty(plan.getActions()))
        {
            throw new ServiceException("Business loop plan must contain actions");
        }
        if (plan.getActions().size() > MAX_ACTIONS)
        {
            throw new ServiceException("Business loop action count exceeds limit");
        }
        validateActions(plan.getActions());
        if (countTransitions(plan) == 0)
        {
            throw new ServiceException("Business loop plan must contain at least one state transition");
        }
        if (StringUtils.isEmpty(plan.getQuantityRules()) && StringUtils.isEmpty(plan.getRecordRules()))
        {
            throw new ServiceException("Business loop plan must contain at least one quantity or record rule");
        }
    }

    private void validateActions(List<BusinessLoopPlan.BusinessLoopAction> actions)
    {
        Set<String> codes = new HashSet<String>();
        boolean hasRequired = false;
        for (BusinessLoopPlan.BusinessLoopAction action : actions)
        {
            if (action == null || StringUtils.isBlank(action.getCode()) || !CODE.matcher(action.getCode()).matches())
            {
                throw new ServiceException("Business loop action code is invalid");
            }
            if (!codes.add(action.getCode()))
            {
                throw new ServiceException("Business loop action code duplicated: " + action.getCode());
            }
            if (action.isRequired())
            {
                hasRequired = true;
            }
        }
        if (!hasRequired)
        {
            throw new ServiceException("Business loop plan must contain at least one required action");
        }
    }

    private int countTransitions(BusinessLoopPlan plan)
    {
        int count = 0;
        if (plan.getStateMachines() != null)
        {
            for (BusinessLoopPlan.BusinessLoopStateMachine machine : plan.getStateMachines())
            {
                if (machine != null && machine.getTransitions() != null)
                {
                    count += machine.getTransitions().size();
                }
            }
        }
        for (BusinessLoopPlan.BusinessLoopAction action : plan.getActions())
        {
            if (action != null && action.getStateTransitions() != null)
            {
                count += action.getStateTransitions().size();
            }
        }
        return count;
    }
}
  • Step 3: Write coverage-validator failing tests

Create BusinessLoopCoverageValidatorTest.java with tests for database coverage and action coverage:

package com.ruoyi.generator.service.front;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.util.Collections;
import org.junit.Test;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign;
import com.ruoyi.generator.domain.front.dto.BusinessLoopAuditResult;
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;

public class BusinessLoopCoverageValidatorTest
{
    private final BusinessLoopCoverageValidator validator = new BusinessLoopCoverageValidator();

    @Test
    public void auditDatabasePassesWhenTablesAndFieldsCoverPlan()
    {
        BusinessLoopAuditResult result = validator.auditDatabase(plan(), database(true));

        assertTrue(result.isComplete());
        assertTrue(result.getErrors().isEmpty());
    }

    @Test
    public void auditDatabaseFailsWhenQuantityFieldIsMissing()
    {
        BusinessLoopAuditResult result = validator.auditDatabase(plan(), database(false));

        assertFalse(result.isComplete());
        assertTrue(result.getErrors().get(0).contains("available_count"));
    }

    @Test
    public void auditBusinessActionsFailsWhenRequiredActionIsMissing()
    {
        BusinessLoopAuditResult result = validator.auditBusinessActions(plan(), Collections.<BusinessActionDesign>emptyList());

        assertFalse(result.isComplete());
        assertTrue(result.getMissingRequiredActions().contains("confirm_borrow"));
    }

    @Test
    public void auditBusinessActionsPassesWhenRequiredEffectsExist()
    {
        BusinessLoopAuditResult result = validator.auditBusinessActions(plan(), Collections.singletonList(action()));

        assertTrue(result.isComplete());
        assertTrue(result.getMissingRequiredActions().isEmpty());
    }

    private BusinessLoopPlan plan()
    {
        BusinessLoopPlan plan = new BusinessLoopPlan();
        BusinessLoopPlan.BusinessLoopAction action = new BusinessLoopPlan.BusinessLoopAction();
        action.setCode("confirm_borrow");
        action.setRequired(true);
        action.setOwnerTable("borrow_record");
        plan.getActions().add(action);
        BusinessLoopPlan.BusinessLoopStateMachine stateMachine = new BusinessLoopPlan.BusinessLoopStateMachine();
        stateMachine.setTableName("borrow_record");
        stateMachine.setStatusField("borrow_status");
        BusinessLoopPlan.BusinessLoopStateTransition transition = new BusinessLoopPlan.BusinessLoopStateTransition();
        transition.setActionCode("confirm_borrow");
        transition.setFromState("pending");
        transition.setToState("borrowing");
        stateMachine.getTransitions().add(transition);
        plan.getStateMachines().add(stateMachine);
        BusinessLoopPlan.BusinessLoopQuantityEffect quantity = new BusinessLoopPlan.BusinessLoopQuantityEffect();
        quantity.setActionCode("confirm_borrow");
        quantity.setTableName("book_info");
        quantity.setQuantityField("available_count");
        quantity.setEffectType("DECREASE_NUMBER");
        plan.getQuantityRules().add(quantity);
        return plan;
    }

    private DatabaseDesignResponse database(boolean includeQuantityField)
    {
        DatabaseDesignResponse response = new DatabaseDesignResponse();
        response.setTables(Arrays.asList(
                table("borrow_record", "id", "borrow_status"),
                includeQuantityField ? table("book_info", "id", "available_count") : table("book_info", "id")));
        return response;
    }

    private DatabaseTableDesign table(String tableName, String... columns)
    {
        DatabaseTableDesign table = new DatabaseTableDesign();
        table.setTableName(tableName);
        for (String columnName : columns)
        {
            DatabaseColumnDesign column = new DatabaseColumnDesign();
            column.setColumnName(columnName);
            table.setColumns(table.getColumns() == null ? new java.util.ArrayList<DatabaseColumnDesign>() : table.getColumns());
            table.getColumns().add(column);
        }
        return table;
    }

    private BusinessActionDesign action()
    {
        BusinessActionDesign action = new BusinessActionDesign();
        action.setCode("confirm_borrow");
        BusinessActionEffectDesign status = new BusinessActionEffectDesign();
        status.setType("SET_STATUS");
        status.setTargetTable("borrow_record");
        status.setTargetField("borrow_status");
        BusinessActionEffectDesign quantity = new BusinessActionEffectDesign();
        quantity.setType("DECREASE_NUMBER");
        quantity.setTargetTable("book_info");
        quantity.setTargetField("available_count");
        action.setEffects(Arrays.asList(status, quantity));
        return action;
    }
}
  • Step 4: Implement BusinessLoopCoverageValidator

Create a validator that returns BusinessLoopAuditResult instead of throwing, so one-click orchestration can put audit errors into task result payload:

package com.ruoyi.generator.service.front;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.stereotype.Component;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign;
import com.ruoyi.generator.domain.front.dto.BusinessLoopAuditResult;
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;

@Component
public class BusinessLoopCoverageValidator
{
    public BusinessLoopAuditResult auditDatabase(BusinessLoopPlan plan, DatabaseDesignResponse database)
    {
        BusinessLoopAuditResult result = baseResult(plan);
        Map<String, Set<String>> columnsByTable = columnsByTable(database);
        for (BusinessLoopPlan.BusinessLoopStateMachine machine : safeStateMachines(plan))
        {
            requireColumn(result, columnsByTable, machine.getTableName(), machine.getStatusField());
        }
        for (BusinessLoopPlan.BusinessLoopQuantityEffect quantity : safeQuantityRules(plan))
        {
            requireColumn(result, columnsByTable, quantity.getTableName(), quantity.getQuantityField());
        }
        for (BusinessLoopPlan.BusinessLoopRecordEffect record : safeRecordRules(plan))
        {
            if (StringUtils.isBlank(record.getTableName()) || !columnsByTable.containsKey(record.getTableName()))
            {
                result.getErrors().add("Missing record table: " + StringUtils.defaultString(record.getTableName()));
            }
        }
        result.setComplete(result.getErrors().isEmpty());
        return result;
    }

    public BusinessLoopAuditResult auditBusinessActions(BusinessLoopPlan plan, List<BusinessActionDesign> actions)
    {
        BusinessLoopAuditResult result = baseResult(plan);
        Map<String, BusinessActionDesign> actionsByCode = actionsByCode(actions);
        for (BusinessLoopPlan.BusinessLoopAction required : safeActions(plan))
        {
            if (!required.isRequired())
            {
                continue;
            }
            BusinessActionDesign action = actionsByCode.get(required.getCode());
            if (action == null)
            {
                result.getMissingRequiredActions().add(required.getCode());
                continue;
            }
            if (!hasStatusEffect(action) && !required.getStateTransitions().isEmpty())
            {
                result.getErrors().add("Missing status effect for action: " + required.getCode());
            }
            if (!hasQuantityEffect(action) && !required.getQuantityEffects().isEmpty())
            {
                result.getErrors().add("Missing quantity effect for action: " + required.getCode());
            }
            if (!hasRecordEffect(action) && !required.getRecordEffects().isEmpty())
            {
                result.getErrors().add("Missing record effect for action: " + required.getCode());
            }
        }
        result.setComplete(result.getMissingRequiredActions().isEmpty() && result.getErrors().isEmpty());
        return result;
    }

    private BusinessLoopAuditResult baseResult(BusinessLoopPlan plan)
    {
        BusinessLoopAuditResult result = new BusinessLoopAuditResult();
        result.setLoopActions(safeActions(plan).size());
        result.setStateTransitions(countTransitions(plan));
        result.setQuantityRules(safeQuantityRules(plan).size());
        result.setRecordRules(safeRecordRules(plan).size());
        return result;
    }

    private void requireColumn(BusinessLoopAuditResult result, Map<String, Set<String>> columnsByTable, String tableName, String columnName)
    {
        if (StringUtils.isBlank(tableName) || !columnsByTable.containsKey(tableName))
        {
            result.getErrors().add("Missing table: " + StringUtils.defaultString(tableName));
            return;
        }
        if (StringUtils.isBlank(columnName) || !columnsByTable.get(tableName).contains(columnName))
        {
            result.getErrors().add("Missing field " + StringUtils.defaultString(columnName) + " on table " + tableName);
        }
    }

    private Map<String, Set<String>> columnsByTable(DatabaseDesignResponse database)
    {
        Map<String, Set<String>> result = new HashMap<String, Set<String>>();
        if (database == null || database.getTables() == null)
        {
            return result;
        }
        for (DatabaseTableDesign table : database.getTables())
        {
            if (table == null || StringUtils.isBlank(table.getTableName()))
            {
                continue;
            }
            Set<String> columns = new HashSet<String>();
            if (table.getColumns() != null)
            {
                for (DatabaseColumnDesign column : table.getColumns())
                {
                    if (column != null && StringUtils.isNotBlank(column.getColumnName()))
                    {
                        columns.add(column.getColumnName());
                    }
                }
            }
            result.put(table.getTableName(), columns);
        }
        return result;
    }

    private Map<String, BusinessActionDesign> actionsByCode(List<BusinessActionDesign> actions)
    {
        Map<String, BusinessActionDesign> result = new HashMap<String, BusinessActionDesign>();
        if (actions == null)
        {
            return result;
        }
        for (BusinessActionDesign action : actions)
        {
            if (action != null && StringUtils.isNotBlank(action.getCode()))
            {
                result.put(action.getCode(), action);
            }
        }
        return result;
    }

    private boolean hasStatusEffect(BusinessActionDesign action)
    {
        return hasEffect(action, "SET_STATUS") || hasEffect(action, "UPDATE_FIELDS") || hasEffect(action, "UPDATE_FIELD");
    }

    private boolean hasQuantityEffect(BusinessActionDesign action)
    {
        return hasEffect(action, "INCREASE_NUMBER") || hasEffect(action, "DECREASE_NUMBER");
    }

    private boolean hasRecordEffect(BusinessActionDesign action)
    {
        return hasEffect(action, "INSERT_ROW") || hasEffect(action, "UPDATE_FIELDS");
    }

    private boolean hasEffect(BusinessActionDesign action, String type)
    {
        if (action == null || action.getEffects() == null)
        {
            return false;
        }
        for (BusinessActionEffectDesign effect : action.getEffects())
        {
            if (effect != null && type.equalsIgnoreCase(effect.getType()))
            {
                return true;
            }
        }
        return false;
    }

    private List<BusinessLoopPlan.BusinessLoopAction> safeActions(BusinessLoopPlan plan)
    {
        return plan == null || plan.getActions() == null
                ? java.util.Collections.<BusinessLoopPlan.BusinessLoopAction>emptyList() : plan.getActions();
    }

    private List<BusinessLoopPlan.BusinessLoopStateMachine> safeStateMachines(BusinessLoopPlan plan)
    {
        return plan == null || plan.getStateMachines() == null
                ? java.util.Collections.<BusinessLoopPlan.BusinessLoopStateMachine>emptyList() : plan.getStateMachines();
    }

    private List<BusinessLoopPlan.BusinessLoopQuantityEffect> safeQuantityRules(BusinessLoopPlan plan)
    {
        return plan == null || plan.getQuantityRules() == null
                ? java.util.Collections.<BusinessLoopPlan.BusinessLoopQuantityEffect>emptyList() : plan.getQuantityRules();
    }

    private List<BusinessLoopPlan.BusinessLoopRecordEffect> safeRecordRules(BusinessLoopPlan plan)
    {
        return plan == null || plan.getRecordRules() == null
                ? java.util.Collections.<BusinessLoopPlan.BusinessLoopRecordEffect>emptyList() : plan.getRecordRules();
    }

    private int countTransitions(BusinessLoopPlan plan)
    {
        int count = 0;
        for (BusinessLoopPlan.BusinessLoopStateMachine machine : safeStateMachines(plan))
        {
            if (machine != null && machine.getTransitions() != null)
            {
                count += machine.getTransitions().size();
            }
        }
        return count;
    }
}
  • Step 5: Run validator tests

Run:

mvn -q -pl ruoyi-generator -Dtest=BusinessLoopPlanValidatorTest,BusinessLoopCoverageValidatorTest test

Expected: both test classes pass.

  • Step 6: Commit
git add -- ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidator.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidator.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopPlanValidatorTest.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopCoverageValidatorTest.java
git commit -m "Add business loop validation and coverage audit"

Task 3: AI Loop Plan Generation and Prompt Propagation

Files:

  • Modify: ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IAiGenerateService.java

  • Modify: ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java

  • Test: ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java

  • Step 1: Write failing AI service tests

Add tests to AiGenerateServiceImplTest.java:

@Test
public void generateBusinessLoopPlanPersistsPlanAndGenerationRecord()
{
    when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
    when(deepSeekClient.chat(anyString())).thenReturn(loopPlanResponse());

    BusinessLoopPlan plan = service.generateBusinessLoopPlan(7L, 10L, loopPlanRequest());

    assertEquals("library_borrow", plan.getDomainName());
    assertEquals(1, plan.getCoreObjects().size());
    assertEquals("confirm_borrow", plan.getActions().get(0).getCode());

    ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
    verify(deepSeekClient).chat(promptCaptor.capture());
    assertTrue(promptCaptor.getValue().contains("Do not generate CRUD"));
    assertTrue(promptCaptor.getValue().contains("state transition"));
    assertTrue(promptCaptor.getValue().contains("quantity"));
    assertTrue(promptCaptor.getValue().contains("record"));

    ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
    verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
    assertTrue(projectCaptor.getValue().getBusinessLoopPlan().contains("confirm_borrow"));

    ArgumentCaptor<FrontProjectGeneration> generationCaptor = ArgumentCaptor.forClass(FrontProjectGeneration.class);
    verify(frontProjectGenerationMapper).insertFrontProjectGeneration(generationCaptor.capture());
    assertEquals("business_loop_plan", generationCaptor.getValue().getGenerateType());
    assertEquals("1", generationCaptor.getValue().getSuccess());
}

@Test
public void generateDatabasePromptIncludesBusinessLoopPlan()
{
    when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
    when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.<FrontProjectTable>emptyList());
    when(deepSeekClient.chat(anyString())).thenReturn(mockResponse());
    GenerateDatabaseRequest request = request();
    request.setBusinessLoopPlan(loopPlanJson());

    service.generateDatabase(7L, 10L, request);

    ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
    verify(deepSeekClient).chat(promptCaptor.capture());
    assertTrue(promptCaptor.getValue().contains("Business loop plan JSON"));
    assertTrue(promptCaptor.getValue().contains("confirm_borrow"));
    assertTrue(promptCaptor.getValue().contains("status field"));
}

@Test
public void generateBusinessBlueprintPromptIncludesBusinessLoopPlan()
{
    when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
    mockSavedLibraryTables();
    when(deepSeekClient.chat(anyString())).thenReturn(executableBusinessBlueprintResponse());
    GenerateBusinessBlueprintRequest request = businessBlueprintRequest();
    request.setBusinessLoopPlan(loopPlanJson());

    service.generateBusinessBlueprint(7L, 10L, request);

    ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
    verify(deepSeekClient).chat(promptCaptor.capture());
    assertTrue(promptCaptor.getValue().contains("Business loop plan JSON"));
    assertTrue(promptCaptor.getValue().contains("Every required action"));
    assertTrue(promptCaptor.getValue().contains("confirm_borrow"));
}

private GenerateBusinessLoopPlanRequest loopPlanRequest()
{
    GenerateBusinessLoopPlanRequest request = new GenerateBusinessLoopPlanRequest();
    request.setProjectName("Library Borrow System");
    request.setProjectDesc("");
    request.setAppBlueprint(frontendPagesBlueprint());
    return request;
}

private String loopPlanJson()
{
    return "{\"domainName\":\"library_borrow\",\"coreObjects\":[{\"code\":\"book\",\"name\":\"Book\",\"tableName\":\"book_info\"}],"
            + "\"stateMachines\":[{\"objectCode\":\"borrow_record\",\"tableName\":\"borrow_record\",\"statusField\":\"borrow_status\","
            + "\"states\":[\"pending\",\"borrowing\"],\"transitions\":[{\"actionCode\":\"confirm_borrow\",\"fromState\":\"pending\",\"toState\":\"borrowing\"}]}],"
            + "\"actions\":[{\"code\":\"confirm_borrow\",\"name\":\"Confirm Borrow\",\"ownerObject\":\"borrow_record\",\"ownerTable\":\"borrow_record\","
            + "\"required\":true,\"stateTransitions\":[{\"actionCode\":\"confirm_borrow\",\"fromState\":\"pending\",\"toState\":\"borrowing\"}],"
            + "\"quantityEffects\":[{\"actionCode\":\"confirm_borrow\",\"tableName\":\"book_info\",\"quantityField\":\"available_count\",\"effectType\":\"DECREASE_NUMBER\",\"amount\":\"1\"}]}],"
            + "\"quantityRules\":[{\"actionCode\":\"confirm_borrow\",\"tableName\":\"book_info\",\"quantityField\":\"available_count\",\"effectType\":\"DECREASE_NUMBER\",\"amount\":\"1\"}],"
            + "\"recordRules\":[],\"pageBindings\":[],\"acceptanceScenarios\":[{\"name\":\"Borrow\",\"steps\":[\"confirm\"],\"expectedResults\":[\"stock decreases\"]}]}";
}

private String loopPlanResponse()
{
    return loopPlanJson();
}

Add imports in the test:

import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
import com.ruoyi.generator.domain.front.dto.GenerateBusinessLoopPlanRequest;
  • Step 2: Extend IAiGenerateService
BusinessLoopPlan generateBusinessLoopPlan(Long userId, Long projectId, GenerateBusinessLoopPlanRequest request);

Add imports for BusinessLoopPlan and GenerateBusinessLoopPlanRequest.

  • Step 3: Add service dependencies

In AiGenerateServiceImpl, add:

@Autowired
private BusinessLoopPlanValidator businessLoopPlanValidator;

In AiGenerateServiceImplTest.setUp, add:

setField("businessLoopPlanValidator", new BusinessLoopPlanValidator());
  • Step 4: Implement generateBusinessLoopPlan

Add this method to AiGenerateServiceImpl:

@Override
@Transactional
public BusinessLoopPlan generateBusinessLoopPlan(Long userId, Long projectId, GenerateBusinessLoopPlanRequest request)
{
    assertFrontUserLoggedIn(userId);
    FrontProject project = getProject(userId, projectId);
    String prompt = buildBusinessLoopPlanPrompt(project, request);
    long start = System.currentTimeMillis();
    FrontProjectGeneration generation = newGeneration(projectId, userId, prompt, "business_loop_plan");
    try
    {
        String aiContent = deepSeekClient.chat(prompt);
        BusinessLoopPlan plan = parseBusinessLoopPlanResponse(aiContent);
        businessLoopPlanValidator.validate(plan);
        markBusinessLoopPlanGenerated(projectId, plan, request);
        generation.setResponsePayload(aiContent);
        generation.setSuccess("1");
        generation.setElapsedMs(System.currentTimeMillis() - start);
        generationRecordService.insert(generation);
        return plan;
    }
    catch (RuntimeException e)
    {
        generation.setSuccess("0");
        generation.setErrorMessage(sanitizeErrorMessage(e.getMessage()));
        generation.setElapsedMs(System.currentTimeMillis() - start);
        generationRecordService.insert(generation);
        throw e;
    }
}
  • Step 5: Add loop-plan prompt and parsing helpers

Add these helpers to AiGenerateServiceImpl:

private String buildBusinessLoopPlanPrompt(FrontProject project, GenerateBusinessLoopPlanRequest request)
{
    StringBuilder prompt = new StringBuilder();
    prompt.append("Generate a generic business loop plan for the target generated project. Return JSON only matching the BusinessLoopPlan shape.\n");
    prompt.append("Do not generate CRUD. Do not generate code, SQL, database tables, or menu rows.\n");
    prompt.append("The plan must contain at least one required end-to-end workflow with a state transition.\n");
    prompt.append("The plan must contain quantity change or record creation rules, and should contain both when the domain supports them.\n");
    prompt.append("Use action codes in lowercase snake_case. Required actions must be executable later with safe DSL effects: INSERT_ROW, UPDATE_FIELDS, SET_STATUS, INCREASE_NUMBER, DECREASE_NUMBER.\n");
    prompt.append("If the project name is clear, infer the loop without asking for more text. Library borrow, order, approval, inventory, appointment, repair, and leave systems must not fall back to CRUD.\n");
    prompt.append("Project name: ").append(firstNonBlank(request == null ? null : request.getProjectName(), project.getProjectName())).append("\n");
    prompt.append("Project description: ").append(firstNonBlank(request == null ? null : request.getProjectDesc(), project.getProjectDesc())).append("\n");
    prompt.append("Industry template: ").append(firstNonBlank(request == null ? null : request.getIndustryTemplate(), project.getIndustryTemplate())).append("\n");
    prompt.append("Extra requirements: ").append(request == null ? "" : StringUtils.defaultString(request.getExtraRequirements())).append("\n");
    prompt.append("Confirmed app blueprint JSON: ").append(request == null ? "" : StringUtils.defaultString(request.getAppBlueprint())).append("\n");
    return prompt.toString();
}

private BusinessLoopPlan parseBusinessLoopPlanResponse(String aiContent)
{
    String jsonText = extractJson(aiContent);
    BusinessLoopPlan plan = JSON.parseObject(jsonText, BusinessLoopPlan.class);
    if (plan == null)
    {
        throw new ServiceException("AI did not return a business loop plan");
    }
    return plan;
}

private void markBusinessLoopPlanGenerated(Long projectId, BusinessLoopPlan plan, GenerateBusinessLoopPlanRequest request)
{
    FrontProject project = new FrontProject();
    project.setProjectId(projectId);
    project.setPreviewStatus("0");
    project.setBusinessLoopPlan(JSON.toJSONString(plan));
    project.setRequirementKeyword(request == null ? null : request.getExtraRequirements());
    project.setProjectDesc(request == null ? null : request.getProjectDesc());
    project.setIndustryTemplate(request == null ? null : request.getIndustryTemplate());
    frontProjectMapper.updateFrontProject(project);
}
  • Step 6: Add business loop prompt propagation

In buildPrompt, after appendAppBlueprintConstraints(prompt, project, request);, add:

appendBusinessLoopDatabaseConstraints(prompt, project, request);

Add:

private void appendBusinessLoopDatabaseConstraints(StringBuilder prompt, FrontProject project, GenerateDatabaseRequest request)
{
    String businessLoopPlan = firstNonBlank(request == null ? null : request.getBusinessLoopPlan(), project.getBusinessLoopPlan());
    if (StringUtils.isBlank(businessLoopPlan))
    {
        return;
    }
    prompt.append("Business loop plan JSON: ").append(businessLoopPlan).append("\n");
    prompt.append("Database generation constraints from the business loop plan:\n");
    prompt.append("- Core objects must map to generated business tables.\n");
    prompt.append("- State machines must have the requested status field on the requested table.\n");
    prompt.append("- Quantity rules must have numeric fields such as stock, available_count, balance, or quantity.\n");
    prompt.append("- Record rules must have record tables and reference fields needed by required actions.\n");
}

In buildBusinessBlueprintPrompt, after appendBusinessAppBlueprintConstraints(prompt, project, request);, add:

appendBusinessLoopActionConstraints(prompt, project, request);

Add:

private void appendBusinessLoopActionConstraints(StringBuilder prompt, FrontProject project, GenerateBusinessBlueprintRequest request)
{
    String businessLoopPlan = firstNonBlank(request == null ? null : request.getBusinessLoopPlan(), project.getBusinessLoopPlan());
    if (StringUtils.isBlank(businessLoopPlan))
    {
        return;
    }
    prompt.append("Business loop plan JSON: ").append(businessLoopPlan).append("\n");
    prompt.append("Every required action in the business loop plan must have one matching businessActions entry.\n");
    prompt.append("Actions with stateTransitions must emit SET_STATUS or UPDATE_FIELDS effects.\n");
    prompt.append("Actions with quantityEffects must emit INCREASE_NUMBER or DECREASE_NUMBER effects.\n");
    prompt.append("Actions with recordEffects must emit INSERT_ROW or UPDATE_FIELDS effects against the record table.\n");
}
  • Step 7: Run AI service tests

Run:

mvn -q -pl ruoyi-generator -Dtest=AiGenerateServiceImplTest test

Expected: existing tests and new loop-plan tests pass.

  • Step 8: Commit
git add -- ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IAiGenerateService.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java
git commit -m "Generate and propagate business loop plans"

Task 4: One-Click Orchestration and Audit Enforcement

Files:

  • Modify: ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImpl.java

  • Test: ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImplTest.java

  • Step 1: Add failing orchestration tests

In OneClickProjectGenerationServiceImplTest, add a mock field:

@Mock
private BusinessLoopCoverageValidator businessLoopCoverageValidator;

In setUp, add:

setField("businessLoopCoverageValidator", businessLoopCoverageValidator);

Add this test:

@Test
public void generateRunsBusinessLoopPlanBeforeDatabaseAndAuditsCoverage()
{
    AiGenerationTask task = oneClickTask();
    AiGenerationTaskCreateRequest request = request();
    FrontProject project = project();
    AppBlueprintDesign appBlueprint = new AppBlueprintDesign();
    BusinessLoopPlan loopPlan = loopPlan();
    DatabaseDesignResponse database = database();
    BusinessActionDesign action = businessAction("confirm_borrow");
    BusinessLoopAuditResult databaseAudit = completeAudit();
    BusinessLoopAuditResult actionAudit = completeAudit();

    when(frontProjectService.getProject(10L, 20L)).thenReturn(project);
    when(aiGenerateService.generateAppBlueprint(eq(10L), eq(20L), any(GenerateAppBlueprintRequest.class))).thenReturn(appBlueprint);
    when(aiGenerateService.generateBusinessLoopPlan(eq(10L), eq(20L), any(GenerateBusinessLoopPlanRequest.class))).thenReturn(loopPlan);
    when(aiGenerateService.generateDatabase(eq(10L), eq(20L), any(GenerateDatabaseRequest.class))).thenReturn(database);
    when(businessLoopCoverageValidator.auditDatabase(eq(loopPlan), eq(database))).thenReturn(databaseAudit);
    when(aiGenerateService.generateBusinessBlueprint(eq(10L), eq(20L), any(GenerateBusinessBlueprintRequest.class))).thenReturn(Collections.singletonList(action));
    when(businessLoopCoverageValidator.auditBusinessActions(eq(loopPlan), any())).thenReturn(actionAudit);
    when(frontendPageDesignService.initializePageDesigns(10L, 20L, "frontend")).thenReturn(initResponse());
    when(frontendPageDesignService.initializePageDesigns(10L, 20L, "admin")).thenReturn(initResponse());
    when(frontProjectRunPreviewService.start(10L, 20L)).thenReturn(runningPreview());

    OneClickProjectGenerationResult result = service.generate(task, request);

    assertTrue(result.getReport().isBusinessLoopComplete());
    assertEquals(1, result.getReport().getLoopActions());
    InOrder inOrder = inOrder(aiGenerateService, businessLoopCoverageValidator, frontendPageDesignService);
    inOrder.verify(aiGenerateService).generateAppBlueprint(eq(10L), eq(20L), any(GenerateAppBlueprintRequest.class));
    inOrder.verify(aiGenerateService).generateBusinessLoopPlan(eq(10L), eq(20L), any(GenerateBusinessLoopPlanRequest.class));
    inOrder.verify(aiGenerateService).generateDatabase(eq(10L), eq(20L), any(GenerateDatabaseRequest.class));
    inOrder.verify(businessLoopCoverageValidator).auditDatabase(eq(loopPlan), eq(database));
    inOrder.verify(aiGenerateService).generateBusinessBlueprint(eq(10L), eq(20L), any(GenerateBusinessBlueprintRequest.class));
    inOrder.verify(businessLoopCoverageValidator).auditBusinessActions(eq(loopPlan), any());
    inOrder.verify(frontendPageDesignService).initializePageDesigns(10L, 20L, "frontend");
}

@Test
public void generateFailsWhenBusinessLoopAuditIsIncomplete()
{
    stubSuccessfulGeneration(project());
    BusinessLoopPlan loopPlan = loopPlan();
    when(aiGenerateService.generateBusinessLoopPlan(eq(10L), eq(20L), any(GenerateBusinessLoopPlanRequest.class))).thenReturn(loopPlan);
    BusinessLoopAuditResult failedAudit = new BusinessLoopAuditResult();
    failedAudit.setComplete(false);
    failedAudit.getErrors().add("Missing field available_count on table book_info");
    when(businessLoopCoverageValidator.auditDatabase(eq(loopPlan), any(DatabaseDesignResponse.class))).thenReturn(failedAudit);

    try
    {
        service.generate(oneClickTask(), request());
        fail("Expected OneClickProjectGenerationException");
    }
    catch (OneClickProjectGenerationException e)
    {
        assertEquals(OneClickProjectGenerationResult.STAGE_DATABASE_LOOP_AUDIT, e.getResult().getFailedStage());
        assertTrue(e.getResult().getErrorMessage().contains("available_count"));
    }
}

Add helper methods:

private BusinessLoopPlan loopPlan()
{
    BusinessLoopPlan plan = new BusinessLoopPlan();
    BusinessLoopPlan.BusinessLoopAction action = new BusinessLoopPlan.BusinessLoopAction();
    action.setCode("confirm_borrow");
    action.setRequired(true);
    action.setOwnerTable("borrow_record");
    plan.getActions().add(action);
    BusinessLoopPlan.BusinessLoopStateMachine stateMachine = new BusinessLoopPlan.BusinessLoopStateMachine();
    stateMachine.setTableName("borrow_record");
    stateMachine.setStatusField("borrow_status");
    BusinessLoopPlan.BusinessLoopStateTransition transition = new BusinessLoopPlan.BusinessLoopStateTransition();
    transition.setActionCode("confirm_borrow");
    transition.setFromState("pending");
    transition.setToState("borrowing");
    stateMachine.getTransitions().add(transition);
    plan.getStateMachines().add(stateMachine);
    BusinessLoopPlan.BusinessLoopQuantityEffect quantity = new BusinessLoopPlan.BusinessLoopQuantityEffect();
    quantity.setActionCode("confirm_borrow");
    quantity.setTableName("book_info");
    quantity.setQuantityField("available_count");
    plan.getQuantityRules().add(quantity);
    return plan;
}

private DatabaseDesignResponse database()
{
    DatabaseDesignResponse database = new DatabaseDesignResponse();
    database.setProjectId(20L);
    database.setTables(Collections.singletonList(new DatabaseTableDesign()));
    return database;
}

private BusinessActionDesign businessAction(String code)
{
    BusinessActionDesign action = new BusinessActionDesign();
    action.setCode(code);
    return action;
}

private BusinessLoopAuditResult completeAudit()
{
    BusinessLoopAuditResult result = new BusinessLoopAuditResult();
    result.setComplete(true);
    result.setLoopActions(1);
    result.setStateTransitions(1);
    result.setQuantityRules(1);
    result.setRecordRules(0);
    return result;
}

Add imports:

import com.ruoyi.generator.domain.front.dto.BusinessLoopAuditResult;
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
import com.ruoyi.generator.domain.front.dto.GenerateBusinessLoopPlanRequest;
  • Step 2: Wire dependencies and stage calls

In OneClickProjectGenerationServiceImpl, add:

@Autowired
private BusinessLoopCoverageValidator businessLoopCoverageValidator;

After app-blueprint generation, add:

updateStage(task, result, OneClickProjectGenerationResult.STAGE_BUSINESS_LOOP_PLAN, 25, "正在规划业务闭环");
BusinessLoopPlan businessLoopPlan = aiGenerateService.generateBusinessLoopPlan(task.getUserId(), task.getProjectId(),
        toBusinessLoopPlanRequest(request, appBlueprintJson));
String businessLoopPlanJson = JSON.toJSONString(businessLoopPlan);

Change database progress from 35 to 42 and pass the loop plan:

DatabaseDesignResponse database = aiGenerateService.generateDatabase(task.getUserId(), task.getProjectId(),
        toDatabaseRequest(request, appBlueprintJson, businessLoopPlanJson));

After table count:

updateStage(task, result, OneClickProjectGenerationResult.STAGE_DATABASE_LOOP_AUDIT, 48, "正在校验数据库是否覆盖业务闭环");
BusinessLoopAuditResult databaseAudit = businessLoopCoverageValidator.auditDatabase(businessLoopPlan, database);
applyLoopAudit(result, databaseAudit);
if (!databaseAudit.isComplete())
{
    throw auditFailure(result, OneClickProjectGenerationResult.STAGE_DATABASE_LOOP_AUDIT, databaseAudit);
}

Change business generation call:

List<BusinessActionDesign> businessActions = aiGenerateService.generateBusinessBlueprint(task.getUserId(),
        task.getProjectId(), toBusinessBlueprintRequest(request, appBlueprintJson, businessLoopPlanJson));

After business action count:

updateStage(task, result, OneClickProjectGenerationResult.STAGE_BUSINESS_LOOP_AUDIT, 58, "正在校验业务动作是否形成闭环");
BusinessLoopAuditResult actionAudit = businessLoopCoverageValidator.auditBusinessActions(businessLoopPlan, businessActions);
applyLoopAudit(result, actionAudit);
if (!actionAudit.isComplete())
{
    throw auditFailure(result, OneClickProjectGenerationResult.STAGE_BUSINESS_LOOP_AUDIT, actionAudit);
}
  • Step 3: Add request and audit helpers

Add helpers:

private GenerateBusinessLoopPlanRequest toBusinessLoopPlanRequest(AiGenerationTaskCreateRequest source, String appBlueprintJson)
{
    GenerateBusinessLoopPlanRequest request = new GenerateBusinessLoopPlanRequest();
    request.setProjectName(source.getProjectName());
    request.setProjectDesc(source.getProjectDesc());
    request.setIndustryTemplate(source.getIndustryTemplate());
    request.setExtraRequirements(source.getExtraRequirements());
    request.setAppBlueprint(appBlueprintJson);
    return request;
}

private GenerateDatabaseRequest toDatabaseRequest(AiGenerationTaskCreateRequest source, String appBlueprintJson,
        String businessLoopPlanJson)
{
    GenerateDatabaseRequest request = new GenerateDatabaseRequest();
    request.setProjectName(source.getProjectName());
    request.setProjectDesc(source.getProjectDesc());
    request.setIndustryTemplate(source.getIndustryTemplate());
    request.setExtraRequirements(source.getExtraRequirements());
    request.setAppBlueprint(appBlueprintJson);
    request.setBusinessLoopPlan(businessLoopPlanJson);
    return request;
}

private GenerateBusinessBlueprintRequest toBusinessBlueprintRequest(AiGenerationTaskCreateRequest source,
        String appBlueprintJson, String businessLoopPlanJson)
{
    GenerateBusinessBlueprintRequest request = new GenerateBusinessBlueprintRequest();
    request.setProjectName(source.getProjectName());
    request.setProjectDesc(source.getProjectDesc());
    request.setIndustryTemplate(source.getIndustryTemplate());
    request.setExtraRequirements(source.getExtraRequirements());
    request.setAppBlueprint(appBlueprintJson);
    request.setBusinessLoopPlan(businessLoopPlanJson);
    return request;
}

private void applyLoopAudit(OneClickProjectGenerationResult result, BusinessLoopAuditResult audit)
{
    if (audit == null || result.getReport() == null)
    {
        return;
    }
    result.getReport().setBusinessLoopComplete(audit.isComplete());
    result.getReport().setLoopActions(audit.getLoopActions());
    result.getReport().setStateTransitions(audit.getStateTransitions());
    result.getReport().setQuantityRules(audit.getQuantityRules());
    result.getReport().setRecordRules(audit.getRecordRules());
    result.getReport().setMissingRequiredActions(audit.getMissingRequiredActions());
}

private OneClickProjectGenerationException auditFailure(OneClickProjectGenerationResult result, String stage,
        BusinessLoopAuditResult audit)
{
    result.setFailedStage(stage);
    String message = audit == null || audit.getErrors() == null || audit.getErrors().isEmpty()
            ? "业务闭环不完整"
            : audit.getErrors().get(0);
    result.setErrorMessage(message);
    result.getSuggestedActions().add("进入高级调整查看业务闭环计划和缺失项后重新生成");
    return new OneClickProjectGenerationException(message, result, null);
}

Remove or replace the old two-argument toDatabaseRequest and toBusinessBlueprintRequest methods so there is no overload ambiguity.

  • Step 4: Update successful generation stubs

In stubSuccessfulGeneration, add:

BusinessLoopPlan loopPlan = loopPlan();
when(aiGenerateService.generateBusinessLoopPlan(eq(10L), eq(20L), any(GenerateBusinessLoopPlanRequest.class)))
        .thenReturn(loopPlan);
when(businessLoopCoverageValidator.auditDatabase(eq(loopPlan), any(DatabaseDesignResponse.class)))
        .thenReturn(completeAudit());
when(businessLoopCoverageValidator.auditBusinessActions(eq(loopPlan), any()))
        .thenReturn(completeAudit());
  • Step 5: Run one-click tests

Run:

mvn -q -pl ruoyi-generator -Dtest=OneClickProjectGenerationServiceImplTest test

Expected: one-click tests pass and verify the new business loop stages.

  • Step 6: Commit
git add -- ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImpl.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImplTest.java
git commit -m "Enforce business loop audit in one-click generation"

Task 5: Page Action Binding From Loop Plan

Files:

  • Modify: ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontendPageDesignService.java

  • Modify: ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImpl.java

  • Test: ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontendPageDesignServiceTest.java

  • Test: ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImplTest.java

  • Step 1: Add failing page binding test

In FrontendPageDesignServiceTest, add:

@Test
public void applyBusinessLoopBindingsAddsBusinessRowActions()
{
    FrontProjectPageDesign current = currentDesign();
    current.setActionJson("{\"toolbarActions\":[],\"rowActions\":[]}");
    when(frontProjectService.getProject(7L, 10L)).thenReturn(project());
    when(pageDesignMapper.selectDesignsByProjectIdAndScope(10L, "frontend"))
            .thenReturn(Collections.singletonList(current));
    when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(bookTable()));
    when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Collections.singletonList(column("book_title", "bookTitle", "1", "1", "1")));

    service.applyBusinessLoopBindings(7L, 10L, bindingPlan(), "frontend");

    ArgumentCaptor<FrontProjectPageDesign> captor = ArgumentCaptor.forClass(FrontProjectPageDesign.class);
    verify(pageDesignMapper).updateFrontProjectPageDesign(captor.capture());
    assertTrue(captor.getValue().getActionJson().contains("business:borrow_book"));
}

private BusinessLoopPlan bindingPlan()
{
    BusinessLoopPlan plan = new BusinessLoopPlan();
    BusinessLoopPlan.BusinessLoopPageBinding binding = new BusinessLoopPlan.BusinessLoopPageBinding();
    binding.setActionCode("borrow_book");
    binding.setTarget("frontend");
    binding.setPageCode("book_catalog_page");
    binding.setTableName("book_info");
    binding.setSlot("list.rowActions");
    plan.getPageBindings().add(binding);
    return plan;
}

Add import:

import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
  • Step 2: Implement applyBusinessLoopBindings

In FrontendPageDesignService, add public method:

@Transactional
public void applyBusinessLoopBindings(Long userId, Long projectId, BusinessLoopPlan plan, String scope)
{
    frontProjectService.getProject(userId, projectId);
    String normalizedScope = normalizeScope(scope);
    List<FrontProjectPageDesign> designs = safeDesigns(pageDesignMapper.selectDesignsByProjectIdAndScope(projectId, normalizedScope));
    Map<String, FrontProjectPageDesign> designsByPageCode = new LinkedHashMap<String, FrontProjectPageDesign>();
    for (FrontProjectPageDesign design : designs)
    {
        if (design != null && StringUtils.isNotEmpty(design.getPageCode()))
        {
            designsByPageCode.put(design.getPageCode(), design);
        }
    }
    Map<String, TableBundle> tablesByName = loadTables(projectId);
    for (BusinessLoopPlan.BusinessLoopPageBinding binding : safeLoopBindings(plan))
    {
        if (binding == null || !bindingTargetMatches(binding, normalizedScope))
        {
            continue;
        }
        FrontProjectPageDesign design = designsByPageCode.get(binding.getPageCode());
        if (design == null)
        {
            continue;
        }
        TableBundle table = StringUtils.isEmpty(design.getTableName()) ? null : tablesByName.get(design.getTableName());
        String actionJson = mergeBusinessActionBinding(design.getActionJson(), binding);
        design.setActionJson(normalizeAction(actionJson, table, normalizedScope));
        design.setVersion(design.getVersion() == null ? 2 : design.getVersion() + 1);
        pageDesignMapper.updateFrontProjectPageDesign(design);
    }
}

Add helpers:

private List<BusinessLoopPlan.BusinessLoopPageBinding> safeLoopBindings(BusinessLoopPlan plan)
{
    return plan == null || plan.getPageBindings() == null
            ? new ArrayList<BusinessLoopPlan.BusinessLoopPageBinding>() : plan.getPageBindings();
}

private boolean bindingTargetMatches(BusinessLoopPlan.BusinessLoopPageBinding binding, String scope)
{
    String target = StringUtils.isEmpty(binding.getTarget()) ? FRONTEND_SCOPE : binding.getTarget();
    return normalizeScope(scope).equals(normalizeScope("admin_frontend".equals(target) ? ADMIN_SCOPE : target));
}

@SuppressWarnings("unchecked")
private String mergeBusinessActionBinding(String actionJson, BusinessLoopPlan.BusinessLoopPageBinding binding)
{
    Map<String, Object> action = StringUtils.isEmpty(actionJson)
            ? new LinkedHashMap<String, Object>()
            : readMap(actionJson, "页面按钮JSON格式错误");
    String key = "list.toolbarActions".equals(binding.getSlot()) ? "toolbarActions" : "rowActions";
    Object current = action.get(key);
    List<Object> actions = current instanceof List ? new ArrayList<Object>((List<Object>) current) : new ArrayList<Object>();
    String actionCode = "business:" + binding.getActionCode();
    for (Object item : actions)
    {
        if (item instanceof Map && actionCode.equals(stringValue(((Map<String, Object>) item).get("code"))))
        {
            return writeJson(action);
        }
        if (actionCode.equals(stringValue(item)))
        {
            return writeJson(action);
        }
    }
    Map<String, Object> item = new LinkedHashMap<String, Object>();
    item.put("code", actionCode);
    actions.add(item);
    action.put(key, actions);
    if (!action.containsKey("toolbarActions"))
    {
        action.put("toolbarActions", new ArrayList<Object>());
    }
    if (!action.containsKey("rowActions"))
    {
        action.put("rowActions", new ArrayList<Object>());
    }
    return writeJson(action);
}

Add import:

import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
  • Step 3: Wire one-click page binding

In OneClickProjectGenerationServiceImpl, after each initializePageDesigns call, add:

frontendPageDesignService.applyBusinessLoopBindings(task.getUserId(), task.getProjectId(), businessLoopPlan, "frontend");

for frontend, and:

frontendPageDesignService.applyBusinessLoopBindings(task.getUserId(), task.getProjectId(), businessLoopPlan, "admin");

for admin.

Update OneClickProjectGenerationServiceImplTest.generateRunsBusinessLoopPlanBeforeDatabaseAndAuditsCoverage to verify:

verify(frontendPageDesignService).applyBusinessLoopBindings(10L, 20L, loopPlan, "frontend");
verify(frontendPageDesignService).applyBusinessLoopBindings(10L, 20L, loopPlan, "admin");
  • Step 4: Run page binding tests

Run:

mvn -q -pl ruoyi-generator -Dtest=FrontendPageDesignServiceTest,OneClickProjectGenerationServiceImplTest test

Expected: page design and one-click tests pass.

  • Step 5: Commit
git add -- ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontendPageDesignService.java ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImpl.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontendPageDesignServiceTest.java ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImplTest.java
git commit -m "Bind business loop actions to generated pages"

Task 6: Frontend Result Reporting

Files:

  • Modify: easycode-web/src/views/GenerateView.vue

  • Test: easycode-web/src/views/generateView.test.mjs

  • Step 1: Add failing source-level test

In generateView.test.mjs, add:

test('GenerateView shows business loop completion in one-click result', () => {
  assert.match(source, /businessLoopComplete/)
  assert.match(source, /loopActions/)
  assert.match(source, /missingRequiredActions/)
  assert.match(source, /业务闭环/)
})
  • Step 2: Render loop report fields

In the one-click result card inside GenerateView.vue, after the existing el-descriptions block, add:

<el-descriptions v-if="oneClickResult.report" class="loop-report" :column="2" border>
  <el-descriptions-item label="业务闭环">
    {{ oneClickResult.report.businessLoopComplete ? '已完成' : '未完成' }}
  </el-descriptions-item>
  <el-descriptions-item label="闭环动作">
    {{ oneClickResult.report.loopActions || 0 }}
  </el-descriptions-item>
  <el-descriptions-item label="状态流转">
    {{ oneClickResult.report.stateTransitions || 0 }}
  </el-descriptions-item>
  <el-descriptions-item label="数量规则">
    {{ oneClickResult.report.quantityRules || 0 }}
  </el-descriptions-item>
</el-descriptions>
<el-alert
  v-if="oneClickResult.report?.missingRequiredActions?.length"
  type="warning"
  :title="`缺少业务动作:${oneClickResult.report.missingRequiredActions.join('、')}`"
  show-icon
  :closable="false"
/>

Add style:

.loop-report {
  margin-top: 14px;
}
  • Step 3: Run frontend test

Run:

cd easycode-web
node --test src/views/generateView.test.mjs

Expected: generateView.test.mjs passes.

  • Step 4: Commit
git add -- easycode-web/src/views/GenerateView.vue easycode-web/src/views/generateView.test.mjs
git commit -m "Show business loop report in one-click UI"

Task 7: Verification and Regression Sweep

Files:

  • All files modified by Tasks 1-6.

  • Step 1: Run focused backend tests

mvn -q -pl ruoyi-generator -Dtest=BusinessLoopPlanValidatorTest,BusinessLoopCoverageValidatorTest,AiGenerateServiceImplTest,OneClickProjectGenerationServiceImplTest,FrontendPageDesignServiceTest test

Expected: all focused backend tests pass.

  • Step 2: Run generator module tests
mvn -q -pl ruoyi-generator test

Expected: generator module tests pass.

  • Step 3: Run frontend source-level tests
cd easycode-web
node --test src/views/generateView.test.mjs src/views/previewView.test.mjs src/components/frontendPageDesigner.test.mjs

Expected: listed frontend tests pass.

  • Step 4: Run frontend build
cd easycode-web
npm run build

Expected: Vite build completes successfully.

  • Step 5: Run diff checks
git diff --check
git status --short

Expected: git diff --check prints no errors. git status --short shows only intentional files from this implementation plus pre-existing unrelated workspace changes.

  • Step 6: Manual smoke test
  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:

git add -- <fixed-files>
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.