Compare commits

...

29 Commits

Author SHA1 Message Date
Scottxjw
9fd788bc7f feat(analyze): 支持递归扫描子目录 2026-08-25 13:35:22 +08:00
Scottxjw
a554000674 docs: 移除旧的AnalyzeCommand文档 2026-08-25 13:25:26 +08:00
Scottxjw
df5234ffe0 fix(publish): 适配SDK风格csproj的版本号读取 2026-08-25 13:24:13 +08:00
Scottxjw
0677f2ea15 docs: 添加AnalyzeCommand使用指南并更新需求文档
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 11:30:31 +08:00
Scottxjw
7a8a1e9a87 test: 更新分析功能和迁移测试用例
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 11:30:16 +08:00
Scottxjw
ee8ea29265 feat(cli): 增强Analyze命令支持自动扫描和Excel输出
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 11:30:00 +08:00
Scottxjw
820bc1c2bc feat(analysis): 添加Excel报告导出功能
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 11:29:45 +08:00
Scottxjw
abaa9cb6d1 feat(db): 扩展TestScenario维度模型
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-25 11:29:30 +08:00
Scottxjw
3951606cfa chore(publish): 禁用单文件发布的调试符号 2026-08-17 13:39:24 +08:00
Scottxjw
b0b55c0d4e docs: 添加 AnalyzeCommand 使用指南 2026-08-17 13:30:42 +08:00
Scottxjw
ccf3885c38 chore: 忽略 Resources 目录 2026-08-17 13:24:52 +08:00
Scottxjw
81c5316eac test: 添加真实产线数据的集成测试用例 2026-08-17 13:24:46 +08:00
Scottxjw
eba1d2080c feat(parser): 支持 Field1 的 0x/0X 十六进制前缀格式 2026-08-17 13:24:39 +08:00
Scottxjw
348d11c480 chore(scripts): 添加单文件发布脚本
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-17 10:48:33 +08:00
Scottxjw
6d56a4a01b feat(parser): 支持 Ploss Field1 十六进制解析
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-17 10:47:33 +08:00
Scottxjw
36c21f03dd refactor(config): 将 appsettings.json 转为嵌入资源
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-17 10:47:00 +08:00
Scottxjw
759da546ee test: 添加 CLI 命令单元测试 2026-08-17 09:24:57 +08:00
Scottxjw
d45d9f4ca3 chore: 添加应用默认配置文件 2026-08-17 09:24:06 +08:00
Scottxjw
0667e2e9b8 feat(analyze): 新增 Ploss/Qfod 分析命令
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-14 15:36:29 +08:00
Scottxjw
b88c07356e refactor(ploss): 统一字段语义 Field9=ploss Field10=threshold
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-14 15:35:48 +08:00
Scottxjw
dea7af045b docs(requirement): 清理遗留的 QA 问题与编号错误
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-14 09:25:47 +08:00
Scottxjw
7cd08f934a feat(parse): 支持解析 txt/dat 扩展名数据文件
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-14 09:25:12 +08:00
Scottxjw
430b38b361 feat(aggregation): 按 Threshold 分桶并优化 Qfod 聚合
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-14 09:24:38 +08:00
Scottxjw
77d4bfe953 feat(cli): 配置相对路径按 exe 目录解析
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-14 09:24:12 +08:00
Scottxjw
f3472d21ee refactor: 移除 stats 统计功能
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-14 09:23:26 +08:00
Scottxjw
2b031995e1 docs: 更新项目架构与数据模型文档
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-13 15:01:32 +08:00
Scottxjw
a3100c73c5 refactor(core): 重构核心分层结构并添加发布库聚合
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-13 15:00:39 +08:00
Scottxjw
6e197a0cce docs: 同步项目重命名后的架构文档 2026-08-13 13:29:56 +08:00
Scottxjw
645f46abcd refactor(project): 迁移项目命名并移除WPF客户端 2026-08-13 13:29:56 +08:00
144 changed files with 10173 additions and 3595 deletions

8
.gitignore vendored
View File

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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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
```
---

View File

@@ -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`

View File

@@ -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);
```

View File

@@ -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 | ✅ 发布库 |
| 双库隔离 | ✅ 无外键/导航属性 |

View File

@@ -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
```
两个数据库独立迁移,不共享迁移历史。

View File

@@ -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
{

View File

@@ -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 > Field10ploss 大于阈值时计数超过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 状态 | 含义 |

View File

@@ -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>
{

View File

@@ -18,7 +18,7 @@
```csharp
// Parsers/IParser.cs
namespace WCTDataMiner.Parsers;
namespace Gpulse.WCT.DataAnalyzer.Parsers;
public interface IParser<TRecord>
{

View File

@@ -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 拆分 | 取第一个分隔符后部分 |
| 350mW2250mW9列 | 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.dbexport 阶段仅按 (车厂, 车型, 手机厂商, 型号) 排序后原样写出。
> 所有数值列均按同一个分组键聚合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 / ΣCountQ基值 = Σ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 带 BOMRFC 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.dbaggregate 不写 local.db

View File

@@ -1,68 +1,147 @@
# Architecture: WCTDataMiner
# Architecture: Gpulse.WCT.DataAnalyzer
> 无线充电 FOD异物检测日志数据采集系统从日志文件解析 QfodPloss 数据并存储到数据库
> 无线充电参数采集系统。从 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
```

View File

@@ -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
```

View File

@@ -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 自动拆分(分隔符 `/_:`)。

View File

@@ -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 > Field10ploss 大于阈值时计数超过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;
}
```

View File

@@ -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>
{

View File

@@ -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>
{

View File

@@ -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>
{

View File

@@ -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>
{

View File

@@ -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>
{

View File

@@ -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>
{

View File

@@ -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 通过纯值映射,不依赖导航属性

View 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下有意义旧单行格式无 headerPowLoss/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

View 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 数值区间的参考;真实数据中「纯手机」与「手机+异物」分属两个日志文件。

View File

@@ -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 数据是孤立的

127
scripts/publish.ps1 Normal file
View File

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

View File

@@ -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>
/// 数据清理服务 - 支持软删除

View File

@@ -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;
}

View File

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

View File

@@ -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;
}

View File

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

View File

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

View File

@@ -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)
{

View File

@@ -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>

View File

@@ -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);

View File

@@ -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);

View File

@@ -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
);

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Parsers;
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
/// <summary>
/// 通用解析器接口

View File

@@ -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);
}
}

View File

@@ -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日志解析器

View File

@@ -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);

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Services;
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
/// <summary>
/// 阈值计算器 - 根据接收功率查表获取动态阈值

View File

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

View File

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

View File

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

View File

@@ -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 > Field10ploss 大于阈值时计数超过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 - Field9Threshold - Ploss</summary>
public int Margin => Field10 - Field9;
}

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Models;
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// Qfod记录实体 - 存储Qfod格式解析数据

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Models;
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// RX类型维度表

View File

@@ -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!;

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Models;
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// TX硬件版本维度表

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Models;
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// TX面板类型维度表

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Models;
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
/// <summary>
/// TX软件版本维度表

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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; }

View File

@@ -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;
}

View File

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

View File

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

View File

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

View File

@@ -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)

View File

@@ -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>
{

View File

@@ -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>
{

View File

@@ -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);
}
}
}

View File

@@ -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>
{

View File

@@ -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>
{

View File

@@ -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>
{

View File

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

View File

@@ -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;
}

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Data;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
/// <summary>
/// 数据库上下文工厂接口

View File

@@ -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>
/// 初始数据种子

View File

@@ -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>();

View File

@@ -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);
}
}

View File

@@ -0,0 +1,6 @@
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
public interface IReleaseDbContextFactory
{
ReleaseDbContext CreateDbContext();
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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)
{

View File

@@ -1,4 +1,4 @@
namespace WCTDataMiner.Core.Security;
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
/// <summary>
/// 安全相关常量定义

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
}
}

View 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}");
}
}

View File

@@ -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>
/// 数据清理命令

View File

@@ -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);

View File

@@ -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>
/// 解析日志文件命令

View File

@@ -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>

View 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;
}
}

View File

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

View File

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

View File

@@ -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
);

View File

@@ -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);

View File

@@ -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