优化分发规则
This commit is contained in:
+195
-800
File diff suppressed because it is too large
Load Diff
+31
-31
@@ -14,40 +14,40 @@ using Telerik.WinControls.UI;
|
|||||||
|
|
||||||
namespace NSAnalysis
|
namespace NSAnalysis
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
///
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// 2025.08.28
|
||||||
|
/// ----------------------------------------------
|
||||||
|
/// 1、当解析文件时,增加备份到根目录
|
||||||
|
/// 2、解析失败或提取有效内容失败,移动到源文件夹的Error文件夹
|
||||||
|
/// 3、修复查询结果,显示错误问题
|
||||||
|
///
|
||||||
///
|
///
|
||||||
///
|
/// 2025.08.31
|
||||||
///
|
/// --------------------------------------------
|
||||||
/// 2025.08.28
|
/// 1、新增将客户报告 默认生成到备份文件夹,然后拷贝到目标路径,并记录错误日志
|
||||||
/// ----------------------------------------------
|
///
|
||||||
/// 1、当解析文件时,增加备份到根目录
|
///
|
||||||
/// 2、解析失败或提取有效内容失败,移动到源文件夹的Error文件夹
|
///
|
||||||
/// 3、修复查询结果,显示错误问题
|
/// 2026.05.20
|
||||||
///
|
/// --------------------------------------------
|
||||||
/// 2025.08.31
|
/// 1、修复现场反馈的问题(数据库连接、任务加载情况、定时器重复的问题)
|
||||||
/// --------------------------------------------
|
///
|
||||||
/// 1、新增将客户报告 默认生成到备份文件夹,然后拷贝到目标路径,并记录错误日志
|
///
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
///
|
/// </summary>
|
||||||
///
|
|
||||||
///
|
|
||||||
///
|
|
||||||
///
|
|
||||||
///
|
|
||||||
///
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
|
|
||||||
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 FileSorter fileSorter = new FileSorter();
|
||||||
private int _isProcessing = 0; // 0=空闲, 1=处理中,防止定时器重入
|
private int _isProcessing = 0; // 0=空闲, 1=处理中,防止定时器重入
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
<RootNamespace>NSAnalysis</RootNamespace>
|
<RootNamespace>NSAnalysis</RootNamespace>
|
||||||
<AssemblyName>NSAnalysis</AssemblyName>
|
<AssemblyName>NSAnalysis</AssemblyName>
|
||||||
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
|
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
|
||||||
<FileAlignment>512</FileAlignment>
|
<FileAlignment>512</FileAlignment>
|
||||||
<TargetFrameworkProfile />
|
<TargetFrameworkProfile />
|
||||||
<PublishUrl>publish\</PublishUrl>
|
<PublishUrl>publish\</PublishUrl>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
2026-04-24 13:45:58.350----软件Program Main函数开始执行--
|
2026-05-20 11:40:36.119----软件Program Main函数开始执行--
|
||||||
2026-04-24 13:45:58.352--加载配置文件——>开始
|
2026-05-20 11:40:36.121--加载配置文件——>开始
|
||||||
2026-04-24 13:45:58.364--数据库连接 SqlServerName:127.0.0.1
|
2026-05-20 11:40:36.125--数据库连接 SqlServerName:127.0.0.1
|
||||||
2026-04-24 13:45:58.364--数据库连接 SqlUserName:sa
|
2026-05-20 11:40:36.126--数据库连接 SqlUserName:sa
|
||||||
2026-04-24 13:45:58.364--数据库连接 SqlPassword:Hexagon123
|
2026-05-20 11:40:36.126--数据库连接 SqlPassword:Hexagon123
|
||||||
2026-04-24 13:45:58.364--数据库连接 SqlDbName:CJLR
|
2026-05-20 11:40:36.127--数据库连接 SqlDbName:CJLR
|
||||||
2026-04-24 13:45:58.366--加载配置文件——>完成
|
2026-05-20 11:40:36.128--加载配置文件——>完成
|
||||||
2026-04-24 13:45:58.367--Log in using the original authorization mode.
|
2026-05-20 11:40:36.130--Log in using the original authorization mode.
|
||||||
2026-04-24 13:46:03.487--软件授权失败
|
2026-05-20 11:40:41.124--软件授权失败
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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层 | 低 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
你希望我从哪个修复项开始实施?还是需要我对某个方案做进一步细化?
|
|
||||||
@@ -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` 标准化不足
|
||||||
|
|
||||||
|
本次已经先把这些最影响现场效率和可读性的部分落地优化。
|
||||||
|
报告生成规则暂未改动,后续如果继续处理“双侧配对失败”的问题,需要结合现场车号规则再单独分析。
|
||||||
Reference in New Issue
Block a user