Spring Boot + Vue实现用户状态跟踪与个性化问候功能实战

用户状态跟踪会话管理JWT
于 2026-08-02 03:59:18 修改
·本内容遵循CC 4.0 BY-SA版权协议

在实际的软件开发项目中,我们经常需要处理一些看似简单,但背后逻辑复杂的“日常”任务。例如,一个用户登录后的问候语功能,标题“小庄,下次见面,请先说你好。。”就暗示了一个典型的场景:系统需要记住用户上次的访问状态,并在下次见面时给出个性化的问候。这背后涉及到会话管理、状态持久化、个性化逻辑以及前后端交互等多个技术点。很多新手开发者可能会直接在前端写死一句问候语,或者简单地从后端返回一个固定字符串,但这样做既无法实现“下次见面”的识别,也无法应对多设备登录、会话过期等真实场景。

本文将围绕如何构建一个健壮的、可复现的“下次见面问候”功能展开。我们将从最基础的会话(Session)与令牌(Token)机制讲起,逐步深入到如何在后端持久化用户访问记录,并基于此生成动态的问候语。整个过程会使用一个典型的 Spring Boot + Vue 的前后端分离项目作为示例,但核心思路适用于任何技术栈。通过本文,你将能理解用户状态跟踪的常见方案,掌握从数据库设计到接口实现的全流程,并学会处理相关的边界情况和异常。

1. 理解“下次见面”背后的技术机制:会话、令牌与状态持久化

“下次见面”这个需求,本质上是一个用户状态识别与记忆的问题。在无状态的 HTTP 协议下,服务器需要一种机制来识别连续请求是否来自同一个用户,并记住该用户的一些信息(比如上次访问时间)。

1.1 会话(Session)与 Cookie 的经典模式

在最传统的 Web 应用中,服务器使用 Session 来维持用户状态。当用户第一次访问时,服务器会创建一个唯一的 Session ID,并将其通过 Set-Cookie 头部返回给浏览器。浏览器后续的每次请求都会自动携带这个 Cookie,服务器通过解析 Cookie 中的 Session ID 来找到对应的会话数据。

JAVA
// 一个简化的 Servlet 示例,展示 Session 的创建与使用
HttpSession session = request.getSession(true); // 获取或创建 Session
session.setAttribute("lastVisitTime", new Date()); // 存储上次访问时间
 
// 在后续请求中获取
Date lastVisit = (Date) session.getAttribute("lastVisitTime");
if (lastVisit != null) {
// 计算时间差,生成如“好久不见”或“欢迎回来”的问候语
}

这种模式的优点是对于开发者透明,使用简单。但其缺点也很明显:Session 数据默认存储在服务器内存中,在集群环境下需要做 Session 共享(如使用 Redis),否则用户请求落到不同服务器就会“失忆”。此外,Cookie 可能因浏览器禁用或跨域问题而失效。

1.2 基于令牌(Token)的无状态认证

在现代前后端分离和微服务架构中,更流行使用无状态的 Token 机制,如 JWT (JSON Web Token)。用户登录成功后,服务器生成一个包含用户标识和过期时间的 Token 返回给前端。前端后续请求在 Authorization 头部携带此 Token。服务器只需验证 Token 的签名和有效性,无需在服务器端存储会话。

JAVA
// 使用 JJWT 库生成一个简单的 JWT
String token = Jwts.builder()
.setSubject(userId) // 用户标识
.setIssuedAt(new Date()) // 签发时间
.setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 过期时间(1小时)
.signWith(SignatureAlgorithm.HS512, secretKey) // 签名算法和密钥
.compact();

Token 的 payload 部分可以自定义一些非敏感信息,比如 lastLoginIp,但不建议将上次访问时间这类频繁变化的数据放入 JWT,因为 Token 一旦签发,在过期前其内容是不可变的。要实现“下次见面”的判断,我们需要将用户的上次活动时间持久化到数据库或缓存中。

1.3 状态持久化的设计选择

为了可靠地记录用户“上次见面”的时间,我们需要一个持久化存储。常见的选择有:

  1. 用户表新增字段:在 user 表中增加 last_visit_time 字段。每次用户发起有效请求(通过认证的请求)时,更新此字段。
    • 优点:结构简单,直接与用户绑定。
    • 缺点:频繁更新对数据库有压力,且该字段的更新可能与其他用户信息更新产生锁竞争。
  2. 独立的用户行为表:创建 user_activity 表,记录每次请求的日志,然后通过查询最近一条记录来获取“上次见面”时间。
    • 优点:数据维度丰富,可用于审计和分析。
    • 缺点:数据量增长快,查询“最近一次”需要优化(如通过索引或定时汇总)。
  3. 缓存(如 Redis):以 user:${userId}:lastVisit 为 key,存储时间戳。设置合理的过期时间(如 30 天)。
    • 优点:读写性能极高,适合高频更新。
    • 缺点:数据可能丢失(取决于 Redis 持久化策略),需要处理缓存穿透/雪崩。

对于“小庄的日常”这类以用户为中心的功能,结合使用 数据库记录核心时间点缓存提供快速访问 是一种平衡的方案。我们将采用在用户表记录 last_login_time(登录时更新)和 last_active_time(每次请求更新),同时在 Redis 缓存一份活跃状态的设计。

2. 环境准备与项目结构搭建

我们将构建一个 Spring Boot 后端和一个简单的 Vue 3 前端来演示。确保你的开发环境已就绪。

2.1 后端技术栈与依赖

  • JDK: 17 或以上
  • 构建工具: Maven 3.6+
  • 主要框架: Spring Boot 3.x
  • 数据库: MySQL 8.0,并安装 Redis 7.x 作为缓存。
  • IDE: IntelliJ IDEA 或 VS Code

创建一个新的 Spring Boot 项目,或在你现有项目的 pom.xml 中添加以下关键依赖:

XML
<dependencies>
<!-- Web 支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 数据 JPA,用于数据库操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Redis 缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JWT 支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Lombok 简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

2.2 前端技术栈与初始化

前端我们使用 Vue 3 的 Composition API 和 Element Plus 组件库快速搭建。

BASH
# 使用 Vite 创建 Vue 项目
npm create vue@latest hello-xiaozhuang
cd hello-xiaozhuang
npm install
# 安装路由、状态管理、HTTP 库和 UI 库
npm install vue-router@4 pinia axios element-plus
# 安装图标库(可选)
npm install @element-plus/icons-vue

2.3 数据库表设计

在 MySQL 中创建数据库 demo_xiaozhuang,并执行以下 SQL 创建用户表:

SQL
CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(64) NOT NULL COMMENT '用户名',
`password` varchar(255) NOT NULL COMMENT '加密后的密码',
`nickname` varchar(64) DEFAULT NULL COMMENT '用户昵称(如“小庄”)',
`last_login_time` datetime DEFAULT NULL COMMENT '上次登录时间',
`last_active_time` datetime DEFAULT NULL COMMENT '上次活跃时间(任何请求)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统用户表';
 
-- 插入一条测试数据,密码是 '123456' 经过 BCrypt 加密后的结果
INSERT INTO `sys_user` (`username`, `password`, `nickname`) VALUES
('zhuang', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTV6UiC', '小庄');

注意:生产环境的密码必须使用 BCryptPasswordEncoder 这类强哈希算法加密,切勿明文存储。上述插入语句中的密文仅用于测试。

2.4 后端项目结构

创建完成后,你的后端项目核心结构应类似如下:

TEXT
src/main/java/com/example/demo/
├── DemoApplication.java
├── config/
│ ├── JwtConfig.java # JWT 配置类
│ ├── RedisConfig.java # Redis 序列化配置
│ └── WebConfig.java # 跨域等 Web 配置
├── controller/
│ ├── AuthController.java # 认证相关接口(登录、登出)
│ └── UserController.java # 用户信息、问候语接口
├── entity/
│ └── SysUser.java # 用户实体类,对应数据库表
├── repository/
│ └── UserRepository.java # 用户数据访问层(JPA)
├── service/
│ ├── AuthService.java # 认证服务
│ ├── UserService.java # 用户服务
│ └── impl/ # 服务实现
├── dto/
│ ├── LoginRequest.java # 登录请求 DTO
│ ├── LoginResponse.java # 登录响应 DTO
│ └── UserProfile.java # 用户信息 DTO
├── util/
│ └── JwtUtil.java # JWT 工具类
└── interceptor/
└── JwtInterceptor.java # JWT 令牌拦截器

3. 核心代码实现:从登录到生成问候语

现在,我们开始实现核心逻辑。整个过程分为:用户登录并颁发 Token、拦截器验证 Token 并更新活跃时间、提供接口返回个性化问候语。

3.1 JWT 工具类与配置

首先创建 JWT 工具类,用于生成和解析 Token。

JAVA
// file: src/main/java/com/example/demo/util/JwtUtil.java
package com.example.demo.util;
 
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
 
@Component
public class JwtUtil {
// 从 application.yml 注入密钥和过期时间
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
 
private SecretKey getSigningKey() {
// 将字符串密钥转换为安全的 SecretKey
return Keys.hmacShaKeyFor(secret.getBytes());
}
 
// 生成 Token,以用户ID作为主题
public String generateToken(String userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(userId)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.compact();
}
 
// 从 Token 中解析出用户ID
public String getUserIdFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
 
// 验证 Token 是否有效(未过期且签名正确)
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
// 日志记录异常,生产环境需细化处理
return false;
}
}
}

application.yml 中配置 JWT 参数和数据库连接:

YAML
# file: src/main/resources/application.yml
server:
port: 8080
 
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo_xiaozhuang?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update # 首次启动可设为 update 自动建表,生产环境用 validate 或 none
show-sql: true
properties:
hibernate:
format_sql: true
redis:
host: localhost
port: 6379
password: # 如果 Redis 有密码则填写
database: 0
 
jwt:
secret: "YourSuperSecretKeyHereMakeItLongAndComplexEnoughForHS512" # 至少 32 位
expiration: 86400000 # Token 过期时间,单位毫秒,这里是 24 小时

3.2 用户实体与数据访问层

创建用户实体类,并使用 JPA 进行映射。

JAVA
// file: src/main/java/com/example/demo/entity/SysUser.java
package com.example.demo.entity;
 
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.time.LocalDateTime;
 
@Data
@Entity
@Table(name = "sys_user")
public class SysUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
 
@Column(nullable = false, unique = true, length = 64)
private String username;
 
@Column(nullable = false, length = 255)
private String password;
 
@Column(length = 64)
private String nickname; // 昵称,用于生成问候语
 
@Column(name = "last_login_time")
private LocalDateTime lastLoginTime; // 上次登录时间
 
@Column(name = "last_active_time")
private LocalDateTime lastActiveTime; // 上次活跃时间
 
@CreationTimestamp
@Column(name = "create_time", updatable = false)
private LocalDateTime createTime;
 
@UpdateTimestamp
@Column(name = "update_time")
private LocalDateTime updateTime;
}

创建对应的 Repository 接口。

JAVA
// file: src/main/java/com/example/demo/repository/UserRepository.java
package com.example.demo.repository;
 
import com.example.demo.entity.SysUser;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
 
public interface UserRepository extends JpaRepository<SysUser, Long> {
Optional<SysUser> findByUsername(String username);
}

3.3 认证服务与登录接口

实现登录逻辑,验证密码,更新登录时间,并生成 JWT Token。

JAVA
// file: src/main/java/com/example/demo/service/impl/AuthServiceImpl.java
package com.example.demo.service.impl;
 
import com.example.demo.dto.LoginRequest;
import com.example.demo.dto.LoginResponse;
import com.example.demo.entity.SysUser;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.AuthService;
import com.example.demo.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Optional;
 
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
 
@Override
@Transactional
public LoginResponse login(LoginRequest request) {
// 1. 根据用户名查找用户
Optional<SysUser> userOpt = userRepository.findByUsername(request.getUsername());
if (userOpt.isEmpty()) {
throw new RuntimeException("用户名或密码错误"); // 生产环境应使用自定义业务异常
}
SysUser user = userOpt.get();
 
// 2. 验证密码
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new RuntimeException("用户名或密码错误");
}
 
// 3. 更新用户的最后登录时间和最后活跃时间
LocalDateTime now = LocalDateTime.now();
user.setLastLoginTime(now);
user.setLastActiveTime(now);
userRepository.save(user); // 保存更新
 
// 4. 生成 JWT Token
String token = jwtUtil.generateToken(String.valueOf(user.getId()));
 
// 5. 构建响应
LoginResponse response = new LoginResponse();
response.setToken(token);
response.setUserId(user.getId());
response.setNickname(user.getNickname());
return response;
}
}

对应的 DTO 和 Controller。

JAVA
// file: src/main/java/com/example/demo/dto/LoginRequest.java
package com.example.demo.dto;
 
import lombok.Data;
import javax.validation.constraints.NotBlank;
 
@Data
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
JAVA
// file: src/main/java/com/example/demo/controller/AuthController.java
package com.example.demo.controller;
 
import com.example.demo.dto.LoginRequest;
import com.example.demo.dto.LoginResponse;
import com.example.demo.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
 
@PostMapping("/login")
public LoginResponse login(@Validated @RequestBody LoginRequest request) {
return authService.login(request);
}
}

3.4 JWT 拦截器与活跃时间更新

这是实现“下次见面”判断的核心。我们需要一个拦截器,在每次认证通过的请求中,更新用户的 last_active_time

JAVA
// file: src/main/java/com/example/demo/interceptor/JwtInterceptor.java
package com.example.demo.interceptor;
 
import com.example.demo.service.UserService;
import com.example.demo.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
@Component
@RequiredArgsConstructor
public class JwtInterceptor implements HandlerInterceptor {
private final JwtUtil jwtUtil;
private final UserService userService;
 
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 从请求头获取 Token
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
// 如果没有 Token,直接放行(可能是公开接口)。实际项目应根据路径判断。
// 这里为了演示,假设所有 /api/private/ 开头的路径都需要认证。
if (request.getRequestURI().startsWith("/api/private/")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
return true;
}
 
String token = authHeader.substring(7); // 去掉 "Bearer " 前缀
 
// 2. 验证 Token
if (!jwtUtil.validateToken(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
 
// 3. 从 Token 中解析用户ID
String userId = jwtUtil.getUserIdFromToken(token);
 
// 4. 将用户ID存入请求属性,供后续控制器使用
request.setAttribute("userId", userId);
 
// 5. 更新用户的最后活跃时间(异步或同步,根据性能要求)
// 注意:频繁的数据库更新可能成为瓶颈,生产环境可考虑放入队列或使用缓存延迟写入。
userService.updateLastActiveTime(Long.valueOf(userId));
 
return true;
}
}

注册拦截器到 Spring MVC。

JAVA
// file: src/main/java/com/example/demo/config/WebConfig.java
package com.example.demo.config;
 
import com.example.demo.interceptor.JwtInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
 
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final JwtInterceptor jwtInterceptor;
 
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截所有 /api/private/ 开头的请求
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/api/private/**")
.excludePathPatterns("/api/auth/**"); // 登录接口不需要拦截
}
}

UserService 中实现更新活跃时间的方法。

JAVA
// file: src/main/java/com/example/demo/service/impl/UserServiceImpl.java (部分代码)
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
 
@Override
@Transactional
public void updateLastActiveTime(Long userId) {
// 使用 @Modifying 和 @Query 进行批量更新效率更高,这里为清晰起见使用 findById
userRepository.findById(userId).ifPresent(user -> {
user.setLastActiveTime(LocalDateTime.now());
userRepository.save(user);
});
}
 
// 获取用户问候语的核心方法
@Override
public String generateGreeting(Long userId) {
SysUser user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("用户不存在"));
LocalDateTime lastActive = user.getLastActiveTime();
LocalDateTime now = LocalDateTime.now();
 
if (lastActive == null) {
// 第一次活跃
return String.format("%s,欢迎你首次到来!", user.getNickname());
}
 
long hoursBetween = Duration.between(lastActive, now).toHours();
String greeting;
if (hoursBetween < 1) {
greeting = "刚刚才见过,又想我啦?";
} else if (hoursBetween < 24) {
greeting = String.format("距离上次见面才过去 %d 小时。", hoursBetween);
} else if (hoursBetween < 24 * 7) {
greeting = String.format("哇,有 %d 天没见了!", hoursBetween / 24);
} else {
greeting = "好久不见,甚是想念!";
}
return String.format("%s,%s", user.getNickname(), greeting);
}
}

3.5 提供问候语接口

最后,创建一个受保护的接口,返回个性化的问候语。

JAVA
// file: src/main/java/com/example/demo/controller/UserController.java
package com.example.demo.controller;
 
import com.example.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
@RequestMapping("/api/private/user")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
 
@GetMapping("/greeting")
public String getGreeting(@RequestAttribute("userId") Long userId) {
// 从拦截器设置的请求属性中获取 userId
return userService.generateGreeting(userId);
}
}

4. 前端实现与联调测试

后端完成后,我们实现一个简单的前端页面进行测试。

4.1 前端登录与 Token 管理

在 Vue 项目中,创建 src/utils/request.js 封装 Axios,并自动携带 Token。

JAVASCRIPT
// file: src/utils/request.js
import axios from 'axios';
import { ElMessage } from 'element-plus';
import router from '../router';
 
const service = axios.create({
baseURL: 'http://localhost:8080/api', // 后端 API 地址
timeout: 10000,
});
 
// 请求拦截器:为需要认证的请求添加 Token
service.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
 
// 响应拦截器:统一处理错误
service.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
if (error.response) {
if (error.response.status === 401) {
ElMessage.error('登录已过期,请重新登录');
localStorage.removeItem('token');
router.push('/login');
} else {
ElMessage.error(error.response.data?.message || '请求失败');
}
} else {
ElMessage.error('网络错误或服务器无响应');
}
return Promise.reject(error);
}
);
 
export default service;

创建登录页面 src/views/Login.vue

VUE
<!-- file: src/views/Login.vue -->
<template>
<div class="login-container">
<el-card class="login-card">
<h2>小庄的日常 - 登录</h2>
<el-form :model="form" :rules="rules" ref="formRef" @submit.prevent="handleLogin">
<el-form-item prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" prefix-icon="User" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="form.password" type="password" placeholder="请输入密码" prefix-icon="Lock" show-password />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin" :loading="loading" style="width:100%">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
 
<script setup>
import { ref, reactive } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import request from '@/utils/request';
 
const router = useRouter();
const formRef = ref();
const loading = ref(false);
 
const form = reactive({
username: 'zhuang',
password: '123456',
});
 
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
};
 
const handleLogin = async () => {
await formRef.value.validate();
loading.value = true;
try {
const res = await request.post('/auth/login', form);
localStorage.setItem('token', res.token);
localStorage.setItem('nickname', res.nickname);
ElMessage.success('登录成功');
router.push('/home');
} catch (error) {
// 错误已在 request.js 中统一处理
} finally {
loading.value = false;
}
};
</script>
 
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f5f7fa;
}
.login-card {
width: 400px;
}
</style>

4.2 主页与问候语展示

创建主页 src/views/Home.vue,在页面加载时调用问候语接口。

VUE
<!-- file: src/views/Home.vue -->
<template>
<div class="home-container">
<el-card class="greeting-card">
<h1>{{ greeting }}</h1>
<p>这里是你的个人主页。</p>
<el-button @click="refreshGreeting" :loading="loading">刷新问候语</el-button>
<el-button type="danger" @click="handleLogout">退出登录</el-button>
</el-card>
</div>
</template>
 
<script setup>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import request from '@/utils/request';
 
const router = useRouter();
const greeting = ref('');
const loading = ref(false);
 
const fetchGreeting = async () => {
loading.value = true;
try {
const res = await request.get('/private/user/greeting');
greeting.value = res;
} catch (error) {
// 错误已处理
} finally {
loading.value = false;
}
};
 
const refreshGreeting = () => {
fetchGreeting();
};
 
const handleLogout = () => {
localStorage.removeItem('token');
localStorage.removeItem('nickname');
ElMessage.success('已退出登录');
router.push('/login');
};
 
onMounted(() => {
fetchGreeting();
});
</script>
 
<style scoped>
.home-container {
padding: 20px;
}
.greeting-card {
max-width: 600px;
margin: 0 auto;
text-align: center;
}
</style>

配置路由 src/router/index.js

JAVASCRIPT
// file: src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Login from '@/views/Login.vue';
import Home from '@/views/Home.vue';
 
const routes = [
{ path: '/', redirect: '/login' },
{ path: '/login', name: 'Login', component: Login },
{ path: '/home', name: 'Home', component: Home },
];
 
const router = createRouter({
history: createWebHistory(),
routes,
});
 
// 简单的路由守卫,检查登录状态
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token');
if (to.name !== 'Login' && !token) {
next({ name: 'Login' });
} else if (to.name === 'Login' && token) {
next({ name: 'Home' });
} else {
next();
}
});
 
export default router;

4.3 运行与验证

  1. 启动后端:运行 Spring Boot 的 DemoApplication 主类。确保 MySQL 和 Redis 服务已启动。
  2. 启动前端:在终端进入前端项目目录,执行 npm run dev
  3. 测试流程
    • 访问 http://localhost:5173 (Vite 默认端口) 会自动跳转到登录页。
    • 使用测试账号 zhuang / 123456 登录。
    • 登录成功后跳转到主页,会显示类似“小庄,欢迎你首次到来!”的问候语。
    • 点击“刷新问候语”按钮,由于活跃时间刚更新,可能会显示“小庄,刚刚才见过,又想我啦?”。
    • 关闭浏览器标签页,等待一段时间(或手动修改数据库中的 last_active_time 为更早的时间),重新打开页面并登录,问候语会根据时间差变化。

5. 常见问题排查与优化实践

将功能跑通只是第一步,在实际部署和运行中,你会遇到各种问题。以下是基于此项目的常见问题排查清单和优化建议。

5.1 常见问题排查表

问题现象 可能原因 检查点与解决方案
登录接口返回 500 或 “用户名密码错误” 1. 数据库连接失败。
2. 用户数据不存在或密码不匹配。
3. BCrypt 密码加密不匹配。
1. 检查 application.yml 中的数据库连接信息,确认 MySQL 服务运行。
2. 检查 sys_user 表中是否存在对应用户,密码密文是否正确(可使用 BCryptPasswordEncoder 重新生成)。
3. 确认登录时前端传递的密码是明文,且与注册/插入时使用的加密算法一致。
前端请求 /private/user/greeting 返回 401 1. 前端未携带 Token 或 Token 格式错误。
2. Token 已过期。
3. 后端 JWT 密钥不匹配。
1. 打开浏览器开发者工具的 Network 面板,查看请求头中 Authorization 是否为 Bearer <token> 格式。
2. 检查 Token 是否过期(默认24小时)。重新登录获取新 Token。
3. 确认后端 jwt.secret 在应用重启后未改变,否则旧 Token 会验证失败。
问候语始终是“首次到来”或时间差计算不对 1. last_active_time 字段未成功更新。
2. 时区问题导致时间计算错误。
3. 拦截器未生效或更新逻辑有误。
1. 检查数据库 sys_user 表的 last_active_time 字段是否在每次请求后更新。查看后端日志中 SQL 更新语句。
2. 确保数据库、后端应用服务器使用统一的时区(如 Asia/Shanghai)。在 application.yml 的数据库 URL 中指定 serverTimezone
3. 确认请求路径匹配拦截器的 addPathPatterns,且拦截器中的 updateLastActiveTime 方法被调用。
更新活跃时间导致数据库压力大 每次请求都更新数据库,高并发下会成为瓶颈。 1. 引入缓存:将活跃时间先写入 Redis,设置一个较短的过期时间(如5分钟),然后通过定时任务批量同步到数据库。
2. 异步更新:将更新操作放入消息队列(如 RabbitMQ)或线程池,避免阻塞主请求线程。
多设备登录时,问候语以哪个时间为准? 当前设计以最后一次活跃请求为准,所有设备共享同一个 last_active_time 如果业务需要区分设备,可以在用户-设备维度再建一张表,记录每个设备的上次活跃时间。生成问候语时,取当前设备的上次活跃时间。

5.2 生产环境优化建议

  1. Token 管理增强
    • 退出登录与 Token 黑名单:当前 JWT 是无状态的,服务端无法主动使其失效。实现退出功能时,可将未过期的 Token 加入 Redis 黑名单(key 为 Token 指纹,value 为过期时间),拦截器校验时需额外检查黑名单。
    • 刷新 Token:实现双 Token 机制(Access Token 短时间有效,Refresh Token 较长时间有效),避免用户频繁重新登录。
  2. 活跃时间更新策略优化
    • 防抖更新:在前端或拦截器层,对短时间内(如1分钟内)的连续请求,只更新一次活跃时间。
    • 分级更新:对不同的接口按重要性分级,只有核心用户行为接口才更新活跃时间,减少不必要的写操作。
  3. 问候语逻辑扩展
    • 个性化规则:将问候语规则(时间区间对应的文案)配置化,存储在数据库或配置中心,便于运营修改。
    • 结合其他数据:问候语不仅可以基于时间,还可以结合用户历史行为(如上次购买商品、上次阅读文章)来生成更精准的文案。
  4. 安全加固
    • 密码安全:强制密码复杂度,定期提醒修改密码,记录密码错误次数以防暴力破解。
    • 接口限流:对登录、刷新 Token 等接口进行限流,防止恶意攻击。
    • SQL 注入与 XSS:确保使用 JPA 的参数化查询,对用户输入的昵称等字段进行适当的过滤或转义,防止 XSS 攻击。

6. 扩展方向与总结

通过这个“小庄的日常”项目,我们完整实现了一个基于用户活跃时间的动态问候系统。它虽然是一个简单的功能,但串联了 Web 开发中多个核心概念:用户认证(JWT)、状态管理(Session/Token)、数据持久化(JPA)、缓存思想、拦截器、前后端分离交互等。

你可以在此基础上进行更多探索:

  • 扩展为完整的用户中心:加入注册、个人信息修改、头像上传、消息通知等功能。
  • 实现实时在线状态:利用 WebSocket,当用户连接时标记为“在线”,断开后标记为“离线”,并在问候语中体现。
  • 接入更复杂的推荐系统:根据用户活跃时间、访问频率、浏览内容等,生成更加个性化的每日问候或内容推荐。
  • 微服务化改造:将用户服务、认证服务、问候语服务拆分为独立的微服务,通过 Spring Cloud 或 Dubbo 进行通信,体验分布式架构下的状态管理挑战。

关键点回顾:实现“下次见面”功能,技术选型上需要在无状态 Token 的便利性有状态数据持久化的必要性之间取得平衡。JWT 解决了认证问题,但用户动态数据(如上次活跃时间)仍需存储在中心化的数据库或缓存中。在设计更新策略时,必须仔细权衡数据一致性、实时性与系统性能之间的关系。

从零构建WebOA系统:Spring Boot+Vue3全栈实战与架构设计
本文系统阐述了基于Spring Boot与Vue3从零构建WebOA系统的完整路径,涵盖RBAC权限模型、轻量级自研流程引擎、JWT认证鉴权、文件对象存储权限集成、动态路由菜单渲染等核心技术。重点解析了数据库设计(用户/角色/菜单/流程实例/任务表)、前后端分离部署方案及性能安全优化策略,强调业务解耦、状态机驱动工程化落地能力。
weixin_33829657
371
Spring Boot+Vue3汽车租赁系统:从环境搭建到功能测试全流程实战
本文详细介绍了基于Spring Boot与Vue3构建的汽车租赁系统的全流程实践,涵盖环境搭建(JDK、Node.js、MySQL)、前后端启动、双角色功能测试(用户租车、管理员车辆订单管理)、RESTful API调用及二次开发(智能推荐、操作日志增强)。系统具备完整CRUD、权限分离、数据报表等能力,适用于教学、毕设及小型商业原型开发,但不包含分布式、高并发等企业级特性。
weixin_33730836
285
计算机毕业设计springboot+Vue的健身房会员系统的设计与实现 基于Spring Boot与Vue.js的健身俱乐部会员管理系统开发实践 Spring Boot结合Vue实现的健身房会员
本文介绍基于Spring BootVue技术栈构建的健身房会员管理系统。该系统采用B/S架构MySQL数据库,具备用户、健身卡、商品等管理功能。系统能提升管理效率、降低成本,为会员提供便捷服务,且具有良好的可扩展性和可维护性。
毕设源码-小云学姐
1307
基于Spring Boot+Vue的校企实习管理系统毕业设计实战指南
本文详细阐述基于Spring Boot与Vue构建校企实习管理系统的完整实现路径,涵盖技术选型(Spring Boot 2.x、MyBatis-Plus、MySQL 8、Vue 3、Element Plus)、JWT认证、RBAC权限控制、岗位管理、实习申请流程、前后端分离部署(Nginx+Docker)、文件上传预览、WebSocket实时通知、ECharts数据可视化及工作流引擎(Flowable)等关键技术点,突出系统在毕业设计中的工程规范性可扩展性。
weixin_33701564
338
Spring Boot+Vue高校毕业生就业平台:全栈开发实战与毕设指南
本文详解基于Spring Boot与Vue.js开发的高校毕业生就业平台,涵盖环境搭建、前后端分离开发、多角色权限管理(学生/企业/管理员)、核心功能实现(职位发布、简历投递、数据统计)、API设计、打包部署及常见问题排查。项目聚焦真实就业场景,整合用户认证、数据库操作、RESTful接口、跨域代理、Nginx部署等关键技术点,适合作为计算机专业毕业设计或全栈学习实践案例。
chaoyv
187
Spring Boot + Vue 2 的企业级 SaaS 多租户招聘管理系统
本文介绍了一个基于Spring BootVue 2的企业级SaaS多租户招聘管理系统,涵盖招聘、审批、系统管理和报表中心四大模块。系统采用前后端分离架构,集成JWT认证、Redis缓存、MyBatis Plus ORM及Knife4j API文档工具,支持全流程招聘管理多租户隔离。
SUDO-1
1031
Spring Boot 3 + Vue 3 鲜花商城前后端分离源码实战
本项目是基于Spring Boot 3与Vue 3构建的前后端分离鲜花电商系统,支持普通用户、商家、管理员三角色权限隔离。后端采用RESTful API设计,集成MyBatis-Plus、MySQL及WebSocket实现实时咨询;前端通过Axios调用接口,实现商品管理、订单处理、购物车、收藏、评论、售后等完整电商功能。系统具备完善的权限控制、数据统计多端界面展示能力。
KK ???
9
Spring Boot 3 + Vue 3 医药营销项目信息管理平台源码 前后端分离实战
本平台基于Spring Boot 3与Vue 3构建前后端分离架构,实现医药营销项目全生命周期管理。支持系统管理员、项目主管、项目经理三角色权限控制,涵盖客户管理、项目创建与跟踪、加权任务进度计算、交付物上传、跟进记录、业绩看板等功能。技术上集成JWT鉴权、MyBatis-Plus逻辑删除自动填充、定时逾期检测及文件分目录存储。
KK ???
580
基于springboot的物流跟踪系统的开发与实现(源码+论文+部署+安装)
本文介绍了一个基于SpringBoot与Vue开发的物流跟踪系统,旨在解决电商背景下物流信息不透明、更新滞后等问题。系统采用前后端分离架构,支持用户、商家、管理员多角色协作,实现从下单、物流信息录入到实时追踪的全链路闭环管理,并具备良好的可扩展性和模块化设计,适用于高校毕业设计实际应用部署。
发量不够代码来凑
694
Spring Boot 3 + Vue 3 社区管理服务平台源码 前后端分离实战
本项目基于Spring Boot 3与Vue 3构建前后端分离的社区管理服务平台,涵盖用户、管理员、维修工三端功能。核心模块包括活动报名签到、报修工单流转、投诉建议处理、论坛互动、积分商城、AI智能助手(集成千帆大模型)、通知公告及轮播图管理。后端采用MyBatis-Plus操作MySQL,前端使用Vue 3+Element Plus,通过JWT实现权限控制身份认证。
KK ???
10
Java SSM+Vue建筑公司官网项目实战:从环境搭建到功能测试全流程
本文详细讲解基于Java SSM(Spring+Spring MVC+MyBatis)与Vue.js的建筑公司官网项目全流程实现,涵盖环境搭建(JDK、Maven、MySQL、Node.js)、前后端项目导入配置、数据库初始化、API联调、功能测试(前台展示后台CRUD)、接口验证及常见问题排查。重点突出前后端分离架构下的数据交互机制工程实践要点。
weixin_30621959
483
企业级spring boot软件管理系统源码|SpringBoot+Vue+MyBatis架构+MySQL数据库【完整版】
本文介绍了一个基于Spring BootVue和MyBatis构建的企业级软件管理系统,采用前后端分离架构,支持用户权限管理、项目任务分配、日志审计等功能。系统通过RESTful API通信,使用JWT认证和Redis缓存提升安全性性能,结合MySQL实现数据持久化,具备高可用性可扩展性,适用于现代化企业的项目管理需求。
QQ1__811517515
781
(附源码)spring boot智能养老系统平台-毕业设计 课程设计 170900
本系统基于Spring Boot框架开发,采用Java语言MySQL关系型数据库,实现老人健康中心管理、会员员工管理、健康档案、慢病干预、随访及老人认领等核心功能。系统采用B/S架构模块化设计,支持浏览器端操作,注重数据安全、稳定易维护性,适用于养老院及社区养老服务信息化场景。
星光开发者
5463
Vue+SpringBoot音乐网站全栈开发实战:从环境搭建到部署上线
本文详细介绍了基于Vue前端与Spring Boot后端的前后端分离音乐网站项目,涵盖环境搭建(JDK、MySQL、Node.js、Vue CLI)、前后端启动配置、核心功能测试(用户认证、音乐播放、歌单管理)、RESTful API设计、性能优化(Redis缓存、Nginx部署)及常见问题排查。重点突出全栈技术整合、数据库设计、接口联调生产部署实践。
csxc65837
279
Spring Boot 3 + Vue 3 + MySQL 养老一站式服务平台源码 前后端分离实战
本项目基于Spring Boot 3与Vue 3构建前后端分离的养老一站式服务平台,涵盖用户、服务商、管理员三角色权限体系。核心功能包括服务预约、订单管理、紧急求助、投诉处理、评价反馈及老人档案管理。后端采用JWT无状态认证、MyBatis-Plus操作MySQL;前端实现响应式交互模块化路由。系统具备完整业务闭环可扩展架构,适用于养老服务类O2O系统开发教学实践。
KK ???
7
Spring Boot3+Vue3+MySQL 企业客户关系管理系统前后端分离源码实战
本项目基于Spring Boot 3、Vue 3MySQL构建前后端分离的企业级客户关系管理系统(CRM),支持管理员、销售、客服三类角色,涵盖客户管理、订单处理、售后服务、知识库、统计分析等核心模块。采用Spring Security 6 + JWT实现动态权限控制,MyBatis-Plus简化数据访问,EasyExcel支持报表导出,前端通过角色动态渲染菜单路由,具备完整权限体系可视化数据分析能力。
KK ???
5
基于VUE的吉视传媒通信故障报维修系统[VUE]-计算机毕业设计源码+LW文档
本文设计并实现了一套基于Vue.js和Spring Boot的通信故障报修维修系统,采用前后端分离架构,集成用户管理、工单分配、维修跟踪与数据统计功能。系统通过Element UI构建交互界面,MySQL实现数据持久化,支持多角色权限控制流程自动化,显著提升故障处理效率服务质量。
毕业论文辅导老师
754
基于SpringBoot+Vue+WebSocket的在线聊天室
本文介绍了一个在线聊天系统的开发,采用前后端分离模式,后端用Spring Boot、MySql和Mybatis - Plus,前端用Vue3框架。运用WebSocket实现双向通信和消息实时推送,还介绍了Session和Cookie工作原理,解决了跨域问题,展示了登录、注册和群聊界面。
大木-
2735
SpringBoot+Vue+微信小程序框架程序设计|二手旧衣物回收平台的设计与实现功能演示+说明文档)
本博客介绍了基于Spring BootVue.js和微信小程序构建的二手旧衣物回收平台,涵盖预约回收、订单跟踪、审核积分发放、积分商城兑换、个人中心及数据统计等核心功能。系统采用前后端分离架构,后端提供RESTful API,前端小程序实现用户交互,Web后台支持管理员运营审核。数据库设计包含回收单、图片、商品、订单、资讯及反馈等关键表,支撑环保闭环业务流程。
毕设研究员
583
【SpringBoot框架篇】37.使用gRPC实现远程服务调用
本文详细介绍了在Spring Boot中集成gRPC实现远程服务调用的完整流程,涵盖RPCgRPC核心概念、Protocol Buffer(protobuf)语法规范及服务定义、grpc-api/grpc-server/grpc-client三模块工程搭建、proto文件编译、服务端接口实现(@GrpcService)、客户端调用(@GrpcClient)以及HTTP网关对接测试。重点突出gRPC在微服务场景下的高性能、跨语言、强类型和流式通信优势。
皓亮君
5165
使用Vue+Spring Boot实现Excel上传功能
使用Vue+Spring Boot实现Excel上传功能在本文中,我们将介绍如何使用Vue+Spring Boot实现Excel上传功能
weixin_38748875
3737
Spring Boot Vue前后端分离开发实战.pdf
综上所述,文档《Spring Boot Vue前后端分离开发实战.pdf》涵盖了前后端分离的开发模式、Spring BootVue.js的基础知识和使用、开发环境的搭建、前后端交互的实现、项目配置启动
亚历山大伟
6725
Vue Spring Boot前后端分离开发实战》源码Vue+Spring Boot前后端分离开发实战教学课件(PPT)
这本《Vue Spring Boot前后端分离开发实战》的源码提供了深入学习和实践这一技术栈的机会。以下是对其中涉及知识点的详细说明:1.
1804
spring boot+vue+websocket带token身份认证推送消息实现
本知识点将介绍如何在Spring Boot与Vue前后端分离架构中集成WebSocket,并实现带有身份认证的消息推送功能。这是实现即时通讯、实时数据更新等应用场景的常用技术方案。### 1.
xie_k
18310
Spring Boot + Vue3 完整开发全栈项目附资料.zip
本项目实战将深入讲解如何结合这两个强大的技术栈,实现一个全栈应用。下面,我们将详细探讨Spring Boot后端框架与Vue3前端框架的集成应用。
花纵酒
2916
spring boot项目和vue一起放在tomcat部署
"Spring Boot 项目与 Vue 一起部署到 Tomcat 服务器"在本文中,我们将介绍如何将 Spring Boot 项目和 Vue 一起部署到 Tomcat 服务器中。
qq_40783136
12542
spring boot+vue 的前后端分离合并方案实例详解
"spring boot+vue 的前后端分离合并方案实例详解"本文主要介绍了spring boot+vue 的前后端分离合并方案实例详解,讨论了前后端分离合并的必要性、前后端分离的两种方式、springboot
weixin_38570406
3069
Spring boot + mybatis + Vue.js + ElementUI 实现数据的增删改查实例代码(一)
本篇文档主要介绍了一个使用Spring Boot、MyBatis、Vue.js和ElementUI组合实现的增删改查实例。
weixin_38735541
3608
Spring Boot + Vue全栈开发实战随书源码附件
综上所述,"Spring Boot + Vue全栈开发实战"项目涉及的技术点包括Spring Boot的自动配置、Web开发、数据库操作、安全框架,以及Vue.js的组件化开发、状态管理、HTTP请求等
风起风来
819
Spring Boot后端 + Vue管理员前端 + 微信小程序用户前端 + Vue用户移动端
微信小程序和Vue移动端同样通过API后端进行通信,实现数据同步和功能交互。7.
热心网友!
2777