# 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<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**

```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<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**

```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<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:

```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
<result property="businessLoopPlan" column="business_loop_plan" />
```

Add `business_loop_plan` to the `selectFrontProjectVo` column list immediately after `app_blueprint`.

Add insert mappings:

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

and:

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

Add update mapping:

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

- [ ] **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.<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`:

```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:

```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.<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:

```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<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:

```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<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:

```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<BusinessActionDesign> 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<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:

```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<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:

```java
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:

```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
<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:

```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 -- <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.
