Compare commits

..

18 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
Scottxjw
759da546ee test: 添加 CLI 命令单元测试 2026-08-17 09:24:57 +08:00
Scottxjw
d45d9f4ca3 chore: 添加应用默认配置文件 2026-08-17 09:24:06 +08:00
55 changed files with 7868 additions and 214 deletions

8
.gitignore vendored
View File

@@ -13,6 +13,7 @@ logs/
output/
*.csv
*.json
!**/appsettings*.json
# 测试日志
test_logs/
@@ -26,4 +27,9 @@ test_logs/
.idea/
# 包目录(由 nuget restore 生成)
packages/
packages/
# 发布产物
publish/
Resources/

View File

@@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
@@ -7,24 +7,65 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gpulse.WCT.DataAnalyzer.Cor
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gpulse.WCT.DataAnalyzer", "src\Gpulse.WCT.DataAnalyzer\Gpulse.WCT.DataAnalyzer.csproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345603}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gpulse.WCT.DataAnalyzer.Tests", "tests\Gpulse.WCT.DataAnalyzer.Tests\Gpulse.WCT.DataAnalyzer.Tests.csproj", "{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Debug|x64.ActiveCfg = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Debug|x64.Build.0 = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Debug|x86.ActiveCfg = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Debug|x86.Build.0 = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|x64.ActiveCfg = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|x64.Build.0 = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|x86.ActiveCfg = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|x86.Build.0 = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|x64.ActiveCfg = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|x64.Build.0 = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|x86.ActiveCfg = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Debug|x86.Build.0 = Debug|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|x64.ActiveCfg = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|x64.Build.0 = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|x86.ActiveCfg = Release|Any CPU
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|x86.Build.0 = Release|Any CPU
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Debug|x64.ActiveCfg = Debug|Any CPU
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Debug|x64.Build.0 = Debug|Any CPU
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Debug|x86.ActiveCfg = Debug|Any CPU
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Debug|x86.Build.0 = Debug|Any CPU
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Release|Any CPU.Build.0 = Release|Any CPU
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Release|x64.ActiveCfg = Release|Any CPU
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Release|x64.Build.0 = Release|Any CPU
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Release|x86.ActiveCfg = Release|Any CPU
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{FE1E3A36-A114-4B21-BE29-53ECA9B773FF} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A1B2C3D4-1234-5678-9ABC-DEF012345600}
EndGlobalSection

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/{车厂}/{车型}/{手机名}/`
---
## 联系支持
如有问题,请联系开发团队。

View File

@@ -48,6 +48,21 @@
| ORM | Entity Framework Core | 8.0 |
| 数据库 | SQLite / PostgreSQL | - |
| CLI | System.CommandLine | 2.0-beta4 |
| 测试 | NUnit | 4.2 |
---
## 测试
CLI 单元测试项目位于 `tests/Gpulse.WCT.DataAnalyzer.Tests`,使用 NUnit + 内存 SQLite
- 直接实例化 `ParseCommand` / `AggregateCommand` / `ExportCommand` / `CleanCommand` / `AnalyzeCommand`(构造方式与 `Program.Main` 一致),验证命令绑定、参数解析与输出。
- 每个测试独立的「本地库/发布库」内存连接 + 临时日志 fixture 目录,互不污染。
- 不启动完整 Host/Serilog仅覆盖 CLI 命令层行为。
```bash
dotnet test Gpulse.WCT.DataAnalyzer.sln
```
---

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;
/// <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)
{
_plossParser = plossParser;
}
/// <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>
public async Task<PlossAnalysisReport> AnalyzeFileAsync(string filePath)
{
@@ -48,7 +206,7 @@ public class PlossAnalysisService
var rows = new List<PlossRowReport>();
int errorCount = 0;
// 用于暂存两行格式的 header 行(与 ParseService 相同的合并逻辑)
// 用于暂存两行格式的 header 行
string? pendingHeaderLine = null;
int pendingHeaderLineNumber = 0;
@@ -81,12 +239,8 @@ public class PlossAnalysisService
}
else
{
// 单行格式(旧格式兼容)
var result = _plossParser.Parse(line, Guid.Empty);
if (result.IsSuccess && result.Record != null)
rows.Add(BuildRow(lineNumber, result.Record));
else
errorCount++;
// 缺少 header 行,记录错误
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.Domain.Local;
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
@@ -17,7 +18,140 @@ public class QfodCalibrationService
}
/// <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>
/// <param name="pureFile">纯手机充电日志文件</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>
/// 通用的并发安全 GetOrCreate 模式
/// </summary>

View File

@@ -7,7 +7,7 @@ using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
/// <summary>
/// 选条件常量
/// 选条件常量
/// </summary>
public static class FilterConstants
{
@@ -38,14 +38,17 @@ public class ScenarioService
/// </summary>
public async Task<TestScenario> GetOrCreateScenarioAsync(ScenarioInfo info)
{
_logger.LogDebug("查找/创建场景: TxPanel={TxPanel}, TxHardware={TxHardware}, TxSoftware={TxSoftware}, RxType={RxType}",
info.TxPanel, info.TxHardware, info.TxSoftware, info.RxType);
_logger.LogDebug("查找/创建场景: Manufacturer={Manufacturer}, CarModel={CarModel}, TxPanel={TxPanel}, TxHardware={TxHardware}, TxSoftware={TxSoftware}, RxType={RxType}, ForeignObject={ForeignObject}",
info.Manufacturer, info.CarModel, info.TxPanel, info.TxHardware, info.TxSoftware, info.RxType, info.ForeignObject);
// 1. 获取或创建各维度
var manufacturer = await _dimensionService.GetOrCreateManufacturerAsync(info.Manufacturer);
var carModel = await _dimensionService.GetOrCreateCarModelAsync(info.CarModel);
var txPanel = await _dimensionService.GetOrCreateTxPanelAsync(info.TxPanel);
var txHardware = await _dimensionService.GetOrCreateTxHardwareAsync(info.TxHardware);
var txSoftware = await _dimensionService.GetOrCreateTxSoftwareAsync(info.TxSoftware);
var rxType = await _dimensionService.GetOrCreateRxTypeAsync(info.RxType);
var foreignObject = await _dimensionService.GetOrCreateForeignObjectAsync(info.ForeignObject);
// 2. 并发安全的场景创建
const int maxRetries = 3;
@@ -56,10 +59,13 @@ public class ScenarioService
var existing = await _context.TestScenarios
.AsNoTracking()
.FirstOrDefaultAsync(s =>
s.ManufacturerId == manufacturer.Id &&
s.CarModelId == carModel.Id &&
s.TxPanelId == txPanel.Id &&
s.TxHardwareId == txHardware.Id &&
s.TxSoftwareId == txSoftware.Id &&
s.RxTypeId == rxType.Id &&
s.ForeignObjectId == foreignObject.Id &&
s.TestPurpose == info.TestPurpose &&
s.TestDate == info.TestDate &&
s.TestSequence == info.TestSequence);
@@ -73,10 +79,13 @@ public class ScenarioService
// 3. 创建新场景
var scenario = new TestScenario
{
ManufacturerId = manufacturer.Id,
CarModelId = carModel.Id,
TxPanelId = txPanel.Id,
TxHardwareId = txHardware.Id,
TxSoftwareId = txSoftware.Id,
RxTypeId = rxType.Id,
ForeignObjectId = foreignObject.Id,
TestPurpose = info.TestPurpose,
TestDate = info.TestDate,
TestSequence = info.TestSequence
@@ -88,8 +97,8 @@ public class ScenarioService
{
await _context.SaveChangesAsync();
_logger.LogInformation("创建新测试场景: {ScenarioId}, TxPanel={TxPanel}, TxHardware={TxHardware}, RxType={RxType}, TestDate={TestDate}",
scenario.Id, info.TxPanel, info.TxHardware, info.RxType, info.TestDate);
_logger.LogInformation("创建新测试场景: {ScenarioId}, Manufacturer={Manufacturer}, CarModel={CarModel}, TxPanel={TxPanel}, TxHardware={TxHardware}, RxType={RxType}, ForeignObject={ForeignObject}, TestDate={TestDate}",
scenario.Id, info.Manufacturer, info.CarModel, info.TxPanel, info.TxHardware, info.RxType, info.ForeignObject, info.TestDate);
return scenario;
}
@@ -97,7 +106,7 @@ public class ScenarioService
{
// Detach 实体以避免 ChangeTracker 问题
_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)
{
@@ -111,17 +120,20 @@ public class ScenarioService
var finalScenario = await _context.TestScenarios
.AsNoTracking()
.FirstOrDefaultAsync(s =>
s.ManufacturerId == manufacturer.Id &&
s.CarModelId == carModel.Id &&
s.TxPanelId == txPanel.Id &&
s.TxHardwareId == txHardware.Id &&
s.TxSoftwareId == txSoftware.Id &&
s.RxTypeId == rxType.Id &&
s.ForeignObjectId == foreignObject.Id &&
s.TestPurpose == info.TestPurpose &&
s.TestDate == info.TestDate &&
s.TestSequence == info.TestSequence);
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;
@@ -133,10 +145,13 @@ public class ScenarioService
public async Task<TestScenario?> GetScenarioWithDimensionsAsync(Guid scenarioId)
{
return await _context.TestScenarios
.Include(s => s.Manufacturer)
.Include(s => s.CarModel)
.Include(s => s.TxPanel)
.Include(s => s.TxHardware)
.Include(s => s.TxSoftware)
.Include(s => s.RxType)
.Include(s => s.ForeignObject)
.FirstOrDefaultAsync(s => s.Id == scenarioId);
}
@@ -148,10 +163,13 @@ public class ScenarioService
public async Task<List<TestScenario>> GetAllScenariosAsync(int pageSize = 50, int pageIndex = 0)
{
var query = _context.TestScenarios
.Include(s => s.Manufacturer)
.Include(s => s.CarModel)
.Include(s => s.TxPanel)
.Include(s => s.TxHardware)
.Include(s => s.TxSoftware)
.Include(s => s.RxType)
.Include(s => s.ForeignObject)
.OrderByDescending(s => s.TestDate)
.ThenByDescending(s => s.TestSequence)
.AsNoTracking();
@@ -177,6 +195,18 @@ public class ScenarioService
/// </summary>
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
.Select(p => p.Name)
.Distinct()
@@ -201,37 +231,56 @@ public class ScenarioService
.OrderBy(n => n)
.ToListAsync();
var foreignObjects = await _context.ForeignObjects
.Select(f => f.Name)
.Distinct()
.OrderBy(n => n)
.ToListAsync();
return new FilterOptions(
manufacturers.Prepend(FilterConstants.AllOption).ToList(),
carModels.Prepend(FilterConstants.AllOption).ToList(),
txPanels.Prepend(FilterConstants.AllOption).ToList(),
txHardwares.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>
/// <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(
string? manufacturer = null,
string? carModel = null,
string? txPanel = null,
string? txHardware = null,
string? txSoftware = null,
string? rxType = null,
string? foreignObject = null,
int pageSize = 50,
int pageIndex = 0)
{
var query = _context.TestScenarios
.Include(s => s.Manufacturer)
.Include(s => s.CarModel)
.Include(s => s.TxPanel)
.Include(s => s.TxHardware)
.Include(s => s.TxSoftware)
.Include(s => s.RxType)
.Include(s => s.ForeignObject)
.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)
{
query = query.Where(s => s.TxPanel.Name == txPanel);
@@ -252,6 +301,11 @@ public class ScenarioService
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
.OrderByDescending(s => s.TestDate)
.ThenByDescending(s => s.TestSequence);
@@ -307,7 +361,10 @@ public class ScenarioService
/// 筛选选项数据
/// </summary>
public record FilterOptions(
List<string> Manufacturers,
List<string> CarModels,
List<string> TxPanels,
List<string> TxHardwares,
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>
/// 文件名解析器 - 从文件名提取场景信息
/// 格式: 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>
public class FileNameParser
{
@@ -19,39 +19,60 @@ public class FileNameParser
/// <returns>场景信息,若格式不匹配返回 null</returns>
public ScenarioInfo? Parse(string fileName)
{
// 移除 .log 扩展名
// 移除扩展名
var nameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
if (string.IsNullOrEmpty(nameWithoutExt))
return null;
var parts = nameWithoutExt.Split(Separators);
if (parts.Length < 7)
if (parts.Length < 9)
return null;
// 解析日期
if (!TryParseDate(parts[5], out var testDate))
// 解析日期+序号最后一个字段格式yyyyMMddNN
var dateSeqStr = parts[8];
if (!TryParseDateSequence(dateSeqStr, out var testDate, out var testSequence))
return null;
// 解析测试序号
if (!int.TryParse(parts[6], out var testSequence))
testSequence = 1;
return new ScenarioInfo(
TxPanel: parts[0],
TxHardware: parts[1],
TxSoftware: parts[2],
RxType: parts[3],
TestPurpose: parts.Length > 4 ? parts[4] : null,
Manufacturer: parts[0],
CarModel: parts[1],
TxPanel: parts[2],
TxHardware: parts[3],
TxSoftware: parts[4],
RxType: parts[5],
ForeignObject: parts[6],
TestPurpose: parts[7],
TestDate: testDate,
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)
{
// 支持多种日期格式
var formats = new[] { "yyyyMMdd", "yyyy-MM-dd", "yyMMdd" };
return DateOnly.TryParseExact(dateStr, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out date);
date = default;
sequence = 1;
// 最小长度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>
public record ScenarioInfo(
string Manufacturer,
string CarModel,
string TxPanel,
string TxHardware,
string TxSoftware,
string RxType,
string ForeignObject,
string? TestPurpose,
DateOnly TestDate,
int TestSequence
);
);

View File

@@ -1,13 +1,13 @@
using System.Globalization;
using System.Text.RegularExpressions;
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
/// <summary>
/// Ploss FOD日志解析器 - 支持两行格式
/// Ploss FOD日志解析器 - 两行格式
/// 第一行: pow_loss = 3053, delta_p = 1950
/// 第二行: FOD-> (1) (2) ... (16)
/// 同时兼容旧的单行格式
/// </summary>
public class PlossParser : IParser<PlossRecord>
{
@@ -17,22 +17,19 @@ public class PlossParser : IParser<PlossRecord>
RegexOptions.Compiled
);
// 第二行正则FOD-> 后跟16个数字(均支持负数)
// 第二行正则FOD-> 后跟16个字段
// Field1 支持十六进制格式0X/0x 前缀,如 0C、0xFF或普通十进制
// 其余字段为十进制整数(均支持负数)。
// 共 16 个捕获组Field1 文本 + Field2..Field16对应 PlossRecord 的 Field1..Field16。
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+)$",
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+)$",
@"^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
);
/// <summary>
/// 匹配类型枚举
/// </summary>
public enum MatchType { None, Header, Fod, Legacy }
public enum MatchType { None, Header, Fod }
/// <summary>
/// 单次匹配结果(避免重复正则匹配)
@@ -47,10 +44,6 @@ public class PlossParser : IParser<PlossRecord>
if (fodMatch.Success)
return (MatchType.Fod, fodMatch);
var legacyMatch = LegacyPattern.Match(line);
if (legacyMatch.Success)
return (MatchType.Legacy, legacyMatch);
return (MatchType.None, Match.Empty);
}
@@ -65,7 +58,7 @@ public class PlossParser : IParser<PlossRecord>
public bool IsMultiLineStart(string line) => TryMatch(line).Type == MatchType.Header;
/// <summary>
/// 单行解析(兼容旧格式
/// 单行解析(仅支持FOD行用于错误提示
/// </summary>
public ParseResult<PlossRecord> Parse(string line, Guid scenarioId)
{
@@ -73,15 +66,17 @@ public class PlossParser : IParser<PlossRecord>
return type switch
{
MatchType.Legacy => ParseLegacyFormat(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")
};
}
/// <summary>
/// 多行解析(两行格式)
/// 多行解析(两行格式)
/// </summary>
public ParseResult<PlossRecord>? ParseMultiLine(string[] lines, Guid scenarioId)
{
@@ -103,7 +98,7 @@ public class PlossParser : IParser<PlossRecord>
PowLoss = int.Parse(headerMatch.Groups[1].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),
Field3 = int.Parse(fodMatch.Groups[3].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)
{
// 单独解析FOD行新格式,无header
// 单独解析FOD行无header,返回部分记录
try
{
var record = new PlossRecord
@@ -206,7 +137,7 @@ public class PlossParser : IParser<PlossRecord>
ScenarioId = scenarioId,
PowLoss = null,
DeltaP = null,
Field1 = byte.Parse(match.Groups[1].Value),
Field1 = ParseFieldValue(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),
@@ -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()
.Where(r => !r.IsDeleted)
.Include(r => r.Scenario)
.ThenInclude(s => s!.TxPanel)
.ThenInclude(s => s!.Manufacturer)
.Include(r => r.Scenario)
.ThenInclude(s => s!.TxHardware)
.ThenInclude(s => s!.CarModel)
.Include(r => r.Scenario)
.ThenInclude(s => s!.RxType)
.ToListAsync(cancellationToken);
@@ -79,8 +79,8 @@ public class AggregationService
return records.Select(r => new PlossRow(
r,
r.Scenario,
r.Scenario.TxPanel.Name,
r.Scenario.TxHardware.Version,
r.Scenario.Manufacturer.Name,
r.Scenario.CarModel.Name,
r.Scenario.RxType.Name)).ToList();
}
@@ -109,11 +109,9 @@ public class AggregationService
private ChargingParameterKey BuildKey(TestScenario scenario)
{
var panelName = scenario.TxPanel.Name;
var mapping = _configuration.GetSection("Aggregation:TxPanelMappings")[panelName];
var carFactory = mapping ?? panelName;
var carModel = _configuration[$"Aggregation:CarModelMappings:{scenario.TxHardware.Version}"]
?? scenario.TxHardware.Version;
// 直接使用 Manufacturer 和 CarModel 维度
var carFactory = scenario.Manufacturer.Name;
var carModel = scenario.CarModel.Name;
var (phoneBrand, phoneModel) = SplitPhoneName(scenario.RxType.Name);
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 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 TxHardwareId { get; set; }
public Guid TxSoftwareId { get; set; }
@@ -18,6 +24,11 @@ public class TestScenario
public bool IsDeleted { get; set; } = false;
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 - 维度表
public TxPanel TxPanel { 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.DependencyInjection.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>
</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.Analysis;
using Gpulse.WCT.DataAnalyzer.Core.Application;
using Gpulse.WCT.DataAnalyzer.Core.Application.Exporting;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
@@ -63,6 +64,9 @@ public static class ServiceCollectionExtensions
services.AddScoped<PlossAnalysisService>();
services.AddScoped<QfodCalibrationService>();
// Excel Export
services.AddScoped<AnalyzeExcelExporter>();
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.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)
.IsRequired()
.HasColumnName("tx_panel_id");
@@ -29,6 +38,10 @@ public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
.IsRequired()
.HasColumnName("rx_type_id");
builder.Property(e => e.ForeignObjectId)
.IsRequired()
.HasColumnName("foreign_object_id");
builder.Property(e => e.TestPurpose)
.HasMaxLength(200)
.HasColumnName("test_purpose");
@@ -63,18 +76,31 @@ public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
.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.TxHardwareId).HasDatabaseName("idx_scenario_tx_hardware");
builder.HasIndex(e => e.TxSoftwareId).HasDatabaseName("idx_scenario_tx_software");
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 => 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()
.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)
.WithMany(e => e.TestScenarios)
.HasForeignKey(e => e.TxPanelId)
@@ -95,6 +121,11 @@ public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
.HasForeignKey(e => e.RxTypeId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(e => e.ForeignObject)
.WithMany(e => e.TestScenarios)
.HasForeignKey(e => e.ForeignObjectId)
.OnDelete(DeleteBehavior.Restrict);
// 关系配置 - 数据记录
builder.HasMany(e => e.QfodRecords)
.WithOne(e => e.Scenario)
@@ -109,4 +140,4 @@ public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
// 全局查询过滤器
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);
// 确保数据库和表结构已创建(适用于 SQLite
context.Database.EnsureCreated();
// 迁移Migrate只在启动期由 Program.Main / CliTestHost.CreateAsync 触发,
// 不在每次 scope 解析时执行(避免每条命令都跑迁移)。
return context;
}

View File

@@ -12,10 +12,13 @@ public class WctMinerDbContext : DbContext
: base(options) { }
// 维度表
public DbSet<Manufacturer> Manufacturers => Set<Manufacturer>();
public DbSet<CarModel> CarModels => Set<CarModel>();
public DbSet<TxPanel> TxPanels => Set<TxPanel>();
public DbSet<TxHardware> TxHardwares => Set<TxHardware>();
public DbSet<TxSoftware> TxSoftwares => Set<TxSoftware>();
public DbSet<RxType> RxTypes => Set<RxType>();
public DbSet<ForeignObject> ForeignObjects => Set<ForeignObject>();
// 事实表
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 Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
using Gpulse.WCT.DataAnalyzer.Core.Application.Exporting;
namespace Gpulse.WCT.DataAnalyzer.Commands;
@@ -8,9 +9,12 @@ namespace Gpulse.WCT.DataAnalyzer.Commands;
/// </summary>
public class AnalyzeCommand : Command
{
private static readonly string[] LogExtensions = [".DAT", ".dat", ".LOG", ".log"];
public AnalyzeCommand(
PlossAnalysisService plossAnalysisService,
QfodCalibrationService qfodCalibrationService)
QfodCalibrationService qfodCalibrationService,
AnalyzeExcelExporter excelExporter)
: base("analyze", "Analyze Ploss margin or Qfod calibration threshold from log files")
{
var typeOption = new Option<string>(
@@ -36,31 +40,246 @@ public class AnalyzeCommand : Command
"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(fileOption);
AddOption(pureOption);
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();
// 自动扫描模式
if (auto || (string.IsNullOrEmpty(file) && string.IsNullOrEmpty(pure) && string.IsNullOrEmpty(foreign)))
{
await HandleAutoScanAsync(plossAnalysisService, qfodCalibrationService, excelExporter, type);
return;
}
switch (type)
{
case "ploss":
await HandlePlossAsync(plossAnalysisService, file);
await HandlePlossAsync(plossAnalysisService, excelExporter, file, output);
break;
case "qfod":
await HandleQfodAsync(qfodCalibrationService, pure, foreign);
await HandleQfodAsync(qfodCalibrationService, excelExporter, pure, foreign, output);
break;
default:
Console.WriteLine($"Unknown type: {type}. Use 'ploss' or 'qfod'.");
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))
{
@@ -68,48 +287,27 @@ public class AnalyzeCommand : Command
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;
}
Console.WriteLine($"\n=== Ploss Analysis: {report.FileName} ===");
Console.WriteLine($"Total: {report.TotalCount} (TwoLine: {report.TwoLineCount}, Legacy: {report.LegacyCount}), FOD: {report.FodCount}, Errors: {report.ErrorCount}");
var outputPath = GetOutputPath(file, output);
var normalRatio = Percent(report.NormalAboveCount, report.NormalCount);
var fodRatio = Percent(report.FodAboveCount, report.FodCount);
Console.WriteLine($"Normal margin>2000: {report.NormalAboveCount}/{report.NormalCount} ({normalRatio}%)");
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)");
}
}
await exporter.ExportPlossReportAsync(excelReport, outputPath);
Console.WriteLine($"[OK] {outputPath}");
Console.WriteLine($"Total: {excelReport.TotalCount}, Dropped: {excelReport.DroppedCount}, Errors: {excelReport.ErrorCount}");
}
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))
{
@@ -117,28 +315,17 @@ public class AnalyzeCommand : Command
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;
}
Console.WriteLine("\n=== Qfod Calibration ===");
PrintQfodFileSummary("Pure phone", report.Pure!);
PrintQfodFileSummary("Foreign object", report.Foreign!);
var outputPath = output ?? GetOutputPath(pure, output, "qfod");
Console.WriteLine($"\nThreshold = (pureMax + foreignMin) / 2 = ({report.Pure!.Max} + {report.Foreign!.Min}) / 2 = {report.Threshold}");
Console.WriteLine($"Margin: pureMax - threshold = {report.PureMargin} (负为安全), foreignMin - threshold = {report.ForeignMargin} (正为安全)");
await exporter.ExportQfodReportAsync(excelReport, outputPath);
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>
<Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion>
<!-- 版本信息 -->
<Version>1.1.0</Version>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="Gpulse.WCT.DataAnalyzer.Tests" />
</ItemGroup>
<ItemGroup>
<!-- CLI -->
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
@@ -36,9 +43,11 @@
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<EmbeddedResource Include="appsettings.json" />
</ItemGroup>
<ItemGroup>
<None Include="analyze.bat" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,7 @@
using System.CommandLine;
using System.Reflection;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
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.Application;
using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
using Gpulse.WCT.DataAnalyzer.Core.Application.Exporting;
namespace Gpulse.WCT.DataAnalyzer;
@@ -48,14 +52,14 @@ public class Program
var app = builder.Build();
// 初始化数据库
// 初始化数据库(经 EF 迁移建表/升级,替代 EnsureCreated 以支持既有库升级)
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
await dbContext.Database.EnsureCreatedAsync();
await DatabaseMigrationInitializer.MigrateOrUpgradeAsync(dbContext);
var releaseContext = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>();
await releaseContext.Database.EnsureCreatedAsync();
await DatabaseMigrationInitializer.MigrateOrUpgradeAsync(releaseContext);
// 种子数据
SeedData.Initialize(dbContext);
@@ -73,7 +77,8 @@ public class Program
rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService<CleanService>()));
rootCommand.AddCommand(new AnalyzeCommand(
provider.GetRequiredService<PlossAnalysisService>(),
provider.GetRequiredService<QfodCalibrationService>()));
provider.GetRequiredService<QfodCalibrationService>(),
provider.GetRequiredService<AnalyzeExcelExporter>()));
return await rootCommand.InvokeAsync(args);
}
@@ -97,8 +102,16 @@ public class Program
{
var builder = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true)
.AddEmbeddedResourceJson(
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()
.AddCommandLine(args);
@@ -163,3 +176,84 @@ public class Program
: 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

@@ -0,0 +1,28 @@
{
"Database": {
"Type": "sqlite",
"LocalPath": "./data/database/local.db",
"ReleasePath": "./data/database/release.db"
},
"Parser": { "BatchSize": 1000 },
"Paths": {
"LogDir": "./data/logs",
"OutputDir": "./data/output"
},
"Serilog": {
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"Gpulse.WCT.DataAnalyzer": "Debug"
}
},
"WriteTo": [
{ "Name": "Console" },
{ "Name": "File", "Args": { "path": "./data/logs/gpulse-wct-data-analyzer-.log", "rollingInterval": "Day", "retainedFileCountLimit": 7, "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}" } }
],
"Enrich": ["FromLogContext"]
}
}

View File

@@ -0,0 +1,55 @@
using Gpulse.WCT.DataAnalyzer.Commands;
using Gpulse.WCT.DataAnalyzer.Core.Application;
using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
using Gpulse.WCT.DataAnalyzer.Core.Application.Exporting;
using Microsoft.Extensions.DependencyInjection;
using System.CommandLine;
namespace Gpulse.WCT.DataAnalyzer.Tests;
[TestFixture]
public class CliStructureTests : CommandTestBase
{
/// <summary>按 Program.Main 的方式组装根命令。</summary>
private RootCommand BuildRoot()
{
var root = new RootCommand("Gpulse.WCT.DataAnalyzer - FOD Log Data Parser");
root.AddCommand(new ParseCommand(Host.Services.GetRequiredService<ParseService>()));
root.AddCommand(new AggregateCommand(Host.Services.GetRequiredService<AggregationService>()));
root.AddCommand(new ExportCommand(Host.Services.GetRequiredService<ExportService>()));
root.AddCommand(new CleanCommand(Host.Services.GetRequiredService<CleanService>()));
root.AddCommand(new AnalyzeCommand(
Host.Services.GetRequiredService<PlossAnalysisService>(),
Host.Services.GetRequiredService<QfodCalibrationService>(),
Host.Services.GetRequiredService<AnalyzeExcelExporter>()));
return root;
}
[Test]
public async Task Help_ListsAllSubcommands()
{
var (code, stdout, _) = await BuildRoot().RunCaptureAsync("--help");
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("parse"));
Assert.That(stdout, Does.Contain("aggregate"));
Assert.That(stdout, Does.Contain("export"));
Assert.That(stdout, Does.Contain("clean"));
Assert.That(stdout, Does.Contain("analyze"));
});
}
[Test]
public async Task UnknownCommand_ReturnsError()
{
var (code, stdout, stderr) = await BuildRoot().RunCaptureAsync("nosuchcommand");
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(1));
Assert.That(stdout + stderr, Does.Contain("nosuchcommand"));
});
}
}

View File

@@ -0,0 +1,38 @@
using System.CommandLine;
namespace Gpulse.WCT.DataAnalyzer.Tests;
/// <summary>
/// 命令执行辅助:运行命令并捕获标准输出/标准错误。
/// 命令 handler 直接使用 Console.WriteLine因此重定向 System.Console 即可捕获;
/// System.CommandLine 自身的帮助/校验输出也会经默认控制台写入同一流。
/// </summary>
public static class CliTestExtensions
{
/// <summary>
/// 执行命令并返回 (退出码, 标准输出, 标准错误)。
/// </summary>
public static async Task<(int ExitCode, string StdOut, string StdErr)> RunCaptureAsync(
this Command command,
params string[] args)
{
var originalOut = Console.Out;
var originalError = Console.Error;
using var outWriter = new StringWriter();
using var errWriter = new StringWriter();
Console.SetOut(outWriter);
Console.SetError(errWriter);
try
{
var exitCode = await command.InvokeAsync(args);
return (exitCode, outWriter.ToString(), errWriter.ToString());
}
finally
{
Console.SetOut(originalOut);
Console.SetError(originalError);
}
}
}

View File

@@ -0,0 +1,106 @@
using System.Reflection;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Gpulse.WCT.DataAnalyzer.Tests;
/// <summary>
/// CLI 测试宿主:用内存 SQLite 搭建与 Program.Main 一致的服务容器。
/// 每个测试实例持有独立的「本地库」/「发布库」连接与一个临时目录,互不污染。
/// </summary>
public sealed class CliTestHost : IDisposable
{
private readonly SqliteConnection _localConnection;
private readonly SqliteConnection _releaseConnection;
private readonly ServiceProvider _provider;
private readonly IServiceScope _scope;
/// <summary>测试作用域的服务提供者(与命令执行共用同一作用域)。</summary>
public IServiceProvider Services => _scope.ServiceProvider;
/// <summary>每个测试独立的临时目录,用于放置日志 fixture 文件。</summary>
public string TempDir { get; }
public WctMinerDbContext LocalDb => Services.GetRequiredService<WctMinerDbContext>();
public ReleaseDbContext ReleaseDb => Services.GetRequiredService<ReleaseDbContext>();
private CliTestHost(
SqliteConnection localConnection,
SqliteConnection releaseConnection,
ServiceProvider provider,
IServiceScope scope,
string tempDir)
{
_localConnection = localConnection;
_releaseConnection = releaseConnection;
_provider = provider;
_scope = scope;
TempDir = tempDir;
}
/// <summary>
/// 创建测试宿主:本地库/发布库各用一条独立的内存 SQLite 连接(镜像生产环境的两个库文件),
/// 并做 EnsureCreated 与种子数据初始化。
/// </summary>
public static async Task<CliTestHost> CreateAsync()
{
var tempDir = Directory.CreateTempSubdirectory("wct-tests-").FullName;
// 两个库各自独立连接,避免共享连接时 EnsureCreated 相互干扰
var localConnection = new SqliteConnection("Data Source=:memory:");
await localConnection.OpenAsync();
var releaseConnection = new SqliteConnection("Data Source=:memory:");
await releaseConnection.OpenAsync();
// 从嵌入资源加载 appsettings.json与 Program.Main 生产路径一致
var configuration = new ConfigurationBuilder()
.AddEmbeddedResourceJson(
typeof(Program).Assembly,
"Gpulse.WCT.DataAnalyzer.appsettings.json")
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Database:Type"] = "sqlite",
["Parser:BatchSize"] = "1000",
})
.Build();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration);
services.AddLogging();
services.AddDbContext<WctMinerDbContext>(o => o.UseSqlite(localConnection));
services.AddDbContext<ReleaseDbContext>(o => o.UseSqlite(releaseConnection));
services.AddApplicationServices();
var provider = services.BuildServiceProvider();
// 初始化表结构与种子数据(与 Program.Main 一致,经 EF 迁移建表)
using (var scope = provider.CreateScope())
{
var local = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
await DatabaseMigrationInitializer.MigrateOrUpgradeAsync(local);
SeedData.Initialize(local);
var release = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>();
await DatabaseMigrationInitializer.MigrateOrUpgradeAsync(release);
}
var hostScope = provider.CreateScope();
return new CliTestHost(localConnection, releaseConnection, provider, hostScope, tempDir);
}
public void Dispose()
{
_scope.Dispose();
_provider.Dispose();
_localConnection.Dispose();
_releaseConnection.Dispose();
try { Directory.Delete(TempDir, recursive: true); }
catch { /* 尽力清理临时目录 */ }
}
}

View File

@@ -0,0 +1,60 @@
using Gpulse.WCT.DataAnalyzer.Commands;
using Gpulse.WCT.DataAnalyzer.Core.Application;
using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
using Gpulse.WCT.DataAnalyzer.Core.Application.Exporting;
using Microsoft.Extensions.DependencyInjection;
namespace Gpulse.WCT.DataAnalyzer.Tests;
/// <summary>
/// CLI 命令测试基类:每个测试一个独立的内存数据库 + 临时目录。
/// </summary>
public abstract class CommandTestBase
{
protected CliTestHost Host { get; private set; } = null!;
[SetUp]
public async Task SetUp()
{
// 清理 Datas 和 Output 文件夹,避免测试间干扰
CleanupDatasAndOutput();
Host = await CliTestHost.CreateAsync();
}
[TearDown]
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 解析服务再构造命令
protected ParseCommand Parse() => new(Host.Services.GetRequiredService<ParseService>());
protected AggregateCommand Aggregate() => new(Host.Services.GetRequiredService<AggregationService>());
protected ExportCommand Export() => new(Host.Services.GetRequiredService<ExportService>());
protected CleanCommand Clean() => new(Host.Services.GetRequiredService<CleanService>());
protected AnalyzeCommand Analyze() => new(
Host.Services.GetRequiredService<PlossAnalysisService>(),
Host.Services.GetRequiredService<QfodCalibrationService>(),
Host.Services.GetRequiredService<AnalyzeExcelExporter>());
/// <summary>在测试临时目录下新建一个子目录。</summary>
protected string NewDir() =>
Directory.CreateDirectory(Path.Combine(Host.TempDir, Guid.NewGuid().ToString("N"))).FullName;
}

View File

@@ -0,0 +1,85 @@
using Microsoft.EntityFrameworkCore;
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
[TestFixture]
public class AggregateCommandTests : CommandTestBase
{
[Test]
public async Task Aggregate_EmptyDatabase_ReportsZero()
{
var (code, stdout, _) = await Aggregate().RunCaptureAsync();
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("Aggregated 0 local Ploss records into 0 release records."));
});
}
[Test]
public async Task Aggregate_AfterParse_GroupsByThresholdIntoReleaseDb()
{
var dir = NewDir();
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
new TestFixtures.PlossTwoLineRow(Field7: 100, Field9: 350, Field10: 350),
new TestFixtures.PlossTwoLineRow(Field7: 110, Field9: 350, Field10: 350),
new TestFixtures.PlossTwoLineRow(Field7: 200, Field9: 500, Field10: 500));
await Parse().RunCaptureAsync("--file", file);
var (code, stdout, _) = await Aggregate().RunCaptureAsync();
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("Aggregated 3 local Ploss records into 1 release records."));
});
var record = await Host.ReleaseDb.ChargingParameters.SingleAsync();
Assert.Multiple(() =>
{
// 维度来自文件名:奇瑞(车厂) / E5(车型) / iPhone15(RxType)
Assert.That(record.CarFactory, Is.EqualTo("奇瑞"));
Assert.That(record.CarModel, Is.EqualTo("E5"));
Assert.That(record.PhoneModel, Is.EqualTo("iPhone15"));
// Field9=350 的两行 Field7 取平均 = (100+110)/2
Assert.That(record.Power350mW, Is.EqualTo(105));
Assert.That(record.Power500mW, Is.EqualTo(200));
});
}
[Test]
public async Task Aggregate_DefaultRebuild_DoesNotDuplicateOnSecondRun()
{
var dir = NewDir();
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
new TestFixtures.PlossTwoLineRow(Field7: 100, Field9: 350, Field10: 350));
await Parse().RunCaptureAsync("--file", file);
await Aggregate().RunCaptureAsync();
await Aggregate().RunCaptureAsync();
Assert.That(await Host.ReleaseDb.ChargingParameters.CountAsync(), Is.EqualTo(1));
}
[Test]
public async Task Aggregate_RebuildFalse_InsertsIntoEmptyTable()
{
// 注:发布库对业务键 (车厂/车型/手机厂商/型号) 有唯一索引;
// append 模式会对全部本地记录重新入库,因此只适合表为空时使用,
// 对已聚合过的同一数据再 append 会因唯一索引冲突而失败(聚合命令捕获并输出 "Aggregation failed")。
var dir = NewDir();
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
new TestFixtures.PlossTwoLineRow(Field7: 100, Field9: 350, Field10: 350));
await Parse().RunCaptureAsync("--file", file);
var (code, stdout, _) = await Aggregate().RunCaptureAsync("--rebuild", "false");
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("Aggregated 1 local Ploss records into 1 release records."));
});
Assert.That(await Host.ReleaseDb.ChargingParameters.CountAsync(), Is.EqualTo(1));
}
}

View File

@@ -0,0 +1,511 @@
using ClosedXML.Excel;
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
[TestFixture]
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]
public async Task Analyze_NoType_ReturnsValidationError()
{
var (code, stdout, stderr) = await Analyze().RunCaptureAsync();
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(1));
Assert.That(stdout + stderr, Does.Contain("--type"));
});
}
[Test]
public async Task Analyze_UnknownType_WithFile_PrintsMessage()
{
// 确保没有 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.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("Unknown type: foo. Use 'ploss' or 'qfod'."));
});
}
[Test]
public async Task Analyze_Ploss_MissingFile_TriggersAutoScan()
{
// 不提供文件路径时触发自动扫描
CleanupDatasAndOutput();
var (_, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss");
// 没有 Datas 文件夹时报错
Assert.That(stdout, Does.Contain("未找到 Datas 文件夹"));
}
[Test]
public async Task Analyze_Ploss_ValidTwoLineFile_GeneratesExcel()
{
var dir = NewDir();
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
// 正常两行Field10=350功率段理论Δp = 6000-800-2200-500=2500
new TestFixtures.PlossTwoLineRow(Field10: 350),
// FOD 两行Field10=500功率段
new TestFixtures.PlossTwoLineRow(Field5: 8000, Field9: 2500, Field10: 500, Field12: 1));
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(File.Exists(outputPath), Is.True);
});
}
[Test]
public async Task Analyze_Qfod_MissingArgs_TriggersAutoScan()
{
// 不提供文件路径时触发自动扫描
CleanupDatasAndOutput();
var (_, stdout, _) = await Analyze().RunCaptureAsync("--type", "qfod");
// 没有 Datas 文件夹时报错
Assert.That(stdout, Does.Contain("未找到 Datas 文件夹"));
}
[Test]
public async Task Analyze_Qfod_ValidFiles_GeneratesExcel()
{
var dir = NewDir();
var pure = TestFixtures.WriteQfodLog(
dir, "singleMold-v1.0-hex2_1-iPhone15-qfod_pure-20260710-1.log", 20, 25, 21);
var foreign = TestFixtures.WriteQfodLog(
dir, "singleMold-v1.0-hex2_1-iPhone15-qfod_foreign-20260710-1.log", 42, 40, 45);
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]
public async Task Analyze_Qfod_MissingFile_ReportsFailure()
{
var dir = NewDir();
var foreign = TestFixtures.WriteQfodLog(
dir, "singleMold-v1.0-hex2_1-iPhone15-qfod_foreign-20260710-1.log", 42, 40);
var (_, stdout, _) = await Analyze().RunCaptureAsync(
"--type", "qfod",
"--pure", Path.Combine(dir, "nope.log"),
"--foreign", foreign);
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,81 @@
using Microsoft.EntityFrameworkCore;
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
[TestFixture]
public class CleanCommandTests : CommandTestBase
{
[Test]
public async Task Clean_NoConfirm_PrintsPrompt()
{
var (code, stdout, _) = await Clean().RunCaptureAsync("--all");
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("Please add --confirm to confirm the clean operation."));
});
}
[Test]
public async Task Clean_InvalidScenarioGuid_PrintsMessage()
{
var (_, stdout, _) = await Clean().RunCaptureAsync("--scenario", "not-a-guid", "--confirm");
Assert.That(stdout, Does.Contain("Invalid scenario ID: not-a-guid"));
}
[Test]
public async Task Clean_Scenario_NotExist_PrintsZeroCount()
{
var scenarioId = Guid.NewGuid();
var (_, stdout, _) = await Clean().RunCaptureAsync("--scenario", scenarioId.ToString(), "--confirm");
Assert.That(stdout, Does.Contain($"Cleaned 0 records for scenario {scenarioId}"));
}
[Test]
public async Task Clean_Scenario_WithConfirm_SoftDeletesRelatedRecords()
{
var dir = NewDir();
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
new TestFixtures.PlossTwoLineRow(),
new TestFixtures.PlossTwoLineRow(Field9: 2500, Field12: 1));
await Parse().RunCaptureAsync("--file", file);
var scenarioId = await Host.LocalDb.TestScenarios.Select(s => s.Id).SingleAsync();
var (code, stdout, _) = await Clean().RunCaptureAsync("--scenario", scenarioId.ToString(), "--confirm");
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain($"Cleaned 3 records for scenario {scenarioId}"));
});
// 2 条 Ploss + 1 个场景全部软删除
Assert.That(await Host.LocalDb.PlossRecords.IgnoreQueryFilters().CountAsync(r => r.IsDeleted), Is.EqualTo(2));
Assert.That(await Host.LocalDb.TestScenarios.IgnoreQueryFilters().CountAsync(s => s.IsDeleted), Is.EqualTo(1));
}
[Test]
public async Task Clean_All_WithConfirm_SoftDeletesEverything()
{
var dir = NewDir();
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
new TestFixtures.PlossTwoLineRow(),
new TestFixtures.PlossTwoLineRow());
await Parse().RunCaptureAsync("--file", file);
var (code, stdout, _) = await Clean().RunCaptureAsync("--all", "--confirm");
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("Cleaned 3 records in total"));
});
Assert.That(await Host.LocalDb.PlossRecords.IgnoreQueryFilters().CountAsync(r => r.IsDeleted), Is.EqualTo(2));
Assert.That(await Host.LocalDb.TestScenarios.IgnoreQueryFilters().CountAsync(s => s.IsDeleted), Is.EqualTo(1));
}
}

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

@@ -0,0 +1,105 @@
using Gpulse.WCT.DataAnalyzer.Core.Domain.Release;
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
[TestFixture]
public class ExportCommandTests : CommandTestBase
{
private static string ResolveOutputDir(params string[] segments) =>
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, string.Join(Path.DirectorySeparatorChar, segments));
[Test]
public async Task Export_ChargingParametersCsv_WritesFile()
{
Host.ReleaseDb.ChargingParameters.Add(new ChargingParameterRecord
{
CarFactory = "TestFactory",
CarModel = "ModelX",
PhoneBrand = "Apple",
PhoneModel = "iPhone15",
Power350mW = 100,
Power500mW = 200,
});
await Host.ReleaseDb.SaveChangesAsync();
var (code, stdout, _) = await Export().RunCaptureAsync(
"--format", "csv",
"--type", "charging-parameters",
"--output", "data/output/export_cp");
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("ChargingParameterDatabase CSV exported"));
});
var file = Path.Combine(ResolveOutputDir("data", "output", "export_cp"), "WCT-ChargingParameterDatabase.csv");
Assert.That(File.Exists(file), Is.True);
var content = await File.ReadAllTextAsync(file);
Assert.Multiple(() =>
{
Assert.That(content, Does.Contain("车厂"));
Assert.That(content, Does.Contain("TestFactory"));
Assert.That(content, Does.Contain("iPhone15"));
});
}
[Test]
public async Task Export_QfodCsv_AfterParse_WritesFile()
{
var dir = NewDir();
var file = TestFixtures.WriteQfodLog(dir, TestFixtures.QfodFileName, 20, 25, 21);
await Parse().RunCaptureAsync("--file", file);
// 导出文件名带时间戳,先清空输出目录避免跨运行残留文件
var outDir = ResolveOutputDir("data", "output", "export_qfod");
if (Directory.Exists(outDir))
Directory.Delete(outDir, recursive: true);
var (code, stdout, _) = await Export().RunCaptureAsync(
"--format", "csv",
"--type", "qfod",
"--output", "data/output/export_qfod");
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("Qfod CSV exported"));
});
var exported = Directory.GetFiles(outDir, "qfod_export_*.csv").Single();
var content = await File.ReadAllTextAsync(exported);
Assert.Multiple(() =>
{
Assert.That(content, Does.Contain("delta_q"));
Assert.That(content, Does.Contain("iPhone15"));
});
}
[Test]
public async Task Export_UnknownFormat_PrintsMessage()
{
var (code, stdout, _) = await Export().RunCaptureAsync("--format", "xml");
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("Unknown format: xml. Use 'csv' or 'json'."));
});
}
[Test]
public async Task Export_PathTraversal_PrintsFailure()
{
var (code, stdout, _) = await Export().RunCaptureAsync(
"--format", "csv",
"--output", "../evil");
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("Export failed"));
});
}
}

View File

@@ -0,0 +1,178 @@
using Microsoft.EntityFrameworkCore;
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
[TestFixture]
public class ParseCommandTests : CommandTestBase
{
[Test]
public async Task Parse_File_Missing_ReturnsFailureReport()
{
var missing = Path.Combine(Host.TempDir, "not-exist.log");
var (code, stdout, _) = await Parse().RunCaptureAsync("--file", missing);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("not-exist.log"));
Assert.That(stdout, Does.Contain("FAILED"));
});
}
[Test]
public async Task Parse_File_WithBadFileName_ReportsFilenameError()
{
var dir = NewDir();
var file = Path.Combine(dir, "bad-name.log");
await File.WriteAllTextAsync(file, "0#:Q 1->25 45.5 20.5 2 0\n");
var (code, stdout, _) = await Parse().RunCaptureAsync("--file", file);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("bad-name.log"));
Assert.That(stdout, Does.Contain("文件名格式不匹配"));
});
}
[Test]
public async Task Parse_File_QfodLog_StoresRecordsAndReportsCount()
{
var dir = NewDir();
var file = TestFixtures.WriteQfodLog(dir, TestFixtures.QfodFileName, 20, 25, 21, -8);
var (code, stdout, _) = await Parse().RunCaptureAsync("--file", file);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain(TestFixtures.QfodFileName));
Assert.That(stdout, Does.Contain("Qfod: 4"));
Assert.That(stdout, Does.Contain("Ploss: 0"));
});
Assert.That(await Host.LocalDb.QfodRecords.CountAsync(), Is.EqualTo(4));
}
[Test]
public async Task Parse_File_PlossLog_StoresRecordsAndReportsCount()
{
var dir = NewDir();
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
new TestFixtures.PlossTwoLineRow(),
new TestFixtures.PlossTwoLineRow(Field9: 2500, Field10: 3000, Field12: 1));
var (code, stdout, _) = await Parse().RunCaptureAsync("--file", file);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("Qfod: 0"));
Assert.That(stdout, Does.Contain("Ploss: 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]
public async Task Parse_File_WithForceOption_Accepted()
{
var dir = NewDir();
var file = TestFixtures.WriteQfodLog(dir, TestFixtures.QfodFileName, 20, 25, 21);
var (code, stdout, _) = await Parse().RunCaptureAsync("--file", file, "--force");
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("Qfod: 3"));
});
}
[Test]
public async Task Parse_Dir_ReportsSummaryAndTotals()
{
var dir = NewDir();
TestFixtures.WriteQfodLog(dir, TestFixtures.QfodFileName, 20, 25, 21);
TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
new TestFixtures.PlossTwoLineRow(),
new TestFixtures.PlossTwoLineRow());
var (code, stdout, _) = await Parse().RunCaptureAsync("--dir", dir);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("2 files processed"));
Assert.That(stdout, Does.Contain("Total: Qfod=3, Ploss=2, Errors=0"));
});
}
[Test]
public async Task Parse_Dir_Recursive_IncludesSubfolders()
{
var root = NewDir();
var sub = Directory.CreateDirectory(Path.Combine(root, "sub")).FullName;
TestFixtures.WriteQfodLog(root, TestFixtures.QfodFileName, 20, 25);
TestFixtures.WritePlossLog(sub, TestFixtures.PlossFileName, new TestFixtures.PlossTwoLineRow());
var (_, plain, _) = await Parse().RunCaptureAsync("--dir", root);
Assert.That(plain, Does.Contain("1 files processed"));
var (_, recursive, _) = await Parse().RunCaptureAsync("--dir", root, "--recursive");
Assert.That(recursive, Does.Contain("2 files processed"));
}
[Test]
public async Task Parse_Dir_NotExists_ReturnsFailureReport()
{
var missing = Path.Combine(Host.TempDir, "no-such-dir");
var (code, stdout, _) = await Parse().RunCaptureAsync("--dir", missing);
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("FAILED"));
Assert.That(stdout, Does.Contain("目录不存在"));
});
}
[Test]
public async Task Parse_NeitherFileNorDir_PrintsPrompt()
{
var (code, stdout, _) = await Parse().RunCaptureAsync();
Assert.Multiple(() =>
{
Assert.That(code, Is.EqualTo(0));
Assert.That(stdout, Does.Contain("Please specify --file or --dir"));
});
}
}

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

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

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Gpulse.WCT.DataAnalyzer.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<!-- 测试框架 -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<!-- 测试宿主依赖Core 只引用 Abstractions这里补具体实现 -->
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Gpulse.WCT.DataAnalyzer\Gpulse.WCT.DataAnalyzer.csproj" />
<ProjectReference Include="..\..\src\Gpulse.WCT.DataAnalyzer.Core\Gpulse.WCT.DataAnalyzer.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,80 @@
namespace Gpulse.WCT.DataAnalyzer.Tests;
/// <summary>
/// 构造 CLI 测试用的日志 fixture 文件。文件名符合 FileNameParser 约定的
/// `车厂-车型-TX面板类型-TX硬件-TX软件-RX类型-异物类型-测试目的-年月日测试次数` 格式。
/// </summary>
public static class TestFixtures
{
/// <summary>
/// 新格式文件名示例:
/// 车厂-车型-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>
/// 两行 Ploss 记录header + FOD 16 字段)。
/// 默认值让恒等式成立Field5 - Field4 - PowLoss - ploss == DeltaP且余量 > 2000。
/// Field10 为功率段,默认 350。
/// </summary>
/// <param name="Field1AsHex">
/// 当不为 null 时,覆写 Field1按十六进制字面量写入日志如 "0C" = 十进制 12
/// 用于验证解析器对 Field1 十六进制格式的支持。
/// </param>
public sealed record PlossTwoLineRow(
int PowLoss = 2200,
int DeltaP = 2500,
int Field1 = 1,
string? Field1AsHex = null,
int Field2 = 0,
int Field3 = 0,
int Field4 = 800, // RX power
int Field5 = 6000, // TX power
int Field6 = 0,
int Field7 = 0,
int Field8 = 0,
int Field9 = 500, // ploss
int Field10 = 350, // threshold/power segment (350/500/750/1000/1250/1500/1750/2000/2250)
int Field11 = 0,
int Field12 = 0, // FOD 标志
int Field13 = 0,
int Field14 = 0,
int Field15 = 0,
int Field16 = 0);
/// <summary>写两行格式 Ploss 日志文件。</summary>
public static string WritePlossLog(string dir, string fileName, params PlossTwoLineRow[] rows)
{
var path = Path.Combine(dir, fileName);
var lines = new List<string>(rows.Length * 2);
foreach (var r in rows)
{
lines.Add($"pow_loss = {r.PowLoss}, delta_p = {r.DeltaP}");
var field1 = r.Field1AsHex ?? r.Field1.ToString();
lines.Add(
$"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.Field13} {r.Field14} {r.Field15} {r.Field16}");
}
File.WriteAllLines(path, lines);
return path;
}
/// <summary>写 Qfod 日志文件,每行 DeltaQ 一个。</summary>
public static string WriteQfodLog(string dir, string fileName, params int[] deltaQs)
{
var path = Path.Combine(dir, fileName);
var lines = deltaQs
.Select((d, i) =>
{
var charger = i % 2;
var coil = i % 3;
var currentQ = 40 + d;
return $"{charger}#:Q {coil}->{d} {currentQ}.0 20.0 0 0";
})
.ToArray();
File.WriteAllLines(path, lines);
return path;
}
}