From 84ccc4861506227cd1459115e5c62bc797c349cf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=90=91=E5=AE=81?= <1772105645@qq.com>
Date: Mon, 25 May 2026 14:16:34 +0800
Subject: [PATCH] refactor: SchemaValidator supports dynamic component
whitelist from registry
---
.../Commands/CreateFormDefinitionCommand.cs | 38 +++--
.../Commands/UpdateFormDefinitionCommand.cs | 66 +++------
.../Form/Schema/SchemaValidator.cs | 135 ++++++++++++++++++
3 files changed, 172 insertions(+), 67 deletions(-)
create mode 100644 src/Workflow.Application/Form/Schema/SchemaValidator.cs
diff --git a/src/Workflow.Application/Form/FormDefinition/Commands/CreateFormDefinitionCommand.cs b/src/Workflow.Application/Form/FormDefinition/Commands/CreateFormDefinitionCommand.cs
index 9af2c12..6c002f8 100644
--- a/src/Workflow.Application/Form/FormDefinition/Commands/CreateFormDefinitionCommand.cs
+++ b/src/Workflow.Application/Form/FormDefinition/Commands/CreateFormDefinitionCommand.cs
@@ -1,18 +1,21 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using Workflow.Application.Form.DTOs;
-using Workflow.Domain.Entities;
+using Workflow.Application.Form.Schema;
using Workflow.Domain.Enums;
using Workflow.Domain.Exceptions;
using Workflow.Infrastructure.Persistence;
namespace Workflow.Application.Form.FormDefinition.Commands;
+///
+/// 创建表单定义:接收 Formily JSON Schema,校验后存入 SchemaJson。
+///
public record CreateFormDefinitionCommand(
string Name,
string Code,
string? Description,
- List Fields
+ string SchemaJson
) : IRequest;
public class CreateFormDefinitionCommandHandler(WorkflowDbContext db)
@@ -20,7 +23,6 @@ public class CreateFormDefinitionCommandHandler(WorkflowDbContext db)
{
public async Task Handle(CreateFormDefinitionCommand request, CancellationToken cancellationToken)
{
- // 检查编码是否重复
var exists = await db.FormDefinitions
.AnyAsync(f => f.Code == request.Code, cancellationToken);
if (exists)
@@ -28,6 +30,18 @@ public class CreateFormDefinitionCommandHandler(WorkflowDbContext db)
throw new BusinessException($"表单编码 {request.Code} 已存在");
}
+ var components = await db.FormComponentRegistries
+ .Where(c => c.IsActive)
+ .Select(c => c.Name)
+ .ToListAsync(cancellationToken);
+ var allowedSet = components.ToHashSet();
+
+ var validation = SchemaValidator.Validate(request.SchemaJson, allowedSet);
+ if (!validation.IsValid)
+ {
+ throw new BusinessException($"Schema 校验失败: {string.Join("; ", validation.Errors)}");
+ }
+
var entity = new Domain.Entities.FormDefinition
{
Id = Guid.NewGuid(),
@@ -36,25 +50,9 @@ public class CreateFormDefinitionCommandHandler(WorkflowDbContext db)
Description = request.Description,
Version = 1,
Status = FormStatus.Draft,
- SchemaJson = "{}",
+ SchemaJson = request.SchemaJson,
};
- foreach (var fieldDto in request.Fields)
- {
- entity.Fields.Add(new FormDefinitionField
- {
- Id = Guid.NewGuid(),
- FormDefinitionId = entity.Id,
- FieldKey = fieldDto.FieldKey,
- Label = fieldDto.Label,
- FieldType = fieldDto.FieldType,
- Required = fieldDto.Required,
- DefaultValue = fieldDto.DefaultValue,
- Config = fieldDto.Config,
- SortOrder = fieldDto.SortOrder,
- });
- }
-
db.FormDefinitions.Add(entity);
await db.SaveChangesAsync(cancellationToken);
diff --git a/src/Workflow.Application/Form/FormDefinition/Commands/UpdateFormDefinitionCommand.cs b/src/Workflow.Application/Form/FormDefinition/Commands/UpdateFormDefinitionCommand.cs
index e2ff7cf..0644f5f 100644
--- a/src/Workflow.Application/Form/FormDefinition/Commands/UpdateFormDefinitionCommand.cs
+++ b/src/Workflow.Application/Form/FormDefinition/Commands/UpdateFormDefinitionCommand.cs
@@ -1,16 +1,20 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using Workflow.Application.Form.DTOs;
+using Workflow.Application.Form.Schema;
using Workflow.Domain.Exceptions;
using Workflow.Infrastructure.Persistence;
namespace Workflow.Application.Form.FormDefinition.Commands;
+///
+/// 更新表单定义:接收新的 Formily JSON Schema,校验后替换 SchemaJson 并递增 Version。
+///
public record UpdateFormDefinitionCommand(
Guid Id,
string Name,
string? Description,
- List Fields
+ string SchemaJson
) : IRequest;
public class UpdateFormDefinitionCommandHandler(WorkflowDbContext db)
@@ -19,57 +23,25 @@ public class UpdateFormDefinitionCommandHandler(WorkflowDbContext db)
public async Task Handle(UpdateFormDefinitionCommand request, CancellationToken cancellationToken)
{
var entity = await db.FormDefinitions
- .Include(f => f.Fields)
.FirstOrDefaultAsync(f => f.Id == request.Id, cancellationToken)
?? throw new NotFoundException($"表单 {request.Id} 不存在");
+ var components = await db.FormComponentRegistries
+ .Where(c => c.IsActive)
+ .Select(c => c.Name)
+ .ToListAsync(cancellationToken);
+ var allowedSet = components.ToHashSet();
+
+ var validation = SchemaValidator.Validate(request.SchemaJson, allowedSet);
+ if (!validation.IsValid)
+ {
+ throw new BusinessException($"Schema 校验失败: {string.Join("; ", validation.Errors)}");
+ }
+
entity.Name = request.Name;
entity.Description = request.Description;
-
- // 根据 FieldKey 匹配,处理字段的增删改
- var existingFields = entity.Fields.ToDictionary(f => f.FieldKey);
- var requestedKeys = request.Fields.Select(f => f.FieldKey).ToHashSet();
-
- // 删除不在请求中的字段
- var fieldsToRemove = entity.Fields
- .Where(f => !requestedKeys.Contains(f.FieldKey))
- .ToList();
- foreach (var field in fieldsToRemove)
- {
- entity.Fields.Remove(field);
- db.FormDefinitionFields.Remove(field);
- }
-
- // 更新已有字段或新增字段
- foreach (var fieldDto in request.Fields)
- {
- if (existingFields.TryGetValue(fieldDto.FieldKey, out var existingField))
- {
- // 更新已有字段属性
- existingField.Label = fieldDto.Label;
- existingField.FieldType = fieldDto.FieldType;
- existingField.Required = fieldDto.Required;
- existingField.DefaultValue = fieldDto.DefaultValue;
- existingField.Config = fieldDto.Config;
- existingField.SortOrder = fieldDto.SortOrder;
- }
- else
- {
- // 新增字段
- entity.Fields.Add(new Domain.Entities.FormDefinitionField
- {
- Id = Guid.NewGuid(),
- FormDefinitionId = entity.Id,
- FieldKey = fieldDto.FieldKey,
- Label = fieldDto.Label,
- FieldType = fieldDto.FieldType,
- Required = fieldDto.Required,
- DefaultValue = fieldDto.DefaultValue,
- Config = fieldDto.Config,
- SortOrder = fieldDto.SortOrder,
- });
- }
- }
+ entity.SchemaJson = request.SchemaJson;
+ entity.Version++;
await db.SaveChangesAsync(cancellationToken);
diff --git a/src/Workflow.Application/Form/Schema/SchemaValidator.cs b/src/Workflow.Application/Form/Schema/SchemaValidator.cs
new file mode 100644
index 0000000..4297997
--- /dev/null
+++ b/src/Workflow.Application/Form/Schema/SchemaValidator.cs
@@ -0,0 +1,135 @@
+using System.Text.Json;
+
+namespace Workflow.Application.Form.Schema;
+
+///
+/// 校验 Formily JSON Schema 的合法性,并从中提取数据字段摘要。
+/// 校验规则:顶层必须是 object + properties;节点类型和 x-component 必须在白名单内;
+/// void 节点不能有 required/enum;递归校验嵌套 properties。
+///
+public static class SchemaValidator
+{
+ private static readonly HashSet AllowedTypes =
+ ["string", "number", "integer", "boolean", "array", "object", "void"];
+
+ private static readonly HashSet DefaultAllowedComponents =
+ [
+ "Input", "Input.TextArea", "Input.Password", "Input.Number",
+ "Select", "DatePicker", "TimePicker", "Switch",
+ "Radio.Group", "Checkbox.Group",
+ "Upload", "FormGrid", "FormLayout", "FormItem",
+ "ArrayItems", "ArrayItems.Item", "ArrayItems.Addition",
+ "ArrayItems.SortHandle", "ArrayItems.Remove",
+ "Space", "Card", "Tabs", "TabPane", "Collapse", "CollapsePanel",
+ "Editable", "Editable.Popover"
+ ];
+
+ ///
+ /// 校验 Formily Schema 并提取数据字段摘要。
+ /// 当 allowedComponents 为 null 时,使用内置的 DefaultAllowedComponents 作为回退白名单。
+ ///
+ public static SchemaValidationResult Validate(string schemaJson, HashSet? allowedComponents = null)
+ {
+ var allowed = allowedComponents ?? DefaultAllowedComponents;
+ var errors = new List();
+ var fields = new List();
+
+ JsonElement root;
+ try
+ {
+ root = JsonSerializer.Deserialize(schemaJson);
+ }
+ catch (JsonException ex)
+ {
+ return new SchemaValidationResult(false, [$"JSON 格式无效: {ex.Message}"], []);
+ }
+
+ if (root.ValueKind != JsonValueKind.Object)
+ {
+ return new SchemaValidationResult(false, ["Schema 必须是 JSON 对象"], []);
+ }
+
+ if (!root.TryGetProperty("type", out var typeProp) || typeProp.GetString() != "object")
+ {
+ errors.Add("Schema 顶层 type 必须为 'object'");
+ }
+
+ if (!root.TryGetProperty("properties", out var props) || props.ValueKind != JsonValueKind.Object)
+ {
+ errors.Add("Schema 顶层必须包含 properties 对象");
+ }
+ else
+ {
+ foreach (var prop in props.EnumerateObject())
+ {
+ ValidateNode(prop.Name, prop.Value, errors, fields, "", allowed);
+ }
+ }
+
+ return new SchemaValidationResult(errors.Count == 0, errors, fields);
+ }
+
+ private static void ValidateNode(
+ string name,
+ JsonElement node,
+ List errors,
+ List fields,
+ string parentPath,
+ HashSet allowed)
+ {
+ var path = string.IsNullOrEmpty(parentPath) ? name : $"{parentPath}.{name}";
+
+ if (node.ValueKind != JsonValueKind.Object)
+ {
+ errors.Add($"节点 {path} 必须是 JSON 对象");
+ return;
+ }
+
+ var typeValue = node.TryGetProperty("type", out var tp) ? tp.GetString() : null;
+ if (typeValue is null || !AllowedTypes.Contains(typeValue))
+ {
+ errors.Add($"节点 {path} 的 type '{typeValue}' 不合法,允许值: {string.Join(", ", AllowedTypes)}");
+ }
+
+ if (node.TryGetProperty("x-component", out var compProp))
+ {
+ var comp = compProp.GetString();
+ if (comp is not null && !allowed.Contains(comp))
+ {
+ errors.Add($"节点 {path} 的 x-component '{comp}' 不在允许列表中");
+ }
+ }
+
+ var isVoid = typeValue == "void";
+
+ if (isVoid && node.TryGetProperty("required", out _))
+ {
+ errors.Add($"节点 {path} 是 void 类型,不能设置 required");
+ }
+
+ if (isVoid && node.TryGetProperty("enum", out _))
+ {
+ errors.Add($"节点 {path} 是 void 类型,不能设置 enum");
+ }
+
+ if (!isVoid && typeValue is not null)
+ {
+ var title = node.TryGetProperty("title", out var titleProp) ? titleProp.GetString() : null;
+ var required = node.TryGetProperty("required", out var reqProp) && reqProp.GetBoolean();
+ fields.Add(new FieldSummary(path, typeValue, required, title));
+ }
+
+ if (node.TryGetProperty("properties", out var childProps) && childProps.ValueKind == JsonValueKind.Object)
+ {
+ foreach (var child in childProps.EnumerateObject())
+ {
+ ValidateNode(child.Name, child.Value, errors, fields, path, allowed);
+ }
+ }
+
+ if (node.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Object)
+ {
+ ValidateNode("items", items, errors, fields, path, allowed);
+ }
+ }
+}