refactor(core): 重构核心分层结构并添加发布库聚合
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Services;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 数据清理服务 - 支持软删除
|
/// 数据清理服务 - 支持软删除
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Services;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 统计查询服务 - 使用数据库端聚合优化内存效率
|
/// 统计查询服务 - 使用数据库端聚合优化内存效率
|
||||||
@@ -3,11 +3,11 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Security;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Services;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 数据导出服务 - 支持安全的路径验证和 RFC 4180 标准 CSV 转义
|
/// 数据导出服务 - 支持安全的路径验证和 RFC 4180 标准 CSV 转义
|
||||||
@@ -15,12 +15,14 @@ namespace Gpulse.WCT.DataAnalyzer.Core.Services;
|
|||||||
public class ExportService
|
public class ExportService
|
||||||
{
|
{
|
||||||
private readonly WctMinerDbContext _context;
|
private readonly WctMinerDbContext _context;
|
||||||
|
private readonly ReleaseDbContext _releaseContext;
|
||||||
private readonly ILogger<ExportService> _logger;
|
private readonly ILogger<ExportService> _logger;
|
||||||
private readonly PathValidator _pathValidator;
|
private readonly PathValidator _pathValidator;
|
||||||
|
|
||||||
public ExportService(WctMinerDbContext context, ILogger<ExportService> logger)
|
public ExportService(WctMinerDbContext context, ReleaseDbContext releaseContext, ILogger<ExportService> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
_releaseContext = releaseContext;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_pathValidator = new PathValidator(AppDomain.CurrentDomain.BaseDirectory);
|
_pathValidator = new PathValidator(AppDomain.CurrentDomain.BaseDirectory);
|
||||||
}
|
}
|
||||||
@@ -305,7 +307,49 @@ public class ExportService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// RFC 4180 标准 CSV 字段转义
|
/// 导出正式发布库中的 ChargingParameterDatabase CSV。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<string> ExportChargingParametersToCsvAsync(string outputDir)
|
||||||
|
{
|
||||||
|
var validationResult = _pathValidator.ValidateOutputDirectory(outputDir);
|
||||||
|
if (!validationResult.IsValid)
|
||||||
|
throw new ArgumentException($"无效的输出目录: {validationResult.ErrorMessage}");
|
||||||
|
|
||||||
|
var safeOutputDir = validationResult.NormalizedPath!;
|
||||||
|
Directory.CreateDirectory(safeOutputDir);
|
||||||
|
var filePath = Path.Combine(safeOutputDir, "WCT-ChargingParameterDatabase.csv");
|
||||||
|
|
||||||
|
await using var writer = new StreamWriter(filePath, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
|
||||||
|
await writer.WriteLineAsync("车厂,车型,手机厂商,型号,350mW,500mW,750mW,1000mW,1250mW,1500mW,1750mW,2000mW,2250mW,Q值,Q基值,P-Q值系数,谐振频率");
|
||||||
|
|
||||||
|
var query = _releaseContext.ChargingParameters
|
||||||
|
.AsNoTracking()
|
||||||
|
.OrderBy(r => r.CarFactory)
|
||||||
|
.ThenBy(r => r.CarModel)
|
||||||
|
.ThenBy(r => r.PhoneBrand)
|
||||||
|
.ThenBy(r => r.PhoneModel);
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
await foreach (var record in query.AsAsyncEnumerable())
|
||||||
|
{
|
||||||
|
var line = string.Join(',',
|
||||||
|
EscapeCsvField(record.CarFactory), EscapeCsvField(record.CarModel),
|
||||||
|
EscapeCsvField(record.PhoneBrand), EscapeCsvField(record.PhoneModel),
|
||||||
|
EscapeCsvField(record.Power350mW), EscapeCsvField(record.Power500mW),
|
||||||
|
EscapeCsvField(record.Power750mW), EscapeCsvField(record.Power1000mW),
|
||||||
|
EscapeCsvField(record.Power1250mW), EscapeCsvField(record.Power1500mW),
|
||||||
|
EscapeCsvField(record.Power1750mW), EscapeCsvField(record.Power2000mW),
|
||||||
|
EscapeCsvField(record.Power2250mW), EscapeCsvField(record.QValue),
|
||||||
|
EscapeCsvField(record.QBaseValue), EscapeCsvField(record.PqCoefficient),
|
||||||
|
EscapeCsvField(record.ResonanceFrequency));
|
||||||
|
await writer.WriteLineAsync(line);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("ChargingParameterDatabase CSV 导出完成: {FilePath}, 共 {Count} 条记录", filePath, count);
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string EscapeCsvField(object? value)
|
private static string EscapeCsvField(object? value)
|
||||||
{
|
{
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Services;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 维度表管理服务 - 负责维度表的CRUD操作(并发安全版本)
|
/// 维度表管理服务 - 负责维度表的CRUD操作(并发安全版本)
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Parsers;
|
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Services;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析流程协调服务 - 协调文件名解析、行解析、批量写入
|
/// 解析流程协调服务 - 协调文件名解析、行解析、批量写入
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Parsers;
|
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Services;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 篮选条件常量
|
/// 篮选条件常量
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Parsers;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 文件名解析器 - 从文件名提取场景信息
|
/// 文件名解析器 - 从文件名提取场景信息
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Parsers;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 通用解析器接口
|
/// 通用解析器接口
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Parsers;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ploss FOD日志解析器 - 支持两行格式
|
/// Ploss FOD日志解析器 - 支持两行格式
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Parsers;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Qfod日志解析器
|
/// Qfod日志解析器
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Release;
|
||||||
|
|
||||||
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从本地解析库生成独立正式发布库。
|
||||||
|
/// 当前业务规则:功率列使用 Ploss 的 Field7,重复结果取平均值;缺失值保留为空。
|
||||||
|
/// </summary>
|
||||||
|
public class AggregationService
|
||||||
|
{
|
||||||
|
private static readonly int[] PowerLevels = [350, 500, 750, 1000, 1250, 1500, 1750, 2000, 2250];
|
||||||
|
private readonly WctMinerDbContext _localContext;
|
||||||
|
private readonly ReleaseDbContext _releaseContext;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly ILogger<AggregationService> _logger;
|
||||||
|
|
||||||
|
public AggregationService(
|
||||||
|
WctMinerDbContext localContext,
|
||||||
|
ReleaseDbContext releaseContext,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ILogger<AggregationService> logger)
|
||||||
|
{
|
||||||
|
_localContext = localContext;
|
||||||
|
_releaseContext = releaseContext;
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AggregationReport> AggregateAsync(bool rebuild = true, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var rows = await LoadRowsAsync(cancellationToken);
|
||||||
|
var records = rows
|
||||||
|
.GroupBy(r => BuildKey(r.Scenario))
|
||||||
|
.Select(group => CreateRecord(group.Key, group.ToList()))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await using var transaction = await _releaseContext.Database.BeginTransactionAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (rebuild)
|
||||||
|
await _releaseContext.Database.ExecuteSqlRawAsync("DELETE FROM charging_parameter", cancellationToken);
|
||||||
|
|
||||||
|
await _releaseContext.ChargingParameters.AddRangeAsync(records, cancellationToken);
|
||||||
|
await _releaseContext.SaveChangesAsync(cancellationToken);
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("聚合完成: {Count} 条正式发布记录", records.Count);
|
||||||
|
return new AggregationReport(records.Count, rows.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<PlossRow>> LoadRowsAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var records = await _localContext.PlossRecords
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(r => !r.IsDeleted)
|
||||||
|
.Include(r => r.Scenario)
|
||||||
|
.ThenInclude(s => s!.TxPanel)
|
||||||
|
.Include(r => r.Scenario)
|
||||||
|
.ThenInclude(s => s!.TxHardware)
|
||||||
|
.Include(r => r.Scenario)
|
||||||
|
.ThenInclude(s => s!.RxType)
|
||||||
|
.Include(r => r.Scenario)
|
||||||
|
.ThenInclude(s => s!.QfodRecords)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return records.Select(r => new PlossRow(
|
||||||
|
r,
|
||||||
|
r.Scenario,
|
||||||
|
r.Scenario.TxPanel.Name,
|
||||||
|
r.Scenario.TxHardware.Version,
|
||||||
|
r.Scenario.RxType.Name)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChargingParameterKey BuildKey(TestScenario scenario)
|
||||||
|
{
|
||||||
|
var panelName = scenario.TxPanel.Name;
|
||||||
|
var mapping = _configuration.GetSection("Aggregation:TxPanelMappings")[panelName];
|
||||||
|
var carFactory = mapping ?? panelName;
|
||||||
|
var carModel = _configuration[$"Aggregation:CarModelMappings:{scenario.TxHardware.Version}"]
|
||||||
|
?? scenario.TxHardware.Version;
|
||||||
|
var (phoneBrand, phoneModel) = SplitPhoneName(scenario.RxType.Name);
|
||||||
|
return new ChargingParameterKey(carFactory, carModel, phoneBrand, phoneModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string Brand, string Model) SplitPhoneName(string value)
|
||||||
|
{
|
||||||
|
var separator = value.IndexOfAny(['/', '_', ':']);
|
||||||
|
if (separator > 0 && separator < value.Length - 1)
|
||||||
|
return (value[..separator], value[(separator + 1)..]);
|
||||||
|
|
||||||
|
return ("Unknown", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChargingParameterRecord CreateRecord(ChargingParameterKey key, List<PlossRow> rows)
|
||||||
|
{
|
||||||
|
var values = PowerLevels.Select(power => Average(rows
|
||||||
|
.Where(row => IsPowerMatch(row.Record, power))
|
||||||
|
.Select(row => (double?)row.Record.Field7))).ToArray();
|
||||||
|
|
||||||
|
return new ChargingParameterRecord
|
||||||
|
{
|
||||||
|
CarFactory = key.CarFactory,
|
||||||
|
CarModel = key.CarModel,
|
||||||
|
PhoneBrand = key.PhoneBrand,
|
||||||
|
PhoneModel = key.PhoneModel,
|
||||||
|
Power350mW = values[0],
|
||||||
|
Power500mW = values[1],
|
||||||
|
Power750mW = values[2],
|
||||||
|
Power1000mW = values[3],
|
||||||
|
Power1250mW = values[4],
|
||||||
|
Power1500mW = values[5],
|
||||||
|
Power1750mW = values[6],
|
||||||
|
Power2000mW = values[7],
|
||||||
|
Power2250mW = values[8],
|
||||||
|
QValue = Average(rows.SelectMany(row => row.Scenario.QfodRecords).Select(q => (double?)q.CurrentQ)),
|
||||||
|
QBaseValue = Average(rows.SelectMany(row => row.Scenario.QfodRecords).Select(q => (double?)q.RawQ)),
|
||||||
|
PqCoefficient = Average(rows.Select(row => row.Record.DeltaP.HasValue ? (double?)row.Record.DeltaP.Value : null)),
|
||||||
|
ResonanceFrequency = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPowerMatch(PlossRecord record, int power)
|
||||||
|
{
|
||||||
|
return record.Field4 == power || record.Field5 == power || record.PowLoss == power;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double? Average(IEnumerable<double?> values)
|
||||||
|
{
|
||||||
|
var valid = values.Where(value => value.HasValue).Select(value => value!.Value).ToArray();
|
||||||
|
return valid.Length == 0 ? null : valid.Average();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record PlossRow(PlossRecord Record, TestScenario Scenario, string Panel, string Hardware, string RxType);
|
||||||
|
private record ChargingParameterKey(string CarFactory, string CarModel, string PhoneBrand, string PhoneModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record AggregationReport(int ReleaseRecordCount, int LocalRecordCount);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Services;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 阈值计算器 - 根据接收功率查表获取动态阈值
|
/// 阈值计算器 - 根据接收功率查表获取动态阈值
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ploss记录实体 - 存储Ploss FOD格式解析数据
|
/// Ploss记录实体 - 存储Ploss FOD格式解析数据
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Qfod记录实体 - 存储Qfod格式解析数据
|
/// Qfod记录实体 - 存储Qfod格式解析数据
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// RX类型维度表
|
/// RX类型维度表
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 测试场景实体 - 通过外键关联各维度表
|
/// 测试场景实体 - 通过外键关联各维度表
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// TX硬件版本维度表
|
/// TX硬件版本维度表
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// TX面板类型维度表
|
/// TX面板类型维度表
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Models;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// TX软件版本维度表
|
/// TX软件版本维度表
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Release;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 正式发布库中的充电参数记录。
|
||||||
|
/// 此实体只属于 ReleaseDbContext,不引用本地日志数据库实体。
|
||||||
|
/// </summary>
|
||||||
|
public class ChargingParameterRecord
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public string CarFactory { get; set; } = null!;
|
||||||
|
public string CarModel { get; set; } = null!;
|
||||||
|
public string PhoneBrand { get; set; } = null!;
|
||||||
|
public string PhoneModel { get; set; } = null!;
|
||||||
|
|
||||||
|
public double? Power350mW { get; set; }
|
||||||
|
public double? Power500mW { get; set; }
|
||||||
|
public double? Power750mW { get; set; }
|
||||||
|
public double? Power1000mW { get; set; }
|
||||||
|
public double? Power1250mW { get; set; }
|
||||||
|
public double? Power1500mW { get; set; }
|
||||||
|
public double? Power1750mW { get; set; }
|
||||||
|
public double? Power2000mW { get; set; }
|
||||||
|
public double? Power2250mW { get; set; }
|
||||||
|
|
||||||
|
public double? QValue { get; set; }
|
||||||
|
public double? QBaseValue { get; set; }
|
||||||
|
public double? PqCoefficient { get; set; }
|
||||||
|
public double? ResonanceFrequency { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Configuration;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Configuration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 应用配置
|
/// 应用配置
|
||||||
@@ -16,7 +16,9 @@ public class AppSettings
|
|||||||
public class DatabaseSettings
|
public class DatabaseSettings
|
||||||
{
|
{
|
||||||
public string Type { get; set; } = "sqlite";
|
public string Type { get; set; } = "sqlite";
|
||||||
public string Path { get; set; } = "./data/database/wctminer.db";
|
public string Path { get; set; } = "./data/database/local.db";
|
||||||
|
public string LocalPath { get; set; } = "./data/database/local.db";
|
||||||
|
public string ReleasePath { get; set; } = "./data/database/release.db";
|
||||||
|
|
||||||
// PostgreSQL 配置(可选)
|
// PostgreSQL 配置(可选)
|
||||||
public string? Host { get; set; }
|
public string? Host { get; set; }
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Parsers;
|
using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Services;
|
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Extensions;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 服务注册扩展
|
/// 服务注册扩展
|
||||||
@@ -28,6 +28,15 @@ public static class ServiceCollectionExtensions
|
|||||||
return factory.CreateDbContext();
|
return factory.CreateDbContext();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddScoped<IReleaseDbContextFactory, ReleaseDbContextFactory>();
|
||||||
|
|
||||||
|
// 注册正式发布数据库上下文(与本地解析数据库完全独立)
|
||||||
|
services.AddScoped<ReleaseDbContext>(sp =>
|
||||||
|
{
|
||||||
|
var factory = sp.GetRequiredService<IReleaseDbContextFactory>();
|
||||||
|
return factory.CreateDbContext();
|
||||||
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +57,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<StatsService>();
|
services.AddScoped<StatsService>();
|
||||||
services.AddScoped<ExportService>();
|
services.AddScoped<ExportService>();
|
||||||
services.AddScoped<CleanService>();
|
services.AddScoped<CleanService>();
|
||||||
|
services.AddScoped<AggregationService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Data.Configurations;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||||
|
|
||||||
public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Data.Configurations;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||||
|
|
||||||
public class QfodRecordConfiguration : IEntityTypeConfiguration<QfodRecord>
|
public class QfodRecordConfiguration : IEntityTypeConfiguration<QfodRecord>
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Data.Configurations;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||||
|
|
||||||
public class RxTypeConfiguration : IEntityTypeConfiguration<RxType>
|
public class RxTypeConfiguration : IEntityTypeConfiguration<RxType>
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Data.Configurations;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||||
|
|
||||||
public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
|
public class TestScenarioConfiguration : IEntityTypeConfiguration<TestScenario>
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Data.Configurations;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||||
|
|
||||||
public class TxHardwareConfiguration : IEntityTypeConfiguration<TxHardware>
|
public class TxHardwareConfiguration : IEntityTypeConfiguration<TxHardware>
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Data.Configurations;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||||
|
|
||||||
public class TxPanelConfiguration : IEntityTypeConfiguration<TxPanel>
|
public class TxPanelConfiguration : IEntityTypeConfiguration<TxPanel>
|
||||||
{
|
{
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Data.Configurations;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData.Configurations;
|
||||||
|
|
||||||
public class TxSoftwareConfiguration : IEntityTypeConfiguration<TxSoftware>
|
public class TxSoftwareConfiguration : IEntityTypeConfiguration<TxSoftware>
|
||||||
{
|
{
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Data;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 数据库上下文工厂实现 - 支持SQLite和PostgreSQL切换
|
/// 数据库上下文工厂实现 - 支持SQLite和PostgreSQL切换
|
||||||
@@ -30,7 +30,9 @@ public class DbContextFactory : IDbContextFactory
|
|||||||
|
|
||||||
case "sqlite":
|
case "sqlite":
|
||||||
default:
|
default:
|
||||||
var dbPath = _configuration["Database:Path"] ?? "./data/database/wctminer.db";
|
var dbPath = _configuration["Database:LocalPath"]
|
||||||
|
?? _configuration["Database:Path"]
|
||||||
|
?? "./data/database/local.db";
|
||||||
EnsureDirectoryExists(dbPath);
|
EnsureDirectoryExists(dbPath);
|
||||||
optionsBuilder.UseSqlite($"Data Source={dbPath}");
|
optionsBuilder.UseSqlite($"Data Source={dbPath}");
|
||||||
break;
|
break;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Data;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 数据库上下文工厂接口
|
/// 数据库上下文工厂接口
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Data;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 初始数据种子
|
/// 初始数据种子
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Data;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// WCT数据采集数据库上下文
|
/// WCT数据采集数据库上下文
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Security;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 路径验证结果
|
/// 路径验证结果
|
||||||
@@ -36,7 +36,7 @@ public class PathValidator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 默认允许的输出目录
|
/// 默认允许的输出目录
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly string[] DefaultAllowedDirectories = ["exports", "output", "data/exports"];
|
private static readonly string[] DefaultAllowedDirectories = ["exports", "output", "data/exports", "data/output"];
|
||||||
|
|
||||||
public PathValidator(string applicationBasePath, IEnumerable<string>? allowedDirectories = null)
|
public PathValidator(string applicationBasePath, IEnumerable<string>? allowedDirectories = null)
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Gpulse.WCT.DataAnalyzer.Core.Security;
|
namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 安全相关常量定义
|
/// 安全相关常量定义
|
||||||
33
src/Gpulse.WCT.DataAnalyzer/Commands/AggregateCommand.cs
Normal file
33
src/Gpulse.WCT.DataAnalyzer/Commands/AggregateCommand.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
|
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从本地解析库生成正式发布库。
|
||||||
|
/// </summary>
|
||||||
|
public class AggregateCommand : Command
|
||||||
|
{
|
||||||
|
public AggregateCommand(AggregationService aggregationService)
|
||||||
|
: base("aggregate", "Aggregate local log data into the release database")
|
||||||
|
{
|
||||||
|
var rebuildOption = new Option<bool>(
|
||||||
|
"--rebuild",
|
||||||
|
() => true,
|
||||||
|
"Rebuild the release table before inserting aggregated records");
|
||||||
|
AddOption(rebuildOption);
|
||||||
|
|
||||||
|
this.SetHandler(async rebuild =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var report = await aggregationService.AggregateAsync(rebuild);
|
||||||
|
Console.WriteLine($"Aggregated {report.LocalRecordCount} local Ploss records into {report.ReleaseRecordCount} release records.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Aggregation failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}, rebuildOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Services;
|
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Services;
|
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||||
|
|
||||||
@@ -25,8 +25,8 @@ public class ExportCommand : Command
|
|||||||
|
|
||||||
var typeOption = new Option<string>(
|
var typeOption = new Option<string>(
|
||||||
"--type",
|
"--type",
|
||||||
() => "all",
|
() => "charging-parameters",
|
||||||
"Data type to export: qfod, ploss, or all"
|
"Data type: charging-parameters, qfod, ploss, or all"
|
||||||
);
|
);
|
||||||
|
|
||||||
AddOption(formatOption);
|
AddOption(formatOption);
|
||||||
@@ -44,6 +44,12 @@ public class ExportCommand : Command
|
|||||||
{
|
{
|
||||||
if (format == "csv")
|
if (format == "csv")
|
||||||
{
|
{
|
||||||
|
if (type is "charging-parameters" or "charging" or "all")
|
||||||
|
{
|
||||||
|
var path = await exportService.ExportChargingParametersToCsvAsync(output);
|
||||||
|
Console.WriteLine($" ChargingParameterDatabase CSV exported: {path}");
|
||||||
|
}
|
||||||
|
|
||||||
if (type is "qfod" or "all")
|
if (type is "qfod" or "all")
|
||||||
{
|
{
|
||||||
var path = await exportService.ExportQfodToCsvAsync(output);
|
var path = await exportService.ExportQfodToCsvAsync(output);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Services;
|
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Services;
|
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
namespace Gpulse.WCT.DataAnalyzer.Commands;
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Gpulse.WCT.DataAnalyzer.Commands;
|
using Gpulse.WCT.DataAnalyzer.Commands;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Data;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Extensions;
|
using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions;
|
||||||
using Gpulse.WCT.DataAnalyzer.Core.Services;
|
using Gpulse.WCT.DataAnalyzer.Core.Application;
|
||||||
|
|
||||||
namespace Gpulse.WCT.DataAnalyzer;
|
namespace Gpulse.WCT.DataAnalyzer;
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ public class Program
|
|||||||
{
|
{
|
||||||
// 1. 构建引导配置(用于 Serilog 初始化)
|
// 1. 构建引导配置(用于 Serilog 初始化)
|
||||||
var bootstrapConfig = new ConfigurationBuilder()
|
var bootstrapConfig = new ConfigurationBuilder()
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
.SetBasePath(AppContext.BaseDirectory)
|
||||||
.AddJsonFile("appsettings.json", optional: false)
|
.AddJsonFile("appsettings.json", optional: false)
|
||||||
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true)
|
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true)
|
||||||
.AddEnvironmentVariables()
|
.AddEnvironmentVariables()
|
||||||
@@ -59,6 +59,9 @@ public class Program
|
|||||||
var dbContext = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
|
var dbContext = scope.ServiceProvider.GetRequiredService<WctMinerDbContext>();
|
||||||
await dbContext.Database.EnsureCreatedAsync();
|
await dbContext.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
var releaseContext = scope.ServiceProvider.GetRequiredService<ReleaseDbContext>();
|
||||||
|
await releaseContext.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
// 种子数据
|
// 种子数据
|
||||||
SeedData.Initialize(dbContext);
|
SeedData.Initialize(dbContext);
|
||||||
}
|
}
|
||||||
@@ -70,6 +73,7 @@ public class Program
|
|||||||
var rootCommand = new RootCommand("Gpulse.WCT.DataAnalyzer - FOD Log Data Parser");
|
var rootCommand = new RootCommand("Gpulse.WCT.DataAnalyzer - FOD Log Data Parser");
|
||||||
|
|
||||||
rootCommand.AddCommand(new ParseCommand(provider.GetRequiredService<ParseService>()));
|
rootCommand.AddCommand(new ParseCommand(provider.GetRequiredService<ParseService>()));
|
||||||
|
rootCommand.AddCommand(new AggregateCommand(provider.GetRequiredService<AggregationService>()));
|
||||||
rootCommand.AddCommand(new StatsCommand(provider.GetRequiredService<StatsService>()));
|
rootCommand.AddCommand(new StatsCommand(provider.GetRequiredService<StatsService>()));
|
||||||
rootCommand.AddCommand(new ExportCommand(provider.GetRequiredService<ExportService>()));
|
rootCommand.AddCommand(new ExportCommand(provider.GetRequiredService<ExportService>()));
|
||||||
rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService<CleanService>()));
|
rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService<CleanService>()));
|
||||||
|
|||||||
Reference in New Issue
Block a user