zjk
2026-09-29 54dcf0a82e7490fa58b1c056d5c0b9cb739c2931
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/service/UserAppService.java
@@ -7,24 +7,19 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tcm.training.admin.common.DateTimes;
import com.tcm.training.admin.common.PageResponse;
import com.tcm.training.admin.common.PhoneMasker;
import com.tcm.training.admin.live.entity.Clinic;
import com.tcm.training.admin.member.entity.AppUser;
import com.tcm.training.admin.live.mapper.ClinicMapper;
import com.tcm.training.admin.member.excel.MemberUserExcelRow;
import com.tcm.training.admin.member.dto.UserBatchClinicUpdateRequest;
import com.tcm.training.admin.member.dto.UserResetPasswordRequest;
import com.tcm.training.admin.member.dto.UserStatusRequest;
import com.tcm.training.admin.member.dto.UserUpdateRequest;
import com.tcm.training.admin.member.entity.AppUser;
import com.tcm.training.admin.member.excel.MemberUserExcelRow;
import com.tcm.training.admin.member.excel.UserExcelRow;
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.ClinicInUserVO;
import com.tcm.training.admin.member.vo.ClinicOptionVO;
import com.tcm.training.admin.member.vo.UserDetailVO;
import com.tcm.training.admin.member.vo.UserListItemVO;
import com.tcm.training.admin.member.vo.UserStatusVO;
import com.tcm.training.admin.member.vo.WechatBindingVO;
import com.tcm.training.admin.member.vo.*;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -59,6 +54,7 @@
        this.passwordEncoder = passwordEncoder;
    }
    /** 分页查询用户。 */
    public PageResponse<UserListItemVO> page(int page,
                                             int pageSize,
                                             String nickname,
@@ -90,7 +86,7 @@
            UserListItemVO item = new UserListItemVO();
            item.setId(row.getId());
            item.setNickname(row.getNickname());
            item.setPhone(PhoneMasker.mask(row.getPhone()));
            item.setPhone(row.getPhone());
            item.setAvatar(row.getAvatar());
            item.setType(row.getType());
            item.setClinicName(row.getClinicName());
@@ -105,6 +101,7 @@
        return PageResponse.of(list, result.getTotal(), result.getCurrent(), result.getSize());
    }
    /** 查询用户详情。 */
    public UserDetailVO detail(Long id) {
        UserDetailRow row = userMapper.selectUserDetail(id);
        if (row == null) {
@@ -145,15 +142,12 @@
        return vo;
    }
    @Transactional
    @Transactional(rollbackFor = Exception.class)
    /** 更新用户信息。 */
    public UserDetailVO update(Long id, UserUpdateRequest req) {
        AppUser user = requireUser(id);
        if (req.getClinicId() != null) {
            Clinic clinic = clinicMapper.selectById(req.getClinicId());
            if (clinic == null || (clinic.getDeleted() != null && clinic.getDeleted() == 1)) {
                throw new IllegalArgumentException("绑定诊所不存在");
            }
        }
        boolean personalUser = Integer.valueOf(0).equals(user.getType());
        Long clinicUserId = personalUser ? resolveClinicUserId(req.getClinicId()) : null;
        AppUser phoneConflict = userMapper.selectOne(new LambdaQueryWrapper<AppUser>()
                .eq(AppUser::getPhone, req.getPhone().trim())
                .ne(AppUser::getId, id)
@@ -167,13 +161,16 @@
        user.setPhone(req.getPhone().trim());
        user.setAvatar(trimToNull(req.getAvatar()));
        user.setStatus(req.getStatus());
        user.setClinicId(req.getClinicId());
        user.setMemberExpireAt(req.getMemberExpireAt());
        userMapper.updateById(user);
        if (personalUser) {
            updateClinicBinding(java.util.Collections.singletonList(id), req.getClinicId(), clinicUserId);
        }
        return detail(id);
    }
    @Transactional
    @Transactional(rollbackFor = Exception.class)
    /** 修改用户状态。 */
    public UserStatusVO patchStatus(Long id, UserStatusRequest req) {
        AppUser user = requireUser(id);
        user.setStatus(req.getStatus());
@@ -181,7 +178,8 @@
        return new UserStatusVO(id, req.getStatus());
    }
    @Transactional
    @Transactional(rollbackFor = Exception.class)
    /** 重置用户密码。 */
    public void resetPassword(Long id, UserResetPasswordRequest req) {
        AppUser user = requireUser(id);
        if (!req.getNewPassword().equals(req.getConfirmPassword())) {
@@ -192,13 +190,15 @@
        userMapper.updateById(user);
    }
    @Transactional
    @Transactional(rollbackFor = Exception.class)
    /** 删除用户。 */
    public void delete(Long id) {
        requireUser(id);
        userMapper.deleteById(id);
    }
    @Transactional
    @Transactional(rollbackFor = Exception.class)
    /** 解除用户微信绑定。 */
    public UserDetailVO unbindWechat(Long id) {
        requireUser(id);
        userMapper.update(null, new LambdaUpdateWrapper<AppUser>()
@@ -210,6 +210,54 @@
        return detail(id);
    }
    @Transactional(rollbackFor = Exception.class)
    /** 批量更新用户诊所归属。 */
    public void batchUpdateClinic(UserBatchClinicUpdateRequest req) {
        Long clinicUserId = resolveClinicUserId(req.getClinicId());
        Long userCount = userMapper.selectCount(new LambdaQueryWrapper<AppUser>()
                .in(AppUser::getId, req.getUserIds())
                .eq(AppUser::getType, 0)
                .eq(AppUser::getDeleted, 0));
        if (userCount != req.getUserIds().size()) {
            throw new IllegalArgumentException("部分用户不存在或不是普通用户");
        }
        updateClinicBinding(req.getUserIds(), req.getClinicId(), clinicUserId);
    }
    /** 诊所 ID 与诊所账号用户 ID 不同,患者查询依赖后者。 */
    private Long resolveClinicUserId(Long clinicId) {
        if (clinicId == null) {
            return null;
        }
        Clinic clinic = clinicMapper.selectById(clinicId);
        if (clinic == null || Integer.valueOf(1).equals(clinic.getDeleted())) {
            throw new IllegalArgumentException("绑定诊所不存在");
        }
        List<AppUser> clinicUsers = userMapper.selectList(new LambdaQueryWrapper<AppUser>()
                .eq(AppUser::getClinicId, clinicId)
                .eq(AppUser::getType, 1)
                .eq(AppUser::getDeleted, 0)
                .last("limit 2"));
        if (clinicUsers.isEmpty()) {
            throw new IllegalArgumentException("目标诊所尚未关联诊所账号,无法绑定患者");
        }
        if (clinicUsers.size() > 1) {
            throw new IllegalArgumentException("目标诊所关联多个账号,请先确认患者归属");
        }
        return clinicUsers.get(0).getId();
    }
    /** 同步两个归属字段;显式 set 确保解除绑定时 null 也能写入。 */
    private void updateClinicBinding(List<Long> userIds, Long clinicId, Long clinicUserId) {
        userMapper.update(null, new LambdaUpdateWrapper<AppUser>()
                .in(AppUser::getId, userIds)
                .eq(AppUser::getType, 0)
                .eq(AppUser::getDeleted, 0)
                .set(AppUser::getClinicId, clinicId)
                .set(AppUser::getSuperiorClinicUserId, clinicUserId));
    }
    /** 查询诊所选项。 */
    public List<ClinicOptionVO> listClinicOptions() {
        List<Clinic> clinics = clinicMapper.selectList(new LambdaQueryWrapper<Clinic>()
                .eq(Clinic::getDeleted, 0)
@@ -258,7 +306,7 @@
                MemberUserExcelRow excel = new MemberUserExcelRow();
                excel.setId(row.getId());
                excel.setNickname(row.getNickname());
                excel.setPhone(PhoneMasker.mask(row.getPhone()));
                excel.setPhone(row.getPhone());
                excel.setMemberStatus(row.getIsMember() != null && row.getIsMember() == 1 ? "生效中" : "已失效");
                excel.setMemberExpireAt(DateTimes.format(row.getMemberExpireAt()));
                memberExcelRows.add(excel);
@@ -274,7 +322,7 @@
            excel.setId(row.getId());
            excel.setNickname(row.getNickname());
            excel.setType(typeText(row.getType()));
            excel.setPhone(PhoneMasker.mask(row.getPhone()));
            excel.setPhone(row.getPhone());
            excel.setClinicName(row.getClinicName() == null ? "-" : row.getClinicName());
            excel.setAge(row.getAge());
            excel.setGender(genderText(row.getGender()));