diff --git a/src/main/java/com/gxwebsoft/auto/dto/QrLoginGenerateResponse.java b/src/main/java/com/gxwebsoft/auto/dto/QrLoginGenerateResponse.java index f0b69e5..5c487d1 100644 --- a/src/main/java/com/gxwebsoft/auto/dto/QrLoginGenerateResponse.java +++ b/src/main/java/com/gxwebsoft/auto/dto/QrLoginGenerateResponse.java @@ -20,10 +20,23 @@ public class QrLoginGenerateResponse { @Schema(description = "扫码登录token") private String token; - @Schema(description = "二维码内容") - private String qrCode; + @Schema(description = "二维码内容(APP扫码使用)") + private String qrCodeContent; + + @Schema(description = "微信小程序页面路径") + private String miniprogramPath; + + @Schema(description = "微信小程序码图片URL") + private String miniprogramQrCodeUrl; @Schema(description = "过期时间(秒)") private Long expiresIn; + // 保持向后兼容的构造函数 + public QrLoginGenerateResponse(String token, String qrCodeContent, Long expiresIn) { + this.token = token; + this.qrCodeContent = qrCodeContent; + this.expiresIn = expiresIn; + } + } diff --git a/src/main/java/com/gxwebsoft/auto/service/impl/QrLoginServiceImpl.java b/src/main/java/com/gxwebsoft/auto/service/impl/QrLoginServiceImpl.java index b361e5c..13f4cfe 100644 --- a/src/main/java/com/gxwebsoft/auto/service/impl/QrLoginServiceImpl.java +++ b/src/main/java/com/gxwebsoft/auto/service/impl/QrLoginServiceImpl.java @@ -1,23 +1,28 @@ package com.gxwebsoft.auto.service.impl; -import cn.hutool.core.date.DateTime; import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.UUID; import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; import com.gxwebsoft.auto.dto.*; import com.gxwebsoft.auto.service.QrLoginService; +import com.gxwebsoft.common.core.config.ConfigProperties; import com.gxwebsoft.common.core.security.JwtSubject; import com.gxwebsoft.common.core.security.JwtUtil; import com.gxwebsoft.common.core.utils.RedisUtil; import com.gxwebsoft.common.system.entity.User; +import com.gxwebsoft.common.system.service.SettingService; 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.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; -import java.time.LocalDateTime; -import java.time.temporal.ChronoUnit; +import java.io.File; +import java.util.HashMap; import java.util.concurrent.TimeUnit; import static com.gxwebsoft.common.core.constants.RedisConstants.*; @@ -38,11 +43,16 @@ public class QrLoginServiceImpl implements QrLoginService { @Autowired private UserService userService; - @Value("${config.token-key}") - private String tokenKey; + @Autowired + private ConfigProperties configProperties; - @Value("${config.token-expire-time:86400}") - private Long tokenExpireTime; + @Autowired + private SettingService settingService; + + @Autowired + private RedisTemplate redisTemplate; + + private static final String ACCESS_TOKEN_KEY = "WX_ACCESS_TOKEN"; @Override public QrLoginGenerateResponse generateQrLoginToken() { @@ -53,7 +63,7 @@ public class QrLoginServiceImpl implements QrLoginService { QrLoginData qrLoginData = new QrLoginData(); qrLoginData.setToken(token); qrLoginData.setStatus(QR_LOGIN_STATUS_PENDING); - // 五分钟过期 + qrLoginData.setCreateTime(DateUtil.date()); qrLoginData.setExpireTime(DateUtil.offsetSecond(DateUtil.date(), QR_LOGIN_TOKEN_TTL.intValue())); // 存储到Redis,设置过期时间 @@ -62,10 +72,27 @@ public class QrLoginServiceImpl implements QrLoginService { log.info("生成扫码登录token: {}", token); - // 构造二维码内容(这里可以是前端登录页面的URL + token参数) - String qrCodeContent = "qr-login:" + token; + // 构造响应对象 + QrLoginGenerateResponse response = new QrLoginGenerateResponse(); + response.setToken(token); + response.setExpiresIn(QR_LOGIN_TOKEN_TTL); - return new QrLoginGenerateResponse(token, qrCodeContent, QR_LOGIN_TOKEN_TTL); + // APP扫码内容 + response.setQrCodeContent("qr-login:" + token); + + // 微信小程序路径 + response.setMiniprogramPath("/pages/qr-login?token=" + token); + + // 生成微信小程序码 + try { + String miniprogramQrCodeUrl = generateMiniprogramQrCode(token); + response.setMiniprogramQrCodeUrl(miniprogramQrCodeUrl); + } catch (Exception e) { + log.warn("生成微信小程序码失败: {}", e.getMessage()); + // 小程序码生成失败不影响整体功能,继续返回其他信息 + } + + return response; } @Override @@ -82,15 +109,14 @@ public class QrLoginServiceImpl implements QrLoginService { } // 检查是否过期 - // 替换第85行代码为以下内容: - if (LocalDateTime.now().isAfter(DateUtil.toLocalDateTime(qrLoginData.getExpireTime()))) { + if (DateUtil.date().after(qrLoginData.getExpireTime())) { // 删除过期的token redisUtil.delete(redisKey); return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L); } // 计算剩余过期时间 - long expiresIn = ChronoUnit.SECONDS.between(LocalDateTime.now(), DateUtil.toLocalDateTime(qrLoginData.getExpireTime())); + long expiresIn = (qrLoginData.getExpireTime().getTime() - DateUtil.date().getTime()) / 1000; QrLoginStatusResponse response = new QrLoginStatusResponse(); response.setStatus(qrLoginData.getStatus()); @@ -134,7 +160,7 @@ public class QrLoginServiceImpl implements QrLoginService { } // 检查是否过期 - if (LocalDateTime.now().isAfter(DateUtil.toLocalDateTime(qrLoginData.getExpireTime()))) { + if (DateUtil.date().after(qrLoginData.getExpireTime())) { redisUtil.delete(redisKey); throw new RuntimeException("扫码登录token已过期"); } @@ -152,7 +178,7 @@ public class QrLoginServiceImpl implements QrLoginService { // 生成JWT token JwtSubject jwtSubject = new JwtSubject(user.getUsername(), user.getTenantId()); - String accessToken = JwtUtil.buildToken(jwtSubject, tokenExpireTime, tokenKey); + String accessToken = JwtUtil.buildToken(jwtSubject, configProperties.getTokenExpireTime(), configProperties.getTokenKey()); // 更新扫码登录数据 qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED); @@ -185,7 +211,7 @@ public class QrLoginServiceImpl implements QrLoginService { } // 检查是否过期 - if (LocalDateTime.now().isAfter(DateUtil.toLocalDateTime(qrLoginData.getExpireTime()))) { + if (DateUtil.date().after(qrLoginData.getExpireTime())) { redisUtil.delete(redisKey); return false; } @@ -195,7 +221,7 @@ public class QrLoginServiceImpl implements QrLoginService { qrLoginData.setStatus(QR_LOGIN_STATUS_SCANNED); // 计算剩余过期时间 - long remainingSeconds = ChronoUnit.SECONDS.between(LocalDateTime.now(), DateUtil.toLocalDateTime(qrLoginData.getExpireTime())); + long remainingSeconds = (qrLoginData.getExpireTime().getTime() - DateUtil.date().getTime()) / 1000; redisUtil.set(redisKey, qrLoginData, remainingSeconds, TimeUnit.SECONDS); log.info("扫码登录token {} 状态更新为已扫码", token); @@ -204,4 +230,124 @@ public class QrLoginServiceImpl implements QrLoginService { return false; } + + /** + * 生成微信小程序码 + */ + private String generateMiniprogramQrCode(String token) { + try { + String accessToken = getWxAccessToken(); + if (StrUtil.isBlank(accessToken)) { + throw new RuntimeException("获取微信AccessToken失败"); + } + + String apiUrl = "https://api.weixin.qq.com/wxa/getwxacode?access_token=" + accessToken; + HashMap params = new HashMap<>(); + params.put("path", "/pages/qr-login?token=" + token); + params.put("width", 430); // 二维码宽度,默认430px + + // 调用微信API生成小程序码 + byte[] qrCodeBytes = HttpRequest.post(apiUrl) + .body(JSON.toJSONString(params)) + .execute().bodyBytes(); + + // 保存文件 + String fileName = "qr-login-" + token + ".png"; + String uploadPath = getUploadPath(); + String filePath = uploadPath + "qrcode/" + fileName; + + // 确保目录存在 + File dir = new File(uploadPath + "qrcode/"); + if (!dir.exists()) { + dir.mkdirs(); + } + + File file = FileUtil.writeBytes(qrCodeBytes, filePath); + if (file != null && file.exists()) { + // 返回可访问的URL + return configProperties.getFileServer() + "/qrcode/" + fileName; + } else { + throw new RuntimeException("保存小程序码文件失败"); + } + } catch (Exception e) { + log.error("生成微信小程序码失败: {}", e.getMessage(), e); + throw new RuntimeException("生成微信小程序码失败: " + e.getMessage()); + } + } + + /** + * 获取微信AccessToken + */ + private String getWxAccessToken() { + try { + String key = ACCESS_TOKEN_KEY + ":10048"; // 默认租户ID,可以根据需要调整 + + // 从缓存获取 + String cachedToken = redisTemplate.opsForValue().get(key); + if (StrUtil.isNotBlank(cachedToken)) { + try { + JSONObject tokenData = JSON.parseObject(cachedToken); + String accessToken = tokenData.getString("access_token"); + if (StrUtil.isNotBlank(accessToken)) { + return accessToken; + } + } catch (Exception e) { + // 解析失败,可能是旧格式,直接使用 + if (!cachedToken.startsWith("{")) { + return cachedToken; + } + } + } + + // 缓存中没有,重新获取 + JSONObject setting = settingService.getBySettingKey("mp-weixin"); + if (setting == null) { + throw new RuntimeException("请先配置微信小程序"); + } + + String appId = setting.getString("appId"); + String appSecret = setting.getString("appSecret"); + if (StrUtil.isBlank(appId) || StrUtil.isBlank(appSecret)) { + throw new RuntimeException("微信小程序配置不完整"); + } + + // 调用微信API获取AccessToken + String apiUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + + appId + "&secret=" + appSecret; + String response = HttpRequest.get(apiUrl).execute().body(); + + JSONObject result = JSON.parseObject(response); + String accessToken = result.getString("access_token"); + if (StrUtil.isNotBlank(accessToken)) { + // 存入缓存 + JSONObject tokenData = new JSONObject(); + tokenData.put("access_token", accessToken); + tokenData.put("expires_in", result.get("expires_in")); + redisTemplate.opsForValue().set(key, tokenData.toJSONString(), 7000L, TimeUnit.SECONDS); + return accessToken; + } else { + throw new RuntimeException("获取AccessToken失败: " + response); + } + } catch (Exception e) { + log.error("获取微信AccessToken失败: {}", e.getMessage(), e); + return null; + } + } + + /** + * 获取文件上传路径 + */ + private String getUploadPath() { + String uploadPath = configProperties.getUploadPath(); + if (StrUtil.isBlank(uploadPath)) { + uploadPath = configProperties.getLocalUploadPath(); + } + if (StrUtil.isBlank(uploadPath)) { + uploadPath = "/tmp/uploads/"; + } + if (!uploadPath.endsWith("/")) { + uploadPath += "/"; + } + return uploadPath; + } }