Generate executable business action effects
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
package com.ruoyi.generator.domain.front.dto;
|
package com.ruoyi.generator.domain.front.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class BusinessActionEffectDesign
|
public class BusinessActionEffectDesign
|
||||||
{
|
{
|
||||||
private String type;
|
private String type;
|
||||||
private String targetTable;
|
private String targetTable;
|
||||||
private String targetField;
|
private String targetField;
|
||||||
private String value;
|
private String value;
|
||||||
|
private List<String> conditionFields;
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
public String getType() { return type; }
|
public String getType() { return type; }
|
||||||
@@ -16,6 +19,8 @@ public class BusinessActionEffectDesign
|
|||||||
public void setTargetField(String targetField) { this.targetField = targetField; }
|
public void setTargetField(String targetField) { this.targetField = targetField; }
|
||||||
public String getValue() { return value; }
|
public String getValue() { return value; }
|
||||||
public void setValue(String value) { this.value = 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 String getDescription() { return description; }
|
||||||
public void setDescription(String description) { this.description = description; }
|
public void setDescription(String description) { this.description = description; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ public class AiBusinessBlueprintValidator
|
|||||||
{
|
{
|
||||||
throw new ServiceException("业务影响目标字段不存在:" + effect.getTargetField());
|
throw new ServiceException("业务影响目标字段不存在:" + effect.getTargetField());
|
||||||
}
|
}
|
||||||
|
validateEffectConditionFields(effect, columnNames);
|
||||||
if (StringUtils.defaultString(effect.getValue()).length() > 200
|
if (StringUtils.defaultString(effect.getValue()).length() > 200
|
||||||
|| StringUtils.defaultString(effect.getDescription()).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)
|
private Set<String> collectTableNames(List<DatabaseTableDesign> tables)
|
||||||
{
|
{
|
||||||
Set<String> tableNames = new HashSet<String>();
|
Set<String> tableNames = new HashSet<String>();
|
||||||
|
|||||||
@@ -109,9 +109,9 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
|||||||
private String buildPrompt(FrontProject project, GenerateDatabaseRequest request)
|
private String buildPrompt(FrontProject project, GenerateDatabaseRequest request)
|
||||||
{
|
{
|
||||||
StringBuilder prompt = new StringBuilder();
|
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("限制:表名和字段名使用小写字母、数字、下划线;必须且只能有一个主键;字段类型只能使用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、DELETE;effects.type优先使用UPDATE_FIELD,conditionFields表示更新或删除时的WHERE条件字段。\n");
|
||||||
prompt.append("项目名称:").append(firstNonBlank(request == null ? null : request.getProjectName(), project.getProjectName())).append("\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.getProjectDesc(), project.getProjectDesc())).append("\n");
|
||||||
prompt.append("行业模板:").append(firstNonBlank(request == null ? null : request.getIndustryTemplate(), project.getIndustryTemplate())).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.setTransaction(action.getTransaction() == null ? Boolean.TRUE : action.getTransaction());
|
||||||
action.setRequestFields(normalizeExistingFieldList(action.getRequestFields(), columnNames));
|
action.setRequestFields(normalizeExistingFieldList(action.getRequestFields(), columnNames));
|
||||||
action.setRules(trimStringList(action.getRules(), 200));
|
action.setRules(trimStringList(action.getRules(), 200));
|
||||||
normalizeBusinessEffects(action);
|
normalizeBusinessEffects(action, columnNames);
|
||||||
actionIndex++;
|
actionIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,7 +422,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
|||||||
return normalizedValues;
|
return normalizedValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void normalizeBusinessEffects(BusinessActionDesign action)
|
private void normalizeBusinessEffects(BusinessActionDesign action, Set<String> columnNames)
|
||||||
{
|
{
|
||||||
if (action.getEffects() == null)
|
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.setTargetTable(StringUtils.isBlank(effect.getTargetTable()) ? "" : normalizeDatabaseName(effect.getTargetTable(), "table_" + effectIndex));
|
||||||
effect.setTargetField(StringUtils.isBlank(effect.getTargetField()) ? "" : normalizeDatabaseName(effect.getTargetField(), "field_" + effectIndex));
|
effect.setTargetField(StringUtils.isBlank(effect.getTargetField()) ? "" : normalizeDatabaseName(effect.getTargetField(), "field_" + effectIndex));
|
||||||
effect.setValue(trimToLength(effect.getValue(), 200));
|
effect.setValue(trimToLength(effect.getValue(), 200));
|
||||||
|
effect.setConditionFields(normalizeExistingFieldList(effect.getConditionFields(), columnNames));
|
||||||
effect.setDescription(trimToLength(effect.getDescription(), 200));
|
effect.setDescription(trimToLength(effect.getDescription(), 200));
|
||||||
effectIndex++;
|
effectIndex++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ import org.mockito.Mock;
|
|||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
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.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -169,6 +173,20 @@ public class GenProjectServiceImplTest {
|
|||||||
assertFalse(content.contains("return_book"));
|
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
|
@Test
|
||||||
public void getProjectStructureForDraftProjectUsesFirstActiveTemplateWithoutLoadingProject() {
|
public void getProjectStructureForDraftProjectUsesFirstActiveTemplateWithoutLoadingProject() {
|
||||||
GenProject project = project();
|
GenProject project = project();
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ public class AiGenerateServiceImplTest
|
|||||||
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
|
ArgumentCaptor<String> promptCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
verify(deepSeekClient).chat(promptCaptor.capture());
|
verify(deepSeekClient).chat(promptCaptor.capture());
|
||||||
assertTrue(promptCaptor.getValue().contains("businessActions"));
|
assertTrue(promptCaptor.getValue().contains("businessActions"));
|
||||||
|
assertTrue(promptCaptor.getValue().contains("conditionFields"));
|
||||||
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
|
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
|
||||||
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
|
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
|
||||||
assertEquals(Long.valueOf(10L), projectCaptor.getValue().getProjectId());
|
assertEquals(Long.valueOf(10L), projectCaptor.getValue().getProjectId());
|
||||||
@@ -233,6 +234,7 @@ public class AiGenerateServiceImplTest
|
|||||||
assertEquals("UPDATE_FIELD", effect.getType());
|
assertEquals("UPDATE_FIELD", effect.getType());
|
||||||
assertEquals("book_info", effect.getTargetTable());
|
assertEquals("book_info", effect.getTargetTable());
|
||||||
assertEquals("stock", effect.getTargetField());
|
assertEquals("stock", effect.getTargetField());
|
||||||
|
assertEquals("book_id", effect.getConditionFields().get(0));
|
||||||
|
|
||||||
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
|
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
|
||||||
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
|
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
|
||||||
@@ -522,7 +524,7 @@ public class AiGenerateServiceImplTest
|
|||||||
+ " \"requestFields\": [\"book_id\", \"reader_id\"],\n"
|
+ " \"requestFields\": [\"book_id\", \"reader_id\"],\n"
|
||||||
+ " \"rules\": [\"读者状态必须正常\", \"图书库存必须大于0\"],\n"
|
+ " \"rules\": [\"读者状态必须正常\", \"图书库存必须大于0\"],\n"
|
||||||
+ " \"effects\": [\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"
|
+ " }\n"
|
||||||
+ " ]\n"
|
+ " ]\n"
|
||||||
|
|||||||
@@ -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);
|
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
|
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'),
|
(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');
|
(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.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
@@ -298,6 +299,10 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service
|
|||||||
{
|
{
|
||||||
@Resource
|
@Resource
|
||||||
private ${ClassName}Mapper ${className}Mapper;
|
private ${ClassName}Mapper ${className}Mapper;
|
||||||
|
#if($hasTableBusinessActions)
|
||||||
|
@Resource
|
||||||
|
private JdbcTemplate jdbcTemplate;
|
||||||
|
#end
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField})
|
public ${ClassName} select${ClassName}By${pkColumn.capJavaField}(${pkColumn.javaType} ${pkColumn.javaField})
|
||||||
@@ -362,10 +367,93 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service
|
|||||||
@Override
|
@Override
|
||||||
public int ${action.code}(Map<String, Object> params)
|
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
|
#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
|
#end
|
||||||
}
|
}
|
||||||
', 'admin', sysdate()),
|
', 'admin', sysdate()),
|
||||||
|
|||||||
Reference in New Issue
Block a user