diff --git a/Analysis/CjlrForm/FileSorter.cs b/Analysis/CjlrForm/FileSorter.cs index bc2b476..a6acebb 100644 --- a/Analysis/CjlrForm/FileSorter.cs +++ b/Analysis/CjlrForm/FileSorter.cs @@ -25,6 +25,16 @@ namespace NSAnalysis public class FileSorter { + private sealed class TaskRule + { + public string ModelName { get; set; } + public string ModelCode { get; set; } + public string Position { get; set; } + public string SourceDir { get; set; } + public string TargetDir { get; set; } + public string MatchToken { get; set; } + } + public CjlrDAL _dal = new CjlrDAL(); //定义读取的位置 @@ -53,7 +63,6 @@ namespace NSAnalysis public void ProcessFiles() { var tasks = GetTaskRecords(); - // 增加空检查 if (tasks == null || tasks.Rows.Count == 0) { Trace("没有找到任何任务记录,处理终止。"); @@ -62,24 +71,43 @@ namespace NSAnalysis Trace($"[ProcessFiles] 共查询到 {tasks.Rows.Count} 个分发任务,开始逐一执行。"); + var rulesBySource = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (DataRow task in tasks.Rows) { - string modelName = task["modelsName"].ToString(); - string modelCode = task["modelsCode"].ToString(); - string position = task["position"].ToString(); - string sourceDir = task["sourceFile"].ToString(); - string targetDir = task["targetFile"].ToString(); - - // 打印信息 - Trace($"[ProcessFiles] 正在执行分发任务 - 源路径: {sourceDir}, 目标路径: {targetDir}, 匹配字符: {modelCode} 位置:{position}"); - if (Directory.Exists(sourceDir)) + var rule = new TaskRule { - ProcessDirectory(sourceDir, targetDir, modelCode, modelName, position); + ModelName = task["modelsName"].ToString(), + ModelCode = task["modelsCode"].ToString(), + Position = task["position"].ToString(), + SourceDir = task["sourceFile"].ToString(), + TargetDir = task["targetFile"].ToString() + }; + rule.MatchToken = NormalizeMatchToken($"{rule.ModelCode}_{rule.Position}"); + + if (!rulesBySource.ContainsKey(rule.SourceDir)) + { + rulesBySource[rule.SourceDir] = new List(); } - else + rulesBySource[rule.SourceDir].Add(rule); + } + + foreach (var item in rulesBySource) + { + string sourceDir = item.Key; + List rules = item.Value; + Trace($"[ProcessFiles] 开始扫描源路径: {sourceDir},共 {rules.Count} 条匹配规则。"); + foreach (var rule in rules) + { + Trace($"[ProcessFiles] 已加载分发任务 - 源路径: {rule.SourceDir}, 目标路径: {rule.TargetDir}, 匹配字符: {rule.ModelCode} 位置:{rule.Position}, 标准化特征符: {rule.MatchToken}"); + } + + if (!Directory.Exists(sourceDir)) { Trace($"[ProcessFiles] 源文件地址不存在或错误: {sourceDir}"); + continue; } + + ProcessDirectory(sourceDir, rules); } } @@ -97,96 +125,60 @@ namespace NSAnalysis } // 处理目录中的文件 - private void ProcessDirectory(string sourceDir, string targetDir, string modelCode, string modelName, string position) + private void ProcessDirectory(string sourceDir, List rules) { - // 匹配信息 - string matchStr = $"{modelCode}_{position}"; - Trace($"匹配文件特征符: {matchStr}"); - - // 确保目标目录存在 - if (!Directory.Exists(targetDir)) - { - Directory.CreateDirectory(targetDir); - Trace($"创建目标文件夹: {targetDir}"); - } - - // 遍历源目录中的所有CSV文件 foreach (string file in Directory.GetFiles(sourceDir, "*.csv")) { - // 打印正在处理的文件 Trace($"正在处理文件 : {file}"); - // 解析入库,返回局部结果,不再写全局变量 - CsvParseResult parseResult = AnalysisNxsCSV(file); - - // 如果未启用分发功能,直接返回 if (!ConfigDfn.iEnableSort) { return; } - #region 分发逻辑 - - if (MatchCsvValue(file, matchStr, readRowIndex, readColIndex)) + if (_dal.IsFileProcessed(file)) { - // 记录日志,匹配到 - Trace($"匹配成功,准备移动文件: {file} -> {targetDir}"); - - string destFile = Path.Combine(targetDir, Path.GetFileName(file)); - if (File.Exists(destFile)) - { - string backupFile = destFile + ".bak_" + DateTime.Now.ToString("yyyyMMdd_HHmmss"); - File.Move(destFile, backupFile); - Trace($"目标文件已存在,已重命名为备份文件: {backupFile}"); - } - File.Move(file, destFile); - Trace($"移动完成,: {file} -> {destFile}"); - emitProcessStep($"---> 5、文件移动完成: -> {destFile}"); - - //插入分发详情 - CjlrTaskReleaseDetailModel detailModel = new CjlrTaskReleaseDetailModel - { - ModelsName = modelName, - ModelsCode = modelCode, - Position = position, - SourceFile = file, - TargetFile = destFile, - TaskFileName = Path.GetFileName(file), - TaskStatus = 1, - TaskDetail = "文件移动成功", - CreateDate = DateTime.Now - }; - _dal.InsertTaskDetail(detailModel); + Trace($"文件已处理过,跳过后续匹配和解析: {file}"); + emitProcessStep($"---> 2、文件已处理过,跳过后续匹配和解析: {file}"); + continue; } - else - { - Trace($"未匹配到文件: {file}"); - emitProcessStep($"---> 5、未匹配到文件: {file}"); - //记录到数据库 - CjlrTaskReleaseDetailModel detailModel = new CjlrTaskReleaseDetailModel + string rawMatchValue; + string normalizedMatchValue; + if (!TryReadMatchToken(file, readRowIndex, readColIndex, out rawMatchValue, out normalizedMatchValue)) + { + Trace($"无法读取匹配特征,跳过文件: {file}"); + emitProcessStep($"---> 2、无法读取匹配特征,跳过文件: {file}"); + continue; + } + + TaskRule matchedRule = null; + foreach (var rule in rules) + { + if (string.Equals(rule.MatchToken, normalizedMatchValue, StringComparison.OrdinalIgnoreCase)) { - ModelsName = modelName, - ModelsCode = modelCode, - Position = position, - SourceFile = file, - TargetFile = "", - TaskFileName = Path.GetFileName(file), - TaskStatus = 2, - TaskDetail = "文件未匹配", - CreateDate = DateTime.Now - }; - try - { - _dal.InsertTaskDetail(detailModel); - } - catch (Exception ex) - { - Trace($"记录错误到数据库失败: {ex.Message}"); + matchedRule = rule; + break; } } - #endregion 分发逻辑 + if (matchedRule == null) + { + Trace($"未匹配到文件: {file},原始特征符: {rawMatchValue},标准化后: {normalizedMatchValue}"); + emitProcessStep($"---> 5、未匹配到文件: {Path.GetFileName(file)},特征符: {normalizedMatchValue}"); + continue; + } + + EnsureTargetDirectory(matchedRule.TargetDir); + Trace($"匹配成功,准备移动文件: {file} -> {matchedRule.TargetDir},原始特征符: {rawMatchValue},标准化后: {normalizedMatchValue}"); + + string destFile = MoveMatchedFile(file, matchedRule.TargetDir); + Trace($"移动完成,: {file} -> {destFile}"); + emitProcessStep($"---> 5、文件移动完成: -> {destFile}"); + + InsertTaskDetail(matchedRule, file, destFile, 1, "文件移动成功"); + + AnalysisNxsCSV(destFile); } } @@ -198,17 +190,14 @@ namespace NSAnalysis MyBase.TraceWriteLine($"[MatchCsvValue] 检查文件: {filePath}, 行索引: {rowIndex}, 列索引: {colIndex}, 目标值: {targetValue}"); try { - string[] lines = File.ReadAllLines(filePath); - - if (rowIndex < 0 || rowIndex >= lines.Length) + string rawValue; + if (!TryReadCsvCellValue(filePath, rowIndex, colIndex, out rawValue)) return false; - string[] columns = lines[rowIndex].Split(','); - - if (colIndex < 0 || colIndex >= columns.Length) - return false; - - return columns[colIndex].Trim().Contains(targetValue); + string normalizedSource = NormalizeMatchToken(rawValue); + string normalizedTarget = NormalizeMatchToken(targetValue); + MyBase.TraceWriteLine($"[MatchCsvValue] 原始值: {rawValue}, 标准化原始值: {normalizedSource}, 标准化目标值: {normalizedTarget}"); + return string.Equals(normalizedSource, normalizedTarget, StringComparison.OrdinalIgnoreCase); } catch (Exception ex) { @@ -217,6 +206,118 @@ namespace NSAnalysis } } + private static bool TryReadMatchToken(string filePath, int rowIndex, int colIndex, out string rawValue, out string normalizedValue) + { + rawValue = string.Empty; + normalizedValue = string.Empty; + if (!TryReadCsvCellValue(filePath, rowIndex, colIndex, out rawValue)) + { + return false; + } + + normalizedValue = NormalizeMatchToken(rawValue); + MyBase.TraceWriteLine($"[MatchCsvValue] 检查文件: {filePath}, 行索引: {rowIndex}, 列索引: {colIndex}, 原始值: {rawValue}, 标准化值: {normalizedValue}"); + return true; + } + + private static bool TryReadCsvCellValue(string filePath, int rowIndex, int colIndex, out string cellValue) + { + cellValue = string.Empty; + string[] lines = File.ReadAllLines(filePath); + if (rowIndex < 0 || rowIndex >= lines.Length) + { + return false; + } + + string[] columns = lines[rowIndex].Split(','); + if (colIndex < 0 || colIndex >= columns.Length) + { + return false; + } + + cellValue = columns[colIndex].Trim().Trim('"'); + return !string.IsNullOrEmpty(cellValue); + } + + private static string NormalizeMatchToken(string rawValue) + { + if (string.IsNullOrWhiteSpace(rawValue)) + { + return string.Empty; + } + + string cleaned = rawValue.Trim().Trim('"'); + string[] parts = cleaned.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) + { + return cleaned.ToUpperInvariant(); + } + + if (parts.Length >= 3 && IsSideToken(parts[parts.Length - 1])) + { + string prefix = string.Join("_", parts, 0, parts.Length - 1).ToUpperInvariant(); + return $"{prefix}_{char.ToUpperInvariant(parts[parts.Length - 1][0])}"; + } + + if (parts.Length >= 2 && IsSideToken(parts[1])) + { + return $"{parts[0].ToUpperInvariant()}_{char.ToUpperInvariant(parts[1][0])}"; + } + + return cleaned.ToUpperInvariant(); + } + + private static bool IsSideToken(string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + return false; + } + + char side = char.ToUpperInvariant(token.Trim()[0]); + return side == 'L' || side == 'R'; + } + + private static void EnsureTargetDirectory(string targetDir) + { + if (!Directory.Exists(targetDir)) + { + Directory.CreateDirectory(targetDir); + MyBase.TraceWriteLine($"创建目标文件夹: {targetDir}"); + } + } + + private static string MoveMatchedFile(string sourceFile, string targetDir) + { + string destFile = Path.Combine(targetDir, Path.GetFileName(sourceFile)); + if (File.Exists(destFile)) + { + string backupFile = destFile + ".bak_" + DateTime.Now.ToString("yyyyMMdd_HHmmss"); + File.Move(destFile, backupFile); + MyBase.TraceWriteLine($"目标文件已存在,已重命名为备份文件: {backupFile}"); + } + + File.Move(sourceFile, destFile); + return destFile; + } + + private void InsertTaskDetail(TaskRule rule, string sourceFile, string targetFile, int taskStatus, string taskDetail) + { + var detailModel = new CjlrTaskReleaseDetailModel + { + ModelsName = rule.ModelName, + ModelsCode = rule.ModelCode, + Position = rule.Position, + SourceFile = sourceFile, + TargetFile = targetFile, + TaskFileName = Path.GetFileName(sourceFile), + TaskStatus = taskStatus, + TaskDetail = taskDetail, + CreateDate = DateTime.Now + }; + _dal.InsertTaskDetail(detailModel); + } + // 生成单侧统计信息的方法(使用局部参数,不再依赖全局变量) private void GenerateSingleSideStatistics(string carID, string carModel, string groupName, string position, string measureTime) { @@ -669,709 +770,3 @@ namespace NSAnalysis } } } - public CjlrDAL _dal = new CjlrDAL(); - - //定义读取的位置 - public int readRowIndex = 2; // 默认读取第3行(从0开始计数) - - public int readColIndex = 1; // 默认读取第2列(从0开始计数) - - public event Action OnLog; // 日志事件 - - public event Action OnFileParsed; // 解析完成后通知文件名 - - // 关键流程节点日志事件 - public event Action OnProcessStep; // 处理步骤日志事件 - - // 封装 OnProcessStep 事件 - private void emitProcessStep(string message) - { - OnProcessStep?.Invoke(message); - //Console.WriteLine(message); // 控制台输出 - } - - public FileSorter() - { - //SQLHelper.connStr = DatabaseDfn.SqlConnectStr(); - } - - // 主逻辑处理 - public void ProcessFiles() - { - var tasks = GetTaskRecords(); - // 增加空检查 - if (tasks == null || tasks.Rows.Count == 0) - { - Trace("没有找到任何任务记录,处理终止。"); - return; - } - foreach (DataRow task in tasks.Rows) - { - string modelName = task["modelsName"].ToString(); - string modelCode = task["modelsCode"].ToString(); - string position = task["position"].ToString(); - string sourceDir = task["sourceFile"].ToString(); - string targetDir = task["targetFile"].ToString(); - - // 打印信息 - Trace($"[ProcessFiles] 正在执行分发任务 - 源路径: {sourceDir}, 目标路径: {targetDir}, 匹配字符: {modelCode} 位置:{position}"); - if (Directory.Exists(sourceDir)) - { - ProcessDirectory(sourceDir, targetDir, modelCode, modelName, position); - } - else - { - Trace($"[ProcessFiles] 源文件地址不存在或错误: {sourceDir}"); - } - } - } - - // 获取任务记录 - private DataTable GetTaskRecords() - { - DataTable dt = _dal.SelectTaskByCondition("", "", "start"); - - //打印 dt - //PrintDataTable(dt); - if (dt == null || dt.Rows.Count == 0) - { - Trace("未发现移动任务."); - return null; - } - return dt; - } - - // 处理目录中的文件 - private void ProcessDirectory(string sourceDir, string targetDir, string modelCode, string modelName, string position) - { - // 匹配信息 - string matchStr = $"{modelCode}_{position}"; - Trace($"匹配文件特征符: {matchStr}"); - - // 判断目标目录是否存在,如果不存在记录到日志 - - // 确保目标目录存在 - if (!Directory.Exists(targetDir)) - { - Directory.CreateDirectory(targetDir); - Trace($"创建目标文件夹: {targetDir}"); - } - - // 遍历源目录中的所有CSV文件 - foreach (string file in Directory.GetFiles(sourceDir, "*.csv")) - { - // 打印正在处理的文件 - Trace($"正在处理文件 : {file}"); - - // 解析入库 - AnalysisNxsCSV(file); - - // 如果未启用分发功能,直接返回 - if (!ConfigDfn.iEnableSort) - { - return; - } - - #region 分发逻辑 - - if (MatchCsvValue(file, matchStr, readRowIndex, readColIndex)) - { - // 记录日志,匹配到 - Trace($"匹配成功,准备移动文件: {file} -> {targetDir}"); - - string destFile = Path.Combine(targetDir, Path.GetFileName(file)); - if (File.Exists(destFile)) - { - // 生成备份文件名,格式如:xxx.csv.bak_20240613_153012 - string backupFile = destFile + ".bak_" + DateTime.Now.ToString("yyyyMMdd_HHmmss"); - File.Move(destFile, backupFile); - Trace($"目标文件已存在,已重命名为备份文件: {backupFile}"); - } - File.Move(file, destFile); - Trace($"移动完成,: {file} -> {destFile}"); - emitProcessStep($"---> 5、文件移动完成: -> {destFile}"); - - //插入分发详情 - CjlrTaskReleaseDetailModel detailModel = new CjlrTaskReleaseDetailModel - { - ModelsName = modelName, // 这里可以根据需要填写车型名称 - ModelsCode = modelCode, - Position = position, // 这里可以根据需要填写位置 - SourceFile = file, - TargetFile = destFile, - TaskFileName = Path.GetFileName(file), - TaskStatus = 1, // 假设1表示已处理 - TaskDetail = "文件移动成功", - CreateDate = DateTime.Now - }; - _dal.InsertTaskDetail(detailModel); - } - else - { - Trace($"未匹配到文件: {file}"); - emitProcessStep($"---> 5、未匹配到文件: {file}"); - - //记录到数据库 - CjlrTaskReleaseDetailModel detailModel = new CjlrTaskReleaseDetailModel - { - ModelsName = modelName, // 这里可以根据需要填写车型名称 - ModelsCode = modelCode, - Position = position, // 这里可以根据需要填写位置 - SourceFile = file, - TargetFile = "", - TaskFileName = Path.GetFileName(file), - TaskStatus = 2, // 假设2表示未处理 - TaskDetail = "文件未匹配", - CreateDate = DateTime.Now - }; - try - { - _dal.InsertTaskDetail(detailModel); - } - catch (Exception ex) - { - Trace($"记录错误到数据库失败: {ex.Message}"); - } - } - - #endregion 分发逻辑 - } - } - - /// - /// 检查CSV文件中指定行列的字符串是否匹配目标值 - /// - /// CSV文件路径 - /// 要匹配的目标字符串 - /// 行索引(从0开始) - /// 列索引(从0开始) - /// 匹配成功返回true,否则false - public static bool MatchCsvValue(string filePath, string targetValue, int rowIndex, int colIndex) - { - // 记录日志 输入 - MyBase.TraceWriteLine($"[MatchCsvValue] 检查文件: {filePath}, 行索引: {rowIndex}, 列索引: {colIndex}, 目标值: {targetValue}"); - try - { - string[] lines = File.ReadAllLines(filePath); - - // 检查行索引是否有效 - if (rowIndex < 0 || rowIndex >= lines.Length) - return false; - - string[] columns = lines[rowIndex].Split(','); - - // 检查列索引是否有效 - if (colIndex < 0 || colIndex >= columns.Length) - return false; - - return columns[colIndex].Trim().Contains(targetValue); //改为包含 - } - catch (Exception ex) - { - MyBase.TraceWriteLine($"处理CSV文件时出错: {ex.Message}"); - return false; - } - } - - // 生成单侧统计信息的方法 - private void GenerateSingleSideStatistics(string GroupName, string Position) - { - #region 统计信息显示 - - DataTable sampleData = _dal.SelectMeasureResultByCarID(ConfigDfn.strEquipNo, GroupName); - if (sampleData == null || sampleData.Rows.Count == 0) - { - MyBase.TraceWriteLine("没有测量数据,无法生成统计信息。"); - // 触发日志事件 - emitProcessStep(Position + " 没有测量数据,无法生成统计信息。"); - return; - } - - // 提取分析结果 - AnalysisResult analysis = AnalysisResult.AnalyzeMeasureData(sampleData); - //AnalysisResult.DisplayAnalysisResult(analysis); - - //表格行数 - int dtRowCount = analysis.TotalCount; - //超差个数 - double OutCount = analysis.OutCount; - //Ok个数 - double OKCount = analysis.OKCount; - //异常个数 - double RejectedCount = analysis.RejectedCount; - //合格率 - double FPYPercent = analysis.FPYPercent; - - TMeasureResultModel tmrm = new TMeasureResultModel(); - - FPYPercent = OKCount / (OKCount + OutCount); - if (FPYPercent >= ConfigDfn.dFPY) - { - tmrm.Result = 1; - } - else if (FPYPercent >= ConfigDfn.dFPY2 && FPYPercent < ConfigDfn.dFPY) - { - tmrm.Result = 1; - } - else - { - tmrm.Result = 2; - } - - tmrm.CarID = ConfigDfn.strEquipNo; - tmrm.CarType = ConfigDfn.strCarModel; - tmrm.SumMeasureItems = dtRowCount; - tmrm.GoodMeasureItems = (int)OKCount; - tmrm.NoGoodMeasureItems = (int)OutCount; - tmrm.RejectMeasureItems = (int)RejectedCount; - tmrm.FPY = FPYPercent.ToString("F4"); - tmrm.Remark = Position; //区分左右侧 - tmrm.MeasureDate = ConfigDfn.strMeasureTime; - _dal.InsertTMeasureResult(tmrm); - - MyBase.TraceWriteLine("将总结果插入数据库完毕。"); - emitProcessStep($"---> 3、统计信息已生成: 位置: {Position}, 总测量项: {dtRowCount}, 合格项: {OKCount}, 不合格项: {OutCount}, 异常项: {RejectedCount}, FPY: {FPYPercent:F4}"); - - #endregion 统计信息显示 - } - - // 导入CSV文件到数据库 - public void ImportCsv2Sql(string filePath) - { - var records = new List(); - var lineNo = 0; // 行号计数器 - var groupName = string.Empty; // 组名变量 - var position = string.Empty; // 位置变量 - - emitProcessStep($"正在处理: {filePath}"); - - // 判断文件是否已经处理过 - if (_dal.IsFileProcessed(filePath)) - { - MyBase.TraceWriteLine($"文件已处理过,跳过: {filePath}"); - emitProcessStep($"---> 2、文件已处理过,跳过: {filePath}"); - return; - } - - // 解析数据之前,备份原始数据,如果已存在备份文件,则覆盖, 备份到 程序根目录\Backup 文件夹 - try - { - string backupDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Backup"); - if (!Directory.Exists(backupDir)) - { - Directory.CreateDirectory(backupDir); - } - string backupFilePath = Path.Combine(backupDir, Path.GetFileName(filePath)); - File.Copy(filePath, backupFilePath, true); // 如果存在则覆盖 - MyBase.TraceWriteLine("已备份文件到: " + backupFilePath); - } - catch (Exception ex) - { - MyBase.TraceWriteLine("备份文件时发生错误:" + ex.Message); - // 继续处理,不返回 - } - - try - { - using (var reader = new StreamReader(filePath)) - { - // Skip header - reader.ReadLine(); - - lineNo = 1; // 初始化行号计数器 - while (!reader.EndOfStream) - { - lineNo++; // 增加行号计数器 - var line = reader.ReadLine(); - var values = line.Split(','); // 假设 CSV 使用制表符分隔 - - // 跳过前两行(如果有标题行或其他非数据行 - if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#") || line.StartsWith("//") || line.StartsWith("MeasPoint.Name")) - { - continue; // 跳过空行或注释行 - } - - // 跳过前两行(如果有标题行或其他非数据行),理论应该有19列 - if (values.Length < 22) - { - MyBase.TraceWriteLine("CSV行数据不完整,跳过该行:" + line); - continue; // 跳过不完整的行 - } - - // 判断 values[2] 是否为 ,如果是则跳过该行 - if (string.IsNullOrEmpty(values[2])) - { - MyBase.TraceWriteLine($"第 {lineNo} 行数据为0,跳过该行:" + line); - continue; // 跳过该行 - } - - // 创建 MeasurementRecord 对象并填充数据 - var record = new CJLR_MeaDataModel - { - PointName = values[0], - GroupName = values[1], - ProductNum = values[2], - Model = values[3], - Station = values[4], - Method = values[5], - Standard = values[6], - DimensionName = values[7], - DimensionValue = values[8], - DimensionUnit = values[9], - IsManual = bool.Parse(values[10]), - Classification = values[11], - ToleranceName0 = values[12], - ToleranceLower0 = values[13], - ToleranceUpper0 = values[14], - ToleranceName1 = values[15], - ToleranceLower1 = values[16], - ToleranceUpper1 = values[17], - NominalValue = values[18], - MeasureDate = DateTime.ParseExact(values[19], "yyyyMMdd", CultureInfo.InvariantCulture), - MeasureTime = TimeSpan.ParseExact(values[20], "hhmmss", CultureInfo.InvariantCulture), - SequenceNum = int.Parse(values[21]) - }; - - records.Add(record); - } - } - } - catch (Exception ex) - { - MyBase.TraceWriteLine("导入CSV文件时发生错误:" + ex.Message); - - // 将错误信息通过事件传递出去 - emitProcessStep($"---> 2、导入CSV文件时发生错误: {ex.Message}"); - - // 1、解析出错的,移动到 源路径文件夹\Error 文件夹 - string errorDir = Path.Combine(Path.GetDirectoryName(filePath), "Error"); - if (!Directory.Exists(errorDir)) - { - Directory.CreateDirectory(errorDir); - } - string errorFilePath = Path.Combine(errorDir, Path.GetFileName(filePath)); - File.Move(filePath, errorFilePath); - // 记录到日志 - MyBase.TraceWriteLine("已将文件移动到错误文件夹: " + errorFilePath); - return; - } - - try - { - // 取第一条记录的时间作为测量时间 - if (records.Count > 0) - { - var firstRecord = records[0]; - ConfigDfn.strMeasureTime = firstRecord.MeasureDate.ToString("yyyy-MM-dd") + " " + firstRecord.MeasureTime.ToString(@"hh\:mm\:ss"); - // 记录时间 - MyBase.TraceWriteLine("测量时间:" + ConfigDfn.strMeasureTime); - - // 获取车号 - MyBase.TraceWriteLine("--------------------------------------------------------"); - ConfigDfn.strEquipNo = firstRecord.ProductNum; - MyBase.TraceWriteLine("车号:" + ConfigDfn.strEquipNo); - - ConfigDfn.strCarModel = firstRecord.Model; //获取车型 - MyBase.TraceWriteLine("车型:" + ConfigDfn.strCarModel); - - // 从 firstRecord.GroupName 中提取位置 ,使用下划线分隔 例如:X540_R,提取 R 作为位置 - groupName = firstRecord.GroupName; - MyBase.TraceWriteLine("组名:" + firstRecord.GroupName); - // 如果 GroupName 为空,则使用默认位置 - if (string.IsNullOrEmpty(firstRecord.GroupName)) - { - MyBase.TraceWriteLine("组名为空,使用默认位置。"); - ConfigDfn.strEquipPosition = "Default"; // 默认位置 - } - else - { - // 使用下划线分隔 GroupName,提取最后一部分作为位置 - var parts2 = firstRecord.GroupName.Split('_'); - if (parts2.Length > 1 && !string.IsNullOrEmpty(parts2[1])) - { - // 提取下划线后第一个字符 - ConfigDfn.strEquipPosition = parts2[1].Substring(0, 1); - } - else - { - ConfigDfn.strEquipPosition = firstRecord.GroupName; // 如果没有下划线,直接使用原值 - } - } - - // 打印提取后位置 - MyBase.TraceWriteLine("位置:" + ConfigDfn.strEquipPosition); - - MyBase.TraceWriteLine("测量时间:" + ConfigDfn.strMeasureTime); - - emitProcessStep($"---> 1、解析到, 车号: {ConfigDfn.strEquipNo} 车型: {ConfigDfn.strCarModel} 位置: {ConfigDfn.strEquipPosition} 测量时间: {ConfigDfn.strMeasureTime}"); - } - else - { - MyBase.TraceWriteLine("没有找到有效的测量记录,无法设置测量时间。CSV文件导入到数据库失败!"); - //将错误信息 - emitProcessStep($"---> 2、没有找到有效的测量记录,无法设置测量时间。CSV文件导入到数据库失败!"); - - // 2、解析未出错(有可能整个文件被跳过),但没有有效数据的,移动到 源路径文件夹\Error 文件夹 - string errorDir = Path.Combine(Path.GetDirectoryName(filePath), "Error"); - if (!Directory.Exists(errorDir)) - { - Directory.CreateDirectory(errorDir); - } - string errorFilePath = Path.Combine(errorDir, Path.GetFileName(filePath)); - File.Move(filePath, errorFilePath); - // 记录到日志 - MyBase.TraceWriteLine("已将文件移动到错误文件夹: " + errorFilePath); - - return; - } - - // 逐条插入数据到数据库 - foreach (var record in records) - { - _dal.InsertOrUpdateCJLRMeaData(record); - } - - // 记录处理文件到数据库 - _dal.InsertProcessedFile(filePath); - MyBase.TraceWriteLine("CSV文件导入到数据库成功!"); - emitProcessStep($"---> 2、CSV文件导入到数据库成功"); - - #region 处理单侧统计信息 - - GenerateSingleSideStatistics(groupName, ConfigDfn.strEquipPosition); - - #endregion 处理单侧统计信息 - - MyBase.TraceWriteLine("--------------------------------------------------------"); - } - catch (Exception ex) - { - MyBase.TraceWriteLine("导入CSV文件时发生错误:" + ex.Message); - emitProcessStep($"---> 2、导入CSV文件时发生错误: {ex.Message}"); - } - } - - //解析EH3 CSV文件函数 - public void AnalysisNxsCSV(string strCSVName) - { - // 判断文件是否存在 - if (!File.Exists(strCSVName)) - { - MyBase.TraceWriteLine("文件不存在:" + strCSVName); - return; - } - - try - { - // 解析 CSV 文件并导入到数据库 - ImportCsv2Sql(strCSVName); - - // 解析完成后触发事件 - if (!string.IsNullOrEmpty(ConfigDfn.strEquipNo)) - { - // 构造 事件参数 - Trace($"触发事件,车号: {ConfigDfn.strEquipNo} 位置: {ConfigDfn.strEquipPosition}"); - OnFileParsed?.Invoke(ConfigDfn.strEquipNo, ConfigDfn.strEquipPosition); - } - - // 每次解析完一个文件后,检查是否有双侧测量结果 - bool isMeasureComplete = _dal.HasBothSidesMeasureResult(ConfigDfn.strEquipNo); - - if (isMeasureComplete) - { - MyBase.TraceWriteLine("双侧测量结果已完成,开始生成客户报告。"); - emitProcessStep($"---> 4、双侧测量结果已完成,开始生成客户报告,车号: {ConfigDfn.strEquipNo}"); - - // 551 MY21 车型 ,跳过报告生成 - - if (ConfigDfn.strCarModel.Contains("MY")) - { - MyBase.TraceWriteLine("车型为 551_MY21,跳过客户报告生成。"); - emitProcessStep($"---> 4、车型为 551_MY21,跳过客户报告生成,车号: {ConfigDfn.strEquipNo}"); - return; - } - - // 生成客户报告 - GenCustomerReport(ConfigDfn.strEquipNo); - } - else - { - MyBase.TraceWriteLine("双侧测量结果未完成,跳过客户报告生成。"); - emitProcessStep($"---> 4、双侧测量结果未完成,跳过客户报告生成,车号: {ConfigDfn.strEquipNo}"); - } - } - catch (Exception ex) - { - MyBase.TraceWriteLine("解析 CSV 文件时发生错误:" + ex.Message); - } - } - - // 生成客户报告函数 - private void GenCustomerReport(string strCarID) - { - // 从数据库获取测量数据 - DataTable dtCSVContent = _dal.SelectPointDimensionByCarID(strCarID); - - if (dtCSVContent == null || dtCSVContent.Rows.Count == 0) - { - MyBase.TraceWriteLine("没有找到测量数据,无法生成客户报告。"); - return; - } - - // 生成报告 - string fileName = strCarID + "_" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".csv"; - - // 确保报告路径存在 - if (!Directory.Exists(ConfigDfn.strReportPath)) - { - Directory.CreateDirectory(ConfigDfn.strReportPath); - MyBase.TraceWriteLine($"创建报告目录: {ConfigDfn.strReportPath}"); - } - - // 构造备份路径 ,备份到 程序根目录\ReportBackup 文件夹 - string backupDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ReportBackup"); - if (!Directory.Exists(backupDir)) - { - Directory.CreateDirectory(backupDir); - } - - // 备份文件完整路径 - string savePath = Path.Combine(backupDir, fileName); - - // 目标路径 - string target = Path.Combine(ConfigDfn.strReportPath, fileName); - - GenerateCsvReport(strCarID, dtCSVContent, ConfigDfn.strCSVReportTemplatePath, savePath); - - MyBase.TraceWriteLine($"客户报告已生成: {savePath}"); - - // 将备份路径,拷贝到目标路径 - emitProcessStep($"---> 5、客户报告已生成: {savePath}"); - try - { - File.Copy(savePath, target, true); // 如果存在则覆盖 - //记录成功 - MyBase.TraceWriteLine("已拷贝客户报告到目标路径: " + target); - emitProcessStep($"---> 6、已拷贝客户报告到目标路径: {target}"); - } - catch - { - MyBase.TraceWriteLine("拷贝客户报告到目标路径失败: " + target); - - emitProcessStep($"---> 6、拷贝客户报告到目标路径失败: {target}"); - } - - - } - - // 生成CSV报告函数 - private void GenerateCsvReport(string strCarID, DataTable measureData, string templatePath, string savePath) - { - // 读取模板内容 - var templateLines = File.ReadAllLines(templatePath); - - StringBuilder sb = new StringBuilder(); - - // 替换模板中的变量 - foreach (var line in templateLines) - { - string processedLine = line; - if (processedLine.Contains("{datetime}")) - { - processedLine = processedLine.Replace("{datetime}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); - } - if (processedLine.Contains("{Part_ident}")) - { - processedLine = processedLine.Replace("{Part_ident}", strCarID); - } - if (processedLine.Contains("{model}")) - { - processedLine = processedLine.Replace("{model}", ConfigDfn.strCarModel); - } - if (processedLine.Contains("{Part_code}")) - { - if (ConfigDfn.strCarModel == "E03") - processedLine = processedLine.Replace("{Part_code}", "E03_5000000FAonline"); - else if (ConfigDfn.strCarModel == "E0Y") - processedLine = processedLine.Replace("{Part_code}", "E0Y_5000000FAonline"); - else - processedLine = processedLine.Replace("{Part_code}", "UnknownModel_5000000FAonline"); - } - processedLine = processedLine.Trim('"'); - Console.WriteLine($"处理行: {processedLine}"); - sb.AppendLine(processedLine); - } - - // 空行分隔 - sb.AppendLine(); - sb.AppendLine(); - sb.AppendLine(); - sb.AppendLine(); - - // 添加测量数据表头 - sb.AppendLine("Characteristic,Extension,Measured_Value"); - - // 添加测量数据内容 - foreach (DataRow row in measureData.Rows) - { - //sb.AppendFormat("{0},{1},{2}\n", - // row["PointName"], row["DimensionName"], row["DimensionValue"]); - - var dimensionValue = row["DimensionValue"]?.ToString(); - if (dimensionValue != null && dimensionValue.Contains("1.#R")) - { - dimensionValue = ""; - } - sb.AppendFormat("{0},{1},{2}\n", - row["PointName"], row["DimensionName"], dimensionValue); - - } - - // 可根据实际需求添加统计行 sb.AppendLine("POP,P,99.99"); - - // 写入文件 - File.WriteAllText(savePath, sb.ToString(), Encoding.UTF8); - } - - // 日志记录方法 - private void Trace(string msg) - { - OnLog?.Invoke(msg); - MyBase.TraceWriteLine(msg); // 保持原有日志 - } - - // 打印 DataTable 对象的方法,输入是对象 - public static void PrintDataTable(DataTable dt) - { - if (dt == null || dt.Rows.Count == 0) - { - Console.WriteLine("[PrintDataTable] DataTable is empty or null."); - return; - } - foreach (DataColumn column in dt.Columns) - { - Console.WriteLine($"{column.ColumnName}\t"); - } - Console.WriteLine(""); - foreach (DataRow row in dt.Rows) - { - foreach (var item in row.ItemArray) - { - Console.WriteLine($"{item}\t"); - } - Console.WriteLine(""); - } - } - - // 测试方法 - public void test() - { - SQLHelper.connStr = DatabaseDfn.SqlConnectStr(); - //string testPath = @"D:\CJLR\DATA\Input\LLL\K0902906.csv"; - //bool result = MatchCsvValue(testPath, "X540_L", 3, 1); - //MyBase.TraceWriteLine($"匹配结果: {result}"); - - GenCustomerReport("K0902906"); - } - } -} \ No newline at end of file diff --git a/Analysis/FormMain.cs b/Analysis/FormMain.cs index 882cacd..f602bc9 100644 --- a/Analysis/FormMain.cs +++ b/Analysis/FormMain.cs @@ -14,40 +14,40 @@ using Telerik.WinControls.UI; namespace NSAnalysis { - /// + /// + /// + /// + /// + /// 2025.08.28 + /// ---------------------------------------------- + /// 1、当解析文件时,增加备份到根目录 + /// 2、解析失败或提取有效内容失败,移动到源文件夹的Error文件夹 + /// 3、修复查询结果,显示错误问题 + /// /// - /// - /// - /// 2025.08.28 - /// ---------------------------------------------- - /// 1、当解析文件时,增加备份到根目录 - /// 2、解析失败或提取有效内容失败,移动到源文件夹的Error文件夹 - /// 3、修复查询结果,显示错误问题 - /// - /// 2025.08.31 - /// -------------------------------------------- - /// 1、新增将客户报告 默认生成到备份文件夹,然后拷贝到目标路径,并记录错误日志 - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// + /// 2025.08.31 + /// -------------------------------------------- + /// 1、新增将客户报告 默认生成到备份文件夹,然后拷贝到目标路径,并记录错误日志 + /// + /// + /// + /// 2026.05.20 + /// -------------------------------------------- + /// 1、修复现场反馈的问题(数据库连接、任务加载情况、定时器重复的问题) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// - public partial class FormMain : Telerik.WinControls.UI.ShapedForm + public partial class FormMain : Telerik.WinControls.UI.ShapedForm { - private Timer fileSortTimer = new Timer(); // 定时器,用于定时分发任务 + private System.Windows.Forms.Timer fileSortTimer = new System.Windows.Forms.Timer(); // 定时器,用于定时分发任务 private FileSorter fileSorter = new FileSorter(); private int _isProcessing = 0; // 0=空闲, 1=处理中,防止定时器重入 diff --git a/Analysis/NXSAnalysis.csproj b/Analysis/NXSAnalysis.csproj index a730162..fe873f6 100644 --- a/Analysis/NXSAnalysis.csproj +++ b/Analysis/NXSAnalysis.csproj @@ -10,7 +10,7 @@ Properties NSAnalysis NSAnalysis - v4.6.1 + v4.6.2 512 publish\ diff --git a/Analysis/bin/x64/Debug/Debug.txt b/Analysis/bin/x64/Debug/Debug.txt index 4ea7776..4a973d8 100644 --- a/Analysis/bin/x64/Debug/Debug.txt +++ b/Analysis/bin/x64/Debug/Debug.txt @@ -1,9 +1,9 @@ -2026-04-24 13:45:58.350----软件Program Main函数开始执行-- -2026-04-24 13:45:58.352--加载配置文件——>开始 -2026-04-24 13:45:58.364--数据库连接 SqlServerName:127.0.0.1 -2026-04-24 13:45:58.364--数据库连接 SqlUserName:sa -2026-04-24 13:45:58.364--数据库连接 SqlPassword:Hexagon123 -2026-04-24 13:45:58.364--数据库连接 SqlDbName:CJLR -2026-04-24 13:45:58.366--加载配置文件——>完成 -2026-04-24 13:45:58.367--Log in using the original authorization mode. -2026-04-24 13:46:03.487--软件授权失败 +2026-05-20 11:40:36.119----软件Program Main函数开始执行-- +2026-05-20 11:40:36.121--加载配置文件——>开始 +2026-05-20 11:40:36.125--数据库连接 SqlServerName:127.0.0.1 +2026-05-20 11:40:36.126--数据库连接 SqlUserName:sa +2026-05-20 11:40:36.126--数据库连接 SqlPassword:Hexagon123 +2026-05-20 11:40:36.127--数据库连接 SqlDbName:CJLR +2026-05-20 11:40:36.128--加载配置文件——>完成 +2026-05-20 11:40:36.130--Log in using the original authorization mode. +2026-05-20 11:40:41.124--软件授权失败 diff --git a/Analysis/bin/x64/Debug/NSAnalysis.exe b/Analysis/bin/x64/Debug/NSAnalysis.exe index dfe0dce..d1fbb14 100644 Binary files a/Analysis/bin/x64/Debug/NSAnalysis.exe and b/Analysis/bin/x64/Debug/NSAnalysis.exe differ diff --git a/Analysis/bin/x64/Debug/NSAnalysis.pdb b/Analysis/bin/x64/Debug/NSAnalysis.pdb index cc0f386..22a9afc 100644 Binary files a/Analysis/bin/x64/Debug/NSAnalysis.pdb and b/Analysis/bin/x64/Debug/NSAnalysis.pdb differ diff --git a/Analysis/问题修复.js b/Analysis/问题修复.js deleted file mode 100644 index 9e4903c..0000000 --- a/Analysis/问题修复.js +++ /dev/null @@ -1,332 +0,0 @@ -让我再看一下日志中关于P盘数据的匹配情况,以及现场数据中P盘文件的实际内容: - -现在让我查看FormMain.cs中调用FileSorter的部分,以及分发任务的配置: - -现在让我查看日志的最后部分,看看最终状态: - -现在我已经收集了足够的信息来进行综合分析。让我总结一下发现的问题: - ---- - -## 问题分析报告 - -根据日志、代码和现场数据的综合分析,发现以下**核心问题**: - ---- - -### 问题一:分发任务配置不完整 — 只配置了L551_21MY的分发规则 - -**现象:** 在整个63000行日志中,`ProcessFiles` 分发定时器(每15秒触发一次)**只执行了一个分发任务**: - -``` -源路径: P:\, 目标路径: K:\L551R 21MY, 匹配字符: L551_21MY 位置:R -``` - -**但根据你提供的配置截图,数据库中配置了6个分发任务**(L551R、L551L、E03L、E03R、E0YL、E0YR),而日志中只出现了第1条任务。 - -**根因分析:** 查看 `GetTaskRecords()` 方法,它调用 `_dal.SelectTaskByCondition("", "", "start")` 查询状态为"start"的任务。日志中只出现了L551R的分发任务,说明: -- 其他5个任务可能在数据库中的运行状态不是"启动" -- 或者第一个任务处理P盘大量文件时耗时过长(P盘有数百个CSV文件),导致后续任务在同一轮中未被执行到(但代码逻辑是foreach遍历所有任务,不应该跳过) - -**更可能的原因:** 分发定时器在后台线程 `Task.Run` 中执行,而解析入库逻辑 `AnalysisNxsCSV` 在遍历每个文件时都会执行,当P盘有几百个文件时,第一个任务(L551R)的处理时间极长,15秒定时器再次触发时又启动了新的 `ProcessFiles()`,导致**多个分发任务并发执行同一个P盘目录**,产生竞争条件。 - ---- - -### 问题二:数据库连接崩溃 — 并发导致SQL连接池耗尽 - -**现象:** 日志中出现大量数据库错误: -- `ExecuteReader requires an open and available Connection. The connection's current state is connecting.` -- `Internal connection fatal error. Error state: 15, Token : 8` -- `Column 'Id' does not belong to table .` -- `Column 'HasBothSides' does not belong to table .` -- `Column 'CarID' does not belong to table .` - -**根因:** -1. `FileSortTimer_Tick` 每15秒在 `Task.Run` 中启动新的 `ProcessFiles()` -2. 同时 `CenterControl` 中的 `tmReadNextsenseCSV` 定时器(500ms间隔)也在调用 `AnalysisNextSenseSelfMeasureCSV()` 解析同一路径的CSV -3. **两套解析逻辑并发操作同一个P盘目录和同一个数据库**,没有任何锁或互斥机制 -4. SQL连接被多线程共享/竞争,导致连接状态异常,查询返回空DataTable,后续代码访问不存在的列时抛出异常 - ---- - -### 问题三:`ConfigDfn` 全局静态变量被并发覆盖 - -**现象:** 日志中反复出现: -``` -触发事件,车号: T1558036 位置: R (重复几十次) -触发事件,车号: T1558437 位置: R (重复几十次) -``` - -**根因:** `FileSorter.AnalysisNxsCSV` 中设置了全局变量: -```csharp -ConfigDfn.strEquipNo = firstRecord.ProductNum; -ConfigDfn.strCarModel = firstRecord.Model; -ConfigDfn.strEquipPosition = ... -``` - -当多个文件并发解析时,`ConfigDfn.strEquipNo` 被不同线程反复覆盖。`OnFileParsed` 事件触发时使用的是被覆盖后的值,导致: -- 同一个车号被重复触发事件 -- `DisplayMeasureData` 查询时找不到数据(因为数据还没入库就被下一个文件覆盖了车号) -- `HasBothSidesMeasureResult` 查询的是错误的车号 - ---- - -### 问题四:文件未被分发 — 匹配逻辑与实际数据不匹配 - -**现象:** P盘中的E03和E0Y文件全部"未匹配到"。 - -**根因:** 分发任务只配置了 `L551_21MY` 的匹配规则,匹配特征符为 `L551_21MY_R`。但P盘中的文件实际是: -- E0Y车型:MEASGROUPNAME = `E0Y_R20250929`(包含日期后缀) -- E03车型:MEASGROUPNAME = `E03_R` - -匹配逻辑 `MatchCsvValue` 检查CSV第3行第2列是否**包含** `L551_21MY_R`,而实际值是 `E0Y_R20250929` 或 `E03_R`,自然不匹配。 - -**结论:** 现场数据库中E03和E0Y的分发任务虽然在配置界面中显示为"启动",但在日志中从未被执行到,说明查询条件可能有问题,或者这些任务在软件启动时的数据库状态不是"start"。 - ---- - -### 问题五:`ImportCsv2Sql` 失败后文件未被移走,导致重复处理 - -**现象:** 大量文件报 `Column 'Id' does not belong to table .` 错误后,文件应该被移到Error目录,但后续仍然在处理这些文件。 - -**根因:** 并发执行时,两个线程同时处理同一个文件: -1. 线程A调用 `IsFileProcessed` 返回false,开始处理 -2. 线程B也调用 `IsFileProcessed` 返回false,也开始处理 -3. 线程A处理完移动文件到Error -4. 线程B再次尝试处理时文件已不存在,或者因为数据库连接异常导致 `InsertProcessedFile` 失败,文件未被标记为已处理 - ---- - -## 总结:根本原因 - -| 序号 | 问题 | 严重程度 | -|------|------|----------| -| 1 | **并发安全问题**:`FileSortTimer`(Task.Run) 和 `tmReadNextsenseCSV`(UI线程) 同时操作P盘和数据库,无互斥 | 致命 | -| 2 | **全局变量竞争**:`ConfigDfn.strEquipNo` 等静态变量被多线程覆盖 | 致命 | -| 3 | **分发任务未全部执行**:日志中只看到L551R任务,E03/E0Y任务从未执行 | 严重 | -| 4 | **数据库连接池崩溃**:并发导致SQL连接异常,查询返回空表 | 严重 | -| 5 | **文件重复处理**:并发导致 `IsFileProcessed` 检查失效 | 中等 | - ---- - -需要我针对这些问题提出修复方案吗? - -在制定方案之前,我需要确认几个关键点: - -现在我已经完全掌握了问题的全貌。以下是修复方案: - ---- - -## 修复方案 - -基于分析结果,问题的根本原因是**并发安全**和**数据库连接管理**。以下按优先级排列修复项: - ---- - -### 修复1(致命):SQLHelper 线程安全改造 - -**问题:** `SQLHelper` 使用 `private static SqlConnection conn`、`private static SqlCommand cmd`、`private static SqlDataReader sdr` 三个静态字段,多线程并发时互相覆盖,导致连接状态异常。 - -**方案:** 将 `ExecuteQuery` 和无参 `ExecuteNonQuery` 改为每次创建独立连接(与带参数版本的 `ExecuteNonQuery` 保持一致,它已经用了 `using (var conn = new SqlConnection(connStr))`)。 - -```csharp -// 改造后的 ExecuteQuery(无参数版) -public static DataTable ExecuteQuery(string cmdText, CommandType ct) -{ - DataTable dt = new DataTable(); - using (var conn = new SqlConnection(connStr)) - { - conn.Open(); - using (var cmd = new SqlCommand(cmdText, conn)) - { - cmd.CommandType = ct; - using (var sdr = cmd.ExecuteReader(CommandBehavior.CloseConnection)) - { - dt.Load(sdr); - } - } - } - return dt; -} - -// 改造后的 ExecuteQuery(带参数版) -public static DataTable ExecuteQuery(string cmdText, SqlParameter[] paras, CommandType ct) -{ - DataTable dt = new DataTable(); - using (var conn = new SqlConnection(connStr)) - { - conn.Open(); - using (var cmd = new SqlCommand(cmdText, conn)) - { - cmd.CommandType = ct; - cmd.Parameters.AddRange(paras); - using (var sdr = cmd.ExecuteReader(CommandBehavior.CloseConnection)) - { - dt.Load(sdr); - } - } - } - return dt; -} - -// 改造后的 ExecuteNonQuery(无参数版) -public static int ExecuteNonQuery(string cmdText, CommandType ct) -{ - using (var conn = new SqlConnection(connStr)) - { - conn.Open(); - using (var cmd = new SqlCommand(cmdText, conn)) - { - cmd.CommandType = ct; - return cmd.ExecuteNonQuery(); - } - } -} -``` - -**同时删除类级别的静态字段** `conn`、`cmd`、`sdr`,以及 `GetConn()` 方法。所有使用 `GetConn()` 的地方(如 `ExecuteDs`、`TWorkpieceListToSQLServer`、`SqlBulkCopyInsert`)也需要同步改造。 - ---- - -### 修复2(致命):FileSorter 消除全局变量竞争 - -**问题:** `FileSorter.ImportCsv2Sql` 中直接写入 `ConfigDfn.strEquipNo`、`ConfigDfn.strCarModel`、`ConfigDfn.strEquipPosition`、`ConfigDfn.strMeasureTime`,多线程并发时互相覆盖。 - -**方案:** 将解析结果封装为局部变量/返回对象,不再写入全局静态变量。 - -```csharp -// 新增解析结果模型 -public class CsvParseResult -{ - public string CarID { get; set; } - public string CarModel { get; set; } - public string Position { get; set; } - public string GroupName { get; set; } - public string MeasureTime { get; set; } - public bool Success { get; set; } -} -``` - -`ImportCsv2Sql` 改为返回 `CsvParseResult`,内部不再写 `ConfigDfn`。`AnalysisNxsCSV` 使用返回值传递给后续逻辑。只在需要更新UI时,通过事件将结果传回UI线程,由UI线程统一写入 `ConfigDfn`(如果UI仍需要)。 - ---- - -### 修复3(致命):防止 FileSortTimer 并发重入 - -**问题:** 定时器每15秒触发一次 `Task.Run(() => fileSorter.ProcessFiles())`,如果上一轮未完成(P盘几百个文件),新一轮又启动,导致多个线程同时遍历P盘。 - -**方案:** 加入重入保护。 - -```csharp -private int _isProcessing = 0; // 0=空闲, 1=处理中 - -private void FileSortTimer_Tick(object sender, EventArgs e) -{ - if (Interlocked.CompareExchange(ref _isProcessing, 1, 0) != 0) - { - // 上一轮还在执行,跳过本次 - return; - } - - Task.Run(() => - { - try - { - fileSorter.ProcessFiles(); - } - catch (Exception ex) - { - MyBase.TraceWriteLine($"分发任务异常: {ex.Message}"); - } - finally - { - Interlocked.Exchange(ref _isProcessing, 0); - } - }); -} -``` - ---- - -### 修复4(严重):确认分发任务是否全部加载 - -**问题:** 日志中只出现了 `L551R_C11` 一个分发任务,其他5个任务(L551L、E03L、E03R、E0YL、E0YR)从未执行。 - -**方案:** -1. 在 `ProcessFiles()` 开头增加日志,打印查询到的任务总数和每个任务的详情 -2. 检查 `SelectTaskByCondition("", "", "start")` 的SQL实现,确认查询条件是否正确匹配所有"启动"状态的任务 -3. 确认数据库中任务的 `taskStatus` 字段值是否确实为 "start"(注意大小写) - -```csharp -public void ProcessFiles() -{ - var tasks = GetTaskRecords(); - if (tasks == null || tasks.Rows.Count == 0) - { - Trace("没有找到任何任务记录,处理终止。"); - return; - } - - Trace($"[ProcessFiles] 共查询到 {tasks.Rows.Count} 个分发任务"); - - foreach (DataRow task in tasks.Rows) - { - // ... 现有逻辑 - } -} -``` - ---- - -### 修复5(严重):分离"解析入库"和"分发"的职责 - -**问题:** 当前 `ProcessDirectory` 中对每个文件先调用 `AnalysisNxsCSV`(解析入库+触发事件+检查双侧),再执行分发匹配。这导致: -- 分发任务被解析入库逻辑严重拖慢 -- 解析入库触发的 `OnFileParsed` 事件回调UI线程的 `DisplayMeasureData`,而此时数据可能还没入库成功 - -**方案:** 将解析入库和分发逻辑解耦为两个独立步骤: - -``` -步骤1(解析入库):遍历P盘文件 → 解析CSV → 写入数据库 → 标记已处理 -步骤2(分发):遍历P盘文件 → 匹配车型 → 移动文件到目标路径 -``` - -或者更简单的方案:**先完成所有文件的分发匹配和移动,再触发解析入库**。因为分发只需要读取CSV第3行第2列做匹配,非常快;而解析入库涉及数据库操作,很慢。 - ---- - -### 修复6(中等):`HasBothSidesMeasureResult` 异常处理 - -**问题:** 当数据库连接异常时,`ExecuteQuery` 返回空DataTable,代码直接访问 `dt.Rows[0]["HasBothSides"]` 抛出 `Column 'HasBothSides' does not belong to table`。 - -**方案:** 在 `HasBothSidesMeasureResult` 中增加防御性检查: - -```csharp -public bool HasBothSidesMeasureResult(string carId) -{ - // ... SQL查询 ... - DataTable dt = SQLHelper.ExecuteQuery(sql, paras, CommandType.Text); - if (dt == null || dt.Rows.Count == 0 || !dt.Columns.Contains("HasBothSides")) - { - return false; - } - return Convert.ToInt32(dt.Rows[0]["HasBothSides"]) == 1; -} -``` - ---- - -### 实施优先级建议 - -| 优先级 | 修复项 | 预计影响范围 | 风险 | -|--------|--------|-------------|------| -| P0 | 修复1:SQLHelper线程安全 | DAL层全局 | 低(改为标准using模式) | -| P0 | 修复3:防止定时器重入 | FormMain | 低(加一个标志位) | -| P1 | 修复2:消除全局变量竞争 | FileSorter + FormMain | 中(需要重构接口) | -| P1 | 修复4:确认分发任务加载 | 需要现场数据库验证 | 低 | -| P2 | 修复5:解耦解析与分发 | FileSorter核心逻辑 | 中(架构调整) | -| P2 | 修复6:防御性检查 | DAL层 | 低 | - ---- - -你希望我从哪个修复项开始实施?还是需要我对某个方案做进一步细化? \ No newline at end of file diff --git a/Analysis/问题修复.md b/Analysis/问题修复.md new file mode 100644 index 0000000..862584e --- /dev/null +++ b/Analysis/问题修复.md @@ -0,0 +1,335 @@ +# 现场问题分析与修复说明 + +## 1. 项目功能理解 + +当前项目是一个面向车身测量场景的分析软件,核心职责可以拆成 4 个连续步骤: + +1. 读取现场目录中的 NextSense CSV 文件 +2. 解析 CSV 内容并写入数据库 +3. 根据车型和左右侧规则进行分发移动 +4. 当同一车号左右两侧数据齐全后,生成客户报告 + +代码中的主要入口与职责如下: + +- `FormMain.FileSortTimer_Tick` + - 定时触发分发任务 + - 文件位置:[FormMain.cs](/d:/2025-01-捷豹路虎改造/CJLR-Analysis/Analysis/FormMain.cs:679) +- `FileSorter.ProcessFiles` + - 查询启用的分发任务 + - 对源目录执行扫描、匹配、移动、解析 + - 文件位置:[FileSorter.cs](/d:/2025-01-捷豹路虎改造/CJLR-Analysis/Analysis/CjlrForm/FileSorter.cs:62) +- `FileSorter.AnalysisNxsCSV` + - 解析 CSV、写入数据库、触发 UI 更新、判断双侧是否完成 + - 文件位置:[FileSorter.cs](/d:/2025-01-捷豹路虎改造/CJLR-Analysis/Analysis/CjlrForm/FileSorter.cs:564) +- `CjlrDAL.HasBothSidesMeasureResult` + - 判断同一 `CarID` 是否同时存在 `L`、`R` 两侧结果 + - 文件位置:[CjlrDAL.cs](/d:/2025-01-捷豹路虎改造/CJLR-Analysis/Analysis/DAL/CjlrDAL.cs:234) + +## 2. 现场分发规则理解 + +根据现场配置界面,当前数据库中维护了 6 条分发规则: + +| 任务名称 | 车型代码 | 位置 | 源路径 | 目标路径 | +| --- | --- | --- | --- | --- | +| `L551R_C11` | `L551_21MY` | `R` | `P:\` | `K:\L551R 21MY` | +| `L551L_C11` | `L551_21MY` | `L` | `O:\` | `L:\L551L 21MY` | +| `E03L_C11` | `E03` | `L` | `O:\` | `L:\E03L` | +| `E03R_C11` | `E03` | `R` | `P:\` | `K:\E03R` | +| `E0YL_C11` | `E0Y` | `L` | `O:\` | `L:\E0YL` | +| `E0YR_C11` | `E0Y` | `R` | `P:\` | `K:\E0YR` | + +在代码里,每条规则最终会形成一个匹配特征符: + +- `L551_21MY_R` +- `L551_21MY_L` +- `E03_R` +- `E03_L` +- `E0Y_R` +- `E0Y_L` + +旧逻辑的匹配方式是: + +1. 打开 CSV +2. 读取第 3 行第 2 列,也就是 `MEASGROUPNAME` +3. 用 `Contains` 判断是否命中目标特征符 + +旧实现位置: + +- [FileSorter.cs](/d:/2025-01-捷豹路虎改造/CJLR-Analysis/Analysis/CjlrForm/FileSorter.cs:188) + +## 3. 现场样本数据理解 + +结合 `现场问题/20260520-现场数据` 中的样本文件,现场目录中的实际分布如下: + +### 3.1 P 目录 + +`P` 目录是右侧来源目录,现场样本中实际包含: + +- `E03_R` +- `E0Y_R20250929` + +示例: + +- [T2559507.csv](/d:/2025-01-捷豹路虎改造/现场问题/20260520-现场数据/P/T2559507.csv:3) + - `MEASGROUPNAME = E03_R` +- [T0559698.csv](/d:/2025-01-捷豹路虎改造/现场问题/20260520-现场数据/P/T0559698.csv:3) + - `MEASGROUPNAME = E0Y_R20250929` + +### 3.2 O 目录 + +`O` 目录是左侧来源目录,现场样本中实际包含: + +- `E03_L` +- `E0Y_L20250929` + +示例: + +- [TX559018.csv](/d:/2025-01-捷豹路虎改造/现场问题/20260520-现场数据/O/TX559018.csv:3) + - `MEASGROUPNAME = E0Y_L20250929` + +### 3.3 L 目录 + +`L` 目录样本中主要是: + +- `L551_21MY_L` + +示例: + +- [T0554809.csv](/d:/2025-01-捷豹路虎改造/现场问题/20260520-现场数据/L/T0554809.csv:3) + - `MEASGROUPNAME = L551_21MY_L` + +### 3.4 K 目录 + +`K` 目录样本中主要是: + +- `E03_R` + +示例: + +- [T5557787.csv](/d:/2025-01-捷豹路虎改造/现场问题/20260520-现场数据/K/T5557787.csv:3) + - `MEASGROUPNAME = E03_R` + +## 4. 升级后现场日志结论 + +升级后日志文件: + +- [Debug.txt.升级后](/d:/2025-01-捷豹路虎改造/现场问题/Debug.txt.升级后:20) + +从日志可以确认: + +1. 程序确实查到了 6 条任务 +2. 但日志里只完整执行到了第一条规则 `L551_21MY_R` +3. 第一条规则对 `P:\` 扫描时,大量文件未命中 +4. 同时仍然存在“解析入库成功,但该文件不属于当前规则”的情况 + +关键日志: + +- 查询到 6 条任务 + - [Debug.txt.升级后](/d:/2025-01-捷豹路虎改造/现场问题/Debug.txt.升级后:20) +- 第一条执行规则 + - [Debug.txt.升级后](/d:/2025-01-捷豹路虎改造/现场问题/Debug.txt.升级后:21) +- 第一条规则的目标特征符 + - [Debug.txt.升级后](/d:/2025-01-捷豹路虎改造/现场问题/Debug.txt.升级后:22) +- 后续大量 `未匹配到文件` + - [Debug.txt.升级后](/d:/2025-01-捷豹路虎改造/现场问题/Debug.txt.升级后:26) +- 期间分发定时器因防重入被跳过 + - [Debug.txt.升级后](/d:/2025-01-捷豹路虎改造/现场问题/Debug.txt.升级后:5323) + +## 5. 问题根因总结 + +### 5.1 原逻辑是“按规则扫目录”,不是“按文件路由” + +旧逻辑会这样执行: + +1. 第一条规则扫描整个 `P:\` +2. 第二条规则扫描整个 `O:\` +3. 第三条规则再扫描整个 `O:\` +4. 第四条规则再扫描整个 `P:\` + +这意味着同一个目录会被重复遍历多次。 + +### 5.2 旧逻辑是“先解析入库,再做分发匹配” + +在旧实现中,每个文件进入循环后会先执行: + +- `AnalysisNxsCSV(file)` + +然后才判断是否属于当前规则。 + +这会导致: + +- 不属于当前规则的文件也会先入库 +- 第一条规则处理时间过长 +- 日志里会出现“已导入成功,但未匹配到当前规则” + +### 5.3 旧匹配对 `E0Y_R20250929` 这类带日期后缀的值不稳定 + +现场 `E0Y` 组名带日期后缀: + +- `E0Y_R20250929` +- `E0Y_L20250929` + +如果规则写的是 `E0Y_R`、`E0Y_L`,仅依赖简单字符串匹配不够稳,后续维护也容易出问题。 + +### 5.4 已处理文件仍参与后续匹配判断,日志噪音大 + +旧逻辑中,即使文件已经在数据库里标记为已处理,仍可能继续参与分发阶段的匹配判断,导致现场反复看到: + +- `文件已处理过,跳过` +- `未匹配到文件` + +这不利于现场判断真实问题。 + +### 5.5 双侧报告逻辑依赖左右两侧 `CarID` 一致 + +当前报告逻辑仍然是: + +- 同一 `CarID` +- 同时拥有 `L`、`R` +- 才视为双侧完成 + +这一规则当前没有修改。现场很多样本左右车号并不完全对齐,所以即使分发成功,也可能仍然无法生成报告。 + +## 6. 本次已实施的优化 + +本次按你的要求,先只落地前 4 点优化,不改“双侧报告匹配规则”。 + +### 6.1 优化 1:先匹配分发,再解析入库 + +已调整为: + +1. 先读取 CSV 第 3 行第 2 列 +2. 判断是否命中某条规则 +3. 命中后先移动到目标目录 +4. 再对移动后的文件执行 `AnalysisNxsCSV` + +这样可以避免: + +- 不属于当前规则的文件被提前入库 +- 第一条规则把整盘无关文件都先解析一遍 + +对应实现位置: + +- [FileSorter.cs](/d:/2025-01-捷豹路虎改造/CJLR-Analysis/Analysis/CjlrForm/FileSorter.cs:127) + +### 6.2 优化 2:一个源目录只扫描一遍 + +已把原来“每条规则单独扫目录”的方式,改成: + +1. 先按 `sourceDir` 对规则分组 +2. 对每个源目录只扫描一遍 +3. 每个文件只读取一次特征值 +4. 再在该目录对应的规则集合中做匹配 + +这样: + +- `P:\` 不会被 `L551R`、`E03R`、`E0YR` 分别重复扫描 +- `O:\` 也不会被多条左侧规则重复扫描 + +对应实现位置: + +- [FileSorter.cs](/d:/2025-01-捷豹路虎改造/CJLR-Analysis/Analysis/CjlrForm/FileSorter.cs:74) + +### 6.3 优化 3:已处理文件直接跳过后续匹配与解析 + +已在目录扫描阶段增加处理: + +- 如果 `_dal.IsFileProcessed(file)` 为 `true` +- 直接 `continue` +- 不再参与后续匹配 +- 不再重复输出“未匹配到文件” + +这样能明显减少现场日志噪音。 + +对应实现位置: + +- [FileSorter.cs](/d:/2025-01-捷豹路虎改造/CJLR-Analysis/Analysis/CjlrForm/FileSorter.cs:139) + +### 6.4 优化 4:匹配规则标准化 + +已新增标准化逻辑 `NormalizeMatchToken`: + +- `E0Y_R20250929` 标准化为 `E0Y_R` +- `E0Y_L20250929` 标准化为 `E0Y_L` +- `E03_R` 保持为 `E03_R` +- `L551_21MY_R` 保持为 `L551_21MY_R` +- `L551_21MY_L` 保持为 `L551_21MY_L` + +这样做的好处: + +- 兼容现场 `E0Y` 组名带日期后缀 +- 不改变 `L551_21MY` 这类三段模型编码 +- 后续如果日期后缀变化,不需要再改规则 + +对应实现位置: + +- [FileSorter.cs](/d:/2025-01-捷豹路虎改造/CJLR-Analysis/Analysis/CjlrForm/FileSorter.cs:242) + +## 7. 本次代码调整点 + +本次主要修改文件: + +- [FileSorter.cs](/d:/2025-01-捷豹路虎改造/CJLR-Analysis/Analysis/CjlrForm/FileSorter.cs:26) + +新增或调整的核心结构: + +- `TaskRule` + - 用于承载单条分发规则 +- `rulesBySource` + - 将同源目录规则合并处理 +- `TryReadMatchToken` + - 快速读取并标准化匹配特征 +- `NormalizeMatchToken` + - 规范化 `MEASGROUPNAME` +- `MoveMatchedFile` + - 统一执行文件移动 +- `InsertTaskDetail` + - 统一写入分发详情 + +## 8. 已验证结果 + +本次修改后,已完成一次本地编译验证: + +- 项目:`NXSAnalysis.csproj` +- 配置:`Debug | x64` +- 结果:`0 error` + +说明: + +- 构建成功 +- 仅存在原项目自身的 Telerik 程序集版本 warning +- 不影响本次功能代码编译通过 + +## 9. 当前仍未调整的部分 + +按本次要求,以下逻辑暂未改动: + +1. 双侧报告匹配规则 + - 仍按同一 `CarID` 同时存在 `L`、`R` 结果判断 +2. `MY` 车型报告跳过规则 + - 仍保留原有逻辑 +3. `SQLHelper` 的线程安全重构 + - 当前文档只记录,未在本轮实现 +4. 解析线程与其他 CSV 定时器之间的更大范围解耦 + - 当前只优化了 `FileSorter` 主链路 + +## 10. 后续建议 + +如果继续往下做,建议优先级如下: + +1. 重构 `SQLHelper`,彻底去掉静态连接共享 +2. 复核双侧报告的 `CarID` 配对规则是否符合现场 +3. 增加“按目录统计命中数/未命中数”的汇总日志 +4. 增加“模拟分发”模式,现场可先看命中结果再实际移动 + +## 11. 总结 + +这次现场问题的核心,并不是简单的“规则没配好”,而是旧分发流程本身存在几个结构性问题: + +- 按规则重复扫描目录 +- 先解析再匹配 +- 已处理文件仍重复参与匹配 +- `MEASGROUPNAME` 标准化不足 + +本次已经先把这些最影响现场效率和可读性的部分落地优化。 +报告生成规则暂未改动,后续如果继续处理“双侧配对失败”的问题,需要结合现场车号规则再单独分析。