package com.ruoyi.user.service.impl; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; 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.ArgumentMatchers.anyString; 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.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import com.ruoyi.common.core.constant.UserConstants; import com.ruoyi.common.core.exception.ServiceException; import com.ruoyi.user.config.UniversalCodeProperties; import com.ruoyi.user.domain.AccountUser; import com.ruoyi.user.domain.vo.AccountUserInfoVo; import com.ruoyi.user.domain.vo.EarningSummaryVo; import com.ruoyi.user.enums.MemberLevelEnum; import com.ruoyi.user.mapper.AccountEarningRecordMapper; import com.ruoyi.user.mapper.AccountUserMapper; /** * 会员服务单元测试 * * 覆盖会员注册(通用推荐码放行、无效/停用推荐码拒绝、正常绑定推荐人) * 与个人中心会员信息查询(正常返回、入参/会员缺失校验、收益与等级兜底)。 * * @author hugray */ @ExtendWith(MockitoExtension.class) class AccountUserServiceImplTest { private static final String PHONE = "13800000000"; private static final String UNIVERSAL_CODE = "HUGRAY88"; @Mock private AccountUserMapper accountUserMapper; @Mock private AccountEarningRecordMapper accountEarningRecordMapper; @Mock private UniversalCodeProperties universalCodeProperties; @InjectMocks private AccountUserServiceImpl accountUserService; @Test @DisplayName("命中通用推荐码应注册成功且不绑定推荐人") void registerMember_shouldNotBindReferrer_whenUniversalCodeHit() { when(universalCodeProperties.getUniversalReferralCode()).thenReturn(UNIVERSAL_CODE); when(accountUserMapper.selectAccountUserByPhone(PHONE)).thenReturn(null); when(accountUserMapper.selectAccountUserByReferralCode(anyString())).thenReturn(null); when(accountUserMapper.insertAccountUser(any(AccountUser.class))).thenReturn(1); accountUserService.registerMember(PHONE, UNIVERSAL_CODE); ArgumentCaptor captor = ArgumentCaptor.forClass(AccountUser.class); verify(accountUserMapper).insertAccountUser(captor.capture()); AccountUser saved = captor.getValue(); assertNull(saved.getReferrerId()); assertNotNull(saved.getMyReferralCode()); // 姓名注册未采集,落库留空(不再以手机号填充) assertNull(saved.getNickname()); // 通用推荐码注册直接发放 L4 会员等级 assertEquals(MemberLevelEnum.L4.getCode(), saved.getSystemLevel()); // 通用码不参与推荐人定位 verify(accountUserMapper, never()).selectAccountUserByReferralCode(UNIVERSAL_CODE); } @Test @DisplayName("未配置通用推荐码且推荐码无效时应拒绝注册") void registerMember_shouldThrow_whenReferralCodeInvalid() { when(universalCodeProperties.getUniversalReferralCode()).thenReturn(null); when(accountUserMapper.selectAccountUserByPhone(PHONE)).thenReturn(null); when(accountUserMapper.selectAccountUserByReferralCode("BADCODE")).thenReturn(null); ServiceException ex = assertThrows(ServiceException.class, () -> accountUserService.registerMember(PHONE, "BADCODE")); assertEquals("推荐人码无效", ex.getMessage()); } @Test @DisplayName("推荐人账号停用时应拒绝注册") void registerMember_shouldThrow_whenReferrerDisabled() { AccountUser referrer = new AccountUser(); referrer.setUserId(100L); referrer.setStatus(UserConstants.USER_DISABLE); when(universalCodeProperties.getUniversalReferralCode()).thenReturn(null); when(accountUserMapper.selectAccountUserByPhone(PHONE)).thenReturn(null); when(accountUserMapper.selectAccountUserByReferralCode("VALID123")).thenReturn(referrer); ServiceException ex = assertThrows(ServiceException.class, () -> accountUserService.registerMember(PHONE, "VALID123")); assertEquals("推荐人账号异常", ex.getMessage()); } @Test @DisplayName("推荐码有效时应绑定推荐人") void registerMember_shouldBindReferrer_whenReferralCodeValid() { AccountUser referrer = new AccountUser(); referrer.setUserId(100L); referrer.setStatus(UserConstants.NORMAL); when(universalCodeProperties.getUniversalReferralCode()).thenReturn(null); when(accountUserMapper.selectAccountUserByPhone(PHONE)).thenReturn(null); // 仅 "VALID123" 命中推荐人;其余(本人推荐码查重)返回 null,避免影响唯一码生成 when(accountUserMapper.selectAccountUserByReferralCode(anyString())) .thenAnswer(invocation -> "VALID123".equals(invocation.getArgument(0)) ? referrer : null); when(accountUserMapper.insertAccountUser(any(AccountUser.class))).thenReturn(1); accountUserService.registerMember(PHONE, "VALID123"); ArgumentCaptor captor = ArgumentCaptor.forClass(AccountUser.class); verify(accountUserMapper).insertAccountUser(captor.capture()); assertEquals(100L, captor.getValue().getReferrerId()); // 非通用码注册不设等级,走库默认值 C(普通用户) assertNull(captor.getValue().getSystemLevel()); } @Test @DisplayName("会员存在时应返回昵称/头像/等级与收益汇总") void getAccountUserInfo_shouldReturnProfile_whenMemberExists() { AccountUser member = new AccountUser(); member.setUserId(66L); member.setNickname("辉煌G891"); member.setPhone("13355555555"); member.setAvatar("https://cdn.example.com/a.png"); member.setSystemLevel(MemberLevelEnum.L3.getCode()); member.setAuthStatus("2"); EarningSummaryVo summary = new EarningSummaryVo(1000L, 40000L, 12800000L); when(accountUserMapper.selectAccountUserById(66L)).thenReturn(member); when(accountEarningRecordMapper.selectEarningSummary(66L)).thenReturn(summary); AccountUserInfoVo vo = accountUserService.getAccountUserInfo(66L); assertEquals(66L, vo.getUserId()); assertEquals("辉煌G891", vo.getNickname()); assertEquals("133****5555", vo.getPhone()); assertEquals("https://cdn.example.com/a.png", vo.getAvatar()); assertEquals(MemberLevelEnum.L3.getCode(), vo.getMemberLevel()); // 认证状态原样下发,供个人中心「实名认证」行徽标回显 assertEquals("2", vo.getAuthStatus()); assertEquals(1000L, vo.getEarnings().getToday()); assertEquals(40000L, vo.getEarnings().getMonth()); assertEquals(12800000L, vo.getEarnings().getTotal()); } @Test @DisplayName("会员ID为空时应抛出获取会员信息失败") void getAccountUserInfo_shouldThrow_whenUserIdNull() { ServiceException ex = assertThrows(ServiceException.class, () -> accountUserService.getAccountUserInfo(null)); assertEquals("获取会员信息失败", ex.getMessage()); } @Test @DisplayName("会员不存在时应抛出获取会员信息失败") void getAccountUserInfo_shouldThrow_whenMemberNotFound() { when(accountUserMapper.selectAccountUserById(66L)).thenReturn(null); ServiceException ex = assertThrows(ServiceException.class, () -> accountUserService.getAccountUserInfo(66L)); assertEquals("获取会员信息失败", ex.getMessage()); } @Test @DisplayName("无收益流水时应兜底返回全零汇总") void getAccountUserInfo_shouldFallbackZeroEarnings_whenNoRecord() { AccountUser member = new AccountUser(); member.setUserId(66L); member.setSystemLevel(MemberLevelEnum.L1.getCode()); when(accountUserMapper.selectAccountUserById(66L)).thenReturn(member); when(accountEarningRecordMapper.selectEarningSummary(66L)).thenReturn(null); AccountUserInfoVo vo = accountUserService.getAccountUserInfo(66L); assertNotNull(vo.getEarnings()); assertEquals(0L, vo.getEarnings().getToday()); assertEquals(0L, vo.getEarnings().getMonth()); assertEquals(0L, vo.getEarnings().getTotal()); } @Test @DisplayName("等级为空时应兜底为 C") void getAccountUserInfo_shouldFallbackC_whenSystemLevelBlank() { assertFallbackLevel(null, MemberLevelEnum.C.getCode()); assertFallbackLevel("", MemberLevelEnum.C.getCode()); } @Test @DisplayName("等级为非法值时应兜底为 C") void getAccountUserInfo_shouldFallbackC_whenSystemLevelIllegal() { assertFallbackLevel("L9", MemberLevelEnum.C.getCode()); } @Test @DisplayName("认证状态为空时应兜底为未认证(0)") void getAccountUserInfo_shouldFallbackNoneAuthStatus_whenBlank() { AccountUser member = new AccountUser(); member.setUserId(66L); member.setSystemLevel(MemberLevelEnum.C.getCode()); when(accountUserMapper.selectAccountUserById(66L)).thenReturn(member); when(accountEarningRecordMapper.selectEarningSummary(66L)).thenReturn(EarningSummaryVo.zero()); AccountUserInfoVo vo = accountUserService.getAccountUserInfo(66L); assertEquals("0", vo.getAuthStatus()); } /** * 校验不同等级取值下的等级兜底结果 * * @param rawLevel 库中原始等级值 * @param expectedLevel 期望下发的等级 */ private void assertFallbackLevel(String rawLevel, String expectedLevel) { AccountUser member = new AccountUser(); member.setUserId(66L); member.setSystemLevel(rawLevel); when(accountUserMapper.selectAccountUserById(66L)).thenReturn(member); when(accountEarningRecordMapper.selectEarningSummary(66L)).thenReturn(EarningSummaryVo.zero()); AccountUserInfoVo vo = accountUserService.getAccountUserInfo(66L); assertEquals(expectedLevel, vo.getMemberLevel()); } @Test @DisplayName("更新头像时应只写入 avatar 列") void updateAvatar_shouldUpdateAvatar_whenValid() { when(accountUserMapper.updateUserAvatar(any(AccountUser.class))).thenReturn(1); accountUserService.updateAvatar(66L, "https://cdn.example.com/new.png"); ArgumentCaptor captor = ArgumentCaptor.forClass(AccountUser.class); verify(accountUserMapper).updateUserAvatar(captor.capture()); assertEquals(66L, captor.getValue().getUserId()); assertEquals("https://cdn.example.com/new.png", captor.getValue().getAvatar()); } @Test @DisplayName("头像地址为空时应抛出提示且不落库") void updateAvatar_shouldThrow_whenAvatarBlank() { ServiceException ex = assertThrows(ServiceException.class, () -> accountUserService.updateAvatar(66L, "")); assertEquals("头像地址不能为空", ex.getMessage()); verify(accountUserMapper, never()).updateUserAvatar(any(AccountUser.class)); } @Test @DisplayName("头像地址超长时应抛出提示且不落库") void updateAvatar_shouldThrow_whenAvatarTooLong() { String longUrl = "a".repeat(256); ServiceException ex = assertThrows(ServiceException.class, () -> accountUserService.updateAvatar(66L, longUrl)); assertEquals("头像地址过长", ex.getMessage()); verify(accountUserMapper, never()).updateUserAvatar(any(AccountUser.class)); } @Test @DisplayName("会员不存在(影响行数为 0)时应抛出更新失败") void updateAvatar_shouldThrow_whenMemberNotFound() { when(accountUserMapper.updateUserAvatar(any(AccountUser.class))).thenReturn(0); ServiceException ex = assertThrows(ServiceException.class, () -> accountUserService.updateAvatar(66L, "https://cdn.example.com/new.png")); assertEquals("更新头像失败", ex.getMessage()); } @Test @DisplayName("会员ID为空时应抛出更新失败") void updateAvatar_shouldThrow_whenUserIdNull() { ServiceException ex = assertThrows(ServiceException.class, () -> accountUserService.updateAvatar(null, "https://cdn.example.com/new.png")); assertEquals("更新头像失败", ex.getMessage()); } }