package com.ruoyi.user.service.impl; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import com.ruoyi.common.core.exception.ServiceException; import com.ruoyi.common.security.utils.SecurityUtils; import com.ruoyi.user.domain.AccountLevelApply; import com.ruoyi.user.domain.AccountUser; import com.ruoyi.user.mapper.AccountCertificationMapper; import com.ruoyi.user.mapper.AccountLevelApplyMapper; import com.ruoyi.user.mapper.AccountUserMapper; import com.ruoyi.user.service.IAccountUserService; /** * 等级升级审核服务单元测试 * * 重点覆盖审核通过时的四条分支(无上级、上级 L2/L3/L4、上级 L1 有上级、上级 L1 无上级) * 与三类拒绝(降级、目标等级非法、供货关系异常),以及「算不出来就一行都不落」这条约定。 * * @author hugray */ @ExtendWith(MockitoExtension.class) class AccountLevelApplyServiceImplTest { private static final Long APPLY_ID = 100L; private static final Long USER_ID = 300L; @Mock private AccountLevelApplyMapper levelApplyMapper; @Mock private AccountUserMapper accountUserMapper; @Mock private AccountCertificationMapper certificationMapper; @Mock private IAccountUserService accountUserService; @InjectMocks private AccountLevelApplyServiceImpl levelApplyService; private static AccountLevelApply applyOf(String targetLevel) { AccountLevelApply apply = new AccountLevelApply(); apply.setId(APPLY_ID); apply.setUserId(USER_ID); apply.setCurrentLevel("C"); apply.setTargetLevel(targetLevel); apply.setAuditStatus("0"); return apply; } private static AccountUser userOf(String level, Long supplierId) { AccountUser user = new AccountUser(); user.setUserId(USER_ID); user.setSystemLevel(level); user.setSupplierId(supplierId); // 推荐关系非空,用来验证换挂时 referrerId 被原样带上而不是被清空 user.setReferrerId(777L); return user; } private static AccountUser memberOf(Long userId, String level, Long supplierId) { AccountUser member = new AccountUser(); member.setUserId(userId); member.setSystemLevel(level); member.setSupplierId(supplierId); return member; } /** 捕获写入 account_user 的等级语句参数,供断言实际生效的等级 */ private AccountUser captureLevelUpdate() { ArgumentCaptor captor = ArgumentCaptor.forClass(AccountUser.class); verify(accountUserMapper).updateUserLevel(captor.capture()); return captor.getValue(); } @Test @DisplayName("审核结果既不是通过也不是驳回时应拒绝,且不碰任何数据") void audit_shouldThrow_whenStatusInvalid() { ServiceException ex = assertThrows(ServiceException.class, () -> levelApplyService.auditLevelApply(APPLY_ID, "3", "随便写写")); assertEquals("审核结果只能是「通过」或「驳回」", ex.getMessage()); verify(levelApplyMapper, never()).auditLevelApply(any(AccountLevelApply.class)); } @Test @DisplayName("驳回未填审核意见时应拒绝") void audit_shouldThrow_whenRejectWithoutRemark() { ServiceException ex = assertThrows(ServiceException.class, () -> levelApplyService.auditLevelApply(APPLY_ID, "2", " ")); assertEquals("驳回时必须填写审核意见", ex.getMessage()); verify(levelApplyMapper, never()).auditLevelApply(any(AccountLevelApply.class)); } @Test @DisplayName("申请不存在时应拒绝") void audit_shouldThrow_whenApplyMissing() { when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(null); ServiceException ex = assertThrows(ServiceException.class, () -> levelApplyService.auditLevelApply(APPLY_ID, "1", null)); assertEquals("升级申请不存在", ex.getMessage()); } @Test @DisplayName("并发闸门更新 0 行时应拒绝,且等级与关系一行都没动") void audit_shouldThrow_whenGateRejects() { when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2")); when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", null)); when(levelApplyMapper.auditLevelApply(any(AccountLevelApply.class))).thenReturn(0); try (MockedStatic mocked = mockStatic(SecurityUtils.class)) { ServiceException ex = assertThrows(ServiceException.class, () -> levelApplyService.auditLevelApply(APPLY_ID, "1", null)); assertEquals("该申请已审核,不能重复处理", ex.getMessage()); } // 闸门是第一条写语句,被挡住时后面两步不该执行 verify(accountUserMapper, never()).updateUserLevel(any(AccountUser.class)); verify(accountUserService, never()).updateAccountUserRelation(any(AccountUser.class)); } @Test @DisplayName("驳回只改申请单,不读会员也不动等级与关系") void audit_reject_shouldOnlyUpdateApply() { when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2")); when(levelApplyMapper.auditLevelApply(any(AccountLevelApply.class))).thenReturn(1); try (MockedStatic mocked = mockStatic(SecurityUtils.class)) { mocked.when(SecurityUtils::getUsername).thenReturn("admin"); assertEquals(1, levelApplyService.auditLevelApply(APPLY_ID, "2", "条件未达标")); ArgumentCaptor captor = ArgumentCaptor.forClass(AccountLevelApply.class); verify(levelApplyMapper).auditLevelApply(captor.capture()); // 驳回不产生生效结果,两列必须保持为空,否则事后分不清这单是没生效还是生效成了空 assertNull(captor.getValue().getFinalLevel()); assertNull(captor.getValue().getSupplierAfterId()); } verify(accountUserMapper, never()).selectAccountUserById(any()); verify(accountUserMapper, never()).updateUserLevel(any(AccountUser.class)); verify(accountUserService, never()).updateAccountUserRelation(any(AccountUser.class)); } @Test @DisplayName("无上级时通过应升到目标等级,且不改供货关系") void audit_passWithoutSupplier_shouldUseTargetLevel() { when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2")); when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", null)); when(levelApplyMapper.auditLevelApply(any(AccountLevelApply.class))).thenReturn(1); try (MockedStatic mocked = mockStatic(SecurityUtils.class)) { mocked.when(SecurityUtils::getUsername).thenReturn("admin"); levelApplyService.auditLevelApply(APPLY_ID, "1", null); assertEquals("L2", captureLevelUpdate().getSystemLevel()); ArgumentCaptor captor = ArgumentCaptor.forClass(AccountLevelApply.class); verify(levelApplyMapper).auditLevelApply(captor.capture()); assertEquals("L2", captor.getValue().getFinalLevel()); assertNull(captor.getValue().getSupplierAfterId()); } verify(accountUserService, never()).updateAccountUserRelation(any(AccountUser.class)); } @ParameterizedTest(name = "上级等级 {0} 时通过应压到 L1") @ValueSource(strings = { "L2", "L3", "L4" }) @DisplayName("上级等级高于 L1 时通过应压到 L1,供货关系不动") void audit_passWithHigherSupplier_shouldForceL1(String supplierLevel) { when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L4")); when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", 301L)); when(accountUserMapper.selectAccountUserById(301L)).thenReturn(memberOf(301L, supplierLevel, null)); when(levelApplyMapper.auditLevelApply(any(AccountLevelApply.class))).thenReturn(1); try (MockedStatic mocked = mockStatic(SecurityUtils.class)) { mocked.when(SecurityUtils::getUsername).thenReturn("admin"); levelApplyService.auditLevelApply(APPLY_ID, "1", null); // 目标等级是 L4,但有上级,故生效等级是 L1 —— 这正是必须留 final_level 的原因 assertEquals("L1", captureLevelUpdate().getSystemLevel()); ArgumentCaptor captor = ArgumentCaptor.forClass(AccountLevelApply.class); verify(levelApplyMapper).auditLevelApply(captor.capture()); assertEquals("L1", captor.getValue().getFinalLevel()); assertEquals(301L, captor.getValue().getSupplierAfterId()); } // 上级仍严格高于 L1,关系合法,不该触发换挂 verify(accountUserService, never()).updateAccountUserRelation(any(AccountUser.class)); } @Test @DisplayName("上级是 L1 且有上级时通过应换挂到上级的供货人,且推荐关系原样保留") void audit_passWithL1Supplier_shouldRebindToGrandSupplier() { when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2")); when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", 301L)); when(accountUserMapper.selectAccountUserById(301L)).thenReturn(memberOf(301L, "L1", 302L)); when(accountUserMapper.selectAccountUserById(302L)).thenReturn(memberOf(302L, "L3", null)); when(levelApplyMapper.auditLevelApply(any(AccountLevelApply.class))).thenReturn(1); try (MockedStatic mocked = mockStatic(SecurityUtils.class)) { mocked.when(SecurityUtils::getUsername).thenReturn("admin"); levelApplyService.auditLevelApply(APPLY_ID, "1", null); assertEquals("L1", captureLevelUpdate().getSystemLevel()); ArgumentCaptor captor = ArgumentCaptor.forClass(AccountUser.class); verify(accountUserService).updateAccountUserRelation(captor.capture()); assertEquals(302L, captor.getValue().getSupplierId()); // 换挂走的是无条件赋值 4 列的语句,referrerId 漏传就是静默解除推荐关系 assertEquals(777L, captor.getValue().getReferrerId()); ArgumentCaptor applyCaptor = ArgumentCaptor.forClass(AccountLevelApply.class); verify(levelApplyMapper).auditLevelApply(applyCaptor.capture()); assertEquals(302L, applyCaptor.getValue().getSupplierAfterId()); } } @Test @DisplayName("上级是 L1 且自己也没有上级时通过应解绑供货人") void audit_passWithL1SupplierNoGrand_shouldUnbind() { when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L3")); when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", 301L)); when(accountUserMapper.selectAccountUserById(301L)).thenReturn(memberOf(301L, "L1", null)); when(levelApplyMapper.auditLevelApply(any(AccountLevelApply.class))).thenReturn(1); try (MockedStatic mocked = mockStatic(SecurityUtils.class)) { mocked.when(SecurityUtils::getUsername).thenReturn("admin"); levelApplyService.auditLevelApply(APPLY_ID, "1", null); ArgumentCaptor captor = ArgumentCaptor.forClass(AccountUser.class); verify(accountUserService).updateAccountUserRelation(captor.capture()); assertNull(captor.getValue().getSupplierId()); assertEquals(777L, captor.getValue().getReferrerId()); ArgumentCaptor applyCaptor = ArgumentCaptor.forClass(AccountLevelApply.class); verify(levelApplyMapper).auditLevelApply(applyCaptor.capture()); assertNull(applyCaptor.getValue().getSupplierAfterId()); } } @Test @DisplayName("换挂时应先写等级再写关系,关系校验才按升级后的等级为基线") void audit_passWithRebind_shouldWriteLevelBeforeRelation() { when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2")); when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", 301L)); when(accountUserMapper.selectAccountUserById(301L)).thenReturn(memberOf(301L, "L1", 302L)); when(accountUserMapper.selectAccountUserById(302L)).thenReturn(memberOf(302L, "L2", null)); when(levelApplyMapper.auditLevelApply(any(AccountLevelApply.class))).thenReturn(1); try (MockedStatic mocked = mockStatic(SecurityUtils.class)) { mocked.when(SecurityUtils::getUsername).thenReturn("admin"); levelApplyService.auditLevelApply(APPLY_ID, "1", null); } InOrder inOrder = inOrder(accountUserMapper, accountUserService); inOrder.verify(accountUserMapper).updateUserLevel(any(AccountUser.class)); inOrder.verify(accountUserService).updateAccountUserRelation(any(AccountUser.class)); } @Test @DisplayName("供货人已被逻辑删除时应拒绝且不落库,不静默当「无上级」") void audit_shouldThrow_whenSupplierMissing() { when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2")); when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", 301L)); // selectAccountUserById 的 where 自带 del_flag='0',已删除的供货人查不到 when(accountUserMapper.selectAccountUserById(301L)).thenReturn(null); try (MockedStatic mocked = mockStatic(SecurityUtils.class)) { ServiceException ex = assertThrows(ServiceException.class, () -> levelApplyService.auditLevelApply(APPLY_ID, "1", null)); assertEquals("该会员的供货人(编号 301)不存在或已注销,请先修正供货关系再审核", ex.getMessage()); } verify(accountUserMapper, never()).updateUserLevel(any(AccountUser.class)); verify(levelApplyMapper, never()).auditLevelApply(any(AccountLevelApply.class)); } @Test @DisplayName("换挂后的新供货人等级仍不高于生效等级时应拒绝(含自环脏数据)") void audit_shouldThrow_whenGrandSupplierNotHigher() { when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2")); when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", 301L)); when(accountUserMapper.selectAccountUserById(301L)).thenReturn(memberOf(301L, "L1", 302L)); // 302 是 L1,不严格高于生效等级 L1:换挂等于白换,还是一条非法关系 when(accountUserMapper.selectAccountUserById(302L)).thenReturn(memberOf(302L, "L1", null)); try (MockedStatic mocked = mockStatic(SecurityUtils.class)) { ServiceException ex = assertThrows(ServiceException.class, () -> levelApplyService.auditLevelApply(APPLY_ID, "1", null)); assertEquals("该会员上级(编号 301)的供货关系异常,换挂后等级仍不合法,请先修正关系", ex.getMessage()); } verify(accountUserMapper, never()).updateUserLevel(any(AccountUser.class)); verify(levelApplyMapper, never()).auditLevelApply(any(AccountLevelApply.class)); } @Test @DisplayName("按规则算出的等级低于会员当前等级时应拒绝(升级审核不支持降级)") void audit_shouldThrow_whenDowngrade() { // 会员已是 L2,有上级(L4),按规则通过后是 L1 —— 低于当前等级 when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L3")); when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("L2", 301L)); when(accountUserMapper.selectAccountUserById(301L)).thenReturn(memberOf(301L, "L4", null)); try (MockedStatic mocked = mockStatic(SecurityUtils.class)) { ServiceException ex = assertThrows(ServiceException.class, () -> levelApplyService.auditLevelApply(APPLY_ID, "1", null)); assertEquals("按当前规则通过后等级为 L1,低于该会员现有等级 L2,升级审核不支持降级,请驳回该申请", ex.getMessage()); } verify(accountUserMapper, never()).updateUserLevel(any(AccountUser.class)); verify(levelApplyMapper, never()).auditLevelApply(any(AccountLevelApply.class)); } @Test @DisplayName("目标等级不在 C/L1~L4 之内时应拒绝(无上级分支会把它直接落到会员身上)") void audit_shouldThrow_whenTargetLevelInvalid() { when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L5")); when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", null)); try (MockedStatic mocked = mockStatic(SecurityUtils.class)) { ServiceException ex = assertThrows(ServiceException.class, () -> levelApplyService.auditLevelApply(APPLY_ID, "1", null)); assertEquals("申请的目标等级不合法:L5", ex.getMessage()); } verify(accountUserMapper, never()).updateUserLevel(any(AccountUser.class)); } @Test @DisplayName("申请人已被逻辑删除时应拒绝") void audit_shouldThrow_whenApplicantMissing() { when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2")); when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(null); try (MockedStatic mocked = mockStatic(SecurityUtils.class)) { ServiceException ex = assertThrows(ServiceException.class, () -> levelApplyService.auditLevelApply(APPLY_ID, "1", null)); assertEquals("申请人不存在或已注销", ex.getMessage()); } verify(levelApplyMapper, never()).auditLevelApply(any(AccountLevelApply.class)); } }