diff --git a/src/CncCollector/Core/ProductionTracker.cs b/src/CncCollector/Core/ProductionTracker.cs index 8304a9c..80dc6ec 100644 --- a/src/CncCollector/Core/ProductionTracker.cs +++ b/src/CncCollector/Core/ProductionTracker.cs @@ -1,17 +1,14 @@ using System; -using System.Collections.Concurrent; -using System.Collections.Generic; using Dapper; using MySqlConnector; -using CncModels.Entity; -using CncModels.Enum; using log4net; namespace CncCollector.Core { /// - /// 零件产量分段跟踪引擎。 - /// 为每台机床维护内存中的当前活跃段状态,检测程序切换和手动清零。 + /// 零件产量跟踪引擎。 + /// 基于数据库 cnc_daily_production 表对比加工零件总数(Tag1)计算当日产量。 + /// 记录不可变:每条记录创建后仅 end_total_count 可更新。 /// public class ProductionTracker : IDisposable { @@ -19,16 +16,6 @@ namespace CncCollector.Core private readonly string _connectionString; private readonly object _lock = new object(); - /// - /// 内存缓存:machineId → 当前活跃段ID(减少DB查询) - /// - private readonly ConcurrentDictionary _activeSegmentIds = new ConcurrentDictionary(); - - /// - /// 内存缓存:machineId → 上一次采集的 (programName, partCount) - /// - private readonly ConcurrentDictionary> _lastCollectState = new ConcurrentDictionary>(); - /// /// 初始化产量跟踪器 /// @@ -39,241 +26,213 @@ namespace CncCollector.Core } /// - /// 处理一次采集结果:检测程序切换/手动清零,管理活跃段 + /// 处理一次采集结果:基于数据库最新记录对比加工零件总数,计算当日产量。 /// /// 机床ID /// 当前NC程序名 - /// 当前零件数 + /// 加工零件总数(Tag1) /// 采集时间 - public void Track(int machineId, string programName, decimal? partCount, DateTime collectTime) + /// + /// logText:描述本次处理结果的文本 + /// changed:数据库是否发生了变更 + /// todayTotal:当前机床当日总产量 = SUM(end_total_count - base_total_count) + /// + public (string logText, bool changed, decimal todayTotal) Track( + int machineId, string programName, decimal? totalPartCount, DateTime collectTime) { - if (string.IsNullOrEmpty(programName)) return; + // 前置校验:程序名为空 或 零件总数为空 → 跳过 + if (string.IsNullOrEmpty(programName) || !totalPartCount.HasValue) + { + var skipReason = string.IsNullOrEmpty(programName) + ? "程序名为空" + : "加工零件总数为空"; + _log.Debug($"机床{machineId}: 跳过产量跟踪({skipReason})"); + return ($"跳过:{skipReason}", false, 0); + } lock (_lock) { try { - // 获取上次采集状态 - Tuple lastState; - bool hasLast = _lastCollectState.TryGetValue(machineId, out lastState); - - // 检测是否需要结账 - bool needClose = false; - string closeReason = ""; + string logText; + bool changed; - if (hasLast) + using (var conn = new MySqlConnection(_connectionString)) { - string lastProgram = lastState.Item1; - decimal? lastPartCount = lastState.Item2; + conn.Open(); + + // 查询当天最新一条记录 + var record = GetCurrentRecord(conn, machineId); - // 情况1:NC程序名变化 - if (!string.Equals(lastProgram, programName, StringComparison.OrdinalIgnoreCase)) + if (record == null) { - needClose = true; - closeReason = SegmentCloseReason.ProgramChange; - _log.Info($"机床{machineId}: 程序切换 {lastProgram} → {programName}({closeReason})"); + // 当天首次采集 → 创建记录 + InsertRecord(conn, machineId, programName, + totalPartCount.Value, totalPartCount.Value); + logText = "当天首次采集,创建记录"; + changed = true; } - // 情况2:同程序下 part_count 下降(手动清零) - else if (partCount.HasValue && lastPartCount.HasValue && partCount.Value < lastPartCount.Value) + else if (record.ProductionDate != DateTime.Today) { - needClose = true; - closeReason = SegmentCloseReason.ManualReset; - _log.Info($"机床{machineId}: 零件数下降 {lastPartCount} → {partCount}({closeReason})"); + // 防御性检查:跨天(理论上不会发生,因为查询条件限定了 CURDATE()) + InsertRecord(conn, machineId, programName, + totalPartCount.Value, totalPartCount.Value); + logText = "跨天首次采集,创建记录"; + changed = true; + } + else if (string.Equals(record.ProgramName, programName, StringComparison.OrdinalIgnoreCase)) + { + (logText, changed) = HandleSameProgram(conn, record, totalPartCount.Value); + } + else + { + (logText, changed) = HandleProgramChange(conn, record, programName, totalPartCount.Value); } - } - - // 结账当前活跃段 - // 修复:使用结账前最后已知的零件数(lastPartCount),而非当前观测值(partCount) - // 当 manual_reset 时 partCount 是复位后的值(如0),当 program_change 时 partCount 是新程序的值 - // 正确的 endPartCount 应该是结账段最后观测到的最大计数值 - if (needClose) - { - var closingEndCount = hasLast ? lastState.Item2 : partCount; - CloseActiveSegment(machineId, closingEndCount, closeReason, collectTime); - } - // 确保有活跃段 - long activeSegmentId = EnsureActiveSegment(machineId, programName, partCount, collectTime); + // 计算当日总产量 = 当天所有记录的 (end - base) 之和 + var todayTotal = conn.ExecuteScalar( + @"SELECT COALESCE(SUM(end_total_count - base_total_count), 0) + FROM cnc_daily_production + WHERE machine_id = @Mid AND production_date = CURDATE()", + new { Mid = machineId }); - // 更新活跃段的实时 end_part_count - if (activeSegmentId > 0 && partCount.HasValue) - { - UpdateSegmentEndCount(activeSegmentId, partCount.Value); + _log.Info($"机床{machineId}: {logText}(当日累计产量={todayTotal:F5})"); + return (logText, changed, todayTotal); } - - // 更新内存缓存 - _lastCollectState[machineId] = Tuple.Create(programName, partCount); } catch (Exception ex) { _log.Error($"产量跟踪处理失败(machine_id={machineId})", ex); - // 写入告警:产量跟踪失败意味着产量数据可能丢失 + + // 写入告警 try { using (var conn2 = new MySqlConnection(_connectionString)) { - conn2.Execute(@"INSERT INTO cnc_alert (alert_type, machine_id, title, detail, is_resolved, created_at) - VALUES (@Type, @Mid, @Title, @Detail, 0, NOW())", - new { Type = "production_error", Mid = machineId, Title = "产量跟踪处理异常", - Detail = $"机床{machineId}产量跟踪失败: {ex.Message}" }); + conn2.Execute( + @"INSERT INTO cnc_alert (alert_type, machine_id, title, detail, is_resolved, created_at) + VALUES (@Type, @Mid, @Title, @Detail, 0, NOW())", + new + { + Type = "production_error", + Mid = machineId, + Title = "产量跟踪处理异常", + Detail = $"机床{machineId}产量跟踪失败: {ex.Message}" + }); } } catch { /* 告警写入失败不影响主流程 */ } + + return ($"异常:{ex.Message}", false, 0); } } } /// - /// 服务停止时结账所有活跃段 + /// 查询当天最新一条日产量记录 /// - public void CloseAllActiveSegments() + private DailyProductionRecord GetCurrentRecord(MySqlConnection conn, int machineId) { - _log.Info("正在结账所有活跃段(服务停止)..."); - - try - { - using (var conn = new MySqlConnection(_connectionString)) - { - // 结账所有活跃段:is_settled=0 且 end_time IS NULL - // 正确计算quantity:保留当前end_part_count,用end-start计算产量 - conn.Execute(@"UPDATE cnc_production_segment - SET end_time = NOW(), - end_part_count = COALESCE(end_part_count, start_part_count), - quantity = GREATEST(0, COALESCE(end_part_count, start_part_count) - start_part_count), - close_reason = @Reason, is_settled = 1, updated_at = NOW() - WHERE is_settled = 0 AND end_time IS NULL", - new { Reason = SegmentCloseReason.ServiceStop }); - } - - _activeSegmentIds.Clear(); - _lastCollectState.Clear(); - _log.Info("所有活跃段已结账"); - } - catch (Exception ex) - { - _log.Error("结账所有活跃段失败", ex); - } + return conn.QueryFirstOrDefault( + @"SELECT id AS Id, machine_id AS MachineId, production_date AS ProductionDate, + program_name AS ProgramName, base_total_count AS BaseTotalCount, + end_total_count AS EndTotalCount + FROM cnc_daily_production + WHERE machine_id = @Mid AND production_date = CURDATE() + ORDER BY id DESC + LIMIT 1", + new { Mid = machineId }); } /// - /// 结账指定机床的活跃段 + /// 处理同程序情况:对比 end_total_count 与当前的 totalPartCount /// - /// 机床ID - /// 结束时的零件数 - /// 关闭原因 - /// 结束时间 - public void CloseActiveSegment(int machineId, decimal? endPartCount, string reason, DateTime endTime) + private (string logText, bool changed) HandleSameProgram( + MySqlConnection conn, DailyProductionRecord record, decimal totalPartCount) { - try + if (totalPartCount > record.EndTotalCount) { - long segmentId; - if (!_activeSegmentIds.TryGetValue(machineId, out segmentId)) - { - // 从DB查找 - using (var conn = new MySqlConnection(_connectionString)) - { - segmentId = conn.ExecuteScalar( - "SELECT id FROM cnc_production_segment WHERE machine_id=@MId AND is_settled=0 AND end_time IS NULL ORDER BY start_time DESC LIMIT 1", - new { MId = machineId }); - } - } - - if (segmentId <= 0) return; - - using (var conn = new MySqlConnection(_connectionString)) - { - // 获取 start_part_count 计算 quantity - var startCount = conn.ExecuteScalar( - "SELECT start_part_count FROM cnc_production_segment WHERE id=@Id", new { Id = segmentId }); - - decimal? quantity = null; - if (startCount.HasValue && endPartCount.HasValue) - { - quantity = Math.Max(0, endPartCount.Value - startCount.Value); - } - - conn.Execute(@"UPDATE cnc_production_segment - SET end_time = @EndTime, end_part_count = @EndPartCount, quantity = @Quantity, - close_reason = @Reason, is_settled = 1, updated_at = NOW() - WHERE id = @Id", - new { Id = segmentId, EndTime = endTime, EndPartCount = endPartCount, Quantity = quantity, Reason = reason }); - } - - _activeSegmentIds.TryRemove(machineId, out _); - _log.Debug($"机床{machineId}: 结账段{segmentId}({reason},quantity={endPartCount})"); + // 正常加工增长 → 更新结束计数 + conn.Execute( + "UPDATE cnc_daily_production SET end_total_count = @Count WHERE id = @Id", + new { Id = record.Id, Count = totalPartCount }); + return ("正常加工,更新结束总零件数", true); } - catch (Exception ex) + + if (totalPartCount == record.EndTotalCount) { - _log.Error($"结账活跃段失败(machine_id={machineId})", ex); + // 无变化 → 跳过 + return ("无变化", false); } + + // totalPartCount < record.EndTotalCount → 数据异常,不写库 + _log.Warn($"机床{record.MachineId}: 数据异常,加工零件总数从{record.EndTotalCount}变为{totalPartCount},不更新数据库"); + return ("数据异常:加工零件总数变小", false); } /// - /// 确保指定机床有一个活跃段,没有则创建 + /// 处理程序切换:旧程序→新程序,创建新记录 /// - private long EnsureActiveSegment(int machineId, string programName, decimal? partCount, DateTime collectTime) + private (string logText, bool changed) HandleProgramChange( + MySqlConnection conn, DailyProductionRecord oldRecord, + string newProgramName, decimal totalPartCount) { - long segmentId; - - if (_activeSegmentIds.TryGetValue(machineId, out segmentId) && segmentId > 0) - return segmentId; - - // 从DB查找 - using (var conn = new MySqlConnection(_connectionString)) + if (totalPartCount >= oldRecord.EndTotalCount) { - segmentId = conn.ExecuteScalar( - "SELECT id FROM cnc_production_segment WHERE machine_id=@MId AND is_settled=0 AND end_time IS NULL ORDER BY start_time DESC LIMIT 1", - new { MId = machineId }); - - if (segmentId <= 0) - { - // 创建新段 - var now = DateTime.Now; - segmentId = conn.ExecuteScalar(@"INSERT INTO cnc_production_segment - (machine_id, program_name, production_date, start_time, start_part_count, is_settled, created_at, updated_at) - VALUES (@MachineId, @ProgramName, @ProductionDate, @StartTime, @StartPartCount, 0, @CreatedAt, @UpdatedAt); - SELECT LAST_INSERT_ID();", - new - { - MachineId = machineId, - ProgramName = programName, - ProductionDate = collectTime.Date, - StartTime = collectTime, - StartPartCount = partCount ?? 0, - CreatedAt = now, - UpdatedAt = now - }); - - _log.Debug($"机床{machineId}: 创建新段{segmentId}(程序={programName}, startCount={partCount})"); - } + // 合法程序切换 → 创建新记录 + // 关键:新记录的 base 使用旧记录的 end,保证产量连续 + InsertRecord(conn, oldRecord.MachineId, newProgramName, + oldRecord.EndTotalCount, totalPartCount); + _log.Info($"机床{oldRecord.MachineId}: 程序切换 {oldRecord.ProgramName} → {newProgramName}," + + $"新记录 base={oldRecord.EndTotalCount} end={totalPartCount}"); + return ("程序切换,创建新记录", true); } - _activeSegmentIds[machineId] = segmentId; - return segmentId; + // totalPartCount < oldRecord.EndTotalCount → 数据异常,不写库 + _log.Warn($"机床{oldRecord.MachineId}: 程序切换时数据异常," + + $"旧程序{oldRecord.ProgramName} end={oldRecord.EndTotalCount}," + + $"新程序{newProgramName} total={totalPartCount}"); + return ("数据异常", false); } /// - /// 更新活跃段的实时 end_part_count + /// 插入一条新的日产量记录 /// - private void UpdateSegmentEndCount(long segmentId, decimal partCount) + private void InsertRecord(MySqlConnection conn, int machineId, + string programName, decimal baseTotalCount, decimal endTotalCount) { - try - { - using (var conn = new MySqlConnection(_connectionString)) + conn.Execute( + @"INSERT INTO cnc_daily_production + (machine_id, production_date, program_name, base_total_count, end_total_count, created_at, updated_at) + VALUES (@MachineId, CURDATE(), @ProgramName, @BaseCount, @EndCount, NOW(), NOW())", + new { - conn.Execute("UPDATE cnc_production_segment SET end_part_count = @Count, updated_at = NOW() WHERE id = @Id", - new { Id = segmentId, Count = partCount }); - } - } - catch (Exception ex) - { - _log.Error($"更新活跃段end_part_count失败(segment_id={segmentId})", ex); - } + MachineId = machineId, + ProgramName = programName, + BaseCount = baseTotalCount, + EndCount = endTotalCount + }); + } + + /// + /// 日产量记录的数据库行映射 + /// + private class DailyProductionRecord + { + public int Id { get; set; } + public int MachineId { get; set; } + public DateTime ProductionDate { get; set; } + public string ProgramName { get; set; } + public decimal BaseTotalCount { get; set; } + public decimal EndTotalCount { get; set; } } + /// + /// 释放资源(当前无需要清理的资源) + /// public void Dispose() { - CloseAllActiveSegments(); + // 无内存缓存需清理,连接由 using 管理 } } }