bugfix: 修复已知bug

This commit is contained in:
ssss
2026-07-03 18:06:01 +08:00
parent 4bff6e1e87
commit d4ea6b454f
8 changed files with 537 additions and 185 deletions

View File

@@ -5,65 +5,95 @@
---
## 1. 日志格式
**格式:** `FOD-> (1) X X X (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13)`
**示例:** `FOD-> 4 2 1 127 10690 12013 35377 9904 1213 -2882 750 0 0 1 500 1`
> 括号数字表示有效字段编号X 表示忽略该位置数据
```
pow_loss = 3053, delta_p = 1950
FOD-> (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13) (14) (15) (16)
```
---
## 2. 字段定义
### 2.1 第一行字段
| 编号 | 字段名称 | 数据类型 | 说明 |
|------|----------|----------|------|
| (1) | RxType | byte | 接收端类型标识 |
| (2) | RxPower | short | 接收端功率mW |
| (3) | TxPower | short | 发射端功率mW |
| (4) | Vcoil | short | 发射线圈电压 |
| (5) | Vin | short | 输入电压 |
| (6) | Isns | short | 输入电流采样值 |
| (7) | Ploss | int | **核心值**:计算损耗(负数表示安全) |
| (8) | Threshold | int | **安全红线**:当前功率段阈值 |
| (9) | TriggerCount | short | 触发计数器 |
| (10) | FodResult | short | FOD判定结果状态 |
| (11) | ProtocolType | byte | 充电协议类型 |
| (12) | PwmDuty | short | PWM控制占空比 |
| (13) | CoilIndex | byte | 充电线圈索引 |
| (A) | PowLoss | int | 功率损耗值 |
| (B) | DeltaP | int | 功率差值 |
### 2.2 第二行字段 (FOD->)
| 编号 | 字段名称 | 数据类型 | 说明 |
|------|----------|----------|------|
| (1) | Field1 | int | 待定义 |
| (2) | Field2 | int | 待定义 |
| (3) | Field3 | int | 待定义 |
| (4) | Field4 | int | 待定义 |
| (5) | Field5 | int | 待定义 |
| (6) | Field6 | int | 待定义 |
| (7) | Field7 | int | 待定义 |
| (8) | Field8 | int | 待定义 |
| (9) | Field9 | int | 待定义 |
| (10) | Field10 | int | 待定义 |
| (11) | Field11 | int | 待定义 |
| (12) | Field12 | int | 待定义 |
| (13) | Field13 | int | 待定义 |
| (14) | Field14 | int | 待定义 |
| (15) | Field15 | int | 待定义 |
| (16) | Field16 | int | 待定义 |
---
## 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+)\s+(\d+)$",
// 第一行格式: pow_loss = 3053, delta_p = 1950
private static readonly Regex HeaderPattern = new(
@"^pow_loss\s*=\s*(\d+),\s*delta_p\s*=\s*(\d+)$",
RegexOptions.Compiled
);
// 第二行格式: FOD-> 后跟16个数字均支持负数
private static readonly Regex FodPattern = 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+)\s+(-?\d+)$",
RegexOptions.Compiled
);
```
> **说明**所有16个字段均支持负数使用 `-?\d+` 匹配。
**捕获组映射:**
**第一行 (HeaderPattern)**
| 捕获组 | 字段 | 类型转换 |
|--------|------|----------|
| Group 1 | RxType | byte.Parse() |
| Group 2 | RxPower | short.Parse() |
| Group 3 | TxPower | short.Parse() |
| Group 4 | Vcoil | short.Parse() |
| Group 5 | Vin | short.Parse() |
| Group 6 | Isns | short.Parse() |
| Group 7 | Ploss | int.Parse() |
| Group 8 | Threshold | int.Parse() |
| Group 9 | TriggerCount | short.Parse() |
| Group 10 | FodResult | short.Parse() |
| Group 11 | ProtocolType | byte.Parse() |
| Group 12 | PwmDuty | short.Parse() |
| Group 13 | CoilIndex | byte.Parse() |
| Group 1 | PowLoss | int.Parse() |
| Group 2 | DeltaP | int.Parse() |
**第二行 (FodPattern)**
| 捕获组 | 字段 | 类型转换 |
|--------|------|----------|
| Group 1 | Field1 | int.Parse() |
| Group 2 | Field2 | int.Parse() |
| Group 3 | Field3 | int.Parse() |
| Group 4 | Field4 | int.Parse() |
| Group 5 | Field5 | int.Parse() |
| Group 6 | Field6 | int.Parse() |
| Group 7 | Field7 | int.Parse() |
| Group 8 | Field8 | int.Parse() |
| Group 9 | Field9 | int.Parse() |
| Group 10 | Field10 | int.Parse() |
| Group 11 | Field11 | int.Parse() |
| Group 12 | Field12 | int.Parse() |
| Group 13 | Field13 | int.Parse() |
| Group 14 | Field14 | int.Parse() |
| Group 15 | Field15 | int.Parse() |
| Group 16 | Field16 | int.Parse() |
---
@@ -78,57 +108,102 @@ namespace WCTDataMiner.Parsers;
public class PlossParser : IParser<PlossRecord>
{
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+)$",
private static readonly Regex HeaderPattern = new(
@"^pow_loss\s*=\s*(\d+),\s*delta_p\s*=\s*(\d+)$",
RegexOptions.Compiled
);
public bool CanParse(string line) => PlossPattern.IsMatch(line);
private static readonly Regex FodPattern = 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+)\s+(-?\d+)$",
RegexOptions.Compiled
);
public bool CanParse(string line) => HeaderPattern.IsMatch(line) || FodPattern.IsMatch(line);
public ParseResult<PlossRecord> Parse(string line, Guid scenarioId)
{
var match = PlossPattern.Match(line);
if (!match.Success)
// 尝试匹配第一行
var headerMatch = HeaderPattern.Match(line);
if (headerMatch.Success)
{
return ParseHeaderLine(headerMatch, scenarioId);
}
// 尝试匹配第二行
var fodMatch = FodPattern.Match(line);
if (fodMatch.Success)
{
return ParseFodLine(fodMatch, scenarioId);
}
return ParseResult<PlossRecord>.Failure(
"FORMAT_MISMATCH",
"Line does not match Ploss pattern"
);
}
private ParseResult<PlossRecord> ParseHeaderLine(Match match, Guid scenarioId)
{
try
{
var record = new PlossRecord
{
ScenarioId = scenarioId,
RxType = byte.Parse(match.Groups[1].Value),
RxPower = short.Parse(match.Groups[2].Value),
TxPower = short.Parse(match.Groups[3].Value),
Vcoil = short.Parse(match.Groups[4].Value),
Vin = short.Parse(match.Groups[5].Value),
Isns = short.Parse(match.Groups[6].Value),
Ploss = int.Parse(match.Groups[7].Value),
Threshold = int.Parse(match.Groups[8].Value),
TriggerCount = short.Parse(match.Groups[9].Value),
FodResult = short.Parse(match.Groups[10].Value),
ProtocolType = byte.Parse(match.Groups[11].Value),
PwmDuty = short.Parse(match.Groups[12].Value),
CoilIndex = byte.Parse(match.Groups[13].Value)
PowLoss = int.Parse(match.Groups[1].Value),
DeltaP = int.Parse(match.Groups[2].Value),
IsHeaderLine = true
};
return ParseResult<PlossRecord>.Success(record);
}
catch (Exception ex)
{
return ParseResult<PlossRecord>.Failure(
"TYPE_CONVERSION",
$"Failed to convert field: {ex.Message}"
$"Failed to convert header field: {ex.Message}"
);
}
}
private ParseResult<PlossRecord> ParseFodLine(Match match, Guid scenarioId)
{
try
{
var record = new PlossRecord
{
ScenarioId = scenarioId,
Field1 = int.Parse(match.Groups[1].Value),
Field2 = int.Parse(match.Groups[2].Value),
Field3 = int.Parse(match.Groups[3].Value),
Field4 = int.Parse(match.Groups[4].Value),
Field5 = int.Parse(match.Groups[5].Value),
Field6 = int.Parse(match.Groups[6].Value),
Field7 = int.Parse(match.Groups[7].Value),
Field8 = int.Parse(match.Groups[8].Value),
Field9 = int.Parse(match.Groups[9].Value),
Field10 = int.Parse(match.Groups[10].Value),
Field11 = int.Parse(match.Groups[11].Value),
Field12 = int.Parse(match.Groups[12].Value),
Field13 = int.Parse(match.Groups[13].Value),
Field14 = int.Parse(match.Groups[14].Value),
Field15 = int.Parse(match.Groups[15].Value),
Field16 = int.Parse(match.Groups[16].Value),
IsHeaderLine = false
};
return ParseResult<PlossRecord>.Success(record);
}
catch (Exception ex)
{
return ParseResult<PlossRecord>.Failure(
"TYPE_CONVERSION",
$"Failed to convert FOD field: {ex.Message}"
);
}
}
}
```
> **注意**:实际实现中,两行日志需要合并为一条完整记录。可以使用 `PlossRecordMerger` 或在业务逻辑层处理合并。
---
## 5. 核心判定逻辑

View File

@@ -17,59 +17,84 @@ public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
.IsRequired()
.HasColumnName("scenario_id");
builder.Property(e => e.RxType)
.IsRequired()
.HasColumnName("rx_type");
// ===== 第一行字段nullable =====
builder.Property(e => e.PowLoss)
.IsRequired(false)
.HasColumnName("pow_loss");
builder.Property(e => e.RxPower)
.IsRequired()
.HasColumnName("rx_power");
builder.Property(e => e.DeltaP)
.IsRequired(false)
.HasColumnName("delta_p");
builder.Property(e => e.TxPower)
// ===== 第二行字段(通用命名) =====
builder.Property(e => e.Field1)
.IsRequired()
.HasColumnName("tx_power");
.HasColumnName("field_1");
builder.Property(e => e.Vcoil)
builder.Property(e => e.Field2)
.IsRequired()
.HasColumnName("vcoil");
.HasColumnName("field_2");
builder.Property(e => e.Vin)
builder.Property(e => e.Field3)
.IsRequired()
.HasColumnName("vin");
.HasColumnName("field_3");
builder.Property(e => e.Isns)
builder.Property(e => e.Field4)
.IsRequired()
.HasColumnName("isns");
.HasColumnName("field_4");
builder.Property(e => e.Ploss)
builder.Property(e => e.Field5)
.IsRequired()
.HasColumnName("ploss");
.HasColumnName("field_5");
builder.Property(e => e.Threshold)
builder.Property(e => e.Field6)
.IsRequired()
.HasColumnName("threshold");
.HasColumnName("field_6");
builder.Property(e => e.TriggerCount)
builder.Property(e => e.Field7)
.IsRequired()
.HasColumnName("field_7");
builder.Property(e => e.Field8)
.IsRequired()
.HasColumnName("field_8");
builder.Property(e => e.Field9)
.IsRequired()
.HasDefaultValue(0)
.HasColumnName("trigger_count");
.HasColumnName("field_9");
builder.Property(e => e.FodResult)
builder.Property(e => e.Field10)
.IsRequired()
.HasDefaultValue(0)
.HasColumnName("fod_result");
.HasColumnName("field_10");
builder.Property(e => e.ProtocolType)
builder.Property(e => e.Field11)
.IsRequired()
.HasColumnName("protocol_type");
.HasColumnName("field_11");
builder.Property(e => e.PwmDuty)
builder.Property(e => e.Field12)
.IsRequired()
.HasColumnName("pwm_duty");
.HasColumnName("field_12");
builder.Property(e => e.CoilIndex)
builder.Property(e => e.Field13)
.IsRequired()
.HasColumnName("coil_index");
.HasColumnName("field_13");
builder.Property(e => e.Field14)
.IsRequired()
.HasDefaultValue(0)
.HasColumnName("field_14");
builder.Property(e => e.Field15)
.IsRequired()
.HasDefaultValue(0)
.HasColumnName("field_15");
builder.Property(e => e.Field16)
.IsRequired()
.HasDefaultValue(0)
.HasColumnName("field_16");
builder.Property(e => e.IsDeleted)
.IsRequired()
@@ -86,10 +111,12 @@ public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
// 索引
builder.HasIndex(e => e.ScenarioId).HasDatabaseName("idx_ploss_scenario");
builder.HasIndex(e => e.RxPower).HasDatabaseName("idx_ploss_rx_power");
builder.HasIndex(e => e.Ploss).HasDatabaseName("idx_ploss_ploss");
builder.HasIndex(e => e.Threshold).HasDatabaseName("idx_ploss_threshold");
builder.HasIndex(e => e.FodResult).HasDatabaseName("idx_ploss_fod_result");
builder.HasIndex(e => e.Field2).HasDatabaseName("idx_ploss_field_2"); // 原 rx_power
builder.HasIndex(e => e.Field7).HasDatabaseName("idx_ploss_field_7"); // 原 ploss
builder.HasIndex(e => e.Field8).HasDatabaseName("idx_ploss_field_8"); // 原 threshold
builder.HasIndex(e => e.Field10).HasDatabaseName("idx_ploss_field_10"); // 原 fod_result
builder.HasIndex(e => e.PowLoss).HasDatabaseName("idx_ploss_pow_loss"); // 新增
builder.HasIndex(e => e.DeltaP).HasDatabaseName("idx_ploss_delta_p"); // 新增
// 关系配置
builder.HasOne(e => e.Scenario)

View File

@@ -2,35 +2,51 @@ namespace WCTDataMiner.Core.Models;
/// <summary>
/// Ploss记录实体 - 存储Ploss FOD格式解析数据
/// 支持两行格式:
/// 第一行: pow_loss = X, delta_p = Y
/// 第二行: FOD-> (16个字段)
/// </summary>
public class PlossRecord
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid ScenarioId { get; set; }
public byte RxType { get; set; }
public int RxPower { get; set; } // uint16 范围 0-65535使用 int 存储
public int TxPower { get; set; } // uint16 范围 0-65535使用 int 存储
public int Vcoil { get; set; } // uint16 范围 0-65535使用 int 存储
public int Vin { get; set; } // uint16 范围 0-65535使用 int 存储
public int Isns { get; set; } // uint16 范围 0-65535使用 int 存储
/// <summary>核心判定值:负数表示安全</summary>
public int Ploss { get; set; }
// ===== 第一行字段nullable兼容旧数据 =====
/// <summary>功率损耗值(来自第一行)</summary>
public int? PowLoss { get; set; }
/// <summary>安全红线:当前功率段阈值</summary>
public int Threshold { get; set; }
/// <summary>功率差值(来自第一行)</summary>
public int? DeltaP { get; set; }
// ===== 第二行字段16个通用字段均支持负数 =====
public int Field1 { get; set; } // 原 RxType
public int Field2 { get; set; } // 原 RxPower
public int Field3 { get; set; } // 原 TxPower
public int Field4 { get; set; } // 原 Vcoil
public int Field5 { get; set; } // 原 Vin
public int Field6 { get; set; } // 原 Isns
/// <summary>核心判定值Field7 (原Ploss) - 负数表示安全</summary>
public int Field7 { get; set; }
/// <summary>安全红线Field8 (原Threshold) - 当前功率段阈值</summary>
public int Field8 { get; set; }
public int Field9 { get; set; } // 原 TriggerCount
public int Field10 { get; set; } // 原 FodResult
public int Field11 { get; set; } // 原 ProtocolType
public int Field12 { get; set; } // 原 PwmDuty
public int Field13 { get; set; } // 原 CoilIndex
public int Field14 { get; set; } // 新增
public int Field15 { get; set; } // 新增
public int Field16 { get; set; } // 新增
public int TriggerCount { get; set; } = 0;
public int FodResult { get; set; } = 0;
public byte ProtocolType { get; set; }
public int PwmDuty { get; set; } // uint16 范围 0-65535使用 int 存储
public byte CoilIndex { get; set; }
public bool IsDeleted { get; set; } = false;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation Properties
public TestScenario Scenario { get; set; } = null!;
/// <summary>派生属性:安全余量(不落库)</summary>
public int Margin => Threshold - Ploss;
/// <summary>派生属性:安全余量(不落库) = Field8 - Field7</summary>
public int Margin => Field8 - Field7;
}

View File

@@ -11,11 +11,21 @@ public interface IParser<TRecord>
bool CanParse(string line);
/// <summary>
/// 解析日志行
/// 解析日志行(单行模式)
/// </summary>
/// <param name="line">日志行内容</param>
/// <param name="scenarioId">关联的测试场景ID</param>
ParseResult<TRecord> Parse(string line, Guid scenarioId);
/// <summary>
/// 判断是否为多行记录的起始行
/// </summary>
bool IsMultiLineStart(string line) => false;
/// <summary>
/// 解析多行记录(子类可重写)
/// </summary>
ParseResult<TRecord>? ParseMultiLine(string[] lines, Guid scenarioId) => null;
}
/// <summary>

View File

@@ -4,49 +4,113 @@ using WCTDataMiner.Core.Models;
namespace WCTDataMiner.Core.Parsers;
/// <summary>
/// Ploss FOD日志解析器
/// 格式: FOD-> (1) X X X (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13)
/// 示例: FOD-> 4 2 1 127 10690 12013 35377 9904 1213 -2882 750 0 0 1 500 1
/// Ploss FOD日志解析器 - 支持两行格式
/// 第一行: pow_loss = 3053, delta_p = 1950
/// 第二行: FOD-> (1) (2) ... (16)
/// 同时兼容旧的单行格式
/// </summary>
public class PlossParser : IParser<PlossRecord>
{
// 格式说明有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+)\s+(\d+)$",
// 第一行正则pow_loss = 3053, delta_p = 1950
private static readonly Regex HeaderPattern = new(
@"^pow_loss\s*=\s*(\d+),\s*delta_p\s*=\s*(\d+)$",
RegexOptions.Compiled
);
public bool CanParse(string line) => PlossPattern.IsMatch(line);
// 第二行正则FOD-> 后跟16个数字均支持负数
private static readonly Regex FodPattern = 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+)\s+(-?\d+)$",
RegexOptions.Compiled
);
// 旧格式正则兼容前3个字段被忽略所有捕获组支持负数
private static readonly Regex LegacyPattern = 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+)$",
RegexOptions.Compiled
);
/// <summary>
/// 判断是否可以解析该行(任意一行)
/// </summary>
public bool CanParse(string line) =>
HeaderPattern.IsMatch(line) || FodPattern.IsMatch(line) || LegacyPattern.IsMatch(line);
/// <summary>
/// 判断是否为多行记录的起始行header行
/// </summary>
public bool IsMultiLineStart(string line) => HeaderPattern.IsMatch(line);
/// <summary>
/// 单行解析(兼容旧格式)
/// </summary>
public ParseResult<PlossRecord> Parse(string line, Guid scenarioId)
{
var match = PlossPattern.Match(line);
if (!match.Success)
// 尝试匹配旧格式
var legacyMatch = LegacyPattern.Match(line);
if (legacyMatch.Success)
{
return ParseLegacyFormat(legacyMatch, scenarioId);
}
// 尝试匹配第二行(新格式)
var fodMatch = FodPattern.Match(line);
if (fodMatch.Success)
{
return ParseFodLine(fodMatch, scenarioId);
}
// 尝试匹配第一行
var headerMatch = HeaderPattern.Match(line);
if (headerMatch.Success)
{
return ParseHeaderLine(headerMatch, scenarioId);
}
return ParseResult<PlossRecord>.Failure(
"FORMAT_MISMATCH",
"Line does not match Ploss pattern"
);
}
/// <summary>
/// 多行解析(新两行格式)
/// </summary>
public ParseResult<PlossRecord>? ParseMultiLine(string[] lines, Guid scenarioId)
{
if (lines.Length < 2)
return null;
var headerMatch = HeaderPattern.Match(lines[0].Trim());
var fodMatch = FodPattern.Match(lines[1].Trim());
if (!headerMatch.Success || !fodMatch.Success)
return null;
try
{
var record = new PlossRecord
{
ScenarioId = scenarioId,
RxType = byte.Parse(match.Groups[1].Value),
RxPower = int.Parse(match.Groups[2].Value),
TxPower = int.Parse(match.Groups[3].Value),
Vcoil = int.Parse(match.Groups[4].Value),
Vin = int.Parse(match.Groups[5].Value),
Isns = int.Parse(match.Groups[6].Value),
Ploss = int.Parse(match.Groups[7].Value),
Threshold = int.Parse(match.Groups[8].Value),
TriggerCount = int.Parse(match.Groups[9].Value),
FodResult = int.Parse(match.Groups[10].Value),
ProtocolType = byte.Parse(match.Groups[11].Value),
PwmDuty = int.Parse(match.Groups[12].Value),
CoilIndex = byte.Parse(match.Groups[13].Value)
// 第一行字段
PowLoss = int.Parse(headerMatch.Groups[1].Value),
DeltaP = int.Parse(headerMatch.Groups[2].Value),
// 第二行字段
Field1 = int.Parse(fodMatch.Groups[1].Value),
Field2 = int.Parse(fodMatch.Groups[2].Value),
Field3 = int.Parse(fodMatch.Groups[3].Value),
Field4 = int.Parse(fodMatch.Groups[4].Value),
Field5 = int.Parse(fodMatch.Groups[5].Value),
Field6 = int.Parse(fodMatch.Groups[6].Value),
Field7 = int.Parse(fodMatch.Groups[7].Value),
Field8 = int.Parse(fodMatch.Groups[8].Value),
Field9 = int.Parse(fodMatch.Groups[9].Value), // 支持负数
Field10 = int.Parse(fodMatch.Groups[10].Value),
Field11 = int.Parse(fodMatch.Groups[11].Value),
Field12 = int.Parse(fodMatch.Groups[12].Value),
Field13 = int.Parse(fodMatch.Groups[13].Value),
Field14 = int.Parse(fodMatch.Groups[14].Value),
Field15 = int.Parse(fodMatch.Groups[15].Value),
Field16 = int.Parse(fodMatch.Groups[16].Value)
};
return ParseResult<PlossRecord>.Success(record);
@@ -59,4 +123,106 @@ public class PlossParser : IParser<PlossRecord>
);
}
}
private ParseResult<PlossRecord> ParseLegacyFormat(Match match, Guid scenarioId)
{
// 保持原有逻辑不变,映射到新字段名
try
{
var record = new PlossRecord
{
ScenarioId = scenarioId,
// 旧格式没有第一行字段
PowLoss = null,
DeltaP = null,
// 映射到新字段名
Field1 = int.Parse(match.Groups[1].Value), // RxType
Field2 = int.Parse(match.Groups[2].Value), // RxPower
Field3 = int.Parse(match.Groups[3].Value), // TxPower
Field4 = int.Parse(match.Groups[4].Value), // Vcoil
Field5 = int.Parse(match.Groups[5].Value), // Vin
Field6 = int.Parse(match.Groups[6].Value), // Isns
Field7 = int.Parse(match.Groups[7].Value), // Ploss
Field8 = int.Parse(match.Groups[8].Value), // Threshold
Field9 = int.Parse(match.Groups[9].Value), // TriggerCount
Field10 = int.Parse(match.Groups[10].Value), // FodResult
Field11 = int.Parse(match.Groups[11].Value), // ProtocolType
Field12 = int.Parse(match.Groups[12].Value), // PwmDuty
Field13 = int.Parse(match.Groups[13].Value), // CoilIndex
// 旧格式没有Field14-16使用默认值
Field14 = 0,
Field15 = 0,
Field16 = 0
};
return ParseResult<PlossRecord>.Success(record);
}
catch (Exception ex)
{
return ParseResult<PlossRecord>.Failure(
"TYPE_CONVERSION",
$"Failed to convert field: {ex.Message}"
);
}
}
private ParseResult<PlossRecord> ParseHeaderLine(Match match, Guid scenarioId)
{
// 单独解析header行返回部分记录
// 实际使用时应通过ParseMultiLine合并
try
{
var record = new PlossRecord
{
ScenarioId = scenarioId,
PowLoss = int.Parse(match.Groups[1].Value),
DeltaP = int.Parse(match.Groups[2].Value)
};
return ParseResult<PlossRecord>.Success(record);
}
catch (Exception ex)
{
return ParseResult<PlossRecord>.Failure(
"TYPE_CONVERSION",
$"Failed to convert header field: {ex.Message}"
);
}
}
private ParseResult<PlossRecord> ParseFodLine(Match match, Guid scenarioId)
{
// 单独解析FOD行新格式无header
try
{
var record = new PlossRecord
{
ScenarioId = scenarioId,
PowLoss = null,
DeltaP = null,
Field1 = byte.Parse(match.Groups[1].Value),
Field2 = int.Parse(match.Groups[2].Value),
Field3 = int.Parse(match.Groups[3].Value),
Field4 = int.Parse(match.Groups[4].Value),
Field5 = int.Parse(match.Groups[5].Value),
Field6 = int.Parse(match.Groups[6].Value),
Field7 = int.Parse(match.Groups[7].Value),
Field8 = int.Parse(match.Groups[8].Value),
Field9 = int.Parse(match.Groups[9].Value),
Field10 = int.Parse(match.Groups[10].Value),
Field11 = int.Parse(match.Groups[11].Value),
Field12 = int.Parse(match.Groups[12].Value),
Field13 = int.Parse(match.Groups[13].Value),
Field14 = int.Parse(match.Groups[14].Value),
Field15 = int.Parse(match.Groups[15].Value),
Field16 = int.Parse(match.Groups[16].Value)
};
return ParseResult<PlossRecord>.Success(record);
}
catch (Exception ex)
{
return ParseResult<PlossRecord>.Failure(
"TYPE_CONVERSION",
$"Failed to convert FOD field: {ex.Message}"
);
}
}
}

View File

@@ -120,8 +120,8 @@ public class ExportService
// 使用流式导出避免内存问题
await using var writer = new StreamWriter(filePath, false, Encoding.UTF8);
// CSV Header
await writer.WriteLineAsync("id,scenario_id,tx_panel,tx_hardware,tx_software,rx_type,test_purpose,test_date,test_sequence,rx_type_field,rx_power,tx_power,vcoil,vin,isns,ploss,threshold,trigger_count,fod_result,protocol_type,pwm_duty,coil_index,margin");
// CSV Header(包含新字段)
await writer.WriteLineAsync("id,scenario_id,tx_panel,tx_hardware,tx_software,rx_type,test_purpose,test_date,test_sequence,pow_loss,delta_p,field_1,field_2,field_3,field_4,field_5,field_6,field_7,field_8,field_9,field_10,field_11,field_12,field_13,field_14,field_15,field_16,margin");
// 使用 Select 投影避免 N+1 查询
var query = _context.PlossRecords
@@ -136,20 +136,25 @@ public class ExportService
r.Scenario.TestPurpose,
r.Scenario.TestDate,
r.Scenario.TestSequence,
r.RxType,
r.RxPower,
r.TxPower,
r.Vcoil,
r.Vin,
r.Isns,
r.Ploss,
r.Threshold,
r.TriggerCount,
r.FodResult,
r.ProtocolType,
r.PwmDuty,
r.CoilIndex,
Margin = r.Threshold - r.Ploss
r.PowLoss,
r.DeltaP,
r.Field1,
r.Field2,
r.Field3,
r.Field4,
r.Field5,
r.Field6,
r.Field7,
r.Field8,
r.Field9,
r.Field10,
r.Field11,
r.Field12,
r.Field13,
r.Field14,
r.Field15,
r.Field16,
Margin = r.Field8 - r.Field7
})
.OrderBy(r => r.Id);
@@ -166,19 +171,24 @@ public class ExportService
EscapeCsvField(r.TestPurpose),
EscapeCsvField(r.TestDate),
EscapeCsvField(r.TestSequence),
EscapeCsvField(r.RxType),
EscapeCsvField(r.RxPower),
EscapeCsvField(r.TxPower),
EscapeCsvField(r.Vcoil),
EscapeCsvField(r.Vin),
EscapeCsvField(r.Isns),
EscapeCsvField(r.Ploss),
EscapeCsvField(r.Threshold),
EscapeCsvField(r.TriggerCount),
EscapeCsvField(r.FodResult),
EscapeCsvField(r.ProtocolType),
EscapeCsvField(r.PwmDuty),
EscapeCsvField(r.CoilIndex),
EscapeCsvField(r.PowLoss),
EscapeCsvField(r.DeltaP),
EscapeCsvField(r.Field1),
EscapeCsvField(r.Field2),
EscapeCsvField(r.Field3),
EscapeCsvField(r.Field4),
EscapeCsvField(r.Field5),
EscapeCsvField(r.Field6),
EscapeCsvField(r.Field7),
EscapeCsvField(r.Field8),
EscapeCsvField(r.Field9),
EscapeCsvField(r.Field10),
EscapeCsvField(r.Field11),
EscapeCsvField(r.Field12),
EscapeCsvField(r.Field13),
EscapeCsvField(r.Field14),
EscapeCsvField(r.Field15),
EscapeCsvField(r.Field16),
EscapeCsvField(r.Margin)
);
await writer.WriteLineAsync(line);
@@ -265,20 +275,25 @@ public class ExportService
r.Scenario.TestPurpose,
TestDate = r.Scenario.TestDate.ToString("yyyy-MM-dd"),
r.Scenario.TestSequence,
RecordRxType = r.RxType,
r.RxPower,
r.TxPower,
r.Vcoil,
r.Vin,
r.Isns,
r.Ploss,
r.Threshold,
r.TriggerCount,
r.FodResult,
r.ProtocolType,
r.PwmDuty,
r.CoilIndex,
Margin = r.Threshold - r.Ploss
r.PowLoss,
r.DeltaP,
r.Field1,
r.Field2,
r.Field3,
r.Field4,
r.Field5,
r.Field6,
r.Field7,
r.Field8,
r.Field9,
r.Field10,
r.Field11,
r.Field12,
r.Field13,
r.Field14,
r.Field15,
r.Field16,
Margin = r.Field8 - r.Field7
})
.ToListAsync();

View File

@@ -71,6 +71,10 @@ public class ParseService
var plossRecords = new List<PlossRecord>();
int errorCount = 0;
// 用于暂存多行解析的header行
string? pendingHeaderLine = null;
int pendingHeaderLineNumber = 0;
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i].Trim();
@@ -90,8 +94,35 @@ public class ParseService
errorCount++;
}
}
else if (_plossParser.IsMultiLineStart(line))
{
// 检测到两行格式的header行暂存
pendingHeaderLine = line;
pendingHeaderLineNumber = lineNumber;
}
else if (_plossParser.CanParse(line))
{
if (pendingHeaderLine != null)
{
// 两行格式:合并解析
var result = _plossParser.ParseMultiLine(
new[] { pendingHeaderLine, line }, scenario.Id);
if (result != null && result.IsSuccess)
{
plossRecords.Add(result.Record!);
}
else
{
_logger.LogError("Ploss两行解析失败 [{Line}] {Error}: {Message}",
pendingHeaderLineNumber, result?.ErrorType, result?.ErrorMessage);
errorCount++;
}
pendingHeaderLine = null;
}
else
{
// 单行格式(旧格式兼容)
var result = _plossParser.Parse(line, scenario.Id);
if (result.IsSuccess)
plossRecords.Add(result.Record!);
@@ -103,6 +134,15 @@ public class ParseService
}
}
}
}
// 处理末尾遗留的header行异常情况
if (pendingHeaderLine != null)
{
_logger.LogWarning("发现未匹配的Ploss header行 [{Line}]: {Content}",
pendingHeaderLineNumber, pendingHeaderLine);
errorCount++;
}
// 批量保存
await BatchInsertAsync(qfodRecords, plossRecords);

View File

@@ -18,6 +18,9 @@
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<!-- EF Core Tools -->
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
<!-- Logging -->
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />