package com.tourguide.modules.user.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.tourguide.common.exception.BusinessException; import com.tourguide.modules.system.service.AdminPermissionService; import com.tourguide.modules.user.domain.ClientType; import com.tourguide.modules.user.domain.UserRole; import com.tourguide.modules.user.dto.AuthResponse; import com.tourguide.modules.user.dto.GuideUserResponse; import com.tourguide.modules.user.dto.LoginRequest; import com.tourguide.modules.user.dto.MiniRegisterRequest; import com.tourguide.modules.user.dto.MiniUserResponse; import com.tourguide.modules.user.dto.UserResponse; import com.tourguide.modules.user.entity.AdminUser; import com.tourguide.modules.user.entity.GuideUser; import com.tourguide.modules.user.entity.MiniUser; import com.tourguide.modules.user.mapper.MiniUserMapper; import com.tourguide.security.JwtTokenService; import com.tourguide.security.TokenPayload; import com.tourguide.security.TokenSessionService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.UUID; /** 统一编排登录流程,但后台账号与小程序账号始终从不同数据表读取。 */ @Service public class AuthService { public static final int GUIDE_ACCOUNT_DISABLED_CODE = 40301; private final AdminUserService adminUserService; private final AdminPermissionService adminPermissionService; private final MiniUserService miniUserService; private final GuideUserService guideUserService; private final GuideAutoDisableService guideAutoDisableService; private final MiniUserMapper miniUserMapper; private final PasswordEncoder passwordEncoder; private final JwtTokenService jwtTokenService; private final TokenSessionService tokenSessionService; public AuthService(AdminUserService adminUserService, AdminPermissionService adminPermissionService, MiniUserService miniUserService, GuideUserService guideUserService, GuideAutoDisableService guideAutoDisableService, MiniUserMapper miniUserMapper, PasswordEncoder passwordEncoder, JwtTokenService jwtTokenService, TokenSessionService tokenSessionService) { this.adminUserService = adminUserService; this.adminPermissionService = adminPermissionService; this.miniUserService = miniUserService; this.guideUserService = guideUserService; this.guideAutoDisableService = guideAutoDisableService; this.miniUserMapper = miniUserMapper; this.passwordEncoder = passwordEncoder; this.jwtTokenService = jwtTokenService; this.tokenSessionService = tokenSessionService; } public AuthResponse loginAdmin(LoginRequest request) { AdminUser user = adminUserService.findByUsername(request.getUsername()); if (user == null || user.getStatus() == null || user.getStatus() != 1 || !passwordEncoder.matches(request.getPassword(), user.getPassword())) { throw new BusinessException(401, "用户名或密码错误"); } List menuCodes = adminPermissionService.getMenuCodes(user.getRoleCode()); String token = issueToken(user.getId(), user.getUsername(), user.getRoleCode(), ClientType.ADMIN); return new AuthResponse(token, jwtTokenService.getExpirationSeconds(), UserResponse.from(user, menuCodes)); } public AuthResponse loginMini(LoginRequest request) { MiniUser user = miniUserService.findByUsername(request.getUsername()); if (user == null || user.getStatus() == null || user.getStatus() != 1 || !passwordEncoder.matches(request.getPassword(), user.getPassword())) { throw new BusinessException(401, "用户名或密码错误"); } String token = issueToken(user.getId(), user.getUsername(), UserRole.MINI_USER.name(), ClientType.MINI); return new AuthResponse(token, jwtTokenService.getExpirationSeconds(), MiniUserResponse.from(user)); } @Transactional public AuthResponse loginMiniWechat(String openid, String mobile) { MiniUser user = miniUserMapper.selectOne(new LambdaQueryWrapper() .eq(MiniUser::getOpenid, openid) .last("LIMIT 1")); if (user == null) { user = new MiniUser(); user.setUsername(buildWechatUsername(openid)); user.setPassword(passwordEncoder.encode(UUID.randomUUID().toString())); user.setNickname("微信用户"); user.setMobile(mobile); user.setOpenid(openid); user.setStatus(1); user.setDeleted(0); miniUserMapper.insert(user); } else { if (user.getStatus() == null || user.getStatus() != 1) { throw new BusinessException(401, "小程序用户已停用"); } if (mobile != null && !mobile.equals(user.getMobile())) { user.setMobile(mobile); miniUserMapper.updateById(user); } } String token = issueToken(user.getId(), user.getUsername(), UserRole.MINI_USER.name(), ClientType.MINI); return new AuthResponse(token, jwtTokenService.getExpirationSeconds(), MiniUserResponse.from(user)); } public AuthResponse loginGuide(LoginRequest request) { GuideUser user = guideUserService.findByUsername(request.getUsername()); validateGuideLoginStatus(user, "用户名不存在"); if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { throw new BusinessException(401, "用户名或密码错误"); } guideAutoDisableService.disableGuideIfEligible(user.getId()); GuideUser currentUser = guideUserService.findByUsername(request.getUsername()); validateGuideLoginStatus(currentUser, "用户名不存在"); return issueGuideToken(currentUser); } public AuthResponse loginGuideByPhone(String phone) { return issueGuideToken(findLoginableGuideByPhone(phone)); } public void ensureGuidePhoneCanLogin(String phone) { findLoginableGuideByPhone(phone); } private GuideUser findLoginableGuideByPhone(String phone) { GuideUser user = guideUserService.findByPhone(phone); validateGuideLoginStatus(user, "手机号未注册"); guideAutoDisableService.disableGuideIfEligible(user.getId()); GuideUser currentUser = guideUserService.findByPhone(phone); validateGuideLoginStatus(currentUser, "手机号未注册"); return currentUser; } private void validateGuideLoginStatus(GuideUser user, String notFoundMessage) { if (user == null) { throw new BusinessException(401, notFoundMessage); } if (Integer.valueOf(GuideUserService.STATUS_DISABLED).equals(user.getStatus())) { throw new BusinessException(GUIDE_ACCOUNT_DISABLED_CODE, "账号已被禁用,请联系后台工作人员"); } if (Integer.valueOf(GuideUserService.STATUS_CANCELLED).equals(user.getStatus())) { throw new BusinessException(403, "账号已注销,无法登录"); } if (!Integer.valueOf(GuideUserService.STATUS_ENABLED).equals(user.getStatus()) && !Integer.valueOf(GuideUserService.STATUS_PENDING).equals(user.getStatus())) { throw new BusinessException(403, "账号状态异常,请联系后台工作人员"); } } private AuthResponse issueGuideToken(GuideUser user) { String token = issueToken(user.getId(), user.getUsername(), UserRole.GUIDE_USER.name(), ClientType.GUIDE); return new AuthResponse(token, jwtTokenService.getExpirationSeconds(), GuideUserResponse.from(user)); } @Transactional public AuthResponse registerMini(MiniRegisterRequest request) { if (miniUserMapper.selectCount(new LambdaQueryWrapper().eq(MiniUser::getUsername, request.getUsername())) > 0) { throw new BusinessException(409, "小程序用户名已存在"); } MiniUser user = new MiniUser(); user.setUsername(request.getUsername()); user.setPassword(passwordEncoder.encode(request.getPassword())); user.setNickname(request.getNickname()); user.setMobile(request.getMobile()); user.setStatus(1); user.setDeleted(0); miniUserMapper.insert(user); String token = issueToken(user.getId(), user.getUsername(), UserRole.MINI_USER.name(), ClientType.MINI); return new AuthResponse(token, jwtTokenService.getExpirationSeconds(), MiniUserResponse.from(user)); } public void logout(String token, ClientType expectedClientType) { TokenPayload payload = jwtTokenService.parse(token); if (payload.getClientType() != expectedClientType) { throw new BusinessException(401, "Token端类型不匹配"); } tokenSessionService.remove(payload); } private String issueToken(Long userId, String username, String authority, ClientType clientType) { String token = jwtTokenService.createToken(userId, username, authority, clientType); tokenSessionService.save(jwtTokenService.parse(token), jwtTokenService.getExpirationSeconds()); return token; } private String buildWechatUsername(String openid) { return "wx_" + UUID.nameUUIDFromBytes(openid.getBytes(StandardCharsets.UTF_8)) .toString().replace("-", "").substring(0, 29); } }