前言

本学期围绕 Web 应用程序开发展开了系统性学习,核心聚焦 Spring Boot+Vue+MySQL 全栈技术体系,同时延伸学习了 Redis 缓存优化、WebSocket 实时通信、JWT 认证、RBAC 权限管理、AI 能力集成(Ollama)、应用测试体系搭建等关键技术。本文并非简单的知识点罗列,而是结合10 + 实战场景百余个可落地代码片段企业级工程规范,从原理剖析、代码实现、问题排查、性能优化四个维度,完整还原从「需求分析」到「部署运维」的全流程,所有内容均为原创实战总结,力求达到 90 分以上的专业深度与实用性。

本文总字数超 15000 字,核心结构如下:

  1. 技术栈底层原理与工程化规范
  2. 核心模块实战(认证、权限、缓存、实时通信、AI 集成)
  3. 前端可视化与交互优化
  4. 全链路测试体系搭建
  5. 企业级部署与运维
  6. 常见问题与性能优化实战
  7. 学习心得与进阶方向

一、技术栈底层原理与工程化规范

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 环境准备
  1. 安装 Ollama:https://ollama.com/
  2. 拉取模型:ollama pull llama3(Llama 3 8B)
  3. 启动 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
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐