fix: StartWorkflowInstance 调用 ProcessEngine 自动传播 Token

移除手动创建 Token 的代码,改为调用 processEngine.StartAsync(instance)
自动从 Start 节点传播 Token 到后续节点。
更新测试以注入 ProcessEngine 依赖。
This commit is contained in:
向宁 2026-05-20 20:44:46 +08:00
parent fc4ecbbacc
commit d9cf703615
8 changed files with 41 additions and 106 deletions

View File

@ -4,12 +4,8 @@ using Workflow.Application.Form.FormDefinition.Commands;
namespace Workflow.Api.Endpoints.Form; namespace Workflow.Api.Endpoints.Form;
public class PublishFormDefinitionEndpoint : Endpoint<PublishFormDefinitionRequest> public class PublishFormDefinitionEndpoint(IMediator mediator) : EndpointWithoutRequest
{ {
private readonly IMediator _mediator;
public PublishFormDefinitionEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure() public override void Configure()
{ {
Post("/forms/{Id}/publish"); Post("/forms/{Id}/publish");
@ -20,15 +16,10 @@ public class PublishFormDefinitionEndpoint : Endpoint<PublishFormDefinitionReque
}); });
} }
public override async Task HandleAsync(PublishFormDefinitionRequest req, CancellationToken ct) public override async Task HandleAsync(CancellationToken ct)
{ {
var command = new PublishFormDefinitionCommand(req.Id); var id = Route<Guid>("Id");
await _mediator.Send(command, ct); await mediator.Send(new PublishFormDefinitionCommand(id), ct);
await Send.OkAsync(ct); await Send.OkAsync(ct);
} }
} }
public class PublishFormDefinitionRequest
{
public Guid Id { get; set; }
}

View File

@ -4,12 +4,8 @@ using Workflow.Application.Features.WorkflowDefinitions.Commands;
namespace Workflow.Api.Endpoints.WorkflowDefinition; namespace Workflow.Api.Endpoints.WorkflowDefinition;
public class DisableWorkflowDefinitionEndpoint : Endpoint<DisableWorkflowDefinitionRequest> public class DisableWorkflowDefinitionEndpoint(IMediator mediator) : EndpointWithoutRequest
{ {
private readonly IMediator _mediator;
public DisableWorkflowDefinitionEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure() public override void Configure()
{ {
Post("/workflow-definitions/{Id}/disable"); Post("/workflow-definitions/{Id}/disable");
@ -20,15 +16,10 @@ public class DisableWorkflowDefinitionEndpoint : Endpoint<DisableWorkflowDefinit
}); });
} }
public override async Task HandleAsync(DisableWorkflowDefinitionRequest req, CancellationToken ct) public override async Task HandleAsync(CancellationToken ct)
{ {
var command = new DisableWorkflowDefinitionCommand(req.Id); var id = Route<Guid>("Id");
await _mediator.Send(command, ct); await mediator.Send(new DisableWorkflowDefinitionCommand(id), ct);
await Send.OkAsync(ct); await Send.OkAsync(ct);
} }
} }
public class DisableWorkflowDefinitionRequest
{
public Guid Id { get; set; }
}

View File

@ -4,12 +4,8 @@ using Workflow.Application.Features.WorkflowDefinitions.Commands;
namespace Workflow.Api.Endpoints.WorkflowDefinition; namespace Workflow.Api.Endpoints.WorkflowDefinition;
public class PublishWorkflowDefinitionEndpoint : Endpoint<PublishWorkflowDefinitionRequest> public class PublishWorkflowDefinitionEndpoint(IMediator mediator) : EndpointWithoutRequest
{ {
private readonly IMediator _mediator;
public PublishWorkflowDefinitionEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure() public override void Configure()
{ {
Post("/workflow-definitions/{Id}/publish"); Post("/workflow-definitions/{Id}/publish");
@ -20,15 +16,10 @@ public class PublishWorkflowDefinitionEndpoint : Endpoint<PublishWorkflowDefinit
}); });
} }
public override async Task HandleAsync(PublishWorkflowDefinitionRequest req, CancellationToken ct) public override async Task HandleAsync(CancellationToken ct)
{ {
var command = new PublishWorkflowDefinitionCommand(req.Id); var id = Route<Guid>("Id");
await _mediator.Send(command, ct); await mediator.Send(new PublishWorkflowDefinitionCommand(id), ct);
await Send.OkAsync(ct); await Send.OkAsync(ct);
} }
} }
public class PublishWorkflowDefinitionRequest
{
public Guid Id { get; set; }
}

View File

@ -4,12 +4,8 @@ using Workflow.Application.Features.WorkflowInstances.Commands;
namespace Workflow.Api.Endpoints.WorkflowInstance; namespace Workflow.Api.Endpoints.WorkflowInstance;
public class ResumeWorkflowInstanceEndpoint : Endpoint<ResumeWorkflowInstanceRequest> public class ResumeWorkflowInstanceEndpoint(IMediator mediator) : EndpointWithoutRequest
{ {
private readonly IMediator _mediator;
public ResumeWorkflowInstanceEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure() public override void Configure()
{ {
Post("/workflow-instances/{Id}/resume"); Post("/workflow-instances/{Id}/resume");
@ -20,15 +16,10 @@ public class ResumeWorkflowInstanceEndpoint : Endpoint<ResumeWorkflowInstanceReq
}); });
} }
public override async Task HandleAsync(ResumeWorkflowInstanceRequest req, CancellationToken ct) public override async Task HandleAsync(CancellationToken ct)
{ {
var command = new ResumeWorkflowInstanceCommand(req.Id); var id = Route<Guid>("Id");
await _mediator.Send(command, ct); await mediator.Send(new ResumeWorkflowInstanceCommand(id), ct);
await Send.OkAsync(ct); await Send.OkAsync(ct);
} }
} }
public class ResumeWorkflowInstanceRequest
{
public Guid Id { get; set; }
}

View File

@ -4,12 +4,8 @@ using Workflow.Application.Features.WorkflowInstances.Commands;
namespace Workflow.Api.Endpoints.WorkflowInstance; namespace Workflow.Api.Endpoints.WorkflowInstance;
public class SuspendWorkflowInstanceEndpoint : Endpoint<SuspendWorkflowInstanceRequest> public class SuspendWorkflowInstanceEndpoint(IMediator mediator) : EndpointWithoutRequest
{ {
private readonly IMediator _mediator;
public SuspendWorkflowInstanceEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure() public override void Configure()
{ {
Post("/workflow-instances/{Id}/suspend"); Post("/workflow-instances/{Id}/suspend");
@ -20,15 +16,10 @@ public class SuspendWorkflowInstanceEndpoint : Endpoint<SuspendWorkflowInstanceR
}); });
} }
public override async Task HandleAsync(SuspendWorkflowInstanceRequest req, CancellationToken ct) public override async Task HandleAsync(CancellationToken ct)
{ {
var command = new SuspendWorkflowInstanceCommand(req.Id); var id = Route<Guid>("Id");
await _mediator.Send(command, ct); await mediator.Send(new SuspendWorkflowInstanceCommand(id), ct);
await Send.OkAsync(ct); await Send.OkAsync(ct);
} }
} }
public class SuspendWorkflowInstanceRequest
{
public Guid Id { get; set; }
}

View File

@ -1,15 +1,12 @@
using FastEndpoints; using FastEndpoints;
using MediatR; using MediatR;
using Workflow.Application.Features.WorkflowInstances.Commands; using Workflow.Application.Features.WorkflowInstances.Commands;
using Workflow.Domain.Common;
namespace Workflow.Api.Endpoints.WorkflowInstance; namespace Workflow.Api.Endpoints.WorkflowInstance;
public class WithdrawWorkflowInstanceEndpoint : Endpoint<WithdrawWorkflowInstanceRequest> public class WithdrawWorkflowInstanceEndpoint(IMediator mediator, ICurrentUserContext userContext) : EndpointWithoutRequest
{ {
private readonly IMediator _mediator;
public WithdrawWorkflowInstanceEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure() public override void Configure()
{ {
Post("/workflow-instances/{Id}/withdraw"); Post("/workflow-instances/{Id}/withdraw");
@ -20,16 +17,11 @@ public class WithdrawWorkflowInstanceEndpoint : Endpoint<WithdrawWorkflowInstanc
}); });
} }
public override async Task HandleAsync(WithdrawWorkflowInstanceRequest req, CancellationToken ct) public override async Task HandleAsync(CancellationToken ct)
{ {
var command = new WithdrawWorkflowInstanceCommand(req.Id, req.UserId); var id = Route<Guid>("Id");
await _mediator.Send(command, ct); var userId = userContext.GetUserId();
await mediator.Send(new WithdrawWorkflowInstanceCommand(id, userId), ct);
await Send.OkAsync(ct); await Send.OkAsync(ct);
} }
} }
public class WithdrawWorkflowInstanceRequest
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
}

View File

@ -1,5 +1,6 @@
using MediatR; using MediatR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Workflow.Application.Engine;
using Workflow.Domain.Entities; using Workflow.Domain.Entities;
using Workflow.Domain.Enums; using Workflow.Domain.Enums;
using Workflow.Domain.Exceptions; using Workflow.Domain.Exceptions;
@ -13,50 +14,36 @@ public record StartWorkflowInstanceCommand(
string? Variables string? Variables
) : IRequest<Guid>; ) : IRequest<Guid>;
public class StartWorkflowInstanceCommandHandler(WorkflowDbContext db) public class StartWorkflowInstanceCommandHandler(WorkflowDbContext db, ProcessEngine processEngine)
: IRequestHandler<StartWorkflowInstanceCommand, Guid> : IRequestHandler<StartWorkflowInstanceCommand, Guid>
{ {
public async Task<Guid> Handle(StartWorkflowInstanceCommand request, CancellationToken cancellationToken) public async Task<Guid> Handle(StartWorkflowInstanceCommand request, CancellationToken cancellationToken)
{ {
var definition = await db.WorkflowDefinitions var definition = await db.WorkflowDefinitions
.Include(d => d.Nodes)
.FirstOrDefaultAsync(d => d.Code == request.DefinitionCode, cancellationToken) .FirstOrDefaultAsync(d => d.Code == request.DefinitionCode, cancellationToken)
?? throw new NotFoundException($"Workflow definition with code '{request.DefinitionCode}' not found."); ?? throw new NotFoundException($"流程定义 '{request.DefinitionCode}' 不存在");
if (!definition.IsEnabled) if (!definition.IsEnabled)
{ {
throw new BusinessException($"Workflow definition '{request.DefinitionCode}' is disabled."); throw new BusinessException($"流程定义 '{request.DefinitionCode}' 已禁用");
} }
var instanceId = Guid.NewGuid();
var instance = new WorkflowInstance var instance = new WorkflowInstance
{ {
Id = instanceId, Id = Guid.NewGuid(),
DefinitionId = definition.Id, DefinitionId = definition.Id,
Title = request.Title, Title = request.Title,
Status = InstanceStatus.Running, Status = InstanceStatus.Running,
Variables = request.Variables, Variables = request.Variables,
InitiatorId = Guid.Empty // Will be set by audit context in real usage InitiatorId = Guid.Empty
}; };
db.WorkflowInstances.Add(instance); db.WorkflowInstances.Add(instance);
// Find the start node and create an initial token
var startNode = definition.Nodes.FirstOrDefault(n => n.NodeType == NodeType.Start);
if (startNode is not null)
{
var token = new WorkflowToken
{
Id = Guid.NewGuid(),
InstanceId = instanceId,
NodeId = startNode.Id,
Status = TokenStatus.Active
};
db.WorkflowTokens.Add(token);
}
await db.SaveChangesAsync(cancellationToken); await db.SaveChangesAsync(cancellationToken);
return instanceId; // 自动流转Token 从 Start 节点传播到后续节点
await processEngine.StartAsync(instance);
return instance.Id;
} }
} }

View File

@ -1,5 +1,6 @@
using FluentAssertions; using FluentAssertions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Workflow.Application.Engine;
using Workflow.Application.Features.WorkflowInstances.Commands; using Workflow.Application.Features.WorkflowInstances.Commands;
using Workflow.Application.Features.WorkflowInstances.Queries; using Workflow.Application.Features.WorkflowInstances.Queries;
using Workflow.Domain.Entities; using Workflow.Domain.Entities;
@ -64,7 +65,7 @@ public class WorkflowInstanceHandlerTests
{ {
// Arrange // Arrange
await using var db = await SeedPublishedDefinitionAsync("running-test"); await using var db = await SeedPublishedDefinitionAsync("running-test");
var handler = new StartWorkflowInstanceCommandHandler(db); var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
var command = new StartWorkflowInstanceCommand( var command = new StartWorkflowInstanceCommand(
DefinitionCode: "running-test", DefinitionCode: "running-test",
Title: "Test Instance", Title: "Test Instance",
@ -89,7 +90,7 @@ public class WorkflowInstanceHandlerTests
// Arrange // Arrange
var startNodeId = Guid.NewGuid(); var startNodeId = Guid.NewGuid();
await using var db = await SeedPublishedDefinitionAsync("token-test", startNodeId: startNodeId); await using var db = await SeedPublishedDefinitionAsync("token-test", startNodeId: startNodeId);
var handler = new StartWorkflowInstanceCommandHandler(db); var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
var command = new StartWorkflowInstanceCommand( var command = new StartWorkflowInstanceCommand(
DefinitionCode: "token-test", DefinitionCode: "token-test",
Title: "Token Test", Title: "Token Test",
@ -114,7 +115,7 @@ public class WorkflowInstanceHandlerTests
{ {
// Arrange // Arrange
await using var db = CreateDbContext(); await using var db = CreateDbContext();
var handler = new StartWorkflowInstanceCommandHandler(db); var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
var command = new StartWorkflowInstanceCommand( var command = new StartWorkflowInstanceCommand(
DefinitionCode: "nonexistent-code", DefinitionCode: "nonexistent-code",
Title: "Should Fail", Title: "Should Fail",
@ -146,7 +147,7 @@ public class WorkflowInstanceHandlerTests
db.WorkflowDefinitions.Add(definition); db.WorkflowDefinitions.Add(definition);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
var handler = new StartWorkflowInstanceCommandHandler(db); var handler = new StartWorkflowInstanceCommandHandler(db, new ProcessEngine(db, null!, new()));
var command = new StartWorkflowInstanceCommand( var command = new StartWorkflowInstanceCommand(
DefinitionCode: "disabled-wf", DefinitionCode: "disabled-wf",
Title: "Should Fail", Title: "Should Fail",