diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionEffectDesign.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionEffectDesign.java index a914d2b..e1f2d40 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionEffectDesign.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionEffectDesign.java @@ -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 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 getConditionFields() { return conditionFields; } + public void setConditionFields(List conditionFields) { this.conditionFields = conditionFields; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiBusinessBlueprintValidator.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiBusinessBlueprintValidator.java index 9db3e41..69dee98 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiBusinessBlueprintValidator.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiBusinessBlueprintValidator.java @@ -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 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 collectTableNames(List tables) { Set tableNames = new HashSet(); diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java index 4398a5f..c6a6106 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java @@ -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、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.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 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++; } diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java index b667130..07a1701 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java @@ -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(); diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java index 85c18fd..acf2f74 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java @@ -94,6 +94,7 @@ public class AiGenerateServiceImplTest ArgumentCaptor promptCaptor = ArgumentCaptor.forClass(String.class); verify(deepSeekClient).chat(promptCaptor.capture()); assertTrue(promptCaptor.getValue().contains("businessActions")); + assertTrue(promptCaptor.getValue().contains("conditionFields")); ArgumentCaptor 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 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" diff --git a/RuoYi-Vue/sql/business_blueprint_templates.sql b/RuoYi-Vue/sql/business_blueprint_templates.sql index 425ac17..3235c2d 100644 --- a/RuoYi-Vue/sql/business_blueprint_templates.sql +++ b/RuoYi-Vue/sql/business_blueprint_templates.sql @@ -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 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 params, String field) + { + if (!params.containsKey(field) || params.get(field) == null) { + throw new ServiceException("缺少业务动作参数:" + field); + } + } + + private int executeUpdateFieldEffect(Map 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 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 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()),