Add executable business blueprint workflow

This commit is contained in:
王鹏
2026-05-24 13:29:42 +08:00
parent ecdc015c2f
commit 6682a44a43
40 changed files with 4756 additions and 113 deletions

View File

@@ -55,6 +55,8 @@ public class GenProject extends BaseEntity
@Excel(name = "后端框架")
private String backFramework;
private transient String frontendEnabled;
/** 状态0正常 1停用 */
@Excel(name = "状态", readConverterExp = "0=正常,1=停用")
private String status;
@@ -71,6 +73,8 @@ public class GenProject extends BaseEntity
private transient List<BusinessActionDesign> businessActions;
private transient List<SysProjectModule> modules;
private List<GenTable> tables;
public void setProjectId(Long projectId)
@@ -163,6 +167,14 @@ public class GenProject extends BaseEntity
return backFramework;
}
public String getFrontendEnabled() {
return frontendEnabled;
}
public void setFrontendEnabled(String frontendEnabled) {
this.frontendEnabled = frontendEnabled;
}
public void setStatus(String status)
{
this.status = status;
@@ -181,6 +193,14 @@ public class GenProject extends BaseEntity
this.tables = tables;
}
public List<SysProjectModule> getModules() {
return modules;
}
public void setModules(List<SysProjectModule> modules) {
this.modules = modules;
}
public Boolean getFrontDraft() {
return frontDraft;
}

View File

@@ -2,6 +2,7 @@ package com.ruoyi.generator.domain.front;
import java.util.List;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.generator.domain.SysProjectModule;
public class FrontProject extends BaseEntity
{
@@ -18,6 +19,7 @@ public class FrontProject extends BaseEntity
private String industryTemplate;
private String frontFramework;
private String backFramework;
private String frontendEnabled;
private String generateStatus;
private String previewStatus;
private String appBlueprint;
@@ -25,6 +27,8 @@ public class FrontProject extends BaseEntity
private String businessBlueprint;
private String status;
private List<FrontProjectTable> tables;
private List<Long> moduleIds;
private List<SysProjectModule> modules;
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
@@ -48,6 +52,8 @@ public class FrontProject extends BaseEntity
public void setFrontFramework(String frontFramework) { this.frontFramework = frontFramework; }
public String getBackFramework() { return backFramework; }
public void setBackFramework(String backFramework) { this.backFramework = backFramework; }
public String getFrontendEnabled() { return frontendEnabled; }
public void setFrontendEnabled(String frontendEnabled) { this.frontendEnabled = frontendEnabled; }
public String getGenerateStatus() { return generateStatus; }
public void setGenerateStatus(String generateStatus) { this.generateStatus = generateStatus; }
public String getPreviewStatus() { return previewStatus; }
@@ -62,4 +68,8 @@ public class FrontProject extends BaseEntity
public void setStatus(String status) { this.status = status; }
public List<FrontProjectTable> getTables() { return tables; }
public void setTables(List<FrontProjectTable> tables) { this.tables = tables; }
public List<Long> getModuleIds() { return moduleIds; }
public void setModuleIds(List<Long> moduleIds) { this.moduleIds = moduleIds; }
public List<SysProjectModule> getModules() { return modules; }
public void setModules(List<SysProjectModule> modules) { this.modules = modules; }
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.generator.domain.front;
import com.ruoyi.common.core.domain.BaseEntity;
public class FrontProjectModule extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long id;
private Long projectId;
private Long moduleId;
private String status;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public Long getModuleId() { return moduleId; }
public void setModuleId(Long moduleId) { this.moduleId = moduleId; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
}

View File

@@ -12,6 +12,7 @@ public class BusinessActionDesign
private Boolean transaction;
private List<String> requestFields;
private List<String> rules;
private List<BusinessActionRuleDesign> ruleChecks;
private List<BusinessActionEffectDesign> effects;
public String getCode() { return code; }
@@ -30,6 +31,8 @@ public class BusinessActionDesign
public void setRequestFields(List<String> requestFields) { this.requestFields = requestFields; }
public List<String> getRules() { return rules; }
public void setRules(List<String> rules) { this.rules = rules; }
public List<BusinessActionRuleDesign> getRuleChecks() { return ruleChecks; }
public void setRuleChecks(List<BusinessActionRuleDesign> ruleChecks) { this.ruleChecks = ruleChecks; }
public List<BusinessActionEffectDesign> getEffects() { return effects; }
public void setEffects(List<BusinessActionEffectDesign> effects) { this.effects = effects; }
}

View File

@@ -1,6 +1,7 @@
package com.ruoyi.generator.domain.front.dto;
import java.util.List;
import java.util.Map;
public class BusinessActionEffectDesign
{
@@ -8,7 +9,11 @@ public class BusinessActionEffectDesign
private String targetTable;
private String targetField;
private String value;
private Map<String, String> values;
private String amount;
private String minValue;
private List<String> conditionFields;
private String message;
private String description;
public String getType() { return type; }
@@ -19,8 +24,16 @@ public class BusinessActionEffectDesign
public void setTargetField(String targetField) { this.targetField = targetField; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; }
public Map<String, String> getValues() { return values; }
public void setValues(Map<String, String> values) { this.values = values; }
public String getAmount() { return amount; }
public void setAmount(String amount) { this.amount = amount; }
public String getMinValue() { return minValue; }
public void setMinValue(String minValue) { this.minValue = minValue; }
public List<String> getConditionFields() { return conditionFields; }
public void setConditionFields(List<String> conditionFields) { this.conditionFields = conditionFields; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
}

View File

@@ -0,0 +1,32 @@
package com.ruoyi.generator.domain.front.dto;
import java.util.List;
public class BusinessActionRuleDesign
{
private String type;
private String targetTable;
private String targetField;
private String expectedValue;
private List<String> expectedValues;
private String compareValue;
private List<String> conditionFields;
private String message;
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getTargetTable() { return targetTable; }
public void setTargetTable(String targetTable) { this.targetTable = targetTable; }
public String getTargetField() { return targetField; }
public void setTargetField(String targetField) { this.targetField = targetField; }
public String getExpectedValue() { return expectedValue; }
public void setExpectedValue(String expectedValue) { this.expectedValue = expectedValue; }
public List<String> getExpectedValues() { return expectedValues; }
public void setExpectedValues(List<String> expectedValues) { this.expectedValues = expectedValues; }
public String getCompareValue() { return compareValue; }
public void setCompareValue(String compareValue) { this.compareValue = compareValue; }
public List<String> getConditionFields() { return conditionFields; }
public void setConditionFields(List<String> conditionFields) { this.conditionFields = conditionFields; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
}

View File

@@ -1,5 +1,7 @@
package com.ruoyi.generator.domain.front.dto;
import java.util.List;
public class FrontProjectCreateRequest
{
private String projectName;
@@ -8,6 +10,8 @@ public class FrontProjectCreateRequest
private String industryTemplate;
private String packageName;
private String version;
private Boolean frontendEnabled;
private List<Long> moduleIds;
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
@@ -21,4 +25,8 @@ public class FrontProjectCreateRequest
public void setPackageName(String packageName) { this.packageName = packageName; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public Boolean getFrontendEnabled() { return frontendEnabled; }
public void setFrontendEnabled(Boolean frontendEnabled) { this.frontendEnabled = frontendEnabled; }
public List<Long> getModuleIds() { return moduleIds; }
public void setModuleIds(List<Long> moduleIds) { this.moduleIds = moduleIds; }
}

View File

@@ -1,5 +1,7 @@
package com.ruoyi.generator.domain.front.dto;
import java.util.List;
public class FrontProjectUpdateRequest
{
private String projectName;
@@ -8,6 +10,8 @@ public class FrontProjectUpdateRequest
private String industryTemplate;
private String packageName;
private String version;
private Boolean frontendEnabled;
private List<Long> moduleIds;
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
@@ -21,4 +25,8 @@ public class FrontProjectUpdateRequest
public void setPackageName(String packageName) { this.packageName = packageName; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public Boolean getFrontendEnabled() { return frontendEnabled; }
public void setFrontendEnabled(Boolean frontendEnabled) { this.frontendEnabled = frontendEnabled; }
public List<Long> getModuleIds() { return moduleIds; }
public void setModuleIds(List<Long> moduleIds) { this.moduleIds = moduleIds; }
}

View File

@@ -0,0 +1,18 @@
package com.ruoyi.generator.domain.front.dto;
public class GenerateBusinessBlueprintRequest
{
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

@@ -0,0 +1,16 @@
package com.ruoyi.generator.mapper.front;
import java.util.List;
import com.ruoyi.generator.domain.SysProjectModule;
import com.ruoyi.generator.domain.front.FrontProjectModule;
public interface FrontProjectModuleMapper
{
public List<FrontProjectModule> selectModulesByProjectId(Long projectId);
public List<SysProjectModule> selectSysProjectModulesByProjectId(Long projectId);
public int insertFrontProjectModule(FrontProjectModule frontProjectModule);
public int deleteModulesByProjectId(Long projectId);
}

View File

@@ -106,12 +106,17 @@ public class GenProjectServiceImpl implements IGenProjectService {
List<SysProjectStructure> structureNodes = selectStructureNodes(project, projectTemplate);
Map<Long, SysProjectStructure> sourceNodeMap = new HashMap<Long, SysProjectStructure>();
Map<String, Map<String, Object>> nodeMap = new LinkedHashMap<String, Map<String, Object>>();
for (SysProjectStructure node : structureNodes) {
sourceNodeMap.put(node.getNodeId(), node);
}
Map<String, Map<String, Object>> nodeMap = new LinkedHashMap<String, Map<String, Object>>();
Map<String, Map<String, Object>> mergedFolderMap = new LinkedHashMap<String, Map<String, Object>>();
for (SysProjectStructure node : structureNodes) {
if (isTableScopedNode(node)) {
for (GenTable table : projectTables) {
Map<String, Object> tableNodeData = createStructureNodeData(node, project, table);
tableNodeData = mergeTableScopedFolderNode(mergedFolderMap, node, project, table, sourceNodeMap, tableNodeData);
nodeMap.put(getNodeKey(node.getNodeId(), table.getTableId()), tableNodeData);
}
} else {
@@ -239,7 +244,7 @@ public class GenProjectServiceImpl implements IGenProjectService {
private List<SysProjectModule> getProjectModules(GenProject project) {
if (project.isFrontDraft()) {
return new ArrayList<SysProjectModule>();
return project.getModules() == null ? new ArrayList<SysProjectModule>() : project.getModules();
}
SysProjectModule projectModuleFilter = new SysProjectModule();
projectModuleFilter.setProjectId(project.getProjectId());
@@ -338,6 +343,7 @@ public class GenProjectServiceImpl implements IGenProjectService {
result = result.replace("{projectName}", StringUtils.defaultString(project.getProjectFileName()));
result = result.replace("{packageName}", StringUtils.defaultString(project.getPackageName()));
if (table != null) {
result = result.replace("{moduleName}", StringUtils.defaultString(table.getModuleName()));
result = result.replace("{ClassName}", StringUtils.defaultString(table.getClassName()));
result = result.replace("{className}", StringUtils.uncapitalize(StringUtils.defaultString(table.getClassName())));
result = result.replace("{tableName}", StringUtils.defaultString(table.getTableName()));
@@ -346,6 +352,45 @@ public class GenProjectServiceImpl implements IGenProjectService {
return result;
}
private Map<String, Object> mergeTableScopedFolderNode(Map<String, Map<String, Object>> mergedFolderMap,
SysProjectStructure node, GenProject project, GenTable table,
Map<Long, SysProjectStructure> sourceNodeMap,
Map<String, Object> nodeData) {
if (!"folder".equals(node.getNodeType()) || nodeData == null) {
return nodeData;
}
String mergeKey = getTableScopedFolderMergeKey(node, project, table, sourceNodeMap);
Map<String, Object> existingNode = mergedFolderMap.get(mergeKey);
if (existingNode != null) {
return existingNode;
}
mergedFolderMap.put(mergeKey, nodeData);
return nodeData;
}
private String getTableScopedFolderMergeKey(SysProjectStructure node, GenProject project, GenTable table,
Map<Long, SysProjectStructure> sourceNodeMap) {
List<String> pathParts = new ArrayList<String>();
SysProjectStructure current = node;
while (current != null) {
GenTable scopedTable = isTableScopedNode(current) ? table : null;
pathParts.add(0, current.getNodeId() + ":" + replaceNodeName(current.getNodeName(), project, scopedTable));
if (current.getParentId() == null || current.getParentId() == 0) {
break;
}
current = sourceNodeMap.get(current.getParentId());
}
StringBuilder key = new StringBuilder();
for (String part : pathParts) {
if (key.length() > 0) {
key.append("/");
}
key.append(part);
}
return key.toString();
}
private void addNodeToParent(List<Map<String, Object>> roots, Map<String, Object> nodeData,
SysProjectStructure node, Map<String, Map<String, Object>> nodeMap,
Map<Long, SysProjectStructure> sourceNodeMap, GenTable table) {
@@ -353,7 +398,7 @@ public class GenProjectServiceImpl implements IGenProjectService {
return;
}
if (node.getParentId() == null || node.getParentId() == 0) {
roots.add(nodeData);
addNodeIfAbsent(roots, nodeData);
return;
}
@@ -367,10 +412,19 @@ public class GenProjectServiceImpl implements IGenProjectService {
@SuppressWarnings("unchecked")
List<Map<String, Object>> children = (List<Map<String, Object>>) parentNode.get("children");
if (children != null) {
children.add(nodeData);
addNodeIfAbsent(children, nodeData);
}
}
private void addNodeIfAbsent(List<Map<String, Object>> nodes, Map<String, Object> nodeData) {
for (Map<String, Object> node : nodes) {
if (node == nodeData) {
return;
}
}
nodes.add(nodeData);
}
private boolean matchesTemplateFile(TemplateFile templateFile, String templateFileName) {
if (templateFile == null || StringUtils.isEmpty(templateFileName)) {
return false;

View File

@@ -3,13 +3,16 @@ package com.ruoyi.generator.service.front;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.stereotype.Component;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionRuleDesign;
import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
@@ -22,6 +25,8 @@ public class AiBusinessBlueprintValidator
private static final int MAX_EFFECTS = 12;
private static final Pattern ACTION_CODE = Pattern.compile("^[a-z][a-z0-9_]{1,63}$");
private static final Pattern API_PATH = Pattern.compile("^/[A-Za-z0-9_./-]{1,180}$");
private static final Pattern PARAM_PLACEHOLDER = Pattern.compile("^\\$\\{param\\.([a-z][a-z0-9_]*)}$");
private static final Pattern NUMBER_LITERAL = Pattern.compile("^-?\\d+(\\.\\d+)?$");
public void validate(DatabaseDesignResponse response)
{
@@ -31,7 +36,7 @@ public class AiBusinessBlueprintValidator
}
if (response.getBusinessActions().size() > MAX_ACTIONS)
{
throw new ServiceException("业务动作数量超过限制");
throw new ServiceException("Business action count exceeds limit");
}
Set<String> tableNames = collectTableNames(response.getTables());
Set<String> columnNames = collectColumnNames(response.getTables());
@@ -46,19 +51,19 @@ public class AiBusinessBlueprintValidator
{
if (action == null)
{
throw new ServiceException("业务动作不能为空");
throw new ServiceException("Business action cannot be empty");
}
if (StringUtils.isBlank(action.getCode()) || !ACTION_CODE.matcher(action.getCode()).matches())
{
throw new ServiceException("业务动作编码只能包含小写字母、数字和下划线");
throw new ServiceException("Business action code is invalid");
}
if (!actionCodes.add(action.getCode()))
{
throw new ServiceException("业务动作编码重复:" + action.getCode());
throw new ServiceException("Business action code duplicated: " + action.getCode());
}
if (StringUtils.isBlank(action.getName()) || action.getName().length() > 80)
{
throw new ServiceException("业务动作名称不能为空且不能超过80个字符");
throw new ServiceException("Business action name is required and cannot exceed 80 characters");
}
if (StringUtils.isBlank(action.getOwnerTable()) || !tableNames.contains(action.getOwnerTable()))
{
@@ -66,10 +71,11 @@ public class AiBusinessBlueprintValidator
}
if (StringUtils.isNotBlank(action.getPath()) && !API_PATH.matcher(action.getPath()).matches())
{
throw new ServiceException("业务动作接口路径不合法");
throw new ServiceException("Business action API path is invalid");
}
validateRequestFields(action, columnNames);
validateRules(action);
validateRuleChecks(action, tableNames, columnNames);
validateEffects(action, tableNames, columnNames);
}
@@ -83,7 +89,7 @@ public class AiBusinessBlueprintValidator
{
if (StringUtils.isBlank(field) || !columnNames.contains(field))
{
throw new ServiceException("业务动作请求字段不存在:" + StringUtils.defaultString(field));
throw new ServiceException("Business action request field does not exist: " + StringUtils.defaultString(field));
}
}
}
@@ -96,13 +102,71 @@ public class AiBusinessBlueprintValidator
}
if (action.getRules().size() > MAX_RULES)
{
throw new ServiceException("业务规则数量超过限制");
throw new ServiceException("Business rule count exceeds limit");
}
for (String rule : action.getRules())
{
if (StringUtils.isBlank(rule) || rule.length() > 200)
{
throw new ServiceException("业务规则不能为空且不能超过200个字符");
throw new ServiceException("Business rule is required and cannot exceed 200 characters");
}
}
}
private void validateRuleChecks(BusinessActionDesign action, Set<String> tableNames, Set<String> columnNames)
{
if (StringUtils.isEmpty(action.getRuleChecks()))
{
return;
}
if (action.getRuleChecks().size() > MAX_RULES)
{
throw new ServiceException("Business rule check count exceeds limit");
}
for (BusinessActionRuleDesign rule : action.getRuleChecks())
{
if (rule == null)
{
throw new ServiceException("Business rule check cannot be empty");
}
String type = StringUtils.defaultString(rule.getType()).toUpperCase(Locale.ENGLISH);
if (!isSupportedRuleType(type))
{
throw new ServiceException("Unsupported business rule type: " + type);
}
validateTargetTable(rule.getTargetTable(), tableNames, "Business rule target table does not exist: ");
validateConditionFields(rule.getConditionFields(), columnNames, "Business rule condition field does not exist: ");
if (requiresRuleTargetField(type) && StringUtils.isBlank(rule.getTargetField()))
{
throw new ServiceException("Business rule target field is required: " + type);
}
if (StringUtils.isNotBlank(rule.getTargetField()) && !columnNames.contains(rule.getTargetField()))
{
throw new ServiceException("Business rule target field does not exist: " + rule.getTargetField());
}
if (requiresExpectedValue(type) && StringUtils.isBlank(rule.getExpectedValue()))
{
throw new ServiceException("Business rule expected value is required: " + type);
}
if ("FIELD_IN".equals(type) && StringUtils.isEmpty(rule.getExpectedValues()))
{
throw new ServiceException("Business rule expected values are required: " + type);
}
if ("NUMBER_GTE".equals(type))
{
validateNumberExpression(rule.getCompareValue(), columnNames, "Business rule compare value is invalid");
}
validateSafeValue(rule.getExpectedValue(), columnNames);
if (rule.getExpectedValues() != null)
{
for (String expectedValue : rule.getExpectedValues())
{
validateSafeValue(expectedValue, columnNames);
}
}
if (StringUtils.defaultString(rule.getMessage()).length() > 200)
{
throw new ServiceException("Business rule message cannot exceed 200 characters");
}
}
}
@@ -115,33 +179,61 @@ public class AiBusinessBlueprintValidator
}
if (action.getEffects().size() > MAX_EFFECTS)
{
throw new ServiceException("业务影响数量超过限制");
throw new ServiceException("Business effect count exceeds limit");
}
for (BusinessActionEffectDesign effect : action.getEffects())
{
if (effect == null)
validateEffect(effect, tableNames, columnNames);
}
}
private void validateEffect(BusinessActionEffectDesign effect, Set<String> tableNames, Set<String> columnNames)
{
if (effect == null)
{
throw new ServiceException("Business effect cannot be empty");
}
String type = StringUtils.defaultString(effect.getType()).toUpperCase(Locale.ENGLISH);
if (StringUtils.isBlank(type) || type.length() > 50)
{
throw new ServiceException("Business effect type is required and cannot exceed 50 characters");
}
if (!isSupportedEffectType(type))
{
throw new ServiceException("Unsupported business effect type: " + type);
}
validateTargetTable(effect.getTargetTable(), tableNames, "Business effect target table does not exist: ");
if (requiresEffectTargetField(type) && StringUtils.isBlank(effect.getTargetField()))
{
throw new ServiceException("Business effect target field is required: " + type);
}
if (StringUtils.isNotBlank(effect.getTargetField()) && !columnNames.contains(effect.getTargetField()))
{
throw new ServiceException("Business effect target field does not exist: " + effect.getTargetField());
}
validateEffectConditionFields(effect, columnNames);
validateEffectValues(effect, columnNames);
if (requiresEffectValues(type) && (effect.getValues() == null || effect.getValues().isEmpty()))
{
throw new ServiceException("Business effect values are required: " + type);
}
if ("INCREASE_NUMBER".equals(type) || "DECREASE_NUMBER".equals(type))
{
validateNumberExpression(effect.getAmount(), columnNames, "Business effect amount is invalid");
if (StringUtils.isNotBlank(effect.getMinValue()))
{
throw new ServiceException("业务影响不能为空");
}
if (StringUtils.isBlank(effect.getType()) || effect.getType().length() > 50)
{
throw new ServiceException("业务影响类型不能为空且不能超过50个字符");
}
if (StringUtils.isNotBlank(effect.getTargetTable()) && !tableNames.contains(effect.getTargetTable()))
{
throw new ServiceException("业务影响目标表不存在:" + effect.getTargetTable());
}
if (StringUtils.isNotBlank(effect.getTargetField()) && !columnNames.contains(effect.getTargetField()))
{
throw new ServiceException("业务影响目标字段不存在:" + effect.getTargetField());
}
validateEffectConditionFields(effect, columnNames);
if (StringUtils.defaultString(effect.getValue()).length() > 200
|| StringUtils.defaultString(effect.getDescription()).length() > 200)
{
throw new ServiceException("业务影响描述不能超过200个字符");
validateNumberExpression(effect.getMinValue(), columnNames, "Business effect min value is invalid");
}
}
validateSafeValue(effect.getValue(), columnNames);
if (StringUtils.defaultString(effect.getValue()).length() > 200
|| StringUtils.defaultString(effect.getAmount()).length() > 80
|| StringUtils.defaultString(effect.getMinValue()).length() > 80
|| StringUtils.defaultString(effect.getMessage()).length() > 200
|| StringUtils.defaultString(effect.getDescription()).length() > 200)
{
throw new ServiceException("Business effect text cannot exceed limit");
}
}
private void validateEffectConditionFields(BusinessActionEffectDesign effect, Set<String> columnNames)
@@ -154,11 +246,132 @@ public class AiBusinessBlueprintValidator
{
if (StringUtils.isBlank(field) || !columnNames.contains(field))
{
throw new ServiceException("业务影响条件字段不存在:" + StringUtils.defaultString(field));
throw new ServiceException("Business effect condition field does not exist: " + StringUtils.defaultString(field));
}
}
}
private void validateEffectValues(BusinessActionEffectDesign effect, Set<String> columnNames)
{
if (effect.getValues() == null)
{
return;
}
for (Map.Entry<String, String> entry : effect.getValues().entrySet())
{
if (StringUtils.isBlank(entry.getKey()) || !columnNames.contains(entry.getKey()))
{
throw new ServiceException("Business effect value field does not exist: " + StringUtils.defaultString(entry.getKey()));
}
validateSafeValue(entry.getValue(), columnNames);
}
}
private void validateTargetTable(String targetTable, Set<String> tableNames, String messagePrefix)
{
if (StringUtils.isBlank(targetTable) || !tableNames.contains(targetTable))
{
throw new ServiceException(messagePrefix + StringUtils.defaultString(targetTable));
}
}
private void validateConditionFields(List<String> conditionFields, Set<String> columnNames, String messagePrefix)
{
if (StringUtils.isEmpty(conditionFields))
{
throw new ServiceException(messagePrefix + "");
}
for (String field : conditionFields)
{
if (StringUtils.isBlank(field) || !columnNames.contains(field))
{
throw new ServiceException(messagePrefix + StringUtils.defaultString(field));
}
}
}
private boolean isSupportedRuleType(String type)
{
return "EXISTS".equals(type) || "NOT_EXISTS".equals(type) || "FIELD_EQUALS".equals(type)
|| "FIELD_NOT_EQUALS".equals(type) || "FIELD_IN".equals(type) || "NUMBER_GTE".equals(type);
}
private boolean requiresRuleTargetField(String type)
{
return "FIELD_EQUALS".equals(type) || "FIELD_NOT_EQUALS".equals(type) || "FIELD_IN".equals(type)
|| "NUMBER_GTE".equals(type);
}
private boolean requiresExpectedValue(String type)
{
return "FIELD_EQUALS".equals(type) || "FIELD_NOT_EQUALS".equals(type);
}
private boolean isSupportedEffectType(String type)
{
return "UPDATE_FIELD".equals(type) || "INSERT_ROW".equals(type) || "UPDATE_FIELDS".equals(type)
|| "SET_STATUS".equals(type) || "INCREASE_NUMBER".equals(type) || "DECREASE_NUMBER".equals(type);
}
private boolean requiresEffectTargetField(String type)
{
return "UPDATE_FIELD".equals(type) || "SET_STATUS".equals(type) || "INCREASE_NUMBER".equals(type)
|| "DECREASE_NUMBER".equals(type);
}
private boolean requiresEffectValues(String type)
{
return "INSERT_ROW".equals(type) || "UPDATE_FIELDS".equals(type);
}
private void validateSafeValue(String value, Set<String> columnNames)
{
if (StringUtils.isBlank(value))
{
return;
}
Matcher matcher = PARAM_PLACEHOLDER.matcher(value);
if (matcher.matches())
{
String field = matcher.group(1);
if (!columnNames.contains(field))
{
throw new ServiceException("Business effect value references missing field: " + field);
}
return;
}
if ("${now}".equals(value))
{
return;
}
if (value.contains(";") || value.contains("--") || value.contains("`") || value.length() > 200)
{
throw new ServiceException("Business effect value is invalid");
}
}
private void validateNumberExpression(String value, Set<String> columnNames, String message)
{
if (StringUtils.isBlank(value))
{
throw new ServiceException(message);
}
Matcher matcher = PARAM_PLACEHOLDER.matcher(value);
if (matcher.matches())
{
String field = matcher.group(1);
if (!columnNames.contains(field))
{
throw new ServiceException("Business effect value references missing field: " + field);
}
return;
}
if (!NUMBER_LITERAL.matcher(value).matches())
{
throw new ServiceException(message);
}
}
private Set<String> collectTableNames(List<DatabaseTableDesign> tables)
{
Set<String> tableNames = new HashSet<String>();

View File

@@ -3,6 +3,7 @@ package com.ruoyi.generator.service.front;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -26,10 +27,12 @@ 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.BusinessActionRuleDesign;
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.GenerateBusinessBlueprintRequest;
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
import com.ruoyi.generator.mapper.front.FrontProjectMapper;
@@ -99,7 +102,6 @@ public class AiGenerateServiceImpl implements IAiGenerateService
{
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, "database");
@@ -109,11 +111,10 @@ public class AiGenerateServiceImpl implements IAiGenerateService
DatabaseDesignResponse response = parseResponse(aiContent);
response.setProjectId(projectId);
normalizeAndBuildSql(response);
normalizeBusinessActions(response);
response.setBusinessActions(new ArrayList<BusinessActionDesign>());
validator.validate(response, maxTables(), maxColumnsPerTable());
businessBlueprintValidator.validate(response);
persistDatabase(userId, projectId, response);
markDatabaseGenerated(projectId, response, confirmedAppBlueprint, request);
markDatabaseGenerated(projectId, request);
generation.setResponsePayload(aiContent);
generation.setSuccess("1");
generation.setElapsedMs(System.currentTimeMillis() - start);
@@ -130,6 +131,39 @@ public class AiGenerateServiceImpl implements IAiGenerateService
}
}
@Override
@Transactional
public List<BusinessActionDesign> generateBusinessBlueprint(Long userId, Long projectId, GenerateBusinessBlueprintRequest request)
{
assertFrontUserLoggedIn(userId);
FrontProject project = getProject(userId, projectId);
DatabaseDesignResponse database = loadSavedDatabaseDesign(projectId);
String prompt = buildBusinessBlueprintPrompt(project, request, database);
long start = System.currentTimeMillis();
FrontProjectGeneration generation = newGeneration(projectId, userId, prompt, "business_blueprint");
try
{
String aiContent = deepSeekClient.chat(prompt);
DatabaseDesignResponse response = parseBusinessBlueprintResponse(aiContent, database);
normalizeBusinessActions(response);
businessBlueprintValidator.validate(response);
markBusinessBlueprintGenerated(projectId, response.getBusinessActions(), request);
generation.setResponsePayload(aiContent);
generation.setSuccess("1");
generation.setElapsedMs(System.currentTimeMillis() - start);
generationRecordService.insert(generation);
return response.getBusinessActions();
}
catch (RuntimeException e)
{
generation.setSuccess("0");
generation.setErrorMessage(sanitizeErrorMessage(e.getMessage()));
generation.setElapsedMs(System.currentTimeMillis() - start);
generationRecordService.insert(generation);
throw e;
}
}
private void assertFrontUserLoggedIn(Long userId)
{
if (userId == null)
@@ -157,6 +191,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
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("role.name must be Simplified Chinese so the blueprint summary can display Chinese role names. Keep role.code in lowercase English snake_case.\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");
@@ -170,9 +205,10 @@ public class AiGenerateServiceImpl implements IAiGenerateService
{
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("Generate a MySQL database design for an enterprise management system. Return JSON only in this shape: ");
prompt.append("{\"tables\":[{\"tableName\":\"car_info\",\"tableComment\":\"Car info table\",\"columns\":[]}]}.\n");
prompt.append("Do not generate businessActions, business blueprint, workflows, code, menu tables, role tables, or permission tables in this step.\n");
prompt.append("限制表名和字段名使用小写字母、数字、下划线必须且只能有一个主键字段类型只能使用bigint(20)、int(11)、varchar(n)、char(n)、decimal(p,s)、datetime、date、text、longtext。\n");
prompt.append("业务蓝图限制businessActions只描述业务动作DSL不直接返回代码ownerTable、requestFields、effects.targetTable、effects.targetField、effects.conditionFields必须引用已生成的表和字段method只用GET、POST、PUT、DELETEeffects.type优先使用UPDATE_FIELDconditionFields表示更新或删除时的WHERE条件字段。\n");
prompt.append("项目名称:").append(firstNonBlank(request == null ? null : request.getProjectName(), project.getProjectName())).append("\n");
prompt.append("项目描述:").append(firstNonBlank(request == null ? null : request.getProjectDesc(), project.getProjectDesc())).append("\n");
prompt.append("行业模板:").append(firstNonBlank(request == null ? null : request.getIndustryTemplate(), project.getIndustryTemplate())).append("\n");
@@ -181,11 +217,6 @@ public class AiGenerateServiceImpl implements IAiGenerateService
return prompt.toString();
}
private String firstNonBlank(String first, String second)
{
return StringUtils.isNotBlank(first) ? first : StringUtils.defaultString(second);
}
private void appendAppBlueprintConstraints(StringBuilder prompt, FrontProject project, GenerateDatabaseRequest request)
{
String confirmedAppBlueprint = resolveConfirmedAppBlueprint(project, request);
@@ -198,7 +229,6 @@ public class AiGenerateServiceImpl implements IAiGenerateService
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)
@@ -206,6 +236,64 @@ public class AiGenerateServiceImpl implements IAiGenerateService
return firstNonBlank(request == null ? null : request.getAppBlueprint(), project.getAppBlueprint());
}
private String buildBusinessBlueprintPrompt(FrontProject project, GenerateBusinessBlueprintRequest request, DatabaseDesignResponse database)
{
StringBuilder prompt = new StringBuilder();
prompt.append("Generate a business blueprint from the saved database schema. Return JSON only in this shape: ");
prompt.append("{\"businessActions\":[{\"code\":\"borrow_book\",\"name\":\"借书\",\"ownerTable\":\"borrow_record\",\"method\":\"POST\",\"path\":\"/library/borrow/borrow\",\"transaction\":true,\"requestFields\":[\"book_id\"],\"rules\":[\"图书必须可借\"],\"ruleChecks\":[{\"type\":\"NUMBER_GTE\",\"targetTable\":\"book_info\",\"targetField\":\"stock\",\"compareValue\":\"1\",\"conditionFields\":[\"book_id\"],\"message\":\"库存不足\"}],\"effects\":[{\"type\":\"INSERT_ROW\",\"targetTable\":\"borrow_record\",\"values\":{\"book_id\":\"${param.book_id}\",\"status\":\"borrowed\"},\"description\":\"创建借阅记录\"},{\"type\":\"DECREASE_NUMBER\",\"targetTable\":\"book_info\",\"targetField\":\"stock\",\"amount\":\"1\",\"minValue\":\"0\",\"conditionFields\":[\"book_id\"],\"message\":\"库存扣减失败\",\"description\":\"库存减一\"}]}]}.\n");
prompt.append("Do not generate CRUD. Do not generate create, read, update, delete, list, detail, import, export, or simple table maintenance actions.\n");
prompt.append("Generate only business workflows that go beyond CRUD, such as approval, payment confirmation, borrow, return, stock adjustment by workflow, scheduling, check-in, settlement, or status transition.\n");
prompt.append("Do not generate database tables or code. businessActions only describes a safe DSL.\n");
prompt.append("Supported ruleChecks are EXISTS, NOT_EXISTS, FIELD_EQUALS, FIELD_NOT_EQUALS, FIELD_IN, NUMBER_GTE.\n");
prompt.append("Supported effects are INSERT_ROW, UPDATE_FIELDS, SET_STATUS, INCREASE_NUMBER, DECREASE_NUMBER.\n");
prompt.append("businessActions.name, rules, ruleChecks.message, effects.message, and effects.description must be Simplified Chinese. Keep API paths and request parameter fields as technical identifiers.\n");
prompt.append("ownerTable, requestFields, effects.targetTable, effects.targetField, and effects.conditionFields must reference the saved database schema below.\n");
prompt.append("method must be GET, POST, PUT, or DELETE. Prefer POST for command workflows.\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");
prompt.append("Saved database schema:\n");
appendSavedDatabaseSchema(prompt, database);
return prompt.toString();
}
private void appendSavedDatabaseSchema(StringBuilder prompt, DatabaseDesignResponse database)
{
if (database == null || database.getTables() == null)
{
return;
}
for (DatabaseTableDesign table : database.getTables())
{
if (table == null)
{
continue;
}
prompt.append("- ").append(table.getTableName()).append(" (")
.append(StringUtils.defaultString(table.getTableComment())).append(")\n");
if (table.getColumns() == null)
{
continue;
}
for (DatabaseColumnDesign column : table.getColumns())
{
if (column == null)
{
continue;
}
prompt.append(" - ").append(column.getColumnName()).append(" ")
.append(StringUtils.defaultString(column.getColumnType())).append(" ")
.append(StringUtils.defaultString(column.getColumnComment())).append("\n");
}
}
}
private String firstNonBlank(String first, String second)
{
return StringUtils.isNotBlank(first) ? first : StringUtils.defaultString(second);
}
private AppBlueprintDesign parseAppBlueprintResponse(String aiContent)
{
String content = stripMarkdownFence(aiContent);
@@ -356,6 +444,29 @@ public class AiGenerateServiceImpl implements IAiGenerateService
return response;
}
private DatabaseDesignResponse parseBusinessBlueprintResponse(String aiContent, DatabaseDesignResponse savedDatabase)
{
String content = stripMarkdownFence(aiContent);
JSONObject json = JSON.parseObject(content);
if (json == null)
{
throw new ServiceException("AI business blueprint is empty");
}
DatabaseDesignResponse response = new DatabaseDesignResponse();
response.setProjectId(savedDatabase.getProjectId());
response.setTables(savedDatabase.getTables());
JSONArray actions = json.getJSONArray("businessActions");
if (actions == null)
{
response.setBusinessActions(new ArrayList<BusinessActionDesign>());
}
else
{
response.setBusinessActions(actions.toJavaList(BusinessActionDesign.class));
}
return response;
}
private void normalizeResponseAliases(JSONObject response)
{
if (response == null)
@@ -512,6 +623,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
Set<String> actionCodes = new HashSet<String>();
Set<String> columnNames = collectColumnNames(response);
Map<String, Set<String>> columnNamesByTable = collectColumnNamesByTable(response);
List<BusinessActionDesign> normalizedActions = new ArrayList<BusinessActionDesign>();
int actionIndex = 1;
for (BusinessActionDesign action : response.getBusinessActions())
{
@@ -528,9 +640,53 @@ public class AiGenerateServiceImpl implements IAiGenerateService
action.setTransaction(action.getTransaction() == null ? Boolean.TRUE : action.getTransaction());
action.setRequestFields(normalizeExistingFieldList(action.getRequestFields(), columnNames));
action.setRules(trimStringList(action.getRules(), 200));
normalizeBusinessRuleChecks(action, columnNames, columnNamesByTable);
normalizeBusinessEffects(action, columnNames, columnNamesByTable);
if (!isCrudBusinessAction(action))
{
normalizedActions.add(action);
}
actionIndex++;
}
response.setBusinessActions(normalizedActions);
}
private boolean isCrudBusinessAction(BusinessActionDesign action)
{
String code = StringUtils.defaultString(action.getCode()).toLowerCase(Locale.ENGLISH);
String name = normalizeDatabaseName(action.getName(), "action");
String path = StringUtils.defaultString(action.getPath()).toLowerCase(Locale.ENGLISH);
return startsWithCrudToken(code) || startsWithCrudToken(name) || containsCrudPathToken(path);
}
private boolean startsWithCrudToken(String value)
{
String[] tokens = new String[] {"create", "add", "new", "read", "query", "list", "page", "get", "view",
"detail", "edit", "update", "delete", "remove", "import", "export"};
String normalized = StringUtils.defaultString(value).toLowerCase(Locale.ENGLISH);
for (String token : tokens)
{
if (normalized.equals(token) || normalized.startsWith(token + "_") || normalized.startsWith(token + "-"))
{
return true;
}
}
return false;
}
private boolean containsCrudPathToken(String path)
{
String[] tokens = new String[] {"create", "add", "list", "page", "detail", "edit", "update", "delete",
"remove", "import", "export"};
String normalized = StringUtils.defaultString(path).toLowerCase(Locale.ENGLISH);
for (String token : tokens)
{
if (normalized.endsWith("/" + token) || normalized.contains("/" + token + "/"))
{
return true;
}
}
return false;
}
private String uniqueBusinessCode(String rawCode, int index, Set<String> usedCodes)
@@ -663,6 +819,57 @@ public class AiGenerateServiceImpl implements IAiGenerateService
return normalizedValues;
}
private void normalizeBusinessRuleChecks(BusinessActionDesign action, Set<String> columnNames, Map<String, Set<String>> columnNamesByTable)
{
if (action.getRuleChecks() == null)
{
action.setRuleChecks(new ArrayList<BusinessActionRuleDesign>());
return;
}
List<BusinessActionRuleDesign> normalizedRules = new ArrayList<BusinessActionRuleDesign>();
int ruleIndex = 1;
for (BusinessActionRuleDesign rule : action.getRuleChecks())
{
if (rule == null)
{
continue;
}
rule.setType(normalizeRuleType(rule.getType(), ruleIndex));
rule.setTargetTable(StringUtils.isBlank(rule.getTargetTable()) ? "" : normalizeDatabaseName(rule.getTargetTable(), "table_" + ruleIndex));
String rawTargetField = StringUtils.isBlank(rule.getTargetField()) ? "" : normalizeDatabaseName(rule.getTargetField(), "field_" + ruleIndex);
rule.setTargetField(resolveStrictTargetField(rawTargetField, rule.getTargetTable(), columnNamesByTable, columnNames));
rule.setExpectedValue(trimToLength(rule.getExpectedValue(), 200));
rule.setExpectedValues(trimStringList(rule.getExpectedValues(), 200));
rule.setCompareValue(trimToLength(rule.getCompareValue(), 80));
rule.setConditionFields(normalizeExistingFieldList(rule.getConditionFields(), columnNames));
rule.setMessage(trimToLength(rule.getMessage(), 200));
normalizedRules.add(rule);
ruleIndex++;
}
action.setRuleChecks(normalizedRules);
}
private String resolveStrictTargetField(String targetField, String targetTable, Map<String, Set<String>> columnNamesByTable, Set<String> columnNames)
{
if (StringUtils.isBlank(targetField))
{
return "";
}
Set<String> tableColumnNames = columnNamesByTable == null ? null : columnNamesByTable.get(targetTable);
if (tableColumnNames != null)
{
return tableColumnNames.contains(targetField) ? targetField : "";
}
return columnNames != null && columnNames.contains(targetField) ? targetField : "";
}
private String normalizeRuleType(String type, int index)
{
String normalized = StringUtils.defaultString(type).trim().toUpperCase(Locale.ENGLISH);
normalized = normalized.replaceAll("[^A-Z0-9]+", "_").replaceAll("_+", "_").replaceAll("^_+|_+$", "");
return StringUtils.defaultIfEmpty(normalized, "RULE_" + index);
}
private void normalizeBusinessEffects(BusinessActionDesign action, Set<String> columnNames, Map<String, Set<String>> columnNamesByTable)
{
if (action.getEffects() == null)
@@ -689,11 +896,15 @@ public class AiGenerateServiceImpl implements IAiGenerateService
}
effect.setTargetField(targetField);
effect.setValue(trimToLength(effect.getValue(), 200));
effect.setValues(normalizeEffectValues(effect.getValues(), effect.getTargetTable(), columnNamesByTable, columnNames));
effect.setAmount(trimToLength(effect.getAmount(), 80));
effect.setMinValue(trimToLength(effect.getMinValue(), 80));
if (StringUtils.isNotBlank(rawTargetField) && StringUtils.isNotBlank(targetField) && !rawTargetField.equals(targetField))
{
effect.setValue(rewriteEffectValue(effect.getValue(), rawTargetField, targetField));
}
effect.setConditionFields(normalizeExistingFieldList(effect.getConditionFields(), columnNames));
effect.setMessage(trimToLength(effect.getMessage(), 200));
effect.setDescription(trimToLength(effect.getDescription(), 200));
normalizedEffects.add(effect);
effectIndex++;
@@ -701,6 +912,30 @@ public class AiGenerateServiceImpl implements IAiGenerateService
action.setEffects(normalizedEffects);
}
private Map<String, String> normalizeEffectValues(Map<String, String> values, String targetTable,
Map<String, Set<String>> columnNamesByTable, Set<String> columnNames)
{
Map<String, String> normalizedValues = new LinkedHashMap<String, String>();
if (values == null)
{
return normalizedValues;
}
Set<String> tableColumnNames = columnNamesByTable == null ? null : columnNamesByTable.get(targetTable);
int valueIndex = 1;
for (Map.Entry<String, String> entry : values.entrySet())
{
String fieldName = normalizeDatabaseName(entry.getKey(), "field_" + valueIndex);
boolean exists = tableColumnNames != null ? tableColumnNames.contains(fieldName)
: columnNames != null && columnNames.contains(fieldName);
if (exists)
{
normalizedValues.put(fieldName, trimToLength(entry.getValue(), 200));
}
valueIndex++;
}
return normalizedValues;
}
private String resolveEffectTargetField(String targetField, String targetTable, Map<String, Set<String>> columnNamesByTable, Set<String> columnNames)
{
if (StringUtils.isBlank(targetField))
@@ -1052,6 +1287,43 @@ public class AiGenerateServiceImpl implements IAiGenerateService
}
}
private DatabaseDesignResponse loadSavedDatabaseDesign(Long projectId)
{
List<FrontProjectTable> savedTables = frontProjectTableMapper.selectTablesByProjectId(projectId);
if (savedTables == null || savedTables.isEmpty())
{
throw new ServiceException("Please save database design before generating business blueprint");
}
DatabaseDesignResponse response = new DatabaseDesignResponse();
response.setProjectId(projectId);
List<DatabaseTableDesign> tables = new ArrayList<DatabaseTableDesign>();
for (FrontProjectTable savedTable : savedTables)
{
if (savedTable == null)
{
continue;
}
DatabaseTableDesign table = toDatabaseTableDesign(savedTable);
List<FrontProjectColumn> savedColumns = frontProjectColumnMapper.selectColumnsByTableId(savedTable.getTableId());
List<DatabaseColumnDesign> columns = new ArrayList<DatabaseColumnDesign>();
if (savedColumns != null)
{
for (FrontProjectColumn savedColumn : savedColumns)
{
if (savedColumn != null)
{
columns.add(toDatabaseColumnDesign(savedColumn));
}
}
}
table.setColumns(columns);
tables.add(table);
}
response.setTables(tables);
response.setBusinessActions(new ArrayList<BusinessActionDesign>());
return response;
}
private void persistDatabase(Long userId, Long projectId, DatabaseDesignResponse response)
{
frontProjectColumnMapper.deleteColumnsByProjectId(projectId);
@@ -1068,20 +1340,28 @@ public class AiGenerateServiceImpl implements IAiGenerateService
}
}
private void markDatabaseGenerated(Long projectId, DatabaseDesignResponse response, String confirmedAppBlueprint, GenerateDatabaseRequest request)
private void markDatabaseGenerated(Long projectId, GenerateDatabaseRequest request)
{
FrontProject project = new FrontProject();
project.setProjectId(projectId);
project.setGenerateStatus("1");
project.setPreviewStatus("0");
if (StringUtils.isNotBlank(confirmedAppBlueprint))
{
project.setAppBlueprint(confirmedAppBlueprint);
}
project.setRequirementKeyword(request == null ? null : request.getExtraRequirements());
project.setProjectDesc(request == null ? null : request.getProjectDesc());
project.setIndustryTemplate(request == null ? null : request.getIndustryTemplate());
project.setBusinessBlueprint(JSON.toJSONString(response.getBusinessActions()));
project.setBusinessBlueprint("[]");
frontProjectMapper.updateFrontProject(project);
}
private void markBusinessBlueprintGenerated(Long projectId, List<BusinessActionDesign> actions, GenerateBusinessBlueprintRequest request)
{
FrontProject project = new FrontProject();
project.setProjectId(projectId);
project.setPreviewStatus("0");
project.setRequirementKeyword(request == null ? null : request.getExtraRequirements());
project.setProjectDesc(request == null ? null : request.getProjectDesc());
project.setIndustryTemplate(request == null ? null : request.getIndustryTemplate());
project.setBusinessBlueprint(JSON.toJSONString(actions == null ? new ArrayList<BusinessActionDesign>() : actions));
frontProjectMapper.updateFrontProject(project);
}
@@ -1124,6 +1404,49 @@ public class AiGenerateServiceImpl implements IAiGenerateService
return sanitized.substring(0, MAX_ERROR_MESSAGE_LENGTH);
}
private DatabaseTableDesign toDatabaseTableDesign(FrontProjectTable source)
{
DatabaseTableDesign table = new DatabaseTableDesign();
table.setTableId(source.getTableId());
table.setTableName(source.getTableName());
table.setTableComment(source.getTableComment());
table.setClassName(source.getClassName());
table.setModuleName(source.getModuleName());
table.setBusinessName(source.getBusinessName());
table.setFunctionName(source.getFunctionName());
table.setTplCategory(source.getTplCategory());
table.setTplWebType(source.getTplWebType());
table.setOptions(source.getOptions());
table.setCreateTableSql(source.getCreateTableSql());
table.setSort(source.getSort());
return table;
}
private DatabaseColumnDesign toDatabaseColumnDesign(FrontProjectColumn source)
{
DatabaseColumnDesign column = new DatabaseColumnDesign();
column.setColumnId(source.getColumnId());
column.setTableId(source.getTableId());
column.setColumnName(source.getColumnName());
column.setColumnComment(source.getColumnComment());
column.setColumnType(source.getColumnType());
column.setJavaType(source.getJavaType());
column.setJavaField(source.getJavaField());
column.setIsPk(source.getIsPk());
column.setIsIncrement(source.getIsIncrement());
column.setIsRequired(source.getIsRequired());
column.setIsInsert(source.getIsInsert());
column.setIsEdit(source.getIsEdit());
column.setIsList(source.getIsList());
column.setIsQuery(source.getIsQuery());
column.setQueryType(source.getQueryType());
column.setHtmlType(source.getHtmlType());
column.setDictType(source.getDictType());
column.setDefaultValue(source.getDefaultValue());
column.setSort(source.getSort());
return column;
}
private FrontProjectTable toFrontProjectTable(Long projectId, Long userId, DatabaseTableDesign source)
{
FrontProjectTable table = new FrontProjectTable();

View File

@@ -30,7 +30,9 @@ public class FrontProjectConverter
genProject.setProjectDesc(project.getProjectDesc());
genProject.setFrontFramework(project.getFrontFramework());
genProject.setBackFramework(project.getBackFramework());
genProject.setFrontendEnabled(project.getFrontendEnabled());
genProject.setStatus(project.getStatus());
genProject.setModules(project.getModules());
genProject.setTables(toGenTables(project.getTables(), project.getPackageName()));
AppBlueprintDesign appBlueprint = parseAppBlueprint(project.getAppBlueprint());
genProject.setAppBlueprint(appBlueprint);

View File

@@ -40,6 +40,7 @@ public class FrontProjectPreviewServiceImpl implements IFrontProjectPreviewServi
public List<Map<String, Object>> getStructure(Long userId, Long projectId, String templateType)
{
GenProject genProject = loadGenProject(userId, projectId);
assertTemplateTypeEnabled(genProject, templateType);
return genProjectService.getProjectStructure(genProject, templateType);
}
@@ -51,6 +52,7 @@ public class FrontProjectPreviewServiceImpl implements IFrontProjectPreviewServi
throw new ServiceException("预览文件参数不能为空");
}
GenProject genProject = loadGenProject(userId, projectId);
assertTemplateTypeEnabled(genProject, request.getTemplateType());
return genProjectService.previewCode(genProject, request.getTableId(), request.getCategory(), request.getTemplateType());
}
@@ -58,6 +60,7 @@ public class FrontProjectPreviewServiceImpl implements IFrontProjectPreviewServi
public byte[] download(Long userId, Long projectId, String templateType)
{
GenProject genProject = loadGenProject(userId, projectId);
assertTemplateTypeEnabled(genProject, templateType);
return genProjectService.downloadStructure(genProject, templateType);
}
@@ -69,7 +72,10 @@ public class FrontProjectPreviewServiceImpl implements IFrontProjectPreviewServi
try (ZipOutputStream zip = new ZipOutputStream(outputStream))
{
appendZip(zip, "backend", genProjectService.downloadStructure(genProject, "backend"));
appendZip(zip, "front-web", genProjectService.downloadStructure(genProject, "frontend"));
if (isFrontendEnabled(genProject))
{
appendZip(zip, "front-web", genProjectService.downloadStructure(genProject, "frontend"));
}
appendZip(zip, "admin-web", genProjectService.downloadStructure(genProject, "admin_frontend"));
}
catch (IOException e)
@@ -108,6 +114,19 @@ public class FrontProjectPreviewServiceImpl implements IFrontProjectPreviewServi
return FrontProjectConverter.toGenProject(frontProject);
}
private void assertTemplateTypeEnabled(GenProject project, String templateType)
{
if ("frontend".equals(templateType) && !isFrontendEnabled(project))
{
throw new ServiceException("该项目未启用前台前端");
}
}
private boolean isFrontendEnabled(GenProject project)
{
return project == null || !"0".equals(project.getFrontendEnabled());
}
private void appendZip(ZipOutputStream targetZip, String rootFolder, byte[] sourceData) throws IOException
{
ZipInputStream sourceZip = new ZipInputStream(new ByteArrayInputStream(sourceData));

View File

@@ -1,8 +1,10 @@
package com.ruoyi.generator.service.front;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -11,9 +13,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.SysModule;
import com.ruoyi.generator.domain.SysProjectModule;
import com.ruoyi.generator.domain.front.FrontProject;
import com.ruoyi.generator.domain.front.FrontProjectColumn;
import com.ruoyi.generator.domain.front.FrontProjectModule;
import com.ruoyi.generator.domain.front.FrontProjectTable;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
@@ -25,7 +31,9 @@ import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
import com.ruoyi.generator.mapper.front.FrontProjectFileMapper;
import com.ruoyi.generator.mapper.front.FrontProjectGenerationMapper;
import com.ruoyi.generator.mapper.front.FrontProjectMapper;
import com.ruoyi.generator.mapper.front.FrontProjectModuleMapper;
import com.ruoyi.generator.mapper.front.FrontProjectTableMapper;
import com.ruoyi.generator.service.ISysModuleService;
@Service
public class FrontProjectServiceImpl implements IFrontProjectService
@@ -49,8 +57,13 @@ public class FrontProjectServiceImpl implements IFrontProjectService
private FrontProjectFileMapper frontProjectFileMapper;
@Autowired
private FrontProjectGenerationMapper frontProjectGenerationMapper;
@Autowired
private FrontProjectModuleMapper frontProjectModuleMapper;
@Autowired
private ISysModuleService sysModuleService;
@Override
@Transactional
public FrontProject createProject(Long userId, FrontProjectCreateRequest request)
{
assertFrontUserLoggedIn(userId);
@@ -69,10 +82,13 @@ public class FrontProjectServiceImpl implements IFrontProjectService
project.setProjectFileName(toProjectFileName(request.getProjectName()));
project.setFrontFramework("Vue3 + Element Plus");
project.setBackFramework("Spring Boot + MyBatis");
project.setFrontendEnabled(toFrontendEnabled(request.getFrontendEnabled()));
project.setGenerateStatus("0");
project.setPreviewStatus("0");
project.setStatus("0");
frontProjectMapper.insertFrontProject(project);
saveProjectModules(project.getProjectId(), request.getModuleIds());
attachProjectModules(project);
return project;
}
@@ -82,7 +98,15 @@ public class FrontProjectServiceImpl implements IFrontProjectService
assertFrontUserLoggedIn(userId);
FrontProject query = new FrontProject();
query.setUserId(userId);
return frontProjectMapper.selectFrontProjectList(query);
List<FrontProject> projects = frontProjectMapper.selectFrontProjectList(query);
if (projects != null)
{
for (FrontProject project : projects)
{
attachProjectModules(project);
}
}
return projects;
}
@Override
@@ -94,10 +118,12 @@ public class FrontProjectServiceImpl implements IFrontProjectService
{
throw new ServiceException("项目不存在或无权限");
}
attachProjectModules(project);
return project;
}
@Override
@Transactional
public int updateProject(Long userId, Long projectId, FrontProjectUpdateRequest request)
{
assertFrontUserLoggedIn(userId);
@@ -115,7 +141,13 @@ public class FrontProjectServiceImpl implements IFrontProjectService
project.setIndustryTemplate(request.getIndustryTemplate());
project.setPackageName(request.getPackageName());
project.setVersion(request.getVersion());
return frontProjectMapper.updateFrontProject(project);
project.setFrontendEnabled(toNullableFrontendEnabled(request.getFrontendEnabled()));
int rows = frontProjectMapper.updateFrontProject(project);
if (request.getModuleIds() != null)
{
saveProjectModules(projectId, request.getModuleIds());
}
return rows;
}
@Override
@@ -126,6 +158,7 @@ public class FrontProjectServiceImpl implements IFrontProjectService
getProject(userId, projectId);
frontProjectFileMapper.deleteFilesByProjectId(projectId);
frontProjectGenerationMapper.deleteGenerationsByProjectId(projectId);
frontProjectModuleMapper.deleteModulesByProjectId(projectId);
frontProjectColumnMapper.deleteColumnsByProjectId(projectId);
frontProjectTableMapper.deleteTablesByProjectId(projectId);
return frontProjectMapper.deleteFrontProjectByUserAndId(userId, projectId);
@@ -180,6 +213,15 @@ public class FrontProjectServiceImpl implements IFrontProjectService
return getDatabase(userId, projectId);
}
@Override
public List<SysModule> listAvailableModules()
{
SysModule query = new SysModule();
query.setStatus("0");
query.setDelFlag("0");
return sysModuleService.selectSysModuleList(query);
}
private void assertFrontUserLoggedIn(Long userId)
{
if (userId == null)
@@ -202,6 +244,65 @@ public class FrontProjectServiceImpl implements IFrontProjectService
return fileName;
}
private String toFrontendEnabled(Boolean frontendEnabled)
{
return Boolean.FALSE.equals(frontendEnabled) ? "0" : "1";
}
private String toNullableFrontendEnabled(Boolean frontendEnabled)
{
return frontendEnabled == null ? null : toFrontendEnabled(frontendEnabled);
}
private void attachProjectModules(FrontProject project)
{
if (project == null || project.getProjectId() == null || frontProjectModuleMapper == null)
{
return;
}
List<SysProjectModule> modules = frontProjectModuleMapper.selectSysProjectModulesByProjectId(project.getProjectId());
project.setModules(modules);
List<Long> moduleIds = new ArrayList<Long>();
if (modules != null)
{
for (SysProjectModule module : modules)
{
if (module != null && module.getModuleId() != null)
{
moduleIds.add(module.getModuleId());
}
}
}
project.setModuleIds(moduleIds);
}
private void saveProjectModules(Long projectId, List<Long> moduleIds)
{
if (projectId == null)
{
return;
}
frontProjectModuleMapper.deleteModulesByProjectId(projectId);
if (moduleIds == null || moduleIds.isEmpty())
{
return;
}
Set<Long> uniqueModuleIds = new LinkedHashSet<Long>(moduleIds);
for (Long moduleId : uniqueModuleIds)
{
if (moduleId == null)
{
continue;
}
FrontProjectModule module = new FrontProjectModule();
module.setProjectId(projectId);
module.setModuleId(moduleId);
module.setStatus("0");
module.setCreateTime(DateUtils.getNowDate());
frontProjectModuleMapper.insertFrontProjectModule(module);
}
}
private void validateDatabaseDesign(DatabaseDesignResponse request)
{
if (request == null || request.getTables() == null || request.getTables().isEmpty())

View File

@@ -1,8 +1,11 @@
package com.ruoyi.generator.service.front;
import java.util.List;
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
import com.ruoyi.generator.domain.front.dto.GenerateBusinessBlueprintRequest;
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
public interface IAiGenerateService
@@ -10,4 +13,6 @@ public interface IAiGenerateService
AppBlueprintDesign generateAppBlueprint(Long userId, Long projectId, GenerateAppBlueprintRequest request);
DatabaseDesignResponse generateDatabase(Long userId, Long projectId, GenerateDatabaseRequest request);
List<BusinessActionDesign> generateBusinessBlueprint(Long userId, Long projectId, GenerateBusinessBlueprintRequest request);
}

View File

@@ -1,6 +1,7 @@
package com.ruoyi.generator.service.front;
import java.util.List;
import com.ruoyi.generator.domain.SysModule;
import com.ruoyi.generator.domain.front.FrontProject;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.FrontProjectCreateRequest;
@@ -15,4 +16,5 @@ public interface IFrontProjectService
public int deleteProject(Long userId, Long projectId);
public DatabaseDesignResponse getDatabase(Long userId, Long projectId);
public DatabaseDesignResponse saveDatabase(Long userId, Long projectId, DatabaseDesignResponse request);
public List<SysModule> listAvailableModules();
}

View File

@@ -16,6 +16,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="industryTemplate" column="industry_template" />
<result property="frontFramework" column="front_framework" />
<result property="backFramework" column="back_framework" />
<result property="frontendEnabled" column="frontend_enabled" />
<result property="generateStatus" column="generate_status" />
<result property="previewStatus" column="preview_status" />
<result property="appBlueprint" column="app_blueprint" />
@@ -28,7 +29,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, requirement_keyword, 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
select project_id, user_id, project_name, project_file_name, package_name, version, project_desc, requirement_keyword, industry_template, front_framework, back_framework, frontend_enabled, 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">
@@ -67,6 +68,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="industryTemplate != null">industry_template,</if>
<if test="frontFramework != null">front_framework,</if>
<if test="backFramework != null">back_framework,</if>
<if test="frontendEnabled != null">frontend_enabled,</if>
<if test="generateStatus != null">generate_status,</if>
<if test="previewStatus != null">preview_status,</if>
<if test="appBlueprint != null">app_blueprint,</if>
@@ -87,6 +89,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="industryTemplate != null">#{industryTemplate},</if>
<if test="frontFramework != null">#{frontFramework},</if>
<if test="backFramework != null">#{backFramework},</if>
<if test="frontendEnabled != null">#{frontendEnabled},</if>
<if test="generateStatus != null">#{generateStatus},</if>
<if test="previewStatus != null">#{previewStatus},</if>
<if test="appBlueprint != null">#{appBlueprint},</if>
@@ -110,6 +113,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="industryTemplate != null">industry_template = #{industryTemplate},</if>
<if test="frontFramework != null">front_framework = #{frontFramework},</if>
<if test="backFramework != null">back_framework = #{backFramework},</if>
<if test="frontendEnabled != null">frontend_enabled = #{frontendEnabled},</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>

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.generator.mapper.front.FrontProjectModuleMapper">
<resultMap type="FrontProjectModule" id="FrontProjectModuleResult">
<id property="id" column="id" />
<result property="projectId" column="project_id" />
<result property="moduleId" column="module_id" />
<result property="status" column="status" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<resultMap type="SysProjectModule" id="SysProjectModuleResult">
<id property="id" column="id" />
<result property="projectId" column="project_id" />
<result property="moduleId" column="module_id" />
<result property="status" column="status" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<select id="selectModulesByProjectId" parameterType="Long" resultMap="FrontProjectModuleResult">
select id, project_id, module_id, status, create_time, update_time, remark
from front_project_module
where project_id = #{projectId}
and status = '0'
order by id asc
</select>
<select id="selectSysProjectModulesByProjectId" parameterType="Long" resultMap="SysProjectModuleResult">
select id, project_id, module_id, status, create_time, update_time, remark
from front_project_module
where project_id = #{projectId}
and status = '0'
order by id asc
</select>
<insert id="insertFrontProjectModule" parameterType="FrontProjectModule" useGeneratedKeys="true" keyProperty="id">
insert into front_project_module
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="projectId != null">project_id,</if>
<if test="moduleId != null">module_id,</if>
<if test="status != null">status,</if>
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="projectId != null">#{projectId},</if>
<if test="moduleId != null">#{moduleId},</if>
<if test="status != null">#{status},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
</trim>
</insert>
<delete id="deleteModulesByProjectId" parameterType="Long">
delete from front_project_module where project_id = #{projectId}
</delete>
</mapper>

View File

@@ -29,6 +29,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="moduleAlias != null and moduleAlias != ''"> and module_alias = #{moduleAlias}</if>
<if test="moduleDesc != null and moduleDesc != ''"> and module_desc = #{moduleDesc}</if>
<if test="status != null and status != ''"> and status = #{status}</if>
<if test="delFlag != null and delFlag != ''"> and del_flag = #{delFlag}</if>
</where>
</select>
@@ -92,4 +93,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{moduleId}
</foreach>
</delete>
</mapper>
</mapper>

View File

@@ -13,6 +13,10 @@ import com.ruoyi.generator.domain.TemplateFile;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.mapper.GenProjectMapper;
import com.ruoyi.generator.mapper.GenTableMapper;
import com.ruoyi.generator.util.VelocityInitializer;
import com.ruoyi.generator.util.VelocityUtils;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@@ -20,6 +24,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.lang.reflect.Field;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -183,6 +188,14 @@ public class GenProjectServiceImplTest {
assertFalse(sql.contains("Business action not implemented"));
assertTrue(sql.contains("executeUpdateFieldEffect"));
assertTrue(sql.contains("assertExists"));
assertTrue(sql.contains("assertFieldEquals"));
assertTrue(sql.contains("assertNumberGte"));
assertTrue(sql.contains("insertRow"));
assertTrue(sql.contains("updateFields"));
assertTrue(sql.contains("setStatus"));
assertTrue(sql.contains("increaseNumberField"));
assertTrue(sql.contains("decreaseNumberField"));
assertTrue(sql.contains("jdbcTemplate.update"));
assertTrue(sql.contains("conditionFields"));
}
@@ -204,6 +217,24 @@ public class GenProjectServiceImplTest {
assertFalse(sql.contains("insert into generated_menu"));
}
@Test
public void businessBlueprintSqlVueTemplatesAreVelocityParseable() throws Exception {
String sql = readBusinessBlueprintSql();
GenTable table = table(11L, "sys_user", "User");
table.setPackageName("com.example.demo");
table.setPkColumn(table.getColumns().get(0));
VelocityContext context = VelocityUtils.prepareContext(table);
context.put("hasTableBusinessActions", true);
context.put("tableBusinessActions", Arrays.asList(businessAction("sync_user", "sys_user")));
VelocityInitializer.initVelocity();
for (String templateFileId : Arrays.asList("910108", "910203", "910308", "910309")) {
StringWriter output = new StringWriter();
Velocity.evaluate(context, output, "", extractTemplateSqlContent(sql, templateFileId));
assertTrue(output.toString().contains("sync_user"));
}
}
@Test
public void getProjectStructureForDraftProjectUsesFirstActiveTemplateWithoutLoadingProject() {
GenProject project = project();
@@ -266,6 +297,47 @@ public class GenProjectServiceImplTest {
assertEquals("0", structureCaptor.getValue().getStatus());
}
@Test
public void getProjectStructureReplacesAndGroupsTableScopedModuleFolders() {
GenProject project = project();
project.setFrontDraft(true);
GenTable userTable = table(11L, "sys_user", "User");
GenTable bookTable = table(12L, "book", "Book");
userTable.setModuleName("library");
bookTable.setModuleName("library");
project.setTables(Arrays.asList(userTable, bookTable));
when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(102L, 0L)));
when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
structure(1L, 0L, "{projectName}-admin", "folder", null, null),
structure(2L, 1L, "src", "folder", null, null),
structure(3L, 2L, "api", "folder", null, null),
structure(4L, 3L, "{moduleName}", "folder", 0L, null),
structure(5L, 4L, "{businessName}.js", "file", 0L, "api.js.vm"),
structure(6L, 2L, "views", "folder", null, null),
structure(7L, 6L, "{moduleName}", "folder", 0L, null),
structure(8L, 7L, "{businessName}", "folder", 0L, null),
structure(9L, 8L, "index.vue", "file", 0L, "index.vue.vm")
));
List<Map<String, Object>> structure = service.getProjectStructure(project, "admin_frontend");
Map<String, Object> src = child(structure.get(0), 0);
Map<String, Object> api = child(src, 0);
assertEquals(1, children(api).size());
Map<String, Object> apiModuleFolder = child(api, 0);
assertEquals("library", apiModuleFolder.get("name"));
assertEquals("user.js", child(apiModuleFolder, 0).get("name"));
assertEquals("book.js", child(apiModuleFolder, 1).get("name"));
Map<String, Object> views = child(src, 1);
assertEquals(1, children(views).size());
Map<String, Object> viewsModuleFolder = child(views, 0);
assertEquals("library", viewsModuleFolder.get("name"));
assertEquals("user", child(viewsModuleFolder, 0).get("name"));
assertEquals("book", child(viewsModuleFolder, 1).get("name"));
}
@Test(expected = ServiceException.class)
public void frontDraftPreviewRejectsTableOutsideDraftWithoutMapperFallback() {
GenProject project = project();
@@ -284,9 +356,38 @@ public class GenProjectServiceImplTest {
}
}
@Test
public void frontDraftPreviewAppliesSelectedModuleSnippets() {
GenProject project = project();
project.setFrontDraft(true);
project.setModules(Arrays.asList(projectModule(1L)));
GenTable table = table(11L, "sys_user", "User");
project.setTables(Arrays.asList(table));
SysCodeSnippet selectedModule = snippet(1L, "sys_user", "controller.java.vm", "controllerExtraMethods", "public void selected() {}");
SysCodeSnippet unselectedModule = snippet(2L, "sys_user", "controller.java.vm", "controllerExtraMethods", "public void skipped() {}");
when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(template(101L, 0L)));
when(templateFileService.selectTemplateFilesByTemplateId(101L)).thenReturn(Arrays.asList(
templateFile("controller.java.vm", "class UserController { $controllerExtraMethods }")
));
when(codeSnippetService.selectSysCodeSnippetList(any(SysCodeSnippet.class))).thenReturn(Arrays.asList(selectedModule, unselectedModule));
Map<String, String> preview = service.previewCode(project, 11L, "controller.java.vm", "backend");
String content = preview.get("controller.java.vm");
assertTrue(content.contains("public void selected() {}"));
assertFalse(content.contains("public void skipped() {}"));
verify(sysProjectModuleService, never()).selectSysProjectModuleList(any(SysProjectModule.class));
}
@SuppressWarnings("unchecked")
private Map<String, Object> child(Map<String, Object> node, int index) {
return ((List<Map<String, Object>>) node.get("children")).get(index);
return children(node).get(index);
}
@SuppressWarnings("unchecked")
private List<Map<String, Object>> children(Map<String, Object> node) {
return (List<Map<String, Object>>) node.get("children");
}
private void setField(String name, Object value) throws Exception {
@@ -398,4 +499,24 @@ public class GenProjectServiceImplTest {
action.setPath("/" + code);
return action;
}
private String readBusinessBlueprintSql() throws Exception {
Path sqlPath = Paths.get("sql/business_blueprint_templates.sql");
if (!Files.exists(sqlPath)) {
sqlPath = Paths.get("../sql/business_blueprint_templates.sql");
}
return new String(Files.readAllBytes(sqlPath), StandardCharsets.UTF_8);
}
private String extractTemplateSqlContent(String sql, String templateFileId) {
String marker = "(" + templateFileId + ",";
int rowStart = sql.indexOf(marker);
assertTrue("Template row not found: " + templateFileId, rowStart >= 0);
int contentStart = sql.indexOf("\n'", rowStart);
assertTrue("Template content start not found: " + templateFileId, contentStart >= 0);
contentStart += 2;
int contentEnd = sql.indexOf("\n', 'admin'", contentStart);
assertTrue("Template content end not found: " + templateFileId, contentEnd >= 0);
return sql.substring(contentStart, contentEnd);
}
}

View File

@@ -13,6 +13,7 @@ import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Collections;
import org.junit.Before;
@@ -31,8 +32,10 @@ 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.BusinessActionRuleDesign;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
import com.ruoyi.generator.domain.front.dto.GenerateBusinessBlueprintRequest;
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
import com.ruoyi.generator.mapper.front.FrontProjectGenerationMapper;
@@ -93,15 +96,17 @@ public class AiGenerateServiceImplTest
assertEquals(2, columnCaptor.getAllValues().size());
assertEquals("1", generationCaptor.getValue().getSuccess());
assertTrue(response.getSql().contains("CREATE TABLE `car_info`"));
assertTrue(response.getBusinessActions().isEmpty());
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
verify(deepSeekClient).chat(promptCaptor.capture());
assertTrue(promptCaptor.getValue().contains("businessActions"));
assertTrue(promptCaptor.getValue().contains("conditionFields"));
assertFalse(promptCaptor.getValue().contains("\"businessActions\""));
assertFalse(promptCaptor.getValue().contains("conditionFields"));
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
assertEquals(Long.valueOf(10L), projectCaptor.getValue().getProjectId());
assertEquals("1", projectCaptor.getValue().getGenerateStatus());
assertEquals("0", projectCaptor.getValue().getPreviewStatus());
assertEquals("[]", projectCaptor.getValue().getBusinessBlueprint());
}
@Test
@@ -112,6 +117,11 @@ public class AiGenerateServiceImplTest
AppBlueprintDesign blueprint = service.generateAppBlueprint(7L, 10L, appBlueprintRequest());
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
verify(deepSeekClient).chat(promptCaptor.capture());
assertTrue(promptCaptor.getValue().contains("role.name"));
assertTrue(promptCaptor.getValue().contains("Simplified Chinese"));
assertEquals(2, blueprint.getRoles().size());
assertEquals("reader", blueprint.getRoles().get(0).getCode());
assertEquals("admin", blueprint.getRoles().get(1).getCode());
@@ -152,7 +162,118 @@ public class AiGenerateServiceImplTest
}
@Test
public void generateDatabaseIncludesConfirmedAppBlueprintConstraintsInPrompt()
public void generateBusinessBlueprintUsesSavedDatabaseAndPersistsActions()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
mockSavedLibraryTables();
when(deepSeekClient.chat(anyString())).thenReturn(businessBlueprintOnlyResponse());
List<BusinessActionDesign> actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
assertEquals(1, actions.size());
BusinessActionDesign action = actions.get(0);
assertEquals("borrow_book", action.getCode());
assertEquals("book_borrow", action.getOwnerTable());
assertEquals("book_id", action.getRequestFields().get(0));
assertEquals("UPDATE_FIELD", action.getEffects().get(0).getType());
assertEquals("stock", action.getEffects().get(0).getTargetField());
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
verify(deepSeekClient).chat(promptCaptor.capture());
String prompt = promptCaptor.getValue();
assertTrue(prompt.contains("Saved database schema"));
assertTrue(prompt.contains("book_info"));
assertTrue(prompt.contains("stock"));
assertTrue(prompt.contains("Do not generate CRUD"));
assertTrue(prompt.contains("Do not generate create, read, update, delete"));
assertTrue(prompt.contains("businessActions.name"));
assertTrue(prompt.contains("rules"));
assertTrue(prompt.contains("effects.description"));
assertTrue(prompt.contains("Simplified Chinese"));
assertTrue(prompt.contains("Keep API paths and request parameter fields as technical identifiers"));
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
assertEquals(Long.valueOf(10L), projectCaptor.getValue().getProjectId());
assertTrue(projectCaptor.getValue().getBusinessBlueprint().contains("borrow_book"));
assertEquals("0", projectCaptor.getValue().getPreviewStatus());
ArgumentCaptor<FrontProjectGeneration> generationCaptor = ArgumentCaptor.forClass(FrontProjectGeneration.class);
verify(frontProjectGenerationMapper).insertFrontProjectGeneration(generationCaptor.capture());
assertEquals("business_blueprint", generationCaptor.getValue().getGenerateType());
assertEquals("1", generationCaptor.getValue().getSuccess());
}
@Test
public void generateBusinessBlueprintParsesExecutableRulesAndWorkflowEffects()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
mockSavedLibraryTables();
when(deepSeekClient.chat(anyString())).thenReturn(executableBusinessBlueprintResponse());
List<BusinessActionDesign> actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
assertEquals(1, actions.size());
BusinessActionDesign action = actions.get(0);
assertEquals("borrow_book", action.getCode());
assertEquals(2, action.getRuleChecks().size());
BusinessActionRuleDesign existsRule = action.getRuleChecks().get(0);
assertEquals("EXISTS", existsRule.getType());
assertEquals("book_info", existsRule.getTargetTable());
assertEquals("book_id", existsRule.getConditionFields().get(0));
BusinessActionRuleDesign stockRule = action.getRuleChecks().get(1);
assertEquals("NUMBER_GTE", stockRule.getType());
assertEquals("stock", stockRule.getTargetField());
assertEquals("1", stockRule.getCompareValue());
BusinessActionEffectDesign insertEffect = action.getEffects().get(0);
assertEquals("INSERT_ROW", insertEffect.getType());
assertEquals("book_borrow", insertEffect.getTargetTable());
assertEquals("${param.book_id}", insertEffect.getValues().get("book_id"));
assertEquals("borrowed", insertEffect.getValues().get("borrow_status"));
BusinessActionEffectDesign decreaseEffect = action.getEffects().get(1);
assertEquals("DECREASE_NUMBER", decreaseEffect.getType());
assertEquals("book_info", decreaseEffect.getTargetTable());
assertEquals("stock", decreaseEffect.getTargetField());
assertEquals("1", decreaseEffect.getAmount());
assertEquals("0", decreaseEffect.getMinValue());
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
verify(deepSeekClient).chat(promptCaptor.capture());
assertTrue(promptCaptor.getValue().contains("ruleChecks"));
assertTrue(promptCaptor.getValue().contains("INSERT_ROW"));
assertTrue(promptCaptor.getValue().contains("DECREASE_NUMBER"));
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
String persisted = projectCaptor.getValue().getBusinessBlueprint();
assertTrue(persisted.contains("ruleChecks"));
assertTrue(persisted.contains("INSERT_ROW"));
assertTrue(persisted.contains("DECREASE_NUMBER"));
}
@Test
public void generateBusinessBlueprintDropsCrudActionsReturnedByAi()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
mockSavedLibraryTables();
when(deepSeekClient.chat(anyString())).thenReturn(businessBlueprintWithCrudActionsResponse());
List<BusinessActionDesign> actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
assertEquals(1, actions.size());
assertEquals("borrow_book", actions.get(0).getCode());
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
assertTrue(projectCaptor.getValue().getBusinessBlueprint().contains("borrow_book"));
assertFalse(projectCaptor.getValue().getBusinessBlueprint().contains("create_book"));
}
@Test
public void generateDatabaseIncludesConfirmedAppBlueprintConstraintsButNoBusinessActions()
{
FrontProject project = project();
project.setAppBlueprint("{\"frontendMenus\":[{\"code\":\"my_borrows\",\"requiresLogin\":true,\"dataScope\":\"CURRENT_USER\"}],\"adminMenus\":[]}");
@@ -168,6 +289,9 @@ public class AiGenerateServiceImplTest
assertTrue(prompt.contains("CURRENT_USER"));
assertTrue(prompt.contains("Do not generate menu or permission tables"));
assertTrue(prompt.contains("user_id"));
assertFalse(prompt.contains("\"businessActions\""));
assertFalse(prompt.contains("conditionFields"));
assertFalse(prompt.contains("Business actions must align"));
}
@Test
@@ -280,14 +404,14 @@ public class AiGenerateServiceImplTest
}
@Test
public void generateDatabaseParsesNormalizesAndPersistsBusinessActionDraft()
public void generateBusinessBlueprintParsesNormalizesAndPersistsBusinessActionDraft()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
mockSavedLibraryTables();
when(deepSeekClient.chat(anyString())).thenReturn(responseWithBusinessActions());
DatabaseDesignResponse response = service.generateDatabase(7L, 10L, request());
List<BusinessActionDesign> actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
List<BusinessActionDesign> actions = response.getBusinessActions();
assertEquals(1, actions.size());
BusinessActionDesign action = actions.get(0);
assertEquals("borrow_book", action.getCode());
@@ -310,14 +434,15 @@ public class AiGenerateServiceImplTest
}
@Test
public void generateDatabaseDropsMissingBusinessActionRequestFields()
public void generateBusinessBlueprintDropsMissingBusinessActionRequestFields()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
mockSavedFineTables();
when(deepSeekClient.chat(anyString())).thenReturn(responseWithMissingBusinessActionRequestField());
DatabaseDesignResponse response = service.generateDatabase(7L, 10L, request());
List<BusinessActionDesign> actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
List<String> requestFields = response.getBusinessActions().get(0).getRequestFields();
List<String> requestFields = actions.get(0).getRequestFields();
assertEquals(1, requestFields.size());
assertEquals("reader_id", requestFields.get(0));
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
@@ -326,14 +451,15 @@ public class AiGenerateServiceImplTest
}
@Test
public void generateDatabaseRepairsMissingBusinessEffectTargetField()
public void generateBusinessBlueprintRepairsMissingBusinessEffectTargetField()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
mockSavedEffectRepairTables();
when(deepSeekClient.chat(anyString())).thenReturn(responseWithMissingBusinessEffectTargetField());
DatabaseDesignResponse response = service.generateDatabase(7L, 10L, request());
List<BusinessActionDesign> actions = service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
BusinessActionEffectDesign effect = response.getBusinessActions().get(0).getEffects().get(0);
BusinessActionEffectDesign effect = actions.get(0).getEffects().get(0);
assertEquals("stock", effect.getTargetField());
assertEquals("stock - 1", effect.getValue());
@@ -344,14 +470,15 @@ public class AiGenerateServiceImplTest
}
@Test
public void generateDatabaseRejectsBusinessActionReferencingMissingTable()
public void generateBusinessBlueprintRejectsBusinessActionReferencingMissingTable()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
mockSavedBookInfoOnly();
when(deepSeekClient.chat(anyString())).thenReturn(responseWithMissingBusinessActionTable());
try
{
service.generateDatabase(7L, 10L, request());
service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
fail("Expected invalid business action table");
}
catch (ServiceException e)
@@ -363,6 +490,42 @@ public class AiGenerateServiceImplTest
verify(frontProjectTableMapper, never()).deleteTablesByProjectId(10L);
}
@Test
public void generateBusinessBlueprintRejectsRuleCheckWithMissingTargetField()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
mockSavedLibraryTables();
when(deepSeekClient.chat(anyString())).thenReturn(ruleCheckWithMissingTargetFieldResponse());
try
{
service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
fail("Expected missing rule target field to be rejected");
}
catch (ServiceException e)
{
assertEquals("Business rule target field is required: NUMBER_GTE", e.getMessage());
}
}
@Test
public void generateBusinessBlueprintRejectsEffectValueWithUnknownParam()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
mockSavedLibraryTables();
when(deepSeekClient.chat(anyString())).thenReturn(effectValueWithUnknownParamResponse());
try
{
service.generateBusinessBlueprint(7L, 10L, businessBlueprintRequest());
fail("Expected unknown value placeholder to be rejected");
}
catch (ServiceException e)
{
assertEquals("Business effect value references missing field: missing_reader_id", e.getMessage());
}
}
@Test
public void generationRecordInsertUsesRequiresNewTransaction() throws Exception
{
@@ -416,6 +579,86 @@ public class AiGenerateServiceImplTest
return request;
}
private GenerateBusinessBlueprintRequest businessBlueprintRequest()
{
GenerateBusinessBlueprintRequest request = new GenerateBusinessBlueprintRequest();
request.setProjectName("Library");
request.setProjectDesc("Readers borrow and return books");
request.setIndustryTemplate("library");
request.setExtraRequirements("Need borrow and return workflows, not CRUD");
return request;
}
private FrontProjectTable savedTable(Long tableId, String tableName, String tableComment)
{
FrontProjectTable table = new FrontProjectTable();
table.setTableId(tableId);
table.setProjectId(10L);
table.setTableName(tableName);
table.setTableComment(tableComment);
return table;
}
private FrontProjectColumn savedColumn(String columnName, String columnType, String columnComment)
{
FrontProjectColumn column = new FrontProjectColumn();
column.setProjectId(10L);
column.setColumnName(columnName);
column.setColumnType(columnType);
column.setColumnComment(columnComment);
return column;
}
private void mockSavedLibraryTables()
{
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Arrays.asList(
savedTable(101L, "book_info", "Book info"),
savedTable(102L, "reader_info", "Reader info"),
savedTable(103L, "book_borrow", "Book borrow")));
when(frontProjectColumnMapper.selectColumnsByTableId(101L)).thenReturn(Arrays.asList(
savedColumn("book_id", "bigint(20)", "Book ID"),
savedColumn("stock", "int(11)", "Stock")));
when(frontProjectColumnMapper.selectColumnsByTableId(102L)).thenReturn(Arrays.asList(
savedColumn("reader_id", "bigint(20)", "Reader ID"),
savedColumn("status", "varchar(20)", "Status")));
when(frontProjectColumnMapper.selectColumnsByTableId(103L)).thenReturn(Arrays.asList(
savedColumn("borrow_id", "bigint(20)", "Borrow ID"),
savedColumn("book_id", "bigint(20)", "Book ID"),
savedColumn("reader_id", "bigint(20)", "Reader ID"),
savedColumn("borrow_status", "varchar(20)", "Borrow status")));
}
private void mockSavedFineTables()
{
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Arrays.asList(
savedTable(201L, "fine_record", "Fine record")));
when(frontProjectColumnMapper.selectColumnsByTableId(201L)).thenReturn(Arrays.asList(
savedColumn("id", "bigint(20)", "ID"),
savedColumn("reader_id", "bigint(20)", "Reader ID"),
savedColumn("fine_amount", "decimal(10,2)", "Fine amount")));
}
private void mockSavedEffectRepairTables()
{
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Arrays.asList(
savedTable(301L, "book_info", "Book info"),
savedTable(302L, "borrow_record", "Borrow record")));
when(frontProjectColumnMapper.selectColumnsByTableId(301L)).thenReturn(Arrays.asList(
savedColumn("book_id", "bigint(20)", "Book ID"),
savedColumn("stock", "int(11)", "Stock")));
when(frontProjectColumnMapper.selectColumnsByTableId(302L)).thenReturn(Arrays.asList(
savedColumn("borrow_id", "bigint(20)", "Borrow ID"),
savedColumn("book_id", "bigint(20)", "Book ID")));
}
private void mockSavedBookInfoOnly()
{
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Arrays.asList(
savedTable(401L, "book_info", "Book info")));
when(frontProjectColumnMapper.selectColumnsByTableId(401L)).thenReturn(Arrays.asList(
savedColumn("book_id", "bigint(20)", "Book ID")));
}
private String repeated(String value, int count)
{
StringBuilder builder = new StringBuilder();
@@ -644,6 +887,73 @@ public class AiGenerateServiceImplTest
+ "}";
}
private String businessBlueprintOnlyResponse()
{
return "{\n"
+ " \"businessActions\": [\n"
+ " {\n"
+ " \"code\": \"Borrow Book\",\n"
+ " \"name\": \"Borrow Book\",\n"
+ " \"ownerTable\": \"book_borrow\",\n"
+ " \"method\": \"post\",\n"
+ " \"path\": \"/library/borrow/borrow\",\n"
+ " \"transaction\": true,\n"
+ " \"requestFields\": [\"book_id\", \"reader_id\"],\n"
+ " \"rules\": [\"Book stock must be greater than zero\"],\n"
+ " \"effects\": [\n"
+ " {\"type\": \"update_field\", \"targetTable\": \"book_info\", \"targetField\": \"stock\", \"value\": \"stock - 1\", \"conditionFields\": [\"book_id\"], \"description\": \"Decrease stock\"}\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}";
}
private String businessBlueprintWithCrudActionsResponse()
{
return "{\n"
+ " \"businessActions\": [\n"
+ " {\"code\": \"create_book\", \"name\": \"Create Book\", \"ownerTable\": \"book_info\", \"method\": \"POST\", \"path\": \"/book/create\", \"requestFields\": [\"book_id\"]},\n"
+ " {\n"
+ " \"code\": \"borrow_book\",\n"
+ " \"name\": \"Borrow Book\",\n"
+ " \"ownerTable\": \"book_borrow\",\n"
+ " \"method\": \"POST\",\n"
+ " \"path\": \"/library/borrow/borrow\",\n"
+ " \"requestFields\": [\"book_id\", \"reader_id\"],\n"
+ " \"effects\": [\n"
+ " {\"type\": \"UPDATE_FIELD\", \"targetTable\": \"book_info\", \"targetField\": \"stock\", \"value\": \"stock - 1\", \"conditionFields\": [\"book_id\"], \"description\": \"Decrease stock\"}\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}";
}
private String executableBusinessBlueprintResponse()
{
return "{\n"
+ " \"businessActions\": [\n"
+ " {\n"
+ " \"code\": \"Borrow Book\",\n"
+ " \"name\": \"Borrow Book\",\n"
+ " \"ownerTable\": \"book_borrow\",\n"
+ " \"method\": \"post\",\n"
+ " \"path\": \"/library/borrow/borrow\",\n"
+ " \"transaction\": true,\n"
+ " \"requestFields\": [\"book_id\", \"reader_id\"],\n"
+ " \"rules\": [\"Book must exist\", \"Stock must be enough\"],\n"
+ " \"ruleChecks\": [\n"
+ " {\"type\": \"exists\", \"targetTable\": \"book_info\", \"conditionFields\": [\"book_id\"], \"message\": \"Book not found\"},\n"
+ " {\"type\": \"number_gte\", \"targetTable\": \"book_info\", \"targetField\": \"stock\", \"compareValue\": \"1\", \"conditionFields\": [\"book_id\"], \"message\": \"Stock is not enough\"}\n"
+ " ],\n"
+ " \"effects\": [\n"
+ " {\"type\": \"insert_row\", \"targetTable\": \"book_borrow\", \"values\": {\"book_id\": \"${param.book_id}\", \"reader_id\": \"${param.reader_id}\", \"borrow_status\": \"borrowed\"}, \"description\": \"Create borrow record\"},\n"
+ " {\"type\": \"decrease_number\", \"targetTable\": \"book_info\", \"targetField\": \"stock\", \"amount\": \"1\", \"minValue\": \"0\", \"conditionFields\": [\"book_id\"], \"message\": \"Stock decrease failed\", \"description\": \"Decrease stock\"}\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}";
}
private String responseWithMissingBusinessActionRequestField()
{
return "{\n"
@@ -694,4 +1004,42 @@ public class AiGenerateServiceImplTest
+ " ]\n"
+ "}";
}
private String ruleCheckWithMissingTargetFieldResponse()
{
return "{\n"
+ " \"businessActions\": [\n"
+ " {\n"
+ " \"code\": \"borrow_book\",\n"
+ " \"name\": \"Borrow Book\",\n"
+ " \"ownerTable\": \"book_borrow\",\n"
+ " \"method\": \"POST\",\n"
+ " \"path\": \"/library/borrow/borrow\",\n"
+ " \"requestFields\": [\"book_id\", \"reader_id\"],\n"
+ " \"ruleChecks\": [\n"
+ " {\"type\": \"NUMBER_GTE\", \"targetTable\": \"book_info\", \"targetField\": \"missing_stock\", \"compareValue\": \"1\", \"conditionFields\": [\"book_id\"], \"message\": \"Stock is not enough\"}\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}";
}
private String effectValueWithUnknownParamResponse()
{
return "{\n"
+ " \"businessActions\": [\n"
+ " {\n"
+ " \"code\": \"borrow_book\",\n"
+ " \"name\": \"Borrow Book\",\n"
+ " \"ownerTable\": \"book_borrow\",\n"
+ " \"method\": \"POST\",\n"
+ " \"path\": \"/library/borrow/borrow\",\n"
+ " \"requestFields\": [\"book_id\", \"reader_id\"],\n"
+ " \"effects\": [\n"
+ " {\"type\": \"INSERT_ROW\", \"targetTable\": \"book_borrow\", \"values\": {\"book_id\": \"${param.book_id}\", \"reader_id\": \"${param.missing_reader_id}\"}, \"description\": \"Create borrow record\"}\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}";
}
}

View File

@@ -1,7 +1,9 @@
package com.ruoyi.generator.service.front;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.generator.domain.GenProject;
import com.ruoyi.generator.domain.GenTable;
import com.ruoyi.generator.domain.SysProjectModule;
import com.ruoyi.generator.domain.front.FrontProject;
import com.ruoyi.generator.domain.front.FrontProjectColumn;
import com.ruoyi.generator.domain.front.FrontProjectTable;
@@ -33,6 +35,8 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
@@ -144,6 +148,7 @@ public class FrontProjectPreviewServiceImplTest
public void downloadAllPackagesBackendFrontendAndAdminFrontend() throws Exception
{
FrontProject project = frontProjectWithOneTable();
project.setFrontendEnabled("1");
project.setPreviewStatus("1");
when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
when(genProjectService.downloadStructure(any(GenProject.class), eq("backend")))
@@ -161,6 +166,64 @@ public class FrontProjectPreviewServiceImplTest
assertTrue(entries.contains("admin-web/demo/admin.txt"));
}
@Test
public void rejectsFrontendStructureWhenProjectDisablesFrontend()
{
FrontProject project = frontProjectWithOneTable();
project.setFrontendEnabled("0");
when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
ServiceException exception = expectServiceException(new ThrowingRunnable()
{
@Override
public void run()
{
previewService.getStructure(100L, 200L, "frontend");
}
});
assertEquals("该项目未启用前台前端", exception.getMessage());
verify(genProjectService, never()).getProjectStructure(any(GenProject.class), eq("frontend"));
}
@Test
public void downloadAllSkipsFrontendWhenProjectDisablesFrontend() throws Exception
{
FrontProject project = frontProjectWithOneTable();
project.setFrontendEnabled("0");
when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
when(genProjectService.downloadStructure(any(GenProject.class), eq("backend")))
.thenReturn(zipWithEntry("demo/backend.txt", "backend"));
when(genProjectService.downloadStructure(any(GenProject.class), eq("admin_frontend")))
.thenReturn(zipWithEntry("demo/admin.txt", "admin"));
byte[] data = previewService.downloadAll(100L, 200L);
Set<String> entries = zipEntries(data);
assertTrue(entries.contains("backend/demo/backend.txt"));
assertTrue(entries.contains("admin-web/demo/admin.txt"));
assertFalse(entries.contains("front-web/demo/frontend.txt"));
verify(genProjectService, never()).downloadStructure(any(GenProject.class), eq("frontend"));
}
@Test
public void passesSelectedModulesIntoDraftProject()
{
FrontProject project = frontProjectWithOneTable();
project.setModules(Arrays.asList(projectModule(11L), projectModule(22L)));
when(frontProjectService.getProject(100L, 200L)).thenReturn(project);
when(genProjectService.getProjectStructure(any(GenProject.class), eq("backend")))
.thenReturn(Collections.<Map<String, Object>>emptyList());
previewService.getStructure(100L, 200L, "backend");
ArgumentCaptor<GenProject> projectCaptor = ArgumentCaptor.forClass(GenProject.class);
verify(genProjectService, times(1)).getProjectStructure(projectCaptor.capture(), eq("backend"));
assertEquals(2, projectCaptor.getValue().getModules().size());
assertEquals(Long.valueOf(11L), projectCaptor.getValue().getModules().get(0).getModuleId());
assertEquals(Long.valueOf(22L), projectCaptor.getValue().getModules().get(1).getModuleId());
}
private void setField(String name, Object value) throws Exception
{
Field field = FrontProjectPreviewServiceImpl.class.getDeclaredField(name);
@@ -175,10 +238,20 @@ public class FrontProjectPreviewServiceImplTest
project.setProjectName("Demo");
project.setProjectFileName("demo");
project.setPackageName("com.example.demo");
project.setFrontendEnabled("1");
project.setTables(Arrays.asList(table()));
return project;
}
private SysProjectModule projectModule(Long moduleId)
{
SysProjectModule module = new SysProjectModule();
module.setProjectId(200L);
module.setModuleId(moduleId);
module.setStatus("0");
return module;
}
private FrontProjectTable table()
{
FrontProjectTable table = new FrontProjectTable();
@@ -224,4 +297,22 @@ public class FrontProjectPreviewServiceImplTest
zip.close();
return entries;
}
private ServiceException expectServiceException(ThrowingRunnable runnable)
{
try
{
runnable.run();
}
catch (ServiceException e)
{
return e;
}
throw new AssertionError("Expected ServiceException");
}
private interface ThrowingRunnable
{
void run();
}
}

View File

@@ -0,0 +1,70 @@
package com.ruoyi.generator.service.front;
import static org.junit.Assert.assertTrue;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.junit.Test;
public class FrontProjectSchemaScriptTest
{
private static final String[] FRONT_PROJECT_DRAFT_COLUMNS = {
"requirement_keyword",
"industry_template",
"frontend_enabled",
"app_blueprint",
"er_diagram",
"business_blueprint"
};
@Test
public void upgradeScriptBackfillsMappedFrontProjectDraftColumns() throws Exception
{
Path repoRoot = repoRoot();
String mapperXml = read(repoRoot.resolve("ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml"));
String upgradeSql = read(repoRoot.resolve("sql/front_project_upgrade.sql")).toLowerCase();
for (String column : FRONT_PROJECT_DRAFT_COLUMNS)
{
assertTrue("FrontProjectMapper should map " + column, mapperXml.contains(column));
assertTrue("front_project_upgrade.sql should check " + column,
upgradeSql.contains("column_name = '" + column + "'"));
assertTrue("front_project_upgrade.sql should add " + column,
upgradeSql.contains("add column " + column));
}
assertTrue("front_project_upgrade.sql should create front_project_module",
upgradeSql.contains("create table if not exists front_project_module"));
assertTrue("front_workbench.sql should create front_project_module",
read(repoRoot.resolve("sql/front_workbench.sql")).contains("create table front_project_module"));
}
@Test
public void dbSqlIncludesBusinessBlueprintTemplates() throws Exception
{
String dbSql = read(repoRoot().resolve("sql/db.sql"));
assertTrue("db.sql should include business blueprint backend template",
dbSql.contains("Business Blueprint Backend"));
assertTrue("db.sql should include executable blueprint effects",
dbSql.contains("executeUpdateFieldEffect"));
assertTrue("db.sql should include frontend menu template",
dbSql.contains("frontendMenus.js.vm"));
}
private Path repoRoot()
{
Path cwd = Paths.get("").toAbsolutePath();
if ("ruoyi-generator".equals(cwd.getFileName().toString()))
{
return cwd.getParent();
}
return cwd;
}
private String read(Path path) throws Exception
{
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
}
}

View File

@@ -24,11 +24,13 @@ 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.FrontProjectCreateRequest;
import com.ruoyi.generator.mapper.front.FrontProjectModuleMapper;
import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
import com.ruoyi.generator.mapper.front.FrontProjectFileMapper;
import com.ruoyi.generator.mapper.front.FrontProjectGenerationMapper;
import com.ruoyi.generator.mapper.front.FrontProjectMapper;
import com.ruoyi.generator.mapper.front.FrontProjectTableMapper;
import com.ruoyi.generator.service.ISysModuleService;
public class FrontProjectServiceImplTest
{
@@ -44,6 +46,10 @@ public class FrontProjectServiceImplTest
private FrontProjectFileMapper frontProjectFileMapper;
@Mock
private FrontProjectGenerationMapper frontProjectGenerationMapper;
@Mock
private FrontProjectModuleMapper frontProjectModuleMapper;
@Mock
private ISysModuleService sysModuleService;
@Before
public void setUp() throws Exception
@@ -55,6 +61,8 @@ public class FrontProjectServiceImplTest
setField("frontProjectColumnMapper", frontProjectColumnMapper);
setField("frontProjectFileMapper", frontProjectFileMapper);
setField("frontProjectGenerationMapper", frontProjectGenerationMapper);
setField("frontProjectModuleMapper", frontProjectModuleMapper);
setField("sysModuleService", sysModuleService);
}
@Test
@@ -73,6 +81,7 @@ public class FrontProjectServiceImplTest
assertEquals("1.0.0", project.getVersion());
assertEquals("Vue3 + Element Plus", project.getFrontFramework());
assertEquals("Spring Boot + MyBatis", project.getBackFramework());
assertEquals("1", project.getFrontendEnabled());
assertEquals("0", project.getGenerateStatus());
assertEquals("0", project.getPreviewStatus());
assertEquals("0", project.getStatus());
@@ -80,6 +89,32 @@ public class FrontProjectServiceImplTest
verify(frontProjectMapper).insertFrontProject(project);
}
@Test
public void createProjectPersistsFrontendSwitchAndSelectedModules()
{
org.mockito.Mockito.doAnswer(invocation -> {
FrontProject inserted = invocation.getArgument(0);
inserted.setProjectId(88L);
return 1;
}).when(frontProjectMapper).insertFrontProject(any(FrontProject.class));
FrontProjectCreateRequest request = new FrontProjectCreateRequest();
request.setProjectName("客户中心");
request.setFrontendEnabled(Boolean.FALSE);
request.setModuleIds(Arrays.asList(11L, 22L));
FrontProject project = service.createProject(7L, request);
assertEquals("0", project.getFrontendEnabled());
verify(frontProjectMapper).insertFrontProject(project);
verify(frontProjectModuleMapper).deleteModulesByProjectId(88L);
ArgumentCaptor<com.ruoyi.generator.domain.front.FrontProjectModule> moduleCaptor =
ArgumentCaptor.forClass(com.ruoyi.generator.domain.front.FrontProjectModule.class);
verify(frontProjectModuleMapper, org.mockito.Mockito.times(2)).insertFrontProjectModule(moduleCaptor.capture());
assertEquals(Long.valueOf(11L), moduleCaptor.getAllValues().get(0).getModuleId());
assertEquals(Long.valueOf(22L), moduleCaptor.getAllValues().get(1).getModuleId());
assertEquals("0", moduleCaptor.getAllValues().get(0).getStatus());
}
@Test
public void saveDatabaseRejectsInvalidTableName()
{