From 7617d46abee2950b7336e3379f8c7a564939cb54 Mon Sep 17 00:00:00 2001
From: yanderong <307678080@qq.com>
Date: 星期六, 10 十月 2026 17:54:56 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/2.3' into 2.3

---
 ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TCompanyAllocateRecordServiceImplTest.java |  142 +++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 142 insertions(+), 0 deletions(-)

diff --git a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TCompanyAllocateRecordServiceImplTest.java b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TCompanyAllocateRecordServiceImplTest.java
index b55cf0d..b391f11 100644
--- a/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TCompanyAllocateRecordServiceImplTest.java
+++ b/ruoyi-service/ruoyi-account/src/test/java/com/ruoyi/account/service/impl/TCompanyAllocateRecordServiceImplTest.java
@@ -3,9 +3,17 @@
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ruoyi.account.api.dto.CompanyAllocateRecordQueryDTO;
+import com.ruoyi.account.api.model.TAppUserWhitelist;
+import com.ruoyi.account.api.model.TChargingCard;
 import com.ruoyi.account.api.model.TCompanyAllocateRecord;
+import com.ruoyi.account.api.model.TVinManagement;
+import com.ruoyi.account.api.vo.CompanyAllocateSummaryVO;
+import com.ruoyi.account.mapper.TAppUserWhitelistMapper;
+import com.ruoyi.account.mapper.TChargingCardMapper;
 import com.ruoyi.account.mapper.TCompanyAllocateRecordMapper;
+import com.ruoyi.account.mapper.TVinManagementMapper;
 import com.ruoyi.common.core.context.SecurityContextHolder;
+import com.ruoyi.common.core.exception.ServiceException;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -13,17 +21,26 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
 import org.springframework.test.util.ReflectionTestUtils;
 
 import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -35,13 +52,23 @@
 class TCompanyAllocateRecordServiceImplTest {
 
     @Mock private TCompanyAllocateRecordMapper mapper;
+    @Mock private TVinManagementMapper vinMapper;
+    @Mock private TChargingCardMapper cardMapper;
+    @Mock private TAppUserWhitelistMapper whitelistMapper;
+    @Mock private RedissonClient redissonClient;
+    @Mock private RLock lock;
     private TCompanyAllocateRecordServiceImpl service;
 
     @BeforeEach
     void setUp() {
         service = spy(new TCompanyAllocateRecordServiceImpl());
         ReflectionTestUtils.setField(service, "baseMapper", mapper);
+        ReflectionTestUtils.setField(service, "vinManagementMapper", vinMapper);
+        ReflectionTestUtils.setField(service, "chargingCardMapper", cardMapper);
+        ReflectionTestUtils.setField(service, "appUserWhitelistMapper", whitelistMapper);
+        ReflectionTestUtils.setField(service, "redissonClient", redissonClient);
         lenient().when(mapper.insert(any(TCompanyAllocateRecord.class))).thenReturn(1);
+        lenient().when(redissonClient.getLock(anyString())).thenReturn(lock);
         SecurityContextHolder.setUserId("123");
         SecurityContextHolder.setUserName("admin");
     }
@@ -161,6 +188,121 @@
         return captor.getValue();
     }
 
+    // ============ 账户模式切换回收相关 ============
+
+    @Test
+    void sumAllocatedBalanceAggregatesThreeTypes() {
+        TVinManagement vin = new TVinManagement();
+        vin.setId(1L);
+        vin.setBalance(new BigDecimal("10.50"));
+        vin.setLockedAmount(new BigDecimal("2.00"));
+        TVinManagement vin2 = new TVinManagement();
+        vin2.setId(2L);
+        vin2.setBalance(new BigDecimal("5.25"));
+        vin2.setLockedAmount(BigDecimal.ZERO);
+        when(vinMapper.selectList(any())).thenReturn(Arrays.asList(vin, vin2));
+
+        TChargingCard card = new TChargingCard();
+        card.setId(3L);
+        card.setBalance(new BigDecimal("8.00"));
+        card.setLockedAmount(new BigDecimal("1.50"));
+        when(cardMapper.selectList(any())).thenReturn(Collections.singletonList(card));
+
+        TAppUserWhitelist whitelist = new TAppUserWhitelist();
+        whitelist.setId(4L);
+        whitelist.setBalance(new BigDecimal("6.00"));
+        whitelist.setLockedAmount(BigDecimal.ZERO);
+        when(whitelistMapper.selectList(any())).thenReturn(Collections.singletonList(whitelist));
+
+        CompanyAllocateSummaryVO summary = service.sumAllocatedBalance(9);
+
+        assertEquals(9, summary.getCompanyId());
+        assertEquals(new BigDecimal("29.75"), summary.getTotalBalance());
+        assertEquals(new BigDecimal("3.50"), summary.getTotalLocked());
+        assertEquals(4, summary.getBalanceUserCount());
+    }
+
+    @Test
+    void recycleAllocatedBalanceClearsAndWritesBothSides() throws InterruptedException {
+        when(lock.tryLock(5L, 30L, TimeUnit.SECONDS)).thenReturn(true);
+        when(lock.isHeldByCurrentThread()).thenReturn(true);
+
+        TVinManagement vin = new TVinManagement();
+        vin.setId(1L);
+        vin.setBalance(new BigDecimal("10.00"));
+        vin.setVin("LVIN0001");
+        vin.setPlateNumber("粤A12345");
+        when(vinMapper.selectList(any())).thenReturn(Collections.singletonList(vin));
+        when(vinMapper.clearBalance(eq(1L), eq(123L))).thenReturn(1);
+
+        TChargingCard card = new TChargingCard();
+        card.setId(2L);
+        card.setBalance(new BigDecimal("20.00"));
+        card.setCardNo("CARD0001");
+        when(cardMapper.selectList(any())).thenReturn(Collections.singletonList(card));
+        when(cardMapper.clearBalance(eq(2L), eq(123L))).thenReturn(1);
+
+        TAppUserWhitelist whitelist = new TAppUserWhitelist();
+        whitelist.setId(3L);
+        whitelist.setBalance(new BigDecimal("30.00"));
+        whitelist.setPhone("13800000000");
+        whitelist.setName("张三");
+        when(whitelistMapper.selectList(any())).thenReturn(Collections.singletonList(whitelist));
+        when(whitelistMapper.clearBalance(eq(3L), eq("admin"))).thenReturn(1);
+
+        BigDecimal recycled = service.recycleAllocatedBalance(9, "admin", "测试公司", new BigDecimal("100.00"));
+
+        assertEquals(new BigDecimal("60.00"), recycled);
+        verify(vinMapper).clearBalance(1L, 123L);
+        verify(cardMapper).clearBalance(2L, 123L);
+        verify(whitelistMapper).clearBalance(3L, "admin");
+        // 3 条用户负流水 + 1 条企业账户正流水
+        verify(mapper, times(4)).insert(any(TCompanyAllocateRecord.class));
+        ArgumentCaptor<TCompanyAllocateRecord> captor = ArgumentCaptor.forClass(TCompanyAllocateRecord.class);
+        verify(mapper, times(4)).insert(captor.capture());
+        List<TCompanyAllocateRecord> records = captor.getAllValues();
+        TCompanyAllocateRecord companyRecord = records.get(records.size() - 1);
+        assertEquals(TCompanyAllocateRecord.USER_TYPE_COMPANY, companyRecord.getUserType());
+        assertEquals("测试公司", companyRecord.getUserInfo());
+        assertEquals(TCompanyAllocateRecord.SOURCE_MODE_SWITCH, companyRecord.getChangeSource());
+        assertEquals(new BigDecimal("100.00"), companyRecord.getBeforeBalance());
+        assertEquals(new BigDecimal("60.00"), companyRecord.getAmount());
+        assertEquals(new BigDecimal("160.00"), companyRecord.getAfterBalance());
+        for (int i = 0; i < 3; i++) {
+            assertEquals(TCompanyAllocateRecord.SOURCE_MODE_SWITCH, records.get(i).getChangeSource());
+            assertTrue(records.get(i).getAmount().signum() < 0);
+        }
+    }
+
+    @Test
+    void recycleAllocatedBalanceIsIdempotentWhenNoBalanceLeft() throws InterruptedException {
+        when(lock.tryLock(5L, 30L, TimeUnit.SECONDS)).thenReturn(true);
+        when(lock.isHeldByCurrentThread()).thenReturn(true);
+        when(vinMapper.selectList(any())).thenReturn(Collections.emptyList());
+        when(cardMapper.selectList(any())).thenReturn(Collections.emptyList());
+        when(whitelistMapper.selectList(any())).thenReturn(Collections.emptyList());
+
+        BigDecimal recycled = service.recycleAllocatedBalance(9, "admin", "测试公司", new BigDecimal("100.00"));
+
+        assertEquals(new BigDecimal("0.00"), recycled);
+        verify(mapper, never()).insert(any(TCompanyAllocateRecord.class));
+    }
+
+    @Test
+    void recycleAllocatedBalanceThrowsWhenLockBusy() throws InterruptedException {
+        when(lock.tryLock(5L, 30L, TimeUnit.SECONDS)).thenReturn(false);
+
+        assertThrows(ServiceException.class,
+                () -> service.recycleAllocatedBalance(9, "admin", "测试公司", new BigDecimal("100.00")));
+        verify(vinMapper, never()).selectList(any());
+    }
+
+    @Test
+    void recycleAllocatedBalanceRejectsMissingCompany() {
+        assertThrows(ServiceException.class, () -> service.recycleAllocatedBalance(null, "admin", null, null));
+        verify(mapper, never()).insert(any(TCompanyAllocateRecord.class));
+    }
+
     private String repeat(char value, int count) {
         StringBuilder builder = new StringBuilder(count);
         for (int i = 0; i < count; i++) {

--
Gitblit v1.7.1