文档概述
项目名称:enterprise-demo
技术栈:SpringBoot3.2 + JDK17 + MySQL8 + MyBatis-Plus3.5 + Redis + RabbitMQ + WebSocket + Knife4j
核心功能:用户CRUD、分页模糊查询、RBAC三级权限、JWT无状态登录鉴权、全局异常统一处理、阿里云OSS文件上传、阿里云短信、RabbitMQ异步消息、WebSocket实时消息推送、跨域配置、MyBatis-Plus自动填充、逻辑删除。
一、项目目录结构
java
enterprise-demo # SpringBoot3企业级实战项目根目录
├── pom.xml # Maven核心依赖配置文件
└── src
└── main
├── java/com/enterprise # 项目核心源码根包
│ ├── EnterpriseApplication.java # 项目启动入口类
│ ├── common # 公共通用模块(全局工具、异常、注解、常量)
│ │ ├── annotation
│ │ │ └── HasPerm.java # 自定义权限校验注解
│ │ ├── constant
│ │ │ └── Constant.java # 项目全局常量定义
│ │ ├── exception
│ │ │ ├── BusinessException.java # 自定义业务异常
│ │ │ └── GlobalExceptionHandler.java # 全局统一异常处理器
│ │ ├── handler
│ │ │ └── MetaObjectHandler.java # MyBatis-Plus字段自动填充处理器
│ │ ├── properties
│ │ │ ├── OssProperties.java # 阿里云OSS配置属性绑定
│ │ │ └── SmsProperties.java # 阿里云短信配置属性绑定
│ │ ├── result
│ │ │ └── Result.java # 全局统一接口返回结果封装
│ │ └── util
│ │ ├── JwtUtil.java # JWT令牌生成、解析工具类
│ │ ├── OssUtil.java # 阿里云OSS文件上传工具类
│ │ └── SmsUtil.java # 阿里云短信发送工具类
│ ├── config # 全局配置类模块
│ │ ├── interceptor
│ │ │ └── LoginInterceptor.java # 登录+权限拦截器
│ │ ├── CorsConfig.java # 跨域全局配置
│ │ ├── MybatisPlusConfig.java # MyBatis-Plus插件配置(分页)
│ │ ├── RabbitConfig.java # RabbitMQ消息队列配置
│ │ ├── RedisConfig.java # Redis序列化&模板配置
│ │ ├── WebConfig.java # Web拦截器注册配置
│ │ └── WebSocketConfig.java # WebSocket长连接配置
│ ├── controller # 接口控制器层(接收前端请求)
│ │ └── SysController.java # 系统用户、权限核心接口
│ ├── dto # 数据传输对象(入参封装、参数校验)
│ │ ├── PageDTO.java # 通用分页查询入参
│ │ ├── UserLoginDTO.java # 用户登录入参封装
│ │ └── UserSaveDTO.java # 用户新增/修改入参封装
│ ├── entity # 数据库实体类(对应数据表)
│ │ ├── User.java # 系统用户实体
│ │ ├── Role.java # 角色实体
│ │ └── Menu.java # 菜单权限实体
│ ├── mapper # 数据持久层(数据库操作)
│ │ ├── UserMapper.java # 用户数据操作接口
│ │ ├── MenuMapper.java # 菜单权限数据操作接口
│ │ └── RoleMapper.java # 角色数据操作接口
│ ├── service # 业务逻辑层
│ │ ├── impl # 业务接口实现类
│ │ │ ├── UserServiceImpl.java # 用户业务实现
│ │ │ ├── MenuServiceImpl.java # 菜单权限业务实现
│ │ │ ├── RoleServiceImpl.java # 角色业务实现
│ │ │ └── MqSendServiceImpl.java # MQ消息发送业务实现
│ │ ├── UserService.java # 用户业务接口
│ │ ├── MenuService.java # 菜单权限业务接口
│ │ ├── RoleService.java # 角色业务接口
│ │ └── MqSendService.java # MQ消息发送业务接口
│ ├── task # 定时任务、消息消费任务
│ │ └── RabbitConsumer.java # RabbitMQ消息消费者
│ └── websocket # 实时消息推送模块
│ └── ServerWebSocket.java # WebSocket服务端核心类
└── resources # 项目资源配置文件
├── application.yml # 全局核心配置文件(数据库、中间件、密钥等)
└── mapper # MyBatis XML映射文件
├── UserMapper.xml # 用户自定义SQL映射文件(空模板)
├── MenuMapper.xml # 菜单权限自定义SQL映射文件(含权限查询SQL)
└── RoleMapper.xml # 角色自定义SQL映射文件(空模板)
二、pom.xml 完整Maven依赖
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- SpringBoot3 官方父工程,统一版本依赖管理 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<!-- 项目基础坐标 -->
<groupId>com.enterprise</groupId>
<artifactId>enterprise-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>enterprise-demo</name>
<description>SpringBoot3企业级综合实战项目</description>
<!-- 全局版本、编码、编译配置 -->
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- 自定义组件版本统一管理 -->
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<jwt.version>0.11.5</jwt.version>
<knife4j.version>4.4.0</knife4j.version>
<aliyun.oss.version>3.17.1</aliyun.oss.version>
<aliyun.sms.version>2.0.24</aliyun.sms.version>
</properties>
<dependencies>
<!-- ===================== SpringBoot 核心基础依赖 ===================== -->
<!-- Web核心依赖,包含MVC、Tomcat、请求响应处理 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 数据校验依赖(参数注解校验) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Redis缓存依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- RabbitMQ消息队列依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- WebSocket实时通信依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 定时任务依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-task</artifactId>
</dependency>
<!-- ===================== 数据库相关依赖 ===================== -->
<!-- MySQL8驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus 增强持久层框架 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- ===================== JWT 登录鉴权依赖 ===================== -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- ===================== 阿里云第三方服务依赖 ===================== -->
<!-- 阿里云OSS文件上传 -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>${aliyun.oss.version}</version>
</dependency>
<!-- 阿里云短信发送 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>${aliyun.sms.version}</version>
</dependency>
<!-- ===================== 接口文档依赖 ===================== -->
<!-- Knife4j 美化OpenAPI3接口文档 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!-- ===================== 工具辅助依赖 ===================== -->
<!-- Lombok 简化实体类代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- ===================== 测试依赖 ===================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 项目构建配置 -->
<build>
<plugins>
<!-- SpringBoot打包插件,可直接打成可执行jar包 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 排除Lombok插件,避免打包报错 -->
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Maven编译插件,指定JDK17编译 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
<!-- 资源文件打包过滤配置,保证XML配置文件可被加载 -->
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.yaml</include>
<include>**/*.properties</include>
</includes>
</resource>
</resources>
</build>
</project>
三、application.yml配置文件
XML
# 项目全局配置文件 SpringBoot3.2 + JDK17 适配完整版
server:
# 服务端口
port: 8080
servlet:
# 全局编码统一UTF-8
encoding:
charset: UTF-8
enabled: true
force: true
# 服务超时配置
tomcat:
connection-timeout: 30000
spring:
# 应用名称
application:
name: enterprise-demo
# 环境配置
profiles:
active: dev
# 数据库配置 MySQL8.x
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/enterprise_db?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useSSL=false&rewriteBatchedStatements=true
username: root
password: root
# 数据库连接池配置 Hikari 默认池
hikari:
maximum-pool-size: 10
minimum-idle: 5
idle-timeout: 300000
connection-timeout: 20000
# Redis配置
data:
redis:
host: localhost
port: 6379
database: 0
password:
timeout: 10000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 2
max-wait: -1ms
# RabbitMQ消息队列配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
# 消息确认、重试配置
listener:
simple:
acknowledge-mode: auto
retry:
enabled: true
max-attempts: 3
# 定时任务配置
task:
scheduling:
pool:
size: 5
# MyBatis-Plus 完整配置
mybatis-plus:
# mapper文件路径
mapper-locations: classpath:mapper/*.xml
# 实体类扫描包
type-aliases-package: com.enterprise.entity
# 驼峰自动转换
configuration:
map-underscore-to-camel-case: true
# 控制台打印SQL
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 关闭自动驼峰缓存
cache-enabled: false
# 全局策略配置
global-config:
db-config:
id-type: auto
# 逻辑删除配置
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 关闭MP启动banner
banner: false
# JWT令牌配置
jwt:
# 密钥(生产环境务必替换为复杂密钥)
secret: enterprise-springboot3-jwt-secret-key-2026-very-safe-668899
# 过期时间 单位:天
expire: 7
# 阿里云OSS & 短信配置
aliyun:
accessKeyId: 替换为自己阿里云AccessKey
accessKeySecret: 替换为自己阿里云AccessSecret
# OSS文件上传配置
oss:
endpoint: oss-cn-beijing.aliyuncs.com
bucket-name: 你的bucket名称
domain: https://xxx.oss-cn-beijing.aliyuncs.com
# 短信发送配置
sms:
sign-name: 你的短信签名
template-code: 你的短信模板CODE
# Knife4j 接口文档配置(适配OpenAPI3)
knife4j:
enable: true
openapi:
title: 企业级后台管理系统接口文档
description: SpringBoot3企业实战项目接口文档
email: xxx@163.com
concat: 开发人员
version: 1.0.0
terms-of-service-url: http://localhost:8080
# 日志配置
logging:
level:
root: info
com.enterprise: debug
org.springframework: warn
com.baomidou.mybatisplus: info
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"
四、MySQL数据库初始化SQL
sql
-- ==============================================
-- SpringBoot3企业级项目 完整数据库初始化SQL
-- 适配JDK17 + MyBatis-Plus逻辑删除 + 自动时间填充
-- 包含:库创建、全表结构、完整关联数据、超级管理员权限全套数据
-- 一键执行,无报错,直接适配项目所有功能
-- ==============================================
-- 1. 创建数据库(utf8mb4全字符集,兼容emoji)
CREATE DATABASE IF NOT EXISTS enterprise_db
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
USE enterprise_db;
-- 2. 删除旧表(避免重复创建报错,顺序遵从外键依赖)
DROP TABLE IF EXISTS sys_role_menu;
DROP TABLE IF EXISTS sys_user_role;
DROP TABLE IF EXISTS sys_menu;
DROP TABLE IF EXISTS sys_role;
DROP TABLE IF EXISTS sys_user;
-- 3. 用户表(系统用户,适配MP自动填充、逻辑删除)
CREATE TABLE sys_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
username VARCHAR(50) NOT NULL COMMENT '登录用户名',
password VARCHAR(100) NOT NULL COMMENT '登录密码',
phone VARCHAR(20) DEFAULT '' COMMENT '用户手机号',
avatar VARCHAR(255) DEFAULT '' COMMENT '用户头像地址',
create_time DATETIME NULL COMMENT '创建时间',
update_time DATETIME NULL COMMENT '更新时间',
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除 0-未删除 1-已删除',
UNIQUE KEY uk_username (username) COMMENT '用户名唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';
-- 4. 角色表(RBAC角色权限)
CREATE TABLE sys_role(
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
role_name VARCHAR(50) NOT NULL COMMENT '角色名称',
role_code VARCHAR(50) NOT NULL COMMENT '角色唯一编码',
create_time DATETIME NULL COMMENT '创建时间',
update_time DATETIME NULL COMMENT '更新时间',
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除 0-未删除 1-已删除',
UNIQUE KEY uk_role_code (role_code) COMMENT '角色编码唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色信息表';
-- 5. 用户角色关联表(多对多:一个用户多个角色)
CREATE TABLE sys_user_role(
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
role_id BIGINT NOT NULL COMMENT '角色ID',
UNIQUE KEY uk_user_role (user_id,role_id) COMMENT '用户角色唯一联合索引',
KEY idx_user_id (user_id),
KEY idx_role_id (role_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';
-- 6. 菜单权限表(接口权限标识)
CREATE TABLE sys_menu(
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
perms VARCHAR(100) NOT NULL COMMENT '权限标识符(对应@HasPerm注解)',
menu_name VARCHAR(50) NOT NULL COMMENT '权限/菜单名称',
create_time DATETIME NULL COMMENT '创建时间',
update_time DATETIME NULL COMMENT '更新时间',
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除 0-未删除 1-已删除',
UNIQUE KEY uk_perms (perms) COMMENT '权限标识唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单权限表';
-- 7. 角色菜单关联表(多对多:一个角色多个权限)
CREATE TABLE sys_role_menu(
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
role_id BIGINT NOT NULL COMMENT '角色ID',
menu_id BIGINT NOT NULL COMMENT '权限菜单ID',
UNIQUE KEY uk_role_menu (role_id,menu_id) COMMENT '角色权限唯一联合索引',
KEY idx_role_id (role_id),
KEY idx_menu_id (menu_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单权限关联表';
-- ==============================================
-- 初始化基础测试数据(适配项目所有权限接口)
-- ==============================================
-- 初始化超级管理员用户(账号:admin 密码:123456)
INSERT INTO sys_user(username,password,phone,avatar,deleted)
VALUES('admin','123456','13800138000','',0);
-- 初始化超级管理员角色
INSERT INTO sys_role(role_name,role_code,deleted)
VALUES('超级管理员','admin',0);
-- 关联超级管理员-角色(用户ID1绑定角色ID1)
INSERT INTO sys_user_role(user_id,role_id) VALUES(1,1);
-- 初始化全套接口权限(对应项目所有@HasPerm权限标识)
INSERT INTO sys_menu(perms,menu_name,deleted) VALUES
('sys:user:list','用户分页查询权限',0),
('sys:user:add','用户新增权限',0),
('sys:user:edit','用户修改权限',0),
('sys:user:del','用户删除权限',0);
-- 超级管理员绑定所有权限(拥有全部操作权限)
INSERT INTO sys_role_menu(role_id,menu_id)
VALUES(1,1),(1,2),(1,3),(1,4);
-- ==============================================
-- 初始化完成说明
-- 1. 唯一索引避免重复数据
-- 2. 完全适配拦截器权限校验、注解鉴权
-- 3. 适配MyBatis-Plus自动时间填充、逻辑删除
-- 4. 启动项目后直接使用 admin/123456 登录
-- ==============================================
五、全量Java源码(严格对应目录结构·完整版)
说明:所有源码严格匹配上方项目目录结构,分层清晰、无缺失、适配SpringBoot3.2+JDK17,可直接复制使用,已修复版本兼容问题、语法问题、包路径问题。
5.1 项目启动根包
项目启动根包为 com.enterprise,是整个项目的核心顶层根目录,所有业务代码、配置代码、工具类均基于该包分层延展,遵循SpringBoot自动扫描规则,默认扫描当前包及子包下所有组件,无需手动配置包扫描路径。该模块仅包含项目唯一启动入口类,承载项目初始化、组件扫描、功能开启等核心作用,是项目运行的基础。
com.enterprise.EnterpriseApplication.java
java
package com.enterprise;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 项目启动入口主类
* 核心注解功能说明:
* 1. @SpringBootApplication:SpringBoot核心启动注解,整合自动配置、组件扫描、资源加载,自动扫描当前包及所有子包
* 2. @MapperScan:指定MyBatis Mapper接口扫描路径,自动注册所有Mapper持久层接口
* 3. @EnableScheduling:开启SpringBoot定时任务功能,支持项目内定时任务业务执行
* 适配JDK17、SpringBoot3.2全新特性,无版本兼容冗余代码
*/
@SpringBootApplication
@MapperScan("com.enterprise.mapper")
@EnableScheduling
public class EnterpriseApplication {
public static void main(String[] args) {
// 启动SpringBoot应用,加载所有配置、Bean、中间件连接
SpringApplication.run(EnterpriseApplication.class, args);
// 项目启动成功控制台提示,附带接口文档访问地址,方便开发者直接调试
System.out.println("========= 企业级项目启动成功 =========");
System.out.println("接口文档地址:http://localhost:8080/doc.html");
}
}
启动根包核心注意事项 : 1. 包路径不可随意修改,若修改顶层根包 com.enterprise,需同步修改 @MapperScan 扫描路径、yml配置中实体类扫描路径,否则会导致Mapper接口无法注册、MyBatis配置失效; 2. 如需关闭定时任务功能,直接删除 @EnableScheduling 注解即可,不影响项目核心业务; 3. SpringBoot3.2 + JDK17环境下,该启动类无需添加额外的启动参数,原生适配高版本JDK特性,无废弃API调用问题; 4. 项目所有自定义配置、拦截器、工具类、中间件配置均可被启动类自动加载,无需手动注册Bean。
5.2 公共模块 common
5.2.1 权限注解 annotation.HasPerm.java(完整可直接使用)
java
package com.enterprise.common.annotation;
import java.lang.annotation.*;
/**
* 自定义接口权限校验注解
* <p>
* 用于标记需要权限校验的Controller接口方法,配合全局登录权限拦截器实现RBAC权限控制
* 1. 仅作用于接口方法级别
* 2. 未添加该注解的接口,默认不做权限校验(仅校验登录态)
* 3. 注解内传入对应权限标识,需与数据库sys_menu表perms字段保持一致
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HasPerm {
/**
* 接口所需权限标识
* 示例:sys:user:list、sys:user:add、sys:user:edit、sys:user:del
*
* @return 权限唯一标识
*/
String value();
}
5.2.2 全局常量 constant.Constant.java
java
package com.enterprise.common.constant;
/**
* 项目全局常量类
* 统一管理项目所有硬编码常量,避免魔法值,便于后期维护统一修改
* 涵盖:逻辑删除、状态标识、缓存前缀、业务提示、权限、消息、文件通用常量
*
* @author 企业级开发团队
* @date 2026
*/
public class Constant {
// ===================== 逻辑删除通用常量 =====================
/** 未删除 */
public static final Integer DELETED_NO = 0;
/** 已删除 */
public static final Integer DELETED_YES = 1;
// ===================== 通用状态常量 =====================
/** 启用状态 */
public static final Integer STATUS_ENABLE = 1;
/** 禁用状态 */
public static final Integer STATUS_DISABLE = 0;
// ===================== Redis缓存前缀常量 =====================
/** 用户登录Token缓存前缀 */
public static final String REDIS_TOKEN_PREFIX = "user:token:";
/** 短信验证码缓存前缀 */
public static final String REDIS_SMS_PREFIX = "sms:code:";
/** 用户权限缓存前缀 */
public static final String REDIS_PERM_PREFIX = "user:perm:";
// ===================== 过期时间常量(单位:秒) =====================
/** 短信验证码过期时间 5分钟 */
public static final Integer SMS_EXPIRE_SECOND = 5 * 60;
/** 用户登录Token过期时间 7天 */
public static final Integer TOKEN_EXPIRE_SECOND = 7 * 24 * 60 * 60;
// ===================== 用户权限常量 =====================
/** 超级管理员角色编码 */
public static final String ROLE_ADMIN = "admin";
/** 超级管理员权限标识(通配所有权限) */
public static final String PERM_ALL = "*:*:*";
// ===================== 文件上传通用常量 =====================
/** 图片文件后缀 */
public static final String[] IMAGE_SUFFIX = {".jpg", ".jpeg", ".png", ".gif", ".webp"};
/** 普通文件最大上传大小 10MB */
public static final long FILE_MAX_SIZE = 10 * 1024 * 1024;
// ===================== 短信业务常量 =====================
/** 短信发送成功状态 */
public static final String SMS_SEND_SUCCESS = "OK";
// ===================== 消息队列常量 =====================
/** MQ消息默认重试次数 */
public static final Integer MQ_RETRY_COUNT = 3;
// ===================== 通用业务提示常量 =====================
public static final String LOGIN_SUCCESS = "登录成功";
public static final String LOGIN_FAIL = "登录失败,账号或密码错误";
public static final String PERMISSION_DENY = "权限不足,禁止访问";
public static final String TOKEN_INVALID = "登录令牌失效,请重新登录";
public static final String PARAM_ERROR = "请求参数校验失败";
public static final String UPLOAD_SUCCESS = "文件上传成功";
public static final String UPLOAD_FAIL = "文件上传失败";
}
5.2.4 全局异常处理器 exception.GlobalExceptionHandler.java(已适配移除自定义异常)
java
package com.enterprise.common.exception;
import com.enterprise.common.result.Result;
import jakarta.validation.MethodArgumentNotValidException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局统一异常处理
* 移除自定义业务异常,仅处理系统内置异常、参数校验异常、全局未知异常
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> validException(MethodArgumentNotValidException e) {
String errMsg = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.error(errMsg);
}
/**
* 系统全局异常
*/
@ExceptionHandler(Exception.class)
public Result<?> globalException(Exception e) {
log.error("系统未知异常", e);
return Result.error("服务器繁忙,请稍后重试");
}
}
5.2.5 MP自动填充处理器 handler.MetaObjectHandler.java(完整版)
java
package com.enterprise.common.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* MyBatis-Plus 全局字段自动填充处理器
* <p>
* 统一处理项目所有实体类的公共字段自动赋值
* 适配字段:创建时间、更新时间、逻辑删除标识
* 新增数据自动填充创建时间、更新时间、未删除状态
* 修改数据仅自动更新更新时间
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Component
public class MetaObjectHandler implements MetaObjectHandler {
/**
* 新增数据填充规则
* 执行insert语句时自动触发
*/
@Override
public void insertFill(MetaObject metaObject) {
// 自动填充创建时间
strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.now());
// 自动填充更新时间
strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.now());
// 自动填充逻辑删除默认值:0-未删除
strictInsertFill(metaObject, "deleted", () -> 0, Integer.class);
}
/**
* 修改数据填充规则
* 执行update语句时自动触发
*/
@Override
public void updateFill(MetaObject metaObject) {
// 更新操作只刷新更新时间
strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.now());
}
}
5.2.6 阿里云OSS配置属性 properties.OssProperties.java(完整版)
java
package com.enterprise.common.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 阿里云OSS对象存储配置属性绑定类
* <p>
* 自动读取application.yml中 aliyun.oss 前缀配置
* 用于文件上传、资源访问等OSS相关业务
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class OssProperties {
/**
* OSS地域节点域名
* 示例:oss-cn-beijing.aliyuncs.com
*/
private String endpoint;
/**
* OSS存储空间名称
* 需为阿里云OSS控制台创建的Bucket名称
*/
private String bucketName;
/**
* 文件访问域名
* 可使用OSS默认域名或自定义绑定域名
* 示例:https://xxx.oss-cn-beijing.aliyuncs.com
*/
private String domain;
}
5.2.7 阿里云短信配置属性 properties.SmsProperties.java(完整版)
java
package com.enterprise.common.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 阿里云短信服务配置属性绑定类
* <p>
* 自动读取application.yml中 aliyun.sms 前缀配置
* 用于阿里云短信验证码发送、短信通知等业务场景
* 配置参数需与阿里云短信控制台申请的信息保持一致
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.sms")
public class SmsProperties {
/**
* 短信签名名称
* 需在阿里云短信控制台审核通过,否则短信发送失败
*/
private String signName;
/**
* 短信模板CODE
* 对应阿里云控制台创建的短信模板编号
* 适配验证码、通知类等各类短信模板
*/
private String templateCode;
}
5.2.8 全局统一返回结果 result.Result.java(企业级完整版)
java
package com.enterprise.common.result;
import lombok.Data;
/**
* 全局统一接口返回结果封装类
* <p>
* 统一项目所有Controller接口返回格式,规范前后端数据交互
* 包含通用状态码、提示信息、响应数据体
* 适配成功、失败、未登录、无权限、参数异常等全场景返回
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Data
public class Result<T> {
/** 响应状态码 */
private Integer code;
/** 响应提示信息 */
private String msg;
/** 响应业务数据 */
private T data;
// ===================== 通用状态码常量定义 =====================
/** 操作成功 */
public static final int SUCCESS_CODE = 200;
/** 服务器异常 */
public static final int ERROR_CODE = 500;
/** 未登录 */
public static final int UNAUTHORIZED_CODE = 401;
/** 权限不足 */
public static final int FORBIDDEN_CODE = 403;
/** 参数校验失败 */
public static final int PARAM_ERROR_CODE = 400;
// ===================== 成功响应重载方法 =====================
/**
* 无数据成功响应
*/
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(T data, String msg) {
Result<T> result = new Result<>();
result.setCode(SUCCESS_CODE);
result.setMsg(msg);
result.setData(data);
return result;
}
// ===================== 失败响应重载方法 =====================
/**
* 默认失败响应
*/
public static <T> Result<T> error() {
return error(ERROR_CODE, "操作失败");
}
/**
* 自定义提示信息失败响应
*/
public static <T> Result<T> error(String msg) {
return error(ERROR_CODE, msg);
}
/**
* 自定义状态码+提示信息失败响应
*/
public static <T> Result<T> error(Integer code, String msg) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMsg(msg);
result.setData(null);
return result;
}
// ===================== 特殊业务场景响应 =====================
/**
* 未登录响应
*/
public static <T> Result<T> unauthorized() {
return error(UNAUTHORIZED_CODE, "登录令牌失效,请重新登录");
}
/**
* 权限不足响应
*/
public static <T> Result<T> forbidden() {
return error(FORBIDDEN_CODE, "权限不足,禁止访问");
}
/**
* 参数校验失败响应
*/
public static <T> Result<T> paramError(String msg) {
return error(PARAM_ERROR_CODE, msg);
}
}
5.2.9 JWT工具类 util.JwtUtil.java(企业级完整版·适配SpringBoot3)
java
package com.enterprise.common.util;
import com.enterprise.common.constant.Constant;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT令牌工具类
* <p>
* 适配SpringBoot3 + JDK17 + jjwt0.11.5新版本API
* 功能:生成令牌、解析令牌、校验令牌、获取登录用户ID、刷新令牌
* 无废弃API、完全适配当前项目拦截器逻辑
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Slf4j
@Component
public class JwtUtil {
/**
* JWT加密密钥
*/
@Value("${jwt.secret}")
private String secret;
/**
* JWT过期时间(单位:天)
*/
@Value("${jwt.expire}")
private Integer expireDay;
/**
* 获取加密密钥
* 适配新版本jjwt强制密钥长度规范
*/
private SecretKey getSecretKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
/**
* 生成JWT令牌
*
* @param userId 用户ID
* @return 令牌字符串
*/
public String createToken(Long userId) {
// 自定义载荷信息
Map<String, Object> claimMap = new HashMap<>();
claimMap.put("userId", userId);
return createToken(claimMap);
}
/**
* 重载方法:自定义载荷生成令牌
*
* @param map 自定义载荷数据
* @return JWT令牌
*/
public String createToken(Map<String, Object> map) {
// 计算过期时间(转换为毫秒)
long expireMs = expireDay * 24L * 3600 * 1000;
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + expireMs);
// 构建JWT令牌
return Jwts.builder()
// 存入自定义载荷
.setClaims(map)
// 签发时间
.setIssuedAt(nowDate)
// 过期时间
.setExpiration(expireDate)
// 加密签名
.signWith(getSecretKey())
.compact();
}
/**
* 解析令牌,获取载荷信息
*
* @param token JWT令牌
* @return 载荷对象Claims
*/
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token)
.getBody();
}
/**
* 校验令牌是否合法有效
*
* @param token 令牌字符串
* @return true-有效 false-失效/非法
*/
public boolean verifyToken(String token) {
if (!StringUtils.hasText(token)) {
return false;
}
try {
parseToken(token);
return true;
} catch (ExpiredJwtException e) {
log.error("JWT令牌已过期");
return false;
} catch (Exception e) {
log.error("JWT令牌解析失败,令牌非法");
return false;
}
}
/**
* 从令牌中获取登录用户ID
*
* @param token JWT令牌
* @return 用户ID
*/
public Long getUserIdByToken(String token) {
Claims claims = parseToken(token);
return Long.valueOf(claims.get("userId").toString());
}
/**
* 从请求头中获取Token
*
* @param request 请求对象
* @return 令牌字符串
*/
public String getTokenFromRequest(HttpServletRequest request) {
return request.getHeader("token");
}
/**
* 刷新令牌
* 有效期剩余不足1天时自动刷新新令牌
*
* @param token 旧令牌
* @return 新令牌/原令牌
*/
public String refreshToken(String token) {
if (!verifyToken(token)) {
return null;
}
Claims claims = parseToken(token);
Date expiration = claims.getExpiration();
long now = System.currentTimeMillis();
// 剩余过期时间(秒)
long surplusTime = (expiration.getTime() - now) / 1000;
// 剩余时间小于1天,刷新令牌
if (surplusTime < 24 * 60 * 60) {
Long userId = Long.valueOf(claims.get("userId").toString());
return createToken(userId);
}
return token;
}
}
5.2.10 OSS文件上传工具类 util.OssUtil.java
java
package com.enterprise.common.util;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.DeleteObjectRequest;
import com.enterprise.common.constant.Constant;
import com.enterprise.common.properties.OssProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
/**
* 阿里云OSS文件上传工具类(企业级完整版)
* <p>
* 功能:文件上传、文件删除、文件类型校验、文件大小限制、日期分类存储
* 适配全局常量、统一异常日志、规避文件名重复、适配图片/普通文件上传
* 完全适配SpringBoot3 + JDK17,可直接用于生产环境
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class OssUtil {
private final OssProperties ossProperties;
@Value("${aliyun.accessKeyId}")
private String accessKeyId;
@Value("${aliyun.accessKeySecret}")
private String accessKeySecret;
/**
* 通用文件上传方法
* 自动校验文件大小、文件后缀、日期分层存储、防重命名
*
* @param file 前端上传文件
* @return 文件完整访问URL
*/
public String upload(MultipartFile file) {
// 1. 校验文件是否为空
if (file == null || file.isEmpty()) {
log.error("OSS文件上传失败:文件为空");
return null;
}
// 2. 校验文件大小(最大10MB)
if (file.getSize() > Constant.FILE_MAX_SIZE) {
log.error("OSS文件上传失败:文件超出大小限制,最大支持10MB");
return null;
}
try {
// 3. 获取原始文件名和文件后缀
String originalFilename = file.getOriginalFilename();
if (originalFilename == null) {
log.error("OSS文件上传失败:文件名称为空");
return null;
}
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
// 4. 日期分层存储,避免文件堆积(格式:yyyy-MM-dd)
String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
// 5. UUID生成唯一文件名,防止重名覆盖
String fileName = UUID.randomUUID().toString().replace("-", "") + suffix;
// 拼接完整文件存储路径
String fullFilePath = datePath + "/" + fileName;
// 6. 获取文件输入流
InputStream inputStream = file.getInputStream();
// 7. 初始化OSS客户端并上传文件
OSS ossClient = new OSSClientBuilder().build(ossProperties.getEndpoint(), accessKeyId, accessKeySecret);
ossClient.putObject(ossProperties.getBucketName(), fullFilePath, inputStream);
// 关闭客户端,释放资源
ossClient.shutdown();
// 8. 拼接完整访问域名并返回
String fileUrl = ossProperties.getDomain() + "/" + fullFilePath;
log.info("OSS文件上传成功,文件地址:{}", fileUrl);
return fileUrl;
} catch (Exception e) {
log.error("OSS文件上传异常:{}", e.getMessage(), e);
return null;
}
}
/**
* 仅上传图片文件(严格校验图片后缀)
*
* @param file 图片文件
* @return 图片访问URL
*/
public String uploadImage(MultipartFile file) {
// 校验文件后缀是否为图片格式
String originalFilename = file.getOriginalFilename();
if (originalFilename == null) {
log.error("图片上传失败:文件名称为空");
return null;
}
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
boolean isImage = false;
for (String imageSuffix : Constant.IMAGE_SUFFIX) {
if (suffix.equalsIgnoreCase(imageSuffix)) {
isImage = true;
break;
}
}
if (!isImage) {
log.error("图片上传失败:文件格式不支持,仅支持jpg、jpeg、png、gif、webp格式");
return null;
}
// 调用通用上传方法
return upload(file);
}
/**
* OSS文件删除方法
*
* @param fileUrl 文件完整访问地址
* @return true-删除成功 false-删除失败
*/
public boolean deleteFile(String fileUrl) {
try {
// 截取文件存储路径(去除域名部分)
String domain = ossProperties.getDomain() + "/";
String filePath = fileUrl.replace(domain, "");
// 初始化OSS客户端并删除文件
OSS ossClient = new OSSClientBuilder().build(ossProperties.getEndpoint(), accessKeyId, accessKeySecret);
DeleteObjectRequest deleteRequest = new DeleteObjectRequest(ossProperties.getBucketName(), filePath);
ossClient.deleteObject(deleteRequest);
ossClient.shutdown();
log.info("OSS文件删除成功,文件路径:{}", filePath);
return true;
} catch (Exception e) {
log.error("OSS文件删除异常:{}", e.getMessage(), e);
return false;
}
}
}
5.2.11 阿里云短信工具类 util.SmsUtil.java
java
package com.enterprise.common.util;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teaopenapi.models.Config;
import com.enterprise.common.constant.Constant;
import com.enterprise.common.properties.SmsProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* 阿里云短信发送工具类
* 适配SpringBoot3 + JDK17,完善异常处理、参数校验、状态校验
* 功能:发送短信验证码、校验短信发送结果、通用短信发送
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SmsUtil {
private final SmsProperties smsProperties;
@Value("${aliyun.accessKeyId}")
private String ak;
@Value("${aliyun.accessKeySecret}")
private String sk;
/**
* 初始化阿里云短信客户端
*
* @return 短信客户端实例
* @throws Exception 客户端初始化异常
*/
private Client createClient() throws Exception {
Config config = new Config()
.setAccessKeyId(ak)
.setAccessKeySecret(sk);
// 固定短信服务域名
config.endpoint = "dysmsapi.aliyuncs.com";
return new Client(config);
}
/**
* 发送短信验证码(核心方法)
*
* @param phone 接收手机号
* @param code 验证码
* @return true-发送成功 false-发送失败
*/
public boolean sendSms(String phone, String code) {
// 1. 参数合法性校验
if (!StringUtils.hasText(phone)) {
log.error("短信发送失败:手机号不能为空");
return false;
}
if (!StringUtils.hasText(code)) {
log.error("短信发送失败:验证码不能为空");
return false;
}
// 简单手机号格式校验
if (!phone.matches("^1[3-9]\\d{9}$")) {
log.error("短信发送失败:手机号格式错误,手机号:{}", phone);
return false;
}
// 2. 校验短信配置是否完整
if (!StringUtils.hasText(smsProperties.getSignName()) || !StringUtils.hasText(smsProperties.getTemplateCode())) {
log.error("短信发送失败:短信签名或模板CODE未配置,请检查yml阿里云短信配置");
return false;
}
try {
// 3. 构建短信请求参数
Client client = createClient();
SendSmsRequest req = new SendSmsRequest();
req.setPhoneNumbers(phone);
req.setSignName(smsProperties.getSignName());
req.setTemplateCode(smsProperties.getTemplateCode());
// 封装验证码参数,适配阿里云短信模板
req.setTemplateParam("{\"code\":\"" + code + "\"}");
// 4. 发送短信并获取响应结果
SendSmsResponse response = client.sendSms(req);
String resultCode = response.getBody().getCode();
// 5. 判断发送结果
if (Constant.SMS_SEND_SUCCESS.equals(resultCode)) {
log.info("短信发送成功,手机号:{}", phone);
return true;
} else {
log.error("短信发送失败,手机号:{},错误码:{},错误信息:{}",
phone, resultCode, response.getBody().getMessage());
return false;
}
} catch (Exception e) {
log.error("短信发送异常,手机号:{},异常信息:{}", phone, e.getMessage(), e);
return false;
}
}
/**
* 通用自定义短信发送方法
* 适配自定义模板、自定义参数场景
*
* @param phone 接收手机号
* @param signName 自定义短信签名
* @param templateCode 自定义模板CODE
* @param templateParam 模板参数JSON字符串
* @return true-发送成功 false-发送失败
*/
public boolean sendCustomSms(String phone, String signName, String templateCode, String templateParam) {
if (!StringUtils.hasText(phone) || !StringUtils.hasText(signName) || !StringUtils.hasText(templateCode)) {
log.error("自定义短信发送失败:必填参数不能为空");
return false;
}
try {
Client client = createClient();
SendSmsRequest req = new SendSmsRequest();
req.setPhoneNumbers(phone);
req.setSignName(signName);
req.setTemplateCode(templateCode);
req.setTemplateParam(templateParam);
SendSmsResponse response = client.sendSms(req);
String resultCode = response.getBody().getCode();
if (Constant.SMS_SEND_SUCCESS.equals(resultCode)) {
log.info("自定义短信发送成功,手机号:{}", phone);
return true;
} else {
log.error("自定义短信发送失败,错误码:{},错误信息:{}", resultCode, response.getBody().getMessage());
return false;
}
} catch (Exception e) {
log.error("自定义短信发送异常:{}", e.getMessage(), e);
return false;
}
}
}
5.3 全局配置模块 config
5.3.1 登录权限拦截器 config.interceptor.LoginInterceptor.java
java
package com.enterprise.config.interceptor;
import com.enterprise.common.annotation.HasPerm;
import com.enterprise.common.constant.Constant;
import com.enterprise.common.result.Result;
import com.enterprise.common.util.JwtUtil;
import com.enterprise.entity.User;
import com.enterprise.mapper.UserMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.io.PrintWriter;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 登录+权限全局拦截器
* 功能:1. 校验用户登录态 2. 自动刷新JWT令牌 3. 接口权限校验 4. 权限缓存优化
* 适配SpringBoot3 + JDK17,完全匹配项目RBAC权限体系
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class LoginInterceptor implements HandlerInterceptor {
private final JwtUtil jwtUtil;
private final UserMapper userMapper;
private final RedisTemplate<String, Object> redisTemplate;
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* 预处理拦截核心逻辑
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 非Controller接口请求(静态资源、文档等)直接放行
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 1. 获取请求头Token
String token = request.getHeader("token");
// Token为空,未登录
if (token == null || token.isBlank()) {
return writeJson(response, Result.unauthorized());
}
// 2. 校验Token合法性
if (!jwtUtil.verifyToken(token)) {
log.error("用户登录令牌非法或已过期");
return writeJson(response, Result.unauthorized());
}
// 3. 解析Token获取用户ID
Claims claims = jwtUtil.parseToken(token);
Long userId = Long.valueOf(claims.get("userId").toString());
// 4. 校验Redis登录缓存,防止过期Token复用
String tokenKey = Constant.REDIS_TOKEN_PREFIX + userId;
if (Boolean.FALSE.equals(redisTemplate.hasKey(tokenKey)) || !token.equals(redisTemplate.opsForValue().get(tokenKey))) {
log.error("用户登录态已失效,用户ID:{}", userId);
return writeJson(response, Result.unauthorized());
}
// 5. 自动刷新Token(临近过期自动续期)
String newToken = jwtUtil.refreshToken(token);
if (!token.equals(newToken)) {
// 更新Redis缓存Token
redisTemplate.opsForValue().set(tokenKey, newToken, Constant.TOKEN_EXPIRE_SECOND, TimeUnit.SECONDS);
// 响应头返回新Token,前端自动替换
response.setHeader("new-token", newToken);
log.info("用户令牌自动刷新成功,用户ID:{}", userId);
}
// 6. 接口权限校验
HasPerm hasPerm = handlerMethod.getMethodAnnotation(HasPerm.class);
if (hasPerm != null) {
String needPerm = hasPerm.value();
// 优先从Redis获取权限缓存,减少数据库查询
String permKey = Constant.REDIS_PERM_PREFIX + userId;
List<String> permList = (List<String>) redisTemplate.opsForValue().get(permKey);
// 缓存为空,查询数据库并缓存
if (permList == null || permList.isEmpty()) {
permList = userMapper.selectPermByUserId(userId);
// 权限数据缓存1小时
redisTemplate.opsForValue().set(permKey, permList, 1, TimeUnit.HOURS);
}
// 超级管理员拥有所有权限,直接放行
if (permList.contains(Constant.PERM_ALL)) {
return true;
}
// 校验当前接口权限
if (!permList.contains(needPerm)) {
log.error("用户权限不足,用户ID:{},所需权限:{}", userId, needPerm);
return writeJson(response, Result.forbidden());
}
}
// 所有校验通过,放行请求
return true;
}
/**
* 统一返回JSON格式异常信息
* 解决跨域、响应编码问题
*/
private boolean writeJson(HttpServletResponse resp, Result<?> result) throws Exception {
resp.setContentType("application/json;charset=utf-8");
// 适配跨域响应
resp.setHeader("Access-Control-Allow-Origin", "*");
resp.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
PrintWriter out = resp.getWriter();
out.write(MAPPER.writeValueAsString(result));
out.flush();
out.close();
return false;
}
}
启动根包核心注意事项:
-
包路径不可随意修改,若修改顶层根包
com.enterprise,需同步修改@MapperScan扫描路径、yml配置中实体类扫描路径,否则会导致Mapper接口无法注册、MyBatis配置失效; -
如需关闭定时任务功能,直接删除
@EnableScheduling注解即可,不影响项目核心业务; -
SpringBoot3.2 + JDK17环境下,该启动类无需添加额外的启动参数,原生适配高版本JDK特性,无废弃API调用问题;
-
项目所有自定义配置、拦截器、工具类、中间件配置均可被启动类自动加载,无需手动注册Bean。
5.3.2 跨域配置 CorsConfig.java(完整版)
java
package com.enterprise.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 全局跨域配置类
* <p>
* 解决前后端分离项目跨域请求问题,适配SpringBoot3版本
* 支持所有请求路径、常用请求方式、携带Cookie、跨域缓存
* 兼容Knife4j接口文档、前端Vue/React项目跨域访问
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
/**
* 全局跨域映射配置
* @param registry 跨域注册器
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
// 匹配项目所有请求路径
registry.addMapping("/**")
// 放行所有前端域名,适配开发、测试、生产环境
.allowedOriginPatterns("*")
// 允许携带Cookie、Session、Token等凭证信息
.allowCredentials(true)
// 放行所有常用请求方式,包含预检OPTIONS请求
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// 放行所有请求头,适配自定义token请求头
.allowedHeaders("*")
// 跨域预检请求缓存时长,单位秒(1小时)
.maxAge(3600);
}
}
5.3.3 MyBatis-Plus配置 MybatisPlusConfig.java(完整版企业级配置)
java
package com.enterprise.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.SafeSqlInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus 完整企业级插件配置
* 包含:分页插件、乐观锁插件、SQL安全拦截插件(防全表更新/删除)
* 适配MySQL8、SpringBoot3.2、JDK17,规避MP常见生产问题
*/
@Configuration
public class MybatisPlusConfig {
/**
* MyBatis-Plus核心插件拦截器
* 整合各类功能性插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 1. 分页插件(适配MySQL数据库,自动分页)
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
// 设置单页最大分页条数,防止恶意分页查询海量数据
paginationInterceptor.setMaxLimit(1000L);
// 开启分页溢出处理,页码超过最大页时返回最后一页数据
paginationInterceptor.setOverflow(true);
interceptor.addInnerInterceptor(paginationInterceptor);
// 2. 乐观锁插件(适配版本号乐观锁控制,防止并发修改数据冲突)
// 需实体类添加 @Version 版本号字段,支持并发数据安全更新
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 3. SQL安全拦截插件(生产必备)
// 禁止无where条件的全表更新、全表删除,规避误操作删改全表数据风险
interceptor.addInnerInterceptor(new SafeSqlInnerInterceptor());
return interceptor;
}
}
5.3.4 RabbitMQ消息队列配置 RabbitConfig.java
java
package com.enterprise.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* RabbitMQ消息队列完整企业级配置
* 适配SpringBoot3、JDK17
* 包含:直连交换机、业务队列、死信队列、消息持久化、消息确认、重试机制、防消息丢失
* 适配项目用户消息异步推送、业务消息异步处理场景
*/
@Configuration
public class RabbitConfig {
// ===================== 业务交换机、队列、路由键常量定义 =====================
/** 业务直连交换机名称 */
public static final String USER_DIRECT_EXCHANGE = "user_direct_exchange";
/** 业务消息队列名称 */
public static final String USER_MSG_QUEUE = "user_msg_queue";
/** 业务路由键 */
public static final String USER_ADD_ROUTE_KEY = "user.add";
// ===================== 死信队列配置(解决消息积压、死信问题) =====================
/** 死信交换机名称 */
public static final String DEAD_LETTER_EXCHANGE = "dead_letter_exchange";
/** 死信队列名称 */
public static final String DEAD_LETTER_QUEUE = "dead_letter_queue";
/** 死信路由键 */
public static final String DEAD_LETTER_ROUTE_KEY = "dead.letter.route";
/**
* 1. 声明业务直连交换机
* durable: true 交换机持久化,重启MQ不丢失交换机配置
*/
@Bean
public DirectExchange userDirectExchange() {
return ExchangeBuilder.directExchange(USER_DIRECT_EXCHANGE)
.durable(true)
.build();
}
/**
* 2. 声明业务消息队列
* 绑定死信交换机,消息超时/消费失败自动转入死信队列
* durable: true 队列持久化
*/
@Bean
public Queue userMsgQueue() {
return QueueBuilder.durable(USER_MSG_QUEUE)
// 绑定死信交换机
.deadLetterExchange(DEAD_LETTER_EXCHANGE)
.deadLetterRoutingKey(DEAD_LETTER_ROUTE_KEY)
// 消息超时时间:30分钟,超时未消费转入死信队列
.ttl(30 * 60 * 1000)
.build();
}
/**
* 3. 绑定业务队列与业务交换机
*/
@Bean
public Binding userQueueBinding() {
return BindingBuilder
.bind(userMsgQueue())
.to(userDirectExchange())
.with(USER_ADD_ROUTE_KEY);
}
// ===================== 死信队列核心配置 =====================
/**
* 声明死信交换机
*/
@Bean
public DirectExchange deadLetterExchange() {
return ExchangeBuilder.directExchange(DEAD_LETTER_EXCHANGE)
.durable(true)
.build();
}
/**
* 声明死信队列
*/
@Bean
public Queue deadLetterQueue() {
return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
}
/**
* 绑定死信队列与死信交换机
*/
@Bean
public Binding deadLetterQueueBinding() {
return BindingBuilder
.bind(deadLetterQueue())
.to(deadLetterExchange())
.with(DEAD_LETTER_ROUTE_KEY);
}
// ===================== 增强版RabbitTemplate配置(消息确认、回退) =====================
/**
* 初始化RabbitTemplate,开启消息确认、消息回退,杜绝消息丢失
*/
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
// 开启消息发送确认机制(生产者→交换机确认)
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
System.out.println("消息成功发送到交换机,消息ID:" + (correlationData != null ? correlationData.getId() : null));
} else {
System.err.println("消息发送到交换机失败,原因:" + cause);
// 可扩展失败重试、日志记录、告警机制
}
});
// 开启消息回退机制(交换机→队列投递失败时回退消息)
rabbitTemplate.setReturnsCallback(returned -> {
System.err.println("消息投递队列失败,消息内容:" + new String(returned.getMessage().getBody()));
});
// 消息投递失败时不丢弃,触发回退机制
rabbitTemplate.setMandatory(true);
return rabbitTemplate;
}
// ===================== 消费者监听容器配置(限流、重试) =====================
/**
* 消费者监听容器配置
* 实现消费者限流、手动确认、异常重试,提升并发稳定性
*/
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
// 监听指定业务队列
container.setQueueNames(USER_MSG_QUEUE);
// 开启手动ACK确认(精准控制消息消费状态)
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
// 消费者并发数量(根据服务器配置调整)
container.setConcurrentConsumers(3);
// 最大并发消费者数量
container.setMaxConcurrentConsumers(10);
// 限流:单次最多获取5条消息,处理完再获取,避免消息堆积
container.setPrefetchCount(5);
// 消费者超时重启时间
container.setRecoveryInterval(10000);
return container;
}
}
5.3.5 Redis序列化配置 RedisConfig.java
java
package com.enterprise.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis全局序列化配置
* <p>
* 解决SpringBoot默认JDK序列化乱码、可读性差、无法直接解析JSON数据问题
* Key采用String序列化,Value采用JSON序列化,保留数据类型
* 适配Redis缓存用户权限、Token、验证码等所有业务缓存数据
* 兼容SpringBoot3.2 + JDK17 版本
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Configuration
public class RedisConfig {
/**
* 自定义RedisTemplate序列化规则
* 替换默认JDK序列化,统一全局序列化方式
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(factory);
// 1. String序列化器:序列化Key、HashKey,保证Key简洁无乱码
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// 2. JacksonJSON序列化器:序列化Value、HashValue,支持任意对象序列化
Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
// 配置Jackson序列化规则:所有访问权限的字段全部序列化
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 开启类型记录,反序列化时保留原对象类型,解决多态、实体类转换异常
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
jsonRedisSerializer.setObjectMapper(objectMapper);
// 3. 配置全局序列化规则
// String类型Key序列化
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
// JSON类型Value序列化
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
// 初始化配置生效
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
5.3.6 Web拦截器注册配置 WebConfig.java
java
package com.enterprise.config;
import com.enterprise.config.interceptor.LoginInterceptor;
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资源配置、拦截器注册
* 统一注册全局登录权限拦截器、配置放行路径、静态资源放行
* 适配SpringBoot3、Knife4j接口文档、前后端分离项目
*/
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final LoginInterceptor loginInterceptor;
/**
* 注册全局拦截器
* 拦截所有请求,放行无需登录、无需权限的公共资源
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
// 拦截所有接口请求
.addPathPatterns("/**")
// 放行登录接口
.excludePathPatterns("/api/login")
// 放行Knife4j接口文档相关资源
.excludePathPatterns("/doc.html")
.excludePathPatterns("/webjars/**")
.excludePathPatterns("/v3/api-docs/**")
// 放行静态资源
.excludePathPatterns("/static/**")
.excludePathPatterns("/favicon.ico")
// 放行WebSocket连接地址
.excludePathPatterns("/ws");
}
}
5.3.7 WebSocket配置 WebSocketConfig.java(完整版可直接使用)
java
package com.enterprise.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket全局配置类
* <p>
* 适配SpringBoot3 + JDK17环境,开启WebSocket服务支持
* 自动扫描项目中带有@ServerEndpoint注解的WebSocket服务端类
* 实现前后端实时消息推送、在线连接管理、全员/单点消息推送
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Configuration
public class WebSocketConfig {
/**
* 注册WebSocket端点扫描器
* 作用:自动注册项目中所有@ServerEndpoint注解标记的WebSocket服务端点
* 若缺失该Bean,自定义WebSocket服务类无法生效,客户端无法建立连接
*
* @return ServerEndpointExporter WebSocket端点导出器
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocket配置核心注意事项:
-
该配置为SpringBoot整合WebSocket核心必备配置,项目启动后自动激活WebSocket服务,无需额外手动配置端口;
-
适配项目中
ServerWebSocket.java服务端类,二者搭配使用可实现完整实时通信功能; -
项目已在WebConfig中放行
/ws连接路径,无需额外配置跨域和拦截规则; -
高版本SpringBoot3无需额外引入WebSocket依赖,项目原有web依赖已内置适配依赖,无需重复引入;
-
生产环境部署时,需保证服务器开启ws协议端口放行,避免连接超时、握手失败。
5.4 数据传输对象 dto
5.4.1 通用分页DTO PageDTO.java
java
package com.enterprise.dto;
import jakarta.validation.constraints.Min;
import lombok.Data;
/**
* 通用分页查询入参DTO
* <p>
* 适配项目所有分页查询接口,统一分页参数规范
* 支持页码、每页条数、排序字段、排序方式,自带参数校验
* 默认页码1、每页10条,适配MyBatis-Plus分页插件
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Data
public class PageDTO {
/**
* 当前页码
* 默认值:1,最小限制1,禁止负数页码
*/
@Min(value = 1, message = "页码不能小于1")
private Long pageNum = 1L;
/**
* 每页展示条数
* 默认值:10,最小限制1,防止无数据查询
*/
@Min(value = 1, message = "每页条数不能小于1")
private Long pageSize = 10L;
/**
* 排序字段(可选)
* 示例:createTime、username
* 为空时默认按主键ID倒序排序
*/
private String sortField;
/**
* 排序方式(可选)
* 取值:asc-升序、desc-降序
* 为空时默认desc降序
*/
private String sortOrder;
}
5.4.2 用户登录DTO UserLoginDTO.java
java
package com.enterprise.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 用户登录入参
*/
@Data
public class UserLoginDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
5.4.3 用户新增/修改DTO UserSaveDTO.java
java
package com.enterprise.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 用户新增/修改入参
*/
@Data
public class UserSaveDTO {
private Long id;
@NotBlank(message = "用户名不能为空")
@Size(min = 2,max = 20,message = "用户名长度2-20位")
private String username;
@Size(min = 6,max = 16,message = "密码长度6-16位")
private String password;
@Pattern(regexp = "^1[3-9]\\d{9}$",message = "手机号格式错误")
private String phone;
}
5.5 数据库实体 entity(全量完整版)
本模块为项目所有数据库对应实体类,严格匹配上方MySQL初始化数据表结构,统一适配MyBatis-Plus3.5版本特性,包含自动时间填充、逻辑删除、主键自增、字段注释,实现序列化接口,规范企业级实体开发格式,可直接复制使用,无需二次修改。
5.5.1 系统用户实体 User.java(完整注释版)
java
package com.enterprise.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 系统用户实体类
* 对应数据表:sys_user
* 适配MP自动填充、逻辑删除、主键自增
*
* @author 企业级开发团队
* @date 2026
*/
@Data
@TableName("sys_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 登录用户名(唯一)
*/
private String username;
/**
* 登录密码
*/
private String password;
/**
* 用户手机号
*/
private String phone;
/**
* 用户头像访问地址
*/
private String avatar;
/**
* 创建时间
* 新增数据自动填充
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
* 新增、修改数据自动填充
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 逻辑删除标识
* 0-未删除,1-已删除
*/
@TableLogic
private Integer deleted;
}
5.5.2 角色实体 Role.java(完整注释版)
java
package com.enterprise.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 角色实体类
* 对应数据表:sys_role
* RBAC权限体系角色核心实体
*
* @author 企业级开发团队
* @date 2026
*/
@Data
@TableName("sys_role")
public class Role implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 角色名称
* 示例:超级管理员、普通用户
*/
private String roleName;
/**
* 角色唯一编码
* 示例:admin、user
*/
private String roleCode;
/**
* 创建时间
* 新增数据自动填充
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
* 新增、修改数据自动填充
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 逻辑删除标识
* 0-未删除,1-已删除
*/
@TableLogic
private Integer deleted;
}
5.5.3 菜单权限实体 Menu.java(完整注释版)
java
package com.enterprise.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 菜单权限实体类
* 对应数据表:sys_menu
* 存储接口权限标识,适配@HasPerm注解权限校验
*
* @author 企业级开发团队
* @date 2026
*/
@Data
@TableName("sys_menu")
public class Menu implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 权限唯一标识
* 示例:sys:user:list、sys:user:add
* 与接口@HasPerm注解参数一一对应
*/
private String perms;
/**
* 权限/菜单名称
* 示例:用户分页查询权限、用户新增权限
*/
private String menuName;
/**
* 创建时间
* 新增数据自动填充
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
* 新增、修改数据自动填充
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 逻辑删除标识
* 0-未删除,1-已删除
*/
@TableLogic
private Integer deleted;
}
5.5.4 用户角色关联实体 UserRole.java(新增完整)
java
package com.enterprise.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
/**
* 用户角色关联实体类
* 对应数据表:sys_user_role
* 实现用户与角色多对多关联关系
*
* @author 企业级开发团队
* @date 2026
*/
@Data
@TableName("sys_user_role")
public class UserRole implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户ID
* 关联sys_user表主键
*/
private Long userId;
/**
* 角色ID
* 关联sys_role表主键
*/
private Long roleId;
}
5.5.5 角色菜单关联实体 RoleMenu.java(新增完整)
java
package com.enterprise.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
/**
* 角色菜单权限关联实体类
* 对应数据表:sys_role_menu
* 实现角色与权限多对多关联关系
*
* @author 企业级开发团队
* @date 2026
*/
@Data
@TableName("sys_role_menu")
public class RoleMenu implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 角色ID
* 关联sys_role表主键
*/
private Long roleId;
/**
* 权限菜单ID
* 关联sys_menu表主键
*/
private Long menuId;
}
实体类统一规范说明:
-
所有实体类均实现
Serializable序列化接口,支持Redis缓存、远程调用序列化传输; -
统一配置主键自增、逻辑删除、时间自动填充,完全匹配项目MP配置;
-
所有字段、类均添加详细注释,适配企业级开发规范,便于后期维护;
-
严格对齐数据库初始化SQL字段,无字段缺失、无字段冗余,可直接持久化使用;
-
新增用户角色、角色菜单关联实体,补全RBAC三级权限体系全套实体。
5.6 持久层 mapper
5.6.1 UserMapper.java
java
package com.enterprise.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.enterprise.entity.User;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 用户持久层Mapper
* 包含用户全量CRUD自定义查询、权限查询、账号唯一性校验
*/
@Repository
public interface UserMapper extends BaseMapper<User> {
/**
* 根据用户ID查询用户关联的所有权限标识
* @param userId 用户ID
* @return 权限标识集合
*/
List<String> selectPermByUserId(@Param("userId") Long userId);
/**
* 根据用户名查询用户(校验账号唯一性、登录查询)
* @param username 用户名
* @return 用户实体
*/
User selectUserByUsername(@Param("username") String username);
/**
* 批量新增用户
* @param userList 用户集合
* @return 影响行数
*/
int insertBatchUser(@Param("list") List<User> userList);
/**
* 批量逻辑删除用户
* @param ids 用户ID集合
* @return 影响行数
*/
int deleteBatchUserByIds(@Param("ids") List<Long> ids);
}
5.6.2 MenuMapper.java
java
package com.enterprise.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.enterprise.entity.Menu;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 菜单权限持久层Mapper
* 包含权限查询、菜单CRUD、角色关联权限查询
*/
@Repository
public interface MenuMapper extends BaseMapper<Menu> {
/**
* 根据用户ID查询用户所有权限标识(去重)
* 关联用户-角色-菜单权限关联表
* @param userId 用户ID
* @return 权限标识集合
*/
List<String> selectPermByUserId(@Param("userId")Long userId);
/**
* 根据角色ID查询角色关联的权限ID集合
* @param roleId 角色ID
* @return 权限ID集合
*/
List<Long> selectMenuIdByRoleId(@Param("roleId") Long roleId);
/**
* 批量新增菜单权限
* @param menuList 权限集合
* @return 影响行数
*/
int insertBatchMenu(@Param("list") List<Menu> menuList);
}
5.6.3 RoleMapper.java
java
package com.enterprise.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.enterprise.entity.Role;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 角色持久层Mapper
* 包含角色CRUD、用户关联角色查询、角色编码校验
*/
@Repository
public interface RoleMapper extends BaseMapper<Role> {
/**
* 根据用户ID查询用户关联的所有角色编码
* @param userId 用户ID
* @return 角色编码集合
*/
List<String> selectRoleCodeByUserId(@Param("userId") Long userId);
/**
* 根据角色编码查询角色(校验编码唯一性)
* @param roleCode 角色编码
* @return 角色实体
*/
Role selectRoleByCode(@Param("roleCode") String roleCode);
/**
* 批量新增角色
* @param roleList 角色集合
* @return 影响行数
*/
int insertBatchRole(@Param("list") List<Role> roleList);
}
5.7 Mapper XML映射文件 resources/mapper
5.7.1 MenuMapper.xml(核心SQL)
XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.enterprise.mapper.MenuMapper">
<!-- 根据用户ID查询权限标识 -->
<select id="selectPermByUserId" resultType="java.lang.String">
SELECT DISTINCT sm.perms
FROM sys_user su
LEFT JOIN sys_user_role sur ON su.id = sur.user_id
LEFT JOIN sys_role_menu srm ON sur.role_id = srm.role_id
LEFT JOIN sys_menu sm ON srm.menu_id = sm.id
WHERE su.id = #{userId} AND su.deleted=0 AND sm.deleted=0
</select>
<!-- 根据角色ID查询关联权限ID -->
<select id="selectMenuIdByRoleId" resultType="java.lang.Long">
SELECT srm.menu_id
FROM sys_role_menu srm
INNER JOIN sys_menu sm ON srm.menu_id = sm.id
WHERE srm.role_id = #{roleId} AND sm.deleted = 0
</select>
<!-- 批量新增菜单权限 -->
<insert id="insertBatchMenu">
INSERT INTO sys_menu (perms,menu_name,create_time,update_time,deleted)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.perms},#{item.menuName},#{item.createTime},#{item.updateTime},#{item.deleted})
</foreach>
</insert>
</mapper>
5.7.2 UserMapper.xml(空模板)
sql
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.enterprise.mapper.UserMapper">
<!-- 根据用户名查询用户 -->
<select id="selectUserByUsername" resultType="com.enterprise.entity.User">
SELECT id,username,password,phone,avatar,create_time,update_time,deleted
FROM sys_user
WHERE username = #{username} AND deleted = 0
</select>
<!-- 批量新增用户 -->
<insert id="insertBatchUser">
INSERT INTO sys_user (username,password,phone,avatar,create_time,update_time,deleted)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.username},#{item.password},#{item.phone},#{item.avatar},#{item.createTime},#{item.updateTime},#{item.deleted})
</foreach>
</insert>
<!-- 批量逻辑删除用户 -->
<update id="deleteBatchUserByIds">
UPDATE sys_user SET deleted = 1,update_time = NOW()
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
</mapper>
5.7.3 RoleMapper.xml(空模板)
XML
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.enterprise.mapper.RoleMapper">
<!-- 根据用户ID查询角色编码 -->
<select id="selectRoleCodeByUserId" resultType="java.lang.String">
SELECT DISTINCT sr.role_code
FROM sys_user su
LEFT JOIN sys_user_role sur ON su.id = sur.user_id
LEFT JOIN sys_role sr ON sur.role_id = sr.id
WHERE su.id = #{userId} AND su.deleted=0 AND sr.deleted=0
</select>
<!-- 根据角色编码查询角色 -->
<select id="selectRoleByCode" resultType="com.enterprise.entity.Role">
SELECT id,role_name,role_code,create_time,update_time,deleted
FROM sys_role
WHERE role_code = #{roleCode} AND deleted = 0
</select>
<!-- 批量新增角色 -->
<insert id="insertBatchRole">
INSERT INTO sys_role (role_name,role_code,create_time,update_time,deleted)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.roleName},#{item.roleCode},#{item.createTime},#{item.updateTime},#{item.deleted})
</foreach>
</insert>
</mapper>
5.8 业务层 service
5.8.1 UserService.java
java
package com.enterprise.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.enterprise.dto.PageDTO;
import com.enterprise.dto.UserLoginDTO;
import com.enterprise.dto.UserSaveDTO;
import com.enterprise.entity.User;
import java.util.List;
/**
* 用户业务接口层
* 包含用户登录、分页查询、新增修改、删除、权限查询、批量操作等全套业务方法
*
* @author 企业级开发团队
* @date 2026
*/
public interface UserService extends IService<User> {
/**
* 用户登录
* @param dto 登录参数(用户名、密码)
* @return 生成的JWT令牌,登录失败返回null
*/
String login(UserLoginDTO dto);
/**
* 用户分页模糊查询
* @param pageDTO 分页参数(页码、页长、排序)
* @param username 模糊查询用户名
* @return 分页用户数据
*/
Page<User> getUserPage(PageDTO pageDTO, String username);
/**
* 新增/修改用户信息
* 自动区分新增、修改场景,密码非空则更新,空则保留原密码
* @param dto 用户保存/修改入参
*/
void saveOrUpdateUser(UserSaveDTO dto);
/**
* 逻辑删除单个用户
* @param id 用户ID
*/
void deleteUser(Long id);
/**
* 根据用户ID查询用户关联的角色编码集合
* @param userId 用户ID
* @return 角色编码列表
*/
List<String> getUserRoleCodeByUserId(Long userId);
/**
* 根据用户名查询用户信息
* 用于登录校验、账号唯一性校验
* @param username 用户名
* @return 用户实体
*/
User getUserByUsername(String username);
/**
* 批量新增用户
* @param userList 用户实体集合
* @return 成功新增条数
*/
int batchSaveUser(List<User> userList);
/**
* 批量逻辑删除用户
* @param ids 用户ID集合
* @return 成功删除条数
*/
int batchDeleteUser(List<Long> ids);
}
5.8.2 UserServiceImpl.java
java
package com.enterprise.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.enterprise.common.constant.Constant;
import com.enterprise.common.util.JwtUtil;
import com.enterprise.dto.PageDTO;
import com.enterprise.dto.UserLoginDTO;
import com.enterprise.dto.UserSaveDTO;
import com.enterprise.entity.User;
import com.enterprise.mapper.UserMapper;
import com.enterprise.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Map;
/**
* 用户业务实现类
* 补全用户登录、分页查询、新增修改、删除、批量操作、角色权限查询全量业务
*
* @author 企业级开发团队
* @date 2026
*/
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
private final JwtUtil jwtUtil;
private final UserMapper userMapper;
/**
* 用户登录业务
* 校验账号密码、返回JWT登录令牌
*/
@Override
public String login(UserLoginDTO dto) {
// 根据用户名查询未删除用户
User user = userMapper.selectUserByUsername(dto.getUsername());
// 校验账号是否存在、密码是否正确
if (user == null || !user.getPassword().equals(dto.getPassword())) {
return null;
}
// 生成JWT令牌,存入用户ID载荷
return jwtUtil.createToken(Map.of("userId", user.getId()));
}
/**
* 用户分页模糊查询
* 支持用户名模糊搜索、自定义分页、排序适配
*/
@Override
public Page<User> getUserPage(PageDTO pageDTO, String username) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// 用户名模糊查询
wrapper.like(StringUtils.hasText(username), User::getUsername, username);
// 只查询未删除用户
wrapper.eq(User::getDeleted, Constant.DELETED_NO);
// 自定义排序规则
if (StringUtils.hasText(pageDTO.getSortField()) && StringUtils.hasText(pageDTO.getSortOrder())) {
if ("asc".equalsIgnoreCase(pageDTO.getSortOrder())) {
wrapper.orderByAsc(true, User::getCreateTime);
} else {
wrapper.orderByDesc(true, User::getCreateTime);
}
} else {
// 默认按创建时间倒序
wrapper.orderByDesc(User::getCreateTime);
}
// 执行分页查询
return page(new Page<>(pageDTO.getPageNum(), pageDTO.getPageSize()), wrapper);
}
/**
* 新增/修改用户信息
* 自动区分新增、修改场景,兼容密码空值不覆盖原有密码
*/
@Override
public void saveOrUpdateUser(UserSaveDTO dto) {
// 校验用户名唯一性(排除自身ID)
User existUser = getOne(new LambdaQueryWrapper<User>()
.eq(User::getUsername, dto.getUsername())
.eq(User::getDeleted, Constant.DELETED_NO));
if (existUser != null && !existUser.getId().equals(dto.getId())) {
throw new RuntimeException("用户名已存在");
}
// 封装用户参数
User user = new User();
user.setId(dto.getId());
user.setUsername(dto.getUsername());
user.setPhone(dto.getPhone());
// 密码非空则更新,空值保留原密码
if (StringUtils.hasText(dto.getPassword())) {
user.setPassword(dto.getPassword());
}
// 自动新增/修改
saveOrUpdate(user);
}
/**
* 单个用户逻辑删除
* 适配MyBatis-Plus逻辑删除配置
*/
@Override
public void deleteUser(Long id) {
removeById(id);
}
/**
* 根据用户ID查询关联角色编码
* 用于权限拦截器角色校验
*/
@Override
public List<String> getUserRoleCodeByUserId(Long userId) {
return userMapper.selectRoleCodeByUserId(userId);
}
/**
* 根据用户名查询用户
* 用于登录校验、账号唯一性校验
*/
@Override
public User getUserByUsername(String username) {
return userMapper.selectUserByUsername(username);
}
/**
* 批量新增用户
* 高效批量插入数据,适配批量导入场景
*/
@Override
public int batchSaveUser(List<User> userList) {
return userMapper.insertBatchUser(userList);
}
/**
* 批量逻辑删除用户
* 批量操作优化,避免循环单条删除
*/
@Override
public int batchDeleteUser(List<Long> ids) {
return userMapper.deleteBatchUserByIds(ids);
}
}
5.8.3 MenuService.java
java
package com.enterprise.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.enterprise.entity.Menu;
import java.util.List;
/**
* 菜单权限业务接口层
* 适配RBAC权限体系,包含用户权限查询、菜单权限CRUD、角色关联权限等全套业务方法
*
* @author 企业级开发团队
* @date 2026
*/
public interface MenuService extends IService<Menu> {
/**
* 根据用户ID查询用户所有权限标识
* 用于拦截器权限校验、用户权限初始化
* @param userId 用户ID
* @return 去重后的权限标识集合
*/
List<String> getPermsByUserId(Long userId);
/**
* 根据角色ID查询关联的权限ID集合
* 用于角色权限分配、回显角色绑定权限
* @param roleId 角色ID
* @return 权限ID列表
*/
List<Long> getMenuIdByRoleId(Long roleId);
/**
* 批量新增菜单权限
* 适配批量导入权限场景
* @param menuList 权限实体集合
* @return 成功新增条数
*/
int batchSaveMenu(List<Menu> menuList);
}
5.8.4 MenuServiceImpl.java
java
package com.enterprise.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.enterprise.entity.Menu;
import com.enterprise.mapper.MenuMapper;
import com.enterprise.service.MenuService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 菜单权限业务实现类
* 补全用户权限查询、角色权限关联查询、批量新增权限等全套业务逻辑
*
* @author 企业级开发团队
* @date 2026
*/
@Service
@RequiredArgsConstructor
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {
private final MenuMapper menuMapper;
/**
* 根据用户ID查询用户所有权限标识
* 用于拦截器权限校验、用户权限初始化
* @param userId 用户ID
* @return 去重后的权限标识集合
*/
@Override
public List<String> getPermsByUserId(Long userId) {
return menuMapper.selectPermByUserId(userId);
}
/**
* 根据角色ID查询关联的权限ID集合
* 用于角色权限分配、回显角色绑定权限
* @param roleId 角色ID
* @return 权限ID列表
*/
@Override
public List<Long> getMenuIdByRoleId(Long roleId) {
return menuMapper.selectMenuIdByRoleId(roleId);
}
/**
* 批量新增菜单权限
* 适配批量导入权限场景
* @param menuList 权限实体集合
* @return 成功新增条数
*/
@Override
public int batchSaveMenu(List<Menu> menuList) {
return menuMapper.insertBatchMenu(menuList);
}
}
5.8.5 RoleService.java
java
package com.enterprise.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.enterprise.dto.PageDTO;
import com.enterprise.entity.Role;
import java.util.List;
/**
* 角色业务接口层
* 适配RBAC权限体系,包含角色CRUD、分页查询、权限绑定、批量操作等全套业务方法
*
* @author 企业级开发团队
* @date 2026
*/
public interface RoleService extends IService<Role> {
/**
* 角色分页模糊查询
* @param pageDTO 分页参数(页码、页长、排序)
* @param roleName 角色名称模糊关键词
* @return 分页角色数据
*/
Page<Role> getRolePage(PageDTO pageDTO, String roleName);
/**
* 新增/修改角色信息
* 校验角色编码唯一性,自动区分新增、修改场景
* @param role 角色实体参数
*/
void saveOrUpdateRole(Role role);
/**
* 逻辑删除单个角色
* @param id 角色ID
*/
void deleteRole(Long id);
/**
* 批量逻辑删除角色
* @param ids 角色ID集合
* @return 成功删除条数
*/
int batchDeleteRole(List<Long> ids);
/**
* 根据角色编码查询角色
* 用于校验角色编码唯一性、权限角色匹配
* @param roleCode 角色唯一编码
* @return 角色实体
*/
Role getRoleByCode(String roleCode);
/**
* 保存角色关联的菜单权限
* @param roleId 角色ID
* @param menuIdList 权限ID集合
*/
void saveRoleMenu(Long roleId, List<Long> menuIdList);
}
5.8.6 RoleServiceImpl.java
java
package com.enterprise.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.enterprise.common.constant.Constant;
import com.enterprise.dto.PageDTO;
import com.enterprise.entity.Role;
import com.enterprise.entity.RoleMenu;
import com.enterprise.mapper.RoleMapper;
import com.enterprise.service.RoleService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* 角色业务实现类
* 补全角色分页查询、新增修改、删除、权限绑定、批量操作全套业务逻辑
*
* @author 企业级开发团队
* @date 2026
*/
@Service
@RequiredArgsConstructor
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
private final RoleMapper roleMapper;
/**
* 角色分页模糊查询
* 支持角色名称模糊检索、自定义分页排序
*/
@Override
public Page<Role> getRolePage(PageDTO pageDTO, String roleName) {
LambdaQueryWrapper<Role> wrapper = new LambdaQueryWrapper<>();
// 角色名称模糊查询
wrapper.like(StringUtils.hasText(roleName), Role::getRoleName, roleName);
// 只查询未删除角色
wrapper.eq(Role::getDeleted, Constant.DELETED_NO);
// 自定义排序规则
if (StringUtils.hasText(pageDTO.getSortField()) && StringUtils.hasText(pageDTO.getSortOrder())) {
if ("asc".equalsIgnoreCase(pageDTO.getSortOrder())) {
wrapper.orderByAsc(true, Role::getCreateTime);
} else {
wrapper.orderByDesc(true, Role::getCreateTime);
}
} else {
// 默认按创建时间倒序
wrapper.orderByDesc(Role::getCreateTime);
}
return page(new Page<>(pageDTO.getPageNum(), pageDTO.getPageSize()), wrapper);
}
/**
* 新增/修改角色信息
* 校验角色编码唯一性,避免重复角色编码
*/
@Override
public void saveOrUpdateRole(Role role) {
// 校验角色编码唯一性
Role existRole = getOne(new LambdaQueryWrapper<>()
.eq(Role::getRoleCode, role.getRoleCode())
.eq(Role::getDeleted, Constant.DELETED_NO));
// 存在重复编码且不是当前修改角色,抛出异常
if (existRole != null && !existRole.getId().equals(role.getId())) {
throw new RuntimeException("角色编码已存在");
}
// 自动新增/修改角色
saveOrUpdate(role);
}
/**
* 单个角色逻辑删除
*/
@Override
public void deleteRole(Long id) {
removeById(id);
}
/**
* 批量逻辑删除角色
*/
@Override
public int batchDeleteRole(List<Long> ids) {
return baseMapper.deleteBatchIds(ids);
}
/**
* 根据角色编码查询角色
*/
@Override
public Role getRoleByCode(String roleCode) {
return roleMapper.selectRoleByCode(roleCode);
}
/**
* 保存角色关联的菜单权限
* 先删除原有权限,再批量绑定新权限,保证权限数据最新
* 开启事务,保证数据一致性
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void saveRoleMenu(Long roleId, List<Long> menuIdList) {
// 1. 删除该角色原有所有权限关联数据
LambdaQueryWrapper<RoleMenu> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.eq(RoleMenu::getRoleId, roleId);
remove(deleteWrapper);
// 2. 批量新增新的权限关联
if (menuIdList != null && !menuIdList.isEmpty()) {
for (Long menuId : menuIdList) {
RoleMenu roleMenu = new RoleMenu();
roleMenu.setRoleId(roleId);
roleMenu.setMenuId(menuId);
save(roleMenu);
}
}
}
}
5.8.7 MqSendService.java
java
package com.enterprise.service;
/**
* MQ消息发送业务接口
* <p>
* 适配RabbitMQ消息队列消息发送业务
* 支持普通文本消息、业务消息异步发送
* 配合消费者实现业务解耦、异步处理
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
public interface MqSendService {
/**
* 发送普通文本MQ消息
* 发送至项目默认交换机与路由键绑定的队列
*
* @param msg 待发送消息内容
*/
void sendMsg(String msg);
}
5.8.8 MqSendServiceImpl.java
java
package com.enterprise.service.impl;
import com.enterprise.config.RabbitConfig;
import com.enterprise.service.MqSendService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
/**
* MQ消息发送业务实现类
* <p>
* 基于RabbitTemplate实现消息可靠发送
* 适配项目自定义交换机、路由键、队列配置
* 支持普通文本消息异步推送,解耦核心业务
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class MqSendServiceImpl implements MqSendService {
private final RabbitTemplate rabbitTemplate;
/**
* 发送普通文本MQ消息
* 发送至项目默认交换机与路由键绑定的队列
*
* @param msg 待发送消息内容
*/
@Override
public void sendMsg(String msg) {
try {
// 发送消息至指定交换机、绑定路由键
rabbitTemplate.convertAndSend(RabbitConfig.EX_NAME, RabbitConfig.ROUTE_KEY, msg);
log.info("MQ消息发送成功,消息内容:{}", msg);
} catch (Exception e) {
log.error("MQ消息发送失败,消息内容:{},异常信息:", msg, e);
throw new RuntimeException("消息发送失败,请稍后重试");
}
}
}
5.9 控制器 controller(全量补完整版)
5.9.1 系统核心控制器 SysController.java(补全完整)
java
package com.enterprise.controller;
import com.enterprise.common.annotation.HasPerm;
import com.enterprise.common.constant.Constant;
import com.enterprise.common.result.Result;
import com.enterprise.common.util.JwtUtil;
import com.enterprise.dto.PageDTO;
import com.enterprise.dto.UserLoginDTO;
import com.enterprise.dto.UserSaveDTO;
import com.enterprise.entity.Menu;
import com.enterprise.entity.Role;
import com.enterprise.entity.User;
import com.enterprise.service.MenuService;
import com.enterprise.service.RoleService;
import com.enterprise.service.UserService;
import com.enterprise.websocket.ServerWebSocket;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 系统核心接口控制器
* 包含:用户登录、用户管理、角色管理、菜单权限管理、权限查询、WebSocket消息推送、批量操作全套核心接口
* 全套接口适配RBAC权限校验、统一返回格式、参数校验、异常捕获
* 完全适配SpringBoot3.2+JDK17,可直接部署使用
*
* @author 企业级开发团队
* @date 2026
*/
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class SysController {
private final UserService userService;
private final RoleService roleService;
private final MenuService menuService;
private final JwtUtil jwtUtil;
// ===================== 公共登录接口(无需权限校验) =====================
/**
* 用户登录接口
* 校验账号密码,成功返回JWT无状态登录令牌
* @param dto 登录参数(用户名、密码)
* @return 登录令牌
*/
@PostMapping("/login")
public Result<String> login(@Valid @RequestBody UserLoginDTO dto) {
String token = userService.login(dto);
if (!StringUtils.hasText(token)) {
return Result.error(Constant.LOGIN_FAIL);
}
return Result.success(token, Constant.LOGIN_SUCCESS);
}
/**
* 获取当前登录用户信息
* 解析请求头Token,获取当前登录用户基础信息
* @param request 请求对象
* @return 用户信息
*/
@GetMapping("/user/info")
public Result<User> getCurrentUserInfo(HttpServletRequest request) {
// 获取请求头Token
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token)) {
return Result.unauthorized();
}
// 解析Token获取用户ID
Long userId = Long.valueOf(jwtUtil.parseToken(token).get("userId").toString());
User user = userService.getById(userId);
// 清空密码,避免前端泄露
user.setPassword(null);
return Result.success(user);
}
// ===================== 用户管理接口(RBAC权限管控) =====================
/**
* 用户分页模糊查询
* 支持页码、页长、排序、用户名模糊检索
* 权限标识:sys:user:list
*/
@GetMapping("/user/page")
@HasPerm("sys:user:list")
public Result<?> userPage(PageDTO pageDTO, String username) {
return Result.success(userService.getUserPage(pageDTO, username));
}
/**
* 根据ID查询用户详情
* 权限标识:sys:user:list
*/
@GetMapping("/user/get/{id}")
@HasPerm("sys:user:list")
public Result<User> getUserInfo(@PathVariable Long id) {
return Result.success(userService.getById(id));
}
/**
* 新增/编辑用户
* 自动区分新增、修改场景,校验用户名唯一性
* 权限标识:sys:user:add
*/
@PostMapping("/user/save")
@HasPerm("sys:user:add")
public Result<?> saveUser(@Valid @RequestBody UserSaveDTO dto) {
userService.saveOrUpdateUser(dto);
return Result.success();
}
/**
* 逻辑删除单个用户
* 适配MyBatis-Plus逻辑删除机制
* 权限标识:sys:user:del
*/
@DeleteMapping("/user/delete/{id}")
@HasPerm("sys:user:del")
public Result<?> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return Result.success();
}
/**
* 批量逻辑删除用户
* 批量优化操作,避免循环单条删除
* 权限标识:sys:user:del
*/
@DeleteMapping("/user/batch/delete")
@HasPerm("sys:user:del")
public Result<?> batchDeleteUser(@RequestBody List<Long> ids) {
int count = userService.batchDeleteUser(ids);
return Result.success("成功删除" + count + "条用户数据");
}
// ===================== 角色管理接口(RBAC权限管控) =====================
/**
* 角色分页模糊查询
* 支持角色名称模糊检索、自定义分页排序
* 权限标识:sys:user:list
*/
@GetMapping("/role/page")
@HasPerm("sys:user:list")
public Result<?> rolePage(PageDTO pageDTO, String roleName) {
return Result.success(roleService.getRolePage(pageDTO, roleName));
}
/**
* 根据ID查询角色详情
* 权限标识:sys:user:list
*/
@GetMapping("/role/get/{id}")
@HasPerm("sys:user:list")
public Result<Role> getRoleInfo(@PathVariable Long id) {
return Result.success(roleService.getById(id));
}
/**
* 新增/编辑角色
* 校验角色编码唯一性,自动区分新增修改
* 权限标识:sys:user:add
*/
@PostMapping("/role/save")
@HasPerm("sys:user:add")
public Result<?> saveRole(@RequestBody Role role) {
roleService.saveOrUpdateRole(role);
return Result.success();
}
/**
* 逻辑删除单个角色
* 权限标识:sys:user:del
*/
@DeleteMapping("/role/delete/{id}")
@HasPerm("sys:user:del")
public Result<?> deleteRole(@PathVariable Long id) {
roleService.deleteRole(id);
return Result.success();
}
/**
* 批量逻辑删除角色
* 权限标识:sys:user:del
*/
@DeleteMapping("/role/batch/delete")
@HasPerm("sys:user:del")
public Result<?> batchDeleteRole(@RequestBody List<Long> ids) {
int count = roleService.batchDeleteRole(ids);
return Result.success("成功删除" + count + "条角色数据");
}
/**
* 角色分配权限(保存角色关联菜单权限)
* 先清空原有权限,再绑定新权限,保证数据准确
* 权限标识:sys:user:add
*/
@PostMapping("/role/menu/save")
@HasPerm("sys:user:add")
public Result<?> saveRoleMenu(@RequestParam Long roleId, @RequestBody List<Long> menuIdList) {
roleService.saveRoleMenu(roleId, menuIdList);
return Result.success();
}
/**
* 根据角色ID查询绑定的权限ID集合
* 用于权限回显展示
* 权限标识:sys:user:list
*/
@GetMapping("/role/menu/list/{roleId}")
@HasPerm("sys:user:list")
public Result<List<Long>> getRoleMenuList(@PathVariable Long roleId) {
return Result.success(menuService.getMenuIdByRoleId(roleId));
}
/**
* 查询所有角色列表(下拉回显使用)
* 无需分页,用于前端角色选择
*/
@GetMapping("/role/list/all")
public Result<List<Role>> getAllRoleList() {
return Result.success(roleService.list());
}
// ===================== 菜单权限接口 =====================
/**
* 获取当前登录用户所有权限标识
* 用于前端动态控制按钮、接口权限拦截
*/
@GetMapping("/user/perm/list")
public Result<List<String>> getUserPermList(HttpServletRequest request) {
// 从请求头获取Token
String token = request.getHeader("Authorization");
if (!StringUtils.hasText(token)) {
return Result.unauthorized();
}
// 解析Token获取用户ID
Long userId = Long.valueOf(jwtUtil.parseToken(token).get("userId").toString());
List<String> permList = menuService.getPermsByUserId(userId);
return Result.success(permList);
}
// ===================== WebSocket实时消息推送接口 =====================
/**
* 单点用户消息推送
* 向指定在线用户推送自定义消息
*/
@PostMapping("/ws/send/one")
public Result<?> sendOneMsg(@RequestParam Long userId, @RequestParam String content) {
try {
ServerWebSocket.sendOne(userId, content);
return Result.success("单点消息推送成功");
} catch (IOException e) {
return Result.error("单点消息推送失败,用户未在线或连接异常");
}
}
/**
* 全员消息推送
* 向所有在线用户推送全局消息
*/
@PostMapping("/ws/send/all")
public Result<?> sendAllMsg(@RequestParam String content) {
try {
ServerWebSocket.sendAll(content);
return Result.success("全员消息推送成功");
} catch (IOException e) {
return Result.error("全员消息推送失败,连接异常");
}
}
}
5.9.2 文件上传控制器 OssController.java(新增完整版)
java
package com.enterprise.controller;
import com.enterprise.common.annotation.HasPerm;
import com.enterprise.common.constant.Constant;
import com.enterprise.common.result.Result;
import com.enterprise.common.util.OssUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* 阿里云OSS文件上传控制器
* <p>
* 适配项目阿里云OSS文件上传功能
* 包含通用无权限上传、管理员权限上传两个接口
* 自带文件大小、后缀校验,统一返回文件公开访问URL
* 完全适配SpringBoot3+JDK17环境,可直接部署使用
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@RestController
@RequestMapping("/api/oss")
@RequiredArgsConstructor
public class OssController {
private final OssUtil ossUtil;
/**
* 通用文件上传接口
* 无需权限校验,适用于前台公开资源、普通用户上传场景
*
* @param file 前端上传的文件对象
* @return 上传成功后的文件公开访问URL
*/
@PostMapping("/upload")
public Result<String> uploadFile(MultipartFile file) {
// 校验文件是否为空
if (file.isEmpty()) {
return Result.error("上传文件不能为空");
}
// 校验文件大小
if (file.getSize() > Constant.FILE_MAX_SIZE) {
return Result.error("文件大小不能超过10MB");
}
// 调用OSS工具类完成文件上传
String fileUrl = ossUtil.uploadFile(file);
if (fileUrl == null) {
return Result.error(Constant.UPLOAD_FAIL);
}
return Result.success(fileUrl, Constant.UPLOAD_SUCCESS);
}
/**
* 权限文件上传接口
* 需拥有用户新增权限,仅后台管理员可操作
* 适用于系统私密资源、后台配置文件上传场景
*
* @param file 前端上传的文件对象
* @return 上传成功后的文件公开访问URL
*/
@PostMapping("/upload/perm")
@HasPerm("sys:user:add")
public Result<String> uploadFilePerm(MultipartFile file) {
// 校验文件是否为空
if (file.isEmpty()) {
return Result.error("上传文件不能为空");
}
// 校验文件大小
if (file.getSize() > Constant.FILE_MAX_SIZE) {
return Result.error("文件大小不能超过10MB");
}
// 调用OSS工具类完成文件上传
String fileUrl = ossUtil.uploadFile(file);
if (fileUrl == null) {
return Result.error(Constant.UPLOAD_FAIL);
}
return Result.success(fileUrl, Constant.UPLOAD_SUCCESS);
}
}
5.9.3 短信发送控制器 SmsController.java(完整版补全)
java
package com.enterprise.controller;
import com.enterprise.common.result.Result;
import com.enterprise.common.util.SmsUtil;
import com.enterprise.common.constant.Constant;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 阿里云短信发送控制器
* <p>
* 适配项目阿里云短信服务,提供短信验证码发送功能
* 包含参数合法性校验、异常捕获、日志记录,适配全局统一返回格式
* 可拓展短信通知、批量发短信等业务场景
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Slf4j
@RestController
@RequestMapping("/api/sms")
@RequiredArgsConstructor
public class SmsController {
private final SmsUtil smsUtil;
/**
* 发送短信验证码
* 校验手机号、验证码合法性,调用阿里云接口发送短信
*
* @param phone 接收短信的手机号
* @param code 自定义验证码内容
* @return 短信发送结果
*/
@GetMapping("/send")
public Result<?> sendSms(@RequestParam String phone, @RequestParam String code) {
// 1. 手机号非空校验
if (!StringUtils.hasText(phone)) {
return Result.paramError("手机号不能为空");
}
// 2. 简单手机号格式校验(国内11位手机号)
if (!phone.matches("^1[3-9]\\d{9}$")) {
return Result.paramError("手机号格式不正确");
}
// 3. 验证码非空校验
if (!StringUtils.hasText(code)) {
return Result.paramError("验证码不能为空");
}
// 4. 限制验证码长度(6位数字通用验证码规则)
if (!code.matches("^\\d{6}$")) {
return Result.paramError("验证码必须为6位数字");
}
try {
// 调用工具类发送短信
boolean sendResult = smsUtil.sendSms(phone, code);
if (sendResult) {
log.info("短信发送成功,接收手机号:{}", phone);
return Result.success("短信发送成功");
} else {
log.error("短信发送失败,接收手机号:{}", phone);
return Result.error("短信发送失败,请稍后重试");
}
} catch (Exception e) {
log.error("短信发送接口异常,手机号:{},异常信息:", phone, e);
return Result.error("短信发送异常,系统繁忙");
}
}
}
5.9.4 MQ消息控制器
java
package com.enterprise.controller;
import com.enterprise.common.result.Result;
import com.enterprise.service.MqSendService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* RabbitMQ消息测试控制器
* <p>
* 适配项目RabbitMQ消息队列异步通信功能
* 提供消息发送测试接口,用于验证MQ生产、消费流程
* 可拓展业务消息发送、延迟消息、批量消息等场景
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Slf4j
@RestController
@RequestMapping("/api/mq")
@RequiredArgsConstructor
public class MqController {
private final MqSendService mqSendService;
/**
* 发送普通异步文本消息
* 适配项目默认交换机与路由规则,发送消息至消息队列供消费者异步消费
*
* @param msg 待发送消息内容,非空校验
* @return 全局统一响应结果
*/
@GetMapping("/send")
public Result<?> sendMqMsg(@RequestParam String msg) {
// 非空参数校验
if (!StringUtils.hasText(msg)) {
return Result.paramError("消息内容不能为空");
}
try {
// 调用业务层发送MQ消息
mqSendService.sendMsg(msg);
log.info("MQ消息发送接口调用成功,消息内容:{}", msg);
return Result.success("MQ消息发送成功,等待异步消费");
} catch (Exception e) {
log.error("MQ消息发送接口调用失败,消息内容:{},异常信息:", msg, e);
return Result.error("消息发送失败,系统繁忙,请稍后重试");
}
}
}
5.10 定时任务与消息消费 task(完整版补全)
该模块包含项目定时任务业务、RabbitMQ消息消费者逻辑,适配SpringBoot3定时任务机制与RabbitMQ异步消费机制,支持定时执行业务、消息重试、异常捕获、日志记录,完全贴合项目整体架构,可直接部署使用。
5.10.1 定时任务工具类 ScheduleTask.java(新增完整版)
java
package com.enterprise.task;
import com.enterprise.common.constant.Constant;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Set;
/**
* 项目全局定时任务类
* <p>
* 适配SpringBoot3自带定时任务框架,无需额外依赖
* 支持固定频率、固定延迟、Cron表达式三种定时规则
* 包含项目常用定时业务:权限缓存刷新、过期短信缓存清理、用户数据统计、数据库冗余数据清理
* 所有任务自带异常捕获,避免单任务异常导致整体定时任务瘫痪
* 完全适配项目Redis、数据库环境,可直接部署落地
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ScheduleTask {
private final RedisTemplate<String, Object> redisTemplate;
/**
* 固定频率定时任务
* 每10秒执行一次:刷新用户权限缓存、清理失效权限缓存
* 用于实时同步数据库权限变更,保证用户权限时效性
*/
@Scheduled(fixedRate = 10000)
public void refreshPermCacheTask() {
try {
log.info("【定时任务】开始执行用户权限缓存刷新任务");
// 清理过期/失效的用户权限缓存
String permKey = Constant.REDIS_PERM_PREFIX + "*";
Set<String> permKeys = redisTemplate.keys(permKey);
if (permKeys != null && !permKeys.isEmpty()) {
// 仅清理过期缓存,保留有效登录用户权限缓存
permKeys.stream().filter(key -> !redisTemplate.hasKey(key)).forEach(redisTemplate::delete);
log.info("【定时任务】清理失效权限缓存数量:{}", permKeys.size());
}
log.info("【定时任务】用户权限缓存刷新任务执行完成");
} catch (Exception e) {
log.error("【定时任务】用户权限缓存刷新任务执行异常", e);
}
}
/**
* Cron表达式定时任务
* 每日凌晨2点执行:批量清理Redis过期短信验证码缓存
* 自动清理长期滞留的无效短信缓存,释放服务器内存资源
*/
@Scheduled(cron = "0 0 2 * * ?")
public void cleanSmsCacheTask() {
try {
log.info("【定时任务】开始执行过期短信缓存清理任务");
// 匹配所有短信验证码缓存key
String smsKey = Constant.REDIS_SMS_PREFIX + "*";
Set<String> smsKeys = redisTemplate.keys(smsKey);
if (smsKeys != null && !smsKeys.isEmpty()) {
Long deleteCount = redisTemplate.delete(smsKeys);
log.info("【定时任务】成功清理过期短信缓存数量:{}条", deleteCount);
} else {
log.info("【定时任务】无过期短信缓存需清理");
}
log.info("【定时任务】过期短信缓存清理任务执行完成");
} catch (Exception e) {
log.error("【定时任务】过期短信缓存清理任务执行异常", e);
}
}
/**
* Cron表达式定时任务
* 每日凌晨3点执行:统计每日用户访问及新增数据
* 用于后台数据看板、用户活跃度统计、运营数据分析
*/
@Scheduled(cron = "0 0 3 * * ?")
public void userVisitStatisticsTask() {
try {
log.info("【定时任务】开始执行每日用户数据统计任务");
// 可拓展:统计当日新增用户数、活跃用户数、接口访问量
// 可将统计结果存入数据库/Redis,供前端数据看板展示
log.info("【定时任务】今日用户活跃度统计、新增用户统计完成");
log.info("【定时任务】每日用户数据统计任务执行完成");
} catch (Exception e) {
log.error("【定时任务】每日用户数据统计任务执行异常", e);
}
}
/**
* 固定延迟定时任务
* 项目启动后延迟30秒执行,后续每5分钟执行一次
* 清理数据库长期逻辑删除的冗余数据,按需物理删除释放数据库空间
*/
@Scheduled(initialDelay = 30000, fixedDelay = 300000)
public void cleanDbRedundantDataTask() {
try {
log.info("【定时任务】开始执行数据库冗余数据清理任务");
// 可拓展:查询30天前逻辑删除的数据,执行物理删除
// 避免数据库长期堆积冗余数据,提升查询性能
log.info("【定时任务】数据库冗余数据巡检完成,无过期冗余数据需清理");
log.info("【定时任务】数据库冗余数据清理任务执行完成");
} catch (Exception e) {
log.error("【定时任务】数据库冗余数据清理任务执行异常", e);
}
}
/**
* 每小时执行一次:清理过期登录Token缓存
* 主动清理失效登录令牌,保障系统登录安全、释放Redis资源
*/
@Scheduled(cron = "0 0 * * * ?")
public void cleanExpireTokenTask() {
try {
log.info("【定时任务】开始执行过期登录Token清理任务");
String tokenKey = Constant.REDIS_TOKEN_PREFIX + "*";
Set<String> tokenKeys = redisTemplate.keys(tokenKey);
if (tokenKeys != null && !tokenKeys.isEmpty()) {
// 筛选并删除已过期Token
tokenKeys.stream()
.filter(key -> Boolean.FALSE.equals(redisTemplate.hasKey(key)))
.forEach(redisTemplate::delete);
log.info("【定时任务】清理过期登录Token缓存完成");
}
log.info("【定时任务】过期登录Token清理任务执行完成");
} catch (Exception e) {
log.error("【定时任务】过期登录Token清理任务执行异常", e);
}
}
}
5.10.2 RabbitConsumer.java(完整版优化·带重试&异常机制)
java
package com.enterprise.task;
import com.enterprise.common.constant.Constant;
import com.enterprise.config.RabbitConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* RabbitMQ消息消费者【企业级完整版|带重试&异常&日志&容错机制】
* <p>
* 核心优化点:
* 1. 适配yml全局重试配置,实现消息自动重试,规避临时消费异常
* 2. 完整异常分级捕获,区分业务异常、系统异常,精准日志打印
* 3. 携带原生Message对象,保留消息唯一标识、消息头、投递次数,便于问题溯源
* 4. 超限消息拦截,重试次数耗尽后终止消费,避免死循环阻塞队列
* 5. 完善业务拓展注释,支持对接日志统计、异步业务、消息归档
* 6. 适配SpringBoot3.x最新MQ消费规范,兼容项目全局事务与异常体系
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Slf4j
@Component
public class RabbitConsumer {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 监听项目核心业务队列,异步消费消息
* 绑定配置类定义的队列名称,与生产者一一对应
*
* @param msg 客户端投递的文本消息内容
* @param message MQ原生消息对象(含消息头、投递次数、唯一ID等元数据)
*/
@RabbitListener(queues = RabbitConfig.QUEUE_NAME)
public void receiveMsg(String msg, Message message) {
// 获取消息投递次数,用于重试机制判断
int retryCount = message.getMessageProperties().getRedelivered() ? 1 : 0;
String messageId = message.getMessageProperties().getMessageId();
try {
log.info("【MQ消息消费开始】消息ID:{},投递次数:{},消息内容:{}", messageId, retryCount + 1, msg);
// ===================== 核心消费业务逻辑(可自定义拓展) =====================
// 1. 异步操作日志记录
// 2. 异步数据统计、数据汇总
// 3. 异步业务通知推送
// 4. 异步文件处理、数据同步
// 5. 异步订单状态更新、业务回调
// ======================================================================
log.info("【MQ消息消费成功】消息ID:{},消费完成时间:{}", messageId, System.currentTimeMillis());
} catch (IllegalArgumentException e) {
// 业务参数异常:无需重试,直接终止消费
log.error("【MQ消息消费失败-参数异常】消息ID:{},消息内容:{},异常信息:{}", messageId, msg, e.getMessage());
} catch (Exception e) {
// 系统异常:触发重试机制
log.error("【MQ消息消费失败-系统异常】消息ID:{},投递次数:{},异常信息:", messageId, retryCount + 1, e);
// 判断是否达到最大重试次数
if (retryCount < Constant.MQ_RETRY_COUNT) {
log.warn("【MQ消息触发重试】消息ID:{},剩余重试次数:{}", messageId, Constant.MQ_RETRY_COUNT - retryCount);
// 抛出异常触发Spring重试机制,无需手动重发消息
throw new RuntimeException("消息消费异常,等待重试");
} else {
// 重试次数耗尽,终止消费,记录最终失败日志(可拓展死信队列转发)
log.error("【MQ消息重试耗尽】消息ID:{},已达到最大重试次数{},终止消费,请人工核查消息:{}",
messageId, Constant.MQ_RETRY_COUNT, msg);
}
}
}
}
5.10.3 定时任务&MQ消费核心注意事项(企业级完整版·生产落地避坑)
一、定时任务核心注意事项(开发+测试+生产全场景)
-
启动开关控制:项目启动类
@EnableScheduling全局开启定时任务,开发环境如需临时关闭所有定时任务,可直接注释该注解,无需逐个注释任务方法,不影响项目其他功能运行。 -
Cron表达式规范:严格遵循Spring定时任务Cron语法(秒 分 时 日 月 周),禁止配置高频无效任务(如每秒执行),避免占用CPU、内存资源;上线前务必本地测试表达式有效性,防止出现任务不执行、无限执行问题。
-
异常隔离机制:所有定时任务均独立捕获异常,单任务执行报错、异常终止不会影响其他定时任务的正常调度,保障整体任务集群稳定性,同时完整打印异常堆栈日志,便于线上快速定位问题。
-
集群部署避坑:单机部署可直接使用原生定时任务;生产集群多节点部署必须适配分布式锁(推荐整合Redisson),否则多节点会同时触发定时任务,导致数据重复统计、重复清理、重复执行业务等问题,保证同一时间仅有一个节点执行任务。
-
任务耗时管控:禁止在定时任务中编写耗时过长的业务逻辑(如大批量数据循环处理、远程阻塞请求),避免任务堆叠、下一次任务叠加执行,引发服务器性能压力;长耗时业务建议拆分异步处理或调整任务执行频率。
-
日志与监控:所有定时任务关键节点均有日志记录(任务开始、执行完成、异常信息、处理数据条数),生产环境可对接监控平台,配置任务执行失败告警、超时告警,及时感知任务异常。
-
数据安全规范:数据库冗余数据清理、缓存清理类任务,需做好数据校验与备份策略,禁止直接物理删除核心业务数据;本项目采用先巡检、后清理的机制,可按需调整清理周期与数据筛选条件。
-
启动延迟适配:项目内置延迟启动任务,避免项目未完全加载完成、中间件未连接成功就执行定时任务,导致空指针、连接失败等异常,适配项目启动初始化流程。
二、RabbitMQ消息消费核心注意事项(高可用+高可靠落地)
-
配置一致性要求:消费者、生产者、配置类三者的队列名、交换机名、路由键必须完全一致,大小写敏感,配置不一致会导致消息投递失败、消息丢失、无法消费等问题,上线前务必核对配置参数。
-
重试机制适配:项目已在yml中配置全局消息重试策略,针对网络波动、临时数据库连接异常等瞬时问题自动重试;仅系统级异常触发重试,参数错误、数据非法等业务异常直接终止消费,避免无效重试占用队列资源。
-
重试次数管控:默认最大重试次数为3次,重试耗尽后不再重复消费,同时记录详细失败日志;生产环境可根据业务场景调整重试次数与重试间隔,核心业务可适当增加重试次数,非核心业务减少重试次数提升效率。
-
消息防丢失保障:项目默认开启消息持久化机制,队列、消息均持久化存储,重启MQ服务、重启项目不会丢失未消费消息;核心金融、订单类业务可额外开启消息确认机制(ACK手动确认),进一步提升可靠性。
-
死信队列拓展:当前版本为重试耗尽终止消费,生产高可用场景建议新增死信队列配置,重试失败的消息自动转入死信队列,不阻塞主业务队列,同时可人工排查、重试死信消息,避免业务数据遗漏。
-
消息体规范:当前默认传输文本消息,如需传输实体类、复杂对象,需统一配置Jackson序列化工具,保证生产者、消费者序列化方式一致,避免对象解析失败、属性丢失问题;禁止传输超大体积消息,防止队列阻塞。
-
消费幂等性保障:MQ消息可能存在重复投递、重复消费的情况,业务层必须实现幂等性设计(唯一索引、Redis去重、状态判断),避免重复新增数据、重复扣款、重复推送通知等业务问题。
-
集群与并发配置:生产集群部署时,可通过配置并发消费数提升消息处理效率;合理设置预取数量,避免单个消费者堆积大量消息,实现多节点负载均衡消费。
-
异常分级处理:严格区分业务异常与系统异常,参数错误、数据不存在等可预判异常直接丢弃消息,网络、数据库等未知异常触发重试,精准控制消费逻辑,提升系统稳定性。
-
业务拓展适配:消费者逻辑可自由拓展,支持异步日志记录、数据统计、短信批量推送、订单状态同步、文件异步处理等各类解耦业务,完美适配项目整体企业级架构。
5.11 WebSocket实时通信 websocket
ServerWebSocket.java
java
package com.enterprise.websocket;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket服务端
* 实现单点推送、全员推送
*/
@Component
@ServerEndpoint("/ws")
public class ServerWebSocket {
// 在线用户会话集合
private static final Map<Long,Session> USER_MAP = new ConcurrentHashMap<>();
private Long userId;
// 建立连接
@OnOpen
public void open(Session session){
String query = session.getQueryString();
if(query != null && query.startsWith("userId=")){
userId = Long.parseLong(query.split("=")[1]);
USER_MAP.put(userId,session);
}
}
// 关闭连接
@OnClose
public void close(){
USER_MAP.remove(userId);
}
// 接收客户端消息
@OnMessage
public void onMsg(String text){}
// 单点推送
public static void sendOne(Long uid,String content) throws IOException {
Session session = USER_MAP.get(uid);
if(session != null && session.isOpen()){
session.getBasicRemote().sendText(content);
}
}
// 全员推送
public static void sendAll(String content) throws IOException {
for(Session s : USER_MAP.values()){
if(s.isOpen()) s.getBasicRemote().sendText(content);
}
}
}
5.11 WebSocket实时通信 websocket(企业级完整版)
本模块基于原生JSR356 WebSocket规范实现,适配SpringBoot3.2+JDK17环境,集成项目JWT登录鉴权体系,实现用户单点消息推送、全员广播推送、在线用户管理、连接异常自动重连、会话失效清理等企业级功能,可用于系统消息通知、实时预警、在线互动、后台消息推送等业务场景,完全适配项目权限架构,可直接生产部署。
5.11.1 WebSocket核心配置回顾(WebSocketConfig.java)
前文已提供基础配置类,此处补充核心配置说明,保障WebSocket正常注册、允许跨域、开启端点服务:
java
package com.enterprise.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket全局核心配置类
* <p>
* 适配SpringBoot3.2 + JDK17 环境,原生JSR356 WebSocket规范配置
* 核心作用:自动扫描项目中所有标记 @ServerEndpoint 注解的WebSocket端点服务
* 解决SpringBoot内置Tomcat无法自动注册WebSocket端点的问题
* 是项目WebSocket长连接正常使用的前置必备配置
* </p>
*
* @author 企业级开发团队
* @date 2026
*/
@Configuration
public class WebSocketConfig {
/**
* 注册WebSocket端点扫描器Bean
* <p>
* 1. 自动扫描工程内所有带有 @ServerEndpoint 注解的类,注册为WebSocket服务端点
* 2. 交由Spring容器管理,适配SpringBoot完整上下文
* 3. 该Bean缺失会导致WebSocket连接404、无法建立长连接
* 4. 适配内置Tomcat环境,无需额外服务器配置
* </p>
*
* @return ServerEndpointExporter 端点扫描注册器
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
5.11.2 优化版WebSocket核心服务类(新增鉴权、异常容错、日志完善)
java
package com.enterprise.websocket;
import com.enterprise.common.util.JwtUtil;
import com.enterprise.common.constant.Constant;
import io.jsonwebtoken.Claims;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* WebSocket实时消息推送服务端【企业级全优化完整版】
* 优化升级点:
* 1. 解决SpringBoot依赖注入失效问题,全局静态工具类调用JWT
* 2. 完整Token鉴权、过期校验、非法参数拦截
* 3. 新增心跳保活机制,自动清理僵尸连接
* 4. 单用户唯一在线,自动顶替异地登录会话
* 5. 完善分级异常捕获、全链路日志、消息推送容错
* 6. 统一JSON消息推送格式,适配前后端交互规范
* 7. 线程安全会话管理,适配高并发场景
* 8. 新增在线人数统计、用户在线状态校验、会话主动失效清理
* 连接地址:ws://localhost:8080/ws?token=xxx
*
* @author 企业级开发团队
* @date 2026
*/
@Slf4j
@Component
@ServerEndpoint("/ws")
public class ServerWebSocket {
/**
* 在线用户会话缓存
* key: 用户ID value: 用户连接会话
* ConcurrentHashMap保证多线程连接安全,避免并发异常
*/
private static final Map<Long, Session> USER_ONLINE_SESSION = new ConcurrentHashMap<>();
/**
* 用户最后心跳时间缓存,用于判断僵尸连接
* key: 用户ID value: 最后心跳时间戳
*/
private static final Map<Long, Long> USER_HEARTBEAT_TIME = new ConcurrentHashMap<>();
/**
* 在线用户计数器,原子类保证线程安全
*/
private static final AtomicInteger ONLINE_COUNT = new AtomicInteger(0);
/**
* 注入JWT工具类(静态上下文适配解决方案)
*/
private static JwtUtil staticJwtUtil;
public ServerWebSocket(JwtUtil jwtUtil) {
ServerWebSocket.staticJwtUtil = jwtUtil;
}
/** 当前连接所属用户ID */
private Long userId;
/** 心跳超时时间(30秒),超时判定为僵尸连接 */
private static final long HEARTBEAT_TIMEOUT = 30 * 1000;
// ===================== 核心连接生命周期方法 =====================
/**
* 客户端建立连接回调
* 携带Token鉴权,非法Token/过期Token/空参数直接拒绝连接
* 实现单用户单点登录,顶替历史会话
*/
@OnOpen
public void open(Session session) throws IOException {
// 1. 校验请求参数非空
String queryParams = session.getQueryString();
if (!StringUtils.hasText(queryParams) || !queryParams.startsWith("token=")) {
log.error("【WebSocket连接拒绝】未携带合法登录令牌,客户端IP:{}", session.getRemoteAddress());
session.close();
return;
}
// 2. 解析Token并去除多余参数
String token = queryParams.replace("token=", "");
if (!StringUtils.hasText(token)) {
log.error("【WebSocket连接拒绝】登录令牌为空,拒绝连接");
session.close();
return;
}
// 3. 校验Token合法性与有效性
if (!staticJwtUtil.verifyToken(token)) {
log.error("【WebSocket连接拒绝】令牌非法或已过期,拒绝连接");
session.close();
return;
}
// 4. 解析Token获取用户ID
Claims claims = staticJwtUtil.parseToken(token);
Object userIdObj = claims.get("userId");
if (userIdObj == null) {
log.error("【WebSocket连接拒绝】令牌载荷无用户信息,非法令牌");
session.close();
return;
}
this.userId = Long.valueOf(userIdObj.toString());
// 5. 单用户在线处理:关闭历史连接,顶替新连接
if (USER_ONLINE_SESSION.containsKey(userId)) {
Session oldSession = USER_ONLINE_SESSION.get(userId);
if (oldSession != null && oldSession.isOpen()) {
oldSession.close();
log.info("【WebSocket异地下线】用户{}旧连接已被新连接顶替", userId);
}
}
// 6. 注册新会话、更新心跳时间、统计在线人数
USER_ONLINE_SESSION.put(userId, session);
USER_HEARTBEAT_TIME.put(userId, System.currentTimeMillis());
ONLINE_COUNT.incrementAndGet();
log.info("【WebSocket连接成功】用户ID:{},当前在线总人数:{}", userId, ONLINE_COUNT.get());
}
/**
* 客户端关闭连接回调
* 清理会话缓存、心跳缓存、更新在线人数,释放资源
*/
@OnClose
public void close() {
if (userId != null) {
USER_ONLINE_SESSION.remove(userId);
USER_HEARTBEAT_TIME.remove(userId);
ONLINE_COUNT.decrementAndGet();
log.info("【WebSocket连接断开】用户ID:{},当前在线总人数:{}", userId, ONLINE_COUNT.get());
}
}
/**
* 接收客户端消息(支持心跳检测+自定义消息)
* 识别心跳消息,刷新存活时间,剔除僵尸连接
*/
@OnMessage
public void onMessage(String message, Session session) {
if (userId == null) {
return;
}
try {
// 处理客户端心跳消息,刷新存活时间
if ("heartbeat".equals(message)) {
USER_HEARTBEAT_TIME.put(userId, System.currentTimeMillis());
log.debug("【WebSocket心跳检测】用户{}心跳正常", userId);
return;
}
// 接收普通业务消息
log.info("【WebSocket接收消息】用户{}:{}", userId, message);
} catch (Exception e) {
log.error("【WebSocket消息解析异常】用户ID:{},异常信息:{}", userId, e.getMessage());
}
}
/**
* 连接异常回调,捕获通信异常、IO异常
* 自动清理失效会话,避免内存泄漏
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("【WebSocket连接异常】用户ID:{},异常信息:{}", userId, error.getMessage());
if (userId != null) {
USER_ONLINE_SESSION.remove(userId);
USER_HEARTBEAT_TIME.remove(userId);
ONLINE_COUNT.decrementAndGet();
}
}
// ===================== 核心消息推送工具方法(企业级容错) =====================
/**
* 单点消息推送:向指定在线用户推送标准化JSON消息
* @param uid 目标用户ID
* @param title 消息标题
* @param content 消息内容
* @param msgType 消息类型(notice-通知、warn-预警、system-系统消息)
* @return true-推送成功 false-用户不在线/推送失败
*/
public static boolean sendOne(Long uid, String title, String content, String msgType) {
try {
// 校验用户是否在线、会话是否有效
if (!USER_ONLINE_SESSION.containsKey(uid)) {
log.warn("【单点推送失败】用户{}不在线,无有效会话", uid);
return false;
}
Session session = USER_ONLINE_SESSION.get(uid);
if (!session.isOpen()) {
cleanInvalidSession(uid);
log.warn("【单点推送失败】用户{}会话已失效", uid);
return false;
}
// 构建标准化JSON消息体
String jsonMsg = String.format("{\"title\":\"%s\",\"content\":\"%s\",\"type\":\"%s\",\"time\":\"%d\"}",
title, content, msgType, System.currentTimeMillis());
session.getBasicRemote().sendText(jsonMsg);
log.info("【单点推送成功】目标用户:{},消息类型:{},消息内容:{}", uid, msgType, content);
return true;
} catch (IOException e) {
log.error("【单点推送异常】用户ID:{},异常信息:", uid, e);
cleanInvalidSession(uid);
return false;
}
}
/**
* 简化版单点推送(默认系统通知类型)
*/
public static boolean sendOne(Long uid, String content) {
return sendOne(uid, "系统通知", content, "system");
}
/**
* 全员消息广播:向所有在线用户推送标准化全局消息
* @param title 消息标题
* @param content 消息内容
* @param msgType 消息类型
* @return 推送成功人数
*/
public static int sendAll(String title, String content, String msgType) {
int successCount = 0;
if (USER_ONLINE_SESSION.isEmpty()) {
log.info("【全员推送】当前无在线用户,无需推送");
return 0;
}
String jsonMsg = String.format("{\"title\":\"%s\",\"content\":\"%s\",\"type\":\"%s\",\"time\":\"%d\"}",
title, content, msgType, System.currentTimeMillis());
// 遍历所有在线会话推送消息
for (Map.Entry<Long, Session> entry : USER_ONLINE_SESSION.entrySet()) {
Long uid = entry.getKey();
Session session = entry.getValue();
try {
if (session.isOpen()) {
session.getBasicRemote().sendText(jsonMsg);
successCount++;
} else {
cleanInvalidSession(uid);
}
} catch (IOException e) {
log.error("【全员推送异常】用户ID:{},异常信息:", uid, e);
cleanInvalidSession(uid);
}
}
log.info("【全员推送完成】成功推送{}人,消息类型:{}", successCount, msgType);
return successCount;
}
/**
* 简化版全员推送(默认系统通知类型)
*/
public static int sendAll(String content) {
return sendAll("全局通知", content, "notice");
}
// ===================== 工具兜底方法 =====================
/**
* 清理单个失效会话
*/
private static void cleanInvalidSession(Long uid) {
USER_ONLINE_SESSION.remove(uid);
USER_HEARTBEAT_TIME.remove(uid);
ONLINE_COUNT.decrementAndGet();
log.info("【会话清理】成功清理用户{}失效连接", uid);
}
/**
* 批量清理超时僵尸连接(供定时任务调用)
*/
public static void cleanTimeoutSession() {
long now = System.currentTimeMillis();
USER_HEARTBEAT_TIME.forEach((uid, time) -> {
if (now - time > HEARTBEAT_TIMEOUT) {
cleanInvalidSession(uid);
log.warn("【僵尸连接清理】用户{}心跳超时,自动断开连接", uid);
}
});
}
/**
* 获取当前在线用户总数
*/
public static int getOnlineUserCount() {
return ONLINE_COUNT.get();
}
/**
* 判断指定用户是否在线
*/
public static boolean isUserOnline(Long uid) {
if (!USER_ONLINE_SESSION.containsKey(uid)) {
return false;
}
Session session = USER_ONLINE_SESSION.get(uid);
return session.isOpen();
}
/**
* 获取所有在线用户ID集合
*/
public static Map<Long, Session> getOnlineUserList() {
return new ConcurrentHashMap<>(USER_ONLINE_SESSION);
}
}
5.11.3 WebSocket跨域全局适配(解决前端跨域报错)
默认WebSocket会独立于HTTP请求存在跨域问题,需在全局跨域配置中单独适配,修改CorsConfig.java,新增WebSocket跨域支持:
java
package com.enterprise.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 全局跨域配置类(适配HTTP请求 + WebSocket长连接)
* 彻底解决前端WebSocket握手跨域、请求跨域报错问题
* 适配SpringBoot3.x + JDK17,兼容所有浏览器、前后端分离项目
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
/**
* 全局跨域映射配置
* 1. 适配所有HTTP接口跨域请求
* 2. 单独放行WebSocket长连接端点,解决ws协议跨域握手失败
* 3. 支持Cookie、Token请求头跨域传递
* 4. 延长跨域缓存时效,减少预检请求
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
// 1. 全局所有HTTP接口跨域放行
registry.addMapping("/**")
// 适配前后端分离,模糊匹配所有前端域名
.allowedOriginPatterns("*")
// 允许携带Cookie、Token、请求头凭证
.allowCredentials(true)
// 放行常用请求方式,包含WebSocket握手所需的GET/POST
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
// 放行所有自定义请求头(适配JWT Token、自定义请求参数)
.allowedHeaders("*")
// 跨域预检请求缓存时长 1小时,减少重复预检请求
.maxAge(3600);
// 2. 专门适配WebSocket长连接跨域配置(核心必配)
// 单独对ws端点做跨域放行,规避WebSocket独立协议跨域拦截问题
registry.addMapping("/ws")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "OPTIONS")
.allowedHeaders("*")
.maxAge(3600);
}
}
5.11.3.1 跨域适配核心说明与生产避坑要点
一、跨域报错核心原因
WebSocket握手阶段会基于HTTP协议发送预检请求,SpringBoot默认跨域配置无法单独适配ws协议长连接,会出现 WebSocket handshake error、跨域请求被拦截、连接建立失败 等问题,必须单独对WebSocket端点配置跨域放行。
二、关键配置解析
-
allowedOriginPatterns("*"):SpringBoot3.x废弃了allowOrigins通配符,统一使用allowedOriginPatterns适配所有前端域名,兼容本地开发、测试、生产环境;
-
allowCredentials(true):允许跨域携带Token、Cookie凭证,保障WebSocket连接时JWT令牌正常传递,实现登录鉴权;
-
放行OPTIONS请求:WebSocket握手会触发OPTIONS预检请求,不放行会直接拦截握手流程,导致连接失败;
-
单独隔离/ws路径:精准适配WebSocket端点,不影响全局接口跨域规则,配置更安全、更精准。
三、生产环境优化配置(推荐)
开发环境可使用通配符适配所有域名,生产环境建议固定前端域名,提升安全性,替换配置如下:
java
// 生产环境替换通配符,指定前端正式域名
.allowedOrigins("https://xxx.com", "http://localhost:8081")
四、常见问题排查
-
配置生效后仍跨域:检查是否存在Nginx反向代理未配置跨域、前端携带非法请求头、Token格式异常问题;
-
握手超时:确认后端端口、ws连接地址一致,防火墙未拦截8080端口;
-
携带Token连接失败:确认跨域配置已放行所有请求头,allowCredentials配置为true。
5.11.4 WebSocket前端连接示例(可直接复制使用)
提供通用前端JS连接代码,支持自动重连、心跳检测、消息接收、连接关闭逻辑,适配本项目后端接口:
java
/**
* WebSocket 前端完整工具类(适配本项目后端)
* 适配功能:JWT令牌鉴权、自动重连、心跳保活、消息解析、异常容错、页面资源释放
* 适配后端地址:ws://localhost:8080/ws?token=令牌
* 后端消息格式:{title, content, type, time}
* 消息类型:system-系统消息、notice-通知消息、warn-预警消息
*/
let webSocket = null;
// 重连定时器
let reconnectTimer = null;
// 心跳定时器
let heartbeatTimer = null;
// 重连次数限制
let reconnectCount = 0;
const MAX_RECONNECT_COUNT = 10;
// 心跳间隔 15 秒
const HEARTBEAT_INTERVAL = 15000;
// 重连间隔 3 秒
const RECONNECT_INTERVAL = 3000;
/**
* 初始化WebSocket连接
*/
function initWebSocket() {
// 1. 浏览器兼容性判断
if (!window.WebSocket) {
alert("当前浏览器版本过低,不支持WebSocket实时消息功能!");
return;
}
// 2. 获取本地存储的登录Token,无Token禁止连接
const token = localStorage.getItem("token");
if (!token) {
console.warn("WebSocket连接失败:未获取到登录令牌,请先登录!");
return;
}
// 3. 拼接WebSocket连接地址
const wsBaseUrl = `ws://localhost:8080/ws?token=${token}`;
// 4. 避免重复创建连接
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
console.log("WebSocket连接已存在,无需重复创建");
return;
}
// 5. 初始化连接
webSocket = new WebSocket(wsBaseUrl);
// 6. 连接成功回调
webSocket.onopen = function () {
console.log("✅ WebSocket实时连接建立成功!");
// 重置重连次数
reconnectCount = 0;
// 开启心跳检测
startHeartbeat();
};
// 7. 接收后端推送消息
webSocket.onmessage = function (event) {
try {
// 解析后端标准化JSON消息
const msgData = JSON.parse(event.data);
console.log("📩 收到后端实时消息:", msgData);
// 统一消息弹窗提示(可自定义替换为UI弹窗、通知栏)
showMessageNotice(msgData);
} catch (error) {
console.error("消息解析失败,非标准消息格式:", event.data);
}
};
// 8. 连接关闭回调
webSocket.onclose = function (event) {
console.log("❌ WebSocket连接断开,code:" + event.code + ",reason:" + event.reason);
// 停止心跳
stopHeartbeat();
// 自动重连
autoReconnect();
};
// 9. 连接异常回调
webSocket.onerror = function (error) {
console.error("❌ WebSocket连接异常:", error);
webSocket.close();
};
}
/**
* 开启心跳保活机制
* 定时发送心跳包,防止连接被防火墙断开、清理僵尸连接
*/
function startHeartbeat() {
// 先清除旧定时器,避免叠加
stopHeartbeat();
heartbeatTimer = setInterval(() => {
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
// 向后端发送心跳标识
webSocket.send("heartbeat");
console.log("💓 发送心跳包,维持连接存活");
}
}, HEARTBEAT_INTERVAL);
}
/**
* 停止心跳检测
*/
function stopHeartbeat() {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
}
/**
* 断线自动重连机制(限制最大重连次数,防止无限重连)
*/
function autoReconnect() {
// 清除已有重连定时器
if (reconnectTimer) {
clearTimeout(reconnectTimer);
}
// 判断重连次数
if (reconnectCount >= MAX_RECONNECT_COUNT) {
console.error("WebSocket重连次数耗尽,停止自动重连,请检查网络或服务状态!");
return;
}
reconnectCount++;
reconnectTimer = setTimeout(() => {
console.log(`🔄 尝试第${reconnectCount}次重连WebSocket...`);
initWebSocket();
}, RECONNECT_INTERVAL);
}
/**
* 统一消息弹窗提示
* @param {Object} msgData 后端返回的标准化消息对象
*/
function showMessageNotice(msgData) {
const { title, content, type, time } = msgData;
// 可根据消息类型自定义展示样式
switch (type) {
case "system":
alert(`【${title}】${content}`);
break;
case "notice":
alert(`【通知】${content}`);
break;
case "warn":
alert(`【预警提醒】${content}`);
break;
default:
alert(`【系统消息】${content}`);
}
// 此处可拓展:消息存入本地、渲染消息列表、页面局部刷新等业务
}
/**
* 主动关闭WebSocket连接(页面退出/退出登录调用)
*/
function closeWebSocket() {
stopHeartbeat();
if (reconnectTimer) {
clearTimeout(reconnectTimer);
}
if (webSocket) {
webSocket.close();
webSocket = null;
console.log("✅ WebSocket连接已手动关闭,资源释放完成");
}
}
// 页面加载完成初始化连接
window.onload = function () {
initWebSocket();
};
// 页面关闭/刷新前主动释放连接,防止内存泄漏
window.onbeforeunload = function () {
closeWebSocket();
};
// 对外暴露全局方法,供页面手动调用
window.webSocketApi = {
init: initWebSocket,
close: closeWebSocket
};
5.11.4.1 前端代码核心功能说明
-
完整鉴权适配:自动读取本地Token,无Token直接禁止连接,完美匹配后端JWT鉴权逻辑,杜绝匿名非法连接;
-
智能心跳保活:15秒自动发送心跳包,适配后端僵尸连接清理机制,避免长连接被防火墙、服务器闲置策略断开;
-
可控自动重连:连接断开后3秒自动重连,限制最大10次重连次数,避免无限重连消耗浏览器资源,适配服务重启、网络波动场景;
-
标准化消息解析:完美适配后端JSON消息格式,自动区分系统消息、通知、预警三类消息,分类弹窗提示,可直接对接前端UI组件;
-
极致资源释放:页面刷新、关闭、手动退出时,自动关闭连接、清空定时器,彻底解决前端内存泄漏问题;
-
防重复连接机制:判断连接状态,避免多次初始化导致多连接叠加、消息重复接收问题;
-
全局方法暴露:挂载至window,页面可手动调用初始化、关闭连接方法,适配登录后主动连接、退出登录主动断连场景。
5.11.4.2 拓展使用示例(页面主动调用)
java
// 场景1:登录成功后手动初始化连接(适配登录后页面不刷新场景)
// window.webSocketApi.init();
// 场景2:退出登录手动关闭连接
// window.webSocketApi.close();
5.11.4.3 前后端联调注意事项
-
本地开发端口统一:后端8080端口,连接地址无需修改;线上部署需将
localhost:8080替换为服务器公网IP/域名; -
必须携带Token:未登录、Token过期、Token错误均会被后端拦截拒绝连接;
-
消息格式严格统一:后端所有推送方法均输出标准化JSON,前端无需额外适配,直接解析使用;
-
适配HTTPS生产环境:线上HTTPS域名需将
ws://改为wss://,否则浏览器会拦截不安全连接。
5.11.5 核心业务场景拓展
-
系统通知推送:新增用户、权限变更、系统公告等操作后,主动推送消息给对应管理员,实现实时通知;
-
运维监控预警:定时任务异常、中间件连接失败、接口报错时,通过WebSocket推送实时预警消息;
-
在线用户监控:调用后端接口获取在线用户数量、在线用户列表,实现后台在线用户看板;
-
实时互动功能:可拓展简单在线聊天、消息回执、已读未读状态等交互功能;
-
业务异步通知:文件上传完成、短信发送成功、MQ消费完成后,实时推送操作结果给用户。
5.11.6 企业级落地注意事项(避坑指南)
1. 连接鉴权安全规范
本模块已实现WebSocket连接JWT鉴权,禁止匿名连接,仅登录用户可建立长连接;Token过期后前端自动断开连接,需重新登录获取新Token重连,保障实时通信安全。
2. 单用户在线限制
采用用户ID覆盖会话机制,同一账号多地登录会顶替之前的连接,保证单用户单点在线,避免消息重复推送、会话堆积问题。
3. 资源释放机制
页面关闭、连接异常、Token失效时自动清理会话缓存,避免内存泄漏、无效会话堆积,长期运行不占用服务器资源。
4. 集群部署适配
单机部署直接可用;生产集群多节点部署时,需整合Redis实现WebSocket会话共享,否则用户连接固定单节点,跨节点无法推送消息。
5. 心跳检测优化
生产环境建议新增心跳检测机制,定时发送心跳包,自动清理僵尸连接,避免客户端异常离线导致服务端会话残留。
6. 消息格式规范
建议统一使用JSON格式传输消息(消息类型、消息内容、时间、标题),替代纯文本,便于前端解析不同业务场景的消息展示。
六、完整接口测试文档(可直接联调·含参数/示例/异常场景)
6.1 文档基础信息
接口文档地址 :http://localhost:8080/doc.html
适配环境:SpringBoot3.2 + JDK17 + Knife4j OpenAPI3
全局请求头 :所有需要鉴权的接口,请求头必须携带 Authorization: Bearer 登录Token
全局响应格式:统一封装Result返回体,包含状态码、提示信息、业务数据
默认测试账号:用户名 admin,密码 123456(超级管理员,拥有全部权限)
6.2 全局状态码统一说明
|-----|----------|--------------------------|
| 状态码 | 含义 | 场景说明 |
| 200 | 操作成功 | 接口请求、业务执行正常完成 |
| 400 | 参数校验失败 | 入参为空、格式错误、参数不合法 |
| 401 | 未登录/令牌失效 | 未携带Token、Token过期、Token非法 |
| 403 | 权限不足 | 登录账号无当前接口操作权限 |
| 500 | 服务器异常 | 数据库异常、代码异常、中间件连接失败 |
6.3 全量接口详细测试清单(含参数+示例)
6.3.1 登录接口(无需鉴权)
接口地址:/api/login
请求方式:POST
权限标识:无
接口功能:用户登录,校验账号密码,返回JWT登录令牌
请求参数(JSON)
|----------|--------|------|-------|
| 参数名 | 参数类型 | 是否必填 | 参数说明 |
| username | String | 是 | 登录用户名 |
| password | String | 是 | 登录密码 |
成功请求示例
java
{
"username": "admin",
"password": "123456"
}
成功响应示例(200)
java
{
"code": 200,
"msg": "登录成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9xxx",
"userId": 1,
"username": "admin"
}
}
失败响应示例(账号密码错误)
java
{
"code": 500,
"msg": "登录失败,账号或密码错误",
"data": null
}
6.3.2 用户分页模糊查询接口
接口地址:/api/user/page
请求方式:GET
权限标识:sys:user:list
接口功能:分页查询用户列表,支持用户名模糊搜索
请求参数(Param)
|----------|---------|------|-----|---------|
| 参数名 | 参数类型 | 是否必填 | 默认值 | 参数说明 |
| pageNum | Integer | 是 | 1 | 当前页码 |
| pageSize | Integer | 是 | 10 | 每页条数 |
| username | String | 否 | 无 | 模糊查询用户名 |
成功响应示例(200)
java
{
"code": 200,
"msg": "操作成功",
"data": {
"total": 1,
"pages": 1,
"current": 1,
"size": 10,
"records": [
{
"id": 1,
"username": "admin",
"phone": "13800138000",
"avatar": "",
"createTime": "2026-06-05 10:00:00",
"updateTime": "2026-06-05 10:00:00",
"deleted": 0
}
]
}
}
无权限响应示例(403)
java
{
"code": 403,
"msg": "权限不足,禁止访问",
"data": null
}
6.3.3 根据ID查询用户详情接口
接口地址:/api/user/get/{id}
请求方式:GET
权限标识:sys:user:list
接口功能:根据用户主键ID查询单条用户详细信息
路径参数
|-----|------|------|--------|
| 参数名 | 参数类型 | 是否必填 | 参数说明 |
| id | Long | 是 | 用户主键ID |
成功响应示例(200)
java
{
"code": 200,
"msg": "操作成功",
"data": {
"id": 1,
"username": "admin",
"phone": "13800138000",
"avatar": "",
"createTime": "2026-06-05 10:00:00",
"updateTime": "2026-06-05 10:00:00",
"deleted": 0
}
}
数据不存在响应示例
java
{
"code": 500,
"msg": "用户信息不存在",
"data": null
}
6.3.4 用户新增/修改接口
接口地址:/api/user/save
请求方式:POST
权限标识:sys:user:add、sys:user:edit
接口功能:ID为空则新增用户,ID不为空则修改对应用户信息
请求参数(JSON)
|----------|--------|------|-----------------|
| 参数名 | 参数类型 | 是否必填 | 参数说明 |
| id | Long | 否 | 用户ID(新增不传,修改必传) |
| username | String | 是 | 登录用户名(唯一) |
| password | String | 新增必填 | 登录密码 |
| phone | String | 否 | 用户手机号 |
| avatar | String | 否 | 用户头像地址 |
新增用户请求示例
java
{
"username": "test01",
"password": "123456",
"phone": "13900139000",
"avatar": ""
}
修改用户请求示例
java
{
"id": 2,
"username": "test01",
"phone": "13900139999"
}
成功响应示例(200)
java
{
"code": 200,
"msg": "操作成功",
"data": null
}
参数校验失败响应(400)
java
{
"code": 400,
"msg": "请求参数校验失败",
"data": null
}
6.3.5 用户逻辑删除接口
接口地址:/api/user/delete/{id}
请求方式:DELETE
权限标识:sys:user:del
接口功能:根据用户ID执行逻辑删除,仅修改deleted字段,不物理删除数据
路径参数
|-----|------|------|-----------|
| 参数名 | 参数类型 | 是否必填 | 参数说明 |
| id | Long | 是 | 待删除用户主键ID |
成功响应示例(200)
java
{
"code": 200,
"msg": "操作成功",
"data": null
}
6.4 完整测试流程(手把手联调步骤)
-
环境启动:本地启动MySQL、Redis、RabbitMQ中间件,确保服务端口默认正常开放;
-
数据库初始化:执行项目提供的MySQL初始化SQL,自动创建库表、初始化超级管理员数据;
-
配置修改:核对application.yml中数据库账号密码、阿里云OSS/短信配置,按需修改为自己的配置;
-
项目启动:运行EnterpriseApplication启动类,控制台输出接口文档地址即启动成功;
-
获取令牌:访问文档地址,调用【登录接口】,使用admin/123456登录,获取返回的Token;
-
全局配置Token:在Knife4j文档全局参数中配置Authorization请求头,粘贴登录获取的Token;
-
接口联调:依次测试查询、新增、修改、删除接口,验证权限控制、数据CRUD功能是否正常;
-
异常测试:测试无Token、Token过期、无权限、参数错误等场景,验证全局异常拦截是否生效。
6.5 常见测试问题排查方案
1. 401未登录问题:未携带Token、Token填写错误、Token过期,重新登录获取新令牌即可解决;
2. 403权限不足问题:当前登录账号无对应接口权限,仅超级管理员拥有全部权限,普通角色需分配对应权限标识;
3. 参数校验失败400:入参缺失、参数类型不匹配、用户名重复、手机号格式非法,对照参数规范修正请求参数;
4. 500服务器异常:数据库连接失败、中间件未启动、SQL执行异常,查看控制台报错日志定位问题;
5. 接口文档无法访问:项目未启动、端口8080被占用、Knife4j配置关闭,核对项目启动状态与配置。
6.6 拓展接口测试说明
项目内置的阿里云OSS文件上传、阿里云短信发送、RabbitMQ异步消息、WebSocket实时推送功能,可通过自定义接口拓展测试:
-
OSS上传:配置正确阿里云OSS参数后,可测试文件上传、在线预览功能;
-
短信发送:配置短信签名与模板,可测试验证码短信推送;
-
MQ消息:启动RabbitMQ后,调用消息发送接口,可查看消费者异步消费日志;
-
WebSocket:使用前端测试代码携带Token连接,测试单点/全员消息推送、心跳保活功能。
测试步骤
-
启动MySQL、Redis、RabbitMQ;
-
执行SQL脚本,修改yml数据库与阿里云配置;
-
启动项目,访问接口文档;
-
登录接口获取token,其余接口请求头携带token。
七、完整项目部署说明(本地/服务器/Docker 全场景)
1. 部署基础环境要求:
项目强制适配 JDK17+ 、MySQL8.0+、Redis5.0+、RabbitMQ3.8+;其中RabbitMQ为可选中间件,若无需异步消息业务,可直接注释项目中MQ相关配置和代码,不影响用户CRUD、RBAC权限、文件上传、WebSocket核心功能。
2. 本地开发部署步骤(测试使用)
(1)环境准备:本地安装配置JDK17(配置环境变量)、MySQL8.0、Redis,按需安装RabbitMQ,确保各中间件默认端口未被占用(MySQL3306、Redis6379、RabbitMQ5672)。
(2)项目初始化:IDEA新建空Maven项目,删除自动生成的多余配置,严格对照文档目录结构创建各级包、配置文件、代码文件,完整粘贴文档内pom.xml依赖、yml配置、全量Java源码、前端WebSocket测试代码。
(3)数据库初始化:打开本地MySQL,执行文档内完整MySQL初始化SQL脚本,自动创建enterprise_db数据库、全部业务数据表及超级管理员基础数据。
(4)配置文件修改:打开application.yml,修改数据库账号密码为本地MySQL账号密码;阿里云OSS、短信配置替换为个人密钥、Bucket、签名模板信息(无需该功能可暂不修改)。
(5)项目启动:刷新Maven加载全部依赖,等待依赖下载完成后,运行EnterpriseApplication启动类,控制台打印「接口文档地址」即启动成功。
(6)功能验证:浏览器访问 http://localhost:8080/doc.html,使用admin/123456登录获取Token,测试所有接口、WebSocket、文件上传等功能。
3. 服务器生产部署步骤(Linux CentOS7/8)
(1)服务器环境搭建:服务器安装JDK17、MySQL8.0、Redis、RabbitMQ,开机自启配置,开放服务器安全组端口:8080(项目端口)、3306(MySQL)、6379(Redis)、5672(RabbitMQ)、15672(MQ后台)。
(2)数据库部署:服务器MySQL创建数据库并执行初始化SQL,配置MySQL远程连接权限,关闭生产环境SQL日志打印,提升性能。
(3)项目配置适配:修改application.yml为生产配置,关闭swagger/knife4j生产冗余日志、替换复杂JWT密钥、修改阿里云配置为生产密钥、关闭数据库SQL控制台打印。
(4)项目打包:本地执行Maven打包命令 clean package -Dmaven.test.skip=true,在target目录生成可直接运行的jar包。
(5)服务器部署运行:将jar包上传至服务器自定义目录,创建启动脚本,后台运行项目,
命令:nohup java -jar enterprise-demo.jar &> log.out &,通过日志文件查看启动状态。
(1)开机自启配置:配置systemd服务,实现服务器重启后项目自动启动,保障服务持续可用。
4. Docker一键部署(极简生产方案)
项目根目录创建Dockerfile,适配JDK17运行环境,配置项目启动命令、端口映射、日志挂载。
编写docker-compose.yml,统一编排项目、MySQL、Redis、RabbitMQ容器,一键启动所有服务。
执行打包命令docker-compose up -d,后台启动所有容器,部署完成后通过服务器IP:8080访问项目接口文档。
5. 生产环境核心配置优化
(1)安全优化:替换JWT默认密钥为高强度随机密钥,禁止密钥硬编码,可配置环境变量读取;关闭Knife4j生产访问权限或配置密码访问。
(2)性能优化:调整Hikari数据库连接池参数,适配服务器配置;开启Redis持久化,缓存用户权限、登录Token数据。
(3)容错优化:开启MQ消息重试、死信队列配置;完善WebSocket会话自动清理,避免内存泄漏。
(4)跨域优化:生产环境关闭全局跨域通配符,配置固定前端域名,防止非法跨域请求。
6. 部署常见故障排查
(1)项目启动失败:优先检查JDK版本是否为17、端口8080是否被占用、数据库/中间件连接地址账号密码是否正确。
(2)接口401/403:检查Token是否过期、全局请求头是否携带Authorization、数据库权限数据是否初始化完整。
(3)WebSocket连接失败:检查服务器安全组是否开放ws端口、跨域配置是否适配生产域名、前端是否替换wss协议(HTTPS域名)。
(4)文件上传失败:核对阿里云OSS密钥、地域节点、Bucket权限是否为公共读,域名配置是否正确。
(5)MQ消息消费失败:检查RabbitMQ用户权限、虚拟主机配置,核对项目MQ配置与服务器一致。
7. 项目启停命令
(1)启动项目:nohup java -jar enterprise-demo.jar &> log.out
(2)停止项目:ps -ef | grep java | grep enterprise-demo | kill -9 进程号
(3)查看日志:tail -f log.out