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; /// /// 从本地解析库生成独立正式发布库。 /// 当前业务规则:功率列按 Threshold(Field9) 分桶、取值 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 qfodAggregates = await LoadQfodAggregatesAsync(cancellationToken); var records = rows .GroupBy(r => BuildKey(r.Scenario)) .Select(group => CreateRecord(group.Key, group.ToList(), qfodAggregates)) .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) { // 只加载 Ploss 及其引用维度。Qfod 明细不在此加载(避免集合 Include 产生笛卡尔积), // 仅在 LoadQfodAggregatesAsync 中按场景聚合出 Sum/Count。 var records = await _localContext.PlossRecords .AsNoTracking() .AsSplitQuery() .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) .ToListAsync(cancellationToken); return records.Select(r => new PlossRow( r, r.Scenario, r.Scenario.TxPanel.Name, r.Scenario.TxHardware.Version, r.Scenario.RxType.Name)).ToList(); } /// /// 按场景聚合 Qfod 记录的 CurrentQ/RawQ 求和与计数(在数据库侧完成,只返回每场景 3 个数字)。 /// private async Task> LoadQfodAggregatesAsync(CancellationToken cancellationToken) { var groups = await _localContext.QfodRecords .AsNoTracking() .Where(r => !r.IsDeleted) .GroupBy(r => r.ScenarioId) .Select(g => new { ScenarioId = g.Key, QValueSum = g.Sum(r => (double)r.CurrentQ), QBaseSum = g.Sum(r => (double)r.RawQ), Count = g.Count() }) .ToListAsync(cancellationToken); return groups.ToDictionary( g => g.ScenarioId, g => new QfodAggregate(g.QValueSum, g.QBaseSum, g.Count)); } 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, IReadOnlyDictionary qfodAggregates) { var values = PowerLevels.Select(power => Average(rows .Where(row => IsPowerMatch(row.Record, power)) .Select(row => (double?)row.Record.Field7))).ToArray(); var (qValueSum, qBaseSum, qCount) = AggregateQfod(rows, qfodAggregates); 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 = qCount > 0 ? qValueSum / qCount : null, QBaseValue = qCount > 0 ? qBaseSum / qCount : null, PqCoefficient = Average(rows.Select(row => row.Record.DeltaP.HasValue ? (double?)row.Record.DeltaP.Value : null)), ResonanceFrequency = null }; } /// /// 累加分组内各行的场景 Qfod 聚合值。 /// 等价于原实现的 SelectMany(row => row.Scenario.QfodRecords):同一场景出现 k 次,其 Sum 与 Count 同倍放大, /// 商保持不变,因此多场景分组时加权语义与原来完全一致,但无需加载 Qfod 明细。 /// private static (double Sum, double BaseSum, int Count) AggregateQfod( List rows, IReadOnlyDictionary qfodAggregates) { double sum = 0, baseSum = 0; int count = 0; foreach (var row in rows) { if (qfodAggregates.TryGetValue(row.Scenario.Id, out var agg)) { sum += agg.QValueSum; baseSum += agg.QBaseSum; count += agg.Count; } } return (sum, baseSum, count); } private static bool IsPowerMatch(PlossRecord record, int power) { // 功率列按 Threshold(Field9) 分桶:记录的阈值等于哪个档位,其 Ploss 就进哪一列。 // 注:需求文档中 Threshold 为逻辑编号 (8),但当前解析数据里阈值实际落在 field_9。 return record.Field9 == 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); private readonly record struct QfodAggregate(double QValueSum, double QBaseSum, int Count); } public record AggregationReport(int ReleaseRecordCount, int LocalRecordCount);