docs: 完善数据模型设计

- 重命名 log_file 为 test_scenario(测试场景)
- 拆分维度表:tx_panel、tx_hardware、tx_software、rx_type
- 采用星型模型设计,消除数据冗余
- 为所有表添加软删除字段 is_deleted
- 更新关系说明,包含全局查询过滤器配置
- 更新 ER 图和查询示例

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ssss
2026-07-02 15:53:30 +08:00
commit 28cb2533a7
23 changed files with 2919 additions and 0 deletions

View File

@@ -0,0 +1,360 @@
# 关系说明
> 所属模块:[数据模型概览](../概览.md)
---
## 1. 关系概览
### 维度表 → 场景表
| 关系 | 类型 | 说明 |
|------|------|------|
| tx_panel → test_scenario | 1:N | 一个面板类型被多个场景引用 |
| tx_hardware → test_scenario | 1:N | 一个硬件版本被多个场景引用 |
| tx_software → test_scenario | 1:N | 一个软件版本被多个场景引用 |
| rx_type → test_scenario | 1:N | 一个RX类型被多个场景引用 |
### 场景表 → 数据表
| 关系 | 类型 | 说明 |
|------|------|------|
| test_scenario → qfod_record | 1:N | 一个场景包含多条 Qfod 记录 |
| test_scenario → ploss_record | 1:N | 一个场景包含多条 Ploss 记录 |
---
## 2. 关系详情
### 2.1 维度表 → test_scenario
```mermaid
erDiagram
tx_panel ||--o{ test_scenario : "引用"
tx_hardware ||--o{ test_scenario : "引用"
tx_software ||--o{ test_scenario : "引用"
rx_type ||--o{ test_scenario : "引用"
```
| 属性 | 说明 |
|------|------|
| 类型 | 一对多1:N |
| 外键 | 场景表存储维度表的外键 |
| 删除行为 | RESTRICT维度被引用时不能删除 |
| 业务含义 | 维度值可被多个场景复用,消除冗余 |
### 2.2 test_scenario → qfod_record
```mermaid
erDiagram
test_scenario ||--o{ qfod_record : "包含"
```
| 属性 | 说明 |
|------|------|
| 类型 | 一对多1:N |
| 外键 | qfod_record.scenario_id → test_scenario.id必需 |
| 删除行为 | CASCADE删除场景时同步删除关联记录 |
| 业务含义 | 一个测试场景产生多条 Qfod 检测数据 |
### 2.3 test_scenario → ploss_record
```mermaid
erDiagram
test_scenario ||--o{ ploss_record : "包含"
```
| 属性 | 说明 |
|------|------|
| 类型 | 一对多1:N |
| 外键 | ploss_record.scenario_id → test_scenario.id必需 |
| 删除行为 | CASCADE删除场景时同步删除关联记录 |
| 业务含义 | 一个测试场景产生多条 Ploss FOD 数据 |
---
## 3. EF Core 配置
```csharp
// Data/WctMinerDbContext.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 维度表 → 场景表 关系配置
modelBuilder.Entity<TestScenario>(entity =>
{
// TX面板
entity.HasOne(e => e.TxPanel)
.WithMany(p => p.TestScenarios)
.HasForeignKey(e => e.TxPanelId)
.OnDelete(DeleteBehavior.Restrict); // 维度被引用时不能删除
// TX硬件版本
entity.HasOne(e => e.TxHardware)
.WithMany(h => h.TestScenarios)
.HasForeignKey(e => e.TxHardwareId)
.OnDelete(DeleteBehavior.Restrict);
// TX软件版本
entity.HasOne(e => e.TxSoftware)
.WithMany(s => s.TestScenarios)
.HasForeignKey(e => e.TxSoftwareId)
.OnDelete(DeleteBehavior.Restrict);
// RX类型
entity.HasOne(e => e.RxType)
.WithMany(r => r.TestScenarios)
.HasForeignKey(e => e.RxTypeId)
.OnDelete(DeleteBehavior.Restrict);
// 唯一约束
entity.HasIndex(e => new {
e.TxPanelId,
e.TxHardwareId,
e.TxSoftwareId,
e.RxTypeId,
e.TestPurpose,
e.TestDate,
e.TestSequence
}).IsUnique();
});
// 场景表 → QfodRecord 关系配置
modelBuilder.Entity<QfodRecord>(entity =>
{
entity.HasOne(e => e.Scenario)
.WithMany(s => s.QfodRecords)
.HasForeignKey(e => e.ScenarioId)
.OnDelete(DeleteBehavior.Cascade); // 删除场景时同步删除记录
});
// 场景表 → PlossRecord 关系配置
modelBuilder.Entity<PlossRecord>(entity =>
{
entity.HasOne(e => e.Scenario)
.WithMany(s => s.PlossRecords)
.HasForeignKey(e => e.ScenarioId)
.OnDelete(DeleteBehavior.Cascade);
});
}
```
---
## 4. 查询示例
### 基础查询
```csharp
// 查询场景及其所有维度属性
var scenario = await context.TestScenarios
.Include(s => s.TxPanel)
.Include(s => s.TxHardware)
.Include(s => s.TxSoftware)
.Include(s => s.RxType)
.FirstOrDefaultAsync(s => s.Id == scenarioId);
// 输出维度属性
Console.WriteLine($"面板: {scenario.TxPanel.Name}");
Console.WriteLine($"硬件: {scenario.TxHardware.Version}");
Console.WriteLine($"软件: {scenario.TxSoftware.Version}");
Console.WriteLine($"RX: {scenario.RxType.Name}");
```
### 按维度筛选
```csharp
// 查询指定面板类型的所有场景
var scenarios = await context.TestScenarios
.Include(s => s.TxPanel)
.Where(s => s.TxPanel.Name == "single-mold")
.ToListAsync();
// 查询指定硬件版本的所有场景
var scenarios = await context.TestScenarios
.Include(s => s.TxHardware)
.Where(s => s.TxHardware.Version == "V1.0")
.ToListAsync();
```
### 按维度分组统计
```csharp
// 按面板类型统计 Qfod 记录数
var statsByPanel = await context.QfodRecords
.Include(r => r.Scenario)
.ThenInclude(s => s.TxPanel)
.GroupBy(r => r.Scenario.TxPanel.Name)
.Select(g => new { TxPanel = g.Key, Count = g.Count() })
.ToListAsync();
// 按硬件版本统计场景数
var statsByHardware = await context.TestScenarios
.Include(s => s.TxHardware)
.GroupBy(s => s.TxHardware.Version)
.Select(g => new { TxHardware = g.Key, ScenarioCount = g.Count() })
.ToListAsync();
// 按RX类型统计 Ploss 记录数
var statsByRx = await context.PlossRecords
.Include(r => r.Scenario)
.ThenInclude(s => s.RxType)
.GroupBy(r => r.Scenario.RxType.Name)
.Select(g => new { RxType = g.Key, Count = g.Count() })
.ToListAsync();
```
### 多维度联合查询
```csharp
// 查询指定面板+硬件+软件的所有场景
var scenarios = await context.TestScenarios
.Include(s => s.TxPanel)
.Include(s => s.TxHardware)
.Include(s => s.TxSoftware)
.Include(s => s.RxType)
.Where(s =>
s.TxPanel.Name == "single-mold" &&
s.TxHardware.Version == "V1.0" &&
s.TxSoftware.Version == "0x1234")
.ToListAsync();
// 查询指定维度组合的所有 Qfod 记录
var records = await context.QfodRecords
.Include(r => r.Scenario)
.ThenInclude(s => s.TxPanel)
.Include(r => r.Scenario)
.ThenInclude(s => s.TxHardware)
.Where(r =>
r.Scenario.TxPanel.Name == "single-mold" &&
r.Scenario.TxHardware.Version == "V1.0")
.ToListAsync();
```
---
## 5. 软删除设计
### 全局查询过滤器
```csharp
// Data/WctMinerDbContext.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 维度表 → 场景表 关系配置
modelBuilder.Entity<TestScenario>(entity =>
{
// ... 其他配置
// 唯一约束(排除已删除记录)
entity.HasIndex(e => new {
e.TxPanelId,
e.TxHardwareId,
e.TxSoftwareId,
e.RxTypeId,
e.TestPurpose,
e.TestDate,
e.TestSequence
}).HasFilter("is_deleted = false").IsUnique();
});
// 全局查询过滤器(自动过滤已删除记录)
modelBuilder.Entity<TxPanel>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<TxHardware>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<TxSoftware>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<RxType>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<TestScenario>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<QfodRecord>().HasQueryFilter(e => !e.IsDeleted);
modelBuilder.Entity<PlossRecord>().HasQueryFilter(e => !e.IsDeleted);
}
```
### 删除行为说明
| 操作 | 行为 | 说明 |
|------|------|------|
| 删除维度 | 软删除 | 设置 is_deleted = true不物理删除 |
| 删除场景 | 软删除 | 设置 is_deleted = true关联记录保留 |
| 删除记录 | 软删除 | 设置 is_deleted = true不物理删除 |
> **注意**:软删除后,关联记录不会被级联删除,因为这是软删除而非物理删除。
### 软删除操作示例
```csharp
// 软删除维度
public async Task SoftDeleteTxPanelAsync(Guid txPanelId)
{
var entity = await context.TxPanels
.IgnoreQueryFilters() // 包含已删除记录
.FirstOrDefaultAsync(t => t.Id == txPanelId);
if (entity != null && !entity.IsDeleted)
{
entity.IsDeleted = true;
await context.SaveChangesAsync();
}
}
// 软删除场景及其关联记录
public async Task SoftDeleteScenarioWithRecordsAsync(Guid scenarioId)
{
var scenario = await context.TestScenarios.FindAsync(scenarioId);
if (scenario != null)
{
scenario.IsDeleted = true;
// 同时软删除关联记录
var qfodRecords = await context.QfodRecords
.Where(r => r.ScenarioId == scenarioId)
.ToListAsync();
foreach (var record in qfodRecords)
{
record.IsDeleted = true;
}
var plossRecords = await context.PlossRecords
.Where(r => r.ScenarioId == scenarioId)
.ToListAsync();
foreach (var record in plossRecords)
{
record.IsDeleted = true;
}
await context.SaveChangesAsync();
}
}
// 恢复已删除记录
public async Task RestoreAsync(Guid id)
{
var entity = await context.TxPanels
.IgnoreQueryFilters()
.FirstOrDefaultAsync(t => t.Id == id);
if (entity != null && entity.IsDeleted)
{
entity.IsDeleted = false;
await context.SaveChangesAsync();
}
}
```
### 查询已删除记录
```csharp
// 包含已删除记录的查询
var allRecords = await context.TxPanels
.IgnoreQueryFilters()
.ToListAsync();
// 只查询已删除记录
var deletedRecords = await context.TxPanels
.IgnoreQueryFilters()
.Where(t => t.IsDeleted)
.ToListAsync();
// 统计已删除记录数
var deletedCount = await context.TxPanels
.IgnoreQueryFilters()
.CountAsync(t => t.IsDeleted);
```