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<UserListRow> pageResult = new Page<UserListRow>(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<UserListItemVO> 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<LambdaUpdateWrapper<AppUser>> 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<AppUser> 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<AppUser> 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<Long> userIds, Long clinicId, Long doctorId) {
|
ArgumentCaptor<LambdaUpdateWrapper<AppUser>> captor = ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
|
verify(userMapper).update(isNull(), captor.capture());
|
LambdaUpdateWrapper<AppUser> 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<AppUser> 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));
|
}
|
}
|