diff --git a/schoolNewsServ/.bin/mysql/sql/createTableFile.sql b/schoolNewsServ/.bin/mysql/sql/createTableFile.sql
new file mode 100644
index 0000000..a076c87
--- /dev/null
+++ b/schoolNewsServ/.bin/mysql/sql/createTableFile.sql
@@ -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='文件上传记录表';
+
diff --git a/schoolNewsServ/api/api-file/pom.xml b/schoolNewsServ/api/api-file/pom.xml
new file mode 100644
index 0000000..cd324d2
--- /dev/null
+++ b/schoolNewsServ/api/api-file/pom.xml
@@ -0,0 +1,24 @@
+
+
+ 4.0.0
+
+ org.xyzh
+ api
+ ${school-news.version}
+
+
+ org.xyzh
+ api-file
+ ${school-news.version}
+ jar
+ api-file
+ 文件模块API
+
+
+ 21
+ 21
+
+
+
\ No newline at end of file
diff --git a/schoolNewsServ/api/api-file/src/main/java/org/xyzh/api/file/FileService.java b/schoolNewsServ/api/api-file/src/main/java/org/xyzh/api/file/FileService.java
new file mode 100644
index 0000000..b14f4c2
--- /dev/null
+++ b/schoolNewsServ/api/api-file/src/main/java/org/xyzh/api/file/FileService.java
@@ -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 上传结果,包含文件信息
+ * @author system
+ * @since 2025-10-16
+ */
+ ResultDomain uploadFile(MultipartFile file, String module, String businessId);
+
+ /**
+ * @description 上传文件(带上传者信息)
+ * @param file 文件对象
+ * @param module 所属模块
+ * @param businessId 业务ID
+ * @param uploader 上传者用户ID
+ * @return ResultDomain 上传结果,包含文件信息
+ * @author system
+ * @since 2025-10-16
+ */
+ ResultDomain uploadFile(MultipartFile file, String module, String businessId, String uploader);
+
+ /**
+ * @description 下载文件
+ * @param fileId 文件ID
+ * @return ResultDomain 文件字节数组
+ * @author system
+ * @since 2025-10-16
+ */
+ ResultDomain downloadFile(String fileId);
+
+ /**
+ * @description 删除文件(逻辑删除)
+ * @param fileId 文件ID
+ * @return ResultDomain 删除结果
+ * @author system
+ * @since 2025-10-16
+ */
+ ResultDomain deleteFile(String fileId);
+
+ /**
+ * @description 物理删除文件(同时删除存储和数据库记录)
+ * @param fileId 文件ID
+ * @return ResultDomain 删除结果
+ * @author system
+ * @since 2025-10-16
+ */
+ ResultDomain deleteFilePhysically(String fileId);
+
+ /**
+ * @description 根据文件ID查询文件信息
+ * @param fileId 文件ID
+ * @return ResultDomain 文件信息
+ * @author system
+ * @since 2025-10-16
+ */
+ ResultDomain getFileById(String fileId);
+
+ /**
+ * @description 根据业务ID查询文件列表
+ * @param module 所属模块
+ * @param businessId 业务ID
+ * @return ResultDomain 文件列表
+ * @author system
+ * @since 2025-10-16
+ */
+ ResultDomain getFilesByBusinessId(String module, String businessId);
+
+ /**
+ * @description 根据上传者查询文件列表
+ * @param uploader 上传者用户ID
+ * @return ResultDomain 文件列表
+ * @author system
+ * @since 2025-10-16
+ */
+ ResultDomain getFilesByUploader(String uploader);
+
+ /**
+ * @description 获取文件访问URL
+ * @param fileId 文件ID
+ * @return ResultDomain 文件访问URL
+ * @author system
+ * @since 2025-10-16
+ */
+ ResultDomain getFileUrl(String fileId);
+
+ /**
+ * @description 批量上传文件
+ * @param files 文件对象列表
+ * @param module 所属模块
+ * @param businessId 业务ID
+ * @param uploader 上传者用户ID(可选)
+ * @return ResultDomain 上传结果,包含文件信息列表
+ * @author system
+ * @since 2025-10-16
+ */
+ ResultDomain batchUploadFiles(MultipartFile[] files, String module, String businessId, String uploader);
+
+ /**
+ * @description 批量删除文件(逻辑删除)
+ * @param fileIds 文件ID列表
+ * @return ResultDomain 删除结果
+ * @author system
+ * @since 2025-10-16
+ */
+ ResultDomain batchDeleteFiles(String[] fileIds);
+}
+
diff --git a/schoolNewsServ/api/pom.xml b/schoolNewsServ/api/pom.xml
index 2cf0650..a1ecc67 100644
--- a/schoolNewsServ/api/pom.xml
+++ b/schoolNewsServ/api/pom.xml
@@ -21,6 +21,7 @@
api-ai
api-study
api-news
+ api-file
diff --git a/schoolNewsServ/file/pom.xml b/schoolNewsServ/file/pom.xml
new file mode 100644
index 0000000..26ea5dd
--- /dev/null
+++ b/schoolNewsServ/file/pom.xml
@@ -0,0 +1,81 @@
+
+
+ 4.0.0
+
+ org.xyzh
+ school-news
+ ${school-news.version}
+
+
+ org.xyzh
+ file
+ ${school-news.version}
+ jar
+ file
+ 文件模块
+
+
+ 21
+ 21
+ 8.5.7
+
+
+
+
+
+ org.xyzh
+ api-file
+ ${school-news.version}
+
+
+
+
+ org.xyzh
+ common-all
+ ${school-news.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter
+
+
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+
+
+ io.minio
+ minio
+ ${minio.version}
+
+
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
\ No newline at end of file
diff --git a/schoolNewsServ/file/src/main/java/org/xyzh/file/config/FileStorageConfig.java b/schoolNewsServ/file/src/main/java/org/xyzh/file/config/FileStorageConfig.java
new file mode 100644
index 0000000..390df04
--- /dev/null
+++ b/schoolNewsServ/file/src/main/java/org/xyzh/file/config/FileStorageConfig.java
@@ -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 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;
+ }
+}
+
diff --git a/schoolNewsServ/file/src/main/java/org/xyzh/file/controller/FileController.java b/schoolNewsServ/file/src/main/java/org/xyzh/file/controller/FileController.java
new file mode 100644
index 0000000..08a1105
--- /dev/null
+++ b/schoolNewsServ/file/src/main/java/org/xyzh/file/controller/FileController.java
@@ -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 上传结果
+ * @author system
+ * @since 2025-10-16
+ */
+ @PostMapping("/upload")
+ public ResultDomain 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 文件字节数组
+ * @author system
+ * @since 2025-10-16
+ */
+ @GetMapping("/download/{fileId}")
+ public ResponseEntity downloadFile(@PathVariable String fileId) {
+ log.info("下载文件请求: fileId={}", fileId);
+
+ ResultDomain fileResult = fileService.getFileById(fileId);
+ if (!fileResult.isSuccess() || fileResult.getData() == null) {
+ return ResponseEntity.notFound().build();
+ }
+
+ ResultDomain 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 删除结果
+ * @author system
+ * @since 2025-10-16
+ */
+ @DeleteMapping("/{fileId}")
+ public ResultDomain deleteFile(@PathVariable String fileId) {
+ log.info("删除文件请求: fileId={}", fileId);
+ return fileService.deleteFile(fileId);
+ }
+
+ /**
+ * @description 物理删除文件
+ * @param fileId 文件ID
+ * @return ResultDomain 删除结果
+ * @author system
+ * @since 2025-10-16
+ */
+ @DeleteMapping("/physical/{fileId}")
+ public ResultDomain deleteFilePhysically(@PathVariable String fileId) {
+ log.info("物理删除文件请求: fileId={}", fileId);
+ return fileService.deleteFilePhysically(fileId);
+ }
+
+ /**
+ * @description 根据文件ID查询文件信息
+ * @param fileId 文件ID
+ * @return ResultDomain 文件信息
+ * @author system
+ * @since 2025-10-16
+ */
+ @GetMapping("/{fileId}")
+ public ResultDomain getFileById(@PathVariable String fileId) {
+ return fileService.getFileById(fileId);
+ }
+
+ /**
+ * @description 根据业务ID查询文件列表
+ * @param module 所属模块
+ * @param businessId 业务ID
+ * @return ResultDomain 文件列表
+ * @author system
+ * @since 2025-10-16
+ */
+ @GetMapping("/business/{module}/{businessId}")
+ public ResultDomain getFilesByBusinessId(
+ @PathVariable String module,
+ @PathVariable String businessId) {
+ return fileService.getFilesByBusinessId(module, businessId);
+ }
+
+ /**
+ * @description 根据上传者查询文件列表
+ * @param uploader 上传者用户ID
+ * @return ResultDomain 文件列表
+ * @author system
+ * @since 2025-10-16
+ */
+ @GetMapping("/uploader/{uploader}")
+ public ResultDomain getFilesByUploader(@PathVariable String uploader) {
+ return fileService.getFilesByUploader(uploader);
+ }
+
+ /**
+ * @description 获取文件访问URL
+ * @param fileId 文件ID
+ * @return ResultDomain 文件访问URL
+ * @author system
+ * @since 2025-10-16
+ */
+ @GetMapping("/url/{fileId}")
+ public ResultDomain getFileUrl(@PathVariable String fileId) {
+ return fileService.getFileUrl(fileId);
+ }
+
+ /**
+ * @description 批量上传文件
+ * @param files 文件对象数组
+ * @param module 所属模块
+ * @param businessId 业务ID
+ * @param uploader 上传者用户ID(可选)
+ * @return ResultDomain 上传结果
+ * @author system
+ * @since 2025-10-16
+ */
+ @PostMapping("/batch-upload")
+ public ResultDomain 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 删除结果
+ * @author system
+ * @since 2025-10-16
+ */
+ @DeleteMapping("/batch")
+ public ResultDomain batchDeleteFiles(@RequestBody String[] fileIds) {
+ log.info("批量删除文件请求: count={}", fileIds != null ? fileIds.length : 0);
+ return fileService.batchDeleteFiles(fileIds);
+ }
+}
+
diff --git a/schoolNewsServ/file/src/main/java/org/xyzh/file/mapper/FileMapper.java b/schoolNewsServ/file/src/main/java/org/xyzh/file/mapper/FileMapper.java
new file mode 100644
index 0000000..a983eee
--- /dev/null
+++ b/schoolNewsServ/file/src/main/java/org/xyzh/file/mapper/FileMapper.java
@@ -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{
+
+ /**
+ * @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 文件列表
+ * @author system
+ * @since 2025-10-16
+ */
+ List selectFilesByBusinessId(@Param("module") String module, @Param("businessId") String businessId);
+
+ /**
+ * @description 根据上传者查询文件列表
+ * @param uploader 上传者用户ID
+ * @return List 文件列表
+ * @author system
+ * @since 2025-10-16
+ */
+ List 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 fileIds);
+}
+
diff --git a/schoolNewsServ/file/src/main/java/org/xyzh/file/service/FileServiceImpl.java b/schoolNewsServ/file/src/main/java/org/xyzh/file/service/FileServiceImpl.java
new file mode 100644
index 0000000..6c65a7f
--- /dev/null
+++ b/schoolNewsServ/file/src/main/java/org/xyzh/file/service/FileServiceImpl.java
@@ -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 uploadFile(MultipartFile file, String module, String businessId) {
+ return uploadFile(file, module, businessId, null);
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public ResultDomain uploadFile(MultipartFile file, String module, String businessId, String uploader) {
+ ResultDomain 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 downloadFile(String fileId) {
+ ResultDomain 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 deleteFile(String fileId) {
+ ResultDomain 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 deleteFilePhysically(String fileId) {
+ ResultDomain 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 getFileById(String fileId) {
+ ResultDomain 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 getFilesByBusinessId(String module, String businessId) {
+ ResultDomain resultDomain = new ResultDomain<>();
+
+ try {
+ List 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 getFilesByUploader(String uploader) {
+ ResultDomain resultDomain = new ResultDomain<>();
+
+ try {
+ List 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 getFileUrl(String fileId) {
+ ResultDomain 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 batchUploadFiles(MultipartFile[] files, String module, String businessId, String uploader) {
+ ResultDomain resultDomain = new ResultDomain<>();
+
+ try {
+ if (files == null || files.length == 0) {
+ resultDomain.fail("文件列表不能为空");
+ return resultDomain;
+ }
+
+ List uploadedFiles = new ArrayList<>();
+ List errorMessages = new ArrayList<>();
+
+ for (int i = 0; i < files.length; i++) {
+ MultipartFile file = files[i];
+ try {
+ ResultDomain 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 batchDeleteFiles(String[] fileIds) {
+ ResultDomain resultDomain = new ResultDomain<>();
+
+ try {
+ if (fileIds == null || fileIds.length == 0) {
+ resultDomain.fail("文件ID列表不能为空");
+ return resultDomain;
+ }
+
+ List fileIdList = Arrays.asList(fileIds);
+ int result = fileMapper.batchLogicDeleteFiles(fileIdList);
+
+ log.info("批量逻辑删除文件成功: count={}", result);
+
+ resultDomain.success("批量删除成功", new ArrayList());
+ return resultDomain;
+ } catch (Exception e) {
+ log.error("批量删除文件失败", e);
+ resultDomain.fail("批量删除文件失败: " + e.getMessage());
+ return resultDomain;
+ }
+ }
+}
+
diff --git a/schoolNewsServ/file/src/main/java/org/xyzh/file/strategy/FileStorageStrategy.java b/schoolNewsServ/file/src/main/java/org/xyzh/file/strategy/FileStorageStrategy.java
new file mode 100644
index 0000000..27bc175
--- /dev/null
+++ b/schoolNewsServ/file/src/main/java/org/xyzh/file/strategy/FileStorageStrategy.java
@@ -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();
+}
+
diff --git a/schoolNewsServ/file/src/main/java/org/xyzh/file/strategy/FileStorageStrategyFactory.java b/schoolNewsServ/file/src/main/java/org/xyzh/file/strategy/FileStorageStrategyFactory.java
new file mode 100644
index 0000000..5fb90cf
--- /dev/null
+++ b/schoolNewsServ/file/src/main/java/org/xyzh/file/strategy/FileStorageStrategyFactory.java
@@ -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 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);
+ }
+}
+
diff --git a/schoolNewsServ/file/src/main/java/org/xyzh/file/strategy/impl/LocalFileStorageStrategy.java b/schoolNewsServ/file/src/main/java/org/xyzh/file/strategy/impl/LocalFileStorageStrategy.java
new file mode 100644
index 0000000..85c1a80
--- /dev/null
+++ b/schoolNewsServ/file/src/main/java/org/xyzh/file/strategy/impl/LocalFileStorageStrategy.java
@@ -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";
+ }
+}
+
diff --git a/schoolNewsServ/file/src/main/java/org/xyzh/file/strategy/impl/MinIOFileStorageStrategy.java b/schoolNewsServ/file/src/main/java/org/xyzh/file/strategy/impl/MinIOFileStorageStrategy.java
new file mode 100644
index 0000000..782c276
--- /dev/null
+++ b/schoolNewsServ/file/src/main/java/org/xyzh/file/strategy/impl/MinIOFileStorageStrategy.java
@@ -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";
+ }
+}
+
diff --git a/schoolNewsServ/file/src/main/resources/application.yaml b/schoolNewsServ/file/src/main/resources/application.yaml
new file mode 100644
index 0000000..a211b86
--- /dev/null
+++ b/schoolNewsServ/file/src/main/resources/application.yaml
@@ -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
+
diff --git a/schoolNewsServ/file/src/main/resources/log4j2-spring.xml b/schoolNewsServ/file/src/main/resources/log4j2-spring.xml
new file mode 100644
index 0000000..7fafe75
--- /dev/null
+++ b/schoolNewsServ/file/src/main/resources/log4j2-spring.xml
@@ -0,0 +1,81 @@
+
+
+
+ %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
+ ./file/logs
+ school-news-file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/schoolNewsServ/file/src/main/resources/mapper/FileMapper.xml b/schoolNewsServ/file/src/main/resources/mapper/FileMapper.xml
new file mode 100644
index 0000000..d015e9b
--- /dev/null
+++ b/schoolNewsServ/file/src/main/resources/mapper/FileMapper.xml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+ 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}
+ )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UPDATE tb_sys_file
+ SET deleted = 1,
+ delete_time = NOW()
+ WHERE id = #{fileId}
+ AND deleted = 0
+
+
+
+
+ DELETE FROM tb_sys_file
+ WHERE id = #{fileId}
+
+
+
+
+ UPDATE tb_sys_file
+ SET deleted = 1,
+ delete_time = NOW()
+ WHERE id IN
+
+ #{fileId}
+
+ AND deleted = 0
+
+
+