using Serilog.Events; using SqlSugar; using Xunit; using YZWater.Core.Models; using YZWater.Core.Services; namespace YZWater.Core.Tests.Services; /// /// 上次运行遗留的"未恢复"报警收尾。 /// conditionId→DbId 的映射只在内存里,进程被杀/正常重启都来不及回写 RecoverTime: /// 现场库里那条 07:36:19 的 PLC_Connection 记录一直是 NULL,历史页把它显示成"进行中/无限时长", /// 操作员以为设备还在报警。诊断流水(ConditionId 为空)是瞬时事件,本就没有恢复一说,不该被盖上时间。 /// [Collection("Serilog minimum level")] public class AlarmOrphanRepairTests : IDisposable { private readonly LogEventLevel _originalLevel = LogService.MinimumLevel; private readonly AlarmService _service; public AlarmOrphanRepairTests() { LogService.ApplyMinimumLevel("Error"); DatabaseService.Initialize(); _service = new AlarmService(PlcPollingService.Instance); } public void Dispose() { _service.Dispose(); LogService.ApplyMinimumLevel(_originalLevel.ToString()); } private int Insert(AlarmRecord record) => DatabaseService.Db.Insertable(record).ExecuteReturnIdentity(); private DateTime? ReadRecoverTime(int id) => DatabaseService.Db.Queryable().Where(r => r.Id == id).Select(r => r.RecoverTime).First(); [Fact] public async Task Startup_ClosesOrphanedRealAlarm() { var marker = Guid.NewGuid().ToString("N"); var orphanId = Insert(new AlarmRecord { AlarmType = "PLC", AlarmMessage = $"遗留活跃报警 {marker}", AlarmTime = DateTime.Now.AddHours(-2), ConditionId = $"PLC_{marker}" }); Assert.Null(ReadRecoverTime(orphanId)); await _service.CloseOrphanedAlarmsAsync(); var closed = ReadRecoverTime(orphanId); Assert.NotNull(closed); // 收尾时间取本次启动时刻,不是报警时间:中间这段时间设备确实处于无人监视状态 Assert.True(closed!.Value > DateTime.Now.AddMinutes(-5)); } [Fact] public async Task Startup_LeavesJournalEventsAndRecoveredRowsAlone() { var marker = Guid.NewGuid().ToString("N"); // 诊断流水:没有 conditionId,也没有恢复概念 var journalId = Insert(new AlarmRecord { AlarmType = "Quality", AlarmMessage = $"测点质量流水 {marker}", AlarmTime = DateTime.Now.AddHours(-2), IsConfirmed = true, ConfirmedBy = "系统" }); // 正常已恢复的报警,恢复时间不能被覆盖成启动时刻 var recoveredAt = DateTime.Now.AddHours(-3); var recoveredId = Insert(new AlarmRecord { AlarmType = "DI", AlarmMessage = $"已恢复报警 {marker}", AlarmTime = recoveredAt.AddHours(-1), ConditionId = $"DI_{marker}", RecoverTime = recoveredAt }); await _service.CloseOrphanedAlarmsAsync(); Assert.Null(ReadRecoverTime(journalId)); // SQLite 只存到毫秒,比较放宽到 1 秒,避免亚毫秒精度造成假红 var kept = ReadRecoverTime(recoveredId); Assert.NotNull(kept); Assert.True(Math.Abs((kept!.Value - recoveredAt).TotalSeconds) < 1); } }