using BuildFlow.Workflow.Tests.Reliability; using FluentAssertions; using Microsoft.EntityFrameworkCore; using System.Text.Json.Nodes; using Workflow.Application.Form.Aggregates; using Workflow.Domain.Entities; using Workflow.Domain.Enums; using Xunit; namespace BuildFlow.Workflow.Tests.Performance; /// /// 表单聚合刷新查询次数门禁,避免目标行和统计字段触发 N+1。 /// [Collection(PostgresWorkflowCollection.Name)] public class FormAggregateQueryCountTests(PostgresWorkflowFixture fixture) { [Fact] public async Task AggregateRefreshUsesConstantSelectCountForManyRowsAndFields() { await ResetAsync(); var targetFormId = Guid.NewGuid(); var sourceFormId = Guid.NewGuid(); var instanceId = Guid.NewGuid(); await using (var seed = fixture.CreateDbContext()) { seed.FormDefinitions.AddRange( new FormDefinition { Id = targetFormId, Name = "费用汇总", Code = "expense-main", Version = 1, Status = FormStatus.Published, SchemaJson = AggregateSchema, }, new FormDefinition { Id = sourceFormId, Name = "费用明细", Code = "expense-line", Version = 1, Status = FormStatus.Published, SchemaJson = """{"type":"object","properties":{}}""", }); seed.WorkflowInstances.Add(new WorkflowInstance { Id = instanceId, DefinitionId = Guid.NewGuid(), DefinitionVersion = 1, Title = "聚合查询计数", Status = InstanceStatus.Running, InitiatorId = Guid.NewGuid(), Variables = "{}", }); for (var i = 0; i < 10; i++) { seed.FormData.Add(new global::Workflow.Domain.Entities.FormData { Id = Guid.NewGuid(), FormDefinitionId = targetFormId, InstanceId = instanceId, DataJson = $$"""{"requestId":"REQ-{{i}}","lineTotal":0,"lineCount":0}""", }); seed.FormData.AddRange( CreateSourceRow(sourceFormId, instanceId, i, 10), CreateSourceRow(sourceFormId, instanceId, i, 20)); } await seed.SaveChangesAsync(); } var interceptor = new CountingCommandInterceptor(); await using (var db = fixture.CreateDbContext(interceptor)) { interceptor.Reset(); var service = new FormAggregateRefreshService(db); await service.RefreshAsync(targetFormId, instanceId, CancellationToken.None); interceptor.SelectCount.Should().Be(4); } await using var verify = fixture.CreateDbContext(); var targetRows = await verify.FormData .Where(x => x.FormDefinitionId == targetFormId) .ToListAsync(); targetRows.Should().HaveCount(10); targetRows.Should().OnlyContain(x => HasExpectedAggregates(x)); var instance = await verify.WorkflowInstances.SingleAsync(x => x.Id == instanceId); var variables = JsonNode.Parse(instance.Variables!)!.AsObject(); variables["lineTotal"]!.GetValue().Should().Be(30); variables["lineCount"]!.GetValue().Should().Be(2); } [Fact] public async Task AggregateRefreshSupportsSourceRowsFromTheTargetForm() { await ResetAsync(); var formDefinitionId = Guid.NewGuid(); var instanceId = Guid.NewGuid(); await using (var seed = fixture.CreateDbContext()) { seed.FormDefinitions.Add(new FormDefinition { Id = formDefinitionId, Name = "自聚合表单", Code = "self-aggregate", Version = 1, Status = FormStatus.Published, SchemaJson = SelfAggregateSchema, }); seed.FormData.AddRange( new global::Workflow.Domain.Entities.FormData { Id = Guid.NewGuid(), FormDefinitionId = formDefinitionId, InstanceId = instanceId, DataJson = """{"requestId":"A","amount":10,"total":0}""", }, new global::Workflow.Domain.Entities.FormData { Id = Guid.NewGuid(), FormDefinitionId = formDefinitionId, InstanceId = instanceId, DataJson = """{"requestId":"B","amount":20,"total":0}""", }); await seed.SaveChangesAsync(); } await using (var db = fixture.CreateDbContext()) { var service = new FormAggregateRefreshService(db); await service.RefreshAsync(formDefinitionId, instanceId, CancellationToken.None); } await using var verify = fixture.CreateDbContext(); var rows = await verify.FormData .Where(x => x.FormDefinitionId == formDefinitionId) .ToListAsync(); rows.Should().Contain(x => HasTotal(x, 10)); rows.Should().Contain(x => HasTotal(x, 20)); } private async Task ResetAsync() { await using var db = fixture.CreateDbContext(); await db.WorkflowTimerJobs.ExecuteDeleteAsync(); await db.WorkflowTasks.ExecuteDeleteAsync(); await db.WorkflowTokens.ExecuteDeleteAsync(); await db.FormData.ExecuteDeleteAsync(); await db.BuildFlowImportJobs.ExecuteDeleteAsync(); await db.WorkflowInstances.ExecuteDeleteAsync(); await db.FormDefinitions.ExecuteDeleteAsync(); } private static global::Workflow.Domain.Entities.FormData CreateSourceRow( Guid sourceFormId, Guid instanceId, int requestIndex, int amount) => new() { Id = Guid.NewGuid(), FormDefinitionId = sourceFormId, InstanceId = instanceId, DataJson = $$"""{"requestId":"REQ-{{requestIndex}}","amount":{{amount}}}""", }; private static bool HasExpectedAggregates( global::Workflow.Domain.Entities.FormData row) { var data = JsonNode.Parse(row.DataJson)!.AsObject(); return data["lineTotal"]!.GetValue() == 30 && data["lineCount"]!.GetValue() == 2; } private static bool HasTotal( global::Workflow.Domain.Entities.FormData row, decimal expected) { var data = JsonNode.Parse(row.DataJson)!.AsObject(); return data["amount"]!.GetValue() == expected && data["total"]!.GetValue() == expected; } private const string AggregateSchema = """ { "type": "object", "properties": { "requestId": { "type": "string", "title": "申请编号", "x-component": "Input" }, "lineTotal": { "type": "number", "title": "明细总金额", "x-component": "InputNumber", "x-field-kind": "aggregate", "x-derived": { "mode": "aggregate", "source": { "type": "formData", "formDefinitionCode": "expense-line", "relationPath": "requestId", "relationValuePath": "requestId" }, "aggregate": { "function": "sum", "fieldPath": "amount" } } }, "lineCount": { "type": "number", "title": "明细数量", "x-component": "InputNumber", "x-field-kind": "aggregate", "x-derived": { "mode": "aggregate", "source": { "type": "formData", "formDefinitionCode": "expense-line", "relationPath": "requestId", "relationValuePath": "requestId" }, "aggregate": { "function": "count" } } } } } """; private const string SelfAggregateSchema = """ { "type": "object", "properties": { "requestId": { "type": "string", "x-component": "Input" }, "amount": { "type": "number", "x-component": "InputNumber" }, "total": { "type": "number", "x-component": "InputNumber", "x-field-kind": "aggregate", "x-derived": { "mode": "aggregate", "source": { "type": "formData", "formDefinitionCode": "self-aggregate", "relationPath": "requestId", "relationValuePath": "requestId" }, "aggregate": { "function": "sum", "fieldPath": "amount" } } } } } """; }