Compare commits
29 Commits
5189c665c3
...
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 | ||
|
|
2b031995e1 | ||
|
|
a3100c73c5 | ||
|
|
6e197a0cce | ||
|
|
645f46abcd |
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/
|
||||
72
Gpulse.WCT.DataAnalyzer.sln
Normal file
72
Gpulse.WCT.DataAnalyzer.sln
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gpulse.WCT.DataAnalyzer.Core", "src\Gpulse.WCT.DataAnalyzer.Core\Gpulse.WCT.DataAnalyzer.Core.csproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345601}"
|
||||
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
|
||||
EndGlobal
|
||||
@@ -1,37 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WCTDataMiner.Core", "src\WCTDataMiner.Core\WCTDataMiner.Core.csproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345601}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WCTDataMiner.Wpf", "src\WCTDataMiner.Wpf\WCTDataMiner.Wpf.csproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345602}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WCTDataMiner", "src\WCTDataMiner\WCTDataMiner.csproj", "{A1B2C3D4-1234-5678-9ABC-DEF012345603}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
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}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345601}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345602}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345602}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345602}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345602}.Release|Any CPU.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}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A1B2C3D4-1234-5678-9ABC-DEF012345603}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {A1B2C3D4-1234-5678-9ABC-DEF012345600}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
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/{车厂}/{车型}/{手机名}/`
|
||||
|
||||
---
|
||||
|
||||
## 联系支持
|
||||
|
||||
如有问题,请联系开发团队。
|
||||
@@ -1,4 +1,4 @@
|
||||
# WCTDataMiner 文档中心
|
||||
# Gpulse.WCT.DataAnalyzer 文档中心
|
||||
|
||||
> 无线充电 FOD(异物检测)日志数据采集系统
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -6,18 +6,19 @@
|
||||
|
||||
## 1. 命令概览
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `parse` | 解析日志文件并存储到数据库 |
|
||||
| `stats` | 查询统计信息 |
|
||||
| `export` | 导出数据 |
|
||||
| `clean` | 清理数据 |
|
||||
| 命令 | 说明 | 数据流 |
|
||||
|------|------|--------|
|
||||
| `parse` | 解析日志文件到本地库 | log → local.db |
|
||||
| `aggregate` | 从本地库汇总生成发布库 | local.db → release.db |
|
||||
| `export` | 导出发布库到 CSV | release.db → CSV |
|
||||
| `clean` | 清理本地库数据(软删除) | 只写 local.db |
|
||||
| `analyze` | 独立分析:Ploss 余量/delta_p、Qfod 标定阈值 | log → 控制台报告 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 命令详解
|
||||
|
||||
### 2.1 parse - 解析日志文件
|
||||
### 2.1 parse
|
||||
|
||||
```bash
|
||||
# 解析单个文件
|
||||
@@ -29,41 +30,41 @@ dotnet run parse --dir path/to/logs/
|
||||
# 解析目录(递归)
|
||||
dotnet run parse --dir path/to/logs/ --recursive
|
||||
|
||||
# 强制重新解析(覆盖已有数据)
|
||||
# 强制重新解析
|
||||
dotnet run parse --file log.txt --force
|
||||
```
|
||||
|
||||
### 2.2 stats - 查询统计
|
||||
### 2.2 aggregate
|
||||
|
||||
```bash
|
||||
# 查看文件解析统计
|
||||
dotnet run stats --file log.txt
|
||||
# 从 local.db 汇总生成 release.db(默认重建)
|
||||
dotnet run aggregate
|
||||
|
||||
# 查看整体统计
|
||||
dotnet run stats --summary
|
||||
|
||||
# 按 FOD 类型统计
|
||||
dotnet run stats --by-fod-type
|
||||
# 追加模式(不删除已有记录)
|
||||
dotnet run aggregate --rebuild false
|
||||
```
|
||||
|
||||
### 2.3 export - 导出数据
|
||||
### 2.3 export
|
||||
|
||||
```bash
|
||||
# 导出为 CSV
|
||||
dotnet run export --format csv --output ./output/
|
||||
# 导出正式充电参数 CSV(默认)
|
||||
dotnet run export --output ./data/output
|
||||
|
||||
# 导出为 JSON
|
||||
dotnet run export --format json --output ./output/
|
||||
# 导出正式充电参数 CSV + legacy Qfod/Ploss
|
||||
dotnet run export --type all --output ./data/output
|
||||
|
||||
# 按文件导出
|
||||
dotnet run export --file log.txt --format csv
|
||||
# 仅导出 legacy Qfod CSV
|
||||
dotnet run export --type qfod --format csv
|
||||
|
||||
# 导出 JSON
|
||||
dotnet run export --format json --type qfod
|
||||
```
|
||||
|
||||
### 2.4 clean - 清理数据
|
||||
### 2.4 clean
|
||||
|
||||
```bash
|
||||
# 清理指定文件的数据
|
||||
dotnet run clean --file log.txt --confirm
|
||||
# 清理指定场景
|
||||
dotnet run clean --scenario <guid> --confirm
|
||||
|
||||
# 清理所有数据
|
||||
dotnet run clean --all --confirm
|
||||
@@ -71,98 +72,37 @@ dotnet run clean --all --confirm
|
||||
|
||||
---
|
||||
|
||||
## 3. 命令实现
|
||||
### 2.5 analyze
|
||||
|
||||
```csharp
|
||||
// Commands/ParseCommand.cs
|
||||
using System.CommandLine;
|
||||
```bash
|
||||
# Ploss 余量 / delta_p 分析(单文件,直接解析日志,不依赖本地库)
|
||||
dotnet run analyze --type ploss --file <log>
|
||||
|
||||
namespace WCTDataMiner.Commands;
|
||||
|
||||
public class ParseCommand : Command
|
||||
{
|
||||
public ParseCommand(ParseService parseService)
|
||||
: base("parse", "Parse log files and store data to database")
|
||||
{
|
||||
var fileOption = new Option<string>(
|
||||
"--file",
|
||||
"Path to a single log file to parse"
|
||||
);
|
||||
|
||||
var dirOption = new Option<string>(
|
||||
"--dir",
|
||||
"Path to directory containing log files"
|
||||
);
|
||||
|
||||
var recursiveOption = new Option<bool>(
|
||||
"--recursive",
|
||||
() => false,
|
||||
"Recursively search subdirectories"
|
||||
);
|
||||
|
||||
var forceOption = new Option<bool>(
|
||||
"--force",
|
||||
() => false,
|
||||
"Force re-parse even if file already parsed"
|
||||
);
|
||||
|
||||
AddOption(fileOption);
|
||||
AddOption(dirOption);
|
||||
AddOption(recursiveOption);
|
||||
AddOption(forceOption);
|
||||
|
||||
this.SetHandler(async (file, dir, recursive, force) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(file))
|
||||
{
|
||||
await parseService.ParseFileAsync(file, force);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(dir))
|
||||
{
|
||||
await parseService.ParseDirectoryAsync(dir, recursive, force);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Please specify --file or --dir");
|
||||
}
|
||||
}, fileOption, dirOption, recursiveOption, forceOption);
|
||||
}
|
||||
}
|
||||
# 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`。
|
||||
|
||||
---
|
||||
|
||||
## 4. Program.cs 入口
|
||||
## 3. 典型工作流
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
using System.CommandLine;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using WCTDataMiner.Commands;
|
||||
```bash
|
||||
# 1. 解析日志
|
||||
dotnet run parse --dir ./data/test_input --recursive
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
# 2. 汇总生成发布库
|
||||
dotnet run aggregate
|
||||
|
||||
// 注册服务
|
||||
builder.Services.AddScoped<ParseService>();
|
||||
builder.Services.AddScoped<StatsService>();
|
||||
builder.Services.AddScoped<ExportService>();
|
||||
builder.Services.AddScoped<CleanService>();
|
||||
builder.Services.AddDatabaseServices(builder.Configuration);
|
||||
# 3. 导出正式 CSV
|
||||
dotnet run export --output ./data/output
|
||||
```
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// 创建根命令
|
||||
var rootCommand = new RootCommand("WCTDataMiner - FOD Log Data Parser");
|
||||
|
||||
// 使用服务提供者获取服务
|
||||
using var scope = app.Services.CreateScope();
|
||||
var provider = scope.ServiceProvider;
|
||||
|
||||
rootCommand.AddCommand(new ParseCommand(provider.GetRequiredService<ParseService>()));
|
||||
rootCommand.AddCommand(new StatsCommand(provider.GetRequiredService<StatsService>()));
|
||||
rootCommand.AddCommand(new ExportCommand(provider.GetRequiredService<ExportService>()));
|
||||
rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService<CleanService>()));
|
||||
|
||||
return await rootCommand.InvokeAsync(args);
|
||||
```
|
||||
输出文件: `./data/output/WCT-ChargingParameterDatabase.csv`
|
||||
@@ -1,16 +1,14 @@
|
||||
# DbContext 工厂
|
||||
|
||||
> 所属模块:[数据库切换支持](./概览.md)
|
||||
> 所属模块:[数据库支持](./概览.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 接口定义
|
||||
## 1. 本地库工厂
|
||||
|
||||
```csharp
|
||||
// Data/IDbContextFactory.cs
|
||||
using WCTDataMiner.Data;
|
||||
|
||||
namespace WCTDataMiner.Data;
|
||||
// Infrastructure/LocalData/IDbContextFactory.cs
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
public interface IDbContextFactory
|
||||
{
|
||||
@@ -18,105 +16,68 @@ public interface IDbContextFactory
|
||||
}
|
||||
```
|
||||
|
||||
工厂实现读取 `Database:LocalPath`(fallback `Database:Path`),支持 SQLite 和 PostgreSQL。
|
||||
|
||||
---
|
||||
|
||||
## 2. 工厂实现
|
||||
## 2. 发布库工厂
|
||||
|
||||
```csharp
|
||||
// Data/DbContextFactory.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
// Infrastructure/ReleaseData/IReleaseDbContextFactory.cs
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.ReleaseData;
|
||||
|
||||
namespace WCTDataMiner.Data;
|
||||
|
||||
public class DbContextFactory : IDbContextFactory
|
||||
public interface IReleaseDbContextFactory
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public DbContextFactory(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public WctMinerDbContext CreateDbContext()
|
||||
{
|
||||
var dbType = _configuration["Database:Type"]?.ToLower() ?? "sqlite";
|
||||
var optionsBuilder = new DbContextOptionsBuilder<WctMinerDbContext>();
|
||||
|
||||
switch (dbType)
|
||||
{
|
||||
case "postgresql":
|
||||
case "postgres":
|
||||
var pgConnStr = BuildPostgreSqlConnectionString();
|
||||
optionsBuilder.UseNpgsql(pgConnStr);
|
||||
break;
|
||||
|
||||
case "sqlite":
|
||||
default:
|
||||
var dbPath = _configuration["Database:Path"] ?? "./data/database/wctminer.db";
|
||||
EnsureDirectoryExists(dbPath);
|
||||
optionsBuilder.UseSqlite($"Data Source={dbPath}");
|
||||
break;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
optionsBuilder.EnableSensitiveDataLogging();
|
||||
optionsBuilder.EnableDetailedErrors();
|
||||
#endif
|
||||
|
||||
return new WctMinerDbContext(optionsBuilder.Options);
|
||||
}
|
||||
|
||||
private string BuildPostgreSqlConnectionString()
|
||||
{
|
||||
var host = _configuration["Database:Host"] ?? "localhost";
|
||||
var port = _configuration["Database:Port"] ?? "5432";
|
||||
var name = _configuration["Database:Name"] ?? "wctminer";
|
||||
var user = _configuration["Database:User"] ?? "postgres";
|
||||
var password = _configuration["Database:Password"] ?? "";
|
||||
|
||||
return $"Host={host};Port={port};Database={name};Username={user};Password={password}";
|
||||
}
|
||||
|
||||
private static void EnsureDirectoryExists(string dbPath)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(dbPath);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
}
|
||||
ReleaseDbContext CreateDbContext();
|
||||
}
|
||||
```
|
||||
|
||||
工厂实现读取 `Database:ReleasePath`,支持 SQLite 和 PostgreSQL。PostgreSQL 时使用 `Release*` 前缀配置(`ReleaseHost`, `ReleasePort`, `ReleaseName`, `ReleaseUser`, `ReleasePassword`),未设置时回退到 local 的对应配置。
|
||||
|
||||
---
|
||||
|
||||
## 3. 依赖注入扩展
|
||||
## 3. DI 注册
|
||||
|
||||
```csharp
|
||||
// Extensions/ServiceCollectionExtensions.cs
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using WCTDataMiner.Data;
|
||||
|
||||
namespace WCTDataMiner.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
// Infrastructure/Extensions/ServiceCollectionExtensions.cs
|
||||
public static IServiceCollection AddDatabaseServices(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
public static IServiceCollection AddDatabaseServices(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
// 本地库
|
||||
services.AddScoped<IDbContextFactory, DbContextFactory>();
|
||||
services.AddScoped<WctMinerDbContext>(sp =>
|
||||
{
|
||||
// 注册 DbContext 工厂
|
||||
services.AddScoped<IDbContextFactory, DbContextFactory>();
|
||||
|
||||
// 注册 DbContext(通过工厂创建)
|
||||
services.AddScoped<WctMinerDbContext>(sp =>
|
||||
{
|
||||
var factory = sp.GetRequiredService<IDbContextFactory>();
|
||||
return factory.CreateDbContext();
|
||||
});
|
||||
var factory = sp.GetRequiredService<IDbContextFactory>();
|
||||
return factory.CreateDbContext();
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
// 发布库(独立)
|
||||
services.AddScoped<IReleaseDbContextFactory, ReleaseDbContextFactory>();
|
||||
services.AddScoped<ReleaseDbContext>(sp =>
|
||||
{
|
||||
var factory = sp.GetRequiredService<IReleaseDbContextFactory>();
|
||||
return factory.CreateDbContext();
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 启动初始化
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
using var scope = app.Services.CreateScope();
|
||||
|
||||
// 初始化本地库
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
|
||||
await dbContext.Database.EnsureCreatedAsync();
|
||||
|
||||
// 初始化发布库
|
||||
var releaseContext = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>();
|
||||
await releaseContext.Database.EnsureCreatedAsync();
|
||||
|
||||
// 本地库种子数据
|
||||
SeedData.Initialize(dbContext);
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
# 数据库切换支持
|
||||
# 数据库切换与双数据库架构
|
||||
|
||||
> 所属模块:[架构概览](../概览.md)
|
||||
|
||||
@@ -8,38 +8,48 @@
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [DbContext 工厂](./DbContext工厂.md) | 数据库上下文工厂设计 |
|
||||
| [迁移策略](./迁移策略.md) | 数据库迁移和切换步骤 |
|
||||
| [DbContext 工厂](./DbContext工厂.md) | 两个数据库上下文工厂设计 |
|
||||
| [迁移策略](./迁移策略.md) | 数据库迁移步骤 |
|
||||
|
||||
---
|
||||
|
||||
## 切换策略概览
|
||||
## 双数据库架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ appsettings.json │
|
||||
│ "Database": { "Type": "sqlite" | "postgresql" } │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ appsettings.json │
|
||||
│ Database:LocalPath → local.db │
|
||||
│ Database:ReleasePath → release.db │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ DbContextFactory │
|
||||
│ 读取配置 → 选择 Provider → 创建 DbContext │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ DbContextFactory │
|
||||
│ 读取配置 → 选择 Provider → 创建 WctMinerDbContext │
|
||||
│ ReleaseDbContextFactory │
|
||||
│ 读取配置 → 选择 Provider → 创建 ReleaseDbContext │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ WctMinerDbContext │
|
||||
│ 统一的 DbModel 配置,适配两种数据库 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ local.db release.db │
|
||||
│ ──────── ────────── │
|
||||
│ tx_panel charging_parameter │
|
||||
│ tx_hardware 独立实体,无外键关联 │
|
||||
│ tx_software │
|
||||
│ rx_type │
|
||||
│ test_scenario │
|
||||
│ qfod_record │
|
||||
│ ploss_record │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 当前状态
|
||||
|
||||
| 项目 | 状态 |
|
||||
|------|------|
|
||||
| SQLite | ✅ 默认启用 |
|
||||
| PostgreSQL | ✅ 架构支持,待配置 |
|
||||
| 切换方式 | 配置文件切换 |
|
||||
| PostgreSQL | ✅ 架构支持 |
|
||||
| local.db | ✅ 解析库 |
|
||||
| release.db | ✅ 发布库 |
|
||||
| 双库隔离 | ✅ 无外键/导航属性 |
|
||||
@@ -1,16 +1,17 @@
|
||||
# 数据库迁移策略
|
||||
|
||||
> 所属模块:[数据库切换支持](./概览.md)
|
||||
> 所属模块:[数据库支持](./概览.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 迁移命令
|
||||
## 1. 本地库迁移
|
||||
|
||||
### SQLite
|
||||
|
||||
```bash
|
||||
# 添加迁移
|
||||
dotnet ef migrations add InitialCreate --output-dir Migrations/SQLite
|
||||
dotnet ef migrations add InitialCreate --output-dir Migrations/SQLite \
|
||||
--context WctMinerDbContext --project src/Gpulse.WCT.DataAnalyzer.Core
|
||||
|
||||
# 应用迁移
|
||||
dotnet ef database update
|
||||
@@ -20,42 +21,43 @@ dotnet ef database update
|
||||
|
||||
```bash
|
||||
# 切换配置后添加迁移
|
||||
dotnet ef migrations add InitialCreate --output-dir Migrations/PostgreSQL -- --environment PostgreSQL
|
||||
|
||||
# 应用迁移
|
||||
dotnet ef database update -- --environment PostgreSQL
|
||||
dotnet ef migrations add InitialCreate --output-dir Migrations/PostgreSQL \
|
||||
--context WctMinerDbContext --project src/Gpulse.WCT.DataAnalyzer.Core \
|
||||
-- --environment Production
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 切换步骤
|
||||
## 2. 发布库迁移
|
||||
|
||||
### 从 SQLite 切换到 PostgreSQL
|
||||
发布库的迁移需要使用 `ReleaseDbContext`:
|
||||
|
||||
| 步骤 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| 1 | 修改配置 | `appsettings.json` 中 `Database:Type` 改为 `postgresql` |
|
||||
| 2 | 设置连接 | 配置 `Host`, `Port`, `Name`, `User`, `Password` |
|
||||
| 3 | 创建数据库 | `CREATE DATABASE wctminer;` |
|
||||
| 4 | 应用迁移 | `dotnet ef database update` |
|
||||
| 5 | 数据迁移 | 使用工具迁移 SQLite 数据到 PostgreSQL |
|
||||
```bash
|
||||
# 添加迁移
|
||||
dotnet ef migrations add InitialCreate --output-dir Migrations/Release \
|
||||
--context ReleaseDbContext --project src/Gpulse.WCT.DataAnalyzer.Core
|
||||
|
||||
# 应用迁移
|
||||
dotnet ef database update --context ReleaseDbContext
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 配置文件示例
|
||||
|
||||
### SQLite (默认)
|
||||
### SQLite(默认双库)
|
||||
|
||||
```json
|
||||
{
|
||||
"Database": {
|
||||
"Type": "sqlite",
|
||||
"Path": "./data/database/wctminer.db"
|
||||
"LocalPath": "./data/database/local.db",
|
||||
"ReleasePath": "./data/database/release.db"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PostgreSQL
|
||||
### PostgreSQL(双库独立)
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -63,9 +65,8 @@ dotnet ef database update -- --environment PostgreSQL
|
||||
"Type": "postgresql",
|
||||
"Host": "localhost",
|
||||
"Port": 5432,
|
||||
"Name": "wctminer",
|
||||
"User": "wctminer_user",
|
||||
"Password": "${DB_PASSWORD}"
|
||||
"LocalPath": "Server=localhost;Database=wct_local",
|
||||
"ReleasePath": "Server=db-server;Database=wct_charging_parameters"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -77,5 +78,11 @@ dotnet ef database update -- --environment PostgreSQL
|
||||
推荐使用 [pgloader](https://github.com/dimitri/pgloader) 迁移 SQLite 到 PostgreSQL:
|
||||
|
||||
```bash
|
||||
pgloader ./data/database/wctminer.db postgresql://user:password@localhost:5432/wctminer
|
||||
```
|
||||
# 本地库
|
||||
pgloader ./data/database/local.db postgresql://user:password@localhost:5432/wct_local
|
||||
|
||||
# 发布库
|
||||
pgloader ./data/database/release.db postgresql://user:password@db-server:5432/wct_charging_parameters
|
||||
```
|
||||
|
||||
两个数据库独立迁移,不共享迁移历史。
|
||||
@@ -39,9 +39,9 @@
|
||||
```csharp
|
||||
// Parsers/FileNameParser.cs
|
||||
using System.Globalization;
|
||||
using WCTDataMiner.Services;
|
||||
using Gpulse.WCT.DataAnalyzer.Services;
|
||||
|
||||
namespace WCTDataMiner.Parsers;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Parsers;
|
||||
|
||||
public class FileNameParser
|
||||
{
|
||||
|
||||
@@ -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 WCTDataMiner.Models;
|
||||
public enum MatchType { None, Header, Fod, Legacy }
|
||||
|
||||
namespace WCTDataMiner.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 状态 | 含义 |
|
||||
|
||||
@@ -53,9 +53,9 @@ private static readonly Regex QfodPattern = new(
|
||||
```csharp
|
||||
// Parsers/QfodParser.cs
|
||||
using System.Text.RegularExpressions;
|
||||
using WCTDataMiner.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Models;
|
||||
|
||||
namespace WCTDataMiner.Parsers;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Parsers;
|
||||
|
||||
public class QfodParser : IParser<QfodRecord>
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
```csharp
|
||||
// Parsers/IParser.cs
|
||||
namespace WCTDataMiner.Parsers;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Parsers;
|
||||
|
||||
public interface IParser<TRecord>
|
||||
{
|
||||
|
||||
@@ -4,213 +4,217 @@
|
||||
|
||||
---
|
||||
|
||||
## 1. 完整解析流程
|
||||
## 1. 完整流程
|
||||
|
||||
```text
|
||||
log 文件
|
||||
→ parse
|
||||
→ local.db
|
||||
→ aggregate
|
||||
→ release.db
|
||||
→ export
|
||||
→ WCT-ChargingParameterDatabase.csv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 阶段一:parse → local.db
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant CLI as CLI Entry
|
||||
participant CLI as CLI
|
||||
participant PS as ParseService
|
||||
participant SS as ScenarioService
|
||||
participant FNP as FileNameParser
|
||||
participant QP as QfodParser
|
||||
participant PP as PlossParser
|
||||
participant DB as Database
|
||||
participant Log as Serilog
|
||||
participant DB as local.db
|
||||
|
||||
CLI->>PS: 扫描日志目录
|
||||
PS-->>CLI: 返回文件列表
|
||||
CLI->>PS: ParseFileAsync(filePath)
|
||||
PS->>FNP: 解析文件名
|
||||
FNP-->>PS: ScenarioInfo
|
||||
PS->>SS: GetOrCreateScenarioAsync
|
||||
SS->>DB: 维度 upsert + 场景 upsert
|
||||
SS-->>PS: TestScenario
|
||||
PS->>PS: 逐行读取文件
|
||||
|
||||
loop 每个日志文件
|
||||
CLI->>PS: ParseFileAsync(filePath)
|
||||
|
||||
PS->>FNP: 解析文件名
|
||||
FNP-->>PS: 场景信息(面板、版本、RX类型等)
|
||||
|
||||
PS->>SS: GetOrCreateScenarioAsync(场景信息)
|
||||
SS->>DB: 查询/创建维度记录
|
||||
SS->>DB: 查询/创建场景记录
|
||||
SS-->>PS: 返回 TestScenario
|
||||
|
||||
PS->>PS: 读取文件内容
|
||||
|
||||
loop 逐行遍历
|
||||
alt 是 Qfod 行
|
||||
PS->>QP: Parse(line, scenarioId)
|
||||
QP-->>PS: ParseResult
|
||||
else 是 Ploss 行
|
||||
PS->>PP: Parse(line, scenarioId)
|
||||
PP-->>PS: ParseResult
|
||||
end
|
||||
|
||||
alt 解析成功
|
||||
PS->>PS: 加入记录列表
|
||||
else 解析失败
|
||||
PS->>Log: LogError(line, error)
|
||||
end
|
||||
loop 每行
|
||||
alt Qfod 行
|
||||
PS->>QP: Parse(line)
|
||||
QP-->>PS: QfodRecord
|
||||
else Ploss 行
|
||||
PS->>PP: Parse(line)
|
||||
PP-->>PS: PlossRecord
|
||||
end
|
||||
|
||||
PS->>DB: BATCH INSERT QfodRecords
|
||||
PS->>DB: BATCH INSERT PlossRecords
|
||||
PS->>DB: UPDATE scenario (qfod_count, ploss_count)
|
||||
|
||||
PS-->>CLI: 返回 ParseReport
|
||||
end
|
||||
|
||||
CLI-->>CLI: 输出解析报告
|
||||
PS->>DB: BatchInsert QfodRecords
|
||||
PS->>DB: BatchInsert PlossRecords
|
||||
PS->>DB: UpdateScenarioStats
|
||||
PS-->>CLI: ParseReport
|
||||
```
|
||||
|
||||
**关键点:**
|
||||
|
||||
- 文件名格式: `TxPanel-TxHardware-TxSoftware-RxType-Purpose-Date-Seq.log`
|
||||
- 支持两行 Ploss 格式(header 行 + 数据行)
|
||||
- 事务批量写入,读取 `Parser:BatchSize` 配置分批
|
||||
- 解析错误记录到 Serilog,不阻断流程
|
||||
|
||||
---
|
||||
|
||||
## 3. 阶段二:aggregate → release.db
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant CLI as CLI
|
||||
participant AS as AggregationService
|
||||
participant LDB as local.db
|
||||
participant RDB as release.db
|
||||
|
||||
CLI->>AS: AggregateAsync()
|
||||
AS->>LDB: 读取所有 PlossRecord + 关联维度
|
||||
AS->>AS: 按 (车厂, 车型, 手机厂商, 型号) 分组
|
||||
AS->>AS: 按 Threshold(Field9) 分桶计算 9 个功率列
|
||||
AS->>AS: 计算 Q值/Q基值/P-Q系数
|
||||
AS->>RDB: DELETE + INSERT (事务)
|
||||
AS-->>CLI: AggregationReport
|
||||
```
|
||||
|
||||
**聚合规则:**
|
||||
|
||||
| 字段 | 来源 | 计算方式 |
|
||||
|------|------|----------|
|
||||
| 车厂 (CarFactory) | 配置映射 `Aggregation:TxPanelMappings` | 找不到映射时 fallback 为 TxPanel.Name |
|
||||
| 车型 (CarModel) | 配置映射 `Aggregation:CarModelMappings` | fallback 为 TxHardware.Version |
|
||||
| 手机厂商 (PhoneBrand) | RxType.Name 拆分 | 取第一个分隔符前部分 |
|
||||
| 型号 (PhoneModel) | RxType.Name 拆分 | 取第一个分隔符后部分 |
|
||||
| 350mW~2250mW(9列) | PlossRecord.Field7 | 按 Threshold(Field9) 分桶(记录阈值=档位),取 Field7 平均值 |
|
||||
| Q值 (QValue) | QfodRecord.CurrentQ | 平均值 |
|
||||
| Q基值 (QBaseValue) | QfodRecord.RawQ | 平均值 |
|
||||
| P-Q值系数 (PqCoefficient) | PlossRecord.DeltaP | 平均值 |
|
||||
| 谐振频率 (ResonanceFrequency) | 暂无 | 允许为空 |
|
||||
|
||||
> 各列的详细计算规则(含公式、功率匹配与空值逻辑)见 [§5 输出 CSV 各列计算详解](#5-输出-csv-各列计算详解)。
|
||||
|
||||
**替代配置:**
|
||||
|
||||
配置文件 `appsettings.json` 中可添加映射:
|
||||
|
||||
```json
|
||||
{
|
||||
"Aggregation": {
|
||||
"TxPanelMappings": {
|
||||
"single-mold": "奇瑞",
|
||||
"dual-rapid": "智己"
|
||||
},
|
||||
"CarModelMappings": {
|
||||
"L6": "L6",
|
||||
"CM3": "CM3"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. ParseService 实现
|
||||
## 4. 阶段三:export → CSV
|
||||
|
||||
```csharp
|
||||
// Services/ParseService.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
using WCTDataMiner.Data;
|
||||
using WCTDataMiner.Models;
|
||||
using WCTDataMiner.Parsers;
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant CLI as CLI
|
||||
participant ES as ExportService
|
||||
participant RDB as release.db
|
||||
participant FS as Filesystem
|
||||
|
||||
namespace WCTDataMiner.Services;
|
||||
CLI->>ES: ExportChargingParametersToCsvAsync(outputDir)
|
||||
ES->>RDB: 读取 ChargingParameters (排序)
|
||||
ES->>FS: 写入 WCT-ChargingParameterDatabase.csv
|
||||
ES-->>CLI: 文件路径
|
||||
```
|
||||
|
||||
public class ParseService
|
||||
{
|
||||
private readonly WctMinerDbContext _context;
|
||||
private readonly ScenarioService _scenarioService;
|
||||
private readonly QfodParser _qfodParser;
|
||||
private readonly PlossParser _plossParser;
|
||||
private readonly FileNameParser _fileNameParser;
|
||||
private readonly int _batchSize;
|
||||
**导出规格:**
|
||||
|
||||
public ParseService(
|
||||
WctMinerDbContext context,
|
||||
ScenarioService scenarioService,
|
||||
QfodParser qfodParser,
|
||||
PlossParser plossParser,
|
||||
FileNameParser fileNameParser,
|
||||
IConfiguration config)
|
||||
{
|
||||
_context = context;
|
||||
_scenarioService = scenarioService;
|
||||
_qfodParser = qfodParser;
|
||||
_plossParser = plossParser;
|
||||
_fileNameParser = fileNameParser;
|
||||
_batchSize = config.GetValue("Parser:BatchSize", 1000);
|
||||
}
|
||||
- 固定文件名: `WCT-ChargingParameterDatabase.csv`
|
||||
- 固定 17 列 UTF-8 BOM
|
||||
- RFC 4180 转义
|
||||
- 数字使用 invariant culture
|
||||
|
||||
public async Task<ParseReport> ParseFileAsync(string filePath)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
---
|
||||
|
||||
// 解析文件名获取场景信息
|
||||
var scenarioInfo = _fileNameParser.Parse(fileInfo.Name);
|
||||
if (scenarioInfo == null)
|
||||
{
|
||||
Log.Error("无法解析文件名: {FileName}", fileInfo.Name);
|
||||
return new ParseReport(fileInfo.Name, 0, 0, 1, "文件名格式不匹配");
|
||||
}
|
||||
## 5. 输出 CSV 各列计算详解
|
||||
|
||||
// 获取或创建测试场景
|
||||
var scenario = await _scenarioService.GetOrCreateScenarioAsync(scenarioInfo);
|
||||
> 最终 `WCT-ChargingParameterDatabase.csv` 的每一列都在 aggregate 阶段由 `AggregationService`
|
||||
> 计算写入 release.db,export 阶段仅按 (车厂, 车型, 手机厂商, 型号) 排序后原样写出。
|
||||
> 所有数值列均按同一个分组键聚合;release.db 中每条记录对应 CSV 中的一行。
|
||||
|
||||
var lines = await File.ReadAllLinesAsync(filePath);
|
||||
var qfodRecords = new List<QfodRecord>();
|
||||
var plossRecords = new List<PlossRecord>();
|
||||
int errorCount = 0;
|
||||
### 5.1 分组键(一行 = 一个分组)
|
||||
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var line = lines[i];
|
||||
var lineNumber = i + 1;
|
||||
| CSV 列 | 计算方式 |
|
||||
|--------|----------|
|
||||
| 车厂 | `Aggregation:TxPanelMappings` 映射 TxPanel.Name;未配置映射时 fallback 为 TxPanel.Name |
|
||||
| 车型 | `Aggregation:CarModelMappings` 映射 TxHardware.Version;未配置映射时 fallback 为 TxHardware.Version |
|
||||
| 手机厂商 | RxType.Name 按第一个分隔符(`/`、`_`、`:`)拆分的**前半部分** |
|
||||
| 型号 | 分隔符的**后半部分** |
|
||||
|
||||
if (_qfodParser.CanParse(line))
|
||||
{
|
||||
var result = _qfodParser.Parse(line, scenario.Id);
|
||||
if (result.IsSuccess)
|
||||
qfodRecords.Add(result.Record!);
|
||||
else
|
||||
{
|
||||
Log.Error("Qfod解析失败 [{Line}] {Error}: {Message}",
|
||||
lineNumber, result.ErrorType, result.ErrorMessage);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
else if (_plossParser.CanParse(line))
|
||||
{
|
||||
var result = _plossParser.Parse(line, scenario.Id);
|
||||
if (result.IsSuccess)
|
||||
plossRecords.Add(result.Record!);
|
||||
else
|
||||
{
|
||||
Log.Error("Ploss解析失败 [{Line}] {Error}: {Message}",
|
||||
lineNumber, result.ErrorType, result.ErrorMessage);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
- RxType.Name 无分隔符时:手机厂商固定为 `Unknown`,型号取整个 RxType.Name。
|
||||
- **分组基于 Ploss 记录**:只有 Qfod 记录、没有任何 Ploss 记录的场景不会产生发布记录,
|
||||
因而不会出现在 CSV 中。
|
||||
|
||||
// 批量保存
|
||||
await BatchInsertAsync(qfodRecords, plossRecords);
|
||||
### 5.2 功率列(350mW / 500mW / 750mW / 1000mW / 1250mW / 1500mW / 1750mW / 2000mW / 2250mW)
|
||||
|
||||
// 更新场景统计
|
||||
await UpdateScenarioStatsAsync(scenario.Id, qfodRecords.Count, plossRecords.Count);
|
||||
- **语义**:列头即"允许的最大损耗限值"(安全红线)档位,对应需求文档 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` 为负数、非阈值)。解析器按原始位置逐字段存储,字段语义以实体注释与实测数据为准。
|
||||
|
||||
return new ParseReport(fileInfo.Name, qfodRecords.Count, plossRecords.Count, errorCount, null);
|
||||
}
|
||||
### 5.3 Q值 / Q基值
|
||||
|
||||
private async Task BatchInsertAsync(
|
||||
List<QfodRecord> qfodRecords,
|
||||
List<PlossRecord> plossRecords)
|
||||
{
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
- **来源字段**:Qfod 记录的 `CurrentQ` / `RawQ`。
|
||||
- **分场景预聚合**:每个测试场景先对 Qfod 记录求 `ΣCurrentQ`、`ΣRawQ`、`Count`(数据库端完成)。
|
||||
- **组内加权累加**:分组内每一条 Ploss 记录,按其所属场景把该场景的预聚合值累加一次。
|
||||
同一场景出现 k 条 Ploss 记录时,其 Sum 与 Count 同倍放大,商保持不变,
|
||||
等效于将该场景全部 Qfod 明细拉平后求平均。
|
||||
- **结果**:Q值 = ΣCurrentQ / ΣCount;Q基值 = ΣRawQ / ΣCount。
|
||||
- **空值**:分组内没有任何 Qfod 记录时,两列均留空。
|
||||
|
||||
try
|
||||
{
|
||||
// 批量插入 QfodRecord
|
||||
foreach (var batch in qfodRecords.Chunk(_batchSize))
|
||||
{
|
||||
_context.QfodRecords.AddRange(batch);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
### 5.4 P-Q值系数
|
||||
|
||||
// 批量插入 PlossRecord
|
||||
foreach (var batch in plossRecords.Chunk(_batchSize))
|
||||
{
|
||||
_context.PlossRecords.AddRange(batch);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
- **来源字段**:Ploss 两行格式第一行的 `delta_p`(旧单行格式无此字段,为 null)。
|
||||
- **聚合**:分组内所有 Ploss 记录 `DeltaP` 的平均值(忽略空值)。
|
||||
- **空值**:分组内 `DeltaP` 全部为空时留空。
|
||||
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
### 5.5 谐振频率
|
||||
|
||||
private async Task UpdateScenarioStatsAsync(Guid scenarioId, int qfodCount, int plossCount)
|
||||
{
|
||||
// 使用累加更新(+=),支持同一场景的多次解析
|
||||
// 若需幂等解析,应在 ParseFileAsync 开始时检查场景是否已处理
|
||||
var scenario = await _context.TestScenarios.FindAsync(scenarioId);
|
||||
if (scenario != null)
|
||||
{
|
||||
scenario.QfodCount += qfodCount;
|
||||
scenario.PlossCount += plossCount;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
- 当前版本固定为 `null`,**未实现计算**,导出时该列恒为空。
|
||||
|
||||
public record ParseReport(string FileName, int QfodCount, int PlossCount, int ErrorCount, string? ErrorMessage);
|
||||
### 5.6 数值输出格式
|
||||
|
||||
public record ScenarioInfo(
|
||||
string TxPanel,
|
||||
string TxHardware,
|
||||
string TxSoftware,
|
||||
string RxType,
|
||||
string? TestPurpose,
|
||||
DateOnly TestDate,
|
||||
int TestSequence);
|
||||
```
|
||||
- 数字使用 invariant culture 输出 double 全精度(如 `2531.5737704918033`),不四舍五入。
|
||||
- CSV 为 UTF-8 带 BOM,RFC 4180 转义。
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据库隔离
|
||||
|
||||
```text
|
||||
local.db release.db
|
||||
───────── ──────────
|
||||
tx_panel charging_parameter
|
||||
tx_hardware (独立实体,无外键)
|
||||
tx_software
|
||||
rx_type
|
||||
test_scenario
|
||||
qfod_record
|
||||
ploss_record
|
||||
|
||||
无关联 ←→ 无关联
|
||||
```
|
||||
|
||||
- 代码中不存在 local entity 到 release entity 的导航属性
|
||||
- 数据库中没有跨库外键
|
||||
- export 不读 local.db,aggregate 不写 local.db
|
||||
@@ -1,68 +1,147 @@
|
||||
# Architecture: WCTDataMiner
|
||||
# Architecture: Gpulse.WCT.DataAnalyzer
|
||||
|
||||
> 无线充电 FOD(异物检测)日志数据采集系统,从日志文件中解析 Qfod 和 Ploss 数据并存储到数据库
|
||||
> 无线充电参数采集系统。从 log 文件解析 Qfod/Ploss 数据 → 本地分析库 → 汇总生成正式充电参数数据库 → 导出 CSV。
|
||||
|
||||
---
|
||||
|
||||
## 数据流
|
||||
|
||||
```text
|
||||
log 文件
|
||||
→ parse
|
||||
→ local.db(本地解析库)
|
||||
→ aggregate
|
||||
→ release.db(正式发布库)
|
||||
→ export
|
||||
→ WCT-ChargingParameterDatabase.csv
|
||||
```
|
||||
|
||||
| 阶段 | 命令 | 存储 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 解析 | `parse` | `local.db` | 本地日志解析库,保存原始 Qfod/Ploss 记录 |
|
||||
| 汇总 | `aggregate` | `release.db` | 从 local.db 汇总生成正式充电参数记录 |
|
||||
| 导出 | `export` | CSV | 读取 release.db,输出固定格式 CSV |
|
||||
|
||||
## 文档索引
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [技术选型](./技术选型.md) | 技术栈总览、决策记录 |
|
||||
| [模块划分](./模块划分.md) | 模块总览、分层架构、依赖关系 |
|
||||
| [安全架构](./安全架构.md) | 认证方案、数据安全 |
|
||||
| [解析器设计](./parser/概览.md) | Qfod/Ploss 解析器详细设计 |
|
||||
| [数据处理流程](./数据处理流程.md) | 解析流程、批量写入、错误处理 |
|
||||
| [配置管理](./配置管理.md) | 配置文件、配置类定义 |
|
||||
| [CLI 命令设计](./CLI命令设计.md) | 命令行接口定义 |
|
||||
| [数据库切换支持](./database/概览.md) | SQLite/PostgreSQL 切换方案 |
|
||||
| [技术选型](./技术选型.md) | 技术栈、决策记录 |
|
||||
| [模块划分](./模块划分.md) | 分层架构、目录结构、模块职责 |
|
||||
| [数据处理流程](./数据处理流程.md) | 解析→汇总→导出完整流程 |
|
||||
| [CLI 命令设计](./CLI命令设计.md) | 命令行接口 |
|
||||
| [配置管理](./配置管理.md) | 配置结构与双数据库配置 |
|
||||
| [数据库支持](./database/概览.md) | 本地/发布双数据库架构 |
|
||||
| [安全架构](./安全架构.md) | 路径校验、安全策略 |
|
||||
| [解析器设计](./parser/概览.md) | Qfod/Ploss 解析器 |
|
||||
|
||||
---
|
||||
|
||||
## 架构图
|
||||
## 架构分层
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ CLI Entry Point │
|
||||
│ (Program.cs / System.CommandLine) │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ Application Layer │
|
||||
│ ┌───────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐ │
|
||||
│ │ LocalIngestion│ │ Publishing │ │ Exporting │ │Administration│ │
|
||||
│ │ ParseService │ │AggregationSvc│ │ExportSvc │ │ CleanService │ │
|
||||
│ │ ScenarioSvc │ │ThresholdCalc │ │ │ │ CleanService │ │
|
||||
│ │ DimensionSvc │ │ │ │ │ │ │ │
|
||||
│ └───────────────┘ └──────────────┘ └────────────┘ └──────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Parsing │ │
|
||||
│ │ QfodParser │ PlossParser │ FileNameParser │ │
|
||||
│ └──────────────────────────────────────────────────────────────────┘ │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ Domain Layer │
|
||||
│ ┌─────────────────────────┐ ┌──────────────────────────────────────┐ │
|
||||
│ │ Local/ (本地实体) │ │ Release/ (发布实体) │ │
|
||||
│ │ TxPanel, TxHardware │ │ ChargingParameterRecord │ │
|
||||
│ │ TxSoftware, RxType │ │ │ │
|
||||
│ │ TestScenario │ │ │ │
|
||||
│ │ QfodRecord, PlossRecord │ │ │ │
|
||||
│ └─────────────────────────┘ └──────────────────────────────────────┘ │
|
||||
├──────────────────────────────────────────────────────────────────────┤
|
||||
│ Infrastructure Layer │
|
||||
│ ┌───────────────┐ ┌──────────────┐ ┌────────────┐ ┌──────────────┐ │
|
||||
│ │ LocalData │ │ ReleaseData │ │Configuration│ │ Security │ │
|
||||
│ │ WctMinerDbCtx │ │ReleaseDbCtx │ │AppSettings │ │PathValidator │ │
|
||||
│ │ DbCtxFactory │ │ReleaseDbCtxF │ │ │ │ │ │
|
||||
│ │ SeedData │ │ │ │ │ │ │ │
|
||||
│ └───────────────┘ └──────────────┘ └────────────┘ └──────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CLI Entry Point │
|
||||
│ (Program.cs / System.CommandLine) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Service Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ParseService │ ErrorHandlingService │ ThresholdCalc │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Parser Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ QfodParser │ PlossParser │ LogFileReader │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Data Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Models │ WctMinerDbContext │ IDbContextFactory │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Infrastructure │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Configuration │ Serilog │ AppSettings │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心设计原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| 技术栈 | .NET 8.0 LTS + C# 12.0 + EF Core 8.0 |
|
||||
| 数据库 | SQLite(当前)/ PostgreSQL(后续),配置切换 |
|
||||
| 解析模式 | 批处理,正则表达式匹配 |
|
||||
| ORM 模式 | Code First,支持迁移自动化 |
|
||||
| CLI 框架 | System.CommandLine,命令模式 |
|
||||
| 技术栈 | .NET 8.0 + C# 12.0 + EF Core 8.0 |
|
||||
| 数据库 | local.db / release.db 两个独立 SQLite 数据库 |
|
||||
| 本地/发布隔离 | 无外键、无导航属性、无数据库级关联 |
|
||||
| 解析模式 | 批处理,正则表达式 |
|
||||
| CLI | System.CommandLine,命令模式 |
|
||||
| 汇总规则 | 按业务键分组取平均值,缺失字段允许为空 |
|
||||
|
||||
---
|
||||
## 目录结构
|
||||
|
||||
## 假设/歧义标注
|
||||
|
||||
| 标注 | 内容 |
|
||||
|------|------|
|
||||
| [A] | 使用 .NET 8.0 LTS 版本 |
|
||||
| [A] | 初期使用 SQLite,架构支持切换 PostgreSQL |
|
||||
| [?] | 增量解析策略需确认 |
|
||||
| [?] | 多次解析同一文件的处理策略需确认 |
|
||||
```text
|
||||
src/
|
||||
├── Gpulse.WCT.DataAnalyzer/ # CLI 入口
|
||||
│ ├── Commands/ # 命令实现
|
||||
│ │ ├── ParseCommand.cs
|
||||
│ │ ├── AggregateCommand.cs
|
||||
│ │ ├── ExportCommand.cs
|
||||
│ │ └── CleanCommand.cs
|
||||
│ ├── Program.cs
|
||||
│ └── appsettings.json
|
||||
│
|
||||
└── Gpulse.WCT.DataAnalyzer.Core/ # 核心库
|
||||
├── Application/ # 应用层
|
||||
│ ├── Parsing/ # 解析器
|
||||
│ │ ├── IParser.cs
|
||||
│ │ ├── QfodParser.cs
|
||||
│ │ ├── PlossParser.cs
|
||||
│ │ └── FileNameParser.cs
|
||||
│ ├── LocalIngestion/ # 本地日志解析
|
||||
│ │ ├── ParseService.cs
|
||||
│ │ ├── ScenarioService.cs
|
||||
│ │ └── DimensionService.cs
|
||||
│ ├── Publishing/ # 发布汇总
|
||||
│ │ ├── AggregationService.cs
|
||||
│ │ └── ThresholdCalculator.cs
|
||||
│ ├── Exporting/ # 导出
|
||||
│ │ └── ExportService.cs
|
||||
│ └── Administration/ # 管理
|
||||
│ └── CleanService.cs
|
||||
├── Domain/ # 领域实体
|
||||
│ └── Models/
|
||||
│ ├── TxPanel.cs # 本地-维度
|
||||
│ ├── TxHardware.cs
|
||||
│ ├── TxSoftware.cs
|
||||
│ ├── RxType.cs
|
||||
│ ├── TestScenario.cs
|
||||
│ ├── QfodRecord.cs
|
||||
│ ├── PlossRecord.cs
|
||||
│ └── ChargingParameterRecord.cs # 发布-实体
|
||||
└── Infrastructure/ # 基础设施
|
||||
├── LocalData/ # 本地数据库
|
||||
│ ├── WctMinerDbContext.cs
|
||||
│ ├── DbContextFactory.cs
|
||||
│ ├── IDbContextFactory.cs
|
||||
│ ├── SeedData.cs
|
||||
│ └── Configurations/
|
||||
├── ReleaseData/ # 发布数据库
|
||||
│ ├── ReleaseDbContext.cs
|
||||
│ ├── ReleaseDbContextFactory.cs
|
||||
│ ├── IReleaseDbContextFactory.cs
|
||||
│ └── Configurations/
|
||||
├── Configuration/ # 配置
|
||||
│ └── AppSettings.cs
|
||||
├── Security/ # 安全
|
||||
│ ├── PathValidator.cs
|
||||
│ └── SecurityConstants.cs
|
||||
└── Extensions/ # DI 扩展
|
||||
└── ServiceCollectionExtensions.cs
|
||||
```
|
||||
@@ -4,167 +4,171 @@
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块总览
|
||||
## 1. 分层架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CLI Entry Point │
|
||||
│ (Program.cs / System.CommandLine) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Service Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ParseService │ ScenarioService │ DimensionService │ ThresholdCalc │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Parser Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ QfodParser │ PlossParser │ FileNameParser │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Data Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Models │ WctMinerDbContext │ IDbContextFactory │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Infrastructure │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Configuration │ Serilog │ AppSettings │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
| 层级 | 目录 | 职责 | 依赖 |
|
||||
|------|------|------|------|
|
||||
| CLI | `Gpulse.WCT.DataAnalyzer/Commands/` | 接收参数、流程调度、结果输出 | Application |
|
||||
| Application | `Core/Application/` | 业务流程编排 | Domain, Infrastructure |
|
||||
| Domain | `Core/Domain/Models/` | 实体定义,无行为 | 无 |
|
||||
| Infrastructure | `Core/Infrastructure/` | 数据持久化、配置、安全 | Domain |
|
||||
|
||||
**依赖规则:**
|
||||
|
||||
- 上层可依赖下层,下层不可依赖上层
|
||||
- Application 依 Domain 和 Infrastructure
|
||||
- Infrastructure 依 Domain
|
||||
- Domain 不依赖任何其他层
|
||||
- 解析器之间不可互相调用
|
||||
|
||||
---
|
||||
|
||||
## 2. 模块职责
|
||||
## 2. Application 模块职责
|
||||
|
||||
| 模块名 | 职责 | 依赖模块 |
|
||||
|--------|------|----------|
|
||||
| CLI Entry | 接收命令行参数、调度解析流程 | ParseService, Infrastructure |
|
||||
| ParseService | 协调解析流程、批量写入、场景关联 | Parser Layer, ScenarioService, DbContext |
|
||||
| ScenarioService | 创建/查询测试场景,解析文件名获取场景信息 | DimensionService, DbContext |
|
||||
| DimensionService | 维度表 CRUD(获取或创建维度记录) | DbContext |
|
||||
| FileNameParser | 解析文件名提取场景信息(面板、版本、RX类型等) | 无 |
|
||||
| LogFileReader | 读取日志文件、行过滤、文件遍历 | 无 |
|
||||
| QfodParser | 解析 Qfod 格式日志,返回 QfodRecord | 无 |
|
||||
| PlossParser | 解析 Ploss 格式日志,返回 PlossRecord | 无 |
|
||||
| Models | 定义数据实体结构(维度表 + 事实表) | 无 |
|
||||
| WctMinerDbContext | EF Core 数据库上下文 | Models, Configurations |
|
||||
| Configuration | 加载配置文件 | 无 |
|
||||
| Serilog | 结构化日志输出(含解析错误日志) | 无 |
|
||||
| ThresholdCalculator | 根据 RxPower 计算动态阈值 | 无 |
|
||||
### 2.1 Parsing — 解析
|
||||
|
||||
| 类 | 职责 |
|
||||
|----|------|
|
||||
| `FileNameParser` | 从文件名提取场景信息(面板、硬件、软件、RX类型、日期、序号) |
|
||||
| `QfodParser` | 解析 Qfod 格式日志行 → `QfodRecord` |
|
||||
| `PlossParser` | 解析 Ploss 单行/两行格式日志 → `PlossRecord` |
|
||||
|
||||
### 2.2 LocalIngestion — 本地解析
|
||||
|
||||
| 类 | 职责 |
|
||||
|----|------|
|
||||
| `ParseService` | 协调解析流程:读取文件 → 解析 → 批量写入 local.db |
|
||||
| `ScenarioService` | 创建/查询测试场景,并发安全 upsert |
|
||||
| `DimensionService` | 维度表 get-or-create |
|
||||
|
||||
### 2.3 Publishing — 发布汇总
|
||||
|
||||
| 类 | 职责 |
|
||||
|----|------|
|
||||
| `AggregationService` | 读取 local.db → 按业务键分组计算 → upsert 到 release.db |
|
||||
| `ThresholdCalculator` | 动态阈值计算 |
|
||||
|
||||
### 2.4 Exporting — 导出
|
||||
|
||||
| 类 | 职责 |
|
||||
|----|------|
|
||||
| `ExportService` | 导出 release.db 到固定 CSV,也支持 legacy Qfod/Ploss 导出 |
|
||||
|
||||
### 2.5 Administration — 管理
|
||||
|
||||
| 类 | 职责 |
|
||||
|----|------|
|
||||
| `CleanService` | 软删除管理 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 分层架构
|
||||
## 3. Domain 实体
|
||||
|
||||
### 3.1 分层定义
|
||||
### 3.1 本地实体(local.db)
|
||||
|
||||
| 层级 | 职责 | 允许调用 |
|
||||
|------|------|----------|
|
||||
| CLI Entry | 接收参数、流程调度、结果输出 | Service, Infrastructure |
|
||||
| Service | 协调解析流程、场景管理、维度管理、批量写入 | Parser, Data, Infrastructure |
|
||||
| Parser | 日志解析、文件名解析、数据提取 | 无 |
|
||||
| Data | 数据持久化、DbContext 管理、实体配置 | Infrastructure |
|
||||
| Infrastructure | 配置、日志 | 无 |
|
||||
| 实体 | 表 | 说明 |
|
||||
|------|----|------|
|
||||
| `TxPanel` | tx_panel | TX 面板类型维度 |
|
||||
| `TxHardware` | tx_hardware | TX 硬件版本维度 |
|
||||
| `TxSoftware` | tx_software | TX 软件版本维度 |
|
||||
| `RxType` | rx_type | RX 类型维度 |
|
||||
| `TestScenario` | test_scenario | 测试场景(4 个维度外键 + 日期 + 序号) |
|
||||
| `QfodRecord` | qfod_record | Qfod 检测数据 |
|
||||
| `PlossRecord` | ploss_record | Ploss FOD 数据 |
|
||||
|
||||
### 3.2 依赖规则
|
||||
关系:维度表 1:N TestScenario 1:N QfodRecord/PlossRecord(星型模型)
|
||||
|
||||
- 上层可调用下层
|
||||
- 下层不可调用上层
|
||||
- Parser 之间不可互相调用(独立解析器)
|
||||
- Service 层协调 Parser 和 Data 层
|
||||
- 解析错误通过 Serilog 记录,不存入业务数据库
|
||||
### 3.2 发布实体(release.db)
|
||||
|
||||
| 实体 | 表 | 唯一键 |
|
||||
|------|----|--------|
|
||||
| `ChargingParameterRecord` | charging_parameter | (car_factory, car_model, phone_brand, phone_model) |
|
||||
|
||||
**两个数据库不建立外键或导航属性关联。**
|
||||
|
||||
---
|
||||
|
||||
## 4. 模块依赖图
|
||||
## 4. Infrastructure 模块
|
||||
|
||||
### 4.1 LocalData — 本地数据库
|
||||
|
||||
| 类 | 职责 |
|
||||
|----|------|
|
||||
| `WctMinerDbContext` | 本地解析库 EF Core 上下文 |
|
||||
| `DbContextFactory` | 根据配置创建 SQLite/PostgreSQL 本地上下文 |
|
||||
| `IDbContextFactory` | 工厂接口 |
|
||||
| `SeedData` | 默认维度数据初始化 |
|
||||
| `Configurations/*` | EF Core `IEntityTypeConfiguration` 实现 |
|
||||
|
||||
### 4.2 ReleaseData — 发布数据库
|
||||
|
||||
| 类 | 职责 |
|
||||
|----|------|
|
||||
| `ReleaseDbContext` | 发布库 EF Core 上下文 |
|
||||
| `ReleaseDbContextFactory` | 创建发布库上下文 |
|
||||
| `IReleaseDbContextFactory` | 工厂接口 |
|
||||
| `Configurations/*` | 发布实体配置 |
|
||||
|
||||
### 4.3 Configuration
|
||||
|
||||
| 类 | 职责 |
|
||||
|----|------|
|
||||
| `AppSettings` | 应用配置 POCO |
|
||||
|
||||
### 4.4 Security
|
||||
|
||||
| 类 | 职责 |
|
||||
|----|------|
|
||||
| `PathValidator` | 路径安全校验(遍历保护、白名单、应用目录限制) |
|
||||
| `SecurityConstants` | 安全常量 |
|
||||
|
||||
### 4.5 Extensions
|
||||
|
||||
| 类 | 职责 |
|
||||
|----|------|
|
||||
| `ServiceCollectionExtensions` | `AddDatabaseServices` / `AddApplicationServices` DI 注册 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 依赖图
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
CLI[CLI Entry] --> PS[ParseService]
|
||||
CLI --> Serilog
|
||||
CLI[CLI Commands] --> Parsing[Parsing]
|
||||
CLI --> LocalIngestion[LocalIngestion]
|
||||
CLI --> Publishing[Publishing]
|
||||
CLI --> Exporting[Exporting]
|
||||
CLI --> Admin[Administration]
|
||||
|
||||
PS --> SS[ScenarioService]
|
||||
PS --> QP[QfodParser]
|
||||
PS --> PP[PlossParser]
|
||||
PS --> DBM[WctMinerDbContext]
|
||||
PS --> Serilog
|
||||
LocalIngestion --> Parsing
|
||||
LocalIngestion --> LocalData[LocalData]
|
||||
Publishing --> LocalData
|
||||
Publishing --> ReleaseData[ReleaseData]
|
||||
Exporting --> LocalData
|
||||
Exporting --> ReleaseData
|
||||
Admin --> LocalData
|
||||
|
||||
SS --> DS[DimensionService]
|
||||
SS --> FNP[FileNameParser]
|
||||
SS --> DBM
|
||||
LocalData --> Domain[Domain Models]
|
||||
ReleaseData --> Domain
|
||||
Parsing --> Domain
|
||||
|
||||
DS --> DBM
|
||||
|
||||
QP --> Serilog
|
||||
PP --> Serilog
|
||||
FNP --> Serilog
|
||||
|
||||
DBM --> Models[Models]
|
||||
DBM --> Configs[Configurations]
|
||||
|
||||
subgraph Parser Layer
|
||||
QP
|
||||
PP
|
||||
FNP
|
||||
subgraph Application
|
||||
Parsing
|
||||
LocalIngestion
|
||||
Publishing
|
||||
Exporting
|
||||
Admin
|
||||
end
|
||||
|
||||
subgraph Data Layer
|
||||
DBM
|
||||
Models
|
||||
Configs
|
||||
subgraph Domain
|
||||
Domain
|
||||
end
|
||||
|
||||
subgraph Infrastructure
|
||||
Serilog
|
||||
LocalData
|
||||
ReleaseData
|
||||
Configuration
|
||||
Security
|
||||
Extensions
|
||||
end
|
||||
|
||||
subgraph Service Layer
|
||||
PS
|
||||
SS
|
||||
DS
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 目录结构
|
||||
|
||||
```
|
||||
src/WCTDataMiner/
|
||||
├── Parsers/
|
||||
│ ├── IParser.cs
|
||||
│ ├── QfodParser.cs
|
||||
│ ├── PlossParser.cs
|
||||
│ ├── FileNameParser.cs # 解析文件名获取场景信息(Parser Layer)
|
||||
│ └── LogFileReader.cs # 读取日志文件(可选)
|
||||
├── Models/
|
||||
│ ├── TxPanel.cs # TX面板维度
|
||||
│ ├── TxHardware.cs # TX硬件版本维度
|
||||
│ ├── TxSoftware.cs # TX软件版本维度
|
||||
│ ├── RxType.cs # RX类型维度
|
||||
│ ├── TestScenario.cs # 测试场景
|
||||
│ ├── QfodRecord.cs # Qfod记录
|
||||
│ └── PlossRecord.cs # Ploss记录
|
||||
├── Data/
|
||||
│ ├── WctMinerDbContext.cs
|
||||
│ ├── IDbContextFactory.cs
|
||||
│ └── Configurations/ # IEntityTypeConfiguration 配置类
|
||||
│ ├── TxPanelConfiguration.cs
|
||||
│ ├── TxHardwareConfiguration.cs
|
||||
│ ├── TxSoftwareConfiguration.cs
|
||||
│ ├── RxTypeConfiguration.cs
|
||||
│ ├── TestScenarioConfiguration.cs
|
||||
│ ├── QfodRecordConfiguration.cs
|
||||
│ └── PlossRecordConfiguration.cs
|
||||
├── Services/
|
||||
│ ├── ParseService.cs # 解析流程协调
|
||||
│ ├── ScenarioService.cs # 场景创建/查询
|
||||
│ ├── DimensionService.cs # 维度表管理
|
||||
│ └── ThresholdCalculator.cs # 阈值计算
|
||||
├── Configuration/
|
||||
│ ├── AppSettings.cs
|
||||
│ └── appsettings.json
|
||||
├── Commands/
|
||||
│ ├── ParseCommand.cs
|
||||
│ ├── StatsCommand.cs
|
||||
│ ├── ExportCommand.cs
|
||||
│ └── CleanCommand.cs
|
||||
└── Program.cs
|
||||
```
|
||||
@@ -4,27 +4,31 @@
|
||||
|
||||
---
|
||||
|
||||
## 1. 配置文件结构
|
||||
## 1. 配置文件
|
||||
|
||||
```json
|
||||
// Configuration/appsettings.json
|
||||
{
|
||||
"Database": {
|
||||
"Type": "sqlite",
|
||||
"Path": "./data/database/wctminer.db"
|
||||
"LocalPath": "./data/database/local.db",
|
||||
"ReleasePath": "./data/database/release.db"
|
||||
},
|
||||
"Parser": {
|
||||
"BatchSize": 1000
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"WCTDataMiner": "Debug"
|
||||
}
|
||||
},
|
||||
"Paths": {
|
||||
"LogDir": "./data/logs",
|
||||
"OutputDir": "./data/output"
|
||||
},
|
||||
"Aggregation": {
|
||||
"TxPanelMappings": {
|
||||
"single-mold": "奇瑞",
|
||||
"dual-rapid": "智己"
|
||||
},
|
||||
"CarModelMappings": {
|
||||
"L6": "L6",
|
||||
"CM3": "CM3"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -34,8 +38,8 @@
|
||||
## 2. 配置类定义
|
||||
|
||||
```csharp
|
||||
// Configuration/AppSettings.cs
|
||||
namespace WCTDataMiner.Configuration;
|
||||
// Infrastructure/Configuration/AppSettings.cs
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Configuration;
|
||||
|
||||
public class AppSettings
|
||||
{
|
||||
@@ -47,8 +51,9 @@ public class AppSettings
|
||||
public class DatabaseSettings
|
||||
{
|
||||
public string Type { get; set; } = "sqlite";
|
||||
public string Path { get; set; } = "./data/database/wctminer.db";
|
||||
|
||||
public string LocalPath { get; set; } = "./data/database/local.db";
|
||||
public string ReleasePath { get; set; } = "./data/database/release.db";
|
||||
|
||||
// PostgreSQL 配置(可选)
|
||||
public string? Host { get; set; }
|
||||
public int Port { get; set; } = 5432;
|
||||
@@ -71,35 +76,51 @@ public class PathSettings
|
||||
|
||||
---
|
||||
|
||||
## 3. 配置加载
|
||||
## 3. 数据库配置说明
|
||||
|
||||
```csharp
|
||||
// Program.cs 配置加载部分
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using WCTDataMiner.Configuration;
|
||||
### 双数据库路径
|
||||
|
||||
var builder = Host.CreateDefaultBuilder(args);
|
||||
| 配置键 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `Database:LocalPath` | `./data/database/local.db` | 本地解析库 |
|
||||
| `Database:ReleasePath` | `./data/database/release.db` | 正式发布库 |
|
||||
|
||||
builder.ConfigureAppConfiguration(config =>
|
||||
两个数据库独立配置,可以位于不同路径,也可以使用不同类型的数据库。
|
||||
|
||||
### 兼容旧配置
|
||||
|
||||
`Database:Path` 仍然作为 local 数据库的 fallback 路径,推荐使用 `Database:LocalPath`。
|
||||
|
||||
### PostgreSQL 支持
|
||||
|
||||
当 `Database:Type` 设为 `postgresql` 时,local 和 release 都可以使用独立的 PostgreSQL 连接:
|
||||
|
||||
```json
|
||||
{
|
||||
config.AddJsonFile("appsettings.json", optional: false);
|
||||
config.AddJsonFile($"appsettings.{environment}.json", optional: true);
|
||||
config.AddEnvironmentVariables();
|
||||
config.AddCommandLine(args);
|
||||
});
|
||||
"Database": {
|
||||
"Type": "postgresql",
|
||||
"Host": "localhost",
|
||||
"Port": 5432,
|
||||
"Name": "wct_local",
|
||||
"User": "wct_user",
|
||||
"Password": "...",
|
||||
"ReleaseHost": "db-server",
|
||||
"ReleaseName": "wct_charging_parameters",
|
||||
"ReleaseUser": "wct_release",
|
||||
"ReleasePassword": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
builder.ConfigureServices((context, services) =>
|
||||
{
|
||||
services.Configure<AppSettings>(context.Configuration.GetSection(""));
|
||||
|
||||
// 注册数据库服务
|
||||
services.AddDatabaseServices(context.Configuration);
|
||||
|
||||
// 注册其他服务
|
||||
services.AddScoped<QfodParser>();
|
||||
services.AddScoped<PlossParser>();
|
||||
services.AddScoped<ParseService>();
|
||||
services.AddScoped<ErrorHandlingService>();
|
||||
});
|
||||
```
|
||||
Release 数据库的 PostgreSQL 配置前缀为 `Release*`,未设置时回退到 local 的对应配置。
|
||||
|
||||
---
|
||||
|
||||
## 4. 聚合配置
|
||||
|
||||
| 配置键 | 说明 |
|
||||
|--------|------|
|
||||
| `Aggregation:TxPanelMappings` | TxPanel 名称 → 车厂名称的映射表 |
|
||||
| `Aggregation:CarModelMappings` | TxHardware 版本 → 车型名称的映射表 |
|
||||
|
||||
未配置映射时,直接使用 TxPanel.Name 和 TxHardware.Version 作为最终值。手机品牌/型号从 RxType.Name 自动拆分(分隔符 `/_:`)。
|
||||
@@ -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 WCTDataMiner.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 WCTDataMiner.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace WCTDataMiner.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;
|
||||
}
|
||||
```
|
||||
@@ -66,7 +66,7 @@
|
||||
|
||||
```csharp
|
||||
// Models/QfodRecord.cs
|
||||
namespace WCTDataMiner.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Models;
|
||||
|
||||
public class QfodRecord
|
||||
{
|
||||
@@ -93,9 +93,9 @@ public class QfodRecord
|
||||
// Data/Configurations/QfodRecordConfiguration.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Data.Configurations;
|
||||
|
||||
public class QfodRecordConfiguration : IEntityTypeConfiguration<QfodRecord>
|
||||
{
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
```csharp
|
||||
// Models/RxType.cs
|
||||
namespace WCTDataMiner.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Models;
|
||||
|
||||
public class RxType
|
||||
{
|
||||
@@ -58,9 +58,9 @@ public class RxType
|
||||
// Data/Configurations/RxTypeConfiguration.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Data.Configurations;
|
||||
|
||||
public class RxTypeConfiguration : IEntityTypeConfiguration<RxType>
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
```csharp
|
||||
// Models/TxHardware.cs
|
||||
namespace WCTDataMiner.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Models;
|
||||
|
||||
public class TxHardware
|
||||
{
|
||||
@@ -49,9 +49,9 @@ public class TxHardware
|
||||
// Data/Configurations/TxHardwareConfiguration.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Data.Configurations;
|
||||
|
||||
public class TxHardwareConfiguration : IEntityTypeConfiguration<TxHardware>
|
||||
{
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
```csharp
|
||||
// Models/TxPanel.cs
|
||||
namespace WCTDataMiner.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Models;
|
||||
|
||||
public class TxPanel
|
||||
{
|
||||
@@ -63,9 +63,9 @@ public class TxPanel
|
||||
// Data/Configurations/TxPanelConfiguration.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Data.Configurations;
|
||||
|
||||
public class TxPanelConfiguration : IEntityTypeConfiguration<TxPanel>
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
```csharp
|
||||
// Models/TxSoftware.cs
|
||||
namespace WCTDataMiner.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Models;
|
||||
|
||||
public class TxSoftware
|
||||
{
|
||||
@@ -49,9 +49,9 @@ public class TxSoftware
|
||||
// Data/Configurations/TxSoftwareConfiguration.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Data.Configurations;
|
||||
|
||||
public class TxSoftwareConfiguration : IEntityTypeConfiguration<TxSoftware>
|
||||
{
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
```csharp
|
||||
// Models/TestScenario.cs
|
||||
namespace WCTDataMiner.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Models;
|
||||
|
||||
public class TestScenario
|
||||
{
|
||||
@@ -110,9 +110,9 @@ public class TestScenario
|
||||
// Data/Configurations/TestScenarioConfiguration.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Data.Configurations;
|
||||
|
||||
public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
|
||||
{
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Data Model: WCTDataMiner
|
||||
# Data Model: Gpulse.WCT.DataAnalyzer
|
||||
|
||||
> WCTDataMiner 数据库模型规格,采用维度表设计,消除数据冗余
|
||||
> 双数据库数据模型:本地解析库 + 正式发布库,相互独立。
|
||||
|
||||
---
|
||||
|
||||
## 文档索引
|
||||
|
||||
### 维度表
|
||||
### 本地库(local.db)
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
@@ -14,253 +14,74 @@
|
||||
| [TX硬件版本维度](./entities/TxHardware维度.md) | TX硬件版本维度 |
|
||||
| [TX软件版本维度](./entities/TxSoftware维度.md) | TX软件版本维度 |
|
||||
| [RX类型维度](./entities/RxType维度.md) | RX类型维度 |
|
||||
|
||||
### 事实表
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [测试场景实体](./entities/测试场景.md) | 测试场景(外键组合) |
|
||||
| [Qfod 记录实体](./entities/Qfod记录.md) | Qfod 检测数据 |
|
||||
| [Ploss 记录实体](./entities/Ploss记录.md) | Ploss FOD 数据 |
|
||||
| [关系说明](./关系说明.md) | 实体间关系定义 |
|
||||
| [关系说明](./关系说明.md) | 本地库实体间关系 |
|
||||
|
||||
### 发布库(release.db)
|
||||
|
||||
| 实体 | 表 | 说明 |
|
||||
|------|----|------|
|
||||
| ChargingParameterRecord | charging_parameter | 正式充电参数记录 |
|
||||
|
||||
---
|
||||
|
||||
## 实体列表
|
||||
|
||||
### 维度表(4张)
|
||||
|
||||
| 表名 | 职责 | 关键字段 |
|
||||
|------|------|----------|
|
||||
| tx_panel | TX面板类型维度 | name (UNIQUE) |
|
||||
| tx_hardware | TX硬件版本维度 | version (UNIQUE) |
|
||||
| tx_software | TX软件版本维度 | version (UNIQUE) |
|
||||
| rx_type | RX类型维度 | name (UNIQUE) |
|
||||
|
||||
### 事实表(3张)
|
||||
|
||||
| 表名 | 职责 | 关键字段 |
|
||||
|------|------|----------|
|
||||
| test_scenario | 测试场景组合 | 4个维度外键 + test_purpose + test_date |
|
||||
| qfod_record | Qfod 检测数据 | scenario_id, delta_q, fod_type |
|
||||
| ploss_record | Ploss FOD 数据 | scenario_id, ploss, threshold |
|
||||
|
||||
---
|
||||
|
||||
## ER 关系图
|
||||
## 架构概览
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
%% 本地库
|
||||
tx_panel ||--o{ test_scenario : "引用"
|
||||
tx_hardware ||--o{ test_scenario : "引用"
|
||||
tx_software ||--o{ test_scenario : "引用"
|
||||
rx_type ||--o{ test_scenario : "引用"
|
||||
|
||||
test_scenario ||--o{ qfod_record : "包含"
|
||||
test_scenario ||--o{ ploss_record : "包含"
|
||||
|
||||
tx_panel {
|
||||
uuid id PK "主键"
|
||||
varchar name UK "面板类型"
|
||||
}
|
||||
|
||||
tx_hardware {
|
||||
uuid id PK "主键"
|
||||
varchar version UK "硬件版本"
|
||||
}
|
||||
|
||||
tx_software {
|
||||
uuid id PK "主键"
|
||||
varchar version UK "软件版本"
|
||||
}
|
||||
|
||||
rx_type {
|
||||
uuid id PK "主键"
|
||||
varchar name UK "RX类型"
|
||||
}
|
||||
|
||||
test_scenario {
|
||||
uuid id PK "主键"
|
||||
uuid tx_panel_id FK "面板外键"
|
||||
uuid tx_hardware_id FK "硬件外键"
|
||||
uuid tx_software_id FK "软件外键"
|
||||
uuid rx_type_id FK "RX外键"
|
||||
varchar test_purpose "测试目的"
|
||||
date test_date "测试日期"
|
||||
int test_sequence "测试序号"
|
||||
int qfod_count "Qfod记录数"
|
||||
int ploss_count "Ploss记录数"
|
||||
}
|
||||
|
||||
qfod_record {
|
||||
uuid id PK "主键"
|
||||
uuid scenario_id FK "场景外键"
|
||||
tinyint charger_index "充电器索引"
|
||||
tinyint coil_index "线圈索引"
|
||||
int delta_q "Q值变化量"
|
||||
float current_q "当前Q值"
|
||||
float raw_q "原始Q值"
|
||||
tinyint fod_type "异物类型"
|
||||
}
|
||||
|
||||
ploss_record {
|
||||
uuid id PK "主键"
|
||||
uuid scenario_id FK "场景外键"
|
||||
tinyint rx_type "接收端类型"
|
||||
smallint rx_power "接收端功率"
|
||||
smallint tx_power "发射端功率"
|
||||
int ploss "计算损耗值"
|
||||
int threshold "阈值"
|
||||
smallint fod_result "FOD判定结果"
|
||||
%% 发布库(独立)
|
||||
charging_parameter {
|
||||
uuid id PK
|
||||
varchar car_factory UK
|
||||
varchar car_model UK
|
||||
varchar phone_brand UK
|
||||
varchar phone_model UK
|
||||
double power_350mw
|
||||
double power_500mw
|
||||
double power_750mw
|
||||
double power_1000mw
|
||||
double power_1250mw
|
||||
double power_1500mw
|
||||
double power_1750mw
|
||||
double power_2000mw
|
||||
double power_2250mw
|
||||
double q_value
|
||||
double q_base_value
|
||||
double pq_coefficient
|
||||
double resonance_frequency
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
框外是本地库,框内是发布库——两库之间没有外键或导航属性。
|
||||
|
||||
## 设计说明
|
||||
|
||||
### 星型模型架构
|
||||
### 本地库:星型模型
|
||||
|
||||
本设计采用星型模型(Star Schema):
|
||||
- 维度表:TxPanel, TxHardware, TxSoftware, RxType
|
||||
- 事实表:TestScenario, QfodRecord, PlossRecord
|
||||
- 所有表支持软删除(is_deleted + query filter)
|
||||
|
||||
- **维度表**:存储可能重复的属性值(面板、硬件版本、软件版本、RX类型)
|
||||
- **事实表**:存储外键组合和度量数据
|
||||
### 发布库:独立扁平表
|
||||
|
||||
**优势**:
|
||||
- 维度值只存一份,消除冗余
|
||||
- 维度可复用,相同硬件版本在不同场景中引用同一记录
|
||||
- 查询可按维度灵活筛选和分组
|
||||
- 一个业务多维表,无外键依赖
|
||||
- 唯一键:(car_factory, car_model, phone_brand, phone_model)
|
||||
- 无 is_deleted、无软删除、无审计字段
|
||||
- 更新时间 updated_at 记录发布记录的最后变更时间
|
||||
|
||||
### test_purpose 处理
|
||||
### 隔离原则
|
||||
|
||||
`test_purpose` 直接存储在场景表中,不拆分维度表,原因:
|
||||
- 测试目的描述较自由,不适合标准化为维度
|
||||
- 便于用户灵活输入
|
||||
|
||||
### 场景唯一性
|
||||
|
||||
场景表设置唯一约束,相同属性组合不重复创建:
|
||||
|
||||
```
|
||||
UNIQUE(tx_panel_id, tx_hardware_id, tx_software_id, rx_type_id, test_purpose, test_date, test_sequence)
|
||||
```
|
||||
|
||||
### 软删除设计
|
||||
|
||||
所有表均包含 `is_deleted` 字段,支持软删除:
|
||||
|
||||
| 字段名 | 类型 | 约束 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| is_deleted | BOOLEAN | NOT NULL, DEFAULT FALSE | 软删除标记 |
|
||||
|
||||
**删除行为**:
|
||||
- 删除操作设置 `is_deleted = true`,不物理删除数据
|
||||
- 查询时默认过滤已删除记录(`WHERE is_deleted = false`)
|
||||
- 支持数据恢复和审计追溯
|
||||
|
||||
**查询示例**:
|
||||
```csharp
|
||||
// 默认查询(不含已删除)
|
||||
var activeRecords = context.TxPanels
|
||||
.Where(t => !t.IsDeleted)
|
||||
.ToList();
|
||||
|
||||
// 包含已删除记录
|
||||
var allRecords = context.TxPanels
|
||||
.IgnoreQueryFilters()
|
||||
.ToList();
|
||||
|
||||
// 软删除操作
|
||||
public async Task SoftDeleteAsync(Guid id)
|
||||
{
|
||||
var entity = await context.TxPanels.FindAsync(id);
|
||||
if (entity != null)
|
||||
{
|
||||
entity.IsDeleted = true;
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实体配置方式
|
||||
|
||||
采用 `IEntityTypeConfiguration<T>` Fluent API 配置,实体类保持纯净 POCO:
|
||||
|
||||
```csharp
|
||||
// Data/WctMinerDbContext.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data;
|
||||
|
||||
public class WctMinerDbContext : DbContext
|
||||
{
|
||||
public WctMinerDbContext(DbContextOptions<WctMinerDbContext> options)
|
||||
: base(options) { }
|
||||
|
||||
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<TestScenario> TestScenarios => Set<TestScenario>();
|
||||
public DbSet<QfodRecord> QfodRecords => Set<QfodRecord>();
|
||||
public DbSet<PlossRecord> PlossRecords => Set<PlossRecord>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// 自动应用所有 IEntityTypeConfiguration<T> 配置
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(WctMinerDbContext).Assembly);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **数据库兼容性说明**:
|
||||
> - `HasDefaultValueSql("NOW()")` 适用于 PostgreSQL
|
||||
> - 若使用 SQLite,需改为 `HasDefaultValueSql("datetime('now')")`
|
||||
> - 若使用 MySQL,需改为 `HasDefaultValueSql("NOW()")`(MySQL 也支持)
|
||||
> - 若使用 SQL Server,需改为 `HasDefaultValueSql("GETDATE()")`
|
||||
|
||||
**配置类列表**:
|
||||
|
||||
| 配置类 | 文件路径 | 说明 |
|
||||
|--------|----------|------|
|
||||
| TxPanelConfiguration | Data/Configurations/TxPanelConfiguration.cs | TX面板维度 |
|
||||
| TxHardwareConfiguration | Data/Configurations/TxHardwareConfiguration.cs | TX硬件版本维度 |
|
||||
| TxSoftwareConfiguration | Data/Configurations/TxSoftwareConfiguration.cs | TX软件版本维度 |
|
||||
| RxTypeConfiguration | Data/Configurations/RxTypeConfiguration.cs | RX类型维度 |
|
||||
| TestScenarioConfiguration | Data/Configurations/TestScenarioConfiguration.cs | 测试场景 |
|
||||
| QfodRecordConfiguration | Data/Configurations/QfodRecordConfiguration.cs | Qfod记录 |
|
||||
| PlossRecordConfiguration | Data/Configurations/PlossRecordConfiguration.cs | Ploss记录 |
|
||||
|
||||
> **迁移注意事项**:从 DataAnnotations `DateTime.UtcNow` 迁移到 `HasDefaultValueSql("NOW()")` 后,测试代码需注意:未显式设置 `CreatedAt` 时,值由数据库生成而非 C# 运行时。单元测试中需 mock 数据库或显式设置值。
|
||||
|
||||
### 系统日志处理
|
||||
|
||||
解析过程中的错误日志通过 Serilog 记录到系统日志文件,不存入业务数据库。
|
||||
|
||||
---
|
||||
|
||||
## 数据量估算
|
||||
|
||||
| 表 | 增长方式 | 预估规模 |
|
||||
|------|----------|----------|
|
||||
| tx_panel | 预置数据 | 7 条(固定枚举) |
|
||||
| tx_hardware | 随版本迭代 | 10-50 条/年 |
|
||||
| tx_software | 随版本迭代 | 10-50 条/年 |
|
||||
| rx_type | 随设备增加 | 20-100 条/年 |
|
||||
| test_scenario | 随测试批次 | 3,650 条/年 |
|
||||
| qfod_record | 随测试数据 | 3.6M-18M 条/年 |
|
||||
| ploss_record | 随测试数据 | 3.6M-18M 条/年 |
|
||||
|
||||
---
|
||||
|
||||
## 假设/歧义标注
|
||||
|
||||
| 标注 | 内容 |
|
||||
|------|------|
|
||||
| [A] | 使用 UUID 作为主键 |
|
||||
| [?] | FOD Type 具体编码含义需确认 |
|
||||
| [?] | Ploss 日志格式与实际日志对应关系需确认 |
|
||||
| [!] | ploss 负数表示安全,正数可能触发 FOD |
|
||||
- 代码中两个实体的命名空间不同(Domain.Models)
|
||||
- 两个 DbContext 不共享 DbSet
|
||||
- 发布库不从本地库继承实体配置
|
||||
- aggregate 通过纯值映射,不依赖导航属性
|
||||
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,9 +1,9 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WCTDataMiner.Core.Data;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace WCTDataMiner.Core.Services;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 数据清理服务 - 支持软删除
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,11 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WCTDataMiner.Core.Data;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using WCTDataMiner.Core.Security;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
|
||||
|
||||
namespace WCTDataMiner.Core.Services;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 数据导出服务 - 支持安全的路径验证和 RFC 4180 标准 CSV 转义
|
||||
@@ -15,12 +15,14 @@ namespace WCTDataMiner.Core.Services;
|
||||
public class ExportService
|
||||
{
|
||||
private readonly WctMinerDbContext _context;
|
||||
private readonly ReleaseDbContext _releaseContext;
|
||||
private readonly ILogger<ExportService> _logger;
|
||||
private readonly PathValidator _pathValidator;
|
||||
|
||||
public ExportService(WctMinerDbContext context, ILogger<ExportService> logger)
|
||||
public ExportService(WctMinerDbContext context, ReleaseDbContext releaseContext, ILogger<ExportService> logger)
|
||||
{
|
||||
_context = context;
|
||||
_releaseContext = releaseContext;
|
||||
_logger = logger;
|
||||
_pathValidator = new PathValidator(AppDomain.CurrentDomain.BaseDirectory);
|
||||
}
|
||||
@@ -305,7 +307,49 @@ public class ExportService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// RFC 4180 标准 CSV 字段转义
|
||||
/// 导出正式发布库中的 ChargingParameterDatabase CSV。
|
||||
/// </summary>
|
||||
public async Task<string> ExportChargingParametersToCsvAsync(string outputDir)
|
||||
{
|
||||
var validationResult = _pathValidator.ValidateOutputDirectory(outputDir);
|
||||
if (!validationResult.IsValid)
|
||||
throw new ArgumentException($"无效的输出目录: {validationResult.ErrorMessage}");
|
||||
|
||||
var safeOutputDir = validationResult.NormalizedPath!;
|
||||
Directory.CreateDirectory(safeOutputDir);
|
||||
var filePath = Path.Combine(safeOutputDir, "WCT-ChargingParameterDatabase.csv");
|
||||
|
||||
await using var writer = new StreamWriter(filePath, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
|
||||
await writer.WriteLineAsync("车厂,车型,手机厂商,型号,350mW,500mW,750mW,1000mW,1250mW,1500mW,1750mW,2000mW,2250mW,Q值,Q基值,P-Q值系数,谐振频率");
|
||||
|
||||
var query = _releaseContext.ChargingParameters
|
||||
.AsNoTracking()
|
||||
.OrderBy(r => r.CarFactory)
|
||||
.ThenBy(r => r.CarModel)
|
||||
.ThenBy(r => r.PhoneBrand)
|
||||
.ThenBy(r => r.PhoneModel);
|
||||
|
||||
var count = 0;
|
||||
await foreach (var record in query.AsAsyncEnumerable())
|
||||
{
|
||||
var line = string.Join(',',
|
||||
EscapeCsvField(record.CarFactory), EscapeCsvField(record.CarModel),
|
||||
EscapeCsvField(record.PhoneBrand), EscapeCsvField(record.PhoneModel),
|
||||
EscapeCsvField(record.Power350mW), EscapeCsvField(record.Power500mW),
|
||||
EscapeCsvField(record.Power750mW), EscapeCsvField(record.Power1000mW),
|
||||
EscapeCsvField(record.Power1250mW), EscapeCsvField(record.Power1500mW),
|
||||
EscapeCsvField(record.Power1750mW), EscapeCsvField(record.Power2000mW),
|
||||
EscapeCsvField(record.Power2250mW), EscapeCsvField(record.QValue),
|
||||
EscapeCsvField(record.QBaseValue), EscapeCsvField(record.PqCoefficient),
|
||||
EscapeCsvField(record.ResonanceFrequency));
|
||||
await writer.WriteLineAsync(line);
|
||||
count++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("ChargingParameterDatabase CSV 导出完成: {FilePath}, 共 {Count} 条记录", filePath, count);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// </summary>
|
||||
private static string EscapeCsvField(object? value)
|
||||
{
|
||||
@@ -1,9 +1,9 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WCTDataMiner.Core.Data;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace WCTDataMiner.Core.Services;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 维度表管理服务 - 负责维度表的CRUD操作(并发安全版本)
|
||||
@@ -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>
|
||||
@@ -1,11 +1,11 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WCTDataMiner.Core.Data;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using WCTDataMiner.Core.Parsers;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||
|
||||
namespace WCTDataMiner.Core.Services;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
/// <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);
|
||||
@@ -1,13 +1,13 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WCTDataMiner.Core.Data;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using WCTDataMiner.Core.Parsers;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||
|
||||
namespace WCTDataMiner.Core.Services;
|
||||
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);
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名解析器 - 从文件名提取场景信息
|
||||
/// 格式: 车厂-车型-TX面板类型-TX硬件-TX软件-RX类型-异物类型-测试目的-年月日测试次数.dat
|
||||
/// 分隔符: 使用 '-' 作为字段分隔符,字段内部可包含 '_' 或 '.' 等符号
|
||||
/// 示例: 奇瑞-E5-singleMold-A00-V001-伏达小蓝板-无-compatibility-2026081701.dat
|
||||
/// </summary>
|
||||
public class FileNameParser
|
||||
{
|
||||
private static readonly char[] Separators = { '-' };
|
||||
|
||||
/// <summary>
|
||||
/// 解析日志文件名,提取场景信息
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件名(不含路径)</param>
|
||||
/// <returns>场景信息,若格式不匹配返回 null</returns>
|
||||
public ScenarioInfo? Parse(string fileName)
|
||||
{
|
||||
// 移除扩展名
|
||||
var nameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
|
||||
if (string.IsNullOrEmpty(nameWithoutExt))
|
||||
return null;
|
||||
|
||||
var parts = nameWithoutExt.Split(Separators);
|
||||
if (parts.Length < 9)
|
||||
return null;
|
||||
|
||||
// 解析日期+序号(最后一个字段,格式:yyyyMMddNN)
|
||||
var dateSeqStr = parts[8];
|
||||
if (!TryParseDateSequence(dateSeqStr, out var testDate, out var testSequence))
|
||||
return null;
|
||||
|
||||
return new ScenarioInfo(
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析日期+序号组合字段
|
||||
/// 格式: yyyyMMddNN (如 2026081701 表示 2026年8月17日 第01次测试)
|
||||
/// </summary>
|
||||
private static bool TryParseDateSequence(string dateSeqStr, out DateOnly date, out int sequence)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 场景信息 - 从文件名解析出的测试场景数据
|
||||
/// </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,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Parsers;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||
|
||||
/// <summary>
|
||||
/// 通用解析器接口
|
||||
@@ -1,13 +1,13 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace WCTDataMiner.Core.Parsers;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace WCTDataMiner.Core.Parsers;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||
|
||||
/// <summary>
|
||||
/// Qfod日志解析器
|
||||
@@ -0,0 +1,204 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Release;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 从本地解析库生成独立正式发布库。
|
||||
/// 当前业务规则:功率列按 Threshold(Field9) 分桶、取值 Ploss(Field7),重复结果取平均值;缺失值保留为空。
|
||||
/// </summary>
|
||||
public class AggregationService
|
||||
{
|
||||
private static readonly int[] PowerLevels = [350, 500, 750, 1000, 1250, 1500, 1750, 2000, 2250];
|
||||
private readonly WctMinerDbContext _localContext;
|
||||
private readonly ReleaseDbContext _releaseContext;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<AggregationService> _logger;
|
||||
|
||||
public AggregationService(
|
||||
WctMinerDbContext localContext,
|
||||
ReleaseDbContext releaseContext,
|
||||
IConfiguration configuration,
|
||||
ILogger<AggregationService> logger)
|
||||
{
|
||||
_localContext = localContext;
|
||||
_releaseContext = releaseContext;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
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(), qfodAggregates))
|
||||
.ToList();
|
||||
|
||||
await using var transaction = await _releaseContext.Database.BeginTransactionAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
if (rebuild)
|
||||
await _releaseContext.Database.ExecuteSqlRawAsync("DELETE FROM charging_parameter", cancellationToken);
|
||||
|
||||
await _releaseContext.ChargingParameters.AddRangeAsync(records, cancellationToken);
|
||||
await _releaseContext.SaveChangesAsync(cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync(cancellationToken);
|
||||
throw;
|
||||
}
|
||||
|
||||
_logger.LogInformation("聚合完成: {Count} 条正式发布记录", records.Count);
|
||||
return new AggregationReport(records.Count, rows.Count);
|
||||
}
|
||||
|
||||
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!.Manufacturer)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.CarModel)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.RxType)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return records.Select(r => new PlossRow(
|
||||
r,
|
||||
r.Scenario,
|
||||
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)
|
||||
{
|
||||
// 直接使用 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);
|
||||
}
|
||||
|
||||
private static (string Brand, string Model) SplitPhoneName(string value)
|
||||
{
|
||||
var separator = value.IndexOfAny(['/', '_', ':']);
|
||||
if (separator > 0 && separator < value.Length - 1)
|
||||
return (value[..separator], value[(separator + 1)..]);
|
||||
|
||||
return ("Unknown", value);
|
||||
}
|
||||
|
||||
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,
|
||||
CarModel = key.CarModel,
|
||||
PhoneBrand = key.PhoneBrand,
|
||||
PhoneModel = key.PhoneModel,
|
||||
Power350mW = values[0],
|
||||
Power500mW = values[1],
|
||||
Power750mW = values[2],
|
||||
Power1000mW = values[3],
|
||||
Power1250mW = values[4],
|
||||
Power1500mW = values[5],
|
||||
Power1750mW = values[6],
|
||||
Power2000mW = values[7],
|
||||
Power2250mW = values[8],
|
||||
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)
|
||||
{
|
||||
// 功率列按 Threshold(Field9) 分桶:记录的阈值等于哪个档位,其 Ploss 就进哪一列。
|
||||
// 注:需求文档中 Threshold 为逻辑编号 (8),但当前解析数据里阈值实际落在 field_9。
|
||||
return record.Field9 == power;
|
||||
}
|
||||
|
||||
private static double? Average(IEnumerable<double?> values)
|
||||
{
|
||||
var valid = values.Where(value => value.HasValue).Select(value => value!.Value).ToArray();
|
||||
return valid.Length == 0 ? null : valid.Average();
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Services;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
/// <summary>
|
||||
/// 阈值计算器 - 根据接收功率查表获取动态阈值
|
||||
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; } = [];
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
/// <summary>
|
||||
/// Ploss记录实体 - 存储Ploss FOD格式解析数据
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
/// <summary>
|
||||
/// Qfod记录实体 - 存储Qfod格式解析数据
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
/// <summary>
|
||||
/// RX类型维度表
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
/// <summary>
|
||||
/// 测试场景实体 - 通过外键关联各维度表
|
||||
@@ -6,6 +6,12 @@ namespace WCTDataMiner.Core.Models;
|
||||
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!;
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
/// <summary>
|
||||
/// TX硬件版本维度表
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
/// <summary>
|
||||
/// TX面板类型维度表
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Models;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
/// <summary>
|
||||
/// TX软件版本维度表
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Release;
|
||||
|
||||
/// <summary>
|
||||
/// 正式发布库中的充电参数记录。
|
||||
/// 此实体只属于 ReleaseDbContext,不引用本地日志数据库实体。
|
||||
/// </summary>
|
||||
public class ChargingParameterRecord
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
public string CarFactory { get; set; } = null!;
|
||||
public string CarModel { get; set; } = null!;
|
||||
public string PhoneBrand { get; set; } = null!;
|
||||
public string PhoneModel { get; set; } = null!;
|
||||
|
||||
public double? Power350mW { get; set; }
|
||||
public double? Power500mW { get; set; }
|
||||
public double? Power750mW { get; set; }
|
||||
public double? Power1000mW { get; set; }
|
||||
public double? Power1250mW { get; set; }
|
||||
public double? Power1500mW { get; set; }
|
||||
public double? Power1750mW { get; set; }
|
||||
public double? Power2000mW { get; set; }
|
||||
public double? Power2250mW { get; set; }
|
||||
|
||||
public double? QValue { get; set; }
|
||||
public double? QBaseValue { get; set; }
|
||||
public double? PqCoefficient { get; set; }
|
||||
public double? ResonanceFrequency { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>Gpulse.WCT.DataAnalyzer.Core</AssemblyName>
|
||||
<RootNamespace>Gpulse.WCT.DataAnalyzer.Core</RootNamespace>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
@@ -18,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>
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Configuration;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 应用配置
|
||||
@@ -16,7 +16,9 @@ public class AppSettings
|
||||
public class DatabaseSettings
|
||||
{
|
||||
public string Type { get; set; } = "sqlite";
|
||||
public string Path { get; set; } = "./data/database/wctminer.db";
|
||||
public string Path { get; set; } = "./data/database/local.db";
|
||||
public string LocalPath { get; set; } = "./data/database/local.db";
|
||||
public string ReleasePath { get; set; } = "./data/database/release.db";
|
||||
|
||||
// PostgreSQL 配置(可选)
|
||||
public string? Host { get; set; }
|
||||
@@ -1,10 +1,12 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using WCTDataMiner.Core.Data;
|
||||
using WCTDataMiner.Core.Parsers;
|
||||
using WCTDataMiner.Core.Services;
|
||||
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 WCTDataMiner.Core.Extensions;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// 服务注册扩展
|
||||
@@ -28,6 +30,15 @@ public static class ServiceCollectionExtensions
|
||||
return factory.CreateDbContext();
|
||||
});
|
||||
|
||||
services.AddScoped<IReleaseDbContextFactory, ReleaseDbContextFactory>();
|
||||
|
||||
// 注册正式发布数据库上下文(与本地解析数据库完全独立)
|
||||
services.AddScoped<ReleaseDbContext>(sp =>
|
||||
{
|
||||
var factory = sp.GetRequiredService<IReleaseDbContextFactory>();
|
||||
return factory.CreateDbContext();
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -45,9 +56,16 @@ 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);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace WCTDataMiner.Core.Data.Configurations;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||
|
||||
public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
||||
{
|
||||
@@ -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)
|
||||
@@ -1,8 +1,8 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace WCTDataMiner.Core.Data.Configurations;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||
|
||||
public class QfodRecordConfiguration : IEntityTypeConfiguration<QfodRecord>
|
||||
{
|
||||
@@ -1,8 +1,8 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace WCTDataMiner.Core.Data.Configurations;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||
|
||||
public class RxTypeConfiguration : IEntityTypeConfiguration<RxType>
|
||||
{
|
||||
@@ -1,8 +1,8 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace WCTDataMiner.Core.Data.Configurations;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||
|
||||
public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
|
||||
{
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace WCTDataMiner.Core.Data.Configurations;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||
|
||||
public class TxHardwareConfiguration : IEntityTypeConfiguration<TxHardware>
|
||||
{
|
||||
@@ -1,8 +1,8 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace WCTDataMiner.Core.Data.Configurations;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||
|
||||
public class TxPanelConfiguration : IEntityTypeConfiguration<TxPanel>
|
||||
{
|
||||
@@ -1,8 +1,8 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace WCTDataMiner.Core.Data.Configurations;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||
|
||||
public class TxSoftwareConfiguration : IEntityTypeConfiguration<TxSoftware>
|
||||
{
|
||||
@@ -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};");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace WCTDataMiner.Core.Data;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库上下文工厂实现 - 支持SQLite和PostgreSQL切换
|
||||
@@ -30,7 +30,9 @@ public class DbContextFactory : IDbContextFactory
|
||||
|
||||
case "sqlite":
|
||||
default:
|
||||
var dbPath = _configuration["Database:Path"] ?? "./data/database/wctminer.db";
|
||||
var dbPath = _configuration["Database:LocalPath"]
|
||||
?? _configuration["Database:Path"]
|
||||
?? "./data/database/local.db";
|
||||
EnsureDirectoryExists(dbPath);
|
||||
optionsBuilder.UseSqlite($"Data Source={dbPath}");
|
||||
break;
|
||||
@@ -43,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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Data;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库上下文工厂接口
|
||||
@@ -1,6 +1,6 @@
|
||||
using WCTDataMiner.Core.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace WCTDataMiner.Core.Data;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
/// <summary>
|
||||
/// 初始数据种子
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WCTDataMiner.Core.Models;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace WCTDataMiner.Core.Data;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
/// <summary>
|
||||
/// WCT数据采集数据库上下文
|
||||
@@ -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>();
|
||||
@@ -0,0 +1,57 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Release;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||
|
||||
public class ChargingParameterRecordConfiguration : IEntityTypeConfiguration<ChargingParameterRecord>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ChargingParameterRecord> builder)
|
||||
{
|
||||
builder.ToTable("charging_parameter");
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).HasColumnName("id");
|
||||
|
||||
ConfigureText(builder, e => e.CarFactory, "car_factory");
|
||||
ConfigureText(builder, e => e.CarModel, "car_model");
|
||||
ConfigureText(builder, e => e.PhoneBrand, "phone_brand");
|
||||
ConfigureText(builder, e => e.PhoneModel, "phone_model");
|
||||
|
||||
ConfigureNumber(builder, e => e.Power350mW, "power_350mw");
|
||||
ConfigureNumber(builder, e => e.Power500mW, "power_500mw");
|
||||
ConfigureNumber(builder, e => e.Power750mW, "power_750mw");
|
||||
ConfigureNumber(builder, e => e.Power1000mW, "power_1000mw");
|
||||
ConfigureNumber(builder, e => e.Power1250mW, "power_1250mw");
|
||||
ConfigureNumber(builder, e => e.Power1500mW, "power_1500mw");
|
||||
ConfigureNumber(builder, e => e.Power1750mW, "power_1750mw");
|
||||
ConfigureNumber(builder, e => e.Power2000mW, "power_2000mw");
|
||||
ConfigureNumber(builder, e => e.Power2250mW, "power_2250mw");
|
||||
ConfigureNumber(builder, e => e.QValue, "q_value");
|
||||
ConfigureNumber(builder, e => e.QBaseValue, "q_base_value");
|
||||
ConfigureNumber(builder, e => e.PqCoefficient, "pq_coefficient");
|
||||
ConfigureNumber(builder, e => e.ResonanceFrequency, "resonance_frequency");
|
||||
|
||||
builder.Property(e => e.CreatedAt).IsRequired().HasColumnName("created_at");
|
||||
builder.Property(e => e.UpdatedAt).IsRequired().HasColumnName("updated_at");
|
||||
|
||||
builder.HasIndex(e => new { e.CarFactory, e.CarModel, e.PhoneBrand, e.PhoneModel })
|
||||
.IsUnique()
|
||||
.HasDatabaseName("uq_charging_parameter_business_key");
|
||||
}
|
||||
|
||||
private static void ConfigureText(
|
||||
EntityTypeBuilder<ChargingParameterRecord> builder,
|
||||
System.Linq.Expressions.Expression<Func<ChargingParameterRecord, string>> property,
|
||||
string columnName)
|
||||
{
|
||||
builder.Property(property).IsRequired().HasMaxLength(128).HasColumnName(columnName);
|
||||
}
|
||||
|
||||
private static void ConfigureNumber(
|
||||
EntityTypeBuilder<ChargingParameterRecord> builder,
|
||||
System.Linq.Expressions.Expression<Func<ChargingParameterRecord, double?>> property,
|
||||
string columnName)
|
||||
{
|
||||
builder.Property(property).IsRequired(false).HasColumnName(columnName).HasPrecision(18, 6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
public interface IReleaseDbContextFactory
|
||||
{
|
||||
ReleaseDbContext CreateDbContext();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Release;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
/// <summary>
|
||||
/// 正式发布数据库上下文。只包含最终充电参数记录。
|
||||
/// </summary>
|
||||
public class ReleaseDbContext : DbContext
|
||||
{
|
||||
public ReleaseDbContext(DbContextOptions<ReleaseDbContext> options)
|
||||
: base(options) { }
|
||||
|
||||
public DbSet<ChargingParameterRecord> ChargingParameters => Set<ChargingParameterRecord>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ReleaseDbContext).Assembly);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
/// <summary>
|
||||
/// 正式发布数据库上下文工厂。
|
||||
/// </summary>
|
||||
public class ReleaseDbContextFactory : IReleaseDbContextFactory
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public ReleaseDbContextFactory(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public ReleaseDbContext CreateDbContext()
|
||||
{
|
||||
var dbType = _configuration["Database:Type"]?.ToLowerInvariant() ?? "sqlite";
|
||||
var optionsBuilder = new DbContextOptionsBuilder<ReleaseDbContext>();
|
||||
|
||||
if (dbType is "postgresql" or "postgres")
|
||||
{
|
||||
optionsBuilder.UseNpgsql(BuildPostgreSqlConnectionString());
|
||||
}
|
||||
else
|
||||
{
|
||||
var dbPath = _configuration["Database:ReleasePath"] ?? "./data/database/release.db";
|
||||
EnsureDirectoryExists(dbPath);
|
||||
optionsBuilder.UseSqlite($"Data Source={dbPath}");
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
optionsBuilder.EnableSensitiveDataLogging();
|
||||
optionsBuilder.EnableDetailedErrors();
|
||||
#endif
|
||||
|
||||
return new ReleaseDbContext(optionsBuilder.Options);
|
||||
}
|
||||
|
||||
private string BuildPostgreSqlConnectionString()
|
||||
{
|
||||
var host = _configuration["Database:ReleaseHost"] ?? _configuration["Database:Host"] ?? "localhost";
|
||||
var port = _configuration.GetValue("Database:ReleasePort", _configuration.GetValue("Database:Port", 5432));
|
||||
var name = _configuration["Database:ReleaseName"] ?? "wct_charging_parameters";
|
||||
var user = _configuration["Database:ReleaseUser"] ?? _configuration["Database:User"] ?? "postgres";
|
||||
var password = _configuration["Database:ReleasePassword"] ?? _configuration["Database:Password"] ?? "";
|
||||
return $"Host={host};Port={port};Database={name};Username={user};Password={password}";
|
||||
}
|
||||
|
||||
private static void EnsureDirectoryExists(string dbPath)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(dbPath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace WCTDataMiner.Core.Security;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
|
||||
|
||||
/// <summary>
|
||||
/// 路径验证结果
|
||||
@@ -36,7 +36,7 @@ public class PathValidator
|
||||
/// <summary>
|
||||
/// 默认允许的输出目录
|
||||
/// </summary>
|
||||
private static readonly string[] DefaultAllowedDirectories = ["exports", "output", "data/exports"];
|
||||
private static readonly string[] DefaultAllowedDirectories = ["exports", "output", "data/exports", "data/output"];
|
||||
|
||||
public PathValidator(string applicationBasePath, IEnumerable<string>? allowedDirectories = null)
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace WCTDataMiner.Core.Security;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
|
||||
|
||||
/// <summary>
|
||||
/// 安全相关常量定义
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/Gpulse.WCT.DataAnalyzer/Commands/AggregateCommand.cs
Normal file
33
src/Gpulse.WCT.DataAnalyzer/Commands/AggregateCommand.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.CommandLine;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 从本地解析库生成正式发布库。
|
||||
/// </summary>
|
||||
public class AggregateCommand : Command
|
||||
{
|
||||
public AggregateCommand(AggregationService aggregationService)
|
||||
: base("aggregate", "Aggregate local log data into the release database")
|
||||
{
|
||||
var rebuildOption = new Option<bool>(
|
||||
"--rebuild",
|
||||
() => true,
|
||||
"Rebuild the release table before inserting aggregated records");
|
||||
AddOption(rebuildOption);
|
||||
|
||||
this.SetHandler(async rebuild =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var report = await aggregationService.AggregateAsync(rebuild);
|
||||
Console.WriteLine($"Aggregated {report.LocalRecordCount} local Ploss records into {report.ReleaseRecordCount} release records.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Aggregation failed: {ex.Message}");
|
||||
}
|
||||
}, rebuildOption);
|
||||
}
|
||||
}
|
||||
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,7 +1,7 @@
|
||||
using System.CommandLine;
|
||||
using WCTDataMiner.Core.Services;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
namespace WCTDataMiner.Commands;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 数据清理命令
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.CommandLine;
|
||||
using WCTDataMiner.Core.Services;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
namespace WCTDataMiner.Commands;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 数据导出命令
|
||||
@@ -25,8 +25,8 @@ public class ExportCommand : Command
|
||||
|
||||
var typeOption = new Option<string>(
|
||||
"--type",
|
||||
() => "all",
|
||||
"Data type to export: qfod, ploss, or all"
|
||||
() => "charging-parameters",
|
||||
"Data type: charging-parameters, qfod, ploss, or all"
|
||||
);
|
||||
|
||||
AddOption(formatOption);
|
||||
@@ -44,6 +44,12 @@ public class ExportCommand : Command
|
||||
{
|
||||
if (format == "csv")
|
||||
{
|
||||
if (type is "charging-parameters" or "charging" or "all")
|
||||
{
|
||||
var path = await exportService.ExportChargingParametersToCsvAsync(output);
|
||||
Console.WriteLine($" ChargingParameterDatabase CSV exported: {path}");
|
||||
}
|
||||
|
||||
if (type is "qfod" or "all")
|
||||
{
|
||||
var path = await exportService.ExportQfodToCsvAsync(output);
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.CommandLine;
|
||||
using WCTDataMiner.Core.Services;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
namespace WCTDataMiner.Commands;
|
||||
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 解析日志文件命令
|
||||
@@ -1,13 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyName>Gpulse.WCT.DataAnalyzer</AssemblyName>
|
||||
<RootNamespace>Gpulse.WCT.DataAnalyzer</RootNamespace>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<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" />
|
||||
@@ -30,13 +39,15 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\WCTDataMiner.Core\WCTDataMiner.Core.csproj" />
|
||||
<ProjectReference Include="..\Gpulse.WCT.DataAnalyzer.Core\Gpulse.WCT.DataAnalyzer.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<EmbeddedResource Include="appsettings.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="analyze.bat" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
259
src/Gpulse.WCT.DataAnalyzer/Program.cs
Normal file
259
src/Gpulse.WCT.DataAnalyzer/Program.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
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;
|
||||
using Serilog;
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gpulse.WCT.DataAnalyzer - 无线充电FOD日志数据采集系统
|
||||
/// </summary>
|
||||
public class Program
|
||||
{
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
// 1. 构建引导配置(用于 Serilog 初始化),并将数据库/日志等相对路径解析到 exe 所在目录
|
||||
var bootstrapConfig = BuildConfiguration(args);
|
||||
|
||||
// 2. 创建引导日志器
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(bootstrapConfig)
|
||||
.Enrich.FromLogContext()
|
||||
.CreateBootstrapLogger();
|
||||
|
||||
try
|
||||
{
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// 3. 使用 AddConfiguration 添加已构建的配置(关键修复)
|
||||
builder.Configuration.AddConfiguration(bootstrapConfig);
|
||||
|
||||
// 4. 配置 Serilog
|
||||
builder.Services.AddSerilog((_, loggerConfig) =>
|
||||
{
|
||||
loggerConfig
|
||||
.ReadFrom.Configuration(builder.Configuration)
|
||||
.Enrich.FromLogContext();
|
||||
});
|
||||
|
||||
// 5. 注册服务
|
||||
builder.Services.AddDatabaseServices(builder.Configuration);
|
||||
builder.Services.AddApplicationServices();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// 初始化数据库(经 EF 迁移建表/升级,替代 EnsureCreated 以支持既有库升级)
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
|
||||
await DatabaseMigrationInitializer.MigrateOrUpgradeAsync(dbContext);
|
||||
|
||||
var releaseContext = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>();
|
||||
await DatabaseMigrationInitializer.MigrateOrUpgradeAsync(releaseContext);
|
||||
|
||||
// 种子数据
|
||||
SeedData.Initialize(dbContext);
|
||||
}
|
||||
|
||||
// 创建 CLI 命令
|
||||
using var commandScope = app.Services.CreateScope();
|
||||
var provider = commandScope.ServiceProvider;
|
||||
|
||||
var rootCommand = new RootCommand("Gpulse.WCT.DataAnalyzer - FOD Log Data Parser");
|
||||
|
||||
rootCommand.AddCommand(new ParseCommand(provider.GetRequiredService<ParseService>()));
|
||||
rootCommand.AddCommand(new AggregateCommand(provider.GetRequiredService<AggregationService>()));
|
||||
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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Application terminated unexpectedly");
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
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"]
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace WCTDataMiner.Core.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名解析器 - 从文件名提取场景信息
|
||||
/// 格式: TX面板-TX硬件-TX软件-RX类型-测试目的-年月日-测试次数.log
|
||||
/// 分隔符: 使用 '-' 作为字段分隔符,字段内部可包含 '_' 或 '.' 等符号
|
||||
/// 示例: singleMold-v1.0-hex2_1-iPhone15-compatibility-20260702-1.log
|
||||
/// </summary>
|
||||
public class FileNameParser
|
||||
{
|
||||
private static readonly char[] Separators = { '-' };
|
||||
|
||||
/// <summary>
|
||||
/// 解析日志文件名,提取场景信息
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件名(不含路径)</param>
|
||||
/// <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)
|
||||
return null;
|
||||
|
||||
// 解析日期
|
||||
if (!TryParseDate(parts[5], out var testDate))
|
||||
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,
|
||||
TestDate: testDate,
|
||||
TestSequence: testSequence
|
||||
);
|
||||
}
|
||||
|
||||
private static bool TryParseDate(string dateStr, out DateOnly date)
|
||||
{
|
||||
// 支持多种日期格式
|
||||
var formats = new[] { "yyyyMMdd", "yyyy-MM-dd", "yyMMdd" };
|
||||
return DateOnly.TryParseExact(dateStr, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out date);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 场景信息 - 从文件名解析出的测试场景数据
|
||||
/// </summary>
|
||||
public record ScenarioInfo(
|
||||
string TxPanel,
|
||||
string TxHardware,
|
||||
string TxSoftware,
|
||||
string RxType,
|
||||
string? TestPurpose,
|
||||
DateOnly TestDate,
|
||||
int TestSequence
|
||||
);
|
||||
@@ -1,142 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WCTDataMiner.Core.Data;
|
||||
using WCTDataMiner.Core.Models;
|
||||
|
||||
namespace WCTDataMiner.Core.Services;
|
||||
|
||||
/// <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);
|
||||
@@ -1,10 +0,0 @@
|
||||
<Application x:Class="WCTDataMiner.Wpf.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:WCTDataMiner.Wpf"
|
||||
xmlns:converters="clr-namespace:WCTDataMiner.Wpf.Converters">
|
||||
<Application.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
|
||||
<converters:InverseBooleanConverter x:Key="InverseBooleanConverter"/>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user