feat(workflow): 添加表单提交功能并优化任务审批端点
- 在 ApproveTaskEndpoint 中返回布尔值响应并使用 Send.OkAsync - 在 RejectTaskEndpoint 中返回布尔值响应并使用 Send.OkAsync - 向 BuildFlowButtonActionExecutor 添加 submitForm 动作类型支持 - 实现 SubmitFormAsync 方法处理表单数据提交和保存逻辑 - 添加 ReadGuid 辅助方法解析 JSON 中的 GUID 值 - 更新 RAGStreamEndpoint 中的系统提示词以优化 AI 回答格式 - 为按钮动作执行器添加表单聚合服务和派生服务依赖注入 - 添加单元测试验证提交表单后运行函数的动作链执行流程
This commit is contained in:
parent
54dc3fc26e
commit
96f11b5d87
@ -68,7 +68,7 @@ public class RAGStreamEndpoint(
|
||||
|
||||
var ragMessages = new List<AiChatMessage>
|
||||
{
|
||||
new(AiChatRole.System, "基于提供的上下文回答问题。如果上下文中没有相关信息,请如实说明。"),
|
||||
new(AiChatRole.System, "基于提供的上下文直接回答问题。不要输出身份开场、Markdown标题或推理过程;上下文没有依据时,只用一句话说明未检索到相关内容。"),
|
||||
new(AiChatRole.User, $"{contextBuilder}\n\n问题:{req.Question}"),
|
||||
};
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ namespace BuildFlow.Host.Endpoints.Workflow.WorkflowTask;
|
||||
/// <summary>
|
||||
/// 审批通过任务端点。驱动 TaskStateMachine 转 Approved,按 Approved 边推进流程。
|
||||
/// </summary>
|
||||
public class ApproveTaskEndpoint : Endpoint<ApproveTaskRequest>
|
||||
public class ApproveTaskEndpoint : Endpoint<ApproveTaskRequest, bool>
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@ -27,8 +27,6 @@ public class ApproveTaskEndpoint : Endpoint<ApproveTaskRequest>
|
||||
{
|
||||
var command = new ApproveTaskCommand(req.Id, req.UserId, req.Comment, req.FormDataJson);
|
||||
await _mediator.Send(command, ct);
|
||||
HttpContext.Response.StatusCode = 200;
|
||||
HttpContext.Response.ContentType = "application/json";
|
||||
await HttpContext.Response.WriteAsync("{}", ct);
|
||||
await Send.OkAsync(true, ct);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ namespace BuildFlow.Host.Endpoints.Workflow.WorkflowTask;
|
||||
/// <summary>
|
||||
/// 驳回任务端点。转 Rejected 终态,按 Rejected 边流转(若无则结束实例)。
|
||||
/// </summary>
|
||||
public class RejectTaskEndpoint : Endpoint<RejectTaskRequest>
|
||||
public class RejectTaskEndpoint : Endpoint<RejectTaskRequest, bool>
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@ -27,8 +27,6 @@ public class RejectTaskEndpoint : Endpoint<RejectTaskRequest>
|
||||
{
|
||||
var command = new RejectTaskCommand(req.Id, req.UserId, req.Comment);
|
||||
await _mediator.Send(command, ct);
|
||||
HttpContext.Response.StatusCode = 200;
|
||||
HttpContext.Response.ContentType = "application/json";
|
||||
await HttpContext.Response.WriteAsync("{}", ct);
|
||||
await Send.OkAsync(true, ct);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,10 @@ using MediatR;
|
||||
using Workflow.Application.BuildFlow.Buttons.DTOs;
|
||||
using Workflow.Application.BuildFlow.CustomFunctions.DTOs;
|
||||
using Workflow.Application.BuildFlow.CustomFunctions.Services;
|
||||
using Workflow.Application.Form.Aggregates;
|
||||
using Workflow.Application.Form.Derivation;
|
||||
using Workflow.Application.Form.FormData.Commands;
|
||||
using Workflow.Application.Form.FormData.Handlers;
|
||||
using Workflow.Domain.Entities;
|
||||
using Workflow.Domain.Enums;
|
||||
using Workflow.Infrastructure.Persistence;
|
||||
@ -21,7 +25,9 @@ public class BuildFlowButtonActionExecutor(
|
||||
WorkflowDbContext db,
|
||||
ICustomFunctionExecutionService functionExecutionService,
|
||||
ISender? sender = null,
|
||||
ICurrentUserContext? currentUserContext = null) : IBuildFlowButtonActionExecutor
|
||||
ICurrentUserContext? currentUserContext = null,
|
||||
IFormValueDerivationService? derivationService = null,
|
||||
IFormAggregateRefreshService? aggregateRefreshService = null) : IBuildFlowButtonActionExecutor
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
@ -51,6 +57,12 @@ public class BuildFlowButtonActionExecutor(
|
||||
foreach (var action in actions)
|
||||
{
|
||||
var type = action["type"]?.GetValue<string>();
|
||||
if (type == "submitForm")
|
||||
{
|
||||
await SubmitFormAsync(action, input, ct);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type != "runFunction")
|
||||
{
|
||||
throw new BusinessException($"按钮动作类型 {type} 暂不支持");
|
||||
@ -83,6 +95,32 @@ public class BuildFlowButtonActionExecutor(
|
||||
return new BuildFlowButtonActionResult(true, results);
|
||||
}
|
||||
|
||||
private async Task SubmitFormAsync(JsonObject action, JsonObject input, CancellationToken ct)
|
||||
{
|
||||
var formDefinitionId = ReadGuid(action, "formDefinitionId") ?? ReadGuid(input, "formDefinitionId")
|
||||
?? throw new BusinessException("submitForm 动作缺少有效表单定义ID");
|
||||
var instanceId = ReadGuid(input, "instanceId") ?? ReadGuid(action, "instanceId") ?? Guid.NewGuid();
|
||||
if (input["values"] is not JsonObject values)
|
||||
{
|
||||
throw new BusinessException("submitForm 动作输入缺少 values 表单数据对象");
|
||||
}
|
||||
|
||||
var submitHandler = new SubmitFormDataCommandHandler(
|
||||
db,
|
||||
derivationService ?? new FormValueDerivationService([], [], db),
|
||||
aggregateRefreshService ?? new FormAggregateRefreshService(db));
|
||||
var formDataId = await submitHandler.Handle(
|
||||
new SubmitFormDataCommand(formDefinitionId, instanceId, values.ToJsonString(JsonOptions)),
|
||||
ct);
|
||||
var saved = await db.FormData.FindAsync([formDataId], ct)
|
||||
?? throw new BusinessException($"表单数据 {formDataId} 保存失败");
|
||||
|
||||
input["formDefinitionId"] = formDefinitionId;
|
||||
input["instanceId"] = instanceId;
|
||||
input["formDataId"] = formDataId;
|
||||
input["values"] = JsonNode.Parse(saved.DataJson)?.DeepClone();
|
||||
}
|
||||
|
||||
private async Task EnsurePermissionAsync(BuildFlowButtonDefinition button, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(button.PermissionCode))
|
||||
@ -125,6 +163,16 @@ public class BuildFlowButtonActionExecutor(
|
||||
?? throw new BusinessException("按钮输入 JSON 必须是对象");
|
||||
}
|
||||
|
||||
private static Guid? ReadGuid(JsonObject source, string propertyName)
|
||||
{
|
||||
var value = source[propertyName];
|
||||
return value is JsonValue jsonValue &&
|
||||
jsonValue.TryGetValue<string>(out var text) &&
|
||||
Guid.TryParse(text, out var id)
|
||||
? id
|
||||
: null;
|
||||
}
|
||||
|
||||
private static string BuildFunctionInputJson(JsonObject? parameters, JsonObject input)
|
||||
{
|
||||
if (parameters is null || parameters.Count == 0)
|
||||
|
||||
@ -111,6 +111,40 @@ public class BuildFlowButtonActionTests
|
||||
(await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_SubmitFormThenRunFunction_PersistsFormDataAndContinuesActionChain()
|
||||
{
|
||||
await using var db = TestDbFactory.Create();
|
||||
var formDefinition = CreatePublishedFormDefinition(Guid.NewGuid());
|
||||
db.FormDefinitions.Add(formDefinition);
|
||||
var function = await CreatePublishedFunctionAsync(db);
|
||||
var button = CreateSubmitAndRunButton(function.Id);
|
||||
db.BuildFlowButtons.Add(button);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var executor = new BuildFlowButtonActionExecutor(
|
||||
db,
|
||||
new CustomFunctionExecutionService(db));
|
||||
|
||||
var result = await executor.ExecuteAsync(
|
||||
button.Id,
|
||||
$$"""
|
||||
{
|
||||
"formDefinitionId": "{{formDefinition.Id}}",
|
||||
"values": { "name": "张三", "amount": 1200 }
|
||||
}
|
||||
""",
|
||||
CancellationToken.None);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.Results.Should().ContainSingle().Which.ResultJson.Should().Be("\"high\"");
|
||||
|
||||
var saved = await db.FormData.SingleAsync(f => f.FormDefinitionId == formDefinition.Id);
|
||||
saved.InstanceId.Should().NotBe(Guid.Empty);
|
||||
saved.DataJson.Should().Be("""{"name":"张三","amount":1200}""");
|
||||
(await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_WhenButtonRequiresPermission_RejectsExecutionWithoutPermissionContext()
|
||||
{
|
||||
@ -199,6 +233,36 @@ public class BuildFlowButtonActionTests
|
||||
return await db.CustomFunctions.FindAsync(function.Id) ?? throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
private static FormDefinition CreatePublishedFormDefinition(Guid formDefinitionId)
|
||||
=> new()
|
||||
{
|
||||
Id = formDefinitionId,
|
||||
Name = "按钮提交表单",
|
||||
Code = $"button-submit-{formDefinitionId:N}",
|
||||
Version = 1,
|
||||
Status = FormStatus.Published,
|
||||
SchemaJson = """
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"title": "姓名",
|
||||
"required": true,
|
||||
"x-decorator": "FormItem",
|
||||
"x-component": "Input"
|
||||
},
|
||||
"amount": {
|
||||
"type": "number",
|
||||
"title": "金额",
|
||||
"x-decorator": "FormItem",
|
||||
"x-component": "InputNumber"
|
||||
}
|
||||
}
|
||||
}
|
||||
""",
|
||||
};
|
||||
|
||||
private static BuildFlowButtonDefinition CreateButton(Guid functionId)
|
||||
=> new()
|
||||
{
|
||||
@ -219,6 +283,29 @@ public class BuildFlowButtonActionTests
|
||||
""",
|
||||
};
|
||||
|
||||
private static BuildFlowButtonDefinition CreateSubmitAndRunButton(Guid functionId)
|
||||
=> new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = "submit-form-and-run",
|
||||
Label = "提交表单并计算",
|
||||
Placement = BuildFlowButtonPlacement.FormToolbar,
|
||||
Status = BuildFlowMetadataStatus.Published,
|
||||
ActionsJson = $$"""
|
||||
[
|
||||
{
|
||||
"type": "submitForm"
|
||||
},
|
||||
{
|
||||
"type": "runFunction",
|
||||
"target": "{{functionId}}",
|
||||
"params": { "amount": "$input.values.amount" },
|
||||
"runPolicy": "stopOnFailure"
|
||||
}
|
||||
]
|
||||
""",
|
||||
};
|
||||
|
||||
private sealed class TestCurrentUserContext(string userId) : ICurrentUserContext
|
||||
{
|
||||
public string UserId { get; } = userId;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user