diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Services/CleanService.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Administration/CleanService.cs similarity index 94% rename from src/Gpulse.WCT.DataAnalyzer.Core/Services/CleanService.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Application/Administration/CleanService.cs index 32d56ef..e863071 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Services/CleanService.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Administration/CleanService.cs @@ -1,9 +1,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Gpulse.WCT.DataAnalyzer.Core.Data; -using Gpulse.WCT.DataAnalyzer.Core.Models; +using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData; +using Gpulse.WCT.DataAnalyzer.Core.Domain.Local; -namespace Gpulse.WCT.DataAnalyzer.Core.Services; +namespace Gpulse.WCT.DataAnalyzer.Core.Application; /// /// 数据清理服务 - 支持软删除 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Services/StatsService.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Administration/StatsService.cs similarity index 96% rename from src/Gpulse.WCT.DataAnalyzer.Core/Services/StatsService.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Application/Administration/StatsService.cs index 72c5b5b..2a3d9a0 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Services/StatsService.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Administration/StatsService.cs @@ -1,9 +1,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Gpulse.WCT.DataAnalyzer.Core.Data; -using Gpulse.WCT.DataAnalyzer.Core.Models; +using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData; +using Gpulse.WCT.DataAnalyzer.Core.Domain.Local; -namespace Gpulse.WCT.DataAnalyzer.Core.Services; +namespace Gpulse.WCT.DataAnalyzer.Core.Application; /// /// 统计查询服务 - 使用数据库端聚合优化内存效率 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Services/ExportService.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Exporting/ExportService.cs similarity index 81% rename from src/Gpulse.WCT.DataAnalyzer.Core/Services/ExportService.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Application/Exporting/ExportService.cs index 1ae8631..cdda938 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Services/ExportService.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Exporting/ExportService.cs @@ -3,11 +3,11 @@ using System.Text; using System.Text.Json; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Gpulse.WCT.DataAnalyzer.Core.Data; -using Gpulse.WCT.DataAnalyzer.Core.Models; -using Gpulse.WCT.DataAnalyzer.Core.Security; +using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData; +using Gpulse.WCT.DataAnalyzer.Core.Domain.Local; +using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security; -namespace Gpulse.WCT.DataAnalyzer.Core.Services; +namespace Gpulse.WCT.DataAnalyzer.Core.Application; /// /// 数据导出服务 - 支持安全的路径验证和 RFC 4180 标准 CSV 转义 @@ -15,12 +15,14 @@ namespace Gpulse.WCT.DataAnalyzer.Core.Services; public class ExportService { private readonly WctMinerDbContext _context; + private readonly ReleaseDbContext _releaseContext; private readonly ILogger _logger; private readonly PathValidator _pathValidator; - public ExportService(WctMinerDbContext context, ILogger logger) + public ExportService(WctMinerDbContext context, ReleaseDbContext releaseContext, ILogger logger) { _context = context; + _releaseContext = releaseContext; _logger = logger; _pathValidator = new PathValidator(AppDomain.CurrentDomain.BaseDirectory); } @@ -305,7 +307,49 @@ public class ExportService } /// - /// RFC 4180 标准 CSV 字段转义 + /// 导出正式发布库中的 ChargingParameterDatabase CSV。 + /// + public async Task 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; + } + /// private static string EscapeCsvField(object? value) { diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Services/DimensionService.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Application/LocalIngestion/DimensionService.cs similarity index 96% rename from src/Gpulse.WCT.DataAnalyzer.Core/Services/DimensionService.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Application/LocalIngestion/DimensionService.cs index 95b0a02..911267f 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Services/DimensionService.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Application/LocalIngestion/DimensionService.cs @@ -1,9 +1,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Gpulse.WCT.DataAnalyzer.Core.Data; -using Gpulse.WCT.DataAnalyzer.Core.Models; +using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData; +using Gpulse.WCT.DataAnalyzer.Core.Domain.Local; -namespace Gpulse.WCT.DataAnalyzer.Core.Services; +namespace Gpulse.WCT.DataAnalyzer.Core.Application; /// /// 维度表管理服务 - 负责维度表的CRUD操作(并发安全版本) diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Services/ParseService.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Application/LocalIngestion/ParseService.cs similarity index 97% rename from src/Gpulse.WCT.DataAnalyzer.Core/Services/ParseService.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Application/LocalIngestion/ParseService.cs index c214028..dc7a128 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Services/ParseService.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Application/LocalIngestion/ParseService.cs @@ -1,11 +1,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; -using Gpulse.WCT.DataAnalyzer.Core.Data; -using Gpulse.WCT.DataAnalyzer.Core.Models; -using Gpulse.WCT.DataAnalyzer.Core.Parsers; +using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData; +using Gpulse.WCT.DataAnalyzer.Core.Domain.Local; +using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing; -namespace Gpulse.WCT.DataAnalyzer.Core.Services; +namespace Gpulse.WCT.DataAnalyzer.Core.Application; /// /// 解析流程协调服务 - 协调文件名解析、行解析、批量写入 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Services/ScenarioService.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Application/LocalIngestion/ScenarioService.cs similarity index 98% rename from src/Gpulse.WCT.DataAnalyzer.Core/Services/ScenarioService.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Application/LocalIngestion/ScenarioService.cs index 1b8c7e8..b75e9e2 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Services/ScenarioService.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Application/LocalIngestion/ScenarioService.cs @@ -1,10 +1,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using Gpulse.WCT.DataAnalyzer.Core.Data; -using Gpulse.WCT.DataAnalyzer.Core.Models; -using Gpulse.WCT.DataAnalyzer.Core.Parsers; +using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData; +using Gpulse.WCT.DataAnalyzer.Core.Domain.Local; +using Gpulse.WCT.DataAnalyzer.Core.Application.Parsing; -namespace Gpulse.WCT.DataAnalyzer.Core.Services; +namespace Gpulse.WCT.DataAnalyzer.Core.Application; /// /// 篮选条件常量 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Parsers/FileNameParser.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/FileNameParser.cs similarity index 97% rename from src/Gpulse.WCT.DataAnalyzer.Core/Parsers/FileNameParser.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/FileNameParser.cs index 612771a..fd834c0 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Parsers/FileNameParser.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/FileNameParser.cs @@ -1,6 +1,6 @@ using System.Globalization; -namespace Gpulse.WCT.DataAnalyzer.Core.Parsers; +namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing; /// /// 文件名解析器 - 从文件名提取场景信息 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Parsers/IParser.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/IParser.cs similarity index 95% rename from src/Gpulse.WCT.DataAnalyzer.Core/Parsers/IParser.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/IParser.cs index 5134a9d..4ce6212 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Parsers/IParser.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/IParser.cs @@ -1,4 +1,4 @@ -namespace Gpulse.WCT.DataAnalyzer.Core.Parsers; +namespace Gpulse.WCT.DataAnalyzer.Core.Application.Parsing; /// /// 通用解析器接口 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Parsers/PlossParser.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/PlossParser.cs similarity index 98% rename from src/Gpulse.WCT.DataAnalyzer.Core/Parsers/PlossParser.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/PlossParser.cs index f61464e..7609dc0 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Parsers/PlossParser.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/PlossParser.cs @@ -1,7 +1,7 @@ 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; /// /// Ploss FOD日志解析器 - 支持两行格式 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Parsers/QfodParser.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/QfodParser.cs similarity index 94% rename from src/Gpulse.WCT.DataAnalyzer.Core/Parsers/QfodParser.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/QfodParser.cs index 3c3242d..7b4961c 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Parsers/QfodParser.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Parsing/QfodParser.cs @@ -1,7 +1,7 @@ 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; /// /// Qfod日志解析器 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Application/Publishing/AggregationService.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Publishing/AggregationService.cs new file mode 100644 index 0000000..3712db2 --- /dev/null +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Publishing/AggregationService.cs @@ -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; + +/// +/// 从本地解析库生成独立正式发布库。 +/// 当前业务规则:功率列使用 Ploss 的 Field7,重复结果取平均值;缺失值保留为空。 +/// +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 _logger; + + public AggregationService( + WctMinerDbContext localContext, + ReleaseDbContext releaseContext, + IConfiguration configuration, + ILogger logger) + { + _localContext = localContext; + _releaseContext = releaseContext; + _configuration = configuration; + _logger = logger; + } + + public async Task 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> 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 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 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); diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Services/ThresholdCalculator.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Publishing/ThresholdCalculator.cs similarity index 96% rename from src/Gpulse.WCT.DataAnalyzer.Core/Services/ThresholdCalculator.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Application/Publishing/ThresholdCalculator.cs index 2def79a..6e9b172 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Services/ThresholdCalculator.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Application/Publishing/ThresholdCalculator.cs @@ -1,4 +1,4 @@ -namespace Gpulse.WCT.DataAnalyzer.Core.Services; +namespace Gpulse.WCT.DataAnalyzer.Core.Application; /// /// 阈值计算器 - 根据接收功率查表获取动态阈值 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Models/PlossRecord.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/PlossRecord.cs similarity index 97% rename from src/Gpulse.WCT.DataAnalyzer.Core/Models/PlossRecord.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/PlossRecord.cs index 95d5eb2..0f1b0b5 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Models/PlossRecord.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/PlossRecord.cs @@ -1,4 +1,4 @@ -namespace Gpulse.WCT.DataAnalyzer.Core.Models; +namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local; /// /// Ploss记录实体 - 存储Ploss FOD格式解析数据 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Models/QfodRecord.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/QfodRecord.cs similarity index 92% rename from src/Gpulse.WCT.DataAnalyzer.Core/Models/QfodRecord.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/QfodRecord.cs index 3dcfecc..9b470e9 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Models/QfodRecord.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/QfodRecord.cs @@ -1,4 +1,4 @@ -namespace Gpulse.WCT.DataAnalyzer.Core.Models; +namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local; /// /// Qfod记录实体 - 存储Qfod格式解析数据 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Models/RxType.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/RxType.cs similarity index 87% rename from src/Gpulse.WCT.DataAnalyzer.Core/Models/RxType.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/RxType.cs index 71989b0..281d66b 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Models/RxType.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/RxType.cs @@ -1,4 +1,4 @@ -namespace Gpulse.WCT.DataAnalyzer.Core.Models; +namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local; /// /// RX类型维度表 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Models/TestScenario.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/TestScenario.cs similarity index 95% rename from src/Gpulse.WCT.DataAnalyzer.Core/Models/TestScenario.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/TestScenario.cs index 1e53e72..4efd970 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Models/TestScenario.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/TestScenario.cs @@ -1,4 +1,4 @@ -namespace Gpulse.WCT.DataAnalyzer.Core.Models; +namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local; /// /// 测试场景实体 - 通过外键关联各维度表 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Models/TxHardware.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/TxHardware.cs similarity index 88% rename from src/Gpulse.WCT.DataAnalyzer.Core/Models/TxHardware.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/TxHardware.cs index f5cb815..a03ee01 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Models/TxHardware.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/TxHardware.cs @@ -1,4 +1,4 @@ -namespace Gpulse.WCT.DataAnalyzer.Core.Models; +namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local; /// /// TX硬件版本维度表 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Models/TxPanel.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/TxPanel.cs similarity index 88% rename from src/Gpulse.WCT.DataAnalyzer.Core/Models/TxPanel.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/TxPanel.cs index d631f3d..cd5fbf7 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Models/TxPanel.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/TxPanel.cs @@ -1,4 +1,4 @@ -namespace Gpulse.WCT.DataAnalyzer.Core.Models; +namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local; /// /// TX面板类型维度表 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Models/TxSoftware.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/TxSoftware.cs similarity index 88% rename from src/Gpulse.WCT.DataAnalyzer.Core/Models/TxSoftware.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/TxSoftware.cs index 7e8b4b0..1136678 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Models/TxSoftware.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Local/TxSoftware.cs @@ -1,4 +1,4 @@ -namespace Gpulse.WCT.DataAnalyzer.Core.Models; +namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Local; /// /// TX软件版本维度表 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Release/ChargingParameterRecord.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Release/ChargingParameterRecord.cs new file mode 100644 index 0000000..b1fb5fc --- /dev/null +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Domain/Release/ChargingParameterRecord.cs @@ -0,0 +1,33 @@ +namespace Gpulse.WCT.DataAnalyzer.Core.Domain.Release; + +/// +/// 正式发布库中的充电参数记录。 +/// 此实体只属于 ReleaseDbContext,不引用本地日志数据库实体。 +/// +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; +} diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Configuration/AppSettings.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/Configuration/AppSettings.cs similarity index 76% rename from src/Gpulse.WCT.DataAnalyzer.Core/Configuration/AppSettings.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/Configuration/AppSettings.cs index 0011007..3f5a3d4 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Configuration/AppSettings.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/Configuration/AppSettings.cs @@ -1,4 +1,4 @@ -namespace Gpulse.WCT.DataAnalyzer.Core.Configuration; +namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Configuration; /// /// 应用配置 @@ -16,7 +16,9 @@ public class AppSettings public class DatabaseSettings { 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 配置(可选) public string? Host { get; set; } diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Extensions/ServiceCollectionExtensions.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/Extensions/ServiceCollectionExtensions.cs similarity index 68% rename from src/Gpulse.WCT.DataAnalyzer.Core/Extensions/ServiceCollectionExtensions.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/Extensions/ServiceCollectionExtensions.cs index 95fdd87..e26d5e3 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Extensions/ServiceCollectionExtensions.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/Extensions/ServiceCollectionExtensions.cs @@ -1,10 +1,10 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Gpulse.WCT.DataAnalyzer.Core.Data; -using Gpulse.WCT.DataAnalyzer.Core.Parsers; -using Gpulse.WCT.DataAnalyzer.Core.Services; +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.Extensions; +namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions; /// /// 服务注册扩展 @@ -28,6 +28,15 @@ public static class ServiceCollectionExtensions return factory.CreateDbContext(); }); + services.AddScoped(); + + // 注册正式发布数据库上下文(与本地解析数据库完全独立) + services.AddScoped(sp => + { + var factory = sp.GetRequiredService(); + return factory.CreateDbContext(); + }); + return services; } @@ -48,6 +57,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/PlossRecordConfiguration.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/PlossRecordConfiguration.cs similarity index 96% rename from src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/PlossRecordConfiguration.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/PlossRecordConfiguration.cs index 5b52287..9338744 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/PlossRecordConfiguration.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/PlossRecordConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; 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 { diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/QfodRecordConfiguration.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/QfodRecordConfiguration.cs similarity index 94% rename from src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/QfodRecordConfiguration.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/QfodRecordConfiguration.cs index d892f22..1716115 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/QfodRecordConfiguration.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/QfodRecordConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; 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 { diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/RxTypeConfiguration.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/RxTypeConfiguration.cs similarity index 88% rename from src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/RxTypeConfiguration.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/RxTypeConfiguration.cs index c5926e8..37b7363 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/RxTypeConfiguration.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/RxTypeConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; 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 { diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/TestScenarioConfiguration.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/TestScenarioConfiguration.cs similarity index 96% rename from src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/TestScenarioConfiguration.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/TestScenarioConfiguration.cs index 4510afc..eea344d 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/TestScenarioConfiguration.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/TestScenarioConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; 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 { diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/TxHardwareConfiguration.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/TxHardwareConfiguration.cs similarity index 88% rename from src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/TxHardwareConfiguration.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/TxHardwareConfiguration.cs index 4f66f31..16b4d10 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/TxHardwareConfiguration.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/TxHardwareConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; 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 { diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/TxPanelConfiguration.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/TxPanelConfiguration.cs similarity index 88% rename from src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/TxPanelConfiguration.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/TxPanelConfiguration.cs index 13b706d..fcdce2a 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/TxPanelConfiguration.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/TxPanelConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; 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 { diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/TxSoftwareConfiguration.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/TxSoftwareConfiguration.cs similarity index 88% rename from src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/TxSoftwareConfiguration.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/TxSoftwareConfiguration.cs index e33e3b1..61ea2ab 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Data/Configurations/TxSoftwareConfiguration.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/Configurations/TxSoftwareConfiguration.cs @@ -1,8 +1,8 @@ using Microsoft.EntityFrameworkCore; 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 { diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Data/DbContextFactory.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/DbContextFactory.cs similarity index 90% rename from src/Gpulse.WCT.DataAnalyzer.Core/Data/DbContextFactory.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/DbContextFactory.cs index b69274b..8020743 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Data/DbContextFactory.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/DbContextFactory.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; -namespace Gpulse.WCT.DataAnalyzer.Core.Data; +namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData; /// /// 数据库上下文工厂实现 - 支持SQLite和PostgreSQL切换 @@ -30,7 +30,9 @@ public class DbContextFactory : IDbContextFactory case "sqlite": default: - var dbPath = _configuration["Database:Path"] ?? "./data/database/wctminer.db"; + var dbPath = _configuration["Database:LocalPath"] + ?? _configuration["Database:Path"] + ?? "./data/database/local.db"; EnsureDirectoryExists(dbPath); optionsBuilder.UseSqlite($"Data Source={dbPath}"); break; diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Data/IDbContextFactory.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/IDbContextFactory.cs similarity index 68% rename from src/Gpulse.WCT.DataAnalyzer.Core/Data/IDbContextFactory.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/IDbContextFactory.cs index f0283b2..a936789 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Data/IDbContextFactory.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/IDbContextFactory.cs @@ -1,4 +1,4 @@ -namespace Gpulse.WCT.DataAnalyzer.Core.Data; +namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData; /// /// 数据库上下文工厂接口 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Data/SeedData.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/SeedData.cs similarity index 87% rename from src/Gpulse.WCT.DataAnalyzer.Core/Data/SeedData.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/SeedData.cs index 72654b1..d981923 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Data/SeedData.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/SeedData.cs @@ -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; /// /// 初始数据种子 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Data/WctMinerDbContext.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/WctMinerDbContext.cs similarity index 89% rename from src/Gpulse.WCT.DataAnalyzer.Core/Data/WctMinerDbContext.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/WctMinerDbContext.cs index f2e912d..2743999 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Data/WctMinerDbContext.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/LocalData/WctMinerDbContext.cs @@ -1,7 +1,7 @@ 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; /// /// WCT数据采集数据库上下文 diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/ReleaseData/Configurations/ChargingParameterRecordConfiguration.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/ReleaseData/Configurations/ChargingParameterRecordConfiguration.cs new file mode 100644 index 0000000..5d675aa --- /dev/null +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/ReleaseData/Configurations/ChargingParameterRecordConfiguration.cs @@ -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 +{ + public void Configure(EntityTypeBuilder 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 builder, + System.Linq.Expressions.Expression> property, + string columnName) + { + builder.Property(property).IsRequired().HasMaxLength(128).HasColumnName(columnName); + } + + private static void ConfigureNumber( + EntityTypeBuilder builder, + System.Linq.Expressions.Expression> property, + string columnName) + { + builder.Property(property).IsRequired(false).HasColumnName(columnName).HasPrecision(18, 6); + } +} diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/ReleaseData/IReleaseDbContextFactory.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/ReleaseData/IReleaseDbContextFactory.cs new file mode 100644 index 0000000..8d1ab5b --- /dev/null +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/ReleaseData/IReleaseDbContextFactory.cs @@ -0,0 +1,6 @@ +namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData; + +public interface IReleaseDbContextFactory +{ + ReleaseDbContext CreateDbContext(); +} diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/ReleaseData/ReleaseDbContext.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/ReleaseData/ReleaseDbContext.cs new file mode 100644 index 0000000..afb5ac8 --- /dev/null +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/ReleaseData/ReleaseDbContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Gpulse.WCT.DataAnalyzer.Core.Domain.Release; + +namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData; + +/// +/// 正式发布数据库上下文。只包含最终充电参数记录。 +/// +public class ReleaseDbContext : DbContext +{ + public ReleaseDbContext(DbContextOptions options) + : base(options) { } + + public DbSet ChargingParameters => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(ReleaseDbContext).Assembly); + } +} diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/ReleaseData/ReleaseDbContextFactory.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/ReleaseData/ReleaseDbContextFactory.cs new file mode 100644 index 0000000..0b15ff5 --- /dev/null +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/ReleaseData/ReleaseDbContextFactory.cs @@ -0,0 +1,58 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData; + +/// +/// 正式发布数据库上下文工厂。 +/// +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(); + + 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); + } +} diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Security/PathValidator.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/Security/PathValidator.cs similarity index 97% rename from src/Gpulse.WCT.DataAnalyzer.Core/Security/PathValidator.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/Security/PathValidator.cs index 3edd08b..25aae0a 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Security/PathValidator.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/Security/PathValidator.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace Gpulse.WCT.DataAnalyzer.Core.Security; +namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security; /// /// 路径验证结果 @@ -36,7 +36,7 @@ public class PathValidator /// /// 默认允许的输出目录 /// - 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? allowedDirectories = null) { diff --git a/src/Gpulse.WCT.DataAnalyzer.Core/Security/SecurityConstants.cs b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/Security/SecurityConstants.cs similarity index 95% rename from src/Gpulse.WCT.DataAnalyzer.Core/Security/SecurityConstants.cs rename to src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/Security/SecurityConstants.cs index 29467c0..e89b47e 100644 --- a/src/Gpulse.WCT.DataAnalyzer.Core/Security/SecurityConstants.cs +++ b/src/Gpulse.WCT.DataAnalyzer.Core/Infrastructure/Security/SecurityConstants.cs @@ -1,4 +1,4 @@ -namespace Gpulse.WCT.DataAnalyzer.Core.Security; +namespace Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Security; /// /// 安全相关常量定义 diff --git a/src/Gpulse.WCT.DataAnalyzer/Commands/AggregateCommand.cs b/src/Gpulse.WCT.DataAnalyzer/Commands/AggregateCommand.cs new file mode 100644 index 0000000..06cb47c --- /dev/null +++ b/src/Gpulse.WCT.DataAnalyzer/Commands/AggregateCommand.cs @@ -0,0 +1,33 @@ +using System.CommandLine; +using Gpulse.WCT.DataAnalyzer.Core.Application; + +namespace Gpulse.WCT.DataAnalyzer.Commands; + +/// +/// 从本地解析库生成正式发布库。 +/// +public class AggregateCommand : Command +{ + public AggregateCommand(AggregationService aggregationService) + : base("aggregate", "Aggregate local log data into the release database") + { + var rebuildOption = new Option( + "--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); + } +} diff --git a/src/Gpulse.WCT.DataAnalyzer/Commands/CleanCommand.cs b/src/Gpulse.WCT.DataAnalyzer/Commands/CleanCommand.cs index 216db79..0a8bcff 100644 --- a/src/Gpulse.WCT.DataAnalyzer/Commands/CleanCommand.cs +++ b/src/Gpulse.WCT.DataAnalyzer/Commands/CleanCommand.cs @@ -1,5 +1,5 @@ using System.CommandLine; -using Gpulse.WCT.DataAnalyzer.Core.Services; +using Gpulse.WCT.DataAnalyzer.Core.Application; namespace Gpulse.WCT.DataAnalyzer.Commands; diff --git a/src/Gpulse.WCT.DataAnalyzer/Commands/ExportCommand.cs b/src/Gpulse.WCT.DataAnalyzer/Commands/ExportCommand.cs index 1b8156a..fb124a2 100644 --- a/src/Gpulse.WCT.DataAnalyzer/Commands/ExportCommand.cs +++ b/src/Gpulse.WCT.DataAnalyzer/Commands/ExportCommand.cs @@ -1,5 +1,5 @@ using System.CommandLine; -using Gpulse.WCT.DataAnalyzer.Core.Services; +using Gpulse.WCT.DataAnalyzer.Core.Application; namespace Gpulse.WCT.DataAnalyzer.Commands; @@ -25,8 +25,8 @@ public class ExportCommand : Command var typeOption = new Option( "--type", - () => "all", - "Data type to export: qfod, ploss, or all" + () => "charging-parameters", + "Data type: charging-parameters, qfod, ploss, or all" ); AddOption(formatOption); @@ -44,6 +44,12 @@ public class ExportCommand : Command { 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") { var path = await exportService.ExportQfodToCsvAsync(output); diff --git a/src/Gpulse.WCT.DataAnalyzer/Commands/ParseCommand.cs b/src/Gpulse.WCT.DataAnalyzer/Commands/ParseCommand.cs index 9f7c040..685f712 100644 --- a/src/Gpulse.WCT.DataAnalyzer/Commands/ParseCommand.cs +++ b/src/Gpulse.WCT.DataAnalyzer/Commands/ParseCommand.cs @@ -1,5 +1,5 @@ using System.CommandLine; -using Gpulse.WCT.DataAnalyzer.Core.Services; +using Gpulse.WCT.DataAnalyzer.Core.Application; namespace Gpulse.WCT.DataAnalyzer.Commands; diff --git a/src/Gpulse.WCT.DataAnalyzer/Commands/StatsCommand.cs b/src/Gpulse.WCT.DataAnalyzer/Commands/StatsCommand.cs index 3d517bf..8ac1c98 100644 --- a/src/Gpulse.WCT.DataAnalyzer/Commands/StatsCommand.cs +++ b/src/Gpulse.WCT.DataAnalyzer/Commands/StatsCommand.cs @@ -1,5 +1,5 @@ using System.CommandLine; -using Gpulse.WCT.DataAnalyzer.Core.Services; +using Gpulse.WCT.DataAnalyzer.Core.Application; namespace Gpulse.WCT.DataAnalyzer.Commands; diff --git a/src/Gpulse.WCT.DataAnalyzer/Program.cs b/src/Gpulse.WCT.DataAnalyzer/Program.cs index 2f0da0c..3418911 100644 --- a/src/Gpulse.WCT.DataAnalyzer/Program.cs +++ b/src/Gpulse.WCT.DataAnalyzer/Program.cs @@ -4,9 +4,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Serilog; using Gpulse.WCT.DataAnalyzer.Commands; -using Gpulse.WCT.DataAnalyzer.Core.Data; -using Gpulse.WCT.DataAnalyzer.Core.Extensions; -using Gpulse.WCT.DataAnalyzer.Core.Services; +using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.LocalData; +using Gpulse.WCT.DataAnalyzer.Core.Infrastructure.Extensions; +using Gpulse.WCT.DataAnalyzer.Core.Application; namespace Gpulse.WCT.DataAnalyzer; @@ -19,7 +19,7 @@ public class Program { // 1. 构建引导配置(用于 Serilog 初始化) var bootstrapConfig = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) + .SetBasePath(AppContext.BaseDirectory) .AddJsonFile("appsettings.json", optional: false) .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true) .AddEnvironmentVariables() @@ -59,6 +59,9 @@ public class Program var dbContext = scope.ServiceProvider.GetRequiredService(); await dbContext.Database.EnsureCreatedAsync(); + var releaseContext = scope.ServiceProvider.GetRequiredService(); + await releaseContext.Database.EnsureCreatedAsync(); + // 种子数据 SeedData.Initialize(dbContext); } @@ -70,6 +73,7 @@ public class Program var rootCommand = new RootCommand("Gpulse.WCT.DataAnalyzer - FOD Log Data Parser"); rootCommand.AddCommand(new ParseCommand(provider.GetRequiredService())); + rootCommand.AddCommand(new AggregateCommand(provider.GetRequiredService())); rootCommand.AddCommand(new StatsCommand(provider.GetRequiredService())); rootCommand.AddCommand(new ExportCommand(provider.GetRequiredService())); rootCommand.AddCommand(new CleanCommand(provider.GetRequiredService()));