package com.sinata.appointment.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.sinata.appointment.constants.BizErrorCode; 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; import com.sinata.appointment.exception.BizException; import com.sinata.appointment.integration.WechatMiniGateway; import com.sinata.appointment.integration.WxCode2SessionResult; import com.sinata.appointment.mapper.SysDeptMapper; import com.sinata.appointment.mapper.MpUserMapper; import java.util.List; import java.util.Objects; import com.sinata.appointment.util.PhoneNormalize; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; /** * 小程序登录:js_code → openId;phoneCode + access_token → 手机号;访客/员工与平台 sys_user 无关联。 */ @Service public class MiniAuthApplicationService { private final MpUserMapper mpUserMapper; private final SysDeptMapper sysDeptMapper; private final WechatMiniGateway wechatMiniGateway; public MiniAuthApplicationService( MpUserMapper mpUserMapper, SysDeptMapper sysDeptMapper, WechatMiniGateway wechatMiniGateway) { this.mpUserMapper = mpUserMapper; this.sysDeptMapper = sysDeptMapper; this.wechatMiniGateway = wechatMiniGateway; } @Transactional(rollbackFor = Exception.class) public MiniLoginResponse loginByWx(MiniWxLoginRequest req) { WxCode2SessionResult session = wechatMiniGateway.code2Session(req.getWxCode()); String openId = session.getOpenId(); MpUser user = mpUserMapper.selectOne( new LambdaQueryWrapper() .eq(MpUser::getOpenId, openId) .eq(MpUser::getDelFlag, 0)); if (user == null) { if (!StringUtils.hasText(req.getPhoneCode())) { throw new BizException(BizErrorCode.MINI_PHONE_AUTH_REQUIRED, "请先授权手机号完成登录"); } String phoneRaw = wechatMiniGateway.getPhoneNumber(req.getPhoneCode()); String phone = PhoneNormalize.mainland(phoneRaw); user = resolveUserByPhoneForWxLogin(openId, session.getUnionId(), phone, phoneRaw); } assertUserCanLogin(user); syncUnionIdIfNeeded(user, session.getUnionId()); MiniStpUtil.login(user.getId()); return buildResponse(user); } @Transactional(rollbackFor = Exception.class) public MiniLoginResponse loginByPhone(String mobileRaw) { String phone = PhoneNormalize.mainland(mobileRaw); if (!phone.matches("^\\d{11}$")) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "手机号格式不正确"); } List users = mpUserMapper.selectList( new LambdaQueryWrapper() .eq(MpUser::getDelFlag, 0)); MpUser user = users.stream() .filter(Objects::nonNull) .filter(u -> phone.equals(PhoneNormalize.mainland(u.getMobile()))) .sorted(this::compareByLoginPriority) .findFirst() .orElse(null); if (user == null) { user = registerVisitorForPhoneLogin(mobileRaw); } 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 users = mpUserMapper.selectList( new LambdaQueryWrapper() .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, "员工未分配部门"); } bindOpenIdIfNeeded(user, openId, unionId, phoneRaw); return mpUserMapper.selectById(user.getId()); } /** * 管理端预建档员工(openId 为 PLHD_ 占位):按手机号绑定当前微信 openId。 */ private MpUser tryBindPreRegisteredEmployee( String openId, String unionId, String normalizedPhone, String phoneRaw) { List candidates = mpUserMapper.selectList( new LambdaQueryWrapper() .eq(MpUser::getUserType, MiniUserType.EMPLOYEE) .eq(MpUser::getDelFlag, 0) .likeRight(MpUser::getOpenId, MpStaffConstants.PLACEHOLDER_OPEN_ID_PREFIX)); for (MpUser u : candidates) { if (u.getOpenId() == null || !MpStaffConstants.isPlaceholderOpenId(u.getOpenId())) { continue; } String stored = PhoneNormalize.mainland(u.getMobile()); if (stored.isBlank() || !stored.equals(normalizedPhone)) { continue; } u.setOpenId(openId); if (unionId != null) { u.setUnionId(unionId); } syncMpUserMobile(u, phoneRaw); mpUserMapper.updateById(u); return mpUserMapper.selectById(u.getId()); } return null; } private void syncMpUserMobile(MpUser user, String phoneRaw) { boolean needUpdate = false; if (user.getMobile() == null || user.getMobile().isBlank()) { user.setMobile(phoneRaw); needUpdate = true; } 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) { if (user == null || user.getStatus() == null || user.getStatus() != 0) { return; } if (MiniUserType.EMPLOYEE.equals(user.getUserType())) { throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "员工账号已禁用"); } throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "账号已被禁用"); } private int compareByLoginPriority(MpUser a, MpUser b) { int pa = loginPriority(a); int pb = loginPriority(b); if (pa != pb) { return Integer.compare(pa, pb); } Long aid = a.getId() == null ? Long.MAX_VALUE : a.getId(); Long bid = b.getId() == null ? Long.MAX_VALUE : b.getId(); return Long.compare(aid, bid); } private int loginPriority(MpUser u) { if (MiniUserType.EMPLOYEE.equals(u.getUserType())) { return 0; } if (MiniUserType.VISITOR.equals(u.getUserType())) { return 1; } return 2; } private MpUser registerVisitorForPhoneLogin(String mobileRaw) { MpUser created = new MpUser(); created.setUserType(MiniUserType.VISITOR); created.setMobile(mobileRaw); created.setStatus(1); mpUserMapper.insert(created); return mpUserMapper.selectById(created.getId()); } public MpUser currentMpUser() { long id = MiniStpUtil.getLoginIdAsLong(); MpUser u = mpUserMapper.selectById(id); if (u == null || u.getDelFlag() != null && u.getDelFlag() == 1) { throw new BizException(BizErrorCode.AUTH_FORBIDDEN, "用户不存在或已禁用"); } 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() { MiniStpUtil.logout(); } private MiniLoginResponse buildResponse(MpUser user) { String name = user.getNickName() != null && !user.getNickName().isBlank() ? user.getNickName() : (MiniUserType.EMPLOYEE.equals(user.getUserType()) ? "员工" : "访客"); String deptName = null; if (MiniUserType.EMPLOYEE.equals(user.getUserType()) && user.getDeptId() != null) { SysDept dept = sysDeptMapper.selectById(user.getDeptId()); deptName = dept == null ? null : dept.getDeptName(); } Boolean leader = MiniUserType.EMPLOYEE.equals(user.getUserType()) ? Boolean.TRUE.equals(user.getDeptLeaderFlag()) : null; return MiniLoginResponse.builder() .token(MiniStpUtil.getTokenValue()) .mpUserId(user.getId()) .userType(user.getUserType()) .deptId(user.getDeptId()) .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; } }