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);
}
}
}
}