serv-文件管理
This commit is contained in:
81
schoolNewsServ/file/pom.xml
Normal file
81
schoolNewsServ/file/pom.xml
Normal file
@@ -0,0 +1,81 @@
|
||||
<?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>
|
||||
<parent>
|
||||
<groupId>org.xyzh</groupId>
|
||||
<artifactId>school-news</artifactId>
|
||||
<version>${school-news.version}</version>
|
||||
</parent>
|
||||
|
||||
<groupId>org.xyzh</groupId>
|
||||
<artifactId>file</artifactId>
|
||||
<version>${school-news.version}</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>file</name>
|
||||
<description>文件模块</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<minio.version>8.5.7</minio.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- API模块 -->
|
||||
<dependency>
|
||||
<groupId>org.xyzh</groupId>
|
||||
<artifactId>api-file</artifactId>
|
||||
<version>${school-news.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 通用模块 -->
|
||||
<dependency>
|
||||
<groupId>org.xyzh</groupId>
|
||||
<artifactId>common-all</artifactId>
|
||||
<version>${school-news.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- MinIO -->
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
<version>${minio.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Configuration Processor -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.xyzh.file.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @description 文件存储配置类
|
||||
* @filename FileStorageConfig.java
|
||||
* @author system
|
||||
* @copyright xyzh
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "file.storage")
|
||||
public class FileStorageConfig {
|
||||
|
||||
/**
|
||||
* @description 默认存储类型
|
||||
*/
|
||||
private String defaultType = "local";
|
||||
|
||||
/**
|
||||
* @description 存储配置列表
|
||||
*/
|
||||
private List<StorageProperties> storages = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* @description 单个存储配置
|
||||
*/
|
||||
@Data
|
||||
public static class StorageProperties {
|
||||
/**
|
||||
* @description 存储类型(local、minio)
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* @description 是否启用
|
||||
*/
|
||||
private Boolean enabled = true;
|
||||
|
||||
/**
|
||||
* @description 本地存储基础路径
|
||||
*/
|
||||
private String basePath;
|
||||
|
||||
/**
|
||||
* @description 本地存储URL前缀
|
||||
*/
|
||||
private String urlPrefix;
|
||||
|
||||
/**
|
||||
* @description MinIO端点
|
||||
*/
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* @description MinIO访问密钥
|
||||
*/
|
||||
private String accessKey;
|
||||
|
||||
/**
|
||||
* @description MinIO密钥
|
||||
*/
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* @description MinIO桶名称
|
||||
*/
|
||||
private String bucketName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
package org.xyzh.file.controller;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.xyzh.api.file.FileService;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.dto.system.TbSysFile;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* @description 文件控制器
|
||||
* @filename FileController.java
|
||||
* @author system
|
||||
* @copyright xyzh
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/file")
|
||||
public class FileController {
|
||||
|
||||
@Autowired
|
||||
private FileService fileService;
|
||||
|
||||
/**
|
||||
* @description 上传文件
|
||||
* @param file 文件对象
|
||||
* @param module 所属模块
|
||||
* @param businessId 业务ID
|
||||
* @param uploader 上传者用户ID(可选)
|
||||
* @return ResultDomain<TbSysFile> 上传结果
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@PostMapping("/upload")
|
||||
public ResultDomain<TbSysFile> uploadFile(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "module", required = false, defaultValue = "common") String module,
|
||||
@RequestParam(value = "businessId", required = false) String businessId,
|
||||
@RequestParam(value = "uploader", required = false) String uploader) {
|
||||
|
||||
log.info("上传文件请求: module={}, businessId={}, uploader={}, fileName={}",
|
||||
module, businessId, uploader, file.getOriginalFilename());
|
||||
|
||||
return fileService.uploadFile(file, module, businessId, uploader);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 下载文件
|
||||
* @param fileId 文件ID
|
||||
* @return ResponseEntity<byte[]> 文件字节数组
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@GetMapping("/download/{fileId}")
|
||||
public ResponseEntity<byte[]> downloadFile(@PathVariable String fileId) {
|
||||
log.info("下载文件请求: fileId={}", fileId);
|
||||
|
||||
ResultDomain<TbSysFile> fileResult = fileService.getFileById(fileId);
|
||||
if (!fileResult.isSuccess() || fileResult.getData() == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
ResultDomain<byte[]> dataResult = fileService.downloadFile(fileId);
|
||||
if (!dataResult.isSuccess() || dataResult.getData() == null) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
|
||||
TbSysFile sysFile = (TbSysFile) fileResult.getData();
|
||||
byte[] fileData = (byte[]) dataResult.getData();
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.parseMediaType(
|
||||
sysFile.getMimeType() != null ? sysFile.getMimeType() : MediaType.APPLICATION_OCTET_STREAM_VALUE));
|
||||
headers.setContentDispositionFormData("attachment",
|
||||
URLEncoder.encode(sysFile.getOriginalName(), StandardCharsets.UTF_8));
|
||||
headers.setContentLength(fileData.length);
|
||||
|
||||
return new ResponseEntity<>(fileData, headers, HttpStatus.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 删除文件(逻辑删除)
|
||||
* @param fileId 文件ID
|
||||
* @return ResultDomain<TbSysFile> 删除结果
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@DeleteMapping("/{fileId}")
|
||||
public ResultDomain<TbSysFile> deleteFile(@PathVariable String fileId) {
|
||||
log.info("删除文件请求: fileId={}", fileId);
|
||||
return fileService.deleteFile(fileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 物理删除文件
|
||||
* @param fileId 文件ID
|
||||
* @return ResultDomain<TbSysFile> 删除结果
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@DeleteMapping("/physical/{fileId}")
|
||||
public ResultDomain<TbSysFile> deleteFilePhysically(@PathVariable String fileId) {
|
||||
log.info("物理删除文件请求: fileId={}", fileId);
|
||||
return fileService.deleteFilePhysically(fileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 根据文件ID查询文件信息
|
||||
* @param fileId 文件ID
|
||||
* @return ResultDomain<TbSysFile> 文件信息
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@GetMapping("/{fileId}")
|
||||
public ResultDomain<TbSysFile> getFileById(@PathVariable String fileId) {
|
||||
return fileService.getFileById(fileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 根据业务ID查询文件列表
|
||||
* @param module 所属模块
|
||||
* @param businessId 业务ID
|
||||
* @return ResultDomain<TbSysFile> 文件列表
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@GetMapping("/business/{module}/{businessId}")
|
||||
public ResultDomain<TbSysFile> getFilesByBusinessId(
|
||||
@PathVariable String module,
|
||||
@PathVariable String businessId) {
|
||||
return fileService.getFilesByBusinessId(module, businessId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 根据上传者查询文件列表
|
||||
* @param uploader 上传者用户ID
|
||||
* @return ResultDomain<TbSysFile> 文件列表
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@GetMapping("/uploader/{uploader}")
|
||||
public ResultDomain<TbSysFile> getFilesByUploader(@PathVariable String uploader) {
|
||||
return fileService.getFilesByUploader(uploader);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取文件访问URL
|
||||
* @param fileId 文件ID
|
||||
* @return ResultDomain<String> 文件访问URL
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@GetMapping("/url/{fileId}")
|
||||
public ResultDomain<String> getFileUrl(@PathVariable String fileId) {
|
||||
return fileService.getFileUrl(fileId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 批量上传文件
|
||||
* @param files 文件对象数组
|
||||
* @param module 所属模块
|
||||
* @param businessId 业务ID
|
||||
* @param uploader 上传者用户ID(可选)
|
||||
* @return ResultDomain<TbSysFile> 上传结果
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@PostMapping("/batch-upload")
|
||||
public ResultDomain<TbSysFile> batchUploadFiles(
|
||||
@RequestParam("files") MultipartFile[] files,
|
||||
@RequestParam(value = "module", required = false, defaultValue = "common") String module,
|
||||
@RequestParam(value = "businessId", required = false) String businessId,
|
||||
@RequestParam(value = "uploader", required = false) String uploader) {
|
||||
|
||||
log.info("批量上传文件请求: count={}, module={}, businessId={}, uploader={}",
|
||||
files != null ? files.length : 0, module, businessId, uploader);
|
||||
|
||||
return fileService.batchUploadFiles(files, module, businessId, uploader);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 批量删除文件
|
||||
* @param fileIds 文件ID数组
|
||||
* @return ResultDomain<TbSysFile> 删除结果
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@DeleteMapping("/batch")
|
||||
public ResultDomain<TbSysFile> batchDeleteFiles(@RequestBody String[] fileIds) {
|
||||
log.info("批量删除文件请求: count={}", fileIds != null ? fileIds.length : 0);
|
||||
return fileService.batchDeleteFiles(fileIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.xyzh.file.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.xyzh.common.dto.system.TbSysFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @description 文件Mapper接口
|
||||
* @filename FileMapper.java
|
||||
* @author system
|
||||
* @copyright xyzh
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@Mapper
|
||||
public interface FileMapper extends BaseMapper<TbSysFile>{
|
||||
|
||||
/**
|
||||
* @description 插入文件记录
|
||||
* @param file 文件对象
|
||||
* @return int 影响行数
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
int insertFile(@Param("file") TbSysFile file);
|
||||
|
||||
/**
|
||||
* @description 根据文件ID查询文件信息
|
||||
* @param fileId 文件ID
|
||||
* @return TbSysFile 文件信息
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
TbSysFile selectFileById(@Param("fileId") String fileId);
|
||||
|
||||
/**
|
||||
* @description 根据文件ID查询文件信息(包括已删除)
|
||||
* @param fileId 文件ID
|
||||
* @return TbSysFile 文件信息
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
TbSysFile selectFileByIdIncludeDeleted(@Param("fileId") String fileId);
|
||||
|
||||
/**
|
||||
* @description 根据业务ID查询文件列表
|
||||
* @param module 所属模块
|
||||
* @param businessId 业务ID
|
||||
* @return List<TbSysFile> 文件列表
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
List<TbSysFile> selectFilesByBusinessId(@Param("module") String module, @Param("businessId") String businessId);
|
||||
|
||||
/**
|
||||
* @description 根据上传者查询文件列表
|
||||
* @param uploader 上传者用户ID
|
||||
* @return List<TbSysFile> 文件列表
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
List<TbSysFile> selectFilesByUploader(@Param("uploader") String uploader);
|
||||
|
||||
/**
|
||||
* @description 逻辑删除文件
|
||||
* @param fileId 文件ID
|
||||
* @return int 影响行数
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
int logicDeleteFileById(@Param("fileId") String fileId);
|
||||
|
||||
/**
|
||||
* @description 物理删除文件
|
||||
* @param fileId 文件ID
|
||||
* @return int 影响行数
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
int deleteFileById(@Param("fileId") String fileId);
|
||||
|
||||
/**
|
||||
* @description 批量逻辑删除文件
|
||||
* @param fileIds 文件ID列表
|
||||
* @return int 影响行数
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
int batchLogicDeleteFiles(@Param("fileIds") List<String> fileIds);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
package org.xyzh.file.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.xyzh.api.file.FileService;
|
||||
import org.xyzh.common.core.domain.ResultDomain;
|
||||
import org.xyzh.common.dto.system.TbSysFile;
|
||||
import org.xyzh.common.utils.IDUtils;
|
||||
import org.xyzh.file.mapper.FileMapper;
|
||||
import org.xyzh.file.strategy.FileStorageStrategy;
|
||||
import org.xyzh.file.strategy.FileStorageStrategyFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @description 文件服务实现
|
||||
* @filename FileServiceImpl.java
|
||||
* @author system
|
||||
* @copyright xyzh
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class FileServiceImpl implements FileService {
|
||||
|
||||
@Autowired
|
||||
private FileMapper fileMapper;
|
||||
|
||||
@Autowired
|
||||
private FileStorageStrategyFactory strategyFactory;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ResultDomain<TbSysFile> uploadFile(MultipartFile file, String module, String businessId) {
|
||||
return uploadFile(file, module, businessId, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ResultDomain<TbSysFile> uploadFile(MultipartFile file, String module, String businessId, String uploader) {
|
||||
ResultDomain<TbSysFile> resultDomain = new ResultDomain<>();
|
||||
|
||||
try {
|
||||
if (file == null || file.isEmpty()) {
|
||||
resultDomain.fail("文件不能为空");
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
// 获取存储策略
|
||||
FileStorageStrategy strategy = strategyFactory.getDefaultStrategy();
|
||||
|
||||
// 生成文件名
|
||||
String originalFileName = file.getOriginalFilename();
|
||||
String fileExtension = "";
|
||||
if (originalFileName != null && originalFileName.contains(".")) {
|
||||
fileExtension = originalFileName.substring(originalFileName.lastIndexOf("."));
|
||||
}
|
||||
String fileName = UUID.randomUUID().toString().replace("-", "") + fileExtension;
|
||||
|
||||
// 上传文件
|
||||
String filePath = strategy.upload(file, fileName, module);
|
||||
|
||||
// 保存文件记录
|
||||
TbSysFile sysFile = new TbSysFile();
|
||||
String fileId = IDUtils.generateID();
|
||||
sysFile.setID(fileId);
|
||||
sysFile.setFileID(fileId);
|
||||
sysFile.setFileName(fileName);
|
||||
sysFile.setOriginalName(originalFileName);
|
||||
sysFile.setFilePath(filePath);
|
||||
sysFile.setFileUrl(strategy.getFileUrl(filePath));
|
||||
sysFile.setFileSize(file.getSize());
|
||||
sysFile.setMimeType(file.getContentType());
|
||||
sysFile.setStorageType(strategy.getStorageType());
|
||||
sysFile.setModule(module);
|
||||
sysFile.setBusinessID(businessId);
|
||||
sysFile.setUploader(uploader);
|
||||
sysFile.setCreateTime(new Date());
|
||||
sysFile.setUpdateTime(new Date());
|
||||
sysFile.setDeleted(false);
|
||||
|
||||
// 判断文件类型
|
||||
String mimeType = file.getContentType();
|
||||
if (mimeType != null) {
|
||||
if (mimeType.startsWith("image/")) {
|
||||
sysFile.setFileType("image");
|
||||
} else if (mimeType.startsWith("video/")) {
|
||||
sysFile.setFileType("video");
|
||||
} else if (mimeType.startsWith("audio/")) {
|
||||
sysFile.setFileType("audio");
|
||||
} else if (mimeType.contains("pdf")) {
|
||||
sysFile.setFileType("pdf");
|
||||
} else if (mimeType.contains("word") || mimeType.contains("document")) {
|
||||
sysFile.setFileType("document");
|
||||
} else if (mimeType.contains("excel") || mimeType.contains("spreadsheet")) {
|
||||
sysFile.setFileType("excel");
|
||||
} else {
|
||||
sysFile.setFileType("other");
|
||||
}
|
||||
}
|
||||
|
||||
fileMapper.insertFile(sysFile);
|
||||
|
||||
log.info("文件上传成功: fileId={}, fileName={}, storageType={}",
|
||||
sysFile.getID(), fileName, strategy.getStorageType());
|
||||
|
||||
resultDomain.success("文件上传成功", sysFile);
|
||||
return resultDomain;
|
||||
} catch (Exception e) {
|
||||
log.error("文件上传失败", e);
|
||||
resultDomain.fail("文件上传失败: " + e.getMessage());
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<byte[]> downloadFile(String fileId) {
|
||||
ResultDomain<byte[]> resultDomain = new ResultDomain<>();
|
||||
|
||||
try {
|
||||
TbSysFile sysFile = fileMapper.selectFileById(fileId);
|
||||
if (sysFile == null || sysFile.getDeleted()) {
|
||||
resultDomain.fail("文件不存在或已被删除");
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
// 获取存储策略
|
||||
FileStorageStrategy strategy = strategyFactory.getStrategy(sysFile.getStorageType());
|
||||
|
||||
// 下载文件
|
||||
byte[] fileData = strategy.download(sysFile.getFilePath());
|
||||
|
||||
log.info("文件下载成功: fileId={}, fileName={}", fileId, sysFile.getFileName());
|
||||
|
||||
resultDomain.success("文件下载成功", fileData);
|
||||
return resultDomain;
|
||||
} catch (Exception e) {
|
||||
log.error("文件下载失败: fileId={}", fileId, e);
|
||||
resultDomain.fail("文件下载失败: " + e.getMessage());
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ResultDomain<TbSysFile> deleteFile(String fileId) {
|
||||
ResultDomain<TbSysFile> resultDomain = new ResultDomain<>();
|
||||
|
||||
try {
|
||||
TbSysFile sysFile = fileMapper.selectFileById(fileId);
|
||||
if (sysFile == null) {
|
||||
resultDomain.fail("文件不存在");
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
if (sysFile.getDeleted()) {
|
||||
resultDomain.fail("文件已被删除");
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
// 逻辑删除
|
||||
int result = fileMapper.logicDeleteFileById(fileId);
|
||||
if (result > 0) {
|
||||
log.info("文件逻辑删除成功: fileId={}", fileId);
|
||||
resultDomain.success("文件删除成功", sysFile);
|
||||
return resultDomain;
|
||||
} else {
|
||||
resultDomain.fail("文件删除失败");
|
||||
return resultDomain;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("文件删除失败: fileId={}", fileId, e);
|
||||
resultDomain.fail("文件删除失败: " + e.getMessage());
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ResultDomain<TbSysFile> deleteFilePhysically(String fileId) {
|
||||
ResultDomain<TbSysFile> resultDomain = new ResultDomain<>();
|
||||
|
||||
try {
|
||||
TbSysFile sysFile = fileMapper.selectFileByIdIncludeDeleted(fileId);
|
||||
if (sysFile == null) {
|
||||
resultDomain.fail("文件不存在");
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
// 获取存储策略
|
||||
FileStorageStrategy strategy = strategyFactory.getStrategy(sysFile.getStorageType());
|
||||
|
||||
// 删除物理文件
|
||||
boolean deleted = strategy.delete(sysFile.getFilePath());
|
||||
if (!deleted) {
|
||||
log.warn("物理文件删除失败,可能文件不存在: filePath={}", sysFile.getFilePath());
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
fileMapper.deleteFileById(fileId);
|
||||
|
||||
log.info("文件物理删除成功: fileId={}", fileId);
|
||||
|
||||
resultDomain.success("文件物理删除成功", sysFile);
|
||||
return resultDomain;
|
||||
} catch (Exception e) {
|
||||
log.error("文件物理删除失败: fileId={}", fileId, e);
|
||||
resultDomain.fail("文件物理删除失败: " + e.getMessage());
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbSysFile> getFileById(String fileId) {
|
||||
ResultDomain<TbSysFile> resultDomain = new ResultDomain<>();
|
||||
|
||||
try {
|
||||
TbSysFile sysFile = fileMapper.selectFileById(fileId);
|
||||
if (sysFile == null || sysFile.getDeleted()) {
|
||||
resultDomain.fail("文件不存在或已被删除");
|
||||
return resultDomain;
|
||||
}
|
||||
resultDomain.success("查询成功", sysFile);
|
||||
return resultDomain;
|
||||
} catch (Exception e) {
|
||||
log.error("查询文件失败: fileId={}", fileId, e);
|
||||
resultDomain.fail("查询文件失败: " + e.getMessage());
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbSysFile> getFilesByBusinessId(String module, String businessId) {
|
||||
ResultDomain<TbSysFile> resultDomain = new ResultDomain<>();
|
||||
|
||||
try {
|
||||
List<TbSysFile> files = fileMapper.selectFilesByBusinessId(module, businessId);
|
||||
resultDomain.success("查询成功", files);
|
||||
return resultDomain;
|
||||
} catch (Exception e) {
|
||||
log.error("查询业务文件列表失败: module={}, businessId={}", module, businessId, e);
|
||||
resultDomain.fail("查询文件列表失败: " + e.getMessage());
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<TbSysFile> getFilesByUploader(String uploader) {
|
||||
ResultDomain<TbSysFile> resultDomain = new ResultDomain<>();
|
||||
|
||||
try {
|
||||
List<TbSysFile> files = fileMapper.selectFilesByUploader(uploader);
|
||||
resultDomain.success("查询成功", files);
|
||||
return resultDomain;
|
||||
} catch (Exception e) {
|
||||
log.error("查询用户文件列表失败: uploader={}", uploader, e);
|
||||
resultDomain.fail("查询文件列表失败: " + e.getMessage());
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<String> getFileUrl(String fileId) {
|
||||
ResultDomain<String> resultDomain = new ResultDomain<>();
|
||||
|
||||
try {
|
||||
TbSysFile sysFile = fileMapper.selectFileById(fileId);
|
||||
if (sysFile == null || sysFile.getDeleted()) {
|
||||
resultDomain.fail("文件不存在或已被删除");
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
// 如果数据库中已有URL且不是MinIO类型(MinIO的URL有时效性),直接返回
|
||||
if (sysFile.getFileUrl() != null && !"minio".equals(sysFile.getStorageType())) {
|
||||
resultDomain.success("获取成功", sysFile.getFileUrl());
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
// 重新生成URL
|
||||
FileStorageStrategy strategy = strategyFactory.getStrategy(sysFile.getStorageType());
|
||||
String url = strategy.getFileUrl(sysFile.getFilePath());
|
||||
|
||||
resultDomain.success("获取成功", url);
|
||||
return resultDomain;
|
||||
} catch (Exception e) {
|
||||
log.error("获取文件URL失败: fileId={}", fileId, e);
|
||||
resultDomain.fail("获取文件URL失败: " + e.getMessage());
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ResultDomain<TbSysFile> batchUploadFiles(MultipartFile[] files, String module, String businessId, String uploader) {
|
||||
ResultDomain<TbSysFile> resultDomain = new ResultDomain<>();
|
||||
|
||||
try {
|
||||
if (files == null || files.length == 0) {
|
||||
resultDomain.fail("文件列表不能为空");
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
List<TbSysFile> uploadedFiles = new ArrayList<>();
|
||||
List<String> errorMessages = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
MultipartFile file = files[i];
|
||||
try {
|
||||
ResultDomain<TbSysFile> uploadResult = uploadFile(file, module, businessId, uploader);
|
||||
if (uploadResult.isSuccess() && uploadResult.getData() != null) {
|
||||
uploadedFiles.add((TbSysFile) uploadResult.getData());
|
||||
} else {
|
||||
errorMessages.add(String.format("文件%d上传失败: %s", i + 1, uploadResult.getMessage()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("批量上传文件时出错: index={}, fileName={}", i, file.getOriginalFilename(), e);
|
||||
errorMessages.add(String.format("文件%d(%s)上传失败: %s", i + 1, file.getOriginalFilename(), e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadedFiles.isEmpty()) {
|
||||
resultDomain.fail("所有文件上传失败: " + String.join("; ", errorMessages));
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
if (!errorMessages.isEmpty()) {
|
||||
log.warn("批量上传部分失败: 成功={}, 失败={}", uploadedFiles.size(), errorMessages.size());
|
||||
resultDomain.success(String.format("批量上传完成: 成功%d个,失败%d个。失败信息: %s",
|
||||
uploadedFiles.size(), errorMessages.size(), String.join("; ", errorMessages)), uploadedFiles);
|
||||
} else {
|
||||
log.info("批量上传全部成功: count={}", uploadedFiles.size());
|
||||
resultDomain.success(String.format("批量上传成功: 共%d个文件", uploadedFiles.size()), uploadedFiles);
|
||||
}
|
||||
|
||||
return resultDomain;
|
||||
} catch (Exception e) {
|
||||
log.error("批量上传文件失败", e);
|
||||
resultDomain.fail("批量上传文件失败: " + e.getMessage());
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ResultDomain<TbSysFile> batchDeleteFiles(String[] fileIds) {
|
||||
ResultDomain<TbSysFile> resultDomain = new ResultDomain<>();
|
||||
|
||||
try {
|
||||
if (fileIds == null || fileIds.length == 0) {
|
||||
resultDomain.fail("文件ID列表不能为空");
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
List<String> fileIdList = Arrays.asList(fileIds);
|
||||
int result = fileMapper.batchLogicDeleteFiles(fileIdList);
|
||||
|
||||
log.info("批量逻辑删除文件成功: count={}", result);
|
||||
|
||||
resultDomain.success("批量删除成功", new ArrayList<TbSysFile>());
|
||||
return resultDomain;
|
||||
} catch (Exception e) {
|
||||
log.error("批量删除文件失败", e);
|
||||
resultDomain.fail("批量删除文件失败: " + e.getMessage());
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.xyzh.file.strategy;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @description 文件存储策略接口
|
||||
* @filename FileStorageStrategy.java
|
||||
* @author system
|
||||
* @copyright xyzh
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
public interface FileStorageStrategy {
|
||||
|
||||
/**
|
||||
* @description 上传文件
|
||||
* @param file 文件对象
|
||||
* @param fileName 文件名
|
||||
* @param module 所属模块
|
||||
* @return String 文件存储路径
|
||||
* @throws IOException IO异常
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
String upload(MultipartFile file, String fileName, String module) throws IOException;
|
||||
|
||||
/**
|
||||
* @description 下载文件
|
||||
* @param filePath 文件路径
|
||||
* @return byte[] 文件字节数组
|
||||
* @throws IOException IO异常
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
byte[] download(String filePath) throws IOException;
|
||||
|
||||
/**
|
||||
* @description 删除文件
|
||||
* @param filePath 文件路径
|
||||
* @return boolean 删除是否成功
|
||||
* @throws IOException IO异常
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
boolean delete(String filePath) throws IOException;
|
||||
|
||||
/**
|
||||
* @description 获取文件访问URL
|
||||
* @param filePath 文件路径
|
||||
* @return String 文件访问URL
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
String getFileUrl(String filePath);
|
||||
|
||||
/**
|
||||
* @description 获取存储类型
|
||||
* @return String 存储类型
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
String getStorageType();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package org.xyzh.file.strategy;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.xyzh.file.config.FileStorageConfig;
|
||||
import org.xyzh.file.strategy.impl.LocalFileStorageStrategy;
|
||||
import org.xyzh.file.strategy.impl.MinIOFileStorageStrategy;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* @description 文件存储策略工厂
|
||||
* @filename FileStorageStrategyFactory.java
|
||||
* @author system
|
||||
* @copyright xyzh
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class FileStorageStrategyFactory {
|
||||
|
||||
private final String defaultStorageType;
|
||||
private final Map<String, FileStorageStrategy> strategyMap = new ConcurrentHashMap<>();
|
||||
|
||||
public FileStorageStrategyFactory(FileStorageConfig config) {
|
||||
this.defaultStorageType = config.getDefaultType();
|
||||
|
||||
// 根据配置动态创建存储策略
|
||||
for (FileStorageConfig.StorageProperties storage : config.getStorages()) {
|
||||
if (storage.getEnabled() == null || !storage.getEnabled()) {
|
||||
log.info("跳过未启用的存储策略: {}", storage.getType());
|
||||
continue;
|
||||
}
|
||||
|
||||
FileStorageStrategy strategy = createStrategy(storage);
|
||||
if (strategy != null) {
|
||||
strategyMap.put(storage.getType(), strategy);
|
||||
log.info("注册存储策略: {}", storage.getType());
|
||||
}
|
||||
}
|
||||
|
||||
if (strategyMap.isEmpty()) {
|
||||
log.warn("没有配置任何存储策略,将使用默认本地存储");
|
||||
strategyMap.put("local", new LocalFileStorageStrategy("./uploads", "http://localhost:8080/files"));
|
||||
}
|
||||
|
||||
log.info("文件存储策略工厂初始化完成,默认存储类型: {}, 已注册策略: {}",
|
||||
defaultStorageType, strategyMap.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 根据配置创建存储策略
|
||||
* @param storage 存储配置
|
||||
* @return FileStorageStrategy 存储策略
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
private FileStorageStrategy createStrategy(FileStorageConfig.StorageProperties storage) {
|
||||
try {
|
||||
return switch (storage.getType().toLowerCase()) {
|
||||
case "local" -> new LocalFileStorageStrategy(
|
||||
storage.getBasePath(),
|
||||
storage.getUrlPrefix()
|
||||
);
|
||||
case "minio" -> new MinIOFileStorageStrategy(
|
||||
storage.getEndpoint(),
|
||||
storage.getAccessKey(),
|
||||
storage.getSecretKey(),
|
||||
storage.getBucketName()
|
||||
);
|
||||
default -> {
|
||||
log.warn("未知的存储类型: {}", storage.getType());
|
||||
yield null;
|
||||
}
|
||||
};
|
||||
} catch (Exception e) {
|
||||
log.error("创建存储策略失败: type={}", storage.getType(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取默认存储策略
|
||||
* @return FileStorageStrategy 存储策略
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
public FileStorageStrategy getDefaultStrategy() {
|
||||
return getStrategy(defaultStorageType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 根据类型获取存储策略
|
||||
* @param storageType 存储类型
|
||||
* @return FileStorageStrategy 存储策略
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
public FileStorageStrategy getStrategy(String storageType) {
|
||||
FileStorageStrategy strategy = strategyMap.get(storageType);
|
||||
if (strategy == null) {
|
||||
log.warn("未找到存储类型 {} 的策略,使用默认策略: {}", storageType, defaultStorageType);
|
||||
strategy = strategyMap.get(defaultStorageType);
|
||||
}
|
||||
return strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 注册新的存储策略
|
||||
* @param storageType 存储类型
|
||||
* @param strategy 存储策略
|
||||
* @author system
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
public void registerStrategy(String storageType, FileStorageStrategy strategy) {
|
||||
strategyMap.put(storageType, strategy);
|
||||
log.info("注册新的存储策略: {}", storageType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.xyzh.file.strategy.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.xyzh.file.strategy.FileStorageStrategy;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* @description 本地文件存储策略实现
|
||||
* @filename LocalFileStorageStrategy.java
|
||||
* @author system
|
||||
* @copyright xyzh
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@Slf4j
|
||||
public class LocalFileStorageStrategy implements FileStorageStrategy {
|
||||
|
||||
private final String basePath;
|
||||
private final String urlPrefix;
|
||||
|
||||
public LocalFileStorageStrategy(String basePath, String urlPrefix) {
|
||||
this.basePath = basePath;
|
||||
this.urlPrefix = urlPrefix;
|
||||
log.info("初始化本地存储策略: basePath={}, urlPrefix={}", basePath, urlPrefix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(MultipartFile file, String fileName, String module) throws IOException {
|
||||
// 创建模块目录
|
||||
String modulePath = basePath + File.separator + module;
|
||||
File moduleDir = new File(modulePath);
|
||||
if (!moduleDir.exists()) {
|
||||
moduleDir.mkdirs();
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
String filePath = modulePath + File.separator + fileName;
|
||||
Path path = Paths.get(filePath);
|
||||
Files.write(path, file.getBytes());
|
||||
|
||||
log.info("文件上传成功,本地路径: {}", filePath);
|
||||
return module + "/" + fileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] download(String filePath) throws IOException {
|
||||
Path path = Paths.get(basePath + File.separator + filePath);
|
||||
if (!Files.exists(path)) {
|
||||
throw new IOException("文件不存在: " + filePath);
|
||||
}
|
||||
return Files.readAllBytes(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(String filePath) throws IOException {
|
||||
Path path = Paths.get(basePath + File.separator + filePath);
|
||||
if (Files.exists(path)) {
|
||||
Files.delete(path);
|
||||
log.info("文件删除成功: {}", filePath);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFileUrl(String filePath) {
|
||||
return urlPrefix + "/" + filePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStorageType() {
|
||||
return "local";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package org.xyzh.file.strategy.impl;
|
||||
|
||||
import io.minio.*;
|
||||
import io.minio.http.Method;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.xyzh.file.strategy.FileStorageStrategy;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @description MinIO文件存储策略实现
|
||||
* @filename MinIOFileStorageStrategy.java
|
||||
* @author system
|
||||
* @copyright xyzh
|
||||
* @since 2025-10-16
|
||||
*/
|
||||
@Slf4j
|
||||
public class MinIOFileStorageStrategy implements FileStorageStrategy {
|
||||
|
||||
private final String bucketName;
|
||||
private final MinioClient minioClient;
|
||||
|
||||
public MinIOFileStorageStrategy(String endpoint, String accessKey, String secretKey, String bucketName) {
|
||||
this.bucketName = bucketName;
|
||||
|
||||
// 初始化MinIO客户端
|
||||
this.minioClient = MinioClient.builder()
|
||||
.endpoint(endpoint)
|
||||
.credentials(accessKey, secretKey)
|
||||
.build();
|
||||
|
||||
// 检查并创建bucket
|
||||
try {
|
||||
boolean bucketExists = minioClient.bucketExists(
|
||||
BucketExistsArgs.builder().bucket(bucketName).build()
|
||||
);
|
||||
if (!bucketExists) {
|
||||
minioClient.makeBucket(
|
||||
MakeBucketArgs.builder().bucket(bucketName).build()
|
||||
);
|
||||
log.info("创建MinIO Bucket成功: {}", bucketName);
|
||||
}
|
||||
log.info("初始化MinIO存储策略: endpoint={}, bucket={}", endpoint, bucketName);
|
||||
} catch (Exception e) {
|
||||
log.error("初始化MinIO失败", e);
|
||||
throw new RuntimeException("初始化MinIO失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(MultipartFile file, String fileName, String module) throws IOException {
|
||||
try {
|
||||
String objectName = module + "/" + fileName;
|
||||
|
||||
minioClient.putObject(
|
||||
PutObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.object(objectName)
|
||||
.stream(file.getInputStream(), file.getSize(), -1)
|
||||
.contentType(file.getContentType())
|
||||
.build()
|
||||
);
|
||||
|
||||
log.info("文件上传MinIO成功: {}", objectName);
|
||||
return objectName;
|
||||
} catch (Exception e) {
|
||||
log.error("MinIO文件上传失败", e);
|
||||
throw new IOException("MinIO文件上传失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] download(String filePath) throws IOException {
|
||||
try {
|
||||
InputStream stream = minioClient.getObject(
|
||||
GetObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.object(filePath)
|
||||
.build()
|
||||
);
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = stream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
return outputStream.toByteArray();
|
||||
} catch (Exception e) {
|
||||
log.error("MinIO文件下载失败", e);
|
||||
throw new IOException("MinIO文件下载失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete(String filePath) throws IOException {
|
||||
try {
|
||||
minioClient.removeObject(
|
||||
RemoveObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.object(filePath)
|
||||
.build()
|
||||
);
|
||||
|
||||
log.info("MinIO文件删除成功: {}", filePath);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("MinIO文件删除失败", e);
|
||||
throw new IOException("MinIO文件删除失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFileUrl(String filePath) {
|
||||
try {
|
||||
// 生成7天有效期的预签名URL
|
||||
return minioClient.getPresignedObjectUrl(
|
||||
GetPresignedObjectUrlArgs.builder()
|
||||
.method(Method.GET)
|
||||
.bucket(bucketName)
|
||||
.object(filePath)
|
||||
.expiry(7, TimeUnit.DAYS)
|
||||
.build()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("生成MinIO文件URL失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStorageType() {
|
||||
return "minio";
|
||||
}
|
||||
}
|
||||
|
||||
62
schoolNewsServ/file/src/main/resources/application.yaml
Normal file
62
schoolNewsServ/file/src/main/resources/application.yaml
Normal file
@@ -0,0 +1,62 @@
|
||||
spring:
|
||||
application:
|
||||
name: file-service
|
||||
|
||||
servlet:
|
||||
multipart:
|
||||
enabled: true
|
||||
max-file-size: 100MB
|
||||
max-request-size: 100MB
|
||||
|
||||
datasource:
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://localhost:3306/school_news?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: root
|
||||
hikari:
|
||||
minimum-idle: 5
|
||||
maximum-pool-size: 20
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
connection-timeout: 30000
|
||||
|
||||
# MyBatis Plus配置
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath:mapper/*.xml
|
||||
type-aliases-package: org.xyzh.common.dto
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
|
||||
global-config:
|
||||
db-config:
|
||||
id-type: assign_uuid
|
||||
logic-delete-field: deleted
|
||||
logic-delete-value: 1
|
||||
logic-not-delete-value: 0
|
||||
|
||||
# 文件存储配置
|
||||
file:
|
||||
storage:
|
||||
# 默认存储类型
|
||||
default-type: local
|
||||
|
||||
# 存储配置列表(只创建配置了的存储策略)
|
||||
storages:
|
||||
# 本地存储配置
|
||||
- type: local
|
||||
enabled: true
|
||||
base-path: ./uploads
|
||||
url-prefix: http://localhost:8080/files
|
||||
|
||||
# MinIO存储配置(如不需要可以删除或设置enabled为false)
|
||||
# - type: minio
|
||||
# enabled: true
|
||||
# endpoint: http://localhost:9000
|
||||
# access-key: minioadmin
|
||||
# secret-key: minioadmin
|
||||
# bucket-name: school-news
|
||||
|
||||
# 服务端口
|
||||
server:
|
||||
port: 8086
|
||||
|
||||
81
schoolNewsServ/file/src/main/resources/log4j2-spring.xml
Normal file
81
schoolNewsServ/file/src/main/resources/log4j2-spring.xml
Normal file
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="WARN" monitorInterval="30">
|
||||
<Properties>
|
||||
<Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Property>
|
||||
<Property name="LOG_PATH">./file/logs</Property>
|
||||
<Property name="APP_NAME">school-news-file</Property>
|
||||
</Properties>
|
||||
|
||||
<Appenders>
|
||||
<!-- 控制台输出 -->
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="${LOG_PATTERN}"/>
|
||||
</Console>
|
||||
|
||||
<!-- 文件输出 - INFO级别 -->
|
||||
<RollingFile name="InfoFile" fileName="${LOG_PATH}/${APP_NAME}-info.log"
|
||||
filePattern="${LOG_PATH}/${APP_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz">
|
||||
<PatternLayout pattern="${LOG_PATTERN}"/>
|
||||
<Policies>
|
||||
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
|
||||
<SizeBasedTriggeringPolicy size="100MB"/>
|
||||
</Policies>
|
||||
<DefaultRolloverStrategy max="30"/>
|
||||
<Filters>
|
||||
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL"/>
|
||||
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
|
||||
</Filters>
|
||||
</RollingFile>
|
||||
|
||||
<!-- 文件输出 - WARN级别 -->
|
||||
<RollingFile name="WarnFile" fileName="${LOG_PATH}/${APP_NAME}-warn.log"
|
||||
filePattern="${LOG_PATH}/${APP_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz">
|
||||
<PatternLayout pattern="${LOG_PATTERN}"/>
|
||||
<Policies>
|
||||
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
|
||||
<SizeBasedTriggeringPolicy size="100MB"/>
|
||||
</Policies>
|
||||
<DefaultRolloverStrategy max="30"/>
|
||||
<Filters>
|
||||
<ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="NEUTRAL"/>
|
||||
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
|
||||
</Filters>
|
||||
</RollingFile>
|
||||
|
||||
<!-- 文件输出 - ERROR级别 -->
|
||||
<RollingFile name="ErrorFile" fileName="${LOG_PATH}/${APP_NAME}-error.log"
|
||||
filePattern="${LOG_PATH}/${APP_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz">
|
||||
<PatternLayout pattern="${LOG_PATTERN}"/>
|
||||
<Policies>
|
||||
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
|
||||
<SizeBasedTriggeringPolicy size="100MB"/>
|
||||
</Policies>
|
||||
<DefaultRolloverStrategy max="30"/>
|
||||
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
|
||||
</RollingFile>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<!-- 应用日志 -->
|
||||
<Logger name="org.xyzh" level="INFO" additivity="false">
|
||||
<AppenderRef ref="Console"/>
|
||||
<AppenderRef ref="InfoFile"/>
|
||||
<AppenderRef ref="WarnFile"/>
|
||||
<AppenderRef ref="ErrorFile"/>
|
||||
</Logger>
|
||||
|
||||
<!-- MyBatis日志 -->
|
||||
<Logger name="org.xyzh.file.mapper" level="DEBUG" additivity="false">
|
||||
<AppenderRef ref="Console"/>
|
||||
<AppenderRef ref="InfoFile"/>
|
||||
</Logger>
|
||||
|
||||
<!-- 根日志 -->
|
||||
<Root level="INFO">
|
||||
<AppenderRef ref="Console"/>
|
||||
<AppenderRef ref="InfoFile"/>
|
||||
<AppenderRef ref="WarnFile"/>
|
||||
<AppenderRef ref="ErrorFile"/>
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
111
schoolNewsServ/file/src/main/resources/mapper/FileMapper.xml
Normal file
111
schoolNewsServ/file/src/main/resources/mapper/FileMapper.xml
Normal file
@@ -0,0 +1,111 @@
|
||||
<?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="org.xyzh.file.mapper.FileMapper">
|
||||
|
||||
<!-- 结果映射 -->
|
||||
<resultMap id="BaseResultMap" type="org.xyzh.common.dto.system.TbSysFile">
|
||||
<id column="id" property="ID" />
|
||||
<result column="file_id" property="fileID" />
|
||||
<result column="file_name" property="fileName" />
|
||||
<result column="original_name" property="originalName" />
|
||||
<result column="file_path" property="filePath" />
|
||||
<result column="file_url" property="fileUrl" />
|
||||
<result column="file_size" property="fileSize" />
|
||||
<result column="file_type" property="fileType" />
|
||||
<result column="mime_type" property="mimeType" />
|
||||
<result column="storage_type" property="storageType" />
|
||||
<result column="module" property="module" />
|
||||
<result column="business_id" property="businessID" />
|
||||
<result column="uploader" property="uploader" />
|
||||
<result column="create_time" property="createTime" />
|
||||
<result column="update_time" property="updateTime" />
|
||||
<result column="delete_time" property="deleteTime" />
|
||||
<result column="deleted" property="deleted" />
|
||||
</resultMap>
|
||||
|
||||
<!-- 基础列 -->
|
||||
<sql id="Base_Column_List">
|
||||
id, file_id, file_name, original_name, file_path, file_url, file_size,
|
||||
file_type, mime_type, storage_type, module, business_id, uploader,
|
||||
create_time, update_time, delete_time, deleted
|
||||
</sql>
|
||||
|
||||
<!-- 插入文件记录 -->
|
||||
<insert id="insertFile">
|
||||
INSERT INTO tb_sys_file (
|
||||
id, file_id, file_name, original_name, file_path, file_url, file_size,
|
||||
file_type, mime_type, storage_type, module, business_id, uploader,
|
||||
create_time, update_time, deleted
|
||||
) VALUES (
|
||||
#{file.ID}, #{file.fileID}, #{file.fileName}, #{file.originalName}, #{file.filePath}, #{file.fileUrl}, #{file.fileSize},
|
||||
#{file.fileType}, #{file.mimeType}, #{file.storageType}, #{file.module}, #{file.businessID}, #{file.uploader},
|
||||
#{file.createTime}, #{file.updateTime}, #{file.deleted}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<!-- 根据文件ID查询文件信息 -->
|
||||
<select id="selectFileById" resultMap="BaseResultMap">
|
||||
SELECT
|
||||
<include refid="Base_Column_List" />
|
||||
FROM tb_sys_file
|
||||
WHERE id = #{fileId}
|
||||
AND deleted = 0
|
||||
</select>
|
||||
|
||||
<!-- 根据文件ID查询文件信息(包括已删除) -->
|
||||
<select id="selectFileByIdIncludeDeleted" resultMap="BaseResultMap">
|
||||
SELECT
|
||||
<include refid="Base_Column_List" />
|
||||
FROM tb_sys_file
|
||||
WHERE id = #{fileId}
|
||||
</select>
|
||||
|
||||
<!-- 根据业务ID查询文件列表 -->
|
||||
<select id="selectFilesByBusinessId" resultMap="BaseResultMap">
|
||||
SELECT
|
||||
<include refid="Base_Column_List" />
|
||||
FROM tb_sys_file
|
||||
WHERE module = #{module}
|
||||
AND business_id = #{businessId}
|
||||
AND deleted = 0
|
||||
ORDER BY create_time DESC
|
||||
</select>
|
||||
|
||||
<!-- 根据上传者查询文件列表 -->
|
||||
<select id="selectFilesByUploader" resultMap="BaseResultMap">
|
||||
SELECT
|
||||
<include refid="Base_Column_List" />
|
||||
FROM tb_sys_file
|
||||
WHERE uploader = #{uploader}
|
||||
AND deleted = 0
|
||||
ORDER BY create_time DESC
|
||||
</select>
|
||||
|
||||
<!-- 逻辑删除文件 -->
|
||||
<update id="logicDeleteFileById">
|
||||
UPDATE tb_sys_file
|
||||
SET deleted = 1,
|
||||
delete_time = NOW()
|
||||
WHERE id = #{fileId}
|
||||
AND deleted = 0
|
||||
</update>
|
||||
|
||||
<!-- 物理删除文件 -->
|
||||
<delete id="deleteFileById">
|
||||
DELETE FROM tb_sys_file
|
||||
WHERE id = #{fileId}
|
||||
</delete>
|
||||
|
||||
<!-- 批量逻辑删除文件 -->
|
||||
<update id="batchLogicDeleteFiles">
|
||||
UPDATE tb_sys_file
|
||||
SET deleted = 1,
|
||||
delete_time = NOW()
|
||||
WHERE id IN
|
||||
<foreach collection="fileIds" item="fileId" open="(" separator="," close=")">
|
||||
#{fileId}
|
||||
</foreach>
|
||||
AND deleted = 0
|
||||
</update>
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user