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,189 @@
# ploss_record 实体
> 所属模块:[数据模型概览](../概览.md)
---
## 1. 表定义
**表名**`ploss_record`
**描述**:存储 Ploss FOD 格式解析数据,对应日志格式 `FOD-> (1)...(13)`
---
## 2. 字段定义
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | UUID | PK, NOT NULL | 主键 |
| scenario_id | UUID | FK, NOT NULL | 关联测试场景 |
| rx_type | TINYINT | NOT NULL | 接收端类型标识 |
| rx_power | SMALLINT | NOT NULL | 接收端功率mW |
| tx_power | SMALLINT | NOT NULL | 发射端功率mW |
| vcoil | SMALLINT | NOT NULL | 发射线圈电压 |
| vin | SMALLINT | NOT NULL | 输入电压 |
| isns | SMALLINT | NOT NULL | 输入电流采样值 |
| ploss | INT | NOT NULL | **核心值**:计算损耗(负数=安全) |
| threshold | INT | NOT NULL | **安全红线**:当前功率段阈值 |
| trigger_count | SMALLINT | NOT NULL, DEFAULT 0 | 触发计数器 |
| fod_result | SMALLINT | NOT NULL, DEFAULT 0 | FOD判定结果 |
| protocol_type | TINYINT | NOT NULL | 充电协议类型 |
| pwm_duty | SMALLINT | NOT NULL | PWM占空比 |
| coil_index | TINYINT | NOT NULL | 充电线圈索引 |
| is_deleted | BOOLEAN | NOT NULL, DEFAULT FALSE | 软删除标记 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
---
## 3. 外键
| 外键 | 引用表 | 说明 |
|------|--------|------|
| scenario_id | test_scenario.id | 关联测试场景(必需) |
---
## 4. 派生字段
> **注意**:以下字段不落库,查询时动态计算
| 字段 | 计算公式 | 说明 |
|------|----------|------|
| margin | threshold - ploss | 安全余量(>0 安全,<0 危险) |
---
## 5. 索引
| 索引名 | 字段 | 用途 |
|--------|------|------|
| idx_ploss_scenario | scenario_id | 按测试场景查询 |
| idx_ploss_rx_power | rx_power | 阈值表匹配 |
| idx_ploss_ploss | ploss | 损耗值查询 |
| idx_ploss_threshold | threshold | 阈值查询 |
| idx_ploss_fod_result | fod_result | FOD结果筛选 |
---
## 6. EF Core 模型
```csharp
// Models/PlossRecord.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace WCTDataMiner.Models;
[Table("ploss_record")]
[Index(nameof(ScenarioId), Name = "idx_ploss_scenario")]
[Index(nameof(RxPower), Name = "idx_ploss_rx_power")]
[Index(nameof(Ploss), Name = "idx_ploss_ploss")]
[Index(nameof(Threshold), Name = "idx_ploss_threshold")]
[Index(nameof(FodResult), Name = "idx_ploss_fod_result")]
public class PlossRecord
{
[Key]
[Column("id")]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[Column("scenario_id")]
public Guid ScenarioId { get; set; }
[Required]
[Column("rx_type")]
public byte RxType { get; set; }
[Required]
[Column("rx_power")]
public short RxPower { get; set; }
[Required]
[Column("tx_power")]
public short TxPower { get; set; }
[Required]
[Column("vcoil")]
public short Vcoil { get; set; }
[Required]
[Column("vin")]
public short Vin { get; set; }
[Required]
[Column("isns")]
public short Isns { get; set; }
/// <summary>核心判定值:负数表示安全</summary>
[Required]
[Column("ploss")]
public int Ploss { get; set; }
/// <summary>安全红线:当前功率段阈值</summary>
[Required]
[Column("threshold")]
public int Threshold { get; set; }
[Required]
[Column("trigger_count")]
public short TriggerCount { get; set; } = 0;
[Required]
[Column("fod_result")]
public short FodResult { get; set; } = 0;
[Required]
[Column("protocol_type")]
public byte ProtocolType { get; set; }
[Required]
[Column("pwm_duty")]
public short PwmDuty { get; set; }
[Required]
[Column("coil_index")]
public byte CoilIndex { get; set; }
[Required]
[Column("is_deleted")]
public bool IsDeleted { get; set; } = false;
[Required]
[Column("created_at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[ForeignKey(nameof(ScenarioId))]
public TestScenario Scenario { get; set; } = null!;
/// <summary>派生属性:安全余量</summary>
[NotMapped]
public int Margin => Threshold - Ploss;
}
```
---
## 7. 动态阈值表
> 阈值根据 RxPower 动态查表(不落库)
| Rx Power 区间 | Threshold | 备注 |
|---------------|-----------|------|
| 0-5000 mW | 350 mW | 低功率最敏感 |
| 5000-10000 mW | 500 mW | |
| 10000-15000 mW | 750 mW | |
| 15000-30000 mW | 1000 mW | |
| 30000-50000 mW | 1250 mW | 高功率 |
```csharp
// Services/ThresholdCalculator.cs
public static int GetThresholdByPower(int rxPower)
{
if (rxPower <= 5000) return 350;
if (rxPower <= 10000) return 500;
if (rxPower <= 15000) return 750;
if (rxPower <= 30000) return 1000;
return 1250;
}
```

View File

@@ -0,0 +1,132 @@
# qfod_record 实体
> 所属模块:[数据模型概览](../概览.md)
---
## 1. 表定义
**表名**`qfod_record`
**描述**:存储 Qfod 格式解析数据,对应日志格式 `X#:Q A->B C D E`
---
## 2. 字段定义
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | UUID | PK, NOT NULL | 主键 |
| scenario_id | UUID | FK, NOT NULL | 关联测试场景 |
| charger_index | TINYINT | NOT NULL | 充电器索引0 或 1 |
| coil_index | TINYINT | NOT NULL | 线圈索引0、1、2 |
| delta_q | INT | NOT NULL | Q值变化量ΔQ = current_q - raw_q |
| current_q | FLOAT | NOT NULL | 当前实时 Q值 |
| raw_q | FLOAT | NOT NULL | 原始 Q值 |
| fod_type | TINYINT | NOT NULL | 异物类型编码 |
| is_deleted | BOOLEAN | NOT NULL, DEFAULT FALSE | 软删除标记 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
---
## 3. 外键
| 外键 | 引用表 | 说明 |
|------|--------|------|
| scenario_id | test_scenario.id | 关联测试场景(必需) |
---
## 4. 索引
| 索引名 | 字段 | 用途 |
|--------|------|------|
| idx_qfod_scenario | scenario_id | 按测试场景查询 |
| idx_qfod_charger_coil | (charger_index, coil_index) | 按检测通道查询 |
| idx_qfod_fod_type | fod_type | 按异物类型统计 |
| idx_qfod_delta_q | delta_q | 阈值分析 |
---
## 5. 约束
| 约束名 | 类型 | 定义 |
|--------|------|------|
| chk_qfod_charger | CHECK | charger_index IN (0, 1) |
| chk_qfod_coil | CHECK | coil_index IN (0, 1, 2) |
---
## 6. EF Core 模型
```csharp
// Models/QfodRecord.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace WCTDataMiner.Models;
[Table("qfod_record")]
[Index(nameof(ScenarioId), Name = "idx_qfod_scenario")]
[Index(nameof(ChargerIndex), nameof(CoilIndex), Name = "idx_qfod_charger_coil")]
[Index(nameof(FodType), Name = "idx_qfod_fod_type")]
[Index(nameof(DeltaQ), Name = "idx_qfod_delta_q")]
public class QfodRecord
{
[Key]
[Column("id")]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[Column("scenario_id")]
public Guid ScenarioId { get; set; }
[Required]
[Column("charger_index")]
public byte ChargerIndex { get; set; }
[Required]
[Column("coil_index")]
public byte CoilIndex { get; set; }
[Required]
[Column("delta_q")]
public int DeltaQ { get; set; }
[Required]
[Column("current_q")]
public float CurrentQ { get; set; }
[Required]
[Column("raw_q")]
public float RawQ { get; set; }
[Required]
[Column("fod_type")]
public byte FodType { get; set; }
[Required]
[Column("is_deleted")]
public bool IsDeleted { get; set; } = false;
[Required]
[Column("created_at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
[ForeignKey(nameof(ScenarioId))]
public TestScenario Scenario { get; set; } = null!;
}
```
---
## 7. 检测通道组合
| ChargerIndex | CoilIndex | 检测通道 |
|--------------|-----------|----------|
| 0 | 0 | 充电器0-线圈0 |
| 0 | 1 | 充电器0-线圈1 |
| 0 | 2 | 充电器0-线圈2 |
| 1 | 0 | 充电器1-线圈0 |
| 1 | 1 | 充电器1-线圈1 |
| 1 | 2 | 充电器1-线圈2 |

View File

@@ -0,0 +1,89 @@
# rx_type 维度表
> 所属模块:[数据模型概览](../概览.md)
---
## 1. 表定义
**表名**`rx_type`
**描述**RX类型维度表存储接收端类型模组品牌或手机型号
---
## 2. 字段定义
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | UUID | PK, NOT NULL | 主键,自动生成 |
| name | VARCHAR(100) | UNIQUE, NOT NULL | RX类型名称 |
| is_deleted | BOOLEAN | NOT NULL, DEFAULT FALSE | 软删除标记 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
---
## 3. 类型示例
| 类型分类 | name 示例 |
|----------|----------|
| 模组 | 伏达、易冲、YBZ、... |
| 手机 | iPhone15、Samsung-S24、小米14、... |
---
## 4. EF Core 模型
```csharp
// Models/RxType.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace WCTDataMiner.Models;
[Table("rx_type")]
public class RxType
{
[Key]
[Column("id")]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[MaxLength(100)]
[Column("name")]
public string Name { get; set; } = null!;
[Required]
[Column("is_deleted")]
public bool IsDeleted { get; set; } = false;
[Required]
[Column("created_at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation Properties
public ICollection<TestScenario> TestScenarios { get; set; } = [];
}
```
---
## 5. 使用说明
RX类型在解析文件名时自动创建无需预置数据
```csharp
// 获取或创建RX类型
public async Task<RxType> GetOrCreateAsync(string name)
{
var existing = await context.RxTypes
.FirstOrDefaultAsync(r => r.Name == name);
if (existing != null) return existing;
var newRxType = new RxType { Name = name };
context.RxTypes.Add(newRxType);
await context.SaveChangesAsync();
return newRxType;
}
```

View File

@@ -0,0 +1,80 @@
# tx_hardware 维度表
> 所属模块:[数据模型概览](../概览.md)
---
## 1. 表定义
**表名**`tx_hardware`
**描述**TX硬件版本维度表存储所有硬件版本号
---
## 2. 字段定义
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | UUID | PK, NOT NULL | 主键,自动生成 |
| version | VARCHAR(50) | UNIQUE, NOT NULL | 硬件版本号 |
| is_deleted | BOOLEAN | NOT NULL, DEFAULT FALSE | 软删除标记 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
---
## 3. EF Core 模型
```csharp
// Models/TxHardware.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace WCTDataMiner.Models;
[Table("tx_hardware")]
public class TxHardware
{
[Key]
[Column("id")]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[MaxLength(50)]
[Column("version")]
public string Version { get; set; } = null!;
[Required]
[Column("is_deleted")]
public bool IsDeleted { get; set; } = false;
[Required]
[Column("created_at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation Properties
public ICollection<TestScenario> TestScenarios { get; set; } = [];
}
```
---
## 4. 使用说明
硬件版本在解析文件名时自动创建,无需预置数据:
```csharp
// 获取或创建硬件版本
public async Task<TxHardware> GetOrCreateAsync(string version)
{
var existing = await context.TxHardwares
.FirstOrDefaultAsync(h => h.Version == version);
if (existing != null) return existing;
var newHardware = new TxHardware { Version = version };
context.TxHardwares.Add(newHardware);
await context.SaveChangesAsync();
return newHardware;
}
```

View File

@@ -0,0 +1,89 @@
# tx_panel 维度表
> 所属模块:[数据模型概览](../概览.md)
---
## 1. 表定义
**表名**`tx_panel`
**描述**TX面板类型维度表存储所有可用的面板类型
---
## 2. 字段定义
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | UUID | PK, NOT NULL | 主键,自动生成 |
| name | VARCHAR(50) | UNIQUE, NOT NULL | 面板类型名称 |
| is_deleted | BOOLEAN | NOT NULL, DEFAULT FALSE | 软删除标记 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
---
## 3. 面板类型枚举值
| name | 说明 |
|------|------|
| none | 无面板 |
| single-mold | 单充模具面板 |
| single-rapid | 单充快速成型面板 |
| single-3d | 单充3D打印面板 |
| dual-mold | 双充模具面板 |
| dual-rapid | 双充快速成型面板 |
| dual-3d | 双充3D打印面板 |
---
## 4. EF Core 模型
```csharp
// Models/TxPanel.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace WCTDataMiner.Models;
[Table("tx_panel")]
public class TxPanel
{
[Key]
[Column("id")]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[MaxLength(50)]
[Column("name")]
public string Name { get; set; } = null!;
[Required]
[Column("is_deleted")]
public bool IsDeleted { get; set; } = false;
[Required]
[Column("created_at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation Properties
public ICollection<TestScenario> TestScenarios { get; set; } = [];
}
```
---
## 5. 初始化数据
```csharp
// Data/SeedData.cs
public static readonly TxPanel[] DefaultTxPanels = [
new() { Name = "none" },
new() { Name = "single-mold" },
new() { Name = "single-rapid" },
new() { Name = "single-3d" },
new() { Name = "dual-mold" },
new() { Name = "dual-rapid" },
new() { Name = "dual-3d" }
];
```

View File

@@ -0,0 +1,80 @@
# tx_software 维度表
> 所属模块:[数据模型概览](../概览.md)
---
## 1. 表定义
**表名**`tx_software`
**描述**TX软件版本维度表存储所有软件版本号hex版本
---
## 2. 字段定义
| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | UUID | PK, NOT NULL | 主键,自动生成 |
| version | VARCHAR(50) | UNIQUE, NOT NULL | 软件版本号hex版本 |
| is_deleted | BOOLEAN | NOT NULL, DEFAULT FALSE | 软删除标记 |
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
---
## 3. EF Core 模型
```csharp
// Models/TxSoftware.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace WCTDataMiner.Models;
[Table("tx_software")]
public class TxSoftware
{
[Key]
[Column("id")]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
[MaxLength(50)]
[Column("version")]
public string Version { get; set; } = null!;
[Required]
[Column("is_deleted")]
public bool IsDeleted { get; set; } = false;
[Required]
[Column("created_at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation Properties
public ICollection<TestScenario> TestScenarios { get; set; } = [];
}
```
---
## 4. 使用说明
软件版本在解析文件名时自动创建,无需预置数据:
```csharp
// 获取或创建软件版本
public async Task<TxSoftware> GetOrCreateAsync(string version)
{
var existing = await context.TxSoftwares
.FirstOrDefaultAsync(s => s.Version == version);
if (existing != null) return existing;
var newSoftware = new TxSoftware { Version = version };
context.TxSoftwares.Add(newSoftware);
await context.SaveChangesAsync();
return newSoftware;
}
```

View File

@@ -0,0 +1,230 @@
# 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) | 相同属性组合不重复创建 |
---
## 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 - 数据记录
public ICollection<QfodRecord> QfodRecords { get; set; } = [];
public ICollection<PlossRecord> PlossRecords { get; set; } = [];
}
```
---
## 7. 场景创建流程
### 从文件名解析
```csharp
// Services/ScenarioService.cs
public async Task<TestScenario> GetOrCreateFromFileNameAsync(string fileName)
{
// 1. 解析文件名
var parts = ParseFileName(fileName);
// 格式: TX面板-TX硬件-TX软件-RX类型-测试目的-年月日-测试次数.log
// 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
.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);
if (existing != null) return existing;
// 4. 创建新场景
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
};
context.TestScenarios.Add(scenario);
await context.SaveChangesAsync();
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;
}
```

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);
```

212
docs/data-model/概览.md Normal file
View File

@@ -0,0 +1,212 @@
# Data Model: WCTDataMiner
> WCTDataMiner 数据库模型规格,采用维度表设计,消除数据冗余
---
## 文档索引
### 维度表
| 文档 | 说明 |
|------|------|
| [TX面板维度](./entities/TxPanel维度.md) | TX面板类型维度 |
| [TX硬件版本维度](./entities/TxHardware维度.md) | TX硬件版本维度 |
| [TX软件版本维度](./entities/TxSoftware维度.md) | TX软件版本维度 |
| [RX类型维度](./entities/RxType维度.md) | RX类型维度 |
### 事实表
| 文档 | 说明 |
|------|------|
| [测试场景实体](./entities/测试场景.md) | 测试场景(外键组合) |
| [Qfod 记录实体](./entities/Qfod记录.md) | Qfod 检测数据 |
| [Ploss 记录实体](./entities/Ploss记录.md) | Ploss FOD 数据 |
| [关系说明](./关系说明.md) | 实体间关系定义 |
---
## 实体列表
### 维度表4张
| 表名 | 职责 | 关键字段 |
|------|------|----------|
| tx_panel | TX面板类型维度 | name (UNIQUE) |
| tx_hardware | TX硬件版本维度 | version (UNIQUE) |
| tx_software | TX软件版本维度 | version (UNIQUE) |
| rx_type | RX类型维度 | name (UNIQUE) |
### 事实表3张
| 表名 | 职责 | 关键字段 |
|------|------|----------|
| test_scenario | 测试场景组合 | 4个维度外键 + test_purpose + test_date |
| qfod_record | Qfod 检测数据 | scenario_id, delta_q, fod_type |
| ploss_record | Ploss FOD 数据 | scenario_id, ploss, threshold |
---
## ER 关系图
```mermaid
erDiagram
tx_panel ||--o{ test_scenario : "引用"
tx_hardware ||--o{ test_scenario : "引用"
tx_software ||--o{ test_scenario : "引用"
rx_type ||--o{ test_scenario : "引用"
test_scenario ||--o{ qfod_record : "包含"
test_scenario ||--o{ ploss_record : "包含"
tx_panel {
uuid id PK "主键"
varchar name UK "面板类型"
}
tx_hardware {
uuid id PK "主键"
varchar version UK "硬件版本"
}
tx_software {
uuid id PK "主键"
varchar version UK "软件版本"
}
rx_type {
uuid id PK "主键"
varchar name UK "RX类型"
}
test_scenario {
uuid id PK "主键"
uuid tx_panel_id FK "面板外键"
uuid tx_hardware_id FK "硬件外键"
uuid tx_software_id FK "软件外键"
uuid rx_type_id FK "RX外键"
varchar test_purpose "测试目的"
date test_date "测试日期"
int test_sequence "测试序号"
int qfod_count "Qfod记录数"
int ploss_count "Ploss记录数"
}
qfod_record {
uuid id PK "主键"
uuid scenario_id FK "场景外键"
tinyint charger_index "充电器索引"
tinyint coil_index "线圈索引"
int delta_q "Q值变化量"
float current_q "当前Q值"
float raw_q "原始Q值"
tinyint fod_type "异物类型"
}
ploss_record {
uuid id PK "主键"
uuid scenario_id FK "场景外键"
tinyint rx_type "接收端类型"
smallint rx_power "接收端功率"
smallint tx_power "发射端功率"
int ploss "计算损耗值"
int threshold "阈值"
smallint fod_result "FOD判定结果"
}
```
---
## 设计说明
### 星型模型架构
本设计采用星型模型Star Schema
- **维度表**存储可能重复的属性值面板、硬件版本、软件版本、RX类型
- **事实表**:存储外键组合和度量数据
**优势**
- 维度值只存一份,消除冗余
- 维度可复用,相同硬件版本在不同场景中引用同一记录
- 查询可按维度灵活筛选和分组
### test_purpose 处理
`test_purpose` 直接存储在场景表中,不拆分维度表,原因:
- 测试目的描述较自由,不适合标准化为维度
- 便于用户灵活输入
### 场景唯一性
场景表设置唯一约束,相同属性组合不重复创建:
```
UNIQUE(tx_panel_id, tx_hardware_id, tx_software_id, rx_type_id, test_purpose, test_date, test_sequence)
```
### 软删除设计
所有表均包含 `is_deleted` 字段,支持软删除:
| 字段名 | 类型 | 约束 | 说明 |
|--------|------|------|------|
| is_deleted | BOOLEAN | NOT NULL, DEFAULT FALSE | 软删除标记 |
**删除行为**
- 删除操作设置 `is_deleted = true`,不物理删除数据
- 查询时默认过滤已删除记录(`WHERE is_deleted = false`
- 支持数据恢复和审计追溯
**查询示例**
```csharp
// 默认查询(不含已删除)
var activeRecords = context.TxPanels
.Where(t => !t.IsDeleted)
.ToList();
// 包含已删除记录
var allRecords = context.TxPanels
.IgnoreQueryFilters()
.ToList();
// 软删除操作
public async Task SoftDeleteAsync(Guid id)
{
var entity = await context.TxPanels.FindAsync(id);
if (entity != null)
{
entity.IsDeleted = true;
await context.SaveChangesAsync();
}
}
```
### 系统日志处理
解析过程中的错误日志通过 Serilog 记录到系统日志文件,不存入业务数据库。
---
## 数据量估算
| 表 | 增长方式 | 预估规模 |
|------|----------|----------|
| tx_panel | 预置数据 | 7 条(固定枚举) |
| tx_hardware | 随版本迭代 | 10-50 条/年 |
| tx_software | 随版本迭代 | 10-50 条/年 |
| rx_type | 随设备增加 | 20-100 条/年 |
| test_scenario | 随测试批次 | 3,650 条/年 |
| qfod_record | 随测试数据 | 3.6M-18M 条/年 |
| ploss_record | 随测试数据 | 3.6M-18M 条/年 |
---
## 假设/歧义标注
| 标注 | 内容 |
|------|------|
| [A] | 使用 UUID 作为主键 |
| [?] | FOD Type 具体编码含义需确认 |
| [?] | Ploss 日志格式与实际日志对应关系需确认 |
| [!] | ploss 负数表示安全,正数可能触发 FOD |