Add app blueprint generation flow
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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、DELETE;effects.type优先使用UPDATE_FIELD,conditionFields表示更新或删除时的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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user