Compare commits

..
24 Commits
Author SHA1 Message Date
zkh 9f97cab38f feat(log): 扩展日志服务支持操作日志上下文
- 在 ConsoleLogService 中新增 log(LogContext) 方法实现
- 更新 ILogService 接口添加 LogContext 参数的日志方法并提供默认实现
- 在 LogConfig 中添加 LogAspect 切面的异常处理和日志上下文构建功能
- 新增 LogContext 记录类用于封装完整的操作日志信息
- 在 zkh-log 模块中添加 jakarta.servlet-api 依赖以获取请求信息
- 更新所有模块的版本号从 1.5.11 到 1.5.12
2026-04-16 12:19:51 +08:00
zkh 9e75d3b392 chore(version): 更新框架版本号
- 将 zkh-framework 父项目版本从 1.5.10 升级到 1.5.11
- 同步更新 zkh-common 模块依赖版本至 1.5.11
- 同步更新 zkh-data 模块依赖版本至 1.5.11
- 同步更新 zkh-file 模块依赖版本至 1.5.11
- 同步更新 zkh-log 模块依赖版本至 1.5.11
- 同步更新 zkh-web 模块依赖版本至 1.5.11
2026-02-24 11:25:41 +08:00
zkh 42228c63a5 refactor(error): 优化全局异常处理器配置
- 移除未使用的 HttpStatus 和 ResponseStatus 注解导入
- 简化 RestControllerAdvice 注解配置,移除不必要的 bean 名称参数
- 保持原有的全局异常处理功能不变
- 提高代码简洁性和可读性
2026-02-24 11:23:55 +08:00
zkh 411565f812 refactor(error): 优化自定义错误控制器实现
- 添加 SLF4J 日志记录功能
- 实现详细的错误信息提取和处理逻辑
- 增加对不同 HTTP 状态码的友好错误消息映射
- 添加错误日志记录,区分服务器错误和客户端错误
- 实现 getErrorPath 方法以符合 ErrorController 接口要求
- 添加详细的 JavaDoc 注释文档
2026-02-14 12:39:40 +08:00
zkh c0448ff6ab feat(web): 添加自定义错误控制器处理HTTP错误
- 实现了ErrorController接口来处理错误请求
- 添加了/error端点的错误处理逻辑
- 返回统一格式的错误响应对象R
- 根据请求状态码动态设置错误状态
- 提供了默认500错误码的异常处理机制
2026-02-14 12:37:02 +08:00
zkh 7b346802e0 feat(security): 更新安全配置并升级框架版本
- 将父项目版本从 1.5.7 升级到 1.5.8
- 在 WebSecurityConfig 中注入 SecurityProps 配置
- 修改 CustomAuthenticationEntryPoint 构造函数以接受 securityProps 参数
- 将硬编码的访问令牌持续时间替换为 securityProps 配置值
- 将认证失败响应状态码从 401 更改为 40
- 为 zkh-web 模块添加 spring-boot-starter-aop 依赖
2026-02-04 09:48:51 +08:00
zkh f152b1e655 feat(security): 配置安全过滤器链支持异步请求
- 添加 DispatcherType.ASYNC 类型匹配以允许异步请求通过
- 添加 Order 注解确保安全过滤器链正确排序
- 导入必要的 jakarta.servlet.DispatcherType 和 org.springframework.core.annotation.Order 依赖
2026-02-04 01:15:08 +08:00
zkh 01d29e6ec3 feat(security): 更新安全配置并升级框架版本
- 将父项目版本从 1.5.7 升级到 1.5.8
- 在 WebSecurityConfig 中注入 SecurityProps 配置
- 修改 CustomAuthenticationEntryPoint 构造函数以接受 securityProps 参数
- 将硬编码的访问令牌持续时间替换为 securityProps 配置值
- 将认证失败响应状态码从 401 更改为 40
- 为 zkh-web 模块添加 spring-boot-starter-aop 依赖
2026-01-21 12:04:00 +08:00
zkh 06b5258824 refactor(error): 移除全局异常处理器中的重复状态码注解
- 移除 MethodArgumentNotValidException 处理器的 @ResponseStatus 注解
- 移除 ConstraintViolationException 处理器的 @ResponseStatus 注解
- 统一通过返回结果对象控制响应状态码
- 简化异常处理逻辑,提高代码一致性
2026-01-17 12:25:28 +08:00
zkh 3d4bec6e96 feat(web): 添加全局异常处理器支持请求参数验证
- 新增 MethodArgumentNotValidException 处理器,用于处理 @RequestBody + @Valid 校验失败
- 新增 ConstraintViolationException 处理器,用于处理 @RequestParam/@PathVariable 校验失败
- 实现了统一的参数验证错误响应格式
- 添加了详细的字段错误信息提取和返回机制
- 集成了日志记录功能以跟踪验证失败情况
- 更新了项目版本从 1.5.6 到 1.5.7
2026-01-17 12:17:55 +08:00
zkh eb82090586 feat(common): 添加 R2dbcBaseEntity 基础实体类并升级版本依赖
- 在 zkh-common 模块中新增 R2dbcBaseEntity 基础实体类
- 集成 Spring Data JPA 和 R2DBC 相关注解支持
- 添加审计功能支持创建时间和更新时间自动管理
- 实现基础字段如 ID、创建者、更新者等属性定义
- 将父项目及所有子模块版本从 1.5.5 升级至 1.5.6
- 在 zkh-common 中添加 spring-data-relational 依赖支持
2026-01-12 10:11:50 +08:00
zkh 753a07f71d chore(release): 更新框架版本到 1.5.5
- 更新 zkh-common 模块父版本到 1.5.5
- 更新 zkh-data 模块父版本到 1.5.5
- 更新 zkh-file 模块父版本到 1.5.5
- 更新 zkh-log 模块父版本到 1.5.5
- 更新 zkh-web 模块父版本到 1.5.5
- 更新主 pom.xml 版本到 1.5.5
2025-12-31 12:14:56 +08:00
zkh 209769d024 feat(jackson): 添加 LocalDateTime 和 LocalDate 的 JSON 序列化支持
- 引入 LocalDateTimeSerializer 和 LocalDateTimeDeserializer 处理 LocalDateTime 类型
- 添加 LocalDateSerializer 和 LocalDateDeserializer 处理 LocalDate 类型
- 配置 Jackson 使用自定义的时间格式化器
- 扩展 ObjectMapper 配置以支持新的序列化器和反序列化器
- 实现时间类型的空值安全处理
- 统一时间格式为 "yyyy-MM-dd HH:mm:ss" 和 "yyyy-MM-dd"
2025-12-31 12:13:32 +08:00
zkh 88afa8b47e refactor(jackson): 优化Long类型序列化器和反序列化器的类型处理
- 为LongSerializer添加handledType方法重写,明确指定处理Long类型
- 为LongDeserializer添加handledType方法重写,明确指定处理Long类型
- 提升Jackson配置中Long类型处理的准确性和性能
2025-12-31 12:07:24 +08:00
zkh ab39c0f9b2 feat(base): 为 BaseEntity 添加序列化接口并配置 Jackson 时间和 Long 类型处理
- 为 BaseEntity 类实现 Serializable 接口以支持序列化
- 新增 JacksonConfig 配置类处理时间格式和 Long 类型精度问题
- 配置时区为 Asia/Shanghai 和中国地区格式
- 添加 Long 类型的序列化和反序列化器避免前端精度丢失
- 将项目版本从 1.5.3 升级到 1.5.4
2025-12-31 12:03:26 +08:00
zkh eeac5b430c fix(web): 修复全局异常处理器并更新版本号
- 添加 ValidationException 异常处理支持
- 扩展异常处理器以捕获 RuntimeException
- 为 BindException 添加文档注释
- 新增 ValidationException 专门处理方法
- 更新父项目及所有子模块版本从 1.5.2 到 1.5.3
2025-12-31 11:53:04 +08:00
zkh a0cb0cb6b7 chore(deps): 更新框架版本到1.5.2并改进安全配置日志
- 将zkh-framework父项目版本从1.5.1更新到1.5.2
- 同步更新所有子模块(zkh-common, zkh-data, zkh-file, zkh-log, zkh-web)的父版本
- 在WebSecurityConfig中改进认证失败日志记录,添加请求URI信息
2025-12-31 11:34:57 +08:00
zkh f679be8cee chore(deps): 更新框架版本到1.5.1
- 将zkh-framework父项目版本从1.5更新到1.5.1
-
2025-12-30 16:20:43 +08:00
zkh 9d7a061305 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	zkh-web/pom.xml
2025-12-30 16:18:55 +08:00
zkh 5014411874 fix(dependencies): 移除springdoc-openapi依赖的provided作用域
- 移除了springdoc-openapi-starter-webmvc-ui依赖的provided作用域配置
- 确保该依赖在运行时能够正确加载和使用
2025-12-30 16:18:23 +08:00
zkh 1fe871faa8 feat(file): 优化文件上传下载接口
- 添加 MediaType 导入以支持 multipart 表单数据
- 修改上传接口 consumes 属性为 MULTIPART_FORM_DATA_VALUE
- 使用 @RequestPart 注解明确指定文件参数
- 将下载接口路径参数改为查询参数 path
- 统一代码缩进风格为四个空格
2025-12-07 21:44:30 +08:00
zkh 1f3fea8277 feat(file): 添加文件上传接口参数名称
- 为 FileController 的 upload 方法 MultipartFile 参数添加 name 属性
- 在 zkh-common 模块中引入 jakarta.validation-api 和 jakarta.annotation-api 依赖
- 将 springdoc-openapi-common 依赖升级为 springdoc-openapi-starter-webmvc-ui 并指定版本
- 为多个模块的 maven-compiler-plugin 配置添加 -parameters 编译参数
- 移除 zkh-file 和 zkh-web 模块中重复或不必要的 springdoc-openapi 依赖声明
2025-12-07 18:19:29 +08:00
zkh 042ef9a81e feat(file): 实现文件上传下载功能
- 新增文件上传接口,支持MultipartFile格式文件上传至MinIO
- 新增文件下载接口,根据文件路径返回对应资源
- 集成MinIO客户端,实现文件存储与获取
- 添加文件信息服务,记录文件元数据
- 引入SpringDoc OpenAPI,为文件接口提供文档支持
- 配置Maven插件,生成源码包和JavaDoc包
- 升级项目版本至1.5,统一依赖管理
2025-12-06 11:54:40 +08:00
zkh fe2240e266 feat(log): 新增日志注解功能并集成到认证流程
- 新增 @Log 注解及其配置类 LogConfig
- 在 AuthController 的 refreshToken 方法上添加 @Log 注解
- 创建 zkh-log 模块并引入 spring-boot-starter-aop 依赖
- 在 zkh-web 模块中引入 zkh-log 依赖
- 调整 GlobalExceptionHandler 和 RedisConfig 类中的代码缩进格式
- 更新 pom.xml 文件,加入 zkh-log 模块并调整依赖版本声明位置
- 修改 SpringDocConfig、WebSecurityConfig 等配置类的 bean 名称以避免冲突
2025-12-01 16:31:45 +08:00
27 changed files with 1194 additions and 168 deletions
+13 -1
View File
@@ -6,7 +6,7 @@
<groupId>vip.jcfd</groupId> <groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId> <artifactId>zkh-framework</artifactId>
<version>1.4</version> <version>1.5.12</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>ZKH Framework</name> <name>ZKH Framework</name>
<description>A Java framework for ZKH applications</description> <description>A Java framework for ZKH applications</description>
@@ -38,6 +38,8 @@
<module>zkh-common</module> <module>zkh-common</module>
<module>zkh-web</module> <module>zkh-web</module>
<module>zkh-data</module> <module>zkh-data</module>
<module>zkh-log</module>
<module>zkh-file</module>
</modules> </modules>
<properties> <properties>
@@ -71,6 +73,16 @@
<artifactId>zkh-data</artifactId> <artifactId>zkh-data</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </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> <dependency>
<groupId>org.springdoc</groupId> <groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-common</artifactId> <artifactId>springdoc-openapi-common</artifactId>
+25 -2
View File
@@ -6,7 +6,7 @@
<parent> <parent>
<groupId>vip.jcfd</groupId> <groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId> <artifactId>zkh-framework</artifactId>
<version>1.4</version> <version>1.5.12</version>
</parent> </parent>
<artifactId>zkh-common</artifactId> <artifactId>zkh-common</artifactId>
@@ -14,6 +14,14 @@
<description>Common utilities and base classes for ZKH framework</description> <description>Common utilities and base classes for ZKH framework</description>
<dependencies> <dependencies>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<dependency> <dependency>
<groupId>jakarta.persistence</groupId> <groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId> <artifactId>jakarta.persistence-api</artifactId>
@@ -30,14 +38,29 @@
<groupId>org.springframework.data</groupId> <groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId> <artifactId>spring-data-jpa</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springdoc</groupId> <groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-common</artifactId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.14</version>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <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> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId> <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.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.io.Serializable;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@MappedSuperclass @MappedSuperclass
@EntityListeners(AuditingEntityListener.class) @EntityListeners(AuditingEntityListener.class)
public class BaseEntity { public class BaseEntity implements Serializable {
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
private Long id; private Long id;
@Column @Column
@CreatedDate @CreatedDate
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime; private LocalDateTime createTime;
@Column @Column
@LastModifiedDate @LastModifiedDate
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; private LocalDateTime updateTime;
@Column @Column
@CreatedBy @CreatedBy
private String createBy; private String createBy;
@Column @Column
@LastModifiedBy @LastModifiedBy
private String updateBy; private String updateBy;
public Long getId() { public Long getId() {
return id; return id;
} }
public void setId(Long id) { public void setId(Long id) {
this.id = id; this.id = id;
} }
public LocalDateTime getCreateTime() { public LocalDateTime getCreateTime() {
return createTime; return createTime;
} }
public void setCreateTime(LocalDateTime createTime) { public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime; this.createTime = createTime;
} }
public LocalDateTime getUpdateTime() { public LocalDateTime getUpdateTime() {
return updateTime; return updateTime;
} }
public void setUpdateTime(LocalDateTime updateTime) { public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime; this.updateTime = updateTime;
} }
public String getCreateBy() { public String getCreateBy() {
return createBy; return createBy;
} }
public void setCreateBy(String createBy) { public void setCreateBy(String createBy) {
this.createBy = createBy; this.createBy = createBy;
} }
public String getUpdateBy() { public String getUpdateBy() {
return updateBy; return updateBy;
} }
public void setUpdateBy(String updateBy) { public void setUpdateBy(String updateBy) {
this.updateBy = 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
View File
@@ -6,19 +6,13 @@
<parent> <parent>
<groupId>vip.jcfd</groupId> <groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId> <artifactId>zkh-framework</artifactId>
<version>1.4</version> <version>1.5.12</version>
</parent> </parent>
<artifactId>zkh-data</artifactId> <artifactId>zkh-data</artifactId>
<name>ZKH Data</name> <name>ZKH Data</name>
<description>Data layer components for ZKH framework</description> <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> <dependencies>
<dependency> <dependency>
<groupId>jakarta.persistence</groupId> <groupId>jakarta.persistence</groupId>
@@ -52,6 +46,9 @@
<version>1.18.42</version> <version>1.18.42</version>
</path> </path>
</annotationProcessorPaths> </annotationProcessorPaths>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
+90
View File
@@ -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("上传失败");
}
}
+71
View File
@@ -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
View File
@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>vip.jcfd</groupId> <groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId> <artifactId>zkh-framework</artifactId>
<version>1.4</version> <version>1.5.12</version>
</parent> </parent>
<artifactId>zkh-web</artifactId> <artifactId>zkh-web</artifactId>
@@ -19,6 +19,14 @@
<groupId>vip.jcfd</groupId> <groupId>vip.jcfd</groupId>
<artifactId>zkh-common</artifactId> <artifactId>zkh-common</artifactId>
</dependency> </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> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
@@ -39,12 +47,6 @@
<groupId>com.fasterxml.jackson.datatype</groupId> <groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId> <artifactId>jackson-datatype-jsr310</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.14</version>
<scope>provided</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -60,6 +62,9 @@
<artifactId>spring-boot-configuration-processor</artifactId> <artifactId>spring-boot-configuration-processor</artifactId>
</path> </path>
</annotationProcessorPaths> </annotationProcessorPaths>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
@@ -1,9 +1,13 @@
package vip.jcfd.web.config; package vip.jcfd.web.config;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.ValidationException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException; import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException; import org.springframework.web.servlet.resource.NoResourceFoundException;
@@ -16,32 +20,73 @@ import java.util.List;
@RestControllerAdvice @RestControllerAdvice
public class GlobalExceptionHandler { 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) @ExceptionHandler(value = {Exception.class, RuntimeException.class})
public R<String> handleException(Exception e) { public R<String> handleException(Throwable e) {
log.error("服务异常", e); log.error("服务异常", e);
return R.serverError("服务器繁忙,请稍候重试"); return R.serverError("服务器繁忙,请稍候重试");
} }
@ExceptionHandler(value = BizException.class) @ExceptionHandler(value = BizException.class)
public R<String> handleBizException(BizException e) { public R<String> handleBizException(BizException e) {
log.error("业务异常", e); log.error("业务异常", e);
return R.error(e.getMessage()); return R.error(e.getMessage());
} }
@ExceptionHandler(value = NoResourceFoundException.class) @ExceptionHandler(value = NoResourceFoundException.class)
public R<String> handleNotFoundException(NoResourceFoundException e) { public R<String> handleNotFoundException(NoResourceFoundException e) {
log.error("404异常", e); log.error("404异常", e);
return new R<>(404, "您访问的地址不存在", false, null); return new R<>(404, "您访问的地址不存在", false, null);
} }
@ExceptionHandler(value = BindException.class) /**
public R<String> handleBindException(BindException e) { * Handles bind exceptions; logs and returns formatted field errors
log.error("接口入参校验失败", e); */
@ExceptionHandler(value = BindException.class)
public R<String> handleBindException(BindException e) {
log.error("接口入参校验失败", e);
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors(); List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
return R.error(String.join("\n", fieldErrors.stream().map(FieldError::getDefaultMessage).toList())); 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.config.props.SecurityProps;
import vip.jcfd.web.redis.TokenRedisStorage; import vip.jcfd.web.redis.TokenRedisStorage;
@Configuration @Configuration("_redisConfiguration")
public class RedisConfig { public class RedisConfig {
private final SecurityProps securityProps; private final SecurityProps securityProps;
public RedisConfig(SecurityProps securityProps) { public RedisConfig(SecurityProps securityProps) {
this.securityProps = securityProps; this.securityProps = securityProps;
} }
@Bean @Bean
public TokenRedisStorage tokenRedisTemplate(RedisConnectionFactory factory, StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper) { public TokenRedisStorage tokenRedisTemplate(RedisConnectionFactory factory, StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper) {
TokenRedisStorage tokenRedisStorage = new TokenRedisStorage( TokenRedisStorage tokenRedisStorage = new TokenRedisStorage(
securityProps.getAccessTokenDuration(), securityProps.getAccessTokenDuration(),
securityProps.getRefreshTokenDuration(), securityProps.getRefreshTokenDuration(),
stringRedisTemplate, stringRedisTemplate,
objectMapper objectMapper
); );
tokenRedisStorage.setConnectionFactory(factory); tokenRedisStorage.setConnectionFactory(factory);
tokenRedisStorage.setValueSerializer(new JdkSerializationRedisSerializer()); tokenRedisStorage.setValueSerializer(new JdkSerializationRedisSerializer());
tokenRedisStorage.setKeySerializer(new StringRedisSerializer()); tokenRedisStorage.setKeySerializer(new StringRedisSerializer());
return tokenRedisStorage; return tokenRedisStorage;
} }
} }
@@ -10,63 +10,63 @@ import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@Configuration("_springDocConfig") @Configuration("_springDocConfiguration")
public class SpringDocConfig { public class SpringDocConfig {
@Bean @Bean
public OpenApiCustomizer openApiCustomizer() { public OpenApiCustomizer openApiCustomizer() {
return (openAPI) -> { return (openAPI) -> {
openAPI.path("/login", new PathItem() openAPI.path("/login", new PathItem()
.post(new Operation() .post(new Operation()
.summary("登录接口") .summary("登录接口")
.description("用于用户登录,返回token") .description("用于用户登录,返回token")
.addTagsItem("认证管理") .addTagsItem("认证管理")
.requestBody(new RequestBody() .requestBody(new RequestBody()
.description("帐号密码") .description("帐号密码")
.required(true) .required(true)
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>() .content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
.addProperty("username", new StringSchema().example("admin")) .addProperty("username", new StringSchema().example("admin"))
.addProperty("password", new StringSchema().example("123456")))))) .addProperty("password", new StringSchema().example("123456"))))))
.responses(new ApiResponses() .responses(new ApiResponses()
.addApiResponse("成功", new ApiResponse() .addApiResponse("成功", new ApiResponse()
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>() .content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
.addProperty("data", new JsonSchema() .addProperty("data", new JsonSchema()
.addProperty("accessToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440000")) .addProperty("accessToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440000"))
.addProperty("refreshToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440001")) .addProperty("refreshToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440001"))
.addProperty("tokenType", new StringSchema().example("Bearer")) .addProperty("tokenType", new StringSchema().example("Bearer"))
.addProperty("expiresIn", new NumberSchema().example(1800)) .addProperty("expiresIn", new NumberSchema().example(1800))
.addProperty("username", new StringSchema().example("admin")) .addProperty("username", new StringSchema().example("admin"))
) )
.addProperty("success", new BooleanSchema().example(true)) .addProperty("success", new BooleanSchema().example(true))
.addProperty("code", new IntegerSchema().example(200)) .addProperty("code", new IntegerSchema().example(200))
.addProperty("message", new StringSchema().example("登录成功")) .addProperty("message", new StringSchema().example("登录成功"))
)))) ))))
.addApiResponse("失败", new ApiResponse() .addApiResponse("失败", new ApiResponse()
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>() .content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
.addProperty("data", new StringSchema().example(null)) .addProperty("data", new StringSchema().example(null))
.addProperty("success", new BooleanSchema().example(false)) .addProperty("success", new BooleanSchema().example(false))
.addProperty("code", new IntegerSchema().example(401)) .addProperty("code", new IntegerSchema().example(401))
.addProperty("message", new StringSchema().example("用户名或密码错误")) .addProperty("message", new StringSchema().example("用户名或密码错误"))
)))) ))))
))); )));
openAPI.path("/logout", new PathItem() openAPI.path("/logout", new PathItem()
.post(new Operation() .post(new Operation()
.summary("登出接口") .summary("登出接口")
.description("用于用户登出") .description("用于用户登出")
.addTagsItem("认证管理") .addTagsItem("认证管理")
.responses(new ApiResponses() .responses(new ApiResponses()
.addApiResponse("成功", new ApiResponse() .addApiResponse("成功", new ApiResponse()
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>() .content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
.addProperty("data", new StringSchema().example(null)) .addProperty("data", new StringSchema().example(null))
.addProperty("success", new BooleanSchema().example(true)) .addProperty("success", new BooleanSchema().example(true))
.addProperty("code", new IntegerSchema().example(200)) .addProperty("code", new IntegerSchema().example(200))
.addProperty("message", new StringSchema().example("登出成功")) .addProperty("message", new StringSchema().example("登出成功"))
) )
) )
) )
) )
) )
)); ));
}; };
} }
} }
@@ -1,6 +1,7 @@
package vip.jcfd.web.config; package vip.jcfd.web.config;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; 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.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.domain.AuditorAware; import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -52,7 +54,7 @@ import java.io.IOException;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@Configuration @Configuration("_webSecurityConfiguration")
@EnableWebSecurity @EnableWebSecurity
@ConfigurationPropertiesScan(basePackageClasses = {SecurityProps.class}) @ConfigurationPropertiesScan(basePackageClasses = {SecurityProps.class})
@EnableJpaAuditing @EnableJpaAuditing
@@ -110,12 +112,14 @@ public class WebSecurityConfig {
} }
@Bean @Bean
@Order
public SecurityFilterChain security(HttpSecurity http, TokenFilter tokenFilter, AuthenticationManager authenticationManager) throws Exception { public SecurityFilterChain security(HttpSecurity http, TokenFilter tokenFilter, AuthenticationManager authenticationManager) throws Exception {
http.authorizeHttpRequests(config -> { http.authorizeHttpRequests(config -> {
config.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll();
config.requestMatchers(securityProps.getIgnoreUrls()).permitAll(); config.requestMatchers(securityProps.getIgnoreUrls()).permitAll();
config.anyRequest().authenticated(); config.anyRequest().authenticated();
}); });
CustomAuthenticationEntryPoint authenticationEntryPoint = new CustomAuthenticationEntryPoint(objectMapper, tokenRedisStorage); CustomAuthenticationEntryPoint authenticationEntryPoint = new CustomAuthenticationEntryPoint(objectMapper, tokenRedisStorage, securityProps);
http.formLogin(config -> { http.formLogin(config -> {
config.loginProcessingUrl("/login"); config.loginProcessingUrl("/login");
}); });
@@ -140,10 +144,11 @@ public class WebSecurityConfig {
private record CustomAuthenticationEntryPoint( private record CustomAuthenticationEntryPoint(
ObjectMapper objectMapper, ObjectMapper objectMapper,
TokenRedisStorage tokenRedisStorage) implements AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler { TokenRedisStorage tokenRedisStorage,
SecurityProps securityProps) implements AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler {
@Override @Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { 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); R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "未登录", false, null);
response.setContentType("application/json;charset=UTF-8"); response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), data); objectMapper.writeValue(response.getWriter(), data);
@@ -152,7 +157,7 @@ public class WebSecurityConfig {
@Override @Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.warn("登录失败", exception); 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"); response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), data); objectMapper.writeValue(response.getWriter(), data);
} }
@@ -177,7 +182,7 @@ public class WebSecurityConfig {
accessToken, accessToken,
refreshToken, refreshToken,
"Bearer", "Bearer",
1800, // 30分钟,秒数 securityProps.getDuration().getSeconds(), // 30分钟,秒数
authentication.getName() authentication.getName()
); );
@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.*;
import vip.jcfd.common.core.R; import vip.jcfd.common.core.R;
import vip.jcfd.common.dto.TokenRefreshRequest; import vip.jcfd.common.dto.TokenRefreshRequest;
import vip.jcfd.common.dto.TokenRefreshResponse; import vip.jcfd.common.dto.TokenRefreshResponse;
import vip.jcfd.log.annotation.Log;
import vip.jcfd.web.auth.RefreshTokenAuthenticationToken; import vip.jcfd.web.auth.RefreshTokenAuthenticationToken;
import vip.jcfd.web.redis.TokenRedisStorage; import vip.jcfd.web.redis.TokenRedisStorage;
@@ -37,6 +38,7 @@ public class AuthController {
@PostMapping("/refresh-token") @PostMapping("/refresh-token")
@Operation(summary = "刷新Token", description = "使用Refresh Token获取新的Access Token和Refresh Token") @Operation(summary = "刷新Token", description = "使用Refresh Token获取新的Access Token和Refresh Token")
@Log("刷新了token")
public R<TokenRefreshResponse> refreshToken( public R<TokenRefreshResponse> refreshToken(
@Valid @RequestBody TokenRefreshRequest request) { @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";
}
}