diff --git a/coder-common-thin-modules/coder-common-thin-system/pom.xml b/coder-common-thin-modules/coder-common-thin-system/pom.xml index cff360b..e3bd83a 100644 --- a/coder-common-thin-modules/coder-common-thin-system/pom.xml +++ b/coder-common-thin-modules/coder-common-thin-system/pom.xml @@ -45,6 +45,12 @@ org.springdoc springdoc-openapi-starter-webmvc-ui + + + org.leocoder.thin + coder-common-thin-oss + ${revision} + \ 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 4c47acb..95ba829 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 @@ -3,22 +3,24 @@ package org.leocoder.thin.system.controller.file; import cn.dev33.satoken.annotation.SaIgnore; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -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; import org.leocoder.thin.common.utils.ip.IpUtil; import org.leocoder.thin.common.utils.ip.ServletUtil; import org.leocoder.thin.domain.pojo.system.SysFile; import org.leocoder.thin.domain.pojo.system.SysPicture; +import org.leocoder.thin.oss.service.StorageService; +import org.leocoder.thin.oss.service.StorageServiceFactory; +import org.leocoder.thin.oss.utils.OssUtil; import org.leocoder.thin.system.service.file.SysFileService; import org.leocoder.thin.system.service.picture.SysPictureService; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; +import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -40,6 +42,9 @@ public class FileController { @Value("${coder.filePath}") private String basePath; + + @Value("${coder.storage.type:local}") + private String storageType; // 允许的图片文件扩展名 private static final List ALLOWED_IMAGE_EXTENSIONS = Arrays.asList( @@ -62,10 +67,9 @@ public class FileController { private static final long MAX_FILE_SIZE_10MB = 10485760L; private final Environment env; - private final SysFileService sysFileService; - private final SysPictureService sysPictureService; + private final StorageServiceFactory storageServiceFactory; /** * @param fileSize 文件大小 @@ -80,23 +84,61 @@ public class FileController { // 文件预检查 validateUploadFile(file, fileSize, folderName); - Map fileMap = UploadUtil.coderSingleFile(file, basePath + "/" + folderName + "/"+ CoderLoginUtil.getLoginName() + "/", fileSize); + Map fileMap; + + try { + // 根据配置选择存储服务 + StorageService storageService = storageServiceFactory.getStorageService(storageType); + + // 生成唯一文件名 + String fileName = OssUtil.generateUniqueFileName(file.getOriginalFilename()); + + // 构建文件夹路径 + String folderPath = OssUtil.buildFolderPath(folderName, CoderLoginUtil.getLoginName()); + + // 上传文件 + fileMap = storageService.uploadFile(file, fileName, folderPath); + + log.info("文件上传成功: storageType={}, fileName={}, filePath={}", + storageType, fileName, fileMap.get("filePath")); + + } catch (Exception e) { + log.error("文件上传失败,尝试使用本地存储降级", e); + + // 降级到本地存储 + try { + StorageService localService = storageServiceFactory.getStorageService("local"); + String fileName = OssUtil.generateUniqueFileName(file.getOriginalFilename()); + String folderPath = OssUtil.buildFolderPath(folderName, CoderLoginUtil.getLoginName()); + fileMap = localService.uploadFile(file, fileName, folderPath); + + log.warn("使用本地存储降级成功: fileName={}", fileName); + + } catch (Exception ex) { + log.error("本地存储降级也失败", ex); + throw new BusinessException(500, "文件上传失败: " + ex.getMessage()); + } + } + + // 获取实际使用的存储服务类型 + String actualStorageType = determineActualStorageType(fileMap); // 统一保存到文件表,便于文件管理页面统一显示 - saveUploadFilesInformation(fileMap, CoderConstants.TRUE); + saveUploadFilesInformation(fileMap, actualStorageType, CoderConstants.TRUE); // 如果是图片,同时保存到图库表(保持图库管理功能) if (CoderConstants.PICTURES.equals(folderName)) { - saveUploadPicturesInformation(fileMap, fileParam, CoderConstants.TRUE); + saveUploadPicturesInformation(fileMap, fileParam, actualStorageType, CoderConstants.TRUE); } + return fileMap; } /** - * @description [保存上传文件信息] + * @description [保存上传图片信息] * @author Leocoder */ - private void saveUploadPicturesInformation(Map fileMap, String fileParam, boolean isCreateBy) { + private void saveUploadPicturesInformation(Map fileMap, String fileParam, String storageServiceType, boolean isCreateBy) { log.info("图库上传 ->"); // 新增文件信息 SysPicture sysPicture = new SysPicture(); @@ -105,16 +147,26 @@ public class FileController { sysPicture.setPictureSize(fileMap.get("fileSize").toString()); sysPicture.setPictureSuffix(fileMap.get("suffixName").toString()); sysPicture.setPictureUpload(fileMap.get("filePath").toString()); - sysPicture.setPictureService(CoderConstants.ONE_STRING); - String protocol = IpUtil.getProtocol(ServletUtil.getRequest()); - if (StringUtils.isBlank(protocol)) { - protocol = "http"; + sysPicture.setPictureService(storageServiceType); + + // 设置文件访问路径 + String fileUploadPath = (String) fileMap.get("fileUploadPath"); + if (isFullUrl(fileUploadPath)) { + // 如果已经是完整URL(如OSS),直接使用 + sysPicture.setPicturePath(fileUploadPath); + } else { + // 如果是相对路径(如本地存储),构建完整URL + String protocol = IpUtil.getProtocol(ServletUtil.getRequest()); + if (StringUtils.isBlank(protocol)) { + protocol = "http"; + } + String hostIp = IpUtil.getHostIp(ServletUtil.getRequest()); + String hostPort = StringUtils.isNotBlank(env.getProperty("server.port")) ? env.getProperty("server.port") : "18099"; + sysPicture.setPicturePath(protocol + "://" + hostIp + ":" + hostPort + fileUploadPath); } - String hostIp = IpUtil.getHostIp(ServletUtil.getRequest()); - 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()); + if (CoderConstants.MINUS_ONE_STRING.equals(fileParam)) { sysPicture.setPictureType("9"); } else { @@ -130,25 +182,35 @@ public class FileController { * @description [保存上传文件信息] * @author Leocoder */ - private void saveUploadFilesInformation(Map fileMap, boolean isCreateBy) { + private void saveUploadFilesInformation(Map fileMap, String storageServiceType, boolean isCreateBy) { log.info("文件上传 ->"); - // 新增图库信息 + // 新增文件信息 SysFile sysFile = new SysFile(); sysFile.setFileName(fileMap.get("fileName").toString()); sysFile.setNewName(fileMap.get("newName").toString()); sysFile.setFileSize(fileMap.get("fileSize").toString()); sysFile.setFileSuffix(fileMap.get("suffixName").toString()); sysFile.setFileUpload(fileMap.get("filePath").toString()); - sysFile.setFileService(CoderConstants.ONE_STRING); - String protocol = IpUtil.getProtocol(ServletUtil.getRequest()); - if (StringUtils.isBlank(protocol)) { - protocol = "http"; + sysFile.setFileService(storageServiceType); + + // 设置文件访问路径 + String fileUploadPath = (String) fileMap.get("fileUploadPath"); + if (isFullUrl(fileUploadPath)) { + // 如果已经是完整URL(如OSS),直接使用 + sysFile.setFilePath(fileUploadPath); + } else { + // 如果是相对路径(如本地存储),构建完整URL + String protocol = IpUtil.getProtocol(ServletUtil.getRequest()); + if (StringUtils.isBlank(protocol)) { + protocol = "http"; + } + String hostIp = IpUtil.getHostIp(ServletUtil.getRequest()); + String hostPort = StringUtils.isNotBlank(env.getProperty("server.port")) ? env.getProperty("server.port") : "18099"; + sysFile.setFilePath(protocol + "://" + hostIp + ":" + hostPort + fileUploadPath); } - String hostIp = IpUtil.getHostIp(ServletUtil.getRequest()); - String hostPort = StringUtils.isNotBlank(env.getProperty("server.port")) ? env.getProperty("server.port") : "18088"; - log.info("IP地址:{},端口号:{}", hostIp, env.getProperty("server.port")); - sysFile.setFilePath(protocol + "://" + hostIp + ":" + hostPort + fileMap.get("fileUploadPath").toString()); + log.info("文件回显地址:{}", sysFile.getFilePath()); + String fileType = FileTypeUtil.checkFileExtension(fileMap.get("suffixName").toString()); sysFile.setFileType(fileType); if (isCreateBy) { @@ -156,6 +218,33 @@ public class FileController { } sysFileService.save(sysFile); } + + /** + * 判断字符串是否为完整URL + */ + private boolean isFullUrl(String url) { + return StringUtils.isNotBlank(url) && (url.startsWith("http://") || url.startsWith("https://")); + } + + /** + * 确定实际使用的存储服务类型 + */ + private String determineActualStorageType(Map fileMap) { + String fileUploadPath = (String) fileMap.get("fileUploadPath"); + + // 根据返回的文件路径判断实际使用的存储类型 + if (isFullUrl(fileUploadPath)) { + // 如果是完整URL,检查是否包含OSS域名 + if (fileUploadPath.contains(".aliyuncs.com") || fileUploadPath.contains("oss-")) { + return "3"; // OSS + } else if (fileUploadPath.contains("minio") || fileUploadPath.contains(":9000")) { + return "2"; // MinIO + } + } + + // 默认为本地存储 + return "1"; // Local + } /** * @param fileSize 文件大小 @@ -171,15 +260,53 @@ public class FileController { // 文件预检查 validateUploadFile(file, fileSize, folderName); - Map fileMap = UploadUtil.coderSingleFile(file, basePath + "/" + folderName + "/", fileSize); + Map fileMap; + + try { + // 根据配置选择存储服务 + StorageService storageService = storageServiceFactory.getStorageService(storageType); + + // 生成唯一文件名 + String fileName = OssUtil.generateUniqueFileName(file.getOriginalFilename()); + + // 构建文件夹路径(匿名上传不包含用户名) + String folderPath = folderName; + + // 上传文件 + fileMap = storageService.uploadFile(file, fileName, folderPath); + + log.info("匿名文件上传成功: storageType={}, fileName={}, filePath={}", + storageType, fileName, fileMap.get("filePath")); + + } catch (Exception e) { + log.error("匿名文件上传失败,尝试使用本地存储降级", e); + + // 降级到本地存储 + try { + StorageService localService = storageServiceFactory.getStorageService("local"); + String fileName = OssUtil.generateUniqueFileName(file.getOriginalFilename()); + String folderPath = folderName; + fileMap = localService.uploadFile(file, fileName, folderPath); + + log.warn("匿名上传使用本地存储降级成功: fileName={}", fileName); + + } catch (Exception ex) { + log.error("本地存储降级也失败", ex); + throw new BusinessException(500, "文件上传失败: " + ex.getMessage()); + } + } + + // 获取实际使用的存储服务类型 + String actualStorageType = determineActualStorageType(fileMap); // 统一保存到文件表 - saveUploadFilesInformation(fileMap, false); + saveUploadFilesInformation(fileMap, actualStorageType, false); // 如果是图片,同时保存到图库表 if (CoderConstants.PICTURES.equals(folderName)) { - saveUploadPicturesInformation(fileMap, fileParam, false); + saveUploadPicturesInformation(fileMap, fileParam, actualStorageType, false); } + return fileMap; } diff --git a/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/file/SysFileController.java b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/file/SysFileController.java index 6e64a6c..8cb49e6 100755 --- a/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/file/SysFileController.java +++ b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/file/SysFileController.java @@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.leocoder.thin.common.constants.CoderConstants; import org.leocoder.thin.common.exception.coder.YUtil; @@ -17,6 +18,8 @@ import org.leocoder.thin.domain.enums.oper.OperType; import org.leocoder.thin.domain.model.vo.system.SysFileVo; import org.leocoder.thin.domain.pojo.system.SysFile; import org.leocoder.thin.operlog.annotation.OperLog; +import org.leocoder.thin.oss.service.StorageService; +import org.leocoder.thin.oss.service.StorageServiceFactory; import org.leocoder.thin.system.service.file.SysFileService; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; @@ -33,9 +36,12 @@ import java.util.List; @RequestMapping("/coder") @RequiredArgsConstructor @RestController +@Slf4j public class SysFileController { + private final SysFileService sysFileService; + private final StorageServiceFactory storageServiceFactory; /** * @description [分页查询] @@ -124,8 +130,8 @@ public class SysFileController { SysFile sysFile = sysFileService.getById(id); if (!ObjectUtil.isEmpty(sysFile)) { if (StringUtils.isNotBlank(sysFile.getFileUpload())) { - // 删除文件 - FileUtil.deleteFile(sysFile.getFileUpload()); + // 根据存储服务类型删除文件 + deletePhysicalFile(sysFile.getFileUpload(), sysFile.getFileService()); } } YUtil.isTrue(!sysFileService.removeById(id), "删除失败,请稍后重试"); @@ -147,12 +153,58 @@ public class SysFileController { if (!sysFileList.isEmpty()) { for (SysFile sysFile : sysFileList) { if (StringUtils.isNotBlank(sysFile.getFileUpload())) { - // 删除文件 - FileUtil.deleteFile(sysFile.getFileUpload()); + // 根据存储服务类型删除文件 + deletePhysicalFile(sysFile.getFileUpload(), sysFile.getFileService()); } } } YUtil.isTrue(!sysFileService.removeBatchByIds(ids), "删除失败,请稍后重试"); } + + /** + * 根据存储服务类型删除物理文件 + */ + private void deletePhysicalFile(String filePath, String fileService) { + try { + boolean deleteResult = false; + + switch (fileService) { + case "1": // 本地存储 + deleteResult = FileUtil.deleteFile(filePath); + break; + case "2": // MinIO存储 + try { + StorageService minioService = storageServiceFactory.getStorageService("minio"); + deleteResult = minioService.deleteFile(filePath); + } catch (Exception e) { + log.error("MinIO存储服务不可用,尝试本地删除", e); + deleteResult = FileUtil.deleteFile(filePath); + } + break; + case "3": // OSS存储 + try { + StorageService ossService = storageServiceFactory.getStorageService("oss"); + deleteResult = ossService.deleteFile(filePath); + } catch (Exception e) { + log.error("OSS存储服务不可用,尝试本地删除", e); + deleteResult = FileUtil.deleteFile(filePath); + } + break; + default: + log.warn("未知的存储服务类型: {},使用本地删除", fileService); + deleteResult = FileUtil.deleteFile(filePath); + break; + } + + if (deleteResult) { + log.info("文件删除成功: filePath={}, fileService={}", filePath, fileService); + } else { + log.warn("文件删除失败: filePath={}, fileService={}", filePath, fileService); + } + + } catch (Exception e) { + log.error("删除物理文件时发生异常: filePath={}, fileService={}", filePath, fileService, e); + } + } } diff --git a/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/picture/SysPictureController.java b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/picture/SysPictureController.java index d777977..a74b9f7 100755 --- a/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/picture/SysPictureController.java +++ b/coder-common-thin-modules/coder-common-thin-system/src/main/java/org/leocoder/thin/system/controller/picture/SysPictureController.java @@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.leocoder.thin.common.constants.CoderConstants; import org.leocoder.thin.common.exception.coder.YUtil; @@ -15,6 +16,8 @@ import org.leocoder.thin.domain.enums.oper.OperType; import org.leocoder.thin.domain.model.vo.system.SysPictureVo; import org.leocoder.thin.domain.pojo.system.SysPicture; import org.leocoder.thin.operlog.annotation.OperLog; +import org.leocoder.thin.oss.service.StorageService; +import org.leocoder.thin.oss.service.StorageServiceFactory; import org.leocoder.thin.system.service.picture.SysPictureService; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; @@ -31,9 +34,11 @@ import java.util.List; @RequestMapping("/coder") @RequiredArgsConstructor @RestController +@Slf4j public class SysPictureController { private final SysPictureService sysPictureService; + private final StorageServiceFactory storageServiceFactory; /** * @description [分页查询] @@ -117,6 +122,13 @@ public class SysPictureController { @PostMapping("/sysPicture/deleteById/{id}") @OperLog(value = "删除图库", operType = OperType.DELETE) public void delete(@PathVariable("id") Long id) { + SysPicture sysPicture = sysPictureService.getById(id); + if (sysPicture != null) { + if (StringUtils.isNotBlank(sysPicture.getPictureUpload())) { + // 根据存储服务类型删除图片文件 + deletePhysicalFile(sysPicture.getPictureUpload(), sysPicture.getPictureService()); + } + } YUtil.isTrue(!sysPictureService.removeById(id), "删除失败,请稍后重试"); } @@ -130,7 +142,63 @@ public class SysPictureController { @PostMapping("/sysPicture/batchDelete") @OperLog(value = "批量删除图库", operType = OperType.DELETE) public void batchDelete(@RequestBody List ids) { + List sysPictureList = sysPictureService.listByIds(ids); + + // 批量删除物理文件 + for (SysPicture sysPicture : sysPictureList) { + if (StringUtils.isNotBlank(sysPicture.getPictureUpload())) { + deletePhysicalFile(sysPicture.getPictureUpload(), sysPicture.getPictureService()); + } + } + YUtil.isTrue(!sysPictureService.removeBatchByIds(ids), "删除失败,请稍后重试"); } + + /** + * 根据存储服务类型删除物理文件 + */ + private void deletePhysicalFile(String filePath, String fileService) { + try { + boolean deleteResult = false; + + switch (fileService) { + case "1": // 本地存储 + // 对于本地存储,需要使用完整路径 + deleteResult = new java.io.File(filePath).delete(); + break; + case "2": // MinIO存储 + try { + StorageService minioService = storageServiceFactory.getStorageService("minio"); + deleteResult = minioService.deleteFile(filePath); + } catch (Exception e) { + log.error("MinIO存储服务不可用,跳过删除", e); + deleteResult = false; + } + break; + case "3": // OSS存储 + try { + StorageService ossService = storageServiceFactory.getStorageService("oss"); + deleteResult = ossService.deleteFile(filePath); + } catch (Exception e) { + log.error("OSS存储服务不可用,跳过删除", e); + deleteResult = false; + } + break; + default: + log.warn("未知的存储服务类型: {},跳过文件删除", fileService); + deleteResult = false; + break; + } + + if (deleteResult) { + log.info("图片文件删除成功: filePath={}, fileService={}", filePath, fileService); + } else { + log.warn("图片文件删除失败: filePath={}, fileService={}", filePath, fileService); + } + + } catch (Exception e) { + log.error("删除物理图片文件时发生异常: filePath={}, fileService={}", filePath, fileService, e); + } + } }