- 实现钉钉式节点级字段权限:从审批节点config解析fieldPermissions进行表单校验 - 添加依次审批模式支持:按顺序逐个创建审批任务并推进流程 - 扩展工作流定义实体:新增DefinitionTree字段存储钉式审批流树JSON - 更新表单验证器:支持节点级字段权限配置影响必填校验逻辑 - 重构后台服务:修复OperationCanceledException处理防止进程崩溃 - 扩展脚本执行服务:支持加载各模块Application/Domain程序集 - 更新菜单配置:调整组件管理和脚本函数菜单顺序 - 完善工作流引擎:修复实例图加载和任务完成逻辑
233 lines
9.3 KiB
C#
233 lines
9.3 KiB
C#
using global::BuildFlow.Shared.Domain.Common;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using Workflow.Application.Engine;
|
||
using Workflow.Domain.Entities;
|
||
using Workflow.Domain.Enums;
|
||
using Workflow.Domain.Expressions;
|
||
using TaskStatus = Workflow.Domain.Enums.TaskStatus;
|
||
using InstanceStatus = Workflow.Domain.Enums.InstanceStatus;
|
||
|
||
namespace BuildFlow.Workflow.Tests.Engine;
|
||
|
||
using FluentAssertions;
|
||
using Xunit;
|
||
|
||
/// <summary>
|
||
/// 依次审批(Sequential)测试:A → B → C 按顺序逐个审,全部通过才推进,任一驳回中断。
|
||
/// 这是钉钉式审批流的核心审批方式,区别于会签(同时)/或签(任一)。
|
||
/// </summary>
|
||
public class SequentialApprovalTests
|
||
{
|
||
private readonly WorkflowDbContext _dbContext;
|
||
private readonly ProcessEngine _engine;
|
||
|
||
public SequentialApprovalTests()
|
||
{
|
||
var options = new DbContextOptionsBuilder<WorkflowDbContext>()
|
||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||
.Options;
|
||
_dbContext = new WorkflowDbContext(options);
|
||
|
||
// InMemory 不跑审计拦截器,手动填充非空审计字段
|
||
_dbContext.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";
|
||
}
|
||
}
|
||
};
|
||
|
||
_engine = new ProcessEngine(_dbContext, new ServiceCollection().BuildServiceProvider(), new ConditionEvaluator());
|
||
}
|
||
|
||
/// <summary>启动依次审批节点:只创建第 0 个审批人的任务,记录完整序列。</summary>
|
||
[Fact]
|
||
public async Task Sequential_Start_CreatesOnlyFirstAssigneeTask()
|
||
{
|
||
var (approvalNode, instance, endNode) = await SetupSequentialFlow(3);
|
||
|
||
await _engine.StartAsync(instance);
|
||
|
||
var tasks = await _dbContext.WorkflowTasks
|
||
.Where(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending)
|
||
.ToListAsync();
|
||
|
||
// 只建第 0 个任务(区别于会签一次性建 3 个)
|
||
tasks.Should().HaveCount(1);
|
||
tasks[0].SequentialOrder.Should().Be(0);
|
||
tasks[0].SequentialAssignees.Should().NotBeNull("应记录完整序列供逐个推进");
|
||
tasks[0].SequentialAssignees!.Should().Contain("user:");
|
||
}
|
||
|
||
/// <summary>依次审批:第 1 个通过后建第 2 个任务,token 不推进。</summary>
|
||
[Fact]
|
||
public async Task Sequential_FirstApproved_CreatesSecondTaskWithoutPropagating()
|
||
{
|
||
var (approvalNode, instance, endNode) = await SetupSequentialFlow(3);
|
||
|
||
await _engine.StartAsync(instance);
|
||
|
||
var firstTask = await _dbContext.WorkflowTasks
|
||
.FirstAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending);
|
||
|
||
// 第 1 个通过
|
||
await _engine.CompleteTaskAsync(firstTask, TaskResult.Approved);
|
||
|
||
// 第 1 个任务已 Approved
|
||
firstTask.Status.Should().Be(TaskStatus.Approved);
|
||
|
||
// 新建了第 2 个任务(SequentialOrder=1),仍是 Pending
|
||
var secondTask = await _dbContext.WorkflowTasks
|
||
.SingleOrDefaultAsync(t => t.NodeId == approvalNode.Id && t.SequentialOrder == 1 && t.Status == TaskStatus.Pending);
|
||
secondTask.Should().NotBeNull("应创建下一个审批人任务");
|
||
secondTask!.SequentialAssignees.Should().Be(firstTask.SequentialAssignees);
|
||
|
||
// token 未推进到 End
|
||
var endTokens = await _dbContext.WorkflowTokens.CountAsync(t => t.NodeId == endNode.Id);
|
||
endTokens.Should().Be(0, "依次审批未完成,token 不应到达 End");
|
||
}
|
||
|
||
/// <summary>依次审批:全部通过后推进到下游 End。</summary>
|
||
[Fact]
|
||
public async Task Sequential_AllApproved_PropagatesDownstream()
|
||
{
|
||
var (approvalNode, instance, endNode) = await SetupSequentialFlow(3);
|
||
|
||
await _engine.StartAsync(instance);
|
||
|
||
// 逐个通过 3 个审批人
|
||
for (var i = 0; i < 3; i++)
|
||
{
|
||
var current = await _dbContext.WorkflowTasks
|
||
.FirstAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending);
|
||
await _engine.CompleteTaskAsync(current, TaskResult.Approved);
|
||
}
|
||
|
||
// 全部通过后 token 推进,实例完成
|
||
instance.Status.Should().Be(InstanceStatus.Completed);
|
||
}
|
||
|
||
/// <summary>依次审批:中间任一驳回则中断(走 Rejected 边语义;本测试无 Rejected 边故验证不继续建任务)。</summary>
|
||
[Fact]
|
||
public async Task Sequential_RejectedInMiddle_StopsSequentialChain()
|
||
{
|
||
var user1 = Guid.NewGuid();
|
||
var user2 = Guid.NewGuid();
|
||
var approvalNode = new WorkflowNode
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
NodeType = NodeType.Approval,
|
||
Name = "seq",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
Config = $$"""{ "approvalMode": "sequential", "assigneeRules": ["user:{{user1}}", "user:{{user2}}"] }""",
|
||
};
|
||
// 提供一条 Rejected 边回到自己(避免 No reject edge 异常),再一条 Normal 边到 End
|
||
var startNode = CreateNode(NodeType.Start);
|
||
var endNode = CreateNode(NodeType.End);
|
||
var edgeStart = CreateEdge(startNode, approvalNode);
|
||
var edgeReject = CreateEdge(approvalNode, approvalNode);
|
||
edgeReject.EdgeType = EdgeType.Rejected;
|
||
var edgeApprove = CreateEdge(approvalNode, endNode);
|
||
|
||
var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edgeReject, edgeApprove]);
|
||
await _engine.StartAsync(instance);
|
||
|
||
var firstTask = await _dbContext.WorkflowTasks
|
||
.FirstAsync(t => t.NodeId == approvalNode.Id && t.Status == TaskStatus.Pending);
|
||
|
||
// 第 1 个驳回 → 走 Rejected 边,不建第 2 个任务
|
||
await _engine.CompleteTaskAsync(firstTask, TaskResult.Rejected);
|
||
firstTask.Status.Should().Be(TaskStatus.Rejected);
|
||
|
||
var secondTask = await _dbContext.WorkflowTasks
|
||
.CountAsync(t => t.NodeId == approvalNode.Id && t.SequentialOrder == 1);
|
||
secondTask.Should().Be(0, "驳回后不应继续依次建任务");
|
||
}
|
||
|
||
// ===== Setup helpers(与 MultiApprovalTests 同范式)=====
|
||
|
||
private async Task<(WorkflowNode ApprovalNode, WorkflowInstance Instance, WorkflowNode EndNode)> SetupSequentialFlow(int assigneeCount)
|
||
{
|
||
var users = Enumerable.Range(0, assigneeCount).Select(_ => Guid.NewGuid()).ToArray();
|
||
var rules = users.Select(u => $"user:{u}").ToArray();
|
||
var rulesJson = System.Text.Json.JsonSerializer.Serialize(rules);
|
||
var approvalNode = new WorkflowNode
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
NodeType = NodeType.Approval,
|
||
Name = "依次审批",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
Config = $$"""{ "approvalMode": "sequential", "assigneeRules": {{rulesJson}} }""",
|
||
};
|
||
var startNode = CreateNode(NodeType.Start);
|
||
var endNode = CreateNode(NodeType.End);
|
||
var edgeStart = CreateEdge(startNode, approvalNode);
|
||
var edge = CreateEdge(approvalNode, endNode);
|
||
|
||
var (_, instance) = PersistSetup([startNode, approvalNode, endNode], [edgeStart, edge]);
|
||
// PersistSetup 内部已 SaveChanges;返回前 await 无副作用,保持签名一致
|
||
await Task.CompletedTask;
|
||
return (approvalNode, instance, endNode);
|
||
}
|
||
|
||
private static WorkflowNode CreateNode(NodeType type) => new()
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
NodeType = type,
|
||
Name = type.ToString().ToLowerInvariant(),
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
};
|
||
|
||
private static WorkflowEdge CreateEdge(WorkflowNode source, WorkflowNode target) => new()
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
SourceNodeId = source.Id,
|
||
TargetNodeId = target.Id,
|
||
EdgeType = EdgeType.Normal,
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
};
|
||
|
||
private (WorkflowDefinition Definition, WorkflowInstance Instance) PersistSetup(
|
||
List<WorkflowNode> nodes, List<WorkflowEdge> edges)
|
||
{
|
||
var definition = new WorkflowDefinition
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
Name = "seq-test-flow",
|
||
Status = DefinitionStatus.Published,
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
};
|
||
foreach (var n in nodes) { n.DefinitionId = definition.Id; _dbContext.WorkflowNodes.Add(n); }
|
||
foreach (var e in edges) { e.DefinitionId = definition.Id; _dbContext.WorkflowEdges.Add(e); }
|
||
_dbContext.WorkflowDefinitions.Add(definition);
|
||
var instance = new WorkflowInstance
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
DefinitionId = definition.Id,
|
||
Title = "seq-test",
|
||
Status = InstanceStatus.Running,
|
||
Variables = "{}",
|
||
CreatedBy = "test",
|
||
UpdatedBy = "test",
|
||
};
|
||
_dbContext.WorkflowInstances.Add(instance);
|
||
_dbContext.SaveChanges();
|
||
return (definition, instance);
|
||
}
|
||
}
|