feat: 增强文件上传功能和菜单管理优化

- 新增文件上传预检查功能,支持文件格式和大小验证
- 完善全局异常处理器,增加文件上传异常处理
- 优化菜单管理,添加菜单类型枚举,提升代码可读性
- 完善常量类,新增数据计数阈值常量
- 统一文件上传逻辑,支持图片和文档分类管理
This commit is contained in:
Leo 2025-07-08 19:55:47 +08:00
parent 9d17718747
commit 77d2ad543b
6 changed files with 348 additions and 11 deletions

View File

@ -126,4 +126,19 @@ public class CoderConstants {
*/
public static final String CODER_COMMON = "CODER_COMMON";
/**
* 数据计数阈值 - 多条记录
*/
public static final Long COUNT_THRESHOLD_MULTIPLE = 1L;
/**
* 数据计数阈值 - 单条记录
*/
public static final Long COUNT_THRESHOLD_SINGLE = 1L;
/**
* 数据计数阈值 - 存在性判断
*/
public static final Long COUNT_THRESHOLD_EXISTS = 0L;
}

View File

@ -0,0 +1,68 @@
package org.leocoder.thin.domain.enums.menu;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author Leocoder
* @description [菜单状态枚举]
*/
@Getter
@AllArgsConstructor
public enum MenuStatusEnum {
/**
* 启用
*/
ENABLED("0", "启用"),
/**
* 停用
*/
DISABLED("1", "停用");
/**
* 状态值
*/
private final String value;
/**
* 状态描述
*/
private final String description;
/**
* 根据值获取枚举
*
* @param value 状态值
* @return 对应的枚举
*/
public static MenuStatusEnum getByValue(String value) {
for (MenuStatusEnum status : values()) {
if (status.getValue().equals(value)) {
return status;
}
}
return null;
}
/**
* 判断是否是启用状态
*
* @param value 状态值
* @return 是否是启用状态
*/
public static boolean isEnabled(String value) {
return ENABLED.getValue().equals(value);
}
/**
* 判断是否是停用状态
*
* @param value 状态值
* @return 是否是停用状态
*/
public static boolean isDisabled(String value) {
return DISABLED.getValue().equals(value);
}
}

View File

@ -0,0 +1,83 @@
package org.leocoder.thin.domain.enums.menu;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author Leocoder
* @description [菜单类型枚举]
*/
@Getter
@AllArgsConstructor
public enum MenuTypeEnum {
/**
* 目录
*/
DIRECTORY("1", "目录"),
/**
* 菜单
*/
MENU("2", "菜单"),
/**
* 按钮
*/
BUTTON("3", "按钮");
/**
* 类型值
*/
private final String value;
/**
* 类型描述
*/
private final String description;
/**
* 根据值获取枚举
*
* @param value 类型值
* @return 对应的枚举
*/
public static MenuTypeEnum getByValue(String value) {
for (MenuTypeEnum type : values()) {
if (type.getValue().equals(value)) {
return type;
}
}
return null;
}
/**
* 判断是否是按钮类型
*
* @param value 类型值
* @return 是否是按钮类型
*/
public static boolean isButton(String value) {
return BUTTON.getValue().equals(value);
}
/**
* 判断是否是目录类型
*
* @param value 类型值
* @return 是否是目录类型
*/
public static boolean isDirectory(String value) {
return DIRECTORY.getValue().equals(value);
}
/**
* 判断是否是菜单类型
*
* @param value 类型值
* @return 是否是菜单类型
*/
public static boolean isMenu(String value) {
return MENU.getValue().equals(value);
}
}

View File

@ -7,6 +7,7 @@ import com.alibaba.excel.util.StringUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.leocoder.thin.common.constants.CoderConstants;
import org.leocoder.thin.common.exception.BusinessException;
import org.leocoder.thin.common.satoken.CoderLoginUtil;
import org.leocoder.thin.common.utils.file.FileTypeUtil;
import org.leocoder.thin.common.utils.file.UploadUtil;
@ -21,6 +22,9 @@ import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
@ -37,6 +41,26 @@ public class FileController {
@Value("${coder.filePath}")
private String basePath;
// 允许的图片文件扩展名
private static final List<String> ALLOWED_IMAGE_EXTENSIONS = Arrays.asList(
"jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"
);
// 允许的文档文件扩展名
private static final List<String> ALLOWED_DOCUMENT_EXTENSIONS = Arrays.asList(
"doc", "docx", "pdf", "txt", "xls", "xlsx", "ppt", "md", "zip", "rar", "7z"
);
// 文件大小限制字节
// 1MB
private static final long MAX_FILE_SIZE_1MB = 1048576L;
// 2MB
private static final long MAX_FILE_SIZE_2MB = 2097152L;
// 5MB
private static final long MAX_FILE_SIZE_5MB = 5242880L;
// 10MB
private static final long MAX_FILE_SIZE_10MB = 10485760L;
private final Environment env;
private final SysFileService sysFileService;
@ -52,13 +76,18 @@ public class FileController {
@Operation(summary = "上传文件", description = "上传单个文件到服务器")
@PostMapping("/file/uploadFile/{fileSize}/{folderName}/{fileParam}")
public Map<String, Object> uploadSingleFile(@RequestParam("file") MultipartFile file, @PathVariable("fileSize") Integer fileSize, @PathVariable("folderName") String folderName, @PathVariable("fileParam") String fileParam) {
// 文件预检查
validateUploadFile(file, fileSize, folderName);
Map<String, Object> fileMap = UploadUtil.coderSingleFile(file, basePath + "/" + folderName + "/"+ CoderLoginUtil.getLoginName() + "/", fileSize);
// 统一保存到文件表便于文件管理页面统一显示
saveUploadFilesInformation(fileMap, CoderConstants.TRUE);
// 如果是图片同时保存到图库表保持图库管理功能
if (CoderConstants.PICTURES.equals(folderName)) {
// 保存图库信息
saveUploadPicturesInformation(fileMap, fileParam, CoderConstants.TRUE);
} else {
// 保存文件信息
saveUploadFilesInformation(fileMap, CoderConstants.TRUE);
}
return fileMap;
}
@ -82,7 +111,7 @@ public class FileController {
protocol = "http";
}
String hostIp = IpUtil.getHostIp(ServletUtil.getRequest());
String hostPort = StringUtils.isNotBlank(env.getProperty("server.port")) ? env.getProperty("server.port") : "18088";
String hostPort = StringUtils.isNotBlank(env.getProperty("server.port")) ? env.getProperty("server.port") : "18099";
log.info("IP地址{},端口号:{}", hostIp, env.getProperty("server.port"));
sysPicture.setPicturePath(protocol + "://" + hostIp + ":" + hostPort + fileMap.get("fileUploadPath").toString());
log.info("图片回显地址:{}", sysPicture.getPicturePath());
@ -138,9 +167,96 @@ public class FileController {
@SaIgnore
@PostMapping("/file/uploadAnyFile/{fileSize}/{folderName}/{fileParam}")
public Map<String, Object> uploadAnyFile(@RequestParam("file") MultipartFile file, @PathVariable("fileSize") Integer fileSize, @PathVariable("folderName") String folderName, @PathVariable("fileParam") String fileParam) {
// 文件预检查
validateUploadFile(file, fileSize, folderName);
Map<String, Object> fileMap = UploadUtil.coderSingleFile(file, basePath + "/" + folderName + "/", fileSize);
// 统一保存到文件表
saveUploadFilesInformation(fileMap, false);
// 如果是图片同时保存到图库表
if (CoderConstants.PICTURES.equals(folderName)) {
saveUploadPicturesInformation(fileMap, fileParam, false);
}
return fileMap;
}
/**
* @description [文件上传预检查]
* @author Leocoder
*/
private void validateUploadFile(MultipartFile file, Integer fileSizeLimit, String folderName) {
// 检查文件是否为空
if (file == null || file.isEmpty()) {
throw new BusinessException(400, "请选择要上传的文件");
}
// 检查文件大小是否超限
long fileSize = file.getSize();
long maxSizeBytes = convertMBToBytes(fileSizeLimit);
if (fileSize > maxSizeBytes) {
String sizeMsg = fileSizeLimit + "MB";
throw new BusinessException(413, "文件大小超出限制,最大允许上传" + sizeMsg + "的文件");
}
// 获取文件扩展名
String originalFilename = file.getOriginalFilename();
if (StringUtils.isBlank(originalFilename)) {
throw new BusinessException(400, "文件名不能为空");
}
String fileExtension = getFileExtension(originalFilename).toLowerCase();
if (StringUtils.isBlank(fileExtension)) {
throw new BusinessException(400, "文件必须有扩展名");
}
// 根据文件夹类型检查文件格式
if (CoderConstants.PICTURES.equals(folderName)) {
// 图片文件检查
if (!ALLOWED_IMAGE_EXTENSIONS.contains(fileExtension)) {
throw new BusinessException(400, "不支持的图片格式,仅支持:" + String.join(", ", ALLOWED_IMAGE_EXTENSIONS));
}
} else {
// 文档文件检查 - 允许图片和文档类型
if (!ALLOWED_IMAGE_EXTENSIONS.contains(fileExtension) && !ALLOWED_DOCUMENT_EXTENSIONS.contains(fileExtension)) {
List<String> allAllowedExtensions = new ArrayList<>();
allAllowedExtensions.addAll(ALLOWED_IMAGE_EXTENSIONS);
allAllowedExtensions.addAll(ALLOWED_DOCUMENT_EXTENSIONS);
throw new BusinessException(400, "不支持的文件格式,仅支持:" + String.join(", ", allAllowedExtensions));
}
}
log.info("文件验证通过:文件名={}, 大小={}bytes, 类型={}", originalFilename, fileSize, fileExtension);
}
/**
* @description [将MB转换为字节]
* @author Leocoder
*/
private long convertMBToBytes(Integer fileSizeMB) {
if (fileSizeMB == null || fileSizeMB <= 0) {
// 默认2MB
return MAX_FILE_SIZE_2MB;
}
return fileSizeMB * 1024L * 1024L;
}
/**
* @description [获取文件扩展名]
* @author Leocoder
*/
private String getFileExtension(String filename) {
if (StringUtils.isBlank(filename)) {
return "";
}
int lastDotIndex = filename.lastIndexOf('.');
if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) {
return "";
}
return filename.substring(lastDotIndex + 1);
}
}

View File

@ -14,6 +14,7 @@ import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.leocoder.thin.common.constants.CoderConstants;
import org.leocoder.thin.common.exception.coder.YUtil;
import org.leocoder.thin.domain.enums.menu.MenuTypeEnum;
import org.leocoder.thin.common.satoken.CoderLoginUtil;
import org.leocoder.thin.domain.enums.oper.OperType;
import org.leocoder.thin.domain.model.bo.element.CascaderLongBo;
@ -103,14 +104,14 @@ public class SysMenuController {
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysMenu::getPath, sysMenu.getPath());
long count = sysMenuService.count(wrapper);
YUtil.isTrue(count > 1L, "请联系管理员检查路由path重复多次");
if (count == 1L) {
YUtil.isTrue(count > CoderConstants.ONE_LONG, "请联系管理员检查路由path重复多次");
if (count == CoderConstants.ONE_LONG) {
SysMenu menu = sysMenuService.getOne(wrapper);
YUtil.isTrue(!Objects.equals(menu.getMenuId(), sysMenu.getMenuId()), "该路由path已存在");
}
}
// 如果是按钮类型并且非外链必须得隐藏
if (sysMenu.getMenuType().equals("3") && StringUtils.isBlank(sysMenu.getIsLink())) {
if (MenuTypeEnum.isButton(sysMenu.getMenuType()) && StringUtils.isBlank(sysMenu.getIsLink())) {
sysMenu.setIsHide(CoderConstants.ZERO_STRING);
}
if (StringUtils.isNotBlank(CoderLoginUtil.getUserName())) {
@ -134,8 +135,8 @@ public class SysMenuController {
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysMenu::getPath, sysMenu.getPath());
long count = sysMenuService.count(wrapper);
YUtil.isTrue(count > 1L, "请联系管理员检查路由path重复多次");
if (count == 1L) {
YUtil.isTrue(count > CoderConstants.ONE_LONG, "请联系管理员检查路由path重复多次");
if (count == CoderConstants.ONE_LONG) {
SysMenu menu = sysMenuService.getOne(wrapper);
YUtil.isTrue(!Objects.equals(menu.getMenuId(), sysMenu.getMenuId()), "该路由path已存在");
}
@ -161,7 +162,7 @@ public class SysMenuController {
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysMenu::getParentId, menu.getMenuId());
long count = sysMenuService.count(wrapper);
YUtil.isTrue(count > 0L, "请先删除该节点下的子节点");
YUtil.isTrue(count > CoderConstants.ZERO_LONG, "请先删除该节点下的子节点");
YUtil.isTrue(!sysMenuService.removeById(id), "删除失败,请稍后重试");
}

View File

@ -20,6 +20,8 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
import org.yaml.snakeyaml.constructor.DuplicateKeyException;
import java.io.FileNotFoundException;
@ -305,4 +307,56 @@ public class GlobalExceptionHandler {
return ErrorHandler.error(500, ex.getMessage(), request.getRequestURL().toString());
}
/**
* @description [文件上传大小超限异常]
* @author Leocoder
*/
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ErrorHandler handleMaxUploadSizeExceededException(HttpServletRequest request, MaxUploadSizeExceededException ex) {
log.error("文件上传大小超限异常:{},请求地址:{}", ex.getMessage(), request.getRequestURL().toString());
// 提取文件大小限制信息提供更友好的错误提示
String message = "文件大小超出限制请上传小于2MB的文件";
// 尝试从异常信息中提取具体的大小限制
String exceptionMsg = ex.getMessage();
if (exceptionMsg != null && exceptionMsg.contains("maximum permitted size")) {
if (exceptionMsg.contains("1048576")) {
message = "文件大小超出限制最大允许上传1MB的文件";
} else if (exceptionMsg.contains("2097152")) {
message = "文件大小超出限制最大允许上传2MB的文件";
} else if (exceptionMsg.contains("5242880")) {
message = "文件大小超出限制最大允许上传5MB的文件";
} else if (exceptionMsg.contains("10485760")) {
message = "文件大小超出限制最大允许上传10MB的文件";
}
}
return ErrorHandler.error(413, message, request.getRequestURL().toString());
}
/**
* @description [文件上传异常]
* @author Leocoder
*/
@ExceptionHandler(MultipartException.class)
public ErrorHandler handleMultipartException(HttpServletRequest request, MultipartException ex) {
log.error("文件上传异常:{},请求地址:{}", ex.getMessage(), request.getRequestURL().toString());
String message = "文件上传失败";
String exceptionMsg = ex.getMessage();
if (exceptionMsg != null) {
if (exceptionMsg.contains("size")) {
message = "文件大小超出限制,请选择较小的文件";
} else if (exceptionMsg.contains("format") || exceptionMsg.contains("type")) {
message = "文件格式不支持,请选择正确的文件格式";
} else if (exceptionMsg.contains("empty")) {
message = "请选择要上传的文件";
}
}
return ErrorHandler.error(400, message, request.getRequestURL().toString());
}
}