package com.ruoyi.order.service.impl; import com.ruoyi.account.api.feignClient.ChargingCardClient; import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient; import com.ruoyi.chargingPile.api.model.TChargingGun; import com.ruoyi.common.core.domain.R; import com.ruoyi.order.api.model.TCardChargingOrder; import com.ruoyi.order.api.model.TVinChargingOrder; import com.ruoyi.order.api.vo.TransactionRecordMessageVO; import com.ruoyi.order.mapper.TCardChargingOrderMapper; import com.ruoyi.order.mapper.TVinChargingOrderMapper; 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 java.util.Collections; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; /** * SD/VD 定时补偿与结算锁行为测试(文档13.2/13.3): * 1) 启动超时关单(状态-1+结束方式0+解锁卡片+复位枪,沿用 t_charging_order 枚举约定); * 2) 结算入口并发锁抢占失败快速返回,不触达订单数据; * 3) 已结算订单重复账单幂等跳过,不再扣款/解锁。 */ class CardVinOrderCompensationTest { @SuppressWarnings("unchecked") private R okBoolean() { R result = mock(R.class); when(result.getData()).thenReturn(true); return result; } @SuppressWarnings("unchecked") private R gunOf(TChargingGun gun) { R result = mock(R.class); when(result.getData()).thenReturn(gun); return result; } 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; } // ===== SD:启动超时关单 ===== @Test void closesCardTimeoutStartingOrderWithCardUnlockAndGunReset() { TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl(); TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class); ChargingCardClient cardClient = mock(ChargingCardClient.class); ChargingGunClient gunClient = mock(ChargingGunClient.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "chargingCardClient", cardClient); ReflectionTestUtils.setField(service, "chargingGunClient", gunClient); when(mapper.updateById(any(TCardChargingOrder.class))).thenReturn(1); R unlockResult = okBoolean(); when(cardClient.unlockForCharging(anyLong(), anyString())).thenReturn(unlockResult); TChargingGun gun = new TChargingGun(); gun.setId(9); gun.setStatus(4); R gunResult = gunOf(gun); when(gunClient.getChargingGunById(any())).thenReturn(gunResult); TCardChargingOrder order = new TCardChargingOrder(); order.setId(1L); order.setCode("SD1"); order.setStatus(2); order.setCardId(5L); order.setChargingGunId(9); service.closeTimeoutStartingOrder(order); ArgumentCaptor captor = ArgumentCaptor.forClass(TCardChargingOrder.class); verify(mapper).updateById(captor.capture()); assertEquals(-1, captor.getValue().getStatus()); assertEquals(0, captor.getValue().getEndMode()); assertNotNull(captor.getValue().getEndTime()); verify(cardClient).unlockForCharging(5L, "SD1"); ArgumentCaptor gunCaptor = ArgumentCaptor.forClass(TChargingGun.class); verify(gunClient).updateChargingGunById(gunCaptor.capture()); assertEquals(2, gunCaptor.getValue().getStatus()); } // ===== SD:结算锁抢占失败快速返回 ===== @Test void returnsOkWithoutTouchingOrderWhenSettleLockBusy() throws InterruptedException { TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl(); TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(false))); TransactionRecordMessageVO vo = new TransactionRecordMessageVO(); vo.setTransaction_serial_number("SD1"); R result = service.cardEndChargeBillingCharge(vo); assertEquals(R.SUCCESS, result.getCode()); verifyNoInteractions(mapper); } // ===== SD:已结算订单重复账单幂等 ===== @Test void skipsCardSettlementWhenOrderAlreadyFinished() throws InterruptedException { TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl(); TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class); ChargingCardClient cardClient = mock(ChargingCardClient.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "chargingCardClient", cardClient); ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true))); TCardChargingOrder finished = new TCardChargingOrder(); finished.setId(1L); finished.setCode("SD1"); finished.setStatus(5); finished.setCardId(5L); when(mapper.selectOne(any())).thenReturn(finished); when(mapper.selectList(any())).thenReturn(Collections.singletonList(finished)); TransactionRecordMessageVO vo = new TransactionRecordMessageVO(); vo.setTransaction_serial_number("SD1"); vo.setFlat_charge1(new java.math.BigDecimal("10")); R result = service.cardEndChargeBillingCharge(vo); assertEquals(R.SUCCESS, result.getCode()); verify(cardClient, never()).deductForCharging(any(com.ruoyi.account.api.dto.ChargingCardDeductDTO.class)); verify(cardClient, never()).unlockForCharging(anyLong(), anyString()); } // ===== VD:启动超时关单(无卡片,仅复位枪) ===== @Test void closesVinTimeoutStartingOrderWithGunReset() { TVinChargingOrderServiceImpl service = new TVinChargingOrderServiceImpl(); TVinChargingOrderMapper mapper = mock(TVinChargingOrderMapper.class); ChargingGunClient gunClient = mock(ChargingGunClient.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "chargingGunClient", gunClient); when(mapper.updateById(any(TVinChargingOrder.class))).thenReturn(1); TChargingGun gun = new TChargingGun(); gun.setId(7); gun.setStatus(4); R gunResult = gunOf(gun); when(gunClient.getChargingGunById(any())).thenReturn(gunResult); TVinChargingOrder order = new TVinChargingOrder(); order.setId(2L); order.setCode("VD1"); order.setStatus(2); order.setChargingGunId(7); service.closeTimeoutStartingOrder(order); ArgumentCaptor captor = ArgumentCaptor.forClass(TVinChargingOrder.class); verify(mapper).updateById(captor.capture()); assertEquals(-1, captor.getValue().getStatus()); assertEquals(0, captor.getValue().getEndMode()); assertNotNull(captor.getValue().getEndTime()); ArgumentCaptor gunCaptor = ArgumentCaptor.forClass(TChargingGun.class); verify(gunClient).updateChargingGunById(gunCaptor.capture()); assertEquals(2, gunCaptor.getValue().getStatus()); } // ===== VD:已结算订单重复账单幂等 ===== @Test void skipsVinSettlementWhenOrderAlreadyFinished() throws InterruptedException { TVinChargingOrderServiceImpl service = new TVinChargingOrderServiceImpl(); TVinChargingOrderMapper mapper = mock(TVinChargingOrderMapper.class); ChargingGunClient gunClient = mock(ChargingGunClient.class); ReflectionTestUtils.setField(service, "baseMapper", mapper); ReflectionTestUtils.setField(service, "chargingGunClient", gunClient); ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true))); TVinChargingOrder finished = new TVinChargingOrder(); finished.setId(2L); finished.setCode("VD1"); finished.setStatus(5); finished.setChargingGunId(7); when(mapper.selectOne(any())).thenReturn(finished); when(mapper.selectList(any())).thenReturn(Collections.singletonList(finished)); TransactionRecordMessageVO vo = new TransactionRecordMessageVO(); vo.setTransaction_serial_number("VD1"); vo.setFlat_charge1(new java.math.BigDecimal("8")); R result = service.vinEndChargeBillingCharge(vo); assertEquals(R.SUCCESS, result.getCode()); verify(gunClient, never()).updateChargingGunById(any(TChargingGun.class)); } }