refactor: SchemaValidator supports dynamic component whitelist from registry

This commit is contained in:
向宁 2026-05-25 14:16:34 +08:00
parent d704c02d18
commit 84ccc48615
3 changed files with 172 additions and 67 deletions

View File

@ -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;
/// <summary>
/// 创建表单定义:接收 Formily JSON Schema校验后存入 SchemaJson。
/// </summary>
public record CreateFormDefinitionCommand(
string Name,
string Code,
string? Description,
List<FormFieldDto> Fields
string SchemaJson
) : IRequest<FormDefinitionDto>;
public class CreateFormDefinitionCommandHandler(WorkflowDbContext db)
@ -20,7 +23,6 @@ public class CreateFormDefinitionCommandHandler(WorkflowDbContext db)
{
public async Task<FormDefinitionDto> 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);

View File

@ -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;
/// <summary>
/// 更新表单定义:接收新的 Formily JSON Schema校验后替换 SchemaJson 并递增 Version。
/// </summary>
public record UpdateFormDefinitionCommand(
Guid Id,
string Name,
string? Description,
List<FormFieldDto> Fields
string SchemaJson
) : IRequest<FormDefinitionDto>;
public class UpdateFormDefinitionCommandHandler(WorkflowDbContext db)
@ -19,57 +23,25 @@ public class UpdateFormDefinitionCommandHandler(WorkflowDbContext db)
public async Task<FormDefinitionDto> 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);

View File

@ -0,0 +1,135 @@
using System.Text.Json;
namespace Workflow.Application.Form.Schema;
/// <summary>
/// 校验 Formily JSON Schema 的合法性,并从中提取数据字段摘要。
/// 校验规则:顶层必须是 object + properties节点类型和 x-component 必须在白名单内;
/// void 节点不能有 required/enum递归校验嵌套 properties。
/// </summary>
public static class SchemaValidator
{
private static readonly HashSet<string> AllowedTypes =
["string", "number", "integer", "boolean", "array", "object", "void"];
private static readonly HashSet<string> 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"
];
/// <summary>
/// 校验 Formily Schema 并提取数据字段摘要。
/// 当 allowedComponents 为 null 时,使用内置的 DefaultAllowedComponents 作为回退白名单。
/// </summary>
public static SchemaValidationResult Validate(string schemaJson, HashSet<string>? allowedComponents = null)
{
var allowed = allowedComponents ?? DefaultAllowedComponents;
var errors = new List<string>();
var fields = new List<FieldSummary>();
JsonElement root;
try
{
root = JsonSerializer.Deserialize<JsonElement>(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<string> errors,
List<FieldSummary> fields,
string parentPath,
HashSet<string> 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);
}
}
}