package com.gxwebsoft.auto.service.impl; import cn.hutool.core.lang.UUID; import cn.hutool.core.util.StrUtil; import com.gxwebsoft.auto.dto.*; import com.gxwebsoft.auto.service.QrLoginService; import com.gxwebsoft.common.core.security.JwtSubject; import com.gxwebsoft.common.core.security.JwtUtil; import com.gxwebsoft.common.core.utils.JSONUtil; import com.gxwebsoft.common.core.utils.RedisUtil; import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.concurrent.TimeUnit; import static com.gxwebsoft.common.core.constants.RedisConstants.*; /** * 扫码登录服务实现 * * @author 科技小王子 * @since 2025-08-31 */ @Slf4j @Service public class QrLoginServiceImpl implements QrLoginService { @Autowired private RedisUtil redisUtil; @Autowired private UserService userService; @Value("${config.jwt.secret:websoft-jwt-secret-key-2025}") private String jwtSecret; @Value("${config.jwt.expire:86400}") private Long jwtExpire; @Override public QrLoginGenerateResponse generateQrLoginToken() { // 生成唯一的扫码登录token String token = UUID.randomUUID().toString(true); // 创建扫码登录数据 QrLoginData qrLoginData = new QrLoginData(); qrLoginData.setToken(token); qrLoginData.setStatus(QR_LOGIN_STATUS_PENDING); qrLoginData.setCreateTime(LocalDateTime.now()); qrLoginData.setExpireTime(LocalDateTime.now().plusSeconds(QR_LOGIN_TOKEN_TTL)); // 存储到Redis,设置过期时间 String redisKey = QR_LOGIN_TOKEN_KEY + token; redisUtil.set(redisKey, qrLoginData, QR_LOGIN_TOKEN_TTL, TimeUnit.SECONDS); log.info("生成扫码登录token: {}", token); // 构造二维码内容(这里可以是前端登录页面的URL + token参数) String qrCodeContent = "qr-login:" + token; return new QrLoginGenerateResponse(token, qrCodeContent, QR_LOGIN_TOKEN_TTL); } @Override public QrLoginStatusResponse checkQrLoginStatus(String token) { if (StrUtil.isBlank(token)) { return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L); } String redisKey = QR_LOGIN_TOKEN_KEY + token; QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class); if (qrLoginData == null) { return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L); } // 检查是否过期 if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) { // 删除过期的token redisUtil.delete(redisKey); return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L); } // 计算剩余过期时间 long expiresIn = ChronoUnit.SECONDS.between(LocalDateTime.now(), qrLoginData.getExpireTime()); QrLoginStatusResponse response = new QrLoginStatusResponse(); response.setStatus(qrLoginData.getStatus()); response.setExpiresIn(expiresIn); // 如果已确认,返回token和用户信息 if (QR_LOGIN_STATUS_CONFIRMED.equals(qrLoginData.getStatus())) { response.setAccessToken(qrLoginData.getAccessToken()); // 获取用户信息 if (qrLoginData.getUserId() != null) { User user = userService.getByIdRel(qrLoginData.getUserId()); if (user != null) { // 清除敏感信息 user.setPassword(null); response.setUserInfo(user); } } // 确认后删除token,防止重复使用 redisUtil.delete(redisKey); } return response; } @Override public QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request) { String token = request.getToken(); Integer userId = request.getUserId(); String platform = request.getPlatform(); if (StrUtil.isBlank(token) || userId == null) { throw new RuntimeException("参数不能为空"); } String redisKey = QR_LOGIN_TOKEN_KEY + token; QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class); if (qrLoginData == null) { throw new RuntimeException("扫码登录token不存在或已过期"); } // 检查是否过期 if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) { redisUtil.delete(redisKey); throw new RuntimeException("扫码登录token已过期"); } // 获取用户信息 User user = userService.getByIdRel(userId); if (user == null) { throw new RuntimeException("用户不存在"); } // 检查用户状态 if (user.getStatus() != null && user.getStatus() != 0) { throw new RuntimeException("用户已被冻结"); } // 如果是微信小程序登录,处理微信相关信息 if ("miniprogram".equals(platform) && request.getWechatInfo() != null) { handleWechatMiniProgramLogin(user, request.getWechatInfo()); } // 生成JWT token JwtSubject jwtSubject = new JwtSubject(user.getUsername(), user.getTenantId()); String accessToken = JwtUtil.buildToken(jwtSubject, jwtExpire, jwtSecret); // 更新扫码登录数据 qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED); qrLoginData.setUserId(userId); qrLoginData.setUsername(user.getUsername()); qrLoginData.setAccessToken(accessToken); // 更新Redis中的数据 redisUtil.set(redisKey, qrLoginData, 60L, TimeUnit.SECONDS); // 给前端60秒时间获取token log.info("用户 {} 通过 {} 平台确认扫码登录,token: {}", user.getUsername(), platform != null ? platform : "unknown", token); // 清除敏感信息 user.setPassword(null); return new QrLoginStatusResponse(QR_LOGIN_STATUS_CONFIRMED, accessToken, user, 60L); } /** * 处理微信小程序登录相关逻辑 */ private void handleWechatMiniProgramLogin(User user, QrLoginConfirmRequest.WechatMiniProgramInfo wechatInfo) { // 更新用户的微信信息 if (StrUtil.isNotBlank(wechatInfo.getOpenid())) { user.setOpenid(wechatInfo.getOpenid()); } if (StrUtil.isNotBlank(wechatInfo.getUnionid())) { user.setUnionid(wechatInfo.getUnionid()); } if (StrUtil.isNotBlank(wechatInfo.getNickname()) && StrUtil.isBlank(user.getNickname())) { user.setNickname(wechatInfo.getNickname()); } if (StrUtil.isNotBlank(wechatInfo.getAvatar()) && StrUtil.isBlank(user.getAvatar())) { user.setAvatar(wechatInfo.getAvatar()); } // 更新用户信息到数据库 try { userService.updateById(user); log.info("更新用户 {} 的微信小程序信息成功", user.getUsername()); } catch (Exception e) { log.warn("更新用户 {} 的微信小程序信息失败: {}", user.getUsername(), e.getMessage()); } } @Override public boolean scanQrCode(String token) { if (StrUtil.isBlank(token)) { return false; } String redisKey = QR_LOGIN_TOKEN_KEY + token; QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class); if (qrLoginData == null) { return false; } // 检查是否过期 if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) { redisUtil.delete(redisKey); return false; } // 只有pending状态才能更新为scanned if (QR_LOGIN_STATUS_PENDING.equals(qrLoginData.getStatus())) { qrLoginData.setStatus(QR_LOGIN_STATUS_SCANNED); // 计算剩余过期时间 long remainingSeconds = ChronoUnit.SECONDS.between(LocalDateTime.now(), qrLoginData.getExpireTime()); redisUtil.set(redisKey, qrLoginData, remainingSeconds, TimeUnit.SECONDS); log.info("扫码登录token {} 状态更新为已扫码", token); return true; } return false; } }