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

@@ -60,84 +60,42 @@
|--------|----------|------|
| uq_scenario | (tx_panel_id, tx_hardware_id, tx_software_id, rx_type_id, test_purpose, test_date, test_sequence) | 相同属性组合不重复创建 |
> **关于 test_purpose 的 NULL 处理**:由于 `test_purpose` 是可空字段,含 NULL 的唯一约束在不同数据库中行为不同:
> - **PostgreSQL**:默认允许多个 NULL被视为不同值可通过 `NULLS NOT DISTINCT` 约束行为
> - **SQL Server**:只允许一个 NULL
> - **SQLite/MySQL**:允多个 NULL
>
> 若需严格幂等(无论 test_purpose 是否为 NULL 都不重复),建议使用 filtered index 或将空值转为默认字符串(如 `(empty)`)。
---
## 6. EF Core 模型
### 实体类
```csharp
// Models/TestScenario.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace WCTDataMiner.Models;
[Table("test_scenario")]
[Index(nameof(TxPanelId), Name = "idx_scenario_tx_panel")]
[Index(nameof(TxHardwareId), Name = "idx_scenario_tx_hardware")]
[Index(nameof(TxSoftwareId), Name = "idx_scenario_tx_software")]
[Index(nameof(RxTypeId), Name = "idx_scenario_rx_type")]
[Index(nameof(TestDate), Name = "idx_scenario_test_date")]
public class TestScenario
{
[Key]
[Column("id")]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[Column("tx_panel_id")]
public Guid TxPanelId { get; set; }
[Required]
[Column("tx_hardware_id")]
public Guid TxHardwareId { get; set; }
[Required]
[Column("tx_software_id")]
public Guid TxSoftwareId { get; set; }
[Required]
[Column("rx_type_id")]
public Guid RxTypeId { get; set; }
[MaxLength(200)]
[Column("test_purpose")]
public string? TestPurpose { get; set; }
[Required]
[Column("test_date")]
public DateOnly TestDate { get; set; }
[Required]
[Column("test_sequence")]
public int TestSequence { get; set; } = 1;
[Required]
[Column("qfod_count")]
public int QfodCount { get; set; } = 0;
[Required]
[Column("ploss_count")]
public int PlossCount { get; set; } = 0;
[Required]
[Column("is_deleted")]
public bool IsDeleted { get; set; } = false;
[Required]
[Column("created_at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation Properties - 维度表
[ForeignKey(nameof(TxPanelId))]
public TxPanel TxPanel { get; set; } = null!;
[ForeignKey(nameof(TxHardwareId))]
public TxHardware TxHardware { get; set; } = null!;
[ForeignKey(nameof(TxSoftwareId))]
public TxSoftware TxSoftware { get; set; } = null!;
[ForeignKey(nameof(RxTypeId))]
public RxType RxType { get; set; } = null!;
// Navigation Properties - 数据记录
@@ -146,6 +104,118 @@ public class TestScenario
}
```
### 配置类
```csharp
// Data/Configurations/TestScenarioConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Models;
namespace WCTDataMiner.Data.Configurations;
public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
{
public void Configure(EntityTypeBuilder<TestScenario> builder)
{
builder.ToTable("test_scenario");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).HasColumnName("id");
builder.Property(e => e.TxPanelId)
.IsRequired()
.HasColumnName("tx_panel_id");
builder.Property(e => e.TxHardwareId)
.IsRequired()
.HasColumnName("tx_hardware_id");
builder.Property(e => e.TxSoftwareId)
.IsRequired()
.HasColumnName("tx_software_id");
builder.Property(e => e.RxTypeId)
.IsRequired()
.HasColumnName("rx_type_id");
builder.Property(e => e.TestPurpose)
.HasMaxLength(200)
.HasColumnName("test_purpose");
builder.Property(e => e.TestDate)
.IsRequired()
.HasColumnName("test_date");
builder.Property(e => e.TestSequence)
.IsRequired()
.HasDefaultValue(1)
.HasColumnName("test_sequence");
builder.Property(e => e.QfodCount)
.IsRequired()
.HasDefaultValue(0)
.HasColumnName("qfod_count");
builder.Property(e => e.PlossCount)
.IsRequired()
.HasDefaultValue(0)
.HasColumnName("ploss_count");
builder.Property(e => e.IsDeleted)
.IsRequired()
.HasDefaultValue(false)
.HasColumnName("is_deleted");
builder.Property(e => e.CreatedAt)
.IsRequired()
.HasDefaultValueSql("NOW()")
.HasColumnName("created_at");
// 索引
builder.HasIndex(e => e.TxPanelId).HasDatabaseName("idx_scenario_tx_panel");
builder.HasIndex(e => e.TxHardwareId).HasDatabaseName("idx_scenario_tx_hardware");
builder.HasIndex(e => e.TxSoftwareId).HasDatabaseName("idx_scenario_tx_software");
builder.HasIndex(e => e.RxTypeId).HasDatabaseName("idx_scenario_rx_type");
builder.HasIndex(e => e.TestDate).HasDatabaseName("idx_scenario_test_date");
// 唯一约束
builder.HasIndex(e => new { e.TxPanelId, e.TxHardwareId, e.TxSoftwareId, e.RxTypeId, e.TestPurpose, e.TestDate, e.TestSequence })
.IsUnique()
.HasDatabaseName("uq_scenario");
// 关系配置
builder.HasOne(e => e.TxPanel)
.WithMany(e => e.TestScenarios)
.HasForeignKey(e => e.TxPanelId)
.IsRequired();
builder.HasOne(e => e.TxHardware)
.WithMany(e => e.TestScenarios)
.HasForeignKey(e => e.TxHardwareId)
.IsRequired();
builder.HasOne(e => e.TxSoftware)
.WithMany(e => e.TestScenarios)
.HasForeignKey(e => e.TxSoftwareId)
.IsRequired();
builder.HasOne(e => e.RxType)
.WithMany(e => e.TestScenarios)
.HasForeignKey(e => e.RxTypeId)
.IsRequired();
builder.HasMany(e => e.QfodRecords)
.WithOne(e => e.Scenario)
.HasForeignKey(e => e.ScenarioId);
builder.HasMany(e => e.PlossRecords)
.WithOne(e => e.Scenario)
.HasForeignKey(e => e.ScenarioId);
}
}
```
---
## 7. 场景创建流程
@@ -154,45 +224,41 @@ public class TestScenario
```csharp
// Services/ScenarioService.cs
public async Task<TestScenario> GetOrCreateFromFileNameAsync(string fileName)
public async Task<TestScenario> GetOrCreateScenarioAsync(ScenarioInfo info)
{
// 1. 解析文件名
var parts = ParseFileName(fileName);
// 格式: TX面板-TX硬件-TX软件-RX类型-测试目的-年月日-测试次数.log
// 1. 获取或创建各维度(通过 DimensionService
var txPanel = await _dimensionService.GetOrCreateTxPanelAsync(info.TxPanel);
var txHardware = await _dimensionService.GetOrCreateTxHardwareAsync(info.TxHardware);
var txSoftware = await _dimensionService.GetOrCreateTxSoftwareAsync(info.TxSoftware);
var rxType = await _dimensionService.GetOrCreateRxTypeAsync(info.RxType);
// 2. 获取或创建各维度
var txPanel = await txPanelService.GetOrCreateAsync(parts.TxPanel);
var txHardware = await txHardwareService.GetOrCreateAsync(parts.TxHardware);
var txSoftware = await txSoftwareService.GetOrCreateAsync(parts.TxSoftware);
var rxType = await rxTypeService.GetOrCreateAsync(parts.RxType);
// 3. 检查场景是否已存在
var existing = await context.TestScenarios
// 2. 检查场景是否已存在(利用唯一约束)
var existing = await _context.TestScenarios
.FirstOrDefaultAsync(s =>
s.TxPanelId == txPanel.Id &&
s.TxHardwareId == txHardware.Id &&
s.TxSoftwareId == txSoftware.Id &&
s.RxTypeId == rxType.Id &&
s.TestPurpose == parts.TestPurpose &&
s.TestDate == parts.TestDate &&
s.TestSequence == parts.TestSequence);
s.TestPurpose == info.TestPurpose &&
s.TestDate == info.TestDate &&
s.TestSequence == info.TestSequence);
if (existing != null) return existing;
// 4. 创建新场景
// 3. 创建新场景
var scenario = new TestScenario
{
TxPanelId = txPanel.Id,
TxHardwareId = txHardware.Id,
TxSoftwareId = txSoftware.Id,
RxTypeId = rxType.Id,
TestPurpose = parts.TestPurpose,
TestDate = parts.TestDate,
TestSequence = parts.TestSequence
TestPurpose = info.TestPurpose,
TestDate = info.TestDate,
TestSequence = info.TestSequence
};
context.TestScenarios.Add(scenario);
await context.SaveChangesAsync();
_context.TestScenarios.Add(scenario);
await _context.SaveChangesAsync();
return scenario;
}