对齐测试控制台当前有效数据
后端三类数据查询统一应用修订覆盖并保留原始采样,删除仅改变当前有效视图;前端数据列表直接读取后端当前有效数据。
| | |
| | | } |
| | | |
| | | @Test |
| | | void physicallyDeletesSamplesBehindStepAggregate() |
| | | void keepsOriginalSamplesWhenDeletingStepAggregate() |
| | | { |
| | | TTestDataRevisionAction action = new TTestDataRevisionAction(); |
| | | action.setTestId(26L); action.setDataType("STEP"); action.setOperationType("DELETE"); |
| | |
| | | action.setReason("异常工步"); action.setOperatorName("tester"); |
| | | TTestDataOverride override = new TTestDataOverride(); |
| | | override.setStableIdentity("step:1:2:3:4"); override.setDeleted(true); |
| | | when(mapper.selectBtsSampleIdsByDeviceChannelStep(26L, 1L, 2L, 3, 4)) |
| | | .thenReturn(List.of(101L, 102L)); |
| | | |
| | | service.submitAction(action, List.of(override), List.of(), List.of()); |
| | | |
| | | verify(mapper).deleteBtsMeasurementsBySampleIds(List.of(101L, 102L)); |
| | | verify(mapper).deleteBtsSamplesByIds(26L, List.of(101L, 102L)); |
| | | verify(mapper).insertDataRevisionAction(action); |
| | | verify(mapper).upsertDataOverride(override); |
| | | org.mockito.Mockito.verifyNoMoreInteractions(mapper); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.system.service.impl; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | import static org.junit.jupiter.api.Assertions.assertTrue; |
| | | import static org.mockito.Mockito.when; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.junit.jupiter.api.extension.ExtendWith; |
| | | import org.mockito.Mock; |
| | | import org.mockito.junit.jupiter.MockitoExtension; |
| | | import com.ruoyi.system.domain.TTest; |
| | | import com.ruoyi.system.domain.TTestDataOverride; |
| | | import com.ruoyi.system.mapper.TTestMapper; |
| | | import com.ruoyi.system.service.ITTestService; |
| | | |
| | | /** Ticket01:三类查询直接返回数据库计算的当前有效数据,删除不抹除原始事实。 */ |
| | | @ExtendWith(MockitoExtension.class) |
| | | class TestDataQueryServiceEffectiveDataTest |
| | | { |
| | | @Mock |
| | | private TTestMapper mapper; |
| | | @Mock |
| | | private ITTestService tests; |
| | | |
| | | @Test |
| | | void appliesRealtimeEditAndFiltersDeletedRow() |
| | | { |
| | | when(tests.selectTestById(7L)).thenReturn(new TTest()); |
| | | Map<String, Object> edited = row("deviceId", 22L, "channelId", 8L, "sequenceId", 3L, |
| | | "voltage", 3.7d); |
| | | Map<String, Object> deleted = row("deviceId", 22L, "channelId", 8L, "sequenceId", 4L, |
| | | "voltage", 3.8d); |
| | | TTestDataOverride edit = override("22:8:3", "{\"voltage\":\"4.1000\"}", false); |
| | | TTestDataOverride remove = override("22:8:4", null, true); |
| | | when(mapper.selectPersistedRecords(7L)).thenReturn(List.of(edited, deleted)); |
| | | when(mapper.selectDataOverrides(7L, "REALTIME")).thenReturn(List.of(edit, remove)); |
| | | |
| | | List<Map<String, Object>> result = new TestDataQueryServiceImpl(mapper, tests).records(7L); |
| | | |
| | | assertEquals(1, result.size()); |
| | | assertEquals("4.1000", result.get(0).get("voltage")); |
| | | assertEquals(3L, result.get(0).get("sequenceId")); |
| | | } |
| | | |
| | | @Test |
| | | void usesStepAndCycleStableIdentities() |
| | | { |
| | | when(tests.selectTestById(8L)).thenReturn(new TTest()); |
| | | Map<String, Object> step = row("deviceId", 22L, "channelId", 8L, "cycleNo", 2, "stepNo", 4, |
| | | "capacity", 1d); |
| | | Map<String, Object> cycle = row("deviceId", 22L, "channelId", 8L, "cycleNo", 2, |
| | | "efficiency", 90d); |
| | | when(mapper.selectPersistedSteps(8L)).thenReturn(List.of(step)); |
| | | when(mapper.selectPersistedCycles(8L)).thenReturn(List.of(cycle)); |
| | | when(mapper.selectDataOverrides(8L, "STEP")).thenReturn(List.of(override("step:22:8:2:4", |
| | | "{\"capacity\":\"2.0000\"}", false))); |
| | | when(mapper.selectDataOverrides(8L, "CYCLE")).thenReturn(List.of(override("cycle:22:8:2", |
| | | "{\"efficiency\":\"95.00\"}", false))); |
| | | TestDataQueryServiceImpl service = new TestDataQueryServiceImpl(mapper, tests); |
| | | |
| | | assertEquals("2.0000", service.steps(8L).get(0).get("capacity")); |
| | | assertEquals("95.00", service.cycles(8L).get(0).get("efficiency")); |
| | | assertTrue(service.cycles(8L).get(0).containsKey("cycleNo")); |
| | | } |
| | | |
| | | private static TTestDataOverride override(String identity, String values, boolean deleted) |
| | | { |
| | | TTestDataOverride result = new TTestDataOverride(); |
| | | result.setStableIdentity(identity); |
| | | result.setFieldValues(values); |
| | | result.setDeleted(deleted); |
| | | return result; |
| | | } |
| | | |
| | | private static Map<String, Object> row(Object... values) |
| | | { |
| | | Map<String, Object> result = new HashMap<>(); |
| | | for (int index = 0; index < values.length; index += 2) result.put(String.valueOf(values[index]), values[index + 1]); |
| | | return result; |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | mapper.upsertDataOverride(item); |
| | | } |
| | | } |
| | | if ("DELETE".equals(action.getOperationType()) && overrides != null) |
| | | { |
| | | List<Long> resolvedIds = new ArrayList<>(); |
| | | for (TTestDataOverride item : overrides) |
| | | { |
| | | Long[] identity = parseDeleteIdentity(item.getStableIdentity(), action.getDataType()); |
| | | if (identity == null) throw new ServiceException("删除数据稳定身份无效"); |
| | | switch (action.getDataType()) |
| | | { |
| | | case "REALTIME" -> { |
| | | Long sampleId = mapper.selectBtsSampleIdByDeviceChannelSequence(action.getTestId(), |
| | | identity[0], identity[1], identity[2]); |
| | | if (sampleId != null) resolvedIds.add(sampleId); |
| | | } |
| | | case "STEP" -> resolvedIds.addAll(mapper.selectBtsSampleIdsByDeviceChannelStep(action.getTestId(), |
| | | identity[0], identity[1], identity[2].intValue(), identity[3].intValue())); |
| | | case "CYCLE" -> resolvedIds.addAll(mapper.selectBtsSampleIdsByDeviceChannelCycle(action.getTestId(), |
| | | identity[0], identity[1], identity[2].intValue())); |
| | | default -> throw new ServiceException("控制台数据类型无效"); |
| | | } |
| | | } |
| | | if (!resolvedIds.isEmpty()) |
| | | { |
| | | resolvedIds = new ArrayList<>(new HashSet<>(resolvedIds)); |
| | | // 工步和循环是采样聚合视图,物理删除需在同一事务内清理对应底层采样。 |
| | | 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); |
| | | // 删除仅写入 deleted 覆盖,保留设备原始采样和采集器事实供追溯。 |
| | | // btsSampleIds/collectorSampleIds 保留在接口中兼容旧调用方,但不再执行物理删除。 |
| | | return action.getActionId(); |
| | | } |
| | | |
| | |
| | | |
| | | private void validateOverride(TTestDataOverride item, String operation, String dataType) |
| | | { |
| | | if ("DELETE".equals(operation) && parseDeleteIdentity(item.getStableIdentity(), dataType) == null) |
| | | throw new ServiceException("删除数据稳定身份无效"); |
| | | if (!"EDIT".equals(operation) || item.getFieldValues() == null || item.getFieldValues().isBlank()) return; |
| | | try |
| | | { |
| | |
| | | package com.ruoyi.system.service.impl; |
| | | |
| | | import java.util.List; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | import java.util.HashMap; |
| | | import com.alibaba.fastjson2.JSON; |
| | | import org.springframework.stereotype.Service; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | import com.ruoyi.system.domain.TTestDataOverride; |
| | | import com.ruoyi.system.mapper.TTestMapper; |
| | | import com.ruoyi.system.service.ITTestService; |
| | | import com.ruoyi.system.service.ITestDataQueryService; |
| | |
| | | @Override public List<Map<String, Object>> records(Long testId) |
| | | { |
| | | requireTest(testId); |
| | | return mapper.selectPersistedRecords(testId); |
| | | return applyEffectiveData(testId, "REALTIME", mapper.selectPersistedRecords(testId)); |
| | | } |
| | | |
| | | @Override public List<Map<String, Object>> steps(Long testId) |
| | | { |
| | | requireTest(testId); |
| | | return mapper.selectPersistedSteps(testId); |
| | | return applyEffectiveData(testId, "STEP", mapper.selectPersistedSteps(testId)); |
| | | } |
| | | |
| | | @Override public List<Map<String, Object>> cycles(Long testId) |
| | | { |
| | | requireTest(testId); |
| | | return mapper.selectPersistedCycles(testId); |
| | | return applyEffectiveData(testId, "CYCLE", mapper.selectPersistedCycles(testId)); |
| | | } |
| | | |
| | | /** 将最新人工覆盖应用到设备事实,删除只影响当前有效视图,不抹除原始采样。 */ |
| | | private List<Map<String, Object>> applyEffectiveData(Long testId, String dataType, |
| | | List<Map<String, Object>> source) |
| | | { |
| | | List<TTestDataOverride> overrides = mapper.selectDataOverrides(testId, dataType); |
| | | if (source == null || source.isEmpty() || overrides == null || overrides.isEmpty()) |
| | | return source == null ? List.of() : source; |
| | | Map<String, TTestDataOverride> byIdentity = new HashMap<>(); |
| | | for (TTestDataOverride override : overrides) |
| | | if (override != null && override.getStableIdentity() != null) |
| | | byIdentity.put(override.getStableIdentity(), override); |
| | | List<Map<String, Object>> effective = new java.util.ArrayList<>(source.size()); |
| | | for (Map<String, Object> row : source) |
| | | { |
| | | String identity = stableIdentity(dataType, row); |
| | | TTestDataOverride override = byIdentity.get(identity); |
| | | if (override != null && Boolean.TRUE.equals(override.getDeleted())) continue; |
| | | Map<String, Object> copy = new LinkedHashMap<>(row); |
| | | if (override != null && override.getFieldValues() != null |
| | | && !override.getFieldValues().isBlank()) |
| | | { |
| | | try |
| | | { |
| | | Map<String, Object> values = JSON.parseObject(override.getFieldValues(), Map.class); |
| | | if (values != null) copy.putAll(values); |
| | | } |
| | | catch (RuntimeException ignored) |
| | | { |
| | | // 覆盖已由修订服务校验;解析异常时保留原始事实,避免查询整体失败。 |
| | | } |
| | | } |
| | | effective.add(copy); |
| | | } |
| | | return effective; |
| | | } |
| | | |
| | | private String stableIdentity(String dataType, Map<String, Object> row) |
| | | { |
| | | String deviceId = value(row, "deviceId"); |
| | | String channelId = value(row, "channelId"); |
| | | String cycleNo = value(row, "cycleNo"); |
| | | if ("REALTIME".equals(dataType)) return deviceId + ":" + channelId + ":" + value(row, "sequenceId"); |
| | | if ("STEP".equals(dataType)) return "step:" + deviceId + ":" + channelId + ":" + cycleNo + ":" + value(row, "stepNo"); |
| | | return "cycle:" + deviceId + ":" + channelId + ":" + cycleNo; |
| | | } |
| | | |
| | | private String value(Map<String, Object> row, String key) |
| | | { |
| | | Object value = row == null ? null : row.get(key); |
| | | return value == null ? "" : String.valueOf(value); |
| | | } |
| | | |
| | | private void requireTest(Long testId) |