yeg
2026-09-06 37600a85b79387ab6afa7bf342eedbf938331e1d
实现对比报表快照编辑与历史版本

普通编辑基于最新快照追加版本,主动更新来源使用独立接口。补齐版本历史、编辑权限、测量字段拒绝与事务回滚边界。
2个文件已添加
4个文件已修改
162 ■■■■■ 已修改文件
battery-business/src/main/java/com/battery/business/report/CompareReportEditRequest.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/report/CompareReportRepository.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/report/CompareReportRequest.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/report/CompareReportService.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/battery/business/web/CompareReportController.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/db/migration-final/V11__compare_report_edit_permission.sql 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/report/CompareReportEditRequest.java
New file
@@ -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); }
    }
}
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)
    {
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); }
    }
}
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");
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())); }
}
ruoyi-admin/src/main/resources/db/migration-final/V11__compare_report_edit_permission.sql
New file
@@ -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';