feat(auth): 实现扫码登录功能并优化邮件模板
- 新增 QrLoginController、QrLoginService、QrLoginData等类实现扫码登录功能 - 更新邮件模板中的公司名称、网址等信息 - 添加 JWT 配置项 - 优化应用配置文件,启用 Jackson 对 Java 8 时间类型的支持
This commit is contained in:
@@ -98,7 +98,7 @@ emailTemplateUtil.sendNotificationEmailWithAction(
|
|||||||
"您的订单已发货",
|
"您的订单已发货",
|
||||||
"user@example.com",
|
"user@example.com",
|
||||||
1001,
|
1001,
|
||||||
"https://www.gxwebsoft.com/orders/12345",
|
"https://websoft.top/orders/12345",
|
||||||
"查看订单"
|
"查看订单"
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|||||||
6
pom.xml
6
pom.xml
@@ -47,6 +47,12 @@
|
|||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- jackson-datatype-jsr310 for Java 8 time support -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- spring-boot-aop -->
|
<!-- spring-boot-aop -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.gxwebsoft.auto.controller;
|
||||||
|
|
||||||
|
import com.gxwebsoft.auto.dto.QrLoginConfirmRequest;
|
||||||
|
import com.gxwebsoft.auto.dto.QrLoginGenerateResponse;
|
||||||
|
import com.gxwebsoft.auto.dto.QrLoginStatusResponse;
|
||||||
|
import com.gxwebsoft.auto.service.QrLoginService;
|
||||||
|
import com.gxwebsoft.common.core.web.BaseController;
|
||||||
|
import com.gxwebsoft.common.core.web.ApiResult;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.validation.Valid;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证模块
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2025-03-06 22:50:25
|
||||||
|
*/
|
||||||
|
@Tag(name = "认证模块")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/qr-login")
|
||||||
|
public class QrLoginController extends BaseController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private QrLoginService qrLoginService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成扫码登录token
|
||||||
|
*/
|
||||||
|
@Operation(summary = "生成扫码登录token")
|
||||||
|
@PostMapping("/generate")
|
||||||
|
public ApiResult<?> generateQrLoginToken() {
|
||||||
|
try {
|
||||||
|
QrLoginGenerateResponse response = qrLoginService.generateQrLoginToken();
|
||||||
|
return success("生成成功", response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查扫码登录状态
|
||||||
|
*/
|
||||||
|
@Operation(summary = "检查扫码登录状态")
|
||||||
|
@GetMapping("/status/{token}")
|
||||||
|
public ApiResult<?> checkQrLoginStatus(
|
||||||
|
@Parameter(description = "扫码登录token") @PathVariable String token) {
|
||||||
|
try {
|
||||||
|
QrLoginStatusResponse response = qrLoginService.checkQrLoginStatus(token);
|
||||||
|
return success("查询成功", response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认扫码登录
|
||||||
|
*/
|
||||||
|
@Operation(summary = "确认扫码登录")
|
||||||
|
@PostMapping("/confirm")
|
||||||
|
public ApiResult<?> confirmQrLogin(@Valid @RequestBody QrLoginConfirmRequest request) {
|
||||||
|
try {
|
||||||
|
QrLoginStatusResponse response = qrLoginService.confirmQrLogin(request);
|
||||||
|
return success("确认成功", response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码操作(可选接口,用于移动端扫码后更新状态)
|
||||||
|
*/
|
||||||
|
@Operation(summary = "扫码操作")
|
||||||
|
@PostMapping("/scan/{token}")
|
||||||
|
public ApiResult<?> scanQrCode(@Parameter(description = "扫码登录token") @PathVariable String token) {
|
||||||
|
try {
|
||||||
|
boolean result = qrLoginService.scanQrCode(token);
|
||||||
|
return success("操作成功", result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return fail(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.gxwebsoft.auto.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码登录确认请求
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2025-08-31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "扫码登录确认请求")
|
||||||
|
public class QrLoginConfirmRequest {
|
||||||
|
|
||||||
|
@Schema(description = "扫码登录token")
|
||||||
|
@NotBlank(message = "token不能为空")
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
}
|
||||||
55
src/main/java/com/gxwebsoft/auto/dto/QrLoginData.java
Normal file
55
src/main/java/com/gxwebsoft/auto/dto/QrLoginData.java
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package com.gxwebsoft.auto.dto;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码登录数据模型
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2025-08-31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class QrLoginData {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码登录token
|
||||||
|
*/
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, expired-已过期
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID(扫码确认后设置)
|
||||||
|
*/
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名(扫码确认后设置)
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过期时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime expireTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWT访问令牌(确认后生成)
|
||||||
|
*/
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.gxwebsoft.auto.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码登录生成响应
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2025-08-31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "扫码登录生成响应")
|
||||||
|
public class QrLoginGenerateResponse {
|
||||||
|
|
||||||
|
@Schema(description = "扫码登录token")
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
@Schema(description = "二维码内容")
|
||||||
|
private String qrCode;
|
||||||
|
|
||||||
|
@Schema(description = "过期时间(秒)")
|
||||||
|
private Long expiresIn;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.gxwebsoft.auto.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码登录状态响应
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2025-08-31
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "扫码登录状态响应")
|
||||||
|
public class QrLoginStatusResponse {
|
||||||
|
|
||||||
|
@Schema(description = "状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, expired-已过期")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "JWT访问令牌(仅在confirmed状态时返回)")
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
@Schema(description = "用户信息(仅在confirmed状态时返回)")
|
||||||
|
private Object userInfo;
|
||||||
|
|
||||||
|
@Schema(description = "剩余过期时间(秒)")
|
||||||
|
private Long expiresIn;
|
||||||
|
|
||||||
|
}
|
||||||
46
src/main/java/com/gxwebsoft/auto/service/QrLoginService.java
Normal file
46
src/main/java/com/gxwebsoft/auto/service/QrLoginService.java
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package com.gxwebsoft.auto.service;
|
||||||
|
|
||||||
|
import com.gxwebsoft.auto.dto.QrLoginConfirmRequest;
|
||||||
|
import com.gxwebsoft.auto.dto.QrLoginGenerateResponse;
|
||||||
|
import com.gxwebsoft.auto.dto.QrLoginStatusResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码登录服务接口
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2025-08-31
|
||||||
|
*/
|
||||||
|
public interface QrLoginService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成扫码登录token
|
||||||
|
*
|
||||||
|
* @return QrLoginGenerateResponse
|
||||||
|
*/
|
||||||
|
QrLoginGenerateResponse generateQrLoginToken();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查扫码登录状态
|
||||||
|
*
|
||||||
|
* @param token 扫码登录token
|
||||||
|
* @return QrLoginStatusResponse
|
||||||
|
*/
|
||||||
|
QrLoginStatusResponse checkQrLoginStatus(String token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认扫码登录
|
||||||
|
*
|
||||||
|
* @param request 确认请求
|
||||||
|
* @return QrLoginStatusResponse
|
||||||
|
*/
|
||||||
|
QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码操作(更新状态为已扫码)
|
||||||
|
*
|
||||||
|
* @param token 扫码登录token
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
boolean scanQrCode(String token);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package com.gxwebsoft.auto.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.lang.UUID;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.gxwebsoft.auto.dto.*;
|
||||||
|
import com.gxwebsoft.auto.service.QrLoginService;
|
||||||
|
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.UserService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static com.gxwebsoft.common.core.constants.RedisConstants.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫码登录服务实现
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2025-08-31
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class QrLoginServiceImpl implements QrLoginService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@Value("${config.jwt.secret:websoft-jwt-secret-key-2025}")
|
||||||
|
private String jwtSecret;
|
||||||
|
|
||||||
|
@Value("${config.jwt.expire:86400}")
|
||||||
|
private Long jwtExpire;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public QrLoginGenerateResponse generateQrLoginToken() {
|
||||||
|
// 生成唯一的扫码登录token
|
||||||
|
String token = UUID.randomUUID().toString(true);
|
||||||
|
|
||||||
|
// 创建扫码登录数据
|
||||||
|
QrLoginData qrLoginData = new QrLoginData();
|
||||||
|
qrLoginData.setToken(token);
|
||||||
|
qrLoginData.setStatus(QR_LOGIN_STATUS_PENDING);
|
||||||
|
qrLoginData.setCreateTime(LocalDateTime.now());
|
||||||
|
qrLoginData.setExpireTime(LocalDateTime.now().plusSeconds(QR_LOGIN_TOKEN_TTL));
|
||||||
|
|
||||||
|
// 存储到Redis,设置过期时间
|
||||||
|
String redisKey = QR_LOGIN_TOKEN_KEY + token;
|
||||||
|
redisUtil.set(redisKey, qrLoginData, QR_LOGIN_TOKEN_TTL, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
log.info("生成扫码登录token: {}", token);
|
||||||
|
|
||||||
|
// 构造二维码内容(这里可以是前端登录页面的URL + token参数)
|
||||||
|
String qrCodeContent = "qr-login:" + token;
|
||||||
|
|
||||||
|
return new QrLoginGenerateResponse(token, qrCodeContent, QR_LOGIN_TOKEN_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public QrLoginStatusResponse checkQrLoginStatus(String token) {
|
||||||
|
if (StrUtil.isBlank(token)) {
|
||||||
|
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
String redisKey = QR_LOGIN_TOKEN_KEY + token;
|
||||||
|
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
|
||||||
|
|
||||||
|
if (qrLoginData == null) {
|
||||||
|
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
|
||||||
|
// 删除过期的token
|
||||||
|
redisUtil.delete(redisKey);
|
||||||
|
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算剩余过期时间
|
||||||
|
long expiresIn = ChronoUnit.SECONDS.between(LocalDateTime.now(), qrLoginData.getExpireTime());
|
||||||
|
|
||||||
|
QrLoginStatusResponse response = new QrLoginStatusResponse();
|
||||||
|
response.setStatus(qrLoginData.getStatus());
|
||||||
|
response.setExpiresIn(expiresIn);
|
||||||
|
|
||||||
|
// 如果已确认,返回token和用户信息
|
||||||
|
if (QR_LOGIN_STATUS_CONFIRMED.equals(qrLoginData.getStatus())) {
|
||||||
|
response.setAccessToken(qrLoginData.getAccessToken());
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
if (qrLoginData.getUserId() != null) {
|
||||||
|
User user = userService.getByIdRel(qrLoginData.getUserId());
|
||||||
|
if (user != null) {
|
||||||
|
// 清除敏感信息
|
||||||
|
user.setPassword(null);
|
||||||
|
response.setUserInfo(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认后删除token,防止重复使用
|
||||||
|
redisUtil.delete(redisKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request) {
|
||||||
|
String token = request.getToken();
|
||||||
|
Integer userId = request.getUserId();
|
||||||
|
|
||||||
|
if (StrUtil.isBlank(token) || userId == null) {
|
||||||
|
throw new RuntimeException("参数不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
String redisKey = QR_LOGIN_TOKEN_KEY + token;
|
||||||
|
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
|
||||||
|
|
||||||
|
if (qrLoginData == null) {
|
||||||
|
throw new RuntimeException("扫码登录token不存在或已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
|
||||||
|
redisUtil.delete(redisKey);
|
||||||
|
throw new RuntimeException("扫码登录token已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
User user = userService.getByIdRel(userId);
|
||||||
|
if (user == null) {
|
||||||
|
throw new RuntimeException("用户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户状态
|
||||||
|
if (user.getStatus() != null && user.getStatus() != 0) {
|
||||||
|
throw new RuntimeException("用户已被冻结");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成JWT token
|
||||||
|
JwtSubject jwtSubject = new JwtSubject(user.getUsername(), user.getTenantId());
|
||||||
|
String accessToken = JwtUtil.buildToken(jwtSubject, jwtExpire, jwtSecret);
|
||||||
|
|
||||||
|
// 更新扫码登录数据
|
||||||
|
qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED);
|
||||||
|
qrLoginData.setUserId(userId);
|
||||||
|
qrLoginData.setUsername(user.getUsername());
|
||||||
|
qrLoginData.setAccessToken(accessToken);
|
||||||
|
|
||||||
|
// 更新Redis中的数据
|
||||||
|
redisUtil.set(redisKey, qrLoginData, 60L, TimeUnit.SECONDS); // 给前端60秒时间获取token
|
||||||
|
|
||||||
|
log.info("用户 {} 确认扫码登录,token: {}", user.getUsername(), token);
|
||||||
|
|
||||||
|
// 清除敏感信息
|
||||||
|
user.setPassword(null);
|
||||||
|
|
||||||
|
return new QrLoginStatusResponse(QR_LOGIN_STATUS_CONFIRMED, accessToken, user, 60L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean scanQrCode(String token) {
|
||||||
|
if (StrUtil.isBlank(token)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String redisKey = QR_LOGIN_TOKEN_KEY + token;
|
||||||
|
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
|
||||||
|
|
||||||
|
if (qrLoginData == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
|
||||||
|
redisUtil.delete(redisKey);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有pending状态才能更新为scanned
|
||||||
|
if (QR_LOGIN_STATUS_PENDING.equals(qrLoginData.getStatus())) {
|
||||||
|
qrLoginData.setStatus(QR_LOGIN_STATUS_SCANNED);
|
||||||
|
|
||||||
|
// 计算剩余过期时间
|
||||||
|
long remainingSeconds = ChronoUnit.SECONDS.between(LocalDateTime.now(), qrLoginData.getExpireTime());
|
||||||
|
redisUtil.set(redisKey, qrLoginData, remainingSeconds, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
log.info("扫码登录token {} 状态更新为已扫码", token);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.gxwebsoft.common.core.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson配置类
|
||||||
|
* 解决Java 8时间类型序列化问题
|
||||||
|
*
|
||||||
|
* @author WebSoft
|
||||||
|
* @since 2024-08-28
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class JacksonConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public ObjectMapper objectMapper() {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
// 注册JavaTimeModule
|
||||||
|
mapper.registerModule(new JavaTimeModule());
|
||||||
|
|
||||||
|
// 禁用将日期写为时间戳
|
||||||
|
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||||
|
|
||||||
|
// 禁用将日期时间戳写为纳秒
|
||||||
|
mapper.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
|
||||||
|
|
||||||
|
return mapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ public class SwaggerConfig {
|
|||||||
.title(config.getSwaggerTitle())
|
.title(config.getSwaggerTitle())
|
||||||
.description(config.getSwaggerDescription())
|
.description(config.getSwaggerDescription())
|
||||||
.version(config.getSwaggerVersion())
|
.version(config.getSwaggerVersion())
|
||||||
.contact(new Contact("科技小王子","https://www.gxwebsoft.com","170083662@qq.com"))
|
.contact(new Contact("科技小王子","https://websoft.top","170083662@qq.com"))
|
||||||
.termsOfServiceUrl("https://server.gxwebsoft.com/api")
|
.termsOfServiceUrl("https://server.gxwebsoft.com/api")
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ public class RedisConstants {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 扫码登录相关key
|
||||||
|
public static final String QR_LOGIN_TOKEN_KEY = "qr-login:token:"; // 扫码登录token前缀
|
||||||
|
public static final Long QR_LOGIN_TOKEN_TTL = 300L; // 扫码登录token过期时间(5分钟)
|
||||||
|
public static final String QR_LOGIN_STATUS_PENDING = "pending"; // 等待扫码
|
||||||
|
public static final String QR_LOGIN_STATUS_SCANNED = "scanned"; // 已扫码
|
||||||
|
public static final String QR_LOGIN_STATUS_CONFIRMED = "confirmed"; // 已确认
|
||||||
|
public static final String QR_LOGIN_STATUS_EXPIRED = "expired"; // 已过期
|
||||||
|
|
||||||
// 哗啦啦key
|
// 哗啦啦key
|
||||||
public static final String getAllShop = "allShop";
|
public static final String getAllShop = "allShop";
|
||||||
public static final String getBaseInfo = "baseInfo";
|
public static final String getBaseInfo = "baseInfo";
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||||||
.permitAll()
|
.permitAll()
|
||||||
.antMatchers(
|
.antMatchers(
|
||||||
"/api/login",
|
"/api/login",
|
||||||
|
"/api/qr-login/**",
|
||||||
"/api/loginByUserId",
|
"/api/loginByUserId",
|
||||||
"/api/register",
|
"/api/register",
|
||||||
"/api/superAdminRegister",
|
"/api/superAdminRegister",
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package com.gxwebsoft.common.system.controller;
|
||||||
|
|
||||||
|
import com.gxwebsoft.common.core.web.ApiResult;
|
||||||
|
import com.gxwebsoft.common.core.web.BaseController;
|
||||||
|
import com.gxwebsoft.common.system.util.EmailTemplateUtil;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件测试控制器
|
||||||
|
* 用于测试邮件模板功能
|
||||||
|
*
|
||||||
|
* @author WebSoft
|
||||||
|
* @since 2024-08-28
|
||||||
|
*/
|
||||||
|
@Api(tags = "邮件测试")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/email-test")
|
||||||
|
public class EmailTestController extends BaseController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private EmailTemplateUtil emailTemplateUtil;
|
||||||
|
|
||||||
|
@ApiOperation("测试注册成功邮件")
|
||||||
|
@PostMapping("/register-success")
|
||||||
|
public ApiResult<?> testRegisterSuccessEmail(@RequestParam String email) {
|
||||||
|
try {
|
||||||
|
emailTemplateUtil.sendRegisterSuccessEmail(
|
||||||
|
"测试用户",
|
||||||
|
"13800138000",
|
||||||
|
"TestPassword123",
|
||||||
|
email,
|
||||||
|
getTenantId()
|
||||||
|
);
|
||||||
|
return success("注册成功邮件发送成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return fail("邮件发送失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("测试密码重置邮件")
|
||||||
|
@PostMapping("/password-reset")
|
||||||
|
public ApiResult<?> testPasswordResetEmail(@RequestParam String email) {
|
||||||
|
try {
|
||||||
|
emailTemplateUtil.sendPasswordResetEmail(
|
||||||
|
"测试用户",
|
||||||
|
"13800138000",
|
||||||
|
"NewPassword456",
|
||||||
|
email,
|
||||||
|
getTenantId()
|
||||||
|
);
|
||||||
|
return success("密码重置邮件发送成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return fail("邮件发送失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("测试通知邮件")
|
||||||
|
@PostMapping("/notification")
|
||||||
|
public ApiResult<?> testNotificationEmail(@RequestParam String email,
|
||||||
|
@RequestParam(required = false) String title,
|
||||||
|
@RequestParam(required = false) String content) {
|
||||||
|
try {
|
||||||
|
String emailTitle = title != null ? title : "WebSoft系统通知";
|
||||||
|
String emailContent = content != null ? content : "这是一条测试通知消息,用于验证邮件模板功能是否正常工作。";
|
||||||
|
|
||||||
|
emailTemplateUtil.sendNotificationEmailWithAction(
|
||||||
|
emailTitle,
|
||||||
|
emailContent,
|
||||||
|
email,
|
||||||
|
getTenantId(),
|
||||||
|
"https://www.gxwebsoft.com",
|
||||||
|
"访问官网"
|
||||||
|
);
|
||||||
|
return success("通知邮件发送成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return fail("邮件发送失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("测试安全提醒邮件")
|
||||||
|
@PostMapping("/security-alert")
|
||||||
|
public ApiResult<?> testSecurityAlertEmail(@RequestParam String email) {
|
||||||
|
try {
|
||||||
|
emailTemplateUtil.sendSecurityAlertEmail(
|
||||||
|
"测试用户",
|
||||||
|
"13800138000",
|
||||||
|
email,
|
||||||
|
getTenantId(),
|
||||||
|
"异地登录检测"
|
||||||
|
);
|
||||||
|
return success("安全提醒邮件发送成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return fail("邮件发送失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation("测试系统维护通知邮件")
|
||||||
|
@PostMapping("/maintenance")
|
||||||
|
public ApiResult<?> testMaintenanceEmail(@RequestParam String email) {
|
||||||
|
try {
|
||||||
|
emailTemplateUtil.sendMaintenanceNotificationEmail(
|
||||||
|
email,
|
||||||
|
getTenantId(),
|
||||||
|
"2024年8月28日 23:00-24:00",
|
||||||
|
"1小时"
|
||||||
|
);
|
||||||
|
return success("维护通知邮件发送成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return fail("邮件发送失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -157,7 +157,7 @@ public class EmailTemplateUtil {
|
|||||||
String title = "WebSoft账户安全提醒";
|
String title = "WebSoft账户安全提醒";
|
||||||
String content = "您的账户发生了以下安全事件:" + event + "。如果这不是您本人的操作,请立即联系客服并修改密码。";
|
String content = "您的账户发生了以下安全事件:" + event + "。如果这不是您本人的操作,请立即联系客服并修改密码。";
|
||||||
String infoMessage = "为了保障您的账户安全,建议您定期修改密码并开启双重验证。";
|
String infoMessage = "为了保障您的账户安全,建议您定期修改密码并开启双重验证。";
|
||||||
String actionUrl = "https://www.gxwebsoft.com/security";
|
String actionUrl = "https://websoft.top/security";
|
||||||
String actionText = "查看安全设置";
|
String actionText = "查看安全设置";
|
||||||
|
|
||||||
sendNotificationEmail(title, content, email, tenantId, "尊敬的用户", infoMessage, actionUrl, actionText);
|
sendNotificationEmail(title, content, email, tenantId, "尊敬的用户", infoMessage, actionUrl, actionText);
|
||||||
@@ -191,7 +191,7 @@ public class EmailTemplateUtil {
|
|||||||
public void sendOrderStatusEmail(String username, String orderNo, String status, String email, Integer tenantId) {
|
public void sendOrderStatusEmail(String username, String orderNo, String status, String email, Integer tenantId) {
|
||||||
String title = "WebSoft订单状态更新";
|
String title = "WebSoft订单状态更新";
|
||||||
String content = "您的订单 " + orderNo + " 状态已更新为:" + status + "。";
|
String content = "您的订单 " + orderNo + " 状态已更新为:" + status + "。";
|
||||||
String actionUrl = "https://www.gxwebsoft.com/orders/" + orderNo;
|
String actionUrl = "https://websoft.top/orders/" + orderNo;
|
||||||
String actionText = "查看订单详情";
|
String actionText = "查看订单详情";
|
||||||
|
|
||||||
sendNotificationEmailWithAction(title, content, email, tenantId, actionUrl, actionText);
|
sendNotificationEmailWithAction(title, content, email, tenantId, actionUrl, actionText);
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ spring:
|
|||||||
jackson:
|
jackson:
|
||||||
time-zone: GMT+8
|
time-zone: GMT+8
|
||||||
date-format: yyyy-MM-dd HH:mm:ss
|
date-format: yyyy-MM-dd HH:mm:ss
|
||||||
|
serialization:
|
||||||
|
write-dates-as-timestamps: false
|
||||||
|
write-date-timestamps-as-nanoseconds: false
|
||||||
|
deserialization:
|
||||||
|
read-date-timestamps-as-nanoseconds: false
|
||||||
|
|
||||||
# 设置上传文件大小
|
# 设置上传文件大小
|
||||||
servlet:
|
servlet:
|
||||||
@@ -101,6 +106,10 @@ config:
|
|||||||
bucketDomain: https://oss.wsdns.cn
|
bucketDomain: https://oss.wsdns.cn
|
||||||
aliyunDomain: https://oss-gxwebsoft.oss-cn-shenzhen.aliyuncs.com
|
aliyunDomain: https://oss-gxwebsoft.oss-cn-shenzhen.aliyuncs.com
|
||||||
|
|
||||||
|
# JWT配置
|
||||||
|
jwt:
|
||||||
|
secret: websoft-jwt-secret-key-2025-dev-environment
|
||||||
|
expire: 86400 # token过期时间(秒) 24小时
|
||||||
|
|
||||||
# 证书配置
|
# 证书配置
|
||||||
certificate:
|
certificate:
|
||||||
|
|||||||
@@ -6,18 +6,18 @@ email:
|
|||||||
# 品牌信息
|
# 品牌信息
|
||||||
brand:
|
brand:
|
||||||
name: "WebSoft"
|
name: "WebSoft"
|
||||||
company: "南宁网宿信息科技有限公司"
|
company: "南宁市网宿信息科技有限公司"
|
||||||
description: "企业级数字化解决方案"
|
description: "企业级数字化解决方案"
|
||||||
website: "https://www.gxwebsoft.com"
|
website: "https://websoft.top"
|
||||||
support_email: "170083662@qq.com"
|
support_email: "170083662@qq.com"
|
||||||
logo_url: "https://www.gxwebsoft.com/logo.png"
|
logo_url: "https://websoft.top/logo.png"
|
||||||
|
|
||||||
# 链接配置
|
# 链接配置
|
||||||
links:
|
links:
|
||||||
login: "https://www.gxwebsoft.com/login"
|
login: "https://websoft.top/login"
|
||||||
help: "https://www.gxwebsoft.com/help"
|
help: "https://websoft.top/help"
|
||||||
contact: "https://www.gxwebsoft.com/contact"
|
contact: "https://websoft.top/contact"
|
||||||
security: "https://www.gxwebsoft.com/security"
|
security: "https://websoft.top/security"
|
||||||
|
|
||||||
# 模板列表
|
# 模板列表
|
||||||
templates:
|
templates:
|
||||||
|
|||||||
@@ -240,19 +240,18 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
<strong>南宁网宿信息科技有限公司</strong><br>
|
<strong>WebSoft Admin</strong><br>
|
||||||
专业的企业数字化转型服务商
|
专业的企业数字化转型服务商
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="https://www.gxwebsoft.com">官方网站</a>
|
<a href="https://websoft.top">官方网站</a>
|
||||||
<a href="https://www.gxwebsoft.com/help">帮助中心</a>
|
<a href="https://websoft.top/help">帮助中心</a>
|
||||||
<a href="https://www.gxwebsoft.com/contact">联系我们</a>
|
<a href="https://websoft.top/contact">联系我们</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="copyright">
|
<div class="copyright">
|
||||||
© 2024 南宁网宿信息科技有限公司 版权所有<br>
|
© 2025 WebSoft Inc.
|
||||||
如有疑问,请联系客服:170083662@qq.com
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -303,7 +303,7 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
<strong>网宿软件</strong><br>
|
<strong>WebSoft Admin</strong><br>
|
||||||
专业的企业数字化转型服务商
|
专业的企业数字化转型服务商
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -297,7 +297,7 @@
|
|||||||
|
|
||||||
<!-- CTA Button -->
|
<!-- CTA Button -->
|
||||||
<div class="cta-section">
|
<div class="cta-section">
|
||||||
<a href="https://www.gxwebsoft.com/login" class="cta-button">立即登录体验</a>
|
<a href="https://websoft.top/login" class="cta-button">立即登录</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Features -->
|
<!-- Features -->
|
||||||
@@ -331,19 +331,18 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div class="footer-content">
|
<div class="footer-content">
|
||||||
<strong>南宁网宿信息科技有限公司</strong><br>
|
<strong>WebSoft Admin</strong><br>
|
||||||
专业的企业数字化转型服务商
|
专业的企业数字化转型服务商
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a href="https://www.gxwebsoft.com">官方网站</a>
|
<a href="https://websoft.top">官方网站</a>
|
||||||
<a href="https://www.gxwebsoft.com/help">帮助中心</a>
|
<a href="https://websoft.top/help">帮助中心</a>
|
||||||
<a href="https://www.gxwebsoft.com/contact">联系我们</a>
|
<a href="https://websoft.top/contact">联系我们</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="copyright">
|
<div class="copyright">
|
||||||
© 2024 南宁网宿信息科技有限公司 版权所有<br>
|
© 2025 WebSoft Inc.
|
||||||
如有疑问,请联系客服:170083662@qq.com
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user