puzhibing
2026-09-30 67d25f3d9eb0124c7aab544bf8f77031d6d8adc3
ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TCardChargingOrderAmountFieldsTest.java
@@ -9,15 +9,20 @@
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.model.TCompanyAllocateRecord;
import com.ruoyi.account.api.vo.ChargingCardDeductVO;
import com.ruoyi.chargingPile.api.feignClient.AccountingStrategyClient;
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.TAccountingStrategy;
import com.ruoyi.chargingPile.api.model.TAccountingStrategyDetail;
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.TCardAccountingStrategyOrder;
import com.ruoyi.order.api.model.TCardChargingOrder;
import com.ruoyi.order.api.model.TCardChargingOrderAccountingStrategy;
import com.ruoyi.order.api.query.UploadRealTimeMonitoringDataQuery;
@@ -25,6 +30,7 @@
import com.ruoyi.order.api.vo.TransactionRecordMessageVO;
import com.ruoyi.order.mapper.TCardChargingOrderMapper;
import com.ruoyi.order.service.TCardAccountingStrategyDetailOrderService;
import com.ruoyi.order.service.TCardAccountingStrategyOrderService;
import com.ruoyi.order.service.TCardChargingOrderAccountingStrategyService;
import com.ruoyi.order.support.ChargeAccountSupport;
import com.ruoyi.other.api.domain.TGroupCompany;
@@ -48,9 +54,12 @@
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
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.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -80,14 +89,24 @@
     * 纯单测不启 Spring 容器,此处内联装配,远程依赖以宽松 mock 兜底(内部均有判空与兜底),
     * 需要走真实分支的用例可再覆盖具体依赖字段。
     */
    /** 账户侧分配流水客户端(与 ChargeAccountSupport 内持有的是同一 mock,便于补偿双判据用例 stub 与校验) */
    private CompanyAllocateRecordClient companyAllocateRecordClient;
    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));
        companyAllocateRecordClient = mock(CompanyAllocateRecordClient.class);
        ReflectionTestUtils.setField(support, "companyAllocateRecordClient", companyAllocateRecordClient);
        return support;
    }
    /** 企业侧无消费流水(补偿双判据默认:卡侧与企业侧均可确认未结算) */
    private void mockNoCompanyConsumeRecord() {
        R<java.util.List<String>> empty = okWithData(Collections.emptyList());
        when(companyAllocateRecordClient.listConsumedOrderCodes(any())).thenReturn(empty);
    }
    private RLock mockedLock(boolean tryLockResult) throws InterruptedException {
@@ -131,11 +150,17 @@
        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);
        // 订单主键在真实库里由 MyBatis-Plus 回填,纯单测手工回填,保证策略快照与锁定额度回写能拿到订单ID
        when(mapper.insert(any(TCardChargingOrder.class))).thenAnswer(invocation -> {
            TCardChargingOrder entity = invocation.getArgument(0);
            entity.setId(100L);
            return 1;
        });
        TChargingGun gun = new TChargingGun();
        gun.setId(9);
        gun.setStatus(2);
        gun.setAccountingStrategyId(31);
        R<TChargingGun> gunResult = okWithData(gun);
        when(gunClient.getChargingGunByCode(any(GetChargingGunByCode.class))).thenReturn(gunResult);
@@ -158,6 +183,24 @@
        when(groupCompanyClient.lockBalance(any(), any())).thenReturn(lockBalanceResult);
        R<Boolean> lockResult = okWithData(true);
        when(cardClient.lockForCharging(anyLong(), anyString())).thenReturn(lockResult);
        // 点5 改造后建单必须先成功写入计费策略快照(否则回滚删单):枪配置策略 31,策略与明细均可查且快照落库成功
        AccountingStrategyClient accountingStrategyClient = mock(AccountingStrategyClient.class);
        AccountingStrategyDetailClient accountingStrategyDetailClient = mock(AccountingStrategyDetailClient.class);
        TCardAccountingStrategyOrderService strategyOrderService = mock(TCardAccountingStrategyOrderService.class);
        TCardAccountingStrategyDetailOrderService strategyDetailOrderService = mock(TCardAccountingStrategyDetailOrderService.class);
        ReflectionTestUtils.setField(service, "accountingStrategyClient", accountingStrategyClient);
        ReflectionTestUtils.setField(service, "accountingStrategyDetailClient", accountingStrategyDetailClient);
        ReflectionTestUtils.setField(service, "cardAccountingStrategyOrderService", strategyOrderService);
        ReflectionTestUtils.setField(service, "cardAccountingStrategyDetailOrderService", strategyDetailOrderService);
        R<TAccountingStrategy> strategyResult = okWithData(new TAccountingStrategy());
        when(accountingStrategyClient.getAccountingStrategyById(31)).thenReturn(strategyResult);
        TAccountingStrategyDetail strategyDetail = new TAccountingStrategyDetail();
        strategyDetail.setType(1);
        R<java.util.List<TAccountingStrategyDetail>> strategyDetailResult =
                okWithData(Collections.singletonList(strategyDetail));
        when(accountingStrategyDetailClient.getListByAccountingStrategyId(31)).thenReturn(strategyDetailResult);
        when(strategyOrderService.save(any(TCardAccountingStrategyOrder.class))).thenReturn(true);
        when(strategyDetailOrderService.saveBatch(any(Collection.class))).thenReturn(true);
        DeviceStartChargingDTO dto = new DeviceStartChargingDTO();
        dto.setChargingPileCode("P1");
@@ -378,6 +421,7 @@
        when(mapper.update(any(), any())).thenReturn(1);
        R<java.util.List<String>> noDeductRecords = okWithData(Collections.emptyList());
        when(cardClient.listDeductedOrderCodes(any())).thenReturn(noDeductRecords);
        mockNoCompanyConsumeRecord();
        assertEquals(1, service.recoverFinishedOrdersWithoutDeduct());
        verify(cardClient, times(2)).listDeductedOrderCodes(any());
@@ -468,9 +512,206 @@
        when(mapper.selectList(any())).thenReturn(Collections.singletonList(candidate));
        R<java.util.List<String>> noDeductRecords = okWithData(Collections.emptyList());
        when(cardClient.listDeductedOrderCodes(any())).thenReturn(noDeductRecords);
        mockNoCompanyConsumeRecord();
        assertEquals(0, service.recoverFinishedOrdersWithoutDeduct());
        verify(cardClient, times(1)).listDeductedOrderCodes(any());
        verify(mapper, never()).update(any(), any());
    }
    @Test
    void compensationSkipsFinishedOrderWithCompanyConsumeRecord() {
        // 共享模式订单的消费流水落在企业侧:卡侧无流水但企业侧命中时必须保持已完成状态,不得回退造成重复扣款
        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 shared = new TCardChargingOrder();
        shared.setId(6L);
        shared.setCode("SD-SHARED");
        shared.setStatus(5);
        shared.setCardId(5L);
        shared.setOrderAmount(BigDecimal.ONE);
        when(mapper.selectList(any())).thenReturn(Collections.singletonList(shared));
        R<java.util.List<String>> noDeductRecords = okWithData(Collections.emptyList());
        when(cardClient.listDeductedOrderCodes(any())).thenReturn(noDeductRecords);
        R<java.util.List<String>> consumeRecords = okWithData(Collections.singletonList("SD-SHARED"));
        when(companyAllocateRecordClient.listConsumedOrderCodes(any())).thenReturn(consumeRecords);
        assertEquals(0, service.recoverFinishedOrdersWithoutDeduct());
        verify(cardClient, times(1)).listDeductedOrderCodes(any());
        verify(mapper, never()).update(any(), any());
    }
    @Test
    void compensationKeepsFinishedStatusWhenCompanyConsumeQueryFails() {
        // 企业侧流水查询失败说明无法确认结算结果,必须保持原状态等待下轮,避免误回退后重复扣款
        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(7L);
        candidate.setCode("SD-CONSUME-UNKNOWN");
        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);
        when(companyAllocateRecordClient.listConsumedOrderCodes(any())).thenReturn(R.fail("account unavailable"));
        assertEquals(0, service.recoverFinishedOrdersWithoutDeduct());
        verify(mapper, never()).update(any(), any());
    }
    @Test
    void rollsBackOrderWhenNoAccountingStrategyConfigured() throws InterruptedException {
        // 点5:枪与站点均未配置计费策略(strategyId 为空)时必须回滚删单并回告启动失败,禁止产生无策略订单
        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);
        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))).thenAnswer(invocation -> {
            TCardChargingOrder entity = invocation.getArgument(0);
            entity.setId(101L);
            return 1;
        });
        when(mapper.deleteById(anyLong())).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);
        DeviceStartChargingDTO dto = new DeviceStartChargingDTO();
        dto.setChargingPileCode("P1");
        dto.setChargingGunCode("G1");
        dto.setAccount("C001");
        DeviceStartChargingVO result = service.startCardCharging(dto);
        assertEquals(0, result.getAuthentication());
        // 订单回滚删除,且未锁定账户额度、未锁卡
        verify(mapper).deleteById(anyLong());
        verify(groupCompanyClient, never()).lockBalance(any(), any());
        verify(cardClient, never()).lockForCharging(anyLong(), anyString());
    }
    @Test
    void sharedModeSettlementPassesOrderCodeAndSkipsLocalConsumeRecord() {
        // 共享模式:结算传订单号启用账户侧幂等(账户侧写企业消费流水),订单侧不得再写一份导致重复记账
        TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
        ReflectionTestUtils.setField(service, "chargeAccountSupport", newChargeAccountSupport());
        GroupCompanyClient groupCompanyClient = mock(GroupCompanyClient.class);
        ReflectionTestUtils.setField(service, "groupCompanyClient", groupCompanyClient);
        TCardChargingOrder order = new TCardChargingOrder();
        order.setId(8L);
        order.setCode("SD-SHARED-SETTLE");
        order.setCardId(5L);
        order.setCardNo("C001");
        order.setCompanyId(77);
        order.setAccountMode(ChargeAccountSupport.ACCOUNT_MODE_SHARED);
        order.setLockedAmount(new BigDecimal("200.00"));
        R<BigDecimal> settleResult = okWithData(new BigDecimal("663.50"));
        when(groupCompanyClient.settleBalance(eq(77), eq(new BigDecimal("200.00")), eq(new BigDecimal("36.50")),
                eq("SD-SHARED-SETTLE"), eq("C001"), isNull(), eq("刷卡充电结算"))).thenReturn(settleResult);
        Boolean settled = ReflectionTestUtils.invokeMethod(service, "settleCardChargeAccount", order,
                new BigDecimal("36.50"), "SD-SHARED-SETTLE");
        assertTrue(settled);
        verify(companyAllocateRecordClient, never()).recordConsume(any(), any(), any(), any(), any(), any(), any(),
                any());
    }
    @Test
    void allocatedModeSettlementWritesCardConsumeRecord() {
        // 分配模式:扣的是卡账户余额,消费流水仍由订单侧记到该卡上(user_type=卡)
        TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
        ReflectionTestUtils.setField(service, "chargeAccountSupport", newChargeAccountSupport());
        ChargingCardClient cardClient = mock(ChargingCardClient.class);
        ReflectionTestUtils.setField(service, "chargingCardClient", cardClient);
        TCardChargingOrder order = new TCardChargingOrder();
        order.setId(9L);
        order.setCode("SD-ALLOC-SETTLE");
        order.setCardId(5L);
        order.setCardNo("C001");
        order.setCompanyId(77);
        order.setAccountMode(ChargeAccountSupport.ACCOUNT_MODE_ALLOCATED);
        order.setLockedAmount(new BigDecimal("200.00"));
        ChargingCardDeductVO deductVO = new ChargingCardDeductVO();
        deductVO.setDuplicated(false);
        deductVO.setAfterBalance(new BigDecimal("63.50"));
        R<ChargingCardDeductVO> settleCardResult = okWithData(deductVO);
        when(cardClient.settleForCharging(any(ChargingCardDeductDTO.class))).thenReturn(settleCardResult);
        Boolean settled = ReflectionTestUtils.invokeMethod(service, "settleCardChargeAccount", order,
                new BigDecimal("36.50"), "SD-ALLOC-SETTLE");
        assertTrue(settled);
        verify(companyAllocateRecordClient).recordConsume(eq(77), eq(TCompanyAllocateRecord.USER_TYPE_CARD),
                eq("C001"), eq("C001"), any(), eq(new BigDecimal("-36.50")), eq("SD-ALLOC-SETTLE"),
                eq("刷卡充电结算"));
    }
    @Test
    void skipsUnlockWhenLockReleasedFlagAlreadySet() {
        // 点2-D:lock_released 抢占未命中(标记已是 1)说明锁定额已释放过,禁止重复解锁
        TCardChargingOrderServiceImpl service = new TCardChargingOrderServiceImpl();
        TCardChargingOrderMapper mapper = mock(TCardChargingOrderMapper.class);
        GroupCompanyClient groupCompanyClient = mock(GroupCompanyClient.class);
        ChargingCardClient cardClient = mock(ChargingCardClient.class);
        ReflectionTestUtils.setField(service, "baseMapper", mapper);
        ReflectionTestUtils.setField(service, "groupCompanyClient", groupCompanyClient);
        ReflectionTestUtils.setField(service, "chargingCardClient", cardClient);
        when(mapper.update(any(), any())).thenReturn(0);
        TCardChargingOrder order = new TCardChargingOrder();
        order.setId(10L);
        order.setCode("SD-RELEASED");
        order.setCompanyId(77);
        order.setAccountMode(ChargeAccountSupport.ACCOUNT_MODE_SHARED);
        order.setLockedAmount(new BigDecimal("200.00"));
        ReflectionTestUtils.invokeMethod(service, "unlockCardChargeContextQuietly", order, "启动关单");
        verify(groupCompanyClient, never()).unlockBalance(any(), any());
    }
}