package com.ruoyi.system.service.impl;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.when;
|
|
import java.math.BigDecimal;
|
import java.time.Clock;
|
import java.time.Instant;
|
import java.time.ZoneOffset;
|
import java.util.Date;
|
import java.util.HexFormat;
|
import java.util.List;
|
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.mockito.ArgumentCaptor;
|
import org.springframework.boot.test.system.CapturedOutput;
|
import org.springframework.boot.test.system.OutputCaptureExtension;
|
import com.battery.device.simulator.BtsDevelopmentSimulator;
|
import com.battery.device.simulator.BtsDevelopmentSimulator.BtsCycleRecord;
|
import com.battery.device.simulator.BtsDevelopmentSimulator.BtsStepRecord;
|
import com.battery.device.simulator.BtsSimulatorStateStore.BtsHistoryRecord;
|
import com.battery.device.simulator.BtsSimulatorStateStore.CollectorRealtimeMeasurement;
|
import com.battery.device.simulator.DevelopmentCollectorSimulation.CollectorRealtimeSnapshot;
|
import com.ruoyi.system.domain.TTest;
|
import com.ruoyi.system.domain.TTestBattery;
|
import com.ruoyi.system.domain.TTestChannelBinding;
|
import com.ruoyi.system.domain.TTestCollectorChannel;
|
import com.ruoyi.system.domain.TTestCycleRecord;
|
import com.ruoyi.system.domain.TTestRealtimeRecord;
|
import com.ruoyi.system.domain.TTestStepRecord;
|
import com.ruoyi.system.mapper.TTestMapper;
|
import com.ruoyi.system.service.ITTestService;
|
|
/** Ticket08 通过接收入口验证三类持久化事实。 */
|
@ExtendWith(OutputCaptureExtension.class)
|
class TestDataReceptionServiceImplTest
|
{
|
private static final Instant CAPTURED_AT = Instant.parse("2026-09-02T04:01:00Z");
|
private static final Clock CLOCK = Clock.fixed(CAPTURED_AT, ZoneOffset.UTC);
|
|
@Test
|
void savesOneCombinedRealtimeRowForOneBatteryPollingSlot()
|
{
|
Fixture fixture = fixture();
|
when(fixture.simulator.btsRecords(1, 1)).thenReturn(List.of(cabinetRecord()));
|
when(fixture.simulator.collectorRealtime(9)).thenReturn(collectorSnapshot(true, List.of(
|
collector("S-01", 10L, "TEMPERATURE", "25.100", "℃"),
|
collector("S-02", 10L, "POSITIVE_POTENTIAL", "1.620", "V"),
|
collector("S-03", 10L, "NEGATIVE_POTENTIAL", "1.610", "V"),
|
collector("S-04", 10L, "AMBIENT_TEMPERATURE", "23.400", "℃"))));
|
when(fixture.mapper.insertRealtimeRecord(any())).thenReturn(1);
|
|
fixture.service().captureDevelopmentRealtimeSnapshots();
|
|
ArgumentCaptor<TTestRealtimeRecord> saved = ArgumentCaptor.forClass(TTestRealtimeRecord.class);
|
verify(fixture.mapper).insertRealtimeRecord(saved.capture());
|
TTestRealtimeRecord row = saved.getValue();
|
assertEquals(1L, row.getTestId());
|
assertEquals(4L, row.getTestBatteryId());
|
assertEquals(2L, row.getBindingId());
|
assertEquals("dev-sim:22:1:1:1", row.getSourceIdentity());
|
assertEquals("cabinet:7:collector:10", row.getSourceRecordId());
|
assertEquals(Date.from(Instant.parse("2026-09-02T04:00:30Z")), row.getCabinetDeviceTimeUtc());
|
assertEquals(Date.from(Instant.parse("2026-09-02T04:00:40Z")), row.getCollectorDeviceTimeUtc());
|
assertEquals(new BigDecimal("25.100"), row.getTemperature());
|
assertEquals(new BigDecimal("1.620"), row.getPositivePotential());
|
assertEquals(new BigDecimal("1.610"), row.getNegativePotential());
|
assertTrue(row.getExtensionMeasurementsJson().contains("AMBIENT_TEMPERATURE"));
|
assertTrue(!row.getExtensionMeasurementsJson().contains("POSITIVE_POTENTIAL"));
|
}
|
|
@Test
|
void savesCollectorFieldsWhenCabinetSourceFails()
|
{
|
Fixture fixture = fixture();
|
when(fixture.simulator.btsRecords(1, 1)).thenThrow(new IllegalArgumentException("offline"));
|
when(fixture.simulator.collectorRealtime(9)).thenReturn(collectorSnapshot(true,
|
List.of(collector("S-01", 11L, "TEMPERATURE", "26.2", "℃"))));
|
when(fixture.mapper.insertRealtimeRecord(any())).thenReturn(1);
|
|
fixture.service().captureDevelopmentRealtimeSnapshots();
|
|
ArgumentCaptor<TTestRealtimeRecord> saved = ArgumentCaptor.forClass(TTestRealtimeRecord.class);
|
verify(fixture.mapper).insertRealtimeRecord(saved.capture());
|
assertNull(saved.getValue().getCabinetSequenceId());
|
assertNull(saved.getValue().getCabinetDevtype());
|
assertNull(saved.getValue().getCabinetAdapterTimezoneId());
|
assertNull(saved.getValue().getVoltageV());
|
assertEquals(11L, saved.getValue().getCollectorSequenceId());
|
assertEquals("collector:11", saved.getValue().getSourceRecordId());
|
assertEquals(new BigDecimal("26.2"), saved.getValue().getTemperature());
|
assertNull(saved.getValue().getPositivePotential());
|
}
|
|
@Test
|
void savesOnlyDeviceReturnedStepAndCycleRecords()
|
{
|
Fixture fixture = fixture();
|
Instant started = Instant.parse("2026-09-02T04:00:10Z");
|
Instant ended = Instant.parse("2026-09-02T04:00:50Z");
|
when(fixture.simulator.btsSteps(1, 1)).thenReturn(List.of(new BtsStepRecord(
|
1, 2, 2, "恒流充电", "cc", 41, started, ended, 40,
|
2.5, 8.4, 3.1, 3.4, 3.4, 0)));
|
when(fixture.simulator.btsCycles(1, 1)).thenReturn(List.of(new BtsCycleRecord(
|
"CYCLE-1-1-1", 1, 80, started, ended, 2.5, 2.4, 8.4, 8.0, 96)));
|
when(fixture.mapper.insertStepRecord(any())).thenReturn(1);
|
when(fixture.mapper.insertCycleRecord(any())).thenReturn(1);
|
|
fixture.service().captureDevelopmentCabinetRecords();
|
|
ArgumentCaptor<TTestStepRecord> step = ArgumentCaptor.forClass(TTestStepRecord.class);
|
ArgumentCaptor<TTestCycleRecord> cycle = ArgumentCaptor.forClass(TTestCycleRecord.class);
|
verify(fixture.mapper).insertStepRecord(step.capture());
|
verify(fixture.mapper).insertCycleRecord(cycle.capture());
|
assertEquals("fallback:2026-09-02T04:00:50Z:1:2", step.getValue().getDedupeKey());
|
assertEquals("CYCLE-1-1-1", cycle.getValue().getSourceRecordId());
|
assertEquals("record:CYCLE-1-1-1", cycle.getValue().getDedupeKey());
|
assertEquals(Date.from(CAPTURED_AT), step.getValue().getServerReceivedAtUtc());
|
assertEquals(Date.from(CAPTURED_AT), cycle.getValue().getServerReceivedAtUtc());
|
}
|
|
@Test
|
void ignoresSameRealtimeContentOnRepeatedReception()
|
{
|
Fixture fixture = fixture();
|
when(fixture.simulator.btsRecords(1, 1)).thenReturn(List.of(cabinetRecord()));
|
when(fixture.simulator.collectorRealtime(9)).thenReturn(collectorSnapshot(false, List.of()));
|
when(fixture.mapper.insertRealtimeRecord(any())).thenReturn(1);
|
|
fixture.service().captureDevelopmentRealtimeSnapshots();
|
ArgumentCaptor<TTestRealtimeRecord> saved = ArgumentCaptor.forClass(TTestRealtimeRecord.class);
|
verify(fixture.mapper).insertRealtimeRecord(saved.capture());
|
when(fixture.mapper.selectRealtimeRecordContentHash(eq(1L), eq(4L), any(Date.class)))
|
.thenReturn(HexFormat.of().formatHex(saved.getValue().getContentSha256()));
|
|
fixture.service().captureDevelopmentRealtimeSnapshots();
|
|
verify(fixture.mapper, times(1)).insertRealtimeRecord(any());
|
}
|
|
@Test
|
void rejectsConflictingRealtimeContentAndWritesApplicationLog(CapturedOutput output)
|
{
|
Fixture fixture = fixture();
|
when(fixture.simulator.btsRecords(1, 1)).thenReturn(List.of(cabinetRecord()));
|
when(fixture.simulator.collectorRealtime(9)).thenReturn(collectorSnapshot(false, List.of()));
|
when(fixture.mapper.selectRealtimeRecordContentHash(eq(1L), eq(4L), any(Date.class)))
|
.thenReturn("different-content");
|
|
fixture.service().captureDevelopmentRealtimeSnapshots();
|
|
verify(fixture.mapper, never()).insertRealtimeRecord(any());
|
assertTrue(output.getOut().contains("实时采样内容冲突"));
|
}
|
|
private static Fixture fixture()
|
{
|
TTestMapper mapper = mock(TTestMapper.class);
|
ITTestService tests = mock(ITTestService.class);
|
BtsDevelopmentSimulator simulator = mock(BtsDevelopmentSimulator.class);
|
TTest test = receivingTest();
|
when(mapper.selectReceivingTestIds()).thenReturn(List.of(1L));
|
when(mapper.selectTestByIdForUpdate(1L)).thenReturn(test);
|
when(tests.selectTestById(1L)).thenReturn(test);
|
when(simulator.persistenceIntervalMillis(1, 1)).thenReturn(60000L);
|
return new Fixture(mapper, tests, simulator);
|
}
|
|
private static TTest receivingTest()
|
{
|
TTestChannelBinding binding = new TTestChannelBinding();
|
binding.setBindingId(2L);
|
binding.setChannelId(3L);
|
binding.setAdapterCode("dev-sim");
|
binding.setAdapterTimezoneId("Asia/Hong_Kong");
|
binding.setDevtype(22);
|
binding.setDevid(1);
|
binding.setSubdevid(1);
|
binding.setChlid(1);
|
TTestBattery battery = new TTestBattery();
|
battery.setTestBatteryId(4L);
|
battery.setCollectorId(9L);
|
battery.setBinding(binding);
|
battery.setCollectorChannels(List.of(
|
mapping("TEMPERATURE", "S-01"), mapping("POSITIVE_POTENTIAL", "S-02"),
|
mapping("NEGATIVE_POTENTIAL", "S-03"), mapping("AMBIENT_TEMPERATURE", "S-04")));
|
TTest test = new TTest();
|
test.setTestId(1L);
|
test.setTestStatus("RUNNING");
|
test.setReceptionStatus("RECEIVING");
|
test.setStartedAtUtc(Date.from(Instant.parse("2026-09-02T04:00:00Z")));
|
test.setReceptionBoundaryAtUtc(Date.from(Instant.parse("2026-09-02T04:00:00Z")));
|
test.setBatteries(List.of(battery));
|
return test;
|
}
|
|
private static TTestCollectorChannel mapping(String metric, String channel)
|
{
|
TTestCollectorChannel mapping = new TTestCollectorChannel();
|
mapping.setMeasurementCode(metric);
|
mapping.setChannelCode(channel);
|
return mapping;
|
}
|
|
private static BtsHistoryRecord cabinetRecord()
|
{
|
Instant deviceTime = Instant.parse("2026-09-02T04:00:30Z");
|
return new BtsHistoryRecord(7L, deviceTime, 1, 3.2, 2.5, 8.4,
|
1, 2, "恒流充电", "cc", Instant.parse("2026-09-02T04:00:35Z"));
|
}
|
|
private static CollectorRealtimeMeasurement collector(String channel, long sequenceId, String metric,
|
String value, String unit)
|
{
|
return new CollectorRealtimeMeasurement("dev-sim", "collector-9", channel, sequenceId,
|
Instant.parse("2026-09-02T04:00:40Z"), "2026-09-02 12:00:40.000",
|
Instant.parse("2026-09-02T04:00:50Z"), metric, new BigDecimal(value), unit);
|
}
|
|
private static CollectorRealtimeSnapshot collectorSnapshot(boolean online,
|
List<CollectorRealtimeMeasurement> measurements)
|
{
|
return new CollectorRealtimeSnapshot("collector-9", online, 1000, measurements);
|
}
|
|
private record Fixture(TTestMapper mapper, ITTestService tests, BtsDevelopmentSimulator simulator)
|
{
|
TestDataReceptionServiceImpl service()
|
{
|
return new TestDataReceptionServiceImpl(mapper, tests, simulator, CLOCK);
|
}
|
}
|
}
|