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 { /// /// 采集分析引擎。 /// 在每次采集周期后,对比每台机床的当前数据与上一次采集数据, /// 生成分析记录(log_collect_analysis)和周期汇总(log_collect_cycle)。 /// 异常类分析自动写入告警表(cnc_alert)。 /// public class AnalysisEngine { private static readonly ILog _log = LogManager.GetLogger(typeof(AnalysisEngine)); /// 业务库连接字符串(写告警用) private readonly string _businessConnStr; /// 日志库连接字符串(写分析/周期表用) private readonly string _logConnStr; /// 内存缓存:machineId → 上一次采集状态快照 private readonly ConcurrentDictionary _lastSnapshot = new ConcurrentDictionary(); /// /// 采集缓存快照(每台机床的上一次状态) /// public class MachineSnapshot { public string ProgramName { get; set; } public decimal? PartCount { get; set; } public string DeviceStatus { get; set; } public DateTime CollectTime { get; set; } } /// /// 初始化分析引擎 /// /// 业务库连接字符串 /// 日志库连接字符串 public AnalysisEngine(string businessConnStr, string logConnStr) { _businessConnStr = businessConnStr ?? throw new ArgumentNullException(nameof(businessConnStr)); _logConnStr = logConnStr ?? throw new ArgumentNullException(nameof(logConnStr)); } /// /// 分析一次采集周期的所有设备数据,写入分析记录和周期汇总。 /// /// 本次原始日志ID(log_collect_raw.id) /// 采集地址ID /// 采集地址名称 /// 本次采集的结构化记录列表 /// device_code → Machine 的查找字典 /// 周期开始时间 /// 本次采集耗时(毫秒) public void AnalyzeAndRecord(long rawLogId, int collectAddressId, string addressName, List records, Dictionary machineDict, DateTime cycleStartTime, long durationMs) { if (records == null || records.Count == 0) return; try { var analysisTime = DateTime.Now; var hasAnomaly = false; var changeDistribution = new Dictionary(); int successCount = 0; // 构建 machineId → Machine 查找字典 var machineById = new Dictionary(); 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); } } /// /// 根据前后状态对比,确定分析类型 /// 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}数据无重大变化"; } /// /// 写入单条分析记录到 log_collect_analysis /// 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); } } /// /// 写入周期汇总到 log_collect_cycle /// 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); } } /// /// 写入告警到 cnc_alert(业务库) /// 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); } } } }