serv-文件管理

This commit is contained in:
2025-10-16 10:31:13 +08:00
parent 0dd65e9eda
commit aace132108
16 changed files with 1671 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
-- --------------------------------------------------------
-- 文件上传记录表
-- --------------------------------------------------------
CREATE TABLE IF NOT EXISTS `tb_sys_file` (
`id` VARCHAR(50) NOT NULL COMMENT '主键ID',
`file_id` VARCHAR(64) NOT NULL COMMENT '文件ID',
`file_name` VARCHAR(255) NOT NULL COMMENT '存储文件名UUID生成',
`original_name` VARCHAR(255) NOT NULL COMMENT '原始文件名',
`file_path` VARCHAR(500) NOT NULL COMMENT '文件存储路径',
`file_url` VARCHAR(500) DEFAULT NULL COMMENT '文件访问URL',
`file_size` BIGINT NOT NULL COMMENT '文件大小(字节)',
`file_type` VARCHAR(50) DEFAULT NULL COMMENT '文件类型image、document、video等',
`mime_type` VARCHAR(100) DEFAULT NULL COMMENT 'MIME类型image/jpeg',
`storage_type` VARCHAR(20) NOT NULL DEFAULT 'local' COMMENT '存储类型local-本地存储、minio-MinIO存储、oss-阿里云OSS等',
`module` VARCHAR(50) DEFAULT NULL COMMENT '所属模块user、news、course等',
`business_id` VARCHAR(64) DEFAULT NULL COMMENT '业务ID关联的业务数据ID',
`uploader` VARCHAR(64) DEFAULT NULL COMMENT '上传者用户ID',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` DATETIME DEFAULT NULL COMMENT '删除时间',
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除0-否1-是)',
PRIMARY KEY (`id`),
INDEX `idx_file_id` (`file_id`),
INDEX `idx_file_name` (`file_name`),
INDEX `idx_uploader` (`uploader`),
INDEX `idx_module_business` (`module`, `business_id`),
INDEX `idx_storage_type` (`storage_type`),
INDEX `idx_deleted` (`deleted`),
INDEX `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文件上传记录表';

View File

@@ -0,0 +1,24 @@
<?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>api</artifactId>
<version>${school-news.version}</version>
</parent>
<groupId>org.xyzh</groupId>
<artifactId>api-file</artifactId>
<version>${school-news.version}</version>
<packaging>jar</packaging>
<name>api-file</name>
<description>文件模块API</description>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
</project>

View File

@@ -0,0 +1,124 @@
package org.xyzh.api.file;
import org.springframework.web.multipart.MultipartFile;
import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.dto.system.TbSysFile;
/**
* @description 文件服务接口
* @filename FileService.java
* @author system
* @copyright xyzh
* @since 2025-10-16
*/
public interface FileService {
/**
* @description 上传文件
* @param file 文件对象
* @param module 所属模块
* @param businessId 业务ID
* @return ResultDomain<TbSysFile> 上传结果,包含文件信息
* @author system
* @since 2025-10-16
*/
ResultDomain<TbSysFile> uploadFile(MultipartFile file, String module, String businessId);
/**
* @description 上传文件(带上传者信息)
* @param file 文件对象
* @param module 所属模块
* @param businessId 业务ID
* @param uploader 上传者用户ID
* @return ResultDomain<TbSysFile> 上传结果,包含文件信息
* @author system
* @since 2025-10-16
*/
ResultDomain<TbSysFile> uploadFile(MultipartFile file, String module, String businessId, String uploader);
/**
* @description 下载文件
* @param fileId 文件ID
* @return ResultDomain<byte[]> 文件字节数组
* @author system
* @since 2025-10-16
*/
ResultDomain<byte[]> downloadFile(String fileId);
/**
* @description 删除文件(逻辑删除)
* @param fileId 文件ID
* @return ResultDomain<TbSysFile> 删除结果
* @author system
* @since 2025-10-16
*/
ResultDomain<TbSysFile> deleteFile(String fileId);
/**
* @description 物理删除文件(同时删除存储和数据库记录)
* @param fileId 文件ID
* @return ResultDomain<TbSysFile> 删除结果
* @author system
* @since 2025-10-16
*/
ResultDomain<TbSysFile> deleteFilePhysically(String fileId);
/**
* @description 根据文件ID查询文件信息
* @param fileId 文件ID
* @return ResultDomain<TbSysFile> 文件信息
* @author system
* @since 2025-10-16
*/
ResultDomain<TbSysFile> getFileById(String fileId);
/**
* @description 根据业务ID查询文件列表
* @param module 所属模块
* @param businessId 业务ID
* @return ResultDomain<TbSysFile> 文件列表
* @author system
* @since 2025-10-16
*/
ResultDomain<TbSysFile> getFilesByBusinessId(String module, String businessId);
/**
* @description 根据上传者查询文件列表
* @param uploader 上传者用户ID
* @return ResultDomain<TbSysFile> 文件列表
* @author system
* @since 2025-10-16
*/
ResultDomain<TbSysFile> getFilesByUploader(String uploader);
/**
* @description 获取文件访问URL
* @param fileId 文件ID
* @return ResultDomain<String> 文件访问URL
* @author system
* @since 2025-10-16
*/
ResultDomain<String> getFileUrl(String fileId);
/**
* @description 批量上传文件
* @param files 文件对象列表
* @param module 所属模块
* @param businessId 业务ID
* @param uploader 上传者用户ID可选
* @return ResultDomain<TbSysFile> 上传结果,包含文件信息列表
* @author system
* @since 2025-10-16
*/
ResultDomain<TbSysFile> batchUploadFiles(MultipartFile[] files, String module, String businessId, String uploader);
/**
* @description 批量删除文件(逻辑删除)
* @param fileIds 文件ID列表
* @return ResultDomain<TbSysFile> 删除结果
* @author system
* @since 2025-10-16
*/
ResultDomain<TbSysFile> batchDeleteFiles(String[] fileIds);
}

View File

@@ -21,6 +21,7 @@
<module>api-ai</module>
<module>api-study</module>
<module>api-news</module>
<module>api-file</module>
</modules>
<properties>

View 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>

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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";
}
}

View File

@@ -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";
}
}

View 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

View 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>

View 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>