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 管理
}
}
}