1981 lines
82 KiB
Markdown
1981 lines
82 KiB
Markdown
# 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.
|