fix(workflow): 修复 WorkflowInstance.InitiatorId 系统性空值

全系统 InitiatorId 始终为 Guid.Empty,导致撤回鉴权与发起人通知失效。
自下而上修复:
- StartWorkflowInstanceCommand 加 InitiatorId 参数,Handler 写入实例
- StartWorkflowByNameCommandHandler 透传发起人(原为死参数)
- StartWorkflowInstanceEndpoint 取当前认证用户作为发起人
- IAiBotHandler.AnswerAsync 加 initiatorId 参数,AI 触发流程用真实发起人
- SendMessageCommandHandler 传 request.SenderId 给 AI 问答
- 补充测试断言验证 InitiatorId 正确赋值
This commit is contained in:
xiangning 2026-07-09 12:52:13 +08:00
parent 368dc0abf0
commit 3d45aed822
8 changed files with 59 additions and 41 deletions

View File

@ -1,18 +1,18 @@
using MediatR;
using Workflow.Application.Features.WorkflowInstances.Commands;
using BuildFlow.Shared.Domain.Common;
using BuildFlow.Host.Services.Users;
namespace BuildFlow.Host.Endpoints.Workflow.WorkflowInstance;
/// <summary>
/// 发起流程实例端点。POST /instances,按定义创建实例并产生首个 token。
/// 发起流程实例端点。POST /workflow-instances,按定义创建实例并产生首个 token。
/// </summary>
public class StartWorkflowInstanceEndpoint : Endpoint<StartWorkflowInstanceRequest, Guid>
public class StartWorkflowInstanceEndpoint(
IMediator mediator,
ICurrentUserContext currentUserContext) : Endpoint<StartWorkflowInstanceRequest, Guid>
{
private readonly IMediator _mediator;
public StartWorkflowInstanceEndpoint(IMediator mediator) => _mediator = mediator;
public override void Configure()
{
Post("/workflow-instances");
@ -25,8 +25,14 @@ public class StartWorkflowInstanceEndpoint : Endpoint<StartWorkflowInstanceReque
public override async Task HandleAsync(StartWorkflowInstanceRequest req, CancellationToken ct)
{
var command = new StartWorkflowInstanceCommand(req.DefinitionCode, req.Title, req.Variables, req.FormDataJson);
var result = await _mediator.Send(command, ct);
// 发起人取自当前认证用户,写入 WorkflowInstance.InitiatorId,供撤回鉴权与发起人通知使用。
var command = new StartWorkflowInstanceCommand(
req.DefinitionCode,
req.Title,
req.Variables,
req.FormDataJson,
currentUserContext.GetUserIdGuid());
var result = await mediator.Send(command, ct);
await Send.OkAsync(result, ct);
}
}

View File

@ -169,8 +169,9 @@ public class SendMessageCommandHandler(
try
{
// 异步调 RAG 问答(不阻塞消息发送响应)
var answer = await aiBotHandler.AnswerAsync(question, request.ConversationId, ct);
// 异步调 RAG 问答(不阻塞消息发送响应)。传 request.SenderId 作为发起人,
// 供 AI 触发工作流时写入 WorkflowInstance.InitiatorId。
var answer = await aiBotHandler.AnswerAsync(question, request.ConversationId, request.SenderId, ct);
// AI 回复作为新消息发回会话(System 类型,标注来自 AI 助手)
var aiMessage = new MessageDocument

View File

@ -16,11 +16,11 @@ public class AiBotHandler(
IMediator mediator,
RagDbContext db) : IAiBotHandler
{
public async Task<string> AnswerAsync(string question, Guid conversationId, CancellationToken ct)
public async Task<string> AnswerAsync(string question, Guid conversationId, Guid initiatorId, CancellationToken ct)
{
// 串联链路4:AI 对话 → 触发工作流
// 检测"发起/启动/申请 XXX 流程"等意图
var triggerResult = await TryDetectWorkflowIntentAsync(question, ct);
var triggerResult = await TryDetectWorkflowIntentAsync(question, initiatorId, ct);
if (triggerResult is not null)
return triggerResult;
@ -49,7 +49,7 @@ public class AiBotHandler(
/// 检测工作流触发意图(如"帮我发起报销流程")。
/// 命中则触发对应流程并返回结果消息,未命中返回 null 走默认 RAG 问答。
/// </summary>
private async Task<string?> TryDetectWorkflowIntentAsync(string question, CancellationToken ct)
private async Task<string?> TryDetectWorkflowIntentAsync(string question, Guid initiatorId, CancellationToken ct)
{
var lowerQ = question.ToLowerInvariant();
@ -69,7 +69,7 @@ public class AiBotHandler(
if (string.IsNullOrWhiteSpace(flowName)) return null;
var result = await mediator.Send(new StartWorkflowByNameCommand(flowName, Guid.Empty, null), ct);
var result = await mediator.Send(new StartWorkflowByNameCommand(flowName, initiatorId, null), ct);
if (result.Success)
{

View File

@ -6,11 +6,13 @@ namespace Workflow.Application.Features.WorkflowInstances.Commands;
/// <summary>
/// 发起WorkflowInstance命令。封装写操作的入参与意图。
/// </summary>
/// <param name="InitiatorId">发起人用户 ID。写入 WorkflowInstance.InitiatorId,供撤回鉴权与发起人通知使用。</param>
public record StartWorkflowInstanceCommand(
string DefinitionCode,
string Title,
string? Variables,
string? FormDataJson
string? FormDataJson,
Guid InitiatorId
) : IRequest<Guid>;
/// <summary>

View File

@ -83,7 +83,7 @@ public class StartWorkflowInstanceCommandHandler(
Title = request.Title,
Status = InstanceStatus.Running,
Variables = BuildVariablesJson(normalizedFormDataJson, request.Variables),
InitiatorId = Guid.Empty
InitiatorId = request.InitiatorId
};
db.WorkflowInstances.Add(instance);

View File

@ -40,7 +40,8 @@ public sealed class StartWorkflowByNameCommandHandler(
definition.Code ?? definition.Id.ToString(),
$"{request.DefinitionKey}-{DateTime.UtcNow:MMddHHmm}",
request.Variables,
null),
null,
request.InitiatorId),
ct);
return new WorkflowTriggerResultDto(true, instanceId, request.DefinitionKey, null);

View File

@ -22,7 +22,8 @@ public interface IAiBotHandler
/// </summary>
/// <param name="question">用户提问内容(去掉 @AI 前缀后的纯问题)</param>
/// <param name="conversationId">IM 会话 ID(可用于关联知识库)</param>
/// <param name="initiatorId">提问发起人用户 ID(触发工作流时作为实例发起人)</param>
/// <param name="ct">取消令牌</param>
/// <returns>AI 回答文本</returns>
Task<string> AnswerAsync(string question, Guid conversationId, CancellationToken ct);
Task<string> AnswerAsync(string question, Guid conversationId, Guid initiatorId, CancellationToken ct);
}

View File

@ -17,6 +17,8 @@ namespace BuildFlow.Workflow.Tests.Handlers;
public class WorkflowInstanceHandlerTests
{
private static readonly Guid TestInitiatorId = Guid.Parse("11111111-1111-1111-1111-111111111111");
private const string ComputedExpenseSchema = """
{
"type": "object",
@ -123,8 +125,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "running-test",
Title: "Test Instance",
Variables: null,
FormDataJson: null
);
FormDataJson: null,
InitiatorId: TestInitiatorId);
// Act
var instanceId = await handler.Handle(command, CancellationToken.None);
@ -136,6 +138,8 @@ public class WorkflowInstanceHandlerTests
instance.Should().NotBeNull();
instance!.Status.Should().Be(InstanceStatus.Running);
instance.Title.Should().Be("Test Instance");
// 发起人必须正确写入,供撤回鉴权与发起人通知使用(修复历史 Guid.Empty 缺陷)
instance.InitiatorId.Should().Be(TestInitiatorId);
}
[Fact]
@ -149,8 +153,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "token-test",
Title: "Token Test",
Variables: null,
FormDataJson: null
);
FormDataJson: null,
InitiatorId: TestInitiatorId);
// Act
var instanceId = await handler.Handle(command, CancellationToken.None);
@ -175,8 +179,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "nonexistent-code",
Title: "Should Fail",
Variables: null,
FormDataJson: null
);
FormDataJson: null,
InitiatorId: TestInitiatorId);
// Act
var act = () => handler.Handle(command, CancellationToken.None);
@ -208,8 +212,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "disabled-wf",
Title: "Should Fail",
Variables: null,
FormDataJson: null
);
FormDataJson: null,
InitiatorId: TestInitiatorId);
// Act
var act = () => handler.Handle(command, CancellationToken.None);
@ -243,8 +247,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "form-vars-test",
Title: "Expense",
Variables: null,
FormDataJson: """{"amount":6500,"category":"travel"}"""
);
FormDataJson: """{"amount":6500,"category":"travel"}""",
InitiatorId: TestInitiatorId);
// Act
var instanceId = await handler.Handle(command, CancellationToken.None);
@ -286,7 +290,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "computed-start-form-test",
Title: "Computed Start",
Variables: null,
FormDataJson: """{"quantity":2,"unitPrice":15,"total":1}"""),
FormDataJson: """{"quantity":2,"unitPrice":15,"total":1}""",
InitiatorId: TestInitiatorId),
CancellationToken.None);
var savedData = await db.FormData.SingleAsync(f => f.InstanceId == instanceId);
@ -326,8 +331,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "deleted-start-form-test",
Title: "X",
Variables: null,
FormDataJson: """{"amount":100}"""
);
FormDataJson: """{"amount":100}""",
InitiatorId: TestInitiatorId);
// Act
var act = () => handler.Handle(command, CancellationToken.None);
@ -360,8 +365,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "nonexistent-start-form-test",
Title: "X",
Variables: null,
FormDataJson: """{"amount":100}"""
);
FormDataJson: """{"amount":100}""",
InitiatorId: TestInitiatorId);
var act = () => handler.Handle(command, CancellationToken.None);
@ -396,8 +401,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "disabled-start-form-test",
Title: "X",
Variables: null,
FormDataJson: """{"amount":100}"""
);
FormDataJson: """{"amount":100}""",
InitiatorId: TestInitiatorId);
var act = () => handler.Handle(command, CancellationToken.None);
@ -433,8 +438,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "merge-vars-test",
Title: "Leave",
Variables: """{"days":5,"urgent":true}""",
FormDataJson: """{"days":2,"leaveType":"annual"}"""
);
FormDataJson: """{"days":2,"leaveType":"annual"}""",
InitiatorId: TestInitiatorId);
// Act
var instanceId = await handler.Handle(command, CancellationToken.None);
@ -484,8 +489,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "invalid-start-form-test",
Title: "Invalid Start Form",
Variables: null,
FormDataJson: """{"name":""}"""
);
FormDataJson: """{"name":""}""",
InitiatorId: TestInitiatorId);
// Act
var act = () => handler.Handle(command, CancellationToken.None);
@ -559,7 +564,8 @@ public class WorkflowInstanceHandlerTests
"nested-start-form-test",
"嵌套合法发起",
null,
"""{"leaveType":"annual","dateRange":{"startDate":"2026-06-15","endDate":"2026-06-16"}}"""),
"""{"leaveType":"annual","dateRange":{"startDate":"2026-06-15","endDate":"2026-06-16"}}""",
TestInitiatorId),
CancellationToken.None);
instanceId.Should().NotBeEmpty();
@ -571,7 +577,8 @@ public class WorkflowInstanceHandlerTests
"nested-start-form-test",
"嵌套缺失",
null,
"""{"leaveType":"annual","dateRange":{}}"""),
"""{"leaveType":"annual","dateRange":{}}""",
TestInitiatorId),
CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>()