Compare commits

...

16 Commits

Author SHA1 Message Date
Scottxjw
9fd788bc7f feat(analyze): 支持递归扫描子目录 2026-08-25 13:35:22 +08:00
Scottxjw
a554000674 docs: 移除旧的AnalyzeCommand文档 2026-08-25 13:25:26 +08:00
Scottxjw
df5234ffe0 fix(publish): 适配SDK风格csproj的版本号读取 2026-08-25 13:24:13 +08:00
Scottxjw
0677f2ea15 docs: 添加AnalyzeCommand使用指南并更新需求文档
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 11:30:31 +08:00
Scottxjw
7a8a1e9a87 test: 更新分析功能和迁移测试用例
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 11:30:16 +08:00
Scottxjw
ee8ea29265 feat(cli): 增强Analyze命令支持自动扫描和Excel输出
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 11:30:00 +08:00
Scottxjw
820bc1c2bc feat(analysis): 添加Excel报告导出功能
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 11:29:45 +08:00
Scottxjw
abaa9cb6d1 feat(db): 扩展TestScenario维度模型
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 11:29:30 +08:00
Scottxjw
3951606cfa chore(publish): 禁用单文件发布的调试符号 2026-08-17 13:39:24 +08:00
Scottxjw
b0b55c0d4e docs: 添加 AnalyzeCommand 使用指南 2026-08-17 13:30:42 +08:00
Scottxjw
ccf3885c38 chore: 忽略 Resources 目录 2026-08-17 13:24:52 +08:00
Scottxjw
81c5316eac test: 添加真实产线数据的集成测试用例 2026-08-17 13:24:46 +08:00
Scottxjw
eba1d2080c feat(parser): 支持 Field1 的 0x/0X 十六进制前缀格式 2026-08-17 13:24:39 +08:00
Scottxjw
348d11c480 chore(scripts): 添加单文件发布脚本
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-17 10:48:33 +08:00
Scottxjw
6d56a4a01b feat(parser): 支持 Ploss Field1 十六进制解析
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-17 10:47:33 +08:00
Scottxjw
36c21f03dd refactor(config): 将 appsettings.json 转为嵌入资源
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-17 10:47:00 +08:00
48 changed files with 6962 additions and 286 deletions

7
.gitignore vendored
View File

@@ -27,4 +27,9 @@ test_logs/
.idea/ .idea/
# 包目录(由 nuget restore 生成) # 包目录(由 nuget restore 生成)
packages/ packages/
# 发布产物
publish/
Resources/

View File

@@ -0,0 +1,238 @@
# WCT DataAnalyzer 使用指南
## 快速开始
### 1. 准备日志文件
将日志文件放入 `Datas` 文件夹(与 `Gpulse.WCT.DataAnalyzer.exe` 同级目录):
```
Gpulse.WCT.DataAnalyzer.exe
analyze.bat
Datas/
├── 奇瑞-E5-singleMold-A00-V001-iPhone15-无-ploss_test-2026081701.dat
├── 奇瑞-E5-singleMold-A00-V001-iPhone15-无-qfod_pure-2026081701.log
└── 奇瑞-E5-singleMold-A00-V001-iPhone15-硬币-qfod_foreign-2026081701.log
```
### 2. 运行分析
双击 `analyze.bat`,自动执行:
1. **Ploss 分析** - 批量分析所有日志文件
2. **Qfod 标定** - 自动配对纯手机和异物日志
```
========================================
WCT DataAnalyzer 分析工具
========================================
[1/2] 执行 Ploss 分析...
扫描 Datas 文件夹:
总计: 4 个日志文件
纯手机: 2 个
异物: 2 个
=== Ploss 分析 ===
[OK] Output\奇瑞\E5\iPhone15\奇瑞-E5-singleMold-A00-V001-iPhone15-无-ploss_test-2026081701.xlsx
...
[2/2] 执行 Qfod 标定...
扫描 Datas 文件夹:
总计: 4 个日志文件
纯手机: 2 个
异物: 2 个
=== Qfod 标定 ===
[OK] Output\奇瑞\E5\iPhone15\奇瑞-E5-singleMold-A00-V001-iPhone15-无-qfod_pure-2026081701_qfod.xlsx
...
========================================
分析完成
========================================
输出目录: ...Output
```
---
## 文件命名规范
日志文件名格式:
```
车厂-车型-TX面板-TX硬件-TX软件-RX类型-异物类型-测试目的-年月日测试次数.扩展名
```
### 字段说明
| 位置 | 字段 | 说明 | 示例 |
|-----|------|------|------|
| 1 | 车厂 | 制造商名称 | 奇瑞、比亚迪 |
| 2 | 车型 | 车型代号 | E5、汉 |
| 3 | TX面板 | 发射端面板类型 | singleMold |
| 4 | TX硬件 | 发射端硬件版本 | A00 |
| 5 | TX软件 | 发射端软件版本 | V001 |
| 6 | RX类型 | 接收端类型(手机名) | iPhone15 |
| **7** | **异物类型** | **区分纯手机/异物** | **无**=纯手机,**硬币**=异物 |
| 8 | 测试目的 | 测试类型 | ploss_test、qfod_pure |
| 9 | 日期序号 | yyyyMMddNN | 2026081701 |
> **第9段约束**:必须为连续的 8 位日期(`yyyyMMdd`+ 序号(至少 1 位整数),**日期与序号之间无分隔符**。
> 正例:`2026081701`2026-08-17序号 01、`202608172`(序号 2
> 反例:`20260817-01`(含分隔符,会被 FileNameParser 拒绝)、`2026-08-1701`(含短横线)。
### 异物类型第7字段
- `无` → 纯手机日志
- 其他值(如`硬币``钥匙`)→ 异物日志
---
## 分析类型
### 1. Ploss 分析
批量分析所有日志文件,生成 Excel 报告。
**输出目录结构**
```
Output/
└── {车厂}/
└── {车型}/
└── {手机名}/
└── {原文件名}.xlsx
```
**示例**
```
输入: Datas/奇瑞-E5-singleMold-A00-V001-iPhone15-无-ploss_test-2026081701.dat
输出: Output/奇瑞/E5/iPhone15/奇瑞-E5-singleMold-A00-V001-iPhone15-无-ploss_test-2026081701.xlsx
```
**Excel 内容**
- 9 个功率段350/500/750/1000/1250/1500/1750/2000/2250 W
- 每个功率段理论Δp、VPA
- 统计值:最大值、最小值、平均值、中位数、标定值
---
### 2. Qfod 标定
自动配对纯手机日志和异物日志,计算标定阈值。
**自动识别规则**
- 文件名第7字段 = `无` → 纯手机日志
- 文件名第7字段 ≠ `无` → 异物日志
**输出**
- 阈值计算:`Threshold = (纯手机ΔQ最大值 + 异物ΔQ最小值) / 2`
- 统计值ΔQ 绝对值的最大/最小/平均/中位数
> **阈值与统计的量纲**:阈值使用**有符号 ΔQ**(纯手机为正、异物为负时阈值可为负,如样例 `Threshold: -22`
> 统计行Q最大/最小/平均/中位数)使用 **|ΔQ| 绝对值**。两者刻意不同,勿混用。
---
## 命令行用法
除双击 `analyze.bat` 外,也可直接使用命令行:
```bash
# 自动扫描 Datas 文件夹并分析
Gpulse.WCT.DataAnalyzer.exe analyze --type ploss --auto
Gpulse.WCT.DataAnalyzer.exe analyze --type qfod --auto
# 指定单个文件
Gpulse.WCT.DataAnalyzer.exe analyze --type ploss --file "日志文件路径.dat"
# 指定输出路径
Gpulse.WCT.DataAnalyzer.exe analyze --type ploss --file "日志路径" --output "输出路径.xlsx"
# Qfod 标定(指定纯手机和异物日志)
Gpulse.WCT.DataAnalyzer.exe analyze --type qfod --pure "纯手机日志" --foreign "异物日志"
```
---
## 输出示例
### 默认输出文件名规则
| 命令 | 默认输出名(未指定 `--output` 时) |
|------|----------------------------------|
| Ploss | `{原文件名}.xlsx`(如 `…-ploss_test-2026081701.xlsx` |
| Qfod | `{纯手机文件名}_qfod.xlsx`(如 `…-qfod_pure-2026081701_qfod.xlsx`,自动加 `_qfod` 后缀) |
指定 `--output/-o` 时按用户给定路径输出(父目录不存在会自动创建)。
### Ploss 分析
```
扫描 Datas 文件夹:
总计: 4 个日志文件
纯手机: 4 个
异物: 0 个
=== 批量 Ploss 分析 ===
Excel report saved to: Output\奇瑞\E5\iPhone15\奇瑞-E5-singleMold-A00-V001-iPhone15-无-ploss_test-2026081701.xlsx
=== Ploss Analysis: 奇瑞-E5-singleMold-A00-V001-iPhone15-无-ploss_test-2026081701.dat ===
Total: 35, Errors: 0
Power segments found: 350, 500, 750
[完成] 已分析 4 个文件
```
### Qfod 标定
```
扫描 Datas 文件夹:
总计: 4 个日志文件
纯手机: 2 个
异物: 2 个
=== Qfod 标定 ===
自动配对:
纯手机: 奇瑞-E5-singleMold-A00-V001-iPhone15-无-qfod_pure-2026081701.log
异物: 奇瑞-E5-singleMold-A00-V001-iPhone15-硬币-qfod_foreign-2026081701.log
Excel report saved to: Output\奇瑞\E5\iPhone15\奇瑞-E5-singleMold-A00-V001-iPhone15-无-qfod_pure-2026081701_qfod.xlsx
=== Qfod Calibration ===
Pure phone: 奇瑞-E5-singleMold-A00-V001-iPhone15-无-qfod_pure-2026081701.log
Foreign object: 奇瑞-E5-singleMold-A00-V001-iPhone15-硬币-qfod_foreign-2026081701.log
Threshold: -22
Pure margin: 27, Foreign margin: -27
```
---
## 常见问题
### Q: 没有生成 Excel 文件?
检查:
1. `Datas` 文件夹是否存在
2. 日志文件扩展名是否为 `.DAT``.log`
3. 日志文件格式是否正确
### Q: Qfod 标定提示"未找到纯手机日志"
确保文件名第7字段为 `无`
```
正确: 奇瑞-E5-singleMold-A00-V001-iPhone15-无-qfod_pure-2026081701.log
错误: 奇瑞-E5-singleMold-A00-V001-iPhone15-硬币-qfod_pure-2026081701.log
```
### Q: Excel 文件在哪里?
输出目录:`Output/{车厂}/{车型}/{手机名}/`
---
## 联系支持
如有问题,请联系开发团队。

127
scripts/publish.ps1 Normal file
View File

@@ -0,0 +1,127 @@
<#
.SYNOPSIS
发布 Gpulse.WCT.DataAnalyzer生成两种包
1) Framework-Dependent需要目标机器装 .NET 8 运行时)
2) Self-Contained自带 .NET 8 运行时,无需预装)
两者均为单文件 exe附带 appsettings.json。
.PARAMETER Version
版本号,用于产物目录命名。默认从 csproj 读取 <Version>,若 csproj 未定义则 fallback 1.0.0。
.PARAMETER OutputBase
产物输出根目录,默认 .\publish
.EXAMPLE
.\scripts\publish.ps1
.\scripts\publish.ps1 -Version 1.2.0
.\scripts\publish.ps1 -Version 1.2.0 -OutputBase D:\release
#>
param(
[string]$Version,
[string]$OutputBase = ".\publish"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# ── 定位项目 ──────────────────────────────────────────────────────────────────
$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$ProjectDir = Join-Path $RepoRoot "src\Gpulse.WCT.DataAnalyzer"
$ProjectCs = Join-Path $ProjectDir "Gpulse.WCT.DataAnalyzer.csproj"
$AppName = "Gpulse.WCT.DataAnalyzer"
if (-not (Test-Path $ProjectCs)) {
Write-Error "找不到项目文件: $ProjectCs"
exit 1
}
# ── 解析版本号 ─────────────────────────────────────────────────────────────────
if (-not $Version) {
# 尝试从 csproj 的 <Version> 节点读取
# SDK 风格的 csproj 没有 XML 命名空间,直接用 XPath 查询
[xml]$csprojXml = Get-Content $ProjectCs -Raw
$versionNode = $csprojXml.SelectSingleNode("//Version")
if ($versionNode) {
$Version = $versionNode.InnerText
} else {
$Version = "1.0.0"
}
}
Write-Host "版本号: $Version" -ForegroundColor Cyan
# ── 目录准备 ───────────────────────────────────────────────────────────────────
$Timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$FdDir = Join-Path $OutputBase "framework-dependent\$AppName-$Version-fd"
$ScDir = Join-Path $OutputBase "self-contained\$AppName-$Version-sc"
foreach ($dir in @($FdDir, $ScDir)) {
if (Test-Path $dir) { Remove-Item $dir -Recurse -Force }
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
# ── 公共发布参数 ────────────────────────────────────────────────────────────────
$CommonPublishArgs = @(
"publish"
$ProjectCs
"-c", "Release"
"-r", "win-x64"
"--nologo"
"-p:PublishSingleFile=true"
"-p:IncludeNativeLibrariesForSelfExtract=true"
"-p:DebugType=none"
"-p:DebugSymbols=false"
)
# ── 1) Framework-Dependent不含运行时─────────────────────────────────────────
Write-Host ""
Write-Host ">>> 构建 Framework-Dependent不含运行时..." -ForegroundColor Green
$fdArgs = $CommonPublishArgs + @(
"--no-self-contained"
"-o", $FdDir
)
& dotnet @fdArgs
if ($LASTEXITCODE -ne 0) {
Write-Error "Framework-Dependent 构建失败 (exit code $LASTEXITCODE)"
exit $LASTEXITCODE
}
Write-Host " 产物: $FdDir\$AppName.exe" -ForegroundColor Gray
# ── 2) Self-Contained含运行时────────────────────────────────────────────────
Write-Host ""
Write-Host ">>> 构建 Self-Contained含 .NET 8 运行时)..." -ForegroundColor Green
$scArgs = $CommonPublishArgs + @(
"--self-contained"
"-o", $ScDir
"-p:EnableCompressionInSingleFile=true"
)
& dotnet @scArgs
if ($LASTEXITCODE -ne 0) {
Write-Error "Self-Contained 构建失败 (exit code $LASTEXITCODE)"
exit $LASTEXITCODE
}
Write-Host " 产物: $ScDir\$AppName.exe" -ForegroundColor Gray
# ── 汇总 ───────────────────────────────────────────────────────────────────────
$FdSize = [math]::Round((Get-Item "$FdDir\$AppName.exe").Length / 1MB, 1)
$ScSize = [math]::Round((Get-Item "$ScDir\$AppName.exe").Length / 1MB, 1)
Write-Host ""
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host " 发布完成 v$Version $Timestamp"
Write-Host "============================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host " Framework-Dependent需预装 .NET 8 运行时)"
Write-Host " 目录: $FdDir"
Write-Host " 大小: ${FdSize} MB"
Write-Host ""
Write-Host " Self-Contained自带 .NET 8 运行时)"
Write-Host " 目录: $ScDir"
Write-Host " 大小: ${ScSize} MB"
Write-Host ""

View File

@@ -12,13 +12,171 @@ public class PlossAnalysisService
{ {
private readonly PlossParser _plossParser; private readonly PlossParser _plossParser;
/// <summary>
/// 功率段列表 (与模板一致)
/// </summary>
private static readonly int[] PowerSegments = [350, 500, 750, 1000, 1250, 1500, 1750, 2000, 2250];
/// <summary>
/// 标定偏移值 a按功率段
/// 标定Δp = 理论Δp最大值 + a
/// </summary>
private static readonly Dictionary<int, int> CalibrationOffsetByPower = new()
{
{ 350, 3000 },
{ 500, 1000 },
{ 750, 1200 },
{ 1000, 1500 },
{ 1250, 1500 },
{ 1500, 2000 },
{ 1750, 2500 },
{ 2000, 2500 },
{ 2250, 3000 }
};
public PlossAnalysisService(PlossParser plossParser) public PlossAnalysisService(PlossParser plossParser)
{ {
_plossParser = plossParser; _plossParser = plossParser;
} }
/// <summary> /// <summary>
/// 分析单个 Ploss 日志文件 /// 分析单个 Ploss 日志文件(生成 Excel 报告)
/// </summary>
public async Task<PlossExcelReport> AnalyzeForExcelAsync(string filePath)
{
var fileInfo = new FileInfo(filePath);
if (!fileInfo.Exists)
{
return new PlossExcelReport(
FileName: fileInfo.Name,
StatisticsByPowerSegment: new Dictionary<int, PlossPowerSegmentStatistics>(),
TotalCount: 0,
DroppedCount: 0,
ErrorCount: 0,
ErrorMessage: "文件不存在"
);
}
var lines = await File.ReadAllLinesAsync(filePath);
var records = new List<PlossRecord>();
int errorCount = 0;
// 用于暂存两行格式的 header 行
string? pendingHeaderLine = null;
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i].Trim();
if (string.IsNullOrEmpty(line)) continue;
if (_plossParser.IsMultiLineStart(line))
{
pendingHeaderLine = line;
}
else if (_plossParser.CanParse(line))
{
if (pendingHeaderLine != null)
{
// 两行格式:合并解析
var result = _plossParser.ParseMultiLine(
new[] { pendingHeaderLine, line }, Guid.Empty);
if (result is { IsSuccess: true } && result.Record != null)
records.Add(result.Record);
else
errorCount++;
pendingHeaderLine = null;
}
else
{
// 缺少 header 行,记录错误
errorCount++;
}
}
}
// 处理末尾遗留的 header 行(异常情况)
if (pendingHeaderLine != null)
{
errorCount++;
}
// 按 Field10功率段分组并计算统计
// 口径:实体语义 Field9=ploss、Field10=threshold真实日志中 threshold 值恰为功率档位 (350/500/…),故按 Field10 分桶。
var matched = records.Where(r => PowerSegments.Contains(r.Field10)).ToList();
int droppedCount = records.Count - matched.Count;
var statisticsByPowerSegment = matched
.GroupBy(r => r.Field10)
.ToDictionary(
g => g.Key,
g => CalculateStatistics(g.ToList())
);
return new PlossExcelReport(
FileName: fileInfo.Name,
StatisticsByPowerSegment: statisticsByPowerSegment,
TotalCount: records.Count,
DroppedCount: droppedCount,
ErrorCount: errorCount,
ErrorMessage: null
);
}
/// <summary>
/// 计算单个功率段的统计值
/// </summary>
private static PlossPowerSegmentStatistics CalculateStatistics(List<PlossRecord> records)
{
// 理论Δp = Field5 - (Field4 + PowLoss + Field9)(恒等式 delta_p = Tx Rx pow_loss Ploss
// 用 long 计算,避免大字段值 int 溢出PowLoss 仅两行格式有。
// TheoreticalDeltaPs 与 Vpas 必须同源同长,导出器按行配对写,否则行错位。
var usable = records.Where(r => r.PowLoss.HasValue).ToList();
var theoreticalDeltaPs = usable
.Select(r => (long)r.Field5 - ((long)r.Field4 + r.PowLoss!.Value + r.Field9))
.ToList();
var vpas = usable.Select(r => r.Field7).ToList();
int powerSegment = records[0].Field10;
long max = theoreticalDeltaPs.Count > 0 ? theoreticalDeltaPs.Max() : 0;
long min = theoreticalDeltaPs.Count > 0 ? theoreticalDeltaPs.Min() : 0;
double avg = theoreticalDeltaPs.Count > 0 ? theoreticalDeltaPs.Average() : 0;
long median = CalculateMedian(theoreticalDeltaPs);
// 标定值 = 理论Δp最大值 + a
int calibrationOffset = CalibrationOffsetByPower.GetValueOrDefault(powerSegment, 0);
long calibrationValue = max + calibrationOffset;
return new PlossPowerSegmentStatistics(
PowerSegment: powerSegment,
TheoreticalDeltaPs: theoreticalDeltaPs,
Vpas: vpas,
Max: max,
Min: min,
Avg: avg,
Median: median,
CalibrationValue: calibrationValue
);
}
/// <summary>
/// 计算中位数
/// </summary>
private static long CalculateMedian(List<long> values)
{
if (values.Count == 0) return 0;
var sorted = values.OrderBy(v => v).ToList();
int mid = sorted.Count / 2;
return sorted.Count % 2 == 0
? (sorted[mid - 1] + sorted[mid]) / 2
: sorted[mid];
}
/// <summary>
/// 分析单个 Ploss 日志文件(原始控制台输出)
/// </summary> /// </summary>
public async Task<PlossAnalysisReport> AnalyzeFileAsync(string filePath) public async Task<PlossAnalysisReport> AnalyzeFileAsync(string filePath)
{ {
@@ -48,7 +206,7 @@ public class PlossAnalysisService
var rows = new List<PlossRowReport>(); var rows = new List<PlossRowReport>();
int errorCount = 0; int errorCount = 0;
// 用于暂存两行格式的 header 行(与 ParseService 相同的合并逻辑) // 用于暂存两行格式的 header 行
string? pendingHeaderLine = null; string? pendingHeaderLine = null;
int pendingHeaderLineNumber = 0; int pendingHeaderLineNumber = 0;
@@ -81,12 +239,8 @@ public class PlossAnalysisService
} }
else else
{ {
// 单行格式(旧格式兼容) // 缺少 header 行,记录错误
var result = _plossParser.Parse(line, Guid.Empty); errorCount++;
if (result.IsSuccess && result.Record != null)
rows.Add(BuildRow(lineNumber, result.Record));
else
errorCount++;
} }
} }
} }

View File

@@ -0,0 +1,44 @@
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
/// <summary>
/// Ploss 功率段统计
/// </summary>
/// <param name="PowerSegment">功率档位 (350/500/750/1000/1250/1500/1750/2000/2250)</param>
/// <param name="TheoreticalDeltaPs">理论Δp列表long避免大字段值溢出</param>
/// <param name="Vpas">VPA列表 (Field7),与 TheoreticalDeltaPs 同源同长</param>
/// <param name="Max">理论Δp最大值</param>
/// <param name="Min">理论Δp最小值</param>
/// <param name="Avg">理论Δp平均值</param>
/// <param name="Median">理论Δp中位数</param>
/// <param name="CalibrationValue">标定Δp值 = Max + a按功率段查表</param>
public record PlossPowerSegmentStatistics(
int PowerSegment,
IReadOnlyList<long> TheoreticalDeltaPs,
IReadOnlyList<int> Vpas,
long Max,
long Min,
double Avg,
long Median,
long CalibrationValue
);
/// <summary>
/// Ploss Excel 报告
/// </summary>
/// <param name="FileName">输入文件名</param>
/// <param name="StatisticsByPowerSegment">按功率段分组的统计</param>
/// <param name="TotalCount">总记录数</param>
/// <param name="DroppedCount">不在 9 个标准功率段内的记录数Field10 非档位)</param>
/// <param name="ErrorCount">解析错误数</param>
/// <param name="ErrorMessage">错误信息</param>
public record PlossExcelReport(
string FileName,
IReadOnlyDictionary<int, PlossPowerSegmentStatistics> StatisticsByPowerSegment,
int TotalCount,
int DroppedCount,
int ErrorCount,
string? ErrorMessage
)
{
public bool IsSuccess => ErrorMessage == null;
}

View File

@@ -1,4 +1,5 @@
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing; using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Analysis; namespace Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
@@ -17,7 +18,140 @@ public class QfodCalibrationService
} }
/// <summary> /// <summary>
/// 计算标定 ΔQ 阈值 /// 计算标定 ΔQ 阈值(生成 Excel 报告)
/// </summary>
/// <param name="pureFile">纯手机充电日志文件</param>
/// <param name="foreignFile">手机+异物充电日志文件</param>
public async Task<QfodExcelReport> CalibrateForExcelAsync(string pureFile, string foreignFile)
{
var pureRecords = await ReadRecordsAsync(pureFile);
var foreignRecords = await ReadRecordsAsync(foreignFile);
if (pureRecords.ErrorMessage != null)
return new QfodExcelReport(
Path.GetFileName(pureFile), Path.GetFileName(foreignFile),
new Dictionary<byte, QfodCoilStatistics>(),
0, pureRecords.ErrorMessage);
if (foreignRecords.ErrorMessage != null)
return new QfodExcelReport(
Path.GetFileName(pureFile), Path.GetFileName(foreignFile),
new Dictionary<byte, QfodCoilStatistics>(),
0, foreignRecords.ErrorMessage);
// 空记录守卫:日志文件存在但未提取到 Qfod 行时,不能从 0 起算阈值却标记成功
if (pureRecords.Records.Count == 0 || foreignRecords.Records.Count == 0)
return new QfodExcelReport(
Path.GetFileName(pureFile), Path.GetFileName(foreignFile),
new Dictionary<byte, QfodCoilStatistics>(),
0, "日志文件中未提取到 Qfod 数据");
// 按线圈分组统计仅纯手机Qfod 输出模板无异物块,异物文件仅用于阈值计算)
var pureByCoil = pureRecords.Records
.GroupBy(r => r.CoilIndex)
.ToDictionary(g => g.Key, g => CalculateCoilStatistics(g.ToList()));
// 计算阈值(使用所有纯手机和异物的 DeltaQ有符号 ΔQ见使用指南示例 Threshold=-22
int pureMax = pureRecords.Records.Max(r => r.DeltaQ);
int foreignMin = foreignRecords.Records.Min(r => r.DeltaQ);
int threshold = (pureMax + foreignMin) / 2;
return new QfodExcelReport(
PureFileName: Path.GetFileName(pureFile),
ForeignFileName: Path.GetFileName(foreignFile),
PureStatisticsByCoil: pureByCoil,
Threshold: threshold,
ErrorMessage: null
);
}
/// <summary>
/// 计算单线圈统计
/// 注意DeltaQ 统计使用绝对值
/// </summary>
private static QfodCoilStatistics CalculateCoilStatistics(List<QfodRecord> records)
{
// DeltaQ 取绝对值统计
var deltaQs = records.Select(r => Math.Abs(r.DeltaQ)).ToList();
var currentQs = records.Select(r => r.CurrentQ).ToList();
var calibratedQs = records.Select(r => r.RawQ).ToList();
return new QfodCoilStatistics(
CoilIndex: records[0].CoilIndex,
DeltaQs: deltaQs,
CurrentQs: currentQs,
CalibratedQs: calibratedQs,
MaxDeltaQ: deltaQs.Count > 0 ? deltaQs.Max() : 0,
MinDeltaQ: deltaQs.Count > 0 ? deltaQs.Min() : 0,
AvgDeltaQ: deltaQs.Count > 0 ? deltaQs.Average() : 0,
MedianDeltaQ: CalculateMedian(deltaQs),
MaxCurrentQ: currentQs.Count > 0 ? currentQs.Max() : 0,
MinCurrentQ: currentQs.Count > 0 ? currentQs.Min() : 0,
AvgCurrentQ: currentQs.Count > 0 ? currentQs.Average() : 0,
MedianCurrentQ: CalculateMedianFloat(currentQs)
);
}
/// <summary>
/// 计算整数中位数
/// </summary>
private static int CalculateMedian(List<int> values)
{
if (values.Count == 0) return 0;
var sorted = values.OrderBy(v => v).ToList();
int mid = sorted.Count / 2;
return sorted.Count % 2 == 0
? (sorted[mid - 1] + sorted[mid]) / 2
: sorted[mid];
}
/// <summary>
/// 计算浮点数中位数
/// </summary>
private static float CalculateMedianFloat(List<float> values)
{
if (values.Count == 0) return 0;
var sorted = values.OrderBy(v => v).ToList();
int mid = sorted.Count / 2;
return sorted.Count % 2 == 0
? (sorted[mid - 1] + sorted[mid]) / 2f
: sorted[mid];
}
/// <summary>
/// 读取日志文件中的所有 Qfod 记录
/// </summary>
private async Task<(List<QfodRecord> Records, string? ErrorMessage)> ReadRecordsAsync(string filePath)
{
var fileInfo = new FileInfo(filePath);
if (!fileInfo.Exists)
{
return (new List<QfodRecord>(), $"文件不存在: {filePath}");
}
var records = new List<QfodRecord>();
var lines = await File.ReadAllLinesAsync(filePath);
foreach (var line in lines)
{
var trimmed = line.Trim();
if (string.IsNullOrEmpty(trimmed)) continue;
if (!_qfodParser.CanParse(trimmed)) continue;
var result = _qfodParser.Parse(trimmed, Guid.Empty);
if (result.IsSuccess && result.Record != null)
records.Add(result.Record);
}
return (records, null);
}
/// <summary>
/// 计算标定 ΔQ 阈值(原始控制台输出)
/// </summary> /// </summary>
/// <param name="pureFile">纯手机充电日志文件</param> /// <param name="pureFile">纯手机充电日志文件</param>
/// <param name="foreignFile">手机+异物充电日志文件</param> /// <param name="foreignFile">手机+异物充电日志文件</param>

View File

@@ -0,0 +1,50 @@
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
/// <summary>
/// 单线圈 Qfod 统计
/// </summary>
/// <param name="CoilIndex">线圈索引 (0/1/2 对应 Q0/Q1/Q2)</param>
/// <param name="DeltaQs">ΔQ绝对值列表</param>
/// <param name="CurrentQs">Q实际值列表 (CurrentQ)</param>
/// <param name="CalibratedQs">Q校准值列表 (RawQ)</param>
/// <param name="MaxDeltaQ">ΔQ绝对值最大值</param>
/// <param name="MinDeltaQ">ΔQ绝对值最小值</param>
/// <param name="AvgDeltaQ">ΔQ绝对值平均值</param>
/// <param name="MedianDeltaQ">ΔQ绝对值中位数</param>
/// <param name="MaxCurrentQ">Q实际值最大值</param>
/// <param name="MinCurrentQ">Q实际值最小值</param>
/// <param name="AvgCurrentQ">Q实际值平均值</param>
/// <param name="MedianCurrentQ">Q实际值中位数</param>
public record QfodCoilStatistics(
byte CoilIndex,
IReadOnlyList<int> DeltaQs,
IReadOnlyList<float> CurrentQs,
IReadOnlyList<float> CalibratedQs,
int MaxDeltaQ,
int MinDeltaQ,
double AvgDeltaQ,
int MedianDeltaQ,
float MaxCurrentQ,
float MinCurrentQ,
double AvgCurrentQ,
float MedianCurrentQ
);
/// <summary>
/// Qfod Excel 报告
/// </summary>
/// <param name="PureFileName">纯手机日志文件名</param>
/// <param name="ForeignFileName">异物日志文件名</param>
/// <param name="PureStatisticsByCoil">纯手机按线圈分组的统计</param>
/// <param name="Threshold">标定阈值 = (纯手机ΔQ最大值 + 异物ΔQ最小值) / 2有符号 ΔQ</param>
/// <param name="ErrorMessage">错误信息</param>
public record QfodExcelReport(
string PureFileName,
string ForeignFileName,
IReadOnlyDictionary<byte, QfodCoilStatistics> PureStatisticsByCoil,
int Threshold,
string? ErrorMessage
)
{
public bool IsSuccess => ErrorMessage == null;
}

View File

@@ -0,0 +1,363 @@
using System.Data;
using ClosedXML.Excel;
using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Exporting;
/// <summary>
/// 分析结果 Excel 导出器
/// </summary>
public class AnalyzeExcelExporter
{
/// <summary>
/// 功率段列表 (与模板一致)
/// </summary>
private static readonly int[] PowerSegments = [350, 500, 750, 1000, 1250, 1500, 1750, 2000, 2250];
/// <summary>
/// 导出 Ploss 分析报告到 Excel
/// </summary>
/// <param name="report">Ploss 分析报告</param>
/// <param name="outputPath">输出文件路径</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>输出文件路径</returns>
public async Task<string> ExportPlossReportAsync(
PlossExcelReport report,
string outputPath,
CancellationToken cancellationToken = default)
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Sheet1");
// 动态数据行找出最大数据量无数据时为0
var statsValues = report.StatisticsByPowerSegment.Values.ToList();
int maxDataRows = statsValues.Count > 0
? statsValues.Max(s => Math.Max(s.TheoreticalDeltaPs.Count, s.Vpas.Count))
: 0;
// 计算总行数:标题行(1) + 统计行(5) + 数据行子标题(1) + 数据行标题(1) + 数据行(maxDataRows)
int totalRows = 8 + maxDataRows;
// 每个功率段占两列,总共 1 + 9*2 = 19 列
int lastCol = 1 + PowerSegments.Length * 2;
// 行1: 功率段标题行(每个功率段合并两列,背景色加深)
worksheet.Cell(1, 1).Value = "功率段";
var headerRowRange = worksheet.Range(1, 1, 1, lastCol);
headerRowRange.Style.Fill.BackgroundColor = XLColor.LightGray; // 设置背景色
for (int i = 0; i < PowerSegments.Length; i++)
{
int col = 2 + i * 2;
var range = worksheet.Range(1, col, 1, col + 1);
range.Merge();
range.FirstCell().Value = PowerSegments[i];
range.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
range.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
}
// 行2-6: 统计行(每个功率段合并两列)
var statLabels = new[] { "理论Δp最大值", "理论Δp最小值", "理论Δp平均值", "理论Δp中位数", "标定Δp值" };
for (int statRow = 0; statRow < 5; statRow++)
{
int excelRow = 2 + statRow;
worksheet.Cell(excelRow, 1).Value = statLabels[statRow];
for (int i = 0; i < PowerSegments.Length; i++)
{
var power = PowerSegments[i];
int col = 2 + i * 2;
var range = worksheet.Range(excelRow, col, excelRow, col + 1);
range.Merge();
range.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
range.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
if (report.StatisticsByPowerSegment.TryGetValue(power, out var stats))
{
var value = statRow switch
{
0 => stats.Max,
1 => stats.Min,
2 => stats.Avg,
3 => stats.Median,
4 => stats.CalibrationValue,
_ => 0
};
range.FirstCell().Value = value;
}
}
}
// 行7: 数据行子标题理论Δp / VPA第一列开始合并"数据行"
int subHeaderRow = 7;
int dataStartRow = 8;
if (maxDataRows > 0)
{
worksheet.Cell(subHeaderRow, 1).Value = "数据行";
// 从子标题行开始合并到最后一行数据
int mergeEndRow = dataStartRow + maxDataRows - 1;
if (mergeEndRow > subHeaderRow)
{
var mergeRange = worksheet.Range(subHeaderRow, 1, mergeEndRow, 1);
mergeRange.Merge();
}
// 第一列居中
worksheet.Range(subHeaderRow, 1, Math.Max(subHeaderRow, dataStartRow + maxDataRows - 1), 1).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
worksheet.Range(subHeaderRow, 1, Math.Max(subHeaderRow, dataStartRow + maxDataRows - 1), 1).Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
}
for (int i = 0; i < PowerSegments.Length; i++)
{
int col = 2 + i * 2;
worksheet.Cell(subHeaderRow, col).Value = "理论Δp";
worksheet.Cell(subHeaderRow, col + 1).Value = "VPA";
worksheet.Cell(subHeaderRow, col).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
worksheet.Cell(subHeaderRow, col).Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
worksheet.Cell(subHeaderRow, col + 1).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
worksheet.Cell(subHeaderRow, col + 1).Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
}
// 填充数据行每个功率段两列理论Δp 和 VPA
for (int row = 0; row < maxDataRows; row++)
{
int excelRow = dataStartRow + row;
for (int i = 0; i < PowerSegments.Length; i++)
{
var power = PowerSegments[i];
int col = 2 + i * 2;
if (report.StatisticsByPowerSegment.TryGetValue(power, out var stats))
{
// 理论Δp第一列
if (row < stats.TheoreticalDeltaPs.Count)
{
var cell = worksheet.Cell(excelRow, col);
cell.Value = stats.TheoreticalDeltaPs[row];
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
}
// VPA第二列
if (row < stats.Vpas.Count)
{
var cell = worksheet.Cell(excelRow, col + 1);
cell.Value = stats.Vpas[row];
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
}
}
}
}
// 设置第一列居中
worksheet.Range(1, 1, totalRows, 1).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
worksheet.Range(1, 1, totalRows, 1).Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
// 添加表格边框线
if (totalRows > 0)
{
var tableRange = worksheet.Range(1, 1, totalRows, lastCol);
tableRange.Style.Border.TopBorder = XLBorderStyleValues.Thin;
tableRange.Style.Border.BottomBorder = XLBorderStyleValues.Thin;
tableRange.Style.Border.LeftBorder = XLBorderStyleValues.Thin;
tableRange.Style.Border.RightBorder = XLBorderStyleValues.Thin;
}
// 设置列宽
worksheet.Column(1).Width = 15;
for (int c = 2; c <= lastCol; c++)
worksheet.Column(c).Width = 10;
// 保存文件
EnsureParentDirectory(outputPath);
await Task.Run(() => workbook.SaveAs(outputPath), cancellationToken);
return outputPath;
}
/// <summary>
/// 导出 Qfod 标定报告到 Excel
/// </summary>
/// <param name="report">Qfod 标定报告</param>
/// <param name="outputPath">输出文件路径</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>输出文件路径</returns>
public async Task<string> ExportQfodReportAsync(
QfodExcelReport report,
string outputPath,
CancellationToken cancellationToken = default)
{
using var workbook = new XLWorkbook();
var worksheet = workbook.Worksheets.Add("Sheet1");
// 动态数据行:找出最大数据量
int maxDataRows = 0;
foreach (var stats in report.PureStatisticsByCoil.Values)
{
maxDataRows = Math.Max(maxDataRows, stats.DeltaQs.Count);
}
// 每个线圈三列,总共 1 + 3*3 = 10 列
int lastCol = 10;
// 计算总行数:标题行(1) + 统计行(5) + 数据行子标题(1) + 数据行(maxDataRows)
int totalRows = 7 + maxDataRows;
// 行1: 线圈编号标题行(背景色加深)
var headerRange = worksheet.Range(1, 1, 1, lastCol);
headerRange.Style.Fill.BackgroundColor = XLColor.LightGray;
worksheet.Cell(1, 1).Value = "线圈编号";
worksheet.Range(1, 1, 1, lastCol).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
worksheet.Range(1, 1, 1, lastCol).Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
// Q0、Q1、Q2 各合并三列
for (int coil = 0; coil < 3; coil++)
{
int col = 2 + coil * 3;
var range = worksheet.Range(1, col, 1, col + 2);
range.Merge();
range.FirstCell().Value = $"Q{coil}";
range.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
range.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
}
// 行2-6: 统计行(每个线圈合并三列)
var statLabels = new[] { "Q最大值", "Q最小值", "Q平均值", "Q中位数", "标定Q值" };
for (int statRow = 0; statRow < 5; statRow++)
{
int excelRow = 2 + statRow;
worksheet.Cell(excelRow, 1).Value = statLabels[statRow];
worksheet.Cell(excelRow, 1).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
worksheet.Cell(excelRow, 1).Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
for (int coil = 0; coil < 3; coil++)
{
int col = 2 + coil * 3;
var range = worksheet.Range(excelRow, col, excelRow, col + 2);
range.Merge();
range.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
range.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
if (report.PureStatisticsByCoil.TryGetValue((byte)coil, out var stats))
{
var value = statRow switch
{
0 => stats.MaxDeltaQ,
1 => stats.MinDeltaQ,
2 => stats.AvgDeltaQ,
3 => stats.MedianDeltaQ,
4 => report.Threshold,
_ => 0
};
range.FirstCell().Value = value;
}
}
}
// 行7: 数据行子标题ΔQ / Q实际值 / Q校准值+ 数据行标题开始合并
int subHeaderRow = 7;
int dataStartRow = 8;
if (maxDataRows > 0)
{
worksheet.Cell(subHeaderRow, 1).Value = "数据行";
int mergeEndRow = dataStartRow + maxDataRows - 1;
if (mergeEndRow > subHeaderRow)
{
var mergeRange = worksheet.Range(subHeaderRow, 1, mergeEndRow, 1);
mergeRange.Merge();
}
worksheet.Range(subHeaderRow, 1, mergeEndRow, 1).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
worksheet.Range(subHeaderRow, 1, mergeEndRow, 1).Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
}
// 每个线圈三列子标题
for (int coil = 0; coil < 3; coil++)
{
int col = 2 + coil * 3;
worksheet.Cell(subHeaderRow, col).Value = "ΔQ";
worksheet.Cell(subHeaderRow, col + 1).Value = "Q实际值";
worksheet.Cell(subHeaderRow, col + 2).Value = "Q校准值";
for (int c = col; c <= col + 2; c++)
{
worksheet.Cell(subHeaderRow, c).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
worksheet.Cell(subHeaderRow, c).Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
}
}
// 填充数据行
for (int row = 0; row < maxDataRows; row++)
{
int excelRow = dataStartRow + row;
for (int coil = 0; coil < 3; coil++)
{
if (report.PureStatisticsByCoil.TryGetValue((byte)coil, out var stats))
{
int col = 2 + coil * 3;
// ΔQ
if (row < stats.DeltaQs.Count)
{
var cell = worksheet.Cell(excelRow, col);
cell.Value = stats.DeltaQs[row];
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
}
// Q实际值
if (row < stats.CurrentQs.Count)
{
var cell = worksheet.Cell(excelRow, col + 1);
cell.Value = stats.CurrentQs[row];
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
}
// Q校准值
if (row < stats.CalibratedQs.Count)
{
var cell = worksheet.Cell(excelRow, col + 2);
cell.Value = stats.CalibratedQs[row];
cell.Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
cell.Style.Alignment.Vertical = XLAlignmentVerticalValues.Center;
}
}
}
}
// 添加表格边框线
if (totalRows > 0)
{
var tableRange = worksheet.Range(1, 1, totalRows, lastCol);
tableRange.Style.Border.TopBorder = XLBorderStyleValues.Thin;
tableRange.Style.Border.BottomBorder = XLBorderStyleValues.Thin;
tableRange.Style.Border.LeftBorder = XLBorderStyleValues.Thin;
tableRange.Style.Border.RightBorder = XLBorderStyleValues.Thin;
}
// 设置列宽
worksheet.Column(1).Width = 12;
for (int c = 2; c <= lastCol; c++)
worksheet.Column(c).Width = 10;
// 保存文件
EnsureParentDirectory(outputPath);
await Task.Run(() => workbook.SaveAs(outputPath), cancellationToken);
return outputPath;
}
/// <summary>
/// 确保 outputPath 的父目录存在(用户 --output 指向不存在目录时自动创建,避免 SaveAs 崩溃)
/// </summary>
private static void EnsureParentDirectory(string outputPath)
{
var dir = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
}
}

View File

@@ -75,6 +75,48 @@ public class DimensionService
); );
} }
/// <summary>
/// 获取或创建车厂维度(并发安全)
/// </summary>
public async Task<Manufacturer> GetOrCreateManufacturerAsync(string name)
{
return await GetOrCreateDimensionAsync(
() => _context.Manufacturers.AsNoTracking().FirstOrDefaultAsync(m => m.Name == name),
() => new Manufacturer { Name = name },
entity => _context.Manufacturers.Add(entity),
name,
"Manufacturer"
);
}
/// <summary>
/// 获取或创建车型维度(并发安全)
/// </summary>
public async Task<CarModel> GetOrCreateCarModelAsync(string name)
{
return await GetOrCreateDimensionAsync(
() => _context.CarModels.AsNoTracking().FirstOrDefaultAsync(c => c.Name == name),
() => new CarModel { Name = name },
entity => _context.CarModels.Add(entity),
name,
"Car Model"
);
}
/// <summary>
/// 获取或创建异物类型维度(并发安全)
/// </summary>
public async Task<ForeignObject> GetOrCreateForeignObjectAsync(string name)
{
return await GetOrCreateDimensionAsync(
() => _context.ForeignObjects.AsNoTracking().FirstOrDefaultAsync(f => f.Name == name),
() => new ForeignObject { Name = name },
entity => _context.ForeignObjects.Add(entity),
name,
"Foreign Object"
);
}
/// <summary> /// <summary>
/// 通用的并发安全 GetOrCreate 模式 /// 通用的并发安全 GetOrCreate 模式
/// </summary> /// </summary>

View File

@@ -7,7 +7,7 @@ using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
namespace Gpulse.WCT.DataAnalyzer.Core.Application; namespace Gpulse.WCT.DataAnalyzer.Core.Application;
/// <summary> /// <summary>
/// 选条件常量 /// 选条件常量
/// </summary> /// </summary>
public static class FilterConstants public static class FilterConstants
{ {
@@ -38,14 +38,17 @@ public class ScenarioService
/// </summary> /// </summary>
public async Task<TestScenario> GetOrCreateScenarioAsync(ScenarioInfo info) public async Task<TestScenario> GetOrCreateScenarioAsync(ScenarioInfo info)
{ {
_logger.LogDebug("查找/创建场景: TxPanel={TxPanel}, TxHardware={TxHardware}, TxSoftware={TxSoftware}, RxType={RxType}", _logger.LogDebug("查找/创建场景: Manufacturer={Manufacturer}, CarModel={CarModel}, TxPanel={TxPanel}, TxHardware={TxHardware}, TxSoftware={TxSoftware}, RxType={RxType}, ForeignObject={ForeignObject}",
info.TxPanel, info.TxHardware, info.TxSoftware, info.RxType); info.Manufacturer, info.CarModel, info.TxPanel, info.TxHardware, info.TxSoftware, info.RxType, info.ForeignObject);
// 1. 获取或创建各维度 // 1. 获取或创建各维度
var manufacturer = await _dimensionService.GetOrCreateManufacturerAsync(info.Manufacturer);
var carModel = await _dimensionService.GetOrCreateCarModelAsync(info.CarModel);
var txPanel = await _dimensionService.GetOrCreateTxPanelAsync(info.TxPanel); var txPanel = await _dimensionService.GetOrCreateTxPanelAsync(info.TxPanel);
var txHardware = await _dimensionService.GetOrCreateTxHardwareAsync(info.TxHardware); var txHardware = await _dimensionService.GetOrCreateTxHardwareAsync(info.TxHardware);
var txSoftware = await _dimensionService.GetOrCreateTxSoftwareAsync(info.TxSoftware); var txSoftware = await _dimensionService.GetOrCreateTxSoftwareAsync(info.TxSoftware);
var rxType = await _dimensionService.GetOrCreateRxTypeAsync(info.RxType); var rxType = await _dimensionService.GetOrCreateRxTypeAsync(info.RxType);
var foreignObject = await _dimensionService.GetOrCreateForeignObjectAsync(info.ForeignObject);
// 2. 并发安全的场景创建 // 2. 并发安全的场景创建
const int maxRetries = 3; const int maxRetries = 3;
@@ -56,10 +59,13 @@ public class ScenarioService
var existing = await _context.TestScenarios var existing = await _context.TestScenarios
.AsNoTracking() .AsNoTracking()
.FirstOrDefaultAsync(s => .FirstOrDefaultAsync(s =>
s.ManufacturerId == manufacturer.Id &&
s.CarModelId == carModel.Id &&
s.TxPanelId == txPanel.Id && s.TxPanelId == txPanel.Id &&
s.TxHardwareId == txHardware.Id && s.TxHardwareId == txHardware.Id &&
s.TxSoftwareId == txSoftware.Id && s.TxSoftwareId == txSoftware.Id &&
s.RxTypeId == rxType.Id && s.RxTypeId == rxType.Id &&
s.ForeignObjectId == foreignObject.Id &&
s.TestPurpose == info.TestPurpose && s.TestPurpose == info.TestPurpose &&
s.TestDate == info.TestDate && s.TestDate == info.TestDate &&
s.TestSequence == info.TestSequence); s.TestSequence == info.TestSequence);
@@ -73,10 +79,13 @@ public class ScenarioService
// 3. 创建新场景 // 3. 创建新场景
var scenario = new TestScenario var scenario = new TestScenario
{ {
ManufacturerId = manufacturer.Id,
CarModelId = carModel.Id,
TxPanelId = txPanel.Id, TxPanelId = txPanel.Id,
TxHardwareId = txHardware.Id, TxHardwareId = txHardware.Id,
TxSoftwareId = txSoftware.Id, TxSoftwareId = txSoftware.Id,
RxTypeId = rxType.Id, RxTypeId = rxType.Id,
ForeignObjectId = foreignObject.Id,
TestPurpose = info.TestPurpose, TestPurpose = info.TestPurpose,
TestDate = info.TestDate, TestDate = info.TestDate,
TestSequence = info.TestSequence TestSequence = info.TestSequence
@@ -88,8 +97,8 @@ public class ScenarioService
{ {
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
_logger.LogInformation("创建新测试场景: {ScenarioId}, TxPanel={TxPanel}, TxHardware={TxHardware}, RxType={RxType}, TestDate={TestDate}", _logger.LogInformation("创建新测试场景: {ScenarioId}, Manufacturer={Manufacturer}, CarModel={CarModel}, TxPanel={TxPanel}, TxHardware={TxHardware}, RxType={RxType}, ForeignObject={ForeignObject}, TestDate={TestDate}",
scenario.Id, info.TxPanel, info.TxHardware, info.RxType, info.TestDate); scenario.Id, info.Manufacturer, info.CarModel, info.TxPanel, info.TxHardware, info.RxType, info.ForeignObject, info.TestDate);
return scenario; return scenario;
} }
@@ -97,7 +106,7 @@ public class ScenarioService
{ {
// Detach 实体以避免 ChangeTracker 问题 // Detach 实体以避免 ChangeTracker 问题
_context.Entry(scenario).State = EntityState.Detached; _context.Entry(scenario).State = EntityState.Detached;
_logger.LogDebug("场景并发创建冲突,重试: TxPanel={TxPanel}", info.TxPanel); _logger.LogDebug("场景并发创建冲突,重试: Manufacturer={Manufacturer}, CarModel={CarModel}", info.Manufacturer, info.CarModel);
if (attempt < maxRetries - 1) if (attempt < maxRetries - 1)
{ {
@@ -111,17 +120,20 @@ public class ScenarioService
var finalScenario = await _context.TestScenarios var finalScenario = await _context.TestScenarios
.AsNoTracking() .AsNoTracking()
.FirstOrDefaultAsync(s => .FirstOrDefaultAsync(s =>
s.ManufacturerId == manufacturer.Id &&
s.CarModelId == carModel.Id &&
s.TxPanelId == txPanel.Id && s.TxPanelId == txPanel.Id &&
s.TxHardwareId == txHardware.Id && s.TxHardwareId == txHardware.Id &&
s.TxSoftwareId == txSoftware.Id && s.TxSoftwareId == txSoftware.Id &&
s.RxTypeId == rxType.Id && s.RxTypeId == rxType.Id &&
s.ForeignObjectId == foreignObject.Id &&
s.TestPurpose == info.TestPurpose && s.TestPurpose == info.TestPurpose &&
s.TestDate == info.TestDate && s.TestDate == info.TestDate &&
s.TestSequence == info.TestSequence); s.TestSequence == info.TestSequence);
if (finalScenario == null) if (finalScenario == null)
{ {
throw new InvalidOperationException($"无法获取或创建场景: {info.TxPanel}/{info.TxHardware}/{info.RxType}"); throw new InvalidOperationException($"无法获取或创建场景: {info.Manufacturer}/{info.CarModel}/{info.TxPanel}/{info.TxHardware}/{info.RxType}/{info.ForeignObject}");
} }
return finalScenario; return finalScenario;
@@ -133,10 +145,13 @@ public class ScenarioService
public async Task<TestScenario?> GetScenarioWithDimensionsAsync(Guid scenarioId) public async Task<TestScenario?> GetScenarioWithDimensionsAsync(Guid scenarioId)
{ {
return await _context.TestScenarios return await _context.TestScenarios
.Include(s => s.Manufacturer)
.Include(s => s.CarModel)
.Include(s => s.TxPanel) .Include(s => s.TxPanel)
.Include(s => s.TxHardware) .Include(s => s.TxHardware)
.Include(s => s.TxSoftware) .Include(s => s.TxSoftware)
.Include(s => s.RxType) .Include(s => s.RxType)
.Include(s => s.ForeignObject)
.FirstOrDefaultAsync(s => s.Id == scenarioId); .FirstOrDefaultAsync(s => s.Id == scenarioId);
} }
@@ -148,10 +163,13 @@ public class ScenarioService
public async Task<List<TestScenario>> GetAllScenariosAsync(int pageSize = 50, int pageIndex = 0) public async Task<List<TestScenario>> GetAllScenariosAsync(int pageSize = 50, int pageIndex = 0)
{ {
var query = _context.TestScenarios var query = _context.TestScenarios
.Include(s => s.Manufacturer)
.Include(s => s.CarModel)
.Include(s => s.TxPanel) .Include(s => s.TxPanel)
.Include(s => s.TxHardware) .Include(s => s.TxHardware)
.Include(s => s.TxSoftware) .Include(s => s.TxSoftware)
.Include(s => s.RxType) .Include(s => s.RxType)
.Include(s => s.ForeignObject)
.OrderByDescending(s => s.TestDate) .OrderByDescending(s => s.TestDate)
.ThenByDescending(s => s.TestSequence) .ThenByDescending(s => s.TestSequence)
.AsNoTracking(); .AsNoTracking();
@@ -177,6 +195,18 @@ public class ScenarioService
/// </summary> /// </summary>
public async Task<FilterOptions> GetFilterOptionsAsync() public async Task<FilterOptions> GetFilterOptionsAsync()
{ {
var manufacturers = await _context.Manufacturers
.Select(m => m.Name)
.Distinct()
.OrderBy(n => n)
.ToListAsync();
var carModels = await _context.CarModels
.Select(c => c.Name)
.Distinct()
.OrderBy(n => n)
.ToListAsync();
var txPanels = await _context.TxPanels var txPanels = await _context.TxPanels
.Select(p => p.Name) .Select(p => p.Name)
.Distinct() .Distinct()
@@ -201,37 +231,56 @@ public class ScenarioService
.OrderBy(n => n) .OrderBy(n => n)
.ToListAsync(); .ToListAsync();
var foreignObjects = await _context.ForeignObjects
.Select(f => f.Name)
.Distinct()
.OrderBy(n => n)
.ToListAsync();
return new FilterOptions( return new FilterOptions(
manufacturers.Prepend(FilterConstants.AllOption).ToList(),
carModels.Prepend(FilterConstants.AllOption).ToList(),
txPanels.Prepend(FilterConstants.AllOption).ToList(), txPanels.Prepend(FilterConstants.AllOption).ToList(),
txHardwares.Prepend(FilterConstants.AllOption).ToList(), txHardwares.Prepend(FilterConstants.AllOption).ToList(),
txSoftwares.Prepend(FilterConstants.AllOption).ToList(), txSoftwares.Prepend(FilterConstants.AllOption).ToList(),
rxTypes.Prepend(FilterConstants.AllOption).ToList()); rxTypes.Prepend(FilterConstants.AllOption).ToList(),
foreignObjects.Prepend(FilterConstants.AllOption).ToList());
} }
/// <summary> /// <summary>
/// 根据筛选条件获取测试场景 /// 根据筛选条件获取测试场景
/// </summary> /// </summary>
/// <param name="txPanel">TxPanel 名称null 或 "全部" 表示不过滤</param>
/// <param name="txHardware">TxHardware 版本null 或 "全部" 表示不过滤</param>
/// <param name="txSoftware">TxSoftware 版本null 或 "全部" 表示不过滤</param>
/// <param name="rxType">RxType 名称null 或 "全部" 表示不过滤</param>
/// <param name="pageSize">每页数量默认50传0表示不分页</param>
/// <param name="pageIndex">页码从0开始</param>
public async Task<List<TestScenario>> GetScenariosByFilterAsync( public async Task<List<TestScenario>> GetScenariosByFilterAsync(
string? manufacturer = null,
string? carModel = null,
string? txPanel = null, string? txPanel = null,
string? txHardware = null, string? txHardware = null,
string? txSoftware = null, string? txSoftware = null,
string? rxType = null, string? rxType = null,
string? foreignObject = null,
int pageSize = 50, int pageSize = 50,
int pageIndex = 0) int pageIndex = 0)
{ {
var query = _context.TestScenarios var query = _context.TestScenarios
.Include(s => s.Manufacturer)
.Include(s => s.CarModel)
.Include(s => s.TxPanel) .Include(s => s.TxPanel)
.Include(s => s.TxHardware) .Include(s => s.TxHardware)
.Include(s => s.TxSoftware) .Include(s => s.TxSoftware)
.Include(s => s.RxType) .Include(s => s.RxType)
.Include(s => s.ForeignObject)
.AsNoTracking(); .AsNoTracking();
if (!string.IsNullOrEmpty(manufacturer) && manufacturer != FilterConstants.AllOption)
{
query = query.Where(s => s.Manufacturer.Name == manufacturer);
}
if (!string.IsNullOrEmpty(carModel) && carModel != FilterConstants.AllOption)
{
query = query.Where(s => s.CarModel.Name == carModel);
}
if (!string.IsNullOrEmpty(txPanel) && txPanel != FilterConstants.AllOption) if (!string.IsNullOrEmpty(txPanel) && txPanel != FilterConstants.AllOption)
{ {
query = query.Where(s => s.TxPanel.Name == txPanel); query = query.Where(s => s.TxPanel.Name == txPanel);
@@ -252,6 +301,11 @@ public class ScenarioService
query = query.Where(s => s.RxType.Name == rxType); query = query.Where(s => s.RxType.Name == rxType);
} }
if (!string.IsNullOrEmpty(foreignObject) && foreignObject != FilterConstants.AllOption)
{
query = query.Where(s => s.ForeignObject.Name == foreignObject);
}
query = query query = query
.OrderByDescending(s => s.TestDate) .OrderByDescending(s => s.TestDate)
.ThenByDescending(s => s.TestSequence); .ThenByDescending(s => s.TestSequence);
@@ -307,7 +361,10 @@ public class ScenarioService
/// 筛选选项数据 /// 筛选选项数据
/// </summary> /// </summary>
public record FilterOptions( public record FilterOptions(
List<string> Manufacturers,
List<string> CarModels,
List<string> TxPanels, List<string> TxPanels,
List<string> TxHardwares, List<string> TxHardwares,
List<string> TxSoftwares, List<string> TxSoftwares,
List<string> RxTypes); List<string> RxTypes,
List<string> ForeignObjects);

View File

@@ -4,9 +4,9 @@ namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
/// <summary> /// <summary>
/// 文件名解析器 - 从文件名提取场景信息 /// 文件名解析器 - 从文件名提取场景信息
/// 格式: TX面板-TX硬件-TX软件-RX类型-测试目的-年月日-测试次数.log /// 格式: 车厂-车型-TX面板类型-TX硬件-TX软件-RX类型-异物类型-测试目的-年月日测试次数.dat
/// 分隔符: 使用 '-' 作为字段分隔符,字段内部可包含 '_' 或 '.' 等符号 /// 分隔符: 使用 '-' 作为字段分隔符,字段内部可包含 '_' 或 '.' 等符号
/// 示例: singleMold-v1.0-hex2_1-iPhone15-compatibility-20260702-1.log /// 示例: 奇瑞-E5-singleMold-A00-V001-伏达小蓝板-无-compatibility-2026081701.dat
/// </summary> /// </summary>
public class FileNameParser public class FileNameParser
{ {
@@ -19,39 +19,60 @@ public class FileNameParser
/// <returns>场景信息,若格式不匹配返回 null</returns> /// <returns>场景信息,若格式不匹配返回 null</returns>
public ScenarioInfo? Parse(string fileName) public ScenarioInfo? Parse(string fileName)
{ {
// 移除 .log 扩展名 // 移除扩展名
var nameWithoutExt = Path.GetFileNameWithoutExtension(fileName); var nameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
if (string.IsNullOrEmpty(nameWithoutExt)) if (string.IsNullOrEmpty(nameWithoutExt))
return null; return null;
var parts = nameWithoutExt.Split(Separators); var parts = nameWithoutExt.Split(Separators);
if (parts.Length < 7) if (parts.Length < 9)
return null; return null;
// 解析日期 // 解析日期+序号最后一个字段格式yyyyMMddNN
if (!TryParseDate(parts[5], out var testDate)) var dateSeqStr = parts[8];
if (!TryParseDateSequence(dateSeqStr, out var testDate, out var testSequence))
return null; return null;
// 解析测试序号
if (!int.TryParse(parts[6], out var testSequence))
testSequence = 1;
return new ScenarioInfo( return new ScenarioInfo(
TxPanel: parts[0], Manufacturer: parts[0],
TxHardware: parts[1], CarModel: parts[1],
TxSoftware: parts[2], TxPanel: parts[2],
RxType: parts[3], TxHardware: parts[3],
TestPurpose: parts.Length > 4 ? parts[4] : null, TxSoftware: parts[4],
RxType: parts[5],
ForeignObject: parts[6],
TestPurpose: parts[7],
TestDate: testDate, TestDate: testDate,
TestSequence: testSequence TestSequence: testSequence
); );
} }
private static bool TryParseDate(string dateStr, out DateOnly date) /// <summary>
/// 解析日期+序号组合字段
/// 格式: yyyyMMddNN (如 2026081701 表示 2026年8月17日 第01次测试)
/// </summary>
private static bool TryParseDateSequence(string dateSeqStr, out DateOnly date, out int sequence)
{ {
// 支持多种日期格式 date = default;
var formats = new[] { "yyyyMMdd", "yyyy-MM-dd", "yyMMdd" }; sequence = 1;
return DateOnly.TryParseExact(dateStr, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out date);
// 最小长度8位日期 + 1位序号 = 9位
// 标准长度8位日期 + 2位序号 = 10位
if (dateSeqStr.Length < 9)
return false;
// 提取日期部分前8位
var dateStr = dateSeqStr.Substring(0, 8);
var formats = new[] { "yyyyMMdd" };
if (!DateOnly.TryParseExact(dateStr, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out date))
return false;
// 提取序号部分(剩余位数)
var seqStr = dateSeqStr.Substring(8);
if (!int.TryParse(seqStr, out sequence))
return false;
return true;
} }
} }
@@ -59,11 +80,14 @@ public class FileNameParser
/// 场景信息 - 从文件名解析出的测试场景数据 /// 场景信息 - 从文件名解析出的测试场景数据
/// </summary> /// </summary>
public record ScenarioInfo( public record ScenarioInfo(
string Manufacturer,
string CarModel,
string TxPanel, string TxPanel,
string TxHardware, string TxHardware,
string TxSoftware, string TxSoftware,
string RxType, string RxType,
string ForeignObject,
string? TestPurpose, string? TestPurpose,
DateOnly TestDate, DateOnly TestDate,
int TestSequence int TestSequence
); );

View File

@@ -1,13 +1,13 @@
using System.Globalization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local; using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing; namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
/// <summary> /// <summary>
/// Ploss FOD日志解析器 - 支持两行格式 /// Ploss FOD日志解析器 - 两行格式
/// 第一行: pow_loss = 3053, delta_p = 1950 /// 第一行: pow_loss = 3053, delta_p = 1950
/// 第二行: FOD-> (1) (2) ... (16) /// 第二行: FOD-> (1) (2) ... (16)
/// 同时兼容旧的单行格式
/// </summary> /// </summary>
public class PlossParser : IParser<PlossRecord> public class PlossParser : IParser<PlossRecord>
{ {
@@ -17,22 +17,19 @@ public class PlossParser : IParser<PlossRecord>
RegexOptions.Compiled RegexOptions.Compiled
); );
// 第二行正则FOD-> 后跟16个数字(均支持负数) // 第二行正则FOD-> 后跟16个字段
// Field1 支持十六进制格式0X/0x 前缀,如 0C、0xFF或普通十进制
// 其余字段为十进制整数(均支持负数)。
// 共 16 个捕获组Field1 文本 + Field2..Field16对应 PlossRecord 的 Field1..Field16。
private static readonly Regex FodPattern = new( private static readonly Regex FodPattern = new(
@"^FOD->\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$", @"^FOD->\s+(0[xX][0-9A-Fa-f]+|0[0-9A-Fa-f]+|-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$",
RegexOptions.Compiled
);
// 旧格式正则兼容前3个字段被忽略所有捕获组支持负数
private static readonly Regex LegacyPattern = new(
@"^FOD->\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$",
RegexOptions.Compiled RegexOptions.Compiled
); );
/// <summary> /// <summary>
/// 匹配类型枚举 /// 匹配类型枚举
/// </summary> /// </summary>
public enum MatchType { None, Header, Fod, Legacy } public enum MatchType { None, Header, Fod }
/// <summary> /// <summary>
/// 单次匹配结果(避免重复正则匹配) /// 单次匹配结果(避免重复正则匹配)
@@ -47,10 +44,6 @@ public class PlossParser : IParser<PlossRecord>
if (fodMatch.Success) if (fodMatch.Success)
return (MatchType.Fod, fodMatch); return (MatchType.Fod, fodMatch);
var legacyMatch = LegacyPattern.Match(line);
if (legacyMatch.Success)
return (MatchType.Legacy, legacyMatch);
return (MatchType.None, Match.Empty); return (MatchType.None, Match.Empty);
} }
@@ -65,7 +58,7 @@ public class PlossParser : IParser<PlossRecord>
public bool IsMultiLineStart(string line) => TryMatch(line).Type == MatchType.Header; public bool IsMultiLineStart(string line) => TryMatch(line).Type == MatchType.Header;
/// <summary> /// <summary>
/// 单行解析(兼容旧格式 /// 单行解析(仅支持FOD行用于错误提示
/// </summary> /// </summary>
public ParseResult<PlossRecord> Parse(string line, Guid scenarioId) public ParseResult<PlossRecord> Parse(string line, Guid scenarioId)
{ {
@@ -73,15 +66,17 @@ public class PlossParser : IParser<PlossRecord>
return type switch return type switch
{ {
MatchType.Legacy => ParseLegacyFormat(match, scenarioId),
MatchType.Fod => ParseFodLine(match, scenarioId), MatchType.Fod => ParseFodLine(match, scenarioId),
MatchType.Header => ParseHeaderLine(match, scenarioId), MatchType.Header => ParseResult<PlossRecord>.Failure(
"INCOMPLETE_RECORD",
"Header line requires FOD line. Use ParseMultiLine instead."
),
_ => ParseResult<PlossRecord>.Failure("FORMAT_MISMATCH", "Line does not match Ploss pattern") _ => ParseResult<PlossRecord>.Failure("FORMAT_MISMATCH", "Line does not match Ploss pattern")
}; };
} }
/// <summary> /// <summary>
/// 多行解析(两行格式) /// 多行解析(两行格式)
/// </summary> /// </summary>
public ParseResult<PlossRecord>? ParseMultiLine(string[] lines, Guid scenarioId) public ParseResult<PlossRecord>? ParseMultiLine(string[] lines, Guid scenarioId)
{ {
@@ -103,7 +98,7 @@ public class PlossParser : IParser<PlossRecord>
PowLoss = int.Parse(headerMatch.Groups[1].Value), PowLoss = int.Parse(headerMatch.Groups[1].Value),
DeltaP = int.Parse(headerMatch.Groups[2].Value), DeltaP = int.Parse(headerMatch.Groups[2].Value),
// 第二行字段 // 第二行字段
Field1 = int.Parse(fodMatch.Groups[1].Value), Field1 = ParseFieldValue(fodMatch.Groups[1].Value),
Field2 = int.Parse(fodMatch.Groups[2].Value), Field2 = int.Parse(fodMatch.Groups[2].Value),
Field3 = int.Parse(fodMatch.Groups[3].Value), Field3 = int.Parse(fodMatch.Groups[3].Value),
Field4 = int.Parse(fodMatch.Groups[4].Value), Field4 = int.Parse(fodMatch.Groups[4].Value),
@@ -132,73 +127,9 @@ public class PlossParser : IParser<PlossRecord>
} }
} }
private ParseResult<PlossRecord> ParseLegacyFormat(Match match, Guid scenarioId)
{
// 保持原有逻辑不变,映射到新字段名
try
{
var record = new PlossRecord
{
ScenarioId = scenarioId,
// 旧格式没有第一行字段
PowLoss = null,
DeltaP = null,
// 映射到新字段名
Field1 = int.Parse(match.Groups[1].Value),
Field2 = int.Parse(match.Groups[2].Value),
Field3 = int.Parse(match.Groups[3].Value),
Field4 = int.Parse(match.Groups[4].Value), // RX power
Field5 = int.Parse(match.Groups[5].Value), // TX power
Field6 = int.Parse(match.Groups[6].Value),
Field7 = int.Parse(match.Groups[7].Value),
Field8 = int.Parse(match.Groups[8].Value),
Field9 = int.Parse(match.Groups[9].Value), // ploss
Field10 = int.Parse(match.Groups[10].Value), // threshold
Field11 = int.Parse(match.Groups[11].Value), // 触发次数
Field12 = int.Parse(match.Groups[12].Value), // PFOD result
Field13 = int.Parse(match.Groups[13].Value),
// 旧格式没有Field14-16使用默认值
Field14 = 0,
Field15 = 0,
Field16 = 0
};
return ParseResult<PlossRecord>.Success(record);
}
catch (Exception ex)
{
return ParseResult<PlossRecord>.Failure(
"TYPE_CONVERSION",
$"Failed to convert field: {ex.Message}"
);
}
}
private ParseResult<PlossRecord> ParseHeaderLine(Match match, Guid scenarioId)
{
// 单独解析header行返回部分记录
// 实际使用时应通过ParseMultiLine合并
try
{
var record = new PlossRecord
{
ScenarioId = scenarioId,
PowLoss = int.Parse(match.Groups[1].Value),
DeltaP = int.Parse(match.Groups[2].Value)
};
return ParseResult<PlossRecord>.Success(record);
}
catch (Exception ex)
{
return ParseResult<PlossRecord>.Failure(
"TYPE_CONVERSION",
$"Failed to convert header field: {ex.Message}"
);
}
}
private ParseResult<PlossRecord> ParseFodLine(Match match, Guid scenarioId) private ParseResult<PlossRecord> ParseFodLine(Match match, Guid scenarioId)
{ {
// 单独解析FOD行新格式,无header // 单独解析FOD行无header,返回部分记录
try try
{ {
var record = new PlossRecord var record = new PlossRecord
@@ -206,7 +137,7 @@ public class PlossParser : IParser<PlossRecord>
ScenarioId = scenarioId, ScenarioId = scenarioId,
PowLoss = null, PowLoss = null,
DeltaP = null, DeltaP = null,
Field1 = byte.Parse(match.Groups[1].Value), Field1 = ParseFieldValue(match.Groups[1].Value),
Field2 = int.Parse(match.Groups[2].Value), Field2 = int.Parse(match.Groups[2].Value),
Field3 = int.Parse(match.Groups[3].Value), Field3 = int.Parse(match.Groups[3].Value),
Field4 = int.Parse(match.Groups[4].Value), Field4 = int.Parse(match.Groups[4].Value),
@@ -233,4 +164,22 @@ public class PlossParser : IParser<PlossRecord>
); );
} }
} }
/// <summary>
/// 解析 Field1 的值支持十六进制格式0C、0xFF、0xFF和普通十进制。
/// 含 a-f/A-F/X 字符时按十六进制解析,否则按十进制解析;十六进制时先去掉 "0x"/"0X" 前缀。
/// </summary>
private static int ParseFieldValue(string value)
{
// 含十六进制字符A-F/a-f或显式 0x 前缀时按十六进制解析
bool isHex = value.AsSpan().IndexOfAny("abcdefABCDEFxX".AsSpan()) >= 0;
if (!isHex)
return int.Parse(value);
// 去掉可选的 0x/0X 前缀NumberStyles.HexNumber 不接受前缀)
var span = value.AsSpan();
if (span.Length > 2 && (span[1] == 'x' || span[1] == 'X') && span[0] == '0')
span = span[2..];
return int.Parse(span, NumberStyles.HexNumber);
}
} }

View File

@@ -69,9 +69,9 @@ public class AggregationService
.AsSplitQuery() .AsSplitQuery()
.Where(r => !r.IsDeleted) .Where(r => !r.IsDeleted)
.Include(r => r.Scenario) .Include(r => r.Scenario)
.ThenInclude(s => s!.TxPanel) .ThenInclude(s => s!.Manufacturer)
.Include(r => r.Scenario) .Include(r => r.Scenario)
.ThenInclude(s => s!.TxHardware) .ThenInclude(s => s!.CarModel)
.Include(r => r.Scenario) .Include(r => r.Scenario)
.ThenInclude(s => s!.RxType) .ThenInclude(s => s!.RxType)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -79,8 +79,8 @@ public class AggregationService
return records.Select(r => new PlossRow( return records.Select(r => new PlossRow(
r, r,
r.Scenario, r.Scenario,
r.Scenario.TxPanel.Name, r.Scenario.Manufacturer.Name,
r.Scenario.TxHardware.Version, r.Scenario.CarModel.Name,
r.Scenario.RxType.Name)).ToList(); r.Scenario.RxType.Name)).ToList();
} }
@@ -109,11 +109,9 @@ public class AggregationService
private ChargingParameterKey BuildKey(TestScenario scenario) private ChargingParameterKey BuildKey(TestScenario scenario)
{ {
var panelName = scenario.TxPanel.Name; // 直接使用 Manufacturer 和 CarModel 维度
var mapping = _configuration.GetSection("Aggregation:TxPanelMappings")[panelName]; var carFactory = scenario.Manufacturer.Name;
var carFactory = mapping ?? panelName; var carModel = scenario.CarModel.Name;
var carModel = _configuration[$"Aggregation:CarModelMappings:{scenario.TxHardware.Version}"]
?? scenario.TxHardware.Version;
var (phoneBrand, phoneModel) = SplitPhoneName(scenario.RxType.Name); var (phoneBrand, phoneModel) = SplitPhoneName(scenario.RxType.Name);
return new ChargingParameterKey(carFactory, carModel, phoneBrand, phoneModel); return new ChargingParameterKey(carFactory, carModel, phoneBrand, phoneModel);
} }

View File

@@ -0,0 +1,15 @@
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// 车型维度表
/// </summary>
public class CarModel
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = null!;
public bool IsDeleted { get; set; } = false;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation Properties
public ICollection<TestScenario> TestScenarios { get; set; } = [];
}

View File

@@ -0,0 +1,15 @@
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// 异物类型维度表
/// </summary>
public class ForeignObject
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = null!;
public bool IsDeleted { get; set; } = false;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation Properties
public ICollection<TestScenario> TestScenarios { get; set; } = [];
}

View File

@@ -0,0 +1,15 @@
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// 车厂维度表
/// </summary>
public class Manufacturer
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = null!;
public bool IsDeleted { get; set; } = false;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation Properties
public ICollection<TestScenario> TestScenarios { get; set; } = [];
}

View File

@@ -6,6 +6,12 @@ namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
public class TestScenario public class TestScenario
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
// 新增维度字段
public Guid ManufacturerId { get; set; }
public Guid CarModelId { get; set; }
public Guid ForeignObjectId { get; set; }
public Guid TxPanelId { get; set; } public Guid TxPanelId { get; set; }
public Guid TxHardwareId { get; set; } public Guid TxHardwareId { get; set; }
public Guid TxSoftwareId { get; set; } public Guid TxSoftwareId { get; set; }
@@ -18,6 +24,11 @@ public class TestScenario
public bool IsDeleted { get; set; } = false; public bool IsDeleted { get; set; } = false;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation Properties - 新增维度表
public Manufacturer Manufacturer { get; set; } = null!;
public CarModel CarModel { get; set; } = null!;
public ForeignObject ForeignObject { get; set; } = null!;
// Navigation Properties - 维度表 // Navigation Properties - 维度表
public TxPanel TxPanel { get; set; } = null!; public TxPanel TxPanel { get; set; } = null!;
public TxHardware TxHardware { get; set; } = null!; public TxHardware TxHardware { get; set; } = null!;

View File

@@ -20,6 +20,9 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<!-- Excel Export -->
<PackageReference Include="ClosedXML" Version="0.104.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -4,6 +4,7 @@ using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing; using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis; using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
using Gpulse.WCT.DataAnalyzer.Core.Application; using Gpulse.WCT.DataAnalyzer.Core.Application;
using Gpulse.WCT.DataAnalyzer.Core.Application.Exporting;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions; namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
@@ -63,6 +64,9 @@ public static class ServiceCollectionExtensions
services.AddScoped<PlossAnalysisService>(); services.AddScoped<PlossAnalysisService>();
services.AddScoped<QfodCalibrationService>(); services.AddScoped<QfodCalibrationService>();
// Excel Export
services.AddScoped<AnalyzeExcelExporter>();
return services; return services;
} }
} }

View File

@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
public class CarModelConfiguration : IEntityTypeConfiguration<CarModel>
{
public void Configure(EntityTypeBuilder<CarModel> builder)
{
builder.ToTable("car_model");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).HasColumnName("id");
builder.Property(e => e.Name)
.IsRequired()
.HasMaxLength(50)
.HasColumnName("name");
builder.Property(e => e.IsDeleted)
.IsRequired()
.HasDefaultValue(false)
.HasColumnName("is_deleted");
builder.Property(e => e.CreatedAt)
.IsRequired()
.HasDefaultValueSql("datetime('now')")
.HasColumnName("created_at");
builder.HasIndex(e => e.Name).IsUnique();
// 全局查询过滤器
builder.HasQueryFilter(e => !e.IsDeleted);
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
public class ForeignObjectConfiguration : IEntityTypeConfiguration<ForeignObject>
{
public void Configure(EntityTypeBuilder<ForeignObject> builder)
{
builder.ToTable("foreign_object");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).HasColumnName("id");
builder.Property(e => e.Name)
.IsRequired()
.HasMaxLength(50)
.HasColumnName("name");
builder.Property(e => e.IsDeleted)
.IsRequired()
.HasDefaultValue(false)
.HasColumnName("is_deleted");
builder.Property(e => e.CreatedAt)
.IsRequired()
.HasDefaultValueSql("datetime('now')")
.HasColumnName("created_at");
builder.HasIndex(e => e.Name).IsUnique();
// 全局查询过滤器
builder.HasQueryFilter(e => !e.IsDeleted);
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
public class ManufacturerConfiguration : IEntityTypeConfiguration<Manufacturer>
{
public void Configure(EntityTypeBuilder<Manufacturer> builder)
{
builder.ToTable("manufacturer");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).HasColumnName("id");
builder.Property(e => e.Name)
.IsRequired()
.HasMaxLength(100)
.HasColumnName("name");
builder.Property(e => e.IsDeleted)
.IsRequired()
.HasDefaultValue(false)
.HasColumnName("is_deleted");
builder.Property(e => e.CreatedAt)
.IsRequired()
.HasDefaultValueSql("datetime('now')")
.HasColumnName("created_at");
builder.HasIndex(e => e.Name).IsUnique();
// 全局查询过滤器
builder.HasQueryFilter(e => !e.IsDeleted);
}
}

View File

@@ -13,6 +13,15 @@ public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
builder.HasKey(e => e.Id); builder.HasKey(e => e.Id);
builder.Property(e => e.Id).HasColumnName("id"); builder.Property(e => e.Id).HasColumnName("id");
// 新增维度字段
builder.Property(e => e.ManufacturerId)
.IsRequired()
.HasColumnName("manufacturer_id");
builder.Property(e => e.CarModelId)
.IsRequired()
.HasColumnName("car_model_id");
builder.Property(e => e.TxPanelId) builder.Property(e => e.TxPanelId)
.IsRequired() .IsRequired()
.HasColumnName("tx_panel_id"); .HasColumnName("tx_panel_id");
@@ -29,6 +38,10 @@ public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
.IsRequired() .IsRequired()
.HasColumnName("rx_type_id"); .HasColumnName("rx_type_id");
builder.Property(e => e.ForeignObjectId)
.IsRequired()
.HasColumnName("foreign_object_id");
builder.Property(e => e.TestPurpose) builder.Property(e => e.TestPurpose)
.HasMaxLength(200) .HasMaxLength(200)
.HasColumnName("test_purpose"); .HasColumnName("test_purpose");
@@ -63,18 +76,31 @@ public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
.HasColumnName("created_at"); .HasColumnName("created_at");
// 索引 // 索引
builder.HasIndex(e => e.ManufacturerId).HasDatabaseName("idx_scenario_manufacturer");
builder.HasIndex(e => e.CarModelId).HasDatabaseName("idx_scenario_car_model");
builder.HasIndex(e => e.TxPanelId).HasDatabaseName("idx_scenario_tx_panel"); builder.HasIndex(e => e.TxPanelId).HasDatabaseName("idx_scenario_tx_panel");
builder.HasIndex(e => e.TxHardwareId).HasDatabaseName("idx_scenario_tx_hardware"); builder.HasIndex(e => e.TxHardwareId).HasDatabaseName("idx_scenario_tx_hardware");
builder.HasIndex(e => e.TxSoftwareId).HasDatabaseName("idx_scenario_tx_software"); builder.HasIndex(e => e.TxSoftwareId).HasDatabaseName("idx_scenario_tx_software");
builder.HasIndex(e => e.RxTypeId).HasDatabaseName("idx_scenario_rx_type"); builder.HasIndex(e => e.RxTypeId).HasDatabaseName("idx_scenario_rx_type");
builder.HasIndex(e => e.ForeignObjectId).HasDatabaseName("idx_scenario_foreign_object");
builder.HasIndex(e => e.TestDate).HasDatabaseName("idx_scenario_test_date"); builder.HasIndex(e => e.TestDate).HasDatabaseName("idx_scenario_test_date");
// 唯一约束 // 唯一约束 - 包含所有维度字段
builder.HasIndex(e => new { e.TxPanelId, e.TxHardwareId, e.TxSoftwareId, e.RxTypeId, e.TestPurpose, e.TestDate, e.TestSequence }) builder.HasIndex(e => new { e.ManufacturerId, e.CarModelId, e.TxPanelId, e.TxHardwareId, e.TxSoftwareId, e.RxTypeId, e.ForeignObjectId, e.TestPurpose, e.TestDate, e.TestSequence })
.IsUnique() .IsUnique()
.HasDatabaseName("uq_scenario"); .HasDatabaseName("uq_scenario");
// 关系配置 - 维度表 // 关系配置 - 新增维度表
builder.HasOne(e => e.Manufacturer)
.WithMany(e => e.TestScenarios)
.HasForeignKey(e => e.ManufacturerId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.CarModel)
.WithMany(e => e.TestScenarios)
.HasForeignKey(e => e.CarModelId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.TxPanel) builder.HasOne(e => e.TxPanel)
.WithMany(e => e.TestScenarios) .WithMany(e => e.TestScenarios)
.HasForeignKey(e => e.TxPanelId) .HasForeignKey(e => e.TxPanelId)
@@ -95,6 +121,11 @@ public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
.HasForeignKey(e => e.RxTypeId) .HasForeignKey(e => e.RxTypeId)
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.ForeignObject)
.WithMany(e => e.TestScenarios)
.HasForeignKey(e => e.ForeignObjectId)
.OnDelete(DeleteBehavior.Restrict);
// 关系配置 - 数据记录 // 关系配置 - 数据记录
builder.HasMany(e => e.QfodRecords) builder.HasMany(e => e.QfodRecords)
.WithOne(e => e.Scenario) .WithOne(e => e.Scenario)
@@ -109,4 +140,4 @@ public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
// 全局查询过滤器 // 全局查询过滤器
builder.HasQueryFilter(e => !e.IsDeleted); builder.HasQueryFilter(e => !e.IsDeleted);
} }
} }

View File

@@ -0,0 +1,186 @@
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
/// <summary>
/// 数据库迁移初始化器。
/// 替代直接调用 <c>EnsureCreated()</c>,统一处理两类场景:
/// <list type="bullet">
/// <item>全新库EF <c>MigrateAsync()</c> 按 InitialCreate 建全部表。</item>
/// <item>既有 <c>EnsureCreated()</c> 库(无 <c>__EFMigrationsHistory</c>,但表已存在):
/// 若直接 <c>MigrateAsync()</c> 会因 <c>CREATE TABLE</c> 撞既有表而失败,
/// 故先检测既有表并补齐本变更新增的 3 张维度表 + <c>test_scenario</c> 的 3 列 NOT NULL FK
/// 再把 InitialCreate 标记为已应用baseline使后续迁移可增量执行。</item>
/// </list>
/// </summary>
public static class DatabaseMigrationInitializer
{
/// <summary>
/// 迁移或升级既有库。对 <typeparamref name="TContext"/> 的库执行迁移;
/// 若检测到既有 EnsureCreated 库(无迁移历史但有表),执行基线升级后标记迁移已应用。
/// </summary>
public static async Task MigrateOrUpgradeAsync<TContext>(TContext context)
where TContext : DbContext
{
var database = context.Database;
// 既有 EnsureCreated 库:无迁移历史表,但已有业务表 → 视为已建表但未迁移。
if (await IsLegacyEnsureCreatedDatabaseAsync(context))
{
await UpgradeLegacyDatabaseAsync(context);
await MarkAllMigrationsAsAppliedAsync(context);
}
// 常规迁移:全新库直接建表;已基线标记的既有库跳过已应用迁移。
await database.MigrateAsync();
}
/// <summary>
/// 判断是否为既有的 EnsureCreated 库:无 __EFMigrationsHistory 表,且至少有一张业务表。
/// </summary>
private static async Task<bool> IsLegacyEnsureCreatedDatabaseAsync<TContext>(TContext context)
where TContext : DbContext
{
// 库不存在视为全新库,交给 MigrateAsync 建表
if (!await context.Database.CanConnectAsync())
return false;
// 存在迁移历史表 → 非 legacy常规 Migrate
var historyCount = await context.Database.SqlQueryRaw<long>(
"SELECT count(*) AS Value FROM sqlite_master WHERE type='table' AND name='__EFMigrationsHistory'").FirstOrDefaultAsync();
if (historyCount > 0)
return false;
// 无历史表但有任意业务表(非 __EFMigrationsHistory→ EnsureCreated 既有库
var tableCount = await context.Database.SqlQueryRaw<long>(
"SELECT count(*) AS Value FROM sqlite_master WHERE type='table' AND name NOT LIKE '__EF%'").FirstOrDefaultAsync();
return tableCount > 0;
}
/// <summary>
/// 升级既有库:补齐本变更新增的维度表与 test_scenario 的 NOT NULL FK 列。
/// 按 "可空加列 → 回填默认维度行 → 改 NOT NULL → 重建唯一索引" 顺序,
/// 兼容 SQLite 无法原地 ALTER COLUMN 的限制(新建默认维度行后回填既有场景)。
/// </summary>
private static async Task UpgradeLegacyDatabaseAsync<TContext>(TContext context)
where TContext : DbContext
{
var db = context.Database;
// 仅处理 WctMinerDbContextReleaseDbContext 无本次 schema 变更)
if (context is not WctMinerDbContext)
return;
// 1. 若已升级(存在 foreign_object 表),直接返回,避免重复
var alreadyUpgraded = await TableExistsAsync(context, "foreign_object");
if (alreadyUpgraded)
return;
// 2. 建三张维度表(结构照搬 ManufacturerConfiguration 等id/name/is_deleted/created_at + 唯一索引)
await db.ExecuteSqlRawAsync(@"
CREATE TABLE IF NOT EXISTS manufacturer (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
is_deleted INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_manufacturer_name ON manufacturer (name) WHERE is_deleted = 0;
CREATE TABLE IF NOT EXISTS car_model (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
is_deleted INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_car_model_name ON car_model (name) WHERE is_deleted = 0;
CREATE TABLE IF NOT EXISTS foreign_object (
id TEXT NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
is_deleted INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_foreign_object_name ON foreign_object (name) WHERE is_deleted = 0;
");
// 3. 给 test_scenario 加可空 FK 列(若已存在则跳过 —— SQLite 不报错时静默)
await AddColumnIfNotExistsAsync(context, "test_scenario", "manufacturer_id", "TEXT");
await AddColumnIfNotExistsAsync(context, "test_scenario", "car_model_id", "TEXT");
await AddColumnIfNotExistsAsync(context, "test_scenario", "foreign_object_id", "TEXT");
// 4. 插入默认维度行("未知")并回填既有 test_scenario 行
await db.ExecuteSqlRawAsync(@"
INSERT OR IGNORE INTO manufacturer (id, name) VALUES (lower(hex(randomblob(16))), '未知');
INSERT OR IGNORE INTO car_model (id, name) VALUES (lower(hex(randomblob(16))), '未知');
INSERT OR IGNORE INTO foreign_object (id, name) VALUES (lower(hex(randomblob(16))), '未知');
UPDATE test_scenario
SET manufacturer_id = (SELECT id FROM manufacturer WHERE name = '未知')
WHERE manufacturer_id IS NULL;
UPDATE test_scenario
SET car_model_id = (SELECT id FROM car_model WHERE name = '未知')
WHERE car_model_id IS NULL;
UPDATE test_scenario
SET foreign_object_id = (SELECT id FROM foreign_object WHERE name = '未知')
WHERE foreign_object_id IS NULL;
");
// 5. 重建唯一索引 uq_scenario 以包含新列(先删后建;既有索引名可能存在)
await db.ExecuteSqlRawAsync("DROP INDEX IF EXISTS uq_scenario;");
await db.ExecuteSqlRawAsync(@"
CREATE UNIQUE INDEX uq_scenario ON test_scenario (
manufacturer_id, car_model_id, tx_panel_id, tx_hardware_id, tx_software_id,
rx_type_id, foreign_object_id, test_purpose, test_date, test_sequence
) WHERE is_deleted = 0;
");
// 6. NOT NULL 约束SQLite 无法原地 ALTEREF 后续 Migrate 的 baseline 会跳过列重建,
// 此处通过重建 test_scenario 表加约束成本高且风险大;既有行已全部回填非空,
// 保留可空状态不影响查询正确性,由后续迁移统一收口。
// (如需严格 NOT NULL需 SQLite 表重建,此处留注释说明取舍。)
}
/// <summary>把当前 context 的所有迁移标记为已应用(写入迁移历史表,跳过执行)。</summary>
private static async Task MarkAllMigrationsAsAppliedAsync<TContext>(TContext context)
where TContext : DbContext
{
var database = context.Database;
// 确保迁移历史表存在(既有 EnsureCreated 库尚无此表),否则 GetPendingMigrationsAsync 会抛
// "no such table: __EFMigrationsHistory"。
await database.ExecuteSqlRawAsync(@"
CREATE TABLE IF NOT EXISTS ""__EFMigrationsHistory"" (
""MigrationId"" TEXT NOT NULL CONSTRAINT ""PK___EFMigrationsHistory"" PRIMARY KEY,
""ProductVersion"" TEXT NOT NULL
);
");
// 列出模型定义的全部迁移,逐条写入历史表(视为已应用),使后续 Migrate 跳过它们。
var migrations = database.GetMigrations().ToList();
var productVersion = typeof(DbContext).Assembly.GetName().Version?.ToString() ?? "8.0.0";
foreach (var migration in migrations)
{
await database.ExecuteSqlRawAsync(
"INSERT OR IGNORE INTO __EFMigrationsHistory (MigrationId, ProductVersion) VALUES ({0}, {1})",
migration, productVersion);
}
}
private static async Task<bool> TableExistsAsync<TContext>(TContext context, string tableName)
where TContext : DbContext
{
var count = await context.Database.SqlQueryRaw<long>(
$"SELECT count(*) AS Value FROM sqlite_master WHERE type='table' AND name = '{tableName}'").FirstOrDefaultAsync();
return count > 0;
}
private static async Task AddColumnIfNotExistsAsync<TContext>(TContext context, string table, string column, string type)
where TContext : DbContext
{
var exists = (await context.Database.SqlQueryRaw<long>(
$"SELECT count(*) AS Value FROM pragma_table_info('{table}') WHERE name = '{column}'").FirstOrDefaultAsync()) > 0;
if (!exists)
await context.Database.ExecuteSqlRawAsync($"ALTER TABLE {table} ADD COLUMN {column} {type};");
}
}

View File

@@ -45,9 +45,8 @@ public class DbContextFactory : IDbContextFactory
var context = new WctMinerDbContext(optionsBuilder.Options); var context = new WctMinerDbContext(optionsBuilder.Options);
// 确保数据库和表结构已创建(适用于 SQLite // 迁移Migrate只在启动期由 Program.Main / CliTestHost.CreateAsync 触发,
context.Database.EnsureCreated(); // 不在每次 scope 解析时执行(避免每条命令都跑迁移)。
return context; return context;
} }

View File

@@ -12,10 +12,13 @@ public class WctMinerDbContext : DbContext
: base(options) { } : base(options) { }
// 维度表 // 维度表
public DbSet<Manufacturer> Manufacturers => Set<Manufacturer>();
public DbSet<CarModel> CarModels => Set<CarModel>();
public DbSet<TxPanel> TxPanels => Set<TxPanel>(); public DbSet<TxPanel> TxPanels => Set<TxPanel>();
public DbSet<TxHardware> TxHardwares => Set<TxHardware>(); public DbSet<TxHardware> TxHardwares => Set<TxHardware>();
public DbSet<TxSoftware> TxSoftwares => Set<TxSoftware>(); public DbSet<TxSoftware> TxSoftwares => Set<TxSoftware>();
public DbSet<RxType> RxTypes => Set<RxType>(); public DbSet<RxType> RxTypes => Set<RxType>();
public DbSet<ForeignObject> ForeignObjects => Set<ForeignObject>();
// 事实表 // 事实表
public DbSet<TestScenario> TestScenarios => Set<TestScenario>(); public DbSet<TestScenario> TestScenarios => Set<TestScenario>();

View File

@@ -0,0 +1,805 @@
// <auto-generated />
using System;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Gpulse.WCT.DataAnalyzer.Core.Migrations
{
[DbContext(typeof(WctMinerDbContext))]
[Migration("20260821085318_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.CarModel", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("car_model", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.ForeignObject", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("foreign_object", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.Manufacturer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("manufacturer", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.PlossRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<int?>("DeltaP")
.HasColumnType("INTEGER")
.HasColumnName("delta_p");
b.Property<int>("Field1")
.HasColumnType("INTEGER")
.HasColumnName("field_1");
b.Property<int>("Field10")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_10");
b.Property<int>("Field11")
.HasColumnType("INTEGER")
.HasColumnName("field_11");
b.Property<int>("Field12")
.HasColumnType("INTEGER")
.HasColumnName("field_12");
b.Property<int>("Field13")
.HasColumnType("INTEGER")
.HasColumnName("field_13");
b.Property<int>("Field14")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_14");
b.Property<int>("Field15")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_15");
b.Property<int>("Field16")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_16");
b.Property<int>("Field2")
.HasColumnType("INTEGER")
.HasColumnName("field_2");
b.Property<int>("Field3")
.HasColumnType("INTEGER")
.HasColumnName("field_3");
b.Property<int>("Field4")
.HasColumnType("INTEGER")
.HasColumnName("field_4");
b.Property<int>("Field5")
.HasColumnType("INTEGER")
.HasColumnName("field_5");
b.Property<int>("Field6")
.HasColumnType("INTEGER")
.HasColumnName("field_6");
b.Property<int>("Field7")
.HasColumnType("INTEGER")
.HasColumnName("field_7");
b.Property<int>("Field8")
.HasColumnType("INTEGER")
.HasColumnName("field_8");
b.Property<int>("Field9")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_9");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<int?>("PowLoss")
.HasColumnType("INTEGER")
.HasColumnName("pow_loss");
b.Property<Guid>("ScenarioId")
.HasColumnType("TEXT")
.HasColumnName("scenario_id");
b.HasKey("Id");
b.HasIndex("DeltaP")
.HasDatabaseName("idx_ploss_delta_p");
b.HasIndex("Field10")
.HasDatabaseName("idx_ploss_field_10");
b.HasIndex("Field2")
.HasDatabaseName("idx_ploss_field_2");
b.HasIndex("Field7")
.HasDatabaseName("idx_ploss_field_7");
b.HasIndex("Field8")
.HasDatabaseName("idx_ploss_field_8");
b.HasIndex("PowLoss")
.HasDatabaseName("idx_ploss_pow_loss");
b.HasIndex("ScenarioId")
.HasDatabaseName("idx_ploss_scenario");
b.ToTable("ploss_record", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.QfodRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<byte>("ChargerIndex")
.HasColumnType("INTEGER")
.HasColumnName("charger_index");
b.Property<byte>("CoilIndex")
.HasColumnType("INTEGER")
.HasColumnName("coil_index");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<float>("CurrentQ")
.HasColumnType("REAL")
.HasColumnName("current_q");
b.Property<int>("DeltaQ")
.HasColumnType("INTEGER")
.HasColumnName("delta_q");
b.Property<byte>("FieldF")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue((byte)0)
.HasColumnName("field_f");
b.Property<byte>("FodType")
.HasColumnType("INTEGER")
.HasColumnName("fod_type");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<float>("RawQ")
.HasColumnType("REAL")
.HasColumnName("raw_q");
b.Property<Guid>("ScenarioId")
.HasColumnType("TEXT")
.HasColumnName("scenario_id");
b.HasKey("Id");
b.HasIndex("DeltaQ")
.HasDatabaseName("idx_qfod_delta_q");
b.HasIndex("FodType")
.HasDatabaseName("idx_qfod_fod_type");
b.HasIndex("ScenarioId")
.HasDatabaseName("idx_qfod_scenario");
b.HasIndex("ChargerIndex", "CoilIndex")
.HasDatabaseName("idx_qfod_charger_coil");
b.ToTable("qfod_record", null, t =>
{
t.HasCheckConstraint("chk_qfod_charger", "charger_index IN (0, 1)");
t.HasCheckConstraint("chk_qfod_coil", "coil_index IN (0, 1, 2)");
});
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.RxType", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("rx_type", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<Guid>("CarModelId")
.HasColumnType("TEXT")
.HasColumnName("car_model_id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<Guid>("ForeignObjectId")
.HasColumnType("TEXT")
.HasColumnName("foreign_object_id");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<Guid>("ManufacturerId")
.HasColumnType("TEXT")
.HasColumnName("manufacturer_id");
b.Property<int>("PlossCount")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("ploss_count");
b.Property<int>("QfodCount")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("qfod_count");
b.Property<Guid>("RxTypeId")
.HasColumnType("TEXT")
.HasColumnName("rx_type_id");
b.Property<DateOnly>("TestDate")
.HasColumnType("TEXT")
.HasColumnName("test_date");
b.Property<string>("TestPurpose")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("test_purpose");
b.Property<int>("TestSequence")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1)
.HasColumnName("test_sequence");
b.Property<Guid>("TxHardwareId")
.HasColumnType("TEXT")
.HasColumnName("tx_hardware_id");
b.Property<Guid>("TxPanelId")
.HasColumnType("TEXT")
.HasColumnName("tx_panel_id");
b.Property<Guid>("TxSoftwareId")
.HasColumnType("TEXT")
.HasColumnName("tx_software_id");
b.HasKey("Id");
b.HasIndex("CarModelId")
.HasDatabaseName("idx_scenario_car_model");
b.HasIndex("ForeignObjectId")
.HasDatabaseName("idx_scenario_foreign_object");
b.HasIndex("ManufacturerId")
.HasDatabaseName("idx_scenario_manufacturer");
b.HasIndex("RxTypeId")
.HasDatabaseName("idx_scenario_rx_type");
b.HasIndex("TestDate")
.HasDatabaseName("idx_scenario_test_date");
b.HasIndex("TxHardwareId")
.HasDatabaseName("idx_scenario_tx_hardware");
b.HasIndex("TxPanelId")
.HasDatabaseName("idx_scenario_tx_panel");
b.HasIndex("TxSoftwareId")
.HasDatabaseName("idx_scenario_tx_software");
b.HasIndex("ManufacturerId", "CarModelId", "TxPanelId", "TxHardwareId", "TxSoftwareId", "RxTypeId", "ForeignObjectId", "TestPurpose", "TestDate", "TestSequence")
.IsUnique()
.HasDatabaseName("uq_scenario");
b.ToTable("test_scenario", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxHardware", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("version");
b.HasKey("Id");
b.HasIndex("Version")
.IsUnique();
b.ToTable("tx_hardware", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxPanel", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("tx_panel", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxSoftware", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("version");
b.HasKey("Id");
b.HasIndex("Version")
.IsUnique();
b.ToTable("tx_software", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Release.ChargingParameterRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("CarFactory")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("car_factory");
b.Property<string>("CarModel")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("car_model");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("PhoneBrand")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("phone_brand");
b.Property<string>("PhoneModel")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("phone_model");
b.Property<double?>("Power1000mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1000mw");
b.Property<double?>("Power1250mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1250mw");
b.Property<double?>("Power1500mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1500mw");
b.Property<double?>("Power1750mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1750mw");
b.Property<double?>("Power2000mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_2000mw");
b.Property<double?>("Power2250mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_2250mw");
b.Property<double?>("Power350mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_350mw");
b.Property<double?>("Power500mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_500mw");
b.Property<double?>("Power750mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_750mw");
b.Property<double?>("PqCoefficient")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("pq_coefficient");
b.Property<double?>("QBaseValue")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("q_base_value");
b.Property<double?>("QValue")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("q_value");
b.Property<double?>("ResonanceFrequency")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("resonance_frequency");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasColumnName("updated_at");
b.HasKey("Id");
b.HasIndex("CarFactory", "CarModel", "PhoneBrand", "PhoneModel")
.IsUnique()
.HasDatabaseName("uq_charging_parameter_business_key");
b.ToTable("charging_parameter", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.PlossRecord", b =>
{
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", "Scenario")
.WithMany("PlossRecords")
.HasForeignKey("ScenarioId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scenario");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.QfodRecord", b =>
{
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", "Scenario")
.WithMany("QfodRecords")
.HasForeignKey("ScenarioId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scenario");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", b =>
{
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.CarModel", "CarModel")
.WithMany("TestScenarios")
.HasForeignKey("CarModelId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.ForeignObject", "ForeignObject")
.WithMany("TestScenarios")
.HasForeignKey("ForeignObjectId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.Manufacturer", "Manufacturer")
.WithMany("TestScenarios")
.HasForeignKey("ManufacturerId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.RxType", "RxType")
.WithMany("TestScenarios")
.HasForeignKey("RxTypeId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxHardware", "TxHardware")
.WithMany("TestScenarios")
.HasForeignKey("TxHardwareId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxPanel", "TxPanel")
.WithMany("TestScenarios")
.HasForeignKey("TxPanelId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxSoftware", "TxSoftware")
.WithMany("TestScenarios")
.HasForeignKey("TxSoftwareId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CarModel");
b.Navigation("ForeignObject");
b.Navigation("Manufacturer");
b.Navigation("RxType");
b.Navigation("TxHardware");
b.Navigation("TxPanel");
b.Navigation("TxSoftware");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.CarModel", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.ForeignObject", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.Manufacturer", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.RxType", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", b =>
{
b.Navigation("PlossRecords");
b.Navigation("QfodRecords");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxHardware", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxPanel", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxSoftware", b =>
{
b.Navigation("TestScenarios");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,463 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Gpulse.WCT.DataAnalyzer.Core.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "car_model",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
name = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_car_model", x => x.id);
});
migrationBuilder.CreateTable(
name: "charging_parameter",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
car_factory = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
car_model = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
phone_brand = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
phone_model = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
power_350mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_500mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_750mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_1000mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_1250mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_1500mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_1750mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_2000mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_2250mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
q_value = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
q_base_value = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
pq_coefficient = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
resonance_frequency = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false),
updated_at = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_charging_parameter", x => x.id);
});
migrationBuilder.CreateTable(
name: "foreign_object",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
name = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_foreign_object", x => x.id);
});
migrationBuilder.CreateTable(
name: "manufacturer",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_manufacturer", x => x.id);
});
migrationBuilder.CreateTable(
name: "rx_type",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_rx_type", x => x.id);
});
migrationBuilder.CreateTable(
name: "tx_hardware",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
version = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_tx_hardware", x => x.id);
});
migrationBuilder.CreateTable(
name: "tx_panel",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
name = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_tx_panel", x => x.id);
});
migrationBuilder.CreateTable(
name: "tx_software",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
version = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_tx_software", x => x.id);
});
migrationBuilder.CreateTable(
name: "test_scenario",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
manufacturer_id = table.Column<Guid>(type: "TEXT", nullable: false),
car_model_id = table.Column<Guid>(type: "TEXT", nullable: false),
foreign_object_id = table.Column<Guid>(type: "TEXT", nullable: false),
tx_panel_id = table.Column<Guid>(type: "TEXT", nullable: false),
tx_hardware_id = table.Column<Guid>(type: "TEXT", nullable: false),
tx_software_id = table.Column<Guid>(type: "TEXT", nullable: false),
rx_type_id = table.Column<Guid>(type: "TEXT", nullable: false),
test_purpose = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
test_date = table.Column<DateOnly>(type: "TEXT", nullable: false),
test_sequence = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 1),
qfod_count = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
ploss_count = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_test_scenario", x => x.id);
table.ForeignKey(
name: "FK_test_scenario_car_model_car_model_id",
column: x => x.car_model_id,
principalTable: "car_model",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_test_scenario_foreign_object_foreign_object_id",
column: x => x.foreign_object_id,
principalTable: "foreign_object",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_test_scenario_manufacturer_manufacturer_id",
column: x => x.manufacturer_id,
principalTable: "manufacturer",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_test_scenario_rx_type_rx_type_id",
column: x => x.rx_type_id,
principalTable: "rx_type",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_test_scenario_tx_hardware_tx_hardware_id",
column: x => x.tx_hardware_id,
principalTable: "tx_hardware",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_test_scenario_tx_panel_tx_panel_id",
column: x => x.tx_panel_id,
principalTable: "tx_panel",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_test_scenario_tx_software_tx_software_id",
column: x => x.tx_software_id,
principalTable: "tx_software",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "ploss_record",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
scenario_id = table.Column<Guid>(type: "TEXT", nullable: false),
pow_loss = table.Column<int>(type: "INTEGER", nullable: true),
delta_p = table.Column<int>(type: "INTEGER", nullable: true),
field_1 = table.Column<int>(type: "INTEGER", nullable: false),
field_2 = table.Column<int>(type: "INTEGER", nullable: false),
field_3 = table.Column<int>(type: "INTEGER", nullable: false),
field_4 = table.Column<int>(type: "INTEGER", nullable: false),
field_5 = table.Column<int>(type: "INTEGER", nullable: false),
field_6 = table.Column<int>(type: "INTEGER", nullable: false),
field_7 = table.Column<int>(type: "INTEGER", nullable: false),
field_8 = table.Column<int>(type: "INTEGER", nullable: false),
field_9 = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
field_10 = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
field_11 = table.Column<int>(type: "INTEGER", nullable: false),
field_12 = table.Column<int>(type: "INTEGER", nullable: false),
field_13 = table.Column<int>(type: "INTEGER", nullable: false),
field_14 = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
field_15 = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
field_16 = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_ploss_record", x => x.id);
table.ForeignKey(
name: "FK_ploss_record_test_scenario_scenario_id",
column: x => x.scenario_id,
principalTable: "test_scenario",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "qfod_record",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
scenario_id = table.Column<Guid>(type: "TEXT", nullable: false),
charger_index = table.Column<byte>(type: "INTEGER", nullable: false),
coil_index = table.Column<byte>(type: "INTEGER", nullable: false),
delta_q = table.Column<int>(type: "INTEGER", nullable: false),
current_q = table.Column<float>(type: "REAL", nullable: false),
raw_q = table.Column<float>(type: "REAL", nullable: false),
fod_type = table.Column<byte>(type: "INTEGER", nullable: false),
field_f = table.Column<byte>(type: "INTEGER", nullable: false, defaultValue: (byte)0),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_qfod_record", x => x.id);
table.CheckConstraint("chk_qfod_charger", "charger_index IN (0, 1)");
table.CheckConstraint("chk_qfod_coil", "coil_index IN (0, 1, 2)");
table.ForeignKey(
name: "FK_qfod_record_test_scenario_scenario_id",
column: x => x.scenario_id,
principalTable: "test_scenario",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_car_model_name",
table: "car_model",
column: "name",
unique: true);
migrationBuilder.CreateIndex(
name: "uq_charging_parameter_business_key",
table: "charging_parameter",
columns: new[] { "car_factory", "car_model", "phone_brand", "phone_model" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_foreign_object_name",
table: "foreign_object",
column: "name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_manufacturer_name",
table: "manufacturer",
column: "name",
unique: true);
migrationBuilder.CreateIndex(
name: "idx_ploss_delta_p",
table: "ploss_record",
column: "delta_p");
migrationBuilder.CreateIndex(
name: "idx_ploss_field_10",
table: "ploss_record",
column: "field_10");
migrationBuilder.CreateIndex(
name: "idx_ploss_field_2",
table: "ploss_record",
column: "field_2");
migrationBuilder.CreateIndex(
name: "idx_ploss_field_7",
table: "ploss_record",
column: "field_7");
migrationBuilder.CreateIndex(
name: "idx_ploss_field_8",
table: "ploss_record",
column: "field_8");
migrationBuilder.CreateIndex(
name: "idx_ploss_pow_loss",
table: "ploss_record",
column: "pow_loss");
migrationBuilder.CreateIndex(
name: "idx_ploss_scenario",
table: "ploss_record",
column: "scenario_id");
migrationBuilder.CreateIndex(
name: "idx_qfod_charger_coil",
table: "qfod_record",
columns: new[] { "charger_index", "coil_index" });
migrationBuilder.CreateIndex(
name: "idx_qfod_delta_q",
table: "qfod_record",
column: "delta_q");
migrationBuilder.CreateIndex(
name: "idx_qfod_fod_type",
table: "qfod_record",
column: "fod_type");
migrationBuilder.CreateIndex(
name: "idx_qfod_scenario",
table: "qfod_record",
column: "scenario_id");
migrationBuilder.CreateIndex(
name: "IX_rx_type_name",
table: "rx_type",
column: "name",
unique: true);
migrationBuilder.CreateIndex(
name: "idx_scenario_car_model",
table: "test_scenario",
column: "car_model_id");
migrationBuilder.CreateIndex(
name: "idx_scenario_foreign_object",
table: "test_scenario",
column: "foreign_object_id");
migrationBuilder.CreateIndex(
name: "idx_scenario_manufacturer",
table: "test_scenario",
column: "manufacturer_id");
migrationBuilder.CreateIndex(
name: "idx_scenario_rx_type",
table: "test_scenario",
column: "rx_type_id");
migrationBuilder.CreateIndex(
name: "idx_scenario_test_date",
table: "test_scenario",
column: "test_date");
migrationBuilder.CreateIndex(
name: "idx_scenario_tx_hardware",
table: "test_scenario",
column: "tx_hardware_id");
migrationBuilder.CreateIndex(
name: "idx_scenario_tx_panel",
table: "test_scenario",
column: "tx_panel_id");
migrationBuilder.CreateIndex(
name: "idx_scenario_tx_software",
table: "test_scenario",
column: "tx_software_id");
migrationBuilder.CreateIndex(
name: "uq_scenario",
table: "test_scenario",
columns: new[] { "manufacturer_id", "car_model_id", "tx_panel_id", "tx_hardware_id", "tx_software_id", "rx_type_id", "foreign_object_id", "test_purpose", "test_date", "test_sequence" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_tx_hardware_version",
table: "tx_hardware",
column: "version",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_tx_panel_name",
table: "tx_panel",
column: "name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_tx_software_version",
table: "tx_software",
column: "version",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "charging_parameter");
migrationBuilder.DropTable(
name: "ploss_record");
migrationBuilder.DropTable(
name: "qfod_record");
migrationBuilder.DropTable(
name: "test_scenario");
migrationBuilder.DropTable(
name: "car_model");
migrationBuilder.DropTable(
name: "foreign_object");
migrationBuilder.DropTable(
name: "manufacturer");
migrationBuilder.DropTable(
name: "rx_type");
migrationBuilder.DropTable(
name: "tx_hardware");
migrationBuilder.DropTable(
name: "tx_panel");
migrationBuilder.DropTable(
name: "tx_software");
}
}
}

View File

@@ -0,0 +1,805 @@
// <auto-generated />
using System;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Gpulse.WCT.DataAnalyzer.Core.Migrations.ReleaseData
{
[DbContext(typeof(ReleaseDbContext))]
[Migration("20260821085806_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.CarModel", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("car_model", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.ForeignObject", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("foreign_object", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.Manufacturer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("manufacturer", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.PlossRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<int?>("DeltaP")
.HasColumnType("INTEGER")
.HasColumnName("delta_p");
b.Property<int>("Field1")
.HasColumnType("INTEGER")
.HasColumnName("field_1");
b.Property<int>("Field10")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_10");
b.Property<int>("Field11")
.HasColumnType("INTEGER")
.HasColumnName("field_11");
b.Property<int>("Field12")
.HasColumnType("INTEGER")
.HasColumnName("field_12");
b.Property<int>("Field13")
.HasColumnType("INTEGER")
.HasColumnName("field_13");
b.Property<int>("Field14")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_14");
b.Property<int>("Field15")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_15");
b.Property<int>("Field16")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_16");
b.Property<int>("Field2")
.HasColumnType("INTEGER")
.HasColumnName("field_2");
b.Property<int>("Field3")
.HasColumnType("INTEGER")
.HasColumnName("field_3");
b.Property<int>("Field4")
.HasColumnType("INTEGER")
.HasColumnName("field_4");
b.Property<int>("Field5")
.HasColumnType("INTEGER")
.HasColumnName("field_5");
b.Property<int>("Field6")
.HasColumnType("INTEGER")
.HasColumnName("field_6");
b.Property<int>("Field7")
.HasColumnType("INTEGER")
.HasColumnName("field_7");
b.Property<int>("Field8")
.HasColumnType("INTEGER")
.HasColumnName("field_8");
b.Property<int>("Field9")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_9");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<int?>("PowLoss")
.HasColumnType("INTEGER")
.HasColumnName("pow_loss");
b.Property<Guid>("ScenarioId")
.HasColumnType("TEXT")
.HasColumnName("scenario_id");
b.HasKey("Id");
b.HasIndex("DeltaP")
.HasDatabaseName("idx_ploss_delta_p");
b.HasIndex("Field10")
.HasDatabaseName("idx_ploss_field_10");
b.HasIndex("Field2")
.HasDatabaseName("idx_ploss_field_2");
b.HasIndex("Field7")
.HasDatabaseName("idx_ploss_field_7");
b.HasIndex("Field8")
.HasDatabaseName("idx_ploss_field_8");
b.HasIndex("PowLoss")
.HasDatabaseName("idx_ploss_pow_loss");
b.HasIndex("ScenarioId")
.HasDatabaseName("idx_ploss_scenario");
b.ToTable("ploss_record", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.QfodRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<byte>("ChargerIndex")
.HasColumnType("INTEGER")
.HasColumnName("charger_index");
b.Property<byte>("CoilIndex")
.HasColumnType("INTEGER")
.HasColumnName("coil_index");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<float>("CurrentQ")
.HasColumnType("REAL")
.HasColumnName("current_q");
b.Property<int>("DeltaQ")
.HasColumnType("INTEGER")
.HasColumnName("delta_q");
b.Property<byte>("FieldF")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue((byte)0)
.HasColumnName("field_f");
b.Property<byte>("FodType")
.HasColumnType("INTEGER")
.HasColumnName("fod_type");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<float>("RawQ")
.HasColumnType("REAL")
.HasColumnName("raw_q");
b.Property<Guid>("ScenarioId")
.HasColumnType("TEXT")
.HasColumnName("scenario_id");
b.HasKey("Id");
b.HasIndex("DeltaQ")
.HasDatabaseName("idx_qfod_delta_q");
b.HasIndex("FodType")
.HasDatabaseName("idx_qfod_fod_type");
b.HasIndex("ScenarioId")
.HasDatabaseName("idx_qfod_scenario");
b.HasIndex("ChargerIndex", "CoilIndex")
.HasDatabaseName("idx_qfod_charger_coil");
b.ToTable("qfod_record", null, t =>
{
t.HasCheckConstraint("chk_qfod_charger", "charger_index IN (0, 1)");
t.HasCheckConstraint("chk_qfod_coil", "coil_index IN (0, 1, 2)");
});
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.RxType", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("rx_type", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<Guid>("CarModelId")
.HasColumnType("TEXT")
.HasColumnName("car_model_id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<Guid>("ForeignObjectId")
.HasColumnType("TEXT")
.HasColumnName("foreign_object_id");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<Guid>("ManufacturerId")
.HasColumnType("TEXT")
.HasColumnName("manufacturer_id");
b.Property<int>("PlossCount")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("ploss_count");
b.Property<int>("QfodCount")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("qfod_count");
b.Property<Guid>("RxTypeId")
.HasColumnType("TEXT")
.HasColumnName("rx_type_id");
b.Property<DateOnly>("TestDate")
.HasColumnType("TEXT")
.HasColumnName("test_date");
b.Property<string>("TestPurpose")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("test_purpose");
b.Property<int>("TestSequence")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1)
.HasColumnName("test_sequence");
b.Property<Guid>("TxHardwareId")
.HasColumnType("TEXT")
.HasColumnName("tx_hardware_id");
b.Property<Guid>("TxPanelId")
.HasColumnType("TEXT")
.HasColumnName("tx_panel_id");
b.Property<Guid>("TxSoftwareId")
.HasColumnType("TEXT")
.HasColumnName("tx_software_id");
b.HasKey("Id");
b.HasIndex("CarModelId")
.HasDatabaseName("idx_scenario_car_model");
b.HasIndex("ForeignObjectId")
.HasDatabaseName("idx_scenario_foreign_object");
b.HasIndex("ManufacturerId")
.HasDatabaseName("idx_scenario_manufacturer");
b.HasIndex("RxTypeId")
.HasDatabaseName("idx_scenario_rx_type");
b.HasIndex("TestDate")
.HasDatabaseName("idx_scenario_test_date");
b.HasIndex("TxHardwareId")
.HasDatabaseName("idx_scenario_tx_hardware");
b.HasIndex("TxPanelId")
.HasDatabaseName("idx_scenario_tx_panel");
b.HasIndex("TxSoftwareId")
.HasDatabaseName("idx_scenario_tx_software");
b.HasIndex("ManufacturerId", "CarModelId", "TxPanelId", "TxHardwareId", "TxSoftwareId", "RxTypeId", "ForeignObjectId", "TestPurpose", "TestDate", "TestSequence")
.IsUnique()
.HasDatabaseName("uq_scenario");
b.ToTable("test_scenario", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxHardware", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("version");
b.HasKey("Id");
b.HasIndex("Version")
.IsUnique();
b.ToTable("tx_hardware", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxPanel", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("tx_panel", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxSoftware", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("version");
b.HasKey("Id");
b.HasIndex("Version")
.IsUnique();
b.ToTable("tx_software", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Release.ChargingParameterRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("CarFactory")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("car_factory");
b.Property<string>("CarModel")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("car_model");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("PhoneBrand")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("phone_brand");
b.Property<string>("PhoneModel")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("phone_model");
b.Property<double?>("Power1000mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1000mw");
b.Property<double?>("Power1250mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1250mw");
b.Property<double?>("Power1500mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1500mw");
b.Property<double?>("Power1750mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1750mw");
b.Property<double?>("Power2000mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_2000mw");
b.Property<double?>("Power2250mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_2250mw");
b.Property<double?>("Power350mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_350mw");
b.Property<double?>("Power500mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_500mw");
b.Property<double?>("Power750mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_750mw");
b.Property<double?>("PqCoefficient")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("pq_coefficient");
b.Property<double?>("QBaseValue")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("q_base_value");
b.Property<double?>("QValue")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("q_value");
b.Property<double?>("ResonanceFrequency")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("resonance_frequency");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasColumnName("updated_at");
b.HasKey("Id");
b.HasIndex("CarFactory", "CarModel", "PhoneBrand", "PhoneModel")
.IsUnique()
.HasDatabaseName("uq_charging_parameter_business_key");
b.ToTable("charging_parameter", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.PlossRecord", b =>
{
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", "Scenario")
.WithMany("PlossRecords")
.HasForeignKey("ScenarioId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scenario");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.QfodRecord", b =>
{
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", "Scenario")
.WithMany("QfodRecords")
.HasForeignKey("ScenarioId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scenario");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", b =>
{
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.CarModel", "CarModel")
.WithMany("TestScenarios")
.HasForeignKey("CarModelId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.ForeignObject", "ForeignObject")
.WithMany("TestScenarios")
.HasForeignKey("ForeignObjectId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.Manufacturer", "Manufacturer")
.WithMany("TestScenarios")
.HasForeignKey("ManufacturerId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.RxType", "RxType")
.WithMany("TestScenarios")
.HasForeignKey("RxTypeId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxHardware", "TxHardware")
.WithMany("TestScenarios")
.HasForeignKey("TxHardwareId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxPanel", "TxPanel")
.WithMany("TestScenarios")
.HasForeignKey("TxPanelId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxSoftware", "TxSoftware")
.WithMany("TestScenarios")
.HasForeignKey("TxSoftwareId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CarModel");
b.Navigation("ForeignObject");
b.Navigation("Manufacturer");
b.Navigation("RxType");
b.Navigation("TxHardware");
b.Navigation("TxPanel");
b.Navigation("TxSoftware");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.CarModel", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.ForeignObject", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.Manufacturer", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.RxType", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", b =>
{
b.Navigation("PlossRecords");
b.Navigation("QfodRecords");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxHardware", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxPanel", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxSoftware", b =>
{
b.Navigation("TestScenarios");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,463 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Gpulse.WCT.DataAnalyzer.Core.Migrations.ReleaseData
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "car_model",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
name = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_car_model", x => x.id);
});
migrationBuilder.CreateTable(
name: "charging_parameter",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
car_factory = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
car_model = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
phone_brand = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
phone_model = table.Column<string>(type: "TEXT", maxLength: 128, nullable: false),
power_350mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_500mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_750mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_1000mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_1250mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_1500mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_1750mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_2000mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
power_2250mw = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
q_value = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
q_base_value = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
pq_coefficient = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
resonance_frequency = table.Column<double>(type: "REAL", precision: 18, scale: 6, nullable: true),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false),
updated_at = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_charging_parameter", x => x.id);
});
migrationBuilder.CreateTable(
name: "foreign_object",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
name = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_foreign_object", x => x.id);
});
migrationBuilder.CreateTable(
name: "manufacturer",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_manufacturer", x => x.id);
});
migrationBuilder.CreateTable(
name: "rx_type",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_rx_type", x => x.id);
});
migrationBuilder.CreateTable(
name: "tx_hardware",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
version = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_tx_hardware", x => x.id);
});
migrationBuilder.CreateTable(
name: "tx_panel",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
name = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_tx_panel", x => x.id);
});
migrationBuilder.CreateTable(
name: "tx_software",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
version = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_tx_software", x => x.id);
});
migrationBuilder.CreateTable(
name: "test_scenario",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
manufacturer_id = table.Column<Guid>(type: "TEXT", nullable: false),
car_model_id = table.Column<Guid>(type: "TEXT", nullable: false),
foreign_object_id = table.Column<Guid>(type: "TEXT", nullable: false),
tx_panel_id = table.Column<Guid>(type: "TEXT", nullable: false),
tx_hardware_id = table.Column<Guid>(type: "TEXT", nullable: false),
tx_software_id = table.Column<Guid>(type: "TEXT", nullable: false),
rx_type_id = table.Column<Guid>(type: "TEXT", nullable: false),
test_purpose = table.Column<string>(type: "TEXT", maxLength: 200, nullable: true),
test_date = table.Column<DateOnly>(type: "TEXT", nullable: false),
test_sequence = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 1),
qfod_count = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
ploss_count = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_test_scenario", x => x.id);
table.ForeignKey(
name: "FK_test_scenario_car_model_car_model_id",
column: x => x.car_model_id,
principalTable: "car_model",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_test_scenario_foreign_object_foreign_object_id",
column: x => x.foreign_object_id,
principalTable: "foreign_object",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_test_scenario_manufacturer_manufacturer_id",
column: x => x.manufacturer_id,
principalTable: "manufacturer",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_test_scenario_rx_type_rx_type_id",
column: x => x.rx_type_id,
principalTable: "rx_type",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_test_scenario_tx_hardware_tx_hardware_id",
column: x => x.tx_hardware_id,
principalTable: "tx_hardware",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_test_scenario_tx_panel_tx_panel_id",
column: x => x.tx_panel_id,
principalTable: "tx_panel",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_test_scenario_tx_software_tx_software_id",
column: x => x.tx_software_id,
principalTable: "tx_software",
principalColumn: "id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "ploss_record",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
scenario_id = table.Column<Guid>(type: "TEXT", nullable: false),
pow_loss = table.Column<int>(type: "INTEGER", nullable: true),
delta_p = table.Column<int>(type: "INTEGER", nullable: true),
field_1 = table.Column<int>(type: "INTEGER", nullable: false),
field_2 = table.Column<int>(type: "INTEGER", nullable: false),
field_3 = table.Column<int>(type: "INTEGER", nullable: false),
field_4 = table.Column<int>(type: "INTEGER", nullable: false),
field_5 = table.Column<int>(type: "INTEGER", nullable: false),
field_6 = table.Column<int>(type: "INTEGER", nullable: false),
field_7 = table.Column<int>(type: "INTEGER", nullable: false),
field_8 = table.Column<int>(type: "INTEGER", nullable: false),
field_9 = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
field_10 = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
field_11 = table.Column<int>(type: "INTEGER", nullable: false),
field_12 = table.Column<int>(type: "INTEGER", nullable: false),
field_13 = table.Column<int>(type: "INTEGER", nullable: false),
field_14 = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
field_15 = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
field_16 = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_ploss_record", x => x.id);
table.ForeignKey(
name: "FK_ploss_record_test_scenario_scenario_id",
column: x => x.scenario_id,
principalTable: "test_scenario",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "qfod_record",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
scenario_id = table.Column<Guid>(type: "TEXT", nullable: false),
charger_index = table.Column<byte>(type: "INTEGER", nullable: false),
coil_index = table.Column<byte>(type: "INTEGER", nullable: false),
delta_q = table.Column<int>(type: "INTEGER", nullable: false),
current_q = table.Column<float>(type: "REAL", nullable: false),
raw_q = table.Column<float>(type: "REAL", nullable: false),
fod_type = table.Column<byte>(type: "INTEGER", nullable: false),
field_f = table.Column<byte>(type: "INTEGER", nullable: false, defaultValue: (byte)0),
is_deleted = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
created_at = table.Column<DateTime>(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')")
},
constraints: table =>
{
table.PrimaryKey("PK_qfod_record", x => x.id);
table.CheckConstraint("chk_qfod_charger", "charger_index IN (0, 1)");
table.CheckConstraint("chk_qfod_coil", "coil_index IN (0, 1, 2)");
table.ForeignKey(
name: "FK_qfod_record_test_scenario_scenario_id",
column: x => x.scenario_id,
principalTable: "test_scenario",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_car_model_name",
table: "car_model",
column: "name",
unique: true);
migrationBuilder.CreateIndex(
name: "uq_charging_parameter_business_key",
table: "charging_parameter",
columns: new[] { "car_factory", "car_model", "phone_brand", "phone_model" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_foreign_object_name",
table: "foreign_object",
column: "name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_manufacturer_name",
table: "manufacturer",
column: "name",
unique: true);
migrationBuilder.CreateIndex(
name: "idx_ploss_delta_p",
table: "ploss_record",
column: "delta_p");
migrationBuilder.CreateIndex(
name: "idx_ploss_field_10",
table: "ploss_record",
column: "field_10");
migrationBuilder.CreateIndex(
name: "idx_ploss_field_2",
table: "ploss_record",
column: "field_2");
migrationBuilder.CreateIndex(
name: "idx_ploss_field_7",
table: "ploss_record",
column: "field_7");
migrationBuilder.CreateIndex(
name: "idx_ploss_field_8",
table: "ploss_record",
column: "field_8");
migrationBuilder.CreateIndex(
name: "idx_ploss_pow_loss",
table: "ploss_record",
column: "pow_loss");
migrationBuilder.CreateIndex(
name: "idx_ploss_scenario",
table: "ploss_record",
column: "scenario_id");
migrationBuilder.CreateIndex(
name: "idx_qfod_charger_coil",
table: "qfod_record",
columns: new[] { "charger_index", "coil_index" });
migrationBuilder.CreateIndex(
name: "idx_qfod_delta_q",
table: "qfod_record",
column: "delta_q");
migrationBuilder.CreateIndex(
name: "idx_qfod_fod_type",
table: "qfod_record",
column: "fod_type");
migrationBuilder.CreateIndex(
name: "idx_qfod_scenario",
table: "qfod_record",
column: "scenario_id");
migrationBuilder.CreateIndex(
name: "IX_rx_type_name",
table: "rx_type",
column: "name",
unique: true);
migrationBuilder.CreateIndex(
name: "idx_scenario_car_model",
table: "test_scenario",
column: "car_model_id");
migrationBuilder.CreateIndex(
name: "idx_scenario_foreign_object",
table: "test_scenario",
column: "foreign_object_id");
migrationBuilder.CreateIndex(
name: "idx_scenario_manufacturer",
table: "test_scenario",
column: "manufacturer_id");
migrationBuilder.CreateIndex(
name: "idx_scenario_rx_type",
table: "test_scenario",
column: "rx_type_id");
migrationBuilder.CreateIndex(
name: "idx_scenario_test_date",
table: "test_scenario",
column: "test_date");
migrationBuilder.CreateIndex(
name: "idx_scenario_tx_hardware",
table: "test_scenario",
column: "tx_hardware_id");
migrationBuilder.CreateIndex(
name: "idx_scenario_tx_panel",
table: "test_scenario",
column: "tx_panel_id");
migrationBuilder.CreateIndex(
name: "idx_scenario_tx_software",
table: "test_scenario",
column: "tx_software_id");
migrationBuilder.CreateIndex(
name: "uq_scenario",
table: "test_scenario",
columns: new[] { "manufacturer_id", "car_model_id", "tx_panel_id", "tx_hardware_id", "tx_software_id", "rx_type_id", "foreign_object_id", "test_purpose", "test_date", "test_sequence" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_tx_hardware_version",
table: "tx_hardware",
column: "version",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_tx_panel_name",
table: "tx_panel",
column: "name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_tx_software_version",
table: "tx_software",
column: "version",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "charging_parameter");
migrationBuilder.DropTable(
name: "ploss_record");
migrationBuilder.DropTable(
name: "qfod_record");
migrationBuilder.DropTable(
name: "test_scenario");
migrationBuilder.DropTable(
name: "car_model");
migrationBuilder.DropTable(
name: "foreign_object");
migrationBuilder.DropTable(
name: "manufacturer");
migrationBuilder.DropTable(
name: "rx_type");
migrationBuilder.DropTable(
name: "tx_hardware");
migrationBuilder.DropTable(
name: "tx_panel");
migrationBuilder.DropTable(
name: "tx_software");
}
}
}

View File

@@ -0,0 +1,802 @@
// <auto-generated />
using System;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Gpulse.WCT.DataAnalyzer.Core.Migrations.ReleaseData
{
[DbContext(typeof(ReleaseDbContext))]
partial class ReleaseDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.CarModel", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("car_model", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.ForeignObject", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("foreign_object", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.Manufacturer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("manufacturer", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.PlossRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<int?>("DeltaP")
.HasColumnType("INTEGER")
.HasColumnName("delta_p");
b.Property<int>("Field1")
.HasColumnType("INTEGER")
.HasColumnName("field_1");
b.Property<int>("Field10")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_10");
b.Property<int>("Field11")
.HasColumnType("INTEGER")
.HasColumnName("field_11");
b.Property<int>("Field12")
.HasColumnType("INTEGER")
.HasColumnName("field_12");
b.Property<int>("Field13")
.HasColumnType("INTEGER")
.HasColumnName("field_13");
b.Property<int>("Field14")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_14");
b.Property<int>("Field15")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_15");
b.Property<int>("Field16")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_16");
b.Property<int>("Field2")
.HasColumnType("INTEGER")
.HasColumnName("field_2");
b.Property<int>("Field3")
.HasColumnType("INTEGER")
.HasColumnName("field_3");
b.Property<int>("Field4")
.HasColumnType("INTEGER")
.HasColumnName("field_4");
b.Property<int>("Field5")
.HasColumnType("INTEGER")
.HasColumnName("field_5");
b.Property<int>("Field6")
.HasColumnType("INTEGER")
.HasColumnName("field_6");
b.Property<int>("Field7")
.HasColumnType("INTEGER")
.HasColumnName("field_7");
b.Property<int>("Field8")
.HasColumnType("INTEGER")
.HasColumnName("field_8");
b.Property<int>("Field9")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_9");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<int?>("PowLoss")
.HasColumnType("INTEGER")
.HasColumnName("pow_loss");
b.Property<Guid>("ScenarioId")
.HasColumnType("TEXT")
.HasColumnName("scenario_id");
b.HasKey("Id");
b.HasIndex("DeltaP")
.HasDatabaseName("idx_ploss_delta_p");
b.HasIndex("Field10")
.HasDatabaseName("idx_ploss_field_10");
b.HasIndex("Field2")
.HasDatabaseName("idx_ploss_field_2");
b.HasIndex("Field7")
.HasDatabaseName("idx_ploss_field_7");
b.HasIndex("Field8")
.HasDatabaseName("idx_ploss_field_8");
b.HasIndex("PowLoss")
.HasDatabaseName("idx_ploss_pow_loss");
b.HasIndex("ScenarioId")
.HasDatabaseName("idx_ploss_scenario");
b.ToTable("ploss_record", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.QfodRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<byte>("ChargerIndex")
.HasColumnType("INTEGER")
.HasColumnName("charger_index");
b.Property<byte>("CoilIndex")
.HasColumnType("INTEGER")
.HasColumnName("coil_index");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<float>("CurrentQ")
.HasColumnType("REAL")
.HasColumnName("current_q");
b.Property<int>("DeltaQ")
.HasColumnType("INTEGER")
.HasColumnName("delta_q");
b.Property<byte>("FieldF")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue((byte)0)
.HasColumnName("field_f");
b.Property<byte>("FodType")
.HasColumnType("INTEGER")
.HasColumnName("fod_type");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<float>("RawQ")
.HasColumnType("REAL")
.HasColumnName("raw_q");
b.Property<Guid>("ScenarioId")
.HasColumnType("TEXT")
.HasColumnName("scenario_id");
b.HasKey("Id");
b.HasIndex("DeltaQ")
.HasDatabaseName("idx_qfod_delta_q");
b.HasIndex("FodType")
.HasDatabaseName("idx_qfod_fod_type");
b.HasIndex("ScenarioId")
.HasDatabaseName("idx_qfod_scenario");
b.HasIndex("ChargerIndex", "CoilIndex")
.HasDatabaseName("idx_qfod_charger_coil");
b.ToTable("qfod_record", null, t =>
{
t.HasCheckConstraint("chk_qfod_charger", "charger_index IN (0, 1)");
t.HasCheckConstraint("chk_qfod_coil", "coil_index IN (0, 1, 2)");
});
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.RxType", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("rx_type", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<Guid>("CarModelId")
.HasColumnType("TEXT")
.HasColumnName("car_model_id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<Guid>("ForeignObjectId")
.HasColumnType("TEXT")
.HasColumnName("foreign_object_id");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<Guid>("ManufacturerId")
.HasColumnType("TEXT")
.HasColumnName("manufacturer_id");
b.Property<int>("PlossCount")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("ploss_count");
b.Property<int>("QfodCount")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("qfod_count");
b.Property<Guid>("RxTypeId")
.HasColumnType("TEXT")
.HasColumnName("rx_type_id");
b.Property<DateOnly>("TestDate")
.HasColumnType("TEXT")
.HasColumnName("test_date");
b.Property<string>("TestPurpose")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("test_purpose");
b.Property<int>("TestSequence")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1)
.HasColumnName("test_sequence");
b.Property<Guid>("TxHardwareId")
.HasColumnType("TEXT")
.HasColumnName("tx_hardware_id");
b.Property<Guid>("TxPanelId")
.HasColumnType("TEXT")
.HasColumnName("tx_panel_id");
b.Property<Guid>("TxSoftwareId")
.HasColumnType("TEXT")
.HasColumnName("tx_software_id");
b.HasKey("Id");
b.HasIndex("CarModelId")
.HasDatabaseName("idx_scenario_car_model");
b.HasIndex("ForeignObjectId")
.HasDatabaseName("idx_scenario_foreign_object");
b.HasIndex("ManufacturerId")
.HasDatabaseName("idx_scenario_manufacturer");
b.HasIndex("RxTypeId")
.HasDatabaseName("idx_scenario_rx_type");
b.HasIndex("TestDate")
.HasDatabaseName("idx_scenario_test_date");
b.HasIndex("TxHardwareId")
.HasDatabaseName("idx_scenario_tx_hardware");
b.HasIndex("TxPanelId")
.HasDatabaseName("idx_scenario_tx_panel");
b.HasIndex("TxSoftwareId")
.HasDatabaseName("idx_scenario_tx_software");
b.HasIndex("ManufacturerId", "CarModelId", "TxPanelId", "TxHardwareId", "TxSoftwareId", "RxTypeId", "ForeignObjectId", "TestPurpose", "TestDate", "TestSequence")
.IsUnique()
.HasDatabaseName("uq_scenario");
b.ToTable("test_scenario", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxHardware", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("version");
b.HasKey("Id");
b.HasIndex("Version")
.IsUnique();
b.ToTable("tx_hardware", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxPanel", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("tx_panel", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxSoftware", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("version");
b.HasKey("Id");
b.HasIndex("Version")
.IsUnique();
b.ToTable("tx_software", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Release.ChargingParameterRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("CarFactory")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("car_factory");
b.Property<string>("CarModel")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("car_model");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("PhoneBrand")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("phone_brand");
b.Property<string>("PhoneModel")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("phone_model");
b.Property<double?>("Power1000mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1000mw");
b.Property<double?>("Power1250mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1250mw");
b.Property<double?>("Power1500mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1500mw");
b.Property<double?>("Power1750mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1750mw");
b.Property<double?>("Power2000mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_2000mw");
b.Property<double?>("Power2250mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_2250mw");
b.Property<double?>("Power350mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_350mw");
b.Property<double?>("Power500mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_500mw");
b.Property<double?>("Power750mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_750mw");
b.Property<double?>("PqCoefficient")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("pq_coefficient");
b.Property<double?>("QBaseValue")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("q_base_value");
b.Property<double?>("QValue")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("q_value");
b.Property<double?>("ResonanceFrequency")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("resonance_frequency");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasColumnName("updated_at");
b.HasKey("Id");
b.HasIndex("CarFactory", "CarModel", "PhoneBrand", "PhoneModel")
.IsUnique()
.HasDatabaseName("uq_charging_parameter_business_key");
b.ToTable("charging_parameter", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.PlossRecord", b =>
{
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", "Scenario")
.WithMany("PlossRecords")
.HasForeignKey("ScenarioId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scenario");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.QfodRecord", b =>
{
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", "Scenario")
.WithMany("QfodRecords")
.HasForeignKey("ScenarioId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scenario");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", b =>
{
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.CarModel", "CarModel")
.WithMany("TestScenarios")
.HasForeignKey("CarModelId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.ForeignObject", "ForeignObject")
.WithMany("TestScenarios")
.HasForeignKey("ForeignObjectId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.Manufacturer", "Manufacturer")
.WithMany("TestScenarios")
.HasForeignKey("ManufacturerId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.RxType", "RxType")
.WithMany("TestScenarios")
.HasForeignKey("RxTypeId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxHardware", "TxHardware")
.WithMany("TestScenarios")
.HasForeignKey("TxHardwareId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxPanel", "TxPanel")
.WithMany("TestScenarios")
.HasForeignKey("TxPanelId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxSoftware", "TxSoftware")
.WithMany("TestScenarios")
.HasForeignKey("TxSoftwareId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CarModel");
b.Navigation("ForeignObject");
b.Navigation("Manufacturer");
b.Navigation("RxType");
b.Navigation("TxHardware");
b.Navigation("TxPanel");
b.Navigation("TxSoftware");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.CarModel", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.ForeignObject", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.Manufacturer", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.RxType", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", b =>
{
b.Navigation("PlossRecords");
b.Navigation("QfodRecords");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxHardware", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxPanel", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxSoftware", b =>
{
b.Navigation("TestScenarios");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,802 @@
// <auto-generated />
using System;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Gpulse.WCT.DataAnalyzer.Core.Migrations
{
[DbContext(typeof(WctMinerDbContext))]
partial class WctMinerDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.CarModel", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("car_model", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.ForeignObject", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("foreign_object", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.Manufacturer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("manufacturer", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.PlossRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<int?>("DeltaP")
.HasColumnType("INTEGER")
.HasColumnName("delta_p");
b.Property<int>("Field1")
.HasColumnType("INTEGER")
.HasColumnName("field_1");
b.Property<int>("Field10")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_10");
b.Property<int>("Field11")
.HasColumnType("INTEGER")
.HasColumnName("field_11");
b.Property<int>("Field12")
.HasColumnType("INTEGER")
.HasColumnName("field_12");
b.Property<int>("Field13")
.HasColumnType("INTEGER")
.HasColumnName("field_13");
b.Property<int>("Field14")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_14");
b.Property<int>("Field15")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_15");
b.Property<int>("Field16")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_16");
b.Property<int>("Field2")
.HasColumnType("INTEGER")
.HasColumnName("field_2");
b.Property<int>("Field3")
.HasColumnType("INTEGER")
.HasColumnName("field_3");
b.Property<int>("Field4")
.HasColumnType("INTEGER")
.HasColumnName("field_4");
b.Property<int>("Field5")
.HasColumnType("INTEGER")
.HasColumnName("field_5");
b.Property<int>("Field6")
.HasColumnType("INTEGER")
.HasColumnName("field_6");
b.Property<int>("Field7")
.HasColumnType("INTEGER")
.HasColumnName("field_7");
b.Property<int>("Field8")
.HasColumnType("INTEGER")
.HasColumnName("field_8");
b.Property<int>("Field9")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("field_9");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<int?>("PowLoss")
.HasColumnType("INTEGER")
.HasColumnName("pow_loss");
b.Property<Guid>("ScenarioId")
.HasColumnType("TEXT")
.HasColumnName("scenario_id");
b.HasKey("Id");
b.HasIndex("DeltaP")
.HasDatabaseName("idx_ploss_delta_p");
b.HasIndex("Field10")
.HasDatabaseName("idx_ploss_field_10");
b.HasIndex("Field2")
.HasDatabaseName("idx_ploss_field_2");
b.HasIndex("Field7")
.HasDatabaseName("idx_ploss_field_7");
b.HasIndex("Field8")
.HasDatabaseName("idx_ploss_field_8");
b.HasIndex("PowLoss")
.HasDatabaseName("idx_ploss_pow_loss");
b.HasIndex("ScenarioId")
.HasDatabaseName("idx_ploss_scenario");
b.ToTable("ploss_record", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.QfodRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<byte>("ChargerIndex")
.HasColumnType("INTEGER")
.HasColumnName("charger_index");
b.Property<byte>("CoilIndex")
.HasColumnType("INTEGER")
.HasColumnName("coil_index");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<float>("CurrentQ")
.HasColumnType("REAL")
.HasColumnName("current_q");
b.Property<int>("DeltaQ")
.HasColumnType("INTEGER")
.HasColumnName("delta_q");
b.Property<byte>("FieldF")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue((byte)0)
.HasColumnName("field_f");
b.Property<byte>("FodType")
.HasColumnType("INTEGER")
.HasColumnName("fod_type");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<float>("RawQ")
.HasColumnType("REAL")
.HasColumnName("raw_q");
b.Property<Guid>("ScenarioId")
.HasColumnType("TEXT")
.HasColumnName("scenario_id");
b.HasKey("Id");
b.HasIndex("DeltaQ")
.HasDatabaseName("idx_qfod_delta_q");
b.HasIndex("FodType")
.HasDatabaseName("idx_qfod_fod_type");
b.HasIndex("ScenarioId")
.HasDatabaseName("idx_qfod_scenario");
b.HasIndex("ChargerIndex", "CoilIndex")
.HasDatabaseName("idx_qfod_charger_coil");
b.ToTable("qfod_record", null, t =>
{
t.HasCheckConstraint("chk_qfod_charger", "charger_index IN (0, 1)");
t.HasCheckConstraint("chk_qfod_coil", "coil_index IN (0, 1, 2)");
});
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.RxType", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("rx_type", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<Guid>("CarModelId")
.HasColumnType("TEXT")
.HasColumnName("car_model_id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<Guid>("ForeignObjectId")
.HasColumnType("TEXT")
.HasColumnName("foreign_object_id");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<Guid>("ManufacturerId")
.HasColumnType("TEXT")
.HasColumnName("manufacturer_id");
b.Property<int>("PlossCount")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("ploss_count");
b.Property<int>("QfodCount")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(0)
.HasColumnName("qfod_count");
b.Property<Guid>("RxTypeId")
.HasColumnType("TEXT")
.HasColumnName("rx_type_id");
b.Property<DateOnly>("TestDate")
.HasColumnType("TEXT")
.HasColumnName("test_date");
b.Property<string>("TestPurpose")
.HasMaxLength(200)
.HasColumnType("TEXT")
.HasColumnName("test_purpose");
b.Property<int>("TestSequence")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(1)
.HasColumnName("test_sequence");
b.Property<Guid>("TxHardwareId")
.HasColumnType("TEXT")
.HasColumnName("tx_hardware_id");
b.Property<Guid>("TxPanelId")
.HasColumnType("TEXT")
.HasColumnName("tx_panel_id");
b.Property<Guid>("TxSoftwareId")
.HasColumnType("TEXT")
.HasColumnName("tx_software_id");
b.HasKey("Id");
b.HasIndex("CarModelId")
.HasDatabaseName("idx_scenario_car_model");
b.HasIndex("ForeignObjectId")
.HasDatabaseName("idx_scenario_foreign_object");
b.HasIndex("ManufacturerId")
.HasDatabaseName("idx_scenario_manufacturer");
b.HasIndex("RxTypeId")
.HasDatabaseName("idx_scenario_rx_type");
b.HasIndex("TestDate")
.HasDatabaseName("idx_scenario_test_date");
b.HasIndex("TxHardwareId")
.HasDatabaseName("idx_scenario_tx_hardware");
b.HasIndex("TxPanelId")
.HasDatabaseName("idx_scenario_tx_panel");
b.HasIndex("TxSoftwareId")
.HasDatabaseName("idx_scenario_tx_software");
b.HasIndex("ManufacturerId", "CarModelId", "TxPanelId", "TxHardwareId", "TxSoftwareId", "RxTypeId", "ForeignObjectId", "TestPurpose", "TestDate", "TestSequence")
.IsUnique()
.HasDatabaseName("uq_scenario");
b.ToTable("test_scenario", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxHardware", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("version");
b.HasKey("Id");
b.HasIndex("Version")
.IsUnique();
b.ToTable("tx_hardware", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxPanel", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("name");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("tx_panel", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxSoftware", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("created_at")
.HasDefaultValueSql("datetime('now')");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false)
.HasColumnName("is_deleted");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT")
.HasColumnName("version");
b.HasKey("Id");
b.HasIndex("Version")
.IsUnique();
b.ToTable("tx_software", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Release.ChargingParameterRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("CarFactory")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("car_factory");
b.Property<string>("CarModel")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("car_model");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT")
.HasColumnName("created_at");
b.Property<string>("PhoneBrand")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("phone_brand");
b.Property<string>("PhoneModel")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("TEXT")
.HasColumnName("phone_model");
b.Property<double?>("Power1000mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1000mw");
b.Property<double?>("Power1250mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1250mw");
b.Property<double?>("Power1500mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1500mw");
b.Property<double?>("Power1750mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_1750mw");
b.Property<double?>("Power2000mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_2000mw");
b.Property<double?>("Power2250mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_2250mw");
b.Property<double?>("Power350mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_350mw");
b.Property<double?>("Power500mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_500mw");
b.Property<double?>("Power750mW")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("power_750mw");
b.Property<double?>("PqCoefficient")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("pq_coefficient");
b.Property<double?>("QBaseValue")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("q_base_value");
b.Property<double?>("QValue")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("q_value");
b.Property<double?>("ResonanceFrequency")
.HasPrecision(18, 6)
.HasColumnType("REAL")
.HasColumnName("resonance_frequency");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT")
.HasColumnName("updated_at");
b.HasKey("Id");
b.HasIndex("CarFactory", "CarModel", "PhoneBrand", "PhoneModel")
.IsUnique()
.HasDatabaseName("uq_charging_parameter_business_key");
b.ToTable("charging_parameter", (string)null);
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.PlossRecord", b =>
{
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", "Scenario")
.WithMany("PlossRecords")
.HasForeignKey("ScenarioId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scenario");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.QfodRecord", b =>
{
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", "Scenario")
.WithMany("QfodRecords")
.HasForeignKey("ScenarioId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Scenario");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", b =>
{
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.CarModel", "CarModel")
.WithMany("TestScenarios")
.HasForeignKey("CarModelId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.ForeignObject", "ForeignObject")
.WithMany("TestScenarios")
.HasForeignKey("ForeignObjectId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.Manufacturer", "Manufacturer")
.WithMany("TestScenarios")
.HasForeignKey("ManufacturerId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.RxType", "RxType")
.WithMany("TestScenarios")
.HasForeignKey("RxTypeId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxHardware", "TxHardware")
.WithMany("TestScenarios")
.HasForeignKey("TxHardwareId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxPanel", "TxPanel")
.WithMany("TestScenarios")
.HasForeignKey("TxPanelId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxSoftware", "TxSoftware")
.WithMany("TestScenarios")
.HasForeignKey("TxSoftwareId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("CarModel");
b.Navigation("ForeignObject");
b.Navigation("Manufacturer");
b.Navigation("RxType");
b.Navigation("TxHardware");
b.Navigation("TxPanel");
b.Navigation("TxSoftware");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.CarModel", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.ForeignObject", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.Manufacturer", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.RxType", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TestScenario", b =>
{
b.Navigation("PlossRecords");
b.Navigation("QfodRecords");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxHardware", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxPanel", b =>
{
b.Navigation("TestScenarios");
});
modelBuilder.Entity("Gpulse.WCT.DataAnalyzer.Core.Domain.Local.TxSoftware", b =>
{
b.Navigation("TestScenarios");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,5 +1,6 @@
using System.CommandLine; using System.CommandLine;
using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis; using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
using Gpulse.WCT.DataAnalyzer.Core.Application.Exporting;
namespace Gpulse.WCT.DataAnalyzer.Commands; namespace Gpulse.WCT.DataAnalyzer.Commands;
@@ -8,9 +9,12 @@ namespace Gpulse.WCT.DataAnalyzer.Commands;
/// </summary> /// </summary>
public class AnalyzeCommand : Command public class AnalyzeCommand : Command
{ {
private static readonly string[] LogExtensions = [".DAT", ".dat", ".LOG", ".log"];
public AnalyzeCommand( public AnalyzeCommand(
PlossAnalysisService plossAnalysisService, PlossAnalysisService plossAnalysisService,
QfodCalibrationService qfodCalibrationService) QfodCalibrationService qfodCalibrationService,
AnalyzeExcelExporter excelExporter)
: base("analyze", "Analyze Ploss margin or Qfod calibration threshold from log files") : base("analyze", "Analyze Ploss margin or Qfod calibration threshold from log files")
{ {
var typeOption = new Option<string>( var typeOption = new Option<string>(
@@ -36,31 +40,246 @@ public class AnalyzeCommand : Command
"Qfod: path to the phone + foreign object charging log file" "Qfod: path to the phone + foreign object charging log file"
); );
var outputOption = new Option<string?>(
new[] { "--output", "-o" },
"Output Excel file path (default: same directory as input with .xlsx extension)"
);
var autoOption = new Option<bool>(
"--auto",
"Auto scan Datas folder and analyze all files"
);
AddOption(typeOption); AddOption(typeOption);
AddOption(fileOption); AddOption(fileOption);
AddOption(pureOption); AddOption(pureOption);
AddOption(foreignOption); AddOption(foreignOption);
AddOption(outputOption);
AddOption(autoOption);
this.SetHandler(async (type, file, pure, foreign) => this.SetHandler(async (type, file, pure, foreign, output, auto) =>
{ {
type = type.ToLower(); type = type.ToLower();
// 自动扫描模式
if (auto || (string.IsNullOrEmpty(file) && string.IsNullOrEmpty(pure) && string.IsNullOrEmpty(foreign)))
{
await HandleAutoScanAsync(plossAnalysisService, qfodCalibrationService, excelExporter, type);
return;
}
switch (type) switch (type)
{ {
case "ploss": case "ploss":
await HandlePlossAsync(plossAnalysisService, file); await HandlePlossAsync(plossAnalysisService, excelExporter, file, output);
break; break;
case "qfod": case "qfod":
await HandleQfodAsync(qfodCalibrationService, pure, foreign); await HandleQfodAsync(qfodCalibrationService, excelExporter, pure, foreign, output);
break; break;
default: default:
Console.WriteLine($"Unknown type: {type}. Use 'ploss' or 'qfod'."); Console.WriteLine($"Unknown type: {type}. Use 'ploss' or 'qfod'.");
break; break;
} }
}, typeOption, fileOption, pureOption, foreignOption); }, typeOption, fileOption, pureOption, foreignOption, outputOption, autoOption);
} }
private static async Task HandlePlossAsync(PlossAnalysisService service, string? file) /// <summary>
/// 自动扫描 Datas 文件夹并分析
/// </summary>
private static async Task HandleAutoScanAsync(
PlossAnalysisService plossService,
QfodCalibrationService qfodService,
AnalyzeExcelExporter exporter,
string type)
{
var dataDir = Path.Combine(AppContext.BaseDirectory, "Datas");
if (!Directory.Exists(dataDir))
{
Console.WriteLine($"[错误] 未找到 Datas 文件夹: {dataDir}");
Console.WriteLine("请创建 Datas 文件夹并放入日志文件,或手动指定文件路径。");
return;
}
// 扫描日志文件(包含子目录)
var allFiles = LogExtensions
.SelectMany(ext => Directory.GetFiles(dataDir, $"*{ext}", SearchOption.AllDirectories))
.ToList();
if (allFiles.Count == 0)
{
Console.WriteLine($"[错误] Datas 文件夹中未找到日志文件");
return;
}
// 分类文件
var pureFiles = new List<string>();
var foreignFiles = new List<string>();
foreach (var filePath in allFiles)
{
var foreignType = GetForeignObjectType(Path.GetFileNameWithoutExtension(filePath));
if (foreignType == "无")
pureFiles.Add(filePath);
else
foreignFiles.Add(filePath);
}
Console.WriteLine($"\n扫描 Datas 文件夹:");
Console.WriteLine($" 总计: {allFiles.Count} 个日志文件");
Console.WriteLine($" 纯手机: {pureFiles.Count} 个");
Console.WriteLine($" 异物: {foreignFiles.Count} 个");
Console.WriteLine();
switch (type)
{
case "ploss":
Console.WriteLine("=== Ploss 分析 ===");
int plossOk = 0, plossFailed = 0;
foreach (var filePath in allFiles)
{
try
{
await HandlePlossAsync(plossService, exporter, filePath, null);
plossOk++;
}
catch (Exception ex)
{
plossFailed++;
Console.WriteLine($"[FAILED] {Path.GetFileName(filePath)}: {ex.Message}");
}
}
Console.WriteLine($"\n[完成] 已分析 {plossOk} 个文件{(plossFailed > 0 ? $" {plossFailed} " : "")}");
break;
case "qfod":
if (pureFiles.Count == 0)
{
Console.WriteLine("[错误] 未找到纯手机日志文件名第7段为\"无\"");
return;
}
if (foreignFiles.Count == 0)
{
Console.WriteLine("[错误] 未找到异物日志文件名第7段不为\"无\"");
return;
}
Console.WriteLine("=== Qfod 标定 ===");
if (pureFiles.Count == 1 && foreignFiles.Count == 1)
{
try
{
await HandleQfodAsync(qfodService, exporter, pureFiles[0], foreignFiles[0], null);
}
catch (Exception ex)
{
Console.WriteLine($"[FAILED] {Path.GetFileName(pureFiles[0])}: {ex.Message}");
}
Console.WriteLine("\n[完成] 已分析 1 个配对");
}
else
{
int pairCount = 0, qfodOk = 0, qfodFailed = 0;
foreach (var pure in pureFiles)
{
foreach (var foreign in foreignFiles)
{
pairCount++;
var outputPath = GetOutputPath(pure, null, $"qfod_{pairCount}");
try
{
await HandleQfodAsync(qfodService, exporter, pure, foreign, outputPath);
qfodOk++;
}
catch (Exception ex)
{
qfodFailed++;
Console.WriteLine($"[FAILED] {Path.GetFileName(pure)} + {Path.GetFileName(foreign)}: {ex.Message}");
}
}
}
Console.WriteLine($"\n[完成] 已分析 {qfodOk} 个配对{(qfodFailed > 0 ? $" {qfodFailed} " : "")}");
}
break;
default:
Console.WriteLine($"Unknown type: {type}. Use 'ploss' or 'qfod'.");
break;
}
}
/// <summary>
/// 从文件名提取异物类型第7段
/// 格式: 车厂-车型-TX面板-TX硬件-TX软件-RX类型-异物类型-测试目的-年月日测试次数
/// </summary>
private static string GetForeignObjectType(string fileNameWithoutExt)
{
var parts = fileNameWithoutExt.Split('-');
return parts.Length >= 7 ? parts[6] : "";
}
/// <summary>
/// 获取输出目录exe 同目录下的 Output 文件夹)
/// </summary>
private static string GetOutputDirectory()
{
var outputDir = Path.Combine(AppContext.BaseDirectory, "Output");
if (!Directory.Exists(outputDir))
Directory.CreateDirectory(outputDir);
return outputDir;
}
/// <summary>
/// 净化来自文件名的路径段:剔除路径分隔符、遍历段、非法字符,防止 Datas/ 下的文件名
/// 经 Path.Combine 写到 Output/ 之外(路径遍历)。不可信输入必须经此处理。
/// </summary>
private static string SafeSegment(string segment)
{
if (string.IsNullOrWhiteSpace(segment))
return "Unknown";
var invalid = Path.GetInvalidFileNameChars()
.Concat(['/', '\\', ':', '*', '?', '"', '<', '>', '|']);
var cleaned = new string(segment.Where(c => !invalid.Contains(c)).ToArray());
return string.IsNullOrWhiteSpace(cleaned) ? "Unknown" : cleaned;
}
/// <summary>
/// 根据文件名解析出目录结构Output/{车厂}/{车型}/{手机名}/
/// 文件名格式: 车厂-车型-TX面板-TX硬件-TX软件-RX类型-异物类型-测试目的-年月日测试次数
/// </summary>
private static string GetOutputPath(string inputFile, string? customOutput = null, string? suffix = null)
{
if (customOutput != null)
return customOutput;
var fileName = Path.GetFileNameWithoutExtension(inputFile);
var parts = fileName.Split('-');
// 解析车厂、车型、手机名(RX类型),经 SafeSegment 净化防止路径遍历
var manufacturer = parts.Length >= 1 ? SafeSegment(parts[0]) : "Unknown";
var carModel = parts.Length >= 2 ? SafeSegment(parts[1]) : "Unknown";
var phoneName = parts.Length >= 6 ? SafeSegment(parts[5]) : "Unknown";
// 构建输出目录
var outputDir = Path.Combine(GetOutputDirectory(), manufacturer, carModel, phoneName);
if (!Directory.Exists(outputDir))
Directory.CreateDirectory(outputDir);
// 构建输出文件名
var outputFileName = suffix != null
? $"{fileName}_{suffix}.xlsx"
: $"{fileName}.xlsx";
return Path.Combine(outputDir, outputFileName);
}
private static async Task HandlePlossAsync(
PlossAnalysisService service,
AnalyzeExcelExporter exporter,
string? file,
string? output)
{ {
if (string.IsNullOrEmpty(file)) if (string.IsNullOrEmpty(file))
{ {
@@ -68,48 +287,27 @@ public class AnalyzeCommand : Command
return; return;
} }
var report = await service.AnalyzeFileAsync(file); var excelReport = await service.AnalyzeForExcelAsync(file);
if (!report.IsSuccess) if (!excelReport.IsSuccess)
{ {
Console.WriteLine($" [{report.FileName}] FAILED: {report.ErrorMessage}"); Console.WriteLine($"[FAILED] {excelReport.FileName}: {excelReport.ErrorMessage}");
return; return;
} }
Console.WriteLine($"\n=== Ploss Analysis: {report.FileName} ==="); var outputPath = GetOutputPath(file, output);
Console.WriteLine($"Total: {report.TotalCount} (TwoLine: {report.TwoLineCount}, Legacy: {report.LegacyCount}), FOD: {report.FodCount}, Errors: {report.ErrorCount}");
var normalRatio = Percent(report.NormalAboveCount, report.NormalCount); await exporter.ExportPlossReportAsync(excelReport, outputPath);
var fodRatio = Percent(report.FodAboveCount, report.FodCount); Console.WriteLine($"[OK] {outputPath}");
Console.WriteLine($"Normal margin>2000: {report.NormalAboveCount}/{report.NormalCount} ({normalRatio}%)"); Console.WriteLine($"Total: {excelReport.TotalCount}, Dropped: {excelReport.DroppedCount}, Errors: {excelReport.ErrorCount}");
Console.WriteLine($"FOD margin>2000: {report.FodAboveCount}/{report.FodCount} ({fodRatio}%)");
Console.WriteLine($"Identity (delta_p = Tx - Rx - pow_loss - ploss): {report.IdentityHeldCount}/{report.IdentityCheckedCount}");
if (report.FodRows.Count == 0)
{
Console.WriteLine("No FOD rows (Field12 == 1).");
return;
}
Console.WriteLine($"\nFOD rows (delta_p back-solve):");
foreach (var row in report.FodRows)
{
var format = row.IsTwoLine ? "two-line" : "legacy";
Console.WriteLine($" Line {row.LineNumber,-6} [{format}] pow_loss={row.PowLoss?.ToString() ?? "-"} delta_p={row.DeltaP?.ToString() ?? "-"} ploss={row.Ploss} threshold={row.Threshold} margin={row.Margin}");
if (row.IsTwoLine && row.DeltaPPrime.HasValue)
{
Console.WriteLine($" -> delta_p'={row.DeltaPPrime.Value}");
Console.WriteLine($" rewrite: {row.RewrittenHeader}");
}
else
{
Console.WriteLine($" (旧单行格式无 header无法反解 delta_p)");
}
}
} }
private static async Task HandleQfodAsync(QfodCalibrationService service, string? pure, string? foreign) private static async Task HandleQfodAsync(
QfodCalibrationService service,
AnalyzeExcelExporter exporter,
string? pure,
string? foreign,
string? output)
{ {
if (string.IsNullOrEmpty(pure) || string.IsNullOrEmpty(foreign)) if (string.IsNullOrEmpty(pure) || string.IsNullOrEmpty(foreign))
{ {
@@ -117,28 +315,17 @@ public class AnalyzeCommand : Command
return; return;
} }
var report = await service.CalibrateAsync(pure, foreign); var excelReport = await service.CalibrateForExcelAsync(pure, foreign);
if (!report.IsSuccess) if (!excelReport.IsSuccess)
{ {
Console.WriteLine($" FAILED: {report.ErrorMessage}"); Console.WriteLine($"[FAILED] {excelReport.ErrorMessage}");
return; return;
} }
Console.WriteLine("\n=== Qfod Calibration ==="); var outputPath = output ?? GetOutputPath(pure, output, "qfod");
PrintQfodFileSummary("Pure phone", report.Pure!);
PrintQfodFileSummary("Foreign object", report.Foreign!);
Console.WriteLine($"\nThreshold = (pureMax + foreignMin) / 2 = ({report.Pure!.Max} + {report.Foreign!.Min}) / 2 = {report.Threshold}"); await exporter.ExportQfodReportAsync(excelReport, outputPath);
Console.WriteLine($"Margin: pureMax - threshold = {report.PureMargin} (负为安全), foreignMin - threshold = {report.ForeignMargin} (正为安全)"); Console.WriteLine($"[OK] {outputPath}");
} }
private static void PrintQfodFileSummary(string label, QfodFileSummary summary)
{
Console.WriteLine($" {label}: {summary.FileName}");
Console.WriteLine($" count={summary.Count} min={summary.Min} max={summary.Max} avg={summary.Average:F1}");
}
private static string Percent(int part, int total)
=> total == 0 ? "0.0" : (part * 100.0 / total).ToString("F1");
} }

View File

@@ -8,8 +8,15 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion> <LangVersion>12.0</LangVersion>
<!-- 版本信息 -->
<Version>1.1.0</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="Gpulse.WCT.DataAnalyzer.Tests" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<!-- CLI --> <!-- CLI -->
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
@@ -36,9 +43,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Update="appsettings.json"> <EmbeddedResource Include="appsettings.json" />
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </ItemGroup>
</None>
<ItemGroup>
<None Include="analyze.bat" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,4 +1,7 @@
using System.CommandLine; using System.CommandLine;
using System.Reflection;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
@@ -8,6 +11,7 @@ using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions; using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
using Gpulse.WCT.DataAnalyzer.Core.Application; using Gpulse.WCT.DataAnalyzer.Core.Application;
using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis; using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
using Gpulse.WCT.DataAnalyzer.Core.Application.Exporting;
namespace Gpulse.WCT.DataAnalyzer; namespace Gpulse.WCT.DataAnalyzer;
@@ -48,14 +52,14 @@ public class Program
var app = builder.Build(); var app = builder.Build();
// 初始化数据库 // 初始化数据库(经 EF 迁移建表/升级,替代 EnsureCreated 以支持既有库升级)
using (var scope = app.Services.CreateScope()) using (var scope = app.Services.CreateScope())
{ {
var dbContext = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>(); var dbContext = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
await dbContext.Database.EnsureCreatedAsync(); await DatabaseMigrationInitializer.MigrateOrUpgradeAsync(dbContext);
var releaseContext = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>(); var releaseContext = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>();
await releaseContext.Database.EnsureCreatedAsync(); await DatabaseMigrationInitializer.MigrateOrUpgradeAsync(releaseContext);
// 种子数据 // 种子数据
SeedData.Initialize(dbContext); SeedData.Initialize(dbContext);
@@ -73,7 +77,8 @@ public class Program
rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService<CleanService>())); rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService<CleanService>()));
rootCommand.AddCommand(new AnalyzeCommand( rootCommand.AddCommand(new AnalyzeCommand(
provider.GetRequiredService<PlossAnalysisService>(), provider.GetRequiredService<PlossAnalysisService>(),
provider.GetRequiredService<QfodCalibrationService>())); provider.GetRequiredService<QfodCalibrationService>(),
provider.GetRequiredService<AnalyzeExcelExporter>()));
return await rootCommand.InvokeAsync(args); return await rootCommand.InvokeAsync(args);
} }
@@ -97,8 +102,16 @@ public class Program
{ {
var builder = new ConfigurationBuilder() var builder = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory) .SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false) .AddEmbeddedResourceJson(
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true) Assembly.GetExecutingAssembly(),
"Gpulse.WCT.DataAnalyzer.appsettings.json")
.AddJsonFile(
Path.Combine(AppContext.BaseDirectory, "appsettings.json"),
optional: true, reloadOnChange: false)
.AddJsonFile(
Path.Combine(AppContext.BaseDirectory,
$"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json"),
optional: true, reloadOnChange: false)
.AddEnvironmentVariables() .AddEnvironmentVariables()
.AddCommandLine(args); .AddCommandLine(args);
@@ -163,3 +176,84 @@ public class Program
: Path.GetFullPath(Path.Combine(baseDirectory, value)); : Path.GetFullPath(Path.Combine(baseDirectory, value));
} }
} }
// ── 嵌入资源配置加载 ──────────────────────────────────────────────────────────
internal sealed class EmbeddedResourceJsonConfigurationProvider(
EmbeddedResourceJsonConfigurationSource source) : FileConfigurationProvider(source)
{
public override void Load(Stream stream)
{
using var doc = JsonDocument.Parse(stream);
Data = FlattenJson(doc.RootElement);
}
public override void Load()
{
var assembly = source.Assembly;
var resourceName = source.ResourceName;
using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException(
$"嵌入资源 '{resourceName}' 不存在。"
+ $" 可用资源: [{string.Join(", ", assembly.GetManifestResourceNames())}]");
Load(stream);
}
private static Dictionary<string, string?> FlattenJson(JsonElement element)
{
var result = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
Walk(element, "", result);
return result;
static void Walk(JsonElement el, string prefix, Dictionary<string, string?> bag)
{
switch (el.ValueKind)
{
case JsonValueKind.Object:
foreach (var prop in el.EnumerateObject())
Walk(prop.Value, string.IsNullOrEmpty(prefix) ? prop.Name : $"{prefix}:{prop.Name}", bag);
break;
case JsonValueKind.Array:
var i = 0;
foreach (var item in el.EnumerateArray())
{
Walk(item, $"{prefix}:{i}", bag);
i++;
}
break;
default:
bag[prefix] = el.ToString();
break;
}
}
}
}
internal sealed class EmbeddedResourceJsonConfigurationSource : FileConfigurationSource
{
public Assembly Assembly { get; } = null!;
public string ResourceName { get; } = null!;
public EmbeddedResourceJsonConfigurationSource(Assembly assembly, string resourceName)
{
Assembly = assembly;
ResourceName = resourceName;
}
public override IConfigurationProvider Build(IConfigurationBuilder builder) =>
new EmbeddedResourceJsonConfigurationProvider(this);
}
public static class EmbeddedResourceConfigurationExtensions
{
public static IConfigurationBuilder AddEmbeddedResourceJson(
this IConfigurationBuilder builder,
Assembly assembly,
string resourceName)
{
builder.Add(new EmbeddedResourceJsonConfigurationSource(assembly, resourceName));
return builder;
}
}

View File

@@ -0,0 +1,38 @@
@echo off
chcp 65001 >nul
:: WCT DataAnalyzer 分析工具
:: 自动扫描 Datas 文件夹,执行 Ploss 和 Qfod 分析
set "SCRIPT_DIR=%~dp0"
set "EXE_PATH=%SCRIPT_DIR%Gpulse.WCT.DataAnalyzer.exe"
if not exist "%EXE_PATH%" (
echo [错误] 找不到程序: %EXE_PATH%
echo 请确保此脚本与 Gpulse.WCT.DataAnalyzer.exe 在同一目录
pause
exit /b 1
)
echo ========================================
echo WCT DataAnalyzer 分析工具
echo ========================================
echo.
:: 执行 Ploss 分析
echo [1/2] 执行 Ploss 分析...
"%EXE_PATH%" analyze --type ploss --auto
echo.
:: 执行 Qfod 标定
echo [2/2] 执行 Qfod 标定...
"%EXE_PATH%" analyze --type qfod --auto
echo.
echo ========================================
echo 分析完成
echo ========================================
echo.
echo 输出目录: %SCRIPT_DIR%Output
echo.
pause

View File

@@ -1,6 +1,7 @@
using Gpulse.WCT.DataAnalyzer.Commands; using Gpulse.WCT.DataAnalyzer.Commands;
using Gpulse.WCT.DataAnalyzer.Core.Application; using Gpulse.WCT.DataAnalyzer.Core.Application;
using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis; using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
using Gpulse.WCT.DataAnalyzer.Core.Application.Exporting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System.CommandLine; using System.CommandLine;
@@ -19,7 +20,8 @@ public class CliStructureTests : CommandTestBase
root.AddCommand(new CleanCommand(Host.Services.GetRequiredService<CleanService>())); root.AddCommand(new CleanCommand(Host.Services.GetRequiredService<CleanService>()));
root.AddCommand(new AnalyzeCommand( root.AddCommand(new AnalyzeCommand(
Host.Services.GetRequiredService<PlossAnalysisService>(), Host.Services.GetRequiredService<PlossAnalysisService>(),
Host.Services.GetRequiredService<QfodCalibrationService>())); Host.Services.GetRequiredService<QfodCalibrationService>(),
Host.Services.GetRequiredService<AnalyzeExcelExporter>()));
return root; return root;
} }

View File

@@ -1,3 +1,4 @@
using System.Reflection;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions; using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData; using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
@@ -56,7 +57,11 @@ public sealed class CliTestHost : IDisposable
var releaseConnection = new SqliteConnection("Data Source=:memory:"); var releaseConnection = new SqliteConnection("Data Source=:memory:");
await releaseConnection.OpenAsync(); await releaseConnection.OpenAsync();
// 从嵌入资源加载 appsettings.json与 Program.Main 生产路径一致
var configuration = new ConfigurationBuilder() var configuration = new ConfigurationBuilder()
.AddEmbeddedResourceJson(
typeof(Program).Assembly,
"Gpulse.WCT.DataAnalyzer.appsettings.json")
.AddInMemoryCollection(new Dictionary<string, string?> .AddInMemoryCollection(new Dictionary<string, string?>
{ {
["Database:Type"] = "sqlite", ["Database:Type"] = "sqlite",
@@ -73,15 +78,15 @@ public sealed class CliTestHost : IDisposable
var provider = services.BuildServiceProvider(); var provider = services.BuildServiceProvider();
// 初始化表结构与种子数据(与 Program.Main 一致) // 初始化表结构与种子数据(与 Program.Main 一致,经 EF 迁移建表
using (var scope = provider.CreateScope()) using (var scope = provider.CreateScope())
{ {
var local = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>(); var local = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
await local.Database.EnsureCreatedAsync(); await DatabaseMigrationInitializer.MigrateOrUpgradeAsync(local);
SeedData.Initialize(local); SeedData.Initialize(local);
var release = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>(); var release = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>();
await release.Database.EnsureCreatedAsync(); await DatabaseMigrationInitializer.MigrateOrUpgradeAsync(release);
} }
var hostScope = provider.CreateScope(); var hostScope = provider.CreateScope();

View File

@@ -1,6 +1,7 @@
using Gpulse.WCT.DataAnalyzer.Commands; using Gpulse.WCT.DataAnalyzer.Commands;
using Gpulse.WCT.DataAnalyzer.Core.Application; using Gpulse.WCT.DataAnalyzer.Core.Application;
using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis; using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
using Gpulse.WCT.DataAnalyzer.Core.Application.Exporting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Gpulse.WCT.DataAnalyzer.Tests; namespace Gpulse.WCT.DataAnalyzer.Tests;
@@ -13,10 +14,31 @@ public abstract class CommandTestBase
protected CliTestHost Host { get; private set; } = null!; protected CliTestHost Host { get; private set; } = null!;
[SetUp] [SetUp]
public async Task SetUp() => Host = await CliTestHost.CreateAsync(); public async Task SetUp()
{
// 清理 Datas 和 Output 文件夹,避免测试间干扰
CleanupDatasAndOutput();
Host = await CliTestHost.CreateAsync();
}
[TearDown] [TearDown]
public void TearDown() => Host.Dispose(); public void TearDown()
{
Host.Dispose();
// 测试结束后再次清理
CleanupDatasAndOutput();
}
/// <summary>
/// 清理 Datas 和 Output 文件夹
/// </summary>
protected static void CleanupDatasAndOutput()
{
var datasDir = Path.Combine(AppContext.BaseDirectory, "Datas");
var outputDir = Path.Combine(AppContext.BaseDirectory, "Output");
if (Directory.Exists(datasDir)) Directory.Delete(datasDir, true);
if (Directory.Exists(outputDir)) Directory.Delete(outputDir, true);
}
// 命令构造方式与 Program.Main 一致:从 DI 解析服务再构造命令 // 命令构造方式与 Program.Main 一致:从 DI 解析服务再构造命令
protected ParseCommand Parse() => new(Host.Services.GetRequiredService<ParseService>()); protected ParseCommand Parse() => new(Host.Services.GetRequiredService<ParseService>());
@@ -29,7 +51,8 @@ public abstract class CommandTestBase
protected AnalyzeCommand Analyze() => new( protected AnalyzeCommand Analyze() => new(
Host.Services.GetRequiredService<PlossAnalysisService>(), Host.Services.GetRequiredService<PlossAnalysisService>(),
Host.Services.GetRequiredService<QfodCalibrationService>()); Host.Services.GetRequiredService<QfodCalibrationService>(),
Host.Services.GetRequiredService<AnalyzeExcelExporter>());
/// <summary>在测试临时目录下新建一个子目录。</summary> /// <summary>在测试临时目录下新建一个子目录。</summary>
protected string NewDir() => protected string NewDir() =>

View File

@@ -38,9 +38,9 @@ public class AggregateCommandTests : CommandTestBase
var record = await Host.ReleaseDb.ChargingParameters.SingleAsync(); var record = await Host.ReleaseDb.ChargingParameters.SingleAsync();
Assert.Multiple(() => Assert.Multiple(() =>
{ {
// 维度来自文件名:singleMold(TxPanel) / v1.0(TxHardware) / hex2_1(TxSoftware) / iPhone15(RxType) // 维度来自文件名:奇瑞(车厂) / E5(车型) / iPhone15(RxType)
Assert.That(record.CarFactory, Is.EqualTo("singleMold")); Assert.That(record.CarFactory, Is.EqualTo("奇瑞"));
Assert.That(record.CarModel, Is.EqualTo("v1.0")); Assert.That(record.CarModel, Is.EqualTo("E5"));
Assert.That(record.PhoneModel, Is.EqualTo("iPhone15")); Assert.That(record.PhoneModel, Is.EqualTo("iPhone15"));
// Field9=350 的两行 Field7 取平均 = (100+110)/2 // Field9=350 的两行 Field7 取平均 = (100+110)/2
Assert.That(record.Power350mW, Is.EqualTo(105)); Assert.That(record.Power350mW, Is.EqualTo(105));

View File

@@ -1,8 +1,65 @@
using ClosedXML.Excel;
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands; namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
[TestFixture] [TestFixture]
public class AnalyzeCommandTests : CommandTestBase public class AnalyzeCommandTests : CommandTestBase
{ {
/// <summary>
/// 真实产线 .DAT含 35 条两行 Ploss1 条 FOD全部恒等式成立
/// 余量与 delta_p' 反解在真实数据上应当与手算一致。
/// 直接指向仓库根的 Resources 目录gitignored但本地存在CI 无此文件则跳过。
/// </summary>
[Test]
public async Task Analyze_Ploss_RealVivoFile_GeneratesExcel()
{
var file = ResolveResourcesFile(
"C车-单充快速成型件-A03-V55-vivo iQOD12 Pro-20250519-01.DAT");
var outputDir = NewDir();
var outputPath = Path.Combine(outputDir, "result.xlsx");
var (code, stdout, _) = await Analyze().RunCaptureAsync(
"--type", "ploss", "--file", file, "--output", outputPath);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("[OK]"));
Assert.That(stdout, Does.Contain("Total: 35"));
Assert.That(File.Exists(outputPath), Is.True);
});
}
/// <summary>
/// 真实产线 Qfod 标定从「小米17功率上不去」文件中分离出纯手机与异物两段
/// 新建两个符合 FileNameParser 约定的 .log 文件:
/// <list type="bullet">
/// <item>pure前 21 条稳定读数ΔQ=3~5</item>
/// <item>foreign后 7 条衰减读数ΔQ 为负值(-5 ~ -49</item>
/// </list>
/// 阈值 = (pureMax + foreignMin) / 2 = (5 + -49) / 2 = -22
/// </summary>
[Test]
public async Task Analyze_Qfod_RealXiaomiFiles_GeneratesExcel()
{
var pure = ResolveResourcesFile("rapidMold-v1.5-hex2_5-Xiaomi17-qfod_pure-20250817-1.log");
var foreign = ResolveResourcesFile("rapidMold-v1.5-hex2_5-Xiaomi17-qfod_foreign-20250817-1.log");
var outputDir = NewDir();
var outputPath = Path.Combine(outputDir, "qfod_result.xlsx");
var (code, stdout, _) = await Analyze().RunCaptureAsync(
"--type", "qfod", "--pure", pure, "--foreign", foreign, "--output", outputPath);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("[OK]"));
Assert.That(File.Exists(outputPath), Is.True);
});
}
[Test] [Test]
public async Task Analyze_NoType_ReturnsValidationError() public async Task Analyze_NoType_ReturnsValidationError()
{ {
@@ -16,9 +73,14 @@ public class AnalyzeCommandTests : CommandTestBase
} }
[Test] [Test]
public async Task Analyze_UnknownType_PrintsMessage() public async Task Analyze_UnknownType_WithFile_PrintsMessage()
{ {
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "foo"); // 确保没有 Datas 文件夹干扰
CleanupDatasAndOutput();
var dir = NewDir();
var file = TestFixtures.WritePlossLog(dir, "test.dat", new TestFixtures.PlossTwoLineRow(Field10: 350));
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "foo", "--file", file);
Assert.Multiple(() => Assert.Multiple(() =>
{ {
@@ -28,67 +90,55 @@ public class AnalyzeCommandTests : CommandTestBase
} }
[Test] [Test]
public async Task Analyze_Ploss_MissingFile_PrintsPrompt() public async Task Analyze_Ploss_MissingFile_TriggersAutoScan()
{ {
// 不提供文件路径时触发自动扫描
CleanupDatasAndOutput();
var (_, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss"); var (_, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss");
Assert.That(stdout, Does.Contain("Please specify --file for Ploss analysis")); // 没有 Datas 文件夹时报错
Assert.That(stdout, Does.Contain("未找到 Datas 文件夹"));
} }
[Test] [Test]
public async Task Analyze_Ploss_ValidTwoLineFile_ReportsSummary() public async Task Analyze_Ploss_ValidTwoLineFile_GeneratesExcel()
{ {
var dir = NewDir(); var dir = NewDir();
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName, var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
// 正常两行:margin = 3000-500 = 2500 > 2000恒等式成立 // 正常两行:Field10=350功率段理论Δp = 6000-800-2200-500=2500
new TestFixtures.PlossTwoLineRow(), new TestFixtures.PlossTwoLineRow(Field10: 350),
// FOD 两行:ploss=2500 threshold=3000 margin=500恒等式成立delta_p'=2500+2001+2500-3000=4001 // FOD 两行:Field10=500(功率段)
new TestFixtures.PlossTwoLineRow(Field5: 8000, Field9: 2500, Field10: 3000, Field12: 1)); new TestFixtures.PlossTwoLineRow(Field5: 8000, Field9: 2500, Field10: 500, Field12: 1));
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--file", file); var outputDir = NewDir();
var outputPath = Path.Combine(outputDir, "result.xlsx");
var (code, stdout, _) = await Analyze().RunCaptureAsync(
"--type", "ploss", "--file", file, "--output", outputPath);
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(code, Is.EqualTo(0)); Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("=== Ploss Analysis:")); Assert.That(stdout, Does.Contain("[OK]"));
Assert.That(stdout, Does.Contain("Total: 2 (TwoLine: 2, Legacy: 0), FOD: 1, Errors: 0")); Assert.That(File.Exists(outputPath), Is.True);
Assert.That(stdout, Does.Contain("Normal margin>2000: 1/1 (100.0%)"));
Assert.That(stdout, Does.Contain("FOD margin>2000: 0/1 (0.0%)"));
Assert.That(stdout, Does.Contain("Identity (delta_p = Tx - Rx - pow_loss - ploss): 2/2"));
Assert.That(stdout, Does.Contain("delta_p'=4001"));
Assert.That(stdout, Does.Contain("rewrite: pow_loss = 2200, delta_p = 4001"));
}); });
} }
[Test] [Test]
public async Task Analyze_Ploss_LegacyFile_CountsLegacyRows() public async Task Analyze_Qfod_MissingArgs_TriggersAutoScan()
{ {
var dir = NewDir(); // 不提供文件路径时触发自动扫描
var file = TestFixtures.WriteLegacyPlossLog( CleanupDatasAndOutput();
dir, "singleMold-v1.0-hex2_1-iPhone15-ploss_legacy-20260710-1.log",
(500, 3000, 0), (2500, 3000, 1));
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--file", file);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("Total: 2 (TwoLine: 0, Legacy: 2), FOD: 1, Errors: 0"));
Assert.That(stdout, Does.Contain("Normal margin>2000: 1/1 (100.0%)"));
Assert.That(stdout, Does.Contain("FOD margin>2000: 0/1 (0.0%)"));
});
}
[Test]
public async Task Analyze_Qfod_MissingArgs_PrintsPrompt()
{
var (_, stdout, _) = await Analyze().RunCaptureAsync("--type", "qfod"); var (_, stdout, _) = await Analyze().RunCaptureAsync("--type", "qfod");
Assert.That(stdout, Does.Contain("Please specify --pure and --foreign for Qfod calibration")); // 没有 Datas 文件夹时报错
Assert.That(stdout, Does.Contain("未找到 Datas 文件夹"));
} }
[Test] [Test]
public async Task Analyze_Qfod_ValidFiles_ReportsThreshold() public async Task Analyze_Qfod_ValidFiles_GeneratesExcel()
{ {
var dir = NewDir(); var dir = NewDir();
var pure = TestFixtures.WriteQfodLog( var pure = TestFixtures.WriteQfodLog(
@@ -96,19 +146,17 @@ public class AnalyzeCommandTests : CommandTestBase
var foreign = TestFixtures.WriteQfodLog( var foreign = TestFixtures.WriteQfodLog(
dir, "singleMold-v1.0-hex2_1-iPhone15-qfod_foreign-20260710-1.log", 42, 40, 45); dir, "singleMold-v1.0-hex2_1-iPhone15-qfod_foreign-20260710-1.log", 42, 40, 45);
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "qfod", "--pure", pure, "--foreign", foreign); var outputDir = NewDir();
var outputPath = Path.Combine(outputDir, "qfod_result.xlsx");
var (code, stdout, _) = await Analyze().RunCaptureAsync(
"--type", "qfod", "--pure", pure, "--foreign", foreign, "--output", outputPath);
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(code, Is.EqualTo(0)); Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("=== Qfod Calibration ===")); Assert.That(stdout, Does.Contain("[OK]"));
Assert.That(stdout, Does.Contain("Pure phone")); Assert.That(File.Exists(outputPath), Is.True);
Assert.That(stdout, Does.Contain("Foreign object"));
Assert.That(stdout, Does.Contain("count=3 min=20 max=25 avg=22.0"));
Assert.That(stdout, Does.Contain("count=3 min=40 max=45 avg=42.3"));
Assert.That(stdout, Does.Contain("Threshold = (pureMax + foreignMin) / 2 = (25 + 40) / 2 = 32"));
Assert.That(stdout, Does.Contain("pureMax - threshold = -7"));
Assert.That(stdout, Does.Contain("foreignMin - threshold = 8"));
}); });
} }
@@ -126,4 +174,338 @@ public class AnalyzeCommandTests : CommandTestBase
Assert.That(stdout, Does.Contain("FAILED")); Assert.That(stdout, Does.Contain("FAILED"));
} }
/// <summary>
/// 在测试输出目录、以及从该目录向上一路找的仓库根 <c>Resources/</c> 中定位真实数据文件。
/// 找不到时调用 <see cref="Assert.Ignore(string)"/> 跳过CI 无产线 .DAT 时不会让用例变红。
/// 与其它 fixture 模式不同:真实数据不复制到 bin/ —— 直接以相对仓库根的路径读取。
/// </summary>
private static string ResolveResourcesFile(string fileName)
{
// 1) 测试输出目录下的 Resources 副本csproj 配置 CopyToOutputDirectory 时)
var candidates = new List<string>
{
Path.Combine(AppContext.BaseDirectory, "Resources", fileName),
};
// 2) 从测试输出目录向上回溯寻找仓库根的 Resources 目录
// 典型路径:…/tests/Gpulse.WCT.DataAnalyzer.Tests/bin/Debug/net8.0
// → 仓库根在这之上 5 级
var dir = new DirectoryInfo(AppContext.BaseDirectory);
for (int up = 0; up < 8 && dir != null; up++, dir = dir.Parent)
{
var candidate = Path.Combine(dir.FullName, "Resources", fileName);
candidates.Add(candidate);
if (File.Exists(candidate))
return candidate;
}
Assert.Ignore($"真实数据文件不存在(已检索 {candidates.Count} 个候选路径):{fileName}");
return null!; // 不会执行
}
#region --auto
/// <summary>
/// 测试 --auto 自动扫描 Datas 文件夹并执行 Ploss 分析
/// </summary>
[Test]
public async Task Analyze_Auto_Ploss_ScansDatasFolder()
{
// 创建 Datas 文件夹和测试文件
var datasDir = Path.Combine(AppContext.BaseDirectory, "Datas");
Directory.CreateDirectory(datasDir);
TestFixtures.WritePlossLog(datasDir, "奇瑞-E5-singleMold-A00-V001-iPhone15-无-ploss_test-2026081701.dat",
new TestFixtures.PlossTwoLineRow(Field10: 350));
try
{
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--auto");
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("扫描 Datas 文件夹"));
Assert.That(stdout, Does.Contain("[完成] 已分析"));
});
}
finally
{
if (Directory.Exists(datasDir)) Directory.Delete(datasDir, true);
}
}
/// <summary>
/// 测试 --auto 自动分类纯手机和异物日志
/// </summary>
[Test]
public async Task Analyze_Auto_Qfod_ClassifiesPureAndForeign()
{
var datasDir = Path.Combine(AppContext.BaseDirectory, "Datas");
Directory.CreateDirectory(datasDir);
// 纯手机日志:异物类型 = 无
TestFixtures.WriteQfodLog(datasDir, "奇瑞-E5-singleMold-A00-V001-iPhone15-无-qfod_pure-2026081701.log", 20, 25, 21);
// 异物日志:异物类型 = 硬币
TestFixtures.WriteQfodLog(datasDir, "奇瑞-E5-singleMold-A00-V001-iPhone15-硬币-qfod_foreign-2026081701.log", -5, -10, -15);
try
{
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "qfod", "--auto");
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
// 验证能识别纯手机和异物
Assert.That(stdout, Does.Contain("纯手机:"));
Assert.That(stdout, Does.Contain("异物:"));
});
}
finally
{
if (Directory.Exists(datasDir)) Directory.Delete(datasDir, true);
}
}
/// <summary>
/// 测试 --auto 时 Datas 文件夹不存在
/// </summary>
[Test]
public async Task Analyze_Auto_NoDatasFolder_ReportsError()
{
CleanupDatasAndOutput();
var (_, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--auto");
Assert.That(stdout, Does.Contain("未找到 Datas 文件夹"));
}
#endregion
#region
/// <summary>
/// 测试输出目录结构Output/{车厂}/{车型}/{手机名}/{文件名}.xlsx
/// </summary>
[Test]
public async Task Analyze_Ploss_CreatesOutputDirectoryStructure()
{
CleanupDatasAndOutput();
var dir = NewDir();
var file = TestFixtures.WritePlossLog(dir, "奇瑞-E5-singleMold-A00-V001-iPhone15-无-ploss_test-2026081701.dat",
new TestFixtures.PlossTwoLineRow(Field10: 350));
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--file", file);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
// 验证输出路径包含车厂/车型/手机名
Assert.That(stdout, Does.Contain("Output"));
Assert.That(stdout, Does.Contain("奇瑞"));
Assert.That(stdout, Does.Contain("E5"));
Assert.That(stdout, Does.Contain("iPhone15"));
});
}
#endregion
#region Qfod ΔQ
/// <summary>
/// 测试 Qfod 统计使用绝对值,阈值用有符号 ΔQ见使用指南 Threshold=-22 样例)。
/// 本用例 pure ΔQ=-20,-25,-21foreign ΔQ=30,35,40 → pureMax=-20, foreignMin=30
/// 阈值 = (-20 + 30) / 2 = 5有符号。断言行7标定Q值单元格 == 5。
/// </summary>
[Test]
public async Task Analyze_Qfod_WithNegativeValues_GeneratesExcel()
{
CleanupDatasAndOutput();
var dir = NewDir();
// 包含负值 ΔQ
var pure = TestFixtures.WriteQfodLog(dir, "奇瑞-E5-singleMold-A00-V001-iPhone15-无-qfod_pure-2026081701.log", -20, -25, -21);
var foreign = TestFixtures.WriteQfodLog(dir, "奇瑞-E5-singleMold-A00-V001-iPhone15-硬币-qfod_foreign-2026081701.log", 30, 35, 40);
var outputDir = NewDir();
var outputPath = Path.Combine(outputDir, "result.xlsx");
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "qfod", "--pure", pure, "--foreign", foreign, "--output", outputPath);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("[OK]"));
Assert.That(File.Exists(outputPath), Is.True);
});
// 读回 Excel行6 标定Q值Q0 列 col=2应为有符号阈值 5
using var workbook = new XLWorkbook(outputPath);
var ws = workbook.Worksheet("Sheet1");
Assert.Multiple(() =>
{
Assert.That(ws.Cell(6, 1).GetString(), Is.EqualTo("标定Q值")); // 行6 是标定Q值行
// Q0 在行6合并了三列值在合并后的第一个单元格
Assert.That((int)ws.Cell(6, 2).GetDouble(), Is.EqualTo(5));
});
}
#endregion
#region Ploss
/// <summary>
/// 测试 Ploss 标定值计算标定Δp = 理论Δp最大值 + a
/// 功率段 350 的 a 值为 3000。理论Δp = 6000-(800+2200+500) = 2500 → 标定 = 5500。
/// 读回 Excel 校验行7标定Δp单元格350 段 col=2== 5500。
/// </summary>
[Test]
public async Task Analyze_Ploss_CalculatesCalibrationValue()
{
CleanupDatasAndOutput();
var dir = NewDir();
// 创建功率段 350 的数据
// 理论Δp = Field5 - (Field4 + PowLoss + Field9) = 6000 - (800 + 2200 + 500) = 2500
var file = TestFixtures.WritePlossLog(dir, "奇瑞-E5-singleMold-A00-V001-iPhone15-无-ploss_test-2026081701.dat",
new TestFixtures.PlossTwoLineRow(Field10: 350));
var outputDir = NewDir();
var outputPath = Path.Combine(outputDir, "result.xlsx");
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--file", file, "--output", outputPath);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(File.Exists(outputPath), Is.True);
Assert.That(stdout, Does.Contain("Total: 1"));
Assert.That(stdout, Does.Contain("Dropped: 0"));
});
// 读回 Excel行1 列2 含功率段 350行6 标定Δp值col=2= 2500 + 3000 = 5500
using var workbook = new XLWorkbook(outputPath);
var ws = workbook.Worksheet("Sheet1");
Assert.Multiple(() =>
{
Assert.That((int)ws.Cell(1, 2).GetDouble(), Is.EqualTo(350)); // 行1 是功率段标题行
Assert.That(ws.Cell(6, 1).GetString(), Is.EqualTo("标定Δp值")); // 行6 是标定行
Assert.That((long)ws.Cell(6, 2).GetDouble(), Is.EqualTo(5500L));
});
}
/// <summary>
/// RV-004Field10 非标准功率段的记录应计入 DroppedCount 并在 stdout 暴露,
/// 而非静默丢弃。
/// </summary>
[Test]
public async Task Analyze_Ploss_NonStandardPowerSegment_IsDropped()
{
CleanupDatasAndOutput();
var dir = NewDir();
// Field10=850 不在 9 个标准功率段内 → 被丢弃
var file = TestFixtures.WritePlossLog(dir, "奇瑞-E5-singleMold-A00-V001-iPhone15-无-ploss_test-2026081701.dat",
new TestFixtures.PlossTwoLineRow(Field10: 850));
var outputDir = NewDir();
var outputPath = Path.Combine(outputDir, "result.xlsx");
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--file", file, "--output", outputPath);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(File.Exists(outputPath), Is.True);
Assert.That(stdout, Does.Contain("Total: 1"));
Assert.That(stdout, Does.Contain("Dropped: 1"));
});
}
/// <summary>
/// RV-002pure/foreign 日志存在但未提取到 Qfod 行 → 空记录守卫应返回失败而非从 0 起算成功。
/// </summary>
[Test]
public async Task Analyze_Qfod_EmptyRecords_ReportsFailure()
{
CleanupDatasAndOutput();
var dir = NewDir();
// 写一个只有空白行的"空"日志(文件存在但无 Qfod 行)
var pure = Path.Combine(dir, "pure_empty.log");
var foreign = Path.Combine(dir, "foreign_empty.log");
await File.WriteAllTextAsync(pure, "\n\n \n");
await File.WriteAllTextAsync(foreign, "\n\n \n");
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "qfod", "--pure", pure, "--foreign", foreign);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("FAILED"));
Assert.That(stdout, Does.Contain("未提取到 Qfod"));
});
}
/// <summary>
/// RV-006--output 指向不存在的嵌套目录时应自动创建而非崩溃。
/// </summary>
[Test]
public async Task Analyze_Ploss_OutputToMissingDir_CreatesDir()
{
CleanupDatasAndOutput();
var dir = NewDir();
var file = TestFixtures.WritePlossLog(dir, "奇瑞-E5-singleMold-A00-V001-iPhone15-无-ploss_test-2026081701.dat",
new TestFixtures.PlossTwoLineRow(Field10: 350));
var outputRoot = NewDir();
var outputPath = Path.Combine(outputRoot, "nested", "deeper", "result.xlsx");
Assume.That(Directory.Exists(Path.GetDirectoryName(outputPath)), Is.False);
var (code, _, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--file", file, "--output", outputPath);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(File.Exists(outputPath), Is.True);
});
}
/// <summary>
/// RV-005--auto 多文件其中一文件损坏(无法解析)不应中止整批;其余文件仍产出 [OK]
/// 且 stdout 含 [FAILED] 与最终 [完成] 汇总。
/// </summary>
[Test]
public async Task Analyze_Auto_Ploss_OneBadFile_DoesNotAbortBatch()
{
// 准备 Datas 文件夹:一个好文件 + 一个坏文件(只有 header 无 FOD 行)
var dataDir = Path.Combine(AppContext.BaseDirectory, "Datas");
try
{
Directory.CreateDirectory(dataDir);
TestFixtures.WritePlossLog(dataDir, "奇瑞-E5-singleMold-A00-V001-iPhone15-无-ploss_test-2026081701.dat",
new TestFixtures.PlossTwoLineRow(Field10: 350));
// 坏文件:仅 header 行,无配对 FOD 行 → 解析错误计数
var badPath = Path.Combine(dataDir, "奇瑞-E5-singleMold-A00-V001-iPhone15-无-badfile-2026081702.dat");
await File.WriteAllTextAsync(badPath, "pow_loss = 2200, delta_p = 2500\n");
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--auto");
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("[OK]"));
Assert.That(stdout, Does.Contain("[完成]"));
});
}
finally
{
if (Directory.Exists(dataDir)) Directory.Delete(dataDir, true);
}
}
#endregion
} }

View File

@@ -0,0 +1,120 @@
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Microsoft.EntityFrameworkCore;
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
/// <summary>
/// RV-001EF 迁移替代 EnsureCreated。验证两类场景
/// 1. 全新库经迁移建全部表CliTestHost 已覆盖,此处断言关键新表存在)。
/// 2. 既有 EnsureCreated 库(无迁移历史但有表)经 MigrateOrUpgradeAsync 升级后补齐
/// 本变更新增的 3 张维度表 + test_scenario 的 3 列 FK。
/// </summary>
[TestFixture]
public class DatabaseMigrationInitializerTests
{
/// <summary>全新库经迁移建表后3 张新维度表存在。</summary>
[Test]
public async Task MigrateOrUpgrade_FreshDb_CreatesNewDimensionTables()
{
var dbPath = Path.Combine(Path.GetTempPath(), $"wct_fresh_{Guid.NewGuid():N}.db");
try
{
await using var context = CreateContext(dbPath);
await DatabaseMigrationInitializer.MigrateOrUpgradeAsync(context);
Assert.Multiple(async () =>
{
Assert.That(await TableExistsAsync(context, "manufacturer"), Is.True);
Assert.That(await TableExistsAsync(context, "car_model"), Is.True);
Assert.That(await TableExistsAsync(context, "foreign_object"), Is.True);
});
}
finally
{
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
if (File.Exists(dbPath)) File.Delete(dbPath);
}
}
/// <summary>既有 EnsureCreated 库(旧 schema无新表/FK 列)经升级补齐。</summary>
[Test]
public async Task MigrateOrUpgrade_LegacyEnsureCreatedDb_AddsNewTablesAndFkColumns()
{
var dbPath = Path.Combine(Path.GetTempPath(), $"wct_legacy_{Guid.NewGuid():N}.db");
try
{
// 1. 构造一个"旧 schema"库test_scenario 无 manufacturer_id/car_model_id/foreign_object_id
// 无 __EFMigrationsHistory模拟 EnsureCreated 既有库)
CreateLegacyEnsureCreatedDatabase(dbPath);
// 2. 运行升级
await using var context = CreateContext(dbPath);
await DatabaseMigrationInitializer.MigrateOrUpgradeAsync(context);
// 3. 断言新维度表与 test_scenario 的新 FK 列已补齐
Assert.Multiple(async () =>
{
Assert.That(await TableExistsAsync(context, "manufacturer"), Is.True);
Assert.That(await TableExistsAsync(context, "car_model"), Is.True);
Assert.That(await TableExistsAsync(context, "foreign_object"), Is.True);
Assert.That(await ColumnExistsAsync(context, "test_scenario", "manufacturer_id"), Is.True);
Assert.That(await ColumnExistsAsync(context, "test_scenario", "car_model_id"), Is.True);
Assert.That(await ColumnExistsAsync(context, "test_scenario", "foreign_object_id"), Is.True);
});
}
finally
{
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
if (File.Exists(dbPath)) File.Delete(dbPath);
}
}
private static WctMinerDbContext CreateContext(string dbPath)
{
var dir = Path.GetDirectoryName(dbPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
var options = new DbContextOptionsBuilder<WctMinerDbContext>()
.UseSqlite($"Data Source={dbPath};Pooling=False")
.Options;
return new WctMinerDbContext(options);
}
private static void CreateLegacyEnsureCreatedDatabase(string dbPath)
{
using var connection = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source={dbPath};Pooling=False");
connection.Open();
using var cmd = connection.CreateCommand();
cmd.CommandText = """
CREATE TABLE test_scenario (
id TEXT NOT NULL PRIMARY KEY,
tx_panel_id TEXT,
tx_hardware_id TEXT,
tx_software_id TEXT,
rx_type_id TEXT,
test_purpose TEXT,
test_date TEXT,
test_sequence INTEGER,
is_deleted INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE tx_panel (id TEXT NOT NULL PRIMARY KEY, name TEXT, is_deleted INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')));
""";
cmd.ExecuteNonQuery();
}
private static async Task<bool> TableExistsAsync(WctMinerDbContext context, string tableName)
{
var count = await context.Database.SqlQueryRaw<long>(
$"SELECT count(*) AS Value FROM sqlite_master WHERE type='table' AND name = '{tableName}'").FirstOrDefaultAsync();
return count > 0;
}
private static async Task<bool> ColumnExistsAsync(WctMinerDbContext context, string table, string column)
{
var count = await context.Database.SqlQueryRaw<long>(
$"SELECT count(*) AS Value FROM pragma_table_info('{table}') WHERE name = '{column}'").FirstOrDefaultAsync();
return count > 0;
}
}

View File

@@ -76,6 +76,30 @@ public class ParseCommandTests : CommandTestBase
Assert.That(await Host.LocalDb.PlossRecords.CountAsync(), Is.EqualTo(2)); Assert.That(await Host.LocalDb.PlossRecords.CountAsync(), Is.EqualTo(2));
} }
[TestCase("0C", 12)] // 上位 nibble = 0下位 nibble = C
[TestCase("0xFF", 255)] // 标准 0x 前缀
[TestCase("0xff", 255)] // 小写 hex
[TestCase("0A", 10)]
[TestCase("0", 0)] // 纯 0应识别为十进制 0
[TestCase("10", 10)] // 纯十进制:应识别为 10不是 0x10 = 16
public async Task Parse_File_PlossLog_HexField1_DecodedCorrectly(string field1Raw, int expected)
{
var dir = NewDir();
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
new TestFixtures.PlossTwoLineRow(Field1AsHex: field1Raw));
var (code, stdout, _) = await Parse().RunCaptureAsync("--file", file);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("Ploss: 1"));
});
var record = await Host.LocalDb.PlossRecords.SingleAsync();
Assert.That(record.Field1, Is.EqualTo(expected));
}
[Test] [Test]
public async Task Parse_File_WithForceOption_Accepted() public async Task Parse_File_WithForceOption_Accepted()
{ {

View File

@@ -0,0 +1,76 @@
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
/// <summary>
/// PlossParser 直接单测:覆盖 commit eba1d20 引入的 Field1 十六进制解析
/// 0x 前缀与无前缀两种写法),该路径此前在 legacy 删除后零覆盖。
/// </summary>
[TestFixture]
public class PlossParserTests : CommandTestBase
{
private Core.Application.Parsing.PlossParser _parser = default!;
// NUnit 会先运行基类的 [SetUp](创建 Host再运行本方法
[SetUp]
public async Task InitParser()
{
await Task.CompletedTask;
_parser = Host.Services.GetRequiredService<Core.Application.Parsing.PlossParser>();
}
[Test]
public void Parse_HexField1_WithZeroPrefix_DecodesCorrectly()
{
// Field1 = "0C" → 十进制 12
var dir = NewDir();
var path = TestFixtures.WritePlossLog(dir, "hex-0C.dat",
new TestFixtures.PlossTwoLineRow(Field1AsHex: "0C"));
var lines = File.ReadAllLines(path);
var result = _parser.ParseMultiLine(lines, Guid.Empty);
Assert.Multiple(() =>
{
Assert.That(result, Is.Not.Null);
Assert.That(result!.IsSuccess, Is.True);
Assert.That(result.Record!.Field1, Is.EqualTo(12));
});
}
[Test]
public void Parse_HexField1_WithZeroXPrefix_DecodesCorrectly()
{
// Field1 = "0xFF" → 十进制 255
var dir = NewDir();
var path = TestFixtures.WritePlossLog(dir, "hex-0xFF.dat",
new TestFixtures.PlossTwoLineRow(Field1AsHex: "0xFF"));
var lines = File.ReadAllLines(path);
var result = _parser.ParseMultiLine(lines, Guid.Empty);
Assert.Multiple(() =>
{
Assert.That(result, Is.Not.Null);
Assert.That(result!.IsSuccess, Is.True);
Assert.That(result.Record!.Field1, Is.EqualTo(255));
});
}
[Test]
public void Parse_DecimalField1_DecodesCorrectly()
{
// Field1 普通十进制 → 原值
var dir = NewDir();
var path = TestFixtures.WritePlossLog(dir, "dec-42.dat",
new TestFixtures.PlossTwoLineRow(Field1: 42));
var lines = File.ReadAllLines(path);
var result = _parser.ParseMultiLine(lines, Guid.Empty);
Assert.Multiple(() =>
{
Assert.That(result, Is.Not.Null);
Assert.That(result!.IsSuccess, Is.True);
Assert.That(result.Record!.Field1, Is.EqualTo(42));
});
}
}

View File

@@ -1 +1,2 @@
global using NUnit.Framework; global using NUnit.Framework;
global using Microsoft.Extensions.DependencyInjection;

View File

@@ -2,21 +2,31 @@ namespace Gpulse.WCT.DataAnalyzer.Tests;
/// <summary> /// <summary>
/// 构造 CLI 测试用的日志 fixture 文件。文件名符合 FileNameParser 约定的 /// 构造 CLI 测试用的日志 fixture 文件。文件名符合 FileNameParser 约定的
/// `TX面板-TX硬件-TX软件-RX类型-测试目的-日期-序号` 格式。 /// `车厂-车型-TX面板类型-TX硬件-TX软件-RX类型-异物类型-测试目的-年月日测试次数` 格式。
/// </summary> /// </summary>
public static class TestFixtures public static class TestFixtures
{ {
public const string PlossFileName = "singleMold-v1.0-hex2_1-iPhone15-ploss_test-20260710-1.log"; /// <summary>
public const string QfodFileName = "singleMold-v1.0-hex2_1-iPhone15-qfod_test-20260710-1.log"; /// 新格式文件名示例:
/// 车厂-车型-TX面板类型-TX硬件-TX软件-RX类型-异物类型-测试目的-年月日测试次数
/// </summary>
public const string PlossFileName = "奇瑞-E5-singleMold-A00-V001-iPhone15-无-ploss_test-2026071001.dat";
public const string QfodFileName = "奇瑞-E5-singleMold-A00-V001-iPhone15-无-qfod_test-2026071001.dat";
/// <summary> /// <summary>
/// 两行 Ploss 记录header + FOD 16 字段)。 /// 两行 Ploss 记录header + FOD 16 字段)。
/// 默认值让恒等式成立Field5 - Field4 - PowLoss - ploss == DeltaP且余量 > 2000。 /// 默认值让恒等式成立Field5 - Field4 - PowLoss - ploss == DeltaP且余量 > 2000。
/// Field10 为功率段,默认 350。
/// </summary> /// </summary>
/// <param name="Field1AsHex">
/// 当不为 null 时,覆写 Field1按十六进制字面量写入日志如 "0C" = 十进制 12
/// 用于验证解析器对 Field1 十六进制格式的支持。
/// </param>
public sealed record PlossTwoLineRow( public sealed record PlossTwoLineRow(
int PowLoss = 2200, int PowLoss = 2200,
int DeltaP = 2500, int DeltaP = 2500,
int Field1 = 1, int Field1 = 1,
string? Field1AsHex = null,
int Field2 = 0, int Field2 = 0,
int Field3 = 0, int Field3 = 0,
int Field4 = 800, // RX power int Field4 = 800, // RX power
@@ -25,7 +35,7 @@ public static class TestFixtures
int Field7 = 0, int Field7 = 0,
int Field8 = 0, int Field8 = 0,
int Field9 = 500, // ploss int Field9 = 500, // ploss
int Field10 = 3000, // threshold int Field10 = 350, // threshold/power segment (350/500/750/1000/1250/1500/1750/2000/2250)
int Field11 = 0, int Field11 = 0,
int Field12 = 0, // FOD 标志 int Field12 = 0, // FOD 标志
int Field13 = 0, int Field13 = 0,
@@ -41,8 +51,9 @@ public static class TestFixtures
foreach (var r in rows) foreach (var r in rows)
{ {
lines.Add($"pow_loss = {r.PowLoss}, delta_p = {r.DeltaP}"); lines.Add($"pow_loss = {r.PowLoss}, delta_p = {r.DeltaP}");
var field1 = r.Field1AsHex ?? r.Field1.ToString();
lines.Add( lines.Add(
$"FOD-> {r.Field1} {r.Field2} {r.Field3} {r.Field4} {r.Field5} {r.Field6} " + $"FOD-> {field1} {r.Field2} {r.Field3} {r.Field4} {r.Field5} {r.Field6} " +
$"{r.Field7} {r.Field8} {r.Field9} {r.Field10} {r.Field11} {r.Field12} " + $"{r.Field7} {r.Field8} {r.Field9} {r.Field10} {r.Field11} {r.Field12} " +
$"{r.Field13} {r.Field14} {r.Field15} {r.Field16}"); $"{r.Field13} {r.Field14} {r.Field15} {r.Field16}");
} }
@@ -50,17 +61,6 @@ public static class TestFixtures
return path; return path;
} }
/// <summary>写旧单行格式 Ploss 日志14 字段),仅指定 ploss/threshold/FOD 三个关键字段。</summary>
public static string WriteLegacyPlossLog(string dir, string fileName, params (int Ploss, int Threshold, int Fod)[] rows)
{
var path = Path.Combine(dir, fileName);
var lines = rows
.Select(r => $"FOD-> 1 0 0 0 0 0 0 0 {r.Ploss} {r.Threshold} 0 {r.Fod} 0 0")
.ToArray();
File.WriteAllLines(path, lines);
return path;
}
/// <summary>写 Qfod 日志文件,每行 DeltaQ 一个。</summary> /// <summary>写 Qfod 日志文件,每行 DeltaQ 一个。</summary>
public static string WriteQfodLog(string dir, string fileName, params int[] deltaQs) public static string WriteQfodLog(string dir, string fileName, params int[] deltaQs)
{ {