hejianhao
2026-09-01 f4e8dfd7f8f11966f4b5dc8c6661cbc3feccc4cf
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/service/UserAppService.java
@@ -2,27 +2,25 @@
import cn.idev.excel.FastExcel;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
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.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;
@@ -41,6 +39,9 @@
    private static final Pattern PASSWORD_PATTERN = Pattern.compile(
            "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^A-Za-z\\d]).{12,64}$"
    );
    private static final String MEMBER_STATUS_ALL = "ALL";
    private static final String MEMBER_STATUS_ACTIVE = "ACTIVE";
    private static final String MEMBER_STATUS_EXPIRED = "EXPIRED";
    private final UserMapper userMapper;
    private final ClinicMapper clinicMapper;
@@ -61,8 +62,12 @@
                                             Long clinicId,
                                             Integer status,
                                             Integer isMember,
                                             String memberStatus,
                                             LocalDateTime memberExpireFrom,
                                             LocalDateTime memberExpireTo,
                                             LocalDateTime registeredFrom,
                                             LocalDateTime registeredTo) {
        String normalizedMemberStatus = normalizeMemberStatus(memberStatus);
        IPage<UserListRow> result = userMapper.selectUserPage(
                new Page<UserListRow>(page, pageSize),
                trimToNull(nickname),
@@ -70,6 +75,9 @@
                clinicId,
                status,
                isMember,
                normalizedMemberStatus,
                memberExpireFrom,
                memberExpireTo,
                registeredFrom,
                registeredTo
        );
@@ -78,13 +86,14 @@
            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());
            item.setAge(row.getAge());
            item.setGender(row.getGender());
            item.setIsMember(row.getIsMember() != null && row.getIsMember() == 1);
            item.setMemberExpireAt(row.getMemberExpireAt());
            item.setStatus(row.getStatus());
            item.setCreatedAt(row.getCreatedAt());
            list.add(item);
@@ -132,7 +141,7 @@
        return vo;
    }
    @Transactional
    @Transactional(rollbackFor = Exception.class)
    public UserDetailVO update(Long id, UserUpdateRequest req) {
        AppUser user = requireUser(id);
        if (req.getClinicId() != null) {
@@ -160,7 +169,7 @@
        return detail(id);
    }
    @Transactional
    @Transactional(rollbackFor = Exception.class)
    public UserStatusVO patchStatus(Long id, UserStatusRequest req) {
        AppUser user = requireUser(id);
        user.setStatus(req.getStatus());
@@ -168,7 +177,7 @@
        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())) {
@@ -179,21 +188,40 @@
        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) {
        AppUser user = requireUser(id);
        user.setWechatOpenid(null);
        user.setWechatNickname(null);
        user.setWechatAvatar(null);
        user.setWechatBoundAt(null);
        userMapper.updateById(user);
        requireUser(id);
        userMapper.update(null, new LambdaUpdateWrapper<AppUser>()
                .eq(AppUser::getId, id)
                .set(AppUser::getWechatOpenid, null)
                .set(AppUser::getWechatNickname, null)
                .set(AppUser::getWechatAvatar, null)
                .set(AppUser::getWechatBoundAt, null));
        return detail(id);
    }
    @Transactional(rollbackFor = Exception.class)
    public void batchUpdateClinic(UserBatchClinicUpdateRequest req) {
        Clinic clinic = clinicMapper.selectById(req.getClinicId());
        if (clinic == null || Integer.valueOf(1).equals(clinic.getDeleted())) {
            throw new IllegalArgumentException("诊所不存在");
        }
        Long userCount = userMapper.selectCount(new LambdaQueryWrapper<AppUser>()
                .in(AppUser::getId, req.getUserIds())
                .eq(AppUser::getDeleted, 0));
        if (userCount != req.getUserIds().size()) {
            throw new IllegalArgumentException("部分用户不存在");
        }
        userMapper.update(null, new LambdaUpdateWrapper<AppUser>()
                .in(AppUser::getId, req.getUserIds())
                .eq(AppUser::getDeleted, 0)
                .set(AppUser::getClinicId, req.getClinicId()));
    }
    public List<ClinicOptionVO> listClinicOptions() {
@@ -213,12 +241,17 @@
                       Long clinicId,
                       Integer status,
                       Integer isMember,
                       String memberStatus,
                       LocalDateTime memberExpireFrom,
                       LocalDateTime memberExpireTo,
                       LocalDateTime registeredFrom,
                       LocalDateTime registeredTo,
                       HttpServletResponse response) throws IOException {
        String normalizedMemberStatus = normalizeMemberStatus(memberStatus);
        boolean memberMode = normalizedMemberStatus != null;
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String filename = URLEncoder.encode("用户列表", "UTF-8").replace("+", "%20");
        String filename = URLEncoder.encode(memberMode ? "会员用户列表" : "用户列表", "UTF-8").replace("+", "%20");
        response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + filename + ".xlsx");
        List<UserListRow> rows = userMapper.selectUserList(
@@ -227,9 +260,28 @@
                clinicId,
                status,
                isMember,
                normalizedMemberStatus,
                memberExpireFrom,
                memberExpireTo,
                registeredFrom,
                registeredTo
        );
        if (memberMode) {
            List<MemberUserExcelRow> memberExcelRows = new ArrayList<MemberUserExcelRow>();
            for (UserListRow row : rows) {
                MemberUserExcelRow excel = new MemberUserExcelRow();
                excel.setId(row.getId());
                excel.setNickname(row.getNickname());
                excel.setPhone(PhoneMasker.mask(row.getPhone()));
                excel.setMemberStatus(row.getIsMember() != null && row.getIsMember() == 1 ? "生效中" : "已失效");
                excel.setMemberExpireAt(DateTimes.format(row.getMemberExpireAt()));
                memberExcelRows.add(excel);
            }
            FastExcel.write(response.getOutputStream(), MemberUserExcelRow.class)
                    .sheet("会员用户列表")
                    .doWrite(memberExcelRows);
            return;
        }
        List<UserExcelRow> excelRows = new ArrayList<UserExcelRow>();
        for (UserListRow row : rows) {
            UserExcelRow excel = new UserExcelRow();
@@ -275,6 +327,18 @@
        return trimmed.isEmpty() ? null : trimmed;
    }
    private String normalizeMemberStatus(String memberStatus) {
        String normalized = trimToNull(memberStatus);
        if (normalized == null) {
            return null;
        }
        String upper = normalized.toUpperCase();
        if (MEMBER_STATUS_ALL.equals(upper) || MEMBER_STATUS_ACTIVE.equals(upper) || MEMBER_STATUS_EXPIRED.equals(upper)) {
            return upper;
        }
        throw new IllegalArgumentException("会员状态筛选不合法");
    }
    private String typeText(Integer type) {
        return type != null && type == 1 ? "诊所" : "个人";
    }