Add AI business blueprint drafts

This commit is contained in:
王鹏
2026-05-23 12:00:28 +08:00
parent 2c8b74ea8a
commit 37358dc779
18 changed files with 918 additions and 8 deletions

View File

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

View File

@@ -81,6 +81,52 @@
</template>
<el-empty v-else description="点击生成数据库后,这里会展示可编辑表结构" />
</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>
</template>
@@ -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;
}

View File

@@ -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<BusinessActionDesign> businessActions;
private List<GenTable> tables;
public void setProjectId(Long projectId)
@@ -179,6 +182,14 @@ public class GenProject extends BaseEntity
return Boolean.TRUE.equals(frontDraft);
}
public List<BusinessActionDesign> getBusinessActions() {
return businessActions;
}
public void setBusinessActions(List<BusinessActionDesign> businessActions) {
this.businessActions = businessActions;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)

View File

@@ -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<FrontProjectTable> 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<FrontProjectTable> getTables() { return tables; }

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -6,6 +6,7 @@ public class DatabaseDesignResponse
{
private Long projectId;
private List<DatabaseTableDesign> tables;
private List<BusinessActionDesign> businessActions;
private String sql;
private Object erDiagram;
@@ -13,6 +14,8 @@ public class DatabaseDesignResponse
public void setProjectId(Long projectId) { this.projectId = projectId; }
public List<DatabaseTableDesign> getTables() { return 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 void setSql(String sql) { this.sql = sql; }
public Object getErDiagram() { return erDiagram; }

View File

@@ -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;
}
}

View File

@@ -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<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)
{
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();
project.setProjectId(projectId);
project.setGenerateStatus("1");
project.setPreviewStatus("0");
project.setBusinessBlueprint(JSON.toJSONString(response.getBusinessActions()));
frontProjectMapper.updateFrontProject(project);
}

View File

@@ -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<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)
{
List<GenTable> genTables = new ArrayList<GenTable>();

View File

@@ -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<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)
{
FrontProjectTable table = new FrontProjectTable();

View File

@@ -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");

View File

@@ -18,6 +18,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="generateStatus" column="generate_status" />
<result property="previewStatus" column="preview_status" />
<result property="erDiagram" column="er_diagram" />
<result property="businessBlueprint" column="business_blueprint" />
<result property="status" column="status" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
@@ -25,7 +26,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<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>
<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="previewStatus != null">preview_status,</if>
<if test="erDiagram != null">er_diagram,</if>
<if test="businessBlueprint != null">business_blueprint,</if>
<if test="status != null">status,</if>
create_time,
<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="previewStatus != null">#{previewStatus},</if>
<if test="erDiagram != null">#{erDiagram},</if>
<if test="businessBlueprint != null">#{businessBlueprint},</if>
<if test="status != null">#{status},</if>
sysdate(),
<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="previewStatus != null">preview_status = #{previewStatus},</if>
<if test="erDiagram != null">er_diagram = #{erDiagram},</if>
<if test="businessBlueprint != null">business_blueprint = #{businessBlueprint},</if>
<if test="status != null">status = #{status},</if>
update_time = sysdate(),
<if test="remark != null">remark = #{remark},</if>

View File

@@ -13,6 +13,7 @@ import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
@@ -21,11 +22,14 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.generator.config.DeepSeekProperties;
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.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
@@ -62,6 +66,7 @@ public class AiGenerateServiceImplTest
setField("frontProjectColumnMapper", frontProjectColumnMapper);
setField("generationRecordService", generationRecordService);
setField("validator", new AiDatabaseSchemaValidator());
setField("businessBlueprintValidator", new AiBusinessBlueprintValidator());
setField("ddlBuilder", new MysqlDdlBuilder());
setField("deepSeekProperties", new DeepSeekProperties());
}
@@ -86,6 +91,9 @@ public class AiGenerateServiceImplTest
assertEquals(2, columnCaptor.getAllValues().size());
assertEquals("1", generationCaptor.getValue().getSuccess());
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);
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
assertEquals(Long.valueOf(10L), projectCaptor.getValue().getProjectId());
@@ -202,6 +210,55 @@ public class AiGenerateServiceImplTest
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
public void generationRecordInsertUsesRequiresNewTransaction() throws Exception
{
@@ -418,4 +475,55 @@ public class AiGenerateServiceImplTest
+ " ]\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"
+ "}";
}
}

View File

@@ -5,8 +5,11 @@ 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.service.IGenProjectService;
import com.ruoyi.generator.util.VelocityUtils;
import org.apache.velocity.VelocityContext;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -25,10 +28,12 @@ import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
public class FrontProjectPreviewServiceImplTest
{
@@ -60,6 +65,40 @@ public class FrontProjectPreviewServiceImplTest
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
public void downloadAllPackagesBackendFrontendAndAdminFrontend() throws Exception
{

View File

@@ -10,6 +10,7 @@ import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
@@ -18,6 +19,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import com.ruoyi.common.exception.ServiceException;
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.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
@@ -158,6 +160,7 @@ public class FrontProjectServiceImplTest
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.emptyList());
DatabaseDesignResponse request = database(table("sys_user", pkColumn(), column("user_name", "varchar(64)", "0")));
request.setErDiagram(Collections.singletonMap("relations", Collections.singletonList("student_department")));
request.setBusinessActions(Collections.singletonList(businessAction()));
service.saveDatabase(7L, 10L, request);
@@ -174,6 +177,7 @@ public class FrontProjectServiceImplTest
assertEquals("1", projectCaptor.getValue().getGenerateStatus());
assertEquals("0", projectCaptor.getValue().getPreviewStatus());
assertTrue(projectCaptor.getValue().getErDiagram().contains("student_department"));
assertTrue(projectCaptor.getValue().getBusinessBlueprint().contains("borrow_book"));
}
@Test
@@ -191,6 +195,22 @@ public class FrontProjectServiceImplTest
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
{
Field field = FrontProjectServiceImpl.class.getDeclaredField(name);
@@ -236,6 +256,20 @@ public class FrontProjectServiceImplTest
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)
{
try

View File

@@ -1300,6 +1300,7 @@ create table front_project (
generate_status char(1) default '0' comment '数据库生成状态0未生成 1生成中 2已生成 3失败',
preview_status char(1) default '0' comment '预览状态0未预览 1已预览 2失败',
er_diagram longtext comment 'ER图草稿JSON',
business_blueprint longtext comment '业务蓝图草稿JSON',
status char(1) default '0' comment '状态0正常 1停用',
create_time datetime default null comment '创建时间',
update_time datetime default null comment '更新时间',

View File

@@ -43,6 +43,7 @@ create table front_project (
generate_status char(1) default '0' comment '数据库生成状态0未生成 1生成中 2已生成 3失败',
preview_status char(1) default '0' comment '预览状态0未预览 1已预览 2失败',
er_diagram longtext comment 'ER图草稿JSON',
business_blueprint longtext comment '业务蓝图草稿JSON',
status char(1) default '0' comment '状态0正常 1停用',
create_time datetime default null comment '创建时间',
update_time datetime default null comment '更新时间',