# 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) | 相同属性组合不重复创建 | > **关于 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 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 QfodRecords { get; set; } = []; public ICollection PlossRecords { get; set; } = []; } ``` ### 配置类 ```csharp // Data/Configurations/TestScenarioConfiguration.cs using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using WCTDataMiner.Models; namespace WCTDataMiner.Data.Configurations; public class TestScenarioConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder 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. 场景创建流程 ### 从文件名解析 ```csharp // Services/ScenarioService.cs public async Task GetOrCreateScenarioAsync(ScenarioInfo info) { // 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 existing = await _context.TestScenarios .FirstOrDefaultAsync(s => s.TxPanelId == txPanel.Id && s.TxHardwareId == txHardware.Id && s.TxSoftwareId == txSoftware.Id && s.RxTypeId == rxType.Id && s.TestPurpose == info.TestPurpose && s.TestDate == info.TestDate && s.TestSequence == info.TestSequence); if (existing != null) return existing; // 3. 创建新场景 var scenario = new TestScenario { TxPanelId = txPanel.Id, TxHardwareId = txHardware.Id, TxSoftwareId = txSoftware.Id, RxTypeId = rxType.Id, TestPurpose = info.TestPurpose, TestDate = info.TestDate, TestSequence = info.TestSequence }; _context.TestScenarios.Add(scenario); await _context.SaveChangesAsync(); return scenario; } ``` ### 手动创建 ```csharp public async Task 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; } ```