You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
haoliang-net/src/CncCollector/Core/AnalysisEngine.cs

402 lines
18 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Dapper;
using MySqlConnector;
using Newtonsoft.Json;
using CncModels.Entity;
using log4net;
namespace CncCollector.Core
{
/// <summary>
/// 采集分析引擎。
/// 在每次采集周期后,对比每台机床的当前数据与上一次采集数据,
/// 生成分析记录log_collect_analysis和周期汇总log_collect_cycle
/// 异常类分析自动写入告警表cnc_alert
/// </summary>
public class AnalysisEngine
{
private static readonly ILog _log = LogManager.GetLogger(typeof(AnalysisEngine));
/// <summary>业务库连接字符串(写告警用)</summary>
private readonly string _businessConnStr;
/// <summary>日志库连接字符串(写分析/周期表用)</summary>
private readonly string _logConnStr;
/// <summary>内存缓存machineId → 上一次采集状态快照</summary>
private readonly ConcurrentDictionary<int, MachineSnapshot> _lastSnapshot = new ConcurrentDictionary<int, MachineSnapshot>();
/// <summary>
/// 采集缓存快照(每台机床的上一次状态)
/// </summary>
public class MachineSnapshot
{
public string ProgramName { get; set; }
public decimal? PartCount { get; set; }
public string DeviceStatus { get; set; }
public DateTime CollectTime { get; set; }
}
/// <summary>
/// 初始化分析引擎
/// </summary>
/// <param name="businessConnStr">业务库连接字符串</param>
/// <param name="logConnStr">日志库连接字符串</param>
public AnalysisEngine(string businessConnStr, string logConnStr)
{
_businessConnStr = businessConnStr ?? throw new ArgumentNullException(nameof(businessConnStr));
_logConnStr = logConnStr ?? throw new ArgumentNullException(nameof(logConnStr));
}
/// <summary>
/// 分析一次采集周期的所有设备数据,写入分析记录和周期汇总。
/// </summary>
/// <param name="rawLogId">本次原始日志IDlog_collect_raw.id</param>
/// <param name="collectAddressId">采集地址ID</param>
/// <param name="addressName">采集地址名称</param>
/// <param name="records">本次采集的结构化记录列表</param>
/// <param name="machineDict">device_code → Machine 的查找字典</param>
/// <param name="cycleStartTime">周期开始时间</param>
/// <param name="durationMs">本次采集耗时(毫秒)</param>
public void AnalyzeAndRecord(long rawLogId, int collectAddressId, string addressName,
List<CollectRecord> records, Dictionary<string, Machine> machineDict,
DateTime cycleStartTime, long durationMs)
{
if (records == null || records.Count == 0) return;
try
{
var analysisTime = DateTime.Now;
var hasAnomaly = false;
var changeDistribution = new Dictionary<string, int>();
int successCount = 0;
// 构建 machineId → Machine 查找字典
var machineById = new Dictionary<int, Machine>();
foreach (var m in machineDict.Values)
{
machineById[m.Id] = m;
}
// 逐条分析
foreach (var rec in records)
{
try
{
// 获取机床信息
Machine machine = null;
machineById.TryGetValue(rec.MachineId, out machine);
string machineName = machine?.Name ?? ("机床" + rec.MachineId);
// 当前值
string currentProgram = rec.ProgramName;
decimal? currentPartCount = rec.PartCount;
string currentStatus = rec.DeviceStatus;
// 获取上次快照
MachineSnapshot prev;
_lastSnapshot.TryGetValue(rec.MachineId, out prev);
// 计算分析类型和摘要
string analysisType;
string summary;
bool needAlert = false;
string alertType = null;
DetermineAnalysis(prev, currentProgram, currentPartCount, currentStatus,
machineName, out analysisType, out summary, out needAlert, out alertType);
// 计算变化量
decimal? partCountDelta = null;
if (currentPartCount.HasValue && prev != null && prev.PartCount.HasValue)
{
partCountDelta = currentPartCount.Value - prev.PartCount.Value;
}
// 构建分析明细JSON
var detail = new
{
previous = prev != null ? new
{
program = prev.ProgramName,
partCount = prev.PartCount,
status = prev.DeviceStatus
} : null,
current = new
{
program = currentProgram,
partCount = currentPartCount,
status = currentStatus
},
delta = new { partCount = partCountDelta },
collectTime = rec.CollectTime.ToString("yyyy-MM-dd HH:mm:ss")
};
string detailJson = JsonConvert.SerializeObject(detail);
// 写入分析记录
WriteAnalysis(new CncModels.Entity.CollectAnalysis
{
AnalysisTime = analysisTime,
RawLogId = rawLogId,
CollectAddressId = collectAddressId,
MachineId = rec.MachineId,
AnalysisType = analysisType,
PreviousProgram = prev?.ProgramName,
CurrentProgram = currentProgram,
PreviousPartCount = prev?.PartCount,
CurrentPartCount = currentPartCount,
PartCountDelta = partCountDelta,
PreviousStatus = prev?.DeviceStatus,
CurrentStatus = currentStatus,
AnalysisSummary = summary,
AnalysisDetail = detailJson
});
// 更新快照
_lastSnapshot[rec.MachineId] = new MachineSnapshot
{
ProgramName = currentProgram,
PartCount = currentPartCount,
DeviceStatus = currentStatus,
CollectTime = rec.CollectTime
};
// 统计分布
if (changeDistribution.ContainsKey(analysisType))
changeDistribution[analysisType]++;
else
changeDistribution[analysisType] = 1;
// 异常告警
if (needAlert)
{
hasAnomaly = true;
WriteAlert(alertType, rec.MachineId, collectAddressId, summary, detailJson);
}
// 统计成功数(非异常即为成功)
if (analysisType != "COLLECTION_FAILED" && analysisType != "DATA_ANOMALY")
successCount++;
}
catch (Exception ex)
{
_log.Error($"分析单条记录失败machineId={rec.MachineId}", ex);
}
}
// 写入周期汇总
WriteCycleSummary(new CncModels.Entity.CollectCycle
{
CycleTime = cycleStartTime,
CollectAddressId = collectAddressId,
RawLogId = rawLogId,
EndTime = analysisTime,
DurationMs = (int)durationMs,
TotalMachines = records.Count,
SuccessCount = successCount,
FailCount = records.Count - successCount,
ChangeDistribution = JsonConvert.SerializeObject(changeDistribution),
HasAnomaly = hasAnomaly ? 1 : 0,
CycleSummary = $"共{records.Count}台机床完成分析" + (hasAnomaly ? ",存在异常" : "")
});
}
catch (Exception ex)
{
_log.Error($"采集分析失败(地址={addressName}, rawLogId={rawLogId}", ex);
}
}
/// <summary>
/// 根据前后状态对比,确定分析类型
/// </summary>
private void DetermineAnalysis(MachineSnapshot prev, string currentProgram, decimal? currentPartCount,
string currentStatus, string machineName, out string analysisType, out string summary,
out bool needAlert, out string alertType)
{
needAlert = false;
alertType = null;
// 无历史快照 → 首次上线
if (prev == null)
{
analysisType = "DEVICE_ONLINE";
summary = $"机床{machineName}首次上线,程序={currentProgram ?? ""}";
needAlert = true;
alertType = "unknown_device";
return;
}
string prevProgram = prev.ProgramName;
decimal? prevPartCount = prev.PartCount;
string prevStatus = prev.DeviceStatus;
// 检测程序切换
if (!string.IsNullOrEmpty(currentProgram) && !string.IsNullOrEmpty(prevProgram) &&
!string.Equals(prevProgram, currentProgram, StringComparison.OrdinalIgnoreCase))
{
analysisType = "PROGRAM_SWITCH";
summary = $"机床{machineName}程序切换: {prevProgram} → {currentProgram}";
return;
}
// 检测手动清零(同程序下零件数下降)
if (currentPartCount.HasValue && prevPartCount.HasValue &&
currentPartCount.Value < prevPartCount.Value)
{
analysisType = "MANUAL_RESET";
summary = $"机床{machineName}零件计数手动清零: {prevPartCount} → {currentPartCount}";
return;
}
// 检测零件数增加
if (currentPartCount.HasValue && prevPartCount.HasValue &&
currentPartCount.Value > prevPartCount.Value)
{
decimal delta = currentPartCount.Value - prevPartCount.Value;
analysisType = "PART_COUNT_INCREASE";
summary = $"机床{machineName}新增{delta}个零件({prevPartCount} → {currentPartCount}";
return;
}
// 检测设备离线/告警
if (!string.IsNullOrEmpty(currentStatus) &&
(currentStatus.Equals("OFFLINE", StringComparison.OrdinalIgnoreCase) ||
currentStatus.Equals("ALARM", StringComparison.OrdinalIgnoreCase) ||
currentStatus.Equals("EMERGENCY", StringComparison.OrdinalIgnoreCase)))
{
analysisType = "DEVICE_OFFLINE";
summary = $"机床{machineName}设备离线/告警: {currentStatus}";
needAlert = true;
alertType = "device_offline";
return;
}
// 检测数据异常(关键字段缺失但设备应该在线)
if (string.IsNullOrEmpty(currentProgram) && !string.IsNullOrEmpty(currentStatus) &&
!currentStatus.Equals("OFFLINE", StringComparison.OrdinalIgnoreCase))
{
analysisType = "DATA_ANOMALY";
summary = $"机床{machineName}数据异常: 缺少程序名字段";
needAlert = true;
alertType = "data_anomaly";
return;
}
// 无重大变化
analysisType = "NORMAL_UNCHANGED";
summary = $"机床{machineName}数据无重大变化";
}
/// <summary>
/// 写入单条分析记录到 log_collect_analysis
/// </summary>
private void WriteAnalysis(CncModels.Entity.CollectAnalysis entity)
{
try
{
using (var conn = new MySqlConnection(_logConnStr))
{
conn.Execute(@"INSERT INTO log_collect_analysis
(analysis_time, raw_log_id, collect_address_id, machine_id, analysis_type,
previous_program, current_program, previous_part_count, current_part_count,
part_count_delta, previous_status, current_status, analysis_summary,
analysis_detail, created_at)
VALUES (@AnalysisTime, @RawLogId, @CollectAddressId, @MachineId, @AnalysisType,
@PreviousProgram, @CurrentProgram, @PreviousPartCount, @CurrentPartCount,
@PartCountDelta, @PreviousStatus, @CurrentStatus, @AnalysisSummary,
@AnalysisDetail, NOW())",
new
{
entity.AnalysisTime,
entity.RawLogId,
entity.CollectAddressId,
entity.MachineId,
entity.AnalysisType,
entity.PreviousProgram,
entity.CurrentProgram,
entity.PreviousPartCount,
entity.CurrentPartCount,
entity.PartCountDelta,
entity.PreviousStatus,
entity.CurrentStatus,
entity.AnalysisSummary,
entity.AnalysisDetail
});
}
}
catch (Exception ex)
{
_log.Error($"写入分析记录失败machineId={entity.MachineId}", ex);
}
}
/// <summary>
/// 写入周期汇总到 log_collect_cycle
/// </summary>
private void WriteCycleSummary(CncModels.Entity.CollectCycle entity)
{
try
{
using (var conn = new MySqlConnection(_logConnStr))
{
conn.Execute(@"INSERT INTO log_collect_cycle
(cycle_time, collect_address_id, raw_log_id, end_time, duration_ms,
total_machines, success_count, fail_count, change_distribution,
has_anomaly, cycle_summary, created_at)
VALUES (@CycleTime, @CollectAddressId, @RawLogId, @EndTime, @DurationMs,
@TotalMachines, @SuccessCount, @FailCount, @ChangeDistribution,
@HasAnomaly, @CycleSummary, NOW())",
new
{
entity.CycleTime,
entity.CollectAddressId,
entity.RawLogId,
entity.EndTime,
entity.DurationMs,
entity.TotalMachines,
entity.SuccessCount,
entity.FailCount,
entity.ChangeDistribution,
entity.HasAnomaly,
entity.CycleSummary
});
}
}
catch (Exception ex)
{
_log.Error("写入周期汇总失败", ex);
}
}
/// <summary>
/// 写入告警到 cnc_alert业务库
/// </summary>
private void WriteAlert(string alertType, int machineId, int collectAddressId, string title, string detail)
{
try
{
using (var conn = new MySqlConnection(_businessConnStr))
{
conn.Execute(@"INSERT INTO cnc_alert (alert_type, machine_id, collect_address_id, title, detail, is_resolved, created_at)
VALUES (@AlertType, @MachineId, @AddressId, @Title, @Detail, 0, NOW())",
new
{
AlertType = alertType,
MachineId = machineId,
AddressId = collectAddressId,
Title = title,
Detail = detail
});
}
}
catch (Exception ex)
{
_log.Error($"写入告警失败alertType={alertType}, machineId={machineId}", ex);
}
}
}
}