docs: 更新项目架构与数据模型文档

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scottxjw
2026-08-13 15:01:32 +08:00
parent a3100c73c5
commit 2b031995e1
9 changed files with 678 additions and 885 deletions

View File

@@ -6,18 +6,19 @@
## 1. 命令概览
| 命令 | 说明 |
|------|------|
| `parse` | 解析日志文件并存储到数据库 |
| `stats` | 查询统计信息 |
| `export` | 导出数据 |
| `clean` | 清理数据 |
| 命令 | 说明 | 数据流 |
|------|------|--------|
| `parse` | 解析日志文件到本地库 | log → local.db |
| `aggregate` | 从本地库汇总生成发布库 | local.db → release.db |
| `export` | 导出发布库到 CSV | release.db → CSV |
| `stats` | 查询本地库统计信息 | 只读 local.db |
| `clean` | 清理本地库数据(软删除) | 只写 local.db |
---
## 2. 命令详解
### 2.1 parse - 解析日志文件
### 2.1 parse
```bash
# 解析单个文件
@@ -29,41 +30,60 @@ dotnet run parse --dir path/to/logs/
# 解析目录(递归)
dotnet run parse --dir path/to/logs/ --recursive
# 强制重新解析(覆盖已有数据)
# 强制重新解析
dotnet run parse --file log.txt --force
```
### 2.2 stats - 查询统计
### 2.2 aggregate
```bash
# 查看文件解析统计
dotnet run stats --file log.txt
# 从 local.db 汇总生成 release.db默认重建
dotnet run aggregate
# 追加模式(不删除已有记录)
dotnet run aggregate --rebuild false
```
### 2.3 export
```bash
# 导出正式充电参数 CSV默认
dotnet run export --output ./data/output
# 导出正式充电参数 CSV + legacy Qfod/Ploss
dotnet run export --type all --output ./data/output
# 仅导出 legacy Qfod CSV
dotnet run export --type qfod --format csv
# 导出 JSON
dotnet run export --format json --type qfod
```
### 2.4 stats
```bash
# 查看整体统计
dotnet run stats --summary
# 按 FOD 类型统计
dotnet run stats --by-fod-type
# 按面板统计
dotnet run stats --by-panel
# 按硬件版本统计
dotnet run stats --by-hardware
# 按 RX 类型统计
dotnet run stats --by-rx-type
```
### 2.3 export - 导出数据
### 2.5 clean
```bash
# 导出为 CSV
dotnet run export --format csv --output ./output/
# 导出为 JSON
dotnet run export --format json --output ./output/
# 按文件导出
dotnet run export --file log.txt --format csv
```
### 2.4 clean - 清理数据
```bash
# 清理指定文件的数据
dotnet run clean --file log.txt --confirm
# 清理指定场景
dotnet run clean --scenario <guid> --confirm
# 清理所有数据
dotnet run clean --all --confirm
@@ -71,98 +91,20 @@ dotnet run clean --all --confirm
---
## 3. 命令实现
## 3. 典型工作流
```csharp
// Commands/ParseCommand.cs
using System.CommandLine;
```bash
# 1. 解析日志
dotnet run parse --dir ./data/test_input --recursive
namespace Gpulse.WCT.DataAnalyzer.Commands;
# 2. 查看统计
dotnet run stats --summary
public class ParseCommand : Command
{
public ParseCommand(ParseService parseService)
: base("parse", "Parse log files and store data to database")
{
var fileOption = new Option<string>(
"--file",
"Path to a single log file to parse"
);
# 3. 汇总生成发布库
dotnet run aggregate
var dirOption = new Option<string>(
"--dir",
"Path to directory containing log files"
);
var recursiveOption = new Option<bool>(
"--recursive",
() => false,
"Recursively search subdirectories"
);
var forceOption = new Option<bool>(
"--force",
() => false,
"Force re-parse even if file already parsed"
);
AddOption(fileOption);
AddOption(dirOption);
AddOption(recursiveOption);
AddOption(forceOption);
this.SetHandler(async (file, dir, recursive, force) =>
{
if (!string.IsNullOrEmpty(file))
{
await parseService.ParseFileAsync(file, force);
}
else if (!string.IsNullOrEmpty(dir))
{
await parseService.ParseDirectoryAsync(dir, recursive, force);
}
else
{
Console.WriteLine("Please specify --file or --dir");
}
}, fileOption, dirOption, recursiveOption, forceOption);
}
}
# 4. 导出正式 CSV
dotnet run export --output ./data/output
```
---
## 4. Program.cs 入口
```csharp
// Program.cs
using System.CommandLine;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Gpulse.WCT.DataAnalyzer.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("Gpulse.WCT.DataAnalyzer - FOD Log Data Parser");
// 使用服务提供者获取服务
using var scope = app.Services.CreateScope();
var provider = scope.ServiceProvider;
rootCommand.AddCommand(new ParseCommand(provider.GetRequiredService<ParseService>()));
rootCommand.AddCommand(new StatsCommand(provider.GetRequiredService<StatsService>()));
rootCommand.AddCommand(new ExportCommand(provider.GetRequiredService<ExportService>()));
rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService<CleanService>()));
return await rootCommand.InvokeAsync(args);
```
输出文件: `./data/output/WCT-ChargingParameterDatabase.csv`

View File

@@ -1,16 +1,14 @@
# DbContext 工厂
> 所属模块:[数据库切换支持](./概览.md)
> 所属模块:[数据库支持](./概览.md)
---
## 1. 接口定义
## 1. 本地库工厂
```csharp
// Data/IDbContextFactory.cs
using Gpulse.WCT.DataAnalyzer.Data;
namespace Gpulse.WCT.DataAnalyzer.Data;
// Infrastructure/LocalData/IDbContextFactory.cs
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
public interface IDbContextFactory
{
@@ -18,105 +16,68 @@ public interface IDbContextFactory
}
```
工厂实现读取 `Database:LocalPath`fallback `Database:Path`),支持 SQLite 和 PostgreSQL。
---
## 2. 工厂实现
## 2. 发布库工厂
```csharp
// Data/DbContextFactory.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
// Infrastructure/ReleaseData/IReleaseDbContextFactory.cs
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.ReleaseData;
namespace Gpulse.WCT.DataAnalyzer.Data;
public class DbContextFactory : IDbContextFactory
public interface IReleaseDbContextFactory
{
private readonly IConfiguration _configuration;
public DbContextFactory(IConfiguration configuration)
{
_configuration = configuration;
}
public WctMinerDbContext CreateDbContext()
{
var dbType = _configuration["Database:Type"]?.ToLower() ?? "sqlite";
var optionsBuilder = new DbContextOptionsBuilder<WctMinerDbContext>();
switch (dbType)
{
case "postgresql":
case "postgres":
var pgConnStr = BuildPostgreSqlConnectionString();
optionsBuilder.UseNpgsql(pgConnStr);
break;
case "sqlite":
default:
var dbPath = _configuration["Database:Path"] ?? "./data/database/wctminer.db";
EnsureDirectoryExists(dbPath);
optionsBuilder.UseSqlite($"Data Source={dbPath}");
break;
}
#if DEBUG
optionsBuilder.EnableSensitiveDataLogging();
optionsBuilder.EnableDetailedErrors();
#endif
return new WctMinerDbContext(optionsBuilder.Options);
}
private string BuildPostgreSqlConnectionString()
{
var host = _configuration["Database:Host"] ?? "localhost";
var port = _configuration["Database:Port"] ?? "5432";
var name = _configuration["Database:Name"] ?? "wctminer";
var user = _configuration["Database:User"] ?? "postgres";
var password = _configuration["Database:Password"] ?? "";
return $"Host={host};Port={port};Database={name};Username={user};Password={password}";
}
private static void EnsureDirectoryExists(string dbPath)
{
var directory = Path.GetDirectoryName(dbPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
}
ReleaseDbContext CreateDbContext();
}
```
工厂实现读取 `Database:ReleasePath`,支持 SQLite 和 PostgreSQL。PostgreSQL 时使用 `Release*` 前缀配置(`ReleaseHost`, `ReleasePort`, `ReleaseName`, `ReleaseUser`, `ReleasePassword`),未设置时回退到 local 的对应配置。
---
## 3. 依赖注入扩展
## 3. DI 注册
```csharp
// Extensions/ServiceCollectionExtensions.cs
using Microsoft.Extensions.DependencyInjection;
using Gpulse.WCT.DataAnalyzer.Data;
namespace Gpulse.WCT.DataAnalyzer.Extensions;
public static class ServiceCollectionExtensions
// Infrastructure/Extensions/ServiceCollectionExtensions.cs
public static IServiceCollection AddDatabaseServices(
this IServiceCollection services,
IConfiguration configuration)
{
public static IServiceCollection AddDatabaseServices(
this IServiceCollection services,
IConfiguration configuration)
// 本地库
services.AddScoped<IDbContextFactory, DbContextFactory>();
services.AddScoped<WctMinerDbContext>(sp =>
{
// 注册 DbContext 工厂
services.AddScoped<IDbContextFactory, DbContextFactory>();
// 注册 DbContext通过工厂创建
services.AddScoped<WctMinerDbContext>(sp =>
{
var factory = sp.GetRequiredService<IDbContextFactory>();
return factory.CreateDbContext();
});
var factory = sp.GetRequiredService<IDbContextFactory>();
return factory.CreateDbContext();
});
return services;
}
// 发布库(独立)
services.AddScoped<IReleaseDbContextFactory, ReleaseDbContextFactory>();
services.AddScoped<ReleaseDbContext>(sp =>
{
var factory = sp.GetRequiredService<IReleaseDbContextFactory>();
return factory.CreateDbContext();
});
return services;
}
```
## 4. 启动初始化
```csharp
// Program.cs
using var scope = app.Services.CreateScope();
// 初始化本地库
var dbContext = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
await dbContext.Database.EnsureCreatedAsync();
// 初始化发布库
var releaseContext = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>();
await releaseContext.Database.EnsureCreatedAsync();
// 本地库种子数据
SeedData.Initialize(dbContext);
```

View File

@@ -1,4 +1,4 @@
# 数据库切换支持
# 数据库切换与双数据库架构
> 所属模块:[架构概览](../概览.md)
@@ -8,38 +8,48 @@
| 文档 | 说明 |
|------|------|
| [DbContext 工厂](./DbContext工厂.md) | 数据库上下文工厂设计 |
| [迁移策略](./迁移策略.md) | 数据库迁移和切换步骤 |
| [DbContext 工厂](./DbContext工厂.md) | 两个数据库上下文工厂设计 |
| [迁移策略](./迁移策略.md) | 数据库迁移步骤 |
---
## 切换策略概览
## 双数据库架构
```
┌─────────────────────────────────────────────────────────────┐
│ appsettings.json │
"Database": { "Type": "sqlite" | "postgresql" }
└─────────────────────────────────────────────────────────────┘
```text
┌─────────────────────────────────────────────────────────────────
appsettings.json
│ Database:LocalPath → local.db
│ Database:ReleasePath → release.db │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ DbContextFactory │
│ 读取配置 → 选择 Provider → 创建 DbContext
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────
DbContextFactory
│ 读取配置 → 选择 Provider → 创建 WctMinerDbContext │
│ ReleaseDbContextFactory │
│ 读取配置 → 选择 Provider → 创建 ReleaseDbContext │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
WctMinerDbContext
统一的 DbModel 配置,适配两种数据库
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────
local.db release.db
──────── ──────────
│ tx_panel charging_parameter │
│ tx_hardware 独立实体,无外键关联 │
│ tx_software │
│ rx_type │
│ test_scenario │
│ qfod_record │
│ ploss_record │
└─────────────────────────────────────────────────────────────────┘
```
---
## 当前状态
| 项目 | 状态 |
|------|------|
| SQLite | ✅ 默认启用 |
| PostgreSQL | ✅ 架构支持,待配置 |
| 切换方式 | 配置文件切换 |
| PostgreSQL | ✅ 架构支持 |
| local.db | ✅ 解析库 |
| release.db | ✅ 发布库 |
| 双库隔离 | ✅ 无外键/导航属性 |

View File

@@ -1,16 +1,17 @@
# 数据库迁移策略
> 所属模块:[数据库切换支持](./概览.md)
> 所属模块:[数据库支持](./概览.md)
---
## 1. 迁移命令
## 1. 本地库迁移
### SQLite
```bash
# 添加迁移
dotnet ef migrations add InitialCreate --output-dir Migrations/SQLite
dotnet ef migrations add InitialCreate --output-dir Migrations/SQLite \
--context WctMinerDbContext --project src/Gpulse.WCT.DataAnalyzer.Core
# 应用迁移
dotnet ef database update
@@ -20,42 +21,43 @@ dotnet ef database update
```bash
# 切换配置后添加迁移
dotnet ef migrations add InitialCreate --output-dir Migrations/PostgreSQL -- --environment PostgreSQL
# 应用迁移
dotnet ef database update -- --environment PostgreSQL
dotnet ef migrations add InitialCreate --output-dir Migrations/PostgreSQL \
--context WctMinerDbContext --project src/Gpulse.WCT.DataAnalyzer.Core \
-- --environment Production
```
---
## 2. 切换步骤
## 2. 发布库迁移
### 从 SQLite 切换到 PostgreSQL
发布库的迁移需要使用 `ReleaseDbContext`
| 步骤 | 操作 | 说明 |
|------|------|------|
| 1 | 修改配置 | `appsettings.json``Database:Type` 改为 `postgresql` |
| 2 | 设置连接 | 配置 `Host`, `Port`, `Name`, `User`, `Password` |
| 3 | 创建数据库 | `CREATE DATABASE wctminer;` |
| 4 | 应用迁移 | `dotnet ef database update` |
| 5 | 数据迁移 | 使用工具迁移 SQLite 数据到 PostgreSQL |
```bash
# 添加迁移
dotnet ef migrations add InitialCreate --output-dir Migrations/Release \
--context ReleaseDbContext --project src/Gpulse.WCT.DataAnalyzer.Core
# 应用迁移
dotnet ef database update --context ReleaseDbContext
```
---
## 3. 配置文件示例
### SQLite (默认)
### SQLite(默认双库)
```json
{
"Database": {
"Type": "sqlite",
"Path": "./data/database/wctminer.db"
"LocalPath": "./data/database/local.db",
"ReleasePath": "./data/database/release.db"
}
}
```
### PostgreSQL
### PostgreSQL(双库独立)
```json
{
@@ -63,9 +65,8 @@ dotnet ef database update -- --environment PostgreSQL
"Type": "postgresql",
"Host": "localhost",
"Port": 5432,
"Name": "wctminer",
"User": "wctminer_user",
"Password": "${DB_PASSWORD}"
"LocalPath": "Server=localhost;Database=wct_local",
"ReleasePath": "Server=db-server;Database=wct_charging_parameters"
}
}
```
@@ -77,5 +78,11 @@ dotnet ef database update -- --environment PostgreSQL
推荐使用 [pgloader](https://github.com/dimitri/pgloader) 迁移 SQLite 到 PostgreSQL
```bash
pgloader ./data/database/wctminer.db postgresql://user:password@localhost:5432/wctminer
```
# 本地库
pgloader ./data/database/local.db postgresql://user:password@localhost:5432/wct_local
# 发布库
pgloader ./data/database/release.db postgresql://user:password@db-server:5432/wct_charging_parameters
```
两个数据库独立迁移,不共享迁移历史。

View File

@@ -4,213 +4,158 @@
---
## 1. 完整解析流程
## 1. 完整流程
```text
log 文件
→ parse
→ local.db
→ aggregate
→ release.db
→ export
→ WCT-ChargingParameterDatabase.csv
```
---
## 2. 阶段一parse → local.db
```mermaid
sequenceDiagram
participant CLI as CLI Entry
participant CLI as CLI
participant PS as ParseService
participant SS as ScenarioService
participant FNP as FileNameParser
participant QP as QfodParser
participant PP as PlossParser
participant DB as Database
participant Log as Serilog
participant DB as local.db
CLI->>PS: 扫描日志目录
PS-->>CLI: 返回文件列表
CLI->>PS: ParseFileAsync(filePath)
PS->>FNP: 解析文件名
FNP-->>PS: ScenarioInfo
PS->>SS: GetOrCreateScenarioAsync
SS->>DB: 维度 upsert + 场景 upsert
SS-->>PS: TestScenario
PS->>PS: 逐行读取文件
loop 每个日志文件
CLI->>PS: ParseFileAsync(filePath)
PS->>FNP: 解析文件名
FNP-->>PS: 场景信息面板、版本、RX类型等
PS->>SS: GetOrCreateScenarioAsync(场景信息)
SS->>DB: 查询/创建维度记录
SS->>DB: 查询/创建场景记录
SS-->>PS: 返回 TestScenario
PS->>PS: 读取文件内容
loop 逐行遍历
alt 是 Qfod 行
PS->>QP: Parse(line, scenarioId)
QP-->>PS: ParseResult
else 是 Ploss 行
PS->>PP: Parse(line, scenarioId)
PP-->>PS: ParseResult
end
alt 解析成功
PS->>PS: 加入记录列表
else 解析失败
PS->>Log: LogError(line, error)
end
loop 每
alt Qfod 行
PS->>QP: Parse(line)
QP-->>PS: QfodRecord
else Ploss 行
PS->>PP: Parse(line)
PP-->>PS: PlossRecord
end
PS->>DB: BATCH INSERT QfodRecords
PS->>DB: BATCH INSERT PlossRecords
PS->>DB: UPDATE scenario (qfod_count, ploss_count)
PS-->>CLI: 返回 ParseReport
end
CLI-->>CLI: 输出解析报告
PS->>DB: BatchInsert QfodRecords
PS->>DB: BatchInsert PlossRecords
PS->>DB: UpdateScenarioStats
PS-->>CLI: ParseReport
```
**关键点:**
- 文件名格式: `TxPanel-TxHardware-TxSoftware-RxType-Purpose-Date-Seq.log`
- 支持两行 Ploss 格式header 行 + 数据行)
- 事务批量写入,读取 `Parser:BatchSize` 配置分批
- 解析错误记录到 Serilog不阻断流程
---
## 3. 阶段二aggregate → release.db
```mermaid
sequenceDiagram
participant CLI as CLI
participant AS as AggregationService
participant LDB as local.db
participant RDB as release.db
CLI->>AS: AggregateAsync()
AS->>LDB: 读取所有 PlossRecord + 关联维度
AS->>AS: 按 (车厂, 车型, 手机厂商, 型号) 分组
AS->>AS: 计算 9 个功率列平均值
AS->>AS: 计算 Q值/Q基值/P-Q系数
AS->>RDB: DELETE + INSERT (事务)
AS-->>CLI: AggregationReport
```
**聚合规则:**
| 字段 | 来源 | 计算方式 |
|------|------|----------|
| 车厂 (CarFactory) | 配置映射 `Aggregation:TxPanelMappings` | 找不到映射时 fallback 为 TxPanel.Name |
| 车型 (CarModel) | 配置映射 `Aggregation:CarModelMappings` | fallback 为 TxHardware.Version |
| 手机厂商 (PhoneBrand) | RxType.Name 拆分 | 取第一个分隔符前部分 |
| 型号 (PhoneModel) | RxType.Name 拆分 | 取第一个分隔符后部分 |
| 350mW2250mW9列 | PlossRecord.Field7 | 按功率匹配后取平均值 |
| Q值 (QValue) | QfodRecord.CurrentQ | 平均值 |
| Q基值 (QBaseValue) | QfodRecord.RawQ | 平均值 |
| P-Q值系数 (PqCoefficient) | PlossRecord.DeltaP | 平均值 |
| 谐振频率 (ResonanceFrequency) | 暂无 | 允许为空 |
**替代配置:**
配置文件 `appsettings.json` 中可添加映射:
```json
{
"Aggregation": {
"TxPanelMappings": {
"single-mold": "奇瑞",
"dual-rapid": "智己"
},
"CarModelMappings": {
"L6": "L6",
"CM3": "CM3"
}
}
}
```
---
## 2. ParseService 实现
## 4. 阶段三export → CSV
```csharp
// Services/ParseService.cs
using Microsoft.EntityFrameworkCore;
using Serilog;
using Gpulse.WCT.DataAnalyzer.Data;
using Gpulse.WCT.DataAnalyzer.Models;
using Gpulse.WCT.DataAnalyzer.Parsers;
```mermaid
sequenceDiagram
participant CLI as CLI
participant ES as ExportService
participant RDB as release.db
participant FS as Filesystem
namespace Gpulse.WCT.DataAnalyzer.Services;
CLI->>ES: ExportChargingParametersToCsvAsync(outputDir)
ES->>RDB: 读取 ChargingParameters (排序)
ES->>FS: 写入 WCT-ChargingParameterDatabase.csv
ES-->>CLI: 文件路径
```
public class ParseService
{
private readonly WctMinerDbContext _context;
private readonly ScenarioService _scenarioService;
private readonly QfodParser _qfodParser;
private readonly PlossParser _plossParser;
private readonly FileNameParser _fileNameParser;
private readonly int _batchSize;
**导出规格:**
public ParseService(
WctMinerDbContext context,
ScenarioService scenarioService,
QfodParser qfodParser,
PlossParser plossParser,
FileNameParser fileNameParser,
IConfiguration config)
{
_context = context;
_scenarioService = scenarioService;
_qfodParser = qfodParser;
_plossParser = plossParser;
_fileNameParser = fileNameParser;
_batchSize = config.GetValue("Parser:BatchSize", 1000);
}
- 固定文件名: `WCT-ChargingParameterDatabase.csv`
- 固定 17 列 UTF-8 BOM
- RFC 4180 转义
- 数字使用 invariant culture
public async Task<ParseReport> ParseFileAsync(string filePath)
{
var fileInfo = new FileInfo(filePath);
---
// 解析文件名获取场景信息
var scenarioInfo = _fileNameParser.Parse(fileInfo.Name);
if (scenarioInfo == null)
{
Log.Error("无法解析文件名: {FileName}", fileInfo.Name);
return new ParseReport(fileInfo.Name, 0, 0, 1, "文件名格式不匹配");
}
## 5. 数据库隔离
// 获取或创建测试场景
var scenario = await _scenarioService.GetOrCreateScenarioAsync(scenarioInfo);
```text
local.db release.db
───────── ──────────
tx_panel charging_parameter
tx_hardware (独立实体,无外键)
tx_software
rx_type
test_scenario
qfod_record
ploss_record
var lines = await File.ReadAllLinesAsync(filePath);
var qfodRecords = new List<QfodRecord>();
var plossRecords = new List<PlossRecord>();
int errorCount = 0;
无关联 ←→ 无关联
```
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i];
var lineNumber = i + 1;
if (_qfodParser.CanParse(line))
{
var result = _qfodParser.Parse(line, scenario.Id);
if (result.IsSuccess)
qfodRecords.Add(result.Record!);
else
{
Log.Error("Qfod解析失败 [{Line}] {Error}: {Message}",
lineNumber, result.ErrorType, result.ErrorMessage);
errorCount++;
}
}
else if (_plossParser.CanParse(line))
{
var result = _plossParser.Parse(line, scenario.Id);
if (result.IsSuccess)
plossRecords.Add(result.Record!);
else
{
Log.Error("Ploss解析失败 [{Line}] {Error}: {Message}",
lineNumber, result.ErrorType, result.ErrorMessage);
errorCount++;
}
}
}
// 批量保存
await BatchInsertAsync(qfodRecords, plossRecords);
// 更新场景统计
await UpdateScenarioStatsAsync(scenario.Id, qfodRecords.Count, plossRecords.Count);
return new ParseReport(fileInfo.Name, qfodRecords.Count, plossRecords.Count, errorCount, null);
}
private async Task BatchInsertAsync(
List<QfodRecord> qfodRecords,
List<PlossRecord> plossRecords)
{
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// 批量插入 QfodRecord
foreach (var batch in qfodRecords.Chunk(_batchSize))
{
_context.QfodRecords.AddRange(batch);
await _context.SaveChangesAsync();
}
// 批量插入 PlossRecord
foreach (var batch in plossRecords.Chunk(_batchSize))
{
_context.PlossRecords.AddRange(batch);
await _context.SaveChangesAsync();
}
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
private async Task UpdateScenarioStatsAsync(Guid scenarioId, int qfodCount, int plossCount)
{
// 使用累加更新(+=),支持同一场景的多次解析
// 若需幂等解析,应在 ParseFileAsync 开始时检查场景是否已处理
var scenario = await _context.TestScenarios.FindAsync(scenarioId);
if (scenario != null)
{
scenario.QfodCount += qfodCount;
scenario.PlossCount += plossCount;
await _context.SaveChangesAsync();
}
}
}
public record ParseReport(string FileName, int QfodCount, int PlossCount, int ErrorCount, string? ErrorMessage);
public record ScenarioInfo(
string TxPanel,
string TxHardware,
string TxSoftware,
string RxType,
string? TestPurpose,
DateOnly TestDate,
int TestSequence);
```
- 代码中不存在 local entity 到 release entity 的导航属性
- 数据库中没有跨库外键
- export 不读 local.dbaggregate 不写 local.db

View File

@@ -1,68 +1,149 @@
# Architecture: Gpulse.WCT.DataAnalyzer
> 无线充电 FOD异物检测日志数据采集系统从日志文件解析 QfodPloss 数据并存储到数据库
> 无线充电参数采集系统。从 log 文件解析 Qfod/Ploss 数据 → 本地分析库 → 汇总生成正式充电参数数据库 → 导出 CSV。
---
## 数据流
```text
log 文件
→ parse
→ local.db本地解析库
→ aggregate
→ release.db正式发布库
→ export
→ WCT-ChargingParameterDatabase.csv
```
| 阶段 | 命令 | 存储 | 说明 |
|------|------|------|------|
| 解析 | `parse` | `local.db` | 本地日志解析库,保存原始 Qfod/Ploss 记录 |
| 汇总 | `aggregate` | `release.db` | 从 local.db 汇总生成正式充电参数记录 |
| 导出 | `export` | CSV | 读取 release.db输出固定格式 CSV |
## 文档索引
| 文档 | 说明 |
|------|------|
| [技术选型](./技术选型.md) | 技术栈总览、决策记录 |
| [模块划分](./模块划分.md) | 模块总览、分层架构、依赖关系 |
| [安全架构](./安全架构.md) | 认证方案、数据安全 |
| [解析器设计](./parser/概览.md) | Qfod/Ploss 解析器详细设计 |
| [数据处理流程](./数据处理流程.md) | 解析流程、批量写入、错误处理 |
| [配置管理](./配置管理.md) | 配置文件、配置类定义 |
| [CLI 命令设计](./CLI命令设计.md) | 命令行接口定义 |
| [数据库切换支持](./database/概览.md) | SQLite/PostgreSQL 切换方案 |
| [技术选型](./技术选型.md) | 技术栈、决策记录 |
| [模块划分](./模块划分.md) | 分层架构、目录结构、模块职责 |
| [数据处理流程](./数据处理流程.md) | 解析→汇总→导出完整流程 |
| [CLI 命令设计](./CLI命令设计.md) | 命令行接口 |
| [配置管理](./配置管理.md) | 配置结构与双数据库配置 |
| [数据库支持](./database/概览.md) | 本地/发布双数据库架构 |
| [安全架构](./安全架构.md) | 路径校验、安全策略 |
| [解析器设计](./parser/概览.md) | Qfod/Ploss 解析器 |
---
## 架构图
## 架构分层
```text
┌──────────────────────────────────────────────────────────────────────┐
│ CLI Entry Point │
│ (Program.cs / System.CommandLine) │
├──────────────────────────────────────────────────────────────────────┤
│ Application Layer │
│ ┌───────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ LocalIngestion│ │ Publishing │ │ Exporting │ │Administration│ │
│ │ ParseService │ │AggregationSvc│ │ExportSvc │ │ StatsService │ │
│ │ ScenarioSvc │ │ThresholdCalc │ │ │ │ CleanService │ │
│ │ DimensionSvc │ │ │ │ │ │ │ │
│ └───────────────┘ └──────────────┘ └────────────┘ └──────────────┘ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Parsing │ │
│ │ QfodParser │ PlossParser │ FileNameParser │ │
│ └──────────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ Domain Layer │
│ ┌─────────────────────────┐ ┌──────────────────────────────────────┐ │
│ │ Local/ (本地实体) │ │ Release/ (发布实体) │ │
│ │ TxPanel, TxHardware │ │ ChargingParameterRecord │ │
│ │ TxSoftware, RxType │ │ │ │
│ │ TestScenario │ │ │ │
│ │ QfodRecord, PlossRecord │ │ │ │
│ └─────────────────────────┘ └──────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ ┌───────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ LocalData │ │ ReleaseData │ │Configuration│ │ Security │ │
│ │ WctMinerDbCtx │ │ReleaseDbCtx │ │AppSettings │ │PathValidator │ │
│ │ DbCtxFactory │ │ReleaseDbCtxF │ │ │ │ │ │
│ │ SeedData │ │ │ │ │ │ │ │
│ └───────────────┘ └──────────────┘ └────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
┌─────────────────────────────────────────────────────────────┐
│ CLI Entry Point │
│ (Program.cs / System.CommandLine) │
├─────────────────────────────────────────────────────────────┤
│ Service Layer │
├─────────────────────────────────────────────────────────────┤
│ ParseService │ ErrorHandlingService │ ThresholdCalc │
├─────────────────────────────────────────────────────────────┤
│ Parser Layer │
├─────────────────────────────────────────────────────────────┤
│ QfodParser │ PlossParser │ LogFileReader │
├─────────────────────────────────────────────────────────────┤
│ Data Layer │
├─────────────────────────────────────────────────────────────┤
│ Models │ WctMinerDbContext │ IDbContextFactory │
├─────────────────────────────────────────────────────────────┤
│ Infrastructure │
├─────────────────────────────────────────────────────────────┤
│ Configuration │ Serilog │ AppSettings │
└─────────────────────────────────────────────────────────────┘
```
---
## 核心设计原则
| 原则 | 说明 |
|------|------|
| 技术栈 | .NET 8.0 LTS + C# 12.0 + EF Core 8.0 |
| 数据库 | SQLite当前/ PostgreSQL后续配置切换 |
| 解析模式 | 批处理,正则表达式匹配 |
| ORM 模式 | Code First支持迁移自动化 |
| CLI 框架 | System.CommandLine命令模式 |
| 技术栈 | .NET 8.0 + C# 12.0 + EF Core 8.0 |
| 数据库 | local.db / release.db 两个独立 SQLite 数据库 |
| 本地/发布隔离 | 无外键、无导航属性、无数据库级关联 |
| 解析模式 | 批处理,正则表达式 |
| CLI | System.CommandLine命令模式 |
| 汇总规则 | 按业务键分组取平均值,缺失字段允许为空 |
---
## 目录结构
## 假设/歧义标注
| 标注 | 内容 |
|------|------|
| [A] | 使用 .NET 8.0 LTS 版本 |
| [A] | 初期使用 SQLite架构支持切换 PostgreSQL |
| [?] | 增量解析策略需确认 |
| [?] | 多次解析同一文件的处理策略需确认 |
```text
src/
├── Gpulse.WCT.DataAnalyzer/ # CLI 入口
│ ├── Commands/ # 命令实现
│ │ ├── ParseCommand.cs
│ │ ├── AggregateCommand.cs
│ │ ├── StatsCommand.cs
│ │ ├── ExportCommand.cs
│ │ └── CleanCommand.cs
│ ├── Program.cs
│ └── appsettings.json
└── Gpulse.WCT.DataAnalyzer.Core/ # 核心库
├── Application/ # 应用层
│ ├── Parsing/ # 解析器
│ │ ├── IParser.cs
│ │ ├── QfodParser.cs
│ │ ├── PlossParser.cs
│ │ └── FileNameParser.cs
│ ├── LocalIngestion/ # 本地日志解析
│ │ ├── ParseService.cs
│ │ ├── ScenarioService.cs
│ │ └── DimensionService.cs
│ ├── Publishing/ # 发布汇总
│ │ ├── AggregationService.cs
│ │ └── ThresholdCalculator.cs
│ ├── Exporting/ # 导出
│ │ └── ExportService.cs
│ └── Administration/ # 管理
│ ├── StatsService.cs
│ └── CleanService.cs
├── Domain/ # 领域实体
│ └── Models/
│ ├── TxPanel.cs # 本地-维度
│ ├── TxHardware.cs
│ ├── TxSoftware.cs
│ ├── RxType.cs
│ ├── TestScenario.cs
│ ├── QfodRecord.cs
│ ├── PlossRecord.cs
│ └── ChargingParameterRecord.cs # 发布-实体
└── Infrastructure/ # 基础设施
├── LocalData/ # 本地数据库
│ ├── WctMinerDbContext.cs
│ ├── DbContextFactory.cs
│ ├── IDbContextFactory.cs
│ ├── SeedData.cs
│ └── Configurations/
├── ReleaseData/ # 发布数据库
│ ├── ReleaseDbContext.cs
│ ├── ReleaseDbContextFactory.cs
│ ├── IReleaseDbContextFactory.cs
│ └── Configurations/
├── Configuration/ # 配置
│ └── AppSettings.cs
├── Security/ # 安全
│ ├── PathValidator.cs
│ └── SecurityConstants.cs
└── Extensions/ # DI 扩展
└── ServiceCollectionExtensions.cs
```

View File

@@ -4,167 +4,172 @@
---
## 1. 模块总览
## 1. 分层架构
```
┌─────────────────────────────────────────────────────────────┐
│ CLI Entry Point │
│ (Program.cs / System.CommandLine) │
├─────────────────────────────────────────────────────────────┤
│ Service Layer │
├─────────────────────────────────────────────────────────────┤
│ ParseService │ ScenarioService │ DimensionService │ ThresholdCalc │
├─────────────────────────────────────────────────────────────┤
│ Parser Layer │
├─────────────────────────────────────────────────────────────┤
│ QfodParser │ PlossParser │ FileNameParser │
├─────────────────────────────────────────────────────────────┤
│ Data Layer │
├─────────────────────────────────────────────────────────────┤
│ Models │ WctMinerDbContext │ IDbContextFactory │
├─────────────────────────────────────────────────────────────┤
│ Infrastructure │
├─────────────────────────────────────────────────────────────┤
│ Configuration │ Serilog │ AppSettings │
└─────────────────────────────────────────────────────────────┘
```
| 层级 | 目录 | 职责 | 依赖 |
|------|------|------|------|
| CLI | `Gpulse.WCT.DataAnalyzer/Commands/` | 接收参数、流程调度、结果输出 | Application |
| Application | `Core/Application/` | 业务流程编排 | Domain, Infrastructure |
| Domain | `Core/Domain/Models/` | 实体定义,无行为 | 无 |
| Infrastructure | `Core/Infrastructure/` | 数据持久化、配置、安全 | Domain |
**依赖规则:**
- 上层可依赖下层,下层不可依赖上层
- Application 依 Domain 和 Infrastructure
- Infrastructure 依 Domain
- Domain 不依赖任何其他层
- 解析器之间不可互相调用
---
## 2. 模块职责
## 2. Application 模块职责
| 模块名 | 职责 | 依赖模块 |
|--------|------|----------|
| CLI Entry | 接收命令行参数、调度解析流程 | ParseService, Infrastructure |
| ParseService | 协调解析流程、批量写入、场景关联 | Parser Layer, ScenarioService, DbContext |
| ScenarioService | 创建/查询测试场景,解析文件名取场景信息 | DimensionService, DbContext |
| DimensionService | 维度表 CRUD获取或创建维度记录 | DbContext |
| FileNameParser | 解析文件名提取场景信息面板、版本、RX类型等 | 无 |
| LogFileReader | 读取日志文件、行过滤、文件遍历 | 无 |
| QfodParser | 解析 Qfod 格式日志,返回 QfodRecord | 无 |
| PlossParser | 解析 Ploss 格式日志,返回 PlossRecord | 无 |
| Models | 定义数据实体结构(维度表 + 事实表) | |
| WctMinerDbContext | EF Core 数据库上下文 | Models, Configurations |
| Configuration | 加载配置文件 | 无 |
| Serilog | 结构化日志输出(含解析错误日志) | 无 |
| ThresholdCalculator | 根据 RxPower 计算动态阈值 | 无 |
### 2.1 Parsing — 解析
| 类 | 职责 |
|----|------|
| `FileNameParser` | 从文件名取场景信息面板、硬件、软件、RX类型、日期、序号 |
| `QfodParser` | 解析 Qfod 格式日志行 → `QfodRecord` |
| `PlossParser` | 解析 Ploss 单行/两行格式日志 → `PlossRecord` |
### 2.2 LocalIngestion — 本地解析
| | 职责 |
|----|------|
| `ParseService` | 协调解析流程:读取文件 → 解析 → 批量写入 local.db |
| `ScenarioService` | 创建/查询测试场景,并发安全 upsert |
| `DimensionService` | 维度表 get-or-create |
### 2.3 Publishing — 发布汇总
| 类 | 职责 |
|----|------|
| `AggregationService` | 读取 local.db → 按业务键分组计算 → upsert 到 release.db |
| `ThresholdCalculator` | 动态阈值计算 |
### 2.4 Exporting — 导出
| 类 | 职责 |
|----|------|
| `ExportService` | 导出 release.db 到固定 CSV也支持 legacy Qfod/Ploss 导出 |
### 2.5 Administration — 管理
| 类 | 职责 |
|----|------|
| `StatsService` | 本地库统计查询 |
| `CleanService` | 软删除管理 |
---
## 3. 分层架构
## 3. Domain 实体
### 3.1 分层定义
### 3.1 本地实体local.db
| 层级 | 职责 | 允许调用 |
|------|------|----------|
| CLI Entry | 接收参数、流程调度、结果输出 | Service, Infrastructure |
| Service | 协调解析流程、场景管理、维度管理、批量写入 | Parser, Data, Infrastructure |
| Parser | 日志解析、文件名解析、数据提取 | 无 |
| Data | 数据持久化、DbContext 管理、实体配置 | Infrastructure |
| Infrastructure | 配置、日志 | 无 |
| 实体 | | 说明 |
|------|----|------|
| `TxPanel` | tx_panel | TX 面板类型维度 |
| `TxHardware` | tx_hardware | TX 硬件版本维度 |
| `TxSoftware` | tx_software | TX 软件版本维度 |
| `RxType` | rx_type | RX 类型维度 |
| `TestScenario` | test_scenario | 测试场景4 个维度外键 + 日期 + 序号) |
| `QfodRecord` | qfod_record | Qfod 检测数据 |
| `PlossRecord` | ploss_record | Ploss FOD 数据 |
### 3.2 依赖规则
关系:维度表 1:N TestScenario 1:N QfodRecord/PlossRecord星型模型
- 上层可调用下层
- 下层不可调用上层
- Parser 之间不可互相调用(独立解析器)
- Service 层协调 Parser 和 Data 层
- 解析错误通过 Serilog 记录,不存入业务数据库
### 3.2 发布实体release.db
| 实体 | 表 | 唯一键 |
|------|----|--------|
| `ChargingParameterRecord` | charging_parameter | (car_factory, car_model, phone_brand, phone_model) |
**两个数据库不建立外键或导航属性关联。**
---
## 4. 模块依赖图
## 4. Infrastructure 模块
### 4.1 LocalData — 本地数据库
| 类 | 职责 |
|----|------|
| `WctMinerDbContext` | 本地解析库 EF Core 上下文 |
| `DbContextFactory` | 根据配置创建 SQLite/PostgreSQL 本地上下文 |
| `IDbContextFactory` | 工厂接口 |
| `SeedData` | 默认维度数据初始化 |
| `Configurations/*` | EF Core `IEntityTypeConfiguration` 实现 |
### 4.2 ReleaseData — 发布数据库
| 类 | 职责 |
|----|------|
| `ReleaseDbContext` | 发布库 EF Core 上下文 |
| `ReleaseDbContextFactory` | 创建发布库上下文 |
| `IReleaseDbContextFactory` | 工厂接口 |
| `Configurations/*` | 发布实体配置 |
### 4.3 Configuration
| 类 | 职责 |
|----|------|
| `AppSettings` | 应用配置 POCO |
### 4.4 Security
| 类 | 职责 |
|----|------|
| `PathValidator` | 路径安全校验(遍历保护、白名单、应用目录限制) |
| `SecurityConstants` | 安全常量 |
### 4.5 Extensions
| 类 | 职责 |
|----|------|
| `ServiceCollectionExtensions` | `AddDatabaseServices` / `AddApplicationServices` DI 注册 |
---
## 5. 依赖图
```mermaid
graph TD
CLI[CLI Entry] --> PS[ParseService]
CLI --> Serilog
CLI[CLI Commands] --> Parsing[Parsing]
CLI --> LocalIngestion[LocalIngestion]
CLI --> Publishing[Publishing]
CLI --> Exporting[Exporting]
CLI --> Admin[Administration]
PS --> SS[ScenarioService]
PS --> QP[QfodParser]
PS --> PP[PlossParser]
PS --> DBM[WctMinerDbContext]
PS --> Serilog
LocalIngestion --> Parsing
LocalIngestion --> LocalData[LocalData]
Publishing --> LocalData
Publishing --> ReleaseData[ReleaseData]
Exporting --> LocalData
Exporting --> ReleaseData
Admin --> LocalData
SS --> DS[DimensionService]
SS --> FNP[FileNameParser]
SS --> DBM
LocalData --> Domain[Domain Models]
ReleaseData --> Domain
Parsing --> Domain
DS --> DBM
QP --> Serilog
PP --> Serilog
FNP --> Serilog
DBM --> Models[Models]
DBM --> Configs[Configurations]
subgraph Parser Layer
QP
PP
FNP
subgraph Application
Parsing
LocalIngestion
Publishing
Exporting
Admin
end
subgraph Data Layer
DBM
Models
Configs
subgraph Domain
Domain
end
subgraph Infrastructure
Serilog
LocalData
ReleaseData
Configuration
Security
Extensions
end
subgraph Service Layer
PS
SS
DS
end
```
---
## 5. 目录结构
```
src/Gpulse.WCT.DataAnalyzer/
├── Parsers/
│ ├── IParser.cs
│ ├── QfodParser.cs
│ ├── PlossParser.cs
│ ├── FileNameParser.cs # 解析文件名获取场景信息Parser Layer
│ └── LogFileReader.cs # 读取日志文件(可选)
├── Models/
│ ├── TxPanel.cs # TX面板维度
│ ├── TxHardware.cs # TX硬件版本维度
│ ├── TxSoftware.cs # TX软件版本维度
│ ├── RxType.cs # RX类型维度
│ ├── TestScenario.cs # 测试场景
│ ├── QfodRecord.cs # Qfod记录
│ └── PlossRecord.cs # Ploss记录
├── Data/
│ ├── WctMinerDbContext.cs
│ ├── IDbContextFactory.cs
│ └── Configurations/ # IEntityTypeConfiguration 配置类
│ ├── TxPanelConfiguration.cs
│ ├── TxHardwareConfiguration.cs
│ ├── TxSoftwareConfiguration.cs
│ ├── RxTypeConfiguration.cs
│ ├── TestScenarioConfiguration.cs
│ ├── QfodRecordConfiguration.cs
│ └── PlossRecordConfiguration.cs
├── Services/
│ ├── ParseService.cs # 解析流程协调
│ ├── ScenarioService.cs # 场景创建/查询
│ ├── DimensionService.cs # 维度表管理
│ └── ThresholdCalculator.cs # 阈值计算
├── Configuration/
│ ├── AppSettings.cs
│ └── appsettings.json
├── Commands/
│ ├── ParseCommand.cs
│ ├── StatsCommand.cs
│ ├── ExportCommand.cs
│ └── CleanCommand.cs
└── Program.cs
```

View File

@@ -4,27 +4,31 @@
---
## 1. 配置文件结构
## 1. 配置文件
```json
// Configuration/appsettings.json
{
"Database": {
"Type": "sqlite",
"Path": "./data/database/wctminer.db"
"LocalPath": "./data/database/local.db",
"ReleasePath": "./data/database/release.db"
},
"Parser": {
"BatchSize": 1000
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Gpulse.WCT.DataAnalyzer": "Debug"
}
},
"Paths": {
"LogDir": "./data/logs",
"OutputDir": "./data/output"
},
"Aggregation": {
"TxPanelMappings": {
"single-mold": "奇瑞",
"dual-rapid": "智己"
},
"CarModelMappings": {
"L6": "L6",
"CM3": "CM3"
}
}
}
```
@@ -34,8 +38,8 @@
## 2. 配置类定义
```csharp
// Configuration/AppSettings.cs
namespace Gpulse.WCT.DataAnalyzer.Configuration;
// Infrastructure/Configuration/AppSettings.cs
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Configuration;
public class AppSettings
{
@@ -47,8 +51,9 @@ public class AppSettings
public class DatabaseSettings
{
public string Type { get; set; } = "sqlite";
public string Path { get; set; } = "./data/database/wctminer.db";
public string LocalPath { get; set; } = "./data/database/local.db";
public string ReleasePath { get; set; } = "./data/database/release.db";
// PostgreSQL 配置(可选)
public string? Host { get; set; }
public int Port { get; set; } = 5432;
@@ -71,35 +76,51 @@ public class PathSettings
---
## 3. 配置加载
## 3. 数据库配置说明
```csharp
// Program.cs 配置加载部分
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Gpulse.WCT.DataAnalyzer.Configuration;
### 双数据库路径
var builder = Host.CreateDefaultBuilder(args);
| 配置键 | 默认值 | 说明 |
|--------|--------|------|
| `Database:LocalPath` | `./data/database/local.db` | 本地解析库 |
| `Database:ReleasePath` | `./data/database/release.db` | 正式发布库 |
builder.ConfigureAppConfiguration(config =>
两个数据库独立配置,可以位于不同路径,也可以使用不同类型的数据库。
### 兼容旧配置
`Database:Path` 仍然作为 local 数据库的 fallback 路径,推荐使用 `Database:LocalPath`
### PostgreSQL 支持
`Database:Type` 设为 `postgresql`local 和 release 都可以使用独立的 PostgreSQL 连接:
```json
{
config.AddJsonFile("appsettings.json", optional: false);
config.AddJsonFile($"appsettings.{environment}.json", optional: true);
config.AddEnvironmentVariables();
config.AddCommandLine(args);
});
"Database": {
"Type": "postgresql",
"Host": "localhost",
"Port": 5432,
"Name": "wct_local",
"User": "wct_user",
"Password": "...",
"ReleaseHost": "db-server",
"ReleaseName": "wct_charging_parameters",
"ReleaseUser": "wct_release",
"ReleasePassword": "..."
}
}
```
builder.ConfigureServices((context, services) =>
{
services.Configure<AppSettings>(context.Configuration.GetSection(""));
// 注册数据库服务
services.AddDatabaseServices(context.Configuration);
// 注册其他服务
services.AddScoped<QfodParser>();
services.AddScoped<PlossParser>();
services.AddScoped<ParseService>();
services.AddScoped<ErrorHandlingService>();
});
```
Release 数据库的 PostgreSQL 配置前缀为 `Release*`,未设置时回退到 local 的对应配置。
---
## 4. 聚合配置
| 配置键 | 说明 |
|--------|------|
| `Aggregation:TxPanelMappings` | TxPanel 名称 → 车厂名称的映射表 |
| `Aggregation:CarModelMappings` | TxHardware 版本 → 车型名称的映射表 |
未配置映射时,直接使用 TxPanel.Name 和 TxHardware.Version 作为最终值。手机品牌/型号从 RxType.Name 自动拆分(分隔符 `/_:`)。