From 3d781111a5289987fdda13ce45bcfcabad207644 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期二, 01 九月 2026 00:48:33 +0800
Subject: [PATCH] 实现控制台数据修订导入删除
---
ruoyi-admin/src/test/java/com/ruoyi/config/Ticket25MigrationContractTest.java | 30 +++
ruoyi-admin/src/main/resources/db/migration/V24__console_data_revision_actions.sql | 37 ++++
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java | 36 ++++
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml | 53 +++++
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestConsoleImportRecord.java | 31 +++
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataRevisionAction.java | 44 ++++
ruoyi-admin/src/main/resources/db/migration/V25__console_import_records.sql | 16 +
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java | 15 +
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestConsoleDataServiceImpl.java | 204 ++++++++++++++++++++++
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataOverride.java | 35 +++
ruoyi-system/src/main/java/com/ruoyi/system/service/ITestConsoleDataService.java | 15 +
11 files changed, 516 insertions(+), 0 deletions(-)
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java
index 5146d8a..e111730 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java
@@ -21,7 +21,10 @@
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
@@ -30,6 +33,9 @@
{
@Autowired
private ITTestService testService;
+
+ @Autowired
+ private ITestConsoleDataService consoleDataService;
@PreAuthorize("@ss.hasAnyPermi('system:test:add,system:test:edit')")
@GetMapping("/options")
@@ -147,6 +153,36 @@
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}")
diff --git a/ruoyi-admin/src/main/resources/db/migration/V24__console_data_revision_actions.sql b/ruoyi-admin/src/main/resources/db/migration/V24__console_data_revision_actions.sql
new file mode 100644
index 0000000..190e164
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/db/migration/V24__console_data_revision_actions.sql
@@ -0,0 +1,37 @@
+-- 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='控制台数据最新人工覆盖或删除状态';
diff --git a/ruoyi-admin/src/main/resources/db/migration/V25__console_import_records.sql b/ruoyi-admin/src/main/resources/db/migration/V25__console_import_records.sql
new file mode 100644
index 0000000..bbe0324
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/db/migration/V25__console_import_records.sql
@@ -0,0 +1,16 @@
+-- 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='控制台导入原始记录';
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/config/Ticket25MigrationContractTest.java b/ruoyi-admin/src/test/java/com/ruoyi/config/Ticket25MigrationContractTest.java
new file mode 100644
index 0000000..db49cc8
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/ruoyi/config/Ticket25MigrationContractTest.java
@@ -0,0 +1,30 @@
+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.*"));
+ }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestConsoleImportRecord.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestConsoleImportRecord.java
new file mode 100644
index 0000000..d7e86b9
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestConsoleImportRecord.java
@@ -0,0 +1,31 @@
+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; }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataOverride.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataOverride.java
new file mode 100644
index 0000000..5e18a76
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataOverride.java
@@ -0,0 +1,35 @@
+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; }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataRevisionAction.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataRevisionAction.java
new file mode 100644
index 0000000..1e5d79f
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataRevisionAction.java
@@ -0,0 +1,44 @@
+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; }
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
index c4baf6e..33224d9 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
@@ -18,6 +18,9 @@
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;
@@ -187,4 +190,16 @@
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);
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ITestConsoleDataService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITestConsoleDataService.java
new file mode 100644
index 0000000..ef91987
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITestConsoleDataService.java
@@ -0,0 +1,15 @@
+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);
+}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestConsoleDataServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestConsoleDataServiceImpl.java
new file mode 100644
index 0000000..d557873
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestConsoleDataServiceImpl.java
@@ -0,0 +1,204 @@
+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();
+ }
+}
diff --git a/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml b/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
index 0364b4d..7f26929 100644
--- a/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
@@ -784,4 +784,57 @@
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>
--
Gitblit v1.7.1