package com.ruoyi.order.service.impl; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; import com.ruoyi.account.api.dto.ChargingCardDeductDTO; import com.ruoyi.account.api.feignClient.ChargingCardClient; import com.ruoyi.account.api.vo.ChargingCardDeductVO; import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient; import com.ruoyi.chargingPile.api.model.TChargingGun; import com.ruoyi.common.core.domain.R; import com.ruoyi.integration.api.model.UploadRealTimeMonitoringData; import com.ruoyi.order.api.model.TCardAccountingStrategyDetailOrder; import com.ruoyi.order.api.model.TCardChargingOrder; import com.ruoyi.order.api.model.TCardChargingOrderAccountingStrategy; import com.ruoyi.order.api.model.TVinAccountingStrategyDetailOrder; import com.ruoyi.order.api.model.TVinChargingOrder; import com.ruoyi.order.api.model.TVinChargingOrderAccountingStrategy; import com.ruoyi.order.api.vo.TransactionRecordMessageVO; import com.ruoyi.order.mapper.TCardChargingOrderMapper; import com.ruoyi.order.mapper.TVinChargingOrderMapper; import com.ruoyi.order.service.TCardAccountingStrategyDetailOrderService; import com.ruoyi.order.service.TCardChargingOrderAccountingStrategyService; import com.ruoyi.order.service.TVinAccountingStrategyDetailOrderService; import com.ruoyi.order.service.TVinChargingOrderAccountingStrategyService; import com.ruoyi.order.util.mongodb.service.UploadRealTimeMonitoringDataService; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * VIN/刷卡充电结算明细开始/结束时间口径对齐测试(与 CD 充电 TaskUtil.endOrder 一致): * 1) 循环内:start_time/end_time 截取快照时段位 HH:mm; * 2) 循环外:details 按策略开始时间顺序生成,首段 start_time 用订单实际开始时间 HH:mm 覆盖, * 末段 end_time 用订单实际结束时间 HH:mm 覆盖; * 3) 订单实际时间缺失时保持快照时段位兜底。 * * 注:本测试类中所有 R 类型的 mock 都在测试体内显式分步 stubbing, * 避免在 .when().thenReturn() 调用链中再嵌套调用其他会触发 Mockito 状态切换的辅助方法。 */ class CardVinSettlementTimeAlignmentTest { @BeforeAll static void initMybatisPlusTableInfo() { MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); TableInfoHelper.initTableInfo(assistant, TCardChargingOrder.class); TableInfoHelper.initTableInfo(assistant, TVinChargingOrder.class); } private RLock mockedLock(boolean tryLockResult) throws InterruptedException { RLock lock = mock(RLock.class); when(lock.tryLock(anyLong(), anyLong(), any(TimeUnit.class))).thenReturn(tryLockResult); when(lock.isHeldByCurrentThread()).thenReturn(false); return lock; } private RedissonClient mockedRedisson(RLock lock) { RedissonClient redisson = mock(RedissonClient.class); when(redisson.getLock(anyString())).thenReturn(lock); return redisson; } /** 构造三段(尖 00-08 / 平 08-16 / 谷 16-23:59)的快照 */ private List vinThreePeriodSnapshots() { TVinAccountingStrategyDetailOrder spike = new TVinAccountingStrategyDetailOrder(); spike.setId(1); spike.setType(1); spike.setStartTime("00:00"); spike.setEndTime("08:00"); spike.setElectrovalence(new BigDecimal("1.2000")); spike.setServiceCharge(new BigDecimal("0.6000")); spike.setCostServiceCharge(new BigDecimal("0.6000")); TVinAccountingStrategyDetailOrder flat = new TVinAccountingStrategyDetailOrder(); flat.setId(3); flat.setType(3); flat.setStartTime("08:00"); flat.setEndTime("16:00"); flat.setElectrovalence(new BigDecimal("0.9000")); flat.setServiceCharge(new BigDecimal("0.5000")); flat.setCostServiceCharge(new BigDecimal("0.5000")); TVinAccountingStrategyDetailOrder valley = new TVinAccountingStrategyDetailOrder(); valley.setId(4); valley.setType(4); valley.setStartTime("16:00"); valley.setEndTime("23:59"); valley.setElectrovalence(new BigDecimal("0.5000")); valley.setServiceCharge(new BigDecimal("0.4000")); valley.setCostServiceCharge(new BigDecimal("0.4000")); return Arrays.asList(spike, flat, valley); } private List cardThreePeriodSnapshots() { TCardAccountingStrategyDetailOrder spike = new TCardAccountingStrategyDetailOrder(); spike.setId(11); spike.setType(1); spike.setStartTime("00:00"); spike.setEndTime("08:00"); spike.setElectrovalence(new BigDecimal("1.2000")); spike.setServiceCharge(new BigDecimal("0.6000")); spike.setCostServiceCharge(new BigDecimal("0.6000")); TCardAccountingStrategyDetailOrder flat = new TCardAccountingStrategyDetailOrder(); flat.setId(13); flat.setType(3); flat.setStartTime("08:00"); flat.setEndTime("16:00"); flat.setElectrovalence(new BigDecimal("0.9000")); flat.setServiceCharge(new BigDecimal("0.5000")); flat.setCostServiceCharge(new BigDecimal("0.5000")); TCardAccountingStrategyDetailOrder valley = new TCardAccountingStrategyDetailOrder(); valley.setId(14); valley.setType(4); valley.setStartTime("16:00"); valley.setEndTime("23:59"); valley.setElectrovalence(new BigDecimal("0.5000")); valley.setServiceCharge(new BigDecimal("0.4000")); valley.setCostServiceCharge(new BigDecimal("0.4000")); return Arrays.asList(spike, flat, valley); } private List vinSixPeriodSnapshots() { return Arrays.asList( vinSnapshot(948524, 4, "00:00", "08:00"), vinSnapshot(948525, 3, "08:00", "10:00"), vinSnapshot(948526, 2, "10:00", "12:00"), vinSnapshot(948527, 3, "12:00", "17:00"), vinSnapshot(948528, 2, "17:00", "22:00"), vinSnapshot(948529, 4, "22:00", "00:00")); } private TVinAccountingStrategyDetailOrder vinSnapshot(int id, int type, String start, String end) { TVinAccountingStrategyDetailOrder snapshot = new TVinAccountingStrategyDetailOrder(); snapshot.setId(id); snapshot.setType(type); snapshot.setStartTime(start); snapshot.setEndTime(end); snapshot.setElectrovalence(new BigDecimal("1.0000")); snapshot.setServiceCharge(new BigDecimal("1.0000")); snapshot.setCostServiceCharge(new BigDecimal("1.0000")); return snapshot; } private List cardSixPeriodSnapshots() { return Arrays.asList( cardSnapshot(31, 4, "00:00", "08:00"), cardSnapshot(32, 3, "08:00", "10:00"), cardSnapshot(33, 2, "10:00", "12:00"), cardSnapshot(34, 3, "12:00", "17:00"), cardSnapshot(35, 2, "17:00", "22:00"), cardSnapshot(36, 4, "22:00", "00:00")); } private TCardAccountingStrategyDetailOrder cardSnapshot(int id, int type, String start, String end) { TCardAccountingStrategyDetailOrder snapshot = new TCardAccountingStrategyDetailOrder(); snapshot.setId(id); snapshot.setType(type); snapshot.setStartTime(start); snapshot.setEndTime(end); snapshot.setElectrovalence(new BigDecimal("1.0000")); snapshot.setServiceCharge(new BigDecimal("1.0000")); snapshot.setCostServiceCharge(new BigDecimal("1.0000")); return snapshot; } private void stubVinStrategyQuerySave(TVinAccountingStrategyDetailOrderService detailOrderService, TVinChargingOrderAccountingStrategyService strategyService, List snapshots) { LambdaQueryChainWrapper queryWrapper = mock(LambdaQueryChainWrapper.class); when(detailOrderService.lambdaQuery()).thenReturn(queryWrapper); when(queryWrapper.eq(any(), any())).thenReturn(queryWrapper); when(queryWrapper.list()).thenReturn(snapshots); LambdaUpdateChainWrapper updateWrapper = mock(LambdaUpdateChainWrapper.class); when(strategyService.lambdaUpdate()).thenReturn(updateWrapper); when(updateWrapper.eq(any(), any())).thenReturn(updateWrapper); when(updateWrapper.remove()).thenReturn(true); when(strategyService.saveBatch(any(Collection.class))).thenReturn(true); } private void stubCardStrategyQuerySave(TCardAccountingStrategyDetailOrderService detailOrderService, TCardChargingOrderAccountingStrategyService strategyService, List snapshots) { LambdaQueryChainWrapper queryWrapper = mock(LambdaQueryChainWrapper.class); when(detailOrderService.lambdaQuery()).thenReturn(queryWrapper); when(queryWrapper.eq(any(), any())).thenReturn(queryWrapper); when(queryWrapper.list()).thenReturn(snapshots); LambdaUpdateChainWrapper updateWrapper = mock(LambdaUpdateChainWrapper.class); when(strategyService.lambdaUpdate()).thenReturn(updateWrapper); when(updateWrapper.eq(any(), any())).thenReturn(updateWrapper); when(updateWrapper.remove()).thenReturn(true); when(strategyService.saveBatch(any(Collection.class))).thenReturn(true); } private void stubTxExecute(TransactionTemplate txTemplate) { when(txTemplate.execute(any())).thenAnswer(invocation -> { TransactionCallback callback = invocation.getArgument(0); return callback.doInTransaction(mock(TransactionStatus.class)); }); } /** mock 复位枪桩需要的两个 R 返回:getChargingGunById / updateChargingGunById */ private void stubGunAndUpdate(ChargingGunClient gunClient, int gunId) { TChargingGun gun = new TChargingGun(); gun.setId(gunId); gun.setStatus(4); R gunResult = mock(R.class); when(gunResult.getCode()).thenReturn(R.SUCCESS); when(gunResult.getData()).thenReturn(gun); when(gunClient.getChargingGunById(any())).thenReturn(gunResult); R updateResult = mock(R.class); when(updateResult.getCode()).thenReturn(R.SUCCESS); when(gunClient.updateChargingGunById(any(TChargingGun.class))).thenReturn(updateResult); } /** mock 刷卡解锁/扣款 R 返回 */ private void stubCardDeductAndUnlock(ChargingCardClient cardClient) { R unlockResult = mock(R.class); when(unlockResult.getCode()).thenReturn(R.SUCCESS); when(unlockResult.getData()).thenReturn(true); when(cardClient.unlockForCharging(anyLong(), anyString())).thenReturn(unlockResult); ChargingCardDeductVO deductVO = new ChargingCardDeductVO(); deductVO.setDuplicated(false); R deductResult = mock(R.class); when(deductResult.getCode()).thenReturn(R.SUCCESS); when(deductResult.getData()).thenReturn(deductVO); when(cardClient.deductForCharging(any(ChargingCardDeductDTO.class))).thenReturn(deductResult); } // ===== VD:多时段首末段覆盖 ===== // 订单实际开始 07:30 / 实际结束 21:45;快照三段(尖/平/谷)。 // 期望:首段(type=1)start_time=07:30;末段(type=4)end_time=21:45;中段(type=3)保持快照时段位。 @Test @SuppressWarnings("unchecked") void vinThreePeriodOverlaysStartAndEndWithOrderActualTime() throws InterruptedException { TVinChargingOrderServiceImpl service = new TVinChargingOrderServiceImpl(); TVinChargingOrderMapper mapper = mock(TVinChargingOrderMapper.class); ChargingGunClient gunClient = mock(ChargingGunClient.class); UploadRealTimeMonitoringDataService monitoringDataService = mock(UploadRealTimeMonitoringDataService.class); TVinAccountingStrategyDetailOrderService detailOrderService = mock(TVinAccountingStrategyDetailOrderService.class); TVinChargingOrderAccountingStrategyService strategyService = mock(TVinChargingOrderAccountingStrategyService.class); TransactionTemplate txTemplate = mock(TransactionTemplate.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "chargingGunClient", gunClient); ReflectionTestUtils.setField(service, "monitoringDataService", monitoringDataService); ReflectionTestUtils.setField(service, "vinAccountingStrategyDetailOrderService", detailOrderService); ReflectionTestUtils.setField(service, "vinChargingOrderAccountingStrategyService", strategyService); ReflectionTestUtils.setField(service, "transactionTemplate", txTemplate); ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true))); TVinChargingOrder charging = new TVinChargingOrder(); charging.setId(2L); charging.setCode("VD1"); charging.setStatus(4); charging.setChargingGunId(7); charging.setStartTime(LocalDateTime.of(2026, 9, 14, 7, 30, 0)); charging.setEndTime(LocalDateTime.of(2026, 9, 14, 21, 45, 0)); when(mapper.selectOne(any())).thenReturn(charging); when(mapper.updateById(any())).thenReturn(1); UploadRealTimeMonitoringData firstMonitoring = new UploadRealTimeMonitoringData(); firstMonitoring.setSoc(18); UploadRealTimeMonitoringData lastMonitoring = new UploadRealTimeMonitoringData(); lastMonitoring.setSoc(87); when(monitoringDataService.getDataByOrderCode("VD1", "asc")).thenReturn(firstMonitoring); when(monitoringDataService.getDataByOrderCode("VD1", "desc")).thenReturn(lastMonitoring); stubGunAndUpdate(gunClient, 7); stubVinStrategyQuerySave(detailOrderService, strategyService, vinThreePeriodSnapshots()); stubTxExecute(txTemplate); // VO:尖 3 / 平 5 / 谷 8 度,触发三段写入 TransactionRecordMessageVO vo = new TransactionRecordMessageVO(); vo.setTransaction_serial_number("VD1"); vo.setSpike_charge1(new BigDecimal("3")); vo.setFlat_charge2(new BigDecimal("5")); vo.setValley_charge3(new BigDecimal("8")); R result = service.vinEndChargeBillingCharge(vo); assertEquals(R.SUCCESS, result.getCode()); ArgumentCaptor orderCaptor = ArgumentCaptor.forClass(TVinChargingOrder.class); verify(mapper).updateById(orderCaptor.capture()); assertEquals(0, new BigDecimal("18").compareTo(orderCaptor.getValue().getStartSoc())); assertEquals(0, new BigDecimal("87").compareTo(orderCaptor.getValue().getEndSoc())); verify(monitoringDataService).getDataByOrderCode("VD1", "asc"); verify(monitoringDataService).getDataByOrderCode("VD1", "desc"); ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); verify(strategyService).saveBatch(captor.capture()); List saved = new ArrayList<>(captor.getValue()); assertEquals(3, saved.size()); assertEquals(Integer.valueOf(1), saved.get(0).getType()); assertEquals(Integer.valueOf(3), saved.get(1).getType()); assertEquals(Integer.valueOf(4), saved.get(2).getType()); // 首段:start_time = 07:30(订单实际开始时间 HH:mm);end_time 保持快照 "08:00" assertEquals("07:30", saved.get(0).getStartTime()); assertEquals("08:00", saved.get(0).getEndTime()); // 中段:保持快照时段位 assertEquals("08:00", saved.get(1).getStartTime()); assertEquals("16:00", saved.get(1).getEndTime()); // 末段:end_time = 21:45(订单实际结束时间 HH:mm);start_time 保持快照 "16:00" assertEquals("16:00", saved.get(2).getStartTime()); assertEquals("21:45", saved.get(2).getEndTime()); saved.forEach(s -> { assertNotNull(s.getCreateTime()); assertNotNull(s.getAccountingStrategyDetailId()); }); } // ===== VD:订单 startTime/endTime 缺失时首末段保持快照兜底 ===== @Test @SuppressWarnings("unchecked") void vinThreePeriodKeepsSnapshotWhenOrderTimeMissing() throws InterruptedException { TVinChargingOrderServiceImpl service = new TVinChargingOrderServiceImpl(); TVinChargingOrderMapper mapper = mock(TVinChargingOrderMapper.class); ChargingGunClient gunClient = mock(ChargingGunClient.class); UploadRealTimeMonitoringDataService monitoringDataService = mock(UploadRealTimeMonitoringDataService.class); TVinAccountingStrategyDetailOrderService detailOrderService = mock(TVinAccountingStrategyDetailOrderService.class); TVinChargingOrderAccountingStrategyService strategyService = mock(TVinChargingOrderAccountingStrategyService.class); TransactionTemplate txTemplate = mock(TransactionTemplate.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "chargingGunClient", gunClient); ReflectionTestUtils.setField(service, "monitoringDataService", monitoringDataService); ReflectionTestUtils.setField(service, "vinAccountingStrategyDetailOrderService", detailOrderService); ReflectionTestUtils.setField(service, "vinChargingOrderAccountingStrategyService", strategyService); ReflectionTestUtils.setField(service, "transactionTemplate", txTemplate); ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true))); TVinChargingOrder charging = new TVinChargingOrder(); charging.setId(2L); charging.setCode("VD1"); charging.setStatus(4); charging.setChargingGunId(7); // startTime/endTime 均为 null,Mongo 首末监控记录也不存在 when(mapper.selectOne(any())).thenReturn(charging); when(mapper.updateById(any())).thenReturn(1); when(monitoringDataService.getDataByOrderCode("VD1", "asc")).thenReturn(null); when(monitoringDataService.getDataByOrderCode("VD1", "desc")).thenReturn(null); stubGunAndUpdate(gunClient, 7); stubVinStrategyQuerySave(detailOrderService, strategyService, vinThreePeriodSnapshots()); stubTxExecute(txTemplate); TransactionRecordMessageVO vo = new TransactionRecordMessageVO(); vo.setTransaction_serial_number("VD1"); vo.setSpike_charge1(new BigDecimal("3")); vo.setFlat_charge2(new BigDecimal("5")); vo.setValley_charge3(new BigDecimal("8")); R result = service.vinEndChargeBillingCharge(vo); assertEquals(R.SUCCESS, result.getCode()); ArgumentCaptor orderCaptor = ArgumentCaptor.forClass(TVinChargingOrder.class); verify(mapper).updateById(orderCaptor.capture()); assertNull(orderCaptor.getValue().getStartSoc()); assertNull(orderCaptor.getValue().getEndSoc()); verify(monitoringDataService).getDataByOrderCode("VD1", "asc"); verify(monitoringDataService).getDataByOrderCode("VD1", "desc"); ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); verify(strategyService).saveBatch(captor.capture()); List saved = new ArrayList<>(captor.getValue()); assertEquals(3, saved.size()); // 首末段均未覆盖,保持快照时段位 assertEquals("00:00", saved.get(0).getStartTime()); assertEquals("08:00", saved.get(0).getEndTime()); assertEquals("16:00", saved.get(2).getStartTime()); assertEquals("23:59", saved.get(2).getEndTime()); } // ===== SD:多时段首末段覆盖 ===== @Test @SuppressWarnings("unchecked") void cardThreePeriodOverlaysStartAndEndWithOrderActualTime() throws InterruptedException { TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl(); TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class); ChargingCardClient cardClient = mock(ChargingCardClient.class); ChargingGunClient gunClient = mock(ChargingGunClient.class); TCardAccountingStrategyDetailOrderService detailOrderService = mock(TCardAccountingStrategyDetailOrderService.class); TCardChargingOrderAccountingStrategyService strategyService = mock(TCardChargingOrderAccountingStrategyService.class); TransactionTemplate txTemplate = mock(TransactionTemplate.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "chargingCardClient", cardClient); ReflectionTestUtils.setField(service, "chargingGunClient", gunClient); ReflectionTestUtils.setField(service, "cardAccountingStrategyDetailOrderService", detailOrderService); ReflectionTestUtils.setField(service, "cardChargingOrderAccountingStrategyService", strategyService); ReflectionTestUtils.setField(service, "transactionTemplate", txTemplate); ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true))); TCardChargingOrder charging = new TCardChargingOrder(); charging.setId(1L); charging.setCode("SD1"); charging.setStatus(4); charging.setCardId(5L); charging.setChargingGunId(9); charging.setChargeAmount(new BigDecimal("100.00")); charging.setStartTime(LocalDateTime.of(2026, 9, 14, 7, 30, 0)); charging.setEndTime(LocalDateTime.of(2026, 9, 14, 21, 45, 0)); when(mapper.selectOne(any())).thenReturn(charging); when(mapper.updateById(any())).thenReturn(1); stubGunAndUpdate(gunClient, 9); stubCardDeductAndUnlock(cardClient); stubCardStrategyQuerySave(detailOrderService, strategyService, cardThreePeriodSnapshots()); stubTxExecute(txTemplate); TransactionRecordMessageVO vo = new TransactionRecordMessageVO(); vo.setTransaction_serial_number("SD1"); vo.setSpike_charge1(new BigDecimal("3")); vo.setFlat_charge2(new BigDecimal("5")); vo.setValley_charge3(new BigDecimal("8")); R result = service.cardEndChargeBillingCharge(vo); assertEquals(R.SUCCESS, result.getCode()); ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); verify(strategyService).saveBatch(captor.capture()); List saved = new ArrayList<>(captor.getValue()); assertEquals(3, saved.size()); assertEquals(Integer.valueOf(1), saved.get(0).getType()); assertEquals(Integer.valueOf(3), saved.get(1).getType()); assertEquals(Integer.valueOf(4), saved.get(2).getType()); // 首段:start_time = 07:30;end_time 保持快照 "08:00" assertEquals("07:30", saved.get(0).getStartTime()); assertEquals("08:00", saved.get(0).getEndTime()); // 中段:保持快照时段位 assertEquals("08:00", saved.get(1).getStartTime()); assertEquals("16:00", saved.get(1).getEndTime()); // 末段:end_time = 21:45;start_time 保持快照 "16:00" assertEquals("16:00", saved.get(2).getStartTime()); assertEquals("21:45", saved.get(2).getEndTime()); saved.forEach(s -> { assertNotNull(s.getCreateTime()); assertNotNull(s.getAccountingStrategyDetailId()); }); } // ===== SD:订单 startTime/endTime 缺失时首末段保持快照兜底 ===== @Test @SuppressWarnings("unchecked") void cardThreePeriodKeepsSnapshotWhenOrderTimeMissing() throws InterruptedException { TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl(); TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class); ChargingCardClient cardClient = mock(ChargingCardClient.class); ChargingGunClient gunClient = mock(ChargingGunClient.class); TCardAccountingStrategyDetailOrderService detailOrderService = mock(TCardAccountingStrategyDetailOrderService.class); TCardChargingOrderAccountingStrategyService strategyService = mock(TCardChargingOrderAccountingStrategyService.class); TransactionTemplate txTemplate = mock(TransactionTemplate.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "chargingCardClient", cardClient); ReflectionTestUtils.setField(service, "chargingGunClient", gunClient); ReflectionTestUtils.setField(service, "cardAccountingStrategyDetailOrderService", detailOrderService); ReflectionTestUtils.setField(service, "cardChargingOrderAccountingStrategyService", strategyService); ReflectionTestUtils.setField(service, "transactionTemplate", txTemplate); ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true))); TCardChargingOrder charging = new TCardChargingOrder(); charging.setId(1L); charging.setCode("SD1"); charging.setStatus(4); charging.setCardId(5L); charging.setChargingGunId(9); charging.setChargeAmount(new BigDecimal("100.00")); when(mapper.selectOne(any())).thenReturn(charging); when(mapper.updateById(any())).thenReturn(1); stubGunAndUpdate(gunClient, 9); stubCardDeductAndUnlock(cardClient); stubCardStrategyQuerySave(detailOrderService, strategyService, cardThreePeriodSnapshots()); stubTxExecute(txTemplate); TransactionRecordMessageVO vo = new TransactionRecordMessageVO(); vo.setTransaction_serial_number("SD1"); vo.setSpike_charge1(new BigDecimal("3")); vo.setFlat_charge2(new BigDecimal("5")); vo.setValley_charge3(new BigDecimal("8")); R result = service.cardEndChargeBillingCharge(vo); assertEquals(R.SUCCESS, result.getCode()); ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); verify(strategyService).saveBatch(captor.capture()); List saved = new ArrayList<>(captor.getValue()); assertEquals(3, saved.size()); assertEquals("00:00", saved.get(0).getStartTime()); assertEquals("08:00", saved.get(0).getEndTime()); assertEquals("16:00", saved.get(2).getStartTime()); assertEquals("23:59", saved.get(2).getEndTime()); } // ===== SD:单时段且订单时间缺失,保持快照时段位(兼容 V0.2 现有行为) ===== @Test @SuppressWarnings("unchecked") void cardSinglePeriodKeepsSnapshotWhenOrderTimeMissing() throws InterruptedException { TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl(); TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class); ChargingCardClient cardClient = mock(ChargingCardClient.class); ChargingGunClient gunClient = mock(ChargingGunClient.class); TCardAccountingStrategyDetailOrderService detailOrderService = mock(TCardAccountingStrategyDetailOrderService.class); TCardChargingOrderAccountingStrategyService strategyService = mock(TCardChargingOrderAccountingStrategyService.class); TransactionTemplate txTemplate = mock(TransactionTemplate.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "chargingCardClient", cardClient); ReflectionTestUtils.setField(service, "chargingGunClient", gunClient); ReflectionTestUtils.setField(service, "cardAccountingStrategyDetailOrderService", detailOrderService); ReflectionTestUtils.setField(service, "cardChargingOrderAccountingStrategyService", strategyService); ReflectionTestUtils.setField(service, "transactionTemplate", txTemplate); ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true))); TCardChargingOrder charging = new TCardChargingOrder(); charging.setId(1L); charging.setCode("SD1"); charging.setStatus(4); charging.setCardId(5L); charging.setChargingGunId(9); charging.setChargeAmount(new BigDecimal("100.00")); when(mapper.selectOne(any())).thenReturn(charging); when(mapper.updateById(any())).thenReturn(1); stubGunAndUpdate(gunClient, 9); stubCardDeductAndUnlock(cardClient); // 单段快照(type=1,00:00-06:00) TCardAccountingStrategyDetailOrder spike = new TCardAccountingStrategyDetailOrder(); spike.setId(11); spike.setType(1); spike.setStartTime("00:00"); spike.setEndTime("06:00"); spike.setElectrovalence(new BigDecimal("1.0000")); spike.setServiceCharge(new BigDecimal("0.5000")); spike.setCostServiceCharge(new BigDecimal("0.5000")); stubCardStrategyQuerySave(detailOrderService, strategyService, Collections.singletonList(spike)); stubTxExecute(txTemplate); TransactionRecordMessageVO vo = new TransactionRecordMessageVO(); vo.setTransaction_serial_number("SD1"); vo.setSpike_charge1(new BigDecimal("5")); R result = service.cardEndChargeBillingCharge(vo); assertEquals(R.SUCCESS, result.getCode()); ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); verify(strategyService).saveBatch(captor.capture()); List saved = new ArrayList<>(captor.getValue()); assertEquals(1, saved.size()); // 单段:start/end 都是快照时段位(订单时间为 null 时不覆盖) assertEquals("00:00", saved.get(0).getStartTime()); assertEquals("06:00", saved.get(0).getEndTime()); } @Test @SuppressWarnings("unchecked") void vinFlatCharge4UsesSnapshot948527ForReportedOrder() throws InterruptedException { TVinChargingOrderServiceImpl service = new TVinChargingOrderServiceImpl(); TVinChargingOrderMapper mapper = mock(TVinChargingOrderMapper.class); ChargingGunClient gunClient = mock(ChargingGunClient.class); TVinAccountingStrategyDetailOrderService detailOrderService = mock(TVinAccountingStrategyDetailOrderService.class); TVinChargingOrderAccountingStrategyService strategyService = mock(TVinChargingOrderAccountingStrategyService.class); TransactionTemplate txTemplate = mock(TransactionTemplate.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "chargingGunClient", gunClient); ReflectionTestUtils.setField(service, "vinAccountingStrategyDetailOrderService", detailOrderService); ReflectionTestUtils.setField(service, "vinChargingOrderAccountingStrategyService", strategyService); ReflectionTestUtils.setField(service, "transactionTemplate", txTemplate); ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true))); TVinChargingOrder charging = new TVinChargingOrder(); charging.setId(2099403373421596674L); charging.setCode("VD10220260914154223913"); charging.setStatus(4); charging.setChargingGunId(7); charging.setStartTime(LocalDateTime.of(2026, 9, 14, 15, 41, 49)); when(mapper.selectOne(any())).thenReturn(charging); when(mapper.updateById(any())).thenReturn(1); stubGunAndUpdate(gunClient, 7); List snapshots = new ArrayList<>(vinSixPeriodSnapshots()); snapshots.get(1).setElectrovalence(new BigDecimal("0.1000")); snapshots.get(3).setElectrovalence(new BigDecimal("1.0000")); Collections.reverse(snapshots); stubVinStrategyQuerySave(detailOrderService, strategyService, snapshots); stubTxExecute(txTemplate); TransactionRecordMessageVO vo = new TransactionRecordMessageVO(); vo.setTransaction_serial_number("VD10220260914154223913"); vo.setFlat_charge4(new BigDecimal("0.167")); vo.setTotal_electricity(new BigDecimal("0.167")); vo.setStart_time("2026-09-14 15:41:49.00"); vo.setEnd_time("2026-09-14 15:43:45.00"); R result = service.vinEndChargeBillingCharge(vo); assertEquals(R.SUCCESS, result.getCode()); ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); verify(strategyService).saveBatch(captor.capture()); List saved = new ArrayList<>(captor.getValue()); assertEquals(1, saved.size()); assertEquals(Integer.valueOf(948527), saved.get(0).getAccountingStrategyDetailId()); assertEquals(Integer.valueOf(3), saved.get(0).getType()); assertEquals("15:41", saved.get(0).getStartTime()); assertEquals("15:43", saved.get(0).getEndTime()); assertEquals(0, new BigDecimal("0.167").compareTo(saved.get(0).getChargingCapacity())); assertEquals(0, new BigDecimal("1.0000").compareTo(saved.get(0).getElectrovalence())); assertEquals(0, new BigDecimal("0.17").compareTo(saved.get(0).getPeriodElectricPrice())); } @Test @SuppressWarnings("unchecked") void cardFlatCharge4UsesFourthSnapshotInsteadOfFirstFlatSnapshot() throws InterruptedException { TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl(); TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class); ChargingCardClient cardClient = mock(ChargingCardClient.class); ChargingGunClient gunClient = mock(ChargingGunClient.class); TCardAccountingStrategyDetailOrderService detailOrderService = mock(TCardAccountingStrategyDetailOrderService.class); TCardChargingOrderAccountingStrategyService strategyService = mock(TCardChargingOrderAccountingStrategyService.class); TransactionTemplate txTemplate = mock(TransactionTemplate.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "chargingCardClient", cardClient); ReflectionTestUtils.setField(service, "chargingGunClient", gunClient); ReflectionTestUtils.setField(service, "cardAccountingStrategyDetailOrderService", detailOrderService); ReflectionTestUtils.setField(service, "cardChargingOrderAccountingStrategyService", strategyService); ReflectionTestUtils.setField(service, "transactionTemplate", txTemplate); ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true))); TCardChargingOrder charging = new TCardChargingOrder(); charging.setId(1L); charging.setCode("SD-SLOT-4"); charging.setStatus(4); charging.setCardId(5L); charging.setChargingGunId(9); charging.setChargeAmount(new BigDecimal("100.00")); charging.setStartTime(LocalDateTime.of(2026, 9, 14, 15, 41, 49)); when(mapper.selectOne(any())).thenReturn(charging); when(mapper.updateById(any())).thenReturn(1); stubGunAndUpdate(gunClient, 9); stubCardDeductAndUnlock(cardClient); List snapshots = new ArrayList<>(cardSixPeriodSnapshots()); snapshots.get(1).setElectrovalence(new BigDecimal("0.1000")); snapshots.get(3).setElectrovalence(new BigDecimal("1.2000")); Collections.reverse(snapshots); stubCardStrategyQuerySave(detailOrderService, strategyService, snapshots); stubTxExecute(txTemplate); TransactionRecordMessageVO vo = new TransactionRecordMessageVO(); vo.setTransaction_serial_number("SD-SLOT-4"); vo.setFlat_charge4(new BigDecimal("0.167")); vo.setTotal_electricity(new BigDecimal("0.167")); vo.setStart_time("2026-09-14 15:41:49.00"); vo.setEnd_time("2026-09-14 15:43:45.00"); R result = service.cardEndChargeBillingCharge(vo); assertEquals(R.SUCCESS, result.getCode()); ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); verify(strategyService).saveBatch(captor.capture()); List saved = new ArrayList<>(captor.getValue()); assertEquals(1, saved.size()); assertEquals(Integer.valueOf(34), saved.get(0).getAccountingStrategyDetailId()); assertEquals(Integer.valueOf(3), saved.get(0).getType()); assertEquals("15:41", saved.get(0).getStartTime()); assertEquals("15:43", saved.get(0).getEndTime()); assertEquals(0, new BigDecimal("0.167").compareTo(saved.get(0).getChargingCapacity())); assertEquals(0, new BigDecimal("1.2000").compareTo(saved.get(0).getElectrovalence())); assertEquals(0, new BigDecimal("0.20").compareTo(saved.get(0).getPeriodElectricPrice())); } @Test @SuppressWarnings("unchecked") void vinPeakCharge5UsesFifthSnapshotAndDeviceEndTime() throws InterruptedException { TVinChargingOrderServiceImpl service = new TVinChargingOrderServiceImpl(); TVinChargingOrderMapper mapper = mock(TVinChargingOrderMapper.class); ChargingGunClient gunClient = mock(ChargingGunClient.class); TVinAccountingStrategyDetailOrderService detailOrderService = mock(TVinAccountingStrategyDetailOrderService.class); TVinChargingOrderAccountingStrategyService strategyService = mock(TVinChargingOrderAccountingStrategyService.class); TransactionTemplate txTemplate = mock(TransactionTemplate.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "chargingGunClient", gunClient); ReflectionTestUtils.setField(service, "vinAccountingStrategyDetailOrderService", detailOrderService); ReflectionTestUtils.setField(service, "vinChargingOrderAccountingStrategyService", strategyService); ReflectionTestUtils.setField(service, "transactionTemplate", txTemplate); ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true))); TVinChargingOrder charging = new TVinChargingOrder(); charging.setId(2L); charging.setCode("VD-SLOT-5"); charging.setStatus(4); charging.setChargingGunId(7); charging.setStartTime(LocalDateTime.of(2026, 9, 11, 19, 46, 15)); when(mapper.selectOne(any())).thenReturn(charging); when(mapper.updateById(any())).thenReturn(1); stubGunAndUpdate(gunClient, 7); stubVinStrategyQuerySave(detailOrderService, strategyService, vinSixPeriodSnapshots()); stubTxExecute(txTemplate); TransactionRecordMessageVO vo = new TransactionRecordMessageVO(); vo.setTransaction_serial_number("VD-SLOT-5"); vo.setPeak_charge5(new BigDecimal("0.182")); vo.setStart_time("2026-09-11 19:46:15.00"); vo.setEnd_time("2026-09-11 19:53:44.00"); R result = service.vinEndChargeBillingCharge(vo); assertEquals(R.SUCCESS, result.getCode()); ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); verify(strategyService).saveBatch(captor.capture()); List saved = new ArrayList<>(captor.getValue()); assertEquals(1, saved.size()); assertEquals(Integer.valueOf(948528), saved.get(0).getAccountingStrategyDetailId()); assertEquals(Integer.valueOf(2), saved.get(0).getType()); assertEquals("19:46", saved.get(0).getStartTime()); assertEquals("19:53", saved.get(0).getEndTime()); assertEquals(0, new BigDecimal("0.182").compareTo(saved.get(0).getChargingCapacity())); } @Test @SuppressWarnings("unchecked") void cardPeakCharge5UsesFifthSnapshotAndDeviceEndTime() throws InterruptedException { TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl(); TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class); ChargingCardClient cardClient = mock(ChargingCardClient.class); ChargingGunClient gunClient = mock(ChargingGunClient.class); TCardAccountingStrategyDetailOrderService detailOrderService = mock(TCardAccountingStrategyDetailOrderService.class); TCardChargingOrderAccountingStrategyService strategyService = mock(TCardChargingOrderAccountingStrategyService.class); TransactionTemplate txTemplate = mock(TransactionTemplate.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "chargingCardClient", cardClient); ReflectionTestUtils.setField(service, "chargingGunClient", gunClient); ReflectionTestUtils.setField(service, "cardAccountingStrategyDetailOrderService", detailOrderService); ReflectionTestUtils.setField(service, "cardChargingOrderAccountingStrategyService", strategyService); ReflectionTestUtils.setField(service, "transactionTemplate", txTemplate); ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true))); TCardChargingOrder charging = new TCardChargingOrder(); charging.setId(1L); charging.setCode("SD-SLOT-5"); charging.setStatus(4); charging.setCardId(5L); charging.setChargingGunId(9); charging.setChargeAmount(new BigDecimal("100.00")); charging.setStartTime(LocalDateTime.of(2026, 9, 11, 20, 16, 48)); when(mapper.selectOne(any())).thenReturn(charging); when(mapper.updateById(any())).thenReturn(1); stubGunAndUpdate(gunClient, 9); stubCardDeductAndUnlock(cardClient); stubCardStrategyQuerySave(detailOrderService, strategyService, cardSixPeriodSnapshots()); stubTxExecute(txTemplate); TransactionRecordMessageVO vo = new TransactionRecordMessageVO(); vo.setTransaction_serial_number("SD-SLOT-5"); vo.setPeak_charge5(new BigDecimal("0.079")); vo.setStart_time("2026-09-11 20:16:48.00"); vo.setEnd_time("2026-09-11 20:20:54.00"); R result = service.cardEndChargeBillingCharge(vo); assertEquals(R.SUCCESS, result.getCode()); ArgumentCaptor> captor = ArgumentCaptor.forClass(Collection.class); verify(strategyService).saveBatch(captor.capture()); List saved = new ArrayList<>(captor.getValue()); assertEquals(1, saved.size()); assertEquals(Integer.valueOf(35), saved.get(0).getAccountingStrategyDetailId()); assertEquals(Integer.valueOf(2), saved.get(0).getType()); assertEquals("20:16", saved.get(0).getStartTime()); assertEquals("20:20", saved.get(0).getEndTime()); assertEquals(0, new BigDecimal("0.079").compareTo(saved.get(0).getChargingCapacity())); } }