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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,8 @@ public interface IAiBotHandler
/// </summary> /// </summary>
/// <param name="question">用户提问内容(去掉 @AI 前缀后的纯问题)</param> /// <param name="question">用户提问内容(去掉 @AI 前缀后的纯问题)</param>
/// <param name="conversationId">IM 会话 ID(可用于关联知识库)</param> /// <param name="conversationId">IM 会话 ID(可用于关联知识库)</param>
/// <param name="initiatorId">提问发起人用户 ID(触发工作流时作为实例发起人)</param>
/// <param name="ct">取消令牌</param> /// <param name="ct">取消令牌</param>
/// <returns>AI 回答文本</returns> /// <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 public class WorkflowInstanceHandlerTests
{ {
private static readonly Guid TestInitiatorId = Guid.Parse("11111111-1111-1111-1111-111111111111");
private const string ComputedExpenseSchema = """ private const string ComputedExpenseSchema = """
{ {
"type": "object", "type": "object",
@ -123,8 +125,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "running-test", DefinitionCode: "running-test",
Title: "Test Instance", Title: "Test Instance",
Variables: null, Variables: null,
FormDataJson: null FormDataJson: null,
); InitiatorId: TestInitiatorId);
// Act // Act
var instanceId = await handler.Handle(command, CancellationToken.None); var instanceId = await handler.Handle(command, CancellationToken.None);
@ -136,6 +138,8 @@ public class WorkflowInstanceHandlerTests
instance.Should().NotBeNull(); instance.Should().NotBeNull();
instance!.Status.Should().Be(InstanceStatus.Running); instance!.Status.Should().Be(InstanceStatus.Running);
instance.Title.Should().Be("Test Instance"); instance.Title.Should().Be("Test Instance");
// 发起人必须正确写入,供撤回鉴权与发起人通知使用(修复历史 Guid.Empty 缺陷)
instance.InitiatorId.Should().Be(TestInitiatorId);
} }
[Fact] [Fact]
@ -149,8 +153,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "token-test", DefinitionCode: "token-test",
Title: "Token Test", Title: "Token Test",
Variables: null, Variables: null,
FormDataJson: null FormDataJson: null,
); InitiatorId: TestInitiatorId);
// Act // Act
var instanceId = await handler.Handle(command, CancellationToken.None); var instanceId = await handler.Handle(command, CancellationToken.None);
@ -175,8 +179,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "nonexistent-code", DefinitionCode: "nonexistent-code",
Title: "Should Fail", Title: "Should Fail",
Variables: null, Variables: null,
FormDataJson: null FormDataJson: null,
); InitiatorId: TestInitiatorId);
// Act // Act
var act = () => handler.Handle(command, CancellationToken.None); var act = () => handler.Handle(command, CancellationToken.None);
@ -208,8 +212,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "disabled-wf", DefinitionCode: "disabled-wf",
Title: "Should Fail", Title: "Should Fail",
Variables: null, Variables: null,
FormDataJson: null FormDataJson: null,
); InitiatorId: TestInitiatorId);
// Act // Act
var act = () => handler.Handle(command, CancellationToken.None); var act = () => handler.Handle(command, CancellationToken.None);
@ -243,8 +247,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "form-vars-test", DefinitionCode: "form-vars-test",
Title: "Expense", Title: "Expense",
Variables: null, Variables: null,
FormDataJson: """{"amount":6500,"category":"travel"}""" FormDataJson: """{"amount":6500,"category":"travel"}""",
); InitiatorId: TestInitiatorId);
// Act // Act
var instanceId = await handler.Handle(command, CancellationToken.None); var instanceId = await handler.Handle(command, CancellationToken.None);
@ -286,7 +290,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "computed-start-form-test", DefinitionCode: "computed-start-form-test",
Title: "Computed Start", Title: "Computed Start",
Variables: null, Variables: null,
FormDataJson: """{"quantity":2,"unitPrice":15,"total":1}"""), FormDataJson: """{"quantity":2,"unitPrice":15,"total":1}""",
InitiatorId: TestInitiatorId),
CancellationToken.None); CancellationToken.None);
var savedData = await db.FormData.SingleAsync(f => f.InstanceId == instanceId); var savedData = await db.FormData.SingleAsync(f => f.InstanceId == instanceId);
@ -326,8 +331,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "deleted-start-form-test", DefinitionCode: "deleted-start-form-test",
Title: "X", Title: "X",
Variables: null, Variables: null,
FormDataJson: """{"amount":100}""" FormDataJson: """{"amount":100}""",
); InitiatorId: TestInitiatorId);
// Act // Act
var act = () => handler.Handle(command, CancellationToken.None); var act = () => handler.Handle(command, CancellationToken.None);
@ -360,8 +365,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "nonexistent-start-form-test", DefinitionCode: "nonexistent-start-form-test",
Title: "X", Title: "X",
Variables: null, Variables: null,
FormDataJson: """{"amount":100}""" FormDataJson: """{"amount":100}""",
); InitiatorId: TestInitiatorId);
var act = () => handler.Handle(command, CancellationToken.None); var act = () => handler.Handle(command, CancellationToken.None);
@ -396,8 +401,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "disabled-start-form-test", DefinitionCode: "disabled-start-form-test",
Title: "X", Title: "X",
Variables: null, Variables: null,
FormDataJson: """{"amount":100}""" FormDataJson: """{"amount":100}""",
); InitiatorId: TestInitiatorId);
var act = () => handler.Handle(command, CancellationToken.None); var act = () => handler.Handle(command, CancellationToken.None);
@ -433,8 +438,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "merge-vars-test", DefinitionCode: "merge-vars-test",
Title: "Leave", Title: "Leave",
Variables: """{"days":5,"urgent":true}""", Variables: """{"days":5,"urgent":true}""",
FormDataJson: """{"days":2,"leaveType":"annual"}""" FormDataJson: """{"days":2,"leaveType":"annual"}""",
); InitiatorId: TestInitiatorId);
// Act // Act
var instanceId = await handler.Handle(command, CancellationToken.None); var instanceId = await handler.Handle(command, CancellationToken.None);
@ -484,8 +489,8 @@ public class WorkflowInstanceHandlerTests
DefinitionCode: "invalid-start-form-test", DefinitionCode: "invalid-start-form-test",
Title: "Invalid Start Form", Title: "Invalid Start Form",
Variables: null, Variables: null,
FormDataJson: """{"name":""}""" FormDataJson: """{"name":""}""",
); InitiatorId: TestInitiatorId);
// Act // Act
var act = () => handler.Handle(command, CancellationToken.None); var act = () => handler.Handle(command, CancellationToken.None);
@ -559,7 +564,8 @@ public class WorkflowInstanceHandlerTests
"nested-start-form-test", "nested-start-form-test",
"嵌套合法发起", "嵌套合法发起",
null, 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); CancellationToken.None);
instanceId.Should().NotBeEmpty(); instanceId.Should().NotBeEmpty();
@ -571,7 +577,8 @@ public class WorkflowInstanceHandlerTests
"nested-start-form-test", "nested-start-form-test",
"嵌套缺失", "嵌套缺失",
null, null,
"""{"leaveType":"annual","dateRange":{}}"""), """{"leaveType":"annual","dateRange":{}}""",
TestInitiatorId),
CancellationToken.None); CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>() await act.Should().ThrowAsync<BusinessException>()