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<AccountUser> 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<SecurityUtils> 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<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
|
{
|
mocked.when(SecurityUtils::getUsername).thenReturn("admin");
|
assertEquals(1, levelApplyService.auditLevelApply(APPLY_ID, "2", "条件未达标"));
|
|
ArgumentCaptor<AccountLevelApply> 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<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
|
{
|
mocked.when(SecurityUtils::getUsername).thenReturn("admin");
|
levelApplyService.auditLevelApply(APPLY_ID, "1", null);
|
|
assertEquals("L2", captureLevelUpdate().getSystemLevel());
|
|
ArgumentCaptor<AccountLevelApply> 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<SecurityUtils> 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<AccountLevelApply> 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<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
|
{
|
mocked.when(SecurityUtils::getUsername).thenReturn("admin");
|
levelApplyService.auditLevelApply(APPLY_ID, "1", null);
|
|
assertEquals("L1", captureLevelUpdate().getSystemLevel());
|
|
ArgumentCaptor<AccountUser> captor = ArgumentCaptor.forClass(AccountUser.class);
|
verify(accountUserService).updateAccountUserRelation(captor.capture());
|
assertEquals(302L, captor.getValue().getSupplierId());
|
// 换挂走的是无条件赋值 4 列的语句,referrerId 漏传就是静默解除推荐关系
|
assertEquals(777L, captor.getValue().getReferrerId());
|
|
ArgumentCaptor<AccountLevelApply> 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<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
|
{
|
mocked.when(SecurityUtils::getUsername).thenReturn("admin");
|
levelApplyService.auditLevelApply(APPLY_ID, "1", null);
|
|
ArgumentCaptor<AccountUser> captor = ArgumentCaptor.forClass(AccountUser.class);
|
verify(accountUserService).updateAccountUserRelation(captor.capture());
|
assertNull(captor.getValue().getSupplierId());
|
assertEquals(777L, captor.getValue().getReferrerId());
|
|
ArgumentCaptor<AccountLevelApply> 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<SecurityUtils> 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<SecurityUtils> 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<SecurityUtils> 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<SecurityUtils> 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<SecurityUtils> 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<SecurityUtils> 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));
|
}
|
}
|