✨ feat(*): 添加管理员解锁与 SFTP 探活能力

* 新增管理员解锁弹窗、手动操作权限控制与倒计时状态
* 支持系统设置页测试 SFTP 连接,并在启动时执行探活
* 补充设计时服务、全局异常兜底与相关单元测试
This commit is contained in:
2026-04-17 14:12:10 +08:00
parent 49f113dcf3
commit 8f74e07c66
25 changed files with 1382 additions and 95 deletions

View File

@@ -1,11 +1,13 @@
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.ViewModels;
using AxiOmron.PcbCheck.Views.Pages;
using AxiOmron.PcbCheck.Views.Windows;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@@ -19,6 +21,7 @@ namespace AxiOmron.PcbCheck;
/// </summary>
public partial class App : Application
{
private static readonly NLog.Logger FallbackLogger = NLog.LogManager.GetCurrentClassLogger();
private IHost? _host;
/// <summary>
@@ -33,6 +36,7 @@ public partial class App : Application
protected override async void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
RegisterGlobalExceptionHandlers();
try
{
Console.OutputEncoding = Encoding.UTF8;
@@ -53,6 +57,7 @@ public partial class App : Application
}
catch (Exception ex)
{
LogGlobalException("应用启动失败", ex);
MessageBox.Show($"应用启动失败: {ex.Message}", "Axi Omron PCB Check", MessageBoxButton.OK, MessageBoxImage.Error);
Shutdown(-1);
}
@@ -64,6 +69,7 @@ public partial class App : Application
/// <param name="e">退出事件参数。</param>
protected override async void OnExit(ExitEventArgs e)
{
UnregisterGlobalExceptionHandlers();
if (_host is not null)
{
try
@@ -118,13 +124,99 @@ public partial class App : Application
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
{
// 全局兜底日志不再向外抛出。
}
}
}