From 84d3c8fc053f2fb25da06b4fe1fab15d9bd70d3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Wed, 15 Jul 2026 17:28:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(generator):=20=E4=BF=AE=E5=A4=8D=20create?= =?UTF-8?q?=5Frecord=20=E5=BF=85=E5=A1=AB=E5=AD=97=E6=AE=B5=E7=94=9F?= =?UTF-8?q?=E6=88=90=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../front/dto/BusinessActionDesign.java | 3 + .../TypedBusinessActionRenderModel.java | 20 +++ .../factory/flow/FlowConfigCompiler.java | 1 + ...usinessLoopDatabaseContractReconciler.java | 38 +++++ .../FlowConfigDatabaseContractValidator.java | 38 ++--- .../util/BusinessActionRenderSupport.java | 13 +- .../util/CurrentUserFieldSupport.java | 11 +- .../util/TypedBusinessActionCompiler.java | 125 ++++++++++++++-- .../main/resources/qing/controller.java.vm | 31 ++++ .../src/main/resources/qing/service.java.vm | 6 + ...essLoopDatabaseContractReconcilerTest.java | 35 +++++ ...owConfigDatabaseContractValidatorTest.java | 33 ++++- .../util/BusinessActionRenderSupportTest.java | 22 +++ .../util/TypedBusinessActionCompilerTest.java | 56 ++++++++ ...e_record_action_input_upgrade_20260715.sql | 135 ++++++++++++++++++ 15 files changed, 515 insertions(+), 52 deletions(-) create mode 100644 RuoYi-Vue/sql/prompt_create_record_action_input_upgrade_20260715.sql diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java index 2c32480..abe36dc 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/BusinessActionDesign.java @@ -7,6 +7,7 @@ public class BusinessActionDesign { private String code; private String name; + private String actor; private String ownerTable; private String method; private String path; @@ -36,6 +37,8 @@ public class BusinessActionDesign public void setCode(String code) { this.code = code; } public String getName() { return name; } public void setName(String name) { this.name = name; } + public String getActor() { return actor; } + public void setActor(String actor) { this.actor = actor; } public String getOwnerTable() { return ownerTable; } public void setOwnerTable(String ownerTable) { this.ownerTable = ownerTable; } public String getMethod() { return method; } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/render/TypedBusinessActionRenderModel.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/render/TypedBusinessActionRenderModel.java index d144031..ad5c337 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/render/TypedBusinessActionRenderModel.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/render/TypedBusinessActionRenderModel.java @@ -56,6 +56,7 @@ public class TypedBusinessActionRenderModel private boolean currentUserRequired; private String auditUserSetter; private String auditTimeSetter; + private List requestParameters = new ArrayList(); private List effects = new ArrayList(); public String getCode() { return code; } @@ -104,10 +105,29 @@ public class TypedBusinessActionRenderModel public void setAuditUserSetter(String auditUserSetter) { this.auditUserSetter = auditUserSetter; } public String getAuditTimeSetter() { return auditTimeSetter; } public void setAuditTimeSetter(String auditTimeSetter) { this.auditTimeSetter = auditTimeSetter; } + public List getRequestParameters() { return requestParameters; } + public void setRequestParameters(List requestParameters) { this.requestParameters = requestParameters; } public List getEffects() { return effects; } public void setEffects(List effects) { this.effects = effects; } } + public static class RequestParameter + { + private String field; + private String variable; + private String javaType; + private String controllerExpression; + + public String getField() { return field; } + public void setField(String field) { this.field = field; } + public String getVariable() { return variable; } + public void setVariable(String variable) { this.variable = variable; } + public String getJavaType() { return javaType; } + public void setJavaType(String javaType) { this.javaType = javaType; } + public String getControllerExpression() { return controllerExpression; } + public void setControllerExpression(String controllerExpression) { this.controllerExpression = controllerExpression; } + } + public static class Effect { private String type; diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/factory/flow/FlowConfigCompiler.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/factory/flow/FlowConfigCompiler.java index bb50388..c82f63b 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/factory/flow/FlowConfigCompiler.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/factory/flow/FlowConfigCompiler.java @@ -284,6 +284,7 @@ public class FlowConfigCompiler BusinessActionDesign action = new BusinessActionDesign(); action.setCode(source.getCode()); action.setName(source.getName()); + action.setActor(source.getActor()); action.setOwnerTable(ownerTable(config, source)); action.setMethod(defaultValue(source.getMethod(), "POST")); action.setPath(defaultValue(source.getPath(), "/" + source.getCode())); diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopDatabaseContractReconciler.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopDatabaseContractReconciler.java index aec75e0..42b5bed 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopDatabaseContractReconciler.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessLoopDatabaseContractReconciler.java @@ -120,6 +120,7 @@ public class BusinessLoopDatabaseContractReconciler } repairAuditFields(tables, flowConfig, maxColumnsPerTable, messages); repairEffectFields(tables, flowConfig, maxColumnsPerTable, messages); + repairCreateRecordSourceRequirements(tables, flowConfig, messages); if (!messages.isEmpty()) { List allMessages = database.getRepairMessages() == null @@ -183,6 +184,43 @@ public class BusinessLoopDatabaseContractReconciler } } + private void repairCreateRecordSourceRequirements(Map tables, + SimpleFlowConfig flowConfig, List messages) + { + if (flowConfig == null) return; + for (SimpleFlowConfig flow : FlowConfigSupport.leafFlows(flowConfig)) + { + for (Action action : safe(flow.getActions())) + { + if (action == null) continue; + String ownerTableName = StringUtils.defaultIfBlank(action.getOwnerTable(), flow.getMainTable()); + DatabaseTableDesign ownerTable = tables.get(normalize(ownerTableName)); + for (Effect effect : safe(action.getEffects())) + { + if (effect == null || !"create_record".equals(effect.getType())) continue; + DatabaseTableDesign targetTable = tables.get(normalize(effect.getTable())); + for (Map.Entry value : effect.getValues().entrySet()) + { + String expression = StringUtils.defaultString(value.getValue()).trim(); + if (!expression.startsWith("record.")) continue; + DatabaseColumnDesign target = findColumn(targetTable, value.getKey()); + DatabaseColumnDesign source = findColumn(ownerTable, + expression.substring("record.".length())); + if (target == null || source == null || !"1".equals(target.getIsRequired()) + || "1".equals(source.getIsRequired())) + { + continue; + } + source.setIsRequired("1"); + messages.add("Required create_record source field: " + ownerTableName + "." + + source.getColumnName() + " -> " + effect.getTable() + "." + + target.getColumnName()); + } + } + } + } + } + private DatabaseColumnDesign primaryKey(DatabaseTableDesign table) { for (DatabaseColumnDesign column : table == null diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FlowConfigDatabaseContractValidator.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FlowConfigDatabaseContractValidator.java index 1adb831..bc9efbd 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FlowConfigDatabaseContractValidator.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FlowConfigDatabaseContractValidator.java @@ -19,7 +19,6 @@ import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig.Effect; import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig.Metric; import com.ruoyi.generator.factory.flow.FlowConfigCompilation; import com.ruoyi.generator.factory.flow.FlowConfigSupport; -import com.ruoyi.generator.util.CreateRecordFieldSupport; /** Validates compiled flow actions against the database before it is persisted. */ @Component @@ -176,38 +175,17 @@ public class FlowConfigDatabaseContractValidator throw new ServiceException("Flow config value types do not match: " + expression + " -> " + targetTable + "." + value.getKey()); } + if ("create_record".equals(effect.getType()) + && "1".equals(target.getIsRequired()) + && !"1".equals(source.getIsRequired())) + { + throw new ServiceException("Flow config create_record source may be null for required field: " + + ownerTable + "." + source.getColumnName() + " -> " + + targetTable + "." + target.getColumnName()); + } } else validateLiteral(expression, target, targetTable + "." + value.getKey()); } - if ("create_record".equals(effect.getType())) - { - validateRequiredCreateFields(schema.get(normalize(targetTable)), effect, targetTable); - } - } - - private void validateRequiredCreateFields(Map columns, Effect effect, - String targetTable) - { - if (columns == null) return; - Map values = effect.getValues(); - for (DatabaseColumnDesign column : columns.values()) - { - if (column == null || !"1".equals(column.getIsRequired())) continue; - if ("1".equals(column.getIsIncrement()) || hasUsableDefault(column.getDefaultValue())) continue; - if (values != null && values.containsKey(column.getColumnName())) continue; - if (CreateRecordFieldSupport.isCreationTimeField( - column.getColumnName(), column.getJavaField()) && family(column) == TypeFamily.TEMPORAL) - { - continue; - } - throw new ServiceException("Flow config create_record missing required field: " - + targetTable + "." + column.getColumnName()); - } - } - - private boolean hasUsableDefault(String defaultValue) - { - return StringUtils.isNotBlank(defaultValue) && !"null".equalsIgnoreCase(defaultValue.trim()); } private boolean requiresNumericMetric(Metric metric) diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/BusinessActionRenderSupport.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/BusinessActionRenderSupport.java index 4638c82..dc2e608 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/BusinessActionRenderSupport.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/BusinessActionRenderSupport.java @@ -205,7 +205,7 @@ public final class BusinessActionRenderSupport { continue; } - String expression = automaticInsertValueExpression(column); + String expression = automaticInsertValueExpression(action, column); if (!isEmpty(expression)) { values.put(column.getColumnName(), expression); @@ -222,13 +222,14 @@ public final class BusinessActionRenderSupport && !values.containsKey(column.getColumnName()); } - private static String automaticInsertValueExpression(GenTableColumn column) + private static String automaticInsertValueExpression(BusinessActionDesign action, GenTableColumn column) { if (isCreationTimeColumn(column)) { return "${now}"; } - if (CurrentUserFieldSupport.isCurrentUserIdField(column)) + if (CurrentUserFieldSupport.isCurrentUserIdField(column, + action == null ? null : action.getActor())) { return "${current_user.id}"; } @@ -328,10 +329,8 @@ public final class BusinessActionRenderSupport } private static boolean isCreationTimeColumn(GenTableColumn column) { - String columnName = normalize(column.getColumnName()); - String javaField = normalize(column.getJavaField()); - boolean creationField = "created_at".equals(columnName) || "create_time".equals(columnName) - || "createdat".equals(javaField) || "createtime".equals(javaField); + boolean creationField = CreateRecordFieldSupport.isCreationTimeField( + column.getColumnName(), column.getJavaField()); String columnType = normalize(column.getColumnType()); String javaType = normalize(column.getJavaType()); return creationField && (columnType.contains("date") || columnType.contains("time") diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/CurrentUserFieldSupport.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/CurrentUserFieldSupport.java index a6f1b47..f04ac3e 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/CurrentUserFieldSupport.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/CurrentUserFieldSupport.java @@ -23,6 +23,15 @@ public final class CurrentUserFieldSupport column.getColumnComment()); } + public static boolean isCurrentUserIdField(GenTableColumn column, String actor) + { + if (isCurrentUserIdField(column)) return true; + if (column == null || StringUtils.isBlank(actor)) return false; + String actorId = compact(actor) + "id"; + return actorId.equals(compact(column.getColumnName())) + || actorId.equals(compact(column.getJavaField())); + } + public static boolean isCurrentUserIdField(FrontProjectColumn column) { return column != null && isCurrentUserIdField(column.getColumnName(), column.getJavaField(), @@ -111,4 +120,4 @@ public final class CurrentUserFieldSupport { return StringUtils.defaultString(value).trim().toLowerCase(Locale.ENGLISH).replaceAll("[^a-z0-9]+", ""); } -} \ No newline at end of file +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/TypedBusinessActionCompiler.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/TypedBusinessActionCompiler.java index 4d9f354..a8ba104 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/TypedBusinessActionCompiler.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/util/TypedBusinessActionCompiler.java @@ -18,6 +18,7 @@ import com.ruoyi.generator.domain.render.TypedBusinessActionRenderModel.Action; import com.ruoyi.generator.domain.render.TypedBusinessActionRenderModel.Assignment; import com.ruoyi.generator.domain.render.TypedBusinessActionRenderModel.Dependency; import com.ruoyi.generator.domain.render.TypedBusinessActionRenderModel.Effect; +import com.ruoyi.generator.domain.render.TypedBusinessActionRenderModel.RequestParameter; /** Converts flow_config to explicit, strongly typed Java render instructions. */ public final class TypedBusinessActionCompiler @@ -94,9 +95,6 @@ public final class TypedBusinessActionCompiler } } action.setCurrentUserRequired(currentUserRequired); - action.setServiceParameters(pk.getJavaType() + " " + pk.getJavaField() - + (currentUserRequired ? ", Long currentUserId" : "")); - action.setServiceArguments(pk.getJavaField() + (currentUserRequired ? ", currentUser.getUserId()" : "")); action.setControllerIdExpression(idExpression(pk.getJavaType(), "params.get(\"" + pk.getJavaField() + "\")")); if (source.getAudit() != null) { @@ -116,14 +114,15 @@ public final class TypedBusinessActionCompiler for (SimpleFlowConfig.Effect sourceEffect : source.getEffects()) { action.getEffects().add(compileEffect(model, sourceEffect, action, owner, tables, - dependencies, index++)); + dependencies, source.getActor(), index++)); } + configureActionParameters(action, pk); return action; } private static Effect compileEffect(TypedBusinessActionRenderModel model, SimpleFlowConfig.Effect source, Action action, GenTable owner, Map tables, Map dependencies, - int index) + String actor, int index) { GenTable target = tables.get(source.getTable()); if (target == null) throw new ServiceException("flow_config 引用了不存在的表: " + source.getTable()); @@ -165,27 +164,89 @@ public final class TypedBusinessActionCompiler } if ("create_record".equals(source.getType())) { - appendImplicitCreationTimeAssignments(model, effect, target, assignedFields); + appendImplicitCreateAssignments(model, effect, target, assignedFields, action, actor); } } return effect; } - private static void appendImplicitCreationTimeAssignments(TypedBusinessActionRenderModel model, Effect effect, - GenTable target, Map assignedFields) + private static void appendImplicitCreateAssignments(TypedBusinessActionRenderModel model, Effect effect, + GenTable target, Map assignedFields, Action action, String actor) { if (target.getColumns() == null) return; for (GenTableColumn column : target.getColumns()) { if (assignedFields.containsKey(column.getColumnName()) || assignedFields.containsValue(column.getJavaField())) continue; - if (!CreateRecordFieldSupport.isCreationTimeField( - column.getColumnName(), column.getJavaField())) continue; - String expression = currentTimeExpression(model, column.getJavaType()); + if (column.isIncrement()) continue; + String expression = null; + if (CreateRecordFieldSupport.isCreationTimeField( + column.getColumnName(), column.getJavaField())) + { + expression = currentTimeExpression(model, column.getJavaType()); + } + else if (!column.isRequired()) + { + continue; + } + else if (CurrentUserFieldSupport.isCurrentUserIdField(column, actor)) + { + action.setCurrentUserRequired(true); + expression = "currentUserId"; + } + else + { + expression = ensureRequestParameter(model, action, column).getVariable(); + } if (expression != null) addAssignment(effect, column, expression); } } + private static RequestParameter ensureRequestParameter(TypedBusinessActionRenderModel model, + Action action, GenTableColumn column) + { + for (RequestParameter existing : action.getRequestParameters()) + { + if (!StringUtils.equals(existing.getField(), column.getColumnName())) continue; + if (!StringUtils.equals(existing.getJavaType(), renderJavaType(column.getJavaType()))) + { + throw new ServiceException("create_record input field type conflict: " + + column.getColumnName()); + } + return existing; + } + String javaType = renderJavaType(column.getJavaType()); + RequestParameter parameter = new RequestParameter(); + parameter.setField(column.getColumnName()); + parameter.setVariable("input" + Character.toUpperCase(column.getJavaField().charAt(0)) + + column.getJavaField().substring(1)); + parameter.setJavaType(javaType); + parameter.setControllerExpression(requestParameterExpression(model, javaType, + "params.get(\"" + javaEscape(column.getColumnName()) + "\")", + column.getColumnName())); + action.getRequestParameters().add(parameter); + return parameter; + } + + private static void configureActionParameters(Action action, GenTableColumn pk) + { + StringBuilder parameters = new StringBuilder(pk.getJavaType()).append(" ").append(pk.getJavaField()); + StringBuilder arguments = new StringBuilder(pk.getJavaField()); + if (action.isCurrentUserRequired()) + { + parameters.append(", Long currentUserId"); + arguments.append(", currentUser.getUserId()"); + } + for (RequestParameter parameter : action.getRequestParameters()) + { + parameters.append(", ").append(parameter.getJavaType()).append(" ") + .append(parameter.getVariable()); + arguments.append(", ").append(parameter.getVariable()); + } + action.setServiceParameters(parameters.toString()); + action.setServiceArguments(arguments.toString()); + } + private static String currentTimeExpression(TypedBusinessActionRenderModel model, String javaType) { if ("Date".equals(javaType) || "java.util.Date".equals(javaType)) @@ -275,6 +336,48 @@ public final class TypedBusinessActionCompiler return normalized; } + private static String renderJavaType(String javaType) + { + return StringUtils.defaultIfBlank(javaType, "String"); + } + + private static String requestParameterExpression(TypedBusinessActionRenderModel model, + String javaType, String source, String field) + { + if ("String".equals(javaType)) return "String.valueOf(" + source + ")"; + if ("Integer".equals(javaType) || "int".equals(javaType)) + return "Integer.valueOf(String.valueOf(" + source + "))"; + if ("Long".equals(javaType) || "long".equals(javaType)) + return "Long.valueOf(String.valueOf(" + source + "))"; + if ("Short".equals(javaType) || "short".equals(javaType)) + return "Short.valueOf(String.valueOf(" + source + "))"; + if ("Double".equals(javaType) || "double".equals(javaType)) + return "Double.valueOf(String.valueOf(" + source + "))"; + if ("Float".equals(javaType) || "float".equals(javaType)) + return "Float.valueOf(String.valueOf(" + source + "))"; + if ("Boolean".equals(javaType) || "boolean".equals(javaType)) + return "Boolean.valueOf(String.valueOf(" + source + "))"; + if ("BigDecimal".equals(javaType) || "java.math.BigDecimal".equals(javaType)) + { + model.setUsesBigDecimal(true); + return "new BigDecimal(String.valueOf(" + source + "))"; + } + if ("Date".equals(javaType) || "java.util.Date".equals(javaType)) + { + model.setUsesDate(true); + return "parseBusinessActionDate(" + source + ", \"" + javaEscape(field) + "\")"; + } + if ("LocalDate".equals(javaType) || "java.time.LocalDate".equals(javaType)) + return "java.time.LocalDate.parse(String.valueOf(" + source + "))"; + if ("LocalDateTime".equals(javaType) || "java.time.LocalDateTime".equals(javaType)) + return "java.time.LocalDateTime.parse(String.valueOf(" + source + "))"; + if ("Instant".equals(javaType) || "java.time.Instant".equals(javaType)) + return "java.time.Instant.parse(String.valueOf(" + source + "))"; + if ("Timestamp".equals(javaType) || "java.sql.Timestamp".equals(javaType)) + return "java.sql.Timestamp.valueOf(String.valueOf(" + source + ").replace('T', ' '))"; + return "(" + javaType + ") " + source; + } + private static String idExpression(String javaType, String source) { if ("String".equals(javaType)) return "String.valueOf(" + source + ")"; diff --git a/RuoYi-Vue/ruoyi-generator/src/main/resources/qing/controller.java.vm b/RuoYi-Vue/ruoyi-generator/src/main/resources/qing/controller.java.vm index 7862e0b..55fbbda 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/resources/qing/controller.java.vm +++ b/RuoYi-Vue/ruoyi-generator/src/main/resources/qing/controller.java.vm @@ -13,7 +13,13 @@ import ${packageName}.security.PortalAuthTokenStore; #end import ${packageName}.service.I${ClassName}Service; import java.io.InputStream; +#if($typedBusinessUsesBigDecimal) +import java.math.BigDecimal; +#end import java.net.URLEncoder; +#if($typedBusinessUsesDate) +import java.util.Date; +#end import java.util.List; import java.util.Map; import javax.annotation.Resource; @@ -179,6 +185,13 @@ public class ${ClassName}Controller } AuthPrincipal currentUser = requireCurrentUser(request); ${action.pkType} ${action.pkField} = ${action.controllerIdExpression}; +#foreach($parameter in $action.requestParameters) + if (!hasValue(params.get("${parameter.field}"))) + { + throw new IllegalArgumentException("Missing business action parameter: ${parameter.field}"); + } + ${parameter.javaType} ${parameter.variable} = ${parameter.controllerExpression}; +#end ${className}Service.${action.javaMethodName}(${action.serviceArguments}); return Result.success(); } @@ -200,6 +213,24 @@ public class ${ClassName}Controller } #end +#if($hasTypedBusinessActions) +#if($typedBusinessUsesDate) + private Date parseBusinessActionDate(Object value, String field) + { + String text = String.valueOf(value).trim().replace('T', ' '); + String pattern = text.length() <= 10 ? "yyyy-MM-dd" : "yyyy-MM-dd HH:mm:ss"; + try + { + return new java.text.SimpleDateFormat(pattern).parse(text); + } + catch (java.text.ParseException e) + { + throw new IllegalArgumentException("Invalid business action date parameter: " + field); + } + } + +#end +#end private boolean hasValue(Object value) { return value != null && StringUtils.isNotBlank(String.valueOf(value)); diff --git a/RuoYi-Vue/ruoyi-generator/src/main/resources/qing/service.java.vm b/RuoYi-Vue/ruoyi-generator/src/main/resources/qing/service.java.vm index 235bcf6..fa389c3 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/resources/qing/service.java.vm +++ b/RuoYi-Vue/ruoyi-generator/src/main/resources/qing/service.java.vm @@ -2,6 +2,12 @@ package ${packageName}.service; import ${packageName}.entity.${ClassName}; import com.baomidou.mybatisplus.extension.service.IService; +#if($typedBusinessUsesBigDecimal) +import java.math.BigDecimal; +#end +#if($typedBusinessUsesDate) +import java.util.Date; +#end /** * ${functionName}Service接口 diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopDatabaseContractReconcilerTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopDatabaseContractReconcilerTest.java index 4e2bbf8..de1d6ee 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopDatabaseContractReconcilerTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessLoopDatabaseContractReconcilerTest.java @@ -17,6 +17,7 @@ import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign; import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig; import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig.Action; import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig.Audit; +import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig.Effect; public class BusinessLoopDatabaseContractReconcilerTest { @@ -120,6 +121,40 @@ public class BusinessLoopDatabaseContractReconcilerTest new FlowConfigDatabaseContractValidator().validate(database, flowConfig, null); } + @Test + public void requiresNullableSourceUsedByRequiredCreateRecordTarget() + { + DatabaseColumnDesign opportunityAmount = column("amount", "decimal(10,2)"); + DatabaseTableDesign opportunity = new DatabaseTableDesign(); + opportunity.setTableName("opportunity"); + opportunity.setColumns(Arrays.asList(column("id", "bigint(20)"), opportunityAmount)); + DatabaseColumnDesign contractAmount = column("amount", "decimal(10,2)"); + contractAmount.setIsRequired("1"); + DatabaseTableDesign contract = new DatabaseTableDesign(); + contract.setTableName("contract"); + contract.setColumns(Arrays.asList(column("id", "bigint(20)"), contractAmount)); + DatabaseDesignResponse database = new DatabaseDesignResponse(); + database.setTables(Arrays.asList(opportunity, contract)); + + Effect create = new Effect(); + create.setType("create_record"); + create.setTable("contract"); + create.getValues().put("amount", "record.amount"); + Action action = new Action(); + action.setOwnerTable("opportunity"); + action.setEffects(Collections.singletonList(create)); + SimpleFlowConfig flowConfig = new SimpleFlowConfig(); + flowConfig.setMainTable("contract"); + flowConfig.setActions(Collections.singletonList(action)); + + java.util.List messages = reconciler.repair(database, new BusinessLoopPlan(), + flowConfig, 30); + + assertEquals("1", opportunityAmount.getIsRequired()); + assertTrue(messages.toString(), messages.toString().contains( + "opportunity.amount -> contract.amount")); + } + private BusinessLoopPlan metricPlan(String field, String aggregate, String unit) { BusinessLoopMetric metric = new BusinessLoopMetric(); diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FlowConfigDatabaseContractValidatorTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FlowConfigDatabaseContractValidatorTest.java index dd784b2..78376e5 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FlowConfigDatabaseContractValidatorTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FlowConfigDatabaseContractValidatorTest.java @@ -124,7 +124,7 @@ public class FlowConfigDatabaseContractValidatorTest } @Test - public void rejectsCreateRecordThatOmitsNonGeneratedRequiredField() + public void acceptsCreateRecordThatUsesImplicitInputForRequiredField() { DatabaseTableDesign book = table("book_info", "id"); DatabaseTableDesign borrow = table("borrow_record", "id", "book_id", "reader_id", "status"); @@ -143,14 +143,41 @@ public class FlowConfigDatabaseContractValidatorTest + "\"reader_id\":\"currentUserId\",\"status\":\"pending\"}}]}]}", SimpleFlowConfig.class); + validator.validate(database, flow, new FlowConfigCompiler().compile(flow)); + } + + @Test + public void rejectsNullableRecordSourceForRequiredCreateField() + { + DatabaseTableDesign opportunity = table("opportunity", "id", "status"); + DatabaseColumnDesign amount = column("amount"); + amount.setColumnType("decimal(10,2)"); + opportunity.getColumns().add(amount); + DatabaseTableDesign contract = table("contract", "id", "status"); + contract.getColumns().add(requiredColumn("amount", "decimal(10,2)")); + DatabaseDesignResponse database = new DatabaseDesignResponse(); + database.setTables(Arrays.asList(opportunity, contract)); + database.setBusinessActions(Collections.emptyList()); + SimpleFlowConfig flow = JSON.parseObject("{" + + "\"code\":\"contract_flow\",\"name\":\"Contract\"," + + "\"mainTable\":\"contract\",\"statusField\":\"status\"," + + "\"states\":[{\"code\":\"pending\",\"label\":\"Pending\"}," + + "{\"code\":\"active\",\"label\":\"Active\"}]," + + "\"actions\":[{\"code\":\"create_contract\",\"name\":\"Create contract\"," + + "\"ownerTable\":\"opportunity\",\"effects\":[{\"type\":\"create_record\"," + + "\"table\":\"contract\",\"values\":{\"amount\":\"record.amount\"," + + "\"status\":\"pending\"}}]}]}", + SimpleFlowConfig.class); + try { validator.validate(database, flow, new FlowConfigCompiler().compile(flow)); - fail("Expected missing required create_record field validation to fail"); + fail("Expected nullable create_record source validation to fail"); } catch (ServiceException e) { - assertEquals("Flow config create_record missing required field: borrow_record.tenant_id", e.getMessage()); + assertEquals("Flow config create_record source may be null for required field: " + + "opportunity.amount -> contract.amount", e.getMessage()); } } diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/util/BusinessActionRenderSupportTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/util/BusinessActionRenderSupportTest.java index 0eabae8..a906cdb 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/util/BusinessActionRenderSupportTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/util/BusinessActionRenderSupportTest.java @@ -148,6 +148,28 @@ public class BusinessActionRenderSupportTest assertEquals(Arrays.asList("book_id", "due_date"), fields(action)); } + @Test + public void usesCustomActionActorIdAsTrustedCurrentUserInput() + { + BusinessActionDesign action = new BusinessActionDesign(); + action.setActor("salesperson"); + action.setOwnerTable("customer_info"); + BusinessActionEffectDesign effect = new BusinessActionEffectDesign(); + effect.setType("INSERT_ROW"); + effect.setTargetTable("opportunity"); + action.setEffects(Arrays.asList(effect)); + GenTableColumn salespersonId = column( + "salesperson_id", "salespersonId", "Salesperson", "input", ""); + salespersonId.setIsRequired("1"); + GenTable opportunity = table("opportunity", salespersonId); + + BusinessActionRenderSupport.enrich(action, Arrays.asList(opportunity)); + + assertEquals("${current_user.id}", effect.getValues().get("salesperson_id")); + assertEquals(Arrays.asList("salesperson_id"), action.getCurrentUserFields()); + assertEquals(Arrays.asList(), fields(action)); + } + @Test public void infersTargetFieldsFromEffectsAndEveryRuleValueInRequestOrder() { diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/util/TypedBusinessActionCompilerTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/util/TypedBusinessActionCompilerTest.java index 5c34e74..b88025e 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/util/TypedBusinessActionCompilerTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/util/TypedBusinessActionCompilerTest.java @@ -140,6 +140,62 @@ public class TypedBusinessActionCompilerTest assertTrue(TypedBusinessActionCompiler.compile(project, borrowRecord).getActions().isEmpty()); } + @Test + public void rendersMissingRequiredCreateFieldsAsTypedActionInputs() + { + GenTable customer = table("customer_info", "CustomerInfo", "Customer", + column("id", "Long", "id", true, "Customer ID"), + column("name", "String", "name", false, "Customer name")); + GenTable opportunity = table("opportunity", "Opportunity", "Opportunity", + column("id", "Long", "id", true, "Opportunity ID"), + requiredColumn("customer_id", "Long", "customerId", "Customer"), + requiredColumn("opportunity_name", "String", "opportunityName", "Opportunity name"), + requiredColumn("amount", "BigDecimal", "amount", "Amount"), + requiredColumn("status", "String", "status", "Status"), + requiredColumn("salesperson_id", "Long", "salespersonId", "Salesperson ID"), + requiredColumn("create_time", "Date", "createTime", "Create time")); + GenProject project = new GenProject(); + project.setPackageName("com.example.crm"); + project.setTables(Arrays.asList(customer, opportunity)); + project.setFlowConfig(JSON.parseObject("{" + + "\"code\":\"opportunity_flow\",\"name\":\"Opportunity flow\"," + + "\"mainTable\":\"opportunity\",\"statusField\":\"status\"," + + "\"states\":[{\"code\":\"new\",\"label\":\"New\"}]," + + "\"actions\":[{\"code\":\"create_opportunity\",\"name\":\"Create opportunity\"," + + "\"actor\":\"salesperson\"," + + "\"ownerTable\":\"customer_info\",\"method\":\"POST\"," + + "\"path\":\"/customer-info/create-opportunity\",\"effects\":[{" + + "\"type\":\"create_record\",\"table\":\"opportunity\"," + + "\"values\":{\"customer_id\":\"record.id\",\"status\":\"new\"}}]}]}", + SimpleFlowConfig.class)); + + TypedBusinessActionRenderModel model = TypedBusinessActionCompiler.compile(project, customer); + VelocityContext context = VelocityUtils.prepareContext(customer); + context.put("typedBusinessActionModel", model); + context.put("typedBusinessActions", model.getActions()); + context.put("typedBusinessDependencies", model.getDependencies()); + context.put("hasTypedBusinessActions", !model.getActions().isEmpty()); + context.put("typedBusinessUsesBigDecimal", model.isUsesBigDecimal()); + context.put("typedBusinessUsesDate", model.isUsesDate()); + + VelocityInitializer.initVelocity(); + String implementation = render("qing/serviceImpl.java.vm", context); + String service = render("qing/service.java.vm", context); + String controller = render("qing/controller.java.vm", context); + + assertTrue(implementation.contains("opportunity.setOpportunityName(inputOpportunityName);")); + assertTrue(implementation.contains("opportunity.setAmount(inputAmount);")); + assertTrue(implementation.contains("opportunity.setSalespersonId(currentUserId);")); + assertTrue(implementation.contains("opportunity.setCreateTime(new Date());")); + assertTrue(service.contains("void create_opportunity(Long id, Long currentUserId, " + + "String inputOpportunityName, BigDecimal inputAmount);")); + assertTrue(controller.contains("Missing business action parameter: opportunity_name")); + assertTrue(controller.contains("String inputOpportunityName = String.valueOf(")); + assertTrue(controller.contains("BigDecimal inputAmount = new BigDecimal(String.valueOf(")); + assertTrue(controller.contains("customerInfoService.create_opportunity(id, currentUser.getUserId(), " + + "inputOpportunityName, inputAmount);")); + } + @Test public void rendersJavaSafeMethodNameForKeywordActionCode() { diff --git a/RuoYi-Vue/sql/prompt_create_record_action_input_upgrade_20260715.sql b/RuoYi-Vue/sql/prompt_create_record_action_input_upgrade_20260715.sql new file mode 100644 index 0000000..cfc95a7 --- /dev/null +++ b/RuoYi-Vue/sql/prompt_create_record_action_input_upgrade_20260715.sql @@ -0,0 +1,135 @@ +-- Align managed flow/database Prompts with deterministic create_record action inputs. +-- Idempotent: each Prompt is versioned only when its current release lacks the marker. + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS upgrade_create_record_action_input_prompts_20260715$$ + +CREATE PROCEDURE upgrade_create_record_action_input_prompts_20260715() +BEGIN + DECLARE v_done INT DEFAULT 0; + DECLARE v_template_id BIGINT; + DECLARE v_current_version_id BIGINT; + DECLARE v_new_version_id BIGINT; + DECLARE v_version_no INT; + DECLARE v_version_label VARCHAR(50); + DECLARE v_generate_type VARCHAR(50); + DECLARE v_prompt_code VARCHAR(100); + DECLARE v_system_prompt LONGTEXT; + DECLARE v_user_prompt_contract VARCHAR(255); + DECLARE v_user_prompt_template LONGTEXT; + DECLARE v_target_system_prompt LONGTEXT; + DECLARE v_target_user_prompt LONGTEXT; + DECLARE v_provider_code VARCHAR(50); + DECLARE v_model VARCHAR(100); + DECLARE v_pipeline_release_code VARCHAR(100); + DECLARE v_content_hash CHAR(64); + DECLARE v_marker VARCHAR(100); + DECLARE v_rule LONGTEXT; + + DECLARE prompt_cursor CURSOR FOR + SELECT t.prompt_template_id, t.current_version_id, t.generate_type, t.prompt_code, + v.system_prompt, v.user_prompt_contract, v.user_prompt_template, + v.provider_code, v.model, v.pipeline_release_code + FROM factory_prompt_template t + JOIN factory_prompt_version v ON v.prompt_version_id = t.current_version_id + WHERE t.prompt_code IN ('front.flow_config', 'front.database') + ORDER BY t.prompt_template_id; + + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + RESIGNAL; + END; + DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_done = 1; + + START TRANSACTION; + OPEN prompt_cursor; + + prompt_loop: LOOP + FETCH prompt_cursor INTO v_template_id, v_current_version_id, v_generate_type, v_prompt_code, + v_system_prompt, v_user_prompt_contract, v_user_prompt_template, + v_provider_code, v_model, v_pipeline_release_code; + IF v_done = 1 THEN + LEAVE prompt_loop; + END IF; + + SET v_target_system_prompt = v_system_prompt; + SET v_target_user_prompt = v_user_prompt_template; + + IF v_prompt_code = 'front.flow_config' THEN + SET v_marker = 'CREATE_RECORD_ACTION_INPUT_V1'; + SET v_rule = CONCAT( + '13. CREATE_RECORD_ACTION_INPUT_V1: Rule 7 coverage applies to values derivable ', + 'from currentUserId, now, ownerTable record fields, or constants before the database ', + 'schema is finalized. Meaningful business fields that must be entered when the action ', + 'runs may be omitted from values; after schema finalization every omitted non-generated ', + 'required column becomes a required typed action input. Never invent record.x unless x ', + 'is a real ownerTable field.'); + IF LOCATE(v_marker, v_system_prompt) > 0 THEN + ITERATE prompt_loop; + END IF; + SET v_target_system_prompt = CONCAT(v_system_prompt, CHAR(10), v_rule); + ELSE + SET v_marker = 'CREATE_RECORD_NULLABILITY_V1'; + SET v_rule = CONCAT( + 'CREATE_RECORD_NULLABILITY_V1: Required create_record target columns omitted from ', + 'flowConfig values are intentional typed action inputs; keep meaningful columns required ', + 'instead of weakening them to nullable. When a create_record value maps record.source_field ', + 'into a target column with isRequired=1, the ownerTable source_field must also use ', + 'isRequired=1 so the generated insert cannot propagate null.'); + IF LOCATE(v_marker, v_user_prompt_template) > 0 THEN + ITERATE prompt_loop; + END IF; + SET v_target_user_prompt = CONCAT(v_user_prompt_template, CHAR(10), v_rule); + END IF; + + SELECT COALESCE(MAX(version_no), 0) + 1 + INTO v_version_no + FROM factory_prompt_version + WHERE prompt_template_id = v_template_id + FOR UPDATE; + SET v_version_label = CONCAT('V', v_version_no); + SET v_content_hash = SHA2(CONCAT( + '{"generateType":', JSON_QUOTE(v_generate_type), + ',"promptCode":', JSON_QUOTE(v_prompt_code), + ',"systemPrompt":', JSON_QUOTE(v_target_system_prompt), + ',"userPromptContract":', JSON_QUOTE(v_user_prompt_contract), + ',"userPromptTemplate":', JSON_QUOTE(v_target_user_prompt), + ',"version":', JSON_QUOTE(v_version_label), '}'), 256); + + INSERT INTO factory_prompt_version + (prompt_template_id, version_no, version_label, system_prompt, + user_prompt_contract, user_prompt_template, provider_code, model, + content_hash, status, parent_version_id, pipeline_release_code, + published_by, published_at, create_by, create_time, remark) + VALUES + (v_template_id, v_version_no, v_version_label, v_target_system_prompt, + v_user_prompt_contract, v_target_user_prompt, v_provider_code, v_model, + v_content_hash, 'DRAFT', v_current_version_id, v_pipeline_release_code, + '', NULL, 'system-upgrade', NOW(), + 'Align create_record required fields with typed action inputs'); + SET v_new_version_id = LAST_INSERT_ID(); + + UPDATE factory_prompt_version + SET status = 'RETIRED' + WHERE prompt_template_id = v_template_id + AND status = 'PUBLISHED' + AND prompt_version_id <> v_new_version_id; + UPDATE factory_prompt_version + SET status = 'PUBLISHED', published_by = 'system-upgrade', published_at = NOW() + WHERE prompt_version_id = v_new_version_id AND status = 'DRAFT'; + UPDATE factory_prompt_template + SET current_version_id = v_new_version_id, + update_by = 'system-upgrade', update_time = NOW() + WHERE prompt_template_id = v_template_id; + END LOOP; + + CLOSE prompt_cursor; + COMMIT; +END$$ + +CALL upgrade_create_record_action_input_prompts_20260715()$$ +DROP PROCEDURE upgrade_create_record_action_input_prompts_20260715$$ + +DELIMITER ;