Add AI business blueprint drafts
This commit is contained in:
@@ -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<BusinessActionDesign> 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.
|
||||||
|
|
||||||
@@ -81,6 +81,52 @@
|
|||||||
</template>
|
</template>
|
||||||
<el-empty v-else description="点击生成数据库后,这里会展示可编辑表结构" />
|
<el-empty v-else description="点击生成数据库后,这里会展示可编辑表结构" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="panel blueprint-panel">
|
||||||
|
<div class="panel-header database-header">
|
||||||
|
<h2 class="panel-title">业务蓝图草稿</h2>
|
||||||
|
<span class="muted">动作数量:{{ database.businessActions.length }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="database.businessActions.length" class="blueprint-list">
|
||||||
|
<article
|
||||||
|
v-for="action in database.businessActions"
|
||||||
|
:key="action.code || action.path"
|
||||||
|
class="blueprint-action"
|
||||||
|
>
|
||||||
|
<div class="blueprint-action-header">
|
||||||
|
<div>
|
||||||
|
<h3>{{ action.name || action.code }}</h3>
|
||||||
|
<p class="muted">{{ action.ownerTable || '未指定主表' }}</p>
|
||||||
|
</div>
|
||||||
|
<el-tag size="small" effect="plain">{{ action.method || 'POST' }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="blueprint-path">{{ action.path || '-' }}</div>
|
||||||
|
<div class="blueprint-meta">
|
||||||
|
<span v-if="action.transaction !== false">事务</span>
|
||||||
|
<span v-if="Array.isArray(action.requestFields) && action.requestFields.length">
|
||||||
|
入参:{{ action.requestFields.join('、') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="Array.isArray(action.rules) && action.rules.length" class="blueprint-section">
|
||||||
|
<strong>规则</strong>
|
||||||
|
<ul>
|
||||||
|
<li v-for="rule in action.rules" :key="rule">{{ rule }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div v-if="Array.isArray(action.effects) && action.effects.length" class="blueprint-section">
|
||||||
|
<strong>影响</strong>
|
||||||
|
<ul>
|
||||||
|
<li v-for="effect in action.effects" :key="`${effect.type}-${effect.targetTable}-${effect.targetField}`">
|
||||||
|
{{ effect.description || effect.type }}
|
||||||
|
<span v-if="effect.targetTable"> / {{ effect.targetTable }}</span>
|
||||||
|
<span v-if="effect.targetField">.{{ effect.targetField }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="生成数据库后,这里会展示 AI 识别出的业务动作草稿" />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -103,7 +149,7 @@ const loadingProject = ref(false)
|
|||||||
const databaseView = ref('editor')
|
const databaseView = ref('editor')
|
||||||
const projectId = ref(normalizeProjectId(route.query.projectId))
|
const projectId = ref(normalizeProjectId(route.query.projectId))
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const database = ref({ tables: [], sql: '', erDiagram: emptyErDiagram() })
|
const database = ref({ tables: [], sql: '', erDiagram: emptyErDiagram(), businessActions: [] })
|
||||||
const projectForm = reactive({
|
const projectForm = reactive({
|
||||||
projectName: '',
|
projectName: '',
|
||||||
keyword: String(route.query.keyword || ''),
|
keyword: String(route.query.keyword || ''),
|
||||||
@@ -139,7 +185,8 @@ function normalizeDatabase(payload) {
|
|||||||
return {
|
return {
|
||||||
tables: Array.isArray(data.tables) ? data.tables : [],
|
tables: Array.isArray(data.tables) ? data.tables : [],
|
||||||
sql: data.sql || '',
|
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;
|
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 {
|
.database-header {
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.ruoyi.generator.domain;
|
|||||||
|
|
||||||
import com.ruoyi.common.annotation.Excel;
|
import com.ruoyi.common.annotation.Excel;
|
||||||
import com.ruoyi.common.core.domain.BaseEntity;
|
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.ToStringBuilder;
|
||||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||||
|
|
||||||
@@ -57,6 +58,8 @@ public class GenProject extends BaseEntity
|
|||||||
|
|
||||||
private transient Boolean frontDraft;
|
private transient Boolean frontDraft;
|
||||||
|
|
||||||
|
private transient List<BusinessActionDesign> businessActions;
|
||||||
|
|
||||||
private List<GenTable> tables;
|
private List<GenTable> tables;
|
||||||
|
|
||||||
public void setProjectId(Long projectId)
|
public void setProjectId(Long projectId)
|
||||||
@@ -179,6 +182,14 @@ public class GenProject extends BaseEntity
|
|||||||
return Boolean.TRUE.equals(frontDraft);
|
return Boolean.TRUE.equals(frontDraft);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<BusinessActionDesign> getBusinessActions() {
|
||||||
|
return businessActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBusinessActions(List<BusinessActionDesign> businessActions) {
|
||||||
|
this.businessActions = businessActions;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
|
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public class FrontProject extends BaseEntity
|
|||||||
private String generateStatus;
|
private String generateStatus;
|
||||||
private String previewStatus;
|
private String previewStatus;
|
||||||
private String erDiagram;
|
private String erDiagram;
|
||||||
|
private String businessBlueprint;
|
||||||
private String status;
|
private String status;
|
||||||
private List<FrontProjectTable> tables;
|
private List<FrontProjectTable> tables;
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ public class FrontProject extends BaseEntity
|
|||||||
public void setPreviewStatus(String previewStatus) { this.previewStatus = previewStatus; }
|
public void setPreviewStatus(String previewStatus) { this.previewStatus = previewStatus; }
|
||||||
public String getErDiagram() { return erDiagram; }
|
public String getErDiagram() { return erDiagram; }
|
||||||
public void setErDiagram(String erDiagram) { this.erDiagram = 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 String getStatus() { return status; }
|
||||||
public void setStatus(String status) { this.status = status; }
|
public void setStatus(String status) { this.status = status; }
|
||||||
public List<FrontProjectTable> getTables() { return tables; }
|
public List<FrontProjectTable> getTables() { return tables; }
|
||||||
|
|||||||
@@ -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<String> requestFields;
|
||||||
|
private List<String> rules;
|
||||||
|
private List<BusinessActionEffectDesign> 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<String> getRequestFields() { return requestFields; }
|
||||||
|
public void setRequestFields(List<String> requestFields) { this.requestFields = requestFields; }
|
||||||
|
public List<String> getRules() { return rules; }
|
||||||
|
public void setRules(List<String> rules) { this.rules = rules; }
|
||||||
|
public List<BusinessActionEffectDesign> getEffects() { return effects; }
|
||||||
|
public void setEffects(List<BusinessActionEffectDesign> effects) { this.effects = effects; }
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ public class DatabaseDesignResponse
|
|||||||
{
|
{
|
||||||
private Long projectId;
|
private Long projectId;
|
||||||
private List<DatabaseTableDesign> tables;
|
private List<DatabaseTableDesign> tables;
|
||||||
|
private List<BusinessActionDesign> businessActions;
|
||||||
private String sql;
|
private String sql;
|
||||||
private Object erDiagram;
|
private Object erDiagram;
|
||||||
|
|
||||||
@@ -13,6 +14,8 @@ public class DatabaseDesignResponse
|
|||||||
public void setProjectId(Long projectId) { this.projectId = projectId; }
|
public void setProjectId(Long projectId) { this.projectId = projectId; }
|
||||||
public List<DatabaseTableDesign> getTables() { return tables; }
|
public List<DatabaseTableDesign> getTables() { return tables; }
|
||||||
public void setTables(List<DatabaseTableDesign> tables) { this.tables = tables; }
|
public void setTables(List<DatabaseTableDesign> tables) { this.tables = tables; }
|
||||||
|
public List<BusinessActionDesign> getBusinessActions() { return businessActions; }
|
||||||
|
public void setBusinessActions(List<BusinessActionDesign> businessActions) { this.businessActions = businessActions; }
|
||||||
public String getSql() { return sql; }
|
public String getSql() { return sql; }
|
||||||
public void setSql(String sql) { this.sql = sql; }
|
public void setSql(String sql) { this.sql = sql; }
|
||||||
public Object getErDiagram() { return erDiagram; }
|
public Object getErDiagram() { return erDiagram; }
|
||||||
|
|||||||
@@ -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<String> tableNames = collectTableNames(response.getTables());
|
||||||
|
Set<String> columnNames = collectColumnNames(response.getTables());
|
||||||
|
Set<String> actionCodes = new HashSet<String>();
|
||||||
|
for (BusinessActionDesign action : response.getBusinessActions())
|
||||||
|
{
|
||||||
|
validateAction(action, tableNames, columnNames, actionCodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateAction(BusinessActionDesign action, Set<String> tableNames, Set<String> columnNames, Set<String> 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<String> 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<String> tableNames, Set<String> 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<String> collectTableNames(List<DatabaseTableDesign> tables)
|
||||||
|
{
|
||||||
|
Set<String> tableNames = new HashSet<String>();
|
||||||
|
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<String> collectColumnNames(List<DatabaseTableDesign> tables)
|
||||||
|
{
|
||||||
|
Set<String> columnNames = new HashSet<String>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,8 @@ import com.ruoyi.generator.domain.front.FrontProject;
|
|||||||
import com.ruoyi.generator.domain.front.FrontProjectColumn;
|
import com.ruoyi.generator.domain.front.FrontProjectColumn;
|
||||||
import com.ruoyi.generator.domain.front.FrontProjectGeneration;
|
import com.ruoyi.generator.domain.front.FrontProjectGeneration;
|
||||||
import com.ruoyi.generator.domain.front.FrontProjectTable;
|
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.DatabaseColumnDesign;
|
||||||
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
||||||
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
|
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
|
||||||
@@ -38,6 +40,8 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
|||||||
@Autowired
|
@Autowired
|
||||||
private AiDatabaseSchemaValidator validator;
|
private AiDatabaseSchemaValidator validator;
|
||||||
@Autowired
|
@Autowired
|
||||||
|
private AiBusinessBlueprintValidator businessBlueprintValidator;
|
||||||
|
@Autowired
|
||||||
private MysqlDdlBuilder ddlBuilder;
|
private MysqlDdlBuilder ddlBuilder;
|
||||||
@Autowired
|
@Autowired
|
||||||
private FrontProjectMapper frontProjectMapper;
|
private FrontProjectMapper frontProjectMapper;
|
||||||
@@ -63,9 +67,11 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
|||||||
DatabaseDesignResponse response = parseResponse(aiContent);
|
DatabaseDesignResponse response = parseResponse(aiContent);
|
||||||
response.setProjectId(projectId);
|
response.setProjectId(projectId);
|
||||||
normalizeAndBuildSql(response);
|
normalizeAndBuildSql(response);
|
||||||
|
normalizeBusinessActions(response);
|
||||||
validator.validate(response, maxTables(), maxColumnsPerTable());
|
validator.validate(response, maxTables(), maxColumnsPerTable());
|
||||||
|
businessBlueprintValidator.validate(response);
|
||||||
persistDatabase(userId, projectId, response);
|
persistDatabase(userId, projectId, response);
|
||||||
markDatabaseGenerated(projectId);
|
markDatabaseGenerated(projectId, response);
|
||||||
generation.setResponsePayload(aiContent);
|
generation.setResponsePayload(aiContent);
|
||||||
generation.setSuccess("1");
|
generation.setSuccess("1");
|
||||||
generation.setElapsedMs(System.currentTimeMillis() - start);
|
generation.setElapsedMs(System.currentTimeMillis() - start);
|
||||||
@@ -103,8 +109,9 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
|||||||
private String buildPrompt(FrontProject project, GenerateDatabaseRequest request)
|
private String buildPrompt(FrontProject project, GenerateDatabaseRequest request)
|
||||||
{
|
{
|
||||||
StringBuilder prompt = new StringBuilder();
|
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("限制:表名和字段名使用小写字母、数字、下划线;必须且只能有一个主键;字段类型只能使用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.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.getProjectDesc(), project.getProjectDesc())).append("\n");
|
||||||
prompt.append("行业模板:").append(firstNonBlank(request == null ? null : request.getIndustryTemplate(), project.getIndustryTemplate())).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"));
|
response.setSql(StringUtils.join(ddlList, "\n\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void normalizeBusinessActions(DatabaseDesignResponse response)
|
||||||
|
{
|
||||||
|
if (response.getBusinessActions() == null)
|
||||||
|
{
|
||||||
|
response.setBusinessActions(new ArrayList<BusinessActionDesign>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<String> actionCodes = new HashSet<String>();
|
||||||
|
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<String> 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<String> normalizeFieldList(List<String> fields)
|
||||||
|
{
|
||||||
|
List<String> normalizedFields = new ArrayList<String>();
|
||||||
|
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<String> trimStringList(List<String> values, int maxLength)
|
||||||
|
{
|
||||||
|
List<String> normalizedValues = new ArrayList<String>();
|
||||||
|
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<BusinessActionEffectDesign>());
|
||||||
|
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)
|
private void normalizeDatabaseNames(DatabaseDesignResponse response)
|
||||||
{
|
{
|
||||||
Set<String> tableNames = new HashSet<String>();
|
Set<String> tableNames = new HashSet<String>();
|
||||||
@@ -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();
|
FrontProject project = new FrontProject();
|
||||||
project.setProjectId(projectId);
|
project.setProjectId(projectId);
|
||||||
project.setGenerateStatus("1");
|
project.setGenerateStatus("1");
|
||||||
project.setPreviewStatus("0");
|
project.setPreviewStatus("0");
|
||||||
|
project.setBusinessBlueprint(JSON.toJSONString(response.getBusinessActions()));
|
||||||
frontProjectMapper.updateFrontProject(project);
|
frontProjectMapper.updateFrontProject(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package com.ruoyi.generator.service.front;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.ruoyi.generator.domain.GenProject;
|
import com.ruoyi.generator.domain.GenProject;
|
||||||
import com.ruoyi.generator.domain.GenTable;
|
import com.ruoyi.generator.domain.GenTable;
|
||||||
import com.ruoyi.generator.domain.GenTableColumn;
|
import com.ruoyi.generator.domain.GenTableColumn;
|
||||||
import com.ruoyi.generator.domain.front.FrontProject;
|
import com.ruoyi.generator.domain.front.FrontProject;
|
||||||
import com.ruoyi.generator.domain.front.FrontProjectColumn;
|
import com.ruoyi.generator.domain.front.FrontProjectColumn;
|
||||||
import com.ruoyi.generator.domain.front.FrontProjectTable;
|
import com.ruoyi.generator.domain.front.FrontProjectTable;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
|
||||||
|
|
||||||
public class FrontProjectConverter
|
public class FrontProjectConverter
|
||||||
{
|
{
|
||||||
@@ -29,9 +31,27 @@ public class FrontProjectConverter
|
|||||||
genProject.setBackFramework(project.getBackFramework());
|
genProject.setBackFramework(project.getBackFramework());
|
||||||
genProject.setStatus(project.getStatus());
|
genProject.setStatus(project.getStatus());
|
||||||
genProject.setTables(toGenTables(project.getTables(), project.getPackageName()));
|
genProject.setTables(toGenTables(project.getTables(), project.getPackageName()));
|
||||||
|
genProject.setBusinessActions(parseBusinessActions(project.getBusinessBlueprint()));
|
||||||
return genProject;
|
return genProject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<BusinessActionDesign> parseBusinessActions(String businessBlueprint)
|
||||||
|
{
|
||||||
|
if (businessBlueprint == null || businessBlueprint.trim().length() == 0)
|
||||||
|
{
|
||||||
|
return new ArrayList<BusinessActionDesign>();
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<BusinessActionDesign> actions = JSON.parseArray(businessBlueprint, BusinessActionDesign.class);
|
||||||
|
return actions == null ? new ArrayList<BusinessActionDesign>() : actions;
|
||||||
|
}
|
||||||
|
catch (RuntimeException e)
|
||||||
|
{
|
||||||
|
return new ArrayList<BusinessActionDesign>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static List<GenTable> toGenTables(List<FrontProjectTable> tables, String packageName)
|
private static List<GenTable> toGenTables(List<FrontProjectTable> tables, String packageName)
|
||||||
{
|
{
|
||||||
List<GenTable> genTables = new ArrayList<GenTable>();
|
List<GenTable> genTables = new ArrayList<GenTable>();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
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.FrontProject;
|
||||||
import com.ruoyi.generator.domain.front.FrontProjectColumn;
|
import com.ruoyi.generator.domain.front.FrontProjectColumn;
|
||||||
import com.ruoyi.generator.domain.front.FrontProjectTable;
|
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.DatabaseColumnDesign;
|
||||||
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
||||||
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
|
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
|
||||||
@@ -149,6 +151,7 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
|||||||
response.setTables(tables);
|
response.setTables(tables);
|
||||||
response.setSql(joinCreateTableSql(tables));
|
response.setSql(joinCreateTableSql(tables));
|
||||||
response.setErDiagram(readJson(project.getErDiagram()));
|
response.setErDiagram(readJson(project.getErDiagram()));
|
||||||
|
response.setBusinessActions(readBusinessActions(project.getBusinessBlueprint()));
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +174,7 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
|||||||
frontProjectColumnMapper.insertFrontProjectColumn(column);
|
frontProjectColumnMapper.insertFrontProjectColumn(column);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
markDatabaseGenerated(projectId, writeJson(request.getErDiagram()));
|
markDatabaseGenerated(projectId, writeJson(request.getErDiagram()), writeBusinessActions(request.getBusinessActions()));
|
||||||
return getDatabase(userId, projectId);
|
return getDatabase(userId, projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,13 +296,14 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
|||||||
return StringUtils.join(sqlList, "\n\n");
|
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();
|
FrontProject project = new FrontProject();
|
||||||
project.setProjectId(projectId);
|
project.setProjectId(projectId);
|
||||||
project.setGenerateStatus("1");
|
project.setGenerateStatus("1");
|
||||||
project.setPreviewStatus("0");
|
project.setPreviewStatus("0");
|
||||||
project.setErDiagram(erDiagram);
|
project.setErDiagram(erDiagram);
|
||||||
|
project.setBusinessBlueprint(businessBlueprint);
|
||||||
frontProjectMapper.updateFrontProject(project);
|
frontProjectMapper.updateFrontProject(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,6 +339,34 @@ public class FrontProjectServiceImpl implements IFrontProjectService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<BusinessActionDesign> readBusinessActions(String json)
|
||||||
|
{
|
||||||
|
if (StringUtils.isEmpty(json))
|
||||||
|
{
|
||||||
|
return new ArrayList<BusinessActionDesign>();
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return OBJECT_MAPPER.readValue(json, new TypeReference<List<BusinessActionDesign>>() { });
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return new ArrayList<BusinessActionDesign>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String writeBusinessActions(List<BusinessActionDesign> actions)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return OBJECT_MAPPER.writeValueAsString(actions == null ? new ArrayList<BusinessActionDesign>() : actions);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new ServiceException("业务蓝图草稿保存失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private FrontProjectTable toFrontProjectTable(Long projectId, Long userId, DatabaseTableDesign source)
|
private FrontProjectTable toFrontProjectTable(Long projectId, Long userId, DatabaseTableDesign source)
|
||||||
{
|
{
|
||||||
FrontProjectTable table = new FrontProjectTable();
|
FrontProjectTable table = new FrontProjectTable();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.ruoyi.generator.util;
|
package com.ruoyi.generator.util;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -126,6 +127,10 @@ public class VelocityUtils {
|
|||||||
velocityContext.put("packageName", genProject.getPackageName());
|
velocityContext.put("packageName", genProject.getPackageName());
|
||||||
velocityContext.put("projectName", genProject.getProjectName());
|
velocityContext.put("projectName", genProject.getProjectName());
|
||||||
velocityContext.put("projectFileName", genProject.getProjectFileName());
|
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();
|
StringBuffer createTableSql = new StringBuffer();
|
||||||
for (GenTable table : genProject.getTables()) {
|
for (GenTable table : genProject.getTables()) {
|
||||||
createTableSql.append("\n");
|
createTableSql.append("\n");
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<result property="generateStatus" column="generate_status" />
|
<result property="generateStatus" column="generate_status" />
|
||||||
<result property="previewStatus" column="preview_status" />
|
<result property="previewStatus" column="preview_status" />
|
||||||
<result property="erDiagram" column="er_diagram" />
|
<result property="erDiagram" column="er_diagram" />
|
||||||
|
<result property="businessBlueprint" column="business_blueprint" />
|
||||||
<result property="status" column="status" />
|
<result property="status" column="status" />
|
||||||
<result property="createTime" column="create_time" />
|
<result property="createTime" column="create_time" />
|
||||||
<result property="updateTime" column="update_time" />
|
<result property="updateTime" column="update_time" />
|
||||||
@@ -25,7 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="selectFrontProjectVo">
|
<sql id="selectFrontProjectVo">
|
||||||
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
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<select id="selectFrontProjectById" parameterType="Long" resultMap="FrontProjectResult">
|
<select id="selectFrontProjectById" parameterType="Long" resultMap="FrontProjectResult">
|
||||||
@@ -66,6 +67,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="generateStatus != null">generate_status,</if>
|
<if test="generateStatus != null">generate_status,</if>
|
||||||
<if test="previewStatus != null">preview_status,</if>
|
<if test="previewStatus != null">preview_status,</if>
|
||||||
<if test="erDiagram != null">er_diagram,</if>
|
<if test="erDiagram != null">er_diagram,</if>
|
||||||
|
<if test="businessBlueprint != null">business_blueprint,</if>
|
||||||
<if test="status != null">status,</if>
|
<if test="status != null">status,</if>
|
||||||
create_time,
|
create_time,
|
||||||
<if test="remark != null">remark,</if>
|
<if test="remark != null">remark,</if>
|
||||||
@@ -83,6 +85,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="generateStatus != null">#{generateStatus},</if>
|
<if test="generateStatus != null">#{generateStatus},</if>
|
||||||
<if test="previewStatus != null">#{previewStatus},</if>
|
<if test="previewStatus != null">#{previewStatus},</if>
|
||||||
<if test="erDiagram != null">#{erDiagram},</if>
|
<if test="erDiagram != null">#{erDiagram},</if>
|
||||||
|
<if test="businessBlueprint != null">#{businessBlueprint},</if>
|
||||||
<if test="status != null">#{status},</if>
|
<if test="status != null">#{status},</if>
|
||||||
sysdate(),
|
sysdate(),
|
||||||
<if test="remark != null">#{remark},</if>
|
<if test="remark != null">#{remark},</if>
|
||||||
@@ -103,6 +106,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<if test="generateStatus != null">generate_status = #{generateStatus},</if>
|
<if test="generateStatus != null">generate_status = #{generateStatus},</if>
|
||||||
<if test="previewStatus != null">preview_status = #{previewStatus},</if>
|
<if test="previewStatus != null">preview_status = #{previewStatus},</if>
|
||||||
<if test="erDiagram != null">er_diagram = #{erDiagram},</if>
|
<if test="erDiagram != null">er_diagram = #{erDiagram},</if>
|
||||||
|
<if test="businessBlueprint != null">business_blueprint = #{businessBlueprint},</if>
|
||||||
<if test="status != null">status = #{status},</if>
|
<if test="status != null">status = #{status},</if>
|
||||||
update_time = sysdate(),
|
update_time = sysdate(),
|
||||||
<if test="remark != null">remark = #{remark},</if>
|
<if test="remark != null">remark = #{remark},</if>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import static org.mockito.Mockito.when;
|
|||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -21,11 +22,14 @@ import org.mockito.Mock;
|
|||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.springframework.transaction.annotation.Propagation;
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import com.ruoyi.generator.config.DeepSeekProperties;
|
import com.ruoyi.generator.config.DeepSeekProperties;
|
||||||
import com.ruoyi.generator.domain.front.FrontProject;
|
import com.ruoyi.generator.domain.front.FrontProject;
|
||||||
import com.ruoyi.generator.domain.front.FrontProjectColumn;
|
import com.ruoyi.generator.domain.front.FrontProjectColumn;
|
||||||
import com.ruoyi.generator.domain.front.FrontProjectGeneration;
|
import com.ruoyi.generator.domain.front.FrontProjectGeneration;
|
||||||
import com.ruoyi.generator.domain.front.FrontProjectTable;
|
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.DatabaseDesignResponse;
|
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
||||||
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
|
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
|
||||||
import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
|
import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
|
||||||
@@ -62,6 +66,7 @@ public class AiGenerateServiceImplTest
|
|||||||
setField("frontProjectColumnMapper", frontProjectColumnMapper);
|
setField("frontProjectColumnMapper", frontProjectColumnMapper);
|
||||||
setField("generationRecordService", generationRecordService);
|
setField("generationRecordService", generationRecordService);
|
||||||
setField("validator", new AiDatabaseSchemaValidator());
|
setField("validator", new AiDatabaseSchemaValidator());
|
||||||
|
setField("businessBlueprintValidator", new AiBusinessBlueprintValidator());
|
||||||
setField("ddlBuilder", new MysqlDdlBuilder());
|
setField("ddlBuilder", new MysqlDdlBuilder());
|
||||||
setField("deepSeekProperties", new DeepSeekProperties());
|
setField("deepSeekProperties", new DeepSeekProperties());
|
||||||
}
|
}
|
||||||
@@ -86,6 +91,9 @@ public class AiGenerateServiceImplTest
|
|||||||
assertEquals(2, columnCaptor.getAllValues().size());
|
assertEquals(2, columnCaptor.getAllValues().size());
|
||||||
assertEquals("1", generationCaptor.getValue().getSuccess());
|
assertEquals("1", generationCaptor.getValue().getSuccess());
|
||||||
assertTrue(response.getSql().contains("CREATE TABLE `car_info`"));
|
assertTrue(response.getSql().contains("CREATE TABLE `car_info`"));
|
||||||
|
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
verify(deepSeekClient).chat(promptCaptor.capture());
|
||||||
|
assertTrue(promptCaptor.getValue().contains("businessActions"));
|
||||||
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
|
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
|
||||||
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
|
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
|
||||||
assertEquals(Long.valueOf(10L), projectCaptor.getValue().getProjectId());
|
assertEquals(Long.valueOf(10L), projectCaptor.getValue().getProjectId());
|
||||||
@@ -202,6 +210,55 @@ public class AiGenerateServiceImplTest
|
|||||||
verify(frontProjectTableMapper, never()).deleteTablesByProjectId(10L);
|
verify(frontProjectTableMapper, never()).deleteTablesByProjectId(10L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void generateDatabaseParsesNormalizesAndPersistsBusinessActionDraft()
|
||||||
|
{
|
||||||
|
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||||
|
when(deepSeekClient.chat(anyString())).thenReturn(responseWithBusinessActions());
|
||||||
|
|
||||||
|
DatabaseDesignResponse response = service.generateDatabase(7L, 10L, request());
|
||||||
|
|
||||||
|
List<BusinessActionDesign> actions = response.getBusinessActions();
|
||||||
|
assertEquals(1, actions.size());
|
||||||
|
BusinessActionDesign action = actions.get(0);
|
||||||
|
assertEquals("borrow_book", action.getCode());
|
||||||
|
assertEquals("借书", action.getName());
|
||||||
|
assertEquals("book_borrow", action.getOwnerTable());
|
||||||
|
assertEquals("POST", action.getMethod());
|
||||||
|
assertEquals("/library/borrow/borrow", action.getPath());
|
||||||
|
assertEquals(Boolean.TRUE, action.getTransaction());
|
||||||
|
assertEquals("book_id", action.getRequestFields().get(0));
|
||||||
|
assertEquals("读者状态必须正常", action.getRules().get(0));
|
||||||
|
BusinessActionEffectDesign effect = action.getEffects().get(0);
|
||||||
|
assertEquals("UPDATE_FIELD", effect.getType());
|
||||||
|
assertEquals("book_info", effect.getTargetTable());
|
||||||
|
assertEquals("stock", effect.getTargetField());
|
||||||
|
|
||||||
|
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
|
||||||
|
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
|
||||||
|
assertTrue(projectCaptor.getValue().getBusinessBlueprint().contains("borrow_book"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void generateDatabaseRejectsBusinessActionReferencingMissingTable()
|
||||||
|
{
|
||||||
|
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||||
|
when(deepSeekClient.chat(anyString())).thenReturn(responseWithMissingBusinessActionTable());
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
service.generateDatabase(7L, 10L, request());
|
||||||
|
fail("Expected invalid business action table");
|
||||||
|
}
|
||||||
|
catch (ServiceException e)
|
||||||
|
{
|
||||||
|
assertEquals("业务动作关联表不存在:missing_table", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(frontProjectColumnMapper, never()).deleteColumnsByProjectId(10L);
|
||||||
|
verify(frontProjectTableMapper, never()).deleteTablesByProjectId(10L);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void generationRecordInsertUsesRequiresNewTransaction() throws Exception
|
public void generationRecordInsertUsesRequiresNewTransaction() throws Exception
|
||||||
{
|
{
|
||||||
@@ -418,4 +475,55 @@ public class AiGenerateServiceImplTest
|
|||||||
+ " ]\n"
|
+ " ]\n"
|
||||||
+ "}";
|
+ "}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String responseWithBusinessActions()
|
||||||
|
{
|
||||||
|
return "{\n"
|
||||||
|
+ " \"tables\": [\n"
|
||||||
|
+ " {\"tableName\": \"book_info\", \"tableComment\": \"图书信息表\", \"columns\": [\n"
|
||||||
|
+ " {\"columnName\": \"book_id\", \"columnType\": \"bigint(20)\", \"primaryKey\": true, \"autoIncrement\": true, \"columnComment\": \"图书ID\"},\n"
|
||||||
|
+ " {\"columnName\": \"stock\", \"columnType\": \"int(11)\", \"columnComment\": \"库存\"}\n"
|
||||||
|
+ " ]},\n"
|
||||||
|
+ " {\"tableName\": \"reader_info\", \"tableComment\": \"读者信息表\", \"columns\": [\n"
|
||||||
|
+ " {\"columnName\": \"reader_id\", \"columnType\": \"bigint(20)\", \"primaryKey\": true, \"autoIncrement\": true, \"columnComment\": \"读者ID\"},\n"
|
||||||
|
+ " {\"columnName\": \"status\", \"columnType\": \"varchar(20)\", \"columnComment\": \"读者状态\"}\n"
|
||||||
|
+ " ]},\n"
|
||||||
|
+ " {\"tableName\": \"book_borrow\", \"tableComment\": \"借阅记录表\", \"columns\": [\n"
|
||||||
|
+ " {\"columnName\": \"borrow_id\", \"columnType\": \"bigint(20)\", \"primaryKey\": true, \"autoIncrement\": true, \"columnComment\": \"借阅ID\"},\n"
|
||||||
|
+ " {\"columnName\": \"book_id\", \"columnType\": \"bigint(20)\", \"columnComment\": \"图书ID\"},\n"
|
||||||
|
+ " {\"columnName\": \"reader_id\", \"columnType\": \"bigint(20)\", \"columnComment\": \"读者ID\"},\n"
|
||||||
|
+ " {\"columnName\": \"borrow_status\", \"columnType\": \"varchar(20)\", \"columnComment\": \"借阅状态\"}\n"
|
||||||
|
+ " ]}\n"
|
||||||
|
+ " ],\n"
|
||||||
|
+ " \"businessActions\": [\n"
|
||||||
|
+ " {\n"
|
||||||
|
+ " \"code\": \"Borrow Book\",\n"
|
||||||
|
+ " \"name\": \"借书\",\n"
|
||||||
|
+ " \"ownerTable\": \"book_borrow\",\n"
|
||||||
|
+ " \"method\": \"post\",\n"
|
||||||
|
+ " \"path\": \"/library/borrow/borrow\",\n"
|
||||||
|
+ " \"transaction\": true,\n"
|
||||||
|
+ " \"requestFields\": [\"book_id\", \"reader_id\"],\n"
|
||||||
|
+ " \"rules\": [\"读者状态必须正常\", \"图书库存必须大于0\"],\n"
|
||||||
|
+ " \"effects\": [\n"
|
||||||
|
+ " {\"type\": \"update_field\", \"targetTable\": \"book_info\", \"targetField\": \"stock\", \"value\": \"stock - 1\", \"description\": \"图书库存减一\"}\n"
|
||||||
|
+ " ]\n"
|
||||||
|
+ " }\n"
|
||||||
|
+ " ]\n"
|
||||||
|
+ "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String responseWithMissingBusinessActionTable()
|
||||||
|
{
|
||||||
|
return "{\n"
|
||||||
|
+ " \"tables\": [\n"
|
||||||
|
+ " {\"tableName\": \"book_info\", \"tableComment\": \"图书信息表\", \"columns\": [\n"
|
||||||
|
+ " {\"columnName\": \"book_id\", \"columnType\": \"bigint(20)\", \"primaryKey\": true, \"autoIncrement\": true, \"columnComment\": \"图书ID\"}\n"
|
||||||
|
+ " ]}\n"
|
||||||
|
+ " ],\n"
|
||||||
|
+ " \"businessActions\": [\n"
|
||||||
|
+ " {\"code\": \"borrow\", \"name\": \"借书\", \"ownerTable\": \"missing_table\", \"method\": \"POST\", \"path\": \"/library/borrow\", \"requestFields\": [\"book_id\"]}\n"
|
||||||
|
+ " ]\n"
|
||||||
|
+ "}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ import com.ruoyi.generator.domain.front.FrontProject;
|
|||||||
import com.ruoyi.generator.domain.front.FrontProjectColumn;
|
import com.ruoyi.generator.domain.front.FrontProjectColumn;
|
||||||
import com.ruoyi.generator.domain.front.FrontProjectTable;
|
import com.ruoyi.generator.domain.front.FrontProjectTable;
|
||||||
import com.ruoyi.generator.service.IGenProjectService;
|
import com.ruoyi.generator.service.IGenProjectService;
|
||||||
|
import com.ruoyi.generator.util.VelocityUtils;
|
||||||
|
import org.apache.velocity.VelocityContext;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
|
|
||||||
@@ -25,10 +28,12 @@ import java.util.zip.ZipInputStream;
|
|||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
public class FrontProjectPreviewServiceImplTest
|
public class FrontProjectPreviewServiceImplTest
|
||||||
{
|
{
|
||||||
@@ -60,6 +65,40 @@ public class FrontProjectPreviewServiceImplTest
|
|||||||
assertEquals("backend", structure.get(0).get("label"));
|
assertEquals("backend", structure.get(0).get("label"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void passesBusinessActionsIntoGenerationContext()
|
||||||
|
{
|
||||||
|
FrontProject project = frontProjectWithOneTable();
|
||||||
|
project.setBusinessBlueprint("[{\"code\":\"borrow_book\",\"name\":\"借书\",\"ownerTable\":\"book_borrow\",\"method\":\"POST\",\"path\":\"/library/borrow/borrow\"}]");
|
||||||
|
when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
|
||||||
|
when(genProjectService.getProjectStructure(any(GenProject.class), eq("backend")))
|
||||||
|
.thenReturn(Collections.<Map<String, Object>>emptyList());
|
||||||
|
|
||||||
|
previewService.getStructure(100L, 200L, "backend");
|
||||||
|
|
||||||
|
ArgumentCaptor<GenProject> projectCaptor = ArgumentCaptor.forClass(GenProject.class);
|
||||||
|
verify(genProjectService).getProjectStructure(projectCaptor.capture(), eq("backend"));
|
||||||
|
GenProject genProject = projectCaptor.getValue();
|
||||||
|
assertEquals("borrow_book", genProject.getBusinessActions().get(0).getCode());
|
||||||
|
VelocityContext context = VelocityUtils.prepareContextProject(genProject);
|
||||||
|
assertEquals(genProject.getBusinessActions(), context.get("businessActions"));
|
||||||
|
assertEquals(Boolean.TRUE, context.get("hasBusinessActions"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void projectVelocityContextMarksEmptyBusinessActions()
|
||||||
|
{
|
||||||
|
GenProject genProject = new GenProject();
|
||||||
|
genProject.setProjectName("Demo");
|
||||||
|
genProject.setProjectFileName("demo");
|
||||||
|
genProject.setPackageName("com.example.demo");
|
||||||
|
genProject.setTables(Collections.emptyList());
|
||||||
|
|
||||||
|
VelocityContext context = VelocityUtils.prepareContextProject(genProject);
|
||||||
|
|
||||||
|
assertFalse((Boolean) context.get("hasBusinessActions"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void downloadAllPackagesBackendFrontendAndAdminFrontend() throws Exception
|
public void downloadAllPackagesBackendFrontendAndAdminFrontend() throws Exception
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import static org.mockito.Mockito.when;
|
|||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -18,6 +19,7 @@ import org.mockito.Mock;
|
|||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import com.ruoyi.generator.domain.front.FrontProject;
|
import com.ruoyi.generator.domain.front.FrontProject;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
|
||||||
import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
|
import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
|
||||||
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
|
||||||
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
|
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
|
||||||
@@ -158,6 +160,7 @@ public class FrontProjectServiceImplTest
|
|||||||
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.emptyList());
|
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.emptyList());
|
||||||
DatabaseDesignResponse request = database(table("sys_user", pkColumn(), column("user_name", "varchar(64)", "0")));
|
DatabaseDesignResponse request = database(table("sys_user", pkColumn(), column("user_name", "varchar(64)", "0")));
|
||||||
request.setErDiagram(Collections.singletonMap("relations", Collections.singletonList("student_department")));
|
request.setErDiagram(Collections.singletonMap("relations", Collections.singletonList("student_department")));
|
||||||
|
request.setBusinessActions(Collections.singletonList(businessAction()));
|
||||||
|
|
||||||
service.saveDatabase(7L, 10L, request);
|
service.saveDatabase(7L, 10L, request);
|
||||||
|
|
||||||
@@ -174,6 +177,7 @@ public class FrontProjectServiceImplTest
|
|||||||
assertEquals("1", projectCaptor.getValue().getGenerateStatus());
|
assertEquals("1", projectCaptor.getValue().getGenerateStatus());
|
||||||
assertEquals("0", projectCaptor.getValue().getPreviewStatus());
|
assertEquals("0", projectCaptor.getValue().getPreviewStatus());
|
||||||
assertTrue(projectCaptor.getValue().getErDiagram().contains("student_department"));
|
assertTrue(projectCaptor.getValue().getErDiagram().contains("student_department"));
|
||||||
|
assertTrue(projectCaptor.getValue().getBusinessBlueprint().contains("borrow_book"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -191,6 +195,22 @@ public class FrontProjectServiceImplTest
|
|||||||
assertTrue(erDiagram.containsKey("positions"));
|
assertTrue(erDiagram.containsKey("positions"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getDatabaseReturnsBusinessActionDraft()
|
||||||
|
{
|
||||||
|
FrontProject project = project();
|
||||||
|
project.setBusinessBlueprint("[{\"code\":\"borrow_book\",\"name\":\"借书\",\"ownerTable\":\"book_borrow\",\"method\":\"POST\",\"path\":\"/library/borrow/borrow\"}]");
|
||||||
|
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project);
|
||||||
|
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
DatabaseDesignResponse response = service.getDatabase(7L, 10L);
|
||||||
|
|
||||||
|
List<BusinessActionDesign> actions = response.getBusinessActions();
|
||||||
|
assertEquals(1, actions.size());
|
||||||
|
assertEquals("borrow_book", actions.get(0).getCode());
|
||||||
|
assertEquals("借书", actions.get(0).getName());
|
||||||
|
}
|
||||||
|
|
||||||
private void setField(String name, Object value) throws Exception
|
private void setField(String name, Object value) throws Exception
|
||||||
{
|
{
|
||||||
Field field = FrontProjectServiceImpl.class.getDeclaredField(name);
|
Field field = FrontProjectServiceImpl.class.getDeclaredField(name);
|
||||||
@@ -236,6 +256,20 @@ public class FrontProjectServiceImplTest
|
|||||||
return column;
|
return column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BusinessActionDesign businessAction()
|
||||||
|
{
|
||||||
|
BusinessActionDesign action = new BusinessActionDesign();
|
||||||
|
action.setCode("borrow_book");
|
||||||
|
action.setName("借书");
|
||||||
|
action.setOwnerTable("sys_user");
|
||||||
|
action.setMethod("POST");
|
||||||
|
action.setPath("/library/borrow/borrow");
|
||||||
|
action.setTransaction(Boolean.TRUE);
|
||||||
|
action.setRequestFields(Collections.singletonList("id"));
|
||||||
|
action.setRules(Collections.singletonList("用户状态正常"));
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
private ServiceException expectServiceException(ThrowingRunnable runnable)
|
private ServiceException expectServiceException(ThrowingRunnable runnable)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -1300,6 +1300,7 @@ create table front_project (
|
|||||||
generate_status char(1) default '0' comment '数据库生成状态(0未生成 1生成中 2已生成 3失败)',
|
generate_status char(1) default '0' comment '数据库生成状态(0未生成 1生成中 2已生成 3失败)',
|
||||||
preview_status char(1) default '0' comment '预览状态(0未预览 1已预览 2失败)',
|
preview_status char(1) default '0' comment '预览状态(0未预览 1已预览 2失败)',
|
||||||
er_diagram longtext comment 'ER图草稿JSON',
|
er_diagram longtext comment 'ER图草稿JSON',
|
||||||
|
business_blueprint longtext comment '业务蓝图草稿JSON',
|
||||||
status char(1) default '0' comment '状态(0正常 1停用)',
|
status char(1) default '0' comment '状态(0正常 1停用)',
|
||||||
create_time datetime default null comment '创建时间',
|
create_time datetime default null comment '创建时间',
|
||||||
update_time datetime default null comment '更新时间',
|
update_time datetime default null comment '更新时间',
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ create table front_project (
|
|||||||
generate_status char(1) default '0' comment '数据库生成状态(0未生成 1生成中 2已生成 3失败)',
|
generate_status char(1) default '0' comment '数据库生成状态(0未生成 1生成中 2已生成 3失败)',
|
||||||
preview_status char(1) default '0' comment '预览状态(0未预览 1已预览 2失败)',
|
preview_status char(1) default '0' comment '预览状态(0未预览 1已预览 2失败)',
|
||||||
er_diagram longtext comment 'ER图草稿JSON',
|
er_diagram longtext comment 'ER图草稿JSON',
|
||||||
|
business_blueprint longtext comment '业务蓝图草稿JSON',
|
||||||
status char(1) default '0' comment '状态(0正常 1停用)',
|
status char(1) default '0' comment '状态(0正常 1停用)',
|
||||||
create_time datetime default null comment '创建时间',
|
create_time datetime default null comment '创建时间',
|
||||||
update_time datetime default null comment '更新时间',
|
update_time datetime default null comment '更新时间',
|
||||||
|
|||||||
Reference in New Issue
Block a user