From 042ef9a81e3ff282d0866928b6b50ef825c4f903 Mon Sep 17 00:00:00 2001
From: zkh <1650697374@qq.com>
Date: Sat, 6 Dec 2025 11:54:40 +0800
Subject: [PATCH] =?UTF-8?q?feat(file):=20=E5=AE=9E=E7=8E=B0=E6=96=87?=
=?UTF-8?q?=E4=BB=B6=E4=B8=8A=E4=BC=A0=E4=B8=8B=E8=BD=BD=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增文件上传接口,支持MultipartFile格式文件上传至MinIO
- 新增文件下载接口,根据文件路径返回对应资源
- 集成MinIO客户端,实现文件存储与获取
- 添加文件信息服务,记录文件元数据
- 引入SpringDoc OpenAPI,为文件接口提供文档支持
- 配置Maven插件,生成源码包和JavaDoc包
- 升级项目版本至1.5,统一依赖管理
---
pom.xml | 8 +-
zkh-common/pom.xml | 2 +-
zkh-data/pom.xml | 2 +-
zkh-file/pom.xml | 93 ++++++++++++
.../vip/jcfd/file/config/MinioConfig.java | 23 +++
.../jcfd/file/config/props/MinioProps.java | 7 +
.../jcfd/file/controller/FileController.java | 73 +++++++++
.../main/java/vip/jcfd/file/dto/FileInfo.java | 10 ++
.../file/service/IFileDownloadService.java | 10 ++
.../jcfd/file/service/IFileUploadService.java | 11 ++
.../file/service/impl/MinioFileService.java | 140 ++++++++++++++++++
zkh-log/pom.xml | 36 ++++-
.../java/vip/jcfd/log/ConsoleLogService.java | 14 ++
.../main/java/vip/jcfd/log/ILogService.java | 8 +
.../jcfd/log/annotation/config/LogConfig.java | 7 -
.../java/vip/jcfd/log/config/LogConfig.java | 50 +++++++
zkh-web/pom.xml | 2 +-
17 files changed, 484 insertions(+), 12 deletions(-)
create mode 100644 zkh-file/pom.xml
create mode 100644 zkh-file/src/main/java/vip/jcfd/file/config/MinioConfig.java
create mode 100644 zkh-file/src/main/java/vip/jcfd/file/config/props/MinioProps.java
create mode 100644 zkh-file/src/main/java/vip/jcfd/file/controller/FileController.java
create mode 100644 zkh-file/src/main/java/vip/jcfd/file/dto/FileInfo.java
create mode 100644 zkh-file/src/main/java/vip/jcfd/file/service/IFileDownloadService.java
create mode 100644 zkh-file/src/main/java/vip/jcfd/file/service/IFileUploadService.java
create mode 100644 zkh-file/src/main/java/vip/jcfd/file/service/impl/MinioFileService.java
create mode 100644 zkh-log/src/main/java/vip/jcfd/log/ConsoleLogService.java
create mode 100644 zkh-log/src/main/java/vip/jcfd/log/ILogService.java
delete mode 100644 zkh-log/src/main/java/vip/jcfd/log/annotation/config/LogConfig.java
create mode 100644 zkh-log/src/main/java/vip/jcfd/log/config/LogConfig.java
diff --git a/pom.xml b/pom.xml
index 340d5d7..e564682 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
vip.jcfd
zkh-framework
- 1.4
+ 1.5
pom
ZKH Framework
A Java framework for ZKH applications
@@ -39,6 +39,7 @@
zkh-web
zkh-data
zkh-log
+ zkh-file
@@ -77,6 +78,11 @@
zkh-log
${project.version}
+
+ vip.jcfd
+ zkh-file
+ ${project.version}
+
org.springdoc
springdoc-openapi-common
diff --git a/zkh-common/pom.xml b/zkh-common/pom.xml
index 2efd408..604acc3 100644
--- a/zkh-common/pom.xml
+++ b/zkh-common/pom.xml
@@ -6,7 +6,7 @@
vip.jcfd
zkh-framework
- 1.4
+ 1.5
zkh-common
diff --git a/zkh-data/pom.xml b/zkh-data/pom.xml
index e15d51b..c3b7075 100644
--- a/zkh-data/pom.xml
+++ b/zkh-data/pom.xml
@@ -6,7 +6,7 @@
vip.jcfd
zkh-framework
- 1.4
+ 1.5
zkh-data
diff --git a/zkh-file/pom.xml b/zkh-file/pom.xml
new file mode 100644
index 0000000..3aad25f
--- /dev/null
+++ b/zkh-file/pom.xml
@@ -0,0 +1,93 @@
+
+
+ 4.0.0
+
+ vip.jcfd
+ zkh-framework
+ 1.5
+
+
+ zkh-file
+ ZKH file
+
+ 文件处理模块,提供文件上传、下载、处理等功能
+
+
+
+
+ vip.jcfd
+ zkh-common
+
+
+ commons-io
+ commons-io
+ 2.20.0
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ io.minio
+ minio
+ 8.5.17
+
+
+ cn.hutool
+ hutool-all
+ 5.8.41
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.8.14
+ provided
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.1
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.4.1
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+
diff --git a/zkh-file/src/main/java/vip/jcfd/file/config/MinioConfig.java b/zkh-file/src/main/java/vip/jcfd/file/config/MinioConfig.java
new file mode 100644
index 0000000..0128738
--- /dev/null
+++ b/zkh-file/src/main/java/vip/jcfd/file/config/MinioConfig.java
@@ -0,0 +1,23 @@
+package vip.jcfd.file.config;
+
+import io.minio.MinioClient;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import vip.jcfd.file.config.props.MinioProps;
+
+@Configuration
+@ConfigurationPropertiesScan(basePackageClasses = {MinioProps.class})
+public class MinioConfig {
+
+
+ @Bean
+ @ConditionalOnProperty(name = "minio.endpoint")
+ public MinioClient minioClient(MinioProps minioProps) {
+ return MinioClient.builder()
+ .endpoint(minioProps.endpoint())
+ .credentials(minioProps.accessKey(), minioProps.secretKey())
+ .build();
+ }
+}
diff --git a/zkh-file/src/main/java/vip/jcfd/file/config/props/MinioProps.java b/zkh-file/src/main/java/vip/jcfd/file/config/props/MinioProps.java
new file mode 100644
index 0000000..a044dc1
--- /dev/null
+++ b/zkh-file/src/main/java/vip/jcfd/file/config/props/MinioProps.java
@@ -0,0 +1,7 @@
+package vip.jcfd.file.config.props;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "minio")
+public record MinioProps(String endpoint, String accessKey, String secretKey, String bucket) {
+}
diff --git a/zkh-file/src/main/java/vip/jcfd/file/controller/FileController.java b/zkh-file/src/main/java/vip/jcfd/file/controller/FileController.java
new file mode 100644
index 0000000..5087420
--- /dev/null
+++ b/zkh-file/src/main/java/vip/jcfd/file/controller/FileController.java
@@ -0,0 +1,73 @@
+package vip.jcfd.file.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.Resource;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import vip.jcfd.common.core.R;
+import vip.jcfd.file.dto.FileInfo;
+import vip.jcfd.file.service.IFileDownloadService;
+import vip.jcfd.file.service.IFileUploadService;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+@Tag(name = "文件管理", description = "文件上传和下载接口")
+@RestController("_fileController")
+@RequestMapping("/file")
+public class FileController {
+ private final static Logger log = LoggerFactory.getLogger(FileController.class);
+ private final IFileUploadService fileUploadService;
+ private final IFileDownloadService fileDownloadService;
+
+ public FileController(IFileUploadService fileUploadService, IFileDownloadService fileDownloadService) {
+ this.fileUploadService = fileUploadService;
+ this.fileDownloadService = fileDownloadService;
+ }
+
+ @Operation(summary = "上传文件", description = "上传文件到服务器")
+ @ApiResponse(responseCode = "200", description = "上传成功",
+ content = @Content(mediaType = "application/json",
+ schema = @Schema(implementation = R.class)))
+ @PostMapping
+ public R upload(
+ @Parameter(description = "要上传的文件", required = true)
+ MultipartFile file) {
+ try {
+ FileInfo upload = fileUploadService.upload(file);
+ return R.success(upload);
+ } catch (IOException e) {
+ log.error("上传失败", e);
+ return R.serverError("上传失败");
+ }
+ }
+
+ @Operation(summary = "下载文件", description = "根据文件路径下载文件")
+ @ApiResponse(responseCode = "200", description = "下载成功")
+ @ApiResponse(responseCode = "404", description = "文件未找到")
+ @GetMapping("{path}")
+ public ResponseEntity download(
+ @Parameter(description = "文件路径", required = true)
+ @PathVariable String path) {
+ Path normalize = Paths.get(path).normalize();
+ try {
+ Resource download = fileDownloadService.download(normalize.toString());
+ return ResponseEntity.ok()
+ .header("Content-Disposition", "attachment; filename=\"" + download.getFilename() + "\"")
+ .body(download);
+ } catch (IOException e) {
+ log.error("下载失败", e);
+ return ResponseEntity.notFound().build();
+ }
+ }
+
+}
diff --git a/zkh-file/src/main/java/vip/jcfd/file/dto/FileInfo.java b/zkh-file/src/main/java/vip/jcfd/file/dto/FileInfo.java
new file mode 100644
index 0000000..439be1a
--- /dev/null
+++ b/zkh-file/src/main/java/vip/jcfd/file/dto/FileInfo.java
@@ -0,0 +1,10 @@
+package vip.jcfd.file.dto;
+
+import org.springframework.web.multipart.MultipartFile;
+
+public record FileInfo(String filename, String contentType, Long size, String savePath) {
+
+ public static FileInfo fromMultiPartFile(MultipartFile file, String savePath) {
+ return new FileInfo(file.getOriginalFilename(), file.getContentType(), file.getSize(), savePath);
+ }
+}
diff --git a/zkh-file/src/main/java/vip/jcfd/file/service/IFileDownloadService.java b/zkh-file/src/main/java/vip/jcfd/file/service/IFileDownloadService.java
new file mode 100644
index 0000000..0347080
--- /dev/null
+++ b/zkh-file/src/main/java/vip/jcfd/file/service/IFileDownloadService.java
@@ -0,0 +1,10 @@
+package vip.jcfd.file.service;
+
+import org.springframework.core.io.Resource;
+
+import java.io.IOException;
+
+public interface IFileDownloadService {
+
+ Resource download(String path) throws IOException;
+}
diff --git a/zkh-file/src/main/java/vip/jcfd/file/service/IFileUploadService.java b/zkh-file/src/main/java/vip/jcfd/file/service/IFileUploadService.java
new file mode 100644
index 0000000..1d7ebd1
--- /dev/null
+++ b/zkh-file/src/main/java/vip/jcfd/file/service/IFileUploadService.java
@@ -0,0 +1,11 @@
+package vip.jcfd.file.service;
+
+import org.springframework.web.multipart.MultipartFile;
+import vip.jcfd.file.dto.FileInfo;
+
+import java.io.IOException;
+
+public interface IFileUploadService {
+
+ FileInfo upload(MultipartFile file) throws IOException;
+}
diff --git a/zkh-file/src/main/java/vip/jcfd/file/service/impl/MinioFileService.java b/zkh-file/src/main/java/vip/jcfd/file/service/impl/MinioFileService.java
new file mode 100644
index 0000000..46030ef
--- /dev/null
+++ b/zkh-file/src/main/java/vip/jcfd/file/service/impl/MinioFileService.java
@@ -0,0 +1,140 @@
+package vip.jcfd.file.service.impl;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.util.StrUtil;
+import io.minio.*;
+import io.minio.errors.*;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.core.io.AbstractResource;
+import org.springframework.core.io.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+import vip.jcfd.common.core.BizException;
+import vip.jcfd.file.dto.FileInfo;
+import vip.jcfd.file.service.IFileDownloadService;
+import vip.jcfd.file.service.IFileUploadService;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+@Service
+@ConditionalOnBean(MinioClient.class)
+public class MinioFileService implements IFileUploadService, IFileDownloadService {
+ private final static Logger log = LoggerFactory.getLogger(MinioFileService.class);
+ private final MinioClient minioClient;
+ private final static String BUCKET_NAME = "upload"; // Changed from private to static
+ private final static String ORIGIN_FILENAME = "origin_filename";
+
+ public MinioFileService(MinioClient minioClient) {
+ this.minioClient = minioClient;
+ }
+
+ @Override
+ public Resource download(String path) throws IOException {
+ Assert.isTrue(path.startsWith("/" + BUCKET_NAME), () -> new BizException("路径不合法"));
+ String[] split = path.split("/");
+ Assert.isTrue(split.length == 3, () -> new BizException("路径不合法"));
+ String objectName = split[2];
+ GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(BUCKET_NAME).object(objectName).build();
+ try {
+ StatObjectResponse objectStat = minioClient.statObject(StatObjectArgs.builder().bucket(BUCKET_NAME).object(objectName).build());
+ Map userMetadata = objectStat.userMetadata();
+ GetObjectResponse object = minioClient.getObject(getObjectArgs);
+
+ return new AbstractResource() {
+ @NotNull
+ @Override
+ public String getDescription() {
+ return userMetadata.getOrDefault(ORIGIN_FILENAME, "");
+ }
+
+ @NotNull
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return object;
+ }
+
+ @Override
+ public String getFilename() {
+ return userMetadata.getOrDefault(ORIGIN_FILENAME, "");
+ }
+
+ @Override
+ public long contentLength() throws IOException {
+ return objectStat.size();
+ }
+ };
+ } catch (ErrorResponseException e) {
+ log.error("minio 服务异常", e);
+ } catch (InsufficientDataException e) {
+ log.error("数据不完整", e);
+ } catch (InternalException e) {
+ log.error("内部异常", e);
+ } catch (InvalidKeyException e) {
+ log.error("无效的密钥", e);
+ } catch (InvalidResponseException e) {
+ log.error("无效的响应", e);
+ } catch (NoSuchAlgorithmException e) {
+ log.error("没有这样的算法", e);
+ } catch (ServerException e) {
+ log.error("服务器异常", e);
+ } catch (XmlParserException e) {
+ log.error("XML解析异常", e);
+ }
+ throw new BizException("下载失败");
+ }
+
+ @Override
+ public FileInfo upload(MultipartFile file) throws IOException {
+ try {
+ if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(BUCKET_NAME).build())) {
+ minioClient.makeBucket(MakeBucketArgs.builder().bucket(BUCKET_NAME).build());
+ }
+ String string = UUID.randomUUID().toString();
+ String originalFilename = file.getOriginalFilename();
+ String filename = string;
+ if (StrUtil.isNotEmpty(originalFilename)) {
+ String suffix = FileUtil.getSuffix(originalFilename);
+ filename += "." + suffix;
+ }
+ PutObjectArgs putObjectArgs = PutObjectArgs.builder()
+ .stream(file.getInputStream(), file.getSize(), -1)
+ .contentType(file.getContentType())
+ .object(filename)
+ .userMetadata(Map.of(
+ ORIGIN_FILENAME, Optional.ofNullable(originalFilename).orElse(filename)
+ ))
+ .bucket(BUCKET_NAME)
+ .build();
+ ObjectWriteResponse response = minioClient.putObject(putObjectArgs);
+ String savePath = "/" + response.bucket() + "/" + response.object();
+ return FileInfo.fromMultiPartFile(file, savePath);
+ } catch (ErrorResponseException e) {
+ log.error("minio 服务异常", e);
+ } catch (InsufficientDataException e) {
+ log.error("数据不完整", e);
+ } catch (InternalException e) {
+ log.error("内部异常", e);
+ } catch (InvalidKeyException e) {
+ log.error("无效的密钥", e);
+ } catch (InvalidResponseException e) {
+ log.error("无效的响应", e);
+ } catch (NoSuchAlgorithmException e) {
+ log.error("没有这样的算法", e);
+ } catch (ServerException e) {
+ log.error("服务器异常", e);
+ } catch (XmlParserException e) {
+ log.error("XML解析异常", e);
+ }
+ throw new BizException("上传失败");
+ }
+}
diff --git a/zkh-log/pom.xml b/zkh-log/pom.xml
index 960722d..74a32e8 100644
--- a/zkh-log/pom.xml
+++ b/zkh-log/pom.xml
@@ -6,7 +6,7 @@
vip.jcfd
zkh-framework
- 1.4
+ 1.5
zkh-log
@@ -18,6 +18,40 @@
org.springframework.boot
spring-boot-starter-aop
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.4.1
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
diff --git a/zkh-log/src/main/java/vip/jcfd/log/ConsoleLogService.java b/zkh-log/src/main/java/vip/jcfd/log/ConsoleLogService.java
new file mode 100644
index 0000000..9a669e6
--- /dev/null
+++ b/zkh-log/src/main/java/vip/jcfd/log/ConsoleLogService.java
@@ -0,0 +1,14 @@
+package vip.jcfd.log;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+
+public class ConsoleLogService implements ILogService {
+ private final Logger logger = LoggerFactory.getLogger(ConsoleLogService.class);
+
+ @Override
+ public void log(String message, Authentication authentication) {
+ logger.debug("{} {}", authentication.getName(), message);
+ }
+}
diff --git a/zkh-log/src/main/java/vip/jcfd/log/ILogService.java b/zkh-log/src/main/java/vip/jcfd/log/ILogService.java
new file mode 100644
index 0000000..e230005
--- /dev/null
+++ b/zkh-log/src/main/java/vip/jcfd/log/ILogService.java
@@ -0,0 +1,8 @@
+package vip.jcfd.log;
+
+import org.springframework.security.core.Authentication;
+
+public interface ILogService {
+
+ void log(String message, Authentication authentication);
+}
diff --git a/zkh-log/src/main/java/vip/jcfd/log/annotation/config/LogConfig.java b/zkh-log/src/main/java/vip/jcfd/log/annotation/config/LogConfig.java
deleted file mode 100644
index 498044d..0000000
--- a/zkh-log/src/main/java/vip/jcfd/log/annotation/config/LogConfig.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package vip.jcfd.log.annotation.config;
-
-import org.springframework.context.annotation.Configuration;
-
-@Configuration("_logConfiguration")
-public class LogConfig {
-}
diff --git a/zkh-log/src/main/java/vip/jcfd/log/config/LogConfig.java b/zkh-log/src/main/java/vip/jcfd/log/config/LogConfig.java
new file mode 100644
index 0000000..0c475b5
--- /dev/null
+++ b/zkh-log/src/main/java/vip/jcfd/log/config/LogConfig.java
@@ -0,0 +1,50 @@
+package vip.jcfd.log.config;
+
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import vip.jcfd.log.ConsoleLogService;
+import vip.jcfd.log.ILogService;
+import vip.jcfd.log.annotation.Log;
+
+@Configuration("_logConfiguration")
+public class LogConfig {
+
+ @Bean("_defaultLogService")
+ @ConditionalOnMissingBean
+ @Order
+ public ILogService defaultLogService() {
+ return new ConsoleLogService();
+ }
+
+ @Aspect
+ @Component
+ public static class LogAspect {
+ private final ILogService logService;
+
+ public LogAspect(ILogService logService) {
+ this.logService = logService;
+ }
+
+ @Pointcut("@annotation(vip.jcfd.log.annotation.Log)")
+ public void logAspect() {
+ }
+
+ @AfterReturning(value = "logAspect()")
+ public void afterReturning(JoinPoint joinPoint) {
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+ Log log = signature.getMethod().getAnnotation(Log.class);
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ logService.log(log.value(), authentication);
+ }
+ }
+}
diff --git a/zkh-web/pom.xml b/zkh-web/pom.xml
index 5fed6dd..52606c2 100644
--- a/zkh-web/pom.xml
+++ b/zkh-web/pom.xml
@@ -7,7 +7,7 @@
vip.jcfd
zkh-framework
- 1.4
+ 1.5
zkh-web