Spring Boot+Vue 全栈开发:从核心原理到企业级落地的全维度实战总结
前言
本学期围绕 Web 应用程序开发展开了系统性学习,核心聚焦 Spring Boot+Vue+MySQL 全栈技术体系,同时延伸学习了 Redis 缓存优化、WebSocket 实时通信、JWT 认证、RBAC 权限管理、AI 能力集成(Ollama)、应用测试体系搭建等关键技术。本文并非简单的知识点罗列,而是结合10 + 实战场景、百余个可落地代码片段、企业级工程规范,从原理剖析、代码实现、问题排查、性能优化四个维度,完整还原从「需求分析」到「部署运维」的全流程,所有内容均为原创实战总结,力求达到 90 分以上的专业深度与实用性。
本文总字数超 15000 字,核心结构如下:
- 技术栈底层原理与工程化规范
- 核心模块实战(认证、权限、缓存、实时通信、AI 集成)
- 前端可视化与交互优化
- 全链路测试体系搭建
- 企业级部署与运维
- 常见问题与性能优化实战
- 学习心得与进阶方向
一、技术栈底层原理与工程化规范
1.1 核心技术栈选型与底层逻辑
1.1.1 后端技术栈:Spring Boot 3.x
Spring Boot 作为主流后端框架,核心优势在于「约定优于配置」,其底层基于 Spring Framework,通过自动配置(AutoConfiguration)机制简化了传统 Spring 应用的 xml 配置。本学期深入理解了其核心原理:
- 自动配置原理:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件定义了自动配置类,结合 @Conditional 注解(如 @ConditionalOnClass、@ConditionalOnMissingBean)实现「按需加载」;
- 启动流程:SpringApplication.run () → 初始化环境 → 加载配置 → 刷新上下文 → 注册 Bean → 启动内嵌容器(Tomcat);
- 核心注解:@SpringBootApplication(组合 @Configuration、@EnableAutoConfiguration、@ComponentScan)、@RestController(@Controller+@ResponseBody)、@Service、@Repository 等。
1.1.2 前端技术栈:Vue 3 + Element Plus
Vue 3 采用 Composition API 替代传统 Options API,核心优势是更好的代码复用性和类型支持,底层基于 Proxy 实现响应式(相比 Vue 2 的 Object.defineProperty,支持数组和新增属性监听):
- 响应式原理:通过 reactive/refs 创建响应式对象,effect 实现依赖收集与更新;
- 组件化思想:父子组件通信(Props/Emits)、全局组件注册、插槽(Slot)、自定义指令;
- 路由与状态管理:Vue Router 4 实现页面跳转,Pinia 替代 Vuex,支持 TypeScript 且无嵌套命名空间限制。
1.1.3 数据层:MySQL + Redis
- MySQL:关系型数据库,核心掌握 InnoDB 存储引擎(事务 ACID、行锁、聚簇索引)、索引优化(B + 树索引、联合索引、避免索引失效)、事务隔离级别(读已提交、可重复读);
- Redis:非关系型内存数据库,核心掌握 5 种数据结构(String、Hash、List、Set、ZSet)、持久化机制(RDB/AOF)、缓存策略(过期淘汰、缓存穿透 / 击穿 / 雪崩解决方案)。
1.2 工程化规范(企业级标准)
1.2.1 后端工程结构(Maven 多模块)
plaintext
web-demo/
├── web-demo-common/ // 公共模块:工具类、常量、通用DTO
├── web-demo-mapper/ // 数据访问层:Mapper接口、XML
├── web-demo-service/ // 业务层:Service接口+实现
├── web-demo-api/ // 接口层:Controller、请求/响应对象
├── web-demo-config/ // 配置层:Redis、Cors、MyBatis配置
└── web-demo-test/ // 测试层:单元测试、集成测试
POM.xml 核心依赖(Spring Boot 3.2):
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>web-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>web-demo</name>
<description>Spring Boot+Vue全栈开发实战</description>
<packaging>pom</packaging>
<modules>
<module>web-demo-common</module>
<module>web-demo-mapper</module>
<module>web-demo-service</module>
<module>web-demo-api</module>
<module>web-demo-config</module>
<module>web-demo-test</module>
</modules>
<!-- 依赖版本管理 -->
<dependencyManagement>
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</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-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<!-- 代码格式化插件 -->
<plugin>
<groupId>net.revelc.code.formatter</groupId>
<artifactId>formatter-maven-plugin</artifactId>
<version>2.23.0</version>
<configuration>
<configFile>src/main/resources/formatter.xml</configFile>
</configuration>
</plugin>
</plugins>
</build>
</project>
1.2.2 前端工程结构(Vue 3 + Vite)
plaintext
web-demo-front/
├── public/ // 静态资源(不打包)
├── src/
│ ├── api/ // 接口请求封装
│ ├── assets/ // 静态资源(图片、样式)
│ ├── components/ // 通用组件(全局/业务)
│ ├── layouts/ // 布局组件
│ ├── router/ // 路由配置
│ ├── store/ // Pinia状态管理
│ ├── utils/ // 工具类(axios、时间、加密)
│ ├── views/ // 页面视图
│ ├── App.vue // 根组件
│ └── main.ts // 入口文件
├── .eslintrc.js // ESLint配置
├── vite.config.ts // Vite配置
└── package.json // 依赖配置
package.json 核心依赖:
json
{
"name": "web-demo-front",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext vue,js,ts --fix"
},
"dependencies": {
"vue": "^3.3.8",
"vue-router": "^4.2.5",
"pinia": "^2.1.7",
"element-plus": "^2.4.2",
"axios": "^1.6.2",
"echarts": "^5.4.3",
"js-cookie": "^3.0.5",
"dayjs": "^1.11.10",
"crypto-js": "^4.2.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.0",
"typescript": "^5.2.2",
"vue-tsc": "^1.8.25",
"vite": "^5.0.0",
"eslint": "^8.54.0",
"eslint-plugin-vue": "^9.18.1",
"@types/crypto-js": "^4.2.1",
"@types/js-cookie": "^3.0.6"
}
}
1.2.3 编码规范
- 后端:遵循 Alibaba Java 开发手册,使用 Lombok 简化代码(@Data、@Slf4j),日志使用 SLF4J+Logback,异常统一处理;
- 前端:遵循 Vue 官方风格指南,使用 ESLint+Prettier 格式化代码,组件命名采用 PascalCase,接口请求统一封装;
- 数据库:表名使用小写 + 下划线,字段名与实体类属性驼峰对应,主键使用自增 ID 或雪花算法,索引命名规范(idx_表名_字段名)。
二、核心模块实战:从原理到落地
2.1 JWT 令牌认证 + 密码加密:安全体系基石
2.1.1 密码加密:BCrypt 算法原理与实现
密码明文存储是 Web 应用的「致命漏洞」,BCrypt 算法通过「加盐哈希」实现不可逆加密,核心特性:
- 自动生成随机盐(Salt),每个密码的盐不同,避免彩虹表破解;
- 计算强度可配置(cost 因子),值越大加密 / 解密耗时越长,抵御暴力破解;
- Spring Security 内置 BCryptPasswordEncoder,无需额外依赖。
实战代码:密码加密工具类(企业级封装)
java
运行
package com.example.common.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 密码加密工具类(单例模式+线程安全)
*/
@Slf4j
@Component
public class PasswordUtil {
private static final BCryptPasswordEncoder ENCODER = new BCryptPasswordEncoder(12); // cost因子12,平衡安全与性能
/**
* 加密密码
* @param rawPassword 明文密码
* @return 加密后的密码
*/
public String encrypt(String rawPassword) {
if (rawPassword == null || rawPassword.isEmpty()) {
throw new IllegalArgumentException("密码不能为空");
}
try {
return ENCODER.encode(rawPassword);
} catch (Exception e) {
log.error("密码加密失败", e);
throw new RuntimeException("密码加密失败");
}
}
/**
* 校验密码
* @param rawPassword 明文密码
* @param encodedPassword 加密后的密码
* @return 是否匹配
*/
public boolean match(String rawPassword, String encodedPassword) {
if (rawPassword == null || encodedPassword == null) {
return false;
}
try {
return ENCODER.matches(rawPassword, encodedPassword);
} catch (Exception e) {
log.error("密码校验失败", e);
return false;
}
}
}
2.1.2 JWT 认证:无状态设计与安全加固
JWT(JSON Web Token)是无状态认证方案,核心由 Header(算法)、Payload(数据)、Signature(签名)三部分组成,相比 Session 的优势:
- 无状态:服务端无需存储 Token,减轻服务器压力;
- 跨域友好:Token 通过请求头传递,支持跨域访问;
- 多端兼容:支持 Web、APP、小程序等多端认证。
实战代码 1:JWT 工具类(安全加固版)
java
运行
package com.example.common.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT工具类(生产级配置)
*/
@Slf4j
@Component
public class JwtUtil {
// 密钥(生产环境通过配置中心管理,避免硬编码)
@Value("${jwt.secret-key:default-secret-key-2025-web-demo}")
private String secretKey;
// 访问令牌过期时间:2小时(毫秒)
@Value("${jwt.access-token-expire:7200000}")
private long accessTokenExpire;
// 刷新令牌过期时间:7天(毫秒)
@Value("${jwt.refresh-token-expire:604800000}")
private long refreshTokenExpire;
/**
* 生成密钥(避免弱密钥)
*/
private SecretKey getSecretKey() {
// 密钥长度至少256位(32个字符),不足则填充
if (secretKey.length() < 32) {
throw new IllegalArgumentException("JWT密钥长度不能小于32位");
}
return Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
}
/**
* 生成访问令牌
* @param username 用户名
* @param userId 用户ID
* @return 访问令牌
*/
public String generateAccessToken(String username, Long userId) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("type", "access");
return generateToken(claims, username, accessTokenExpire);
}
/**
* 生成刷新令牌
* @param username 用户名
* @return 刷新令牌
*/
public String generateRefreshToken(String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("type", "refresh");
return generateToken(claims, username, refreshTokenExpire);
}
/**
* 通用生成Token方法
*/
private String generateToken(Map<String, Object> claims, String subject, long expireTime) {
try {
return Jwts.builder()
.setClaims(claims) // 自定义载荷
.setSubject(subject) // 主题(用户名)
.setIssuedAt(new Date()) // 签发时间
.setExpiration(new Date(System.currentTimeMillis() + expireTime)) // 过期时间
.signWith(getSecretKey(), SignatureAlgorithm.HS256) // 签名算法+密钥
.compact();
} catch (Exception e) {
log.error("生成JWT Token失败", e);
throw new RuntimeException("生成Token失败");
}
}
/**
* 校验Token有效性
* @param token Token字符串
* @return 校验通过返回Claims,否则返回null
*/
public Claims validateToken(String token) {
try {
// 解析Token,验证签名和过期时间
return Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
log.warn("JWT Token已过期:{}", token);
} catch (UnsupportedJwtException e) {
log.error("不支持的JWT Token格式:{}", token);
} catch (MalformedJwtException e) {
log.error("JWT Token格式错误:{}", token);
} catch (SignatureException e) {
log.error("JWT Token签名验证失败:{}", token);
} catch (IllegalArgumentException e) {
log.error("JWT Token参数异常:{}", token);
} catch (Exception e) {
log.error("JWT Token校验失败", e);
}
return null;
}
/**
* 从Token中解析用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = validateToken(token);
return claims != null ? claims.getSubject() : null;
}
/**
* 从Token中解析用户ID
*/
public Long getUserIdFromToken(String token) {
Claims claims = validateToken(token);
return claims != null ? claims.get("userId", Long.class) : null;
}
/**
* 判断Token是否为刷新令牌
*/
public boolean isRefreshToken(String token) {
Claims claims = validateToken(token);
if (claims == null) {
return false;
}
return "refresh".equals(claims.get("type"));
}
}
实战代码 2:认证接口与拦截器
java
运行
// 1. 登录DTO(数据传输对象)
package com.example.api.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 登录请求DTO
*/
@Data
public class LoginDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
// 2. 统一响应结果
package com.example.api.vo;
import lombok.Data;
/**
* 统一响应结果
* @param <T> 数据类型
*/
@Data
public class Result<T> {
// 响应码:200成功,500失败,401未认证,403无权限
private Integer code;
// 响应消息
private String msg;
// 响应数据
private T data;
// 成功响应(无数据)
public static <T> Result<T> success() {
return success("操作成功", null);
}
// 成功响应(有数据)
public static <T> Result<T> success(T data) {
return success("操作成功", data);
}
// 成功响应(自定义消息+数据)
public static <T> Result<T> success(String msg, T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg(msg);
result.setData(data);
return result;
}
// 失败响应
public static <T> Result<T> error(String msg) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMsg(msg);
result.setData(null);
return result;
}
// 未认证响应
public static <T> Result<T> unAuth(String msg) {
Result<T> result = new Result<>();
result.setCode(401);
result.setMsg(msg);
result.setData(null);
return result;
}
// 无权限响应
public static <T> Result<T> forbidden(String msg) {
Result<T> result = new Result<>();
result.setCode(403);
result.setMsg(msg);
result.setData(null);
return result;
}
}
// 3. 登录接口实现
package com.example.api.controller;
import com.example.api.dto.LoginDTO;
import com.example.api.vo.Result;
import com.example.common.util.JwtUtil;
import com.example.common.util.PasswordUtil;
import com.example.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;
import java.util.HashMap;
import java.util.Map;
/**
* 认证控制器
*/
@Slf4j
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final UserService userService;
private final PasswordUtil passwordUtil;
private final JwtUtil jwtUtil;
/**
* 登录接口
*/
@PostMapping("/login")
public Result<Map<String, String>> login(@Valid @RequestBody LoginDTO loginDTO) {
// 1. 查询用户(不存在则返回错误)
com.example.service.entity.User user = userService.getByUsername(loginDTO.getUsername());
if (user == null) {
log.warn("登录失败:用户名不存在,username={}", loginDTO.getUsername());
return Result.error("用户名或密码错误");
}
// 2. 校验密码(加密后对比)
if (!passwordUtil.match(loginDTO.getPassword(), user.getPassword())) {
log.warn("登录失败:密码错误,username={}", loginDTO.getUsername());
return Result.error("用户名或密码错误");
}
// 3. 生成Token(访问令牌+刷新令牌)
String accessToken = jwtUtil.generateAccessToken(user.getUsername(), user.getId());
String refreshToken = jwtUtil.generateRefreshToken(user.getUsername());
// 4. 返回结果(刷新令牌可存储在HttpOnly Cookie中,提升安全性)
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("accessToken", accessToken);
tokenMap.put("refreshToken", refreshToken);
log.info("登录成功,username={}", user.getUsername());
return Result.success("登录成功", tokenMap);
}
/**
* 刷新令牌接口
*/
@PostMapping("/refresh-token")
public Result<Map<String, String>> refreshToken(@RequestBody Map<String, String> params) {
String refreshToken = params.get("refreshToken");
if (refreshToken == null) {
return Result.unAuth("刷新令牌不能为空");
}
// 1. 校验刷新令牌
if (!jwtUtil.isRefreshToken(refreshToken)) {
return Result.unAuth("刷新令牌格式错误");
}
String username = jwtUtil.getUsernameFromToken(refreshToken);
if (username == null) {
return Result.unAuth("刷新令牌已过期或无效");
}
// 2. 生成新的访问令牌
com.example.service.entity.User user = userService.getByUsername(username);
String newAccessToken = jwtUtil.generateAccessToken(username, user.getId());
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("accessToken", newAccessToken);
return Result.success("令牌刷新成功", tokenMap);
}
/**
* 退出登录接口
*/
@PostMapping("/logout")
public Result<Void> logout() {
// 前端:清除localStorage中的Token
// 后端:若使用黑名单机制,需将Token加入黑名单(Redis)
return Result.success("退出登录成功");
}
}
// 4. JWT拦截器(验证访问令牌)
package com.example.config.interceptor;
import com.example.api.vo.Result;
import com.example.common.util.JwtUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.io.PrintWriter;
/**
* JWT拦截器:验证访问令牌
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtInterceptor implements HandlerInterceptor {
private final JwtUtil jwtUtil;
private final ObjectMapper objectMapper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 排除不需要认证的接口(登录、刷新令牌、静态资源)
String requestURI = request.getRequestURI();
if (requestURI.contains("/api/auth/login") || requestURI.contains("/api/auth/refresh-token")) {
return true;
}
// 2. 获取Token(从请求头Authorization中获取,格式:Bearer xxx)
String authorization = request.getHeader("Authorization");
if (authorization == null || !authorization.startsWith("Bearer ")) {
log.warn("请求未携带Token,URI={}", requestURI);
returnJson(response, Result.unAuth("请先登录"));
return false;
}
String token = authorization.substring(7);
// 3. 校验Token
if (jwtUtil.validateToken(token) == null) {
log.warn("Token无效或已过期,URI={}", requestURI);
returnJson(response, Result.unAuth("登录已过期,请重新登录"));
return false;
}
// 4. 将用户信息存入请求上下文(方便后续业务使用)
String username = jwtUtil.getUsernameFromToken(token);
Long userId = jwtUtil.getUserIdFromToken(token);
request.setAttribute("username", username);
request.setAttribute("userId", userId);
return true;
}
/**
* 响应JSON数据
*/
private void returnJson(HttpServletResponse response, Result<?> result) throws Exception {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(result));
writer.flush();
writer.close();
}
}
// 5. 注册拦截器
package com.example.config;
import com.example.config.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;
/**
* Web配置:注册拦截器
*/
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册JWT拦截器,拦截所有/api请求(排除认证接口)
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/login", "/api/auth/refresh-token");
}
}
实战代码 3:前端登录与 Token 管理
typescript
运行
// src/utils/request.ts:Axios封装
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useUserStore } from '@/store/user';
import router from '@/router';
// 创建Axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量读取接口地址
timeout: 10000, // 超时时间
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
});
// 请求拦截器:添加Token
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
const userStore = useUserStore();
if (userStore.token) {
config.headers = config.headers || {};
config.headers['Authorization'] = `Bearer ${userStore.token}`;
}
return config;
},
(error: AxiosError) => {
ElMessage.error('请求发送失败:' + error.message);
return Promise.reject(error);
}
);
// 响应拦截器:处理Token过期、业务错误
service.interceptors.response.use(
(response: AxiosResponse) => {
const res = response.data;
// 业务码非200时,统一提示错误
if (res.code !== 200) {
ElMessage.error(res.msg || '请求失败');
// 401:未认证,跳转登录页
if (res.code === 401) {
const userStore = useUserStore();
ElMessageBox.confirm('登录已过期,请重新登录', '提示', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
userStore.logout(); // 清除Token
router.push('/login'); // 跳转登录页
});
}
return Promise.reject(res);
}
return res;
},
(error: AxiosError) => {
ElMessage.error('服务器响应失败:' + (error.message || '未知错误'));
return Promise.reject(error);
}
);
export default service;
// src/store/user.ts:Pinia状态管理(用户信息)
import { defineStore } from 'pinia';
import { ref } from 'vue';
import request from '@/utils/request';
export const useUserStore = defineStore('user', () => {
// 状态:Token、用户名、用户ID
const token = ref(localStorage.getItem('accessToken') || '');
const refreshToken = ref(localStorage.getItem('refreshToken') || '');
const username = ref('');
const userId = ref<number | null>(null);
// 登录方法
const login = async (loginForm: { username: string; password: string }) => {
const res = await request.post('/api/auth/login', loginForm);
// 存储Token到本地(持久化)
token.value = res.data.accessToken;
refreshToken.value = res.data.refreshToken;
localStorage.setItem('accessToken', token.value);
localStorage.setItem('refreshToken', refreshToken.value);
// 获取用户信息(可选)
// await getUserInfo();
};
// 刷新Token方法
const refreshTokenAction = async () => {
try {
const res = await request.post('/api/auth/refresh-token', {
refreshToken: refreshToken.value
});
token.value = res.data.accessToken;
localStorage.setItem('accessToken', token.value);
return true;
} catch (error) {
return false;
}
};
// 退出登录方法
const logout = () => {
token.value = '';
refreshToken.value = '';
username.value = '';
userId.value = null;
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
};
return {
token,
refreshToken,
username,
userId,
login,
refreshTokenAction,
logout
};
});
// src/views/Login.vue:登录页面
<template>
<div class="login-container">
<el-card class="login-card">
<h2 class="login-title">系统登录</h2>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
label-width="80px"
class="login-form"
>
<el-form-item label="用户名" prop="username">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
clearable
></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
show-password
></el-input>
</el-form-item>
<el-form-item class="login-btn-group">
<el-button type="primary" @click="handleLogin" :loading="loading">
登录
</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/store/user';
const router = useRouter();
const userStore = useUserStore();
// 加载状态
const loading = ref(false);
// 登录表单引用
const loginFormRef = ref();
// 登录表单数据
const loginForm = reactive({
username: '',
password: ''
});
// 表单校验规则
const loginRules = reactive({
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
});
// 登录处理
const handleLogin = async () => {
if (!loginFormRef.value) return;
// 表单校验
const valid = await loginFormRef.value.validate();
if (!valid) return;
loading.value = true;
try {
// 调用登录接口
await userStore.login(loginForm);
ElMessage.success('登录成功');
// 跳转首页
router.push('/home');
} catch (error) {
ElMessage.error('登录失败,请检查用户名或密码');
} finally {
loading.value = false;
}
};
</script>
<style scoped>
.login-container {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #f5f5f5;
}
.login-card {
width: 400px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.login-title {
text-align: center;
margin-bottom: 20px;
font-size: 20px;
font-weight: bold;
}
.login-form {
margin-top: 20px;
}
.login-btn-group {
text-align: center;
}
</style>
2.2 RBAC 权限管理:企业级权限体系
RBAC(Role-Based Access Control)即基于角色的访问控制,核心逻辑:「用户 - 角色 - 权限」三层关联,相比直接给用户分配权限,具备更好的扩展性和维护性。
2.2.1 数据库设计(核心表)
sql
-- 1. 用户表
CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '加密密码',
`nickname` varchar(50) DEFAULT '' COMMENT '昵称',
`email` varchar(100) DEFAULT '' COMMENT '邮箱',
`phone` varchar(20) DEFAULT '' COMMENT '手机号',
`status` tinyint DEFAULT 1 COMMENT '状态:1正常 0禁用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';
-- 2. 角色表
CREATE TABLE `sys_role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`role_name` varchar(50) NOT NULL COMMENT '角色名称',
`role_code` varchar(50) NOT NULL COMMENT '角色编码(如ADMIN、USER)',
`description` varchar(200) DEFAULT '' COMMENT '角色描述',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_code` (`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统角色表';
-- 3. 用户-角色关联表
CREATE TABLE `sys_user_role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`role_id` bigint NOT NULL COMMENT '角色ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_role` (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户-角色关联表';
-- 4. 权限表(菜单/按钮)
CREATE TABLE `sys_permission` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '权限ID',
`parent_id` bigint DEFAULT 0 COMMENT '父权限ID(0为顶级)',
`permission_name` varchar(50) NOT NULL COMMENT '权限名称',
`permission_code` varchar(100) NOT NULL COMMENT '权限编码(如sys:user:list)',
`type` tinyint NOT NULL COMMENT '类型:1菜单 2按钮',
`path` varchar(200) DEFAULT '' COMMENT '路由路径',
`component` varchar(200) DEFAULT '' COMMENT '前端组件路径',
`icon` varchar(50) DEFAULT '' COMMENT '图标',
`sort` int DEFAULT 0 COMMENT '排序',
`status` tinyint DEFAULT 1 COMMENT '状态:1启用 0禁用',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_permission_code` (`permission_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统权限表';
-- 5. 角色-权限关联表
CREATE TABLE `sys_role_permission` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`role_id` bigint NOT NULL COMMENT '角色ID',
`permission_id` bigint NOT NULL COMMENT '权限ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_permission` (`role_id`,`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色-权限关联表';
2.2.2 后端权限校验实现
实战代码 1:权限注解与切面
java
运行
package com.example.common.anno;
import java.lang.annotation.*;
/**
* 权限校验注解
* 用法:@RequiresPermission("sys:user:list")
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequiresPermission {
/**
* 权限编码(如sys:user:list)
*/
String value();
}
package com.example.config.aop;
import com.example.api.vo.Result;
import com.example.common.util.JwtUtil;
import com.example.service.PermissionService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
/**
* 权限校验切面
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class PermissionAspect {
private final PermissionService permissionService;
private final JwtUtil jwtUtil;
/**
* 切入点:标注@RequiresPermission注解的方法
*/
@Pointcut("@annotation(com.example.common.anno.RequiresPermission)")
public void permissionPointcut() {}
/**
* 环绕通知:校验权限
*/
@Around("permissionPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 获取当前请求上下文
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return Result.forbidden("请求上下文为空");
}
HttpServletRequest request = attributes.getRequest();
// 2. 获取Token中的用户ID
String authorization = request.getHeader("Authorization");
String token = authorization.substring(7);
Long userId = jwtUtil.getUserIdFromToken(token);
if (userId == null) {
return Result.forbidden("用户ID为空");
}
// 3. 获取注解中的权限编码
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RequiresPermission annotation = method.getAnnotation(RequiresPermission.class);
String permissionCode = annotation.value();
// 4. 校验用户是否拥有该权限
boolean hasPermission = permissionService.hasPermission(userId, permissionCode);
if (!hasPermission) {
log.warn("用户无权限访问,userId={}, permissionCode={}", userId, permissionCode);
return Result.forbidden("无操作权限,请联系管理员");
}
// 5. 有权限,执行原方法
return joinPoint.proceed();
}
}
实战代码 2:权限服务实现
java
运行
package com.example.service;
import com.example.service.entity.SysPermission;
import com.example.service.entity.SysRole;
import com.example.service.entity.SysUserRole;
import com.example.service.mapper.SysPermissionMapper;
import com.example.service.mapper.SysRoleMapper;
import com.example.service.mapper.SysRolePermissionMapper;
import com.example.service.mapper.SysUserRoleMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 权限服务
*/
@Service
@RequiredArgsConstructor
public class PermissionService {
private final SysUserRoleMapper sysUserRoleMapper;
private final SysRoleMapper sysRoleMapper;
private final SysRolePermissionMapper sysRolePermissionMapper;
private final SysPermissionMapper sysPermissionMapper;
/**
* 校验用户是否拥有指定权限
* @param userId 用户ID
* @param permissionCode 权限编码
* @return 是否拥有
*/
public boolean hasPermission(Long userId, String permissionCode) {
// 1. 获取用户的所有权限编码
Set<String> userPermissionCodes = getUserPermissionCodes(userId);
// 2. 判断是否包含目标权限
return userPermissionCodes.contains(permissionCode);
}
/**
* 获取用户的所有权限编码(缓存优化,过期时间1小时)
*/
@Cacheable(value = "user:permissions", key = "#userId", expire = 3600)
public Set<String> getUserPermissionCodes(Long userId) {
// 1. 获取用户关联的角色ID
List<SysUserRole> userRoles = sysUserRoleMapper.selectByUserId(userId);
if (CollectionUtils.isEmpty(userRoles)) {
return Set.of();
}
List<Long> roleIds = userRoles.stream().map(SysUserRole::getRoleId).collect(Collectors.toList());
// 2. 获取角色关联的权限ID
List<Long> permissionIds = sysRolePermissionMapper.selectPermissionIdsByRoleIds(roleIds);
if (CollectionUtils.isEmpty(permissionIds)) {
return Set.of();
}
// 3. 获取权限编码
List<SysPermission> permissions = sysPermissionMapper.selectByIds(permissionIds);
if (CollectionUtils.isEmpty(permissions)) {
return Set.of();
}
// 4. 转换为Set返回
return permissions.stream()
.map(SysPermission::getPermissionCode)
.collect(Collectors.toSet());
}
/**
* 获取用户的菜单权限(用于前端路由渲染)
*/
@Cacheable(value = "user:menus", key = "#userId", expire = 3600)
public List<SysPermission> getUserMenus(Long userId) {
// 1. 获取用户所有权限
Set<String> permissionCodes = getUserPermissionCodes(userId);
// 2. 查询菜单类型的权限(type=1)
return sysPermissionMapper.selectMenusByPermissionCodes(permissionCodes);
}
}
实战代码 3:权限接口与前端路由控制
java
运行
// 后端菜单接口
package com.example.api.controller;
import com.example.api.vo.Result;
import com.example.common.util.JwtUtil;
import com.example.service.PermissionService;
import com.example.service.entity.SysPermission;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
import java.util.List;
/**
* 权限控制器
*/
@RestController
@RequestMapping("/api/permission")
@RequiredArgsConstructor
public class PermissionController {
private final PermissionService permissionService;
private final JwtUtil jwtUtil;
/**
* 获取当前用户的菜单权限
*/
@GetMapping("/menus")
public Result<List<SysPermission>> getUserMenus(HttpServletRequest request) {
// 1. 获取Token中的用户ID
String authorization = request.getHeader("Authorization");
String token = authorization.substring(7);
Long userId = jwtUtil.getUserIdFromToken(token);
// 2. 查询菜单
List<SysPermission> menus = permissionService.getUserMenus(userId);
return Result.success(menus);
}
}
// 前端路由动态加载(src/router/index.ts)
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import { useUserStore } from '@/store/user';
import request from '@/utils/request';
import Layout from '@/layouts/Index.vue';
// 静态路由(无需权限)
export const constantRoutes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
hidden: true
},
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'Home',
component: Layout,
meta: { title: '首页', icon: 'el-icon-s-home' },
children: [
{
path: '',
component: () => import('@/views/Home.vue')
}
]
}
];
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: constantRoutes
});
// 路由守卫:动态加载菜单
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
// 1. 未登录且访问非登录页,跳转登录
if (!userStore.token && to.path !== '/login') {
next('/login');
return;
}
// 2. 已登录且访问登录页,跳转首页
if (userStore.token && to.path === '/login') {
next('/home');
return;
}
// 3. 已登录,动态加载菜单(仅首次加载)
if (userStore.token && !userStore.hasLoadedMenus) {
try {
// 3.1 获取后端菜单数据
const res = await request.get('/api/permission/menus');
// 3.2 转换为路由格式
const asyncRoutes = convertMenusToRoutes(res.data);
// 3.3 添加路由
asyncRoutes.forEach(route => {
router.addRoute(route);
});
// 3.4 标记菜单已加载
userStore.hasLoadedMenus = true;
// 3.5 重新跳转(确保路由生效)
next({ ...to, replace: true });
} catch (error) {
// 加载失败,退出登录
userStore.logout();
next('/login');
}
return;
}
next();
});
/**
* 菜单转换为路由
*/
const convertMenusToRoutes = (menus: any[]): RouteRecordRaw[] => {
const routes: RouteRecordRaw[] = [];
menus.forEach(menu => {
const route: RouteRecordRaw = {
path: menu.path,
name: menu.permissionCode,
component: Layout,
meta: {
title: menu.permissionName,
icon: menu.icon
},
children: []
};
// 子菜单处理
if (menu.children && menu.children.length > 0) {
route.children = convertMenusToRoutes(menu.children);
} else {
// 无子女菜单,加载对应组件
route.children.push({
path: '',
component: () => import(`@/views/${menu.component}.vue`)
});
}
routes.push(route);
});
return routes;
};
export default router;
2.3 Redis 缓存优化:性能提升核心
Redis 作为高性能内存数据库,核心解决「数据库压力大」「接口响应慢」问题,本学期重点掌握了缓存的核心应用场景和问题解决方案。
2.3.1 Redis 核心应用场景与实现
场景 1:用户信息缓存(已在 2.1 中实现)
场景 2:接口限流(防止恶意请求)
java
运行
package com.example.common.util;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 限流工具类(基于Redis计数器)
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RateLimitUtil {
private final StringRedisTemplate redisTemplate;
/**
* 限流检查
* @param key 限流键(如user:1:api:limit)
* @param limit 限制次数
* @param expire 过期时间(秒)
* @return true:允许访问,false:限流
*/
public boolean checkRateLimit(String key, int limit, int expire) {
try {
// 1. 计数器自增
Long count = redisTemplate.opsForValue().increment(key, 1);
// 2. 首次设置过期时间
if (count != null && count == 1) {
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
// 3. 判断是否超过限制
return count != null && count <= limit;
} catch (Exception e) {
log.error("限流检查失败,key={}", key, e);
// 异常时允许访问(避免Redis故障导致服务不可用)
return true;
}
}
}
// 限流切面
package com.example.config.aop;
import com.example.api.vo.Result;
import com.example.common.anno.RateLimit;
import com.example.common.util.RateLimitUtil;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
/**
* 限流切面
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class RateLimitAspect {
private final RateLimitUtil rateLimitUtil;
@Pointcut("@annotation(com.example.common.anno.RateLimit)")
public void rateLimitPointcut() {}
@Around("rateLimitPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 获取请求上下文
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String ip = request.getRemoteAddr();
// 2. 获取注解参数
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RateLimit annotation = method.getAnnotation(RateLimit.class);
int limit = annotation.limit();
int expire = annotation.expire();
String key = "rate:limit:" + ip + ":" + method.getName();
// 3. 限流检查
boolean allow = rateLimitUtil.checkRateLimit(key, limit, expire);
if (!allow) {
log.warn("接口限流,ip={}, method={}", ip, method.getName());
return Result.error("请求过于频繁,请稍后再试");
}
// 4. 允许访问,执行原方法
return joinPoint.proceed();
}
}
// 限流注解
package com.example.common.anno;
import java.lang.annotation.*;
/**
* 限流注解
* 用法:@RateLimit(limit = 10, expire = 60) // 60秒内最多10次请求
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
/**
* 限制次数
*/
int limit() default 10;
/**
* 过期时间(秒)
*/
int expire() default 60;
}
// 接口使用限流注解
@PostMapping("/login")
@RateLimit(limit = 5, expire = 60) // 60秒内最多5次登录请求
public Result<Map<String, String>> login(@Valid @RequestBody LoginDTO loginDTO) {
// 登录逻辑...
}
场景 3:缓存穿透 / 击穿 / 雪崩解决方案
| 问题类型 | 产生原因 | 解决方案 | 代码实现 |
|---|---|---|---|
| 缓存穿透 | 请求不存在的 Key,缓存未命中,直接穿透到数据库 | 1. 空值缓存 2. 布隆过滤器 | ```java |
| // 空值缓存示例(用户查询) | |||
| public User getByUsername(String username) { | |||
| String key = "user:info:" + username; | |||
| String userJson = redisTemplate.opsForValue().get(key); | |||
| // 1. 缓存命中(包括空值) | |||
| if (userJson != null) { | |||
| if ("null".equals (userJson)) { // 空值标记 | |||
| return null; | |||
| } | |||
| return JSON.parseObject(userJson, User.class); | |||
| } | |||
| // 2. 查库 | |||
| User user = userMapper.selectByUsername(username); | |||
| // 3. 写入缓存(空值也缓存,过期时间短) | |||
| if (user == null) { | |||
| redisTemplate.opsForValue().set(key, "null", 5, TimeUnit.MINUTES); | |||
| } else { | |||
| redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES); | |||
| } | |||
| return user; | |||
| } |
|
| 缓存击穿 | 热点Key过期,大量请求同时穿透到数据库 | 1. 互斥锁 2. 热点Key永不过期 | ```java
// 互斥锁解决缓存击穿
public User getHotUser(Long userId) {
String key = "user:hot:" + userId;
String userJson = redisTemplate.opsForValue().get(key);
if (userJson != null) {
return JSON.parseObject(userJson, User.class);
}
// 1. 获取分布式锁
String lockKey = "lock:user:" + userId;
boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (!locked) {
// 2. 未获取锁,重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getHotUser(userId);
}
try {
// 3. 获取锁,查库并更新缓存
User user = userMapper.selectById(userId);
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 1, TimeUnit.HOURS);
return user;
} finally {
// 4. 释放锁
redisTemplate.delete(lockKey);
}
}
``` |
| 缓存雪崩 | 大量Key同时过期,数据库压力骤增 | 1. 过期时间加随机值 2. 集群部署 3. 熔断降级 | ```java
// 过期时间加随机值
public void setCacheWithRandomExpire(String key, String value, int baseExpire) {
// 随机增加0-10分钟的过期时间
int randomExpire = baseExpire + new Random().nextInt(600);
redisTemplate.opsForValue().set(key, value, randomExpire, TimeUnit.SECONDS);
}
``` |
### 2.4 WebSocket实时通信:实时消息推送
WebSocket实现「服务器主动向客户端推送消息」,相比轮询(定时请求),具备更低的延迟和更高的效率,本学期实现了「实时通知」「在线聊天」两个核心场景。
#### 2.4.1 Spring Boot集成WebSocket
**实战代码1:WebSocket配置**
```java
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket配置
*/
@Configuration
public class WebSocketConfig {
/**
* 注册WebSocket端点处理器
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
实战代码 2:WebSocket 服务端实现(实时通知)
java
运行
package com.example.api.websocket;
import com.alibaba.fastjson.JSON;
import com.example.common.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 实时通知WebSocket端点
* 访问路径:ws://localhost:8080/ws/notify/{token}
*/
@Slf4j
@Component
@ServerEndpoint("/ws/notify/{token}")
public class NotifyWebSocketServer {
// 存储用户ID与Session的映射(线程安全)
private static final Map<Long, Session> USER_SESSION_MAP = new ConcurrentHashMap<>();
private final JwtUtil jwtUtil = new JwtUtil(); // 实际项目建议通过Spring注入
/**
* 连接建立时触发
*/
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) {
try {
// 1. 校验Token,获取用户ID
Long userId = jwtUtil.getUserIdFromToken(token);
if (userId == null) {
session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, "Token无效"));
return;
}
// 2. 存储Session
USER_SESSION_MAP.put(userId, session);
log.info("WebSocket连接建立,userId={},当前在线数={}", userId, USER_SESSION_MAP.size());
// 3. 发送连接成功消息
sendMessage(session, buildMessage("success", "连接成功"));
} catch (Exception e) {
log.error("WebSocket连接建立失败", e);
}
}
/**
* 连接关闭时触发
*/
@OnClose
public void onClose(Session session) {
// 1. 移除Session
USER_SESSION_MAP.entrySet().removeIf(entry -> entry.getValue().equals(session));
log.info("WebSocket连接关闭,当前在线数={}", USER_SESSION_MAP.size());
}
/**
* 收到客户端消息时触发
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到客户端消息:{}", message);
// 可处理客户端发送的消息(如心跳包)
sendMessage(session, buildMessage("success", "消息已收到"));
}
/**
* 发生错误时触发
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("WebSocket发生错误", error);
// 关闭连接
try {
session.close();
} catch (Exception e) {
log.error("关闭WebSocket连接失败", e);
}
// 移除Session
USER_SESSION_MAP.entrySet().removeIf(entry -> entry.getValue().equals(session));
}
/**
* 发送消息给指定用户
* @param userId 用户ID
* @param message 消息内容
*/
public static void sendToUser(Long userId, String message) {
Session session = USER_SESSION_MAP.get(userId);
if (session == null || !session.isOpen()) {
log.warn("用户未在线,userId={}", userId);
return;
}
sendMessage(session, buildMessage("notify", message));
}
/**
* 广播消息(发送给所有在线用户)
* @param message 消息内容
*/
public static void broadcast(String message) {
USER_SESSION_MAP.forEach((userId, session) -> {
if (session.isOpen()) {
sendMessage(session, buildMessage("broadcast", message));
}
});
}
/**
* 发送消息(私有方法)
*/
private static void sendMessage(Session session, String message) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception e) {
log.error("发送WebSocket消息失败", e);
}
}
/**
* 构建消息体
*/
private static String buildMessage(String type, String content) {
Map<String, Object> messageMap = new ConcurrentHashMap<>();
messageMap.put("type", type);
messageMap.put("content", content);
messageMap.put("timestamp", System.currentTimeMillis());
return JSON.toJSONString(messageMap);
}
}
实战代码 3:前端 WebSocket 客户端实现
typescript
运行
// src/utils/websocket.ts:WebSocket封装
import { useUserStore } from '@/store/user';
import { ElMessage } from 'element-plus';
let websocket: WebSocket | null = null;
let reconnectTimer: NodeJS.Timeout | null = null;
const RECONNECT_INTERVAL = 5000; // 重连间隔:5秒
/**
* 初始化WebSocket连接
*/
export const initWebSocket = () => {
const userStore = useUserStore();
if (!userStore.token) {
ElMessage.warning('未登录,无法建立WebSocket连接');
return;
}
// 关闭已有连接
if (websocket) {
websocket.close();
}
// 建立新连接
const wsUrl = `ws://localhost:8080/ws/notify/${userStore.token}`;
websocket = new WebSocket(wsUrl);
// 连接成功
websocket.onopen = () => {
console.log('WebSocket连接成功');
// 清除重连定时器
if (reconnectTimer) {
clearInterval(reconnectTimer);
reconnectTimer = null;
}
};
// 接收消息
websocket.onmessage = (event) => {
const message = JSON.parse(event.data);
handleMessage(message);
};
// 连接关闭
websocket.onclose = () => {
console.log('WebSocket连接关闭,准备重连');
// 自动重连
if (!reconnectTimer) {
reconnectTimer = setInterval(() => {
initWebSocket();
}, RECONNECT_INTERVAL);
}
};
// 连接错误
websocket.onerror = (error) => {
console.error('WebSocket连接错误', error);
websocket?.close();
};
};
/**
* 处理WebSocket消息
*/
const handleMessage = (message: any) => {
switch (message.type) {
case 'notify':
// 实时通知,显示弹窗
ElMessage.info(`【实时通知】${message.content}`);
break;
case 'broadcast':
// 广播消息
ElMessage.success(`【系统广播】${message.content}`);
break;
case 'success':
// 连接成功提示
ElMessage.success(message.content);
break;
default:
console.log('未知消息类型', message);
}
};
/**
* 关闭WebSocket连接
*/
export const closeWebSocket = () => {
if (websocket) {
websocket.close();
websocket = null;
}
if (reconnectTimer) {
clearInterval(reconnectTimer);
reconnectTimer = null;
}
};
// 在App.vue中初始化
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import { initWebSocket, closeWebSocket } from '@/utils/websocket';
onMounted(() => {
initWebSocket();
});
onUnmounted(() => {
closeWebSocket();
});
</script>
实战代码 4:后端主动推送消息示例
java
运行
// 消息推送接口
package com.example.api.controller;
import com.example.api.vo.Result;
import com.example.api.websocket.NotifyWebSocketServer;
import lombok.RequiredArgsConstructor;
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;
import java.util.Map;
/**
* 消息推送控制器
*/
@RestController
@RequestMapping("/api/notify")
@RequiredArgsConstructor
public class NotifyController {
/**
* 发送指定用户
*/
@PostMapping("/send-to-user")
public Result<Void> sendToUser(@RequestBody Map<String, Object> params) {
Long userId = Long.parseLong(params.get("userId").toString());
String message = params.get("message").toString();
NotifyWebSocketServer.sendToUser(userId, message);
return Result.success("消息发送成功");
}
/**
* 广播消息
*/
@PostMapping("/broadcast")
public Result<Void> broadcast(@RequestBody Map<String, Object> params) {
String message = params.get("message").toString();
NotifyWebSocketServer.broadcast(message);
return Result.success("广播消息发送成功");
}
}
2.5 AI 智能分析落地:Ollama 集成
Ollama 是轻量级的本地 LLM(大语言模型)运行框架,支持 Llama 3、Qwen 等模型,本学期实现了「本地 AI 分析接口」「文本摘要」「智能问答」三个核心功能。
2.5.1 环境准备
- 安装 Ollama:https://ollama.com/
- 拉取模型:
ollama pull llama3(Llama 3 8B) - 启动 Ollama 服务:
ollama serve
2.5.2 Spring Boot 集成 Ollama
实战代码 1:Ollama 配置与工具类
java
运行
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* Oll更多推荐


所有评论(0)