From 28cb2533a74ed276f06beab6e66487be8918d017 Mon Sep 17 00:00:00 2001 From: ssss <111@qq.com> Date: Thu, 2 Jul 2026 15:53:30 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=AE=8C=E5=96=84=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重命名 log_file 为 test_scenario(测试场景) - 拆分维度表:tx_panel、tx_hardware、tx_software、rx_type - 采用星型模型设计,消除数据冗余 - 为所有表添加软删除字段 is_deleted - 更新关系说明,包含全局查询过滤器配置 - 更新 ER 图和查询示例 Co-Authored-By: Claude Opus 4.8 --- docs/README.md | 61 ++++ docs/architecture/CLI命令设计.md | 168 +++++++++ docs/architecture/database/DbContext工厂.md | 122 +++++++ docs/architecture/database/概览.md | 45 +++ docs/architecture/database/迁移策略.md | 81 +++++ docs/architecture/parser/Ploss解析器.md | 165 +++++++++ docs/architecture/parser/Qfod解析器.md | 120 +++++++ docs/architecture/parser/概览.md | 78 +++++ docs/architecture/安全架构.md | 39 +++ docs/architecture/技术选型.md | 58 ++++ docs/architecture/数据处理流程.md | 205 +++++++++++ docs/architecture/概览.md | 68 ++++ docs/architecture/模块划分.md | 143 ++++++++ docs/architecture/配置管理.md | 105 ++++++ docs/data-model/entities/Ploss记录.md | 189 ++++++++++ docs/data-model/entities/Qfod记录.md | 132 +++++++ docs/data-model/entities/RxType维度.md | 89 +++++ docs/data-model/entities/TxHardware维度.md | 80 +++++ docs/data-model/entities/TxPanel维度.md | 89 +++++ docs/data-model/entities/TxSoftware维度.md | 80 +++++ docs/data-model/entities/测试场景.md | 230 +++++++++++++ docs/data-model/关系说明.md | 360 ++++++++++++++++++++ docs/data-model/概览.md | 212 ++++++++++++ 23 files changed, 2919 insertions(+) create mode 100644 docs/README.md create mode 100644 docs/architecture/CLI命令设计.md create mode 100644 docs/architecture/database/DbContext工厂.md create mode 100644 docs/architecture/database/概览.md create mode 100644 docs/architecture/database/迁移策略.md create mode 100644 docs/architecture/parser/Ploss解析器.md create mode 100644 docs/architecture/parser/Qfod解析器.md create mode 100644 docs/architecture/parser/概览.md create mode 100644 docs/architecture/安全架构.md create mode 100644 docs/architecture/技术选型.md create mode 100644 docs/architecture/数据处理流程.md create mode 100644 docs/architecture/概览.md create mode 100644 docs/architecture/模块划分.md create mode 100644 docs/architecture/配置管理.md create mode 100644 docs/data-model/entities/Ploss记录.md create mode 100644 docs/data-model/entities/Qfod记录.md create mode 100644 docs/data-model/entities/RxType维度.md create mode 100644 docs/data-model/entities/TxHardware维度.md create mode 100644 docs/data-model/entities/TxPanel维度.md create mode 100644 docs/data-model/entities/TxSoftware维度.md create mode 100644 docs/data-model/entities/测试场景.md create mode 100644 docs/data-model/关系说明.md create mode 100644 docs/data-model/概览.md diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..deb685f --- /dev/null +++ b/docs/README.md @@ -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 | \ No newline at end of file diff --git a/docs/architecture/CLI命令设计.md b/docs/architecture/CLI命令设计.md new file mode 100644 index 0000000..38450de --- /dev/null +++ b/docs/architecture/CLI命令设计.md @@ -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( + "--file", + "Path to a single log file to parse" + ); + + var dirOption = new Option( + "--dir", + "Path to directory containing log files" + ); + + var recursiveOption = new Option( + "--recursive", + () => false, + "Recursively search subdirectories" + ); + + var forceOption = new Option( + "--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(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +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())); +rootCommand.AddCommand(new StatsCommand(provider.GetRequiredService())); +rootCommand.AddCommand(new ExportCommand(provider.GetRequiredService())); +rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService())); + +return await rootCommand.InvokeAsync(args); +``` \ No newline at end of file diff --git a/docs/architecture/database/DbContext工厂.md b/docs/architecture/database/DbContext工厂.md new file mode 100644 index 0000000..cc003c4 --- /dev/null +++ b/docs/architecture/database/DbContext工厂.md @@ -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(); + + 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(); + + // 注册 DbContext(通过工厂创建) + services.AddScoped(sp => + { + var factory = sp.GetRequiredService(); + return factory.CreateDbContext(); + }); + + return services; + } +} +``` \ No newline at end of file diff --git a/docs/architecture/database/概览.md b/docs/architecture/database/概览.md new file mode 100644 index 0000000..e50eff0 --- /dev/null +++ b/docs/architecture/database/概览.md @@ -0,0 +1,45 @@ +# 数据库切换支持 + +> 所属模块:[架构概览](../概览.md) + +--- + +## 文档索引 + +| 文档 | 说明 | +|------|------| +| [DbContext 工厂](./DbContext工厂.md) | 数据库上下文工厂设计 | +| [迁移策略](./迁移策略.md) | 数据库迁移和切换步骤 | + +--- + +## 切换策略概览 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ appsettings.json │ +│ "Database": { "Type": "sqlite" | "postgresql" } │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ DbContextFactory │ +│ 读取配置 → 选择 Provider → 创建 DbContext │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ WctMinerDbContext │ +│ 统一的 DbModel 配置,适配两种数据库 │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 当前状态 + +| 项目 | 状态 | +|------|------| +| SQLite | ✅ 默认启用 | +| PostgreSQL | ✅ 架构支持,待配置 | +| 切换方式 | 配置文件切换 | \ No newline at end of file diff --git a/docs/architecture/database/迁移策略.md b/docs/architecture/database/迁移策略.md new file mode 100644 index 0000000..07e6660 --- /dev/null +++ b/docs/architecture/database/迁移策略.md @@ -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 +``` \ No newline at end of file diff --git a/docs/architecture/parser/Ploss解析器.md b/docs/architecture/parser/Ploss解析器.md new file mode 100644 index 0000000..d7fb9bb --- /dev/null +++ b/docs/architecture/parser/Ploss解析器.md @@ -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 +{ + 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 Parse(string line, int lineNumber, Guid logFileId) + { + var match = PlossPattern.Match(line); + if (!match.Success) + { + return ParseResult.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.Success(record); + } + catch (Exception ex) + { + return ParseResult.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 | 高功率允许更多损耗 | \ No newline at end of file diff --git a/docs/architecture/parser/Qfod解析器.md b/docs/architecture/parser/Qfod解析器.md new file mode 100644 index 0000000..bf19710 --- /dev/null +++ b/docs/architecture/parser/Qfod解析器.md @@ -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 +{ + 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 Parse(string line, int lineNumber, Guid logFileId) + { + var match = QfodPattern.Match(line); + if (!match.Success) + { + return ParseResult.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.Success(record); + } + catch (Exception ex) + { + return ParseResult.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 | \ No newline at end of file diff --git a/docs/architecture/parser/概览.md b/docs/architecture/parser/概览.md new file mode 100644 index 0000000..3d3d36c --- /dev/null +++ b/docs/architecture/parser/概览.md @@ -0,0 +1,78 @@ +# 解析器设计 + +> 所属模块:[架构概览](../概览.md) + +--- + +## 文档索引 + +| 文档 | 说明 | +|------|------| +| [Qfod 解析器](./Qfod解析器.md) | Qfod 格式日志解析设计 | +| [Ploss 解析器](./Ploss解析器.md) | Ploss FOD 格式日志解析设计 | + +--- + +## 通用接口定义 + +```csharp +// Parsers/IParser.cs +namespace WCTDataMiner.Parsers; + +public interface IParser +{ + /// + /// 判断是否可以解析该行 + /// + bool CanParse(string line); + + /// + /// 解析日志行 + /// + ParseResult Parse(string line, int lineNumber, Guid logFileId); +} + +public record ParseResult +{ + public bool IsSuccess { get; init; } + public TRecord? Record { get; init; } + public string? ErrorType { get; init; } + public string? ErrorMessage { get; init; } + + public static ParseResult Success(TRecord record) => + new() { IsSuccess = true, Record = record }; + + public static ParseResult 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 | 字段值超出有效范围 | 使用边界值或跳过 | \ No newline at end of file diff --git a/docs/architecture/安全架构.md b/docs/architecture/安全架构.md new file mode 100644 index 0000000..a12c47f --- /dev/null +++ b/docs/architecture/安全架构.md @@ -0,0 +1,39 @@ +# 安全架构 + +> 所属模块:[架构概览](./概览.md) + +--- + +## 1. 概述 + +> [A] 本系统为本地日志解析工具,无网络暴露,安全需求较低 + +--- + +## 2. 认证方案 + +| 项目 | 方案 | +|------|------| +| 认证方式 | [A] 无(本地工具) | +| 数据库访问 | 本地文件权限控制(SQLite)或连接串认证(PostgreSQL) | + +--- + +## 3. 数据安全 + +| 项目 | 内容 | +|------|------| +| 数据类型 | [!] 日志数据不含敏感信息,为设备检测数据 | +| 数据保护 | 本地存储,操作系统文件权限控制 | +| 备份策略 | [?] 需确认:是否需要定期备份? | + +--- + +## 4. 安全检查清单 + +| 检查项 | 状态 | +|--------|------| +| 无网络暴露 | ✅ 本地工具 | +| 数据库认证 | ✅ SQLite 本地文件权限 / PostgreSQL 连接串 | +| 敏感数据处理 | ✅ 无敏感数据 | +| 日志脱敏 | ✅ 不需要(设备检测数据) | \ No newline at end of file diff --git a/docs/architecture/技术选型.md b/docs/architecture/技术选型.md new file mode 100644 index 0000000..759a2ac --- /dev/null +++ b/docs/architecture/技术选型.md @@ -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 + + + + + + + + + + + + + + + + + + + + +``` \ No newline at end of file diff --git a/docs/architecture/数据处理流程.md b/docs/architecture/数据处理流程.md new file mode 100644 index 0000000..5f9c513 --- /dev/null +++ b/docs/architecture/数据处理流程.md @@ -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 _logger; + private readonly int _batchSize; + + public ParseService( + WctMinerDbContext context, + QfodParser qfodParser, + PlossParser plossParser, + ErrorHandlingService errorService, + ILogger logger, + IConfiguration config) + { + _context = context; + _qfodParser = qfodParser; + _plossParser = plossParser; + _errorService = errorService; + _logger = logger; + _batchSize = config.GetValue("Parser:BatchSize", 1000); + } + + public async Task ParseFileAsync(string filePath) + { + var fileInfo = new FileInfo(filePath); + var logFile = await CreateLogFileRecordAsync(fileInfo); + + var lines = await File.ReadAllLinesAsync(filePath); + var qfodRecords = new List(); + var plossRecords = new List(); + + 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 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 qfodRecords, + List plossRecords, + List 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); +``` \ No newline at end of file diff --git a/docs/architecture/概览.md b/docs/architecture/概览.md new file mode 100644 index 0000000..8ef67cf --- /dev/null +++ b/docs/architecture/概览.md @@ -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 | +| [?] | 增量解析策略需确认 | +| [?] | 多次解析同一文件的处理策略需确认 | \ No newline at end of file diff --git a/docs/architecture/模块划分.md b/docs/architecture/模块划分.md new file mode 100644 index 0000000..1143269 --- /dev/null +++ b/docs/architecture/模块划分.md @@ -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 +``` \ No newline at end of file diff --git a/docs/architecture/配置管理.md b/docs/architecture/配置管理.md new file mode 100644 index 0000000..e1eb3c5 --- /dev/null +++ b/docs/architecture/配置管理.md @@ -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(context.Configuration.GetSection("")); + + // 注册数据库服务 + services.AddDatabaseServices(context.Configuration); + + // 注册其他服务 + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); +}); +``` \ No newline at end of file diff --git a/docs/data-model/entities/Ploss记录.md b/docs/data-model/entities/Ploss记录.md new file mode 100644 index 0000000..5fb5b0b --- /dev/null +++ b/docs/data-model/entities/Ploss记录.md @@ -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; } + + /// 核心判定值:负数表示安全 + [Required] + [Column("ploss")] + public int Ploss { get; set; } + + /// 安全红线:当前功率段阈值 + [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!; + + /// 派生属性:安全余量 + [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; +} +``` \ No newline at end of file diff --git a/docs/data-model/entities/Qfod记录.md b/docs/data-model/entities/Qfod记录.md new file mode 100644 index 0000000..0a9f5f9 --- /dev/null +++ b/docs/data-model/entities/Qfod记录.md @@ -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 | \ No newline at end of file diff --git a/docs/data-model/entities/RxType维度.md b/docs/data-model/entities/RxType维度.md new file mode 100644 index 0000000..b78b3d2 --- /dev/null +++ b/docs/data-model/entities/RxType维度.md @@ -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 TestScenarios { get; set; } = []; +} +``` + +--- + +## 5. 使用说明 + +RX类型在解析文件名时自动创建,无需预置数据: + +```csharp +// 获取或创建RX类型 +public async Task 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; +} +``` \ No newline at end of file diff --git a/docs/data-model/entities/TxHardware维度.md b/docs/data-model/entities/TxHardware维度.md new file mode 100644 index 0000000..eb9e4e4 --- /dev/null +++ b/docs/data-model/entities/TxHardware维度.md @@ -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 TestScenarios { get; set; } = []; +} +``` + +--- + +## 4. 使用说明 + +硬件版本在解析文件名时自动创建,无需预置数据: + +```csharp +// 获取或创建硬件版本 +public async Task 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; +} +``` \ No newline at end of file diff --git a/docs/data-model/entities/TxPanel维度.md b/docs/data-model/entities/TxPanel维度.md new file mode 100644 index 0000000..df2d9d5 --- /dev/null +++ b/docs/data-model/entities/TxPanel维度.md @@ -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 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" } +]; +``` \ No newline at end of file diff --git a/docs/data-model/entities/TxSoftware维度.md b/docs/data-model/entities/TxSoftware维度.md new file mode 100644 index 0000000..dd67e3b --- /dev/null +++ b/docs/data-model/entities/TxSoftware维度.md @@ -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 TestScenarios { get; set; } = []; +} +``` + +--- + +## 4. 使用说明 + +软件版本在解析文件名时自动创建,无需预置数据: + +```csharp +// 获取或创建软件版本 +public async Task 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; +} +``` \ No newline at end of file diff --git a/docs/data-model/entities/测试场景.md b/docs/data-model/entities/测试场景.md new file mode 100644 index 0000000..a2e0c24 --- /dev/null +++ b/docs/data-model/entities/测试场景.md @@ -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 QfodRecords { get; set; } = []; + public ICollection PlossRecords { get; set; } = []; +} +``` + +--- + +## 7. 场景创建流程 + +### 从文件名解析 + +```csharp +// Services/ScenarioService.cs +public async Task 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 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; +} +``` \ No newline at end of file diff --git a/docs/data-model/关系说明.md b/docs/data-model/关系说明.md new file mode 100644 index 0000000..63400d8 --- /dev/null +++ b/docs/data-model/关系说明.md @@ -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(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(entity => + { + entity.HasOne(e => e.Scenario) + .WithMany(s => s.QfodRecords) + .HasForeignKey(e => e.ScenarioId) + .OnDelete(DeleteBehavior.Cascade); // 删除场景时同步删除记录 + }); + + // 场景表 → PlossRecord 关系配置 + modelBuilder.Entity(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(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().HasQueryFilter(e => !e.IsDeleted); + modelBuilder.Entity().HasQueryFilter(e => !e.IsDeleted); + modelBuilder.Entity().HasQueryFilter(e => !e.IsDeleted); + modelBuilder.Entity().HasQueryFilter(e => !e.IsDeleted); + modelBuilder.Entity().HasQueryFilter(e => !e.IsDeleted); + modelBuilder.Entity().HasQueryFilter(e => !e.IsDeleted); + modelBuilder.Entity().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); +``` \ No newline at end of file diff --git a/docs/data-model/概览.md b/docs/data-model/概览.md new file mode 100644 index 0000000..b6baf1e --- /dev/null +++ b/docs/data-model/概览.md @@ -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 | \ No newline at end of file