docs: 完善数据模型设计

- 重命名 log_file 为 test_scenario(测试场景)
- 拆分维度表:tx_panel、tx_hardware、tx_software、rx_type
- 采用星型模型设计,消除数据冗余
- 为所有表添加软删除字段 is_deleted
- 更新关系说明,包含全局查询过滤器配置
- 更新 ER 图和查询示例

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ssss
2026-07-02 15:53:30 +08:00
commit 28cb2533a7
23 changed files with 2919 additions and 0 deletions

View File

@@ -0,0 +1,168 @@
# CLI 命令设计
> 所属模块:[架构概览](./概览.md)
---
## 1. 命令概览
| 命令 | 说明 |
|------|------|
| `parse` | 解析日志文件并存储到数据库 |
| `stats` | 查询统计信息 |
| `export` | 导出数据 |
| `clean` | 清理数据 |
---
## 2. 命令详解
### 2.1 parse - 解析日志文件
```bash
# 解析单个文件
dotnet run parse --file path/to/log.txt
# 解析目录下所有文件
dotnet run parse --dir path/to/logs/
# 解析目录(递归)
dotnet run parse --dir path/to/logs/ --recursive
# 强制重新解析(覆盖已有数据)
dotnet run parse --file log.txt --force
```
### 2.2 stats - 查询统计
```bash
# 查看文件解析统计
dotnet run stats --file log.txt
# 查看整体统计
dotnet run stats --summary
# 按 FOD 类型统计
dotnet run stats --by-fod-type
```
### 2.3 export - 导出数据
```bash
# 导出为 CSV
dotnet run export --format csv --output ./output/
# 导出为 JSON
dotnet run export --format json --output ./output/
# 按文件导出
dotnet run export --file log.txt --format csv
```
### 2.4 clean - 清理数据
```bash
# 清理指定文件的数据
dotnet run clean --file log.txt --confirm
# 清理所有数据
dotnet run clean --all --confirm
```
---
## 3. 命令实现
```csharp
// Commands/ParseCommand.cs
using System.CommandLine;
namespace WCTDataMiner.Commands;
public class ParseCommand : Command
{
public ParseCommand(ParseService parseService)
: base("parse", "Parse log files and store data to database")
{
var fileOption = new Option<string>(
"--file",
"Path to a single log file to parse"
);
var dirOption = new Option<string>(
"--dir",
"Path to directory containing log files"
);
var recursiveOption = new Option<bool>(
"--recursive",
() => false,
"Recursively search subdirectories"
);
var forceOption = new Option<bool>(
"--force",
() => false,
"Force re-parse even if file already parsed"
);
AddOption(fileOption);
AddOption(dirOption);
AddOption(recursiveOption);
AddOption(forceOption);
this.SetHandler(async (file, dir, recursive, force) =>
{
if (!string.IsNullOrEmpty(file))
{
await parseService.ParseFileAsync(file, force);
}
else if (!string.IsNullOrEmpty(dir))
{
await parseService.ParseDirectoryAsync(dir, recursive, force);
}
else
{
Console.WriteLine("Please specify --file or --dir");
}
}, fileOption, dirOption, recursiveOption, forceOption);
}
}
```
---
## 4. Program.cs 入口
```csharp
// Program.cs
using System.CommandLine;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using WCTDataMiner.Commands;
var builder = Host.CreateApplicationBuilder(args);
// 注册服务
builder.Services.AddScoped<ParseService>();
builder.Services.AddScoped<StatsService>();
builder.Services.AddScoped<ExportService>();
builder.Services.AddScoped<CleanService>();
builder.Services.AddDatabaseServices(builder.Configuration);
var app = builder.Build();
// 创建根命令
var rootCommand = new RootCommand("WCTDataMiner - FOD Log Data Parser");
// 使用服务提供者获取服务
using var scope = app.Services.CreateScope();
var provider = scope.ServiceProvider;
rootCommand.AddCommand(new ParseCommand(provider.GetRequiredService<ParseService>()));
rootCommand.AddCommand(new StatsCommand(provider.GetRequiredService<StatsService>()));
rootCommand.AddCommand(new ExportCommand(provider.GetRequiredService<ExportService>()));
rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService<CleanService>()));
return await rootCommand.InvokeAsync(args);
```

View File

@@ -0,0 +1,122 @@
# DbContext 工厂
> 所属模块:[数据库切换支持](./概览.md)
---
## 1. 接口定义
```csharp
// Data/IDbContextFactory.cs
using WCTDataMiner.Data;
namespace WCTDataMiner.Data;
public interface IDbContextFactory
{
WctMinerDbContext CreateDbContext();
}
```
---
## 2. 工厂实现
```csharp
// Data/DbContextFactory.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace WCTDataMiner.Data;
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["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);
}
}
}
```
---
## 3. 依赖注入扩展
```csharp
// Extensions/ServiceCollectionExtensions.cs
using Microsoft.Extensions.DependencyInjection;
using WCTDataMiner.Data;
namespace WCTDataMiner.Extensions;
public static class ServiceCollectionExtensions
{
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;
}
}
```

View File

@@ -0,0 +1,45 @@
# 数据库切换支持
> 所属模块:[架构概览](../概览.md)
---
## 文档索引
| 文档 | 说明 |
|------|------|
| [DbContext 工厂](./DbContext工厂.md) | 数据库上下文工厂设计 |
| [迁移策略](./迁移策略.md) | 数据库迁移和切换步骤 |
---
## 切换策略概览
```
┌─────────────────────────────────────────────────────────────┐
│ appsettings.json │
│ "Database": { "Type": "sqlite" | "postgresql" } │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ DbContextFactory │
│ 读取配置 → 选择 Provider → 创建 DbContext │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ WctMinerDbContext │
│ 统一的 DbModel 配置,适配两种数据库 │
└─────────────────────────────────────────────────────────────┘
```
---
## 当前状态
| 项目 | 状态 |
|------|------|
| SQLite | ✅ 默认启用 |
| PostgreSQL | ✅ 架构支持,待配置 |
| 切换方式 | 配置文件切换 |

View File

@@ -0,0 +1,81 @@
# 数据库迁移策略
> 所属模块:[数据库切换支持](./概览.md)
---
## 1. 迁移命令
### SQLite
```bash
# 添加迁移
dotnet ef migrations add InitialCreate --output-dir Migrations/SQLite
# 应用迁移
dotnet ef database update
```
### PostgreSQL
```bash
# 切换配置后添加迁移
dotnet ef migrations add InitialCreate --output-dir Migrations/PostgreSQL -- --environment PostgreSQL
# 应用迁移
dotnet ef database update -- --environment PostgreSQL
```
---
## 2. 切换步骤
### 从 SQLite 切换到 PostgreSQL
| 步骤 | 操作 | 说明 |
|------|------|------|
| 1 | 修改配置 | `appsettings.json``Database:Type` 改为 `postgresql` |
| 2 | 设置连接 | 配置 `Host`, `Port`, `Name`, `User`, `Password` |
| 3 | 创建数据库 | `CREATE DATABASE wctminer;` |
| 4 | 应用迁移 | `dotnet ef database update` |
| 5 | 数据迁移 | 使用工具迁移 SQLite 数据到 PostgreSQL |
---
## 3. 配置文件示例
### SQLite (默认)
```json
{
"Database": {
"Type": "sqlite",
"Path": "./data/database/wctminer.db"
}
}
```
### PostgreSQL
```json
{
"Database": {
"Type": "postgresql",
"Host": "localhost",
"Port": 5432,
"Name": "wctminer",
"User": "wctminer_user",
"Password": "${DB_PASSWORD}"
}
}
```
---
## 4. 数据迁移工具
推荐使用 [pgloader](https://github.com/dimitri/pgloader) 迁移 SQLite 到 PostgreSQL
```bash
pgloader ./data/database/wctminer.db postgresql://user:password@localhost:5432/wctminer
```

View File

@@ -0,0 +1,165 @@
# Ploss 解析器
> 所属模块:[解析器设计](./概览.md)
---
## 1. 日志格式
**格式:** `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`
> 括号数字表示有效字段编号X 表示忽略该位置数据
---
## 2. 字段定义
| 编号 | 字段名称 | 数据类型 | 说明 |
|------|----------|----------|------|
| (1) | RxType | byte | 接收端类型标识 |
| (2) | RxPower | short | 接收端功率mW |
| (3) | TxPower | short | 发射端功率mW |
| (4) | Vcoil | short | 发射线圈电压 |
| (5) | Vin | short | 输入电压 |
| (6) | Isns | short | 输入电流采样值 |
| (7) | Ploss | int | **核心值**:计算损耗(负数表示安全) |
| (8) | Threshold | int | **安全红线**:当前功率段阈值 |
| (9) | TriggerCount | short | 触发计数器 |
| (10) | FodResult | short | FOD判定结果状态 |
| (11) | ProtocolType | byte | 充电协议类型 |
| (12) | PwmDuty | short | PWM控制占空比 |
| (13) | CoilIndex | byte | 充电线圈索引 |
---
## 3. 正则表达式
```csharp
// Parsers/PlossParser.cs
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+)$",
RegexOptions.Compiled
);
```
**捕获组映射:**
| 捕获组 | 字段 | 类型转换 |
|--------|------|----------|
| Group 1 | RxType | byte.Parse() |
| Group 2 | RxPower | short.Parse() |
| Group 3 | TxPower | short.Parse() |
| Group 4 | Vcoil | short.Parse() |
| Group 5 | Vin | short.Parse() |
| Group 6 | Isns | short.Parse() |
| Group 7 | Ploss | int.Parse() |
| Group 8 | Threshold | int.Parse() |
| Group 9 | TriggerCount | short.Parse() |
| Group 10 | FodResult | short.Parse() |
| Group 11 | ProtocolType | byte.Parse() |
| Group 12 | PwmDuty | short.Parse() |
| Group 13 | CoilIndex | byte.Parse() |
---
## 4. 解析器实现
```csharp
// Parsers/PlossParser.cs
using System.Text.RegularExpressions;
using WCTDataMiner.Models;
namespace WCTDataMiner.Parsers;
public class PlossParser : IParser<PlossRecord>
{
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+)$",
RegexOptions.Compiled
);
public bool CanParse(string line) => PlossPattern.IsMatch(line);
public ParseResult<PlossRecord> Parse(string line, int lineNumber, Guid logFileId)
{
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
{
LogFileId = logFileId,
LineNumber = lineNumber,
RxType = byte.Parse(match.Groups[1].Value),
RxPower = short.Parse(match.Groups[2].Value),
TxPower = short.Parse(match.Groups[3].Value),
Vcoil = short.Parse(match.Groups[4].Value),
Vin = short.Parse(match.Groups[5].Value),
Isns = short.Parse(match.Groups[6].Value),
Ploss = int.Parse(match.Groups[7].Value),
Threshold = int.Parse(match.Groups[8].Value),
TriggerCount = short.Parse(match.Groups[9].Value),
FodResult = short.Parse(match.Groups[10].Value),
ProtocolType = byte.Parse(match.Groups[11].Value),
PwmDuty = short.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}"
);
}
}
}
```
---
## 5. 核心判定逻辑
### 5.1 FOD 触发条件
```
if (Ploss > Threshold) → 计数器累积
if (Count > Limit) → 触发 FOD 报警,停止充电
```
### 5.2 安全余量计算
```csharp
// 派生属性(不落库)
Margin = Threshold - Ploss
```
| Margin 状态 | 含义 |
|-------------|------|
| Margin > 0 | 安全,损耗低于阈值 |
| Margin < 0 | 危险,损耗超过阈值,可能触发 FOD |
---
## 6. 动态阈值表
阈值根据接收功率动态查表:
| 接收功率区间 (Rx Power) | 阈值 (Threshold) | 备注 |
|------------------------|------------------|------|
| 0 ~ 5W (0-5000 mW) | 350 mW | 低功率最敏感 |
| 5 ~ 10W (5000-10000 mW) | 500 mW | |
| 10 ~ 15W (10000-15000 mW) | 750 mW | |
| 15 ~ 30W (15000-30000 mW) | 1000 mW | 示例 Log 处于此区间 |
| 30 ~ 50W (30000-50000 mW) | 1250 mW | 高功率允许更多损耗 |

View File

@@ -0,0 +1,120 @@
# Qfod 解析器
> 所属模块:[解析器设计](./概览.md)
---
## 1. 日志格式
**格式:** `X#:Q A->B C D E`
**示例:** `0#:Q 1->25 45.5 20.5 2`
---
## 2. 字段定义
| 字段标识 | 字段名称 | 数据类型 | 取值范围 | 说明 |
|----------|----------|----------|----------|------|
| X | ChargerIndex | byte | 0, 1 | 充电器索引 |
| A | CoilIndex | byte | 0, 1, 2 | 线圈索引 |
| B | DeltaQ | int | - | Q值变化量ΔQ = CurrentQ - RawQ |
| C | CurrentQ | float | - | 当前实时Q值 |
| D | RawQ | float | - | 原始Q值 |
| E | FodType | byte | 见Table 2 | 异物类型编码 |
---
## 3. 正则表达式
```csharp
// Parsers/QfodParser.cs
private static readonly Regex QfodPattern = new(
@"^(\d)#:Q\s+(\d+)->(-?\d+)\s+([-\d.]+)\s+([-\d.]+)\s+(\d+)$",
RegexOptions.Compiled
);
```
**捕获组映射:**
| 捕获组 | 字段 | 类型转换 |
|--------|------|----------|
| Group 1 | ChargerIndex | byte.Parse() |
| Group 2 | CoilIndex | byte.Parse() |
| Group 3 | DeltaQ | int.Parse() |
| Group 4 | CurrentQ | float.Parse() |
| Group 5 | RawQ | float.Parse() |
| Group 6 | FodType | byte.Parse() |
---
## 4. 解析器实现
```csharp
// Parsers/QfodParser.cs
using System.Text.RegularExpressions;
using WCTDataMiner.Models;
namespace WCTDataMiner.Parsers;
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, int lineNumber, Guid logFileId)
{
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
{
LogFileId = logFileId,
LineNumber = lineNumber,
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}"
);
}
}
}
```
---
## 5. 检测通道组合
`ChargerIndex + CoilIndex` 组合唯一标识一个检测通道:
| ChargerIndex | CoilIndex | 检测通道 |
|--------------|-----------|----------|
| 0 | 0 | 充电器0 线圈0 |
| 0 | 1 | 充电器0 线圈1 |
| 0 | 2 | 充电器0 线圈2 |
| 1 | 0 | 充电器1 线圈0 |
| 1 | 1 | 充电器1 线圈1 |
| 1 | 2 | 充电器1 线圈2 |

View File

@@ -0,0 +1,78 @@
# 解析器设计
> 所属模块:[架构概览](../概览.md)
---
## 文档索引
| 文档 | 说明 |
|------|------|
| [Qfod 解析器](./Qfod解析器.md) | Qfod 格式日志解析设计 |
| [Ploss 解析器](./Ploss解析器.md) | Ploss FOD 格式日志解析设计 |
---
## 通用接口定义
```csharp
// Parsers/IParser.cs
namespace WCTDataMiner.Parsers;
public interface IParser<TRecord>
{
/// <summary>
/// 判断是否可以解析该行
/// </summary>
bool CanParse(string line);
/// <summary>
/// 解析日志行
/// </summary>
ParseResult<TRecord> Parse(string line, int lineNumber, Guid logFileId);
}
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 };
}
```
---
## 解析流程
```mermaid
flowchart TD
A[读取日志文件] --> B[逐行遍历]
B --> C{判断行类型}
C -->|Qfod| D[QfodParser.Parse]
C -->|Ploss| E[PlossParser.Parse]
C -->|其他| F[跳过]
D --> G{解析成功?}
E --> G
G -->|成功| H[添加到记录列表]
G -->|失败| I[记录到 ParseError]
H --> J[批量保存到数据库]
I --> J
J --> K[输出解析报告]
```
---
## 错误类型定义
| ErrorType | 说明 | 处理方式 |
|-----------|------|----------|
| FORMAT_MISMATCH | 行格式不匹配正则表达式 | 跳过该行,记录原始内容 |
| TYPE_CONVERSION | 字段类型转换失败 | 跳过该行,记录错误字段 |
| VALUE_OUT_OF_RANGE | 字段值超出有效范围 | 使用边界值或跳过 |

View File

@@ -0,0 +1,39 @@
# 安全架构
> 所属模块:[架构概览](./概览.md)
---
## 1. 概述
> [A] 本系统为本地日志解析工具,无网络暴露,安全需求较低
---
## 2. 认证方案
| 项目 | 方案 |
|------|------|
| 认证方式 | [A] 无(本地工具) |
| 数据库访问 | 本地文件权限控制SQLite或连接串认证PostgreSQL |
---
## 3. 数据安全
| 项目 | 内容 |
|------|------|
| 数据类型 | [!] 日志数据不含敏感信息,为设备检测数据 |
| 数据保护 | 本地存储,操作系统文件权限控制 |
| 备份策略 | [?] 需确认:是否需要定期备份? |
---
## 4. 安全检查清单
| 检查项 | 状态 |
|--------|------|
| 无网络暴露 | ✅ 本地工具 |
| 数据库认证 | ✅ SQLite 本地文件权限 / PostgreSQL 连接串 |
| 敏感数据处理 | ✅ 无敏感数据 |
| 日志脱敏 | ✅ 不需要(设备检测数据) |

View File

@@ -0,0 +1,58 @@
# 技术选型
> 所属模块:[架构概览](./概览.md)
---
## 1. 技术栈总览
| 层级 | 技术选型 | 版本 | 选型理由 |
|------|----------|------|----------|
| 运行时 | .NET | 8.0 LTS | 长期支持版本,性能优秀,跨平台 |
| 语言 | C# | 12.0 | 强类型、现代语法、优秀的正则表达式支持 |
| ORM | Entity Framework Core | 8.0 | .NET 标准 ORMCode First 支持LINQ 查询 |
| 数据库 | SQLite / PostgreSQL | - | SQLite 适合单机PostgreSQL 适合多用户并发 |
| 日志框架 | Serilog | 4.0+ | 结构化日志,多种 Sink 支持 |
| 配置管理 | Microsoft.Extensions.Configuration | - | 强类型配置,支持 JSON/环境变量 |
| CLI 框架 | System.CommandLine | 2.0-beta4 | 官方 CLI 库,参数解析完善 |
| 正则表达式 | System.Text.RegularExpressions | - | .NET 原生支持,编译正则高效 |
---
## 2. 技术决策记录
| # | 决策点 | 选择 | 备选方案 | 决策理由 |
|---|--------|------|----------|----------|
| 1 | .NET 版本 | .NET 8.0 LTS | .NET 9.0 | LTS 版本稳定,企业级应用首选 |
| 2 | 数据库选择 | SQLite当前/ PostgreSQL后续 | SQL Server | 初期单机使用 SQLite后续可切换 PostgreSQL |
| 3 | 解析模式 | 批处理 | 流式处理 | 日志文件是静态文件,批处理更合适 |
| 4 | 数据格式 | 结构化存储 | 原始存储 + 计算 | 解析后存储便于后续分析 |
| 5 | ORM 模式 | Code First | Database First | 便于版本控制,迁移自动化 |
| 6 | 数据库切换 | 抽象 DbContext 工厂 | 硬编码连接 | 支持配置切换数据库类型 |
---
## 3. NuGet 包依赖
```xml
<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" />
<!-- CLI -->
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<!-- 配置 -->
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<!-- 日志 -->
<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>
```

View File

@@ -0,0 +1,205 @@
# 数据处理流程
> 所属模块:[架构概览](./概览.md)
---
## 1. 完整解析流程
```mermaid
sequenceDiagram
participant CLI as CLI Entry
participant PS as ParseService
participant LR as LogFileReader
participant QP as QfodParser
participant PP as PlossParser
participant EHS as ErrorHandlingService
participant DB as Database
CLI->>LR: 扫描日志目录
LR-->>CLI: 返回文件列表
loop 每个日志文件
CLI->>PS: ParseFileAsync(filePath)
PS->>DB: INSERT log_file (status=processing)
DB-->>PS: 返回 file_id
PS->>LR: 读取文件内容
LR-->>PS: 返回行列表
loop 逐行遍历
alt 是 Qfod 行
PS->>QP: Parse(line)
QP-->>PS: ParseResult
else 是 Ploss 行
PS->>PP: Parse(line)
PP-->>PS: ParseResult
end
alt 解析成功
PS->>PS: 加入记录列表
else 解析失败
PS->>EHS: RecordError(line, error)
end
end
PS->>DB: BATCH INSERT records
PS->>DB: BATCH INSERT errors
PS->>DB: UPDATE log_file (counts, status=completed)
PS-->>CLI: 返回 ParseReport
end
CLI-->>CLI: 输出解析报告
```
---
## 2. ParseService 实现
```csharp
// Services/ParseService.cs
using Microsoft.EntityFrameworkCore;
using WCTDataMiner.Data;
using WCTDataMiner.Models;
using WCTDataMiner.Parsers;
namespace WCTDataMiner.Services;
public class ParseService
{
private readonly WctMinerDbContext _context;
private readonly QfodParser _qfodParser;
private readonly PlossParser _plossParser;
private readonly ErrorHandlingService _errorService;
private readonly ILogger<ParseService> _logger;
private readonly int _batchSize;
public ParseService(
WctMinerDbContext context,
QfodParser qfodParser,
PlossParser plossParser,
ErrorHandlingService errorService,
ILogger<ParseService> logger,
IConfiguration config)
{
_context = context;
_qfodParser = qfodParser;
_plossParser = plossParser;
_errorService = errorService;
_logger = logger;
_batchSize = config.GetValue("Parser:BatchSize", 1000);
}
public async Task<ParseReport> ParseFileAsync(string filePath)
{
var fileInfo = new FileInfo(filePath);
var logFile = await CreateLogFileRecordAsync(fileInfo);
var lines = await File.ReadAllLinesAsync(filePath);
var qfodRecords = new List<QfodRecord>();
var plossRecords = new List<PlossRecord>();
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i];
var lineNumber = i + 1;
if (_qfodParser.CanParse(line))
{
var result = _qfodParser.Parse(line, lineNumber, logFile.Id);
if (result.IsSuccess)
qfodRecords.Add(result.Record!);
else
_errorService.RecordError(logFile.Id, lineNumber, line, result.ErrorType!, result.ErrorMessage!);
}
else if (_plossParser.CanParse(line))
{
var result = _plossParser.Parse(line, lineNumber, logFile.Id);
if (result.IsSuccess)
plossRecords.Add(result.Record!);
else
_errorService.RecordError(logFile.Id, lineNumber, line, result.ErrorType!, result.ErrorMessage!);
}
}
// 批量保存
await BatchInsertAsync(qfodRecords, plossRecords, _errorService.GetErrors());
// 更新统计
await UpdateLogFileStatsAsync(logFile.Id, qfodRecords.Count, plossRecords.Count, _errorService.ErrorCount);
return new ParseReport(fileInfo.Name, qfodRecords.Count, plossRecords.Count, _errorService.ErrorCount);
}
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;
}
private async Task BatchInsertAsync(
List<QfodRecord> qfodRecords,
List<PlossRecord> plossRecords,
List<ParseError> errors)
{
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();
}
// 批量插入 ParseError
foreach (var batch in errors.Chunk(_batchSize))
{
_context.ParseErrors.AddRange(batch);
await _context.SaveChangesAsync();
}
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
private async Task UpdateLogFileStatsAsync(Guid logFileId, int qfodCount, int plossCount, int errorCount)
{
var logFile = await _context.LogFiles.FindAsync(logFileId);
if (logFile != null)
{
logFile.QfodCount = qfodCount;
logFile.PlossCount = plossCount;
logFile.ErrorCount = errorCount;
logFile.Status = ParseStatus.Completed;
await _context.SaveChangesAsync();
}
}
}
public record ParseReport(string FileName, int QfodCount, int PlossCount, int ErrorCount);
```

View File

@@ -0,0 +1,68 @@
# Architecture: WCTDataMiner
> 无线充电 FOD异物检测日志数据采集系统从日志文件中解析 Qfod 和 Ploss 数据并存储到数据库
---
## 文档索引
| 文档 | 说明 |
|------|------|
| [技术选型](./技术选型.md) | 技术栈总览、决策记录 |
| [模块划分](./模块划分.md) | 模块总览、分层架构、依赖关系 |
| [安全架构](./安全架构.md) | 认证方案、数据安全 |
| [解析器设计](./parser/概览.md) | Qfod/Ploss 解析器详细设计 |
| [数据处理流程](./数据处理流程.md) | 解析流程、批量写入、错误处理 |
| [配置管理](./配置管理.md) | 配置文件、配置类定义 |
| [CLI 命令设计](./CLI命令设计.md) | 命令行接口定义 |
| [数据库切换支持](./database/概览.md) | SQLite/PostgreSQL 切换方案 |
---
## 架构图
```
┌─────────────────────────────────────────────────────────────┐
│ CLI Entry Point │
│ (Program.cs / System.CommandLine) │
├─────────────────────────────────────────────────────────────┤
│ Service Layer │
├─────────────────────────────────────────────────────────────┤
│ ParseService │ ErrorHandlingService │ ThresholdCalc │
├─────────────────────────────────────────────────────────────┤
│ Parser Layer │
├─────────────────────────────────────────────────────────────┤
│ QfodParser │ PlossParser │ LogFileReader │
├─────────────────────────────────────────────────────────────┤
│ Data Layer │
├─────────────────────────────────────────────────────────────┤
│ Models │ WctMinerDbContext │ IDbContextFactory │
├─────────────────────────────────────────────────────────────┤
│ Infrastructure │
├─────────────────────────────────────────────────────────────┤
│ Configuration │ Serilog │ AppSettings │
└─────────────────────────────────────────────────────────────┘
```
---
## 核心设计原则
| 原则 | 说明 |
|------|------|
| 技术栈 | .NET 8.0 LTS + C# 12.0 + EF Core 8.0 |
| 数据库 | SQLite当前/ PostgreSQL后续配置切换 |
| 解析模式 | 批处理,正则表达式匹配 |
| ORM 模式 | Code First支持迁移自动化 |
| CLI 框架 | System.CommandLine命令模式 |
---
## 假设/歧义标注
| 标注 | 内容 |
|------|------|
| [A] | 使用 .NET 8.0 LTS 版本 |
| [A] | 初期使用 SQLite架构支持切换 PostgreSQL |
| [?] | 增量解析策略需确认 |
| [?] | 多次解析同一文件的处理策略需确认 |

View File

@@ -0,0 +1,143 @@
# 模块划分
> 所属模块:[架构概览](./概览.md)
---
## 1. 模块总览
```
┌─────────────────────────────────────────────────────────────┐
│ CLI Entry Point │
│ (Program.cs / System.CommandLine) │
├─────────────────────────────────────────────────────────────┤
│ Service Layer │
├─────────────────────────────────────────────────────────────┤
│ ParseService │ ErrorHandlingService │ ThresholdCalc │
├─────────────────────────────────────────────────────────────┤
│ Parser Layer │
├─────────────────────────────────────────────────────────────┤
│ QfodParser │ PlossParser │ LogFileReader │
├─────────────────────────────────────────────────────────────┤
│ Data Layer │
├─────────────────────────────────────────────────────────────┤
│ Models │ WctMinerDbContext │ IDbContextFactory │
├─────────────────────────────────────────────────────────────┤
│ Infrastructure │
├─────────────────────────────────────────────────────────────┤
│ Configuration │ Serilog │ AppSettings │
└─────────────────────────────────────────────────────────────┘
```
---
## 2. 模块职责
| 模块名 | 职责 | 依赖模块 |
|--------|------|----------|
| CLI Entry | 接收命令行参数、调度解析流程 | ParseService, Infrastructure |
| ParseService | 协调解析流程、批量写入、统计更新 | Parser Layer, DbContext, Infrastructure |
| LogFileReader | 读取日志文件、行过滤、文件遍历 | 无 |
| QfodParser | 解析 Qfod 格式日志,返回 Record | LogFileReader |
| PlossParser | 解析 Ploss 格式日志,返回 Record | LogFileReader |
| Models | 定义数据实体结构 | 无 |
| WctMinerDbContext | EF Core 数据库上下文 | Models |
| Configuration | 加载配置文件 | 无 |
| Serilog | 结构化日志输出 | 无 |
| ErrorHandlingService | 解析错误记录、错误统计 | 无 |
---
## 3. 分层架构
### 3.1 分层定义
| 层级 | 职责 | 允许调用 |
|------|------|----------|
| CLI Entry | 接收参数、流程调度、结果输出 | Service, Infrastructure |
| Service | 协调解析流程、批量写入、统计管理 | Parser, Data, Infrastructure |
| Parser | 日志解析、数据提取、返回 Record | 无 |
| Data | 数据持久化、DbContext 管理 | Infrastructure |
| Infrastructure | 配置、日志、错误处理 | 无 |
### 3.2 依赖规则
- 上层可调用下层
- 下层不可调用上层
- Parser 之间不可互相调用(独立解析器)
- Service 层协调 Parser 和 Data 层
---
## 4. 模块依赖图
```mermaid
graph TD
CLI[CLI Entry] --> PS[ParseService]
CLI --> Serilog
CLI --> EHS[ErrorHandlingService]
PS --> QP[QfodParser]
PS --> PP[PlossParser]
PS --> DBM[WctMinerDbContext]
PS --> EHS
PS --> Serilog
PS --> Config[Configuration]
QP --> LFR[LogFileReader]
PP --> LFR
DBM --> Models[Models]
subgraph Parser Layer
QP
PP
LFR
end
subgraph Data Layer
DBM
Models
end
subgraph Infrastructure
Config
Serilog
EHS
end
```
---
## 5. 目录结构
```
src/WCTDataMiner/
├── Parsers/
│ ├── IParser.cs
│ ├── QfodParser.cs
│ ├── PlossParser.cs
│ └── LogFileReader.cs
├── Models/
│ ├── LogFile.cs
│ ├── QfodRecord.cs
│ ├── PlossRecord.cs
│ ├── ParseError.cs
│ └── ParseStatus.cs
├── Data/
│ ├── WctMinerDbContext.cs
│ └── IDbContextFactory.cs
├── Services/
│ ├── ParseService.cs
│ ├── ThresholdCalculator.cs
│ └── ErrorHandlingService.cs
├── Configuration/
│ ├── AppSettings.cs
│ └── appsettings.json
├── Commands/
│ ├── ParseCommand.cs
│ ├── StatsCommand.cs
│ ├── ExportCommand.cs
│ └── CleanCommand.cs
└── Program.cs
```

View File

@@ -0,0 +1,105 @@
# 配置管理
> 所属模块:[架构概览](./概览.md)
---
## 1. 配置文件结构
```json
// Configuration/appsettings.json
{
"Database": {
"Type": "sqlite",
"Path": "./data/database/wctminer.db"
},
"Parser": {
"BatchSize": 1000
},
"Logging": {
"LogLevel": {
"Default": "Information",
"WCTDataMiner": "Debug"
}
},
"Paths": {
"LogDir": "./data/logs",
"OutputDir": "./data/output"
}
}
```
---
## 2. 配置类定义
```csharp
// Configuration/AppSettings.cs
namespace WCTDataMiner.Configuration;
public class AppSettings
{
public DatabaseSettings Database { get; set; } = new();
public ParserSettings Parser { get; set; } = new();
public PathSettings Paths { get; set; } = new();
}
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; }
}
public class ParserSettings
{
public int BatchSize { get; set; } = 1000;
}
public class PathSettings
{
public string LogDir { get; set; } = "./data/logs";
public string OutputDir { get; set; } = "./data/output";
}
```
---
## 3. 配置加载
```csharp
// Program.cs 配置加载部分
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using WCTDataMiner.Configuration;
var builder = Host.CreateDefaultBuilder(args);
builder.ConfigureAppConfiguration(config =>
{
config.AddJsonFile("appsettings.json", optional: false);
config.AddJsonFile($"appsettings.{environment}.json", optional: true);
config.AddEnvironmentVariables();
config.AddCommandLine(args);
});
builder.ConfigureServices((context, services) =>
{
services.Configure<AppSettings>(context.Configuration.GetSection(""));
// 注册数据库服务
services.AddDatabaseServices(context.Configuration);
// 注册其他服务
services.AddScoped<QfodParser>();
services.AddScoped<PlossParser>();
services.AddScoped<ParseService>();
services.AddScoped<ErrorHandlingService>();
});
```