From 8f124fb08df5b6f8913e923f9388b8cae25766f7 Mon Sep 17 00:00:00 2001 From: hsc Date: Fri, 26 Jun 2026 16:07:22 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9B=91=E6=8E=A7=E6=95=B0=E6=8D=AE=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ADP/App.xaml.cs | 10 +- Model/Entity/MonitorValueEntity.cs | 17 + Model/Models/AvailableMethodItem.cs | 21 + Model/Models/HardwareReportArgs.cs | 18 + MonitorModule/ViewModels/MonitorChannel.cs | 14 - MonitorModule/ViewModels/MonitorViewModel.cs | 383 ++++++++++++------ ORM/DatabaseConfig.cs | 2 +- Service/Implement/MonitorValueService.cs | 71 ++++ Service/Interface/IMonitorValueService.cs | 32 ++ UIShare/PubEvent/HardwareDataReportedEvent.cs | 13 + 10 files changed, 440 insertions(+), 141 deletions(-) create mode 100644 Model/Entity/MonitorValueEntity.cs create mode 100644 Model/Models/AvailableMethodItem.cs create mode 100644 Model/Models/HardwareReportArgs.cs create mode 100644 Service/Implement/MonitorValueService.cs create mode 100644 Service/Interface/IMonitorValueService.cs create mode 100644 UIShare/PubEvent/HardwareDataReportedEvent.cs diff --git a/ADP/App.xaml.cs b/ADP/App.xaml.cs index ad332b4..de0c3d7 100644 --- a/ADP/App.xaml.cs +++ b/ADP/App.xaml.cs @@ -64,10 +64,10 @@ namespace ADP LoggerHelper.Progress = new ScopeLogDispatcher(globalInfo); //初始化数据库 - //DatabaseConfig.SetTenant(10001); - //DatabaseConfig.InitMySql("127.0.0.1",3306,"ADP","root","123456"); - //DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true); - //SqlSugarContext.InitDatabase(); + DatabaseConfig.SetTenant(10001); + DatabaseConfig.InitSqlite(); + DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true); + SqlSugarContext.InitDatabase(); //显示登录窗口 var login = Container.Resolve(); var re = Container.Resolve(); @@ -100,6 +100,8 @@ namespace ADP containerRegistry.RegisterInstance(NotificationManager); // 注册仓储 containerRegistry.RegisterScoped(typeof(SqlSugarRepository<>)); + //注册服务 + containerRegistry.Register(); } //指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中) protected override IModuleCatalog CreateModuleCatalog() diff --git a/Model/Entity/MonitorValueEntity.cs b/Model/Entity/MonitorValueEntity.cs new file mode 100644 index 0000000..512aaa6 --- /dev/null +++ b/Model/Entity/MonitorValueEntity.cs @@ -0,0 +1,17 @@ +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Model.Entity +{ + public class MonitorValueEntity:BaseEntity + { + [SugarColumn(ColumnName = "MonitorName")] + public string MonitorName { get; set; } + [SugarColumn(ColumnName = "MonitorValue")] + public double MonitorValue { get; set; } + } +} diff --git a/Model/Models/AvailableMethodItem.cs b/Model/Models/AvailableMethodItem.cs new file mode 100644 index 0000000..edb4671 --- /dev/null +++ b/Model/Models/AvailableMethodItem.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Model.Models +{ + /// + /// 可用设备方法项(供用户选择添加为监测通道) + /// + public class AvailableMethodItem + { + public string DeviceName { get; set; } = string.Empty; + public string MethodName { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public MethodInfo MethodInfo { get; set; } = null!; + public object Device { get; set; } = null!; + } +} diff --git a/Model/Models/HardwareReportArgs.cs b/Model/Models/HardwareReportArgs.cs new file mode 100644 index 0000000..dd76ed5 --- /dev/null +++ b/Model/Models/HardwareReportArgs.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Model.Models +{ + public class HardwareReportArgs + { + public string HardwareFingerprint { get; set; } = string.Empty; + public string Key { get; set; } = string.Empty; + + public double Value { get; set; } + + public DateTime Time { get; set; } = DateTime.Now; + } +} diff --git a/MonitorModule/ViewModels/MonitorChannel.cs b/MonitorModule/ViewModels/MonitorChannel.cs index d2038ce..3887764 100644 --- a/MonitorModule/ViewModels/MonitorChannel.cs +++ b/MonitorModule/ViewModels/MonitorChannel.cs @@ -68,20 +68,6 @@ namespace MonitorModule.ViewModels public void Record(double time, double rawValue) { double displayValue = rawValue; - if (!string.IsNullOrWhiteSpace(MathExpression)) - { - try - { - var expr = new NCalc.Expression(MathExpression); - expr.Parameters["x"] = rawValue; - var result = expr.Evaluate(); - displayValue = Convert.ToDouble(result); - } - catch - { - displayValue = rawValue; - } - } DataPoints.Enqueue((time, rawValue, displayValue)); diff --git a/MonitorModule/ViewModels/MonitorViewModel.cs b/MonitorModule/ViewModels/MonitorViewModel.cs index 10a26f1..213c61e 100644 --- a/MonitorModule/ViewModels/MonitorViewModel.cs +++ b/MonitorModule/ViewModels/MonitorViewModel.cs @@ -1,26 +1,36 @@ -using OxyPlot; +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; -using Prism.Mvvm; -using Common.Attributes; namespace MonitorModule.ViewModels { public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable { #region 属性 + private IMonitorValueService _monitorValueService; public bool KeepAlive => true; - public ScopedContext _scopedContext { get; set; } + public ScopedContext _scopedContext { get; set; } = null!; public GlobalInfo _globalInfo { get; set; } + private string _testStatus = string.Empty; public string TestStatus { get => _testStatus; @@ -49,6 +59,7 @@ namespace MonitorModule.ViewModels set => SetProperty(ref _selectedAvailableMethod, value); } + private string _statusMessage = "图表已就绪,暂无监测项"; public string StatusMessage { get => _statusMessage; @@ -64,11 +75,13 @@ namespace MonitorModule.ViewModels public ICommand RefreshCommand { get; } #endregion - #region 私有字段 + #region 私有字段与并发缓存 private bool IsInitiated = false; - private IScopedProvider _scope; + private IScopedProvider? _scope; private DeviceManager? _deviceManager; + private CancellationTokenSource? _monitorCTS = new(); + // 颜色调色盘 private static readonly OxyColor[] _palette = { OxyColors.SteelBlue, OxyColors.IndianRed, OxyColors.SeaGreen, @@ -77,39 +90,271 @@ namespace MonitorModule.ViewModels }; private int _colorIndex; + // 图表采样定时器 (100ms) private readonly DispatcherTimer _sampleTimer; private double _sampleTime; - private const double _sampleInterval = 0.1; // 100ms - private string _testStatus = string.Empty; - private string _statusMessage = "图表已就绪,暂无监测项"; + private const double _sampleInterval = 0.1; /// 反射方法缓存:Channel → (MethodInfo, DeviceInstance) private readonly Dictionary _methodCache = new(); - /// 方法名前缀:匹配这些开头的公共方法被视为可监测项 - private static readonly string[] _methodPrefixes = { "查询", "读取", "获取", "测量" }; + // ========================================== + // 🟥 基于 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() { @@ -144,9 +389,6 @@ namespace MonitorModule.ViewModels #endregion #region 设备方法发现 - /// - /// 扫描 DeviceManager.DeviceMap 中所有设备,反射找出可监测的方法。 - /// private void DiscoverAvailableMethods() { AvailableMethods.Clear(); @@ -162,18 +404,13 @@ namespace MonitorModule.ViewModels var device = kvp.Value; var deviceType = device.GetType(); - // 核心优化:直接通过特性和参数过滤方法 var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance) .Where(m => { - // 1. 核心过滤:必须包含 [Monitorable] 特性 var attribute = m.GetCustomAttribute(); if (attribute == null) return false; - - // 2. 返回类型校验(如果强制要求是 Task) if (m.ReturnType != typeof(Task)) return false; - // 3. 参数校验:只取无参或仅有 CancellationToken 参数的方法 var parms = m.GetParameters(); bool validParams = parms.Length == 0 || (parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken)); @@ -183,7 +420,6 @@ namespace MonitorModule.ViewModels foreach (var method in methods) { - // 如果你在特性里写了 Description,这里可以拿出来赋给 DisplayName var attr = method.GetCustomAttribute(); string displayName = !string.IsNullOrEmpty(attr?.Description) ? $"{deviceName}.{attr.Description}" @@ -193,7 +429,7 @@ namespace MonitorModule.ViewModels { DeviceName = deviceName, MethodName = method.Name, - DisplayName = displayName, // 优先使用特性的中文描述 + DisplayName = displayName, MethodInfo = method, Device = device }; @@ -217,7 +453,6 @@ namespace MonitorModule.ViewModels return; } - // 防止重复添加同一设备方法 if (Channels.Any(c => c.DeviceName == method.DeviceName && c.MethodName == method.MethodName)) { StatusMessage = $"监测项 [{method.DisplayName}] 已存在"; @@ -237,7 +472,6 @@ namespace MonitorModule.ViewModels IsDisplayed = true }; - // 创建 Series 并加入 Plot channel.Series = new LineSeries { Title = channel.DisplayName, @@ -246,16 +480,12 @@ namespace MonitorModule.ViewModels }; Plot.Series.Add(channel.Series); - // 缓存反射信息 _methodCache[channel] = (method.MethodInfo, method.Device); - // 订阅属性变化:IsDisplayed 控制显示/隐藏,MathExpression 控制数学变换 channel.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(MonitorChannel.IsDisplayed)) OnChannelDisplayChanged(channel); - else if (e.PropertyName == nameof(MonitorChannel.MathExpression)) - OnChannelMathChanged(channel); }; Channels.Add(channel); @@ -274,10 +504,8 @@ namespace MonitorModule.ViewModels return; } - if (target.Series != null) - { - Plot.Series.Remove(target.Series); - } + if (target.Series != null) Plot.Series.Remove(target.Series); + _methodCache.Remove(target); Channels.Remove(target); SelectedChannel = Channels.LastOrDefault(); @@ -307,15 +535,11 @@ namespace MonitorModule.ViewModels } #endregion - #region 显示/隐藏切换 - /// - /// 当 Channel.IsDisplayed 变化时调用:控制 Series 的创建/移除 - /// + #region 显示/隐藏切换与数学变换 public void OnChannelDisplayChanged(MonitorChannel channel) { if (channel.IsDisplayed) { - // 从 false → true:创建 Series,回填最近数据 if (channel.Series == null) { channel.Series = new LineSeries @@ -325,7 +549,6 @@ namespace MonitorModule.ViewModels StrokeThickness = 1.5 }; - // 回填最近 200 个数据点 var recent = channel.DataPoints.ToArray(); var startIdx = Math.Max(0, recent.Length - 200); for (int i = startIdx; i < recent.Length; i++) @@ -339,7 +562,6 @@ namespace MonitorModule.ViewModels } else { - // 从 true → false:移除 Series,数据继续记录 if (channel.Series != null) { Plot.Series.Remove(channel.Series); @@ -349,14 +571,10 @@ namespace MonitorModule.ViewModels } } - /// - /// 当 Channel.MathExpression 变化时调用:重新计算所有已记录数据点的 DisplayValue - /// public void OnChannelMathChanged(MonitorChannel channel) { if (channel.Series == null) return; - // 用新表达式重算 Series 中的点 var points = channel.DataPoints.ToArray(); channel.Series.Points.Clear(); var startIdx = Math.Max(0, points.Length - 200); @@ -367,84 +585,5 @@ namespace MonitorModule.ViewModels Plot.InvalidatePlot(true); } #endregion - - #region 采样定时器 - private async void OnSampleTick(object? sender, EventArgs e) - { - _sampleTime += _sampleInterval; - - if (Channels.Count == 0 || _methodCache.Count == 0) return; - - foreach (var channel in Channels) - { - if (!channel.IsMonitored) continue; - if (!_methodCache.TryGetValue(channel, out var entry)) continue; - - try - { - var (method, device) = entry; - // 调用设备方法获取返回值 - var task = (Task)method.Invoke(device, null)!; - var raw = await task.ConfigureAwait(true); - - // 尝试解析为 double - if (double.TryParse(raw, out double value)) - { - channel.Record(_sampleTime, value); - } - } - catch - { - // 设备读取失败时跳过该通道本次采样 - } - } - - // 让 X 轴跟随最新数据滚动 - 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 重写 - 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; - - // 扫描可用监测项 - DiscoverAvailableMethods(); - - // 启动采样定时器 - _sampleTimer.Start(); - } - } - #endregion } - - /// - /// 可用设备方法项(供用户选择添加为监测通道) - /// - public class AvailableMethodItem - { - public string DeviceName { get; set; } = string.Empty; - public string MethodName { get; set; } = string.Empty; - public string DisplayName { get; set; } = string.Empty; - public MethodInfo MethodInfo { get; set; } = null!; - public object Device { get; set; } = null!; - } -} +} \ No newline at end of file diff --git a/ORM/DatabaseConfig.cs b/ORM/DatabaseConfig.cs index a790081..0fa07cc 100644 --- a/ORM/DatabaseConfig.cs +++ b/ORM/DatabaseConfig.cs @@ -46,7 +46,7 @@ namespace ORM // 拼接数据库文件路径 string DBPath = Path.Combine(folder, "SQL.db"); - DbConnectionString = $"Data Source={DBPath};Version=3;"; + DbConnectionString = $"Data Source={DBPath};"; } public static void InitMySql(string Server, int Port, string Database, string Uid,string Pwd) { diff --git a/Service/Implement/MonitorValueService.cs b/Service/Implement/MonitorValueService.cs new file mode 100644 index 0000000..ed63919 --- /dev/null +++ b/Service/Implement/MonitorValueService.cs @@ -0,0 +1,71 @@ +using Model; +using Model.Entity; +using ORM; +using Service.Interface; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Service.Implement +{ + /// + /// 监测数值记录服务实现 + /// + public class MonitorValueService : BaseService, IMonitorValueService + { + // 继承父类构造函数,注入 SqlSugar 仓储 + public MonitorValueService(SqlSugarRepository repository) : base(repository) + { + } + + /// + /// 批量插入监测数据 + /// + public async Task> InsertRangeAsync(List entities) + { + if (entities == null || entities.Count == 0) + return Result.Success(true); + + try + { + // 使用 SqlSugar 的批量插入,底层会转换为 BulkCopy 或是批量 SQL,速度极快 + var result = await _repository.Context.Insertable(entities).ExecuteCommandAsync(); + return Result.Success(result > 0); + } + catch (Exception ex) + { + return Result.Error("批量插入监测数据失败", ex); + } + } + + /// + /// 根据监测通道名称,分页查询历史记录 + /// + public async Task>> GetPagedByNameAsync(string monitorName, int pageIndex, int pageSize, RefAsync total) + { + try + { + var query = _repository.Entities; + + // 如果传了名字则按名字过滤 + if (!string.IsNullOrEmpty(monitorName)) + { + query = query.Where(x => x.MonitorName == monitorName); + } + + var list = await query + .OrderByDescending(d => d.CreateTime) // 监测项通常优先看最新的数据 + .ToPageListAsync(pageIndex, pageSize, total); + + // 计算总页数 + total.Value = (int)Math.Ceiling((double)total.Value / pageSize); + return Result>.Success(list); + } + catch (Exception ex) + { + return Result>.Error($"根据名称 [{monitorName}] 分页查询数据失败", ex); + } + } + } +} \ No newline at end of file diff --git a/Service/Interface/IMonitorValueService.cs b/Service/Interface/IMonitorValueService.cs new file mode 100644 index 0000000..6e3dc27 --- /dev/null +++ b/Service/Interface/IMonitorValueService.cs @@ -0,0 +1,32 @@ +using Model; +using Model.Entity; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Service.Interface +{ + /// + /// 监测数值记录服务接口 + /// + public interface IMonitorValueService : IBaseService + { + /// + /// 批量插入监测数据(工控高频采样推荐使用,性能远高于单条循环插入) + /// + /// 实体集合 + /// 返回操作是否成功的 Result + Task> InsertRangeAsync(List entities); + + /// + /// 根据监测通道名称,分页查询历史记录 + /// + /// 通道名称(例如:台架1.读取主轴温度) + /// 页码 + /// 每页大小 + /// 总条数输出 + /// 分页数据结果 + Task>> GetPagedByNameAsync(string monitorName, int pageIndex, int pageSize, RefAsync total); + } +} \ No newline at end of file diff --git a/UIShare/PubEvent/HardwareDataReportedEvent.cs b/UIShare/PubEvent/HardwareDataReportedEvent.cs new file mode 100644 index 0000000..bdce1ee --- /dev/null +++ b/UIShare/PubEvent/HardwareDataReportedEvent.cs @@ -0,0 +1,13 @@ +using Model.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace UIShare.PubEvent +{ + public class HardwareDataReportedEvent : PubSubEvent + { + } +}