|
|
#region << 版 本 注 释 >>
|
|
|
|
|
|
/*--------------------------------------------------------------------
|
|
|
* 版权所有 (c) 2025 WenJY 保留所有权利。
|
|
|
* CLR版本:4.0.30319.42000
|
|
|
* 机器名称:Mr.Wen's MacBook Pro
|
|
|
* 命名空间:Sln.Imm.Daemon.Business
|
|
|
* 唯一标识:A7D0F481-367C-4C8F-9E27-416639212E62
|
|
|
*
|
|
|
* 创建者:WenJY
|
|
|
* 电子邮箱:
|
|
|
* 创建时间:2025-12-19 09:57:36
|
|
|
* 版本:V1.0.0
|
|
|
* 描述:
|
|
|
*
|
|
|
*--------------------------------------------------------------------
|
|
|
* 修改人:
|
|
|
* 时间:
|
|
|
* 修改说明:
|
|
|
*
|
|
|
* 版本:V1.0.0
|
|
|
*--------------------------------------------------------------------*/
|
|
|
|
|
|
#endregion << 版 本 注 释 >>
|
|
|
|
|
|
using Dm.util;
|
|
|
using Models;
|
|
|
using Newtonsoft.Json;
|
|
|
using Sln.Imm.Daemon.Cache;
|
|
|
using Sln.Imm.Daemon.Model.dao;
|
|
|
using Sln.Imm.Daemon.Model.dto;
|
|
|
using Sln.Imm.Daemon.Opc;
|
|
|
using Sln.Imm.Daemon.Opc.Impl;
|
|
|
using Sln.Imm.Daemon.Repository.service;
|
|
|
using Sln.Imm.Daemon.Repository.service.@base;
|
|
|
using Sln.Imm.Daemon.Serilog;
|
|
|
using SqlSugar;
|
|
|
using System.Collections.Concurrent;
|
|
|
using System.Diagnostics;
|
|
|
using System.Linq;
|
|
|
using System.Threading;
|
|
|
|
|
|
namespace Sln.Imm.Daemon.Business;
|
|
|
|
|
|
public class DeviceDataCollector : IDisposable
|
|
|
{
|
|
|
|
|
|
private readonly SerilogHelper _serilog;
|
|
|
|
|
|
private readonly SemaphoreSlim _semaphore; // 并发控制:限制同时采集的设备数(避免资源耗尽)
|
|
|
|
|
|
private CancellationTokenSource _cts; // 取消令牌:用于终止所有采集任务
|
|
|
|
|
|
private readonly int _collectIntervalMs; // 循环采集间隔(毫秒)
|
|
|
|
|
|
private readonly int _collectAlarmMs; // 循环采集三色灯间隔(毫秒)
|
|
|
|
|
|
private bool _isCollecting; // 是否正在采集(防止重复启动)
|
|
|
|
|
|
private readonly BaseDeviceInfoCacheService _cacheService;
|
|
|
|
|
|
private readonly IBaseDeviceParamService _paramAlarmService;
|
|
|
|
|
|
private readonly IBaseService<BaseDeviceParamVal> _paramValService;
|
|
|
|
|
|
private readonly IBaseDeviceAlarmValService _alarmValService;
|
|
|
|
|
|
private readonly IBaseRecordShutDownService _shutDownValService;
|
|
|
|
|
|
// 在类级别添加这个字典来存储每个设备的上一次状态
|
|
|
private readonly ConcurrentDictionary<string, DeviceAlarmState> _deviceLastAlarmStates =
|
|
|
new ConcurrentDictionary<string, DeviceAlarmState>();
|
|
|
|
|
|
// 新增:存储每个设备Alarm触发的状态
|
|
|
private readonly ConcurrentDictionary<string, AlarmTriggerState> _deviceAlarmTriggerStates =
|
|
|
new ConcurrentDictionary<string, AlarmTriggerState>();
|
|
|
|
|
|
// 新增:存储每个设备停机触发的状态
|
|
|
private readonly ConcurrentDictionary<string, ShutDownTriggerState> _deviceShutDownTriggerStates =
|
|
|
new ConcurrentDictionary<string, ShutDownTriggerState>();
|
|
|
|
|
|
private Dictionary<string, bool> _deviceAlarmStatus = new Dictionary<string, bool>();
|
|
|
private static readonly TimeSpan DisconnectTimeout = TimeSpan.FromSeconds(5);
|
|
|
private const int AlarmDeviceIntervalMs = 5000; // 每台设备采集完成后,等待 5 秒再采集下一台
|
|
|
private const int AlarmRoundIntervalMs = 10000; // 每轮采集完成后,固定等待 10 秒再开始下一轮
|
|
|
|
|
|
public DeviceDataCollector(SerilogHelper serilogHelper,
|
|
|
BaseDeviceInfoCacheService cacheService,
|
|
|
IBaseService<BaseDeviceParamVal> paramValService,
|
|
|
IBaseDeviceParamService paramAlarmService,
|
|
|
IBaseDeviceAlarmValService alarmValService,
|
|
|
IBaseRecordShutDownService shutDownValService,
|
|
|
int maxConcurrentDevices = 15)
|
|
|
{
|
|
|
_serilog = serilogHelper;
|
|
|
_cacheService = cacheService;
|
|
|
_paramValService = paramValService;
|
|
|
_paramAlarmService = paramAlarmService;
|
|
|
_alarmValService = alarmValService;
|
|
|
_shutDownValService = shutDownValService;
|
|
|
|
|
|
_semaphore = new SemaphoreSlim(maxConcurrentDevices, maxConcurrentDevices);
|
|
|
|
|
|
_cts = new CancellationTokenSource();
|
|
|
_collectIntervalMs = 1000 * 60 *10 ; //设备数据采集时间间隔
|
|
|
_isCollecting = false;
|
|
|
}
|
|
|
|
|
|
#region 私有类
|
|
|
/// <summary>
|
|
|
/// 设备报警状态结构
|
|
|
/// </summary>
|
|
|
private class DeviceAlarmState
|
|
|
{
|
|
|
public bool LastRunning { get; set; }
|
|
|
public bool LastAlarm { get; set; }
|
|
|
public bool LastStopped { get; set; }
|
|
|
public DateTime LastUpdateTime { get; set; }
|
|
|
public bool IsFirstTime { get; set; } = true; // 标记是否为第一次采集
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// Alarm触发状态结构
|
|
|
/// </summary>
|
|
|
private class AlarmTriggerState
|
|
|
{
|
|
|
public bool LastAlarmValue { get; set; }
|
|
|
public long? AlarmRecordId { get; set; } // 存储插入的Alarm记录ID,用于后续更新
|
|
|
public DateTime LastTriggerTime { get; set; }
|
|
|
public bool IsAlarmActive { get; set; } // 标记当前是否有活跃的Alarm记录
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 停机触发状态结构
|
|
|
/// </summary>
|
|
|
private class ShutDownTriggerState
|
|
|
{
|
|
|
public bool LastStoppedValue { get; set; }
|
|
|
public DateTime LastTriggerTime { get; set; }
|
|
|
public bool IsShutDownActive { get; set; } // 标记当前是否有活跃的停机记录
|
|
|
}
|
|
|
#endregion
|
|
|
|
|
|
#region 循环采集
|
|
|
|
|
|
/// <summary>
|
|
|
/// 启动15设备并行循环采集
|
|
|
/// </summary>
|
|
|
/// <param name="devices">15台设备列表</param>
|
|
|
/// <param name="loopCount">循环次数(-1=无限)</param>
|
|
|
public async Task StartParallelLoopCollectAsync(int loopCount = -1)
|
|
|
{
|
|
|
if (_isCollecting)
|
|
|
{
|
|
|
Console.WriteLine($"[{DateTime.Now}] 并行采集已在运行,无需重复启动");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
_isCollecting = true;
|
|
|
int currentLoop = 0;
|
|
|
|
|
|
try
|
|
|
{
|
|
|
while (!_cts.Token.IsCancellationRequested)
|
|
|
{
|
|
|
// 达到指定循环次数退出
|
|
|
if (loopCount > 0 && currentLoop >= loopCount)
|
|
|
{
|
|
|
Console.WriteLine($"[{DateTime.Now}] 完成指定循环次数({loopCount}次),停止采集");
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
currentLoop++;
|
|
|
Console.WriteLine($"\n========== 第 {currentLoop} 轮并行采集开始 [{DateTime.Now}] ==========");
|
|
|
|
|
|
// 记录本轮开始时间(保证10秒间隔精准)
|
|
|
var roundStartTime = DateTime.Now;
|
|
|
|
|
|
// 核心:并行采集15台设备(真正同时启动)
|
|
|
var collectResult = await CollectDevicesInParallelAsync();
|
|
|
|
|
|
// 输出本轮结果
|
|
|
OutputCollectResult(collectResult);
|
|
|
|
|
|
// 计算耗时,补足10秒间隔
|
|
|
var roundCost = (DateTime.Now - roundStartTime).TotalMilliseconds;
|
|
|
var waitMs = _collectIntervalMs - roundCost;
|
|
|
|
|
|
if (waitMs > 0)
|
|
|
{
|
|
|
Console.WriteLine($"\n第 {currentLoop} 轮采集完成(耗时{roundCost:F0}ms),等待{waitMs:F0}ms后下一轮");
|
|
|
await Task.Delay((int)waitMs, _cts.Token);
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
Console.WriteLine($"\n第 {currentLoop} 轮采集超时(耗时{roundCost:F0}ms),立即开始下一轮");
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
catch (OperationCanceledException)
|
|
|
{
|
|
|
Console.WriteLine($"[{DateTime.Now}] 并行采集已手动终止");
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
Console.WriteLine($"[{DateTime.Now}] 并行采集异常:{ex.Message}", ex);
|
|
|
}
|
|
|
finally
|
|
|
{
|
|
|
_isCollecting = false;
|
|
|
Console.WriteLine($"[{DateTime.Now}] 并行采集循环结束");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 启动报警信息监控循环
|
|
|
/// </summary>
|
|
|
public async Task StartAlarmMonitorLoopSimpleAsync()
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
Console.WriteLine($"[{DateTime.Now}] 报警监控开始(每轮完成后固定等待 {AlarmRoundIntervalMs / 1000} 秒)");
|
|
|
|
|
|
while (!_cts.Token.IsCancellationRequested)
|
|
|
{
|
|
|
// 记录本轮开始时间
|
|
|
var startTime = DateTime.Now;
|
|
|
|
|
|
try
|
|
|
{
|
|
|
// 执行报警采集
|
|
|
await CollectDevicesAlarmAsync();
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
Console.WriteLine($"[报警监控] 采集异常: {ex.Message}");
|
|
|
_serilog.Error($"报警监控采集异常", ex);
|
|
|
}
|
|
|
|
|
|
// 计算耗时
|
|
|
var elapsedMs = (DateTime.Now - startTime).TotalMilliseconds;
|
|
|
|
|
|
// 固定间隔:每轮采集结束后等待 30 秒再开始下一轮(不再做“精准补齐间隔”)
|
|
|
Console.WriteLine($"[报警监控] 本轮耗时 {elapsedMs:F0}ms,等待 {AlarmRoundIntervalMs}ms 后下一轮");
|
|
|
await Task.Delay(AlarmRoundIntervalMs, _cts.Token);
|
|
|
}
|
|
|
}
|
|
|
catch (OperationCanceledException)
|
|
|
{
|
|
|
Console.WriteLine($"[{DateTime.Now}] 报警监控已停止");
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
Console.WriteLine($"[{DateTime.Now}] 报警监控异常:{ex.Message}");
|
|
|
_serilog.Error($"报警监控异常", ex);
|
|
|
}
|
|
|
finally
|
|
|
{
|
|
|
Console.WriteLine($"[{DateTime.Now}] 报警监控结束");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 输出采集结果
|
|
|
/// </summary>
|
|
|
private void OutputCollectResult(Dictionary<string, object> collectResult)
|
|
|
{
|
|
|
Console.WriteLine($"\n[{DateTime.Now}] 本轮采集结果汇总:");
|
|
|
foreach (var kvp in collectResult)
|
|
|
{
|
|
|
var deviceAddress = kvp.Key;
|
|
|
var result = kvp.Value;
|
|
|
|
|
|
if (result is Exception ex)
|
|
|
{
|
|
|
Console.WriteLine($" {deviceAddress}:失败 - {ex.Message}");
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
Console.WriteLine($" {deviceAddress}:成功 - {JsonConvert.SerializeObject(result)}");
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
public async Task<Dictionary<string, object>> CollectDevicesInParallelAsync()
|
|
|
{
|
|
|
var deviceInfos = await _cacheService.GetValueAsync("BaseDeviceInfoCache");
|
|
|
|
|
|
if (deviceInfos == null || deviceInfos.Count == 0)
|
|
|
throw new ArgumentException("设备列表不能为空", nameof(deviceInfos));
|
|
|
|
|
|
var resultDict = new Dictionary<string, object>();
|
|
|
var taskList = new List<Task>();
|
|
|
|
|
|
foreach (var item in deviceInfos)
|
|
|
{
|
|
|
// 每个设备创建独立的采集任务
|
|
|
taskList.Add(CollectSingleDeviceParallelAsync(item, resultDict, _cts.Token));
|
|
|
}
|
|
|
|
|
|
// 等待所有任务完成(无论成功/失败)
|
|
|
await Task.WhenAll(taskList);
|
|
|
|
|
|
return resultDict;
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 采集三色灯报警:顺序循环采集每台设备,每台采集完成后等待 10 秒再采集下一台;一轮完成后间隔时间不变。
|
|
|
/// </summary>
|
|
|
public async Task<Dictionary<string, object>> CollectDevicesAlarmAsync()
|
|
|
{
|
|
|
var deviceInfos = await _cacheService.GetValueAsync("BaseDeviceInfoCache");
|
|
|
|
|
|
if (deviceInfos == null || deviceInfos.Count == 0)
|
|
|
{
|
|
|
Console.WriteLine($"[报警监控] 无设备信息");
|
|
|
throw new ArgumentException("设备列表不能为空", nameof(deviceInfos));
|
|
|
}
|
|
|
|
|
|
Console.WriteLine($"[报警监控] 开始采集 {deviceInfos.Count} 台设备(顺序采集,每台间隔 {AlarmDeviceIntervalMs / 1000} 秒)");
|
|
|
var resultDict = new Dictionary<string, object>();
|
|
|
|
|
|
for (var i = 0; i < deviceInfos.Count; i++)
|
|
|
{
|
|
|
if (_cts.Token.IsCancellationRequested)
|
|
|
break;
|
|
|
|
|
|
var device = deviceInfos[i];
|
|
|
await CollectOneDeviceAlarmAsync(device, resultDict, _cts.Token);
|
|
|
|
|
|
// 每台设备采集完成后等待 10 秒再采集下一台(最后一台后不等待)
|
|
|
if (i < deviceInfos.Count - 1)
|
|
|
{
|
|
|
await Task.Delay(AlarmDeviceIntervalMs, _cts.Token);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
Console.WriteLine($"[报警监控] 本轮采集完成(共 {deviceInfos.Count} 台设备,结果数: {resultDict.Count})");
|
|
|
return resultDict;
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 顺序模式下单台设备报警采集(无信号量,由上层循环调用)。
|
|
|
/// </summary>
|
|
|
private async Task CollectOneDeviceAlarmAsync(
|
|
|
BaseDeviceInfo device,
|
|
|
Dictionary<string, object> resultDict,
|
|
|
CancellationToken cancellationToken)
|
|
|
{
|
|
|
IOpcService opcUa = null;
|
|
|
List<OpcNode> opcItemValues = null;
|
|
|
|
|
|
try
|
|
|
{
|
|
|
if (device.deviceFacture.Contains("伊之密"))
|
|
|
{
|
|
|
opcUa = new OpcUaService();
|
|
|
}
|
|
|
else if (device.deviceFacture.Contains("老设备"))
|
|
|
{
|
|
|
opcUa = new OpcDaService();
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
bool connectResult = await opcUa.ConnectAsync(device.networkAddress);
|
|
|
if (!connectResult)
|
|
|
{
|
|
|
Console.WriteLine($"报警信息 - [{DateTime.Now}] 设备 {device.deviceName} 连接失败");
|
|
|
resultDict[device.networkAddress] = new Exception($"设备 {device.deviceName} 连接失败");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
_serilog.Info($"报警信息开始采集{device.deviceName};");
|
|
|
|
|
|
opcItemValues = await ReadAlarm(device, opcUa);
|
|
|
|
|
|
if (opcItemValues == null || opcItemValues.Count == 0)
|
|
|
{
|
|
|
Console.WriteLine($"报警信息 - [{DateTime.Now}] {device.deviceName} - 未读取到报警点位");
|
|
|
resultDict[device.networkAddress] = opcItemValues ?? new List<OpcNode>();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (HasAlarmStateChanged(device, opcItemValues))
|
|
|
{
|
|
|
_serilog.Info($"{device.deviceName}三色灯状态改变");
|
|
|
try
|
|
|
{
|
|
|
SaveParam(device, opcItemValues, out List<BaseDeviceParamVal> paramValues);
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
_serilog.Error($"设备 {device.deviceName} 保存参数失败", ex);
|
|
|
}
|
|
|
}
|
|
|
//报警处理
|
|
|
try
|
|
|
{
|
|
|
int should = ShouldRecordAlarmTrue(device, opcItemValues);
|
|
|
if (should == 1)
|
|
|
{
|
|
|
_serilog.Info($"{device.deviceName}报警触发");
|
|
|
BaseDeviceAlarmVal paramVal = new BaseDeviceAlarmVal
|
|
|
{
|
|
|
DEVICE_ID = device.objid,
|
|
|
ALARM_BEGIN_TIME = DateTime.Now,
|
|
|
CONTINUE_TIME = 0
|
|
|
};
|
|
|
_alarmValService.Insert(paramVal);
|
|
|
}
|
|
|
else if (should == 2)
|
|
|
{
|
|
|
_serilog.Info($"{device.deviceName}报警恢复");
|
|
|
_alarmValService.UpdateAlarmVal(device.objid);
|
|
|
}
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
_serilog.Error($"设备 {device.deviceName} 报警记录操作失败", ex);
|
|
|
}
|
|
|
|
|
|
// 处理停机记录
|
|
|
try
|
|
|
{
|
|
|
int shutDownShould = ShouldRecordShutDownTrue(device, opcItemValues);
|
|
|
if (shutDownShould == 1)
|
|
|
{
|
|
|
_serilog.Info($"{device.deviceName}停机触发");
|
|
|
BaseRecordShutDown shutDownVal = new BaseRecordShutDown
|
|
|
{
|
|
|
MACHINE_ID = device.objid,
|
|
|
SHUT_TYPE_ID = 1,
|
|
|
SHUT_REASON_ID = 1,
|
|
|
SHUT_REASON = "R001",
|
|
|
SHUT_BEGIN_TIME = DateTime.Now,
|
|
|
SHUT_TIME = 0,
|
|
|
DOWNTIME_FLAG = "0",
|
|
|
ACTIVE_FLAG = "1"
|
|
|
};
|
|
|
_shutDownValService.Insert(shutDownVal);
|
|
|
}
|
|
|
else if (shutDownShould == 2)
|
|
|
{
|
|
|
_serilog.Info($"{device.deviceName}停机恢复");
|
|
|
_shutDownValService.UpdateShutDownVal(device.objid);
|
|
|
}
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
_serilog.Error($"设备 {device.deviceName} 停机记录操作失败", ex);
|
|
|
}
|
|
|
|
|
|
resultDict[device.networkAddress] = opcItemValues;
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
_serilog.Error($"设备 {device.deviceName} 三色灯采集异常", ex);
|
|
|
resultDict[device.networkAddress] = new Exception($"设备 {device.deviceName} 三色灯采集异常:{ex.Message}", ex);
|
|
|
}
|
|
|
finally
|
|
|
{
|
|
|
if (opcUa != null)
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
await SafeDisconnectAsync(opcUa, device.deviceName).ConfigureAwait(false);
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
_serilog.Error($"设备 {device.deviceName} 断开连接失败", ex);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private async Task CollectSingleDeviceParallelAsync(BaseDeviceInfo device, Dictionary<string, object> resultDict,
|
|
|
CancellationToken cancellationToken)
|
|
|
{
|
|
|
await _semaphore.WaitAsync(cancellationToken);
|
|
|
List<OpcNode> opcItemValues = null;
|
|
|
|
|
|
IOpcService opcUa = null;
|
|
|
try
|
|
|
{
|
|
|
if (device.deviceFacture.Contains("伊之密"))
|
|
|
{
|
|
|
opcUa =new OpcUaService();
|
|
|
}
|
|
|
else if(device.deviceFacture.Contains("老设备"))
|
|
|
{
|
|
|
opcUa =new OpcDaService();
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
Console.WriteLine($"[{DateTime.Now}] {device.deviceName} - 开始连接并采集");
|
|
|
bool connectResult = await opcUa.ConnectAsync(device.networkAddress);
|
|
|
if (!connectResult)
|
|
|
{
|
|
|
resultDict[device.networkAddress] = new Exception($"设备 {device.deviceName} 连接失败");
|
|
|
Console.WriteLine($"[{DateTime.Now}] 设备 {device.deviceName} 连接失败");
|
|
|
return;
|
|
|
}
|
|
|
Console.WriteLine($"[{DateTime.Now}] 设备 {device.deviceName} 连接成功");
|
|
|
|
|
|
// 2. 读取设备数据(你封装的ReadParam方法)
|
|
|
//Console.WriteLine($"[{DateTime.Now}] 开始读取设备 {device.deviceName} 数据");
|
|
|
_serilog.Info($"开始采集{device.deviceName};");
|
|
|
opcItemValues = await ReadParam(device, opcUa);
|
|
|
|
|
|
this.SaveParam(device, opcItemValues, out List<BaseDeviceParamVal> paramValues);
|
|
|
|
|
|
_serilog.Info($"{device.deviceName}数据采集完成:{JsonConvert.SerializeObject(opcItemValues)}");
|
|
|
|
|
|
//Console.WriteLine($"[{DateTime.Now}] 设备 {device.deviceName} 数据读取完成");
|
|
|
|
|
|
// 3. 存储读取结果
|
|
|
resultDict[device.networkAddress] = opcItemValues;
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
// 捕获采集过程中的异常
|
|
|
resultDict[device.networkAddress] = new Exception($"设备 {device.deviceName} 采集异常:{ex.Message}", ex);
|
|
|
Console.WriteLine($"[{DateTime.Now}] 设备 {device.deviceName} 采集异常:{ex.Message}");
|
|
|
}
|
|
|
finally
|
|
|
{
|
|
|
// 无论成功/失败,都断开设备连接(释放资源)
|
|
|
try
|
|
|
{
|
|
|
await SafeDisconnectAsync(opcUa, device.deviceName).ConfigureAwait(false);
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
Console.WriteLine($"[{DateTime.Now}] 设备 {device.deviceName} 断开连接失败:{ex.Message}");
|
|
|
}
|
|
|
|
|
|
// 释放信号量(允许下一个设备采集)
|
|
|
_semaphore.Release();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private static async Task SafeDisconnectAsync(IOpcService opcUa, string deviceName)
|
|
|
{
|
|
|
if (opcUa == null) return;
|
|
|
|
|
|
Console.WriteLine($"[{DateTime.Now}] 设备 {deviceName} 开始断开连接...");
|
|
|
|
|
|
var disconnectTask = opcUa.DisconnectAsync();
|
|
|
var finished = await Task.WhenAny(disconnectTask, Task.Delay(DisconnectTimeout)).ConfigureAwait(false);
|
|
|
|
|
|
if (finished != disconnectTask)
|
|
|
{
|
|
|
Console.WriteLine($"[{DateTime.Now}] 设备 {deviceName} 断开连接超时(>{DisconnectTimeout.TotalSeconds:F0}s),跳过等待以避免阻塞");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
await disconnectTask.ConfigureAwait(false);
|
|
|
Console.WriteLine($"[{DateTime.Now}] 设备 {deviceName} 已断开连接");
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
/// <summary>
|
|
|
/// 读取设备参数
|
|
|
/// </summary>
|
|
|
/// <param name="device"></param>
|
|
|
public async Task<List<OpcNode>> ReadParam(BaseDeviceInfo device,IOpcService opcUa)
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
if (device == null)
|
|
|
{
|
|
|
throw new ArgumentNullException($"设备信息不允许为空");
|
|
|
}
|
|
|
|
|
|
List<string> deviceParams = device.deviceParams.Select(x => x.paramAddr).ToList();
|
|
|
|
|
|
Stopwatch sw = Stopwatch.StartNew();
|
|
|
|
|
|
List<OpcNode> infos = await opcUa.ReadNodeAsync(deviceParams);
|
|
|
|
|
|
sw.Stop();
|
|
|
|
|
|
_serilog.Info($"opc读取耗时{sw.ElapsedMilliseconds.ToString()}");
|
|
|
|
|
|
return infos;
|
|
|
|
|
|
}
|
|
|
catch (Exception e)
|
|
|
{
|
|
|
throw new InvalidOperationException($"设备参数读取异常:{e.Message}");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 读取设备报警
|
|
|
/// </summary>
|
|
|
/// <param name="device"></param>
|
|
|
/// <param name="opcUa"></param>
|
|
|
/// <returns></returns>
|
|
|
public async Task<List<OpcNode>> ReadAlarm(BaseDeviceInfo device, IOpcService opcUa)
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
if (device == null)
|
|
|
{
|
|
|
throw new ArgumentNullException($"设备信息不允许为空");
|
|
|
}
|
|
|
if (opcUa == null)
|
|
|
{
|
|
|
throw new ArgumentNullException($"OPC服务不允许为空");
|
|
|
}
|
|
|
List<string> deviceParams = new List<string>();
|
|
|
List<BaseDeviceParam> paramList = _paramAlarmService.GetDeviceAlarmParams(device);
|
|
|
if (paramList == null || paramList.Count == 0)
|
|
|
return null;
|
|
|
|
|
|
foreach (BaseDeviceParam param in paramList)
|
|
|
{
|
|
|
if (string.IsNullOrEmpty(param?.paramAddr))
|
|
|
continue;
|
|
|
deviceParams.Add(param.paramAddr);
|
|
|
}
|
|
|
if (deviceParams.Count == 0)
|
|
|
return null;
|
|
|
|
|
|
List<OpcNode> infos = await opcUa.ReadNodeAsync(deviceParams);
|
|
|
return infos;
|
|
|
}
|
|
|
catch (Exception e)
|
|
|
{
|
|
|
throw new InvalidOperationException($"设备参数读取异常:{e.Message}", e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 保存设备参数值到数据库
|
|
|
/// </summary>
|
|
|
/// <param name="device">设备信息</param>
|
|
|
/// <param name="opcItemValues">OPC节点值列表</param>
|
|
|
/// <param name="paramValues">输出参数值DTO列表</param>
|
|
|
public void SaveParam(BaseDeviceInfo device, List<OpcNode> opcItemValues,
|
|
|
out List<BaseDeviceParamVal> paramValues)
|
|
|
{
|
|
|
paramValues = new List<BaseDeviceParamVal>();
|
|
|
try
|
|
|
{
|
|
|
foreach (OpcNode opcItem in opcItemValues)
|
|
|
{
|
|
|
BaseDeviceParamVal deviceParamVal = new BaseDeviceParamVal();
|
|
|
|
|
|
var paramInfo = device.deviceParams.Where(x => x.paramAddr == opcItem.NodeId).FirstOrDefault();
|
|
|
if (paramInfo != null)
|
|
|
{
|
|
|
deviceParamVal.paramCode = paramInfo.paramCode;
|
|
|
deviceParamVal.paramName = paramInfo.paramName;
|
|
|
}
|
|
|
deviceParamVal.deviceCode = device.deviceCode;
|
|
|
deviceParamVal.deviceId = device.objid;
|
|
|
if(opcItem.Value != null)
|
|
|
{
|
|
|
deviceParamVal.paramValue = opcItem.Value.ToString();
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
deviceParamVal.paramValue = "无";
|
|
|
}
|
|
|
|
|
|
deviceParamVal.paramType = opcItem.DataType;
|
|
|
deviceParamVal.collectTime = DateTime.Now;
|
|
|
deviceParamVal.recordTime = DateTime.Now;
|
|
|
|
|
|
paramValues.Add(deviceParamVal);
|
|
|
}
|
|
|
|
|
|
var isRes = _paramValService.Insert(paramValues);
|
|
|
if (isRes)
|
|
|
{
|
|
|
_serilog.Info(($"{device.deviceName} 设备参数保存成功"));
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
_serilog.Info(($"{device.deviceName} 设备参数保存失败"));
|
|
|
}
|
|
|
}catch (Exception e)
|
|
|
{
|
|
|
_serilog.Error($"DevicedataCollection报错", e);
|
|
|
throw new InvalidOperationException($"设备参数保存异常:{e.Message}");
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 检查报警状态是否有变化
|
|
|
/// </summary>
|
|
|
/// <param name="device"></param>
|
|
|
/// <param name="currentOpcItemValues"></param>
|
|
|
/// <returns></returns>
|
|
|
private bool HasAlarmStateChanged(BaseDeviceInfo device, List<OpcNode> currentOpcItemValues)
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
// 1. 从当前数据中提取三个关键报警点位
|
|
|
OpcNode runningNode = null;
|
|
|
OpcNode alarmNode = null;
|
|
|
OpcNode stoppedNode = null;
|
|
|
|
|
|
foreach (var node in currentOpcItemValues)
|
|
|
{
|
|
|
if (node.NodeId.Contains("Running", StringComparison.OrdinalIgnoreCase))
|
|
|
runningNode = node;
|
|
|
else if (node.NodeId.Contains("Alarm", StringComparison.OrdinalIgnoreCase))
|
|
|
alarmNode = node;
|
|
|
else if (node.NodeId.Contains("Stopped", StringComparison.OrdinalIgnoreCase))
|
|
|
stoppedNode = node;
|
|
|
}
|
|
|
|
|
|
// 如果三个点位没有全部找到,认为数据不完整,不进行处理
|
|
|
if (runningNode == null || alarmNode == null || stoppedNode == null)
|
|
|
{
|
|
|
Console.WriteLine($"报警信息 - [{DateTime.Now}] 设备 {device.deviceName} - 未找到完整的报警点位(Running/Alarm/Stopped)");
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
// 2. 解析当前状态值
|
|
|
bool currentRunning = GetBoolValue(runningNode.Value);
|
|
|
bool currentAlarm = GetBoolValue(alarmNode.Value);
|
|
|
bool currentStopped = GetBoolValue(stoppedNode.Value);
|
|
|
|
|
|
// 3. 获取或创建该设备的上一次状态记录
|
|
|
string deviceKey = device.objid.ToString(); // 使用设备ID作为字典键
|
|
|
bool hasChanged = false;
|
|
|
|
|
|
// 使用线程安全的方式获取或创建设备状态
|
|
|
_deviceLastAlarmStates.AddOrUpdate(deviceKey,
|
|
|
// 如果设备第一次采集,创建新状态记录
|
|
|
(key) =>
|
|
|
{
|
|
|
// 第一次采集,创建初始状态记录
|
|
|
var newState = new DeviceAlarmState
|
|
|
{
|
|
|
LastRunning = currentRunning,
|
|
|
LastAlarm = currentAlarm,
|
|
|
LastStopped = currentStopped,
|
|
|
LastUpdateTime = DateTime.Now,
|
|
|
IsFirstTime = false // 标记为已初始化
|
|
|
};
|
|
|
|
|
|
Console.WriteLine($"报警信息 - [{DateTime.Now}] 设备 {device.deviceName} - 首次采集,创建初始状态记录");
|
|
|
hasChanged = true; // 第一次采集总是保存
|
|
|
return newState;
|
|
|
},
|
|
|
// 如果设备已有状态记录,进行比较和更新
|
|
|
(key, existingState) =>
|
|
|
{
|
|
|
// 检查状态是否有变化
|
|
|
bool runningChanged = existingState.LastRunning != currentRunning;
|
|
|
bool alarmChanged = existingState.LastAlarm != currentAlarm;
|
|
|
bool stoppedChanged = existingState.LastStopped != currentStopped;
|
|
|
|
|
|
// 只要有一个状态发生变化,就认为有变化
|
|
|
hasChanged = runningChanged || alarmChanged || stoppedChanged;
|
|
|
|
|
|
if (hasChanged)
|
|
|
{
|
|
|
// 记录变化详情
|
|
|
Console.WriteLine($"报警信息 - [{DateTime.Now}] 设备 {device.deviceName} - 状态变化: " +
|
|
|
$"R[{existingState.LastRunning}->{currentRunning}], " +
|
|
|
$"A[{existingState.LastAlarm}->{currentAlarm}], " +
|
|
|
$"S[{existingState.LastStopped}->{currentStopped}]");
|
|
|
|
|
|
// 更新为最新状态
|
|
|
existingState.LastRunning = currentRunning;
|
|
|
existingState.LastAlarm = currentAlarm;
|
|
|
existingState.LastStopped = currentStopped;
|
|
|
existingState.LastUpdateTime = DateTime.Now;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
Console.WriteLine($"报警信息 - [{DateTime.Now}] 设备 {device.deviceName} - 状态无变化: " +
|
|
|
$"R[{currentRunning}], A[{currentAlarm}], S[{currentStopped}]");
|
|
|
}
|
|
|
|
|
|
return existingState;
|
|
|
});
|
|
|
|
|
|
return hasChanged;
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
Console.WriteLine($"报警信息 - [{DateTime.Now}] 设备 {device.deviceName} - 检查状态变化异常: {ex.Message}");
|
|
|
_serilog.Error($"设备 {device.deviceName} 检查状态变化异常", ex);
|
|
|
return false; // 出现异常时,不保存
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 检查是否需要记录Alarm为true的情况(新增逻辑)
|
|
|
/// </summary>
|
|
|
/// <param name="device">设备信息</param>
|
|
|
/// <param name="currentOpcItemValues">当前采集的OPC点位值</param>
|
|
|
/// <returns>true表示需要记录Alarm为true,false表示不需要记录</returns>
|
|
|
private int ShouldRecordAlarmTrue(BaseDeviceInfo device, List<OpcNode> currentOpcItemValues)
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
// 1. 从当前数据中提取Alarm点位
|
|
|
OpcNode alarmNode = null;
|
|
|
|
|
|
foreach (var node in currentOpcItemValues)
|
|
|
{
|
|
|
if (node.NodeId.Contains("Alarm", StringComparison.OrdinalIgnoreCase))
|
|
|
{
|
|
|
alarmNode = node;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果Alarm点位没有找到,不进行处理
|
|
|
if (alarmNode == null)
|
|
|
{
|
|
|
Console.WriteLine($"Alarm记录 - [{DateTime.Now}] 设备 {device.deviceName} - 未找到Alarm点位");
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
// 2. 解析当前Alarm状态值
|
|
|
bool currentAlarm = GetBoolValue(alarmNode.Value);
|
|
|
|
|
|
// 3. 获取设备当前的Alarm触发状态
|
|
|
string deviceKey = device.objid.ToString();
|
|
|
|
|
|
// 使用线程安全的方式获取或创建设备Alarm触发状态
|
|
|
int operationType = 0; // 0-无操作,1-插入,2-更新
|
|
|
|
|
|
_deviceAlarmTriggerStates.AddOrUpdate(deviceKey,
|
|
|
// 如果设备第一次检测到Alarm,创建新状态记录
|
|
|
(key) =>
|
|
|
{
|
|
|
// 第一次检测到Alarm,创建初始状态记录
|
|
|
var newState = new AlarmTriggerState
|
|
|
{
|
|
|
LastAlarmValue = currentAlarm,
|
|
|
AlarmRecordId = null,
|
|
|
LastTriggerTime = DateTime.Now,
|
|
|
IsAlarmActive = false
|
|
|
};
|
|
|
|
|
|
// 如果是第一次采集且Alarm为true,需要插入记录
|
|
|
if (currentAlarm)
|
|
|
{
|
|
|
Console.WriteLine($"Alarm记录 - [{DateTime.Now}] 设备 {device.deviceName} - 首次检测到Alarm为true,需要插入记录");
|
|
|
operationType = 1; // 需要插入记录
|
|
|
newState.IsAlarmActive = true; // 标记有活跃的Alarm记录
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
Console.WriteLine($"Alarm记录 - [{DateTime.Now}] 设备 {device.deviceName} - 首次检测到Alarm为false,无需操作");
|
|
|
}
|
|
|
|
|
|
return newState;
|
|
|
},
|
|
|
// 如果设备已有Alarm触发状态记录,进行比较
|
|
|
(key, existingState) =>
|
|
|
{
|
|
|
// 检查状态变化
|
|
|
if (existingState.LastAlarmValue == false && currentAlarm == true)
|
|
|
{
|
|
|
// 1: Alarm从false变为true,需要插入记录
|
|
|
operationType = 1; // 需要插入记录
|
|
|
existingState.IsAlarmActive = true; // 标记有活跃的Alarm记录
|
|
|
existingState.AlarmRecordId = null; // 重置记录ID,将在插入后设置
|
|
|
}
|
|
|
else if (existingState.LastAlarmValue == true && currentAlarm == false)
|
|
|
{
|
|
|
// 2: Alarm从true变为false,需要更新记录
|
|
|
if (existingState.IsAlarmActive)
|
|
|
{
|
|
|
operationType = 2; // 需要更新记录
|
|
|
existingState.IsAlarmActive = false; // 标记Alarm记录已结束
|
|
|
}
|
|
|
}
|
|
|
else if (existingState.LastAlarmValue == true && currentAlarm == true)
|
|
|
{
|
|
|
// 3.Alarm持续为true,忽略
|
|
|
operationType = 0;
|
|
|
}
|
|
|
else // existingState.LastAlarmValue == false && currentAlarm == false
|
|
|
{
|
|
|
// 4.Alarm持续为false,忽略
|
|
|
operationType = 0;
|
|
|
}
|
|
|
|
|
|
// 更新状态
|
|
|
existingState.LastAlarmValue = currentAlarm;
|
|
|
existingState.LastTriggerTime = DateTime.Now;
|
|
|
|
|
|
return existingState;
|
|
|
});
|
|
|
|
|
|
return operationType;
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
Console.WriteLine($"Alarm记录 - [{DateTime.Now}] 设备 {device.deviceName} - 检查Alarm记录异常: {ex.Message}");
|
|
|
_serilog.Error($"设备 {device.deviceName} 检查Alarm记录异常", ex);
|
|
|
return 0; // 出现异常时,不操作
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 检查是否需要记录停机为true的情况
|
|
|
/// </summary>
|
|
|
/// <param name="device">设备信息</param>
|
|
|
/// <param name="currentOpcItemValues">当前采集的OPC点位值</param>
|
|
|
/// <returns>0-无操作,1-插入,2-更新</returns>
|
|
|
private int ShouldRecordShutDownTrue(BaseDeviceInfo device, List<OpcNode> currentOpcItemValues)
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
// 1. 从当前数据中提取Stopped点位
|
|
|
OpcNode stoppedNode = null;
|
|
|
|
|
|
foreach (var node in currentOpcItemValues)
|
|
|
{
|
|
|
if (node.NodeId.Contains("Stopped", StringComparison.OrdinalIgnoreCase))
|
|
|
{
|
|
|
stoppedNode = node;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果Stopped点位没有找到,不进行处理
|
|
|
if (stoppedNode == null)
|
|
|
{
|
|
|
Console.WriteLine($"停机记录 - [{DateTime.Now}] 设备 {device.deviceName} - 未找到Stopped点位");
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
// 2. 解析当前停机状态值
|
|
|
bool currentStopped = GetBoolValue(stoppedNode.Value);
|
|
|
|
|
|
// 3. 获取设备当前的停机触发状态
|
|
|
string deviceKey = device.objid.ToString();
|
|
|
|
|
|
// 使用线程安全的方式获取或创建设备停机触发状态
|
|
|
int operationType = 0; // 0-无操作,1-插入,2-更新
|
|
|
|
|
|
_deviceShutDownTriggerStates.AddOrUpdate(deviceKey,
|
|
|
// 如果设备第一次检测到停机,创建新状态记录
|
|
|
(key) =>
|
|
|
{
|
|
|
// 第一次检测到停机,创建初始状态记录
|
|
|
var newState = new ShutDownTriggerState
|
|
|
{
|
|
|
LastStoppedValue = currentStopped,
|
|
|
LastTriggerTime = DateTime.Now,
|
|
|
IsShutDownActive = false
|
|
|
};
|
|
|
|
|
|
// 如果是第一次采集且停机为true,需要插入记录
|
|
|
if (currentStopped)
|
|
|
{
|
|
|
Console.WriteLine($"停机记录 - [{DateTime.Now}] 设备 {device.deviceName} - 首次检测到停机为true,需要插入记录");
|
|
|
operationType = 1; // 需要插入记录
|
|
|
newState.IsShutDownActive = true; // 标记有活跃的停机记录
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
Console.WriteLine($"停机记录 - [{DateTime.Now}] 设备 {device.deviceName} - 首次检测到停机为false,无需操作");
|
|
|
}
|
|
|
|
|
|
return newState;
|
|
|
},
|
|
|
// 如果设备已有停机触发状态记录,进行比较
|
|
|
(key, existingState) =>
|
|
|
{
|
|
|
// 检查状态变化
|
|
|
if (existingState.LastStoppedValue == false && currentStopped == true)
|
|
|
{
|
|
|
// 1: 停机从false变为true,需要插入记录
|
|
|
operationType = 1; // 需要插入记录
|
|
|
existingState.IsShutDownActive = true; // 标记有活跃的停机记录
|
|
|
}
|
|
|
else if (existingState.LastStoppedValue == true && currentStopped == false)
|
|
|
{
|
|
|
// 2: 停机从true变为false,需要更新记录
|
|
|
if (existingState.IsShutDownActive)
|
|
|
{
|
|
|
operationType = 2; // 需要更新记录
|
|
|
existingState.IsShutDownActive = false; // 标记停机记录已结束
|
|
|
}
|
|
|
}
|
|
|
else if (existingState.LastStoppedValue == true && currentStopped == true)
|
|
|
{
|
|
|
// 3. 停机持续为true,忽略
|
|
|
operationType = 0;
|
|
|
}
|
|
|
else // existingState.LastStoppedValue == false && currentStopped == false
|
|
|
{
|
|
|
// 4. 停机持续为false,忽略
|
|
|
operationType = 0;
|
|
|
}
|
|
|
|
|
|
// 更新状态
|
|
|
existingState.LastStoppedValue = currentStopped;
|
|
|
existingState.LastTriggerTime = DateTime.Now;
|
|
|
|
|
|
return existingState;
|
|
|
});
|
|
|
|
|
|
return operationType;
|
|
|
}
|
|
|
catch (Exception ex)
|
|
|
{
|
|
|
Console.WriteLine($"停机记录 - [{DateTime.Now}] 设备 {device.deviceName} - 检查停机记录异常: {ex.Message}");
|
|
|
_serilog.Error($"设备 {device.deviceName} 检查停机记录异常", ex);
|
|
|
return 0; // 出现异常时,不操作
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 安全获取bool值
|
|
|
/// </summary>
|
|
|
/// <param name="value"></param>
|
|
|
/// <returns></returns>
|
|
|
private bool GetBoolValue(object value)
|
|
|
{
|
|
|
if (value == null) return false;
|
|
|
|
|
|
try
|
|
|
{
|
|
|
if (value is bool boolValue)
|
|
|
return boolValue;
|
|
|
|
|
|
if (value is string stringValue)
|
|
|
{
|
|
|
if (bool.TryParse(stringValue, out bool result))
|
|
|
return result;
|
|
|
|
|
|
// 尝试其他常见表示方式
|
|
|
if (stringValue == "1" || stringValue.Equals("true", StringComparison.OrdinalIgnoreCase))
|
|
|
return true;
|
|
|
if (stringValue == "0" || stringValue.Equals("false", StringComparison.OrdinalIgnoreCase))
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
// 尝试转换
|
|
|
return Convert.ToBoolean(value);
|
|
|
}
|
|
|
catch
|
|
|
{
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
/// 终止所有采集任务
|
|
|
/// </summary>
|
|
|
public void CancelAllCollectTasks()
|
|
|
{
|
|
|
if (!_cts.IsCancellationRequested)
|
|
|
{
|
|
|
_cts.Cancel();
|
|
|
Console.WriteLine($"[{DateTime.Now}] 已触发采集任务终止");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 计算两个时间之间的持续时间(分钟),四舍五入取整
|
|
|
/// </summary>
|
|
|
/// <param name="startTime">开始时间</param>
|
|
|
/// <param name="endTime">结束时间</param>
|
|
|
/// <returns>持续时间的总分钟数(int类型,四舍五入)</returns>
|
|
|
public int CalculateDurationMinutesRounded(DateTime startTime, DateTime endTime)
|
|
|
{
|
|
|
// 验证参数有效性
|
|
|
if (endTime < startTime)
|
|
|
{
|
|
|
// 这里可以根据需求处理,比如返回0或抛出异常
|
|
|
return 0;
|
|
|
// 或者抛出异常:throw new ArgumentException("结束时间不能早于开始时间");
|
|
|
}
|
|
|
|
|
|
// 计算时间差
|
|
|
TimeSpan duration = endTime - startTime;
|
|
|
|
|
|
// 获取总分钟数(double类型),然后四舍五入转换为int
|
|
|
return (int)Math.Round(duration.TotalMinutes);
|
|
|
}
|
|
|
|
|
|
public void Dispose()
|
|
|
{
|
|
|
throw new NotImplementedException();
|
|
|
}
|
|
|
} |