2026-07-02 15:53:30 +08:00
|
|
|
|
# test_scenario 实体
|
|
|
|
|
|
|
|
|
|
|
|
> 所属模块:[数据模型概览](../概览.md)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 表定义
|
|
|
|
|
|
|
|
|
|
|
|
**表名**:`test_scenario`
|
|
|
|
|
|
|
|
|
|
|
|
**描述**:存储测试场景,通过外键关联各维度表,test_purpose 直接存储
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 字段定义
|
|
|
|
|
|
|
|
|
|
|
|
| 列名 | 类型 | 约束 | 说明 |
|
|
|
|
|
|
|------|------|------|------|
|
|
|
|
|
|
| id | UUID | PK, NOT NULL | 主键,自动生成 |
|
|
|
|
|
|
| tx_panel_id | UUID | FK, NOT NULL | 关联TX面板维度 |
|
|
|
|
|
|
| tx_hardware_id | UUID | FK, NOT NULL | 关联TX硬件版本维度 |
|
|
|
|
|
|
| tx_software_id | UUID | FK, NOT NULL | 关联TX软件版本维度 |
|
|
|
|
|
|
| rx_type_id | UUID | FK, NOT NULL | 关联RX类型维度 |
|
|
|
|
|
|
| test_purpose | VARCHAR(200) | NULL | 测试目的(直接存储) |
|
|
|
|
|
|
| test_date | DATE | NOT NULL | 测试日期 |
|
|
|
|
|
|
| test_sequence | INT | NOT NULL, DEFAULT 1 | 测试次数序号 |
|
|
|
|
|
|
| qfod_count | INT | NOT NULL, DEFAULT 0 | 包含的 Qfod 记录数 |
|
|
|
|
|
|
| ploss_count | INT | NOT NULL, DEFAULT 0 | 包含的 Ploss 记录数 |
|
|
|
|
|
|
| is_deleted | BOOLEAN | NOT NULL, DEFAULT FALSE | 软删除标记 |
|
|
|
|
|
|
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 入库时间 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 3. 外键
|
|
|
|
|
|
|
|
|
|
|
|
| 外键 | 引用表 | 说明 |
|
|
|
|
|
|
|------|--------|------|
|
|
|
|
|
|
| tx_panel_id | tx_panel.id | 关联TX面板维度(必需) |
|
|
|
|
|
|
| tx_hardware_id | tx_hardware.id | 关联TX硬件版本维度(必需) |
|
|
|
|
|
|
| tx_software_id | tx_software.id | 关联TX软件版本维度(必需) |
|
|
|
|
|
|
| rx_type_id | rx_type.id | 关联RX类型维度(必需) |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 4. 索引
|
|
|
|
|
|
|
|
|
|
|
|
| 索引名 | 字段 | 用途 |
|
|
|
|
|
|
|--------|------|------|
|
|
|
|
|
|
| idx_scenario_tx_panel | tx_panel_id | 按面板类型筛选 |
|
|
|
|
|
|
| idx_scenario_tx_hardware | tx_hardware_id | 按硬件版本筛选 |
|
|
|
|
|
|
| idx_scenario_tx_software | tx_software_id | 按软件版本筛选 |
|
|
|
|
|
|
| idx_scenario_rx_type | rx_type_id | 按RX类型筛选 |
|
|
|
|
|
|
| idx_scenario_test_date | test_date | 按测试日期查询 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 5. 唯一约束
|
|
|
|
|
|
|
|
|
|
|
|
| 约束名 | 字段组合 | 说明 |
|
|
|
|
|
|
|--------|----------|------|
|
|
|
|
|
|
| uq_scenario | (tx_panel_id, tx_hardware_id, tx_software_id, rx_type_id, test_purpose, test_date, test_sequence) | 相同属性组合不重复创建 |
|
|
|
|
|
|
|
2026-07-02 18:13:56 +08:00
|
|
|
|
> **关于 test_purpose 的 NULL 处理**:由于 `test_purpose` 是可空字段,含 NULL 的唯一约束在不同数据库中行为不同:
|
|
|
|
|
|
> - **PostgreSQL**:默认允许多个 NULL(被视为不同值),可通过 `NULLS NOT DISTINCT` 约束行为
|
|
|
|
|
|
> - **SQL Server**:只允许一个 NULL
|
|
|
|
|
|
> - **SQLite/MySQL**:允多个 NULL
|
|
|
|
|
|
>
|
|
|
|
|
|
> 若需严格幂等(无论 test_purpose 是否为 NULL 都不重复),建议使用 filtered index 或将空值转为默认字符串(如 `(empty)`)。
|
|
|
|
|
|
|
2026-07-02 15:53:30 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 6. EF Core 模型
|
|
|
|
|
|
|
2026-07-02 18:13:56 +08:00
|
|
|
|
### 实体类
|
|
|
|
|
|
|
2026-07-02 15:53:30 +08:00
|
|
|
|
```csharp
|
|
|
|
|
|
// Models/TestScenario.cs
|
|
|
|
|
|
namespace WCTDataMiner.Models;
|
|
|
|
|
|
|
|
|
|
|
|
public class TestScenario
|
|
|
|
|
|
{
|
|
|
|
|
|
public Guid Id { get; set; } = Guid.NewGuid();
|
|
|
|
|
|
public Guid TxPanelId { get; set; }
|
|
|
|
|
|
public Guid TxHardwareId { get; set; }
|
|
|
|
|
|
public Guid TxSoftwareId { get; set; }
|
|
|
|
|
|
public Guid RxTypeId { get; set; }
|
|
|
|
|
|
public string? TestPurpose { get; set; }
|
|
|
|
|
|
public DateOnly TestDate { get; set; }
|
|
|
|
|
|
public int TestSequence { get; set; } = 1;
|
|
|
|
|
|
public int QfodCount { get; set; } = 0;
|
|
|
|
|
|
public int PlossCount { get; set; } = 0;
|
|
|
|
|
|
public bool IsDeleted { get; set; } = false;
|
|
|
|
|
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
|
|
|
|
|
|
|
|
|
|
// Navigation Properties - 维度表
|
|
|
|
|
|
public TxPanel TxPanel { get; set; } = null!;
|
|
|
|
|
|
public TxHardware TxHardware { get; set; } = null!;
|
|
|
|
|
|
public TxSoftware TxSoftware { get; set; } = null!;
|
|
|
|
|
|
public RxType RxType { get; set; } = null!;
|
|
|
|
|
|
|
|
|
|
|
|
// Navigation Properties - 数据记录
|
|
|
|
|
|
public ICollection<QfodRecord> QfodRecords { get; set; } = [];
|
|
|
|
|
|
public ICollection<PlossRecord> PlossRecords { get; set; } = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-02 18:13:56 +08:00
|
|
|
|
### 配置类
|
|
|
|
|
|
|
|
|
|
|
|
```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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-02 15:53:30 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 7. 场景创建流程
|
|
|
|
|
|
|
|
|
|
|
|
### 从文件名解析
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
// Services/ScenarioService.cs
|
2026-07-02 18:13:56 +08:00
|
|
|
|
public async Task<TestScenario> GetOrCreateScenarioAsync(ScenarioInfo info)
|
2026-07-02 15:53:30 +08:00
|
|
|
|
{
|
2026-07-02 18:13:56 +08:00
|
|
|
|
// 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);
|
2026-07-02 15:53:30 +08:00
|
|
|
|
|
2026-07-02 18:13:56 +08:00
|
|
|
|
// 2. 检查场景是否已存在(利用唯一约束)
|
|
|
|
|
|
var existing = await _context.TestScenarios
|
2026-07-02 15:53:30 +08:00
|
|
|
|
.FirstOrDefaultAsync(s =>
|
|
|
|
|
|
s.TxPanelId == txPanel.Id &&
|
|
|
|
|
|
s.TxHardwareId == txHardware.Id &&
|
|
|
|
|
|
s.TxSoftwareId == txSoftware.Id &&
|
|
|
|
|
|
s.RxTypeId == rxType.Id &&
|
2026-07-02 18:13:56 +08:00
|
|
|
|
s.TestPurpose == info.TestPurpose &&
|
|
|
|
|
|
s.TestDate == info.TestDate &&
|
|
|
|
|
|
s.TestSequence == info.TestSequence);
|
2026-07-02 15:53:30 +08:00
|
|
|
|
|
|
|
|
|
|
if (existing != null) return existing;
|
|
|
|
|
|
|
2026-07-02 18:13:56 +08:00
|
|
|
|
// 3. 创建新场景
|
2026-07-02 15:53:30 +08:00
|
|
|
|
var scenario = new TestScenario
|
|
|
|
|
|
{
|
|
|
|
|
|
TxPanelId = txPanel.Id,
|
|
|
|
|
|
TxHardwareId = txHardware.Id,
|
|
|
|
|
|
TxSoftwareId = txSoftware.Id,
|
|
|
|
|
|
RxTypeId = rxType.Id,
|
2026-07-02 18:13:56 +08:00
|
|
|
|
TestPurpose = info.TestPurpose,
|
|
|
|
|
|
TestDate = info.TestDate,
|
|
|
|
|
|
TestSequence = info.TestSequence
|
2026-07-02 15:53:30 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-02 18:13:56 +08:00
|
|
|
|
_context.TestScenarios.Add(scenario);
|
|
|
|
|
|
await _context.SaveChangesAsync();
|
2026-07-02 15:53:30 +08:00
|
|
|
|
|
|
|
|
|
|
return scenario;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 手动创建
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
public async Task<TestScenario> CreateAsync(CreateScenarioRequest request)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 1. 验证维度是否存在
|
|
|
|
|
|
var txPanel = await context.TxPanels.FindAsync(request.TxPanelId)
|
|
|
|
|
|
?? throw new NotFoundException("TX面板不存在");
|
|
|
|
|
|
var txHardware = await context.TxHardwares.FindAsync(request.TxHardwareId)
|
|
|
|
|
|
?? throw new NotFoundException("TX硬件版本不存在");
|
|
|
|
|
|
// ... 其他维度验证
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 创建场景
|
|
|
|
|
|
var scenario = new TestScenario
|
|
|
|
|
|
{
|
|
|
|
|
|
TxPanelId = request.TxPanelId,
|
|
|
|
|
|
TxHardwareId = request.TxHardwareId,
|
|
|
|
|
|
TxSoftwareId = request.TxSoftwareId,
|
|
|
|
|
|
RxTypeId = request.RxTypeId,
|
|
|
|
|
|
TestPurpose = request.TestPurpose,
|
|
|
|
|
|
TestDate = request.TestDate,
|
|
|
|
|
|
TestSequence = request.TestSequence
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
context.TestScenarios.Add(scenario);
|
|
|
|
|
|
await context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
|
|
return scenario;
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|