package com.sinata.appointment.service; 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.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 static final String H5_PHONE_OPEN_ID_PREFIX = "H5_PHONE_"; 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(); String phoneRaw = wechatMiniGateway.getPhoneNumber(req.getPhoneCode()); String phone = PhoneNormalize.mainland(phoneRaw); MpUser user = mpUserMapper.selectOne( new LambdaQueryWrapper() .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 (user == null) { throw new BizException(BizErrorCode.MINI_STAFF_NOT_BOUND, "员工未登记,请联系管理员开通后再登录"); } 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); } 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) 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); } else { 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); } /** * 管理端预建档员工(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 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()) { user.setMobile(phoneRaw); needUpdate = true; } if (needUpdate) { 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 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); 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; } 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()) .displayName(name) .build(); } }