✨ feat(file-check): 添加 PLC 应答后命中文件删除

* 新增默认关闭的删除开关,仅在收到 PLC 放行应答后执行

* 本地与 SFTP Provider 校验根目录并支持幂等删除

* 补充工作流、设置页和文件删除相关测试与文档
This commit is contained in:
2026-08-01 16:42:28 +08:00
parent 404ebadbe7
commit c67509ec82
15 changed files with 700 additions and 1 deletions

View File

@@ -157,6 +157,178 @@ public sealed class WorkflowHostedServiceTests
Assert.All(snapshot.PlcMonitorItems, item => Assert.NotEqual(default, item.LastUpdatedAt));
}
/// <summary>
/// 启用应答后删除功能时,收到 PLC 放行应答后应删除本次精确命中的产品 ID 文件。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ExecuteAsync_ShouldDeleteMatchedFile_WhenReleaseAckReceivedAndDeletionEnabled()
{
const string matchedFilePath = "/pcb/PCB-DELETE-001.txt";
var fileCheckService = new FakeFileCheckService
{
CheckOutcome = new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
MatchedFilePath = matchedFilePath
}
};
var service = new WorkflowHostedService(
new SequencedPlcService(new[]
{
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
new PlcSignalSnapshot { PcbArrived = false, PlcAckRelease = true }
}),
new CountingScannerService
{
Result = new ScanOperationResult
{
IsSuccess = true,
DeviceConnected = true,
Barcode = "PCB-DELETE-001"
}
},
fileCheckService,
new FakeAndonService(),
new AppStateStore(),
new AppConfig
{
Plc = new PlcOptions
{
PollIntervalMs = 20,
ReleaseAckTimeoutMs = 100,
ReleasePulseMs = 10
},
FileCheck = new FileCheckOptions
{
MaxRetryCount = 0,
DeleteMatchedFileAfterPlcAck = true
}
},
new FakeAppLogger<WorkflowHostedService>());
await service.StartAsync(CancellationToken.None);
await Task.Delay(200);
await service.StopAsync(CancellationToken.None);
Assert.Equal(matchedFilePath, Assert.Single(fileCheckService.DeletedFilePaths));
}
/// <summary>
/// 未收到 PLC 放行应答时,即使启用删除功能也不得删除产品 ID 文件。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ExecuteAsync_ShouldNotDeleteMatchedFile_WhenReleaseAckTimesOut()
{
var fileCheckService = new FakeFileCheckService
{
CheckOutcome = new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
MatchedFilePath = "/pcb/PCB-KEEP-001.txt"
}
};
var service = new WorkflowHostedService(
new SequencedPlcService(new[]
{
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = false },
new PlcSignalSnapshot { PcbArrived = false, PlcAckRelease = false }
}),
new CountingScannerService
{
Result = new ScanOperationResult
{
IsSuccess = true,
DeviceConnected = true,
Barcode = "PCB-KEEP-001"
}
},
fileCheckService,
new FakeAndonService(),
new AppStateStore(),
new AppConfig
{
Plc = new PlcOptions
{
PollIntervalMs = 20,
ReleaseAckTimeoutMs = 50,
ReleasePulseMs = 10
},
FileCheck = new FileCheckOptions
{
MaxRetryCount = 0,
DeleteMatchedFileAfterPlcAck = true
}
},
new FakeAppLogger<WorkflowHostedService>());
await service.StartAsync(CancellationToken.None);
await Task.Delay(200);
await service.StopAsync(CancellationToken.None);
Assert.Empty(fileCheckService.DeletedFilePaths);
}
/// <summary>
/// 删除开关关闭时,即使收到 PLC 放行应答也不得删除产品 ID 文件。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ExecuteAsync_ShouldNotDeleteMatchedFile_WhenDeletionDisabled()
{
var fileCheckService = new FakeFileCheckService
{
CheckOutcome = new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
MatchedFilePath = "/pcb/PCB-KEEP-002.txt"
}
};
var service = new WorkflowHostedService(
new SequencedPlcService(new[]
{
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
new PlcSignalSnapshot { PcbArrived = false, PlcAckRelease = true }
}),
new CountingScannerService
{
Result = new ScanOperationResult
{
IsSuccess = true,
DeviceConnected = true,
Barcode = "PCB-KEEP-002"
}
},
fileCheckService,
new FakeAndonService(),
new AppStateStore(),
new AppConfig
{
Plc = new PlcOptions
{
PollIntervalMs = 20,
ReleaseAckTimeoutMs = 100,
ReleasePulseMs = 10
},
FileCheck = new FileCheckOptions
{
MaxRetryCount = 0,
DeleteMatchedFileAfterPlcAck = false
}
},
new FakeAppLogger<WorkflowHostedService>());
await service.StartAsync(CancellationToken.None);
await Task.Delay(200);
await service.StopAsync(CancellationToken.None);
Assert.Empty(fileCheckService.DeletedFilePaths);
}
/// <summary>
/// 双扫码汇总后,应使用汇总条码继续执行统一文件校验。
/// </summary>
@@ -577,14 +749,28 @@ public sealed class WorkflowHostedServiceTests
public FileCheckOutcome CheckOutcome { get; set; } = new();
public FileDeleteOutcome DeleteOutcome { get; set; } = new()
{
IsSuccess = true
};
public string LastBarcode { get; private set; } = string.Empty;
public List<string> DeletedFilePaths { get; } = new();
public Task<FileCheckOutcome> CheckFileAsync(string barcode, CancellationToken cancellationToken)
{
LastBarcode = barcode;
return Task.FromResult(CheckOutcome);
}
public Task<FileDeleteOutcome> DeleteMatchedFileAsync(string filePath, CancellationToken cancellationToken)
{
DeletedFilePaths.Add(filePath);
DeleteOutcome.FilePath = filePath;
return Task.FromResult(DeleteOutcome);
}
public Task<FileCheckTestOutcome> TestConnectionAsync(AppConfig? config, CancellationToken cancellationToken)
{
return Task.FromResult(TestOutcome);