实现控制台数据修订导入删除
新增三类控制台数据的修订行为、覆盖状态和导入事实记录,补充字段白名单、测试完成校验及实时采样物理删除。
| | |
| | | import com.ruoyi.system.domain.TTest; |
| | | import com.ruoyi.system.domain.TTestResult; |
| | | import com.ruoyi.system.domain.TTestLogicStepMapping; |
| | | import com.ruoyi.system.domain.TTestDataRevisionAction; |
| | | import com.ruoyi.system.domain.TTestDataOverride; |
| | | import com.ruoyi.system.service.ITTestService; |
| | | import com.ruoyi.system.service.ITestConsoleDataService; |
| | | |
| | | /** 测试管理 Controller。 */ |
| | | @RestController |
| | |
| | | { |
| | | @Autowired |
| | | private ITTestService testService; |
| | | |
| | | @Autowired |
| | | private ITestConsoleDataService consoleDataService; |
| | | |
| | | @PreAuthorize("@ss.hasAnyPermi('system:test:add,system:test:edit')") |
| | | @GetMapping("/options") |
| | |
| | | return success(testService.selectTestById(testId)); |
| | | } |
| | | |
| | | /** 返回当前测试某类数据的人工覆盖和删除状态,供刷新后恢复展示。 */ |
| | | @PreAuthorize("@ss.hasPermi('system:test:query')") |
| | | @GetMapping("/{testId}/console-data/{dataType}/overrides") |
| | | public AjaxResult dataOverrides(@PathVariable Long testId, @PathVariable String dataType) |
| | | { |
| | | return success(consoleDataService.selectOverrides(testId, dataType)); |
| | | } |
| | | |
| | | /** 一次提交编辑、导入或删除;服务端只追加一条修订行为。 */ |
| | | @PreAuthorize("@ss.hasPermi('system:test:edit')") |
| | | @Log(title = "测试控制台数据修订", businessType = BusinessType.UPDATE) |
| | | @PostMapping("/{testId}/console-data/actions") |
| | | public AjaxResult dataAction(@PathVariable Long testId, @RequestBody ConsoleDataActionRequest request) |
| | | { |
| | | if (request == null) return error("数据操作不能为空"); |
| | | TTestDataRevisionAction action = new TTestDataRevisionAction(); |
| | | action.setTestId(testId); action.setDataType(request.dataType()); action.setOperationType(request.operationType()); |
| | | action.setScopeSummary(request.scopeSummary()); action.setIdentitySummary(request.identitySummary()); |
| | | action.setChangesSummary(request.changesSummary()); action.setReason(request.reason()); |
| | | action.setOperatorId(getUserId()); action.setOperatorName(getUsername()); |
| | | Long actionId = consoleDataService.submitAction(action, |
| | | request.overrides() == null ? java.util.Collections.emptyList() : request.overrides(), |
| | | request.btsSampleIds(), request.collectorSampleIds()); |
| | | return success().put("actionId", actionId); |
| | | } |
| | | |
| | | public record ConsoleDataActionRequest(String dataType, String operationType, String scopeSummary, |
| | | String identitySummary, String changesSummary, String reason, List<TTestDataOverride> overrides, |
| | | List<Long> btsSampleIds, List<Long> collectorSampleIds) { } |
| | | |
| | | @PreAuthorize("@ss.hasPermi('system:test:remove')") |
| | | @Log(title = "测试管理", businessType = BusinessType.DELETE) |
| | | @DeleteMapping("/{testIds}") |
| New file |
| | |
| | | -- Ticket25:控制台三类数据的一次用户行为修订、覆盖状态和物理删除支持。 |
| | | CREATE TABLE t_test_data_revision_action ( |
| | | action_id BIGINT NOT NULL AUTO_INCREMENT, |
| | | test_id BIGINT NOT NULL, |
| | | data_type VARCHAR(16) NOT NULL, |
| | | operation_type VARCHAR(16) NOT NULL, |
| | | scope_summary VARCHAR(1000) NOT NULL, |
| | | identity_summary JSON NOT NULL, |
| | | changes_summary JSON NULL, |
| | | reason VARCHAR(500) NOT NULL, |
| | | operator_id BIGINT NULL, |
| | | operator_name VARCHAR(64) NOT NULL, |
| | | created_at_utc DATETIME(3) NOT NULL, |
| | | PRIMARY KEY (action_id), |
| | | KEY idx_data_revision_test_time (test_id, data_type, created_at_utc), |
| | | CONSTRAINT fk_data_revision_test FOREIGN KEY (test_id) REFERENCES t_test (test_id), |
| | | CONSTRAINT chk_data_revision_type CHECK (data_type IN ('REALTIME', 'STEP', 'CYCLE')), |
| | | CONSTRAINT chk_data_revision_operation CHECK (operation_type IN ('EDIT', 'IMPORT', 'DELETE')), |
| | | CONSTRAINT chk_data_revision_reason CHECK (CHAR_LENGTH(TRIM(reason)) > 0) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='测试控制台一次数据修订行为'; |
| | | |
| | | CREATE TABLE t_test_data_override ( |
| | | override_id BIGINT NOT NULL AUTO_INCREMENT, |
| | | test_id BIGINT NOT NULL, |
| | | data_type VARCHAR(16) NOT NULL, |
| | | stable_identity VARCHAR(512) NOT NULL, |
| | | field_values JSON NULL, |
| | | deleted TINYINT(1) NOT NULL DEFAULT 0, |
| | | action_id BIGINT NOT NULL, |
| | | created_at_utc DATETIME(3) NOT NULL, |
| | | PRIMARY KEY (override_id), |
| | | UNIQUE KEY uk_data_override_identity (test_id, data_type, stable_identity), |
| | | KEY idx_data_override_action (action_id), |
| | | CONSTRAINT fk_data_override_test FOREIGN KEY (test_id) REFERENCES t_test (test_id), |
| | | CONSTRAINT fk_data_override_action FOREIGN KEY (action_id) REFERENCES t_test_data_revision_action (action_id), |
| | | CONSTRAINT chk_data_override_type CHECK (data_type IN ('REALTIME', 'STEP', 'CYCLE')) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='控制台数据最新人工覆盖或删除状态'; |
| New file |
| | |
| | | -- Ticket25:导入数据保留独立的后端事实记录,覆盖状态只负责展示修订。 |
| | | CREATE TABLE t_test_console_import_record ( |
| | | import_record_id BIGINT NOT NULL AUTO_INCREMENT, |
| | | test_id BIGINT NOT NULL, |
| | | data_type VARCHAR(16) NOT NULL, |
| | | stable_identity VARCHAR(512) NOT NULL, |
| | | row_values JSON NOT NULL, |
| | | action_id BIGINT NOT NULL, |
| | | created_at_utc DATETIME(3) NOT NULL, |
| | | PRIMARY KEY (import_record_id), |
| | | UNIQUE KEY uk_console_import_identity (test_id, data_type, stable_identity), |
| | | KEY idx_console_import_action (action_id), |
| | | CONSTRAINT fk_console_import_test FOREIGN KEY (test_id) REFERENCES t_test (test_id), |
| | | CONSTRAINT fk_console_import_action FOREIGN KEY (action_id) REFERENCES t_test_data_revision_action (action_id), |
| | | CONSTRAINT chk_console_import_type CHECK (data_type IN ('REALTIME', 'STEP', 'CYCLE')) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='控制台导入原始记录'; |
| New file |
| | |
| | | package com.ruoyi.config; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertFalse; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | import java.io.InputStream; |
| | | import java.nio.charset.StandardCharsets; |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | /** Ticket25 迁移契约:一次行为审计、覆盖状态和物理删除所需的持久化结构。 */ |
| | | class Ticket25MigrationContractTest |
| | | { |
| | | @Test |
| | | void storesSingleActionAndStableIdentityOverrides() |
| | | { |
| | | String sql; |
| | | try (InputStream stream = getClass().getClassLoader().getResourceAsStream( |
| | | "db/migration/V24__console_data_revision_actions.sql")) |
| | | { |
| | | if (stream == null) throw new AssertionError("缺少 Ticket25 迁移"); |
| | | sql = new String(stream.readAllBytes(), StandardCharsets.UTF_8); |
| | | } |
| | | catch (Exception exception) { throw new AssertionError(exception); } |
| | | assertTrue(sql.contains("t_test_data_revision_action")); |
| | | assertTrue(sql.contains("t_test_data_override")); |
| | | assertTrue(sql.contains("identity_summary JSON")); |
| | | assertTrue(sql.contains("operation_type")); |
| | | assertTrue(sql.contains("uk_data_override_identity")); |
| | | assertFalse(sql.matches("(?s).*\\bDROP\\s+(TABLE|COLUMN)\\b.*")); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.domain; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.Date; |
| | | |
| | | /** 控制台导入后端事实记录。 */ |
| | | public class TTestConsoleImportRecord implements Serializable |
| | | { |
| | | private static final long serialVersionUID = 1L; |
| | | private Long importRecordId; |
| | | private Long testId; |
| | | private String dataType; |
| | | private String stableIdentity; |
| | | private String rowValues; |
| | | private Long actionId; |
| | | private Date createdAtUtc; |
| | | public Long getImportRecordId() { return importRecordId; } |
| | | public void setImportRecordId(Long value) { importRecordId = value; } |
| | | public Long getTestId() { return testId; } |
| | | public void setTestId(Long value) { testId = value; } |
| | | public String getDataType() { return dataType; } |
| | | public void setDataType(String value) { dataType = value; } |
| | | public String getStableIdentity() { return stableIdentity; } |
| | | public void setStableIdentity(String value) { stableIdentity = value; } |
| | | public String getRowValues() { return rowValues; } |
| | | public void setRowValues(String value) { rowValues = value; } |
| | | public Long getActionId() { return actionId; } |
| | | public void setActionId(Long value) { actionId = value; } |
| | | public Date getCreatedAtUtc() { return createdAtUtc; } |
| | | public void setCreatedAtUtc(Date value) { createdAtUtc = value; } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.domain; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.Date; |
| | | |
| | | /** 某个稳定数据身份的最新人工覆盖状态。 */ |
| | | public class TTestDataOverride implements Serializable |
| | | { |
| | | private static final long serialVersionUID = 1L; |
| | | private Long overrideId; |
| | | private Long testId; |
| | | private String dataType; |
| | | private String stableIdentity; |
| | | private String fieldValues; |
| | | private Boolean deleted; |
| | | private Long actionId; |
| | | private Date createdAtUtc; |
| | | |
| | | public Long getOverrideId() { return overrideId; } |
| | | public void setOverrideId(Long value) { overrideId = value; } |
| | | public Long getTestId() { return testId; } |
| | | public void setTestId(Long value) { testId = value; } |
| | | public String getDataType() { return dataType; } |
| | | public void setDataType(String value) { dataType = value; } |
| | | public String getStableIdentity() { return stableIdentity; } |
| | | public void setStableIdentity(String value) { stableIdentity = value; } |
| | | public String getFieldValues() { return fieldValues; } |
| | | public void setFieldValues(String value) { fieldValues = value; } |
| | | public Boolean getDeleted() { return deleted; } |
| | | public void setDeleted(Boolean value) { deleted = value; } |
| | | public Long getActionId() { return actionId; } |
| | | public void setActionId(Long value) { actionId = value; } |
| | | public Date getCreatedAtUtc() { return createdAtUtc; } |
| | | public void setCreatedAtUtc(Date value) { createdAtUtc = value; } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.domain; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.Date; |
| | | |
| | | /** 一次控制台数据编辑、导入或删除行为。 */ |
| | | public class TTestDataRevisionAction implements Serializable |
| | | { |
| | | private static final long serialVersionUID = 1L; |
| | | private Long actionId; |
| | | private Long testId; |
| | | private String dataType; |
| | | private String operationType; |
| | | private String scopeSummary; |
| | | private String identitySummary; |
| | | private String changesSummary; |
| | | private String reason; |
| | | private Long operatorId; |
| | | private String operatorName; |
| | | private Date createdAtUtc; |
| | | |
| | | public Long getActionId() { return actionId; } |
| | | public void setActionId(Long value) { actionId = value; } |
| | | public Long getTestId() { return testId; } |
| | | public void setTestId(Long value) { testId = value; } |
| | | public String getDataType() { return dataType; } |
| | | public void setDataType(String value) { dataType = value; } |
| | | public String getOperationType() { return operationType; } |
| | | public void setOperationType(String value) { operationType = value; } |
| | | public String getScopeSummary() { return scopeSummary; } |
| | | public void setScopeSummary(String value) { scopeSummary = value; } |
| | | public String getIdentitySummary() { return identitySummary; } |
| | | public void setIdentitySummary(String value) { identitySummary = value; } |
| | | public String getChangesSummary() { return changesSummary; } |
| | | public void setChangesSummary(String value) { changesSummary = value; } |
| | | public String getReason() { return reason; } |
| | | public void setReason(String value) { reason = value; } |
| | | public Long getOperatorId() { return operatorId; } |
| | | public void setOperatorId(Long value) { operatorId = value; } |
| | | public String getOperatorName() { return operatorName; } |
| | | public void setOperatorName(String value) { operatorName = value; } |
| | | public Date getCreatedAtUtc() { return createdAtUtc; } |
| | | public void setCreatedAtUtc(Date value) { createdAtUtc = value; } |
| | | } |
| | |
| | | import com.ruoyi.system.domain.TTestBackfillCursor; |
| | | import com.ruoyi.system.domain.TTestIntegrityIssue; |
| | | import com.ruoyi.system.domain.TDetectionChannel; |
| | | import com.ruoyi.system.domain.TTestDataRevisionAction; |
| | | import com.ruoyi.system.domain.TTestConsoleImportRecord; |
| | | import com.ruoyi.system.domain.TTestDataOverride; |
| | | import com.ruoyi.system.domain.vo.TestBatteryOptionVo; |
| | | import com.ruoyi.system.domain.vo.TestChannelOptionVo; |
| | | import com.ruoyi.system.domain.vo.TestCollectorOptionVo; |
| | |
| | | int resolveIntegrityIssue(@Param("issueId") Long issueId, @Param("resolvedAtUtc") Date resolvedAtUtc); |
| | | int countOpenIntegrityIssues(Long testId); |
| | | int countOpenExceptions(Long testId); |
| | | |
| | | int insertDataRevisionAction(TTestDataRevisionAction action); |
| | | int upsertDataOverride(TTestDataOverride override); |
| | | List<TTestDataOverride> selectDataOverrides(@Param("testId") Long testId, |
| | | @Param("dataType") String dataType); |
| | | int deleteBtsSamplesByIds(@Param("testId") Long testId, @Param("sampleIds") List<Long> sampleIds); |
| | | int deleteBtsMeasurementsBySampleIds(@Param("sampleIds") List<Long> sampleIds); |
| | | int deleteCollectorSamplesByIds(@Param("testId") Long testId, @Param("sampleIds") List<Long> sampleIds); |
| | | Long selectBtsSampleIdByChannelSequence(@Param("testId") Long testId, |
| | | @Param("channelId") Long channelId, @Param("sequenceId") Long sequenceId); |
| | | int insertConsoleImportRecord(TTestConsoleImportRecord record); |
| | | int countTestBatteryNo(@Param("testId") Long testId, @Param("batteryNo") String batteryNo); |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.service; |
| | | |
| | | import java.util.List; |
| | | import com.ruoyi.system.domain.TTestDataOverride; |
| | | import com.ruoyi.system.domain.TTestDataRevisionAction; |
| | | |
| | | /** 控制台三类数据的修订、导入和物理删除服务。 */ |
| | | public interface ITestConsoleDataService |
| | | { |
| | | List<TTestDataOverride> selectOverrides(Long testId, String dataType); |
| | | |
| | | /** 在一个事务中写入一次行为、覆盖状态并执行请求的物理删除。 */ |
| | | Long submitAction(TTestDataRevisionAction action, List<TTestDataOverride> overrides, |
| | | List<Long> btsSampleIds, List<Long> collectorSampleIds); |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.service.impl; |
| | | |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import java.util.Map; |
| | | import java.util.ArrayList; |
| | | import java.util.HashSet; |
| | | import com.alibaba.fastjson2.JSON; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | import com.ruoyi.system.domain.TTestDataOverride; |
| | | import com.ruoyi.system.domain.TTestDataRevisionAction; |
| | | import com.ruoyi.system.domain.TTestConsoleImportRecord; |
| | | import com.ruoyi.system.mapper.TTestMapper; |
| | | import com.ruoyi.system.service.ITestConsoleDataService; |
| | | |
| | | /** 控制台数据操作的边界校验和事务协调。 */ |
| | | @Service |
| | | public class TestConsoleDataServiceImpl implements ITestConsoleDataService |
| | | { |
| | | private static final Set<String> DATA_TYPES = Set.of("REALTIME", "STEP", "CYCLE"); |
| | | private static final Set<String> OPERATIONS = Set.of("EDIT", "IMPORT", "DELETE"); |
| | | |
| | | @Autowired |
| | | private TTestMapper mapper; |
| | | |
| | | @Override |
| | | public List<TTestDataOverride> selectOverrides(Long testId, String dataType) |
| | | { |
| | | validateType(testId, dataType); |
| | | return mapper.selectDataOverrides(testId, dataType); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long submitAction(TTestDataRevisionAction action, List<TTestDataOverride> overrides, |
| | | List<Long> btsSampleIds, List<Long> collectorSampleIds) |
| | | { |
| | | if (action == null || action.getTestId() == null) throw new ServiceException("测试ID不能为空"); |
| | | validateType(action.getTestId(), action.getDataType()); |
| | | if (!OPERATIONS.contains(action.getOperationType())) throw new ServiceException("数据操作类型无效"); |
| | | if ("IMPORT".equals(action.getOperationType())) |
| | | { |
| | | com.ruoyi.system.domain.TTest test = mapper.selectTestById(action.getTestId()); |
| | | if (test == null || !"ENDED".equals(test.getReceptionStatus()) || !"COMPLETED".equals(test.getTestStatus())) |
| | | throw new ServiceException("测试完成后才可以导入数据"); |
| | | } |
| | | if ("EDIT".equals(action.getOperationType())) validateEditableFields(action); |
| | | if (action.getReason() == null || action.getReason().trim().isEmpty()) throw new ServiceException("数据操作原因不能为空"); |
| | | if (action.getOperatorName() == null || action.getOperatorName().trim().isEmpty()) action.setOperatorName("未知用户"); |
| | | action.setReason(action.getReason().trim()); |
| | | action.setCreatedAtUtc(new Date()); |
| | | mapper.insertDataRevisionAction(action); |
| | | if ("IMPORT".equals(action.getOperationType()) && action.getChangesSummary() != null) |
| | | { |
| | | try |
| | | { |
| | | @SuppressWarnings("unchecked") |
| | | List<Map<String, Object>> rows = (List<Map<String, Object>>) (List<?>) JSON.parseArray(action.getChangesSummary(), Map.class); |
| | | Set<String> identities = new HashSet<>(); |
| | | for (Map<String, Object> row : rows) |
| | | { |
| | | Object identity = row.get("recordNo"); |
| | | if (identity == null || !identities.add(String.valueOf(identity))) |
| | | throw new ServiceException("导入数据存在重复稳定身份"); |
| | | Object batteryNo = row.get("batteryNo"); |
| | | if (batteryNo != null && !String.valueOf(batteryNo).isBlank() |
| | | && mapper.countTestBatteryNo(action.getTestId(), String.valueOf(batteryNo)) == 0) |
| | | throw new ServiceException("导入数据包含不属于当前测试的电池号: " + batteryNo); |
| | | TTestConsoleImportRecord record = new TTestConsoleImportRecord(); |
| | | record.setTestId(action.getTestId()); record.setDataType(action.getDataType()); |
| | | record.setStableIdentity(String.valueOf(identity)); record.setRowValues(JSON.toJSONString(row)); |
| | | record.setActionId(action.getActionId()); record.setCreatedAtUtc(action.getCreatedAtUtc()); |
| | | mapper.insertConsoleImportRecord(record); |
| | | } |
| | | } |
| | | catch (ServiceException exception) { throw exception; } |
| | | catch (Exception exception) { throw new ServiceException("导入数据格式无效"); } |
| | | } |
| | | if (overrides != null) |
| | | { |
| | | Set<String> identities = new HashSet<>(); |
| | | for (TTestDataOverride item : overrides) |
| | | { |
| | | if (item == null || item.getStableIdentity() == null || item.getStableIdentity().trim().isEmpty()) |
| | | throw new ServiceException("数据稳定身份不能为空"); |
| | | item.setStableIdentity(item.getStableIdentity().trim()); |
| | | if (!identities.add(item.getStableIdentity())) throw new ServiceException("同一批数据存在重复稳定身份"); |
| | | validateOverride(item, action.getOperationType(), action.getDataType()); |
| | | item.setTestId(action.getTestId()); item.setDataType(action.getDataType()); |
| | | item.setActionId(action.getActionId()); item.setCreatedAtUtc(action.getCreatedAtUtc()); |
| | | mapper.upsertDataOverride(item); |
| | | } |
| | | } |
| | | if ("DELETE".equals(action.getOperationType()) && "REALTIME".equals(action.getDataType()) && overrides != null) |
| | | { |
| | | List<Long> resolvedIds = new ArrayList<>(); |
| | | for (TTestDataOverride item : overrides) |
| | | { |
| | | Long[] identity = parseRealtimeIdentity(item.getStableIdentity()); |
| | | if (identity == null) continue; |
| | | Long sampleId = mapper.selectBtsSampleIdByChannelSequence(action.getTestId(), identity[0], identity[1]); |
| | | if (sampleId != null) resolvedIds.add(sampleId); |
| | | } |
| | | if (!resolvedIds.isEmpty()) |
| | | { |
| | | mapper.deleteBtsMeasurementsBySampleIds(resolvedIds); |
| | | mapper.deleteBtsSamplesByIds(action.getTestId(), resolvedIds); |
| | | } |
| | | } |
| | | if (btsSampleIds != null && !btsSampleIds.isEmpty()) |
| | | { |
| | | // 采样明细受外键保护,必须在主采样物理删除前清理。 |
| | | mapper.deleteBtsMeasurementsBySampleIds(btsSampleIds); |
| | | mapper.deleteBtsSamplesByIds(action.getTestId(), btsSampleIds); |
| | | } |
| | | if (collectorSampleIds != null && !collectorSampleIds.isEmpty()) mapper.deleteCollectorSamplesByIds(action.getTestId(), collectorSampleIds); |
| | | return action.getActionId(); |
| | | } |
| | | |
| | | private void validateType(Long testId, String dataType) |
| | | { |
| | | if (testId == null || testId < 1) throw new ServiceException("测试ID无效"); |
| | | if (!DATA_TYPES.contains(dataType)) throw new ServiceException("控制台数据类型无效"); |
| | | } |
| | | |
| | | private void validateEditableFields(TTestDataRevisionAction action) |
| | | { |
| | | if (action.getChangesSummary() == null || action.getChangesSummary().isBlank()) |
| | | throw new ServiceException("修改内容不能为空"); |
| | | try |
| | | { |
| | | @SuppressWarnings("unchecked") |
| | | List<Map<String, Object>> changes = (List<Map<String, Object>>) (List<?>) JSON.parseArray(action.getChangesSummary(), Map.class); |
| | | Set<String> editable = switch (action.getDataType()) |
| | | { |
| | | case "REALTIME" -> Set.of("voltage", "current", "capacity", "energy"); |
| | | case "STEP" -> Set.of("stepDuration", "capacity", "energy", "startVoltage", "endVoltage", |
| | | "chargePeakVoltage", "dischargeValleyVoltage"); |
| | | case "CYCLE" -> Set.of("chargeCapacity", "dischargeCapacity", "efficiency", "chargeEnergy", |
| | | "dischargeEnergy"); |
| | | default -> Set.of(); |
| | | }; |
| | | Set<String> immutable = Set.of("recordNo", "batteryNo", "cycleNo", "stepNo", "stepName", |
| | | "sequenceId", "recordedAt", "absoluteTime"); |
| | | for (Map<String, Object> item : changes) |
| | | { |
| | | Map<String, Object> before = objectMap(item.get("before")); |
| | | Map<String, Object> after = objectMap(item.get("after")); |
| | | for (String key : immutable) |
| | | if (!java.util.Objects.equals(before.get(key), after.get(key))) |
| | | throw new ServiceException("记录身份字段不可修改: " + key); |
| | | for (String key : after.keySet()) |
| | | if (!java.util.Objects.equals(before.get(key), after.get(key)) && !editable.contains(key)) |
| | | throw new ServiceException("字段不可修改: " + key); |
| | | } |
| | | } |
| | | catch (ServiceException exception) { throw exception; } |
| | | catch (Exception exception) { throw new ServiceException("修改内容格式无效"); } |
| | | } |
| | | |
| | | private void validateOverride(TTestDataOverride item, String operation, String dataType) |
| | | { |
| | | if (!"EDIT".equals(operation) || item.getFieldValues() == null || item.getFieldValues().isBlank()) return; |
| | | try |
| | | { |
| | | Map<String, Object> values = JSON.parseObject(item.getFieldValues(), Map.class); |
| | | for (String key : values.keySet()) |
| | | if (!editableFields(dataType).contains(key)) throw new ServiceException("字段不可修改: " + key); |
| | | } |
| | | catch (ServiceException exception) { throw exception; } |
| | | catch (Exception exception) { throw new ServiceException("修改字段格式无效"); } |
| | | } |
| | | |
| | | private Set<String> editableFields(String dataType) |
| | | { |
| | | return switch (dataType) |
| | | { |
| | | case "REALTIME" -> Set.of("voltage", "current", "capacity", "energy"); |
| | | case "STEP" -> Set.of("stepDuration", "capacity", "energy", "startVoltage", "endVoltage", |
| | | "chargePeakVoltage", "dischargeValleyVoltage"); |
| | | case "CYCLE" -> Set.of("chargeCapacity", "dischargeCapacity", "efficiency", "chargeEnergy", |
| | | "dischargeEnergy"); |
| | | default -> Set.of(); |
| | | }; |
| | | } |
| | | |
| | | private Long[] parseRealtimeIdentity(String value) |
| | | { |
| | | String[] parts = value.split(":", -1); |
| | | if (parts.length != 3) return null; |
| | | try { return new Long[] { Long.valueOf(parts[0]), Long.valueOf(parts[2]) }; } |
| | | catch (NumberFormatException exception) { return null; } |
| | | } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | private Map<String, Object> objectMap(Object value) |
| | | { |
| | | return value instanceof Map ? (Map<String, Object>) value : Map.of(); |
| | | } |
| | | } |
| | |
| | | where exception_fact.test_id = #{testId} and exception_fact.interval_status = 'OPEN' |
| | | order by exception_fact.opened_at_utc, exception_fact.interval_id |
| | | </select> |
| | | <resultMap id="TTestDataOverrideResultMap" type="com.ruoyi.system.domain.TTestDataOverride"> |
| | | <id property="overrideId" column="override_id"/><result property="testId" column="test_id"/> |
| | | <result property="dataType" column="data_type"/><result property="stableIdentity" column="stable_identity"/> |
| | | <result property="fieldValues" column="field_values"/><result property="deleted" column="deleted"/> |
| | | <result property="actionId" column="action_id"/><result property="createdAtUtc" column="created_at_utc"/> |
| | | </resultMap> |
| | | <insert id="insertDataRevisionAction" parameterType="com.ruoyi.system.domain.TTestDataRevisionAction" |
| | | useGeneratedKeys="true" keyProperty="actionId"> |
| | | insert into t_test_data_revision_action |
| | | (test_id, data_type, operation_type, scope_summary, identity_summary, changes_summary, |
| | | reason, operator_id, operator_name, created_at_utc) |
| | | values (#{testId}, #{dataType}, #{operationType}, #{scopeSummary}, cast(#{identitySummary} as json), |
| | | cast(#{changesSummary} as json), #{reason}, #{operatorId}, #{operatorName}, #{createdAtUtc}) |
| | | </insert> |
| | | <insert id="upsertDataOverride" parameterType="com.ruoyi.system.domain.TTestDataOverride"> |
| | | insert into t_test_data_override |
| | | (test_id, data_type, stable_identity, field_values, deleted, action_id, created_at_utc) |
| | | values (#{testId}, #{dataType}, #{stableIdentity}, cast(#{fieldValues} as json), #{deleted}, #{actionId}, #{createdAtUtc}) |
| | | on duplicate key update field_values = values(field_values), deleted = values(deleted), |
| | | action_id = values(action_id), created_at_utc = values(created_at_utc) |
| | | </insert> |
| | | <select id="selectDataOverrides" resultMap="TTestDataOverrideResultMap"> |
| | | select override_id, test_id, data_type, stable_identity, field_values, deleted, action_id, created_at_utc |
| | | from t_test_data_override where test_id = #{testId} and data_type = #{dataType} |
| | | order by override_id |
| | | </select> |
| | | <delete id="deleteBtsSamplesByIds"> |
| | | delete from t_test_sample where test_id = #{testId} and sample_id in |
| | | <foreach collection="sampleIds" item="id" open="(" separator="," close=")">#{id}</foreach> |
| | | </delete> |
| | | <delete id="deleteBtsMeasurementsBySampleIds"> |
| | | delete from t_test_sample_measurement where sample_id in |
| | | <foreach collection="sampleIds" item="id" open="(" separator="," close=")">#{id}</foreach> |
| | | </delete> |
| | | <delete id="deleteCollectorSamplesByIds"> |
| | | delete from t_test_collector_sample where test_id = #{testId} and sample_id in |
| | | <foreach collection="sampleIds" item="id" open="(" separator="," close=")">#{id}</foreach> |
| | | </delete> |
| | | <select id="selectBtsSampleIdByChannelSequence" resultType="long"> |
| | | select sample_id from t_test_sample |
| | | where test_id = #{testId} and channel_id = #{channelId} and seq_id = #{sequenceId} |
| | | limit 1 |
| | | </select> |
| | | <insert id="insertConsoleImportRecord" parameterType="com.ruoyi.system.domain.TTestConsoleImportRecord" |
| | | useGeneratedKeys="true" keyProperty="importRecordId"> |
| | | insert into t_test_console_import_record |
| | | (test_id, data_type, stable_identity, row_values, action_id, created_at_utc) |
| | | values (#{testId}, #{dataType}, #{stableIdentity}, cast(#{rowValues} as json), #{actionId}, #{createdAtUtc}) |
| | | </insert> |
| | | <select id="countTestBatteryNo" resultType="int"> |
| | | select count(1) from t_test_battery |
| | | where test_id = #{testId} and battery_no = #{batteryNo} and del_flag = 0 |
| | | </select> |
| | | </mapper> |