feat(workflow): 添加表单提交功能并优化任务审批端点

- 在 ApproveTaskEndpoint 中返回布尔值响应并使用 Send.OkAsync
- 在 RejectTaskEndpoint 中返回布尔值响应并使用 Send.OkAsync
- 向 BuildFlowButtonActionExecutor 添加 submitForm 动作类型支持
- 实现 SubmitFormAsync 方法处理表单数据提交和保存逻辑
- 添加 ReadGuid 辅助方法解析 JSON 中的 GUID 值
- 更新 RAGStreamEndpoint 中的系统提示词以优化 AI 回答格式
- 为按钮动作执行器添加表单聚合服务和派生服务依赖注入
- 添加单元测试验证提交表单后运行函数的动作链执行流程
This commit is contained in:
xiangning 2026-07-04 21:38:31 +08:00
parent 54dc3fc26e
commit 96f11b5d87
5 changed files with 141 additions and 10 deletions

View File

@ -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}"),
};

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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;