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