修复产量统计3个Bug:manual_reset/program_change产量丢失、日终汇总失败不重试、日志归档文件锁冲突

Bug#1(核心): ProductionTracker.Track()中manual_reset和program_change段结账时
endPartCount传入了复位后/新程序的值而非结账段最后已知值,导致50个manual_reset
段产量全部为0,program_change段产量偏差

Bug#2: _lastSummaryDate在DailySummaryJob.Execute()执行前就设置,若汇总失败则
永久无法重试。修改为Execute返回bool,成功后设置;时间窗口扩大到±2分钟

Bug#3: log4net RollingFileAppender默认排他锁与LogArchiveJob的File.WriteAllText冲突
导致IOException。改用Date滚动+MinimalLock模式,删除手写LogArchiveJob
main
haoliang 1 month ago
parent bdf0e6b085
commit f83364ec7a

@ -30,7 +30,6 @@ namespace CncCollector.Core
private Timer _dailySummaryTimer; private Timer _dailySummaryTimer;
private Timer _logCleanupTimer; private Timer _logCleanupTimer;
private LogCleanupJob _logCleanupJob; private LogCleanupJob _logCleanupJob;
private LogArchiveJob _logArchiveJob;
private DateTime _startTime; private DateTime _startTime;
private long _totalSuccess; private long _totalSuccess;
private long _totalFail; private long _totalFail;
@ -60,8 +59,6 @@ namespace CncCollector.Core
_dailySummary = new DailySummaryJob(config.BusinessConnection); _dailySummary = new DailySummaryJob(config.BusinessConnection);
// 初始化分析引擎(与业务库和日志库同源,后续按需调整) // 初始化分析引擎(与业务库和日志库同源,后续按需调整)
_analysisEngine = new AnalysisEngine(config.BusinessConnection, config.LogConnection); _analysisEngine = new AnalysisEngine(config.BusinessConnection, config.LogConnection);
// 初始化日志归档任务保留30天
_logArchiveJob = new LogArchiveJob(30);
} }
/// <summary> /// <summary>
@ -184,10 +181,11 @@ namespace CncCollector.Core
/// 手动执行日终汇总(指定日期) /// 手动执行日终汇总(指定日期)
/// </summary> /// </summary>
/// <param name="summaryDate">要汇总的日期</param> /// <param name="summaryDate">要汇总的日期</param>
public void RunDailySummary(DateTime summaryDate) /// <returns>true=汇总成功</returns>
public bool RunDailySummary(DateTime summaryDate)
{ {
_log.Info($"手动触发日终汇总(日期={summaryDate:yyyy-MM-dd}"); _log.Info($"手动触发日终汇总(日期={summaryDate:yyyy-MM-dd}");
_dailySummary.Execute(summaryDate); return _dailySummary.Execute(summaryDate);
} }
/// <summary> /// <summary>
@ -331,17 +329,22 @@ namespace CncCollector.Core
var now = DateTime.Now; var now = DateTime.Now;
var targetTime = new DateTime(now.Year, now.Month, now.Day, summaryTime.Hours, summaryTime.Minutes, 0); var targetTime = new DateTime(now.Year, now.Month, now.Day, summaryTime.Hours, summaryTime.Minutes, 0);
// 检查是否到了汇总时间(±1分钟内 // 检查是否到了汇总时间(±2分钟内比之前±1分钟更宽松防止定时器偏移导致错过窗口
if (Math.Abs((now - targetTime).TotalMinutes) <= 1) if (Math.Abs((now - targetTime).TotalMinutes) <= 2)
{ {
// 汇总昨天的数据 // 汇总昨天的数据
DateTime summaryDate = now.Date.AddDays(-1); DateTime summaryDate = now.Date.AddDays(-1);
if (_lastSummaryDate != summaryDate) if (_lastSummaryDate != summaryDate)
{ {
_lastSummaryDate = summaryDate; // 修复:只在汇总成功后设置 _lastSummaryDate失败时允许重试
_dailySummary.Execute(summaryDate); if (_dailySummary.Execute(summaryDate))
// 同步执行日志归档:把前一天的日志移到日期子目录 {
_logArchiveJob.Execute(); _lastSummaryDate = summaryDate;
}
else
{
_log.Warn($"日终汇总失败(日期={summaryDate:yyyy-MM-dd}),将在下次检查时重试");
}
} }
} }
} }

@ -31,7 +31,8 @@ namespace CncCollector.Core
/// 执行日终汇总,汇总指定日期的产量数据 /// 执行日终汇总,汇总指定日期的产量数据
/// </summary> /// </summary>
/// <param name="summaryDate">要汇总的日期(通常为昨天)</param> /// <param name="summaryDate">要汇总的日期(通常为昨天)</param>
public void Execute(DateTime summaryDate) /// <returns>true=汇总成功false=汇总失败(可重试)</returns>
public bool Execute(DateTime summaryDate)
{ {
_log.Info($"========== 日终汇总开始(日期={summaryDate:yyyy-MM-dd} =========="); _log.Info($"========== 日终汇总开始(日期={summaryDate:yyyy-MM-dd} ==========");
var sw = System.Diagnostics.Stopwatch.StartNew(); var sw = System.Diagnostics.Stopwatch.StartNew();
@ -124,10 +125,12 @@ namespace CncCollector.Core
sw.Stop(); sw.Stop();
_log.Info($"========== 日终汇总完成(日期={summaryDate:yyyy-MM-dd}, 耗时={sw.ElapsedMilliseconds}ms =========="); _log.Info($"========== 日终汇总完成(日期={summaryDate:yyyy-MM-dd}, 耗时={sw.ElapsedMilliseconds}ms ==========");
return true;
} }
catch (Exception ex) catch (Exception ex)
{ {
_log.Error($"日终汇总失败(日期={summaryDate:yyyy-MM-dd}", ex); _log.Error($"日终汇总失败(日期={summaryDate:yyyy-MM-dd}", ex);
return false;
} }
} }
} }

@ -83,9 +83,13 @@ namespace CncCollector.Core
} }
// 结账当前活跃段 // 结账当前活跃段
// 修复使用结账前最后已知的零件数lastPartCount而非当前观测值partCount
// 当 manual_reset 时 partCount 是复位后的值如0当 program_change 时 partCount 是新程序的值
// 正确的 endPartCount 应该是结账段最后观测到的最大计数值
if (needClose) if (needClose)
{ {
CloseActiveSegment(machineId, partCount, closeReason, collectTime); var closingEndCount = hasLast ? lastState.Item2 : partCount;
CloseActiveSegment(machineId, closingEndCount, closeReason, collectTime);
} }
// 确保有活跃段 // 确保有活跃段

@ -1,151 +0,0 @@
using System;
using System.IO;
using log4net;
namespace CncCollector.Jobs
{
/// <summary>
/// 日志文件归档任务。
/// 每天凌晨将前一天的日志文件移动到按日期命名的子目录中。
/// 目录结构示例:
/// logs\collector.log ← 当前在写的运行日志
/// logs\collector_error.log ← 当前在写的错误日志
/// logs\2026-05-07\collector.log
/// logs\2026-05-07\collector_error.log
/// logs\2026-05-06\collector.log
/// logs\2026-05-06\collector_error.log
/// 保留天数外的旧目录自动清理。
/// </summary>
public class LogArchiveJob
{
private static readonly ILog _log = LogManager.GetLogger(typeof(LogArchiveJob));
/// <summary>
/// 日志保留天数
/// </summary>
private readonly int _retainDays;
/// <summary>
/// 日志根目录相对于BaseDirectory
/// </summary>
private readonly string _logsDir;
public LogArchiveJob(int retainDays = 30)
{
_retainDays = retainDays;
_logsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs");
}
/// <summary>
/// 执行归档:把当前日志文件复制到昨天的日期目录,然后截断当前文件。
/// 应在每天凌晨00:00左右调用。
/// </summary>
public void Execute()
{
try
{
if (!Directory.Exists(_logsDir))
{
return;
}
var yesterday = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd");
var archiveDir = Path.Combine(_logsDir, yesterday);
// 归档每个日志文件
ArchiveLogFile("collector.log", archiveDir);
ArchiveLogFile("collector_error.log", archiveDir);
// 清理过期目录
CleanupOldDirectories();
_log.Info($"日志归档完成({yesterday}");
}
catch (Exception ex)
{
_log.Error("日志归档失败", ex);
}
}
/// <summary>
/// 归档单个日志文件到日期目录
/// </summary>
private void ArchiveLogFile(string fileName, string archiveDir)
{
var sourceFile = Path.Combine(_logsDir, fileName);
if (!File.Exists(sourceFile))
{
return;
}
var fileInfo = new FileInfo(sourceFile);
// 文件为空或太新最后写入在1小时内则跳过
// 避免归档正在活跃写入的当天日志
if (fileInfo.Length == 0)
{
return;
}
// 创建日期目录
if (!Directory.Exists(archiveDir))
{
Directory.CreateDirectory(archiveDir);
}
var destFile = Path.Combine(archiveDir, fileName);
// 如果目标已存在,追加序号
if (File.Exists(destFile))
{
var seq = 1;
var baseName = Path.GetFileNameWithoutExtension(fileName);
var ext = Path.GetExtension(fileName);
do
{
destFile = Path.Combine(archiveDir, $"{baseName}_{seq}{ext}");
seq++;
} while (File.Exists(destFile));
}
// 复制到日期目录不是移动因为log4net还持有文件句柄
File.Copy(sourceFile, destFile);
// 截断当前日志文件清空内容但不删除文件保持log4net文件句柄有效
File.WriteAllText(sourceFile, string.Empty);
_log.Info($"已归档 {fileName} → {archiveDir}{fileInfo.Length} 字节)");
}
/// <summary>
/// 清理超过保留天数的日期目录
/// </summary>
private void CleanupOldDirectories()
{
if (!Directory.Exists(_logsDir))
{
return;
}
var cutoff = DateTime.Now.AddDays(-_retainDays).ToString("yyyy-MM-dd");
foreach (var dir in Directory.GetDirectories(_logsDir))
{
var dirName = Path.GetFileName(dir);
// 日期目录格式yyyy-MM-dd
if (dirName.Length == 10 && dirName.Contains("-") && dirName.CompareTo(cutoff) < 0)
{
try
{
Directory.Delete(dir, true);
_log.Info($"已清理过期日志目录:{dirName}");
}
catch (Exception ex)
{
_log.Warn($"清理过期日志目录失败:{dirName}", ex);
}
}
}
}
}
}

@ -7,27 +7,29 @@
</layout> </layout>
</appender> </appender>
<!-- 运行日志INFO及以上含INFO/WARN/ERROR/FATAL --> <!-- 运行日志INFO及以上按日期滚动保留30天 -->
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender"> <appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="logs\collector.log" /> <file value="logs\collector.log" />
<appendToFile value="true" /> <appendToFile value="true" />
<rollingStyle value="Size" /> <rollingStyle value="Date" />
<maxSizeRollBackups value="10" /> <datePattern value="yyyy-MM-dd" />
<maximumFileSize value="50MB" /> <maxSizeRollBackups value="30" />
<staticLogFileName value="true" /> <staticLogFileName value="true" />
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<layout type="log4net.Layout.PatternLayout"> <layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" /> <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
</layout> </layout>
</appender> </appender>
<!-- 错误日志仅ERROR和FATAL --> <!-- 错误日志仅ERROR和FATAL按日期滚动保留30天 -->
<appender name="ErrorFileAppender" type="log4net.Appender.RollingFileAppender"> <appender name="ErrorFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="logs\collector_error.log" /> <file value="logs\collector_error.log" />
<appendToFile value="true" /> <appendToFile value="true" />
<rollingStyle value="Size" /> <rollingStyle value="Date" />
<maxSizeRollBackups value="10" /> <datePattern value="yyyy-MM-dd" />
<maximumFileSize value="50MB" /> <maxSizeRollBackups value="30" />
<staticLogFileName value="true" /> <staticLogFileName value="true" />
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<threshold value="ERROR" /> <threshold value="ERROR" />
<layout type="log4net.Layout.PatternLayout"> <layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger - %message%newline" /> <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />

Loading…
Cancel
Save