| | |
| | | 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; |
| | |
| | | @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) |
| | | { this.tests = tests; this.data = data; this.reports = reports; this.definitions = definitions; } |
| | | 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) { } |
| | | public record ChargeStep(int stepNo, String name, String stepType, BigDecimal endVoltage) { } |
| | | public record ChargeStep(int stepNo, String name, String stepType, BigDecimal endVoltage, Direction direction) { } |
| | | 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, Curve> curves) { } |
| | | Map<Integer, List<ChargeStep>> chargeSteps, Map<Integer, Curve> curves, Map<Integer, Map<Integer, Curve>> stepCurves) { } |
| | | public record Matrix(List<Item> items, List<Integer> cycleNos) { } |
| | | public record Summary(long id, String name, String creator, long createdAt, String testName, String batteryGroup) { } |
| | | |
| | |
| | | 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), curves(realtime, actual))); |
| | | 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()); |
| | | } |
| | |
| | | { |
| | | 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().metrics(), request.settings().seriesColors(), request.settings().manualChargeSteps(), |
| | | request.settings().curveStepSelections(), request.settings().hiddenSeriesKeys(), request.settings().activeCycle()); |
| | | 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<String, Object>(); |
| | | snapshot.put("name", request.name().trim()); snapshot.put("description", request.description()); |
| | |
| | | { |
| | | var snapshot = current(id, request.baseVersionNo()); |
| | | validateFields(request.settings().fields(), false); |
| | | validateValues(request.settings(), items(snapshot)); validateChargeSelections(request.settings(), items(snapshot)); |
| | | snapshot.put("name", request.name().trim()); snapshot.put("description", request.description()); snapshot.put("settings", request.settings()); |
| | | var settings = "actual-steps".equals(snapshot.getString("curveMode")) ? stepSettings(request.settings(), items(snapshot), false) : request.settings(); |
| | | 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); |
| | | } |
| | |
| | | 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.metrics(), settings.seriesColors())); |
| | | settings.chargeStepSelections() == null ? Map.of() : settings.chargeStepSelections(), settings.metrics(), settings.seriesColors(), settings.manualChargeSteps(), settings.curveStepSelections(), settings.hiddenSeriesKeys(), settings.activeCycle())); |
| | | curveUnits(snapshot); |
| | | reports.append(id, request.baseVersionNo() + 1, snapshot.getString("name"), json(snapshot), userId, userName, "按最新来源生成:" + request.changeSummary().trim()); |
| | | return get(id, null); |
| | |
| | | }).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()); |
| | | ? field.key().equals(value.getFieldName()) : field.fieldDefinitionId().equals(value.getFieldDefinitionId())))).toList(), item.chargeSteps(), item.curves(), item.stepCurves()); |
| | | }).toList(); |
| | | return new Matrix(frozen, matrix.cycleNos()); |
| | | } |
| | |
| | | { |
| | | 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()); |
| | | Set<String> sources = new HashSet<>(items.stream().map(item -> sourceKey(item)).toList()); |
| | | 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()) |
| | |
| | | return result; |
| | | } |
| | | |
| | | private String sourceKey(Item item) { return item.source().selection().testId() + "|" + item.source().selection().testBatteryId(); } |
| | | |
| | | private Map<Integer, Map<Integer, Curve>> stepCurves(List<Map<String, Object>> rows, List<Integer> cycles) |
| | | { |
| | | Map<Integer, Map<Integer, Curve>> result = new LinkedHashMap<>(); |
| | | for (int cycle : cycles) { |
| | | Map<Integer, Curve> 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<Item> items, boolean refresh) |
| | | { |
| | | Map<String, List<Integer>> manual = new LinkedHashMap<>(); |
| | | Map<String, FormationReportRequest.StepSelection> selected = new LinkedHashMap<>(); |
| | | Set<String> 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.<Integer>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<String> 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.<String>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); |
| | | } |
| | | |
| | | private void validateChargeSelections(Settings settings, List<Item> items) |
| | | { |
| | | if (items.stream().allMatch(item -> item.stepCurves() != null)) return; |
| | | 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; |
| | |
| | | } |
| | | } |
| | | |
| | | private Map<Integer, List<ChargeStep>> chargeSteps(List<Map<String, Object>> rows, TTestBattery battery, List<Integer> cycles) |
| | | private Map<Integer, List<ChargeStep>> chargeSteps(List<Map<String, Object>> rows, List<Map<String, Object>> realtime, TTestBattery battery, List<Integer> cycles) |
| | | { |
| | | Map<Integer, List<ChargeStep>> result = new LinkedHashMap<>(); |
| | | for (int cycle : cycles) result.put(cycle, rows == null ? List.of() : rows.stream() |
| | | .filter(row -> Objects.equals(battery.getBatteryNo(), row.get("batteryNo")) && cycleNo(row) == cycle) |
| | | .map(row -> new ChargeStep(number(row.get("stepNo")), String.valueOf(row.getOrDefault("stepName", "")), |
| | | String.valueOf(row.getOrDefault("stepType", "")), decimal(row.get("endVoltage")))) |
| | | // 无法可靠识别类型时保留该循环的全部实际工步,由操作员选择,不凭名称或电流猜测。 |
| | | .filter(step -> step.stepNo() > 0).toList()); |
| | | for (int cycle : cycles) { |
| | | Map<Integer, ChargeStep> 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) { String value = type == null ? "" : type.trim().toLowerCase(); return value.equals("cc") || value.equals("charge") || value.equals("charging"); } |
| | | 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; } |
| | | |