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) { } public record Curve(Long zeroTime, List> records) { } public record Item(Candidate source, List cycleNos, List> cycles, Map> metrics, List businessFields, Map> chargeSteps, Map curves, Map> stepCurves) { } public record Matrix(List items, List cycleNos) { } public record Summary(long id, String name, String creator, long createdAt, String testName, String batteryGroup) { } /** 列表来源名称取自已保存版本,测试删除后仍保留筛选与识别信息。 */ public List list() { return reports.list().stream().map(summary -> { var sources = items(JSON.parseObject(reports.get(summary.id()).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())); }).toList(); } /** 只提供类型名称精确为“化成”的检测完成测试及其真实参与电池。 */ public List sources() { TTest query = new TTest(); query.setTestStatus("COMPLETED"); List 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 selections) { if (selections == null || selections.isEmpty() || selections.contains(null) || new HashSet<>(selections).size() != selections.size()) throw new ServiceException("请选择不重复的化成测试电池"); Map loaded = new HashMap<>(); Map>> cycles = new HashMap<>(), records = new HashMap<>(), steps = new HashMap<>(); List 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> 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 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(); 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("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 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 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); 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 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 allowed = valueKeys(settings.fields(), matrix.items()); Map values = new LinkedHashMap<>(); settings.values().forEach((key, value) -> { if (allowed.contains(key)) values.put(key, value); }); snapshot.put("items", matrix.items()); snapshot.put("cycleNos", matrix.cycleNos()); snapshot.put("settings", 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())); 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 validateFields(List fields, boolean resolveDefinitions) { if (fields == null || fields.isEmpty() || fields.size() > 100 || fields.contains(null)) throw new ServiceException("化成字段不能为空"); var names = new HashSet(); var ids = new HashSet(); var current = resolveDefinitions ? definitions.selectBatteryGroupFieldList(new TBatteryGroupField()) : List.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", "拐点F克容量").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 fields) { Set 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 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 -> 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 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 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 || (override.experimentName() != null && override.experimentName().length() > 500)) throw new ServiceException("详情调整无效"); if (override.fieldValues() != null && override.fieldValues().entrySet().stream().anyMatch(entry -> entry.getKey() == null || !entry.getKey().matches("(?:battery|special)\\|.{1,100}") || entry.getValue() == null || entry.getValue().length() > 500)) throw new ServiceException("详情字段名称或值无效"); } } for (Item item : items) if (item.stepCurves() != null) item.stepCurves().forEach((cycle, ranges) -> ranges.keySet().forEach(step -> sources.add(sourceKey(item) + "|" + cycle + "|" + step))); 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("人工值不属于当前字段、测试电池或实际循环"); } private void curveUnits(Map 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 curves(List> rows, List cycles) { Map 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> stepCurves(List> rows, List cycles) { Map> result = new LinkedHashMap<>(); for (int cycle : cycles) { Map 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 items, boolean refresh) { Map> manual = new LinkedHashMap<>(); Map selected = new LinkedHashMap<>(); Set allowed = new HashSet<>(); for (Item item : items) for (int cycle : item.cycleNos()) { String key = sourceKey(item) + "|" + cycle; allowed.add(key); var steps = item.chargeSteps().getOrDefault(cycle, List.of()); var unknown = steps.stream().filter(step -> step.direction() == Direction.UNKNOWN).map(ChargeStep::stepNo).toList(); var requested = settings.manualChargeSteps() == null ? List.of() : settings.manualChargeSteps().getOrDefault(key, List.of()); if (refresh) requested = requested.stream().filter(unknown::contains).toList(); if (requested == null || !unknown.containsAll(requested) || new HashSet<>(requested).size() != requested.size()) throw new ServiceException("人工充电工步不属于当前测试电池、循环或未知类型"); manual.put(key, requested); var charge = steps.stream().filter(step -> step.direction() == Direction.CHARGE).map(ChargeStep::stepNo).toList(); var discharge = steps.stream().filter(step -> step.direction() == Direction.DISCHARGE).map(ChargeStep::stepNo).toList(); var chargeAllowed = new HashSet<>(charge); chargeAllowed.addAll(unknown); var dischargeAllowed = new HashSet<>(discharge); dischargeAllowed.addAll(unknown); var choice = settings.curveStepSelections() == null ? null : settings.curveStepSelections().get(key); if (choice == null) choice = new FormationReportRequest.StepSelection(charge.isEmpty() ? requested : charge, discharge); if (refresh) choice = new FormationReportRequest.StepSelection(choice.charge().stream().filter(chargeAllowed::contains).toList(), choice.discharge().stream().filter(dischargeAllowed::contains).toList()); if (choice.charge() == null || choice.discharge() == null || !chargeAllowed.containsAll(choice.charge()) || !dischargeAllowed.containsAll(choice.discharge()) || new HashSet<>(choice.charge()).size() != choice.charge().size() || new HashSet<>(choice.discharge()).size() != choice.discharge().size()) throw new ServiceException("曲线工步不属于当前测试电池、循环或充放电分组"); if (choice.charge().stream().anyMatch(choice.discharge()::contains)) throw new ServiceException("同一实际工步不能同时指定为充电和放电"); selected.put(key, choice); } if (!refresh && ((settings.manualChargeSteps() != null && !allowed.containsAll(settings.manualChargeSteps().keySet())) || (settings.curveStepSelections() != null && !allowed.containsAll(settings.curveStepSelections().keySet())))) throw new ServiceException("工步选择不属于所选测试电池和循环"); 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 series = new HashSet<>(); for (Item item : items) { series.add(sourceKey(item)); item.stepCurves().forEach((cycle, ranges) -> ranges.keySet().forEach(step -> series.add(sourceKey(item) + "|" + cycle + "|" + step))); } var colors = new LinkedHashMap<>(settings.seriesColors()); var hidden = settings.hiddenSeriesKeys() == null ? List.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, manual, selected, hidden, active, settings.sourceDetailOverrides()); } private void validateChargeSelections(Settings settings, List items) { if (items.stream().allMatch(item -> item.stepCurves() != null)) return; Map 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 choices = item.chargeSteps() == null ? List.of() : item.chargeSteps().getOrDefault(cycle, List.of()); Integer selected = selections.get(key); if (choices.stream().filter(step -> isReliableCharge(step.stepType())).count() == 1) { int expected = choices.stream().filter(step -> isReliableCharge(step.stepType())).findFirst().get().stepNo(); if (selected != null && selected != expected) throw new ServiceException("充电工步选择与实际工步不一致"); } else if (!choices.isEmpty()) { if (selected == null) throw new ServiceException("请为无法自动识别的充电工步完成选择"); if (choices.stream().noneMatch(step -> step.stepNo() == selected)) throw new ServiceException("所选充电工步不属于当前测试、电池和循环"); } } } private Map> chargeSteps(List> rows, List> realtime, TTestBattery battery, List cycles) { Map> result = new LinkedHashMap<>(); for (int cycle : cycles) { Map 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))); } 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))); } result.put(cycle, new ArrayList<>(found.values())); } return result; } private boolean isReliableCharge(String type) { return directions.resolve(type) == Direction.CHARGE; } 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 valueKeys(List fields, List items) { Set 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> metrics(List> rows, List cycleNos) { Map> result = new LinkedHashMap<>(); for (int no : cycleNos) { var matches = rows.stream().filter(row -> cycleNo(row) == no).toList(); Map 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 "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> 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> batteryRows(List> rows, TTestBattery battery) { return rows.stream().filter(row -> Objects.equals(battery.getBatteryNo(), row.get("batteryNo"))).toList(); } private int cycleNo(Map row) { return row.get("cycleNo") instanceof Number number ? number.intValue() : 0; } private List 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); } }