refactor(core): 重构核心分层结构并添加发布库聚合
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.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/local.db";
|
||||
public string LocalPath { get; set; } = "./data/database/local.db";
|
||||
public string ReleasePath { get; set; } = "./data/database/release.db";
|
||||
|
||||
// PostgreSQL 配置(可选)
|
||||
public string? Host { get; set; }
|
||||
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";
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.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();
|
||||
});
|
||||
|
||||
services.AddScoped<IReleaseDbContextFactory, ReleaseDbContextFactory>();
|
||||
|
||||
// 注册正式发布数据库上下文(与本地解析数据库完全独立)
|
||||
services.AddScoped<ReleaseDbContext>(sp =>
|
||||
{
|
||||
var factory = sp.GetRequiredService<IReleaseDbContextFactory>();
|
||||
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>();
|
||||
services.AddScoped<AggregationService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||
|
||||
public class 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");
|
||||
|
||||
// ===== 第一行字段(nullable) =====
|
||||
builder.Property(e => e.PowLoss)
|
||||
.IsRequired(false)
|
||||
.HasColumnName("pow_loss");
|
||||
|
||||
builder.Property(e => e.DeltaP)
|
||||
.IsRequired(false)
|
||||
.HasColumnName("delta_p");
|
||||
|
||||
// ===== 第二行字段(通用命名) =====
|
||||
builder.Property(e => e.Field1)
|
||||
.IsRequired()
|
||||
.HasColumnName("field_1");
|
||||
|
||||
builder.Property(e => e.Field2)
|
||||
.IsRequired()
|
||||
.HasColumnName("field_2");
|
||||
|
||||
builder.Property(e => e.Field3)
|
||||
.IsRequired()
|
||||
.HasColumnName("field_3");
|
||||
|
||||
builder.Property(e => e.Field4)
|
||||
.IsRequired()
|
||||
.HasColumnName("field_4");
|
||||
|
||||
builder.Property(e => e.Field5)
|
||||
.IsRequired()
|
||||
.HasColumnName("field_5");
|
||||
|
||||
builder.Property(e => e.Field6)
|
||||
.IsRequired()
|
||||
.HasColumnName("field_6");
|
||||
|
||||
builder.Property(e => e.Field7)
|
||||
.IsRequired()
|
||||
.HasColumnName("field_7");
|
||||
|
||||
builder.Property(e => e.Field8)
|
||||
.IsRequired()
|
||||
.HasColumnName("field_8");
|
||||
|
||||
builder.Property(e => e.Field9)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("field_9");
|
||||
|
||||
builder.Property(e => e.Field10)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("field_10");
|
||||
|
||||
builder.Property(e => e.Field11)
|
||||
.IsRequired()
|
||||
.HasColumnName("field_11");
|
||||
|
||||
builder.Property(e => e.Field12)
|
||||
.IsRequired()
|
||||
.HasColumnName("field_12");
|
||||
|
||||
builder.Property(e => e.Field13)
|
||||
.IsRequired()
|
||||
.HasColumnName("field_13");
|
||||
|
||||
builder.Property(e => e.Field14)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("field_14");
|
||||
|
||||
builder.Property(e => e.Field15)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("field_15");
|
||||
|
||||
builder.Property(e => e.Field16)
|
||||
.IsRequired()
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("field_16");
|
||||
|
||||
builder.Property(e => e.IsDeleted)
|
||||
.IsRequired()
|
||||
.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.Field2).HasDatabaseName("idx_ploss_field_2"); // 原 rx_power
|
||||
builder.HasIndex(e => e.Field7).HasDatabaseName("idx_ploss_field_7"); // 原 ploss
|
||||
builder.HasIndex(e => e.Field8).HasDatabaseName("idx_ploss_field_8"); // 原 threshold
|
||||
builder.HasIndex(e => e.Field10).HasDatabaseName("idx_ploss_field_10"); // 原 fod_result
|
||||
builder.HasIndex(e => e.PowLoss).HasDatabaseName("idx_ploss_pow_loss"); // 新增
|
||||
builder.HasIndex(e => e.DeltaP).HasDatabaseName("idx_ploss_delta_p"); // 新增
|
||||
|
||||
// 关系配置
|
||||
builder.HasOne(e => e.Scenario)
|
||||
.WithMany(e => e.PlossRecords)
|
||||
.HasForeignKey(e => e.ScenarioId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// 全局查询过滤器
|
||||
builder.HasQueryFilter(e => !e.IsDeleted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||
|
||||
public class 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.FieldF)
|
||||
.IsRequired()
|
||||
.HasDefaultValue((byte)0)
|
||||
.HasColumnName("field_f");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||
|
||||
public class 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 Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.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 Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||
|
||||
public class 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 Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
/// <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:LocalPath"]
|
||||
?? _configuration["Database:Path"]
|
||||
?? "./data/database/local.db";
|
||||
EnsureDirectoryExists(dbPath);
|
||||
optionsBuilder.UseSqlite($"Data Source={dbPath}");
|
||||
break;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
optionsBuilder.EnableSensitiveDataLogging();
|
||||
optionsBuilder.EnableDetailedErrors();
|
||||
#endif
|
||||
|
||||
var context = new WctMinerDbContext(optionsBuilder.Options);
|
||||
|
||||
// 确保数据库和表结构已创建(适用于 SQLite)
|
||||
context.Database.EnsureCreated();
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库上下文工厂接口
|
||||
/// </summary>
|
||||
public interface IDbContextFactory
|
||||
{
|
||||
WctMinerDbContext CreateDbContext();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Release;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||
|
||||
public class ChargingParameterRecordConfiguration : IEntityTypeConfiguration<ChargingParameterRecord>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ChargingParameterRecord> builder)
|
||||
{
|
||||
builder.ToTable("charging_parameter");
|
||||
builder.HasKey(e => e.Id);
|
||||
builder.Property(e => e.Id).HasColumnName("id");
|
||||
|
||||
ConfigureText(builder, e => e.CarFactory, "car_factory");
|
||||
ConfigureText(builder, e => e.CarModel, "car_model");
|
||||
ConfigureText(builder, e => e.PhoneBrand, "phone_brand");
|
||||
ConfigureText(builder, e => e.PhoneModel, "phone_model");
|
||||
|
||||
ConfigureNumber(builder, e => e.Power350mW, "power_350mw");
|
||||
ConfigureNumber(builder, e => e.Power500mW, "power_500mw");
|
||||
ConfigureNumber(builder, e => e.Power750mW, "power_750mw");
|
||||
ConfigureNumber(builder, e => e.Power1000mW, "power_1000mw");
|
||||
ConfigureNumber(builder, e => e.Power1250mW, "power_1250mw");
|
||||
ConfigureNumber(builder, e => e.Power1500mW, "power_1500mw");
|
||||
ConfigureNumber(builder, e => e.Power1750mW, "power_1750mw");
|
||||
ConfigureNumber(builder, e => e.Power2000mW, "power_2000mw");
|
||||
ConfigureNumber(builder, e => e.Power2250mW, "power_2250mw");
|
||||
ConfigureNumber(builder, e => e.QValue, "q_value");
|
||||
ConfigureNumber(builder, e => e.QBaseValue, "q_base_value");
|
||||
ConfigureNumber(builder, e => e.PqCoefficient, "pq_coefficient");
|
||||
ConfigureNumber(builder, e => e.ResonanceFrequency, "resonance_frequency");
|
||||
|
||||
builder.Property(e => e.CreatedAt).IsRequired().HasColumnName("created_at");
|
||||
builder.Property(e => e.UpdatedAt).IsRequired().HasColumnName("updated_at");
|
||||
|
||||
builder.HasIndex(e => new { e.CarFactory, e.CarModel, e.PhoneBrand, e.PhoneModel })
|
||||
.IsUnique()
|
||||
.HasDatabaseName("uq_charging_parameter_business_key");
|
||||
}
|
||||
|
||||
private static void ConfigureText(
|
||||
EntityTypeBuilder<ChargingParameterRecord> builder,
|
||||
System.Linq.Expressions.Expression<Func<ChargingParameterRecord, string>> property,
|
||||
string columnName)
|
||||
{
|
||||
builder.Property(property).IsRequired().HasMaxLength(128).HasColumnName(columnName);
|
||||
}
|
||||
|
||||
private static void ConfigureNumber(
|
||||
EntityTypeBuilder<ChargingParameterRecord> builder,
|
||||
System.Linq.Expressions.Expression<Func<ChargingParameterRecord, double?>> property,
|
||||
string columnName)
|
||||
{
|
||||
builder.Property(property).IsRequired(false).HasColumnName(columnName).HasPrecision(18, 6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
public interface IReleaseDbContextFactory
|
||||
{
|
||||
ReleaseDbContext CreateDbContext();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Release;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
/// <summary>
|
||||
/// 正式发布数据库上下文。只包含最终充电参数记录。
|
||||
/// </summary>
|
||||
public class ReleaseDbContext : DbContext
|
||||
{
|
||||
public ReleaseDbContext(DbContextOptions<ReleaseDbContext> options)
|
||||
: base(options) { }
|
||||
|
||||
public DbSet<ChargingParameterRecord> ChargingParameters => Set<ChargingParameterRecord>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ReleaseDbContext).Assembly);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||
|
||||
/// <summary>
|
||||
/// 正式发布数据库上下文工厂。
|
||||
/// </summary>
|
||||
public class ReleaseDbContextFactory : IReleaseDbContextFactory
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public ReleaseDbContextFactory(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public ReleaseDbContext CreateDbContext()
|
||||
{
|
||||
var dbType = _configuration["Database:Type"]?.ToLowerInvariant() ?? "sqlite";
|
||||
var optionsBuilder = new DbContextOptionsBuilder<ReleaseDbContext>();
|
||||
|
||||
if (dbType is "postgresql" or "postgres")
|
||||
{
|
||||
optionsBuilder.UseNpgsql(BuildPostgreSqlConnectionString());
|
||||
}
|
||||
else
|
||||
{
|
||||
var dbPath = _configuration["Database:ReleasePath"] ?? "./data/database/release.db";
|
||||
EnsureDirectoryExists(dbPath);
|
||||
optionsBuilder.UseSqlite($"Data Source={dbPath}");
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
optionsBuilder.EnableSensitiveDataLogging();
|
||||
optionsBuilder.EnableDetailedErrors();
|
||||
#endif
|
||||
|
||||
return new ReleaseDbContext(optionsBuilder.Options);
|
||||
}
|
||||
|
||||
private string BuildPostgreSqlConnectionString()
|
||||
{
|
||||
var host = _configuration["Database:ReleaseHost"] ?? _configuration["Database:Host"] ?? "localhost";
|
||||
var port = _configuration.GetValue("Database:ReleasePort", _configuration.GetValue("Database:Port", 5432));
|
||||
var name = _configuration["Database:ReleaseName"] ?? "wct_charging_parameters";
|
||||
var user = _configuration["Database:ReleaseUser"] ?? _configuration["Database:User"] ?? "postgres";
|
||||
var password = _configuration["Database:ReleasePassword"] ?? _configuration["Database:Password"] ?? "";
|
||||
return $"Host={host};Port={port};Database={name};Username={user};Password={password}";
|
||||
}
|
||||
|
||||
private static void EnsureDirectoryExists(string dbPath)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(dbPath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
|
||||
|
||||
/// <summary>
|
||||
/// 路径验证结果
|
||||
/// </summary>
|
||||
public class PathValidationResult
|
||||
{
|
||||
public bool IsValid { get; }
|
||||
public string? NormalizedPath { get; }
|
||||
public string? ErrorMessage { get; }
|
||||
|
||||
private PathValidationResult(bool isValid, string? normalizedPath, string? errorMessage)
|
||||
{
|
||||
IsValid = isValid;
|
||||
NormalizedPath = normalizedPath;
|
||||
ErrorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public static PathValidationResult Success(string normalizedPath)
|
||||
=> new(true, normalizedPath, null);
|
||||
|
||||
public static PathValidationResult Fail(string errorMessage)
|
||||
=> new(false, null, errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 路径验证器 - 防止路径遍历攻击
|
||||
/// </summary>
|
||||
public class PathValidator
|
||||
{
|
||||
private readonly string _applicationBasePath;
|
||||
private readonly HashSet<string> _allowedDirectories;
|
||||
|
||||
/// <summary>
|
||||
/// 默认允许的输出目录
|
||||
/// </summary>
|
||||
private static readonly string[] DefaultAllowedDirectories = ["exports", "output", "data/exports", "data/output"];
|
||||
|
||||
public PathValidator(string applicationBasePath, IEnumerable<string>? allowedDirectories = null)
|
||||
{
|
||||
_applicationBasePath = Path.GetFullPath(applicationBasePath);
|
||||
_allowedDirectories = new HashSet<string>(
|
||||
(allowedDirectories ?? DefaultAllowedDirectories).Select(d => d.Replace('/', Path.DirectorySeparatorChar)),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证输出目录路径
|
||||
/// </summary>
|
||||
/// <param name="outputDir">用户输入的输出目录路径</param>
|
||||
/// <returns>验证结果</returns>
|
||||
public PathValidationResult ValidateOutputDirectory(string outputDir)
|
||||
{
|
||||
// 1. 基础验证
|
||||
if (string.IsNullOrWhiteSpace(outputDir))
|
||||
return PathValidationResult.Fail("输出目录不能为空");
|
||||
|
||||
// 2. 禁止的模式检查
|
||||
var forbiddenPatterns = new[]
|
||||
{
|
||||
"..", // 路径遍历
|
||||
"~", // 用户主目录
|
||||
"\\\\", // UNC 路径
|
||||
};
|
||||
|
||||
foreach (var pattern in forbiddenPatterns)
|
||||
{
|
||||
if (outputDir.Contains(pattern))
|
||||
{
|
||||
return PathValidationResult.Fail($"路径包含禁止的模式: {pattern}");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查是否为绝对路径(Windows 和 Unix)
|
||||
if (Path.IsPathRooted(outputDir) ||
|
||||
Regex.IsMatch(outputDir, @"^[A-Za-z]:") || // Windows 驱动器
|
||||
outputDir.StartsWith("/")) // Unix 绝对路径
|
||||
{
|
||||
return PathValidationResult.Fail("仅允许相对路径");
|
||||
}
|
||||
|
||||
// 4. 规范化并验证
|
||||
string normalizedPath;
|
||||
try
|
||||
{
|
||||
normalizedPath = Path.GetFullPath(Path.Combine(_applicationBasePath, outputDir));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return PathValidationResult.Fail($"路径格式无效: {ex.Message}");
|
||||
}
|
||||
|
||||
// 5. 严格验证必须在应用目录内
|
||||
if (!normalizedPath.StartsWith(_applicationBasePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return PathValidationResult.Fail("路径必须在应用程序目录内");
|
||||
}
|
||||
|
||||
// 6. 白名单检查
|
||||
var relative = normalizedPath
|
||||
.Substring(_applicationBasePath.Length)
|
||||
.TrimStart(Path.DirectorySeparatorChar);
|
||||
|
||||
bool inAllowedDir = _allowedDirectories
|
||||
.Any(allowed => relative.StartsWith(allowed + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(relative, allowed, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!inAllowedDir)
|
||||
{
|
||||
var allowedList = string.Join(", ", _allowedDirectories);
|
||||
return PathValidationResult.Fail($"输出目录必须在允许列表中: {allowedList}");
|
||||
}
|
||||
|
||||
return PathValidationResult.Success(normalizedPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 简化版验证 - 仅检查路径安全性,不做白名单检查
|
||||
/// </summary>
|
||||
public PathValidationResult ValidatePathSafety(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return PathValidationResult.Fail("路径不能为空");
|
||||
|
||||
// 检查路径遍历
|
||||
if (path.Contains(".."))
|
||||
return PathValidationResult.Fail("路径包含非法的遍历字符");
|
||||
|
||||
// 检查绝对路径
|
||||
if (Path.IsPathRooted(path) ||
|
||||
Regex.IsMatch(path, @"^[A-Za-z]:") ||
|
||||
path.StartsWith("/"))
|
||||
{
|
||||
return PathValidationResult.Fail("仅允许相对路径");
|
||||
}
|
||||
|
||||
// 检查 UNC 路径
|
||||
if (path.StartsWith("\\\\"))
|
||||
return PathValidationResult.Fail("不允许 UNC 路径");
|
||||
|
||||
// 规范化验证
|
||||
try
|
||||
{
|
||||
var normalized = Path.GetFullPath(Path.Combine(_applicationBasePath, path));
|
||||
if (!normalized.StartsWith(_applicationBasePath, StringComparison.OrdinalIgnoreCase))
|
||||
return PathValidationResult.Fail("路径超出应用程序目录范围");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return PathValidationResult.Fail($"路径格式无效: {ex.Message}");
|
||||
}
|
||||
|
||||
return PathValidationResult.Success(path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
|
||||
|
||||
/// <summary>
|
||||
/// 安全相关常量定义
|
||||
/// </summary>
|
||||
public static class SecurityConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认允许的输出目录
|
||||
/// </summary>
|
||||
public static readonly string[] DefaultAllowedExportDirectories =
|
||||
[
|
||||
"exports",
|
||||
"output",
|
||||
"data/exports",
|
||||
"data/output"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 路径最大长度限制
|
||||
/// </summary>
|
||||
public const int MaxPathLength = 260;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名最大长度限制
|
||||
/// </summary>
|
||||
public const int MaxFileNameLength = 255;
|
||||
|
||||
/// <summary>
|
||||
/// 禁止的路径模式
|
||||
/// </summary>
|
||||
public static readonly string[] ForbiddenPathPatterns =
|
||||
[
|
||||
"..", // 路径遍历
|
||||
"~", // 用户主目录
|
||||
"\\\\", // UNC 路径开始
|
||||
"|", // 管道字符(Windows 禁止)
|
||||
">", // 重定向字符
|
||||
"<", // 重定向字符
|
||||
"*", // 通配符(用于文件名时)
|
||||
"?" // 通配符(用于文件名时)
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 安全的文件扩展名白名单
|
||||
/// </summary>
|
||||
public static readonly string[] AllowedExportExtensions =
|
||||
[
|
||||
".csv",
|
||||
".json",
|
||||
".txt",
|
||||
".log"
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user