package com.yupi.springbootinit.service.impl; import cn.dev33.satoken.stp.StpUtil; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.date.DateUtil; import com.yupi.springbootinit.common.ErrorCode; import com.yupi.springbootinit.exception.BusinessException; import com.yupi.springbootinit.mapper.SystemTenantMapper; import com.yupi.springbootinit.model.dto.user.SystemUsersDTO; import com.yupi.springbootinit.model.entity.SystemTenant; import com.yupi.springbootinit.model.entity.SystemUsers; import com.yupi.springbootinit.model.enums.CommonStatusEnum; import com.yupi.springbootinit.model.enums.LoginSceneEnum; import com.yupi.springbootinit.model.vo.user.SystemUsersVO; import com.yupi.springbootinit.service.SystemUsersService; import com.yupi.springbootinit.utils.DateUtils; import com.yupi.springbootinit.utils.RedisUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.*; import org.springframework.amqp.rabbit.core.RabbitAdmin; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.time.LocalDate; import java.time.ZoneId; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; /** * 登录相关业务实现类 */ @Slf4j @Service @RequiredArgsConstructor public class LoginService { /** 用户业务服务 */ private final SystemUsersService usersService; /** 用于缓存 AI 登录状态的 RedisTemplate(布尔值) */ @Resource private RedisTemplate redisTemplate; /** 已创建过 RabbitMQ 队列的租户 ID 集合,防止重复创建 */ private final Set created = ConcurrentHashMap.newKeySet(); /** 用户事件使用的 HeadersExchange */ private final HeadersExchange userHeadersExchange; /** RabbitMQ 管理组件 */ @Resource private RabbitAdmin rabbitAdmin; /** 通用 Redis 工具类 */ @Resource private RedisUtils redisUtils; @Resource private SystemTenantMapper tenantMapper; /** * 统一登录入口 * * @param scene 登录场景(HOST / BIG_BROTHER / AI_CHAT) * @param dto 登录参数(用户名、密码、租户 ID) * @return 登录成功后的用户信息 + Token */ public SystemUsersVO login(LoginSceneEnum scene, SystemUsersDTO dto) { // 1. 校验用户名、密码、状态、租户过期 SystemUsers user = validateUser(dto); // 2. 按场景校验角色权限 checkRole(scene, user.getId()); // 3. AI_CHAT 场景专属逻辑:缓存登录状态并动态创建 RabbitMQ 队列 if (scene.equals(LoginSceneEnum.AI_CHAT)) { // 记录该用户已登录 AI_CHAT redisTemplate.opsForValue().set("ai_login:" + user.getTenantId() + ":" + user.getId(), true); String queueName = "q.tenant." + user.getTenantId(); // 若该租户队列尚未创建,则创建队列并绑定到 HeadersExchange if (created.add(String.valueOf(user.getTenantId()))) { Queue queue = QueueBuilder.durable(queueName).build(); rabbitAdmin.declareQueue(queue); Map headers = Map.of("tenantId", user.getTenantId(), "x-match", "all"); Binding binding = BindingBuilder .bind(queue) .to(userHeadersExchange) .whereAll(headers) .match(); rabbitAdmin.declareBinding(binding); } } SystemTenant systemTenant = tenantMapper.selectById(user.getTenantId()); // 5. Sa-Token 登录 StpUtil.login(user.getId(), scene.getSaMode()); switch (scene) { case AI_CHAT: StpUtil.renewTimeout(DateUtils.dateBetween(systemTenant.getAiExpireTime(),DateUtil.date())); break; case HOST: StpUtil.renewTimeout(DateUtils.dateBetween(systemTenant.getExpireTime(),DateUtil.date())); break; case BIG_BROTHER: StpUtil.renewTimeout(DateUtils.dateBetween(systemTenant.getBrotherExpireTime(),DateUtil.date())); break; } // 6. 封装返回数据 SystemUsersVO vo = new SystemUsersVO(); BeanUtil.copyProperties(user, vo); vo.setTokenName(StpUtil.getTokenName()); vo.setTokenValue(StpUtil.getTokenValue()); return vo; } /** * 校验用户登录信息 * * @param dto 登录参数 * @return 校验通过的用户实体 * @throws BusinessException 校验失败时抛出 */ private SystemUsers validateUser(SystemUsersDTO dto) { SystemUsers user = usersService.getUserByUserName(dto.getUsername(), dto.getTenantId()); if (user == null) { throw new BusinessException(ErrorCode.USERNAME_OR_PASSWORD_ERROR); } if (!usersService.isPasswordMatch(dto.getPassword(), user.getPassword())) { throw new BusinessException(ErrorCode.USERNAME_OR_PASSWORD_ERROR); } if (CommonStatusEnum.isDisable(Integer.valueOf(user.getStatus()))) { throw new BusinessException(ErrorCode.USER_DISABLE); } if (usersService.isExpired(dto.getTenantId())) { throw new BusinessException(ErrorCode.PACKAGE_EXPIRED); } return user; } /** * 按登录场景校验角色权限 * * @param scene 登录场景 * @param userId 用户 ID * @throws BusinessException 无权限时抛出 */ private void checkRole(LoginSceneEnum scene, Long userId) { Boolean pass = switch (scene) { case HOST -> usersService.checkCrawlRole(userId); case BIG_BROTHER -> usersService.checkbigBrotherlRole(userId); case AI_CHAT -> usersService.checkAiCHatLoginRole(userId); }; if (!pass) { throw new BusinessException(ErrorCode.LOGIN_NOT_ALLOWED); } } /** * AI_CHAT 场景专属登出 * * @param usersDTO 包含租户 ID 与用户 ID * @return 固定返回 true */ public Boolean aiChatLogout(SystemUsersDTO usersDTO) { // 1. 删除 Redis 中该用户的 AI_CHAT 登录标记 Boolean delete = redisTemplate.delete("ai_login:" + usersDTO.getTenantId() + ":" + usersDTO.getUserId()); // 2. 使当前 Token 失效 String tokenValue = StpUtil.getTokenValue(); StpUtil.logoutByTokenValue(tokenValue); log.info("删除租户:{} 登录状态:{}", usersDTO.getTenantId(), delete); // 3. 若该租户下已无 AI_CHAT 在线用户,则删除队列 if (!redisUtils.hasKeyByPrefix("ai_login:" + usersDTO.getTenantId())) { created.remove(String.valueOf(usersDTO.getTenantId())); boolean b = rabbitAdmin.deleteQueue("q.tenant." + usersDTO.getTenantId()); log.info("删除租户:{} 队列删除状态:{}", usersDTO.getTenantId(), b); } return true; } /** * 通用登出(不区分场景) * * @return 固定返回 true */ public Boolean logout() { String tokenValue = StpUtil.getTokenValue(); Long loginId = (Long) StpUtil.getLoginId(); StpUtil.logoutByTokenValue(tokenValue); log.info("用户:{} 登出成功", loginId); return true; } }