Add app blueprint generation flow

This commit is contained in:
王鹏
2026-05-23 16:09:44 +08:00
parent 0fbe78868b
commit d91161a302
23 changed files with 933 additions and 12 deletions

View File

@@ -43,6 +43,15 @@ export function generateDatabase(projectId, data) {
}).then(unwrap) }).then(unwrap)
} }
export function generateAppBlueprint(projectId, data) {
return request({
url: `/front/project/${projectId}/generate-app-blueprint`,
method: 'post',
timeout: GENERATE_REQUEST_TIMEOUT,
data
}).then(unwrap)
}
export function getDatabase(projectId) { export function getDatabase(projectId) {
return request({ return request({
url: `/front/project/${projectId}/database`, url: `/front/project/${projectId}/database`,

View File

@@ -45,7 +45,10 @@
/> />
<div class="toolbar"> <div class="toolbar">
<el-button type="primary" :icon="DataAnalysis" :loading="generating" @click="handleGenerateDatabase"> <el-button type="primary" :loading="generatingAppBlueprint" @click="handleGenerateAppBlueprint">
生成应用蓝图
</el-button>
<el-button type="primary" :icon="DataAnalysis" :disabled="!hasAppBlueprint" :loading="generating" @click="handleGenerateDatabase">
生成数据库 生成数据库
</el-button> </el-button>
<el-button :disabled="!projectId || !database.tables.length" :loading="saving" @click="handleSaveDatabase"> <el-button :disabled="!projectId || !database.tables.length" :loading="saving" @click="handleSaveDatabase">
@@ -64,6 +67,44 @@
</div> </div>
</div> </div>
<div class="panel app-blueprint-panel">
<div class="panel-header database-header">
<h2 class="panel-title">应用蓝图</h2>
<div class="database-header-actions">
<span class="muted">角色{{ appBlueprint.roles.length }}</span>
<span class="muted">前台菜单{{ appBlueprint.frontendMenus.length }}</span>
<span class="muted">后台菜单{{ appBlueprint.adminMenus.length }}</span>
</div>
</div>
<div v-if="hasAppBlueprint" class="app-blueprint-content">
<div class="app-blueprint-grid">
<section>
<h3>角色</h3>
<el-tag v-for="role in appBlueprint.roles" :key="role.code" effect="plain">
{{ role.name || role.code }}
</el-tag>
</section>
<section>
<h3>前台菜单</h3>
<el-tag v-for="menu in appBlueprint.frontendMenus" :key="menu.code" type="success" effect="plain">
{{ menu.name || menu.code }}
</el-tag>
</section>
<section>
<h3>后台菜单</h3>
<el-tag v-for="menu in appBlueprint.adminMenus" :key="menu.code" type="warning" effect="plain">
{{ menu.name || menu.code }}
</el-tag>
</section>
</div>
<el-input v-model="appBlueprintJson" type="textarea" :rows="12" spellcheck="false" />
<div class="toolbar compact-toolbar">
<el-button @click="applyAppBlueprintJson">应用调整</el-button>
</div>
</div>
<el-empty v-else description="生成应用蓝图后,这里会展示角色和前后台菜单" />
</div>
<div class="panel designer-panel"> <div class="panel designer-panel">
<div class="panel-header database-header"> <div class="panel-header database-header">
<h2 class="panel-title">数据库设计</h2> <h2 class="panel-title">数据库设计</h2>
@@ -131,24 +172,27 @@
</template> </template>
<script setup> <script setup>
import { onMounted, reactive, ref, watch } from 'vue' import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { DataAnalysis, View } from '@element-plus/icons-vue' import { DataAnalysis, View } from '@element-plus/icons-vue'
import DatabaseDesigner from '@/components/DatabaseDesigner.vue' import DatabaseDesigner from '@/components/DatabaseDesigner.vue'
import ErDiagramView from '@/components/ErDiagramView.vue' import ErDiagramView from '@/components/ErDiagramView.vue'
import { createProject, generateDatabase, generateProject, getDatabase, getProject, saveDatabase } from '@/api/project' import { createProject, generateAppBlueprint, generateDatabase, generateProject, getDatabase, getProject, saveDatabase } from '@/api/project'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const generating = ref(false) const generating = ref(false)
const generatingAppBlueprint = ref(false)
const saving = ref(false) const saving = ref(false)
const previewing = ref(false) const previewing = ref(false)
const loadingProject = ref(false) 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 appBlueprint = ref(emptyAppBlueprint())
const appBlueprintJson = ref(formatJson(emptyAppBlueprint()))
const database = ref({ tables: [], sql: '', erDiagram: emptyErDiagram(), businessActions: [] }) const database = ref({ tables: [], sql: '', erDiagram: emptyErDiagram(), businessActions: [] })
const projectForm = reactive({ const projectForm = reactive({
projectName: '', projectName: '',
@@ -158,6 +202,9 @@ const projectForm = reactive({
}) })
const LAST_PROJECT_KEY = 'easycode_last_project_id' const LAST_PROJECT_KEY = 'easycode_last_project_id'
const hasAppBlueprint = computed(() => {
return appBlueprint.value.roles.length > 0 || appBlueprint.value.frontendMenus.length > 0 || appBlueprint.value.adminMenus.length > 0
})
function unwrap(result) { function unwrap(result) {
return result?.data || result || {} return result?.data || result || {}
@@ -171,6 +218,41 @@ function emptyErDiagram() {
} }
} }
function emptyAppBlueprint() {
return {
roles: [],
frontendMenus: [],
adminMenus: []
}
}
function formatJson(value) {
return JSON.stringify(value || {}, null, 2)
}
function parseMaybeJson(value) {
if (!value) return {}
if (typeof value === 'object') return value
try {
return JSON.parse(value)
} catch (error) {
return {}
}
}
function normalizeAppBlueprint(payload) {
const data = parseMaybeJson(unwrap(payload))
return {
roles: Array.isArray(data.roles) ? data.roles : [],
frontendMenus: Array.isArray(data.frontendMenus) ? data.frontendMenus : [],
adminMenus: Array.isArray(data.adminMenus) ? data.adminMenus : []
}
}
function syncAppBlueprintJson() {
appBlueprintJson.value = formatJson(appBlueprint.value)
}
function normalizeErDiagram(value) { function normalizeErDiagram(value) {
const source = value && typeof value === 'object' ? value : {} const source = value && typeof value === 'object' ? value : {}
return { return {
@@ -215,6 +297,8 @@ async function loadProjectDraft(value) {
projectForm.projectName = project.projectName || '' projectForm.projectName = project.projectName || ''
projectForm.projectDesc = project.projectDesc || '' projectForm.projectDesc = project.projectDesc || ''
projectForm.industryTemplate = project.industryTemplate || '' projectForm.industryTemplate = project.industryTemplate || ''
appBlueprint.value = normalizeAppBlueprint(project.appBlueprint)
syncAppBlueprintJson()
database.value = normalizeDatabase(databaseResult) database.value = normalizeDatabase(databaseResult)
rememberProject(id) rememberProject(id)
} catch (error) { } catch (error) {
@@ -251,14 +335,63 @@ async function ensureProject() {
return projectId.value return projectId.value
} }
async function handleGenerateDatabase() { function validateRequirementInput() {
if (!projectForm.projectName.trim()) { if (!projectForm.projectName.trim()) {
ElMessage.warning('请输入项目名称') ElMessage.warning('请输入项目名称')
return return false
} }
if (!projectForm.keyword.trim() && !projectForm.projectDesc.trim()) { if (!projectForm.keyword.trim() && !projectForm.projectDesc.trim()) {
ElMessage.warning('请输入需求关键词或补充说明') ElMessage.warning('请输入需求关键词或补充说明')
return false
}
return true
}
async function handleGenerateAppBlueprint() {
if (!validateRequirementInput()) {
return
}
generatingAppBlueprint.value = true
errorMessage.value = ''
try {
const id = await ensureProject()
const result = await generateAppBlueprint(id, {
projectName: projectForm.projectName,
projectDesc: projectForm.projectDesc || projectForm.keyword,
industryTemplate: projectForm.industryTemplate,
extraRequirements: projectForm.keyword,
appBlueprint: appBlueprintJson.value
})
appBlueprint.value = normalizeAppBlueprint(result)
syncAppBlueprintJson()
ElMessage.success('应用蓝图已生成')
} catch (error) {
errorMessage.value = error.message || '生成应用蓝图失败,请稍后重试。'
} finally {
generatingAppBlueprint.value = false
}
}
function applyAppBlueprintJson() {
try {
appBlueprint.value = normalizeAppBlueprint(JSON.parse(appBlueprintJson.value || '{}'))
syncAppBlueprintJson()
ElMessage.success('应用蓝图已应用')
} catch (error) {
ElMessage.warning('应用蓝图 JSON 格式不正确')
}
}
async function handleGenerateDatabase() {
if (!validateRequirementInput()) {
return
}
if (!hasAppBlueprint.value) {
ElMessage.warning('请先生成并确认应用蓝图')
return return
} }
@@ -354,6 +487,41 @@ onMounted(() => {
overflow: hidden; overflow: hidden;
} }
.app-blueprint-content {
display: grid;
gap: 16px;
padding: 20px;
}
.app-blueprint-grid {
display: grid;
gap: 14px;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.app-blueprint-grid section {
align-content: start;
border: 1px solid var(--border-color);
border-radius: 8px;
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 96px;
padding: 14px;
}
.app-blueprint-grid h3 {
flex-basis: 100%;
font-size: 14px;
font-weight: 700;
line-height: 1.4;
margin: 0;
}
.compact-toolbar {
justify-content: flex-end;
}
.blueprint-list { .blueprint-list {
display: grid; display: grid;
gap: 14px; gap: 14px;
@@ -438,5 +606,9 @@ onMounted(() => {
align-items: flex-start; align-items: flex-start;
flex-direction: column; flex-direction: column;
} }
.app-blueprint-grid {
grid-template-columns: 1fr;
}
} }
</style> </style>

View File

@@ -20,6 +20,7 @@ import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse; import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.FrontProjectCreateRequest; import com.ruoyi.generator.domain.front.dto.FrontProjectCreateRequest;
import com.ruoyi.generator.domain.front.dto.FrontProjectUpdateRequest; import com.ruoyi.generator.domain.front.dto.FrontProjectUpdateRequest;
import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest; import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
import com.ruoyi.generator.domain.front.dto.PreviewFileRequest; import com.ruoyi.generator.domain.front.dto.PreviewFileRequest;
import com.ruoyi.generator.service.front.IAiGenerateService; import com.ruoyi.generator.service.front.IAiGenerateService;
@@ -93,6 +94,13 @@ public class FrontProjectController extends BaseController
return success(aiGenerateService.generateDatabase(userId, projectId, request)); return success(aiGenerateService.generateDatabase(userId, projectId, request));
} }
@PostMapping("/{projectId}/generate-app-blueprint")
public AjaxResult generateAppBlueprint(@PathVariable Long projectId, @RequestBody GenerateAppBlueprintRequest request)
{
Long userId = SecurityUtils.getUserId();
return success(aiGenerateService.generateAppBlueprint(userId, projectId, request));
}
@PostMapping("/{projectId}/preview") @PostMapping("/{projectId}/preview")
public AjaxResult preview(@PathVariable Long projectId) public AjaxResult preview(@PathVariable Long projectId)
{ {

View File

@@ -2,6 +2,9 @@ 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.AppBlueprintDesign;
import com.ruoyi.generator.domain.front.dto.AppMenuDesign;
import com.ruoyi.generator.domain.front.dto.AppRoleDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign; 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;
@@ -58,6 +61,14 @@ public class GenProject extends BaseEntity
private transient Boolean frontDraft; private transient Boolean frontDraft;
private transient AppBlueprintDesign appBlueprint;
private transient List<AppRoleDesign> appRoles;
private transient List<AppMenuDesign> frontendMenus;
private transient List<AppMenuDesign> adminMenus;
private transient List<BusinessActionDesign> businessActions; private transient List<BusinessActionDesign> businessActions;
private List<GenTable> tables; private List<GenTable> tables;
@@ -190,6 +201,38 @@ public class GenProject extends BaseEntity
this.businessActions = businessActions; this.businessActions = businessActions;
} }
public AppBlueprintDesign getAppBlueprint() {
return appBlueprint;
}
public void setAppBlueprint(AppBlueprintDesign appBlueprint) {
this.appBlueprint = appBlueprint;
}
public List<AppRoleDesign> getAppRoles() {
return appRoles;
}
public void setAppRoles(List<AppRoleDesign> appRoles) {
this.appRoles = appRoles;
}
public List<AppMenuDesign> getFrontendMenus() {
return frontendMenus;
}
public void setFrontendMenus(List<AppMenuDesign> frontendMenus) {
this.frontendMenus = frontendMenus;
}
public List<AppMenuDesign> getAdminMenus() {
return adminMenus;
}
public void setAdminMenus(List<AppMenuDesign> adminMenus) {
this.adminMenus = adminMenus;
}
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)

View File

@@ -19,6 +19,7 @@ public class FrontProject extends BaseEntity
private String backFramework; private String backFramework;
private String generateStatus; private String generateStatus;
private String previewStatus; private String previewStatus;
private String appBlueprint;
private String erDiagram; private String erDiagram;
private String businessBlueprint; private String businessBlueprint;
private String status; private String status;
@@ -48,6 +49,8 @@ public class FrontProject extends BaseEntity
public void setGenerateStatus(String generateStatus) { this.generateStatus = generateStatus; } public void setGenerateStatus(String generateStatus) { this.generateStatus = generateStatus; }
public String getPreviewStatus() { return previewStatus; } public String getPreviewStatus() { return previewStatus; }
public void setPreviewStatus(String previewStatus) { this.previewStatus = previewStatus; } public void setPreviewStatus(String previewStatus) { this.previewStatus = previewStatus; }
public String getAppBlueprint() { return appBlueprint; }
public void setAppBlueprint(String appBlueprint) { this.appBlueprint = appBlueprint; }
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 String getBusinessBlueprint() { return businessBlueprint; }

View File

@@ -0,0 +1,18 @@
package com.ruoyi.generator.domain.front.dto;
import java.util.ArrayList;
import java.util.List;
public class AppBlueprintDesign
{
private List<AppRoleDesign> roles = new ArrayList<AppRoleDesign>();
private List<AppMenuDesign> frontendMenus = new ArrayList<AppMenuDesign>();
private List<AppMenuDesign> adminMenus = new ArrayList<AppMenuDesign>();
public List<AppRoleDesign> getRoles() { return roles; }
public void setRoles(List<AppRoleDesign> roles) { this.roles = roles; }
public List<AppMenuDesign> getFrontendMenus() { return frontendMenus; }
public void setFrontendMenus(List<AppMenuDesign> frontendMenus) { this.frontendMenus = frontendMenus; }
public List<AppMenuDesign> getAdminMenus() { return adminMenus; }
public void setAdminMenus(List<AppMenuDesign> adminMenus) { this.adminMenus = adminMenus; }
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.generator.domain.front.dto;
import java.util.ArrayList;
import java.util.List;
public class AppMenuDesign
{
private String code;
private String name;
private String path;
private Boolean requiresLogin;
private List<String> visibleRoles = new ArrayList<String>();
private String dataScope;
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 getPath() { return path; }
public void setPath(String path) { this.path = path; }
public Boolean getRequiresLogin() { return requiresLogin; }
public void setRequiresLogin(Boolean requiresLogin) { this.requiresLogin = requiresLogin; }
public List<String> getVisibleRoles() { return visibleRoles; }
public void setVisibleRoles(List<String> visibleRoles) { this.visibleRoles = visibleRoles; }
public String getDataScope() { return dataScope; }
public void setDataScope(String dataScope) { this.dataScope = dataScope; }
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.generator.domain.front.dto;
public class AppRoleDesign
{
private String code;
private String name;
private String description;
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 getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
}

View File

@@ -0,0 +1,18 @@
package com.ruoyi.generator.domain.front.dto;
public class GenerateAppBlueprintRequest
{
private String projectName;
private String projectDesc;
private String industryTemplate;
private String extraRequirements;
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public String getProjectDesc() { return projectDesc; }
public void setProjectDesc(String projectDesc) { this.projectDesc = projectDesc; }
public String getIndustryTemplate() { return industryTemplate; }
public void setIndustryTemplate(String industryTemplate) { this.industryTemplate = industryTemplate; }
public String getExtraRequirements() { return extraRequirements; }
public void setExtraRequirements(String extraRequirements) { this.extraRequirements = extraRequirements; }
}

View File

@@ -6,6 +6,7 @@ public class GenerateDatabaseRequest
private String projectDesc; private String projectDesc;
private String industryTemplate; private String industryTemplate;
private String extraRequirements; private String extraRequirements;
private String appBlueprint;
public String getProjectName() { return projectName; } public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; } public void setProjectName(String projectName) { this.projectName = projectName; }
@@ -15,4 +16,6 @@ public class GenerateDatabaseRequest
public void setIndustryTemplate(String industryTemplate) { this.industryTemplate = industryTemplate; } public void setIndustryTemplate(String industryTemplate) { this.industryTemplate = industryTemplate; }
public String getExtraRequirements() { return extraRequirements; } public String getExtraRequirements() { return extraRequirements; }
public void setExtraRequirements(String extraRequirements) { this.extraRequirements = extraRequirements; } public void setExtraRequirements(String extraRequirements) { this.extraRequirements = extraRequirements; }
public String getAppBlueprint() { return appBlueprint; }
public void setAppBlueprint(String appBlueprint) { this.appBlueprint = appBlueprint; }
} }

View File

@@ -18,11 +18,15 @@ 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.AppBlueprintDesign;
import com.ruoyi.generator.domain.front.dto.AppMenuDesign;
import com.ruoyi.generator.domain.front.dto.AppRoleDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign; import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign; 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;
import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
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;
import com.ruoyi.generator.mapper.front.FrontProjectMapper; import com.ruoyi.generator.mapper.front.FrontProjectMapper;
@@ -32,6 +36,9 @@ import com.ruoyi.generator.mapper.front.FrontProjectTableMapper;
public class AiGenerateServiceImpl implements IAiGenerateService public class AiGenerateServiceImpl implements IAiGenerateService
{ {
private static final int MAX_ERROR_MESSAGE_LENGTH = 1000; private static final int MAX_ERROR_MESSAGE_LENGTH = 1000;
private static final String DATA_SCOPE_PUBLIC = "PUBLIC";
private static final String DATA_SCOPE_CURRENT_USER = "CURRENT_USER";
private static final String DATA_SCOPE_ROLE = "ROLE";
@Autowired @Autowired
private IDeepSeekClient deepSeekClient; private IDeepSeekClient deepSeekClient;
@@ -52,15 +59,47 @@ public class AiGenerateServiceImpl implements IAiGenerateService
@Autowired @Autowired
private AiGenerationRecordService generationRecordService; private AiGenerationRecordService generationRecordService;
@Override
@Transactional
public AppBlueprintDesign generateAppBlueprint(Long userId, Long projectId, GenerateAppBlueprintRequest request)
{
assertFrontUserLoggedIn(userId);
FrontProject project = getProject(userId, projectId);
String prompt = buildAppBlueprintPrompt(project, request);
long start = System.currentTimeMillis();
FrontProjectGeneration generation = newGeneration(projectId, userId, prompt, "app_blueprint");
try
{
String aiContent = deepSeekClient.chat(prompt);
AppBlueprintDesign blueprint = parseAppBlueprintResponse(aiContent);
normalizeAppBlueprint(blueprint);
markAppBlueprintGenerated(projectId, blueprint);
generation.setResponsePayload(aiContent);
generation.setSuccess("1");
generation.setElapsedMs(System.currentTimeMillis() - start);
generationRecordService.insert(generation);
return blueprint;
}
catch (RuntimeException e)
{
generation.setSuccess("0");
generation.setErrorMessage(sanitizeErrorMessage(e.getMessage()));
generation.setElapsedMs(System.currentTimeMillis() - start);
generationRecordService.insert(generation);
throw e;
}
}
@Override @Override
@Transactional @Transactional
public DatabaseDesignResponse generateDatabase(Long userId, Long projectId, GenerateDatabaseRequest request) public DatabaseDesignResponse generateDatabase(Long userId, Long projectId, GenerateDatabaseRequest request)
{ {
assertFrontUserLoggedIn(userId); assertFrontUserLoggedIn(userId);
FrontProject project = getProject(userId, projectId); FrontProject project = getProject(userId, projectId);
String confirmedAppBlueprint = resolveConfirmedAppBlueprint(project, request);
String prompt = buildPrompt(project, request); String prompt = buildPrompt(project, request);
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
FrontProjectGeneration generation = newGeneration(projectId, userId, prompt); FrontProjectGeneration generation = newGeneration(projectId, userId, prompt, "database");
try try
{ {
String aiContent = deepSeekClient.chat(prompt); String aiContent = deepSeekClient.chat(prompt);
@@ -71,7 +110,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
validator.validate(response, maxTables(), maxColumnsPerTable()); validator.validate(response, maxTables(), maxColumnsPerTable());
businessBlueprintValidator.validate(response); businessBlueprintValidator.validate(response);
persistDatabase(userId, projectId, response); persistDatabase(userId, projectId, response);
markDatabaseGenerated(projectId, response); markDatabaseGenerated(projectId, response, confirmedAppBlueprint);
generation.setResponsePayload(aiContent); generation.setResponsePayload(aiContent);
generation.setSuccess("1"); generation.setSuccess("1");
generation.setElapsedMs(System.currentTimeMillis() - start); generation.setElapsedMs(System.currentTimeMillis() - start);
@@ -106,9 +145,28 @@ public class AiGenerateServiceImpl implements IAiGenerateService
return project; return project;
} }
private String buildAppBlueprintPrompt(FrontProject project, GenerateAppBlueprintRequest request)
{
StringBuilder prompt = new StringBuilder();
prompt.append("Generate an application blueprint for the target generated project. Return JSON only in this shape: ");
prompt.append("{\"roles\":[{\"code\":\"reader\",\"name\":\"Reader\",\"description\":\"\"}],");
prompt.append("\"frontendMenus\":[{\"code\":\"book_catalog\",\"name\":\"Book Catalog\",\"path\":\"/books\",\"requiresLogin\":false,\"visibleRoles\":[],\"dataScope\":\"PUBLIC\"}],");
prompt.append("\"adminMenus\":[{\"code\":\"book_admin\",\"name\":\"Book Admin\",\"path\":\"/admin/books\",\"requiresLogin\":true,\"visibleRoles\":[\"admin\"],\"dataScope\":\"ROLE\"}]}.\n");
prompt.append("This blueprint is for the generated business project, not for the current EasyCode/RuoYi platform.\n");
prompt.append("Do not generate database tables, menu tables, role tables, permission tables, or code. Only return static role and menu configuration.\n");
prompt.append("Every menu must include requiresLogin, visibleRoles, and dataScope. dataScope must be PUBLIC, CURRENT_USER, or ROLE.\n");
prompt.append("If a menu is for personal data such as my orders, my borrows, profile, reservations, or payments, use requiresLogin=true and dataScope=CURRENT_USER.\n");
prompt.append("Project name: ").append(firstNonBlank(request == null ? null : request.getProjectName(), project.getProjectName())).append("\n");
prompt.append("Project description: ").append(firstNonBlank(request == null ? null : request.getProjectDesc(), project.getProjectDesc())).append("\n");
prompt.append("Industry template: ").append(firstNonBlank(request == null ? null : request.getIndustryTemplate(), project.getIndustryTemplate())).append("\n");
prompt.append("Extra requirements: ").append(request == null ? "" : StringUtils.defaultString(request.getExtraRequirements())).append("\n");
return prompt.toString();
}
private String buildPrompt(FrontProject project, GenerateDatabaseRequest request) private String buildPrompt(FrontProject project, GenerateDatabaseRequest request)
{ {
StringBuilder prompt = new StringBuilder(); StringBuilder prompt = new StringBuilder();
appendAppBlueprintConstraints(prompt, project, request);
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\",\"conditionFields\":[\"book_id\"],\"description\":\"库存减一\"}]}]}。\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\",\"conditionFields\":[\"book_id\"],\"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只描述业务动作DSL不直接返回代码ownerTable、requestFields、effects.targetTable、effects.targetField、effects.conditionFields必须引用已生成的表和字段method只用GET、POST、PUT、DELETEeffects.type优先使用UPDATE_FIELDconditionFields表示更新或删除时的WHERE条件字段。\n"); prompt.append("业务蓝图限制businessActions只描述业务动作DSL不直接返回代码ownerTable、requestFields、effects.targetTable、effects.targetField、effects.conditionFields必须引用已生成的表和字段method只用GET、POST、PUT、DELETEeffects.type优先使用UPDATE_FIELDconditionFields表示更新或删除时的WHERE条件字段。\n");
@@ -125,6 +183,159 @@ public class AiGenerateServiceImpl implements IAiGenerateService
return StringUtils.isNotBlank(first) ? first : StringUtils.defaultString(second); return StringUtils.isNotBlank(first) ? first : StringUtils.defaultString(second);
} }
private void appendAppBlueprintConstraints(StringBuilder prompt, FrontProject project, GenerateDatabaseRequest request)
{
String confirmedAppBlueprint = resolveConfirmedAppBlueprint(project, request);
if (StringUtils.isBlank(confirmedAppBlueprint))
{
return;
}
prompt.append("Confirmed app blueprint JSON: ").append(confirmedAppBlueprint).append("\n");
prompt.append("Database generation constraints from the confirmed app blueprint:\n");
prompt.append("- Do not generate menu or permission tables for frontendMenus, adminMenus, roles, or visibleRoles.\n");
prompt.append("- Menus are static generated project configuration, not database rows.\n");
prompt.append("- Any feature behind requiresLogin=true and dataScope=CURRENT_USER must associate data with the current user, usually through a user_id field.\n");
prompt.append("- Business actions must align with the confirmed menus and roles.\n");
}
private String resolveConfirmedAppBlueprint(FrontProject project, GenerateDatabaseRequest request)
{
return firstNonBlank(request == null ? null : request.getAppBlueprint(), project.getAppBlueprint());
}
private AppBlueprintDesign parseAppBlueprintResponse(String aiContent)
{
String content = stripMarkdownFence(aiContent);
AppBlueprintDesign blueprint = JSON.parseObject(content, AppBlueprintDesign.class);
if (blueprint == null)
{
throw new ServiceException("AI app blueprint is empty");
}
return blueprint;
}
private void normalizeAppBlueprint(AppBlueprintDesign blueprint)
{
if (blueprint.getRoles() == null)
{
blueprint.setRoles(new ArrayList<AppRoleDesign>());
}
if (blueprint.getFrontendMenus() == null)
{
blueprint.setFrontendMenus(new ArrayList<AppMenuDesign>());
}
if (blueprint.getAdminMenus() == null)
{
blueprint.setAdminMenus(new ArrayList<AppMenuDesign>());
}
Set<String> roleCodes = normalizeAppRoles(blueprint.getRoles());
normalizeAppMenus(blueprint.getFrontendMenus(), roleCodes, "frontend_menu");
normalizeAppMenus(blueprint.getAdminMenus(), roleCodes, "admin_menu");
}
private Set<String> normalizeAppRoles(List<AppRoleDesign> roles)
{
Set<String> usedCodes = new HashSet<String>();
if (roles == null)
{
return usedCodes;
}
int roleIndex = 1;
for (AppRoleDesign role : roles)
{
if (role == null)
{
continue;
}
String code = uniqueDatabaseName(role.getCode(), "role", roleIndex, usedCodes);
role.setCode(code);
role.setName(StringUtils.defaultIfEmpty(trimToLength(role.getName(), 80), code));
role.setDescription(trimToLength(role.getDescription(), 200));
roleIndex++;
}
return usedCodes;
}
private void normalizeAppMenus(List<AppMenuDesign> menus, Set<String> roleCodes, String fallbackPrefix)
{
if (menus == null)
{
return;
}
Set<String> usedCodes = new HashSet<String>();
int menuIndex = 1;
for (AppMenuDesign menu : menus)
{
if (menu == null)
{
continue;
}
String code = uniqueDatabaseName(menu.getCode(), fallbackPrefix, menuIndex, usedCodes);
menu.setCode(code);
menu.setName(StringUtils.defaultIfEmpty(trimToLength(menu.getName(), 80), code));
menu.setPath(normalizeMenuPath(menu.getPath(), code));
menu.setRequiresLogin(menu.getRequiresLogin() == null ? Boolean.FALSE : menu.getRequiresLogin());
menu.setVisibleRoles(normalizeMenuRoles(menu.getVisibleRoles(), roleCodes));
menu.setDataScope(normalizeDataScope(menu.getDataScope(), menu.getRequiresLogin()));
menuIndex++;
}
}
private List<String> normalizeMenuRoles(List<String> roles, Set<String> roleCodes)
{
List<String> normalizedRoles = new ArrayList<String>();
if (roles == null)
{
return normalizedRoles;
}
Set<String> usedRoles = new HashSet<String>();
for (int i = 0; i < roles.size(); i++)
{
String roleCode = normalizeDatabaseName(roles.get(i), "role_" + (i + 1));
if ((roleCodes == null || roleCodes.isEmpty() || roleCodes.contains(roleCode)) && !usedRoles.contains(roleCode))
{
normalizedRoles.add(roleCode);
usedRoles.add(roleCode);
}
}
return normalizedRoles;
}
private String normalizeMenuPath(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 = "/" + code.replace("_", "-");
}
if (normalized.length() > 180)
{
normalized = normalized.substring(0, 180).replaceAll("[./_-]+$", "");
}
return normalized;
}
private String normalizeDataScope(String dataScope, Boolean requiresLogin)
{
if (!Boolean.TRUE.equals(requiresLogin))
{
return DATA_SCOPE_PUBLIC;
}
String normalized = StringUtils.defaultString(dataScope).trim().toUpperCase(Locale.ENGLISH);
normalized = normalized.replaceAll("[^A-Z0-9]+", "_").replaceAll("_+", "_").replaceAll("^_+|_+$", "");
if (DATA_SCOPE_PUBLIC.equals(normalized) || DATA_SCOPE_CURRENT_USER.equals(normalized) || DATA_SCOPE_ROLE.equals(normalized))
{
return normalized;
}
return DATA_SCOPE_CURRENT_USER;
}
private DatabaseDesignResponse parseResponse(String aiContent) private DatabaseDesignResponse parseResponse(String aiContent)
{ {
String content = stripMarkdownFence(aiContent); String content = stripMarkdownFence(aiContent);
@@ -755,22 +966,36 @@ public class AiGenerateServiceImpl implements IAiGenerateService
} }
} }
private void markDatabaseGenerated(Long projectId, DatabaseDesignResponse response) private void markDatabaseGenerated(Long projectId, DatabaseDesignResponse response, String confirmedAppBlueprint)
{ {
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");
if (StringUtils.isNotBlank(confirmedAppBlueprint))
{
project.setAppBlueprint(confirmedAppBlueprint);
}
project.setBusinessBlueprint(JSON.toJSONString(response.getBusinessActions())); project.setBusinessBlueprint(JSON.toJSONString(response.getBusinessActions()));
frontProjectMapper.updateFrontProject(project); frontProjectMapper.updateFrontProject(project);
} }
private FrontProjectGeneration newGeneration(Long projectId, Long userId, String prompt) private void markAppBlueprintGenerated(Long projectId, AppBlueprintDesign blueprint)
{
FrontProject project = new FrontProject();
project.setProjectId(projectId);
project.setGenerateStatus("0");
project.setPreviewStatus("0");
project.setAppBlueprint(JSON.toJSONString(blueprint));
frontProjectMapper.updateFrontProject(project);
}
private FrontProjectGeneration newGeneration(Long projectId, Long userId, String prompt, String generateType)
{ {
FrontProjectGeneration generation = new FrontProjectGeneration(); FrontProjectGeneration generation = new FrontProjectGeneration();
generation.setProjectId(projectId); generation.setProjectId(projectId);
generation.setUserId(userId); generation.setUserId(userId);
generation.setGenerateType("database"); generation.setGenerateType(generateType);
generation.setProvider("deepseek"); generation.setProvider("deepseek");
generation.setPromptSummary(StringUtils.abbreviate(prompt, 255)); generation.setPromptSummary(StringUtils.abbreviate(prompt, 255));
generation.setRequestPayload(prompt); generation.setRequestPayload(prompt);

View File

@@ -9,6 +9,7 @@ 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.AppBlueprintDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign; import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
public class FrontProjectConverter public class FrontProjectConverter
@@ -31,10 +32,32 @@ 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()));
AppBlueprintDesign appBlueprint = parseAppBlueprint(project.getAppBlueprint());
genProject.setAppBlueprint(appBlueprint);
genProject.setAppRoles(appBlueprint.getRoles());
genProject.setFrontendMenus(appBlueprint.getFrontendMenus());
genProject.setAdminMenus(appBlueprint.getAdminMenus());
genProject.setBusinessActions(parseBusinessActions(project.getBusinessBlueprint())); genProject.setBusinessActions(parseBusinessActions(project.getBusinessBlueprint()));
return genProject; return genProject;
} }
private static AppBlueprintDesign parseAppBlueprint(String appBlueprint)
{
if (appBlueprint == null || appBlueprint.trim().length() == 0)
{
return new AppBlueprintDesign();
}
try
{
AppBlueprintDesign blueprint = JSON.parseObject(appBlueprint, AppBlueprintDesign.class);
return blueprint == null ? new AppBlueprintDesign() : blueprint;
}
catch (RuntimeException e)
{
return new AppBlueprintDesign();
}
}
private static List<BusinessActionDesign> parseBusinessActions(String businessBlueprint) private static List<BusinessActionDesign> parseBusinessActions(String businessBlueprint)
{ {
if (businessBlueprint == null || businessBlueprint.trim().length() == 0) if (businessBlueprint == null || businessBlueprint.trim().length() == 0)

View File

@@ -1,9 +1,13 @@
package com.ruoyi.generator.service.front; package com.ruoyi.generator.service.front;
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse; import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest; import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
public interface IAiGenerateService public interface IAiGenerateService
{ {
AppBlueprintDesign generateAppBlueprint(Long userId, Long projectId, GenerateAppBlueprintRequest request);
DatabaseDesignResponse generateDatabase(Long userId, Long projectId, GenerateDatabaseRequest request); DatabaseDesignResponse generateDatabase(Long userId, Long projectId, GenerateDatabaseRequest request);
} }

View File

@@ -131,6 +131,20 @@ public class VelocityUtils {
? Collections.<GenTable>emptyList() : genProject.getTables(); ? Collections.<GenTable>emptyList() : genProject.getTables();
velocityContext.put("tables", tables); velocityContext.put("tables", tables);
velocityContext.put("hasTables", StringUtils.isNotEmpty(tables)); velocityContext.put("hasTables", StringUtils.isNotEmpty(tables));
velocityContext.put("appBlueprint", genProject.getAppBlueprint());
List<?> appRoles = genProject.getAppRoles() == null
? Collections.emptyList() : genProject.getAppRoles();
List<?> frontendMenus = genProject.getFrontendMenus() == null
? Collections.emptyList() : genProject.getFrontendMenus();
List<?> adminMenus = genProject.getAdminMenus() == null
? Collections.emptyList() : genProject.getAdminMenus();
velocityContext.put("appRoles", appRoles);
velocityContext.put("frontendMenus", frontendMenus);
velocityContext.put("adminMenus", adminMenus);
velocityContext.put("hasAppBlueprint", genProject.getAppBlueprint() != null);
velocityContext.put("hasAppRoles", StringUtils.isNotEmpty(appRoles));
velocityContext.put("hasFrontendMenus", StringUtils.isNotEmpty(frontendMenus));
velocityContext.put("hasAdminMenus", StringUtils.isNotEmpty(adminMenus));
List<?> businessActions = genProject.getBusinessActions() == null List<?> businessActions = genProject.getBusinessActions() == null
? Collections.emptyList() : genProject.getBusinessActions(); ? Collections.emptyList() : genProject.getBusinessActions();
velocityContext.put("businessActions", businessActions); velocityContext.put("businessActions", businessActions);

View File

@@ -17,6 +17,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="backFramework" column="back_framework" /> <result property="backFramework" column="back_framework" />
<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="appBlueprint" column="app_blueprint" />
<result property="erDiagram" column="er_diagram" /> <result property="erDiagram" column="er_diagram" />
<result property="businessBlueprint" column="business_blueprint" /> <result property="businessBlueprint" column="business_blueprint" />
<result property="status" column="status" /> <result property="status" column="status" />
@@ -26,7 +27,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, business_blueprint, 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, app_blueprint, 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="backFramework != null">back_framework,</if> <if test="backFramework != null">back_framework,</if>
<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="appBlueprint != null">app_blueprint,</if>
<if test="erDiagram != null">er_diagram,</if> <if test="erDiagram != null">er_diagram,</if>
<if test="businessBlueprint != null">business_blueprint,</if> <if test="businessBlueprint != null">business_blueprint,</if>
<if test="status != null">status,</if> <if test="status != null">status,</if>
@@ -84,6 +86,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="backFramework != null">#{backFramework},</if> <if test="backFramework != null">#{backFramework},</if>
<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="appBlueprint != null">#{appBlueprint},</if>
<if test="erDiagram != null">#{erDiagram},</if> <if test="erDiagram != null">#{erDiagram},</if>
<if test="businessBlueprint != null">#{businessBlueprint},</if> <if test="businessBlueprint != null">#{businessBlueprint},</if>
<if test="status != null">#{status},</if> <if test="status != null">#{status},</if>
@@ -105,6 +108,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="backFramework != null">back_framework = #{backFramework},</if> <if test="backFramework != null">back_framework = #{backFramework},</if>
<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="appBlueprint != null">app_blueprint = #{appBlueprint},</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="businessBlueprint != null">business_blueprint = #{businessBlueprint},</if>
<if test="status != null">status = #{status},</if> <if test="status != null">status = #{status},</if>

View File

@@ -187,6 +187,23 @@ public class GenProjectServiceImplTest {
assertTrue(sql.contains("conditionFields")); assertTrue(sql.contains("conditionFields"));
} }
@Test
public void businessBlueprintSqlTemplateGeneratesStaticMenuAuthFiles() throws Exception {
Path sqlPath = Paths.get("sql/business_blueprint_templates.sql");
if (!Files.exists(sqlPath)) {
sqlPath = Paths.get("../sql/business_blueprint_templates.sql");
}
String sql = new String(Files.readAllBytes(sqlPath), StandardCharsets.UTF_8);
assertTrue(sql.contains("frontendMenus.js.vm"));
assertTrue(sql.contains("adminMenus.js.vm"));
assertTrue(sql.contains("canShowMenu"));
assertTrue(sql.contains("requiresLogin"));
assertTrue(sql.contains("router.beforeEach"));
assertTrue(sql.contains("installAdminGuard"));
assertFalse(sql.contains("insert into generated_menu"));
}
@Test @Test
public void getProjectStructureForDraftProjectUsesFirstActiveTemplateWithoutLoadingProject() { public void getProjectStructureForDraftProjectUsesFirstActiveTemplateWithoutLoadingProject() {
GenProject project = project(); GenProject project = project();

View File

@@ -28,9 +28,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.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.AppBlueprintDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign; import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign; 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.GenerateAppBlueprintRequest;
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;
import com.ruoyi.generator.mapper.front.FrontProjectGenerationMapper; import com.ruoyi.generator.mapper.front.FrontProjectGenerationMapper;
@@ -102,6 +104,56 @@ public class AiGenerateServiceImplTest
assertEquals("0", projectCaptor.getValue().getPreviewStatus()); assertEquals("0", projectCaptor.getValue().getPreviewStatus());
} }
@Test
public void generateAppBlueprintNormalizesAndPersistsStaticMenus()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
when(deepSeekClient.chat(anyString())).thenReturn(appBlueprintResponse());
AppBlueprintDesign blueprint = service.generateAppBlueprint(7L, 10L, appBlueprintRequest());
assertEquals(2, blueprint.getRoles().size());
assertEquals("reader", blueprint.getRoles().get(0).getCode());
assertEquals("admin", blueprint.getRoles().get(1).getCode());
assertEquals("my_borrows", blueprint.getFrontendMenus().get(0).getCode());
assertEquals(Boolean.TRUE, blueprint.getFrontendMenus().get(0).getRequiresLogin());
assertEquals("CURRENT_USER", blueprint.getFrontendMenus().get(0).getDataScope());
assertEquals("reader", blueprint.getFrontendMenus().get(0).getVisibleRoles().get(0));
assertEquals("book_admin", blueprint.getAdminMenus().get(0).getCode());
assertEquals("ROLE", blueprint.getAdminMenus().get(0).getDataScope());
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
assertEquals(Long.valueOf(10L), projectCaptor.getValue().getProjectId());
assertTrue(projectCaptor.getValue().getAppBlueprint().contains("my_borrows"));
assertEquals("0", projectCaptor.getValue().getGenerateStatus());
assertEquals("0", projectCaptor.getValue().getPreviewStatus());
ArgumentCaptor<FrontProjectGeneration> generationCaptor = ArgumentCaptor.forClass(FrontProjectGeneration.class);
verify(frontProjectGenerationMapper).insertFrontProjectGeneration(generationCaptor.capture());
assertEquals("app_blueprint", generationCaptor.getValue().getGenerateType());
assertEquals("1", generationCaptor.getValue().getSuccess());
}
@Test
public void generateDatabaseIncludesConfirmedAppBlueprintConstraintsInPrompt()
{
FrontProject project = project();
project.setAppBlueprint("{\"frontendMenus\":[{\"code\":\"my_borrows\",\"requiresLogin\":true,\"dataScope\":\"CURRENT_USER\"}],\"adminMenus\":[]}");
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project);
when(deepSeekClient.chat(anyString())).thenReturn(mockResponse());
service.generateDatabase(7L, 10L, request());
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
verify(deepSeekClient).chat(promptCaptor.capture());
String prompt = promptCaptor.getValue();
assertTrue(prompt.contains("Confirmed app blueprint JSON"));
assertTrue(prompt.contains("CURRENT_USER"));
assertTrue(prompt.contains("Do not generate menu or permission tables"));
assertTrue(prompt.contains("user_id"));
}
@Test @Test
public void generateDatabaseAddsIdPrimaryKeyWhenAiOmitsPrimaryKey() public void generateDatabaseAddsIdPrimaryKeyWhenAiOmitsPrimaryKey()
{ {
@@ -320,6 +372,16 @@ public class AiGenerateServiceImplTest
return request; return request;
} }
private GenerateAppBlueprintRequest appBlueprintRequest()
{
GenerateAppBlueprintRequest request = new GenerateAppBlueprintRequest();
request.setProjectName("Library");
request.setProjectDesc("Readers borrow and return books");
request.setIndustryTemplate("library");
request.setExtraRequirements("Need portal and admin menus");
return request;
}
private String repeated(String value, int count) private String repeated(String value, int count)
{ {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
@@ -364,6 +426,23 @@ public class AiGenerateServiceImplTest
+ "}"; + "}";
} }
private String appBlueprintResponse()
{
return "{\n"
+ " \"roles\": [\n"
+ " {\"code\": \"Reader\", \"name\": \"Reader\", \"description\": \"Portal reader\"},\n"
+ " {\"code\": \"admin\", \"name\": \"Admin\", \"description\": \"Back office\"}\n"
+ " ],\n"
+ " \"frontendMenus\": [\n"
+ " {\"code\": \"My Borrows\", \"name\": \"My Borrows\", \"path\": \"my-borrows\", \"requiresLogin\": true, \"visibleRoles\": [\"Reader\"], \"dataScope\": \"current_user\"},\n"
+ " {\"code\": \"Catalog\", \"name\": \"Catalog\", \"path\": \"/catalog\", \"requiresLogin\": false, \"visibleRoles\": []}\n"
+ " ],\n"
+ " \"adminMenus\": [\n"
+ " {\"code\": \"Book Admin\", \"name\": \"Books\", \"path\": \"/books\", \"requiresLogin\": true, \"visibleRoles\": [\"admin\"], \"dataScope\": \"role\"}\n"
+ " ]\n"
+ "}";
}
private String responseWithoutPrimaryKey() private String responseWithoutPrimaryKey()
{ {
return "{\n" return "{\n"

View File

@@ -4,6 +4,7 @@ import com.ruoyi.generator.domain.GenProject;
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.AppBlueprintDesign;
import org.junit.Test; import org.junit.Test;
import java.util.Collections; import java.util.Collections;
@@ -55,4 +56,21 @@ public class FrontProjectConverterTest
assertEquals("carId", genProject.getTables().get(0).getColumns().get(0).getJavaField()); assertEquals("carId", genProject.getTables().get(0).getColumns().get(0).getJavaField());
assertEquals("carId", genProject.getTables().get(0).getPkColumn().getJavaField()); assertEquals("carId", genProject.getTables().get(0).getPkColumn().getJavaField());
} }
@Test
public void convertsAppBlueprintToGenProject()
{
FrontProject project = new FrontProject();
project.setProjectId(10L);
project.setProjectName("Library");
project.setPackageName("com.example.library");
project.setAppBlueprint("{\"roles\":[{\"code\":\"reader\",\"name\":\"Reader\"}],\"frontendMenus\":[{\"code\":\"my_borrows\",\"name\":\"My Borrows\",\"path\":\"/my-borrows\",\"requiresLogin\":true,\"visibleRoles\":[\"reader\"],\"dataScope\":\"CURRENT_USER\"}],\"adminMenus\":[{\"code\":\"book_admin\",\"name\":\"Books\",\"path\":\"/books\",\"requiresLogin\":true,\"visibleRoles\":[\"admin\"],\"dataScope\":\"ROLE\"}]}");
GenProject genProject = FrontProjectConverter.toGenProject(project);
AppBlueprintDesign blueprint = genProject.getAppBlueprint();
assertEquals("reader", blueprint.getRoles().get(0).getCode());
assertEquals("my_borrows", genProject.getFrontendMenus().get(0).getCode());
assertEquals("book_admin", genProject.getAdminMenus().get(0).getCode());
}
} }

View File

@@ -86,6 +86,28 @@ public class FrontProjectPreviewServiceImplTest
assertEquals(Boolean.TRUE, context.get("hasBusinessActions")); assertEquals(Boolean.TRUE, context.get("hasBusinessActions"));
} }
@Test
public void passesAppBlueprintMenusIntoGenerationContext()
{
FrontProject project = frontProjectWithOneTable();
project.setAppBlueprint("{\"roles\":[{\"code\":\"reader\",\"name\":\"Reader\"}],\"frontendMenus\":[{\"code\":\"my_borrows\",\"name\":\"My Borrows\",\"path\":\"/my-borrows\",\"requiresLogin\":true,\"visibleRoles\":[\"reader\"],\"dataScope\":\"CURRENT_USER\"}],\"adminMenus\":[{\"code\":\"book_admin\",\"name\":\"Books\",\"path\":\"/books\",\"requiresLogin\":true,\"visibleRoles\":[\"admin\"],\"dataScope\":\"ROLE\"}]}");
when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
when(genProjectService.getProjectStructure(any(GenProject.class), eq("frontend")))
.thenReturn(Collections.<Map<String, Object>>emptyList());
previewService.getStructure(100L, 200L, "frontend");
ArgumentCaptor<GenProject> projectCaptor = ArgumentCaptor.forClass(GenProject.class);
verify(genProjectService).getProjectStructure(projectCaptor.capture(), eq("frontend"));
GenProject genProject = projectCaptor.getValue();
VelocityContext context = VelocityUtils.prepareContextProject(genProject);
assertEquals("reader", genProject.getAppRoles().get(0).getCode());
assertEquals(genProject.getFrontendMenus(), context.get("frontendMenus"));
assertEquals(genProject.getAdminMenus(), context.get("adminMenus"));
assertEquals(Boolean.TRUE, context.get("hasFrontendMenus"));
assertEquals(Boolean.TRUE, context.get("hasAdminMenus"));
}
@Test @Test
public void projectVelocityContextMarksEmptyBusinessActions() public void projectVelocityContextMarksEmptyBusinessActions()
{ {

View File

@@ -820,6 +820,83 @@ export default {
</script> </script>
', 'admin', sysdate()); ', 'admin', sysdate());
INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES
(910204, 9102, 'adminMenus.js.vm', NULL, 'adminMenus.js.vm',
'export const appRoles = [
#foreach($role in $appRoles)
{ code: "${role.code}", name: "${role.name}", description: "${role.description}" }#if($foreach.hasNext),#end
#end
]
export const adminMenus = [
#foreach($menu in $adminMenus)
{
code: "${menu.code}",
name: "${menu.name}",
path: "${menu.path}",
requiresLogin: ${menu.requiresLogin},
visibleRoles: [
#foreach($role in $menu.visibleRoles)
"${role}"#if($foreach.hasNext),#end
#end
],
dataScope: "${menu.dataScope}"
}#if($foreach.hasNext),#end
#end
]
export function requiresLogin(menu) {
return Boolean(menu && menu.requiresLogin)
}
export function canShowMenu(menu, currentUser = {}) {
if (!menu) {
return false
}
if (menu.requiresLogin && !currentUser.id && !currentUser.userId) {
return false
}
const userRoles = Array.isArray(currentUser.roles) ? currentUser.roles : []
if (!Array.isArray(menu.visibleRoles) || menu.visibleRoles.length === 0) {
return true
}
return menu.visibleRoles.some(role => userRoles.includes(role))
}
', 'admin', sysdate());
INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES
(910205, 9102, 'adminPermission.js.vm', NULL, 'adminPermission.js.vm',
'import { adminMenus, canShowMenu, requiresLogin } from "./config/adminMenus"
function defaultCurrentUser() {
try {
return JSON.parse(localStorage.getItem("currentUser") || "{}")
} catch (error) {
return {}
}
}
function isLoggedIn(user) {
return Boolean(user && (user.id || user.userId))
}
export function installAdminGuard(router, getCurrentUser = defaultCurrentUser) {
router.beforeEach((to, from, next) => {
const menu = adminMenus.find(item => item.path === to.path)
const user = getCurrentUser() || {}
if (menu && !canShowMenu(menu, user)) {
next(false)
return
}
if (menu && requiresLogin(menu) && !isLoggedIn(user)) {
next("/login")
return
}
next()
})
}
', 'admin', sysdate());
-- Portal frontend template files -- Portal frontend template files
INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES
(910301, 9103, 'package.json.vm', NULL, 'package.json.vm', (910301, 9103, 'package.json.vm', NULL, 'package.json.vm',
@@ -873,6 +950,9 @@ createApp(App).use(router).use(ElementPlus).mount("#app")
<strong>${projectName}</strong> <strong>${projectName}</strong>
<el-menu mode="horizontal" router :default-active="$route.path"> <el-menu mode="horizontal" router :default-active="$route.path">
<el-menu-item index="/">工作台</el-menu-item> <el-menu-item index="/">工作台</el-menu-item>
<el-menu-item v-for="menu in visibleMenus" :key="menu.code" :index="menu.path">
{{ menu.name }}
</el-menu-item>
</el-menu> </el-menu>
</el-header> </el-header>
<el-main> <el-main>
@@ -881,6 +961,22 @@ createApp(App).use(router).use(ElementPlus).mount("#app")
</el-container> </el-container>
</template> </template>
<script setup>
import { computed, ref } from "vue"
import { canShowMenu, frontendMenus } from "./config/frontendMenus"
function loadCurrentUser() {
try {
return JSON.parse(localStorage.getItem("currentUser") || "{}")
} catch (error) {
return {}
}
}
const currentUser = ref(loadCurrentUser())
const visibleMenus = computed(() => frontendMenus.filter(menu => canShowMenu(menu, currentUser.value)))
</script>
<style scoped> <style scoped>
.shell { .shell {
min-height: 100vh; min-height: 100vh;
@@ -897,6 +993,7 @@ createApp(App).use(router).use(ElementPlus).mount("#app")
', 'admin', sysdate()), ', 'admin', sysdate()),
(910305, 9103, 'router.js.vm', NULL, 'router.js.vm', (910305, 9103, 'router.js.vm', NULL, 'router.js.vm',
'import { createRouter, createWebHistory } from "vue-router" 'import { createRouter, createWebHistory } from "vue-router"
import { canShowMenu, frontendMenus, requiresLogin } from "../config/frontendMenus"
#foreach($table in $tables) #foreach($table in $tables)
import ${table.className}Index from "../views/${table.businessName}/index.vue" import ${table.className}Index from "../views/${table.businessName}/index.vue"
import ${table.className}Detail from "../views/${table.businessName}/detail.vue" import ${table.className}Detail from "../views/${table.businessName}/detail.vue"
@@ -927,10 +1024,38 @@ const routes = [
#end #end
] ]
export default createRouter({ function currentUser() {
try {
return JSON.parse(localStorage.getItem("currentUser") || "{}")
} catch (error) {
return {}
}
}
function isLoggedIn(user) {
return Boolean(user && (user.id || user.userId))
}
const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes routes
}) })
router.beforeEach((to, from, next) => {
const menu = frontendMenus.find(item => item.path === to.path)
const user = currentUser()
if (menu && !canShowMenu(menu, user)) {
next(false)
return
}
if (menu && requiresLogin(menu) && !isLoggedIn(user)) {
next("/login")
return
}
next()
})
export default router
', 'admin', sysdate()), ', 'admin', sysdate()),
(910306, 9103, 'request.js.vm', NULL, 'request.js.vm', (910306, 9103, 'request.js.vm', NULL, 'request.js.vm',
'import axios from "axios" 'import axios from "axios"
@@ -1146,6 +1271,50 @@ onMounted(loadDetail)
</style> </style>
', 'admin', sysdate()); ', 'admin', sysdate());
INSERT INTO sys_template_file (template_file_id, template_id, file_name, module_id, file_path, file_content, create_by, create_time) VALUES
(910310, 9103, 'frontendMenus.js.vm', NULL, 'frontendMenus.js.vm',
'export const appRoles = [
#foreach($role in $appRoles)
{ code: "${role.code}", name: "${role.name}", description: "${role.description}" }#if($foreach.hasNext),#end
#end
]
export const frontendMenus = [
#foreach($menu in $frontendMenus)
{
code: "${menu.code}",
name: "${menu.name}",
path: "${menu.path}",
requiresLogin: ${menu.requiresLogin},
visibleRoles: [
#foreach($role in $menu.visibleRoles)
"${role}"#if($foreach.hasNext),#end
#end
],
dataScope: "${menu.dataScope}"
}#if($foreach.hasNext),#end
#end
]
export function requiresLogin(menu) {
return Boolean(menu && menu.requiresLogin)
}
export function canShowMenu(menu, currentUser = {}) {
if (!menu) {
return false
}
if (menu.requiresLogin && !currentUser.id && !currentUser.userId) {
return false
}
const userRoles = Array.isArray(currentUser.roles) ? currentUser.roles : []
if (!Array.isArray(menu.visibleRoles) || menu.visibleRoles.length === 0) {
return true
}
return menu.visibleRoles.some(role => userRoles.includes(role))
}
', 'admin', sysdate());
-- Backend project structure -- Backend project structure
INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES
(910100, 0, '{projectName}-backend', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL), (910100, 0, '{projectName}-backend', 'folder', NULL, 9101, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
@@ -1184,6 +1353,13 @@ INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, mod
(910208, 910207, '{businessName}', 'folder', NULL, 9102, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL), (910208, 910207, '{businessName}', 'folder', NULL, 9102, 0, NULL, 1, '0', 'admin', sysdate(), '', NULL),
(910209, 910208, 'index.vue', 'file', NULL, 9102, 0, 'index.vue.vm', 1, '0', 'admin', sysdate(), '', NULL); (910209, 910208, 'index.vue', 'file', NULL, 9102, 0, 'index.vue.vm', 1, '0', 'admin', sysdate(), '', NULL);
INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES
(910210, 910202, 'config', 'folder', NULL, 9102, NULL, NULL, 3, '0', 'admin', sysdate(), '', NULL),
(910211, 910210, 'adminMenus.js', 'file', NULL, 9102, -1, 'adminMenus.js.vm', 1, '0', 'admin', sysdate(), '', NULL);
INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES
(910212, 910202, 'permission.js', 'file', NULL, 9102, -1, 'adminPermission.js.vm', 4, '0', 'admin', sysdate(), '', NULL);
-- Portal frontend project structure -- Portal frontend project structure
INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES
(910300, 0, '{projectName}-portal', 'folder', NULL, 9103, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL), (910300, 0, '{projectName}-portal', 'folder', NULL, 9103, NULL, NULL, 1, '0', 'admin', sysdate(), '', NULL),
@@ -1202,4 +1378,8 @@ INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, mod
(910313, 910312, 'index.vue', 'file', NULL, 9103, 0, 'index.vue.vm', 1, '0', 'admin', sysdate(), '', NULL), (910313, 910312, 'index.vue', 'file', NULL, 9103, 0, 'index.vue.vm', 1, '0', 'admin', sysdate(), '', NULL),
(910314, 910312, 'detail.vue', 'file', NULL, 9103, 0, 'detail.vue.vm', 2, '0', 'admin', sysdate(), '', NULL); (910314, 910312, 'detail.vue', 'file', NULL, 9103, 0, 'detail.vue.vm', 2, '0', 'admin', sysdate(), '', NULL);
INSERT INTO sys_project_structure (node_id, parent_id, node_name, node_type, module, template_id, table_id, category, sort_order, status, create_by, create_time, update_by, update_time) VALUES
(910315, 910303, 'config', 'folder', NULL, 9103, NULL, NULL, 6, '0', 'admin', sysdate(), '', NULL),
(910316, 910315, 'frontendMenus.js', 'file', NULL, 9103, -1, 'frontendMenus.js.vm', 1, '0', 'admin', sysdate(), '', NULL);
SET FOREIGN_KEY_CHECKS = 1; SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -1299,6 +1299,7 @@ create table front_project (
back_framework varchar(50) default 'Spring Boot + MyBatis' comment '后端框架', back_framework varchar(50) default 'Spring Boot + MyBatis' comment '后端框架',
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失败',
app_blueprint longtext comment '应用蓝图草稿JSON',
er_diagram longtext comment 'ER图草稿JSON', er_diagram longtext comment 'ER图草稿JSON',
business_blueprint longtext comment '业务蓝图草稿JSON', business_blueprint longtext comment '业务蓝图草稿JSON',
status char(1) default '0' comment '状态0正常 1停用', status char(1) default '0' comment '状态0正常 1停用',

View File

@@ -0,0 +1,17 @@
set @front_app_blueprint_exists := (
select count(1)
from information_schema.columns
where table_schema = database()
and table_name = 'front_project'
and column_name = 'app_blueprint'
);
set @front_app_blueprint_sql := if(
@front_app_blueprint_exists = 0,
'alter table front_project add column app_blueprint longtext comment ''应用蓝图草稿JSON'' after preview_status',
'select ''front_project.app_blueprint already exists'''
);
prepare front_app_blueprint_stmt from @front_app_blueprint_sql;
execute front_app_blueprint_stmt;
deallocate prepare front_app_blueprint_stmt;

View File

@@ -42,6 +42,7 @@ create table front_project (
back_framework varchar(50) default 'Spring Boot + MyBatis' comment '后端框架', back_framework varchar(50) default 'Spring Boot + MyBatis' comment '后端框架',
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失败',
app_blueprint longtext comment '应用蓝图草稿JSON',
er_diagram longtext comment 'ER图草稿JSON', er_diagram longtext comment 'ER图草稿JSON',
business_blueprint longtext comment '业务蓝图草稿JSON', business_blueprint longtext comment '业务蓝图草稿JSON',
status char(1) default '0' comment '状态0正常 1停用', status char(1) default '0' comment '状态0正常 1停用',