✨ feat(*): 添加 PCB 检测记录的 CSV 持久化存储功能
* 新增 CsvBoardRecordRepository 实现按日期分卷的 CSV 记录存储 * 新增 RecordPersistenceModels 定义板件检测记录数据结构 * 在 WorkflowHostedService 中集成检测完成后的记录持久化 * 更新 MainWindowViewModel 支持记录查询与异常标记 * 更新 AppStateStore 添加记录存储相关状态管理 * 新增 DashboardPage UI 元素展示记录存储状态 * 更新 SystemSettingViewModel 与 appConfig 添加存储路径配置 * 注册 TimeProvider 与 IBoardRecordRepository 到 DI 容器 * 新增 CsvBoardRecordRepositoryTests 与 MainWindowViewModelTests * 配置 Release 模式 portable PDB 并在发布时自动移除
This commit is contained in:
@@ -21,6 +21,7 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
private readonly SecurityOptions _securityOptions;
|
||||
private readonly IAdminUnlockDialogService _adminUnlockDialogService;
|
||||
private readonly DispatcherTimer _adminUnlockTimer;
|
||||
private DateTimeOffset _lastAppliedRecordDataUpdatedAt = DateTimeOffset.MinValue;
|
||||
private DateTimeOffset? _adminUnlockedUntil;
|
||||
|
||||
/// <summary>
|
||||
@@ -62,10 +63,8 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
|
||||
_stateStore.SnapshotChanged += OnSnapshotChanged;
|
||||
_stateStore.LogAdded += OnLogAdded;
|
||||
_stateStore.RecordAdded += OnRecordAdded;
|
||||
ApplySnapshot(_stateStore.GetSnapshot());
|
||||
RecalculateLogStatistics();
|
||||
RecalculateProcessStatistics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -225,6 +224,12 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
[ObservableProperty]
|
||||
private string _lastProcessUpdateTime = "-";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置当前处理记录统计日期文本。
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private string _recordStatisticDateText = "-";
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置管理员功能是否已解锁。
|
||||
/// </summary>
|
||||
@@ -377,7 +382,7 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
[RelayCommand]
|
||||
private void RefreshProcessRecords()
|
||||
{
|
||||
RecalculateProcessStatistics();
|
||||
LastProcessUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -399,12 +404,40 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
LastCompletedAt = snapshot.LastCompletedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "-";
|
||||
IsBusy = snapshot.IsBusy;
|
||||
LastUpdatedAt = snapshot.LastUpdatedAt.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
RecordStatisticDateText = snapshot.RecordStatisticDate?.ToString("yyyy-MM-dd") ?? "-";
|
||||
TodayProcessCount = snapshot.TodayProcessCount;
|
||||
TodayOkCount = snapshot.TodayOkCount;
|
||||
TodayNgCount = snapshot.TodayNgCount;
|
||||
|
||||
PlcMonitorItems.Clear();
|
||||
foreach (PlcMonitorItem item in snapshot.PlcMonitorItems)
|
||||
{
|
||||
PlcMonitorItems.Add(item.Clone());
|
||||
}
|
||||
|
||||
if (snapshot.RecordsLastUpdatedAt != _lastAppliedRecordDataUpdatedAt)
|
||||
{
|
||||
_lastAppliedRecordDataUpdatedAt = snapshot.RecordsLastUpdatedAt;
|
||||
RecentBoards.Clear();
|
||||
foreach (BoardProcessRecord record in snapshot.BoardRecords
|
||||
.OrderByDescending(item => item.CompletedAt)
|
||||
.Take(_workflowOptions.MaxBoardRecords))
|
||||
{
|
||||
RecentBoards.Add(new BoardProcessRecord
|
||||
{
|
||||
StartedAt = record.StartedAt,
|
||||
CompletedAt = record.CompletedAt,
|
||||
Barcode = record.Barcode,
|
||||
ScanTryCount = record.ScanTryCount,
|
||||
SftpTryCount = record.SftpTryCount,
|
||||
ResultCode = record.ResultCode,
|
||||
ResultDescription = record.ResultDescription,
|
||||
ReleaseSent = record.ReleaseSent,
|
||||
AlarmRaised = record.AlarmRaised,
|
||||
ExceptionSummary = record.ExceptionSummary
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -450,7 +483,8 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
/// <param name="e">集合变化参数。</param>
|
||||
private void OnRecentBoardsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
RecalculateProcessStatistics();
|
||||
HasProcessRecords = RecentBoards.Count > 0;
|
||||
LastProcessUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -488,42 +522,6 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
LastLogUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据当前处理记录集合重新计算处理区的统计信息与最后刷新时间。
|
||||
/// </summary>
|
||||
private void RecalculateProcessStatistics()
|
||||
{
|
||||
DateTime today = DateTime.Today;
|
||||
int totalToday = 0;
|
||||
int okToday = 0;
|
||||
int ngToday = 0;
|
||||
|
||||
foreach (BoardProcessRecord record in RecentBoards)
|
||||
{
|
||||
if (record.CompletedAt.LocalDateTime.Date != today)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
totalToday++;
|
||||
if (record.ResultCode == (ushort)WorkflowResultCode.Passed)
|
||||
{
|
||||
okToday++;
|
||||
}
|
||||
else if (record.ResultCode != (ushort)WorkflowResultCode.Processing
|
||||
&& record.ResultCode != (ushort)WorkflowResultCode.None)
|
||||
{
|
||||
ngToday++;
|
||||
}
|
||||
}
|
||||
|
||||
TodayProcessCount = totalToday;
|
||||
TodayOkCount = okToday;
|
||||
TodayNgCount = ngToday;
|
||||
HasProcessRecords = RecentBoards.Count > 0;
|
||||
LastProcessUpdateTime = DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理运行态快照变化事件。
|
||||
/// </summary>
|
||||
@@ -531,7 +529,7 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
/// <param name="snapshot">最新快照。</param>
|
||||
private async void OnSnapshotChanged(object? sender, RuntimeSnapshot snapshot)
|
||||
{
|
||||
await _dispatcherService.InvokeAsync(() => ApplySnapshot(snapshot)).ConfigureAwait(false);
|
||||
await ApplySnapshotOnUiAsync(snapshot).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -541,34 +539,9 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
/// <param name="entry">新增日志。</param>
|
||||
private async void OnLogAdded(object? sender, UiLogEntry entry)
|
||||
{
|
||||
await _dispatcherService.InvokeAsync(() =>
|
||||
{
|
||||
Logs.Insert(0, entry);
|
||||
while (Logs.Count > _workflowOptions.MaxUiLogEntries)
|
||||
{
|
||||
Logs.RemoveAt(Logs.Count - 1);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
await AppendLogOnUiAsync(entry).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理新增单板记录事件。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源。</param>
|
||||
/// <param name="record">新增单板记录。</param>
|
||||
private async void OnRecordAdded(object? sender, BoardProcessRecord record)
|
||||
{
|
||||
await _dispatcherService.InvokeAsync(() =>
|
||||
{
|
||||
RecentBoards.Insert(0, record);
|
||||
while (RecentBoards.Count > _workflowOptions.MaxBoardRecords)
|
||||
{
|
||||
RecentBoards.RemoveAt(RecentBoards.Count - 1);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理管理员解锁倒计时,超时后自动恢复锁定。
|
||||
/// </summary>
|
||||
/// <param name="sender">事件源。</param>
|
||||
@@ -641,4 +614,45 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在 UI 线程中安全应用最新运行态快照;若应用关闭导致 Dispatcher 调度被取消,则忽略本次刷新。
|
||||
/// </summary>
|
||||
/// <param name="snapshot">待应用的运行态快照。</param>
|
||||
/// <returns>表示刷新流程结束的任务。</returns>
|
||||
private async Task ApplySnapshotOnUiAsync(RuntimeSnapshot snapshot)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _dispatcherService.InvokeAsync(() => ApplySnapshot(snapshot)).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 应用关闭或 Dispatcher 停止时,取消属于预期行为,避免 async void 事件将其上抛为未处理异常。
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在 UI 线程中安全追加日志;若应用关闭导致 Dispatcher 调度被取消,则忽略本次日志刷新。
|
||||
/// </summary>
|
||||
/// <param name="entry">待追加的日志项。</param>
|
||||
/// <returns>表示日志刷新流程结束的任务。</returns>
|
||||
private async Task AppendLogOnUiAsync(UiLogEntry entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _dispatcherService.InvokeAsync(() =>
|
||||
{
|
||||
Logs.Insert(0, entry);
|
||||
while (Logs.Count > _workflowOptions.MaxUiLogEntries)
|
||||
{
|
||||
Logs.RemoveAt(Logs.Count - 1);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 应用关闭或 Dispatcher 停止时,取消属于预期行为,避免 async void 事件将其上抛为未处理异常。
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user