fix(generator): 修复 create_record 必填字段生成流程
This commit is contained in:
@@ -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; }
|
||||
|
||||
@@ -56,6 +56,7 @@ public class TypedBusinessActionRenderModel
|
||||
private boolean currentUserRequired;
|
||||
private String auditUserSetter;
|
||||
private String auditTimeSetter;
|
||||
private List<RequestParameter> requestParameters = new ArrayList<RequestParameter>();
|
||||
private List<Effect> effects = new ArrayList<Effect>();
|
||||
|
||||
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<RequestParameter> getRequestParameters() { return requestParameters; }
|
||||
public void setRequestParameters(List<RequestParameter> requestParameters) { this.requestParameters = requestParameters; }
|
||||
public List<Effect> getEffects() { return effects; }
|
||||
public void setEffects(List<Effect> 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;
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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<String> allMessages = database.getRepairMessages() == null
|
||||
@@ -183,6 +184,43 @@ public class BusinessLoopDatabaseContractReconciler
|
||||
}
|
||||
}
|
||||
|
||||
private void repairCreateRecordSourceRequirements(Map<String, DatabaseTableDesign> tables,
|
||||
SimpleFlowConfig flowConfig, List<String> 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<String, String> 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
|
||||
|
||||
@@ -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<String, DatabaseColumnDesign> columns, Effect effect,
|
||||
String targetTable)
|
||||
{
|
||||
if (columns == null) return;
|
||||
Map<String, String> 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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<String, GenTable> tables, Map<String, Dependency> 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<String, String> assignedFields)
|
||||
private static void appendImplicitCreateAssignments(TypedBusinessActionRenderModel model, Effect effect,
|
||||
GenTable target, Map<String, String> 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 + ")";
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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接口
|
||||
|
||||
@@ -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<String> 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();
|
||||
|
||||
@@ -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.<BusinessActionDesign>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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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 ;
|
||||
Reference in New Issue
Block a user