feat(qr-login): 为微信小程序添加专用登录二维码
- 在 QrLoginGenerateResponse 中添加了 miniprogramPath 和 miniprogramQrCodeUrl 字段 - 在 QrLoginServiceImpl 中实现了微信小程序码的生成和返回 - 优化了原有二维码生成逻辑,增加了向后兼容的构造函数- 改进了时间处理方式,使用 DateUtil 替代 LocalDateTime- 重构了部分代码,提高了可维护性和扩展性
This commit is contained in:
@@ -20,10 +20,23 @@ public class QrLoginGenerateResponse {
|
|||||||
@Schema(description = "扫码登录token")
|
@Schema(description = "扫码登录token")
|
||||||
private String token;
|
private String token;
|
||||||
|
|
||||||
@Schema(description = "二维码内容")
|
@Schema(description = "二维码内容(APP扫码使用)")
|
||||||
private String qrCode;
|
private String qrCodeContent;
|
||||||
|
|
||||||
|
@Schema(description = "微信小程序页面路径")
|
||||||
|
private String miniprogramPath;
|
||||||
|
|
||||||
|
@Schema(description = "微信小程序码图片URL")
|
||||||
|
private String miniprogramQrCodeUrl;
|
||||||
|
|
||||||
@Schema(description = "过期时间(秒)")
|
@Schema(description = "过期时间(秒)")
|
||||||
private Long expiresIn;
|
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;
|
package com.gxwebsoft.auto.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.date.DateTime;
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import cn.hutool.core.io.FileUtil;
|
||||||
import cn.hutool.core.lang.UUID;
|
import cn.hutool.core.lang.UUID;
|
||||||
import cn.hutool.core.util.StrUtil;
|
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.dto.*;
|
||||||
import com.gxwebsoft.auto.service.QrLoginService;
|
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.JwtSubject;
|
||||||
import com.gxwebsoft.common.core.security.JwtUtil;
|
import com.gxwebsoft.common.core.security.JwtUtil;
|
||||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||||
import com.gxwebsoft.common.system.entity.User;
|
import com.gxwebsoft.common.system.entity.User;
|
||||||
|
import com.gxwebsoft.common.system.service.SettingService;
|
||||||
import com.gxwebsoft.common.system.service.UserService;
|
import com.gxwebsoft.common.system.service.UserService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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 org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.io.File;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.util.HashMap;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static com.gxwebsoft.common.core.constants.RedisConstants.*;
|
import static com.gxwebsoft.common.core.constants.RedisConstants.*;
|
||||||
@@ -38,11 +43,16 @@ public class QrLoginServiceImpl implements QrLoginService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
@Value("${config.token-key}")
|
@Autowired
|
||||||
private String tokenKey;
|
private ConfigProperties configProperties;
|
||||||
|
|
||||||
@Value("${config.token-expire-time:86400}")
|
@Autowired
|
||||||
private Long tokenExpireTime;
|
private SettingService settingService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisTemplate<String, String> redisTemplate;
|
||||||
|
|
||||||
|
private static final String ACCESS_TOKEN_KEY = "WX_ACCESS_TOKEN";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public QrLoginGenerateResponse generateQrLoginToken() {
|
public QrLoginGenerateResponse generateQrLoginToken() {
|
||||||
@@ -53,7 +63,7 @@ public class QrLoginServiceImpl implements QrLoginService {
|
|||||||
QrLoginData qrLoginData = new QrLoginData();
|
QrLoginData qrLoginData = new QrLoginData();
|
||||||
qrLoginData.setToken(token);
|
qrLoginData.setToken(token);
|
||||||
qrLoginData.setStatus(QR_LOGIN_STATUS_PENDING);
|
qrLoginData.setStatus(QR_LOGIN_STATUS_PENDING);
|
||||||
// 五分钟过期
|
qrLoginData.setCreateTime(DateUtil.date());
|
||||||
qrLoginData.setExpireTime(DateUtil.offsetSecond(DateUtil.date(), QR_LOGIN_TOKEN_TTL.intValue()));
|
qrLoginData.setExpireTime(DateUtil.offsetSecond(DateUtil.date(), QR_LOGIN_TOKEN_TTL.intValue()));
|
||||||
|
|
||||||
// 存储到Redis,设置过期时间
|
// 存储到Redis,设置过期时间
|
||||||
@@ -62,10 +72,27 @@ public class QrLoginServiceImpl implements QrLoginService {
|
|||||||
|
|
||||||
log.info("生成扫码登录token: {}", token);
|
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
|
@Override
|
||||||
@@ -82,15 +109,14 @@ public class QrLoginServiceImpl implements QrLoginService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否过期
|
// 检查是否过期
|
||||||
// 替换第85行代码为以下内容:
|
if (DateUtil.date().after(qrLoginData.getExpireTime())) {
|
||||||
if (LocalDateTime.now().isAfter(DateUtil.toLocalDateTime(qrLoginData.getExpireTime()))) {
|
|
||||||
// 删除过期的token
|
// 删除过期的token
|
||||||
redisUtil.delete(redisKey);
|
redisUtil.delete(redisKey);
|
||||||
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
|
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();
|
QrLoginStatusResponse response = new QrLoginStatusResponse();
|
||||||
response.setStatus(qrLoginData.getStatus());
|
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);
|
redisUtil.delete(redisKey);
|
||||||
throw new RuntimeException("扫码登录token已过期");
|
throw new RuntimeException("扫码登录token已过期");
|
||||||
}
|
}
|
||||||
@@ -152,7 +178,7 @@ public class QrLoginServiceImpl implements QrLoginService {
|
|||||||
|
|
||||||
// 生成JWT token
|
// 生成JWT token
|
||||||
JwtSubject jwtSubject = new JwtSubject(user.getUsername(), user.getTenantId());
|
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);
|
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);
|
redisUtil.delete(redisKey);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -195,7 +221,7 @@ public class QrLoginServiceImpl implements QrLoginService {
|
|||||||
qrLoginData.setStatus(QR_LOGIN_STATUS_SCANNED);
|
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);
|
redisUtil.set(redisKey, qrLoginData, remainingSeconds, TimeUnit.SECONDS);
|
||||||
|
|
||||||
log.info("扫码登录token {} 状态更新为已扫码", token);
|
log.info("扫码登录token {} 状态更新为已扫码", token);
|
||||||
@@ -204,4 +230,124 @@ public class QrLoginServiceImpl implements QrLoginService {
|
|||||||
|
|
||||||
return false;
|
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