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.ChargingCardAuthDTO;
|
import com.ruoyi.account.api.dto.ChargingCardDeductDTO;
|
import com.ruoyi.account.api.feignClient.ChargingCardClient;
|
import com.ruoyi.account.api.feignClient.CompanyAllocateRecordClient;
|
import com.ruoyi.account.api.model.TChargingCard;
|
import com.ruoyi.account.api.vo.ChargingCardDeductVO;
|
import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyDetailClient;
|
import com.ruoyi.chargingPile.api.feignClient.BillingTemplateClient;
|
import com.ruoyi.chargingPile.api.feignClient.ChargingGunClient;
|
import com.ruoyi.chargingPile.api.model.TChargingGun;
|
import com.ruoyi.chargingPile.api.vo.GetChargingGunByCode;
|
import com.ruoyi.common.core.domain.R;
|
import com.ruoyi.order.api.dto.DeviceStartChargingDTO;
|
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.query.UploadRealTimeMonitoringDataQuery;
|
import com.ruoyi.order.api.vo.DeviceStartChargingVO;
|
import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
|
import com.ruoyi.order.mapper.TCardChargingOrderMapper;
|
import com.ruoyi.order.service.TCardAccountingStrategyDetailOrderService;
|
import com.ruoyi.order.service.TCardChargingOrderAccountingStrategyService;
|
import com.ruoyi.order.support.ChargeAccountSupport;
|
import com.ruoyi.other.api.domain.TGroupCompany;
|
import com.ruoyi.other.api.feignClient.ChargeAuthorizationClient;
|
import com.ruoyi.other.api.feignClient.CompanyPreferentialClient;
|
import com.ruoyi.other.api.feignClient.GroupCompanyClient;
|
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.util.Collection;
|
import java.util.Collections;
|
import java.util.concurrent.TimeUnit;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.never;
|
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.when;
|
|
/**
|
* 刷卡订单金额字段单测(charge_amount / residual_amount / balance_pay_amount):
|
* 1) 建单写入可用金额基数与启动剩余金额(与返回设备的余额同源);
|
* 2) 监控按"可用金额基数 - 设备已充金额"刷新剩余金额;
|
* 3) 结算写入剩余金额(基数 - 本单金额)与余额支付金额,并同步回写内存订单对象。
|
*/
|
class TCardChargingOrderAmountFieldsTest {
|
|
/**
|
* 纯单测无 Spring 容器,MyBatis-Plus 的 lambda 列缓存需手工初始化,
|
* 否则服务实现里的 LambdaQueryWrapper/LambdaUpdateWrapper 解析列名时抛 "can not find lambda cache"
|
*/
|
@BeforeAll
|
static void initMybatisPlusTableInfo() {
|
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
|
TableInfoHelper.initTableInfo(assistant, TCardChargingOrder.class);
|
}
|
|
/**
|
* 账户支持组件(与生产同源):企业校验/授权窗口/优惠装载/分配流水等公共逻辑已抽取到该组件。
|
* 纯单测不启 Spring 容器,此处内联装配,远程依赖以宽松 mock 兜底(内部均有判空与兜底),
|
* 需要走真实分支的用例可再覆盖具体依赖字段。
|
*/
|
private ChargeAccountSupport newChargeAccountSupport() {
|
ChargeAccountSupport support = new ChargeAccountSupport();
|
ReflectionTestUtils.setField(support, "accountingStrategyDetailClient", mock(AccountingStrategyDetailClient.class));
|
ReflectionTestUtils.setField(support, "groupCompanyClient", mock(GroupCompanyClient.class));
|
ReflectionTestUtils.setField(support, "companyPreferentialClient", mock(CompanyPreferentialClient.class));
|
ReflectionTestUtils.setField(support, "billingTemplateClient", mock(BillingTemplateClient.class));
|
ReflectionTestUtils.setField(support, "companyAllocateRecordClient", mock(CompanyAllocateRecordClient.class));
|
return support;
|
}
|
|
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;
|
}
|
|
@SuppressWarnings("unchecked")
|
private <T> R<T> okWithData(T data) {
|
R<T> result = mock(R.class);
|
when(result.getCode()).thenReturn(R.SUCCESS);
|
when(result.getData()).thenReturn(data);
|
return result;
|
}
|
|
// ===== 建单:charge_amount / residual_amount = 卡余额 =====
|
|
@Test
|
void writesChargeAndResidualAmountOnCardOrderCreation() throws InterruptedException {
|
TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
|
TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class);
|
ChargingCardClient cardClient = mock(ChargingCardClient.class);
|
ChargingGunClient gunClient = mock(ChargingGunClient.class);
|
GroupCompanyClient groupCompanyClient = mock(GroupCompanyClient.class);
|
ChargeAuthorizationClient chargeAuthorizationClient = mock(ChargeAuthorizationClient.class);
|
ReflectionTestUtils.setField(service, "baseMapper", mapper);
|
ReflectionTestUtils.setField(service, "chargingCardClient", cardClient);
|
ReflectionTestUtils.setField(service, "chargingGunClient", gunClient);
|
ReflectionTestUtils.setField(service, "chargeAuthorizationClient", chargeAuthorizationClient);
|
ReflectionTestUtils.setField(service, "groupCompanyClient", groupCompanyClient);
|
// 账户支持组件与订单服务共用同一企业账户 mock,保证「企业校验 + 锁定企业账户」走同一返回值
|
ChargeAccountSupport chargeAccountSupport = newChargeAccountSupport();
|
ReflectionTestUtils.setField(chargeAccountSupport, "groupCompanyClient", groupCompanyClient);
|
ReflectionTestUtils.setField(service, "chargeAccountSupport", chargeAccountSupport);
|
ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(mockedLock(true)));
|
when(mapper.insert(any(TCardChargingOrder.class))).thenReturn(1);
|
|
TChargingGun gun = new TChargingGun();
|
gun.setId(9);
|
gun.setStatus(2);
|
R<TChargingGun> gunResult = okWithData(gun);
|
when(gunClient.getChargingGunByCode(any(GetChargingGunByCode.class))).thenReturn(gunResult);
|
|
TChargingCard card = new TChargingCard();
|
card.setId(5L);
|
card.setCardNo("C001");
|
card.setCompanyId(77);
|
card.setBalance(new BigDecimal("100.00"));
|
R<TChargingCard> verifyResult = okWithData(card);
|
when(cardClient.verifyCharging(any(ChargingCardAuthDTO.class))).thenReturn(verifyResult);
|
// 共享账户模式(默认):企业状态启用、可用余额即下发给设备的可用金额,未配置充电授权按放行处理
|
TGroupCompany company = new TGroupCompany();
|
company.setId(77);
|
company.setStatus(1);
|
company.setAccountMode(1);
|
company.setAvailableBalance(new BigDecimal("100.00"));
|
R<TGroupCompany> companyResult = okWithData(company);
|
when(groupCompanyClient.getCompanyById(77)).thenReturn(companyResult);
|
R<BigDecimal> lockBalanceResult = okWithData(new BigDecimal("100.00"));
|
when(groupCompanyClient.lockBalance(any(), any())).thenReturn(lockBalanceResult);
|
R<Boolean> lockResult = okWithData(true);
|
when(cardClient.lockForCharging(anyLong(), anyString())).thenReturn(lockResult);
|
|
DeviceStartChargingDTO dto = new DeviceStartChargingDTO();
|
dto.setChargingPileCode("P1");
|
dto.setChargingGunCode("G1");
|
dto.setAccount("C001");
|
DeviceStartChargingVO result = service.startCardCharging(dto);
|
|
assertEquals(1, result.getAuthentication());
|
assertEquals(new BigDecimal("100.00"), result.getBalance());
|
ArgumentCaptor<TCardChargingOrder> captor = ArgumentCaptor.forClass(TCardChargingOrder.class);
|
verify(mapper).insert(captor.capture());
|
assertEquals(new BigDecimal("100.00"), captor.getValue().getChargeAmount());
|
assertEquals(new BigDecimal("100.00"), captor.getValue().getResidualAmount());
|
}
|
|
// ===== 监控:residual_amount = charge_amount - paid_amount =====
|
|
@Test
|
void updatesResidualAmountFromPaidAmountWhileMonitoring() {
|
TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
|
ReflectionTestUtils.setField(service, "chargeAccountSupport", newChargeAccountSupport());
|
TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class);
|
ReflectionTestUtils.setField(service, "baseMapper", mapper);
|
|
TCardChargingOrder charging = new TCardChargingOrder();
|
charging.setId(1L);
|
charging.setCode("SD1");
|
charging.setStatus(3);
|
charging.setChargeAmount(new BigDecimal("100.00"));
|
when(mapper.selectOne(any())).thenReturn(charging);
|
when(mapper.update(any(TCardChargingOrder.class), any())).thenReturn(1);
|
|
UploadRealTimeMonitoringDataQuery query = new UploadRealTimeMonitoringDataQuery();
|
query.setTransaction_serial_number("SD1");
|
query.setPaid_amount(new BigDecimal("12.34"));
|
service.cardChargeMonitoring(query);
|
|
ArgumentCaptor<TCardChargingOrder> captor = ArgumentCaptor.forClass(TCardChargingOrder.class);
|
verify(mapper).update(captor.capture(), any());
|
assertEquals(new BigDecimal("87.66"), captor.getValue().getResidualAmount());
|
}
|
|
// ===== 结算:residual_amount = charge_amount - orderAmount;balance_pay_amount = orderAmount =====
|
|
@Test
|
@SuppressWarnings("unchecked")
|
void writesResidualChargeAndBalancePayAmountOnSettlement() throws InterruptedException {
|
TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
|
ReflectionTestUtils.setField(service, "chargeAccountSupport", newChargeAccountSupport());
|
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(TCardChargingOrder.class))).thenReturn(1);
|
|
TChargingGun gun = new TChargingGun();
|
gun.setId(9);
|
gun.setStatus(4);
|
R<TChargingGun> gunResult = okWithData(gun);
|
when(gunClient.getChargingGunById(any())).thenReturn(gunResult);
|
R<String> gunUpdateResult = mock(R.class);
|
when(gunUpdateResult.getCode()).thenReturn(R.SUCCESS);
|
when(gunClient.updateChargingGunById(any(TChargingGun.class))).thenReturn(gunUpdateResult);
|
R<Boolean> unlockResult = okWithData(true);
|
when(cardClient.unlockForCharging(anyLong(), anyString())).thenReturn(unlockResult);
|
ChargingCardDeductVO deductVO = new ChargingCardDeductVO();
|
deductVO.setDuplicated(false);
|
R<ChargingCardDeductVO> deductResult = okWithData(deductVO);
|
when(cardClient.deductForCharging(any(ChargingCardDeductDTO.class))).thenReturn(deductResult);
|
|
// 计费策略快照:尖段电费 1.0 元/度、服务费 0.5 元/度
|
TCardAccountingStrategyDetailOrder snapshot = new TCardAccountingStrategyDetailOrder();
|
snapshot.setType(1);
|
snapshot.setElectrovalence(new BigDecimal("1.0000"));
|
snapshot.setServiceCharge(new BigDecimal("0.5000"));
|
snapshot.setCostServiceCharge(new BigDecimal("0.5000"));
|
LambdaQueryChainWrapper<TCardAccountingStrategyDetailOrder> queryWrapper = mock(LambdaQueryChainWrapper.class);
|
when(detailOrderService.lambdaQuery()).thenReturn(queryWrapper);
|
when(queryWrapper.eq(any(), any())).thenReturn(queryWrapper);
|
when(queryWrapper.list()).thenReturn(Collections.singletonList(snapshot));
|
|
LambdaUpdateChainWrapper<TCardChargingOrderAccountingStrategy> 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);
|
|
when(txTemplate.execute(any())).thenAnswer(invocation -> {
|
TransactionCallback<?> callback = invocation.getArgument(0);
|
return callback.doInTransaction(mock(TransactionStatus.class));
|
});
|
|
TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
|
vo.setTransaction_serial_number("SD1");
|
vo.setSpike_charge1(new BigDecimal("10"));
|
R result = service.cardEndChargeBillingCharge(vo);
|
|
assertEquals(R.SUCCESS, result.getCode());
|
ArgumentCaptor<TCardChargingOrder> captor = ArgumentCaptor.forClass(TCardChargingOrder.class);
|
verify(mapper).updateById(captor.capture());
|
TCardChargingOrder update = captor.getValue();
|
// 尖段 10 度 * (1.0 + 0.5) = 15.00 元
|
assertEquals(new BigDecimal("15.00"), update.getOrderAmount());
|
assertEquals(new BigDecimal("15.00"), update.getBalancePayAmount());
|
assertEquals(new BigDecimal("100.00"), update.getChargeAmount());
|
assertEquals(new BigDecimal("85.00"), update.getResidualAmount());
|
// 内存订单对象同步回写,保证结算后监管推送字段完整
|
assertEquals(new BigDecimal("85.00"), charging.getResidualAmount());
|
assertEquals(new BigDecimal("15.00"), charging.getBalancePayAmount());
|
// 余额扣款金额与余额支付金额同源
|
ArgumentCaptor<ChargingCardDeductDTO> deductCaptor = ArgumentCaptor.forClass(ChargingCardDeductDTO.class);
|
verify(cardClient).deductForCharging(deductCaptor.capture());
|
assertEquals(new BigDecimal("15.00"), deductCaptor.getValue().getAmount());
|
}
|
|
@Test
|
@SuppressWarnings("unchecked")
|
void deductFailureRollsFinishedOrderBackToStopping() throws InterruptedException {
|
TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
|
ReflectionTestUtils.setField(service, "chargeAccountSupport", newChargeAccountSupport());
|
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-FAIL");
|
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(TCardChargingOrder.class))).thenReturn(1);
|
when(mapper.update(any(), any())).thenReturn(1);
|
|
R<ChargingCardDeductVO> deductFailure = mock(R.class);
|
when(deductFailure.getCode()).thenReturn(500);
|
when(deductFailure.getMsg()).thenReturn("account unavailable");
|
when(cardClient.deductForCharging(any(ChargingCardDeductDTO.class))).thenReturn(deductFailure);
|
|
TCardAccountingStrategyDetailOrder snapshot = new TCardAccountingStrategyDetailOrder();
|
snapshot.setType(1);
|
snapshot.setElectrovalence(new BigDecimal("1.0000"));
|
snapshot.setServiceCharge(new BigDecimal("0.5000"));
|
snapshot.setCostServiceCharge(new BigDecimal("0.5000"));
|
LambdaQueryChainWrapper<TCardAccountingStrategyDetailOrder> queryWrapper = mock(LambdaQueryChainWrapper.class);
|
when(detailOrderService.lambdaQuery()).thenReturn(queryWrapper);
|
when(queryWrapper.eq(any(), any())).thenReturn(queryWrapper);
|
when(queryWrapper.list()).thenReturn(Collections.singletonList(snapshot));
|
|
LambdaUpdateChainWrapper<TCardChargingOrderAccountingStrategy> 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);
|
when(txTemplate.execute(any())).thenAnswer(invocation -> {
|
TransactionCallback<?> callback = invocation.getArgument(0);
|
return callback.doInTransaction(mock(TransactionStatus.class));
|
});
|
|
TransactionRecordMessageVO vo = new TransactionRecordMessageVO();
|
vo.setTransaction_serial_number("SD-FAIL");
|
vo.setSpike_charge1(BigDecimal.ONE);
|
R result = service.cardEndChargeBillingCharge(vo);
|
|
assertEquals(500, result.getCode());
|
verify(mapper).update(any(), any());
|
verify(cardClient, never()).unlockForCharging(anyLong(), anyString());
|
verify(gunClient, never()).getChargingGunById(any());
|
}
|
|
@Test
|
void compensationRollsBackOnlyFinishedOrdersWithoutDeductRecord() throws InterruptedException {
|
TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
|
ReflectionTestUtils.setField(service, "chargeAccountSupport", newChargeAccountSupport());
|
TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class);
|
ChargingCardClient cardClient = mock(ChargingCardClient.class);
|
RLock lock = mockedLock(true);
|
ReflectionTestUtils.setField(service, "baseMapper", mapper);
|
ReflectionTestUtils.setField(service, "chargingCardClient", cardClient);
|
ReflectionTestUtils.setField(service, "redissonClient", mockedRedisson(lock));
|
|
TCardChargingOrder missing = new TCardChargingOrder();
|
missing.setId(1L);
|
missing.setCode("SD-MISSING");
|
missing.setStatus(5);
|
missing.setCardId(5L);
|
missing.setOrderAmount(BigDecimal.ONE);
|
when(mapper.selectList(any())).thenReturn(Collections.singletonList(missing));
|
when(mapper.update(any(), any())).thenReturn(1);
|
R<java.util.List<String>> noDeductRecords = okWithData(Collections.emptyList());
|
when(cardClient.listDeductedOrderCodes(any())).thenReturn(noDeductRecords);
|
|
assertEquals(1, service.recoverFinishedOrdersWithoutDeduct());
|
verify(cardClient, times(2)).listDeductedOrderCodes(any());
|
verify(mapper).update(any(), any());
|
}
|
|
@Test
|
void compensationSkipsFinishedOrdersHavingDeductRecord() {
|
TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
|
ReflectionTestUtils.setField(service, "chargeAccountSupport", newChargeAccountSupport());
|
TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class);
|
ChargingCardClient cardClient = mock(ChargingCardClient.class);
|
ReflectionTestUtils.setField(service, "baseMapper", mapper);
|
ReflectionTestUtils.setField(service, "chargingCardClient", cardClient);
|
|
TCardChargingOrder deducted = new TCardChargingOrder();
|
deducted.setId(2L);
|
deducted.setCode("SD-DEDUCTED");
|
deducted.setStatus(5);
|
deducted.setCardId(5L);
|
deducted.setOrderAmount(BigDecimal.ONE);
|
when(mapper.selectList(any())).thenReturn(Collections.singletonList(deducted));
|
// 批量查询结果已包含该订单号时不再逐单复核,也不回退状态
|
R<java.util.List<String>> deductedRecords = okWithData(Collections.singletonList("SD-DEDUCTED"));
|
when(cardClient.listDeductedOrderCodes(any())).thenReturn(deductedRecords);
|
|
assertEquals(0, service.recoverFinishedOrdersWithoutDeduct());
|
verify(cardClient, times(1)).listDeductedOrderCodes(any());
|
verify(mapper, never()).update(any(), any());
|
}
|
|
@Test
|
void compensationKeepsFinishedStatusWhenDeductQueryFails() {
|
TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
|
ReflectionTestUtils.setField(service, "chargeAccountSupport", newChargeAccountSupport());
|
TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class);
|
ChargingCardClient cardClient = mock(ChargingCardClient.class);
|
ReflectionTestUtils.setField(service, "baseMapper", mapper);
|
ReflectionTestUtils.setField(service, "chargingCardClient", cardClient);
|
|
TCardChargingOrder candidate = new TCardChargingOrder();
|
candidate.setId(3L);
|
candidate.setCode("SD-UNKNOWN");
|
candidate.setStatus(5);
|
candidate.setCardId(5L);
|
candidate.setOrderAmount(BigDecimal.ONE);
|
when(mapper.selectList(any())).thenReturn(Collections.singletonList(candidate));
|
// 账户服务不可用时无法确认扣款结果,必须保持原状态等待下轮,不能误回退
|
when(cardClient.listDeductedOrderCodes(any())).thenReturn(R.fail("account unavailable"));
|
|
assertEquals(0, service.recoverFinishedOrdersWithoutDeduct());
|
verify(mapper, never()).update(any(), any());
|
}
|
|
@Test
|
void compensationSkipsRemoteQueryWhenNoFinishedCandidate() {
|
TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
|
ReflectionTestUtils.setField(service, "chargeAccountSupport", newChargeAccountSupport());
|
TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class);
|
ChargingCardClient cardClient = mock(ChargingCardClient.class);
|
ReflectionTestUtils.setField(service, "baseMapper", mapper);
|
ReflectionTestUtils.setField(service, "chargingCardClient", cardClient);
|
|
when(mapper.selectList(any())).thenReturn(Collections.emptyList());
|
|
assertEquals(0, service.recoverFinishedOrdersWithoutDeduct());
|
verify(cardClient, never()).listDeductedOrderCodes(any());
|
verify(mapper, never()).update(any(), any());
|
}
|
|
@Test
|
void compensationSkipsOrderWhenSettleLockNotAcquired() throws InterruptedException {
|
TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
|
ReflectionTestUtils.setField(service, "chargeAccountSupport", newChargeAccountSupport());
|
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(false)));
|
|
TCardChargingOrder candidate = new TCardChargingOrder();
|
candidate.setId(4L);
|
candidate.setCode("SD-LOCKED");
|
candidate.setStatus(5);
|
candidate.setCardId(5L);
|
candidate.setOrderAmount(BigDecimal.ONE);
|
when(mapper.selectList(any())).thenReturn(Collections.singletonList(candidate));
|
R<java.util.List<String>> noDeductRecords = okWithData(Collections.emptyList());
|
when(cardClient.listDeductedOrderCodes(any())).thenReturn(noDeductRecords);
|
|
assertEquals(0, service.recoverFinishedOrdersWithoutDeduct());
|
verify(cardClient, times(1)).listDeductedOrderCodes(any());
|
verify(mapper, never()).update(any(), any());
|
}
|
}
|