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

233 lines
9.3 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 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);
}
}