From 67d25f3d9eb0124c7aab544bf8f77031d6d8adc3 Mon Sep 17 00:00:00 2001
From: puzhibing <393733352@qq.com>
Date: 星期三, 30 九月 2026 10:36:18 +0800
Subject: [PATCH] 增加公司账号权限功能

---
 ruoyi-service/ruoyi-order/src/test/java/com/ruoyi/order/service/impl/TCardChargingOrderAmountFieldsTest.java |  245 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 243 insertions(+), 2 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 282459d..103ddf8 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
@@ -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());
+    }
 }

--
Gitblit v1.7.1