diff --git a/RuoYi-Vue/docs/superpowers/plans/2026-05-23-business-blueprint-draft.md b/RuoYi-Vue/docs/superpowers/plans/2026-05-23-business-blueprint-draft.md new file mode 100644 index 0000000..0a8cda5 --- /dev/null +++ b/RuoYi-Vue/docs/superpowers/plans/2026-05-23-business-blueprint-draft.md @@ -0,0 +1,147 @@ +# Business Blueprint Draft 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:** Add a safe business blueprint draft flow so AI can return business actions such as borrow and return alongside generated table design. + +**Architecture:** Extend the existing `DatabaseDesignResponse` draft contract with `businessActions`, normalize and validate action metadata in the backend, persist it on `front_project`, expose it to the Vue workbench, and pass it into generated project Velocity context. The MVP stores blueprint JSON on the project draft and keeps actual code expansion template-ready rather than hardcoding generated business methods in this pass. + +**Tech Stack:** Java 8, Spring Boot, MyBatis, Fastjson2/Jackson, JUnit 4/Mockito, Vue 3, Element Plus, Velocity. + +--- + +## File Structure + +- `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java`: new DTO for one business action draft. +- `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionEffectDesign.java`: new DTO for normalized effects such as update/insert/status change. +- `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/DatabaseDesignResponse.java`: add `List businessActions`. +- `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java`: add `businessBlueprint`. +- `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java`: add transient `businessActions`. +- `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiBusinessBlueprintValidator.java`: new validator for action codes, table references, request fields, rule/effect limits, and safe strings. +- `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java`: prompt for business actions, parse aliases, normalize actions, validate blueprint, and persist JSON. +- `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java`: save/load blueprint JSON with database draft. +- `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectConverter.java`: copy business actions into `GenProject`. +- `ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java`: expose `businessActions` to project-level templates. +- `ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml`: map `business_blueprint`. +- `sql/front_workbench.sql` and `sql/db.sql`: add `business_blueprint longtext`. +- `easycode-web/src/views/GenerateView.vue`: show business blueprint draft returned by AI and saved with the database design. +- Backend tests in `AiGenerateServiceImplTest`, `FrontProjectServiceImplTest`, `FrontProjectPreviewServiceImplTest`. + +--- + +### Task 1: Backend Blueprint DTO And Validation + +**Files:** +- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java` +- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionEffectDesign.java` +- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/DatabaseDesignResponse.java` +- Create: `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiBusinessBlueprintValidator.java` +- Test: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java` + +- [ ] **Step 1: Write failing tests** + +Add tests asserting AI returns `businessActions`, unsafe action codes are normalized, and actions referencing missing tables are rejected. + +- [ ] **Step 2: Run tests and verify RED** + +Run: `mvn -pl ruoyi-generator -Dtest=AiGenerateServiceImplTest test` +Expected: compile/test failure because `getBusinessActions()` and validator classes do not exist. + +- [ ] **Step 3: Add DTOs and validator** + +Add action fields: `code`, `name`, `ownerTable`, `method`, `path`, `transaction`, `requestFields`, `rules`, `effects`. +Add effect fields: `type`, `targetTable`, `targetField`, `value`, `description`. +Validator rules: max 12 actions, code/path safe, owner table exists, request fields exist in any table, effect tables exist, rule/effect text length bounded. + +- [ ] **Step 4: Run tests and verify GREEN** + +Run: `mvn -pl ruoyi-generator -Dtest=AiGenerateServiceImplTest test` +Expected: PASS. + +### Task 2: Persist And Reload Blueprint Draft + +**Files:** +- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java` +- Modify: `ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml` +- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java` +- Modify: `sql/front_workbench.sql` +- Modify: `sql/db.sql` +- Test: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectServiceImplTest.java` + +- [ ] **Step 1: Write failing tests** + +Add tests asserting `saveDatabase()` writes `businessBlueprint` JSON and `getDatabase()` returns `businessActions`. + +- [ ] **Step 2: Run tests and verify RED** + +Run: `mvn -pl ruoyi-generator -Dtest=FrontProjectServiceImplTest test` +Expected: failure because `businessBlueprint` is not mapped or returned. + +- [ ] **Step 3: Implement persistence** + +Add `business_blueprint longtext` to front project SQL, map it in MyBatis, add `FrontProject.businessBlueprint`, serialize `request.getBusinessActions()` on save, and deserialize on get. + +- [ ] **Step 4: Run tests and verify GREEN** + +Run: `mvn -pl ruoyi-generator -Dtest=FrontProjectServiceImplTest test` +Expected: PASS. + +### Task 3: Expose Blueprint To Preview Generation + +**Files:** +- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java` +- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectConverter.java` +- Modify: `ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java` +- Test: `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImplTest.java` + +- [ ] **Step 1: Write failing test** + +Add a test that converts a front draft with one business action and asserts `GenProject.businessActions` is passed into the generation service and project Velocity context can read it. + +- [ ] **Step 2: Run test and verify RED** + +Run: `mvn -pl ruoyi-generator -Dtest=FrontProjectPreviewServiceImplTest test` +Expected: compile/test failure because `GenProject.getBusinessActions()` does not exist. + +- [ ] **Step 3: Implement conversion and Velocity context** + +Add transient `businessActions` to `GenProject`, copy it in `FrontProjectConverter`, and put `businessActions` plus `hasBusinessActions` into `VelocityUtils.prepareContextProject`. + +- [ ] **Step 4: Run test and verify GREEN** + +Run: `mvn -pl ruoyi-generator -Dtest=FrontProjectPreviewServiceImplTest test` +Expected: PASS. + +### Task 4: Show Blueprint Draft In Workbench + +**Files:** +- Modify: `easycode-web/src/views/GenerateView.vue` + +- [ ] **Step 1: Add frontend normalization** + +Extend `normalizeDatabase()` with `businessActions: Array.isArray(data.businessActions) ? data.businessActions : []`. + +- [ ] **Step 2: Add compact UI** + +Add a full-width panel under database design that lists action name, method, path, owner table, rules, and effects. Keep it read-only for this MVP. + +- [ ] **Step 3: Build frontend** + +Run: use the existing Node build command for `easycode-web` if dependencies are available. +Expected: build passes, or dependency/network blocker is reported. + +### Task 5: Full Verification + +**Files:** +- No new files. + +- [ ] **Step 1: Run focused backend tests** + +Run: `mvn -pl ruoyi-generator -Dtest=AiGenerateServiceImplTest,FrontProjectServiceImplTest,FrontProjectPreviewServiceImplTest test` +Expected: PASS. + +- [ ] **Step 2: Run final status check** + +Run: `git status --short` +Expected: only intended files modified. + diff --git a/RuoYi-Vue/easycode-web/src/views/GenerateView.vue b/RuoYi-Vue/easycode-web/src/views/GenerateView.vue index e9df1e2..165fe91 100644 --- a/RuoYi-Vue/easycode-web/src/views/GenerateView.vue +++ b/RuoYi-Vue/easycode-web/src/views/GenerateView.vue @@ -81,6 +81,52 @@ + +
+
+

业务蓝图草稿

+ 动作数量:{{ database.businessActions.length }} +
+
+
+
+
+

{{ action.name || action.code }}

+

{{ action.ownerTable || '未指定主表' }}

+
+ {{ action.method || 'POST' }} +
+
{{ action.path || '-' }}
+
+ 事务 + + 入参:{{ action.requestFields.join('、') }} + +
+
+ 规则 +
    +
  • {{ rule }}
  • +
+
+
+ 影响 +
    +
  • + {{ effect.description || effect.type }} + / {{ effect.targetTable }} + .{{ effect.targetField }} +
  • +
+
+
+
+ +
@@ -103,7 +149,7 @@ const loadingProject = ref(false) const databaseView = ref('editor') const projectId = ref(normalizeProjectId(route.query.projectId)) const errorMessage = ref('') -const database = ref({ tables: [], sql: '', erDiagram: emptyErDiagram() }) +const database = ref({ tables: [], sql: '', erDiagram: emptyErDiagram(), businessActions: [] }) const projectForm = reactive({ projectName: '', keyword: String(route.query.keyword || ''), @@ -139,7 +185,8 @@ function normalizeDatabase(payload) { return { tables: Array.isArray(data.tables) ? data.tables : [], sql: data.sql || '', - erDiagram: normalizeErDiagram(data.erDiagram) + erDiagram: normalizeErDiagram(data.erDiagram), + businessActions: Array.isArray(data.businessActions) ? data.businessActions : [] } } @@ -307,6 +354,70 @@ onMounted(() => { overflow: hidden; } +.blueprint-list { + display: grid; + gap: 14px; + padding: 20px; +} + +.blueprint-action { + border: 1px solid var(--border-color); + border-radius: 8px; + display: grid; + gap: 10px; + padding: 16px; +} + +.blueprint-action-header { + align-items: flex-start; + display: flex; + gap: 12px; + justify-content: space-between; +} + +.blueprint-action h3 { + font-size: 16px; + font-weight: 700; + line-height: 1.3; + margin: 0 0 4px; +} + +.blueprint-path { + background: #f6f8fb; + border-radius: 6px; + color: #2c3e50; + font-family: Consolas, Monaco, monospace; + font-size: 13px; + padding: 8px 10px; + word-break: break-all; +} + +.blueprint-meta { + color: #64748b; + display: flex; + flex-wrap: wrap; + gap: 8px 14px; + font-size: 13px; +} + +.blueprint-section { + display: grid; + gap: 6px; +} + +.blueprint-section strong { + color: #1f2d3d; + font-size: 13px; +} + +.blueprint-section ul { + color: #475569; + display: grid; + gap: 4px; + margin: 0; + padding-left: 18px; +} + .database-header { gap: 16px; } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java index ccee17d..4e88dc3 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java @@ -2,6 +2,7 @@ package com.ruoyi.generator.domain; import com.ruoyi.common.annotation.Excel; import com.ruoyi.common.core.domain.BaseEntity; +import com.ruoyi.generator.domain.front.dto.BusinessActionDesign; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; @@ -57,6 +58,8 @@ public class GenProject extends BaseEntity private transient Boolean frontDraft; + private transient List businessActions; + private List tables; public void setProjectId(Long projectId) @@ -179,6 +182,14 @@ public class GenProject extends BaseEntity return Boolean.TRUE.equals(frontDraft); } + public List getBusinessActions() { + return businessActions; + } + + public void setBusinessActions(List businessActions) { + this.businessActions = businessActions; + } + @Override public String toString() { return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java index e0dd881..f2f0043 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java @@ -20,6 +20,7 @@ public class FrontProject extends BaseEntity private String generateStatus; private String previewStatus; private String erDiagram; + private String businessBlueprint; private String status; private List tables; @@ -49,6 +50,8 @@ public class FrontProject extends BaseEntity public void setPreviewStatus(String previewStatus) { this.previewStatus = previewStatus; } public String getErDiagram() { return erDiagram; } public void setErDiagram(String erDiagram) { this.erDiagram = erDiagram; } + public String getBusinessBlueprint() { return businessBlueprint; } + public void setBusinessBlueprint(String businessBlueprint) { this.businessBlueprint = businessBlueprint; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public List getTables() { return tables; } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java new file mode 100644 index 0000000..4ceae1e --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java @@ -0,0 +1,35 @@ +package com.ruoyi.generator.domain.front.dto; + +import java.util.List; + +public class BusinessActionDesign +{ + private String code; + private String name; + private String ownerTable; + private String method; + private String path; + private Boolean transaction; + private List requestFields; + private List rules; + private List effects; + + 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 getOwnerTable() { return ownerTable; } + public void setOwnerTable(String ownerTable) { this.ownerTable = ownerTable; } + public String getMethod() { return method; } + public void setMethod(String method) { this.method = method; } + public String getPath() { return path; } + public void setPath(String path) { this.path = path; } + public Boolean getTransaction() { return transaction; } + public void setTransaction(Boolean transaction) { this.transaction = transaction; } + public List getRequestFields() { return requestFields; } + public void setRequestFields(List requestFields) { this.requestFields = requestFields; } + public List getRules() { return rules; } + public void setRules(List rules) { this.rules = rules; } + public List getEffects() { return effects; } + public void setEffects(List effects) { this.effects = effects; } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionEffectDesign.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionEffectDesign.java new file mode 100644 index 0000000..a914d2b --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionEffectDesign.java @@ -0,0 +1,21 @@ +package com.ruoyi.generator.domain.front.dto; + +public class BusinessActionEffectDesign +{ + private String type; + private String targetTable; + private String targetField; + private String value; + private String description; + + public String getType() { return type; } + public void setType(String type) { this.type = type; } + public String getTargetTable() { return targetTable; } + public void setTargetTable(String targetTable) { this.targetTable = targetTable; } + public String getTargetField() { return targetField; } + public void setTargetField(String targetField) { this.targetField = targetField; } + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/DatabaseDesignResponse.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/DatabaseDesignResponse.java index 25cde6d..ae1e7ac 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/DatabaseDesignResponse.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/DatabaseDesignResponse.java @@ -6,6 +6,7 @@ public class DatabaseDesignResponse { private Long projectId; private List tables; + private List businessActions; private String sql; private Object erDiagram; @@ -13,6 +14,8 @@ public class DatabaseDesignResponse public void setProjectId(Long projectId) { this.projectId = projectId; } public List getTables() { return tables; } public void setTables(List tables) { this.tables = tables; } + public List getBusinessActions() { return businessActions; } + public void setBusinessActions(List businessActions) { this.businessActions = businessActions; } public String getSql() { return sql; } public void setSql(String sql) { this.sql = sql; } public Object getErDiagram() { return erDiagram; } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiBusinessBlueprintValidator.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiBusinessBlueprintValidator.java new file mode 100644 index 0000000..9db3e41 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiBusinessBlueprintValidator.java @@ -0,0 +1,186 @@ +package com.ruoyi.generator.service.front; + +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +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.BusinessActionDesign; +import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign; +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 AiBusinessBlueprintValidator +{ + private static final int MAX_ACTIONS = 12; + private static final int MAX_RULES = 12; + private static final int MAX_EFFECTS = 12; + private static final Pattern ACTION_CODE = Pattern.compile("^[a-z][a-z0-9_]{1,63}$"); + private static final Pattern API_PATH = Pattern.compile("^/[A-Za-z0-9_./-]{1,180}$"); + + public void validate(DatabaseDesignResponse response) + { + if (response == null || StringUtils.isEmpty(response.getBusinessActions())) + { + return; + } + if (response.getBusinessActions().size() > MAX_ACTIONS) + { + throw new ServiceException("业务动作数量超过限制"); + } + Set tableNames = collectTableNames(response.getTables()); + Set columnNames = collectColumnNames(response.getTables()); + Set actionCodes = new HashSet(); + for (BusinessActionDesign action : response.getBusinessActions()) + { + validateAction(action, tableNames, columnNames, actionCodes); + } + } + + private void validateAction(BusinessActionDesign action, Set tableNames, Set columnNames, Set actionCodes) + { + if (action == null) + { + throw new ServiceException("业务动作不能为空"); + } + if (StringUtils.isBlank(action.getCode()) || !ACTION_CODE.matcher(action.getCode()).matches()) + { + throw new ServiceException("业务动作编码只能包含小写字母、数字和下划线"); + } + if (!actionCodes.add(action.getCode())) + { + throw new ServiceException("业务动作编码重复:" + action.getCode()); + } + if (StringUtils.isBlank(action.getName()) || action.getName().length() > 80) + { + throw new ServiceException("业务动作名称不能为空且不能超过80个字符"); + } + if (StringUtils.isBlank(action.getOwnerTable()) || !tableNames.contains(action.getOwnerTable())) + { + throw new ServiceException("业务动作关联表不存在:" + StringUtils.defaultString(action.getOwnerTable())); + } + if (StringUtils.isNotBlank(action.getPath()) && !API_PATH.matcher(action.getPath()).matches()) + { + throw new ServiceException("业务动作接口路径不合法"); + } + validateRequestFields(action, columnNames); + validateRules(action); + validateEffects(action, tableNames, columnNames); + } + + private void validateRequestFields(BusinessActionDesign action, Set columnNames) + { + if (StringUtils.isEmpty(action.getRequestFields())) + { + return; + } + for (String field : action.getRequestFields()) + { + if (StringUtils.isBlank(field) || !columnNames.contains(field)) + { + throw new ServiceException("业务动作请求字段不存在:" + StringUtils.defaultString(field)); + } + } + } + + private void validateRules(BusinessActionDesign action) + { + if (StringUtils.isEmpty(action.getRules())) + { + return; + } + if (action.getRules().size() > MAX_RULES) + { + throw new ServiceException("业务规则数量超过限制"); + } + for (String rule : action.getRules()) + { + if (StringUtils.isBlank(rule) || rule.length() > 200) + { + throw new ServiceException("业务规则不能为空且不能超过200个字符"); + } + } + } + + private void validateEffects(BusinessActionDesign action, Set tableNames, Set columnNames) + { + if (StringUtils.isEmpty(action.getEffects())) + { + return; + } + if (action.getEffects().size() > MAX_EFFECTS) + { + throw new ServiceException("业务影响数量超过限制"); + } + for (BusinessActionEffectDesign effect : action.getEffects()) + { + if (effect == null) + { + throw new ServiceException("业务影响不能为空"); + } + if (StringUtils.isBlank(effect.getType()) || effect.getType().length() > 50) + { + throw new ServiceException("业务影响类型不能为空且不能超过50个字符"); + } + if (StringUtils.isNotBlank(effect.getTargetTable()) && !tableNames.contains(effect.getTargetTable())) + { + throw new ServiceException("业务影响目标表不存在:" + effect.getTargetTable()); + } + if (StringUtils.isNotBlank(effect.getTargetField()) && !columnNames.contains(effect.getTargetField())) + { + throw new ServiceException("业务影响目标字段不存在:" + effect.getTargetField()); + } + if (StringUtils.defaultString(effect.getValue()).length() > 200 + || StringUtils.defaultString(effect.getDescription()).length() > 200) + { + throw new ServiceException("业务影响描述不能超过200个字符"); + } + } + } + + private Set collectTableNames(List tables) + { + Set tableNames = new HashSet(); + if (tables == null) + { + return tableNames; + } + for (DatabaseTableDesign table : tables) + { + if (table != null && StringUtils.isNotBlank(table.getTableName())) + { + tableNames.add(table.getTableName().toLowerCase(Locale.ENGLISH)); + } + } + return tableNames; + } + + private Set collectColumnNames(List tables) + { + Set columnNames = new HashSet(); + if (tables == null) + { + return columnNames; + } + for (DatabaseTableDesign table : tables) + { + if (table == null || table.getColumns() == null) + { + continue; + } + for (DatabaseColumnDesign column : table.getColumns()) + { + if (column != null && StringUtils.isNotBlank(column.getColumnName())) + { + columnNames.add(column.getColumnName().toLowerCase(Locale.ENGLISH)); + } + } + } + return columnNames; + } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java index fea70c9..f0bcb56 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java @@ -18,6 +18,8 @@ import com.ruoyi.generator.domain.front.FrontProject; import com.ruoyi.generator.domain.front.FrontProjectColumn; import com.ruoyi.generator.domain.front.FrontProjectGeneration; import com.ruoyi.generator.domain.front.FrontProjectTable; +import com.ruoyi.generator.domain.front.dto.BusinessActionDesign; +import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign; import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign; import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse; import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign; @@ -38,6 +40,8 @@ public class AiGenerateServiceImpl implements IAiGenerateService @Autowired private AiDatabaseSchemaValidator validator; @Autowired + private AiBusinessBlueprintValidator businessBlueprintValidator; + @Autowired private MysqlDdlBuilder ddlBuilder; @Autowired private FrontProjectMapper frontProjectMapper; @@ -63,9 +67,11 @@ public class AiGenerateServiceImpl implements IAiGenerateService DatabaseDesignResponse response = parseResponse(aiContent); response.setProjectId(projectId); normalizeAndBuildSql(response); + normalizeBusinessActions(response); validator.validate(response, maxTables(), maxColumnsPerTable()); + businessBlueprintValidator.validate(response); persistDatabase(userId, projectId, response); - markDatabaseGenerated(projectId); + markDatabaseGenerated(projectId, response); generation.setResponsePayload(aiContent); generation.setSuccess("1"); generation.setElapsedMs(System.currentTimeMillis() - start); @@ -103,8 +109,9 @@ public class AiGenerateServiceImpl implements IAiGenerateService private String buildPrompt(FrontProject project, GenerateDatabaseRequest request) { StringBuilder prompt = new StringBuilder(); - prompt.append("请为企业管理系统生成MySQL数据库设计,只返回JSON,格式为{\"tables\":[{\"tableName\":\"car_info\",\"tableComment\":\"车辆信息表\",\"columns\":[]}]}。\n"); + prompt.append("请为企业管理系统生成MySQL数据库设计和业务蓝图草稿,只返回JSON,格式为{\"tables\":[{\"tableName\":\"car_info\",\"tableComment\":\"车辆信息表\",\"columns\":[]}],\"businessActions\":[{\"code\":\"borrow_book\",\"name\":\"借书\",\"ownerTable\":\"borrow_record\",\"method\":\"POST\",\"path\":\"/library/borrow/borrow\",\"transaction\":true,\"requestFields\":[\"book_id\"],\"rules\":[\"图书必须可借\"],\"effects\":[{\"type\":\"UPDATE_FIELD\",\"targetTable\":\"book_info\",\"targetField\":\"stock\",\"value\":\"stock - 1\",\"description\":\"库存减一\"}]}]}。\n"); prompt.append("限制:表名和字段名使用小写字母、数字、下划线;必须且只能有一个主键;字段类型只能使用bigint(20)、int(11)、varchar(n)、char(n)、decimal(p,s)、datetime、date、text、longtext。\n"); + prompt.append("业务蓝图限制:businessActions只描述业务动作草稿,不直接返回代码;ownerTable、requestFields、effects.targetTable、effects.targetField必须引用已生成的表和字段;method只用GET、POST、PUT、DELETE。\n"); prompt.append("项目名称:").append(firstNonBlank(request == null ? null : request.getProjectName(), project.getProjectName())).append("\n"); prompt.append("项目描述:").append(firstNonBlank(request == null ? null : request.getProjectDesc(), project.getProjectDesc())).append("\n"); prompt.append("行业模板:").append(firstNonBlank(request == null ? null : request.getIndustryTemplate(), project.getIndustryTemplate())).append("\n"); @@ -281,6 +288,147 @@ public class AiGenerateServiceImpl implements IAiGenerateService response.setSql(StringUtils.join(ddlList, "\n\n")); } + private void normalizeBusinessActions(DatabaseDesignResponse response) + { + if (response.getBusinessActions() == null) + { + response.setBusinessActions(new ArrayList()); + return; + } + Set actionCodes = new HashSet(); + int actionIndex = 1; + for (BusinessActionDesign action : response.getBusinessActions()) + { + if (action == null) + { + continue; + } + String code = uniqueBusinessCode(action.getCode(), actionIndex, actionCodes); + action.setCode(code); + action.setName(StringUtils.defaultIfEmpty(trimToLength(action.getName(), 80), code)); + action.setOwnerTable(normalizeDatabaseName(action.getOwnerTable(), "table_" + actionIndex)); + action.setMethod(normalizeHttpMethod(action.getMethod())); + action.setPath(normalizeBusinessPath(action.getPath(), code)); + action.setTransaction(action.getTransaction() == null ? Boolean.TRUE : action.getTransaction()); + action.setRequestFields(normalizeFieldList(action.getRequestFields())); + action.setRules(trimStringList(action.getRules(), 200)); + normalizeBusinessEffects(action); + actionIndex++; + } + } + + private String uniqueBusinessCode(String rawCode, int index, Set usedCodes) + { + String baseCode = normalizeDatabaseName(rawCode, "action_" + index); + String uniqueCode = baseCode; + int suffix = 2; + while (usedCodes.contains(uniqueCode)) + { + uniqueCode = appendNameSuffix(baseCode, "_" + suffix); + suffix++; + } + usedCodes.add(uniqueCode); + return uniqueCode; + } + + private String normalizeHttpMethod(String method) + { + String normalized = StringUtils.defaultString(method).trim().toUpperCase(Locale.ENGLISH); + if ("GET".equals(normalized) || "POST".equals(normalized) || "PUT".equals(normalized) || "DELETE".equals(normalized)) + { + return normalized; + } + return "POST"; + } + + private String normalizeBusinessPath(String path, String code) + { + String normalized = StringUtils.defaultString(path).trim(); + normalized = normalized.replaceAll("[^A-Za-z0-9_./-]+", "-").replaceAll("-+", "-"); + if (!normalized.startsWith("/")) + { + normalized = "/" + normalized; + } + normalized = normalized.replaceAll("/{2,}", "/"); + if (normalized.length() < 2) + { + normalized = "/business/" + code; + } + if (normalized.length() > 180) + { + normalized = normalized.substring(0, 180).replaceAll("[./_-]+$", ""); + } + return normalized; + } + + private List normalizeFieldList(List fields) + { + List normalizedFields = new ArrayList(); + if (fields == null) + { + return normalizedFields; + } + for (int i = 0; i < fields.size(); i++) + { + normalizedFields.add(normalizeDatabaseName(fields.get(i), "field_" + (i + 1))); + } + return normalizedFields; + } + + private List trimStringList(List values, int maxLength) + { + List normalizedValues = new ArrayList(); + if (values == null) + { + return normalizedValues; + } + for (String value : values) + { + String normalized = trimToLength(value, maxLength); + if (StringUtils.isNotBlank(normalized)) + { + normalizedValues.add(normalized); + } + } + return normalizedValues; + } + + private void normalizeBusinessEffects(BusinessActionDesign action) + { + if (action.getEffects() == null) + { + action.setEffects(new ArrayList()); + return; + } + int effectIndex = 1; + for (BusinessActionEffectDesign effect : action.getEffects()) + { + if (effect == null) + { + continue; + } + effect.setType(normalizeEffectType(effect.getType(), effectIndex)); + effect.setTargetTable(StringUtils.isBlank(effect.getTargetTable()) ? "" : normalizeDatabaseName(effect.getTargetTable(), "table_" + effectIndex)); + effect.setTargetField(StringUtils.isBlank(effect.getTargetField()) ? "" : normalizeDatabaseName(effect.getTargetField(), "field_" + effectIndex)); + effect.setValue(trimToLength(effect.getValue(), 200)); + effect.setDescription(trimToLength(effect.getDescription(), 200)); + effectIndex++; + } + } + + private String normalizeEffectType(String type, int index) + { + String normalized = StringUtils.defaultString(type).trim().toUpperCase(Locale.ENGLISH); + normalized = normalized.replaceAll("[^A-Z0-9]+", "_").replaceAll("_+", "_").replaceAll("^_+|_+$", ""); + return StringUtils.defaultIfEmpty(normalized, "EFFECT_" + index); + } + + private String trimToLength(String value, int maxLength) + { + String normalized = StringUtils.defaultString(value).trim(); + return normalized.length() <= maxLength ? normalized : normalized.substring(0, maxLength); + } + private void normalizeDatabaseNames(DatabaseDesignResponse response) { Set tableNames = new HashSet(); @@ -577,12 +725,13 @@ public class AiGenerateServiceImpl implements IAiGenerateService } } - private void markDatabaseGenerated(Long projectId) + private void markDatabaseGenerated(Long projectId, DatabaseDesignResponse response) { FrontProject project = new FrontProject(); project.setProjectId(projectId); project.setGenerateStatus("1"); project.setPreviewStatus("0"); + project.setBusinessBlueprint(JSON.toJSONString(response.getBusinessActions())); frontProjectMapper.updateFrontProject(project); } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectConverter.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectConverter.java index 2a8df4b..df02cb2 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectConverter.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectConverter.java @@ -2,12 +2,14 @@ package com.ruoyi.generator.service.front; import java.util.ArrayList; import java.util.List; +import com.alibaba.fastjson2.JSON; import com.ruoyi.generator.domain.GenProject; import com.ruoyi.generator.domain.GenTable; import com.ruoyi.generator.domain.GenTableColumn; import com.ruoyi.generator.domain.front.FrontProject; import com.ruoyi.generator.domain.front.FrontProjectColumn; import com.ruoyi.generator.domain.front.FrontProjectTable; +import com.ruoyi.generator.domain.front.dto.BusinessActionDesign; public class FrontProjectConverter { @@ -29,9 +31,27 @@ public class FrontProjectConverter genProject.setBackFramework(project.getBackFramework()); genProject.setStatus(project.getStatus()); genProject.setTables(toGenTables(project.getTables(), project.getPackageName())); + genProject.setBusinessActions(parseBusinessActions(project.getBusinessBlueprint())); return genProject; } + private static List parseBusinessActions(String businessBlueprint) + { + if (businessBlueprint == null || businessBlueprint.trim().length() == 0) + { + return new ArrayList(); + } + try + { + List actions = JSON.parseArray(businessBlueprint, BusinessActionDesign.class); + return actions == null ? new ArrayList() : actions; + } + catch (RuntimeException e) + { + return new ArrayList(); + } + } + private static List toGenTables(List tables, String packageName) { List genTables = new ArrayList(); diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java index ee592bd..eff2c9d 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -14,6 +15,7 @@ import com.ruoyi.common.utils.StringUtils; import com.ruoyi.generator.domain.front.FrontProject; import com.ruoyi.generator.domain.front.FrontProjectColumn; import com.ruoyi.generator.domain.front.FrontProjectTable; +import com.ruoyi.generator.domain.front.dto.BusinessActionDesign; import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign; import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse; import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign; @@ -149,6 +151,7 @@ public class FrontProjectServiceImpl implements IFrontProjectService response.setTables(tables); response.setSql(joinCreateTableSql(tables)); response.setErDiagram(readJson(project.getErDiagram())); + response.setBusinessActions(readBusinessActions(project.getBusinessBlueprint())); return response; } @@ -171,7 +174,7 @@ public class FrontProjectServiceImpl implements IFrontProjectService frontProjectColumnMapper.insertFrontProjectColumn(column); } } - markDatabaseGenerated(projectId, writeJson(request.getErDiagram())); + markDatabaseGenerated(projectId, writeJson(request.getErDiagram()), writeBusinessActions(request.getBusinessActions())); return getDatabase(userId, projectId); } @@ -293,13 +296,14 @@ public class FrontProjectServiceImpl implements IFrontProjectService return StringUtils.join(sqlList, "\n\n"); } - private void markDatabaseGenerated(Long projectId, String erDiagram) + private void markDatabaseGenerated(Long projectId, String erDiagram, String businessBlueprint) { FrontProject project = new FrontProject(); project.setProjectId(projectId); project.setGenerateStatus("1"); project.setPreviewStatus("0"); project.setErDiagram(erDiagram); + project.setBusinessBlueprint(businessBlueprint); frontProjectMapper.updateFrontProject(project); } @@ -335,6 +339,34 @@ public class FrontProjectServiceImpl implements IFrontProjectService } } + private List readBusinessActions(String json) + { + if (StringUtils.isEmpty(json)) + { + return new ArrayList(); + } + try + { + return OBJECT_MAPPER.readValue(json, new TypeReference>() { }); + } + catch (Exception e) + { + return new ArrayList(); + } + } + + private String writeBusinessActions(List actions) + { + try + { + return OBJECT_MAPPER.writeValueAsString(actions == null ? new ArrayList() : actions); + } + catch (Exception e) + { + throw new ServiceException("业务蓝图草稿保存失败"); + } + } + private FrontProjectTable toFrontProjectTable(Long projectId, Long userId, DatabaseTableDesign source) { FrontProjectTable table = new FrontProjectTable(); diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java index e5fa9e9..5f1cfa4 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java @@ -1,5 +1,6 @@ package com.ruoyi.generator.util; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -126,6 +127,10 @@ public class VelocityUtils { velocityContext.put("packageName", genProject.getPackageName()); velocityContext.put("projectName", genProject.getProjectName()); velocityContext.put("projectFileName", genProject.getProjectFileName()); + List businessActions = genProject.getBusinessActions() == null + ? Collections.emptyList() : genProject.getBusinessActions(); + velocityContext.put("businessActions", businessActions); + velocityContext.put("hasBusinessActions", StringUtils.isNotEmpty(businessActions)); StringBuffer createTableSql = new StringBuffer(); for (GenTable table : genProject.getTables()) { createTableSql.append("\n"); diff --git a/RuoYi-Vue/ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml b/RuoYi-Vue/ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml index 424be61..3050afd 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml +++ b/RuoYi-Vue/ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml @@ -18,6 +18,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -25,7 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - select project_id, user_id, project_name, project_file_name, package_name, version, project_desc, industry_template, front_framework, back_framework, generate_status, preview_status, er_diagram, status, create_time, update_time, remark from front_project + select project_id, user_id, project_name, project_file_name, package_name, version, project_desc, industry_template, front_framework, back_framework, generate_status, preview_status, er_diagram, business_blueprint, status, create_time, update_time, remark from front_project