From 690dfcda3262145b9d5dff4d3f463ebe4d41c5ef Mon Sep 17 00:00:00 2001 From: "zhengxuan.zhang" Date: Wed, 20 May 2026 10:31:25 +0800 Subject: [PATCH] =?UTF-8?q?#260520=20=E6=96=B0=E5=A2=9E=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Analysis/问题修复.js | 332 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 Analysis/问题修复.js diff --git a/Analysis/问题修复.js b/Analysis/问题修复.js new file mode 100644 index 0000000..9e4903c --- /dev/null +++ b/Analysis/问题修复.js @@ -0,0 +1,332 @@ +让我再看一下日志中关于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