feat: 重构项目结构,添加 WPF 应用

- 将核心功能提取到 WCTDataMiner.Core 类库
- 添加 WCTDataMiner.Wpf 桌面应用
- 支持数据解析、导出、统计等功能

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ssss
2026-07-03 10:47:27 +08:00
parent 3dcd9f7c0b
commit d1a89cb86a
55 changed files with 659 additions and 87 deletions

View File

@@ -0,0 +1,103 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Core.Models;
namespace WCTDataMiner.Core.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);
}
}

View File

@@ -0,0 +1,73 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Core.Models;
namespace WCTDataMiner.Core.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);
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Core.Models;
namespace WCTDataMiner.Core.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);
}
}

View File

@@ -0,0 +1,112 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Core.Models;
namespace WCTDataMiner.Core.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);
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Core.Models;
namespace WCTDataMiner.Core.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);
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Core.Models;
namespace WCTDataMiner.Core.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);
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using WCTDataMiner.Core.Models;
namespace WCTDataMiner.Core.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);
}
}

View File

@@ -0,0 +1,66 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
namespace WCTDataMiner.Core.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);
}
}
}

View File

@@ -0,0 +1,9 @@
namespace WCTDataMiner.Core.Data;
/// <summary>
/// 数据库上下文工厂接口
/// </summary>
public interface IDbContextFactory
{
WctMinerDbContext CreateDbContext();
}

View File

@@ -0,0 +1,36 @@
using WCTDataMiner.Core.Models;
namespace WCTDataMiner.Core.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();
}
}
}

View File

@@ -0,0 +1,32 @@
using Microsoft.EntityFrameworkCore;
using WCTDataMiner.Core.Models;
namespace WCTDataMiner.Core.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);
}
}