Compare commits
4 Commits
5189c665c3
...
2b031995e1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b031995e1 | ||
|
|
a3100c73c5 | ||
|
|
6e197a0cce | ||
|
|
645f46abcd |
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
# WCTDataMiner 文档中心
|
||||
# Gpulse.WCT.DataAnalyzer 文档中心
|
||||
|
||||
> 无线充电 FOD(异物检测)日志数据采集系统
|
||||
|
||||
|
||||
@@ -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`
|
||||
@@ -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);
|
||||
```
|
||||
@@ -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 | ✅ 发布库 |
|
||||
| 双库隔离 | ✅ 无外键/导航属性 |
|
||||
@@ -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
|
||||
```
|
||||
|
||||
两个数据库独立迁移,不共享迁移历史。
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
```csharp
|
||||
// Parsers/IParser.cs
|
||||
namespace WCTDataMiner.Parsers;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Parsers;
|
||||
|
||||
public interface IParser<TRecord>
|
||||
{
|
||||
|
||||
@@ -4,213 +4,158 @@
|
||||
|
||||
---
|
||||
|
||||
## 1. 完整解析流程
|
||||
## 1. 完整流程
|
||||
|
||||
```text
|
||||
log 文件
|
||||
→ parse
|
||||
→ local.db
|
||||
→ aggregate
|
||||
→ release.db
|
||||
→ export
|
||||
→ WCT-ChargingParameterDatabase.csv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 阶段一:parse → local.db
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant CLI as CLI Entry
|
||||
participant CLI as CLI
|
||||
participant PS as ParseService
|
||||
participant SS as ScenarioService
|
||||
participant FNP as FileNameParser
|
||||
participant QP as QfodParser
|
||||
participant PP as PlossParser
|
||||
participant DB as Database
|
||||
participant Log as Serilog
|
||||
participant DB as local.db
|
||||
|
||||
CLI->>PS: 扫描日志目录
|
||||
PS-->>CLI: 返回文件列表
|
||||
CLI->>PS: ParseFileAsync(filePath)
|
||||
PS->>FNP: 解析文件名
|
||||
FNP-->>PS: ScenarioInfo
|
||||
PS->>SS: GetOrCreateScenarioAsync
|
||||
SS->>DB: 维度 upsert + 场景 upsert
|
||||
SS-->>PS: TestScenario
|
||||
PS->>PS: 逐行读取文件
|
||||
|
||||
loop 每个日志文件
|
||||
CLI->>PS: ParseFileAsync(filePath)
|
||||
|
||||
PS->>FNP: 解析文件名
|
||||
FNP-->>PS: 场景信息(面板、版本、RX类型等)
|
||||
|
||||
PS->>SS: GetOrCreateScenarioAsync(场景信息)
|
||||
SS->>DB: 查询/创建维度记录
|
||||
SS->>DB: 查询/创建场景记录
|
||||
SS-->>PS: 返回 TestScenario
|
||||
|
||||
PS->>PS: 读取文件内容
|
||||
|
||||
loop 逐行遍历
|
||||
alt 是 Qfod 行
|
||||
PS->>QP: Parse(line, scenarioId)
|
||||
QP-->>PS: ParseResult
|
||||
else 是 Ploss 行
|
||||
PS->>PP: Parse(line, scenarioId)
|
||||
PP-->>PS: ParseResult
|
||||
end
|
||||
|
||||
alt 解析成功
|
||||
PS->>PS: 加入记录列表
|
||||
else 解析失败
|
||||
PS->>Log: LogError(line, error)
|
||||
end
|
||||
loop 每行
|
||||
alt Qfod 行
|
||||
PS->>QP: Parse(line)
|
||||
QP-->>PS: QfodRecord
|
||||
else Ploss 行
|
||||
PS->>PP: Parse(line)
|
||||
PP-->>PS: PlossRecord
|
||||
end
|
||||
|
||||
PS->>DB: BATCH INSERT QfodRecords
|
||||
PS->>DB: BATCH INSERT PlossRecords
|
||||
PS->>DB: UPDATE scenario (qfod_count, ploss_count)
|
||||
|
||||
PS-->>CLI: 返回 ParseReport
|
||||
end
|
||||
|
||||
CLI-->>CLI: 输出解析报告
|
||||
PS->>DB: BatchInsert QfodRecords
|
||||
PS->>DB: BatchInsert PlossRecords
|
||||
PS->>DB: UpdateScenarioStats
|
||||
PS-->>CLI: ParseReport
|
||||
```
|
||||
|
||||
**关键点:**
|
||||
|
||||
- 文件名格式: `TxPanel-TxHardware-TxSoftware-RxType-Purpose-Date-Seq.log`
|
||||
- 支持两行 Ploss 格式(header 行 + 数据行)
|
||||
- 事务批量写入,读取 `Parser:BatchSize` 配置分批
|
||||
- 解析错误记录到 Serilog,不阻断流程
|
||||
|
||||
---
|
||||
|
||||
## 3. 阶段二:aggregate → release.db
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant CLI as CLI
|
||||
participant AS as AggregationService
|
||||
participant LDB as local.db
|
||||
participant RDB as release.db
|
||||
|
||||
CLI->>AS: AggregateAsync()
|
||||
AS->>LDB: 读取所有 PlossRecord + 关联维度
|
||||
AS->>AS: 按 (车厂, 车型, 手机厂商, 型号) 分组
|
||||
AS->>AS: 计算 9 个功率列平均值
|
||||
AS->>AS: 计算 Q值/Q基值/P-Q系数
|
||||
AS->>RDB: DELETE + INSERT (事务)
|
||||
AS-->>CLI: AggregationReport
|
||||
```
|
||||
|
||||
**聚合规则:**
|
||||
|
||||
| 字段 | 来源 | 计算方式 |
|
||||
|------|------|----------|
|
||||
| 车厂 (CarFactory) | 配置映射 `Aggregation:TxPanelMappings` | 找不到映射时 fallback 为 TxPanel.Name |
|
||||
| 车型 (CarModel) | 配置映射 `Aggregation:CarModelMappings` | fallback 为 TxHardware.Version |
|
||||
| 手机厂商 (PhoneBrand) | RxType.Name 拆分 | 取第一个分隔符前部分 |
|
||||
| 型号 (PhoneModel) | RxType.Name 拆分 | 取第一个分隔符后部分 |
|
||||
| 350mW~2250mW(9列) | PlossRecord.Field7 | 按功率匹配后取平均值 |
|
||||
| Q值 (QValue) | QfodRecord.CurrentQ | 平均值 |
|
||||
| Q基值 (QBaseValue) | QfodRecord.RawQ | 平均值 |
|
||||
| P-Q值系数 (PqCoefficient) | PlossRecord.DeltaP | 平均值 |
|
||||
| 谐振频率 (ResonanceFrequency) | 暂无 | 允许为空 |
|
||||
|
||||
**替代配置:**
|
||||
|
||||
配置文件 `appsettings.json` 中可添加映射:
|
||||
|
||||
```json
|
||||
{
|
||||
"Aggregation": {
|
||||
"TxPanelMappings": {
|
||||
"single-mold": "奇瑞",
|
||||
"dual-rapid": "智己"
|
||||
},
|
||||
"CarModelMappings": {
|
||||
"L6": "L6",
|
||||
"CM3": "CM3"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. ParseService 实现
|
||||
## 4. 阶段三:export → CSV
|
||||
|
||||
```csharp
|
||||
// Services/ParseService.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
using 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.db,aggregate 不写 local.db
|
||||
@@ -1,68 +1,149 @@
|
||||
# Architecture: WCTDataMiner
|
||||
# Architecture: Gpulse.WCT.DataAnalyzer
|
||||
|
||||
> 无线充电 FOD(异物检测)日志数据采集系统,从日志文件中解析 Qfod 和 Ploss 数据并存储到数据库
|
||||
> 无线充电参数采集系统。从 log 文件解析 Qfod/Ploss 数据 → 本地分析库 → 汇总生成正式充电参数数据库 → 导出 CSV。
|
||||
|
||||
---
|
||||
|
||||
## 数据流
|
||||
|
||||
```text
|
||||
log 文件
|
||||
→ parse
|
||||
→ local.db(本地解析库)
|
||||
→ aggregate
|
||||
→ release.db(正式发布库)
|
||||
→ export
|
||||
→ WCT-ChargingParameterDatabase.csv
|
||||
```
|
||||
|
||||
| 阶段 | 命令 | 存储 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 解析 | `parse` | `local.db` | 本地日志解析库,保存原始 Qfod/Ploss 记录 |
|
||||
| 汇总 | `aggregate` | `release.db` | 从 local.db 汇总生成正式充电参数记录 |
|
||||
| 导出 | `export` | CSV | 读取 release.db,输出固定格式 CSV |
|
||||
|
||||
## 文档索引
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [技术选型](./技术选型.md) | 技术栈总览、决策记录 |
|
||||
| [模块划分](./模块划分.md) | 模块总览、分层架构、依赖关系 |
|
||||
| [安全架构](./安全架构.md) | 认证方案、数据安全 |
|
||||
| [解析器设计](./parser/概览.md) | Qfod/Ploss 解析器详细设计 |
|
||||
| [数据处理流程](./数据处理流程.md) | 解析流程、批量写入、错误处理 |
|
||||
| [配置管理](./配置管理.md) | 配置文件、配置类定义 |
|
||||
| [CLI 命令设计](./CLI命令设计.md) | 命令行接口定义 |
|
||||
| [数据库切换支持](./database/概览.md) | SQLite/PostgreSQL 切换方案 |
|
||||
| [技术选型](./技术选型.md) | 技术栈、决策记录 |
|
||||
| [模块划分](./模块划分.md) | 分层架构、目录结构、模块职责 |
|
||||
| [数据处理流程](./数据处理流程.md) | 解析→汇总→导出完整流程 |
|
||||
| [CLI 命令设计](./CLI命令设计.md) | 命令行接口 |
|
||||
| [配置管理](./配置管理.md) | 配置结构与双数据库配置 |
|
||||
| [数据库支持](./database/概览.md) | 本地/发布双数据库架构 |
|
||||
| [安全架构](./安全架构.md) | 路径校验、安全策略 |
|
||||
| [解析器设计](./parser/概览.md) | Qfod/Ploss 解析器 |
|
||||
|
||||
---
|
||||
|
||||
## 架构图
|
||||
## 架构分层
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ CLI Entry Point │
|
||||
│ (Program.cs / System.CommandLine) │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ Application Layer │
|
||||
│ ┌───────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐ │
|
||||
│ │ LocalIngestion│ │ Publishing │ │ Exporting │ │Administration│ │
|
||||
│ │ ParseService │ │AggregationSvc│ │ExportSvc │ │ StatsService │ │
|
||||
│ │ ScenarioSvc │ │ThresholdCalc │ │ │ │ CleanService │ │
|
||||
│ │ DimensionSvc │ │ │ │ │ │ │ │
|
||||
│ └───────────────┘ └──────────────┘ └────────────┘ └──────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Parsing │ │
|
||||
│ │ QfodParser │ PlossParser │ FileNameParser │ │
|
||||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ Domain Layer │
|
||||
│ ┌─────────────────────────┐ ┌──────────────────────────────────────┐ │
|
||||
│ │ Local/ (本地实体) │ │ Release/ (发布实体) │ │
|
||||
│ │ TxPanel, TxHardware │ │ ChargingParameterRecord │ │
|
||||
│ │ TxSoftware, RxType │ │ │ │
|
||||
│ │ TestScenario │ │ │ │
|
||||
│ │ QfodRecord, PlossRecord │ │ │ │
|
||||
│ └─────────────────────────┘ └──────────────────────────────────────┘ │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ Infrastructure Layer │
|
||||
│ ┌───────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐ │
|
||||
│ │ LocalData │ │ ReleaseData │ │Configuration│ │ Security │ │
|
||||
│ │ WctMinerDbCtx │ │ReleaseDbCtx │ │AppSettings │ │PathValidator │ │
|
||||
│ │ DbCtxFactory │ │ReleaseDbCtxF │ │ │ │ │ │
|
||||
│ │ SeedData │ │ │ │ │ │ │ │
|
||||
│ └───────────────┘ └──────────────┘ └────────────┘ └──────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CLI Entry Point │
|
||||
│ (Program.cs / System.CommandLine) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Service Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ParseService │ ErrorHandlingService │ ThresholdCalc │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Parser Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ QfodParser │ PlossParser │ LogFileReader │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Data Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Models │ WctMinerDbContext │ IDbContextFactory │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Infrastructure │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Configuration │ Serilog │ AppSettings │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心设计原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| 技术栈 | .NET 8.0 LTS + C# 12.0 + EF Core 8.0 |
|
||||
| 数据库 | SQLite(当前)/ PostgreSQL(后续),配置切换 |
|
||||
| 解析模式 | 批处理,正则表达式匹配 |
|
||||
| ORM 模式 | Code First,支持迁移自动化 |
|
||||
| CLI 框架 | System.CommandLine,命令模式 |
|
||||
| 技术栈 | .NET 8.0 + C# 12.0 + EF Core 8.0 |
|
||||
| 数据库 | local.db / release.db 两个独立 SQLite 数据库 |
|
||||
| 本地/发布隔离 | 无外键、无导航属性、无数据库级关联 |
|
||||
| 解析模式 | 批处理,正则表达式 |
|
||||
| CLI | System.CommandLine,命令模式 |
|
||||
| 汇总规则 | 按业务键分组取平均值,缺失字段允许为空 |
|
||||
|
||||
---
|
||||
## 目录结构
|
||||
|
||||
## 假设/歧义标注
|
||||
|
||||
| 标注 | 内容 |
|
||||
|------|------|
|
||||
| [A] | 使用 .NET 8.0 LTS 版本 |
|
||||
| [A] | 初期使用 SQLite,架构支持切换 PostgreSQL |
|
||||
| [?] | 增量解析策略需确认 |
|
||||
| [?] | 多次解析同一文件的处理策略需确认 |
|
||||
```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
|
||||
```
|
||||
@@ -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
|
||||
```
|
||||
@@ -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 自动拆分(分隔符 `/_:`)。
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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 通过纯值映射,不依赖导航属性
|
||||
@@ -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>
|
||||
/// 数据清理服务 - 支持软删除
|
||||
@@ -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>
|
||||
/// 统计查询服务 - 使用数据库端聚合优化内存效率
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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操作(并发安全版本)
|
||||
@@ -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>
|
||||
/// 解析流程协调服务 - 协调文件名解析、行解析、批量写入
|
||||
@@ -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>
|
||||
/// 篮选条件常量
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace WCTDataMiner.Core.Parsers;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名解析器 - 从文件名提取场景信息
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Parsers;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||
|
||||
/// <summary>
|
||||
/// 通用解析器接口
|
||||
@@ -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日志解析器 - 支持两行格式
|
||||
@@ -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日志解析器
|
||||
@@ -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);
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Services;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 阈值计算器 - 根据接收功率查表获取动态阈值
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
/// <summary>
|
||||
/// Ploss记录实体 - 存储Ploss FOD格式解析数据
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
/// <summary>
|
||||
/// Qfod记录实体 - 存储Qfod格式解析数据
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
/// <summary>
|
||||
/// RX类型维度表
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
/// <summary>
|
||||
/// 测试场景实体 - 通过外键关联各维度表
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
/// <summary>
|
||||
/// TX硬件版本维度表
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
/// <summary>
|
||||
/// TX面板类型维度表
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
/// <summary>
|
||||
/// TX软件版本维度表
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
{
|
||||
@@ -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>
|
||||
{
|
||||
@@ -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>
|
||||
{
|
||||
@@ -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>
|
||||
{
|
||||
@@ -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>
|
||||
{
|
||||
@@ -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>
|
||||
{
|
||||
@@ -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>
|
||||
{
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Data;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库上下文工厂接口
|
||||
@@ -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>
|
||||
/// 初始数据种子
|
||||
@@ -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数据采集数据库上下文
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
public interface IReleaseDbContextFactory
|
||||
{
|
||||
ReleaseDbContext CreateDbContext();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Security;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
|
||||
|
||||
/// <summary>
|
||||
/// 安全相关常量定义
|
||||
33
src/Gpulse.WCT.DataAnalyzer/Commands/AggregateCommand.cs
Normal file
33
src/Gpulse.WCT.DataAnalyzer/Commands/AggregateCommand.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
/// 数据清理命令
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
/// 解析日志文件命令
|
||||
@@ -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>
|
||||
/// 统计查询命令
|
||||
@@ -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>
|
||||
@@ -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>()));
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)]
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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 ?? "失败";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,12 +0,0 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace WCTDataMiner.Wpf.Views;
|
||||
|
||||
public partial class ImportView : UserControl
|
||||
{
|
||||
public ImportView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,14 +0,0 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace WCTDataMiner.Wpf.Views;
|
||||
|
||||
/// <summary>
|
||||
/// PlossTableView.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class PlossTableView : UserControl
|
||||
{
|
||||
public PlossTableView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1,14 +0,0 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace WCTDataMiner.Wpf.Views;
|
||||
|
||||
/// <summary>
|
||||
/// QfodTableView.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class QfodTableView : UserControl
|
||||
{
|
||||
public QfodTableView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user