using Common.Attributes;
using Model.Entity;
using Model.Models;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
using Service.Interface;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Threading;
using UIShare.GlobalVariable;
using UIShare.PubEvent;
using UIShare.ViewModelBase;
namespace MonitorModule.ViewModels
{
public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
{
#region 属性
private IMonitorValueService _monitorValueService;
public bool KeepAlive => true;
public ScopedContext _scopedContext { get; set; } = null!;
public GlobalInfo _globalInfo { get; set; }
private string _testStatus = string.Empty;
public string TestStatus
{
get => _testStatus;
set => SetProperty(ref _testStatus, value);
}
public PlotModel Plot { get; }
/// 已添加的监测通道列表
public ObservableCollection Channels { get; } = new();
/// 可添加的设备方法列表(供用户选择)
public ObservableCollection AvailableMethods { get; } = new();
private MonitorChannel? _selectedChannel;
public MonitorChannel? SelectedChannel
{
get => _selectedChannel;
set => SetProperty(ref _selectedChannel, value);
}
private AvailableMethodItem? _selectedAvailableMethod;
public AvailableMethodItem? SelectedAvailableMethod
{
get => _selectedAvailableMethod;
set => SetProperty(ref _selectedAvailableMethod, value);
}
private string _statusMessage = "图表已就绪,暂无监测项";
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
#endregion
#region 命令
public ICommand AddChannelCommand { get; }
public ICommand DeleteChannelCommand { get; }
public ICommand ResetViewCommand { get; }
public ICommand RefreshDataCommand { get; }
public ICommand RefreshCommand { get; }
#endregion
#region 私有字段与并发缓存
private bool IsInitiated = false;
private IScopedProvider? _scope;
private DeviceManager? _deviceManager;
private CancellationTokenSource? _monitorCTS = new();
// 颜色调色盘
private static readonly OxyColor[] _palette =
{
OxyColors.SteelBlue, OxyColors.IndianRed, OxyColors.SeaGreen,
OxyColors.DarkOrange, OxyColors.MediumPurple, OxyColors.Goldenrod,
OxyColors.Teal, OxyColors.Crimson, OxyColors.OliveDrab
};
private int _colorIndex;
// 图表采样定时器 (100ms)
private readonly DispatcherTimer _sampleTimer;
private double _sampleTime;
private const double _sampleInterval = 0.1;
/// 反射方法缓存:Channel → (MethodInfo, DeviceInstance)
private readonly Dictionary _methodCache = new();
// ==========================================
// 🟥 基于 Task.Run 批量入库的核心高并发结构
// ==========================================
/// 高并发无锁无阻塞队列
private readonly ConcurrentQueue _insertQueue = new();
/// 常驻后台的数据库消费任务
private Task? _dbFlushTask;
/// 定量触发阈值
private const int BulkInsertThreshold = 50;
/// 防并发消费锁标志
private int _isFlushing = 0;
#endregion
public MonitorViewModel(IContainerExtension container) : base(container)
{
_globalInfo = container.Resolve();
_monitorValueService = container.Resolve();
Plot = BuildEmptyPlot();
AddChannelCommand = new DelegateCommand(OnAddChannel);
DeleteChannelCommand = new DelegateCommand(OnDeleteChannel);
ResetViewCommand = new DelegateCommand(OnResetView);
RefreshDataCommand = new DelegateCommand(OnRefreshData);
RefreshCommand = new DelegateCommand(OnExpand);
// 前端采样定时器保持 100ms
_sampleTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(_sampleInterval) };
_sampleTimer.Tick += OnSampleTick;
}
///
/// 页面销毁与清理
///
public void Dispose()
{
// 1. 率先掐断所有底层的异步信号(引发 Task.Delay 抛出异常安全退出常驻循环)
_monitorCTS?.Cancel();
_monitorCTS?.Dispose();
_monitorCTS = null;
// 2. 停掉前端 UI 定时器
_sampleTimer?.Stop();
_sampleTimer.Tick -= OnSampleTick;
// 3. 🟥 等待后台入库消费 Task 彻底结束,最大安全等待 1.5 秒
if (_dbFlushTask != null)
{
try
{
_dbFlushTask.Wait(TimeSpan.FromSeconds(1.5));
}
catch { /* 忽略退出时的线程取消异常 */ }
}
// 4. 释放容器作用域及清空集合
_scope?.Dispose();
Channels.Clear();
AvailableMethods.Clear();
_methodCache.Clear();
}
#region 🟥 核心逻辑:基于 Task.Run 的常驻后台消费循环
///
/// 开启常驻后台的数据库消费线程
///
private void StartDbFlushWorker(CancellationToken token)
{
_dbFlushTask = Task.Run(async () =>
{
// 只要未触发取消信号,就一直在后台默默轮询
while (!token.IsCancellationRequested)
{
try
{
// 💡 核心策略一:定时兜底。每隔 1 秒强制检查一次队列。
// ConfigureAwait(false) 彻底丢弃 UI 上下文,拥抱线程池极致速度。
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
// 批量提取并刷入数据库
await DoFlushWorkAsync().ConfigureAwait(false);
}
catch (OperationCanceledException)
{
break; // 正常收到退出信号,跳出循环
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[DB Worker Error] 后台写入数据库异常: {ex.Message}");
}
}
// 💡 核心策略二:临终遗言。当页面被关闭后,把队列里剩余的所有漏网之鱼彻底一次性洗干净。
await DoFlushWorkAsync().ConfigureAwait(false);
}, token);
}
///
/// 纯粹的数据消费与批量 BulkCopy 入库
///
private async Task DoFlushWorkAsync()
{
if (_insertQueue.IsEmpty) return;
// CAS 原子自增锁,确保同一时间只有一个线程在向 SqlSugar 投递这批数据
if (Interlocked.CompareExchange(ref _isFlushing, 1, 0) != 0) return;
try
{
var listToInsert = new List();
// 一口气掏空当前并发队列里的所有实体
while (_insertQueue.TryDequeue(out var entity))
{
listToInsert.Add(entity);
}
if (listToInsert.Count > 0)
{
// 调用你的 Service 层的 SqlSugar 批量写入
var result = await _monitorValueService.InsertRangeAsync(listToInsert).ConfigureAwait(false);
if (!result.IsSuccess)
{
System.Diagnostics.Debug.WriteLine($"[DB Bulk Error] 批量入库失败: {result.Msg}");
}
}
}
finally
{
Interlocked.Exchange(ref _isFlushing, 0); // 释放锁
}
}
#endregion
#region 采样定时器与高并发处理(100ms)
private async void OnSampleTick(object? sender, EventArgs e)
{
_sampleTime += _sampleInterval;
if (Channels.Count == 0 || _methodCache.Count == 0) return;
var sampleTasks = new List();
var token = _monitorCTS?.Token ?? CancellationToken.None;
// 1. 并行发起所有通道的数据采集
foreach (var channel in Channels)
{
if (!channel.IsMonitored) continue;
if (!_methodCache.TryGetValue(channel, out var entry)) continue;
sampleTasks.Add(Task.Run(async () =>
{
try
{
var (method, device) = entry;
var parmsInfo = method.GetParameters();
object?[]? invokeArgs = parmsInfo.Length == 1 && parmsInfo[0].ParameterType == typeof(CancellationToken)
? new object[] { token }
: null;
if (method.Invoke(device, invokeArgs) is Task task)
{
// 硬件读取,彻底跑在后台线程池
string raw = await task.ConfigureAwait(false);
if (double.TryParse(raw, out double value))
{
// A. 更新本地图表数据缓存
channel.Record(_sampleTime, value);
// B. 封装实体
var entity = new MonitorValueEntity
{
MonitorName = channel.DisplayName,
MonitorValue = value,
CreateTime = DateTime.Now,
IsDel = 0
};
// C. 🟥 直接压入无锁队列,耗时极其微小,绝不卡硬件采集
_insertQueue.Enqueue(entity);
// D. 🟥 核心策略三:定量触发。如果采集数据爆棚,瞬间堆积超过50条,不等1秒定时器,直接开辟后台任务去写入
if (_insertQueue.Count >= BulkInsertThreshold)
{
_ = Task.Run(async () => await DoFlushWorkAsync().ConfigureAwait(false));
}
}
}
}
catch
{
// 某个硬件通道故障时不干扰其他通道
}
}, token));
}
// 2. 批量等待并发采样,加入 2 秒强制超时保护兜底
if (sampleTasks.Count > 0)
{
try
{
var delayTask = Task.Delay(TimeSpan.FromSeconds(2), token);
await Task.WhenAny(Task.WhenAll(sampleTasks), delayTask);
}
catch (OperationCanceledException)
{
return;
}
}
// 3. 【回归 UI 线程】执行 OxyPlot 坐标滚动与刷新
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
if (xAxis != null)
{
double window = 200 * _sampleInterval;
xAxis.Minimum = Math.Max(0, _sampleTime - window);
xAxis.Maximum = _sampleTime + 0.5;
}
Plot.InvalidatePlot(true);
}
#endregion
#region Navigation 导航重写
public override void OnNavigatedTo(NavigationContext navigationContext)
{
base.OnNavigatedTo(navigationContext);
if (!IsInitiated && navigationContext.Parameters.ContainsKey("Name"))
{
TestStatus = navigationContext.Parameters.GetValue("Name");
Plot.Title = $"监控 - {TestStatus}";
Plot.InvalidatePlot(false);
_scope = _globalInfo.ScopeDic[TestStatus];
_scopedContext = _scope.Resolve();
_deviceManager = _scope.Resolve();
IsInitiated = true;
if (_monitorCTS == null) _monitorCTS = new CancellationTokenSource();
// 🟥 1. 先把后台数据库消费专线拉起来
StartDbFlushWorker(_monitorCTS.Token);
// 2. 扫描可用硬件方法
DiscoverAvailableMethods();
// 3. 开启 100ms 硬件采集
_sampleTimer.Start();
}
}
#endregion
#region PlotModel 构建
private static PlotModel BuildEmptyPlot()
{
var pm = new PlotModel
{
Title = string.Empty,
PlotAreaBorderColor = OxyColors.LightGray,
Background = OxyColors.White
};
pm.Axes.Add(new LinearAxis
{
Position = AxisPosition.Bottom,
Title = "时间 (s)",
MajorGridlineStyle = LineStyle.Dot,
MinorGridlineStyle = LineStyle.None
});
pm.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = "值",
MajorGridlineStyle = LineStyle.Dot,
MinorGridlineStyle = LineStyle.None
});
pm.Legends.Add(new Legend
{
LegendPosition = LegendPosition.RightTop,
LegendBackground = OxyColor.FromAColor(200, OxyColors.White),
LegendBorder = OxyColors.LightGray
});
return pm;
}
#endregion
#region 设备方法发现
private void DiscoverAvailableMethods()
{
AvailableMethods.Clear();
if (_deviceManager?.DeviceMap == null || _deviceManager.DeviceMap.Count == 0)
{
StatusMessage = "当前工位无可用设备";
return;
}
foreach (var kvp in _deviceManager.DeviceMap)
{
string deviceName = kvp.Key;
var device = kvp.Value;
var deviceType = device.GetType();
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.Where(m =>
{
var attribute = m.GetCustomAttribute();
if (attribute == null) return false;
if (m.ReturnType != typeof(Task)) return false;
var parms = m.GetParameters();
bool validParams = parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
return validParams;
});
foreach (var method in methods)
{
var attr = method.GetCustomAttribute();
string displayName = !string.IsNullOrEmpty(attr?.Description)
? $"{deviceName}.{attr.Description}"
: $"{deviceName}.{method.Name}";
var item = new AvailableMethodItem
{
DeviceName = deviceName,
MethodName = method.Name,
DisplayName = displayName,
MethodInfo = method,
Device = device
};
AvailableMethods.Add(item);
}
}
StatusMessage = AvailableMethods.Count > 0
? $"发现 {AvailableMethods.Count} 个可监测项"
: "未发现可监测的设备方法";
}
#endregion
#region 命令处理
private void OnAddChannel()
{
var method = SelectedAvailableMethod;
if (method == null)
{
StatusMessage = "请先在列表中选择一个监测项";
return;
}
if (Channels.Any(c => c.DeviceName == method.DeviceName && c.MethodName == method.MethodName))
{
StatusMessage = $"监测项 [{method.DisplayName}] 已存在";
return;
}
var color = _palette[_colorIndex % _palette.Length];
_colorIndex++;
var channel = new MonitorChannel
{
DeviceName = method.DeviceName,
MethodName = method.MethodName,
DisplayName = method.DisplayName,
Color = color,
IsMonitored = true,
IsDisplayed = true
};
channel.Series = new LineSeries
{
Title = channel.DisplayName,
Color = color,
StrokeThickness = 1.5
};
Plot.Series.Add(channel.Series);
_methodCache[channel] = (method.MethodInfo, method.Device);
channel.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(MonitorChannel.IsDisplayed))
OnChannelDisplayChanged(channel);
};
Channels.Add(channel);
SelectedChannel = channel;
Plot.InvalidatePlot(true);
StatusMessage = $"已添加监测项 [{channel.DisplayName}],当前共 {Channels.Count} 项";
}
private void OnDeleteChannel()
{
var target = SelectedChannel ?? Channels.LastOrDefault();
if (target == null)
{
StatusMessage = "无可删除的监测项";
return;
}
if (target.Series != null) Plot.Series.Remove(target.Series);
_methodCache.Remove(target);
Channels.Remove(target);
SelectedChannel = Channels.LastOrDefault();
Plot.InvalidatePlot(true);
StatusMessage = $"已删除监测项 [{target.DisplayName}],剩余 {Channels.Count} 项";
}
private void OnResetView()
{
Plot.ResetAllAxes();
Plot.InvalidatePlot(false);
StatusMessage = "视图已按数据范围复原";
}
private void OnRefreshData()
{
Plot.InvalidatePlot(true);
StatusMessage = $"已刷新({DateTime.Now:HH:mm:ss})";
}
private void OnExpand()
{
if (string.IsNullOrEmpty(TestStatus)) return;
_globalInfo.CurrentScope = TestStatus;
_eventAggregator.GetEvent().Publish(TestStatus);
}
#endregion
#region 显示/隐藏切换与数学变换
public void OnChannelDisplayChanged(MonitorChannel channel)
{
if (channel.IsDisplayed)
{
if (channel.Series == null)
{
channel.Series = new LineSeries
{
Title = channel.DisplayName,
Color = channel.Color,
StrokeThickness = 1.5
};
var recent = channel.DataPoints.ToArray();
var startIdx = Math.Max(0, recent.Length - 200);
for (int i = startIdx; i < recent.Length; i++)
{
channel.Series.Points.Add(new DataPoint(recent[i].Time, recent[i].DisplayValue));
}
Plot.Series.Add(channel.Series);
Plot.InvalidatePlot(true);
}
}
else
{
if (channel.Series != null)
{
Plot.Series.Remove(channel.Series);
channel.Series = null;
Plot.InvalidatePlot(true);
}
}
}
public void OnChannelMathChanged(MonitorChannel channel)
{
if (channel.Series == null) return;
var points = channel.DataPoints.ToArray();
channel.Series.Points.Clear();
var startIdx = Math.Max(0, points.Length - 200);
for (int i = startIdx; i < points.Length; i++)
{
channel.Series.Points.Add(new DataPoint(points[i].Time, points[i].DisplayValue));
}
Plot.InvalidatePlot(true);
}
#endregion
}
}