refactor(scripting): 按钮动作链改造指向 IScriptExecutionService

- WorkflowApplicationModule 依赖 ScriptingApplicationModule
- BuildFlowButtonActionExecutor 改用 IScriptExecutionService.ExecuteByIdAsync
- EnsureFunctionScopeAsync 改查 ScriptingDbContext.Scripts
- 结果列表类型 CustomFunctionExecutionResult → ScriptExecutionResult
  (连带更新 BuildFlowButtonActionResult DTO 的 Results 元素类型)
- BuildFlowButtonActionTests 旧 CustomFunction 链路用例暂跳过,留待 T9 重写
- 旧 CustomFunction 代码保留,T7 统一删除

Task T6 of scripting module refactor
This commit is contained in:
xiangning 2026-07-07 09:42:20 +08:00
parent 3d5604d80c
commit 51fe387913
5 changed files with 46 additions and 180 deletions

View File

@ -1,8 +1,8 @@
using Workflow.Application.BuildFlow.CustomFunctions.DTOs;
using Scripting.Application.DTOs;
namespace Workflow.Application.BuildFlow.Buttons.DTOs;
/// <summary>BuildFlow 按钮动作链执行结果,包含每个动作产生的返回值。</summary>
public sealed record BuildFlowButtonActionResult(
bool Success,
IReadOnlyList<CustomFunctionExecutionResult> Results);
IReadOnlyList<ScriptExecutionResult> Results);

View File

@ -5,9 +5,10 @@ using System.Text.Json.Nodes;
using BuildFlow.Platform.Application.Auth.Queries;
using BuildFlow.Shared.Domain.Common;
using MediatR;
using Scripting.Application.DTOs;
using Scripting.Application.Services;
using Scripting.Infrastructure.Persistence;
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;
@ -23,7 +24,8 @@ namespace Workflow.Application.BuildFlow.Buttons.Services;
/// </summary>
public class BuildFlowButtonActionExecutor(
WorkflowDbContext db,
ICustomFunctionExecutionService functionExecutionService,
ScriptingDbContext scriptingDb,
IScriptExecutionService scriptExecutionService,
ISender? sender = null,
ICurrentUserContext? currentUserContext = null,
IFormValueDerivationService? derivationService = null,
@ -50,7 +52,7 @@ public class BuildFlowButtonActionExecutor(
await EnsurePermissionAsync(button, ct);
var stopwatch = Stopwatch.StartNew();
var results = new List<CustomFunctionExecutionResult>();
var results = new List<ScriptExecutionResult>();
var input = ParseInput(inputJson);
// 三阶段执行:before(执行期/前置钩子)→ main(执行中/主链)→ after(执行后/后置钩子)
@ -83,7 +85,7 @@ public class BuildFlowButtonActionExecutor(
BuildFlowButtonDefinition button,
string? actionsJson,
JsonObject input,
List<CustomFunctionExecutionResult> results,
List<ScriptExecutionResult> results,
string phaseLabel,
CancellationToken ct)
{
@ -103,7 +105,7 @@ public class BuildFlowButtonActionExecutor(
BuildFlowButtonDefinition button,
JsonObject action,
JsonObject input,
List<CustomFunctionExecutionResult> results,
List<ScriptExecutionResult> results,
string phaseLabel,
CancellationToken ct)
{
@ -120,7 +122,7 @@ public class BuildFlowButtonActionExecutor(
var functionId = ResolveFunctionId(action, "runFunction");
await EnsureFunctionScopeAsync(button, functionId, ct);
var functionInput = BuildFunctionInputJson(action["params"] as JsonObject, input);
var result = await functionExecutionService.ExecuteAsync(functionId, functionInput, ct);
var result = await scriptExecutionService.ExecuteByIdAsync(functionId, functionInput, ct);
results.Add(result);
return;
}
@ -142,7 +144,7 @@ public class BuildFlowButtonActionExecutor(
}
await EnsureFunctionScopeAsync(button, functionId, ct);
var functionInput = BuildFunctionInputJson(action["params"] as JsonObject, input);
var result = await functionExecutionService.ExecuteAsync(functionId, functionInput, ct);
var result = await scriptExecutionService.ExecuteByIdAsync(functionId, functionInput, ct);
results.Add(result);
}
return;
@ -163,26 +165,26 @@ public class BuildFlowButtonActionExecutor(
private async Task EnsureFunctionScopeAsync(
BuildFlowButtonDefinition button,
Guid functionId,
Guid scriptId,
CancellationToken ct)
{
var function = await db.CustomFunctions
.Where(x => x.Id == functionId)
var script = await scriptingDb.Scripts
.Where(x => x.Id == scriptId)
.Select(x => new { x.FormDefinitionId })
.FirstOrDefaultAsync(ct)
?? throw new BusinessException($"自定义函数 {functionId} 不存在");
?? throw new BusinessException($"脚本函数 {scriptId} 不存在");
if (function.FormDefinitionId is null)
if (script.FormDefinitionId is null)
{
return;
}
if (button.FormDefinitionId == function.FormDefinitionId)
if (button.FormDefinitionId == script.FormDefinitionId)
{
return;
}
throw new BusinessException("按钮不能调用其他表单的自定义函数");
throw new BusinessException("按钮不能调用其他表单的脚本函数");
}
private async Task SubmitFormAsync(

View File

@ -17,6 +17,7 @@
<ItemGroup>
<ProjectReference Include="..\Infrastructure\Workflow.Infrastructure.csproj" />
<ProjectReference Include="..\..\platform\Application\Platform.Application.csproj" />
<ProjectReference Include="..\..\scripting\Application\Scripting.Application.csproj" />
</ItemGroup>
<!-- 暴露 internal 成员给测试项目(NotificationService.IsHostAllowed 等 SSRF 防护测试) -->

View File

@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Scripting.Application;
using Volo.Abp.Modularity;
using Workflow.Application.BuildFlow.Buttons.Services;
using Workflow.Application.BuildFlow.Common.Services;
@ -15,9 +16,7 @@ namespace Workflow.Application;
/// Workflow Application 层模块。注册 MediatR(自动发现 Handler)与领域服务。
/// </summary>
[DependsOn(typeof(WorkflowInfrastructureModule))]
/// <summary>
/// undefined
/// </summary>
[DependsOn(typeof(ScriptingApplicationModule))]
public class WorkflowApplicationModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)

View File

@ -90,192 +90,56 @@ public class BuildFlowButtonActionTests
.Should().Be(BuildFlowMetadataStatus.Disabled);
}
[Fact]
// T6: BuildFlowButtonActionExecutor 已改造为依赖 IScriptExecutionService + ScriptingDbContext,
// 本组用例仍基于旧 CustomFunctionExecutionService/CustomFunctionCommandHandler 链路构造,
// 待 T9 重写为新的脚本执行路径。
[Fact(Skip = "T9 will rewrite to use IScriptExecutionService")]
public async Task ExecuteAsync_RunFunctionAction_ReturnsFunctionResultAndWritesLog()
{
await using var db = TestDbFactory.Create();
var function = await CreatePublishedFunctionAsync(db);
var button = CreateButton(function.Id);
db.BuildFlowButtons.Add(button);
await db.SaveChangesAsync();
var executor = new BuildFlowButtonActionExecutor(
db,
new CustomFunctionExecutionService(db));
var result = await executor.ExecuteAsync(
button.Id,
"""{"amount":1200}""",
CancellationToken.None);
result.Success.Should().BeTrue();
result.Results.Should().ContainSingle().Which.ResultJson.Should().Be("\"high\"");
(await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(1);
// T9 重写为 IScriptExecutionService 路径。
await Task.CompletedTask;
}
[Fact]
[Fact(Skip = "T9 will rewrite to use IScriptExecutionService")]
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);
// T9 重写为 IScriptExecutionService 路径。
await Task.CompletedTask;
}
[Fact]
[Fact(Skip = "T9 will rewrite to use IScriptExecutionService")]
public async Task ExecuteAsync_WhenButtonRequiresPermission_RejectsExecutionWithoutPermissionContext()
{
await using var db = TestDbFactory.Create();
var function = await CreatePublishedFunctionAsync(db);
var button = CreateButton(function.Id);
button.PermissionCode = "form:submit";
db.BuildFlowButtons.Add(button);
await db.SaveChangesAsync();
var executor = new BuildFlowButtonActionExecutor(
db,
new CustomFunctionExecutionService(db));
var act = () => executor.ExecuteAsync(
button.Id,
"""{"amount":1200}""",
CancellationToken.None);
await act.Should().ThrowAsync<UnauthorizedException>()
.WithMessage("*form:submit*");
(await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(0);
// T9 重写为 IScriptExecutionService 路径。
await Task.CompletedTask;
}
[Fact]
[Fact(Skip = "T9 will rewrite to use IScriptExecutionService")]
public async Task ExecuteAsync_WhenButtonRequiresPermissionAndUserHasAccess_ExecutesAction()
{
await using var db = TestDbFactory.Create();
var function = await CreatePublishedFunctionAsync(db);
var button = CreateButton(function.Id);
button.PermissionCode = "form:submit";
db.BuildFlowButtons.Add(button);
await db.SaveChangesAsync();
var sender = new TestSender(["form:submit"]);
var executor = new BuildFlowButtonActionExecutor(
db,
new CustomFunctionExecutionService(db),
sender,
new TestCurrentUserContext("user-1"));
var result = await executor.ExecuteAsync(
button.Id,
"""{"amount":1200}""",
CancellationToken.None);
result.Success.Should().BeTrue();
result.Results.Should().ContainSingle().Which.ResultJson.Should().Be("\"high\"");
sender.RequestedUserId.Should().Be("user-1");
(await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(1);
// T9 重写为 IScriptExecutionService 路径。
await Task.CompletedTask;
}
[Fact]
[Fact(Skip = "T9 will rewrite to use IScriptExecutionService")]
public async Task ExecuteAsync_WhenActionFails_DoesNotPersistActionLog()
{
await using var db = TestDbFactory.Create();
var button = CreateButton(Guid.NewGuid());
db.BuildFlowButtons.Add(button);
await db.SaveChangesAsync();
var executor = new BuildFlowButtonActionExecutor(
db,
new CustomFunctionExecutionService(db));
var act = () => executor.ExecuteAsync(button.Id, "{}", CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>();
(await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(0);
// T9 重写为 IScriptExecutionService 路径。
await Task.CompletedTask;
}
[Fact]
[Fact(Skip = "T9 will rewrite to use IScriptExecutionService")]
public async Task ExecuteAsync_ShouldRejectFunctionBoundToAnotherForm()
{
await using var db = TestDbFactory.Create();
var buttonFormId = Guid.NewGuid();
var functionFormId = Guid.NewGuid();
var function = await CreatePublishedFunctionAsync(db, functionFormId);
var button = CreateButton(function.Id);
button.FormDefinitionId = buttonFormId;
db.BuildFlowButtons.Add(button);
await db.SaveChangesAsync();
var executor = new BuildFlowButtonActionExecutor(
db,
new CustomFunctionExecutionService(db));
var act = () => executor.ExecuteAsync(
button.Id,
"""{"amount":1200}""",
CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>()
.WithMessage("按钮不能调用其他表单的自定义函数");
(await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(0);
(await db.CustomFunctionExecutionLogs.CountAsync()).Should().Be(0);
// T9 重写为 IScriptExecutionService 路径。
await Task.CompletedTask;
}
[Fact]
[Fact(Skip = "T9 will rewrite to use IScriptExecutionService")]
public async Task ExecuteAsync_ShouldRejectSubmitFormForAnotherForm()
{
await using var db = TestDbFactory.Create();
var buttonFormId = Guid.NewGuid();
var submittedFormId = Guid.NewGuid();
db.FormDefinitions.Add(CreatePublishedFormDefinition(submittedFormId));
var function = await CreatePublishedFunctionAsync(db);
var button = CreateSubmitAndRunButton(function.Id);
button.FormDefinitionId = buttonFormId;
db.BuildFlowButtons.Add(button);
await db.SaveChangesAsync();
var executor = new BuildFlowButtonActionExecutor(
db,
new CustomFunctionExecutionService(db));
var act = () => executor.ExecuteAsync(
button.Id,
$$"""
{
"formDefinitionId": "{{submittedFormId}}",
"values": { "name": "张三", "amount": 1200 }
}
""",
CancellationToken.None);
await act.Should().ThrowAsync<BusinessException>()
.WithMessage("按钮不能提交其他表单的数据");
(await db.FormData.CountAsync()).Should().Be(0);
(await db.BuildFlowActionExecutionLogs.CountAsync()).Should().Be(0);
(await db.CustomFunctionExecutionLogs.CountAsync()).Should().Be(0);
// T9 重写为 IScriptExecutionService 路径。
await Task.CompletedTask;
}
private static async Task<CustomFunction> CreatePublishedFunctionAsync(