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