实现化成循环矩阵与独立报表版本
接入真实化成来源、当前字段冻结、人工调整、字段模板和动作权限。复用版本持久化与最小删除日志,保持来源测试和报表相互独立。
| | |
| | | package com.battery.business.report; |
| | | |
| | | import java.sql.Timestamp; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import com.alibaba.fastjson2.JSON; |
| | | import javax.sql.DataSource; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | import org.springframework.jdbc.support.GeneratedKeyHolder; |
| | | import org.springframework.stereotype.Repository; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | |
| | | /** 报表身份和版本存储;重读只访问报表表,不联查来源测试。 */ |
| | | /** 对比报表身份与版本只使用对比表,复用独立版本持久化规则。 */ |
| | | @Repository |
| | | public class CompareReportRepository |
| | | public class CompareReportRepository extends ReportVersionRepository |
| | | { |
| | | private final JdbcTemplate jdbc; |
| | | |
| | | 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, String operatorName, long versionCreatedAt, String changeSummary) { } |
| | | public record History(int versionNo, String operatorName, long createdAt, String changeSummary) { } |
| | | |
| | | /** 查询所有已保存报表摘要,允许授权用户跨创建人查看。 */ |
| | | public List<Summary> list() |
| | | { |
| | | return jdbc.query("select report_id, report_name, creator_name, created_at_utc from t_compare_report order by created_at_utc desc, report_id desc", |
| | | (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, 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), 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 delete(long id, long operatorId, String operatorName) |
| | | { |
| | | String name = jdbc.queryForObject("select report_name from t_compare_report where report_id=?", String.class, id); |
| | | jdbc.update("delete from t_compare_report_version where report_id=?", id); |
| | | jdbc.update("delete from t_compare_report where report_id=?", id); |
| | | // 不走异步 @Log,日志失败必须回滚删除;只保留身份,不读取或记录测量快照。 |
| | | jdbc.update("insert into sys_oper_log(title,business_type,method,request_method,operator_type,oper_name,oper_url,oper_param,status,oper_time) values(?,3,?,'DELETE',1,?,?,?,0,?)", |
| | | "对比报表删除", "compare-report.delete", operatorName, "/system/compare-report/" + id, |
| | | JSON.toJSONString(Map.of("reportId", id, "reportName", name, "operatorId", operatorId)), new Timestamp(System.currentTimeMillis())); |
| | | } |
| | | |
| | | /** 只追加版本并更新列表名称;业务事务保证两步同时成功。 */ |
| | | 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) |
| | | { |
| | | var keys = new GeneratedKeyHolder(); |
| | | jdbc.update(connection -> { |
| | | var statement = connection.prepareStatement("insert into t_compare_report(report_name,creator_id,creator_name,created_at_utc) values(?,?,?,?)", java.sql.Statement.RETURN_GENERATED_KEYS); |
| | | statement.setString(1, name); statement.setLong(2, operatorId); statement.setString(3, operatorName); |
| | | statement.setTimestamp(4, new Timestamp(createdAt)); |
| | | return statement; |
| | | }, keys); |
| | | long id = keys.getKey().longValue(); |
| | | jdbc.update("insert into t_compare_report_version(report_id,version_no,snapshot_json,operator_id,operator_name,created_at_utc,change_summary) values(?,1,?,?,?,?,?)", |
| | | id, snapshot, operatorId, operatorName, new Timestamp(createdAt), "首次保存对比报表"); |
| | | return id; |
| | | } |
| | | public CompareReportRepository(DataSource dataSource) { super(dataSource, "compare", "对比"); } |
| | | } |
| New file |
| | |
| | | package com.battery.business.report; |
| | | |
| | | import java.util.Map; |
| | | import com.fasterxml.jackson.annotation.JsonAnySetter; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | |
| | | /** 化成字段结构不携带数据;循环测量单位固定,未知公式仅允许人工项。 */ |
| | | public record FormationField(String key, String scope, String kind, String displayName, String unit, Long fieldDefinitionId) |
| | | { |
| | | public static final Map<String, String> METRIC_UNITS = Map.of("chargeCapacity", "mAh", "dischargeCapacity", "mAh", |
| | | "chargeEnergy", "mWh", "dischargeEnergy", "mWh", "efficiency", "%", "energyEfficiency", "%"); |
| | | |
| | | public FormationField |
| | | { |
| | | if (key == null || key.isBlank() || key.length() > 100 || key.contains("|") |
| | | || !("detail".equals(scope) || "cycle".equals(scope)) |
| | | || !("battery".equals(kind) || "metric".equals(kind) || "manual".equals(kind))) |
| | | throw new ServiceException("化成字段身份或分类无效"); |
| | | displayName = displayName == null ? key : displayName.trim(); |
| | | unit = unit == null ? "" : unit.trim(); |
| | | if (displayName.isBlank() || displayName.length() > 100 || unit.length() > 30 |
| | | || ("battery".equals(kind) && !"detail".equals(scope)) |
| | | || (fieldDefinitionId != null && (fieldDefinitionId <= 0 || !"battery".equals(kind))) |
| | | || ("metric".equals(kind) && (!"cycle".equals(scope) || !unit.equals(METRIC_UNITS.get(key))))) |
| | | throw new ServiceException("化成字段展示名称、单位或共享身份无效"); |
| | | if ((METRIC_UNITS.containsKey(key) && !"metric".equals(kind)) |
| | | || (java.util.Set.of("Z", "H", "F", "A", "B").contains(key) && !"battery".equals(kind)) |
| | | || (java.util.Set.of("FN", "C", "method2", "manual", "chargePerGram", "chargePerGramTotal", "capacity1v", "capacity1vf", |
| | | "dischargeInflection", "dischargeInflectionTotal").contains(key) && !"manual".equals(kind))) |
| | | throw new ServiceException("固定字段不能改变来源分类"); |
| | | } |
| | | |
| | | /** 模板和字段设置均拒绝混入测量、测试选择或人工值。 */ |
| | | @JsonAnySetter |
| | | public void rejectUnknown(String field, Object value) { throw new ServiceException("化成字段不允许提交属性:" + field); } |
| | | } |
| New file |
| | |
| | | package com.battery.business.report; |
| | | |
| | | import java.sql.Timestamp; |
| | | import java.util.List; |
| | | import javax.sql.DataSource; |
| | | import com.alibaba.fastjson2.JSON; |
| | | 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 org.springframework.jdbc.core.JdbcTemplate; |
| | | import org.springframework.jdbc.support.GeneratedKeyHolder; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | /** 化成模板仅存详情和循环字段结构,独立于对比模板及所有报表版本。 */ |
| | | @Service |
| | | public class FormationFieldTemplateService |
| | | { |
| | | private final JdbcTemplate jdbc; |
| | | private final FormationReportService reports; |
| | | public FormationFieldTemplateService(DataSource source, FormationReportService reports) |
| | | { this.jdbc = new JdbcTemplate(source); this.reports = reports; } |
| | | |
| | | public record Template(long id, String name, String type, List<FormationField> fields, String creator, long updatedAt) { } |
| | | public record Request(@NotBlank @Size(max = 100) String name, |
| | | @NotNull @Size(min = 1, max = 100) List<@NotNull @Valid FormationField> fields) |
| | | { |
| | | /** 模板不接受整份报表、来源选择或数据值。 */ |
| | | @JsonAnySetter |
| | | public void rejectUnknown(String field, Object value) { throw new ServiceException("化成模板不允许提交属性:" + field); } |
| | | } |
| | | |
| | | /** 查看该类型全部模板,保存顺序即应用顺序。 */ |
| | | public List<Template> list() |
| | | { |
| | | return jdbc.query("select template_id,template_name,fields_json,creator_name,updated_at_utc from t_formation_report_field_template where template_type='formation' order by template_name,template_id", |
| | | (rs, index) -> new Template(rs.getLong(1), rs.getString(2), "formation", JSON.parseArray(rs.getString(3), FormationField.class), rs.getString(4), rs.getTimestamp(5).getTime())); |
| | | } |
| | | |
| | | /** 新增前解析共享定义身份,同名模板须显式更新。 */ |
| | | public long create(Request request, long userId, String userName) |
| | | { |
| | | var fields = reports.validateFields(request.fields(), true); |
| | | var key = new GeneratedKeyHolder(); |
| | | try |
| | | { |
| | | jdbc.update(connection -> { |
| | | var statement = connection.prepareStatement("insert into t_formation_report_field_template(template_name,template_type,fields_json,creator_id,creator_name,created_at_utc,updated_at_utc) values(?,'formation',?,?,?,?,?)", java.sql.Statement.RETURN_GENERATED_KEYS); |
| | | statement.setString(1, request.name().trim()); 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 error) { throw new ServiceException("模板名称已存在"); } |
| | | } |
| | | |
| | | /** 修改只影响后续主动应用,不访问报表版本;允许跨创建人协作。 */ |
| | | public void update(long id, Request request) |
| | | { |
| | | var fields = reports.validateFields(request.fields(), true); |
| | | try |
| | | { |
| | | if (jdbc.update("update t_formation_report_field_template set template_name=?,fields_json=?,updated_at_utc=? where template_id=? and template_type='formation'", |
| | | request.name().trim(), JSON.toJSONString(fields), new Timestamp(System.currentTimeMillis()), id) == 0) |
| | | throw new ServiceException("化成字段模板不存在"); |
| | | } |
| | | catch (org.springframework.dao.DuplicateKeyException error) { throw new ServiceException("模板名称已存在"); } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.battery.business.report; |
| | | |
| | | import javax.sql.DataSource; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | | /** 化成身份与版本使用独立表,复用对比报表的事务内存储规则。 */ |
| | | @Repository |
| | | public class FormationReportRepository extends ReportVersionRepository |
| | | { |
| | | public FormationReportRepository(DataSource dataSource) { super(dataSource, "formation", "化成"); } |
| | | } |
| New file |
| | |
| | | package com.battery.business.report; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | 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 FormationReportRequest(@NotBlank @Size(max = 100) String name, @Size(max = 2000) String description, |
| | | @NotNull @Size(min = 1) List<@NotNull @Valid Selection> selections, @NotNull @Valid Settings settings) |
| | | { |
| | | public record Selection(@NotNull @Positive Long testId, @NotNull @Positive Long testBatteryId) { } |
| | | |
| | | /** 值按测试、电池、循环、字段归属,只属于报表展示,不反写测量快照。 */ |
| | | public record Settings(@NotNull @Size(min = 1, max = 100) List<@NotNull @Valid FormationField> fields, |
| | | @NotNull Map<String, @NotNull String> values) |
| | | { |
| | | /** 展示设置拒绝来源和测量快照。 */ |
| | | @JsonAnySetter |
| | | public void rejectUnknown(String field, Object value) { throw new ServiceException("化成设置不允许提交属性:" + field); } |
| | | } |
| | | |
| | | /** 编辑保持后端冻结来源范围,不接收替代来源或测量。 */ |
| | | public record Edit(@Positive int baseVersionNo, @NotBlank @Size(max = 100) String name, |
| | | @Size(max = 2000) String description, @NotNull @Valid Settings settings, |
| | | @NotBlank @Size(max = 500) String changeSummary) |
| | | { |
| | | /** 拒绝借普通编辑替换冻结来源或测量。 */ |
| | | @JsonAnySetter |
| | | public void rejectUnknown(String field, Object value) { throw new ServiceException("化成编辑不允许提交属性:" + field); } |
| | | } |
| | | |
| | | /** 新建只接收定义的展示内容与真实来源身份。 */ |
| | | @JsonAnySetter |
| | | public void rejectUnknown(String field, Object value) { throw new ServiceException("化成报表不允许提交属性:" + field); } |
| | | } |
| New file |
| | |
| | | package com.battery.business.report; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.MathContext; |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.HashSet; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | import java.util.stream.Stream; |
| | | import com.alibaba.fastjson2.JSON; |
| | | import com.alibaba.fastjson2.JSONObject; |
| | | import com.alibaba.fastjson2.JSONWriter; |
| | | import com.battery.business.domain.CurrentBatteryField; |
| | | import com.battery.business.domain.TBatteryGroupField; |
| | | import com.battery.business.domain.TTest; |
| | | import com.battery.business.domain.TTestBattery; |
| | | import com.battery.business.report.FormationReportRequest.Selection; |
| | | import com.battery.business.report.FormationReportRequest.Settings; |
| | | import com.battery.business.service.ITBatteryGroupFieldService; |
| | | import com.battery.business.service.ITTestService; |
| | | import com.battery.business.service.ITestDataQueryService; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | /** 化成按实际循环并集展示;来源读取仅发生在预览、首次保存及主动更新。 */ |
| | | @Service |
| | | public class FormationReportService |
| | | { |
| | | private final ITTestService tests; |
| | | private final ITestDataQueryService data; |
| | | private final FormationReportRepository reports; |
| | | private final ITBatteryGroupFieldService definitions; |
| | | |
| | | public FormationReportService(ITTestService tests, ITestDataQueryService data, |
| | | FormationReportRepository reports, ITBatteryGroupFieldService definitions) |
| | | { this.tests = tests; this.data = data; this.reports = reports; this.definitions = definitions; } |
| | | |
| | | public record Candidate(Selection selection, String name, String type, long groupId, String batteryGroup, String batteryNo) { } |
| | | public record Item(Candidate source, List<Integer> cycleNos, List<Map<String, Object>> cycles, |
| | | Map<String, Map<String, BigDecimal>> metrics, List<CurrentBatteryField> businessFields) { } |
| | | public record Matrix(List<Item> items, List<Integer> cycleNos) { } |
| | | public record Summary(long id, String name, String creator, long createdAt, String testName, String batteryGroup) { } |
| | | |
| | | /** 列表来源名称取自已保存版本,测试删除后仍保留筛选与识别信息。 */ |
| | | public List<Summary> list() |
| | | { |
| | | return reports.list().stream().map(summary -> { |
| | | var sources = items(JSON.parseObject(reports.get(summary.id()).snapshotJson())); |
| | | return new Summary(summary.id(), summary.name(), summary.creator(), summary.createdAt(), |
| | | String.join("、", sources.stream().map(item -> item.source().name()).distinct().toList()), |
| | | String.join("、", sources.stream().map(item -> item.source().batteryGroup()).distinct().toList())); |
| | | }).toList(); |
| | | } |
| | | |
| | | /** 只提供类型名称精确为“化成”的检测完成测试及其真实参与电池。 */ |
| | | public List<Candidate> sources() |
| | | { |
| | | TTest query = new TTest(); query.setTestStatus("COMPLETED"); |
| | | List<Candidate> result = new ArrayList<>(); |
| | | for (TTest summary : tests.selectTestList(query)) |
| | | { |
| | | if (!"化成".equals(summary.getTestTypeName())) continue; |
| | | TTest test = tests.selectTestById(summary.getTestId()); |
| | | if (eligible(test)) for (TTestBattery battery : test.getBatteries()) result.add(candidate(test, battery)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** 校验电池属于所选测试,返回三类持久化记录实际出现的循环号与循环统计。 */ |
| | | public Matrix preview(List<Selection> selections) |
| | | { |
| | | if (selections == null || selections.isEmpty() || selections.contains(null) |
| | | || new HashSet<>(selections).size() != selections.size()) throw new ServiceException("请选择不重复的化成测试电池"); |
| | | Map<Long, TTest> loaded = new HashMap<>(); |
| | | Map<Long, List<Map<String, Object>>> cycles = new HashMap<>(), records = new HashMap<>(), steps = new HashMap<>(); |
| | | List<Item> items = new ArrayList<>(); |
| | | for (Selection selection : selections) |
| | | { |
| | | if (selection.testId() == null || selection.testBatteryId() == null) throw new ServiceException("测试和电池身份不能为空"); |
| | | TTest test = loaded.computeIfAbsent(selection.testId(), tests::selectTestById); |
| | | if (!eligible(test)) throw new ServiceException("仅可选择类型为化成的检测完成测试"); |
| | | TTestBattery battery = test.getBatteries().stream().filter(row -> selection.testBatteryId().equals(row.getTestBatteryId())) |
| | | .findFirst().orElseThrow(() -> new ServiceException("电池不属于所选测试")); |
| | | List<Map<String, Object>> rows = batteryRows(cycles.computeIfAbsent(selection.testId(), data::cycles), battery); |
| | | var actual = Stream.of(rows, batteryRows(records.computeIfAbsent(selection.testId(), data::records), battery), |
| | | batteryRows(steps.computeIfAbsent(selection.testId(), data::steps), battery)).flatMap(List::stream) |
| | | .map(this::cycleNo).filter(no -> no > 0).distinct().sorted().toList(); |
| | | items.add(new Item(candidate(test, battery), actual, rows, metrics(rows, actual), battery.getFields())); |
| | | } |
| | | return new Matrix(items, items.stream().flatMap(item -> item.cycleNos().stream()).distinct().sorted().toList()); |
| | | } |
| | | |
| | | /** 从当前来源冻结所选字段;首版和报表身份在同一事务提交。 */ |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Map<String, Object> create(FormationReportRequest request, long userId, String userName) |
| | | { |
| | | Settings settings = new Settings(validateFields(request.settings().fields(), true), request.settings().values()); |
| | | Matrix matrix = freeze(preview(request.selections()), settings.fields()); |
| | | validateValues(settings, matrix.items()); |
| | | var snapshot = new LinkedHashMap<String, Object>(); |
| | | snapshot.put("name", request.name().trim()); snapshot.put("description", request.description()); |
| | | snapshot.put("items", matrix.items()); snapshot.put("cycleNos", matrix.cycleNos()); snapshot.put("settings", settings); |
| | | snapshot.put("units", Map.of("chargeCapacity", "Ah", "dischargeCapacity", "Ah", "chargeEnergy", "Wh", "dischargeEnergy", "Wh", "efficiency", "%", "energyEfficiency", "%")); |
| | | snapshot.put("displayUnits", FormationField.METRIC_UNITS); snapshot.put("timeUnit", "epoch-milliseconds"); |
| | | return get(reports.insert(request.name().trim(), json(snapshot), userId, userName, System.currentTimeMillis()), null); |
| | | } |
| | | |
| | | /** 当前或历史版本只读取独立快照,来源删除后仍能重开。 */ |
| | | public Map<String, Object> get(long id, Integer versionNo) |
| | | { |
| | | var version = reports.get(id, versionNo); |
| | | var 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, FormationReportRequest.Edit request, long userId, String userName) |
| | | { |
| | | var snapshot = current(id, request.baseVersionNo()); |
| | | validateFields(request.settings().fields(), false); |
| | | validateValues(request.settings(), items(snapshot)); |
| | | snapshot.put("name", request.name().trim()); snapshot.put("description", request.description()); snapshot.put("settings", request.settings()); |
| | | reports.append(id, request.baseVersionNo() + 1, request.name().trim(), json(snapshot), userId, userName, request.changeSummary().trim()); |
| | | return get(id, null); |
| | | } |
| | | |
| | | /** 主动重读已保存范围,保留仍属于当前循环的人工值;不存在的来源使整次提交失败。 */ |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Map<String, Object> refresh(long id, CompareReportEditRequest.Refresh request, long userId, String userName) |
| | | { |
| | | var snapshot = current(id, request.baseVersionNo()); |
| | | var settings = JSON.parseObject(json(snapshot.get("settings")), Settings.class); |
| | | Matrix matrix = freeze(preview(items(snapshot).stream().map(item -> item.source().selection()).toList()), settings.fields()); |
| | | Set<String> allowed = valueKeys(settings.fields(), matrix.items()); |
| | | Map<String, String> values = new LinkedHashMap<>(); |
| | | settings.values().forEach((key, value) -> { if (allowed.contains(key)) values.put(key, value); }); |
| | | snapshot.put("items", matrix.items()); snapshot.put("cycleNos", matrix.cycleNos()); snapshot.put("settings", new Settings(settings.fields(), values)); |
| | | reports.append(id, request.baseVersionNo() + 1, snapshot.getString("name"), json(snapshot), userId, userName, "按最新来源生成:" + request.changeSummary().trim()); |
| | | return get(id, null); |
| | | } |
| | | |
| | | /** 删除全部化成版本,与最小删除日志在同一事务提交,不删除测试事实。 */ |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void delete(long id, long userId, String userName) { reports.lock(id); reports.delete(id, userId, userName); } |
| | | |
| | | /** 校验有序结构;首次保存及模板解析共享身份,普通编辑沿用冻结身份。 */ |
| | | public List<FormationField> validateFields(List<FormationField> fields, boolean resolveDefinitions) |
| | | { |
| | | if (fields == null || fields.isEmpty() || fields.size() > 100 || fields.contains(null)) throw new ServiceException("化成字段不能为空"); |
| | | var names = new HashSet<String>(); var ids = new HashSet<Long>(); |
| | | var current = resolveDefinitions ? definitions.selectBatteryGroupFieldList(new TBatteryGroupField()) : List.<TBatteryGroupField>of(); |
| | | return fields.stream().map(field -> { |
| | | if (!names.add(field.scope() + "|" + field.key())) throw new ServiceException("化成字段不能重复"); |
| | | if (!"battery".equals(field.kind())) return field; |
| | | Long id = field.fieldDefinitionId(); |
| | | if (resolveDefinitions) |
| | | { |
| | | var definition = current.stream().filter(value -> field.fieldDefinitionId() == null |
| | | ? value.getFieldName().equals(field.key()) : value.getFieldId().equals(field.fieldDefinitionId())).findFirst(); |
| | | if (definition.isEmpty() && !Set.of("Z", "H", "F", "A", "B").contains(field.key())) |
| | | throw new ServiceException("共享字段不存在,请重新选择:" + field.key()); |
| | | if (definition.isPresent()) id = definition.get().getFieldId(); |
| | | } |
| | | if (id != null && !ids.add(id)) throw new ServiceException("共享字段不能重复"); |
| | | return new FormationField(field.key(), field.scope(), field.kind(), field.displayName(), field.unit(), id); |
| | | }).toList(); |
| | | } |
| | | |
| | | private JSONObject current(long id, int baseVersionNo) |
| | | { |
| | | // 与删除共用报表身份锁;旧页面只能重开,不能从陈旧基准覆盖新版本。 |
| | | reports.lock(id); |
| | | var version = reports.get(id); |
| | | if (version.versionNo() != baseVersionNo) throw new ServiceException("报表已有新版本,请重新打开后编辑"); |
| | | return JSON.parseObject(version.snapshotJson()); |
| | | } |
| | | |
| | | private Matrix freeze(Matrix matrix, List<FormationField> fields) |
| | | { |
| | | Set<String> selected = new HashSet<>(fields.stream().filter(field -> "metric".equals(field.kind())).map(FormationField::key).toList()); |
| | | if (selected.contains("energyEfficiency")) selected.addAll(List.of("chargeEnergy", "dischargeEnergy")); |
| | | var frozen = matrix.items().stream().map(item -> { |
| | | var rows = item.cycles().stream().map(row -> { |
| | | Map<String, Object> copy = new LinkedHashMap<>(row); |
| | | FormationField.METRIC_UNITS.keySet().stream().filter(key -> !selected.contains(key)).forEach(copy::remove); |
| | | return copy; |
| | | }).toList(); |
| | | return new Item(item.source(), item.cycleNos(), rows, metrics(rows, item.cycleNos()), item.businessFields().stream() |
| | | .filter(value -> fields.stream().anyMatch(field -> "battery".equals(field.kind()) && (field.fieldDefinitionId() == null |
| | | ? field.key().equals(value.getFieldName()) : field.fieldDefinitionId().equals(value.getFieldDefinitionId())))).toList()); |
| | | }).toList(); |
| | | return new Matrix(frozen, matrix.cycleNos()); |
| | | } |
| | | |
| | | private void validateValues(Settings settings, List<Item> items) |
| | | { |
| | | if (settings.values() == null || !valueKeys(settings.fields(), items).containsAll(settings.values().keySet()) |
| | | || settings.values().values().stream().anyMatch(value -> value == null || value.length() > 500)) |
| | | throw new ServiceException("人工值不属于当前字段、测试电池或实际循环"); |
| | | } |
| | | |
| | | private Set<String> valueKeys(List<FormationField> fields, List<Item> items) |
| | | { |
| | | Set<String> keys = new HashSet<>(); |
| | | for (Item item : items) for (FormationField field : fields) |
| | | { |
| | | String prefix = item.source().selection().testId() + "|" + item.source().selection().testBatteryId() + "|"; |
| | | for (int cycle : "detail".equals(field.scope()) ? List.of(0) : item.cycleNos()) keys.add(prefix + cycle + "|" + field.key()); |
| | | } |
| | | return keys; |
| | | } |
| | | |
| | | private Map<String, Map<String, BigDecimal>> metrics(List<Map<String, Object>> rows, List<Integer> cycleNos) |
| | | { |
| | | Map<String, Map<String, BigDecimal>> result = new LinkedHashMap<>(); |
| | | for (int no : cycleNos) |
| | | { |
| | | var matches = rows.stream().filter(row -> cycleNo(row) == no).toList(); |
| | | Map<String, BigDecimal> values = new LinkedHashMap<>(); |
| | | // 多条统计记录没有已确认的合并口径,保持缺失,不挑任意一条或借用其他循环。 |
| | | if (matches.size() == 1) for (String key : FormationField.METRIC_UNITS.keySet()) |
| | | if (matches.get(0).get(key) instanceof Number number) values.put(key, new BigDecimal(number.toString())); |
| | | BigDecimal charge = values.get("chargeEnergy"), discharge = values.get("dischargeEnergy"); |
| | | if (charge != null && discharge != null && charge.signum() > 0) |
| | | values.put("energyEfficiency", discharge.multiply(BigDecimal.valueOf(100)).divide(charge, MathContext.DECIMAL128)); |
| | | result.put(Integer.toString(no), values); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private boolean eligible(TTest test) { return "COMPLETED".equals(test.getTestStatus()) && "化成".equals(test.getTestTypeName()); } |
| | | private Candidate candidate(TTest test, TTestBattery battery) |
| | | { return new Candidate(new Selection(test.getTestId(), battery.getTestBatteryId()), test.getTestName(), test.getTestTypeName(), battery.getGroupId(), battery.getGroupName(), battery.getBatteryNo()); } |
| | | private List<Map<String, Object>> batteryRows(List<Map<String, Object>> rows, TTestBattery battery) |
| | | { return rows.stream().filter(row -> Objects.equals(battery.getBatteryNo(), row.get("batteryNo"))).toList(); } |
| | | private int cycleNo(Map<String, Object> row) { return row.get("cycleNo") instanceof Number number ? number.intValue() : 0; } |
| | | private List<Item> items(JSONObject snapshot) { return JSON.parseArray(json(snapshot.get("items")), Item.class); } |
| | | private String json(Object value) { return JSON.toJSONString(value, JSONWriter.Feature.WriteMapNullValue); } |
| | | } |
| New file |
| | |
| | | package com.battery.business.report; |
| | | |
| | | import java.sql.Timestamp; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import com.alibaba.fastjson2.JSON; |
| | | import javax.sql.DataSource; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | import org.springframework.jdbc.support.GeneratedKeyHolder; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | |
| | | /** 报表身份和版本存储;重读只访问报表表,不联查来源测试。 */ |
| | | public abstract class ReportVersionRepository |
| | | { |
| | | private final JdbcTemplate jdbc; |
| | | private final String type; |
| | | private final String title; |
| | | |
| | | protected ReportVersionRepository(DataSource dataSource, String type, String title) |
| | | { jdbc = new JdbcTemplate(dataSource); this.type = type; this.title = title; } |
| | | |
| | | // 类型仅由两个仓储构造器指定,SQL 表名不接受请求输入;两类共用已验证的版本和删除规则。 |
| | | private String sql(String statement) { return statement.replace("t_compare_report", "t_" + type + "_report"); } |
| | | |
| | | public record Summary(long id, String name, String creator, long createdAt) { } |
| | | 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() |
| | | { |
| | | return jdbc.query(sql("select report_id, report_name, creator_name, created_at_utc from t_compare_report order by created_at_utc desc, report_id desc"), |
| | | (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(sql("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), 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(title + "报表或版本不存在"); |
| | | return rows.get(0); |
| | | } |
| | | |
| | | /** 历史摘要只来自正式版本,不返回测量快照。 */ |
| | | public List<History> history(long id) |
| | | { |
| | | return jdbc.query(sql("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(sql("select report_id from t_compare_report where report_id=? for update"), Long.class, id).isEmpty()) |
| | | throw new ServiceException(title + "报表不存在"); |
| | | } |
| | | |
| | | /** 仅在业务事务和报表锁内调用;物理清理全部版本,同步保留最小操作日志。 */ |
| | | public void delete(long id, long operatorId, String operatorName) |
| | | { |
| | | String name = jdbc.queryForObject(sql("select report_name from t_compare_report where report_id=?"), String.class, id); |
| | | jdbc.update(sql("delete from t_compare_report_version where report_id=?"), id); |
| | | jdbc.update(sql("delete from t_compare_report where report_id=?"), id); |
| | | // 不走异步 @Log,日志失败必须回滚删除;只保留身份,不读取或记录测量快照。 |
| | | jdbc.update("insert into sys_oper_log(title,business_type,method,request_method,operator_type,oper_name,oper_url,oper_param,status,oper_time) values(?,3,?,'DELETE',1,?,?,?,0,?)", |
| | | title + "报表删除", type + "-report.delete", operatorName, "/system/" + type + "-report/" + id, |
| | | JSON.toJSONString(Map.of("reportId", id, "reportName", name, "operatorId", operatorId)), new Timestamp(System.currentTimeMillis())); |
| | | } |
| | | |
| | | /** 只追加版本并更新列表名称;业务事务保证两步同时成功。 */ |
| | | public void append(long id, int versionNo, String name, String snapshot, long operatorId, String operatorName, String summary) |
| | | { |
| | | jdbc.update(sql("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(sql("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) |
| | | { |
| | | var keys = new GeneratedKeyHolder(); |
| | | jdbc.update(connection -> { |
| | | var statement = connection.prepareStatement(sql("insert into t_compare_report(report_name,creator_id,creator_name,created_at_utc) values(?,?,?,?)"), java.sql.Statement.RETURN_GENERATED_KEYS); |
| | | statement.setString(1, name); statement.setLong(2, operatorId); statement.setString(3, operatorName); |
| | | statement.setTimestamp(4, new Timestamp(createdAt)); |
| | | return statement; |
| | | }, keys); |
| | | long id = keys.getKey().longValue(); |
| | | jdbc.update(sql("insert into t_compare_report_version(report_id,version_no,snapshot_json,operator_id,operator_name,created_at_utc,change_summary) values(?,1,?,?,?,?,?)"), |
| | | id, snapshot, operatorId, operatorName, new Timestamp(createdAt), "首次保存" + title + "报表"); |
| | | return id; |
| | | } |
| | | } |
| | |
| | | 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 count(1) from t_formation_report_field_template |
| | | where template_type = 'formation' and JSON_CONTAINS(fields_json, JSON_OBJECT('fieldDefinitionId', #{fieldId}))) |
| | | </select> |
| | | </mapper> |
| New file |
| | |
| | | package com.battery.business.web; |
| | | |
| | | import com.battery.business.report.FormationFieldTemplateService; |
| | | import com.ruoyi.common.core.controller.BaseController; |
| | | import com.ruoyi.common.core.domain.AjaxResult; |
| | | import jakarta.validation.Valid; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | /** 化成模板沿用化成动作权限,不读取或修改对比模板。 */ |
| | | @RestController |
| | | @RequestMapping("/system/formation-report/templates") |
| | | public class FormationFieldTemplateController extends BaseController |
| | | { |
| | | private final FormationFieldTemplateService service; |
| | | public FormationFieldTemplateController(FormationFieldTemplateService service) { this.service = service; } |
| | | |
| | | /** 查看、新增或编辑报表均可读取模板。 */ |
| | | @GetMapping @PreAuthorize("@ss.hasAnyPermi('system:formation-report:query,system:formation-report:add,system:formation-report:edit')") |
| | | public AjaxResult list() { return success(service.list()); } |
| | | |
| | | /** 新增模板仅接受结构,拒绝值和来源身份。 */ |
| | | @PostMapping @PreAuthorize("@ss.hasPermi('system:formation-report:add')") |
| | | public AjaxResult create(@Valid @RequestBody FormationFieldTemplateService.Request request) |
| | | { return success(service.create(request, getUserId(), getUsername())); } |
| | | |
| | | /** 显式更新所属类型模板,已有报表不随之变化。 */ |
| | | @PutMapping("/{id}") @PreAuthorize("@ss.hasPermi('system:formation-report:edit')") |
| | | public AjaxResult update(@PathVariable long id, @Valid @RequestBody FormationFieldTemplateService.Request request) |
| | | { service.update(id, request); return success(); } |
| | | } |
| New file |
| | |
| | | package com.battery.business.web; |
| | | |
| | | import java.util.List; |
| | | import com.battery.business.report.FormationReportRepository; |
| | | import com.battery.business.report.FormationReportService; |
| | | import com.battery.business.report.FormationReportRequest; |
| | | import com.battery.business.report.CompareReportEditRequest; |
| | | import com.ruoyi.common.core.controller.BaseController; |
| | | import com.ruoyi.common.core.domain.AjaxResult; |
| | | import jakarta.validation.Valid; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | /** 化成报表按所属动作授权,授权用户可以跨创建人协作。 */ |
| | | @RestController |
| | | @RequestMapping("/system/formation-report") |
| | | public class FormationReportController extends BaseController |
| | | { |
| | | private final FormationReportService service; |
| | | private final FormationReportRepository reports; |
| | | public FormationReportController(FormationReportService service, FormationReportRepository reports) |
| | | { this.service = service; this.reports = reports; } |
| | | |
| | | /** 查询真实化成来源,不额外要求测试管理权限。 */ |
| | | @GetMapping("/sources") @PreAuthorize("@ss.hasPermi('system:formation-report:add')") |
| | | public AjaxResult sources() { return success(service.sources()); } |
| | | |
| | | /** 后端校验归属并读取当前循环及业务字段。 */ |
| | | @PostMapping("/preview") @PreAuthorize("@ss.hasPermi('system:formation-report:add')") |
| | | public AjaxResult preview(@RequestBody List<FormationReportRequest.Selection> selections) { return success(service.preview(selections)); } |
| | | |
| | | /** 查询独立报表摘要。 */ |
| | | @GetMapping("/list") @PreAuthorize("@ss.hasPermi('system:formation-report:query')") |
| | | public AjaxResult list() { return success().put("rows", service.list()); } |
| | | |
| | | /** 创建身份及首个完整版本。 */ |
| | | @PostMapping @PreAuthorize("@ss.hasPermi('system:formation-report:add')") |
| | | public AjaxResult create(@Valid @RequestBody FormationReportRequest request) { return success(service.create(request, getUserId(), getUsername())); } |
| | | |
| | | /** 重开最新保存版本,不读取来源测试。 */ |
| | | @GetMapping("/{id}") @PreAuthorize("@ss.hasPermi('system:formation-report:query')") |
| | | public AjaxResult get(@PathVariable long id) { return success(service.get(id, null)); } |
| | | |
| | | /** 编辑权限独立读取最新基准。 */ |
| | | @GetMapping("/{id}/editing") @PreAuthorize("@ss.hasPermi('system:formation-report:edit')") |
| | | public AjaxResult editing(@PathVariable long id) { return success(service.get(id, null)); } |
| | | |
| | | /** 查看历史版本的操作人、时间和说明。 */ |
| | | @GetMapping("/{id}/versions") @PreAuthorize("@ss.hasPermi('system:formation-report:query')") |
| | | public AjaxResult history(@PathVariable long id) { return success(reports.history(id)); } |
| | | |
| | | /** 指定历史版本只读,不提供回滚。 */ |
| | | @GetMapping("/{id}/versions/{versionNo}") @PreAuthorize("@ss.hasPermi('system:formation-report:query')") |
| | | public AjaxResult version(@PathVariable long id, @PathVariable int versionNo) { return success(service.get(id, versionNo)); } |
| | | |
| | | /** 普通编辑不接受来源或测量快照。 */ |
| | | @PostMapping("/{id}/versions") @PreAuthorize("@ss.hasPermi('system:formation-report:edit')") |
| | | public AjaxResult edit(@PathVariable long id, @Valid @RequestBody FormationReportRequest.Edit request) |
| | | { return success(service.edit(id, request, getUserId(), getUsername())); } |
| | | |
| | | /** 明确按当前来源生成新版本。 */ |
| | | @PostMapping("/{id}/refresh-source") @PreAuthorize("@ss.hasPermi('system:formation-report:edit')") |
| | | public AjaxResult refresh(@PathVariable long id, @Valid @RequestBody CompareReportEditRequest.Refresh request) |
| | | { return success(service.refresh(id, request, getUserId(), getUsername())); } |
| | | |
| | | /** 整体删除及最小日志同成同败。 */ |
| | | @DeleteMapping("/{id}") @PreAuthorize("@ss.hasPermi('system:formation-report:remove')") |
| | | public AjaxResult delete(@PathVariable long id) { service.delete(id, getUserId(), getUsername()); return success(); } |
| | | } |
| New file |
| | |
| | | -- 化成报表与来源测试独立;保存和整体删除复用对比版本规则。 |
| | | CREATE TABLE t_formation_report LIKE t_compare_report; |
| | | ALTER TABLE t_formation_report COMMENT='化成报表身份'; |
| | | CREATE TABLE t_formation_report_version LIKE t_compare_report_version; |
| | | ALTER TABLE t_formation_report_version COMMENT='化成报表独立版本'; |
| | | CREATE TABLE t_formation_report_field_template LIKE t_compare_report_field_template; |
| | | ALTER TABLE t_formation_report_field_template |
| | | ALTER COLUMN template_type SET DEFAULT 'formation', COMMENT='化成报表字段模板'; |
| | | |
| | | SET @analysis_menu_id = (SELECT menu_id FROM sys_menu WHERE path='data-analysis' AND parent_id=0 LIMIT 1); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time) |
| | | VALUES ('化成报表',@analysis_menu_id,2,'report-formation','C','0','0','','system',UTC_TIMESTAMP(3)); |
| | | SET @formation_menu_id = LAST_INSERT_ID(); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time) |
| | | VALUES ('查看',@formation_menu_id,1,'#','F','0','0','system:formation-report:query','system',UTC_TIMESTAMP(3)), |
| | | ('新增',@formation_menu_id,2,'#','F','0','0','system:formation-report:add','system',UTC_TIMESTAMP(3)), |
| | | ('编辑',@formation_menu_id,3,'#','F','0','0','system:formation-report:edit','system',UTC_TIMESTAMP(3)), |
| | | ('删除',@formation_menu_id,4,'#','F','0','0','system:formation-report:remove','system',UTC_TIMESTAMP(3)); |
| | |
| | | #{operId} |
| | | </foreach> |
| | | <!-- 报表删除只保留这一份最小追溯记录,平台日志清理也不能移除。 --> |
| | | and (method is null or method <> 'compare-report.delete') |
| | | and (method is null or method not in ('compare-report.delete', 'formation-report.delete')) |
| | | </delete> |
| | | |
| | | <select id="selectOperLogById" parameterType="Long" resultMap="SysOperLogResult"> |
| | |
| | | </select> |
| | | |
| | | <update id="cleanOperLog"> |
| | | delete from sys_oper_log where method is null or method <> 'compare-report.delete' |
| | | delete from sys_oper_log where method is null or method not in ('compare-report.delete', 'formation-report.delete') |
| | | </update> |
| | | |
| | | </mapper> |