zjk
2026-09-18 e429c291995f3f20184079e480031533ae5c03e6
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/member/service/UserAppServiceTest.java
@@ -6,7 +6,10 @@
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;
@@ -17,6 +20,9 @@
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;
@@ -24,7 +30,9 @@
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;
@@ -133,4 +141,191 @@
        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));
    }
}