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:
parent
368dc0abf0
commit
3d45aed822
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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>()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user