From eee00fd63f558cfa0caf737c868de8fc0c652fcd Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期一, 07 九月 2026 10:39:30 +0800
Subject: [PATCH] 修复对比字段模板结构与权限边界
---
battery-business/src/main/java/com/battery/business/report/CompareFieldTemplateRepository.java | 35 ++++++--
battery-business/src/main/java/com/battery/business/report/CompareReportRequest.java | 3
battery-business/src/main/java/com/battery/business/report/CompareReportService.java | 35 +++++++-
battery-business/src/main/resources/mapper/battery/TBatteryGroupFieldMapper.xml | 2
battery-business/src/main/java/com/battery/business/report/CompareField.java | 25 ++++++
battery-business/src/main/java/com/battery/business/report/CompareFieldTemplateRequest.java | 19 ++++
ruoyi-admin/src/main/java/com/battery/business/web/CompareReportController.java | 26 ++++--
battery-business/src/main/java/com/battery/business/report/CompareFieldTemplateService.java | 51 ++++++++++++
8 files changed, 172 insertions(+), 24 deletions(-)
diff --git a/battery-business/src/main/java/com/battery/business/report/CompareField.java b/battery-business/src/main/java/com/battery/business/report/CompareField.java
new file mode 100644
index 0000000..48418b4
--- /dev/null
+++ b/battery-business/src/main/java/com/battery/business/report/CompareField.java
@@ -0,0 +1,25 @@
+package com.battery.business.report;
+
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.ruoyi.common.exception.ServiceException;
+
+/** 测试详情字段的身份和展示语义;不承载值,也不改变测量曲线的单位契约。 */
+public record CompareField(String name, String category, String unit, Long fieldDefinitionId, String displayName)
+{
+ public CompareField
+ {
+ if (name == null || name.isBlank() || name.length() > 100
+ || !("battery".equals(category) || "special".equals(category)))
+ throw new ServiceException("模板字段名称或分类无效");
+ unit = unit == null ? "" : unit.trim();
+ displayName = displayName == null ? name : displayName.trim();
+ if (unit.length() > 30 || displayName.isBlank() || displayName.length() > 100
+ || (fieldDefinitionId != null && (fieldDefinitionId <= 0 || !"battery".equals(category))))
+ throw new ServiceException("模板字段展示名称、单位或共享身份无效");
+ }
+
+ /** 请求不允许混入来源、测量或人工值;不能依靠前端删减属性保证模板边界。 */
+ @JsonAnySetter
+ public void rejectUnknown(String field, Object value)
+ { throw new ServiceException("模板字段不允许提交属性:" + field); }
+}
diff --git a/battery-business/src/main/java/com/battery/business/report/CompareFieldTemplateRepository.java b/battery-business/src/main/java/com/battery/business/report/CompareFieldTemplateRepository.java
index a229bef..67530b4 100644
--- a/battery-business/src/main/java/com/battery/business/report/CompareFieldTemplateRepository.java
+++ b/battery-business/src/main/java/com/battery/business/report/CompareFieldTemplateRepository.java
@@ -7,28 +7,43 @@
import javax.sql.DataSource;
import java.sql.Timestamp;
import java.util.List;
-import java.util.Map;
+/** 对比模板独立存储;模板更新不访问或回写报表版本。 */
@Repository
public class CompareFieldTemplateRepository {
private final JdbcTemplate jdbc;
public CompareFieldTemplateRepository(DataSource dataSource) { jdbc = new JdbcTemplate(dataSource); }
- public record Template(long id, String name, String type, List<Map<String, Object>> fields, String creator, long updatedAt) {}
- @SuppressWarnings("unchecked") private List<Map<String,Object>> fields(String json) { return (List<Map<String,Object>>)(List<?>) JSON.parseArray(json, Map.class); }
+ public record Template(long id, String name, String type, List<CompareField> fields, String creator, long updatedAt) {}
+ private List<CompareField> fields(String json) { return JSON.parseArray(json, CompareField.class); }
+
+ /** 返回该类全部模板,不按创建人过滤;字段数组次序即展示次序。 */
public List<Template> list() {
return jdbc.query("select template_id,template_name,template_type,fields_json,creator_name,updated_at_utc from t_compare_report_field_template where template_type='compare' order by template_name,template_id",
(rs, i) -> new Template(rs.getLong(1), rs.getString(2), rs.getString(3), fields(rs.getString(4)), rs.getString(5), rs.getTimestamp(6).getTime()));
}
- public Template get(long id) {
- List<Template> rows = jdbc.query("select template_id,template_name,template_type,fields_json,creator_name,updated_at_utc from t_compare_report_field_template where template_id=?", (rs,i) -> new Template(rs.getLong(1),rs.getString(2),rs.getString(3),fields(rs.getString(4)),rs.getString(5),rs.getTimestamp(6).getTime()), id);
- if (rows.isEmpty()) throw new ServiceException("字段模板不存在");
- return rows.get(0);
- }
- public long insert(String name, List<Map<String,Object>> fields, long userId, String userName) {
+
+ /** 保存已验证的结构;同类同名模板须由用户明确选择更新,不隐式覆盖。 */
+ public long insert(String name, List<CompareField> fields, long userId, String userName) {
try {
var key = new org.springframework.jdbc.support.GeneratedKeyHolder();
- jdbc.update(c -> { var s=c.prepareStatement("insert into t_compare_report_field_template(template_name,template_type,fields_json,creator_id,creator_name,created_at_utc,updated_at_utc) values(?,'compare',?,?,?,?,?)", java.sql.Statement.RETURN_GENERATED_KEYS); s.setString(1,name); s.setString(2,JSON.toJSONString(fields)); s.setLong(3,userId); s.setString(4,userName); var now=new Timestamp(System.currentTimeMillis()); s.setTimestamp(5,now); s.setTimestamp(6,now); return s; }, key);
+ jdbc.update(connection -> {
+ var statement = connection.prepareStatement("insert into t_compare_report_field_template(template_name,template_type,fields_json,creator_id,creator_name,created_at_utc,updated_at_utc) values(?,'compare',?,?,?,?,?)", java.sql.Statement.RETURN_GENERATED_KEYS);
+ statement.setString(1, name); statement.setString(2, JSON.toJSONString(fields));
+ statement.setLong(3, userId); statement.setString(4, userName);
+ var now = new Timestamp(System.currentTimeMillis());
+ statement.setTimestamp(5, now); statement.setTimestamp(6, now);
+ return statement;
+ }, key);
return key.getKey().longValue();
} catch (org.springframework.dao.DuplicateKeyException e) { throw new ServiceException("模板名称已存在"); }
}
+
+ /** 授权编辑只替换模板结构,不改变既有报表或模板创建人。 */
+ public void update(long id, String name, List<CompareField> fields) {
+ try {
+ if (jdbc.update("update t_compare_report_field_template set template_name=?,fields_json=?,updated_at_utc=? where template_id=? and template_type='compare'",
+ name, JSON.toJSONString(fields), new Timestamp(System.currentTimeMillis()), id) == 0)
+ throw new ServiceException("字段模板不存在");
+ } catch (org.springframework.dao.DuplicateKeyException e) { throw new ServiceException("模板名称已存在"); }
+ }
}
diff --git a/battery-business/src/main/java/com/battery/business/report/CompareFieldTemplateRequest.java b/battery-business/src/main/java/com/battery/business/report/CompareFieldTemplateRequest.java
new file mode 100644
index 0000000..dfd2d4d
--- /dev/null
+++ b/battery-business/src/main/java/com/battery/business/report/CompareFieldTemplateRequest.java
@@ -0,0 +1,19 @@
+package com.battery.business.report;
+
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.ruoyi.common.exception.ServiceException;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import java.util.List;
+
+/** 模板写入只接收名称与有序结构,不接收整份报表。 */
+public record CompareFieldTemplateRequest(@NotBlank @Size(max = 100) String name,
+ @NotNull @Size(min = 1, max = 100) List<@NotNull @Valid CompareField> fields)
+{
+ /** 顶层也拒绝来源选择、人工值等非结构属性。 */
+ @JsonAnySetter
+ public void rejectUnknown(String field, Object value)
+ { throw new ServiceException("字段模板不允许提交属性:" + field); }
+}
diff --git a/battery-business/src/main/java/com/battery/business/report/CompareFieldTemplateService.java b/battery-business/src/main/java/com/battery/business/report/CompareFieldTemplateService.java
new file mode 100644
index 0000000..1fdc87f
--- /dev/null
+++ b/battery-business/src/main/java/com/battery/business/report/CompareFieldTemplateService.java
@@ -0,0 +1,51 @@
+package com.battery.business.report;
+
+import com.battery.business.domain.TBatteryGroupField;
+import com.battery.business.service.ITBatteryGroupFieldService;
+import com.ruoyi.common.exception.ServiceException;
+import org.springframework.stereotype.Service;
+import java.util.HashSet;
+import java.util.List;
+
+/** 模板复用共享定义的编号;结构校验属于业务层,不交给 REST 控制器。 */
+@Service
+public class CompareFieldTemplateService
+{
+ private final CompareFieldTemplateRepository templates;
+ private final ITBatteryGroupFieldService definitions;
+ public CompareFieldTemplateService(CompareFieldTemplateRepository templates, ITBatteryGroupFieldService definitions)
+ { this.templates = templates; this.definitions = definitions; }
+
+ /** 名称不重复且共享字段仍有效时,新增不含数据值的模板。 */
+ public long create(CompareFieldTemplateRequest request, long userId, String userName)
+ {
+ var fields = validate(request);
+ return templates.insert(request.name().trim(), fields, userId, userName);
+ }
+
+ /** 修改模板仅影响后续主动应用的草稿,既有版本不重新解析模板。 */
+ public void update(long id, CompareFieldTemplateRequest request)
+ {
+ var fields = validate(request);
+ templates.update(id, request.name().trim(), fields);
+ }
+
+ private List<CompareField> validate(CompareFieldTemplateRequest request)
+ {
+ if (request.name() == null || request.name().isBlank() || request.name().length() > 100
+ || request.fields() == null || request.fields().isEmpty() || request.fields().size() > 100
+ || request.fields().stream().anyMatch(java.util.Objects::isNull)) throw new ServiceException("模板名称和字段不能为空");
+ var names = new HashSet<String>();
+ var ids = new HashSet<Long>();
+ var current = definitions.selectBatteryGroupFieldList(new TBatteryGroupField());
+ return request.fields().stream().map(field -> {
+ if (!names.add(field.name())) throw new ServiceException("模板字段不能重复");
+ if (!"battery".equals(field.category())) return field;
+ var definition = current.stream().filter(item -> field.fieldDefinitionId() == null
+ ? item.getFieldName().equals(field.name()) : item.getFieldId().equals(field.fieldDefinitionId())).findFirst()
+ .orElseThrow(() -> new ServiceException("共享字段不存在,请重新选择电池组字段:" + field.name()));
+ if (!ids.add(definition.getFieldId())) throw new ServiceException("共享字段不能重复");
+ return new CompareField(field.name(), field.category(), field.unit(), definition.getFieldId(), field.displayName());
+ }).toList();
+ }
+}
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 986d89e..0897f8b 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
@@ -21,7 +21,8 @@
public record Settings(@NotNull List<String> metrics, @NotNull List<String> seriesColors,
@NotNull List<String> batteryTemplateFields, @NotNull List<String> specialFields,
- @NotNull Map<String, @NotNull Map<String, String>> detailFieldValues)
+ @NotNull Map<String, @NotNull Map<String, String>> detailFieldValues,
+ List<@NotNull @Valid CompareField> fieldStructure)
{
/** 展示设置不能夹带测量快照或其他未支持的修改。 */
@com.fasterxml.jackson.annotation.JsonAnySetter
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 13140ec..c9ee39f 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
@@ -93,10 +93,10 @@
throw new ServiceException("请至少添加两条比对项");
validateSettings(request.settings());
List<Item> items = preview(request.selections()).stream().map(item -> new Item(item.source(), item.zeroTime(), item.records(), item.logicalMapping(),
- item.businessFields().stream().filter(field -> request.settings().batteryTemplateFields().contains(field.getFieldName())).toList())).toList();
+ item.businessFields().stream().filter(field -> selectedBusinessField(field, request.settings())).toList())).toList();
Map<String, Object> snapshot = new LinkedHashMap<>();
snapshot.put("name", request.name().trim()); snapshot.put("description", request.description());
- snapshot.put("experimentName", request.experimentName()); snapshot.put("items", items); snapshot.put("settings", request.settings());
+ snapshot.put("experimentName", request.experimentName()); snapshot.put("items", items); snapshot.put("settings", snapshotSettings(request.settings()));
snapshot.put("units", Map.of("voltage", "V", "current", "A", "temperature", "℃", "positiveVoltage", "V", "negativeVoltage", "V", "capacity", "Ah", "energy", "Wh"));
snapshot.put("displayUnits", Map.of("voltage", "V", "current", "mA", "temperature", "℃", "positiveVoltage", "V", "negativeVoltage", "V", "capacity", "mAh", "energy", "mWh"));
snapshot.put("timeUnit", "epoch-milliseconds");
@@ -127,7 +127,7 @@
var snapshot = currentSnapshot(id, request.baseVersionNo());
validateSettings(request.settings());
snapshot.put("name", request.name().trim()); snapshot.put("description", request.description());
- snapshot.put("experimentName", request.experimentName()); snapshot.put("settings", request.settings());
+ snapshot.put("experimentName", request.experimentName()); snapshot.put("settings", snapshotSettings(request.settings()));
reports.append(id, request.baseVersionNo() + 1, request.name().trim(), JSON.toJSONString(snapshot, JSONWriter.Feature.WriteMapNullValue),
operatorId, operatorName, request.changeSummary().trim());
return get(id);
@@ -142,7 +142,7 @@
var settings = JSON.parseObject(JSON.toJSONString(snapshot.get("settings")), CompareReportRequest.Settings.class);
var updated = preview(items.stream().map(item -> item.source().selection()).toList()).stream()
.map(item -> new Item(item.source(), item.zeroTime(), item.records(), item.logicalMapping(),
- item.businessFields().stream().filter(field -> settings.batteryTemplateFields().contains(field.getFieldName())).toList())).toList();
+ item.businessFields().stream().filter(field -> selectedBusinessField(field, settings)).toList())).toList();
snapshot.put("items", updated);
reports.append(id, request.baseVersionNo() + 1, snapshot.getString("name"), JSON.toJSONString(snapshot, JSONWriter.Feature.WriteMapNullValue),
operatorId, operatorName, "按最新来源生成:" + request.changeSummary().trim());
@@ -176,6 +176,33 @@
List<String> fields = new ArrayList<>(settings.batteryTemplateFields()); fields.addAll(settings.specialFields());
if (fields.stream().anyMatch(field -> field == null || field.isBlank() || field.length() > 100)
|| new HashSet<>(fields).size() != fields.size()) throw new ServiceException("报表字段无效或重复");
+ if (settings.fieldStructure() != null)
+ {
+ var structure = settings.fieldStructure();
+ if (structure.stream().anyMatch(Objects::isNull) || structure.size() != fields.size()
+ || !new HashSet<>(structure.stream().map(CompareField::name).toList()).equals(new HashSet<>(fields))
+ || structure.stream().anyMatch(field -> "battery".equals(field.category()) != settings.batteryTemplateFields().contains(field.name())))
+ throw new ServiceException("报表字段结构与所选字段不一致");
+ }
+ }
+
+ private boolean selectedBusinessField(CurrentBatteryField value, CompareReportRequest.Settings settings)
+ {
+ // 新结构按共享定义身份归属,字段改名不借用另一个同名字段的值;旧版本继续按其冻结名称读取。
+ if (settings.fieldStructure() == null) return settings.batteryTemplateFields().contains(value.getFieldName());
+ return settings.fieldStructure().stream().anyMatch(field -> "battery".equals(field.category())
+ && (field.fieldDefinitionId() == null ? field.name().equals(value.getFieldName()) : field.fieldDefinitionId().equals(value.getFieldDefinitionId())));
+ }
+
+ private Map<String, Object> snapshotSettings(CompareReportRequest.Settings settings)
+ {
+ var result = new LinkedHashMap<String, Object>();
+ result.put("metrics", settings.metrics()); result.put("seriesColors", settings.seriesColors());
+ result.put("batteryTemplateFields", settings.batteryTemplateFields()); result.put("specialFields", settings.specialFields());
+ result.put("detailFieldValues", settings.detailFieldValues());
+ // 独立复制结构而非保存模板引用,重开、编辑与打印均不重新读取模板或共享定义。
+ if (settings.fieldStructure() != null) result.put("fieldStructure", settings.fieldStructure());
+ return result;
}
private List<Candidate> candidates(TTest test, List<Map<String, Object>> records)
diff --git a/battery-business/src/main/resources/mapper/battery/TBatteryGroupFieldMapper.xml b/battery-business/src/main/resources/mapper/battery/TBatteryGroupFieldMapper.xml
index 8d77e46..56007ea 100644
--- a/battery-business/src/main/resources/mapper/battery/TBatteryGroupFieldMapper.xml
+++ b/battery-business/src/main/resources/mapper/battery/TBatteryGroupFieldMapper.xml
@@ -102,5 +102,7 @@
where field_id = #{fieldId} and del_flag = 0)
+ (select count(1) from t_battery_group_relation_field
where battery_group_field_id = #{fieldId} and del_flag = '0')
+ + (select count(1) from t_compare_report_field_template
+ where template_type = 'compare' and JSON_CONTAINS(fields_json, JSON_OBJECT('fieldDefinitionId', #{fieldId})))
</select>
</mapper>
diff --git a/ruoyi-admin/src/main/java/com/battery/business/web/CompareReportController.java b/ruoyi-admin/src/main/java/com/battery/business/web/CompareReportController.java
index 11d5462..755e479 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/web/CompareReportController.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/web/CompareReportController.java
@@ -5,9 +5,10 @@
import com.battery.business.report.CompareReportRequest;
import com.battery.business.report.CompareReportEditRequest;
import com.battery.business.report.CompareFieldTemplateRepository;
+import com.battery.business.report.CompareFieldTemplateRequest;
+import com.battery.business.report.CompareFieldTemplateService;
import jakarta.validation.Valid;
import java.util.List;
-import java.util.Map;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -16,6 +17,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
@@ -27,23 +29,29 @@
private final CompareReportRepository reports;
private final CompareReportService service;
private final CompareFieldTemplateRepository templates;
- public CompareReportController(CompareReportRepository reports, CompareReportService service, CompareFieldTemplateRepository templates)
- { this.reports = reports; this.service = service; this.templates = templates; }
+ private final CompareFieldTemplateService templateService;
+ public CompareReportController(CompareReportRepository reports, CompareReportService service, CompareFieldTemplateRepository templates, CompareFieldTemplateService templateService)
+ { this.reports = reports; this.service = service; this.templates = templates; this.templateService = templateService; }
- @PreAuthorize("@ss.hasPermi('system:compare-report:query')")
+ /** 查看或编写报表均可读取模板,不额外要求测试或共享字段管理权限。 */
+ @PreAuthorize("@ss.hasAnyPermi('system:compare-report:query,system:compare-report:add,system:compare-report:edit')")
@GetMapping("/templates")
public AjaxResult templates() { return success(templates.list()); }
+ /** 模板新增复用报表新增权限;只接收经过验证的字段结构。 */
@PreAuthorize("@ss.hasPermi('system:compare-report:add')")
@PostMapping("/templates")
- public AjaxResult createTemplate(@RequestBody Map<String, Object> body)
+ public AjaxResult createTemplate(@Valid @RequestBody CompareFieldTemplateRequest request)
{
- String name = String.valueOf(body.getOrDefault("name", "")).trim();
- Object fields = body.get("fields");
- if (name.isBlank() || !(fields instanceof List<?> list) || list.isEmpty() || list.size() > 100) throw new com.ruoyi.common.exception.ServiceException("模板名称和字段不能为空");
- return success().put("id", templates.insert(name, (List<Map<String,Object>>) (List<?>) list, getUserId(), getUsername()));
+ return success().put("id", templateService.create(request, getUserId(), getUsername()));
}
+ /** 模板更新复用报表编辑权限,允许跨创建人协作。 */
+ @PreAuthorize("@ss.hasPermi('system:compare-report:edit')")
+ @PutMapping("/templates/{id}")
+ public AjaxResult updateTemplate(@PathVariable long id, @Valid @RequestBody CompareFieldTemplateRequest request)
+ { templateService.update(id, request); return success(); }
+
/** 查询报表摘要;筛选与分页沿用现有列表交互。 */
@PreAuthorize("@ss.hasPermi('system:compare-report:query')")
@GetMapping("/list")
--
Gitblit v1.7.1