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); + } + } +}