feat: 实现 FOD 日志数据采集 CLI 应用
This commit is contained in:
@@ -51,11 +51,16 @@
|
||||
|
||||
---
|
||||
|
||||
## 已解决问题
|
||||
|
||||
| # | 问题 | 解决方案 |
|
||||
|---|------|----------|
|
||||
| 4 | Ploss 日志格式对应 | 修正正则表达式,添加第3个忽略字段 |
|
||||
|
||||
## 待确认问题
|
||||
|
||||
| # | 问题 | 来源 |
|
||||
|---|------|------|
|
||||
| 1 | 增量解析策略 | 日志是否持续追加? |
|
||||
| 2 | 重复解析处理 | 多次解析同一文件如何处理? |
|
||||
| 3 | FOD Type 编码含义 | 需求文档 QA 3.2 |
|
||||
| 4 | Ploss 日志格式对应 | 需求文档 QA 3.3 |
|
||||
| 3 | FOD Type 编码含义 | 需求文档 QA 3.2 |
|
||||
123
docs/architecture/parser/FileNameParser.md
Normal file
123
docs/architecture/parser/FileNameParser.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 文件名解析器
|
||||
|
||||
> 所属模块:[解析器设计](./概览.md)
|
||||
|
||||
---
|
||||
|
||||
## 1. 文件名格式
|
||||
|
||||
**格式约定**:`TX面板-TX硬件-TX软件-RX类型-测试目的-年月日-测试次数.log`
|
||||
|
||||
**分隔符**:支持 `-`(短横线)和 `_`(下划线)两种分隔符,文件名解析时将同时拆分这两种字符。
|
||||
|
||||
> ⚠️ **注意**:字段值内部不应包含分隔符字符。例如,若 TX面板值为 `single-mold`,解析时会拆分为 `single` 和 `mold` 两个字段,导致解析失败。请确保字段值本身不含 `-` 或 `_`。
|
||||
|
||||
**示例**:
|
||||
- 标准:`singleMold-v1.0-hex2_1-iPhone15-compatibility-20260702-1.log`(字段值无分隔符)
|
||||
- 避免:`single-mold-v1.0-hex2.1-iPhone15-兼容性测试-20260702-1.log`(`single-mold` 会拆分)
|
||||
|
||||
**位置说明**:表格中的"位置"采用 1-based 索引(从 1 开始计数),与代码中的 0-based 数组索引对应。
|
||||
|
||||
---
|
||||
|
||||
## 2. 字段映射
|
||||
|
||||
| 位置 | 字段 | 对应维度表 | 示例值 |
|
||||
|------|------|------------|--------|
|
||||
| 1 | TxPanel | tx_panel.name | single-mold |
|
||||
| 2 | TxHardware | tx_hardware.version | v1.0 |
|
||||
| 3 | TxSoftware | tx_software.version | hex2.1 |
|
||||
| 4 | RxType | rx_type.name | iPhone15 |
|
||||
| 5 | TestPurpose | test_scenario.test_purpose | 兼容性测试 |
|
||||
| 6 | TestDate | test_scenario.test_date | 20260702 |
|
||||
| 7 | TestSequence | test_scenario.test_sequence | 1 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 解析器实现
|
||||
|
||||
```csharp
|
||||
// Parsers/FileNameParser.cs
|
||||
using System.Globalization;
|
||||
using WCTDataMiner.Services;
|
||||
|
||||
namespace WCTDataMiner.Parsers;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 容错处理
|
||||
|
||||
| 场景 | 处理方式 |
|
||||
|------|----------|
|
||||
| 文件名为空或 null | 返回 null,ParseService 记录错误日志 |
|
||||
| 文件名仅含扩展名(如 `.log`) | 返回 null |
|
||||
| 文件名少于 7 个字段 | 返回 null,缺少必要字段 |
|
||||
| 日期格式不匹配 | 返回 null,无法解析 test_date |
|
||||
| 测试序号解析失败 | 使用默认值 1 |
|
||||
| 文件名扩展名无效(非 `.log`) | 仍会解析,仅去除扩展名后尝试 |
|
||||
|
||||
> **说明**:由于解析器要求 `parts.Length >= 7` 才会成功返回,"测试目的缺失"不会单独发生——当字段不足时整体解析失败。若需支持可选字段,需调整解析逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 5. 使用示例
|
||||
|
||||
```csharp
|
||||
// 在 ParseService 中使用
|
||||
var fileNameParser = new FileNameParser();
|
||||
var scenarioInfo = fileNameParser.Parse("single-mold-v1.0-hex2.1-iPhone15-兼容性测试-20260702-1.log");
|
||||
|
||||
if (scenarioInfo != null)
|
||||
{
|
||||
var scenario = await scenarioService.GetOrCreateScenarioAsync(scenarioInfo);
|
||||
// 继续解析文件内容...
|
||||
}
|
||||
```
|
||||
@@ -36,10 +36,13 @@
|
||||
|
||||
## 3. 正则表达式
|
||||
|
||||
> **修正说明**:原设计稿正则只有2个忽略字段,实际格式有3个X(忽略字段),已修正。
|
||||
|
||||
```csharp
|
||||
// Parsers/PlossParser.cs
|
||||
// 格式: FOD-> (1) X X X (2) (3) ... (13),其中3个X需要3个 \d+ 忽略
|
||||
private static readonly Regex PlossPattern = 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+)$",
|
||||
@"^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
|
||||
);
|
||||
```
|
||||
@@ -82,7 +85,7 @@ public class PlossParser : IParser<PlossRecord>
|
||||
|
||||
public bool CanParse(string line) => PlossPattern.IsMatch(line);
|
||||
|
||||
public ParseResult<PlossRecord> Parse(string line, int lineNumber, Guid logFileId)
|
||||
public ParseResult<PlossRecord> Parse(string line, Guid scenarioId)
|
||||
{
|
||||
var match = PlossPattern.Match(line);
|
||||
if (!match.Success)
|
||||
@@ -97,8 +100,7 @@ public class PlossParser : IParser<PlossRecord>
|
||||
{
|
||||
var record = new PlossRecord
|
||||
{
|
||||
LogFileId = logFileId,
|
||||
LineNumber = lineNumber,
|
||||
ScenarioId = scenarioId,
|
||||
RxType = byte.Parse(match.Groups[1].Value),
|
||||
RxPower = short.Parse(match.Groups[2].Value),
|
||||
TxPower = short.Parse(match.Groups[3].Value),
|
||||
|
||||
@@ -66,7 +66,7 @@ public class QfodParser : IParser<QfodRecord>
|
||||
|
||||
public bool CanParse(string line) => QfodPattern.IsMatch(line);
|
||||
|
||||
public ParseResult<QfodRecord> Parse(string line, int lineNumber, Guid logFileId)
|
||||
public ParseResult<QfodRecord> Parse(string line, Guid scenarioId)
|
||||
{
|
||||
var match = QfodPattern.Match(line);
|
||||
if (!match.Success)
|
||||
@@ -81,8 +81,7 @@ public class QfodParser : IParser<QfodRecord>
|
||||
{
|
||||
var record = new QfodRecord
|
||||
{
|
||||
LogFileId = logFileId,
|
||||
LineNumber = lineNumber,
|
||||
ScenarioId = scenarioId,
|
||||
ChargerIndex = byte.Parse(match.Groups[1].Value),
|
||||
CoilIndex = byte.Parse(match.Groups[2].Value),
|
||||
DeltaQ = int.Parse(match.Groups[3].Value),
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [文件名解析器](./FileNameParser.md) | 从文件名提取场景信息 |
|
||||
| [Qfod 解析器](./Qfod解析器.md) | Qfod 格式日志解析设计 |
|
||||
| [Ploss 解析器](./Ploss解析器.md) | Ploss FOD 格式日志解析设计 |
|
||||
|
||||
@@ -29,7 +30,9 @@ public interface IParser<TRecord>
|
||||
/// <summary>
|
||||
/// 解析日志行
|
||||
/// </summary>
|
||||
ParseResult<TRecord> Parse(string line, int lineNumber, Guid logFileId);
|
||||
/// <param name="line">日志行内容</param>
|
||||
/// <param name="scenarioId">关联的测试场景ID,用于外键关联(由 ParseService 从 FileNameParser 获取后传入)</param>
|
||||
ParseResult<TRecord> Parse(string line, Guid scenarioId);
|
||||
}
|
||||
|
||||
public record ParseResult<TRecord>
|
||||
@@ -75,4 +78,5 @@ flowchart TD
|
||||
|-----------|------|----------|
|
||||
| FORMAT_MISMATCH | 行格式不匹配正则表达式 | 跳过该行,记录原始内容 |
|
||||
| TYPE_CONVERSION | 字段类型转换失败 | 跳过该行,记录错误字段 |
|
||||
| VALUE_OUT_OF_RANGE | 字段值超出有效范围 | 使用边界值或跳过 |
|
||||
|
||||
> **说明**:VALUE_OUT_OF_RANGE 错误类型暂未实现,后续可根据需要添加字段值范围校验逻辑。
|
||||
@@ -10,47 +10,52 @@
|
||||
sequenceDiagram
|
||||
participant CLI as CLI Entry
|
||||
participant PS as ParseService
|
||||
participant LR as LogFileReader
|
||||
participant SS as ScenarioService
|
||||
participant FNP as FileNameParser
|
||||
participant QP as QfodParser
|
||||
participant PP as PlossParser
|
||||
participant EHS as ErrorHandlingService
|
||||
participant DB as Database
|
||||
participant Log as Serilog
|
||||
|
||||
CLI->>PS: 扫描日志目录
|
||||
PS-->>CLI: 返回文件列表
|
||||
|
||||
CLI->>LR: 扫描日志目录
|
||||
LR-->>CLI: 返回文件列表
|
||||
|
||||
loop 每个日志文件
|
||||
CLI->>PS: ParseFileAsync(filePath)
|
||||
|
||||
PS->>DB: INSERT log_file (status=processing)
|
||||
DB-->>PS: 返回 file_id
|
||||
|
||||
PS->>LR: 读取文件内容
|
||||
LR-->>PS: 返回行列表
|
||||
|
||||
|
||||
PS->>FNP: 解析文件名
|
||||
FNP-->>PS: 场景信息(面板、版本、RX类型等)
|
||||
|
||||
PS->>SS: GetOrCreateScenarioAsync(场景信息)
|
||||
SS->>DB: 查询/创建维度记录
|
||||
SS->>DB: 查询/创建场景记录
|
||||
SS-->>PS: 返回 TestScenario
|
||||
|
||||
PS->>PS: 读取文件内容
|
||||
|
||||
loop 逐行遍历
|
||||
alt 是 Qfod 行
|
||||
PS->>QP: Parse(line)
|
||||
PS->>QP: Parse(line, scenarioId)
|
||||
QP-->>PS: ParseResult
|
||||
else 是 Ploss 行
|
||||
PS->>PP: Parse(line)
|
||||
PS->>PP: Parse(line, scenarioId)
|
||||
PP-->>PS: ParseResult
|
||||
end
|
||||
|
||||
|
||||
alt 解析成功
|
||||
PS->>PS: 加入记录列表
|
||||
else 解析失败
|
||||
PS->>EHS: RecordError(line, error)
|
||||
PS->>Log: LogError(line, error)
|
||||
end
|
||||
end
|
||||
|
||||
PS->>DB: BATCH INSERT records
|
||||
PS->>DB: BATCH INSERT errors
|
||||
PS->>DB: UPDATE log_file (counts, status=completed)
|
||||
|
||||
|
||||
PS->>DB: BATCH INSERT QfodRecords
|
||||
PS->>DB: BATCH INSERT PlossRecords
|
||||
PS->>DB: UPDATE scenario (qfod_count, ploss_count)
|
||||
|
||||
PS-->>CLI: 返回 ParseReport
|
||||
end
|
||||
|
||||
|
||||
CLI-->>CLI: 输出解析报告
|
||||
```
|
||||
|
||||
@@ -61,6 +66,7 @@ sequenceDiagram
|
||||
```csharp
|
||||
// Services/ParseService.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
using WCTDataMiner.Data;
|
||||
using WCTDataMiner.Models;
|
||||
using WCTDataMiner.Parsers;
|
||||
@@ -70,36 +76,47 @@ namespace WCTDataMiner.Services;
|
||||
public class ParseService
|
||||
{
|
||||
private readonly WctMinerDbContext _context;
|
||||
private readonly ScenarioService _scenarioService;
|
||||
private readonly QfodParser _qfodParser;
|
||||
private readonly PlossParser _plossParser;
|
||||
private readonly ErrorHandlingService _errorService;
|
||||
private readonly ILogger<ParseService> _logger;
|
||||
private readonly FileNameParser _fileNameParser;
|
||||
private readonly int _batchSize;
|
||||
|
||||
public ParseService(
|
||||
WctMinerDbContext context,
|
||||
ScenarioService scenarioService,
|
||||
QfodParser qfodParser,
|
||||
PlossParser plossParser,
|
||||
ErrorHandlingService errorService,
|
||||
ILogger<ParseService> logger,
|
||||
FileNameParser fileNameParser,
|
||||
IConfiguration config)
|
||||
{
|
||||
_context = context;
|
||||
_scenarioService = scenarioService;
|
||||
_qfodParser = qfodParser;
|
||||
_plossParser = plossParser;
|
||||
_errorService = errorService;
|
||||
_logger = logger;
|
||||
_fileNameParser = fileNameParser;
|
||||
_batchSize = config.GetValue("Parser:BatchSize", 1000);
|
||||
}
|
||||
|
||||
public async Task<ParseReport> ParseFileAsync(string filePath)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
var logFile = await CreateLogFileRecordAsync(fileInfo);
|
||||
|
||||
|
||||
// 解析文件名获取场景信息
|
||||
var scenarioInfo = _fileNameParser.Parse(fileInfo.Name);
|
||||
if (scenarioInfo == null)
|
||||
{
|
||||
Log.Error("无法解析文件名: {FileName}", fileInfo.Name);
|
||||
return new ParseReport(fileInfo.Name, 0, 0, 1, "文件名格式不匹配");
|
||||
}
|
||||
|
||||
// 获取或创建测试场景
|
||||
var scenario = await _scenarioService.GetOrCreateScenarioAsync(scenarioInfo);
|
||||
|
||||
var lines = await File.ReadAllLinesAsync(filePath);
|
||||
var qfodRecords = new List<QfodRecord>();
|
||||
var plossRecords = new List<PlossRecord>();
|
||||
int errorCount = 0;
|
||||
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
@@ -108,53 +125,45 @@ public class ParseService
|
||||
|
||||
if (_qfodParser.CanParse(line))
|
||||
{
|
||||
var result = _qfodParser.Parse(line, lineNumber, logFile.Id);
|
||||
var result = _qfodParser.Parse(line, scenario.Id);
|
||||
if (result.IsSuccess)
|
||||
qfodRecords.Add(result.Record!);
|
||||
else
|
||||
_errorService.RecordError(logFile.Id, lineNumber, line, result.ErrorType!, result.ErrorMessage!);
|
||||
{
|
||||
Log.Error("Qfod解析失败 [{Line}] {Error}: {Message}",
|
||||
lineNumber, result.ErrorType, result.ErrorMessage);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
else if (_plossParser.CanParse(line))
|
||||
{
|
||||
var result = _plossParser.Parse(line, lineNumber, logFile.Id);
|
||||
var result = _plossParser.Parse(line, scenario.Id);
|
||||
if (result.IsSuccess)
|
||||
plossRecords.Add(result.Record!);
|
||||
else
|
||||
_errorService.RecordError(logFile.Id, lineNumber, line, result.ErrorType!, result.ErrorMessage!);
|
||||
{
|
||||
Log.Error("Ploss解析失败 [{Line}] {Error}: {Message}",
|
||||
lineNumber, result.ErrorType, result.ErrorMessage);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量保存
|
||||
await BatchInsertAsync(qfodRecords, plossRecords, _errorService.GetErrors());
|
||||
|
||||
// 更新统计
|
||||
await UpdateLogFileStatsAsync(logFile.Id, qfodRecords.Count, plossRecords.Count, _errorService.ErrorCount);
|
||||
await BatchInsertAsync(qfodRecords, plossRecords);
|
||||
|
||||
return new ParseReport(fileInfo.Name, qfodRecords.Count, plossRecords.Count, _errorService.ErrorCount);
|
||||
}
|
||||
// 更新场景统计
|
||||
await UpdateScenarioStatsAsync(scenario.Id, qfodRecords.Count, plossRecords.Count);
|
||||
|
||||
private async Task<LogFile> CreateLogFileRecordAsync(FileInfo fileInfo)
|
||||
{
|
||||
var logFile = new LogFile
|
||||
{
|
||||
FileName = fileInfo.Name,
|
||||
FilePath = fileInfo.FullName,
|
||||
FileSize = fileInfo.Length,
|
||||
Status = ParseStatus.Processing
|
||||
};
|
||||
|
||||
_context.LogFiles.Add(logFile);
|
||||
await _context.SaveChangesAsync();
|
||||
return logFile;
|
||||
return new ParseReport(fileInfo.Name, qfodRecords.Count, plossRecords.Count, errorCount, null);
|
||||
}
|
||||
|
||||
private async Task BatchInsertAsync(
|
||||
List<QfodRecord> qfodRecords,
|
||||
List<PlossRecord> plossRecords,
|
||||
List<ParseError> errors)
|
||||
List<PlossRecord> plossRecords)
|
||||
{
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
// 批量插入 QfodRecord
|
||||
@@ -171,13 +180,6 @@ public class ParseService
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// 批量插入 ParseError
|
||||
foreach (var batch in errors.Chunk(_batchSize))
|
||||
{
|
||||
_context.ParseErrors.AddRange(batch);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
catch
|
||||
@@ -187,19 +189,28 @@ public class ParseService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateLogFileStatsAsync(Guid logFileId, int qfodCount, int plossCount, int errorCount)
|
||||
private async Task UpdateScenarioStatsAsync(Guid scenarioId, int qfodCount, int plossCount)
|
||||
{
|
||||
var logFile = await _context.LogFiles.FindAsync(logFileId);
|
||||
if (logFile != null)
|
||||
// 使用累加更新(+=),支持同一场景的多次解析
|
||||
// 若需幂等解析,应在 ParseFileAsync 开始时检查场景是否已处理
|
||||
var scenario = await _context.TestScenarios.FindAsync(scenarioId);
|
||||
if (scenario != null)
|
||||
{
|
||||
logFile.QfodCount = qfodCount;
|
||||
logFile.PlossCount = plossCount;
|
||||
logFile.ErrorCount = errorCount;
|
||||
logFile.Status = ParseStatus.Completed;
|
||||
scenario.QfodCount += qfodCount;
|
||||
scenario.PlossCount += plossCount;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record ParseReport(string FileName, int QfodCount, int PlossCount, int ErrorCount);
|
||||
public record ParseReport(string FileName, int QfodCount, int PlossCount, int ErrorCount, string? ErrorMessage);
|
||||
|
||||
public record ScenarioInfo(
|
||||
string TxPanel,
|
||||
string TxHardware,
|
||||
string TxSoftware,
|
||||
string RxType,
|
||||
string? TestPurpose,
|
||||
DateOnly TestDate,
|
||||
int TestSequence);
|
||||
```
|
||||
@@ -13,11 +13,11 @@
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Service Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ParseService │ ErrorHandlingService │ ThresholdCalc │
|
||||
│ ParseService │ ScenarioService │ DimensionService │ ThresholdCalc │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Parser Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ QfodParser │ PlossParser │ LogFileReader │
|
||||
│ QfodParser │ PlossParser │ FileNameParser │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Data Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
@@ -36,15 +36,18 @@
|
||||
| 模块名 | 职责 | 依赖模块 |
|
||||
|--------|------|----------|
|
||||
| CLI Entry | 接收命令行参数、调度解析流程 | ParseService, Infrastructure |
|
||||
| ParseService | 协调解析流程、批量写入、统计更新 | Parser Layer, DbContext, Infrastructure |
|
||||
| ParseService | 协调解析流程、批量写入、场景关联 | Parser Layer, ScenarioService, DbContext |
|
||||
| ScenarioService | 创建/查询测试场景,解析文件名获取场景信息 | DimensionService, DbContext |
|
||||
| DimensionService | 维度表 CRUD(获取或创建维度记录) | DbContext |
|
||||
| FileNameParser | 解析文件名提取场景信息(面板、版本、RX类型等) | 无 |
|
||||
| LogFileReader | 读取日志文件、行过滤、文件遍历 | 无 |
|
||||
| QfodParser | 解析 Qfod 格式日志,返回 Record | LogFileReader |
|
||||
| PlossParser | 解析 Ploss 格式日志,返回 Record | LogFileReader |
|
||||
| Models | 定义数据实体结构 | 无 |
|
||||
| WctMinerDbContext | EF Core 数据库上下文 | Models |
|
||||
| QfodParser | 解析 Qfod 格式日志,返回 QfodRecord | 无 |
|
||||
| PlossParser | 解析 Ploss 格式日志,返回 PlossRecord | 无 |
|
||||
| Models | 定义数据实体结构(维度表 + 事实表) | 无 |
|
||||
| WctMinerDbContext | EF Core 数据库上下文 | Models, Configurations |
|
||||
| Configuration | 加载配置文件 | 无 |
|
||||
| Serilog | 结构化日志输出 | 无 |
|
||||
| ErrorHandlingService | 解析错误记录、错误统计 | 无 |
|
||||
| Serilog | 结构化日志输出(含解析错误日志) | 无 |
|
||||
| ThresholdCalculator | 根据 RxPower 计算动态阈值 | 无 |
|
||||
|
||||
---
|
||||
|
||||
@@ -55,10 +58,10 @@
|
||||
| 层级 | 职责 | 允许调用 |
|
||||
|------|------|----------|
|
||||
| CLI Entry | 接收参数、流程调度、结果输出 | Service, Infrastructure |
|
||||
| Service | 协调解析流程、批量写入、统计管理 | Parser, Data, Infrastructure |
|
||||
| Parser | 日志解析、数据提取、返回 Record | 无 |
|
||||
| Data | 数据持久化、DbContext 管理 | Infrastructure |
|
||||
| Infrastructure | 配置、日志、错误处理 | 无 |
|
||||
| Service | 协调解析流程、场景管理、维度管理、批量写入 | Parser, Data, Infrastructure |
|
||||
| Parser | 日志解析、文件名解析、数据提取 | 无 |
|
||||
| Data | 数据持久化、DbContext 管理、实体配置 | Infrastructure |
|
||||
| Infrastructure | 配置、日志 | 无 |
|
||||
|
||||
### 3.2 依赖规则
|
||||
|
||||
@@ -66,6 +69,7 @@
|
||||
- 下层不可调用上层
|
||||
- Parser 之间不可互相调用(独立解析器)
|
||||
- Service 层协调 Parser 和 Data 层
|
||||
- 解析错误通过 Serilog 记录,不存入业务数据库
|
||||
|
||||
---
|
||||
|
||||
@@ -75,35 +79,46 @@
|
||||
graph TD
|
||||
CLI[CLI Entry] --> PS[ParseService]
|
||||
CLI --> Serilog
|
||||
CLI --> EHS[ErrorHandlingService]
|
||||
|
||||
PS --> SS[ScenarioService]
|
||||
PS --> QP[QfodParser]
|
||||
PS --> PP[PlossParser]
|
||||
PS --> DBM[WctMinerDbContext]
|
||||
PS --> EHS
|
||||
PS --> Serilog
|
||||
PS --> Config[Configuration]
|
||||
|
||||
QP --> LFR[LogFileReader]
|
||||
PP --> LFR
|
||||
SS --> DS[DimensionService]
|
||||
SS --> FNP[FileNameParser]
|
||||
SS --> DBM
|
||||
|
||||
DS --> DBM
|
||||
|
||||
QP --> Serilog
|
||||
PP --> Serilog
|
||||
FNP --> Serilog
|
||||
|
||||
DBM --> Models[Models]
|
||||
DBM --> Configs[Configurations]
|
||||
|
||||
subgraph Parser Layer
|
||||
QP
|
||||
PP
|
||||
LFR
|
||||
FNP
|
||||
end
|
||||
|
||||
subgraph Data Layer
|
||||
DBM
|
||||
Models
|
||||
Configs
|
||||
end
|
||||
|
||||
subgraph Infrastructure
|
||||
Config
|
||||
Serilog
|
||||
EHS
|
||||
end
|
||||
|
||||
subgraph Service Layer
|
||||
PS
|
||||
SS
|
||||
DS
|
||||
end
|
||||
```
|
||||
|
||||
@@ -117,20 +132,32 @@ src/WCTDataMiner/
|
||||
│ ├── IParser.cs
|
||||
│ ├── QfodParser.cs
|
||||
│ ├── PlossParser.cs
|
||||
│ └── LogFileReader.cs
|
||||
│ ├── FileNameParser.cs # 解析文件名获取场景信息(Parser Layer)
|
||||
│ └── LogFileReader.cs # 读取日志文件(可选)
|
||||
├── Models/
|
||||
│ ├── LogFile.cs
|
||||
│ ├── QfodRecord.cs
|
||||
│ ├── PlossRecord.cs
|
||||
│ ├── ParseError.cs
|
||||
│ └── ParseStatus.cs
|
||||
│ ├── 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
|
||||
│ ├── IDbContextFactory.cs
|
||||
│ └── Configurations/ # IEntityTypeConfiguration 配置类
|
||||
│ ├── TxPanelConfiguration.cs
|
||||
│ ├── TxHardwareConfiguration.cs
|
||||
│ ├── TxSoftwareConfiguration.cs
|
||||
│ ├── RxTypeConfiguration.cs
|
||||
│ ├── TestScenarioConfiguration.cs
|
||||
│ ├── QfodRecordConfiguration.cs
|
||||
│ └── PlossRecordConfiguration.cs
|
||||
├── Services/
|
||||
│ ├── ParseService.cs
|
||||
│ ├── ThresholdCalculator.cs
|
||||
│ └── ErrorHandlingService.cs
|
||||
│ ├── ParseService.cs # 解析流程协调
|
||||
│ ├── ScenarioService.cs # 场景创建/查询
|
||||
│ ├── DimensionService.cs # 维度表管理
|
||||
│ └── ThresholdCalculator.cs # 阈值计算
|
||||
├── Configuration/
|
||||
│ ├── AppSettings.cs
|
||||
│ └── appsettings.json
|
||||
|
||||
@@ -14,22 +14,24 @@
|
||||
|
||||
## 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 | SMALLINT | NOT NULL | 接收端功率(mW) |
|
||||
| tx_power | SMALLINT | NOT NULL | 发射端功率(mW) |
|
||||
| vcoil | SMALLINT | NOT NULL | 发射线圈电压 |
|
||||
| vin | SMALLINT | NOT NULL | 输入电压 |
|
||||
| isns | SMALLINT | 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 | SMALLINT | NOT NULL, DEFAULT 0 | 触发计数器 |
|
||||
| fod_result | SMALLINT | NOT NULL, DEFAULT 0 | FOD判定结果 |
|
||||
| trigger_count | INT | NOT NULL, DEFAULT 0 | 触发计数器 |
|
||||
| fod_result | INT | NOT NULL, DEFAULT 0 | FOD判定结果 |
|
||||
| protocol_type | TINYINT | NOT NULL | 充电协议类型 |
|
||||
| pwm_duty | SMALLINT | NOT NULL | PWM占空比 |
|
||||
| pwm_duty | INT | NOT NULL | PWM占空比,原 uint16 |
|
||||
| coil_index | TINYINT | NOT NULL | 充电线圈索引 |
|
||||
| is_deleted | BOOLEAN | NOT NULL, DEFAULT FALSE | 软删除标记 |
|
||||
| created_at | TIMESTAMP | NOT NULL, DEFAULT NOW() | 创建时间 |
|
||||
@@ -52,6 +54,8 @@
|
||||
|------|----------|------|
|
||||
| margin | threshold - ploss | 安全余量(>0 安全,<0 危险) |
|
||||
|
||||
> **使用限制**:`Margin` 属性通过 EF Core `Ignore()` 排除映射,仅用于内存计算。不可在数据库查询中直接使用(如 `WHERE Margin > 0`),需改用 `WHERE (threshold - ploss) > 0` 或先加载到内存再计算。
|
||||
|
||||
---
|
||||
|
||||
## 5. 索引
|
||||
@@ -68,100 +72,151 @@
|
||||
|
||||
## 6. EF Core 模型
|
||||
|
||||
### 实体类
|
||||
|
||||
```csharp
|
||||
// Models/PlossRecord.cs
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace WCTDataMiner.Models;
|
||||
|
||||
[Table("ploss_record")]
|
||||
[Index(nameof(ScenarioId), Name = "idx_ploss_scenario")]
|
||||
[Index(nameof(RxPower), Name = "idx_ploss_rx_power")]
|
||||
[Index(nameof(Ploss), Name = "idx_ploss_ploss")]
|
||||
[Index(nameof(Threshold), Name = "idx_ploss_threshold")]
|
||||
[Index(nameof(FodResult), Name = "idx_ploss_fod_result")]
|
||||
public class PlossRecord
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
[Column("scenario_id")]
|
||||
public Guid ScenarioId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("rx_type")]
|
||||
public byte RxType { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("rx_power")]
|
||||
public short RxPower { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("tx_power")]
|
||||
public short TxPower { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("vcoil")]
|
||||
public short Vcoil { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("vin")]
|
||||
public short Vin { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("isns")]
|
||||
public short Isns { get; set; }
|
||||
|
||||
/// <summary>核心判定值:负数表示安全</summary>
|
||||
[Required]
|
||||
[Column("ploss")]
|
||||
public int Ploss { get; set; }
|
||||
|
||||
/// <summary>安全红线:当前功率段阈值</summary>
|
||||
[Required]
|
||||
[Column("threshold")]
|
||||
public int Threshold { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("trigger_count")]
|
||||
public short TriggerCount { get; set; } = 0;
|
||||
|
||||
[Required]
|
||||
[Column("fod_result")]
|
||||
public short FodResult { get; set; } = 0;
|
||||
|
||||
[Required]
|
||||
[Column("protocol_type")]
|
||||
public byte ProtocolType { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("pwm_duty")]
|
||||
public short PwmDuty { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("coil_index")]
|
||||
public byte CoilIndex { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("is_deleted")]
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
|
||||
[Required]
|
||||
[Column("created_at")]
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[ForeignKey(nameof(ScenarioId))]
|
||||
// Navigation Properties
|
||||
public TestScenario Scenario { get; set; } = null!;
|
||||
|
||||
/// <summary>派生属性:安全余量</summary>
|
||||
[NotMapped]
|
||||
/// <summary>派生属性:安全余量(不落库)</summary>
|
||||
public int Margin => Threshold - Ploss;
|
||||
}
|
||||
```
|
||||
|
||||
### 配置类
|
||||
|
||||
```csharp
|
||||
// Data/Configurations/PlossRecordConfiguration.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
|
||||
public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlossRecord> builder)
|
||||
{
|
||||
builder.ToTable("ploss_record");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).HasColumnName("id");
|
||||
|
||||
builder.Property(e => e.ScenarioId)
|
||||
.IsRequired()
|
||||
.HasColumnName("scenario_id");
|
||||
|
||||
builder.Property(e => e.RxType)
|
||||
.IsRequired()
|
||||
.HasColumnName("rx_type");
|
||||
|
||||
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.IsDeleted)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
builder.Property(e => e.CreatedAt)
|
||||
.IsRequired()
|
||||
.HasDefaultValueSql("NOW()")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
// 派生属性不落库
|
||||
builder.Ignore(e => e.Margin);
|
||||
|
||||
// 索引
|
||||
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.HasOne(e => e.Scenario)
|
||||
.WithMany(e => e.PlossRecords)
|
||||
.HasForeignKey(e => e.ScenarioId)
|
||||
.IsRequired();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 动态阈值表
|
||||
|
||||
@@ -55,69 +55,112 @@
|
||||
| chk_qfod_charger | CHECK | charger_index IN (0, 1) |
|
||||
| chk_qfod_coil | CHECK | coil_index IN (0, 1, 2) |
|
||||
|
||||
> **数据库兼容性**:`IN (0, 1)` 语法在 SQLite、PostgreSQL、MySQL 中均可使用。若使用其他数据库,需验证 CHECK 约束语法兼容性。
|
||||
|
||||
---
|
||||
|
||||
## 6. EF Core 模型
|
||||
|
||||
### 实体类
|
||||
|
||||
```csharp
|
||||
// Models/QfodRecord.cs
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace WCTDataMiner.Models;
|
||||
|
||||
[Table("qfod_record")]
|
||||
[Index(nameof(ScenarioId), Name = "idx_qfod_scenario")]
|
||||
[Index(nameof(ChargerIndex), nameof(CoilIndex), Name = "idx_qfod_charger_coil")]
|
||||
[Index(nameof(FodType), Name = "idx_qfod_fod_type")]
|
||||
[Index(nameof(DeltaQ), Name = "idx_qfod_delta_q")]
|
||||
public class QfodRecord
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
[Column("scenario_id")]
|
||||
public Guid ScenarioId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("charger_index")]
|
||||
public byte ChargerIndex { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("coil_index")]
|
||||
public byte CoilIndex { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("delta_q")]
|
||||
public int DeltaQ { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("current_q")]
|
||||
public float CurrentQ { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("raw_q")]
|
||||
public float RawQ { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("fod_type")]
|
||||
public byte FodType { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("is_deleted")]
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
|
||||
[Required]
|
||||
[Column("created_at")]
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[ForeignKey(nameof(ScenarioId))]
|
||||
// Navigation Properties
|
||||
public TestScenario Scenario { get; set; } = null!;
|
||||
}
|
||||
```
|
||||
|
||||
### 配置类
|
||||
|
||||
```csharp
|
||||
// Data/Configurations/QfodRecordConfiguration.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
|
||||
public class QfodRecordConfiguration : IEntityTypeConfiguration<QfodRecord>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<QfodRecord> builder)
|
||||
{
|
||||
builder.ToTable("qfod_record");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).HasColumnName("id");
|
||||
|
||||
builder.Property(e => e.ScenarioId)
|
||||
.IsRequired()
|
||||
.HasColumnName("scenario_id");
|
||||
|
||||
builder.Property(e => e.ChargerIndex)
|
||||
.IsRequired()
|
||||
.HasColumnName("charger_index");
|
||||
|
||||
builder.Property(e => e.CoilIndex)
|
||||
.IsRequired()
|
||||
.HasColumnName("coil_index");
|
||||
|
||||
builder.Property(e => e.DeltaQ)
|
||||
.IsRequired()
|
||||
.HasColumnName("delta_q");
|
||||
|
||||
builder.Property(e => e.CurrentQ)
|
||||
.IsRequired()
|
||||
.HasColumnName("current_q");
|
||||
|
||||
builder.Property(e => e.RawQ)
|
||||
.IsRequired()
|
||||
.HasColumnName("raw_q");
|
||||
|
||||
builder.Property(e => e.FodType)
|
||||
.IsRequired()
|
||||
.HasColumnName("fod_type");
|
||||
|
||||
builder.Property(e => e.IsDeleted)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
builder.Property(e => e.CreatedAt)
|
||||
.IsRequired()
|
||||
.HasDefaultValueSql("NOW()")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
// 索引
|
||||
builder.HasIndex(e => e.ScenarioId).HasDatabaseName("idx_qfod_scenario");
|
||||
builder.HasIndex(e => new { e.ChargerIndex, e.CoilIndex }).HasDatabaseName("idx_qfod_charger_coil");
|
||||
builder.HasIndex(e => e.FodType).HasDatabaseName("idx_qfod_fod_type");
|
||||
builder.HasIndex(e => e.DeltaQ).HasDatabaseName("idx_qfod_delta_q");
|
||||
|
||||
// CHECK 约束
|
||||
builder.HasCheckConstraint("chk_qfod_charger", "charger_index IN (0, 1)");
|
||||
builder.HasCheckConstraint("chk_qfod_coil", "coil_index IN (0, 1, 2)");
|
||||
|
||||
// 关系配置
|
||||
builder.HasOne(e => e.Scenario)
|
||||
.WithMany(e => e.QfodRecords)
|
||||
.HasForeignKey(e => e.ScenarioId)
|
||||
.IsRequired();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 检测通道组合
|
||||
|
||||
@@ -34,31 +34,17 @@
|
||||
|
||||
## 4. EF Core 模型
|
||||
|
||||
### 实体类
|
||||
|
||||
```csharp
|
||||
// Models/RxType.cs
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace WCTDataMiner.Models;
|
||||
|
||||
[Table("rx_type")]
|
||||
public class RxType
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
[MaxLength(100)]
|
||||
[Column("name")]
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
[Column("is_deleted")]
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
|
||||
[Required]
|
||||
[Column("created_at")]
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation Properties
|
||||
@@ -66,6 +52,45 @@ public class RxType
|
||||
}
|
||||
```
|
||||
|
||||
### 配置类
|
||||
|
||||
```csharp
|
||||
// Data/Configurations/RxTypeConfiguration.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
|
||||
public class RxTypeConfiguration : IEntityTypeConfiguration<RxType>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<RxType> builder)
|
||||
{
|
||||
builder.ToTable("rx_type");
|
||||
|
||||
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("NOW()")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
builder.HasIndex(e => e.Name).IsUnique();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 使用说明
|
||||
|
||||
@@ -25,31 +25,17 @@
|
||||
|
||||
## 3. EF Core 模型
|
||||
|
||||
### 实体类
|
||||
|
||||
```csharp
|
||||
// Models/TxHardware.cs
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace WCTDataMiner.Models;
|
||||
|
||||
[Table("tx_hardware")]
|
||||
public class TxHardware
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
[Column("version")]
|
||||
public string Version { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
[Column("is_deleted")]
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
|
||||
[Required]
|
||||
[Column("created_at")]
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation Properties
|
||||
@@ -57,6 +43,45 @@ public class TxHardware
|
||||
}
|
||||
```
|
||||
|
||||
### 配置类
|
||||
|
||||
```csharp
|
||||
// Data/Configurations/TxHardwareConfiguration.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
|
||||
public class TxHardwareConfiguration : IEntityTypeConfiguration<TxHardware>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TxHardware> builder)
|
||||
{
|
||||
builder.ToTable("tx_hardware");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).HasColumnName("id");
|
||||
|
||||
builder.Property(e => e.Version)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnName("version");
|
||||
|
||||
builder.Property(e => e.IsDeleted)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
builder.Property(e => e.CreatedAt)
|
||||
.IsRequired()
|
||||
.HasDefaultValueSql("NOW()")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
builder.HasIndex(e => e.Version).IsUnique();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 使用说明
|
||||
|
||||
@@ -39,31 +39,17 @@
|
||||
|
||||
## 4. EF Core 模型
|
||||
|
||||
### 实体类
|
||||
|
||||
```csharp
|
||||
// Models/TxPanel.cs
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace WCTDataMiner.Models;
|
||||
|
||||
[Table("tx_panel")]
|
||||
public class TxPanel
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
[Column("name")]
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
[Column("is_deleted")]
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
|
||||
[Required]
|
||||
[Column("created_at")]
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation Properties
|
||||
@@ -71,6 +57,45 @@ public class TxPanel
|
||||
}
|
||||
```
|
||||
|
||||
### 配置类
|
||||
|
||||
```csharp
|
||||
// Data/Configurations/TxPanelConfiguration.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
|
||||
public class TxPanelConfiguration : IEntityTypeConfiguration<TxPanel>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TxPanel> builder)
|
||||
{
|
||||
builder.ToTable("tx_panel");
|
||||
|
||||
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("NOW()")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
builder.HasIndex(e => e.Name).IsUnique();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 初始化数据
|
||||
|
||||
@@ -25,31 +25,17 @@
|
||||
|
||||
## 3. EF Core 模型
|
||||
|
||||
### 实体类
|
||||
|
||||
```csharp
|
||||
// Models/TxSoftware.cs
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace WCTDataMiner.Models;
|
||||
|
||||
[Table("tx_software")]
|
||||
public class TxSoftware
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
[MaxLength(50)]
|
||||
[Column("version")]
|
||||
public string Version { get; set; } = null!;
|
||||
|
||||
[Required]
|
||||
[Column("is_deleted")]
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
|
||||
[Required]
|
||||
[Column("created_at")]
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation Properties
|
||||
@@ -57,6 +43,45 @@ public class TxSoftware
|
||||
}
|
||||
```
|
||||
|
||||
### 配置类
|
||||
|
||||
```csharp
|
||||
// Data/Configurations/TxSoftwareConfiguration.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
|
||||
public class TxSoftwareConfiguration : IEntityTypeConfiguration<TxSoftware>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TxSoftware> builder)
|
||||
{
|
||||
builder.ToTable("tx_software");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).HasColumnName("id");
|
||||
|
||||
builder.Property(e => e.Version)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnName("version");
|
||||
|
||||
builder.Property(e => e.IsDeleted)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
builder.Property(e => e.CreatedAt)
|
||||
.IsRequired()
|
||||
.HasDefaultValueSql("NOW()")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
builder.HasIndex(e => e.Version).IsUnique();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 使用说明
|
||||
|
||||
@@ -60,84 +60,42 @@
|
||||
|--------|----------|------|
|
||||
| uq_scenario | (tx_panel_id, tx_hardware_id, tx_software_id, rx_type_id, test_purpose, test_date, test_sequence) | 相同属性组合不重复创建 |
|
||||
|
||||
> **关于 test_purpose 的 NULL 处理**:由于 `test_purpose` 是可空字段,含 NULL 的唯一约束在不同数据库中行为不同:
|
||||
> - **PostgreSQL**:默认允许多个 NULL(被视为不同值),可通过 `NULLS NOT DISTINCT` 约束行为
|
||||
> - **SQL Server**:只允许一个 NULL
|
||||
> - **SQLite/MySQL**:允多个 NULL
|
||||
>
|
||||
> 若需严格幂等(无论 test_purpose 是否为 NULL 都不重复),建议使用 filtered index 或将空值转为默认字符串(如 `(empty)`)。
|
||||
|
||||
---
|
||||
|
||||
## 6. EF Core 模型
|
||||
|
||||
### 实体类
|
||||
|
||||
```csharp
|
||||
// Models/TestScenario.cs
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace WCTDataMiner.Models;
|
||||
|
||||
[Table("test_scenario")]
|
||||
[Index(nameof(TxPanelId), Name = "idx_scenario_tx_panel")]
|
||||
[Index(nameof(TxHardwareId), Name = "idx_scenario_tx_hardware")]
|
||||
[Index(nameof(TxSoftwareId), Name = "idx_scenario_tx_software")]
|
||||
[Index(nameof(RxTypeId), Name = "idx_scenario_rx_type")]
|
||||
[Index(nameof(TestDate), Name = "idx_scenario_test_date")]
|
||||
public class TestScenario
|
||||
{
|
||||
[Key]
|
||||
[Column("id")]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
[Column("tx_panel_id")]
|
||||
public Guid TxPanelId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("tx_hardware_id")]
|
||||
public Guid TxHardwareId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("tx_software_id")]
|
||||
public Guid TxSoftwareId { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("rx_type_id")]
|
||||
public Guid RxTypeId { get; set; }
|
||||
|
||||
[MaxLength(200)]
|
||||
[Column("test_purpose")]
|
||||
public string? TestPurpose { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("test_date")]
|
||||
public DateOnly TestDate { get; set; }
|
||||
|
||||
[Required]
|
||||
[Column("test_sequence")]
|
||||
public int TestSequence { get; set; } = 1;
|
||||
|
||||
[Required]
|
||||
[Column("qfod_count")]
|
||||
public int QfodCount { get; set; } = 0;
|
||||
|
||||
[Required]
|
||||
[Column("ploss_count")]
|
||||
public int PlossCount { get; set; } = 0;
|
||||
|
||||
[Required]
|
||||
[Column("is_deleted")]
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
|
||||
[Required]
|
||||
[Column("created_at")]
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation Properties - 维度表
|
||||
[ForeignKey(nameof(TxPanelId))]
|
||||
public TxPanel TxPanel { get; set; } = null!;
|
||||
|
||||
[ForeignKey(nameof(TxHardwareId))]
|
||||
public TxHardware TxHardware { get; set; } = null!;
|
||||
|
||||
[ForeignKey(nameof(TxSoftwareId))]
|
||||
public TxSoftware TxSoftware { get; set; } = null!;
|
||||
|
||||
[ForeignKey(nameof(RxTypeId))]
|
||||
public RxType RxType { get; set; } = null!;
|
||||
|
||||
// Navigation Properties - 数据记录
|
||||
@@ -146,6 +104,118 @@ public class TestScenario
|
||||
}
|
||||
```
|
||||
|
||||
### 配置类
|
||||
|
||||
```csharp
|
||||
// Data/Configurations/TestScenarioConfiguration.cs
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
|
||||
public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TestScenario> builder)
|
||||
{
|
||||
builder.ToTable("test_scenario");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).HasColumnName("id");
|
||||
|
||||
builder.Property(e => e.TxPanelId)
|
||||
.IsRequired()
|
||||
.HasColumnName("tx_panel_id");
|
||||
|
||||
builder.Property(e => e.TxHardwareId)
|
||||
.IsRequired()
|
||||
.HasColumnName("tx_hardware_id");
|
||||
|
||||
builder.Property(e => e.TxSoftwareId)
|
||||
.IsRequired()
|
||||
.HasColumnName("tx_software_id");
|
||||
|
||||
builder.Property(e => e.RxTypeId)
|
||||
.IsRequired()
|
||||
.HasColumnName("rx_type_id");
|
||||
|
||||
builder.Property(e => e.TestPurpose)
|
||||
.HasMaxLength(200)
|
||||
.HasColumnName("test_purpose");
|
||||
|
||||
builder.Property(e => e.TestDate)
|
||||
.IsRequired()
|
||||
.HasColumnName("test_date");
|
||||
|
||||
builder.Property(e => e.TestSequence)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(1)
|
||||
.HasColumnName("test_sequence");
|
||||
|
||||
builder.Property(e => e.QfodCount)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("qfod_count");
|
||||
|
||||
builder.Property(e => e.PlossCount)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("ploss_count");
|
||||
|
||||
builder.Property(e => e.IsDeleted)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
builder.Property(e => e.CreatedAt)
|
||||
.IsRequired()
|
||||
.HasDefaultValueSql("NOW()")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
// 索引
|
||||
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.TestDate).HasDatabaseName("idx_scenario_test_date");
|
||||
|
||||
// 唯一约束
|
||||
builder.HasIndex(e => new { e.TxPanelId, e.TxHardwareId, e.TxSoftwareId, e.RxTypeId, e.TestPurpose, e.TestDate, e.TestSequence })
|
||||
.IsUnique()
|
||||
.HasDatabaseName("uq_scenario");
|
||||
|
||||
// 关系配置
|
||||
builder.HasOne(e => e.TxPanel)
|
||||
.WithMany(e => e.TestScenarios)
|
||||
.HasForeignKey(e => e.TxPanelId)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasOne(e => e.TxHardware)
|
||||
.WithMany(e => e.TestScenarios)
|
||||
.HasForeignKey(e => e.TxHardwareId)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasOne(e => e.TxSoftware)
|
||||
.WithMany(e => e.TestScenarios)
|
||||
.HasForeignKey(e => e.TxSoftwareId)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasOne(e => e.RxType)
|
||||
.WithMany(e => e.TestScenarios)
|
||||
.HasForeignKey(e => e.RxTypeId)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasMany(e => e.QfodRecords)
|
||||
.WithOne(e => e.Scenario)
|
||||
.HasForeignKey(e => e.ScenarioId);
|
||||
|
||||
builder.HasMany(e => e.PlossRecords)
|
||||
.WithOne(e => e.Scenario)
|
||||
.HasForeignKey(e => e.ScenarioId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 场景创建流程
|
||||
@@ -154,45 +224,41 @@ public class TestScenario
|
||||
|
||||
```csharp
|
||||
// Services/ScenarioService.cs
|
||||
public async Task<TestScenario> GetOrCreateFromFileNameAsync(string fileName)
|
||||
public async Task<TestScenario> GetOrCreateScenarioAsync(ScenarioInfo info)
|
||||
{
|
||||
// 1. 解析文件名
|
||||
var parts = ParseFileName(fileName);
|
||||
// 格式: TX面板-TX硬件-TX软件-RX类型-测试目的-年月日-测试次数.log
|
||||
// 1. 获取或创建各维度(通过 DimensionService)
|
||||
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);
|
||||
|
||||
// 2. 获取或创建各维度
|
||||
var txPanel = await txPanelService.GetOrCreateAsync(parts.TxPanel);
|
||||
var txHardware = await txHardwareService.GetOrCreateAsync(parts.TxHardware);
|
||||
var txSoftware = await txSoftwareService.GetOrCreateAsync(parts.TxSoftware);
|
||||
var rxType = await rxTypeService.GetOrCreateAsync(parts.RxType);
|
||||
|
||||
// 3. 检查场景是否已存在
|
||||
var existing = await context.TestScenarios
|
||||
// 2. 检查场景是否已存在(利用唯一约束)
|
||||
var existing = await _context.TestScenarios
|
||||
.FirstOrDefaultAsync(s =>
|
||||
s.TxPanelId == txPanel.Id &&
|
||||
s.TxHardwareId == txHardware.Id &&
|
||||
s.TxSoftwareId == txSoftware.Id &&
|
||||
s.RxTypeId == rxType.Id &&
|
||||
s.TestPurpose == parts.TestPurpose &&
|
||||
s.TestDate == parts.TestDate &&
|
||||
s.TestSequence == parts.TestSequence);
|
||||
s.TestPurpose == info.TestPurpose &&
|
||||
s.TestDate == info.TestDate &&
|
||||
s.TestSequence == info.TestSequence);
|
||||
|
||||
if (existing != null) return existing;
|
||||
|
||||
// 4. 创建新场景
|
||||
// 3. 创建新场景
|
||||
var scenario = new TestScenario
|
||||
{
|
||||
TxPanelId = txPanel.Id,
|
||||
TxHardwareId = txHardware.Id,
|
||||
TxSoftwareId = txSoftware.Id,
|
||||
RxTypeId = rxType.Id,
|
||||
TestPurpose = parts.TestPurpose,
|
||||
TestDate = parts.TestDate,
|
||||
TestSequence = parts.TestSequence
|
||||
TestPurpose = info.TestPurpose,
|
||||
TestDate = info.TestDate,
|
||||
TestSequence = info.TestSequence
|
||||
};
|
||||
|
||||
context.TestScenarios.Add(scenario);
|
||||
await context.SaveChangesAsync();
|
||||
_context.TestScenarios.Add(scenario);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return scenario;
|
||||
}
|
||||
|
||||
@@ -182,6 +182,60 @@ public async Task SoftDeleteAsync(Guid id)
|
||||
}
|
||||
```
|
||||
|
||||
### 实体配置方式
|
||||
|
||||
采用 `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 记录到系统日志文件,不存入业务数据库。
|
||||
|
||||
65
src/WCTDataMiner/Commands/CleanCommand.cs
Normal file
65
src/WCTDataMiner/Commands/CleanCommand.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System.CommandLine;
|
||||
using WCTDataMiner.Services;
|
||||
|
||||
namespace WCTDataMiner.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 数据清理命令
|
||||
/// </summary>
|
||||
public class CleanCommand : Command
|
||||
{
|
||||
public CleanCommand(CleanService cleanService)
|
||||
: base("clean", "Clean data from database (soft delete)")
|
||||
{
|
||||
var scenarioOption = new Option<string?>(
|
||||
"--scenario",
|
||||
"Scenario ID to clean"
|
||||
);
|
||||
|
||||
var allOption = new Option<bool>(
|
||||
"--all",
|
||||
() => false,
|
||||
"Clean all data"
|
||||
);
|
||||
|
||||
var confirmOption = new Option<bool>(
|
||||
"--confirm",
|
||||
() => false,
|
||||
"Confirm the clean operation"
|
||||
);
|
||||
|
||||
AddOption(scenarioOption);
|
||||
AddOption(allOption);
|
||||
AddOption(confirmOption);
|
||||
|
||||
this.SetHandler(async (scenario, all, confirm) =>
|
||||
{
|
||||
if (!confirm)
|
||||
{
|
||||
Console.WriteLine("Please add --confirm to confirm the clean operation.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(scenario))
|
||||
{
|
||||
if (!Guid.TryParse(scenario, out var scenarioId))
|
||||
{
|
||||
Console.WriteLine($"Invalid scenario ID: {scenario}");
|
||||
return;
|
||||
}
|
||||
|
||||
var count = await cleanService.CleanByScenarioAsync(scenarioId);
|
||||
Console.WriteLine($"Cleaned {count} records for scenario {scenarioId}");
|
||||
}
|
||||
else if (all)
|
||||
{
|
||||
var count = await cleanService.CleanAllAsync();
|
||||
Console.WriteLine($"Cleaned {count} records in total");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Please specify --scenario <id> or --all");
|
||||
}
|
||||
}, scenarioOption, allOption, confirmOption);
|
||||
}
|
||||
}
|
||||
84
src/WCTDataMiner/Commands/ExportCommand.cs
Normal file
84
src/WCTDataMiner/Commands/ExportCommand.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.CommandLine;
|
||||
using WCTDataMiner.Services;
|
||||
|
||||
namespace WCTDataMiner.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 数据导出命令
|
||||
/// </summary>
|
||||
public class ExportCommand : Command
|
||||
{
|
||||
public ExportCommand(ExportService exportService)
|
||||
: base("export", "Export data to CSV or JSON")
|
||||
{
|
||||
var formatOption = new Option<string>(
|
||||
"--format",
|
||||
() => "csv",
|
||||
"Export format: csv or json"
|
||||
);
|
||||
|
||||
var outputOption = new Option<string>(
|
||||
"--output",
|
||||
() => "./data/output",
|
||||
"Output directory path"
|
||||
);
|
||||
|
||||
var typeOption = new Option<string>(
|
||||
"--type",
|
||||
() => "all",
|
||||
"Data type to export: qfod, ploss, or all"
|
||||
);
|
||||
|
||||
AddOption(formatOption);
|
||||
AddOption(outputOption);
|
||||
AddOption(typeOption);
|
||||
|
||||
this.SetHandler(async (format, output, type) =>
|
||||
{
|
||||
format = format.ToLower();
|
||||
type = type.ToLower();
|
||||
|
||||
Console.WriteLine($"Exporting {type} data as {format.ToUpper()} to {output}...");
|
||||
|
||||
try
|
||||
{
|
||||
if (format == "csv")
|
||||
{
|
||||
if (type is "qfod" or "all")
|
||||
{
|
||||
var path = await exportService.ExportQfodToCsvAsync(output);
|
||||
Console.WriteLine($" Qfod CSV exported: {path}");
|
||||
}
|
||||
|
||||
if (type is "ploss" or "all")
|
||||
{
|
||||
var path = await exportService.ExportPlossToCsvAsync(output);
|
||||
Console.WriteLine($" Ploss CSV exported: {path}");
|
||||
}
|
||||
}
|
||||
else if (format == "json")
|
||||
{
|
||||
if (type is "qfod" or "all")
|
||||
{
|
||||
var path = await exportService.ExportQfodToJsonAsync(output);
|
||||
Console.WriteLine($" Qfod JSON exported: {path}");
|
||||
}
|
||||
|
||||
if (type is "ploss" or "all")
|
||||
{
|
||||
var path = await exportService.ExportPlossToJsonAsync(output);
|
||||
Console.WriteLine($" Ploss JSON exported: {path}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Unknown format: {format}. Use 'csv' or 'json'.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Export failed: {ex.Message}");
|
||||
}
|
||||
}, formatOption, outputOption, typeOption);
|
||||
}
|
||||
}
|
||||
80
src/WCTDataMiner/Commands/ParseCommand.cs
Normal file
80
src/WCTDataMiner/Commands/ParseCommand.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System.CommandLine;
|
||||
using WCTDataMiner.Services;
|
||||
|
||||
namespace WCTDataMiner.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 解析日志文件命令
|
||||
/// </summary>
|
||||
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))
|
||||
{
|
||||
var report = await parseService.ParseFileAsync(file, force);
|
||||
PrintReport(report);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(dir))
|
||||
{
|
||||
var reports = await parseService.ParseDirectoryAsync(dir, recursive, force);
|
||||
Console.WriteLine($"\n=== Summary: {reports.Count} files processed ===");
|
||||
foreach (var report in reports)
|
||||
{
|
||||
PrintReport(report);
|
||||
}
|
||||
|
||||
var totalQfod = reports.Sum(r => r.QfodCount);
|
||||
var totalPloss = reports.Sum(r => r.PlossCount);
|
||||
var totalErrors = reports.Sum(r => r.ErrorCount);
|
||||
Console.WriteLine($"\nTotal: Qfod={totalQfod}, Ploss={totalPloss}, Errors={totalErrors}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Please specify --file or --dir");
|
||||
}
|
||||
}, fileOption, dirOption, recursiveOption, forceOption);
|
||||
}
|
||||
|
||||
private static void PrintReport(ParseReport report)
|
||||
{
|
||||
if (report.IsSuccess)
|
||||
{
|
||||
Console.WriteLine($" [{report.FileName}] Qfod: {report.QfodCount}, Ploss: {report.PlossCount}, Errors: {report.ErrorCount}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($" [{report.FileName}] FAILED: {report.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
}
|
||||
112
src/WCTDataMiner/Commands/StatsCommand.cs
Normal file
112
src/WCTDataMiner/Commands/StatsCommand.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System.CommandLine;
|
||||
using WCTDataMiner.Services;
|
||||
|
||||
namespace WCTDataMiner.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// 统计查询命令
|
||||
/// </summary>
|
||||
public class StatsCommand : Command
|
||||
{
|
||||
public StatsCommand(StatsService statsService)
|
||||
: base("stats", "Query statistics from database")
|
||||
{
|
||||
var summaryOption = new Option<bool>(
|
||||
"--summary",
|
||||
() => false,
|
||||
"Show overall statistics summary"
|
||||
);
|
||||
|
||||
var byFodTypeOption = new Option<bool>(
|
||||
"--by-fod-type",
|
||||
() => false,
|
||||
"Show statistics by FOD type"
|
||||
);
|
||||
|
||||
var byPanelOption = new Option<bool>(
|
||||
"--by-panel",
|
||||
() => false,
|
||||
"Show statistics by TX panel"
|
||||
);
|
||||
|
||||
var byHardwareOption = new Option<bool>(
|
||||
"--by-hardware",
|
||||
() => false,
|
||||
"Show statistics by TX hardware version"
|
||||
);
|
||||
|
||||
var byRxTypeOption = new Option<bool>(
|
||||
"--by-rx-type",
|
||||
() => false,
|
||||
"Show statistics by RX type"
|
||||
);
|
||||
|
||||
AddOption(summaryOption);
|
||||
AddOption(byFodTypeOption);
|
||||
AddOption(byPanelOption);
|
||||
AddOption(byHardwareOption);
|
||||
AddOption(byRxTypeOption);
|
||||
|
||||
this.SetHandler(async (summary, byFodType, byPanel, byHardware, byRxType) =>
|
||||
{
|
||||
// 如果没有指定任何选项,默认显示摘要
|
||||
if (!summary && !byFodType && !byPanel && !byHardware && !byRxType)
|
||||
{
|
||||
summary = true;
|
||||
}
|
||||
|
||||
if (summary)
|
||||
{
|
||||
var s = await statsService.GetSummaryAsync();
|
||||
Console.WriteLine("=== Statistics Summary ===");
|
||||
Console.WriteLine($" Scenarios: {s.ScenarioCount}");
|
||||
Console.WriteLine($" Qfod Records: {s.QfodCount}");
|
||||
Console.WriteLine($" Ploss Records: {s.PlossCount}");
|
||||
Console.WriteLine($" TX Panels: {s.PanelCount}");
|
||||
Console.WriteLine($" TX Hardware: {s.HardwareCount}");
|
||||
Console.WriteLine($" TX Software: {s.SoftwareCount}");
|
||||
Console.WriteLine($" RX Types: {s.RxTypeCount}");
|
||||
}
|
||||
|
||||
if (byFodType)
|
||||
{
|
||||
Console.WriteLine("\n=== By FOD Type ===");
|
||||
var stats = await statsService.GetStatsByFodTypeAsync();
|
||||
foreach (var s in stats)
|
||||
{
|
||||
Console.WriteLine($" FOD Type {s.FodType}: {s.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
if (byPanel)
|
||||
{
|
||||
Console.WriteLine("\n=== By TX Panel ===");
|
||||
var stats = await statsService.GetStatsByPanelAsync();
|
||||
foreach (var s in stats)
|
||||
{
|
||||
Console.WriteLine($" {s.Name}: {s.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
if (byHardware)
|
||||
{
|
||||
Console.WriteLine("\n=== By TX Hardware ===");
|
||||
var stats = await statsService.GetStatsByHardwareAsync();
|
||||
foreach (var s in stats)
|
||||
{
|
||||
Console.WriteLine($" {s.Name}: {s.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
if (byRxType)
|
||||
{
|
||||
Console.WriteLine("\n=== By RX Type ===");
|
||||
var stats = await statsService.GetStatsByRxTypeAsync();
|
||||
foreach (var s in stats)
|
||||
{
|
||||
Console.WriteLine($" {s.Name}: {s.Count}");
|
||||
}
|
||||
}
|
||||
}, summaryOption, byFodTypeOption, byPanelOption, byHardwareOption, byRxTypeOption);
|
||||
}
|
||||
}
|
||||
44
src/WCTDataMiner/Configuration/AppSettings.cs
Normal file
44
src/WCTDataMiner/Configuration/AppSettings.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
namespace WCTDataMiner.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// 应用配置
|
||||
/// </summary>
|
||||
public class AppSettings
|
||||
{
|
||||
public DatabaseSettings Database { get; set; } = new();
|
||||
public ParserSettings Parser { get; set; } = new();
|
||||
public PathSettings Paths { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据库配置
|
||||
/// </summary>
|
||||
public class DatabaseSettings
|
||||
{
|
||||
public string Type { get; set; } = "sqlite";
|
||||
public string Path { get; set; } = "./data/database/wctminer.db";
|
||||
|
||||
// PostgreSQL 配置(可选)
|
||||
public string? Host { get; set; }
|
||||
public int Port { get; set; } = 5432;
|
||||
public string? Name { get; set; }
|
||||
public string? User { get; set; }
|
||||
public string? Password { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析器配置
|
||||
/// </summary>
|
||||
public class ParserSettings
|
||||
{
|
||||
public int BatchSize { get; set; } = 1000;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 路径配置
|
||||
/// </summary>
|
||||
public class PathSettings
|
||||
{
|
||||
public string LogDir { get; set; } = "./data/logs";
|
||||
public string OutputDir { get; set; } = "./data/output";
|
||||
}
|
||||
103
src/WCTDataMiner/Data/Configurations/PlossRecordConfiguration.cs
Normal file
103
src/WCTDataMiner/Data/Configurations/PlossRecordConfiguration.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
|
||||
public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PlossRecord> builder)
|
||||
{
|
||||
builder.ToTable("ploss_record");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).HasColumnName("id");
|
||||
|
||||
builder.Property(e => e.ScenarioId)
|
||||
.IsRequired()
|
||||
.HasColumnName("scenario_id");
|
||||
|
||||
builder.Property(e => e.RxType)
|
||||
.IsRequired()
|
||||
.HasColumnName("rx_type");
|
||||
|
||||
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(0)
|
||||
.HasColumnName("trigger_count");
|
||||
|
||||
builder.Property(e => e.FodResult)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(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.IsDeleted)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
builder.Property(e => e.CreatedAt)
|
||||
.IsRequired()
|
||||
.HasDefaultValueSql("datetime('now')")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
// 派生属性不落库
|
||||
builder.Ignore(e => e.Margin);
|
||||
|
||||
// 索引
|
||||
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.HasOne(e => e.Scenario)
|
||||
.WithMany(e => e.PlossRecords)
|
||||
.HasForeignKey(e => e.ScenarioId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// 全局查询过滤器
|
||||
builder.HasQueryFilter(e => !e.IsDeleted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
|
||||
public class QfodRecordConfiguration : IEntityTypeConfiguration<QfodRecord>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<QfodRecord> builder)
|
||||
{
|
||||
builder.ToTable("qfod_record", t =>
|
||||
{
|
||||
t.HasCheckConstraint("chk_qfod_charger", "charger_index IN (0, 1)");
|
||||
t.HasCheckConstraint("chk_qfod_coil", "coil_index IN (0, 1, 2)");
|
||||
});
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).HasColumnName("id");
|
||||
|
||||
builder.Property(e => e.ScenarioId)
|
||||
.IsRequired()
|
||||
.HasColumnName("scenario_id");
|
||||
|
||||
builder.Property(e => e.ChargerIndex)
|
||||
.IsRequired()
|
||||
.HasColumnName("charger_index");
|
||||
|
||||
builder.Property(e => e.CoilIndex)
|
||||
.IsRequired()
|
||||
.HasColumnName("coil_index");
|
||||
|
||||
builder.Property(e => e.DeltaQ)
|
||||
.IsRequired()
|
||||
.HasColumnName("delta_q");
|
||||
|
||||
builder.Property(e => e.CurrentQ)
|
||||
.IsRequired()
|
||||
.HasColumnName("current_q");
|
||||
|
||||
builder.Property(e => e.RawQ)
|
||||
.IsRequired()
|
||||
.HasColumnName("raw_q");
|
||||
|
||||
builder.Property(e => e.FodType)
|
||||
.IsRequired()
|
||||
.HasColumnName("fod_type");
|
||||
|
||||
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.ScenarioId).HasDatabaseName("idx_qfod_scenario");
|
||||
builder.HasIndex(e => new { e.ChargerIndex, e.CoilIndex }).HasDatabaseName("idx_qfod_charger_coil");
|
||||
builder.HasIndex(e => e.FodType).HasDatabaseName("idx_qfod_fod_type");
|
||||
builder.HasIndex(e => e.DeltaQ).HasDatabaseName("idx_qfod_delta_q");
|
||||
|
||||
// 关系配置
|
||||
builder.HasOne(e => e.Scenario)
|
||||
.WithMany(e => e.QfodRecords)
|
||||
.HasForeignKey(e => e.ScenarioId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// 全局查询过滤器
|
||||
builder.HasQueryFilter(e => !e.IsDeleted);
|
||||
}
|
||||
}
|
||||
36
src/WCTDataMiner/Data/Configurations/RxTypeConfiguration.cs
Normal file
36
src/WCTDataMiner/Data/Configurations/RxTypeConfiguration.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
|
||||
public class RxTypeConfiguration : IEntityTypeConfiguration<RxType>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<RxType> builder)
|
||||
{
|
||||
builder.ToTable("rx_type");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
|
||||
public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TestScenario> builder)
|
||||
{
|
||||
builder.ToTable("test_scenario");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).HasColumnName("id");
|
||||
|
||||
builder.Property(e => e.TxPanelId)
|
||||
.IsRequired()
|
||||
.HasColumnName("tx_panel_id");
|
||||
|
||||
builder.Property(e => e.TxHardwareId)
|
||||
.IsRequired()
|
||||
.HasColumnName("tx_hardware_id");
|
||||
|
||||
builder.Property(e => e.TxSoftwareId)
|
||||
.IsRequired()
|
||||
.HasColumnName("tx_software_id");
|
||||
|
||||
builder.Property(e => e.RxTypeId)
|
||||
.IsRequired()
|
||||
.HasColumnName("rx_type_id");
|
||||
|
||||
builder.Property(e => e.TestPurpose)
|
||||
.HasMaxLength(200)
|
||||
.HasColumnName("test_purpose");
|
||||
|
||||
builder.Property(e => e.TestDate)
|
||||
.IsRequired()
|
||||
.HasColumnName("test_date");
|
||||
|
||||
builder.Property(e => e.TestSequence)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(1)
|
||||
.HasColumnName("test_sequence");
|
||||
|
||||
builder.Property(e => e.QfodCount)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("qfod_count");
|
||||
|
||||
builder.Property(e => e.PlossCount)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("ploss_count");
|
||||
|
||||
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.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.TestDate).HasDatabaseName("idx_scenario_test_date");
|
||||
|
||||
// 唯一约束
|
||||
builder.HasIndex(e => new { e.TxPanelId, e.TxHardwareId, e.TxSoftwareId, e.RxTypeId, e.TestPurpose, e.TestDate, e.TestSequence })
|
||||
.IsUnique()
|
||||
.HasDatabaseName("uq_scenario");
|
||||
|
||||
// 关系配置 - 维度表
|
||||
builder.HasOne(e => e.TxPanel)
|
||||
.WithMany(e => e.TestScenarios)
|
||||
.HasForeignKey(e => e.TxPanelId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(e => e.TxHardware)
|
||||
.WithMany(e => e.TestScenarios)
|
||||
.HasForeignKey(e => e.TxHardwareId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(e => e.TxSoftware)
|
||||
.WithMany(e => e.TestScenarios)
|
||||
.HasForeignKey(e => e.TxSoftwareId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(e => e.RxType)
|
||||
.WithMany(e => e.TestScenarios)
|
||||
.HasForeignKey(e => e.RxTypeId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// 关系配置 - 数据记录
|
||||
builder.HasMany(e => e.QfodRecords)
|
||||
.WithOne(e => e.Scenario)
|
||||
.HasForeignKey(e => e.ScenarioId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasMany(e => e.PlossRecords)
|
||||
.WithOne(e => e.Scenario)
|
||||
.HasForeignKey(e => e.ScenarioId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// 全局查询过滤器
|
||||
builder.HasQueryFilter(e => !e.IsDeleted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
|
||||
public class TxHardwareConfiguration : IEntityTypeConfiguration<TxHardware>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TxHardware> builder)
|
||||
{
|
||||
builder.ToTable("tx_hardware");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).HasColumnName("id");
|
||||
|
||||
builder.Property(e => e.Version)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnName("version");
|
||||
|
||||
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.Version).IsUnique();
|
||||
|
||||
// 全局查询过滤器
|
||||
builder.HasQueryFilter(e => !e.IsDeleted);
|
||||
}
|
||||
}
|
||||
36
src/WCTDataMiner/Data/Configurations/TxPanelConfiguration.cs
Normal file
36
src/WCTDataMiner/Data/Configurations/TxPanelConfiguration.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
|
||||
public class TxPanelConfiguration : IEntityTypeConfiguration<TxPanel>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TxPanel> builder)
|
||||
{
|
||||
builder.ToTable("tx_panel");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).HasColumnName("id");
|
||||
|
||||
builder.Property(e => e.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnName("name");
|
||||
|
||||
builder.Property(e => e.IsDeleted)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
builder.Property(e => e.CreatedAt)
|
||||
.IsRequired()
|
||||
.HasDefaultValueSql("datetime('now')")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
builder.HasIndex(e => e.Name).IsUnique();
|
||||
|
||||
// 全局查询过滤器
|
||||
builder.HasQueryFilter(e => !e.IsDeleted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data.Configurations;
|
||||
|
||||
public class TxSoftwareConfiguration : IEntityTypeConfiguration<TxSoftware>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TxSoftware> builder)
|
||||
{
|
||||
builder.ToTable("tx_software");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).HasColumnName("id");
|
||||
|
||||
builder.Property(e => e.Version)
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnName("version");
|
||||
|
||||
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.Version).IsUnique();
|
||||
|
||||
// 全局查询过滤器
|
||||
builder.HasQueryFilter(e => !e.IsDeleted);
|
||||
}
|
||||
}
|
||||
66
src/WCTDataMiner/Data/DbContextFactory.cs
Normal file
66
src/WCTDataMiner/Data/DbContextFactory.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace WCTDataMiner.Data;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库上下文工厂实现 - 支持SQLite和PostgreSQL切换
|
||||
/// </summary>
|
||||
public class DbContextFactory : IDbContextFactory
|
||||
{
|
||||
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.GetValue("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);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/WCTDataMiner/Data/IDbContextFactory.cs
Normal file
9
src/WCTDataMiner/Data/IDbContextFactory.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace WCTDataMiner.Data;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库上下文工厂接口
|
||||
/// </summary>
|
||||
public interface IDbContextFactory
|
||||
{
|
||||
WctMinerDbContext CreateDbContext();
|
||||
}
|
||||
36
src/WCTDataMiner/Data/SeedData.cs
Normal file
36
src/WCTDataMiner/Data/SeedData.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data;
|
||||
|
||||
/// <summary>
|
||||
/// 初始数据种子
|
||||
/// </summary>
|
||||
public static class SeedData
|
||||
{
|
||||
/// <summary>
|
||||
/// TX面板类型预设数据
|
||||
/// </summary>
|
||||
public static readonly TxPanel[] DefaultTxPanels =
|
||||
[
|
||||
new() { Name = "none" },
|
||||
new() { Name = "single-mold" },
|
||||
new() { Name = "single-rapid" },
|
||||
new() { Name = "single-3d" },
|
||||
new() { Name = "dual-mold" },
|
||||
new() { Name = "dual-rapid" },
|
||||
new() { Name = "dual-3d" }
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 初始化种子数据
|
||||
/// </summary>
|
||||
public static void Initialize(WctMinerDbContext context)
|
||||
{
|
||||
// 确保 TxPanel 表有预设数据
|
||||
if (!context.TxPanels.Any())
|
||||
{
|
||||
context.TxPanels.AddRange(DefaultTxPanels);
|
||||
context.SaveChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/WCTDataMiner/Data/WctMinerDbContext.cs
Normal file
32
src/WCTDataMiner/Data/WctMinerDbContext.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Data;
|
||||
|
||||
/// <summary>
|
||||
/// WCT数据采集数据库上下文
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
54
src/WCTDataMiner/Extensions/ServiceCollectionExtensions.cs
Normal file
54
src/WCTDataMiner/Extensions/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using WCTDataMiner.Data;
|
||||
using WCTDataMiner.Parsers;
|
||||
using WCTDataMiner.Services;
|
||||
|
||||
namespace WCTDataMiner.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// 服务注册扩展
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 注册数据库服务
|
||||
/// </summary>
|
||||
public static IServiceCollection AddDatabaseServices(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// 注册 DbContext 工厂
|
||||
services.AddScoped<IDbContextFactory, DbContextFactory>();
|
||||
|
||||
// 注册 DbContext(通过工厂创建)
|
||||
services.AddScoped<WctMinerDbContext>(sp =>
|
||||
{
|
||||
var factory = sp.GetRequiredService<IDbContextFactory>();
|
||||
return factory.CreateDbContext();
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册应用服务
|
||||
/// </summary>
|
||||
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
|
||||
{
|
||||
// Parsers
|
||||
services.AddScoped<QfodParser>();
|
||||
services.AddScoped<PlossParser>();
|
||||
services.AddSingleton<FileNameParser>();
|
||||
|
||||
// Services
|
||||
services.AddScoped<DimensionService>();
|
||||
services.AddScoped<ScenarioService>();
|
||||
services.AddScoped<ParseService>();
|
||||
services.AddScoped<StatsService>();
|
||||
services.AddScoped<ExportService>();
|
||||
services.AddScoped<CleanService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
36
src/WCTDataMiner/Models/PlossRecord.cs
Normal file
36
src/WCTDataMiner/Models/PlossRecord.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace WCTDataMiner.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Ploss记录实体 - 存储Ploss FOD格式解析数据
|
||||
/// </summary>
|
||||
public class PlossRecord
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid ScenarioId { get; set; }
|
||||
public byte RxType { get; set; }
|
||||
public int RxPower { get; set; } // uint16 范围 0-65535,使用 int 存储
|
||||
public int TxPower { get; set; } // uint16 范围 0-65535,使用 int 存储
|
||||
public int Vcoil { get; set; } // uint16 范围 0-65535,使用 int 存储
|
||||
public int Vin { get; set; } // uint16 范围 0-65535,使用 int 存储
|
||||
public int Isns { get; set; } // uint16 范围 0-65535,使用 int 存储
|
||||
|
||||
/// <summary>核心判定值:负数表示安全</summary>
|
||||
public int Ploss { get; set; }
|
||||
|
||||
/// <summary>安全红线:当前功率段阈值</summary>
|
||||
public int Threshold { get; set; }
|
||||
|
||||
public int TriggerCount { get; set; } = 0;
|
||||
public int FodResult { get; set; } = 0;
|
||||
public byte ProtocolType { get; set; }
|
||||
public int PwmDuty { get; set; } // uint16 范围 0-65535,使用 int 存储
|
||||
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;
|
||||
}
|
||||
21
src/WCTDataMiner/Models/QfodRecord.cs
Normal file
21
src/WCTDataMiner/Models/QfodRecord.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace WCTDataMiner.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Qfod记录实体 - 存储Qfod格式解析数据
|
||||
/// </summary>
|
||||
public class QfodRecord
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid ScenarioId { get; set; }
|
||||
public byte ChargerIndex { get; set; }
|
||||
public byte CoilIndex { get; set; }
|
||||
public int DeltaQ { get; set; }
|
||||
public float CurrentQ { get; set; }
|
||||
public float RawQ { get; set; }
|
||||
public byte FodType { get; set; }
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation Properties
|
||||
public TestScenario Scenario { get; set; } = null!;
|
||||
}
|
||||
15
src/WCTDataMiner/Models/RxType.cs
Normal file
15
src/WCTDataMiner/Models/RxType.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace WCTDataMiner.Models;
|
||||
|
||||
/// <summary>
|
||||
/// RX类型维度表
|
||||
/// </summary>
|
||||
public class RxType
|
||||
{
|
||||
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; } = [];
|
||||
}
|
||||
30
src/WCTDataMiner/Models/TestScenario.cs
Normal file
30
src/WCTDataMiner/Models/TestScenario.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace WCTDataMiner.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 测试场景实体 - 通过外键关联各维度表
|
||||
/// </summary>
|
||||
public class TestScenario
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid TxPanelId { get; set; }
|
||||
public Guid TxHardwareId { get; set; }
|
||||
public Guid TxSoftwareId { get; set; }
|
||||
public Guid RxTypeId { get; set; }
|
||||
public string? TestPurpose { get; set; }
|
||||
public DateOnly TestDate { get; set; }
|
||||
public int TestSequence { get; set; } = 1;
|
||||
public int QfodCount { get; set; } = 0;
|
||||
public int PlossCount { get; set; } = 0;
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation Properties - 维度表
|
||||
public TxPanel TxPanel { get; set; } = null!;
|
||||
public TxHardware TxHardware { get; set; } = null!;
|
||||
public TxSoftware TxSoftware { get; set; } = null!;
|
||||
public RxType RxType { get; set; } = null!;
|
||||
|
||||
// Navigation Properties - 数据记录
|
||||
public ICollection<QfodRecord> QfodRecords { get; set; } = [];
|
||||
public ICollection<PlossRecord> PlossRecords { get; set; } = [];
|
||||
}
|
||||
15
src/WCTDataMiner/Models/TxHardware.cs
Normal file
15
src/WCTDataMiner/Models/TxHardware.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace WCTDataMiner.Models;
|
||||
|
||||
/// <summary>
|
||||
/// TX硬件版本维度表
|
||||
/// </summary>
|
||||
public class TxHardware
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string Version { get; set; } = null!;
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation Properties
|
||||
public ICollection<TestScenario> TestScenarios { get; set; } = [];
|
||||
}
|
||||
15
src/WCTDataMiner/Models/TxPanel.cs
Normal file
15
src/WCTDataMiner/Models/TxPanel.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace WCTDataMiner.Models;
|
||||
|
||||
/// <summary>
|
||||
/// TX面板类型维度表
|
||||
/// </summary>
|
||||
public class TxPanel
|
||||
{
|
||||
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; } = [];
|
||||
}
|
||||
15
src/WCTDataMiner/Models/TxSoftware.cs
Normal file
15
src/WCTDataMiner/Models/TxSoftware.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace WCTDataMiner.Models;
|
||||
|
||||
/// <summary>
|
||||
/// TX软件版本维度表
|
||||
/// </summary>
|
||||
public class TxSoftware
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string Version { get; set; } = null!;
|
||||
public bool IsDeleted { get; set; } = false;
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation Properties
|
||||
public ICollection<TestScenario> TestScenarios { get; set; } = [];
|
||||
}
|
||||
68
src/WCTDataMiner/Parsers/FileNameParser.cs
Normal file
68
src/WCTDataMiner/Parsers/FileNameParser.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace WCTDataMiner.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
|
||||
);
|
||||
36
src/WCTDataMiner/Parsers/IParser.cs
Normal file
36
src/WCTDataMiner/Parsers/IParser.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace WCTDataMiner.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// 通用解析器接口
|
||||
/// </summary>
|
||||
public interface IParser<TRecord>
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断是否可以解析该行
|
||||
/// </summary>
|
||||
bool CanParse(string line);
|
||||
|
||||
/// <summary>
|
||||
/// 解析日志行
|
||||
/// </summary>
|
||||
/// <param name="line">日志行内容</param>
|
||||
/// <param name="scenarioId">关联的测试场景ID</param>
|
||||
ParseResult<TRecord> Parse(string line, Guid scenarioId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析结果
|
||||
/// </summary>
|
||||
public record ParseResult<TRecord>
|
||||
{
|
||||
public bool IsSuccess { get; init; }
|
||||
public TRecord? Record { get; init; }
|
||||
public string? ErrorType { get; init; }
|
||||
public string? ErrorMessage { get; init; }
|
||||
|
||||
public static ParseResult<TRecord> Success(TRecord record) =>
|
||||
new() { IsSuccess = true, Record = record };
|
||||
|
||||
public static ParseResult<TRecord> Failure(string errorType, string errorMessage) =>
|
||||
new() { IsSuccess = false, ErrorType = errorType, ErrorMessage = errorMessage };
|
||||
}
|
||||
62
src/WCTDataMiner/Parsers/PlossParser.cs
Normal file
62
src/WCTDataMiner/Parsers/PlossParser.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Ploss FOD日志解析器
|
||||
/// 格式: FOD-> (1) X X X (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13)
|
||||
/// 示例: FOD-> 4 2 1 127 10690 12013 35377 9904 1213 -2882 750 0 0 1 500 1
|
||||
/// </summary>
|
||||
public class PlossParser : IParser<PlossRecord>
|
||||
{
|
||||
// 格式说明有3个X(忽略字段),正则需要3个 \d+ 来匹配
|
||||
private static readonly Regex PlossPattern = 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
|
||||
);
|
||||
|
||||
public bool CanParse(string line) => PlossPattern.IsMatch(line);
|
||||
|
||||
public ParseResult<PlossRecord> Parse(string line, Guid scenarioId)
|
||||
{
|
||||
var match = PlossPattern.Match(line);
|
||||
if (!match.Success)
|
||||
{
|
||||
return ParseResult<PlossRecord>.Failure(
|
||||
"FORMAT_MISMATCH",
|
||||
"Line does not match Ploss pattern"
|
||||
);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var record = new PlossRecord
|
||||
{
|
||||
ScenarioId = scenarioId,
|
||||
RxType = byte.Parse(match.Groups[1].Value),
|
||||
RxPower = int.Parse(match.Groups[2].Value),
|
||||
TxPower = int.Parse(match.Groups[3].Value),
|
||||
Vcoil = int.Parse(match.Groups[4].Value),
|
||||
Vin = int.Parse(match.Groups[5].Value),
|
||||
Isns = int.Parse(match.Groups[6].Value),
|
||||
Ploss = int.Parse(match.Groups[7].Value),
|
||||
Threshold = int.Parse(match.Groups[8].Value),
|
||||
TriggerCount = int.Parse(match.Groups[9].Value),
|
||||
FodResult = int.Parse(match.Groups[10].Value),
|
||||
ProtocolType = byte.Parse(match.Groups[11].Value),
|
||||
PwmDuty = int.Parse(match.Groups[12].Value),
|
||||
CoilIndex = byte.Parse(match.Groups[13].Value)
|
||||
};
|
||||
|
||||
return ParseResult<PlossRecord>.Success(record);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ParseResult<PlossRecord>.Failure(
|
||||
"TYPE_CONVERSION",
|
||||
$"Failed to convert field: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/WCTDataMiner/Parsers/QfodParser.cs
Normal file
54
src/WCTDataMiner/Parsers/QfodParser.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Qfod日志解析器
|
||||
/// 格式: X#:Q A->B C D E
|
||||
/// 示例: 0#:Q 1->25 45.5 20.5 2
|
||||
/// </summary>
|
||||
public class QfodParser : IParser<QfodRecord>
|
||||
{
|
||||
private static readonly Regex QfodPattern = new(
|
||||
@"^(\d)#:Q\s+(\d+)->(-?\d+)\s+([-\d.]+)\s+([-\d.]+)\s+(\d+)$",
|
||||
RegexOptions.Compiled
|
||||
);
|
||||
|
||||
public bool CanParse(string line) => QfodPattern.IsMatch(line);
|
||||
|
||||
public ParseResult<QfodRecord> Parse(string line, Guid scenarioId)
|
||||
{
|
||||
var match = QfodPattern.Match(line);
|
||||
if (!match.Success)
|
||||
{
|
||||
return ParseResult<QfodRecord>.Failure(
|
||||
"FORMAT_MISMATCH",
|
||||
"Line does not match Qfod pattern"
|
||||
);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var record = new QfodRecord
|
||||
{
|
||||
ScenarioId = scenarioId,
|
||||
ChargerIndex = byte.Parse(match.Groups[1].Value),
|
||||
CoilIndex = byte.Parse(match.Groups[2].Value),
|
||||
DeltaQ = int.Parse(match.Groups[3].Value),
|
||||
CurrentQ = float.Parse(match.Groups[4].Value),
|
||||
RawQ = float.Parse(match.Groups[5].Value),
|
||||
FodType = byte.Parse(match.Groups[6].Value)
|
||||
};
|
||||
|
||||
return ParseResult<QfodRecord>.Success(record);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ParseResult<QfodRecord>.Failure(
|
||||
"TYPE_CONVERSION",
|
||||
$"Failed to convert field: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/WCTDataMiner/Program.cs
Normal file
83
src/WCTDataMiner/Program.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System.CommandLine;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Serilog;
|
||||
using WCTDataMiner.Commands;
|
||||
using WCTDataMiner.Data;
|
||||
using WCTDataMiner.Extensions;
|
||||
using WCTDataMiner.Services;
|
||||
|
||||
namespace WCTDataMiner;
|
||||
|
||||
/// <summary>
|
||||
/// WCTDataMiner - 无线充电FOD日志数据采集系统
|
||||
/// </summary>
|
||||
public class Program
|
||||
{
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
// 配置 Serilog
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Information()
|
||||
.MinimumLevel.Override("WCTDataMiner", Serilog.Events.LogEventLevel.Debug)
|
||||
.WriteTo.Console()
|
||||
.WriteTo.File(
|
||||
"./data/logs/wctminer-.log",
|
||||
rollingInterval: RollingInterval.Day,
|
||||
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||
.CreateLogger();
|
||||
|
||||
try
|
||||
{
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// 配置
|
||||
builder.Configuration.AddJsonFile("appsettings.json", optional: false);
|
||||
builder.Configuration.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true);
|
||||
builder.Configuration.AddEnvironmentVariables();
|
||||
builder.Configuration.AddCommandLine(args);
|
||||
|
||||
// Serilog
|
||||
builder.Services.AddSerilog();
|
||||
|
||||
// 注册服务
|
||||
builder.Services.AddDatabaseServices(builder.Configuration);
|
||||
builder.Services.AddApplicationServices();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// 初始化数据库
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
|
||||
await dbContext.Database.EnsureCreatedAsync();
|
||||
|
||||
// 种子数据
|
||||
SeedData.Initialize(dbContext);
|
||||
}
|
||||
|
||||
// 创建 CLI 命令
|
||||
using var commandScope = app.Services.CreateScope();
|
||||
var provider = commandScope.ServiceProvider;
|
||||
|
||||
var rootCommand = new RootCommand("WCTDataMiner - FOD Log Data Parser");
|
||||
|
||||
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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Application terminated unexpectedly");
|
||||
return 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await Log.CloseAndFlushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/WCTDataMiner/Services/CleanService.cs
Normal file
87
src/WCTDataMiner/Services/CleanService.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WCTDataMiner.Data;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 数据清理服务 - 支持软删除
|
||||
/// </summary>
|
||||
public class CleanService
|
||||
{
|
||||
private readonly WctMinerDbContext _context;
|
||||
|
||||
public CleanService(WctMinerDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 软删除指定场景及其关联记录
|
||||
/// </summary>
|
||||
public async Task<int> CleanByScenarioAsync(Guid scenarioId)
|
||||
{
|
||||
var scenario = await _context.TestScenarios.FindAsync(scenarioId);
|
||||
if (scenario == null) return 0;
|
||||
|
||||
// 软删除关联的Qfod记录
|
||||
var qfodRecords = await _context.QfodRecords
|
||||
.Where(r => r.ScenarioId == scenarioId)
|
||||
.ToListAsync();
|
||||
foreach (var record in qfodRecords)
|
||||
{
|
||||
record.IsDeleted = true;
|
||||
}
|
||||
|
||||
// 软删除关联的Ploss记录
|
||||
var plossRecords = await _context.PlossRecords
|
||||
.Where(r => r.ScenarioId == scenarioId)
|
||||
.ToListAsync();
|
||||
foreach (var record in plossRecords)
|
||||
{
|
||||
record.IsDeleted = true;
|
||||
}
|
||||
|
||||
// 软删除场景
|
||||
scenario.IsDeleted = true;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return qfodRecords.Count + plossRecords.Count + 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 软删除所有数据
|
||||
/// </summary>
|
||||
public async Task<int> CleanAllAsync()
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
// 软删除所有Qfod记录
|
||||
var qfodRecords = await _context.QfodRecords.IgnoreQueryFilters().ToListAsync();
|
||||
foreach (var record in qfodRecords.Where(r => !r.IsDeleted))
|
||||
{
|
||||
record.IsDeleted = true;
|
||||
count++;
|
||||
}
|
||||
|
||||
// 软删除所有Ploss记录
|
||||
var plossRecords = await _context.PlossRecords.IgnoreQueryFilters().ToListAsync();
|
||||
foreach (var record in plossRecords.Where(r => !r.IsDeleted))
|
||||
{
|
||||
record.IsDeleted = true;
|
||||
count++;
|
||||
}
|
||||
|
||||
// 软删除所有场景
|
||||
var scenarios = await _context.TestScenarios.IgnoreQueryFilters().ToListAsync();
|
||||
foreach (var scenario in scenarios.Where(s => !s.IsDeleted))
|
||||
{
|
||||
scenario.IsDeleted = true;
|
||||
count++;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return count;
|
||||
}
|
||||
}
|
||||
82
src/WCTDataMiner/Services/DimensionService.cs
Normal file
82
src/WCTDataMiner/Services/DimensionService.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WCTDataMiner.Data;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 维度表管理服务 - 负责维度表的CRUD操作
|
||||
/// </summary>
|
||||
public class DimensionService
|
||||
{
|
||||
private readonly WctMinerDbContext _context;
|
||||
|
||||
public DimensionService(WctMinerDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或创建TX面板维度
|
||||
/// </summary>
|
||||
public async Task<TxPanel> GetOrCreateTxPanelAsync(string name)
|
||||
{
|
||||
var existing = await _context.TxPanels
|
||||
.FirstOrDefaultAsync(p => p.Name == name);
|
||||
|
||||
if (existing != null) return existing;
|
||||
|
||||
var newPanel = new TxPanel { Name = name };
|
||||
_context.TxPanels.Add(newPanel);
|
||||
await _context.SaveChangesAsync();
|
||||
return newPanel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或创建TX硬件版本维度
|
||||
/// </summary>
|
||||
public async Task<TxHardware> GetOrCreateTxHardwareAsync(string version)
|
||||
{
|
||||
var existing = await _context.TxHardwares
|
||||
.FirstOrDefaultAsync(h => h.Version == version);
|
||||
|
||||
if (existing != null) return existing;
|
||||
|
||||
var newHardware = new TxHardware { Version = version };
|
||||
_context.TxHardwares.Add(newHardware);
|
||||
await _context.SaveChangesAsync();
|
||||
return newHardware;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或创建TX软件版本维度
|
||||
/// </summary>
|
||||
public async Task<TxSoftware> GetOrCreateTxSoftwareAsync(string version)
|
||||
{
|
||||
var existing = await _context.TxSoftwares
|
||||
.FirstOrDefaultAsync(s => s.Version == version);
|
||||
|
||||
if (existing != null) return existing;
|
||||
|
||||
var newSoftware = new TxSoftware { Version = version };
|
||||
_context.TxSoftwares.Add(newSoftware);
|
||||
await _context.SaveChangesAsync();
|
||||
return newSoftware;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或创建RX类型维度
|
||||
/// </summary>
|
||||
public async Task<RxType> GetOrCreateRxTypeAsync(string name)
|
||||
{
|
||||
var existing = await _context.RxTypes
|
||||
.FirstOrDefaultAsync(r => r.Name == name);
|
||||
|
||||
if (existing != null) return existing;
|
||||
|
||||
var newRxType = new RxType { Name = name };
|
||||
_context.RxTypes.Add(newRxType);
|
||||
await _context.SaveChangesAsync();
|
||||
return newRxType;
|
||||
}
|
||||
}
|
||||
177
src/WCTDataMiner/Services/ExportService.cs
Normal file
177
src/WCTDataMiner/Services/ExportService.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WCTDataMiner.Data;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 数据导出服务
|
||||
/// </summary>
|
||||
public class ExportService
|
||||
{
|
||||
private readonly WctMinerDbContext _context;
|
||||
|
||||
public ExportService(WctMinerDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出Qfod数据为CSV
|
||||
/// </summary>
|
||||
public async Task<string> ExportQfodToCsvAsync(string outputDir)
|
||||
{
|
||||
Directory.CreateDirectory(outputDir);
|
||||
var filePath = Path.Combine(outputDir, $"qfod_export_{DateTime.Now:yyyyMMdd_HHmmss}.csv");
|
||||
|
||||
var records = await _context.QfodRecords
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.TxPanel)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.TxHardware)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.TxSoftware)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.RxType)
|
||||
.ToListAsync();
|
||||
|
||||
var sb = new StringBuilder();
|
||||
// CSV Header
|
||||
sb.AppendLine("id,scenario_id,tx_panel,tx_hardware,tx_software,rx_type,test_purpose,test_date,test_sequence,charger_index,coil_index,delta_q,current_q,raw_q,fod_type");
|
||||
|
||||
foreach (var r in records)
|
||||
{
|
||||
sb.AppendLine($"{r.Id},{r.ScenarioId},{r.Scenario?.TxPanel?.Name},{r.Scenario?.TxHardware?.Version},{r.Scenario?.TxSoftware?.Version},{r.Scenario?.RxType?.Name},{r.Scenario?.TestPurpose},{r.Scenario?.TestDate:yyyy-MM-dd},{r.Scenario?.TestSequence},{r.ChargerIndex},{r.CoilIndex},{r.DeltaQ},{r.CurrentQ},{r.RawQ},{r.FodType}");
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(filePath, sb.ToString());
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出Ploss数据为CSV
|
||||
/// </summary>
|
||||
public async Task<string> ExportPlossToCsvAsync(string outputDir)
|
||||
{
|
||||
Directory.CreateDirectory(outputDir);
|
||||
var filePath = Path.Combine(outputDir, $"ploss_export_{DateTime.Now:yyyyMMdd_HHmmss}.csv");
|
||||
|
||||
var records = await _context.PlossRecords
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.TxPanel)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.TxHardware)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.TxSoftware)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.RxType)
|
||||
.ToListAsync();
|
||||
|
||||
var sb = new StringBuilder();
|
||||
// CSV Header
|
||||
sb.AppendLine("id,scenario_id,tx_panel,tx_hardware,tx_software,rx_type,test_purpose,test_date,test_sequence,rx_type_field,rx_power,tx_power,vcoil,vin,isns,ploss,threshold,trigger_count,fod_result,protocol_type,pwm_duty,coil_index,margin");
|
||||
|
||||
foreach (var r in records)
|
||||
{
|
||||
sb.AppendLine($"{r.Id},{r.ScenarioId},{r.Scenario?.TxPanel?.Name},{r.Scenario?.TxHardware?.Version},{r.Scenario?.TxSoftware?.Version},{r.Scenario?.RxType?.Name},{r.Scenario?.TestPurpose},{r.Scenario?.TestDate:yyyy-MM-dd},{r.Scenario?.TestSequence},{r.RxType},{r.RxPower},{r.TxPower},{r.Vcoil},{r.Vin},{r.Isns},{r.Ploss},{r.Threshold},{r.TriggerCount},{r.FodResult},{r.ProtocolType},{r.PwmDuty},{r.CoilIndex},{r.Margin}");
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(filePath, sb.ToString());
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出Qfod数据为JSON
|
||||
/// </summary>
|
||||
public async Task<string> ExportQfodToJsonAsync(string outputDir)
|
||||
{
|
||||
Directory.CreateDirectory(outputDir);
|
||||
var filePath = Path.Combine(outputDir, $"qfod_export_{DateTime.Now:yyyyMMdd_HHmmss}.json");
|
||||
|
||||
var records = await _context.QfodRecords
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.TxPanel)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.TxHardware)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.TxSoftware)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.RxType)
|
||||
.Select(r => new
|
||||
{
|
||||
r.Id,
|
||||
r.ScenarioId,
|
||||
TxPanel = r.Scenario!.TxPanel.Name,
|
||||
TxHardware = r.Scenario.TxHardware.Version,
|
||||
TxSoftware = r.Scenario.TxSoftware.Version,
|
||||
RxType = r.Scenario.RxType.Name,
|
||||
r.Scenario.TestPurpose,
|
||||
TestDate = r.Scenario.TestDate.ToString("yyyy-MM-dd"),
|
||||
r.Scenario.TestSequence,
|
||||
r.ChargerIndex,
|
||||
r.CoilIndex,
|
||||
r.DeltaQ,
|
||||
r.CurrentQ,
|
||||
r.RawQ,
|
||||
r.FodType
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var json = JsonSerializer.Serialize(records, new JsonSerializerOptions { WriteIndented = true });
|
||||
await File.WriteAllTextAsync(filePath, json);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出Ploss数据为JSON
|
||||
/// </summary>
|
||||
public async Task<string> ExportPlossToJsonAsync(string outputDir)
|
||||
{
|
||||
Directory.CreateDirectory(outputDir);
|
||||
var filePath = Path.Combine(outputDir, $"ploss_export_{DateTime.Now:yyyyMMdd_HHmmss}.json");
|
||||
|
||||
var records = await _context.PlossRecords
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.TxPanel)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.TxHardware)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.TxSoftware)
|
||||
.Include(r => r.Scenario)
|
||||
.ThenInclude(s => s!.RxType)
|
||||
.Select(r => new
|
||||
{
|
||||
r.Id,
|
||||
r.ScenarioId,
|
||||
TxPanel = r.Scenario!.TxPanel.Name,
|
||||
TxHardware = r.Scenario.TxHardware.Version,
|
||||
TxSoftware = r.Scenario.TxSoftware.Version,
|
||||
ScenarioRxType = r.Scenario.RxType.Name,
|
||||
r.Scenario.TestPurpose,
|
||||
TestDate = r.Scenario.TestDate.ToString("yyyy-MM-dd"),
|
||||
r.Scenario.TestSequence,
|
||||
RecordRxType = r.RxType,
|
||||
r.RxPower,
|
||||
r.TxPower,
|
||||
r.Vcoil,
|
||||
r.Vin,
|
||||
r.Isns,
|
||||
r.Ploss,
|
||||
r.Threshold,
|
||||
r.TriggerCount,
|
||||
r.FodResult,
|
||||
r.ProtocolType,
|
||||
r.PwmDuty,
|
||||
r.CoilIndex,
|
||||
Margin = r.Threshold - r.Ploss
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var json = JsonSerializer.Serialize(records, new JsonSerializerOptions { WriteIndented = true });
|
||||
await File.WriteAllTextAsync(filePath, json);
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
201
src/WCTDataMiner/Services/ParseService.cs
Normal file
201
src/WCTDataMiner/Services/ParseService.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Serilog;
|
||||
using WCTDataMiner.Data;
|
||||
using WCTDataMiner.Models;
|
||||
using WCTDataMiner.Parsers;
|
||||
|
||||
namespace WCTDataMiner.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 解析流程协调服务 - 协调文件名解析、行解析、批量写入
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析单个日志文件
|
||||
/// </summary>
|
||||
public async Task<ParseReport> ParseFileAsync(string filePath, bool force = false)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
Log.Error("文件不存在: {FilePath}", filePath);
|
||||
return new ParseReport(fileInfo.Name, 0, 0, 0, "文件不存在");
|
||||
}
|
||||
|
||||
// 解析文件名获取场景信息
|
||||
var scenarioInfo = _fileNameParser.Parse(fileInfo.Name);
|
||||
if (scenarioInfo == null)
|
||||
{
|
||||
Log.Error("无法解析文件名: {FileName}", fileInfo.Name);
|
||||
return new ParseReport(fileInfo.Name, 0, 0, 1, "文件名格式不匹配");
|
||||
}
|
||||
|
||||
Log.Information("解析文件: {FileName} => 场景: {TxPanel}/{TxHardware}/{TxSoftware}/{RxType}/{TestDate}",
|
||||
fileInfo.Name, scenarioInfo.TxPanel, scenarioInfo.TxHardware,
|
||||
scenarioInfo.TxSoftware, scenarioInfo.RxType, scenarioInfo.TestDate);
|
||||
|
||||
// 获取或创建测试场景
|
||||
var scenario = await _scenarioService.GetOrCreateScenarioAsync(scenarioInfo);
|
||||
|
||||
var lines = await File.ReadAllLinesAsync(filePath);
|
||||
var qfodRecords = new List<QfodRecord>();
|
||||
var plossRecords = new List<PlossRecord>();
|
||||
int errorCount = 0;
|
||||
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
|
||||
var lineNumber = i + 1;
|
||||
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量保存
|
||||
await BatchInsertAsync(qfodRecords, plossRecords);
|
||||
|
||||
// 更新场景统计
|
||||
await UpdateScenarioStatsAsync(scenario.Id, qfodRecords.Count, plossRecords.Count);
|
||||
|
||||
Log.Information("解析完成: {FileName} => Qfod: {QfodCount}, Ploss: {PlossCount}, Errors: {ErrorCount}",
|
||||
fileInfo.Name, qfodRecords.Count, plossRecords.Count, errorCount);
|
||||
|
||||
return new ParseReport(fileInfo.Name, qfodRecords.Count, plossRecords.Count, errorCount, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析目录下所有日志文件
|
||||
/// </summary>
|
||||
public async Task<List<ParseReport>> ParseDirectoryAsync(string dirPath, bool recursive = false, bool force = false)
|
||||
{
|
||||
var dirInfo = new DirectoryInfo(dirPath);
|
||||
if (!dirInfo.Exists)
|
||||
{
|
||||
Log.Error("目录不存在: {DirPath}", dirPath);
|
||||
return [new ParseReport(dirPath, 0, 0, 0, "目录不存在")];
|
||||
}
|
||||
|
||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
||||
var logFiles = dirInfo.GetFiles("*.log", searchOption);
|
||||
|
||||
Log.Information("发现 {FileCount} 个日志文件", logFiles.Length);
|
||||
|
||||
var reports = new List<ParseReport>();
|
||||
foreach (var file in logFiles)
|
||||
{
|
||||
var report = await ParseFileAsync(file.FullName, force);
|
||||
reports.Add(report);
|
||||
}
|
||||
|
||||
return reports;
|
||||
}
|
||||
|
||||
private async Task BatchInsertAsync(
|
||||
List<QfodRecord> qfodRecords,
|
||||
List<PlossRecord> plossRecords)
|
||||
{
|
||||
if (qfodRecords.Count == 0 && plossRecords.Count == 0)
|
||||
return;
|
||||
|
||||
await using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// 批量插入 QfodRecord
|
||||
foreach (var batch in qfodRecords.Chunk(_batchSize))
|
||||
{
|
||||
_context.QfodRecords.AddRange(batch);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// 批量插入 PlossRecord
|
||||
foreach (var batch in plossRecords.Chunk(_batchSize))
|
||||
{
|
||||
_context.PlossRecords.AddRange(batch);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateScenarioStatsAsync(Guid scenarioId, int qfodCount, int plossCount)
|
||||
{
|
||||
var scenario = await _context.TestScenarios.FindAsync(scenarioId);
|
||||
if (scenario != null)
|
||||
{
|
||||
scenario.QfodCount += qfodCount;
|
||||
scenario.PlossCount += plossCount;
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析报告
|
||||
/// </summary>
|
||||
public record ParseReport(
|
||||
string FileName,
|
||||
int QfodCount,
|
||||
int PlossCount,
|
||||
int ErrorCount,
|
||||
string? ErrorMessage
|
||||
)
|
||||
{
|
||||
public bool IsSuccess => ErrorMessage == null;
|
||||
}
|
||||
76
src/WCTDataMiner/Services/ScenarioService.cs
Normal file
76
src/WCTDataMiner/Services/ScenarioService.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WCTDataMiner.Data;
|
||||
using WCTDataMiner.Models;
|
||||
using WCTDataMiner.Parsers;
|
||||
|
||||
namespace WCTDataMiner.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 测试场景服务 - 负责测试场景的创建和查询
|
||||
/// </summary>
|
||||
public class ScenarioService
|
||||
{
|
||||
private readonly WctMinerDbContext _context;
|
||||
private readonly DimensionService _dimensionService;
|
||||
|
||||
public ScenarioService(WctMinerDbContext context, DimensionService dimensionService)
|
||||
{
|
||||
_context = context;
|
||||
_dimensionService = dimensionService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据场景信息获取或创建测试场景
|
||||
/// </summary>
|
||||
public async Task<TestScenario> GetOrCreateScenarioAsync(ScenarioInfo info)
|
||||
{
|
||||
// 1. 获取或创建各维度
|
||||
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);
|
||||
|
||||
// 2. 检查场景是否已存在(利用唯一约束)
|
||||
var existing = await _context.TestScenarios
|
||||
.FirstOrDefaultAsync(s =>
|
||||
s.TxPanelId == txPanel.Id &&
|
||||
s.TxHardwareId == txHardware.Id &&
|
||||
s.TxSoftwareId == txSoftware.Id &&
|
||||
s.RxTypeId == rxType.Id &&
|
||||
s.TestPurpose == info.TestPurpose &&
|
||||
s.TestDate == info.TestDate &&
|
||||
s.TestSequence == info.TestSequence);
|
||||
|
||||
if (existing != null) return existing;
|
||||
|
||||
// 3. 创建新场景
|
||||
var scenario = new TestScenario
|
||||
{
|
||||
TxPanelId = txPanel.Id,
|
||||
TxHardwareId = txHardware.Id,
|
||||
TxSoftwareId = txSoftware.Id,
|
||||
RxTypeId = rxType.Id,
|
||||
TestPurpose = info.TestPurpose,
|
||||
TestDate = info.TestDate,
|
||||
TestSequence = info.TestSequence
|
||||
};
|
||||
|
||||
_context.TestScenarios.Add(scenario);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return scenario;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取场景详情(含维度属性)
|
||||
/// </summary>
|
||||
public async Task<TestScenario?> GetScenarioWithDimensionsAsync(Guid scenarioId)
|
||||
{
|
||||
return await _context.TestScenarios
|
||||
.Include(s => s.TxPanel)
|
||||
.Include(s => s.TxHardware)
|
||||
.Include(s => s.TxSoftware)
|
||||
.Include(s => s.RxType)
|
||||
.FirstOrDefaultAsync(s => s.Id == scenarioId);
|
||||
}
|
||||
}
|
||||
128
src/WCTDataMiner/Services/StatsService.cs
Normal file
128
src/WCTDataMiner/Services/StatsService.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using WCTDataMiner.Data;
|
||||
using WCTDataMiner.Models;
|
||||
|
||||
namespace WCTDataMiner.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 统计查询服务
|
||||
/// </summary>
|
||||
public class StatsService
|
||||
{
|
||||
private readonly WctMinerDbContext _context;
|
||||
|
||||
public StatsService(WctMinerDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取整体统计摘要
|
||||
/// </summary>
|
||||
public async Task<StatsSummary> GetSummaryAsync()
|
||||
{
|
||||
var scenarioCount = await _context.TestScenarios.CountAsync();
|
||||
var qfodCount = await _context.QfodRecords.CountAsync();
|
||||
var plossCount = await _context.PlossRecords.CountAsync();
|
||||
var panelCount = await _context.TxPanels.CountAsync();
|
||||
var hardwareCount = await _context.TxHardwares.CountAsync();
|
||||
var softwareCount = await _context.TxSoftwares.CountAsync();
|
||||
var rxTypeCount = await _context.RxTypes.CountAsync();
|
||||
|
||||
return new StatsSummary(
|
||||
scenarioCount, qfodCount, plossCount,
|
||||
panelCount, hardwareCount, softwareCount, rxTypeCount
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按FOD类型统计Qfod记录
|
||||
/// </summary>
|
||||
public async Task<List<FodTypeStats>> GetStatsByFodTypeAsync()
|
||||
{
|
||||
// 先加载到内存再分组(EF Core 对 GroupBy + 自定义类型有限制)
|
||||
var data = await _context.QfodRecords
|
||||
.Select(r => r.FodType)
|
||||
.ToListAsync();
|
||||
|
||||
return data
|
||||
.GroupBy(f => f)
|
||||
.Select(g => new FodTypeStats(g.Key, g.Count()))
|
||||
.OrderBy(s => s.FodType)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按面板类型统计
|
||||
/// </summary>
|
||||
public async Task<List<DimensionStats>> GetStatsByPanelAsync()
|
||||
{
|
||||
var data = await _context.TestScenarios
|
||||
.Include(s => s.TxPanel)
|
||||
.Select(s => s.TxPanel.Name)
|
||||
.ToListAsync();
|
||||
|
||||
return data
|
||||
.GroupBy(n => n)
|
||||
.Select(g => new DimensionStats(g.Key, g.Count()))
|
||||
.OrderByDescending(s => s.Count)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按硬件版本统计
|
||||
/// </summary>
|
||||
public async Task<List<DimensionStats>> GetStatsByHardwareAsync()
|
||||
{
|
||||
var data = await _context.TestScenarios
|
||||
.Include(s => s.TxHardware)
|
||||
.Select(s => s.TxHardware.Version)
|
||||
.ToListAsync();
|
||||
|
||||
return data
|
||||
.GroupBy(v => v)
|
||||
.Select(g => new DimensionStats(g.Key, g.Count()))
|
||||
.OrderByDescending(s => s.Count)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按RX类型统计
|
||||
/// </summary>
|
||||
public async Task<List<DimensionStats>> GetStatsByRxTypeAsync()
|
||||
{
|
||||
var data = await _context.TestScenarios
|
||||
.Include(s => s.RxType)
|
||||
.Select(s => s.RxType.Name)
|
||||
.ToListAsync();
|
||||
|
||||
return data
|
||||
.GroupBy(n => n)
|
||||
.Select(g => new DimensionStats(g.Key, g.Count()))
|
||||
.OrderByDescending(s => s.Count)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
43
src/WCTDataMiner/Services/ThresholdCalculator.cs
Normal file
43
src/WCTDataMiner/Services/ThresholdCalculator.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
namespace WCTDataMiner.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 阈值计算器 - 根据接收功率查表获取动态阈值
|
||||
/// </summary>
|
||||
public static class ThresholdCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据RxPower获取对应的动态阈值
|
||||
/// </summary>
|
||||
/// <param name="rxPower">接收端功率(mW)</param>
|
||||
/// <returns>当前功率段的阈值</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算安全余量
|
||||
/// </summary>
|
||||
/// <param name="threshold">阈值</param>
|
||||
/// <param name="ploss">计算损耗值</param>
|
||||
/// <returns>安全余量(正值安全,负值危险)</returns>
|
||||
public static int CalculateMargin(int threshold, int ploss)
|
||||
{
|
||||
return threshold - ploss;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否触发FOD
|
||||
/// </summary>
|
||||
/// <param name="ploss">计算损耗值</param>
|
||||
/// <param name="threshold">阈值</param>
|
||||
/// <returns>是否触发FOD</returns>
|
||||
public static bool IsFodTriggered(int ploss, int threshold)
|
||||
{
|
||||
return ploss > threshold;
|
||||
}
|
||||
}
|
||||
43
src/WCTDataMiner/WCTDataMiner.csproj
Normal file
43
src/WCTDataMiner/WCTDataMiner.csproj
Normal file
@@ -0,0 +1,43 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- ORM -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
<!-- CLI -->
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
|
||||
<!-- Configuration -->
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
|
||||
<!-- Logging -->
|
||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user