- 将流程改为 OMRON FINS + TCP 双扫码 + SFTP 文件校验 - 新增文件校验与扫码结果汇总实现,补充对应单测 - 同步更新流程设计文档、配置和系统设置页面文案
228 lines
8.6 KiB
C#
228 lines
8.6 KiB
C#
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
|
||
{
|
||
// 全局兜底日志不再向外抛出。
|
||
}
|
||
}
|
||
}
|