返回对象(common)
package top.yxqz.springboot.common;
import lombok.Data;
@Data
public class Result<T> {
private String code;
private String msg;
private T data;
public Result() {
}
public Result(T data) {
this.data = data;
}
public Result(String code, String msg) {
this.code = code;
this.msg = msg;
}
public Result(String code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static <T> Result<T> success() {
return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg());
}
public static <T> Result<T> success(T data) {
return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), data);
}
public static <T> Result<T> success(String msg, T data) {
return new Result<>(ResultCode.SUCCESS.getCode(), msg, data);
}
public static <T> Result<T> error() {
return new Result<>(ResultCode.ERROR.getCode(), ResultCode.ERROR.getMsg());
}
public static <T> Result<T> error(String msg) {
return new Result<>(ResultCode.ERROR.getCode(), msg);
}
public static <T> Result<T> error(String code, String msg) {
return new Result<>(code, msg);
}
public static <T> Result<T> error(String code, String msg, T data) {
return new Result<>(code, msg, data);
}
/**
* 判断操作是否成功
*/
public boolean isSuccess() {
return ResultCode.SUCCESS.getCode().equals(this.code);
}
/**
* 获取错误消息
*/
public String getMessage() {
return this.msg;
}
}返回状态密码(common)
package top.yxqz.springboot.common;
public enum ResultCode {
SUCCESS("200", "操作成功"),
ERROR("-1", "操作失败"),
VALIDATE_FAILED("404", "参数检验失败"),
UNAUTHORIZED("401", "暂未登录或token已经过期"),
FORBIDDEN("403", "没有相关权限"),
SYSTEM_ERROR("500", "系统错误"),
// 参数相关错误
PARAM_ERROR("400", "参数错误"),
PARAM_MISSING("4001", "缺少必要参数"),
PARAM_INVALID("4002", "参数格式不正确"),
// 文件操作相关错误
FILE_NOT_FOUND("5001", "文件不存在"),
FILE_UPLOAD_FAILED("5002", "文件上传失败"),
FILE_DELETE_FAILED("5003", "文件删除失败"),
FILE_SIZE_EXCEEDED("5004", "文件大小超过限制"),
FILE_TYPE_NOT_SUPPORTED("5005", "不支持的文件类型"),
FILE_NAME_INVALID("5006", "文件名不合法"),
FILE_CONTENT_INVALID("5007", "文件内容不合法"),
FILE_SAVE_FAILED("5008", "文件保存失败"),
// 业务相关错误
BUSINESS_ERROR("6000", "业务处理失败"),
ACCOUNT_SAME("6001", "用户名已存在"),
USER_NOT_EXIST("6002", "用户不存在");
private String code;
private String msg;
ResultCode(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String code() {
return code;
}
public String getCode() {
return code;
}
public String getMsg() {
return msg;
}
} 业务异常类(exception)
package top.yxqz.springboot.exception;
import lombok.Getter;
/**
* 业务异常类
* @author 余小小
*/
@Getter
public class BusinessException extends RuntimeException {
private final String code;
public BusinessException(String message) {
super(message);
this.code = "BUSINESS_ERROR";
}
public BusinessException(String code, String message) {
super(message);
this.code = code;
}
public BusinessException(String message, Throwable cause) {
super(message, cause);
this.code = "BUSINESS_ERROR";
}
public BusinessException(String code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
}全局异常(exception)
package top.yxqz.springboot.exception;
import jakarta.validation.ConstraintViolationException;
import top.yxqz.springboot.common.Result;
import top.yxqz.springboot.common.ResultCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}
/**
* 处理服务异常
*/
@ExceptionHandler(ServiceException.class)
public Result<?> handleServiceException(ServiceException e) {
log.error("服务异常: {}", e.getMessage(), e);
return Result.error(e.getCode(), e.getMessage());
}
/**
* 参数验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
log.error(e.getMessage(), e);
String message = e.getBindingResult().getFieldError().getDefaultMessage();
return Result.error(ResultCode.VALIDATE_FAILED.getCode(), message);
}
/**
* 约束违反异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public Result<?> handleConstraintViolationException(ConstraintViolationException e) {
log.error(e.getMessage(), e);
return Result.error(ResultCode.VALIDATE_FAILED.getCode(), e.getMessage());
}
/**
* 处理其他异常
*/
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error(e.getMessage(), e);
return Result.error(ResultCode.SYSTEM_ERROR.getCode(), "系统错误");
}
} 服务异常(exception)
package top.yxqz.springboot.exception;
import lombok.Getter;
/**
* @author 余小小
*/
@Getter
public class ServiceException extends RuntimeException {
private final String code;
private final String msg;
public ServiceException(String code, String msg) {
super(msg);
this.code = code;
this.msg = msg;
}
public ServiceException(String msg) {
super(msg);
this.code = "-1";
this.msg = msg;
}
} 日期工具类(util)
package top.yxqz.springboot.util;
import java.time.LocalDate;
public class DateUtils {
/**
* 计算上个月的第一天。
* @return 上个月第一天的LocalDate对象。
*/
public static LocalDate getLastMonthFirstDay() {
LocalDate today = LocalDate.now(); // 获取当前日期
int currentMonth = today.getMonthValue(); // 获取当前月份
int currentYear = today.getYear(); // 获取当前年份
// 如果当前月份是1月,则上个月是去年的12月
if (currentMonth == 1) {
return LocalDate.of(currentYear - 1, 12, 1);
} else {
// 否则,上个月的第一天就是当前年加上上个月的月份减1,日期为1
return LocalDate.of(currentYear, currentMonth - 1, 1);
}
}
}文件操作工具类(util)(后期引入)
package top.yxqz.springboot.util;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import top.yxqz.springboot.enums.FileBusinessTypeEnum;
import top.yxqz.springboot.enums.FileTypeEnum;
import top.yxqz.springboot.exception.BusinessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 文件操作工具类
* 专注于文件的基础操作:保存、删除、检查等
* 文件类型验证统一使用FileTypeEnum
*
* @author 余小小
*/
public class FileUtil {
private final static Logger log = LoggerFactory.getLogger(FileUtil.class);
public final static String FILE_BASE_PATH = System.getProperty("user.dir") + "/files/";
private static final String ROOT_PATH = "/files/";
/**
* 将访问路径转换为相对物理路径
* @param filename 访问路径,可能包含/files/前缀
* @return 相对物理路径,不包含前缀
*/
public static String convertToRelativePath(String filename) {
if (StrUtil.isBlank(filename)) {
return filename;
}
// 如果包含ROOT_PATH前缀,去掉它,避免路径重复
if (filename.startsWith(ROOT_PATH)) {
return filename.substring(ROOT_PATH.length());
}
// 如果路径有前导斜杠,移除它
else if (filename.startsWith("/")) {
return filename.substring(1);
}
return filename;
}
/**
* 最大文件大小(500MB)
*/
private static final long maxFileSize = 524288000L; // 500MB (500 * 1024 * 1024)
/**
* 获取最大文件大小
* @return 最大文件大小(字节)
*/
public static long getMaxFileSize() {
return maxFileSize;
}
/**
* 安全的文件保存方法
* 统一使用FileTypeEnum进行文件类型验证
*
* @param file 上传的文件
* @param folderName 子目录名称,位于relativeDir(可选)
* @param relativeDir 基础目录(应与FileTypeEnum的code对应)
* @return 文件访问路径
* @throws BusinessException 文件保存失败时抛出
*/
public static String saveFile(MultipartFile file, String relativeDir,String folderName) {
try {
log.info("开始保存文件,原始文件名:{},目标目录:{}", file.getOriginalFilename(), relativeDir);
validateBasicFile(file);
String originalFilename = file.getOriginalFilename();
// 获取文件扩展名
String extension = getFileExtension(originalFilename);
if (StrUtil.isBlank(extension)) {
log.error("文件没有扩展名:{}", originalFilename);
throw new BusinessException("文件没有扩展名");
}
// 生成唯一文件名
long timestamp = System.currentTimeMillis();
String uniqueFilename = timestamp + extension.toLowerCase();
// 构造安全的保存路径
Path fileDirectory = buildSafeFilePath(relativeDir, folderName);
if (fileDirectory == null) {
throw new BusinessException("构建文件保存路径失败");
}
// 创建目录
if (!Files.exists(fileDirectory)) {
Files.createDirectories(fileDirectory);
log.info("创建目录:{}", fileDirectory);
}
// 保存文件
Path uploadFilePath = fileDirectory.resolve(uniqueFilename);
File uploadFile = uploadFilePath.toFile();
file.transferTo(uploadFile);
log.info("文件保存成功:{}", uploadFile.getAbsolutePath());
// 返回相对路径
String relativePath = ROOT_PATH + relativeDir + "/" +
(StrUtil.isNotBlank(folderName) ? folderName + "/" : "") + uniqueFilename;
log.info("返回文件访问路径:{}", relativePath);
return relativePath;
} catch (IOException e) {
log.error("文件保存异常,文件名:{},错误:{}", file.getOriginalFilename(), e.getMessage(), e);
throw new BusinessException("文件保存失败: " + e.getMessage());
} catch (BusinessException e) {
// 业务异常直接抛出
throw e;
} catch (Exception e) {
log.error("文件保存时发生未知异常,文件名:{},错误:{}", file.getOriginalFilename(), e.getMessage(), e);
throw new BusinessException("文件保存失败: " + e.getMessage());
}
}
/**
* 保存图片的便捷方法
*/
public static String saveImage(MultipartFile file, String folderName) {
return saveFile(file, folderName, "img");
}
/**
* 保存视频的便捷方法
*/
public static String saveVideo(MultipartFile file, String folderName) {
return saveFile(file, folderName, "video");
}
/**
* 安全的文件删除方法
*/
public static boolean deleteFile(String filename) {
try {
log.info("开始删除文件:{}", filename);
validateName(filename);
// 转换为相对物理路径
filename = convertToRelativePath(filename);
// 获取文件的绝对路径
Path filePath = Paths.get(FILE_BASE_PATH, filename);
// 验证文件路径是否在允许的目录内
Path basePath = Paths.get(FILE_BASE_PATH);
if (!filePath.toAbsolutePath().startsWith(basePath.toAbsolutePath())) {
log.error("文件路径超出允许范围:{}", filePath);
return false;
}
if (Files.exists(filePath)) {
Files.delete(filePath);
log.info("文件删除成功:{}", filePath);
return true;
} else {
log.warn("文件不存在:{}", filePath);
return false;
}
} catch (Exception e) {
log.error("文件删除异常,文件名:{},错误:{}", filename, e.getMessage(), e);
return false;
}
}
/**
* 写入文件内容
*/
public static void writeToFile(String fileName, String content) throws IOException {
log.info("开始写入文件:{}", fileName);
if (StrUtil.isBlank(content)) {
throw new IllegalArgumentException("内容不能为空");
}
//安全性检查
validateName(fileName);
// 创建文件对象
File file = new File(fileName);
log.info("文件绝对路径:{}", file.getAbsolutePath());
// 使用 try-with-resources 确保 FileWriter 在使用完毕后自动关闭
try (FileWriter fileWriter = new FileWriter(file)) {
fileWriter.write(content);
log.info("文件写入成功:{}", fileName);
} catch (IOException e) {
log.error("文件写入失败,文件名:{},错误:{}", fileName, e.getMessage(), e);
throw e;
}
}
/**
* 检查文件是否存在
*/
public static boolean fileExists(String filename) {
if (StrUtil.isBlank(filename)) {
return false;
}
try {
// 转换为相对物理路径
filename = convertToRelativePath(filename);
Path filePath = Paths.get(FILE_BASE_PATH, filename);
return Files.exists(filePath);
} catch (Exception e) {
log.error("检查文件存在性异常,文件名:{},错误:{}", filename, e.getMessage());
return false;
}
}
/**
* 获取文件大小
*/
public static long getFileSize(String filename) {
if (StrUtil.isBlank(filename)) {
return -1;
}
try {
// 转换为相对物理路径
filename = convertToRelativePath(filename);
Path filePath = Paths.get(FILE_BASE_PATH, filename);
if (Files.exists(filePath)) {
return Files.size(filePath);
}
return -1;
} catch (Exception e) {
log.error("获取文件大小异常,文件名:{},错误:{}", filename, e.getMessage());
return -1;
}
}
// ==================== 私有辅助方法 ====================
/**
* 获取文件扩展名
*/
public static String getFileExtension(String filename) {
if (StrUtil.isBlank(filename)) {
return "";
}
int dotIndex = filename.lastIndexOf('.');
if (dotIndex > 0 && dotIndex < filename.length() - 1) {
return filename.substring(dotIndex);
}
return "";
}
/**
* 构建安全的文件路径
*/
private static Path buildSafeFilePath(String relativeDir, String folderName) {
try {
Path projectRootPath = Paths.get(FILE_BASE_PATH);
String filePath=relativeDir;
// 如果folderName不为null,则在指定目录后面加入folderName
if (StringUtils.isNotBlank(folderName)) {
// 验证folderName的安全性
validateName(folderName);
filePath=relativeDir + File.separator + folderName;
}
return projectRootPath.resolve(filePath);
} catch (Exception e) {
log.error("构建文件路径失败,baseDir:{},folderName:{},错误:{}", relativeDir, folderName, e.getMessage());
return null;
}
}
public static void validateBasicFile(MultipartFile file) {
//验证是否为空
if (file.isEmpty()) {
throw new BusinessException("上传文件不能为空");
}
//文件大小验证
if (file.getSize() > maxFileSize) {
throw new BusinessException(String.format(
"文件大小超出限制,当前: %d 字节,最大允许: %d 字节",
file.getSize(), maxFileSize
));
}
//文件名安全性检查
String originalName = file.getOriginalFilename();
validateName(originalName);
}
/**
* 文件名安全性与非空性检查
* @param name 文件(夹)名
*/
public static void validateName(String name){
//验证文件名是否为空
if (!StringUtils.isNotBlank(name)) {
throw new BusinessException("文件(夹)名不能为空");
}
// 检查危险字符
String[] dangerousChars = {"..", "\\", ":", "*", "?", "\"", "<", ">", "|"};
for (String dangerousChar : dangerousChars) {
if (name.contains(dangerousChar)) {
throw new BusinessException("文件(夹)名包含非法字符: " + dangerousChar);
}
}
}
/**
* 解析文件类型为目录名(小写)
*/
public static String parseFileTypeToRelativeDir(String fileType) {
if (StrUtil.isBlank(fileType)) {
return FileTypeEnum.OTHER.getCode();
}
if(!(FileTypeEnum.isAllowType(fileType))) {
return "commom";
}
return fileType.toLowerCase();
}
/**
* 解析业务文件类型为目录名
*/
public static String parseBussinessFileTypeToFolerName(String bussinessType) {
if (StrUtil.isBlank(bussinessType)) {
return FileTypeEnum.OTHER.getCode();
}
if(!FileBusinessTypeEnum.isAllowedFileBussinessType(bussinessType)) {
return "commom";
}
return bussinessType.toLowerCase();
}
/**
* 构建完整的文件路径(用于检查文件是否存在)
* @param originalFilename 原始文件名
* @param relativeDir 相对目录
* @param folderName 子目录名称(可选)
* @return 完整的文件路径(相对于FILE_BASE_PATH)
*/
public static String buildFullFilePath(String originalFilename, String relativeDir, String folderName) {
if (StrUtil.isBlank(originalFilename)) {
return null;
}
StringBuilder pathBuilder = new StringBuilder();
pathBuilder.append("files/").append(relativeDir).append("/");
if (StrUtil.isNotBlank(folderName)) {
pathBuilder.append(folderName).append("/");
}
pathBuilder.append(originalFilename);
return pathBuilder.toString();
}
/**
* 构建带时间戳的唯一文件路径
* @param originalFilename 原始文件名
* @param relativeDir 相对目录
* @param folderName 子目录名称(可选)
* @return 带时间戳的文件路径
*/
public static String buildUniqueFilePath(String originalFilename, String relativeDir, String folderName) {
if (StrUtil.isBlank(originalFilename)) {
return null;
}
// 获取文件扩展名
String extension = getFileExtension(originalFilename);
if (StrUtil.isBlank(extension)) {
return null;
}
// 生成唯一文件名
long timestamp = System.currentTimeMillis();
String uniqueFilename = timestamp + extension.toLowerCase();
return buildFullFilePath(uniqueFilename, relativeDir, folderName);
}
}JWT工具类(util)(后期引入)
package top.yxqz.springboot.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import top.yxqz.springboot.dto.response.UserDetailResponseDTO;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Date;
/**
* JWT工具类 - 用于JWT token的生成、验证和用户信息获取
*
* 主要功能:
* 1. 生成JWT token(包含userId、username、roleType)
* 2. 验证JWT token有效性和过期检查
* 3. 从请求属性中获取当前用户信息(由JwtAuthenticationFilter设置)
*
* 使用说明:
* - Token的解析和提取由JwtAuthenticationFilter负责
* - Controller中使用getCurrentUser()和getCurrentUserId()获取当前用户信息
* - 不要直接操作token,所有token相关逻辑在Filter中处理
*
* Token传输规范:
* - 只支持标准的 Authorization: Bearer <token> 方式
*
* 安全特性:
* - 使用HMAC256算法签名
* - Token有效期7天
* - 完善的异常处理和日志记录
*/
@Slf4j
public class JwtTokenUtils {
/**
* JWT密钥
*/
private static final String SECRET = "drone_management_system_jwt_secret_key_2024";
/**
* Token过期时间(7天)
*/
private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L;
/**
* Token发行者
*/
private static final String ISSUER = "drone-management-system";
/**
* 生成JWT token
* @param userId 用户ID
* @param username 用户名
* @param roleType 角色代码
* @return JWT token
*/
public static String generateToken(Long userId, String username, String roleType) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
return JWT.create()
.withClaim("userId", userId)
.withClaim("username", username)
.withClaim("roleType", roleType)
.withExpiresAt(expireDate)
.withIssuedAt(new Date())
.withIssuer(ISSUER)
.sign(algorithm);
} catch (Exception e) {
log.error("生成JWT token失败", e);
throw new RuntimeException("生成JWT token失败", e);
}
}
/**
* 验证JWT token有效性
* @param token JWT token
* @return 解码后的JWT
* @throws JWTVerificationException token验证失败
*/
public static DecodedJWT verifyToken(String token) throws JWTVerificationException {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(ISSUER)
.build();
return verifier.verify(token);
}
/**
* 检查token是否过期
* @param token JWT token
* @return 是否过期
*/
public static boolean isTokenExpired(String token) {
try {
DecodedJWT jwt = verifyToken(token);
return jwt.getExpiresAt().before(new Date());
} catch (Exception e) {
return true;
}
}
/**
* 获取当前请求的用户ID(从RequestContextHolder获取)
* @return 当前用户ID,获取失败返回null
*/
public static Long getCurrentUserId() {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
Object userId = request.getAttribute("currentUserId");
if (userId instanceof Long) {
return (Long) userId;
}
}
} catch (Exception e) {
log.error("获取当前用户ID失败", e);
}
return null;
}
/**
* 获取当前请求的用户信息(从请求属性中获取)
* 注意:此方法依赖于JwtAuthenticationFilter设置的请求属性
* @return 当前用户对象,获取失败返回null
*/
public static UserDetailResponseDTO getCurrentUser() {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
return (UserDetailResponseDTO)request.getAttribute("currentUser");
}
} catch (Exception e) {
log.error("获取当前用户对象失败", e);
}
return null;
}
/**
* 获取当前用户ID的字符串形式
* @return 当前用户ID字符串,获取失败返回null
*/
public static String getCurrentUserIdAsString() {
Long userId = getCurrentUserId();
return userId != null ? String.valueOf(userId) : null;
}
/**
* 获取当前用户类型/角色
* @return 当前用户类型,获取失败返回null
*/
public static String getCurrentUserType() {
UserDetailResponseDTO currentUser = getCurrentUser();
if(currentUser==null)return null;
return currentUser.getUserType();
}
/**
* 判断当前用户是否为管理员
* @return 是否为管理员
*/
public static boolean isAdmin() {
return "ADMIN".equals(getCurrentUserType());
}
}