浏览代码

fix: PLC 断连时停止数据记录,防止过期数据污染数据库

- PlcDataModel 添加 HasValidData 计算属性,统一数据有效性判断
- PlcPollingService 断连时立即标记 IsPlcConnected/IsDataValid 为 false
- DataLoggingService 三个写入方法增加连接状态检查
- AutoControlService 断连时跳过泵启停控制
- WorkflowService 断连时跳过泵控制操作
DESKTOP-74CLTRG\Leol 1 月之前
父节点
当前提交
ffb50e722a

+ 4 - 0
src/YZWater.Core/Models/PlcDataModel.cs

@@ -1,5 +1,6 @@
 using System.Collections.Concurrent;
 using CommunityToolkit.Mvvm.ComponentModel;
+using YZWater.Core.Services;
 
 namespace YZWater.Core.Models;
 
@@ -189,6 +190,9 @@ public partial class PlcDataModel : ObservableObject
     public bool HasAnyFault => Pump1Fault || Pump2Fault || Pump3Fault || Pump4Fault || Pump5Fault ||
                                Fan1Fault || Fan2Fault;
 
+    /// <summary>数据可用于记录/控制(Mock 模式视为始终有效)</summary>
+    public bool HasValidData => MockPlcService.IsEnabled || IsPlcConnected;
+
     /// <summary>
     /// 获取泵运行状态
     /// </summary>

+ 3 - 0
src/YZWater.Core/Services/AutoControlService.cs

@@ -157,6 +157,9 @@ public class AutoControlService : IDisposable
     {
         if (!_isEnabled) return;
 
+        // PLC 未连接时不执行自动控制(Mock 模式除外,因为本来就没有真实 PLC)
+        if (!data.HasValidData) return;
+
         var profile = ProjectService.Current;
         if (profile == null || profile.Tanks.Count == 0 || profile.Pumps.Count == 0)
             return;

+ 78 - 37
src/YZWater.Core/Services/DataLoggingService.cs

@@ -40,11 +40,6 @@ public class DataLoggingService : IDisposable
     /// </summary>
     public int DeviceStatusLogIntervalSeconds { get; set; } = 300;
 
-    /// <summary>
-    /// 数据保留天数
-    /// </summary>
-    public int DataRetentionDays { get; set; } = 30;
-
     public DataLoggingService(PlcPollingService pollingService)
     {
         _pollingService = pollingService;
@@ -151,7 +146,6 @@ public class DataLoggingService : IDisposable
     {
         var flowTimer = 0;
         var deviceTimer = 0;
-        var cleanupTimer = 0;
 
         while (!ct.IsCancellationRequested)
         {
@@ -161,7 +155,6 @@ public class DataLoggingService : IDisposable
 
                 flowTimer++;
                 deviceTimer++;
-                cleanupTimer++;
 
                 // 定期记录流量数据和模拟量
                 if (flowTimer >= FlowLogIntervalSeconds)
@@ -177,13 +170,7 @@ public class DataLoggingService : IDisposable
                     deviceTimer = 0;
                     await LogDeviceStatusAsync();
                 }
-
-                // 每天清理一次过期数据
-                if (cleanupTimer >= 86400)
-                {
-                    cleanupTimer = 0;
-                    await CleanupOldDataAsync();
-                }
+                // 数据清理由 DataRetentionService 统一管理(使用 SystemConfig.DataRetention 配置)
             }
             catch (OperationCanceledException)
             {
@@ -237,6 +224,12 @@ public class DataLoggingService : IDisposable
         try
         {
             var data = _pollingService.Data;
+            if (!data.HasValidData)
+            {
+                Log.Debug("PLC 未连接,跳过流量记录");
+                return;
+            }
+
             var (totalIn, totalOut) = AccumulateFlow(data.InflowRate, data.OutflowRate);
 
             var record = new FlowRecord
@@ -259,31 +252,65 @@ public class DataLoggingService : IDisposable
     }
 
     /// <summary>
-    /// 记录模拟量数据(液位、泵频率等)
+    /// 记录模拟量数据(按项目统计配置动态采集)
     /// </summary>
     public async Task LogAnalogDataAsync()
     {
         try
         {
             var data = _pollingService.Data;
+            if (!data.HasValidData)
+            {
+                Log.Debug("PLC 未连接,跳过模拟量记录");
+                return;
+            }
+
             var now = DateTime.Now;
+            var profile = ProjectService.Current;
+
+            List<AnalogRecord> records;
 
-            var records = new List<AnalogRecord>
+            if (profile?.Statistics?.Items is { Count: > 0 })
             {
-                new() { RecordTime = now, TagName = "Tank1Level", Value = data.Tank1Level },
-                new() { RecordTime = now, TagName = "Tank2Level", Value = data.Tank2Level },
-                new() { RecordTime = now, TagName = "Tank3Level", Value = data.Tank3Level },
-                new() { RecordTime = now, TagName = "Tank4Level", Value = data.Tank4Level },
-                new() { RecordTime = now, TagName = "Pump1Freq", Value = data.Pump1Freq },
-                new() { RecordTime = now, TagName = "Pump2Freq", Value = data.Pump2Freq },
-                new() { RecordTime = now, TagName = "Pump3Freq", Value = data.Pump3Freq },
-                new() { RecordTime = now, TagName = "Pump4Freq", Value = data.Pump4Freq },
-                new() { RecordTime = now, TagName = "Pump5Freq", Value = data.Pump5Freq },
-                new() { RecordTime = now, TagName = "InflowRate", Value = data.InflowRate },
-                new() { RecordTime = now, TagName = "OutflowRate", Value = data.OutflowRate },
-            };
+                // ── 按项目统计配置采集 ──
+                records = new List<AnalogRecord>();
+                foreach (var item in profile.Statistics.Items)
+                {
+                    if (!item.Enabled) continue;
+
+                    // 从动态寄存器读取值(TagName 与 AnalogRegisters key 约定一致)
+                    if (data.AnalogRegisters.TryGetValue(item.Key, out var value))
+                    {
+                        records.Add(new AnalogRecord
+                        {
+                            RecordTime = now,
+                            TagName = item.Key,
+                            Value = value
+                        });
+                    }
+                }
+            }
+            else
+            {
+                // ── 兼容旧逻辑(无统计配置时使用硬编码) ──
+                records = new List<AnalogRecord>
+                {
+                    new() { RecordTime = now, TagName = "Tank1Level", Value = data.Tank1Level },
+                    new() { RecordTime = now, TagName = "Tank2Level", Value = data.Tank2Level },
+                    new() { RecordTime = now, TagName = "Tank3Level", Value = data.Tank3Level },
+                    new() { RecordTime = now, TagName = "Tank4Level", Value = data.Tank4Level },
+                    new() { RecordTime = now, TagName = "Pump1Freq", Value = data.Pump1Freq },
+                    new() { RecordTime = now, TagName = "Pump2Freq", Value = data.Pump2Freq },
+                    new() { RecordTime = now, TagName = "Pump3Freq", Value = data.Pump3Freq },
+                    new() { RecordTime = now, TagName = "Pump4Freq", Value = data.Pump4Freq },
+                    new() { RecordTime = now, TagName = "Pump5Freq", Value = data.Pump5Freq },
+                    new() { RecordTime = now, TagName = "InflowRate", Value = data.InflowRate },
+                    new() { RecordTime = now, TagName = "OutflowRate", Value = data.OutflowRate },
+                };
+            }
 
-            await _bgDb.Insertable(records).ExecuteCommandAsync();
+            if (records.Count > 0)
+                await _bgDb.Insertable(records).ExecuteCommandAsync();
             Log.Debug("模拟量数据已记录: {Count} 个标签", records.Count);
         }
         catch (Exception ex)
@@ -300,6 +327,12 @@ public class DataLoggingService : IDisposable
         try
         {
             var data = _pollingService.Data;
+            if (!data.HasValidData)
+            {
+                Log.Debug("PLC 未连接,跳过设备状态记录");
+                return;
+            }
+
             var now = DateTime.Now;
             var intervalHours = (float)(now - _lastDeviceLogTime).TotalHours;
             _lastDeviceLogTime = now;
@@ -371,32 +404,40 @@ public class DataLoggingService : IDisposable
     }
 
     /// <summary>
-    /// 清理过期数据
+    /// 清理过期数据(使用 SystemConfig.DataRetention 配置,与 DataRetentionService 保持一致)
+    /// 通常由 DataRetentionService 自动调度,此方法仅供手动触发
     /// </summary>
     public async Task CleanupOldDataAsync()
     {
         try
         {
-            var cutoff = DateTime.Now.AddDays(-DataRetentionDays);
+            var config = ConfigService.GetConfig();
+            var flowRetention = config?.DataRetention?.FlowRecordRetentionDays ?? 360;
+            var analogRetention = config?.DataRetention?.AnalogRecordRetentionDays ?? 360;
+            var alarmRetention = config?.DataRetention?.AlarmRecordRetentionDays ?? 90;
+
+            var flowCutoff = DateTime.Now.AddDays(-flowRetention);
+            var analogCutoff = DateTime.Now.AddDays(-analogRetention);
+            var alarmCutoff = DateTime.Now.AddDays(-alarmRetention);
 
             var flowDeleted = await _bgDb.Deleteable<FlowRecord>()
-                .Where(r => r.RecordTime < cutoff)
+                .Where(r => r.RecordTime < flowCutoff)
                 .ExecuteCommandAsync();
 
             var alarmDeleted = await _bgDb.Deleteable<AlarmRecord>()
-                .Where(r => r.AlarmTime < cutoff && r.IsConfirmed)
+                .Where(r => r.AlarmTime < alarmCutoff && r.IsConfirmed)
                 .ExecuteCommandAsync();
 
             var equipDeleted = await _bgDb.Deleteable<EquipmentStatus>()
-                .Where(r => r.LastUpdateTime < cutoff)
+                .Where(r => r.LastUpdateTime < flowCutoff)
                 .ExecuteCommandAsync();
 
             var analogDeleted = await _bgDb.Deleteable<AnalogRecord>()
-                .Where(r => r.RecordTime < cutoff)
+                .Where(r => r.RecordTime < analogCutoff)
                 .ExecuteCommandAsync();
 
-            Log.Information("数据清理完成: 删除 {Flow} 条流量记录, {Alarm} 条报警记录, {Equip} 条设备状态, {Analog} 条模拟量记录",
-                flowDeleted, alarmDeleted, equipDeleted, analogDeleted);
+            Log.Information("数据清理完成: 删除 {Flow} 条流量(>{FlowDays}d), {Alarm} 条报警(>{AlarmDays}d), {Equip} 条设备, {Analog} 条模拟量(>{AnalogDays}d)",
+                flowDeleted, flowRetention, alarmDeleted, alarmRetention, equipDeleted, analogDeleted, analogRetention);
         }
         catch (Exception ex)
         {

+ 4 - 0
src/YZWater.Core/Services/PlcPollingService.cs

@@ -135,6 +135,10 @@ public partial class PlcPollingService : ObservableObject, IDisposable, IService
                         {
                             reconnectAttempts++;
 
+                            // 立即标记数据无效,防止消费者基于过期数据做决策
+                            _data.IsPlcConnected = false;
+                            _data.IsDataValid = false;
+
                             // 进入或保持在降级模式
                             if (reconnectAttempts >= MaxReconnectAttempts)
                             {

+ 7 - 0
src/YZWater.Core/Services/WorkflowService.cs

@@ -275,6 +275,13 @@ public class WorkflowService : IDisposable
     /// </summary>
     private async void ControlPump(IDictionary<string, object>? context)
     {
+        // PLC 未连接时跳过泵控制,防止基于过期数据误操作
+        if (!PlcPollingService.Instance.Data.HasValidData)
+        {
+            Log.Debug("    PLC 未连接,跳过泵控制");
+            return;
+        }
+
         var requiredCount = 0;
         if (context != null && context.TryGetValue("RequiredPumpCount", out var obj))
             requiredCount = Convert.ToInt32(obj);