From 2b031995e1a18760cc3f0f9fa4a76ac58b3b11c7 Mon Sep 17 00:00:00 2001 From: Scottxjw <13374147+scottxjw@user.noreply.gitee.com> Date: Thu, 13 Aug 2026 15:01:32 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E4=B8=8E=E6=95=B0=E6=8D=AE=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/architecture/CLI命令设计.md | 174 ++++------- docs/architecture/database/DbContext工厂.md | 141 ++++----- docs/architecture/database/概览.md | 52 ++-- docs/architecture/database/迁移策略.md | 55 ++-- docs/architecture/数据处理流程.md | 313 ++++++++------------ docs/architecture/概览.md | 179 ++++++++--- docs/architecture/模块划分.md | 277 ++++++++--------- docs/architecture/配置管理.md | 101 ++++--- docs/data-model/概览.md | 271 +++-------------- 9 files changed, 678 insertions(+), 885 deletions(-) diff --git a/docs/architecture/CLI命令设计.md b/docs/architecture/CLI命令设计.md index a74b85f..33b374e 100644 --- a/docs/architecture/CLI命令设计.md +++ b/docs/architecture/CLI命令设计.md @@ -6,18 +6,19 @@ ## 1. 命令概览 -| 命令 | 说明 | -|------|------| -| `parse` | 解析日志文件并存储到数据库 | -| `stats` | 查询统计信息 | -| `export` | 导出数据 | -| `clean` | 清理数据 | +| 命令 | 说明 | 数据流 | +|------|------|--------| +| `parse` | 解析日志文件到本地库 | log → local.db | +| `aggregate` | 从本地库汇总生成发布库 | local.db → release.db | +| `export` | 导出发布库到 CSV | release.db → CSV | +| `stats` | 查询本地库统计信息 | 只读 local.db | +| `clean` | 清理本地库数据(软删除) | 只写 local.db | --- ## 2. 命令详解 -### 2.1 parse - 解析日志文件 +### 2.1 parse ```bash # 解析单个文件 @@ -29,41 +30,60 @@ 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 - 查询统计 +### 2.2 aggregate ```bash -# 查看文件解析统计 -dotnet run stats --file log.txt +# 从 local.db 汇总生成 release.db(默认重建) +dotnet run aggregate +# 追加模式(不删除已有记录) +dotnet run aggregate --rebuild false +``` + +### 2.3 export + +```bash +# 导出正式充电参数 CSV(默认) +dotnet run export --output ./data/output + +# 导出正式充电参数 CSV + legacy Qfod/Ploss +dotnet run export --type all --output ./data/output + +# 仅导出 legacy Qfod CSV +dotnet run export --type qfod --format csv + +# 导出 JSON +dotnet run export --format json --type qfod +``` + +### 2.4 stats + +```bash # 查看整体统计 dotnet run stats --summary # 按 FOD 类型统计 dotnet run stats --by-fod-type + +# 按面板统计 +dotnet run stats --by-panel + +# 按硬件版本统计 +dotnet run stats --by-hardware + +# 按 RX 类型统计 +dotnet run stats --by-rx-type ``` -### 2.3 export - 导出数据 +### 2.5 clean ```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 --scenario --confirm # 清理所有数据 dotnet run clean --all --confirm @@ -71,98 +91,20 @@ dotnet run clean --all --confirm --- -## 3. 命令实现 +## 3. 典型工作流 -```csharp -// Commands/ParseCommand.cs -using System.CommandLine; +```bash +# 1. 解析日志 +dotnet run parse --dir ./data/test_input --recursive -namespace Gpulse.WCT.DataAnalyzer.Commands; +# 2. 查看统计 +dotnet run stats --summary -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" - ); +# 3. 汇总生成发布库 +dotnet run aggregate - 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. 导出正式 CSV +dotnet run export --output ./data/output ``` ---- - -## 4. Program.cs 入口 - -```csharp -// Program.cs -using System.CommandLine; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Gpulse.WCT.DataAnalyzer.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("Gpulse.WCT.DataAnalyzer - 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 +输出文件: `./data/output/WCT-ChargingParameterDatabase.csv` \ No newline at end of file diff --git a/docs/architecture/database/DbContext工厂.md b/docs/architecture/database/DbContext工厂.md index c32bbd8..716b29e 100644 --- a/docs/architecture/database/DbContext工厂.md +++ b/docs/architecture/database/DbContext工厂.md @@ -1,16 +1,14 @@ # DbContext 工厂 -> 所属模块:[数据库切换支持](./概览.md) +> 所属模块:[数据库支持](./概览.md) --- -## 1. 接口定义 +## 1. 本地库工厂 ```csharp -// Data/IDbContextFactory.cs -using Gpulse.WCT.DataAnalyzer.Data; - -namespace Gpulse.WCT.DataAnalyzer.Data; +// Infrastructure/LocalData/IDbContextFactory.cs +namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData; public interface IDbContextFactory { @@ -18,105 +16,68 @@ public interface IDbContextFactory } ``` +工厂实现读取 `Database:LocalPath`(fallback `Database:Path`),支持 SQLite 和 PostgreSQL。 + --- -## 2. 工厂实现 +## 2. 发布库工厂 ```csharp -// Data/DbContextFactory.cs -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; +// Infrastructure/ReleaseData/IReleaseDbContextFactory.cs +namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.ReleaseData; -namespace Gpulse.WCT.DataAnalyzer.Data; - -public class DbContextFactory : IDbContextFactory +public interface IReleaseDbContextFactory { - 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); - } - } + ReleaseDbContext CreateDbContext(); } ``` +工厂实现读取 `Database:ReleasePath`,支持 SQLite 和 PostgreSQL。PostgreSQL 时使用 `Release*` 前缀配置(`ReleaseHost`, `ReleasePort`, `ReleaseName`, `ReleaseUser`, `ReleasePassword`),未设置时回退到 local 的对应配置。 + --- -## 3. 依赖注入扩展 +## 3. DI 注册 ```csharp -// Extensions/ServiceCollectionExtensions.cs -using Microsoft.Extensions.DependencyInjection; -using Gpulse.WCT.DataAnalyzer.Data; - -namespace Gpulse.WCT.DataAnalyzer.Extensions; - -public static class ServiceCollectionExtensions +// Infrastructure/Extensions/ServiceCollectionExtensions.cs +public static IServiceCollection AddDatabaseServices( + this IServiceCollection services, + IConfiguration configuration) { - public static IServiceCollection AddDatabaseServices( - this IServiceCollection services, - IConfiguration configuration) + // 本地库 + services.AddScoped(); + services.AddScoped(sp => { - // 注册 DbContext 工厂 - services.AddScoped(); - - // 注册 DbContext(通过工厂创建) - services.AddScoped(sp => - { - var factory = sp.GetRequiredService(); - return factory.CreateDbContext(); - }); + var factory = sp.GetRequiredService(); + return factory.CreateDbContext(); + }); - return services; - } + // 发布库(独立) + services.AddScoped(); + services.AddScoped(sp => + { + var factory = sp.GetRequiredService(); + return factory.CreateDbContext(); + }); + + return services; } +``` + +## 4. 启动初始化 + +```csharp +// Program.cs +using var scope = app.Services.CreateScope(); + +// 初始化本地库 +var dbContext = scope.ServiceProvider.GetRequiredService(); +await dbContext.Database.EnsureCreatedAsync(); + +// 初始化发布库 +var releaseContext = scope.ServiceProvider.GetRequiredService(); +await releaseContext.Database.EnsureCreatedAsync(); + +// 本地库种子数据 +SeedData.Initialize(dbContext); ``` \ No newline at end of file diff --git a/docs/architecture/database/概览.md b/docs/architecture/database/概览.md index e50eff0..839ffe3 100644 --- a/docs/architecture/database/概览.md +++ b/docs/architecture/database/概览.md @@ -1,4 +1,4 @@ -# 数据库切换支持 +# 数据库切换与双数据库架构 > 所属模块:[架构概览](../概览.md) @@ -8,38 +8,48 @@ | 文档 | 说明 | |------|------| -| [DbContext 工厂](./DbContext工厂.md) | 数据库上下文工厂设计 | -| [迁移策略](./迁移策略.md) | 数据库迁移和切换步骤 | +| [DbContext 工厂](./DbContext工厂.md) | 两个数据库上下文工厂设计 | +| [迁移策略](./迁移策略.md) | 数据库迁移步骤 | --- -## 切换策略概览 +## 双数据库架构 -``` -┌─────────────────────────────────────────────────────────────┐ -│ appsettings.json │ -│ "Database": { "Type": "sqlite" | "postgresql" } │ -└─────────────────────────────────────────────────────────────┘ +```text +┌─────────────────────────────────────────────────────────────────┐ +│ appsettings.json │ +│ Database:LocalPath → local.db │ +│ Database:ReleasePath → release.db │ +└─────────────────────────────────────────────────────────────────┘ │ ▼ -┌─────────────────────────────────────────────────────────────┐ -│ DbContextFactory │ -│ 读取配置 → 选择 Provider → 创建 DbContext │ -└─────────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────────────────┐ +│ DbContextFactory │ +│ 读取配置 → 选择 Provider → 创建 WctMinerDbContext │ +│ ReleaseDbContextFactory │ +│ 读取配置 → 选择 Provider → 创建 ReleaseDbContext │ +└─────────────────────────────────────────────────────────────────┘ │ ▼ -┌─────────────────────────────────────────────────────────────┐ -│ WctMinerDbContext │ -│ 统一的 DbModel 配置,适配两种数据库 │ -└─────────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────────────────┐ +│ local.db release.db │ +│ ──────── ────────── │ +│ tx_panel charging_parameter │ +│ tx_hardware 独立实体,无外键关联 │ +│ tx_software │ +│ rx_type │ +│ test_scenario │ +│ qfod_record │ +│ ploss_record │ +└─────────────────────────────────────────────────────────────────┘ ``` ---- - ## 当前状态 | 项目 | 状态 | |------|------| | SQLite | ✅ 默认启用 | -| PostgreSQL | ✅ 架构支持,待配置 | -| 切换方式 | 配置文件切换 | \ No newline at end of file +| PostgreSQL | ✅ 架构支持 | +| local.db | ✅ 解析库 | +| release.db | ✅ 发布库 | +| 双库隔离 | ✅ 无外键/导航属性 | \ No newline at end of file diff --git a/docs/architecture/database/迁移策略.md b/docs/architecture/database/迁移策略.md index 07e6660..6a1136c 100644 --- a/docs/architecture/database/迁移策略.md +++ b/docs/architecture/database/迁移策略.md @@ -1,16 +1,17 @@ # 数据库迁移策略 -> 所属模块:[数据库切换支持](./概览.md) +> 所属模块:[数据库支持](./概览.md) --- -## 1. 迁移命令 +## 1. 本地库迁移 ### SQLite ```bash # 添加迁移 -dotnet ef migrations add InitialCreate --output-dir Migrations/SQLite +dotnet ef migrations add InitialCreate --output-dir Migrations/SQLite \ + --context WctMinerDbContext --project src/Gpulse.WCT.DataAnalyzer.Core # 应用迁移 dotnet ef database update @@ -20,42 +21,43 @@ dotnet ef database update ```bash # 切换配置后添加迁移 -dotnet ef migrations add InitialCreate --output-dir Migrations/PostgreSQL -- --environment PostgreSQL - -# 应用迁移 -dotnet ef database update -- --environment PostgreSQL +dotnet ef migrations add InitialCreate --output-dir Migrations/PostgreSQL \ + --context WctMinerDbContext --project src/Gpulse.WCT.DataAnalyzer.Core \ + -- --environment Production ``` --- -## 2. 切换步骤 +## 2. 发布库迁移 -### 从 SQLite 切换到 PostgreSQL +发布库的迁移需要使用 `ReleaseDbContext`: -| 步骤 | 操作 | 说明 | -|------|------|------| -| 1 | 修改配置 | `appsettings.json` 中 `Database:Type` 改为 `postgresql` | -| 2 | 设置连接 | 配置 `Host`, `Port`, `Name`, `User`, `Password` | -| 3 | 创建数据库 | `CREATE DATABASE wctminer;` | -| 4 | 应用迁移 | `dotnet ef database update` | -| 5 | 数据迁移 | 使用工具迁移 SQLite 数据到 PostgreSQL | +```bash +# 添加迁移 +dotnet ef migrations add InitialCreate --output-dir Migrations/Release \ + --context ReleaseDbContext --project src/Gpulse.WCT.DataAnalyzer.Core + +# 应用迁移 +dotnet ef database update --context ReleaseDbContext +``` --- ## 3. 配置文件示例 -### SQLite (默认) +### SQLite(默认双库) ```json { "Database": { "Type": "sqlite", - "Path": "./data/database/wctminer.db" + "LocalPath": "./data/database/local.db", + "ReleasePath": "./data/database/release.db" } } ``` -### PostgreSQL +### PostgreSQL(双库独立) ```json { @@ -63,9 +65,8 @@ dotnet ef database update -- --environment PostgreSQL "Type": "postgresql", "Host": "localhost", "Port": 5432, - "Name": "wctminer", - "User": "wctminer_user", - "Password": "${DB_PASSWORD}" + "LocalPath": "Server=localhost;Database=wct_local", + "ReleasePath": "Server=db-server;Database=wct_charging_parameters" } } ``` @@ -77,5 +78,11 @@ dotnet ef database update -- --environment PostgreSQL 推荐使用 [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 +# 本地库 +pgloader ./data/database/local.db postgresql://user:password@localhost:5432/wct_local + +# 发布库 +pgloader ./data/database/release.db postgresql://user:password@db-server:5432/wct_charging_parameters +``` + +两个数据库独立迁移,不共享迁移历史。 \ No newline at end of file diff --git a/docs/architecture/数据处理流程.md b/docs/architecture/数据处理流程.md index 1fcba3e..f534fc7 100644 --- a/docs/architecture/数据处理流程.md +++ b/docs/architecture/数据处理流程.md @@ -4,213 +4,158 @@ --- -## 1. 完整解析流程 +## 1. 完整流程 + +```text +log 文件 + → parse + → local.db + → aggregate + → release.db + → export + → WCT-ChargingParameterDatabase.csv +``` + +--- + +## 2. 阶段一:parse → local.db ```mermaid sequenceDiagram - participant CLI as CLI Entry + participant CLI as CLI participant PS as ParseService participant SS as ScenarioService participant FNP as FileNameParser participant QP as QfodParser participant PP as PlossParser - participant DB as Database - participant Log as Serilog + participant DB as local.db - CLI->>PS: 扫描日志目录 - PS-->>CLI: 返回文件列表 + CLI->>PS: ParseFileAsync(filePath) + PS->>FNP: 解析文件名 + FNP-->>PS: ScenarioInfo + PS->>SS: GetOrCreateScenarioAsync + SS->>DB: 维度 upsert + 场景 upsert + SS-->>PS: TestScenario + PS->>PS: 逐行读取文件 - loop 每个日志文件 - CLI->>PS: ParseFileAsync(filePath) - - PS->>FNP: 解析文件名 - FNP-->>PS: 场景信息(面板、版本、RX类型等) - - PS->>SS: GetOrCreateScenarioAsync(场景信息) - SS->>DB: 查询/创建维度记录 - SS->>DB: 查询/创建场景记录 - SS-->>PS: 返回 TestScenario - - PS->>PS: 读取文件内容 - - loop 逐行遍历 - alt 是 Qfod 行 - PS->>QP: Parse(line, scenarioId) - QP-->>PS: ParseResult - else 是 Ploss 行 - PS->>PP: Parse(line, scenarioId) - PP-->>PS: ParseResult - end - - alt 解析成功 - PS->>PS: 加入记录列表 - else 解析失败 - PS->>Log: LogError(line, error) - end + loop 每行 + alt Qfod 行 + PS->>QP: Parse(line) + QP-->>PS: QfodRecord + else Ploss 行 + PS->>PP: Parse(line) + PP-->>PS: PlossRecord end - - PS->>DB: BATCH INSERT QfodRecords - PS->>DB: BATCH INSERT PlossRecords - PS->>DB: UPDATE scenario (qfod_count, ploss_count) - - PS-->>CLI: 返回 ParseReport end - CLI-->>CLI: 输出解析报告 + PS->>DB: BatchInsert QfodRecords + PS->>DB: BatchInsert PlossRecords + PS->>DB: UpdateScenarioStats + PS-->>CLI: ParseReport +``` + +**关键点:** + +- 文件名格式: `TxPanel-TxHardware-TxSoftware-RxType-Purpose-Date-Seq.log` +- 支持两行 Ploss 格式(header 行 + 数据行) +- 事务批量写入,读取 `Parser:BatchSize` 配置分批 +- 解析错误记录到 Serilog,不阻断流程 + +--- + +## 3. 阶段二:aggregate → release.db + +```mermaid +sequenceDiagram + participant CLI as CLI + participant AS as AggregationService + participant LDB as local.db + participant RDB as release.db + + CLI->>AS: AggregateAsync() + AS->>LDB: 读取所有 PlossRecord + 关联维度 + AS->>AS: 按 (车厂, 车型, 手机厂商, 型号) 分组 + AS->>AS: 计算 9 个功率列平均值 + AS->>AS: 计算 Q值/Q基值/P-Q系数 + AS->>RDB: DELETE + INSERT (事务) + AS-->>CLI: AggregationReport +``` + +**聚合规则:** + +| 字段 | 来源 | 计算方式 | +|------|------|----------| +| 车厂 (CarFactory) | 配置映射 `Aggregation:TxPanelMappings` | 找不到映射时 fallback 为 TxPanel.Name | +| 车型 (CarModel) | 配置映射 `Aggregation:CarModelMappings` | fallback 为 TxHardware.Version | +| 手机厂商 (PhoneBrand) | RxType.Name 拆分 | 取第一个分隔符前部分 | +| 型号 (PhoneModel) | RxType.Name 拆分 | 取第一个分隔符后部分 | +| 350mW~2250mW(9列) | PlossRecord.Field7 | 按功率匹配后取平均值 | +| Q值 (QValue) | QfodRecord.CurrentQ | 平均值 | +| Q基值 (QBaseValue) | QfodRecord.RawQ | 平均值 | +| P-Q值系数 (PqCoefficient) | PlossRecord.DeltaP | 平均值 | +| 谐振频率 (ResonanceFrequency) | 暂无 | 允许为空 | + +**替代配置:** + +配置文件 `appsettings.json` 中可添加映射: + +```json +{ + "Aggregation": { + "TxPanelMappings": { + "single-mold": "奇瑞", + "dual-rapid": "智己" + }, + "CarModelMappings": { + "L6": "L6", + "CM3": "CM3" + } + } +} ``` --- -## 2. ParseService 实现 +## 4. 阶段三:export → CSV -```csharp -// Services/ParseService.cs -using Microsoft.EntityFrameworkCore; -using Serilog; -using Gpulse.WCT.DataAnalyzer.Data; -using Gpulse.WCT.DataAnalyzer.Models; -using Gpulse.WCT.DataAnalyzer.Parsers; +```mermaid +sequenceDiagram + participant CLI as CLI + participant ES as ExportService + participant RDB as release.db + participant FS as Filesystem -namespace Gpulse.WCT.DataAnalyzer.Services; + CLI->>ES: ExportChargingParametersToCsvAsync(outputDir) + ES->>RDB: 读取 ChargingParameters (排序) + ES->>FS: 写入 WCT-ChargingParameterDatabase.csv + ES-->>CLI: 文件路径 +``` -public class ParseService -{ - private readonly WctMinerDbContext _context; - private readonly ScenarioService _scenarioService; - private readonly QfodParser _qfodParser; - private readonly PlossParser _plossParser; - private readonly FileNameParser _fileNameParser; - private readonly int _batchSize; +**导出规格:** - public ParseService( - WctMinerDbContext context, - ScenarioService scenarioService, - QfodParser qfodParser, - PlossParser plossParser, - FileNameParser fileNameParser, - IConfiguration config) - { - _context = context; - _scenarioService = scenarioService; - _qfodParser = qfodParser; - _plossParser = plossParser; - _fileNameParser = fileNameParser; - _batchSize = config.GetValue("Parser:BatchSize", 1000); - } +- 固定文件名: `WCT-ChargingParameterDatabase.csv` +- 固定 17 列 UTF-8 BOM +- RFC 4180 转义 +- 数字使用 invariant culture - public async Task ParseFileAsync(string filePath) - { - var fileInfo = new FileInfo(filePath); +--- - // 解析文件名获取场景信息 - var scenarioInfo = _fileNameParser.Parse(fileInfo.Name); - if (scenarioInfo == null) - { - Log.Error("无法解析文件名: {FileName}", fileInfo.Name); - return new ParseReport(fileInfo.Name, 0, 0, 1, "文件名格式不匹配"); - } +## 5. 数据库隔离 - // 获取或创建测试场景 - var scenario = await _scenarioService.GetOrCreateScenarioAsync(scenarioInfo); +```text +local.db release.db +───────── ────────── +tx_panel charging_parameter +tx_hardware (独立实体,无外键) +tx_software +rx_type +test_scenario +qfod_record +ploss_record - var lines = await File.ReadAllLinesAsync(filePath); - var qfodRecords = new List(); - var plossRecords = new List(); - int errorCount = 0; +无关联 ←→ 无关联 +``` - 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, scenario.Id); - if (result.IsSuccess) - qfodRecords.Add(result.Record!); - else - { - Log.Error("Qfod解析失败 [{Line}] {Error}: {Message}", - lineNumber, result.ErrorType, result.ErrorMessage); - errorCount++; - } - } - else if (_plossParser.CanParse(line)) - { - var result = _plossParser.Parse(line, scenario.Id); - if (result.IsSuccess) - plossRecords.Add(result.Record!); - else - { - Log.Error("Ploss解析失败 [{Line}] {Error}: {Message}", - lineNumber, result.ErrorType, result.ErrorMessage); - errorCount++; - } - } - } - - // 批量保存 - await BatchInsertAsync(qfodRecords, plossRecords); - - // 更新场景统计 - await UpdateScenarioStatsAsync(scenario.Id, qfodRecords.Count, plossRecords.Count); - - return new ParseReport(fileInfo.Name, qfodRecords.Count, plossRecords.Count, errorCount, null); - } - - private async Task BatchInsertAsync( - List qfodRecords, - List plossRecords) - { - 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(); - } - - await transaction.CommitAsync(); - } - catch - { - await transaction.RollbackAsync(); - throw; - } - } - - private async Task UpdateScenarioStatsAsync(Guid scenarioId, int qfodCount, int plossCount) - { - // 使用累加更新(+=),支持同一场景的多次解析 - // 若需幂等解析,应在 ParseFileAsync 开始时检查场景是否已处理 - var scenario = await _context.TestScenarios.FindAsync(scenarioId); - if (scenario != null) - { - scenario.QfodCount += qfodCount; - scenario.PlossCount += plossCount; - await _context.SaveChangesAsync(); - } - } -} - -public record ParseReport(string FileName, int QfodCount, int PlossCount, int ErrorCount, string? ErrorMessage); - -public record ScenarioInfo( - string TxPanel, - string TxHardware, - string TxSoftware, - string RxType, - string? TestPurpose, - DateOnly TestDate, - int TestSequence); -``` \ No newline at end of file +- 代码中不存在 local entity 到 release entity 的导航属性 +- 数据库中没有跨库外键 +- export 不读 local.db,aggregate 不写 local.db \ No newline at end of file diff --git a/docs/architecture/概览.md b/docs/architecture/概览.md index b9a9915..38ba8c1 100644 --- a/docs/architecture/概览.md +++ b/docs/architecture/概览.md @@ -1,68 +1,149 @@ # Architecture: Gpulse.WCT.DataAnalyzer -> 无线充电 FOD(异物检测)日志数据采集系统,从日志文件中解析 Qfod 和 Ploss 数据并存储到数据库 +> 无线充电参数采集系统。从 log 文件解析 Qfod/Ploss 数据 → 本地分析库 → 汇总生成正式充电参数数据库 → 导出 CSV。 --- +## 数据流 + +```text +log 文件 + → parse + → local.db(本地解析库) + → aggregate + → release.db(正式发布库) + → export + → WCT-ChargingParameterDatabase.csv +``` + +| 阶段 | 命令 | 存储 | 说明 | +|------|------|------|------| +| 解析 | `parse` | `local.db` | 本地日志解析库,保存原始 Qfod/Ploss 记录 | +| 汇总 | `aggregate` | `release.db` | 从 local.db 汇总生成正式充电参数记录 | +| 导出 | `export` | CSV | 读取 release.db,输出固定格式 CSV | + ## 文档索引 | 文档 | 说明 | |------|------| -| [技术选型](./技术选型.md) | 技术栈总览、决策记录 | -| [模块划分](./模块划分.md) | 模块总览、分层架构、依赖关系 | -| [安全架构](./安全架构.md) | 认证方案、数据安全 | -| [解析器设计](./parser/概览.md) | Qfod/Ploss 解析器详细设计 | -| [数据处理流程](./数据处理流程.md) | 解析流程、批量写入、错误处理 | -| [配置管理](./配置管理.md) | 配置文件、配置类定义 | -| [CLI 命令设计](./CLI命令设计.md) | 命令行接口定义 | -| [数据库切换支持](./database/概览.md) | SQLite/PostgreSQL 切换方案 | +| [技术选型](./技术选型.md) | 技术栈、决策记录 | +| [模块划分](./模块划分.md) | 分层架构、目录结构、模块职责 | +| [数据处理流程](./数据处理流程.md) | 解析→汇总→导出完整流程 | +| [CLI 命令设计](./CLI命令设计.md) | 命令行接口 | +| [配置管理](./配置管理.md) | 配置结构与双数据库配置 | +| [数据库支持](./database/概览.md) | 本地/发布双数据库架构 | +| [安全架构](./安全架构.md) | 路径校验、安全策略 | +| [解析器设计](./parser/概览.md) | Qfod/Ploss 解析器 | ---- - -## 架构图 +## 架构分层 +```text +┌──────────────────────────────────────────────────────────────────────┐ +│ CLI Entry Point │ +│ (Program.cs / System.CommandLine) │ +├──────────────────────────────────────────────────────────────────────┤ +│ Application Layer │ +│ ┌───────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐ │ +│ │ LocalIngestion│ │ Publishing │ │ Exporting │ │Administration│ │ +│ │ ParseService │ │AggregationSvc│ │ExportSvc │ │ StatsService │ │ +│ │ ScenarioSvc │ │ThresholdCalc │ │ │ │ CleanService │ │ +│ │ DimensionSvc │ │ │ │ │ │ │ │ +│ └───────────────┘ └──────────────┘ └────────────┘ └──────────────┘ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ Parsing │ │ +│ │ QfodParser │ PlossParser │ FileNameParser │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +├──────────────────────────────────────────────────────────────────────┤ +│ Domain Layer │ +│ ┌─────────────────────────┐ ┌──────────────────────────────────────┐ │ +│ │ Local/ (本地实体) │ │ Release/ (发布实体) │ │ +│ │ TxPanel, TxHardware │ │ ChargingParameterRecord │ │ +│ │ TxSoftware, RxType │ │ │ │ +│ │ TestScenario │ │ │ │ +│ │ QfodRecord, PlossRecord │ │ │ │ +│ └─────────────────────────┘ └──────────────────────────────────────┘ │ +├──────────────────────────────────────────────────────────────────────┤ +│ Infrastructure Layer │ +│ ┌───────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐ │ +│ │ LocalData │ │ ReleaseData │ │Configuration│ │ Security │ │ +│ │ WctMinerDbCtx │ │ReleaseDbCtx │ │AppSettings │ │PathValidator │ │ +│ │ DbCtxFactory │ │ReleaseDbCtxF │ │ │ │ │ │ +│ │ SeedData │ │ │ │ │ │ │ │ +│ └───────────────┘ └──────────────┘ └────────────┘ └──────────────┘ │ +└──────────────────────────────────────────────────────────────────────┘ ``` -┌─────────────────────────────────────────────────────────────┐ -│ 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,命令模式 | +| 技术栈 | .NET 8.0 + C# 12.0 + EF Core 8.0 | +| 数据库 | local.db / release.db 两个独立 SQLite 数据库 | +| 本地/发布隔离 | 无外键、无导航属性、无数据库级关联 | +| 解析模式 | 批处理,正则表达式 | +| CLI | System.CommandLine,命令模式 | +| 汇总规则 | 按业务键分组取平均值,缺失字段允许为空 | ---- +## 目录结构 -## 假设/歧义标注 - -| 标注 | 内容 | -|------|------| -| [A] | 使用 .NET 8.0 LTS 版本 | -| [A] | 初期使用 SQLite,架构支持切换 PostgreSQL | -| [?] | 增量解析策略需确认 | -| [?] | 多次解析同一文件的处理策略需确认 | \ No newline at end of file +```text +src/ +├── Gpulse.WCT.DataAnalyzer/ # CLI 入口 +│ ├── Commands/ # 命令实现 +│ │ ├── ParseCommand.cs +│ │ ├── AggregateCommand.cs +│ │ ├── StatsCommand.cs +│ │ ├── ExportCommand.cs +│ │ └── CleanCommand.cs +│ ├── Program.cs +│ └── appsettings.json +│ +└── Gpulse.WCT.DataAnalyzer.Core/ # 核心库 + ├── Application/ # 应用层 + │ ├── Parsing/ # 解析器 + │ │ ├── IParser.cs + │ │ ├── QfodParser.cs + │ │ ├── PlossParser.cs + │ │ └── FileNameParser.cs + │ ├── LocalIngestion/ # 本地日志解析 + │ │ ├── ParseService.cs + │ │ ├── ScenarioService.cs + │ │ └── DimensionService.cs + │ ├── Publishing/ # 发布汇总 + │ │ ├── AggregationService.cs + │ │ └── ThresholdCalculator.cs + │ ├── Exporting/ # 导出 + │ │ └── ExportService.cs + │ └── Administration/ # 管理 + │ ├── StatsService.cs + │ └── CleanService.cs + ├── Domain/ # 领域实体 + │ └── Models/ + │ ├── TxPanel.cs # 本地-维度 + │ ├── TxHardware.cs + │ ├── TxSoftware.cs + │ ├── RxType.cs + │ ├── TestScenario.cs + │ ├── QfodRecord.cs + │ ├── PlossRecord.cs + │ └── ChargingParameterRecord.cs # 发布-实体 + └── Infrastructure/ # 基础设施 + ├── LocalData/ # 本地数据库 + │ ├── WctMinerDbContext.cs + │ ├── DbContextFactory.cs + │ ├── IDbContextFactory.cs + │ ├── SeedData.cs + │ └── Configurations/ + ├── ReleaseData/ # 发布数据库 + │ ├── ReleaseDbContext.cs + │ ├── ReleaseDbContextFactory.cs + │ ├── IReleaseDbContextFactory.cs + │ └── Configurations/ + ├── Configuration/ # 配置 + │ └── AppSettings.cs + ├── Security/ # 安全 + │ ├── PathValidator.cs + │ └── SecurityConstants.cs + └── Extensions/ # DI 扩展 + └── ServiceCollectionExtensions.cs +``` \ No newline at end of file diff --git a/docs/architecture/模块划分.md b/docs/architecture/模块划分.md index 37ff7ae..4669b04 100644 --- a/docs/architecture/模块划分.md +++ b/docs/architecture/模块划分.md @@ -4,167 +4,172 @@ --- -## 1. 模块总览 +## 1. 分层架构 -``` -┌─────────────────────────────────────────────────────────────┐ -│ CLI Entry Point │ -│ (Program.cs / System.CommandLine) │ -├─────────────────────────────────────────────────────────────┤ -│ Service Layer │ -├─────────────────────────────────────────────────────────────┤ -│ ParseService │ ScenarioService │ DimensionService │ ThresholdCalc │ -├─────────────────────────────────────────────────────────────┤ -│ Parser Layer │ -├─────────────────────────────────────────────────────────────┤ -│ QfodParser │ PlossParser │ FileNameParser │ -├─────────────────────────────────────────────────────────────┤ -│ Data Layer │ -├─────────────────────────────────────────────────────────────┤ -│ Models │ WctMinerDbContext │ IDbContextFactory │ -├─────────────────────────────────────────────────────────────┤ -│ Infrastructure │ -├─────────────────────────────────────────────────────────────┤ -│ Configuration │ Serilog │ AppSettings │ -└─────────────────────────────────────────────────────────────┘ -``` +| 层级 | 目录 | 职责 | 依赖 | +|------|------|------|------| +| CLI | `Gpulse.WCT.DataAnalyzer/Commands/` | 接收参数、流程调度、结果输出 | Application | +| Application | `Core/Application/` | 业务流程编排 | Domain, Infrastructure | +| Domain | `Core/Domain/Models/` | 实体定义,无行为 | 无 | +| Infrastructure | `Core/Infrastructure/` | 数据持久化、配置、安全 | Domain | + +**依赖规则:** + +- 上层可依赖下层,下层不可依赖上层 +- Application 依 Domain 和 Infrastructure +- Infrastructure 依 Domain +- Domain 不依赖任何其他层 +- 解析器之间不可互相调用 --- -## 2. 模块职责 +## 2. Application 模块职责 -| 模块名 | 职责 | 依赖模块 | -|--------|------|----------| -| CLI Entry | 接收命令行参数、调度解析流程 | ParseService, Infrastructure | -| ParseService | 协调解析流程、批量写入、场景关联 | Parser Layer, ScenarioService, DbContext | -| ScenarioService | 创建/查询测试场景,解析文件名获取场景信息 | DimensionService, DbContext | -| DimensionService | 维度表 CRUD(获取或创建维度记录) | DbContext | -| FileNameParser | 解析文件名提取场景信息(面板、版本、RX类型等) | 无 | -| LogFileReader | 读取日志文件、行过滤、文件遍历 | 无 | -| QfodParser | 解析 Qfod 格式日志,返回 QfodRecord | 无 | -| PlossParser | 解析 Ploss 格式日志,返回 PlossRecord | 无 | -| Models | 定义数据实体结构(维度表 + 事实表) | 无 | -| WctMinerDbContext | EF Core 数据库上下文 | Models, Configurations | -| Configuration | 加载配置文件 | 无 | -| Serilog | 结构化日志输出(含解析错误日志) | 无 | -| ThresholdCalculator | 根据 RxPower 计算动态阈值 | 无 | +### 2.1 Parsing — 解析 + +| 类 | 职责 | +|----|------| +| `FileNameParser` | 从文件名提取场景信息(面板、硬件、软件、RX类型、日期、序号) | +| `QfodParser` | 解析 Qfod 格式日志行 → `QfodRecord` | +| `PlossParser` | 解析 Ploss 单行/两行格式日志 → `PlossRecord` | + +### 2.2 LocalIngestion — 本地解析 + +| 类 | 职责 | +|----|------| +| `ParseService` | 协调解析流程:读取文件 → 解析 → 批量写入 local.db | +| `ScenarioService` | 创建/查询测试场景,并发安全 upsert | +| `DimensionService` | 维度表 get-or-create | + +### 2.3 Publishing — 发布汇总 + +| 类 | 职责 | +|----|------| +| `AggregationService` | 读取 local.db → 按业务键分组计算 → upsert 到 release.db | +| `ThresholdCalculator` | 动态阈值计算 | + +### 2.4 Exporting — 导出 + +| 类 | 职责 | +|----|------| +| `ExportService` | 导出 release.db 到固定 CSV,也支持 legacy Qfod/Ploss 导出 | + +### 2.5 Administration — 管理 + +| 类 | 职责 | +|----|------| +| `StatsService` | 本地库统计查询 | +| `CleanService` | 软删除管理 | --- -## 3. 分层架构 +## 3. Domain 实体 -### 3.1 分层定义 +### 3.1 本地实体(local.db) -| 层级 | 职责 | 允许调用 | -|------|------|----------| -| CLI Entry | 接收参数、流程调度、结果输出 | Service, Infrastructure | -| Service | 协调解析流程、场景管理、维度管理、批量写入 | Parser, Data, Infrastructure | -| Parser | 日志解析、文件名解析、数据提取 | 无 | -| Data | 数据持久化、DbContext 管理、实体配置 | Infrastructure | -| Infrastructure | 配置、日志 | 无 | +| 实体 | 表 | 说明 | +|------|----|------| +| `TxPanel` | tx_panel | TX 面板类型维度 | +| `TxHardware` | tx_hardware | TX 硬件版本维度 | +| `TxSoftware` | tx_software | TX 软件版本维度 | +| `RxType` | rx_type | RX 类型维度 | +| `TestScenario` | test_scenario | 测试场景(4 个维度外键 + 日期 + 序号) | +| `QfodRecord` | qfod_record | Qfod 检测数据 | +| `PlossRecord` | ploss_record | Ploss FOD 数据 | -### 3.2 依赖规则 +关系:维度表 1:N TestScenario 1:N QfodRecord/PlossRecord(星型模型) -- 上层可调用下层 -- 下层不可调用上层 -- Parser 之间不可互相调用(独立解析器) -- Service 层协调 Parser 和 Data 层 -- 解析错误通过 Serilog 记录,不存入业务数据库 +### 3.2 发布实体(release.db) + +| 实体 | 表 | 唯一键 | +|------|----|--------| +| `ChargingParameterRecord` | charging_parameter | (car_factory, car_model, phone_brand, phone_model) | + +**两个数据库不建立外键或导航属性关联。** --- -## 4. 模块依赖图 +## 4. Infrastructure 模块 + +### 4.1 LocalData — 本地数据库 + +| 类 | 职责 | +|----|------| +| `WctMinerDbContext` | 本地解析库 EF Core 上下文 | +| `DbContextFactory` | 根据配置创建 SQLite/PostgreSQL 本地上下文 | +| `IDbContextFactory` | 工厂接口 | +| `SeedData` | 默认维度数据初始化 | +| `Configurations/*` | EF Core `IEntityTypeConfiguration` 实现 | + +### 4.2 ReleaseData — 发布数据库 + +| 类 | 职责 | +|----|------| +| `ReleaseDbContext` | 发布库 EF Core 上下文 | +| `ReleaseDbContextFactory` | 创建发布库上下文 | +| `IReleaseDbContextFactory` | 工厂接口 | +| `Configurations/*` | 发布实体配置 | + +### 4.3 Configuration + +| 类 | 职责 | +|----|------| +| `AppSettings` | 应用配置 POCO | + +### 4.4 Security + +| 类 | 职责 | +|----|------| +| `PathValidator` | 路径安全校验(遍历保护、白名单、应用目录限制) | +| `SecurityConstants` | 安全常量 | + +### 4.5 Extensions + +| 类 | 职责 | +|----|------| +| `ServiceCollectionExtensions` | `AddDatabaseServices` / `AddApplicationServices` DI 注册 | + +--- + +## 5. 依赖图 ```mermaid graph TD - CLI[CLI Entry] --> PS[ParseService] - CLI --> Serilog + CLI[CLI Commands] --> Parsing[Parsing] + CLI --> LocalIngestion[LocalIngestion] + CLI --> Publishing[Publishing] + CLI --> Exporting[Exporting] + CLI --> Admin[Administration] - PS --> SS[ScenarioService] - PS --> QP[QfodParser] - PS --> PP[PlossParser] - PS --> DBM[WctMinerDbContext] - PS --> Serilog + LocalIngestion --> Parsing + LocalIngestion --> LocalData[LocalData] + Publishing --> LocalData + Publishing --> ReleaseData[ReleaseData] + Exporting --> LocalData + Exporting --> ReleaseData + Admin --> LocalData - SS --> DS[DimensionService] - SS --> FNP[FileNameParser] - SS --> DBM + LocalData --> Domain[Domain Models] + ReleaseData --> Domain + Parsing --> Domain - DS --> DBM - - QP --> Serilog - PP --> Serilog - FNP --> Serilog - - DBM --> Models[Models] - DBM --> Configs[Configurations] - - subgraph Parser Layer - QP - PP - FNP + subgraph Application + Parsing + LocalIngestion + Publishing + Exporting + Admin end - subgraph Data Layer - DBM - Models - Configs + subgraph Domain + Domain end subgraph Infrastructure - Serilog + LocalData + ReleaseData + Configuration + Security + Extensions end - - subgraph Service Layer - PS - SS - DS - end -``` - ---- - -## 5. 目录结构 - -``` -src/Gpulse.WCT.DataAnalyzer/ -├── Parsers/ -│ ├── IParser.cs -│ ├── QfodParser.cs -│ ├── PlossParser.cs -│ ├── FileNameParser.cs # 解析文件名获取场景信息(Parser Layer) -│ └── LogFileReader.cs # 读取日志文件(可选) -├── Models/ -│ ├── TxPanel.cs # TX面板维度 -│ ├── TxHardware.cs # TX硬件版本维度 -│ ├── TxSoftware.cs # TX软件版本维度 -│ ├── RxType.cs # RX类型维度 -│ ├── TestScenario.cs # 测试场景 -│ ├── QfodRecord.cs # Qfod记录 -│ └── PlossRecord.cs # Ploss记录 -├── Data/ -│ ├── WctMinerDbContext.cs -│ ├── IDbContextFactory.cs -│ └── Configurations/ # IEntityTypeConfiguration 配置类 -│ ├── TxPanelConfiguration.cs -│ ├── TxHardwareConfiguration.cs -│ ├── TxSoftwareConfiguration.cs -│ ├── RxTypeConfiguration.cs -│ ├── TestScenarioConfiguration.cs -│ ├── QfodRecordConfiguration.cs -│ └── PlossRecordConfiguration.cs -├── Services/ -│ ├── ParseService.cs # 解析流程协调 -│ ├── ScenarioService.cs # 场景创建/查询 -│ ├── DimensionService.cs # 维度表管理 -│ └── ThresholdCalculator.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 index 083ea7b..816e92b 100644 --- a/docs/architecture/配置管理.md +++ b/docs/architecture/配置管理.md @@ -4,27 +4,31 @@ --- -## 1. 配置文件结构 +## 1. 配置文件 ```json -// Configuration/appsettings.json { "Database": { "Type": "sqlite", - "Path": "./data/database/wctminer.db" + "LocalPath": "./data/database/local.db", + "ReleasePath": "./data/database/release.db" }, "Parser": { "BatchSize": 1000 }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Gpulse.WCT.DataAnalyzer": "Debug" - } - }, "Paths": { "LogDir": "./data/logs", "OutputDir": "./data/output" + }, + "Aggregation": { + "TxPanelMappings": { + "single-mold": "奇瑞", + "dual-rapid": "智己" + }, + "CarModelMappings": { + "L6": "L6", + "CM3": "CM3" + } } } ``` @@ -34,8 +38,8 @@ ## 2. 配置类定义 ```csharp -// Configuration/AppSettings.cs -namespace Gpulse.WCT.DataAnalyzer.Configuration; +// Infrastructure/Configuration/AppSettings.cs +namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Configuration; public class AppSettings { @@ -47,8 +51,9 @@ public class AppSettings public class DatabaseSettings { public string Type { get; set; } = "sqlite"; - public string Path { get; set; } = "./data/database/wctminer.db"; - + public string LocalPath { get; set; } = "./data/database/local.db"; + public string ReleasePath { get; set; } = "./data/database/release.db"; + // PostgreSQL 配置(可选) public string? Host { get; set; } public int Port { get; set; } = 5432; @@ -71,35 +76,51 @@ public class PathSettings --- -## 3. 配置加载 +## 3. 数据库配置说明 -```csharp -// Program.cs 配置加载部分 -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Gpulse.WCT.DataAnalyzer.Configuration; +### 双数据库路径 -var builder = Host.CreateDefaultBuilder(args); +| 配置键 | 默认值 | 说明 | +|--------|--------|------| +| `Database:LocalPath` | `./data/database/local.db` | 本地解析库 | +| `Database:ReleasePath` | `./data/database/release.db` | 正式发布库 | -builder.ConfigureAppConfiguration(config => +两个数据库独立配置,可以位于不同路径,也可以使用不同类型的数据库。 + +### 兼容旧配置 + +`Database:Path` 仍然作为 local 数据库的 fallback 路径,推荐使用 `Database:LocalPath`。 + +### PostgreSQL 支持 + +当 `Database:Type` 设为 `postgresql` 时,local 和 release 都可以使用独立的 PostgreSQL 连接: + +```json { - config.AddJsonFile("appsettings.json", optional: false); - config.AddJsonFile($"appsettings.{environment}.json", optional: true); - config.AddEnvironmentVariables(); - config.AddCommandLine(args); -}); + "Database": { + "Type": "postgresql", + "Host": "localhost", + "Port": 5432, + "Name": "wct_local", + "User": "wct_user", + "Password": "...", + "ReleaseHost": "db-server", + "ReleaseName": "wct_charging_parameters", + "ReleaseUser": "wct_release", + "ReleasePassword": "..." + } +} +``` -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 +Release 数据库的 PostgreSQL 配置前缀为 `Release*`,未设置时回退到 local 的对应配置。 + +--- + +## 4. 聚合配置 + +| 配置键 | 说明 | +|--------|------| +| `Aggregation:TxPanelMappings` | TxPanel 名称 → 车厂名称的映射表 | +| `Aggregation:CarModelMappings` | TxHardware 版本 → 车型名称的映射表 | + +未配置映射时,直接使用 TxPanel.Name 和 TxHardware.Version 作为最终值。手机品牌/型号从 RxType.Name 自动拆分(分隔符 `/_:`)。 \ No newline at end of file diff --git a/docs/data-model/概览.md b/docs/data-model/概览.md index 913bc47..6fe8e54 100644 --- a/docs/data-model/概览.md +++ b/docs/data-model/概览.md @@ -1,12 +1,12 @@ # Data Model: Gpulse.WCT.DataAnalyzer -> Gpulse.WCT.DataAnalyzer 数据库模型规格,采用维度表设计,消除数据冗余 +> 双数据库数据模型:本地解析库 + 正式发布库,相互独立。 --- ## 文档索引 -### 维度表 +### 本地库(local.db) | 文档 | 说明 | |------|------| @@ -14,253 +14,74 @@ | [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) | 实体间关系定义 | +| [关系说明](./关系说明.md) | 本地库实体间关系 | + +### 发布库(release.db) + +| 实体 | 表 | 说明 | +|------|----|------| +| ChargingParameterRecord | charging_parameter | 正式充电参数记录 | --- -## 实体列表 - -### 维度表(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判定结果" + %% 发布库(独立) + charging_parameter { + uuid id PK + varchar car_factory UK + varchar car_model UK + varchar phone_brand UK + varchar phone_model UK + double power_350mw + double power_500mw + double power_750mw + double power_1000mw + double power_1250mw + double power_1500mw + double power_1750mw + double power_2000mw + double power_2250mw + double q_value + double q_base_value + double pq_coefficient + double resonance_frequency } ``` ---- +框外是本地库,框内是发布库——两库之间没有外键或导航属性。 ## 设计说明 -### 星型模型架构 +### 本地库:星型模型 -本设计采用星型模型(Star Schema): +- 维度表:TxPanel, TxHardware, TxSoftware, RxType +- 事实表:TestScenario, QfodRecord, PlossRecord +- 所有表支持软删除(is_deleted + query filter) -- **维度表**:存储可能重复的属性值(面板、硬件版本、软件版本、RX类型) -- **事实表**:存储外键组合和度量数据 +### 发布库:独立扁平表 -**优势**: -- 维度值只存一份,消除冗余 -- 维度可复用,相同硬件版本在不同场景中引用同一记录 -- 查询可按维度灵活筛选和分组 +- 一个业务多维表,无外键依赖 +- 唯一键:(car_factory, car_model, phone_brand, phone_model) +- 无 is_deleted、无软删除、无审计字段 +- 更新时间 updated_at 记录发布记录的最后变更时间 -### 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(); - } -} -``` - -### 实体配置方式 - -采用 `IEntityTypeConfiguration` Fluent API 配置,实体类保持纯净 POCO: - -```csharp -// Data/WctMinerDbContext.cs -using Microsoft.EntityFrameworkCore; -using Gpulse.WCT.DataAnalyzer.Models; - -namespace Gpulse.WCT.DataAnalyzer.Data; - -public class WctMinerDbContext : DbContext -{ - public WctMinerDbContext(DbContextOptions options) - : base(options) { } - - public DbSet TxPanels => Set(); - public DbSet TxHardwares => Set(); - public DbSet TxSoftwares => Set(); - public DbSet RxTypes => Set(); - public DbSet TestScenarios => Set(); - public DbSet QfodRecords => Set(); - public DbSet PlossRecords => Set(); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - // 自动应用所有 IEntityTypeConfiguration 配置 - modelBuilder.ApplyConfigurationsFromAssembly(typeof(WctMinerDbContext).Assembly); - } -} -``` - -> **数据库兼容性说明**: -> - `HasDefaultValueSql("NOW()")` 适用于 PostgreSQL -> - 若使用 SQLite,需改为 `HasDefaultValueSql("datetime('now')")` -> - 若使用 MySQL,需改为 `HasDefaultValueSql("NOW()")`(MySQL 也支持) -> - 若使用 SQL Server,需改为 `HasDefaultValueSql("GETDATE()")` - -**配置类列表**: - -| 配置类 | 文件路径 | 说明 | -|--------|----------|------| -| TxPanelConfiguration | Data/Configurations/TxPanelConfiguration.cs | TX面板维度 | -| TxHardwareConfiguration | Data/Configurations/TxHardwareConfiguration.cs | TX硬件版本维度 | -| TxSoftwareConfiguration | Data/Configurations/TxSoftwareConfiguration.cs | TX软件版本维度 | -| RxTypeConfiguration | Data/Configurations/RxTypeConfiguration.cs | RX类型维度 | -| TestScenarioConfiguration | Data/Configurations/TestScenarioConfiguration.cs | 测试场景 | -| QfodRecordConfiguration | Data/Configurations/QfodRecordConfiguration.cs | Qfod记录 | -| PlossRecordConfiguration | Data/Configurations/PlossRecordConfiguration.cs | Ploss记录 | - -> **迁移注意事项**:从 DataAnnotations `DateTime.UtcNow` 迁移到 `HasDefaultValueSql("NOW()")` 后,测试代码需注意:未显式设置 `CreatedAt` 时,值由数据库生成而非 C# 运行时。单元测试中需 mock 数据库或显式设置值。 - -### 系统日志处理 - -解析过程中的错误日志通过 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 +- 代码中两个实体的命名空间不同(Domain.Models) +- 两个 DbContext 不共享 DbSet +- 发布库不从本地库继承实体配置 +- aggregate 通过纯值映射,不依赖导航属性 \ No newline at end of file