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
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.0.31903.59
|
VisualStudioVersion = 17.0.31903.59
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
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
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WCTDataMiner.Wpf", "src\WCTDataMiner.Wpf\WCTDataMiner.Wpf.csproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345602}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gpulse.WCT.DataAnalyzer", "src\Gpulse.WCT.DataAnalyzer\Gpulse.WCT.DataAnalyzer.csproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345603}"
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WCTDataMiner", "src\WCTDataMiner\WCTDataMiner.csproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345603}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
|
||||||
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|Any CPU.Build.0 = 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
|
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# WCTDataMiner 文档中心
|
# Gpulse.WCT.DataAnalyzer 文档中心
|
||||||
|
|
||||||
> 无线充电 FOD(异物检测)日志数据采集系统
|
> 无线充电 FOD(异物检测)日志数据采集系统
|
||||||
|
|
||||||
|
|||||||
@@ -6,18 +6,19 @@
|
|||||||
|
|
||||||
## 1. 命令概览
|
## 1. 命令概览
|
||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 | 数据流 |
|
||||||
|------|------|
|
|------|------|--------|
|
||||||
| `parse` | 解析日志文件并存储到数据库 |
|
| `parse` | 解析日志文件到本地库 | log → local.db |
|
||||||
| `stats` | 查询统计信息 |
|
| `aggregate` | 从本地库汇总生成发布库 | local.db → release.db |
|
||||||
| `export` | 导出数据 |
|
| `export` | 导出发布库到 CSV | release.db → CSV |
|
||||||
| `clean` | 清理数据 |
|
| `stats` | 查询本地库统计信息 | 只读 local.db |
|
||||||
|
| `clean` | 清理本地库数据(软删除) | 只写 local.db |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 命令详解
|
## 2. 命令详解
|
||||||
|
|
||||||
### 2.1 parse - 解析日志文件
|
### 2.1 parse
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 解析单个文件
|
# 解析单个文件
|
||||||
@@ -29,41 +30,60 @@ dotnet run parse --dir path/to/logs/
|
|||||||
# 解析目录(递归)
|
# 解析目录(递归)
|
||||||
dotnet run parse --dir path/to/logs/ --recursive
|
dotnet run parse --dir path/to/logs/ --recursive
|
||||||
|
|
||||||
# 强制重新解析(覆盖已有数据)
|
# 强制重新解析
|
||||||
dotnet run parse --file log.txt --force
|
dotnet run parse --file log.txt --force
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.2 stats - 查询统计
|
### 2.2 aggregate
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 查看文件解析统计
|
# 从 local.db 汇总生成 release.db(默认重建)
|
||||||
dotnet run stats --file log.txt
|
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
|
dotnet run stats --summary
|
||||||
|
|
||||||
# 按 FOD 类型统计
|
# 按 FOD 类型统计
|
||||||
dotnet run stats --by-fod-type
|
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
|
```bash
|
||||||
# 导出为 CSV
|
# 清理指定场景
|
||||||
dotnet run export --format csv --output ./output/
|
dotnet run clean --scenario <guid> --confirm
|
||||||
|
|
||||||
# 导出为 JSON
|
|
||||||
dotnet run export --format json --output ./output/
|
|
||||||
|
|
||||||
# 按文件导出
|
|
||||||
dotnet run export --file log.txt --format csv
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.4 clean - 清理数据
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 清理指定文件的数据
|
|
||||||
dotnet run clean --file log.txt --confirm
|
|
||||||
|
|
||||||
# 清理所有数据
|
# 清理所有数据
|
||||||
dotnet run clean --all --confirm
|
dotnet run clean --all --confirm
|
||||||
@@ -71,98 +91,20 @@ dotnet run clean --all --confirm
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 命令实现
|
## 3. 典型工作流
|
||||||
|
|
||||||
```csharp
|
```bash
|
||||||
// Commands/ParseCommand.cs
|
# 1. 解析日志
|
||||||
using System.CommandLine;
|
dotnet run parse --dir ./data/test_input --recursive
|
||||||
|
|
||||||
namespace WCTDataMiner.Commands;
|
# 2. 查看统计
|
||||||
|
dotnet run stats --summary
|
||||||
|
|
||||||
public class ParseCommand : Command
|
# 3. 汇总生成发布库
|
||||||
{
|
dotnet run aggregate
|
||||||
public ParseCommand(ParseService parseService)
|
|
||||||
: base("parse", "Parse log files and store data to database")
|
|
||||||
{
|
|
||||||
var fileOption = new Option<string>(
|
|
||||||
"--file",
|
|
||||||
"Path to a single log file to parse"
|
|
||||||
);
|
|
||||||
|
|
||||||
var dirOption = new Option<string>(
|
# 4. 导出正式 CSV
|
||||||
"--dir",
|
dotnet run export --output ./data/output
|
||||||
"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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
输出文件: `./data/output/WCT-ChargingParameterDatabase.csv`
|
||||||
|
|
||||||
## 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);
|
|
||||||
```
|
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
# DbContext 工厂
|
# DbContext 工厂
|
||||||
|
|
||||||
> 所属模块:[数据库切换支持](./概览.md)
|
> 所属模块:[数据库支持](./概览.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 接口定义
|
## 1. 本地库工厂
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Data/IDbContextFactory.cs
|
// Infrastructure/LocalData/IDbContextFactory.cs
|
||||||
using WCTDataMiner.Data;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
|
|
||||||
namespace WCTDataMiner.Data;
|
|
||||||
|
|
||||||
public interface IDbContextFactory
|
public interface IDbContextFactory
|
||||||
{
|
{
|
||||||
@@ -18,105 +16,68 @@ public interface IDbContextFactory
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
工厂实现读取 `Database:LocalPath`(fallback `Database:Path`),支持 SQLite 和 PostgreSQL。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 工厂实现
|
## 2. 发布库工厂
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Data/DbContextFactory.cs
|
// Infrastructure/ReleaseData/IReleaseDbContextFactory.cs
|
||||||
using Microsoft.EntityFrameworkCore;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.ReleaseData;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
namespace WCTDataMiner.Data;
|
public interface IReleaseDbContextFactory
|
||||||
|
|
||||||
public class DbContextFactory : IDbContextFactory
|
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
ReleaseDbContext CreateDbContext();
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
工厂实现读取 `Database:ReleasePath`,支持 SQLite 和 PostgreSQL。PostgreSQL 时使用 `Release*` 前缀配置(`ReleaseHost`, `ReleasePort`, `ReleaseName`, `ReleaseUser`, `ReleasePassword`),未设置时回退到 local 的对应配置。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 依赖注入扩展
|
## 3. DI 注册
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Extensions/ServiceCollectionExtensions.cs
|
// Infrastructure/Extensions/ServiceCollectionExtensions.cs
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
public static IServiceCollection AddDatabaseServices(
|
||||||
using WCTDataMiner.Data;
|
this IServiceCollection services,
|
||||||
|
IConfiguration configuration)
|
||||||
namespace WCTDataMiner.Extensions;
|
|
||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddDatabaseServices(
|
// 本地库
|
||||||
this IServiceCollection services,
|
services.AddScoped<IDbContextFactory, DbContextFactory>();
|
||||||
IConfiguration configuration)
|
services.AddScoped<WctMinerDbContext>(sp =>
|
||||||
{
|
{
|
||||||
// 注册 DbContext 工厂
|
var factory = sp.GetRequiredService<IDbContextFactory>();
|
||||||
services.AddScoped<IDbContextFactory, DbContextFactory>();
|
return factory.CreateDbContext();
|
||||||
|
});
|
||||||
// 注册 DbContext(通过工厂创建)
|
|
||||||
services.AddScoped<WctMinerDbContext>(sp =>
|
|
||||||
{
|
|
||||||
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)
|
> 所属模块:[架构概览](../概览.md)
|
||||||
|
|
||||||
@@ -8,38 +8,48 @@
|
|||||||
|
|
||||||
| 文档 | 说明 |
|
| 文档 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| [DbContext 工厂](./DbContext工厂.md) | 数据库上下文工厂设计 |
|
| [DbContext 工厂](./DbContext工厂.md) | 两个数据库上下文工厂设计 |
|
||||||
| [迁移策略](./迁移策略.md) | 数据库迁移和切换步骤 |
|
| [迁移策略](./迁移策略.md) | 数据库迁移步骤 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 切换策略概览
|
## 双数据库架构
|
||||||
|
|
||||||
```
|
```text
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
│ appsettings.json │
|
│ appsettings.json │
|
||||||
│ "Database": { "Type": "sqlite" | "postgresql" } │
|
│ Database:LocalPath → local.db │
|
||||||
└─────────────────────────────────────────────────────────────┘
|
│ Database:ReleasePath → release.db │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
│ DbContextFactory │
|
│ DbContextFactory │
|
||||||
│ 读取配置 → 选择 Provider → 创建 DbContext │
|
│ 读取配置 → 选择 Provider → 创建 WctMinerDbContext │
|
||||||
└─────────────────────────────────────────────────────────────┘
|
│ ReleaseDbContextFactory │
|
||||||
|
│ 读取配置 → 选择 Provider → 创建 ReleaseDbContext │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
│ WctMinerDbContext │
|
│ local.db release.db │
|
||||||
│ 统一的 DbModel 配置,适配两种数据库 │
|
│ ──────── ────────── │
|
||||||
└─────────────────────────────────────────────────────────────┘
|
│ tx_panel charging_parameter │
|
||||||
|
│ tx_hardware 独立实体,无外键关联 │
|
||||||
|
│ tx_software │
|
||||||
|
│ rx_type │
|
||||||
|
│ test_scenario │
|
||||||
|
│ qfod_record │
|
||||||
|
│ ploss_record │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
| 项目 | 状态 |
|
| 项目 | 状态 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| SQLite | ✅ 默认启用 |
|
| SQLite | ✅ 默认启用 |
|
||||||
| PostgreSQL | ✅ 架构支持,待配置 |
|
| PostgreSQL | ✅ 架构支持 |
|
||||||
| 切换方式 | 配置文件切换 |
|
| local.db | ✅ 解析库 |
|
||||||
|
| release.db | ✅ 发布库 |
|
||||||
|
| 双库隔离 | ✅ 无外键/导航属性 |
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
# 数据库迁移策略
|
# 数据库迁移策略
|
||||||
|
|
||||||
> 所属模块:[数据库切换支持](./概览.md)
|
> 所属模块:[数据库支持](./概览.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 迁移命令
|
## 1. 本地库迁移
|
||||||
|
|
||||||
### SQLite
|
### SQLite
|
||||||
|
|
||||||
```bash
|
```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
|
dotnet ef database update
|
||||||
@@ -20,42 +21,43 @@ dotnet ef database update
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 切换配置后添加迁移
|
# 切换配置后添加迁移
|
||||||
dotnet ef migrations add InitialCreate --output-dir Migrations/PostgreSQL -- --environment PostgreSQL
|
dotnet ef migrations add InitialCreate --output-dir Migrations/PostgreSQL \
|
||||||
|
--context WctMinerDbContext --project src/Gpulse.WCT.DataAnalyzer.Core \
|
||||||
# 应用迁移
|
-- --environment Production
|
||||||
dotnet ef database update -- --environment PostgreSQL
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 切换步骤
|
## 2. 发布库迁移
|
||||||
|
|
||||||
### 从 SQLite 切换到 PostgreSQL
|
发布库的迁移需要使用 `ReleaseDbContext`:
|
||||||
|
|
||||||
| 步骤 | 操作 | 说明 |
|
```bash
|
||||||
|------|------|------|
|
# 添加迁移
|
||||||
| 1 | 修改配置 | `appsettings.json` 中 `Database:Type` 改为 `postgresql` |
|
dotnet ef migrations add InitialCreate --output-dir Migrations/Release \
|
||||||
| 2 | 设置连接 | 配置 `Host`, `Port`, `Name`, `User`, `Password` |
|
--context ReleaseDbContext --project src/Gpulse.WCT.DataAnalyzer.Core
|
||||||
| 3 | 创建数据库 | `CREATE DATABASE wctminer;` |
|
|
||||||
| 4 | 应用迁移 | `dotnet ef database update` |
|
# 应用迁移
|
||||||
| 5 | 数据迁移 | 使用工具迁移 SQLite 数据到 PostgreSQL |
|
dotnet ef database update --context ReleaseDbContext
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 配置文件示例
|
## 3. 配置文件示例
|
||||||
|
|
||||||
### SQLite (默认)
|
### SQLite(默认双库)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"Database": {
|
"Database": {
|
||||||
"Type": "sqlite",
|
"Type": "sqlite",
|
||||||
"Path": "./data/database/wctminer.db"
|
"LocalPath": "./data/database/local.db",
|
||||||
|
"ReleasePath": "./data/database/release.db"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### PostgreSQL
|
### PostgreSQL(双库独立)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -63,9 +65,8 @@ dotnet ef database update -- --environment PostgreSQL
|
|||||||
"Type": "postgresql",
|
"Type": "postgresql",
|
||||||
"Host": "localhost",
|
"Host": "localhost",
|
||||||
"Port": 5432,
|
"Port": 5432,
|
||||||
"Name": "wctminer",
|
"LocalPath": "Server=localhost;Database=wct_local",
|
||||||
"User": "wctminer_user",
|
"ReleasePath": "Server=db-server;Database=wct_charging_parameters"
|
||||||
"Password": "${DB_PASSWORD}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -77,5 +78,11 @@ dotnet ef database update -- --environment PostgreSQL
|
|||||||
推荐使用 [pgloader](https://github.com/dimitri/pgloader) 迁移 SQLite 到 PostgreSQL:
|
推荐使用 [pgloader](https://github.com/dimitri/pgloader) 迁移 SQLite 到 PostgreSQL:
|
||||||
|
|
||||||
```bash
|
```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
|
```csharp
|
||||||
// Parsers/FileNameParser.cs
|
// Parsers/FileNameParser.cs
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using WCTDataMiner.Services;
|
using Gpulse.WCT.DataAnalyzer.Services;
|
||||||
|
|
||||||
namespace WCTDataMiner.Parsers;
|
namespace Gpulse.WCT.DataAnalyzer.Parsers;
|
||||||
|
|
||||||
public class FileNameParser
|
public class FileNameParser
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -102,9 +102,9 @@ private static readonly Regex FodPattern = new(
|
|||||||
```csharp
|
```csharp
|
||||||
// Parsers/PlossParser.cs
|
// Parsers/PlossParser.cs
|
||||||
using System.Text.RegularExpressions;
|
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>
|
public class PlossParser : IParser<PlossRecord>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,9 +53,9 @@ private static readonly Regex QfodPattern = new(
|
|||||||
```csharp
|
```csharp
|
||||||
// Parsers/QfodParser.cs
|
// Parsers/QfodParser.cs
|
||||||
using System.Text.RegularExpressions;
|
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>
|
public class QfodParser : IParser<QfodRecord>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Parsers/IParser.cs
|
// Parsers/IParser.cs
|
||||||
namespace WCTDataMiner.Parsers;
|
namespace Gpulse.WCT.DataAnalyzer.Parsers;
|
||||||
|
|
||||||
public interface IParser<TRecord>
|
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
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant CLI as CLI Entry
|
participant CLI as CLI
|
||||||
participant PS as ParseService
|
participant PS as ParseService
|
||||||
participant SS as ScenarioService
|
participant SS as ScenarioService
|
||||||
participant FNP as FileNameParser
|
participant FNP as FileNameParser
|
||||||
participant QP as QfodParser
|
participant QP as QfodParser
|
||||||
participant PP as PlossParser
|
participant PP as PlossParser
|
||||||
participant DB as Database
|
participant DB as local.db
|
||||||
participant Log as Serilog
|
|
||||||
|
|
||||||
CLI->>PS: 扫描日志目录
|
CLI->>PS: ParseFileAsync(filePath)
|
||||||
PS-->>CLI: 返回文件列表
|
PS->>FNP: 解析文件名
|
||||||
|
FNP-->>PS: ScenarioInfo
|
||||||
|
PS->>SS: GetOrCreateScenarioAsync
|
||||||
|
SS->>DB: 维度 upsert + 场景 upsert
|
||||||
|
SS-->>PS: TestScenario
|
||||||
|
PS->>PS: 逐行读取文件
|
||||||
|
|
||||||
loop 每个日志文件
|
loop 每行
|
||||||
CLI->>PS: ParseFileAsync(filePath)
|
alt Qfod 行
|
||||||
|
PS->>QP: Parse(line)
|
||||||
PS->>FNP: 解析文件名
|
QP-->>PS: QfodRecord
|
||||||
FNP-->>PS: 场景信息(面板、版本、RX类型等)
|
else Ploss 行
|
||||||
|
PS->>PP: Parse(line)
|
||||||
PS->>SS: GetOrCreateScenarioAsync(场景信息)
|
PP-->>PS: PlossRecord
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
PS->>DB: BATCH INSERT QfodRecords
|
|
||||||
PS->>DB: BATCH INSERT PlossRecords
|
|
||||||
PS->>DB: UPDATE scenario (qfod_count, ploss_count)
|
|
||||||
|
|
||||||
PS-->>CLI: 返回 ParseReport
|
|
||||||
end
|
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
|
```mermaid
|
||||||
// Services/ParseService.cs
|
sequenceDiagram
|
||||||
using Microsoft.EntityFrameworkCore;
|
participant CLI as CLI
|
||||||
using Serilog;
|
participant ES as ExportService
|
||||||
using WCTDataMiner.Data;
|
participant RDB as release.db
|
||||||
using WCTDataMiner.Models;
|
participant FS as Filesystem
|
||||||
using WCTDataMiner.Parsers;
|
|
||||||
|
|
||||||
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(
|
- 固定文件名: `WCT-ChargingParameterDatabase.csv`
|
||||||
WctMinerDbContext context,
|
- 固定 17 列 UTF-8 BOM
|
||||||
ScenarioService scenarioService,
|
- RFC 4180 转义
|
||||||
QfodParser qfodParser,
|
- 数字使用 invariant culture
|
||||||
PlossParser plossParser,
|
|
||||||
FileNameParser fileNameParser,
|
|
||||||
IConfiguration config)
|
|
||||||
{
|
|
||||||
_context = context;
|
|
||||||
_scenarioService = scenarioService;
|
|
||||||
_qfodParser = qfodParser;
|
|
||||||
_plossParser = plossParser;
|
|
||||||
_fileNameParser = fileNameParser;
|
|
||||||
_batchSize = config.GetValue("Parser:BatchSize", 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ParseReport> ParseFileAsync(string filePath)
|
---
|
||||||
{
|
|
||||||
var fileInfo = new FileInfo(filePath);
|
|
||||||
|
|
||||||
// 解析文件名获取场景信息
|
## 5. 数据库隔离
|
||||||
var scenarioInfo = _fileNameParser.Parse(fileInfo.Name);
|
|
||||||
if (scenarioInfo == null)
|
|
||||||
{
|
|
||||||
Log.Error("无法解析文件名: {FileName}", fileInfo.Name);
|
|
||||||
return new ParseReport(fileInfo.Name, 0, 0, 1, "文件名格式不匹配");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取或创建测试场景
|
```text
|
||||||
var scenario = await _scenarioService.GetOrCreateScenarioAsync(scenarioInfo);
|
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++)
|
- 代码中不存在 local entity 到 release entity 的导航属性
|
||||||
{
|
- 数据库中没有跨库外键
|
||||||
var line = lines[i];
|
- export 不读 local.db,aggregate 不写 local.db
|
||||||
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);
|
|
||||||
```
|
|
||||||
@@ -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) | 模块总览、分层架构、依赖关系 |
|
| [模块划分](./模块划分.md) | 分层架构、目录结构、模块职责 |
|
||||||
| [安全架构](./安全架构.md) | 认证方案、数据安全 |
|
| [数据处理流程](./数据处理流程.md) | 解析→汇总→导出完整流程 |
|
||||||
| [解析器设计](./parser/概览.md) | Qfod/Ploss 解析器详细设计 |
|
| [CLI 命令设计](./CLI命令设计.md) | 命令行接口 |
|
||||||
| [数据处理流程](./数据处理流程.md) | 解析流程、批量写入、错误处理 |
|
| [配置管理](./配置管理.md) | 配置结构与双数据库配置 |
|
||||||
| [配置管理](./配置管理.md) | 配置文件、配置类定义 |
|
| [数据库支持](./database/概览.md) | 本地/发布双数据库架构 |
|
||||||
| [CLI 命令设计](./CLI命令设计.md) | 命令行接口定义 |
|
| [安全架构](./安全架构.md) | 路径校验、安全策略 |
|
||||||
| [数据库切换支持](./database/概览.md) | SQLite/PostgreSQL 切换方案 |
|
| [解析器设计](./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 |
|
| 技术栈 | .NET 8.0 + C# 12.0 + EF Core 8.0 |
|
||||||
| 数据库 | SQLite(当前)/ PostgreSQL(后续),配置切换 |
|
| 数据库 | local.db / release.db 两个独立 SQLite 数据库 |
|
||||||
| 解析模式 | 批处理,正则表达式匹配 |
|
| 本地/发布隔离 | 无外键、无导航属性、无数据库级关联 |
|
||||||
| ORM 模式 | Code First,支持迁移自动化 |
|
| 解析模式 | 批处理,正则表达式 |
|
||||||
| CLI 框架 | System.CommandLine,命令模式 |
|
| CLI | System.CommandLine,命令模式 |
|
||||||
|
| 汇总规则 | 按业务键分组取平均值,缺失字段允许为空 |
|
||||||
|
|
||||||
---
|
## 目录结构
|
||||||
|
|
||||||
## 假设/歧义标注
|
```text
|
||||||
|
src/
|
||||||
| 标注 | 内容 |
|
├── Gpulse.WCT.DataAnalyzer/ # CLI 入口
|
||||||
|------|------|
|
│ ├── Commands/ # 命令实现
|
||||||
| [A] | 使用 .NET 8.0 LTS 版本 |
|
│ │ ├── ParseCommand.cs
|
||||||
| [A] | 初期使用 SQLite,架构支持切换 PostgreSQL |
|
│ │ ├── 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 │
|
| CLI | `Gpulse.WCT.DataAnalyzer/Commands/` | 接收参数、流程调度、结果输出 | Application |
|
||||||
│ (Program.cs / System.CommandLine) │
|
| Application | `Core/Application/` | 业务流程编排 | Domain, Infrastructure |
|
||||||
├─────────────────────────────────────────────────────────────┤
|
| Domain | `Core/Domain/Models/` | 实体定义,无行为 | 无 |
|
||||||
│ Service Layer │
|
| Infrastructure | `Core/Infrastructure/` | 数据持久化、配置、安全 | Domain |
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ ParseService │ ScenarioService │ DimensionService │ ThresholdCalc │
|
**依赖规则:**
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ Parser Layer │
|
- 上层可依赖下层,下层不可依赖上层
|
||||||
├─────────────────────────────────────────────────────────────┤
|
- Application 依 Domain 和 Infrastructure
|
||||||
│ QfodParser │ PlossParser │ FileNameParser │
|
- Infrastructure 依 Domain
|
||||||
├─────────────────────────────────────────────────────────────┤
|
- Domain 不依赖任何其他层
|
||||||
│ Data Layer │
|
- 解析器之间不可互相调用
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ Models │ WctMinerDbContext │ IDbContextFactory │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ Infrastructure │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ Configuration │ Serilog │ AppSettings │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 模块职责
|
## 2. Application 模块职责
|
||||||
|
|
||||||
| 模块名 | 职责 | 依赖模块 |
|
### 2.1 Parsing — 解析
|
||||||
|--------|------|----------|
|
|
||||||
| CLI Entry | 接收命令行参数、调度解析流程 | ParseService, Infrastructure |
|
| 类 | 职责 |
|
||||||
| ParseService | 协调解析流程、批量写入、场景关联 | Parser Layer, ScenarioService, DbContext |
|
|----|------|
|
||||||
| ScenarioService | 创建/查询测试场景,解析文件名获取场景信息 | DimensionService, DbContext |
|
| `FileNameParser` | 从文件名提取场景信息(面板、硬件、软件、RX类型、日期、序号) |
|
||||||
| DimensionService | 维度表 CRUD(获取或创建维度记录) | DbContext |
|
| `QfodParser` | 解析 Qfod 格式日志行 → `QfodRecord` |
|
||||||
| FileNameParser | 解析文件名提取场景信息(面板、版本、RX类型等) | 无 |
|
| `PlossParser` | 解析 Ploss 单行/两行格式日志 → `PlossRecord` |
|
||||||
| LogFileReader | 读取日志文件、行过滤、文件遍历 | 无 |
|
|
||||||
| QfodParser | 解析 Qfod 格式日志,返回 QfodRecord | 无 |
|
### 2.2 LocalIngestion — 本地解析
|
||||||
| PlossParser | 解析 Ploss 格式日志,返回 PlossRecord | 无 |
|
|
||||||
| Models | 定义数据实体结构(维度表 + 事实表) | 无 |
|
| 类 | 职责 |
|
||||||
| WctMinerDbContext | EF Core 数据库上下文 | Models, Configurations |
|
|----|------|
|
||||||
| Configuration | 加载配置文件 | 无 |
|
| `ParseService` | 协调解析流程:读取文件 → 解析 → 批量写入 local.db |
|
||||||
| Serilog | 结构化日志输出(含解析错误日志) | 无 |
|
| `ScenarioService` | 创建/查询测试场景,并发安全 upsert |
|
||||||
| ThresholdCalculator | 根据 RxPower 计算动态阈值 | 无 |
|
| `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 |
|
| `TxPanel` | tx_panel | TX 面板类型维度 |
|
||||||
| Service | 协调解析流程、场景管理、维度管理、批量写入 | Parser, Data, Infrastructure |
|
| `TxHardware` | tx_hardware | TX 硬件版本维度 |
|
||||||
| Parser | 日志解析、文件名解析、数据提取 | 无 |
|
| `TxSoftware` | tx_software | TX 软件版本维度 |
|
||||||
| Data | 数据持久化、DbContext 管理、实体配置 | Infrastructure |
|
| `RxType` | rx_type | RX 类型维度 |
|
||||||
| Infrastructure | 配置、日志 | 无 |
|
| `TestScenario` | test_scenario | 测试场景(4 个维度外键 + 日期 + 序号) |
|
||||||
|
| `QfodRecord` | qfod_record | Qfod 检测数据 |
|
||||||
|
| `PlossRecord` | ploss_record | Ploss FOD 数据 |
|
||||||
|
|
||||||
### 3.2 依赖规则
|
关系:维度表 1:N TestScenario 1:N QfodRecord/PlossRecord(星型模型)
|
||||||
|
|
||||||
- 上层可调用下层
|
### 3.2 发布实体(release.db)
|
||||||
- 下层不可调用上层
|
|
||||||
- Parser 之间不可互相调用(独立解析器)
|
| 实体 | 表 | 唯一键 |
|
||||||
- Service 层协调 Parser 和 Data 层
|
|------|----|--------|
|
||||||
- 解析错误通过 Serilog 记录,不存入业务数据库
|
| `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
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
CLI[CLI Entry] --> PS[ParseService]
|
CLI[CLI Commands] --> Parsing[Parsing]
|
||||||
CLI --> Serilog
|
CLI --> LocalIngestion[LocalIngestion]
|
||||||
|
CLI --> Publishing[Publishing]
|
||||||
|
CLI --> Exporting[Exporting]
|
||||||
|
CLI --> Admin[Administration]
|
||||||
|
|
||||||
PS --> SS[ScenarioService]
|
LocalIngestion --> Parsing
|
||||||
PS --> QP[QfodParser]
|
LocalIngestion --> LocalData[LocalData]
|
||||||
PS --> PP[PlossParser]
|
Publishing --> LocalData
|
||||||
PS --> DBM[WctMinerDbContext]
|
Publishing --> ReleaseData[ReleaseData]
|
||||||
PS --> Serilog
|
Exporting --> LocalData
|
||||||
|
Exporting --> ReleaseData
|
||||||
|
Admin --> LocalData
|
||||||
|
|
||||||
SS --> DS[DimensionService]
|
LocalData --> Domain[Domain Models]
|
||||||
SS --> FNP[FileNameParser]
|
ReleaseData --> Domain
|
||||||
SS --> DBM
|
Parsing --> Domain
|
||||||
|
|
||||||
DS --> DBM
|
subgraph Application
|
||||||
|
Parsing
|
||||||
QP --> Serilog
|
LocalIngestion
|
||||||
PP --> Serilog
|
Publishing
|
||||||
FNP --> Serilog
|
Exporting
|
||||||
|
Admin
|
||||||
DBM --> Models[Models]
|
|
||||||
DBM --> Configs[Configurations]
|
|
||||||
|
|
||||||
subgraph Parser Layer
|
|
||||||
QP
|
|
||||||
PP
|
|
||||||
FNP
|
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Data Layer
|
subgraph Domain
|
||||||
DBM
|
Domain
|
||||||
Models
|
|
||||||
Configs
|
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Infrastructure
|
subgraph Infrastructure
|
||||||
Serilog
|
LocalData
|
||||||
|
ReleaseData
|
||||||
|
Configuration
|
||||||
|
Security
|
||||||
|
Extensions
|
||||||
end
|
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
|
```json
|
||||||
// Configuration/appsettings.json
|
|
||||||
{
|
{
|
||||||
"Database": {
|
"Database": {
|
||||||
"Type": "sqlite",
|
"Type": "sqlite",
|
||||||
"Path": "./data/database/wctminer.db"
|
"LocalPath": "./data/database/local.db",
|
||||||
|
"ReleasePath": "./data/database/release.db"
|
||||||
},
|
},
|
||||||
"Parser": {
|
"Parser": {
|
||||||
"BatchSize": 1000
|
"BatchSize": 1000
|
||||||
},
|
},
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"WCTDataMiner": "Debug"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Paths": {
|
"Paths": {
|
||||||
"LogDir": "./data/logs",
|
"LogDir": "./data/logs",
|
||||||
"OutputDir": "./data/output"
|
"OutputDir": "./data/output"
|
||||||
|
},
|
||||||
|
"Aggregation": {
|
||||||
|
"TxPanelMappings": {
|
||||||
|
"single-mold": "奇瑞",
|
||||||
|
"dual-rapid": "智己"
|
||||||
|
},
|
||||||
|
"CarModelMappings": {
|
||||||
|
"L6": "L6",
|
||||||
|
"CM3": "CM3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -34,8 +38,8 @@
|
|||||||
## 2. 配置类定义
|
## 2. 配置类定义
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Configuration/AppSettings.cs
|
// Infrastructure/Configuration/AppSettings.cs
|
||||||
namespace WCTDataMiner.Configuration;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Configuration;
|
||||||
|
|
||||||
public class AppSettings
|
public class AppSettings
|
||||||
{
|
{
|
||||||
@@ -47,8 +51,9 @@ public class AppSettings
|
|||||||
public class DatabaseSettings
|
public class DatabaseSettings
|
||||||
{
|
{
|
||||||
public string Type { get; set; } = "sqlite";
|
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 配置(可选)
|
// PostgreSQL 配置(可选)
|
||||||
public string? Host { get; set; }
|
public string? Host { get; set; }
|
||||||
public int Port { get; set; } = 5432;
|
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);
|
"Database": {
|
||||||
config.AddJsonFile($"appsettings.{environment}.json", optional: true);
|
"Type": "postgresql",
|
||||||
config.AddEnvironmentVariables();
|
"Host": "localhost",
|
||||||
config.AddCommandLine(args);
|
"Port": 5432,
|
||||||
});
|
"Name": "wct_local",
|
||||||
|
"User": "wct_user",
|
||||||
|
"Password": "...",
|
||||||
|
"ReleaseHost": "db-server",
|
||||||
|
"ReleaseName": "wct_charging_parameters",
|
||||||
|
"ReleaseUser": "wct_release",
|
||||||
|
"ReleasePassword": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
builder.ConfigureServices((context, services) =>
|
Release 数据库的 PostgreSQL 配置前缀为 `Release*`,未设置时回退到 local 的对应配置。
|
||||||
{
|
|
||||||
services.Configure<AppSettings>(context.Configuration.GetSection(""));
|
---
|
||||||
|
|
||||||
// 注册数据库服务
|
## 4. 聚合配置
|
||||||
services.AddDatabaseServices(context.Configuration);
|
|
||||||
|
| 配置键 | 说明 |
|
||||||
// 注册其他服务
|
|--------|------|
|
||||||
services.AddScoped<QfodParser>();
|
| `Aggregation:TxPanelMappings` | TxPanel 名称 → 车厂名称的映射表 |
|
||||||
services.AddScoped<PlossParser>();
|
| `Aggregation:CarModelMappings` | TxHardware 版本 → 车型名称的映射表 |
|
||||||
services.AddScoped<ParseService>();
|
|
||||||
services.AddScoped<ErrorHandlingService>();
|
未配置映射时,直接使用 TxPanel.Name 和 TxHardware.Version 作为最终值。手机品牌/型号从 RxType.Name 自动拆分(分隔符 `/_:`)。
|
||||||
});
|
|
||||||
```
|
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Models/PlossRecord.cs
|
// Models/PlossRecord.cs
|
||||||
namespace WCTDataMiner.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Models;
|
||||||
|
|
||||||
public class PlossRecord
|
public class PlossRecord
|
||||||
{
|
{
|
||||||
@@ -117,9 +117,9 @@ public class PlossRecord
|
|||||||
// Data/Configurations/PlossRecordConfiguration.cs
|
// Data/Configurations/PlossRecordConfiguration.cs
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
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>
|
public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Models/QfodRecord.cs
|
// Models/QfodRecord.cs
|
||||||
namespace WCTDataMiner.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Models;
|
||||||
|
|
||||||
public class QfodRecord
|
public class QfodRecord
|
||||||
{
|
{
|
||||||
@@ -93,9 +93,9 @@ public class QfodRecord
|
|||||||
// Data/Configurations/QfodRecordConfiguration.cs
|
// Data/Configurations/QfodRecordConfiguration.cs
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
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>
|
public class QfodRecordConfiguration : IEntityTypeConfiguration<QfodRecord>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Models/RxType.cs
|
// Models/RxType.cs
|
||||||
namespace WCTDataMiner.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Models;
|
||||||
|
|
||||||
public class RxType
|
public class RxType
|
||||||
{
|
{
|
||||||
@@ -58,9 +58,9 @@ public class RxType
|
|||||||
// Data/Configurations/RxTypeConfiguration.cs
|
// Data/Configurations/RxTypeConfiguration.cs
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
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>
|
public class RxTypeConfiguration : IEntityTypeConfiguration<RxType>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Models/TxHardware.cs
|
// Models/TxHardware.cs
|
||||||
namespace WCTDataMiner.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Models;
|
||||||
|
|
||||||
public class TxHardware
|
public class TxHardware
|
||||||
{
|
{
|
||||||
@@ -49,9 +49,9 @@ public class TxHardware
|
|||||||
// Data/Configurations/TxHardwareConfiguration.cs
|
// Data/Configurations/TxHardwareConfiguration.cs
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
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>
|
public class TxHardwareConfiguration : IEntityTypeConfiguration<TxHardware>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Models/TxPanel.cs
|
// Models/TxPanel.cs
|
||||||
namespace WCTDataMiner.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Models;
|
||||||
|
|
||||||
public class TxPanel
|
public class TxPanel
|
||||||
{
|
{
|
||||||
@@ -63,9 +63,9 @@ public class TxPanel
|
|||||||
// Data/Configurations/TxPanelConfiguration.cs
|
// Data/Configurations/TxPanelConfiguration.cs
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
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>
|
public class TxPanelConfiguration : IEntityTypeConfiguration<TxPanel>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Models/TxSoftware.cs
|
// Models/TxSoftware.cs
|
||||||
namespace WCTDataMiner.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Models;
|
||||||
|
|
||||||
public class TxSoftware
|
public class TxSoftware
|
||||||
{
|
{
|
||||||
@@ -49,9 +49,9 @@ public class TxSoftware
|
|||||||
// Data/Configurations/TxSoftwareConfiguration.cs
|
// Data/Configurations/TxSoftwareConfiguration.cs
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
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>
|
public class TxSoftwareConfiguration : IEntityTypeConfiguration<TxSoftware>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Models/TestScenario.cs
|
// Models/TestScenario.cs
|
||||||
namespace WCTDataMiner.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Models;
|
||||||
|
|
||||||
public class TestScenario
|
public class TestScenario
|
||||||
{
|
{
|
||||||
@@ -110,9 +110,9 @@ public class TestScenario
|
|||||||
// Data/Configurations/TestScenarioConfiguration.cs
|
// Data/Configurations/TestScenarioConfiguration.cs
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
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>
|
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/TxHardware维度.md) | TX硬件版本维度 |
|
||||||
| [TX软件版本维度](./entities/TxSoftware维度.md) | TX软件版本维度 |
|
| [TX软件版本维度](./entities/TxSoftware维度.md) | TX软件版本维度 |
|
||||||
| [RX类型维度](./entities/RxType维度.md) | RX类型维度 |
|
| [RX类型维度](./entities/RxType维度.md) | RX类型维度 |
|
||||||
|
|
||||||
### 事实表
|
|
||||||
|
|
||||||
| 文档 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| [测试场景实体](./entities/测试场景.md) | 测试场景(外键组合) |
|
| [测试场景实体](./entities/测试场景.md) | 测试场景(外键组合) |
|
||||||
| [Qfod 记录实体](./entities/Qfod记录.md) | Qfod 检测数据 |
|
| [Qfod 记录实体](./entities/Qfod记录.md) | Qfod 检测数据 |
|
||||||
| [Ploss 记录实体](./entities/Ploss记录.md) | Ploss FOD 数据 |
|
| [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
|
```mermaid
|
||||||
erDiagram
|
erDiagram
|
||||||
|
%% 本地库
|
||||||
tx_panel ||--o{ test_scenario : "引用"
|
tx_panel ||--o{ test_scenario : "引用"
|
||||||
tx_hardware ||--o{ test_scenario : "引用"
|
tx_hardware ||--o{ test_scenario : "引用"
|
||||||
tx_software ||--o{ test_scenario : "引用"
|
tx_software ||--o{ test_scenario : "引用"
|
||||||
rx_type ||--o{ test_scenario : "引用"
|
rx_type ||--o{ test_scenario : "引用"
|
||||||
|
|
||||||
test_scenario ||--o{ qfod_record : "包含"
|
test_scenario ||--o{ qfod_record : "包含"
|
||||||
test_scenario ||--o{ ploss_record : "包含"
|
test_scenario ||--o{ ploss_record : "包含"
|
||||||
|
|
||||||
tx_panel {
|
%% 发布库(独立)
|
||||||
uuid id PK "主键"
|
charging_parameter {
|
||||||
varchar name UK "面板类型"
|
uuid id PK
|
||||||
}
|
varchar car_factory UK
|
||||||
|
varchar car_model UK
|
||||||
tx_hardware {
|
varchar phone_brand UK
|
||||||
uuid id PK "主键"
|
varchar phone_model UK
|
||||||
varchar version UK "硬件版本"
|
double power_350mw
|
||||||
}
|
double power_500mw
|
||||||
|
double power_750mw
|
||||||
tx_software {
|
double power_1000mw
|
||||||
uuid id PK "主键"
|
double power_1250mw
|
||||||
varchar version UK "软件版本"
|
double power_1500mw
|
||||||
}
|
double power_1750mw
|
||||||
|
double power_2000mw
|
||||||
rx_type {
|
double power_2250mw
|
||||||
uuid id PK "主键"
|
double q_value
|
||||||
varchar name UK "RX类型"
|
double q_base_value
|
||||||
}
|
double pq_coefficient
|
||||||
|
double resonance_frequency
|
||||||
test_scenario {
|
|
||||||
uuid id PK "主键"
|
|
||||||
uuid tx_panel_id FK "面板外键"
|
|
||||||
uuid tx_hardware_id FK "硬件外键"
|
|
||||||
uuid tx_software_id FK "软件外键"
|
|
||||||
uuid rx_type_id FK "RX外键"
|
|
||||||
varchar test_purpose "测试目的"
|
|
||||||
date test_date "测试日期"
|
|
||||||
int test_sequence "测试序号"
|
|
||||||
int qfod_count "Qfod记录数"
|
|
||||||
int ploss_count "Ploss记录数"
|
|
||||||
}
|
|
||||||
|
|
||||||
qfod_record {
|
|
||||||
uuid id PK "主键"
|
|
||||||
uuid scenario_id FK "场景外键"
|
|
||||||
tinyint charger_index "充电器索引"
|
|
||||||
tinyint coil_index "线圈索引"
|
|
||||||
int delta_q "Q值变化量"
|
|
||||||
float current_q "当前Q值"
|
|
||||||
float raw_q "原始Q值"
|
|
||||||
tinyint fod_type "异物类型"
|
|
||||||
}
|
|
||||||
|
|
||||||
ploss_record {
|
|
||||||
uuid id PK "主键"
|
|
||||||
uuid scenario_id FK "场景外键"
|
|
||||||
tinyint rx_type "接收端类型"
|
|
||||||
smallint rx_power "接收端功率"
|
|
||||||
smallint tx_power "发射端功率"
|
|
||||||
int ploss "计算损耗值"
|
|
||||||
int threshold "阈值"
|
|
||||||
smallint fod_result "FOD判定结果"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
框外是本地库,框内是发布库——两库之间没有外键或导航属性。
|
||||||
|
|
||||||
## 设计说明
|
## 设计说明
|
||||||
|
|
||||||
### 星型模型架构
|
### 本地库:星型模型
|
||||||
|
|
||||||
本设计采用星型模型(Star Schema):
|
- 维度表: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` 直接存储在场景表中,不拆分维度表,原因:
|
- 代码中两个实体的命名空间不同(Domain.Models)
|
||||||
- 测试目的描述较自由,不适合标准化为维度
|
- 两个 DbContext 不共享 DbSet
|
||||||
- 便于用户灵活输入
|
- 发布库不从本地库继承实体配置
|
||||||
|
- aggregate 通过纯值映射,不依赖导航属性
|
||||||
### 场景唯一性
|
|
||||||
|
|
||||||
场景表设置唯一约束,相同属性组合不重复创建:
|
|
||||||
|
|
||||||
```
|
|
||||||
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 |
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using WCTDataMiner.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using WCTDataMiner.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace WCTDataMiner.Core.Services;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 数据清理服务 - 支持软删除
|
/// 数据清理服务 - 支持软删除
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using WCTDataMiner.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using WCTDataMiner.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace WCTDataMiner.Core.Services;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 统计查询服务 - 使用数据库端聚合优化内存效率
|
/// 统计查询服务 - 使用数据库端聚合优化内存效率
|
||||||
@@ -3,11 +3,11 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using WCTDataMiner.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using WCTDataMiner.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
using WCTDataMiner.Core.Security;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
|
||||||
|
|
||||||
namespace WCTDataMiner.Core.Services;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 数据导出服务 - 支持安全的路径验证和 RFC 4180 标准 CSV 转义
|
/// 数据导出服务 - 支持安全的路径验证和 RFC 4180 标准 CSV 转义
|
||||||
@@ -15,12 +15,14 @@ namespace WCTDataMiner.Core.Services;
|
|||||||
public class ExportService
|
public class ExportService
|
||||||
{
|
{
|
||||||
private readonly WctMinerDbContext _context;
|
private readonly WctMinerDbContext _context;
|
||||||
|
private readonly ReleaseDbContext _releaseContext;
|
||||||
private readonly ILogger<ExportService> _logger;
|
private readonly ILogger<ExportService> _logger;
|
||||||
private readonly PathValidator _pathValidator;
|
private readonly PathValidator _pathValidator;
|
||||||
|
|
||||||
public ExportService(WctMinerDbContext context, ILogger<ExportService> logger)
|
public ExportService(WctMinerDbContext context, ReleaseDbContext releaseContext, ILogger<ExportService> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_releaseContext = releaseContext;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_pathValidator = new PathValidator(AppDomain.CurrentDomain.BaseDirectory);
|
_pathValidator = new PathValidator(AppDomain.CurrentDomain.BaseDirectory);
|
||||||
}
|
}
|
||||||
@@ -305,7 +307,49 @@ public class ExportService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
private static string EscapeCsvField(object? value)
|
private static string EscapeCsvField(object? value)
|
||||||
{
|
{
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using WCTDataMiner.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using WCTDataMiner.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace WCTDataMiner.Core.Services;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 维度表管理服务 - 负责维度表的CRUD操作(并发安全版本)
|
/// 维度表管理服务 - 负责维度表的CRUD操作(并发安全版本)
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using WCTDataMiner.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using WCTDataMiner.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
using WCTDataMiner.Core.Parsers;
|
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||||
|
|
||||||
namespace WCTDataMiner.Core.Services;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析流程协调服务 - 协调文件名解析、行解析、批量写入
|
/// 解析流程协调服务 - 协调文件名解析、行解析、批量写入
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using WCTDataMiner.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using WCTDataMiner.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
using WCTDataMiner.Core.Parsers;
|
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||||
|
|
||||||
namespace WCTDataMiner.Core.Services;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 篮选条件常量
|
/// 篮选条件常量
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace WCTDataMiner.Core.Parsers;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 文件名解析器 - 从文件名提取场景信息
|
/// 文件名解析器 - 从文件名提取场景信息
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace WCTDataMiner.Core.Parsers;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 通用解析器接口
|
/// 通用解析器接口
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.RegularExpressions;
|
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>
|
/// <summary>
|
||||||
/// Ploss FOD日志解析器 - 支持两行格式
|
/// Ploss FOD日志解析器 - 支持两行格式
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.RegularExpressions;
|
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>
|
/// <summary>
|
||||||
/// Qfod日志解析器
|
/// 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>
|
/// <summary>
|
||||||
/// 阈值计算器 - 根据接收功率查表获取动态阈值
|
/// 阈值计算器 - 根据接收功率查表获取动态阈值
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace WCTDataMiner.Core.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ploss记录实体 - 存储Ploss FOD格式解析数据
|
/// Ploss记录实体 - 存储Ploss FOD格式解析数据
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace WCTDataMiner.Core.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Qfod记录实体 - 存储Qfod格式解析数据
|
/// Qfod记录实体 - 存储Qfod格式解析数据
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace WCTDataMiner.Core.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// RX类型维度表
|
/// RX类型维度表
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace WCTDataMiner.Core.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 测试场景实体 - 通过外键关联各维度表
|
/// 测试场景实体 - 通过外键关联各维度表
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace WCTDataMiner.Core.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// TX硬件版本维度表
|
/// TX硬件版本维度表
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace WCTDataMiner.Core.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// TX面板类型维度表
|
/// TX面板类型维度表
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace WCTDataMiner.Core.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// TX软件版本维度表
|
/// 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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
<AssemblyName>Gpulse.WCT.DataAnalyzer.Core</AssemblyName>
|
||||||
|
<RootNamespace>Gpulse.WCT.DataAnalyzer.Core</RootNamespace>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace WCTDataMiner.Core.Configuration;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Configuration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 应用配置
|
/// 应用配置
|
||||||
@@ -16,7 +16,9 @@ public class AppSettings
|
|||||||
public class DatabaseSettings
|
public class DatabaseSettings
|
||||||
{
|
{
|
||||||
public string Type { get; set; } = "sqlite";
|
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 配置(可选)
|
// PostgreSQL 配置(可选)
|
||||||
public string? Host { get; set; }
|
public string? Host { get; set; }
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using WCTDataMiner.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using WCTDataMiner.Core.Parsers;
|
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||||
using WCTDataMiner.Core.Services;
|
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
namespace WCTDataMiner.Core.Extensions;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 服务注册扩展
|
/// 服务注册扩展
|
||||||
@@ -28,6 +28,15 @@ public static class ServiceCollectionExtensions
|
|||||||
return factory.CreateDbContext();
|
return factory.CreateDbContext();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddScoped<IReleaseDbContextFactory, ReleaseDbContextFactory>();
|
||||||
|
|
||||||
|
// 注册正式发布数据库上下文(与本地解析数据库完全独立)
|
||||||
|
services.AddScoped<ReleaseDbContext>(sp =>
|
||||||
|
{
|
||||||
|
var factory = sp.GetRequiredService<IReleaseDbContextFactory>();
|
||||||
|
return factory.CreateDbContext();
|
||||||
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +57,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<StatsService>();
|
services.AddScoped<StatsService>();
|
||||||
services.AddScoped<ExportService>();
|
services.AddScoped<ExportService>();
|
||||||
services.AddScoped<CleanService>();
|
services.AddScoped<CleanService>();
|
||||||
|
services.AddScoped<AggregationService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
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>
|
public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
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>
|
public class QfodRecordConfiguration : IEntityTypeConfiguration<QfodRecord>
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
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>
|
public class RxTypeConfiguration : IEntityTypeConfiguration<RxType>
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
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>
|
public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
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>
|
public class TxHardwareConfiguration : IEntityTypeConfiguration<TxHardware>
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
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>
|
public class TxPanelConfiguration : IEntityTypeConfiguration<TxPanel>
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
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>
|
public class TxSoftwareConfiguration : IEntityTypeConfiguration<TxSoftware>
|
||||||
{
|
{
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace WCTDataMiner.Core.Data;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 数据库上下文工厂实现 - 支持SQLite和PostgreSQL切换
|
/// 数据库上下文工厂实现 - 支持SQLite和PostgreSQL切换
|
||||||
@@ -30,7 +30,9 @@ public class DbContextFactory : IDbContextFactory
|
|||||||
|
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
default:
|
default:
|
||||||
var dbPath = _configuration["Database:Path"] ?? "./data/database/wctminer.db";
|
var dbPath = _configuration["Database:LocalPath"]
|
||||||
|
?? _configuration["Database:Path"]
|
||||||
|
?? "./data/database/local.db";
|
||||||
EnsureDirectoryExists(dbPath);
|
EnsureDirectoryExists(dbPath);
|
||||||
optionsBuilder.UseSqlite($"Data Source={dbPath}");
|
optionsBuilder.UseSqlite($"Data Source={dbPath}");
|
||||||
break;
|
break;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace WCTDataMiner.Core.Data;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// <summary>
|
||||||
/// 初始数据种子
|
/// 初始数据种子
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
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>
|
/// <summary>
|
||||||
/// WCT数据采集数据库上下文
|
/// 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;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace WCTDataMiner.Core.Security;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 路径验证结果
|
/// 路径验证结果
|
||||||
@@ -36,7 +36,7 @@ public class PathValidator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 默认允许的输出目录
|
/// 默认允许的输出目录
|
||||||
/// </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)
|
public PathValidator(string applicationBasePath, IEnumerable<string>? allowedDirectories = null)
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace WCTDataMiner.Core.Security;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <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 System.CommandLine;
|
||||||
using WCTDataMiner.Core.Services;
|
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
namespace WCTDataMiner.Commands;
|
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 数据清理命令
|
/// 数据清理命令
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
using WCTDataMiner.Core.Services;
|
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
namespace WCTDataMiner.Commands;
|
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 数据导出命令
|
/// 数据导出命令
|
||||||
@@ -25,8 +25,8 @@ public class ExportCommand : Command
|
|||||||
|
|
||||||
var typeOption = new Option<string>(
|
var typeOption = new Option<string>(
|
||||||
"--type",
|
"--type",
|
||||||
() => "all",
|
() => "charging-parameters",
|
||||||
"Data type to export: qfod, ploss, or all"
|
"Data type: charging-parameters, qfod, ploss, or all"
|
||||||
);
|
);
|
||||||
|
|
||||||
AddOption(formatOption);
|
AddOption(formatOption);
|
||||||
@@ -44,6 +44,12 @@ public class ExportCommand : Command
|
|||||||
{
|
{
|
||||||
if (format == "csv")
|
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")
|
if (type is "qfod" or "all")
|
||||||
{
|
{
|
||||||
var path = await exportService.ExportQfodToCsvAsync(output);
|
var path = await exportService.ExportQfodToCsvAsync(output);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
using WCTDataMiner.Core.Services;
|
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
namespace WCTDataMiner.Commands;
|
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析日志文件命令
|
/// 解析日志文件命令
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
using WCTDataMiner.Core.Services;
|
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
namespace WCTDataMiner.Commands;
|
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 统计查询命令
|
/// 统计查询命令
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
<AssemblyName>Gpulse.WCT.DataAnalyzer</AssemblyName>
|
||||||
|
<RootNamespace>Gpulse.WCT.DataAnalyzer</RootNamespace>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
@@ -30,7 +32,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\WCTDataMiner.Core\WCTDataMiner.Core.csproj" />
|
<ProjectReference Include="..\Gpulse.WCT.DataAnalyzer.Core\Gpulse.WCT.DataAnalyzer.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -3,15 +3,15 @@ using Microsoft.Extensions.Configuration;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using WCTDataMiner.Commands;
|
using Gpulse.WCT.DataAnalyzer.Commands;
|
||||||
using WCTDataMiner.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using WCTDataMiner.Core.Extensions;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
|
||||||
using WCTDataMiner.Core.Services;
|
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
namespace WCTDataMiner;
|
namespace Gpulse.WCT.DataAnalyzer;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// WCTDataMiner - 无线充电FOD日志数据采集系统
|
/// Gpulse.WCT.DataAnalyzer - 无线充电FOD日志数据采集系统
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
@@ -19,7 +19,7 @@ public class Program
|
|||||||
{
|
{
|
||||||
// 1. 构建引导配置(用于 Serilog 初始化)
|
// 1. 构建引导配置(用于 Serilog 初始化)
|
||||||
var bootstrapConfig = new ConfigurationBuilder()
|
var bootstrapConfig = new ConfigurationBuilder()
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
.SetBasePath(AppContext.BaseDirectory)
|
||||||
.AddJsonFile("appsettings.json", optional: false)
|
.AddJsonFile("appsettings.json", optional: false)
|
||||||
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true)
|
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true)
|
||||||
.AddEnvironmentVariables()
|
.AddEnvironmentVariables()
|
||||||
@@ -59,6 +59,9 @@ public class Program
|
|||||||
var dbContext = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
|
||||||
await dbContext.Database.EnsureCreatedAsync();
|
await dbContext.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
var releaseContext = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>();
|
||||||
|
await releaseContext.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
// 种子数据
|
// 种子数据
|
||||||
SeedData.Initialize(dbContext);
|
SeedData.Initialize(dbContext);
|
||||||
}
|
}
|
||||||
@@ -67,9 +70,10 @@ public class Program
|
|||||||
using var commandScope = app.Services.CreateScope();
|
using var commandScope = app.Services.CreateScope();
|
||||||
var provider = commandScope.ServiceProvider;
|
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 ParseCommand(provider.GetRequiredService<ParseService>()));
|
||||||
|
rootCommand.AddCommand(new AggregateCommand(provider.GetRequiredService<AggregationService>()));
|
||||||
rootCommand.AddCommand(new StatsCommand(provider.GetRequiredService<StatsService>()));
|
rootCommand.AddCommand(new StatsCommand(provider.GetRequiredService<StatsService>()));
|
||||||
rootCommand.AddCommand(new ExportCommand(provider.GetRequiredService<ExportService>()));
|
rootCommand.AddCommand(new ExportCommand(provider.GetRequiredService<ExportService>()));
|
||||||
rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService<CleanService>()));
|
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