feat(qr-login): 为微信小程序添加专用登录二维码
- 在 QrLoginGenerateResponse 中添加了 miniprogramPath 和 miniprogramQrCodeUrl 字段 - 在 QrLoginServiceImpl 中实现了微信小程序码的生成和返回 - 优化了原有二维码生成逻辑,增加了向后兼容的构造函数- 改进了时间处理方式,使用 DateUtil 替代 LocalDateTime- 重构了部分代码,提高了可维护性和扩展性
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String, String> 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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user