Generate executable business action effects

This commit is contained in:
王鹏
2026-05-23 14:12:09 +08:00
parent c742bf59b8
commit 0fbe78868b
6 changed files with 137 additions and 7 deletions

View File

@@ -1,11 +1,14 @@
package com.ruoyi.generator.domain.front.dto;
import java.util.List;
public class BusinessActionEffectDesign
{
private String type;
private String targetTable;
private String targetField;
private String value;
private List<String> conditionFields;
private String description;
public String getType() { return type; }
@@ -16,6 +19,8 @@ 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 List<String> getConditionFields() { return conditionFields; }
public void setConditionFields(List<String> conditionFields) { this.conditionFields = conditionFields; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
}

View File

@@ -135,6 +135,7 @@ public class AiBusinessBlueprintValidator
{
throw new ServiceException("业务影响目标字段不存在:" + effect.getTargetField());
}
validateEffectConditionFields(effect, columnNames);
if (StringUtils.defaultString(effect.getValue()).length() > 200
|| StringUtils.defaultString(effect.getDescription()).length() > 200)
{
@@ -143,6 +144,21 @@ public class AiBusinessBlueprintValidator
}
}
private void validateEffectConditionFields(BusinessActionEffectDesign effect, Set<String> columnNames)
{
if (StringUtils.isEmpty(effect.getConditionFields()))
{
return;
}
for (String field : effect.getConditionFields())
{
if (StringUtils.isBlank(field) || !columnNames.contains(field))
{
throw new ServiceException("业务影响条件字段不存在:" + StringUtils.defaultString(field));
}
}
}
private Set<String> collectTableNames(List<DatabaseTableDesign> tables)
{
Set<String> tableNames = new HashSet<String>();

View File

@@ -109,9 +109,9 @@ public class AiGenerateServiceImpl implements IAiGenerateService
private String buildPrompt(FrontProject project, GenerateDatabaseRequest request)
{
StringBuilder prompt = new StringBuilder();
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\",\"description\":\"库存减一\"}]}]}。\n");
prompt.append("请为企业管理系统生成MySQL数据库设计和业务蓝图草稿只返回JSON格式为{\"tables\":[{\"tableName\":\"car_info\",\"tableComment\":\"车辆信息表\",\"columns\":[]}],\"businessActions\":[{\"code\":\"borrow_book\",\"name\":\"借书\",\"ownerTable\":\"borrow_record\",\"method\":\"POST\",\"path\":\"/library/borrow/borrow\",\"transaction\":true,\"requestFields\":[\"book_id\"],\"rules\":[\"图书必须可借\"],\"effects\":[{\"type\":\"UPDATE_FIELD\",\"targetTable\":\"book_info\",\"targetField\":\"stock\",\"value\":\"stock - 1\",\"conditionFields\":[\"book_id\"],\"description\":\"库存减一\"}]}]}。\n");
prompt.append("限制表名和字段名使用小写字母、数字、下划线必须且只能有一个主键字段类型只能使用bigint(20)、int(11)、varchar(n)、char(n)、decimal(p,s)、datetime、date、text、longtext。\n");
prompt.append("业务蓝图限制businessActions只描述业务动作草稿不直接返回代码ownerTable、requestFields、effects.targetTable、effects.targetField必须引用已生成的表和字段method只用GET、POST、PUT、DELETE。\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");
@@ -313,7 +313,7 @@ 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));
normalizeBusinessEffects(action);
normalizeBusinessEffects(action, columnNames);
actionIndex++;
}
}
@@ -422,7 +422,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
return normalizedValues;
}
private void normalizeBusinessEffects(BusinessActionDesign action)
private void normalizeBusinessEffects(BusinessActionDesign action, Set<String> columnNames)
{
if (action.getEffects() == null)
{
@@ -440,6 +440,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
effect.setTargetTable(StringUtils.isBlank(effect.getTargetTable()) ? "" : normalizeDatabaseName(effect.getTargetTable(), "table_" + effectIndex));
effect.setTargetField(StringUtils.isBlank(effect.getTargetField()) ? "" : normalizeDatabaseName(effect.getTargetField(), "field_" + effectIndex));
effect.setValue(trimToLength(effect.getValue(), 200));
effect.setConditionFields(normalizeExistingFieldList(effect.getConditionFields(), columnNames));
effect.setDescription(trimToLength(effect.getDescription(), 200));
effectIndex++;
}

View File

@@ -20,6 +20,10 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -169,6 +173,20 @@ public class GenProjectServiceImplTest {
assertFalse(content.contains("return_book"));
}
@Test
public void businessBlueprintSqlTemplateGeneratesExecutableActionEffects() throws Exception {
Path sqlPath = Paths.get("sql/business_blueprint_templates.sql");
if (!Files.exists(sqlPath)) {
sqlPath = Paths.get("../sql/business_blueprint_templates.sql");
}
String sql = new String(Files.readAllBytes(sqlPath), StandardCharsets.UTF_8);
assertFalse(sql.contains("Business action not implemented"));
assertTrue(sql.contains("executeUpdateFieldEffect"));
assertTrue(sql.contains("jdbcTemplate.update"));
assertTrue(sql.contains("conditionFields"));
}
@Test
public void getProjectStructureForDraftProjectUsesFirstActiveTemplateWithoutLoadingProject() {
GenProject project = project();

View File

@@ -94,6 +94,7 @@ public class AiGenerateServiceImplTest
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
verify(deepSeekClient).chat(promptCaptor.capture());
assertTrue(promptCaptor.getValue().contains("businessActions"));
assertTrue(promptCaptor.getValue().contains("conditionFields"));
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
assertEquals(Long.valueOf(10L), projectCaptor.getValue().getProjectId());
@@ -233,6 +234,7 @@ public class AiGenerateServiceImplTest
assertEquals("UPDATE_FIELD", effect.getType());
assertEquals("book_info", effect.getTargetTable());
assertEquals("stock", effect.getTargetField());
assertEquals("book_id", effect.getConditionFields().get(0));
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
@@ -522,7 +524,7 @@ public class AiGenerateServiceImplTest
+ " \"requestFields\": [\"book_id\", \"reader_id\"],\n"
+ " \"rules\": [\"读者状态必须正常\", \"图书库存必须大于0\"],\n"
+ " \"effects\": [\n"
+ " {\"type\": \"update_field\", \"targetTable\": \"book_info\", \"targetField\": \"stock\", \"value\": \"stock - 1\", \"description\": \"图书库存减一\"}\n"
+ " {\"type\": \"update_field\", \"targetTable\": \"book_info\", \"targetField\": \"stock\", \"value\": \"stock - 1\", \"conditionFields\": [\"book_id\"], \"description\": \"图书库存减一\"}\n"
+ " ]\n"
+ " }\n"
+ " ]\n"

View File

@@ -13,7 +13,7 @@ DELETE FROM sys_template_file WHERE template_id IN (9101, 9102, 9103) OR templat
DELETE FROM sys_template WHERE template_id IN (9101, 9102, 9103);
INSERT INTO sys_template (template_id, template_name, template_path, template_desc, template_type, template_status, create_by, create_time, remark) VALUES
(9101, 'Business Blueprint Backend', '/blueprint/backend', 'RuoYi backend with CRUD and blueprint action stubs', 'backend', 0, 'admin', sysdate(), 'business blueprint'),
(9101, 'Business Blueprint Backend', '/blueprint/backend', 'RuoYi backend with CRUD and executable blueprint effects', 'backend', 0, 'admin', sysdate(), 'business blueprint'),
(9102, 'Business Blueprint Admin Frontend', '/blueprint/admin_frontend', 'Vue2 Element UI admin pages with blueprint action buttons', 'admin_frontend', 0, 'admin', sysdate(), 'business blueprint'),
(9103, 'Business Blueprint Portal Frontend', '/blueprint/frontend', 'Vue3 Element Plus portal pages with blueprint action buttons', 'frontend', 0, 'admin', sysdate(), 'business blueprint');
@@ -283,6 +283,7 @@ public interface I${ClassName}Service
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.exception.ServiceException;
@@ -298,6 +299,10 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service
{
@Resource
private ${ClassName}Mapper ${className}Mapper;
#if($hasTableBusinessActions)
@Resource
private JdbcTemplate jdbcTemplate;
#end
@Override
public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField})
@@ -362,10 +367,93 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service
@Override
public int ${action.code}(Map<String, Object> params)
{
throw new ServiceException("Business action not implemented: ${action.name}");
if (params == null) {
throw new ServiceException("业务动作参数不能为空");
}
#if($action.requestFields)
#foreach($field in $action.requestFields)
requireBusinessParam(params, "${field}");
#end
#end
int rows = 0;
#if($action.effects)
#foreach($effect in $action.effects)
#if($effect.type == "UPDATE_FIELD")
#set($conditionFields = $effect.conditionFields)
#if(!$conditionFields || $conditionFields.size() == 0)
#set($conditionFields = $action.requestFields)
#end
rows += executeUpdateFieldEffect(params, "${effect.targetTable}", "${effect.targetField}", "${effect.value}", new String[] {#foreach($field in $conditionFields)"${field}"#if($foreach.hasNext), #end#end});
#else
// Unsupported effect type ${effect.type}: ${effect.description}
#end
#end
#end
return rows;
}
#end
private void requireBusinessParam(Map<String, Object> params, String field)
{
if (!params.containsKey(field) || params.get(field) == null) {
throw new ServiceException("缺少业务动作参数:" + field);
}
}
private int executeUpdateFieldEffect(Map<String, Object> params, String tableName, String targetField,
String valueExpression, String[] conditionFields)
{
String safeTableName = requireSafeIdentifier(tableName, "业务影响目标表不合法");
String safeTargetField = requireSafeIdentifier(targetField, "业务影响目标字段不合法");
String safeValueExpression = requireSafeExpression(valueExpression);
String whereClause = buildWhereClause(params, conditionFields);
Object[] values = conditionValues(params, conditionFields);
String sql = "update " + safeTableName + " set " + safeTargetField + " = " + safeValueExpression + " where " + whereClause;
return jdbcTemplate.update(sql, values);
}
private String buildWhereClause(Map<String, Object> params, String[] conditionFields)
{
if (conditionFields == null || conditionFields.length == 0) {
throw new ServiceException("业务影响缺少条件字段");
}
StringBuilder where = new StringBuilder();
for (String field : conditionFields) {
String safeField = requireSafeIdentifier(field, "业务影响条件字段不合法");
requireBusinessParam(params, safeField);
if (where.length() > 0) {
where.append(" and ");
}
where.append(safeField).append(" = ?");
}
return where.toString();
}
private Object[] conditionValues(Map<String, Object> params, String[] conditionFields)
{
Object[] values = new Object[conditionFields.length];
for (int i = 0; i < conditionFields.length; i++) {
values[i] = params.get(conditionFields[i]);
}
return values;
}
private String requireSafeIdentifier(String value, String message)
{
if (value == null || !value.matches("[A-Za-z][A-Za-z0-9_]*")) {
throw new ServiceException(message);
}
return value;
}
private String requireSafeExpression(String valueExpression)
{
if (valueExpression == null || !valueExpression.matches("[A-Za-z0-9_ +*/().-]{1,200}")
|| valueExpression.contains(";") || valueExpression.contains("--")) {
throw new ServiceException("业务影响字段表达式不合法");
}
return valueExpression;
}
#end
}
', 'admin', sysdate()),