package com.battery.business.report;
|
|
import java.math.BigDecimal;
|
import java.math.MathContext;
|
import java.util.ArrayList;
|
import java.util.HashMap;
|
import java.util.HashSet;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.Set;
|
import java.util.stream.Stream;
|
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONWriter;
|
import com.battery.device.api.StepDirectionResolver;
|
import com.battery.device.api.StepDirectionResolver.Direction;
|
import com.battery.business.domain.CurrentBatteryField;
|
import com.battery.business.domain.TBatteryGroupField;
|
import com.battery.business.domain.TTest;
|
import com.battery.business.domain.TTestBattery;
|
import com.battery.business.domain.TTestConsoleDetail;
|
import com.battery.business.report.FormationReportRequest.Selection;
|
import com.battery.business.report.FormationReportRequest.Settings;
|
import com.battery.business.service.ITBatteryGroupFieldService;
|
import com.battery.business.service.ITTestService;
|
import com.battery.business.service.ITestDataQueryService;
|
import com.ruoyi.common.exception.ServiceException;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
/** 化成按实际循环并集展示;来源读取仅发生在预览、首次保存及主动更新。 */
|
@Service
|
public class FormationReportService
|
{
|
private final StepDirectionResolver directions;
|
private final ITTestService tests;
|
private final ITestDataQueryService data;
|
private final FormationReportRepository reports;
|
private final ITBatteryGroupFieldService definitions;
|
|
public FormationReportService(ITTestService tests, ITestDataQueryService data,
|
FormationReportRepository reports, ITBatteryGroupFieldService definitions, StepDirectionResolver directions)
|
{ this.directions = directions; this.tests = tests; this.data = data; this.reports = reports; this.definitions = definitions; }
|
|
public record Candidate(Selection selection, String name, String type, long groupId, String batteryGroup, String batteryNo, TTestConsoleDetail consoleDetail) { }
|
public record ChargeStep(int stepNo, String name, String stepType, BigDecimal endVoltage, Direction direction, BigDecimal capacity) { }
|
public record Curve(Long zeroTime, List<Map<String, Object>> records) { }
|
public record Item(Candidate source, List<Integer> cycleNos, List<Map<String, Object>> cycles,
|
Map<String, Map<String, BigDecimal>> metrics, List<CurrentBatteryField> businessFields,
|
Map<Integer, List<ChargeStep>> chargeSteps, Map<Integer, Curve> curves, Map<Integer, Map<Integer, Curve>> stepCurves) { }
|
public record Matrix(List<Item> items, List<Integer> cycleNos) { }
|
public record Summary(long id, String name, String creator, long createdAt, String testName, String batteryGroup, int versionNo) { }
|
|
/** 列表来源名称取自已保存版本,测试删除后仍保留筛选与识别信息。 */
|
public List<Summary> list()
|
{
|
return reports.list().stream().map(summary -> {
|
var version = reports.get(summary.id());
|
var sources = items(JSON.parseObject(version.snapshotJson()));
|
return new Summary(summary.id(), summary.name(), summary.creator(), summary.createdAt(),
|
String.join("、", sources.stream().map(item -> item.source().name()).distinct().toList()),
|
String.join("、", sources.stream().map(item -> item.source().batteryGroup()).distinct().toList()), version.versionNo());
|
}).toList();
|
}
|
|
/** 只提供类型名称精确为“化成”的检测完成测试及其真实参与电池。 */
|
public List<Candidate> sources()
|
{
|
TTest query = new TTest(); query.setTestStatus("COMPLETED");
|
List<Candidate> result = new ArrayList<>();
|
for (TTest summary : tests.selectTestList(query))
|
{
|
if (!"化成".equals(summary.getTestTypeName())) continue;
|
TTest test = tests.selectTestById(summary.getTestId());
|
if (eligible(test)) for (TTestBattery battery : test.getBatteries()) result.add(candidate(test, battery));
|
}
|
return result;
|
}
|
|
/** 校验电池属于所选测试,返回三类持久化记录实际出现的循环号与循环统计。 */
|
public Matrix preview(List<Selection> selections)
|
{
|
if (selections == null || selections.isEmpty() || selections.contains(null)
|
|| new HashSet<>(selections).size() != selections.size()) throw new ServiceException("请选择不重复的化成测试电池");
|
Map<Long, TTest> loaded = new HashMap<>();
|
Map<Long, List<Map<String, Object>>> cycles = new HashMap<>(), records = new HashMap<>(), steps = new HashMap<>();
|
List<Item> items = new ArrayList<>();
|
for (Selection selection : selections)
|
{
|
if (selection.testId() == null || selection.testBatteryId() == null) throw new ServiceException("测试和电池身份不能为空");
|
TTest test = loaded.computeIfAbsent(selection.testId(), tests::selectTestById);
|
if (!eligible(test)) throw new ServiceException("仅可选择类型为化成的检测完成测试");
|
TTestBattery battery = test.getBatteries().stream().filter(row -> selection.testBatteryId().equals(row.getTestBatteryId()))
|
.findFirst().orElseThrow(() -> new ServiceException("电池不属于所选测试"));
|
List<Map<String, Object>> rows = batteryRows(cycles.computeIfAbsent(selection.testId(), data::cycles), battery);
|
var realtime = batteryRows(records.computeIfAbsent(selection.testId(), data::records), battery);
|
var actual = Stream.of(rows, realtime,
|
batteryRows(steps.computeIfAbsent(selection.testId(), data::steps), battery)).flatMap(List::stream)
|
.map(this::cycleNo).filter(no -> no > 0).distinct().sorted().toList();
|
items.add(new Item(candidate(test, battery), actual, rows, metrics(rows, actual), battery.getFields(), chargeSteps(steps.get(selection.testId()), realtime, battery, actual), curves(realtime, actual), stepCurves(realtime, actual)));
|
}
|
return new Matrix(items, items.stream().flatMap(item -> item.cycleNos().stream()).distinct().sorted().toList());
|
}
|
|
/** 从当前来源冻结所选字段;首版和报表身份在同一事务提交。 */
|
@Transactional(rollbackFor = Exception.class)
|
public Map<String, Object> create(FormationReportRequest request, long userId, String userName)
|
{
|
Settings settings = new Settings(validateFields(request.settings().fields(), true), request.settings().values(),
|
request.settings().chargeStepSelections() == null ? Map.of() : request.settings().chargeStepSelections(),
|
request.settings().metrics(), request.settings().seriesColors(), request.settings().manualChargeSteps(),
|
request.settings().curveStepSelections(), request.settings().hiddenSeriesKeys(), request.settings().activeCycle(), request.settings().sourceDetailOverrides());
|
Matrix matrix = freeze(preview(request.selections()), settings.fields());
|
settings = stepSettings(settings, matrix.items(), false);
|
validateValues(settings, matrix.items()); validateChargeSelections(settings, matrix.items());
|
var snapshot = new LinkedHashMap<String, Object>();
|
snapshot.put("curveMode", "actual-steps");
|
snapshot.put("name", request.name().trim()); snapshot.put("description", request.description());
|
snapshot.put("items", matrix.items()); snapshot.put("cycleNos", matrix.cycleNos()); snapshot.put("settings", settings);
|
snapshot.put("calculatedValues", FormationReportCalculations.calculate(matrix.items(), settings));
|
snapshot.put("units", Map.of("chargeCapacity", "Ah", "dischargeCapacity", "Ah", "chargeEnergy", "Wh", "dischargeEnergy", "Wh", "efficiency", "%", "energyEfficiency", "%"));
|
snapshot.put("displayUnits", FormationField.METRIC_UNITS); snapshot.put("timeUnit", "epoch-milliseconds");
|
curveUnits(snapshot);
|
return get(reports.insert(request.name().trim(), json(snapshot), userId, userName, System.currentTimeMillis()), null);
|
}
|
|
/** 当前或历史版本只读取独立快照,来源删除后仍能重开。 */
|
public Map<String, Object> get(long id, Integer versionNo)
|
{
|
var version = reports.get(id, versionNo);
|
var result = JSON.parseObject(version.snapshotJson());
|
result.put("id", id); result.put("versionNo", version.versionNo());
|
result.put("creator", version.report().creator()); result.put("createdAt", version.report().createdAt());
|
result.put("operatorName", version.operatorName()); result.put("versionCreatedAt", version.versionCreatedAt());
|
result.put("changeSummary", version.changeSummary());
|
return result;
|
}
|
|
/** 仅追加报表展示内容;共享定义或来源已删除也不影响基于快照的普通编辑。 */
|
@Transactional(rollbackFor = Exception.class)
|
public Map<String, Object> edit(long id, FormationReportRequest.Edit request, long userId, String userName)
|
{
|
var snapshot = current(id, request.baseVersionNo());
|
var submitted = request.settings();
|
var settings = new Settings(validateFields(submitted.fields(), false), submitted.values(), submitted.chargeStepSelections(),
|
submitted.metrics(), submitted.seriesColors(), submitted.manualChargeSteps(), submitted.curveStepSelections(),
|
submitted.hiddenSeriesKeys(), submitted.activeCycle(), submitted.sourceDetailOverrides());
|
if ("actual-steps".equals(snapshot.getString("curveMode"))) settings = stepSettings(settings, items(snapshot), false);
|
validateValues(settings, items(snapshot)); validateChargeSelections(settings, items(snapshot));
|
snapshot.put("name", request.name().trim()); snapshot.put("description", request.description()); snapshot.put("settings", settings);
|
snapshot.put("calculatedValues", FormationReportCalculations.calculate(items(snapshot), settings));
|
reports.append(id, request.baseVersionNo() + 1, request.name().trim(), json(snapshot), userId, userName, request.changeSummary().trim());
|
return get(id, null);
|
}
|
|
/** 主动重读已保存范围,保留仍属于当前循环的人工值;不存在的来源使整次提交失败。 */
|
@Transactional(rollbackFor = Exception.class)
|
public Map<String, Object> refresh(long id, CompareReportEditRequest.Refresh request, long userId, String userName)
|
{
|
var snapshot = current(id, request.baseVersionNo());
|
var settings = JSON.parseObject(json(snapshot.get("settings")), Settings.class);
|
Matrix matrix = freeze(preview(items(snapshot).stream().map(item -> item.source().selection()).toList()), settings.fields());
|
Set<String> allowed = valueKeys(settings.fields(), matrix.items());
|
Map<String, String> values = new LinkedHashMap<>();
|
settings.values().forEach((key, value) -> { if (allowed.contains(key)) values.put(key, value); });
|
var refreshedSettings = new Settings(validateFields(settings.fields(), false), values,
|
settings.chargeStepSelections() == null ? Map.of() : settings.chargeStepSelections(), settings.metrics(), settings.seriesColors(),
|
settings.manualChargeSteps(), settings.curveStepSelections(), settings.hiddenSeriesKeys(), settings.activeCycle(), settings.sourceDetailOverrides());
|
snapshot.put("curveMode", "actual-steps");
|
snapshot.put("items", matrix.items()); snapshot.put("cycleNos", matrix.cycleNos());
|
snapshot.put("settings", stepSettings(refreshedSettings, matrix.items(), true));
|
validateValues(refreshedSettings, matrix.items());
|
snapshot.put("calculatedValues", FormationReportCalculations.calculate(matrix.items(), refreshedSettings));
|
curveUnits(snapshot);
|
reports.append(id, request.baseVersionNo() + 1, snapshot.getString("name"), json(snapshot), userId, userName, "按最新来源生成:" + request.changeSummary().trim());
|
return get(id, null);
|
}
|
|
/** 删除全部化成版本,与最小删除日志在同一事务提交,不删除测试事实。 */
|
@Transactional(rollbackFor = Exception.class)
|
public void delete(long id, long userId, String userName) { reports.lock(id); reports.delete(id, userId, userName); }
|
|
/** 校验有序结构;首次保存及模板解析共享身份,普通编辑沿用冻结身份。 */
|
public List<FormationField> validateFields(List<FormationField> fields, boolean resolveDefinitions)
|
{
|
if (fields == null || fields.isEmpty() || fields.size() > 100 || fields.contains(null)) throw new ServiceException("化成字段不能为空");
|
var names = new HashSet<String>(); var ids = new HashSet<Long>();
|
var current = resolveDefinitions ? definitions.selectBatteryGroupFieldList(new TBatteryGroupField()) : List.<TBatteryGroupField>of();
|
return fields.stream().map(field -> {
|
if (!names.add(field.scope() + "|" + field.key())) throw new ServiceException("化成字段不能重复");
|
if (!"battery".equals(field.kind())) {
|
// 仅新生成的字段结构纠正旧误写,读取历史快照时不改名。
|
String corrected = Map.of("chargePerGram", "每克N充电量", "chargePerGramTotal", "每克F充电量",
|
"capacity1v", "1.0V容量", "capacity1vf", "1.0VF完整容量", "dischargeInflection", "拐点容量",
|
"dischargeInflectionTotal", "拐点完整容量").get(field.key());
|
return corrected == null ? field : new FormationField(field.key(), field.scope(), field.kind(), corrected, field.unit(), field.fieldDefinitionId());
|
}
|
Long id = field.fieldDefinitionId();
|
if (resolveDefinitions)
|
{
|
var definition = current.stream().filter(value -> field.fieldDefinitionId() == null
|
? value.getFieldName().equals(field.key()) : value.getFieldId().equals(field.fieldDefinitionId())).findFirst();
|
if (definition.isEmpty() && !Set.of("Z", "H", "F", "A", "B").contains(field.key()))
|
throw new ServiceException("共享字段不存在,请重新选择:" + field.key());
|
if (definition.isPresent()) id = definition.get().getFieldId();
|
}
|
if (id != null && !ids.add(id)) throw new ServiceException("共享字段不能重复");
|
return new FormationField(field.key(), field.scope(), field.kind(), field.displayName(), field.unit(), id);
|
}).toList();
|
}
|
|
private JSONObject current(long id, int baseVersionNo)
|
{
|
// 与删除共用报表身份锁;旧页面只能重开,不能从陈旧基准覆盖新版本。
|
reports.lock(id);
|
var version = reports.get(id);
|
if (version.versionNo() != baseVersionNo) throw new ServiceException("报表已有新版本,请重新打开后编辑");
|
return JSON.parseObject(version.snapshotJson());
|
}
|
|
private Matrix freeze(Matrix matrix, List<FormationField> fields)
|
{
|
Set<String> selected = new HashSet<>(fields.stream().filter(field -> "metric".equals(field.kind())).map(FormationField::key).toList());
|
// 能量效率依赖两项原始能量,即使未展示它们也须冻结,保证快照可复算。
|
if (selected.contains("energyEfficiency")) selected.addAll(List.of("chargeEnergy", "dischargeEnergy"));
|
var frozen = matrix.items().stream().map(item -> {
|
var rows = item.cycles().stream().map(row -> {
|
Map<String, Object> copy = new LinkedHashMap<>(row);
|
FormationField.METRIC_UNITS.keySet().stream().filter(key -> !selected.contains(key)).forEach(copy::remove);
|
return copy;
|
}).toList();
|
return new Item(item.source(), item.cycleNos(), rows, metrics(rows, item.cycleNos()), item.businessFields().stream()
|
.filter(value -> FormationReportCalculations.INPUTS.contains(value.getFieldName()) || fields.stream().anyMatch(field -> "battery".equals(field.kind()) && (field.fieldDefinitionId() == null
|
? field.key().equals(value.getFieldName()) : field.fieldDefinitionId().equals(value.getFieldDefinitionId())))).toList(), item.chargeSteps(), item.curves(), item.stepCurves());
|
}).toList();
|
return new Matrix(frozen, matrix.cycleNos());
|
}
|
|
private void validateValues(Settings settings, List<Item> items)
|
{
|
if (settings.metrics().stream().anyMatch(value -> value == null || !Set.of("voltage", "current", "positiveVoltage", "negativeVoltage", "temperature").contains(value))
|
|| new HashSet<>(settings.metrics()).size() != settings.metrics().size()) throw new ServiceException("化成曲线指标无效或重复");
|
Set<String> sources = new HashSet<>(items.stream().map(item -> sourceKey(item)).toList());
|
if (settings.sourceDetailOverrides() != null) {
|
// 详情只接受测试电池身份;曲线工步身份不能作为详情列提交。
|
if (!sources.containsAll(settings.sourceDetailOverrides().keySet())) throw new ServiceException("详情调整不属于所选测试电池");
|
for (var override : settings.sourceDetailOverrides().values()) {
|
if (override == null) throw new ServiceException("详情调整无效");
|
override.validate();
|
}
|
}
|
for (Item item : items) if (item.stepCurves() != null) item.chargeSteps().forEach((cycle, ranges) -> ranges.forEach(step -> sources.add(sourceKey(item) + "|" + cycle + "|" + step.stepNo())));
|
if (settings.hiddenSeriesKeys() != null && !sources.containsAll(settings.hiddenSeriesKeys())) throw new ServiceException("隐藏曲线不属于当前来源工步");
|
if (!sources.containsAll(settings.seriesColors().keySet()) || settings.seriesColors().values().stream()
|
.anyMatch(color -> color == null || !color.matches("#[0-9a-fA-F]{6}"))) throw new ServiceException("化成曲线颜色或来源无效");
|
if (settings.values() == null || !valueKeys(settings.fields(), items).containsAll(settings.values().keySet())
|
|| settings.values().values().stream().anyMatch(value -> value == null || value.length() > 500))
|
throw new ServiceException("人工值不属于当前字段、测试电池或实际循环");
|
for (var entry : settings.values().entrySet()) {
|
String key = entry.getKey().substring(entry.getKey().lastIndexOf('|') + 1);
|
if (FormationReportCalculations.RESULTS.contains(key)) throw new ServiceException("自动计算结果不能人工覆盖");
|
if ("manual".equals(key) && !entry.getValue().isBlank()
|
&& !entry.getValue().trim().matches("(?:\\d+(?:\\.\\d*)?|\\.\\d+)"))
|
throw new ServiceException("数字仅允许非负整数或小数");
|
}
|
}
|
|
private void curveUnits(Map<String, Object> snapshot)
|
{
|
for (String key : List.of("units", "displayUnits"))
|
{
|
var units = new LinkedHashMap<>(JSON.parseObject(json(snapshot.get(key))));
|
units.putAll(Map.of("voltage", "V", "current", "units".equals(key) ? "A" : "mA",
|
"positiveVoltage", "V", "negativeVoltage", "V", "temperature", "℃"));
|
snapshot.put(key, units);
|
}
|
}
|
|
private Map<Integer, Curve> curves(List<Map<String, Object>> rows, List<Integer> cycles)
|
{
|
Map<Integer, Curve> result = new LinkedHashMap<>();
|
for (int cycle : cycles)
|
{
|
var selected = rows.stream().filter(row -> cycleNo(row) == cycle).toList();
|
// 每测试、电池、循环共享最早设备时间零点;柜与采集器仍保留各自时间,显隐不裁剪记录。
|
Long zero = selected.stream().flatMap(row -> Stream.of(row.get("deviceTime"), row.get("collectorDeviceTime")))
|
.filter(Number.class::isInstance).map(value -> ((Number) value).longValue()).min(Long::compareTo).orElse(null);
|
result.put(cycle, new Curve(zero, selected));
|
}
|
return result;
|
}
|
|
private String sourceKey(Item item) { return item.source().selection().testId() + "|" + item.source().selection().testBatteryId(); }
|
|
private Map<Integer, Map<Integer, Curve>> stepCurves(List<Map<String, Object>> rows, List<Integer> cycles)
|
{
|
Map<Integer, Map<Integer, Curve>> result = new LinkedHashMap<>();
|
for (int cycle : cycles) {
|
Map<Integer, Curve> ranges = new LinkedHashMap<>();
|
rows.stream().filter(row -> cycleNo(row) == cycle).map(row -> number(row.get("stepNo"))).filter(no -> no > 0).distinct().sorted().forEach(step -> {
|
var selected = rows.stream().filter(row -> cycleNo(row) == cycle && number(row.get("stepNo")) == step).toList();
|
// 每实际工步按最早柜/采集器时间归零,显隐及选组均不改变冻结点。
|
ranges.put(step, curves(selected, List.of(cycle)).get(cycle));
|
});
|
result.put(cycle, ranges);
|
}
|
return result;
|
}
|
|
private Settings stepSettings(Settings settings, List<Item> items, boolean refresh)
|
{
|
// 化成自动选择全部可靠实际工步,忽略旧展示设置中的人工分组和工步子集。
|
Map<String, FormationReportRequest.StepSelection> selected = new LinkedHashMap<>();
|
for (Item item : items) for (int cycle : item.cycleNos()) {
|
var steps = item.chargeSteps().getOrDefault(cycle, List.of());
|
selected.put(sourceKey(item) + "|" + cycle, new FormationReportRequest.StepSelection(
|
steps.stream().filter(step -> step.direction() == Direction.CHARGE).map(ChargeStep::stepNo).toList(),
|
steps.stream().filter(step -> step.direction() == Direction.DISCHARGE).map(ChargeStep::stepNo).toList()));
|
}
|
var cycles = items.stream().flatMap(item -> item.cycleNos().stream()).distinct().sorted().toList();
|
Integer active = settings.activeCycle();
|
if (active != null && !cycles.contains(active) && !refresh) throw new ServiceException("曲线循环不属于当前报表");
|
if (active == null || !cycles.contains(active)) active = cycles.isEmpty() ? null : cycles.get(0);
|
Set<String> series = new HashSet<>();
|
for (Item item : items) { series.add(sourceKey(item)); item.chargeSteps().forEach((cycle, ranges) -> ranges.forEach(step -> series.add(sourceKey(item) + "|" + cycle + "|" + step.stepNo()))); }
|
var colors = new LinkedHashMap<>(settings.seriesColors());
|
var hidden = settings.hiddenSeriesKeys() == null ? List.<String>of() : settings.hiddenSeriesKeys();
|
if (refresh) { colors.keySet().retainAll(series); hidden = hidden.stream().filter(series::contains).toList(); }
|
return new Settings(settings.fields(), settings.values(), settings.chargeStepSelections(), settings.metrics(), colors, Map.of(), selected, hidden, active, settings.sourceDetailOverrides());
|
}
|
|
private void validateChargeSelections(Settings settings, List<Item> items)
|
{
|
if (items.stream().allMatch(item -> item.stepCurves() != null)) return;
|
Map<String, Integer> selections = settings.chargeStepSelections() == null ? Map.of() : settings.chargeStepSelections();
|
for (Item item : items) for (int cycle : item.cycleNos()) {
|
String key = item.source().selection().testId() + "|" + item.source().selection().testBatteryId() + "|" + cycle;
|
// 早期版本未冻结工步时保持缺失,普通编辑不能重新读取设备事实。
|
List<ChargeStep> choices = item.chargeSteps() == null ? List.of() : item.chargeSteps().getOrDefault(cycle, List.of());
|
Integer selected = selections.get(key);
|
// 旧版保留冻结的单选事实,不用新增协议识别规则改判历史选择。
|
if (selected != null && choices.stream().noneMatch(step -> step.stepNo() == selected))
|
throw new ServiceException("所选充电工步不属于当前测试、电池和循环");
|
}
|
}
|
|
private Map<Integer, List<ChargeStep>> chargeSteps(List<Map<String, Object>> rows, List<Map<String, Object>> realtime, TTestBattery battery, List<Integer> cycles)
|
{
|
Map<Integer, List<ChargeStep>> result = new LinkedHashMap<>();
|
for (int cycle : cycles) {
|
Map<Integer, ChargeStep> found = new java.util.TreeMap<>();
|
// 实时记录只补充实际范围身份,不代替缺失的工步终止电压。
|
for (var row : realtime) if (cycleNo(row) == cycle && number(row.get("stepNo")) > 0) {
|
String type = String.valueOf(row.getOrDefault("stepType", ""));
|
found.putIfAbsent(number(row.get("stepNo")), new ChargeStep(number(row.get("stepNo")), String.valueOf(row.getOrDefault("stepName", "")), type, null, directions.resolve(type), null));
|
}
|
if (rows != null) for (var row : rows) if (Objects.equals(battery.getBatteryNo(), row.get("batteryNo")) && cycleNo(row) == cycle && number(row.get("stepNo")) > 0) {
|
String type = String.valueOf(row.getOrDefault("stepType", ""));
|
found.put(number(row.get("stepNo")), new ChargeStep(number(row.get("stepNo")), String.valueOf(row.getOrDefault("stepName", "")), type, decimal(row.get("endVoltage")), directions.resolve(type), decimal(row.get("capacity"))));
|
}
|
result.put(cycle, new ArrayList<>(found.values()));
|
}
|
return result;
|
}
|
private int number(Object value) { return value instanceof Number n ? n.intValue() : 0; }
|
private BigDecimal decimal(Object value) { return value instanceof Number n ? new BigDecimal(n.toString()) : null; }
|
|
private Set<String> valueKeys(List<FormationField> fields, List<Item> items)
|
{
|
Set<String> keys = new HashSet<>();
|
for (Item item : items) for (FormationField field : fields)
|
{
|
String prefix = item.source().selection().testId() + "|" + item.source().selection().testBatteryId() + "|";
|
for (int cycle : "detail".equals(field.scope()) ? List.of(0) : item.cycleNos()) keys.add(prefix + cycle + "|" + field.key());
|
}
|
return keys;
|
}
|
|
private Map<String, Map<String, BigDecimal>> metrics(List<Map<String, Object>> rows, List<Integer> cycleNos)
|
{
|
Map<String, Map<String, BigDecimal>> result = new LinkedHashMap<>();
|
for (int no : cycleNos)
|
{
|
var matches = rows.stream().filter(row -> cycleNo(row) == no).toList();
|
Map<String, BigDecimal> values = new LinkedHashMap<>();
|
// 多条统计记录没有已确认的合并口径,保持缺失,不挑任意一条或借用其他循环。
|
if (matches.size() == 1) for (String key : FormationField.METRIC_UNITS.keySet())
|
if (matches.get(0).get(key) instanceof Number number) values.put(key, new BigDecimal(number.toString()));
|
BigDecimal charge = values.get("chargeEnergy"), discharge = values.get("dischargeEnergy");
|
// 分母必须为正;允许结果超过 100%,不截断计算结果。
|
if (charge != null && discharge != null && charge.signum() > 0)
|
values.put("energyEfficiency", discharge.multiply(BigDecimal.valueOf(100)).divide(charge, MathContext.DECIMAL128));
|
result.put(Integer.toString(no), values);
|
}
|
return result;
|
}
|
|
private boolean eligible(TTest test) { return test != null && "COMPLETED".equals(test.getTestStatus()) && "化成".equals(test.getTestTypeName()); }
|
private Candidate candidate(TTest test, TTestBattery battery)
|
{ return new Candidate(new Selection(test.getTestId(), battery.getTestBatteryId()), test.getTestName(), test.getTestTypeName(), battery.getGroupId(), battery.getGroupName(), battery.getBatteryNo(), consoleDetail(test, battery)); }
|
private TTestConsoleDetail consoleDetail(TTest test, TTestBattery battery)
|
{
|
var original = test.getConsoleDetail();
|
if (original == null) return null;
|
var result = new TTestConsoleDetail();
|
result.setExperimentName(original.getExperimentName()); result.setBatteryFields(original.getBatteryFields()); result.setSpecialFields(original.getSpecialFields());
|
// 只冻结所选电池的人工详情,同名字段不推断共享身份或业务单位。
|
Map<String, Map<String, String>> values = new LinkedHashMap<>();
|
original.getFieldValues().forEach((field, byBattery) -> {
|
if (byBattery.containsKey(battery.getBatteryNo())) values.put(field, Map.of(battery.getBatteryNo(), byBattery.get(battery.getBatteryNo())));
|
});
|
result.setFieldValues(values); return result;
|
}
|
private List<Map<String, Object>> batteryRows(List<Map<String, Object>> rows, TTestBattery battery)
|
{ return rows.stream().filter(row -> Objects.equals(battery.getBatteryNo(), row.get("batteryNo"))).toList(); }
|
private int cycleNo(Map<String, Object> row) { return row.get("cycleNo") instanceof Number number ? number.intValue() : 0; }
|
private List<Item> items(JSONObject snapshot) { return JSON.parseArray(json(snapshot.get("items")), Item.class); }
|
// 循环号是整数键,存入数据库 JSON 时必须写成带引号的成员名。
|
private String json(Object value) { return JSON.toJSONString(value, JSONWriter.Feature.WriteMapNullValue, JSONWriter.Feature.WriteNonStringKeyAsString); }
|
}
|