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

61
docs/README.md Normal file
View File

@@ -0,0 +1,61 @@
# WCTDataMiner 文档中心
> 无线充电 FOD异物检测日志数据采集系统
---
## 文档目录
### 架构设计
| 文档 | 说明 |
|------|------|
| [架构概览](./architecture/概览.md) | 系统架构总览、设计原则 |
| [技术选型](./architecture/技术选型.md) | .NET 8.0 + EF Core 8.0 技术栈 |
| [模块划分](./architecture/模块划分.md) | 分层架构、依赖关系 |
| [安全架构](./architecture/安全架构.md) | 认证方案、数据安全 |
| [解析器设计](./architecture/parser/概览.md) | Qfod/Ploss 解析器 |
| [数据处理流程](./architecture/数据处理流程.md) | 解析流程、批量写入 |
| [配置管理](./architecture/配置管理.md) | appsettings.json 配置 |
| [CLI 命令设计](./architecture/CLI命令设计.md) | 命令行接口 |
| [数据库切换](./architecture/database/概览.md) | SQLite/PostgreSQL 切换 |
### 数据模型
| 文档 | 说明 |
|------|------|
| [数据模型概览](./data-model/概览.md) | ER 图、实体列表 |
| [日志文件](./data-model/entities/日志文件.md) | log_file 实体 |
| [Qfod 记录](./data-model/entities/Qfod记录.md) | qfod_record 实体 |
| [Ploss 记录](./data-model/entities/Ploss记录.md) | ploss_record 实体 |
| [解析错误](./data-model/entities/解析错误.md) | parse_error 实体 |
| [关系说明](./data-model/关系说明.md) | 实体间关系 |
### 需求文档
| 文档 | 说明 |
|------|------|
| [小电机数据需求分析](../小电机数据需求分析.md) | 日志格式、数据分析方法 |
---
## 技术栈概览
| 层级 | 技术 | 版本 |
|------|------|------|
| 运行时 | .NET | 8.0 LTS |
| 语言 | C# | 12.0 |
| ORM | Entity Framework Core | 8.0 |
| 数据库 | SQLite / PostgreSQL | - |
| CLI | System.CommandLine | 2.0-beta4 |
---
## 待确认问题
| # | 问题 | 来源 |
|---|------|------|
| 1 | 增量解析策略 | 日志是否持续追加? |
| 2 | 重复解析处理 | 多次解析同一文件如何处理? |
| 3 | FOD Type 编码含义 | 需求文档 QA 3.2 |
| 4 | Ploss 日志格式对应 | 需求文档 QA 3.3 |

View File

@@ -0,0 +1,168 @@
# CLI 命令设计
> 所属模块:[架构概览](./概览.md)
---
## 1. 命令概览
| 命令 | 说明 |
|------|------|
| `parse` | 解析日志文件并存储到数据库 |
| `stats` | 查询统计信息 |
| `export` | 导出数据 |
| `clean` | 清理数据 |
---
## 2. 命令详解
### 2.1 parse - 解析日志文件
```bash
# 解析单个文件
dotnet run parse --file path/to/log.txt
# 解析目录下所有文件
dotnet run parse --dir path/to/logs/
# 解析目录(递归)
dotnet run parse --dir path/to/logs/ --recursive
# 强制重新解析(覆盖已有数据)
dotnet run parse --file log.txt --force
```
### 2.2 stats - 查询统计
```bash
# 查看文件解析统计
dotnet run stats --file log.txt
# 查看整体统计
dotnet run stats --summary
# 按 FOD 类型统计
dotnet run stats --by-fod-type
```
### 2.3 export - 导出数据
```bash
# 导出为 CSV
dotnet run export --format csv --output ./output/
# 导出为 JSON
dotnet run export --format json --output ./output/
# 按文件导出
dotnet run export --file log.txt --format csv
```
### 2.4 clean - 清理数据
```bash
# 清理指定文件的数据
dotnet run clean --file log.txt --confirm
# 清理所有数据
dotnet run clean --all --confirm
```
---
## 3. 命令实现
```csharp
// Commands/ParseCommand.cs
using System.CommandLine;
namespace WCTDataMiner.Commands;
public class ParseCommand : Command
{
public ParseCommand(ParseService parseService)
: base("parse", "Parse log files and store data to database")
{
var fileOption = new Option<string>(
"--file",
"Path to a single log file to parse"
);
var dirOption = new Option<string>(
"--dir",
"Path to directory containing log files"
);
var recursiveOption = new Option<bool>(
"--recursive",
() => false,
"Recursively search subdirectories"
);
var forceOption = new Option<bool>(
"--force",
() => false,
"Force re-parse even if file already parsed"
);
AddOption(fileOption);
AddOption(dirOption);
AddOption(recursiveOption);
AddOption(forceOption);
this.SetHandler(async (file, dir, recursive, force) =>
{
if (!string.IsNullOrEmpty(file))
{
await parseService.ParseFileAsync(file, force);
}
else if (!string.IsNullOrEmpty(dir))
{
await parseService.ParseDirectoryAsync(dir, recursive, force);
}
else
{
Console.WriteLine("Please specify --file or --dir");
}
}, fileOption, dirOption, recursiveOption, forceOption);
}
}
```
---
## 4. Program.cs 入口
```csharp
// Program.cs
using System.CommandLine;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using WCTDataMiner.Commands;
var builder = Host.CreateApplicationBuilder(args);
// 注册服务
builder.Services.AddScoped<ParseService>();
builder.Services.AddScoped<StatsService>();
builder.Services.AddScoped<ExportService>();
builder.Services.AddScoped<CleanService>();
builder.Services.AddDatabaseServices(builder.Configuration);
var app = builder.Build();
// 创建根命令
var rootCommand = new RootCommand("WCTDataMiner - FOD Log Data Parser");
// 使用服务提供者获取服务
using var scope = app.Services.CreateScope();
var provider = scope.ServiceProvider;
rootCommand.AddCommand(new ParseCommand(provider.GetRequiredService<ParseService>()));
rootCommand.AddCommand(new StatsCommand(provider.GetRequiredService<StatsService>()));
rootCommand.AddCommand(new ExportCommand(provider.GetRequiredService<ExportService>()));
rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService<CleanService>()));
return await rootCommand.InvokeAsync(args);
```

View File

@@ -0,0 +1,122 @@
# DbContext 工厂
> 所属模块:[数据库切换支持](./概览.md)
---
## 1. 接口定义
```csharp
// Data/IDbContextFactory.cs
using WCTDataMiner.Data;
namespace WCTDataMiner.Data;
public interface IDbContextFactory
{
WctMinerDbContext CreateDbContext();
}
```
---
## 2. 工厂实现
```csharp
// Data/DbContextFactory.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace WCTDataMiner.Data;
public class DbContextFactory : IDbContextFactory
{
private readonly IConfiguration _configuration;
public DbContextFactory(IConfiguration configuration)
{
_configuration = configuration;
}
public WctMinerDbContext CreateDbContext()
{
var dbType = _configuration["Database:Type"]?.ToLower() ?? "sqlite";
var optionsBuilder = new DbContextOptionsBuilder<WctMinerDbContext>();
switch (dbType)
{
case "postgresql":
case "postgres":
var pgConnStr = BuildPostgreSqlConnectionString();
optionsBuilder.UseNpgsql(pgConnStr);
break;
case "sqlite":
default:
var dbPath = _configuration["Database:Path"] ?? "./data/database/wctminer.db";
EnsureDirectoryExists(dbPath);
optionsBuilder.UseSqlite($"Data Source={dbPath}");
break;
}
#if DEBUG
optionsBuilder.EnableSensitiveDataLogging();
optionsBuilder.EnableDetailedErrors();
#endif
return new WctMinerDbContext(optionsBuilder.Options);
}
private string BuildPostgreSqlConnectionString()
{
var host = _configuration["Database:Host"] ?? "localhost";
var port = _configuration["Database:Port"] ?? "5432";
var name = _configuration["Database:Name"] ?? "wctminer";
var user = _configuration["Database:User"] ?? "postgres";
var password = _configuration["Database:Password"] ?? "";
return $"Host={host};Port={port};Database={name};Username={user};Password={password}";
}
private static void EnsureDirectoryExists(string dbPath)
{
var directory = Path.GetDirectoryName(dbPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
}
}
```
---
## 3. 依赖注入扩展
```csharp
// Extensions/ServiceCollectionExtensions.cs
using Microsoft.Extensions.DependencyInjection;
using WCTDataMiner.Data;
namespace WCTDataMiner.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddDatabaseServices(
this IServiceCollection services,
IConfiguration configuration)
{
// 注册 DbContext 工厂
services.AddScoped<IDbContextFactory, DbContextFactory>();
// 注册 DbContext通过工厂创建
services.AddScoped<WctMinerDbContext>(sp =>
{
var factory = sp.GetRequiredService<IDbContextFactory>();
return factory.CreateDbContext();
});
return services;
}
}
```

View File

@@ -0,0 +1,45 @@
# 数据库切换支持
> 所属模块:[架构概览](../概览.md)
---
## 文档索引
| 文档 | 说明 |
|------|------|
| [DbContext 工厂](./DbContext工厂.md) | 数据库上下文工厂设计 |
| [迁移策略](./迁移策略.md) | 数据库迁移和切换步骤 |
---
## 切换策略概览
```
┌─────────────────────────────────────────────────────────────┐
│ appsettings.json │
│ "Database": { "Type": "sqlite" | "postgresql" } │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ DbContextFactory │
│ 读取配置 → 选择 Provider → 创建 DbContext │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ WctMinerDbContext │
│ 统一的 DbModel 配置,适配两种数据库 │
└─────────────────────────────────────────────────────────────┘
```
---
## 当前状态
| 项目 | 状态 |
|------|------|
| SQLite | ✅ 默认启用 |
| PostgreSQL | ✅ 架构支持,待配置 |
| 切换方式 | 配置文件切换 |

View File

@@ -0,0 +1,81 @@
# 数据库迁移策略
> 所属模块:[数据库切换支持](./概览.md)
---
## 1. 迁移命令
### SQLite
```bash
# 添加迁移
dotnet ef migrations add InitialCreate --output-dir Migrations/SQLite
# 应用迁移
dotnet ef database update
```
### PostgreSQL
```bash
# 切换配置后添加迁移
dotnet ef migrations add InitialCreate --output-dir Migrations/PostgreSQL -- --environment PostgreSQL
# 应用迁移
dotnet ef database update -- --environment PostgreSQL
```
---
## 2. 切换步骤
### 从 SQLite 切换到 PostgreSQL
| 步骤 | 操作 | 说明 |
|------|------|------|
| 1 | 修改配置 | `appsettings.json``Database:Type` 改为 `postgresql` |
| 2 | 设置连接 | 配置 `Host`, `Port`, `Name`, `User`, `Password` |
| 3 | 创建数据库 | `CREATE DATABASE wctminer;` |
| 4 | 应用迁移 | `dotnet ef database update` |
| 5 | 数据迁移 | 使用工具迁移 SQLite 数据到 PostgreSQL |
---
## 3. 配置文件示例
### SQLite (默认)
```json
{
"Database": {
"Type": "sqlite",
"Path": "./data/database/wctminer.db"
}
}
```
### PostgreSQL
```json
{
"Database": {
"Type": "postgresql",
"Host": "localhost",
"Port": 5432,
"Name": "wctminer",
"User": "wctminer_user",
"Password": "${DB_PASSWORD}"
}
}
```
---
## 4. 数据迁移工具
推荐使用 [pgloader](https://github.com/dimitri/pgloader) 迁移 SQLite 到 PostgreSQL
```bash
pgloader ./data/database/wctminer.db postgresql://user:password@localhost:5432/wctminer
```

View File

@@ -0,0 +1,165 @@
# Ploss 解析器
> 所属模块:[解析器设计](./概览.md)
---
## 1. 日志格式
**格式:** `FOD-> (1) X X X (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13)`
**示例:** `FOD-> 4 2 1 127 10690 12013 35377 9904 1213 -2882 750 0 0 1 500 1`
> 括号数字表示有效字段编号X 表示忽略该位置数据
---
## 2. 字段定义
| 编号 | 字段名称 | 数据类型 | 说明 |
|------|----------|----------|------|
| (1) | RxType | byte | 接收端类型标识 |
| (2) | RxPower | short | 接收端功率mW |
| (3) | TxPower | short | 发射端功率mW |
| (4) | Vcoil | short | 发射线圈电压 |
| (5) | Vin | short | 输入电压 |
| (6) | Isns | short | 输入电流采样值 |
| (7) | Ploss | int | **核心值**:计算损耗(负数表示安全) |
| (8) | Threshold | int | **安全红线**:当前功率段阈值 |
| (9) | TriggerCount | short | 触发计数器 |
| (10) | FodResult | short | FOD判定结果状态 |
| (11) | ProtocolType | byte | 充电协议类型 |
| (12) | PwmDuty | short | PWM控制占空比 |
| (13) | CoilIndex | byte | 充电线圈索引 |
---
## 3. 正则表达式
```csharp
// Parsers/PlossParser.cs
private static readonly Regex PlossPattern = new(
@"^FOD->\s+(\d+)\s+\d+\s+\d+\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(-?\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$",
RegexOptions.Compiled
);
```
**捕获组映射:**
| 捕获组 | 字段 | 类型转换 |
|--------|------|----------|
| Group 1 | RxType | byte.Parse() |
| Group 2 | RxPower | short.Parse() |
| Group 3 | TxPower | short.Parse() |
| Group 4 | Vcoil | short.Parse() |
| Group 5 | Vin | short.Parse() |
| Group 6 | Isns | short.Parse() |
| Group 7 | Ploss | int.Parse() |
| Group 8 | Threshold | int.Parse() |
| Group 9 | TriggerCount | short.Parse() |
| Group 10 | FodResult | short.Parse() |
| Group 11 | ProtocolType | byte.Parse() |
| Group 12 | PwmDuty | short.Parse() |
| Group 13 | CoilIndex | byte.Parse() |
---
## 4. 解析器实现
```csharp
// Parsers/PlossParser.cs
using System.Text.RegularExpressions;
using WCTDataMiner.Models;
namespace WCTDataMiner.Parsers;
public class PlossParser : IParser<PlossRecord>
{
private static readonly Regex PlossPattern = new(
@"^FOD->\s+(\d+)\s+\d+\s+\d+\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(-?\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$",
RegexOptions.Compiled
);
public bool CanParse(string line) => PlossPattern.IsMatch(line);
public ParseResult<PlossRecord> Parse(string line, int lineNumber, Guid logFileId)
{
var match = PlossPattern.Match(line);
if (!match.Success)
{
return ParseResult<PlossRecord>.Failure(
"FORMAT_MISMATCH",
"Line does not match Ploss pattern"
);
}
try
{
var record = new PlossRecord
{
LogFileId = logFileId,
LineNumber = lineNumber,
RxType = byte.Parse(match.Groups[1].Value),
RxPower = short.Parse(match.Groups[2].Value),
TxPower = short.Parse(match.Groups[3].Value),
Vcoil = short.Parse(match.Groups[4].Value),
Vin = short.Parse(match.Groups[5].Value),
Isns = short.Parse(match.Groups[6].Value),
Ploss = int.Parse(match.Groups[7].Value),
Threshold = int.Parse(match.Groups[8].Value),
TriggerCount = short.Parse(match.Groups[9].Value),
FodResult = short.Parse(match.Groups[10].Value),
ProtocolType = byte.Parse(match.Groups[11].Value),
PwmDuty = short.Parse(match.Groups[12].Value),
CoilIndex = byte.Parse(match.Groups[13].Value)
};
return ParseResult<PlossRecord>.Success(record);
}
catch (Exception ex)
{
return ParseResult<PlossRecord>.Failure(
"TYPE_CONVERSION",
$"Failed to convert field: {ex.Message}"
);
}
}
}
```
---
## 5. 核心判定逻辑
### 5.1 FOD 触发条件
```
if (Ploss > Threshold) → 计数器累积
if (Count > Limit) → 触发 FOD 报警,停止充电
```
### 5.2 安全余量计算
```csharp
// 派生属性(不落库)
Margin = Threshold - Ploss
```
| Margin 状态 | 含义 |
|-------------|------|
| Margin > 0 | 安全,损耗低于阈值 |
| Margin < 0 | 危险,损耗超过阈值,可能触发 FOD |
---
## 6. 动态阈值表
阈值根据接收功率动态查表:
| 接收功率区间 (Rx Power) | 阈值 (Threshold) | 备注 |
|------------------------|------------------|------|
| 0 ~ 5W (0-5000 mW) | 350 mW | 低功率最敏感 |
| 5 ~ 10W (5000-10000 mW) | 500 mW | |
| 10 ~ 15W (10000-15000 mW) | 750 mW | |
| 15 ~ 30W (15000-30000 mW) | 1000 mW | 示例 Log 处于此区间 |
| 30 ~ 50W (30000-50000 mW) | 1250 mW | 高功率允许更多损耗 |

View File

@@ -0,0 +1,120 @@
# Qfod 解析器
> 所属模块:[解析器设计](./概览.md)
---
## 1. 日志格式
**格式:** `X#:Q A->B C D E`
**示例:** `0#:Q 1->25 45.5 20.5 2`
---
## 2. 字段定义
| 字段标识 | 字段名称 | 数据类型 | 取值范围 | 说明 |
|----------|----------|----------|----------|------|
| X | ChargerIndex | byte | 0, 1 | 充电器索引 |
| A | CoilIndex | byte | 0, 1, 2 | 线圈索引 |
| B | DeltaQ | int | - | Q值变化量ΔQ = CurrentQ - RawQ |
| C | CurrentQ | float | - | 当前实时Q值 |
| D | RawQ | float | - | 原始Q值 |
| E | FodType | byte | 见Table 2 | 异物类型编码 |
---
## 3. 正则表达式
```csharp
// Parsers/QfodParser.cs
private static readonly Regex QfodPattern = new(
@"^(\d)#:Q\s+(\d+)->(-?\d+)\s+([-\d.]+)\s+([-\d.]+)\s+(\d+)$",
RegexOptions.Compiled
);
```
**捕获组映射:**
| 捕获组 | 字段 | 类型转换 |
|--------|------|----------|
| Group 1 | ChargerIndex | byte.Parse() |
| Group 2 | CoilIndex | byte.Parse() |
| Group 3 | DeltaQ | int.Parse() |
| Group 4 | CurrentQ | float.Parse() |
| Group 5 | RawQ | float.Parse() |
| Group 6 | FodType | byte.Parse() |
---
## 4. 解析器实现
```csharp
// Parsers/QfodParser.cs
using System.Text.RegularExpressions;
using WCTDataMiner.Models;
namespace WCTDataMiner.Parsers;
public class QfodParser : IParser<QfodRecord>
{
private static readonly Regex QfodPattern = new(
@"^(\d)#:Q\s+(\d+)->(-?\d+)\s+([-\d.]+)\s+([-\d.]+)\s+(\d+)$",
RegexOptions.Compiled
);
public bool CanParse(string line) => QfodPattern.IsMatch(line);
public ParseResult<QfodRecord> Parse(string line, int lineNumber, Guid logFileId)
{
var match = QfodPattern.Match(line);
if (!match.Success)
{
return ParseResult<QfodRecord>.Failure(
"FORMAT_MISMATCH",
"Line does not match Qfod pattern"
);
}
try
{
var record = new QfodRecord
{
LogFileId = logFileId,
LineNumber = lineNumber,
ChargerIndex = byte.Parse(match.Groups[1].Value),
CoilIndex = byte.Parse(match.Groups[2].Value),
DeltaQ = int.Parse(match.Groups[3].Value),
CurrentQ = float.Parse(match.Groups[4].Value),
RawQ = float.Parse(match.Groups[5].Value),
FodType = byte.Parse(match.Groups[6].Value)
};
return ParseResult<QfodRecord>.Success(record);
}
catch (Exception ex)
{
return ParseResult<QfodRecord>.Failure(
"TYPE_CONVERSION",
$"Failed to convert field: {ex.Message}"
);
}
}
}
```
---
## 5. 检测通道组合
`ChargerIndex + CoilIndex` 组合唯一标识一个检测通道:
| 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,78 @@
# 解析器设计
> 所属模块:[架构概览](../概览.md)
---
## 文档索引
| 文档 | 说明 |
|------|------|
| [Qfod 解析器](./Qfod解析器.md) | Qfod 格式日志解析设计 |
| [Ploss 解析器](./Ploss解析器.md) | Ploss FOD 格式日志解析设计 |
---
## 通用接口定义
```csharp
// Parsers/IParser.cs
namespace WCTDataMiner.Parsers;
public interface IParser<TRecord>
{
/// <summary>
/// 判断是否可以解析该行
/// </summary>
bool CanParse(string line);
/// <summary>
/// 解析日志行
/// </summary>
ParseResult<TRecord> Parse(string line, int lineNumber, Guid logFileId);
}
public record ParseResult<TRecord>
{
public bool IsSuccess { get; init; }
public TRecord? Record { get; init; }
public string? ErrorType { get; init; }
public string? ErrorMessage { get; init; }
public static ParseResult<TRecord> Success(TRecord record) =>
new() { IsSuccess = true, Record = record };
public static ParseResult<TRecord> Failure(string errorType, string errorMessage) =>
new() { IsSuccess = false, ErrorType = errorType, ErrorMessage = errorMessage };
}
```
---
## 解析流程
```mermaid
flowchart TD
A[读取日志文件] --> B[逐行遍历]
B --> C{判断行类型}
C -->|Qfod| D[QfodParser.Parse]
C -->|Ploss| E[PlossParser.Parse]
C -->|其他| F[跳过]
D --> G{解析成功?}
E --> G
G -->|成功| H[添加到记录列表]
G -->|失败| I[记录到 ParseError]
H --> J[批量保存到数据库]
I --> J
J --> K[输出解析报告]
```
---
## 错误类型定义
| ErrorType | 说明 | 处理方式 |
|-----------|------|----------|
| FORMAT_MISMATCH | 行格式不匹配正则表达式 | 跳过该行,记录原始内容 |
| TYPE_CONVERSION | 字段类型转换失败 | 跳过该行,记录错误字段 |
| VALUE_OUT_OF_RANGE | 字段值超出有效范围 | 使用边界值或跳过 |

View File

@@ -0,0 +1,39 @@
# 安全架构
> 所属模块:[架构概览](./概览.md)
---
## 1. 概述
> [A] 本系统为本地日志解析工具,无网络暴露,安全需求较低
---
## 2. 认证方案
| 项目 | 方案 |
|------|------|
| 认证方式 | [A] 无(本地工具) |
| 数据库访问 | 本地文件权限控制SQLite或连接串认证PostgreSQL |
---
## 3. 数据安全
| 项目 | 内容 |
|------|------|
| 数据类型 | [!] 日志数据不含敏感信息,为设备检测数据 |
| 数据保护 | 本地存储,操作系统文件权限控制 |
| 备份策略 | [?] 需确认:是否需要定期备份? |
---
## 4. 安全检查清单
| 检查项 | 状态 |
|--------|------|
| 无网络暴露 | ✅ 本地工具 |
| 数据库认证 | ✅ SQLite 本地文件权限 / PostgreSQL 连接串 |
| 敏感数据处理 | ✅ 无敏感数据 |
| 日志脱敏 | ✅ 不需要(设备检测数据) |

View File

@@ -0,0 +1,58 @@
# 技术选型
> 所属模块:[架构概览](./概览.md)
---
## 1. 技术栈总览
| 层级 | 技术选型 | 版本 | 选型理由 |
|------|----------|------|----------|
| 运行时 | .NET | 8.0 LTS | 长期支持版本,性能优秀,跨平台 |
| 语言 | C# | 12.0 | 强类型、现代语法、优秀的正则表达式支持 |
| ORM | Entity Framework Core | 8.0 | .NET 标准 ORMCode First 支持LINQ 查询 |
| 数据库 | SQLite / PostgreSQL | - | SQLite 适合单机PostgreSQL 适合多用户并发 |
| 日志框架 | Serilog | 4.0+ | 结构化日志,多种 Sink 支持 |
| 配置管理 | Microsoft.Extensions.Configuration | - | 强类型配置,支持 JSON/环境变量 |
| CLI 框架 | System.CommandLine | 2.0-beta4 | 官方 CLI 库,参数解析完善 |
| 正则表达式 | System.Text.RegularExpressions | - | .NET 原生支持,编译正则高效 |
---
## 2. 技术决策记录
| # | 决策点 | 选择 | 备选方案 | 决策理由 |
|---|--------|------|----------|----------|
| 1 | .NET 版本 | .NET 8.0 LTS | .NET 9.0 | LTS 版本稳定,企业级应用首选 |
| 2 | 数据库选择 | SQLite当前/ PostgreSQL后续 | SQL Server | 初期单机使用 SQLite后续可切换 PostgreSQL |
| 3 | 解析模式 | 批处理 | 流式处理 | 日志文件是静态文件,批处理更合适 |
| 4 | 数据格式 | 结构化存储 | 原始存储 + 计算 | 解析后存储便于后续分析 |
| 5 | ORM 模式 | Code First | Database First | 便于版本控制,迁移自动化 |
| 6 | 数据库切换 | 抽象 DbContext 工厂 | 硬编码连接 | 支持配置切换数据库类型 |
---
## 3. NuGet 包依赖
```xml
<ItemGroup>
<!-- ORM -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<!-- CLI -->
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<!-- 配置 -->
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<!-- 日志 -->
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
</ItemGroup>
```

View File

@@ -0,0 +1,205 @@
# 数据处理流程
> 所属模块:[架构概览](./概览.md)
---
## 1. 完整解析流程
```mermaid
sequenceDiagram
participant CLI as CLI Entry
participant PS as ParseService
participant LR as LogFileReader
participant QP as QfodParser
participant PP as PlossParser
participant EHS as ErrorHandlingService
participant DB as Database
CLI->>LR: 扫描日志目录
LR-->>CLI: 返回文件列表
loop 每个日志文件
CLI->>PS: ParseFileAsync(filePath)
PS->>DB: INSERT log_file (status=processing)
DB-->>PS: 返回 file_id
PS->>LR: 读取文件内容
LR-->>PS: 返回行列表
loop 逐行遍历
alt 是 Qfod 行
PS->>QP: Parse(line)
QP-->>PS: ParseResult
else 是 Ploss 行
PS->>PP: Parse(line)
PP-->>PS: ParseResult
end
alt 解析成功
PS->>PS: 加入记录列表
else 解析失败
PS->>EHS: RecordError(line, error)
end
end
PS->>DB: BATCH INSERT records
PS->>DB: BATCH INSERT errors
PS->>DB: UPDATE log_file (counts, status=completed)
PS-->>CLI: 返回 ParseReport
end
CLI-->>CLI: 输出解析报告
```
---
## 2. ParseService 实现
```csharp
// Services/ParseService.cs
using Microsoft.EntityFrameworkCore;
using WCTDataMiner.Data;
using WCTDataMiner.Models;
using WCTDataMiner.Parsers;
namespace WCTDataMiner.Services;
public class ParseService
{
private readonly WctMinerDbContext _context;
private readonly QfodParser _qfodParser;
private readonly PlossParser _plossParser;
private readonly ErrorHandlingService _errorService;
private readonly ILogger<ParseService> _logger;
private readonly int _batchSize;
public ParseService(
WctMinerDbContext context,
QfodParser qfodParser,
PlossParser plossParser,
ErrorHandlingService errorService,
ILogger<ParseService> logger,
IConfiguration config)
{
_context = context;
_qfodParser = qfodParser;
_plossParser = plossParser;
_errorService = errorService;
_logger = logger;
_batchSize = config.GetValue("Parser:BatchSize", 1000);
}
public async Task<ParseReport> ParseFileAsync(string filePath)
{
var fileInfo = new FileInfo(filePath);
var logFile = await CreateLogFileRecordAsync(fileInfo);
var lines = await File.ReadAllLinesAsync(filePath);
var qfodRecords = new List<QfodRecord>();
var plossRecords = new List<PlossRecord>();
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i];
var lineNumber = i + 1;
if (_qfodParser.CanParse(line))
{
var result = _qfodParser.Parse(line, lineNumber, logFile.Id);
if (result.IsSuccess)
qfodRecords.Add(result.Record!);
else
_errorService.RecordError(logFile.Id, lineNumber, line, result.ErrorType!, result.ErrorMessage!);
}
else if (_plossParser.CanParse(line))
{
var result = _plossParser.Parse(line, lineNumber, logFile.Id);
if (result.IsSuccess)
plossRecords.Add(result.Record!);
else
_errorService.RecordError(logFile.Id, lineNumber, line, result.ErrorType!, result.ErrorMessage!);
}
}
// 批量保存
await BatchInsertAsync(qfodRecords, plossRecords, _errorService.GetErrors());
// 更新统计
await UpdateLogFileStatsAsync(logFile.Id, qfodRecords.Count, plossRecords.Count, _errorService.ErrorCount);
return new ParseReport(fileInfo.Name, qfodRecords.Count, plossRecords.Count, _errorService.ErrorCount);
}
private async Task<LogFile> CreateLogFileRecordAsync(FileInfo fileInfo)
{
var logFile = new LogFile
{
FileName = fileInfo.Name,
FilePath = fileInfo.FullName,
FileSize = fileInfo.Length,
Status = ParseStatus.Processing
};
_context.LogFiles.Add(logFile);
await _context.SaveChangesAsync();
return logFile;
}
private async Task BatchInsertAsync(
List<QfodRecord> qfodRecords,
List<PlossRecord> plossRecords,
List<ParseError> errors)
{
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// 批量插入 QfodRecord
foreach (var batch in qfodRecords.Chunk(_batchSize))
{
_context.QfodRecords.AddRange(batch);
await _context.SaveChangesAsync();
}
// 批量插入 PlossRecord
foreach (var batch in plossRecords.Chunk(_batchSize))
{
_context.PlossRecords.AddRange(batch);
await _context.SaveChangesAsync();
}
// 批量插入 ParseError
foreach (var batch in errors.Chunk(_batchSize))
{
_context.ParseErrors.AddRange(batch);
await _context.SaveChangesAsync();
}
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
private async Task UpdateLogFileStatsAsync(Guid logFileId, int qfodCount, int plossCount, int errorCount)
{
var logFile = await _context.LogFiles.FindAsync(logFileId);
if (logFile != null)
{
logFile.QfodCount = qfodCount;
logFile.PlossCount = plossCount;
logFile.ErrorCount = errorCount;
logFile.Status = ParseStatus.Completed;
await _context.SaveChangesAsync();
}
}
}
public record ParseReport(string FileName, int QfodCount, int PlossCount, int ErrorCount);
```

View File

@@ -0,0 +1,68 @@
# Architecture: WCTDataMiner
> 无线充电 FOD异物检测日志数据采集系统从日志文件中解析 Qfod 和 Ploss 数据并存储到数据库
---
## 文档索引
| 文档 | 说明 |
|------|------|
| [技术选型](./技术选型.md) | 技术栈总览、决策记录 |
| [模块划分](./模块划分.md) | 模块总览、分层架构、依赖关系 |
| [安全架构](./安全架构.md) | 认证方案、数据安全 |
| [解析器设计](./parser/概览.md) | Qfod/Ploss 解析器详细设计 |
| [数据处理流程](./数据处理流程.md) | 解析流程、批量写入、错误处理 |
| [配置管理](./配置管理.md) | 配置文件、配置类定义 |
| [CLI 命令设计](./CLI命令设计.md) | 命令行接口定义 |
| [数据库切换支持](./database/概览.md) | SQLite/PostgreSQL 切换方案 |
---
## 架构图
```
┌─────────────────────────────────────────────────────────────┐
│ CLI Entry Point │
│ (Program.cs / System.CommandLine) │
├─────────────────────────────────────────────────────────────┤
│ Service Layer │
├─────────────────────────────────────────────────────────────┤
│ ParseService │ ErrorHandlingService │ ThresholdCalc │
├─────────────────────────────────────────────────────────────┤
│ Parser Layer │
├─────────────────────────────────────────────────────────────┤
│ QfodParser │ PlossParser │ LogFileReader │
├─────────────────────────────────────────────────────────────┤
│ Data Layer │
├─────────────────────────────────────────────────────────────┤
│ Models │ WctMinerDbContext │ IDbContextFactory │
├─────────────────────────────────────────────────────────────┤
│ Infrastructure │
├─────────────────────────────────────────────────────────────┤
│ Configuration │ Serilog │ AppSettings │
└─────────────────────────────────────────────────────────────┘
```
---
## 核心设计原则
| 原则 | 说明 |
|------|------|
| 技术栈 | .NET 8.0 LTS + C# 12.0 + EF Core 8.0 |
| 数据库 | SQLite当前/ PostgreSQL后续配置切换 |
| 解析模式 | 批处理,正则表达式匹配 |
| ORM 模式 | Code First支持迁移自动化 |
| CLI 框架 | System.CommandLine命令模式 |
---
## 假设/歧义标注
| 标注 | 内容 |
|------|------|
| [A] | 使用 .NET 8.0 LTS 版本 |
| [A] | 初期使用 SQLite架构支持切换 PostgreSQL |
| [?] | 增量解析策略需确认 |
| [?] | 多次解析同一文件的处理策略需确认 |

View File

@@ -0,0 +1,143 @@
# 模块划分
> 所属模块:[架构概览](./概览.md)
---
## 1. 模块总览
```
┌─────────────────────────────────────────────────────────────┐
│ CLI Entry Point │
│ (Program.cs / System.CommandLine) │
├─────────────────────────────────────────────────────────────┤
│ Service Layer │
├─────────────────────────────────────────────────────────────┤
│ ParseService │ ErrorHandlingService │ ThresholdCalc │
├─────────────────────────────────────────────────────────────┤
│ Parser Layer │
├─────────────────────────────────────────────────────────────┤
│ QfodParser │ PlossParser │ LogFileReader │
├─────────────────────────────────────────────────────────────┤
│ Data Layer │
├─────────────────────────────────────────────────────────────┤
│ Models │ WctMinerDbContext │ IDbContextFactory │
├─────────────────────────────────────────────────────────────┤
│ Infrastructure │
├─────────────────────────────────────────────────────────────┤
│ Configuration │ Serilog │ AppSettings │
└─────────────────────────────────────────────────────────────┘
```
---
## 2. 模块职责
| 模块名 | 职责 | 依赖模块 |
|--------|------|----------|
| CLI Entry | 接收命令行参数、调度解析流程 | ParseService, Infrastructure |
| ParseService | 协调解析流程、批量写入、统计更新 | Parser Layer, DbContext, Infrastructure |
| LogFileReader | 读取日志文件、行过滤、文件遍历 | 无 |
| QfodParser | 解析 Qfod 格式日志,返回 Record | LogFileReader |
| PlossParser | 解析 Ploss 格式日志,返回 Record | LogFileReader |
| Models | 定义数据实体结构 | 无 |
| WctMinerDbContext | EF Core 数据库上下文 | Models |
| Configuration | 加载配置文件 | 无 |
| Serilog | 结构化日志输出 | 无 |
| ErrorHandlingService | 解析错误记录、错误统计 | 无 |
---
## 3. 分层架构
### 3.1 分层定义
| 层级 | 职责 | 允许调用 |
|------|------|----------|
| CLI Entry | 接收参数、流程调度、结果输出 | Service, Infrastructure |
| Service | 协调解析流程、批量写入、统计管理 | Parser, Data, Infrastructure |
| Parser | 日志解析、数据提取、返回 Record | 无 |
| Data | 数据持久化、DbContext 管理 | Infrastructure |
| Infrastructure | 配置、日志、错误处理 | 无 |
### 3.2 依赖规则
- 上层可调用下层
- 下层不可调用上层
- Parser 之间不可互相调用(独立解析器)
- Service 层协调 Parser 和 Data 层
---
## 4. 模块依赖图
```mermaid
graph TD
CLI[CLI Entry] --> PS[ParseService]
CLI --> Serilog
CLI --> EHS[ErrorHandlingService]
PS --> QP[QfodParser]
PS --> PP[PlossParser]
PS --> DBM[WctMinerDbContext]
PS --> EHS
PS --> Serilog
PS --> Config[Configuration]
QP --> LFR[LogFileReader]
PP --> LFR
DBM --> Models[Models]
subgraph Parser Layer
QP
PP
LFR
end
subgraph Data Layer
DBM
Models
end
subgraph Infrastructure
Config
Serilog
EHS
end
```
---
## 5. 目录结构
```
src/WCTDataMiner/
├── Parsers/
│ ├── IParser.cs
│ ├── QfodParser.cs
│ ├── PlossParser.cs
│ └── LogFileReader.cs
├── Models/
│ ├── LogFile.cs
│ ├── QfodRecord.cs
│ ├── PlossRecord.cs
│ ├── ParseError.cs
│ └── ParseStatus.cs
├── Data/
│ ├── WctMinerDbContext.cs
│ └── IDbContextFactory.cs
├── Services/
│ ├── ParseService.cs
│ ├── ThresholdCalculator.cs
│ └── ErrorHandlingService.cs
├── Configuration/
│ ├── AppSettings.cs
│ └── appsettings.json
├── Commands/
│ ├── ParseCommand.cs
│ ├── StatsCommand.cs
│ ├── ExportCommand.cs
│ └── CleanCommand.cs
└── Program.cs
```

View File

@@ -0,0 +1,105 @@
# 配置管理
> 所属模块:[架构概览](./概览.md)
---
## 1. 配置文件结构
```json
// Configuration/appsettings.json
{
"Database": {
"Type": "sqlite",
"Path": "./data/database/wctminer.db"
},
"Parser": {
"BatchSize": 1000
},
"Logging": {
"LogLevel": {
"Default": "Information",
"WCTDataMiner": "Debug"
}
},
"Paths": {
"LogDir": "./data/logs",
"OutputDir": "./data/output"
}
}
```
---
## 2. 配置类定义
```csharp
// Configuration/AppSettings.cs
namespace WCTDataMiner.Configuration;
public class AppSettings
{
public DatabaseSettings Database { get; set; } = new();
public ParserSettings Parser { get; set; } = new();
public PathSettings Paths { get; set; } = new();
}
public class DatabaseSettings
{
public string Type { get; set; } = "sqlite";
public string Path { get; set; } = "./data/database/wctminer.db";
// PostgreSQL 配置(可选)
public string? Host { get; set; }
public int Port { get; set; } = 5432;
public string? Name { get; set; }
public string? User { get; set; }
public string? Password { get; set; }
}
public class ParserSettings
{
public int BatchSize { get; set; } = 1000;
}
public class PathSettings
{
public string LogDir { get; set; } = "./data/logs";
public string OutputDir { get; set; } = "./data/output";
}
```
---
## 3. 配置加载
```csharp
// Program.cs 配置加载部分
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using WCTDataMiner.Configuration;
var builder = Host.CreateDefaultBuilder(args);
builder.ConfigureAppConfiguration(config =>
{
config.AddJsonFile("appsettings.json", optional: false);
config.AddJsonFile($"appsettings.{environment}.json", optional: true);
config.AddEnvironmentVariables();
config.AddCommandLine(args);
});
builder.ConfigureServices((context, services) =>
{
services.Configure<AppSettings>(context.Configuration.GetSection(""));
// 注册数据库服务
services.AddDatabaseServices(context.Configuration);
// 注册其他服务
services.AddScoped<QfodParser>();
services.AddScoped<PlossParser>();
services.AddScoped<ParseService>();
services.AddScoped<ErrorHandlingService>();
});
```

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 |