From 3e38957e501078500af9746e61313aabda742272 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期一, 07 九月 2026 16:01:14 +0800
Subject: [PATCH] 实现化成循环曲线快照与指定版本打印授权

---
 battery-business/src/main/java/com/battery/business/report/FormationReportService.java |   53 +++++++++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 45 insertions(+), 8 deletions(-)

diff --git a/battery-business/src/main/java/com/battery/business/report/FormationReportService.java b/battery-business/src/main/java/com/battery/business/report/FormationReportService.java
index 9713bb1..6e8b9de 100644
--- a/battery-business/src/main/java/com/battery/business/report/FormationReportService.java
+++ b/battery-business/src/main/java/com/battery/business/report/FormationReportService.java
@@ -42,9 +42,10 @@
 
     public record Candidate(Selection selection, String name, String type, long groupId, String batteryGroup, String batteryNo) { }
     public record ChargeStep(int stepNo, String name, String stepType, BigDecimal endVoltage) { }
+    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, List<ChargeStep>> chargeSteps, Map<Integer, Curve> curves) { }
     public record Matrix(List<Item> items, List<Integer> cycleNos) { }
     public record Summary(long id, String name, String creator, long createdAt, String testName, String batteryGroup) { }
 
@@ -89,10 +90,11 @@
             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 actual = Stream.of(rows, batteryRows(records.computeIfAbsent(selection.testId(), data::records), 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()), battery, actual)));
+            items.add(new Item(candidate(test, battery), actual, rows, metrics(rows, actual), battery.getFields(), chargeSteps(steps.get(selection.testId()), battery, actual), curves(realtime, actual)));
         }
         return new Matrix(items, items.stream().flatMap(item -> item.cycleNos().stream()).distinct().sorted().toList());
     }
@@ -102,7 +104,8 @@
     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().chargeStepSelections() == null ? Map.of() : request.settings().chargeStepSelections(),
+                request.settings().metrics(), request.settings().seriesColors());
         Matrix matrix = freeze(preview(request.selections()), settings.fields());
         validateValues(settings, matrix.items()); validateChargeSelections(settings, matrix.items());
         var snapshot = new LinkedHashMap<String, Object>();
@@ -110,6 +113,7 @@
         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);
     }
 
@@ -148,7 +152,8 @@
         Map<String, String> 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,
-                settings.chargeStepSelections() == null ? Map.of() : settings.chargeStepSelections()));
+                settings.chargeStepSelections() == null ? Map.of() : settings.chargeStepSelections(), settings.metrics(), settings.seriesColors()));
+        curveUnits(snapshot);
         reports.append(id, request.baseVersionNo() + 1, snapshot.getString("name"), json(snapshot), userId, userName, "按最新来源生成:" + request.changeSummary().trim());
         return get(id, null);
     }
@@ -202,16 +207,46 @@
             }).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());
+                            ? field.key().equals(value.getFieldName()) : field.fieldDefinitionId().equals(value.getFieldDefinitionId())))).toList(), item.chargeSteps(), item.curves());
         }).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 -> item.source().selection().testId() + "|" + item.source().selection().testBatteryId()).toList());
+        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<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 void validateChargeSelections(Settings settings, List<Item> items)
@@ -219,7 +254,8 @@
         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().getOrDefault(cycle, List.of());
+            // 早期版本未冻结工步时保持缺失,普通编辑不能重新读取设备事实。
+            List<ChargeStep> 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();
@@ -283,5 +319,6 @@
     { 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); }
-    private String json(Object value) { return JSON.toJSONString(value, JSONWriter.Feature.WriteMapNullValue); }
+    // 循环号是整数键,存入数据库 JSON 时必须写成带引号的成员名。
+    private String json(Object value) { return JSON.toJSONString(value, JSONWriter.Feature.WriteMapNullValue, JSONWriter.Feature.WriteNonStringKeyAsString); }
 }

--
Gitblit v1.7.1