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)
}
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) {
return request({
url: `/front/project/${projectId}/database`,

View File

@@ -45,7 +45,10 @@
/>
<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 :disabled="!projectId || !database.tables.length" :loading="saving" @click="handleSaveDatabase">
@@ -64,6 +67,44 @@
</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-header database-header">
<h2 class="panel-title">数据库设计</h2>
@@ -131,24 +172,27 @@
</template>
<script setup>
import { onMounted, reactive, ref, watch } from 'vue'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { DataAnalysis, View } from '@element-plus/icons-vue'
import DatabaseDesigner from '@/components/DatabaseDesigner.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 router = useRouter()
const generating = ref(false)
const generatingAppBlueprint = ref(false)
const saving = ref(false)
const previewing = ref(false)
const loadingProject = ref(false)
const databaseView = ref('editor')
const projectId = ref(normalizeProjectId(route.query.projectId))
const errorMessage = ref('')
const appBlueprint = ref(emptyAppBlueprint())
const appBlueprintJson = ref(formatJson(emptyAppBlueprint()))
const database = ref({ tables: [], sql: '', erDiagram: emptyErDiagram(), businessActions: [] })
const projectForm = reactive({
projectName: '',
@@ -158,6 +202,9 @@ const projectForm = reactive({
})
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) {
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) {
const source = value && typeof value === 'object' ? value : {}
return {
@@ -215,6 +297,8 @@ async function loadProjectDraft(value) {
projectForm.projectName = project.projectName || ''
projectForm.projectDesc = project.projectDesc || ''
projectForm.industryTemplate = project.industryTemplate || ''
appBlueprint.value = normalizeAppBlueprint(project.appBlueprint)
syncAppBlueprintJson()
database.value = normalizeDatabase(databaseResult)
rememberProject(id)
} catch (error) {
@@ -251,14 +335,63 @@ async function ensureProject() {
return projectId.value
}
async function handleGenerateDatabase() {
function validateRequirementInput() {
if (!projectForm.projectName.trim()) {
ElMessage.warning('请输入项目名称')
return
return false
}
if (!projectForm.keyword.trim() && !projectForm.projectDesc.trim()) {
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
}
@@ -354,6 +487,41 @@ onMounted(() => {
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 {
display: grid;
gap: 14px;
@@ -438,5 +606,9 @@ onMounted(() => {
align-items: flex-start;
flex-direction: column;
}
.app-blueprint-grid {
grid-template-columns: 1fr;
}
}
</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.FrontProjectCreateRequest;
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.PreviewFileRequest;
import com.ruoyi.generator.service.front.IAiGenerateService;
@@ -93,6 +94,13 @@ public class FrontProjectController extends BaseController
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")
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.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 org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
@@ -58,6 +61,14 @@ public class GenProject extends BaseEntity
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 List<GenTable> tables;
@@ -190,6 +201,38 @@ public class GenProject extends BaseEntity
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
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)

View File

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

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.FrontProjectGeneration;
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.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;
import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
import com.ruoyi.generator.mapper.front.FrontProjectMapper;
@@ -32,6 +36,9 @@ import com.ruoyi.generator.mapper.front.FrontProjectTableMapper;
public class AiGenerateServiceImpl implements IAiGenerateService
{
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
private IDeepSeekClient deepSeekClient;
@@ -52,15 +59,47 @@ public class AiGenerateServiceImpl implements IAiGenerateService
@Autowired
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
@Transactional
public DatabaseDesignResponse generateDatabase(Long userId, Long projectId, GenerateDatabaseRequest request)
{
assertFrontUserLoggedIn(userId);
FrontProject project = getProject(userId, projectId);
String confirmedAppBlueprint = resolveConfirmedAppBlueprint(project, request);
String prompt = buildPrompt(project, request);
long start = System.currentTimeMillis();
FrontProjectGeneration generation = newGeneration(projectId, userId, prompt);
FrontProjectGeneration generation = newGeneration(projectId, userId, prompt, "database");
try
{
String aiContent = deepSeekClient.chat(prompt);
@@ -71,7 +110,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
validator.validate(response, maxTables(), maxColumnsPerTable());
businessBlueprintValidator.validate(response);
persistDatabase(userId, projectId, response);
markDatabaseGenerated(projectId, response);
markDatabaseGenerated(projectId, response, confirmedAppBlueprint);
generation.setResponsePayload(aiContent);
generation.setSuccess("1");
generation.setElapsedMs(System.currentTimeMillis() - start);
@@ -106,9 +145,28 @@ public class AiGenerateServiceImpl implements IAiGenerateService
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)
{
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("限制表名和字段名使用小写字母、数字、下划线必须且只能有一个主键字段类型只能使用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");
@@ -125,6 +183,159 @@ public class AiGenerateServiceImpl implements IAiGenerateService
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)
{
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();
project.setProjectId(projectId);
project.setGenerateStatus("1");
project.setPreviewStatus("0");
if (StringUtils.isNotBlank(confirmedAppBlueprint))
{
project.setAppBlueprint(confirmedAppBlueprint);
}
project.setBusinessBlueprint(JSON.toJSONString(response.getBusinessActions()));
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();
generation.setProjectId(projectId);
generation.setUserId(userId);
generation.setGenerateType("database");
generation.setGenerateType(generateType);
generation.setProvider("deepseek");
generation.setPromptSummary(StringUtils.abbreviate(prompt, 255));
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.FrontProjectColumn;
import com.ruoyi.generator.domain.front.FrontProjectTable;
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
public class FrontProjectConverter
@@ -31,10 +32,32 @@ public class FrontProjectConverter
genProject.setBackFramework(project.getBackFramework());
genProject.setStatus(project.getStatus());
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()));
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)
{
if (businessBlueprint == null || businessBlueprint.trim().length() == 0)

View File

@@ -1,9 +1,13 @@
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.GenerateAppBlueprintRequest;
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
public interface IAiGenerateService
{
AppBlueprintDesign generateAppBlueprint(Long userId, Long projectId, GenerateAppBlueprintRequest request);
DatabaseDesignResponse generateDatabase(Long userId, Long projectId, GenerateDatabaseRequest request);
}

View File

@@ -131,6 +131,20 @@ public class VelocityUtils {
? Collections.<GenTable>emptyList() : genProject.getTables();
velocityContext.put("tables", 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
? Collections.emptyList() : genProject.getBusinessActions();
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="generateStatus" column="generate_status" />
<result property="previewStatus" column="preview_status" />
<result property="appBlueprint" column="app_blueprint" />
<result property="erDiagram" column="er_diagram" />
<result property="businessBlueprint" column="business_blueprint" />
<result property="status" column="status" />
@@ -26,7 +27,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, 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>
<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="generateStatus != null">generate_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="businessBlueprint != null">business_blueprint,</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="generateStatus != null">#{generateStatus},</if>
<if test="previewStatus != null">#{previewStatus},</if>
<if test="appBlueprint != null">#{appBlueprint},</if>
<if test="erDiagram != null">#{erDiagram},</if>
<if test="businessBlueprint != null">#{businessBlueprint},</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="generateStatus != null">generate_status = #{generateStatus},</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="businessBlueprint != null">business_blueprint = #{businessBlueprint},</if>
<if test="status != null">status = #{status},</if>

View File

@@ -187,6 +187,23 @@ public class GenProjectServiceImplTest {
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
public void getProjectStructureForDraftProjectUsesFirstActiveTemplateWithoutLoadingProject() {
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.FrontProjectGeneration;
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.BusinessActionEffectDesign;
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.mapper.front.FrontProjectColumnMapper;
import com.ruoyi.generator.mapper.front.FrontProjectGenerationMapper;
@@ -102,6 +104,56 @@ public class AiGenerateServiceImplTest
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
public void generateDatabaseAddsIdPrimaryKeyWhenAiOmitsPrimaryKey()
{
@@ -320,6 +372,16 @@ public class AiGenerateServiceImplTest
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)
{
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()
{
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.FrontProjectColumn;
import com.ruoyi.generator.domain.front.FrontProjectTable;
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
import org.junit.Test;
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).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"));
}
@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
public void projectVelocityContextMarksEmptyBusinessActions()
{

View File

@@ -820,6 +820,83 @@ export default {
</script>
', '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
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',
@@ -873,6 +950,9 @@ createApp(App).use(router).use(ElementPlus).mount("#app")
<strong>${projectName}</strong>
<el-menu mode="horizontal" router :default-active="$route.path">
<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-header>
<el-main>
@@ -881,6 +961,22 @@ createApp(App).use(router).use(ElementPlus).mount("#app")
</el-container>
</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>
.shell {
min-height: 100vh;
@@ -897,6 +993,7 @@ createApp(App).use(router).use(ElementPlus).mount("#app")
', 'admin', sysdate()),
(910305, 9103, 'router.js.vm', NULL, 'router.js.vm',
'import { createRouter, createWebHistory } from "vue-router"
import { canShowMenu, frontendMenus, requiresLogin } from "../config/frontendMenus"
#foreach($table in $tables)
import ${table.className}Index from "../views/${table.businessName}/index.vue"
import ${table.className}Detail from "../views/${table.businessName}/detail.vue"
@@ -927,10 +1024,38 @@ const routes = [
#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(),
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()),
(910306, 9103, 'request.js.vm', NULL, 'request.js.vm',
'import axios from "axios"
@@ -1146,6 +1271,50 @@ onMounted(loadDetail)
</style>
', '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
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),
@@ -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),
(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
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),
@@ -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),
(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;

View File

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