重构产量跟踪:从内存分段引擎改为数据库对比CNC日产量表

main
haoliang 1 month ago
parent 196e9c97f5
commit d8d5fe32b8

@ -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
{
/// <summary>
/// 零件产量分段跟踪引擎。
/// 为每台机床维护内存中的当前活跃段状态,检测程序切换和手动清零。
/// 零件产量跟踪引擎。
/// 基于数据库 cnc_daily_production 表对比加工零件总数(Tag1)计算当日产量。
/// 记录不可变:每条记录创建后仅 end_total_count 可更新。
/// </summary>
public class ProductionTracker : IDisposable
{
@ -19,16 +16,6 @@ namespace CncCollector.Core
private readonly string _connectionString;
private readonly object _lock = new object();
/// <summary>
/// 内存缓存machineId → 当前活跃段ID减少DB查询
/// </summary>
private readonly ConcurrentDictionary<int, long> _activeSegmentIds = new ConcurrentDictionary<int, long>();
/// <summary>
/// 内存缓存machineId → 上一次采集的 (programName, partCount)
/// </summary>
private readonly ConcurrentDictionary<int, Tuple<string, decimal?>> _lastCollectState = new ConcurrentDictionary<int, Tuple<string, decimal?>>();
/// <summary>
/// 初始化产量跟踪器
/// </summary>
@ -39,241 +26,213 @@ namespace CncCollector.Core
}
/// <summary>
/// 处理一次采集结果:检测程序切换/手动清零,管理活跃段
/// 处理一次采集结果:基于数据库最新记录对比加工零件总数,计算当日产量。
/// </summary>
/// <param name="machineId">机床ID</param>
/// <param name="programName">当前NC程序名</param>
/// <param name="partCount">当前零件数</param>
/// <param name="totalPartCount">加工零件总数(Tag1)</param>
/// <param name="collectTime">采集时间</param>
public void Track(int machineId, string programName, decimal? partCount, DateTime collectTime)
/// <returns>
/// logText描述本次处理结果的文本
/// changed数据库是否发生了变更
/// todayTotal当前机床当日总产量 = SUM(end_total_count - base_total_count)
/// </returns>
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<string, decimal?> 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);
// 情况1NC程序名变化
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<decimal>(
@"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);
}
}
}
/// <summary>
/// 服务停止时结账所有活跃段
/// 查询当天最新一条日产量记录
/// </summary>
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<DailyProductionRecord>(
@"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 });
}
/// <summary>
/// 结账指定机床的活跃段
/// 处理同程序情况:对比 end_total_count 与当前的 totalPartCount
/// </summary>
/// <param name="machineId">机床ID</param>
/// <param name="endPartCount">结束时的零件数</param>
/// <param name="reason">关闭原因</param>
/// <param name="endTime">结束时间</param>
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<long>(
"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<decimal?>(
"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);
}
/// <summary>
/// 确保指定机床有一个活跃段,没有则创建
/// 处理程序切换:旧程序→新程序,创建新记录
/// </summary>
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<long>(
"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<long>(@"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);
}
/// <summary>
/// 更新活跃段的实时 end_part_count
/// 插入一条新的日产量记录
/// </summary>
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
});
}
/// <summary>
/// 日产量记录的数据库行映射
/// </summary>
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; }
}
/// <summary>
/// 释放资源(当前无需要清理的资源)
/// </summary>
public void Dispose()
{
CloseAllActiveSegments();
// 无内存缓存需清理,连接由 using 管理
}
}
}

Loading…
Cancel
Save