xiangning 74e99850e5 feat(workflow): 添加钉钉式节点级字段权限和依次审批功能
- 实现钉钉式节点级字段权限:从审批节点config解析fieldPermissions进行表单校验
- 添加依次审批模式支持:按顺序逐个创建审批任务并推进流程
- 扩展工作流定义实体:新增DefinitionTree字段存储钉式审批流树JSON
- 更新表单验证器:支持节点级字段权限配置影响必填校验逻辑
- 重构后台服务:修复OperationCanceledException处理防止进程崩溃
- 扩展脚本执行服务:支持加载各模块Application/Domain程序集
- 更新菜单配置:调整组件管理和脚本函数菜单顺序
- 完善工作流引擎:修复实例图加载和任务完成逻辑
2026-07-17 17:05:35 +08:00

205 lines
8.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Text.Json;
using FluentAssertions;
using Workflow.Application.Engine;
using Workflow.Application.Features.WorkflowDefinitions.Commands;
using Workflow.Application.Features.WorkflowDefinitions.Handlers;
using Workflow.Domain.Engine;
using Workflow.Domain.Entities;
using Workflow.Domain.Enums;
using Workflow.Infrastructure.Persistence;
using Xunit;
using ShapeMap = global::Workflow.Domain.Engine.WorkflowShapeMap;
namespace BuildFlow.Workflow.Tests.Integration;
/// <summary>
/// 钉钉式树定义的端到端发布测试:构造带 DefinitionTree 的草稿定义 →
/// 发布 → 验证编译产出 DefinitionJson、通过图校验、生成不可变快照、状态变 Published。
/// 这条链路是「树编辑器保存 → 发布 → 引擎可运行」的核心保障。
/// </summary>
public class TreeDefinitionPublishTests
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
/// <summary>构造一棵典型的钉钉式树:发起人 → 审批 → 条件分支(大额/默认) → 抄送。</summary>
private static string BuildTreeJson()
{
var tree = new
{
root = new
{
id = Guid.NewGuid().ToString(),
type = "Initiator",
config = new { title = "发起人" },
child = new
{
id = Guid.NewGuid().ToString(),
type = "Approval",
config = new
{
title = "主管审批",
approvalMode = "countersign",
assigneeRules = new[] { "user:11111111-1111-1111-1111-111111111111" },
},
child = new
{
id = Guid.NewGuid().ToString(),
type = "ConditionBranch",
config = new { title = "金额分支" },
branches = new object[]
{
new
{
id = Guid.NewGuid().ToString(),
label = "大额",
condition = "amount > 5000",
child = new
{
id = Guid.NewGuid().ToString(),
type = "Approval",
config = new
{
title = "总监审批",
approvalMode = "countersign",
assigneeRules = new[] { "user:22222222-2222-2222-2222-222222222222" },
},
},
},
new
{
id = Guid.NewGuid().ToString(),
label = "默认",
condition = (string?)null,
child = (object?)null,
},
},
child = new
{
id = Guid.NewGuid().ToString(),
type = "Cc",
config = new
{
title = "抄送财务",
recipients = new[] { "user:33333333-3333-3333-3333-333333333333" },
},
},
},
},
},
};
return JsonSerializer.Serialize(tree, JsonOptions);
}
[Fact]
public async Task Publish_TreeDefinition_CompilesToValidGraph_AndCreatesSnapshot()
{
// Arrange:构造一个 Draft 草稿,只填 DefinitionTree(不填 DefinitionJson)
await using var db = TestDbFactory.Create();
var definitionId = Guid.NewGuid();
db.WorkflowDefinitions.Add(new WorkflowDefinition
{
Id = definitionId,
Name = "报销审批",
Code = $"expense-{definitionId:N}",
Status = DefinitionStatus.Draft,
Version = 1,
IsEnabled = true,
DefinitionTree = BuildTreeJson(),
});
await db.SaveChangesAsync();
// Act:发布
var handler = new PublishWorkflowDefinitionCommandHandler(db);
await handler.Handle(new PublishWorkflowDefinitionCommand(definitionId), CancellationToken.None);
// Assert:定义状态变 Published、版本自增、DefinitionJson 被编译出来
var updated = await db.WorkflowDefinitions.FindAsync(definitionId);
updated.Should().NotBeNull();
updated!.Status.Should().Be(DefinitionStatus.Published);
updated.Version.Should().Be(2);
updated.DefinitionJson.Should().NotBeNullOrEmpty("树必须被编译成图");
// 编译出的图必须能被引擎解析且通过校验
var graph = WorkflowGraphParser.Parse(updated.DefinitionJson);
graph.Nodes.Should().NotBeEmpty();
var act = () => WorkflowGraphValidator.Validate(graph);
act.Should().NotThrow("编译产物必须通过发布校验");
// 图结构断言:含 Start/End/Approval/Cc/Condition
graph.Nodes.Should().Contain(n => n.Shape == ShapeMap.Shapes.Start);
graph.Nodes.Should().Contain(n => n.Shape == ShapeMap.Shapes.End);
graph.Nodes.Should().Contain(n => n.Shape == ShapeMap.Shapes.Approval);
graph.Nodes.Should().Contain(n => n.Shape == ShapeMap.Shapes.Cc);
graph.Nodes.Should().Contain(n => n.Shape == ShapeMap.Shapes.Condition);
// 快照已生成,且 DefinitionTree/DefinitionJson 都写进快照
var snapshot = db.WorkflowDefinitionSnapshots
.FirstOrDefault(s => s.DefinitionId == definitionId && s.Version == 2);
snapshot.Should().NotBeNull();
snapshot!.DefinitionJson.Should().NotBeNullOrEmpty();
snapshot.DefinitionTree.Should().NotBeNullOrEmpty("快照应同时保存树供审计回显");
}
[Fact]
public async Task Publish_LegacyDefinitionWithoutTree_StillWorks()
{
// 兼容老的自由画布定义(无 DefinitionTree,只有 DefinitionJson)也能发布
await using var db = TestDbFactory.Create();
var definitionId = Guid.NewGuid();
// 手工构造一个最小合法图:Start → End
var minGraph = new
{
nodes = new[]
{
new { id = Guid.NewGuid().ToString(), shape = "wf-start", data = new { title = "开始" } },
new { id = Guid.NewGuid().ToString(), shape = "wf-end", data = new { title = "结束" } },
},
edges = new[]
{
new
{
id = Guid.NewGuid().ToString(),
shape = "wf-edge",
source = new { cell = "" },
target = new { cell = "" },
data = new { edgeType = 0 },
},
},
};
// 填好 source/target 指向上面两个节点
var nodes = minGraph.nodes;
var edges = new[]
{
new
{
id = Guid.NewGuid().ToString(),
shape = "wf-edge",
source = new { cell = nodes[0].id },
target = new { cell = nodes[1].id },
data = new { edgeType = 0 },
},
};
var graphJson = JsonSerializer.Serialize(new { nodes, edges }, JsonOptions);
db.WorkflowDefinitions.Add(new WorkflowDefinition
{
Id = definitionId,
Name = "老流程",
Code = $"legacy-{definitionId:N}",
Status = DefinitionStatus.Draft,
Version = 1,
IsEnabled = true,
DefinitionJson = graphJson,
});
await db.SaveChangesAsync();
var handler = new PublishWorkflowDefinitionCommandHandler(db);
var act = () => handler.Handle(new PublishWorkflowDefinitionCommand(definitionId), CancellationToken.None);
await act.Should().NotThrowAsync("无树的老定义走原 DefinitionJson 路径");
var updated = await db.WorkflowDefinitions.FindAsync(definitionId);
updated!.Status.Should().Be(DefinitionStatus.Published);
}
}