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