package com.tcm.training.admin.member.service; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.tcm.training.admin.common.PageResponse; import com.tcm.training.admin.live.entity.Clinic; import com.tcm.training.admin.live.mapper.ClinicMapper; import com.tcm.training.admin.member.dto.UserBatchClinicUpdateRequest; import com.tcm.training.admin.member.dto.UserUpdateRequest; import com.tcm.training.admin.member.entity.AppUser; import com.tcm.training.admin.member.mapper.UserMapper; import com.tcm.training.admin.member.row.UserDetailRow; import com.tcm.training.admin.member.row.UserListRow; import com.tcm.training.admin.member.vo.UserDetailVO; import com.tcm.training.admin.member.vo.UserListItemVO; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.junit.jupiter.api.BeforeAll; 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.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; 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.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class UserAppServiceTest { @BeforeAll static void initLambdaCache() { MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); TableInfoHelper.initTableInfo(assistant, AppUser.class); } @Mock private UserMapper userMapper; @Mock private ClinicMapper clinicMapper; @Mock private BCryptPasswordEncoder passwordEncoder; @InjectMocks private UserAppService userAppService; @Test void pageShouldMapMemberExpireAtForMemberUserQuery() { LocalDateTime memberExpireAt = LocalDateTime.of(2026, 6, 8, 3, 15); UserListRow row = new UserListRow(); row.setId(11L); row.setNickname("王美丽"); row.setPhone("14712346905"); row.setAvatar("https://cdn.test/avatar.png"); row.setIsMember(1); row.setMemberExpireAt(memberExpireAt); IPage pageResult = new Page(1, 10, 1); pageResult.setRecords(Collections.singletonList(row)); when(userMapper.selectUserPage( any(), eq("王美丽"), eq("14712346905"), isNull(), isNull(), isNull(), eq("ACTIVE"), isNull(), isNull(), isNull(), isNull() )).thenReturn(pageResult); PageResponse result = userAppService.page( 1, 10, " 王美丽 ", "14712346905", null, null, null, "active", null, null, null, null ); assertEquals(1, result.getList().size()); assertEquals(memberExpireAt, result.getList().get(0).getMemberExpireAt()); assertEquals(Boolean.TRUE, result.getList().get(0).getIsMember()); } @Test void pageShouldRejectInvalidMemberStatus() { IllegalArgumentException error = assertThrows( IllegalArgumentException.class, () -> userAppService.page( 1, 10, null, null, null, null, null, "INVALID", null, null, null, null ) ); assertEquals("会员状态筛选不合法", error.getMessage()); } @Test void unbindWechatShouldClearWechatFieldsWithExplicitNullUpdate() { AppUser user = new AppUser(); user.setId(12L); user.setDeleted(0); UserDetailRow detailRow = new UserDetailRow(); detailRow.setId(12L); detailRow.setNickname("张医生"); when(userMapper.selectOne(any())).thenReturn(user); when(userMapper.selectUserDetail(12L)).thenReturn(detailRow); UserDetailVO result = userAppService.unbindWechat(12L); ArgumentCaptor> wrapperCaptor = ArgumentCaptor.forClass(LambdaUpdateWrapper.class); verify(userMapper).update(isNull(), wrapperCaptor.capture()); verify(userMapper, never()).updateById(any(AppUser.class)); String sqlSet = wrapperCaptor.getValue().getSqlSet(); assertTrue(sqlSet.contains("wechat_openid")); assertTrue(sqlSet.contains("wechat_nickname")); assertTrue(sqlSet.contains("wechat_avatar")); assertTrue(sqlSet.contains("wechat_bound_at")); assertTrue(wrapperCaptor.getValue().getParamNameValuePairs().values().stream().allMatch(value -> value == null)); assertEquals(Long.valueOf(12L), result.getId()); } @ParameterizedTest @CsvSource({"10, 100", "20, 100", "20, "}) void updateShouldSynchronizeClinicBindingIncludingStaleRelations(Long oldClinicId, Long oldDoctorId) { AppUser user = prepareUserUpdate(0); user.setClinicId(oldClinicId); user.setSuperiorClinicUserId(oldDoctorId); prepareClinicAccounts(1); userAppService.update(12L, updateRequest(20L)); verify(userMapper).updateById(user); assertClinicBinding(Collections.singletonList(12L), 20L, 200L); } @Test void updateShouldExplicitlyClearBothClinicFields() { AppUser user = prepareUserUpdate(0); user.setClinicId(10L); user.setSuperiorClinicUserId(100L); userAppService.update(12L, updateRequest(null)); assertClinicBinding(Collections.singletonList(12L), null, null); verify(clinicMapper, never()).selectById(any()); } @Test void updateShouldPreserveDoctorAccountClinic() { AppUser user = prepareUserUpdate(1); user.setClinicId(10L); userAppService.update(12L, updateRequest(null)); assertEquals(Long.valueOf(10L), user.getClinicId()); verify(userMapper).updateById(user); verify(userMapper, never()).update(any(), any()); } @Test void batchUpdateShouldSynchronizeBothClinicFields() { prepareClinicAccounts(1); when(userMapper.selectCount(any())).thenReturn(2L); userAppService.batchUpdateClinic(batchRequest()); assertClinicBinding(Arrays.asList(12L, 13L), 20L, 200L); } @ParameterizedTest @ValueSource(ints = {0, 2}) void updateShouldRejectMissingOrAmbiguousClinicAccount(int accountCount) { AppUser user = new AppUser(); user.setId(12L); user.setType(0); when(userMapper.selectOne(any())).thenReturn(user); prepareClinicAccounts(accountCount); IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> userAppService.update(12L, updateRequest(20L))); assertEquals(accountCount == 0 ? "目标诊所尚未关联诊所账号,无法绑定患者" : "目标诊所关联多个账号,请先确认患者归属", error.getMessage()); assertNoUserWrite(); } @ParameterizedTest @ValueSource(ints = {0, 2}) void batchUpdateShouldRejectMissingOrAmbiguousClinicAccount(int accountCount) { prepareClinicAccounts(accountCount); assertThrows(IllegalArgumentException.class, () -> userAppService.batchUpdateClinic(batchRequest())); assertNoUserWrite(); } @ParameterizedTest @ValueSource(booleans = {false, true}) void batchUpdateShouldRejectMissingOrDeletedClinic(boolean deleted) { if (deleted) { Clinic clinic = new Clinic(); clinic.setId(20L); clinic.setDeleted(1); when(clinicMapper.selectById(20L)).thenReturn(clinic); } IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> userAppService.batchUpdateClinic(batchRequest())); assertEquals("绑定诊所不存在", error.getMessage()); assertNoUserWrite(); } @Test void batchUpdateShouldRejectUsersOutsidePersonalUserScope() { prepareClinicAccounts(1); when(userMapper.selectCount(any())).thenAnswer(invocation -> { com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper query = invocation.getArgument(0); assertTrue(query.getSqlSegment().contains("type =")); assertTrue(query.getParamNameValuePairs().containsValue(0)); return 1L; }); IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> userAppService.batchUpdateClinic(batchRequest())); assertEquals("部分用户不存在或不是普通用户", error.getMessage()); assertNoUserWrite(); } private AppUser prepareUserUpdate(int type) { AppUser user = new AppUser(); user.setId(12L); user.setType(type); user.setDeleted(0); // 第一次查询用户,第二次检查手机号冲突。 when(userMapper.selectOne(any())).thenReturn(user, null); UserDetailRow detail = new UserDetailRow(); detail.setId(12L); when(userMapper.selectUserDetail(12L)).thenReturn(detail); return user; } private void prepareClinicAccounts(int count) { Clinic clinic = new Clinic(); clinic.setId(20L); clinic.setDeleted(0); when(clinicMapper.selectById(20L)).thenReturn(clinic); AppUser doctor = new AppUser(); doctor.setId(200L); doctor.setType(1); doctor.setClinicId(20L); when(userMapper.selectList(any())).thenAnswer(invocation -> { com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper query = invocation.getArgument(0); String sql = query.getSqlSegment(); assertTrue(sql.contains("clinic_id =") && sql.contains("type =") && sql.contains("deleted =")); assertTrue(query.getParamNameValuePairs().values().containsAll(Arrays.asList(20L, 1, 0))); return Collections.nCopies(count, doctor); }); } private UserUpdateRequest updateRequest(Long clinicId) { UserUpdateRequest request = new UserUpdateRequest(); request.setNickname("测试用户"); request.setPhone("13800000000"); request.setStatus(1); request.setClinicId(clinicId); return request; } private UserBatchClinicUpdateRequest batchRequest() { UserBatchClinicUpdateRequest request = new UserBatchClinicUpdateRequest(); request.setUserIds(Arrays.asList(12L, 13L)); request.setClinicId(20L); return request; } private void assertNoUserWrite() { verify(userMapper, never()).updateById(any(AppUser.class)); verify(userMapper, never()).update(any(), any()); } private void assertClinicBinding(List userIds, Long clinicId, Long doctorId) { ArgumentCaptor> captor = ArgumentCaptor.forClass(LambdaUpdateWrapper.class); verify(userMapper).update(isNull(), captor.capture()); LambdaUpdateWrapper update = captor.getValue(); // 按列名核对绑定参数,避免诊所 ID 和诊所账号 ID 被误用。 String[] assignments = update.getSqlSet().split(","); assertEquals(2, assignments.length); assertAssignment(update, assignments[0], "clinic_id", clinicId); assertAssignment(update, assignments[1], "superior_clinic_user_id", doctorId); String sql = update.getSqlSegment(); assertTrue(sql.contains("id IN") && sql.contains("type =") && sql.contains("deleted =")); assertTrue(update.getParamNameValuePairs().values().containsAll(userIds)); assertTrue(update.getParamNameValuePairs().containsValue(0)); } private void assertAssignment(LambdaUpdateWrapper update, String assignment, String column, Long expectedValue) { assertTrue(assignment.startsWith(column + "=")); String parameter = assignment.substring(assignment.lastIndexOf('.') + 1, assignment.length() - 1); assertTrue(update.getParamNameValuePairs().containsKey(parameter)); assertEquals(expectedValue, update.getParamNameValuePairs().get(parameter)); } }