Compare commits
24
Commits
a7e1c26853
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f97cab38f | ||
|
|
9e75d3b392 | ||
|
|
42228c63a5 | ||
|
|
411565f812 | ||
|
|
c0448ff6ab | ||
|
|
7b346802e0 | ||
|
|
f152b1e655 | ||
|
|
01d29e6ec3 | ||
|
|
06b5258824 | ||
|
|
3d4bec6e96 | ||
|
|
eb82090586 | ||
|
|
753a07f71d | ||
|
|
209769d024 | ||
|
|
88afa8b47e | ||
|
|
ab39c0f9b2 | ||
|
|
eeac5b430c | ||
|
|
a0cb0cb6b7 | ||
|
|
f679be8cee | ||
|
|
9d7a061305 | ||
|
|
5014411874 | ||
|
|
1fe871faa8 | ||
|
|
1f3fea8277 | ||
|
|
042ef9a81e | ||
|
|
fe2240e266 |
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-framework</artifactId>
|
||||
<version>1.4</version>
|
||||
<version>1.5.12</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>ZKH Framework</name>
|
||||
<description>A Java framework for ZKH applications</description>
|
||||
@@ -38,6 +38,8 @@
|
||||
<module>zkh-common</module>
|
||||
<module>zkh-web</module>
|
||||
<module>zkh-data</module>
|
||||
<module>zkh-log</module>
|
||||
<module>zkh-file</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
@@ -71,6 +73,16 @@
|
||||
<artifactId>zkh-data</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-log</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-file</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-common</artifactId>
|
||||
|
||||
+25
-2
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-framework</artifactId>
|
||||
<version>1.4</version>
|
||||
<version>1.5.12</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zkh-common</artifactId>
|
||||
@@ -14,6 +14,14 @@
|
||||
<description>Common utilities and base classes for ZKH framework</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.annotation</groupId>
|
||||
<artifactId>jakarta.annotation-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.persistence</groupId>
|
||||
<artifactId>jakarta.persistence-api</artifactId>
|
||||
@@ -30,14 +38,29 @@
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-relational</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-common</artifactId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.8.14</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.1</version>
|
||||
<configuration>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
|
||||
@@ -8,72 +8,73 @@ import org.springframework.data.annotation.LastModifiedBy;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class BaseEntity {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
public class BaseEntity implements Serializable {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@Column
|
||||
@CreatedDate
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
@Column
|
||||
@CreatedDate
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Column
|
||||
@LastModifiedDate
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
@Column
|
||||
@LastModifiedDate
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Column
|
||||
@CreatedBy
|
||||
private String createBy;
|
||||
@Column
|
||||
@CreatedBy
|
||||
private String createBy;
|
||||
|
||||
@Column
|
||||
@LastModifiedBy
|
||||
private String updateBy;
|
||||
@Column
|
||||
@LastModifiedBy
|
||||
private String updateBy;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
public LocalDateTime getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public void setCreateTime(LocalDateTime createTime) {
|
||||
this.createTime = createTime;
|
||||
}
|
||||
public void setCreateTime(LocalDateTime createTime) {
|
||||
this.createTime = createTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdateTime() {
|
||||
return updateTime;
|
||||
}
|
||||
public LocalDateTime getUpdateTime() {
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
public void setUpdateTime(LocalDateTime updateTime) {
|
||||
this.updateTime = updateTime;
|
||||
}
|
||||
public void setUpdateTime(LocalDateTime updateTime) {
|
||||
this.updateTime = updateTime;
|
||||
}
|
||||
|
||||
public String getCreateBy() {
|
||||
return createBy;
|
||||
}
|
||||
public String getCreateBy() {
|
||||
return createBy;
|
||||
}
|
||||
|
||||
public void setCreateBy(String createBy) {
|
||||
this.createBy = createBy;
|
||||
}
|
||||
public void setCreateBy(String createBy) {
|
||||
this.createBy = createBy;
|
||||
}
|
||||
|
||||
public String getUpdateBy() {
|
||||
return updateBy;
|
||||
}
|
||||
public String getUpdateBy() {
|
||||
return updateBy;
|
||||
}
|
||||
|
||||
public void setUpdateBy(String updateBy) {
|
||||
this.updateBy = updateBy;
|
||||
}
|
||||
public void setUpdateBy(String updateBy) {
|
||||
this.updateBy = updateBy;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package vip.jcfd.common.core;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import org.springframework.data.annotation.*;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class R2dbcBaseEntity implements Serializable {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@Column("create_time")
|
||||
@CreatedDate
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Column("update_time")
|
||||
@LastModifiedDate
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Column("create_by")
|
||||
@CreatedBy
|
||||
private String createBy;
|
||||
|
||||
@Column("update_by")
|
||||
@LastModifiedBy
|
||||
private String updateBy;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public void setCreateTime(LocalDateTime createTime) {
|
||||
this.createTime = createTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdateTime() {
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
public void setUpdateTime(LocalDateTime updateTime) {
|
||||
this.updateTime = updateTime;
|
||||
}
|
||||
|
||||
public String getCreateBy() {
|
||||
return createBy;
|
||||
}
|
||||
|
||||
public void setCreateBy(String createBy) {
|
||||
this.createBy = createBy;
|
||||
}
|
||||
}
|
||||
+4
-7
@@ -6,19 +6,13 @@
|
||||
<parent>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-framework</artifactId>
|
||||
<version>1.4</version>
|
||||
<version>1.5.12</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zkh-data</artifactId>
|
||||
<name>ZKH Data</name>
|
||||
<description>Data layer components for ZKH framework</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>jakarta.persistence</groupId>
|
||||
@@ -52,6 +46,9 @@
|
||||
<version>1.18.42</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?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>vip.jcfd</groupId>
|
||||
<artifactId>zkh-framework</artifactId>
|
||||
<version>1.5.12</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zkh-file</artifactId>
|
||||
<name>ZKH file</name>
|
||||
<description>
|
||||
文件处理模块,提供文件上传、下载、处理等功能
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.20.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
<version>8.5.17</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.41</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.1</version>
|
||||
<configuration>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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.MediaType;
|
||||
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(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public R<FileInfo> upload(
|
||||
@Parameter(description = "要上传的文件", required = true, name = "file")
|
||||
@RequestPart("file") 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
|
||||
public ResponseEntity<Resource> download(
|
||||
@Parameter(description = "文件路径", required = true)
|
||||
@RequestParam("path") 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<String, String> 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("上传失败");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?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>vip.jcfd</groupId>
|
||||
<artifactId>zkh-framework</artifactId>
|
||||
<version>1.5.12</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zkh-log</artifactId>
|
||||
<name>ZKH log</name>
|
||||
<description>Logging utilities for ZKH framework</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>jakarta.servlet</groupId>
|
||||
<artifactId>jakarta.servlet-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.1</version>
|
||||
<configuration>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,26 @@
|
||||
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) {
|
||||
String operator = authentication != null ? authentication.getName() : "anonymous";
|
||||
logger.debug("{} {}", operator, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(LogContext context) {
|
||||
if (context.status() == LogContext.SUCCESS) {
|
||||
logger.debug("[操作日志] {} {} {} {} {}", context.operator(), context.httpMethod(),
|
||||
context.requestUrl(), context.ip(), context.message());
|
||||
} else {
|
||||
logger.warn("[操作日志] {} {} {} {} {} 错误: {}", context.operator(), context.httpMethod(),
|
||||
context.requestUrl(), context.ip(), context.message(), context.errorMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package vip.jcfd.log;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
public interface ILogService {
|
||||
|
||||
/**
|
||||
* 记录日志(旧接口,保持向后兼容)
|
||||
*/
|
||||
void log(String message, Authentication authentication);
|
||||
|
||||
/**
|
||||
* 记录日志(新接口,带完整上下文)
|
||||
*/
|
||||
default void log(LogContext context) {
|
||||
// 默认实现:降级到旧接口
|
||||
log(context.message(), null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package vip.jcfd.log;
|
||||
|
||||
/**
|
||||
* 日志上下文,包含操作日志的完整信息
|
||||
*/
|
||||
public record LogContext(
|
||||
String message,
|
||||
String operator,
|
||||
String requestUrl,
|
||||
String httpMethod,
|
||||
String ip,
|
||||
int status,
|
||||
String errorMessage
|
||||
) {
|
||||
/** 操作状态:成功 */
|
||||
public static final int SUCCESS = 0;
|
||||
/** 操作状态:失败 */
|
||||
public static final int FAIL = 1;
|
||||
|
||||
public static LogContext success(String message, String operator, String requestUrl, String httpMethod, String ip) {
|
||||
return new LogContext(message, operator, requestUrl, httpMethod, ip, SUCCESS, null);
|
||||
}
|
||||
|
||||
public static LogContext fail(String message, String operator, String requestUrl, String httpMethod, String ip, String errorMessage) {
|
||||
return new LogContext(message, operator, requestUrl, httpMethod, ip, FAIL, errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package vip.jcfd.log.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface Log {
|
||||
String value();
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package vip.jcfd.log.config;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.AfterReturning;
|
||||
import org.aspectj.lang.annotation.AfterThrowing;
|
||||
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 org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import vip.jcfd.log.ConsoleLogService;
|
||||
import vip.jcfd.log.ILogService;
|
||||
import vip.jcfd.log.LogContext;
|
||||
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) {
|
||||
LogContext context = buildLogContext(joinPoint, null);
|
||||
logService.log(context);
|
||||
}
|
||||
|
||||
@AfterThrowing(value = "logAspect()", throwing = "ex")
|
||||
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
|
||||
LogContext context = buildLogContext(joinPoint, ex.getMessage());
|
||||
logService.log(context);
|
||||
}
|
||||
|
||||
private LogContext buildLogContext(JoinPoint joinPoint, String errorMessage) {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Log log = signature.getMethod().getAnnotation(Log.class);
|
||||
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
String operator = authentication != null ? authentication.getName() : "anonymous";
|
||||
|
||||
String requestUrl = "";
|
||||
String httpMethod = "";
|
||||
String ip = "";
|
||||
|
||||
ServletRequestAttributes attributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
requestUrl = request.getRequestURI();
|
||||
httpMethod = request.getMethod();
|
||||
ip = getClientIp(request);
|
||||
}
|
||||
|
||||
if (errorMessage != null) {
|
||||
return LogContext.fail(log.value(), operator, requestUrl, httpMethod, ip, errorMessage);
|
||||
}
|
||||
return LogContext.success(log.value(), operator, requestUrl, httpMethod, ip);
|
||||
}
|
||||
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
// X-Forwarded-For 可能包含多个 IP,取第一个
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-7
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-framework</artifactId>
|
||||
<version>1.4</version>
|
||||
<version>1.5.12</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zkh-web</artifactId>
|
||||
@@ -19,6 +19,14 @@
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-log</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
@@ -39,12 +47,6 @@
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.8.14</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@@ -60,6 +62,9 @@
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package vip.jcfd.web.config;
|
||||
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import jakarta.validation.ValidationException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
@@ -16,32 +20,73 @@ import java.util.List;
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
public R<String> handleException(Exception e) {
|
||||
log.error("服务异常", e);
|
||||
return R.serverError("服务器繁忙,请稍候重试");
|
||||
}
|
||||
@ExceptionHandler(value = {Exception.class, RuntimeException.class})
|
||||
public R<String> handleException(Throwable e) {
|
||||
log.error("服务异常", e);
|
||||
return R.serverError("服务器繁忙,请稍候重试");
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = BizException.class)
|
||||
public R<String> handleBizException(BizException e) {
|
||||
log.error("业务异常", e);
|
||||
return R.error(e.getMessage());
|
||||
}
|
||||
@ExceptionHandler(value = BizException.class)
|
||||
public R<String> handleBizException(BizException e) {
|
||||
log.error("业务异常", e);
|
||||
return R.error(e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = NoResourceFoundException.class)
|
||||
public R<String> handleNotFoundException(NoResourceFoundException e) {
|
||||
log.error("404异常", e);
|
||||
return new R<>(404, "您访问的地址不存在", false, null);
|
||||
}
|
||||
@ExceptionHandler(value = NoResourceFoundException.class)
|
||||
public R<String> handleNotFoundException(NoResourceFoundException e) {
|
||||
log.error("404异常", e);
|
||||
return new R<>(404, "您访问的地址不存在", false, null);
|
||||
}
|
||||
|
||||
|
||||
@ExceptionHandler(value = BindException.class)
|
||||
public R<String> handleBindException(BindException e) {
|
||||
log.error("接口入参校验失败", e);
|
||||
/**
|
||||
* Handles bind exceptions; logs and returns formatted field errors
|
||||
*/
|
||||
@ExceptionHandler(value = BindException.class)
|
||||
public R<String> handleBindException(BindException e) {
|
||||
log.error("接口入参校验失败", e);
|
||||
|
||||
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
|
||||
return R.error(String.join("。\n", fieldErrors.stream().map(FieldError::getDefaultMessage).toList()));
|
||||
}
|
||||
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
|
||||
return R.error(String.join("。\n", fieldErrors.stream().map(FieldError::getDefaultMessage).toList()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = ValidationException.class)
|
||||
public R<String> handleValidationException(ValidationException e) {
|
||||
log.error("接口入参校验失败", e);
|
||||
return R.error(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 @RequestBody + @Valid 校验失败
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public R<?> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
|
||||
log.error("接口入参校验失败", ex);
|
||||
BindingResult bindingResult = ex.getBindingResult();
|
||||
|
||||
String msg = bindingResult.getFieldErrors()
|
||||
.stream()
|
||||
.map(err -> err.getField() + ": " + err.getDefaultMessage())
|
||||
.findFirst()
|
||||
.orElse("参数错误");
|
||||
|
||||
return R.error(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 @RequestParam / @PathVariable 校验失败
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
public R<?> handleConstraintViolation(ConstraintViolationException ex) {
|
||||
log.error("接口入参校验失败", ex);
|
||||
String msg = ex.getConstraintViolations()
|
||||
.stream()
|
||||
.map(v -> v.getPropertyPath() + ": " + v.getMessage())
|
||||
.findFirst()
|
||||
.orElse("参数错误");
|
||||
|
||||
return R.error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package vip.jcfd.web.config;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
/**
|
||||
* Configures Jackson mapper with locale, timezone, date format, serializer
|
||||
*/
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
|
||||
return jacksonObjectMapperBuilder -> {
|
||||
jacksonObjectMapperBuilder.locale(Locale.CHINA);
|
||||
jacksonObjectMapperBuilder.timeZone(TimeZone.getTimeZone(ZoneId.of("Asia/Shanghai")));
|
||||
jacksonObjectMapperBuilder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
jacksonObjectMapperBuilder.serializers(new LongSerializer(), new LocalDateTimeSerializer(), new LocalDateSerializer());
|
||||
jacksonObjectMapperBuilder.deserializers(new LongDeserializer(), new LocalDateTimeDeserializer(), new LocalDateDeserializer());
|
||||
};
|
||||
}
|
||||
|
||||
public static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
|
||||
|
||||
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
if (value == null) {
|
||||
gen.writeNull();
|
||||
return;
|
||||
}
|
||||
gen.writeString(value.format(formatter));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<LocalDateTime> handledType() {
|
||||
return LocalDateTime.class;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
|
||||
|
||||
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@Override
|
||||
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
if (p.getText() == null) {
|
||||
return null;
|
||||
}
|
||||
return LocalDateTime.parse(p.getText(), formatter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> handledType() {
|
||||
return LocalDateTime.class;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LocalDateSerializer extends JsonSerializer<LocalDate> {
|
||||
|
||||
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
@Override
|
||||
public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
if (value == null) {
|
||||
gen.writeNull();
|
||||
return;
|
||||
}
|
||||
gen.writeString(value.format(formatter));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<LocalDate> handledType() {
|
||||
return LocalDate.class;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LocalDateDeserializer extends JsonDeserializer<LocalDate> {
|
||||
|
||||
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
@Override
|
||||
public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
if (p.getText() == null) {
|
||||
return null;
|
||||
}
|
||||
return LocalDate.parse(p.getText(), formatter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> handledType() {
|
||||
return LocalDate.class;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LongSerializer extends JsonSerializer<Long> {
|
||||
@Override
|
||||
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
if (value == null) {
|
||||
gen.writeNull();
|
||||
return;
|
||||
}
|
||||
gen.writeString(String.valueOf(value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Long> handledType() {
|
||||
return Long.class;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LongDeserializer extends JsonDeserializer<Long> {
|
||||
@Override
|
||||
public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
if (p.getText() == null) {
|
||||
return null;
|
||||
}
|
||||
return Long.parseLong(p.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> handledType() {
|
||||
return Long.class;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,26 +10,26 @@ import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import vip.jcfd.web.config.props.SecurityProps;
|
||||
import vip.jcfd.web.redis.TokenRedisStorage;
|
||||
|
||||
@Configuration
|
||||
@Configuration("_redisConfiguration")
|
||||
public class RedisConfig {
|
||||
|
||||
private final SecurityProps securityProps;
|
||||
private final SecurityProps securityProps;
|
||||
|
||||
public RedisConfig(SecurityProps securityProps) {
|
||||
this.securityProps = securityProps;
|
||||
}
|
||||
public RedisConfig(SecurityProps securityProps) {
|
||||
this.securityProps = securityProps;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TokenRedisStorage tokenRedisTemplate(RedisConnectionFactory factory, StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper) {
|
||||
TokenRedisStorage tokenRedisStorage = new TokenRedisStorage(
|
||||
securityProps.getAccessTokenDuration(),
|
||||
securityProps.getRefreshTokenDuration(),
|
||||
stringRedisTemplate,
|
||||
objectMapper
|
||||
);
|
||||
tokenRedisStorage.setConnectionFactory(factory);
|
||||
tokenRedisStorage.setValueSerializer(new JdkSerializationRedisSerializer());
|
||||
tokenRedisStorage.setKeySerializer(new StringRedisSerializer());
|
||||
return tokenRedisStorage;
|
||||
}
|
||||
@Bean
|
||||
public TokenRedisStorage tokenRedisTemplate(RedisConnectionFactory factory, StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper) {
|
||||
TokenRedisStorage tokenRedisStorage = new TokenRedisStorage(
|
||||
securityProps.getAccessTokenDuration(),
|
||||
securityProps.getRefreshTokenDuration(),
|
||||
stringRedisTemplate,
|
||||
objectMapper
|
||||
);
|
||||
tokenRedisStorage.setConnectionFactory(factory);
|
||||
tokenRedisStorage.setValueSerializer(new JdkSerializationRedisSerializer());
|
||||
tokenRedisStorage.setKeySerializer(new StringRedisSerializer());
|
||||
return tokenRedisStorage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,63 +10,63 @@ import org.springdoc.core.customizers.OpenApiCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration("_springDocConfig")
|
||||
@Configuration("_springDocConfiguration")
|
||||
public class SpringDocConfig {
|
||||
|
||||
@Bean
|
||||
public OpenApiCustomizer openApiCustomizer() {
|
||||
return (openAPI) -> {
|
||||
openAPI.path("/login", new PathItem()
|
||||
.post(new Operation()
|
||||
.summary("登录接口")
|
||||
.description("用于用户登录,返回token")
|
||||
.addTagsItem("认证管理")
|
||||
.requestBody(new RequestBody()
|
||||
.description("帐号密码")
|
||||
.required(true)
|
||||
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
||||
.addProperty("username", new StringSchema().example("admin"))
|
||||
.addProperty("password", new StringSchema().example("123456"))))))
|
||||
.responses(new ApiResponses()
|
||||
.addApiResponse("成功", new ApiResponse()
|
||||
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
||||
.addProperty("data", new JsonSchema()
|
||||
.addProperty("accessToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440000"))
|
||||
.addProperty("refreshToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440001"))
|
||||
.addProperty("tokenType", new StringSchema().example("Bearer"))
|
||||
.addProperty("expiresIn", new NumberSchema().example(1800))
|
||||
.addProperty("username", new StringSchema().example("admin"))
|
||||
)
|
||||
.addProperty("success", new BooleanSchema().example(true))
|
||||
.addProperty("code", new IntegerSchema().example(200))
|
||||
.addProperty("message", new StringSchema().example("登录成功"))
|
||||
))))
|
||||
.addApiResponse("失败", new ApiResponse()
|
||||
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
||||
.addProperty("data", new StringSchema().example(null))
|
||||
.addProperty("success", new BooleanSchema().example(false))
|
||||
.addProperty("code", new IntegerSchema().example(401))
|
||||
.addProperty("message", new StringSchema().example("用户名或密码错误"))
|
||||
))))
|
||||
)));
|
||||
openAPI.path("/logout", new PathItem()
|
||||
.post(new Operation()
|
||||
.summary("登出接口")
|
||||
.description("用于用户登出")
|
||||
.addTagsItem("认证管理")
|
||||
.responses(new ApiResponses()
|
||||
.addApiResponse("成功", new ApiResponse()
|
||||
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
||||
.addProperty("data", new StringSchema().example(null))
|
||||
.addProperty("success", new BooleanSchema().example(true))
|
||||
.addProperty("code", new IntegerSchema().example(200))
|
||||
.addProperty("message", new StringSchema().example("登出成功"))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
));
|
||||
};
|
||||
}
|
||||
@Bean
|
||||
public OpenApiCustomizer openApiCustomizer() {
|
||||
return (openAPI) -> {
|
||||
openAPI.path("/login", new PathItem()
|
||||
.post(new Operation()
|
||||
.summary("登录接口")
|
||||
.description("用于用户登录,返回token")
|
||||
.addTagsItem("认证管理")
|
||||
.requestBody(new RequestBody()
|
||||
.description("帐号密码")
|
||||
.required(true)
|
||||
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
||||
.addProperty("username", new StringSchema().example("admin"))
|
||||
.addProperty("password", new StringSchema().example("123456"))))))
|
||||
.responses(new ApiResponses()
|
||||
.addApiResponse("成功", new ApiResponse()
|
||||
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
||||
.addProperty("data", new JsonSchema()
|
||||
.addProperty("accessToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440000"))
|
||||
.addProperty("refreshToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440001"))
|
||||
.addProperty("tokenType", new StringSchema().example("Bearer"))
|
||||
.addProperty("expiresIn", new NumberSchema().example(1800))
|
||||
.addProperty("username", new StringSchema().example("admin"))
|
||||
)
|
||||
.addProperty("success", new BooleanSchema().example(true))
|
||||
.addProperty("code", new IntegerSchema().example(200))
|
||||
.addProperty("message", new StringSchema().example("登录成功"))
|
||||
))))
|
||||
.addApiResponse("失败", new ApiResponse()
|
||||
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
||||
.addProperty("data", new StringSchema().example(null))
|
||||
.addProperty("success", new BooleanSchema().example(false))
|
||||
.addProperty("code", new IntegerSchema().example(401))
|
||||
.addProperty("message", new StringSchema().example("用户名或密码错误"))
|
||||
))))
|
||||
)));
|
||||
openAPI.path("/logout", new PathItem()
|
||||
.post(new Operation()
|
||||
.summary("登出接口")
|
||||
.description("用于用户登出")
|
||||
.addTagsItem("认证管理")
|
||||
.responses(new ApiResponses()
|
||||
.addApiResponse("成功", new ApiResponse()
|
||||
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
||||
.addProperty("data", new StringSchema().example(null))
|
||||
.addProperty("success", new BooleanSchema().example(true))
|
||||
.addProperty("code", new IntegerSchema().example(200))
|
||||
.addProperty("message", new StringSchema().example("登出成功"))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
));
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package vip.jcfd.web.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -10,6 +11,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.data.domain.AuditorAware;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -52,7 +54,7 @@ import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Configuration
|
||||
@Configuration("_webSecurityConfiguration")
|
||||
@EnableWebSecurity
|
||||
@ConfigurationPropertiesScan(basePackageClasses = {SecurityProps.class})
|
||||
@EnableJpaAuditing
|
||||
@@ -110,12 +112,14 @@ public class WebSecurityConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order
|
||||
public SecurityFilterChain security(HttpSecurity http, TokenFilter tokenFilter, AuthenticationManager authenticationManager) throws Exception {
|
||||
http.authorizeHttpRequests(config -> {
|
||||
config.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll();
|
||||
config.requestMatchers(securityProps.getIgnoreUrls()).permitAll();
|
||||
config.anyRequest().authenticated();
|
||||
});
|
||||
CustomAuthenticationEntryPoint authenticationEntryPoint = new CustomAuthenticationEntryPoint(objectMapper, tokenRedisStorage);
|
||||
CustomAuthenticationEntryPoint authenticationEntryPoint = new CustomAuthenticationEntryPoint(objectMapper, tokenRedisStorage, securityProps);
|
||||
http.formLogin(config -> {
|
||||
config.loginProcessingUrl("/login");
|
||||
});
|
||||
@@ -140,10 +144,11 @@ public class WebSecurityConfig {
|
||||
|
||||
private record CustomAuthenticationEntryPoint(
|
||||
ObjectMapper objectMapper,
|
||||
TokenRedisStorage tokenRedisStorage) implements AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler {
|
||||
TokenRedisStorage tokenRedisStorage,
|
||||
SecurityProps securityProps) implements AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler {
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
||||
log.warn("认证失败", authException);
|
||||
log.warn("访问 {} ,但是认证失败", request.getRequestURI(), authException);
|
||||
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "未登录", false, null);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
objectMapper.writeValue(response.getWriter(), data);
|
||||
@@ -152,7 +157,7 @@ public class WebSecurityConfig {
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
|
||||
log.warn("登录失败", exception);
|
||||
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "用户名或密码错误", false, null);
|
||||
R<Object> data = new R<>(HttpServletResponse.SC_BAD_REQUEST, "用户名或密码错误", false, null);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
objectMapper.writeValue(response.getWriter(), data);
|
||||
}
|
||||
@@ -177,7 +182,7 @@ public class WebSecurityConfig {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
"Bearer",
|
||||
1800, // 30分钟,秒数
|
||||
securityProps.getDuration().getSeconds(), // 30分钟,秒数
|
||||
authentication.getName()
|
||||
);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
import vip.jcfd.common.core.R;
|
||||
import vip.jcfd.common.dto.TokenRefreshRequest;
|
||||
import vip.jcfd.common.dto.TokenRefreshResponse;
|
||||
import vip.jcfd.log.annotation.Log;
|
||||
import vip.jcfd.web.auth.RefreshTokenAuthenticationToken;
|
||||
import vip.jcfd.web.redis.TokenRedisStorage;
|
||||
|
||||
@@ -37,6 +38,7 @@ public class AuthController {
|
||||
|
||||
@PostMapping("/refresh-token")
|
||||
@Operation(summary = "刷新Token", description = "使用Refresh Token获取新的Access Token和Refresh Token")
|
||||
@Log("刷新了token")
|
||||
public R<TokenRefreshResponse> refreshToken(
|
||||
@Valid @RequestBody TokenRefreshRequest request) {
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package vip.jcfd.web.controller;
|
||||
|
||||
import jakarta.servlet.RequestDispatcher;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.web.servlet.error.ErrorController;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import vip.jcfd.common.core.R;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 自定义错误控制器
|
||||
* 重写 Spring MVC 默认的错误视图,返回 JSON 格式的错误响应
|
||||
*/
|
||||
@RestController
|
||||
public class CustomErrorController implements ErrorController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(CustomErrorController.class);
|
||||
|
||||
/**
|
||||
* 处理错误请求
|
||||
* 根据 HTTP 状态码返回相应的错误信息
|
||||
*/
|
||||
@RequestMapping("/error")
|
||||
public R<?> handleError(HttpServletRequest request) {
|
||||
// 获取状态码
|
||||
int status = Optional.ofNullable(request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE))
|
||||
.map(Object::toString)
|
||||
.map(Integer::parseInt)
|
||||
.orElse(500);
|
||||
|
||||
// 获取请求 URI
|
||||
String requestUri = Optional.ofNullable(request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI))
|
||||
.map(Object::toString)
|
||||
.orElse("unknown");
|
||||
|
||||
// 获取异常信息
|
||||
String exceptionMessage = Optional.ofNullable(request.getAttribute(RequestDispatcher.ERROR_MESSAGE))
|
||||
.map(Object::toString)
|
||||
.orElse(null);
|
||||
|
||||
// 获取异常类型
|
||||
String exceptionType = Optional.ofNullable(request.getAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE))
|
||||
.map(Object::toString)
|
||||
.orElse(null);
|
||||
|
||||
// 获取异常对象
|
||||
Throwable throwable = Optional.ofNullable(request.getAttribute(RequestDispatcher.ERROR_EXCEPTION))
|
||||
.filter(Throwable.class::isInstance)
|
||||
.map(Throwable.class::cast)
|
||||
.orElse(null);
|
||||
|
||||
// 记录错误日志
|
||||
if (status >= 500) {
|
||||
log.error("服务器错误 - 状态码: {}, 请求路径: {}, 异常类型: {}, 异常信息: {}",
|
||||
status, requestUri, exceptionType, exceptionMessage, throwable);
|
||||
} else if (status >= 400) {
|
||||
log.warn("客户端错误 - 状态码: {}, 请求路径: {}, 异常信息: {}",
|
||||
status, requestUri, exceptionMessage);
|
||||
}
|
||||
|
||||
// 根据状态码获取友好的错误消息
|
||||
String errorMessage = getErrorMessage(status, exceptionMessage);
|
||||
|
||||
return new R<>(status, errorMessage, false, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据状态码获取友好的错误消息
|
||||
*/
|
||||
private String getErrorMessage(int status, String originalMessage) {
|
||||
if (originalMessage != null && !originalMessage.isEmpty()) {
|
||||
return originalMessage;
|
||||
}
|
||||
|
||||
return switch (status) {
|
||||
case 400 -> "请求参数错误";
|
||||
case 401 -> "未授权,请先登录";
|
||||
case 403 -> "无权访问";
|
||||
case 404 -> "您访问的地址不存在";
|
||||
case 405 -> "请求方法不支持";
|
||||
case 500 -> "服务器内部错误";
|
||||
case 502 -> "网关错误";
|
||||
case 503 -> "服务暂时不可用";
|
||||
case 504 -> "网关超时";
|
||||
default -> "请求失败";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回错误路径
|
||||
* 实现 ErrorController 接口要求
|
||||
*/
|
||||
public String getErrorPath() {
|
||||
return "/error";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user