实现对比报表快照编辑与历史版本
普通编辑基于最新快照追加版本,主动更新来源使用独立接口。补齐版本历史、编辑权限、测量字段拒绝与事务回滚边界。
| New file |
| | |
| | | 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); } |
| | | } |
| | | } |
| | |
| | | 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() |
| | |
| | | (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) |
| | | { |
| | |
| | | |
| | | 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); } |
| | | } |
| | | } |
| | |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | /** 对比来源与首版快照共用同一范围解析,测量始终来自后端当前有效数据。 */ |
| | | /** 首次保存和主动更新解析当前来源,普通编辑及历史查看只读取独立快照。 */ |
| | | @Service |
| | | public class CompareReportService |
| | | { |
| | |
| | | |
| | | /** 重开只读取保存版本,来源测试、字段或映射已删除也不重新解析。 */ |
| | | 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"); |
| | |
| | | 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; |
| | |
| | | @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())); } |
| | | } |
| New file |
| | |
| | | -- 编辑与主动更新共用编辑权限,历史查看沿用查看权限。 |
| | | 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'; |