From d4d70fe3a3a20f5fc29c9f37a1f89854a376597f Mon Sep 17 00:00:00 2001
From: yanderong <307678080@qq.com>
Date: 星期三, 16 九月 2026 18:42:36 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/2.3' into 2.3

---
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TCardChargingOrderAmountFieldsTest.java |  179 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 179 insertions(+), 0 deletions(-)

diff --git a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TCardChargingOrderAmountFieldsTest.java b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TCardChargingOrderAmountFieldsTest.java
index 5372e59..fa05bfb 100644
--- a/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TCardChargingOrderAmountFieldsTest.java
+++ b/ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TCardChargingOrderAmountFieldsTest.java
@@ -44,6 +44,8 @@
 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;
 
@@ -244,4 +246,181 @@
         verify(cardClient).deductForCharging(deductCaptor.capture());
         assertEquals(new BigDecimal("15.00"), deductCaptor.getValue().getAmount());
     }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    void deductFailureRollsFinishedOrderBackToStopping() 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-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();
+        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();
+        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();
+        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();
+        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();
+        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());
+    }
 }

--
Gitblit v1.7.1