Files
Axi_Omron/src/AxiOmron.PcbCheck/App.xaml.cs
yunxiao.zhu ce356bffff ✨ feat(*): 重构PCB放行流程并接入双扫码校验
- 将流程改为 OMRON FINS + TCP 双扫码 + SFTP 文件校验
- 新增文件校验与扫码结果汇总实现,补充对应单测
- 同步更新流程设计文档、配置和系统设置页面文案
2026-05-17 14:08:05 +08:00

228 lines
8.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.IO;
using System.Text;
using System.Windows;
using System.Windows.Threading;
using AxiOmron.PcbCheck.Options;
using AxiOmron.PcbCheck.Services.Implementations;
using AxiOmron.PcbCheck.Services.Interfaces;
using AxiOmron.PcbCheck.Utils;
using AxiOmron.PcbCheck.ViewModels;
using AxiOmron.PcbCheck.Views.Pages;
using AxiOmron.PcbCheck.Views.Windows;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;
namespace AxiOmron.PcbCheck;
/// <summary>
/// 表示 WPF 应用入口,负责 Host/DI、配置和日志初始化。
/// </summary>
public partial class App : Application
{
private static readonly NLog.Logger FallbackLogger = NLog.LogManager.GetCurrentClassLogger();
private IHost? _host;
/// <summary>
/// 获取当前应用服务容器。
/// </summary>
public static IServiceProvider Services { get; private set; } = null!;
/// <summary>
/// 应用启动入口。
/// </summary>
/// <param name="e">启动事件参数。</param>
protected override async void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
RegisterGlobalExceptionHandlers();
try
{
Console.OutputEncoding = Encoding.UTF8;
}
catch (IOException)
{
// WinExe 在没有控制台句柄时忽略
}
try
{
_host = BuildHost();
await _host.StartAsync().ConfigureAwait(true);
Services = _host.Services;
var mainWindow = Services.GetRequiredService<MainWindow>();
MainWindow = mainWindow;
mainWindow.Show();
}
catch (Exception ex)
{
LogGlobalException("应用启动失败", ex);
MessageBox.Show($"应用启动失败: {ex.Message}", "Axi Omron PCB Check", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown(-1);
}
}
/// <summary>
/// 应用退出入口。
/// </summary>
/// <param name="e">退出事件参数。</param>
protected override async void OnExit(ExitEventArgs e)
{
UnregisterGlobalExceptionHandlers();
if (_host is not null)
{
try
{
await ShutdownHelper.StopHostAsync(_host, TimeSpan.FromSeconds(2));
}
finally
{
_host.Dispose();
}
}
base.OnExit(e);
}
/// <summary>
/// 构建应用 Host 与依赖注入容器。
/// </summary>
/// <returns>已构建的 Host 实例。</returns>
private static IHost BuildHost()
{
return Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, configurationBuilder) =>
{
configurationBuilder.SetBasePath(AppContext.BaseDirectory);
configurationBuilder.AddJsonFile("appConfig.json", optional: true, reloadOnChange: false);
configurationBuilder.AddJsonFile($"appConfig.{context.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: false);
})
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.SetMinimumLevel(LogLevel.Information);
logging.AddNLog();
})
.ConfigureServices((context, services) =>
{
var appConfig = new AppConfig();
context.Configuration.Bind(appConfig);
services.AddSingleton<IConfiguration>(context.Configuration);
services.AddSingleton<IAppConfigService, AppConfigService>();
services.AddSingleton(appConfig);
services.AddSingleton<IAppStateStore, AppStateStore>();
services.AddSingleton(typeof(IAppLogger<>), typeof(AppLogger<>));
services.AddSingleton<IDispatcherService, DispatcherService>();
services.AddSingleton(TimeProvider.System);
services.AddHttpClient(nameof(AndonService));
services.AddSingleton<IPlcService, OmronFinsPlcService>();
services.AddSingleton<IScannerService, TcpDualScannerService>();
services.AddSingleton<IFileCheckProvider, SftpLookupService>();
services.AddSingleton<IFileCheckProvider, LocalFileCheckProvider>();
services.AddSingleton<IFileCheckService, FileCheckService>();
services.AddSingleton<IAndonService, AndonService>();
services.AddSingleton<IBoardRecordRepository, CsvBoardRecordRepository>();
services.AddSingleton<WorkflowHostedService>();
services.AddSingleton<IWorkflowControlService>(sp => sp.GetRequiredService<WorkflowHostedService>());
services.AddHostedService(sp => sp.GetRequiredService<WorkflowHostedService>());
services.AddSingleton<IAdminUnlockDialogService, AdminUnlockDialogService>();
services.AddSingleton<MainWindowViewModel>();
services.AddSingleton<SystemSettingViewModel>();
services.AddSingleton<DashboardPage>();
services.AddSingleton<SystemSettingsPage>();
services.AddTransient<AdminUnlockDialog>();
services.AddSingleton<MainWindow>();
})
.Build();
}
/// <summary>
/// 注册全局异常处理器,用于兜底记录 UI 线程、未观察任务和域级异常。
/// </summary>
private void RegisterGlobalExceptionHandlers()
{
DispatcherUnhandledException += OnDispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += OnCurrentDomainUnhandledException;
TaskScheduler.UnobservedTaskException += OnTaskSchedulerUnobservedTaskException;
}
/// <summary>
/// 注销全局异常处理器。
/// </summary>
private void UnregisterGlobalExceptionHandlers()
{
DispatcherUnhandledException -= OnDispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException -= OnCurrentDomainUnhandledException;
TaskScheduler.UnobservedTaskException -= OnTaskSchedulerUnobservedTaskException;
}
/// <summary>
/// 处理 UI 线程未捕获异常,记录日志并阻止应用直接闪退。
/// </summary>
/// <param name="sender">事件源。</param>
/// <param name="e">异常事件参数。</param>
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
LogGlobalException("UI 线程未处理异常", e.Exception);
MessageBox.Show($"程序捕获到未处理异常,已写入日志:{e.Exception.Message}", "Axi Omron PCB Check", MessageBoxButton.OK, MessageBoxImage.Warning);
e.Handled = true;
}
/// <summary>
/// 处理未观察任务异常,记录日志并标记为已观察。
/// </summary>
/// <param name="sender">事件源。</param>
/// <param name="e">异常事件参数。</param>
private void OnTaskSchedulerUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
LogGlobalException("未观察任务异常", e.Exception);
e.SetObserved();
}
/// <summary>
/// 处理应用程序域未捕获异常,尽量在进程退出前补充日志。
/// </summary>
/// <param name="sender">事件源。</param>
/// <param name="e">异常事件参数。</param>
private void OnCurrentDomainUnhandledException(object? sender, UnhandledExceptionEventArgs e)
{
var exception = e.ExceptionObject as Exception ?? new Exception(e.ExceptionObject?.ToString() ?? "未知未处理异常");
LogGlobalException($"应用程序域未处理异常(IsTerminating={e.IsTerminating})", exception);
}
/// <summary>
/// 统一记录全局异常日志,优先复用宿主日志器,失败时退回 NLog 直接写入。
/// </summary>
/// <param name="message">日志消息。</param>
/// <param name="exception">异常对象。</param>
private static void LogGlobalException(string message, Exception exception)
{
try
{
if (Services is not null)
{
var logger = Services.GetService<ILogger<App>>();
logger?.LogError(exception, message);
}
}
catch
{
// 忽略宿主日志失败,继续使用回退日志。
}
try
{
FallbackLogger.Error(exception, message);
}
catch
{
// 全局兜底日志不再向外抛出。
}
}
}