diff --git a/src/modules/workflow/Application/BuildFlow/Imports/Services/ImportBatchItem.cs b/src/modules/workflow/Application/BuildFlow/Imports/Services/ImportBatchItem.cs
new file mode 100644
index 0000000..bf7c62a
--- /dev/null
+++ b/src/modules/workflow/Application/BuildFlow/Imports/Services/ImportBatchItem.cs
@@ -0,0 +1,6 @@
+using Workflow.Application.BuildFlow.Common.Services;
+
+namespace Workflow.Application.BuildFlow.Imports.Services;
+
+/// 导入批次中的一行及其从零开始的全局行索引。
+public sealed record ImportBatchItem(int RowIndex, ExcelParsedRow Row);
diff --git a/src/modules/workflow/Application/BuildFlow/Imports/Services/ImportBatchResult.cs b/src/modules/workflow/Application/BuildFlow/Imports/Services/ImportBatchResult.cs
new file mode 100644
index 0000000..b7c0dae
--- /dev/null
+++ b/src/modules/workflow/Application/BuildFlow/Imports/Services/ImportBatchResult.cs
@@ -0,0 +1,6 @@
+namespace Workflow.Application.BuildFlow.Imports.Services;
+
+/// 导入批次写入结果,包含成功数量和逐行错误。
+public sealed record ImportBatchResult(
+ int SuccessCount,
+ IReadOnlyList Errors);
diff --git a/src/modules/workflow/Application/BuildFlow/Imports/Services/ImportBatchWriter.cs b/src/modules/workflow/Application/BuildFlow/Imports/Services/ImportBatchWriter.cs
new file mode 100644
index 0000000..060f4b1
--- /dev/null
+++ b/src/modules/workflow/Application/BuildFlow/Imports/Services/ImportBatchWriter.cs
@@ -0,0 +1,102 @@
+using System.Buffers.Binary;
+using System.Security.Cryptography;
+using Workflow.Application.Form.Aggregates;
+using Workflow.Application.Form.Derivation;
+using Workflow.Application.Form.Schema;
+using Workflow.Infrastructure.Persistence;
+
+namespace Workflow.Application.BuildFlow.Imports.Services;
+
+///
+/// 导入批次写入用例。复用预解析 Schema,批量保存表单行,并在批次末统一刷新聚合字段。
+///
+public sealed class ImportBatchWriter(
+ WorkflowDbContext db,
+ IFormValueDerivationService derivationService,
+ IFormAggregateRefreshService aggregateRefreshService)
+{
+ /// 写入一个已受事务保护的导入批次。
+ public async Task WriteAsync(
+ Guid jobId,
+ Guid formDefinitionId,
+ PreparedFormSchema schema,
+ IReadOnlyList items,
+ CancellationToken ct)
+ {
+ var errors = new List();
+ var formDataRows = new List();
+ var instanceIds = new List();
+
+ foreach (var item in items)
+ {
+ ct.ThrowIfCancellationRequested();
+ if (item.Row.Errors.Count > 0)
+ {
+ errors.Add(new ImportRowError(
+ item.Row.RowNumber,
+ item.Row.Errors.Select(x => x.Message).ToList()));
+ continue;
+ }
+
+ try
+ {
+ var instanceId = DeterministicInstanceId(jobId, item.RowIndex);
+ var derivation = await derivationService.DeriveAsync(
+ schema,
+ item.Row.JsonObject,
+ new FormDerivationContext(formDefinitionId, instanceId, null),
+ ct);
+ var validation = FormDataValidator.Validate(schema, derivation.DataJson);
+ if (!validation.IsValid)
+ {
+ errors.Add(new ImportRowError(item.Row.RowNumber, validation.Errors));
+ continue;
+ }
+
+ formDataRows.Add(new Domain.Entities.FormData
+ {
+ Id = Guid.NewGuid(),
+ FormDefinitionId = formDefinitionId,
+ InstanceId = instanceId,
+ DataJson = derivation.DataJson,
+ });
+ instanceIds.Add(instanceId);
+ }
+ catch (OperationCanceledException) when (ct.IsCancellationRequested)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ errors.Add(new ImportRowError(item.Row.RowNumber, [ex.Message]));
+ }
+ }
+
+ if (formDataRows.Count > 0)
+ {
+ db.FormData.AddRange(formDataRows);
+ await db.SaveChangesAsync(ct);
+ var distinctInstanceIds = instanceIds.Distinct().ToArray();
+ await aggregateRefreshService.RefreshBatchAsync(
+ formDefinitionId,
+ distinctInstanceIds,
+ ct);
+ await aggregateRefreshService.RefreshAffectedBatchAsync(
+ formDefinitionId,
+ distinctInstanceIds,
+ ct);
+ }
+
+ return new ImportBatchResult(formDataRows.Count, errors);
+ }
+
+ private static Guid DeterministicInstanceId(Guid jobId, int rowIndex)
+ {
+ Span input = stackalloc byte[20];
+ jobId.TryWriteBytes(input[..16]);
+ BinaryPrimitives.WriteInt32LittleEndian(input[16..], rowIndex);
+ Span hash = stackalloc byte[32];
+ SHA256.HashData(input, hash);
+ return new Guid(hash[..16]);
+ }
+}
diff --git a/src/modules/workflow/Application/WorkflowApplicationModule.cs b/src/modules/workflow/Application/WorkflowApplicationModule.cs
index b15c7f8..662b282 100644
--- a/src/modules/workflow/Application/WorkflowApplicationModule.cs
+++ b/src/modules/workflow/Application/WorkflowApplicationModule.cs
@@ -28,6 +28,7 @@ public class WorkflowApplicationModule : AbpModule
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddScoped();
// 条件评估器:ProcessEngine 依赖它做条件节点分支判断。无参构造内置对比器责任链。
diff --git a/tests/BuildFlow.Workflow.Tests/Performance/ImportBatchWriterTests.cs b/tests/BuildFlow.Workflow.Tests/Performance/ImportBatchWriterTests.cs
new file mode 100644
index 0000000..ad2939d
--- /dev/null
+++ b/tests/BuildFlow.Workflow.Tests/Performance/ImportBatchWriterTests.cs
@@ -0,0 +1,106 @@
+using FluentAssertions;
+using Microsoft.EntityFrameworkCore;
+using NSubstitute;
+using Workflow.Application.BuildFlow.Common.Services;
+using Workflow.Application.BuildFlow.Imports.Services;
+using Workflow.Application.Form.Aggregates;
+using Workflow.Application.Form.Derivation;
+using Workflow.Application.Form.Schema;
+using Xunit;
+
+namespace BuildFlow.Workflow.Tests.Performance;
+
+///
+/// 导入批次写入、错误隔离和确定性实例 ID 测试。
+///
+public class ImportBatchWriterTests
+{
+ [Fact]
+ public async Task WritesValidRowsAndReturnsInvalidRowsWithoutPerRowAggregateCalls()
+ {
+ await using var db = CreateDbContext();
+ var aggregates = Substitute.For();
+ var writer = new ImportBatchWriter(
+ db,
+ new FormValueDerivationService([], []),
+ aggregates);
+ var formDefinitionId = Guid.NewGuid();
+ var prepared = PreparedFormSchema.Create(Schema);
+
+ var result = await writer.WriteAsync(
+ Guid.NewGuid(),
+ formDefinitionId,
+ prepared,
+ [
+ new ImportBatchItem(0, ValidRow(2, "first")),
+ new ImportBatchItem(
+ 1,
+ new ExcelParsedRow(
+ 3,
+ "{}",
+ [new ExcelCellError("name", "必填字段为空")])),
+ new ImportBatchItem(2, ValidRow(4, "third")),
+ ],
+ CancellationToken.None);
+
+ result.SuccessCount.Should().Be(2);
+ result.Errors.Should().ContainSingle(x => x.Row == 3);
+ (await db.FormData.CountAsync()).Should().Be(2);
+ await aggregates.Received(1).RefreshBatchAsync(
+ formDefinitionId,
+ Arg.Is>(ids => ids.Count == 2),
+ Arg.Any());
+ await aggregates.Received(1).RefreshAffectedBatchAsync(
+ formDefinitionId,
+ Arg.Is>(ids => ids.Count == 2),
+ Arg.Any());
+ }
+
+ [Fact]
+ public async Task UsesFullJobAndRowIndexForDeterministicInstanceIds()
+ {
+ await using var db = CreateDbContext();
+ var aggregates = Substitute.For();
+ var writer = new ImportBatchWriter(
+ db,
+ new FormValueDerivationService([], []),
+ aggregates);
+
+ await writer.WriteAsync(
+ Guid.Parse("8e6fcacf-3bc1-4d5e-84c4-92be4208855c"),
+ Guid.NewGuid(),
+ PreparedFormSchema.Create(Schema),
+ [
+ new ImportBatchItem(1, ValidRow(2, "first")),
+ new ImportBatchItem(65537, ValidRow(3, "second")),
+ ],
+ CancellationToken.None);
+
+ var instanceIds = await db.FormData
+ .Select(x => x.InstanceId)
+ .ToListAsync();
+ instanceIds.Should().OnlyHaveUniqueItems();
+ }
+
+ private static WorkflowDbContext CreateDbContext()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase($"import-batch-writer-{Guid.NewGuid():N}")
+ .Options;
+ var db = new WorkflowDbContext(options);
+ db.SavingChanges += AuditStub.OnSave;
+ return db;
+ }
+
+ private static ExcelParsedRow ValidRow(int rowNumber, string name)
+ => new(rowNumber, $$"""{"name":"{{name}}"}""", []);
+
+ private const string Schema = """
+ {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string", "required": true, "x-component": "Input" }
+ }
+ }
+ """;
+}