package com.battery.business.report; import java.math.BigDecimal; import java.math.MathContext; import java.util.*; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.battery.business.domain.CurrentBatteryField; import com.battery.business.domain.TTest; import com.battery.business.domain.TTestBattery; 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 SpliceReportService { private final ITTestService tests; private final ITestDataQueryService data; private final SpliceReportRepository reports; private final SpliceFieldTemplateService templates; public SpliceReportService(ITTestService tests, ITestDataQueryService data, SpliceReportRepository reports, SpliceFieldTemplateService templates) { this.tests = tests; this.data = data; this.reports = reports; this.templates = templates; } public record Candidate(SpliceReportRequest.Selection selection,String name,String type,long groupId,String group,String batteryNo,long startedAt){} public record Matrix(List> segments,List cycleNos){} /** 仅返回检测完成测试的真实参与电池,默认按开始时间及测试身份排序。 */ public List sources() { TTest query = new TTest(); query.setTestStatus("COMPLETED"); List result = new ArrayList<>(); for (TTest summary : tests.selectTestList(query)) { TTest test = tests.selectTestById(summary.getTestId()); for (TTestBattery battery : test.getBatteries()) result.add(candidate(test, battery)); } return result.stream().sorted(Comparator.comparingLong(Candidate::startedAt) .thenComparingLong(value -> value.selection().testId())).toList(); } /** 校验每个测试电池来源并按提交顺序逐段接续真实循环。 */ public Matrix preview(List selections) { if (selections == null || selections.size() < 2 || selections.contains(null) || new HashSet<>(selections).size() != selections.size()) throw new ServiceException("请选择至少两个不重复的检测完成测试电池"); Map loaded = new LinkedHashMap<>(); Map selected = new LinkedHashMap<>(); for (var selection : selections) { if (selection.testId() == null || selection.testBatteryId() == null) throw new ServiceException("测试和电池身份不能为空"); TTest test = loaded.computeIfAbsent(selection.testId(), tests::selectTestById); if (test == null || !"COMPLETED".equals(test.getTestStatus())) throw new ServiceException("仅可选择检测完成测试"); TTestBattery battery = test.getBatteries().stream().filter(row -> selection.testBatteryId().equals(row.getTestBatteryId())) .findFirst().orElseThrow(() -> new ServiceException("电池不属于所选测试")); selected.put(selection, battery); } List> segments = new ArrayList<>(); Set mappedCycles = new TreeSet<>(); Integer previousEnd = null; for (var entry : selected.entrySet()) { long testId = entry.getKey().testId(); List> cycles = data.cycles(testId), steps = data.steps(testId), records = data.records(testId); List> current = new ArrayList<>(); Set sourceCycles = new TreeSet<>(); for (TTestBattery battery : List.of(entry.getValue())) { var rows = batteryRows(cycles, battery); var stepRows = batteryRows(steps, battery); var nos = java.util.stream.Stream.of(rows, stepRows, batteryRows(records, battery)).flatMap(List::stream) .map(this::cycle).filter(no -> no > 0).distinct().sorted().toList(); Map segment = new LinkedHashMap<>(); segment.put("source", candidate(loaded.get(testId), battery)); segment.put("cycles", rows); segment.put("steps", stepRows); segment.put("businessFields", battery.getFields()); segment.put("originalCycleNos", nos); segment.put("statistics", statistics(rows, stepRows, nos)); current.add(segment); sourceCycles.addAll(nos); } // 每个测试电池独立计算偏移;空段不占用循环,各指标共用同一定位。 int offset = previousEnd == null || sourceCycles.isEmpty() ? 0 : previousEnd + 1 - sourceCycles.iterator().next(); current.forEach(segment -> segment.put("offset", offset)); for (int no : sourceCycles) mappedCycles.add(no + offset); if (!sourceCycles.isEmpty()) previousEnd = Collections.max(sourceCycles) + offset; segments.addAll(current); } return new Matrix(segments, new ArrayList<>(mappedCycles)); } private List> batteryRows(List> rows, TTestBattery battery) { return rows.stream().filter(row -> Objects.equals(row.get("batteryNo"), battery.getBatteryNo())).toList(); } private Map statistics(List> rows, List> stepRows, List nos) { Map result = new LinkedHashMap<>(); for (int no : nos) { Map values = new LinkedHashMap<>(); var cycleRows = rows.stream().filter(row -> cycle(row) == no).toList(); if (cycleRows.size() == 1) { for (String key : List.of("chargeEnergy", "dischargeEnergy", "chargeCapacity", "dischargeCapacity")) if (cycleRows.get(0).get(key) != null) values.put(key, cycleRows.get(0).get(key)); Number charge = num(values.get("chargeEnergy")), discharge = num(values.get("dischargeEnergy")); // 两项能量沿用同一存储单位;非正分母留缺失,计算结果不截断到百分之百。 if (charge != null && discharge != null && charge.doubleValue() > 0) values.put("energyEfficiency", new BigDecimal(discharge.toString()).multiply(BigDecimal.valueOf(100)) .divide(new BigDecimal(charge.toString()), MathContext.DECIMAL128)); } // 查询接口按实际发生顺序返回工步;先选末记录再取值,空值不得向前回退。 var actualSteps = stepRows.stream().filter(row -> cycle(row) == no).toList(); if (!actualSteps.isEmpty()) values.put("finalVoltage1", actualSteps.get(actualSteps.size() - 1).get("endVoltage")); result.put(Integer.toString(no), values); } return result; } private Number num(Object value) { return value instanceof Number number ? number : null; } /** 首次保存时重新读取来源并冻结字段与计算原值,人工调整只写入显示设置。 */ @Transactional public Map create(SpliceReportRequest request, long uid, String user) { var settings = settings(request.settings(), true); Matrix matrix = freeze(preview(request.selections()), fields(settings)); validateStatistics(settings, matrix.segments()); Map snapshot = new LinkedHashMap<>(); snapshot.put("name", request.name().trim()); snapshot.put("description", request.description()); snapshot.put("selections", request.selections()); snapshot.put("segments", matrix.segments()); snapshot.put("cycleNos", matrix.cycleNos()); snapshot.put("settings", settings); return get(reports.insert(request.name().trim(), JSON.toJSONString(snapshot), uid, user, System.currentTimeMillis()), null); } /** 当前和历史版本只读独立快照,来源删除后仍可重开。 */ public Map get(long id, Integer version) { var saved = reports.get(id, version); Map result = JSON.parseObject(saved.snapshotJson()); result.put("id", id); result.put("versionNo", saved.versionNo()); result.put("creator", saved.report().creator()); result.put("createdAt", saved.report().createdAt()); result.put("operatorName", saved.operatorName()); result.put("versionCreatedAt", saved.versionCreatedAt()); result.put("changeSummary", saved.changeSummary()); return result; } /** 普通编辑只追加展示设置,保留原版本来源、计算原值及字段事实。 */ @Transactional public Map edit(long id, SpliceReportRequest.Edit request, long uid, String user) { var snapshot = current(id, request.baseVersionNo()); var settings = settings(request.settings(), false); validateStatistics(settings, segments(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(), snapshot.toJSONString(), uid, user, request.changeSummary().trim()); return get(id, null); } /** 主动按最新来源生成版本,沿用已保存选择及字段结构;来源失效则整次回滚。 */ @Transactional public Map refresh(long id, int base, String summary, long uid, String user) { if (summary == null || summary.isBlank() || summary.length() > 500) throw new ServiceException("请填写有效修改说明"); var snapshot = current(id, base); var selections = JSON.parseArray(snapshot.getString("selections"), SpliceReportRequest.Selection.class); var settings = settings(snapshot.getJSONObject("settings"), false); Matrix matrix = freeze(preview(selections), fields(settings)); // 人工值绑定原循环身份,刷新只清理已不存在的循环,不按新的拼接循环号迁移。 var overrides = JSON.parseObject(JSON.toJSONString(settings.getOrDefault("statisticValues", Map.of()))); overrides.keySet().retainAll(statisticKeys(matrix.segments())); settings.put("statisticValues", overrides); snapshot.put("settings", settings); snapshot.put("segments", matrix.segments()); snapshot.put("cycleNos", matrix.cycleNos()); reports.append(id, base + 1, snapshot.getString("name"), snapshot.toJSONString(), uid, user, "按最新来源生成:" + summary.trim()); return get(id, null); } /** 整份报表及全部版本与最小删除日志在同一事务中提交,日志失败时保留报表。 */ @Transactional public void delete(long id, long uid, String user) { reports.lock(id); reports.delete(id, uid, user); } private JSONObject current(long id, int base) { // 与删除共用身份锁,拒绝从陈旧基准覆盖较新的版本。 reports.lock(id); var saved = reports.get(id); if (saved.versionNo() != base) throw new ServiceException("报表已有新版本,请重新打开后编辑"); return JSON.parseObject(saved.snapshotJson()); } private Map settings(Map input, boolean resolveDefinitions) { var result = new LinkedHashMap<>(input); List structure; if (input.containsKey("fieldStructure")) structure = fields(input); else { structure = new ArrayList<>(); for (String key : List.of("batteryFields", "specialFields")) { var names = JSON.parseArray(JSON.toJSONString(input.getOrDefault(key, List.of())), String.class); for (String name : names) structure.add(new SpliceField(name, key.equals("batteryFields") ? "battery" : "special", "", null, name)); } } structure = templates.validate(structure, resolveDefinitions); result.put("fieldStructure", structure); result.put("batteryFields", structure.stream().filter(field -> field.category().equals("battery")).map(SpliceField::name).toList()); result.put("specialFields", structure.stream().filter(field -> field.category().equals("special")).map(SpliceField::name).toList()); return result; } private List fields(Map settings) { return JSON.parseArray(JSON.toJSONString(settings.get("fieldStructure")), SpliceField.class); } private List> segments(JSONObject snapshot) { return snapshot.getJSONArray("segments").stream().map(value -> (Map) value).toList(); } private Matrix freeze(Matrix matrix, List fields) { for (var segment : matrix.segments()) { var business = JSON.parseArray(JSON.toJSONString(segment.get("businessFields")), CurrentBatteryField.class); segment.put("businessFields", business.stream().filter(value -> fields.stream().anyMatch(field -> field.category().equals("battery") && (field.fieldDefinitionId() == null ? field.name().equals(value.getFieldName()) : field.fieldDefinitionId().equals(value.getFieldDefinitionId())))).toList()); } return matrix; } private Set statisticKeys(List> segments) { // 测试电池身份和原循环号共同限定人工值,重排来源不改变其归属。 Set allowed = new HashSet<>(); for (var segment : segments) { var source = JSON.parseObject(JSON.toJSONString(segment.get("source")), Candidate.class); var cycles = JSON.parseArray(JSON.toJSONString(segment.get("originalCycleNos")), Integer.class); for (int no : cycles) for (String metric : List.of("finalVoltage1", "energyEfficiency")) allowed.add(source.selection().testId() + "|" + source.selection().testBatteryId() + "|" + no + "|" + metric); } return allowed; } private void validateStatistics(Map settings, List> segments) { Object submitted = settings.get("statisticValues"); if (submitted == null) return; if (!(submitted instanceof Map values)) throw new ServiceException("人工指标格式无效"); Set allowed = statisticKeys(segments); for (var entry : values.entrySet()) { if (!allowed.contains(entry.getKey())) throw new ServiceException("人工指标不属于当前报表循环"); if (entry.getValue() == null || entry.getValue().toString().isBlank()) continue; try { new BigDecimal(entry.getValue().toString()); } catch (NumberFormatException error) { throw new ServiceException("人工指标必须为数值或留空"); } } } private Candidate candidate(TTest test, TTestBattery battery) { return new Candidate(new SpliceReportRequest.Selection(test.getTestId(), battery.getTestBatteryId()), test.getTestName(), test.getTestTypeName(), battery.getGroupId(), battery.getGroupName(), battery.getBatteryNo(), test.getStartedAtUtc() == null ? 0 : test.getStartedAtUtc().getTime()); } private int cycle(Map row) { return row.get("cycleNo") instanceof Number number ? number.intValue() : 0; } }