longz
2026-04-27 25df88d0ab9d7444ed1d162b9efa1259ac2f6f66
backend/src/main/java/com/sinata/appointment/service/MiniAuthApplicationService.java
@@ -2,11 +2,11 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.sinata.appointment.constants.BizErrorCode;
import com.sinata.appointment.constants.MiniLoginIntent;
import com.sinata.appointment.constants.MiniUserType;
import com.sinata.appointment.constants.MpStaffConstants;
import com.sinata.appointment.config.MiniStpUtil;
import com.sinata.appointment.dto.MiniLoginResponse;
import com.sinata.appointment.dto.MiniProfileUpdateRequest;
import com.sinata.appointment.dto.MiniWxLoginRequest;
import com.sinata.appointment.entity.MpUser;
import com.sinata.appointment.entity.SysDept;
@@ -28,8 +28,6 @@
@Service
public class MiniAuthApplicationService {
    private static final String H5_PHONE_OPEN_ID_PREFIX = "H5_PHONE_";
    private final MpUserMapper mpUserMapper;
    private final SysDeptMapper sysDeptMapper;
    private final WechatMiniGateway wechatMiniGateway;
@@ -47,68 +45,24 @@
    public MiniLoginResponse loginByWx(MiniWxLoginRequest req) {
        WxCode2SessionResult session = wechatMiniGateway.code2Session(req.getWxCode());
        String openId = session.getOpenId();
        String phoneRaw = wechatMiniGateway.getPhoneNumber(req.getPhoneCode());
        String phone = PhoneNormalize.mainland(phoneRaw);
        MpUser user =
                mpUserMapper.selectOne(
                        new LambdaQueryWrapper<MpUser>()
                                .eq(MpUser::getOpenId, openId)
                                .eq(MpUser::getDelFlag, 0));
        if (MiniLoginIntent.EMPLOYEE.equals(req.getLoginIntent())) {
            if (user == null) {
                user = tryBindPreRegisteredEmployee(openId, session.getUnionId(), phone, phoneRaw);
            if (!StringUtils.hasText(req.getPhoneCode())) {
                throw new BizException(BizErrorCode.MINI_PHONE_AUTH_REQUIRED, "请先授权手机号完成登录");
            }
            if (user == null) {
                throw new BizException(BizErrorCode.MINI_STAFF_NOT_BOUND, "员工未登记,请联系管理员开通后再登录");
            String phoneRaw = wechatMiniGateway.getPhoneNumber(req.getPhoneCode());
            String phone = PhoneNormalize.mainland(phoneRaw);
            user = resolveUserByPhoneForWxLogin(openId, session.getUnionId(), phone, phoneRaw);
            }
            assertAccountEnabled(user);
            if (!MiniUserType.EMPLOYEE.equals(user.getUserType())) {
                throw new BizException(
                        BizErrorCode.MINI_LOGIN_INTENT_MISMATCH, "当前微信为访客身份,请使用访客入口登录");
            }
            if (user.getDeptId() == null) {
                throw new BizException(BizErrorCode.MINI_STAFF_NOT_BOUND, "员工未分配部门");
            }
            assertPhoneMatchesStored(user, phone);
            syncMpUserMobile(user, phoneRaw);
            if (user.getUnionId() == null && session.getUnionId() != null) {
                user.setUnionId(session.getUnionId());
                mpUserMapper.updateById(user);
            }
        assertUserCanLogin(user);
        syncUnionIdIfNeeded(user, session.getUnionId());
            MiniStpUtil.login(user.getId());
            return buildResponse(user);
        }
        if (MiniLoginIntent.VISITOR.equals(req.getLoginIntent())) {
            if (user == null) {
                MpUser created = new MpUser();
                created.setOpenId(openId);
                created.setUnionId(session.getUnionId());
                created.setUserType(MiniUserType.VISITOR);
                created.setMobile(phoneRaw);
                created.setStatus(1);
                mpUserMapper.insert(created);
                user = mpUserMapper.selectById(created.getId());
            } else {
                assertAccountEnabled(user);
                if (!MiniUserType.VISITOR.equals(user.getUserType())) {
                    throw new BizException(
                            BizErrorCode.MINI_LOGIN_INTENT_MISMATCH, "当前微信已登记为员工,请使用员工入口登录");
                }
                assertPhoneMatchesStored(user, phone);
                syncMpUserMobile(user, phoneRaw);
                if (user.getUnionId() == null && session.getUnionId() != null) {
                    user.setUnionId(session.getUnionId());
                    mpUserMapper.updateById(user);
                }
            }
            MiniStpUtil.login(user.getId());
            return buildResponse(user);
        }
        throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "loginIntent 无效");
    }
    @Transactional(rollbackFor = Exception.class)
@@ -130,15 +84,44 @@
                        .orElse(null);
        if (user == null) {
            user = registerVisitorForPhoneLogin(mobileRaw);
        } else {
            assertAccountEnabled(user);
        }
        assertUserCanLogin(user);
        MiniStpUtil.login(user.getId());
        return buildResponse(user);
    }
    private MpUser resolveUserByPhoneForWxLogin(
            String openId, String unionId, String normalizedPhone, String phoneRaw) {
        if (!normalizedPhone.matches("^\\d{11}$")) {
            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "手机号格式不正确");
        }
        List<MpUser> users =
                mpUserMapper.selectList(
                        new LambdaQueryWrapper<MpUser>()
                                .eq(MpUser::getDelFlag, 0));
        MpUser user =
                users.stream()
                        .filter(Objects::nonNull)
                        .filter(u -> normalizedPhone.equals(PhoneNormalize.mainland(u.getMobile())))
                        .sorted(this::compareByLoginPriority)
                        .findFirst()
                        .orElse(null);
        if (user == null) {
            MpUser created = new MpUser();
            created.setOpenId(openId);
            created.setUnionId(unionId);
            created.setUserType(MiniUserType.VISITOR);
            created.setMobile(phoneRaw);
            created.setStatus(1);
            mpUserMapper.insert(created);
            return mpUserMapper.selectById(created.getId());
        }
        assertAccountEnabled(user);
        if (MiniUserType.EMPLOYEE.equals(user.getUserType()) && user.getDeptId() == null) {
            throw new BizException(BizErrorCode.MINI_STAFF_NOT_BOUND, "员工未分配部门");
        }
        ensureOpenIdForPhoneLogin(user);
        MiniStpUtil.login(user.getId());
        return buildResponse(user);
        bindOpenIdIfNeeded(user, openId, unionId, phoneRaw);
        return mpUserMapper.selectById(user.getId());
    }
    /**
@@ -171,16 +154,6 @@
        return null;
    }
    private void assertPhoneMatchesStored(MpUser user, String normalizedPhone) {
        if (user.getMobile() == null || user.getMobile().isBlank()) {
            return;
        }
        String stored = PhoneNormalize.mainland(user.getMobile());
        if (!stored.isBlank() && !stored.equals(normalizedPhone)) {
            throw new BizException(BizErrorCode.MINI_PHONE_MISMATCH, "手机号与登记信息不一致");
        }
    }
    private void syncMpUserMobile(MpUser user, String phoneRaw) {
        boolean needUpdate = false;
        if (user.getMobile() == null || user.getMobile().isBlank()) {
@@ -190,6 +163,57 @@
        if (needUpdate) {
            mpUserMapper.updateById(user);
        }
    }
    private void bindOpenIdIfNeeded(MpUser user, String openId, String unionId, String phoneRaw) {
        if (user == null) {
            return;
        }
        String currentOpenId = user.getOpenId();
        if (StringUtils.hasText(currentOpenId)
                && !Objects.equals(currentOpenId, openId)
                && !MpStaffConstants.isPlaceholderOpenId(currentOpenId)) {
            throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "该手机号已绑定其他微信账号");
        }
        if (MiniUserType.EMPLOYEE.equals(user.getUserType())
                && MpStaffConstants.isPlaceholderOpenId(currentOpenId)) {
            MpUser bound = tryBindPreRegisteredEmployee(openId, unionId, PhoneNormalize.mainland(phoneRaw), phoneRaw);
            if (bound == null) {
                throw new BizException(BizErrorCode.MINI_STAFF_NOT_BOUND, "员工未登记,请联系管理员开通后再登录");
            }
            return;
        }
        boolean needUpdate = false;
        if (!Objects.equals(currentOpenId, openId)) {
            user.setOpenId(openId);
            needUpdate = true;
        }
        if (!StringUtils.hasText(user.getUnionId()) && StringUtils.hasText(unionId)) {
            user.setUnionId(unionId);
            needUpdate = true;
        }
        if (!StringUtils.hasText(user.getMobile()) && StringUtils.hasText(phoneRaw)) {
            user.setMobile(phoneRaw);
            needUpdate = true;
        }
        if (needUpdate) {
            mpUserMapper.updateById(user);
        }
    }
    private void assertUserCanLogin(MpUser user) {
        assertAccountEnabled(user);
        if (MiniUserType.EMPLOYEE.equals(user.getUserType()) && user.getDeptId() == null) {
            throw new BizException(BizErrorCode.MINI_STAFF_NOT_BOUND, "员工未分配部门");
        }
    }
    private void syncUnionIdIfNeeded(MpUser user, String unionId) {
        if (user == null || !StringUtils.hasText(unionId) || StringUtils.hasText(user.getUnionId())) {
            return;
        }
        user.setUnionId(unionId);
        mpUserMapper.updateById(user);
    }
    private void assertAccountEnabled(MpUser user) {
@@ -223,14 +247,6 @@
        return 2;
    }
    private void ensureOpenIdForPhoneLogin(MpUser user) {
        if (StringUtils.hasText(user.getOpenId())) {
            return;
        }
        user.setOpenId(H5_PHONE_OPEN_ID_PREFIX + user.getId());
        mpUserMapper.updateById(user);
    }
    private MpUser registerVisitorForPhoneLogin(String mobileRaw) {
        MpUser created = new MpUser();
        created.setUserType(MiniUserType.VISITOR);
@@ -248,6 +264,27 @@
        }
        assertAccountEnabled(u);
        return u;
    }
    @Transactional(rollbackFor = Exception.class)
    public MpUser updateProfile(MiniProfileUpdateRequest request) {
        MpUser user = currentMpUser();
        String nickName = trimRequired(request.getNickName(), "姓名不能为空");
        if (nickName.length() > 30) {
            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "姓名长度不能超过30个字符");
        }
        String gender = trimToNull(request.getGender());
        if (StringUtils.hasText(gender) && !"男".equals(gender) && !"女".equals(gender)) {
            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "性别仅支持男或女");
        }
        String companyName = trimToNull(request.getCompanyName());
        String avatarUrl = trimToNull(request.getAvatarUrl());
        user.setNickName(nickName);
        user.setGender(gender);
        user.setCompanyName(companyName);
        user.setAvatarUrl(avatarUrl);
        mpUserMapper.updateById(user);
        return mpUserMapper.selectById(user.getId());
    }
    public void logout() {
@@ -276,7 +313,24 @@
                .deptName(deptName)
                .deptLeader(leader)
                .avatarUrl(user.getAvatarUrl())
                .companyName(user.getCompanyName())
                .displayName(name)
                .build();
    }
    private static String trimToNull(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }
    private static String trimRequired(String value, String message) {
        String trimmed = trimToNull(value);
        if (!StringUtils.hasText(trimmed)) {
            throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, message);
        }
        return trimmed;
    }
}