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:
61
docs/README.md
Normal file
61
docs/README.md
Normal 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 |
|
||||||
168
docs/architecture/CLI命令设计.md
Normal file
168
docs/architecture/CLI命令设计.md
Normal 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);
|
||||||
|
```
|
||||||
122
docs/architecture/database/DbContext工厂.md
Normal file
122
docs/architecture/database/DbContext工厂.md
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
45
docs/architecture/database/概览.md
Normal file
45
docs/architecture/database/概览.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# 数据库切换支持
|
||||||
|
|
||||||
|
> 所属模块:[架构概览](../概览.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文档索引
|
||||||
|
|
||||||
|
| 文档 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| [DbContext 工厂](./DbContext工厂.md) | 数据库上下文工厂设计 |
|
||||||
|
| [迁移策略](./迁移策略.md) | 数据库迁移和切换步骤 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 切换策略概览
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ appsettings.json │
|
||||||
|
│ "Database": { "Type": "sqlite" | "postgresql" } │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ DbContextFactory │
|
||||||
|
│ 读取配置 → 选择 Provider → 创建 DbContext │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ WctMinerDbContext │
|
||||||
|
│ 统一的 DbModel 配置,适配两种数据库 │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
|
||||||
|
| 项目 | 状态 |
|
||||||
|
|------|------|
|
||||||
|
| SQLite | ✅ 默认启用 |
|
||||||
|
| PostgreSQL | ✅ 架构支持,待配置 |
|
||||||
|
| 切换方式 | 配置文件切换 |
|
||||||
81
docs/architecture/database/迁移策略.md
Normal file
81
docs/architecture/database/迁移策略.md
Normal 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
|
||||||
|
```
|
||||||
165
docs/architecture/parser/Ploss解析器.md
Normal file
165
docs/architecture/parser/Ploss解析器.md
Normal 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 | 高功率允许更多损耗 |
|
||||||
120
docs/architecture/parser/Qfod解析器.md
Normal file
120
docs/architecture/parser/Qfod解析器.md
Normal 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 |
|
||||||
78
docs/architecture/parser/概览.md
Normal file
78
docs/architecture/parser/概览.md
Normal 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 | 字段值超出有效范围 | 使用边界值或跳过 |
|
||||||
39
docs/architecture/安全架构.md
Normal file
39
docs/architecture/安全架构.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 安全架构
|
||||||
|
|
||||||
|
> 所属模块:[架构概览](./概览.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
> [A] 本系统为本地日志解析工具,无网络暴露,安全需求较低
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 认证方案
|
||||||
|
|
||||||
|
| 项目 | 方案 |
|
||||||
|
|------|------|
|
||||||
|
| 认证方式 | [A] 无(本地工具) |
|
||||||
|
| 数据库访问 | 本地文件权限控制(SQLite)或连接串认证(PostgreSQL) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 数据安全
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 数据类型 | [!] 日志数据不含敏感信息,为设备检测数据 |
|
||||||
|
| 数据保护 | 本地存储,操作系统文件权限控制 |
|
||||||
|
| 备份策略 | [?] 需确认:是否需要定期备份? |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 安全检查清单
|
||||||
|
|
||||||
|
| 检查项 | 状态 |
|
||||||
|
|--------|------|
|
||||||
|
| 无网络暴露 | ✅ 本地工具 |
|
||||||
|
| 数据库认证 | ✅ SQLite 本地文件权限 / PostgreSQL 连接串 |
|
||||||
|
| 敏感数据处理 | ✅ 无敏感数据 |
|
||||||
|
| 日志脱敏 | ✅ 不需要(设备检测数据) |
|
||||||
58
docs/architecture/技术选型.md
Normal file
58
docs/architecture/技术选型.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# 技术选型
|
||||||
|
|
||||||
|
> 所属模块:[架构概览](./概览.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 技术栈总览
|
||||||
|
|
||||||
|
| 层级 | 技术选型 | 版本 | 选型理由 |
|
||||||
|
|------|----------|------|----------|
|
||||||
|
| 运行时 | .NET | 8.0 LTS | 长期支持版本,性能优秀,跨平台 |
|
||||||
|
| 语言 | C# | 12.0 | 强类型、现代语法、优秀的正则表达式支持 |
|
||||||
|
| ORM | Entity Framework Core | 8.0 | .NET 标准 ORM,Code 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>
|
||||||
|
```
|
||||||
205
docs/architecture/数据处理流程.md
Normal file
205
docs/architecture/数据处理流程.md
Normal 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);
|
||||||
|
```
|
||||||
68
docs/architecture/概览.md
Normal file
68
docs/architecture/概览.md
Normal 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 |
|
||||||
|
| [?] | 增量解析策略需确认 |
|
||||||
|
| [?] | 多次解析同一文件的处理策略需确认 |
|
||||||
143
docs/architecture/模块划分.md
Normal file
143
docs/architecture/模块划分.md
Normal 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
|
||||||
|
```
|
||||||
105
docs/architecture/配置管理.md
Normal file
105
docs/architecture/配置管理.md
Normal 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>();
|
||||||
|
});
|
||||||
|
```
|
||||||
189
docs/data-model/entities/Ploss记录.md
Normal file
189
docs/data-model/entities/Ploss记录.md
Normal 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;
|
||||||
|
}
|
||||||
|
```
|
||||||
132
docs/data-model/entities/Qfod记录.md
Normal file
132
docs/data-model/entities/Qfod记录.md
Normal 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 |
|
||||||
89
docs/data-model/entities/RxType维度.md
Normal file
89
docs/data-model/entities/RxType维度.md
Normal 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;
|
||||||
|
}
|
||||||
|
```
|
||||||
80
docs/data-model/entities/TxHardware维度.md
Normal file
80
docs/data-model/entities/TxHardware维度.md
Normal 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;
|
||||||
|
}
|
||||||
|
```
|
||||||
89
docs/data-model/entities/TxPanel维度.md
Normal file
89
docs/data-model/entities/TxPanel维度.md
Normal 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" }
|
||||||
|
];
|
||||||
|
```
|
||||||
80
docs/data-model/entities/TxSoftware维度.md
Normal file
80
docs/data-model/entities/TxSoftware维度.md
Normal 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;
|
||||||
|
}
|
||||||
|
```
|
||||||
230
docs/data-model/entities/测试场景.md
Normal file
230
docs/data-model/entities/测试场景.md
Normal 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;
|
||||||
|
}
|
||||||
|
```
|
||||||
360
docs/data-model/关系说明.md
Normal file
360
docs/data-model/关系说明.md
Normal 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
212
docs/data-model/概览.md
Normal 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 |
|
||||||
Reference in New Issue
Block a user