feat: 实现 FOD 日志数据采集 CLI 应用

This commit is contained in:
ssss
2026-07-02 18:13:56 +08:00
parent c4e5ac0360
commit 3dcd9f7c0b
52 changed files with 3144 additions and 354 deletions

View File

@@ -0,0 +1,123 @@
# 文件名解析器
> 所属模块:[解析器设计](./概览.md)
---
## 1. 文件名格式
**格式约定**`TX面板-TX硬件-TX软件-RX类型-测试目的-年月日-测试次数.log`
**分隔符**:支持 `-`(短横线)和 `_`(下划线)两种分隔符,文件名解析时将同时拆分这两种字符。
> ⚠️ **注意**:字段值内部不应包含分隔符字符。例如,若 TX面板值为 `single-mold`,解析时会拆分为 `single` 和 `mold` 两个字段,导致解析失败。请确保字段值本身不含 `-` 或 `_`。
**示例**
- 标准:`singleMold-v1.0-hex2_1-iPhone15-compatibility-20260702-1.log`(字段值无分隔符)
- 避免:`single-mold-v1.0-hex2.1-iPhone15-兼容性测试-20260702-1.log``single-mold` 会拆分)
**位置说明**:表格中的"位置"采用 1-based 索引(从 1 开始计数),与代码中的 0-based 数组索引对应。
---
## 2. 字段映射
| 位置 | 字段 | 对应维度表 | 示例值 |
|------|------|------------|--------|
| 1 | TxPanel | tx_panel.name | single-mold |
| 2 | TxHardware | tx_hardware.version | v1.0 |
| 3 | TxSoftware | tx_software.version | hex2.1 |
| 4 | RxType | rx_type.name | iPhone15 |
| 5 | TestPurpose | test_scenario.test_purpose | 兼容性测试 |
| 6 | TestDate | test_scenario.test_date | 20260702 |
| 7 | TestSequence | test_scenario.test_sequence | 1 |
---
## 3. 解析器实现
```csharp
// Parsers/FileNameParser.cs
using System.Globalization;
using WCTDataMiner.Services;
namespace WCTDataMiner.Parsers;
public class FileNameParser
{
private static readonly char[] Separators = { '-', '_' };
/// <summary>
/// 解析日志文件名,提取场景信息
/// </summary>
/// <param name="fileName">文件名(不含路径)</param>
/// <returns>场景信息,若格式不匹配返回 null</returns>
public ScenarioInfo? Parse(string fileName)
{
// 移除 .log 扩展名
var nameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
if (string.IsNullOrEmpty(nameWithoutExt))
return null;
var parts = nameWithoutExt.Split(Separators);
if (parts.Length < 7)
return null;
// 解析日期
if (!TryParseDate(parts[5], out var testDate))
return null;
// 解析测试序号
if (!int.TryParse(parts[6], out var testSequence))
testSequence = 1;
return new ScenarioInfo(
TxPanel: parts[0],
TxHardware: parts[1],
TxSoftware: parts[2],
RxType: parts[3],
TestPurpose: parts.Length > 4 ? parts[4] : null,
TestDate: testDate,
TestSequence: testSequence
);
}
private static bool TryParseDate(string dateStr, out DateOnly date)
{
// 支持多种日期格式
var formats = new[] { "yyyyMMdd", "yyyy-MM-dd", "yyMMdd" };
return DateOnly.TryParseExact(dateStr, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out date);
}
}
```
---
## 4. 容错处理
| 场景 | 处理方式 |
|------|----------|
| 文件名为空或 null | 返回 nullParseService 记录错误日志 |
| 文件名仅含扩展名(如 `.log` | 返回 null |
| 文件名少于 7 个字段 | 返回 null缺少必要字段 |
| 日期格式不匹配 | 返回 null无法解析 test_date |
| 测试序号解析失败 | 使用默认值 1 |
| 文件名扩展名无效(非 `.log` | 仍会解析,仅去除扩展名后尝试 |
> **说明**:由于解析器要求 `parts.Length >= 7` 才会成功返回,"测试目的缺失"不会单独发生——当字段不足时整体解析失败。若需支持可选字段,需调整解析逻辑。
---
## 5. 使用示例
```csharp
// 在 ParseService 中使用
var fileNameParser = new FileNameParser();
var scenarioInfo = fileNameParser.Parse("single-mold-v1.0-hex2.1-iPhone15-兼容性测试-20260702-1.log");
if (scenarioInfo != null)
{
var scenario = await scenarioService.GetOrCreateScenarioAsync(scenarioInfo);
// 继续解析文件内容...
}
```

View File

@@ -36,10 +36,13 @@
## 3. 正则表达式
> **修正说明**原设计稿正则只有2个忽略字段实际格式有3个X忽略字段已修正。
```csharp
// Parsers/PlossParser.cs
// 格式: FOD-> (1) X X X (2) (3) ... (13)其中3个X需要3个 \d+ 忽略
private static readonly Regex PlossPattern = new(
@"^FOD->\s+(\d+)\s+\d+\s+\d+\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(-?\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$",
@"^FOD->\s+(\d+)\s+\d+\s+\d+\s+\d+\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(-?\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$",
RegexOptions.Compiled
);
```
@@ -82,7 +85,7 @@ public class PlossParser : IParser<PlossRecord>
public bool CanParse(string line) => PlossPattern.IsMatch(line);
public ParseResult<PlossRecord> Parse(string line, int lineNumber, Guid logFileId)
public ParseResult<PlossRecord> Parse(string line, Guid scenarioId)
{
var match = PlossPattern.Match(line);
if (!match.Success)
@@ -97,8 +100,7 @@ public class PlossParser : IParser<PlossRecord>
{
var record = new PlossRecord
{
LogFileId = logFileId,
LineNumber = lineNumber,
ScenarioId = scenarioId,
RxType = byte.Parse(match.Groups[1].Value),
RxPower = short.Parse(match.Groups[2].Value),
TxPower = short.Parse(match.Groups[3].Value),

View File

@@ -66,7 +66,7 @@ public class QfodParser : IParser<QfodRecord>
public bool CanParse(string line) => QfodPattern.IsMatch(line);
public ParseResult<QfodRecord> Parse(string line, int lineNumber, Guid logFileId)
public ParseResult<QfodRecord> Parse(string line, Guid scenarioId)
{
var match = QfodPattern.Match(line);
if (!match.Success)
@@ -81,8 +81,7 @@ public class QfodParser : IParser<QfodRecord>
{
var record = new QfodRecord
{
LogFileId = logFileId,
LineNumber = lineNumber,
ScenarioId = scenarioId,
ChargerIndex = byte.Parse(match.Groups[1].Value),
CoilIndex = byte.Parse(match.Groups[2].Value),
DeltaQ = int.Parse(match.Groups[3].Value),

View File

@@ -8,6 +8,7 @@
| 文档 | 说明 |
|------|------|
| [文件名解析器](./FileNameParser.md) | 从文件名提取场景信息 |
| [Qfod 解析器](./Qfod解析器.md) | Qfod 格式日志解析设计 |
| [Ploss 解析器](./Ploss解析器.md) | Ploss FOD 格式日志解析设计 |
@@ -29,7 +30,9 @@ public interface IParser<TRecord>
/// <summary>
/// 解析日志行
/// </summary>
ParseResult<TRecord> Parse(string line, int lineNumber, Guid logFileId);
/// <param name="line">日志行内容</param>
/// <param name="scenarioId">关联的测试场景ID用于外键关联由 ParseService 从 FileNameParser 获取后传入)</param>
ParseResult<TRecord> Parse(string line, Guid scenarioId);
}
public record ParseResult<TRecord>
@@ -75,4 +78,5 @@ flowchart TD
|-----------|------|----------|
| FORMAT_MISMATCH | 行格式不匹配正则表达式 | 跳过该行,记录原始内容 |
| TYPE_CONVERSION | 字段类型转换失败 | 跳过该行,记录错误字段 |
| VALUE_OUT_OF_RANGE | 字段值超出有效范围 | 使用边界值或跳过 |
> **说明**VALUE_OUT_OF_RANGE 错误类型暂未实现,后续可根据需要添加字段值范围校验逻辑。