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 step(int no, String type, String voltage, String capacity) { var row = new LinkedHashMap(); 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()); } }