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

@@ -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()
{