Compare commits
25 Commits
2b031995e1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fd788bc7f | ||
|
|
a554000674 | ||
|
|
df5234ffe0 | ||
|
|
0677f2ea15 | ||
|
|
7a8a1e9a87 | ||
|
|
ee8ea29265 | ||
|
|
820bc1c2bc | ||
|
|
abaa9cb6d1 | ||
|
|
3951606cfa | ||
|
|
b0b55c0d4e | ||
|
|
ccf3885c38 | ||
|
|
81c5316eac | ||
|
|
eba1d2080c | ||
|
|
348d11c480 | ||
|
|
6d56a4a01b | ||
|
|
36c21f03dd | ||
|
|
759da546ee | ||
|
|
d45d9f4ca3 | ||
|
|
0667e2e9b8 | ||
|
|
b88c07356e | ||
|
|
dea7af045b | ||
|
|
7cd08f934a | ||
|
|
430b38b361 | ||
|
|
77d4bfe953 | ||
|
|
f3472d21ee |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -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/
|
||||
@@ -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
|
||||
|
||||
238
docs/AnalyzeCommand使用指南.md
Normal file
238
docs/AnalyzeCommand使用指南.md
Normal 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/{车厂}/{车型}/{手机名}/`
|
||||
|
||||
---
|
||||
|
||||
## 联系支持
|
||||
|
||||
如有问题,请联系开发团队。
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
| `parse` | 解析日志文件到本地库 | log → local.db |
|
||||
| `aggregate` | 从本地库汇总生成发布库 | local.db → release.db |
|
||||
| `export` | 导出发布库到 CSV | release.db → CSV |
|
||||
| `stats` | 查询本地库统计信息 | 只读 local.db |
|
||||
| `clean` | 清理本地库数据(软删除) | 只写 local.db |
|
||||
| `analyze` | 独立分析:Ploss 余量/delta_p、Qfod 标定阈值 | log → 控制台报告 |
|
||||
|
||||
---
|
||||
|
||||
@@ -60,26 +60,7 @@ dotnet run export --type qfod --format csv
|
||||
dotnet run export --format json --type qfod
|
||||
```
|
||||
|
||||
### 2.4 stats
|
||||
|
||||
```bash
|
||||
# 查看整体统计
|
||||
dotnet run stats --summary
|
||||
|
||||
# 按 FOD 类型统计
|
||||
dotnet run stats --by-fod-type
|
||||
|
||||
# 按面板统计
|
||||
dotnet run stats --by-panel
|
||||
|
||||
# 按硬件版本统计
|
||||
dotnet run stats --by-hardware
|
||||
|
||||
# 按 RX 类型统计
|
||||
dotnet run stats --by-rx-type
|
||||
```
|
||||
|
||||
### 2.5 clean
|
||||
### 2.4 clean
|
||||
|
||||
```bash
|
||||
# 清理指定场景
|
||||
@@ -91,19 +72,36 @@ dotnet run clean --all --confirm
|
||||
|
||||
---
|
||||
|
||||
### 2.5 analyze
|
||||
|
||||
```bash
|
||||
# Ploss 余量 / delta_p 分析(单文件,直接解析日志,不依赖本地库)
|
||||
dotnet run analyze --type ploss --file <log>
|
||||
|
||||
# Qfod 标定阈值(两个文件:纯手机 / 手机+异物)
|
||||
dotnet run analyze --type qfod --pure <纯手机.log> --foreign <手机+异物.log>
|
||||
```
|
||||
|
||||
- `analyze` 为**独立分析**,不进入 `parse → aggregate` 发布管线,也不写任何文件(仅控制台报告)。
|
||||
- Ploss 口径以实体语义为准(Field9=ploss、Field10=threshold、Field12=FOD 标志):
|
||||
- FOD 报警 = `Field12 == 1`
|
||||
- 安全余量 `Margin = Threshold − Ploss > 2000`
|
||||
- 恒等式校验 `delta_p = Tx − Rx − pow_loss − Ploss`(仅两行格式)
|
||||
- delta_p 反解(两行格式且 FOD=1):`delta_p' = DeltaP + 2001 + Ploss − Threshold`,控制台输出重写后的 header 行
|
||||
- Qfod 标定阈值:`Threshold = (纯手机 ΔQ 最大值 + 手机+异物 ΔQ 最小值) / 2`。因现无法自动区分两个日志文件,需显式传入 `--pure`/`--foreign`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 典型工作流
|
||||
|
||||
```bash
|
||||
# 1. 解析日志
|
||||
dotnet run parse --dir ./data/test_input --recursive
|
||||
|
||||
# 2. 查看统计
|
||||
dotnet run stats --summary
|
||||
|
||||
# 3. 汇总生成发布库
|
||||
# 2. 汇总生成发布库
|
||||
dotnet run aggregate
|
||||
|
||||
# 4. 导出正式 CSV
|
||||
# 3. 导出正式 CSV
|
||||
dotnet run export --output ./data/output
|
||||
```
|
||||
|
||||
|
||||
@@ -28,15 +28,15 @@ FOD-> (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13) (14) (15) (16)
|
||||
| (1) | Field1 | int | 待定义 |
|
||||
| (2) | Field2 | int | 待定义 |
|
||||
| (3) | Field3 | int | 待定义 |
|
||||
| (4) | Field4 | int | 待定义 |
|
||||
| (5) | Field5 | int | 待定义 |
|
||||
| (4) | Field4 | int | RX power - 接收端功率 |
|
||||
| (5) | Field5 | int | TX power - 发射端功率 |
|
||||
| (6) | Field6 | int | 待定义 |
|
||||
| (7) | Field7 | int | 待定义 |
|
||||
| (8) | Field8 | int | 待定义 |
|
||||
| (9) | Field9 | int | 待定义 |
|
||||
| (10) | Field10 | int | 待定义 |
|
||||
| (11) | Field11 | int | 待定义 |
|
||||
| (12) | Field12 | int | 待定义 |
|
||||
| (9) | Field9 | int | ploss - 功率损耗计算值 |
|
||||
| (10) | Field10 | int | threshold - 阈值 |
|
||||
| (11) | Field11 | int | 触发次数 - 当 Field9 > Field10(ploss 大于阈值)时计数,超过5次触发FOD保护 |
|
||||
| (12) | Field12 | int | PFOD result 标志 - 触发时置位 |
|
||||
| (13) | Field13 | int | 待定义 |
|
||||
| (14) | Field14 | int | 待定义 |
|
||||
| (15) | Field15 | int | 待定义 |
|
||||
@@ -99,127 +99,78 @@ private static readonly Regex FodPattern = new(
|
||||
|
||||
## 4. 解析器实现
|
||||
|
||||
> 当前实现位于 `src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/PlossParser.cs`,实现 `IParser<PlossRecord>` 接口。
|
||||
|
||||
解析分两步:
|
||||
|
||||
1. **单行识别**:`TryMatch(line)` 依次尝试 `HeaderPattern` → `FodPattern` → `LegacyPattern`,返回匹配类型(`None/Header/Fod/Legacy`),避免重复正则匹配。
|
||||
2. **多行合并**:`ParseService` 检测到 header 行后暂存,下一行为 Ploss 行时调用 `ParseMultiLine([header, fod], scenarioId)` 合并为一条完整记录;文件末尾残留的 header 行记为解析错误。
|
||||
|
||||
```csharp
|
||||
// Parsers/PlossParser.cs
|
||||
using System.Text.RegularExpressions;
|
||||
using Gpulse.WCT.DataAnalyzer.Models;
|
||||
public enum MatchType { None, Header, Fod, Legacy }
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Parsers;
|
||||
|
||||
public class PlossParser : IParser<PlossRecord>
|
||||
public (MatchType Type, Match Match) TryMatch(string line)
|
||||
{
|
||||
private static readonly Regex HeaderPattern = new(
|
||||
@"^pow_loss\s*=\s*(\d+),\s*delta_p\s*=\s*(\d+)$",
|
||||
RegexOptions.Compiled
|
||||
);
|
||||
var headerMatch = HeaderPattern.Match(line);
|
||||
if (headerMatch.Success) return (MatchType.Header, headerMatch);
|
||||
|
||||
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
|
||||
);
|
||||
var fodMatch = FodPattern.Match(line);
|
||||
if (fodMatch.Success) return (MatchType.Fod, fodMatch);
|
||||
|
||||
public bool CanParse(string line) => HeaderPattern.IsMatch(line) || FodPattern.IsMatch(line);
|
||||
var legacyMatch = LegacyPattern.Match(line);
|
||||
if (legacyMatch.Success) return (MatchType.Legacy, legacyMatch);
|
||||
|
||||
public ParseResult<PlossRecord> Parse(string line, Guid scenarioId)
|
||||
{
|
||||
// 尝试匹配第一行
|
||||
var headerMatch = HeaderPattern.Match(line);
|
||||
if (headerMatch.Success)
|
||||
{
|
||||
return ParseHeaderLine(headerMatch, scenarioId);
|
||||
}
|
||||
return (MatchType.None, Match.Empty);
|
||||
}
|
||||
|
||||
// 尝试匹配第二行
|
||||
var fodMatch = FodPattern.Match(line);
|
||||
if (fodMatch.Success)
|
||||
{
|
||||
return ParseFodLine(fodMatch, scenarioId);
|
||||
}
|
||||
public bool CanParse(string line) => TryMatch(line).Type != MatchType.None;
|
||||
public bool IsMultiLineStart(string line) => TryMatch(line).Type == MatchType.Header;
|
||||
|
||||
return ParseResult<PlossRecord>.Failure(
|
||||
"FORMAT_MISMATCH",
|
||||
"Line does not match Ploss pattern"
|
||||
);
|
||||
}
|
||||
public ParseResult<PlossRecord>? ParseMultiLine(string[] lines, Guid scenarioId)
|
||||
{
|
||||
if (lines.Length < 2 || lines[0] == null || lines[1] == null)
|
||||
return null;
|
||||
|
||||
private ParseResult<PlossRecord> ParseHeaderLine(Match match, Guid scenarioId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var record = new PlossRecord
|
||||
{
|
||||
ScenarioId = scenarioId,
|
||||
PowLoss = int.Parse(match.Groups[1].Value),
|
||||
DeltaP = int.Parse(match.Groups[2].Value),
|
||||
IsHeaderLine = true
|
||||
};
|
||||
return ParseResult<PlossRecord>.Success(record);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ParseResult<PlossRecord>.Failure(
|
||||
"TYPE_CONVERSION",
|
||||
$"Failed to convert header field: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
var headerMatch = HeaderPattern.Match(lines[0].Trim());
|
||||
var fodMatch = FodPattern.Match(lines[1].Trim());
|
||||
if (!headerMatch.Success || !fodMatch.Success)
|
||||
return null;
|
||||
|
||||
private ParseResult<PlossRecord> ParseFodLine(Match match, Guid scenarioId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var record = new PlossRecord
|
||||
{
|
||||
ScenarioId = scenarioId,
|
||||
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),
|
||||
Field5 = int.Parse(match.Groups[5].Value),
|
||||
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),
|
||||
Field10 = int.Parse(match.Groups[10].Value),
|
||||
Field11 = int.Parse(match.Groups[11].Value),
|
||||
Field12 = int.Parse(match.Groups[12].Value),
|
||||
Field13 = int.Parse(match.Groups[13].Value),
|
||||
Field14 = int.Parse(match.Groups[14].Value),
|
||||
Field15 = int.Parse(match.Groups[15].Value),
|
||||
Field16 = int.Parse(match.Groups[16].Value),
|
||||
IsHeaderLine = false
|
||||
};
|
||||
return ParseResult<PlossRecord>.Success(record);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ParseResult<PlossRecord>.Failure(
|
||||
"TYPE_CONVERSION",
|
||||
$"Failed to convert FOD field: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
// 第一行 header 组1/组2 → PowLoss/DeltaP
|
||||
// 第二行 FOD 组1~16 → Field1~Field16
|
||||
return ParseResult<PlossRecord>.Success(new PlossRecord { /* ... */ });
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:实际实现中,两行日志需要合并为一条完整记录。可以使用 `PlossRecordMerger` 或在业务逻辑层处理合并。
|
||||
**多行合并(ParseMultiLine)捕获组映射:**
|
||||
|
||||
| 来源 | 捕获组 | 字段 |
|
||||
|------|--------|------|
|
||||
| 第一行 header | Group 1 / Group 2 | PowLoss / DeltaP |
|
||||
| 第二行 FOD | Group 1 ~ Group 16 | Field1 ~ Field16 |
|
||||
|
||||
> **旧单行格式**:无 header 行,`Field1~13` 直接来自 `FOD->` 后 13 个数字,`Field14~16` 补默认值 0,`PowLoss/DeltaP` 为 null。字段语义以 `PlossRecord` 实体注释为准。
|
||||
|
||||
---
|
||||
|
||||
## 5. 核心判定逻辑
|
||||
|
||||
> 字段语义以 `PlossRecord` 实体注释为准:`Field9` = ploss(功率损耗计算值)、`Field10` = threshold(阈值)、`Field11` = 触发次数。
|
||||
|
||||
### 5.1 FOD 触发条件
|
||||
|
||||
```
|
||||
if (Ploss > Threshold) → 计数器累积
|
||||
if (Count > Limit) → 触发 FOD 报警,停止充电
|
||||
if (Field9 > Field10) → Field11 计数累积
|
||||
if (Field11 > 5) → 触发 FOD 报警,停止充电
|
||||
```
|
||||
|
||||
> **说明**:触发条件统一为 `Field9 > Field10`(ploss > threshold),实体 `Field11` 注释已同步此口径。
|
||||
|
||||
### 5.2 安全余量计算
|
||||
|
||||
```csharp
|
||||
// 派生属性(不落库)
|
||||
Margin = Threshold - Ploss
|
||||
// 派生属性(不落库) = Field10 - Field9
|
||||
Margin = Field10 - Field9
|
||||
```
|
||||
|
||||
| Margin 状态 | 含义 |
|
||||
|
||||
@@ -75,7 +75,7 @@ sequenceDiagram
|
||||
CLI->>AS: AggregateAsync()
|
||||
AS->>LDB: 读取所有 PlossRecord + 关联维度
|
||||
AS->>AS: 按 (车厂, 车型, 手机厂商, 型号) 分组
|
||||
AS->>AS: 计算 9 个功率列平均值
|
||||
AS->>AS: 按 Threshold(Field9) 分桶计算 9 个功率列
|
||||
AS->>AS: 计算 Q值/Q基值/P-Q系数
|
||||
AS->>RDB: DELETE + INSERT (事务)
|
||||
AS-->>CLI: AggregationReport
|
||||
@@ -89,12 +89,14 @@ sequenceDiagram
|
||||
| 车型 (CarModel) | 配置映射 `Aggregation:CarModelMappings` | fallback 为 TxHardware.Version |
|
||||
| 手机厂商 (PhoneBrand) | RxType.Name 拆分 | 取第一个分隔符前部分 |
|
||||
| 型号 (PhoneModel) | RxType.Name 拆分 | 取第一个分隔符后部分 |
|
||||
| 350mW~2250mW(9列) | PlossRecord.Field7 | 按功率匹配后取平均值 |
|
||||
| 350mW~2250mW(9列) | PlossRecord.Field7 | 按 Threshold(Field9) 分桶(记录阈值=档位),取 Field7 平均值 |
|
||||
| Q值 (QValue) | QfodRecord.CurrentQ | 平均值 |
|
||||
| Q基值 (QBaseValue) | QfodRecord.RawQ | 平均值 |
|
||||
| P-Q值系数 (PqCoefficient) | PlossRecord.DeltaP | 平均值 |
|
||||
| 谐振频率 (ResonanceFrequency) | 暂无 | 允许为空 |
|
||||
|
||||
> 各列的详细计算规则(含公式、功率匹配与空值逻辑)见 [§5 输出 CSV 各列计算详解](#5-输出-csv-各列计算详解)。
|
||||
|
||||
**替代配置:**
|
||||
|
||||
配置文件 `appsettings.json` 中可添加映射:
|
||||
@@ -140,7 +142,64 @@ sequenceDiagram
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据库隔离
|
||||
## 5. 输出 CSV 各列计算详解
|
||||
|
||||
> 最终 `WCT-ChargingParameterDatabase.csv` 的每一列都在 aggregate 阶段由 `AggregationService`
|
||||
> 计算写入 release.db,export 阶段仅按 (车厂, 车型, 手机厂商, 型号) 排序后原样写出。
|
||||
> 所有数值列均按同一个分组键聚合;release.db 中每条记录对应 CSV 中的一行。
|
||||
|
||||
### 5.1 分组键(一行 = 一个分组)
|
||||
|
||||
| CSV 列 | 计算方式 |
|
||||
|--------|----------|
|
||||
| 车厂 | `Aggregation:TxPanelMappings` 映射 TxPanel.Name;未配置映射时 fallback 为 TxPanel.Name |
|
||||
| 车型 | `Aggregation:CarModelMappings` 映射 TxHardware.Version;未配置映射时 fallback 为 TxHardware.Version |
|
||||
| 手机厂商 | RxType.Name 按第一个分隔符(`/`、`_`、`:`)拆分的**前半部分** |
|
||||
| 型号 | 分隔符的**后半部分** |
|
||||
|
||||
- RxType.Name 无分隔符时:手机厂商固定为 `Unknown`,型号取整个 RxType.Name。
|
||||
- **分组基于 Ploss 记录**:只有 Qfod 记录、没有任何 Ploss 记录的场景不会产生发布记录,
|
||||
因而不会出现在 CSV 中。
|
||||
|
||||
### 5.2 功率列(350mW / 500mW / 750mW / 1000mW / 1250mW / 1500mW / 1750mW / 2000mW / 2250mW)
|
||||
|
||||
- **语义**:列头即"允许的最大损耗限值"(安全红线)档位,对应需求文档 Table 4
|
||||
(0~5W→350、5~10W→500、10~15W→750、15~30W→1000、30~50W→1250)。
|
||||
- **来源字段**:列值取 Ploss 记录的 `Field7`(实际损耗)。
|
||||
- **分桶规则**:一条记录按其 `Field9`(Threshold)的值落入对应档位列:
|
||||
`Field9 == power` 的记录,其 `Field7` 计入 `power` 列的平均值;`Field9` 不在列头集合中的记录不进任何列。
|
||||
- **聚合**:取分组内落入该档位的 `Field7` 平均值(有效值取平均,全部为空则留空)。
|
||||
- **字段编号说明**:需求文档称阈值为逻辑编号 (8),但当前解析数据中阈值实际落在 `field_9`
|
||||
(`field_8` 为负数、非阈值)。解析器按原始位置逐字段存储,字段语义以实体注释与实测数据为准。
|
||||
|
||||
### 5.3 Q值 / Q基值
|
||||
|
||||
- **来源字段**:Qfod 记录的 `CurrentQ` / `RawQ`。
|
||||
- **分场景预聚合**:每个测试场景先对 Qfod 记录求 `ΣCurrentQ`、`ΣRawQ`、`Count`(数据库端完成)。
|
||||
- **组内加权累加**:分组内每一条 Ploss 记录,按其所属场景把该场景的预聚合值累加一次。
|
||||
同一场景出现 k 条 Ploss 记录时,其 Sum 与 Count 同倍放大,商保持不变,
|
||||
等效于将该场景全部 Qfod 明细拉平后求平均。
|
||||
- **结果**:Q值 = ΣCurrentQ / ΣCount;Q基值 = ΣRawQ / ΣCount。
|
||||
- **空值**:分组内没有任何 Qfod 记录时,两列均留空。
|
||||
|
||||
### 5.4 P-Q值系数
|
||||
|
||||
- **来源字段**:Ploss 两行格式第一行的 `delta_p`(旧单行格式无此字段,为 null)。
|
||||
- **聚合**:分组内所有 Ploss 记录 `DeltaP` 的平均值(忽略空值)。
|
||||
- **空值**:分组内 `DeltaP` 全部为空时留空。
|
||||
|
||||
### 5.5 谐振频率
|
||||
|
||||
- 当前版本固定为 `null`,**未实现计算**,导出时该列恒为空。
|
||||
|
||||
### 5.6 数值输出格式
|
||||
|
||||
- 数字使用 invariant culture 输出 double 全精度(如 `2531.5737704918033`),不四舍五入。
|
||||
- CSV 为 UTF-8 带 BOM,RFC 4180 转义。
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据库隔离
|
||||
|
||||
```text
|
||||
local.db release.db
|
||||
|
||||
@@ -45,7 +45,7 @@ log 文件
|
||||
│ Application Layer │
|
||||
│ ┌───────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐ │
|
||||
│ │ LocalIngestion│ │ Publishing │ │ Exporting │ │Administration│ │
|
||||
│ │ ParseService │ │AggregationSvc│ │ExportSvc │ │ StatsService │ │
|
||||
│ │ ParseService │ │AggregationSvc│ │ExportSvc │ │ CleanService │ │
|
||||
│ │ ScenarioSvc │ │ThresholdCalc │ │ │ │ CleanService │ │
|
||||
│ │ DimensionSvc │ │ │ │ │ │ │ │
|
||||
│ └───────────────┘ └──────────────┘ └────────────┘ └──────────────┘ │
|
||||
@@ -92,7 +92,6 @@ src/
|
||||
│ ├── Commands/ # 命令实现
|
||||
│ │ ├── ParseCommand.cs
|
||||
│ │ ├── AggregateCommand.cs
|
||||
│ │ ├── StatsCommand.cs
|
||||
│ │ ├── ExportCommand.cs
|
||||
│ │ └── CleanCommand.cs
|
||||
│ ├── Program.cs
|
||||
@@ -115,7 +114,6 @@ src/
|
||||
│ ├── Exporting/ # 导出
|
||||
│ │ └── ExportService.cs
|
||||
│ └── Administration/ # 管理
|
||||
│ ├── StatsService.cs
|
||||
│ └── CleanService.cs
|
||||
├── Domain/ # 领域实体
|
||||
│ └── Models/
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
|
||||
| 类 | 职责 |
|
||||
|----|------|
|
||||
| `StatsService` | 本地库统计查询 |
|
||||
| `CleanService` | 软删除管理 |
|
||||
|
||||
---
|
||||
|
||||
@@ -8,34 +8,44 @@
|
||||
|
||||
**表名**:`ploss_record`
|
||||
|
||||
**描述**:存储 Ploss FOD 格式解析数据,对应日志格式 `FOD-> (1)...(13)`
|
||||
**描述**:存储 Ploss FOD 格式解析数据,对应两行日志格式:
|
||||
|
||||
```
|
||||
pow_loss = X, delta_p = Y
|
||||
FOD-> (1) (2) ... (16)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 字段定义
|
||||
|
||||
> **类型修正说明**:需求文档中 rx_power, tx_power, vcoil, vin, isns, pwm_duty 为 `uint16`(0-65535),但 SQLite 不支持无符号类型。实际数据如 vcoil=35377 超出了 SMALLINT(-32768~32767)范围,因此这些字段使用 INT 类型存储。
|
||||
|
||||
| 列名 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | UUID | PK, NOT NULL | 主键 |
|
||||
| scenario_id | UUID | FK, NOT NULL | 关联测试场景 |
|
||||
| rx_type | TINYINT | NOT NULL | 接收端类型标识 |
|
||||
| rx_power | INT | NOT NULL | 接收端功率(mW),原 uint16 |
|
||||
| tx_power | INT | NOT NULL | 发射端功率(mW),原 uint16 |
|
||||
| vcoil | INT | NOT NULL | 发射线圈电压,原 uint16 |
|
||||
| vin | INT | NOT NULL | 输入电压,原 uint16 |
|
||||
| isns | INT | NOT NULL | 输入电流采样值,原 uint16 |
|
||||
| ploss | INT | NOT NULL | **核心值**:计算损耗(负数=安全) |
|
||||
| threshold | INT | NOT NULL | **安全红线**:当前功率段阈值 |
|
||||
| trigger_count | INT | NOT NULL, DEFAULT 0 | 触发计数器 |
|
||||
| fod_result | INT | NOT NULL, DEFAULT 0 | FOD判定结果 |
|
||||
| protocol_type | TINYINT | NOT NULL | 充电协议类型 |
|
||||
| pwm_duty | INT | NOT NULL | PWM占空比,原 uint16 |
|
||||
| coil_index | TINYINT | NOT NULL | 充电线圈索引 |
|
||||
| pow_loss | INT | NULL | 第一行:功率损耗值 |
|
||||
| delta_p | INT | NULL | 第一行:功率差值 |
|
||||
| field_1 | INT | NOT NULL | Field1(语义待定义) |
|
||||
| field_2 | INT | NOT NULL | Field2(语义待定义) |
|
||||
| field_3 | INT | NOT NULL | Field3(语义待定义) |
|
||||
| field_4 | INT | NOT NULL | Field4: RX power - 接收端功率 |
|
||||
| field_5 | INT | NOT NULL | Field5: TX power - 发射端功率 |
|
||||
| field_6 | INT | NOT NULL | Field6(语义待定义) |
|
||||
| field_7 | INT | NOT NULL | Field7(语义待定义) |
|
||||
| field_8 | INT | NOT NULL | Field8(语义待定义) |
|
||||
| field_9 | INT | NOT NULL, DEFAULT 0 | Field9: ploss - 功率损耗计算值 |
|
||||
| field_10 | INT | NOT NULL, DEFAULT 0 | Field10: threshold - 阈值 |
|
||||
| field_11 | INT | NOT NULL | Field11: 触发次数 - 当 Field9 > Field10 时计数,超过5次触发FOD保护 |
|
||||
| field_12 | INT | NOT NULL | Field12: PFOD result 标志 - 触发时置位 |
|
||||
| field_13 | INT | NOT NULL, DEFAULT 0 | Field13(语义待定义) |
|
||||
| field_14 | INT | NOT NULL, DEFAULT 0 | Field14(语义待定义) |
|
||||
| field_15 | INT | NOT NULL, DEFAULT 0 | Field15(语义待定义) |
|
||||
| field_16 | INT | NOT NULL, DEFAULT 0 | Field16(语义待定义) |
|
||||
| is_deleted | BOOLEAN | NOT NULL, DEFAULT FALSE | 软删除标记 |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
|
||||
|
||||
> **字段语义**以 `PlossRecord` 实体注释为准。
|
||||
|
||||
---
|
||||
|
||||
## 3. 外键
|
||||
@@ -52,9 +62,9 @@
|
||||
|
||||
| 字段 | 计算公式 | 说明 |
|
||||
|------|----------|------|
|
||||
| margin | threshold - ploss | 安全余量(>0 安全,<0 危险) |
|
||||
| margin | field_10 - field_9 | 安全余量(>0 安全,<0 危险) |
|
||||
|
||||
> **使用限制**:`Margin` 属性通过 EF Core `Ignore()` 排除映射,仅用于内存计算。不可在数据库查询中直接使用(如 `WHERE Margin > 0`),需改用 `WHERE (threshold - ploss) > 0` 或先加载到内存再计算。
|
||||
> **使用限制**:`Margin` 属性通过 EF Core `Ignore()` 排除映射,仅用于内存计算。不可在数据库查询中直接使用(如 `WHERE Margin > 0`),需改用 `WHERE (field_10 - field_9) > 0` 或先加载到内存再计算。
|
||||
|
||||
---
|
||||
|
||||
@@ -63,10 +73,12 @@
|
||||
| 索引名 | 字段 | 用途 |
|
||||
|--------|------|------|
|
||||
| idx_ploss_scenario | scenario_id | 按测试场景查询 |
|
||||
| idx_ploss_rx_power | rx_power | 阈值表匹配 |
|
||||
| idx_ploss_ploss | ploss | 损耗值查询 |
|
||||
| idx_ploss_threshold | threshold | 阈值查询 |
|
||||
| idx_ploss_fod_result | fod_result | FOD结果筛选 |
|
||||
| idx_ploss_field_2 | field_2 | 字段查询 |
|
||||
| idx_ploss_field_7 | field_7 | 字段查询 |
|
||||
| idx_ploss_field_8 | field_8 | 字段查询 |
|
||||
| idx_ploss_field_10 | field_10 | 字段查询 |
|
||||
| idx_ploss_pow_loss | pow_loss | 第一行损耗值查询 |
|
||||
| idx_ploss_delta_p | delta_p | 第一行功率差值查询 |
|
||||
|
||||
---
|
||||
|
||||
@@ -75,51 +87,73 @@
|
||||
### 实体类
|
||||
|
||||
```csharp
|
||||
// Models/PlossRecord.cs
|
||||
namespace Gpulse.WCT.DataAnalyzer.Models;
|
||||
// Domain/Local/PlossRecord.cs
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
public class PlossRecord
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid ScenarioId { get; set; }
|
||||
public byte RxType { get; set; }
|
||||
public short RxPower { get; set; }
|
||||
public short TxPower { get; set; }
|
||||
public short Vcoil { get; set; }
|
||||
public short Vin { get; set; }
|
||||
public short Isns { get; set; }
|
||||
|
||||
/// <summary>核心判定值:负数表示安全</summary>
|
||||
public int Ploss { get; set; }
|
||||
// ===== 第一行字段(nullable,兼容旧数据) =====
|
||||
/// <summary>功率损耗值(来自第一行)</summary>
|
||||
public int? PowLoss { get; set; }
|
||||
|
||||
/// <summary>安全红线:当前功率段阈值</summary>
|
||||
public int Threshold { get; set; }
|
||||
/// <summary>功率差值(来自第一行)</summary>
|
||||
public int? DeltaP { get; set; }
|
||||
|
||||
// ===== 第二行字段(16个通用字段,均支持负数) =====
|
||||
public int Field1 { get; set; }
|
||||
public int Field2 { get; set; }
|
||||
public int Field3 { get; set; }
|
||||
|
||||
/// <summary>Field4: RX power - 接收端功率</summary>
|
||||
public int Field4 { get; set; }
|
||||
|
||||
/// <summary>Field5: TX power - 发射端功率</summary>
|
||||
public int Field5 { get; set; }
|
||||
|
||||
public int Field6 { get; set; }
|
||||
public int Field7 { get; set; }
|
||||
public int Field8 { get; set; }
|
||||
|
||||
/// <summary>Field9: ploss - 功率损耗计算值</summary>
|
||||
public int Field9 { get; set; }
|
||||
|
||||
/// <summary>Field10: threshold - 阈值</summary>
|
||||
public int Field10 { get; set; }
|
||||
|
||||
/// <summary>Field11: 触发次数 - 当 Field9 > Field10(ploss 大于阈值)时计数,超过5次触发FOD保护</summary>
|
||||
public int Field11 { get; set; }
|
||||
|
||||
/// <summary>Field12: PFOD result 标志 - 触发时置位</summary>
|
||||
public int Field12 { get; set; }
|
||||
|
||||
public int Field13 { get; set; }
|
||||
public int Field14 { get; set; }
|
||||
public int Field15 { get; set; }
|
||||
public int Field16 { get; set; }
|
||||
|
||||
public short TriggerCount { get; set; } = 0;
|
||||
public short FodResult { get; set; } = 0;
|
||||
public byte ProtocolType { get; set; }
|
||||
public short PwmDuty { get; set; }
|
||||
public byte CoilIndex { get; set; }
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation Properties
|
||||
public TestScenario Scenario { get; set; } = null!;
|
||||
|
||||
/// <summary>派生属性:安全余量(不落库)</summary>
|
||||
public int Margin => Threshold - Ploss;
|
||||
/// <summary>派生属性:安全余量(不落库) = Field10 - Field9</summary>
|
||||
public int Margin => Field10 - Field9;
|
||||
}
|
||||
```
|
||||
|
||||
### 配置类
|
||||
|
||||
```csharp
|
||||
// Data/Configurations/PlossRecordConfiguration.cs
|
||||
// Infrastructure/LocalData/Configurations/PlossRecordConfiguration.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Gpulse.WCT.DataAnalyzer.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Data.Configurations;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||
|
||||
public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
||||
{
|
||||
@@ -134,59 +168,27 @@ public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
||||
.IsRequired()
|
||||
.HasColumnName("scenario_id");
|
||||
|
||||
builder.Property(e => e.RxType)
|
||||
.IsRequired()
|
||||
.HasColumnName("rx_type");
|
||||
// ===== 第一行字段(nullable) =====
|
||||
builder.Property(e => e.PowLoss).IsRequired(false).HasColumnName("pow_loss");
|
||||
builder.Property(e => e.DeltaP).IsRequired(false).HasColumnName("delta_p");
|
||||
|
||||
builder.Property(e => e.RxPower)
|
||||
.IsRequired()
|
||||
.HasColumnName("rx_power");
|
||||
|
||||
builder.Property(e => e.TxPower)
|
||||
.IsRequired()
|
||||
.HasColumnName("tx_power");
|
||||
|
||||
builder.Property(e => e.Vcoil)
|
||||
.IsRequired()
|
||||
.HasColumnName("vcoil");
|
||||
|
||||
builder.Property(e => e.Vin)
|
||||
.IsRequired()
|
||||
.HasColumnName("vin");
|
||||
|
||||
builder.Property(e => e.Isns)
|
||||
.IsRequired()
|
||||
.HasColumnName("isns");
|
||||
|
||||
builder.Property(e => e.Ploss)
|
||||
.IsRequired()
|
||||
.HasColumnName("ploss");
|
||||
|
||||
builder.Property(e => e.Threshold)
|
||||
.IsRequired()
|
||||
.HasColumnName("threshold");
|
||||
|
||||
builder.Property(e => e.TriggerCount)
|
||||
.IsRequired()
|
||||
.HasDefaultValue((short)0)
|
||||
.HasColumnName("trigger_count");
|
||||
|
||||
builder.Property(e => e.FodResult)
|
||||
.IsRequired()
|
||||
.HasDefaultValue((short)0)
|
||||
.HasColumnName("fod_result");
|
||||
|
||||
builder.Property(e => e.ProtocolType)
|
||||
.IsRequired()
|
||||
.HasColumnName("protocol_type");
|
||||
|
||||
builder.Property(e => e.PwmDuty)
|
||||
.IsRequired()
|
||||
.HasColumnName("pwm_duty");
|
||||
|
||||
builder.Property(e => e.CoilIndex)
|
||||
.IsRequired()
|
||||
.HasColumnName("coil_index");
|
||||
// ===== 第二行字段(通用命名) =====
|
||||
builder.Property(e => e.Field1).IsRequired().HasColumnName("field_1");
|
||||
builder.Property(e => e.Field2).IsRequired().HasColumnName("field_2");
|
||||
builder.Property(e => e.Field3).IsRequired().HasColumnName("field_3");
|
||||
builder.Property(e => e.Field4).IsRequired().HasColumnName("field_4");
|
||||
builder.Property(e => e.Field5).IsRequired().HasColumnName("field_5");
|
||||
builder.Property(e => e.Field6).IsRequired().HasColumnName("field_6");
|
||||
builder.Property(e => e.Field7).IsRequired().HasColumnName("field_7");
|
||||
builder.Property(e => e.Field8).IsRequired().HasColumnName("field_8");
|
||||
builder.Property(e => e.Field9).IsRequired().HasDefaultValue(0).HasColumnName("field_9");
|
||||
builder.Property(e => e.Field10).IsRequired().HasDefaultValue(0).HasColumnName("field_10");
|
||||
builder.Property(e => e.Field11).IsRequired().HasColumnName("field_11");
|
||||
builder.Property(e => e.Field12).IsRequired().HasColumnName("field_12");
|
||||
builder.Property(e => e.Field13).IsRequired().HasDefaultValue(0).HasColumnName("field_13");
|
||||
builder.Property(e => e.Field14).IsRequired().HasDefaultValue(0).HasColumnName("field_14");
|
||||
builder.Property(e => e.Field15).IsRequired().HasDefaultValue(0).HasColumnName("field_15");
|
||||
builder.Property(e => e.Field16).IsRequired().HasDefaultValue(0).HasColumnName("field_16");
|
||||
|
||||
builder.Property(e => e.IsDeleted)
|
||||
.IsRequired()
|
||||
@@ -195,7 +197,7 @@ public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
||||
|
||||
builder.Property(e => e.CreatedAt)
|
||||
.IsRequired()
|
||||
.HasDefaultValueSql("NOW()")
|
||||
.HasDefaultValueSql("datetime('now')")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
// 派生属性不落库
|
||||
@@ -203,16 +205,21 @@ public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
||||
|
||||
// 索引
|
||||
builder.HasIndex(e => e.ScenarioId).HasDatabaseName("idx_ploss_scenario");
|
||||
builder.HasIndex(e => e.RxPower).HasDatabaseName("idx_ploss_rx_power");
|
||||
builder.HasIndex(e => e.Ploss).HasDatabaseName("idx_ploss_ploss");
|
||||
builder.HasIndex(e => e.Threshold).HasDatabaseName("idx_ploss_threshold");
|
||||
builder.HasIndex(e => e.FodResult).HasDatabaseName("idx_ploss_fod_result");
|
||||
builder.HasIndex(e => e.Field2).HasDatabaseName("idx_ploss_field_2");
|
||||
builder.HasIndex(e => e.Field7).HasDatabaseName("idx_ploss_field_7");
|
||||
builder.HasIndex(e => e.Field8).HasDatabaseName("idx_ploss_field_8");
|
||||
builder.HasIndex(e => e.Field10).HasDatabaseName("idx_ploss_field_10");
|
||||
builder.HasIndex(e => e.PowLoss).HasDatabaseName("idx_ploss_pow_loss");
|
||||
builder.HasIndex(e => e.DeltaP).HasDatabaseName("idx_ploss_delta_p");
|
||||
|
||||
// 关系配置
|
||||
builder.HasOne(e => e.Scenario)
|
||||
.WithMany(e => e.PlossRecords)
|
||||
.HasForeignKey(e => e.ScenarioId)
|
||||
.IsRequired();
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// 全局查询过滤器
|
||||
builder.HasQueryFilter(e => !e.IsDeleted);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -221,7 +228,7 @@ public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
||||
|
||||
## 7. 动态阈值表
|
||||
|
||||
> 阈值根据 RxPower 动态查表(不落库)
|
||||
> 阈值根据 RxPower 动态查表(不落库,业务规则)
|
||||
|
||||
| Rx Power 区间 | Threshold | 备注 |
|
||||
|---------------|-----------|------|
|
||||
@@ -230,15 +237,3 @@ public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
||||
| 10000-15000 mW | 750 mW | |
|
||||
| 15000-30000 mW | 1000 mW | |
|
||||
| 30000-50000 mW | 1250 mW | 高功率 |
|
||||
|
||||
```csharp
|
||||
// Services/ThresholdCalculator.cs
|
||||
public static int GetThresholdByPower(int rxPower)
|
||||
{
|
||||
if (rxPower <= 5000) return 350;
|
||||
if (rxPower <= 10000) return 500;
|
||||
if (rxPower <= 15000) return 750;
|
||||
if (rxPower <= 30000) return 1000;
|
||||
return 1250;
|
||||
}
|
||||
```
|
||||
114
docs/requirement/Ploss分析.md
Normal file
114
docs/requirement/Ploss分析.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Ploss 分析(delta_p 与安全余量判定)
|
||||
|
||||
> 本文档单独记录 Ploss 分析的分析口径与判定条件。
|
||||
> 内容独立于既有需求文档 `docs/requirement/WCT数据需求分析.md`,不与原文档合并。
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心口径
|
||||
|
||||
Ploss 两行日志格式:
|
||||
|
||||
```text
|
||||
pow_loss = 2200, delta_p = 2500
|
||||
FOD-> (16 个字段)
|
||||
```
|
||||
|
||||
- **header 行**直接记录固件当前使用的 `pow_loss` / `delta_p`。
|
||||
- **FOD-> 行**记录 16 个字段,其中包含 Ploss(功率损耗计算值)、Threshold(阈值)、触发次数、FOD 结果等。
|
||||
|
||||
**权威字段语义**(以实体 `PlossRecord` 注释为准):
|
||||
|
||||
| 字段 | 语义 |
|
||||
|------|------|
|
||||
| Field9 | ploss - 功率损耗计算值 |
|
||||
| Field10 | threshold - 阈值 |
|
||||
| Field11 | 触发次数 |
|
||||
| Field12 | FOD 结果标志 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 判定条件
|
||||
|
||||
### 2.1 FOD 报警判定
|
||||
|
||||
**直接看 FOD 结果标志 == 1**。固件已完成判定(Ploss>Threshold → 计数累积 → 满限置位),分析无需重建该逻辑。
|
||||
|
||||
### 2.2 delta_p 恒等式
|
||||
|
||||
```
|
||||
delta_p = Tx − Rx − pow_loss − Ploss
|
||||
```
|
||||
|
||||
能量守恒恒等式,固件即用此公式计算 Ploss,故必然满足,可用于校验。
|
||||
|
||||
### 2.3 安全余量判定
|
||||
|
||||
```
|
||||
Field10 − Field9 = Threshold − Ploss > 2000
|
||||
```
|
||||
|
||||
正常充电时安全余量需大于 2000;余量不足(≤2000)即异物或异常信号。
|
||||
|
||||
### 2.4 delta_p 调整反解(使余量 > 2000)
|
||||
|
||||
当 FOD=1(余量不足)时,可依据 2.2 恒等式与 2.3 余量判定,反解出使余量严格 `> 2000`(整数即 `≥ 2001`)所需的 delta_p:
|
||||
|
||||
```
|
||||
margin = Threshold − Ploss
|
||||
delta_p' = DeltaP + (2001 − margin)
|
||||
= DeltaP + 2001 + Ploss − Threshold
|
||||
```
|
||||
|
||||
- `DeltaP`:header 行记录的原始 delta_p
|
||||
- `delta_p'`:调整后的新 delta_p
|
||||
- 将 header 行重写为 `pow_loss = <原值>, delta_p = <delta_p'>` 后,按恒等式 Ploss 下降 `2001 − margin`,余量达到 2001。
|
||||
|
||||
> **注意**:此反解仅在两行格式(有 header)下有意义;旧单行格式无 header,PowLoss/DeltaP 为 null,无法调整 delta_p。
|
||||
|
||||
**测试数据字段位置**(与实体语义错位,以实体语义为准):
|
||||
|
||||
| 文件 | ploss | threshold | FOD 标志 |
|
||||
|------|-------|-----------|----------|
|
||||
| 实体语义(真实固件) | Field9 | Field10 | Field12 |
|
||||
| ploss_test 实测 | Field8 | Field9 | Field11 |
|
||||
| ploss_legacy 实测 | Field10 | Field11 | Field13 |
|
||||
|
||||
**ploss_test 中 14 条 FOD=1 行的计算结果**(新余量校验 = 2001):
|
||||
|
||||
| 行号 | pow_loss | DeltaP | ploss | threshold | margin | delta_p' |
|
||||
|------|----------|--------|-------|-----------|--------|----------|
|
||||
| 247 | 2200 | 2500 | 798 | 1000 | 202 | 4299 |
|
||||
| 259 | 2000 | 2200 | 760 | 750 | −10 | 4211 |
|
||||
| 519 | 2200 | 2500 | 1308 | 1000 | −308 | 4809 |
|
||||
| 527 | 3000 | 3500 | 1355 | 1250 | −105 | 5606 |
|
||||
| 567 | 2200 | 2500 | 674 | 1000 | 326 | 4175 |
|
||||
| 957 | 2000 | 2200 | 736 | 750 | 14 | 4187 |
|
||||
| 1129 | 1000 | 500 | 123 | 350 | 227 | 2274 |
|
||||
| 1265 | 2000 | 2200 | 497 | 750 | 253 | 3948 |
|
||||
| 1533 | 3000 | 3500 | 1474 | 1250 | −224 | 5725 |
|
||||
| 2157 | 2200 | 2500 | 361 | 1000 | 639 | 3862 |
|
||||
| 2379 | 1000 | 500 | 397 | 350 | −47 | 2548 |
|
||||
| 2413 | 3000 | 3500 | 955 | 1250 | 295 | 5206 |
|
||||
| 2477 | 2000 | 2200 | 363 | 750 | 387 | 3814 |
|
||||
| 2575 | 1000 | 500 | 560 | 350 | −210 | 2711 |
|
||||
|
||||
新 delta_p 范围 **2274 ~ 5725**(原 500 ~ 3500)。
|
||||
|
||||
> **局限**:示例日志为生成的测试数据,恒等式 `delta_p = Tx − Rx − pow_loss − Ploss` 在数据中不成立(穷举全部字段组合均无法一致满足),故表中 delta_p' 为"若恒等式成立则应写入的目标值",无法用数据本身验证。真实固件数据(恒等式成立)下此公式精确成立。
|
||||
|
||||
---
|
||||
|
||||
## 3. 实测佐证
|
||||
|
||||
示例日志(自动生成的测试数据)中,按语义(threshold − ploss)计算安全余量:
|
||||
|
||||
| 格式 | 正常行 margin>2000 比例 | FOD 行 margin>2000 比例 |
|
||||
|---|---|---|
|
||||
| 两行 ploss_test | 917/1486 (61.7%) | 0/14 (0%) |
|
||||
| 旧版 ploss_legacy | 361/593 (60.9%) | 0/7 (0%) |
|
||||
|
||||
FOD 行安全余量全部 ≤ 2000,可区分正常/异物。
|
||||
|
||||
> 说明:示例日志为生成的测试数据,其字段位置与实体语义存在错位;**以实体语义为准**
|
||||
> (Field9=ploss、Field10=threshold)。
|
||||
79
docs/requirement/Qfod标定分析.md
Normal file
79
docs/requirement/Qfod标定分析.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Qfod 标定分析(Q值 → 标定 ΔQ 阈值)
|
||||
|
||||
> 本文档单独记录 Qfod 标定分析的分析口径、阶段区分限制与交付方式。
|
||||
> 内容独立于既有需求文档 `docs/requirement/WCT数据需求分析.md`,不与原文档合并。
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心口径
|
||||
|
||||
Q 值由日志直接记录,日志格式 `X#:Q A->B C D E F`:
|
||||
|
||||
| 字段 | 名称 | 说明 |
|
||||
|------|------|------|
|
||||
| X | ChargerIndex | 充电器索引(0, 1) |
|
||||
| A | CoilIndex | 线圈索引(0, 1, 2) |
|
||||
| B | ΔQ (DeltaQ) | Q 值变化量 |
|
||||
| C | CurrentQ | 当前 Q 值 |
|
||||
| D | RawQ | 原始 Q 值 |
|
||||
| E | FodType | 异物类型编码 |
|
||||
|
||||
**关键点**:「纯手机」与「手机+异物」分别对应**两个日志文件(两个测试场景)**:
|
||||
|
||||
- 日志文件 A:纯手机充电
|
||||
- 日志文件 B:手机 + 异物
|
||||
|
||||
标定分析需**先区分这两个日志文件**,再计算标定 ΔQ 阈值。
|
||||
|
||||
---
|
||||
|
||||
## 2. 标定 ΔQ 阈值计算
|
||||
|
||||
1. 分别从两个日志文件提取 ΔQ:
|
||||
- 文件 A(纯手机):取该文件 ΔQ **最大值**(示例:25)
|
||||
- 文件 B(手机+异物):取该文件 ΔQ **最小值**(示例:40)
|
||||
2. 计算标定阈值:`Threshold = (25 + 40) / 2 = 32`
|
||||
|
||||
---
|
||||
|
||||
## 3. 分析步骤
|
||||
|
||||
1. 提取日志中 `#Q` 开头的行
|
||||
2. 绘制 ΔQ 时序曲线,叠加 Y=Threshold 水平线
|
||||
3. 验证余量:纯手机最高点 vs 阈值、有异物最低点 vs 阈值
|
||||
|
||||
---
|
||||
|
||||
## 4. 文件区分方式(现状与后续计划)
|
||||
|
||||
### 4.1 当前限制
|
||||
|
||||
暂无可自动区分「纯手机」/「手机+异物」两个日志文件的可靠依据,因此**当前版本无法自动计算标定 ΔQ 阈值**。
|
||||
|
||||
### 4.2 后续计划
|
||||
|
||||
日志文件将写清楚所对应的场景(文件名 Purpose 段或文件内标记),届时据此区分两个文件再计算标定阈值。
|
||||
|
||||
**待办**:定义日志文件区分标识,解析时识别并关联配对的两个场景。
|
||||
|
||||
---
|
||||
|
||||
## 5. 交付方式
|
||||
|
||||
- 标定 ΔQ 阈值属**独立分析**,**不进入发布库聚合**(发布库现有 Q值/Q基值 列仍为 CurrentQ/RawQ 全量平均,保持不变)。
|
||||
- 计划通过**新建 CLI 分析命令**(`analyze-qfod`)承载/输出,命令**尚未实现**。
|
||||
|
||||
---
|
||||
|
||||
## 6. 实测佐证
|
||||
|
||||
`data/test_input/singleMold-v1.0-hex2_1-iPhone15-qfod_test-20260710-1.log`(示例日志含 Phase 标记):
|
||||
|
||||
| Phase | 含义 | ΔQ 范围 | FOD Type(E) |
|
||||
|---|---|---|---|
|
||||
| 1 正常充电 | 纯手机 | 20~30(均值 24.9) | 0 |
|
||||
| 2 异物检测 | 手机+异物 | 35~55(均值 44.6) | 1/2/3 |
|
||||
| 3 边界值 | 临界区 | 30~35(均值 32.5) | — |
|
||||
| 4 负数 | 异常 | -25~0 | — |
|
||||
|
||||
> 说明:示例日志为生成的测试数据,仅作 ΔQ 数值区间的参考;真实数据中「纯手机」与「手机+异物」分属两个日志文件。
|
||||
@@ -107,7 +107,7 @@
|
||||
1. 从 Log 提取两种场景的 ΔQ:
|
||||
- 场景 A(纯手机):ΔQ = 25(最大值)
|
||||
- 场景 B(手机+异物):ΔQ = 40 (最小值)
|
||||
1. 计算阈值:Threshold = (25 + 40) / 2 = 32
|
||||
2. 计算阈值:Threshold = (25 + 40) / 2 = 32
|
||||
|
||||
### 2.1.2 分析步骤
|
||||
|
||||
@@ -197,10 +197,3 @@ Margin = Threshold - Ploss
|
||||
- 调整 delta_p 使曲线回归安全区(负值区)
|
||||
|
||||
# 3 QA :
|
||||
|
||||
## 3.1 导入之后会有脏数据污染吗? 人工删除?
|
||||
## 3.2 FOD Type 具体的是什么,对数据分析有影响吗?和文件名定义的有什么关系
|
||||
|
||||
## 3.3 Ploss fod 日志格式和 实际日志中的对应不上
|
||||
|
||||
## 3.4 数据是孤立的
|
||||
Binary file not shown.
BIN
docs/requirement/无线充测试log保存及管理方案-20260817.pdf
Normal file
BIN
docs/requirement/无线充测试log保存及管理方案-20260817.pdf
Normal file
Binary file not shown.
127
scripts/publish.ps1
Normal file
127
scripts/publish.ps1
Normal 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 ""
|
||||
@@ -1,142 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 统计查询服务 - 使用数据库端聚合优化内存效率
|
||||
/// </summary>
|
||||
public class StatsService
|
||||
{
|
||||
private readonly WctMinerDbContext _context;
|
||||
private readonly ILogger<StatsService> _logger;
|
||||
|
||||
public StatsService(WctMinerDbContext context, ILogger<StatsService> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取整体统计摘要(并行查询优化)
|
||||
/// </summary>
|
||||
public async Task<StatsSummary> GetSummaryAsync()
|
||||
{
|
||||
// 使用 Task.WhenAll 并行执行多个查询
|
||||
var tasks = new[]
|
||||
{
|
||||
_context.TestScenarios.CountAsync(),
|
||||
_context.QfodRecords.CountAsync(),
|
||||
_context.PlossRecords.CountAsync(),
|
||||
_context.TxPanels.CountAsync(),
|
||||
_context.TxHardwares.CountAsync(),
|
||||
_context.TxSoftwares.CountAsync(),
|
||||
_context.RxTypes.CountAsync()
|
||||
};
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
var summary = new StatsSummary(
|
||||
results[0], results[1], results[2],
|
||||
results[3], results[4], results[5], results[6]
|
||||
);
|
||||
|
||||
_logger.LogDebug("统计摘要: Scenarios={ScenarioCount}, Qfod={QfodCount}, Ploss={PlossCount}",
|
||||
summary.ScenarioCount, summary.QfodCount, summary.PlossCount);
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按FOD类型统计Qfod记录(数据库端聚合)
|
||||
/// </summary>
|
||||
public async Task<List<FodTypeStats>> GetStatsByFodTypeAsync()
|
||||
{
|
||||
// 使用数据库端 GroupBy
|
||||
var stats = await _context.QfodRecords
|
||||
.GroupBy(r => r.FodType)
|
||||
.Select(g => new FodTypeStats(g.Key, g.Count()))
|
||||
.OrderBy(s => s.FodType)
|
||||
.ToListAsync();
|
||||
|
||||
_logger.LogDebug("按 FOD 类型统计完成: {Count} 种类型", stats.Count);
|
||||
return stats;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按面板类型统计(数据库端聚合 - 使用维度表反向统计)
|
||||
/// </summary>
|
||||
public async Task<List<DimensionStats>> GetStatsByPanelAsync()
|
||||
{
|
||||
// 方式: 使用维度表反向统计(更高效)
|
||||
var stats = await _context.TxPanels
|
||||
.Select(p => new DimensionStats(
|
||||
p.Name,
|
||||
p.TestScenarios.Count()
|
||||
))
|
||||
.OrderByDescending(s => s.Count)
|
||||
.ToListAsync();
|
||||
|
||||
_logger.LogDebug("按面板类型统计完成: {Count} 种面板", stats.Count);
|
||||
return stats;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按硬件版本统计(数据库端聚合)
|
||||
/// </summary>
|
||||
public async Task<List<DimensionStats>> GetStatsByHardwareAsync()
|
||||
{
|
||||
var stats = await _context.TxHardwares
|
||||
.Select(h => new DimensionStats(
|
||||
h.Version,
|
||||
h.TestScenarios.Count()
|
||||
))
|
||||
.OrderByDescending(s => s.Count)
|
||||
.ToListAsync();
|
||||
|
||||
_logger.LogDebug("按硬件版本统计完成: {Count} 种版本", stats.Count);
|
||||
return stats;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按RX类型统计(数据库端聚合)
|
||||
/// </summary>
|
||||
public async Task<List<DimensionStats>> GetStatsByRxTypeAsync()
|
||||
{
|
||||
var stats = await _context.RxTypes
|
||||
.Select(r => new DimensionStats(
|
||||
r.Name,
|
||||
r.TestScenarios.Count()
|
||||
))
|
||||
.OrderByDescending(s => s.Count)
|
||||
.ToListAsync();
|
||||
|
||||
_logger.LogDebug("按 RX 类型统计完成: {Count} 种类型", stats.Count);
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 整体统计摘要
|
||||
/// </summary>
|
||||
public record StatsSummary(
|
||||
int ScenarioCount,
|
||||
int QfodCount,
|
||||
int PlossCount,
|
||||
int PanelCount,
|
||||
int HardwareCount,
|
||||
int SoftwareCount,
|
||||
int RxTypeCount
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// FOD类型统计
|
||||
/// </summary>
|
||||
public record FodTypeStats(byte FodType, int Count);
|
||||
|
||||
/// <summary>
|
||||
/// 维度统计
|
||||
/// </summary>
|
||||
public record DimensionStats(string Name, int Count);
|
||||
@@ -0,0 +1,350 @@
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
|
||||
|
||||
/// <summary>
|
||||
/// Ploss 分析服务 - 安全余量判定与 delta_p 反解
|
||||
/// 口径以实体语义为准:Field9=ploss, Field10=threshold, Field12=FOD结果标志
|
||||
/// 直接解析日志文件,不依赖本地库。
|
||||
/// </summary>
|
||||
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 日志文件(生成 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)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return new PlossAnalysisReport(
|
||||
FileName: fileInfo.Name,
|
||||
TotalCount: 0,
|
||||
TwoLineCount: 0,
|
||||
LegacyCount: 0,
|
||||
FodCount: 0,
|
||||
NormalAboveCount: 0,
|
||||
NormalCount: 0,
|
||||
FodAboveCount: 0,
|
||||
IdentityHeldCount: 0,
|
||||
IdentityCheckedCount: 0,
|
||||
FodRows: [],
|
||||
ErrorCount: 0,
|
||||
ErrorMessage: "文件不存在"
|
||||
);
|
||||
}
|
||||
|
||||
var lines = await File.ReadAllLinesAsync(filePath);
|
||||
|
||||
var rows = new List<PlossRowReport>();
|
||||
int errorCount = 0;
|
||||
|
||||
// 用于暂存两行格式的 header 行
|
||||
string? pendingHeaderLine = null;
|
||||
int pendingHeaderLineNumber = 0;
|
||||
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
|
||||
var lineNumber = i + 1;
|
||||
|
||||
if (_plossParser.IsMultiLineStart(line))
|
||||
{
|
||||
pendingHeaderLine = line;
|
||||
pendingHeaderLineNumber = lineNumber;
|
||||
}
|
||||
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)
|
||||
rows.Add(BuildRow(lineNumber, result.Record));
|
||||
else
|
||||
errorCount++;
|
||||
|
||||
pendingHeaderLine = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 缺少 header 行,记录错误
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理末尾遗留的 header 行(异常情况)
|
||||
if (pendingHeaderLine != null)
|
||||
{
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
var fodRows = rows.Where(r => r.IsFod).ToList();
|
||||
var normalRows = rows.Where(r => !r.IsFod).ToList();
|
||||
|
||||
return new PlossAnalysisReport(
|
||||
FileName: fileInfo.Name,
|
||||
TotalCount: rows.Count,
|
||||
TwoLineCount: rows.Count(r => r.IsTwoLine),
|
||||
LegacyCount: rows.Count(r => !r.IsTwoLine),
|
||||
FodCount: fodRows.Count,
|
||||
NormalAboveCount: normalRows.Count(r => r.Margin > 2000),
|
||||
NormalCount: normalRows.Count,
|
||||
FodAboveCount: fodRows.Count(r => r.Margin > 2000),
|
||||
IdentityHeldCount: rows.Count(r => r.IsTwoLine && r.IdentityHeld),
|
||||
IdentityCheckedCount: rows.Count(r => r.IsTwoLine),
|
||||
FodRows: fodRows,
|
||||
ErrorCount: errorCount,
|
||||
ErrorMessage: null
|
||||
);
|
||||
}
|
||||
|
||||
private static PlossRowReport BuildRow(int lineNumber, PlossRecord record)
|
||||
{
|
||||
var isTwoLine = record.PowLoss.HasValue && record.DeltaP.HasValue;
|
||||
var ploss = record.Field9;
|
||||
var threshold = record.Field10;
|
||||
var margin = threshold - ploss;
|
||||
var isFod = record.Field12 == 1;
|
||||
|
||||
// 恒等式校验(仅两行格式有 header 数据):
|
||||
// delta_p = Tx - Rx - pow_loss - Ploss
|
||||
bool identityHeld = isTwoLine
|
||||
&& record.DeltaP == record.Field5 - record.Field4 - record.PowLoss - ploss;
|
||||
|
||||
// delta_p 反解(仅两行格式且 FOD=1):
|
||||
// delta_p' = DeltaP + 2001 + Ploss - Threshold
|
||||
int? deltaPPrime = null;
|
||||
string? rewrittenHeader = null;
|
||||
if (isTwoLine && isFod)
|
||||
{
|
||||
deltaPPrime = record.DeltaP + 2001 + ploss - threshold;
|
||||
rewrittenHeader = $"pow_loss = {record.PowLoss}, delta_p = {deltaPPrime}";
|
||||
}
|
||||
|
||||
return new PlossRowReport(
|
||||
LineNumber: lineNumber,
|
||||
IsTwoLine: isTwoLine,
|
||||
PowLoss: record.PowLoss,
|
||||
DeltaP: record.DeltaP,
|
||||
Ploss: ploss,
|
||||
Threshold: threshold,
|
||||
Margin: margin,
|
||||
IsFod: isFod,
|
||||
IdentityHeld: identityHeld,
|
||||
DeltaPPrime: deltaPPrime,
|
||||
RewrittenHeader: rewrittenHeader
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ploss 单行分析结果
|
||||
/// </summary>
|
||||
public record PlossRowReport(
|
||||
int LineNumber,
|
||||
bool IsTwoLine,
|
||||
int? PowLoss,
|
||||
int? DeltaP,
|
||||
int Ploss,
|
||||
int Threshold,
|
||||
int Margin,
|
||||
bool IsFod,
|
||||
bool IdentityHeld,
|
||||
int? DeltaPPrime,
|
||||
string? RewrittenHeader
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Ploss 分析报告
|
||||
/// </summary>
|
||||
public record PlossAnalysisReport(
|
||||
string FileName,
|
||||
int TotalCount,
|
||||
int TwoLineCount,
|
||||
int LegacyCount,
|
||||
int FodCount,
|
||||
int NormalAboveCount,
|
||||
int NormalCount,
|
||||
int FodAboveCount,
|
||||
int IdentityHeldCount,
|
||||
int IdentityCheckedCount,
|
||||
IReadOnlyList<PlossRowReport> FodRows,
|
||||
int ErrorCount,
|
||||
string? ErrorMessage
|
||||
)
|
||||
{
|
||||
public bool IsSuccess => ErrorMessage == null;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
|
||||
|
||||
/// <summary>
|
||||
/// Qfod 标定分析服务 - 由「纯手机」与「手机+异物」两个日志文件计算标定 ΔQ 阈值
|
||||
/// Threshold = (纯手机 ΔQ 最大值 + 手机+异物 ΔQ 最小值) / 2
|
||||
/// 直接解析日志文件,不依赖本地库,也不进入发布库聚合。
|
||||
/// </summary>
|
||||
public class QfodCalibrationService
|
||||
{
|
||||
private readonly QfodParser _qfodParser;
|
||||
|
||||
public QfodCalibrationService(QfodParser qfodParser)
|
||||
{
|
||||
_qfodParser = qfodParser;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算标定 Δ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>
|
||||
public async Task<QfodCalibrationReport> CalibrateAsync(string pureFile, string foreignFile)
|
||||
{
|
||||
var pureSummary = await ReadSummaryAsync(pureFile);
|
||||
var foreignSummary = await ReadSummaryAsync(foreignFile);
|
||||
|
||||
if (pureSummary.ErrorMessage != null)
|
||||
return new QfodCalibrationReport(null, null, pureSummary, null, 0, 0, 0, pureSummary.ErrorMessage);
|
||||
if (foreignSummary.ErrorMessage != null)
|
||||
return new QfodCalibrationReport(null, null, null, foreignSummary, 0, 0, 0, foreignSummary.ErrorMessage);
|
||||
|
||||
if (pureSummary.Count == 0 || foreignSummary.Count == 0)
|
||||
{
|
||||
return new QfodCalibrationReport(
|
||||
pureSummary.FileName, foreignSummary.FileName,
|
||||
pureSummary, foreignSummary, 0, 0, 0,
|
||||
"日志文件中未提取到 Qfod 数据(无 #:Q 行)");
|
||||
}
|
||||
|
||||
var threshold = (pureSummary.Max + foreignSummary.Min) / 2;
|
||||
|
||||
return new QfodCalibrationReport(
|
||||
PureFileName: pureSummary.FileName,
|
||||
ForeignFileName: foreignSummary.FileName,
|
||||
Pure: pureSummary,
|
||||
Foreign: foreignSummary,
|
||||
Threshold: threshold,
|
||||
PureMargin: pureSummary.Max - threshold,
|
||||
ForeignMargin: foreignSummary.Min - threshold,
|
||||
ErrorMessage: null
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<QfodFileSummary> ReadSummaryAsync(string filePath)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return new QfodFileSummary(fileInfo.Name, 0, 0, 0, 0, $"文件不存在: {filePath}");
|
||||
}
|
||||
|
||||
var deltaQs = new List<int>();
|
||||
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)
|
||||
deltaQs.Add(result.Record.DeltaQ);
|
||||
}
|
||||
|
||||
if (deltaQs.Count == 0)
|
||||
{
|
||||
return new QfodFileSummary(fileInfo.Name, 0, 0, 0, 0, null);
|
||||
}
|
||||
|
||||
return new QfodFileSummary(
|
||||
FileName: fileInfo.Name,
|
||||
Count: deltaQs.Count,
|
||||
Min: deltaQs.Min(),
|
||||
Max: deltaQs.Max(),
|
||||
Average: deltaQs.Average(),
|
||||
ErrorMessage: null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个 Qfod 文件的 ΔQ 统计
|
||||
/// </summary>
|
||||
public record QfodFileSummary(
|
||||
string FileName,
|
||||
int Count,
|
||||
int Min,
|
||||
int Max,
|
||||
double Average,
|
||||
string? ErrorMessage
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Qfod 标定报告
|
||||
/// </summary>
|
||||
public record QfodCalibrationReport(
|
||||
string? PureFileName,
|
||||
string? ForeignFileName,
|
||||
QfodFileSummary? Pure,
|
||||
QfodFileSummary? Foreign,
|
||||
int Threshold,
|
||||
int PureMargin,
|
||||
int ForeignMargin,
|
||||
string? ErrorMessage
|
||||
)
|
||||
{
|
||||
public bool IsSuccess => ErrorMessage == null;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -20,6 +20,11 @@ public class ParseService
|
||||
private readonly ILogger<ParseService> _logger;
|
||||
private readonly int _batchSize;
|
||||
|
||||
/// <summary>
|
||||
/// 支持的输入数据文件扩展名(大小写不敏感)
|
||||
/// </summary>
|
||||
private static readonly string[] SupportedExtensions = [".log", ".txt", ".dat"];
|
||||
|
||||
public ParseService(
|
||||
WctMinerDbContext context,
|
||||
ScenarioService scenarioService,
|
||||
@@ -169,12 +174,16 @@ public class ParseService
|
||||
}
|
||||
|
||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
||||
var logFiles = dirInfo.GetFiles("*.log", searchOption);
|
||||
var dataFiles = dirInfo.EnumerateFiles("*", searchOption)
|
||||
.Where(f => SupportedExtensions.Contains(f.Extension, StringComparer.OrdinalIgnoreCase))
|
||||
.OrderBy(f => f.Name)
|
||||
.ToArray();
|
||||
|
||||
_logger.LogInformation("发现 {FileCount} 个日志文件", logFiles.Length);
|
||||
_logger.LogInformation("发现 {FileCount} 个数据文件({Extensions})",
|
||||
dataFiles.Length, string.Join("/", SupportedExtensions));
|
||||
|
||||
var reports = new List<ParseReport>();
|
||||
foreach (var file in logFiles)
|
||||
foreach (var file in dataFiles)
|
||||
{
|
||||
var report = await ParseFileAsync(file.FullName, force);
|
||||
reports.Add(report);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
);
|
||||
|
||||
@@ -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), // RxType
|
||||
Field2 = int.Parse(match.Groups[2].Value), // RxPower
|
||||
Field3 = int.Parse(match.Groups[3].Value), // TxPower
|
||||
Field4 = int.Parse(match.Groups[4].Value), // Vcoil
|
||||
Field5 = int.Parse(match.Groups[5].Value), // Vin
|
||||
Field6 = int.Parse(match.Groups[6].Value), // Isns
|
||||
Field7 = int.Parse(match.Groups[7].Value), // Ploss
|
||||
Field8 = int.Parse(match.Groups[8].Value), // Threshold
|
||||
Field9 = int.Parse(match.Groups[9].Value), // TriggerCount
|
||||
Field10 = int.Parse(match.Groups[10].Value), // FodResult
|
||||
Field11 = int.Parse(match.Groups[11].Value), // ProtocolType
|
||||
Field12 = int.Parse(match.Groups[12].Value), // PwmDuty
|
||||
Field13 = int.Parse(match.Groups[13].Value), // CoilIndex
|
||||
// 旧格式没有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);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 从本地解析库生成独立正式发布库。
|
||||
/// 当前业务规则:功率列使用 Ploss 的 Field7,重复结果取平均值;缺失值保留为空。
|
||||
/// 当前业务规则:功率列按 Threshold(Field9) 分桶、取值 Ploss(Field7),重复结果取平均值;缺失值保留为空。
|
||||
/// </summary>
|
||||
public class AggregationService
|
||||
{
|
||||
@@ -34,9 +34,10 @@ public class AggregationService
|
||||
public async Task<AggregationReport> AggregateAsync(bool rebuild = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var rows = await LoadRowsAsync(cancellationToken);
|
||||
var qfodAggregates = await LoadQfodAggregatesAsync(cancellationToken);
|
||||
var records = rows
|
||||
.GroupBy(r => BuildKey(r.Scenario))
|
||||
.Select(group => CreateRecord(group.Key, group.ToList()))
|
||||
.Select(group => CreateRecord(group.Key, group.ToList(), qfodAggregates))
|
||||
.ToList();
|
||||
|
||||
await using var transaction = await _releaseContext.Database.BeginTransactionAsync(cancellationToken);
|
||||
@@ -61,34 +62,56 @@ public class AggregationService
|
||||
|
||||
private async Task<List<PlossRow>> LoadRowsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// 只加载 Ploss 及其引用维度。Qfod 明细不在此加载(避免集合 Include 产生笛卡尔积),
|
||||
// 仅在 LoadQfodAggregatesAsync 中按场景聚合出 Sum/Count。
|
||||
var records = await _localContext.PlossRecords
|
||||
.AsNoTracking()
|
||||
.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)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.QfodRecords)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return records.Select(r => new PlossRow(
|
||||
r,
|
||||
r.Scenario,
|
||||
r.Scenario.TxPanel.Name,
|
||||
r.Scenario.TxHardware.Version,
|
||||
r.Scenario.Manufacturer.Name,
|
||||
r.Scenario.CarModel.Name,
|
||||
r.Scenario.RxType.Name)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按场景聚合 Qfod 记录的 CurrentQ/RawQ 求和与计数(在数据库侧完成,只返回每场景 3 个数字)。
|
||||
/// </summary>
|
||||
private async Task<Dictionary<Guid, QfodAggregate>> LoadQfodAggregatesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var groups = await _localContext.QfodRecords
|
||||
.AsNoTracking()
|
||||
.Where(r => !r.IsDeleted)
|
||||
.GroupBy(r => r.ScenarioId)
|
||||
.Select(g => new
|
||||
{
|
||||
ScenarioId = g.Key,
|
||||
QValueSum = g.Sum(r => (double)r.CurrentQ),
|
||||
QBaseSum = g.Sum(r => (double)r.RawQ),
|
||||
Count = g.Count()
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return groups.ToDictionary(
|
||||
g => g.ScenarioId,
|
||||
g => new QfodAggregate(g.QValueSum, g.QBaseSum, g.Count));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -102,12 +125,17 @@ public class AggregationService
|
||||
return ("Unknown", value);
|
||||
}
|
||||
|
||||
private static ChargingParameterRecord CreateRecord(ChargingParameterKey key, List<PlossRow> rows)
|
||||
private static ChargingParameterRecord CreateRecord(
|
||||
ChargingParameterKey key,
|
||||
List<PlossRow> rows,
|
||||
IReadOnlyDictionary<Guid, QfodAggregate> qfodAggregates)
|
||||
{
|
||||
var values = PowerLevels.Select(power => Average(rows
|
||||
.Where(row => IsPowerMatch(row.Record, power))
|
||||
.Select(row => (double?)row.Record.Field7))).ToArray();
|
||||
|
||||
var (qValueSum, qBaseSum, qCount) = AggregateQfod(rows, qfodAggregates);
|
||||
|
||||
return new ChargingParameterRecord
|
||||
{
|
||||
CarFactory = key.CarFactory,
|
||||
@@ -123,16 +151,43 @@ public class AggregationService
|
||||
Power1750mW = values[6],
|
||||
Power2000mW = values[7],
|
||||
Power2250mW = values[8],
|
||||
QValue = Average(rows.SelectMany(row => row.Scenario.QfodRecords).Select(q => (double?)q.CurrentQ)),
|
||||
QBaseValue = Average(rows.SelectMany(row => row.Scenario.QfodRecords).Select(q => (double?)q.RawQ)),
|
||||
QValue = qCount > 0 ? qValueSum / qCount : null,
|
||||
QBaseValue = qCount > 0 ? qBaseSum / qCount : null,
|
||||
PqCoefficient = Average(rows.Select(row => row.Record.DeltaP.HasValue ? (double?)row.Record.DeltaP.Value : null)),
|
||||
ResonanceFrequency = null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 累加分组内各行的场景 Qfod 聚合值。
|
||||
/// 等价于原实现的 SelectMany(row => row.Scenario.QfodRecords):同一场景出现 k 次,其 Sum 与 Count 同倍放大,
|
||||
/// 商保持不变,因此多场景分组时加权语义与原来完全一致,但无需加载 Qfod 明细。
|
||||
/// </summary>
|
||||
private static (double Sum, double BaseSum, int Count) AggregateQfod(
|
||||
List<PlossRow> rows,
|
||||
IReadOnlyDictionary<Guid, QfodAggregate> qfodAggregates)
|
||||
{
|
||||
double sum = 0, baseSum = 0;
|
||||
int count = 0;
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
if (qfodAggregates.TryGetValue(row.Scenario.Id, out var agg))
|
||||
{
|
||||
sum += agg.QValueSum;
|
||||
baseSum += agg.QBaseSum;
|
||||
count += agg.Count;
|
||||
}
|
||||
}
|
||||
|
||||
return (sum, baseSum, count);
|
||||
}
|
||||
|
||||
private static bool IsPowerMatch(PlossRecord record, int power)
|
||||
{
|
||||
return record.Field4 == power || record.Field5 == power || record.PowLoss == power;
|
||||
// 功率列按 Threshold(Field9) 分桶:记录的阈值等于哪个档位,其 Ploss 就进哪一列。
|
||||
// 注:需求文档中 Threshold 为逻辑编号 (8),但当前解析数据里阈值实际落在 field_9。
|
||||
return record.Field9 == power;
|
||||
}
|
||||
|
||||
private static double? Average(IEnumerable<double?> values)
|
||||
@@ -143,6 +198,7 @@ public class AggregationService
|
||||
|
||||
private record PlossRow(PlossRecord Record, TestScenario Scenario, string Panel, string Hardware, string RxType);
|
||||
private record ChargingParameterKey(string CarFactory, string CarModel, string PhoneBrand, string PhoneModel);
|
||||
private readonly record struct QfodAggregate(double QValueSum, double QBaseSum, int Count);
|
||||
}
|
||||
|
||||
public record AggregationReport(int ReleaseRecordCount, int LocalRecordCount);
|
||||
|
||||
15
src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/CarModel.cs
Normal file
15
src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/CarModel.cs
Normal 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; } = [];
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -19,7 +19,6 @@ public class PlossRecord
|
||||
public int? DeltaP { get; set; }
|
||||
|
||||
// ===== 第二行字段(16个通用字段,均支持负数) =====
|
||||
/// <summary>Field1: RX_TYPE - 接收端类型</summary>
|
||||
public int Field1 { get; set; }
|
||||
|
||||
public int Field2 { get; set; }
|
||||
@@ -33,10 +32,9 @@ public class PlossRecord
|
||||
|
||||
public int Field6 { get; set; }
|
||||
|
||||
/// <summary>核心判定值:Field7 (原Ploss) - 负数表示安全</summary>
|
||||
|
||||
public int Field7 { get; set; }
|
||||
|
||||
/// <summary>安全红线:Field8 (原Threshold) - 当前功率段阈值</summary>
|
||||
public int Field8 { get; set; }
|
||||
|
||||
/// <summary>Field9: ploss - 功率损耗计算值</summary>
|
||||
@@ -45,7 +43,7 @@ public class PlossRecord
|
||||
/// <summary>Field10: threshold - 阈值</summary>
|
||||
public int Field10 { get; set; }
|
||||
|
||||
/// <summary>Field11: 触发次数 - 当 Field7 > Field8(损耗大于阈值)时计数,超过5次后触发FOD保护</summary>
|
||||
/// <summary>Field11: 触发次数 - 当 Field9 > Field10(ploss 大于阈值)时计数,超过5次后触发FOD保护</summary>
|
||||
public int Field11 { get; set; }
|
||||
|
||||
/// <summary>Field12: PFOD result 标志 - 触发时置位</summary>
|
||||
@@ -62,6 +60,6 @@ public class PlossRecord
|
||||
// Navigation Properties
|
||||
public TestScenario Scenario { get; set; } = null!;
|
||||
|
||||
/// <summary>派生属性:安全余量(不落库) = Field8 - Field7</summary>
|
||||
public int Margin => Field8 - Field7;
|
||||
/// <summary>派生属性:安全余量(不落库) = Field10 - Field9(Threshold - Ploss)</summary>
|
||||
public int Margin => Field10 - Field9;
|
||||
}
|
||||
@@ -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!;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,7 +2,9 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
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;
|
||||
|
||||
@@ -54,11 +56,17 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<DimensionService>();
|
||||
services.AddScoped<ScenarioService>();
|
||||
services.AddScoped<ParseService>();
|
||||
services.AddScoped<StatsService>();
|
||||
services.AddScoped<ExportService>();
|
||||
services.AddScoped<CleanService>();
|
||||
services.AddScoped<AggregationService>();
|
||||
|
||||
// Analysis
|
||||
services.AddScoped<PlossAnalysisService>();
|
||||
services.AddScoped<QfodCalibrationService>();
|
||||
|
||||
// Excel Export
|
||||
services.AddScoped<AnalyzeExcelExporter>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -111,12 +111,12 @@ public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
||||
|
||||
// 索引
|
||||
builder.HasIndex(e => e.ScenarioId).HasDatabaseName("idx_ploss_scenario");
|
||||
builder.HasIndex(e => e.Field2).HasDatabaseName("idx_ploss_field_2"); // 原 rx_power
|
||||
builder.HasIndex(e => e.Field7).HasDatabaseName("idx_ploss_field_7"); // 原 ploss
|
||||
builder.HasIndex(e => e.Field8).HasDatabaseName("idx_ploss_field_8"); // 原 threshold
|
||||
builder.HasIndex(e => e.Field10).HasDatabaseName("idx_ploss_field_10"); // 原 fod_result
|
||||
builder.HasIndex(e => e.PowLoss).HasDatabaseName("idx_ploss_pow_loss"); // 新增
|
||||
builder.HasIndex(e => e.DeltaP).HasDatabaseName("idx_ploss_delta_p"); // 新增
|
||||
builder.HasIndex(e => e.Field2).HasDatabaseName("idx_ploss_field_2");
|
||||
builder.HasIndex(e => e.Field7).HasDatabaseName("idx_ploss_field_7");
|
||||
builder.HasIndex(e => e.Field8).HasDatabaseName("idx_ploss_field_8");
|
||||
builder.HasIndex(e => e.Field10).HasDatabaseName("idx_ploss_field_10");
|
||||
builder.HasIndex(e => e.PowLoss).HasDatabaseName("idx_ploss_pow_loss");
|
||||
builder.HasIndex(e => e.DeltaP).HasDatabaseName("idx_ploss_delta_p");
|
||||
|
||||
// 关系配置
|
||||
builder.HasOne(e => e.Scenario)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
// 仅处理 WctMinerDbContext(ReleaseDbContext 无本次 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 无法原地 ALTER,EF 后续 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};");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
805
src/Gpulse.WCT.DataAnalyzer.Core/Migrations/20260821085318_InitialCreate.Designer.cs
generated
Normal file
805
src/Gpulse.WCT.DataAnalyzer.Core/Migrations/20260821085318_InitialCreate.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
805
src/Gpulse.WCT.DataAnalyzer.Core/Migrations/ReleaseData/20260821085806_InitialCreate.Designer.cs
generated
Normal file
805
src/Gpulse.WCT.DataAnalyzer.Core/Migrations/ReleaseData/20260821085806_InitialCreate.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
331
src/Gpulse.WCT.DataAnalyzer/Commands/AnalyzeCommand.cs
Normal file
331
src/Gpulse.WCT.DataAnalyzer/Commands/AnalyzeCommand.cs
Normal file
@@ -0,0 +1,331 @@
|
||||
using System.CommandLine;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application.Analysis;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application.Exporting;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 数据分析命令 - 支持 Ploss 余量/delta_p 分析与 Qfod 标定阈值分析
|
||||
/// </summary>
|
||||
public class AnalyzeCommand : Command
|
||||
{
|
||||
private static readonly string[] LogExtensions = [".DAT", ".dat", ".LOG", ".log"];
|
||||
|
||||
public AnalyzeCommand(
|
||||
PlossAnalysisService plossAnalysisService,
|
||||
QfodCalibrationService qfodCalibrationService,
|
||||
AnalyzeExcelExporter excelExporter)
|
||||
: base("analyze", "Analyze Ploss margin or Qfod calibration threshold from log files")
|
||||
{
|
||||
var typeOption = new Option<string>(
|
||||
"--type",
|
||||
"Analysis type: ploss or qfod"
|
||||
)
|
||||
{
|
||||
IsRequired = true
|
||||
};
|
||||
|
||||
var fileOption = new Option<string?>(
|
||||
"--file",
|
||||
"Ploss: path to a log file to analyze"
|
||||
);
|
||||
|
||||
var pureOption = new Option<string?>(
|
||||
"--pure",
|
||||
"Qfod: path to the phone-only charging log file"
|
||||
);
|
||||
|
||||
var foreignOption = new Option<string?>(
|
||||
"--foreign",
|
||||
"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, 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, excelExporter, file, output);
|
||||
break;
|
||||
case "qfod":
|
||||
await HandleQfodAsync(qfodCalibrationService, excelExporter, pure, foreign, output);
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine($"Unknown type: {type}. Use 'ploss' or 'qfod'.");
|
||||
break;
|
||||
}
|
||||
}, typeOption, fileOption, pureOption, foreignOption, outputOption, autoOption);
|
||||
}
|
||||
|
||||
/// <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))
|
||||
{
|
||||
Console.WriteLine("Please specify --file for Ploss analysis");
|
||||
return;
|
||||
}
|
||||
|
||||
var excelReport = await service.AnalyzeForExcelAsync(file);
|
||||
|
||||
if (!excelReport.IsSuccess)
|
||||
{
|
||||
Console.WriteLine($"[FAILED] {excelReport.FileName}: {excelReport.ErrorMessage}");
|
||||
return;
|
||||
}
|
||||
|
||||
var outputPath = GetOutputPath(file, output);
|
||||
|
||||
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,
|
||||
AnalyzeExcelExporter exporter,
|
||||
string? pure,
|
||||
string? foreign,
|
||||
string? output)
|
||||
{
|
||||
if (string.IsNullOrEmpty(pure) || string.IsNullOrEmpty(foreign))
|
||||
{
|
||||
Console.WriteLine("Please specify --pure and --foreign for Qfod calibration");
|
||||
return;
|
||||
}
|
||||
|
||||
var excelReport = await service.CalibrateForExcelAsync(pure, foreign);
|
||||
|
||||
if (!excelReport.IsSuccess)
|
||||
{
|
||||
Console.WriteLine($"[FAILED] {excelReport.ErrorMessage}");
|
||||
return;
|
||||
}
|
||||
|
||||
var outputPath = output ?? GetOutputPath(pure, output, "qfod");
|
||||
|
||||
await exporter.ExportQfodReportAsync(excelReport, outputPath);
|
||||
Console.WriteLine($"[OK] {outputPath}");
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
using System.CommandLine;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 统计查询命令
|
||||
/// </summary>
|
||||
public class StatsCommand : Command
|
||||
{
|
||||
public StatsCommand(StatsService statsService)
|
||||
: base("stats", "Query statistics from database")
|
||||
{
|
||||
var summaryOption = new Option<bool>(
|
||||
"--summary",
|
||||
() => false,
|
||||
"Show overall statistics summary"
|
||||
);
|
||||
|
||||
var byFodTypeOption = new Option<bool>(
|
||||
"--by-fod-type",
|
||||
() => false,
|
||||
"Show statistics by FOD type"
|
||||
);
|
||||
|
||||
var byPanelOption = new Option<bool>(
|
||||
"--by-panel",
|
||||
() => false,
|
||||
"Show statistics by TX panel"
|
||||
);
|
||||
|
||||
var byHardwareOption = new Option<bool>(
|
||||
"--by-hardware",
|
||||
() => false,
|
||||
"Show statistics by TX hardware version"
|
||||
);
|
||||
|
||||
var byRxTypeOption = new Option<bool>(
|
||||
"--by-rx-type",
|
||||
() => false,
|
||||
"Show statistics by RX type"
|
||||
);
|
||||
|
||||
AddOption(summaryOption);
|
||||
AddOption(byFodTypeOption);
|
||||
AddOption(byPanelOption);
|
||||
AddOption(byHardwareOption);
|
||||
AddOption(byRxTypeOption);
|
||||
|
||||
this.SetHandler(async (summary, byFodType, byPanel, byHardware, byRxType) =>
|
||||
{
|
||||
// 如果没有指定任何选项,默认显示摘要
|
||||
if (!summary && !byFodType && !byPanel && !byHardware && !byRxType)
|
||||
{
|
||||
summary = true;
|
||||
}
|
||||
|
||||
if (summary)
|
||||
{
|
||||
var s = await statsService.GetSummaryAsync();
|
||||
Console.WriteLine("=== Statistics Summary ===");
|
||||
Console.WriteLine($" Scenarios: {s.ScenarioCount}");
|
||||
Console.WriteLine($" Qfod Records: {s.QfodCount}");
|
||||
Console.WriteLine($" Ploss Records: {s.PlossCount}");
|
||||
Console.WriteLine($" TX Panels: {s.PanelCount}");
|
||||
Console.WriteLine($" TX Hardware: {s.HardwareCount}");
|
||||
Console.WriteLine($" TX Software: {s.SoftwareCount}");
|
||||
Console.WriteLine($" RX Types: {s.RxTypeCount}");
|
||||
}
|
||||
|
||||
if (byFodType)
|
||||
{
|
||||
Console.WriteLine("\n=== By FOD Type ===");
|
||||
var stats = await statsService.GetStatsByFodTypeAsync();
|
||||
foreach (var s in stats)
|
||||
{
|
||||
Console.WriteLine($" FOD Type {s.FodType}: {s.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
if (byPanel)
|
||||
{
|
||||
Console.WriteLine("\n=== By TX Panel ===");
|
||||
var stats = await statsService.GetStatsByPanelAsync();
|
||||
foreach (var s in stats)
|
||||
{
|
||||
Console.WriteLine($" {s.Name}: {s.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
if (byHardware)
|
||||
{
|
||||
Console.WriteLine("\n=== By TX Hardware ===");
|
||||
var stats = await statsService.GetStatsByHardwareAsync();
|
||||
foreach (var s in stats)
|
||||
{
|
||||
Console.WriteLine($" {s.Name}: {s.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
if (byRxType)
|
||||
{
|
||||
Console.WriteLine("\n=== By RX Type ===");
|
||||
var stats = await statsService.GetStatsByRxTypeAsync();
|
||||
foreach (var s in stats)
|
||||
{
|
||||
Console.WriteLine($" {s.Name}: {s.Count}");
|
||||
}
|
||||
}
|
||||
}, summaryOption, byFodTypeOption, byPanelOption, byHardwareOption, byRxTypeOption);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -7,6 +10,8 @@ using Gpulse.WCT.DataAnalyzer.Commands;
|
||||
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;
|
||||
|
||||
@@ -17,14 +22,8 @@ public class Program
|
||||
{
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
// 1. 构建引导配置(用于 Serilog 初始化)
|
||||
var bootstrapConfig = new ConfigurationBuilder()
|
||||
.SetBasePath(AppContext.BaseDirectory)
|
||||
.AddJsonFile("appsettings.json", optional: false)
|
||||
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true)
|
||||
.AddEnvironmentVariables()
|
||||
.AddCommandLine(args)
|
||||
.Build();
|
||||
// 1. 构建引导配置(用于 Serilog 初始化),并将数据库/日志等相对路径解析到 exe 所在目录
|
||||
var bootstrapConfig = BuildConfiguration(args);
|
||||
|
||||
// 2. 创建引导日志器
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
@@ -53,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);
|
||||
@@ -74,9 +73,12 @@ public class Program
|
||||
|
||||
rootCommand.AddCommand(new ParseCommand(provider.GetRequiredService<ParseService>()));
|
||||
rootCommand.AddCommand(new AggregateCommand(provider.GetRequiredService<AggregationService>()));
|
||||
rootCommand.AddCommand(new StatsCommand(provider.GetRequiredService<StatsService>()));
|
||||
rootCommand.AddCommand(new ExportCommand(provider.GetRequiredService<ExportService>()));
|
||||
rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService<CleanService>()));
|
||||
rootCommand.AddCommand(new AnalyzeCommand(
|
||||
provider.GetRequiredService<PlossAnalysisService>(),
|
||||
provider.GetRequiredService<QfodCalibrationService>(),
|
||||
provider.GetRequiredService<AnalyzeExcelExporter>()));
|
||||
|
||||
return await rootCommand.InvokeAsync(args);
|
||||
}
|
||||
@@ -90,4 +92,168 @@ public class Program
|
||||
await Log.CloseAndFlushAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建配置。除支持 json/环境变量/命令行外,
|
||||
/// 将 appsettings.json 中数据库、日志、输出等相对路径统一解析为基于 exe 所在目录的绝对路径,
|
||||
/// 保证应用从任意工作目录启动时,配置文件、数据库、日志都落在 exe 旁边。
|
||||
/// </summary>
|
||||
private static IConfiguration BuildConfiguration(string[] args)
|
||||
{
|
||||
var builder = new ConfigurationBuilder()
|
||||
.SetBasePath(AppContext.BaseDirectory)
|
||||
.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);
|
||||
|
||||
var config = builder.Build();
|
||||
|
||||
// 收集需要解析的相对路径,通过内存配置覆盖层注入(优先级高于 json/环境变量/命令行)
|
||||
var overrides = ResolveRelativePaths(config);
|
||||
|
||||
if (overrides.Count == 0)
|
||||
return config;
|
||||
|
||||
return new ConfigurationBuilder()
|
||||
.AddConfiguration(config)
|
||||
.AddInMemoryCollection(overrides)
|
||||
.Build();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将配置中的相对路径解析为基于 exe 目录的绝对路径。
|
||||
/// </summary>
|
||||
private static Dictionary<string, string?> ResolveRelativePaths(IConfiguration config)
|
||||
{
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
var overrides = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// 数据库文件
|
||||
foreach (var key in new[] { "Database:Path", "Database:LocalPath", "Database:ReleasePath" })
|
||||
{
|
||||
AddResolved(overrides, config, key, baseDirectory);
|
||||
}
|
||||
|
||||
// 日志与输出目录
|
||||
foreach (var key in new[] { "Paths:LogDir", "Paths:OutputDir" })
|
||||
{
|
||||
AddResolved(overrides, config, key, baseDirectory);
|
||||
}
|
||||
|
||||
// Serilog 文件日志路径(遍历 WriteTo 数组,避免硬编码索引)
|
||||
foreach (var sink in config.GetSection("Serilog:WriteTo").GetChildren())
|
||||
{
|
||||
if (!string.Equals(sink["Name"], "File", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
AddResolved(overrides, config, $"{sink.Path}:Args:path", baseDirectory);
|
||||
}
|
||||
|
||||
return overrides;
|
||||
}
|
||||
|
||||
private static void AddResolved(
|
||||
Dictionary<string, string?> overrides,
|
||||
IConfiguration config,
|
||||
string key,
|
||||
string baseDirectory)
|
||||
{
|
||||
var value = config[key];
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return;
|
||||
|
||||
overrides[key] = Path.IsPathRooted(value)
|
||||
? value
|
||||
: Path.GetFullPath(Path.Combine(baseDirectory, value));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 嵌入资源配置加载 ──────────────────────────────────────────────────────────
|
||||
|
||||
internal sealed class EmbeddedResourceJsonConfigurationProvider(
|
||||
EmbeddedResourceJsonConfigurationSource source) : FileConfigurationProvider(source)
|
||||
{
|
||||
public override void Load(Stream stream)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(stream);
|
||||
Data = FlattenJson(doc.RootElement);
|
||||
}
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
var assembly = source.Assembly;
|
||||
var resourceName = source.ResourceName;
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName)
|
||||
?? throw new InvalidOperationException(
|
||||
$"嵌入资源 '{resourceName}' 不存在。"
|
||||
+ $" 可用资源: [{string.Join(", ", assembly.GetManifestResourceNames())}]");
|
||||
|
||||
Load(stream);
|
||||
}
|
||||
|
||||
private static Dictionary<string, string?> FlattenJson(JsonElement element)
|
||||
{
|
||||
var result = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
Walk(element, "", result);
|
||||
return result;
|
||||
|
||||
static void Walk(JsonElement el, string prefix, Dictionary<string, string?> bag)
|
||||
{
|
||||
switch (el.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
foreach (var prop in el.EnumerateObject())
|
||||
Walk(prop.Value, string.IsNullOrEmpty(prefix) ? prop.Name : $"{prefix}:{prop.Name}", bag);
|
||||
break;
|
||||
case JsonValueKind.Array:
|
||||
var i = 0;
|
||||
foreach (var item in el.EnumerateArray())
|
||||
{
|
||||
Walk(item, $"{prefix}:{i}", bag);
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
bag[prefix] = el.ToString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class EmbeddedResourceJsonConfigurationSource : FileConfigurationSource
|
||||
{
|
||||
public Assembly Assembly { get; } = null!;
|
||||
public string ResourceName { get; } = null!;
|
||||
|
||||
public EmbeddedResourceJsonConfigurationSource(Assembly assembly, string resourceName)
|
||||
{
|
||||
Assembly = assembly;
|
||||
ResourceName = resourceName;
|
||||
}
|
||||
|
||||
public override IConfigurationProvider Build(IConfigurationBuilder builder) =>
|
||||
new EmbeddedResourceJsonConfigurationProvider(this);
|
||||
}
|
||||
|
||||
public static class EmbeddedResourceConfigurationExtensions
|
||||
{
|
||||
public static IConfigurationBuilder AddEmbeddedResourceJson(
|
||||
this IConfigurationBuilder builder,
|
||||
Assembly assembly,
|
||||
string resourceName)
|
||||
{
|
||||
builder.Add(new EmbeddedResourceJsonConfigurationSource(assembly, resourceName));
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
38
src/Gpulse.WCT.DataAnalyzer/analyze.bat
Normal file
38
src/Gpulse.WCT.DataAnalyzer/analyze.bat
Normal 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
|
||||
28
src/Gpulse.WCT.DataAnalyzer/appsettings.json
Normal file
28
src/Gpulse.WCT.DataAnalyzer/appsettings.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
55
tests/Gpulse.WCT.DataAnalyzer.Tests/CliStructureTests.cs
Normal file
55
tests/Gpulse.WCT.DataAnalyzer.Tests/CliStructureTests.cs
Normal 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"));
|
||||
});
|
||||
}
|
||||
}
|
||||
38
tests/Gpulse.WCT.DataAnalyzer.Tests/CliTestExtensions.cs
Normal file
38
tests/Gpulse.WCT.DataAnalyzer.Tests/CliTestExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
106
tests/Gpulse.WCT.DataAnalyzer.Tests/CliTestHost.cs
Normal file
106
tests/Gpulse.WCT.DataAnalyzer.Tests/CliTestHost.cs
Normal 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 { /* 尽力清理临时目录 */ }
|
||||
}
|
||||
}
|
||||
60
tests/Gpulse.WCT.DataAnalyzer.Tests/CommandTestBase.cs
Normal file
60
tests/Gpulse.WCT.DataAnalyzer.Tests/CommandTestBase.cs
Normal 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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
using ClosedXML.Excel;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
|
||||
|
||||
[TestFixture]
|
||||
public class AnalyzeCommandTests : CommandTestBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 真实产线 .DAT:含 35 条两行 Ploss(1 条 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,-21,foreign Δ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-004:Field10 非标准功率段的记录应计入 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-002:pure/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
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Tests.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// RV-001:EF 迁移替代 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;
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
2
tests/Gpulse.WCT.DataAnalyzer.Tests/GlobalUsings.cs
Normal file
2
tests/Gpulse.WCT.DataAnalyzer.Tests/GlobalUsings.cs
Normal file
@@ -0,0 +1,2 @@
|
||||
global using NUnit.Framework;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -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>
|
||||
80
tests/Gpulse.WCT.DataAnalyzer.Tests/TestFixtures.cs
Normal file
80
tests/Gpulse.WCT.DataAnalyzer.Tests/TestFixtures.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user