✨ feat(*): 重构PCB放行流程并接入双扫码校验
- 将流程改为 OMRON FINS + TCP 双扫码 + SFTP 文件校验 - 新增文件校验与扫码结果汇总实现,补充对应单测 - 同步更新流程设计文档、配置和系统设置页面文案
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
using System.IO;
|
||||
using AxiOmron.PcbCheck.Models;
|
||||
using AxiOmron.PcbCheck.Options;
|
||||
using AxiOmron.PcbCheck.Services.Interfaces;
|
||||
|
||||
namespace AxiOmron.PcbCheck.Services.Implementations;
|
||||
|
||||
/// <summary>
|
||||
/// 提供本地磁盘目录的文件存在性校验能力。
|
||||
/// </summary>
|
||||
public sealed class LocalFileCheckProvider : IFileCheckProvider
|
||||
{
|
||||
private readonly IAppLogger<LocalFileCheckProvider> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化本地文件校验提供程序。
|
||||
/// </summary>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
public LocalFileCheckProvider(IAppLogger<LocalFileCheckProvider> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前提供程序对应的通道类型。
|
||||
/// </summary>
|
||||
public FileCheckProvider Provider => FileCheckProvider.Local;
|
||||
|
||||
/// <summary>
|
||||
/// 按条码检查本地目录中是否存在目标文件。
|
||||
/// </summary>
|
||||
/// <param name="barcode">条码内容。</param>
|
||||
/// <param name="config">根配置对象。</param>
|
||||
/// <param name="cancellationToken">取消令牌。取消后立即终止目录枚举。</param>
|
||||
/// <returns>文件校验结果。</returns>
|
||||
public async Task<FileCheckOutcome> CheckFileAsync(string barcode, AppConfig config, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
if (string.IsNullOrWhiteSpace(barcode))
|
||||
{
|
||||
return new FileCheckOutcome
|
||||
{
|
||||
Exists = false,
|
||||
IsConfigurationError = true,
|
||||
ErrorMessage = "条码为空,无法执行本地文件查询。"
|
||||
};
|
||||
}
|
||||
|
||||
FileCheckTestOutcome testOutcome = ValidateRootPath(config.FileCheck.Local.RootPath);
|
||||
if (!testOutcome.IsSuccess)
|
||||
{
|
||||
return new FileCheckOutcome
|
||||
{
|
||||
Exists = false,
|
||||
IsConfigurationError = testOutcome.IsConfigurationError,
|
||||
IsSystemError = testOutcome.IsSystemError,
|
||||
ErrorMessage = testOutcome.StatusMessage
|
||||
};
|
||||
}
|
||||
|
||||
return await Task.Run(() => CheckInternal(barcode, config, cancellationToken), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试本地文件校验目录是否可访问。
|
||||
/// </summary>
|
||||
/// <param name="config">根配置对象。</param>
|
||||
/// <param name="cancellationToken">取消令牌。取消后立即终止测试。</param>
|
||||
/// <returns>通道测试结果。</returns>
|
||||
public Task<FileCheckTestOutcome> TestConnectionAsync(AppConfig config, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(config);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(ValidateRootPath(config.FileCheck.Local.RootPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在同步上下文中执行本地文件查找。
|
||||
/// </summary>
|
||||
/// <param name="barcode">条码内容。</param>
|
||||
/// <param name="config">根配置对象。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>文件校验结果。</returns>
|
||||
private FileCheckOutcome CheckInternal(string barcode, AppConfig config, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
string rootPath = Path.GetFullPath(config.FileCheck.Local.RootPath);
|
||||
string candidateName = FileCheckPathMatcher.BuildExpectedFileName(barcode, config.FileCheck.FileNamePattern);
|
||||
string? matchedPath = Directory.EnumerateFiles(rootPath)
|
||||
.FirstOrDefault(path =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return FileCheckPathMatcher.IsMatch(Path.GetFileName(path), candidateName, barcode);
|
||||
});
|
||||
|
||||
if (matchedPath is null)
|
||||
{
|
||||
return new FileCheckOutcome
|
||||
{
|
||||
Exists = false,
|
||||
ConnectionSucceeded = true,
|
||||
ErrorMessage = $"未找到与条码 {barcode} 匹配的本地文件。"
|
||||
};
|
||||
}
|
||||
|
||||
return new FileCheckOutcome
|
||||
{
|
||||
Exists = true,
|
||||
ConnectionSucceeded = true,
|
||||
MatchedFilePath = matchedPath
|
||||
};
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException)
|
||||
{
|
||||
_logger.LogError(ex, "本地文件校验异常");
|
||||
return new FileCheckOutcome
|
||||
{
|
||||
Exists = false,
|
||||
IsSystemError = true,
|
||||
ErrorMessage = $"本地文件校验异常: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证本地根目录配置和可访问性。
|
||||
/// </summary>
|
||||
/// <param name="rootPath">本地根目录。</param>
|
||||
/// <returns>通道测试结果。</returns>
|
||||
private static FileCheckTestOutcome ValidateRootPath(string rootPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rootPath))
|
||||
{
|
||||
return new FileCheckTestOutcome
|
||||
{
|
||||
IsConfigurationError = true,
|
||||
StatusMessage = "本地文件校验目录未配置。"
|
||||
};
|
||||
}
|
||||
|
||||
string fullPath;
|
||||
try
|
||||
{
|
||||
fullPath = Path.GetFullPath(rootPath);
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException)
|
||||
{
|
||||
return new FileCheckTestOutcome
|
||||
{
|
||||
IsConfigurationError = true,
|
||||
StatusMessage = $"本地文件校验目录配置无效: {ex.Message}"
|
||||
};
|
||||
}
|
||||
|
||||
if (!Directory.Exists(fullPath))
|
||||
{
|
||||
return new FileCheckTestOutcome
|
||||
{
|
||||
IsConfigurationError = true,
|
||||
StatusMessage = $"本地文件校验目录不存在: {fullPath}"
|
||||
};
|
||||
}
|
||||
|
||||
return new FileCheckTestOutcome
|
||||
{
|
||||
IsSuccess = true,
|
||||
ConnectionSucceeded = true,
|
||||
RootPathAccessible = true,
|
||||
StatusMessage = $"本地文件校验目录可访问: {fullPath}"
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user