Compare commits

...

4 Commits

Author SHA1 Message Date
Scottxjw
2b031995e1 docs: 更新项目架构与数据模型文档
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-13 15:01:32 +08:00
Scottxjw
a3100c73c5 refactor(core): 重构核心分层结构并添加发布库聚合
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-13 15:00:39 +08:00
Scottxjw
6e197a0cce docs: 同步项目重命名后的架构文档 2026-08-13 13:29:56 +08:00
Scottxjw
645f46abcd refactor(project): 迁移项目命名并移除WPF客户端 2026-08-13 13:29:56 +08:00
98 changed files with 1225 additions and 2837 deletions

View File

@@ -3,11 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WCTDataMiner.Core", "src\WCTDataMiner.Core\WCTDataMiner.Core.csproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345601}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gpulse.WCT.DataAnalyzer.Core", "src\Gpulse.WCT.DataAnalyzer.Core\Gpulse.WCT.DataAnalyzer.Core.csproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345601}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WCTDataMiner.Wpf", "src\WCTDataMiner.Wpf\WCTDataMiner.Wpf.csproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345602}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WCTDataMiner", "src\WCTDataMiner\WCTDataMiner.csproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345603}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gpulse.WCT.DataAnalyzer", "src\Gpulse.WCT.DataAnalyzer\Gpulse.WCT.DataAnalyzer.csproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345603}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -19,10 +17,6 @@ Global
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345602}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345602}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345602}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345602}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@@ -1,4 +1,4 @@
# WCTDataMiner 文档中心
# Gpulse.WCT.DataAnalyzer 文档中心
> 无线充电 FOD异物检测日志数据采集系统

View File

@@ -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 <guid> --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 WCTDataMiner.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<string>(
"--file",
"Path to a single log file to parse"
);
# 3. 汇总生成发布库
dotnet run aggregate
var dirOption = new Option<string>(
"--dir",
"Path to directory containing log files"
);
var recursiveOption = new Option<bool>(
"--recursive",
() => false,
"Recursively search subdirectories"
);
var forceOption = new Option<bool>(
"--force",
() => false,
"Force re-parse even if file already parsed"
);
AddOption(fileOption);
AddOption(dirOption);
AddOption(recursiveOption);
AddOption(forceOption);
this.SetHandler(async (file, dir, recursive, force) =>
{
if (!string.IsNullOrEmpty(file))
{
await parseService.ParseFileAsync(file, force);
}
else if (!string.IsNullOrEmpty(dir))
{
await parseService.ParseDirectoryAsync(dir, recursive, force);
}
else
{
Console.WriteLine("Please specify --file or --dir");
}
}, fileOption, dirOption, recursiveOption, forceOption);
}
}
# 4. 导出正式 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 WCTDataMiner.Commands;
var builder = Host.CreateApplicationBuilder(args);
// 注册服务
builder.Services.AddScoped<ParseService>();
builder.Services.AddScoped<StatsService>();
builder.Services.AddScoped<ExportService>();
builder.Services.AddScoped<CleanService>();
builder.Services.AddDatabaseServices(builder.Configuration);
var app = builder.Build();
// 创建根命令
var rootCommand = new RootCommand("WCTDataMiner - FOD Log Data Parser");
// 使用服务提供者获取服务
using var scope = app.Services.CreateScope();
var provider = scope.ServiceProvider;
rootCommand.AddCommand(new ParseCommand(provider.GetRequiredService<ParseService>()));
rootCommand.AddCommand(new StatsCommand(provider.GetRequiredService<StatsService>()));
rootCommand.AddCommand(new ExportCommand(provider.GetRequiredService<ExportService>()));
rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService<CleanService>()));
return await rootCommand.InvokeAsync(args);
```
输出文件: `./data/output/WCT-ChargingParameterDatabase.csv`

View File

@@ -1,16 +1,14 @@
# DbContext 工厂
> 所属模块:[数据库切换支持](./概览.md)
> 所属模块:[数据库支持](./概览.md)
---
## 1. 接口定义
## 1. 本地库工厂
```csharp
// Data/IDbContextFactory.cs
using WCTDataMiner.Data;
namespace WCTDataMiner.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 WCTDataMiner.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<WctMinerDbContext>();
switch (dbType)
{
case "postgresql":
case "postgres":
var pgConnStr = BuildPostgreSqlConnectionString();
optionsBuilder.UseNpgsql(pgConnStr);
break;
case "sqlite":
default:
var dbPath = _configuration["Database:Path"] ?? "./data/database/wctminer.db";
EnsureDirectoryExists(dbPath);
optionsBuilder.UseSqlite($"Data Source={dbPath}");
break;
}
#if DEBUG
optionsBuilder.EnableSensitiveDataLogging();
optionsBuilder.EnableDetailedErrors();
#endif
return new WctMinerDbContext(optionsBuilder.Options);
}
private string BuildPostgreSqlConnectionString()
{
var host = _configuration["Database:Host"] ?? "localhost";
var port = _configuration["Database:Port"] ?? "5432";
var name = _configuration["Database:Name"] ?? "wctminer";
var user = _configuration["Database:User"] ?? "postgres";
var password = _configuration["Database:Password"] ?? "";
return $"Host={host};Port={port};Database={name};Username={user};Password={password}";
}
private static void EnsureDirectoryExists(string dbPath)
{
var directory = Path.GetDirectoryName(dbPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
}
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 WCTDataMiner.Data;
namespace WCTDataMiner.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<IDbContextFactory, DbContextFactory>();
services.AddScoped<WctMinerDbContext>(sp =>
{
// 注册 DbContext 工厂
services.AddScoped<IDbContextFactory, DbContextFactory>();
// 注册 DbContext通过工厂创建
services.AddScoped<WctMinerDbContext>(sp =>
{
var factory = sp.GetRequiredService<IDbContextFactory>();
return factory.CreateDbContext();
});
var factory = sp.GetRequiredService<IDbContextFactory>();
return factory.CreateDbContext();
});
return services;
}
// 发布库(独立)
services.AddScoped<IReleaseDbContextFactory, ReleaseDbContextFactory>();
services.AddScoped<ReleaseDbContext>(sp =>
{
var factory = sp.GetRequiredService<IReleaseDbContextFactory>();
return factory.CreateDbContext();
});
return services;
}
```
## 4. 启动初始化
```csharp
// Program.cs
using var scope = app.Services.CreateScope();
// 初始化本地库
var dbContext = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
await dbContext.Database.EnsureCreatedAsync();
// 初始化发布库
var releaseContext = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>();
await releaseContext.Database.EnsureCreatedAsync();
// 本地库种子数据
SeedData.Initialize(dbContext);
```

View File

@@ -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 | ✅ 架构支持,待配置 |
| 切换方式 | 配置文件切换 |
| PostgreSQL | ✅ 架构支持 |
| local.db | ✅ 解析库 |
| release.db | ✅ 发布库 |
| 双库隔离 | ✅ 无外键/导航属性 |

View File

@@ -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
```
# 本地库
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
```
两个数据库独立迁移,不共享迁移历史。

View File

@@ -39,9 +39,9 @@
```csharp
// Parsers/FileNameParser.cs
using System.Globalization;
using WCTDataMiner.Services;
using Gpulse.WCT.DataAnalyzer.Services;
namespace WCTDataMiner.Parsers;
namespace Gpulse.WCT.DataAnalyzer.Parsers;
public class FileNameParser
{

View File

@@ -102,9 +102,9 @@ private static readonly Regex FodPattern = new(
```csharp
// Parsers/PlossParser.cs
using System.Text.RegularExpressions;
using WCTDataMiner.Models;
using Gpulse.WCT.DataAnalyzer.Models;
namespace WCTDataMiner.Parsers;
namespace Gpulse.WCT.DataAnalyzer.Parsers;
public class PlossParser : IParser<PlossRecord>
{

View File

@@ -53,9 +53,9 @@ private static readonly Regex QfodPattern = new(
```csharp
// Parsers/QfodParser.cs
using System.Text.RegularExpressions;
using WCTDataMiner.Models;
using Gpulse.WCT.DataAnalyzer.Models;
namespace WCTDataMiner.Parsers;
namespace Gpulse.WCT.DataAnalyzer.Parsers;
public class QfodParser : IParser<QfodRecord>
{

View File

@@ -18,7 +18,7 @@
```csharp
// Parsers/IParser.cs
namespace WCTDataMiner.Parsers;
namespace Gpulse.WCT.DataAnalyzer.Parsers;
public interface IParser<TRecord>
{

View File

@@ -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 拆分 | 取第一个分隔符后部分 |
| 350mW2250mW9列 | 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 WCTDataMiner.Data;
using WCTDataMiner.Models;
using WCTDataMiner.Parsers;
```mermaid
sequenceDiagram
participant CLI as CLI
participant ES as ExportService
participant RDB as release.db
participant FS as Filesystem
namespace WCTDataMiner.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<ParseReport> 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<QfodRecord>();
var plossRecords = new List<PlossRecord>();
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<QfodRecord> qfodRecords,
List<PlossRecord> 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);
```
- 代码中不存在 local entity 到 release entity 的导航属性
- 数据库中没有跨库外键
- export 不读 local.dbaggregate 不写 local.db

View File

@@ -1,68 +1,149 @@
# Architecture: WCTDataMiner
# Architecture: Gpulse.WCT.DataAnalyzer
> 无线充电 FOD异物检测日志数据采集系统从日志文件解析 QfodPloss 数据并存储到数据库
> 无线充电参数采集系统。从 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 |
| [?] | 增量解析策略需确认 |
| [?] | 多次解析同一文件的处理策略需确认 |
```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
```

View File

@@ -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/WCTDataMiner/
├── 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
```

View File

@@ -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",
"WCTDataMiner": "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 WCTDataMiner.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 WCTDataMiner.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<AppSettings>(context.Configuration.GetSection(""));
// 注册数据库服务
services.AddDatabaseServices(context.Configuration);
// 注册其他服务
services.AddScoped<QfodParser>();
services.AddScoped<PlossParser>();
services.AddScoped<ParseService>();
services.AddScoped<ErrorHandlingService>();
});
```
Release 数据库的 PostgreSQL 配置前缀为 `Release*`,未设置时回退到 local 的对应配置。
---
## 4. 聚合配置
| 配置键 | 说明 |
|--------|------|
| `Aggregation:TxPanelMappings` | TxPanel 名称 → 车厂名称的映射表 |
| `Aggregation:CarModelMappings` | TxHardware 版本 → 车型名称的映射表 |
未配置映射时,直接使用 TxPanel.Name 和 TxHardware.Version 作为最终值。手机品牌/型号从 RxType.Name 自动拆分(分隔符 `/_:`)。

View File

@@ -76,7 +76,7 @@
```csharp
// Models/PlossRecord.cs
namespace WCTDataMiner.Models;
namespace Gpulse.WCT.DataAnalyzer.Models;
public class PlossRecord
{
@@ -117,9 +117,9 @@ public class PlossRecord
// Data/Configurations/PlossRecordConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Models;
using Gpulse.WCT.DataAnalyzer.Models;
namespace WCTDataMiner.Data.Configurations;
namespace Gpulse.WCT.DataAnalyzer.Data.Configurations;
public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
{

View File

@@ -66,7 +66,7 @@
```csharp
// Models/QfodRecord.cs
namespace WCTDataMiner.Models;
namespace Gpulse.WCT.DataAnalyzer.Models;
public class QfodRecord
{
@@ -93,9 +93,9 @@ public class QfodRecord
// Data/Configurations/QfodRecordConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Models;
using Gpulse.WCT.DataAnalyzer.Models;
namespace WCTDataMiner.Data.Configurations;
namespace Gpulse.WCT.DataAnalyzer.Data.Configurations;
public class QfodRecordConfiguration : IEntityTypeConfiguration<QfodRecord>
{

View File

@@ -38,7 +38,7 @@
```csharp
// Models/RxType.cs
namespace WCTDataMiner.Models;
namespace Gpulse.WCT.DataAnalyzer.Models;
public class RxType
{
@@ -58,9 +58,9 @@ public class RxType
// Data/Configurations/RxTypeConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Models;
using Gpulse.WCT.DataAnalyzer.Models;
namespace WCTDataMiner.Data.Configurations;
namespace Gpulse.WCT.DataAnalyzer.Data.Configurations;
public class RxTypeConfiguration : IEntityTypeConfiguration<RxType>
{

View File

@@ -29,7 +29,7 @@
```csharp
// Models/TxHardware.cs
namespace WCTDataMiner.Models;
namespace Gpulse.WCT.DataAnalyzer.Models;
public class TxHardware
{
@@ -49,9 +49,9 @@ public class TxHardware
// Data/Configurations/TxHardwareConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Models;
using Gpulse.WCT.DataAnalyzer.Models;
namespace WCTDataMiner.Data.Configurations;
namespace Gpulse.WCT.DataAnalyzer.Data.Configurations;
public class TxHardwareConfiguration : IEntityTypeConfiguration<TxHardware>
{

View File

@@ -43,7 +43,7 @@
```csharp
// Models/TxPanel.cs
namespace WCTDataMiner.Models;
namespace Gpulse.WCT.DataAnalyzer.Models;
public class TxPanel
{
@@ -63,9 +63,9 @@ public class TxPanel
// Data/Configurations/TxPanelConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Models;
using Gpulse.WCT.DataAnalyzer.Models;
namespace WCTDataMiner.Data.Configurations;
namespace Gpulse.WCT.DataAnalyzer.Data.Configurations;
public class TxPanelConfiguration : IEntityTypeConfiguration<TxPanel>
{

View File

@@ -29,7 +29,7 @@
```csharp
// Models/TxSoftware.cs
namespace WCTDataMiner.Models;
namespace Gpulse.WCT.DataAnalyzer.Models;
public class TxSoftware
{
@@ -49,9 +49,9 @@ public class TxSoftware
// Data/Configurations/TxSoftwareConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Models;
using Gpulse.WCT.DataAnalyzer.Models;
namespace WCTDataMiner.Data.Configurations;
namespace Gpulse.WCT.DataAnalyzer.Data.Configurations;
public class TxSoftwareConfiguration : IEntityTypeConfiguration<TxSoftware>
{

View File

@@ -75,7 +75,7 @@
```csharp
// Models/TestScenario.cs
namespace WCTDataMiner.Models;
namespace Gpulse.WCT.DataAnalyzer.Models;
public class TestScenario
{
@@ -110,9 +110,9 @@ public class TestScenario
// Data/Configurations/TestScenarioConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Models;
using Gpulse.WCT.DataAnalyzer.Models;
namespace WCTDataMiner.Data.Configurations;
namespace Gpulse.WCT.DataAnalyzer.Data.Configurations;
public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
{

View File

@@ -1,12 +1,12 @@
# Data Model: WCTDataMiner
# Data Model: Gpulse.WCT.DataAnalyzer
> WCTDataMiner 数据库模型规格,采用维度表设计,消除数据冗余
> 双数据库数据模型:本地解析库 + 正式发布库,相互独立。
---
## 文档索引
### 维度表
### 本地库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<T>` Fluent API 配置,实体类保持纯净 POCO
```csharp
// Data/WctMinerDbContext.cs
using Microsoft.EntityFrameworkCore;
using WCTDataMiner.Models;
namespace WCTDataMiner.Data;
public class WctMinerDbContext : DbContext
{
public WctMinerDbContext(DbContextOptions<WctMinerDbContext> options)
: base(options) { }
public DbSet<TxPanel> TxPanels => Set<TxPanel>();
public DbSet<TxHardware> TxHardwares => Set<TxHardware>();
public DbSet<TxSoftware> TxSoftwares => Set<TxSoftware>();
public DbSet<RxType> RxTypes => Set<RxType>();
public DbSet<TestScenario> TestScenarios => Set<TestScenario>();
public DbSet<QfodRecord> QfodRecords => Set<QfodRecord>();
public DbSet<PlossRecord> PlossRecords => Set<PlossRecord>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 自动应用所有 IEntityTypeConfiguration<T> 配置
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 |
- 代码中两个实体的命名空间不同Domain.Models
- 两个 DbContext 不共享 DbSet
- 发布库不从本地库继承实体配置
- aggregate 通过纯值映射,不依赖导航属性

View File

@@ -1,9 +1,9 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using WCTDataMiner.Core.Data;
using WCTDataMiner.Core.Models;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace WCTDataMiner.Core.Services;
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
/// <summary>
/// 数据清理服务 - 支持软删除

View File

@@ -1,9 +1,9 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using WCTDataMiner.Core.Data;
using WCTDataMiner.Core.Models;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace WCTDataMiner.Core.Services;
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
/// <summary>
/// 统计查询服务 - 使用数据库端聚合优化内存效率

View File

@@ -3,11 +3,11 @@ using System.Text;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using WCTDataMiner.Core.Data;
using WCTDataMiner.Core.Models;
using WCTDataMiner.Core.Security;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
namespace WCTDataMiner.Core.Services;
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
/// <summary>
/// 数据导出服务 - 支持安全的路径验证和 RFC 4180 标准 CSV 转义
@@ -15,12 +15,14 @@ namespace WCTDataMiner.Core.Services;
public class ExportService
{
private readonly WctMinerDbContext _context;
private readonly ReleaseDbContext _releaseContext;
private readonly ILogger<ExportService> _logger;
private readonly PathValidator _pathValidator;
public ExportService(WctMinerDbContext context, ILogger<ExportService> logger)
public ExportService(WctMinerDbContext context, ReleaseDbContext releaseContext, ILogger<ExportService> logger)
{
_context = context;
_releaseContext = releaseContext;
_logger = logger;
_pathValidator = new PathValidator(AppDomain.CurrentDomain.BaseDirectory);
}
@@ -305,7 +307,49 @@ public class ExportService
}
/// <summary>
/// RFC 4180 标准 CSV 字段转义
/// 导出正式发布库中的 ChargingParameterDatabase CSV。
/// </summary>
public async Task<string> ExportChargingParametersToCsvAsync(string outputDir)
{
var validationResult = _pathValidator.ValidateOutputDirectory(outputDir);
if (!validationResult.IsValid)
throw new ArgumentException($"无效的输出目录: {validationResult.ErrorMessage}");
var safeOutputDir = validationResult.NormalizedPath!;
Directory.CreateDirectory(safeOutputDir);
var filePath = Path.Combine(safeOutputDir, "WCT-ChargingParameterDatabase.csv");
await using var writer = new StreamWriter(filePath, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
await writer.WriteLineAsync("车厂,车型,手机厂商,型号,350mW,500mW,750mW,1000mW,1250mW,1500mW,1750mW,2000mW,2250mW,Q值,Q基值,P-Q值系数,谐振频率");
var query = _releaseContext.ChargingParameters
.AsNoTracking()
.OrderBy(r => r.CarFactory)
.ThenBy(r => r.CarModel)
.ThenBy(r => r.PhoneBrand)
.ThenBy(r => r.PhoneModel);
var count = 0;
await foreach (var record in query.AsAsyncEnumerable())
{
var line = string.Join(',',
EscapeCsvField(record.CarFactory), EscapeCsvField(record.CarModel),
EscapeCsvField(record.PhoneBrand), EscapeCsvField(record.PhoneModel),
EscapeCsvField(record.Power350mW), EscapeCsvField(record.Power500mW),
EscapeCsvField(record.Power750mW), EscapeCsvField(record.Power1000mW),
EscapeCsvField(record.Power1250mW), EscapeCsvField(record.Power1500mW),
EscapeCsvField(record.Power1750mW), EscapeCsvField(record.Power2000mW),
EscapeCsvField(record.Power2250mW), EscapeCsvField(record.QValue),
EscapeCsvField(record.QBaseValue), EscapeCsvField(record.PqCoefficient),
EscapeCsvField(record.ResonanceFrequency));
await writer.WriteLineAsync(line);
count++;
}
_logger.LogInformation("ChargingParameterDatabase CSV 导出完成: {FilePath}, 共 {Count} 条记录", filePath, count);
return filePath;
}
/// </summary>
private static string EscapeCsvField(object? value)
{

View File

@@ -1,9 +1,9 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using WCTDataMiner.Core.Data;
using WCTDataMiner.Core.Models;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace WCTDataMiner.Core.Services;
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
/// <summary>
/// 维度表管理服务 - 负责维度表的CRUD操作并发安全版本

View File

@@ -1,11 +1,11 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using WCTDataMiner.Core.Data;
using WCTDataMiner.Core.Models;
using WCTDataMiner.Core.Parsers;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
namespace WCTDataMiner.Core.Services;
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
/// <summary>
/// 解析流程协调服务 - 协调文件名解析、行解析、批量写入

View File

@@ -1,10 +1,10 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using WCTDataMiner.Core.Data;
using WCTDataMiner.Core.Models;
using WCTDataMiner.Core.Parsers;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
namespace WCTDataMiner.Core.Services;
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
/// <summary>
/// 篮选条件常量

View File

@@ -1,6 +1,6 @@
using System.Globalization;
namespace WCTDataMiner.Core.Parsers;
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
/// <summary>
/// 文件名解析器 - 从文件名提取场景信息

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Parsers;
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
/// <summary>
/// 通用解析器接口

View File

@@ -1,7 +1,7 @@
using System.Text.RegularExpressions;
using WCTDataMiner.Core.Models;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace WCTDataMiner.Core.Parsers;
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
/// <summary>
/// Ploss FOD日志解析器 - 支持两行格式

View File

@@ -1,7 +1,7 @@
using System.Text.RegularExpressions;
using WCTDataMiner.Core.Models;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace WCTDataMiner.Core.Parsers;
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
/// <summary>
/// Qfod日志解析器

View File

@@ -0,0 +1,148 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Release;
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
/// <summary>
/// 从本地解析库生成独立正式发布库。
/// 当前业务规则:功率列使用 Ploss 的 Field7重复结果取平均值缺失值保留为空。
/// </summary>
public class AggregationService
{
private static readonly int[] PowerLevels = [350, 500, 750, 1000, 1250, 1500, 1750, 2000, 2250];
private readonly WctMinerDbContext _localContext;
private readonly ReleaseDbContext _releaseContext;
private readonly IConfiguration _configuration;
private readonly ILogger<AggregationService> _logger;
public AggregationService(
WctMinerDbContext localContext,
ReleaseDbContext releaseContext,
IConfiguration configuration,
ILogger<AggregationService> logger)
{
_localContext = localContext;
_releaseContext = releaseContext;
_configuration = configuration;
_logger = logger;
}
public async Task<AggregationReport> AggregateAsync(bool rebuild = true, CancellationToken cancellationToken = default)
{
var rows = await LoadRowsAsync(cancellationToken);
var records = rows
.GroupBy(r => BuildKey(r.Scenario))
.Select(group => CreateRecord(group.Key, group.ToList()))
.ToList();
await using var transaction = await _releaseContext.Database.BeginTransactionAsync(cancellationToken);
try
{
if (rebuild)
await _releaseContext.Database.ExecuteSqlRawAsync("DELETE FROM charging_parameter", cancellationToken);
await _releaseContext.ChargingParameters.AddRangeAsync(records, cancellationToken);
await _releaseContext.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw;
}
_logger.LogInformation("聚合完成: {Count} 条正式发布记录", records.Count);
return new AggregationReport(records.Count, rows.Count);
}
private async Task<List<PlossRow>> LoadRowsAsync(CancellationToken cancellationToken)
{
var records = await _localContext.PlossRecords
.AsNoTracking()
.Where(r => !r.IsDeleted)
.Include(r => r.Scenario)
.ThenInclude(s => s!.TxPanel)
.Include(r => r.Scenario)
.ThenInclude(s => s!.TxHardware)
.Include(r => r.Scenario)
.ThenInclude(s => s!.RxType)
.Include(r => r.Scenario)
.ThenInclude(s => s!.QfodRecords)
.ToListAsync(cancellationToken);
return records.Select(r => new PlossRow(
r,
r.Scenario,
r.Scenario.TxPanel.Name,
r.Scenario.TxHardware.Version,
r.Scenario.RxType.Name)).ToList();
}
private ChargingParameterKey BuildKey(TestScenario scenario)
{
var panelName = scenario.TxPanel.Name;
var mapping = _configuration.GetSection("Aggregation:TxPanelMappings")[panelName];
var carFactory = mapping ?? panelName;
var carModel = _configuration[$"Aggregation:CarModelMappings:{scenario.TxHardware.Version}"]
?? scenario.TxHardware.Version;
var (phoneBrand, phoneModel) = SplitPhoneName(scenario.RxType.Name);
return new ChargingParameterKey(carFactory, carModel, phoneBrand, phoneModel);
}
private static (string Brand, string Model) SplitPhoneName(string value)
{
var separator = value.IndexOfAny(['/', '_', ':']);
if (separator > 0 && separator < value.Length - 1)
return (value[..separator], value[(separator + 1)..]);
return ("Unknown", value);
}
private static ChargingParameterRecord CreateRecord(ChargingParameterKey key, List<PlossRow> rows)
{
var values = PowerLevels.Select(power => Average(rows
.Where(row => IsPowerMatch(row.Record, power))
.Select(row => (double?)row.Record.Field7))).ToArray();
return new ChargingParameterRecord
{
CarFactory = key.CarFactory,
CarModel = key.CarModel,
PhoneBrand = key.PhoneBrand,
PhoneModel = key.PhoneModel,
Power350mW = values[0],
Power500mW = values[1],
Power750mW = values[2],
Power1000mW = values[3],
Power1250mW = values[4],
Power1500mW = values[5],
Power1750mW = values[6],
Power2000mW = values[7],
Power2250mW = values[8],
QValue = Average(rows.SelectMany(row => row.Scenario.QfodRecords).Select(q => (double?)q.CurrentQ)),
QBaseValue = Average(rows.SelectMany(row => row.Scenario.QfodRecords).Select(q => (double?)q.RawQ)),
PqCoefficient = Average(rows.Select(row => row.Record.DeltaP.HasValue ? (double?)row.Record.DeltaP.Value : null)),
ResonanceFrequency = null
};
}
private static bool IsPowerMatch(PlossRecord record, int power)
{
return record.Field4 == power || record.Field5 == power || record.PowLoss == power;
}
private static double? Average(IEnumerable<double?> values)
{
var valid = values.Where(value => value.HasValue).Select(value => value!.Value).ToArray();
return valid.Length == 0 ? null : valid.Average();
}
private record PlossRow(PlossRecord Record, TestScenario Scenario, string Panel, string Hardware, string RxType);
private record ChargingParameterKey(string CarFactory, string CarModel, string PhoneBrand, string PhoneModel);
}
public record AggregationReport(int ReleaseRecordCount, int LocalRecordCount);

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Services;
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
/// <summary>
/// 阈值计算器 - 根据接收功率查表获取动态阈值

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Models;
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// Ploss记录实体 - 存储Ploss FOD格式解析数据

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Models;
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// Qfod记录实体 - 存储Qfod格式解析数据

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Models;
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// RX类型维度表

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Models;
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// 测试场景实体 - 通过外键关联各维度表

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Models;
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// TX硬件版本维度表

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Models;
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// TX面板类型维度表

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Models;
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// TX软件版本维度表

View File

@@ -0,0 +1,33 @@
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Release;
/// <summary>
/// 正式发布库中的充电参数记录。
/// 此实体只属于 ReleaseDbContext不引用本地日志数据库实体。
/// </summary>
public class ChargingParameterRecord
{
public Guid Id { get; set; } = Guid.NewGuid();
public string CarFactory { get; set; } = null!;
public string CarModel { get; set; } = null!;
public string PhoneBrand { get; set; } = null!;
public string PhoneModel { get; set; } = null!;
public double? Power350mW { get; set; }
public double? Power500mW { get; set; }
public double? Power750mW { get; set; }
public double? Power1000mW { get; set; }
public double? Power1250mW { get; set; }
public double? Power1500mW { get; set; }
public double? Power1750mW { get; set; }
public double? Power2000mW { get; set; }
public double? Power2250mW { get; set; }
public double? QValue { get; set; }
public double? QBaseValue { get; set; }
public double? PqCoefficient { get; set; }
public double? ResonanceFrequency { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,6 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>Gpulse.WCT.DataAnalyzer.Core</AssemblyName>
<RootNamespace>Gpulse.WCT.DataAnalyzer.Core</RootNamespace>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Configuration;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Configuration;
/// <summary>
/// 应用配置
@@ -16,7 +16,9 @@ public class AppSettings
public class DatabaseSettings
{
public string Type { get; set; } = "sqlite";
public string Path { get; set; } = "./data/database/wctminer.db";
public string Path { get; set; } = "./data/database/local.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; }

View File

@@ -1,10 +1,10 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using WCTDataMiner.Core.Data;
using WCTDataMiner.Core.Parsers;
using WCTDataMiner.Core.Services;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
using Gpulse.WCT.DataAnalyzer.Core.Application;
namespace WCTDataMiner.Core.Extensions;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
/// <summary>
/// 服务注册扩展
@@ -28,6 +28,15 @@ public static class ServiceCollectionExtensions
return factory.CreateDbContext();
});
services.AddScoped<IReleaseDbContextFactory, ReleaseDbContextFactory>();
// 注册正式发布数据库上下文(与本地解析数据库完全独立)
services.AddScoped<ReleaseDbContext>(sp =>
{
var factory = sp.GetRequiredService<IReleaseDbContextFactory>();
return factory.CreateDbContext();
});
return services;
}
@@ -48,6 +57,7 @@ public static class ServiceCollectionExtensions
services.AddScoped<StatsService>();
services.AddScoped<ExportService>();
services.AddScoped<CleanService>();
services.AddScoped<AggregationService>();
return services;
}

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Core.Models;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace WCTDataMiner.Core.Data.Configurations;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
{

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Core.Models;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace WCTDataMiner.Core.Data.Configurations;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
public class QfodRecordConfiguration : IEntityTypeConfiguration<QfodRecord>
{

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Core.Models;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace WCTDataMiner.Core.Data.Configurations;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
public class RxTypeConfiguration : IEntityTypeConfiguration<RxType>
{

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Core.Models;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace WCTDataMiner.Core.Data.Configurations;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
{

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Core.Models;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace WCTDataMiner.Core.Data.Configurations;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
public class TxHardwareConfiguration : IEntityTypeConfiguration<TxHardware>
{

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Core.Models;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace WCTDataMiner.Core.Data.Configurations;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
public class TxPanelConfiguration : IEntityTypeConfiguration<TxPanel>
{

View File

@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Core.Models;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace WCTDataMiner.Core.Data.Configurations;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
public class TxSoftwareConfiguration : IEntityTypeConfiguration<TxSoftware>
{

View File

@@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace WCTDataMiner.Core.Data;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
/// <summary>
/// 数据库上下文工厂实现 - 支持SQLite和PostgreSQL切换
@@ -30,7 +30,9 @@ public class DbContextFactory : IDbContextFactory
case "sqlite":
default:
var dbPath = _configuration["Database:Path"] ?? "./data/database/wctminer.db";
var dbPath = _configuration["Database:LocalPath"]
?? _configuration["Database:Path"]
?? "./data/database/local.db";
EnsureDirectoryExists(dbPath);
optionsBuilder.UseSqlite($"Data Source={dbPath}");
break;

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Data;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
/// <summary>
/// 数据库上下文工厂接口

View File

@@ -1,6 +1,6 @@
using WCTDataMiner.Core.Models;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace WCTDataMiner.Core.Data;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
/// <summary>
/// 初始数据种子

View File

@@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
using WCTDataMiner.Core.Models;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace WCTDataMiner.Core.Data;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
/// <summary>
/// WCT数据采集数据库上下文

View File

@@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Release;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
public class ChargingParameterRecordConfiguration : IEntityTypeConfiguration<ChargingParameterRecord>
{
public void Configure(EntityTypeBuilder<ChargingParameterRecord> builder)
{
builder.ToTable("charging_parameter");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).HasColumnName("id");
ConfigureText(builder, e => e.CarFactory, "car_factory");
ConfigureText(builder, e => e.CarModel, "car_model");
ConfigureText(builder, e => e.PhoneBrand, "phone_brand");
ConfigureText(builder, e => e.PhoneModel, "phone_model");
ConfigureNumber(builder, e => e.Power350mW, "power_350mw");
ConfigureNumber(builder, e => e.Power500mW, "power_500mw");
ConfigureNumber(builder, e => e.Power750mW, "power_750mw");
ConfigureNumber(builder, e => e.Power1000mW, "power_1000mw");
ConfigureNumber(builder, e => e.Power1250mW, "power_1250mw");
ConfigureNumber(builder, e => e.Power1500mW, "power_1500mw");
ConfigureNumber(builder, e => e.Power1750mW, "power_1750mw");
ConfigureNumber(builder, e => e.Power2000mW, "power_2000mw");
ConfigureNumber(builder, e => e.Power2250mW, "power_2250mw");
ConfigureNumber(builder, e => e.QValue, "q_value");
ConfigureNumber(builder, e => e.QBaseValue, "q_base_value");
ConfigureNumber(builder, e => e.PqCoefficient, "pq_coefficient");
ConfigureNumber(builder, e => e.ResonanceFrequency, "resonance_frequency");
builder.Property(e => e.CreatedAt).IsRequired().HasColumnName("created_at");
builder.Property(e => e.UpdatedAt).IsRequired().HasColumnName("updated_at");
builder.HasIndex(e => new { e.CarFactory, e.CarModel, e.PhoneBrand, e.PhoneModel })
.IsUnique()
.HasDatabaseName("uq_charging_parameter_business_key");
}
private static void ConfigureText(
EntityTypeBuilder<ChargingParameterRecord> builder,
System.Linq.Expressions.Expression<Func<ChargingParameterRecord, string>> property,
string columnName)
{
builder.Property(property).IsRequired().HasMaxLength(128).HasColumnName(columnName);
}
private static void ConfigureNumber(
EntityTypeBuilder<ChargingParameterRecord> builder,
System.Linq.Expressions.Expression<Func<ChargingParameterRecord, double?>> property,
string columnName)
{
builder.Property(property).IsRequired(false).HasColumnName(columnName).HasPrecision(18, 6);
}
}

View File

@@ -0,0 +1,6 @@
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
public interface IReleaseDbContextFactory
{
ReleaseDbContext CreateDbContext();
}

View File

@@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Release;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
/// <summary>
/// 正式发布数据库上下文。只包含最终充电参数记录。
/// </summary>
public class ReleaseDbContext : DbContext
{
public ReleaseDbContext(DbContextOptions<ReleaseDbContext> options)
: base(options) { }
public DbSet<ChargingParameterRecord> ChargingParameters => Set<ChargingParameterRecord>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ReleaseDbContext).Assembly);
}
}

View File

@@ -0,0 +1,58 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
/// <summary>
/// 正式发布数据库上下文工厂。
/// </summary>
public class ReleaseDbContextFactory : IReleaseDbContextFactory
{
private readonly IConfiguration _configuration;
public ReleaseDbContextFactory(IConfiguration configuration)
{
_configuration = configuration;
}
public ReleaseDbContext CreateDbContext()
{
var dbType = _configuration["Database:Type"]?.ToLowerInvariant() ?? "sqlite";
var optionsBuilder = new DbContextOptionsBuilder<ReleaseDbContext>();
if (dbType is "postgresql" or "postgres")
{
optionsBuilder.UseNpgsql(BuildPostgreSqlConnectionString());
}
else
{
var dbPath = _configuration["Database:ReleasePath"] ?? "./data/database/release.db";
EnsureDirectoryExists(dbPath);
optionsBuilder.UseSqlite($"Data Source={dbPath}");
}
#if DEBUG
optionsBuilder.EnableSensitiveDataLogging();
optionsBuilder.EnableDetailedErrors();
#endif
return new ReleaseDbContext(optionsBuilder.Options);
}
private string BuildPostgreSqlConnectionString()
{
var host = _configuration["Database:ReleaseHost"] ?? _configuration["Database:Host"] ?? "localhost";
var port = _configuration.GetValue("Database:ReleasePort", _configuration.GetValue("Database:Port", 5432));
var name = _configuration["Database:ReleaseName"] ?? "wct_charging_parameters";
var user = _configuration["Database:ReleaseUser"] ?? _configuration["Database:User"] ?? "postgres";
var password = _configuration["Database:ReleasePassword"] ?? _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.CreateDirectory(directory);
}
}

View File

@@ -1,6 +1,6 @@
using System.Text.RegularExpressions;
namespace WCTDataMiner.Core.Security;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
/// <summary>
/// 路径验证结果
@@ -36,7 +36,7 @@ public class PathValidator
/// <summary>
/// 默认允许的输出目录
/// </summary>
private static readonly string[] DefaultAllowedDirectories = ["exports", "output", "data/exports"];
private static readonly string[] DefaultAllowedDirectories = ["exports", "output", "data/exports", "data/output"];
public PathValidator(string applicationBasePath, IEnumerable<string>? allowedDirectories = null)
{

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Security;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
/// <summary>
/// 安全相关常量定义

View File

@@ -0,0 +1,33 @@
using System.CommandLine;
using Gpulse.WCT.DataAnalyzer.Core.Application;
namespace Gpulse.WCT.DataAnalyzer.Commands;
/// <summary>
/// 从本地解析库生成正式发布库。
/// </summary>
public class AggregateCommand : Command
{
public AggregateCommand(AggregationService aggregationService)
: base("aggregate", "Aggregate local log data into the release database")
{
var rebuildOption = new Option<bool>(
"--rebuild",
() => true,
"Rebuild the release table before inserting aggregated records");
AddOption(rebuildOption);
this.SetHandler(async rebuild =>
{
try
{
var report = await aggregationService.AggregateAsync(rebuild);
Console.WriteLine($"Aggregated {report.LocalRecordCount} local Ploss records into {report.ReleaseRecordCount} release records.");
}
catch (Exception ex)
{
Console.WriteLine($"Aggregation failed: {ex.Message}");
}
}, rebuildOption);
}
}

View File

@@ -1,7 +1,7 @@
using System.CommandLine;
using WCTDataMiner.Core.Services;
using Gpulse.WCT.DataAnalyzer.Core.Application;
namespace WCTDataMiner.Commands;
namespace Gpulse.WCT.DataAnalyzer.Commands;
/// <summary>
/// 数据清理命令

View File

@@ -1,7 +1,7 @@
using System.CommandLine;
using WCTDataMiner.Core.Services;
using Gpulse.WCT.DataAnalyzer.Core.Application;
namespace WCTDataMiner.Commands;
namespace Gpulse.WCT.DataAnalyzer.Commands;
/// <summary>
/// 数据导出命令
@@ -25,8 +25,8 @@ public class ExportCommand : Command
var typeOption = new Option<string>(
"--type",
() => "all",
"Data type to export: qfod, ploss, or all"
() => "charging-parameters",
"Data type: charging-parameters, qfod, ploss, or all"
);
AddOption(formatOption);
@@ -44,6 +44,12 @@ public class ExportCommand : Command
{
if (format == "csv")
{
if (type is "charging-parameters" or "charging" or "all")
{
var path = await exportService.ExportChargingParametersToCsvAsync(output);
Console.WriteLine($" ChargingParameterDatabase CSV exported: {path}");
}
if (type is "qfod" or "all")
{
var path = await exportService.ExportQfodToCsvAsync(output);

View File

@@ -1,7 +1,7 @@
using System.CommandLine;
using WCTDataMiner.Core.Services;
using Gpulse.WCT.DataAnalyzer.Core.Application;
namespace WCTDataMiner.Commands;
namespace Gpulse.WCT.DataAnalyzer.Commands;
/// <summary>
/// 解析日志文件命令

View File

@@ -1,7 +1,7 @@
using System.CommandLine;
using WCTDataMiner.Core.Services;
using Gpulse.WCT.DataAnalyzer.Core.Application;
namespace WCTDataMiner.Commands;
namespace Gpulse.WCT.DataAnalyzer.Commands;
/// <summary>
/// 统计查询命令

View File

@@ -1,6 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>Gpulse.WCT.DataAnalyzer</AssemblyName>
<RootNamespace>Gpulse.WCT.DataAnalyzer</RootNamespace>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
@@ -30,7 +32,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WCTDataMiner.Core\WCTDataMiner.Core.csproj" />
<ProjectReference Include="..\Gpulse.WCT.DataAnalyzer.Core\Gpulse.WCT.DataAnalyzer.Core.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -3,15 +3,15 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using WCTDataMiner.Commands;
using WCTDataMiner.Core.Data;
using WCTDataMiner.Core.Extensions;
using WCTDataMiner.Core.Services;
using Gpulse.WCT.DataAnalyzer.Commands;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
using Gpulse.WCT.DataAnalyzer.Core.Application;
namespace WCTDataMiner;
namespace Gpulse.WCT.DataAnalyzer;
/// <summary>
/// WCTDataMiner - 无线充电FOD日志数据采集系统
/// Gpulse.WCT.DataAnalyzer - 无线充电FOD日志数据采集系统
/// </summary>
public class Program
{
@@ -19,7 +19,7 @@ public class Program
{
// 1. 构建引导配置(用于 Serilog 初始化)
var bootstrapConfig = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true)
.AddEnvironmentVariables()
@@ -59,6 +59,9 @@ public class Program
var dbContext = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
await dbContext.Database.EnsureCreatedAsync();
var releaseContext = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>();
await releaseContext.Database.EnsureCreatedAsync();
// 种子数据
SeedData.Initialize(dbContext);
}
@@ -67,9 +70,10 @@ public class Program
using var commandScope = app.Services.CreateScope();
var provider = commandScope.ServiceProvider;
var rootCommand = new RootCommand("WCTDataMiner - FOD Log Data Parser");
var rootCommand = new RootCommand("Gpulse.WCT.DataAnalyzer - FOD Log Data Parser");
rootCommand.AddCommand(new ParseCommand(provider.GetRequiredService<ParseService>()));
rootCommand.AddCommand(new AggregateCommand(provider.GetRequiredService<AggregationService>()));
rootCommand.AddCommand(new StatsCommand(provider.GetRequiredService<StatsService>()));
rootCommand.AddCommand(new ExportCommand(provider.GetRequiredService<ExportService>()));
rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService<CleanService>()));

View File

@@ -1,10 +0,0 @@
<Application x:Class="WCTDataMiner.Wpf.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WCTDataMiner.Wpf"
xmlns:converters="clr-namespace:WCTDataMiner.Wpf.Converters">
<Application.Resources>
<converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<converters:InverseBooleanConverter x:Key="InverseBooleanConverter"/>
</Application.Resources>
</Application>

View File

@@ -1,123 +0,0 @@
using System.IO;
using System.Windows;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Serilog;
using WCTDataMiner.Core.Extensions;
using WCTDataMiner.Core.Services;
using WCTDataMiner.Wpf.Configuration;
using WCTDataMiner.Wpf.ViewModels;
using WCTDataMiner.Wpf.Views;
namespace WCTDataMiner.Wpf;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
private IHost? _host;
private ILogger? _logger;
protected override void OnStartup(StartupEventArgs e)
{
// 使用 ConfigurationLoader 加载配置,支持文件缺失
var configuration = ConfigurationLoader.LoadConfiguration(
AppDomain.CurrentDomain.BaseDirectory,
out var configFileExists);
// 配置 Serilog - 从配置文件读取
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext()
.CreateLogger();
_logger = Log.ForContext<App>();
if (!configFileExists)
{
_logger.Information("配置文件 appsettings.json 不存在,使用默认配置启动");
}
_logger.Information("WCTDataMiner WPF 应用启动");
_host = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, configBuilder) =>
{
configBuilder.Sources.Clear();
configBuilder.SetBasePath(AppDomain.CurrentDomain.BaseDirectory);
if (configFileExists)
{
configBuilder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
}
else
{
configBuilder.AddInMemoryCollection(DefaultAppSettings.GetDefaultConfiguration());
}
configBuilder.AddEnvironmentVariables("WCTDM_");
})
.ConfigureServices((context, services) =>
{
// 添加 Serilog
services.AddSerilog();
// 注册 Core 层服务
services.AddDatabaseServices(context.Configuration);
services.AddApplicationServices();
// 注册 ViewModels使用 Singleton 确保消息订阅正确工作)
services.AddSingleton<MainViewModel>();
services.AddSingleton<ScenarioListViewModel>();
// Chart ViewModels 使用 Transient每次导航创建新实例
services.AddTransient<PlossChartViewModel>();
services.AddTransient<QfodChartViewModel>();
// Table ViewModels 使用 Transient每次导航创建新实例
services.AddTransient<PlossTableViewModel>();
services.AddTransient<QfodTableViewModel>();
services.AddSingleton<ImportViewModel>();
// 注册 Views
services.AddTransient<MainWindow>();
services.AddTransient<ScenarioListView>();
services.AddTransient<PlossChartView>();
services.AddTransient<QfodChartView>();
services.AddTransient<PlossTableView>();
services.AddTransient<QfodTableView>();
services.AddTransient<ImportView>();
})
.Build();
var mainWindow = _host.Services.GetRequiredService<MainWindow>();
// 初始化加载场景数据
var mainViewModel = _host.Services.GetRequiredService<MainViewModel>();
mainWindow.Loaded += async (s, e) =>
{
try
{
await mainViewModel.InitializeAsync();
}
catch (Exception ex)
{
_logger?.Error(ex, "主窗口初始化失败");
MessageBox.Show($"应用初始化失败: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
};
mainWindow.Show();
base.OnStartup(e);
}
protected override void OnExit(ExitEventArgs e)
{
_logger?.Information("WCTDataMiner WPF 应用退出");
Log.CloseAndFlush();
_host?.Dispose();
base.OnExit(e);
}
}

View File

@@ -1,10 +0,0 @@
using System.Windows;
[assembly:ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View File

@@ -1,37 +0,0 @@
using System.IO;
using Microsoft.Extensions.Configuration;
namespace WCTDataMiner.Wpf.Configuration;
/// <summary>
/// 配置加载器 - 支持配置文件缺失时的默认配置
/// </summary>
public static class ConfigurationLoader
{
/// <summary>
/// 加载配置,如果文件不存在则使用默认配置
/// </summary>
/// <param name="basePath">配置文件基础路径</param>
/// <param name="configFileExists">输出参数:指示配置文件是否存在</param>
/// <returns>配置对象</returns>
public static IConfiguration LoadConfiguration(string basePath, out bool configFileExists)
{
var settingsPath = Path.Combine(basePath, "appsettings.json");
configFileExists = File.Exists(settingsPath);
var builder = new ConfigurationBuilder().SetBasePath(basePath);
if (configFileExists)
{
builder.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
}
else
{
// 使用默认配置
builder.AddInMemoryCollection(DefaultAppSettings.GetDefaultConfiguration());
}
builder.AddEnvironmentVariables("WCTDM_");
return builder.Build();
}
}

View File

@@ -1,30 +0,0 @@
namespace WCTDataMiner.Wpf.Configuration;
/// <summary>
/// 默认应用配置 - 当配置文件缺失时使用
/// </summary>
public static class DefaultAppSettings
{
/// <summary>
/// 获取默认配置字典
/// </summary>
public static Dictionary<string, string?> GetDefaultConfiguration()
{
return new Dictionary<string, string?>
{
// Serilog 配置
["Serilog:MinimumLevel:Default"] = "Information",
["Serilog:MinimumLevel:Override:WCTDataMiner"] = "Debug",
["Serilog:MinimumLevel:Override:Microsoft"] = "Warning",
["Serilog:WriteTo:0:Name"] = "Console",
["Serilog:WriteTo:1:Name"] = "File",
["Serilog:WriteTo:1:Args:path"] = "./data/logs/wctdataminer-.log",
["Serilog:WriteTo:1:Args:rollingInterval"] = "Day",
["Serilog:WriteTo:1:Args:outputTemplate"] = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}",
// 数据库配置
["Database:Type"] = "sqlite",
["Database:Path"] = "./data/database/wctminer.db"
};
}
}

View File

@@ -1,53 +0,0 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace WCTDataMiner.Wpf.Converters;
/// <summary>
/// Boolean to Visibility 转换器
/// </summary>
public class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return boolValue ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Visibility visibility)
{
return visibility == Visibility.Visible;
}
return false;
}
}
/// <summary>
/// 反转 Boolean 转换器
/// </summary>
public class InverseBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return !boolValue;
}
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return !boolValue;
}
return false;
}
}

View File

@@ -1,74 +0,0 @@
<Window x:Class="WCTDataMiner.Wpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:WCTDataMiner.Wpf.Views"
xmlns:viewmodels="clr-namespace:WCTDataMiner.Wpf.ViewModels"
mc:Ignorable="d"
Title="WCTDataMiner - FOD 数据分析工具" Height="600" Width="900"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 左侧导航栏 -->
<Border Grid.Column="0" Background="#F5F5F5" BorderBrush="#DDD" BorderThickness="0,0,1,0">
<StackPanel Margin="10">
<TextBlock Text="导航" FontSize="16" FontWeight="Bold" Margin="0,0,0,15"/>
<Button Content="数据导入" Command="{Binding NavigateToImportCommand}"
Margin="0,5" Padding="10,5" HorizontalAlignment="Stretch"
Background="#4CAF50" Foreground="White" FontWeight="Bold"/>
<Button Content="场景列表" Command="{Binding NavigateToScenariosCommand}"
Margin="0,5" Padding="10,5" HorizontalAlignment="Stretch"/>
<TextBlock Text="筛选条件" FontSize="14" FontWeight="Bold" Margin="0,20,0,10"/>
<TextBlock Text="TX 面板:" Margin="0,5"/>
<ComboBox ItemsSource="{Binding TxPanels}" SelectedItem="{Binding SelectedTxPanel}"
Margin="0,0,0,10"/>
<TextBlock Text="TX 硬件:" Margin="0,5"/>
<ComboBox ItemsSource="{Binding TxHardwares}" SelectedItem="{Binding SelectedTxHardware}"
Margin="0,0,0,10"/>
<TextBlock Text="TX 软件:" Margin="0,5"/>
<ComboBox ItemsSource="{Binding TxSoftwares}" SelectedItem="{Binding SelectedTxSoftware}"
Margin="0,0,0,10"/>
<TextBlock Text="RX 类型:" Margin="0,5"/>
<ComboBox ItemsSource="{Binding RxTypes}" SelectedItem="{Binding SelectedRxType}"
Margin="0,0,0,10"/>
</StackPanel>
</Border>
<!-- 右侧内容区域 -->
<ContentControl Grid.Column="1" Content="{Binding CurrentView}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type viewmodels:ImportViewModel}">
<views:ImportView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:ScenarioListViewModel}">
<views:ScenarioListView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:PlossChartViewModel}">
<views:PlossChartView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:QfodChartViewModel}">
<views:QfodChartView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:PlossTableViewModel}">
<views:PlossTableView/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:QfodTableViewModel}">
<views:QfodTableView/>
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</Grid>
</Window>

View File

@@ -1,16 +0,0 @@
using System.Windows;
using WCTDataMiner.Wpf.ViewModels;
namespace WCTDataMiner.Wpf;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow(MainViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}

View File

@@ -1,31 +0,0 @@
namespace WCTDataMiner.Wpf.Messages;
/// <summary>
/// 数据导入完成消息
/// </summary>
/// <param name="ImportedCount">导入的文件数量</param>
public record DataImportedMessage(int ImportedCount);
/// <summary>
/// 导航到 Ploss 图表消息
/// </summary>
/// <param name="ScenarioId">场景 ID</param>
public record NavigateToPlossChartMessage(Guid ScenarioId);
/// <summary>
/// 导航到 Qfod 图表消息
/// </summary>
/// <param name="ScenarioId">场景 ID</param>
public record NavigateToQfodChartMessage(Guid ScenarioId);
/// <summary>
/// 导航到 Ploss 表格消息
/// </summary>
/// <param name="ScenarioId">场景 ID</param>
public record NavigateToPlossTableMessage(Guid ScenarioId);
/// <summary>
/// 导航到 Qfod 表格消息
/// </summary>
/// <param name="ScenarioId">场景 ID</param>
public record NavigateToQfodTableMessage(Guid ScenarioId);

View File

@@ -1,170 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using System.Collections.ObjectModel;
using WCTDataMiner.Core.Services;
using WCTDataMiner.Wpf.Messages;
namespace WCTDataMiner.Wpf.ViewModels;
/// <summary>
/// 数据导入 ViewModel
/// </summary>
public partial class ImportViewModel : ObservableObject
{
private readonly ParseService _parseService;
private readonly ILogger<ImportViewModel> _logger;
[ObservableProperty]
private string _selectedPath = "";
[ObservableProperty]
private bool _isRecursive;
[ObservableProperty]
private bool _isImporting;
[ObservableProperty]
private int _progressValue;
[ObservableProperty]
private string _statusMessage = "请选择日志文件或目录";
[ObservableProperty]
private ObservableCollection<ImportResultItem> _importResults = new();
public ImportViewModel(ParseService parseService, ILogger<ImportViewModel> logger)
{
_parseService = parseService;
_logger = logger;
}
[RelayCommand]
private void SelectFile()
{
var dialog = new OpenFileDialog
{
Title = "选择日志文件",
Filter = "日志文件 (*.log)|*.log|所有文件 (*.*)|*.*",
Multiselect = false
};
if (dialog.ShowDialog() == true)
{
SelectedPath = dialog.FileName;
StatusMessage = $"已选择文件: {System.IO.Path.GetFileName(SelectedPath)}";
_logger.LogDebug("用户选择文件: {FilePath}", SelectedPath);
}
}
[RelayCommand]
private void SelectFolder()
{
var dialog = new OpenFolderDialog
{
Title = "选择日志目录",
Multiselect = false
};
if (dialog.ShowDialog() == true)
{
SelectedPath = dialog.FolderName;
StatusMessage = $"已选择目录: {System.IO.Path.GetFileName(SelectedPath)}";
_logger.LogDebug("用户选择目录: {FolderPath}, 递归={IsRecursive}", SelectedPath, IsRecursive);
}
}
[RelayCommand]
private async Task ImportAsync()
{
if (string.IsNullOrEmpty(SelectedPath))
{
StatusMessage = "请先选择文件或目录";
return;
}
IsImporting = true;
ImportResults.Clear();
StatusMessage = "正在导入...";
_logger.LogInformation("开始导入: {Path}, 递归={IsRecursive}", SelectedPath, IsRecursive);
try
{
if (System.IO.File.Exists(SelectedPath))
{
// 导入单个文件
var report = await _parseService.ParseFileAsync(SelectedPath);
ImportResults.Add(new ImportResultItem(report));
StatusMessage = $"导入完成: Qfod={report.QfodCount}, Ploss={report.PlossCount}, 错误={report.ErrorCount}";
_logger.LogInformation("单文件导入完成: {FileName}, Qfod={Qfod}, Ploss={Ploss}, 错误={Errors}",
report.FileName, report.QfodCount, report.PlossCount, report.ErrorCount);
// 发送导入完成消息,通知其他 ViewModel 刷新数据
WeakReferenceMessenger.Default.Send(new DataImportedMessage(1));
}
else if (System.IO.Directory.Exists(SelectedPath))
{
// 导入目录
var reports = await _parseService.ParseDirectoryAsync(SelectedPath, IsRecursive);
int totalQfod = 0, totalPloss = 0, totalErrors = 0;
foreach (var report in reports)
{
ImportResults.Add(new ImportResultItem(report));
totalQfod += report.QfodCount;
totalPloss += report.PlossCount;
totalErrors += report.ErrorCount;
ProgressValue = (int)((double)ImportResults.Count / reports.Count * 100);
}
StatusMessage = $"导入完成: {reports.Count} 个文件, Qfod={totalQfod}, Ploss={totalPloss}, 错误={totalErrors}";
_logger.LogInformation("目录导入完成: {FileCount} 个文件, Qfod={Qfod}, Ploss={Ploss}, 错误={Errors}",
reports.Count, totalQfod, totalPloss, totalErrors);
// 发送导入完成消息,通知其他 ViewModel 刷新数据
WeakReferenceMessenger.Default.Send(new DataImportedMessage(reports.Count));
}
else
{
StatusMessage = "路径不存在";
_logger.LogWarning("导入路径不存在: {Path}", SelectedPath);
}
}
catch (Exception ex)
{
StatusMessage = $"导入失败: {ex.Message}";
_logger.LogError(ex, "导入失败: {Path}", SelectedPath);
}
finally
{
IsImporting = false;
ProgressValue = 100;
}
}
}
/// <summary>
/// 导入结果项
/// </summary>
public class ImportResultItem
{
public string FileName { get; }
public int QfodCount { get; }
public int PlossCount { get; }
public int ErrorCount { get; }
public string Status { get; }
public bool IsSuccess { get; }
public ImportResultItem(ParseReport report)
{
FileName = report.FileName;
QfodCount = report.QfodCount;
PlossCount = report.PlossCount;
ErrorCount = report.ErrorCount;
IsSuccess = report.IsSuccess;
Status = report.IsSuccess ? "成功" : report.ErrorMessage ?? "失败";
}
}

View File

@@ -1,174 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using System.Windows;
using WCTDataMiner.Core.Services;
using WCTDataMiner.Wpf.Messages;
namespace WCTDataMiner.Wpf.ViewModels;
/// <summary>
/// 主窗口 ViewModel - 导航控制
/// </summary>
public partial class MainViewModel : ObservableObject,
IRecipient<NavigateToPlossChartMessage>,
IRecipient<NavigateToQfodChartMessage>,
IRecipient<NavigateToPlossTableMessage>,
IRecipient<NavigateToQfodTableMessage>
{
private readonly ImportViewModel _importViewModel;
private readonly ScenarioListViewModel _scenarioListViewModel;
private readonly IServiceProvider _serviceProvider;
[ObservableProperty]
private ObservableObject _currentView = null!;
[ObservableProperty]
private string? _selectedTxPanel;
[ObservableProperty]
private string? _selectedTxHardware;
[ObservableProperty]
private string? _selectedTxSoftware;
[ObservableProperty]
private string? _selectedRxType;
// 筛选选项列表(动态加载)
[ObservableProperty]
private List<string> _txPanels = new() { FilterConstants.AllOption };
[ObservableProperty]
private List<string> _txHardwares = new() { FilterConstants.AllOption };
[ObservableProperty]
private List<string> _txSoftwares = new() { FilterConstants.AllOption };
[ObservableProperty]
private List<string> _rxTypes = new() { FilterConstants.AllOption };
public MainViewModel(
ImportViewModel importViewModel,
ScenarioListViewModel scenarioListViewModel,
IServiceProvider serviceProvider)
{
_importViewModel = importViewModel;
_scenarioListViewModel = scenarioListViewModel;
_serviceProvider = serviceProvider;
_currentView = _scenarioListViewModel;
// 注册导航消息订阅
WeakReferenceMessenger.Default.Register<NavigateToPlossChartMessage>(this);
WeakReferenceMessenger.Default.Register<NavigateToQfodChartMessage>(this);
WeakReferenceMessenger.Default.Register<NavigateToPlossTableMessage>(this);
WeakReferenceMessenger.Default.Register<NavigateToQfodTableMessage>(this);
}
/// <summary>
/// 初始化加载场景数据和筛选选项
/// </summary>
public async Task InitializeAsync()
{
// 加载筛选选项
var filterOptions = await _scenarioListViewModel.GetFilterOptionsAsync();
TxPanels = filterOptions.TxPanels;
TxHardwares = filterOptions.TxHardwares;
TxSoftwares = filterOptions.TxSoftwares;
RxTypes = filterOptions.RxTypes;
// 加载场景列表
await _scenarioListViewModel.LoadScenariosAsync();
}
[RelayCommand]
private void NavigateToImport()
{
CurrentView = _importViewModel;
}
[RelayCommand]
private async Task NavigateToScenarios()
{
CurrentView = _scenarioListViewModel;
await _scenarioListViewModel.LoadScenariosAsync();
}
[RelayCommand]
private void NavigateToPlossChart(Guid scenarioId)
{
var vm = _serviceProvider.GetRequiredService<PlossChartViewModel>();
vm.ScenarioId = scenarioId;
CurrentView = vm;
}
[RelayCommand]
private void NavigateToQfodChart(Guid scenarioId)
{
var vm = _serviceProvider.GetRequiredService<QfodChartViewModel>();
vm.ScenarioId = scenarioId;
CurrentView = vm;
}
/// <summary>
/// 应用筛选条件并刷新场景列表
/// </summary>
public async Task ApplyFilterAsync()
{
await _scenarioListViewModel.LoadScenariosByFilterAsync(
SelectedTxPanel, SelectedTxHardware, SelectedTxSoftware, SelectedRxType);
}
/// <summary>
/// 接收导航到 Ploss 图表消息
/// </summary>
public void Receive(NavigateToPlossChartMessage message)
{
Application.Current.Dispatcher.InvokeAsync(() =>
{
var vm = _serviceProvider.GetRequiredService<PlossChartViewModel>();
vm.ScenarioId = message.ScenarioId;
CurrentView = vm;
});
}
/// <summary>
/// 接收导航到 Qfod 图表消息
/// </summary>
public void Receive(NavigateToQfodChartMessage message)
{
Application.Current.Dispatcher.InvokeAsync(() =>
{
var vm = _serviceProvider.GetRequiredService<QfodChartViewModel>();
vm.ScenarioId = message.ScenarioId;
CurrentView = vm;
});
}
/// <summary>
/// 接收导航到 Ploss 表格消息
/// </summary>
public void Receive(NavigateToPlossTableMessage message)
{
Application.Current.Dispatcher.InvokeAsync(() =>
{
var vm = _serviceProvider.GetRequiredService<PlossTableViewModel>();
vm.ScenarioId = message.ScenarioId;
CurrentView = vm;
});
}
/// <summary>
/// 接收导航到 Qfod 表格消息
/// </summary>
public void Receive(NavigateToQfodTableMessage message)
{
Application.Current.Dispatcher.InvokeAsync(() =>
{
var vm = _serviceProvider.GetRequiredService<QfodTableViewModel>();
vm.ScenarioId = message.ScenarioId;
CurrentView = vm;
});
}
}

View File

@@ -1,140 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using Microsoft.Extensions.Logging;
using WCTDataMiner.Core.Services;
using WCTDataMiner.Wpf.Messages;
namespace WCTDataMiner.Wpf.ViewModels;
/// <summary>
/// Ploss 折线图 ViewModel
/// </summary>
public partial class PlossChartViewModel : ObservableObject
{
private readonly ScenarioService _scenarioService;
private readonly ILogger<PlossChartViewModel> _logger;
[ObservableProperty]
private Guid _scenarioId;
[ObservableProperty]
private ISeries[] _series;
[ObservableProperty]
private Axis[] _xAxes;
[ObservableProperty]
private Axis[] _yAxes;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _loadingMessage = "";
public PlossChartViewModel(
ScenarioService scenarioService,
ILogger<PlossChartViewModel> logger)
{
_scenarioService = scenarioService;
_logger = logger;
// 初始化图表配置
Series = Array.Empty<ISeries>();
XAxes = new Axis[] { new Axis { Name = "Trigger Count" } };
YAxes = new Axis[] { new Axis { Name = "Power Loss (mW)" } };
}
/// <summary>
/// ScenarioId 属性变更时自动触发数据加载
/// </summary>
partial void OnScenarioIdChanged(Guid value)
{
if (value != Guid.Empty)
{
_ = LoadDataAsync().ContinueWith(t =>
{
if (t.Exception != null)
{
_logger.LogError(t.Exception, "LoadDataAsync 失败: ScenarioId={ScenarioId}", value);
}
}, TaskContinuationOptions.OnlyOnFaulted);
}
}
[RelayCommand]
private void SwitchToTable()
{
WeakReferenceMessenger.Default.Send(new NavigateToPlossTableMessage(ScenarioId));
}
public async Task LoadDataAsync()
{
if (IsLoading) return; // 防并发
IsLoading = true;
LoadingMessage = "正在加载数据...";
try
{
var records = await _scenarioService.GetPlossRecordsByScenarioIdAsync(ScenarioId);
if (records.Count == 0)
{
Series = Array.Empty<ISeries>();
_logger.LogWarning("未找到 Ploss 数据: ScenarioId={ScenarioId}", ScenarioId);
LoadingMessage = "无数据";
return;
}
// 构建 Ploss 曲线 (Field9) 和 Threshold 阈值线 (Field10)
var plossValues = records.Select(r => (double)r.Field9).ToArray();
var thresholdValues = records.Select(r => (double)r.Field10).ToArray();
var triggerLabels = records.Select(r => r.Field11.ToString()).ToArray();
Series = new ISeries[]
{
new LineSeries<double>
{
Name = "Ploss",
Values = plossValues,
Fill = null,
GeometrySize = 5
},
new LineSeries<double>
{
Name = "Threshold",
Values = thresholdValues,
Fill = null,
GeometrySize = 0,
LineSmoothness = 0 // 直线
}
};
// 更新 X 轴标签
XAxes = new Axis[]
{
new Axis
{
Name = "Trigger Count",
Labels = triggerLabels
}
};
_logger.LogInformation("加载 Ploss 图表数据成功: ScenarioId={ScenarioId}, 记录数={Count}", ScenarioId, records.Count);
LoadingMessage = "";
}
catch (Exception ex)
{
_logger.LogError(ex, "加载 Ploss 图表数据失败: ScenarioId={ScenarioId}", ScenarioId);
LoadingMessage = $"加载失败: {ex.Message}";
}
finally
{
IsLoading = false;
}
}
}

View File

@@ -1,87 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Logging;
using System.Collections.ObjectModel;
using WCTDataMiner.Core.Models;
using WCTDataMiner.Core.Services;
using WCTDataMiner.Wpf.Messages;
namespace WCTDataMiner.Wpf.ViewModels;
/// <summary>
/// Ploss 数据表格 ViewModel
/// </summary>
public partial class PlossTableViewModel : ObservableObject
{
private readonly ScenarioService _scenarioService;
private readonly ILogger<PlossTableViewModel> _logger;
[ObservableProperty]
private Guid _scenarioId;
[ObservableProperty]
private ObservableCollection<PlossRecord> _records = new();
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _loadingMessage = "";
public PlossTableViewModel(
ScenarioService scenarioService,
ILogger<PlossTableViewModel> logger)
{
_scenarioService = scenarioService;
_logger = logger;
}
/// <summary>
/// ScenarioId 属性变更时自动触发数据加载
/// </summary>
partial void OnScenarioIdChanged(Guid value)
{
if (value != Guid.Empty)
{
_ = LoadDataAsync().ContinueWith(t =>
{
if (t.Exception != null)
{
_logger.LogError(t.Exception, "LoadDataAsync 失败: ScenarioId={ScenarioId}", value);
}
}, TaskContinuationOptions.OnlyOnFaulted);
}
}
public async Task LoadDataAsync()
{
if (IsLoading) return; // 防并发
IsLoading = true;
LoadingMessage = "正在加载数据...";
try
{
var records = await _scenarioService.GetPlossRecordsByScenarioIdAsync(ScenarioId);
Records = new ObservableCollection<PlossRecord>(records);
_logger.LogInformation("加载 Ploss 表格数据成功: ScenarioId={ScenarioId}, 记录数={Count}", ScenarioId, records.Count);
LoadingMessage = "";
}
catch (Exception ex)
{
_logger.LogError(ex, "加载 Ploss 表格数据失败: ScenarioId={ScenarioId}", ScenarioId);
LoadingMessage = $"加载失败: {ex.Message}";
}
finally
{
IsLoading = false;
}
}
[RelayCommand]
private void SwitchToChart()
{
WeakReferenceMessenger.Default.Send(new NavigateToPlossChartMessage(ScenarioId));
}
}

View File

@@ -1,144 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using Microsoft.Extensions.Logging;
using WCTDataMiner.Core.Services;
using WCTDataMiner.Wpf.Messages;
namespace WCTDataMiner.Wpf.ViewModels;
/// <summary>
/// Qfod 折线图 ViewModel
/// </summary>
public partial class QfodChartViewModel : ObservableObject
{
private readonly ScenarioService _scenarioService;
private readonly ILogger<QfodChartViewModel> _logger;
[ObservableProperty]
private Guid _scenarioId;
[ObservableProperty]
private ISeries[] _series;
[ObservableProperty]
private Axis[] _xAxes;
[ObservableProperty]
private Axis[] _yAxes;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _loadingMessage = "";
public QfodChartViewModel(
ScenarioService scenarioService,
ILogger<QfodChartViewModel> logger)
{
_scenarioService = scenarioService;
_logger = logger;
// 初始化图表配置
Series = Array.Empty<ISeries>();
XAxes = new Axis[] { new Axis { Name = "Coil Index" } };
YAxes = new Axis[] { new Axis { Name = "Q Value" } };
}
/// <summary>
/// ScenarioId 属性变更时自动触发数据加载
/// </summary>
partial void OnScenarioIdChanged(Guid value)
{
if (value != Guid.Empty)
{
_ = LoadDataAsync().ContinueWith(t =>
{
if (t.Exception != null)
{
_logger.LogError(t.Exception, "LoadDataAsync 失败: ScenarioId={ScenarioId}", value);
}
}, TaskContinuationOptions.OnlyOnFaulted);
}
}
[RelayCommand]
private void SwitchToTable()
{
WeakReferenceMessenger.Default.Send(new NavigateToQfodTableMessage(ScenarioId));
}
public async Task LoadDataAsync()
{
if (IsLoading) return; // 防并发
IsLoading = true;
LoadingMessage = "正在加载数据...";
try
{
var records = await _scenarioService.GetQfodRecordsByScenarioIdAsync(ScenarioId);
if (records.Count == 0)
{
Series = Array.Empty<ISeries>();
_logger.LogWarning("未找到 Qfod 数据: ScenarioId={ScenarioId}", ScenarioId);
LoadingMessage = "无数据";
return;
}
// 构建 DeltaQ、CurrentQ、RawQ 三条曲线
var deltaQValues = records.Select(r => (double)r.DeltaQ).ToArray();
var currentQValues = records.Select(r => (double)r.CurrentQ).ToArray();
var rawQValues = records.Select(r => (double)r.RawQ).ToArray();
var coilLabels = records.Select(r => $"{r.ChargerIndex}-{r.CoilIndex}").ToArray();
Series = new ISeries[]
{
new LineSeries<double>
{
Name = "DeltaQ",
Values = deltaQValues,
GeometrySize = 5
},
new LineSeries<double>
{
Name = "CurrentQ",
Values = currentQValues,
GeometrySize = 5
},
new LineSeries<double>
{
Name = "RawQ",
Values = rawQValues,
GeometrySize = 5
}
};
// 更新 X 轴标签
XAxes = new Axis[]
{
new Axis
{
Name = "Charger-Coil Index",
Labels = coilLabels
}
};
_logger.LogInformation("加载 Qfod 图表数据成功: ScenarioId={ScenarioId}, 记录数={Count}", ScenarioId, records.Count);
LoadingMessage = "";
}
catch (Exception ex)
{
_logger.LogError(ex, "加载 Qfod 图表数据失败: ScenarioId={ScenarioId}", ScenarioId);
LoadingMessage = $"加载失败: {ex.Message}";
}
finally
{
IsLoading = false;
}
}
}

View File

@@ -1,87 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Logging;
using System.Collections.ObjectModel;
using WCTDataMiner.Core.Models;
using WCTDataMiner.Core.Services;
using WCTDataMiner.Wpf.Messages;
namespace WCTDataMiner.Wpf.ViewModels;
/// <summary>
/// Qfod 数据表格 ViewModel
/// </summary>
public partial class QfodTableViewModel : ObservableObject
{
private readonly ScenarioService _scenarioService;
private readonly ILogger<QfodTableViewModel> _logger;
[ObservableProperty]
private Guid _scenarioId;
[ObservableProperty]
private ObservableCollection<QfodRecord> _records = new();
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _loadingMessage = "";
public QfodTableViewModel(
ScenarioService scenarioService,
ILogger<QfodTableViewModel> logger)
{
_scenarioService = scenarioService;
_logger = logger;
}
/// <summary>
/// ScenarioId 属性变更时自动触发数据加载
/// </summary>
partial void OnScenarioIdChanged(Guid value)
{
if (value != Guid.Empty)
{
_ = LoadDataAsync().ContinueWith(t =>
{
if (t.Exception != null)
{
_logger.LogError(t.Exception, "LoadDataAsync 失败: ScenarioId={ScenarioId}", value);
}
}, TaskContinuationOptions.OnlyOnFaulted);
}
}
public async Task LoadDataAsync()
{
if (IsLoading) return; // 防并发
IsLoading = true;
LoadingMessage = "正在加载数据...";
try
{
var records = await _scenarioService.GetQfodRecordsByScenarioIdAsync(ScenarioId);
Records = new ObservableCollection<QfodRecord>(records);
_logger.LogInformation("加载 Qfod 表格数据成功: ScenarioId={ScenarioId}, 记录数={Count}", ScenarioId, records.Count);
LoadingMessage = "";
}
catch (Exception ex)
{
_logger.LogError(ex, "加载 Qfod 表格数据失败: ScenarioId={ScenarioId}", ScenarioId);
LoadingMessage = $"加载失败: {ex.Message}";
}
finally
{
IsLoading = false;
}
}
[RelayCommand]
private void SwitchToChart()
{
WeakReferenceMessenger.Default.Send(new NavigateToQfodChartMessage(ScenarioId));
}
}

View File

@@ -1,177 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.Logging;
using System.Collections.ObjectModel;
using WCTDataMiner.Core.Services;
using WCTDataMiner.Wpf.Messages;
namespace WCTDataMiner.Wpf.ViewModels;
/// <summary>
/// 场景列表 ViewModel
/// </summary>
public partial class ScenarioListViewModel : ObservableObject, IRecipient<DataImportedMessage>
{
private readonly ScenarioService _scenarioService;
private readonly ILogger<ScenarioListViewModel> _logger;
[ObservableProperty]
private ObservableCollection<ScenarioItem> _scenarios = new();
[ObservableProperty]
private ScenarioItem? _selectedScenario;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _loadingMessage = "";
public ScenarioListViewModel(
ScenarioService scenarioService,
ILogger<ScenarioListViewModel> logger)
{
_scenarioService = scenarioService;
_logger = logger;
// 注册消息订阅,接收导入完成通知
WeakReferenceMessenger.Default.Register<DataImportedMessage>(this);
}
/// <summary>
/// 从数据库加载场景列表
/// </summary>
public async Task LoadScenariosAsync()
{
// 避免并发加载
if (IsLoading)
{
_logger.LogWarning("跳过重复加载请求:当前正在加载");
return;
}
IsLoading = true;
LoadingMessage = "正在加载场景列表...";
try
{
var scenarios = await _scenarioService.GetAllScenariosAsync();
// 批量更新,避免逐项 Add 导致频繁 UI 刷新
var items = scenarios.Select(s => new ScenarioItem
{
Id = s.Id,
TxPanel = s.TxPanel?.Name ?? "",
TxHardware = s.TxHardware?.Version ?? "",
TxSoftware = s.TxSoftware?.Version ?? "",
RxType = s.RxType?.Name ?? "",
TestDate = s.TestDate.ToString("yyyy-MM-dd"),
QfodCount = s.QfodCount,
PlossCount = s.PlossCount
}).ToList();
Scenarios = new ObservableCollection<ScenarioItem>(items);
}
catch (Exception ex)
{
_logger.LogError(ex, "加载场景列表失败");
LoadingMessage = $"加载失败: {ex.Message}";
return;
}
finally
{
IsLoading = false;
LoadingMessage = "";
}
}
/// <summary>
/// 根据筛选条件加载场景
/// </summary>
public async Task LoadScenariosByFilterAsync(
string? txPanel = null,
string? txHardware = null,
string? txSoftware = null,
string? rxType = null)
{
// 避免并发加载
if (IsLoading)
{
_logger.LogWarning("跳过重复筛选请求:当前正在加载");
return;
}
IsLoading = true;
LoadingMessage = "正在筛选场景...";
try
{
var scenarios = await _scenarioService.GetScenariosByFilterAsync(
txPanel, txHardware, txSoftware, rxType);
// 批量更新,避免逐项 Add 导致频繁 UI 刷新
var items = scenarios.Select(s => new ScenarioItem
{
Id = s.Id,
TxPanel = s.TxPanel?.Name ?? "",
TxHardware = s.TxHardware?.Version ?? "",
TxSoftware = s.TxSoftware?.Version ?? "",
RxType = s.RxType?.Name ?? "",
TestDate = s.TestDate.ToString("yyyy-MM-dd"),
QfodCount = s.QfodCount,
PlossCount = s.PlossCount
}).ToList();
Scenarios = new ObservableCollection<ScenarioItem>(items);
}
catch (Exception ex)
{
_logger.LogError(ex, "筛选场景失败");
LoadingMessage = $"筛选失败: {ex.Message}";
return;
}
finally
{
IsLoading = false;
LoadingMessage = "";
}
}
/// <summary>
/// 接收数据导入完成消息,自动刷新列表
/// </summary>
public async void Receive(DataImportedMessage message)
{
try
{
await LoadScenariosAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "接收数据导入消息后刷新场景列表失败");
}
}
/// <summary>
/// 获取可用的筛选选项
/// </summary>
public async Task<FilterOptions> GetFilterOptionsAsync()
{
return await _scenarioService.GetFilterOptionsAsync();
}
}
/// <summary>
/// 场景项数据模型
/// </summary>
public class ScenarioItem
{
public Guid Id { get; set; }
public string TxPanel { get; set; } = "";
public string TxHardware { get; set; } = "";
public string TxSoftware { get; set; } = "";
public string RxType { get; set; } = "";
public string TestDate { get; set; } = "";
public int QfodCount { get; set; }
public int PlossCount { get; set; }
}

View File

@@ -1,63 +0,0 @@
<UserControl x:Class="WCTDataMiner.Wpf.Views.ImportView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WCTDataMiner.Wpf.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="600">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 标题 -->
<TextBlock Grid.Row="0" Text="数据导入" FontSize="20" FontWeight="Bold" Margin="0,0,0,20"/>
<!-- 选择路径 -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,15">
<Button Content="选择文件" Command="{Binding SelectFileCommand}"
Width="100" Margin="0,0,10,0" Padding="10,5"/>
<Button Content="选择目录" Command="{Binding SelectFolderCommand}"
Width="100" Margin="0,0,10,0" Padding="10,5"/>
<TextBox Text="{Binding SelectedPath, UpdateSourceTrigger=PropertyChanged}"
Width="300" IsReadOnly="True" VerticalContentAlignment="Center"/>
</StackPanel>
<!-- 选项和导入按钮 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,0,0,15">
<CheckBox Content="递归搜索子目录" IsChecked="{Binding IsRecursive}"
Margin="0,0,20,0" VerticalContentAlignment="Center"/>
<Button Content="开始导入" Command="{Binding ImportCommand}"
Width="120" Padding="15,8" FontWeight="Bold"
IsEnabled="{Binding IsImporting, Converter={StaticResource InverseBooleanConverter}}"/>
</StackPanel>
<!-- 进度和状态 -->
<StackPanel Grid.Row="3" Margin="0,0,0,15">
<ProgressBar Height="20" Value="{Binding ProgressValue}" Maximum="100"
Visibility="{Binding IsImporting, Converter={StaticResource BooleanToVisibilityConverter}}"/>
<TextBlock Text="{Binding StatusMessage}" FontSize="14" Margin="0,5,0,0"/>
</StackPanel>
<!-- 导入结果列表 -->
<DataGrid Grid.Row="4" ItemsSource="{Binding ImportResults}"
AutoGenerateColumns="False"
IsReadOnly="True"
GridLinesVisibility="Horizontal"
AlternatingRowBackground="#F9F9F9"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="文件名" Binding="{Binding FileName}" Width="*"/>
<DataGridTextColumn Header="Qfod" Binding="{Binding QfodCount}" Width="80"/>
<DataGridTextColumn Header="Ploss" Binding="{Binding PlossCount}" Width="80"/>
<DataGridTextColumn Header="错误" Binding="{Binding ErrorCount}" Width="80"/>
<DataGridTextColumn Header="状态" Binding="{Binding Status}" Width="100"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

View File

@@ -1,12 +0,0 @@
using System.Windows;
using System.Windows.Controls;
namespace WCTDataMiner.Wpf.Views;
public partial class ImportView : UserControl
{
public ImportView()
{
InitializeComponent();
}
}

View File

@@ -1,33 +0,0 @@
<UserControl x:Class="WCTDataMiner.Wpf.Views.PlossChartView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF"
xmlns:local="clr-namespace:WCTDataMiner.Wpf.Views"
mc:Ignorable="d"
d:DesignHeight="400" d:DesignWidth="600">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="Ploss 功率损耗趋势图" FontSize="18" FontWeight="Bold"/>
<TextBlock Text="{Binding ScenarioId, StringFormat=' - 场景 {0}'}" FontSize="14" Foreground="Gray" Margin="10,5,0,0"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,10">
<Button Content="切换到表格" Command="{Binding SwitchToTableCommand}" Padding="10,5"/>
</StackPanel>
<lvc:CartesianChart Grid.Row="2"
Series="{Binding Series}"
XAxes="{Binding XAxes}"
YAxes="{Binding YAxes}"
LegendPosition="Bottom"
Background="White"/>
</Grid>
</UserControl>

View File

@@ -1,18 +0,0 @@
using System.Windows.Controls;
namespace WCTDataMiner.Wpf.Views;
public partial class PlossChartView : UserControl
{
public PlossChartView()
{
InitializeComponent();
Loaded += async (s, e) =>
{
if (DataContext is ViewModels.PlossChartViewModel vm)
{
await vm.LoadDataAsync();
}
};
}
}

View File

@@ -1,79 +0,0 @@
<UserControl x:Class="WCTDataMiner.Wpf.Views.PlossTableView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="400" d:DesignWidth="900">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 标题区域 -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="Ploss 数据表格" FontSize="18" FontWeight="Bold"/>
<TextBlock Text="{Binding ScenarioId, StringFormat=' - 场景 {0}'}" FontSize="14" Foreground="Gray" Margin="10,5,0,0"/>
</StackPanel>
<!-- 操作按钮 -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,10">
<Button Content="切换到图表" Command="{Binding SwitchToChartCommand}" Padding="10,5" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Records.Count, StringFormat='共 {0} 条记录'}" VerticalAlignment="Center" Foreground="Gray"/>
<!-- 加载状态/错误消息显示 -->
<TextBlock Text="{Binding LoadingMessage}" VerticalAlignment="Center" Foreground="Orange" Margin="20,0,0,0"/>
</StackPanel>
<!-- 数据表格 -->
<DataGrid Grid.Row="2"
ItemsSource="{Binding Records}"
AutoGenerateColumns="False"
IsReadOnly="True"
GridLinesVisibility="Horizontal"
AlternatingRowBackground="#F9F9F9"
CanUserAddRows="False"
HeadersVisibility="Column"
HorizontalScrollBarVisibility="Auto"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
EnableRowVirtualization="True"
EnableColumnVirtualization="True">
<DataGrid.Columns>
<DataGridTextColumn Header="PowLoss" Binding="{Binding PowLoss}" Width="80"/>
<DataGridTextColumn Header="DeltaP" Binding="{Binding DeltaP}" Width="80"/>
<DataGridTextColumn Header="RX_TYPE" Binding="{Binding Field1}" Width="80"/>
<DataGridTextColumn Header="Field2" Binding="{Binding Field2}" Width="70"/>
<DataGridTextColumn Header="Field3" Binding="{Binding Field3}" Width="70"/>
<DataGridTextColumn Header="RX power" Binding="{Binding Field4}" Width="80"/>
<DataGridTextColumn Header="TX power" Binding="{Binding Field5}" Width="80"/>
<DataGridTextColumn Header="Field6" Binding="{Binding Field6}" Width="70"/>
<DataGridTextColumn Header="Field7" Binding="{Binding Field7}" Width="80">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="Blue"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="Field8" Binding="{Binding Field8}" Width="80">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="Red"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="ploss" Binding="{Binding Field9}" Width="70"/>
<DataGridTextColumn Header="threshold" Binding="{Binding Field10}" Width="80"/>
<DataGridTextColumn Header="Margin" Binding="{Binding Margin}" Width="80"/>
<DataGridTextColumn Header="触发次数" Binding="{Binding Field11}" Width="80"/>
<DataGridTextColumn Header="PFOD result" Binding="{Binding Field12}" Width="90"/>
<DataGridTextColumn Header="Field13" Binding="{Binding Field13}" Width="70"/>
<DataGridTextColumn Header="Field14" Binding="{Binding Field14}" Width="70"/>
<DataGridTextColumn Header="Field15" Binding="{Binding Field15}" Width="70"/>
<DataGridTextColumn Header="Field16" Binding="{Binding Field16}" Width="70"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

View File

@@ -1,14 +0,0 @@
using System.Windows.Controls;
namespace WCTDataMiner.Wpf.Views;
/// <summary>
/// PlossTableView.xaml 的交互逻辑
/// </summary>
public partial class PlossTableView : UserControl
{
public PlossTableView()
{
InitializeComponent();
}
}

View File

@@ -1,33 +0,0 @@
<UserControl x:Class="WCTDataMiner.Wpf.Views.QfodChartView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF"
xmlns:local="clr-namespace:WCTDataMiner.Wpf.Views"
mc:Ignorable="d"
d:DesignHeight="400" d:DesignWidth="600">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="Qfod Q值变化图" FontSize="18" FontWeight="Bold"/>
<TextBlock Text="{Binding ScenarioId, StringFormat=' - 场景 {0}'}" FontSize="14" Foreground="Gray" Margin="10,5,0,0"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,10">
<Button Content="切换到表格" Command="{Binding SwitchToTableCommand}" Padding="10,5"/>
</StackPanel>
<lvc:CartesianChart Grid.Row="2"
Series="{Binding Series}"
XAxes="{Binding XAxes}"
YAxes="{Binding YAxes}"
LegendPosition="Bottom"
Background="White"/>
</Grid>
</UserControl>

View File

@@ -1,18 +0,0 @@
using System.Windows.Controls;
namespace WCTDataMiner.Wpf.Views;
public partial class QfodChartView : UserControl
{
public QfodChartView()
{
InitializeComponent();
Loaded += async (s, e) =>
{
if (DataContext is ViewModels.QfodChartViewModel vm)
{
await vm.LoadDataAsync();
}
};
}
}

View File

@@ -1,61 +0,0 @@
<UserControl x:Class="WCTDataMiner.Wpf.Views.QfodTableView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="400" d:DesignWidth="600">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 标题区域 -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="Qfod 数据表格" FontSize="18" FontWeight="Bold"/>
<TextBlock Text="{Binding ScenarioId, StringFormat=' - 场景 {0}'}" FontSize="14" Foreground="Gray" Margin="10,5,0,0"/>
</StackPanel>
<!-- 操作按钮 -->
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,0,0,10">
<Button Content="切换到图表" Command="{Binding SwitchToChartCommand}" Padding="10,5" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Records.Count, StringFormat='共 {0} 条记录'}" VerticalAlignment="Center" Foreground="Gray"/>
<!-- 加载状态/错误消息显示 -->
<TextBlock Text="{Binding LoadingMessage}" VerticalAlignment="Center" Foreground="Orange" Margin="20,0,0,0"/>
</StackPanel>
<!-- 数据表格 -->
<DataGrid Grid.Row="2"
ItemsSource="{Binding Records}"
AutoGenerateColumns="False"
IsReadOnly="True"
GridLinesVisibility="Horizontal"
AlternatingRowBackground="#F9F9F9"
CanUserAddRows="False"
HeadersVisibility="Column"
HorizontalScrollBarVisibility="Auto"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
EnableRowVirtualization="True"
EnableColumnVirtualization="True">
<DataGrid.Columns>
<DataGridTextColumn Header="ChargerIdx" Binding="{Binding ChargerIndex}" Width="100"/>
<DataGridTextColumn Header="CoilIdx" Binding="{Binding CoilIndex}" Width="80"/>
<DataGridTextColumn Header="DeltaQ" Binding="{Binding DeltaQ}" Width="80">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="Blue"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="CurrentQ" Binding="{Binding CurrentQ}" Width="80"/>
<DataGridTextColumn Header="RawQ" Binding="{Binding RawQ}" Width="80"/>
<DataGridTextColumn Header="FodType" Binding="{Binding FodType}" Width="80"/>
<DataGridTextColumn Header="FieldF" Binding="{Binding FieldF}" Width="80"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

View File

@@ -1,14 +0,0 @@
using System.Windows.Controls;
namespace WCTDataMiner.Wpf.Views;
/// <summary>
/// QfodTableView.xaml 的交互逻辑
/// </summary>
public partial class QfodTableView : UserControl
{
public QfodTableView()
{
InitializeComponent();
}
}

View File

@@ -1,46 +0,0 @@
<UserControl x:Class="WCTDataMiner.Wpf.Views.ScenarioListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WCTDataMiner.Wpf.Views"
mc:Ignorable="d"
d:DesignHeight="400" d:DesignWidth="600">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="测试场景列表" FontSize="18" FontWeight="Bold" Margin="0,0,0,10"/>
<DataGrid Grid.Row="1" ItemsSource="{Binding Scenarios}"
SelectedItem="{Binding SelectedScenario}"
AutoGenerateColumns="False"
IsReadOnly="True"
GridLinesVisibility="Horizontal"
AlternatingRowBackground="#F9F9F9">
<DataGrid.Columns>
<DataGridTextColumn Header="TX 面板" Binding="{Binding TxPanel}" Width="*"/>
<DataGridTextColumn Header="TX 硬件" Binding="{Binding TxHardware}" Width="*"/>
<DataGridTextColumn Header="TX 软件" Binding="{Binding TxSoftware}" Width="*"/>
<DataGridTextColumn Header="RX 类型" Binding="{Binding RxType}" Width="*"/>
<DataGridTextColumn Header="测试日期" Binding="{Binding TestDate}" Width="*"/>
<DataGridTextColumn Header="Qfod 数量" Binding="{Binding QfodCount}" Width="80"/>
<DataGridTextColumn Header="Ploss 数量" Binding="{Binding PlossCount}" Width="80"/>
<DataGridTemplateColumn Header="操作" Width="150">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Content="Ploss图" Margin="2" Padding="5,2"
Click="OnViewPlossChart"/>
<Button Content="Qfod图" Margin="2" Padding="5,2"
Click="OnViewQfodChart"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

View File

@@ -1,31 +0,0 @@
using CommunityToolkit.Mvvm.Messaging;
using System.Windows;
using System.Windows.Controls;
using WCTDataMiner.Wpf.Messages;
using WCTDataMiner.Wpf.ViewModels;
namespace WCTDataMiner.Wpf.Views;
public partial class ScenarioListView : UserControl
{
public ScenarioListView()
{
InitializeComponent();
}
private void OnViewPlossChart(object sender, RoutedEventArgs e)
{
if (DataContext is ScenarioListViewModel vm && vm.SelectedScenario != null)
{
WeakReferenceMessenger.Default.Send(new NavigateToPlossChartMessage(vm.SelectedScenario.Id));
}
}
private void OnViewQfodChart(object sender, RoutedEventArgs e)
{
if (DataContext is ScenarioListViewModel vm && vm.SelectedScenario != null)
{
WeakReferenceMessenger.Default.Send(new NavigateToQfodChartMessage(vm.SelectedScenario.Id));
}
}
}

View File

@@ -1,42 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<LangVersion>12.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<!-- LiveCharts2 -->
<PackageReference Include="LiveChartsCore" Version="2.0.0-rc2" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.WPF" Version="2.0.0-rc2" />
<!-- MVVM -->
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<!-- DI -->
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<!-- Logging -->
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\WCTDataMiner.Core\WCTDataMiner.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>