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> 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, int versionNo) { } /** 列表来源名称取自已保存版本,测试删除后仍保留筛选与识别信息。 */ public List list() { return reports.listWithSnapshots().stream().map(summary -> { // 最新快照由一次列表 SQL 带出,避免每份报表再执行一次版本查询。 var sources = items(JSON.parseObject(summary.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()), summary.versionNo()); }).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), null)); } 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("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 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); 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 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); }); 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 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", "拐点完整容量").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 -> 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 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) 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 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 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 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.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 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 (selected != null && 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 = Objects.toString(row.get("stepType"), ""); String name = Objects.toString(row.get("stepName"), ""); found.putIfAbsent(number(row.get("stepNo")), new ChargeStep(number(row.get("stepNo")), name, type, null, stepDirection(type, name), 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 = Objects.toString(row.get("stepType"), ""); String name = Objects.toString(row.get("stepName"), ""); found.put(number(row.get("stepNo")), new ChargeStep(number(row.get("stepNo")), name, type, decimal(row.get("endVoltage")), stepDirection(type, name), 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 Direction stepDirection(String type, String name) { // 甲方 Excel 只提供工步名称;有协议类型时禁止用名称覆盖未知/混合类型。 if (!type.isBlank()) return directions.resolve(type); return switch (name.trim()) { case "恒流充电" -> Direction.CHARGE; case "恒流放电" -> Direction.DISCHARGE; case "搁置", "静置" -> Direction.REST; default -> Direction.UNKNOWN; }; } 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 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> 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); } }