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);