From 37600a85b79387ab6afa7bf342eedbf938331e1d Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期日, 06 九月 2026 17:37:28 +0800
Subject: [PATCH] 实现对比报表快照编辑与历史版本

---
 battery-business/src/main/java/com/battery/business/report/CompareReportEditRequest.java  |   33 +++++++++++
 battery-business/src/main/java/com/battery/business/report/CompareReportRepository.java   |   41 +++++++++++--
 battery-business/src/main/java/com/battery/business/report/CompareReportRequest.java      |    8 ++
 battery-business/src/main/java/com/battery/business/report/CompareReportService.java      |   48 +++++++++++++++
 ruoyi-admin/src/main/resources/db/migration-final/V11__compare_report_edit_permission.sql |    4 +
 ruoyi-admin/src/main/java/com/battery/business/web/CompareReportController.java           |   28 +++++++++
 6 files changed, 153 insertions(+), 9 deletions(-)

diff --git a/battery-business/src/main/java/com/battery/business/report/CompareReportEditRequest.java b/battery-business/src/main/java/com/battery/business/report/CompareReportEditRequest.java
new file mode 100644
index 0000000..c5d9e5b
--- /dev/null
+++ b/battery-business/src/main/java/com/battery/business/report/CompareReportEditRequest.java
@@ -0,0 +1,33 @@
+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.Positive;
+import jakarta.validation.constraints.Size;
+
+/** 普通编辑只允许展示内容,不能提交来源身份或测量快照。 */
+public record CompareReportEditRequest(
+        @Positive int baseVersionNo,
+        @NotBlank @Size(max = 100) String name,
+        @Size(max = 2000) String description,
+        @Size(max = 500) String experimentName,
+        @NotNull @Valid CompareReportRequest.Settings settings,
+        @NotBlank @Size(max = 500) String changeSummary)
+{
+    /** 拒绝越过编辑边界的字段,避免将篡改测量请求报告为保存成功。 */
+    @JsonAnySetter
+    public void rejectUnknown(String field, Object value)
+    { throw new ServiceException("报表编辑不允许提交字段:" + field); }
+
+    /** 主动更新只携带已有基准和原因,来源范围由后端快照确定。 */
+    public record Refresh(@Positive int baseVersionNo, @NotBlank @Size(max = 480) String changeSummary)
+    {
+        /** 来源和测量不得由调用方替换。 */
+        @JsonAnySetter
+        public void rejectUnknown(String field, Object value)
+        { throw new ServiceException("更新来源不允许提交字段:" + field); }
+    }
+}
diff --git a/battery-business/src/main/java/com/battery/business/report/CompareReportRepository.java b/battery-business/src/main/java/com/battery/business/report/CompareReportRepository.java
index f7ffd21..6c7df00 100644
--- a/battery-business/src/main/java/com/battery/business/report/CompareReportRepository.java
+++ b/battery-business/src/main/java/com/battery/business/report/CompareReportRepository.java
@@ -17,7 +17,8 @@
     public CompareReportRepository(DataSource dataSource) { jdbc = new JdbcTemplate(dataSource); }
 
     public record Summary(long id, String name, String creator, long createdAt) { }
-    public record Version(Summary report, int versionNo, String snapshotJson) { }
+    public record Version(Summary report, int versionNo, String snapshotJson, String operatorName, long versionCreatedAt, String changeSummary) { }
+    public record History(int versionNo, String operatorName, long createdAt, String changeSummary) { }
 
     /** 查询所有已保存报表摘要,允许授权用户跨创建人查看。 */
     public List<Summary> list()
@@ -26,17 +27,45 @@
                 (rs, index) -> new Summary(rs.getLong(1), rs.getString(2), rs.getString(3), rs.getTimestamp(4).getTime()));
     }
 
-    /** 重读首个完整版本;来源不存在不影响结果。 */
+    /** 重读最新完整版本;来源不存在不影响结果。 */
     public Version get(long id)
+    { return get(id, null); }
+
+    /** 读取指定历史版,版本号为空时读取最新版本。 */
+    public Version get(long id, Integer versionNo)
     {
-        var rows = jdbc.query("select r.report_id, r.report_name, r.creator_name, r.created_at_utc, v.version_no, v.snapshot_json "
-                + "from t_compare_report r join t_compare_report_version v on v.report_id=r.report_id and v.version_no=1 where r.report_id=?",
+        var rows = jdbc.query("select r.report_id, r.report_name, r.creator_name, r.created_at_utc, v.version_no, v.snapshot_json, v.operator_name, v.created_at_utc, v.change_summary "
+                + "from t_compare_report r join t_compare_report_version v on v.report_id=r.report_id where r.report_id=? "
+                + (versionNo == null ? "and v.version_no=(select max(v2.version_no) from t_compare_report_version v2 where v2.report_id=r.report_id)" : "and v.version_no=?"),
                 (rs, index) -> new Version(new Summary(rs.getLong(1), rs.getString(2), rs.getString(3), rs.getTimestamp(4).getTime()),
-                        rs.getInt(5), rs.getString(6)), id);
-        if (rows.isEmpty()) throw new ServiceException("对比报表不存在");
+                        rs.getInt(5), rs.getString(6), rs.getString(7), rs.getTimestamp(8).getTime(), rs.getString(9)),
+                versionNo == null ? new Object[] { id } : new Object[] { id, versionNo });
+        if (rows.isEmpty()) throw new ServiceException("对比报表或版本不存在");
         return rows.get(0);
     }
 
+    /** 历史摘要只来自正式版本,不返回测量快照。 */
+    public List<History> history(long id)
+    {
+        return jdbc.query("select version_no,operator_name,created_at_utc,change_summary from t_compare_report_version where report_id=? order by version_no desc",
+                (rs, index) -> new History(rs.getInt(1), rs.getString(2), rs.getTimestamp(3).getTime(), rs.getString(4)), id);
+    }
+
+    /** 业务事务内锁定报表,版本号分配与旧基准拒绝共用此锁。 */
+    public void lock(long id)
+    {
+        if (jdbc.queryForList("select report_id from t_compare_report where report_id=? for update", Long.class, id).isEmpty())
+            throw new ServiceException("对比报表不存在");
+    }
+
+    /** 只追加版本并更新列表名称;业务事务保证两步同时成功。 */
+    public void append(long id, int versionNo, String name, String snapshot, long operatorId, String operatorName, String summary)
+    {
+        jdbc.update("insert into t_compare_report_version(report_id,version_no,snapshot_json,operator_id,operator_name,created_at_utc,change_summary) values(?,?,?,?,?,?,?)",
+                id, versionNo, snapshot, operatorId, operatorName, new Timestamp(System.currentTimeMillis()), summary);
+        jdbc.update("update t_compare_report set report_name=? where report_id=?", name, id);
+    }
+
     /** 由业务事务调用,身份与首个版本任一写入失败必须一起回滚。 */
     public long insert(String name, String snapshot, long operatorId, String operatorName, long createdAt)
     {
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 7672837..986d89e 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,5 +21,11 @@
 
     public record Settings(@NotNull List<String> metrics, @NotNull List<String> seriesColors,
             @NotNull List<String> batteryTemplateFields, @NotNull List<String> specialFields,
-            @NotNull Map<String, Map<String, String>> detailFieldValues) { }
+            @NotNull Map<String, @NotNull Map<String, String>> detailFieldValues)
+    {
+        /** 展示设置不能夹带测量快照或其他未支持的修改。 */
+        @com.fasterxml.jackson.annotation.JsonAnySetter
+        public void rejectUnknown(String field, Object value)
+        { throw new com.ruoyi.common.exception.ServiceException("报表设置不允许提交字段:" + field); }
+    }
 }
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 e953489..f0bdf0b 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
@@ -22,7 +22,7 @@
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-/** 对比来源与首版快照共用同一范围解析,测量始终来自后端当前有效数据。 */
+/** 首次保存和主动更新解析当前来源,普通编辑及历史查看只读取独立快照。 */
 @Service
 public class CompareReportService
 {
@@ -106,14 +106,58 @@
 
     /** 重开只读取保存版本,来源测试、字段或映射已删除也不重新解析。 */
     public Map<String, Object> get(long id)
+    { return get(id, null); }
+
+    /** 指定版本按其冻结内容还原,操作信息来自正式版本记录。 */
+    public Map<String, Object> get(long id, Integer versionNo)
     {
-        var version = reports.get(id);
+        var version = reports.get(id, versionNo);
         Map<String, Object> result = JSON.parseObject(version.snapshotJson());
         result.put("id", id); result.put("versionNo", version.versionNo());
         result.put("creator", version.report().creator()); result.put("createdAt", version.report().createdAt());
+        result.put("operatorName", version.operatorName()); result.put("versionCreatedAt", version.versionCreatedAt());
+        result.put("changeSummary", version.changeSummary());
         return result;
     }
 
+    /** 普通编辑追加展示版本,测量、来源身份和单位完全沿用基准快照。 */
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> edit(long id, CompareReportEditRequest request, long operatorId, String operatorName)
+    {
+        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());
+        reports.append(id, request.baseVersionNo() + 1, request.name().trim(), JSON.toJSONString(snapshot, JSONWriter.Feature.WriteMapNullValue),
+                operatorId, operatorName, request.changeSummary().trim());
+        return get(id);
+    }
+
+    /** 仅主动更新重读来源;人工表格值仍保留在设置中,不覆盖本次测量快照。 */
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> refresh(long id, CompareReportEditRequest.Refresh request, long operatorId, String operatorName)
+    {
+        var snapshot = currentSnapshot(id, request.baseVersionNo());
+        var items = JSON.parseArray(JSON.toJSONString(snapshot.get("items")), Item.class);
+        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();
+        snapshot.put("items", updated);
+        reports.append(id, request.baseVersionNo() + 1, snapshot.getString("name"), JSON.toJSONString(snapshot, JSONWriter.Feature.WriteMapNullValue),
+                operatorId, operatorName, "按最新来源生成:" + request.changeSummary().trim());
+        return get(id);
+    }
+
+    private com.alibaba.fastjson2.JSONObject currentSnapshot(long id, int baseVersionNo)
+    {
+        // 同一报表内串行追加;陈旧页面需重新打开,不从历史版生成回滚版本。
+        reports.lock(id);
+        var current = reports.get(id);
+        if (current.versionNo() != baseVersionNo) throw new ServiceException("报表已有新版本,请重新打开后编辑");
+        return JSON.parseObject(current.snapshotJson());
+    }
+
     private void validateSettings(CompareReportRequest.Settings settings)
     {
         Set<String> metrics = Set.of("voltage", "current", "temperature", "positiveVoltage", "negativeVoltage");
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 9570eaa..318663c 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
@@ -3,6 +3,7 @@
 import com.battery.business.report.CompareReportRepository;
 import com.battery.business.report.CompareReportService;
 import com.battery.business.report.CompareReportRequest;
+import com.battery.business.report.CompareReportEditRequest;
 import jakarta.validation.Valid;
 import java.util.List;
 import com.ruoyi.common.core.controller.BaseController;
@@ -51,4 +52,31 @@
     @PreAuthorize("@ss.hasPermi('system:compare-report:query')")
     @GetMapping("/{id}")
     public AjaxResult get(@PathVariable long id) { return success(service.get(id)); }
+
+    /** 授权查看全部历史版本摘要,不限制创建人。 */
+    @PreAuthorize("@ss.hasPermi('system:compare-report:query')")
+    @GetMapping("/{id}/versions")
+    public AjaxResult history(@PathVariable long id) { return success(reports.history(id)); }
+
+    /** 指定历史版本只读,不依赖来源仍存在。 */
+    @PreAuthorize("@ss.hasPermi('system:compare-report:query')")
+    @GetMapping("/{id}/versions/{versionNo}")
+    public AjaxResult version(@PathVariable long id, @PathVariable int versionNo) { return success(service.get(id, versionNo)); }
+
+    /** 编辑权限可读取最新快照作为基准,无需来源测试权限。 */
+    @PreAuthorize("@ss.hasPermi('system:compare-report:edit')")
+    @GetMapping("/{id}/editing")
+    public AjaxResult editing(@PathVariable long id) { return success(service.get(id)); }
+
+    /** 普通编辑只追加新版本,拒绝调用方提供测量事实。 */
+    @PreAuthorize("@ss.hasPermi('system:compare-report:edit')")
+    @PostMapping("/{id}/versions")
+    public AjaxResult edit(@PathVariable long id, @Valid @RequestBody CompareReportEditRequest request)
+    { return success(service.edit(id, request, getUserId(), getUsername())); }
+
+    /** 独立动作重读当前来源并追加快照,失败由业务事务完整回滚。 */
+    @PreAuthorize("@ss.hasPermi('system:compare-report:edit')")
+    @PostMapping("/{id}/refresh-source")
+    public AjaxResult refresh(@PathVariable long id, @Valid @RequestBody CompareReportEditRequest.Refresh request)
+    { return success(service.refresh(id, request, getUserId(), getUsername())); }
 }
diff --git a/ruoyi-admin/src/main/resources/db/migration-final/V11__compare_report_edit_permission.sql b/ruoyi-admin/src/main/resources/db/migration-final/V11__compare_report_edit_permission.sql
new file mode 100644
index 0000000..0cd3803
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/db/migration-final/V11__compare_report_edit_permission.sql
@@ -0,0 +1,4 @@
+-- 编辑与主动更新共用编辑权限,历史查看沿用查看权限。
+INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time)
+SELECT '编辑',parent_id,3,'#','F','0','0','system:compare-report:edit','system',UTC_TIMESTAMP(3)
+FROM sys_menu WHERE perms='system:compare-report:query';

--
Gitblit v1.7.1