From 59a93bdc51766da1387b71df7ba0342e97b068e2 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期二, 08 九月 2026 23:07:08 +0800
Subject: [PATCH] 修复三类报表测试详情的独立字段持久化
---
battery-business/src/main/java/com/battery/business/report/ReportSourceDetail.java | 36 ++++++++++++
battery-business/src/main/java/com/battery/business/report/CompareReportRequest.java | 5 -
battery-business/src/main/java/com/battery/business/report/CompareReportService.java | 5 -
ruoyi-admin/src/test/java/com/battery/business/report/ReportTestDetailPersistenceTest.java | 102 ++++++++++++++++++++++++++++++++++
battery-business/src/main/java/com/battery/business/report/FormationReportRequest.java | 9 --
battery-business/src/main/java/com/battery/business/report/FormationReportService.java | 6 -
battery-business/src/main/java/com/battery/business/report/SpliceReportService.java | 11 +--
7 files changed, 146 insertions(+), 28 deletions(-)
diff --git a/battery-business/src/main/java/com/battery/business/report/CompareReportRequest.java b/battery-business/src/main/java/com/battery/business/report/CompareReportRequest.java
index 5cf671c..a45b15a 100644
--- a/battery-business/src/main/java/com/battery/business/report/CompareReportRequest.java
+++ b/battery-business/src/main/java/com/battery/business/report/CompareReportRequest.java
@@ -24,7 +24,7 @@
@NotNull Map<String, @NotNull Map<String, String>> detailFieldValues,
List<@NotNull @Valid CompareField> fieldStructure,
List<@NotNull String> hiddenSeriesKeys,
- Map<String, @NotNull @Valid SourceDetailOverride> sourceDetailOverrides)
+ Map<String, @NotNull @Valid ReportSourceDetail> sourceDetailOverrides)
{
/** 展示设置不能夹带测量快照或其他未支持的修改。 */
@com.fasterxml.jackson.annotation.JsonAnySetter
@@ -32,7 +32,4 @@
{ throw new com.ruoyi.common.exception.ServiceException("报表设置不允许提交字段:" + field); }
}
- /** 只记录报表中明确修改的格;缺失与空字符串分别表示未修改和人工清空。 */
- public record SourceDetailOverride(@Size(max = 500) String experimentName,
- Map<String, @Size(max = 500) String> fieldValues) { }
}
diff --git a/battery-business/src/main/java/com/battery/business/report/CompareReportService.java b/battery-business/src/main/java/com/battery/business/report/CompareReportService.java
index a8956e2..d14ac6e 100644
--- a/battery-business/src/main/java/com/battery/business/report/CompareReportService.java
+++ b/battery-business/src/main/java/com/battery/business/report/CompareReportService.java
@@ -241,10 +241,7 @@
// 报表人工值只能归入已冻结的来源电池,不接受电池号或列位置作为身份。
if (!identities.contains(entry.getKey()) || entry.getValue() == null)
throw new ServiceException("测试详情来源不属于当前报表");
- var fields = entry.getValue().fieldValues();
- if (fields != null && fields.entrySet().stream().anyMatch(field -> field.getKey() == null
- || !field.getKey().matches("(?:battery|special)\\|.{1,100}") || field.getValue() == null || field.getValue().length() > 500))
- throw new ServiceException("测试详情字段或填写值无效");
+ entry.getValue().validate();
}
}
diff --git a/battery-business/src/main/java/com/battery/business/report/FormationReportRequest.java b/battery-business/src/main/java/com/battery/business/report/FormationReportRequest.java
index 52e481e..9c67e83 100644
--- a/battery-business/src/main/java/com/battery/business/report/FormationReportRequest.java
+++ b/battery-business/src/main/java/com/battery/business/report/FormationReportRequest.java
@@ -24,7 +24,7 @@
Map<String, List<@NotNull Integer>> manualChargeSteps,
Map<String, @Valid StepSelection> curveStepSelections,
List<@NotNull String> hiddenSeriesKeys, Integer activeCycle,
- Map<String, @Valid DetailOverride> sourceDetailOverrides)
+ Map<String, @Valid ReportSourceDetail> sourceDetailOverrides)
{
/** 旧版缺少显隐设置时显示五类指标,不补造测量快照。 */
public List<String> metrics() { return metrics == null ? List.of("voltage", "current", "positiveVoltage", "negativeVoltage", "temperature") : metrics; }
@@ -34,13 +34,6 @@
/** 展示设置拒绝来源和测量快照。 */
@JsonAnySetter
public void rejectUnknown(String field, Object value) { throw new ServiceException("化成设置不允许提交属性:" + field); }
- }
-
- /** 人工详情覆盖与来源快照分开保存,空字符串是明确填写值。 */
- public record DetailOverride(@Size(max = 500) String experimentName, Map<String, @NotNull @Size(max = 500) String> fieldValues)
- {
- @JsonAnySetter
- public void rejectUnknown(String field, Object value) { throw new ServiceException("详情调整不允许提交属性:" + field); }
}
/** 两组选择分别按来源循环归属;每个工步独立成线,不合并测量。 */
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 98fb99c..cda22e7 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
@@ -242,10 +242,8 @@
// 详情只接受测试电池身份;曲线工步身份不能作为详情列提交。
if (!sources.containsAll(settings.sourceDetailOverrides().keySet())) throw new ServiceException("详情调整不属于所选测试电池");
for (var override : settings.sourceDetailOverrides().values()) {
- if (override == null || (override.experimentName() != null && override.experimentName().length() > 500)) throw new ServiceException("详情调整无效");
- if (override.fieldValues() != null && override.fieldValues().entrySet().stream().anyMatch(entry ->
- entry.getKey() == null || !entry.getKey().matches("(?:battery|special)\\|.{1,100}")
- || entry.getValue() == null || entry.getValue().length() > 500)) throw new ServiceException("详情字段名称或值无效");
+ if (override == null) throw new ServiceException("详情调整无效");
+ override.validate();
}
}
for (Item item : items) if (item.stepCurves() != null) item.chargeSteps().forEach((cycle, ranges) -> ranges.forEach(step -> sources.add(sourceKey(item) + "|" + cycle + "|" + step.stepNo())));
diff --git a/battery-business/src/main/java/com/battery/business/report/ReportSourceDetail.java b/battery-business/src/main/java/com/battery/business/report/ReportSourceDetail.java
new file mode 100644
index 0000000..e51bce2
--- /dev/null
+++ b/battery-business/src/main/java/com/battery/business/report/ReportSourceDetail.java
@@ -0,0 +1,36 @@
+package com.battery.business.report;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.ruoyi.common.exception.ServiceException;
+
+/** 报表专属详情完整结构及人工值;空列表表示移除全部对应字段,不回退控制台。 */
+public record ReportSourceDetail(List<String> batteryFields, List<String> specialFields, Map<String, String> fieldValues)
+{
+ /** 校验填写值属于本份详情的字段,名称不解释为共享编号、单位或设备事实。 */
+ public void validate()
+ {
+ Set<String> keys = new HashSet<>();
+ collect(keys, "battery", batteryFields);
+ collect(keys, "special", specialFields);
+ if (fieldValues == null || !keys.containsAll(fieldValues.keySet())
+ || fieldValues.values().stream().anyMatch(value -> value == null || value.length() > 500))
+ throw new ServiceException("测试详情字段或填写值无效");
+ }
+
+ private static void collect(Set<String> keys, String category, List<String> names)
+ {
+ if (names == null || names.size() > 100) throw new ServiceException("测试详情字段列表无效");
+ for (String name : names)
+ if (name == null || name.isBlank() || name.length() > 100 || name.contains("\n") || name.contains("\r")
+ || !keys.add(category + "|" + name)) throw new ServiceException("测试详情字段名称无效或重复");
+ }
+
+ /** 详情仅接收字段结构和值,不能夹带实验或测量快照。 */
+ @JsonAnySetter
+ public void rejectUnknown(String name, Object value)
+ { throw new ServiceException("测试详情不允许提交属性:" + name); }
+}
diff --git a/battery-business/src/main/java/com/battery/business/report/SpliceReportService.java b/battery-business/src/main/java/com/battery/business/report/SpliceReportService.java
index 6791ab7..841f84d 100644
--- a/battery-business/src/main/java/com/battery/business/report/SpliceReportService.java
+++ b/battery-business/src/main/java/com/battery/business/report/SpliceReportService.java
@@ -256,14 +256,9 @@
// 详情按测试电池归属;名称与分类不解释为共享字段身份、单位或设备事实。
if (!sources.containsAll(overrides.keySet())) throw new ServiceException("详情调整不属于所选测试电池");
for (Object value : overrides.values()) {
- if (!(value instanceof Map<?,?> override) || !Set.of("experimentName", "fieldValues").containsAll(override.keySet())) throw new ServiceException("详情调整无效");
- if (override.containsKey("experimentName") && (!(override.get("experimentName") instanceof String name) || name.length() > 500)) throw new ServiceException("实验名称无效");
- if (!override.containsKey("fieldValues")) continue;
- if (!(override.get("fieldValues") instanceof Map<?,?> values)) throw new ServiceException("详情字段格式无效");
- for (var entry : values.entrySet()) {
- if (!(entry.getKey() instanceof String key) || !key.matches("(?:battery|special)\\|.{1,100}")
- || !(entry.getValue() instanceof String text) || text.length() > 500) throw new ServiceException("详情字段名称或值无效");
- }
+ if (!(value instanceof Map<?,?> override) || !Set.of("batteryFields", "specialFields", "fieldValues").containsAll(override.keySet()))
+ throw new ServiceException("详情调整无效");
+ JSON.parseObject(JSON.toJSONString(override), ReportSourceDetail.class).validate();
}
}
diff --git a/ruoyi-admin/src/test/java/com/battery/business/report/ReportTestDetailPersistenceTest.java b/ruoyi-admin/src/test/java/com/battery/business/report/ReportTestDetailPersistenceTest.java
new file mode 100644
index 0000000..28fc6fe
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/battery/business/report/ReportTestDetailPersistenceTest.java
@@ -0,0 +1,102 @@
+package com.battery.business.report;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+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;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.h2.jdbcx.JdbcDataSource;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+class ReportTestDetailPersistenceTest
+{
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ @ParameterizedTest
+ @ValueSource(strings = { "compare", "formation", "splice" })
+ void reportOwnsAddedAndRemovedFieldsAcrossSaveAndReopen(String type) throws Exception
+ {
+ var database = new JdbcDataSource();
+ database.setURL("jdbc:h2:mem:detail_" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1");
+ var jdbc = new JdbcTemplate(database);
+ jdbc.execute("CREATE TABLE t_" + type + "_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_" + type + "_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))");
+ var tests = mock(ITTestService.class);
+ var data = mock(ITestDataQueryService.class);
+ var definitions = mock(ITBatteryGroupFieldService.class);
+ List<TTest> originals = new ArrayList<>();
+ for (long id : List.of(1L, 2L)) {
+ var battery = new TTestBattery();
+ battery.setTestBatteryId(id); battery.setGroupId(id); battery.setGroupName("组" + id);
+ battery.setBatteryNo("同名电池"); battery.setFields(List.of());
+ var test = new TTest();
+ test.setTestId(id); test.setTestName("测试" + id); test.setTestTypeName("化成");
+ test.setTestStatus("COMPLETED"); test.setBatteries(List.of(battery)); test.setLogicStepMappings(List.of());
+ var detail = new TTestConsoleDetail();
+ detail.setExperimentName("仅控制台实验"); detail.setBatteryFields(List.of("额定容量")); detail.setSpecialFields(List.of("待删除"));
+ detail.setFieldValues(new LinkedHashMap<>(Map.of("额定容量", Map.of("同名电池", "来源" + id), "待删除", Map.of("同名电池", "原值"))));
+ test.setConsoleDetail(detail); originals.add(test);
+ when(tests.selectTestById(id)).thenReturn(test);
+ Map<String, Object> row = Map.of("testBatteryId", id, "batteryNo", "同名电池", "cycleNo", 1,
+ "stepNo", 1, "stepName", "充电", "sourceType", "SNAPSHOT", "deviceTime", 1000L, "voltage", 3);
+ when(data.records(id)).thenReturn(List.of(row));
+ when(data.cycles(id)).thenReturn(List.of()); when(data.steps(id)).thenReturn(List.of());
+ }
+ var compare = new CompareReportService(tests, data, new CompareReportRepository(database));
+ var formation = new FormationReportService(tests, data, new FormationReportRepository(database), definitions, value -> StepDirectionResolver.Direction.UNKNOWN);
+ var splice = new SpliceReportService(tests, data, new SpliceReportRepository(database), new SpliceFieldTemplateService(database, definitions));
+ var settings = JSON.parseObject(switch (type) {
+ case "compare" -> "{\"metrics\":[],\"seriesColors\":[],\"batteryTemplateFields\":[],\"specialFields\":[],\"fieldStructure\":[],\"detailFieldValues\":{}}";
+ case "formation" -> "{\"fields\":[{\"key\":\"chargeCapacity\",\"scope\":\"cycle\",\"kind\":\"metric\",\"displayName\":\"充电容量\",\"unit\":\"mAh\"}],\"values\":{},\"chargeStepSelections\":{},\"metrics\":[],\"seriesColors\":{}}";
+ default -> "{\"fieldStructure\":[],\"detailFieldValues\":{}}";
+ });
+ var details = JSON.parseObject("""
+ {"1|1":{"batteryFields":["额定容量"],"specialFields":["新字段"],"fieldValues":{"battery|额定容量":"报表甲","special|新字段":""}},
+ "2|2":{"batteryFields":["额定容量"],"specialFields":["新字段"],"fieldValues":{"battery|额定容量":"报表乙","special|新字段":"乙值"}}}
+ """);
+ settings.put("sourceDetailOverrides", details);
+ var request = new JSONObject(); request.put("name", "独立详情"); request.put("description", ""); request.put("settings", settings);
+ request.put("selections", type.equals("compare") ? List.of(Map.of("testId", 1, "testBatteryId", 1, "cycleNo", 1, "stepNo", 1), Map.of("testId", 2, "testBatteryId", 2, "cycleNo", 1, "stepNo", 1))
+ : List.of(Map.of("testId", 1, "testBatteryId", 1), Map.of("testId", 2, "testBatteryId", 2)));
+ Map<String, Object> created = switch (type) {
+ case "compare" -> compare.create(mapper.readValue(request.toJSONString(), CompareReportRequest.class), 1, "操作员");
+ case "formation" -> formation.create(mapper.readValue(request.toJSONString(), FormationReportRequest.class), 1, "操作员");
+ default -> splice.create(mapper.readValue(request.toJSONString(), SpliceReportRequest.class), 1, "操作员");
+ };
+ long id = ((Number) created.get("id")).longValue();
+ assertEquals(details, JSON.parseObject(JSON.toJSONString(created.get("settings"))).getJSONObject("sourceDetailOverrides"));
+ clearInvocations(tests, data);
+ details.getJSONObject("1|1").put("batteryFields", List.of());
+ details.getJSONObject("1|1").put("specialFields", List.of());
+ details.getJSONObject("1|1").put("fieldValues", Map.of());
+ details.getJSONObject("2|2").getJSONObject("fieldValues").put("special|新字段", "二次修改");
+ request.remove("selections"); request.put("baseVersionNo", 1); request.put("changeSummary", "修改本报表详情");
+ switch (type) {
+ case "compare" -> compare.edit(id, mapper.readValue(request.toJSONString(), CompareReportEditRequest.class), 1, "操作员");
+ case "formation" -> formation.edit(id, mapper.readValue(request.toJSONString(), FormationReportRequest.Edit.class), 1, "操作员");
+ default -> splice.edit(id, mapper.readValue(request.toJSONString(), SpliceReportRequest.Edit.class), 1, "操作员");
+ }
+ Map<String, Object> reopened = switch (type) {
+ case "compare" -> compare.get(id); case "formation" -> formation.get(id, null); default -> splice.get(id, null);
+ };
+ var savedDetails = JSON.parseObject(JSON.toJSONString(reopened.get("settings"))).getJSONObject("sourceDetailOverrides");
+ assertEquals(details, savedDetails);
+ assertTrue(savedDetails.getJSONObject("1|1").getJSONArray("specialFields").isEmpty());
+ assertEquals("二次修改", savedDetails.getJSONObject("2|2").getJSONObject("fieldValues").getString("special|新字段"));
+ Map<String, Object> history = switch (type) {
+ case "compare" -> compare.get(id, 1); case "formation" -> formation.get(id, 1); default -> splice.get(id, 1);
+ };
+ assertEquals("", JSON.parseObject(JSON.toJSONString(history.get("settings"))).getJSONObject("sourceDetailOverrides").getJSONObject("1|1").getJSONObject("fieldValues").getString("special|新字段"));
+ verifyNoInteractions(tests, data);
+ for (var original : originals) {
+ assertEquals(List.of("待删除"), original.getConsoleDetail().getSpecialFields());
+ assertEquals("来源" + original.getTestId(), original.getConsoleDetail().getFieldValues().get("额定容量").get("同名电池"));
+ }
+ }
+}
--
Gitblit v1.7.1