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.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.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 ITTestService tests; private final ITestDataQueryService data; private final FormationReportRepository reports; private final ITBatteryGroupFieldService definitions; public FormationReportService(ITTestService tests, ITestDataQueryService data, FormationReportRepository reports, ITBatteryGroupFieldService definitions) { 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) { } public record Item(Candidate source, List cycleNos, List> cycles, Map> metrics, List businessFields) { } 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 actual = Stream.of(rows, batteryRows(records.computeIfAbsent(selection.testId(), data::records), battery), 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())); } 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()); Matrix matrix = freeze(preview(request.selections()), settings.fields()); validateValues(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"); 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()); validateFields(request.settings().fields(), false); validateValues(request.settings(), items(snapshot)); snapshot.put("name", request.name().trim()); snapshot.put("description", request.description()); snapshot.put("settings", request.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(settings.fields(), values)); 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())) return field; 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()); }).toList(); return new Matrix(frozen, matrix.cycleNos()); } private void validateValues(Settings settings, List items) { 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 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"); 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()); } 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); } private String json(Object value) { return JSON.toJSONString(value, JSONWriter.Feature.WriteMapNullValue); } }