From 77d2ad543bb3335a37ee4bc8abe87288943ec860 Mon Sep 17 00:00:00 2001 From: Leo <98382335+gaoziman@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:55:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD=E5=92=8C=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增文件上传预检查功能,支持文件格式和大小验证 - 完善全局异常处理器,增加文件上传异常处理 - 优化菜单管理,添加菜单类型枚举,提升代码可读性 - 完善常量类,新增数据计数阈值常量 - 统一文件上传逻辑,支持图片和文档分类管理 --- .../thin/common/constants/CoderConstants.java | 15 +++ .../domain/enums/menu/MenuStatusEnum.java | 68 ++++++++++ .../thin/domain/enums/menu/MenuTypeEnum.java | 83 ++++++++++++ .../controller/file/FileController.java | 126 +++++++++++++++++- .../controller/menu/SysMenuController.java | 13 +- .../handler/GlobalExceptionHandler.java | 54 ++++++++ 6 files changed, 348 insertions(+), 11 deletions(-) create mode 100644 coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/menu/MenuStatusEnum.java create mode 100644 coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/menu/MenuTypeEnum.java diff --git a/coder-common-thin-common/src/main/java/org/leocoder/thin/common/constants/CoderConstants.java b/coder-common-thin-common/src/main/java/org/leocoder/thin/common/constants/CoderConstants.java index d484ad0..05b4108 100755 --- a/coder-common-thin-common/src/main/java/org/leocoder/thin/common/constants/CoderConstants.java +++ b/coder-common-thin-common/src/main/java/org/leocoder/thin/common/constants/CoderConstants.java @@ -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; + } diff --git a/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/menu/MenuStatusEnum.java b/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/menu/MenuStatusEnum.java new file mode 100644 index 0000000..22db94c --- /dev/null +++ b/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/menu/MenuStatusEnum.java @@ -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); + } +} \ No newline at end of file diff --git a/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/menu/MenuTypeEnum.java b/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/menu/MenuTypeEnum.java new file mode 100644 index 0000000..18e14a9 --- /dev/null +++ b/coder-common-thin-model/src/main/java/org/leocoder/thin/domain/enums/menu/MenuTypeEnum.java @@ -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); + } +} \ No newline at end of file diff --git a/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/file/FileController.java b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/file/FileController.java index 6820b21..4c47acb 100755 --- a/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/file/FileController.java +++ b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/file/FileController.java @@ -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 ALLOWED_IMAGE_EXTENSIONS = Arrays.asList( + "jpg", "jpeg", "png", "gif", "bmp", "webp", "svg" + ); + + // 允许的文档文件扩展名 + private static final List 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 uploadSingleFile(@RequestParam("file") MultipartFile file, @PathVariable("fileSize") Integer fileSize, @PathVariable("folderName") String folderName, @PathVariable("fileParam") String fileParam) { + + // 文件预检查 + validateUploadFile(file, fileSize, folderName); + Map 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 uploadAnyFile(@RequestParam("file") MultipartFile file, @PathVariable("fileSize") Integer fileSize, @PathVariable("folderName") String folderName, @PathVariable("fileParam") String fileParam) { + + // 文件预检查 + validateUploadFile(file, fileSize, folderName); + Map 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 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); + } + } diff --git a/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/menu/SysMenuController.java b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/menu/SysMenuController.java index c2832bc..cf44f34 100755 --- a/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/menu/SysMenuController.java +++ b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/menu/SysMenuController.java @@ -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 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 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 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), "删除失败,请稍后重试"); } diff --git a/coder-common-thin-plugins/coder-common-thin-resultex/src/main/java/org/leocoder/thin/resultex/handler/GlobalExceptionHandler.java b/coder-common-thin-plugins/coder-common-thin-resultex/src/main/java/org/leocoder/thin/resultex/handler/GlobalExceptionHandler.java index 08ed79c..44e0c56 100755 --- a/coder-common-thin-plugins/coder-common-thin-resultex/src/main/java/org/leocoder/thin/resultex/handler/GlobalExceptionHandler.java +++ b/coder-common-thin-plugins/coder-common-thin-resultex/src/main/java/org/leocoder/thin/resultex/handler/GlobalExceptionHandler.java @@ -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()); + } + }