Files
WCTDataMiner/src/Gpulse.WCT.DataAnalyzer.Core/Application/Publishing/AggregationService.cs
Scottxjw 430b38b361 feat(aggregation): 按 Threshold 分桶并优化 Qfod 聚合
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-14 09:24:38 +08:00

207 lines
8.3 KiB
C#

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>
/// 从本地解析库生成独立正式发布库。
/// 当前业务规则:功率列按 Threshold(Field9) 分桶、取值 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 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<List<PlossRow>> 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();
}
/// <summary>
/// 按场景聚合 Qfod 记录的 CurrentQ/RawQ 求和与计数(在数据库侧完成,只返回每场景 3 个数字)。
/// </summary>
private async Task<Dictionary<Guid, QfodAggregate>> 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<PlossRow> rows,
IReadOnlyDictionary<Guid, QfodAggregate> 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
};
}
/// <summary>
/// 累加分组内各行的场景 Qfod 聚合值。
/// 等价于原实现的 SelectMany(row => row.Scenario.QfodRecords):同一场景出现 k 次,其 Sum 与 Count 同倍放大,
/// 商保持不变,因此多场景分组时加权语义与原来完全一致,但无需加载 Qfod 明细。
/// </summary>
private static (double Sum, double BaseSum, int Count) AggregateQfod(
List<PlossRow> rows,
IReadOnlyDictionary<Guid, QfodAggregate> 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<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);
private readonly record struct QfodAggregate(double QValueSum, double QBaseSum, int Count);
}
public record AggregationReport(int ReleaseRecordCount, int LocalRecordCount);