diff --git a/src/BuildFlow.Host/Endpoints/rag/RAG/RAGStreamEndpoint.cs b/src/BuildFlow.Host/Endpoints/rag/RAG/RAGStreamEndpoint.cs index e610162..26c2fe8 100644 --- a/src/BuildFlow.Host/Endpoints/rag/RAG/RAGStreamEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/rag/RAG/RAGStreamEndpoint.cs @@ -68,7 +68,7 @@ public class RAGStreamEndpoint( var ragMessages = new List { - new(AiChatRole.System, "基于提供的上下文回答问题。如果上下文中没有相关信息,请如实说明。"), + new(AiChatRole.System, "基于提供的上下文直接回答问题。不要输出身份开场、Markdown标题或推理过程;上下文没有依据时,只用一句话说明未检索到相关内容。"), new(AiChatRole.User, $"{contextBuilder}\n\n问题:{req.Question}"), }; diff --git a/src/BuildFlow.Host/Endpoints/workflow/WorkflowTask/ApproveTaskEndpoint.cs b/src/BuildFlow.Host/Endpoints/workflow/WorkflowTask/ApproveTaskEndpoint.cs index 82822cb..68f0538 100644 --- a/src/BuildFlow.Host/Endpoints/workflow/WorkflowTask/ApproveTaskEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/workflow/WorkflowTask/ApproveTaskEndpoint.cs @@ -7,7 +7,7 @@ namespace BuildFlow.Host.Endpoints.Workflow.WorkflowTask; /// /// 审批通过任务端点。驱动 TaskStateMachine 转 Approved,按 Approved 边推进流程。 /// -public class ApproveTaskEndpoint : Endpoint +public class ApproveTaskEndpoint : Endpoint { private readonly IMediator _mediator; @@ -27,8 +27,6 @@ public class ApproveTaskEndpoint : Endpoint { 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); } } diff --git a/src/BuildFlow.Host/Endpoints/workflow/WorkflowTask/RejectTaskEndpoint.cs b/src/BuildFlow.Host/Endpoints/workflow/WorkflowTask/RejectTaskEndpoint.cs index e6d1cea..6f016df 100644 --- a/src/BuildFlow.Host/Endpoints/workflow/WorkflowTask/RejectTaskEndpoint.cs +++ b/src/BuildFlow.Host/Endpoints/workflow/WorkflowTask/RejectTaskEndpoint.cs @@ -7,7 +7,7 @@ namespace BuildFlow.Host.Endpoints.Workflow.WorkflowTask; /// /// 驳回任务端点。转 Rejected 终态,按 Rejected 边流转(若无则结束实例)。 /// -public class RejectTaskEndpoint : Endpoint +public class RejectTaskEndpoint : Endpoint { private readonly IMediator _mediator; @@ -27,8 +27,6 @@ public class RejectTaskEndpoint : Endpoint { 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); } } diff --git a/src/modules/workflow/Application/BuildFlow/Buttons/Services/BuildFlowButtonActionExecutor.cs b/src/modules/workflow/Application/BuildFlow/Buttons/Services/BuildFlowButtonActionExecutor.cs index 86da109..d8a9ea9 100644 --- a/src/modules/workflow/Application/BuildFlow/Buttons/Services/BuildFlowButtonActionExecutor.cs +++ b/src/modules/workflow/Application/BuildFlow/Buttons/Services/BuildFlowButtonActionExecutor.cs @@ -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(); + 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(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) diff --git a/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowButtonActionTests.cs b/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowButtonActionTests.cs index 14aae24..d3a7f9e 100644 --- a/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowButtonActionTests.cs +++ b/tests/BuildFlow.Workflow.Tests/BuildFlow/BuildFlowButtonActionTests.cs @@ -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;