refactor(auto): 优化扫码登录逻辑与状态管理

- 引入统一的过期时间解析和剩余秒数计算方法,提升代码复用性
- 增加扫码登录状态刷新时对用户手机号绑定状态的处理逻辑
- 补充扫码登录状态存储流程,新增持久化方法支持过期时间自动更新
- 完善扫码登录完成流程,支持手机号未绑定时提示绑定操作
- 调整扫码登录相关日志输出,增强异常捕获与日志记录
- 移除冗余的字符串时间解析,改用统一的Date对象处理
- WxOfficialController 中新增构建 JWT 访问令牌的私有方法,简化代码结构
This commit is contained in:
2026-04-06 23:11:09 +08:00
parent 8a1b729e91
commit 4ff46dbefe
5 changed files with 132 additions and 44 deletions

View File

File diff suppressed because one or more lines are too long

View File

@@ -26,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.File; import java.io.File;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -119,21 +120,38 @@ public class QrLoginServiceImpl implements QrLoginService {
return buildExpiredResponse(); return buildExpiredResponse();
} }
if (StrUtil.isBlank(qrLoginData.getExpireTime()) || DateUtil.date().after(DateUtil.parseDateTime(qrLoginData.getExpireTime()))) { Date expireAt = parseExpireTime(qrLoginData.getExpireTime());
if (expireAt == null || DateUtil.date().after(expireAt)) {
redisUtil.delete(redisKey); redisUtil.delete(redisKey);
return buildExpiredResponse(); return buildExpiredResponse();
} }
long expiresIn = Math.max(0L, long expiresIn = calculateExpiresIn(expireAt);
(DateUtil.parseDateTime(qrLoginData.getExpireTime()).getTime() - DateUtil.date().getTime()) / 1000);
if (QR_LOGIN_STATUS_CONFIRMED.equals(qrLoginData.getStatus()) if (QR_LOGIN_STATUS_CONFIRMED.equals(qrLoginData.getStatus())
&& StrUtil.isBlank(qrLoginData.getAccessToken()) && StrUtil.isBlank(qrLoginData.getAccessToken())
&& qrLoginData.getUserId() != null) { && qrLoginData.getUserId() != null) {
User user = userService.getAllByUserId(String.valueOf(qrLoginData.getUserId())); try {
if (user != null) { User user = userService.getAllByUserId(String.valueOf(qrLoginData.getUserId()));
qrLoginData.setAccessToken(buildAccessToken(user)); if (user != null) {
redisUtil.set(redisKey, qrLoginData, Math.max(expiresIn, 120L), TimeUnit.SECONDS); qrLoginData.setUsername(user.getUsername());
if (StrUtil.isBlank(user.getPhone())) {
qrLoginData.setStatus(QR_LOGIN_STATUS_BIND_PHONE);
qrLoginData.setNeedBindPhone(true);
qrLoginData.setAccessToken(null);
qrLoginData.setMessage("请先绑定手机号完成登录");
} else {
qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED);
qrLoginData.setNeedBindPhone(false);
qrLoginData.setAccessToken(buildAccessToken(user));
qrLoginData.setMessage(StrUtil.blankToDefault(qrLoginData.getMessage(), "登录成功"));
}
long refreshedTtl = Math.max(expiresIn, 120L);
persistQrLoginData(redisKey, qrLoginData, refreshedTtl, true);
expiresIn = refreshedTtl;
}
} catch (Exception e) {
log.error("补全扫码登录状态失败token={}", token, e);
} }
} }
@@ -155,7 +173,8 @@ public class QrLoginServiceImpl implements QrLoginService {
throw new RuntimeException("扫码登录token不存在或已过期"); throw new RuntimeException("扫码登录token不存在或已过期");
} }
if (StrUtil.isBlank(qrLoginData.getExpireTime()) || DateUtil.date().after(DateUtil.parseDateTime(qrLoginData.getExpireTime()))) { Date expireAt = parseExpireTime(qrLoginData.getExpireTime());
if (expireAt == null || DateUtil.date().after(expireAt)) {
redisUtil.delete(redisKey); redisUtil.delete(redisKey);
throw new RuntimeException("扫码登录token已过期"); throw new RuntimeException("扫码登录token已过期");
} }
@@ -176,7 +195,7 @@ public class QrLoginServiceImpl implements QrLoginService {
qrLoginData.setTenantId(user.getTenantId()); qrLoginData.setTenantId(user.getTenantId());
qrLoginData.setNeedBindPhone(false); qrLoginData.setNeedBindPhone(false);
qrLoginData.setMessage("登录成功"); qrLoginData.setMessage("登录成功");
redisUtil.set(redisKey, qrLoginData, 120L, TimeUnit.SECONDS); persistQrLoginData(redisKey, qrLoginData, 120L, true);
log.info("用户 {} 确认扫码登录token: {}", user.getUsername(), token); log.info("用户 {} 确认扫码登录token: {}", user.getUsername(), token);
return buildStatusResponse(qrLoginData, 120L); return buildStatusResponse(qrLoginData, 120L);
@@ -194,7 +213,8 @@ public class QrLoginServiceImpl implements QrLoginService {
return false; return false;
} }
if (StrUtil.isBlank(qrLoginData.getExpireTime()) || DateUtil.date().after(DateUtil.parseDateTime(qrLoginData.getExpireTime()))) { Date expireAt = parseExpireTime(qrLoginData.getExpireTime());
if (expireAt == null || DateUtil.date().after(expireAt)) {
redisUtil.delete(redisKey); redisUtil.delete(redisKey);
return false; return false;
} }
@@ -203,7 +223,7 @@ public class QrLoginServiceImpl implements QrLoginService {
qrLoginData.setStatus(QR_LOGIN_STATUS_SCANNED); qrLoginData.setStatus(QR_LOGIN_STATUS_SCANNED);
qrLoginData.setMessage("已识别扫码,等待公众号回调"); qrLoginData.setMessage("已识别扫码,等待公众号回调");
long remainingSeconds = Math.max(1L, long remainingSeconds = Math.max(1L,
(DateUtil.parseDateTime(qrLoginData.getExpireTime()).getTime() - DateUtil.date().getTime()) / 1000); (expireAt.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);
return true; return true;
@@ -226,7 +246,8 @@ public class QrLoginServiceImpl implements QrLoginService {
if (qrLoginData == null) { if (qrLoginData == null) {
throw new RuntimeException("二维码已过期,请刷新后重试"); throw new RuntimeException("二维码已过期,请刷新后重试");
} }
if (StrUtil.isBlank(qrLoginData.getExpireTime()) || DateUtil.date().after(DateUtil.parseDateTime(qrLoginData.getExpireTime()))) { Date expireAt = parseExpireTime(qrLoginData.getExpireTime());
if (expireAt == null || DateUtil.date().after(expireAt)) {
redisUtil.delete(redisKey); redisUtil.delete(redisKey);
throw new RuntimeException("二维码已过期,请刷新后重试"); throw new RuntimeException("二维码已过期,请刷新后重试");
} }
@@ -278,7 +299,7 @@ public class QrLoginServiceImpl implements QrLoginService {
qrLoginData.setAccessToken(accessToken); qrLoginData.setAccessToken(accessToken);
qrLoginData.setNeedBindPhone(false); qrLoginData.setNeedBindPhone(false);
qrLoginData.setMessage("手机号绑定成功,正在登录"); qrLoginData.setMessage("手机号绑定成功,正在登录");
redisUtil.set(redisKey, qrLoginData, 120L, TimeUnit.SECONDS); persistQrLoginData(redisKey, qrLoginData, 120L, true);
return buildStatusResponse(qrLoginData, 120L); return buildStatusResponse(qrLoginData, 120L);
} }
@@ -378,6 +399,32 @@ public class QrLoginServiceImpl implements QrLoginService {
return JwtUtil.buildToken(jwtSubject, configProperties.getTokenExpireTime(), configProperties.getTokenKey()); return JwtUtil.buildToken(jwtSubject, configProperties.getTokenExpireTime(), configProperties.getTokenKey());
} }
private Date parseExpireTime(String expireTime) {
if (StrUtil.isBlank(expireTime)) {
return null;
}
try {
return DateUtil.parseDateTime(expireTime);
} catch (Exception e) {
log.warn("扫码登录 expireTime 解析失败: {}", expireTime, e);
return null;
}
}
private long calculateExpiresIn(Date expireAt) {
if (expireAt == null) {
return 0L;
}
return Math.max(0L, (expireAt.getTime() - DateUtil.date().getTime()) / 1000);
}
private void persistQrLoginData(String redisKey, QrLoginData qrLoginData, long ttlSeconds, boolean refreshExpireTime) {
if (refreshExpireTime) {
qrLoginData.setExpireTime(DateUtil.formatDateTime(DateUtil.offsetSecond(DateUtil.date(), (int) ttlSeconds)));
}
redisUtil.set(redisKey, qrLoginData, ttlSeconds, TimeUnit.SECONDS);
}
private QrLoginStatusResponse buildExpiredResponse() { private QrLoginStatusResponse buildExpiredResponse() {
QrLoginStatusResponse response = new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L, null); QrLoginStatusResponse response = new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L, null);
response.setNeedBindPhone(false); response.setNeedBindPhone(false);
@@ -396,10 +443,14 @@ public class QrLoginServiceImpl implements QrLoginService {
response.setMessage(qrLoginData.getMessage()); response.setMessage(qrLoginData.getMessage());
if (qrLoginData.getUserId() != null) { if (qrLoginData.getUserId() != null) {
User user = userService.getAllByUserId(String.valueOf(qrLoginData.getUserId())); try {
if (user != null) { User user = userService.getAllByUserId(String.valueOf(qrLoginData.getUserId()));
user.setPassword(null); if (user != null) {
response.setUserInfo(user); user.setPassword(null);
response.setUserInfo(user);
}
} catch (Exception e) {
log.error("构建扫码登录状态响应时查询用户失败userId={}", qrLoginData.getUserId(), e);
} }
} }
return response; return response;
@@ -417,7 +468,8 @@ public class QrLoginServiceImpl implements QrLoginService {
if (qrLoginData == null) { if (qrLoginData == null) {
return WechatScanResponse.notBound("二维码已过期,请刷新重试"); return WechatScanResponse.notBound("二维码已过期,请刷新重试");
} }
if (StrUtil.isBlank(qrLoginData.getExpireTime()) || DateUtil.date().after(DateUtil.parseDateTime(qrLoginData.getExpireTime()))) { Date expireAt = parseExpireTime(qrLoginData.getExpireTime());
if (expireAt == null || DateUtil.date().after(expireAt)) {
redisUtil.delete(redisKey); redisUtil.delete(redisKey);
return WechatScanResponse.notBound("二维码已过期,请刷新重试"); return WechatScanResponse.notBound("二维码已过期,请刷新重试");
} }
@@ -490,6 +542,18 @@ public class QrLoginServiceImpl implements QrLoginService {
return WechatScanResponse.notBound("账号已被冻结"); return WechatScanResponse.notBound("账号已被冻结");
} }
if (StrUtil.isBlank(user.getPhone())) {
qrLoginData.setStatus(QR_LOGIN_STATUS_BIND_PHONE);
qrLoginData.setUserId(user.getUserId());
qrLoginData.setUsername(user.getUsername());
qrLoginData.setTenantId(user.getTenantId());
qrLoginData.setAccessToken(null);
qrLoginData.setNeedBindPhone(true);
qrLoginData.setMessage("请先绑定手机号完成登录");
persistQrLoginData(redisKey, qrLoginData, 120L, true);
return WechatScanResponse.needBind("请先绑定手机号完成登录");
}
String accessToken = buildAccessToken(user); String accessToken = buildAccessToken(user);
qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED); qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED);
qrLoginData.setUserId(user.getUserId()); qrLoginData.setUserId(user.getUserId());
@@ -498,7 +562,7 @@ public class QrLoginServiceImpl implements QrLoginService {
qrLoginData.setTenantId(user.getTenantId()); qrLoginData.setTenantId(user.getTenantId());
qrLoginData.setNeedBindPhone(false); qrLoginData.setNeedBindPhone(false);
qrLoginData.setMessage("登录成功"); qrLoginData.setMessage("登录成功");
redisUtil.set(redisKey, qrLoginData, 120L, TimeUnit.SECONDS); persistQrLoginData(redisKey, qrLoginData, 120L, true);
user.setPassword(null); user.setPassword(null);
return WechatScanResponse.success(accessToken, user, user.getTenantId()); return WechatScanResponse.success(accessToken, user, user.getTenantId());

View File

@@ -6,6 +6,7 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import com.gxwebsoft.common.core.Constants; import com.gxwebsoft.common.core.Constants;
import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.utils.JSONUtil;
import com.gxwebsoft.common.system.entity.Role; import com.gxwebsoft.common.system.entity.Role;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;

View File

@@ -1,5 +1,6 @@
package com.gxwebsoft.common.system.controller; package com.gxwebsoft.common.system.controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.XmlUtil; import cn.hutool.core.util.XmlUtil;
@@ -95,6 +96,8 @@ public class WxOfficialController extends BaseController {
private WxService wxService; private WxService wxService;
@Resource @Resource
private RedisUtil redisUtil; private RedisUtil redisUtil;
@Resource
private ConfigProperties configProperties;
@Operation(summary = "验证微信服务器") @Operation(summary = "验证微信服务器")
@GetMapping("/{id}") @GetMapping("/{id}")
@@ -315,37 +318,51 @@ public class WxOfficialController extends BaseController {
*/ */
private void completeQrLogin(String token, Integer userId, Integer tenantId) { private void completeQrLogin(String token, Integer userId, Integer tenantId) {
try { try {
// 获取已有的扫码登录数据
String redisKey = "qr-login:token:" + token; String redisKey = "qr-login:token:" + token;
String existingData = redisUtil.get(redisKey); QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
if (qrLoginData == null) {
// 如果有现有数据,解析后更新 qrLoginData = new QrLoginData();
if (StrUtil.isNotBlank(existingData)) { qrLoginData.setToken(token);
JSONObject jsonData = JSONObject.parseObject(existingData); qrLoginData.setCreateTime(DateUtil.formatDateTime(DateUtil.date()));
jsonData.put("status", "confirmed");
jsonData.put("userId", userId);
jsonData.put("tenantId", tenantId);
jsonData.put("confirmTime", System.currentTimeMillis());
// 保存60秒给前端足够时间获取
redisUtil.set(redisKey, jsonData.toJSONString(), 60L, TimeUnit.SECONDS);
System.out.println("扫码登录完成token=" + token + ", userId=" + userId);
} else {
// 没有现有数据,创建一个新的
JSONObject qrLoginData = new JSONObject();
qrLoginData.put("token", token);
qrLoginData.put("status", "confirmed");
qrLoginData.put("userId", userId);
qrLoginData.put("tenantId", tenantId);
qrLoginData.put("confirmTime", System.currentTimeMillis());
// 保存60秒
redisUtil.set(redisKey, qrLoginData.toJSONString(), 60L, TimeUnit.SECONDS);
System.out.println("扫码登录完成新建token=" + token + ", userId=" + userId);
} }
User user = userService.getAllByUserId(String.valueOf(userId));
if (user == null) {
log.warn("扫码登录完成时未找到用户token={}, userId={}", token, userId);
return;
}
long ttlSeconds = 120L;
qrLoginData.setToken(token);
qrLoginData.setUserId(userId);
qrLoginData.setUsername(user.getUsername());
qrLoginData.setTenantId(user.getTenantId() != null ? user.getTenantId() : tenantId);
qrLoginData.setExpireTime(DateUtil.formatDateTime(DateUtil.offsetSecond(DateUtil.date(), (int) ttlSeconds)));
if (StrUtil.isBlank(user.getPhone())) {
qrLoginData.setStatus("bind_phone");
qrLoginData.setNeedBindPhone(true);
qrLoginData.setAccessToken(null);
qrLoginData.setMessage("请先绑定手机号完成登录");
} else {
qrLoginData.setStatus("confirmed");
qrLoginData.setNeedBindPhone(false);
qrLoginData.setAccessToken(buildAccessToken(user));
qrLoginData.setMessage("登录成功");
}
redisUtil.set(redisKey, qrLoginData, ttlSeconds, TimeUnit.SECONDS);
log.info("扫码登录状态已更新token={}, userId={}, status={}", token, userId, qrLoginData.getStatus());
} catch (Exception e) { } catch (Exception e) {
log.error("完成扫码登录失败: {}", e.getMessage()); log.error("完成扫码登录失败", e);
} }
} }
private String buildAccessToken(User user) {
JwtSubject jwtSubject = new JwtSubject(user.getUsername(), user.getTenantId());
return JwtUtil.buildToken(jwtSubject, configProperties.getTokenExpireTime(), configProperties.getTokenKey());
}
@Operation(summary = "生成微信扫码登录二维码") @Operation(summary = "生成微信扫码登录二维码")
@GetMapping("/qrcode/{token}") @GetMapping("/qrcode/{token}")
public ApiResult<?> generateQrCode(@PathVariable("token") String token) { public ApiResult<?> generateQrCode(@PathVariable("token") String token) {