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

142 lines
5.7 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 Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using global::BuildFlow.Shared.Domain.Common;
using Workflow.Application.Engine;
using Workflow.Domain.Engine;
using Workflow.Domain.Entities;
using Workflow.Domain.Enums;
using Workflow.Domain.Expressions;
using Workflow.Infrastructure.Persistence;
using TaskStatus = global::Workflow.Domain.Enums.TaskStatus;
using InstanceStatus = global::Workflow.Domain.Enums.InstanceStatus;
using ShapeMap = global::Workflow.Domain.Engine.WorkflowShapeMap;
using Xunit;
namespace BuildFlow.Workflow.Tests.Integration;
/// <summary>
/// 钉钉式审批流全链路端到端测试:树 → 编译 → 图落库读回 → ProcessEngine 启动实例 → 审批完成。
/// 这条链路证明:前端树编辑器产出的树,经 TreeToGraphCompiler 编译后,能被真实 ProcessEngine
/// 加载运行并完成审批。是"钉钉式设计器重做"最权威的运行时验证。
/// </summary>
public class TreeEndToEndTests
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
/// <summary>
/// 构造一棵"发起人 → 依次审批(A,B) → 结束"的树,编译成图,启动实例,A 通过后建 B,
/// B 通过后实例完成。
/// </summary>
[Fact]
public async Task Tree_ApprovalChain_RunsThroughEngine_ToCompletion()
{
// 1. 构造树
var userA = Guid.NewGuid();
var userB = Guid.NewGuid();
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 = "sequential",
assigneeRules = new[] { $"user:{userA}", $"user:{userB}" },
},
},
},
};
var treeJson = JsonSerializer.Serialize(tree, JsonOptions);
// 2. 编译成图
var parsedTree = JsonSerializer.Deserialize<WorkflowTree>(treeJson, JsonOptions)!;
var compiledGraph = TreeToGraphCompiler.Compile(parsedTree);
var graphJson = JsonSerializer.Serialize(compiledGraph, JsonOptions);
// 编译产物必须通过发布校验
var act = () => WorkflowGraphValidator.Validate(compiledGraph);
act.Should().NotThrow();
// 3. 落库(DefinitionJson = 编译产物)并读回,模拟引擎从快照加载
var options = new DbContextOptionsBuilder<WorkflowDbContext>()
.UseInMemoryDatabase($"tree-e2e-{Guid.NewGuid()}").Options;
var db = new WorkflowDbContext(options);
db.SavingChanges += (sender, _) =>
{
if (sender is not DbContext ctx) return;
foreach (var entry in ctx.ChangeTracker.Entries())
{
if (entry.Entity is IAuditable a) { a.CreatedBy ??= "test"; a.UpdatedBy ??= "test"; }
if (entry.Entity is IHasOperatorIP o) { o.OperatorIP ??= "127.0.0.1"; }
}
};
// 引擎 ProcessEngine.LoadGraphAsync 优先读 DefinitionJson(整图 JSON)。
// 把编译产物作为 WorkflowDefinition.DefinitionJson 存入。
var definition = new WorkflowDefinition
{
Id = Guid.NewGuid(),
Name = "依次审批流程",
Code = $"seq-e2e-{Guid.NewGuid():N}",
Status = DefinitionStatus.Published,
Version = 1,
IsEnabled = true,
DefinitionJson = graphJson,
DefinitionTree = treeJson,
CreatedBy = "test",
UpdatedBy = "test",
};
db.WorkflowDefinitions.Add(definition);
await db.SaveChangesAsync();
// 4. 启动实例
var engine = new ProcessEngine(db, new ServiceCollection().BuildServiceProvider(), new ConditionEvaluator());
var instance = new WorkflowInstance
{
Id = Guid.NewGuid(),
DefinitionId = definition.Id,
DefinitionCode = definition.Code,
DefinitionVersion = 1,
Title = "e2e-instance",
Status = InstanceStatus.Pending,
Variables = "{}",
InitiatorId = Guid.NewGuid(),
CreatedBy = "test",
UpdatedBy = "test",
};
db.WorkflowInstances.Add(instance);
await db.SaveChangesAsync();
await engine.StartAsync(instance);
// 启动后:依次审批只建 A 的任务(SequentialOrder=0)
var tasks = await db.WorkflowTasks
.Where(t => t.Status == TaskStatus.Pending)
.ToListAsync();
tasks.Should().HaveCount(1);
tasks[0].AssigneeId.Should().Be(userA, "依次审批首个任务应分配给 A");
tasks[0].SequentialOrder.Should().Be(0);
// 5. A 通过 → 建 B 的任务,实例未完成
await engine.CompleteTaskAsync(tasks[0], TaskResult.Approved);
var taskB = await db.WorkflowTasks
.SingleOrDefaultAsync(t => t.SequentialOrder == 1 && t.Status == TaskStatus.Pending);
taskB.Should().NotBeNull("A 通过后应建 B 的任务");
taskB!.AssigneeId.Should().Be(userB);
instance.Status.Should().NotBe(InstanceStatus.Completed, "B 未审完,实例不应完成");
// 6. B 通过 → 实例完成(token 到达 End)
await engine.CompleteTaskAsync(taskB, TaskResult.Approved);
instance.Status.Should().Be(InstanceStatus.Completed, "依次审批全部通过后实例应完成");
}
}