实现化成报表真实工步取数与独立公式快照
冻结末放电工步容量和报表计算基础字段,按已确认公式保存结果。自动选择全部实际充放电工步,校验非负数字输入,并覆盖独立编辑和历史重开。
| | |
| | | import com.fasterxml.jackson.annotation.JsonAnySetter; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | |
| | | /** 化成字段结构不携带数据;循环测量单位固定,未知公式仅允许人工项。 */ |
| | | /** 化成字段结构不携带数据;实际测量与已确认计算结果不可伪装成人工项。 */ |
| | | public record FormationField(String key, String scope, String kind, String displayName, String unit, Long fieldDefinitionId) |
| | | { |
| | | public static final Map<String, String> METRIC_UNITS = Map.of("chargeCapacity", "mAh", "dischargeCapacity", "mAh", |
| | |
| | | { |
| | | if (key == null || key.isBlank() || key.length() > 100 || key.contains("|") |
| | | || !("detail".equals(scope) || "cycle".equals(scope)) |
| | | || !("battery".equals(kind) || "metric".equals(kind) || "manual".equals(kind))) |
| | | || !("battery".equals(kind) || "metric".equals(kind) || "manual".equals(kind) || "calculated".equals(kind))) |
| | | throw new ServiceException("化成字段身份或分类无效"); |
| | | displayName = displayName == null ? key : displayName.trim(); |
| | | unit = unit == null ? "" : unit.trim(); |
| | |
| | | throw new ServiceException("化成字段展示名称、单位或共享身份无效"); |
| | | if ((METRIC_UNITS.containsKey(key) && !"metric".equals(kind)) |
| | | || (java.util.Set.of("Z", "H", "F", "A", "B").contains(key) && !"battery".equals(kind)) |
| | | || (java.util.Set.of("FN", "C", "method2", "manual", "chargePerGram", "chargePerGramTotal", "capacity1v", "capacity1vf", |
| | | "dischargeInflection", "dischargeInflectionTotal").contains(key) && !"manual".equals(kind))) |
| | | || (java.util.Set.of("method", "method2", "manual").contains(key) && !"manual".equals(kind)) |
| | | || (FormationReportCalculations.RESULTS.contains(key) && !"calculated".equals(kind)) |
| | | || ("calculated".equals(kind) && (!FormationReportCalculations.RESULTS.contains(key) |
| | | || !(java.util.Set.of("FN", "C").contains(key) ? "detail" : "cycle").equals(scope)))) |
| | | throw new ServiceException("固定字段不能改变来源分类"); |
| | | } |
| | | |
| New file |
| | |
| | | package com.battery.business.report; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.MathContext; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | import com.battery.device.api.StepDirectionResolver.Direction; |
| | | |
| | | /** 化成公式只读取报表自身的基础字段和冻结工步,结果不反写电池组或设备事实。 */ |
| | | final class FormationReportCalculations |
| | | { |
| | | static final Set<String> INPUTS = Set.of("H", "Z", "A", "F", "B"); |
| | | static final Set<String> RESULTS = Set.of("FN", "C", "chargePerGram", "chargePerGramTotal", "capacity1v", "capacity1vf", |
| | | "dischargeInflection", "dischargeInflectionTotal"); |
| | | |
| | | /** 每次保存重算并冻结结果;隐藏公式输入字段不能改变其归属或丢失依赖。 */ |
| | | static Map<String, BigDecimal> calculate(List<FormationReportService.Item> items, FormationReportRequest.Settings settings) |
| | | { |
| | | Map<String, BigDecimal> result = new LinkedHashMap<>(); |
| | | for (var item : items) { |
| | | String prefix = item.source().selection().testId() + "|" + item.source().selection().testBatteryId() + "|"; |
| | | Map<String, BigDecimal> base = new LinkedHashMap<>(); |
| | | for (String key : INPUTS) { |
| | | var field = settings.fields().stream().filter(value -> "detail".equals(value.scope()) && key.equals(value.key())).findFirst(); |
| | | var stored = item.businessFields().stream().filter(value -> field.isPresent() && field.get().fieldDefinitionId() != null |
| | | ? field.get().fieldDefinitionId().equals(value.getFieldDefinitionId()) : key.equals(value.getFieldName())).findFirst(); |
| | | String value = settings.values().getOrDefault(prefix + "0|" + key, stored.map(valueField -> valueField.getFieldValue()).orElse(null)); |
| | | base.put(key, number(value)); |
| | | } |
| | | BigDecimal c = multiply(base.get("H"), base.get("Z"), base.get("A"), base.get("F"), base.get("B")); |
| | | result.put(prefix + "0|C", c); |
| | | result.put(prefix + "0|FN", divide(divide(base.get("F"), base.get("H")), base.get("Z"))); |
| | | for (int cycle : item.cycleNos()) { |
| | | String row = prefix + cycle + "|"; |
| | | BigDecimal manual = number(settings.values().get(row + "manual")); |
| | | result.put(row + "chargePerGram", divide(divide(multiply(manual, c), base.get("H")), base.get("Z"))); |
| | | result.put(row + "chargePerGramTotal", divide(multiply(manual, c), base.get("F"))); |
| | | var last = item.chargeSteps().getOrDefault(cycle, List.of()).stream().filter(step -> step.direction() == Direction.DISCHARGE) |
| | | .max(java.util.Comparator.comparingInt(FormationReportService.ChargeStep::stepNo)); |
| | | // 工步容量存储为 Ah,报表结果使用 mAh;末工步缺值绝不回退前一工步。 |
| | | BigDecimal capacity = last.map(step -> multiply(step.capacity(), BigDecimal.valueOf(1000))).orElse(null); |
| | | result.put(row + "capacity1v", capacity); |
| | | result.put(row + "capacity1vf", divide(capacity, base.get("F"))); |
| | | // 仅拐点两项保留用户明确要求的演示占位,固定值随版本保存。 |
| | | result.put(row + "dischargeInflection", BigDecimal.valueOf(3.934)); |
| | | result.put(row + "dischargeInflectionTotal", BigDecimal.valueOf(3.982)); |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private static BigDecimal number(String value) |
| | | { |
| | | if (value == null || !value.trim().matches("[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)")) return null; |
| | | return new BigDecimal(value.trim()); |
| | | } |
| | | |
| | | private static BigDecimal multiply(BigDecimal... values) |
| | | { |
| | | BigDecimal result = BigDecimal.ONE; |
| | | for (var value : values) { if (value == null) return null; result = result.multiply(value, MathContext.DECIMAL128); } |
| | | return result; |
| | | } |
| | | |
| | | private static BigDecimal divide(BigDecimal value, BigDecimal divisor) |
| | | { return value == null || divisor == null || divisor.signum() == 0 ? null : value.divide(divisor, MathContext.DECIMAL128); } |
| | | } |
| | |
| | | { 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) { } |
| | | public record ChargeStep(int stepNo, String name, String stepType, BigDecimal endVoltage, Direction direction, BigDecimal capacity) { } |
| | | 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, |
| | |
| | | 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); |
| | |
| | | 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); |
| | | } |
| | |
| | | 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); |
| | |
| | | if (!"battery".equals(field.kind())) { |
| | | // 仅新生成的字段结构纠正旧误写,读取历史快照时不改名。 |
| | | String corrected = Map.of("chargePerGram", "每克N充电量", "chargePerGramTotal", "每克F充电量", |
| | | "capacity1v", "1.0V容量", "capacity1vf", "1.0VF克容量", "dischargeInflection", "拐点容量", |
| | | "dischargeInflectionTotal", "拐点F克容量").get(field.key()); |
| | | "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(); |
| | |
| | | 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 |
| | | .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()); |
| | |
| | | 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<String, Object> snapshot) |
| | |
| | | |
| | | 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) || requested.stream().anyMatch(choice.discharge()::contains)) |
| | | throw new ServiceException("同一实际工步不能同时指定为充电和放电"); |
| | | selected.put(key, choice); |
| | | 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())); |
| | | } |
| | | 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("曲线循环不属于当前报表"); |
| | |
| | | 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, settings.sourceDetailOverrides()); |
| | | 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<Item> items) |
| | |
| | | // 实时记录只补充实际范围身份,不代替缺失的工步终止电压。 |
| | | 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))); |
| | | found.putIfAbsent(number(row.get("stepNo")), new ChargeStep(number(row.get("stepNo")), String.valueOf(row.getOrDefault("stepName", "")), type, null, directions.resolve(type), 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 = 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))); |
| | | 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), decimal(row.get("capacity")))); |
| | | } |
| | | result.put(cycle, new ArrayList<>(found.values())); |
| | | } |
| | |
| | | return result; |
| | | } |
| | | |
| | | private boolean eligible(TTest test) { return "COMPLETED".equals(test.getTestStatus()) && "化成".equals(test.getTestTypeName()); } |
| | | 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) |
| New file |
| | |
| | | package com.battery.business.report; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | import java.math.BigDecimal; |
| | | import java.util.*; |
| | | import com.alibaba.fastjson2.JSON; |
| | | import com.alibaba.fastjson2.JSONObject; |
| | | import com.battery.business.domain.*; |
| | | import com.battery.business.service.*; |
| | | import com.battery.device.api.StepDirectionResolver.Direction; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import org.h2.jdbcx.JdbcDataSource; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | |
| | | class FormationRealReportTest |
| | | { |
| | | private final ObjectMapper mapper = new ObjectMapper(); |
| | | private final ITTestService tests = mock(ITTestService.class); |
| | | private final ITestDataQueryService data = mock(ITestDataQueryService.class); |
| | | private final FormationReportService service; |
| | | |
| | | FormationRealReportTest() |
| | | { |
| | | var db = new JdbcDataSource(); |
| | | db.setURL("jdbc:h2:mem:formation_" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1"); |
| | | var jdbc = new JdbcTemplate(db); |
| | | jdbc.execute("CREATE TABLE t_formation_report(report_id BIGINT AUTO_INCREMENT PRIMARY KEY,report_name VARCHAR(100),creator_id BIGINT,creator_name VARCHAR(100),created_at_utc TIMESTAMP)"); |
| | | jdbc.execute("CREATE TABLE t_formation_report_version(report_id BIGINT,version_no INT,snapshot_json CLOB,operator_id BIGINT,operator_name VARCHAR(100),created_at_utc TIMESTAMP,change_summary VARCHAR(500),PRIMARY KEY(report_id,version_no))"); |
| | | service = new FormationReportService(tests, data, new FormationReportRepository(db), mock(ITBatteryGroupFieldService.class), |
| | | type -> "cc".equals(type) ? Direction.CHARGE : "dc".equals(type) ? Direction.DISCHARGE : Direction.UNKNOWN); |
| | | var battery = new TTestBattery(); battery.setTestBatteryId(11L); battery.setBatteryNo("电池1"); battery.setGroupId(1L); battery.setGroupName("组1"); |
| | | battery.setFields(new ArrayList<>()); |
| | | Map.of("H", "2", "Z", "3", "A", "4", "F", "5", "B", "6").forEach((name, value) -> { |
| | | var field = new CurrentBatteryField(); field.setFieldName(name); field.setFieldValue(value); battery.getFields().add(field); |
| | | }); |
| | | var test = new TTest(); test.setTestId(1L); test.setTestName("化成甲"); test.setTestStatus("COMPLETED"); test.setTestTypeName("化成"); test.setBatteries(List.of(battery)); |
| | | when(tests.selectTestById(1L)).thenReturn(test); |
| | | when(data.records(1L)).thenReturn(List.of()); when(data.cycles(1L)).thenReturn(List.of()); |
| | | when(data.steps(1L)).thenReturn(List.of(step(1, "cc", "4.1", "0.1"), step(2, "cc", "4.2", "0.2"), step(3, "dc", "1", "0.25"))); |
| | | } |
| | | |
| | | private Map<String, Object> step(int no, String type, String voltage, String capacity) |
| | | { |
| | | var row = new LinkedHashMap<String, Object>(); row.put("batteryNo", "电池1"); row.put("cycleNo", 1); row.put("stepNo", no); row.put("stepType", type); |
| | | row.put("endVoltage", new BigDecimal(voltage)); row.put("capacity", capacity == null ? null : new BigDecimal(capacity)); return row; |
| | | } |
| | | |
| | | private JSONObject request() |
| | | { |
| | | return JSON.parseObject(""" |
| | | {"name":"化成真实报表","description":"","selections":[{"testId":1,"testBatteryId":11}], |
| | | "settings":{"fields":[{"key":"manual","scope":"cycle","kind":"manual","displayName":"数字","unit":""}], |
| | | "values":{"1|11|1|manual":"2"},"chargeStepSelections":{}}} |
| | | """); |
| | | } |
| | | |
| | | private JSONObject create(JSONObject request) throws Exception |
| | | { return JSON.parseObject(JSON.toJSONString(service.create(mapper.readValue(request.toJSONString(), FormationReportRequest.class), 1, "操作员"))); } |
| | | |
| | | @Test |
| | | void freezesFormulaInputsAndLastDischargeCapacityWithoutDependingOnDisplayedFields() throws Exception |
| | | { |
| | | var saved = create(request()); |
| | | var values = saved.getJSONObject("calculatedValues"); |
| | | assertNotNull(values, "公式结果必须随版本保存"); |
| | | assertEquals(720, values.getBigDecimal("1|11|0|C").intValueExact()); |
| | | assertEquals(240, values.getBigDecimal("1|11|1|chargePerGram").intValueExact()); |
| | | assertEquals(288, values.getBigDecimal("1|11|1|chargePerGramTotal").intValueExact()); |
| | | assertEquals(250, values.getBigDecimal("1|11|1|capacity1v").intValueExact()); |
| | | assertEquals(50, values.getBigDecimal("1|11|1|capacity1vf").intValueExact()); |
| | | assertEquals(5, saved.getJSONArray("items").getJSONObject(0).getJSONArray("businessFields").size()); |
| | | clearInvocations(tests, data); |
| | | assertEquals(values, JSON.parseObject(JSON.toJSONString(service.get(saved.getLongValue("id"), null))).getJSONObject("calculatedValues")); |
| | | verifyNoInteractions(tests, data); |
| | | } |
| | | |
| | | @Test |
| | | void reportFieldEditsRecalculateWithoutReadingOrChangingDeletedSource() throws Exception |
| | | { |
| | | var request = request(); |
| | | request.getJSONObject("settings").getJSONArray("fields").add(JSON.parseObject("{\"key\":\"F\",\"scope\":\"detail\",\"kind\":\"battery\",\"displayName\":\"F\",\"unit\":\"\"}")); |
| | | var first = create(request); long id = first.getLongValue("id"); |
| | | clearInvocations(tests, data); |
| | | when(tests.selectTestById(1L)).thenThrow(new IllegalStateException("来源已删除")); |
| | | request.remove("selections"); request.put("baseVersionNo", 1); request.put("changeSummary", "修改报表 F"); |
| | | request.getJSONObject("settings").getJSONObject("values").put("1|11|0|F", "10"); |
| | | var second = JSON.parseObject(JSON.toJSONString(service.edit(id, mapper.readValue(request.toJSONString(), FormationReportRequest.Edit.class), 1, "操作员"))); |
| | | assertEquals(1440, second.getJSONObject("calculatedValues").getBigDecimal("1|11|0|C").intValueExact()); |
| | | assertEquals(480, second.getJSONObject("calculatedValues").getBigDecimal("1|11|1|chargePerGram").intValueExact()); |
| | | assertEquals(25, second.getJSONObject("calculatedValues").getBigDecimal("1|11|1|capacity1vf").intValueExact()); |
| | | assertEquals(720, JSON.parseObject(JSON.toJSONString(service.get(id, 1))).getJSONObject("calculatedValues").getBigDecimal("1|11|0|C").intValueExact()); |
| | | verifyNoInteractions(tests, data); |
| | | } |
| | | |
| | | @Test |
| | | void missingLastDischargeDoesNotBorrowEarlierCapacityAndManualMustBeNonnegative() throws Exception |
| | | { |
| | | when(data.steps(1L)).thenReturn(List.of(step(1,"dc","1","0.25"),step(2,"dc","1",null))); |
| | | var saved=create(request()); |
| | | assertNull(saved.getJSONObject("calculatedValues").get("1|11|1|capacity1v")); |
| | | var bad=request();bad.getJSONObject("settings").getJSONObject("values").put("1|11|1|manual","-1"); |
| | | assertThrows(com.ruoyi.common.exception.ServiceException.class,()->create(bad)); |
| | | bad.getJSONObject("settings").getJSONObject("values").put("1|11|1|manual","电量"); |
| | | assertThrows(com.ruoyi.common.exception.ServiceException.class,()->create(bad)); |
| | | } |
| | | |
| | | @Test |
| | | void automaticDirectionSelectionExcludesUnknownAndKeepsAllRealChargeSteps() throws Exception |
| | | { |
| | | when(data.steps(1L)).thenReturn(List.of(step(1,"cc","4.1","0.1"),step(2,"unknown","99","99"),step(3,"cc","4.2","0.2"),step(4,"dc","1","0.25"))); |
| | | var request=request(); |
| | | request.getJSONObject("settings").put("manualChargeSteps",Map.of("1|11|1",List.of(2))); |
| | | request.getJSONObject("settings").put("curveStepSelections",Map.of("1|11|1",Map.of("charge",List.of(1,2),"discharge",List.of(4)))); |
| | | var saved=create(request); |
| | | var selected=saved.getJSONObject("settings").getJSONObject("curveStepSelections").getJSONObject("1|11|1"); |
| | | assertEquals(List.of(1,3),selected.getList("charge",Integer.class)); |
| | | assertEquals(List.of(4),selected.getList("discharge",Integer.class)); |
| | | assertEquals(250,saved.getJSONObject("calculatedValues").getBigDecimal("1|11|1|capacity1v").intValueExact()); |
| | | } |
| | | } |