1.添加 web 端扫码登录功能

This commit is contained in:
2025-08-05 21:15:01 +08:00
parent 17e07e3985
commit 65c41326f4
16 changed files with 311 additions and 10 deletions

Binary file not shown.

12
pom.xml
View File

@@ -140,12 +140,22 @@
<artifactId>hutool-all</artifactId>
<version>5.8.38</version>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
<!-- 二维码生成-->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.4.1</version>
</dependency>
</dependencies>

View File

@@ -0,0 +1,31 @@
package vvpkassistant.Tools;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.Hashtable;
public class QRCodeUtil {
public static String generateQRCode(String content, int width, int height)
throws WriterException, IOException {
Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
BitMatrix matrix = new MultiFormatWriter().encode(
content, BarcodeFormat.QR_CODE, width, height, hints);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(matrix, "PNG", outputStream);
return Base64.getEncoder().encodeToString(outputStream.toByteArray());
}
}

View File

@@ -9,9 +9,12 @@ import vvpkassistant.Data.ResponseData;
import vvpkassistant.Data.ResponseInfo;
import vvpkassistant.Data.WxChatParam;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.model.DTO.LoginInfoDTO;
import vvpkassistant.User.model.DTO.ScanInfoDTO;
import vvpkassistant.User.model.DTO.UserModelDTO;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.User.model.UserModelVO;
import vvpkassistant.User.model.enumeration.LoginStatusEnum;
import vvpkassistant.User.service.UserService;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.config.FunctionConfigHolder;
@@ -123,7 +126,10 @@ public class UserController {
// return ResponseData.error(ResponseInfo.ERROR,"创建聊天账号失败,请稍后再试");
// }
Map<String,Object> result = new HashMap<>();
result.put("info", tempModel);
StpUtil.login(tempModel.getId());
UserModelVO userModelVO = BeanUtil.copyProperties(tempModel, UserModelVO.class);
userModelVO.setToken(StpUtil.getTokenValue());
result.put("info", userModelVO);
result.put("newAccount",false);
//否则直接返回用户
return ResponseData.success(result);
@@ -149,7 +155,10 @@ public class UserController {
UserModel model = userDao.queryWithPhoneNumber(phoneNumber);
Map<String,Object> result = new HashMap<>();
if (model != null) { // 老用户
result.put("info",model);
UserModelVO userModelVO = BeanUtil.copyProperties(model, UserModelVO.class);
StpUtil.login(userModelVO.getId());
userModelVO.setToken(StpUtil.getTokenValue());
result.put("info",userModelVO);
result.put("newAccount", false);
result.put("chatInfo",wxChatParam);
return ResponseData.success(result);
@@ -197,7 +206,9 @@ public class UserController {
@PostMapping("getUserInfo")
public ResponseData<Object> getUserInfo(@RequestBody Map<String,Integer> map) {
UserModel userModel = userDao.selectById(map.get("id"));
return ResponseData.success(userModel);
UserModelVO userModelVO = BeanUtil.copyProperties(userModel, UserModelVO.class);
userModelVO.setHavaPassword(userModel.getPassword() != null);
return ResponseData.success(userModelVO);
}
// 查询用户所有pk数据
@@ -394,4 +405,27 @@ public class UserController {
public ResponseData<Object> verificationMail(@RequestParam("token") String token){
return ResponseData.success(userService.verificationMail(token));
}
@GetMapping("/qrcode")
public ResponseData<Object> generatedQrcode(){
return ResponseData.success(userService.generatedQrcode());
}
@GetMapping("/check/{uuid}")
public ResponseData<Object> checkQrcode(@PathVariable String uuid){
return ResponseData.success(userService.checkQrcode(uuid));
}
@PostMapping("/scan")
public ResponseData<?> scanQrCode(@RequestBody ScanInfoDTO scanInfoDTO) {
userService.scanQrcode(scanInfoDTO);
return ResponseData.success("");
}
@PostMapping("/confirm")
public ResponseData<?> confirm(@RequestBody ScanInfoDTO scanInfoDTO) {
userService.confirm(scanInfoDTO);
return ResponseData.success("");
}
}

View File

@@ -0,0 +1,29 @@
package vvpkassistant.User.model.DTO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginInfoDTO {
/**
* 唯一标识
*/
private String uuid;
/**
* 设备号
*/
private String device;
/**
* 扫码状态
*/
private String status;
private Integer userId;
}

View File

@@ -0,0 +1,13 @@
package vvpkassistant.User.model.DTO;
import lombok.Data;
/*
* @author: ziin
* @date: 2025/8/5 16:43
*/
@Data
public class ScanInfoDTO {
private String uuid;
private Integer userId;
}

View File

@@ -0,0 +1,13 @@
package vvpkassistant.User.model;
import lombok.Data;
/*
* @author: ziin
* @date: 2025/8/5 20:06
*/
@Data
public class QrcodeEntity {
private String uuid;
private String type;
}

View File

@@ -0,0 +1,15 @@
package vvpkassistant.User.model;
import lombok.Builder;
import lombok.Data;
/*
* @author: ziin
* @date: 2025/8/5 18:14
*/
@Data
@Builder
public class QrcodeVO {
private String uuid;
private String qrcode;
}

View File

@@ -0,0 +1,32 @@
package vvpkassistant.User.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ResponseVO {
/**
* 唯一标识
*/
private String uuid;
/**
* 登录二维码
*/
private String qrcode;
/**
* jwt令牌
*/
private String token;
/**
* 扫码状态
*/
private String status;
}

View File

@@ -23,5 +23,6 @@ public class UserModelVO {
private String token;
private Boolean newAccount;
private WxChatParam chatInfo;
private Boolean havaPassword;
private Integer mailVerification;
}

View File

@@ -0,0 +1,17 @@
package vvpkassistant.User.model.enumeration;
import lombok.Getter;
@Getter
public enum LoginStatusEnum {
UNSCANNED("未扫描"),
SCANNED("已扫描"),
CONFIRMED("已确认");
private String desc;
LoginStatusEnum(String desc) {
this.desc = desc;
}
}

View File

@@ -1,6 +1,7 @@
package vvpkassistant.User.service;
import com.baomidou.mybatisplus.extension.service.IService;
import vvpkassistant.User.model.DTO.ScanInfoDTO;
import vvpkassistant.User.model.DTO.UserModelDTO;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.User.model.UserModelVO;
@@ -21,4 +22,12 @@ public interface UserService extends IService<UserModel> {
Boolean activateAccount(String token);
Boolean verificationMail(String token);
Object generatedQrcode();
Object checkQrcode(String uuid);
void scanQrcode(ScanInfoDTO scanInfoDTO);
void confirm(ScanInfoDTO scanInfoDTO);
}

View File

@@ -3,28 +3,41 @@ package vvpkassistant.User.service;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.temp.SaTempUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.zxing.WriterException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import vvpkassistant.Data.WxChatParam;
import vvpkassistant.Tools.BcryptUtils;
import vvpkassistant.Tools.QRCodeUtil;
import vvpkassistant.Tools.VVTools;
import vvpkassistant.User.mapper.UserDao;
import vvpkassistant.User.model.*;
import vvpkassistant.User.model.DTO.LoginInfoDTO;
import vvpkassistant.User.model.DTO.ScanInfoDTO;
import vvpkassistant.User.model.DTO.UserModelDTO;
import vvpkassistant.User.model.UserModel;
import vvpkassistant.User.model.UserModelVO;
import vvpkassistant.User.model.enumeration.LoginStatusEnum;
import vvpkassistant.common.ErrorCode;
import vvpkassistant.exception.BusinessException;
import vvpkassistant.mail.service.MailService;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/*
* @author: ziin
* @date: 2025/8/4 16:19
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserDao, UserModel> implements UserService {
@Resource
@@ -35,6 +48,11 @@ public class UserServiceImpl extends ServiceImpl<UserDao, UserModel> implements
@Autowired
private MailService mailService;
private final Cache<String, LoginInfoDTO> qrcodeCache = Caffeine.newBuilder()
.expireAfterWrite(2, TimeUnit.MINUTES)
.build();
@Override
public UserModelVO loginWithMail(UserModelDTO model) {
@@ -164,4 +182,74 @@ public class UserServiceImpl extends ServiceImpl<UserDao, UserModel> implements
}
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"邮箱验证失败");
}
@Override
public QrcodeVO generatedQrcode() {
String uuid = UUID.randomUUID().toString();
QrcodeEntity qrcodeEntity = new QrcodeEntity();
qrcodeEntity.setUuid(uuid);
qrcodeEntity.setType("qrcdoe");
String base64QR = null;
try {
base64QR = QRCodeUtil.generateQRCode(JSONUtil.toJsonStr(qrcodeEntity), 200, 200);
} catch (WriterException | IOException e) {
log.error(e.getMessage());
throw new BusinessException(ErrorCode.SYSTEM_ERROR,"二维码生成失败");
}
LoginInfoDTO loginInfoDTO = new LoginInfoDTO();
loginInfoDTO.setStatus(LoginStatusEnum.UNSCANNED.name());
loginInfoDTO.setUuid(uuid);
// 二维码uuid绑定存入缓存
qrcodeCache.put(uuid,loginInfoDTO);
// 返回生成的二维码信息
QrcodeVO vo = QrcodeVO.builder().uuid(uuid).qrcode("data:image/png;base64," + base64QR).build();
log.info("-------生成二维码成功:{}-------", uuid);
return vo;
}
@Override
public Object checkQrcode(String uuid) {
LoginInfoDTO loginInfoDTO = qrcodeCache.getIfPresent(uuid);
if (loginInfoDTO == null) {
throw new BusinessException(ErrorCode.QRCODE_EXPIRED);
}
if (Objects.equals(loginInfoDTO.getStatus(), LoginStatusEnum.SCANNED.name())) {
return loginInfoDTO;
}
if (LoginStatusEnum.CONFIRMED.name().equals(loginInfoDTO.getStatus())) {
UserModel userModel = userDao.selectById(loginInfoDTO.getUserId());
StpUtil.login(userModel.getId());
UserModelVO userModelVO = BeanUtil.copyProperties(userModel, UserModelVO.class);
userModelVO.setToken(StpUtil.getTokenValue());
userModelVO.setChatInfo(wxChatParam);
return userModelVO;
}
return null;
}
@Override
public void scanQrcode(ScanInfoDTO scanInfoDTO) {
LoginInfoDTO loginInfoDTO = qrcodeCache.getIfPresent(scanInfoDTO.getUuid());
if (loginInfoDTO != null) {
loginInfoDTO.setStatus(LoginStatusEnum.SCANNED.name());
}
if (loginInfoDTO != null) {
qrcodeCache.put(scanInfoDTO.getUuid(),loginInfoDTO);
}
log.info("-------扫码成功uuid:{}-------", scanInfoDTO.getUuid());
}
@Override
public void confirm(ScanInfoDTO scanInfoDTO) {
LoginInfoDTO loginInfoDTO = qrcodeCache.getIfPresent(scanInfoDTO.getUuid());
if (loginInfoDTO != null) {
loginInfoDTO.setStatus(LoginStatusEnum.CONFIRMED.name());
loginInfoDTO.setUserId(scanInfoDTO.getUserId());
qrcodeCache.put(scanInfoDTO.getUuid(),loginInfoDTO);
}
log.info("-------确认登录成功uuid:{}-------", scanInfoDTO.getUuid());
}
}

View File

@@ -16,6 +16,7 @@ public enum ErrorCode {
CONFIG_NAME_DUPLICATE(1004, "配置名称重复"),
SIGN_IN_FAIL(1004, "当天已签到"),
EMAIL_SEND_FREQUENT(1005,"邮件发送太频繁,请 1 分钟后再试"),
QRCODE_EXPIRED(1006,"二维码已过期"),
/* =============== 主播相关 =============== */
ANCHOR_ALREADY_EXISTS(2001, "主播已存在"),

View File

@@ -45,7 +45,14 @@ public class SaTokenConfigure implements WebMvcConfigurer {
"/user/loginWithPhoneNumber",
"/user/registerWithMail",
"/user/loginWithMail",
"/user/activate"
"/user/inputUserInfo",
"/user/verificationMail",
"/user/activate",
"/user/qrcode",
"/user/check/*",
"/user/scan",
"/user/confirm"
};
}

View File

@@ -30,9 +30,9 @@ sa-token:
# token 名称(同时也是 cookie 名称)
token-name: token
# token 有效期(单位:秒) 默认30天-1 代表永久有效
timeout: 2592000
timeout: -1
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
active-timeout: 648000
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token
@@ -42,4 +42,5 @@ sa-token:
# 是否输出操作日志
is-log: true
activateUrl: http://192.168.1.174:8086/user/activate?token=
activateUrl: http://192.168.1.174:8086/user/activate?token=
verificationMailUrl: http://192.168.1.174:8086/user/verification?token=