✨ feat(*): 重构PCB放行流程并接入双扫码校验

- 将流程改为 OMRON FINS + TCP 双扫码 + SFTP 文件校验
- 新增文件校验与扫码结果汇总实现,补充对应单测
- 同步更新流程设计文档、配置和系统设置页面文案
This commit is contained in:
2026-05-17 14:08:05 +08:00
parent 9a2211ccaa
commit ce356bffff
37 changed files with 4443 additions and 2436 deletions

View File

@@ -7,7 +7,7 @@ using Xunit;
namespace AxiOmron.PcbCheck.Tests;
/// <summary>
/// 验证流程后台服务的 SFTP 启动探活行为。
/// 验证流程后台服务的文件校验启动探活行为。
/// </summary>
public sealed class WorkflowHostedServiceTests
{
@@ -16,15 +16,15 @@ public sealed class WorkflowHostedServiceTests
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ProbeSftpOnStartupAsync_ShouldUpdateSnapshot_WhenConnectionFails()
public async Task ProbeFileCheckOnStartupAsync_ShouldUpdateSnapshot_WhenConnectionFails()
{
var stateStore = new AppStateStore();
var service = new WorkflowHostedService(
new FakePlcService(),
new FakeScannerService(),
new FakeSftpLookupService
new FakeFileCheckService
{
TestOutcome = new SftpConnectionTestOutcome
TestOutcome = new FileCheckTestOutcome
{
IsSuccess = false,
IsSystemError = true,
@@ -36,7 +36,7 @@ public sealed class WorkflowHostedServiceTests
new AppConfig(),
new FakeAppLogger<WorkflowHostedService>());
await service.ProbeSftpOnStartupAsync(CancellationToken.None);
await service.ProbeFileCheckOnStartupAsync(CancellationToken.None);
Assert.Equal("异常", stateStore.GetSnapshot().SftpStatus);
}
@@ -53,7 +53,7 @@ public sealed class WorkflowHostedServiceTests
var service = new WorkflowHostedService(
plcService,
new FakeScannerService(),
new FakeSftpLookupService(),
new FakeFileCheckService(),
new FakeAndonService(),
stateStore,
new AppConfig(),
@@ -78,7 +78,7 @@ public sealed class WorkflowHostedServiceTests
var service = new WorkflowHostedService(
new FailingPlcService(),
new FakeScannerService(),
new FakeSftpLookupService(),
new FakeFileCheckService(),
new FakeAndonService(),
stateStore,
new AppConfig(),
@@ -111,9 +111,9 @@ public sealed class WorkflowHostedServiceTests
Barcode = "PCB-001"
}
};
var sftpService = new FakeSftpLookupService
var fileCheckService = new FakeFileCheckService
{
CheckOutcome = new SftpCheckOutcome
CheckOutcome = new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
@@ -124,7 +124,7 @@ public sealed class WorkflowHostedServiceTests
var service = new WorkflowHostedService(
plcService,
scannerService,
sftpService,
fileCheckService,
new FakeAndonService(),
stateStore,
new AppConfig
@@ -135,11 +135,7 @@ public sealed class WorkflowHostedServiceTests
ReleaseAckTimeoutMs = 100,
ReleasePulseMs = 10
},
Scanner = new ScannerOptions
{
MaxScanAttempts = 1
},
Sftp = new SftpOptions
FileCheck = new FileCheckOptions
{
MaxRetryCount = 0
}
@@ -161,6 +157,69 @@ public sealed class WorkflowHostedServiceTests
Assert.All(snapshot.PlcMonitorItems, item => Assert.NotEqual(default, item.LastUpdatedAt));
}
/// <summary>
/// 双扫码汇总后,应使用汇总条码继续执行统一文件校验。
/// </summary>
/// <returns>异步测试任务。</returns>
[Fact]
public async Task ExecuteAsync_ShouldCheckFileWithMergedBarcode_WhenScannerServiceReturnsMergedBarcode()
{
var stateStore = new AppStateStore();
var plcService = new SequencedPlcService(new[]
{
new PlcSignalSnapshot { PcbArrived = true, PlcAckRelease = true },
new PlcSignalSnapshot { PcbArrived = false, PlcAckRelease = true }
});
var scannerService = new CountingScannerService
{
Result = new ScanOperationResult
{
IsSuccess = true,
DeviceConnected = true,
Barcode = "PCB-MERGED",
RawMessage = "使用扫码枪2"
}
};
var fileCheckService = new FakeFileCheckService
{
CheckOutcome = new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
MatchedFilePath = "/pcb/PCB-MERGED.txt"
}
};
var service = new WorkflowHostedService(
plcService,
scannerService,
fileCheckService,
new FakeAndonService(),
stateStore,
new AppConfig
{
Plc = new PlcOptions
{
PollIntervalMs = 20,
ReleaseAckTimeoutMs = 100,
ReleasePulseMs = 10
},
FileCheck = new FileCheckOptions
{
MaxRetryCount = 0
}
},
new FakeAppLogger<WorkflowHostedService>());
await service.StartAsync(CancellationToken.None);
await Task.Delay(200);
await service.StopAsync(CancellationToken.None);
Assert.Equal(1, scannerService.TriggerCount);
Assert.Equal("PCB-MERGED", fileCheckService.LastBarcode);
Assert.Contains(plcService.WrittenStates, state => state.ResultCode == (ushort)WorkflowResultCode.Passed);
}
/// <summary>
/// 当到位信号保持高电平时,即使执行软件复位,也不应被视为新的上升沿重复触发流程。
/// </summary>
@@ -189,9 +248,9 @@ public sealed class WorkflowHostedServiceTests
var service = new WorkflowHostedService(
plcService,
scannerService,
new FakeSftpLookupService
new FakeFileCheckService
{
CheckOutcome = new SftpCheckOutcome
CheckOutcome = new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
@@ -208,11 +267,7 @@ public sealed class WorkflowHostedServiceTests
ReleaseAckTimeoutMs = 100,
ReleasePulseMs = 10
},
Scanner = new ScannerOptions
{
MaxScanAttempts = 1
},
Sftp = new SftpOptions
FileCheck = new FileCheckOptions
{
MaxRetryCount = 0
}
@@ -266,7 +321,7 @@ public sealed class WorkflowHostedServiceTests
var service = new WorkflowHostedService(
new FakePlcService(),
new FakeScannerService(),
new FakeSftpLookupService(),
new FakeFileCheckService(),
new FakeAndonService(),
stateStore,
new AppConfig
@@ -321,9 +376,9 @@ public sealed class WorkflowHostedServiceTests
var service = new WorkflowHostedService(
plcService,
scannerService,
new FakeSftpLookupService
new FakeFileCheckService
{
CheckOutcome = new SftpCheckOutcome
CheckOutcome = new FileCheckOutcome
{
Exists = true,
ConnectionSucceeded = true,
@@ -340,11 +395,7 @@ public sealed class WorkflowHostedServiceTests
ReleaseAckTimeoutMs = 100,
ReleasePulseMs = 10
},
Scanner = new ScannerOptions
{
MaxScanAttempts = 1
},
Sftp = new SftpOptions
FileCheck = new FileCheckOptions
{
MaxRetryCount = 0
}
@@ -504,15 +555,21 @@ public sealed class WorkflowHostedServiceTests
}
}
private sealed class FakeSftpLookupService : ISftpLookupService
private sealed class FakeFileCheckService : IFileCheckService
{
public SftpConnectionTestOutcome TestOutcome { get; set; } = new();
public FileCheckTestOutcome TestOutcome { get; set; } = new();
public SftpCheckOutcome CheckOutcome { get; set; } = new();
public FileCheckOutcome CheckOutcome { get; set; } = new();
public Task<SftpCheckOutcome> CheckFileAsync(string barcode, CancellationToken cancellationToken) => Task.FromResult(CheckOutcome);
public string LastBarcode { get; private set; } = string.Empty;
public Task<SftpConnectionTestOutcome> TestConnectionAsync(SftpOptions? options, CancellationToken cancellationToken)
public Task<FileCheckOutcome> CheckFileAsync(string barcode, CancellationToken cancellationToken)
{
LastBarcode = barcode;
return Task.FromResult(CheckOutcome);
}
public Task<FileCheckTestOutcome> TestConnectionAsync(AppConfig? config, CancellationToken cancellationToken)
{
return Task.FromResult(TestOutcome);
}